diff --git a/src/widgets/terminal/Command.zig b/src/widgets/terminal/Command.zig index bbbfb3e..bdf7de7 100644 --- a/src/widgets/terminal/Command.zig +++ b/src/widgets/terminal/Command.zig @@ -3,6 +3,7 @@ const Command = @This(); const std = @import("std"); const builtin = @import("builtin"); const Pty = @import("Pty.zig"); +const Terminal = @import("Terminal.zig"); const posix = std.posix; @@ -40,19 +41,45 @@ pub fn spawn(self: *Command, allocator: std.mem.Allocator) !void { 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); + 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 {}; + @panic("a"); + // const EOT = "\x04"; + // _ = std.posix.write(self.pty.tty, EOT) catch {}; } + var act = posix.Sigaction{ + .handler = .{ .handler = handleSigChild }, + .mask = switch (builtin.os.tag) { + .macos => 0, + .linux => posix.empty_sigset, + else => @compileError("os not supported"), + }, + .flags = 0, + }; + try posix.sigaction(posix.SIG.CHLD, &act, null); + // we are the parent self.pid = @intCast(pid); return; } +fn handleSigChild(_: c_int) callconv(.C) void { + std.log.err("sigchild", .{}); + const result = std.posix.waitpid(-1, 0); + + Terminal.global_vt_mutex.lock(); + defer Terminal.global_vt_mutex.unlock(); + if (Terminal.global_vts) |vts| { + var vt = vts.get(result.pid) orelse return; + vt.event_queue.push(.exited); + } +} + pub fn kill(self: *Command) void { if (self.pid) |pid| { std.posix.kill(pid, std.posix.SIG.TERM) catch {}; diff --git a/src/widgets/terminal/Screen.zig b/src/widgets/terminal/Screen.zig index 1fa77ed..b532a38 100644 --- a/src/widgets/terminal/Screen.zig +++ b/src/widgets/terminal/Screen.zig @@ -29,6 +29,20 @@ pub const Cell = struct { self.wrapped = false; self.dirty = true; } + + pub fn copyFrom(self: *Cell, src: Cell) !void { + self.char.clearRetainingCapacity(); + try self.char.appendSlice(src.char.items); + self.style = src.style; + self.uri.clearRetainingCapacity(); + try self.uri.appendSlice(src.uri.items); + self.uri_id.clearRetainingCapacity(); + try self.uri_id.appendSlice(src.uri_id.items); + self.width = src.width; + self.wrapped = src.wrapped; + + self.dirty = true; + } }; pub const Cursor = struct { @@ -198,7 +212,8 @@ pub fn index(self: *Screen) !void { // 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"); + try self.deleteLine(1); + return; } self.cursor.row += 1; } @@ -311,3 +326,55 @@ pub fn eraseRight(self: *Screen) void { self.buf[i].erase(self.cursor.style.bg); } } + +/// delete n lines from te bottom of te scrolling region +pub fn deleteLine(self: *Screen, n: usize) !void { + if (n == 0) return; + + // Don't delete if outside scroll region + if (!self.withinScrollingRegion()) return; + + self.cursor.pending_wrap = false; + + // Number of rows from here to bottom of scroll region or n + const cnt = @min(self.scrolling_region.bottom - self.cursor.row + 1, n); + const stride = (self.width) * cnt; + + var row: usize = self.scrolling_region.top; + while (row <= self.scrolling_region.bottom) : (row += 1) { + var col: usize = self.scrolling_region.left; + while (col <= self.scrolling_region.right) : (col += 1) { + const i = (row * self.width) + col; + if (row + cnt > self.scrolling_region.bottom) + self.buf[i].erase(self.cursor.style.bg) + else + try self.buf[i].copyFrom(self.buf[i + stride]); + } + } +} + +/// insert n lines at the top of the scrolling region +pub fn insertLine(self: *Screen, n: usize) !void { + if (n == 0) return; + + // Don't insert if outside scroll region + if (!self.withinScrollingRegion()) return; + + self.cursor.pending_wrap = false; + + // Number of rows from here to top of scroll region or n + const cnt = @min(self.cursor.row - self.scrolling_region.top + 1, n); + const stride = (self.width) * cnt; + + var row: usize = self.scrolling_region.bottom; + while (row > self.scrolling_region.top) : (row -= 1) { + var col: usize = self.scrolling_region.left; + while (col <= self.scrolling_region.right) : (col += 1) { + const i = (row * self.width) + col; + if (row - cnt < self.scrolling_region.top) + self.buf[i].erase(self.cursor.style.bg) + else + try self.buf[i].copyFrom(self.buf[i - stride]); + } + } +} diff --git a/src/widgets/terminal/Terminal.zig b/src/widgets/terminal/Terminal.zig index fabca2b..7a3e329 100644 --- a/src/widgets/terminal/Terminal.zig +++ b/src/widgets/terminal/Terminal.zig @@ -12,6 +12,12 @@ const Winsize = vaxis.Winsize; const Screen = @import("Screen.zig"); const DisplayWidth = @import("DisplayWidth"); const Key = vaxis.Key; +const Queue = vaxis.Queue(Event, 16); + +pub const Event = union(enum) { + exited, + bell, +}; const grapheme = @import("grapheme"); @@ -34,6 +40,9 @@ pub const InputEvent = union(enum) { key_press: vaxis.Key, }; +pub var global_vt_mutex: std.Thread.Mutex = .{}; +pub var global_vts: ?std.AutoHashMap(i32, *Terminal) = null; + allocator: std.mem.Allocator, scrollback_size: usize, @@ -58,9 +67,7 @@ should_quit: bool = false, mode: Mode = .{}, -pending_events: struct { - bell: std.atomic.Value(bool) = std.atomic.Value(bool).init(false), -} = .{}, +event_queue: Queue = .{}, /// initialize a Terminal. This sets the size of the underlying pty and allocates the sizes of the /// screen @@ -93,6 +100,15 @@ pub fn init( /// release all resources of the Terminal pub fn deinit(self: *Terminal) void { self.should_quit = true; + + pid: { + global_vt_mutex.lock(); + defer global_vt_mutex.unlock(); + var vts = global_vts orelse break :pid; + if (self.cmd.pid) |pid| + _ = vts.remove(pid); + if (vts.count() == 0) vts.deinit(); + } self.cmd.kill(); if (self.thread) |thread| { // write an EOT into the tty to trigger a read on our thread @@ -112,6 +128,17 @@ pub fn spawn(self: *Terminal) !void { self.back_screen = &self.back_screen_pri; try self.cmd.spawn(self.allocator); + + { + // add to our global list + global_vt_mutex.lock(); + defer global_vt_mutex.unlock(); + if (global_vts == null) + global_vts = std.AutoHashMap(i32, *Terminal).init(self.allocator); + if (self.cmd.pid) |pid| + try global_vts.?.put(pid, self); + } + self.thread = try std.Thread.spawn(.{}, Terminal.run, .{self}); } @@ -160,6 +187,10 @@ pub fn draw(self: *Terminal, win: vaxis.Window) !void { } } +pub fn tryEvent(self: *Terminal) ?Event { + return self.event_queue.tryPop(); +} + pub fn update(self: *Terminal, event: InputEvent) !void { switch (event) { .key_press => |key| try self.encodeKey(key, true), @@ -216,7 +247,7 @@ fn run(self: *Terminal) !void { } }, .c0 => |b| try self.handleC0(b), - .escape => |str| std.log.err("unhandled escape: {s}", .{str}), + .escape => |_| {}, // std.log.err("unhandled escape: {s}", .{str}), .ss2 => |ss2| std.log.err("unhandled ss2: {c}", .{ss2}), .ss3 => |ss3| std.log.err("unhandled ss3: {c}", .{ss3}), .csi => |seq| { @@ -250,7 +281,7 @@ fn run(self: *Terminal) !void { self.back_screen.width, ); }, - 'H' => { // CUP + 'H', 'f' => { // CUP var iter = seq.iterator(u16); const row = iter.next() orelse 1; const col = iter.next() orelse 1; @@ -268,6 +299,16 @@ fn run(self: *Terminal) !void { else => continue, } }, + 'L' => { + var iter = seq.iterator(u16); + const n = iter.next() orelse 1; + try self.back_screen.insertLine(n); + }, + 'M' => { + var iter = seq.iterator(u16); + const n = iter.next() orelse 1; + try self.back_screen.deleteLine(n); + }, 'h', 'l' => { var iter = seq.iterator(u16); const mode = iter.next() orelse continue; @@ -293,6 +334,22 @@ fn run(self: *Terminal) !void { } } }, + '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; + }, else => std.log.err("unhandled CSI: {}", .{seq}), } }, @@ -307,7 +364,7 @@ inline fn handleC0(self: *Terminal, b: ansi.C0) !void { .NUL, .SOH, .STX => {}, .EOT => {}, // we send EOT to quit the read thread .ENQ => {}, - .BEL => self.pending_events.bell.store(true, .unordered), + .BEL => self.event_queue.push(.bell), .BS => self.back_screen.cursorLeft(1), .HT => {}, // TODO: HT .LF, .VT, .FF => try self.back_screen.index(),