aboutsummaryrefslogtreecommitdiffstats
path: root/src/app.c
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 /src/app.c
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 'src/app.c')
-rw-r--r--src/app.c32
1 files changed, 32 insertions, 0 deletions
diff --git a/src/app.c b/src/app.c
index 297bf00..d600543 100644
--- a/src/app.c
+++ b/src/app.c
@@ -3037,6 +3037,7 @@ static size_t help_rows(help_row_t *out, size_t cap) {
char chords[40] = {0};
size_t used = 0;
for (size_t i = 0; i < CFG.nbinds; i++) {
+ if (CFG.binds[i].direct) continue; /* listed on their own, below */
action_t bound = CFG.binds[i].action;
/* The nine tab digits are one row: nine of them is a table, not a
* thing to learn. */
@@ -3064,6 +3065,23 @@ static size_t help_rows(help_row_t *out, size_t cap) {
n++;
}
}
+
+ /* Bindings that need no leader, in their own section because the caption at
+ * the top of the sheet says "<prefix> then:" and these are the ones that is
+ * not true of. Listed in the order they were bound rather than by action:
+ * they are a short, deliberate list somebody wrote by hand. */
+ if (config_has_direct(&CFG) && n < cap) {
+ out[n++] = (help_row_t){NULL, {0}, "without the leader"};
+ for (size_t i = 0; i < CFG.nbinds && n < cap; i++) {
+ if (!CFG.binds[i].direct) continue;
+ const char *label = config_action_label(CFG.binds[i].action);
+ if (!label) continue; /* unbound with `none`, or nothing to say */
+ out[n] = (help_row_t){"direct", {0}, label};
+ config_chord_name(CFG.binds[i].key, CFG.binds[i].mods, out[n].chord,
+ sizeof out[n].chord);
+ n++;
+ }
+ }
return n;
}
@@ -3975,6 +3993,20 @@ void app_event(app_t *a, const input_event_t *ev) {
a->prefix = true;
return;
}
+
+ /* Bindings that need no leader. Last, so the leader itself and every
+ * overlay that owns the keyboard have already had their say -- a direct
+ * binding must not fire while you are typing a pane's new name, and it
+ * must never shadow the prefix.
+ *
+ * Everything below this line goes to the program in the pane, so a chord
+ * bound here is a chord that program can no longer see. That is the deal,
+ * it is opt-in, and it is the user's keyboard. */
+ action_t direct = config_lookup_direct(&CFG, ev->key, mods);
+ if (direct != ACT_NONE && direct != ACT_LITERAL_PREFIX) {
+ prefix_command(a, ev);
+ return;
+ }
}
switch (ev->kind) {