From 714da820136ac89d6a79ed0141e7325829ab8204 Mon Sep 17 00:00:00 2001 From: Gabriel Schneider Date: Tue, 29 Sep 2026 21:44:44 -0300 Subject: F1-F25, Insert and the keypad reach a GUI's terminal panes: one vaxis key map, vaxis_input.zig, for the tty, the gui and the board The vaxis-to-Event key mapping was copied in tty.zig, gui.zig and esp32p4.zig, and the copies had drifted: the gui's (its stdin feed, which test windows and a gui started in a terminal read) had 9 arms against the tty's 26, without the function keys, Insert or the keypad, so they reached a terminal pane as private-use codepoints. vaxis_input.zig holds effCp, mapKey and keyEvent once; the gui posts through postKey, and a test runs F5, Insert and the keypad's Up through it into a terminal pane. Co-Authored-By: Claude Opus 5.5 --- src/esp32p4.zig | 34 +++-------------------- src/gui/gui.zig | 71 ++++++++++++++++++++++++------------------------ src/main.zig | 1 + src/tty/tty.zig | 66 +++++---------------------------------------- src/vaxis_input.zig | 77 +++++++++++++++++++++++++++++++++++++++++++++++++++++ 5 files changed, 123 insertions(+), 126 deletions(-) create mode 100644 src/vaxis_input.zig diff --git a/src/esp32p4.zig b/src/esp32p4.zig index b7725b81..4e1fe036 100644 --- a/src/esp32p4.zig +++ b/src/esp32p4.zig @@ -32,6 +32,7 @@ const std = @import("std"); const builtin = @import("builtin"); const pardes = @import("pardes.zig"); const vaxis = @import("vaxis"); +const vaxis_input = @import("vaxis_input.zig"); // ------------------------------------------------------------------ what a freestanding root owes // @@ -394,7 +395,7 @@ fn apply(c: *pardes.Pardes, ev: vaxis.Event) void { // text at all, so a line break inside a paste arrives as a bare CR (Key.enter) or, from // a terminal that does not translate them, as ctrl+j. const text = key.text orelse ""; - const cp = mapKey(effCp(key)); + const cp = vaxis_input.mapKey(vaxis_input.effCp(key)); const bytes: []const u8 = if (text.len > 0) text else if (cp == pardes.Key.tab) @@ -406,7 +407,7 @@ fn apply(c: *pardes.Pardes, ev: vaxis.Event) void { if (bytes.len > 0) paste_buf.appendSlice(gpa(), bytes) catch {}; } else { c.update(.{ .key = .{ - .cp = mapKey(effCp(key)), + .cp = vaxis_input.mapKey(vaxis_input.effCp(key)), .text = key.text orelse "", .ctrl = key.mods.ctrl, .alt = key.mods.alt, @@ -529,35 +530,6 @@ fn apply(c: *pardes.Pardes, ev: vaxis.Event) void { } } -/// The effective codepoint the way vaxis's own `Key.matches` sees it: a single-character `text` -/// wins, because the terminal has already resolved shift; otherwise the shifted codepoint. -fn effCp(key: vaxis.Key) u21 { - if (key.text) |t| { - const view = std.unicode.Utf8View.init(t) catch return key.codepoint; - var it = view.iterator(); - if (it.nextCodepoint()) |cp| { - if (it.nextCodepoint() == null) return cp; - } - } - return key.shifted_codepoint orelse key.codepoint; -} - -/// vaxis functional-key codepoints -> core constants. The ASCII ones already coincide, so -/// enter/tab/escape/backspace pass straight through. -fn mapKey(cp: u21) u21 { - return switch (cp) { - vaxis.Key.up => pardes.Key.up, - vaxis.Key.down => pardes.Key.down, - vaxis.Key.left => pardes.Key.left, - vaxis.Key.right => pardes.Key.right, - vaxis.Key.home => pardes.Key.home, - vaxis.Key.end => pardes.Key.end, - vaxis.Key.page_up => pardes.Key.page_up, - vaxis.Key.page_down => pardes.Key.page_down, - vaxis.Key.delete => pardes.Key.delete, - else => cp, - }; -} export fn pardes_esp32p4_tick(now_ms: u64) callconv(.c) void { const c = core orelse return; last_now_ms = now_ms; diff --git a/src/gui/gui.zig b/src/gui/gui.zig index df565b94..8b100cd8 100644 --- a/src/gui/gui.zig +++ b/src/gui/gui.zig @@ -6,6 +6,7 @@ const libc = std.c; const vaxis = @import("vaxis"); // test modes only: the stdin escape-seq parser const ghostty_vt = @import("ghostty-vt"); // 256-color palette for .index cells const pardes = @import("../pardes.zig"); +const vaxis_input = @import("../vaxis_input.zig"); const dump = @import("../dump.zig"); const config = @import("../config.zig"); const look = @import("../look.zig"); @@ -3225,15 +3226,7 @@ const StdinFeed = struct { const event = result.event orelse continue; switch (event) { .key_press => |key| { - var text: []const u8 = ""; - if (key.text) |t| text = f.cache.put(t); - in.post(.{ .key = .{ - .cp = mapKey(effCp(key)), - .text = text, - .ctrl = key.mods.ctrl, - .alt = key.mods.alt, - .shift = key.mods.shift, - } }); + postKey(in, key, if (key.text) |t| f.cache.put(t) else ""); out.n_events += 1; }, .mouse => |m| { @@ -3382,6 +3375,40 @@ fn dumpGrid(gpa: std.mem.Allocator, surface: *pardes.Surface) !void { _ = host_io.writeFd(1, frame); } +/// A key parsed from the stdin a test window reads, posted as the tty +/// posts one (vaxis_input): F-keys, Insert and the keypad included. `text` +/// is the key's text as the feed keeps it. +fn postKey(in: *Input, key: vaxis.Key, text: []const u8) void { + var ev = vaxis_input.keyEvent(key); + ev.key.text = text; + in.post(ev); +} + +test "the GUI's stdin keys reach a terminal pane as the tty's do: F5, Insert and the keypad's Up" { + const gpa = std.testing.allocator; + const core = try pardes.Pardes.init(gpa, .{ .tty_only = true }); + defer core.deinit(); + core.panes[0].?.body.mode = .tty; + while (core.nextEffect()) |_| {} + var in: Input = .{ .core = core }; + var parser: vaxis.Parser = .{}; + var bytes: []const u8 = "\x1b[15~\x1b[2~\x1b[57419u"; + while (bytes.len > 0) { + const result = try parser.parse(bytes, gpa); + bytes = bytes[result.n..]; + if (result.event) |event| switch (event) { + .key_press => |key| postKey(&in, key, key.text orelse ""), + else => {}, + }; + } + var written: std.Io.Writer.Allocating = .init(gpa); + defer written.deinit(); + while (core.nextEffect()) |effect| { + if (effect == .write) try written.writer.writeAll(effect.write.bytes.slice()); + } + try std.testing.expectEqualStrings("\x1b[15~\x1b[2~\x1b[A", written.written()); +} + const Input = struct { core: ?*pardes.Pardes = null, client: ?*detached_client.Client = null, @@ -10439,32 +10466,6 @@ fn miniGlyph(ch: u8) [5]u8 { }; } -fn effCp(key: vaxis.Key) u21 { - if (key.text) |t| { - const view = std.unicode.Utf8View.init(t) catch return key.codepoint; - var it = view.iterator(); - if (it.nextCodepoint()) |cp| { - if (it.nextCodepoint() == null) return cp; - } - } - return key.shifted_codepoint orelse key.codepoint; -} - -fn mapKey(cp: u21) u21 { - return switch (cp) { - vaxis.Key.up => pardes.Key.up, - vaxis.Key.down => pardes.Key.down, - vaxis.Key.left => pardes.Key.left, - vaxis.Key.right => pardes.Key.right, - vaxis.Key.home => pardes.Key.home, - vaxis.Key.end => pardes.Key.end, - vaxis.Key.page_up => pardes.Key.page_up, - vaxis.Key.page_down => pardes.Key.page_down, - vaxis.Key.delete => pardes.Key.delete, - else => cp, - }; -} - fn envU16(env: *std.process.Environ.Map, name: []const u8) ?u16 { const raw = env.get(name) orelse return null; return std.fmt.parseInt(u16, raw, 10) catch null; diff --git a/src/main.zig b/src/main.zig index 6ea05479..17d0fb5f 100644 --- a/src/main.zig +++ b/src/main.zig @@ -525,6 +525,7 @@ test { _ = @import("fs.zig"); _ = @import("detached/wire.zig"); _ = @import("ShaderBuild.zig"); + _ = @import("vaxis_input.zig"); _ = @import("detached/server.zig"); _ = @import("detached/client.zig"); _ = @import("9p_io.zig"); diff --git a/src/tty/tty.zig b/src/tty/tty.zig index 3902271c..3441bc29 100644 --- a/src/tty/tty.zig +++ b/src/tty/tty.zig @@ -5,6 +5,7 @@ const posix = std.posix; const libc = std.c; const vaxis = @import("vaxis"); const pardes = @import("../pardes.zig"); +const vaxis_input = @import("../vaxis_input.zig"); const dump = @import("../dump.zig"); const tracy = @import("../tracy.zig"); const look = @import("../look.zig"); @@ -1510,7 +1511,7 @@ const Shell = struct { const bytes = pasteBytes(key); const room = max_paste_bytes -| s.paste_buf.written().len; s.paste_buf.writer.writeAll(bytes[0..@min(bytes.len, room)]) catch {}; - } else core.update(keyEvent(key)), + } else core.update(vaxis_input.keyEvent(key)), .mouse => |m| { const pdf_before = nativePdfWheelTarget(core, m); if (mouseEvent(m)) |ev| core.update(ev); @@ -2153,72 +2154,17 @@ fn readPty(io: std.Io, gpa: std.mem.Allocator, pty: std.Io.File, id: usize, gen: _ = loop.tryPostEvent(.{ .pty_eof = .{ .id = id, .gen = gen } }) catch {}; } -fn effCp(key: vaxis.Key) u21 { - if (key.text) |t| { - const view = std.unicode.Utf8View.init(t) catch return key.codepoint; - var it = view.iterator(); - if (it.nextCodepoint()) |c| { - if (it.nextCodepoint() == null) return c; - } - } - return key.shifted_codepoint orelse key.codepoint; -} - -fn mapKey(cp: u21) u21 { - return switch (cp) { - vaxis.Key.kp_enter => pardes.Key.enter, - vaxis.Key.up => pardes.Key.up, - vaxis.Key.down => pardes.Key.down, - vaxis.Key.left => pardes.Key.left, - vaxis.Key.right => pardes.Key.right, - vaxis.Key.home => pardes.Key.home, - vaxis.Key.end => pardes.Key.end, - vaxis.Key.page_up => pardes.Key.page_up, - vaxis.Key.page_down => pardes.Key.page_down, - vaxis.Key.delete => pardes.Key.delete, - vaxis.Key.insert => pardes.Key.insert, - vaxis.Key.print_screen => pardes.Key.print_screen, - vaxis.Key.scroll_lock => pardes.Key.scroll_lock, - vaxis.Key.pause => pardes.Key.pause, - vaxis.Key.menu => pardes.Key.menu, - vaxis.Key.f1...vaxis.Key.f25 => pardes.Key.f1 + (cp - vaxis.Key.f1), - // The keypad's own keys, which the kitty keyboard protocol tells - // apart, do what the keys they are named for do. - vaxis.Key.kp_left => pardes.Key.left, - vaxis.Key.kp_right => pardes.Key.right, - vaxis.Key.kp_up => pardes.Key.up, - vaxis.Key.kp_down => pardes.Key.down, - vaxis.Key.kp_page_up => pardes.Key.page_up, - vaxis.Key.kp_page_down => pardes.Key.page_down, - vaxis.Key.kp_home => pardes.Key.home, - vaxis.Key.kp_end => pardes.Key.end, - vaxis.Key.kp_insert => pardes.Key.insert, - vaxis.Key.kp_delete => pardes.Key.delete, - else => cp, - }; -} - const max_paste_bytes: usize = 4 << 20; fn pasteBytes(key: vaxis.Key) []const u8 { const text = key.text orelse ""; if (text.len > 0) return text; - const cp = mapKey(effCp(key)); + const cp = vaxis_input.mapKey(vaxis_input.effCp(key)); if (cp == pardes.Key.tab) return "\t"; if (cp == pardes.Key.enter or (key.mods.ctrl and cp == 'j')) return "\n"; return ""; } -fn keyEvent(key: vaxis.Key) pardes.Event { - return .{ .key = .{ - .cp = mapKey(effCp(key)), - .text = key.text orelse "", - .ctrl = key.mods.ctrl, - .alt = key.mods.alt, - .shift = key.mods.shift, - } }; -} - test "a terminal pane gets function keys as their sequences and lone modifiers not at all" { if (comptime builtin.os.tag == .windows) return error.SkipZigTest; const gpa = std.testing.allocator; @@ -2271,7 +2217,7 @@ test "a terminal pane gets function keys as their sequences and lone modifiers n core.panes[0].?.body.mode = .tty; while (core.nextEffect()) |_| {} while (try loop.tryEvent()) |event| switch (event) { - .key_press => |key| core.update(keyEvent(key)), + .key_press => |key| core.update(vaxis_input.keyEvent(key)), else => {}, }; var written: std.Io.Writer.Allocating = .init(gpa); @@ -2285,7 +2231,7 @@ test "a terminal pane gets function keys as their sequences and lone modifiers n test "keypad Enter matches Return for editor input terminal writes and paste" { for ([_]u21{ vaxis.Key.enter, vaxis.Key.kp_enter }) |codepoint| { const key: vaxis.Key = .{ .codepoint = codepoint }; - const event = keyEvent(key); + const event = vaxis_input.keyEvent(key); try std.testing.expectEqual(pardes.Key.enter, event.key.cp); try std.testing.expectEqualStrings("\n", pasteBytes(key)); { @@ -2757,7 +2703,7 @@ const Attach = struct { const bytes = pasteBytes(key); const room = max_paste_bytes -| a.paste_buf.written().len; a.paste_buf.writer.writeAll(bytes[0..@min(bytes.len, room)]) catch {}; - } else return a.send(keyEvent(key)), + } else return a.send(vaxis_input.keyEvent(key)), .mouse => |m| if (mouseEvent(m)) |ev| return a.send(ev), .paste => |bytes| { defer a.gpa.free(@constCast(bytes)); diff --git a/src/vaxis_input.zig b/src/vaxis_input.zig new file mode 100644 index 00000000..0e25dea1 --- /dev/null +++ b/src/vaxis_input.zig @@ -0,0 +1,77 @@ +//! A key as vaxis parses it from a terminal's bytes, as the core's Event: +//! the one map for every shell that reads a terminal (tty, gui's stdin, +//! esp32p4). Three copies had drifted; the gui's had lost the F-keys, +//! Insert and the keypad. +const std = @import("std"); +const vaxis = @import("vaxis"); +const pardes = @import("pardes.zig"); + +/// The effective codepoint the way vaxis's own `Key.matches` sees it: a +/// single-character `text` wins, the terminal having resolved shift; +/// otherwise the shifted codepoint. +pub fn effCp(key: vaxis.Key) u21 { + if (key.text) |t| { + const view = std.unicode.Utf8View.init(t) catch return key.codepoint; + var it = view.iterator(); + if (it.nextCodepoint()) |c| { + if (it.nextCodepoint() == null) return c; + } + } + return key.shifted_codepoint orelse key.codepoint; +} + +/// vaxis's functional-key codepoints as the core's; the ASCII ones +/// (enter, tab, escape, backspace) already coincide. +pub fn mapKey(cp: u21) u21 { + return switch (cp) { + vaxis.Key.kp_enter => pardes.Key.enter, + vaxis.Key.up => pardes.Key.up, + vaxis.Key.down => pardes.Key.down, + vaxis.Key.left => pardes.Key.left, + vaxis.Key.right => pardes.Key.right, + vaxis.Key.home => pardes.Key.home, + vaxis.Key.end => pardes.Key.end, + vaxis.Key.page_up => pardes.Key.page_up, + vaxis.Key.page_down => pardes.Key.page_down, + vaxis.Key.delete => pardes.Key.delete, + vaxis.Key.insert => pardes.Key.insert, + vaxis.Key.print_screen => pardes.Key.print_screen, + vaxis.Key.scroll_lock => pardes.Key.scroll_lock, + vaxis.Key.pause => pardes.Key.pause, + vaxis.Key.menu => pardes.Key.menu, + vaxis.Key.f1...vaxis.Key.f25 => pardes.Key.f1 + (cp - vaxis.Key.f1), + // The keypad's own keys, which the kitty keyboard protocol tells + // apart, do what the keys they are named for do. + vaxis.Key.kp_left => pardes.Key.left, + vaxis.Key.kp_right => pardes.Key.right, + vaxis.Key.kp_up => pardes.Key.up, + vaxis.Key.kp_down => pardes.Key.down, + vaxis.Key.kp_page_up => pardes.Key.page_up, + vaxis.Key.kp_page_down => pardes.Key.page_down, + vaxis.Key.kp_home => pardes.Key.home, + vaxis.Key.kp_end => pardes.Key.end, + vaxis.Key.kp_insert => pardes.Key.insert, + vaxis.Key.kp_delete => pardes.Key.delete, + else => cp, + }; +} + +/// The key as the core's event, its text the key's own. +pub fn keyEvent(key: vaxis.Key) pardes.Event { + return .{ .key = .{ + .cp = mapKey(effCp(key)), + .text = key.text orelse "", + .ctrl = key.mods.ctrl, + .alt = key.mods.alt, + .shift = key.mods.shift, + } }; +} + +test "F-keys, Insert and the keypad map to the core's keys" { + try std.testing.expectEqual(pardes.Key.f1 + 4, mapKey(vaxis.Key.f5)); + try std.testing.expectEqual(pardes.Key.f1 + 24, mapKey(vaxis.Key.f25)); + try std.testing.expectEqual(pardes.Key.insert, mapKey(vaxis.Key.insert)); + try std.testing.expectEqual(pardes.Key.up, mapKey(vaxis.Key.kp_up)); + try std.testing.expectEqual(pardes.Key.enter, keyEvent(.{ .codepoint = vaxis.Key.kp_enter }).key.cp); + try std.testing.expectEqual(@as(u21, 'a'), mapKey('a')); +} -- cgit v1.3