diff options
Diffstat (limited to 'modules')
| -rw-r--r-- | modules/signal-config.el | 152 |
1 files changed, 152 insertions, 0 deletions
diff --git a/modules/signal-config.el b/modules/signal-config.el index 0b6668b1..bed0f1e6 100644 --- a/modules/signal-config.el +++ b/modules/signal-config.el @@ -127,5 +127,157 @@ time." (when (file-readable-p cj/signal-private-config-file) (load cj/signal-private-config-file nil t))) +;;; 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 + "Cached `(LABEL . RECIPIENT)' alist for the contact picker. +Populated by `cj/signel--fetch-contacts' on first invocation (or after a +`cj/signel-refresh-contacts'), and cleared on `signel-stop' / restart so +a stale list can't survive a reconnect. In-memory only.") + +(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." + (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 populates the cache as nil; 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 (cj/signal--parse-contacts result)) + (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--contact-cache)) + (table (lambda (string pred action) + (if (eq action 'metadata) + '(metadata + (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/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))) + (provide 'signal-config) ;;; signal-config.el ends here |
