mirror of
https://github.com/ghostty-org/ghostty.git
synced 2025-07-24 04:36:10 +03:00
Merge pull request #288 from mitchellh/push-pop-mode
Save/restore CSI sequences (CSI ? s, CSI ? r)
This commit is contained in:
@ -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) {
|
||||
|
@ -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;
|
||||
}
|
||||
|
@ -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;
|
||||
}
|
||||
|
@ -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
|
||||
|
||||
{
|
||||
|
@ -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
|
||||
|
@ -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
200
src/terminal/modes.zig
Normal 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));
|
||||
}
|
@ -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" {
|
||||
|
@ -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 {
|
||||
|
Reference in New Issue
Block a user