aboutsummaryrefslogtreecommitdiff
path: root/pearl.el
diff options
context:
space:
mode:
authorCraig Jennings <c@cjennings.net>2026-06-03 11:53:12 -0500
committerCraig Jennings <c@cjennings.net>2026-06-03 11:53:50 -0500
commitcb73a13c85c9818979f21e5ca0fd6aee631fb61e (patch)
treeb69074438512112571d9e677785163f59aadfed7 /pearl.el
parent1f729465d2247fb6882185e426860de869fe9b71 (diff)
downloadpearl-cb73a13c85c9818979f21e5ca0fd6aee631fb61e.tar.gz
pearl-cb73a13c85c9818979f21e5ca0fd6aee631fb61e.zip
feat(views): surface the issue assignee as an @-tag on the heading
On a team or "by person" view you couldn't tell who owned an issue at a glance: the assignee was captured but folded inside the :LINEAR-ASSIGNEE-NAME: drawer, with nothing in the heading. I render the assignee as a leading @-tag (:@eric:bug:backend:), behind a pearl-show-assignee defcustom (default on). I picked a tag over a heading suffix because Org strips tags before it hashes the title for sync, so the tag can't corrupt title-sync the way a suffix would. The @ prefix keeps it in its own namespace, separate from label tags: labels are tracked in their drawer, not read back from the heading, so the tag never gets mistaken for a label. The label-edit path preserves any @-tag when it rewrites the heading's tags, and edit-assignee refreshes the @-tag so a reassign doesn't leave a stale name until the next fetch. Verified live against a by-person view: assigned issues carry their @-tag, unassigned ones stay bare.
Diffstat (limited to 'pearl.el')
-rw-r--r--pearl.el54
1 files changed, 47 insertions, 7 deletions
diff --git a/pearl.el b/pearl.el
index 2537cf7..a3b93c0 100644
--- a/pearl.el
+++ b/pearl.el
@@ -276,6 +276,17 @@ never leaks into the title on Linear. Set to nil to omit the prefix."
:type 'boolean
:group 'pearl)
+(defcustom pearl-show-assignee t
+ "When non-nil, render the issue assignee as a leading @-tag on the heading.
+For example `** TODO [#B] SE-401: Fix the bug :@eric:bug:'. This surfaces who
+owns an issue at a glance on a team or \"by person\" view, where the assignee
+would otherwise sit folded in the `:LINEAR-ASSIGNEE-NAME:' drawer. Display-only
+and title-sync-safe: Org strips tags before the title is hashed, and the
+assignee is never read back as a label (labels are tracked in their drawer).
+Set to nil to omit the tag."
+ :type 'boolean
+ :group 'pearl)
+
(defconst pearl--title-case-minor-words
'("a" "an" "and" "as" "at" "but" "by" "for" "if" "in" "nor" "of" "on" "or"
"per" "the" "to" "vs" "via")
@@ -2484,6 +2495,17 @@ names that slugify to the same tag render it once at the first's position."
(push tag tags))))
(nreverse tags)))
+(defun pearl--assignee-tag (assignee)
+ "Return the @-prefixed Org tag for ASSIGNEE (a normalized user plist), or nil.
+Returns nil when ASSIGNEE is nil, `pearl-show-assignee' is off, or the name
+slugifies to empty. The slug reuses `pearl--label-name-to-tag'; the `@' prefix
+keeps the assignee tag in its own namespace, distinct from label tags, so the
+label-edit path can preserve it (see `pearl--set-heading-label-tags').
+Title-sync-safe: Org strips tags before the heading title is hashed."
+ (when (and pearl-show-assignee assignee)
+ (let ((slug (pearl--label-name-to-tag (plist-get assignee :name))))
+ (unless (string-empty-p slug) (concat "@" slug)))))
+
(defun pearl--derive-todo-line (states)
"Build the `#+TODO:' keyword string from an ordered STATES list.
Each state is a plist with at least :name and :type. States whose :type is
@@ -2755,7 +2777,9 @@ identifier prefix -- so an unedited heading is a no-op on title sync)."
(plist-get issue :labels) ", "))
(label-ids (mapconcat (lambda (l) (or (plist-get l :id) ""))
(plist-get issue :labels) " "))
- (tags (pearl--label-tags (plist-get issue :labels)))
+ (label-tags (pearl--label-tags (plist-get issue :labels)))
+ (assignee-tag (pearl--assignee-tag assignee))
+ (tags (if assignee-tag (cons assignee-tag label-tags) label-tags))
(tag-string (if tags (concat " :" (string-join tags ":") ":") ""))
(stars (make-string (or level 2) ?*))
(body-org (pearl--md-to-org description)))
@@ -3708,17 +3732,33 @@ inside an issue subtree."
(user-error "No team member matching %s" assignee-name))
(org-entry-put nil "LINEAR-ASSIGNEE-NAME" assignee-name)
(org-entry-put nil "LINEAR-ASSIGNEE-ID" assignee-id)
+ (pearl--set-heading-assignee-tag assignee-name)
(message "Set assignee to %s (save to push)" assignee-name))))
+(defun pearl--set-heading-assignee-tag (assignee-name)
+ "Refresh the @-assignee tag on the issue heading at point from ASSIGNEE-NAME,
+preserving the label tags so a reassign doesn't leave a stale tag until the next
+fetch. A nil/empty name (or `pearl-show-assignee' off) clears the @-tag. Point
+must be on or inside the issue heading."
+ (save-excursion
+ (org-back-to-heading t)
+ (let* ((label-tags (seq-remove (lambda (tg) (string-prefix-p "@" tg))
+ (org-get-tags nil t)))
+ (atag (pearl--assignee-tag (list :name assignee-name))))
+ (org-set-tags (if atag (cons atag label-tags) label-tags)))))
+
(defun pearl--set-heading-label-tags (tags)
- "Replace the Org tags on the issue heading at point with TAGS, a list of strings.
-Only the heading's tag set changes: the TODO keyword, priority cookie, title,
-identifier prefix, body, and drawer are preserved. Pearl owns the entire
-issue-heading tag set, so any hand-added tags are dropped. An empty TAGS list
-clears the heading's tags. Point must be on or inside the issue heading."
+ "Replace the Org label tags on the issue heading at point with TAGS, a list of
+strings. Only the label tags change: the TODO keyword, priority cookie, title,
+identifier prefix, body, drawer, and the @-prefixed assignee tag (a separate
+namespace, see `pearl--assignee-tag') are preserved. Pearl owns the label tag
+set, so any other hand-added tags are dropped. An empty TAGS list clears the
+label tags. Point must be on or inside the issue heading."
(save-excursion
(org-back-to-heading t)
- (org-set-tags tags)))
+ (let ((assignee-tags (seq-filter (lambda (tg) (string-prefix-p "@" tg))
+ (org-get-tags nil t))))
+ (org-set-tags (append assignee-tags tags)))))
;;;###autoload
(defun pearl-edit-labels (label-names)