aboutsummaryrefslogtreecommitdiff
path: root/archive/modules
diff options
context:
space:
mode:
Diffstat (limited to 'archive/modules')
-rw-r--r--archive/modules/duet-config.el19
-rw-r--r--archive/modules/mu4e-org-contacts-setup.el31
-rw-r--r--archive/modules/show-kill-ring.el122
-rw-r--r--archive/modules/signal-config.el380
4 files changed, 552 insertions, 0 deletions
diff --git a/archive/modules/duet-config.el b/archive/modules/duet-config.el
new file mode 100644
index 00000000..2dc7ad2e
--- /dev/null
+++ b/archive/modules/duet-config.el
@@ -0,0 +1,19 @@
+;;; duet-config.el --- DUET dual-pane commander configuration -*- lexical-binding: t -*-
+
+;;; Commentary:
+;; Personal configuration glue for the DUET package, developed locally at
+;; ~/code/duet. Keybindings, defcustom values, and connection storage live
+;; here; the package itself stays free of personal opinions.
+;;
+;; Not yet required from init.el — DUET is a pre-alpha skeleton. Wire it in
+;; once Stage 1 provides usable commands.
+
+;;; Code:
+
+(use-package duet
+ :load-path "~/code/duet"
+ :ensure nil
+ :commands (duet))
+
+(provide 'duet-config)
+;;; duet-config.el ends here
diff --git a/archive/modules/mu4e-org-contacts-setup.el b/archive/modules/mu4e-org-contacts-setup.el
new file mode 100644
index 00000000..bfb9b1f2
--- /dev/null
+++ b/archive/modules/mu4e-org-contacts-setup.el
@@ -0,0 +1,31 @@
+;;; mu4e-org-contacts-setup.el --- Setup mu4e with org-contacts -*- lexical-binding: t; -*-
+;; author: Craig Jennings <c@cjennings.net>
+
+;;; Commentary:
+;;
+;; Thin activation wrapper for mu4e-org-contacts-integration. If mu4e is loaded,
+;; enable org-contacts completion and disable mu4e's internal contact collector
+;; so completion has one source of truth.
+
+;;; Code:
+
+(defvar mu4e-compose-complete-only-personal)
+(defvar mu4e-compose-complete-only-after)
+(declare-function cj/activate-mu4e-org-contacts-integration "mu4e-org-contacts-integration")
+
+;; Load the integration module. Activation only runs when the module loaded
+;; cleanly AND mu4e is present; otherwise this file is a no-op so the rest
+;; of the config can load without mu4e installed.
+(when (require 'mu4e-org-contacts-integration nil t)
+ (when (featurep 'mu4e)
+ (cj/activate-mu4e-org-contacts-integration)))
+
+;; Optional: If you want to use org-contacts as the primary source,
+;; you might want to disable mu4e's contact caching to save memory
+(with-eval-after-load 'mu4e
+ ;; Disable mu4e's internal contact collection
+ (setq mu4e-compose-complete-only-personal nil)
+ (setq mu4e-compose-complete-only-after nil))
+
+(provide 'mu4e-org-contacts-setup)
+;;; mu4e-org-contacts-setup.el ends here \ No newline at end of file
diff --git a/archive/modules/show-kill-ring.el b/archive/modules/show-kill-ring.el
new file mode 100644
index 00000000..e65d48b5
--- /dev/null
+++ b/archive/modules/show-kill-ring.el
@@ -0,0 +1,122 @@
+;;; show-kill-ring.el --- Displays Previous Kill Ring Entries -*- lexical-binding: t; coding: utf-8; -*-
+;; Show Kill Ring
+;; Stolen from Steve Yegge when he wasn't looking
+;; enhancements and bugs added by Craig Jennings <c@cjennings.net>
+;;
+;;; Commentary:
+;; Browse items you've previously killed.
+;; Yank text using C-u, the index, then C-y.
+;;
+;; I've lovingly kept the nice 1970s aesthetic, complete with wood paneling.
+;; Maybe I'll give it a makeover at some point.
+;;
+;;; Code:
+
+(require 'cl-lib)
+
+(defvar show-kill-max-item-size 1000
+ "This represents the size of a \='kill ring\=' entry.
+A positive number means to limit the display of \='kill-ring\=' items to
+that number of characters.")
+
+(defun show-kill-ring-exit ()
+ "Exit the show-kill-ring buffer."
+ (interactive)
+ (quit-window t))
+
+(defun show-kill-ring ()
+ "Show the current contents of the kill ring in a separate buffer.
+This makes it easy to figure out which prefix to pass to yank."
+ (interactive)
+ ;; kill existing one, since erasing it doesn't work
+ (let ((buf (get-buffer "*Kill Ring*")))
+ (and buf (kill-buffer buf)))
+
+ (let* ((buf (get-buffer-create "*Kill Ring*"))
+ (temp kill-ring)
+ (count 1)
+ (bar (make-string 32 ?=))
+ (bar2 (concat " " bar))
+ (item " Item ")
+ (yptr nil) (ynum 1))
+ (set-buffer buf)
+ (erase-buffer)
+
+ (show-kill-insert-header)
+
+ ;; show each of the items in the kill ring, in order
+ (while temp
+ ;; insert our little divider
+ (insert (concat "\n" bar item (prin1-to-string count) " "
+ (if (< count 10) bar2 bar) "\n"))
+
+ ;; if this is the yank pointer target, grab it
+ (when (equal temp kill-ring-yank-pointer)
+ (setq yptr (car temp) ynum count))
+
+ ;; insert the item and loop
+ (show-kill-insert-item (car temp))
+ (cl-incf count)
+ (setq temp (cdr temp)))
+
+ ;; show info about yank item
+ (show-kill-insert-footer yptr ynum)
+
+ ;; use define-key instead of local-set-key
+ (use-local-map (make-sparse-keymap))
+ (define-key (current-local-map) "q" #'show-kill-ring-exit)
+
+ ;; show it
+ (goto-char (point-min))
+ (setq buffer-read-only t)
+ (set-buffer-modified-p nil)
+ ;; display-buffer rather than pop-to-buffer
+ ;; easier for user to C-u (item#) C-y
+ ;; while the point is where they want to yank
+ (display-buffer buf)))
+
+(defun show-kill-insert-item (item)
+ "Insert an ITEM from the kill ring into the current buffer.
+If it's too long, truncate it first."
+ (let ((max show-kill-max-item-size))
+ (cond
+ ((or (not (numberp max))
+ (< max 0)
+ (< (length item) max))
+ (insert item))
+ (t
+ ;; put ellipsis on its own line if item is longer than 1 line
+ (let ((preview (substring item 0 max)))
+ (if (< (length item) (- (frame-width) 5))
+ (insert (concat preview "..."))
+ (insert (concat preview "\n..."))))))))
+
+(defun show-kill-insert-header ()
+ "Insert the show-kill-ring header or a notice if the kill ring is empty."
+ (if kill-ring
+ (insert "Contents of the kill ring:\n")
+ (insert "The kill ring is empty")))
+
+(defun show-kill-insert-footer (yptr ynum)
+ "Insert final divider and the yank-pointer (YPTR YNUM) info."
+ (when kill-ring
+ (save-excursion
+ (re-search-backward "^\\(=+ Item [0-9]+ =+\\)$"))
+ (insert "\n")
+ (insert (make-string (length (match-string 1)) ?=))
+ ;; Use number-to-string instead of int-to-string
+ (insert (concat "\n\nItem " (number-to-string ynum)
+ " is the next to be yanked:\n\n"))
+ (show-kill-insert-item yptr)
+ (insert "\n\nThe prefix arg will yank relative to this item.")))
+
+(defun empty-kill-ring ()
+ "Force garbage collection of huge kill ring entries that I don't care about."
+ (interactive)
+ (setq kill-ring nil)
+ (garbage-collect))
+
+(keymap-global-set "M-S-k" #'show-kill-ring) ;; was M-K, overrides kill-sentence
+
+(provide 'show-kill-ring)
+;;; show-kill-ring.el ends here
diff --git a/archive/modules/signal-config.el b/archive/modules/signal-config.el
new file mode 100644
index 00000000..a1ec7933
--- /dev/null
+++ b/archive/modules/signal-config.el
@@ -0,0 +1,380 @@
+;;; signal-config.el --- Signal client (forked signel) configuration -*- lexical-binding: t -*-
+
+;;; Commentary:
+;; cj/-namespaced configuration and helpers layered on the forked `signel'
+;; package, a Signal client that drives signal-cli over JSON-RPC.
+;;
+;; This file currently holds the pure, signal-cli-independent helper layer
+;; that the fork edits and `use-package' wiring build on:
+;; - contact-list parsing for a completing-read contact picker, and
+;; - the predicate that suppresses a notification for the chat the user
+;; is actively viewing.
+;; Both are unit-tested without a linked account. The use-package wiring,
+;; keybindings, and the signel fork edits that call these helpers land once
+;; signal-cli is installed and the device is linked.
+
+;;; Code:
+
+(require 'seq)
+(require 'keybindings) ;; provides cj/custom-keymap + cj/register-prefix-map
+(require 'system-lib) ;; for cj/executable-find-or-warn
+
+(declare-function notifications-notify "notifications")
+
+(defun cj/signal--jstr (value)
+ "Return VALUE if it is a non-blank string, else nil.
+Normalizes a JSON field that may arrive as nil, the empty string, or a
+null sentinel symbol into a plain string-or-nil."
+ (and (stringp value)
+ (not (string-empty-p (string-trim value)))
+ value))
+
+(defun cj/signal--combine-name (given family)
+ "Join GIVEN and FAMILY name parts into a trimmed full name, or nil.
+Either part may be nil, the empty string, or a JSON null sentinel."
+ (let ((parts (delq nil (list (cj/signal--jstr given) (cj/signal--jstr family)))))
+ (cj/signal--jstr (mapconcat #'identity parts " "))))
+
+(defun cj/signal--contact-display-name (contact)
+ "Return a display name for CONTACT, or nil when none is set.
+CONTACT is one entry alist from signal-cli `listContacts'. Picks the
+first set source in priority order: the nickname (combined nickName, or
+nickGivenName+nickFamilyName), the stored contact name, the top-level
+givenName+familyName, the profile givenName+familyName, then username.
+signal-cli 0.14 puts givenName/familyName at the top level; the profile
+sub-object's name fields are usually null, so it is the deeper fallback."
+ (let ((profile (alist-get 'profile contact)))
+ (seq-find
+ #'cj/signal--jstr
+ (list (cj/signal--jstr (alist-get 'nickName contact))
+ (cj/signal--combine-name (alist-get 'nickGivenName contact)
+ (alist-get 'nickFamilyName contact))
+ (cj/signal--jstr (alist-get 'name contact))
+ (cj/signal--combine-name (alist-get 'givenName contact)
+ (alist-get 'familyName contact))
+ (cj/signal--combine-name (alist-get 'givenName profile)
+ (alist-get 'familyName profile))
+ (cj/signal--jstr (alist-get 'username contact))))))
+
+(defun cj/signal--parse-contacts (result)
+ "Parse RESULT from signal-cli `listContacts' into a completing-read alist.
+RESULT is the JSON-RPC result value: a sequence (list or vector) of
+contact alists. Returns an alist of (LABEL . RECIPIENT) sorted by LABEL,
+where RECIPIENT is the contact's phone number (falling back to its UUID)
+and LABEL is \"Name (recipient)\" when a name is known, or the bare
+recipient otherwise. Contacts with no usable recipient are dropped."
+ (let (pairs)
+ (dolist (contact (append result nil))
+ (let ((recipient (or (cj/signal--jstr (alist-get 'number contact))
+ (cj/signal--jstr (alist-get 'uuid contact))))
+ (name (cj/signal--contact-display-name contact)))
+ (when recipient
+ (push (cons (if name (format "%s (%s)" name recipient) recipient)
+ recipient)
+ pairs))))
+ (sort pairs (lambda (a b) (string-lessp (car a) (car b))))))
+
+(defun cj/signal--chat-buffer-name (id)
+ "Return the chat buffer name `signel' uses for chat ID."
+ (format "*Signel: %s*" id))
+
+(defun cj/signal--suppress-notify-p (chat-id viewing-buffer-name frame-focused)
+ "Return non-nil when a notification for CHAT-ID should be suppressed.
+Suppress only while the user is actively viewing that chat: the chat
+buffer named by `cj/signal--chat-buffer-name' is VIEWING-BUFFER-NAME and
+FRAME-FOCUSED is non-nil. A nil VIEWING-BUFFER-NAME or an unfocused
+frame never suppresses."
+ (and frame-focused
+ (stringp viewing-buffer-name)
+ (string= viewing-buffer-name (cj/signal--chat-buffer-name chat-id))))
+
+(defun cj/signal--frame-focused-p ()
+ "Return non-nil when the selected frame currently has input focus.
+Treats an unknown focus state as focused."
+ (if (fboundp 'frame-focus-state)
+ (let ((state (frame-focus-state)))
+ (if (eq state 'unknown) t state))
+ t))
+
+(defun cj/signal--should-notify-p (chat-id)
+ "Return non-nil when an incoming message for CHAT-ID should notify.
+Notify unless the user is actively viewing that chat in the selected
+window of a focused frame."
+ (not (cj/signal--suppress-notify-p
+ chat-id
+ (buffer-name (window-buffer (selected-window)))
+ (cj/signal--frame-focused-p))))
+
+;;; Notifications
+
+(defcustom cj/signel-notify-sound nil
+ "When non-nil, incoming-message notifications play the notify script's sound.
+Nil (the default) passes --silent so the toast is visual only."
+ :type 'boolean
+ :group 'signel)
+
+(defconst cj/signal--notify-body-max 120
+ "Maximum character length of a desktop-notification body.
+Longer message text truncates to this length ending in an ellipsis;
+the full text is always in the chat buffer.")
+
+(defun cj/signal--format-notify-body (text)
+ "Collapse whitespace in TEXT and truncate it for a notification body.
+Whitespace runs (including newlines) become single spaces, the result
+is trimmed, and anything over `cj/signal--notify-body-max' characters
+truncates to that length with a trailing ellipsis."
+ (let ((flat (string-trim (replace-regexp-in-string "[ \t\n\r]+" " " text))))
+ (if (<= (length flat) cj/signal--notify-body-max)
+ flat
+ (concat (substring flat 0 (1- cj/signal--notify-body-max)) "…"))))
+
+(defun cj/signel--notify (chat-id sender body)
+ "Raise a desktop notification for an incoming Signal message.
+Suppressed via `cj/signal--should-notify-p' when the user is actively
+viewing CHAT-ID. Routes through the external notify script when it is
+on PATH (type info, sound gated by `cj/signel-notify-sound'), falling
+back to `notifications-notify' otherwise. SENDER names the title;
+BODY is formatted by `cj/signal--format-notify-body'. Installed as
+`signel-notify-function' in the use-package :config below."
+ (when (cj/signal--should-notify-p chat-id)
+ (let ((title (format "Signal: %s" sender))
+ (text (cj/signal--format-notify-body body))
+ (script (executable-find "notify")))
+ (if script
+ (apply #'start-process "signel-notify" nil script "info" title text
+ (unless cj/signel-notify-sound (list "--silent")))
+ ;; notifications.el is not autoloaded; load it on the first fallback.
+ (unless (fboundp 'notifications-notify)
+ (require 'notifications))
+ (notifications-notify :title title :body text)))))
+
+;;; signel — fork integration
+
+(defcustom cj/signal-private-config-file
+ (expand-file-name "signal-config.local.el" user-emacs-directory)
+ "Private signal-config file, loaded when readable.
+This is the place to set `signel-account' to the linked phone number so
+the number stays out of the version-controlled (and publicly mirrored)
+config. A phone number is an identifier rather than a credential, so it
+lives here rather than in authinfo, which avoids a GPG prompt at connect
+time."
+ :type 'file
+ :group 'signel)
+
+(use-package signel
+ :load-path "~/code/signel"
+ :ensure nil
+ :commands (signel-start signel-stop signel-chat signel-dashboard)
+ :custom
+ ;; Don't let an incoming message steal a window by auto-popping its chat
+ ;; buffer; surface arrivals through notifications instead (see child task
+ ;; "Notify only for the unviewed conversation").
+ (signel-auto-open-buffer nil)
+ :config
+ (when (file-readable-p cj/signal-private-config-file)
+ (load cj/signal-private-config-file nil t))
+ ;; Route incoming-message notifications through cj/signel--notify
+ ;; (suppression + notify script + truncation); warn once at load when
+ ;; the script is missing — the runtime path still falls back to
+ ;; notifications-notify, so messages are never silently dropped.
+ (setq signel-notify-function #'cj/signel--notify)
+ (cj/executable-find-or-warn "notify" "Signal desktop notifications via the notify script (falling back to notifications-notify)" 'signal-config))
+
+;; Chat buffers (named `*Signel: <id>*') open in the bottom 30% of the
+;; frame rather than wherever display-buffer's fallback rule picks.
+;; The fork's `signel-chat' uses `pop-to-buffer', so this entry applies.
+(add-to-list
+ 'display-buffer-alist
+ '("\\`\\*Signel: "
+ (display-buffer-reuse-window display-buffer-at-bottom)
+ (window-height . 0.3)
+ (reusable-frames . nil)))
+
+;;; Connection guard, contact fetch, and cache
+
+;; Forward declarations: signel.el is loaded by the use-package above (with
+;; :load-path on the fork), but the byte-compiler doesn't see those symbols
+;; statically. Declaring them keeps the compile clean without changing
+;; runtime behavior.
+(defvar signel-account)
+(defvar signel--process-name)
+(declare-function signel-start "signel" ())
+(declare-function signel--send-rpc "signel" (method params &optional target-buffer success-callback))
+
+(defvar cj/signel--contact-cache nil
+ "Contact-picker cache: nil (cold), `empty', or a `(LABEL . RECIPIENT)' alist.
+Populated by `cj/signel--fetch-contacts' on first invocation (or after a
+`cj/signel-refresh-contacts'). A fetched-and-empty account caches the
+symbol `empty' rather than nil, so it reads as warm and the picker does
+not re-run its blocking fetch on every open -- read through
+`cj/signel--cached-contacts'. Cleared back to cold by
+`cj/signel--clear-contact-cache', advised onto `signel-stop' so a stale
+list can't survive a reconnect. In-memory only.")
+
+(defun cj/signel--clear-contact-cache (&rest _)
+ "Return the contact cache to cold (nil) so the next picker refetches.
+Advised `:after' `signel-stop': a relink or reconnect may change the
+contact list, so a cache from the previous connection must not survive."
+ (setq cj/signel--contact-cache nil))
+
+(advice-add 'signel-stop :after #'cj/signel--clear-contact-cache)
+
+(defun cj/signel--cached-contacts ()
+ "Return the cached contact alist, treating the `empty' sentinel as none."
+ (unless (eq cj/signel--contact-cache 'empty)
+ cj/signel--contact-cache))
+
+(defcustom cj/signel-fetch-timeout 3.0
+ "Seconds the picker blocks on `accept-process-output' for a cold-cache fetch.
+On warm cache the picker opens instantly; on cold cache it kicks off a
+fetch and waits up to this many seconds for the RPC result before
+reporting a `user-error' so a dead or wedged daemon can't hang Emacs."
+ :type 'number
+ :group 'signel)
+
+(defun cj/signel--ensure-started ()
+ "Ensure the signel daemon is live, starting it if needed.
+Three branches:
+- The process is already live -- no-op, return nil.
+- `signel-account' is set but no live process exists -- call `signel-start'
+ and pre-warm the contact cache with a background `listContacts' fetch so
+ the picker is instant on first use.
+- `signel-account' is nil -- `user-error' naming the remedy (set the
+ account in `cj/signal-private-config-file').
+
+If startup launches but the RPC handshake exits before the first response,
+the subsequent `signel--send-rpc' call (in the pre-warm or any later
+fetch) signals through its own error path; check =*signel-log*= and
+=*signel-stderr*= for detail and link the account manually.
+
+Loads the `signel' feature explicitly before reading any of its
+private variables: the use-package above autoloads only on
+`signel-start' / `signel-stop' / `signel-chat' / `signel-dashboard',
+so without this require the first branch's read of `signel--process-name'
+fires a void-variable error before the autoload would trigger."
+ (require 'signel)
+ (cond
+ ((process-live-p (get-process signel--process-name))
+ nil)
+ ((null signel-account)
+ (user-error
+ "signel-account is unset. Set it in %s (or your private config) and link the device manually with `signal-cli link', then retry"
+ cj/signal-private-config-file))
+ (t
+ (signel-start)
+ (cj/signel--fetch-contacts))))
+
+(defun cj/signel--fetch-contacts (&optional after-callback)
+ "Fetch the contact list from signal-cli and populate `cj/signel--contact-cache'.
+Issues a `listContacts' RPC and registers a success callback that runs
+the result through `cj/signal--parse-contacts' (the verified parser) and
+stores the resulting `(LABEL . RECIPIENT)' alist in the cache. An empty
+result caches the `empty' sentinel -- nil would read as a cold cache and
+re-run the picker's blocking fetch on every open. A failure goes
+through the dispatch error path and never invokes the callback, so the
+prior cache survives.
+
+AFTER-CALLBACK, when non-nil, is invoked with no arguments after the
+cache has been populated -- the picker uses this to unblock its
+bounded-wait on cold caches."
+ (signel--send-rpc
+ "listContacts" nil nil
+ (lambda (result)
+ (setq cj/signel--contact-cache
+ (or (cj/signal--parse-contacts result) 'empty))
+ (when after-callback (funcall after-callback)))))
+
+(defun cj/signel-refresh-contacts ()
+ "Clear the picker's contact cache and refetch it from signal-cli.
+Use when a contact added or renamed on the phone hasn't shown up in the
+picker yet; this forces a fresh `listContacts' rather than reading the
+cached snapshot."
+ (interactive)
+ (setq cj/signel--contact-cache nil)
+ (cj/signel--fetch-contacts))
+
+;;; Picker, self-message, and connect
+
+(declare-function signel-chat "signel" (recipient))
+(declare-function signel-dashboard "signel" ())
+(declare-function signel-stop "signel" ())
+
+(defun cj/signel-connect ()
+ "Connect to signal-cli, starting the daemon if needed.
+Thin interactive wrapper around `cj/signel--ensure-started' so the
+keymap has a friendly verb to bind."
+ (interactive)
+ (cj/signel--ensure-started)
+ (message "Signel connected."))
+
+(defun cj/signel-message ()
+ "Pick a Signal contact by name and open the chat buffer.
+Ensures the daemon is connected first (auto-starts and pre-warms on
+cold start, or errors with the remedy if the account isn't set). Uses
+the cached contact list when warm; on a cold cache, kicks off a fetch
+and waits up to `cj/signel-fetch-timeout' seconds for the result before
+raising a `user-error' so a dead daemon can't hang Emacs. The picker
+offers a pinned \"Note to Self\" entry plus every Signal contact, and
+opens the chosen recipient in `signel-chat'."
+ (interactive)
+ (cj/signel--ensure-started)
+ (unless cj/signel--contact-cache
+ (let ((done nil)
+ (deadline (+ (float-time) cj/signel-fetch-timeout)))
+ (cj/signel--fetch-contacts (lambda () (setq done t)))
+ (while (and (not done) (< (float-time) deadline))
+ (accept-process-output nil 0.1))
+ (unless done
+ (user-error
+ "Signal contact fetch timed out after %.1fs; try again or run M-x cj/signel-refresh-contacts (see *signel-log* for detail)"
+ cj/signel-fetch-timeout))))
+ (let* ((note-self (cons "Note to Self" signel-account))
+ (candidates (cons note-self (cj/signel--cached-contacts)))
+ (table (lambda (string pred action)
+ (if (eq action 'metadata)
+ `(metadata
+ (category . signal-contact)
+ (annotation-function
+ . ,(lambda (cand)
+ (let ((r (cdr (assoc cand candidates))))
+ (when r
+ (concat " " (propertize r 'face 'completions-annotations))))))
+ (display-sort-function . identity)
+ (cycle-sort-function . identity))
+ (complete-with-action action candidates string pred))))
+ (label (completing-read "Signal recipient: " table nil t))
+ (recipient (cdr (assoc label candidates))))
+ (when recipient
+ (signel-chat recipient))))
+
+(defun cj/signel-message-self ()
+ "Open a Signal chat buffer addressed to Note to Self.
+Resolves to `signel-account' (the linked phone number). Sending to it
+lands in the Signal Note-to-Self thread on the phone; manual-verify
+that on first use."
+ (interactive)
+ (cj/signel--ensure-started)
+ (unless signel-account
+ (user-error "signel-account is unset; cannot send to self"))
+ (signel-chat signel-account))
+
+(defvar cj/signel-prefix-map
+ (let ((map (make-sparse-keymap)))
+ (keymap-set map "m" #'cj/signel-message)
+ (keymap-set map "s" #'cj/signel-message-self)
+ (keymap-set map "d" #'signel-dashboard)
+ (keymap-set map "q" #'signel-stop)
+ (keymap-set map "SPC" #'cj/signel-connect)
+ map)
+ "Signel \"Messages\" prefix keymap, bound under `C-; M'.
+Leaves =l= unbound for now -- the future =cj/signel-link= command lands
+in a later pass. See =docs/specs/signal-client-spec-doing.org= scope summary.")
+
+;; 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