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,
|
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
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 {
|
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,
|
||||||
|
|
|
@ -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) {
|
||||||
|
|
|
@ -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
|
||||||
|
|
||||||
|
|
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