diff options
Diffstat (limited to 'README.org')
| -rw-r--r-- | README.org | 88 |
1 files changed, 52 insertions, 36 deletions
@@ -7,14 +7,14 @@ " /We like lists because we don't want to die./ " — /Umberto Eco/ -Pearl (backronym: *Pearl Edits and Reflects Linear*) 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 in the buffer, then save the issue; Pearl reconciles each changed field against the remote with a conflict check. +Pearl (backronym: *Pearl Edits and Reflects Linear*) 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 local view; 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 in the buffer, then save the issue; Pearl reconciles each changed field against the remote with a conflict check. ** Features :PROPERTIES: :CUSTOM_ID: features :END: -- Fetch open issues, project issues, server-side Linear Custom Views, ad-hoc filters, or named local saved queries +- Fetch open issues, project issues, server-side Linear Custom Views, ad-hoc filters, or named local views - 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 any field -- title, description, priority, state, assignee, labels, your own comments -- in the buffer, then save; Pearl reconciles each change through a conflict gate @@ -49,7 +49,7 @@ Pearl is not on MELPA yet. #+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-create-issue) + :commands (pearl-menu pearl-list-issues pearl-run-linear-view pearl-create-issue) :bind ("C-c L" . pearl-menu) :custom (pearl-org-file-path (expand-file-name "gtd/linear.org" org-directory)) @@ -78,7 +78,7 @@ In =config.el=: #+begin_src emacs-lisp (use-package! pearl :commands (pearl-menu pearl-list-issues pearl-create-issue - pearl-run-view pearl-save-issue) + pearl-run-linear-view pearl-save-issue) :init (setq pearl-org-file-path (expand-file-name "gtd/linear.org" org-directory)) :config @@ -136,7 +136,7 @@ Install =request=, =dash=, and =s= first if your package manager did not already 4. Run =M-x pearl-menu=, or start with =M-x pearl-list-issues=. -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. +Pearl writes one active Org file. Running a different view or filter replaces that file's contents after checking for dirty buffers. Refresh commands reuse the source stored in the file header. ** Commands :PROPERTIES: @@ -145,7 +145,7 @@ Pearl writes one active Org file. Running a different query or view replaces tha *** Command menu -=M-x pearl-menu= opens a transient dispatcher. Issue actions are grouped as save, edit, create, and delete; workspace actions as fetch, view, and setup. Bind that command if you use Pearl regularly: +=M-x pearl-menu= opens a transient dispatcher. Issue actions are grouped as save, edit, create, and delete; workspace actions as fetch, views, buffer, and setup. Bind that command if you use Pearl regularly: #+begin_src emacs-lisp (global-set-key (kbd "C-c L") #'pearl-menu) @@ -169,10 +169,10 @@ The hot-path commands sit one key under the prefix; the rest are grouped into su | =s= / =S= | save the issue at point / save every issue in the file | | =d= | edit the description | | =m= | open the full transient menu | -| =f= ... | fetch: =s= pick source, =o= open issues, =p= by project, =f= filter, =v= view, =q= saved query, =S= sync saved query to Linear, =P= publish current source | -| =e= ... | edit: =d= description, =s= state, =a= assignee, =l= labels, =c= comment | -| =c= ... | create: =t= issue, =c= comment | -| =k= ... | delete: =t= issue, =c= comment, =q= saved query | +| =f= ... | fetch: =s= pick source, =o= open issues, =p= by project, =f= filter, =l= run local view, =v= run Linear view, =u= publish, =d= save Linear view locally, =P= publish current | +| =e= ... | edit: =d= description, =s= state, =a= assignee, =l= labels, =c= comment, =v= local view | +| =c= ... | create: =t= issue, =c= comment, =v= local view | +| =k= ... | delete: =t= issue, =c= comment, =v= local view | | =o= ... | open: =i= issue in browser, =v= view in Linear | | =y= ... | copy: =u= issue URL | @@ -186,36 +186,36 @@ With =which-key=, each step shows a labeled menu. Every command is also availabl *** Sources -A *source* is anything Pearl can fetch from: a Linear favorite, a Custom View, a project / cycle / label / user, a local saved query, or an ad-hoc filter. The everyday front door is =pearl-pick-source= (=C-; L f s=, or =P= in the transient), which lists all your Linear favorites first (in their Linear sort order) and then your local saved queries, each tagged by kind: +A *source* is anything Pearl can fetch from: a Linear favorite, a Custom View, a project / cycle / label / user, a local view, or an ad-hoc filter. The everyday front door is =pearl-pick-source= (=C-; L f s=, or =P= in the transient), which lists all your Linear favorites first (in their Linear sort order) and then your local views, each tagged by kind: #+begin_example [view] Active sprint bugs [project] Orchestration Dashboard [user] Vrezh Mikayelyan [label] security - [saved] My open work - [saved → Eng] Active sprint bugs (synced) - [saved → Personal] My ICEBOX scratchpad (synced) + [local] My open work + [local → Linear:Eng] Active sprint bugs (synced) + [local → Linear:Personal] My ICEBOX scratchpad (synced) #+end_example Pick one and it fetches. List-capable favorites (Custom View / project / cycle / label / user) resolve to the existing filter or view fetch and render into the active file; non-list favorites (issue, document, dashboard, ...) open in the browser instead. Favorites are picker entries, never persisted -- a chosen favorite resolves to a concrete filter/view source before rendering, so refresh re-runs that resolved source and stays stable even if your favorites list later changes. Label and user favorites resolve by id, not by name or email, so renames in Linear don't break a saved fetch. -A saved query you've synced up to Linear (see [[*Publishing a saved query as a Linear view]] below) renders as =[saved → <Team or Personal>] Name= so you can see at a glance which scope it lives in. The arrow reads "where it lives now." Picking it dispatches as a Linear view (server-side filter run via =customView(id:)=), not as a local filter -- any drift you've made to the view on Linear's side is honored. Plain =[saved]= entries are local-only and still run the authoring filter Pearl has on disk. =[saved → ?]= means the entry is synced but Pearl couldn't resolve the team id (deleted or renamed on Linear) -- the dispatch still works, the label is just flagging stale scope metadata. +A local view you've published to Linear (see [[*Publishing a local view as a Linear view]] below) renders as =[local → Linear:<Team or Personal>] Name= so you can see at a glance that it's published and to which scope. The arrow reads "this local view is mirrored there." The local view is the source of truth: picking it runs *your local filter*, not the Linear mirror, so editing the local view and running it shows your edits even before you publish them. To run the server-side Linear view instead, use =pearl-run-linear-view=. Plain =[local]= entries are local-only. =[local → Linear:?]= means the entry is published but Pearl couldn't resolve the team id (deleted or renamed on Linear) -- running still works, the label is just flagging stale scope metadata. -If the favorites fetch fails (network/auth/transport), the picker still offers any local saved queries -- a failed fetch is not the same as "no favorites." With neither favorites nor saved queries, =pearl-pick-source= refuses with a clear message naming the missing setup. +If the favorites fetch fails (network/auth/transport), the picker still offers any local views -- a failed fetch is not the same as "no favorites." With neither favorites nor local views, =pearl-pick-source= refuses with a clear message naming the missing setup. -*** Publishing a saved query as a Linear view +*** Publishing a local view as a Linear view -A local saved query is great for your own quick filters, but the team can't see it and the Linear web UI can't run it. =pearl-sync-saved-query-to-linear= (=C-; L f S=, or =S= in the transient's Fetch group) promotes a local saved query to a Linear Custom View so it's visible alongside your sidebar favorites and the rest of the team's views. +A local view is great for your own quick filters, but the team can't see it and the Linear web UI can't run it. =pearl-publish-local-view= (=C-; L f u=, or =u= in the transient's Views group) promotes a local view to a Linear Custom View so it's visible alongside your sidebar favorites and the rest of the team's views. This is the *copy up* direction of the lifecycle. Two commands publish: | Command | Binding | What it does | |------------------------------------+--------------------------------------+--------------------------------------------------------------------| -| =pearl-sync-saved-query-to-linear= | =C-; L f S=, =S= in the transient Fetch group | Pick a saved query by name and publish (or update) it on Linear | -| =pearl-publish-current-source= | =C-; L f P=, =U= in the transient Fetch group | Read the buffer's =#+LINEAR-SOURCE=; if it names a local saved query, publish that one | +| =pearl-publish-local-view= | =C-; L f u=, =u= in the transient Views group | Pick a local view by name and publish (or update) it on Linear | +| =pearl-publish-current-view= | =C-; L f P=, =U= in the transient Views group | Read the buffer's =#+LINEAR-SOURCE=; if it names a local view, publish that one | -The first time you sync a query, Pearl asks where the view should live with one enriched prompt that spells out the full end-state per option: +The first time you publish a local view, Pearl asks where the view should live with one enriched prompt that spells out the full end-state per option: #+begin_example Where does this view live? @@ -230,32 +230,48 @@ The first time you sync a query, Pearl asks where the view should live with one The default is the team in your filter's =:team= key (if any) shared to that team, else =[ Personal, only I see it ]=. If a view with the same name already exists in the chosen scope, Pearl prompts =Replace? Rename? Cancel?= -- Replace updates the existing view's filter by id (preserving its url and anyone's favorites on it), Rename re-prompts for a different name, Cancel aborts cleanly. -After a successful sync, the saved-query entry gains four metadata keys (=:linear-view-id=, =:linear-view-team-id=, =:linear-view-shared=, =:linear-view-synced-at=) plus =:linear-view-url= so =pearl-open-current-view-in-linear= can dispatch to the browser. Re-syncing the same query calls =customViewUpdate= against the stored id -- the view is overwritten with whatever your local filter says now. This is one-way push by design: the verb is "publish my version." If you edit the view on Linear's side and then re-sync from Pearl, your local plist wins. +After a successful publish, the local-view entry gains four metadata keys (=:linear-view-id=, =:linear-view-team-id=, =:linear-view-shared=, =:linear-view-synced-at=) plus =:linear-view-url= so =pearl-open-current-view-in-linear= can dispatch to the browser. Re-publishing the same local view calls =customViewUpdate= against the stored id -- the Linear view is overwritten with whatever your local filter says now. This is one-way push by design: the verb is "publish my version." If you edit the view on Linear's side and then re-publish from Pearl, your local plist wins. -Pearl's =:sort= / =:order= on a saved query don't sync up in v1 -- Linear's =CustomView= API has no sort input, so a synced view renders in whatever order Linear's defaults give it. If sort order is load-bearing for the view, set it in the Linear web UI after the first sync. +Pearl's =:sort= / =:order= on a local view don't publish up in v1 -- Linear's =CustomView= API has no sort input, so a published view renders in whatever order Linear's defaults give it. If sort order is load-bearing for the view, set it in the Linear web UI after the first publish. -=pearl-delete-saved-query= (=C-; L k q=) on a synced entry asks a second question after the local-delete confirmation: also delete the linked Linear view? Yes calls =customViewDelete=, which Linear handles as a soft delete (recoverable from the workspace trash). No unlinks the entry locally and leaves the Linear view in place. If the Linear delete fails (permissions, network), Pearl prompts to drop the local entry anyway -- accepting orphans the Linear view, and the message names the view id explicitly so you can clean it up by hand. +=pearl-delete-local-view= (=C-; L k v=) on a published entry asks a second question after the local-delete confirmation: also delete the linked Linear view? Yes calls =customViewDelete=, which Linear handles as a soft delete (recoverable from the workspace trash). No unlinks the entry locally and leaves the Linear view in place. If the Linear delete fails (permissions, network), Pearl prompts to drop the local entry anyway -- accepting orphans the Linear view, and the message names the view id explicitly so you can clean it up by hand. Under multiple accounts, publish and delete refuse a local view tagged to a non-active account before touching Linear. + +*** Saving a Linear view locally + +The other direction -- *copy down* -- forks a Linear view into a new, editable local view. =pearl-save-linear-view-locally= (=C-; L f d=, or =d= in the transient's Views group) lists your favorited Linear views, fetches the chosen view's filter, and reverse-compiles it into a Pearl authoring filter you can then edit and rename. The new local view is independent: it carries no tracking link, so editing or renaming it never touches the Linear view (publish it back later to push your changes up as a new Linear view). + +Linear stores a view's filter as an =and=/=or= tree that's richer than Pearl's AND-only local model. Copy-down works for any view inside that model -- a team / state / label / assignee filter, the common case. When a view uses something Pearl can't represent (OR logic, a label parent, a due-date filter, a multi-value filter on a single dimension), copy-down *refuses and names the construct* rather than silently saving a filter that matches a different set of issues; run such a view directly with =pearl-run-linear-view= instead. To copy down *all* Linear views (not just favorited ones) is a later addition; v1 lists favorites. + +*** Creating and editing local views + +| Command | Binding | What it does | +|------------------------------+----------------+------------------------------------------------------| +| =pearl-create-local-view= | =C-; L c v= | Build a filter interactively and save it as a local view | +| =pearl-edit-local-view= | =C-; L e v= | Edit a local view's filter, name, sort, and order, preserving its metadata | +| =pearl-save-linear-view-locally= | =C-; L f d= | Copy a favorited Linear view down into a new editable local view | + +=pearl-list-issues-filtered= also offers to save its ad-hoc filter as a local view at the end. Create, edit, and copy-down all prompt Replace / Rename / Cancel on a name collision, and under multiple accounts stamp the active account on a new view and refuse a cross-account edit. *** Fetching and refreshing | Command | What it does | |------------------------------+--------------------------------------------------| -| =pearl-pick-source= | Pick a Linear favorite or local saved query and fetch it | +| =pearl-pick-source= | Pick a Linear favorite or local view and fetch it | | =pearl-list-issues= | Fetch your open issues | | =pearl-list-issues-by-project= | Fetch every open issue in a chosen project (all assignees) | | =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-sync-saved-query-to-linear= | Publish (or update) a saved query as a Linear Custom View | -| =pearl-publish-current-source= | Publish the current buffer's saved query (reads =#+LINEAR-SOURCE=) | -| =pearl-delete-saved-query= | Delete a saved query (and optionally its linked Linear view) | +| =pearl-run-linear-view= | Run a Linear Custom View server-side | +| =pearl-run-local-view= | Run a named local view from =pearl-local-views= | +| =pearl-publish-local-view= | Publish (or update) a local view as a Linear Custom View | +| =pearl-publish-current-view= | Publish the current buffer's local view (reads =#+LINEAR-SOURCE=) | +| =pearl-delete-local-view= | Delete a local view (and optionally its linked Linear view) | | =pearl-refresh-current-view= | Re-run the source recorded in the active file | | =pearl-refresh-current-issue= | Re-fetch the issue at point | -Ad-hoc filtering starts with a team, then completes states, projects, and labels from that team's actual Linear values. The assignee prompt offers =me=, a specific =member= (resolved to a user id), or =any= (no scoping). Saved queries are local Lisp data: +Ad-hoc filtering starts with a team, then completes states, projects, and labels from that team's actual Linear values. The assignee prompt offers =me=, a specific =member= (resolved to a user id), or =any= (no scoping). Local views are local Lisp data: #+begin_src emacs-lisp - (setq pearl-saved-queries + (setq pearl-local-views '(("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 @@ -370,7 +386,7 @@ Most users only need an API key and an output path. The rest are knobs for teams | =pearl-default-team-id= | Default team for issue creation | | =pearl-accounts= | Named accounts for multiple workspaces | | =pearl-default-account= | Account made active at first need | -| =pearl-saved-queries= | Named local issue queries | +| =pearl-local-views= | Named local views | | =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 | @@ -439,16 +455,16 @@ Each rendered file is stamped with =#+LINEAR-ACCOUNT:= naming the workspace that Leave =pearl-accounts= unset for single-account use; everything works off =pearl-api-key= and =pearl-org-file-path= exactly as before. -*** Saved queries across accounts +*** Local views across accounts -=pearl-saved-queries= is one shared list. A query entry may carry an optional =:account= so it only runs under that account: +=pearl-local-views= is one shared list. A local-view entry may carry an optional =:account= so it only runs under that account: #+begin_src elisp ("My work bugs" :account "work" :filter (:team "ENG" :label "bug" :assignee :me)) #+end_src -Running it under a different active account refuses before any lookup or fetch. A query *without* =:account= is shared and resolves its team / state / label names against whatever account is active — so the same name can mean different things across workspaces. Tag the ones that must not cross. +Running it under a different active account refuses before any lookup or fetch. A local view *without* =:account= is shared and resolves its team / state / label names against whatever account is active — so the same name can mean different things across workspaces. Tag the ones that must not cross. ** Development & Testing :PROPERTIES: |
