widgets(terminal): begin terminal widget

This commit is contained in:
Tim Culverhouse 2024-06-06 16:41:32 -05:00
parent f76b573a0b
commit f21559cc51
10 changed files with 988 additions and 1 deletions

View file

@ -64,6 +64,7 @@ pub fn build(b: *std.Build) void {
table, table,
text_input, text_input,
vaxis, vaxis,
vt,
xev, xev,
}; };
const example_option = b.option(Example, "example", "Example to run (default: text_input)") orelse .text_input; const example_option = b.option(Example, "example", "Example to run (default: text_input)") orelse .text_input;

136
examples/vt.zig Normal file
View file

@ -0,0 +1,136 @@
const std = @import("std");
const vaxis = @import("vaxis");
const Cell = vaxis.Cell;
const Event = union(enum) {
key_press: vaxis.Key,
winsize: vaxis.Winsize,
};
pub const panic = vaxis.panic_handler;
pub fn main() !void {
var gpa = std.heap.GeneralPurposeAllocator(.{}){};
defer {
const deinit_status = gpa.deinit();
//fail test; can't try in defer as defer is executed after we return
if (deinit_status == .leak) {
std.log.err("memory leak", .{});
}
}
const alloc = gpa.allocator();
var tty = try vaxis.Tty.init();
var vx = try vaxis.init(alloc, .{});
defer vx.deinit(alloc, tty.anyWriter());
var loop: vaxis.Loop(Event) = .{ .tty = &tty, .vaxis = &vx };
try loop.init();
try loop.start();
defer loop.stop();
try vx.enterAltScreen(tty.anyWriter());
try vx.queryTerminal(tty.anyWriter(), 1 * std.time.ns_per_s);
var env = try std.process.getEnvMap(alloc);
defer env.deinit();
const vt_opts: vaxis.widgets.Terminal.Options = .{
.winsize = .{
.rows = 24,
.cols = 100,
.x_pixel = 0,
.y_pixel = 0,
},
.scrollback_size = 0,
};
const argv1 = [_][]const u8{"senpai"};
const argv2 = [_][]const u8{"nvim"};
const argv3 = [_][]const u8{"senpai"};
// const argv = [_][]const u8{"senpai"};
// const argv = [_][]const u8{"comlink"};
var vt1 = try vaxis.widgets.Terminal.init(
alloc,
&argv1,
&env,
&vx.unicode,
vt_opts,
);
defer vt1.deinit();
try vt1.spawn();
var vt2 = try vaxis.widgets.Terminal.init(
alloc,
&argv2,
&env,
&vx.unicode,
vt_opts,
);
defer vt2.deinit();
try vt2.spawn();
var vt3 = try vaxis.widgets.Terminal.init(
alloc,
&argv3,
&env,
&vx.unicode,
vt_opts,
);
defer vt3.deinit();
try vt3.spawn();
while (true) {
std.time.sleep(8 * std.time.ns_per_ms);
while (loop.tryEvent()) |event| {
switch (event) {
.key_press => |key| if (key.matches('c', .{ .ctrl = true })) return,
.winsize => |ws| {
try vx.resize(alloc, tty.anyWriter(), ws);
},
}
}
const win = vx.window();
win.clear();
const left = win.child(.{
.width = .{ .limit = win.width / 2 },
.border = .{
.where = .right,
},
});
const right_top = win.child(.{
.x_off = left.width + 1,
.height = .{ .limit = win.height / 2 },
.border = .{
.where = .bottom,
},
});
const right_bot = win.child(.{
.x_off = left.width + 1,
.y_off = right_top.height + 1,
});
try vt1.resize(.{
.rows = left.height,
.cols = left.width,
.x_pixel = 0,
.y_pixel = 0,
});
try vt2.resize(.{
.rows = right_top.height,
.cols = right_bot.width,
.x_pixel = 0,
.y_pixel = 0,
});
try vt3.resize(.{
.rows = right_bot.height,
.cols = right_bot.width,
.x_pixel = 0,
.y_pixel = 0,
});
try vt1.draw(left);
try vt2.draw(right_top);
try vt3.draw(right_bot);
try vx.render(tty.anyWriter());
}
}

View file

@ -290,7 +290,6 @@ inline fn parseOsc(input: []const u8, paste_allocator: ?std.mem.Allocator) !Resu
inline fn parseCsi(input: []const u8, text_buf: []u8) Result { inline fn parseCsi(input: []const u8, text_buf: []u8) Result {
// We start iterating at index 2 to get past te '[' // We start iterating at index 2 to get past te '['
const sequence = for (input[2..], 2..) |b, i| { const sequence = for (input[2..], 2..) |b, i| {
if (i == 2 and b == '?') continue;
switch (b) { switch (b) {
0x40...0xFF => break input[0 .. i + 1], 0x40...0xFF => break input[0 .. i + 1],
else => continue, else => continue,

View file

@ -26,6 +26,7 @@ pub const ctlseqs = @import("ctlseqs.zig");
pub const GraphemeCache = @import("GraphemeCache.zig"); pub const GraphemeCache = @import("GraphemeCache.zig");
pub const grapheme = @import("grapheme"); pub const grapheme = @import("grapheme");
pub const Event = @import("event.zig").Event; pub const Event = @import("event.zig").Event;
pub const Unicode = @import("Unicode.zig");
/// The target TTY implementation /// The target TTY implementation
pub const Tty = switch (builtin.os.tag) { pub const Tty = switch (builtin.os.tag) {

View file

@ -10,6 +10,7 @@ pub const ScrollView = @import("widgets/ScrollView.zig");
pub const LineNumbers = @import("widgets/LineNumbers.zig"); pub const LineNumbers = @import("widgets/LineNumbers.zig");
pub const TextView = @import("widgets/TextView.zig"); pub const TextView = @import("widgets/TextView.zig");
pub const CodeView = @import("widgets/CodeView.zig"); pub const CodeView = @import("widgets/CodeView.zig");
pub const Terminal = @import("widgets/terminal/Terminal.zig");
// Widgets with dependencies // Widgets with dependencies

View file

@ -0,0 +1,84 @@
const Command = @This();
const std = @import("std");
const builtin = @import("builtin");
const Pty = @import("Pty.zig");
const posix = std.posix;
argv: []const []const u8,
// Set after spawn()
pid: ?std.posix.pid_t = null,
env_map: *const std.process.EnvMap,
pty: Pty,
pub fn spawn(self: *Command, allocator: std.mem.Allocator) !void {
var arena_allocator = std.heap.ArenaAllocator.init(allocator);
defer arena_allocator.deinit();
const arena = arena_allocator.allocator();
const argv_buf = try arena.allocSentinel(?[*:0]const u8, self.argv.len, null);
for (self.argv, 0..) |arg, i| argv_buf[i] = (try arena.dupeZ(u8, arg)).ptr;
const envp = try createEnvironFromMap(arena, self.env_map);
const pid = try std.posix.fork();
if (pid == 0) {
// we are the child
_ = std.os.linux.setsid();
// set the controlling terminal
var u: c_uint = std.posix.STDIN_FILENO;
if (posix.system.ioctl(self.pty.tty, posix.T.IOCSCTTY, @intFromPtr(&u)) != 0) return error.IoctlError;
// set up io
try posix.dup2(self.pty.tty, std.posix.STDIN_FILENO);
try posix.dup2(self.pty.tty, std.posix.STDOUT_FILENO);
try posix.dup2(self.pty.tty, std.posix.STDERR_FILENO);
// posix.close(self.pty.tty);
// if (self.pty.pty > 2) posix.close(self.pty.pty);
// exec
const err = std.posix.execvpeZ(argv_buf.ptr[0].?, argv_buf.ptr, envp);
_ = err catch {};
}
// we are the parent
self.pid = @intCast(pid);
return;
}
pub fn kill(self: *Command) void {
if (self.pid) |pid| {
std.posix.kill(pid, std.posix.SIG.TERM) catch {};
self.pid = null;
}
}
/// Creates a null-deliminated environment variable block in the format expected by POSIX, from a
/// hash map plus options.
fn createEnvironFromMap(
arena: std.mem.Allocator,
map: *const std.process.EnvMap,
) ![:null]?[*:0]u8 {
const envp_count: usize = map.count();
const envp_buf = try arena.allocSentinel(?[*:0]u8, envp_count, null);
var i: usize = 0;
{
var it = map.iterator();
while (it.next()) |pair| {
envp_buf[i] = try std.fmt.allocPrintZ(arena, "{s}={s}", .{ pair.key_ptr.*, pair.value_ptr.* });
i += 1;
}
}
std.debug.assert(i == envp_count);
return envp_buf;
}

View file

@ -0,0 +1,141 @@
//! An ANSI VT Parser
const Parser = @This();
const std = @import("std");
const Reader = std.io.AnyReader;
/// A terminal event
const Event = union(enum) {
print: []const u8,
c0: u8,
escape: []const u8,
ss2: u8,
ss3: u8,
csi: []const u8,
osc: []const u8,
apc: []const u8,
};
buf: std.ArrayList(u8),
/// a leftover byte from a ground event
pending_byte: ?u8 = null,
pub fn parseReader(self: *Parser, reader: Reader) !Event {
self.buf.clearRetainingCapacity();
while (true) {
const b = if (self.pending_byte) |p| p else try reader.readByte();
self.pending_byte = null;
switch (b) {
// Escape sequence
0x1b => {
const next = try reader.readByte();
switch (next) {
0x4E => return .{ .ss2 = try reader.readByte() },
0x4F => return .{ .ss3 = try reader.readByte() },
0x50 => try skipUntilST(reader), // DCS
0x58 => try skipUntilST(reader), // SOS
0x5B => return self.parseCsi(reader), // CSI
0x5D => return self.parseOsc(reader), // OSC
0x5E => try skipUntilST(reader), // PM
0x5F => return self.parseApc(reader), // APC
0x20...0x2F => {
try self.buf.append(next);
return self.parseEscape(reader); // ESC
},
else => {
try self.buf.append(next);
return .{ .escape = self.buf.items };
},
}
},
// C0 control
0x00...0x1a,
0x1c...0x1f,
=> return .{ .c0 = b },
else => {
try self.buf.append(b);
return self.parseGround(reader);
},
}
}
}
inline fn parseGround(self: *Parser, reader: Reader) !Event {
while (true) {
const b = try reader.readByte();
switch (b) {
0x00...0x1f => {
self.pending_byte = b;
return .{ .print = self.buf.items };
},
else => try self.buf.append(b),
}
}
}
/// parse until b >= 0x30
inline fn parseEscape(self: *Parser, reader: Reader) !Event {
while (true) {
const b = try reader.readByte();
switch (b) {
0x20...0x2F => continue,
else => return .{ .escape = self.buf.items },
}
}
}
inline fn parseApc(self: *Parser, reader: Reader) !Event {
while (true) {
const b = try reader.readByte();
switch (b) {
0x00...0x17,
0x19,
0x1c...0x1f,
=> continue,
0x1b => {
try reader.skipBytes(1, .{ .buf_size = 1 });
return .{ .apc = self.buf.items };
},
else => try self.buf.append(b),
}
}
}
/// Skips sequences until we see an ST (String Terminator, ESC \)
inline fn skipUntilST(reader: Reader) !void {
try reader.skipUntilDelimiterOrEof('\x1b');
try reader.skipBytes(1, .{ .buf_size = 1 });
}
/// Parses an OSC sequence
inline fn parseOsc(self: *Parser, reader: Reader) !Event {
while (true) {
const b = try reader.readByte();
switch (b) {
0x00...0x06,
0x08...0x17,
0x19,
0x1c...0x1f,
=> continue,
0x1b => {
try reader.skipBytes(1, .{ .buf_size = 1 });
return .{ .osc = self.buf.items };
},
0x07 => return .{ .osc = self.buf.items },
else => try self.buf.append(b),
}
}
}
inline fn parseCsi(self: *Parser, reader: Reader) !Event {
while (true) {
const b = try reader.readByte();
try self.buf.append(b);
switch (b) {
// Really we should execute C0 controls, but we just ignore them
0x40...0xFF => return .{ .csi = self.buf.items },
else => continue,
}
}
}

View file

@ -0,0 +1,59 @@
//! A PTY pair
const Pty = @This();
const std = @import("std");
const builtin = @import("builtin");
const Winsize = @import("../../main.zig").Winsize;
const posix = std.posix;
pty: posix.fd_t,
tty: posix.fd_t,
/// opens a new tty/pty pair
pub fn init() !Pty {
switch (builtin.os.tag) {
.linux => return openPtyLinux(),
else => @compileError("unsupported os"),
}
}
/// closes the tty and pty
pub fn deinit(self: Pty) void {
posix.close(self.pty);
posix.close(self.tty);
}
/// sets the size of the pty
pub fn setSize(self: Pty, ws: Winsize) !void {
const _ws: posix.winsize = .{
.ws_row = @truncate(ws.rows),
.ws_col = @truncate(ws.cols),
.ws_xpixel = @truncate(ws.x_pixel),
.ws_ypixel = @truncate(ws.y_pixel),
};
if (posix.system.ioctl(self.pty, posix.T.IOCSWINSZ, @intFromPtr(&_ws)) != 0)
return error.SetWinsizeError;
}
fn openPtyLinux() !Pty {
const p = try posix.open("/dev/ptmx", .{ .ACCMODE = .RDWR, .NOCTTY = true }, 0);
errdefer posix.close(p);
// unlockpt
var n: c_uint = 0;
if (posix.system.ioctl(p, posix.T.IOCSPTLCK, @intFromPtr(&n)) != 0) return error.IoctlError;
// ptsname
if (posix.system.ioctl(p, posix.T.IOCGPTN, @intFromPtr(&n)) != 0) return error.IoctlError;
var buf: [16]u8 = undefined;
const sname = try std.fmt.bufPrint(&buf, "/dev/pts/{d}", .{n});
std.log.err("pts: {s}", .{sname});
const t = try posix.open(sname, .{ .ACCMODE = .RDWR, .NOCTTY = true }, 0);
return .{
.pty = p,
.tty = t,
};
}

View file

@ -0,0 +1,333 @@
const std = @import("std");
const assert = std.debug.assert;
const vaxis = @import("../../main.zig");
const log = std.log.scoped(.terminal);
const Screen = @This();
pub const Cell = struct {
char: std.ArrayList(u8) = undefined,
style: vaxis.Style = .{},
uri: std.ArrayList(u8) = undefined,
uri_id: std.ArrayList(u8) = undefined,
width: u8 = 0,
wrapped: bool = false,
dirty: bool = true,
};
pub const Cursor = struct {
style: vaxis.Style = .{},
uri: std.ArrayList(u8) = undefined,
uri_id: std.ArrayList(u8) = undefined,
col: usize = 0,
row: usize = 0,
pending_wrap: bool = false,
shape: vaxis.Cell.CursorShape = .default,
pub fn isOutsideScrollingRegion(self: Cursor, sr: ScrollingRegion) bool {
return self.row < sr.top or
self.row > sr.bottom or
self.col < sr.left or
self.col > sr.right;
}
pub fn isInsideScrollingRegion(self: Cursor, sr: ScrollingRegion) bool {
return !self.isOutsideScrollingRegion(sr);
}
};
pub const ScrollingRegion = struct {
top: usize,
bottom: usize,
left: usize,
right: usize,
pub fn contains(self: ScrollingRegion, col: usize, row: usize) bool {
return col >= self.left and
col <= self.right and
row >= self.top and
row <= self.bottom;
}
};
width: usize = 0,
height: usize = 0,
scrolling_region: ScrollingRegion,
buf: []Cell = undefined,
cursor: Cursor = .{},
/// sets each cell to the default cell
pub fn init(alloc: std.mem.Allocator, w: usize, h: usize) !Screen {
var screen = Screen{
.buf = try alloc.alloc(Cell, w * h),
.scrolling_region = .{
.top = 0,
.bottom = h - 1,
.left = 0,
.right = w - 1,
},
.width = w,
.height = h,
};
for (screen.buf, 0..) |_, i| {
screen.buf[i] = .{
.char = try std.ArrayList(u8).initCapacity(alloc, 1),
.uri = std.ArrayList(u8).init(alloc),
.uri_id = std.ArrayList(u8).init(alloc),
};
try screen.buf[i].char.append(' ');
}
return screen;
}
pub fn deinit(self: *Screen, alloc: std.mem.Allocator) void {
for (self.buf, 0..) |_, i| {
self.buf[i].char.deinit();
self.buf[i].uri.deinit();
self.buf[i].uri_id.deinit();
}
alloc.free(self.buf);
}
/// copies the visible area to the destination screen
pub fn copyTo(self: *Screen, dst: *Screen) !void {
for (self.buf, 0..) |cell, i| {
if (!cell.dirty) continue;
self.buf[i].dirty = false;
const grapheme = cell.char.items;
dst.buf[i].char.clearRetainingCapacity();
try dst.buf[i].char.appendSlice(grapheme);
dst.buf[i].width = cell.width;
dst.buf[i].style = cell.style;
}
}
pub fn readCell(self: *Screen, col: usize, row: usize) ?vaxis.Cell {
if (self.width < col) {
// column out of bounds
return null;
}
if (self.height < row) {
// height out of bounds
return null;
}
const i = (row * self.width) + col;
assert(i < self.buf.len);
const cell = self.buf[i];
return .{
.char = .{ .grapheme = cell.char.items, .width = cell.width },
.style = cell.style,
};
}
/// writes a cell to a location. 0 indexed
pub fn print(
self: *Screen,
grapheme: []const u8,
width: u8,
) void {
// FIXME: wrapping
// if (self.cursor.col + width >= self.width) {
// self.cursor.col = 0;
// self.cursor.row += 1;
// }
if (self.cursor.col >= self.width) return;
if (self.cursor.row >= self.height) return;
const col = self.cursor.col;
const row = self.cursor.row;
const i = (row * self.width) + col;
assert(i < self.buf.len);
self.buf[i].char.clearRetainingCapacity();
self.buf[i].char.appendSlice(grapheme) catch {
log.warn("couldn't write grapheme", .{});
};
self.buf[i].uri.clearRetainingCapacity();
self.buf[i].uri.appendSlice(self.cursor.uri.items) catch {
log.warn("couldn't write uri", .{});
};
self.buf[i].uri_id.clearRetainingCapacity();
self.buf[i].uri_id.appendSlice(self.cursor.uri_id.items) catch {
log.warn("couldn't write uri_id", .{});
};
self.buf[i].style = self.cursor.style;
self.buf[i].width = width;
self.buf[i].dirty = true;
self.cursor.col += width;
// FIXME: when do we set default in this function??
// self.buf[i].default = false;
}
/// IND
pub fn index(self: *Screen) !void {
self.cursor.pending_wrap = false;
if (self.cursor.isOutsideScrollingRegion(self.scrolling_region)) {
// Outside, we just move cursor down one
self.cursor.row = @min(self.height - 1, self.cursor.row + 1);
return;
}
// We are inside the scrolling region
if (self.cursor.row == self.scrolling_region.bottom) {
// Inside scrolling region *and* at bottom of screen, we scroll contents up and insert a
// blank line
// TODO: scrollback if scrolling region is entire visible screen
@panic("TODO");
}
self.cursor.row += 1;
}
fn Parameter(T: type) type {
return struct {
const Self = @This();
val: T,
// indicates the next parameter is a sub-parameter
has_sub: bool = false,
is_empty: bool = false,
const Iterator = struct {
bytes: []const u8,
idx: usize = 0,
fn next(self: *Iterator) ?Self {
const start = self.idx;
var val: T = 0;
while (self.idx < self.bytes.len) {
defer self.idx += 1; // defer so we trigger on return as well
const b = self.bytes[self.idx];
switch (b) {
0x30...0x39 => {
val = (val * 10) + (b - 0x30);
if (self.idx == self.bytes.len - 1) return .{ .val = val };
},
':', ';' => return .{
.val = val,
.is_empty = self.idx == start,
.has_sub = b == ':',
},
else => return null,
}
}
return null;
}
};
};
}
pub fn sgr(self: *Screen, seq: []const u8) void {
if (seq.len == 0) {
self.cursor.style = .{};
return;
}
switch (seq[0]) {
0x30...0x39 => {},
else => return, // TODO: handle private indicator sequences
}
var iter: Parameter(u8).Iterator = .{ .bytes = seq };
while (iter.next()) |ps| {
switch (ps.val) {
0 => self.cursor.style = .{},
1 => self.cursor.style.bold = true,
2 => self.cursor.style.dim = true,
3 => self.cursor.style.italic = true,
4 => {
const kind: vaxis.Style.Underline = if (ps.has_sub) blk: {
const ul = iter.next() orelse break :blk .single;
break :blk @enumFromInt(ul.val);
} else .single;
self.cursor.style.ul_style = kind;
},
5 => self.cursor.style.blink = true,
7 => self.cursor.style.reverse = true,
8 => self.cursor.style.invisible = true,
9 => self.cursor.style.strikethrough = true,
21 => self.cursor.style.ul_style = .double,
22 => {
self.cursor.style.bold = false;
self.cursor.style.dim = false;
},
23 => self.cursor.style.italic = false,
24 => self.cursor.style.ul_style = .off,
25 => self.cursor.style.blink = false,
27 => self.cursor.style.reverse = false,
28 => self.cursor.style.invisible = false,
29 => self.cursor.style.strikethrough = false,
30...37 => self.cursor.style.fg = .{ .index = ps.val - 30 },
38 => {
// must have another parameter
const kind = iter.next() orelse return;
switch (kind.val) {
2 => { // rgb
const r = r: {
// First param can be empty
var ps_r = iter.next() orelse return;
while (ps_r.is_empty) {
ps_r = iter.next() orelse return;
}
break :r ps_r.val;
};
const g = g: {
const ps_g = iter.next() orelse return;
break :g ps_g.val;
};
const b = b: {
const ps_b = iter.next() orelse return;
break :b ps_b.val;
};
self.cursor.style.fg = .{ .rgb = .{ r, g, b } };
},
5 => {
const idx = iter.next() orelse return;
self.cursor.style.fg = .{ .index = idx.val };
}, // index
else => return,
}
},
39 => self.cursor.style.fg = .default,
40...47 => self.cursor.style.bg = .{ .index = ps.val - 40 },
48 => {
// must have another parameter
const kind = iter.next() orelse return;
switch (kind.val) {
2 => { // rgb
const r = r: {
// First param can be empty
var ps_r = iter.next() orelse return;
while (ps_r.is_empty) {
ps_r = iter.next() orelse return;
}
break :r ps_r.val;
};
const g = g: {
const ps_g = iter.next() orelse return;
break :g ps_g.val;
};
const b = b: {
const ps_b = iter.next() orelse return;
break :b ps_b.val;
};
self.cursor.style.bg = .{ .rgb = .{ r, g, b } };
},
5 => { // index
const idx = iter.next() orelse return;
self.cursor.style.bg = .{ .index = idx.val };
},
else => return,
}
},
49 => self.cursor.style.bg = .default,
90...97 => self.cursor.style.fg = .{ .index = ps.val - 90 + 8 },
100...107 => self.cursor.style.bg = .{ .index = ps.val - 100 + 8 },
else => continue,
}
}
}

View file

@ -0,0 +1,232 @@
//! A virtual terminal widget
const Terminal = @This();
const std = @import("std");
const builtin = @import("builtin");
pub const Command = @import("Command.zig");
const Parser = @import("Parser.zig");
const Pty = @import("Pty.zig");
const vaxis = @import("../../main.zig");
const Winsize = vaxis.Winsize;
const Screen = @import("Screen.zig");
const DisplayWidth = @import("DisplayWidth");
const grapheme = @import("grapheme");
const posix = std.posix;
const log = std.log.scoped(.terminal);
pub const Options = struct {
scrollback_size: usize = 500,
winsize: Winsize = .{ .rows = 24, .cols = 80, .x_pixel = 0, .y_pixel = 0 },
};
allocator: std.mem.Allocator,
scrollback_size: usize,
pty: Pty,
cmd: Command,
thread: ?std.Thread = null,
/// the screen we draw from
front_screen: Screen,
front_mutex: std.Thread.Mutex = .{},
/// the back screens
back_screen: *Screen = undefined,
back_screen_pri: Screen,
back_screen_alt: Screen,
// only applies to primary screen
scroll_offset: usize = 0,
back_mutex: std.Thread.Mutex = .{},
unicode: *const vaxis.Unicode,
should_quit: bool = false,
/// initialize a Terminal. This sets the size of the underlying pty and allocates the sizes of the
/// screen
pub fn init(
allocator: std.mem.Allocator,
argv: []const []const u8,
env: *const std.process.EnvMap,
unicode: *const vaxis.Unicode,
opts: Options,
) !Terminal {
const pty = try Pty.init();
try pty.setSize(opts.winsize);
const cmd: Command = .{
.argv = argv,
.env_map = env,
.pty = pty,
};
return .{
.allocator = allocator,
.pty = pty,
.cmd = cmd,
.scrollback_size = opts.scrollback_size,
.front_screen = try Screen.init(allocator, opts.winsize.cols, opts.winsize.rows),
.back_screen_pri = try Screen.init(allocator, opts.winsize.cols, opts.winsize.rows + opts.scrollback_size),
.back_screen_alt = try Screen.init(allocator, opts.winsize.cols, opts.winsize.rows),
.unicode = unicode,
};
}
/// release all resources of the Terminal
pub fn deinit(self: *Terminal) void {
self.should_quit = true;
self.cmd.kill();
if (self.thread) |thread| {
thread.join();
self.thread = null;
}
self.pty.deinit();
self.front_screen.deinit(self.allocator);
self.back_screen_pri.deinit(self.allocator);
self.back_screen_alt.deinit(self.allocator);
}
pub fn spawn(self: *Terminal) !void {
if (self.thread != null) return;
self.back_screen = &self.back_screen_pri;
try self.cmd.spawn(self.allocator);
self.thread = try std.Thread.spawn(.{}, Terminal.run, .{self});
}
/// resize the screen. Locks access to the back screen. Should only be called from the main thread
pub fn resize(self: *Terminal, ws: Winsize) !void {
// don't deinit with no size change
if (ws.cols == self.front_screen.width and
ws.rows == self.front_screen.height)
{
std.log.debug("resize requested but no change", .{});
return;
}
self.back_mutex.lock();
defer self.back_mutex.unlock();
self.front_screen.deinit(self.allocator);
self.front_screen = try Screen.init(self.allocator, ws.cols, ws.rows);
self.back_screen_pri.deinit(self.allocator);
self.back_screen_alt.deinit(self.allocator);
self.back_screen_pri = try Screen.init(self.allocator, ws.cols, ws.rows + self.scrollback_size);
self.back_screen_alt = try Screen.init(self.allocator, ws.cols, ws.rows);
try self.pty.setSize(ws);
}
pub fn draw(self: *Terminal, win: vaxis.Window) !void {
// TODO: check sync
if (self.back_mutex.tryLock()) {
defer self.back_mutex.unlock();
try self.back_screen.copyTo(&self.front_screen);
}
var row: usize = 0;
while (row < self.front_screen.height) : (row += 1) {
var col: usize = 0;
while (col < self.front_screen.width) {
const cell = self.front_screen.readCell(col, row) orelse continue;
win.writeCell(col, row, cell);
col += @max(cell.char.width, 1);
}
}
}
fn opaqueRead(ptr: *const anyopaque, buf: []u8) !usize {
const self: *const Terminal = @ptrCast(@alignCast(ptr));
return posix.read(self.pty.pty, buf);
}
fn anyReader(self: *const Terminal) std.io.AnyReader {
return .{
.context = self,
.readFn = Terminal.opaqueRead,
};
}
/// process the output from the command on the pty
fn run(self: *Terminal) !void {
// ridiculous buffer size so we never have to handle incomplete reads
var parser: Parser = .{
.buf = try std.ArrayList(u8).initCapacity(self.allocator, 128),
};
defer parser.buf.deinit();
// Use our anyReader to make a buffered reader, then get *that* any reader
var buffered = std.io.bufferedReader(self.anyReader());
const reader = buffered.reader().any();
while (!self.should_quit) {
const event = try parser.parseReader(reader);
self.back_mutex.lock();
defer self.back_mutex.unlock();
switch (event) {
.print => |str| {
std.log.err("print: {s}", .{str});
var iter = grapheme.Iterator.init(str, &self.unicode.grapheme_data);
while (iter.next()) |g| {
const bytes = g.bytes(str);
const w = try vaxis.gwidth.gwidth(bytes, .unicode, &self.unicode.width_data);
self.back_screen.print(bytes, @truncate(w));
}
},
.c0 => |b| try self.handleC0(b),
.csi => |seq| {
const final = seq[seq.len - 1];
switch (final) {
'B' => { // CUD
switch (seq.len) {
0 => unreachable,
1 => self.back_screen.cursor.row += 1,
else => {
const delta = parseParam(u16, seq[2 .. seq.len - 1], 1) orelse 1;
self.back_screen.cursor.row = @min(self.back_screen.height - 1, self.back_screen.cursor.row + delta);
},
}
},
'H' => { // CUP
const delim = std.mem.indexOfScalar(u8, seq, ';') orelse {
switch (seq.len) {
0 => unreachable,
1 => {
self.back_screen.cursor.row = 0;
self.back_screen.cursor.col = 0;
},
else => {
const row = parseParam(u16, seq[0 .. seq.len - 1], 1) orelse 1;
self.back_screen.cursor.row = row - 1;
},
}
continue;
};
const row = parseParam(u16, seq[0..delim], 1) orelse 1;
const col = parseParam(u16, seq[delim + 1 .. seq.len - 1], 1) orelse 1;
self.back_screen.cursor.col = col - 1;
self.back_screen.cursor.row = row - 1;
},
'm' => self.back_screen.sgr(seq[0 .. seq.len - 1]),
else => {},
}
},
else => {},
}
}
}
inline fn handleC0(self: *Terminal, b: u8) !void {
switch (b) {
0x0a, 0x0b, 0x0c => try self.back_screen.index(), // line feed
0x0d => {}, // carriage return
else => log.warn("unhandled C0: 0x{x}", .{b}),
}
}
/// Parse a param buffer, returning a default value if the param was empty
inline fn parseParam(comptime T: type, buf: []const u8, default: ?T) ?T {
if (buf.len == 0) return default;
return std.fmt.parseUnsigned(T, buf, 10) catch return null;
}