diff options
Diffstat (limited to 'src/gui')
| -rw-r--r-- | src/gui/gui.zig | 240 |
1 files changed, 208 insertions, 32 deletions
diff --git a/src/gui/gui.zig b/src/gui/gui.zig index 045f22d9..1e58d3ba 100644 --- a/src/gui/gui.zig +++ b/src/gui/gui.zig @@ -3097,6 +3097,9 @@ fn runGrid(init: std.process.Init, opts_in: pardes.Options) !void { const StdinFeed = struct { parser: vaxis.Parser = .{}, + /// Inside the driver's bracketed paste: its keys are the paste's text. + in_paste: bool = false, + paste: std.ArrayList(u8) = .empty, cache: vaxis.GraphemeCache = .{}, buf: [1024]u8 = undefined, fill: usize = 0, @@ -3139,8 +3142,8 @@ const StdinFeed = struct { var seq_start: usize = 0; while (seq_start < len) { - // ESC]777;mouse;<down|up|motion>;<SDL button, 0 for none>;<x>;<y>BEL - // ESC]777;mouse;wheel;<amount>;<x>;<y>BEL + // ESC]777;mouse;<down|up|motion>;<SDL button, 0 for none>;<x>;<y>[;<mods>]BEL + // ESC]777;mouse;wheel;<amount>;<x>;<y>[;<mods>]BEL // A mouse event at window pixels, built as SDL would and run // through dispatch, so a driver exercises the real pointer path // (pixel to cell, tag and body hits). SGR mouse carries only cells. @@ -3159,6 +3162,9 @@ const StdinFeed = struct { const button = std.fmt.parseInt(u8, second, 10) catch 0; const x = std.fmt.parseFloat(f32, parts.next() orelse "") catch 0; const y = std.fmt.parseFloat(f32, parts.next() orelse "") catch 0; + // An optional fifth field holds modifiers as xterm + // counts them: shift 4, meta (alt) 8, ctrl 16. + const held = std.fmt.parseInt(u8, parts.next() orelse "0", 10) catch 0; var sev = std.mem.zeroes(c.SDL_Event); if (std.mem.eql(u8, kind, "wheel")) { // ESC]777;mouse;wheel;<y, SDL's sign: up positive>;<x>;<y>BEL: @@ -3169,6 +3175,9 @@ const StdinFeed = struct { sev.wheel.integer_y = @intFromFloat(@trunc(amount)); sev.wheel.mouse_x = x; sev.wheel.mouse_y = y; + } else if (std.mem.eql(u8, kind, "focus")) { + // ESC]777;mouse;focus;<1 gained, 0 lost>;0;0BEL + sev.type = if (button != 0) c.SDL_EVENT_WINDOW_FOCUS_GAINED else c.SDL_EVENT_WINDOW_FOCUS_LOST; } else if (std.mem.eql(u8, kind, "motion")) { sev.type = c.SDL_EVENT_MOUSE_MOTION; sev.motion.x = x; @@ -3181,7 +3190,14 @@ const StdinFeed = struct { sev.button.x = x; sev.button.y = y; } + const before = c.SDL_GetModState(); + var mods: c.SDL_Keymod = 0; + if (held & 4 != 0) mods |= c.SDL_KMOD_LSHIFT; + if (held & 8 != 0) mods |= c.SDL_KMOD_LALT; + if (held & 16 != 0) mods |= c.SDL_KMOD_LCTRL; + c.SDL_SetModState(mods); dispatch(gp, in, &sev); + c.SDL_SetModState(before); out.n_events += 1; } seq_start += mouse_prefix.len + end + 1; @@ -3228,10 +3244,24 @@ const StdinFeed = struct { seq_start += result.n; const event = result.event orelse continue; switch (event) { - .key_press => |key| { + .key_press => |key| if (f.in_paste) { + // A newline reads as Ctrl-J, as the tty's paste has it. + const text = key.text orelse if (key.codepoint == vaxis.Key.enter or (key.mods.ctrl and key.codepoint == 'j')) "\n" else if (key.codepoint == vaxis.Key.tab) "\t" else ""; + f.paste.appendSlice(gpa, text) catch {}; + } else { postKey(in, key, if (key.text) |t| f.cache.put(t) else ""); out.n_events += 1; }, + .paste_start => { + f.in_paste = true; + f.paste.clearRetainingCapacity(); + }, + .paste_end => { + f.in_paste = false; + if (f.paste.items.len > 0) in.post(.{ .paste = f.paste.items }); + f.paste.clearAndFree(gpa); + out.n_events += 1; + }, .mouse => |m| { const button: ?pardes.Mouse.Button = switch (m.button) { .left => .left, @@ -3256,6 +3286,8 @@ const StdinFeed = struct { .col = @intCast(@max(m.col, 0)), .row = @intCast(@max(m.row, 0)), .ctrl = m.mods.ctrl, + .shift = m.mods.shift, + .alt = m.mods.alt, } }); out.n_events += 1; } @@ -3412,6 +3444,47 @@ test "the GUI's stdin keys reach a terminal pane as the tty's do: F5, Insert and try std.testing.expectEqualStrings("\x1b[15~\x1b[2~\x1b[A", written.written()); } +test "the window's function and editing keys reach a terminal as its cursor keys' mode and keyboard flags say" { + 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 written: std.Io.Writer.Allocating = .init(gpa); + defer written.deinit(); + const Case = struct { modes: []const u8, sym: c.SDL_Keycode, mod: c.SDL_Keymod = 0, sent: []const u8 }; + const cases = [_]Case{ + .{ .modes = "", .sym = c.SDLK_UP, .sent = "\x1b[A" }, + .{ .modes = "", .sym = c.SDLK_HOME, .sent = "\x1b[H" }, + .{ .modes = "", .sym = c.SDLK_F1, .sent = "\x1bOP" }, + .{ .modes = "", .sym = c.SDLK_F5, .sent = "\x1b[15~" }, + .{ .modes = "", .sym = c.SDLK_F12, .mod = c.SDL_KMOD_LSHIFT, .sent = "\x1b[24;2~" }, + .{ .modes = "", .sym = c.SDLK_INSERT, .sent = "\x1b[2~" }, + // DECCKM: less, vim, htop. + .{ .modes = "\x1b[?1h", .sym = c.SDLK_UP, .sent = "\x1bOA" }, + .{ .modes = "", .sym = c.SDLK_END, .sent = "\x1bOF" }, + .{ .modes = "", .sym = c.SDLK_LEFT, .mod = c.SDL_KMOD_LCTRL, .sent = "\x1b[1;5D" }, + // codex's kitty flags (>7u): Ctrl-C and Alt-a are unambiguous. + // (A bare Escape at a shell prompt is pardes's way out of raw input.) + .{ .modes = "\x1b[?1l\x1b[>7u", .sym = 'c', .mod = c.SDL_KMOD_LCTRL, .sent = "\x1b[99;5u" }, + .{ .modes = "", .sym = 'a', .mod = c.SDL_KMOD_LALT, .sent = "\x1b[97;3u" }, + // With event types asked for (2), an arrow says it is a press, as + // ghostty says it. + .{ .modes = "", .sym = c.SDLK_UP, .sent = "\x1b[1;1:1A" }, + }; + for (cases) |case| { + core.update(.{ .output = .{ .pane = 0, .bytes = case.modes } }); + while (core.nextEffect()) |_| {} + keyDown(&in, case.sym, case.mod); + written.clearRetainingCapacity(); + while (core.nextEffect()) |effect| { + if (effect == .write) try written.writer.writeAll(effect.write.bytes.slice()); + } + try std.testing.expectEqualStrings(case.sent, written.written()); + } +} + const Input = struct { core: ?*pardes.Pardes = null, client: ?*detached_client.Client = null, @@ -3457,6 +3530,8 @@ fn dispatch(g: *Gui, in: *Input, sev: *const c.SDL_Event) void { } switch (sev.type) { c.SDL_EVENT_QUIT, c.SDL_EVENT_WINDOW_CLOSE_REQUESTED => in.close(), + c.SDL_EVENT_WINDOW_FOCUS_GAINED => in.post(.{ .focus = true }), + c.SDL_EVENT_WINDOW_FOCUS_LOST => in.post(.{ .focus = false }), c.SDL_EVENT_WINDOW_MOUSE_LEAVE => { g.pointer_present = false; g.pointer_mapped = false; @@ -3526,18 +3601,7 @@ fn dispatch(g: *Gui, in: *Input, sev: *const c.SDL_Event) void { }; g.pointer_mapped = true; g.pointer_cell = mc; - in.post(.{ - .mouse = .{ - .button = button, - .kind = if (b.down) .press else .release, - .col = mc.col, - .row = mc.row, - .tag_hit = mc.tag_hit, - .body_hit = mc.body_hit, - .rule_above = mc.rule_above, - .ctrl = (c.SDL_GetModState() & c.SDL_KMOD_CTRL) != 0, - }, - }); + in.post(.{ .mouse = pointerMouse(button, if (b.down) .press else .release, mc, pointerMods()) }); }, c.SDL_EVENT_MOUSE_MOTION => { const m = sev.motion; @@ -3559,14 +3623,7 @@ fn dispatch(g: *Gui, in: *Input, sev: *const c.SDL_Event) void { }; g.pointer_mapped = true; g.pointer_cell = mc; - in.post(.{ .mouse = .{ - .button = held orelse .none, - .kind = if (held != null) .drag else .motion, - .col = mc.col, - .row = mc.row, - .tag_hit = mc.tag_hit, - .body_hit = mc.body_hit, - } }); + in.post(.{ .mouse = pointerMouse(held orelse .none, if (held != null) .drag else .motion, mc, pointerMods()) }); }, c.SDL_EVENT_MOUSE_WHEEL => { const w = sev.wheel; @@ -3581,7 +3638,7 @@ fn dispatch(g: *Gui, in: *Input, sev: *const c.SDL_Event) void { }; g.pointer_mapped = true; g.pointer_cell = mc; - wheelAt(g, in, .{ .x = w.x, .y = w.y, .integer_y = w.integer_y }, mc); + wheelAt(g, in, .{ .x = w.x, .y = w.y, .integer_y = w.integer_y, .mods = pointerMods() }, mc); }, c.SDL_EVENT_FINGER_DOWN, c.SDL_EVENT_FINGER_MOTION, c.SDL_EVENT_FINGER_UP, c.SDL_EVENT_FINGER_CANCELED => { const f = sev.tfinger; @@ -3767,6 +3824,13 @@ fn keyDown(in: *Input, sym: c.SDL_Keycode, mod: c.SDL_Keymod) void { c.SDLK_PAGEUP => pardes.Key.page_up, c.SDLK_PAGEDOWN => pardes.Key.page_down, c.SDLK_DELETE => pardes.Key.delete, + c.SDLK_INSERT => pardes.Key.insert, + c.SDLK_PRINTSCREEN => pardes.Key.print_screen, + c.SDLK_SCROLLLOCK => pardes.Key.scroll_lock, + c.SDLK_PAUSE => pardes.Key.pause, + c.SDLK_APPLICATION => pardes.Key.menu, + c.SDLK_F1...c.SDLK_F12 => pardes.Key.f1 + @as(u21, @intCast(sym - c.SDLK_F1)), + c.SDLK_F13...c.SDLK_F24 => pardes.Key.f1 + 12 + @as(u21, @intCast(sym - c.SDLK_F13)), else => blk: { if (!(ctrl or alt or gui_mod)) break :blk 0; if (sym < 128 and sym >= ' ') break :blk @intCast(sym); @@ -3913,16 +3977,16 @@ fn wheelAt(g: *Gui, in: *Input, w: WheelTurn, mc: MouseCell) void { .pane = @intCast(id), .delta_pixels = -w.y * @as(f32, @floatFromInt(g.cell_h)), } }); - } else if (pane.isTerminal() and pardes.panes.terminal.ownsWheel(pane)) { + } else if (!w.mods.shift and pane.isTerminal() and pardes.panes.terminal.ownsWheel(pane)) { // The program's (codex, htop, less): every whole // turn goes to it, no glide, for nothing of ours - // moves. + // moves. Shift keeps the wheel pardes's. resetScroll(g); g.program_wheel = accumulateWheelDelta(g.program_wheel, w.y); while (g.program_wheel >= 1) : (g.program_wheel -= 1) - in.post(.{ .mouse = .{ .button = .wheel_down, .kind = .press, .col = mc.col, .row = mc.row, .tag_hit = mc.tag_hit, .body_hit = mc.body_hit } }); + in.post(.{ .mouse = .{ .button = .wheel_down, .kind = .press, .col = mc.col, .row = mc.row, .tag_hit = mc.tag_hit, .body_hit = mc.body_hit, .ctrl = w.mods.ctrl, .shift = w.mods.shift, .alt = w.mods.alt } }); while (g.program_wheel <= -1) : (g.program_wheel += 1) - in.post(.{ .mouse = .{ .button = .wheel_up, .kind = .press, .col = mc.col, .row = mc.row, .tag_hit = mc.tag_hit, .body_hit = mc.body_hit } }); + in.post(.{ .mouse = .{ .button = .wheel_up, .kind = .press, .col = mc.col, .row = mc.row, .tag_hit = mc.tag_hit, .body_hit = mc.body_hit, .ctrl = w.mods.ctrl, .shift = w.mods.shift, .alt = w.mods.alt } }); } else { g.scroll_pane = id; g.scroll_col = mc.col; @@ -3938,9 +4002,9 @@ fn wheelAt(g: *Gui, in: *Input, w: WheelTurn, mc: MouseCell) void { } else { g.scroll_delta = accumulateWheelDelta(g.scroll_delta, w.y); while (g.scroll_delta >= 1) : (g.scroll_delta -= 1) - in.post(.{ .mouse = .{ .button = .wheel_down, .kind = .press, .col = mc.col, .row = mc.row, .tag_hit = mc.tag_hit, .body_hit = mc.body_hit } }); + in.post(.{ .mouse = .{ .button = .wheel_down, .kind = .press, .col = mc.col, .row = mc.row, .tag_hit = mc.tag_hit, .body_hit = mc.body_hit, .ctrl = w.mods.ctrl, .shift = w.mods.shift, .alt = w.mods.alt } }); while (g.scroll_delta <= -1) : (g.scroll_delta += 1) - in.post(.{ .mouse = .{ .button = .wheel_up, .kind = .press, .col = mc.col, .row = mc.row, .tag_hit = mc.tag_hit, .body_hit = mc.body_hit } }); + in.post(.{ .mouse = .{ .button = .wheel_up, .kind = .press, .col = mc.col, .row = mc.row, .tag_hit = mc.tag_hit, .body_hit = mc.body_hit, .ctrl = w.mods.ctrl, .shift = w.mods.shift, .alt = w.mods.alt } }); } } if (w.x != 0) in.post(.{ .mouse = .{ @@ -3950,10 +4014,37 @@ fn wheelAt(g: *Gui, in: *Input, w: WheelTurn, mc: MouseCell) void { .row = mc.row, .tag_hit = mc.tag_hit, .body_hit = mc.body_hit, + .ctrl = w.mods.ctrl, + .shift = w.mods.shift, + .alt = w.mods.alt, } }); } -const WheelTurn = struct { x: f32 = 0, y: f32 = 0, integer_y: i32 = 0 }; +const WheelTurn = struct { x: f32 = 0, y: f32 = 0, integer_y: i32 = 0, mods: PointerMods = .{} }; + +/// The modifiers held with a pointer event, as the core's Mouse carries them. +const PointerMods = struct { ctrl: bool = false, shift: bool = false, alt: bool = false }; + +/// The core's Mouse for an SDL press, release or move over cell `mc`. +fn pointerMouse(button: pardes.Mouse.Button, kind: pardes.Mouse.Kind, mc: MouseCell, mods: PointerMods) pardes.Mouse { + return .{ + .button = button, + .kind = kind, + .col = mc.col, + .row = mc.row, + .tag_hit = mc.tag_hit, + .body_hit = mc.body_hit, + .rule_above = (kind == .press or kind == .release) and mc.rule_above, + .ctrl = mods.ctrl, + .shift = mods.shift, + .alt = mods.alt, + }; +} + +fn pointerMods() PointerMods { + const m = c.SDL_GetModState(); + return .{ .ctrl = (m & c.SDL_KMOD_CTRL) != 0, .shift = (m & c.SDL_KMOD_SHIFT) != 0, .alt = (m & c.SDL_KMOD_ALT) != 0 }; +} fn mouseCellWithGeometry(g: *const Gui, core: ?*const pardes.Pardes, x: f32, y: f32, geometry: WindowGeometry) ?MouseCell { const physical = windowPointToPixels(geometry, x, y); @@ -4794,6 +4885,39 @@ test "the headless grid host round-trips a yank back as a paste" { try std.testing.expectEqual(before + yanked.len, pane.file.?.content.len); } +test "the clipboard pasted into a terminal reaches its pty as xterm pastes: bracketed when asked, no ESC[201~ smuggled through" { + if (comptime !pardes.panes.terminal.enabled) return error.SkipZigTest; + const gpa = std.testing.allocator; + const core = try pardes.Pardes.init(gpa, .{ .cols = 60, .rows = 16, .tty_only = true }); + defer core.deinit(); + var shell: Shell = undefined; + shell.core = core; + var sent: std.ArrayList(u8) = .empty; + defer sent.deinit(gpa); + const take = struct { + fn f(core_: *pardes.Pardes, out: *std.ArrayList(u8)) ![]const u8 { + out.clearRetainingCapacity(); + while (core_.nextEffect()) |effect| { + if (effect == .write and effect.write.pane == 0) try out.appendSlice(std.testing.allocator, effect.write.bytes.slice()); + } + return out.items; + } + }.f; + _ = try take(core, &sent); + // The clipboard is SDL's own with no display to hold one. + _ = c.SDL_SetHint(c.SDL_HINT_VIDEO_DRIVER, "dummy"); + defer _ = c.SDL_ResetHint(c.SDL_HINT_VIDEO_DRIVER); + if (!c.SDL_InitSubSystem(c.SDL_INIT_VIDEO)) return error.SkipZigTest; + defer c.SDL_QuitSubSystem(c.SDL_INIT_VIDEO); + putClipboard(gpa, "ls\x1b[201~; rm -rf ~\nx"); + readClipboard(&shell); + try std.testing.expectEqualStrings("ls [201~; rm -rf ~\rx", try take(core, &sent)); + core.update(.{ .output = .{ .pane = 0, .bytes = "\x1b[?2004h" } }); + _ = try take(core, &sent); + readClipboard(&shell); + try std.testing.expectEqualStrings("\x1b[200~ls [201~; rm -rf ~\nx\x1b[201~", try take(core, &sent)); +} + fn openLink(ctx: ?*anyopaque, url: []const u8) void { _ = ctx; look.openLink(url); // desktop browser @@ -4918,6 +5042,46 @@ test "a wheel's notch is whole lines, a touchpad's fraction is not" { try std.testing.expectEqual(@as(?i32, null), notchOf(std.math.nan(f32), 1)); } +test "a click over codex in a terminal reaches its pty with its modifiers; with Shift it is pardes's" { + if (comptime !pardes.panes.terminal.enabled) return error.SkipZigTest; + const gpa = std.testing.allocator; + const core = try pardes.Pardes.init(gpa, .{ .cols = 60, .rows = 16, .tty_only = true }); + defer core.deinit(); + var frame: std.heap.ArenaAllocator = .init(gpa); + defer frame.deinit(); + _ = try core.render(frame.allocator()); + var in: Input = .{ .core = core }; + var sent: std.ArrayList(u8) = .empty; + defer sent.deinit(gpa); + const take = struct { + fn f(core_: *pardes.Pardes, out: *std.ArrayList(u8)) ![]const u8 { + out.clearRetainingCapacity(); + while (core_.nextEffect()) |effect| { + if (effect == .write and effect.write.pane == 0) try out.appendSlice(std.testing.allocator, effect.write.bytes.slice()); + } + return out.items; + } + }.f; + _ = try take(core, &sent); + const pane = core.panes[0].?; + const r = core.rects[0]; + const mc: MouseCell = .{ .col = r.x + config.GUTTER + 5, .row = core.bodyTop(pane, r) + 3 }; + const next: MouseCell = .{ .col = mc.col + 1, .row = mc.row }; + core.update(.{ .output = .{ .pane = 0, .bytes = "\x1b[?1049h\x1b[?1000h\x1b[?1002h\x1b[?1003h\x1b[?1006h" } }); + _ = try take(core, &sent); + in.post(.{ .mouse = pointerMouse(.none, .motion, mc, .{}) }); + try std.testing.expectEqualStrings("\x1b[<35;6;4M", try take(core, &sent)); + in.post(.{ .mouse = pointerMouse(.left, .press, mc, .{ .ctrl = true }) }); + try std.testing.expectEqualStrings("\x1b[<16;6;4M", try take(core, &sent)); + in.post(.{ .mouse = pointerMouse(.left, .drag, next, .{}) }); + try std.testing.expectEqualStrings("\x1b[<32;7;4M", try take(core, &sent)); + in.post(.{ .mouse = pointerMouse(.left, .release, next, .{}) }); + try std.testing.expectEqualStrings("\x1b[<0;7;4m", try take(core, &sent)); + in.post(.{ .mouse = pointerMouse(.middle, .press, mc, .{ .shift = true }) }); + in.post(.{ .mouse = pointerMouse(.middle, .release, mc, .{ .shift = true }) }); + try std.testing.expectEqualStrings("", try take(core, &sent)); +} + test "a wheel over codex in a terminal reaches its pty as the mouse reports it asked for" { if (comptime !pardes.panes.terminal.enabled) return error.SkipZigTest; const gpa = std.testing.allocator; @@ -4934,6 +5098,7 @@ test "a wheel over codex in a terminal reaches its pty as the mouse reports it a g.scroll_notch = 0; g.program_wheel = 0; g.glide = .{}; + defer g.glide.deinit(gpa); g.cell_h = 20; var in: Input = .{ .core = core }; var sent: std.ArrayList(u8) = .empty; @@ -4967,6 +5132,15 @@ test "a wheel over codex in a terminal reaches its pty as the mouse reports it a try std.testing.expectEqualStrings("", try Sent.take(core, &sent)); wheelAt(&g, &in, .{ .y = 0.5 }, mc); try std.testing.expectEqualStrings("\x1b[<64;6;4M", try Sent.take(core, &sent)); + // Alt is meta, 8; Shift keeps the turn pardes's own, a glide. + wheelAt(&g, &in, .{ .y = 1, .integer_y = 1, .mods = .{ .alt = true } }, mc); + try std.testing.expectEqualStrings("\x1b[<72;6;4M", try Sent.take(core, &sent)); + wheelAt(&g, &in, .{ .y = 1, .integer_y = 1, .mods = .{ .shift = true } }, mc); + try std.testing.expectEqualStrings("", try Sent.take(core, &sent)); + try std.testing.expectEqual(@as(i32, -1), g.scroll_notch); + stepGlide(&g, core, gpa); + try std.testing.expectEqualStrings("", try Sent.take(core, &sent)); + resetScroll(&g); // Its sign-in screen: mouse off, alternate scroll on, so arrow keys. core.update(.{ .output = .{ .pane = 0, .bytes = "\x1b[?1006l\x1b[?1003l\x1b[?1002l\x1b[?1000l\x1b[?1007h" } }); _ = try Sent.take(core, &sent); @@ -5171,7 +5345,8 @@ fn stepGlide(g: *Gui, core: *pardes.Pardes, gpa: std.mem.Allocator) void { const down = left > 0; left += if (down) -1 else 1; const was = pane.scroll(); - core.update(.{ .mouse = .{ .button = if (down) .wheel_down else .wheel_up, .kind = .press, .col = g.scroll_col, .row = g.scroll_row } }); + // Shift: pardes's own scroll, never a program's wheel. + core.update(.{ .mouse = .{ .button = if (down) .wheel_down else .wheel_up, .kind = .press, .col = g.scroll_col, .row = g.scroll_row, .shift = true } }); if (pane.scroll() == was) break; moved = true; } @@ -5344,6 +5519,7 @@ fn stepScroll(g: *Gui, core: *pardes.Pardes, gpa: std.mem.Allocator) void { .kind = .press, .col = g.scroll_col, .row = g.scroll_row, + .shift = true, // pardes's own scroll, never a program's wheel } }); if (pane.scroll() == was) { resetScroll(g); |
