From 0946c42f5caaf715786780b7be8205e6ac289d8a Mon Sep 17 00:00:00 2001 From: Craig Jennings Date: Sat, 6 Jun 2026 12:32:59 -0500 Subject: fix(signal): register C-; M prefix via canonical helper The C-; M Signal prefix didn't take effect on a fresh Emacs launch. signal-config.el was the only feature module that bound into cj/custom-keymap directly, wrapped in (with-eval-after-load 'keybindings (when (boundp 'cj/custom-keymap) ...)). The boundp guard turned a load-order miss into a silent no-op, so the binding never landed at startup. A later live-reload always papered over it because keybindings was loaded by then. I switched to the documented cj/register-prefix-map helper and added (require 'keybindings) at the top, matching every other prefix map. The require guarantees keybindings loads before registration, so the guard is gone. I verified at a full emacs --batch init.el launch, the actual failing scenario, that C-; M resolves to the signel prefix. I added a contract test asserting the registration, since the boundp guard was robust under unit timings and only failed at full launch. --- modules/signal-config.el | 10 ++++++---- tests/test-signal-config.el | 10 ++++++++++ todo.org | 6 ++++-- 3 files changed, 20 insertions(+), 6 deletions(-) diff --git a/modules/signal-config.el b/modules/signal-config.el index 102ece86..317e3520 100644 --- a/modules/signal-config.el +++ b/modules/signal-config.el @@ -16,6 +16,7 @@ ;;; Code: (require 'seq) +(require 'keybindings) ;; provides cj/custom-keymap + cj/register-prefix-map (defun cj/signal--jstr (value) "Return VALUE if it is a non-blank string, else nil. @@ -291,10 +292,11 @@ that on first use." Leaves =l= unbound for now -- the future =cj/signel-link= command lands in a later pass. See =docs/design/signal-client.org= scope summary.") -(declare-function cj/custom-keymap "keybindings" ()) -(with-eval-after-load 'keybindings - (when (boundp 'cj/custom-keymap) - (keymap-set cj/custom-keymap "M" cj/signel-prefix-map))) +;; Register the messages prefix under C-; M via the documented helper. +;; keybindings.el owns cj/custom-keymap; the (require 'keybindings) above +;; guarantees it is loaded before this runs, so no load-order guard is +;; needed. This is the same pattern every other feature module uses. +(cj/register-prefix-map "M" cj/signel-prefix-map "signal messages") (provide 'signal-config) ;;; signal-config.el ends here diff --git a/tests/test-signal-config.el b/tests/test-signal-config.el index 3be63362..7556efdb 100644 --- a/tests/test-signal-config.el +++ b/tests/test-signal-config.el @@ -368,6 +368,16 @@ commands the workflow spec names." (should (eq (keymap-lookup cj/signel-prefix-map "SPC") #'cj/signel-connect))) +(ert-deftest test-signal-config-prefix-map-registered-under-c-semi-m () + "Normal: loading signal-config registers `cj/signel-prefix-map' under +`M' in `cj/custom-keymap', so C-; M reaches the signel prefix. Guards +the wiring contract that the load-order bug broke: signal-config must +register through `cj/register-prefix-map', not a boundp-guarded direct +mutation that silently no-ops when keybindings loaded in a different +order." + (require 'keybindings) + (should (eq (keymap-lookup cj/custom-keymap "M") cj/signel-prefix-map))) + ;;; display-buffer-alist entry for *Signel: ...* chat buffers (ert-deftest test-signal-config-chat-buffer-display-rule-uses-bottom-30 () diff --git a/todo.org b/todo.org index fafbe01b..891cd9e4 100644 --- a/todo.org +++ b/todo.org @@ -102,8 +102,10 @@ Fork commit 5ec56c0 added =signel--pending-input= (capture from input-marker to *** TODO [#D] Include Signal groups in the picker :feature:no-sync: 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. -*** TODO [#B] C-; M prefix binding doesn't take effect on fresh Emacs launch :bug: -Surfaced during the 2026-05-28 manual verify of test 1. After a fresh restart, =C-; M SPC= did nothing — the =cj/signel-prefix-map= wasn't bound under =M= in =cj/custom-keymap=. A live-reload of =modules/signal-config.el= via =emacsclient -e '(load ...)'= immediately activated the binding, so the wiring at =signal-config.el:278-280= (=(with-eval-after-load 'keybindings (when (boundp 'cj/custom-keymap) (keymap-set cj/custom-keymap "M" cj/signel-prefix-map)))=) IS correct in isolation but isn't firing on the real launch sequence. Likely a load-order interaction or a use-package init/config timing issue (cf. the CLAUDE.md gotcha "Run a full Emacs launch after any use-package :config block edit" — modules can byte-compile clean and pass unit tests but fail at full Emacs launch). Probable fix path: switch signal-config.el to the documented =cj/register-prefix-map= helper from =modules/keybindings.el= (the helpers exist exactly to remove each module's hidden assumption that =cj/custom-keymap= is bound at the right moment). Reproduce: restart Emacs and try =C-; M SPC= without first reloading =signal-config.el=. +*** 2026-06-06 Sat @ 12:29:24 -0500 Fixed C-; M load-order bug via canonical register-prefix-map +Root cause: signal-config.el was the only feature module that violated the prefix-registration contract documented in =keybindings.el:41-45=. Every other prefix map uses =(require 'keybindings)= + a top-level =(cj/register-prefix-map "X" map)=; signal-config had neither, mutating =cj/custom-keymap= directly through a =(with-eval-after-load 'keybindings (when (boundp 'cj/custom-keymap) ...))= form. The =boundp= guard turned a load-order miss into a SILENT no-op — no error, the binding just never happened — which is why a live-reload (keybindings definitely loaded by then) papered over it. +Fix: added =(require 'keybindings)= at the top of signal-config.el and replaced the guarded form with =(cj/register-prefix-map "M" cj/signel-prefix-map "signal messages")=, matching the 25+ other prefix maps. +Verified: (1) new contract test =test-signal-config-prefix-map-registered-under-c-semi-m= asserts =C-; M= resolves to =cj/signel-prefix-map= (35/35 green); (2) full =emacs --batch= init.el launch — the exact failing scenario — now shows =C-; M= bound; (3) clean byte-compile; (4) live-reloaded into the daemon, binding confirmed. No unit-level red was possible: the =boundp= guard is robust under all standard test timings, which is the CLAUDE.md launch-only-failure class. *** 2026-05-28 Thu @ 03:09:18 -0500 Chat buffer docks bottom 30% and C-c C-k cancels =display-buffer-alist= entry in =modules/signal-config.el= matches =^\*Signel: = chat buffers and routes them through =display-buffer-at-bottom= with =window-height . 0.3=, so the chat docks to the bottom 30% of the frame. The signel fork's =signel-chat= switched from =switch-to-buffer= to =pop-to-buffer= so the rule can apply (=switch-to-buffer= ignores =display-buffer-alist=). =C-c C-c= was already bound to =signel--send-input= in the mode; =C-c C-k= now binds =signel--cancel-input=, a new fork helper that clears the editable region between =signel--input-marker= and =point-max= and then calls =quit-window=. Buffer stays alive so chat history above the marker survives revisits; cleared input means the next visit lands on a fresh prompt. Five ERT tests in =tests/test-signel-cancel-input.el= (clears pending, empty-area no-op, quit-window called, buffer preserved, keymap binding) and two new tests in =tests/test-signal-config.el= (entry shape + regex match set). Dotemacs commit 998e9c7a, fork commit df02d79. -- cgit v1.2.3