diff options
| author | Craig Jennings <c@cjennings.net> | 2026-05-24 13:56:47 -0500 |
|---|---|---|
| committer | Craig Jennings <c@cjennings.net> | 2026-05-24 13:56:47 -0500 |
| commit | 6cc8eb668fd994655ad951bbdc2cb87a6b8e8a35 (patch) | |
| tree | 4f663c202ac189588b5f7c177253d3747965689f | |
| parent | b081d62276378b3168c92c06153fd59db0589535 (diff) | |
| download | pearl-6cc8eb668fd994655ad951bbdc2cb87a6b8e8a35.tar.gz pearl-6cc8eb668fd994655ad951bbdc2cb87a6b8e8a35.zip | |
docs: restructure the README and repoint URLs to cjennings.net
I rewrote the README into a sectioned guide — a nav header over Features, Installation, Quick Start, Commands, The Org File, Configuration, Development & Testing, FAQ, Troubleshooting, History, and License — and repointed every repository URL off the deleted github.com/cjennings/pearl onto https://git.cjennings.net/pearl.git. The repoint also covers the URL header in pearl.el, the Eask website-url, and the package-summary repo link. The upstream credit to Gael Blanchemain's linear-emacs is unchanged.
| -rw-r--r-- | Eask | 2 | ||||
| -rw-r--r-- | README.org | 506 | ||||
| -rw-r--r-- | package-summary.md | 2 | ||||
| -rw-r--r-- | pearl.el | 2 |
4 files changed, 274 insertions, 238 deletions
@@ -4,7 +4,7 @@ "1.0.0" "Linear.app integration") -(website-url "https://github.com/cjennings/pearl") +(website-url "https://git.cjennings.net/pearl.git") (keywords "tools") (package-file "pearl.el") @@ -1,335 +1,371 @@ -#+TITLE: pearl — Linear.app for Emacs -#+OPTIONS: toc:2 num:nil +* Pearl - Linear.app for Emacs and Org Mode -[[https://www.gnu.org/licenses/gpl-3.0][https://img.shields.io/badge/License-GPLv3-blue.svg]] +[[#features][Features]] | [[#installation][Installation]] | [[#quick-start][Quick Start]] | [[#commands][Commands]] | [[#configuration][Configuration]] | [[#development--testing][Development & Testing]] | [[#faq][FAQ]] | [[#history][History]] | [[#license][License]] -=pearl= is an integration between Emacs and [[https://linear.app][Linear.app]]: fetch, read, edit, and create Linear issues as org-mode entries without leaving Emacs. Issues render as an org outline; their descriptions and comments live in the entry body, structured fields live in a property drawer, and dedicated commands push changes back to Linear. +[[https://www.gnu.org/licenses/gpl-3.0][https://img.shields.io/badge/License-GPLv3-blue.svg]] -* Features +Pearl brings Linear issues into Emacs as a working Org file. Fetch your open issues, a project, a Linear Custom View, an ad-hoc filter, or a saved local query; Pearl renders each issue as an Org heading with the description and comments in the body and Linear metadata in a namespaced property drawer. Edit what you need, then push it back explicitly with conflict-aware commands. -- *Fetch what you want.* List your open issues, narrow by project, build an ad-hoc filter interactively, run a Linear Custom View, or run a named local saved query. -- *Issues as readable org.* Each issue is a heading; its description renders in the body (markdown converted to org), its comments render as a chronological sub-thread, and its structured fields (state, priority, assignee, labels, team, project) live in a namespaced =LINEAR-*= drawer. -- *Edit and push back.* Edit a description in the body and sync it; change the title; set priority, state, assignee, or labels by command; add a comment, or edit one of your own. Each push is explicit and confirmed against the remote. -- *Conflict-aware sync.* Pushing a description or title compares the local edit, the last-fetched baseline, and the current remote — a no-op sends nothing, a clean edit pushes, and a both-sides-changed case is refused and reported rather than clobbering. -- *Self-describing active file.* The generated file records the source it came from, so one command refreshes it; refresh a single issue at point, or the whole view. -- *Org TODO state sync.* Changing an issue's TODO keyword in org pushes the matching Linear workflow state. -- *One menu for everything.* =M-x pearl-menu= opens a transient dispatcher (magit-style) with every command grouped and a key away. +** Features +:PROPERTIES: +:CUSTOM_ID: features +:END: -* Installation +- Fetch open issues, project issues, server-side Linear Custom Views, ad-hoc filters, or named local saved queries +- Read issues as an Org outline: title as heading, description in the body, comments as a chronological subtree +- Keep structured fields in =LINEAR-*= properties: id, URL, team, state, priority, assignee, labels, timestamps, and sync hashes +- Edit descriptions, titles, and your own comments in place, then push them with a three-way conflict check +- Set priority, state, assignee, and labels by command, using Linear ids behind display-name completion +- Add comments, create issues, delete issues, and open the current issue or view in Linear +- Refresh the active view from the source recorded in the file, or refresh one issue at point +- Optionally sync Org TODO keyword changes back to Linear workflow states +- Use one transient dispatcher, =M-x pearl-menu=, for the whole command surface +- [[file:TESTING.org][Well-tested]] with isolated ERT files, request fixtures, and coverage support + +** Installation +:PROPERTIES: +:CUSTOM_ID: installation +:END: -** Prerequisites +Pearl requires Emacs 27.1+, Org, a Linear API key, and the =request=, =dash=, =s=, and =transient= packages. =transient= ships with Emacs 28+, and package managers handle the rest. -A Linear API key (Settings → Account → API → Personal API Keys) and the =request=, =dash=, =s=, and =transient= packages (=transient= ships with Emacs 28+). +*** MELPA -#+begin_src elisp -(require 'package) -(add-to-list 'package-archives '("melpa" . "https://melpa.org/packages/") t) -(package-initialize) -#+end_src +Pearl is not on MELPA yet. -** MELPA (coming soon) +*** package-vc-install (Emacs 29+) -#+begin_src -M-x package-install RET pearl RET +#+begin_src emacs-lisp + (unless (package-installed-p 'pearl) + (package-vc-install "https://git.cjennings.net/pearl.git")) #+end_src -** Manual - -#+begin_src shell -git clone https://github.com/cjennings/pearl.git +*** use-package with =:vc= (Emacs 29+) + +#+begin_src emacs-lisp + (use-package pearl + :vc (:url "https://git.cjennings.net/pearl.git" :rev :newest) + :commands (pearl-menu pearl-list-issues pearl-run-view pearl-new-issue) + :bind ("C-c L" . pearl-menu) + :custom + (pearl-org-file-path (expand-file-name "gtd/linear.org" org-directory)) + :config + (pearl-load-api-key-from-env)) #+end_src -#+begin_src elisp -(add-to-list 'load-path "/path/to/pearl") -(require 'pearl) -;; dependencies, if not already present: -;; M-x package-install RET request RET -;; M-x package-install RET dash RET -;; M-x package-install RET s RET +*** Straight + +#+begin_src emacs-lisp + (straight-use-package + '(pearl :type git :repo "https://git.cjennings.net/pearl.git")) #+end_src -** Doom Emacs +*** Doom Emacs In =packages.el=: -#+begin_src elisp -(package! pearl - :recipe (:host github :repo "cjennings/pearl" :files ("*.el"))) +#+begin_src emacs-lisp + (package! pearl + :recipe (:type git :repo "https://git.cjennings.net/pearl.git" :files ("*.el"))) #+end_src In =config.el=: -#+begin_src elisp -(use-package! pearl - :commands (pearl-list-issues pearl-new-issue - pearl-run-view pearl-enable-org-sync) - :init - (setq pearl-org-file-path (expand-file-name "gtd/linear.org" org-directory)) - :config - (pearl-load-api-key-from-env)) +#+begin_src emacs-lisp + (use-package! pearl + :commands (pearl-menu pearl-list-issues pearl-new-issue + pearl-run-view pearl-enable-org-sync) + :init + (setq pearl-org-file-path (expand-file-name "gtd/linear.org" org-directory)) + :config + (pearl-load-api-key-from-env)) #+end_src -* Configuration - -** The API key - -Three ways, least to most secure: - -#+begin_src elisp -;; 1. Direct (not recommended -- ends up in your config). -(setq pearl-api-key "lin_api_...") +*** Manual -;; 2. Environment variable LINEAR_API_KEY. -(pearl-load-api-key-from-env) +#+begin_src bash + git clone https://git.cjennings.net/pearl.git #+end_src -3. =auth-source= (recommended). Add to =~/.authinfo.gpg=: - -#+begin_src -machine api.linear.app login apikey password YOUR_API_KEY +#+begin_src emacs-lisp + (add-to-list 'load-path "/path/to/pearl") + (require 'pearl) #+end_src -then load it: - -#+begin_src elisp -(setq pearl-api-key - (auth-source-pick-first-password :host "api.linear.app")) -#+end_src +Install =request=, =dash=, and =s= first if your package manager did not already pull them in. -The host (=api.linear.app=) and login (=apikey=) must match the entry. macOS Keychain users can instead run =security add-generic-password -a apikey -s api.linear.app -w YOUR_API_KEY=. +** Quick Start +:PROPERTIES: +:CUSTOM_ID: quick-start +:END: -** Output file +1. Create a Linear API key from Linear's Settings -> Account -> API -> Personal API Keys. -Issues are written to one active file, =pearl-org-file-path= (default =gtd/linear.org= under =org-directory=): +2. Put it somewhere Pearl can read it. The simplest development setup is: -#+begin_src elisp -(setq pearl-org-file-path (expand-file-name "gtd/linear.org" org-directory)) -#+end_src + #+begin_src sh + export LINEAR_API_KEY=lin_api_... + #+end_src -Running a different query or view *replaces* this file's contents (behind a dirty-buffer guard); one issue appears in one place. + Then load it from Emacs: -** Default team + #+begin_src emacs-lisp + (pearl-load-api-key-from-env) + #+end_src -#+begin_src elisp -(setq pearl-default-team-id "your-team-id") ; skips the team prompt on create -#+end_src + For normal use, keep the key in =auth-source= instead: -* The command menu + #+begin_example + machine api.linear.app login apikey password YOUR_API_KEY + #+end_example -=M-x pearl-menu= opens a transient dispatcher with every command grouped by what it does: fetch, view, the issue at point, create and org-sync, and setup. It's the fastest way to reach a command without remembering its name. Bind it to a key if you use it often: + #+begin_src emacs-lisp + (setq pearl-api-key + (auth-source-pick-first-password :host "api.linear.app")) + #+end_src -#+begin_src elisp -(with-eval-after-load 'pearl - (global-set-key (kbd "C-c L") #'pearl-menu)) -#+end_src +3. Choose the Org file Pearl owns: -Every command below is also available directly via =M-x=. + #+begin_src emacs-lisp + (setq pearl-org-file-path (expand-file-name "gtd/linear.org" org-directory)) + #+end_src -* The active org file +4. Run =M-x pearl-menu=, or start with =M-x pearl-list-issues=. -A fetched file carries a self-describing header and one heading per issue: +Pearl writes one active Org file. Running a different query or view replaces that file's contents after checking for dirty buffers. Refresh commands reuse the source stored in the file header. -#+begin_src org -#+title: Linear — My open issues -#+STARTUP: show3levels -#+TODO: TODO IN-PROGRESS IN-REVIEW BACKLOG BLOCKED | DONE -#+LINEAR-SOURCE: (:type filter :name "My open issues" :filter (:assignee :me :open t)) -#+LINEAR-RUN-AT: 2026-05-23 19:30 -#+LINEAR-FILTER: assignee: me, open -#+LINEAR-COUNT: 12 -#+LINEAR-TRUNCATED: no -# -# Body = the issue description; edit it, then M-x pearl-sync-current-issue to push. -# Comments subtree = the thread; add with M-x pearl-add-comment. -# Drawer fields change via M-x pearl-set-priority / -state / -assignee / -labels. -# Refresh with M-x pearl-refresh-current-view (whole file) or -current-issue (one). - -* My open issues -** TODO [#B] ENG-123 Issue title +** Commands :PROPERTIES: -:LINEAR-ID: <uuid> -:LINEAR-IDENTIFIER: ENG-123 -:LINEAR-URL: https://linear.app/.../ENG-123 -:LINEAR-TEAM-ID: <id> -:LINEAR-TEAM-NAME: Engineering -:LINEAR-PROJECT-NAME: Platform -:LINEAR-STATE-NAME: In Progress -:LINEAR-ASSIGNEE-NAME: Craig -:LINEAR-LABELS: [bug, p1] -:LINEAR-DESC-SHA256: <hash of the last-fetched description markdown> -:LINEAR-DESC-ORG-SHA256: <hash of the rendered Org body> -:LINEAR-DESC-UPDATED-AT: <remote timestamp> -:LINEAR-TITLE-SHA256: <hash of the rendered title> +:CUSTOM_ID: commands :END: -The issue description renders here as org, edited in place. - -*** Comments -**** Author Name — 2026-05-23T10:00:00.000Z -A comment, oldest first. -#+end_src - -Every issue nests under one top-level heading named after the view (here =My open issues=), so you can sort the whole set with =C-c ^= on that parent. The =#+LINEAR-SOURCE:= line records what produced the file so =refresh-current-view= can re-run it. The =LINEAR-*= drawer stores ids and display names separately; commands show names and mutate by id, so there is no per-render network lookup. The =SHA256= properties are provenance for the sync conflict gate. +*** Command menu -* Fetching issues +=M-x pearl-menu= opens a transient dispatcher with commands grouped by fetch, view, issue-at-point, creation, Org sync, and setup. Bind that command if you use Pearl regularly: -| Command | What it fetches | -|---------+-----------------| -| =pearl-list-issues= | your open issues | -| =pearl-list-issues-by-project= | open issues in a chosen project | -| =pearl-list-issues-filtered= | an ad-hoc filter built interactively | -| =pearl-run-view= | a Linear Custom View, run server-side | -| =pearl-run-saved-query= | a named local saved query | -| =pearl-refresh-current-view= | re-runs the file's recorded source | -| =pearl-refresh-current-issue= | re-fetches the issue at point | - -=list-issues-filtered= picks a team (which scopes the rest), then completes the state, project, and labels from that team's actual values rather than free text, so a typo can't silently return nothing. It can save the filter as a local query. - -** Saved queries - -Name local queries in =pearl-saved-queries=, then run them with =pearl-run-saved-query=: - -#+begin_src elisp -(setq pearl-saved-queries - '(("My open work" :filter (:assignee :me :open t) :sort updated :order desc) - ("Open bugs" :filter (:labels ("bug") :open t) :sort priority :order asc))) +#+begin_src emacs-lisp + (global-set-key (kbd "C-c L") #'pearl-menu) #+end_src -Each entry is a filter plist plus optional =:sort= (=updated=, =priority=, or =title=) and =:order= (=asc= / =desc=). Sorting happens after fetch, so a refresh always lays headings out the same way. Queries are AND-only; for OR logic, build a Linear Custom View and run it with =run-view=. - -* Editing issues - -All issue commands work from anywhere inside an issue's subtree. +Every command below is also available directly through =M-x=. -** Description and title +*** Fetching and refreshing -Edit the description in the body, then =M-x pearl-sync-current-issue=. The push is gated: unchanged since fetch sends nothing; a local edit against an unchanged remote pushes; if the remote also changed since the fetch the push is refused and the conflict reported (refresh to reconcile). =pearl-sync-current-issue-title= does the same for the heading title (note: square brackets are stripped from titles, so a synced title drops them). +| Command | What it does | +|------------------------------+--------------------------------------------------| +| =pearl-list-issues= | Fetch your open issues | +| =pearl-list-issues-by-project= | Fetch open issues in a chosen project | +| =pearl-list-issues-filtered= | Build an ad-hoc issue filter interactively | +| =pearl-run-view= | Run a Linear Custom View server-side | +| =pearl-run-saved-query= | Run a named local query from =pearl-saved-queries= | +| =pearl-refresh-current-view= | Re-run the source recorded in the active file | +| =pearl-refresh-current-issue= | Re-fetch the issue at point | -** Fields +Ad-hoc filtering starts with a team, then completes states, projects, and labels from that team's actual Linear values. Saved queries are local Lisp data: -| Command | Effect | -|---------+--------| -| =pearl-set-priority= | None / Urgent / High / Medium / Low | -| =pearl-set-state= | a workflow state from the issue's team | -| =pearl-set-assignee= | a team member | -| =pearl-set-labels= | team labels (empty selection clears) | +#+begin_src emacs-lisp + (setq pearl-saved-queries + '(("My open work" :filter (:assignee :me :open t) :sort updated :order desc) + ("Open bugs" :filter (:labels ("bug") :open t) :sort priority :order asc))) +#+end_src -Each completes by display name, resolves to the Linear id, pushes, and updates the drawer. +Sorting is local and deterministic. Query filters are AND-only; use a Linear Custom View for OR-heavy logic. -** Comments +*** Editing issues -Comments render oldest-first under a =*** Comments= subtree, in both the bulk list and a single-issue refresh. =pearl-add-comment= posts a new comment and appends it. +All issue commands work from anywhere inside an issue subtree. -You can also edit your *own* comments. Edit a comment's body in place, then =M-x pearl-edit-current-comment= from inside its subtree. The push is gated like the description sync: unchanged sends nothing, a clean edit pushes, and a both-sides-changed case is refused (refresh to reconcile). Matching Linear's permissions, only comments you authored are editable — others (and bot or integration comments) are refused without a network call. Editability is shown by color: your own comments render green, everyone else's render greyed. Run =M-x pearl-highlight-comments= to recolor a buffer by hand. +| Command | What it changes | +|-----------------------------------+--------------------------------------------------| +| =pearl-sync-current-issue= | Push the edited description body | +| =pearl-sync-current-issue-title= | Push the edited heading title | +| =pearl-set-priority= | Set None, Urgent, High, Medium, or Low | +| =pearl-set-state= | Set a workflow state from the issue's team | +| =pearl-set-assignee= | Set a team member as assignee | +| =pearl-set-labels= | Replace labels; empty selection clears them | +| =pearl-add-comment= | Add a new Linear comment | +| =pearl-edit-current-comment= | Push edits to one of your own comments | +| =pearl-delete-current-issue= | Soft-delete the current issue after confirmation | +| =pearl-open-current-issue= | Open the issue URL in a browser | +| =pearl-open-current-view-in-linear= | Open the active view in Linear | -** Other commands +Description, title, and comment pushes use the same conflict gate: unchanged local text sends nothing; a local edit against an unchanged remote pushes; if both local and remote changed since fetch, Pearl refuses to clobber either side and reports the conflict. Destructive conflict choices stash local text in =*pearl-conflict-backup*= first. -| Command | Effect | -|---------+--------| -| =pearl-open-current-issue= | open the issue's URL in the browser | -| =pearl-open-current-view-in-linear= | open the active view's URL | -| =pearl-delete-current-issue= | delete the issue (confirms; soft delete to Trash) | -| =pearl-new-issue= | create an issue | -| =pearl-clear-cache= | clear the team/state/collection/view lookup caches | +Only comments you authored are editable. Pearl refuses edits to comments from another person, a bot, or an integration before making an API call. -* State mapping and org TODO sync +*** Org TODO sync -=pearl-state-to-todo-mapping= maps Linear workflow states to org TODO keywords for *rendering only* — it no longer decides which issues are fetched (inclusion is server-side via the filter). Default: +=pearl-state-to-todo-mapping= maps Linear state names to Org TODO keywords for rendering and optional state sync: -#+begin_src elisp -'(("Todo" . "TODO") - ("In Progress" . "IN-PROGRESS") - ("In Review" . "IN-REVIEW") - ("Backlog" . "BACKLOG") - ("Blocked" . "BLOCKED") - ("Done" . "DONE")) +#+begin_src emacs-lisp + (setq pearl-state-to-todo-mapping + '(("Todo" . "TODO") + ("In Progress" . "IN-PROGRESS") + ("In Review" . "IN-REVIEW") + ("Backlog" . "BACKLOG") + ("Blocked" . "BLOCKED") + ("Done" . "DONE"))) #+end_src -Customize it to match your workflow, and make sure your =org-todo-keywords= include every keyword you map to. With org sync enabled, changing an issue's TODO keyword pushes the corresponding Linear state: +Make sure your =org-todo-keywords= include every keyword you map to. Enable and disable the save/TODO-change hooks with: -#+begin_src elisp -(add-hook 'find-file-hook - (lambda () - (when (and buffer-file-name - (string-equal (file-truename buffer-file-name) - (file-truename pearl-org-file-path))) - (pearl-enable-org-sync)))) +#+begin_src emacs-lisp + (pearl-enable-org-sync) + (pearl-disable-org-sync) #+end_src -=pearl-enable-org-sync= / =pearl-disable-org-sync= toggle the save-and-TODO-change hooks for the active file. +** The Org File +:PROPERTIES: +:CUSTOM_ID: the-org-file +:END: -* Customization +A fetched Pearl file is intentionally readable. The header records the source, run time, filter summary, count, and whether pagination truncated the result. Issues sit under one top-level view heading: -| Variable | Purpose | -|----------+---------| -| =pearl-org-file-path= | the active output file | -| =pearl-state-to-todo-mapping= | Linear state ↔ org keyword (render/sync) | -| =pearl-saved-queries= | named local queries | -| =pearl-default-team-id= | default team for issue creation | -| =pearl-max-issue-pages= | pagination cap (default 10) | -| =pearl-request-timeout= | API request timeout (seconds) | -| =pearl-fold-after-update= | re-fold the page after a fetch/refresh (default on) | -| =pearl-surface-buffer= | bring the active buffer to a window after a command (default on) | -| =pearl-surface-select-window= | also move focus to that window (default off) | -| =pearl-debug= | enable debug logging | +#+begin_src org + #+title: Linear - My open issues + #+STARTUP: show3levels + #+TODO: TODO IN-PROGRESS IN-REVIEW BACKLOG BLOCKED | DONE + #+LINEAR-SOURCE: (:type filter :name "My open issues" :filter (:assignee :me :open t)) + #+LINEAR-RUN-AT: 2026-05-23 19:30 + #+LINEAR-COUNT: 12 + #+LINEAR-TRUNCATED: no + + * My open issues + ** TODO [#B] ENG-123 Issue title + :PROPERTIES: + :LINEAR-ID: <uuid> + :LINEAR-IDENTIFIER: ENG-123 + :LINEAR-URL: https://linear.app/.../ENG-123 + :LINEAR-STATE-NAME: In Progress + :LINEAR-ASSIGNEE-NAME: Craig + :LINEAR-LABELS: [bug, p1] + :LINEAR-DESC-SHA256: <last-fetched markdown hash> + :LINEAR-DESC-ORG-SHA256: <rendered Org body hash> + :END: + + The issue description renders here as Org and can be edited in place. + + *** Comments + **** Author Name - 2026-05-23T10:00:00.000Z + A comment, oldest first. +#+end_src -* FAQ +The =LINEAR-*= properties store both ids and display names so common commands do not need a network lookup just to render. The hash properties are provenance for conflict-aware sync. -** I edited the title in the heading and ran the description sync, but the title didn't change. +** Configuration +:PROPERTIES: +:CUSTOM_ID: configuration +:END: -Title and description push through separate commands. =pearl-sync-current-issue= pushes the body; the heading title is =pearl-sync-current-issue-title= (menu =t=). Fields and comments are their own commands too — each edit pushes independently. +Most users only need an API key and an output path. The rest are knobs for teams with large issue sets or stronger preferences about window behavior. + +| Variable | Purpose | +|-----------------------------+---------------------------------------------------| +| =pearl-api-key= | Linear API key | +| =pearl-org-file-path= | Active Org output file | +| =pearl-default-team-id= | Default team for issue creation | +| =pearl-saved-queries= | Named local issue queries | +| =pearl-state-to-todo-mapping= | Linear state to Org keyword mapping | +| =pearl-max-issue-pages= | Pagination cap, 100 issues per page | +| =pearl-request-timeout= | Synchronous request timeout in seconds | +| =pearl-fold-after-update= | Re-fold the active page after fetch/refresh | +| =pearl-surface-buffer= | Show the active buffer after a command updates it | +| =pearl-surface-select-window= | Move focus to the surfaced buffer | +| =pearl-debug= | Log request/response details to =*Messages*= | + +If a fetch stops at the pagination cap, Pearl writes =#+LINEAR-TRUNCATED: yes= in the file header. Raise =pearl-max-issue-pages= if your result set is larger than the default 1000 issues. + +** Development & Testing +:PROPERTIES: +:CUSTOM_ID: development--testing +:END: -** My title lost its square brackets after syncing. +Clone the repo and install the Eask-managed dependencies: -The renderer strips =[= and =]= from titles so org doesn't misparse them, so the heading holds the stripped form and a title push sends that. =Fix [URGENT] bug= becomes =Fix URGENT bug=. Keep brackets out of titles you mean to round-trip. +#+begin_src bash + git clone https://git.cjennings.net/pearl.git + cd pearl + make setup +#+end_src -** A markdown heading or =*italic*= in a description turned into bold after I synced it. +Useful development targets: + +| Target | What it does | +|----------------------------------------+-------------------------------------------------------| +| =make test= | Run unit and integration tests, excluding =:slow= tests | +| =make test-all= | Run every test, including =:slow= tests | +| =make test-unit= | Run unit tests only | +| =make test-integration= | Run integration tests only | +| =make test-file FILE=mapping= | Run one test file by fuzzy match | +| =make test-one TEST=priority= | Run one test by fuzzy match | +| =make test-name TEST='test-pearl-map-*'= | Run tests matching an ERT selector | +| =make coverage= | Generate undercover/simplecov coverage data | +| =make compile= | Byte-compile =pearl.el= | +| =make lint= | Run the lint/checkdoc path from the test harness | +| =make validate= | Check parentheses across source and tests | +| =make clean= | Remove test artifacts and coverage output | + +Each test file runs in its own Emacs batch process for isolation. See [[file:TESTING.org][TESTING.org]] for the full test guide, naming conventions, fixture helpers, and coverage notes. + +** FAQ +:PROPERTIES: +:CUSTOM_ID: faq +:END: -Two markdown constructs are intentionally lossy on the round-trip: a markdown =# heading= renders as a bold line on fetch and pushes back as bold (it never returns to a heading), and single-asterisk =*italic*= is read as bold. Edit a description containing either and push, and it comes back rewritten. Use =**bold**= and real Org sub-structure deliberately; don't rely on md headings surviving. +*** I edited the heading title and synced the description. Why did the title stay the same? -** My comment edit was refused with "You can only edit your own comments." +Titles and descriptions push through separate commands. Use =pearl-sync-current-issue= for the body and =pearl-sync-current-issue-title= for the heading title. -By design — Pearl matches Linear's permissions. Only comments you authored are editable; another person's comment, or a bot or integration comment, is refused with no API call. Your own comments render green, everyone else's greyed. +*** Why did square brackets disappear from a synced title? -** A sync or comment edit was refused as a conflict. +Pearl strips =[= and =]= from titles before rendering so Org does not misparse them. A title like =Fix [URGENT] bug= round-trips as =Fix URGENT bug=. -The push is gated three ways: unchanged since fetch sends nothing; a local edit against an unchanged remote pushes; if the remote also changed since your last fetch, the push is refused and the conflict reported. Refresh to reconcile, or take the use-local / use-remote / merge prompt — any destructive choice stashes your local text in =*pearl-conflict-backup*= first, so nothing is lost. +*** Why did a Markdown heading or single-asterisk italic change after syncing? -** I hand-edited a drawer field (priority, state, assignee, labels) and it didn't push. +The Markdown-to-Org round trip is intentionally lossy for a few constructs. Markdown =# heading= renders as a bold line, and single-asterisk =*italic*= is read as bold. Use these constructs carefully in descriptions you plan to edit from Org. -=LINEAR-*= drawer fields change by command, not by hand: =pearl-set-priority=, =-state=, =-assignee=, =-labels=. Each resolves a display name to the Linear id, pushes, and rewrites the drawer. A value typed into the drawer by hand won't push and gets overwritten on the next refresh. +*** Why did Pearl refuse my comment edit? -** My fetch seems to be missing issues. +Linear only lets you edit comments you authored. Pearl checks that before pushing and refuses edits to other people's comments, bot comments, and integration comments. -The fetch is capped at =pearl-max-issue-pages= (default 10 pages × 100 = 1000 issues). A capped fetch sets =#+LINEAR-TRUNCATED: yes= in the header. Raise =pearl-max-issue-pages= if you're assigned more. +*** Why did Pearl refuse a description, title, or comment sync as a conflict? -** Names (team, state, assignee) are stale after I renamed them in Linear. +The local text and remote text both changed since the last fetch. Refresh to reconcile, or use the conflict prompt. Pearl stashes local text before any destructive resolution. -Pearl caches those lookups so it doesn't hit the network on every render. Run =M-x pearl-clear-cache=. +*** Why did a hand-edited drawer field get overwritten? -** The page collapses after every refresh — can I keep it expanded? +Drawer fields are generated metadata. Change priority, state, assignee, and labels through Pearl commands so display names can resolve to Linear ids correctly. -After a fetch or refresh Pearl re-folds the page to its =#+STARTUP= visibility (issue headings shown; descriptions, comments, and drawers folded), because =#+STARTUP= on its own only applies on a file's first visit. Set =pearl-fold-after-update= to nil to leave the buffer however you had it. +*** Why are renamed teams, states, labels, or assignees stale? -** Can I hide the property drawers completely, not just fold them? +Pearl caches Linear lookup tables. Run =M-x pearl-clear-cache=. -Pearl folds drawers with the rest of the page, but an expanded issue still shows the one-line =:PROPERTIES:= stub. For a tidier look, [[https://github.com/jxq0/org-tidy][org-tidy]] replaces each drawer with a small glyph. It composes with Pearl rather than fighting it: Pearl folds the outline (descriptions and comments) to a scannable list, and org-tidy collapses the drawers on top — even on an expanded issue. Editing still works, since org-tidy only adds display overlays and never touches the drawer text, and a Pearl fetch re-applies it cleanly. Turn it on with =M-x org-tidy-mode= (or =global-org-tidy-mode=); if a partial refresh ever leaves a drawer showing, =M-x org-tidy= re-tidies. +*** Can I keep the file expanded after refresh? -* Troubleshooting +Yes. Set =pearl-fold-after-update= to nil. -- =M-x pearl-test-connection= — check the API key and connectivity. -- =M-x pearl-check-setup= — confirm the key loaded. -- =M-x pearl-toggle-debug= (or =(setq pearl-debug t)=) — log requests and responses to =*Messages*=. -- Stale names after renaming things in Linear → =M-x pearl-clear-cache=. +** Troubleshooting +:PROPERTIES: +:CUSTOM_ID: troubleshooting +:END: -* Contributing +- =M-x pearl-check-setup= checks whether the API key is loaded. +- =M-x pearl-test-connection= checks API connectivity. +- =M-x pearl-toggle-debug= enables request/response logging in =*Messages*=. +- =M-x pearl-clear-cache= refreshes cached names after Linear-side changes. -Contributions are welcome — open an issue or a pull request. +** History +:PROPERTIES: +:CUSTOM_ID: history +:END: -* Acknowledgments +Pearl is based on and inspired by Gael Blanchemain's [[https://github.com/gael/linear-emacs][linear-emacs]]. The package has since grown into a separately maintained Linear workflow for Org, including broader fetch modes, editable descriptions and comments, conflict-aware sync, view refresh, command menus, and a dedicated test suite. -Based on and inspired by Gael Blanchemain's linear-emacs. +Bug reports, feature requests, and pull requests are welcome. -* License +** License +:PROPERTIES: +:CUSTOM_ID: license +:END: -GPL-3.0. See [[file:LICENSE][LICENSE]]. +GPL-3.0-or-later. See [[file:LICENSE][LICENSE]]. diff --git a/package-summary.md b/package-summary.md index 894840d..4f60a81 100644 --- a/package-summary.md +++ b/package-summary.md @@ -4,7 +4,7 @@ pearl integrates Linear.app issue tracking with Emacs and org-mode. It fetches y ### Direct link to the package repository -https://github.com/cjennings/pearl +https://git.cjennings.net/pearl.git ### Your association with the package @@ -6,7 +6,7 @@ ;; Version: 1.0.0 ;; Package-Requires: ((emacs "27.1") (request "0.3.0") (dash "2.17.0") (s "1.12.0") (transient "0.3.0")) ;; Keywords: tools -;; URL: https://github.com/cjennings/pearl +;; URL: https://git.cjennings.net/pearl.git ;; This file is not part of GNU Emacs. |
