get tab-scoped keys into the default keys output
This commit is contained in:
parent
8dc01e81ae
commit
0c3ddd1ffc
3 changed files with 213 additions and 20 deletions
135
src/tui.zig
135
src/tui.zig
|
|
@ -1023,15 +1023,8 @@ pub const App = struct {
|
|||
// No overrides — read from the tab's default_bindings.
|
||||
for (Module.tab.default_bindings) |binding| {
|
||||
if (!std.mem.eql(u8, @tagName(binding.action), action_tag_name)) continue;
|
||||
// Tab modules use `tab_framework.KeyCombo`, which is
|
||||
// structurally identical to but type-distinct from
|
||||
// `keybinds.KeyCombo`. Repack for `formatKeyCombo`.
|
||||
const combo: keybinds.KeyCombo = .{
|
||||
.codepoint = binding.key.codepoint,
|
||||
.mods = binding.key.mods,
|
||||
};
|
||||
var key_buf: [32]u8 = undefined;
|
||||
const s = keybinds.formatKeyCombo(combo, &key_buf) orelse continue;
|
||||
const s = keybinds.formatKeyCombo(binding.key, &key_buf) orelse continue;
|
||||
try out.append(arena, try arena.dupe(u8, s));
|
||||
}
|
||||
return out.toOwnedSlice(arena);
|
||||
|
|
@ -2265,6 +2258,63 @@ comptime {
|
|||
_ = tab_modules.projections;
|
||||
}
|
||||
|
||||
/// Write the full default keymap to `out` in keys.srf format,
|
||||
/// covering both the global section and each tab's local bindings.
|
||||
///
|
||||
/// Output shape:
|
||||
///
|
||||
/// ```
|
||||
/// #!srfv1
|
||||
/// # ... preamble + format docs ...
|
||||
///
|
||||
/// # ── Global ──
|
||||
/// action::quit,key::q
|
||||
/// ...
|
||||
///
|
||||
/// # ── Tab: portfolio ──
|
||||
/// scope::portfolio,action::toggle_account_picker,key::a
|
||||
/// ...
|
||||
/// ```
|
||||
///
|
||||
/// Per-tab sections are emitted in `tab_modules` declaration order
|
||||
/// and only when the tab has at least one default binding (skipping
|
||||
/// empty sections keeps the file compact).
|
||||
///
|
||||
/// Caller is responsible for flushing.
|
||||
fn writeDefaultKeys(out: *std.Io.Writer) !void {
|
||||
try keybinds.printDefaultsHeader(out);
|
||||
|
||||
try keybinds.printSectionHeader(out, "Global");
|
||||
try keybinds.printGlobalBindings(out);
|
||||
|
||||
inline for (std.meta.fields(@TypeOf(tab_modules))) |field| {
|
||||
const Module = @field(tab_modules, field.name);
|
||||
if (Module.tab.default_bindings.len == 0) continue;
|
||||
|
||||
const heading = "Tab: " ++ field.name;
|
||||
try keybinds.printSectionHeader(out, heading);
|
||||
|
||||
for (Module.tab.default_bindings) |binding| {
|
||||
try keybinds.printScopedBinding(
|
||||
out,
|
||||
field.name,
|
||||
@tagName(binding.action),
|
||||
binding.key,
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// CLI entry point for `--default-keys`. Writes the full default
|
||||
/// keymap to stdout. See `writeDefaultKeys` for output format.
|
||||
fn printDefaultKeys(io: std.Io) !void {
|
||||
var buf: [4096]u8 = undefined;
|
||||
var writer = std.Io.File.stdout().writer(io, &buf);
|
||||
const out = &writer.interface;
|
||||
try writeDefaultKeys(out);
|
||||
try out.flush();
|
||||
}
|
||||
|
||||
/// Entry point for the interactive TUI.
|
||||
/// `args` contains only command-local tokens (everything after `interactive`).
|
||||
pub fn run(
|
||||
|
|
@ -2286,7 +2336,7 @@ pub fn run(
|
|||
var i: usize = 0;
|
||||
while (i < args.len) : (i += 1) {
|
||||
if (std.mem.eql(u8, args[i], "--default-keys")) {
|
||||
try keybinds.printDefaults(io);
|
||||
try printDefaultKeys(io);
|
||||
return;
|
||||
} else if (std.mem.eql(u8, args[i], "--default-theme")) {
|
||||
try theme.printDefaults(io);
|
||||
|
|
@ -2700,7 +2750,7 @@ test "formatStatusHint: empty fragments returns empty string" {
|
|||
}
|
||||
|
||||
test "formatStatusHint: single fragment has no separator" {
|
||||
var arena_state = std.heap.ArenaAllocator.init(testing.allocator);
|
||||
var arena_state: std.heap.ArenaAllocator = .init(testing.allocator);
|
||||
defer arena_state.deinit();
|
||||
const arena = arena_state.allocator();
|
||||
|
||||
|
|
@ -2710,3 +2760,68 @@ test "formatStatusHint: single fragment has no separator" {
|
|||
const out = try formatStatusHint(arena, &fragments);
|
||||
try testing.expectEqualStrings("ctrl+s save", out);
|
||||
}
|
||||
|
||||
test "writeDefaultKeys: includes preamble, global section, and per-tab sections" {
|
||||
var aw: std.Io.Writer.Allocating = .init(testing.allocator);
|
||||
defer aw.deinit();
|
||||
try writeDefaultKeys(&aw.writer);
|
||||
const out = aw.written();
|
||||
|
||||
// Preamble.
|
||||
try testing.expect(std.mem.indexOf(u8, out, "#!srfv1") != null);
|
||||
try testing.expect(std.mem.indexOf(u8, out, "Regenerate: zfin interactive --default-keys") != null);
|
||||
|
||||
// Global section header + at least one global binding (un-scoped).
|
||||
try testing.expect(std.mem.indexOf(u8, out, "# ── Global ──") != null);
|
||||
try testing.expect(std.mem.indexOf(u8, out, "action::quit,key::q") != null);
|
||||
|
||||
// At least one tab section appears, and bindings inside it carry
|
||||
// their `scope::<tab>,` prefix (the user-edit format).
|
||||
try testing.expect(std.mem.indexOf(u8, out, "# ── Tab: ") != null);
|
||||
try testing.expect(std.mem.indexOf(u8, out, "scope::") != null);
|
||||
}
|
||||
|
||||
test "writeDefaultKeys: every registered tab with default_bindings has a section" {
|
||||
var aw: std.Io.Writer.Allocating = .init(testing.allocator);
|
||||
defer aw.deinit();
|
||||
try writeDefaultKeys(&aw.writer);
|
||||
const out = aw.written();
|
||||
|
||||
inline for (std.meta.fields(@TypeOf(tab_modules))) |field| {
|
||||
const Module = @field(tab_modules, field.name);
|
||||
if (Module.tab.default_bindings.len == 0) continue;
|
||||
|
||||
const heading = "# ── Tab: " ++ field.name ++ " ──";
|
||||
if (std.mem.indexOf(u8, out, heading) == null) {
|
||||
std.debug.print("missing tab section: {s}\n", .{heading});
|
||||
return error.MissingTabSection;
|
||||
}
|
||||
|
||||
// And every binding for that tab must show up as a `scope::<tab>,action::<name>` line.
|
||||
inline for (Module.tab.default_bindings) |binding| {
|
||||
const needle = "scope::" ++ field.name ++ ",action::" ++ @tagName(binding.action);
|
||||
if (std.mem.indexOf(u8, out, needle) == null) {
|
||||
std.debug.print("missing binding line: {s}\n", .{needle});
|
||||
return error.MissingBindingLine;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
test "writeDefaultKeys: tab sections appear in tab_modules declaration order" {
|
||||
var aw: std.Io.Writer.Allocating = .init(testing.allocator);
|
||||
defer aw.deinit();
|
||||
try writeDefaultKeys(&aw.writer);
|
||||
const out = aw.written();
|
||||
|
||||
var prev_pos: usize = 0;
|
||||
inline for (std.meta.fields(@TypeOf(tab_modules))) |field| {
|
||||
const Module = @field(tab_modules, field.name);
|
||||
if (Module.tab.default_bindings.len == 0) continue;
|
||||
|
||||
const heading = "# ── Tab: " ++ field.name ++ " ──";
|
||||
const pos = std.mem.indexOf(u8, out, heading) orelse return error.MissingTabSection;
|
||||
try testing.expect(pos >= prev_pos);
|
||||
prev_pos = pos;
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -307,11 +307,11 @@ fn parseKeyCombo(key_str: []const u8) ?KeyCombo {
|
|||
}
|
||||
|
||||
/// Print default keybindings in SRF format to stdout.
|
||||
pub fn printDefaults(io: std.Io) !void {
|
||||
var buf: [4096]u8 = undefined;
|
||||
var writer = std.Io.File.stdout().writer(io, &buf);
|
||||
const out = &writer.interface;
|
||||
|
||||
/// Write the SRF preamble (header line, file-purpose comments,
|
||||
/// format documentation) to `out`. Used by the `--default-keys`
|
||||
/// CLI to produce a self-documenting keys.srf file. Caller is
|
||||
/// responsible for writing the binding records and flushing.
|
||||
pub fn printDefaultsHeader(out: *std.Io.Writer) !void {
|
||||
try out.writeAll("#!srfv1\n");
|
||||
try out.writeAll("# zfin TUI keybindings\n");
|
||||
try out.writeAll("# This file is the sole source of keybindings when present.\n");
|
||||
|
|
@ -331,13 +331,51 @@ pub fn printDefaults(io: std.Io) !void {
|
|||
try out.writeAll("# refers to that tab's local Action enum. Tab-local bindings\n");
|
||||
try out.writeAll("# cannot use a key that's globally bound — zfin will refuse\n");
|
||||
try out.writeAll("# to start if you create that conflict.\n");
|
||||
}
|
||||
|
||||
/// Write a section comment header to `out`. Used to label the
|
||||
/// global vs. per-tab sections in the generated keys.srf.
|
||||
pub fn printSectionHeader(out: *std.Io.Writer, name: []const u8) !void {
|
||||
try out.writeAll("\n# ── ");
|
||||
try out.writeAll(name);
|
||||
try out.writeAll(" ──\n");
|
||||
}
|
||||
|
||||
/// Write the global default bindings to `out`, one per line.
|
||||
pub fn printGlobalBindings(out: *std.Io.Writer) !void {
|
||||
for (global_default_bindings) |b| {
|
||||
var key_buf: [32]u8 = undefined;
|
||||
const key_str = formatKeyCombo(b.key, &key_buf) orelse continue;
|
||||
try out.print("action::{s},key::{s}\n", .{ @tagName(b.action), key_str });
|
||||
}
|
||||
}
|
||||
|
||||
/// Write a single scoped binding record to `out` in the form:
|
||||
/// `scope::<scope>,action::<action_name>,key::<key>`.
|
||||
pub fn printScopedBinding(
|
||||
out: *std.Io.Writer,
|
||||
scope: []const u8,
|
||||
action_name: []const u8,
|
||||
key: KeyCombo,
|
||||
) !void {
|
||||
var key_buf: [32]u8 = undefined;
|
||||
const key_str = formatKeyCombo(key, &key_buf) orelse return;
|
||||
try out.print("scope::{s},action::{s},key::{s}\n", .{ scope, action_name, key_str });
|
||||
}
|
||||
|
||||
/// Print the full default keymap (globals only — this entry point
|
||||
/// pre-dates the per-tab `default_bindings`). Maintained for
|
||||
/// backward compatibility with anything calling it directly. New
|
||||
/// callers should prefer the per-piece helpers above and orchestrate
|
||||
/// the tab sections themselves; `src/tui.zig`'s `--default-keys`
|
||||
/// handler does this.
|
||||
pub fn printDefaults(io: std.Io) !void {
|
||||
var buf: [4096]u8 = undefined;
|
||||
var writer = std.Io.File.stdout().writer(io, &buf);
|
||||
const out = &writer.interface;
|
||||
try printDefaultsHeader(out);
|
||||
try printSectionHeader(out, "Global");
|
||||
try printGlobalBindings(out);
|
||||
try out.flush();
|
||||
}
|
||||
|
||||
|
|
@ -691,3 +729,43 @@ test "action_labels: every Action variant has a non-empty label" {
|
|||
try std.testing.expect(label.len > 0);
|
||||
}
|
||||
}
|
||||
|
||||
test "printScopedBinding writes scope, action, key in expected order" {
|
||||
var aw: std.Io.Writer.Allocating = .init(std.testing.allocator);
|
||||
defer aw.deinit();
|
||||
const key: KeyCombo = .{ .codepoint = 'a', .mods = .{} };
|
||||
try printScopedBinding(&aw.writer, "history", "compare_select", key);
|
||||
try std.testing.expectEqualStrings(
|
||||
"scope::history,action::compare_select,key::a\n",
|
||||
aw.written(),
|
||||
);
|
||||
}
|
||||
|
||||
test "printScopedBinding renders modifier keys" {
|
||||
var aw: std.Io.Writer.Allocating = .init(std.testing.allocator);
|
||||
defer aw.deinit();
|
||||
const key: KeyCombo = .{ .codepoint = 'c', .mods = .{ .ctrl = true } };
|
||||
try printScopedBinding(&aw.writer, "options", "filter_3", key);
|
||||
try std.testing.expectEqualStrings(
|
||||
"scope::options,action::filter_3,key::ctrl+c\n",
|
||||
aw.written(),
|
||||
);
|
||||
}
|
||||
|
||||
test "printGlobalBindings emits at least quit and refresh" {
|
||||
var aw: std.Io.Writer.Allocating = .init(std.testing.allocator);
|
||||
defer aw.deinit();
|
||||
try printGlobalBindings(&aw.writer);
|
||||
const out = aw.written();
|
||||
try std.testing.expect(std.mem.indexOf(u8, out, "action::quit,key::q") != null);
|
||||
try std.testing.expect(std.mem.indexOf(u8, out, "action::refresh,key::r") != null);
|
||||
// Globals never carry a scope:: prefix.
|
||||
try std.testing.expect(std.mem.indexOf(u8, out, "scope::") == null);
|
||||
}
|
||||
|
||||
test "printSectionHeader writes a comment header line" {
|
||||
var aw: std.Io.Writer.Allocating = .init(std.testing.allocator);
|
||||
defer aw.deinit();
|
||||
try printSectionHeader(&aw.writer, "Tab: portfolio");
|
||||
try std.testing.expectEqualStrings("\n# ── Tab: portfolio ──\n", aw.written());
|
||||
}
|
||||
|
|
|
|||
|
|
@ -114,11 +114,11 @@ const std = @import("std");
|
|||
const vaxis = @import("vaxis");
|
||||
|
||||
/// Re-exported KeyCombo so tab modules don't need to import
|
||||
/// keybinds.zig directly for binding declarations.
|
||||
pub const KeyCombo = struct {
|
||||
codepoint: u21,
|
||||
mods: vaxis.Key.Modifiers = .{},
|
||||
};
|
||||
/// keybinds.zig directly for binding declarations. This is the
|
||||
/// SAME type as `keybinds.KeyCombo` (re-export, not a copy) — the
|
||||
/// two names are interchangeable at type-level so values flow
|
||||
/// freely between framework and keybind code without repacking.
|
||||
pub const KeyCombo = @import("keybinds.zig").KeyCombo;
|
||||
|
||||
/// A single (action, key) pair for a tab's `default_bindings`.
|
||||
/// Generic over the tab's Action enum so `default_bindings` can
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue