From e26352529e9c38701d141b1d3dc4909bbcbaa4b6 Mon Sep 17 00:00:00 2001 From: Mitchell Hashimoto Date: Tue, 10 May 2022 19:31:32 -0700 Subject: [PATCH] move stream handling into the Window --- src/Window.zig | 82 +++++++++++++-- src/terminal/Terminal.zig | 207 +------------------------------------- src/terminal/main.zig | 4 + src/terminal/stream.zig | 90 ++++++++++------- 4 files changed, 134 insertions(+), 249 deletions(-) diff --git a/src/Window.zig b/src/Window.zig index 001d94c8a..60bacd566 100644 --- a/src/Window.zig +++ b/src/Window.zig @@ -15,11 +15,11 @@ const gl = @import("opengl.zig"); const libuv = @import("libuv/main.zig"); const Pty = @import("Pty.zig"); const Command = @import("Command.zig"); -const Terminal = @import("terminal/Terminal.zig"); const SegmentedPool = @import("segmented_pool.zig").SegmentedPool; const frame = @import("tracy/tracy.zig").frame; const trace = @import("tracy/tracy.zig").trace; const max_timer = @import("max_timer.zig"); +const terminal = @import("terminal/main.zig"); const RenderTimer = max_timer.MaxTimer(renderTimerCallback); @@ -48,7 +48,10 @@ command: Command, /// that manages input, grid updating, etc. and is renderer-agnostic. It /// just stores internal state about a grid. This is connected back to /// a renderer. -terminal: Terminal, +terminal: terminal.Terminal, + +/// The stream parser. +terminal_stream: terminal.Stream(*Window), /// Timer that blinks the cursor. cursor_timer: libuv.Timer, @@ -169,7 +172,7 @@ pub fn create(alloc: Allocator, loop: libuv.Loop) !*Window { try stream.readStart(ttyReadAlloc, ttyRead); // Create our terminal - var term = try Terminal.init(alloc, grid.size.columns, grid.size.rows); + var term = try terminal.Terminal.init(alloc, grid.size.columns, grid.size.rows); errdefer term.deinit(alloc); // Setup a timer for blinking the cursor @@ -186,6 +189,7 @@ pub fn create(alloc: Allocator, loop: libuv.Loop) !*Window { .pty = pty, .command = cmd, .terminal = term, + .terminal_stream = .{ .handler = self }, .cursor_timer = timer, .render_timer = try RenderTimer.init(loop, self, 16, 96), .pty_stream = stream, @@ -429,10 +433,6 @@ fn ttyRead(t: *libuv.Tty, n: isize, buf: []const u8) void { return; }; - // Add this character to the terminal buffer. - win.terminal.append(win.alloc, buf[0..@intCast(usize, n)]) catch |err| - log.err("error writing terminal data: {}", .{err}); - // Whenever a character is typed, we ensure the cursor is visible // and we restart the cursor timer. win.grid.cursor_visible = true; @@ -442,6 +442,10 @@ fn ttyRead(t: *libuv.Tty, n: isize, buf: []const u8) void { // Schedule a render win.render_timer.schedule() catch unreachable; + + // Process the terminal data + win.terminal_stream.nextSlice(buf[0..@intCast(usize, n)]) catch |err| + log.err("error processing terminal data: {}", .{err}); } fn ttyWrite(req: *libuv.WriteReq, status: i32) void { @@ -488,3 +492,67 @@ fn renderTimerCallback(t: *libuv.Timer) void { // Record our run win.render_timer.tick(); } + +//------------------------------------------------------------------- +// Stream Callbacks + +pub fn print(self: *Window, c: u8) !void { + try self.terminal.print(self.alloc, c); +} + +pub fn bell(self: Window) !void { + _ = self; + log.info("BELL", .{}); +} + +pub fn backspace(self: *Window) !void { + self.terminal.backspace(); +} + +pub fn horizontalTab(self: *Window) !void { + try self.terminal.horizontalTab(self.alloc); +} + +pub fn linefeed(self: *Window) !void { + self.terminal.linefeed(self.alloc); +} + +pub fn carriageReturn(self: *Window) !void { + self.terminal.carriageReturn(); +} + +pub fn setCursorRight(self: *Window, amount: u16) !void { + self.terminal.cursorRight(amount); +} + +pub fn setCursorCol(self: *Window, col: u16) !void { + try self.terminal.setCursorPos(self.terminal.cursor.y + 1, col); +} + +pub fn setCursorRow(self: *Window, row: u16) !void { + try self.terminal.setCursorPos(row, self.terminal.cursor.x + 1); +} + +pub fn setCursorPos(self: *Window, row: u16, col: u16) !void { + try self.terminal.setCursorPos(row, col); +} + +pub fn eraseDisplay(self: *Window, mode: terminal.EraseDisplay) !void { + try self.terminal.eraseDisplay(self.alloc, mode); +} + +pub fn eraseLine(self: *Window, mode: terminal.EraseLine) !void { + try self.terminal.eraseLine(mode); +} + +pub fn deleteChars(self: *Window, count: usize) !void { + try self.terminal.deleteChars(count); +} + +pub fn eraseChars(self: *Window, count: usize) !void { + try self.terminal.eraseChars(count); +} + +pub fn reverseIndex(self: *Window) !void { + try self.terminal.reverseIndex(self.alloc); +} diff --git a/src/terminal/Terminal.zig b/src/terminal/Terminal.zig index c1b87ace0..ca077897f 100644 --- a/src/terminal/Terminal.zig +++ b/src/terminal/Terminal.zig @@ -111,192 +111,7 @@ pub fn plainString(self: Terminal, alloc: Allocator) ![]const u8 { return buffer[0..i]; } -/// Append a string of characters. See appendChar. -pub fn append(self: *Terminal, alloc: Allocator, str: []const u8) !void { - const tracy = trace(@src()); - defer tracy.end(); - - for (str) |c| { - try self.appendChar(alloc, c); - } -} - -/// Append a single character to the terminal. -/// -/// This may allocate if necessary to store the character in the grid. -pub fn appendChar(self: *Terminal, alloc: Allocator, c: u8) !void { - const tracy = trace(@src()); - defer tracy.end(); - - //log.debug("char: {}", .{c}); - const actions = self.parser.next(c); - for (actions) |action_opt| { - //if (action_opt) |action| log.info("action: {}", .{action}); - switch (action_opt orelse continue) { - .print => |p| try self.print(alloc, p), - .execute => |code| try self.execute(alloc, code), - .csi_dispatch => |csi| try self.csiDispatch(alloc, csi), - .esc_dispatch => |esc| try self.escDispatch(alloc, esc), - .osc_dispatch => |cmd| log.warn("unhandled OSC: {}", .{cmd}), - .dcs_hook => |dcs| log.warn("unhandled DCS hook: {}", .{dcs}), - .dcs_put => |code| log.warn("unhandled DCS put: {}", .{code}), - .dcs_unhook => log.warn("unhandled DCS unhook", .{}), - } - } -} - -fn csiDispatch( - self: *Terminal, - alloc: Allocator, - action: Parser.Action.CSI, -) !void { - switch (action.final) { - // CUF - Cursor Right - 'C' => self.cursorRight(switch (action.params.len) { - 0 => 1, - 1 => action.params[0], - else => { - log.warn("invalid cursor right command: {}", .{action}); - return; - }, - }), - - // HPA - Cursor Horizontal Position Absolute (Alias, see '`') - 'G' => if (action.params.len == 0) { - try self.setCursorPos(self.cursor.y + 1, 1); - } else { - try self.setCursorPos(self.cursor.y + 1, action.params[0]); - }, - - // CUP - Set Cursor Position. - 'H' => { - switch (action.params.len) { - 0 => try self.setCursorPos(1, 1), - 1 => try self.setCursorPos(action.params[0], 1), - 2 => try self.setCursorPos(action.params[0], action.params[1]), - else => log.warn("unimplemented CSI: {}", .{action}), - } - }, - - // Erase Display - 'J' => try self.eraseDisplay(alloc, switch (action.params.len) { - 0 => .below, - 1 => mode: { - // TODO: use meta to get enum max - if (action.params[0] > 3) { - log.warn("invalid erase display command: {}", .{action}); - return; - } - - break :mode @intToEnum( - csi.EraseDisplay, - action.params[0], - ); - }, - else => { - log.warn("invalid erase display command: {}", .{action}); - return; - }, - }), - - // Erase Line - 'K' => try self.eraseLine(switch (action.params.len) { - 0 => .right, - 1 => mode: { - // TODO: use meta to get enum max - if (action.params[0] > 3) { - log.warn("invalid erase line command: {}", .{action}); - return; - } - - break :mode @intToEnum( - csi.EraseLine, - action.params[0], - ); - }, - else => { - log.warn("invalid erase line command: {}", .{action}); - return; - }, - }), - - // Delete Character (DCH) - 'P' => try self.deleteChars(switch (action.params.len) { - 0 => 1, - 1 => action.params[0], - else => { - log.warn("invalid delete characters command: {}", .{action}); - return; - }, - }), - - // Erase Characters (ECH) - 'X' => try self.eraseChars(switch (action.params.len) { - 0 => 1, - 1 => action.params[0], - else => { - log.warn("invalid erase characters command: {}", .{action}); - return; - }, - }), - - // HPA - Cursor Horizontal Position Absolute - '`' => if (action.params.len == 0) { - try self.setCursorPos(self.cursor.y + 1, 1); - } else { - try self.setCursorPos(self.cursor.y + 1, action.params[0]); - }, - - // VPA - Cursor Vertical Position Absolute - 'd' => if (action.params.len == 0) { - try self.setCursorPos(1, self.cursor.x + 1); - } else { - try self.setCursorPos(action.params[0], self.cursor.x + 1); - }, - - // SGR - Select Graphic Rendition - 'm' => if (action.params.len == 0) { - // No values defaults to code 0 - try self.selectGraphicRendition(.default); - } else { - // Each parameter sets a separate aspect - for (action.params) |param| { - try self.selectGraphicRendition(@intToEnum( - ansi.RenditionAspect, - param, - )); - } - }, - - else => log.warn("unimplemented CSI: {}", .{action}), - } -} - -fn escDispatch( - self: *Terminal, - alloc: Allocator, - action: Parser.Action.ESC, -) !void { - _ = alloc; - - switch (action.final) { - // RI - Reverse Index - 'M' => switch (action.intermediates.len) { - 0 => try self.reverseIndex(alloc), - else => { - log.warn("invalid reverse index command: {}", .{action}); - return; - }, - }, - - else => { - log.warn("unimplemented esc dispatch: {}", .{action}); - return; - }, - } -} - -fn print(self: *Terminal, alloc: Allocator, c: u8) !void { +pub fn print(self: *Terminal, alloc: Allocator, c: u8) !void { const tracy = trace(@src()); defer tracy.end(); @@ -315,26 +130,6 @@ fn print(self: *Terminal, alloc: Allocator, c: u8) !void { } } -fn execute(self: *Terminal, alloc: Allocator, c: u8) !void { - const tracy = trace(@src()); - defer tracy.end(); - - switch (@intToEnum(ansi.C0, c)) { - .NUL => {}, - .BEL => self.bell(), - .BS => self.backspace(), - .HT => try self.horizontalTab(alloc), - .LF => self.linefeed(alloc), - .CR => self.carriageReturn(), - } -} - -pub fn bell(self: *Terminal) void { - // TODO: bell - _ = self; - log.info("bell", .{}); -} - pub fn selectGraphicRendition(self: *Terminal, aspect: ansi.RenditionAspect) !void { switch (aspect) { .default => self.cursor.bold = false, diff --git a/src/terminal/main.zig b/src/terminal/main.zig index 7b8234357..16da41dcc 100644 --- a/src/terminal/main.zig +++ b/src/terminal/main.zig @@ -1,12 +1,16 @@ const stream = @import("stream.zig"); +const csi = @import("csi.zig"); pub const Terminal = @import("Terminal.zig"); pub const Parser = @import("Parser.zig"); pub const Stream = stream.Stream; +pub const EraseDisplay = csi.EraseDisplay; +pub const EraseLine = csi.EraseLine; // Not exported because they're just used for tests. test { + _ = csi; _ = stream; _ = Parser; _ = Terminal; diff --git a/src/terminal/stream.zig b/src/terminal/stream.zig index a4ae4f475..e2da79c0d 100644 --- a/src/terminal/stream.zig +++ b/src/terminal/stream.zig @@ -18,11 +18,18 @@ const log = std.log.scoped(.stream); /// This is implemented this way because we purposely do NOT want dynamic /// dispatch for performance reasons. The way this is implemented forces /// comptime resolution for all function calls. -pub fn Stream(comptime T: type) type { +pub fn Stream(comptime Handler: type) type { return struct { const Self = @This(); - handler: T, + // We use T with @hasDecl so it needs to be a struct. Unwrap the + // pointer if we were given one. + const T = switch (@typeInfo(Handler)) { + .Pointer => |p| p.child, + else => Handler, + }; + + handler: Handler, parser: Parser = .{}, /// Process a string of characters. @@ -43,7 +50,7 @@ pub fn Stream(comptime T: type) type { //if (action_opt) |action| log.info("action: {}", .{action}); switch (action_opt orelse continue) { .print => |p| if (@hasDecl(T, "print")) try self.handler.print(p), - .execute => |code| if (@hasDecl(T, "execute")) try self.handler.execute(code), + .execute => |code| try self.execute(code), .csi_dispatch => |csi| try self.csiDispatch(csi), .esc_dispatch => |esc| try self.escDispatch(esc), .osc_dispatch => |cmd| log.warn("unhandled OSC: {}", .{cmd}), @@ -54,10 +61,41 @@ pub fn Stream(comptime T: type) type { } } + fn execute(self: *Self, c: u8) !void { + switch (@intToEnum(ansi.C0, c)) { + .NUL => {}, + + .BEL => if (@hasDecl(T, "bell")) + try self.handler.bell() + else + log.warn("unimplemented execute: {x}", .{c}), + + .BS => if (@hasDecl(T, "backspace")) + try self.handler.backspace() + else + log.warn("unimplemented execute: {x}", .{c}), + + .HT => if (@hasDecl(T, "horizontalTab")) + try self.handler.horizontalTab() + else + log.warn("unimplemented execute: {x}", .{c}), + + .LF => if (@hasDecl(T, "linefeed")) + try self.handler.linefeed() + else + log.warn("unimplemented execute: {x}", .{c}), + + .CR => if (@hasDecl(T, "carriageReturn")) + try self.handler.carriageReturn() + else + log.warn("unimplemented execute: {x}", .{c}), + } + } + fn csiDispatch(self: *Self, action: Parser.Action.CSI) !void { switch (action.final) { // CUF - Cursor Right - 'C' => if (@hasDecl(T, "cursorRight")) try self.handler.cursorRight( + 'C' => if (@hasDecl(T, "setCursorRight")) try self.handler.setCursorRight( switch (action.params.len) { 0 => 1, 1 => action.params[0], @@ -70,13 +108,11 @@ pub fn Stream(comptime T: type) type { // HPA - Cursor Horizontal Position Absolute // TODO: test - 'G', '`' => if (@hasDecl(T, "setCursorCol")) try self.handler.setCursorCol( - switch (action.params.len) { - 0 => 1, - 1 => action.params[0], - else => log.warn("invalid HPA command: {}", .{action}), - }, - ) else log.warn("unimplemented CSI callback: {}", .{action}), + 'G', '`' => if (@hasDecl(T, "setCursorCol")) switch (action.params.len) { + 0 => try self.handler.setCursorCol(1), + 1 => try self.handler.setCursorCol(action.params[0]), + else => log.warn("invalid HPA command: {}", .{action}), + } else log.warn("unimplemented CSI callback: {}", .{action}), // CUP - Set Cursor Position. // TODO: test @@ -89,7 +125,7 @@ pub fn Stream(comptime T: type) type { // Erase Display // TODO: test - 'J' => if (@hasDecl(T, "eraseDisplay")) try self.eraseDisplay( + 'J' => if (@hasDecl(T, "eraseDisplay")) try self.handler.eraseDisplay( switch (action.params.len) { 0 => .below, 1 => mode: { @@ -163,7 +199,10 @@ pub fn Stream(comptime T: type) type { switch (action.params.len) { 0 => 1, 1 => action.params[0], - else => log.warn("invalid VPA command: {}", .{action}), + else => { + log.warn("invalid VPA command: {}", .{action}); + return; + }, }, ) else log.warn("unimplemented CSI callback: {}", .{action}), @@ -197,7 +236,7 @@ pub fn Stream(comptime T: type) type { switch (action.final) { // RI - Reverse Index 'M' => if (@hasDecl(T, "reverseIndex")) switch (action.intermediates.len) { - 0 => try self.reverseIndex(), + 0 => try self.handler.reverseIndex(), else => { log.warn("invalid reverse index command: {}", .{action}); return; @@ -227,32 +266,11 @@ test "stream: print" { try testing.expectEqual(@as(u8, 'x'), s.handler.c.?); } -test "stream: execute" { - const H = struct { - c: ?u8 = 0, - p: bool = false, - - pub fn print(self: *@This(), c: u8) !void { - _ = c; - self.p = true; - } - - pub fn execute(self: *@This(), c: u8) !void { - self.c = c; - } - }; - - var s: Stream(H) = .{ .handler = .{} }; - try s.next('\n'); - try testing.expect(!s.handler.p); - try testing.expectEqual(@as(u8, '\n'), s.handler.c.?); -} - test "stream: cursor right (CUF)" { const H = struct { amount: u16 = 0, - pub fn cursorRight(self: *@This(), v: u16) !void { + pub fn setCursorRight(self: *@This(), v: u16) !void { self.amount = v; } };