summaryrefslogtreecommitdiff
path: root/main.zig
diff options
context:
space:
mode:
authorGabriel Schneider <[email protected]>2026-06-29 19:14:55 -0300
committerGabriel Schneider <[email protected]>2026-06-29 20:17:08 -0300
commit3c1fef20894772e0cb48c7c2ae2c1a7ded48970d (patch)
tree2871622ab4068c8e247aa8a935573274d198b550 /main.zig
parentfd095f7fa6e26fba3aa797d2b57740e776d3bb04 (diff)
downloadpardes-3c1fef20894772e0cb48c7c2ae2c1a7ded48970d.tar.gz
pardes-3c1fef20894772e0cb48c7c2ae2c1a7ded48970d.zip
vibes tagline and commands?
Diffstat (limited to 'main.zig')
-rw-r--r--main.zig272
1 files changed, 191 insertions, 81 deletions
diff --git a/main.zig b/main.zig
index a19fae38..3debdf65 100644
--- a/main.zig
+++ b/main.zig
@@ -72,14 +72,40 @@ const Drag = union(enum) {
select: struct { id: usize, button: Button },
};
-// mouse button driving a selection; also indexes Term.sel / SEL_BG / SEL_FG.
+// mouse button driving a selection; also indexes Term.sel.
const Button = enum { left, middle, right };
-// highlight colors per button (left, middle, right)
-const SEL_BG = [3]u8{ 4, 2, 5 }; // blue, green, magenta
-const SEL_FG = [3]u8{ 7, 0, 7 }; // readable text on the highlight
+// monochrome, low-contrast palette (rgb grays).
+const gray = struct {
+ const bar = [3]u8{ 0x2a, 0x2a, 0x2a }; // dim bars (tag, scrollbar track, box idle)
+ const dim = [3]u8{ 0x3c, 0x3c, 0x3c }; // separators
+ const mid = [3]u8{ 0x52, 0x52, 0x52 }; // box active, scrollbar thumb, move preview
+ const lit = [3]u8{ 0x6e, 0x6e, 0x6e }; // dashed resize preview
+ const txt = [3]u8{ 0x96, 0x96, 0x96 }; // muted text
+};
+// 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 };
+// 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 };
-fn fork_pty(argv: [:0]const u8, ws: std.posix.winsize) std.Io.File {
+// Tag builtins. Middle-clicking the name in a window's tag runs the action;
+// `builtins_str` is the space-joined name list, produced at comptime.
+const SelectAction = enum { none, send, newcol, delcol, del };
+const Builtin = struct { name: []const u8, action: SelectAction };
+const builtins = [_]Builtin{
+ .{ .name = "Newcol", .action = .newcol },
+ .{ .name = "Delcol", .action = .delcol },
+ .{ .name = "Del", .action = .del },
+};
+const builtins_str = blk: {
+ var s: []const u8 = "";
+ for (builtins, 0..) |b, i| s = s ++ (if (i == 0) "" else " ") ++ b.name;
+ break :blk s;
+};
+
+fn fork_pty(argv: [:0]const u8, ws: std.posix.winsize) struct { file: std.Io.File, pid: std.posix.pid_t } {
var master: c_int = undefined;
const pid = forkpty(&master, null, null, &ws);
if (pid == 0) {
@@ -87,7 +113,35 @@ fn fork_pty(argv: [:0]const u8, ws: std.posix.winsize) std.Io.File {
_ = execv(argv.ptr, &child_argv);
_exit(127);
}
- return .{ .handle = master, .flags = .{ .nonblocking = false } };
+ return .{ .file = .{ .handle = master, .flags = .{ .nonblocking = false } }, .pid = pid };
+}
+
+fn termWrite(t: *Term, bytes: []const u8) void {
+ var off: usize = 0;
+ while (off < bytes.len) {
+ const rc = std.os.linux.write(t.pty.handle, bytes[off..].ptr, bytes.len - off);
+ switch (std.posix.errno(rc)) {
+ .SUCCESS => off += @intCast(rc),
+ .INTR => {},
+ else => break,
+ }
+ }
+}
+
+// The window tag: the shell's cwd followed by the builtin command names. Plain,
+// selectable text (acme-style). Caller frees with `alloc`.
+fn tagLine(alloc: std.mem.Allocator, t: *Term) ![]u8 {
+ var link: [1024]u8 = undefined;
+ var pathbuf: [64]u8 = undefined;
+ const cwd: []const u8 = cwd: {
+ const path = std.fmt.bufPrintZ(&pathbuf, "/proc/{d}/cwd", .{t.pid}) catch break :cwd "?";
+ const rc = std.os.linux.readlinkat(std.os.linux.AT.FDCWD, path, &link, link.len);
+ break :cwd switch (std.posix.errno(rc)) {
+ .SUCCESS => link[0..rc],
+ else => "?",
+ };
+ };
+ return std.fmt.allocPrint(alloc, "{s} {s}", .{ cwd, builtins_str });
}
// TIOCSWINSZ: set the pty size; the kernel raises SIGWINCH in the child so it
@@ -120,11 +174,12 @@ fn readPty(io: std.Io, gpa: std.mem.Allocator, pty: std.Io.File, id: usize, loop
// multithreaded process, so this does NOT start the reader task (no new
// threads) — call startReader once the term is wired in.
fn forkTerm(gpa: std.mem.Allocator, init_cols: u16, init_rows: u16) !*Term {
- const pty = fork_pty("/usr/bin/bash", .{ .row = init_rows, .col = init_cols, .xpixel = 0, .ypixel = 0 });
+ const child = fork_pty("/usr/bin/bash", .{ .row = init_rows, .col = init_cols, .xpixel = 0, .ypixel = 0 });
const t = try gpa.create(Term);
errdefer gpa.destroy(t);
t.* = .{
- .pty = pty,
+ .pty = child.file,
+ .pid = child.pid,
.term = try ghostty_vt.Terminal.init(gpa, .{ .cols = init_cols, .rows = init_rows }),
.stream = undefined,
.reader = .{ .any_future = null, .result = {} }, // no-op until startReader
@@ -239,19 +294,42 @@ fn computeGeom(
fn renderPane(root: vaxis.Window, alloc: std.mem.Allocator, t: *Term, r: Rect, active: bool) !void {
if (r.w <= GUTTER or r.h == 0) return;
- // text
+ // text area: tag line on row 0 (same row as the box), terminal body below.
const text = root.child(.{ .x_off = r.x + GUTTER, .y_off = r.y, .width = r.w - GUTTER, .height = r.h });
text.clear();
+
+ // tag: cwd + builtins, drawn as a dim bar; plain selectable text.
+ const tag = try tagLine(alloc, t);
+ const tagw = text.child(.{ .height = BOX_H });
+ tagw.fill(.{ .char = .{ .grapheme = " " }, .style = .{ .bg = .{ .rgb = gray.bar } } });
+ _ = tagw.printSegment(.{ .text = tag, .style = .{ .fg = .{ .rgb = gray.txt }, .bg = .{ .rgb = gray.bar } } }, .{ .wrap = .none });
+
+ // FUTURE: hide shell prompts, render only command output. ghostty-vt tracks
+ // OSC 133 semantic prompts and stores the state we'd need:
+ // - per cell: page.Cell.semantic_content -> .output | .input | .prompt
+ // - per row: page.Row.semantic_prompt -> .none | .prompt | .prompt_continuation
+ // - cursor drives it via OSC 133 A/B/C/D (stream_terminal -> Terminal.semanticPrompt).
+ // Ready-made helpers (all public through ghostty_vt): Screen.selectOutput(pin),
+ // PageList.highlightSemanticContent(pin, .output), screen.pages.rowIterator(...),
+ // pin.rowAndCell().cell.semantic_content, pin.promptIterator/cellIterator.
+ // To do it: swap plainString for a row walk that skips rows where
+ // row.semantic_prompt != .none (coarse) or non-.output cells (fine):
+ // var it = screen.pages.rowIterator(.right_down, .{ .viewport = .{} }, null);
+ // while (it.next()) |pin| { if (pin.rowAndCell().row.semantic_prompt != .none) continue; ... }
+ // CATCH: only populated if the shell emits OSC 133 (shell integration). bash
+ // doesn't by default — every cell stays the default .output, so nothing would
+ // 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);
- const off: i32 = @intCast(t.term.screens.active.pages.scrollbar().offset);
var it = std.mem.splitAny(u8, tty_str, "\n");
var i: u16 = 0;
while (it.next()) |line| : (i += 1) {
- _ = text.printSegment(.{ .text = line }, .{ .row_offset = i });
+ _ = text.printSegment(.{ .text = line }, .{ .row_offset = BOX_H + i });
}
- // recolor selected cells, one pass per button — later buttons win on overlap.
- // only while actively dragging; a released selection is no longer drawn.
+ // recolor selected cells (pane-local visual coords), one pass per button —
+ // later buttons win on overlap; only while actively dragging.
for (t.sel, 0..) |sl, b| {
if (sl.state != .dragging) continue;
const r0 = @min(sl.r0, sl.r1);
@@ -260,31 +338,30 @@ fn renderPane(root: vaxis.Window, alloc: std.mem.Allocator, t: *Term, r: Rect, a
const c1 = @max(0, @max(sl.c0, sl.c1));
var row: u16 = 0;
while (row < r.h) : (row += 1) {
- const j = @as(i32, row) + off;
- if (j < r0 or j > r1) continue;
+ if (@as(i32, row) < r0 or @as(i32, row) > r1) continue;
var col: i32 = c0;
while (col <= c1 and col < @as(i32, r.w - GUTTER)) : (col += 1) {
const cc: u16 = @intCast(col);
if (text.readCell(cc, row)) |cell| {
var nc = cell;
nc.default = false; // else vaxis ignores the style on cleared cells
- nc.style.bg = .{ .index = SEL_BG[b] };
- nc.style.fg = .{ .index = SEL_FG[b] };
+ nc.style.bg = .{ .rgb = SEL_BG[b] };
+ nc.style.fg = .{ .rgb = sel_fg };
text.writeCell(cc, row, nc);
}
}
}
}
- if (active) text.showCursor(t.term.screens.active.cursor.x, t.term.screens.active.cursor.y);
+ if (active) 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 });
- const box_bg: u8 = if (active) 4 else 8;
+ const box_bg = if (active) red else red_dim;
var bx: u16 = 0;
while (bx < GUTTER) : (bx += 1) {
var by: u16 = 0;
while (by < BOX_H) : (by += 1)
- gut.writeCell(bx, by, .{ .char = .{ .grapheme = " " }, .style = .{ .bg = .{ .index = box_bg } } });
+ gut.writeCell(bx, by, .{ .char = .{ .grapheme = " " }, .style = .{ .bg = .{ .rgb = box_bg } } });
}
const sb = t.term.screens.active.pages.scrollbar();
const track_h: usize = if (r.h > BOX_H) r.h - BOX_H else 0;
@@ -295,10 +372,10 @@ fn renderPane(root: vaxis.Window, alloc: std.mem.Allocator, t: *Term, r: Rect, a
var gy: usize = 0;
while (gy < track_h) : (gy += 1) {
const thumb = gy >= pos and gy < pos + len;
- const st_bg: u8 = if (thumb) 6 else 8;
+ const st_bg = if (thumb) gray.mid else gray.bar;
var cx: u16 = 0;
while (cx < GUTTER) : (cx += 1)
- gut.writeCell(cx, @intCast(BOX_H + gy), .{ .char = .{ .grapheme = " " }, .style = .{ .bg = .{ .index = st_bg } } });
+ gut.writeCell(cx, @intCast(BOX_H + gy), .{ .char = .{ .grapheme = " " }, .style = .{ .bg = .{ .rgb = st_bg } } });
}
}
}
@@ -440,15 +517,9 @@ pub fn main(init: std.process.Init) !void {
} else {
active = id;
if (terms[id]) |t| {
- const textx = r.x + GUTTER;
- const off: i32 = @intCast(t.term.screens.active.pages.scrollbar().offset);
- t.sel[@intFromEnum(Button.left)] = .{
- .state = .dragging,
- .c0 = @as(i32, mcol) - @as(i32, textx),
- .r0 = (@as(i32, mrow) - @as(i32, r.y)) + off,
- .c1 = @as(i32, mcol) - @as(i32, textx),
- .r1 = (@as(i32, mrow) - @as(i32, r.y)) + off,
- };
+ const sc: i32 = @as(i32, mcol) - @as(i32, r.x + GUTTER);
+ const v: i32 = @as(i32, mrow) - @as(i32, r.y);
+ t.sel[@intFromEnum(Button.left)] = .{ .state = .dragging, .c0 = sc, .c1 = sc, .r0 = v, .r1 = v };
drag = .{ .select = .{ .id = id, .button = .left } };
}
}
@@ -462,15 +533,9 @@ pub fn main(init: std.process.Init) !void {
if (mcol >= r.x + GUTTER) {
active = id;
if (terms[id]) |t| {
- const textx = r.x + GUTTER;
- const off: i32 = @intCast(t.term.screens.active.pages.scrollbar().offset);
- t.sel[@intFromEnum(b)] = .{
- .state = .dragging,
- .c0 = @as(i32, mcol) - @as(i32, textx),
- .r0 = (@as(i32, mrow) - @as(i32, r.y)) + off,
- .c1 = @as(i32, mcol) - @as(i32, textx),
- .r1 = (@as(i32, mrow) - @as(i32, r.y)) + off,
- };
+ const c: i32 = @as(i32, mcol) - @as(i32, r.x + GUTTER);
+ const v: i32 = @as(i32, mrow) - @as(i32, r.y);
+ t.sel[@intFromEnum(b)] = .{ .state = .dragging, .c0 = c, .c1 = c, .r0 = v, .r1 = v };
drag = .{ .select = .{ .id = id, .button = b } };
}
}
@@ -503,10 +568,8 @@ pub fn main(init: std.process.Init) !void {
.select => |s| {
if (terms[s.id]) |t| {
const r = rects[s.id];
- const textx = r.x + GUTTER;
- const off: i32 = @intCast(t.term.screens.active.pages.scrollbar().offset);
- t.sel[@intFromEnum(s.button)].c1 = @as(i32, mcol) - @as(i32, textx);
- t.sel[@intFromEnum(s.button)].r1 = (@as(i32, mrow) - @as(i32, r.y)) + off;
+ t.sel[@intFromEnum(s.button)].c1 = @as(i32, mcol) - @as(i32, r.x + GUTTER);
+ t.sel[@intFromEnum(s.button)].r1 = @as(i32, mrow) - @as(i32, r.y);
}
},
.none => {},
@@ -548,7 +611,57 @@ pub fn main(init: std.process.Init) !void {
t.sel[@intFromEnum(s.button)].state = .done;
if (selectionText(gpa, t, t.sel[@intFromEnum(s.button)])) |txt| {
defer gpa.free(txt);
- onSelect(s.button, txt);
+ switch (onSelect(s.button, txt)) {
+ .none => {},
+ .send => {
+ // run the selection in the terminal it came from
+ termWrite(t, std.mem.trim(u8, txt, " \t\r\n"));
+ termWrite(t, "\r");
+ },
+ .newcol => {
+ // new terminal in a fresh column
+ const slot: ?usize = for (terms, 0..) |sl, i| {
+ if (sl == null) break i;
+ } else null;
+ if (slot) |free| if (layout.ncol < MAX_COLS) {
+ if (forkTerm(gpa, screen_w, screen_h)) |nt| {
+ terms[free] = nt;
+ startReader(io, gpa, &loop, nt, free) catch {};
+ layoutAppendColumn(&layout, free);
+ active = free;
+ } else |_| {}
+ };
+ },
+ .del => {
+ // close just this window
+ layoutRemove(&layout, s.id);
+ t.deinit(io, gpa);
+ gpa.destroy(t);
+ terms[s.id] = null;
+ if (active == s.id) active = for (terms, 0..) |sl, i| {
+ if (sl != null) break i;
+ } else break;
+ },
+ .delcol => {
+ // close every terminal in this one's column
+ if (layoutFindTerm(&layout, s.id)) |f| {
+ var ids: [MAX_TERMS]usize = undefined;
+ const nids = layout.col_n[f.col];
+ for (0..nids) |k| ids[k] = layout.col_terms[f.col][k];
+ for (ids[0..nids]) |tid| {
+ if (terms[tid]) |tt| {
+ layoutRemove(&layout, tid);
+ tt.deinit(io, gpa);
+ gpa.destroy(tt);
+ terms[tid] = null;
+ }
+ }
+ if (terms[active] == null) active = for (terms, 0..) |sl, i| {
+ if (sl != null) break i;
+ } else break;
+ }
+ },
+ }
} else |_| {}
}
},
@@ -600,19 +713,7 @@ pub fn main(init: std.process.Init) !void {
vaxis.Key.left => "\x1b[D",
else => null,
};
- if (bytes) |b| {
- // raw write straight to the pty master; a fresh
- // File.Writer per keystroke doesn't reliably flush.
- var off: usize = 0;
- while (off < b.len) {
- const rc = std.os.linux.write(at.pty.handle, b[off..].ptr, b.len - off);
- switch (std.posix.errno(rc)) {
- .SUCCESS => off += @intCast(rc),
- .INTR => {},
- else => break,
- }
- }
- }
+ if (bytes) |b| termWrite(at, b);
}
}
},
@@ -650,7 +751,7 @@ pub fn main(init: std.process.Init) !void {
if (slot.*) |t| {
const r = rects[id];
const cols = @max(1, r.w -| GUTTER);
- const rows = @max(1, r.h);
+ const rows = @max(1, r.h -| BOX_H); // the tag steals the top row
if (cols != t.cols or rows != t.rows) {
t.term.resize(gpa, cols, rows) catch {};
pty_set_size(t.pty, rows, cols);
@@ -676,7 +777,7 @@ pub fn main(init: std.process.Init) !void {
var ry: u16 = 0;
while (ry < screen_h) : (ry += 1) {
const g: []const u8 = if (ry == mid) "⇔" else "│";
- win.writeCell(sx, ry, .{ .char = .{ .grapheme = g }, .style = .{ .fg = .{ .index = 5 } } });
+ win.writeCell(sx, ry, .{ .char = .{ .grapheme = g }, .style = .{ .fg = .{ .rgb = gray.dim } } });
}
}
const n = layout.col_n[c];
@@ -688,7 +789,7 @@ pub fn main(init: std.process.Init) !void {
var rx: u16 = col_x[c];
while (rx < col_x[c] + col_w[c]) : (rx += 1) {
const g: []const u8 = if (rx == midx) "⇕" else "─";
- win.writeCell(rx, sy, .{ .char = .{ .grapheme = g }, .style = .{ .fg = .{ .index = 5 } } });
+ win.writeCell(rx, sy, .{ .char = .{ .grapheme = g }, .style = .{ .fg = .{ .rgb = gray.dim } } });
}
}
}
@@ -698,13 +799,13 @@ pub fn main(init: std.process.Init) !void {
.border_v => |d| {
var ry: u16 = 0;
while (ry < screen_h) : (ry += 1)
- win.writeCell(d.cur_x, ry, .{ .char = .{ .grapheme = "╎" }, .style = .{ .fg = .{ .index = 3 } } });
+ win.writeCell(d.cur_x, ry, .{ .char = .{ .grapheme = "╎" }, .style = .{ .fg = .{ .rgb = gray.lit } } });
},
.border_h => |d| {
const c = d.col;
var rx: u16 = col_x[c];
while (rx < col_x[c] + col_w[c]) : (rx += 1)
- win.writeCell(rx, d.cur_y, .{ .char = .{ .grapheme = "╌" }, .style = .{ .fg = .{ .index = 3 } } });
+ win.writeCell(rx, d.cur_y, .{ .char = .{ .grapheme = "╌" }, .style = .{ .fg = .{ .rgb = gray.lit } } });
},
.move => |d| {
// insertion indicator: dashed line at cur_y across the target column
@@ -717,8 +818,8 @@ pub fn main(init: std.process.Init) !void {
}
var rx: u16 = col_x[tc];
while (rx < col_x[tc] + col_w[tc]) : (rx += 1)
- win.writeCell(rx, d.cur_y, .{ .char = .{ .grapheme = "╌" }, .style = .{ .fg = .{ .index = 2 } } });
- win.writeCell(d.cur_x, d.cur_y, .{ .char = .{ .grapheme = "▌" }, .style = .{ .fg = .{ .index = 2 } } });
+ win.writeCell(rx, d.cur_y, .{ .char = .{ .grapheme = "╌" }, .style = .{ .fg = .{ .rgb = gray.mid } } });
+ win.writeCell(d.cur_x, d.cur_y, .{ .char = .{ .grapheme = "▌" }, .style = .{ .fg = .{ .rgb = gray.mid } } });
},
else => {},
}
@@ -740,8 +841,8 @@ pub fn main(init: std.process.Init) !void {
const oh: u16 = 6;
const ox: u16 = if (screen_w > ow) screen_w - ow else 0;
const ov = win.child(.{ .x_off = ox, .y_off = 0, .width = ow, .height = oh, .border = .{ .where = .all } });
- ov.fill(.{ .char = .{ .grapheme = " " }, .style = .{ .bg = .{ .index = 0 } } });
- _ = ov.printSegment(.{ .text = s, .style = .{ .fg = .{ .index = 3 } } }, .{});
+ ov.fill(.{ .char = .{ .grapheme = " " }, .style = .{ .bg = .{ .rgb = gray.bar } } });
+ _ = ov.printSegment(.{ .text = s, .style = .{ .fg = .{ .rgb = gray.txt } } }, .{});
}
try vx.render(tty.writer());
@@ -791,23 +892,27 @@ fn moveTerm(
layoutInsert(l, tc, ti, id);
}
-// Build the block-selected text, newline-joined per row; caller frees.
+// 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.
fn selectionText(gpa: std.mem.Allocator, t: *Term, sl: Term.Sel) ![]u8 {
- const off: i32 = @intCast(t.term.screens.active.pages.scrollbar().offset);
const r0 = @min(sl.r0, sl.r1);
const r1 = @max(sl.r0, sl.r1);
const c0: usize = @intCast(@max(0, @min(sl.c0, sl.c1)));
const c1: usize = @intCast(@max(0, @max(sl.c0, sl.c1)));
- const ps = try t.term.plainString(gpa);
- defer gpa.free(ps);
+ const tag = try tagLine(gpa, t);
+ defer gpa.free(tag);
+ const body = try t.term.plainString(gpa);
+ defer gpa.free(body);
+ const pane = try std.fmt.allocPrint(gpa, "{s}\n{s}", .{ tag, body });
+ defer gpa.free(pane);
var out: std.ArrayList(u8) = .empty;
errdefer out.deinit(gpa);
- var it = std.mem.splitAny(u8, ps, "\n");
- var i: i32 = 0;
+ var it = std.mem.splitAny(u8, pane, "\n");
+ var v: i32 = 0;
var first = true;
- while (it.next()) |line| : (i += 1) {
- const j = i + off;
- if (j < r0 or j > r1) continue;
+ while (it.next()) |line| : (v += 1) {
+ if (v < r0 or v > r1) continue;
if (!first) try out.append(gpa, '\n');
first = false;
const b0 = @min(c0, line.len);
@@ -817,9 +922,14 @@ fn selectionText(gpa: std.mem.Allocator, t: *Term, sl: Term.Sel) ![]u8 {
return out.toOwnedSlice(gpa);
}
-// Empty selection callback; receives the button and the selected text slice.
-// Fill in per-button behaviour later (acme: left=snarf, middle=execute, right=look).
-fn onSelect(button: Button, text: []const u8) void {
- _ = button;
- _ = text;
+// Decide what a selection means. Middle button: a builtin name (comptime-matched
+// against `builtins`) runs that builtin; anything else is sent to the terminal.
+fn onSelect(button: Button, text: []const u8) SelectAction {
+ if (button != .middle) return .none;
+ const cmd = std.mem.trim(u8, text, " \t\r\n");
+ if (cmd.len == 0) return .none;
+ inline for (builtins) |b| {
+ if (std.mem.eql(u8, cmd, b.name)) return b.action;
+ }
+ return .send;
}