From e467a48df0c5f2436228f0c260f84346fa8cb123 Mon Sep 17 00:00:00 2001 From: Mitchell Hashimoto Date: Mon, 14 Aug 2023 22:22:40 -0700 Subject: [PATCH 1/6] terminal: working on new modes storage abstraction --- src/terminal/main.zig | 1 + src/terminal/modes.zig | 161 +++++++++++++++++++++++++++++++++++++++++ 2 files changed, 162 insertions(+) create mode 100644 src/terminal/modes.zig diff --git a/src/terminal/main.zig b/src/terminal/main.zig index 296089b68..547ef61da 100644 --- a/src/terminal/main.zig +++ b/src/terminal/main.zig @@ -36,4 +36,5 @@ pub usingnamespace if (builtin.target.isWasm()) struct { test { @import("std").testing.refAllDecls(@This()); + _ = @import("modes.zig"); } diff --git a/src/terminal/modes.zig b/src/terminal/modes.zig new file mode 100644 index 000000000..4b207a267 --- /dev/null +++ b/src/terminal/modes.zig @@ -0,0 +1,161 @@ +//! This file contains all the terminal modes that we support +//! and various support types for them: an enum of supported modes, +//! a packed struct to store mode values, a more generalized state +//! struct to store values plus handle save/restore, and much more. +//! +//! There is pretty heavy comptime usage and type generation here. +//! I don't love to have this sort of complexity but its a good way +//! to ensure all our various types and logic remain in sync. + +const std = @import("std"); +const testing = std.testing; + +/// A struct that maintains the state of all the settable modes. +pub const ModeState = struct { + /// The values of the current modes. + values: ModePacked = .{}, + + /// Set a mode to a value. + pub fn set(self: *ModeState, mode: Mode, value: bool) void { + switch (mode) { + inline else => |mode_comptime| { + const entry = comptime entryForMode(mode_comptime); + @field(self.values, entry.name) = value; + }, + } + } + + /// Get the value of a mode. + pub fn get(self: *ModeState, mode: Mode) bool { + switch (mode) { + inline else => |mode_comptime| { + const entry = comptime entryForMode(mode_comptime); + return @field(self.values, entry.name); + }, + } + } + + test { + // We have this here so that we explicitly fail when we change the + // size of modes. The size of modes is NOT particularly important, + // we just want to be mentally aware when it happens. + try std.testing.expectEqual(4, @sizeOf(ModeState)); + } +}; + +/// A packed struct of all the settable modes. This shouldn't +/// be used directly but rather through the ModeState struct. +pub const ModePacked = packed_struct: { + const StructField = std.builtin.Type.StructField; + var fields: [entries.len]StructField = undefined; + for (entries, 0..) |entry, i| { + fields[i] = .{ + .name = entry.name, + .type = bool, + .default_value = &entry.default, + .is_comptime = false, + .alignment = 0, + }; + } + + break :packed_struct @Type(.{ .Struct = .{ + .layout = .Packed, + .fields = &fields, + .decls = &.{}, + .is_tuple = false, + } }); +}; + +/// An enum(u16) of the available modes. See entries for available values. +pub const Mode = mode_enum: { + const EnumField = std.builtin.Type.EnumField; + var fields: [entries.len]EnumField = undefined; + for (entries, 0..) |entry, i| { + fields[i] = .{ + .name = entry.name, + .value = entry.value, + }; + } + + break :mode_enum @Type(.{ .Enum = .{ + .tag_type = u16, + .fields = &fields, + .decls = &.{}, + .is_exhaustive = true, + } }); +}; + +/// Returns true if we support the given mode. If this is true then +/// you can use `@enumFromInt` to get the Mode value. We don't do +/// this directly due to a Zig compiler bug. +pub fn hasSupport(v: u16) bool { + inline for (@typeInfo(Mode).Enum.fields) |field| { + if (field.value == v) return true; + } + + return false; +} + +fn entryForMode(comptime mode: Mode) ModeEntry { + const name = @tagName(mode); + for (entries) |entry| { + if (std.mem.eql(u8, entry.name, name)) return entry; + } + + unreachable; +} + +/// A single entry of a possible mode we support. This is used to +/// dynamically define the enum and other tables. +const ModeEntry = struct { + name: []const u8, + value: comptime_int, + default: bool = false, +}; + +/// The full list of available entries. For documentation see how +/// they're used within Ghostty or google their values. It is not +/// valuable to redocument them all here. +const entries: []const ModeEntry = &.{ + .{ .name = "cursor_keys", .value = 1 }, + .{ .name = "132_column", .value = 3 }, + .{ .name = "insert", .value = 4 }, + .{ .name = "reverse_colors", .value = 5 }, + .{ .name = "origin", .value = 6 }, + .{ .name = "autowrap", .value = 7, .default = true }, + .{ .name = "mouse_event_x10", .value = 9 }, + .{ .name = "cursor_visible", .value = 25 }, + .{ .name = "enable_mode_3", .value = 40 }, + .{ .name = "keypad_keys", .value = 66 }, + .{ .name = "mouse_event_normal", .value = 1000 }, + .{ .name = "mouse_event_button", .value = 1002 }, + .{ .name = "mouse_event_any", .value = 1003 }, + .{ .name = "focus_event", .value = 1004 }, + .{ .name = "mouse_format_utf8", .value = 1005 }, + .{ .name = "mouse_format_sgr", .value = 1006 }, + .{ .name = "mouse_alternate_scroll", .value = 1007, .default = true }, + .{ .name = "mouse_format_urxvt", .value = 1015 }, + .{ .name = "mouse_format_sgr_pixels", .value = 1016 }, + .{ .name = "alt_esc_prefix", .value = 1036, .default = true }, + .{ .name = "alt_sends_escape", .value = 1039 }, + .{ .name = "alt_screen_save_cursor_clear_enter", .value = 1049 }, + .{ .name = "bracketed_paste", .value = 2004 }, +}; + +test { + _ = Mode; + _ = ModePacked; +} + +test hasSupport { + try testing.expect(hasSupport(1)); + try testing.expect(hasSupport(2004)); + try testing.expect(!hasSupport(8888)); +} + +test ModeState { + var state: ModeState = .{}; + try testing.expect(!state.get(.cursor_keys)); + state.set(.cursor_keys, true); + try testing.expect(state.get(.cursor_keys)); +} From 716c343f07da110c67b10b31b051e1b1a16feba9 Mon Sep 17 00:00:00 2001 From: Mitchell Hashimoto Date: Mon, 14 Aug 2023 22:35:35 -0700 Subject: [PATCH 2/6] terminal: ModeState can save/restore one set of modes --- src/terminal/modes.zig | 41 ++++++++++++++++++++++++++++++++++++++++- 1 file changed, 40 insertions(+), 1 deletion(-) diff --git a/src/terminal/modes.zig b/src/terminal/modes.zig index 4b207a267..a03326c79 100644 --- a/src/terminal/modes.zig +++ b/src/terminal/modes.zig @@ -15,6 +15,12 @@ pub const ModeState = struct { /// The values of the current modes. values: ModePacked = .{}, + /// The saved values. We only allow saving each mode once. + /// This is in line with other terminals that implement XTSAVE + /// and XTRESTORE. We can improve this in the future if it becomes + /// a real-world issue but we need to be aware of a DoS vector. + saved: ModePacked = .{}, + /// Set a mode to a value. pub fn set(self: *ModeState, mode: Mode, value: bool) void { switch (mode) { @@ -35,11 +41,35 @@ pub const ModeState = struct { } } + /// Save the state of the given mode. This can then be restored + /// with restore. This will only be accurate if the previous + /// mode was saved exactly once and not restored. Otherwise this + /// will just keep restoring the last stored value in memory. + pub fn save(self: *ModeState, mode: Mode) void { + switch (mode) { + inline else => |mode_comptime| { + const entry = comptime entryForMode(mode_comptime); + @field(self.saved, entry.name) = @field(self.values, entry.name); + }, + } + } + + /// See save. This will return the restored value. + pub fn restore(self: *ModeState, mode: Mode) bool { + switch (mode) { + inline else => |mode_comptime| { + const entry = comptime entryForMode(mode_comptime); + @field(self.values, entry.name) = @field(self.saved, entry.name); + return @field(self.values, entry.name); + }, + } + } + test { // We have this here so that we explicitly fail when we change the // size of modes. The size of modes is NOT particularly important, // we just want to be mentally aware when it happens. - try std.testing.expectEqual(4, @sizeOf(ModeState)); + try std.testing.expectEqual(4, @sizeOf(ModePacked)); } }; @@ -155,7 +185,16 @@ test hasSupport { test ModeState { var state: ModeState = .{}; + + // Normal set/get try testing.expect(!state.get(.cursor_keys)); state.set(.cursor_keys, true); try testing.expect(state.get(.cursor_keys)); + + // Save/restore + state.save(.cursor_keys); + state.set(.cursor_keys, false); + try testing.expect(!state.get(.cursor_keys)); + try testing.expect(state.restore(.cursor_keys)); + try testing.expect(state.get(.cursor_keys)); } From 951aa00c63522b6b2e3bdeba0e2383fea5096c95 Mon Sep 17 00:00:00 2001 From: Mitchell Hashimoto Date: Tue, 15 Aug 2023 11:00:22 -0700 Subject: [PATCH 3/6] terminal: move to new modes struct --- src/Surface.zig | 34 ++++++------- src/renderer/Metal.zig | 2 +- src/terminal/Terminal.zig | 91 +++++++++++++--------------------- src/terminal/ansi.zig | 100 -------------------------------------- src/terminal/main.zig | 4 +- src/terminal/stream.zig | 36 ++++++++++---- src/termio/Exec.zig | 87 ++++++++++++--------------------- 7 files changed, 112 insertions(+), 242 deletions(-) diff --git a/src/Surface.zig b/src/Surface.zig index 4af32922e..a2b10fe1b 100644 --- a/src/Surface.zig +++ b/src/Surface.zig @@ -742,7 +742,7 @@ fn clipboardPaste( log.warn("error scrolling to bottom err={}", .{err}); }; - break :bracketed self.io.terminal.modes.bracketed_paste; + break :bracketed self.io.terminal.modes.get(.bracketed_paste); }; if (bracketed) { @@ -1024,8 +1024,8 @@ pub fn charCallback( try self.io.terminal.scrollViewport(.{ .bottom = {} }); break :critical .{ - .alt_esc_prefix = self.io.terminal.modes.alt_esc_prefix, - .modify_other_keys = self.io.terminal.modes.modify_other_keys, + .alt_esc_prefix = self.io.terminal.modes.get(.alt_esc_prefix), + .modify_other_keys = self.io.terminal.flags.modify_other_keys_2, }; }; @@ -1178,9 +1178,9 @@ pub fn keyCallback( // We'll need to know these values here on. self.renderer_state.mutex.lock(); - const cursor_key_application = self.io.terminal.modes.cursor_keys; - const keypad_key_application = self.io.terminal.modes.keypad_keys; - const modify_other_keys = self.io.terminal.modes.modify_other_keys; + const cursor_key_application = self.io.terminal.modes.get(.cursor_keys); + const keypad_key_application = self.io.terminal.modes.get(.keypad_keys); + const modify_other_keys = self.io.terminal.flags.modify_other_keys_2; self.renderer_state.mutex.unlock(); // Check if we're processing a function key. @@ -1355,7 +1355,7 @@ pub fn focusCallback(self: *Surface, focused: bool) !void { // Notify the app about focus in/out if it is requesting it { self.renderer_state.mutex.lock(); - const focus_event = self.io.terminal.modes.focus_event; + const focus_event = self.io.terminal.modes.get(.focus_event); self.renderer_state.mutex.unlock(); if (focus_event) { @@ -1479,7 +1479,7 @@ pub fn scrollCallback( // If we have an active mouse reporting mode, clear the selection. // The selection can occur if the user uses the shift mod key to // override mouse grabbing from the window. - if (self.io.terminal.modes.mouse_event != .none) { + if (self.io.terminal.flags.mouse_event != .none) { self.setSelection(null); } @@ -1488,8 +1488,8 @@ pub fn scrollCallback( // (1) alt screen (2) no explicit mouse reporting and (3) alt // scroll mode enabled. if (self.io.terminal.active_screen == .alternate and - self.io.terminal.modes.mouse_event == .none and - self.io.terminal.modes.mouse_alternate_scroll) + self.io.terminal.flags.mouse_event == .none and + self.io.terminal.modes.get(.mouse_alternate_scroll)) { if (y.delta_unsigned > 0) { const seq = if (y.delta < 0) "\x1bOA" else "\x1bOB"; @@ -1550,7 +1550,7 @@ fn mouseReport( // do we want to not report mouse events at all outside the surface? // Depending on the event, we may do nothing at all. - switch (self.io.terminal.modes.mouse_event) { + switch (self.io.terminal.flags.mouse_event) { .none => return, // X10 only reports clicks with mouse button 1, 2, 3. We verify @@ -1585,7 +1585,7 @@ fn mouseReport( if (button == null) { // Null button means motion without a button pressed acc = 3; - } else if (action == .release and self.io.terminal.modes.mouse_format != .sgr) { + } else if (action == .release and self.io.terminal.flags.mouse_format != .sgr) { // Release is 3. It is NOT 3 in SGR mode because SGR can tell // the application what button was released. acc = 3; @@ -1601,7 +1601,7 @@ fn mouseReport( } // X10 doesn't have modifiers - if (self.io.terminal.modes.mouse_event != .x10) { + if (self.io.terminal.flags.mouse_event != .x10) { if (mods.shift) acc += 4; if (mods.super) acc += 8; if (mods.ctrl) acc += 16; @@ -1613,7 +1613,7 @@ fn mouseReport( break :code acc; }; - switch (self.io.terminal.modes.mouse_format) { + switch (self.io.terminal.flags.mouse_format) { .x10 => { if (viewport_point.x > 222 or viewport_point.y > 222) { log.info("X10 mouse format can only encode X/Y up to 223", .{}); @@ -1784,7 +1784,7 @@ pub fn mouseButtonCallback( defer self.renderer_state.mutex.unlock(); // Report mouse events if enabled - if (self.io.terminal.modes.mouse_event != .none) report: { + if (self.io.terminal.flags.mouse_event != .none) report: { // Shift overrides mouse "grabbing" in the window, taken from Kitty. if (mods.shift) break :report; @@ -1922,7 +1922,7 @@ pub fn cursorPosCallback( defer self.renderer_state.mutex.unlock(); // Do a mouse report - if (self.io.terminal.modes.mouse_event != .none) report: { + if (self.io.terminal.flags.mouse_event != .none) report: { // Shift overrides mouse "grabbing" in the window, taken from Kitty. if (self.mouse.mods.shift) break :report; @@ -2224,7 +2224,7 @@ pub fn performBindingAction(self: *Surface, action: input.Binding.Action) !void log.warn("error scrolling to bottom err={}", .{err}); }; - break :normal !self.io.terminal.modes.cursor_keys; + break :normal !self.io.terminal.modes.get(.cursor_keys); }; if (normal) { diff --git a/src/renderer/Metal.zig b/src/renderer/Metal.zig index f116e94fd..5dfe5e4f7 100644 --- a/src/renderer/Metal.zig +++ b/src/renderer/Metal.zig @@ -564,7 +564,7 @@ pub fn render( self.config.background = bg; self.config.foreground = fg; } - if (state.terminal.modes.reverse_colors) { + if (state.terminal.modes.get(.reverse_colors)) { self.config.background = fg; self.config.foreground = bg; } diff --git a/src/terminal/Terminal.zig b/src/terminal/Terminal.zig index ff9b0c8f3..ca35f659f 100644 --- a/src/terminal/Terminal.zig +++ b/src/terminal/Terminal.zig @@ -12,6 +12,7 @@ const assert = std.debug.assert; const Allocator = std.mem.Allocator; const ansi = @import("ansi.zig"); +const modes = @import("modes.zig"); const charsets = @import("charsets.zig"); const csi = @import("csi.zig"); const sgr = @import("sgr.zig"); @@ -77,45 +78,27 @@ color_palette: color.Palette = color.default, /// char CSI (ESC [ b). previous_char: ?u21 = null, -/// Modes - This isn't exhaustive, since some modes (i.e. cursor origin) -/// are applied to the cursor and others aren't boolean yes/no. -modes: packed struct { - const Self = @This(); - - cursor_keys: bool = false, // 1 - insert: bool = false, // 4 - reverse_colors: bool = false, // 5, - origin: bool = false, // 6 - autowrap: bool = true, // 7 - - deccolm: bool = false, // 3, - deccolm_supported: bool = false, // 40 - keypad_keys: bool = false, // 66 - alt_esc_prefix: bool = true, // 1036 - - focus_event: bool = false, // 1004 - mouse_alternate_scroll: bool = true, // 1007 - mouse_event: MouseEvents = .none, - mouse_format: MouseFormat = .x10, - - bracketed_paste: bool = false, // 2004 - - // This is set via ESC[4;2m. Any other modify key mode just sets - // this to false. - modify_other_keys: bool = false, +/// The modes that this terminal currently has active. +modes: modes.ModeState = .{}, +/// These are just a packed set of flags we may set on the terminal. +flags: packed struct { // This isn't a mode, this is set by OSC 133 using the "A" event. // If this is true, it tells us that the shell supports redrawing // the prompt and that when we resize, if the cursor is at a prompt, // then we should clear the screen below and allow the shell to redraw. shell_redraws_prompt: bool = false, - test { - // We have this here so that we explicitly fail when we change the - // size of modes. The size of modes is NOT particularly important, - // we just want to be mentally aware when it happens. - try std.testing.expectEqual(4, @sizeOf(Self)); - } + // This is set via ESC[4;2m. Any other modify key mode just sets + // this to false and we act in mode 1 by default. + modify_other_keys_2: bool = false, + + /// The mouse event mode and format. These are set to the last + /// set mode in modes. You can't get the right event/format to use + /// based on modes alone because modes don't show you what order + /// this was called so we have to track it separately. + mouse_event: MouseEvents = .none, + mouse_format: MouseFormat = .x10, } = .{}, /// State required for all charset operations. @@ -285,10 +268,10 @@ pub fn deccolm(self: *Terminal, alloc: Allocator, mode: DeccolmMode) !void { // bit. If the mode "?40" is set, then "?3" (DECCOLM) is supported. This // doesn't exactly match VT100 semantics but modern terminals no longer // blindly accept mode 3 since its so weird in modern practice. - if (!self.modes.deccolm_supported) return; + if (!self.modes.get(.enable_mode_3)) return; // Enable it - self.modes.deccolm = mode == .@"132_cols"; + self.modes.set(.@"132_column", mode == .@"132_cols"); // Resize -- we can set cols to 0 because deccolm will force it try self.resize(alloc, 0, self.rows); @@ -300,14 +283,6 @@ pub fn deccolm(self: *Terminal, alloc: Allocator, mode: DeccolmMode) !void { // TODO: left/right margins } -/// Allows or disallows deccolm. -pub fn setDeccolmSupported(self: *Terminal, v: bool) void { - const tracy = trace(@src()); - defer tracy.end(); - - self.modes.deccolm_supported = v; -} - /// Resize the underlying terminal. pub fn resize(self: *Terminal, alloc: Allocator, cols_req: usize, rows: usize) !void { const tracy = trace(@src()); @@ -316,8 +291,8 @@ pub fn resize(self: *Terminal, alloc: Allocator, cols_req: usize, rows: usize) ! // If we have deccolm supported then we are fixed at either 80 or 132 // columns depending on if mode 3 is set or not. // TODO: test - const cols: usize = if (self.modes.deccolm_supported) - if (self.modes.deccolm) 132 else 80 + const cols: usize = if (self.modes.get(.enable_mode_3)) + if (self.modes.get(.@"132_column")) 132 else 80 else cols_req; @@ -349,13 +324,13 @@ pub fn resize(self: *Terminal, alloc: Allocator, cols_req: usize, rows: usize) ! }; } -/// If modes.shell_redraws_prompt is true and we're on the primary screen, +/// If shell_redraws_prompt is true and we're on the primary screen, /// then this will clear the screen from the cursor down if the cursor is /// on a prompt in order to allow the shell to redraw the prompt. fn clearPromptForResize(self: *Terminal) void { assert(self.active_screen == .primary); - if (!self.modes.shell_redraws_prompt) return; + if (!self.flags.shell_redraws_prompt) return; // We need to find the first y that is a prompt. If we find any line // that is NOT a prompt (or input -- which is part of a prompt) then @@ -693,13 +668,16 @@ pub fn print(self: *Terminal, c: u21) !void { self.previous_char = c; // If we're soft-wrapping, then handle that first. - if (self.screen.cursor.pending_wrap and self.modes.autowrap) + if (self.screen.cursor.pending_wrap and self.modes.get(.autowrap)) try self.printWrap(); // If we have insert mode enabled then we need to handle that. We // only do insert mode if we're not at the end of the line. - if (self.modes.insert and self.screen.cursor.x + width < self.cols) + if (self.modes.get(.insert) and + self.screen.cursor.x + width < self.cols) + { self.insertBlanks(width); + } switch (width) { // Single cell is very easy: just write in the cell @@ -962,7 +940,7 @@ pub fn setCursorPos(self: *Terminal, row_req: usize, col_req: usize) void { y_offset: usize = 0, x_max: usize, y_max: usize, - } = if (self.modes.origin) .{ + } = if (self.modes.get(.origin)) .{ .x_offset = 0, // TODO: left/right margins .y_offset = self.scrolling_region.top, .x_max = self.cols, // TODO: left/right margins @@ -994,7 +972,7 @@ pub fn setCursorColAbsolute(self: *Terminal, col_req: usize) void { // TODO: test - assert(!self.modes.origin); // TODO + assert(!self.modes.get(.origin)); // TODO if (self.status_display != .main) return; // TODO @@ -1582,6 +1560,7 @@ pub fn fullReset(self: *Terminal) void { self.eraseDisplay(.scrollback); self.eraseDisplay(.complete); self.modes = .{}; + self.flags = .{}; self.tabstops.reset(0); self.screen.cursor = .{}; self.screen.saved_cursor = .{}; @@ -1890,7 +1869,7 @@ test "Terminal: setCursorPosition" { try testing.expect(!t.screen.cursor.pending_wrap); // Origin mode - t.modes.origin = true; + t.modes.set(.origin, true); // No change without a scroll region t.setCursorPos(81, 81); @@ -2340,7 +2319,7 @@ test "Terminal: insert mode with space" { for ("hello") |c| try t.print(c); t.setCursorPos(1, 2); - t.modes.insert = true; + t.modes.set(.insert, true); try t.print('X'); { @@ -2357,7 +2336,7 @@ test "Terminal: insert mode doesn't wrap pushed characters" { for ("hello") |c| try t.print(c); t.setCursorPos(1, 2); - t.modes.insert = true; + t.modes.set(.insert, true); try t.print('X'); { @@ -2373,7 +2352,7 @@ test "Terminal: insert mode does nothing at the end of the line" { defer t.deinit(alloc); for ("hello") |c| try t.print(c); - t.modes.insert = true; + t.modes.set(.insert, true); try t.print('X'); { @@ -2390,7 +2369,7 @@ test "Terminal: insert mode with wide characters" { for ("hello") |c| try t.print(c); t.setCursorPos(1, 2); - t.modes.insert = true; + t.modes.set(.insert, true); try t.print('😀'); // 0x1F600 { @@ -2406,7 +2385,7 @@ test "Terminal: insert mode with wide characters at end" { defer t.deinit(alloc); for ("well") |c| try t.print(c); - t.modes.insert = true; + t.modes.set(.insert, true); try t.print('😀'); // 0x1F600 { diff --git a/src/terminal/ansi.zig b/src/terminal/ansi.zig index 543f40ada..eb40b32a0 100644 --- a/src/terminal/ansi.zig +++ b/src/terminal/ansi.zig @@ -42,106 +42,6 @@ pub const RenditionAspect = enum(u16) { _, }; -/// Modes that can be set with with Set Mode (SM) (ESC [ h). The enum -/// values correspond to the `?`-prefixed modes, since those are the ones -/// of primary interest. The enum value is the mode value. -pub const Mode = enum(u16) { - /// This control function selects the sequences the arrow keys send. - /// You can use the four arrow keys to move the cursor through the current - /// page or to send special application commands. - /// - /// If the DECCKM function is set, then the arrow keys send application - /// sequences to the host. - /// - /// If the DECCKM function is reset, then the arrow keys send ANSI cursor - /// sequences to the host. - cursor_keys = 1, - - /// Change terminal wide between 80 and 132 column mode. When set - /// (with ?40 set), resizes terminal to 132 columns and keeps it that - /// wide. When unset, resizes to 80 columns. - @"132_column" = 3, - - /// Insert mode. This mode writes a character and pushes all existing - /// characters to the right. The existing contents are never wrapped. - insert = 4, - - /// Reverses the foreground and background colors of all cells. - reverse_colors = 5, - - /// If set, the origin of the coordinate system is relative to the - /// current scroll region. If set the cursor is moved to the top left of - /// the current scroll region. - origin = 6, - - /// Enable or disable automatic line wrapping. - autowrap = 7, - - /// Click-only (press) mouse reporting. - mouse_event_x10 = 9, - - /// Set whether the cursor is visible or not. - cursor_visible = 25, - - /// Enables or disables mode ?3. If disabled, the terminal will resize - /// to the size of the window. If enabled, this will take effect when - /// mode ?3 is set or unset. - enable_mode_3 = 40, - - /// DECNKM. Sets application keypad mode if enabled. - keypad_keys = 66, - - /// "Normal" mouse events: click/release, scroll - mouse_event_normal = 1000, - - /// Same as normal mode but also send events for mouse motion - /// while the button is pressed when the cell in the grid changes. - mouse_event_button = 1002, - - /// Same as button mode but doesn't require a button to be pressed - /// to track mouse movement. - mouse_event_any = 1003, - - /// Send focus in/out events. - focus_event = 1004, - - /// Report mouse position in the utf8 format to support larger screens. - mouse_format_utf8 = 1005, - - /// Report mouse position in the SGR format. - mouse_format_sgr = 1006, - - /// Report mouse scroll events as cursor up/down keys. Any other mouse - /// mode overrides this. - mouse_alternate_scroll = 1007, - - /// Report mouse position in the urxvt format. - mouse_format_urxvt = 1015, - - /// Report mouse position in the SGR format as pixels, instead of cells. - mouse_format_sgr_pixels = 1016, - - /// The alt key sends esc as a prefix before any character. On by default. - alt_esc_prefix = 1036, - - /// altSendsEscape xterm (https://invisible-island.net/xterm/manpage/xterm.html) - alt_sends_escape = 1039, - - /// Alternate screen mode with save cursor and clear on enter. - alt_screen_save_cursor_clear_enter = 1049, - - /// Bracket clipboard paste contents in delimiter sequences. - /// - /// When pasting from the (e.g. system) clipboard add "ESC [ 2 0 0 ~" - /// before the clipboard contents and "ESC [ 2 0 1 ~" after the clipboard - /// contents. This allows applications to distinguish clipboard contents - /// from manually typed text. - bracketed_paste = 2004, - - // Non-exhaustive so that @intToEnum never fails for unsupported modes. - _, -}; - /// The device attribute request type (ESC [ c). pub const DeviceAttributeReq = enum { primary, // Blank diff --git a/src/terminal/main.zig b/src/terminal/main.zig index 547ef61da..3de537656 100644 --- a/src/terminal/main.zig +++ b/src/terminal/main.zig @@ -7,6 +7,7 @@ const csi = @import("csi.zig"); const sgr = @import("sgr.zig"); pub const point = @import("point.zig"); pub const color = @import("color.zig"); +pub const modes = @import("modes.zig"); pub const parse_table = @import("parse_table.zig"); pub const Charset = charsets.Charset; @@ -20,7 +21,7 @@ pub const Stream = stream.Stream; pub const CursorStyle = ansi.CursorStyle; pub const DeviceAttributeReq = ansi.DeviceAttributeReq; pub const DeviceStatusReq = ansi.DeviceStatusReq; -pub const Mode = ansi.Mode; +pub const Mode = modes.Mode; pub const ModifyKeyFormat = ansi.ModifyKeyFormat; pub const StatusLineType = ansi.StatusLineType; pub const StatusDisplay = ansi.StatusDisplay; @@ -36,5 +37,4 @@ pub usingnamespace if (builtin.target.isWasm()) struct { test { @import("std").testing.refAllDecls(@This()); - _ = @import("modes.zig"); } diff --git a/src/terminal/stream.zig b/src/terminal/stream.zig index ea3b2a736..47a44c57e 100644 --- a/src/terminal/stream.zig +++ b/src/terminal/stream.zig @@ -4,6 +4,7 @@ const Parser = @import("Parser.zig"); const ansi = @import("ansi.zig"); const charsets = @import("charsets.zig"); const csi = @import("csi.zig"); +const modes = @import("modes.zig"); const osc = @import("osc.zig"); const sgr = @import("sgr.zig"); const trace = @import("tracy").trace; @@ -426,14 +427,30 @@ pub fn Stream(comptime Handler: type) type { // SM - Set Mode 'h' => if (@hasDecl(T, "setMode")) { - for (action.params) |mode| - try self.handler.setMode(@enumFromInt(mode), true); + for (action.params) |mode| { + if (modes.hasSupport(mode)) { + try self.handler.setMode( + @enumFromInt(mode), + true, + ); + } else { + log.warn("unimplemented mode: {}", .{mode}); + } + } } else log.warn("unimplemented CSI callback: {}", .{action}), // RM - Reset Mode 'l' => if (@hasDecl(T, "setMode")) { - for (action.params) |mode| - try self.handler.setMode(@enumFromInt(mode), false); + for (action.params) |mode| { + if (modes.hasSupport(mode)) { + try self.handler.setMode( + @enumFromInt(mode), + false, + ); + } else { + log.warn("unimplemented mode: {}", .{mode}); + } + } } else log.warn("unimplemented CSI callback: {}", .{action}), // SGR - Select Graphic Rendition @@ -896,20 +913,19 @@ test "stream: cursor right (CUF)" { test "stream: set mode (SM) and reset mode (RM)" { const H = struct { - mode: ansi.Mode = @as(ansi.Mode, @enumFromInt(0)), - - pub fn setMode(self: *@This(), mode: ansi.Mode, v: bool) !void { - self.mode = @as(ansi.Mode, @enumFromInt(0)); + mode: modes.Mode = @as(modes.Mode, @enumFromInt(1)), + pub fn setMode(self: *@This(), mode: modes.Mode, v: bool) !void { + self.mode = @as(modes.Mode, @enumFromInt(1)); if (v) self.mode = mode; } }; var s: Stream(H) = .{ .handler = .{} }; try s.nextSlice("\x1B[?6h"); - try testing.expectEqual(@as(ansi.Mode, .origin), s.handler.mode); + try testing.expectEqual(@as(modes.Mode, .origin), s.handler.mode); try s.nextSlice("\x1B[?6l"); - try testing.expectEqual(@as(ansi.Mode, @enumFromInt(0)), s.handler.mode); + try testing.expectEqual(@as(modes.Mode, @enumFromInt(1)), s.handler.mode); } test "stream: restore mode" { diff --git a/src/termio/Exec.zig b/src/termio/Exec.zig index 64c4dfa96..ffdffc238 100644 --- a/src/termio/Exec.zig +++ b/src/termio/Exec.zig @@ -1119,7 +1119,7 @@ const StreamHandler = struct { } pub fn setCursorRow(self: *StreamHandler, row: u16) !void { - if (self.terminal.modes.origin) { + if (self.terminal.modes.get(.origin)) { // TODO log.err("setCursorRow: implement origin mode", .{}); unreachable; @@ -1184,10 +1184,10 @@ const StreamHandler = struct { } pub fn setModifyKeyFormat(self: *StreamHandler, format: terminal.ModifyKeyFormat) !void { - self.terminal.modes.modify_other_keys = false; + self.terminal.flags.modify_other_keys_2 = false; switch (format) { .other_keys => |v| switch (v) { - .numeric => self.terminal.modes.modify_other_keys = true, + .numeric => self.terminal.flags.modify_other_keys_2 = true, else => {}, }, else => {}, @@ -1199,38 +1199,19 @@ const StreamHandler = struct { // terminal locks because it is only called from process() which // grabs the lock. + // We first always set the raw mode on our mode state. + self.terminal.modes.set(mode, enabled); + + // And then some modes require additional processing. switch (mode) { - .cursor_keys => { - self.terminal.modes.cursor_keys = enabled; - }, + // Schedule a render since we changed colors + .reverse_colors => try self.queueRender(), - .keypad_keys => { - self.terminal.modes.keypad_keys = enabled; - }, + // Origin resets cursor pos + .origin => self.terminal.setCursorPos(1, 1), - .insert => { - self.terminal.modes.insert = enabled; - }, - - .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.ev.renderer_state.cursor.visible = enabled; - }, + // We need to update our renderer state for this mode + .cursor_visible => self.ev.renderer_state.cursor.visible = enabled, .alt_screen_save_cursor_clear_enter => { const opts: terminal.Terminal.AlternateScreenOptions = .{ @@ -1247,15 +1228,13 @@ const StreamHandler = struct { try self.queueRender(); }, - .bracketed_paste => self.terminal.modes.bracketed_paste = enabled, - - .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}); + // Force resize back to the window size + .enable_mode_3 => 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( @@ -1263,21 +1242,17 @@ const StreamHandler = struct { 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_event_x10 => self.terminal.flags.mouse_event = if (enabled) .x10 else .none, + .mouse_event_normal => self.terminal.flags.mouse_event = if (enabled) .normal else .none, + .mouse_event_button => self.terminal.flags.mouse_event = if (enabled) .button else .none, + .mouse_event_any => self.terminal.flags.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, + .mouse_format_utf8 => self.terminal.flags.mouse_format = if (enabled) .utf8 else .x10, + .mouse_format_sgr => self.terminal.flags.mouse_format = if (enabled) .sgr else .x10, + .mouse_format_urxvt => self.terminal.flags.mouse_format = if (enabled) .urxvt else .x10, + .mouse_format_sgr_pixels => self.terminal.flags.mouse_format = if (enabled) .sgr_pixels else .x10, - .mouse_alternate_scroll => self.terminal.modes.mouse_alternate_scroll = enabled, - .focus_event => self.terminal.modes.focus_event = enabled, - .alt_esc_prefix => self.terminal.modes.alt_esc_prefix = enabled, - - else => if (enabled) log.warn("unimplemented mode: {}", .{mode}), + else => {}, } } @@ -1323,7 +1298,7 @@ const StreamHandler = struct { const pos: struct { x: usize, y: usize, - } = if (self.terminal.modes.origin) .{ + } = if (self.terminal.modes.get(.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, @@ -1464,7 +1439,7 @@ const StreamHandler = struct { pub fn promptStart(self: *StreamHandler, aid: ?[]const u8, redraw: bool) !void { _ = aid; self.terminal.markSemanticPrompt(.prompt); - self.terminal.modes.shell_redraws_prompt = redraw; + self.terminal.flags.shell_redraws_prompt = redraw; } pub fn promptEnd(self: *StreamHandler) !void { From 06f82ad7137d8c400502d9685f12fa8ac659447f Mon Sep 17 00:00:00 2001 From: Mitchell Hashimoto Date: Tue, 15 Aug 2023 11:43:07 -0700 Subject: [PATCH 4/6] terminal: parse and handle save/restore mode (CSI ? s, CSI ? r) --- src/terminal/stream.zig | 85 ++++++++++++++++++++++++++++++++++------- src/termio/Exec.zig | 14 +++++++ 2 files changed, 85 insertions(+), 14 deletions(-) diff --git a/src/terminal/stream.zig b/src/terminal/stream.zig index 47a44c57e..0ddfbb5bf 100644 --- a/src/terminal/stream.zig +++ b/src/terminal/stream.zig @@ -571,20 +571,77 @@ pub fn Stream(comptime Handler: type) type { }, ) else log.warn("unimplemented CSI callback: {}", .{action}), - // DECSTBM - Set Top and Bottom Margins - // TODO: test - 'r' => if (action.intermediates.len == 0) { - if (@hasDecl(T, "setTopAndBottomMargin")) switch (action.params.len) { - 0 => try self.handler.setTopAndBottomMargin(0, 0), - 1 => try self.handler.setTopAndBottomMargin(action.params[0], 0), - 2 => try self.handler.setTopAndBottomMargin(action.params[0], action.params[1]), - else => log.warn("invalid DECSTBM command: {}", .{action}), - } else log.warn("unimplemented CSI callback: {}", .{action}); - } else { - log.debug( - "ignoring unimplemented CSI r with intermediates: {s}", - .{action.intermediates}, - ); + 'r' => switch (action.intermediates.len) { + // DECSTBM - Set Top and Bottom Margins + 0 => if (@hasDecl(T, "setTopAndBottomMargin")) { + switch (action.params.len) { + 0 => try self.handler.setTopAndBottomMargin(0, 0), + 1 => try self.handler.setTopAndBottomMargin(action.params[0], 0), + 2 => try self.handler.setTopAndBottomMargin(action.params[0], action.params[1]), + else => log.warn("invalid DECSTBM command: {}", .{action}), + } + } else log.warn( + "unimplemented CSI callback: {}", + .{action}, + ), + + 1 => switch (action.intermediates[0]) { + // Restore Mode + '?' => if (@hasDecl(T, "restoreMode")) { + for (action.params) |mode| { + if (modes.hasSupport(mode)) { + try self.handler.restoreMode( + @enumFromInt(mode), + ); + } else { + log.warn( + "unimplemented restore mode: {}", + .{mode}, + ); + } + } + }, + + else => log.warn( + "unknown CSI s with intermediate: {}", + .{action}, + ), + }, + + else => log.warn( + "ignoring unimplemented CSI s with intermediates: {s}", + .{action}, + ), + }, + + // Save Mode + 's' => switch (action.intermediates.len) { + 1 => switch (action.intermediates[0]) { + '?' => if (@hasDecl(T, "saveMode")) { + for (action.params) |mode| { + if (modes.hasSupport(mode)) { + try self.handler.saveMode( + @enumFromInt(mode), + ); + } else { + log.warn( + "unimplemented save mode: {}", + .{mode}, + ); + } + } + }, + + else => log.warn( + "unknown CSI s with intermediate: {}", + .{action}, + ), + }, + + else => log.warn( + "ignoring unimplemented CSI s with intermediates: {s}", + .{action}, + ), }, // ICH - Insert Blanks diff --git a/src/termio/Exec.zig b/src/termio/Exec.zig index ffdffc238..d574bef00 100644 --- a/src/termio/Exec.zig +++ b/src/termio/Exec.zig @@ -1194,6 +1194,20 @@ const StreamHandler = struct { } } + pub fn saveMode(self: *StreamHandler, mode: terminal.Mode) !void { + // log.debug("save mode={}", .{mode}); + self.terminal.modes.save(mode); + } + + pub fn restoreMode(self: *StreamHandler, mode: terminal.Mode) !void { + // For restore mode we have to restore but if we set it, we + // always have to call setMode because setting some modes have + // side effects and we want to make sure we process those. + const v = self.terminal.modes.restore(mode); + // log.debug("restore mode={} v={}", .{ mode, v }); + if (v) try self.setMode(mode, true); + } + pub fn setMode(self: *StreamHandler, mode: terminal.Mode, enabled: bool) !void { // Note: this function doesn't need to grab the render state or // terminal locks because it is only called from process() which From d0bf6a914f79771d1dd7a663224d1d94c03f0194 Mon Sep 17 00:00:00 2001 From: Mitchell Hashimoto Date: Tue, 15 Aug 2023 11:47:31 -0700 Subject: [PATCH 5/6] termio: on restore mode we need to call setMode no matter what --- src/termio/Exec.zig | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/termio/Exec.zig b/src/termio/Exec.zig index d574bef00..d4d858957 100644 --- a/src/termio/Exec.zig +++ b/src/termio/Exec.zig @@ -1205,7 +1205,7 @@ const StreamHandler = struct { // side effects and we want to make sure we process those. const v = self.terminal.modes.restore(mode); // log.debug("restore mode={} v={}", .{ mode, v }); - if (v) try self.setMode(mode, true); + try self.setMode(mode, v); } pub fn setMode(self: *StreamHandler, mode: terminal.Mode, enabled: bool) !void { From ab68569281a0065331733f06e2a72e888cbde860 Mon Sep 17 00:00:00 2001 From: Mitchell Hashimoto Date: Tue, 15 Aug 2023 13:33:06 -0700 Subject: [PATCH 6/6] renderer/opengl: fix compilation for new modes style --- src/renderer/OpenGL.zig | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/renderer/OpenGL.zig b/src/renderer/OpenGL.zig index a0c4f4ec4..e26492323 100644 --- a/src/renderer/OpenGL.zig +++ b/src/renderer/OpenGL.zig @@ -764,7 +764,7 @@ pub fn render( self.config.background = bg; self.config.foreground = fg; } - if (state.terminal.modes.reverse_colors) { + if (state.terminal.modes.get(.reverse_colors)) { self.config.background = fg; self.config.foreground = bg; }