this repo has no description
13
fork

Configure Feed

Select the types of activity you want to include in your feed.

widgets(terminal): begin terminal widget

+988 -1
+1
build.zig
··· 64 64 table, 65 65 text_input, 66 66 vaxis, 67 + vt, 67 68 xev, 68 69 }; 69 70 const example_option = b.option(Example, "example", "Example to run (default: text_input)") orelse .text_input;
+136
examples/vt.zig
··· 1 + const std = @import("std"); 2 + const vaxis = @import("vaxis"); 3 + const Cell = vaxis.Cell; 4 + 5 + const Event = union(enum) { 6 + key_press: vaxis.Key, 7 + winsize: vaxis.Winsize, 8 + }; 9 + 10 + pub const panic = vaxis.panic_handler; 11 + 12 + pub fn main() !void { 13 + var gpa = std.heap.GeneralPurposeAllocator(.{}){}; 14 + defer { 15 + const deinit_status = gpa.deinit(); 16 + //fail test; can't try in defer as defer is executed after we return 17 + if (deinit_status == .leak) { 18 + std.log.err("memory leak", .{}); 19 + } 20 + } 21 + const alloc = gpa.allocator(); 22 + 23 + var tty = try vaxis.Tty.init(); 24 + var vx = try vaxis.init(alloc, .{}); 25 + defer vx.deinit(alloc, tty.anyWriter()); 26 + 27 + var loop: vaxis.Loop(Event) = .{ .tty = &tty, .vaxis = &vx }; 28 + try loop.init(); 29 + 30 + try loop.start(); 31 + defer loop.stop(); 32 + 33 + try vx.enterAltScreen(tty.anyWriter()); 34 + try vx.queryTerminal(tty.anyWriter(), 1 * std.time.ns_per_s); 35 + var env = try std.process.getEnvMap(alloc); 36 + defer env.deinit(); 37 + 38 + const vt_opts: vaxis.widgets.Terminal.Options = .{ 39 + .winsize = .{ 40 + .rows = 24, 41 + .cols = 100, 42 + .x_pixel = 0, 43 + .y_pixel = 0, 44 + }, 45 + .scrollback_size = 0, 46 + }; 47 + const argv1 = [_][]const u8{"senpai"}; 48 + const argv2 = [_][]const u8{"nvim"}; 49 + const argv3 = [_][]const u8{"senpai"}; 50 + // const argv = [_][]const u8{"senpai"}; 51 + // const argv = [_][]const u8{"comlink"}; 52 + var vt1 = try vaxis.widgets.Terminal.init( 53 + alloc, 54 + &argv1, 55 + &env, 56 + &vx.unicode, 57 + vt_opts, 58 + ); 59 + defer vt1.deinit(); 60 + try vt1.spawn(); 61 + var vt2 = try vaxis.widgets.Terminal.init( 62 + alloc, 63 + &argv2, 64 + &env, 65 + &vx.unicode, 66 + vt_opts, 67 + ); 68 + defer vt2.deinit(); 69 + try vt2.spawn(); 70 + var vt3 = try vaxis.widgets.Terminal.init( 71 + alloc, 72 + &argv3, 73 + &env, 74 + &vx.unicode, 75 + vt_opts, 76 + ); 77 + defer vt3.deinit(); 78 + try vt3.spawn(); 79 + 80 + while (true) { 81 + std.time.sleep(8 * std.time.ns_per_ms); 82 + while (loop.tryEvent()) |event| { 83 + switch (event) { 84 + .key_press => |key| if (key.matches('c', .{ .ctrl = true })) return, 85 + .winsize => |ws| { 86 + try vx.resize(alloc, tty.anyWriter(), ws); 87 + }, 88 + } 89 + } 90 + 91 + const win = vx.window(); 92 + win.clear(); 93 + const left = win.child(.{ 94 + .width = .{ .limit = win.width / 2 }, 95 + .border = .{ 96 + .where = .right, 97 + }, 98 + }); 99 + 100 + const right_top = win.child(.{ 101 + .x_off = left.width + 1, 102 + .height = .{ .limit = win.height / 2 }, 103 + .border = .{ 104 + .where = .bottom, 105 + }, 106 + }); 107 + const right_bot = win.child(.{ 108 + .x_off = left.width + 1, 109 + .y_off = right_top.height + 1, 110 + }); 111 + 112 + try vt1.resize(.{ 113 + .rows = left.height, 114 + .cols = left.width, 115 + .x_pixel = 0, 116 + .y_pixel = 0, 117 + }); 118 + try vt2.resize(.{ 119 + .rows = right_top.height, 120 + .cols = right_bot.width, 121 + .x_pixel = 0, 122 + .y_pixel = 0, 123 + }); 124 + try vt3.resize(.{ 125 + .rows = right_bot.height, 126 + .cols = right_bot.width, 127 + .x_pixel = 0, 128 + .y_pixel = 0, 129 + }); 130 + try vt1.draw(left); 131 + try vt2.draw(right_top); 132 + try vt3.draw(right_bot); 133 + 134 + try vx.render(tty.anyWriter()); 135 + } 136 + }
-1
src/Parser.zig
··· 290 290 inline fn parseCsi(input: []const u8, text_buf: []u8) Result { 291 291 // We start iterating at index 2 to get past te '[' 292 292 const sequence = for (input[2..], 2..) |b, i| { 293 - if (i == 2 and b == '?') continue; 294 293 switch (b) { 295 294 0x40...0xFF => break input[0 .. i + 1], 296 295 else => continue,
+1
src/main.zig
··· 26 26 pub const GraphemeCache = @import("GraphemeCache.zig"); 27 27 pub const grapheme = @import("grapheme"); 28 28 pub const Event = @import("event.zig").Event; 29 + pub const Unicode = @import("Unicode.zig"); 29 30 30 31 /// The target TTY implementation 31 32 pub const Tty = switch (builtin.os.tag) {
+1
src/widgets.zig
··· 10 10 pub const LineNumbers = @import("widgets/LineNumbers.zig"); 11 11 pub const TextView = @import("widgets/TextView.zig"); 12 12 pub const CodeView = @import("widgets/CodeView.zig"); 13 + pub const Terminal = @import("widgets/terminal/Terminal.zig"); 13 14 14 15 // Widgets with dependencies 15 16
+84
src/widgets/terminal/Command.zig
··· 1 + const Command = @This(); 2 + 3 + const std = @import("std"); 4 + const builtin = @import("builtin"); 5 + const Pty = @import("Pty.zig"); 6 + 7 + const posix = std.posix; 8 + 9 + argv: []const []const u8, 10 + 11 + // Set after spawn() 12 + pid: ?std.posix.pid_t = null, 13 + 14 + env_map: *const std.process.EnvMap, 15 + 16 + pty: Pty, 17 + 18 + pub fn spawn(self: *Command, allocator: std.mem.Allocator) !void { 19 + var arena_allocator = std.heap.ArenaAllocator.init(allocator); 20 + defer arena_allocator.deinit(); 21 + 22 + const arena = arena_allocator.allocator(); 23 + 24 + const argv_buf = try arena.allocSentinel(?[*:0]const u8, self.argv.len, null); 25 + for (self.argv, 0..) |arg, i| argv_buf[i] = (try arena.dupeZ(u8, arg)).ptr; 26 + 27 + const envp = try createEnvironFromMap(arena, self.env_map); 28 + 29 + const pid = try std.posix.fork(); 30 + if (pid == 0) { 31 + // we are the child 32 + _ = std.os.linux.setsid(); 33 + 34 + // set the controlling terminal 35 + var u: c_uint = std.posix.STDIN_FILENO; 36 + if (posix.system.ioctl(self.pty.tty, posix.T.IOCSCTTY, @intFromPtr(&u)) != 0) return error.IoctlError; 37 + 38 + // set up io 39 + try posix.dup2(self.pty.tty, std.posix.STDIN_FILENO); 40 + try posix.dup2(self.pty.tty, std.posix.STDOUT_FILENO); 41 + try posix.dup2(self.pty.tty, std.posix.STDERR_FILENO); 42 + 43 + // posix.close(self.pty.tty); 44 + // if (self.pty.pty > 2) posix.close(self.pty.pty); 45 + 46 + // exec 47 + const err = std.posix.execvpeZ(argv_buf.ptr[0].?, argv_buf.ptr, envp); 48 + _ = err catch {}; 49 + } 50 + 51 + // we are the parent 52 + self.pid = @intCast(pid); 53 + return; 54 + } 55 + 56 + pub fn kill(self: *Command) void { 57 + if (self.pid) |pid| { 58 + std.posix.kill(pid, std.posix.SIG.TERM) catch {}; 59 + self.pid = null; 60 + } 61 + } 62 + 63 + /// Creates a null-deliminated environment variable block in the format expected by POSIX, from a 64 + /// hash map plus options. 65 + fn createEnvironFromMap( 66 + arena: std.mem.Allocator, 67 + map: *const std.process.EnvMap, 68 + ) ![:null]?[*:0]u8 { 69 + const envp_count: usize = map.count(); 70 + 71 + const envp_buf = try arena.allocSentinel(?[*:0]u8, envp_count, null); 72 + var i: usize = 0; 73 + 74 + { 75 + var it = map.iterator(); 76 + while (it.next()) |pair| { 77 + envp_buf[i] = try std.fmt.allocPrintZ(arena, "{s}={s}", .{ pair.key_ptr.*, pair.value_ptr.* }); 78 + i += 1; 79 + } 80 + } 81 + 82 + std.debug.assert(i == envp_count); 83 + return envp_buf; 84 + }
+141
src/widgets/terminal/Parser.zig
··· 1 + //! An ANSI VT Parser 2 + const Parser = @This(); 3 + 4 + const std = @import("std"); 5 + const Reader = std.io.AnyReader; 6 + 7 + /// A terminal event 8 + const Event = union(enum) { 9 + print: []const u8, 10 + c0: u8, 11 + escape: []const u8, 12 + ss2: u8, 13 + ss3: u8, 14 + csi: []const u8, 15 + osc: []const u8, 16 + apc: []const u8, 17 + }; 18 + 19 + buf: std.ArrayList(u8), 20 + /// a leftover byte from a ground event 21 + pending_byte: ?u8 = null, 22 + 23 + pub fn parseReader(self: *Parser, reader: Reader) !Event { 24 + self.buf.clearRetainingCapacity(); 25 + while (true) { 26 + const b = if (self.pending_byte) |p| p else try reader.readByte(); 27 + self.pending_byte = null; 28 + switch (b) { 29 + // Escape sequence 30 + 0x1b => { 31 + const next = try reader.readByte(); 32 + switch (next) { 33 + 0x4E => return .{ .ss2 = try reader.readByte() }, 34 + 0x4F => return .{ .ss3 = try reader.readByte() }, 35 + 0x50 => try skipUntilST(reader), // DCS 36 + 0x58 => try skipUntilST(reader), // SOS 37 + 0x5B => return self.parseCsi(reader), // CSI 38 + 0x5D => return self.parseOsc(reader), // OSC 39 + 0x5E => try skipUntilST(reader), // PM 40 + 0x5F => return self.parseApc(reader), // APC 41 + 42 + 0x20...0x2F => { 43 + try self.buf.append(next); 44 + return self.parseEscape(reader); // ESC 45 + }, 46 + else => { 47 + try self.buf.append(next); 48 + return .{ .escape = self.buf.items }; 49 + }, 50 + } 51 + }, 52 + // C0 control 53 + 0x00...0x1a, 54 + 0x1c...0x1f, 55 + => return .{ .c0 = b }, 56 + else => { 57 + try self.buf.append(b); 58 + return self.parseGround(reader); 59 + }, 60 + } 61 + } 62 + } 63 + 64 + inline fn parseGround(self: *Parser, reader: Reader) !Event { 65 + while (true) { 66 + const b = try reader.readByte(); 67 + switch (b) { 68 + 0x00...0x1f => { 69 + self.pending_byte = b; 70 + return .{ .print = self.buf.items }; 71 + }, 72 + else => try self.buf.append(b), 73 + } 74 + } 75 + } 76 + 77 + /// parse until b >= 0x30 78 + inline fn parseEscape(self: *Parser, reader: Reader) !Event { 79 + while (true) { 80 + const b = try reader.readByte(); 81 + switch (b) { 82 + 0x20...0x2F => continue, 83 + else => return .{ .escape = self.buf.items }, 84 + } 85 + } 86 + } 87 + 88 + inline fn parseApc(self: *Parser, reader: Reader) !Event { 89 + while (true) { 90 + const b = try reader.readByte(); 91 + switch (b) { 92 + 0x00...0x17, 93 + 0x19, 94 + 0x1c...0x1f, 95 + => continue, 96 + 0x1b => { 97 + try reader.skipBytes(1, .{ .buf_size = 1 }); 98 + return .{ .apc = self.buf.items }; 99 + }, 100 + else => try self.buf.append(b), 101 + } 102 + } 103 + } 104 + 105 + /// Skips sequences until we see an ST (String Terminator, ESC \) 106 + inline fn skipUntilST(reader: Reader) !void { 107 + try reader.skipUntilDelimiterOrEof('\x1b'); 108 + try reader.skipBytes(1, .{ .buf_size = 1 }); 109 + } 110 + 111 + /// Parses an OSC sequence 112 + inline fn parseOsc(self: *Parser, reader: Reader) !Event { 113 + while (true) { 114 + const b = try reader.readByte(); 115 + switch (b) { 116 + 0x00...0x06, 117 + 0x08...0x17, 118 + 0x19, 119 + 0x1c...0x1f, 120 + => continue, 121 + 0x1b => { 122 + try reader.skipBytes(1, .{ .buf_size = 1 }); 123 + return .{ .osc = self.buf.items }; 124 + }, 125 + 0x07 => return .{ .osc = self.buf.items }, 126 + else => try self.buf.append(b), 127 + } 128 + } 129 + } 130 + 131 + inline fn parseCsi(self: *Parser, reader: Reader) !Event { 132 + while (true) { 133 + const b = try reader.readByte(); 134 + try self.buf.append(b); 135 + switch (b) { 136 + // Really we should execute C0 controls, but we just ignore them 137 + 0x40...0xFF => return .{ .csi = self.buf.items }, 138 + else => continue, 139 + } 140 + } 141 + }
+59
src/widgets/terminal/Pty.zig
··· 1 + //! A PTY pair 2 + const Pty = @This(); 3 + 4 + const std = @import("std"); 5 + const builtin = @import("builtin"); 6 + const Winsize = @import("../../main.zig").Winsize; 7 + 8 + const posix = std.posix; 9 + 10 + pty: posix.fd_t, 11 + tty: posix.fd_t, 12 + 13 + /// opens a new tty/pty pair 14 + pub fn init() !Pty { 15 + switch (builtin.os.tag) { 16 + .linux => return openPtyLinux(), 17 + else => @compileError("unsupported os"), 18 + } 19 + } 20 + 21 + /// closes the tty and pty 22 + pub fn deinit(self: Pty) void { 23 + posix.close(self.pty); 24 + posix.close(self.tty); 25 + } 26 + 27 + /// sets the size of the pty 28 + pub fn setSize(self: Pty, ws: Winsize) !void { 29 + const _ws: posix.winsize = .{ 30 + .ws_row = @truncate(ws.rows), 31 + .ws_col = @truncate(ws.cols), 32 + .ws_xpixel = @truncate(ws.x_pixel), 33 + .ws_ypixel = @truncate(ws.y_pixel), 34 + }; 35 + if (posix.system.ioctl(self.pty, posix.T.IOCSWINSZ, @intFromPtr(&_ws)) != 0) 36 + return error.SetWinsizeError; 37 + } 38 + 39 + fn openPtyLinux() !Pty { 40 + const p = try posix.open("/dev/ptmx", .{ .ACCMODE = .RDWR, .NOCTTY = true }, 0); 41 + errdefer posix.close(p); 42 + 43 + // unlockpt 44 + var n: c_uint = 0; 45 + if (posix.system.ioctl(p, posix.T.IOCSPTLCK, @intFromPtr(&n)) != 0) return error.IoctlError; 46 + 47 + // ptsname 48 + if (posix.system.ioctl(p, posix.T.IOCGPTN, @intFromPtr(&n)) != 0) return error.IoctlError; 49 + var buf: [16]u8 = undefined; 50 + const sname = try std.fmt.bufPrint(&buf, "/dev/pts/{d}", .{n}); 51 + std.log.err("pts: {s}", .{sname}); 52 + 53 + const t = try posix.open(sname, .{ .ACCMODE = .RDWR, .NOCTTY = true }, 0); 54 + 55 + return .{ 56 + .pty = p, 57 + .tty = t, 58 + }; 59 + }
+333
src/widgets/terminal/Screen.zig
··· 1 + const std = @import("std"); 2 + const assert = std.debug.assert; 3 + const vaxis = @import("../../main.zig"); 4 + 5 + const log = std.log.scoped(.terminal); 6 + 7 + const Screen = @This(); 8 + 9 + pub const Cell = struct { 10 + char: std.ArrayList(u8) = undefined, 11 + style: vaxis.Style = .{}, 12 + uri: std.ArrayList(u8) = undefined, 13 + uri_id: std.ArrayList(u8) = undefined, 14 + width: u8 = 0, 15 + 16 + wrapped: bool = false, 17 + dirty: bool = true, 18 + }; 19 + 20 + pub const Cursor = struct { 21 + style: vaxis.Style = .{}, 22 + uri: std.ArrayList(u8) = undefined, 23 + uri_id: std.ArrayList(u8) = undefined, 24 + col: usize = 0, 25 + row: usize = 0, 26 + pending_wrap: bool = false, 27 + shape: vaxis.Cell.CursorShape = .default, 28 + 29 + pub fn isOutsideScrollingRegion(self: Cursor, sr: ScrollingRegion) bool { 30 + return self.row < sr.top or 31 + self.row > sr.bottom or 32 + self.col < sr.left or 33 + self.col > sr.right; 34 + } 35 + 36 + pub fn isInsideScrollingRegion(self: Cursor, sr: ScrollingRegion) bool { 37 + return !self.isOutsideScrollingRegion(sr); 38 + } 39 + }; 40 + 41 + pub const ScrollingRegion = struct { 42 + top: usize, 43 + bottom: usize, 44 + left: usize, 45 + right: usize, 46 + 47 + pub fn contains(self: ScrollingRegion, col: usize, row: usize) bool { 48 + return col >= self.left and 49 + col <= self.right and 50 + row >= self.top and 51 + row <= self.bottom; 52 + } 53 + }; 54 + 55 + width: usize = 0, 56 + height: usize = 0, 57 + 58 + scrolling_region: ScrollingRegion, 59 + 60 + buf: []Cell = undefined, 61 + 62 + cursor: Cursor = .{}, 63 + 64 + /// sets each cell to the default cell 65 + pub fn init(alloc: std.mem.Allocator, w: usize, h: usize) !Screen { 66 + var screen = Screen{ 67 + .buf = try alloc.alloc(Cell, w * h), 68 + .scrolling_region = .{ 69 + .top = 0, 70 + .bottom = h - 1, 71 + .left = 0, 72 + .right = w - 1, 73 + }, 74 + .width = w, 75 + .height = h, 76 + }; 77 + for (screen.buf, 0..) |_, i| { 78 + screen.buf[i] = .{ 79 + .char = try std.ArrayList(u8).initCapacity(alloc, 1), 80 + .uri = std.ArrayList(u8).init(alloc), 81 + .uri_id = std.ArrayList(u8).init(alloc), 82 + }; 83 + try screen.buf[i].char.append(' '); 84 + } 85 + return screen; 86 + } 87 + 88 + pub fn deinit(self: *Screen, alloc: std.mem.Allocator) void { 89 + for (self.buf, 0..) |_, i| { 90 + self.buf[i].char.deinit(); 91 + self.buf[i].uri.deinit(); 92 + self.buf[i].uri_id.deinit(); 93 + } 94 + 95 + alloc.free(self.buf); 96 + } 97 + 98 + /// copies the visible area to the destination screen 99 + pub fn copyTo(self: *Screen, dst: *Screen) !void { 100 + for (self.buf, 0..) |cell, i| { 101 + if (!cell.dirty) continue; 102 + self.buf[i].dirty = false; 103 + const grapheme = cell.char.items; 104 + dst.buf[i].char.clearRetainingCapacity(); 105 + try dst.buf[i].char.appendSlice(grapheme); 106 + dst.buf[i].width = cell.width; 107 + dst.buf[i].style = cell.style; 108 + } 109 + } 110 + 111 + pub fn readCell(self: *Screen, col: usize, row: usize) ?vaxis.Cell { 112 + if (self.width < col) { 113 + // column out of bounds 114 + return null; 115 + } 116 + if (self.height < row) { 117 + // height out of bounds 118 + return null; 119 + } 120 + const i = (row * self.width) + col; 121 + assert(i < self.buf.len); 122 + const cell = self.buf[i]; 123 + return .{ 124 + .char = .{ .grapheme = cell.char.items, .width = cell.width }, 125 + .style = cell.style, 126 + }; 127 + } 128 + 129 + /// writes a cell to a location. 0 indexed 130 + pub fn print( 131 + self: *Screen, 132 + grapheme: []const u8, 133 + width: u8, 134 + ) void { 135 + 136 + // FIXME: wrapping 137 + // if (self.cursor.col + width >= self.width) { 138 + // self.cursor.col = 0; 139 + // self.cursor.row += 1; 140 + // } 141 + if (self.cursor.col >= self.width) return; 142 + if (self.cursor.row >= self.height) return; 143 + const col = self.cursor.col; 144 + const row = self.cursor.row; 145 + 146 + const i = (row * self.width) + col; 147 + assert(i < self.buf.len); 148 + self.buf[i].char.clearRetainingCapacity(); 149 + self.buf[i].char.appendSlice(grapheme) catch { 150 + log.warn("couldn't write grapheme", .{}); 151 + }; 152 + self.buf[i].uri.clearRetainingCapacity(); 153 + self.buf[i].uri.appendSlice(self.cursor.uri.items) catch { 154 + log.warn("couldn't write uri", .{}); 155 + }; 156 + self.buf[i].uri_id.clearRetainingCapacity(); 157 + self.buf[i].uri_id.appendSlice(self.cursor.uri_id.items) catch { 158 + log.warn("couldn't write uri_id", .{}); 159 + }; 160 + self.buf[i].style = self.cursor.style; 161 + self.buf[i].width = width; 162 + self.buf[i].dirty = true; 163 + 164 + self.cursor.col += width; 165 + // FIXME: when do we set default in this function?? 166 + // self.buf[i].default = false; 167 + } 168 + 169 + /// IND 170 + pub fn index(self: *Screen) !void { 171 + self.cursor.pending_wrap = false; 172 + 173 + if (self.cursor.isOutsideScrollingRegion(self.scrolling_region)) { 174 + // Outside, we just move cursor down one 175 + self.cursor.row = @min(self.height - 1, self.cursor.row + 1); 176 + return; 177 + } 178 + // We are inside the scrolling region 179 + if (self.cursor.row == self.scrolling_region.bottom) { 180 + // Inside scrolling region *and* at bottom of screen, we scroll contents up and insert a 181 + // blank line 182 + // TODO: scrollback if scrolling region is entire visible screen 183 + @panic("TODO"); 184 + } 185 + self.cursor.row += 1; 186 + } 187 + 188 + fn Parameter(T: type) type { 189 + return struct { 190 + const Self = @This(); 191 + val: T, 192 + // indicates the next parameter is a sub-parameter 193 + has_sub: bool = false, 194 + is_empty: bool = false, 195 + 196 + const Iterator = struct { 197 + bytes: []const u8, 198 + idx: usize = 0, 199 + 200 + fn next(self: *Iterator) ?Self { 201 + const start = self.idx; 202 + var val: T = 0; 203 + while (self.idx < self.bytes.len) { 204 + defer self.idx += 1; // defer so we trigger on return as well 205 + const b = self.bytes[self.idx]; 206 + switch (b) { 207 + 0x30...0x39 => { 208 + val = (val * 10) + (b - 0x30); 209 + if (self.idx == self.bytes.len - 1) return .{ .val = val }; 210 + }, 211 + ':', ';' => return .{ 212 + .val = val, 213 + .is_empty = self.idx == start, 214 + .has_sub = b == ':', 215 + }, 216 + else => return null, 217 + } 218 + } 219 + return null; 220 + } 221 + }; 222 + }; 223 + } 224 + 225 + pub fn sgr(self: *Screen, seq: []const u8) void { 226 + if (seq.len == 0) { 227 + self.cursor.style = .{}; 228 + return; 229 + } 230 + switch (seq[0]) { 231 + 0x30...0x39 => {}, 232 + else => return, // TODO: handle private indicator sequences 233 + } 234 + 235 + var iter: Parameter(u8).Iterator = .{ .bytes = seq }; 236 + while (iter.next()) |ps| { 237 + switch (ps.val) { 238 + 0 => self.cursor.style = .{}, 239 + 1 => self.cursor.style.bold = true, 240 + 2 => self.cursor.style.dim = true, 241 + 3 => self.cursor.style.italic = true, 242 + 4 => { 243 + const kind: vaxis.Style.Underline = if (ps.has_sub) blk: { 244 + const ul = iter.next() orelse break :blk .single; 245 + break :blk @enumFromInt(ul.val); 246 + } else .single; 247 + self.cursor.style.ul_style = kind; 248 + }, 249 + 5 => self.cursor.style.blink = true, 250 + 7 => self.cursor.style.reverse = true, 251 + 8 => self.cursor.style.invisible = true, 252 + 9 => self.cursor.style.strikethrough = true, 253 + 21 => self.cursor.style.ul_style = .double, 254 + 22 => { 255 + self.cursor.style.bold = false; 256 + self.cursor.style.dim = false; 257 + }, 258 + 23 => self.cursor.style.italic = false, 259 + 24 => self.cursor.style.ul_style = .off, 260 + 25 => self.cursor.style.blink = false, 261 + 27 => self.cursor.style.reverse = false, 262 + 28 => self.cursor.style.invisible = false, 263 + 29 => self.cursor.style.strikethrough = false, 264 + 30...37 => self.cursor.style.fg = .{ .index = ps.val - 30 }, 265 + 38 => { 266 + // must have another parameter 267 + const kind = iter.next() orelse return; 268 + switch (kind.val) { 269 + 2 => { // rgb 270 + const r = r: { 271 + // First param can be empty 272 + var ps_r = iter.next() orelse return; 273 + while (ps_r.is_empty) { 274 + ps_r = iter.next() orelse return; 275 + } 276 + break :r ps_r.val; 277 + }; 278 + const g = g: { 279 + const ps_g = iter.next() orelse return; 280 + break :g ps_g.val; 281 + }; 282 + const b = b: { 283 + const ps_b = iter.next() orelse return; 284 + break :b ps_b.val; 285 + }; 286 + self.cursor.style.fg = .{ .rgb = .{ r, g, b } }; 287 + }, 288 + 5 => { 289 + const idx = iter.next() orelse return; 290 + self.cursor.style.fg = .{ .index = idx.val }; 291 + }, // index 292 + else => return, 293 + } 294 + }, 295 + 39 => self.cursor.style.fg = .default, 296 + 40...47 => self.cursor.style.bg = .{ .index = ps.val - 40 }, 297 + 48 => { 298 + // must have another parameter 299 + const kind = iter.next() orelse return; 300 + switch (kind.val) { 301 + 2 => { // rgb 302 + const r = r: { 303 + // First param can be empty 304 + var ps_r = iter.next() orelse return; 305 + while (ps_r.is_empty) { 306 + ps_r = iter.next() orelse return; 307 + } 308 + break :r ps_r.val; 309 + }; 310 + const g = g: { 311 + const ps_g = iter.next() orelse return; 312 + break :g ps_g.val; 313 + }; 314 + const b = b: { 315 + const ps_b = iter.next() orelse return; 316 + break :b ps_b.val; 317 + }; 318 + self.cursor.style.bg = .{ .rgb = .{ r, g, b } }; 319 + }, 320 + 5 => { // index 321 + const idx = iter.next() orelse return; 322 + self.cursor.style.bg = .{ .index = idx.val }; 323 + }, 324 + else => return, 325 + } 326 + }, 327 + 49 => self.cursor.style.bg = .default, 328 + 90...97 => self.cursor.style.fg = .{ .index = ps.val - 90 + 8 }, 329 + 100...107 => self.cursor.style.bg = .{ .index = ps.val - 100 + 8 }, 330 + else => continue, 331 + } 332 + } 333 + }
+232
src/widgets/terminal/Terminal.zig
··· 1 + //! A virtual terminal widget 2 + const Terminal = @This(); 3 + 4 + const std = @import("std"); 5 + const builtin = @import("builtin"); 6 + pub const Command = @import("Command.zig"); 7 + const Parser = @import("Parser.zig"); 8 + const Pty = @import("Pty.zig"); 9 + const vaxis = @import("../../main.zig"); 10 + const Winsize = vaxis.Winsize; 11 + const Screen = @import("Screen.zig"); 12 + const DisplayWidth = @import("DisplayWidth"); 13 + 14 + const grapheme = @import("grapheme"); 15 + 16 + const posix = std.posix; 17 + 18 + const log = std.log.scoped(.terminal); 19 + 20 + pub const Options = struct { 21 + scrollback_size: usize = 500, 22 + winsize: Winsize = .{ .rows = 24, .cols = 80, .x_pixel = 0, .y_pixel = 0 }, 23 + }; 24 + 25 + allocator: std.mem.Allocator, 26 + scrollback_size: usize, 27 + 28 + pty: Pty, 29 + cmd: Command, 30 + thread: ?std.Thread = null, 31 + 32 + /// the screen we draw from 33 + front_screen: Screen, 34 + front_mutex: std.Thread.Mutex = .{}, 35 + 36 + /// the back screens 37 + back_screen: *Screen = undefined, 38 + back_screen_pri: Screen, 39 + back_screen_alt: Screen, 40 + // only applies to primary screen 41 + scroll_offset: usize = 0, 42 + back_mutex: std.Thread.Mutex = .{}, 43 + 44 + unicode: *const vaxis.Unicode, 45 + should_quit: bool = false, 46 + 47 + /// initialize a Terminal. This sets the size of the underlying pty and allocates the sizes of the 48 + /// screen 49 + pub fn init( 50 + allocator: std.mem.Allocator, 51 + argv: []const []const u8, 52 + env: *const std.process.EnvMap, 53 + unicode: *const vaxis.Unicode, 54 + opts: Options, 55 + ) !Terminal { 56 + const pty = try Pty.init(); 57 + try pty.setSize(opts.winsize); 58 + const cmd: Command = .{ 59 + .argv = argv, 60 + .env_map = env, 61 + .pty = pty, 62 + }; 63 + return .{ 64 + .allocator = allocator, 65 + .pty = pty, 66 + .cmd = cmd, 67 + .scrollback_size = opts.scrollback_size, 68 + .front_screen = try Screen.init(allocator, opts.winsize.cols, opts.winsize.rows), 69 + .back_screen_pri = try Screen.init(allocator, opts.winsize.cols, opts.winsize.rows + opts.scrollback_size), 70 + .back_screen_alt = try Screen.init(allocator, opts.winsize.cols, opts.winsize.rows), 71 + .unicode = unicode, 72 + }; 73 + } 74 + 75 + /// release all resources of the Terminal 76 + pub fn deinit(self: *Terminal) void { 77 + self.should_quit = true; 78 + self.cmd.kill(); 79 + if (self.thread) |thread| { 80 + thread.join(); 81 + self.thread = null; 82 + } 83 + self.pty.deinit(); 84 + self.front_screen.deinit(self.allocator); 85 + self.back_screen_pri.deinit(self.allocator); 86 + self.back_screen_alt.deinit(self.allocator); 87 + } 88 + 89 + pub fn spawn(self: *Terminal) !void { 90 + if (self.thread != null) return; 91 + self.back_screen = &self.back_screen_pri; 92 + 93 + try self.cmd.spawn(self.allocator); 94 + self.thread = try std.Thread.spawn(.{}, Terminal.run, .{self}); 95 + } 96 + 97 + /// resize the screen. Locks access to the back screen. Should only be called from the main thread 98 + pub fn resize(self: *Terminal, ws: Winsize) !void { 99 + // don't deinit with no size change 100 + if (ws.cols == self.front_screen.width and 101 + ws.rows == self.front_screen.height) 102 + { 103 + std.log.debug("resize requested but no change", .{}); 104 + return; 105 + } 106 + 107 + self.back_mutex.lock(); 108 + defer self.back_mutex.unlock(); 109 + 110 + self.front_screen.deinit(self.allocator); 111 + self.front_screen = try Screen.init(self.allocator, ws.cols, ws.rows); 112 + 113 + self.back_screen_pri.deinit(self.allocator); 114 + self.back_screen_alt.deinit(self.allocator); 115 + self.back_screen_pri = try Screen.init(self.allocator, ws.cols, ws.rows + self.scrollback_size); 116 + self.back_screen_alt = try Screen.init(self.allocator, ws.cols, ws.rows); 117 + 118 + try self.pty.setSize(ws); 119 + } 120 + 121 + pub fn draw(self: *Terminal, win: vaxis.Window) !void { 122 + // TODO: check sync 123 + if (self.back_mutex.tryLock()) { 124 + defer self.back_mutex.unlock(); 125 + try self.back_screen.copyTo(&self.front_screen); 126 + } 127 + 128 + var row: usize = 0; 129 + while (row < self.front_screen.height) : (row += 1) { 130 + var col: usize = 0; 131 + while (col < self.front_screen.width) { 132 + const cell = self.front_screen.readCell(col, row) orelse continue; 133 + win.writeCell(col, row, cell); 134 + col += @max(cell.char.width, 1); 135 + } 136 + } 137 + } 138 + 139 + fn opaqueRead(ptr: *const anyopaque, buf: []u8) !usize { 140 + const self: *const Terminal = @ptrCast(@alignCast(ptr)); 141 + return posix.read(self.pty.pty, buf); 142 + } 143 + 144 + fn anyReader(self: *const Terminal) std.io.AnyReader { 145 + return .{ 146 + .context = self, 147 + .readFn = Terminal.opaqueRead, 148 + }; 149 + } 150 + 151 + /// process the output from the command on the pty 152 + fn run(self: *Terminal) !void { 153 + // ridiculous buffer size so we never have to handle incomplete reads 154 + var parser: Parser = .{ 155 + .buf = try std.ArrayList(u8).initCapacity(self.allocator, 128), 156 + }; 157 + defer parser.buf.deinit(); 158 + 159 + // Use our anyReader to make a buffered reader, then get *that* any reader 160 + var buffered = std.io.bufferedReader(self.anyReader()); 161 + const reader = buffered.reader().any(); 162 + 163 + while (!self.should_quit) { 164 + const event = try parser.parseReader(reader); 165 + self.back_mutex.lock(); 166 + defer self.back_mutex.unlock(); 167 + switch (event) { 168 + .print => |str| { 169 + std.log.err("print: {s}", .{str}); 170 + var iter = grapheme.Iterator.init(str, &self.unicode.grapheme_data); 171 + while (iter.next()) |g| { 172 + const bytes = g.bytes(str); 173 + const w = try vaxis.gwidth.gwidth(bytes, .unicode, &self.unicode.width_data); 174 + self.back_screen.print(bytes, @truncate(w)); 175 + } 176 + }, 177 + .c0 => |b| try self.handleC0(b), 178 + .csi => |seq| { 179 + const final = seq[seq.len - 1]; 180 + switch (final) { 181 + 'B' => { // CUD 182 + switch (seq.len) { 183 + 0 => unreachable, 184 + 1 => self.back_screen.cursor.row += 1, 185 + else => { 186 + const delta = parseParam(u16, seq[2 .. seq.len - 1], 1) orelse 1; 187 + self.back_screen.cursor.row = @min(self.back_screen.height - 1, self.back_screen.cursor.row + delta); 188 + }, 189 + } 190 + }, 191 + 'H' => { // CUP 192 + const delim = std.mem.indexOfScalar(u8, seq, ';') orelse { 193 + switch (seq.len) { 194 + 0 => unreachable, 195 + 1 => { 196 + self.back_screen.cursor.row = 0; 197 + self.back_screen.cursor.col = 0; 198 + }, 199 + else => { 200 + const row = parseParam(u16, seq[0 .. seq.len - 1], 1) orelse 1; 201 + self.back_screen.cursor.row = row - 1; 202 + }, 203 + } 204 + continue; 205 + }; 206 + const row = parseParam(u16, seq[0..delim], 1) orelse 1; 207 + const col = parseParam(u16, seq[delim + 1 .. seq.len - 1], 1) orelse 1; 208 + self.back_screen.cursor.col = col - 1; 209 + self.back_screen.cursor.row = row - 1; 210 + }, 211 + 'm' => self.back_screen.sgr(seq[0 .. seq.len - 1]), 212 + else => {}, 213 + } 214 + }, 215 + else => {}, 216 + } 217 + } 218 + } 219 + 220 + inline fn handleC0(self: *Terminal, b: u8) !void { 221 + switch (b) { 222 + 0x0a, 0x0b, 0x0c => try self.back_screen.index(), // line feed 223 + 0x0d => {}, // carriage return 224 + else => log.warn("unhandled C0: 0x{x}", .{b}), 225 + } 226 + } 227 + 228 + /// Parse a param buffer, returning a default value if the param was empty 229 + inline fn parseParam(comptime T: type, buf: []const u8, default: ?T) ?T { 230 + if (buf.len == 0) return default; 231 + return std.fmt.parseUnsigned(T, buf, 10) catch return null; 232 + }