aboutsummaryrefslogtreecommitdiffstats
path: root/tests
diff options
context:
space:
mode:
authoruser <user@clank>2026-08-15 11:45:56 +0200
committeruser <user@clank>2026-08-15 11:45:56 +0200
commite9dddc549de1ecd068fa152bf735a72d3a3d03c9 (patch)
treed2c9cef53917a0c444bcd05e03343e31487b9fcb /tests
parentsl0ppty: the prefix badge names the key you actually bound (diff)
downloadslosh-e9dddc549de1ecd068fa152bf735a72d3a3d03c9.tar.gz
slosh-e9dddc549de1ecd068fa152bf735a72d3a3d03c9.tar.xz
slosh-e9dddc549de1ecd068fa152bf735a72d3a3d03c9.zip
sl0ppty: bindings that need no leader
Every binding needed the prefix first, which is the safe design and the reason a leader exists: a multiplexer that takes chords away from the programs inside it is a multiplexer you fight. But it is the user's keyboard. Somebody who wants ctrl+alt+arrow to move between panes should be allowed to have it, including the part where that chord stops reaching vim. keys { direct { bind "ctrl+alt+left" "focus-left" bind "ctrl+alt+right" "focus-right" } } A block rather than a flag on each line, because these take a chord away from every program in every pane, and that is worth seeing at a glance -- and worth having somewhere to write the warning down, which the config now does in full: prefer chords no program wants, a bare letter certainly is not one, `none` gives it back. Three ordering decisions hold it together, none of them obvious from reading the code, all three tested: - the direct lookup happens *last* in the key path -- after the rename editor, the finder, the cheatsheet, and the prefix itself. An overlay still owns the keyboard, and a direct binding cannot shadow the leader, which would make the leader unusable with no way back. There is a test that tries exactly that and finds the prefix still working. - `direct` is part of a binding's identity rather than a property of it. `x` after the leader and `x` on its own are two bindings; binding one must not silently redefine the other. - the after-the-leader lookup falls through to direct bindings, so pressing the prefix first never makes a binding stop working. A rule you have to keep track of is a rule that will catch you out. The cheatsheet lists them under "without the leader", apart from the rest, because the caption at the top of it says "<prefix> then:" and these are precisely the ones that is not true of. The tests state the bargain rather than just the mechanism: the chord fires with no leader, and what it fires on, the program never sees -- asserted with `cat -v`, so a keystroke arriving in the pane is visible on screen rather than an invisible control byte that would prove nothing either way.
Diffstat (limited to 'tests')
-rw-r--r--tests/test_direct_keys.py148
1 files changed, 148 insertions, 0 deletions
diff --git a/tests/test_direct_keys.py b/tests/test_direct_keys.py
new file mode 100644
index 0000000..3929310
--- /dev/null
+++ b/tests/test_direct_keys.py
@@ -0,0 +1,148 @@
+#!/usr/bin/env python3
+"""Bindings that fire without the leader.
+
+Every binding used to need the prefix first, which is the safe design: a
+multiplexer that takes chords away from the programs inside it is a
+multiplexer you fight. But it is the user's keyboard, and somebody who wants
+ctrl+alt+arrow to move between panes should be allowed to have it — including
+the part where that chord no longer reaches anything else.
+
+So the deal is exactly this, and it is what these tests pin: a `direct` block
+fires without the prefix, and what it fires on, nothing else sees.
+"""
+import sys
+import tempfile
+
+from harness import Session, check, report
+
+# `cat -v` renders control bytes visibly, so "did the program see it" is a
+# question the screen can answer.
+SH = ["/bin/sh", "-c", 'printf "\\033]2;p\\007"; stty raw -echo; cat -v']
+
+C_M_RIGHT = r"\e[1;7C" # ctrl+alt+right
+C_M_LEFT = r"\e[1;7D" # ctrl+alt+left, deliberately left unbound
+
+
+def cfg(text):
+ f = tempfile.NamedTemporaryFile("w", suffix=".kdl", delete=False)
+ f.write(text)
+ f.close()
+ return f.name
+
+
+DIRECT = cfg('keys {\n'
+ ' direct {\n'
+ ' bind "ctrl+alt+right" "split-cols"\n'
+ ' }\n'
+ '}\n')
+
+
+def test_it_fires_with_no_leader():
+ with Session(SH, cols=80, rows=14, config=DIRECT) as s:
+ s.settle()
+ check("one pane to start", len(s.panes()) == 1)
+ s.send(C_M_RIGHT)
+ s.settle()
+ check("the chord split the pane on its own", len(s.panes()) == 2,
+ str(s.panes()))
+
+
+def test_what_it_fires_on_the_program_never_sees():
+ """The footgun, stated: a direct binding takes that chord away."""
+ with Session(SH, cols=80, rows=14, config=DIRECT) as s:
+ s.settle()
+ s.send(C_M_RIGHT)
+ s.settle(40)
+ check("the pane was not sent the keystroke",
+ "1;7C" not in s.snapshot().screen(), s.snapshot().screen()[:300])
+
+
+def test_everything_else_still_reaches_the_program():
+ with Session(SH, cols=80, rows=14, config=DIRECT) as s:
+ s.settle()
+ s.send(C_M_LEFT)
+ snap = s.until(lambda sn: "1;7D" in sn.screen())
+ check("an unbound chord goes straight through",
+ "1;7D" in snap.screen(), snap.screen()[:300])
+ check("and did nothing else", len(s.panes()) == 1, str(s.panes()))
+
+
+def test_it_also_works_after_the_leader():
+ """Pressing the prefix first must not make a binding stop working — that
+ would be a rule nobody could keep track of."""
+ with Session(SH, cols=80, rows=14, config=DIRECT) as s:
+ s.settle()
+ s.send(r"\x01" + C_M_RIGHT)
+ s.settle()
+ check("the leader then the chord does the same thing",
+ len(s.panes()) == 2, str(s.panes()))
+
+
+def test_prefixed_bindings_are_unaffected():
+ with Session(SH, cols=80, rows=14, config=DIRECT) as s:
+ s.settle()
+ s.send(r"\x01\\")
+ s.settle()
+ check("C-a \\ still splits", len(s.panes()) == 2, str(s.panes()))
+
+
+def test_the_leader_itself_cannot_be_stolen():
+ """A direct binding on the prefix key would make the prefix unusable, and
+ the ordering has to guarantee it cannot."""
+ conf = cfg('keys {\n'
+ ' prefix "ctrl+a"\n'
+ ' direct { bind "ctrl+a" "close-pane" }\n'
+ '}\n')
+ with Session(SH, cols=80, rows=14, config=conf) as s:
+ s.settle()
+ s.send(r"\x01")
+ s.settle()
+ check("the prefix is still the prefix", "C-a" in s.snapshot().line(1),
+ s.snapshot().line(1))
+ check("and the pane was not closed", len(s.panes()) == 1, str(s.panes()))
+
+
+def test_an_overlay_still_owns_the_keyboard():
+ """While a modal is up, a direct binding must not fire underneath it."""
+ with Session(SH, cols=92, rows=28, config=DIRECT) as s:
+ s.settle()
+ s.send(r"\x01?") # the cheatsheet
+ s.settle()
+ s.send(C_M_RIGHT)
+ s.settle()
+ check("the modal swallowed it", len(s.panes()) == 1, str(s.panes()))
+ check("and closed, as any key does",
+ "split into columns" not in s.snapshot().screen())
+
+
+def test_the_cheatsheet_lists_them_apart():
+ with Session(SH, cols=96, rows=30, config=DIRECT) as s:
+ s.settle()
+ s.send(r"\x01?")
+ s.settle()
+ screen = s.snapshot().screen()
+ check("under a heading of their own",
+ "without the leader" in screen, screen)
+ check("named the way you press them", "C-M-→" in screen, screen)
+
+
+def test_none_unbinds_a_direct_binding():
+ conf = cfg('keys {\n'
+ ' direct {\n'
+ ' bind "ctrl+alt+right" "split-cols"\n'
+ ' bind "ctrl+alt+right" "none"\n'
+ ' }\n'
+ '}\n')
+ with Session(SH, cols=80, rows=14, config=conf) as s:
+ s.settle()
+ s.send(C_M_RIGHT)
+ snap = s.until(lambda sn: "1;7C" in sn.screen())
+ check("it does not fire", len(s.panes()) == 1, str(s.panes()))
+ check("and the chord goes back to the program", "1;7C" in snap.screen(),
+ snap.screen()[:300])
+
+
+for name, fn in sorted(list(globals().items())):
+ if name.startswith("test_"):
+ fn()
+sys.exit(report())