move stream handling into the Window

This commit is contained in:
Mitchell Hashimoto
2022-05-10 19:31:32 -07:00
parent e172cffd4d
commit e26352529e
4 changed files with 134 additions and 249 deletions

View File

@ -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);
}

View File

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

View File

@ -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;

View File

@ -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;
}
};