diff options
| author | Craig Jennings <c@cjennings.net> | 2026-05-27 15:56:16 -0500 |
|---|---|---|
| committer | Craig Jennings <c@cjennings.net> | 2026-05-27 15:56:16 -0500 |
| commit | 78bd607f8dd1f4c29e4d7dda9744b06bf7352105 (patch) | |
| tree | 7ed2e625334484b4f72044748f33849cc0a643a2 /pearl.el | |
| parent | 805e288178ce2b96dd4a0277b49904131853e8fe (diff) | |
| download | pearl-78bd607f8dd1f4c29e4d7dda9744b06bf7352105.tar.gz pearl-78bd607f8dd1f4c29e4d7dda9744b06bf7352105.zip | |
feat(labels): render Linear labels as Org tags on issue headings
An issue's Linear labels now appear as Org tags on its heading (** TODO [#B] Title :bug:backend:), so the org-native gestures work: filter by tag, build a tag agenda, sparse-tree on :bug:. Before this, labels lived only in the :LINEAR-LABELS: drawer, which Org's tag machinery can't see.
pearl--label-name-to-tag slugifies a label name to a tag: downcase, non-[[:alnum:]_] runs become a single underscore, Unicode letters preserved. pearl--label-tags builds the deduplicated set, first occurrence winning on a collision. The renderer appends them, and pearl-edit-labels rewrites them through pearl--set-heading-label-tags after updating the drawer.
This is render-only. The :LINEAR-LABELS: drawer stays the source of truth, hand-edited heading tags are ignored by save and the dirty scan and get rewritten from Linear on the next change or fetch, and the way to change labels is still pearl-edit-labels. Bidirectional tag editing (heading tags back to Linear) is out of scope.
Diffstat (limited to 'pearl.el')
| -rw-r--r-- | pearl.el | 44 |
1 files changed, 42 insertions, 2 deletions
@@ -1535,6 +1535,32 @@ state-name-to-keyword mapping." "-+" "-+"))) (if (string-empty-p slug) "TODO" slug))) +(defun pearl--label-name-to-tag (name) + "Slugify a Linear label NAME to an Org tag. +Downcased, each run of characters outside [[:alnum:]_] replaced by a single +underscore (alnum is Unicode-aware, so accented and non-Latin letters survive), +with leading and trailing underscores trimmed. Org tags allow no hyphens or +spaces, unlike the TODO keywords `pearl--state-name-to-keyword' builds. A name +that slugifies to empty (all punctuation) returns the empty string; the caller +drops it from the heading and keeps it in the `:LINEAR-LABELS:' drawer." + (string-trim + (replace-regexp-in-string "[^[:alnum:]_]+" "_" (downcase (or name ""))) + "_+" "_+")) + +(defun pearl--label-tags (labels) + "Return the ordered, de-duplicated Org tags for a list of LABEL plists. +Each label's `:name' is slugified via `pearl--label-name-to-tag'; empty slugs +are dropped and duplicates removed, first occurrence winning, so two label +names that slugify to the same tag render it once at the first's position." + (let ((seen (make-hash-table :test 'equal)) + (tags '())) + (dolist (label labels) + (let ((tag (pearl--label-name-to-tag (plist-get label :name)))) + (unless (or (string-empty-p tag) (gethash tag seen)) + (puthash tag t seen) + (push tag tags)))) + (nreverse tags))) + (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 @@ -1804,11 +1830,13 @@ 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))) + (tag-string (if tags (concat " :" (string-join tags ":") ":") "")) (body-org (pearl--md-to-org description))) (concat - (format "** %s %s%s\n" todo + (format "** %s %s%s%s\n" todo (if (string-empty-p priority) "" (concat priority " ")) - heading-title) + heading-title tag-string) ":PROPERTIES:\n" (format ":LINEAR-ID: %s\n" (or (plist-get issue :id) "")) (format ":LINEAR-IDENTIFIER: %s\n" (or (plist-get issue :identifier) "")) @@ -2747,6 +2775,16 @@ inside an issue subtree." (org-entry-put nil "LINEAR-ASSIGNEE-ID" assignee-id) (message "Set assignee to %s (save to push)" assignee-name)))) +(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." + (save-excursion + (org-back-to-heading t) + (org-set-tags tags))) + ;;;###autoload (defun pearl-edit-labels (label-names) "Set the labels of the issue at point to LABEL-NAMES in the buffer. @@ -2774,6 +2812,8 @@ inside an issue subtree." (org-entry-put nil "LINEAR-LABELS" (format "[%s]" (mapconcat #'identity label-names ", "))) (org-entry-put nil "LINEAR-LABEL-IDS" (string-join label-ids " ")) + (pearl--set-heading-label-tags + (pearl--label-tags (mapcar (lambda (n) (list :name n)) label-names))) (message "Set labels to %s (save to push)" (if label-names (mapconcat #'identity label-names ", ") "none"))))) |
