aboutsummaryrefslogtreecommitdiff
path: root/pearl.el
diff options
context:
space:
mode:
authorCraig Jennings <c@cjennings.net>2026-06-23 23:40:27 -0400
committerCraig Jennings <c@cjennings.net>2026-06-23 23:40:27 -0400
commit8869a083c3cc28c20efa63e8932347329b2865f4 (patch)
tree8959775eb2cae28a3418542ab8e2fc235fe465f2 /pearl.el
parent92ae9d8b56a9fabadf6ea8beb2163655c7a8ffb8 (diff)
downloadpearl-8869a083c3cc28c20efa63e8932347329b2865f4.tar.gz
pearl-8869a083c3cc28c20efa63e8932347329b2865f4.zip
feat(sources): single-issue source (favorites + open-issue-by-id)
I wired the issue kind into the same pipeline every other source uses, so a favorited issue renders in the buffer as its own subtree instead of the v1 browser punt. The new pearl-open-issue-by-id command does the same for an issue you type by identifier or id. It's handy when someone hands you ENG-123 and you'd rather read it in Pearl than a browser. Linear's issue(id:) accepts both the UUID and the human identifier, so there's no resolution step. I fetch by whatever was given and build the :type issue source from the returned node, so the stored id and identifier stay authoritative. Refresh re-fetches the one issue and merges by LINEAR-ID like the filter and view sources. Tests cover the source constructor, the header round-trip, and the fetch/render and command paths across Normal, Boundary, and Error.
Diffstat (limited to 'pearl.el')
-rw-r--r--pearl.el74
1 files changed, 71 insertions, 3 deletions
diff --git a/pearl.el b/pearl.el
index e4958e9..05b4c68 100644
--- a/pearl.el
+++ b/pearl.el
@@ -1404,11 +1404,21 @@ both normalize to `:kind label' so the dispatch collapses them."
(cdr (assoc 'identifier (cdr (assoc 'issue node))))))
base)))
+(defun pearl--issue-source (id identifier)
+ "Build a `:type issue' source plist for the single issue ID/IDENTIFIER.
+IDENTIFIER is the human key (ENG-1) and becomes the source name so the buffer
+title and `#+LINEAR-SOURCE' read naturally; it falls back to ID when absent, so
+`:name' is never nil. Mirrors the other source shapes (`:type view',
+`:type filter') so the issue flows through the same fetch/render/refresh path."
+ (list :type 'issue :id id :identifier identifier
+ :name (or identifier id)))
+
(defun pearl--favorite->source (fav)
"Resolve a normalized FAV into a runnable source plist for the existing
query/render path, or a browser action `(:browser t :url U :title T :kind K)'
-when the favorite is not a list source in v1 (issue / team / document /
-dashboard / other)."
+when the favorite is not a list source in v1 (team / document / dashboard /
+other). An issue favorite resolves to a `:type issue' source so a starred
+issue renders in the buffer like every other source."
(let ((kind (plist-get fav :kind))
(title (plist-get fav :title))
(url (plist-get fav :url))
@@ -1423,6 +1433,7 @@ dashboard / other)."
:filter (list :label-id id :open t)))
('user (list :type 'filter :name title
:filter (list :assignee-id id :open t)))
+ ('issue (pearl--issue-source id (plist-get fav :identifier)))
(_ (list :browser t :url url :title title :kind kind)))))
(defconst pearl--favorites-query
@@ -4920,6 +4931,9 @@ missing setup."
(pearl--open-favorite-url source))
((eq (plist-get source :type) 'view)
(pearl--run-view-source source))
+ ((eq (plist-get source :type) 'issue)
+ (pearl--progress "Opening %s..." (pearl--source-name source))
+ (pearl--fetch-and-render-issue (plist-get source :id)))
(t
(let ((filter (plist-get source :filter)))
(pearl--validate-issue-filter filter)
@@ -4929,6 +4943,44 @@ missing setup."
(lambda (result) (pearl--render-query-result result source))
(pearl--sort->order-by (plist-get source :sort))))))))))))
+(defun pearl--fetch-and-render-issue (ref)
+ "Fetch the issue named by REF and render it as the active source.
+REF is a Linear issue id (UUID) or its human identifier (ENG-1); Linear's
+`issue(id:)' accepts either. On success the single node is rendered through
+the shared `pearl--render-query-result' boundary -- the same full-replace path
+list-issues and views use -- tagged with a `:type issue' source built from the
+node's own id and identifier, so refresh and the `#+LINEAR-SOURCE' header work
+exactly as they do for every other source. A missing or errored fetch reports
+and leaves the buffer untouched."
+ (pearl--fetch-issue-async
+ ref
+ (lambda (result)
+ (pcase result
+ (:error
+ (message "Linear returned an error fetching %s" ref))
+ (:missing
+ (message "Issue %s not found on Linear (deleted, or no access)" ref))
+ (raw
+ (pearl--render-query-result
+ (pearl--make-query-result 'ok :issues (list raw))
+ (pearl--issue-source (cdr (assoc 'id raw))
+ (cdr (assoc 'identifier raw)))))))))
+
+;;;###autoload
+(defun pearl-open-issue-by-id (ref)
+ "Open a single Linear issue by its identifier or id and render it in Pearl.
+Prompts for REF -- a human identifier like ENG-123 (or a raw issue id) -- then
+fetches that one issue and renders it in the active file as its own source, the
+same shape every other source produces. Useful when someone hands you an issue
+id and you want it in Pearl rather than a browser."
+ (interactive (list (read-string "Issue identifier (e.g. ENG-123): ")))
+ (pearl--require-account-context)
+ (let ((trimmed (string-trim ref)))
+ (when (string-empty-p trimmed)
+ (user-error "No issue identifier given"))
+ (pearl--progress "Opening %s..." trimmed)
+ (pearl--fetch-and-render-issue trimmed)))
+
;;; Copy down: save a Linear view as a local view (see the reverse-compile above)
(defun pearl--customview-filterdata-async (view-id callback)
@@ -7176,6 +7228,20 @@ commands. Errors if no source is recorded."
(plist-get source :sort)
(pearl--view-completed-filter
(plist-get source :show-completed))))
+ ('issue
+ (pearl--progress "Refreshing %s..." (pearl--source-name source))
+ (pearl--fetch-issue-async
+ (plist-get source :id)
+ (lambda (result)
+ (pcase result
+ (:error
+ (message "Linear returned an error refreshing %s"
+ (pearl--source-name source)))
+ (:missing
+ (message "Issue %s is no longer on Linear (deleted, or no access)"
+ (pearl--source-name source)))
+ (raw
+ (funcall merge (pearl--make-query-result 'ok :issues (list raw))))))))
(_ (user-error "Unknown Linear source type: %s" (plist-get source :type)))))))
;;; Interactive sort/order of the active view
@@ -8366,7 +8432,8 @@ body stay."
("l" "open default view" pearl-open-default-view)
("m" "my open issues" pearl-list-issues)
("p" "by project" pearl-list-issues-by-project)
- ("f" "build a filter" pearl-list-issues-filtered)]
+ ("f" "build a filter" pearl-list-issues-filtered)
+ ("i" "open issue by id" pearl-open-issue-by-id)]
["Views"
("V" "create local view" pearl-create-local-view)
("e" "edit local view" pearl-edit-local-view)
@@ -8423,6 +8490,7 @@ body stay."
(define-key map "o" (cons "my open issues" #'pearl-list-issues))
(define-key map "p" (cons "by project" #'pearl-list-issues-by-project))
(define-key map "f" (cons "build a filter" #'pearl-list-issues-filtered))
+ (define-key map "i" (cons "open issue by id" #'pearl-open-issue-by-id))
(define-key map "c" (cons "all comments for issue" #'pearl-fetch-all-comments))
map)
"Pearl issue-fetch commands; a sub-keymap of `pearl-prefix-map'.