aboutsummaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorCraig Jennings <c@cjennings.net>2026-06-02 12:59:54 -0500
committerCraig Jennings <c@cjennings.net>2026-06-02 12:59:54 -0500
commit1a34cb26cbaec7af31a0d93a8f1c563eb5e45be5 (patch)
tree8cb0c7fb7fbeb0c3ddd5bf721565fdfec7d09c7a
parent42f3a75651ed8d2f8e601c654e482dc85363bb83 (diff)
downloadpearl-1a34cb26cbaec7af31a0d93a8f1c563eb5e45be5.tar.gz
pearl-1a34cb26cbaec7af31a0d93a8f1c563eb5e45be5.zip
feat(views): set-default-view command
Phase 2 of the default-view spec: the command that sets the default that phase 1 reads. It's the v D slot the keybinding work reserved (the binding itself lands in phase 3). From a buffer that's showing a local view, pearl-set-default-view offers that view as the default. Otherwise it prompts over the local-view names with a my-open-issues sentinel on top that clears the default. Quitting leaves it unchanged. pearl--persist-default-view writes through customize-save-variable so the choice survives a restart. In accounts mode it updates the active account's :default-view and preserves the account's other plist keys, the same metadata-preserving rewrite local-view edits use. In legacy mode it writes the global pearl-default-view. Eight tests: persist scope-targeting and key preservation in both modes, plus the offer, prompt, clear, and cancel paths. Suite 801 to 809, compile and lint clean.
-rw-r--r--pearl.el62
-rw-r--r--tests/test-pearl-default-view.el117
2 files changed, 179 insertions, 0 deletions
diff --git a/pearl.el b/pearl.el
index a352fd1..7705e0b 100644
--- a/pearl.el
+++ b/pearl.el
@@ -6182,6 +6182,68 @@ never blocks the hot key."
default)
(pearl-list-issues)))))
+(defconst pearl--default-view-clear "[ My open issues ]"
+ "Sentinel shown atop the `pearl-set-default-view' prompt.
+Choosing it clears the default back to nil -- the built-in my-open-issues
+fetch -- rather than naming a local view.")
+
+(defun pearl--persist-default-view (value)
+ "Persist VALUE (a local-view name string, or nil) as the default view.
+In accounts mode, write VALUE to the active account's `:default-view' in
+`pearl-accounts', preserving the account's other plist keys; in legacy
+single-account mode, write the global `pearl-default-view'. Both persist
+durably through `customize-save-variable' so the choice survives a restart."
+ (let ((ctx (pearl--current-account-context)))
+ (if ctx
+ (let ((name (plist-get ctx :name)))
+ (setq pearl-accounts
+ (mapcar (lambda (entry)
+ (if (string-equal (car entry) name)
+ (cons (car entry)
+ (plist-put (copy-sequence (cdr entry))
+ :default-view value))
+ entry))
+ pearl-accounts))
+ (customize-save-variable 'pearl-accounts pearl-accounts))
+ (setq pearl-default-view value)
+ (customize-save-variable 'pearl-default-view value))))
+
+;;;###autoload
+(defun pearl-set-default-view ()
+ "Set and persist the default view `pearl-open-default-view' opens.
+When the current buffer shows a local view, offers that view as the default.
+Otherwise prompts over `pearl-local-views' names with a clear-to-my-open-issues
+sentinel at the top; choosing it clears the default, and quitting leaves it
+unchanged. Persists per scope (the active account's `:default-view' in
+accounts mode, `pearl-default-view' in legacy) via `customize-save-variable'.
+Bound to \\='C-; L v D\\='."
+ (interactive)
+ (let* ((source (pearl--read-active-source))
+ (buf-view (and source
+ (eq (plist-get source :type) 'filter)
+ (let ((n (plist-get source :name)))
+ (and (stringp n) (assoc n pearl-local-views) n))))
+ (value
+ (if (and buf-view
+ (pearl--read-yes-no
+ (format "Set '%s' as the default view? " buf-view)))
+ buf-view
+ (let ((choice (completing-read
+ "Default view: "
+ (pearl--completion-table-keep-order
+ (pearl--with-sentinel pearl--default-view-clear
+ (mapcar #'car pearl-local-views)))
+ nil t)))
+ (cond
+ ((or (null choice) (string-empty-p choice)) :unchanged)
+ ((string= choice pearl--default-view-clear) nil)
+ (t choice))))))
+ (if (eq value :unchanged)
+ (message "Default view unchanged")
+ (pearl--persist-default-view value)
+ (message "Default view set to %s"
+ (if value (format "'%s'" value) "my open issues")))))
+
;;;###autoload
(defun pearl-list-issues-by-project ()
"List every open Linear issue in a chosen project (all assignees).
diff --git a/tests/test-pearl-default-view.el b/tests/test-pearl-default-view.el
index 2794972..37d638a 100644
--- a/tests/test-pearl-default-view.el
+++ b/tests/test-pearl-default-view.el
@@ -130,5 +130,122 @@ It dispatches to `pearl-list-issues' and never calls `pearl-run-local-view'."
(should (equal calls '((list))))
(should messaged))))
+;;; pearl--persist-default-view -- scope targeting + metadata preservation
+
+(ert-deftest test-pearl-persist-default-view-legacy ()
+ "Legacy mode: persisting writes `pearl-default-view' and saves it."
+ (let ((pearl-accounts nil)
+ (pearl-default-view nil)
+ (saved nil))
+ (cl-letf (((symbol-function 'customize-save-variable)
+ (lambda (sym val) (setq saved (cons sym val)))))
+ (pearl--persist-default-view "My active work")
+ (should (string-equal "My active work" pearl-default-view))
+ (should (equal saved '(pearl-default-view . "My active work"))))))
+
+(ert-deftest test-pearl-persist-default-view-legacy-clear ()
+ "Legacy mode: persisting nil clears the default back to my open issues."
+ (let ((pearl-accounts nil)
+ (pearl-default-view "Something")
+ (saved nil))
+ (cl-letf (((symbol-function 'customize-save-variable)
+ (lambda (sym val) (setq saved (cons sym val)))))
+ (pearl--persist-default-view nil)
+ (should-not pearl-default-view)
+ (should (equal saved '(pearl-default-view))))))
+
+(ert-deftest test-pearl-persist-default-view-accounts-targets-active ()
+ "Accounts mode: persisting updates only the active account's :default-view."
+ (let ((pearl-accounts
+ '(("work" :api-key-source (:literal "kw") :org-file "/tmp/w.org"
+ :default-team-id "team_w")
+ ("home" :api-key-source (:literal "kh") :org-file "/tmp/h.org")))
+ (pearl-default-account nil)
+ (pearl-active-account "work")
+ (saved nil))
+ (cl-letf (((symbol-function 'customize-save-variable)
+ (lambda (sym val) (setq saved (cons sym val)))))
+ (pearl--persist-default-view "Work board")
+ ;; active account got the key
+ (should (string-equal "Work board"
+ (plist-get (cdr (assoc "work" pearl-accounts)) :default-view)))
+ ;; the other account is untouched
+ (should-not (plist-get (cdr (assoc "home" pearl-accounts)) :default-view))
+ ;; pearl-accounts (not the legacy global) was the saved variable
+ (should (eq 'pearl-accounts (car saved))))))
+
+(ert-deftest test-pearl-persist-default-view-accounts-preserves-keys ()
+ "Accounts mode: the rewrite preserves the active account's other plist keys."
+ (let ((pearl-accounts
+ '(("work" :api-key-source (:literal "kw") :org-file "/tmp/w.org"
+ :default-team-id "team_w" :url "https://example/api")))
+ (pearl-default-account nil)
+ (pearl-active-account "work"))
+ (cl-letf (((symbol-function 'customize-save-variable) (lambda (&rest _) nil)))
+ (pearl--persist-default-view "Work board")
+ (let ((spec (cdr (assoc "work" pearl-accounts))))
+ (should (equal '(:literal "kw") (plist-get spec :api-key-source)))
+ (should (string-equal "/tmp/w.org" (plist-get spec :org-file)))
+ (should (string-equal "team_w" (plist-get spec :default-team-id)))
+ (should (string-equal "https://example/api" (plist-get spec :url)))
+ (should (string-equal "Work board" (plist-get spec :default-view)))))))
+
+;;; pearl-set-default-view -- value determination paths
+
+(ert-deftest test-pearl-set-default-view-from-buffer-offer ()
+ "From a local-view buffer, accepting the offer persists that view's name."
+ (let ((pearl-accounts nil)
+ (pearl-default-view nil)
+ (pearl-local-views '(("Sprint board" :filter (:open t))))
+ (persisted 'untouched))
+ (cl-letf (((symbol-function 'pearl--read-active-source)
+ (lambda () '(:type filter :name "Sprint board" :filter (:open t))))
+ ((symbol-function 'pearl--read-yes-no) (lambda (&rest _) t))
+ ((symbol-function 'pearl--persist-default-view)
+ (lambda (v) (setq persisted v))))
+ (pearl-set-default-view)
+ (should (string-equal "Sprint board" persisted)))))
+
+(ert-deftest test-pearl-set-default-view-prompt-pick-name ()
+ "Not on a local-view buffer: the prompt's chosen name is persisted."
+ (let ((pearl-accounts nil)
+ (pearl-default-view nil)
+ (pearl-local-views '(("Sprint board" :filter (:open t))))
+ (persisted 'untouched))
+ (cl-letf (((symbol-function 'pearl--read-active-source) (lambda () nil))
+ ((symbol-function 'completing-read)
+ (lambda (&rest _) "Sprint board"))
+ ((symbol-function 'pearl--persist-default-view)
+ (lambda (v) (setq persisted v))))
+ (pearl-set-default-view)
+ (should (string-equal "Sprint board" persisted)))))
+
+(ert-deftest test-pearl-set-default-view-clear-sentinel-sets-nil ()
+ "Choosing the my-open-issues sentinel persists nil (clears the default)."
+ (let ((pearl-accounts nil)
+ (pearl-default-view "Sprint board")
+ (pearl-local-views '(("Sprint board" :filter (:open t))))
+ (persisted 'untouched))
+ (cl-letf (((symbol-function 'pearl--read-active-source) (lambda () nil))
+ ((symbol-function 'completing-read)
+ (lambda (&rest _) pearl--default-view-clear))
+ ((symbol-function 'pearl--persist-default-view)
+ (lambda (v) (setq persisted v))))
+ (pearl-set-default-view)
+ (should (null persisted)))))
+
+(ert-deftest test-pearl-set-default-view-cancel-leaves-unchanged ()
+ "An empty / quit choice persists nothing -- the default is left as-is."
+ (let ((pearl-accounts nil)
+ (pearl-default-view "Sprint board")
+ (pearl-local-views '(("Sprint board" :filter (:open t))))
+ (persisted 'untouched))
+ (cl-letf (((symbol-function 'pearl--read-active-source) (lambda () nil))
+ ((symbol-function 'completing-read) (lambda (&rest _) ""))
+ ((symbol-function 'pearl--persist-default-view)
+ (lambda (v) (setq persisted v))))
+ (pearl-set-default-view)
+ (should (eq 'untouched persisted)))))
+
(provide 'test-pearl-default-view)
;;; test-pearl-default-view.el ends here