const std = @import("std"); const vaxis = @import("vaxis"); const ghostty_vt = @import("ghostty-vt"); extern "c" fn forkpty( amaster: *c_int, name: ?[*:0]u8, termp: ?*const anyopaque, winp: ?*const std.posix.winsize, ) c_int; extern "c" fn execvp( file: [*:0]const u8, argv: [*:null]const ?[*:0]const u8, ) c_int; extern "c" fn _exit(status: c_int) noreturn; // basic file navigation // basic shell io // basic layouting: for now following acme and ad // * /home/goblin/05-genizah/plan9port/src/cmd/acme // * /home/goblin/05-genizah/ad // * /usr/lib/zig // * ../03-plano // right click and enter on lines to open files at specified lines pub const Command = struct { prev: ?*Command = null, pub var value: union(enum) { close, ping, nop, quit, tick, key_press: vaxis.Key, pty_read: []u8, winsize: vaxis.Winsize, mouse: vaxis.Mouse, } = .nop; }; const Loop = vaxis.Loop(@TypeOf(Command.value)); const MAX_FILES: usize = 0x200; fn ticker(io: std.Io, loop: *Loop) void { while (true) { io.sleep(.fromMilliseconds(2), .awake) catch return; _ = loop.tryPostEvent(.tick) catch return; } } fn fork_pty(argv: [:0]const u8, ws: std.posix.winsize) std.Io.File { var master: c_int = undefined; const pid = forkpty(&master, null, null, &ws); if (pid == 0) { const child_argv = [_:null]?[*:0]const u8{ argv.ptr, null }; _ = execvp(argv.ptr, &child_argv); _exit(127); } // not sure about the false here const f: std.Io.File = .{ .handle = master, .flags = .{ .nonblocking = false } }; return f; } // TIOCGWINSZ: ask the kernel what size the pty currently thinks it is. fn pty_get_size(pty: std.Io.File) std.posix.winsize { var ws: std.posix.winsize = .{ .row = 0, .col = 0, .xpixel = 0, .ypixel = 0 }; _ = std.posix.system.ioctl(pty.handle, std.posix.T.IOCGWINSZ, @intFromPtr(&ws)); return ws; } // TIOCSWINSZ: set the pty size; the kernel raises SIGWINCH in the child (bash) // so it (and anything running in it) reflows to the new grid. fn pty_set_size(pty: std.Io.File, rows: u16, cols: u16) void { const ws: std.posix.winsize = .{ .row = rows, .col = cols, .xpixel = 0, .ypixel = 0 }; _ = std.posix.system.ioctl(pty.handle, std.posix.T.IOCSWINSZ, @intFromPtr(&ws)); } fn readPty(io: std.Io, gpa: std.mem.Allocator, pty: std.Io.File, loop: *Loop) !void { var read_buf: [0x1000]u8 = undefined; var reader = pty.readerStreaming(io, &read_buf); while (true) { var buf: [0x1000]u8 = undefined; var vec = [_][]u8{&buf}; const n = try reader.interface.readVec(&vec); if (n == 0) break; const bytes = try gpa.dupe(u8, buf[0..n]); errdefer gpa.free(bytes); try loop.postEvent(.{ .pty_read = bytes }); } try loop.postEvent(.quit); } const term_x_padding = 3; pub fn main(init: std.process.Init) !void { const io = init.io; const gpa = init.gpa; // live grid size of the embedded terminal; recomputed on every resize. // seeded so the pty/ghostty have something before the first winsize event. var nrows: u16 = 64; var ncols: u16 = 100; // how many winsize events we've handled; shown on the debug panel. var resize_count: usize = 0; var term: ghostty_vt.Terminal = try .init(gpa, .{ .cols = ncols, .rows = nrows, }); defer term.deinit(gpa); var term_stream = term.vtStream(); defer term_stream.deinit(); var tty_buf: [0x10000]u8 = undefined; var tty = try vaxis.Tty.init(io, &tty_buf); var vx = try vaxis.init(io, gpa, init.environ_map, .{}); defer vx.deinit(gpa, tty.writer()); const pty_master = fork_pty("/usr/bin/bash", .{ .row = nrows, .col = ncols, .xpixel = 0, .ypixel = 0, }); var write_buf: [0x100]u8 = undefined; var writer = pty_master.writer(io, &write_buf); std.debug.print("written {}\n", .{try writer.interface.write("ls /\r\n")}); try writer.flush(); try vx.setMouseMode(tty.writer(), true); try vx.enterAltScreen(tty.writer()); defer vx.exitAltScreen(tty.writer()) catch {}; var loop: Loop = .init(io, &tty, &vx); try loop.start(); defer loop.stop(); // start() only spawns the tty-read thread; it does NOT wire up resize // delivery. Without this, the only winsize event we ever see is the initial // one ttyRun posts, so the terminal never tracks the window after startup. // installResizeHandler registers the SIGWINCH handler that posts a winsize // event on every resize. (vaxis resets it automatically if the terminal // later negotiates in-band resize via queryTerminal.) try loop.installResizeHandler(); var pty_reader = try io.concurrent(readPty, .{ io, gpa, pty_master, &loop }); defer _ = pty_reader.cancel(io) catch {}; defer pty_master.close(io); var dbg_print_buf: [0x1000]u8 = undefined; while (true) { const event = try loop.nextEvent(); const win = vx.window(); switch (event) { .mouse => |mouse| { if (mouse.button == .wheel_down) { term.screens.active.scroll(.{ .delta_row = 1 }); } if (mouse.button == .wheel_up) { term.screens.active.scroll(.{ .delta_row = -1 }); } if (mouse.type == .press) { if (mouse.button == .left and mouse.col <= term_x_padding) { term.screens.active.scroll(.{ .delta_row = -(mouse.row) }); } if (mouse.button == .left and mouse.col > term_x_padding) {} if (mouse.button == .right and mouse.col <= term_x_padding) { term.screens.active.scroll(.{ .delta_row = mouse.row }); } } }, .key_press => |key| { if (key.matches('c', .{ .ctrl = true })) { break; } if (key.matches('k', .{ .ctrl = true })) { win.clear(); } // this is not what I expected; it's just blank up or down // if (key.matches('d', .{ .ctrl = true })) { // try term.scrollUp(1); // } if (key.matches('d', .{ .ctrl = true })) { term.screens.active.scroll(.{ .delta_row = 15 }); } if (key.matches('u', .{ .ctrl = true })) { term.screens.active.scroll(.{ .delta_row = -15 }); } if (key.text) |text| { try writer.interface.writeAll(text); } else switch (key.codepoint) { vaxis.Key.enter => try writer.interface.writeAll("\r"), vaxis.Key.backspace => try writer.interface.writeAll("\x7f"), vaxis.Key.tab => try writer.interface.writeAll("\t"), vaxis.Key.escape => try writer.interface.writeAll("\x1b"), vaxis.Key.up => try writer.interface.writeAll("\x1b[A"), vaxis.Key.down => try writer.interface.writeAll("\x1b[B"), vaxis.Key.right => try writer.interface.writeAll("\x1b[C"), vaxis.Key.left => try writer.interface.writeAll("\x1b[D"), else => {}, } try writer.flush(); }, .pty_read => |bytes| { defer gpa.free(bytes); term_stream.nextSlice(bytes); }, .winsize => |ws| { resize_count += 1; // 1. let vaxis resize its own screen buffers try vx.resize(gpa, tty.writer(), ws); // 2. carve a new grid out of the window. layout is: left // scrollbar gutter, then the terminal pane and the debug pane // side by side, each wrapped in a 1-cell border. so the two // panes split the leftover width, minus 4 cols of borders. const usable: u16 = if (ws.cols > term_x_padding + 4) ws.cols - term_x_padding - 4 else 2; // ncols = @max(1, usable / 2); ncols = usable; nrows = @max(1, if (ws.rows > 2) ws.rows - 2 else 1); // 3. push it to ghostty's emulator grid try term.resize(gpa, ncols, nrows); // 4. push it to the pty so bash (and its children) reflow pty_set_size(pty_master, nrows, ncols); }, .quit => break, else => {}, } // term.scrollViewport(.bottom); const tty_str = try term.plainString(gpa); defer gpa.free(tty_str); const win_child = win.child(.{ .border = .{ .where = .bottom }, .x_off = term_x_padding, .height = nrows + 2, .width = ncols + 2, }); const win_scroll = win.child(.{ .border = .{ .where = .none }, .height = nrows + 2, .width = term_x_padding - 1, }); const scroll_pos = (@as(usize, win_scroll.height) * term.screens.active.pages.scrollbar().offset) / term.screens.active.pages.scrollbar().total; const scroll_height = @max(1, (@as(usize, win_scroll.height) * term.screens.active.pages.scrollbar().len) / term.screens.active.pages.scrollbar().total); win_scroll.fill(.{ .char = .{ .grapheme = " " }, .style = .{ .bg = .{ .index = 4 } }, }); for (0..term_x_padding) |col| { for (0..scroll_height) |row| { win_scroll.writeCell(@intCast(col), @intCast(scroll_pos + row), .{ .char = .{ .grapheme = " " }, .style = .{ .bg = .{ .index = 0 }, .fg = .{ .index = 6 } }, }); } } const win_dbg = win.child(.{ .border = .{ .where = .all }, .x_off = (ncols / 2) + 2 + term_x_padding, .height = nrows + 2, .width = (ncols / 2) + 2, }); win_child.clear(); _ = win_child.printSegment(.{ .text = tty_str }, .{}); win_child.showCursor(term.screens.active.cursor.x, term.screens.active.cursor.y); const pty_ws = pty_get_size(pty_master); const dbg_str = try std.fmt.bufPrintSentinel( &dbg_print_buf, "c {} {}\npty pid {}\n grid {}x{} pty {}x{}\n resizes={}\n scroll {}/{} vis: {}\n", .{ pty_master.handle, term.screens.active.cursor.x, term.screens.active.cursor.y, ncols, nrows, pty_ws.col, pty_ws.row, resize_count, term.screens.active.pages.scrollbar().offset, term.screens.active.pages.scrollbar().total, term.screens.active.pages.scrollbar().len, }, 0, ); _ = win_dbg.printSegment(.{ .text = dbg_str }, .{}); try vx.render(tty.writer()); } }