diff options
Diffstat (limited to 'main.zig')
| -rw-r--r-- | main.zig | 200 |
1 files changed, 191 insertions, 9 deletions
@@ -86,6 +86,9 @@ const gray = struct { // the drag button: a distinctive red (brighter when the window is focused). const red = [3]u8{ 0xcc, 0x33, 0x33 }; const red_dim = [3]u8{ 0x66, 0x2a, 0x2a }; +// char-mode inserted text (bright on a faint box so it reads as an overlay). +const edit_fg = [3]u8{ 0xea, 0xea, 0xea }; +const edit_bg = [3]u8{ 0x3a, 0x3a, 0x3a }; // per-button selection highlight: three gray levels, muted text. const SEL_BG = [3][3]u8{ .{ 0x38, 0x38, 0x38 }, .{ 0x4c, 0x4c, 0x4c }, .{ 0x60, 0x60, 0x60 } }; const sel_fg = [3]u8{ 0xc0, 0xc0, 0xc0 }; @@ -141,7 +144,9 @@ fn tagLine(alloc: std.mem.Allocator, t: *Term) ![]u8 { else => "?", }; }; - return std.fmt.allocPrint(alloc, "{s} {s}", .{ cwd, builtins_str }); + // leading 2-char mode indicator: "ch" (char/edit) or "sy" (sym/terminal). + const mode = if (t.editing) "ch" else "sy"; + return std.fmt.allocPrint(alloc, "{s} {s} {s}", .{ mode, cwd, builtins_str }); } // TIOCSWINSZ: set the pty size; the kernel raises SIGWINCH in the child so it @@ -321,11 +326,34 @@ fn renderPane(root: vaxis.Window, alloc: std.mem.Allocator, t: *Term, r: Rect, a // be hidden until we source ghostty's integration script or wrap PS1/ // PROMPT_COMMAND with the 133;A/B/C/D markers in each spawned pty. Best as a // toggle (a Hide/Show tag builtin) so it degrades gracefully without integration. - const tty_str = try t.term.plainString(alloc); - var it = std.mem.splitAny(u8, tty_str, "\n"); + // body = tty rows with insertion runs spliced in (text shifts, not overdrawn). + const off: i32 = @intCast(t.term.screens.active.pages.scrollbar().offset); + const body = try bodyText(alloc, t); + var it = std.mem.splitAny(u8, body, "\n"); var i: u16 = 0; while (it.next()) |line| : (i += 1) { - _ = text.printSegment(.{ .text = line }, .{ .row_offset = BOX_H + i }); + _ = text.printSegment(.{ .text = line }, .{ .row_offset = BOX_H + i, .wrap = .none }); + } + + // recolor the inserted cells so typed text reads distinctly from tty output. + // edit.row is absolute; its visual pane row is (row - off) + BOX_H. + for (t.edits[0..t.nedits]) |*e| { + if (e.len == 0) continue; + const prow = e.row - off + @as(i32, BOX_H); + if (prow < 0 or prow >= @as(i32, r.h)) continue; + const start = renderedCol(t, e.row, e.col, 0); + var k: u16 = 0; + while (k < e.len) : (k += 1) { + const cc = start + @as(i32, k); + if (cc < 0 or cc >= @as(i32, r.w - GUTTER)) continue; + if (text.readCell(@intCast(cc), @intCast(prow))) |cell| { + var nc = cell; + nc.default = false; + nc.style.fg = .{ .rgb = edit_fg }; + nc.style.bg = .{ .rgb = edit_bg }; + text.writeCell(@intCast(cc), @intCast(prow), nc); + } + } } // recolor selected cells (pane-local visual coords), one pass per button — @@ -352,7 +380,17 @@ fn renderPane(root: vaxis.Window, alloc: std.mem.Allocator, t: *Term, r: Rect, a } } } - if (active) text.showCursor(t.term.screens.active.cursor.x, t.term.screens.active.cursor.y + BOX_H); + if (active) { + if (t.editing) { + // char mode: cursor at the rendered cursor position (scroll-adjusted). + const prow = t.cur_row - off + @as(i32, BOX_H); + const cx = renderedCol(t, t.cur_row, t.cur_col, t.cur_off); + if (prow >= 0 and cx >= 0 and prow < @as(i32, r.h) and cx < @as(i32, r.w - GUTTER)) + text.showCursor(@intCast(cx), @intCast(prow)); + } else { + text.showCursor(t.term.screens.active.cursor.x, t.term.screens.active.cursor.y + BOX_H); + } + } // gutter: box (the drag/rearrange handle) on top, scrollbar below const gut = root.child(.{ .x_off = r.x, .y_off = r.y, .width = GUTTER, .height = r.h }); @@ -663,6 +701,22 @@ pub fn main(init: std.process.Init) !void { }, } } else |_| {} + if (s.button == .left) { + // left-click in the body enters char mode and + // anchors the cursor at the selection end. the + // row is stored absolute (visual + scroll off) so + // it rides the scrollback. clicks on the tag (row + // < BOX_H) don't edit. + const sl = t.sel[@intFromEnum(Button.left)]; + const body_vis = sl.r1 - @as(i32, BOX_H); + if (body_vis >= 0) { + const off: i32 = @intCast(t.term.screens.active.pages.scrollbar().offset); + t.cur_row = body_vis + off; + t.cur_col = sl.c1; + t.cur_off = if (runAt(t, t.cur_row, t.cur_col)) |i| t.edits[i].len else 0; + t.editing = true; + } + } } }, .none => {}, @@ -701,7 +755,60 @@ pub fn main(init: std.process.Init) !void { at.term.screens.active.scroll(.{ .delta_row = 15 }); } else if (key.matches('u', .{ .ctrl = true })) { at.term.screens.active.scroll(.{ .delta_row = -15 }); + } else if (at.editing and key.matches(vaxis.Key.escape, .{})) { + at.editing = false; // leave char mode (don't forward Esc) + } else if (at.editing) { + // char mode: printable text inserts at the cursor (shifting + // the current run); arrows move the cursor; backspace/etc. + // are no-ops for now (deletion comes later). + if (key.text) |text| { + // find or create the run at the cursor + var idx: ?usize = runAt(at, at.cur_row, at.cur_col); + if (idx == null and at.nedits < Term.MAX_EDITS) { + at.edits[at.nedits] = .{ .row = at.cur_row, .col = at.cur_col }; + at.nedits += 1; + idx = at.nedits - 1; + } + if (idx) |ei| { + const e = &at.edits[ei]; + for (text) |ch| { + if (e.len >= Term.EDIT_CAP) break; + if (at.cur_off > e.len) at.cur_off = e.len; + var k: usize = e.len; + while (k > at.cur_off) : (k -= 1) e.text[k] = e.text[k - 1]; + e.text[at.cur_off] = ch; + e.len += 1; + at.cur_off += 1; + } + } + } else switch (key.codepoint) { + vaxis.Key.left => { + if (at.cur_off > 0) at.cur_off -= 1 else { + at.cur_col = @max(0, at.cur_col - 1); + at.cur_off = if (runAt(at, at.cur_row, at.cur_col)) |i| at.edits[i].len else 0; + } + }, + vaxis.Key.right => { + const rl: u16 = if (runAt(at, at.cur_row, at.cur_col)) |i| at.edits[i].len else 0; + if (at.cur_off < rl) at.cur_off += 1 else { + at.cur_col += 1; + at.cur_off = 0; + } + }, + vaxis.Key.up => { + const off: i32 = @intCast(at.term.screens.active.pages.scrollbar().offset); + at.cur_row = @max(off, at.cur_row - 1); + at.cur_off = 0; + }, + vaxis.Key.down => { + const off: i32 = @intCast(at.term.screens.active.pages.scrollbar().offset); + at.cur_row = @min(off + @as(i32, at.rows) - 1, at.cur_row + 1); + at.cur_off = 0; + }, + else => {}, + } } else { + // sym mode: forward to the pty as terminal input. const bytes: ?[]const u8 = if (key.text) |text| text else switch (key.codepoint) { vaxis.Key.enter => "\r", vaxis.Key.backspace => "\x7f", @@ -892,9 +999,84 @@ fn moveTerm( layoutInsert(l, tc, ti, id); } -// Build the block-selected text, newline-joined per row; caller frees. Visual -// row 0 is the tag line, rows >= BOX_H are the terminal body — same layout the -// pane renders, so a selection over the tag yields the tag's text. +// index of the run anchored exactly at (row, col), if any. +fn runAt(t: *Term, row: i32, col: i32) ?usize { + for (t.edits[0..t.nedits], 0..) |e, i| if (e.row == row and e.col == col) return i; + return null; +} + +// rendered column of a position: original col + earlier-on-row insertions + the +// offset into the run at this col. (horizontal only; scrolling doesn't matter.) +fn renderedCol(t: *Term, row: i32, col: i32, off: u16) i32 { + var rc: i32 = col + @as(i32, off); + for (t.edits[0..t.nedits]) |e| if (e.row == row and e.col < col) { + rc += @as(i32, e.len); + }; + return rc; +} + +// Splice this row's insertion runs into `line` (the raw tty text for absolute +// row `abs_row`), shifting the underlying text right rather than overwriting it. +fn spliceRow(alloc: std.mem.Allocator, out: *std.ArrayList(u8), t: *Term, abs_row: i32, line: []const u8) !void { + var idxs: [Term.MAX_EDITS]usize = undefined; + var n: usize = 0; + for (t.edits[0..t.nedits], 0..) |e, i| if (e.row == abs_row and e.len > 0) { + idxs[n] = i; + n += 1; + }; + // insertion-sort the runs by column (n is tiny) + var a: usize = 1; + while (a < n) : (a += 1) { + const key = idxs[a]; + var b = a; + while (b > 0 and t.edits[idxs[b - 1]].col > t.edits[key].col) : (b -= 1) idxs[b] = idxs[b - 1]; + idxs[b] = key; + } + var vpos: usize = 0; // original-line columns consumed so far + for (idxs[0..n]) |i| { + const e = &t.edits[i]; + const c: usize = @intCast(@max(0, e.col)); + if (c <= line.len) { + if (c > vpos) try out.appendSlice(alloc, line[vpos..c]); + vpos = c; + } else { + // run anchored past the text: emit the rest, then pad with spaces + if (vpos < line.len) { + try out.appendSlice(alloc, line[vpos..]); + vpos = line.len; + } + while (vpos < c) : (vpos += 1) try out.append(alloc, ' '); + } + try out.appendSlice(alloc, e.text[0..e.len]); + } + if (vpos < line.len) try out.appendSlice(alloc, line[vpos..]); +} + +// The terminal body with all insertion runs spliced in, newline-joined. This is +// the actual on-screen text: render and selection both read it, so there's no +// tty-vs-typed distinction. Caller frees. +fn bodyText(alloc: std.mem.Allocator, t: *Term) ![]u8 { + const off: i32 = @intCast(t.term.screens.active.pages.scrollbar().offset); + const body = try t.term.plainString(alloc); + defer alloc.free(body); + var out: std.ArrayList(u8) = .empty; + errdefer out.deinit(alloc); + // pad to the full grid height: plainString trims trailing blank rows, but we + // must still be able to type (and splice) onto those blank rows. + var it = std.mem.splitAny(u8, body, "\n"); + const nrows: i32 = @intCast(t.rows); + var i: i32 = 0; + while (i < nrows) : (i += 1) { + if (i > 0) try out.append(alloc, '\n'); + const line = it.next() orelse ""; + try spliceRow(alloc, &out, t, i + off, line); // line i shows absolute row i+off + } + return out.toOwnedSlice(alloc); +} + +// Block-selected text, newline-joined per row; caller frees. Reads the rendered +// screen (tag at visual row 0, spliced body below), so selecting typed text and +// tty text works identically. fn selectionText(gpa: std.mem.Allocator, t: *Term, sl: Term.Sel) ![]u8 { const r0 = @min(sl.r0, sl.r1); const r1 = @max(sl.r0, sl.r1); @@ -902,7 +1084,7 @@ fn selectionText(gpa: std.mem.Allocator, t: *Term, sl: Term.Sel) ![]u8 { const c1: usize = @intCast(@max(0, @max(sl.c0, sl.c1))); const tag = try tagLine(gpa, t); defer gpa.free(tag); - const body = try t.term.plainString(gpa); + const body = try bodyText(gpa, t); defer gpa.free(body); const pane = try std.fmt.allocPrint(gpa, "{s}\n{s}", .{ tag, body }); defer gpa.free(pane); |
