mirror-ghostty/src/terminal/stream_readonly.zig

879 lines
34 KiB
Zig

const std = @import("std");
const testing = std.testing;
const stream = @import("stream.zig");
const Action = stream.Action;
const Screen = @import("Screen.zig");
const modes = @import("modes.zig");
const osc_color = @import("osc/color.zig");
const kitty_color = @import("kitty/color.zig");
const Terminal = @import("Terminal.zig");
/// This is a Stream implementation that processes actions against
/// a Terminal and updates the Terminal state. It is called "readonly" because
/// it only processes actions that modify terminal state, while ignoring
/// any actions that require a response (like queries).
///
/// If you're implementing a terminal emulator that only needs to render
/// output and doesn't need to respond (since it maybe isn't running the
/// actual program), this is the stream type to use. For example, this is
/// ideal for replay tooling, CI logs, PaaS builder output, etc.
pub const Stream = stream.Stream(Handler);
/// See Stream, which is just the stream wrapper around this.
///
/// This isn't attached directly to Terminal because there is additional
/// state and options we plan to add in the future, such as APC/DCS which
/// don't make sense to me to add to the Terminal directly. Instead, you
/// can call `vtHandler` on Terminal to initialize this handler.
pub const Handler = struct {
/// The terminal state to modify.
terminal: *Terminal,
pub fn init(terminal: *Terminal) Handler {
return .{
.terminal = terminal,
};
}
pub fn deinit(self: *Handler) void {
// Currently does nothing but may in the future so callers should
// call this.
_ = self;
}
pub fn vt(
self: *Handler,
comptime action: Action.Tag,
value: Action.Value(action),
) !void {
switch (action) {
.print => try self.terminal.print(value.cp),
.print_repeat => try self.terminal.printRepeat(value),
.backspace => self.terminal.backspace(),
.carriage_return => self.terminal.carriageReturn(),
.linefeed => try self.terminal.linefeed(),
.index => try self.terminal.index(),
.next_line => {
try self.terminal.index();
self.terminal.carriageReturn();
},
.reverse_index => self.terminal.reverseIndex(),
.cursor_up => self.terminal.cursorUp(value.value),
.cursor_down => self.terminal.cursorDown(value.value),
.cursor_left => self.terminal.cursorLeft(value.value),
.cursor_right => self.terminal.cursorRight(value.value),
.cursor_pos => self.terminal.setCursorPos(value.row, value.col),
.cursor_col => self.terminal.setCursorPos(self.terminal.screen.cursor.y + 1, value.value),
.cursor_row => self.terminal.setCursorPos(value.value, self.terminal.screen.cursor.x + 1),
.cursor_col_relative => self.terminal.setCursorPos(
self.terminal.screen.cursor.y + 1,
self.terminal.screen.cursor.x + 1 +| value.value,
),
.cursor_row_relative => self.terminal.setCursorPos(
self.terminal.screen.cursor.y + 1 +| value.value,
self.terminal.screen.cursor.x + 1,
),
.cursor_style => {
const blink = switch (value) {
.default, .steady_block, .steady_bar, .steady_underline => false,
.blinking_block, .blinking_bar, .blinking_underline => true,
};
const style: Screen.CursorStyle = switch (value) {
.default, .blinking_block, .steady_block => .block,
.blinking_bar, .steady_bar => .bar,
.blinking_underline, .steady_underline => .underline,
};
self.terminal.modes.set(.cursor_blinking, blink);
self.terminal.screen.cursor.cursor_style = style;
},
.erase_display_below => self.terminal.eraseDisplay(.below, value),
.erase_display_above => self.terminal.eraseDisplay(.above, value),
.erase_display_complete => self.terminal.eraseDisplay(.complete, value),
.erase_display_scrollback => self.terminal.eraseDisplay(.scrollback, value),
.erase_display_scroll_complete => self.terminal.eraseDisplay(.scroll_complete, value),
.erase_line_right => self.terminal.eraseLine(.right, value),
.erase_line_left => self.terminal.eraseLine(.left, value),
.erase_line_complete => self.terminal.eraseLine(.complete, value),
.erase_line_right_unless_pending_wrap => self.terminal.eraseLine(.right_unless_pending_wrap, value),
.delete_chars => self.terminal.deleteChars(value),
.erase_chars => self.terminal.eraseChars(value),
.insert_lines => self.terminal.insertLines(value),
.insert_blanks => self.terminal.insertBlanks(value),
.delete_lines => self.terminal.deleteLines(value),
.scroll_up => self.terminal.scrollUp(value),
.scroll_down => self.terminal.scrollDown(value),
.horizontal_tab => try self.horizontalTab(value),
.horizontal_tab_back => try self.horizontalTabBack(value),
.tab_clear_current => self.terminal.tabClear(.current),
.tab_clear_all => self.terminal.tabClear(.all),
.tab_set => self.terminal.tabSet(),
.tab_reset => self.terminal.tabReset(),
.set_mode => try self.setMode(value.mode, true),
.reset_mode => try self.setMode(value.mode, false),
.save_mode => self.terminal.modes.save(value.mode),
.restore_mode => {
const v = self.terminal.modes.restore(value.mode);
try self.setMode(value.mode, v);
},
.top_and_bottom_margin => self.terminal.setTopAndBottomMargin(value.top_left, value.bottom_right),
.left_and_right_margin => self.terminal.setLeftAndRightMargin(value.top_left, value.bottom_right),
.left_and_right_margin_ambiguous => {
if (self.terminal.modes.get(.enable_left_and_right_margin)) {
self.terminal.setLeftAndRightMargin(0, 0);
} else {
self.terminal.saveCursor();
}
},
.save_cursor => self.terminal.saveCursor(),
.restore_cursor => try self.terminal.restoreCursor(),
.invoke_charset => self.terminal.invokeCharset(value.bank, value.charset, value.locking),
.configure_charset => self.terminal.configureCharset(value.slot, value.charset),
.set_attribute => switch (value) {
.unknown => {},
else => self.terminal.setAttribute(value) catch {},
},
.protected_mode_off => self.terminal.setProtectedMode(.off),
.protected_mode_iso => self.terminal.setProtectedMode(.iso),
.protected_mode_dec => self.terminal.setProtectedMode(.dec),
.mouse_shift_capture => self.terminal.flags.mouse_shift_capture = if (value) .true else .false,
.kitty_keyboard_push => self.terminal.screen.kitty_keyboard.push(value.flags),
.kitty_keyboard_pop => self.terminal.screen.kitty_keyboard.pop(@intCast(value)),
.kitty_keyboard_set => self.terminal.screen.kitty_keyboard.set(.set, value.flags),
.kitty_keyboard_set_or => self.terminal.screen.kitty_keyboard.set(.@"or", value.flags),
.kitty_keyboard_set_not => self.terminal.screen.kitty_keyboard.set(.not, value.flags),
.modify_key_format => {
self.terminal.flags.modify_other_keys_2 = false;
switch (value) {
.other_keys_numeric => self.terminal.flags.modify_other_keys_2 = true,
else => {},
}
},
.active_status_display => self.terminal.status_display = value,
.decaln => try self.terminal.decaln(),
.full_reset => self.terminal.fullReset(),
.start_hyperlink => try self.terminal.screen.startHyperlink(value.uri, value.id),
.end_hyperlink => self.terminal.screen.endHyperlink(),
.prompt_start => {
self.terminal.screen.cursor.page_row.semantic_prompt = .prompt;
self.terminal.flags.shell_redraws_prompt = value.redraw;
},
.prompt_continuation => self.terminal.screen.cursor.page_row.semantic_prompt = .prompt_continuation,
.prompt_end => self.terminal.markSemanticPrompt(.input),
.end_of_input => self.terminal.markSemanticPrompt(.command),
.end_of_command => self.terminal.screen.cursor.page_row.semantic_prompt = .input,
.mouse_shape => self.terminal.mouse_shape = value,
.color_operation => try self.colorOperation(value.op, &value.requests),
.kitty_color_report => try self.kittyColorOperation(value),
// No supported DCS commands have any terminal-modifying effects,
// but they may in the future. For now we just ignore it.
.dcs_hook,
.dcs_put,
.dcs_unhook,
=> {},
// APC can modify terminal state (Kitty graphics) but we don't
// currently support it in the readonly stream.
.apc_start,
.apc_end,
.apc_put,
=> {},
// Have no terminal-modifying effect
.bell,
.enquiry,
.request_mode,
.request_mode_unknown,
.size_report,
.xtversion,
.device_attributes,
.device_status,
.kitty_keyboard_query,
.window_title,
.report_pwd,
.show_desktop_notification,
.progress_report,
.clipboard_contents,
.title_push,
.title_pop,
=> {},
}
}
inline fn horizontalTab(self: *Handler, count: u16) !void {
for (0..count) |_| {
const x = self.terminal.screen.cursor.x;
try self.terminal.horizontalTab();
if (x == self.terminal.screen.cursor.x) break;
}
}
inline fn horizontalTabBack(self: *Handler, count: u16) !void {
for (0..count) |_| {
const x = self.terminal.screen.cursor.x;
try self.terminal.horizontalTabBack();
if (x == self.terminal.screen.cursor.x) break;
}
}
fn setMode(self: *Handler, mode: modes.Mode, enabled: bool) !void {
// Set the mode on the terminal
self.terminal.modes.set(mode, enabled);
// Some modes require additional processing
switch (mode) {
.autorepeat,
.reverse_colors,
=> {},
.origin => self.terminal.setCursorPos(1, 1),
.enable_left_and_right_margin => if (!enabled) {
self.terminal.scrolling_region.left = 0;
self.terminal.scrolling_region.right = self.terminal.cols - 1;
},
.alt_screen_legacy => self.terminal.switchScreenMode(.@"47", enabled),
.alt_screen => self.terminal.switchScreenMode(.@"1047", enabled),
.alt_screen_save_cursor_clear_enter => self.terminal.switchScreenMode(.@"1049", enabled),
.save_cursor => if (enabled) {
self.terminal.saveCursor();
} else {
try self.terminal.restoreCursor();
},
.enable_mode_3 => {},
.@"132_column" => try self.terminal.deccolm(
self.terminal.screen.alloc,
if (enabled) .@"132_cols" else .@"80_cols",
),
.synchronized_output,
.linefeed,
.in_band_size_reports,
.focus_event,
=> {},
.mouse_event_x10 => {
if (enabled) {
self.terminal.flags.mouse_event = .x10;
} else {
self.terminal.flags.mouse_event = .none;
}
},
.mouse_event_normal => {
if (enabled) {
self.terminal.flags.mouse_event = .normal;
} else {
self.terminal.flags.mouse_event = .none;
}
},
.mouse_event_button => {
if (enabled) {
self.terminal.flags.mouse_event = .button;
} else {
self.terminal.flags.mouse_event = .none;
}
},
.mouse_event_any => {
if (enabled) {
self.terminal.flags.mouse_event = .any;
} else {
self.terminal.flags.mouse_event = .none;
}
},
.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,
else => {},
}
}
fn colorOperation(
self: *Handler,
op: osc_color.Operation,
requests: *const osc_color.List,
) !void {
_ = op;
if (requests.count() == 0) return;
var it = requests.constIterator(0);
while (it.next()) |req| {
switch (req.*) {
.set => |set| {
switch (set.target) {
.palette => |i| {
self.terminal.flags.dirty.palette = true;
self.terminal.colors.palette.set(i, set.color);
},
.dynamic => |dynamic| switch (dynamic) {
.foreground => self.terminal.colors.foreground.set(set.color),
.background => self.terminal.colors.background.set(set.color),
.cursor => self.terminal.colors.cursor.set(set.color),
.pointer_foreground,
.pointer_background,
.tektronix_foreground,
.tektronix_background,
.highlight_background,
.tektronix_cursor,
.highlight_foreground,
=> {},
},
.special => {},
}
},
.reset => |target| switch (target) {
.palette => |i| {
self.terminal.flags.dirty.palette = true;
self.terminal.colors.palette.reset(i);
},
.dynamic => |dynamic| switch (dynamic) {
.foreground => self.terminal.colors.foreground.reset(),
.background => self.terminal.colors.background.reset(),
.cursor => self.terminal.colors.cursor.reset(),
.pointer_foreground,
.pointer_background,
.tektronix_foreground,
.tektronix_background,
.highlight_background,
.tektronix_cursor,
.highlight_foreground,
=> {},
},
.special => {},
},
.reset_palette => {
const mask = &self.terminal.colors.palette.mask;
var mask_it = mask.iterator(.{});
while (mask_it.next()) |i| {
self.terminal.flags.dirty.palette = true;
self.terminal.colors.palette.reset(@intCast(i));
}
mask.* = .initEmpty();
},
.query,
.reset_special,
=> {},
}
}
}
fn kittyColorOperation(
self: *Handler,
request: kitty_color.OSC,
) !void {
for (request.list.items) |item| {
switch (item) {
.set => |v| switch (v.key) {
.palette => |palette| {
self.terminal.flags.dirty.palette = true;
self.terminal.colors.palette.set(palette, v.color);
},
.special => |special| switch (special) {
.foreground => self.terminal.colors.foreground.set(v.color),
.background => self.terminal.colors.background.set(v.color),
.cursor => self.terminal.colors.cursor.set(v.color),
else => {},
},
},
.reset => |key| switch (key) {
.palette => |palette| {
self.terminal.flags.dirty.palette = true;
self.terminal.colors.palette.reset(palette);
},
.special => |special| switch (special) {
.foreground => self.terminal.colors.foreground.reset(),
.background => self.terminal.colors.background.reset(),
.cursor => self.terminal.colors.cursor.reset(),
else => {},
},
},
.query => {},
}
}
}
};
test "basic print" {
var t: Terminal = try .init(testing.allocator, .{ .cols = 10, .rows = 10 });
defer t.deinit(testing.allocator);
var s: Stream = .initAlloc(testing.allocator, .init(&t));
defer s.deinit();
try s.nextSlice("Hello");
try testing.expectEqual(@as(usize, 5), t.screen.cursor.x);
try testing.expectEqual(@as(usize, 0), t.screen.cursor.y);
const str = try t.plainString(testing.allocator);
defer testing.allocator.free(str);
try testing.expectEqualStrings("Hello", str);
}
test "cursor movement" {
var t: Terminal = try .init(testing.allocator, .{ .cols = 10, .rows = 10 });
defer t.deinit(testing.allocator);
var s: Stream = .initAlloc(testing.allocator, .init(&t));
defer s.deinit();
// Move cursor using escape sequences
try s.nextSlice("Hello\x1B[1;1H");
try testing.expectEqual(@as(usize, 0), t.screen.cursor.x);
try testing.expectEqual(@as(usize, 0), t.screen.cursor.y);
// Move to position 2,3
try s.nextSlice("\x1B[2;3H");
try testing.expectEqual(@as(usize, 2), t.screen.cursor.x);
try testing.expectEqual(@as(usize, 1), t.screen.cursor.y);
}
test "erase operations" {
var t: Terminal = try .init(testing.allocator, .{ .cols = 20, .rows = 10 });
defer t.deinit(testing.allocator);
var s: Stream = .initAlloc(testing.allocator, .init(&t));
defer s.deinit();
// Print some text
try s.nextSlice("Hello World");
try testing.expectEqual(@as(usize, 11), t.screen.cursor.x);
try testing.expectEqual(@as(usize, 0), t.screen.cursor.y);
// Move cursor to position 1,6 and erase from cursor to end of line
try s.nextSlice("\x1B[1;6H");
try s.nextSlice("\x1B[K");
const str = try t.plainString(testing.allocator);
defer testing.allocator.free(str);
try testing.expectEqualStrings("Hello", str);
}
test "tabs" {
var t: Terminal = try .init(testing.allocator, .{ .cols = 80, .rows = 10 });
defer t.deinit(testing.allocator);
var s: Stream = .initAlloc(testing.allocator, .init(&t));
defer s.deinit();
try s.nextSlice("A\tB");
try testing.expectEqual(@as(usize, 9), t.screen.cursor.x);
const str = try t.plainString(testing.allocator);
defer testing.allocator.free(str);
try testing.expectEqualStrings("A B", str);
}
test "modes" {
var t: Terminal = try .init(testing.allocator, .{ .cols = 80, .rows = 24 });
defer t.deinit(testing.allocator);
var s: Stream = .initAlloc(testing.allocator, .init(&t));
defer s.deinit();
// Test wraparound mode
try testing.expect(t.modes.get(.wraparound));
try s.nextSlice("\x1B[?7l"); // Disable wraparound
try testing.expect(!t.modes.get(.wraparound));
try s.nextSlice("\x1B[?7h"); // Enable wraparound
try testing.expect(t.modes.get(.wraparound));
}
test "scrolling regions" {
var t: Terminal = try .init(testing.allocator, .{ .cols = 80, .rows = 24 });
defer t.deinit(testing.allocator);
var s: Stream = .initAlloc(testing.allocator, .init(&t));
defer s.deinit();
// Set scrolling region from line 5 to 20
try s.nextSlice("\x1B[5;20r");
try testing.expectEqual(@as(usize, 4), t.scrolling_region.top);
try testing.expectEqual(@as(usize, 19), t.scrolling_region.bottom);
try testing.expectEqual(@as(usize, 0), t.scrolling_region.left);
try testing.expectEqual(@as(usize, 79), t.scrolling_region.right);
}
test "charsets" {
var t: Terminal = try .init(testing.allocator, .{ .cols = 80, .rows = 24 });
defer t.deinit(testing.allocator);
var s: Stream = .initAlloc(testing.allocator, .init(&t));
defer s.deinit();
// Configure G0 as DEC special graphics
try s.nextSlice("\x1B(0");
try s.nextSlice("`"); // Should print diamond character
const str = try t.plainString(testing.allocator);
defer testing.allocator.free(str);
try testing.expectEqualStrings("", str);
}
test "alt screen" {
var t: Terminal = try .init(testing.allocator, .{ .cols = 10, .rows = 5 });
defer t.deinit(testing.allocator);
var s: Stream = .initAlloc(testing.allocator, .init(&t));
defer s.deinit();
// Write to primary screen
try s.nextSlice("Primary");
try testing.expectEqual(Terminal.ScreenType.primary, t.active_screen);
// Switch to alt screen
try s.nextSlice("\x1B[?1049h");
try testing.expectEqual(Terminal.ScreenType.alternate, t.active_screen);
// Write to alt screen
try s.nextSlice("Alt");
// Switch back to primary
try s.nextSlice("\x1B[?1049l");
try testing.expectEqual(Terminal.ScreenType.primary, t.active_screen);
const str = try t.plainString(testing.allocator);
defer testing.allocator.free(str);
try testing.expectEqualStrings("Primary", str);
}
test "cursor save and restore" {
var t: Terminal = try .init(testing.allocator, .{ .cols = 80, .rows = 24 });
defer t.deinit(testing.allocator);
var s: Stream = .initAlloc(testing.allocator, .init(&t));
defer s.deinit();
// Move cursor to 10,15
try s.nextSlice("\x1B[10;15H");
try testing.expectEqual(@as(usize, 14), t.screen.cursor.x);
try testing.expectEqual(@as(usize, 9), t.screen.cursor.y);
// Save cursor
try s.nextSlice("\x1B7");
// Move cursor elsewhere
try s.nextSlice("\x1B[1;1H");
try testing.expectEqual(@as(usize, 0), t.screen.cursor.x);
try testing.expectEqual(@as(usize, 0), t.screen.cursor.y);
// Restore cursor
try s.nextSlice("\x1B8");
try testing.expectEqual(@as(usize, 14), t.screen.cursor.x);
try testing.expectEqual(@as(usize, 9), t.screen.cursor.y);
}
test "attributes" {
var t: Terminal = try .init(testing.allocator, .{ .cols = 80, .rows = 24 });
defer t.deinit(testing.allocator);
var s: Stream = .initAlloc(testing.allocator, .init(&t));
defer s.deinit();
// Set bold and write text
try s.nextSlice("\x1B[1mBold\x1B[0m");
// Verify we can write attributes - just check the string was written
const str = try t.plainString(testing.allocator);
defer testing.allocator.free(str);
try testing.expectEqualStrings("Bold", str);
}
test "DECALN screen alignment" {
var t: Terminal = try .init(testing.allocator, .{ .cols = 10, .rows = 3 });
defer t.deinit(testing.allocator);
var s: Stream = .initAlloc(testing.allocator, .init(&t));
defer s.deinit();
// Run DECALN
try s.nextSlice("\x1B#8");
// Verify entire screen is filled with 'E'
const str = try t.plainString(testing.allocator);
defer testing.allocator.free(str);
try testing.expectEqualStrings("EEEEEEEEEE\nEEEEEEEEEE\nEEEEEEEEEE", str);
// Cursor should be at 1,1
try testing.expectEqual(@as(usize, 0), t.screen.cursor.x);
try testing.expectEqual(@as(usize, 0), t.screen.cursor.y);
}
test "full reset" {
var t: Terminal = try .init(testing.allocator, .{ .cols = 80, .rows = 24 });
defer t.deinit(testing.allocator);
var s: Stream = .initAlloc(testing.allocator, .init(&t));
defer s.deinit();
// Make some changes
try s.nextSlice("Hello");
try s.nextSlice("\x1B[10;20H");
try s.nextSlice("\x1B[5;20r"); // Set scroll region
try s.nextSlice("\x1B[?7l"); // Disable wraparound
// Full reset
try s.nextSlice("\x1Bc");
// Verify reset state
try testing.expectEqual(@as(usize, 0), t.screen.cursor.x);
try testing.expectEqual(@as(usize, 0), t.screen.cursor.y);
try testing.expectEqual(@as(usize, 0), t.scrolling_region.top);
try testing.expectEqual(@as(usize, 23), t.scrolling_region.bottom);
try testing.expect(t.modes.get(.wraparound));
}
test "ignores query actions" {
var t: Terminal = try .init(testing.allocator, .{ .cols = 80, .rows = 24 });
defer t.deinit(testing.allocator);
var s: Stream = .initAlloc(testing.allocator, .init(&t));
defer s.deinit();
// These should be ignored without error
try s.nextSlice("\x1B[c"); // Device attributes
try s.nextSlice("\x1B[5n"); // Device status report
try s.nextSlice("\x1B[6n"); // Cursor position report
// Terminal should still be functional
try s.nextSlice("Test");
const str = try t.plainString(testing.allocator);
defer testing.allocator.free(str);
try testing.expectEqualStrings("Test", str);
}
test "OSC 4 set and reset palette" {
var t: Terminal = try .init(testing.allocator, .{ .cols = 10, .rows = 10 });
defer t.deinit(testing.allocator);
var s: Stream = .initAlloc(testing.allocator, .init(&t));
defer s.deinit();
// Save default color
const default_color_0 = t.colors.palette.original[0];
// Set color 0 to red
try s.nextSlice("\x1b]4;0;rgb:ff/00/00\x1b\\");
try testing.expectEqual(@as(u8, 0xff), t.colors.palette.current[0].r);
try testing.expectEqual(@as(u8, 0x00), t.colors.palette.current[0].g);
try testing.expectEqual(@as(u8, 0x00), t.colors.palette.current[0].b);
try testing.expect(t.colors.palette.mask.isSet(0));
// Reset color 0
try s.nextSlice("\x1b]104;0\x1b\\");
try testing.expectEqual(default_color_0, t.colors.palette.current[0]);
try testing.expect(!t.colors.palette.mask.isSet(0));
}
test "OSC 104 reset all palette colors" {
var t: Terminal = try .init(testing.allocator, .{ .cols = 10, .rows = 10 });
defer t.deinit(testing.allocator);
var s: Stream = .initAlloc(testing.allocator, .init(&t));
defer s.deinit();
// Set multiple colors
try s.nextSlice("\x1b]4;0;rgb:ff/00/00\x1b\\");
try s.nextSlice("\x1b]4;1;rgb:00/ff/00\x1b\\");
try s.nextSlice("\x1b]4;2;rgb:00/00/ff\x1b\\");
try testing.expect(t.colors.palette.mask.isSet(0));
try testing.expect(t.colors.palette.mask.isSet(1));
try testing.expect(t.colors.palette.mask.isSet(2));
// Reset all palette colors
try s.nextSlice("\x1b]104\x1b\\");
try testing.expectEqual(t.colors.palette.original[0], t.colors.palette.current[0]);
try testing.expectEqual(t.colors.palette.original[1], t.colors.palette.current[1]);
try testing.expectEqual(t.colors.palette.original[2], t.colors.palette.current[2]);
try testing.expect(!t.colors.palette.mask.isSet(0));
try testing.expect(!t.colors.palette.mask.isSet(1));
try testing.expect(!t.colors.palette.mask.isSet(2));
}
test "OSC 10 set and reset foreground color" {
var t: Terminal = try .init(testing.allocator, .{ .cols = 10, .rows = 10 });
defer t.deinit(testing.allocator);
var s: Stream = .initAlloc(testing.allocator, .init(&t));
defer s.deinit();
// Initially unset
try testing.expect(t.colors.foreground.get() == null);
// Set foreground to red
try s.nextSlice("\x1b]10;rgb:ff/00/00\x1b\\");
const fg = t.colors.foreground.get().?;
try testing.expectEqual(@as(u8, 0xff), fg.r);
try testing.expectEqual(@as(u8, 0x00), fg.g);
try testing.expectEqual(@as(u8, 0x00), fg.b);
// Reset foreground
try s.nextSlice("\x1b]110\x1b\\");
try testing.expect(t.colors.foreground.get() == null);
}
test "OSC 11 set and reset background color" {
var t: Terminal = try .init(testing.allocator, .{ .cols = 10, .rows = 10 });
defer t.deinit(testing.allocator);
var s: Stream = .initAlloc(testing.allocator, .init(&t));
defer s.deinit();
// Set background to green
try s.nextSlice("\x1b]11;rgb:00/ff/00\x1b\\");
const bg = t.colors.background.get().?;
try testing.expectEqual(@as(u8, 0x00), bg.r);
try testing.expectEqual(@as(u8, 0xff), bg.g);
try testing.expectEqual(@as(u8, 0x00), bg.b);
// Reset background
try s.nextSlice("\x1b]111\x1b\\");
try testing.expect(t.colors.background.get() == null);
}
test "OSC 12 set and reset cursor color" {
var t: Terminal = try .init(testing.allocator, .{ .cols = 10, .rows = 10 });
defer t.deinit(testing.allocator);
var s: Stream = .initAlloc(testing.allocator, .init(&t));
defer s.deinit();
// Set cursor to blue
try s.nextSlice("\x1b]12;rgb:00/00/ff\x1b\\");
const cursor = t.colors.cursor.get().?;
try testing.expectEqual(@as(u8, 0x00), cursor.r);
try testing.expectEqual(@as(u8, 0x00), cursor.g);
try testing.expectEqual(@as(u8, 0xff), cursor.b);
// Reset cursor
try s.nextSlice("\x1b]112\x1b\\");
// After reset, cursor might be null (using default)
}
test "kitty color protocol set palette" {
var t: Terminal = try .init(testing.allocator, .{ .cols = 10, .rows = 10 });
defer t.deinit(testing.allocator);
var s: Stream = .initAlloc(testing.allocator, .init(&t));
defer s.deinit();
// Set palette color 5 to magenta using kitty protocol
try s.nextSlice("\x1b]21;5=rgb:ff/00/ff\x1b\\");
try testing.expectEqual(@as(u8, 0xff), t.colors.palette.current[5].r);
try testing.expectEqual(@as(u8, 0x00), t.colors.palette.current[5].g);
try testing.expectEqual(@as(u8, 0xff), t.colors.palette.current[5].b);
try testing.expect(t.colors.palette.mask.isSet(5));
try testing.expect(t.flags.dirty.palette);
}
test "kitty color protocol reset palette" {
var t: Terminal = try .init(testing.allocator, .{ .cols = 10, .rows = 10 });
defer t.deinit(testing.allocator);
var s: Stream = .initAlloc(testing.allocator, .init(&t));
defer s.deinit();
// Set and then reset palette color
const original = t.colors.palette.original[7];
try s.nextSlice("\x1b]21;7=rgb:aa/bb/cc\x1b\\");
try testing.expect(t.colors.palette.mask.isSet(7));
try s.nextSlice("\x1b]21;7=\x1b\\");
try testing.expectEqual(original, t.colors.palette.current[7]);
try testing.expect(!t.colors.palette.mask.isSet(7));
}
test "kitty color protocol set foreground" {
var t: Terminal = try .init(testing.allocator, .{ .cols = 10, .rows = 10 });
defer t.deinit(testing.allocator);
var s: Stream = .initAlloc(testing.allocator, .init(&t));
defer s.deinit();
// Set foreground using kitty protocol
try s.nextSlice("\x1b]21;foreground=rgb:12/34/56\x1b\\");
const fg = t.colors.foreground.get().?;
try testing.expectEqual(@as(u8, 0x12), fg.r);
try testing.expectEqual(@as(u8, 0x34), fg.g);
try testing.expectEqual(@as(u8, 0x56), fg.b);
}
test "kitty color protocol set background" {
var t: Terminal = try .init(testing.allocator, .{ .cols = 10, .rows = 10 });
defer t.deinit(testing.allocator);
var s: Stream = .initAlloc(testing.allocator, .init(&t));
defer s.deinit();
// Set background using kitty protocol
try s.nextSlice("\x1b]21;background=rgb:78/9a/bc\x1b\\");
const bg = t.colors.background.get().?;
try testing.expectEqual(@as(u8, 0x78), bg.r);
try testing.expectEqual(@as(u8, 0x9a), bg.g);
try testing.expectEqual(@as(u8, 0xbc), bg.b);
}
test "kitty color protocol set cursor" {
var t: Terminal = try .init(testing.allocator, .{ .cols = 10, .rows = 10 });
defer t.deinit(testing.allocator);
var s: Stream = .initAlloc(testing.allocator, .init(&t));
defer s.deinit();
// Set cursor using kitty protocol
try s.nextSlice("\x1b]21;cursor=rgb:de/f0/12\x1b\\");
const cursor = t.colors.cursor.get().?;
try testing.expectEqual(@as(u8, 0xde), cursor.r);
try testing.expectEqual(@as(u8, 0xf0), cursor.g);
try testing.expectEqual(@as(u8, 0x12), cursor.b);
}
test "kitty color protocol reset foreground" {
var t: Terminal = try .init(testing.allocator, .{ .cols = 10, .rows = 10 });
defer t.deinit(testing.allocator);
var s: Stream = .initAlloc(testing.allocator, .init(&t));
defer s.deinit();
// Set and reset foreground
try s.nextSlice("\x1b]21;foreground=rgb:11/22/33\x1b\\");
try testing.expect(t.colors.foreground.get() != null);
try s.nextSlice("\x1b]21;foreground=\x1b\\");
// After reset, should be unset
try testing.expect(t.colors.foreground.get() == null);
}
test "palette dirty flag set on color change" {
var t: Terminal = try .init(testing.allocator, .{ .cols = 10, .rows = 10 });
defer t.deinit(testing.allocator);
var s: Stream = .initAlloc(testing.allocator, .init(&t));
defer s.deinit();
// Clear dirty flag
t.flags.dirty.palette = false;
// Setting palette color should set dirty flag
try s.nextSlice("\x1b]4;0;rgb:ff/00/00\x1b\\");
try testing.expect(t.flags.dirty.palette);
// Clear and test reset
t.flags.dirty.palette = false;
try s.nextSlice("\x1b]104;0\x1b\\");
try testing.expect(t.flags.dirty.palette);
// Clear and test kitty protocol
t.flags.dirty.palette = false;
try s.nextSlice("\x1b]21;1=rgb:00/ff/00\x1b\\");
try testing.expect(t.flags.dirty.palette);
}