From 4daf2328756fa29b087870a2b1f672bc01aa6b51 Mon Sep 17 00:00:00 2001 From: Craig Jennings Date: Wed, 27 May 2026 22:11:24 -0500 Subject: feat(signal): initiate-message workflow (picker, guard, cache, keymap) I built pieces 2-7 of the initiate-message workflow from docs/design/signal-client.org and added tests covering the fork's clobber-fix (commit 5ec56c0 over there). The picker is the user-facing change: a single key opens a name-based completing-read for any contact, with "Note to Self" pinned first. The picker stack from the bottom up: cj/signel--ensure-started is the daemon guard. With a live process it's a no-op. With signel-account set but no process it calls signel-start and pre-warms the contact cache. With signel-account nil it user-errors naming the remedy. Pre-warming on start means the picker feels instant on first use. cj/signel--fetch-contacts issues a listContacts RPC through the new request-callback contract (signel--send-rpc with a success-callback). The callback runs the result through the verified cj/signal--parse-contacts and stores the (LABEL . RECIPIENT) alist in cj/signel--contact-cache, a cj-owned variable kept separate from signel's receive-time contact-map. An empty result populates the cache as nil, distinct from an RPC failure (which never invokes the callback so the prior cache survives). cj/signel-refresh-contacts is the user-facing command that clears and refetches. cj/signel-message is the picker. Warm cache opens completing-read immediately. Cold cache kicks off a fetch and accept-process-outputs up to cj/signel-fetch-timeout seconds (3s default), then user-errors if the daemon hasn't responded so a wedged process can't hang Emacs. The candidate list pins "Note to Self" first (resolves to signel-account) with a display-sort metadata function that preserves the given order rather than alphabetizing. cj/signel-message-self skips the picker and goes straight to signel-account. cj/signel-connect is the friendly verb on the prefix key. cj/signel-prefix-map binds m / s / d / q / SPC and attaches under C-; M via with-eval-after-load keybindings so the binding survives load-order. l stays unbound for the future link command. 15 new ERT tests cover the ensure-started branches, the fetch + cache contract (issued, populated, empty), refresh-contacts, the picker's four scenarios (warm-cache contact, warm-cache Note to Self, cold-cache resolves in time, cold-cache timeout), message-self, and the keymap bindings. Plus 4 new tests in tests/test-signel-input-preservation.el for the fork's clobber fix: pending-input captures typed text and returns nil when empty; both signel--insert-msg and signel--insert-system-msg redraw the prompt without clobbering "halfwritten". todo.org closes three tasks as dated event-log entries: the contact picker, the input clobber, and the use-package wiring. --- todo.org | 16 ++++++++-------- 1 file changed, 8 insertions(+), 8 deletions(-) (limited to 'todo.org') diff --git a/todo.org b/todo.org index 59c7d27c..acc1f120 100644 --- a/todo.org +++ b/todo.org @@ -50,11 +50,11 @@ signel is on MELPA but stale (one-author v0.1, all commits in a Jan-2026 burst, *** 2026-05-26 Tue @ 20:06:58 -0500 Linked as secondary device; contact parser verified against live shape Installed signal-cli 0.14.4.1 (AUR; imported AsamK's signing key FA10826A... to clear the makepkg verification). Linked the account via QR. Built and unit-tested the pure helper layer in =modules/signal-config.el= (contact-list parsing, notify-when-not-viewing predicate) with =tests/test-signal-config.el=. Confirmed the live =listContacts= shape: givenName/familyName are top-level in 0.14, not under profile as first assumed; corrected the parser and verified it produces a picker entry for all 94 real contacts. Sent a request to archsetup to add signal-cli to the standard install. -*** TODO [#B] Contact picker command :feature: -=cj/signel-pick-contact=: call signal-cli =listContacts= over JSON-RPC, feed the result through =cj/signal--parse-contacts= (done), =completing-read= the labels, open the chosen recipient's chat. signel today opens by raw phone number only. +*** 2026-05-27 Wed @ 22:08:40 -0500 Shipped initiate-message workflow: picker + Note-to-Self + keymap +=cj/signel-message= (=C-; M m=) names contacts via =completing-read= over the cj-owned =cj/signel--contact-cache=, with "Note to Self" pinned first. =cj/signel-message-self= (=C-; M s=) sends straight to =signel-account=. Daemon guard =cj/signel--ensure-started= auto-starts the daemon when =signel-account= is set and =user-error='s with the remedy when it isn't; on start it pre-warms the cache. =cj/signel--fetch-contacts= rides the new RPC callback contract (=signel--send-rpc= with success-callback), the result feeds =cj/signal--parse-contacts=, and =cj/signel-refresh-contacts= (=C-; M no leaf=) clears + refetches. Cold-cache invocations =accept-process-output= up to =cj/signel-fetch-timeout= seconds (3s default) and =user-error= on timeout so a wedged daemon can't hang Emacs. Prefix keymap =cj/signel-prefix-map= bound under =C-; M= via =keybindings.el='s =cj/custom-keymap=: m / s / d / q / SPC. 15 new ERT tests in =tests/test-signal-config.el= cover ensure-started branches, fetch contract, cache empty-vs-failure, refresh, picker happy-path + cold-cache resolves + cold-cache timeout, message-self, and the prefix map bindings. -*** DOING [#B] JSON-RPC success-result dispatch for signel :feature: -The contact picker needs =listContacts= results, but signel currently dispatches only =receive= notifications and RPC errors; successful =((id . N) (result . VALUE))= responses have no callback/result path. Add a request handler table or equivalent success-result dispatch in the fork, clean handlers up on success/error/reconnect, then build =cj/signel--fetch-contacts= on that contract. Review: [[file:docs/design/signal-client-review.org][docs/design/signal-client-review.org]]. +*** 2026-05-27 Wed @ 21:55:57 -0500 Added JSON-RPC success-result dispatch in the signel fork +Fork commit 4740d97 added =signel--request-handler-map= (id → success callback), extended =signel--send-rpc= with an optional =success-callback= that registers under the new request id, and gave =signel--dispatch= a result branch that invokes the callback and removes the handler. Error responses also remhash the handler entry, and =signel-start= / =signel-stop= both =clrhash= the map so reconnect is reliably empty. Backward-compatible: existing callers that don't pass a callback hit the same code path as before. Five ERT tests in this project (=tests/test-signel-rpc-dispatch.el=, dotemacs commit bfec0eab) lock the contract: Normal (result invokes callback + cleanup, send-rpc registers), Boundary (unknown id is a no-op), Error (error response cleans up handler), reconnect (=signel-stop= empties the map). Refactor audit surfaced a separate pre-existing leak in =signel--handle-error= (request-buffer-map entries aren't removed on error); filed as the [#C] follow-up below. *** TODO [#C] signel--handle-error leaks request-buffer-map entries :bug: Surfaced during the JSON-RPC dispatch refactor audit. =signel--handle-error= reads =signel--request-buffer-map= by id but never =remhash='es the entry, so every error response leaves the request-id → buffer-name mapping behind for the life of the process. Low impact (the map clears on stop/start, and id collisions are unlikely at the counter scale), but unbounded growth in a long-lived session and inconsistent with how the new request-handler-map is cleaned up on error. @@ -62,14 +62,14 @@ Surfaced during the JSON-RPC dispatch refactor audit. =signel--handle-error= rea *** TODO [#B] Notify only for the unviewed conversation :feature: Wire =cj/signal--should-notify-p= (done) into signel's =signel--handle-receive= notify block (signel.el:277), route through Craig's notify script instead of bare =notifications-notify=, and gate sound behind a defcustom that defaults off. -*** TODO [#B] Input clobber on incoming message — upstream signel #2 :bug: -signel.el:502 (=signel--insert-msg=) does =(delete-region (point) (point-max))=, wiping any unsent text in the prompt when a message arrives mid-typing. Preserve and restore the pending input across the redraw. Fix in the fork; it sits next to the notify edit. +*** 2026-05-27 Wed @ 22:08:40 -0500 Shipped clobber fix for both insert paths +Fork commit 5ec56c0 added =signel--pending-input= (capture from input-marker to point-max) and =signel--restore-input= (re-insert after the redrawn prompt; nil-safe), and wired both into =signel--insert-msg= (the receive path) and =signel--insert-system-msg= (the error path). A mid-type send now survives both an incoming message and a system-error insertion. Four ERT tests in =tests/test-signel-input-preservation.el= cover the helpers (typed text, empty) and both insert paths via a temp =signel-chat-mode= buffer. *** TODO [#B] Link command with QR :feature: =cj/signel-link= wrapping =signal-cli link -n NAME=, capturing the =sgnl://linkdevice= URI and rendering it as a scannable QR (qrencode). Convenience for re-linking; the first link was done by hand this session. -*** TODO [#B] use-package wiring :feature: -=use-package signel :load-path "~/code/signel" :ensure nil= in =signal-config.el=, plus a keybinding prefix (candidate =C-; M= for Messages), and the account source (defcustom vs authinfo, mirroring slack-config). +*** 2026-05-27 Wed @ 22:08:40 -0500 use-package wired with C-; M keymap and local account config +=use-package signel :load-path "~/code/signel" :ensure nil= already wired earlier with =signel-auto-open-buffer nil=. Account source is =signel-account= set from =cj/signal-private-config-file= (=signal-config.local.el=, gitignored) loaded in =:config=, decided in the workflow spec. Keymap prefix =C-; M= attached via =with-eval-after-load 'keybindings= so the binding survives load-order. *** TODO [#D] Include Signal groups in the picker :feature: vNext after the 1:1 initiate-message flow is stable. Merge =listGroups= with =listContacts=, label groups distinctly, and preserve the current v1 behavior where the picker is contacts-only. -- cgit v1.2.3