diff options
Diffstat (limited to 'modules')
| -rw-r--r-- | modules/config-utilities.el | 28 | ||||
| -rw-r--r-- | modules/dirvish-config.el | 10 | ||||
| -rw-r--r-- | modules/eat-config.el | 21 | ||||
| -rw-r--r-- | modules/org-capture-config.el | 8 | ||||
| -rw-r--r-- | modules/prog-general.el | 8 | ||||
| -rw-r--r-- | modules/system-defaults.el | 24 | ||||
| -rw-r--r-- | modules/telega-config.el | 63 | ||||
| -rw-r--r-- | modules/transcription-config.el | 22 | ||||
| -rw-r--r-- | modules/undead-buffers.el | 20 | ||||
| -rw-r--r-- | modules/video-audio-recording.el | 41 | ||||
| -rw-r--r-- | modules/weather-config.el | 7 |
11 files changed, 177 insertions, 75 deletions
diff --git a/modules/config-utilities.el b/modules/config-utilities.el index 4332f407..62fc29d0 100644 --- a/modules/config-utilities.el +++ b/modules/config-utilities.el @@ -196,27 +196,27 @@ Returns the count of files deleted." count user-emacs-directory))) (keymap-set cj/debug-config-keymap "c d" 'cj/delete-emacs-home-compiled-files) -(defun cj/compile-this-elisp-buffer () - "Compile the current .el: prefer native (.eln), else .elc. Message if neither." - (interactive) - (unless (and buffer-file-name (string-match-p "\\.el\\'" buffer-file-name)) - (user-error "Not visiting a .el file")) - (save-buffer) - (let ((file buffer-file-name)) +(defun cj/--compile-elisp-file (file &optional available-p) + "Compile FILE: prefer async native, then sync native, then byte-compile. +AVAILABLE-P decides which compilers exist; it defaults to `fboundp'. It is +a parameter so tests can force each branch without redefining `fboundp': +an `fset' on that subr pulls in comp-run and bytecomp, whose own `defun' +of `byte-compile-file' then lands on top of any test double." + (let ((available-p (or available-p #'fboundp))) (cond ;; Native compilation (async preferred) - ((fboundp 'native-compile-async) + ((funcall available-p 'native-compile-async) (native-compile-async file) (message "Queued native compilation for %s" file)) ;; Native compilation (sync, if async not available) - ((fboundp 'native-compile) + ((funcall available-p 'native-compile) (condition-case err (progn (native-compile file) (message "Native-compiled %s" file)) (error (message "Native compile failed: %s" (error-message-string err))))) ;; Byte-compile fallback - ((fboundp 'byte-compile-file) + ((funcall available-p 'byte-compile-file) (let ((out (byte-compile-file file))) (if out (message "Byte-compiled -> %s" out) @@ -224,6 +224,14 @@ Returns the count of files deleted." ;; Neither facility available (t (message "No compilation available (no native-compile, no byte-compile)"))))) + +(defun cj/compile-this-elisp-buffer () + "Compile the current .el: prefer native (.eln), else .elc. Message if neither." + (interactive) + (unless (and buffer-file-name (string-match-p "\\.el\\'" buffer-file-name)) + (user-error "Not visiting a .el file")) + (save-buffer) + (cj/--compile-elisp-file buffer-file-name)) (keymap-set cj/debug-config-keymap "c ." 'cj/compile-this-elisp-buffer) ;; --------------------------- Information Reporting --------------------------- diff --git a/modules/dirvish-config.el b/modules/dirvish-config.el index 6c849198..e9b9fde6 100644 --- a/modules/dirvish-config.el +++ b/modules/dirvish-config.el @@ -29,7 +29,7 @@ ;; - S: Study — start an org-drill session on the .org file at point ;; - M-D (Meta-Shift-d): DWIM shell commands menu ;; - TAB: Toggle subtree expansion -;; - F11: Toggle sidebar view +;; - F9: Toggle sidebar view ;;; Code: @@ -653,7 +653,13 @@ no popup frame is live." (("C-x d" . dirvish) ("C-x C-d" . dirvish) ("C-x D" . dirvish) - ("<f11>" . dirvish-side) + ;; F9, not F11: F11 is stock Emacs's toggle-frame-fullscreen, the same key + ;; every other application uses, and the sidebar sat on it for years by + ;; shadowing that. F9 is the free bare F-key: ai-term left it for M-SPC + ;; in June 2026 and the recording chord came off it on 2026-09-22. F2 is + ;; the preview key (markdown-mode binds it) and F5 is reserved for the + ;; debug backend, so neither was a candidate. + ("<f9>" . dirvish-side) :map dirvish-mode-map ("bg" . cj/set-wallpaper) ("/" . dirvish-narrow) diff --git a/modules/eat-config.el b/modules/eat-config.el index 01d0fbe6..f764848e 100644 --- a/modules/eat-config.el +++ b/modules/eat-config.el @@ -415,7 +415,8 @@ terminal. ai-term's agent buffers are managed separately via M-SPC." ;; Carried over from the ghostel era for the EAT agent terminals (ai-term). ;; Agents run EAT over tmux, so copy-mode is tmux's own copy-mode -- the same UX ;; ghostel-over-tmux had. C-<up> enters it and scrolls up in one stroke; C-; x c -;; enters it via the menu, and C-; x h grabs the whole pane history into a buffer. +;; enters it via the menu, C-; x h grabs the whole pane history into a buffer, +;; and C-; x d detaches the tmux client without going through its prefix. (declare-function cj/register-prefix-map "keybindings") (declare-function eat-emacs-mode "eat") @@ -585,6 +586,20 @@ scrollback) and moves point to the start of the line." (eat-emacs-mode) (beginning-of-line))) +(defun cj/term-tmux-detach () + "Detach the tmux client from inside an agent terminal. +Writes tmux's prefix and the detach key (C-b d) straight into the pty, the +same path `cj/term-copy-mode-dwim' uses for C-b [. A keyboard C-b inside +the Claude Code pane has been observed to land as stray text instead of +reaching tmux as a prefix (root cause not yet pinned down), so the string +path is the reliable one. Outside tmux it writes +nothing and says so, since C-b d typed into a plain shell is just a control +character." + (interactive) + (if (cj/term--in-tmux-p) + (cj/--term-send-string "\C-bd") + (message "cj/term-tmux-detach: not attached to tmux"))) + (defun cj/term--tmux-pane-in-copy-mode-p (pane-id) "Return non-nil when tmux PANE-ID is currently displaying a mode. tmux's `pane_in_mode' is 1 while a pane is in any mode; copy-mode is the only @@ -613,13 +628,15 @@ pty; without tmux, moves point up in EAT's emacs-mode buffer." (cj/term-copy-mode-dwim)) (forward-line -1))))) -;; The C-; x terminal prefix (copy-mode, tmux history, the F12 toggle). C-<up> +;; The C-; x terminal prefix (copy-mode, tmux detach, tmux history, the F12 +;; toggle). C-<up> ;; enters copy-mode + scrolls in one stroke; bound in EAT's semi-char map so it ;; reaches Emacs from inside an agent terminal. (defvar-keymap cj/term-map :doc "Personal terminal command map.") (cj/register-prefix-map "x" cj/term-map) (keymap-set cj/term-map "c" #'cj/term-copy-mode-dwim) +(keymap-set cj/term-map "d" #'cj/term-tmux-detach) (keymap-set cj/term-map "h" #'cj/term-tmux-history) (keymap-set cj/term-map "t" #'cj/term-toggle) diff --git a/modules/org-capture-config.el b/modules/org-capture-config.el index b7250f1e..8f5903fe 100644 --- a/modules/org-capture-config.el +++ b/modules/org-capture-config.el @@ -145,12 +145,16 @@ re-scanning large target files after the first successful lookup." :around #'cj/org-capture--set-target-location-advice)) ;; --------------------------- Capture F-Key Guard ----------------------------- -;; The global popup keys (F1 dashboard sweep, F10 music, F11 dirvish-side, +;; The global popup keys (F1 dashboard sweep, F9 dirvish-side, F10 music, ;; F12 terminal, M-SPC agent swap) fire even while a capture is in progress ;; and pop their UI over the capture popup. org-capture-mode is a minor ;; mode active exactly for the capture's duration and its keymap shadows ;; the global map, so blocking the keys there scopes the guard precisely: ;; the moment the capture finalizes or aborts, the keys work again. +;; +;; The list follows the command, not the key. F11 was on it while it held +;; dirvish-side; now that F11 is the frame fullscreen toggle it pops nothing +;; over a capture and stays usable. (defun cj/--org-capture-blocked-key () "Refuse a global popup key while a capture is in progress." @@ -160,7 +164,7 @@ re-scanning large target files after the first successful lookup." (defvar org-capture-mode-map) (with-eval-after-load 'org-capture - (dolist (key '("<f1>" "<f10>" "<f11>" "<f12>" "M-SPC")) + (dolist (key '("<f1>" "<f9>" "<f10>" "<f12>" "M-SPC")) (keymap-set org-capture-mode-map key #'cj/--org-capture-blocked-key))) ;; ----------------------- Project-Aware Capture Target ------------------------ diff --git a/modules/prog-general.el b/modules/prog-general.el index 77ff88a5..e9586a97 100644 --- a/modules/prog-general.el +++ b/modules/prog-general.el @@ -119,9 +119,11 @@ REGEXP must be a string or an rx form." ;; Manages tree-sitter grammars. Install is 'prompt, never t: with t, ;; merely opening a file could trigger a network download and a compiler -;; build mid-edit. Batch/test runs never load treesit-auto (no package -;; init), so they can never install. Fresh-machine bootstrap is the -;; explicit `cj/install-treesit-grammars' command below. +;; build mid-edit. `make test' runs with no package init and so never +;; loads treesit-auto, but a test file that calls `package-initialize' +;; itself does load it, and a tree-sitter mode then prompts for a missing +;; grammar; such tests must skip on `treesit-ready-p'. Fresh-machine +;; bootstrap is the explicit `cj/install-treesit-grammars' command below. (defun cj/treesit-auto-pin-go-revision (recipes) "Pin the Go grammar revision in treesit-auto RECIPES. Return the updated Go recipe, or nil when RECIPES has no Go entry. diff --git a/modules/system-defaults.el b/modules/system-defaults.el index d9ec1878..47bd1505 100644 --- a/modules/system-defaults.el +++ b/modules/system-defaults.el @@ -89,6 +89,30 @@ indicate the warning was handled." (advice-add 'display-warning :before-until #'cj/log-comp-warning) +;; ------------------ Deferred Daemon Warnings vs. Frame Creation ----------------- + +;; Emacs 31's warnings.el defers warnings raised during daemon startup: it puts +;; a one-shot closure on `after-make-frame-functions' holding the *Warnings* +;; buffer object and calls `warning--display-buffer' on it when the first +;; client frame is made. If that buffer died in between, `display-buffer' +;; signals inside `make-frame', server.el reports "-window-system-unsupported", +;; and emacsclient retries on $DISPLAY -- the first frame of the session +;; silently opens on XWayland. Keeping *Warnings* alive is the root fix +;; (undead-buffers.el); this guard is the backstop, so no future buffer sweep +;; can break frame creation the same way. The function only exists from +;; Emacs 31; advising an undefined symbol is harmless and takes effect once +;; warnings.el defines it. + +(defun cj/warning--display-buffer-if-live (orig buffer) + "Call ORIG with BUFFER only when it names or is a live buffer. +Around advice for `warning--display-buffer'. BUFFER may be a buffer object +or a buffer name, like `display-buffer' accepts. Return nil when skipped." + (let ((buf (and buffer (get-buffer buffer)))) + (when (buffer-live-p buf) + (funcall orig buf)))) + +(advice-add 'warning--display-buffer :around #'cj/warning--display-buffer-if-live) + ;; ---------------------------------- Unicode ---------------------------------- (set-locale-environment "en_US.UTF-8") diff --git a/modules/telega-config.el b/modules/telega-config.el index 5a20d430..b9b80481 100644 --- a/modules/telega-config.el +++ b/modules/telega-config.el @@ -18,9 +18,10 @@ ;; ;; TDLib (Telegram Database Library) runs in a docker container via ;; `telega-use-docker' so a fresh-clone install does not need a -;; system-level TDLib build. =scripts/setup-telega.sh= prepares the -;; container the first time; afterwards telega.el reattaches -;; automatically. +;; system-level TDLib build. The image is built locally from +;; =docker/telega-server/Dockerfile= with =make telega-image= (see the +;; pin section below for why it is not pulled from the registry); +;; =scripts/setup-telega.sh= covers the rest of a fresh clone. ;; ;; First-run auth (phone number + Telegram verification code) is ;; interactive and happens inside `M-x telega'. This module does not @@ -68,20 +69,29 @@ ;; telega picks its container image in `telega-docker--image-name', which only ;; pins to a version tag when `telega-tdlib-min-version' equals ;; `telega-tdlib-max-version' and the version ends in ".0". Here min is -;; "1.8.64" and max is nil, so that test never passes and the image is always +;; "1.8.66" and max is nil, so that test never passes and the image is always ;; "zevlg/telega-server:latest" -- a floating tag. The elpa package is fixed ;; at whatever version was installed, so the server can be replaced underneath ;; a static elisp without anything announcing it. ;; -;; Pinning by digest names one immutable image. Set to nil to hand the choice -;; back to telega. +;; The pin used to be a registry digest. It became a local tag on 2026-08-25: +;; the telega package raised its tdlib floor to 1.8.66, and upstream's only +;; image at that version fails to start (libglycin missing, +;; zevlg/telega.el#596). docker/telega-server/Dockerfile derives a working +;; image from that upstream digest plus the one missing package, and +;; `make telega-image' builds it under the tag below. The digest guarantee +;; now lives in the Dockerfile's FROM line; the Makefile owns the tag and a +;; test holds this default equal to it. Set to nil to hand the choice back +;; to telega. (defcustom cj/telega-docker-image - "zevlg/telega-server@sha256:a4b88e029ba381eca7c37c9618c9e3ad73aa9db2097fe07a0c6684d40d32b84e" + "cj/telega-server:1.8.66-glycin" "Container image reference for `telega-server', or nil for telega's default. -Pin by digest rather than tag: a tag can be re-pushed upstream, a digest -cannot. The default is the image carrying libtdjson 1.8.64, which matches -this telega's `telega-tdlib-min-version'." +The default names the image `make telega-image' builds locally from +docker/telega-server/Dockerfile, whose base is pinned by upstream digest. +It must match TELEGA_IMAGE in the Makefile; `cj/telega' refuses to launch +when the image is not present, since docker would otherwise try to pull a +local-only tag from the registry and fail confusingly." :type '(choice (const :tag "Let telega infer the image" nil) (string :tag "Image reference")) :group 'telega-docker) @@ -175,6 +185,21 @@ into telega's sentinel and abort its status handling and relogin path." (with-eval-after-load 'telega-server (advice-add 'telega-server--sentinel :after #'cj/--telega-server-notify-death)) +(defun cj/--telega-docker-image-present-p (image) + "Return non-nil when IMAGE exists in the local docker image store. +Uses `docker image inspect' rather than `docker images': the listing hides +digest-pulled and untagged images, and this check exists because that +listing lied once. Any failure (docker absent, daemon down) reads as +not-present, which routes the user to the same make target." + (condition-case nil + (zerop (call-process "docker" nil nil nil "image" "inspect" image)) + (error nil))) + +(defun cj/--telega-missing-image-message (image) + "Return the user-facing message for a pinned IMAGE that is not built yet." + (format "telega-server image %s is not built -- run `make telega-image' in %s" + image (abbreviate-file-name user-emacs-directory))) + (defun cj/telega () "Launch telega.el with a helpful message when it isn't installed yet. @@ -183,14 +208,22 @@ stale MELPA archive index can't take startup down with a 404. The trade-off: a fresh clone needs a one-time install before this launcher works. Without this wrapper, the autoload stub fails with the cryptic =Cannot open load file: telega=; with it, the user gets -pointed at =scripts/setup-telega.sh= and the manual fallback." +pointed at =scripts/setup-telega.sh= and the manual fallback. + +When `cj/telega-docker-image' is set, the image must already be built: +it is a local tag, so a missing one would send docker to the registry +for something that was never there. The check is skipped with no pin, +where telega infers and pulls its own image." (interactive) - (if (or (featurep 'telega) - (locate-library "telega")) - (telega) + (unless (or (featurep 'telega) + (locate-library "telega")) (user-error (concat "telega not installed -- run scripts/setup-telega.sh, " - "or `M-x package-install RET telega'")))) + "or `M-x package-install RET telega'"))) + (let ((image (cj/--telega-docker-pinned-image))) + (when (and image (not (cj/--telega-docker-image-present-p image))) + (user-error "%s" (cj/--telega-missing-image-message image)))) + (telega)) (cj/register-command "T" #'cj/telega) diff --git a/modules/transcription-config.el b/modules/transcription-config.el index 944063b8..6d7e39b9 100644 --- a/modules/transcription-config.el +++ b/modules/transcription-config.el @@ -33,6 +33,11 @@ ;; - 'assemblyai: Cloud transcription with speaker diarization ;; API key retrieved from authinfo.gpg (machine api.assemblyai.com) ;; - 'local-whisper: Local transcription (requires whisper installed) +;; - 'ratio: Self-hosted whisper plus speaker diarization on my transcription +;; host, via scripts/ratio-transcribe (ssh queue; falls back to a local +;; worker when the host is unreachable). No API key. SPEAKERS=N in the +;; environment pins the diarizer's speaker count; 1 is right for a +;; recording that holds only one side of a call. ;; ;; NOTIFICATIONS: ;; - "Transcription started on <file>" @@ -56,7 +61,8 @@ "Transcription backend to use. - `openai-api': Fast cloud transcription via OpenAI API - `assemblyai': Cloud transcription with speaker diarization via AssemblyAI -- `local-whisper': Local transcription using installed Whisper") +- `local-whisper': Local transcription using installed Whisper +- `ratio': Self-hosted whisper plus diarization on my transcription host") (defvar cj/transcription-keep-log-when-done nil "Whether to keep log files after successful transcription. @@ -74,9 +80,11 @@ Status: running, complete, error") (defconst cj/--transcription-backends '((openai-api :script "oai-transcribe" :auth-host "api.openai.com" :env-var "OPENAI_API_KEY") (assemblyai :script "assemblyai-transcribe" :auth-host "api.assemblyai.com" :env-var "ASSEMBLYAI_API_KEY") - (local-whisper :script "local-whisper" :auth-host nil :env-var nil)) + (local-whisper :script "local-whisper" :auth-host nil :env-var nil) + (ratio :script "ratio-transcribe" :auth-host nil :env-var nil)) "Per-backend descriptors. Each entry: (SYMBOL :script S :auth-host H :env-var V). -`:auth-host' and `:env-var' are nil for local backends that need no API key.") +`:auth-host' and `:env-var' are nil for backends that need no API key: the +local whisper install, and the self-hosted ratio queue reached over ssh.") (defun cj/--backend-plist (backend) "Return the descriptor plist for BACKEND, or signal if unknown." @@ -470,9 +478,11 @@ duration is computed from START-TIME." "Switch transcription backend. Prompts with completing-read to select from available backends." (interactive) - (let* ((backends '(("assemblyai" . assemblyai) - ("openai-api" . openai-api) - ("local-whisper" . local-whisper))) + ;; Offer exactly the descriptor set. This used to carry its own list, so + ;; a backend added to the descriptors was reachable only by setq. + (let* ((backends (mapcar (lambda (entry) + (cons (symbol-name (car entry)) (car entry))) + cj/--transcription-backends)) (current (symbol-name cj/transcribe-backend)) (prompt (format "Transcription backend (current: %s): " current)) (choice (completing-read prompt backends nil t)) diff --git a/modules/undead-buffers.el b/modules/undead-buffers.el index e5b8dc00..e6574a0e 100644 --- a/modules/undead-buffers.el +++ b/modules/undead-buffers.el @@ -31,7 +31,25 @@ (defvar cj/undead-buffer-list '("*scratch*" "*EMMS-Playlist*" "*Messages*" "*ert*" - "*AI-Assistant*") + "*AI-Assistant*" + ;; *Warnings* stays alive because Emacs 31's warnings.el defers daemon + ;; startup warnings into an `after-make-frame-functions' closure that + ;; holds this buffer object until the first client frame. The startup + ;; sweep in `cj/dashboard-only' used to kill it; the closure then failed + ;; inside `make-frame', server.el reported the window system as + ;; unsupported, and emacsclient silently retried on $DISPLAY, so the first + ;; frame of every 31.1 session opened on XWayland. I bury it instead, the + ;; same choice desktop.el makes in `desktop-clear-preserve-buffers'. + "*Warnings*" + ;; The async native-compile log stays alive for the same reason, one + ;; buffer over. comp-run parks every compile worker on this buffer and + ;; the worker's sentinel reads it back before starting the next job. + ;; The startup sweep killed it, which SIGHUPs every :noquery worker under + ;; it; each sentinel then died in `with-current-buffer' on the dead + ;; buffer and `comp--run-async-workers' never ran again, so the queue + ;; sat stranded for the life of the daemon, nothing was ever cached, and + ;; every boot re-ran the same compile storm at the first frame. + "*Async-native-compile-log*") "Buffer names to bury instead of killing (exact match).") (defvar cj/undead-buffer-regexps nil diff --git a/modules/video-audio-recording.el b/modules/video-audio-recording.el index 65a8612f..8fd3d23b 100644 --- a/modules/video-audio-recording.el +++ b/modules/video-audio-recording.el @@ -300,40 +300,13 @@ Changes take effect on the next recording (not the current one)." (cj/register-prefix-map "r" cj/record-map) -;; Fast chords for the two toggles, alongside C-; r v and C-; r a. F9 is free: -;; ai-term vacated the F9 family when its swap moved to M-SPC, and -;; `test-ai-term-f9-family-removed-globally' keeps it vacated. Both commands -;; still take a prefix argument, so C-u F9 prompts for the recording location. -(keymap-global-set "<f9>" #'cj/video-recording-toggle) -(keymap-global-set "S-<f9>" #'cj/audio-recording-toggle) - -(defvar eat-mode-map) -(defvar eat-semi-char-mode-map) -(defvar eat-char-mode-map) -(defvar eat-eshell-char-mode-map) - -;; EAT builds each input mode's keymap from key categories, and which categories -;; a mode claims is what decides whether a chord reaches Emacs at all. -;; -;; Semi-char mode -- the default, and where every agent buffer sits -- is built -;; from :ascii, :arrow and :navigation. It never claims function keys, so F9 -;; already fell through to the global map. The semi-char entry below is -;; belt-and-braces rather than the fix, which is why it reads as redundant. -;; -;; Char mode is the one that swallows F9. It adds :function, binding f1 through -;; f63 to eat-self-input, and it is a minor mode, so its map outranks -;; eat-mode-map. Without an entry here the pair splits in the worst possible -;; way: :function claims only the unmodified keys, so S-F9 would toggle audio in -;; a char-mode buffer while F9 went to the program under the cursor. That is a -;; recording you believe you started and didn't. -;; -;; Claiming both costs a char-mode program the use of F9. I would rather pay -;; that than ship a toggle that works for audio and silently fails for video. -(with-eval-after-load 'eat - (dolist (map (list eat-semi-char-mode-map eat-mode-map - eat-char-mode-map eat-eshell-char-mode-map)) - (keymap-set map "<f9>" #'cj/video-recording-toggle) - (keymap-set map "S-<f9>" #'cj/audio-recording-toggle))) +;; The toggles live only under C-; r. They had a fast chord (F9 video, S-F9 +;; audio, claimed in every EAT map too) until 2026-09-22: with Fn Lock on the +;; Framework 13 the F-key row sends plain function keys, and a bare F9 press +;; started a screen recording. Both chords went together, because a lone S-F9 +;; on a live F-key row is one fat-finger from an unwanted audio recording. +;; Both commands still take a prefix argument, so C-u C-; r v prompts for the +;; recording location. (with-eval-after-load 'which-key (which-key-add-key-based-replacements diff --git a/modules/weather-config.el b/modules/weather-config.el index 84920776..ea137578 100644 --- a/modules/weather-config.el +++ b/modules/weather-config.el @@ -28,6 +28,13 @@ ;; :vc (:url "git@cjennings.net:emacs-wttrin.git" ;; :branch "release/0.4.0" ;; :rev :newest) + ;; wttrin declares xterm-color in its own Package-Requires, but nothing here + ;; reads that header: `:load-path' keeps package.el out of the picture, and + ;; use-package suppresses the `use-package-always-ensure' default whenever + ;; `:load-path' is present. The dependency came free under the `:vc' form + ;; above and stopped when the local checkout took over. Naming the package + ;; here installs it without disturbing the checkout. + :ensure xterm-color :demand t ;; REQUIRED: mode-line must start at Emacs startup :preface ;; Change this to t to enable debug logging |
