Merge pull request #288 from mitchellh/push-pop-mode

Save/restore CSI sequences (CSI ? s, CSI ? r)
This commit is contained in:
Mitchell Hashimoto
2023-08-15 15:34:44 -07:00
committed by GitHub
9 changed files with 398 additions and 256 deletions

View File

@ -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,
};
};
@ -1185,9 +1185,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.
@ -1362,7 +1362,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) {
@ -1486,7 +1486,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);
}
@ -1495,8 +1495,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";
@ -1557,7 +1557,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
@ -1592,7 +1592,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;
@ -1608,7 +1608,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;
@ -1620,7 +1620,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", .{});
@ -1791,7 +1791,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;
@ -1929,7 +1929,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;
@ -2231,7 +2231,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) {

View File

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

View File

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

View File

@ -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 [ <n> 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
{

View File

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

View File

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

200
src/terminal/modes.zig Normal file
View File

@ -0,0 +1,200 @@
//! 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 = .{},
/// 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) {
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);
},
}
}
/// 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(ModePacked));
}
};
/// 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 = .{};
// 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));
}

View File

@ -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
@ -554,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
@ -896,20 +970,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" {

View File

@ -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,53 +1184,48 @@ 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 => {},
}
}
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 });
try self.setMode(mode, v);
}
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
// 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 +1242,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 +1256,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 +1312,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 +1453,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 {