diff options
| author | Craig Jennings <c@cjennings.net> | 2026-06-02 12:56:09 -0500 |
|---|---|---|
| committer | Craig Jennings <c@cjennings.net> | 2026-06-02 12:56:09 -0500 |
| commit | 42f3a75651ed8d2f8e601c654e482dc85363bb83 (patch) | |
| tree | fa737d9aab0afc407bcc2264ab1ff03a99590e0c | |
| parent | 293c9f2a097991ca64f05f6252d0725f510d2e47 (diff) | |
| download | pearl-42f3a75651ed8d2f8e601c654e482dc85363bb83.tar.gz pearl-42f3a75651ed8d2f8e601c654e482dc85363bb83.zip | |
feat(views): default-view resolver and open command
Phase 1 of the default-view spec. I added the data and dispatch; keymap and the setter come in later phases.
pearl-default-view (defcustom) holds the legacy-mode default: nil for my open issues, or a local-view name. In accounts mode the per-account :default-view key wins instead, so pearl--resolve-account now carries :default-view in its context plist and pearl--resolve-default-view reads from the active account, falling back to the global only in legacy mode.
pearl-open-default-view dispatches the resolved default: nil runs pearl-list-issues, a known local-view name runs pearl-run-local-view, and a name that's since been deleted falls back to pearl-list-issues with a message instead of erroring. pearl-list-issues keeps its contract — it's still literal my-open-issues, the keymap rebind is phase 3.
Nine tests cover the resolver in both modes and the three dispatch paths. Suite 792 to 801, compile and lint clean.
| -rw-r--r-- | pearl.el | 49 | ||||
| -rw-r--r-- | tests/test-pearl-default-view.el | 134 |
2 files changed, 183 insertions, 0 deletions
@@ -112,6 +112,8 @@ per-workspace settings: (:env \"VAR\"), or (:literal \"lin_...\"). :org-file the account's active org file (`~' is expanded). :default-team-id default team for new issues; optional. + :default-view local-view name `pearl-open-default-view' opens for this + account, or nil for my open issues; optional. :url GraphQL endpoint; optional, defaults to `pearl-graphql-url'. @@ -448,6 +450,7 @@ to `pearl-graphql-url'. Errors when NAME is unknown or its key is missing." (plist-get spec :api-key-source) name) :org-file (and org-file (expand-file-name org-file)) :default-team-id (plist-get spec :default-team-id) + :default-view (plist-get spec :default-view) :url (or (plist-get spec :url) pearl-graphql-url))))) (defun pearl--current-account-context () @@ -3764,6 +3767,21 @@ Linear Custom View for OR logic." :type '(alist :key-type string :value-type plist) :group 'pearl) +(defcustom pearl-default-view nil + "Local view `pearl-open-default-view' opens, or nil for my open issues. +A durable preference for what the hot key shows. nil means the built-in +\"my open issues\" fetch (`pearl-list-issues' with no project -- today's +behavior); a string names a local view in `pearl-local-views'. + +In multi-account mode the active account's `:default-view' key (see +`pearl-accounts') takes precedence and this global is unused; it applies in +legacy single-account mode. To default to a Linear Custom View, save it +locally first with `pearl-save-linear-view-locally' and name the resulting +local view here. Set it durably through `pearl-set-default-view'." + :type '(choice (const :tag "My open issues" nil) + (string :tag "Local view name")) + :group 'pearl) + (defun pearl--sort-issues (issues sort order) "Return ISSUES sorted by SORT (a symbol) in ORDER (`asc' or `desc'). SORT is one of `updated', `priority', `title', or nil (no client-side sort). @@ -6133,6 +6151,37 @@ how each issue's state renders as a TODO keyword." filter (lambda (result) (pearl--render-query-result result source))))) +(defun pearl--resolve-default-view () + "Return the active scope's default view: a local-view name string, or nil. +In accounts mode the active account's `:default-view'; in legacy +single-account mode the global `pearl-default-view'. nil means \"my open +issues\". Resolving the account context follows the same active/default +fallback every account-aware command uses." + (let ((ctx (pearl--current-account-context))) + (if ctx + (plist-get ctx :default-view) + pearl-default-view))) + +;;;###autoload +(defun pearl-open-default-view () + "Open the configured default view, or my open issues when none is set. +Resolves the active scope's default (`pearl--resolve-default-view'): nil runs +`pearl-list-issues' (my open issues); a local-view name in `pearl-local-views' +runs `pearl-run-local-view'; a name no longer present falls back to +`pearl-list-issues' with a message rather than erroring -- a stale default +never blocks the hot key." + (interactive) + (let ((default (pearl--resolve-default-view))) + (cond + ((null default) + (pearl-list-issues)) + ((assoc default pearl-local-views) + (pearl-run-local-view default)) + (t + (message "Default view '%s' no longer exists; showing my open issues" + default) + (pearl-list-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 new file mode 100644 index 0000000..2794972 --- /dev/null +++ b/tests/test-pearl-default-view.el @@ -0,0 +1,134 @@ +;;; test-pearl-default-view.el --- Tests for the configurable default view -*- 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: + +;; Unit tests for the default-view resolver and the `pearl-open-default-view' +;; dispatcher (phase 1 of docs/default-view-spec.org). The resolver returns +;; the active scope's default -- `pearl-default-view' in legacy mode, the +;; active account's `:default-view' in accounts mode. The open command +;; dispatches nil -> my open issues, a known local-view name -> that view, and +;; a stale name -> my open issues with a message (never an error). The two +;; fetch commands are stubbed so no network is touched. + +;;; Code: + +(require 'test-bootstrap (expand-file-name "test-bootstrap.el")) + +;;; pearl--resolve-default-view -- scope resolution + +(ert-deftest test-pearl-resolve-default-view-legacy-nil () + "Legacy mode, no default set: the resolver returns nil." + (let ((pearl-accounts nil) + (pearl-default-view nil)) + (should-not (pearl--resolve-default-view)))) + +(ert-deftest test-pearl-resolve-default-view-legacy-name () + "Legacy mode with a default set: the resolver returns the local-view name." + (let ((pearl-accounts nil) + (pearl-default-view "My active work")) + (should (string-equal "My active work" (pearl--resolve-default-view))))) + +(ert-deftest test-pearl-resolve-default-view-accounts-reads-active () + "Accounts mode: the resolver returns the *active* account's :default-view." + (let ((pearl-accounts + '(("work" :api-key-source (:literal "kw") :org-file "/tmp/w.org" + :default-view "Work board") + ("home" :api-key-source (:literal "kh") :org-file "/tmp/h.org" + :default-view "Home triage"))) + (pearl-default-account nil) + (pearl-active-account "work")) + (should (string-equal "Work board" (pearl--resolve-default-view))))) + +(ert-deftest test-pearl-resolve-default-view-accounts-switch () + "Accounts mode: switching the active account switches the resolved default." + (let ((pearl-accounts + '(("work" :api-key-source (:literal "kw") :org-file "/tmp/w.org" + :default-view "Work board") + ("home" :api-key-source (:literal "kh") :org-file "/tmp/h.org" + :default-view "Home triage"))) + (pearl-default-account nil) + (pearl-active-account "home")) + (should (string-equal "Home triage" (pearl--resolve-default-view))))) + +(ert-deftest test-pearl-resolve-default-view-accounts-unset () + "Accounts mode, active account has no :default-view: the resolver returns nil. +The legacy `pearl-default-view' does not leak in when accounts are configured." + (let ((pearl-accounts + '(("work" :api-key-source (:literal "kw") :org-file "/tmp/w.org"))) + (pearl-default-account nil) + (pearl-active-account "work") + (pearl-default-view "Legacy default")) + (should-not (pearl--resolve-default-view)))) + +(ert-deftest test-pearl-resolve-account-carries-default-view () + "`pearl--resolve-account' surfaces the account's :default-view in its context." + (let ((pearl-accounts + '(("work" :api-key-source (:literal "kw") :org-file "/tmp/w.org" + :default-view "Work board")))) + (should (string-equal "Work board" + (plist-get (pearl--resolve-account "work") :default-view))))) + +;;; pearl-open-default-view -- dispatch + +(ert-deftest test-pearl-open-default-view-nil-runs-my-open-issues () + "An unset default dispatches to `pearl-list-issues' (my open issues)." + (let ((pearl-accounts nil) + (pearl-default-view nil) + (calls nil)) + (cl-letf (((symbol-function 'pearl-list-issues) + (lambda (&rest args) (push (cons 'list args) calls))) + ((symbol-function 'pearl-run-local-view) + (lambda (name) (push (cons 'view name) calls)))) + (pearl-open-default-view) + (should (equal calls '((list))))))) + +(ert-deftest test-pearl-open-default-view-name-runs-local-view () + "A default naming an existing local view dispatches to `pearl-run-local-view'." + (let ((pearl-accounts nil) + (pearl-default-view "My active work") + (pearl-local-views '(("My active work" :filter (:assignee :me :open t)))) + (calls nil)) + (cl-letf (((symbol-function 'pearl-list-issues) + (lambda (&rest args) (push (cons 'list args) calls))) + ((symbol-function 'pearl-run-local-view) + (lambda (name) (push (cons 'view name) calls)))) + (pearl-open-default-view) + (should (equal calls '((view . "My active work"))))))) + +(ert-deftest test-pearl-open-default-view-stale-name-falls-back () + "A default naming a deleted local view falls back to my open issues, no error. +It dispatches to `pearl-list-issues' and never calls `pearl-run-local-view'." + (let ((pearl-accounts nil) + (pearl-default-view "Deleted view") + (pearl-local-views '(("Some other view" :filter (:open t)))) + (calls nil) + (messaged nil)) + (cl-letf (((symbol-function 'pearl-list-issues) + (lambda (&rest args) (push (cons 'list args) calls))) + ((symbol-function 'pearl-run-local-view) + (lambda (name) (push (cons 'view name) calls))) + ((symbol-function 'message) + (lambda (&rest args) (setq messaged (or messaged args))))) + (pearl-open-default-view) + (should (equal calls '((list)))) + (should messaged)))) + +(provide 'test-pearl-default-view) +;;; test-pearl-default-view.el ends here |
