diff options
| author | Craig Jennings <c@cjennings.net> | 2026-07-18 15:34:12 -0500 |
|---|---|---|
| committer | Craig Jennings <c@cjennings.net> | 2026-07-18 15:34:12 -0500 |
| commit | bf3857602f3bbc9a485f27ba8b4f461107601367 (patch) | |
| tree | 8b6d950a57ea1c652626fc750f5d8e7f5c3329b8 | |
| parent | 28c02e9e7e1725f98393aa6cbc717939ec71775b (diff) | |
| download | dotemacs-bf3857602f3bbc9a485f27ba8b4f461107601367.tar.gz dotemacs-bf3857602f3bbc9a485f27ba8b4f461107601367.zip | |
feat(music): pin the playlist cursor to the number gutter
Vertical motion over thumbnail rows and the stretch-space that right-aligns the metadata drifts point to arbitrary visual columns, usually line end. The rows aren't editable text, so the cursor now lives at the row start (the number column). A buffer-local post-command snap sends every landing back there. Kill, shift (C-up/C-down), and play already act on the whole row, so all track operations happen from the number. An active isearch keeps point until it ends, then the snap lands on the match's row.
| -rw-r--r-- | modules/music-config.el | 16 | ||||
| -rw-r--r-- | tests/test-music-config--pin-point.el | 78 |
2 files changed, 93 insertions, 1 deletions
diff --git a/modules/music-config.el b/modules/music-config.el index 9174bea3..02df2467 100644 --- a/modules/music-config.el +++ b/modules/music-config.el @@ -354,6 +354,18 @@ so the row reads left-to-right from its number." (recenter (max 1 (/ (window-body-height) 3))))) (set-window-start win pos)))) +(defun cj/music--pin-point-to-bol () + "Keep the playlist cursor in the number gutter (column 0). +The rows are rendered track lines, not editable text: the cursor's home is +the number, and operations on a track (kill, shift, play) act on its row +wherever point sits. Vertical motion over thumbnails and the stretch-space +that right-aligns the metadata drifts point to arbitrary visual columns +(usually line end), so this runs on the buffer-local `post-command-hook' +and snaps every landing back to the row start. An active isearch owns +point until it ends; the snap lands when the search exits." + (unless (or (bolp) (bound-and-true-p isearch-mode)) + (beginning-of-line))) + (defvar-local cj/music--renumber-timer nil "Pending idle timer for the playlist row renumber, or nil.") @@ -407,7 +419,9 @@ inserts or kills into one renumber pass." (add-hook 'after-change-functions #'cj/music--schedule-renumber nil t) ;; The highlighted row stays findable even when the cursor sits on ;; album art (pairs with the row-number prefixes). - (hl-line-mode 1)) + (hl-line-mode 1) + ;; Gutter cursor: point lives at the row start (the number column). + (add-hook 'post-command-hook #'cj/music--pin-point-to-bol nil t)) (cj/music--renumber-rows buffer) ;; Set this as the current EMMS playlist buffer (setq emms-playlist-buffer buffer) diff --git a/tests/test-music-config--pin-point.el b/tests/test-music-config--pin-point.el new file mode 100644 index 00000000..2d6fa916 --- /dev/null +++ b/tests/test-music-config--pin-point.el @@ -0,0 +1,78 @@ +;;; test-music-config--pin-point.el --- Tests for the playlist gutter cursor -*- coding: utf-8; lexical-binding: t; -*- +;; +;; Author: Craig Jennings <c@cjennings.net> +;; +;;; Commentary: +;; The playlist cursor lives pinned at the start of the row (the number +;; gutter). The rows are rendered track lines, not editable text, and +;; vertical motion over thumbnails and stretch-space drifts point to +;; arbitrary visual columns (usually line end). A buffer-local +;; post-command snap enforces the model for every motion command. The one +;; exception is an active isearch, which owns point placement until it ends. + +;;; Code: + +(require 'ert) +(require 'cl-lib) + +(defvar cj/custom-keymap (make-sparse-keymap) + "Stub keymap for testing.") + +(require 'music-config) + +;;; Normal Cases + +(ert-deftest test-music-pin-point-snaps-mid-line-to-bol () + "Normal: point mid-row snaps back to the beginning of the line." + (with-temp-buffer + (insert "track one\ntrack two\n") + (goto-char (point-min)) + (forward-char 5) + (cj/music--pin-point-to-bol) + (should (bolp)) + (should (= (point) (point-min))))) + +(ert-deftest test-music-pin-point-noop-at-bol () + "Normal: point already at the row start stays put." + (with-temp-buffer + (insert "track one\ntrack two\n") + (goto-char (point-min)) + (forward-line 1) + (let ((before (point))) + (cj/music--pin-point-to-bol) + (should (= (point) before))))) + +;;; Boundary Cases + +(ert-deftest test-music-pin-point-skips-during-isearch () + "Boundary: an active isearch owns point; the pin defers until it ends." + (with-temp-buffer + (insert "track one\ntrack two\n") + (goto-char (point-min)) + (forward-char 5) + (let ((isearch-mode t)) + (cj/music--pin-point-to-bol)) + (should-not (bolp)))) + +(ert-deftest test-music-pin-point-empty-buffer-no-error () + "Boundary: an empty buffer is a no-op, no error." + (with-temp-buffer + (should-not (cj/music--pin-point-to-bol)) + (should (bolp)))) + +;;; Hook wiring + +(ert-deftest test-music-pin-point-ensure-wires-post-command-hook () + "Normal: the playlist buffer gets the pin on its buffer-local +post-command-hook." + (let (created) + (cl-letf (((symbol-function 'emms-playlist-mode) #'ignore)) + (unwind-protect + (progn + (setq created (cj/music--ensure-playlist-buffer)) + (with-current-buffer created + (should (member #'cj/music--pin-point-to-bol post-command-hook)))) + (when (buffer-live-p created) (kill-buffer created)))))) + +(provide 'test-music-config--pin-point) +;;; test-music-config--pin-point.el ends here |
