aboutsummaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorCraig Jennings <c@cjennings.net>2026-08-03 09:59:58 -0500
committerCraig Jennings <c@cjennings.net>2026-08-03 09:59:58 -0500
commitfa1e63e4b3e4dbe3841247c8efd2096d2d75390c (patch)
treeaa05e87097caaa426355d553110dc56541b9c7d5
parenteb0c2191b77e795cca7884e0690ea51c515527fd (diff)
downloaddotemacs-fa1e63e4b3e4dbe3841247c8efd2096d2d75390c.tar.gz
dotemacs-fa1e63e4b3e4dbe3841247c8efd2096d2d75390c.zip
feat(recording): put the video and audio toggles on F9 and Shift-F9HEADmain
The two toggles were only on C-; r v and C-; r a. F9 has been free since ai-term moved its family to M-SPC, so it goes to what I reach for mid-work. Both keep their prefix argument, so C-u F9 still prompts for a location. Reaching them from inside a terminal buffer turns on which key categories each EAT input mode claims. My first read of that was backwards. Semi-char mode is the default and where agent buffers sit. It's built from :ascii, :arrow and :navigation, so it never claims function keys and F9 already reached Emacs there. That entry is redundant. Char mode is the one that mattered. It adds :function, binding f1 through f63 to eat-self-input. It's also a minor mode, so its map outranks eat-mode-map. Worse, :function claims only the unmodified keys. Leaving char mode alone would have split the pair rather than broken it: Shift-F9 toggling audio while F9 went to the program under the cursor. That is a recording I believe I started and didn't. Both chords are claimed in eat-char-mode-map and eat-eshell-char-mode-map, which costs a char-mode program the use of F9. I'd rather pay that than ship a toggle that half works. The tests resolve the char-mode cases through key-binding in a fixture that reproduces minor-mode precedence, not by reading the binding back out of the map the module just wrote. A positive control asserts F8 still reaches eat-self-input, since F8 sits in the same category and nothing here touches it. Without that control the fixture could be inert and every assertion would still pass, because F9 resolves through the global map either way.
-rw-r--r--modules/video-audio-recording.el35
-rw-r--r--tests/test-video-audio-recording--keybindings.el130
2 files changed, 165 insertions, 0 deletions
diff --git a/modules/video-audio-recording.el b/modules/video-audio-recording.el
index 10c10854..65a8612f 100644
--- a/modules/video-audio-recording.el
+++ b/modules/video-audio-recording.el
@@ -300,6 +300,41 @@ 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)))
+
(with-eval-after-load 'which-key
(which-key-add-key-based-replacements
"C-; r" "recording menu"
diff --git a/tests/test-video-audio-recording--keybindings.el b/tests/test-video-audio-recording--keybindings.el
new file mode 100644
index 00000000..cdb6493a
--- /dev/null
+++ b/tests/test-video-audio-recording--keybindings.el
@@ -0,0 +1,130 @@
+;;; test-video-audio-recording--keybindings.el --- recording toggle keybinding placement -*- lexical-binding: t; -*-
+
+;;; Commentary:
+;; The two recording toggles get a fast chord alongside the C-; r prefix: F9
+;; starts/stops video, S-F9 starts/stops audio.
+;;
+;; Reaching them from inside an EAT buffer turns on which key categories each
+;; input mode claims. Semi-char mode -- the default, and where agent buffers
+;; sit -- is built from (:ascii :arrow :navigation) and never claims function
+;; keys, so F9 already fell through to the global map there. Char mode adds
+;; :function, binding f1 through f63 to `eat-self-input', and it is a minor
+;; mode, so its map outranks `eat-mode-map'. The char-mode entries are the
+;; load-bearing ones; the semi-char entry is belt-and-braces.
+;;
+;; :function claims only the unmodified keys, which is why getting this wrong
+;; split the pair rather than breaking it outright: S-F9 toggled audio in a
+;; char-mode buffer while F9 went to the program under the cursor.
+;;
+;; These tests require eat first so the module's `with-eval-after-load' fires.
+;; The char-mode cases resolve through `key-binding' in a fixture that
+;; reproduces minor-mode precedence, because reading a binding back out of the
+;; map the module just wrote proves nothing about which map wins on a keypress.
+
+;;; Code:
+
+(require 'ert)
+(require 'package)
+
+(setq package-user-dir (expand-file-name "elpa" user-emacs-directory))
+(package-initialize)
+(add-to-list 'load-path (expand-file-name "modules" user-emacs-directory))
+(require 'eat)
+(require 'video-audio-recording)
+
+;;; Normal
+
+(ert-deftest test-video-audio-recording-f9-bound-globally ()
+ "Normal: F9 toggles video recording, S-F9 toggles audio recording."
+ (should (eq (lookup-key (current-global-map) (kbd "<f9>"))
+ #'cj/video-recording-toggle))
+ (should (eq (lookup-key (current-global-map) (kbd "S-<f9>"))
+ #'cj/audio-recording-toggle)))
+
+(ert-deftest test-video-audio-recording-f9-bound-in-eat-semi-char-mode-map ()
+ "Normal: both chords are bound in `eat-semi-char-mode-map'.
+Redundant rather than load-bearing: semi-char is built without :function, so a
+function key already falls through to the global map. Asserted anyway so the
+entry cannot be dropped silently while the comment explaining it stays."
+ (should (eq (keymap-lookup eat-semi-char-mode-map "<f9>")
+ #'cj/video-recording-toggle))
+ (should (eq (keymap-lookup eat-semi-char-mode-map "S-<f9>")
+ #'cj/audio-recording-toggle)))
+
+(ert-deftest test-video-audio-recording-f9-bound-in-eat-mode-map ()
+ "Normal: both chords are bound in `eat-mode-map', the major-mode map every
+EAT buffer carries regardless of input mode."
+ (should (eq (keymap-lookup eat-mode-map "<f9>")
+ #'cj/video-recording-toggle))
+ (should (eq (keymap-lookup eat-mode-map "S-<f9>")
+ #'cj/audio-recording-toggle)))
+
+(ert-deftest test-video-audio-recording-f9-bound-in-eat-char-mode-maps ()
+ "Normal: both chords are bound in the two char-mode maps.
+Char mode is built with EAT's :function category, which binds f1 through f63
+to `eat-self-input'. These entries are what override that."
+ (dolist (map (list eat-char-mode-map eat-eshell-char-mode-map))
+ (should (eq (keymap-lookup map "<f9>") #'cj/video-recording-toggle))
+ (should (eq (keymap-lookup map "S-<f9>") #'cj/audio-recording-toggle))))
+
+;;; Boundary
+
+(ert-deftest test-video-audio-recording-f9-chords-are-distinct ()
+ "Boundary: the shifted and unshifted chords resolve to different commands.
+A copy-paste binding both to the same toggle would satisfy every
+binding-is-present assertion above, so assert the difference directly."
+ (should-not (eq (lookup-key (current-global-map) (kbd "<f9>"))
+ (lookup-key (current-global-map) (kbd "S-<f9>")))))
+
+(defun test-video-audio-recording--in-char-mode (body)
+ "Run BODY in a buffer wired the way a live EAT char-mode buffer is.
+`eat--char-mode' is a minor mode, so its map is consulted ahead of the
+major-mode map. Reproducing that ordering is the point: reading a binding
+back out of the map the module just wrote proves nothing about which map wins
+when a key is actually pressed."
+ (with-temp-buffer
+ (use-local-map eat-mode-map)
+ (let ((minor-mode-overriding-map-alist
+ (list (cons 'eat--char-mode eat-char-mode-map)))
+ (eat--char-mode t))
+ (funcall body))))
+
+(ert-deftest test-video-audio-recording-f9-resolves-in-char-mode ()
+ "Boundary: both chords resolve to the toggles through the real precedence
+chain in a char-mode buffer. Before this override F9 resolved to
+`eat-self-input' and went to the program under the cursor, while S-F9 reached
+Emacs — so the pair silently split, audio recording and video not."
+ (test-video-audio-recording--in-char-mode
+ (lambda ()
+ (should (eq (key-binding (kbd "<f9>")) #'cj/video-recording-toggle))
+ (should (eq (key-binding (kbd "S-<f9>")) #'cj/audio-recording-toggle)))))
+
+;;; Error
+
+(ert-deftest test-video-audio-recording-char-mode-fixture-really-is-char-mode ()
+ "Error (positive control): the char-mode fixture genuinely puts EAT's map in
+front. F8 sits in the same :function category as F9 and this module never
+touches it, so it must still reach `eat-self-input'. If it resolves anywhere
+else the fixture is inert, and the resolution test above would pass without
+ever consulting `eat-char-mode-map' — which is precisely how the first cut of
+this file missed that F9 was being swallowed there."
+ (test-video-audio-recording--in-char-mode
+ (lambda ()
+ (should (eq (key-binding (kbd "<f8>")) #'eat-self-input)))))
+
+(ert-deftest test-video-audio-recording-f9-targets-are-commands ()
+ "Error: a key bound to a non-interactive function fails at press time with a
+`commandp' error rather than at load, so assert both targets are real commands."
+ (should (commandp (lookup-key (current-global-map) (kbd "<f9>"))))
+ (should (commandp (lookup-key (current-global-map) (kbd "S-<f9>")))))
+
+(ert-deftest test-video-audio-recording-prefix-bindings-still-reachable ()
+ "Error/regression (positive control): the fast chords must not disturb the
+C-; r prefix path. Without this, deleting the prefix map outright would leave
+every assertion above green."
+ (should (eq (keymap-lookup cj/record-map "v") #'cj/video-recording-toggle))
+ (should (eq (keymap-lookup cj/record-map "a") #'cj/audio-recording-toggle))
+ (should (eq (keymap-lookup cj/custom-keymap "r") cj/record-map)))
+
+(provide 'test-video-audio-recording--keybindings)
+;;; test-video-audio-recording--keybindings.el ends here