widgets(terminal): begin terminal widget
This commit is contained in:
parent
f76b573a0b
commit
f21559cc51
10 changed files with 988 additions and 1 deletions
|
@ -64,6 +64,7 @@ pub fn build(b: *std.Build) void {
|
|||
table,
|
||||
text_input,
|
||||
vaxis,
|
||||
vt,
|
||||
xev,
|
||||
};
|
||||
const example_option = b.option(Example, "example", "Example to run (default: text_input)") orelse .text_input;
|
||||
|
|
136
examples/vt.zig
Normal file
136
examples/vt.zig
Normal 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());
|
||||
}
|
||||
}
|
|
@ -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 {
|
||||
// We start iterating at index 2 to get past te '['
|
||||
const sequence = for (input[2..], 2..) |b, i| {
|
||||
if (i == 2 and b == '?') continue;
|
||||
switch (b) {
|
||||
0x40...0xFF => break input[0 .. i + 1],
|
||||
else => continue,
|
||||
|
|
|
@ -26,6 +26,7 @@ pub const ctlseqs = @import("ctlseqs.zig");
|
|||
pub const GraphemeCache = @import("GraphemeCache.zig");
|
||||
pub const grapheme = @import("grapheme");
|
||||
pub const Event = @import("event.zig").Event;
|
||||
pub const Unicode = @import("Unicode.zig");
|
||||
|
||||
/// The target TTY implementation
|
||||
pub const Tty = switch (builtin.os.tag) {
|
||||
|
|
|
@ -10,6 +10,7 @@ pub const ScrollView = @import("widgets/ScrollView.zig");
|
|||
pub const LineNumbers = @import("widgets/LineNumbers.zig");
|
||||
pub const TextView = @import("widgets/TextView.zig");
|
||||
pub const CodeView = @import("widgets/CodeView.zig");
|
||||
pub const Terminal = @import("widgets/terminal/Terminal.zig");
|
||||
|
||||
// Widgets with dependencies
|
||||
|
||||
|
|
84
src/widgets/terminal/Command.zig
Normal file
84
src/widgets/terminal/Command.zig
Normal 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;
|
||||
}
|
141
src/widgets/terminal/Parser.zig
Normal file
141
src/widgets/terminal/Parser.zig
Normal 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,
|
||||
}
|
||||
}
|
||||
}
|
59
src/widgets/terminal/Pty.zig
Normal file
59
src/widgets/terminal/Pty.zig
Normal 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,
|
||||
};
|
||||
}
|
333
src/widgets/terminal/Screen.zig
Normal file
333
src/widgets/terminal/Screen.zig
Normal 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,
|
||||
}
|
||||
}
|
||||
}
|
232
src/widgets/terminal/Terminal.zig
Normal file
232
src/widgets/terminal/Terminal.zig
Normal 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;
|
||||
}
|
Loading…
Reference in a new issue