diff options
| -rw-r--r-- | docs/helix-keys.md | 6 | ||||
| -rw-r--r-- | src/panes.zig | 44 | ||||
| -rw-r--r-- | src/pardes.zig | 125 | ||||
| -rw-r--r-- | test/panes.zig | 149 |
4 files changed, 310 insertions, 14 deletions
diff --git a/docs/helix-keys.md b/docs/helix-keys.md index 6130e493..a2b597d2 100644 --- a/docs/helix-keys.md +++ b/docs/helix-keys.md @@ -438,3 +438,9 @@ File clicks place the editing cursor at the nearest valid insertion position: blank columns stop at the line end, rows below the file stop on its last row, and wide glyphs or tabs stay on their character boundary. Clicking while inserting keeps insert mode. Terminal overlays still support virtual spaces. + +In a raw TTY pane, finishing a mouse drag keeps the exact rectangular text +as the Pardes selection and internal yank register. Switch to editor mode +with Ctrl-B to inspect or yank it with `y`; returning to TTY preserves it +without moving the shell's input cursor. In another TTY pane, hold select +(left) and tap look (right) to paste it using the existing chord. diff --git a/src/panes.zig b/src/panes.zig index a39afe01..b2f05cdf 100644 --- a/src/panes.zig +++ b/src/panes.zig @@ -44,10 +44,13 @@ pub const Pane = struct { hi: usize, prefix: [32]u8 = @splat(0), prefix_len: u8 = 0, + raw_terminal: bool = false, + prompt_bytes: usize = 0, }; pub const PointerSelection = struct { gesture: Sel, rows: []PointerRow, + raw_text: ?[]u8 = null, }; /// A modal line selection (helix `x`): whole rows [r0, r1], absolute. @@ -174,10 +177,21 @@ pub const Pane = struct { msg_len: u16 = 0, pub fn clearPointerSelection(pane: *Pane, slot: usize) void { - if (pane.pointer_selections[slot]) |selection| pane.gpa.free(selection.rows); + if (pane.pointer_selections[slot]) |selection| { + pane.gpa.free(selection.rows); + if (selection.raw_text) |text| pane.gpa.free(text); + } pane.pointer_selections[slot] = null; } + pub fn rawPointerText(pane: *const Pane, gesture: Sel) ?[]const u8 { + if (gesture.state != .done or gesture.source_id == 0) return null; + for (pane.pointer_selections) |selection| { + if (selection) |saved| if (std.meta.eql(saved.gesture, gesture)) return saved.raw_text; + } + return null; + } + pub fn pointerSelection(pane: *const Pane, gesture: Sel) ?[]PointerRow { if (gesture.state != .done or gesture.source_id == 0) return null; for (pane.pointer_selections) |selection| { @@ -6330,7 +6344,7 @@ pub const Terminal = struct { var index: usize = 0; for (pane.sel) |gesture| if (pane.pointerSelection(gesture)) |rows| { for (rows) |*row| { - pins[index] = trackPosition(pages, &row.row, overlay, &failed); + pins[index] = trackPosition(pages, &row.row, if (row.raw_terminal) null else overlay, &failed); index += 1; } }; @@ -6482,7 +6496,8 @@ pub const Terminal = struct { pub fn enterTty(p: *Pardes, id: usize) void { const pane = p.panes[id] orelse return; - if (comptime enabled) if (pane.terminal != null and pane.cur_pinned and pane.terminal.?.vt.cursorIsAtPrompt()) handoff: { + const selected = pane.rawPointerText(pane.sel[@intFromEnum(config.select_button)]) != null; + if (comptime enabled) if (!selected and pane.terminal != null and pane.cur_pinned and pane.terminal.?.vt.cursorIsAtPrompt()) handoff: { const screen = pane.terminal.?.vt.screens.active; const goff: i32 = @intCast(screen.pages.scrollbar().offset); const vp_row = pane.gridRow(pane.cur_row) - goff; @@ -6511,9 +6526,9 @@ pub const Terminal = struct { pane.msel.active = false; pane.vsel.active = false; pane.nsel = 0; - // A pinned row scrolls away. Raw mode must follow the program's live - // cursor, and Last must not restore a stale modal spot on the way back. - pane.cur_pinned = false; + // The raw renderer follows the live cursor. A captured mouse selection + // keeps its modal endpoint for the next return to editor mode. + pane.cur_pinned = selected; pane.select = false; pane.append_at = null; pane.sticky_col = -1; @@ -6590,6 +6605,19 @@ pub const Terminal = struct { return .{ .cut = cols }; } + pub fn promptPrefixBytes(pane: *Pane, row: i32, raw: []const u8) usize { + if (comptime !enabled) return 0; + if (row < 0) return 0; + const state = pane.terminal orelse return 0; + const pin = state.vt.screens.active.pages.pin(.{ .screen = .{ .x = 0, .y = @intCast(row) } }) orelse return 0; + if (pin.rowAndCell().row.semantic_prompt == .none) return 0; + return switch (promptCut(pin)) { + .keep => 0, + .blank => raw.len, + .cut => |cols| @min(raw.len, File.rawAtDisplay(raw, cols)), + }; + } + fn promptRow(pin: ghostty_vt.Pin, raw: []const u8) []const u8 { const cols = switch (promptCut(pin)) { .keep => return raw, @@ -6765,6 +6793,10 @@ pub const Terminal = struct { /// Consume a rewritten overlay, freeing the terminal edit text it replaces. pub fn setEditText(p: *Pardes, pane: *Pane, new: []u8) void { const overlay = if (pane.ovl) |*value| value else return p.gpa.free(new); + for (0..pane.sel.len) |button| { + pane.clearPointerSelection(button); + pane.sel[button].state = .none; + } p.gpa.free(overlay.text); overlay.text = new; } diff --git a/src/pardes.zig b/src/pardes.zig index 1b2e7ff5..0938736f 100644 --- a/src/pardes.zig +++ b/src/pardes.zig @@ -7464,20 +7464,25 @@ pub const Pardes = struct { if (pane.pointerSelection(gesture) != null) return; pane.clearPointerSelection(slot); // Refresh the source map before translating the rendered rectangle. + const raw = pane.isTerminal() and pane.mode == .tty; const body = try p.bodyText(p.scratch.allocator(), pane); - const terminal_lines = if (pane.file == null) try panes.Terminal.cursorLines(p, pane) else &.{}; + const terminal_lines = if (pane.file == null and !raw) try panes.Terminal.cursorLines(p, pane) else &.{}; const first = @max(0, @min(gesture.r0, gesture.r1) - BOX_H); const last = @min(@as(i32, pane.rows) - 1, @max(gesture.r0, gesture.r1) - BOX_H); if (first > last) return; const rows = try p.gpa.alloc(Pane.PointerRow, @intCast(last - first + 1)); + errdefer p.gpa.free(rows); + const raw_text = if (raw) try p.gpa.dupe(u8, try p.selectionText(pane, gesture)) else null; const prefix: i32 = if (pane.file != null) panes.File.gutterWidth(pane) else 0; const left = @max(0, @min(gesture.c0, gesture.c1)); const right = @max(0, @max(gesture.c0, gesture.c1)); for (rows, 0..) |*row, index| { const visible = first + @as(i32, @intCast(index)); - const source = pane.wrapAt(visible); - const line = pointerSourceLine(pane, terminal_lines, source.line); - const next = pane.wrapAt(visible + 1); + var source = pane.wrapAt(visible); + if (raw) source = .{ .line = panes.Terminal.gridOffset(pane) + visible, .at = 0 }; + const line = if (raw) modal.lineSlice(body, @intCast(visible)) else pointerSourceLine(pane, terminal_lines, source.line); + var next = pane.wrapAt(visible + 1); + if (raw) next = .{ .line = source.line + 1, .at = 0 }; const end = if (next.line == source.line) @min(line.len, @as(usize, @intCast(@max(0, next.at)))) else line.len; const start: usize = @intCast(@max(0, source.at)); const lo = @min(end, panes.File.byteAtDisplayFrom(line, start, @intCast(@max(0, left - prefix)))); @@ -7486,6 +7491,8 @@ pub const Pardes = struct { .row = source.line, .lo = lo, .hi = if (right < prefix) lo else @min(end, modal.nextGrapheme(line, at)), + .raw_terminal = raw, + .prompt_bytes = if (raw) panes.Terminal.promptPrefixBytes(pane, source.line, line) else 0, }; const rendered = modal.lineSlice(body, @intCast(visible)); const prefix_end = @min(rendered.len, @as(usize, @intCast(@min(prefix, right + 1)))); @@ -7496,13 +7503,14 @@ pub const Pardes = struct { pane.next_pointer_selection += 1; gesture.source_id = pane.next_pointer_selection; pane.sel[slot] = gesture; - pane.pointer_selections[slot] = .{ .gesture = gesture, .rows = rows }; + pane.pointer_selections[slot] = .{ .gesture = gesture, .rows = rows, .raw_text = raw_text }; } /// block-selected text, newline-joined per row; reads the rendered screen /// so typed text and shell output select identically. Scratch-owned. fn selectionText(p: *Pardes, pane: *Pane, sl: Pane.Sel) ![]const u8 { const arena = p.scratch.allocator(); + if (pane.rawPointerText(sl)) |text| return arena.dupe(u8, text); if (pane.pointerSelection(sl)) |rows| { const terminal_lines = if (pane.file == null) try panes.Terminal.cursorLines(p, pane) else &.{}; var total: usize = rows.len -| 1; @@ -7684,8 +7692,12 @@ pub const Pardes = struct { fn pointerOperand(p: *Pardes, pane: *Pane, clicked: Pane.Sel) PointerOperand { const visible = clicked.r0 - @as(i32, BOX_H); const wrapped = pane.wrapAt(visible); - const row = wrapped.line; - const col = if (clicked.r0 >= BOX_H) + const raw = pane.isTerminal() and pane.mode == .tty and clicked.r0 >= BOX_H; + const row = if (raw) panes.Terminal.gridOffset(pane) + visible else wrapped.line; + const raw_line = if (raw) modal.lineSlice(p.bodyText(p.scratch.allocator(), pane) catch "", @intCast(@max(0, visible))) else ""; + const col = if (raw) + @as(i32, @intCast(panes.File.rawAtDisplay(raw_line, @intCast(@max(0, clicked.c0))))) + else if (clicked.r0 >= BOX_H) p.paneByteAtDisplay( pane, wrapped.line, @@ -7702,7 +7714,21 @@ pub const Pardes = struct { if (pane.file != null and clicked.c0 < panes.File.gutterWidth(pane)) break :inside false; const source_col: usize = @intCast(@max(0, col)); for (rows) |span| { - if (span.row == row and source_col >= span.lo and source_col < span.hi) + const target = if (span.raw_terminal) + (if (raw) span.row else pane.surfRow(span.row)) + else + (if (raw) pane.gridRow(span.row) else span.row); + var lo = span.lo; + var hi = span.hi; + if (span.raw_terminal and !raw) { + lo -|= span.prompt_bytes; + hi -|= span.prompt_bytes; + } else if (!span.raw_terminal and raw) { + const prefix = panes.Terminal.promptPrefixBytes(pane, row, raw_line); + lo += prefix; + hi += prefix; + } + if (target == row and source_col >= lo and source_col < hi) break :inside true; } break :inside false; @@ -8241,6 +8267,13 @@ pub const Pardes = struct { } fn executeNormalAction(p: *Pardes, pane: *Pane, semantic: modal.Normal.Action) void { + const raw_selection = pane.rawPointerText(pane.sel[sel_slot]) != null; + const before = .{ pane.cur_row, pane.cur_col, pane.vsel, pane.msel, pane.select, pane.nsel, pane.mode }; + defer if (raw_selection and !std.meta.eql(before, .{ pane.cur_row, pane.cur_col, pane.vsel, pane.msel, pane.select, pane.nsel, pane.mode })) { + pane.clearPointerSelection(sel_slot); + pane.sel[sel_slot].state = .none; + }; + if (raw_selection and semantic == .edit and semantic.edit.kind == .yank) return p.normalYank(pane); p.pinPaneCursor(pane); const text = p.flatSurface(pane) catch return; const range = pane.primaryRange(text, 0); @@ -9861,6 +9894,10 @@ pub const Pardes = struct { fn normalYank(p: *Pardes, pane: *Pane) void { pane.select = false; + if (pane.rawPointerText(pane.sel[sel_slot])) |text| { + p.setYank(text); + return; + } if (pane.vsel.active) { p.setYank(p.vselText(pane)); return; @@ -11200,6 +11237,34 @@ pub const Pardes = struct { p.runBuiltin(cmd, id, "", p.withArg(operand, arg)); } + fn mirrorTtySelection(p: *Pardes, pane: *Pane) void { + const gesture = pane.sel[sel_slot]; + const rows = pane.pointerSelection(gesture) orelse return; + const text = pane.rawPointerText(gesture) orelse return; + if (rows.len == 0) return; + const endpoint = rows[if (gesture.r1 >= gesture.r0) rows.len - 1 else 0]; + const body = p.bodyText(p.scratch.allocator(), pane) catch return; + const line = modal.lineSlice(body, @intCast(@max(0, endpoint.row - panes.Terminal.gridOffset(pane)))); + const byte = if (gesture.c1 >= gesture.c0) modal.prevGrapheme(line, @min(endpoint.hi, line.len)) else endpoint.lo; + pane.cur_row = pane.surfRow(endpoint.row); + var shown = std.mem.trimEnd(u8, line[@min(endpoint.prompt_bytes, line.len)..], " \t"); + if (pane.ovl) |overlay| { + const relative = pane.cur_row - overlay.row; + if (relative >= 0 and relative < modal.lineCount(overlay.text)) + shown = modal.lineSlice(overlay.text, @intCast(relative)); + } + pane.cur_col = @intCast(modal.graphemeStart(shown, @min(shown.len, byte -| endpoint.prompt_bytes))); + pane.cur_pinned = true; + pane.msel.active = false; + pane.vsel.active = false; + pane.nsel = 0; + pane.select = false; + pane.append_at = null; + pane.sticky_col = -1; + pane.normal.clear(); + p.setYank(text); + } + fn dragRelease(p: *Pardes, button: Mouse.Button) void { // A different button's release belongs to a chord or another host // stream; only the button which began this gesture may end it. @@ -11273,6 +11338,7 @@ pub const Pardes = struct { const dragged = sl.c0 != sl.c1 or sl.r0 != sl.r1; if (!dragged) pane.sel[sel_slot].state = .none; if (dragged) p.capturePointerSelection(pane, sel_slot) catch {}; + if (dragged and pane.mode == .tty) p.mirrorTtySelection(pane); const body_vis = sl.r1 - @as(i32, BOX_H); if (body_vis >= 0 and pane.mode != .tty) { const w = pane.wrapAt(body_vis); @@ -12779,7 +12845,50 @@ pub const Pardes = struct { return !cell.default and !(cell.len == 1 and cell.text[0] == ' '); } + fn paintTerminalSelection(p: *Pardes, pane: *Pane, r: Rect, rows: []const Pane.PointerRow, bg: [3]u8, fg: ?[3]u8) void { + const body = p.bodyText(p.scratch.allocator(), pane) catch return; + const raw = pane.mode == .tty; + const tx = r.x + config.GUTTER; + const width = r.w -| config.GUTTER; + const body_y = if (p.settings.tag_bottom) r.y else r.y + BOX_H; + var lines = std.mem.splitScalar(u8, body, '\n'); + var visible: i32 = 0; + while (lines.next()) |line| : (visible += 1) { + if (visible >= r.h -| BOX_H) break; + const source_row = if (raw) panes.Terminal.gridOffset(pane) + visible else pane.wrapAt(visible).line; + for (rows) |row| { + const target = if (row.raw_terminal) + (if (raw) row.row else pane.surfRow(row.row)) + else + (if (raw) pane.gridRow(row.row) else row.row); + if (target != source_row) continue; + var lo = row.lo; + var hi = row.hi; + if (row.raw_terminal and !raw) { + lo -|= row.prompt_bytes; + hi -|= row.prompt_bytes; + } else if (!row.raw_terminal and raw) { + const prefix = panes.Terminal.promptPrefixBytes(pane, source_row, line); + lo += prefix; + hi += prefix; + } + lo = modal.graphemeStart(line, @min(lo, line.len)); + hi = @min(hi, line.len); + if (lo >= hi) continue; + var col = panes.File.rawDisplayCol(line, lo); + const end = panes.File.rawDisplayCol(line, hi); + while (col < end and col < width) : (col += 1) { + const cell = p.surface.at(tx + @as(u16, @intCast(col)), body_y + @as(u16, @intCast(visible))); + cell.default = false; + cell.style.bg = .{ .rgb = bg }; + if (fg) |ink| cell.style.fg = .{ .rgb = ink }; + } + } + } + } + fn paintSourceSelection(p: *Pardes, pane: *Pane, r: Rect, rows: []const Pane.PointerRow, bg: [3]u8, fg: ?[3]u8) void { + if (pane.isTerminal()) return p.paintTerminalSelection(pane, r, rows, bg, fg); const terminal_lines = if (pane.file == null) panes.Terminal.cursorLines(p, pane) catch return else &.{}; const tx = r.x + config.GUTTER; const width = r.w -| config.GUTTER; diff --git a/test/panes.zig b/test/panes.zig index ded5b075..0577154f 100644 --- a/test/panes.zig +++ b/test/panes.zig @@ -12,6 +12,154 @@ const image = pardes.image; const syntax = pardes.syntax; const filesystem = pardes.filesystem; +const TtySelectionTests = struct { + fn noChildInput(p: *Pardes) !void { + while (p.nextEffect()) |effect| { + if (effect == .write) return error.UnexpectedChildInput; + if (effect == .set_clipboard) p.perform(effect); + } + } + + test "tty selection yields to a new modal cursor selection" { + if (comptime !panes.Terminal.enabled) return error.SkipZigTest; + const p = try Pardes.init(std.testing.allocator, .{ .tty_only = true, .cols = 40, .rows = 12 }); + defer p.deinit(); + p.presentation.enabled = false; + const pane = p.panes[0].?; + pane.tty_filter = false; + while (p.nextEffect()) |_| {} + p.update(.{ .output = .{ .pane = 0, .bytes = "alpha" } }); + var frame = std.heap.ArenaAllocator.init(p.gpa); + defer frame.deinit(); + _ = try p.render(frame.allocator()); + const rect = p.rects[0]; + const x = rect.x + config.GUTTER; + const y = if (p.settings.tag_bottom) rect.y else rect.y + pardes.BOX_H; + JumpSelectionTests.drag(p, x + 1, y, x + 3, y); + try std.testing.expectEqualStrings("lph", p.yank orelse return error.MissingAutomaticTtyYank); + p.update(.{ .key = .{ .cp = p.opts.tty_toggle, .ctrl = true } }); + p.update(.{ .key = .{ .cp = 'l' } }); + try std.testing.expectEqual(@as(i32, 4), pane.cur_col); + p.update(.{ .key = .{ .cp = 'y' } }); + try std.testing.expectEqualStrings("a", p.yank.?); + try noChildInput(p); + } + + test "tty selection preserves exact rectangle through yank clipboard and mode changes" { + if (comptime !panes.Terminal.enabled) return error.SkipZigTest; + for ([_]bool{ false, true }) |prompt| { + const p = try Pardes.init(std.testing.allocator, .{ .tty_only = true, .cols = 40, .rows = 12 }); + defer p.deinit(); + const pane = p.panes[0].?; + pane.mode = .tty; + pane.tty_filter = false; + p.presentation.enabled = false; + while (p.nextEffect()) |_| {} + p.update(.{ .output = .{ .pane = 0, .bytes = if (prompt) + "\x1b]133;A\x07$ \x1b]133;B\x07abc" + else + "alpha\r\nbravo" } }); + var frame = std.heap.ArenaAllocator.init(p.gpa); + defer frame.deinit(); + _ = try p.render(frame.allocator()); + const rect = p.rects[0]; + const x = rect.x + config.GUTTER; + const y = if (p.settings.tag_bottom) rect.y else rect.y + pardes.BOX_H; + JumpSelectionTests.drag(p, x + @as(u16, if (prompt) 0 else 1), y, x + 3, y + @as(u16, if (prompt) 0 else 1)); + const expected = if (prompt) "$ ab" else "lph\nrav"; + try std.testing.expectEqual(Pane.Mode.tty, pane.mode); + try std.testing.expectEqualStrings(expected, pardes.test_api.heldSelection(p, 0).?); + try std.testing.expectEqualStrings(expected, p.yank orelse return error.MissingAutomaticTtyYank); + try noChildInput(p); + + try std.testing.expect(p.executeBuiltinLine(0, "ClipYank")); + try noChildInput(p); + try std.testing.expectEqualStrings(expected, p.fallback.clipboard.items); + p.update(.{ .key = .{ .cp = p.opts.tty_toggle, .ctrl = true } }); + try std.testing.expectEqual(Pane.Mode.normal, pane.mode); + const normal = try p.render(frame.allocator()); + if (!prompt) { + const selection_bg: pardes.Color = .{ .rgb = p.theme().sel_bg }; + try std.testing.expect(!std.meta.eql(selection_bg, normal.at(x + 4, y).style.bg)); + try std.testing.expect(!std.meta.eql(selection_bg, normal.at(x, y + 1).style.bg)); + } + try std.testing.expectEqualStrings(expected, pardes.test_api.heldSelection(p, 0).?); + p.update(.{ .key = .{ .cp = 'y' } }); + try std.testing.expectEqualStrings(expected, p.yank.?); + try noChildInput(p); + p.update(.{ .key = .{ .cp = p.opts.tty_toggle, .ctrl = true } }); + try std.testing.expectEqual(Pane.Mode.tty, pane.mode); + try noChildInput(p); + _ = try p.render(frame.allocator()); + try std.testing.expectEqualStrings(expected, pardes.test_api.heldSelection(p, 0).?); + p.update(.{ .output = .{ .pane = 0, .bytes = "\x1b]133;C\x07" ++ "\r\nnew output" ** 30 } }); + _ = try p.render(frame.allocator()); + try std.testing.expectEqual(Pane.Mode.tty, pane.mode); + try std.testing.expectEqualStrings(expected, pardes.test_api.heldSelection(p, 0).?); + try std.testing.expectEqualStrings(expected, p.yank.?); + try noChildInput(p); + } + } + + test "tty selection pastes across panes once with existing bracket and mouse protocols" { + if (comptime !panes.Terminal.enabled) return error.SkipZigTest; + for ([_]bool{ false, true }) |bracketed| { + for ([_]bool{ false, true }) |reports_mouse| { + const p = try Pardes.init(std.testing.allocator, .{ .tty_only = true, .cols = 40, .rows = 24 }); + defer p.deinit(); + p.presentation.enabled = false; + p.spawnTty(0); + for (p.panes[0..2]) |slot| { + slot.?.mode = .tty; + slot.?.tty_filter = false; + slot.?.greet = false; + slot.?.vweight = 1; + } + p.update(.tick); + while (p.nextEffect()) |_| {} + p.update(.{ .output = .{ .pane = 0, .bytes = "alpha\r\nbravo" } }); + p.update(.{ .output = .{ .pane = 1, .bytes = "target> " } }); + if (bracketed) p.update(.{ .output = .{ .pane = 1, .bytes = "\x1b[?2004h" } }); + if (reports_mouse) p.update(.{ .output = .{ .pane = 1, .bytes = "\x1b[?1000h\x1b[?1006h" } }); + var frame = std.heap.ArenaAllocator.init(p.gpa); + defer frame.deinit(); + _ = try p.render(frame.allocator()); + const source = p.rects[0]; + const sx = source.x + config.GUTTER; + const sy = if (p.settings.tag_bottom) source.y else source.y + pardes.BOX_H; + JumpSelectionTests.drag(p, sx + 1, sy, sx + 3, sy + 1); + try std.testing.expectEqualStrings("lph\nrav", p.yank orelse return error.MissingAutomaticTtyYank); + try noChildInput(p); + + const target = p.rects[1]; + const tx = target.x + config.GUTTER + 2; + const ty = if (p.settings.tag_bottom) target.y else target.y + pardes.BOX_H; + p.update(.{ .mouse = .{ .button = .left, .kind = .press, .col = tx, .row = ty } }); + p.update(.{ .mouse = .{ .button = .right, .kind = .press, .col = tx, .row = ty } }); + p.update(.{ .mouse = .{ .button = .right, .kind = .release, .col = tx, .row = ty } }); + p.update(.{ .mouse = .{ .button = .left, .kind = .release, .col = tx, .row = ty } }); + var written: std.Io.Writer.Allocating = .init(p.gpa); + defer written.deinit(); + while (p.nextEffect()) |effect| { + if (effect == .write) { + try std.testing.expectEqual(@as(u8, 1), effect.write.pane); + try written.writer.writeAll(effect.write.bytes.slice()); + } + } + const expected = try std.fmt.allocPrint(p.gpa, "{s}{s}", .{ + if (reports_mouse) "\x1b[<0;3;1M\x1b[<0;3;1m" else "", + if (bracketed) "\x1b[200~lph\nrav\x1b[201~" else "lph\rrav", + }); + defer p.gpa.free(expected); + try std.testing.expectEqualStrings(expected, written.written()); + try std.testing.expectEqual(Pane.Mode.tty, p.panes[0].?.mode); + try std.testing.expectEqual(Pane.Mode.tty, p.panes[1].?.mode); + try std.testing.expectEqualStrings("lph\nrav", pardes.test_api.heldSelection(p, 0).?); + } + } + } +}; + const ClickCursorTests = struct { fn click(p: *Pardes, button: pardes.Mouse.Button, x: u16, y: u16) void { p.update(.{ .mouse = .{ .button = button, .kind = .press, .col = x, .row = y } }); @@ -2824,6 +2972,7 @@ const TerminalTests = struct { }; test { + _ = TtySelectionTests; _ = ClickCursorTests; _ = JumpSelectionTests; _ = @import("output.zig"); |
