Something went wrong. Try again.
Something went wrong. Try again.
123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757758759760761762763764765766767768769770771772773774775776777778779780781782783784785786787788789790791792793794795796797798799800801802803804805806807808809810811812813814815816817818819820821822823824825826827828829830831832833834835836837838839840841842843844845846847848849850851852853854855856857858859860861862863864865866867868869870871872873//! A virtual terminal widgetconst Terminal = @This();
const std = @import("std");const builtin = @import("builtin");const ansi = @import("ansi.zig");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 Key = vaxis.Key;const key = @import("key.zig");
pub const Event = union(enum) { exited, redraw, bell, title_change: []const u8, pwd_change: []const u8,};
const QueuedEvent = union(enum) { exited, redraw, bell, title_change: []u8, pwd_change: []u8,};const Queue = vaxis.Queue(QueuedEvent, 16);
const posix = std.posix;
const log = std.log.scoped(.terminal);
pub const Options = struct { scrollback_size: u16 = 500, winsize: Winsize = .{ .rows = 24, .cols = 80, .x_pixel = 0, .y_pixel = 0 }, initial_working_directory: ?[]const u8 = null,};
pub const Mode = struct { origin: bool = false, autowrap: bool = true, cursor: bool = true, sync: bool = false,};
pub const InputEvent = union(enum) { key_press: vaxis.Key,};
pub var global_io: std.Io = undefined;pub var global_io_initialized: bool = false;pub var global_vt_mutex: std.Io.Mutex = .init;pub var global_vts: std.AutoHashMapUnmanaged(i32, *Terminal) = .empty;pub var global_sigchild_installed: bool = false;
io: std.Io,allocator: std.mem.Allocator,scrollback_size: u16,
pty: Pty,pty_writer: std.Io.File.Writer,cmd: Command,thread: ?std.Io.Future(void) = null,
/// the screen we draw fromfront_screen: Screen,front_mutex: std.Io.Mutex = .init,
/// the back screensback_screen: *Screen = undefined,back_screen_pri: Screen,back_screen_alt: Screen,// only applies to primary screenscroll_offset: usize = 0,back_mutex: std.Io.Mutex = .init,// dirty is protected by back_mutex. Only access this field when you hold that mutexdirty: bool = false,
should_quit: bool = false,
mode: Mode = .{},
tab_stops: std.ArrayList(u16),title: std.ArrayList(u8) = .empty,working_directory: std.ArrayList(u8) = .empty,
last_printed: []const u8 = "",
event_queue: Queue,event_text: ?[]u8 = null,
/// initialize a Terminal. This sets the size of the underlying pty and allocates the sizes of the/// screenpub fn init( io: std.Io, allocator: std.mem.Allocator, argv: []const []const u8, env: *const std.process.Environ.Map, opts: Options, write_buf: []u8,) !Terminal { if (!global_io_initialized) { global_io = io; global_io_initialized = true; } // Verify we have an absolute path if (opts.initial_working_directory) |pwd| { if (!std.fs.path.isAbsolute(pwd)) return error.InvalidWorkingDirectory; } const pty = try Pty.init(io); errdefer pty.deinit(io); try pty.setSize(opts.winsize); const cmd: Command = .{ .argv = argv, .env_map = env, .pty = pty, .working_directory = opts.initial_working_directory, }; var tabs: std.ArrayList(u16) = try .initCapacity(allocator, opts.winsize.cols / 8); errdefer tabs.deinit(allocator); var col: u16 = 0; while (col < opts.winsize.cols) : (col += 8) { try tabs.append(allocator, col); } var front_screen = try Screen.init(allocator, opts.winsize.cols, opts.winsize.rows); errdefer front_screen.deinit(allocator); var back_screen_pri = try Screen.init(allocator, opts.winsize.cols, opts.winsize.rows + opts.scrollback_size); errdefer back_screen_pri.deinit(allocator); var back_screen_alt = try Screen.init(allocator, opts.winsize.cols, opts.winsize.rows); errdefer back_screen_alt.deinit(allocator); return .{ .io = io, .allocator = allocator, .pty = pty, .pty_writer = pty.pty.writerStreaming(io, write_buf), .cmd = cmd, .scrollback_size = opts.scrollback_size, .front_screen = front_screen, .back_screen_pri = back_screen_pri, .back_screen_alt = back_screen_alt, .tab_stops = tabs, .event_queue = .init(io), };}
/// release all resources of the Terminalpub fn deinit(self: *Terminal) void { self.should_quit = true;
pid: { global_vt_mutex.lock(self.io) catch break :pid; defer global_vt_mutex.unlock(self.io); if (self.cmd.pid) |pid| _ = global_vts.remove(pid); if (global_vts.count() == 0) { global_vts.deinit(self.allocator); } } self.cmd.kill(); if (self.thread) |*thread| { // write an EOT into the tty to trigger a read on our thread const EOT = "\x04"; self.pty.tty.writeStreamingAll(self.io, EOT) catch {}; thread.await(self.io); self.thread = null; } if (self.event_text) |text| self.allocator.free(text); while (self.event_queue.tryPop() catch null) |event| { self.freeQueuedEvent(event); } self.pty.deinit(self.io); self.front_screen.deinit(self.allocator); self.back_screen_pri.deinit(self.allocator); self.back_screen_alt.deinit(self.allocator); self.tab_stops.deinit(self.allocator); self.title.deinit(self.allocator); self.working_directory.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.io, self.allocator);
self.working_directory.clearRetainingCapacity(); if (self.cmd.working_directory) |pwd| { try self.working_directory.appendSlice(self.allocator, pwd); } else { const pwd: std.Io.Dir = .cwd(); const out_path = try pwd.realPathFileAlloc(self.io, ".", self.allocator); try self.working_directory.appendSlice(self.allocator, out_path); }
{ // add to our global list try global_vt_mutex.lock(self.io); defer global_vt_mutex.unlock(self.io); if (self.cmd.pid) |pid| try global_vts.put(self.allocator, pid, self); }
self.thread = try self.io.concurrent(Terminal.run, .{self});}
/// resize the screen. Locks access to the back screen. Should only be called from the main thread./// This is safe to call every render cycle: there is a guard to only perform a resize if the size/// of the window has changed.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) return;
try self.back_mutex.lock(self.io); defer self.back_mutex.unlock(self.io);
var front_screen = try Screen.init(self.allocator, ws.cols, ws.rows); errdefer front_screen.deinit(self.allocator); var back_screen_pri = try Screen.init(self.allocator, ws.cols, ws.rows + self.scrollback_size); errdefer back_screen_pri.deinit(self.allocator); var back_screen_alt = try Screen.init(self.allocator, ws.cols, ws.rows); errdefer back_screen_alt.deinit(self.allocator);
try self.pty.setSize(ws);
self.front_screen.deinit(self.allocator); self.back_screen_pri.deinit(self.allocator); self.back_screen_alt.deinit(self.allocator); self.front_screen = front_screen; self.back_screen_pri = back_screen_pri; self.back_screen_alt = back_screen_alt;}
pub fn draw(self: *Terminal, allocator: std.mem.Allocator, win: vaxis.Window) !void { if (self.back_mutex.tryLock()) { defer self.back_mutex.unlock(self.io); // We keep this as a separate condition so we don't deadlock by obtaining the lock but not // having sync if (!self.mode.sync) { try self.back_screen.copyTo(allocator, &self.front_screen); self.dirty = false; } }
var row: u16 = 0; while (row < self.front_screen.height) : (row += 1) { var col: u16 = 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); } }
if (self.mode.cursor) { win.setCursorShape(self.front_screen.cursor.shape); win.showCursor(self.front_screen.cursor.col, self.front_screen.cursor.row); }}
pub fn tryEvent(self: *Terminal) !?Event { if (self.event_text) |text| { self.allocator.free(text); self.event_text = null; } const event = try self.event_queue.tryPop() orelse return null; return switch (event) { .exited => .exited, .redraw => .redraw, .bell => .bell, .title_change => |text| blk: { self.event_text = text; break :blk .{ .title_change = text }; }, .pwd_change => |text| blk: { self.event_text = text; break :blk .{ .pwd_change = text }; }, };}
fn freeQueuedEvent(self: *Terminal, event: QueuedEvent) void { switch (event) { .title_change, .pwd_change => |text| self.allocator.free(text), else => {}, }}
pub fn update(self: *Terminal, event: InputEvent) !void { switch (event) { .key_press => |k| { const pty_writer = self.get_pty_writer(); defer pty_writer.flush() catch {}; try key.encode(pty_writer, k, true, self.back_screen.csi_u_flags); }, }}
pub fn get_pty_writer(self: *Terminal) *std.Io.Writer { return &self.pty_writer.interface;}
fn reader(self: *const Terminal, buf: []u8) std.Io.File.Reader { return self.pty.pty.readerStreaming(self.io, buf);}
/// process the output from the command on the ptyfn run(self: *Terminal) void { self._run() catch {};}
fn _run(self: *Terminal) !void { var parser: Parser = .{ .buf = try .initCapacity(self.allocator, 128), }; defer parser.buf.deinit();
var reader_buf: [4096]u8 = undefined; var reader_ = self.reader(&reader_buf);
while (!self.should_quit) { const event = try parser.parseReader(&reader_.interface); try self.back_mutex.lock(self.io); defer self.back_mutex.unlock(self.io);
if (!self.dirty and try self.event_queue.tryPush(.redraw)) self.dirty = true;
switch (event) { .print => |str| { var iter = vaxis.unicode.graphemeIterator(str); while (iter.next()) |grapheme| { const gr = grapheme.bytes(str); // TODO: use actual instead of .unicode const w = vaxis.gwidth.gwidth(gr, .unicode); try self.back_screen.print(gr, @truncate(w), self.mode.autowrap); } }, .c0 => |b| try self.handleC0(b), .escape => |esc| { const final = esc[esc.len - 1]; switch (final) { 'B' => {}, // TODO: handle charsets // Index 'D' => try self.back_screen.index(), // Next Line 'E' => { try self.back_screen.index(); self.carriageReturn(); }, // Horizontal Tab Set 'H' => { const already_set: bool = for (self.tab_stops.items) |ts| { if (ts == self.back_screen.cursor.col) break true; } else false; if (already_set) continue; try self.tab_stops.append(self.allocator, @truncate(self.back_screen.cursor.col)); std.mem.sort(u16, self.tab_stops.items, {}, std.sort.asc(u16)); }, // Reverse Index 'M' => try self.back_screen.reverseIndex(), else => log.info("unhandled escape: {s}", .{esc}), } }, .ss2 => |ss2| log.info("unhandled ss2: {c}", .{ss2}), .ss3 => |ss3| log.info("unhandled ss3: {c}", .{ss3}), .csi => |seq| { switch (seq.final) { // Cursor up 'A', 'k' => { var iter = seq.iterator(u16); const delta = iter.next() orelse 1; self.back_screen.cursorUp(delta); }, // Cursor Down 'B' => { var iter = seq.iterator(u16); const delta = iter.next() orelse 1; self.back_screen.cursorDown(delta); }, // Cursor Right 'C' => { var iter = seq.iterator(u16); const delta = iter.next() orelse 1; self.back_screen.cursorRight(delta); }, // Cursor Left 'D', 'j' => { var iter = seq.iterator(u16); const delta = iter.next() orelse 1; self.back_screen.cursorLeft(delta); }, // Cursor Next Line 'E' => { var iter = seq.iterator(u16); const delta = iter.next() orelse 1; self.back_screen.cursorDown(delta); self.carriageReturn(); }, // Cursor Previous Line 'F' => { var iter = seq.iterator(u16); const delta = iter.next() orelse 1; self.back_screen.cursorUp(delta); self.carriageReturn(); }, // Horizontal Position Absolute 'G', '`' => { var iter = seq.iterator(u16); const col = iter.next() orelse 1; self.back_screen.cursor.col = col -| 1; if (self.back_screen.cursor.col < self.back_screen.scrolling_region.left) self.back_screen.cursor.col = self.back_screen.scrolling_region.left; if (self.back_screen.cursor.col > self.back_screen.scrolling_region.right) self.back_screen.cursor.col = self.back_screen.scrolling_region.right; self.back_screen.cursor.pending_wrap = false; }, // Cursor Absolute Position 'H', 'f' => { var iter = seq.iterator(u16); const row = iter.next() orelse 1; const col = iter.next() orelse 1; self.back_screen.cursor.col = col -| 1; self.back_screen.cursor.row = row -| 1; self.back_screen.cursor.pending_wrap = false; }, // Cursor Horizontal Tab 'I' => { var iter = seq.iterator(u16); const n = iter.next() orelse 1; self.horizontalTab(n); }, // Erase In Display 'J' => { // TODO: selective erase (private_marker == '?') var iter = seq.iterator(u16); const kind = iter.next() orelse 0; switch (kind) { 0 => self.back_screen.eraseBelow(), 1 => self.back_screen.eraseAbove(), 2 => self.back_screen.eraseAll(), 3 => {}, else => {}, } }, // Erase in Line 'K' => { // TODO: selective erase (private_marker == '?') var iter = seq.iterator(u8); const ps = iter.next() orelse 0; switch (ps) { 0 => self.back_screen.eraseRight(), 1 => self.back_screen.eraseLeft(), 2 => self.back_screen.eraseLine(), else => continue, } }, // Insert Lines 'L' => { var iter = seq.iterator(u16); const n = iter.next() orelse 1; try self.back_screen.insertLine(n); }, // Delete Lines 'M' => { var iter = seq.iterator(u16); const n = iter.next() orelse 1; try self.back_screen.deleteLine(n); }, // Delete Character 'P' => { var iter = seq.iterator(u16); const n = iter.next() orelse 1; try self.back_screen.deleteCharacters(n); }, // Scroll Up 'S' => { var iter = seq.iterator(u16); const n = iter.next() orelse 1; const cur_row = self.back_screen.cursor.row; const cur_col = self.back_screen.cursor.col; const wrap = self.back_screen.cursor.pending_wrap; defer { self.back_screen.cursor.row = cur_row; self.back_screen.cursor.col = cur_col; self.back_screen.cursor.pending_wrap = wrap; } self.back_screen.cursor.col = self.back_screen.scrolling_region.left; self.back_screen.cursor.row = self.back_screen.scrolling_region.top; try self.back_screen.deleteLine(n); }, // Scroll Down 'T' => { var iter = seq.iterator(u16); const n = iter.next() orelse 1; try self.back_screen.scrollDown(n); }, // Tab Control 'W' => { if (seq.private_marker) |pm| { if (pm != '?') continue; var iter = seq.iterator(u16); const n = iter.next() orelse continue; if (n != 5) continue; self.tab_stops.clearRetainingCapacity(); var col: u16 = 0; while (col < self.back_screen.width) : (col += 8) { try self.tab_stops.append(self.allocator, col); } } }, 'X' => { self.back_screen.cursor.pending_wrap = false; var iter = seq.iterator(u16); const n = iter.next() orelse 1; const start = self.back_screen.cursor.row * self.back_screen.width + self.back_screen.cursor.col; const end = @max( self.back_screen.cursor.row * self.back_screen.width + self.back_screen.width, n, 1, // In case n == 0 ); var i: usize = start; while (i < end) : (i += 1) { self.back_screen.buf[i].erase(self.allocator, self.back_screen.cursor.style.bg); } }, 'Z' => { var iter = seq.iterator(u16); const n = iter.next() orelse 1; self.horizontalBackTab(n); }, // Cursor Horizontal Position Relative 'a' => { var iter = seq.iterator(u16); const n = iter.next() orelse 1; self.back_screen.cursor.pending_wrap = false; const max_end = if (self.mode.origin) self.back_screen.scrolling_region.right else self.back_screen.width - 1; self.back_screen.cursor.col = @min( self.back_screen.cursor.col + max_end, self.back_screen.cursor.col + n, ); }, // Repeat Previous Character 'b' => { var iter = seq.iterator(u16); const n = iter.next() orelse 1; // TODO: maybe not .unicode const w = vaxis.gwidth.gwidth(self.last_printed, .unicode); var i: usize = 0; while (i < n) : (i += 1) { try self.back_screen.print(self.last_printed, @truncate(w), self.mode.autowrap); } }, // Device Attributes 'c' => { const pty_writer = self.get_pty_writer(); defer pty_writer.flush() catch {}; if (seq.private_marker) |pm| { switch (pm) { // Secondary '>' => try pty_writer.writeAll("\x1B[>1;69;0c"), '=' => try pty_writer.writeAll("\x1B[=0000c"), else => log.info("unhandled CSI: {f}", .{seq}), } } else { // Primary try pty_writer.writeAll("\x1B[?62;22c"); } }, // Cursor Vertical Position Absolute 'd' => { self.back_screen.cursor.pending_wrap = false; var iter = seq.iterator(u16); const n = iter.next() orelse 1; const max = if (self.mode.origin) self.back_screen.scrolling_region.bottom else self.back_screen.height -| 1; self.back_screen.cursor.pending_wrap = false; self.back_screen.cursor.row = @min( max, n -| 1, ); }, // Cursor Vertical Position Absolute 'e' => { var iter = seq.iterator(u16); const n = iter.next() orelse 1; self.back_screen.cursor.pending_wrap = false; self.back_screen.cursor.row = @min( self.back_screen.width -| 1, n -| 1, ); }, // Tab Clear 'g' => { var iter = seq.iterator(u16); const n = iter.next() orelse 0; switch (n) { 0 => { const current = try self.tab_stops.toOwnedSlice(self.allocator); defer self.allocator.free(current); self.tab_stops.clearRetainingCapacity(); for (current) |stop| { if (stop == self.back_screen.cursor.col) continue; try self.tab_stops.append(self.allocator, stop); } }, 3 => self.tab_stops.clearAndFree(self.allocator), else => log.info("unhandled CSI: {f}", .{seq}), } }, 'h', 'l' => { var iter = seq.iterator(u16); const mode = iter.next() orelse continue; // There is only one collision (mode = 4), and we don't support the private // version of it if (seq.private_marker != null and mode == 4) continue; self.setMode(mode, seq.final == 'h'); }, 'm' => { if (seq.intermediate == null and seq.private_marker == null) { self.back_screen.sgr(seq); } // TODO: private marker and intermediates }, 'n' => { var iter = seq.iterator(u16); const ps = iter.next() orelse 0; if (seq.intermediate == null and seq.private_marker == null) { const pty_writer = self.get_pty_writer(); defer pty_writer.flush() catch {}; switch (ps) { 5 => try pty_writer.writeAll("\x1b[0n"), 6 => try pty_writer.print("\x1b[{d};{d}R", .{ self.back_screen.cursor.row + 1, self.back_screen.cursor.col + 1, }), else => log.info("unhandled CSI: {f}", .{seq}), } } }, 'p' => { var iter = seq.iterator(u16); const ps = iter.next() orelse 0; if (seq.intermediate) |int| { switch (int) { // report mode '$' => { const pty_writer = self.get_pty_writer(); defer pty_writer.flush() catch {}; switch (ps) { 2026 => try pty_writer.writeAll("\x1b[?2026;2$p"), else => { std.log.warn("unhandled mode: {}", .{ps}); try pty_writer.print("\x1b[?{d};0$p", .{ps}); }, } }, else => log.info("unhandled CSI: {f}", .{seq}), } } }, 'q' => { if (seq.intermediate) |int| { switch (int) { ' ' => { var iter = seq.iterator(u8); const shape = iter.next() orelse 0; self.back_screen.cursor.shape = @fromBackingInt(@intCast(shape)); }, else => {}, } } if (seq.private_marker) |pm| { const pty_writer = self.get_pty_writer(); defer pty_writer.flush() catch {}; switch (pm) { // XTVERSION '>' => try pty_writer.print( "\x1bP>|libvaxis {s}\x1B\\", .{"dev"}, ), else => log.info("unhandled CSI: {f}", .{seq}), } } }, 'r' => { if (seq.intermediate) |_| { // TODO: XTRESTORE continue; } if (seq.private_marker) |_| { // TODO: DECCARA continue; } // DECSTBM var iter = seq.iterator(u16); const top = iter.next() orelse 1; const bottom = iter.next() orelse self.back_screen.height; self.back_screen.scrolling_region.top = top -| 1; self.back_screen.scrolling_region.bottom = bottom -| 1; self.back_screen.cursor.pending_wrap = false; if (self.mode.origin) { self.back_screen.cursor.col = self.back_screen.scrolling_region.left; self.back_screen.cursor.row = self.back_screen.scrolling_region.top; } else { self.back_screen.cursor.col = 0; self.back_screen.cursor.row = 0; } }, else => log.info("unhandled CSI: {f}", .{seq}), } }, .osc => |osc| { const semicolon = std.mem.indexOfScalar(u8, osc, ';') orelse { log.info("unhandled osc: {s}", .{osc}); continue; }; const ps = std.fmt.parseUnsigned(u8, osc[0..semicolon], 10) catch { log.info("unhandled osc: {s}", .{osc}); continue; }; switch (ps) { 0 => { self.title.clearRetainingCapacity(); try self.title.appendSlice(self.allocator, osc[semicolon + 1 ..]); const text = try self.allocator.dupe(u8, self.title.items); errdefer self.allocator.free(text); try self.event_queue.push(.{ .title_change = text }); }, 7 => { // OSC 7 ; file:// <hostname> <pwd> log.err("osc: {s}", .{osc}); self.working_directory.clearRetainingCapacity(); const scheme = "file://"; const start = std.mem.indexOfScalarPos(u8, osc, semicolon + 2 + scheme.len + 1, '/') orelse { log.info("unknown OSC 7 format: {s}", .{osc}); continue; }; const enc = osc[start..]; var i: usize = 0; while (i < enc.len) : (i += 1) { const b = if (enc[i] == '%') blk: { defer i += 2; break :blk try std.fmt.parseUnsigned(u8, enc[i + 1 .. i + 3], 16); } else enc[i]; try self.working_directory.append(self.allocator, b); } const text = try self.allocator.dupe(u8, self.working_directory.items); errdefer self.allocator.free(text); try self.event_queue.push(.{ .pwd_change = text }); }, else => log.info("unhandled osc: {s}", .{osc}), } }, .apc => |apc| log.info("unhandled apc: {s}", .{apc}), } }}
inline fn handleC0(self: *Terminal, b: ansi.C0) !void { switch (b) { .NUL, .SOH, .STX => {}, .EOT => {}, // we send EOT to quit the read thread .ENQ => {}, .BEL => try self.event_queue.push(.bell), .BS => self.back_screen.cursorLeft(1), .HT => self.horizontalTab(1), .LF, .VT, .FF => try self.back_screen.index(), .CR => self.carriageReturn(), .SO => {}, // TODO: Charset shift out .SI => {}, // TODO: Charset shift in else => log.warn("unhandled C0: 0x{x}", .{@backingInt(b)}), }}
fn testInitAllocationFailures(allocator: std.mem.Allocator) !void { var env: std.process.Environ.Map = .init(allocator); defer env.deinit(); var write_buf: [4096]u8 = undefined; var terminal = try Terminal.init( std.testing.io, allocator, &.{"/bin/true"}, &env, .{ .winsize = .{ .rows = 2, .cols = 8, .x_pixel = 0, .y_pixel = 0 } }, &write_buf, ); defer terminal.deinit();}
test "init cleans up allocation failures" { if (comptime builtin.os.tag != .linux) return error.SkipZigTest; try std.testing.checkAllAllocationFailures( std.testing.allocator, testInitAllocationFailures, .{}, );}
pub fn setMode(self: *Terminal, mode: u16, val: bool) void { switch (mode) { 7 => self.mode.autowrap = val, 25 => self.mode.cursor = val, 1049 => { if (val) self.back_screen = &self.back_screen_alt else self.back_screen = &self.back_screen_pri; var i: usize = 0; while (i < self.back_screen.buf.len) : (i += 1) { self.back_screen.buf[i].dirty = true; } }, 2026 => self.mode.sync = val, else => return, }}
pub fn carriageReturn(self: *Terminal) void { self.back_screen.cursor.pending_wrap = false; self.back_screen.cursor.col = if (self.mode.origin) self.back_screen.scrolling_region.left else if (self.back_screen.cursor.col >= self.back_screen.scrolling_region.left) self.back_screen.scrolling_region.left else 0;}
pub fn horizontalTab(self: *Terminal, n: usize) void { // Get the current cursor position const col = self.back_screen.cursor.col;
// Find desired final position var i: usize = 0; const final = for (self.tab_stops.items) |ts| { if (ts <= col) continue; i += 1; if (i == n) break ts; } else self.back_screen.width - 1;
// Move right the delta self.back_screen.cursorRight(final -| col);}
pub fn horizontalBackTab(self: *Terminal, n: usize) void { // Get the current cursor position const col = self.back_screen.cursor.col;
// Find the index of the next backtab const idx = for (self.tab_stops.items, 0..) |ts, i| { if (ts <= col) continue; break i; } else self.tab_stops.items.len - 1;
const final = if (self.mode.origin) @max(self.tab_stops.items[idx -| (n -| 1)], self.back_screen.scrolling_region.left) else self.tab_stops.items[idx -| (n -| 1)];
// Move left the delta self.back_screen.cursorLeft(final - col);}