diff --git a/src/terminal/stream.zig b/src/terminal/stream.zig index 343310af4..085d78964 100644 --- a/src/terminal/stream.zig +++ b/src/terminal/stream.zig @@ -440,7 +440,7 @@ pub fn Stream(comptime Handler: type) type { } fn configureCharset( - self: Self, + self: *Self, intermediates: []const u8, set: charsets.Charset, ) !void { diff --git a/src/termio/Exec.zig b/src/termio/Exec.zig index 0fee6b341..dbb508ece 100644 --- a/src/termio/Exec.zig +++ b/src/termio/Exec.zig @@ -7,6 +7,7 @@ const Allocator = std.mem.Allocator; const termio = @import("../termio.zig"); const Command = @import("../Command.zig"); const Pty = @import("../Pty.zig"); +const SegmentedPool = @import("../segmented_pool.zig").SegmentedPool; const terminal = @import("../terminal/main.zig"); const libuv = @import("libuv"); const renderer = @import("../renderer.zig"); @@ -123,6 +124,7 @@ pub fn threadEnter(self: *Exec, loop: libuv.Loop) !ThreadData { .terminal_stream = .{ .handler = .{ .terminal = &self.terminal, + .renderer_state = self.renderer_state, }, }, }; @@ -148,13 +150,17 @@ const ThreadData = struct { ev: *EventData, pub fn deinit(self: *ThreadData) void { - self.ev.deinit(); + self.ev.deinit(self.alloc); self.alloc.destroy(self.ev); self.* = undefined; } }; const EventData = struct { + // The preallocation size for the write request pool. This should be big + // enough to satisfy most write requests. It must be a power of 2. + const WRITE_REQ_PREALLOC = std.math.pow(usize, 2, 5); + /// This is the arena allocator used for IO read buffers. Since we use /// libuv under the covers, this lets us rarely heap allocate since we're /// usually just reusing buffers from this. @@ -170,9 +176,22 @@ const EventData = struct { /// The data stream is the main IO for the pty. data_stream: libuv.Tty, - pub fn deinit(self: *EventData) void { + /// This is the pool of available (unused) write requests. If you grab + /// one from the pool, you must put it back when you're done! + write_req_pool: SegmentedPool(libuv.WriteReq.T, WRITE_REQ_PREALLOC) = .{}, + + /// The pool of available buffers for writing to the pty. + write_buf_pool: SegmentedPool([64]u8, WRITE_REQ_PREALLOC) = .{}, + + pub fn deinit(self: *EventData, alloc: Allocator) void { self.read_arena.deinit(); + // Clear our write pools. We know we aren't ever going to do + // any more IO since we stop our data stream below so we can just + // drop this. + self.write_req_pool.deinit(alloc); + self.write_buf_pool.deinit(alloc); + // Stop our data stream self.data_stream.readStop(); self.data_stream.close((struct { @@ -268,6 +287,328 @@ fn ttyRead(t: *libuv.Tty, n: isize, buf: []const u8) void { } } +/// This is used as the handler for the terminal.Stream type. This is +/// stateful and is expected to live for the entire lifetime of the terminal. +/// It is NOT VALID to stop a stream handler, create a new one, and use that +/// unless all of the member fields are copied. const StreamHandler = struct { terminal: *terminal.Terminal, + renderer_state: *renderer.State, + + /// Bracketed paste mode + bracketed_paste: bool = false, + + // TODO + fn queueRender(self: *StreamHandler) !void { + _ = self; + } + + // TODO + fn queueWrite(self: *StreamHandler, data: []const u8) !void { + _ = self; + _ = data; + } + + pub fn print(self: *StreamHandler, c: u21) !void { + try self.terminal.print(c); + } + + pub fn bell(self: StreamHandler) !void { + _ = self; + log.info("BELL", .{}); + } + + pub fn backspace(self: *StreamHandler) !void { + self.terminal.backspace(); + } + + pub fn horizontalTab(self: *StreamHandler) !void { + try self.terminal.horizontalTab(); + } + + pub fn linefeed(self: *StreamHandler) !void { + // Small optimization: call index instead of linefeed because they're + // identical and this avoids one layer of function call overhead. + try self.terminal.index(); + } + + pub fn carriageReturn(self: *StreamHandler) !void { + self.terminal.carriageReturn(); + } + + pub fn setCursorLeft(self: *StreamHandler, amount: u16) !void { + self.terminal.cursorLeft(amount); + } + + pub fn setCursorRight(self: *StreamHandler, amount: u16) !void { + self.terminal.cursorRight(amount); + } + + pub fn setCursorDown(self: *StreamHandler, amount: u16) !void { + self.terminal.cursorDown(amount); + } + + pub fn setCursorUp(self: *StreamHandler, amount: u16) !void { + self.terminal.cursorUp(amount); + } + + pub fn setCursorCol(self: *StreamHandler, col: u16) !void { + self.terminal.setCursorColAbsolute(col); + } + + pub fn setCursorRow(self: *StreamHandler, row: u16) !void { + if (self.terminal.modes.origin) { + // TODO + log.err("setCursorRow: implement origin mode", .{}); + unreachable; + } + + self.terminal.setCursorPos(row, self.terminal.screen.cursor.x + 1); + } + + pub fn setCursorPos(self: *StreamHandler, row: u16, col: u16) !void { + self.terminal.setCursorPos(row, col); + } + + pub fn eraseDisplay(self: *StreamHandler, mode: terminal.EraseDisplay) !void { + if (mode == .complete) { + // Whenever we erase the full display, scroll to bottom. + try self.terminal.scrollViewport(.{ .bottom = {} }); + try self.queueRender(); + } + + self.terminal.eraseDisplay(mode); + } + + pub fn eraseLine(self: *StreamHandler, mode: terminal.EraseLine) !void { + self.terminal.eraseLine(mode); + } + + pub fn deleteChars(self: *StreamHandler, count: usize) !void { + try self.terminal.deleteChars(count); + } + + pub fn eraseChars(self: *StreamHandler, count: usize) !void { + self.terminal.eraseChars(count); + } + + pub fn insertLines(self: *StreamHandler, count: usize) !void { + try self.terminal.insertLines(count); + } + + pub fn insertBlanks(self: *StreamHandler, count: usize) !void { + self.terminal.insertBlanks(count); + } + + pub fn deleteLines(self: *StreamHandler, count: usize) !void { + try self.terminal.deleteLines(count); + } + + pub fn reverseIndex(self: *StreamHandler) !void { + try self.terminal.reverseIndex(); + } + + pub fn index(self: *StreamHandler) !void { + try self.terminal.index(); + } + + pub fn nextLine(self: *StreamHandler) !void { + self.terminal.carriageReturn(); + try self.terminal.index(); + } + + pub fn setTopAndBottomMargin(self: *StreamHandler, top: u16, bot: u16) !void { + self.terminal.setScrollingRegion(top, bot); + } + + // pub fn setMode(self: *StreamHandler, mode: terminal.Mode, enabled: bool) !void { + // switch (mode) { + // .reverse_colors => { + // self.terminal.modes.reverse_colors = enabled; + // + // // Schedule a render since we changed colors + // try self.queueRender(); + // }, + // + // .origin => { + // self.terminal.modes.origin = enabled; + // self.terminal.setCursorPos(1, 1); + // }, + // + // .autowrap => { + // self.terminal.modes.autowrap = enabled; + // }, + // + // .cursor_visible => { + // self.renderer_state.cursor.visible = enabled; + // }, + // + // .alt_screen_save_cursor_clear_enter => { + // const opts: terminal.Terminal.AlternateScreenOptions = .{ + // .cursor_save = true, + // .clear_on_enter = true, + // }; + // + // if (enabled) + // self.terminal.alternateScreen(opts) + // else + // self.terminal.primaryScreen(opts); + // + // // Schedule a render since we changed screens + // try self.queueRender(); + // }, + // + // .bracketed_paste => self.bracketed_paste = true, + // + // .enable_mode_3 => { + // // Disable deccolm + // self.terminal.setDeccolmSupported(enabled); + // + // // Force resize back to the window size + // self.terminal.resize(self.alloc, self.grid_size.columns, self.grid_size.rows) catch |err| + // log.err("error updating terminal size: {}", .{err}); + // }, + // + // .@"132_column" => try self.terminal.deccolm( + // self.alloc, + // if (enabled) .@"132_cols" else .@"80_cols", + // ), + // + // .mouse_event_x10 => self.terminal.modes.mouse_event = if (enabled) .x10 else .none, + // .mouse_event_normal => self.terminal.modes.mouse_event = if (enabled) .normal else .none, + // .mouse_event_button => self.terminal.modes.mouse_event = if (enabled) .button else .none, + // .mouse_event_any => self.terminal.modes.mouse_event = if (enabled) .any else .none, + // + // .mouse_format_utf8 => self.terminal.modes.mouse_format = if (enabled) .utf8 else .x10, + // .mouse_format_sgr => self.terminal.modes.mouse_format = if (enabled) .sgr else .x10, + // .mouse_format_urxvt => self.terminal.modes.mouse_format = if (enabled) .urxvt else .x10, + // .mouse_format_sgr_pixels => self.terminal.modes.mouse_format = if (enabled) .sgr_pixels else .x10, + // + // else => if (enabled) log.warn("unimplemented mode: {}", .{mode}), + // } + // } + + pub fn setAttribute(self: *StreamHandler, attr: terminal.Attribute) !void { + switch (attr) { + .unknown => |unk| log.warn("unimplemented or unknown attribute: {any}", .{unk}), + + else => self.terminal.setAttribute(attr) catch |err| + log.warn("error setting attribute {}: {}", .{ attr, err }), + } + } + + pub fn deviceAttributes( + self: *StreamHandler, + req: terminal.DeviceAttributeReq, + params: []const u16, + ) !void { + _ = params; + + switch (req) { + // VT220 + .primary => self.queueWrite("\x1B[?62;c") catch |err| + log.warn("error queueing device attr response: {}", .{err}), + else => log.warn("unimplemented device attributes req: {}", .{req}), + } + } + + pub fn deviceStatusReport( + self: *StreamHandler, + req: terminal.DeviceStatusReq, + ) !void { + switch (req) { + .operating_status => self.queueWrite("\x1B[0n") catch |err| + log.warn("error queueing device attr response: {}", .{err}), + + .cursor_position => { + const pos: struct { + x: usize, + y: usize, + } = if (self.terminal.modes.origin) .{ + // TODO: what do we do if cursor is outside scrolling region? + .x = self.terminal.screen.cursor.x, + .y = self.terminal.screen.cursor.y -| self.terminal.scrolling_region.top, + } else .{ + .x = self.terminal.screen.cursor.x, + .y = self.terminal.screen.cursor.y, + }; + + // Response always is at least 4 chars, so this leaves the + // remainder for the row/column as base-10 numbers. This + // will support a very large terminal. + var buf: [32]u8 = undefined; + const resp = try std.fmt.bufPrint(&buf, "\x1B[{};{}R", .{ + pos.y + 1, + pos.x + 1, + }); + + try self.queueWrite(resp); + }, + + else => log.warn("unimplemented device status req: {}", .{req}), + } + } + + pub fn setCursorStyle( + self: *StreamHandler, + style: terminal.CursorStyle, + ) !void { + self.renderer_state.cursor.style = style; + } + + pub fn decaln(self: *StreamHandler) !void { + try self.terminal.decaln(); + } + + pub fn tabClear(self: *StreamHandler, cmd: terminal.TabClear) !void { + self.terminal.tabClear(cmd); + } + + pub fn tabSet(self: *StreamHandler) !void { + self.terminal.tabSet(); + } + + pub fn saveCursor(self: *StreamHandler) !void { + self.terminal.saveCursor(); + } + + pub fn restoreCursor(self: *StreamHandler) !void { + self.terminal.restoreCursor(); + } + + pub fn enquiry(self: *StreamHandler) !void { + try self.queueWrite(""); + } + + pub fn scrollDown(self: *StreamHandler, count: usize) !void { + try self.terminal.scrollDown(count); + } + + pub fn scrollUp(self: *StreamHandler, count: usize) !void { + try self.terminal.scrollUp(count); + } + + pub fn setActiveStatusDisplay( + self: *StreamHandler, + req: terminal.StatusDisplay, + ) !void { + self.terminal.status_display = req; + } + + pub fn configureCharset( + self: *StreamHandler, + slot: terminal.CharsetSlot, + set: terminal.Charset, + ) !void { + self.terminal.configureCharset(slot, set); + } + + pub fn invokeCharset( + self: *StreamHandler, + active: terminal.CharsetActiveSlot, + slot: terminal.CharsetSlot, + single: bool, + ) !void { + self.terminal.invokeCharset(active, slot, single); + } };