diff options
| -rw-r--r-- | README.org | 16 | ||||
| -rw-r--r-- | pearl.el | 126 | ||||
| -rw-r--r-- | tests/test-pearl-keymap.el | 94 | ||||
| -rw-r--r-- | tests/test-pearl-menu.el | 3 |
4 files changed, 206 insertions, 33 deletions
@@ -144,13 +144,25 @@ Pearl writes one active Org file. Running a different query or view replaces tha *** Command menu -=M-x pearl-menu= opens a transient dispatcher with commands grouped by fetch, view, issue-at-point, creation, Org sync, and setup. Bind that command if you use Pearl regularly: +=M-x pearl-menu= opens a transient dispatcher. Ticket actions are grouped as save, edit, new, and delete; workspace actions as fetch, view, and setup. Bind that command if you use Pearl regularly: #+begin_src emacs-lisp (global-set-key (kbd "C-c L") #'pearl-menu) #+end_src -Every command below is also available directly through =M-x=. +*** Prefix keymap + +For muscle memory, Pearl also defines an opt-in prefix keymap, =pearl-prefix-map=, organized as save / edit / new / delete. It is not bound at load -- a global multi-key prefix isn't reliably free across terminals and GUIs, so you bind it to a prefix that suits your setup. A suggested binding: + +#+begin_src emacs-lisp + (global-set-key (kbd "C-; L") pearl-prefix-map) + ;; or, with use-package: + ;; :bind-keymap ("C-; L" . pearl-prefix-map) +#+end_src + +With that prefix, =C-; L s s= saves the ticket at point, =C-; L s a= saves every ticket in the file, =C-; L e p= edits its priority, =C-; L n t= creates a ticket, and =C-; L m= opens the full transient. If you use =which-key=, each step shows a labeled menu. + +Every command is also available directly through =M-x=. *** Fetching and refreshing @@ -4140,39 +4140,105 @@ reported (refresh to reconcile)." ;;;###autoload (autoload 'pearl-menu "pearl" nil t) (transient-define-prefix pearl-menu () "Dispatch menu for pearl commands." - ["Linear" + ;; Grouped to mirror the `pearl-prefix-map' categories (save / edit / new / + ;; delete); fetch / view / setup are the operational groups with no keymap + ;; equivalent. The org-sync commands (enable / disable / push-file) are + ;; deliberately not surfaced -- they are plumbing (see the todo). + ["Ticket" + ["Save" + ("e" "save ticket" pearl-save-issue) + ("E" "save all" pearl-save-all)] + ["Edit" + ("D" "edit description" pearl-compose-current-description) + ("M" "edit comment" pearl-edit-current-comment) + ("P" "edit priority" pearl-set-priority) + ("s" "edit state" pearl-set-state) + ("a" "edit assignee" pearl-set-assignee) + ("L" "edit labels" pearl-set-labels)] + ["New" + ("n" "new ticket" pearl-new-issue) + ("c" "new comment" pearl-add-comment)] + ["Delete" + ("k" "delete ticket" pearl-delete-current-issue)]] + ["Workspace" ["Fetch" - ("l" "My open issues" pearl-list-issues) - ("p" "By project" pearl-list-issues-by-project) - ("f" "Build a filter" pearl-list-issues-filtered) - ("v" "Custom view" pearl-run-view) - ("Q" "Saved query" pearl-run-saved-query)] + ("l" "my open issues" pearl-list-issues) + ("p" "by project" pearl-list-issues-by-project) + ("f" "build a filter" pearl-list-issues-filtered) + ("v" "custom view" pearl-run-view) + ("Q" "saved query" pearl-run-saved-query)] ["View" - ("g" "Refresh view" pearl-refresh-current-view) - ("r" "Refresh issue" pearl-refresh-current-issue) - ("b" "Open view in Linear" pearl-open-current-view-in-linear)] - ["Issue at point" - ("e" "Edit desc -> push" pearl-sync-current-issue) - ("D" "Compose desc -> push" pearl-compose-current-description) - ("t" "Edit title -> push" pearl-sync-current-issue-title) - ("s" "Set state" pearl-set-state) - ("a" "Set assignee" pearl-set-assignee) - ("P" "Set priority" pearl-set-priority) - ("L" "Set labels" pearl-set-labels) - ("c" "Add comment" pearl-add-comment) - ("M" "Edit comment" pearl-edit-current-comment) - ("k" "Delete issue" pearl-delete-current-issue) - ("o" "Open in browser" pearl-open-current-issue)] - ["Create & org-sync" - ("n" "New issue" pearl-new-issue) - ("E" "Enable org-sync" pearl-enable-org-sync) - ("X" "Disable org-sync" pearl-disable-org-sync) - ("u" "Push file -> Linear" pearl-sync-org-to-linear)] + ("g" "refresh view" pearl-refresh-current-view) + ("r" "refresh issue" pearl-refresh-current-issue) + ("b" "open view in Linear" pearl-open-current-view-in-linear) + ("o" "open ticket in browser" pearl-open-current-issue)] ["Setup" - ("T" "Test connection" pearl-test-connection) - ("C" "Check setup" pearl-check-setup) - ("!" "Toggle debug" pearl-toggle-debug) - ("x" "Clear cache" pearl-clear-cache)]]) + ("T" "test connection" pearl-test-connection) + ("C" "check setup" pearl-check-setup) + ("!" "toggle debug" pearl-toggle-debug) + ("x" "clear cache" pearl-clear-cache)]]) + +;;; Prefix Keymap +;; +;; An opt-in prefix keymap organizing the commands under save / edit / new / +;; delete sub-maps. It is defined but NOT bound at load -- a global prefix +;; isn't reliably free across terminals and GUIs, and auto-installing one is a +;; compatibility call the user should own. Bind it to a free prefix, e.g. +;; +;; (global-set-key (kbd "C-; L") pearl-prefix-map) +;; ;; or, with use-package: :bind-keymap ("C-; L" . pearl-prefix-map) +;; +;; Each binding is a (LABEL . COMMAND) menu item, so `which-key' shows the +;; verb-matched label (e.g. "edit priority") without pearl depending on +;; which-key. Lookups still resolve straight to the command. +;; +;; The labels follow the edit/new verbs even though some commands keep older +;; names (`pearl-compose-current-description', `pearl-set-state', and +;; `pearl-add-comment'); aligning the command names is a separate task. The +;; `d c' delete-comment slot is intentionally empty until that command exists. + +(defvar pearl-save-map + (let ((map (make-sparse-keymap))) + (define-key map "s" (cons "save ticket" #'pearl-save-issue)) + (define-key map "a" (cons "save all" #'pearl-save-all)) + map) + "Pearl save commands; a sub-keymap of `pearl-prefix-map'.") + +(defvar pearl-edit-map + (let ((map (make-sparse-keymap))) + (define-key map "d" (cons "edit description" #'pearl-compose-current-description)) + (define-key map "c" (cons "edit comment" #'pearl-edit-current-comment)) + (define-key map "p" (cons "edit priority" #'pearl-set-priority)) + (define-key map "s" (cons "edit state" #'pearl-set-state)) + (define-key map "a" (cons "edit assignee" #'pearl-set-assignee)) + (define-key map "l" (cons "edit labels" #'pearl-set-labels)) + map) + "Pearl edit commands; a sub-keymap of `pearl-prefix-map'.") + +(defvar pearl-new-map + (let ((map (make-sparse-keymap))) + (define-key map "t" (cons "new ticket" #'pearl-new-issue)) + (define-key map "c" (cons "new comment" #'pearl-add-comment)) + map) + "Pearl new-object commands; a sub-keymap of `pearl-prefix-map'.") + +(defvar pearl-delete-map + (let ((map (make-sparse-keymap))) + (define-key map "t" (cons "delete ticket" #'pearl-delete-current-issue)) + map) + "Pearl delete commands; a sub-keymap of `pearl-prefix-map'.") + +(defvar pearl-prefix-map + (let ((map (make-sparse-keymap))) + (define-key map "s" (cons "+save" pearl-save-map)) + (define-key map "e" (cons "+edit" pearl-edit-map)) + (define-key map "n" (cons "+new" pearl-new-map)) + (define-key map "d" (cons "+delete" pearl-delete-map)) + (define-key map "m" (cons "menu" #'pearl-menu)) + map) + "Opt-in Pearl command prefix keymap. +Not bound at load. Bind it to a free prefix, such as the suggested +\\=`C-; L\\=', in your own configuration (see the Commentary above).") (provide 'pearl) ;;; pearl.el ends here
\ No newline at end of file diff --git a/tests/test-pearl-keymap.el b/tests/test-pearl-keymap.el new file mode 100644 index 0000000..97d8440 --- /dev/null +++ b/tests/test-pearl-keymap.el @@ -0,0 +1,94 @@ +;;; test-pearl-keymap.el --- Tests for the opt-in prefix keymap -*- lexical-binding: t; -*- + +;; Copyright (C) 2026 Craig Jennings + +;; Author: Craig Jennings <c@cjennings.net> + +;; This program is free software: you can redistribute it and/or modify +;; it under the terms of the GNU General Public License as published by +;; the Free Software Foundation, either version 3 of the License, or +;; (at your option) any later version. + +;; This program is distributed in the hope that it will be useful, +;; but WITHOUT ANY WARRANTY; without even the implied warranty of +;; MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +;; GNU General Public License for more details. + +;; You should have received a copy of the GNU General Public License +;; along with this program. If not, see <http://www.gnu.org/licenses/>. + +;;; Commentary: + +;; Tests for `pearl-prefix-map' (see docs/ticket-save-model-spec.org, the +;; keybinding section): the opt-in prefix keymap that organizes the commands +;; under save / edit / new / delete sub-maps with which-key labels. The map is +;; defined but never bound at load -- the user binds it to a free prefix. + +;;; Code: + +(require 'test-bootstrap (expand-file-name "test-bootstrap.el")) + +(defun test-pearl-keymap--label-of (map command) + "Return the menu-item label bound to COMMAND in MAP, or nil. +Walks MAP looking for a labeled binding (\"label\" . COMMAND)." + (let (found) + (map-keymap + (lambda (_ev binding) + (when (and (consp binding) (stringp (car binding)) + (eq (cdr binding) command)) + (setq found (car binding)))) + map) + found)) + +(ert-deftest test-pearl-prefix-map-is-a-keymap () + "`pearl-prefix-map' is defined as a keymap." + (should (keymapp pearl-prefix-map))) + +(ert-deftest test-pearl-prefix-map-resolves-leaf-commands () + "Each chord resolves to its command (existing names, not yet renamed)." + (should (eq 'pearl-save-issue (lookup-key pearl-prefix-map (kbd "s s")))) + (should (eq 'pearl-save-all (lookup-key pearl-prefix-map (kbd "s a")))) + (should (eq 'pearl-compose-current-description (lookup-key pearl-prefix-map (kbd "e d")))) + (should (eq 'pearl-edit-current-comment (lookup-key pearl-prefix-map (kbd "e c")))) + (should (eq 'pearl-set-priority (lookup-key pearl-prefix-map (kbd "e p")))) + (should (eq 'pearl-set-state (lookup-key pearl-prefix-map (kbd "e s")))) + (should (eq 'pearl-set-assignee (lookup-key pearl-prefix-map (kbd "e a")))) + (should (eq 'pearl-set-labels (lookup-key pearl-prefix-map (kbd "e l")))) + (should (eq 'pearl-new-issue (lookup-key pearl-prefix-map (kbd "n t")))) + (should (eq 'pearl-add-comment (lookup-key pearl-prefix-map (kbd "n c")))) + (should (eq 'pearl-delete-current-issue (lookup-key pearl-prefix-map (kbd "d t")))) + (should (eq 'pearl-menu (lookup-key pearl-prefix-map (kbd "m"))))) + +(ert-deftest test-pearl-prefix-map-sub-prefixes-are-keymaps () + "The category keys are sub-prefixes, not commands." + (should (keymapp (lookup-key pearl-prefix-map (kbd "s")))) + (should (keymapp (lookup-key pearl-prefix-map (kbd "e")))) + (should (keymapp (lookup-key pearl-prefix-map (kbd "n")))) + (should (keymapp (lookup-key pearl-prefix-map (kbd "d"))))) + +(ert-deftest test-pearl-prefix-map-no-delete-comment-yet () + "Delete-comment is not bound -- the command is a separate task." + (should-not (commandp (lookup-key pearl-prefix-map (kbd "d c"))))) + +(ert-deftest test-pearl-prefix-map-not-bound-at-load () + "Loading pearl does not bind the commands to any active key (opt-in)." + (should-not (where-is-internal 'pearl-save-issue)) + (should-not (where-is-internal 'pearl-save-all))) + +(ert-deftest test-pearl-prefix-map-which-key-labels () + "Leaf bindings carry the edit/new verb labels; sub-prefixes carry +labels." + (should (string= "edit priority" + (test-pearl-keymap--label-of pearl-edit-map 'pearl-set-priority))) + (should (string= "edit description" + (test-pearl-keymap--label-of pearl-edit-map 'pearl-compose-current-description))) + (should (string= "new ticket" + (test-pearl-keymap--label-of pearl-new-map 'pearl-new-issue))) + (should (string= "new comment" + (test-pearl-keymap--label-of pearl-new-map 'pearl-add-comment))) + (should (string= "save ticket" + (test-pearl-keymap--label-of pearl-save-map 'pearl-save-issue))) + ;; sub-prefix label on the base map + (should (string= "+edit" (test-pearl-keymap--label-of pearl-prefix-map pearl-edit-map)))) + +(provide 'test-pearl-keymap) +;;; test-pearl-keymap.el ends here diff --git a/tests/test-pearl-menu.el b/tests/test-pearl-menu.el index 1362b2e..8af4bb8 100644 --- a/tests/test-pearl-menu.el +++ b/tests/test-pearl-menu.el @@ -62,7 +62,8 @@ menu entry that still points at it fails here." (dolist (expected '(pearl-list-issues pearl-run-view pearl-run-saved-query - pearl-sync-current-issue + pearl-save-issue + pearl-save-all pearl-set-state pearl-add-comment pearl-new-issue |
