const std = @import("std"); const vaxis = @import("vaxis"); const ghostty_vt = @import("ghostty-vt"); const ts = @import("tree-sitter"); const ts_queries = @import("ts_queries"); extern fn tree_sitter_c() callconv(.c) *const ts.Language; extern fn tree_sitter_rust() callconv(.c) *const ts.Language; extern fn tree_sitter_zig() callconv(.c) *const ts.Language; 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 const Event = union(enum) { quit, key_press: vaxis.Key, pty_read: []u8, winsize: vaxis.Winsize, mouse: vaxis.Mouse, }; const Loop = vaxis.Loop(Event); const MAX_LINES: usize = 0x1000; const MAX_LINE_LEN: usize = 0x200; const MAX_HISTORY: usize = 0x100; const FILE_PANE: usize = 0; const TTY_PANE: usize = 1; const TS_STRING: u8 = 1; const TS_NUMBER: u8 = 2; const TS_KEYWORD: u8 = 3; 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; } 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); } fn pathFrom(base: []const u8, name: []const u8, buf: *[MAX_LINE_LEN]u8) []const u8 { if (std.fs.path.isAbsolute(name)) return name; return std.fmt.bufPrint(buf, "{s}/{s}", .{ base, name }) catch name; } // lines point into the backing text (file_text / tty_text); no copy. fn splitLines(lines: *[MAX_LINES][]const u8, text: []const u8) usize { var iter = std.mem.splitScalar(u8, text, '\n'); var i: usize = 0; while (iter.next()) |line| : (i += 1) { if (i >= MAX_LINES) break; lines[i] = line; } return i; } fn extractWord(str: []const u8, i: usize) []const u8 { if (str.len == 0) return ""; var l = @min(i, str.len - 1); var r = @min(i, str.len); while (l > 0 and !std.ascii.isWhitespace(str[l - 1])) : (l -= 1) {} while (r < str.len and !std.ascii.isWhitespace(str[r])) : (r += 1) {} var word = str[l..r]; word = std.mem.trim(u8, word, " \t\r\n\"'`()[]{}<>;,"); if (std.mem.indexOfScalar(u8, word, ':')) |colon| { word = word[0..colon]; } return word; } fn loadFile( io: std.Io, gpa: std.mem.Allocator, path: []const u8, lines: *[MAX_LINES][]const u8, rendered_lines: *usize, current_path_buf: *[MAX_LINE_LEN]u8, current_path: *[]const u8, file_text: *[]u8, highlight_styles: *[MAX_LINES][MAX_LINE_LEN]u8, ) !void { for (highlight_styles) |*row| @memset(row, 0); current_path.* = std.fmt.bufPrint(current_path_buf, "{s}", .{path}) catch path; if (file_text.*.len > 0) gpa.free(file_text.*); file_text.* = &.{}; var file = std.Io.Dir.cwd().openFile(io, path, .{ .mode = .read_only }) catch |err| { file_text.* = try std.fmt.allocPrint(gpa, "could not open {s}: {}", .{ path, err }); rendered_lines.* = splitLines(lines, file_text.*); return; }; defer file.close(io); var read_buf: [0x1000]u8 = undefined; const flen = try file.length(io); var reader = file.reader(io, &read_buf); file_text.* = reader.interface.readAlloc(gpa, @min(flen, 0x100000)) catch |err| { file_text.* = try std.fmt.allocPrint(gpa, "could not read {s}: {}", .{ path, err }); rendered_lines.* = splitLines(lines, file_text.*); return; }; rendered_lines.* = splitLines(lines, file_text.*); var language: ?*const ts.Language = null; var query_text: []const u8 = ""; const ext = std.fs.path.extension(path); if (std.mem.eql(u8, ext, ".c") or std.mem.eql(u8, ext, ".h")) { language = tree_sitter_c(); query_text = ts_queries.c_highlights; } if (std.mem.eql(u8, ext, ".rs")) { language = tree_sitter_rust(); query_text = ts_queries.rust_highlights; } if (std.mem.eql(u8, ext, ".zig") or std.mem.eql(u8, ext, ".zon")) { language = tree_sitter_zig(); query_text = ts_queries.zig_highlights; } if (language) |lang| { defer lang.destroy(); const parser = ts.Parser.create(); defer parser.destroy(); parser.setLanguage(lang) catch return; const tree = parser.parseString(file_text.*, null) orelse return; defer tree.destroy(); var error_offset: u32 = 0; const query = ts.Query.create(lang, query_text, &error_offset) catch return; defer query.destroy(); const cursor = ts.QueryCursor.create(); defer cursor.destroy(); cursor.exec(query, tree.rootNode()); while (cursor.nextMatch()) |match| { for (match.captures) |capture| { const capture_name = query.captureNameForId(capture.index) orelse ""; var style: u8 = 0; if (std.mem.indexOf(u8, capture_name, "string") != null or std.mem.indexOf(u8, capture_name, "character") != null) style = TS_STRING; if (std.mem.indexOf(u8, capture_name, "number") != null or std.mem.indexOf(u8, capture_name, "float") != null or std.mem.indexOf(u8, capture_name, "boolean") != null) style = TS_NUMBER; if (std.mem.indexOf(u8, capture_name, "keyword") != null or std.mem.indexOf(u8, capture_name, "include") != null or std.mem.indexOf(u8, capture_name, "operator") != null) style = TS_KEYWORD; if (style == 0) continue; const start = capture.node.startPoint(); const end = capture.node.endPoint(); var row: usize = start.row; while (row <= end.row and row < MAX_LINES) : (row += 1) { const start_col: usize = if (row == start.row) start.column else 0; const end_col: usize = if (row == end.row) end.column else MAX_LINE_LEN - 1; var col = @min(start_col, MAX_LINE_LEN - 1); while (col < end_col and col < MAX_LINE_LEN - 1) : (col += 1) { highlight_styles[row][col] = style; } } } } } } fn ghostColor(color: ghostty_vt.Style.Color) vaxis.Color { return switch (color) { .none => .default, .palette => |idx| .{ .index = idx }, .rgb => |rgb| .{ .rgb = .{ rgb.r, rgb.g, rgb.b } }, }; } fn cellStyle(cell_info: ghostty_vt.PageList.Cell) vaxis.Style { const style = cell_info.style(); var vstyle: vaxis.Style = .{}; vstyle.fg = ghostColor(style.fg_color); vstyle.bg = ghostColor(style.bg_color); vstyle.bold = style.flags.bold; vstyle.dim = style.flags.faint; vstyle.italic = style.flags.italic; vstyle.blink = style.flags.blink; vstyle.reverse = style.flags.inverse; vstyle.invisible = style.flags.invisible; vstyle.strikethrough = style.flags.strikethrough; vstyle.ul_style = switch (style.flags.underline) { .none => .off, .single => .single, .double => .double, .curly => .curly, .dotted => .dotted, .dashed => .dashed, }; switch (cell_info.cell.content_tag) { .bg_color_palette => vstyle.bg = .{ .index = cell_info.cell.content.color_palette.data }, .bg_color_rgb => { const rgb = cell_info.cell.content.color_rgb; vstyle.bg = .{ .rgb = .{ rgb.r, rgb.g, rgb.b } }; }, else => {}, } return vstyle; } fn appendCp(buf: *[MAX_LINE_LEN]u8, len: *usize, cp: u21) void { if (cp == 0) { if (len.* < buf.len) { buf[len.*] = ' '; len.* += 1; } return; } if (len.* + 4 > buf.len) return; const n = std.unicode.utf8Encode(cp, buf[len.*..]) catch return; len.* += n; } const nrows = 64; const ncols = 100; pub fn main(init: std.process.Init) !void { const io = init.io; const gpa = init.gpa; const args = try init.minimal.args.toSlice(init.arena.allocator()); 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 display_lines: [2][MAX_LINES][]const u8 = undefined; var tty_render_lines: [MAX_LINES][MAX_LINE_LEN]u8 = undefined; var highlight_styles: [MAX_LINES][MAX_LINE_LEN]u8 = undefined; var file_text: []u8 = &.{}; defer if (file_text.len > 0) gpa.free(file_text); var tty_text: []const u8 = &.{}; defer if (tty_text.len > 0) gpa.free(tty_text); var rendered_lines = [_]usize{ 0, 0 }; var line_nums: [MAX_LINES][8]u8 = undefined; var file_scroll: usize = 0; var current_path_buf: [MAX_LINE_LEN]u8 = undefined; var current_path: []const u8 = ""; var term_cwd_buf: [MAX_LINE_LEN]u8 = undefined; var term_cwd: []const u8 = init.environ_map.get("PWD") orelse "."; var cwd_history_buf: [MAX_HISTORY][MAX_LINE_LEN]u8 = undefined; var cwd_history: [MAX_HISTORY][]const u8 = undefined; var cwd_history_len: usize = 0; try loadFile( io, gpa, if (args.len > 1) args[1] else "main.zig", &display_lines[FILE_PANE], &rendered_lines[FILE_PANE], ¤t_path_buf, ¤t_path, &file_text, &highlight_styles, ); 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(); var pty_reader = try io.concurrent(readPty, .{ io, gpa, pty_master, &loop }); defer _ = pty_reader.cancel(io) catch {}; defer pty_master.close(io); var mouse_seen = false; var mouse_row: i16 = 0; var mouse_col: i16 = 0; while (true) { const event = try loop.nextEvent(); const win = vx.window(); const left_outer_width: u16 = @max(1, win.width / 2); const right_x: i16 = @intCast(left_outer_width); switch (event) { .mouse => |mouse| { mouse_seen = true; mouse_row = mouse.row; mouse_col = mouse.col; const mouse_on_left = mouse.col < right_x; if (mouse.button == .wheel_down) { if (mouse_on_left) { if (file_scroll + 1 < rendered_lines[FILE_PANE]) file_scroll += 1; } else { term.screens.active.scroll(.{ .delta_row = 1 }); } } if (mouse.button == .wheel_up) { if (mouse_on_left) { if (file_scroll > 0) file_scroll -= 1; } else { term.screens.active.scroll(.{ .delta_row = -1 }); } } if (mouse.type == .press and (mouse.button == .left or mouse.button == .right)) { var selected_word: ?[]const u8 = null; if (mouse_on_left) { if (mouse.row > 0 and mouse.col >= 5) { const line_idx = @as(usize, @intCast(mouse.row - 1)) + file_scroll; if (line_idx < rendered_lines[FILE_PANE]) { const selected_col: usize = @intCast(mouse.col - 5); selected_word = extractWord(display_lines[FILE_PANE][line_idx], selected_col); } } } else { const line_idx: usize = @intCast(@max(mouse.row, 0)); if (line_idx < rendered_lines[TTY_PANE]) { const selected_col: usize = @intCast(@max(mouse.col - right_x, 0)); selected_word = extractWord(display_lines[TTY_PANE][line_idx], selected_col); } } if (selected_word) |word| { if (word.len > 0) { var candidate_buf: [MAX_LINE_LEN]u8 = undefined; var found_path: []const u8 = ""; var found_kind: ?std.Io.File.Kind = null; const current_candidate = pathFrom(term_cwd, word, &candidate_buf); if (std.Io.Dir.cwd().statFile(io, current_candidate, .{})) |stat| { found_path = current_candidate; found_kind = stat.kind; } else |_| {} var h = cwd_history_len; while (found_kind == null and h > 0) { h -= 1; const candidate = pathFrom(cwd_history[h], word, &candidate_buf); if (std.Io.Dir.cwd().statFile(io, candidate, .{})) |stat| { found_path = candidate; found_kind = stat.kind; } else |_| {} } if (found_kind) |kind| { if (kind == .directory) { if (cwd_history_len < MAX_HISTORY) { cwd_history[cwd_history_len] = std.fmt.bufPrint(&cwd_history_buf[cwd_history_len], "{s}", .{term_cwd}) catch term_cwd; cwd_history_len += 1; } term_cwd = std.fmt.bufPrint(&term_cwd_buf, "{s}", .{found_path}) catch term_cwd; try writer.interface.writeAll("cd "); try writer.interface.writeAll(found_path); try writer.interface.writeAll(" && ls\r"); try writer.flush(); if (!mouse_on_left) term.scrollViewport(.bottom); } else { loadFile(io, gpa, found_path, &display_lines[FILE_PANE], &rendered_lines[FILE_PANE], ¤t_path_buf, ¤t_path, &file_text, &highlight_styles) catch {}; file_scroll = 0; } } } } } }, .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| try vx.resize(gpa, tty.writer(), ws), .quit => break, } // term.scrollViewport(.bottom); if (tty_text.len > 0) gpa.free(tty_text); tty_text = try term.plainString(gpa); rendered_lines[TTY_PANE] = splitLines(&display_lines[TTY_PANE], tty_text); const win_file = win.child(.{ .border = .{ .where = .right }, .width = left_outer_width, }); const win_term = win.child(.{ .x_off = @intCast(left_outer_width), .width = win.width -| left_outer_width, }); win.clear(); _ = win_file.printSegment(.{ .text = current_path }, .{ .row_offset = 0, .col_offset = 5, .wrap = .none, }); const visible_lines = if (win_file.height > 1) win_file.height - 1 else 0; var i: usize = 0; while (i < visible_lines and i + file_scroll < rendered_lines[FILE_PANE]) : (i += 1) { const line_idx = i + file_scroll; const len = @min(display_lines[FILE_PANE][line_idx].len, MAX_LINE_LEN); const num = try std.fmt.bufPrint(&line_nums[i], "{d:0>4}", .{line_idx + 1}); _ = win_file.printSegment(.{ .text = num }, .{ .row_offset = @intCast(i + 1), .col_offset = 0, .wrap = .none, }); var col: usize = 0; while (col < len) { const style_id = highlight_styles[line_idx][col]; var end = col + 1; while (end < len and highlight_styles[line_idx][end] == style_id) : (end += 1) {} var style: vaxis.Style = .{}; var rgb = term.colors.palette.current[0]; switch (style_id) { TS_STRING => { rgb = term.colors.palette.current[2]; style = .{ .fg = .{ .rgb = .{ rgb.r, rgb.g, rgb.b } } }; }, TS_NUMBER => { rgb = term.colors.palette.current[3]; style = .{ .fg = .{ .rgb = .{ rgb.r, rgb.g, rgb.b } } }; }, TS_KEYWORD => { rgb = term.colors.palette.current[5]; style = .{ .fg = .{ .rgb = .{ rgb.r, rgb.g, rgb.b } }, .bold = true }; }, else => {}, } _ = win_file.printSegment(.{ .text = display_lines[FILE_PANE][line_idx][col..end], .style = style, }, .{ .row_offset = @intCast(i + 1), .col_offset = @intCast(5 + col), .wrap = .none, }); col = end; } } i = 0; while (i < win_term.height and i < MAX_LINES) : (i += 1) { var len: usize = 0; var span_start: usize = 0; var span_col: usize = 0; var span_style: vaxis.Style = .{}; var span_open = false; var col: usize = 0; while (col < win_term.width and col < ncols and len < MAX_LINE_LEN - 4) : (col += 1) { const cell_info = term.screens.active.pages.getCell(.{ .viewport = .{ .x = @intCast(col), .y = @intCast(i), } }) orelse continue; if (cell_info.cell.wide == .spacer_tail) continue; const style = cellStyle(cell_info); if (!span_open) { span_open = true; span_start = len; span_col = col; span_style = style; } else if (!vaxis.Style.eql(span_style, style)) { _ = win_term.printSegment(.{ .text = tty_render_lines[i][span_start..len], .style = span_style, }, .{ .row_offset = @intCast(i), .col_offset = @intCast(span_col), .wrap = .none, }); span_start = len; span_col = col; span_style = style; } switch (cell_info.cell.content_tag) { .codepoint, .codepoint_grapheme => { appendCp(&tty_render_lines[i], &len, cell_info.cell.codepoint()); if (cell_info.cell.hasGrapheme()) { if (cell_info.node.data.lookupGrapheme(cell_info.cell)) |grapheme| { for (grapheme) |cp| appendCp(&tty_render_lines[i], &len, cp); } } }, .bg_color_palette, .bg_color_rgb => appendCp(&tty_render_lines[i], &len, ' '), } } if (span_open and len > span_start) { _ = win_term.printSegment(.{ .text = tty_render_lines[i][span_start..len], .style = span_style, }, .{ .row_offset = @intCast(i), .col_offset = @intCast(span_col), .wrap = .none, }); } } win_term.showCursor(term.screens.active.cursor.x, term.screens.active.cursor.y); if (mouse_seen and mouse_row >= 0 and mouse_col >= 0 and mouse_row < win.height and mouse_col < win.width) { _ = win.printSegment(.{ .text = "@", .style = .{ .reverse = true, .bold = true } }, .{ .row_offset = @intCast(mouse_row), .col_offset = @intCast(mouse_col), .wrap = .none, }); } try vx.render(tty.writer()); } }