aboutsummaryrefslogtreecommitdiff
path: root/pearl.el
diff options
context:
space:
mode:
authorCraig Jennings <c@cjennings.net>2026-05-25 21:22:27 -0500
committerCraig Jennings <c@cjennings.net>2026-05-25 21:22:27 -0500
commit60b51e1c996f1c8f152696946b9440f26ea9a956 (patch)
tree3154332cc2b7d5bb3e0eb71a812d9effc8c46633 /pearl.el
parentf9841539b3adb192d0c15470fa9c31d6cbbdf447 (diff)
downloadpearl-60b51e1c996f1c8f152696946b9440f26ea9a956.tar.gz
pearl-60b51e1c996f1c8f152696946b9440f26ea9a956.zip
refactor(save): retire the immediate-push field setters for buffer-editors
The structured fields now reconcile at save (the previous commits), so the immediate-push setters are the duplicate path that made the package feel like it had two save models. I retired them. pearl-set-priority is deleted outright — priority is edited org-natively through the heading cookie. pearl-set-state, pearl-set-assignee, and pearl-set-labels become pearl-edit-state / -assignee / -labels: the same completing-read picker, but it writes the buffer representation (keyword/name/id, the assignee drawer, the labels drawer plus the live LINEAR-LABEL-IDS set) and marks the field dirty instead of pushing. The change goes out at the next pearl-save-issue / pearl-save-all. pearl-compose-current-description is renamed pearl-edit-description (its compose-and-push behavior is unchanged — v2 doesn't touch the compose buffers). The transient, the C-; L e keymap, the affordance preamble, and the README follow the new names, and the priority slots are dropped (no command — it's the org cookie). Deleting the setters left pearl--push-issue-field and pearl--priority-choices with no callers, so both are gone, along with the killed-buffer test that only exercised the removed helper. I filed a follow-up: the atomic savers' async commit callbacks need the same killed-buffer guard that helper carried. The setter command tests are rewritten to assert the buffer-write-and-no-push contract. The reconcile-and-push path they used to cover is exercised in test-pearl-save.
Diffstat (limited to 'pearl.el')
-rw-r--r--pearl.el153
1 files changed, 47 insertions, 106 deletions
diff --git a/pearl.el b/pearl.el
index 9a17662..370ccb2 100644
--- a/pearl.el
+++ b/pearl.el
@@ -2613,7 +2613,7 @@ the new comment is the viewer's own, so it renders editable."
issue-id marker (pearl--org-to-md org))))))))
;;;###autoload
-(defun pearl-compose-current-description ()
+(defun pearl-edit-description ()
"Edit the description of the Linear issue at point in an Org compose buffer.
Pops the current description into a focused buffer; C-c C-c writes it back into
the issue body and syncs to Linear through the usual conflict gate, C-c C-k
@@ -2718,10 +2718,6 @@ the generic mutation the field commands share."
:updated-at (cdr (assoc 'updatedAt issue))))))
(lambda (_error _response _data) (funcall callback '(:success nil))))))
-(defconst pearl--priority-choices
- '(("None" . 0) ("Urgent" . 1) ("High" . 2) ("Medium" . 3) ("Low" . 4))
- "Linear priority names mapped to their numeric API values.")
-
(defun pearl--set-priority-cookie (priority-num)
"Set the Org priority cookie on the heading at point from PRIORITY-NUM.
1-4 map to #A-#D; 0 (None) removes the cookie. The priority range is bound
@@ -2737,46 +2733,6 @@ locally so #D is accepted regardless of the user's `org-priority' settings."
(4 (org-priority ?D))
(_ (org-priority 'remove))))))
-;;;###autoload
-(defun pearl--push-issue-field (issue-id marker fields on-success success-message fail-message)
- "Push FIELDS (an `issueUpdate' input alist) for ISSUE-ID.
-On success, run the ON-SUCCESS thunk at MARKER, report SUCCESS-MESSAGE, and
-surface the buffer; on failure report FAIL-MESSAGE. The shared push-and-report
-tail of the field-setter commands (priority/state/assignee/labels)."
- (pearl--update-issue-async
- issue-id fields
- (lambda (result)
- (if (plist-get result :success)
- ;; The push may complete after the buffer was killed; only touch it
- ;; when it's still live, but still report the (remote) success.
- (progn
- (when (buffer-live-p (marker-buffer marker))
- (save-excursion
- (goto-char marker)
- (funcall on-success))
- (pearl--surface-buffer (marker-buffer marker)))
- (message "%s" success-message))
- (message "%s" fail-message)))))
-
-(defun pearl-set-priority (priority-name)
- "Set the priority of the Linear issue at point to PRIORITY-NAME.
-Interactively, completes over None/Urgent/High/Medium/Low. Pushes the numeric
-priority to Linear and rewrites the heading cookie on success. Works from
-anywhere inside an issue subtree."
- (interactive
- (list (completing-read "Priority: " pearl--priority-choices nil t)))
- (save-excursion
- (pearl--goto-issue-heading-or-error)
- (let ((issue-id (org-entry-get nil "LINEAR-ID"))
- (priority-num (cdr (assoc priority-name pearl--priority-choices)))
- (marker (point-marker)))
- (unless priority-num
- (user-error "Unknown priority: %s" priority-name))
- (pearl--push-issue-field
- issue-id marker `(("priority" . ,priority-num))
- (lambda () (pearl--set-priority-cookie priority-num))
- (format "Set %s priority to %s" issue-id priority-name)
- (format "Failed to set priority for %s" issue-id)))))
(defun pearl--set-heading-state (state-name state-id)
"Update the heading at point to STATE-NAME / STATE-ID.
@@ -2792,11 +2748,12 @@ second push back to Linear."
(org-todo (pearl--map-linear-state-to-org state-name)))))
;;;###autoload
-(defun pearl-set-state (state-name)
- "Set the workflow state of the Linear issue at point to STATE-NAME.
-Interactively, completes over the issue team's workflow states. Pushes the
-resolved state id to Linear and updates the heading keyword and drawer on
-success. Works from anywhere inside an issue subtree."
+(defun pearl-edit-state (state-name)
+ "Set the workflow state of the issue at point to STATE-NAME in the buffer.
+Completes over the issue team's workflow states and writes the keyword, name,
+and id. The change is reconciled and pushed at the next save, not immediately.
+Works from anywhere inside an issue subtree. This picker reaches any state,
+including one a TODO-keyword cycle can't express."
(interactive
(let ((team-id (save-excursion
(when (ignore-errors (org-back-to-heading t) t)
@@ -2808,20 +2765,15 @@ success. Works from anywhere inside an issue subtree."
nil t))))
(save-excursion
(pearl--goto-issue-heading-or-error)
- (let* ((issue-id (org-entry-get nil "LINEAR-ID"))
- (team-id (org-entry-get nil "LINEAR-TEAM-ID"))
- (marker (point-marker))
+ (let* ((team-id (org-entry-get nil "LINEAR-TEAM-ID"))
(states (and team-id (pearl--team-states team-id)))
(state (seq-find (lambda (s) (string= (cdr (assoc 'name s)) state-name))
states))
(state-id (and state (cdr (assoc 'id state)))))
(unless state-id
(user-error "No workflow state named %s in this team" state-name))
- (pearl--push-issue-field
- issue-id marker `(("stateId" . ,state-id))
- (lambda () (pearl--set-heading-state state-name state-id))
- (format "Set %s state to %s" issue-id state-name)
- (format "Failed to set state for %s" issue-id)))))
+ (pearl--set-heading-state state-name state-id)
+ (message "Set state to %s (save to push)" state-name))))
(defun pearl--team-collection-names (kind team-id)
"Return the display labels of the KIND collection for TEAM-ID, for completion."
@@ -2829,11 +2781,12 @@ success. Works from anywhere inside an issue subtree."
(and team-id (pearl--team-collection kind team-id))))
;;;###autoload
-(defun pearl-set-assignee (assignee-name)
- "Set the assignee of the Linear issue at point to ASSIGNEE-NAME.
-Interactively, completes over the issue team's members. Resolves the name to
-a user id, pushes it, and updates the assignee drawer on success. Works from
-anywhere inside an issue subtree."
+(defun pearl-edit-assignee (assignee-name)
+ "Set the assignee of the issue at point to ASSIGNEE-NAME in the buffer.
+Completes over the issue team's members, resolves the name to a user id, and
+writes the assignee drawer; the change is reconciled and pushed at the next
+`pearl-save-issue' / `pearl-save-all', not immediately. Works from anywhere
+inside an issue subtree."
(interactive
(let ((team-id (save-excursion
(when (ignore-errors (org-back-to-heading t) t)
@@ -2843,28 +2796,23 @@ anywhere inside an issue subtree."
nil t))))
(save-excursion
(pearl--goto-issue-heading-or-error)
- (let* ((issue-id (org-entry-get nil "LINEAR-ID"))
- (team-id (org-entry-get nil "LINEAR-TEAM-ID"))
- (marker (point-marker))
+ (let* ((team-id (org-entry-get nil "LINEAR-TEAM-ID"))
(assignee-id (and team-id
(pearl--resolve-team-id 'members assignee-name team-id))))
(unless assignee-id
(user-error "No team member matching %s" assignee-name))
- (pearl--push-issue-field
- issue-id marker `(("assigneeId" . ,assignee-id))
- (lambda ()
- (org-entry-put nil "LINEAR-ASSIGNEE-NAME" assignee-name)
- (org-entry-put nil "LINEAR-ASSIGNEE-ID" assignee-id))
- (format "Set %s assignee to %s" issue-id assignee-name)
- (format "Failed to set assignee for %s" issue-id)))))
+ (org-entry-put nil "LINEAR-ASSIGNEE-NAME" assignee-name)
+ (org-entry-put nil "LINEAR-ASSIGNEE-ID" assignee-id)
+ (message "Set assignee to %s (save to push)" assignee-name))))
;;;###autoload
-(defun pearl-set-labels (label-names)
- "Set the labels of the Linear issue at point to LABEL-NAMES.
-Interactively, completes (multiple) over the issue team's labels; an empty
-selection clears the labels. Resolves each name to a label id, pushes the id
-list, and updates the labels drawer on success. Works from anywhere inside an
-issue subtree."
+(defun pearl-edit-labels (label-names)
+ "Set the labels of the issue at point to LABEL-NAMES in the buffer.
+Completes (multiple) over the issue team's labels; an empty selection clears
+them. Resolves each name to a label id and writes the labels drawer plus the
+`LINEAR-LABEL-IDS' live set; the change is reconciled and pushed at the next
+`pearl-save-issue' / `pearl-save-all', not immediately. Works from anywhere
+inside an issue subtree."
(interactive
(let ((team-id (save-excursion
(when (ignore-errors (org-back-to-heading t) t)
@@ -2874,23 +2822,18 @@ issue subtree."
(pearl--team-collection-names 'labels team-id)))))
(save-excursion
(pearl--goto-issue-heading-or-error)
- (let* ((issue-id (org-entry-get nil "LINEAR-ID"))
- (team-id (org-entry-get nil "LINEAR-TEAM-ID"))
- (marker (point-marker))
+ (let* ((team-id (org-entry-get nil "LINEAR-TEAM-ID"))
(label-ids (mapcar
(lambda (name)
(or (and team-id
(pearl--resolve-team-id 'labels name team-id))
(user-error "No label matching %s" name)))
label-names)))
- (pearl--push-issue-field
- issue-id marker `(("labelIds" . ,label-ids))
- (lambda ()
- (org-entry-put nil "LINEAR-LABELS"
- (format "[%s]" (mapconcat #'identity label-names ", "))))
- (format "Set %s labels to %s" issue-id
- (if label-names (mapconcat #'identity label-names ", ") "none"))
- (format "Failed to set labels for %s" issue-id)))))
+ (org-entry-put nil "LINEAR-LABELS"
+ (format "[%s]" (mapconcat #'identity label-names ", ")))
+ (org-entry-put nil "LINEAR-LABEL-IDS" (string-join label-ids " "))
+ (message "Set labels to %s (save to push)"
+ (if label-names (mapconcat #'identity label-names ", ") "none")))))
;;; User-facing Commands (Async)
@@ -2952,9 +2895,9 @@ page cap was hit. Pure function, no side effects."
(insert (format "#+LINEAR-TRUNCATED: %s\n" (if truncated "yes" "no")))
;; Affordance preamble (org comments -- not rendered content).
(insert "#\n")
- (insert "# Body = the issue description; edit it, then M-x pearl-sync-current-issue to push.\n")
+ (insert "# Body = the issue description; edit it, then M-x pearl-save-issue to push.\n")
(insert "# Comments subtree = the thread; add with M-x pearl-add-comment.\n")
- (insert "# Drawer fields change via M-x pearl-set-priority / -state / -assignee / -labels.\n")
+ (insert "# State/assignee/labels: M-x pearl-edit-state / -assignee / -labels; priority: the org cookie (C-c ,). All push at save.\n")
(insert "# Refresh with M-x pearl-refresh-current-view (whole file) or -current-issue (one).\n")
(insert "\n")
@@ -4693,12 +4636,11 @@ body stay."
("e" "save ticket" pearl-save-issue)
("E" "save all" pearl-save-all)]
["Edit"
- ("D" "edit description" pearl-compose-current-description)
+ ("D" "edit description" pearl-edit-description)
("M" "edit comment" pearl-edit-current-comment)
- ("P" "edit priority" pearl-set-priority)
- ("s" "edit state" pearl-set-state)
- ("a" "edit assignee" pearl-set-assignee)
- ("L" "edit labels" pearl-set-labels)]
+ ("s" "edit state" pearl-edit-state)
+ ("a" "edit assignee" pearl-edit-assignee)
+ ("L" "edit labels" pearl-edit-labels)]
["New"
("n" "new ticket" pearl-new-issue)
("c" "new comment" pearl-add-comment)]
@@ -4737,10 +4679,10 @@ body stay."
;; verb-matched label (e.g. "edit priority") without pearl depending on
;; which-key. Lookups still resolve straight to the command.
;;
-;; The labels follow the edit/new verbs even though some commands keep older
-;; names (`pearl-compose-current-description', `pearl-set-state', and
-;; `pearl-add-comment'); aligning the command names is a separate task. The
-;; `d c' delete-comment slot is intentionally empty until that command exists.
+;; The labels follow the edit/new verbs. The edit commands are named to match
+;; (`pearl-edit-description', `pearl-edit-state', ...); the new-comment command
+;; keeps its `pearl-add-comment' name pending that rename. Priority has no edit
+;; command -- it is edited org-natively via the heading cookie (C-c , / S-up).
(defvar pearl-save-map
(let ((map (make-sparse-keymap)))
@@ -4751,12 +4693,11 @@ body stay."
(defvar pearl-edit-map
(let ((map (make-sparse-keymap)))
- (define-key map "d" (cons "edit description" #'pearl-compose-current-description))
+ (define-key map "d" (cons "edit description" #'pearl-edit-description))
(define-key map "c" (cons "edit comment" #'pearl-edit-current-comment))
- (define-key map "p" (cons "edit priority" #'pearl-set-priority))
- (define-key map "s" (cons "edit state" #'pearl-set-state))
- (define-key map "a" (cons "edit assignee" #'pearl-set-assignee))
- (define-key map "l" (cons "edit labels" #'pearl-set-labels))
+ (define-key map "s" (cons "edit state" #'pearl-edit-state))
+ (define-key map "a" (cons "edit assignee" #'pearl-edit-assignee))
+ (define-key map "l" (cons "edit labels" #'pearl-edit-labels))
map)
"Pearl edit commands; a sub-keymap of `pearl-prefix-map'.")