aboutsummaryrefslogtreecommitdiff
diff options
context:
space:
mode:
-rw-r--r--pearl.el91
-rw-r--r--tests/test-pearl-output.el63
2 files changed, 144 insertions, 10 deletions
diff --git a/pearl.el b/pearl.el
index 95856ca..22ad714 100644
--- a/pearl.el
+++ b/pearl.el
@@ -229,7 +229,7 @@ leave the buffer expanded after updates."
:group 'pearl)
(defcustom pearl-help-header t
- "When non-nil, render a folded \"* Pearl\" heading atop each Linear file.
+ "When non-nil, render a folded \"* Pearl Help\" heading atop each Linear file.
The heading collapses on open (via a `:VISIBILITY: folded' property) and carries
the edit-then-save reminder, the keymap cues, and the priority-cookie legend, so
the help is discoverable without a dozen lines of `#' preamble cluttering the
@@ -238,6 +238,22 @@ it entirely, leaving just the machine `#+' keywords."
:type 'boolean
:group 'pearl)
+(defcustom pearl-hide-preamble t
+ "When non-nil, collapse the `#+' keyword preamble to a one-line summary.
+A display overlay replaces the `#+' block atop each Linear file with a styled
+\"Pearl - <view> · <count> · <time>\" line in `pearl-preamble-summary' face.
+The buffer text is untouched, so refresh and parsing still work; the overlay
+only governs display. Set to nil to show the raw `#+' keyword lines."
+ :type 'boolean
+ :group 'pearl)
+
+(defface pearl-preamble-summary
+ '((t :inherit org-document-title :foreground "goldenrod"))
+ "Face for the one-line summary that replaces the hidden `#+' preamble block.
+Inherits `org-document-title' for the large title look; override via Customize
+\(e.g. a different colour for a light theme)."
+ :group 'pearl)
+
(defcustom pearl-title-case-headings nil
"When non-nil, render issue titles in the heading in smart title case.
The default is nil: titles render verbatim, exactly as Linear stores them, so
@@ -3618,7 +3634,7 @@ only add the ones a failed team fetch left out."
(defvar pearl-keymap-prefix)
(defun pearl--help-header-string ()
- "Return the folded \"* Pearl\" heading shown atop a Linear file.
+ "Return the folded \"* Pearl Help\" heading shown atop a Linear file.
A collapsible section (`:VISIBILITY: folded') carrying the edit-then-save
reminder, a note that the section is local-only, the keymap cues, the
priority-cookie legend, and how to hide it. The cues reflect the live
@@ -3628,7 +3644,7 @@ heading carries no `LINEAR-ID', so save and merge skip it (they walk only
`LINEAR-ID'-bearing subtrees -- see `pearl--issue-subtree-markers')."
(let ((p pearl-keymap-prefix))
(concat
- "* Pearl\n"
+ "* Pearl Help\n"
":PROPERTIES:\n"
":VISIBILITY: folded\n"
":END:\n"
@@ -3642,6 +3658,59 @@ heading carries no `LINEAR-ID', so save and merge skip it (they walk only
"Priority cookies: [#A] Urgent, [#B] High, [#C] Medium, [#D] Low, no cookie = None.\n"
"Hide this section: set pearl-help-header to nil (M-x customize-variable RET pearl-help-header).\n")))
+(defun pearl--preamble-region ()
+ "Return (BEG . END) for the `#+' keyword preamble of the current buffer.
+BEG is `point-min'; END is the start of the first heading. nil when the buffer
+has no heading."
+ (save-excursion
+ (goto-char (point-min))
+ (when (re-search-forward "^\\*+ " nil t)
+ (cons (point-min) (match-beginning 0)))))
+
+(defun pearl--preamble-keyword (keyword)
+ "Return the value of file KEYWORD (e.g. \"LINEAR-COUNT\") in the buffer, or nil."
+ (save-excursion
+ (goto-char (point-min))
+ (when (re-search-forward (format "^#\\+%s: \\(.*\\)$" (regexp-quote keyword))
+ nil t)
+ (string-trim (match-string 1)))))
+
+(defun pearl--preamble-summary ()
+ "Compose the one-line preamble summary for the current Linear buffer.
+\"Pearl - <view> · <count> · <time>\", read from `#+LINEAR-SOURCE' (its name),
+`#+LINEAR-COUNT', and the time portion of `#+LINEAR-RUN-AT'. Missing fields are
+omitted. Leads with \"Pearl\" so the line doubles as the buffer's identity."
+ (let* ((src (pearl--read-active-source))
+ (name (or (and src (plist-get src :name)) "Linear"))
+ (count (pearl--preamble-keyword "LINEAR-COUNT"))
+ (runat (pearl--preamble-keyword "LINEAR-RUN-AT"))
+ (time (and runat (car (last (split-string runat))))))
+ (concat "Pearl - " name
+ (and count (format " · %s" count))
+ (and time (format " · %s" time)))))
+
+(defun pearl--hide-preamble (&optional buffer)
+ "Collapse the `#+' keyword preamble of BUFFER to a styled one-line summary.
+When `pearl-hide-preamble' is non-nil, lay (or refresh) a single overlay over
+the preamble region (`pearl--preamble-region') whose `display' is
+`pearl--preamble-summary' in `pearl-preamble-summary' face. Idempotent: removes
+any prior pearl overlay first, so re-rendering never stacks duplicates; when the
+option is nil it just removes it. Buffer text is untouched, so parsing and the
+merge rewrites keep working."
+ (with-current-buffer (or buffer (current-buffer))
+ (dolist (o (overlays-in (point-min) (point-max)))
+ (when (overlay-get o 'pearl-preamble)
+ (delete-overlay o)))
+ (when pearl-hide-preamble
+ (let ((region (pearl--preamble-region)))
+ (when region
+ (let ((ov (make-overlay (car region) (cdr region))))
+ (overlay-put ov 'pearl-preamble t)
+ (overlay-put ov 'evaporate t)
+ (overlay-put ov 'display
+ (propertize (concat (pearl--preamble-summary) "\n")
+ 'face 'pearl-preamble-summary))))))))
+
(defun pearl--build-org-content (issues &optional source truncated states account)
"Build the Org content string for the linear org file from ISSUES.
SOURCE is the active-source descriptor recorded in the header so a later
@@ -3750,7 +3819,8 @@ just writing the file and leaving it off-screen."
(save-buffer)
(goto-char (min recorded-point (point-max)))
(pearl--restore-page-visibility)
- (pearl-highlight-comments)))
+ (pearl-highlight-comments)
+ (pearl--hide-preamble)))
(message "Updated Linear issues in %s with %d active issues"
org-file-path (length issues))
(pearl--surface-buffer existing-buf))
@@ -3768,7 +3838,8 @@ just writing the file and leaving it off-screen."
(save-buffer)
(goto-char (point-min))
(pearl--restore-page-visibility)
- (pearl-highlight-comments)))
+ (pearl-highlight-comments)
+ (pearl--hide-preamble)))
(message "Updated Linear issues in %s with %d active issues"
org-file-path (length issues))
(pearl--surface-buffer existing-buf))
@@ -3778,6 +3849,7 @@ just writing the file and leaving it off-screen."
(pearl--update-source-header (length issues) truncated)
(pearl--update-derived-todo-header (pearl--gather-header-states issues))
(pearl-highlight-comments)
+ (pearl--hide-preamble)
;; Fold only the subtrees the merge re-rendered or appended; leave
;; the rest of the page (and point) as the user had them, so an
;; edit-then-merge flow doesn't collapse the subtree they were
@@ -6962,9 +7034,14 @@ in already-open buffers."
Binds `pearl-prefix-map' under `pearl-keymap-prefix' so every Pearl command is
reachable from the keyboard. Turns on automatically in any buffer Pearl
rendered (one carrying a `#+LINEAR-SOURCE' header); see
-`pearl--maybe-enable-mode' on `org-mode-hook'."
+`pearl--maybe-enable-mode' on `org-mode-hook'.
+On enable it also collapses the `#+' keyword preamble (see
+`pearl--hide-preamble'), so opening a Linear file hides it the same way a fresh
+fetch does."
:lighter (:eval (pearl--mode-line-lighter))
- :keymap pearl-mode-map)
+ :keymap pearl-mode-map
+ (when pearl-mode
+ (pearl--hide-preamble)))
(defun pearl--buffer-is-pearl-p ()
"Non-nil when the current buffer is a Pearl-rendered Linear file.
diff --git a/tests/test-pearl-output.el b/tests/test-pearl-output.el
index 7edc29d..8b02fcb 100644
--- a/tests/test-pearl-output.el
+++ b/tests/test-pearl-output.el
@@ -53,7 +53,7 @@
(should (string-match-p "^#\\+LINEAR-COUNT: 0$" out))
;; the help header (default on) carries the cues now; the raw `#' affordance
;; comments are gone
- (should (string-match-p "^\\* Pearl$" out))
+ (should (string-match-p "^\\* Pearl Help$" out))
(should-not (string-match-p "^# " out))))
(ert-deftest test-pearl-build-org-content-source-roundtrips ()
@@ -89,7 +89,7 @@ independent of `pearl-title-case-headings' (which governs issue headings only)."
edit-then-save reminder, key cues, the priority legend, and how to hide it.
It has no LINEAR-ID, so save and merge skip it."
(let ((s (pearl--help-header-string)))
- (should (string-match-p "^\\* Pearl$" s))
+ (should (string-match-p "^\\* Pearl Help$" s))
(should (string-match-p "^:VISIBILITY: folded$" s))
(should (string-match-p "save" s)) ; edit-then-save / key cue
(should (string-match-p "\\[#A\\] Urgent" s)) ; priority legend ends...
@@ -113,7 +113,7 @@ instead of printing a bogus chord."
raw `#' affordance comments are not."
(let* ((pearl-help-header t)
(out (pearl--build-org-content '() '(:type filter :name "X" :filter nil))))
- (should (string-match-p "^\\* Pearl$" out))
+ (should (string-match-p "^\\* Pearl Help$" out))
(should (string-match-p "^:VISIBILITY: folded$" out))
(should-not (string-match-p "^# " out))))
@@ -126,6 +126,63 @@ appear -- a minimal preamble of just the machine keywords."
(should-not (string-match-p "^# " out))
(should (string-match-p "^#\\+title:" out))))
+;;; --hide-preamble overlay (`pearl-hide-preamble')
+
+(defun test-pearl-output--sample-content ()
+ "Render a sample Linear buffer string for the overlay tests."
+ (pearl--build-org-content
+ '((:id "i1" :identifier "ENG-1" :title "one" :priority 1
+ :state (:name "In Progress") :team (:id "t1")))
+ '(:type filter :name "My open work" :filter (:assignee :me :open t))))
+
+(ert-deftest test-pearl-preamble-summary-face-defined ()
+ "The summary face exists for styling the collapsed preamble."
+ (should (facep 'pearl-preamble-summary)))
+
+(ert-deftest test-pearl-preamble-region-spans-keywords ()
+ "`pearl--preamble-region' covers point-min through the first heading."
+ (with-temp-buffer
+ (insert (test-pearl-output--sample-content))
+ (org-mode)
+ (let ((region (pearl--preamble-region)))
+ (should region)
+ (should (= (car region) (point-min)))
+ (should (string-prefix-p "* " (buffer-substring-no-properties
+ (cdr region) (+ (cdr region) 2)))))))
+
+(ert-deftest test-pearl-preamble-summary-composes-fields ()
+ "The summary reads the view name, count, and run-at time from the buffer."
+ (with-temp-buffer
+ (insert (test-pearl-output--sample-content))
+ (org-mode)
+ (goto-char (point-min))
+ (let ((s (pearl--preamble-summary)))
+ (should (string-prefix-p "Pearl - My open work" s))
+ (should (string-match-p " · 1" s)) ; one rendered issue
+ (should (string-match-p "·" s)))))
+
+(ert-deftest test-pearl-hide-preamble-lays-one-tagged-overlay ()
+ "With the option on, one tagged display overlay covers the preamble; applying
+twice keeps exactly one (idempotent); off removes it. Buffer text is untouched."
+ (cl-flet ((pearl-ovs () (seq-filter (lambda (o) (overlay-get o 'pearl-preamble))
+ (overlays-in (point-min) (point-max)))))
+ (let ((pearl-hide-preamble t))
+ (with-temp-buffer
+ (insert (test-pearl-output--sample-content))
+ (org-mode)
+ (let ((text-before (buffer-string)))
+ (pearl--hide-preamble)
+ (should (= 1 (length (pearl-ovs))))
+ (let ((ov (car (pearl-ovs))))
+ (should (overlay-get ov 'display))
+ (should (string-match-p "Pearl - My open work" (overlay-get ov 'display))))
+ (pearl--hide-preamble) ; idempotent
+ (should (= 1 (length (pearl-ovs))))
+ (should (string-equal text-before (buffer-string))) ; text untouched
+ (let ((pearl-hide-preamble nil)) ; off removes it
+ (pearl--hide-preamble)
+ (should (= 0 (length (pearl-ovs))))))))))
+
;;; --read-active-source
(ert-deftest test-pearl-read-active-source-absent ()