<feed xmlns='http://www.w3.org/2005/Atom'>
<title>pearl/tests/test-pearl-keymap.el, branch main</title>
<subtitle>Pearl Edits and Reflects Linear — manage Linear.app issues as org-mode in Emacs
</subtitle>
<id>https://git.cjennings.net/pearl/atom?h=main</id>
<link rel='self' href='https://git.cjennings.net/pearl/atom?h=main'/>
<link rel='alternate' type='text/html' href='https://git.cjennings.net/pearl/'/>
<updated>2026-06-07T06:00:55+00:00</updated>
<entry>
<title>fix(keymap): label the issue create/delete keys 'ticket' to match the t chord</title>
<updated>2026-06-07T06:00:55+00:00</updated>
<author>
<name>Craig Jennings</name>
<email>c@cjennings.net</email>
</author>
<published>2026-06-07T06:00:55+00:00</published>
<link rel='alternate' type='text/html' href='https://git.cjennings.net/pearl/commit/?id=865473ebe470d4abc858c67ba1456c10ca0012c9'/>
<id>urn:sha1:865473ebe470d4abc858c67ba1456c10ca0012c9</id>
<content type='text'>
</content>
</entry>
<entry>
<title>feat(keys): bind sort and grouping under the views prefix</title>
<updated>2026-06-06T20:37:22+00:00</updated>
<author>
<name>Craig Jennings</name>
<email>c@cjennings.net</email>
</author>
<published>2026-06-06T20:37:22+00:00</published>
<link rel='alternate' type='text/html' href='https://git.cjennings.net/pearl/commit/?id=9564501efe2488b9136dc19a7a8d385b8b074b46'/>
<id>urn:sha1:9564501efe2488b9136dc19a7a8d385b8b074b46</id>
<content type='text'>
The interactive view-shaping commands shipped M-x-only. I bound them in the views sub-map so they're reachable from the keyboard: C-; L v s sorts, C-; L v g groups, C-; L v r reverses the sort order, with matching which-key labels.

They sit in the views group because there's no dedicated view-shaping group. Opening one means reopening the whole keymap-prefix question that's already parked. So they reshape the active buffer from under a "views" prefix that otherwise manages saved views, a slight category stretch called out in the map's docstring. The transient menu still omits both, pending the same keymap-shape pass.
</content>
</entry>
<entry>
<title>feat(keys): friendlier which-key labels for the prefix sub-maps</title>
<updated>2026-06-02T23:09:48+00:00</updated>
<author>
<name>Craig Jennings</name>
<email>c@cjennings.net</email>
</author>
<published>2026-06-02T23:09:48+00:00</published>
<link rel='alternate' type='text/html' href='https://git.cjennings.net/pearl/commit/?id=9a145f30ff49a0141f342ea775e8f1a5b5899ae6'/>
<id>urn:sha1:9a145f30ff49a0141f342ea775e8f1a5b5899ae6</id>
<content type='text'>
The C-; L sub-prefixes were labelled +fetch / +views / +edit / +create / +delete / +open / +copy / +workspace, the which-key submenu idiom but terse. They now read fetch... / views... / edit... / create... and so on: drop the +, add an ellipsis so the label reads as "do this, then pick what." Leaf commands and the direct hot keys are unchanged.

These labels are the (LABEL . COMMAND) menu-item strings baked into pearl-prefix-map, so which-key, describe-prefix, and which-key-less lookups all pick them up with no which-key dependency. Pure label strings, no behavior change. The keymap-label test moved to the new wording.
</content>
</entry>
<entry>
<title>feat(views): wire the default view into the keymap, transient, and docs</title>
<updated>2026-06-02T18:04:01+00:00</updated>
<author>
<name>Craig Jennings</name>
<email>c@cjennings.net</email>
</author>
<published>2026-06-02T18:04:01+00:00</published>
<link rel='alternate' type='text/html' href='https://git.cjennings.net/pearl/commit/?id=c11592ed56b895dc3ec4cfae7e9df8e77d641c2c'/>
<id>urn:sha1:c11592ed56b895dc3ec4cfae7e9df8e77d641c2c</id>
<content type='text'>
Phase 3, the last of the default-view spec: surface the phase 1-2 commands and flip the hot key over.

Top-level l now runs pearl-open-default-view instead of pearl-list-issues, so it opens your default. The literal my-open-issues fetch stays reachable at f o, and l is unchanged until you set a default. v D binds pearl-set-default-view (the slot the keybinding reconciliation reserved), and the transient Views group gets a . suffix for it. D there stays save-locally.

README documents the default view: what l does, how to set and clear it, the Customize persistence, and the per-account scoping under multiple accounts.

Keymap and menu tests updated for the l rebind, the v D binding, and the transient suffix. Suite holds at 809, compile and lint clean.
</content>
</entry>
<entry>
<title>feat(keys): dedicated views group, de-overloaded fetch, account binding</title>
<updated>2026-06-02T16:56:55+00:00</updated>
<author>
<name>Craig Jennings</name>
<email>c@cjennings.net</email>
</author>
<published>2026-06-02T16:56:55+00:00</published>
<link rel='alternate' type='text/html' href='https://git.cjennings.net/pearl/commit/?id=0dccaaee955c99d2affcf33c145feba2ef1fdcda'/>
<id>urn:sha1:0dccaaee955c99d2affcf33c145feba2ef1fdcda</id>
<content type='text'>
The C-; L keymap and the transient menu had drifted into two different letter schemes for the same commands, the fetch sub-map carried both issue-fetching and view-movement, and pearl-switch-account was bound nowhere.

Give views their own group under v: l/L run local/Linear, c/e/k create/edit/delete, u/U publish local/current, d save a Linear view locally. Fetch (f) is now issue sources only. A new workspace group (w) binds switch-account plus the setup commands, which previously lived only in the transient. v D is reserved, unbound, for the not-yet-built set-default-view.

The transient already had an issue-only fetch group and a dedicated views group, so its changes are additive: switch-account and copy-issue-url were both missing from the menu and are added, and save/save-all/edit-description take s/S/d to match the keymap's hot keys. Matching those three cascades a few view and edit letters because the transient is a flat keyspace where every command needs a unique key.

Suite 789 to 792, compile and lint clean.
</content>
</entry>
<entry>
<title>feat(views): save a Linear view locally via the reverse-compile (copy down)</title>
<updated>2026-06-01T23:21:47+00:00</updated>
<author>
<name>Craig Jennings</name>
<email>c@cjennings.net</email>
</author>
<published>2026-06-01T23:21:47+00:00</published>
<link rel='alternate' type='text/html' href='https://git.cjennings.net/pearl/commit/?id=9b9a264edb0c208892fcfe5101912d00858de766'/>
<id>urn:sha1:9b9a264edb0c208892fcfe5101912d00858de766</id>
<content type='text'>
I implemented copy-down, the one direction that needed real engine work. A live probe showed CustomView.filterData is readable but stored as Linear's and/or view-filter tree, not the flat IssueFilter Pearl emits, so pearl--reverse-compile-issue-filter is a normalize-then-match: it flattens the top-level and, unwraps single-branch and/or, reads a one-element in as a scalar, then matches each conjunct to one authoring key. Every helper returns (ok . plist) or (refuse . reason).

The contract follows the spec's dimension table: distinct :state and :state-type, canonical numeric :priority, :open t mapped from the exact open-state nin while generic nin refuses, and a multi-value in on any singular key refuses rather than silently narrow the view. Anything outside Pearl's AND-only model refuses with a structured reason that names the construct.

pearl-save-linear-view-locally lists favorited Linear views, fetches the chosen view's filterData through a small async helper, reverse-compiles it, and saves a forked local view through pearl--save-local-view: no tracking link, :copied-from-view-id provenance only, active account stamped. On a refusal it points the user at pearl-run-linear-view; on success it states the fork's independence. Bound at f d (down) and the transient Views column.

Phase 6 of the views spec. 30 tests cover per-dimension round-trips (build-&gt;encode-&gt;read bridge), normalize-from-real-JSON trees, the full refusal set, and the command-side. Suite, compile, and lint are green.
</content>
</entry>
<entry>
<title>feat(views): reshape the keymap and transient around the view noun</title>
<updated>2026-06-01T22:53:53+00:00</updated>
<author>
<name>Craig Jennings</name>
<email>c@cjennings.net</email>
</author>
<published>2026-06-01T22:53:53+00:00</published>
<link rel='alternate' type='text/html' href='https://git.cjennings.net/pearl/commit/?id=ad1e0b8c33f909814b69458eb34cc792f032e844'/>
<id>urn:sha1:ad1e0b8c33f909814b69458eb34cc792f032e844</id>
<content type='text'>
I put the view operations under the verb a user would reach for. Create, edit, and delete local views are c v, e v, and k v. The fetch group gains run-local-view at f l and keeps run-linear-view at f v, with publish at f u and save-locally reserved at f d. Since f p already runs issues by project, publish and save-locally take u and d, which read as up and down and mirror the copy-up and copy-down lifecycle.

The transient now has a Views column grouping create, edit, run-local, run-linear, publish, and delete, separate from the source-fetching commands. I updated the spec's keymap to the implemented letters so the two stay in sync.

Phase 3 of the views spec. Keymap tests assert the new bindings. Suite, compile, and lint are green.
</content>
</entry>
<entry>
<title>refactor: rename saved-query surface to local-view vocabulary</title>
<updated>2026-06-01T22:33:09+00:00</updated>
<author>
<name>Craig Jennings</name>
<email>c@cjennings.net</email>
</author>
<published>2026-06-01T22:33:09+00:00</published>
<link rel='alternate' type='text/html' href='https://git.cjennings.net/pearl/commit/?id=c2ba1d8f25463bf6342ac635850500a9738b31e9'/>
<id>urn:sha1:c2ba1d8f25463bf6342ac635850500a9738b31e9</id>
<content type='text'>
I renamed Pearl's "saved query" surface to the local/Linear view vocabulary, with no obsolete aliases, since Pearl has no users to protect. The defcustom pearl-saved-queries is now pearl-local-views. The commands pearl-run-saved-query, pearl-delete-saved-query, pearl-sync-saved-query-to-linear, pearl-run-view, and pearl-publish-current-source become pearl-run-local-view, pearl-delete-local-view, pearl-publish-local-view, pearl-run-linear-view, and pearl-publish-current-view.

User-facing prompts, messages, and docstrings drop "saved query" for "local view", and the publish command reads "publish" instead of "sync". Internal GraphQL and helper names keep their "query" terms, which a user never sees. A naming-regression test asserts the new symbols exist, the old ones are gone rather than aliased, and no public command exposes "query".

Phase 1 of docs/local-and-linear-views-spec.org. No behavior change. Suite, compile, and lint are green.
</content>
</entry>
<entry>
<title>feat(view-sync): pearl-publish-current-source convenience wrapper</title>
<updated>2026-05-28T14:36:06+00:00</updated>
<author>
<name>Craig Jennings</name>
<email>c@cjennings.net</email>
</author>
<published>2026-05-28T14:36:06+00:00</published>
<link rel='alternate' type='text/html' href='https://git.cjennings.net/pearl/commit/?id=0975e7de0c7bfdc6f65bb64537d4eff1aeb09a7e'/>
<id>urn:sha1:0975e7de0c7bfdc6f65bb64537d4eff1aeb09a7e</id>
<content type='text'>
This is Phase 5 of docs/saved-query-sync-spec.org (§ New: pearl-publish-current-source). When I'm reading a local saved query rendered in pearl and decide it should be a Linear view, I now have a one-chord publish instead of the M-x round-trip through pearl-sync-saved-query-to-linear with the name lookup.

The new command reads the active buffer's #+LINEAR-SOURCE header and dispatches to pearl-sync-saved-query-to-linear with the name pre-filled when the source is a :type filter that matches an entry in pearl-saved-queries. Other shapes refuse with a clear message: no source header at all, the source is already a :type view (no publish needed), the type isn't filter, the source has no usable name (transient ad-hoc filter, or whitespace-only name), or the recorded name doesn't match any local saved query.

I bound the command under C-; L f P in pearl-fetch-map. The transient gets ("U" "upload current source") in the Fetch group because P is taken in the transient's flat namespace, and the verb-prefix shape revisit task tracks reshaping that.

7 new tests cover each refusal branch and the dispatch path, including a regression for the whitespace-only :name case the pre-commit review surfaced. The prior check used string-empty-p, which let "   " fall through to the no-matching-saved-query branch with a visibly-blank name in the error message. string-blank-p routes it to the cleaner no-name branch. The keymap and transient menu tests gained coverage for the new binding. 660 ert tests total green. make compile and make lint clean.
</content>
</entry>
<entry>
<title>feat(view-sync): pearl-sync-saved-query-to-linear + transient entry</title>
<updated>2026-05-28T13:55:59+00:00</updated>
<author>
<name>Craig Jennings</name>
<email>c@cjennings.net</email>
</author>
<published>2026-05-28T13:50:07+00:00</published>
<link rel='alternate' type='text/html' href='https://git.cjennings.net/pearl/commit/?id=60a026be803e4fb2e56520dd9e2cc72ba6bfc0e3'/>
<id>urn:sha1:60a026be803e4fb2e56520dd9e2cc72ba6bfc0e3</id>
<content type='text'>
pearl-sync-saved-query-to-linear promotes a local pearl-saved-queries entry to a Linear Custom View via customViewCreate, or updates an already-synced entry in place via customViewUpdate. This is Phase 2 of docs/saved-query-sync-spec.org. Re-syncing keeps the stored scope. First-time sync prompts for the destination (team + visibility), with each candidate spelling out the complete end-state. Same-name collisions in the chosen scope prompt Replace / Rename / Cancel.

A successful sync extends the entry with :linear-view-id, :linear-view-team-id, :linear-view-shared, :linear-view-synced-at. If the customize-save-variable persist fails after a successful API call, the message names the orphan view's id explicitly so the user can re-sync with Replace to reconcile.

I bound the command under C-; L f S in pearl-fetch-map and added ("S" "sync saved query") to the transient's Fetch group. filterData is the existing pearl--build-issue-filter output verbatim. The 2026-05-28 live probe confirmed the shape passes through customView.filterData with no translation needed.

Four related fixes ride along:

- Extended pearl-get-teams-async to fetch `key' alongside `id name'. It shares pearl--cache-teams with pearl--all-teams, so an async-first populate would otherwise leave the cache without `key' and silently break filter-team lookups.
- Normalized the stored :linear-view-shared flag to a strict boolean at the re-sync read site. `:json-false' is truthy in Elisp, so an unnormalized stored false would flip the view to team-shared on re-sync without asking.
- pearl--sync-saved-query-await now returns `(:success nil :timeout t)' on a wait-for elapse, so the caller can distinguish a timeout from a Linear-side rejection and tell the user the request may still complete server-side.
- The success path now emits a single message via pearl--sync-record-or-orphan-error. The prior code clobbered the orphan-recoverable view-id with a second message.

I hoisted the prompt-sentinel block (pearl--filter-cancel etc.) above its first user. The prior order produced a forward-reference compile error the moment any other change touched the file.

I added 31 tests in test-pearl-saved-query-sync.el covering the scope candidates, the customViewCreate/Update parsing, the entry persistence (including the :json-false overwrite case), the orphan-id error path, the timeout sentinel, and the get-teams-async cache parity. The keymap and menu tests gained coverage for the new binding. 631 ert tests total green. make compile and make lint clean.

Lower-severity findings filed as follow-ups: stale pearl--cache-views across a schema upgrade, empty filter encoding to null filterData, the pearl--save-query-mark-synced rebuild dropping unknown plist keys, and the scope-label rassoc rebuilding the candidate list to look up a string the picker already had.
</content>
</entry>
</feed>
