diff --git a/src/bench/page-init.zig b/src/bench/page-init.zig index c3057cd9f..e45d64fbb 100644 --- a/src/bench/page-init.zig +++ b/src/bench/page-init.zig @@ -8,7 +8,6 @@ const assert = std.debug.assert; const Allocator = std.mem.Allocator; const ArenaAllocator = std.heap.ArenaAllocator; const cli = @import("../cli.zig"); -const terminal = @import("../terminal-old/main.zig"); const terminal_new = @import("../terminal/main.zig"); const Args = struct { diff --git a/src/bench/resize.sh b/src/bench/resize.sh deleted file mode 100755 index 8f420bf01..000000000 --- a/src/bench/resize.sh +++ /dev/null @@ -1,12 +0,0 @@ -#!/usr/bin/env bash -# -# Uncomment to test with an active terminal state. -# ARGS=" --terminal" - -hyperfine \ - --warmup 10 \ - -n new \ - "./zig-out/bin/bench-resize --mode=new${ARGS}" \ - -n old \ - "./zig-out/bin/bench-resize --mode=old${ARGS}" - diff --git a/src/bench/resize.zig b/src/bench/resize.zig deleted file mode 100644 index d88803fe7..000000000 --- a/src/bench/resize.zig +++ /dev/null @@ -1,110 +0,0 @@ -//! This benchmark tests the speed of resizing. - -const std = @import("std"); -const assert = std.debug.assert; -const Allocator = std.mem.Allocator; -const ArenaAllocator = std.heap.ArenaAllocator; -const cli = @import("../cli.zig"); -const terminal = @import("../terminal-old/main.zig"); -const terminal_new = @import("../terminal/main.zig"); - -const Args = struct { - mode: Mode = .old, - - /// The number of times to loop. - count: usize = 10_000, - - /// Rows and cols in the terminal. - rows: usize = 50, - cols: usize = 100, - - /// This is set by the CLI parser for deinit. - _arena: ?ArenaAllocator = null, - - pub fn deinit(self: *Args) void { - if (self._arena) |arena| arena.deinit(); - self.* = undefined; - } -}; - -const Mode = enum { - /// The default allocation strategy of the structure. - old, - - /// Use a memory pool to allocate pages from a backing buffer. - new, -}; - -pub const std_options: std.Options = .{ - .log_level = .debug, -}; - -pub fn main() !void { - // We want to use the c allocator because it is much faster than GPA. - const alloc = std.heap.c_allocator; - - // Parse our args - var args: Args = .{}; - defer args.deinit(); - { - var iter = try std.process.argsWithAllocator(alloc); - defer iter.deinit(); - try cli.args.parse(Args, alloc, &args, &iter); - } - - // Handle the modes that do not depend on terminal state first. - switch (args.mode) { - .old => { - var t = try terminal.Terminal.init(alloc, args.cols, args.rows); - defer t.deinit(alloc); - try benchOld(&t, args); - }, - - .new => { - var t = try terminal_new.Terminal.init(alloc, .{ - .cols = @intCast(args.cols), - .rows = @intCast(args.rows), - }); - defer t.deinit(alloc); - try benchNew(&t, args); - }, - } -} - -noinline fn benchOld(t: *terminal.Terminal, args: Args) !void { - // We fill the terminal with letters. - for (0..args.rows) |row| { - for (0..args.cols) |col| { - t.setCursorPos(row + 1, col + 1); - try t.print('A'); - } - } - - for (0..args.count) |i| { - const cols: usize, const rows: usize = if (i % 2 == 0) - .{ args.cols * 2, args.rows * 2 } - else - .{ args.cols, args.rows }; - - try t.screen.resizeWithoutReflow(@intCast(rows), @intCast(cols)); - } -} - -noinline fn benchNew(t: *terminal_new.Terminal, args: Args) !void { - // We fill the terminal with letters. - for (0..args.rows) |row| { - for (0..args.cols) |col| { - t.setCursorPos(row + 1, col + 1); - try t.print('A'); - } - } - - for (0..args.count) |i| { - const cols: usize, const rows: usize = if (i % 2 == 0) - .{ args.cols * 2, args.rows * 2 } - else - .{ args.cols, args.rows }; - - try t.screen.resizeWithoutReflow(@intCast(rows), @intCast(cols)); - } -} diff --git a/src/bench/screen-copy.sh b/src/bench/screen-copy.sh deleted file mode 100755 index 1bb505d63..000000000 --- a/src/bench/screen-copy.sh +++ /dev/null @@ -1,14 +0,0 @@ -#!/usr/bin/env bash -# -# Uncomment to test with an active terminal state. -# ARGS=" --terminal" - -hyperfine \ - --warmup 10 \ - -n new \ - "./zig-out/bin/bench-screen-copy --mode=new${ARGS}" \ - -n new-pooled \ - "./zig-out/bin/bench-screen-copy --mode=new-pooled${ARGS}" \ - -n old \ - "./zig-out/bin/bench-screen-copy --mode=old${ARGS}" - diff --git a/src/bench/screen-copy.zig b/src/bench/screen-copy.zig deleted file mode 100644 index 15cc76658..000000000 --- a/src/bench/screen-copy.zig +++ /dev/null @@ -1,134 +0,0 @@ -//! This benchmark tests the speed of copying the active area of the screen. - -const std = @import("std"); -const assert = std.debug.assert; -const Allocator = std.mem.Allocator; -const ArenaAllocator = std.heap.ArenaAllocator; -const cli = @import("../cli.zig"); -const terminal = @import("../terminal-old/main.zig"); -const terminal_new = @import("../terminal/main.zig"); - -const Args = struct { - mode: Mode = .old, - - /// The number of times to loop. - count: usize = 2500, - - /// Rows and cols in the terminal. - rows: usize = 100, - cols: usize = 300, - - /// This is set by the CLI parser for deinit. - _arena: ?ArenaAllocator = null, - - pub fn deinit(self: *Args) void { - if (self._arena) |arena| arena.deinit(); - self.* = undefined; - } -}; - -const Mode = enum { - /// The default allocation strategy of the structure. - old, - - /// Use a memory pool to allocate pages from a backing buffer. - new, - @"new-pooled", -}; - -pub const std_options: std.Options = .{ - .log_level = .debug, -}; - -pub fn main() !void { - // We want to use the c allocator because it is much faster than GPA. - const alloc = std.heap.c_allocator; - - // Parse our args - var args: Args = .{}; - defer args.deinit(); - { - var iter = try std.process.argsWithAllocator(alloc); - defer iter.deinit(); - try cli.args.parse(Args, alloc, &args, &iter); - } - - // Handle the modes that do not depend on terminal state first. - switch (args.mode) { - .old => { - var t = try terminal.Terminal.init(alloc, args.cols, args.rows); - defer t.deinit(alloc); - try benchOld(alloc, &t, args); - }, - - .new => { - var t = try terminal_new.Terminal.init(alloc, .{ - .cols = @intCast(args.cols), - .rows = @intCast(args.rows), - }); - defer t.deinit(alloc); - try benchNew(alloc, &t, args); - }, - - .@"new-pooled" => { - var t = try terminal_new.Terminal.init(alloc, .{ - .cols = @intCast(args.cols), - .rows = @intCast(args.rows), - }); - defer t.deinit(alloc); - try benchNewPooled(alloc, &t, args); - }, - } -} - -noinline fn benchOld(alloc: Allocator, t: *terminal.Terminal, args: Args) !void { - // We fill the terminal with letters. - for (0..args.rows) |row| { - for (0..args.cols) |col| { - t.setCursorPos(row + 1, col + 1); - try t.print('A'); - } - } - - for (0..args.count) |_| { - var s = try t.screen.clone( - alloc, - .{ .active = 0 }, - .{ .active = t.rows - 1 }, - ); - errdefer s.deinit(); - } -} - -noinline fn benchNew(alloc: Allocator, t: *terminal_new.Terminal, args: Args) !void { - // We fill the terminal with letters. - for (0..args.rows) |row| { - for (0..args.cols) |col| { - t.setCursorPos(row + 1, col + 1); - try t.print('A'); - } - } - - for (0..args.count) |_| { - var s = try t.screen.clone(alloc, .{ .active = .{} }, null); - errdefer s.deinit(); - } -} - -noinline fn benchNewPooled(alloc: Allocator, t: *terminal_new.Terminal, args: Args) !void { - // We fill the terminal with letters. - for (0..args.rows) |row| { - for (0..args.cols) |col| { - t.setCursorPos(row + 1, col + 1); - try t.print('A'); - } - } - - var pool = try terminal_new.PageList.MemoryPool.init(alloc, std.heap.page_allocator, 4); - defer pool.deinit(); - - for (0..args.count) |_| { - var s = try t.screen.clonePool(alloc, &pool, .{ .active = .{} }, null); - errdefer s.deinit(); - } -} diff --git a/src/bench/stream-new.sh b/src/bench/stream-new.sh deleted file mode 100755 index b3d7058a1..000000000 --- a/src/bench/stream-new.sh +++ /dev/null @@ -1,31 +0,0 @@ -#!/usr/bin/env bash -# -# This is a trivial helper script to help run the stream benchmark. -# You probably want to tweak this script depending on what you're -# trying to measure. - -# Options: -# - "ascii", uniform random ASCII bytes -# - "utf8", uniform random unicode characters, encoded as utf8 -# - "rand", pure random data, will contain many invalid code sequences. -DATA="ascii" -SIZE="25000000" - -# Uncomment to test with an active terminal state. -# ARGS=" --terminal" - -# Generate the benchmark input ahead of time so it's not included in the time. -./zig-out/bin/bench-stream --mode=gen-$DATA | head -c $SIZE > /tmp/ghostty_bench_data - -# Uncomment to instead use the contents of `stream.txt` as input. (Ignores SIZE) -# echo $(cat ./stream.txt) > /tmp/ghostty_bench_data - -hyperfine \ - --warmup 10 \ - -n noop \ - "./zig-out/bin/bench-stream --mode=noop = 0) @bitCast(args.seed) else @truncate(@as(u128, @bitCast(std.time.nanoTimestamp()))); // Handle the modes that do not depend on terminal state first. @@ -122,29 +106,13 @@ pub fn main() !void { inline .scalar, .simd, => |tag| switch (args.terminal) { - .old => { - const TerminalStream = terminal.Stream(*TerminalHandler); - var t = try terminal.Terminal.init( - alloc, - args.@"terminal-cols", - args.@"terminal-rows", - ); - var handler: TerminalHandler = .{ .t = &t }; - var stream: TerminalStream = .{ .handler = &handler }; - switch (tag) { - .scalar => try benchScalar(reader, &stream, buf), - .simd => try benchSimd(reader, &stream, buf), - else => @compileError("missing case"), - } - }, - .new => { - const TerminalStream = terminal.Stream(*NewTerminalHandler); - var t = try terminalnew.Terminal.init(alloc, .{ + const TerminalStream = terminal.Stream(*TerminalHandler); + var t = try terminal.Terminal.init(alloc, .{ .cols = @intCast(args.@"terminal-cols"), .rows = @intCast(args.@"terminal-rows"), }); - var handler: NewTerminalHandler = .{ .t = &t }; + var handler: TerminalHandler = .{ .t = &t }; var stream: TerminalStream = .{ .handler = &handler }; switch (tag) { .scalar => try benchScalar(reader, &stream, buf), @@ -278,11 +246,3 @@ const TerminalHandler = struct { try self.t.print(cp); } }; - -const NewTerminalHandler = struct { - t: *terminalnew.Terminal, - - pub fn print(self: *NewTerminalHandler, cp: u21) !void { - try self.t.print(cp); - } -}; diff --git a/src/bench/vt-insert-lines.sh b/src/bench/vt-insert-lines.sh deleted file mode 100755 index 5c19712cc..000000000 --- a/src/bench/vt-insert-lines.sh +++ /dev/null @@ -1,12 +0,0 @@ -#!/usr/bin/env bash -# -# Uncomment to test with an active terminal state. -# ARGS=" --terminal" - -hyperfine \ - --warmup 10 \ - -n new \ - "./zig-out/bin/bench-vt-insert-lines --mode=new${ARGS}" \ - -n old \ - "./zig-out/bin/bench-vt-insert-lines --mode=old${ARGS}" - diff --git a/src/bench/vt-insert-lines.zig b/src/bench/vt-insert-lines.zig deleted file mode 100644 index d61d5354d..000000000 --- a/src/bench/vt-insert-lines.zig +++ /dev/null @@ -1,104 +0,0 @@ -//! This benchmark tests the speed of the "insertLines" operation on a terminal. - -const std = @import("std"); -const assert = std.debug.assert; -const Allocator = std.mem.Allocator; -const ArenaAllocator = std.heap.ArenaAllocator; -const cli = @import("../cli.zig"); -const terminal = @import("../terminal-old/main.zig"); -const terminal_new = @import("../terminal/main.zig"); - -const Args = struct { - mode: Mode = .old, - - /// The number of times to loop. - count: usize = 15_000, - - /// Rows and cols in the terminal. - rows: usize = 100, - cols: usize = 300, - - /// This is set by the CLI parser for deinit. - _arena: ?ArenaAllocator = null, - - pub fn deinit(self: *Args) void { - if (self._arena) |arena| arena.deinit(); - self.* = undefined; - } -}; - -const Mode = enum { - /// The default allocation strategy of the structure. - old, - - /// Use a memory pool to allocate pages from a backing buffer. - new, -}; - -pub const std_options: std.Options = .{ - .log_level = .debug, -}; - -pub fn main() !void { - // We want to use the c allocator because it is much faster than GPA. - const alloc = std.heap.c_allocator; - - // Parse our args - var args: Args = .{}; - defer args.deinit(); - { - var iter = try std.process.argsWithAllocator(alloc); - defer iter.deinit(); - try cli.args.parse(Args, alloc, &args, &iter); - } - - // Handle the modes that do not depend on terminal state first. - switch (args.mode) { - .old => { - var t = try terminal.Terminal.init(alloc, args.cols, args.rows); - defer t.deinit(alloc); - try benchOld(&t, args); - }, - - .new => { - var t = try terminal_new.Terminal.init(alloc, .{ - .cols = @intCast(args.cols), - .rows = @intCast(args.rows), - }); - defer t.deinit(alloc); - try benchNew(&t, args); - }, - } -} - -noinline fn benchOld(t: *terminal.Terminal, args: Args) !void { - // We fill the terminal with letters. - for (0..args.rows) |row| { - for (0..args.cols) |col| { - t.setCursorPos(row + 1, col + 1); - try t.print('A'); - } - } - - for (0..args.count) |_| { - for (0..args.rows) |i| { - _ = try t.insertLines(i); - } - } -} - -noinline fn benchNew(t: *terminal_new.Terminal, args: Args) !void { - // We fill the terminal with letters. - for (0..args.rows) |row| { - for (0..args.cols) |col| { - t.setCursorPos(row + 1, col + 1); - try t.print('A'); - } - } - - for (0..args.count) |_| { - for (0..args.rows) |i| { - _ = t.insertLines(i); - } - } -} diff --git a/src/build_config.zig b/src/build_config.zig index c894917b9..742a2b692 100644 --- a/src/build_config.zig +++ b/src/build_config.zig @@ -144,7 +144,4 @@ pub const ExeEntrypoint = enum { bench_codepoint_width, bench_grapheme_break, bench_page_init, - bench_resize, - bench_screen_copy, - bench_vt_insert_lines, }; diff --git a/src/main.zig b/src/main.zig index 1b83e24d0..3a5357471 100644 --- a/src/main.zig +++ b/src/main.zig @@ -11,7 +11,4 @@ pub usingnamespace switch (build_config.exe_entrypoint) { .bench_codepoint_width => @import("bench/codepoint-width.zig"), .bench_grapheme_break => @import("bench/grapheme-break.zig"), .bench_page_init => @import("bench/page-init.zig"), - .bench_resize => @import("bench/resize.zig"), - .bench_screen_copy => @import("bench/screen-copy.zig"), - .bench_vt_insert_lines => @import("bench/vt-insert-lines.zig"), }; diff --git a/src/main_ghostty.zig b/src/main_ghostty.zig index 497631f31..73e771a7c 100644 --- a/src/main_ghostty.zig +++ b/src/main_ghostty.zig @@ -309,7 +309,6 @@ test { _ = @import("segmented_pool.zig"); _ = @import("inspector/main.zig"); _ = @import("terminal/main.zig"); - _ = @import("terminal-old/main.zig"); _ = @import("terminfo/main.zig"); _ = @import("simd/main.zig"); _ = @import("unicode/main.zig"); diff --git a/src/terminal-old/Parser.zig b/src/terminal-old/Parser.zig deleted file mode 100644 index f160619e2..000000000 --- a/src/terminal-old/Parser.zig +++ /dev/null @@ -1,794 +0,0 @@ -//! VT-series parser for escape and control sequences. -//! -//! This is implemented directly as the state machine described on -//! vt100.net: https://vt100.net/emu/dec_ansi_parser -const Parser = @This(); - -const std = @import("std"); -const builtin = @import("builtin"); -const testing = std.testing; -const table = @import("parse_table.zig").table; -const osc = @import("osc.zig"); - -const log = std.log.scoped(.parser); - -/// States for the state machine -pub const State = enum { - ground, - escape, - escape_intermediate, - csi_entry, - csi_intermediate, - csi_param, - csi_ignore, - dcs_entry, - dcs_param, - dcs_intermediate, - dcs_passthrough, - dcs_ignore, - osc_string, - sos_pm_apc_string, -}; - -/// Transition action is an action that can be taken during a state -/// transition. This is more of an internal action, not one used by -/// end users, typically. -pub const TransitionAction = enum { - none, - ignore, - print, - execute, - collect, - param, - esc_dispatch, - csi_dispatch, - put, - osc_put, - apc_put, -}; - -/// Action is the action that a caller of the parser is expected to -/// take as a result of some input character. -pub const Action = union(enum) { - pub const Tag = std.meta.FieldEnum(Action); - - /// Draw character to the screen. This is a unicode codepoint. - print: u21, - - /// Execute the C0 or C1 function. - execute: u8, - - /// Execute the CSI command. Note that pointers within this - /// structure are only valid until the next call to "next". - csi_dispatch: CSI, - - /// Execute the ESC command. - esc_dispatch: ESC, - - /// Execute the OSC command. - osc_dispatch: osc.Command, - - /// DCS-related events. - dcs_hook: DCS, - dcs_put: u8, - dcs_unhook: void, - - /// APC data - apc_start: void, - apc_put: u8, - apc_end: void, - - pub const CSI = struct { - intermediates: []u8, - params: []u16, - final: u8, - sep: Sep, - - /// The separator used for CSI params. - pub const Sep = enum { semicolon, colon }; - - // Implement formatter for logging - pub fn format( - self: CSI, - comptime layout: []const u8, - opts: std.fmt.FormatOptions, - writer: anytype, - ) !void { - _ = layout; - _ = opts; - try std.fmt.format(writer, "ESC [ {s} {any} {c}", .{ - self.intermediates, - self.params, - self.final, - }); - } - }; - - pub const ESC = struct { - intermediates: []u8, - final: u8, - - // Implement formatter for logging - pub fn format( - self: ESC, - comptime layout: []const u8, - opts: std.fmt.FormatOptions, - writer: anytype, - ) !void { - _ = layout; - _ = opts; - try std.fmt.format(writer, "ESC {s} {c}", .{ - self.intermediates, - self.final, - }); - } - }; - - pub const DCS = struct { - intermediates: []const u8 = "", - params: []const u16 = &.{}, - final: u8, - }; - - // Implement formatter for logging. This is mostly copied from the - // std.fmt implementation, but we modify it slightly so that we can - // print out custom formats for some of our primitives. - pub fn format( - self: Action, - comptime layout: []const u8, - opts: std.fmt.FormatOptions, - writer: anytype, - ) !void { - _ = layout; - const T = Action; - const info = @typeInfo(T).Union; - - try writer.writeAll(@typeName(T)); - if (info.tag_type) |TagType| { - try writer.writeAll("{ ."); - try writer.writeAll(@tagName(@as(TagType, self))); - try writer.writeAll(" = "); - - inline for (info.fields) |u_field| { - // If this is the active field... - if (self == @field(TagType, u_field.name)) { - const value = @field(self, u_field.name); - switch (@TypeOf(value)) { - // Unicode - u21 => try std.fmt.format(writer, "'{u}' (U+{X})", .{ value, value }), - - // Byte - u8 => try std.fmt.format(writer, "0x{x}", .{value}), - - // Note: we don't do ASCII (u8) because there are a lot - // of invisible characters we don't want to handle right - // now. - - // All others do the default behavior - else => try std.fmt.formatType( - @field(self, u_field.name), - "any", - opts, - writer, - 3, - ), - } - } - } - - try writer.writeAll(" }"); - } else { - try format(writer, "@{x}", .{@intFromPtr(&self)}); - } - } -}; - -/// Keeps track of the parameter sep used for CSI params. We allow colons -/// to be used ONLY by the 'm' CSI action. -pub const ParamSepState = enum(u8) { - none = 0, - semicolon = ';', - colon = ':', - mixed = 1, -}; - -/// Maximum number of intermediate characters during parsing. This is -/// 4 because we also use the intermediates array for UTF8 decoding which -/// can be at most 4 bytes. -const MAX_INTERMEDIATE = 4; -const MAX_PARAMS = 16; - -/// Current state of the state machine -state: State = .ground, - -/// Intermediate tracking. -intermediates: [MAX_INTERMEDIATE]u8 = undefined, -intermediates_idx: u8 = 0, - -/// Param tracking, building -params: [MAX_PARAMS]u16 = undefined, -params_idx: u8 = 0, -params_sep: ParamSepState = .none, -param_acc: u16 = 0, -param_acc_idx: u8 = 0, - -/// Parser for OSC sequences -osc_parser: osc.Parser = .{}, - -pub fn init() Parser { - return .{}; -} - -pub fn deinit(self: *Parser) void { - self.osc_parser.deinit(); -} - -/// Next consumes the next character c and returns the actions to execute. -/// Up to 3 actions may need to be executed -- in order -- representing -/// the state exit, transition, and entry actions. -pub fn next(self: *Parser, c: u8) [3]?Action { - const effect = table[c][@intFromEnum(self.state)]; - - // log.info("next: {x}", .{c}); - - const next_state = effect.state; - const action = effect.action; - - // After generating the actions, we set our next state. - defer self.state = next_state; - - // When going from one state to another, the actions take place in this order: - // - // 1. exit action from old state - // 2. transition action - // 3. entry action to new state - return [3]?Action{ - // Exit depends on current state - if (self.state == next_state) null else switch (self.state) { - .osc_string => if (self.osc_parser.end(c)) |cmd| - Action{ .osc_dispatch = cmd } - else - null, - .dcs_passthrough => Action{ .dcs_unhook = {} }, - .sos_pm_apc_string => Action{ .apc_end = {} }, - else => null, - }, - - self.doAction(action, c), - - // Entry depends on new state - if (self.state == next_state) null else switch (next_state) { - .escape, .dcs_entry, .csi_entry => clear: { - self.clear(); - break :clear null; - }, - .osc_string => osc_string: { - self.osc_parser.reset(); - break :osc_string null; - }, - .dcs_passthrough => Action{ - .dcs_hook = .{ - .intermediates = self.intermediates[0..self.intermediates_idx], - .params = self.params[0..self.params_idx], - .final = c, - }, - }, - .sos_pm_apc_string => Action{ .apc_start = {} }, - else => null, - }, - }; -} - -pub fn collect(self: *Parser, c: u8) void { - if (self.intermediates_idx >= MAX_INTERMEDIATE) { - log.warn("invalid intermediates count", .{}); - return; - } - - self.intermediates[self.intermediates_idx] = c; - self.intermediates_idx += 1; -} - -fn doAction(self: *Parser, action: TransitionAction, c: u8) ?Action { - return switch (action) { - .none, .ignore => null, - .print => Action{ .print = c }, - .execute => Action{ .execute = c }, - .collect => collect: { - self.collect(c); - break :collect null; - }, - .param => param: { - // Semicolon separates parameters. If we encounter a semicolon - // we need to store and move on to the next parameter. - if (c == ';' or c == ':') { - // Ignore too many parameters - if (self.params_idx >= MAX_PARAMS) break :param null; - - // If this is our first time seeing a parameter, we track - // the separator used so that we can't mix separators later. - if (self.params_idx == 0) self.params_sep = @enumFromInt(c); - if (@as(ParamSepState, @enumFromInt(c)) != self.params_sep) self.params_sep = .mixed; - - // Set param final value - self.params[self.params_idx] = self.param_acc; - self.params_idx += 1; - - // Reset current param value to 0 - self.param_acc = 0; - self.param_acc_idx = 0; - break :param null; - } - - // A numeric value. Add it to our accumulator. - if (self.param_acc_idx > 0) { - self.param_acc *|= 10; - } - self.param_acc +|= c - '0'; - - // Increment our accumulator index. If we overflow then - // we're out of bounds and we exit immediately. - self.param_acc_idx, const overflow = @addWithOverflow(self.param_acc_idx, 1); - if (overflow > 0) break :param null; - - // The client is expected to perform no action. - break :param null; - }, - .osc_put => osc_put: { - self.osc_parser.next(c); - break :osc_put null; - }, - .csi_dispatch => csi_dispatch: { - // Ignore too many parameters - if (self.params_idx >= MAX_PARAMS) break :csi_dispatch null; - - // Finalize parameters if we have one - if (self.param_acc_idx > 0) { - self.params[self.params_idx] = self.param_acc; - self.params_idx += 1; - } - - const result: Action = .{ - .csi_dispatch = .{ - .intermediates = self.intermediates[0..self.intermediates_idx], - .params = self.params[0..self.params_idx], - .final = c, - .sep = switch (self.params_sep) { - .none, .semicolon => .semicolon, - .colon => .colon, - - // There is nothing that treats mixed separators specially - // afaik so we just treat it as a semicolon. - .mixed => .semicolon, - }, - }, - }; - - // We only allow colon or mixed separators for the 'm' command. - switch (self.params_sep) { - .none => {}, - .semicolon => {}, - .colon, .mixed => if (c != 'm') { - log.warn( - "CSI colon or mixed separators only allowed for 'm' command, got: {}", - .{result}, - ); - break :csi_dispatch null; - }, - } - - break :csi_dispatch result; - }, - .esc_dispatch => Action{ - .esc_dispatch = .{ - .intermediates = self.intermediates[0..self.intermediates_idx], - .final = c, - }, - }, - .put => Action{ .dcs_put = c }, - .apc_put => Action{ .apc_put = c }, - }; -} - -pub fn clear(self: *Parser) void { - self.intermediates_idx = 0; - self.params_idx = 0; - self.params_sep = .none; - self.param_acc = 0; - self.param_acc_idx = 0; -} - -test { - var p = init(); - _ = p.next(0x9E); - try testing.expect(p.state == .sos_pm_apc_string); - _ = p.next(0x9C); - try testing.expect(p.state == .ground); - - { - const a = p.next('a'); - try testing.expect(p.state == .ground); - try testing.expect(a[0] == null); - try testing.expect(a[1].? == .print); - try testing.expect(a[2] == null); - } - - { - const a = p.next(0x19); - try testing.expect(p.state == .ground); - try testing.expect(a[0] == null); - try testing.expect(a[1].? == .execute); - try testing.expect(a[2] == null); - } -} - -test "esc: ESC ( B" { - var p = init(); - _ = p.next(0x1B); - _ = p.next('('); - - { - const a = p.next('B'); - try testing.expect(p.state == .ground); - try testing.expect(a[0] == null); - try testing.expect(a[1].? == .esc_dispatch); - try testing.expect(a[2] == null); - - const d = a[1].?.esc_dispatch; - try testing.expect(d.final == 'B'); - try testing.expect(d.intermediates.len == 1); - try testing.expect(d.intermediates[0] == '('); - } -} - -test "csi: ESC [ H" { - var p = init(); - _ = p.next(0x1B); - _ = p.next(0x5B); - - { - const a = p.next(0x48); - try testing.expect(p.state == .ground); - try testing.expect(a[0] == null); - try testing.expect(a[1].? == .csi_dispatch); - try testing.expect(a[2] == null); - - const d = a[1].?.csi_dispatch; - try testing.expect(d.final == 0x48); - try testing.expect(d.params.len == 0); - } -} - -test "csi: ESC [ 1 ; 4 H" { - var p = init(); - _ = p.next(0x1B); - _ = p.next(0x5B); - _ = p.next(0x31); // 1 - _ = p.next(0x3B); // ; - _ = p.next(0x34); // 4 - - { - const a = p.next(0x48); // H - try testing.expect(p.state == .ground); - try testing.expect(a[0] == null); - try testing.expect(a[1].? == .csi_dispatch); - try testing.expect(a[2] == null); - - const d = a[1].?.csi_dispatch; - try testing.expect(d.final == 'H'); - try testing.expect(d.params.len == 2); - try testing.expectEqual(@as(u16, 1), d.params[0]); - try testing.expectEqual(@as(u16, 4), d.params[1]); - } -} - -test "csi: SGR ESC [ 38 : 2 m" { - var p = init(); - _ = p.next(0x1B); - _ = p.next('['); - _ = p.next('3'); - _ = p.next('8'); - _ = p.next(':'); - _ = p.next('2'); - - { - const a = p.next('m'); - try testing.expect(p.state == .ground); - try testing.expect(a[0] == null); - try testing.expect(a[1].? == .csi_dispatch); - try testing.expect(a[2] == null); - - const d = a[1].?.csi_dispatch; - try testing.expect(d.final == 'm'); - try testing.expect(d.sep == .colon); - try testing.expect(d.params.len == 2); - try testing.expectEqual(@as(u16, 38), d.params[0]); - try testing.expectEqual(@as(u16, 2), d.params[1]); - } -} - -test "csi: SGR colon followed by semicolon" { - var p = init(); - _ = p.next(0x1B); - for ("[48:2") |c| { - const a = p.next(c); - try testing.expect(a[0] == null); - try testing.expect(a[1] == null); - try testing.expect(a[2] == null); - } - - { - const a = p.next('m'); - try testing.expect(p.state == .ground); - try testing.expect(a[0] == null); - try testing.expect(a[1].? == .csi_dispatch); - try testing.expect(a[2] == null); - } - - _ = p.next(0x1B); - _ = p.next('['); - { - const a = p.next('H'); - try testing.expect(p.state == .ground); - try testing.expect(a[0] == null); - try testing.expect(a[1].? == .csi_dispatch); - try testing.expect(a[2] == null); - } -} - -test "csi: SGR mixed colon and semicolon" { - var p = init(); - _ = p.next(0x1B); - for ("[38:5:1;48:5:0") |c| { - const a = p.next(c); - try testing.expect(a[0] == null); - try testing.expect(a[1] == null); - try testing.expect(a[2] == null); - } - - { - const a = p.next('m'); - try testing.expect(p.state == .ground); - try testing.expect(a[0] == null); - try testing.expect(a[1].? == .csi_dispatch); - try testing.expect(a[2] == null); - } -} - -test "csi: SGR ESC [ 48 : 2 m" { - var p = init(); - _ = p.next(0x1B); - for ("[48:2:240:143:104") |c| { - const a = p.next(c); - try testing.expect(a[0] == null); - try testing.expect(a[1] == null); - try testing.expect(a[2] == null); - } - - { - const a = p.next('m'); - try testing.expect(p.state == .ground); - try testing.expect(a[0] == null); - try testing.expect(a[1].? == .csi_dispatch); - try testing.expect(a[2] == null); - - const d = a[1].?.csi_dispatch; - try testing.expect(d.final == 'm'); - try testing.expect(d.sep == .colon); - try testing.expect(d.params.len == 5); - try testing.expectEqual(@as(u16, 48), d.params[0]); - try testing.expectEqual(@as(u16, 2), d.params[1]); - try testing.expectEqual(@as(u16, 240), d.params[2]); - try testing.expectEqual(@as(u16, 143), d.params[3]); - try testing.expectEqual(@as(u16, 104), d.params[4]); - } -} - -test "csi: SGR ESC [4:3m colon" { - var p = init(); - _ = p.next(0x1B); - _ = p.next('['); - _ = p.next('4'); - _ = p.next(':'); - _ = p.next('3'); - - { - const a = p.next('m'); - try testing.expect(p.state == .ground); - try testing.expect(a[0] == null); - try testing.expect(a[1].? == .csi_dispatch); - try testing.expect(a[2] == null); - - const d = a[1].?.csi_dispatch; - try testing.expect(d.final == 'm'); - try testing.expect(d.sep == .colon); - try testing.expect(d.params.len == 2); - try testing.expectEqual(@as(u16, 4), d.params[0]); - try testing.expectEqual(@as(u16, 3), d.params[1]); - } -} - -test "csi: SGR with many blank and colon" { - var p = init(); - _ = p.next(0x1B); - for ("[58:2::240:143:104") |c| { - const a = p.next(c); - try testing.expect(a[0] == null); - try testing.expect(a[1] == null); - try testing.expect(a[2] == null); - } - - { - const a = p.next('m'); - try testing.expect(p.state == .ground); - try testing.expect(a[0] == null); - try testing.expect(a[1].? == .csi_dispatch); - try testing.expect(a[2] == null); - - const d = a[1].?.csi_dispatch; - try testing.expect(d.final == 'm'); - try testing.expect(d.sep == .colon); - try testing.expect(d.params.len == 6); - try testing.expectEqual(@as(u16, 58), d.params[0]); - try testing.expectEqual(@as(u16, 2), d.params[1]); - try testing.expectEqual(@as(u16, 0), d.params[2]); - try testing.expectEqual(@as(u16, 240), d.params[3]); - try testing.expectEqual(@as(u16, 143), d.params[4]); - try testing.expectEqual(@as(u16, 104), d.params[5]); - } -} - -test "csi: colon for non-m final" { - var p = init(); - _ = p.next(0x1B); - for ("[38:2h") |c| { - const a = p.next(c); - try testing.expect(a[0] == null); - try testing.expect(a[1] == null); - try testing.expect(a[2] == null); - } - - try testing.expect(p.state == .ground); -} - -test "csi: request mode decrqm" { - var p = init(); - _ = p.next(0x1B); - for ("[?2026$") |c| { - const a = p.next(c); - try testing.expect(a[0] == null); - try testing.expect(a[1] == null); - try testing.expect(a[2] == null); - } - - { - const a = p.next('p'); - try testing.expect(p.state == .ground); - try testing.expect(a[0] == null); - try testing.expect(a[1].? == .csi_dispatch); - try testing.expect(a[2] == null); - - const d = a[1].?.csi_dispatch; - try testing.expect(d.final == 'p'); - try testing.expectEqual(@as(usize, 2), d.intermediates.len); - try testing.expectEqual(@as(usize, 1), d.params.len); - try testing.expectEqual(@as(u16, '?'), d.intermediates[0]); - try testing.expectEqual(@as(u16, '$'), d.intermediates[1]); - try testing.expectEqual(@as(u16, 2026), d.params[0]); - } -} - -test "csi: change cursor" { - var p = init(); - _ = p.next(0x1B); - for ("[3 ") |c| { - const a = p.next(c); - try testing.expect(a[0] == null); - try testing.expect(a[1] == null); - try testing.expect(a[2] == null); - } - - { - const a = p.next('q'); - try testing.expect(p.state == .ground); - try testing.expect(a[0] == null); - try testing.expect(a[1].? == .csi_dispatch); - try testing.expect(a[2] == null); - - const d = a[1].?.csi_dispatch; - try testing.expect(d.final == 'q'); - try testing.expectEqual(@as(usize, 1), d.intermediates.len); - try testing.expectEqual(@as(usize, 1), d.params.len); - try testing.expectEqual(@as(u16, ' '), d.intermediates[0]); - try testing.expectEqual(@as(u16, 3), d.params[0]); - } -} - -test "osc: change window title" { - var p = init(); - _ = p.next(0x1B); - _ = p.next(']'); - _ = p.next('0'); - _ = p.next(';'); - _ = p.next('a'); - _ = p.next('b'); - _ = p.next('c'); - - { - const a = p.next(0x07); // BEL - try testing.expect(p.state == .ground); - try testing.expect(a[0].? == .osc_dispatch); - try testing.expect(a[1] == null); - try testing.expect(a[2] == null); - - const cmd = a[0].?.osc_dispatch; - try testing.expect(cmd == .change_window_title); - try testing.expectEqualStrings("abc", cmd.change_window_title); - } -} - -test "osc: change window title (end in esc)" { - var p = init(); - _ = p.next(0x1B); - _ = p.next(']'); - _ = p.next('0'); - _ = p.next(';'); - _ = p.next('a'); - _ = p.next('b'); - _ = p.next('c'); - - { - const a = p.next(0x1B); - _ = p.next('\\'); - try testing.expect(p.state == .ground); - try testing.expect(a[0].? == .osc_dispatch); - try testing.expect(a[1] == null); - try testing.expect(a[2] == null); - - const cmd = a[0].?.osc_dispatch; - try testing.expect(cmd == .change_window_title); - try testing.expectEqualStrings("abc", cmd.change_window_title); - } -} - -// https://github.com/darrenstarr/VtNetCore/pull/14 -// Saw this on HN, decided to add a test case because why not. -test "osc: 112 incomplete sequence" { - var p = init(); - _ = p.next(0x1B); - _ = p.next(']'); - _ = p.next('1'); - _ = p.next('1'); - _ = p.next('2'); - - { - const a = p.next(0x07); - try testing.expect(p.state == .ground); - try testing.expect(a[0].? == .osc_dispatch); - try testing.expect(a[1] == null); - try testing.expect(a[2] == null); - - const cmd = a[0].?.osc_dispatch; - try testing.expect(cmd == .reset_color); - try testing.expectEqual(cmd.reset_color.kind, .cursor); - } -} - -test "csi: too many params" { - var p = init(); - _ = p.next(0x1B); - _ = p.next('['); - for (0..100) |_| { - _ = p.next('1'); - _ = p.next(';'); - } - _ = p.next('1'); - - { - const a = p.next('C'); - try testing.expect(p.state == .ground); - try testing.expect(a[0] == null); - try testing.expect(a[1] == null); - try testing.expect(a[2] == null); - } -} diff --git a/src/terminal-old/Screen.zig b/src/terminal-old/Screen.zig deleted file mode 100644 index 917857128..000000000 --- a/src/terminal-old/Screen.zig +++ /dev/null @@ -1,7925 +0,0 @@ -//! Screen represents the internal storage for a terminal screen, including -//! scrollback. This is implemented as a single continuous ring buffer. -//! -//! Definitions: -//! -//! * Screen - The full screen (active + history). -//! * Active - The area that is the current edit-able screen (the -//! bottom of the scrollback). This is "edit-able" because it is -//! the only part that escape sequences such as set cursor position -//! actually affect. -//! * History - The area that contains the lines prior to the active -//! area. This is the scrollback area. Escape sequences can no longer -//! affect this area. -//! * Viewport - The area that is currently visible to the user. This -//! can be thought of as the current window into the screen. -//! * Row - A single visible row in the screen. -//! * Line - A single line of text. This may map to multiple rows if -//! the row is soft-wrapped. -//! -//! The internal storage of the screen is stored in a circular buffer -//! with roughly the following format: -//! -//! Storage (Circular Buffer) -//! ┌─────────────────────────────────────┐ -//! │ ┌─────┐┌─────┐┌─────┐ ┌─────┐ │ -//! │ │ Hdr ││Cell ││Cell │ ... │Cell │ │ -//! │ │ ││ 0 ││ 1 │ │ N-1 │ │ -//! │ └─────┘└─────┘└─────┘ └─────┘ │ -//! │ ┌─────┐┌─────┐┌─────┐ ┌─────┐ │ -//! │ │ Hdr ││Cell ││Cell │ ... │Cell │ │ -//! │ │ ││ 0 ││ 1 │ │ N-1 │ │ -//! │ └─────┘└─────┘└─────┘ └─────┘ │ -//! │ ┌─────┐┌─────┐┌─────┐ ┌─────┐ │ -//! │ │ Hdr ││Cell ││Cell │ ... │Cell │ │ -//! │ │ ││ 0 ││ 1 │ │ N-1 │ │ -//! │ └─────┘└─────┘└─────┘ └─────┘ │ -//! └─────────────────────────────────────┘ -//! -//! There are R rows with N columns. Each row has an extra "cell" which is -//! the row header. The row header is used to track metadata about the row. -//! Each cell itself is a union (see StorageCell) of either the header or -//! the cell. -//! -//! The storage is in a circular buffer so that scrollback can be handled -//! without copying rows. The circular buffer is implemented in circ_buf.zig. -//! The top of the circular buffer (index 0) is the top of the screen, -//! i.e. the scrollback if there is a lot of data. -//! -//! The top of the active area (or end of the history area, same thing) is -//! cached in `self.history` and is an offset in rows. This could always be -//! calculated but profiling showed that caching it saves a lot of time in -//! hot loops for minimal memory cost. -const Screen = @This(); - -const std = @import("std"); -const builtin = @import("builtin"); -const assert = std.debug.assert; -const Allocator = std.mem.Allocator; - -const ziglyph = @import("ziglyph"); -const ansi = @import("ansi.zig"); -const modes = @import("modes.zig"); -const sgr = @import("sgr.zig"); -const color = @import("color.zig"); -const kitty = @import("kitty.zig"); -const point = @import("point.zig"); -const CircBuf = @import("../circ_buf.zig").CircBuf; -const Selection = @import("Selection.zig"); -const StringMap = @import("StringMap.zig"); -const fastmem = @import("../fastmem.zig"); -const charsets = @import("charsets.zig"); - -const log = std.log.scoped(.screen); - -/// State required for all charset operations. -const CharsetState = struct { - /// The list of graphical charsets by slot - charsets: CharsetArray = CharsetArray.initFill(charsets.Charset.utf8), - - /// GL is the slot to use when using a 7-bit printable char (up to 127) - /// GR used for 8-bit printable chars. - gl: charsets.Slots = .G0, - gr: charsets.Slots = .G2, - - /// Single shift where a slot is used for exactly one char. - single_shift: ?charsets.Slots = null, - - /// An array to map a charset slot to a lookup table. - const CharsetArray = std.EnumArray(charsets.Slots, charsets.Charset); -}; - -/// Cursor represents the cursor state. -pub const Cursor = struct { - /// x, y where the cursor currently exists (0-indexed). This x/y is - /// always the offset in the active area. - x: usize = 0, - y: usize = 0, - - /// The visual style of the cursor. This defaults to block because - /// it has to default to something, but users of this struct are - /// encouraged to set their own default. - style: Style = .block, - - /// pen is the current cell styling to apply to new cells. - pen: Cell = .{ .char = 0 }, - - /// The last column flag (LCF) used to do soft wrapping. - pending_wrap: bool = false, - - /// The visual style of the cursor. Whether or not it blinks - /// is determined by mode 12 (modes.zig). This mode is synchronized - /// with CSI q, the same as xterm. - pub const Style = enum { bar, block, underline }; - - /// Saved cursor state. This contains more than just Cursor members - /// because additional state is stored. - pub const Saved = struct { - x: usize, - y: usize, - pen: Cell, - pending_wrap: bool, - origin: bool, - charset: CharsetState, - }; -}; - -/// This is a single item within the storage buffer. We use a union to -/// have different types of data in a single contiguous buffer. -const StorageCell = union { - header: RowHeader, - cell: Cell, - - test { - // log.warn("header={}@{} cell={}@{} storage={}@{}", .{ - // @sizeOf(RowHeader), - // @alignOf(RowHeader), - // @sizeOf(Cell), - // @alignOf(Cell), - // @sizeOf(StorageCell), - // @alignOf(StorageCell), - // }); - } - - comptime { - // We only check this during ReleaseFast because safety checks - // have to be disabled to get this size. - if (!std.debug.runtime_safety) { - // We want to be at most the size of a cell always. We have WAY - // more cells than other fields, so we don't want to pay the cost - // of padding due to other fields. - assert(@sizeOf(Cell) == @sizeOf(StorageCell)); - } else { - // Extra u32 for the tag for safety checks. This is subject to - // change depending on the Zig compiler... - assert((@sizeOf(Cell) + @sizeOf(u32)) == @sizeOf(StorageCell)); - } - } -}; - -/// The row header is at the start of every row within the storage buffer. -/// It can store row-specific data. -pub const RowHeader = struct { - pub const Id = u32; - - /// The ID of this row, used to uniquely identify this row. The cells - /// are also ID'd by id + cell index (0-indexed). This will wrap around - /// when it reaches the maximum value for the type. For caching purposes, - /// when wrapping happens, all rows in the screen will be marked dirty. - id: Id = 0, - - // Packed flags - flags: packed struct { - /// If true, this row is soft-wrapped. The first cell of the next - /// row is a continuous of this row. - wrap: bool = false, - - /// True if this row has had changes. It is up to the caller to - /// set this to false. See the methods on Row to see what will set - /// this to true. - dirty: bool = false, - - /// True if any cell in this row has a grapheme associated with it. - grapheme: bool = false, - - /// True if this row is an active prompt (awaiting input). This is - /// set to false when the semantic prompt events (OSC 133) are received. - /// There are scenarios where the shell may never send this event, so - /// in order to reliably test prompt status, you need to iterate - /// backwards from the cursor to check the current line status going - /// back. - semantic_prompt: SemanticPrompt = .unknown, - } = .{}, - - /// Semantic prompt type. - pub const SemanticPrompt = enum(u3) { - /// Unknown, the running application didn't tell us for this line. - unknown = 0, - - /// This is a prompt line, meaning it only contains the shell prompt. - /// For poorly behaving shells, this may also be the input. - prompt = 1, - prompt_continuation = 2, - - /// This line contains the input area. We don't currently track - /// where this actually is in the line, so we just assume it is somewhere. - input = 3, - - /// This line is the start of command output. - command = 4, - - /// True if this is a prompt or input line. - pub fn promptOrInput(self: SemanticPrompt) bool { - return self == .prompt or self == .prompt_continuation or self == .input; - } - }; -}; - -/// The color associated with a single cell's foreground or background. -const CellColor = union(enum) { - none, - indexed: u8, - rgb: color.RGB, - - pub fn eql(self: CellColor, other: CellColor) bool { - return switch (self) { - .none => other == .none, - .indexed => |i| switch (other) { - .indexed => other.indexed == i, - else => false, - }, - .rgb => |rgb| switch (other) { - .rgb => other.rgb.eql(rgb), - else => false, - }, - }; - } -}; - -/// Cell is a single cell within the screen. -pub const Cell = struct { - /// The primary unicode codepoint for this cell. Most cells (almost all) - /// contain exactly one unicode codepoint. However, it is possible for - /// cells to contain multiple if multiple codepoints are used to create - /// a single grapheme cluster. - /// - /// In the case multiple codepoints make up a single grapheme, the - /// additional codepoints can be looked up in the hash map on the - /// Screen. Since multi-codepoints graphemes are rare, we don't want to - /// waste memory for every cell, so we use a side lookup for it. - char: u32 = 0, - - /// Foreground and background color. - fg: CellColor = .none, - bg: CellColor = .none, - - /// Underline color. - /// NOTE(mitchellh): This is very rarely set so ideally we wouldn't waste - /// cell space for this. For now its on this struct because it is convenient - /// but we should consider a lookaside table for this. - underline_fg: color.RGB = .{}, - - /// On/off attributes that can be set - attrs: packed struct { - bold: bool = false, - italic: bool = false, - faint: bool = false, - blink: bool = false, - inverse: bool = false, - invisible: bool = false, - strikethrough: bool = false, - underline: sgr.Attribute.Underline = .none, - underline_color: bool = false, - protected: bool = false, - - /// True if this is a wide character. This char takes up - /// two cells. The following cell ALWAYS is a space. - wide: bool = false, - - /// Notes that this only exists to be blank for a preceding - /// wide character (tail) or following (head). - wide_spacer_tail: bool = false, - wide_spacer_head: bool = false, - - /// True if this cell has additional codepoints to form a complete - /// grapheme cluster. If this is true, then the row grapheme flag must - /// also be true. The grapheme code points can be looked up in the - /// screen grapheme map. - grapheme: bool = false, - - /// Returns only the attributes related to style. - pub fn styleAttrs(self: @This()) @This() { - var copy = self; - copy.wide = false; - copy.wide_spacer_tail = false; - copy.wide_spacer_head = false; - copy.grapheme = false; - return copy; - } - } = .{}, - - /// True if the cell should be skipped for drawing - pub fn empty(self: Cell) bool { - // Get our backing integer for our packed struct of attributes - const AttrInt = @Type(.{ .Int = .{ - .signedness = .unsigned, - .bits = @bitSizeOf(@TypeOf(self.attrs)), - } }); - - // We're empty if we have no char AND we have no styling - return self.char == 0 and - self.fg == .none and - self.bg == .none and - @as(AttrInt, @bitCast(self.attrs)) == 0; - } - - /// The width of the cell. - /// - /// This uses the legacy calculation of a per-codepoint width calculation - /// to determine the width. This legacy calculation is incorrect because - /// it doesn't take into account multi-codepoint graphemes. - /// - /// The goal of this function is to match the expectation of shells - /// that aren't grapheme aware (at the time of writing this comment: none - /// are grapheme aware). This means it should match wcswidth. - pub fn widthLegacy(self: Cell) u8 { - // Wide is always 2 - if (self.attrs.wide) return 2; - - // Wide spacers are always 0 because their width is accounted for - // in the wide char. - if (self.attrs.wide_spacer_tail or self.attrs.wide_spacer_head) return 0; - - return 1; - } - - test "widthLegacy" { - const testing = std.testing; - - var c: Cell = .{}; - try testing.expectEqual(@as(u16, 1), c.widthLegacy()); - - c = .{ .attrs = .{ .wide = true } }; - try testing.expectEqual(@as(u16, 2), c.widthLegacy()); - - c = .{ .attrs = .{ .wide_spacer_tail = true } }; - try testing.expectEqual(@as(u16, 0), c.widthLegacy()); - } - - test { - // We use this test to ensure we always get the right size of the attrs - // const cell: Cell = .{ .char = 0 }; - // _ = @bitCast(u8, cell.attrs); - // try std.testing.expectEqual(1, @sizeOf(@TypeOf(cell.attrs))); - } - - test { - //log.warn("CELL={} bits={} {}", .{ @sizeOf(Cell), @bitSizeOf(Cell), @alignOf(Cell) }); - try std.testing.expectEqual(20, @sizeOf(Cell)); - } -}; - -/// A row is a single row in the screen. -pub const Row = struct { - /// The screen this row is part of. - screen: *Screen, - - /// Raw internal storage, do NOT write to this, use only the - /// helpers. Writing directly to this can easily mess up state - /// causing future crashes or misrendering. - storage: []StorageCell, - - /// Returns the ID for this row. You can turn this into a cell ID - /// by adding the cell offset plus 1 (so it is 1-indexed). - pub inline fn getId(self: Row) RowHeader.Id { - return self.storage[0].header.id; - } - - /// Set that this row is soft-wrapped. This doesn't change the contents - /// of this row so the row won't be marked dirty. - pub fn setWrapped(self: Row, v: bool) void { - self.storage[0].header.flags.wrap = v; - } - - /// Set a row as dirty or not. Generally you only set a row as NOT dirty. - /// Various Row functions manage flagging dirty to true. - pub fn setDirty(self: Row, v: bool) void { - self.storage[0].header.flags.dirty = v; - } - - pub inline fn isDirty(self: Row) bool { - return self.storage[0].header.flags.dirty; - } - - pub inline fn isWrapped(self: Row) bool { - return self.storage[0].header.flags.wrap; - } - - /// Set the semantic prompt state for this row. - pub fn setSemanticPrompt(self: Row, p: RowHeader.SemanticPrompt) void { - self.storage[0].header.flags.semantic_prompt = p; - } - - /// Retrieve the semantic prompt state for this row. - pub fn getSemanticPrompt(self: Row) RowHeader.SemanticPrompt { - return self.storage[0].header.flags.semantic_prompt; - } - - /// Retrieve the header for this row. - pub fn header(self: Row) RowHeader { - return self.storage[0].header; - } - - /// Returns the number of cells in this row. - pub fn lenCells(self: Row) usize { - return self.storage.len - 1; - } - - /// Returns true if the row only has empty characters. This ignores - /// styling (i.e. styling does not count as non-empty). - pub fn isEmpty(self: Row) bool { - const len = self.storage.len; - for (self.storage[1..len]) |cell| { - if (cell.cell.char != 0) return false; - } - - return true; - } - - /// Clear the row, making all cells empty. - pub fn clear(self: Row, pen: Cell) void { - var empty_pen = pen; - empty_pen.char = 0; - self.fill(empty_pen); - } - - /// Fill the entire row with a copy of a single cell. - pub fn fill(self: Row, cell: Cell) void { - self.fillSlice(cell, 0, self.storage.len - 1); - } - - /// Fill a slice of a row. - pub fn fillSlice(self: Row, cell: Cell, start: usize, len: usize) void { - assert(len <= self.storage.len - 1); - assert(!cell.attrs.grapheme); // you can't fill with graphemes - - // Always mark the row as dirty for this. - self.storage[0].header.flags.dirty = true; - - // If our row has no graphemes, then this is a fast copy - if (!self.storage[0].header.flags.grapheme) { - @memset(self.storage[start + 1 .. len + 1], .{ .cell = cell }); - return; - } - - // We have graphemes, so we have to clear those first. - for (self.storage[start + 1 .. len + 1], 0..) |*storage_cell, x| { - if (storage_cell.cell.attrs.grapheme) self.clearGraphemes(x); - storage_cell.* = .{ .cell = cell }; - } - - // We only reset the grapheme flag if we fill the whole row, for now. - // We can improve performance by more correctly setting this but I'm - // going to defer that until we can measure. - if (start == 0 and len == self.storage.len - 1) { - self.storage[0].header.flags.grapheme = false; - } - } - - /// Get a single immutable cell. - pub fn getCell(self: Row, x: usize) Cell { - assert(x < self.storage.len - 1); - return self.storage[x + 1].cell; - } - - /// Get a pointr to the cell at column x (0-indexed). This always - /// assumes that the cell was modified, notifying the renderer on the - /// next call to re-render this cell. Any change detection to avoid - /// this should be done prior. - pub fn getCellPtr(self: Row, x: usize) *Cell { - assert(x < self.storage.len - 1); - - // Always mark the row as dirty for this. - self.storage[0].header.flags.dirty = true; - - return &self.storage[x + 1].cell; - } - - /// Attach a grapheme codepoint to the given cell. - pub fn attachGrapheme(self: Row, x: usize, cp: u21) !void { - assert(x < self.storage.len - 1); - - const cell = &self.storage[x + 1].cell; - const key = self.getId() + x + 1; - const gop = try self.screen.graphemes.getOrPut(self.screen.alloc, key); - errdefer if (!gop.found_existing) { - _ = self.screen.graphemes.remove(key); - }; - - // Our row now has a grapheme - self.storage[0].header.flags.grapheme = true; - - // Our row is now dirty - self.storage[0].header.flags.dirty = true; - - // If we weren't previously a grapheme and we found an existing value - // it means that it is old grapheme data. Just delete that. - if (!cell.attrs.grapheme and gop.found_existing) { - cell.attrs.grapheme = true; - gop.value_ptr.deinit(self.screen.alloc); - gop.value_ptr.* = .{ .one = cp }; - return; - } - - // If we didn't have a previous value, attach the single codepoint. - if (!gop.found_existing) { - cell.attrs.grapheme = true; - gop.value_ptr.* = .{ .one = cp }; - return; - } - - // We have an existing value, promote - assert(cell.attrs.grapheme); - try gop.value_ptr.append(self.screen.alloc, cp); - } - - /// Removes all graphemes associated with a cell. - pub fn clearGraphemes(self: Row, x: usize) void { - assert(x < self.storage.len - 1); - - // Our row is now dirty - self.storage[0].header.flags.dirty = true; - - const cell = &self.storage[x + 1].cell; - const key = self.getId() + x + 1; - cell.attrs.grapheme = false; - if (self.screen.graphemes.fetchRemove(key)) |kv| { - kv.value.deinit(self.screen.alloc); - } - } - - /// Copy a single cell from column x in src to column x in this row. - pub fn copyCell(self: Row, src: Row, x: usize) !void { - const dst_cell = self.getCellPtr(x); - const src_cell = src.getCellPtr(x); - - // If our destination has graphemes, we have to clear them. - if (dst_cell.attrs.grapheme) self.clearGraphemes(x); - dst_cell.* = src_cell.*; - - // If the source doesn't have any graphemes, then we can just copy. - if (!src_cell.attrs.grapheme) return; - - // Source cell has graphemes. Copy them. - const src_key = src.getId() + x + 1; - const src_data = src.screen.graphemes.get(src_key) orelse return; - const dst_key = self.getId() + x + 1; - const dst_gop = try self.screen.graphemes.getOrPut(self.screen.alloc, dst_key); - dst_gop.value_ptr.* = try src_data.copy(self.screen.alloc); - self.storage[0].header.flags.grapheme = true; - } - - /// Copy the row src into this row. The row can be from another screen. - pub fn copyRow(self: Row, src: Row) !void { - // If we have graphemes, clear first to unset them. - if (self.storage[0].header.flags.grapheme) self.clear(.{}); - - // Copy the flags - self.storage[0].header.flags = src.storage[0].header.flags; - - // Always mark the row as dirty for this. - self.storage[0].header.flags.dirty = true; - - // If the source has no graphemes (likely) then this is fast. - const end = @min(src.storage.len, self.storage.len); - if (!src.storage[0].header.flags.grapheme) { - fastmem.copy(StorageCell, self.storage[1..], src.storage[1..end]); - return; - } - - // Source has graphemes, this is slow. - for (src.storage[1..end], 0..) |storage, x| { - self.storage[x + 1] = .{ .cell = storage.cell }; - - // Copy grapheme data if it exists - if (storage.cell.attrs.grapheme) { - const src_key = src.getId() + x + 1; - const src_data = src.screen.graphemes.get(src_key) orelse continue; - - const dst_key = self.getId() + x + 1; - const dst_gop = try self.screen.graphemes.getOrPut(self.screen.alloc, dst_key); - dst_gop.value_ptr.* = try src_data.copy(self.screen.alloc); - - self.storage[0].header.flags.grapheme = true; - } - } - } - - /// Read-only iterator for the cells in the row. - pub fn cellIterator(self: Row) CellIterator { - return .{ .row = self }; - } - - /// Returns the number of codepoints in the cell at column x, - /// including the primary codepoint. - pub fn codepointLen(self: Row, x: usize) usize { - var it = self.codepointIterator(x); - return it.len() + 1; - } - - /// Read-only iterator for the grapheme codepoints in a cell. This only - /// iterates over the EXTRA GRAPHEME codepoints and not the primary - /// codepoint in cell.char. - pub fn codepointIterator(self: Row, x: usize) CodepointIterator { - const cell = &self.storage[x + 1].cell; - if (!cell.attrs.grapheme) return .{ .data = .{ .zero = {} } }; - - const key = self.getId() + x + 1; - const data: GraphemeData = self.screen.graphemes.get(key) orelse data: { - // This is probably a bug somewhere in our internal state, - // but we don't want to just hard crash so its easier to just - // have zero codepoints. - log.debug("cell with grapheme flag but no grapheme data", .{}); - break :data .{ .zero = {} }; - }; - return .{ .data = data }; - } - - /// Returns true if this cell is the end of a grapheme cluster. - /// - /// NOTE: If/when "real" grapheme cluster support is in then - /// this will be removed because every cell will represent exactly - /// one grapheme cluster. - pub fn graphemeBreak(self: Row, x: usize) bool { - const cell = &self.storage[x + 1].cell; - - // Right now, if we are a grapheme, we only store ZWJs on - // the grapheme data so that means we can't be a break. - if (cell.attrs.grapheme) return false; - - // If we are a tail then we check our prior cell. - if (cell.attrs.wide_spacer_tail and x > 0) { - return self.graphemeBreak(x - 1); - } - - // If we are a wide char, then we have to check our prior cell. - if (cell.attrs.wide and x > 0) { - return self.graphemeBreak(x - 1); - } - - return true; - } -}; - -/// Used to iterate through the rows of a specific region. -pub const RowIterator = struct { - screen: *Screen, - tag: RowIndexTag, - max: usize, - value: usize = 0, - - pub fn next(self: *RowIterator) ?Row { - if (self.value >= self.max) return null; - const idx = self.tag.index(self.value); - const res = self.screen.getRow(idx); - self.value += 1; - return res; - } -}; - -/// Used to iterate through the rows of a specific region. -pub const CellIterator = struct { - row: Row, - i: usize = 0, - - pub fn next(self: *CellIterator) ?Cell { - if (self.i >= self.row.storage.len - 1) return null; - const res = self.row.storage[self.i + 1].cell; - self.i += 1; - return res; - } -}; - -/// Used to iterate through the codepoints of a cell. This only iterates -/// over the extra grapheme codepoints and not the primary codepoint. -pub const CodepointIterator = struct { - data: GraphemeData, - i: usize = 0, - - /// Returns the number of codepoints in the iterator. - pub fn len(self: CodepointIterator) usize { - switch (self.data) { - .zero => return 0, - .one => return 1, - .two => return 2, - .three => return 3, - .four => return 4, - .many => |v| return v.len, - } - } - - pub fn next(self: *CodepointIterator) ?u21 { - switch (self.data) { - .zero => return null, - - .one => |v| { - if (self.i >= 1) return null; - self.i += 1; - return v; - }, - - .two => |v| { - if (self.i >= v.len) return null; - defer self.i += 1; - return v[self.i]; - }, - - .three => |v| { - if (self.i >= v.len) return null; - defer self.i += 1; - return v[self.i]; - }, - - .four => |v| { - if (self.i >= v.len) return null; - defer self.i += 1; - return v[self.i]; - }, - - .many => |v| { - if (self.i >= v.len) return null; - defer self.i += 1; - return v[self.i]; - }, - } - } - - pub fn reset(self: *CodepointIterator) void { - self.i = 0; - } -}; - -/// RowIndex represents a row within the screen. There are various meanings -/// of a row index and this union represents the available types. For example, -/// when talking about row "0" you may want the first row in the viewport, -/// the first row in the scrollback, or the first row in the active area. -/// -/// All row indexes are 0-indexed. -pub const RowIndex = union(RowIndexTag) { - /// The index is from the top of the screen. The screen includes all - /// the history. - screen: usize, - - /// The index is from the top of the viewport. Therefore, depending - /// on where the user has scrolled the viewport, "0" is different. - viewport: usize, - - /// The index is from the top of the active area. The active area is - /// always "rows" tall, and 0 is the top row. The active area is the - /// "edit-able" area where the terminal cursor is. - active: usize, - - /// The index is from the top of the history (scrollback) to just - /// prior to the active area. - history: usize, - - /// Convert this row index into a screen offset. This will validate - /// the value so even if it is already a screen value, this may error. - pub fn toScreen(self: RowIndex, screen: *const Screen) RowIndex { - const y = switch (self) { - .screen => |y| y: { - // NOTE for this and others below: Zig is supposed to optimize - // away assert in releasefast but for some reason these were - // not being optimized away. I don't know why. For these asserts - // only, I comptime gate them. - if (std.debug.runtime_safety) assert(y < RowIndexTag.screen.maxLen(screen)); - break :y y; - }, - - .viewport => |y| y: { - if (std.debug.runtime_safety) assert(y < RowIndexTag.viewport.maxLen(screen)); - break :y y + screen.viewport; - }, - - .active => |y| y: { - if (std.debug.runtime_safety) assert(y < RowIndexTag.active.maxLen(screen)); - break :y screen.history + y; - }, - - .history => |y| y: { - if (std.debug.runtime_safety) assert(y < RowIndexTag.history.maxLen(screen)); - break :y y; - }, - }; - - return .{ .screen = y }; - } -}; - -/// The tags of RowIndex -pub const RowIndexTag = enum { - screen, - viewport, - active, - history, - - /// The max length for a given tag. This is a length, not an index, - /// so it is 1-indexed. If the value is zero, it means that this - /// section of the screen is empty or disabled. - pub inline fn maxLen(self: RowIndexTag, screen: *const Screen) usize { - return switch (self) { - // Screen can be any of the written rows - .screen => screen.rowsWritten(), - - // Viewport can be any of the written rows or the max size - // of a viewport. - .viewport => @max(1, @min(screen.rows, screen.rowsWritten())), - - // History is all the way up to the top of our active area. If - // we haven't filled our active area, there is no history. - .history => screen.history, - - // Active area can be any number of rows. We ignore rows - // written here because this is the only row index that can - // actively grow our rows. - .active => screen.rows, - //TODO .active => @min(rows_written, screen.rows), - }; - } - - /// Construct a RowIndex from a tag. - pub fn index(self: RowIndexTag, value: usize) RowIndex { - return switch (self) { - .screen => .{ .screen = value }, - .viewport => .{ .viewport = value }, - .active => .{ .active = value }, - .history => .{ .history = value }, - }; - } -}; - -/// Stores the extra unicode codepoints that form a complete grapheme -/// cluster alongside a cell. We store this separately from a Cell because -/// grapheme clusters are relatively rare (depending on the language) and -/// we don't want to pay for the full cost all the time. -pub const GraphemeData = union(enum) { - // The named counts allow us to avoid allocators. We do this because - // []u21 is sizeof([4]u21) anyways so if we can store avoid small allocations - // we prefer it. Grapheme clusters are almost always <= 4 codepoints. - - zero: void, - one: u21, - two: [2]u21, - three: [3]u21, - four: [4]u21, - many: []u21, - - pub fn deinit(self: GraphemeData, alloc: Allocator) void { - switch (self) { - .many => |v| alloc.free(v), - else => {}, - } - } - - /// Append the codepoint cp to the grapheme data. - pub fn append(self: *GraphemeData, alloc: Allocator, cp: u21) !void { - switch (self.*) { - .zero => self.* = .{ .one = cp }, - .one => |v| self.* = .{ .two = .{ v, cp } }, - .two => |v| self.* = .{ .three = .{ v[0], v[1], cp } }, - .three => |v| self.* = .{ .four = .{ v[0], v[1], v[2], cp } }, - .four => |v| { - const many = try alloc.alloc(u21, 5); - fastmem.copy(u21, many, &v); - many[4] = cp; - self.* = .{ .many = many }; - }, - - .many => |v| { - // Note: this is super inefficient, we should use an arraylist - // or something so we have extra capacity. - const many = try alloc.realloc(v, v.len + 1); - many[v.len] = cp; - self.* = .{ .many = many }; - }, - } - } - - pub fn copy(self: GraphemeData, alloc: Allocator) !GraphemeData { - // If we're not many we're not allocated so just copy on stack. - if (self != .many) return self; - - // Heap allocated - return GraphemeData{ .many = try alloc.dupe(u21, self.many) }; - } - - test { - log.warn("Grapheme={}", .{@sizeOf(GraphemeData)}); - } - - test "append" { - const testing = std.testing; - const alloc = testing.allocator; - - var data: GraphemeData = .{ .one = 1 }; - defer data.deinit(alloc); - - try data.append(alloc, 2); - try testing.expectEqual(GraphemeData{ .two = .{ 1, 2 } }, data); - try data.append(alloc, 3); - try testing.expectEqual(GraphemeData{ .three = .{ 1, 2, 3 } }, data); - try data.append(alloc, 4); - try testing.expectEqual(GraphemeData{ .four = .{ 1, 2, 3, 4 } }, data); - try data.append(alloc, 5); - try testing.expect(data == .many); - try testing.expectEqualSlices(u21, &[_]u21{ 1, 2, 3, 4, 5 }, data.many); - try data.append(alloc, 6); - try testing.expect(data == .many); - try testing.expectEqualSlices(u21, &[_]u21{ 1, 2, 3, 4, 5, 6 }, data.many); - } - - comptime { - // We want to keep this at most the size of the tag + []u21 so that - // at most we're paying for the cost of a slice. - //assert(@sizeOf(GraphemeData) == 24); - } -}; - -/// A line represents a line of text, potentially across soft-wrapped -/// boundaries. This differs from row, which is a single physical row within -/// the terminal screen. -pub const Line = struct { - screen: *Screen, - tag: RowIndexTag, - start: usize, - len: usize, - - /// Return the string for this line. - pub fn string(self: *const Line, alloc: Allocator) ![:0]const u8 { - return try self.screen.selectionString(alloc, self.selection(), true); - } - - /// Receive the string for this line along with the byte-to-point mapping. - pub fn stringMap(self: *const Line, alloc: Allocator) !StringMap { - return try self.screen.selectionStringMap(alloc, self.selection()); - } - - /// Return a selection that covers the entire line. - pub fn selection(self: *const Line) Selection { - // Get the start and end screen point. - const start_idx = self.tag.index(self.start).toScreen(self.screen).screen; - const end_idx = self.tag.index(self.start + (self.len - 1)).toScreen(self.screen).screen; - - // Convert the start and end screen points into a selection across - // the entire rows. We then use selectionString because it handles - // unwrapping, graphemes, etc. - return .{ - .start = .{ .y = start_idx, .x = 0 }, - .end = .{ .y = end_idx, .x = self.screen.cols - 1 }, - }; - } -}; - -/// Iterator over textual lines within the terminal. This will unwrap -/// wrapped lines and consider them a single line. -pub const LineIterator = struct { - row_it: RowIterator, - - pub fn next(self: *LineIterator) ?Line { - const start = self.row_it.value; - - // Get our current row - var row = self.row_it.next() orelse return null; - var len: usize = 1; - - // While the row is wrapped we keep iterating over the rows - // and incrementing the length. - while (row.isWrapped()) { - // Note: this orelse shouldn't happen. A wrapped row should - // always have a next row. However, this isn't the place where - // we want to assert that. - row = self.row_it.next() orelse break; - len += 1; - } - - return .{ - .screen = self.row_it.screen, - .tag = self.row_it.tag, - .start = start, - .len = len, - }; - } -}; - -// Initialize to header and not a cell so that we can check header.init -// to know if the remainder of the row has been initialized or not. -const StorageBuf = CircBuf(StorageCell, .{ .header = .{} }); - -/// Stores a mapping of cell ID (row ID + cell offset + 1) to -/// graphemes associated with a cell. To know if a cell has graphemes, -/// check the "grapheme" flag of a cell. -const GraphemeMap = std.AutoHashMapUnmanaged(usize, GraphemeData); - -/// The allocator used for all the storage operations -alloc: Allocator, - -/// The full set of storage. -storage: StorageBuf, - -/// Graphemes associated with our current screen. -graphemes: GraphemeMap = .{}, - -/// The next ID to assign to a row. The value of this is NOT assigned. -next_row_id: RowHeader.Id = 1, - -/// The number of rows and columns in the visible space. -rows: usize, -cols: usize, - -/// The maximum number of lines that are available in scrollback. This -/// is in addition to the number of visible rows. -max_scrollback: usize, - -/// The row (offset from the top) where the viewport currently is. -viewport: usize, - -/// The amount of history (scrollback) that has been written so far. This -/// can be calculated dynamically using the storage buffer but its an -/// extremely hot piece of data so we cache it. Empirically this eliminates -/// millions of function calls and saves seconds under high scroll scenarios -/// (i.e. reading a large file). -history: usize, - -/// Each screen maintains its own cursor state. -cursor: Cursor = .{}, - -/// Saved cursor saved with DECSC (ESC 7). -saved_cursor: ?Cursor.Saved = null, - -/// The selection for this screen (if any). -selection: ?Selection = null, - -/// The kitty keyboard settings. -kitty_keyboard: kitty.KeyFlagStack = .{}, - -/// Kitty graphics protocol state. -kitty_images: kitty.graphics.ImageStorage = .{}, - -/// The charset state -charset: CharsetState = .{}, - -/// The current or most recent protected mode. Once a protection mode is -/// set, this will never become "off" again until the screen is reset. -/// The current state of whether protection attributes should be set is -/// set on the Cell pen; this is only used to determine the most recent -/// protection mode since some sequences such as ECH depend on this. -protected_mode: ansi.ProtectedMode = .off, - -/// Initialize a new screen. -pub fn init( - alloc: Allocator, - rows: usize, - cols: usize, - max_scrollback: usize, -) !Screen { - // * Our buffer size is preallocated to fit double our visible space - // or the maximum scrollback whichever is smaller. - // * We add +1 to cols to fit the row header - const buf_size = (rows + @min(max_scrollback, rows)) * (cols + 1); - - return Screen{ - .alloc = alloc, - .storage = try StorageBuf.init(alloc, buf_size), - .rows = rows, - .cols = cols, - .max_scrollback = max_scrollback, - .viewport = 0, - .history = 0, - }; -} - -pub fn deinit(self: *Screen) void { - self.kitty_images.deinit(self.alloc); - self.storage.deinit(self.alloc); - self.deinitGraphemes(); -} - -fn deinitGraphemes(self: *Screen) void { - var grapheme_it = self.graphemes.valueIterator(); - while (grapheme_it.next()) |data| data.deinit(self.alloc); - self.graphemes.deinit(self.alloc); -} - -/// Copy the screen portion given by top and bottom into a new screen instance. -/// This clone is meant for read-only access and hasn't been tested for -/// mutability. -pub fn clone(self: *Screen, alloc: Allocator, top: RowIndex, bottom: RowIndex) !Screen { - // Convert our top/bottom to screen coordinates - const top_y = top.toScreen(self).screen; - const bot_y = bottom.toScreen(self).screen; - assert(bot_y >= top_y); - const height = (bot_y - top_y) + 1; - - // We also figure out the "max y" we can have based on the number - // of rows written. This is used to prevent from reading out of the - // circular buffer where we might have no initialized data yet. - const max_y = max_y: { - const rows_written = self.rowsWritten(); - const index = RowIndex{ .active = @min(rows_written -| 1, self.rows - 1) }; - break :max_y index.toScreen(self).screen; - }; - - // The "real" Y value we use is whichever is smaller: the bottom - // requested or the max. This prevents from reading zero data. - // The "real" height is the amount of height of data we can actually - // copy. - const real_y = @min(bot_y, max_y); - const real_height = (real_y - top_y) + 1; - //log.warn("bot={} max={} top={} real={}", .{ bot_y, max_y, top_y, real_y }); - - // Init a new screen that exactly fits the height. The height is the - // non-real value because we still want the requested height by the - // caller. - var result = try init(alloc, height, self.cols, 0); - errdefer result.deinit(); - - // Copy some data - result.cursor = self.cursor; - - // Get the pointer to our source buffer - const len = real_height * (self.cols + 1); - const src = self.storage.getPtrSlice(top_y * (self.cols + 1), len); - - // Get a direct pointer into our storage buffer. This should always be - // one slice because we created a perfectly fitting buffer. - const dst = result.storage.getPtrSlice(0, len); - assert(dst[1].len == 0); - - // Perform the copy - // std.log.warn("copy bytes={}", .{src[0].len + src[1].len}); - fastmem.copy(StorageCell, dst[0], src[0]); - fastmem.copy(StorageCell, dst[0][src[0].len..], src[1]); - - // If there are graphemes, we just copy them all - if (self.graphemes.count() > 0) { - // Clone the map - const graphemes = try self.graphemes.clone(alloc); - - // Go through all the values and clone the data because it MAY - // (rarely) be allocated. - var it = graphemes.iterator(); - while (it.next()) |kv| { - kv.value_ptr.* = try kv.value_ptr.copy(alloc); - } - - result.graphemes = graphemes; - } - - return result; -} - -/// Returns true if the viewport is scrolled to the bottom of the screen. -pub fn viewportIsBottom(self: Screen) bool { - return self.viewport == self.history; -} - -/// Shortcut for getRow followed by getCell as a quick way to read a cell. -/// This is particularly useful for quickly reading the cell under a cursor -/// with `getCell(.active, cursor.y, cursor.x)`. -pub fn getCell(self: *Screen, tag: RowIndexTag, y: usize, x: usize) Cell { - return self.getRow(tag.index(y)).getCell(x); -} - -/// Shortcut for getRow followed by getCellPtr as a quick way to read a cell. -pub fn getCellPtr(self: *Screen, tag: RowIndexTag, y: usize, x: usize) *Cell { - return self.getRow(tag.index(y)).getCellPtr(x); -} - -/// Returns an iterator that can be used to iterate over all of the rows -/// from index zero of the given row index type. This can therefore iterate -/// from row 0 of the active area, history, viewport, etc. -pub fn rowIterator(self: *Screen, tag: RowIndexTag) RowIterator { - return .{ - .screen = self, - .tag = tag, - .max = tag.maxLen(self), - }; -} - -/// Returns an iterator that iterates over the lines of the screen. A line -/// is a single line of text which may wrap across multiple rows. A row -/// is a single physical row of the terminal. -pub fn lineIterator(self: *Screen, tag: RowIndexTag) LineIterator { - return .{ .row_it = self.rowIterator(tag) }; -} - -/// Returns the line that contains the given point. This may be null if the -/// point is outside the screen. -pub fn getLine(self: *Screen, pt: point.ScreenPoint) ?Line { - // If our y is outside of our written area, we have no line. - if (pt.y >= RowIndexTag.screen.maxLen(self)) return null; - if (pt.x >= self.cols) return null; - - // Find the starting y. We go back and as soon as we find a row that - // isn't wrapped, we know the NEXT line is the one we want. - const start_y: usize = if (pt.y == 0) 0 else start_y: { - for (1..pt.y) |y| { - const bot_y = pt.y - y; - const row = self.getRow(.{ .screen = bot_y }); - if (!row.isWrapped()) break :start_y bot_y + 1; - } - - break :start_y 0; - }; - - // Find the end y, which is the first row that isn't wrapped. - const end_y = end_y: { - for (pt.y..self.rowsWritten()) |y| { - const row = self.getRow(.{ .screen = y }); - if (!row.isWrapped()) break :end_y y; - } - - break :end_y self.rowsWritten() - 1; - }; - - return .{ - .screen = self, - .tag = .screen, - .start = start_y, - .len = (end_y - start_y) + 1, - }; -} - -/// Returns the row at the given index. This row is writable, although -/// only the active area should probably be written to. -pub fn getRow(self: *Screen, index: RowIndex) Row { - // Get our offset into storage - const offset = index.toScreen(self).screen * (self.cols + 1); - - // Get the slices into the storage. This should never wrap because - // we're perfectly aligned on row boundaries. - const slices = self.storage.getPtrSlice(offset, self.cols + 1); - assert(slices[0].len == self.cols + 1 and slices[1].len == 0); - - const row: Row = .{ .screen = self, .storage = slices[0] }; - if (row.storage[0].header.id == 0) { - const Id = @TypeOf(self.next_row_id); - const id = self.next_row_id; - self.next_row_id +%= @as(Id, @intCast(self.cols)); - - // Store the header - row.storage[0].header.id = id; - - // We only set dirty and fill if its not dirty. If its dirty - // we assume this row has been written but just hasn't had - // an ID assigned yet. - if (!row.storage[0].header.flags.dirty) { - // Mark that we're dirty since we're a new row - row.storage[0].header.flags.dirty = true; - - // We only need to fill with runtime safety because unions are - // tag-checked. Otherwise, the default value of zero will be valid. - if (std.debug.runtime_safety) row.fill(.{}); - } - } - return row; -} - -/// Copy the row at src to dst. -pub fn copyRow(self: *Screen, dst: RowIndex, src: RowIndex) !void { - // One day we can make this more efficient but for now - // we do the easy thing. - const dst_row = self.getRow(dst); - const src_row = self.getRow(src); - try dst_row.copyRow(src_row); -} - -/// Scroll rows in a region up. Rows that go beyond the region -/// top or bottom are deleted, and new rows inserted are blank according -/// to the current pen. -/// -/// This does NOT create any new scrollback. This modifies an existing -/// region within the screen (including possibly the scrollback if -/// the top/bottom are within it). -/// -/// This can be used to implement terminal scroll regions efficiently. -pub fn scrollRegionUp(self: *Screen, top: RowIndex, bottom: RowIndex, count_req: usize) void { - // Avoid a lot of work if we're doing nothing. - if (count_req == 0) return; - - // Convert our top/bottom to screen y values. This is the y offset - // in the entire screen buffer. - const top_y = top.toScreen(self).screen; - const bot_y = bottom.toScreen(self).screen; - - // If top is outside of the range of bot, we do nothing. - if (top_y >= bot_y) return; - - // We can only scroll up to the number of rows in the region. The "+ 1" - // is because our y values are 0-based and count is 1-based. - const count = @min(count_req, bot_y - top_y + 1); - - // Get the storage pointer for the full scroll region. We're going to - // be modifying the whole thing so we get it right away. - const height = (bot_y - top_y) + 1; - const len = height * (self.cols + 1); - const slices = self.storage.getPtrSlice(top_y * (self.cols + 1), len); - - // The total amount we're going to copy - const total_copy = (height - count) * (self.cols + 1); - - // The pen we'll use for new cells (only the BG attribute is applied to new - // cells) - const pen: Cell = switch (self.cursor.pen.bg) { - .none => .{}, - else => |bg| .{ .bg = bg }, - }; - - // Fast-path is that we have a contiguous buffer in our circular buffer. - // In this case we can do some memmoves. - if (slices[1].len == 0) { - const buf = slices[0]; - - { - // Our copy starts "count" rows below and is the length of - // the remainder of the data. Our destination is the top since - // we're scrolling up. - // - // Note we do NOT need to set any row headers to dirty because - // the row contents are not changing for the row ID. - const dst = buf; - const src_offset = count * (self.cols + 1); - const src = buf[src_offset..]; - assert(@intFromPtr(dst.ptr) < @intFromPtr(src.ptr)); - fastmem.move(StorageCell, dst, src); - } - - { - // Copy in our empties. The destination is the bottom - // count rows. We first fill with the pen values since there - // is a lot more of that. - const dst_offset = total_copy; - const dst = buf[dst_offset..]; - @memset(dst, .{ .cell = pen }); - - // Then we make sure our row headers are zeroed out. We set - // the value to a dirty row header so that the renderer re-draws. - // - // NOTE: we do NOT set a valid row ID here. The next time getRow - // is called it will be initialized. This should work fine as - // far as I can tell. It is important to set dirty so that the - // renderer knows to redraw this. - var i: usize = dst_offset; - while (i < buf.len) : (i += self.cols + 1) { - buf[i] = .{ .header = .{ - .flags = .{ .dirty = true }, - } }; - } - } - - return; - } - - // If we're split across two buffers this is a "slow" path. This shouldn't - // happen with the "active" area but it appears it does... in the future - // I plan on changing scroll region stuff to make it much faster so for - // now we just deal with this slow path. - - // This is the offset where we have to start copying. - const src_offset = count * (self.cols + 1); - - // Perform the copy and calculate where we need to start zero-ing. - const zero_offset: [2]usize = if (src_offset < slices[0].len) zero_offset: { - var remaining: usize = len; - - // Source starts in the top... so we can copy some from there. - const dst = slices[0]; - const src = slices[0][src_offset..]; - assert(@intFromPtr(dst.ptr) < @intFromPtr(src.ptr)); - fastmem.move(StorageCell, dst, src); - remaining = total_copy - src.len; - if (remaining == 0) break :zero_offset .{ src.len, 0 }; - - // We have data remaining, which means that we have to grab some - // from the bottom slice. - const dst2 = slices[0][src.len..]; - const src2_len = @min(dst2.len, remaining); - const src2 = slices[1][0..src2_len]; - fastmem.copy(StorageCell, dst2, src2); - remaining -= src2_len; - if (remaining == 0) break :zero_offset .{ src.len + src2.len, 0 }; - - // We still have data remaining, which means we copy into the bot. - const dst3 = slices[1]; - const src3 = slices[1][src2_len .. src2_len + remaining]; - fastmem.move(StorageCell, dst3, src3); - - break :zero_offset .{ slices[0].len, src3.len }; - } else zero_offset: { - var remaining: usize = len; - - // Source is in the bottom, so we copy from there into top. - const bot_src_offset = src_offset - slices[0].len; - const dst = slices[0]; - const src = slices[1][bot_src_offset..]; - const src_len = @min(dst.len, src.len); - fastmem.copy(StorageCell, dst, src[0..src_len]); - remaining = total_copy - src_len; - if (remaining == 0) break :zero_offset .{ src_len, 0 }; - - // We have data remaining, this has to go into the bottom. - const dst2 = slices[1]; - const src2_offset = bot_src_offset + src_len; - const src2 = slices[1][src2_offset..]; - const src2_len = remaining; - fastmem.move(StorageCell, dst2, src2[0..src2_len]); - break :zero_offset .{ src_len, src2_len }; - }; - - // Zero - for (zero_offset, 0..) |offset, i| { - if (offset >= slices[i].len) continue; - - const dst = slices[i][offset..]; - @memset(dst, .{ .cell = pen }); - - var j: usize = offset; - while (j < slices[i].len) : (j += self.cols + 1) { - slices[i][j] = .{ .header = .{ - .flags = .{ .dirty = true }, - } }; - } - } -} - -/// Returns the offset into the storage buffer that the given row can -/// be found. This assumes valid input and will crash if the input is -/// invalid. -fn rowOffset(self: Screen, index: RowIndex) usize { - // +1 for row header - return index.toScreen(&self).screen * (self.cols + 1); -} - -/// Returns the number of rows that have actually been written to the -/// screen. This assumes a row is "written" if getRow was ever called -/// on the row. -fn rowsWritten(self: Screen) usize { - // The number of rows we've actually written into our buffer - // This should always be cleanly divisible since we only request - // data in row chunks from the buffer. - assert(@mod(self.storage.len(), self.cols + 1) == 0); - return self.storage.len() / (self.cols + 1); -} - -/// The number of rows our backing storage supports. This should -/// always be self.rows but we use the backing storage as a source of truth. -fn rowsCapacity(self: Screen) usize { - assert(@mod(self.storage.capacity(), self.cols + 1) == 0); - return self.storage.capacity() / (self.cols + 1); -} - -/// The maximum possible capacity of the underlying buffer if we reached -/// the max scrollback. -fn maxCapacity(self: Screen) usize { - return (self.rows + self.max_scrollback) * (self.cols + 1); -} - -pub const ClearMode = enum { - /// Delete all history. This will also move the viewport area to the top - /// so that the viewport area never contains history. This does NOT - /// change the active area. - history, - - /// Clear all the lines above the cursor in the active area. This does - /// not touch history. - above_cursor, -}; - -/// Clear the screen contents according to the given mode. -pub fn clear(self: *Screen, mode: ClearMode) !void { - switch (mode) { - .history => { - // If there is no history, do nothing. - if (self.history == 0) return; - - // Delete all our history - self.storage.deleteOldest(self.history * (self.cols + 1)); - self.history = 0; - - // Back to the top - self.viewport = 0; - }, - - .above_cursor => { - // First we copy all the rows from our cursor down to the top - // of the active area. - var y: usize = self.cursor.y; - const y_max = @min(self.rows, self.rowsWritten()) - 1; - const copy_n = (y_max - y) + 1; - while (y <= y_max) : (y += 1) { - const dst_y = y - self.cursor.y; - const dst = self.getRow(.{ .active = dst_y }); - const src = self.getRow(.{ .active = y }); - try dst.copyRow(src); - } - - // Next we want to clear all the rows below the copied amount. - y = copy_n; - while (y <= y_max) : (y += 1) { - const dst = self.getRow(.{ .active = y }); - dst.clear(.{}); - } - - // Move our cursor to the top - self.cursor.y = 0; - - // Scroll to the top of the viewport - self.viewport = self.history; - }, - } -} - -/// Return the selection for all contents on the screen. Surrounding -/// whitespace is omitted. If there is no selection, this returns null. -pub fn selectAll(self: *Screen) ?Selection { - const whitespace = &[_]u32{ 0, ' ', '\t' }; - const y_max = self.rowsWritten() - 1; - - const start: point.ScreenPoint = start: { - var y: usize = 0; - while (y <= y_max) : (y += 1) { - const current_row = self.getRow(.{ .screen = y }); - var x: usize = 0; - while (x < self.cols) : (x += 1) { - const cell = current_row.getCell(x); - - // Empty is whitespace - if (cell.empty()) continue; - - // Non-empty means we found it. - const this_whitespace = std.mem.indexOfAny( - u32, - whitespace, - &[_]u32{cell.char}, - ) != null; - if (this_whitespace) continue; - - break :start .{ .x = x, .y = y }; - } - } - - // There is no start point and therefore no line that can be selected. - return null; - }; - - const end: point.ScreenPoint = end: { - var y: usize = y_max; - while (true) { - const current_row = self.getRow(.{ .screen = y }); - - var x: usize = 0; - while (x < self.cols) : (x += 1) { - const real_x = self.cols - x - 1; - const cell = current_row.getCell(real_x); - - // Empty or whitespace, ignore. - if (cell.empty()) continue; - const this_whitespace = std.mem.indexOfAny( - u32, - whitespace, - &[_]u32{cell.char}, - ) != null; - if (this_whitespace) continue; - - // Got it - break :end .{ .x = real_x, .y = y }; - } - - if (y == 0) break; - y -= 1; - } - }; - - return Selection{ - .start = start, - .end = end, - }; -} - -/// Select the line under the given point. This will select across soft-wrapped -/// lines and will omit the leading and trailing whitespace. If the point is -/// over whitespace but the line has non-whitespace characters elsewhere, the -/// line will be selected. -pub fn selectLine(self: *Screen, pt: point.ScreenPoint) ?Selection { - // Whitespace characters for selection purposes - const whitespace = &[_]u32{ 0, ' ', '\t' }; - - // Impossible to select anything outside of the area we've written. - const y_max = self.rowsWritten() - 1; - if (pt.y > y_max or pt.x >= self.cols) return null; - - // Get the current point semantic prompt state since that determines - // boundary conditions too. This makes it so that line selection can - // only happen within the same prompt state. For example, if you triple - // click output, but the shell uses spaces to soft-wrap to the prompt - // then the selection will stop prior to the prompt. See issue #1329. - const semantic_prompt_state = self.getRow(.{ .screen = pt.y }) - .getSemanticPrompt() - .promptOrInput(); - - // The real start of the row is the first row in the soft-wrap. - const start_row: usize = start_row: { - if (pt.y == 0) break :start_row 0; - - var y: usize = pt.y - 1; - while (true) { - const current = self.getRow(.{ .screen = y }); - if (!current.header().flags.wrap) break :start_row y + 1; - - // See semantic_prompt_state comment for why - const current_prompt = current.getSemanticPrompt().promptOrInput(); - if (current_prompt != semantic_prompt_state) break :start_row y + 1; - - if (y == 0) break :start_row y; - y -= 1; - } - unreachable; - }; - - // The real end of the row is the final row in the soft-wrap. - const end_row: usize = end_row: { - var y: usize = pt.y; - while (y <= y_max) : (y += 1) { - const current = self.getRow(.{ .screen = y }); - - // See semantic_prompt_state comment for why - const current_prompt = current.getSemanticPrompt().promptOrInput(); - if (current_prompt != semantic_prompt_state) break :end_row y - 1; - - // End of the screen or not wrapped, we're done. - if (y == y_max or !current.header().flags.wrap) break :end_row y; - } - unreachable; - }; - - // Go forward from the start to find the first non-whitespace character. - const start: point.ScreenPoint = start: { - var y: usize = start_row; - while (y <= y_max) : (y += 1) { - const current_row = self.getRow(.{ .screen = y }); - var x: usize = 0; - while (x < self.cols) : (x += 1) { - const cell = current_row.getCell(x); - - // Empty is whitespace - if (cell.empty()) continue; - - // Non-empty means we found it. - const this_whitespace = std.mem.indexOfAny( - u32, - whitespace, - &[_]u32{cell.char}, - ) != null; - if (this_whitespace) continue; - - break :start .{ .x = x, .y = y }; - } - } - - // There is no start point and therefore no line that can be selected. - return null; - }; - - // Go backward from the end to find the first non-whitespace character. - const end: point.ScreenPoint = end: { - var y: usize = end_row; - while (true) { - const current_row = self.getRow(.{ .screen = y }); - - var x: usize = 0; - while (x < self.cols) : (x += 1) { - const real_x = self.cols - x - 1; - const cell = current_row.getCell(real_x); - - // Empty or whitespace, ignore. - if (cell.empty()) continue; - const this_whitespace = std.mem.indexOfAny( - u32, - whitespace, - &[_]u32{cell.char}, - ) != null; - if (this_whitespace) continue; - - // Got it - break :end .{ .x = real_x, .y = y }; - } - - if (y == 0) break; - y -= 1; - } - - // There is no start point and therefore no line that can be selected. - return null; - }; - - return Selection{ - .start = start, - .end = end, - }; -} - -/// Select the nearest word to start point that is between start_pt and -/// end_pt (inclusive). Because it selects "nearest" to start point, start -/// point can be before or after end point. -pub fn selectWordBetween( - self: *Screen, - start_pt: point.ScreenPoint, - end_pt: point.ScreenPoint, -) ?Selection { - const dir: point.Direction = if (start_pt.before(end_pt)) .right_down else .left_up; - var it = start_pt.iterator(self, dir); - while (it.next()) |pt| { - // Boundary conditions - switch (dir) { - .right_down => if (end_pt.before(pt)) return null, - .left_up => if (pt.before(end_pt)) return null, - } - - // If we found a word, then return it - if (self.selectWord(pt)) |sel| return sel; - } - - return null; -} - -/// Select the word under the given point. A word is any consecutive series -/// of characters that are exclusively whitespace or exclusively non-whitespace. -/// A selection can span multiple physical lines if they are soft-wrapped. -/// -/// This will return null if a selection is impossible. The only scenario -/// this happens is if the point pt is outside of the written screen space. -pub fn selectWord(self: *Screen, pt: point.ScreenPoint) ?Selection { - // Boundary characters for selection purposes - const boundary = &[_]u32{ - 0, - ' ', - '\t', - '\'', - '"', - '│', - '`', - '|', - ':', - ',', - '(', - ')', - '[', - ']', - '{', - '}', - '<', - '>', - }; - - // Impossible to select anything outside of the area we've written. - const y_max = self.rowsWritten() - 1; - if (pt.y > y_max) return null; - - // Get our row - const row = self.getRow(.{ .screen = pt.y }); - const start_cell = row.getCell(pt.x); - - // If our cell is empty we can't select a word, because we can't select - // areas where the screen is not yet written. - if (start_cell.empty()) return null; - - // Determine if we are a boundary or not to determine what our boundary is. - const expect_boundary = std.mem.indexOfAny(u32, boundary, &[_]u32{start_cell.char}) != null; - - // Go forwards to find our end boundary - const end: point.ScreenPoint = boundary: { - var prev: point.ScreenPoint = pt; - var y: usize = pt.y; - var x: usize = pt.x; - while (y <= y_max) : (y += 1) { - const current_row = self.getRow(.{ .screen = y }); - - // Go through all the remainining cells on this row until - // we reach a boundary condition. - while (x < self.cols) : (x += 1) { - const cell = current_row.getCell(x); - - // If we reached an empty cell its always a boundary - if (cell.empty()) break :boundary prev; - - // If we do not match our expected set, we hit a boundary - const this_boundary = std.mem.indexOfAny( - u32, - boundary, - &[_]u32{cell.char}, - ) != null; - if (this_boundary != expect_boundary) break :boundary prev; - - // Increase our prev - prev.x = x; - prev.y = y; - } - - // If we aren't wrapping, then we're done this is a boundary. - if (!current_row.header().flags.wrap) break :boundary prev; - - // If we are wrapping, reset some values and search the next line. - x = 0; - } - - break :boundary .{ .x = self.cols - 1, .y = y_max }; - }; - - // Go backwards to find our start boundary - const start: point.ScreenPoint = boundary: { - var current_row = row; - var prev: point.ScreenPoint = pt; - - var y: usize = pt.y; - var x: usize = pt.x; - while (true) { - // Go through all the remainining cells on this row until - // we reach a boundary condition. - while (x > 0) : (x -= 1) { - const cell = current_row.getCell(x - 1); - const this_boundary = std.mem.indexOfAny( - u32, - boundary, - &[_]u32{cell.char}, - ) != null; - if (this_boundary != expect_boundary) break :boundary prev; - - // Update our prev - prev.x = x - 1; - prev.y = y; - } - - // If we're at the start, we need to check if the previous line wrapped. - // If we are wrapped, we continue searching. If we are not wrapped, - // then we've hit a boundary. - assert(prev.x == 0); - - // If we're at the end, we're done! - if (y == 0) break; - - // If the previous row did not wrap, then we're done. Otherwise - // we keep searching. - y -= 1; - current_row = self.getRow(.{ .screen = y }); - if (!current_row.header().flags.wrap) break :boundary prev; - - // Set x to start at the first non-empty cell - x = self.cols; - while (x > 0) : (x -= 1) { - if (!current_row.getCell(x - 1).empty()) break; - } - } - - break :boundary .{ .x = 0, .y = 0 }; - }; - - return Selection{ - .start = start, - .end = end, - }; -} - -/// Select the command output under the given point. The limits of the output -/// are determined by semantic prompt information provided by shell integration. -/// A selection can span multiple physical lines if they are soft-wrapped. -/// -/// This will return null if a selection is impossible. The only scenarios -/// this happens is if: -/// - the point pt is outside of the written screen space. -/// - the point pt is on a prompt / input line. -pub fn selectOutput(self: *Screen, pt: point.ScreenPoint) ?Selection { - // Impossible to select anything outside of the area we've written. - const y_max = self.rowsWritten() - 1; - if (pt.y > y_max) return null; - const point_row = self.getRow(.{ .screen = pt.y }); - switch (point_row.getSemanticPrompt()) { - .input, .prompt_continuation, .prompt => { - // Cursor on a prompt line, selection impossible - return null; - }, - else => {}, - } - - // Go forwards to find our end boundary - // We are looking for input start / prompt markers - const end: point.ScreenPoint = boundary: { - for (pt.y..y_max + 1) |y| { - const row = self.getRow(.{ .screen = y }); - switch (row.getSemanticPrompt()) { - .input, .prompt_continuation, .prompt => { - const prev_row = self.getRow(.{ .screen = y - 1 }); - break :boundary .{ .x = prev_row.lenCells(), .y = y - 1 }; - }, - else => {}, - } - } - - break :boundary .{ .x = self.cols - 1, .y = y_max }; - }; - - // Go backwards to find our start boundary - // We are looking for output start markers - const start: point.ScreenPoint = boundary: { - var y: usize = pt.y; - while (y > 0) : (y -= 1) { - const row = self.getRow(.{ .screen = y }); - switch (row.getSemanticPrompt()) { - .command => break :boundary .{ .x = 0, .y = y }, - else => {}, - } - } - break :boundary .{ .x = 0, .y = 0 }; - }; - - return Selection{ - .start = start, - .end = end, - }; -} - -/// Returns the selection bounds for the prompt at the given point. If the -/// point is not on a prompt line, this returns null. Note that due to -/// the underlying protocol, this will only return the y-coordinates of -/// the prompt. The x-coordinates of the start will always be zero and -/// the x-coordinates of the end will always be the last column. -/// -/// Note that this feature requires shell integration. If shell integration -/// is not enabled, this will always return null. -pub fn selectPrompt(self: *Screen, pt: point.ScreenPoint) ?Selection { - // Ensure that the line the point is on is a prompt. - const pt_row = self.getRow(.{ .screen = pt.y }); - const is_known = switch (pt_row.getSemanticPrompt()) { - .prompt, .prompt_continuation, .input => true, - .command => return null, - - // We allow unknown to continue because not all shells output any - // semantic prompt information for continuation lines. This has the - // possibility of making this function VERY slow (we look at all - // scrollback) so we should try to avoid this in the future by - // setting a flag or something if we have EVER seen a semantic - // prompt sequence. - .unknown => false, - }; - - // Find the start of the prompt. - var saw_semantic_prompt = is_known; - const start: usize = start: for (0..pt.y) |offset| { - const y = pt.y - offset; - const row = self.getRow(.{ .screen = y - 1 }); - switch (row.getSemanticPrompt()) { - // A prompt, we continue searching. - .prompt, .prompt_continuation, .input => saw_semantic_prompt = true, - - // See comment about "unknown" a few lines above. If we have - // previously seen a semantic prompt then if we see an unknown - // we treat it as a boundary. - .unknown => if (saw_semantic_prompt) break :start y, - - // Command output or unknown, definitely not a prompt. - .command => break :start y, - } - } else 0; - - // If we never saw a semantic prompt flag, then we can't trust our - // start value and we return null. This scenario usually means that - // semantic prompts aren't enabled via the shell. - if (!saw_semantic_prompt) return null; - - // Find the end of the prompt. - const end: usize = end: for (pt.y..self.rowsWritten()) |y| { - const row = self.getRow(.{ .screen = y }); - switch (row.getSemanticPrompt()) { - // A prompt, we continue searching. - .prompt, .prompt_continuation, .input => {}, - - // Command output or unknown, definitely not a prompt. - .command, .unknown => break :end y - 1, - } - } else self.rowsWritten() - 1; - - return .{ - .start = .{ .x = 0, .y = start }, - .end = .{ .x = self.cols - 1, .y = end }, - }; -} - -/// Returns the change in x/y that is needed to reach "to" from "from" -/// within a prompt. If "to" is before or after the prompt bounds then -/// the result will be bounded to the prompt. -/// -/// This feature requires shell integration. If shell integration is not -/// enabled, this will always return zero for both x and y (no path). -pub fn promptPath( - self: *Screen, - from: point.ScreenPoint, - to: point.ScreenPoint, -) struct { - x: isize, - y: isize, -} { - // Get our prompt bounds assuming "from" is at a prompt. - const bounds = self.selectPrompt(from) orelse return .{ .x = 0, .y = 0 }; - - // Get our actual "to" point clamped to the bounds of the prompt. - const to_clamped = if (bounds.contains(to)) - to - else if (to.before(bounds.start)) - bounds.start - else - bounds.end; - - // Basic math to calculate our path. - const from_x: isize = @intCast(from.x); - const from_y: isize = @intCast(from.y); - const to_x: isize = @intCast(to_clamped.x); - const to_y: isize = @intCast(to_clamped.y); - return .{ .x = to_x - from_x, .y = to_y - from_y }; -} - -/// Scroll behaviors for the scroll function. -pub const Scroll = union(enum) { - /// Scroll to the top of the scroll buffer. The first line of the - /// viewport will be the top line of the scroll buffer. - top: void, - - /// Scroll to the bottom, where the last line of the viewport - /// will be the last line of the buffer. TODO: are we sure? - bottom: void, - - /// Scroll up (negative) or down (positive) some fixed amount. - /// Scrolling direction (up/down) describes the direction the viewport - /// moves, not the direction text moves. This is the colloquial way that - /// scrolling is described: "scroll the page down". This scrolls the - /// screen (potentially in addition to the viewport) and may therefore - /// create more rows if necessary. - screen: isize, - - /// This is the same as "screen" but only scrolls the viewport. The - /// delta will be clamped at the current size of the screen and will - /// never create new scrollback. - viewport: isize, - - /// Scroll so the given row is in view. If the row is in the viewport, - /// this will change nothing. If the row is outside the viewport, the - /// viewport will change so that this row is at the top of the viewport. - row: RowIndex, - - /// Scroll down and move all viewport contents into the scrollback - /// so that the screen is clear. This isn't eqiuivalent to "screen" with - /// the value set to the viewport size because this will handle the case - /// that the viewport is not full. - /// - /// This will ignore empty trailing rows. An empty row is a row that - /// has never been written to at all. A row with spaces is not empty. - clear: void, -}; - -/// Scroll the screen by the given behavior. Note that this will always -/// "move" the screen. It is up to the caller to determine if they actually -/// want to do that yet (i.e. are they writing to the end of the screen -/// or not). -pub fn scroll(self: *Screen, behavior: Scroll) Allocator.Error!void { - // No matter what, scrolling marks our image state as dirty since - // it could move placements. If there are no placements or no images - // this is still a very cheap operation. - self.kitty_images.dirty = true; - - switch (behavior) { - // Setting viewport offset to zero makes row 0 be at self.top - // which is the top! - .top => self.viewport = 0, - - // Bottom is the end of the history area (end of history is the - // top of the active area). - .bottom => self.viewport = self.history, - - // TODO: deltas greater than the entire scrollback - .screen => |delta| try self.scrollDelta(delta, false), - .viewport => |delta| try self.scrollDelta(delta, true), - - // Scroll to a specific row - .row => |idx| self.scrollRow(idx), - - // Scroll until the viewport is clear by moving the viewport contents - // into the scrollback. - .clear => try self.scrollClear(), - } -} - -fn scrollClear(self: *Screen) Allocator.Error!void { - // The full amount of rows in the viewport - const full_amount = self.rowsWritten() - self.viewport; - - // Find the number of non-empty rows - const non_empty = for (0..full_amount) |i| { - const rev_i = full_amount - i - 1; - const row = self.getRow(.{ .viewport = rev_i }); - if (!row.isEmpty()) break rev_i + 1; - } else full_amount; - - try self.scroll(.{ .screen = @intCast(non_empty) }); -} - -fn scrollRow(self: *Screen, idx: RowIndex) void { - // Convert the given row to a screen point. - const screen_idx = idx.toScreen(self); - const screen_pt: point.ScreenPoint = .{ .y = screen_idx.screen }; - - // Move the viewport so that the screen point is in view. We do the - // @min here so that we don't scroll down below where our "bottom" - // viewport is. - self.viewport = @min(self.history, screen_pt.y); - assert(screen_pt.inViewport(self)); -} - -fn scrollDelta(self: *Screen, delta: isize, viewport_only: bool) Allocator.Error!void { - // Just in case, to avoid a bunch of stuff below. - if (delta == 0) return; - - // If we're scrolling up, then we just subtract and we're done. - // We just clamp at 0 which blocks us from scrolling off the top. - if (delta < 0) { - self.viewport -|= @as(usize, @intCast(-delta)); - return; - } - - // If we're scrolling only the viewport, then we just add to the viewport. - if (viewport_only) { - self.viewport = @min( - self.history, - self.viewport + @as(usize, @intCast(delta)), - ); - return; - } - - // Add our delta to our viewport. If we're less than the max currently - // allowed to scroll to the bottom (the end of the history), then we - // have space and we just return. - const start_viewport_bottom = self.viewportIsBottom(); - const viewport = self.history + @as(usize, @intCast(delta)); - if (viewport <= self.history) return; - - // If our viewport is past the top of our history then we potentially need - // to write more blank rows. If our viewport is more than our rows written - // then we expand out to there. - const rows_written = self.rowsWritten(); - const viewport_bottom = viewport + self.rows; - if (viewport_bottom <= rows_written) return; - - // The number of new rows we need is the number of rows off our - // previous bottom we are growing. - const new_rows_needed = viewport_bottom - rows_written; - - // If we can't fit into our capacity but we have space, resize the - // buffer to allocate more scrollback. - const rows_final = rows_written + new_rows_needed; - if (rows_final > self.rowsCapacity()) { - const max_capacity = self.maxCapacity(); - if (self.storage.capacity() < max_capacity) { - // The capacity we want to allocate. We take whatever is greater - // of what we actually need and two pages. We don't want to - // allocate one row at a time (common for scrolling) so we do this - // to chunk it. - const needed_capacity = @max( - rows_final * (self.cols + 1), - @min(self.storage.capacity() * 2, max_capacity), - ); - - // Allocate what we can. - try self.storage.resize( - self.alloc, - @min(max_capacity, needed_capacity), - ); - } - } - - // If we can't fit our rows into our capacity, we delete some scrollback. - const rows_deleted = if (rows_final > self.rowsCapacity()) deleted: { - const rows_to_delete = rows_final - self.rowsCapacity(); - - // Fast-path: we have no graphemes. - // Slow-path: we have graphemes, we have to check each row - // we're going to delete to see if they contain graphemes and - // clear the ones that do so we clear memory properly. - if (self.graphemes.count() > 0) { - var y: usize = 0; - while (y < rows_to_delete) : (y += 1) { - const row = self.getRow(.{ .screen = y }); - if (row.storage[0].header.flags.grapheme) row.clear(.{}); - } - } - - self.storage.deleteOldest(rows_to_delete * (self.cols + 1)); - break :deleted rows_to_delete; - } else 0; - - // If we are deleting rows and have a selection, then we need to offset - // the selection by the rows we're deleting. - if (self.selection) |*sel| { - // If we're deleting more rows than our Y values, we also move - // the X over to 0 because we're in the middle of the selection now. - if (rows_deleted > sel.start.y) sel.start.x = 0; - if (rows_deleted > sel.end.y) sel.end.x = 0; - - // Remove the deleted rows from both y values. We use saturating - // subtraction so that we can detect when we're at zero. - sel.start.y -|= rows_deleted; - sel.end.y -|= rows_deleted; - - // If the selection is now empty, just clear it. - if (sel.empty()) self.selection = null; - } - - // If we have more rows than what shows on our screen, we have a - // history boundary. - const rows_written_final = rows_final - rows_deleted; - if (rows_written_final > self.rows) { - self.history = rows_written_final - self.rows; - } - - // Ensure we have "written" our last row so that it shows up - const slices = self.storage.getPtrSlice( - (rows_written_final - 1) * (self.cols + 1), - self.cols + 1, - ); - // We should never be wrapped here - assert(slices[1].len == 0); - - // We only grabbed our new row(s), copy cells into the whole slice - const dst = slices[0]; - // The pen we'll use for new cells (only the BG attribute is applied to new - // cells) - const pen: Cell = switch (self.cursor.pen.bg) { - .none => .{}, - else => |bg| .{ .bg = bg }, - }; - @memset(dst, .{ .cell = pen }); - - // Then we make sure our row headers are zeroed out. We set - // the value to a dirty row header so that the renderer re-draws. - var i: usize = 0; - while (i < dst.len) : (i += self.cols + 1) { - dst[i] = .{ .header = .{ - .flags = .{ .dirty = true }, - } }; - } - - if (start_viewport_bottom) { - // If our viewport is on the bottom, we always update the viewport - // to the latest so that it remains in view. - self.viewport = self.history; - } else if (rows_deleted > 0) { - // If our viewport is NOT on the bottom, we want to keep our viewport - // where it was so that we don't jump around. However, we need to - // subtract the final rows written if we had to delete rows since - // that changes the viewport offset. - self.viewport -|= rows_deleted; - } -} - -/// The options for where you can jump to on the screen. -pub const JumpTarget = union(enum) { - /// Jump forwards (positive) or backwards (negative) a set number of - /// prompts. If the absolute value is greater than the number of prompts - /// in either direction, jump to the furthest prompt. - prompt_delta: isize, -}; - -/// Jump the viewport to specific location. -pub fn jump(self: *Screen, target: JumpTarget) bool { - return switch (target) { - .prompt_delta => |delta| self.jumpPrompt(delta), - }; -} - -/// Jump the viewport forwards (positive) or backwards (negative) a set number of -/// prompts (delta). Returns true if the viewport changed and false if no jump -/// occurred. -fn jumpPrompt(self: *Screen, delta: isize) bool { - // If we aren't jumping any prompts then we don't need to do anything. - if (delta == 0) return false; - - // The screen y value we start at - const start_y: isize = start_y: { - const idx: RowIndex = .{ .viewport = 0 }; - const screen = idx.toScreen(self); - break :start_y @intCast(screen.screen); - }; - - // The maximum y in the positive direction. Negative is always 0. - const max_y: isize = @intCast(self.rowsWritten() - 1); - - // Go line-by-line counting the number of prompts we see. - const step: isize = if (delta > 0) 1 else -1; - var y: isize = start_y + step; - const delta_start: usize = @intCast(if (delta > 0) delta else -delta); - var delta_rem: usize = delta_start; - while (y >= 0 and y <= max_y and delta_rem > 0) : (y += step) { - const row = self.getRow(.{ .screen = @intCast(y) }); - switch (row.getSemanticPrompt()) { - .prompt, .prompt_continuation, .input => delta_rem -= 1, - .command, .unknown => {}, - } - } - - //log.warn("delta={} delta_rem={} start_y={} y={}", .{ delta, delta_rem, start_y, y }); - - // If we didn't find any, do nothing. - if (delta_rem == delta_start) return false; - - // Done! We count the number of lines we changed and scroll. - const y_delta = (y - step) - start_y; - const new_y: usize = @intCast(start_y + y_delta); - const old_viewport = self.viewport; - self.scroll(.{ .row = .{ .screen = new_y } }) catch unreachable; - //log.warn("delta={} y_delta={} start_y={} new_y={}", .{ delta, y_delta, start_y, new_y }); - return self.viewport != old_viewport; -} - -/// Returns the raw text associated with a selection. This will unwrap -/// soft-wrapped edges. The returned slice is owned by the caller and allocated -/// using alloc, not the allocator associated with the screen (unless they match). -pub fn selectionString( - self: *Screen, - alloc: Allocator, - sel: Selection, - trim: bool, -) ![:0]const u8 { - // Get the slices for the string - const slices = self.selectionSlices(sel); - - // Use an ArrayList so that we can grow the array as we go. We - // build an initial capacity of just our rows in our selection times - // columns. It can be more or less based on graphemes, newlines, etc. - var strbuilder = try std.ArrayList(u8).initCapacity(alloc, slices.rows * self.cols); - defer strbuilder.deinit(); - - // Get our string result. - try self.selectionSliceString(slices, &strbuilder, null); - - // Remove any trailing spaces on lines. We could do optimize this by - // doing this in the loop above but this isn't very hot path code and - // this is simple. - if (trim) { - var it = std.mem.tokenizeScalar(u8, strbuilder.items, '\n'); - - // Reset our items. We retain our capacity. Because we're only - // removing bytes, we know that the trimmed string must be no longer - // than the original string so we copy directly back into our - // allocated memory. - strbuilder.clearRetainingCapacity(); - while (it.next()) |line| { - const trimmed = std.mem.trimRight(u8, line, " \t"); - const i = strbuilder.items.len; - strbuilder.items.len += trimmed.len; - std.mem.copyForwards(u8, strbuilder.items[i..], trimmed); - strbuilder.appendAssumeCapacity('\n'); - } - - // Remove our trailing newline again - if (strbuilder.items.len > 0) strbuilder.items.len -= 1; - } - - // Get our final string - const string = try strbuilder.toOwnedSliceSentinel(0); - errdefer alloc.free(string); - - return string; -} - -/// Returns the row text associated with a selection along with the -/// mapping of each individual byte in the string to the point in the screen. -fn selectionStringMap( - self: *Screen, - alloc: Allocator, - sel: Selection, -) !StringMap { - // Get the slices for the string - const slices = self.selectionSlices(sel); - - // Use an ArrayList so that we can grow the array as we go. We - // build an initial capacity of just our rows in our selection times - // columns. It can be more or less based on graphemes, newlines, etc. - var strbuilder = try std.ArrayList(u8).initCapacity(alloc, slices.rows * self.cols); - defer strbuilder.deinit(); - var mapbuilder = try std.ArrayList(point.ScreenPoint).initCapacity(alloc, strbuilder.capacity); - defer mapbuilder.deinit(); - - // Get our results - try self.selectionSliceString(slices, &strbuilder, &mapbuilder); - - // Get our final string - const string = try strbuilder.toOwnedSliceSentinel(0); - errdefer alloc.free(string); - const map = try mapbuilder.toOwnedSlice(); - errdefer alloc.free(map); - return .{ .string = string, .map = map }; -} - -/// Takes a SelectionSlices value and builds the string and mapping for it. -fn selectionSliceString( - self: *Screen, - slices: SelectionSlices, - strbuilder: *std.ArrayList(u8), - mapbuilder: ?*std.ArrayList(point.ScreenPoint), -) !void { - // Connect the text from the two slices - const arr = [_][]StorageCell{ slices.top, slices.bot }; - var row_count: usize = 0; - for (arr) |slice| { - const row_start: usize = row_count; - while (row_count < slices.rows) : (row_count += 1) { - const row_i = row_count - row_start; - - // Calculate our start index. If we are beyond the length - // of this slice, then its time to move on (we exhausted top). - const start_idx = row_i * (self.cols + 1); - if (start_idx >= slice.len) break; - - const end_idx = if (slices.sel.rectangle) - // Rectangle select: calculate end with bottom offset. - start_idx + slices.bot_offset + 2 // think "column count" + 1 - else - // Normal select: our end index is usually a full row, but if - // we're the final row then we just use the length. - @min(slice.len, start_idx + self.cols + 1); - - // We may have to skip some cells from the beginning if we're the - // first row, of if we're using rectangle select. - var skip: usize = if (row_count == 0 or slices.sel.rectangle) slices.top_offset else 0; - - // If we have runtime safety we need to initialize the row - // so that the proper union tag is set. In release modes we - // don't need to do this because we zero the memory. - if (std.debug.runtime_safety) { - _ = self.getRow(.{ .screen = slices.sel.start.y + row_i }); - } - - const row: Row = .{ .screen = self, .storage = slice[start_idx..end_idx] }; - var it = row.cellIterator(); - var x: usize = 0; - while (it.next()) |cell| { - defer x += 1; - - if (skip > 0) { - skip -= 1; - continue; - } - - // Skip spacers - if (cell.attrs.wide_spacer_head or - cell.attrs.wide_spacer_tail) continue; - - var buf: [4]u8 = undefined; - const char = if (cell.char > 0) cell.char else ' '; - { - const encode_len = try std.unicode.utf8Encode(@intCast(char), &buf); - try strbuilder.appendSlice(buf[0..encode_len]); - if (mapbuilder) |b| { - for (0..encode_len) |_| try b.append(.{ - .x = x, - .y = slices.sel.start.y + row_i, - }); - } - } - - var cp_it = row.codepointIterator(x); - while (cp_it.next()) |cp| { - const encode_len = try std.unicode.utf8Encode(cp, &buf); - try strbuilder.appendSlice(buf[0..encode_len]); - if (mapbuilder) |b| { - for (0..encode_len) |_| try b.append(.{ - .x = x, - .y = slices.sel.start.y + row_i, - }); - } - } - } - - // If this row is not soft-wrapped or if we're using rectangle - // select, add a newline - if (!row.header().flags.wrap or slices.sel.rectangle) { - try strbuilder.append('\n'); - if (mapbuilder) |b| { - try b.append(.{ - .x = self.cols - 1, - .y = slices.sel.start.y + row_i, - }); - } - } - } - } - - // Remove our trailing newline, its never correct. - if (strbuilder.items.len > 0 and - strbuilder.items[strbuilder.items.len - 1] == '\n') - { - strbuilder.items.len -= 1; - if (mapbuilder) |b| b.items.len -= 1; - } - - if (std.debug.runtime_safety) { - if (mapbuilder) |b| { - assert(strbuilder.items.len == b.items.len); - } - } -} - -const SelectionSlices = struct { - rows: usize, - - // The selection that the slices below represent. This may not - // be the same as the input selection since some normalization - // occurs. - sel: Selection, - - // Top offset can be used to determine if a newline is required by - // seeing if the cell index plus the offset cleanly divides by screen cols. - top_offset: usize, - - // Our bottom offset is used in rectangle select to always determine the - // maximum cell in a given row. - bot_offset: usize, - - // Our selection storage cell chunks. - top: []StorageCell, - bot: []StorageCell, -}; - -/// Returns the slices that make up the selection, in order. There are at most -/// two parts to handle the ring buffer. If the selection fits in one contiguous -/// slice, then the second slice will have a length of zero. -fn selectionSlices(self: *Screen, sel_raw: Selection) SelectionSlices { - // Note: this function is tested via selectionString - - // If the selection starts beyond the end of the screen, then we return empty - if (sel_raw.start.y >= self.rowsWritten()) return .{ - .rows = 0, - .sel = sel_raw, - .top_offset = 0, - .bot_offset = 0, - .top = self.storage.storage[0..0], - .bot = self.storage.storage[0..0], - }; - - const sel = sel: { - var sel = sel_raw; - - // Clamp the selection to the screen - if (sel.end.y >= self.rowsWritten()) { - sel.end.y = self.rowsWritten() - 1; - sel.end.x = self.cols - 1; - } - - // If the end of our selection is a wide char leader, include the - // first part of the next line. - if (sel.end.x == self.cols - 1) { - const row = self.getRow(.{ .screen = sel.end.y }); - const cell = row.getCell(sel.end.x); - if (cell.attrs.wide_spacer_head) { - sel.end.y += 1; - sel.end.x = 0; - } - } - - // If the start of our selection is a wide char spacer, include the - // wide char. - if (sel.start.x > 0) { - const row = self.getRow(.{ .screen = sel.start.y }); - const cell = row.getCell(sel.start.x); - if (cell.attrs.wide_spacer_tail) { - sel.start.x -= 1; - } - } - - break :sel sel; - }; - - // Get the true "top" and "bottom" - const sel_top = sel.topLeft(); - const sel_bot = sel.bottomRight(); - const sel_isRect = sel.rectangle; - - // We get the slices for the full top and bottom (inclusive). - const sel_top_offset = self.rowOffset(.{ .screen = sel_top.y }); - const sel_bot_offset = self.rowOffset(.{ .screen = sel_bot.y }); - const slices = self.storage.getPtrSlice( - sel_top_offset, - (sel_bot_offset - sel_top_offset) + (sel_bot.x + 2), - ); - - // The bottom and top are split into two slices, so we slice to the - // bottom of the storage, then from the top. - return .{ - .rows = sel_bot.y - sel_top.y + 1, - .sel = .{ .start = sel_top, .end = sel_bot, .rectangle = sel_isRect }, - .top_offset = sel_top.x, - .bot_offset = sel_bot.x, - .top = slices[0], - .bot = slices[1], - }; -} - -/// Resize the screen without any reflow. In this mode, columns/rows will -/// be truncated as they are shrunk. If they are grown, the new space is filled -/// with zeros. -pub fn resizeWithoutReflow(self: *Screen, rows: usize, cols: usize) !void { - // If we're resizing to the same size, do nothing. - if (self.cols == cols and self.rows == rows) return; - - // The number of no-character lines after our cursor. This is used - // to trim those lines on a resize first without generating history. - // This is only done if we don't have history yet. - // - // This matches macOS Terminal.app behavior. I chose to match that - // behavior because it seemed fine in an ocean of differing behavior - // between terminal apps. I'm completely open to changing it as long - // as resize behavior isn't regressed in a user-hostile way. - const trailing_blank_lines = blank: { - // If we aren't changing row length, then don't bother calculating - // because we aren't going to trim. - if (self.rows == rows) break :blank 0; - - const blank = self.trailingBlankLines(); - - // If we are shrinking the number of rows, we don't want to trim - // off more blank rows than the number we're shrinking because it - // creates a jarring screen move experience. - if (self.rows > rows) break :blank @min(blank, self.rows - rows); - - break :blank blank; - }; - - // Make a copy so we can access the old indexes. - var old = self.*; - errdefer self.* = old; - - // Change our rows and cols so calculations make sense - self.rows = rows; - self.cols = cols; - - // The end of the screen is the rows we wrote minus any blank lines - // we're trimming. - const end_of_screen_y = old.rowsWritten() - trailing_blank_lines; - - // Calculate our buffer size. This is going to be either the old data - // with scrollback or the max capacity of our new size. We prefer the old - // length so we can save all the data (ignoring col truncation). - const old_len = @max(end_of_screen_y, rows) * (cols + 1); - const new_max_capacity = self.maxCapacity(); - const buf_size = @min(old_len, new_max_capacity); - - // Reallocate the storage - self.storage = try StorageBuf.init(self.alloc, buf_size); - errdefer self.storage.deinit(self.alloc); - defer old.storage.deinit(self.alloc); - - // Our viewport and history resets to the top because we're going to - // rewrite the screen - self.viewport = 0; - self.history = 0; - - // Reset our grapheme map and ensure the old one is deallocated - // on success. - self.graphemes = .{}; - errdefer self.deinitGraphemes(); - defer old.deinitGraphemes(); - - // Rewrite all our rows - var y: usize = 0; - for (0..end_of_screen_y) |it_y| { - const old_row = old.getRow(.{ .screen = it_y }); - - // If we're past the end, scroll - if (y >= self.rows) { - // If we're shrinking rows then its possible we'll trim scrollback - // and we have to account for how much we actually trimmed and - // reflect that in the cursor. - if (self.storage.len() >= self.maxCapacity()) { - old.cursor.y -|= 1; - } - - y -= 1; - try self.scroll(.{ .screen = 1 }); - } - - // Get this row - const new_row = self.getRow(.{ .active = y }); - try new_row.copyRow(old_row); - - // Next row - y += 1; - } - - // Convert our cursor to screen coordinates so we can preserve it. - // The cursor is normally in active coordinates, but by converting to - // screen we can accommodate keeping it on the same place if we retain - // the same scrollback. - const old_cursor_y_screen = RowIndexTag.active.index(old.cursor.y).toScreen(&old).screen; - self.cursor.x = @min(old.cursor.x, self.cols - 1); - self.cursor.y = if (old_cursor_y_screen <= RowIndexTag.screen.maxLen(self)) - old_cursor_y_screen -| self.history - else - self.rows - 1; - - // If our rows increased and our cursor is NOT at the bottom, we want - // to try to preserve the y value of the old cursor. In other words, we - // don't want to "pull down" scrollback. This is purely a UX feature. - if (self.rows > old.rows and - old.cursor.y < old.rows - 1 and - self.cursor.y > old.cursor.y) - { - const delta = self.cursor.y - old.cursor.y; - if (self.scroll(.{ .screen = @intCast(delta) })) { - self.cursor.y -= delta; - } else |err| { - // If this scroll fails its not that big of a deal so we just - // log and ignore. - log.warn("failed to scroll for resize, cursor may be off err={}", .{err}); - } - } -} - -/// Resize the screen. The rows or cols can be bigger or smaller. This -/// function can only be used to resize the viewport. The scrollback size -/// (in lines) can't be changed. But due to the resize, more or less scrollback -/// "space" becomes available due to the width of lines. -/// -/// Due to the internal representation of a screen, this usually involves a -/// significant amount of copying compared to any other operations. -/// -/// This will trim data if the size is getting smaller. This will reflow the -/// soft wrapped text. -pub fn resize(self: *Screen, rows: usize, cols: usize) !void { - if (self.cols == cols) { - // No resize necessary - if (self.rows == rows) return; - - // No matter what we mark our image state as dirty - self.kitty_images.dirty = true; - - // If we have the same number of columns, text can't possibly - // reflow in any way, so we do the quicker thing and do a resize - // without reflow checks. - try self.resizeWithoutReflow(rows, cols); - return; - } - - // No matter what we mark our image state as dirty - self.kitty_images.dirty = true; - - // Keep track if our cursor is at the bottom - const cursor_bottom = self.cursor.y == self.rows - 1; - - // If our columns increased, we alloc space for the new column width - // and go through each row and reflow if necessary. - if (cols > self.cols) { - var old = self.*; - errdefer self.* = old; - - // Allocate enough to store our screen plus history. - const buf_size = (self.rows + @max(self.history, self.max_scrollback)) * (cols + 1); - self.storage = try StorageBuf.init(self.alloc, buf_size); - errdefer self.storage.deinit(self.alloc); - defer old.storage.deinit(self.alloc); - - // Copy grapheme map - self.graphemes = .{}; - errdefer self.deinitGraphemes(); - defer old.deinitGraphemes(); - - // Convert our cursor coordinates to screen coordinates because - // we may have to reflow the cursor if the line it is on is unwrapped. - const cursor_pos = (point.Active{ - .x = old.cursor.x, - .y = old.cursor.y, - }).toScreen(&old); - - // Whether we need to move the cursor or not - var new_cursor: ?point.ScreenPoint = null; - - // Reset our variables because we're going to reprint the screen. - self.cols = cols; - self.viewport = 0; - self.history = 0; - - // Iterate over the screen since we need to check for reflow. - var iter = old.rowIterator(.screen); - var y: usize = 0; - while (iter.next()) |old_row| { - // If we're past the end, scroll - if (y >= self.rows) { - try self.scroll(.{ .screen = 1 }); - y -= 1; - } - - // We need to check if our cursor was on this line. If so, - // we set the new cursor. - if (cursor_pos.y == iter.value - 1) { - assert(new_cursor == null); // should only happen once - new_cursor = .{ .y = self.history + y, .x = cursor_pos.x }; - } - - // At this point, we're always at x == 0 so we can just copy - // the row (we know old.cols < self.cols). - var new_row = self.getRow(.{ .active = y }); - try new_row.copyRow(old_row); - if (!old_row.header().flags.wrap) { - // We used to do have this behavior, but it broke some programs. - // I know I copied this behavior while observing some other - // terminal, but I can't remember which one. I'm leaving this - // here in case we want to bring this back (with probably - // slightly different behavior). - // - // If we have no reflow, we attempt to extend any stylized - // cells at the end of the line if there is one. - // const len = old_row.lenCells(); - // const end = new_row.getCell(len - 1); - // if ((end.char == 0 or end.char == ' ') and !end.empty()) { - // for (len..self.cols) |x| { - // const cell = new_row.getCellPtr(x); - // cell.* = end; - // } - // } - - y += 1; - continue; - } - - // We need to reflow. At this point things get a bit messy. - // The goal is to keep the messiness of reflow down here and - // only reloop when we're back to clean non-wrapped lines. - - // Mark the last element as not wrapped - new_row.setWrapped(false); - - // x is the offset where we start copying into new_row. Its also - // used for cursor tracking. - var x: usize = old.cols; - - // Edge case: if the end of our old row is a wide spacer head, - // we want to overwrite it. - if (old_row.getCellPtr(x - 1).attrs.wide_spacer_head) x -= 1; - - wrapping: while (iter.next()) |wrapped_row| { - const wrapped_cells = trim: { - var i: usize = old.cols; - - // Trim the row from the right so that we ignore all trailing - // empty chars and don't wrap them. We only do this if the - // row is NOT wrapped again because the whitespace would be - // meaningful. - if (!wrapped_row.header().flags.wrap) { - while (i > 0) : (i -= 1) { - if (!wrapped_row.getCell(i - 1).empty()) break; - } - } else { - // If we are wrapped, then similar to above "edge case" - // we want to overwrite the wide spacer head if we end - // in one. - if (wrapped_row.getCellPtr(i - 1).attrs.wide_spacer_head) { - i -= 1; - } - } - - break :trim wrapped_row.storage[1 .. i + 1]; - }; - - var wrapped_i: usize = 0; - while (wrapped_i < wrapped_cells.len) { - // Remaining space in our new row - const new_row_rem = self.cols - x; - - // Remaining cells in our wrapped row - const wrapped_cells_rem = wrapped_cells.len - wrapped_i; - - // We copy as much as we can into our new row - const copy_len = if (new_row_rem <= wrapped_cells_rem) copy_len: { - // We are going to end up filling our new row. We need - // to check if the end of the row is a wide char and - // if so, we need to insert a wide char header and wrap - // there. - var proposed: usize = new_row_rem; - - // If the end of our copy is wide, we copy one less and - // set the wide spacer header now since we're not going - // to write over it anyways. - if (proposed > 0 and wrapped_cells[wrapped_i + proposed - 1].cell.attrs.wide) { - proposed -= 1; - new_row.getCellPtr(x + proposed).* = .{ - .char = ' ', - .attrs = .{ .wide_spacer_head = true }, - }; - } - - break :copy_len proposed; - } else wrapped_cells_rem; - - // The row doesn't fit, meaning we have to soft-wrap the - // new row but probably at a diff boundary. - fastmem.copy( - StorageCell, - new_row.storage[x + 1 ..], - wrapped_cells[wrapped_i .. wrapped_i + copy_len], - ); - - // We need to check if our cursor was on this line - // and in the part that WAS copied. If so, we need to move it. - if (cursor_pos.y == iter.value - 1 and - cursor_pos.x < copy_len and - new_cursor == null) - { - new_cursor = .{ .y = self.history + y, .x = x + cursor_pos.x }; - } - - // We copied the full amount left in this wrapped row. - if (copy_len == wrapped_cells_rem) { - // If this row isn't also wrapped, we're done! - if (!wrapped_row.header().flags.wrap) { - y += 1; - break :wrapping; - } - - // Wrapped again! - x += wrapped_cells_rem; - break; - } - - // We still need to copy the remainder - wrapped_i += copy_len; - - // Move to a new line in our new screen - new_row.setWrapped(true); - y += 1; - x = 0; - - // If we're past the end, scroll - if (y >= self.rows) { - y -= 1; - try self.scroll(.{ .screen = 1 }); - } - new_row = self.getRow(.{ .active = y }); - new_row.setSemanticPrompt(old_row.getSemanticPrompt()); - } - } - } - - // If we have a new cursor, we need to convert that to a viewport - // point and set it up. - if (new_cursor) |pos| { - const viewport_pos = pos.toViewport(self); - self.cursor.x = viewport_pos.x; - self.cursor.y = viewport_pos.y; - } - } - - // We grow rows after cols so that we can do our unwrapping/reflow - // before we do a no-reflow grow. - if (rows > self.rows) try self.resizeWithoutReflow(rows, self.cols); - - // If our rows got smaller, we trim the scrollback. We do this after - // handling cols growing so that we can save as many lines as we can. - // We do it before cols shrinking so we can save compute on that operation. - if (rows < self.rows) try self.resizeWithoutReflow(rows, self.cols); - - // If our cols got smaller, we have to reflow text. This is the worst - // possible case because we can't do any easy tricks to get reflow, - // we just have to iterate over the screen and "print", wrapping as - // needed. - if (cols < self.cols) { - var old = self.*; - errdefer self.* = old; - - // Allocate enough to store our screen plus history. - const buf_size = (self.rows + @max(self.history, self.max_scrollback)) * (cols + 1); - self.storage = try StorageBuf.init(self.alloc, buf_size); - errdefer self.storage.deinit(self.alloc); - defer old.storage.deinit(self.alloc); - - // Create empty grapheme map. Cell IDs change so we can't just copy it, - // we'll rebuild it. - self.graphemes = .{}; - errdefer self.deinitGraphemes(); - defer old.deinitGraphemes(); - - // Convert our cursor coordinates to screen coordinates because - // we may have to reflow the cursor if the line it is on is moved. - const cursor_pos = (point.Active{ - .x = old.cursor.x, - .y = old.cursor.y, - }).toScreen(&old); - - // Whether we need to move the cursor or not - var new_cursor: ?point.ScreenPoint = null; - var new_cursor_wrap: usize = 0; - - // Reset our variables because we're going to reprint the screen. - self.cols = cols; - self.viewport = 0; - self.history = 0; - - // Iterate over the screen since we need to check for reflow. We - // clear all the trailing blank lines so that shells like zsh and - // fish that often clear the display below don't force us to have - // scrollback. - var old_y: usize = 0; - const end_y = RowIndexTag.screen.maxLen(&old) - old.trailingBlankLines(); - var y: usize = 0; - while (old_y < end_y) : (old_y += 1) { - const old_row = old.getRow(.{ .screen = old_y }); - const old_row_wrapped = old_row.header().flags.wrap; - const trimmed_row = self.trimRowForResizeLessCols(&old, old_row); - - // If our y is more than our rows, we need to scroll - if (y >= self.rows) { - try self.scroll(.{ .screen = 1 }); - y -= 1; - } - - // Fast path: our old row is not wrapped AND our old row fits - // into our new smaller size AND this row has no grapheme clusters. - // In this case, we just do a fast copy and move on. - if (!old_row_wrapped and - trimmed_row.len <= self.cols and - !old_row.header().flags.grapheme) - { - // If our cursor is on this line, then set the new cursor. - if (cursor_pos.y == old_y) { - assert(new_cursor == null); - new_cursor = .{ .x = cursor_pos.x, .y = self.history + y }; - } - - const row = self.getRow(.{ .active = y }); - row.setSemanticPrompt(old_row.getSemanticPrompt()); - - fastmem.copy( - StorageCell, - row.storage[1..], - trimmed_row, - ); - - y += 1; - continue; - } - - // Slow path: the row is wrapped or doesn't fit so we have to - // wrap ourselves. In this case, we basically just "print and wrap" - var row = self.getRow(.{ .active = y }); - row.setSemanticPrompt(old_row.getSemanticPrompt()); - var x: usize = 0; - var cur_old_row = old_row; - var cur_old_row_wrapped = old_row_wrapped; - var cur_trimmed_row = trimmed_row; - while (true) { - for (cur_trimmed_row, 0..) |old_cell, old_x| { - var cell: StorageCell = old_cell; - - // This is a really wild edge case if we're resizing down - // to 1 column. In reality this is pretty broken for end - // users so downstream should prevent this. - if (self.cols == 1 and - (cell.cell.attrs.wide or - cell.cell.attrs.wide_spacer_head or - cell.cell.attrs.wide_spacer_tail)) - { - cell = .{ .cell = .{ .char = ' ' } }; - } - - // We need to wrap wide chars with a spacer head. - if (cell.cell.attrs.wide and x == self.cols - 1) { - row.getCellPtr(x).* = .{ - .char = ' ', - .attrs = .{ .wide_spacer_head = true }, - }; - x += 1; - } - - // Soft wrap if we have to. - if (x == self.cols) { - row.setWrapped(true); - x = 0; - y += 1; - - // Wrapping can cause us to overflow our visible area. - // If so, scroll. - if (y >= self.rows) { - try self.scroll(.{ .screen = 1 }); - y -= 1; - - // Clear if our current cell is a wide spacer tail - if (cell.cell.attrs.wide_spacer_tail) { - cell = .{ .cell = .{} }; - } - } - - if (cursor_pos.y == old_y) { - // If this original y is where our cursor is, we - // track the number of wraps we do so we can try to - // keep this whole line on the screen. - new_cursor_wrap += 1; - } - - row = self.getRow(.{ .active = y }); - row.setSemanticPrompt(cur_old_row.getSemanticPrompt()); - } - - // If our cursor is on this char, then set the new cursor. - if (cursor_pos.y == old_y and cursor_pos.x == old_x) { - assert(new_cursor == null); - new_cursor = .{ .x = x, .y = self.history + y }; - } - - // Write the cell - const new_cell = row.getCellPtr(x); - new_cell.* = cell.cell; - - // If the old cell is a multi-codepoint grapheme then we - // need to also attach the graphemes. - if (cell.cell.attrs.grapheme) { - var it = cur_old_row.codepointIterator(old_x); - while (it.next()) |cp| try row.attachGrapheme(x, cp); - } - - x += 1; - } - - // If we're done wrapping, we move on. - if (!cur_old_row_wrapped) { - y += 1; - break; - } - - // If the old row is wrapped we continue with the loop with - // the next row. - old_y += 1; - cur_old_row = old.getRow(.{ .screen = old_y }); - cur_old_row_wrapped = cur_old_row.header().flags.wrap; - cur_trimmed_row = self.trimRowForResizeLessCols(&old, cur_old_row); - } - } - - // If we have a new cursor, we need to convert that to a viewport - // point and set it up. - if (new_cursor) |pos| { - const viewport_pos = pos.toViewport(self); - self.cursor.x = @min(viewport_pos.x, self.cols - 1); - self.cursor.y = @min(viewport_pos.y, self.rows - 1); - - // We want to keep our cursor y at the same place. To do so, we - // scroll the screen. This scrolls all of the content so the cell - // the cursor is over doesn't change. - if (!cursor_bottom and old.cursor.y < self.cursor.y) scroll: { - const delta: isize = delta: { - var delta: isize = @intCast(self.cursor.y - old.cursor.y); - - // new_cursor_wrap is the number of times the line that the - // cursor was on previously was wrapped to fit this new col - // width. We want to scroll that many times less so that - // the whole line the cursor was on attempts to remain - // in view. - delta -= @intCast(new_cursor_wrap); - - if (delta <= 0) break :scroll; - break :delta delta; - }; - - self.scroll(.{ .screen = delta }) catch |err| { - log.warn("failed to scroll for resize, cursor may be off err={}", .{err}); - break :scroll; - }; - - self.cursor.y -= @intCast(delta); - } - } else { - // TODO: why is this necessary? Without this, neovim will - // crash when we shrink the window to the smallest size. We - // never got a test case to cover this. - self.cursor.x = @min(self.cursor.x, self.cols - 1); - self.cursor.y = @min(self.cursor.y, self.rows - 1); - } - } -} - -/// Counts the number of trailing lines from the cursor that are blank. -/// This is specifically used for resizing and isn't meant to be a general -/// purpose tool. -fn trailingBlankLines(self: *Screen) usize { - // Start one line below our cursor and continue to the last line - // of the screen or however many rows we have written. - const start = self.cursor.y + 1; - const end = @min(self.rowsWritten(), self.rows); - if (start >= end) return 0; - - var blank: usize = 0; - for (0..(end - start)) |i| { - const y = end - i - 1; - const row = self.getRow(.{ .active = y }); - if (!row.isEmpty()) break; - blank += 1; - } - - return blank; -} - -/// When resizing to less columns, this trims the row from the right -/// so we don't unnecessarily wrap. This will freely throw away trailing -/// colored but empty (character) cells. This matches Terminal.app behavior, -/// which isn't strictly correct but seems nice. -fn trimRowForResizeLessCols(self: *Screen, old: *Screen, row: Row) []StorageCell { - assert(old.cols > self.cols); - - // We only trim if this isn't a wrapped line. If its a wrapped - // line we need to keep all the empty cells because they are - // meaningful whitespace before our wrap. - if (row.header().flags.wrap) return row.storage[1 .. old.cols + 1]; - - var i: usize = old.cols; - while (i > 0) : (i -= 1) { - const cell = row.getCell(i - 1); - if (!cell.empty()) { - // If we are beyond our new width and this is just - // an empty-character stylized cell, then we trim it. - // We also have to ignore wide spacers because they form - // a critical part of a wide character. - if (i > self.cols) { - if ((cell.char == 0 or cell.char == ' ') and - !cell.attrs.wide_spacer_tail and - !cell.attrs.wide_spacer_head) continue; - } - - break; - } - } - - return row.storage[1 .. i + 1]; -} - -/// Writes a basic string into the screen for testing. Newlines (\n) separate -/// each row. If a line is longer than the available columns, soft-wrapping -/// will occur. This will automatically handle basic wide chars. -pub fn testWriteString(self: *Screen, text: []const u8) !void { - var y: usize = self.cursor.y; - var x: usize = self.cursor.x; - - var grapheme: struct { - x: usize = 0, - cell: ?*Cell = null, - } = .{}; - - const view = std.unicode.Utf8View.init(text) catch unreachable; - var iter = view.iterator(); - while (iter.nextCodepoint()) |c| { - // Explicit newline forces a new row - if (c == '\n') { - y += 1; - x = 0; - grapheme = .{}; - continue; - } - - // If we're writing past the end of the active area, scroll. - if (y >= self.rows) { - y -= 1; - try self.scroll(.{ .screen = 1 }); - } - - // Get our row - var row = self.getRow(.{ .active = y }); - - // NOTE: graphemes are currently disabled - if (false) { - // If we have a previous cell, we check if we're part of a grapheme. - if (grapheme.cell) |prev_cell| { - const grapheme_break = brk: { - var state: u3 = 0; - var cp1 = @as(u21, @intCast(prev_cell.char)); - if (prev_cell.attrs.grapheme) { - var it = row.codepointIterator(grapheme.x); - while (it.next()) |cp2| { - assert(!ziglyph.graphemeBreak( - cp1, - cp2, - &state, - )); - - cp1 = cp2; - } - } - - break :brk ziglyph.graphemeBreak(cp1, c, &state); - }; - - if (!grapheme_break) { - try row.attachGrapheme(grapheme.x, c); - continue; - } - } - } - - const width: usize = @intCast(@max(0, ziglyph.display_width.codePointWidth(c, .half))); - //log.warn("c={x} width={}", .{ c, width }); - - // Zero-width are attached as grapheme data. - // NOTE: if/when grapheme clustering is ever enabled (above) this - // is not necessary - if (width == 0) { - if (grapheme.cell != null) { - try row.attachGrapheme(grapheme.x, c); - } - - continue; - } - - // If we're writing past the end, we need to soft wrap. - if (x == self.cols) { - row.setWrapped(true); - y += 1; - x = 0; - if (y >= self.rows) { - y -= 1; - try self.scroll(.{ .screen = 1 }); - } - row = self.getRow(.{ .active = y }); - } - - // If our character is double-width, handle it. - assert(width == 1 or width == 2); - switch (width) { - 1 => { - const cell = row.getCellPtr(x); - cell.* = self.cursor.pen; - cell.char = @intCast(c); - - grapheme.x = x; - grapheme.cell = cell; - }, - - 2 => { - if (x == self.cols - 1) { - const cell = row.getCellPtr(x); - cell.char = ' '; - cell.attrs.wide_spacer_head = true; - - // wrap - row.setWrapped(true); - y += 1; - x = 0; - if (y >= self.rows) { - y -= 1; - try self.scroll(.{ .screen = 1 }); - } - row = self.getRow(.{ .active = y }); - } - - { - const cell = row.getCellPtr(x); - cell.* = self.cursor.pen; - cell.char = @intCast(c); - cell.attrs.wide = true; - - grapheme.x = x; - grapheme.cell = cell; - } - - { - x += 1; - const cell = row.getCellPtr(x); - cell.char = ' '; - cell.attrs.wide_spacer_tail = true; - } - }, - - else => unreachable, - } - - x += 1; - } - - // So the cursor doesn't go off screen - self.cursor.x = @min(x, self.cols - 1); - self.cursor.y = y; -} - -/// Options for dumping the screen to a string. -pub const Dump = struct { - /// The start and end rows. These don't have to be in order, the dump - /// function will automatically sort them. - start: RowIndex, - end: RowIndex, - - /// If true, this will unwrap soft-wrapped lines into a single line. - unwrap: bool = true, -}; - -/// Dump the screen to a string. The writer given should be buffered; -/// this function does not attempt to efficiently write and generally writes -/// one byte at a time. -/// -/// TODO: look at selectionString implementation for more efficiency -/// TODO: change selectionString to use this too after above todo -pub fn dumpString(self: *Screen, writer: anytype, opts: Dump) !void { - const start_screen = opts.start.toScreen(self); - const end_screen = opts.end.toScreen(self); - - // If we have no rows in our screen, do nothing. - const rows_written = self.rowsWritten(); - if (rows_written == 0) return; - - // Get the actual top and bottom y values. This handles situations - // where start/end are backwards. - const y_top = @min(start_screen.screen, end_screen.screen); - const y_bottom = @min( - @max(start_screen.screen, end_screen.screen), - rows_written - 1, - ); - - // This keeps track of the number of blank rows we see. We don't want - // to output blank rows unless they're followed by a non-blank row. - var blank_rows: usize = 0; - - // Iterate through the rows - var y: usize = y_top; - while (y <= y_bottom) : (y += 1) { - const row = self.getRow(.{ .screen = y }); - - // Handle blank rows - if (row.isEmpty()) { - blank_rows += 1; - continue; - } - if (blank_rows > 0) { - for (0..blank_rows) |_| try writer.writeByte('\n'); - blank_rows = 0; - } - - if (!row.header().flags.wrap) { - // If we're not wrapped, we always add a newline. - blank_rows += 1; - } else if (!opts.unwrap) { - // If we are wrapped, we only add a new line if we're unwrapping - // soft-wrapped lines. - blank_rows += 1; - } - - // Output each of the cells - var cells = row.cellIterator(); - var spacers: usize = 0; - while (cells.next()) |cell| { - // Skip spacers - if (cell.attrs.wide_spacer_head or cell.attrs.wide_spacer_tail) continue; - - // If we have a zero value, then we accumulate a counter. We - // only want to turn zero values into spaces if we have a non-zero - // char sometime later. - if (cell.char == 0) { - spacers += 1; - continue; - } - if (spacers > 0) { - for (0..spacers) |_| try writer.writeByte(' '); - spacers = 0; - } - - const codepoint: u21 = @intCast(cell.char); - try writer.print("{u}", .{codepoint}); - - var it = row.codepointIterator(cells.i - 1); - while (it.next()) |cp| { - try writer.print("{u}", .{cp}); - } - } - } -} - -/// Turns the screen into a string. Different regions of the screen can -/// be selected using the "tag", i.e. if you want to output the viewport, -/// the scrollback, the full screen, etc. -/// -/// This is only useful for testing. -pub fn testString(self: *Screen, alloc: Allocator, tag: RowIndexTag) ![]const u8 { - var builder = std.ArrayList(u8).init(alloc); - defer builder.deinit(); - try self.dumpString(builder.writer(), .{ - .start = tag.index(0), - .end = tag.index(tag.maxLen(self) - 1), - - // historically our testString wants to view the screen as-is without - // unwrapping soft-wrapped lines so turn this off. - .unwrap = false, - }); - return try builder.toOwnedSlice(); -} - -test "Row: isEmpty with no data" { - const testing = std.testing; - const alloc = testing.allocator; - - var s = try init(alloc, 5, 5, 0); - defer s.deinit(); - - const row = s.getRow(.{ .active = 0 }); - try testing.expect(row.isEmpty()); -} - -test "Row: isEmpty with a character at the end" { - const testing = std.testing; - const alloc = testing.allocator; - - var s = try init(alloc, 5, 5, 0); - defer s.deinit(); - - const row = s.getRow(.{ .active = 0 }); - const cell = row.getCellPtr(4); - cell.*.char = 'A'; - try testing.expect(!row.isEmpty()); -} - -test "Row: isEmpty with only styled cells" { - const testing = std.testing; - const alloc = testing.allocator; - - var s = try init(alloc, 5, 5, 0); - defer s.deinit(); - - const row = s.getRow(.{ .active = 0 }); - for (0..s.cols) |x| { - const cell = row.getCellPtr(x); - cell.*.bg = .{ .rgb = .{ .r = 0xAA, .g = 0xBB, .b = 0xCC } }; - } - try testing.expect(row.isEmpty()); -} - -test "Row: clear with graphemes" { - const testing = std.testing; - const alloc = testing.allocator; - - var s = try init(alloc, 5, 5, 0); - defer s.deinit(); - - const row = s.getRow(.{ .active = 0 }); - try testing.expect(row.getId() > 0); - try testing.expectEqual(@as(usize, 5), row.lenCells()); - try testing.expect(!row.header().flags.grapheme); - - // Lets add a cell with a grapheme - { - const cell = row.getCellPtr(2); - cell.*.char = 'A'; - try row.attachGrapheme(2, 'B'); - try testing.expect(cell.attrs.grapheme); - try testing.expect(row.header().flags.grapheme); - try testing.expect(s.graphemes.count() == 1); - } - - // Clear the row - row.clear(.{}); - try testing.expect(!row.header().flags.grapheme); - try testing.expect(s.graphemes.count() == 0); -} - -test "Row: copy row with graphemes in destination" { - const testing = std.testing; - const alloc = testing.allocator; - - var s = try init(alloc, 5, 5, 0); - defer s.deinit(); - - // Source row does NOT have graphemes - const row_src = s.getRow(.{ .active = 0 }); - { - const cell = row_src.getCellPtr(2); - cell.*.char = 'A'; - } - - // Destination has graphemes - const row = s.getRow(.{ .active = 1 }); - { - const cell = row.getCellPtr(1); - cell.*.char = 'B'; - try row.attachGrapheme(1, 'C'); - try testing.expect(cell.attrs.grapheme); - try testing.expect(row.header().flags.grapheme); - try testing.expect(s.graphemes.count() == 1); - } - - // Copy - try row.copyRow(row_src); - try testing.expect(!row.header().flags.grapheme); - try testing.expect(s.graphemes.count() == 0); -} - -test "Row: copy row with graphemes in source" { - const testing = std.testing; - const alloc = testing.allocator; - - var s = try init(alloc, 5, 5, 0); - defer s.deinit(); - - // Source row does NOT have graphemes - const row_src = s.getRow(.{ .active = 0 }); - { - const cell = row_src.getCellPtr(2); - cell.*.char = 'A'; - try row_src.attachGrapheme(2, 'B'); - try testing.expect(cell.attrs.grapheme); - try testing.expect(row_src.header().flags.grapheme); - try testing.expect(s.graphemes.count() == 1); - } - - // Destination has no graphemes - const row = s.getRow(.{ .active = 1 }); - try row.copyRow(row_src); - try testing.expect(row.header().flags.grapheme); - try testing.expect(s.graphemes.count() == 2); - - row_src.clear(.{}); - try testing.expect(s.graphemes.count() == 1); -} - -test "Screen" { - const testing = std.testing; - const alloc = testing.allocator; - - var s = try init(alloc, 5, 5, 0); - defer s.deinit(); - try testing.expect(s.rowsWritten() == 0); - - // Sanity check that our test helpers work - const str = "1ABCD\n2EFGH\n3IJKL"; - try s.testWriteString(str); - try testing.expect(s.rowsWritten() == 3); - { - const contents = try s.testString(alloc, .screen); - defer alloc.free(contents); - try testing.expectEqualStrings(str, contents); - } - - // Test the row iterator - var count: usize = 0; - var iter = s.rowIterator(.viewport); - while (iter.next()) |row| { - // Rows should be pointer equivalent to getRow - const row_other = s.getRow(.{ .viewport = count }); - try testing.expectEqual(row.storage.ptr, row_other.storage.ptr); - count += 1; - } - - // Should go through all rows - try testing.expectEqual(@as(usize, 3), count); - - // Should be able to easily clear screen - { - var it = s.rowIterator(.viewport); - while (it.next()) |row| row.fill(.{ .char = 'A' }); - const contents = try s.testString(alloc, .screen); - defer alloc.free(contents); - try testing.expectEqualStrings("AAAAA\nAAAAA\nAAAAA", contents); - } -} - -test "Screen: write graphemes" { - const testing = std.testing; - const alloc = testing.allocator; - - var s = try init(alloc, 5, 5, 0); - defer s.deinit(); - - // Sanity check that our test helpers work - var buf: [32]u8 = undefined; - var buf_idx: usize = 0; - buf_idx += try std.unicode.utf8Encode(0x1F44D, buf[buf_idx..]); // Thumbs up plain - buf_idx += try std.unicode.utf8Encode(0x1F44D, buf[buf_idx..]); // Thumbs up plain - buf_idx += try std.unicode.utf8Encode(0x1F3FD, buf[buf_idx..]); // Medium skin tone - - // Note the assertions below are NOT the correct way to handle graphemes - // in general, but they're "correct" for historical purposes for terminals. - // For terminals, all double-wide codepoints are counted as part of the - // width. - - try s.testWriteString(buf[0..buf_idx]); - try testing.expect(s.rowsWritten() == 2); - try testing.expectEqual(@as(usize, 2), s.cursor.x); -} - -test "Screen: write long emoji" { - const testing = std.testing; - const alloc = testing.allocator; - - var s = try init(alloc, 5, 30, 0); - defer s.deinit(); - - // Sanity check that our test helpers work - var buf: [32]u8 = undefined; - var buf_idx: usize = 0; - buf_idx += try std.unicode.utf8Encode(0x1F9D4, buf[buf_idx..]); // man: beard - buf_idx += try std.unicode.utf8Encode(0x1F3FB, buf[buf_idx..]); // light skin tone (Fitz 1-2) - buf_idx += try std.unicode.utf8Encode(0x200D, buf[buf_idx..]); // ZWJ - buf_idx += try std.unicode.utf8Encode(0x2642, buf[buf_idx..]); // male sign - buf_idx += try std.unicode.utf8Encode(0xFE0F, buf[buf_idx..]); // emoji representation - - // Note the assertions below are NOT the correct way to handle graphemes - // in general, but they're "correct" for historical purposes for terminals. - // For terminals, all double-wide codepoints are counted as part of the - // width. - - try s.testWriteString(buf[0..buf_idx]); - try testing.expect(s.rowsWritten() == 1); - try testing.expectEqual(@as(usize, 5), s.cursor.x); -} - -// X -test "Screen: lineIterator" { - const testing = std.testing; - const alloc = testing.allocator; - - var s = try init(alloc, 5, 5, 0); - defer s.deinit(); - - // Sanity check that our test helpers work - const str = "1ABCD\n2EFGH"; - try s.testWriteString(str); - - // Test the line iterator - var iter = s.lineIterator(.viewport); - { - const line = iter.next().?; - const actual = try line.string(alloc); - defer alloc.free(actual); - try testing.expectEqualStrings("1ABCD", actual); - } - { - const line = iter.next().?; - const actual = try line.string(alloc); - defer alloc.free(actual); - try testing.expectEqualStrings("2EFGH", actual); - } - try testing.expect(iter.next() == null); -} - -// X -test "Screen: lineIterator soft wrap" { - const testing = std.testing; - const alloc = testing.allocator; - - var s = try init(alloc, 5, 5, 0); - defer s.deinit(); - - // Sanity check that our test helpers work - const str = "1ABCD2EFGH\n3ABCD"; - try s.testWriteString(str); - - // Test the line iterator - var iter = s.lineIterator(.viewport); - { - const line = iter.next().?; - const actual = try line.string(alloc); - defer alloc.free(actual); - try testing.expectEqualStrings("1ABCD2EFGH", actual); - } - { - const line = iter.next().?; - const actual = try line.string(alloc); - defer alloc.free(actual); - try testing.expectEqualStrings("3ABCD", actual); - } - try testing.expect(iter.next() == null); -} - -// X - selectLine in new screen -test "Screen: getLine soft wrap" { - const testing = std.testing; - const alloc = testing.allocator; - - var s = try init(alloc, 5, 5, 0); - defer s.deinit(); - - // Sanity check that our test helpers work - const str = "1ABCD2EFGH\n3ABCD"; - try s.testWriteString(str); - - // Test the line iterator - { - const line = s.getLine(.{ .x = 2, .y = 1 }).?; - const actual = try line.string(alloc); - defer alloc.free(actual); - try testing.expectEqualStrings("1ABCD2EFGH", actual); - } - { - const line = s.getLine(.{ .x = 2, .y = 2 }).?; - const actual = try line.string(alloc); - defer alloc.free(actual); - try testing.expectEqualStrings("3ABCD", actual); - } - - try testing.expect(s.getLine(.{ .x = 2, .y = 3 }) == null); - try testing.expect(s.getLine(.{ .x = 7, .y = 1 }) == null); -} - -// X -test "Screen: scrolling" { - const testing = std.testing; - const alloc = testing.allocator; - - var s = try init(alloc, 3, 5, 0); - defer s.deinit(); - s.cursor.pen.bg = .{ .rgb = .{ .r = 155 } }; - try s.testWriteString("1ABCD\n2EFGH\n3IJKL"); - try testing.expect(s.viewportIsBottom()); - - // Scroll down, should still be bottom - try s.scroll(.{ .screen = 1 }); - try testing.expect(s.viewportIsBottom()); - - { - // Test our contents rotated - const contents = try s.testString(alloc, .viewport); - defer alloc.free(contents); - try testing.expectEqualStrings("2EFGH\n3IJKL", contents); - } - { - // Test that our new row has the correct background - const cell = s.getCell(.active, 2, 0); - try testing.expectEqual(@as(u8, 155), cell.bg.rgb.r); - } - - // Scrolling to the bottom does nothing - try s.scroll(.{ .bottom = {} }); - - { - // Test our contents rotated - const contents = try s.testString(alloc, .viewport); - defer alloc.free(contents); - try testing.expectEqualStrings("2EFGH\n3IJKL", contents); - } -} - -// X -test "Screen: scroll down from 0" { - const testing = std.testing; - const alloc = testing.allocator; - - var s = try init(alloc, 3, 5, 0); - defer s.deinit(); - try s.testWriteString("1ABCD\n2EFGH\n3IJKL"); - - // Scrolling up does nothing, but allows it - try s.scroll(.{ .screen = -1 }); - try testing.expect(s.viewportIsBottom()); - - { - // Test our contents rotated - const contents = try s.testString(alloc, .viewport); - defer alloc.free(contents); - try testing.expectEqualStrings("1ABCD\n2EFGH\n3IJKL", contents); - } -} - -// X -test "Screen: scrollback" { - const testing = std.testing; - const alloc = testing.allocator; - - var s = try init(alloc, 3, 5, 1); - defer s.deinit(); - try s.testWriteString("1ABCD\n2EFGH\n3IJKL"); - try s.scroll(.{ .screen = 1 }); - - { - // Test our contents rotated - const contents = try s.testString(alloc, .viewport); - defer alloc.free(contents); - try testing.expectEqualStrings("2EFGH\n3IJKL", contents); - } - - // Scrolling to the bottom - try s.scroll(.{ .bottom = {} }); - try testing.expect(s.viewportIsBottom()); - - { - // Test our contents rotated - const contents = try s.testString(alloc, .viewport); - defer alloc.free(contents); - try testing.expectEqualStrings("2EFGH\n3IJKL", contents); - } - - // Scrolling back should make it visible again - try s.scroll(.{ .screen = -1 }); - try testing.expect(!s.viewportIsBottom()); - - { - // Test our contents rotated - const contents = try s.testString(alloc, .viewport); - defer alloc.free(contents); - try testing.expectEqualStrings("1ABCD\n2EFGH\n3IJKL", contents); - } - - // Scrolling back again should do nothing - try s.scroll(.{ .screen = -1 }); - - { - // Test our contents rotated - const contents = try s.testString(alloc, .viewport); - defer alloc.free(contents); - try testing.expectEqualStrings("1ABCD\n2EFGH\n3IJKL", contents); - } - - // Scrolling to the bottom - try s.scroll(.{ .bottom = {} }); - - { - // Test our contents rotated - const contents = try s.testString(alloc, .viewport); - defer alloc.free(contents); - try testing.expectEqualStrings("2EFGH\n3IJKL", contents); - } - - // Scrolling forward with no grow should do nothing - try s.scroll(.{ .viewport = 1 }); - - { - // Test our contents rotated - const contents = try s.testString(alloc, .viewport); - defer alloc.free(contents); - try testing.expectEqualStrings("2EFGH\n3IJKL", contents); - } - - // Scrolling to the top should work - try s.scroll(.{ .top = {} }); - - { - // Test our contents rotated - const contents = try s.testString(alloc, .viewport); - defer alloc.free(contents); - try testing.expectEqualStrings("1ABCD\n2EFGH\n3IJKL", contents); - } - - // Should be able to easily clear active area only - var it = s.rowIterator(.active); - while (it.next()) |row| row.clear(.{}); - { - const contents = try s.testString(alloc, .viewport); - defer alloc.free(contents); - try testing.expectEqualStrings("1ABCD", contents); - } - - // Scrolling to the bottom - try s.scroll(.{ .bottom = {} }); - - { - // Test our contents rotated - const contents = try s.testString(alloc, .viewport); - defer alloc.free(contents); - try testing.expectEqualStrings("", contents); - } -} - -// X -test "Screen: scrollback with large delta" { - const testing = std.testing; - const alloc = testing.allocator; - - var s = try init(alloc, 3, 5, 3); - defer s.deinit(); - try s.testWriteString("1ABCD\n2EFGH\n3IJKL\n4ABCD\n5EFGH\n6IJKL"); - try testing.expect(s.viewportIsBottom()); - - // Scroll to top - try s.scroll(.{ .top = {} }); - { - // Test our contents rotated - const contents = try s.testString(alloc, .viewport); - defer alloc.free(contents); - try testing.expectEqualStrings("1ABCD\n2EFGH\n3IJKL", contents); - } - - // Scroll down a ton - try s.scroll(.{ .viewport = 5 }); - try testing.expect(s.viewportIsBottom()); - { - // Test our contents rotated - const contents = try s.testString(alloc, .viewport); - defer alloc.free(contents); - try testing.expectEqualStrings("4ABCD\n5EFGH\n6IJKL", contents); - } -} - -// X -test "Screen: scrollback empty" { - const testing = std.testing; - const alloc = testing.allocator; - - var s = try init(alloc, 3, 5, 50); - defer s.deinit(); - try s.testWriteString("1ABCD\n2EFGH\n3IJKL"); - try s.scroll(.{ .viewport = 1 }); - - { - // Test our contents - const contents = try s.testString(alloc, .viewport); - defer alloc.free(contents); - try testing.expectEqualStrings("1ABCD\n2EFGH\n3IJKL", contents); - } -} - -// X -test "Screen: scrollback doesn't move viewport if not at bottom" { - const testing = std.testing; - const alloc = testing.allocator; - - var s = try init(alloc, 3, 5, 3); - defer s.deinit(); - try s.testWriteString("1ABCD\n2EFGH\n3IJKL\n4ABCD\n5EFGH"); - - // First test: we scroll up by 1, so we're not at the bottom anymore. - try s.scroll(.{ .screen = -1 }); - try testing.expect(!s.viewportIsBottom()); - { - const contents = try s.testString(alloc, .viewport); - defer alloc.free(contents); - try testing.expectEqualStrings("2EFGH\n3IJKL\n4ABCD", contents); - } - - // Next, we scroll back down by 1, this grows the scrollback but we - // shouldn't move. - try s.scroll(.{ .screen = 1 }); - try testing.expect(!s.viewportIsBottom()); - { - const contents = try s.testString(alloc, .viewport); - defer alloc.free(contents); - try testing.expectEqualStrings("2EFGH\n3IJKL\n4ABCD", contents); - } - - // Scroll again, this clears scrollback so we should move viewports - // but still see the same thing since our original view fits. - try s.scroll(.{ .screen = 1 }); - try testing.expect(!s.viewportIsBottom()); - { - const contents = try s.testString(alloc, .viewport); - defer alloc.free(contents); - try testing.expectEqualStrings("2EFGH\n3IJKL\n4ABCD", contents); - } - - // Scroll again, this again goes into scrollback but is now deleting - // what we were looking at. We should see changes. - try s.scroll(.{ .screen = 1 }); - try testing.expect(!s.viewportIsBottom()); - { - const contents = try s.testString(alloc, .viewport); - defer alloc.free(contents); - try testing.expectEqualStrings("3IJKL\n4ABCD\n5EFGH", contents); - } -} - -// X -test "Screen: scrolling moves selection" { - const testing = std.testing; - const alloc = testing.allocator; - - var s = try init(alloc, 3, 5, 0); - defer s.deinit(); - try s.testWriteString("1ABCD\n2EFGH\n3IJKL"); - try testing.expect(s.viewportIsBottom()); - - // Select a single line - s.selection = .{ - .start = .{ .x = 0, .y = 1 }, - .end = .{ .x = s.cols - 1, .y = 1 }, - }; - - // Scroll down, should still be bottom - try s.scroll(.{ .screen = 1 }); - try testing.expect(s.viewportIsBottom()); - - // Our selection should've moved up - try testing.expectEqual(Selection{ - .start = .{ .x = 0, .y = 0 }, - .end = .{ .x = s.cols - 1, .y = 0 }, - }, s.selection.?); - - { - // Test our contents rotated - const contents = try s.testString(alloc, .viewport); - defer alloc.free(contents); - try testing.expectEqualStrings("2EFGH\n3IJKL", contents); - } - - // Scrolling to the bottom does nothing - try s.scroll(.{ .bottom = {} }); - - // Our selection should've stayed the same - try testing.expectEqual(Selection{ - .start = .{ .x = 0, .y = 0 }, - .end = .{ .x = s.cols - 1, .y = 0 }, - }, s.selection.?); - - { - // Test our contents rotated - const contents = try s.testString(alloc, .viewport); - defer alloc.free(contents); - try testing.expectEqualStrings("2EFGH\n3IJKL", contents); - } - - // Scroll up again - try s.scroll(.{ .screen = 1 }); - - // Our selection should be null because it left the screen. - try testing.expect(s.selection == null); -} - -// X - I don't think this is right -test "Screen: scrolling with scrollback available doesn't move selection" { - const testing = std.testing; - const alloc = testing.allocator; - - var s = try init(alloc, 3, 5, 1); - defer s.deinit(); - try s.testWriteString("1ABCD\n2EFGH\n3IJKL"); - try testing.expect(s.viewportIsBottom()); - - // Select a single line - s.selection = .{ - .start = .{ .x = 0, .y = 1 }, - .end = .{ .x = s.cols - 1, .y = 1 }, - }; - - // Scroll down, should still be bottom - try s.scroll(.{ .screen = 1 }); - try testing.expect(s.viewportIsBottom()); - - // Our selection should NOT move since we have scrollback - try testing.expectEqual(Selection{ - .start = .{ .x = 0, .y = 1 }, - .end = .{ .x = s.cols - 1, .y = 1 }, - }, s.selection.?); - - { - // Test our contents rotated - const contents = try s.testString(alloc, .viewport); - defer alloc.free(contents); - try testing.expectEqualStrings("2EFGH\n3IJKL", contents); - } - - // Scrolling back should make it visible again - try s.scroll(.{ .screen = -1 }); - try testing.expect(!s.viewportIsBottom()); - - // Our selection should NOT move since we have scrollback - try testing.expectEqual(Selection{ - .start = .{ .x = 0, .y = 1 }, - .end = .{ .x = s.cols - 1, .y = 1 }, - }, s.selection.?); - - { - // Test our contents rotated - const contents = try s.testString(alloc, .viewport); - defer alloc.free(contents); - try testing.expectEqualStrings("1ABCD\n2EFGH\n3IJKL", contents); - } - - // Scroll down, this sends us off the scrollback - try s.scroll(.{ .screen = 2 }); - - // Selection should be gone since we selected a line that went off. - try testing.expect(s.selection == null); - - { - // Test our contents rotated - const contents = try s.testString(alloc, .viewport); - defer alloc.free(contents); - try testing.expectEqualStrings("3IJKL", contents); - } -} - -// X -test "Screen: scroll and clear full screen" { - const testing = std.testing; - const alloc = testing.allocator; - - var s = try init(alloc, 3, 5, 5); - defer s.deinit(); - try s.testWriteString("1ABCD\n2EFGH\n3IJKL"); - - { - const contents = try s.testString(alloc, .viewport); - defer alloc.free(contents); - try testing.expectEqualStrings("1ABCD\n2EFGH\n3IJKL", contents); - } - - try s.scroll(.{ .clear = {} }); - { - const contents = try s.testString(alloc, .viewport); - defer alloc.free(contents); - try testing.expectEqualStrings("", contents); - } - { - const contents = try s.testString(alloc, .screen); - defer alloc.free(contents); - try testing.expectEqualStrings("1ABCD\n2EFGH\n3IJKL", contents); - } -} - -// X -test "Screen: scroll and clear partial screen" { - const testing = std.testing; - const alloc = testing.allocator; - - var s = try init(alloc, 3, 5, 5); - defer s.deinit(); - try s.testWriteString("1ABCD\n2EFGH"); - - { - const contents = try s.testString(alloc, .viewport); - defer alloc.free(contents); - try testing.expectEqualStrings("1ABCD\n2EFGH", contents); - } - - try s.scroll(.{ .clear = {} }); - { - const contents = try s.testString(alloc, .viewport); - defer alloc.free(contents); - try testing.expectEqualStrings("", contents); - } - { - const contents = try s.testString(alloc, .screen); - defer alloc.free(contents); - try testing.expectEqualStrings("1ABCD\n2EFGH", contents); - } -} - -// X -test "Screen: scroll and clear empty screen" { - const testing = std.testing; - const alloc = testing.allocator; - - var s = try init(alloc, 3, 5, 5); - defer s.deinit(); - try s.scroll(.{ .clear = {} }); - try testing.expectEqual(@as(usize, 0), s.viewport); -} - -// X -test "Screen: scroll and clear ignore blank lines" { - const testing = std.testing; - const alloc = testing.allocator; - - var s = try init(alloc, 3, 5, 10); - defer s.deinit(); - try s.testWriteString("1ABCD\n2EFGH"); - try s.scroll(.{ .clear = {} }); - { - const contents = try s.testString(alloc, .viewport); - defer alloc.free(contents); - try testing.expectEqualStrings("", contents); - } - - // Move back to top-left - s.cursor.x = 0; - s.cursor.y = 0; - - // Write and clear - try s.testWriteString("3ABCD\n"); - try s.scroll(.{ .clear = {} }); - { - const contents = try s.testString(alloc, .viewport); - defer alloc.free(contents); - try testing.expectEqualStrings("", contents); - } - - // Move back to top-left - s.cursor.x = 0; - s.cursor.y = 0; - try s.testWriteString("X"); - - { - const contents = try s.testString(alloc, .screen); - defer alloc.free(contents); - try testing.expectEqualStrings("1ABCD\n2EFGH\n3ABCD\nX", contents); - } -} - -// X - i don't think we need rowIterator -test "Screen: history region with no scrollback" { - const testing = std.testing; - const alloc = testing.allocator; - - var s = try init(alloc, 1, 5, 0); - defer s.deinit(); - - // Write a bunch that WOULD invoke scrollback if exists - const str = "1ABCD\n2EFGH\n3IJKL"; - try s.testWriteString(str); - { - const contents = try s.testString(alloc, .screen); - defer alloc.free(contents); - const expected = "3IJKL"; - try testing.expectEqualStrings(expected, contents); - } - - // Verify no scrollback - var it = s.rowIterator(.history); - var count: usize = 0; - while (it.next()) |_| count += 1; - try testing.expect(count == 0); -} - -// X - duplicated test above -test "Screen: history region with scrollback" { - const testing = std.testing; - const alloc = testing.allocator; - - var s = try init(alloc, 1, 5, 2); - defer s.deinit(); - - // Write a bunch that WOULD invoke scrollback if exists - const str = "1ABCD\n2EFGH\n3IJKL"; - try s.testWriteString(str); - { - const contents = try s.testString(alloc, .viewport); - defer alloc.free(contents); - const expected = "3IJKL"; - try testing.expectEqualStrings(expected, contents); - } - { - // Test our contents - const contents = try s.testString(alloc, .screen); - defer alloc.free(contents); - try testing.expectEqualStrings("1ABCD\n2EFGH\n3IJKL", contents); - } - - { - const contents = try s.testString(alloc, .history); - defer alloc.free(contents); - const expected = "1ABCD\n2EFGH"; - try testing.expectEqualStrings(expected, contents); - } -} - -// X - don't need this, internal API -test "Screen: row copy" { - const testing = std.testing; - const alloc = testing.allocator; - - var s = try init(alloc, 3, 5, 0); - defer s.deinit(); - try s.testWriteString("1ABCD\n2EFGH\n3IJKL"); - - // Copy - try s.scroll(.{ .screen = 1 }); - try s.copyRow(.{ .active = 2 }, .{ .active = 0 }); - - // Test our contents - const contents = try s.testString(alloc, .viewport); - defer alloc.free(contents); - try testing.expectEqualStrings("2EFGH\n3IJKL\n2EFGH", contents); -} - -// X -test "Screen: clone" { - const testing = std.testing; - const alloc = testing.allocator; - - var s = try init(alloc, 3, 5, 0); - defer s.deinit(); - try s.testWriteString("1ABCD\n2EFGH\n3IJKL"); - try testing.expect(s.viewportIsBottom()); - - { - var s2 = try s.clone(alloc, .{ .active = 1 }, .{ .active = 1 }); - defer s2.deinit(); - - // Test our contents rotated - const contents = try s2.testString(alloc, .viewport); - defer alloc.free(contents); - try testing.expectEqualStrings("2EFGH", contents); - } - - { - var s2 = try s.clone(alloc, .{ .active = 1 }, .{ .active = 2 }); - defer s2.deinit(); - - // Test our contents rotated - const contents = try s2.testString(alloc, .viewport); - defer alloc.free(contents); - try testing.expectEqualStrings("2EFGH\n3IJKL", contents); - } -} - -// X -test "Screen: clone empty viewport" { - const testing = std.testing; - const alloc = testing.allocator; - - var s = try init(alloc, 3, 5, 0); - defer s.deinit(); - - { - var s2 = try s.clone(alloc, .{ .viewport = 0 }, .{ .viewport = 0 }); - defer s2.deinit(); - - // Test our contents rotated - const contents = try s2.testString(alloc, .viewport); - defer alloc.free(contents); - try testing.expectEqualStrings("", contents); - } -} - -// X -test "Screen: clone one line viewport" { - const testing = std.testing; - const alloc = testing.allocator; - - var s = try init(alloc, 3, 5, 0); - defer s.deinit(); - try s.testWriteString("1ABC"); - - { - var s2 = try s.clone(alloc, .{ .viewport = 0 }, .{ .viewport = 0 }); - defer s2.deinit(); - - // Test our contents - const contents = try s2.testString(alloc, .viewport); - defer alloc.free(contents); - try testing.expectEqualStrings("1ABC", contents); - } -} - -// X -test "Screen: clone empty active" { - const testing = std.testing; - const alloc = testing.allocator; - - var s = try init(alloc, 3, 5, 0); - defer s.deinit(); - - { - var s2 = try s.clone(alloc, .{ .active = 0 }, .{ .active = 0 }); - defer s2.deinit(); - - // Test our contents rotated - const contents = try s2.testString(alloc, .active); - defer alloc.free(contents); - try testing.expectEqualStrings("", contents); - } -} - -// X -test "Screen: clone one line active with extra space" { - const testing = std.testing; - const alloc = testing.allocator; - - var s = try init(alloc, 3, 5, 0); - defer s.deinit(); - try s.testWriteString("1ABC"); - - // Should have 1 line written - try testing.expectEqual(@as(usize, 1), s.rowsWritten()); - - { - var s2 = try s.clone(alloc, .{ .active = 0 }, .{ .active = s.rows - 1 }); - defer s2.deinit(); - - // Test our contents rotated - const contents = try s2.testString(alloc, .active); - defer alloc.free(contents); - try testing.expectEqualStrings("1ABC", contents); - } - - // Should still have no history. A bug was that we were generating history - // in this case which is not good! This was causing resizes to have all - // sorts of problems. - try testing.expectEqual(@as(usize, 1), s.rowsWritten()); -} - -// X -test "Screen: selectLine" { - const testing = std.testing; - const alloc = testing.allocator; - - var s = try init(alloc, 10, 10, 0); - defer s.deinit(); - try s.testWriteString("ABC DEF\n 123\n456"); - - // Outside of active area - try testing.expect(s.selectLine(.{ .x = 13, .y = 0 }) == null); - try testing.expect(s.selectLine(.{ .x = 0, .y = 5 }) == null); - - // Going forward - { - const sel = s.selectLine(.{ .x = 0, .y = 0 }).?; - try testing.expectEqual(@as(usize, 0), sel.start.x); - try testing.expectEqual(@as(usize, 0), sel.start.y); - try testing.expectEqual(@as(usize, 7), sel.end.x); - try testing.expectEqual(@as(usize, 0), sel.end.y); - } - - // Going backward - { - const sel = s.selectLine(.{ .x = 7, .y = 0 }).?; - try testing.expectEqual(@as(usize, 0), sel.start.x); - try testing.expectEqual(@as(usize, 0), sel.start.y); - try testing.expectEqual(@as(usize, 7), sel.end.x); - try testing.expectEqual(@as(usize, 0), sel.end.y); - } - - // Going forward and backward - { - const sel = s.selectLine(.{ .x = 3, .y = 0 }).?; - try testing.expectEqual(@as(usize, 0), sel.start.x); - try testing.expectEqual(@as(usize, 0), sel.start.y); - try testing.expectEqual(@as(usize, 7), sel.end.x); - try testing.expectEqual(@as(usize, 0), sel.end.y); - } - - // Outside active area - { - const sel = s.selectLine(.{ .x = 9, .y = 0 }).?; - try testing.expectEqual(@as(usize, 0), sel.start.x); - try testing.expectEqual(@as(usize, 0), sel.start.y); - try testing.expectEqual(@as(usize, 7), sel.end.x); - try testing.expectEqual(@as(usize, 0), sel.end.y); - } -} - -// X -test "Screen: selectAll" { - const testing = std.testing; - const alloc = testing.allocator; - - var s = try init(alloc, 10, 10, 0); - defer s.deinit(); - - { - try s.testWriteString("ABC DEF\n 123\n456"); - const sel = s.selectAll().?; - try testing.expectEqual(@as(usize, 0), sel.start.x); - try testing.expectEqual(@as(usize, 0), sel.start.y); - try testing.expectEqual(@as(usize, 2), sel.end.x); - try testing.expectEqual(@as(usize, 2), sel.end.y); - } - - { - try s.testWriteString("\nFOO\n BAR\n BAZ\n QWERTY\n 12345678"); - const sel = s.selectAll().?; - try testing.expectEqual(@as(usize, 0), sel.start.x); - try testing.expectEqual(@as(usize, 0), sel.start.y); - try testing.expectEqual(@as(usize, 8), sel.end.x); - try testing.expectEqual(@as(usize, 7), sel.end.y); - } -} - -// X -test "Screen: selectLine across soft-wrap" { - const testing = std.testing; - const alloc = testing.allocator; - - var s = try init(alloc, 10, 5, 0); - defer s.deinit(); - try s.testWriteString(" 12 34012 \n 123"); - - // Going forward - { - const sel = s.selectLine(.{ .x = 1, .y = 0 }).?; - try testing.expectEqual(@as(usize, 1), sel.start.x); - try testing.expectEqual(@as(usize, 0), sel.start.y); - try testing.expectEqual(@as(usize, 3), sel.end.x); - try testing.expectEqual(@as(usize, 1), sel.end.y); - } -} - -// X -// https://github.com/mitchellh/ghostty/issues/1329 -test "Screen: selectLine semantic prompt boundary" { - const testing = std.testing; - const alloc = testing.allocator; - - var s = try init(alloc, 10, 5, 0); - defer s.deinit(); - try s.testWriteString("ABCDE\nA > "); - - { - const contents = try s.testString(alloc, .screen); - defer alloc.free(contents); - try testing.expectEqualStrings("ABCDE\nA \n> ", contents); - } - - var row = s.getRow(.{ .screen = 2 }); - row.setSemanticPrompt(.prompt); - - // Selecting output stops at the prompt even if soft-wrapped - { - const sel = s.selectLine(.{ .x = 1, .y = 1 }).?; - try testing.expectEqual(@as(usize, 0), sel.start.x); - try testing.expectEqual(@as(usize, 1), sel.start.y); - try testing.expectEqual(@as(usize, 0), sel.end.x); - try testing.expectEqual(@as(usize, 1), sel.end.y); - } - { - const sel = s.selectLine(.{ .x = 1, .y = 2 }).?; - try testing.expectEqual(@as(usize, 0), sel.start.x); - try testing.expectEqual(@as(usize, 2), sel.start.y); - try testing.expectEqual(@as(usize, 0), sel.end.x); - try testing.expectEqual(@as(usize, 2), sel.end.y); - } -} - -// X -test "Screen: selectLine across soft-wrap ignores blank lines" { - const testing = std.testing; - const alloc = testing.allocator; - - var s = try init(alloc, 10, 5, 0); - defer s.deinit(); - try s.testWriteString(" 12 34012 \n 123"); - - // Going forward - { - const sel = s.selectLine(.{ .x = 1, .y = 0 }).?; - try testing.expectEqual(@as(usize, 1), sel.start.x); - try testing.expectEqual(@as(usize, 0), sel.start.y); - try testing.expectEqual(@as(usize, 3), sel.end.x); - try testing.expectEqual(@as(usize, 1), sel.end.y); - } - - // Going backward - { - const sel = s.selectLine(.{ .x = 1, .y = 1 }).?; - try testing.expectEqual(@as(usize, 1), sel.start.x); - try testing.expectEqual(@as(usize, 0), sel.start.y); - try testing.expectEqual(@as(usize, 3), sel.end.x); - try testing.expectEqual(@as(usize, 1), sel.end.y); - } - - // Going forward and backward - { - const sel = s.selectLine(.{ .x = 3, .y = 0 }).?; - try testing.expectEqual(@as(usize, 1), sel.start.x); - try testing.expectEqual(@as(usize, 0), sel.start.y); - try testing.expectEqual(@as(usize, 3), sel.end.x); - try testing.expectEqual(@as(usize, 1), sel.end.y); - } -} - -// X -test "Screen: selectLine with scrollback" { - const testing = std.testing; - const alloc = testing.allocator; - - var s = try init(alloc, 3, 2, 5); - defer s.deinit(); - try s.testWriteString("1A\n2B\n3C\n4D\n5E"); - - // Selecting first line - { - const sel = s.selectLine(.{ .x = 0, .y = 0 }).?; - try testing.expectEqual(@as(usize, 0), sel.start.x); - try testing.expectEqual(@as(usize, 0), sel.start.y); - try testing.expectEqual(@as(usize, 1), sel.end.x); - try testing.expectEqual(@as(usize, 0), sel.end.y); - } - - // Selecting last line - { - const sel = s.selectLine(.{ .x = 0, .y = 4 }).?; - try testing.expectEqual(@as(usize, 0), sel.start.x); - try testing.expectEqual(@as(usize, 4), sel.start.y); - try testing.expectEqual(@as(usize, 1), sel.end.x); - try testing.expectEqual(@as(usize, 4), sel.end.y); - } -} - -// X -test "Screen: selectWord" { - const testing = std.testing; - const alloc = testing.allocator; - - var s = try init(alloc, 10, 10, 0); - defer s.deinit(); - try s.testWriteString("ABC DEF\n 123\n456"); - - // Outside of active area - try testing.expect(s.selectWord(.{ .x = 9, .y = 0 }) == null); - try testing.expect(s.selectWord(.{ .x = 0, .y = 5 }) == null); - - // Going forward - { - const sel = s.selectWord(.{ .x = 0, .y = 0 }).?; - try testing.expectEqual(@as(usize, 0), sel.start.x); - try testing.expectEqual(@as(usize, 0), sel.start.y); - try testing.expectEqual(@as(usize, 2), sel.end.x); - try testing.expectEqual(@as(usize, 0), sel.end.y); - } - - // Going backward - { - const sel = s.selectWord(.{ .x = 2, .y = 0 }).?; - try testing.expectEqual(@as(usize, 0), sel.start.x); - try testing.expectEqual(@as(usize, 0), sel.start.y); - try testing.expectEqual(@as(usize, 2), sel.end.x); - try testing.expectEqual(@as(usize, 0), sel.end.y); - } - - // Going forward and backward - { - const sel = s.selectWord(.{ .x = 1, .y = 0 }).?; - try testing.expectEqual(@as(usize, 0), sel.start.x); - try testing.expectEqual(@as(usize, 0), sel.start.y); - try testing.expectEqual(@as(usize, 2), sel.end.x); - try testing.expectEqual(@as(usize, 0), sel.end.y); - } - - // Whitespace - { - const sel = s.selectWord(.{ .x = 3, .y = 0 }).?; - try testing.expectEqual(@as(usize, 3), sel.start.x); - try testing.expectEqual(@as(usize, 0), sel.start.y); - try testing.expectEqual(@as(usize, 4), sel.end.x); - try testing.expectEqual(@as(usize, 0), sel.end.y); - } - - // Whitespace single char - { - const sel = s.selectWord(.{ .x = 0, .y = 1 }).?; - try testing.expectEqual(@as(usize, 0), sel.start.x); - try testing.expectEqual(@as(usize, 1), sel.start.y); - try testing.expectEqual(@as(usize, 0), sel.end.x); - try testing.expectEqual(@as(usize, 1), sel.end.y); - } - - // End of screen - { - const sel = s.selectWord(.{ .x = 1, .y = 2 }).?; - try testing.expectEqual(@as(usize, 0), sel.start.x); - try testing.expectEqual(@as(usize, 2), sel.start.y); - try testing.expectEqual(@as(usize, 2), sel.end.x); - try testing.expectEqual(@as(usize, 2), sel.end.y); - } -} - -// X -test "Screen: selectWord across soft-wrap" { - const testing = std.testing; - const alloc = testing.allocator; - - var s = try init(alloc, 10, 5, 0); - defer s.deinit(); - try s.testWriteString(" 1234012\n 123"); - - // Going forward - { - const sel = s.selectWord(.{ .x = 1, .y = 0 }).?; - try testing.expectEqual(@as(usize, 1), sel.start.x); - try testing.expectEqual(@as(usize, 0), sel.start.y); - try testing.expectEqual(@as(usize, 2), sel.end.x); - try testing.expectEqual(@as(usize, 1), sel.end.y); - } - - // Going backward - { - const sel = s.selectWord(.{ .x = 1, .y = 1 }).?; - try testing.expectEqual(@as(usize, 1), sel.start.x); - try testing.expectEqual(@as(usize, 0), sel.start.y); - try testing.expectEqual(@as(usize, 2), sel.end.x); - try testing.expectEqual(@as(usize, 1), sel.end.y); - } - - // Going forward and backward - { - const sel = s.selectWord(.{ .x = 3, .y = 0 }).?; - try testing.expectEqual(@as(usize, 1), sel.start.x); - try testing.expectEqual(@as(usize, 0), sel.start.y); - try testing.expectEqual(@as(usize, 2), sel.end.x); - try testing.expectEqual(@as(usize, 1), sel.end.y); - } -} - -// X -test "Screen: selectWord whitespace across soft-wrap" { - const testing = std.testing; - const alloc = testing.allocator; - - var s = try init(alloc, 10, 5, 0); - defer s.deinit(); - try s.testWriteString("1 1\n 123"); - - // Going forward - { - const sel = s.selectWord(.{ .x = 1, .y = 0 }).?; - try testing.expectEqual(@as(usize, 1), sel.start.x); - try testing.expectEqual(@as(usize, 0), sel.start.y); - try testing.expectEqual(@as(usize, 2), sel.end.x); - try testing.expectEqual(@as(usize, 1), sel.end.y); - } - - // Going backward - { - const sel = s.selectWord(.{ .x = 1, .y = 1 }).?; - try testing.expectEqual(@as(usize, 1), sel.start.x); - try testing.expectEqual(@as(usize, 0), sel.start.y); - try testing.expectEqual(@as(usize, 2), sel.end.x); - try testing.expectEqual(@as(usize, 1), sel.end.y); - } - - // Going forward and backward - { - const sel = s.selectWord(.{ .x = 3, .y = 0 }).?; - try testing.expectEqual(@as(usize, 1), sel.start.x); - try testing.expectEqual(@as(usize, 0), sel.start.y); - try testing.expectEqual(@as(usize, 2), sel.end.x); - try testing.expectEqual(@as(usize, 1), sel.end.y); - } -} - -// X -test "Screen: selectWord with character boundary" { - const testing = std.testing; - const alloc = testing.allocator; - - const cases = [_][]const u8{ - " 'abc' \n123", - " \"abc\" \n123", - " │abc│ \n123", - " `abc` \n123", - " |abc| \n123", - " :abc: \n123", - " ,abc, \n123", - " (abc( \n123", - " )abc) \n123", - " [abc[ \n123", - " ]abc] \n123", - " {abc{ \n123", - " }abc} \n123", - " abc> \n123", - }; - - for (cases) |case| { - var s = try init(alloc, 10, 20, 0); - defer s.deinit(); - try s.testWriteString(case); - - // Inside character forward - { - const sel = s.selectWord(.{ .x = 2, .y = 0 }).?; - try testing.expectEqual(@as(usize, 2), sel.start.x); - try testing.expectEqual(@as(usize, 0), sel.start.y); - try testing.expectEqual(@as(usize, 4), sel.end.x); - try testing.expectEqual(@as(usize, 0), sel.end.y); - } - - // Inside character backward - { - const sel = s.selectWord(.{ .x = 4, .y = 0 }).?; - try testing.expectEqual(@as(usize, 2), sel.start.x); - try testing.expectEqual(@as(usize, 0), sel.start.y); - try testing.expectEqual(@as(usize, 4), sel.end.x); - try testing.expectEqual(@as(usize, 0), sel.end.y); - } - - // Inside character bidirectional - { - const sel = s.selectWord(.{ .x = 3, .y = 0 }).?; - try testing.expectEqual(@as(usize, 2), sel.start.x); - try testing.expectEqual(@as(usize, 0), sel.start.y); - try testing.expectEqual(@as(usize, 4), sel.end.x); - try testing.expectEqual(@as(usize, 0), sel.end.y); - } - - // On quote - // NOTE: this behavior is not ideal, so we can change this one day, - // but I think its also not that important compared to the above. - { - const sel = s.selectWord(.{ .x = 1, .y = 0 }).?; - try testing.expectEqual(@as(usize, 0), sel.start.x); - try testing.expectEqual(@as(usize, 0), sel.start.y); - try testing.expectEqual(@as(usize, 1), sel.end.x); - try testing.expectEqual(@as(usize, 0), sel.end.y); - } - } -} - -// X -test "Screen: selectOutput" { - const testing = std.testing; - const alloc = testing.allocator; - - var s = try init(alloc, 15, 10, 0); - defer s.deinit(); - - // zig fmt: off - { - // line number: - try s.testWriteString("output1\n"); // 0 - try s.testWriteString("output1\n"); // 1 - try s.testWriteString("prompt2\n"); // 2 - try s.testWriteString("input2\n"); // 3 - try s.testWriteString("output2\n"); // 4 - try s.testWriteString("output2\n"); // 5 - try s.testWriteString("prompt3$ input3\n"); // 6 - try s.testWriteString("output3\n"); // 7 - try s.testWriteString("output3\n"); // 8 - try s.testWriteString("output3"); // 9 - } - // zig fmt: on - - var row = s.getRow(.{ .screen = 2 }); - row.setSemanticPrompt(.prompt); - row = s.getRow(.{ .screen = 3 }); - row.setSemanticPrompt(.input); - row = s.getRow(.{ .screen = 4 }); - row.setSemanticPrompt(.command); - row = s.getRow(.{ .screen = 6 }); - row.setSemanticPrompt(.input); - row = s.getRow(.{ .screen = 7 }); - row.setSemanticPrompt(.command); - - // No start marker, should select from the beginning - { - const sel = s.selectOutput(.{ .x = 1, .y = 1 }).?; - try testing.expectEqual(@as(usize, 0), sel.start.x); - try testing.expectEqual(@as(usize, 0), sel.start.y); - try testing.expectEqual(@as(usize, 10), sel.end.x); - try testing.expectEqual(@as(usize, 1), sel.end.y); - } - // Both start and end markers, should select between them - { - const sel = s.selectOutput(.{ .x = 3, .y = 5 }).?; - try testing.expectEqual(@as(usize, 0), sel.start.x); - try testing.expectEqual(@as(usize, 4), sel.start.y); - try testing.expectEqual(@as(usize, 10), sel.end.x); - try testing.expectEqual(@as(usize, 5), sel.end.y); - } - // No end marker, should select till the end - { - const sel = s.selectOutput(.{ .x = 2, .y = 7 }).?; - try testing.expectEqual(@as(usize, 0), sel.start.x); - try testing.expectEqual(@as(usize, 7), sel.start.y); - try testing.expectEqual(@as(usize, 9), sel.end.x); - try testing.expectEqual(@as(usize, 10), sel.end.y); - } - // input / prompt at y = 0, pt.y = 0 - { - s.deinit(); - s = try init(alloc, 5, 10, 0); - try s.testWriteString("prompt1$ input1\n"); - try s.testWriteString("output1\n"); - try s.testWriteString("prompt2\n"); - row = s.getRow(.{ .screen = 0 }); - row.setSemanticPrompt(.input); - row = s.getRow(.{ .screen = 1 }); - row.setSemanticPrompt(.command); - try testing.expect(s.selectOutput(.{ .x = 2, .y = 0 }) == null); - } -} - -// X -test "Screen: selectPrompt basics" { - const testing = std.testing; - const alloc = testing.allocator; - - var s = try init(alloc, 15, 10, 0); - defer s.deinit(); - - // zig fmt: off - { - // line number: - try s.testWriteString("output1\n"); // 0 - try s.testWriteString("output1\n"); // 1 - try s.testWriteString("prompt2\n"); // 2 - try s.testWriteString("input2\n"); // 3 - try s.testWriteString("output2\n"); // 4 - try s.testWriteString("output2\n"); // 5 - try s.testWriteString("prompt3$ input3\n"); // 6 - try s.testWriteString("output3\n"); // 7 - try s.testWriteString("output3\n"); // 8 - try s.testWriteString("output3"); // 9 - } - // zig fmt: on - - var row = s.getRow(.{ .screen = 2 }); - row.setSemanticPrompt(.prompt); - row = s.getRow(.{ .screen = 3 }); - row.setSemanticPrompt(.input); - row = s.getRow(.{ .screen = 4 }); - row.setSemanticPrompt(.command); - row = s.getRow(.{ .screen = 6 }); - row.setSemanticPrompt(.input); - row = s.getRow(.{ .screen = 7 }); - row.setSemanticPrompt(.command); - - // Not at a prompt - { - const sel = s.selectPrompt(.{ .x = 0, .y = 1 }); - try testing.expect(sel == null); - } - { - const sel = s.selectPrompt(.{ .x = 0, .y = 8 }); - try testing.expect(sel == null); - } - - // Single line prompt - { - const sel = s.selectPrompt(.{ .x = 1, .y = 6 }).?; - try testing.expectEqual(Selection{ - .start = .{ .x = 0, .y = 6 }, - .end = .{ .x = 9, .y = 6 }, - }, sel); - } - - // Multi line prompt - { - const sel = s.selectPrompt(.{ .x = 1, .y = 3 }).?; - try testing.expectEqual(Selection{ - .start = .{ .x = 0, .y = 2 }, - .end = .{ .x = 9, .y = 3 }, - }, sel); - } -} - -// X -test "Screen: selectPrompt prompt at start" { - const testing = std.testing; - const alloc = testing.allocator; - - var s = try init(alloc, 15, 10, 0); - defer s.deinit(); - - // zig fmt: off - { - // line number: - try s.testWriteString("prompt1\n"); // 0 - try s.testWriteString("input1\n"); // 1 - try s.testWriteString("output2\n"); // 2 - try s.testWriteString("output2\n"); // 3 - } - // zig fmt: on - - var row = s.getRow(.{ .screen = 0 }); - row.setSemanticPrompt(.prompt); - row = s.getRow(.{ .screen = 1 }); - row.setSemanticPrompt(.input); - row = s.getRow(.{ .screen = 2 }); - row.setSemanticPrompt(.command); - - // Not at a prompt - { - const sel = s.selectPrompt(.{ .x = 0, .y = 3 }); - try testing.expect(sel == null); - } - - // Multi line prompt - { - const sel = s.selectPrompt(.{ .x = 1, .y = 1 }).?; - try testing.expectEqual(Selection{ - .start = .{ .x = 0, .y = 0 }, - .end = .{ .x = 9, .y = 1 }, - }, sel); - } -} - -// X -test "Screen: selectPrompt prompt at end" { - const testing = std.testing; - const alloc = testing.allocator; - - var s = try init(alloc, 15, 10, 0); - defer s.deinit(); - - // zig fmt: off - { - // line number: - try s.testWriteString("output2\n"); // 0 - try s.testWriteString("output2\n"); // 1 - try s.testWriteString("prompt1\n"); // 2 - try s.testWriteString("input1\n"); // 3 - } - // zig fmt: on - - var row = s.getRow(.{ .screen = 2 }); - row.setSemanticPrompt(.prompt); - row = s.getRow(.{ .screen = 3 }); - row.setSemanticPrompt(.input); - - // Not at a prompt - { - const sel = s.selectPrompt(.{ .x = 0, .y = 1 }); - try testing.expect(sel == null); - } - - // Multi line prompt - { - const sel = s.selectPrompt(.{ .x = 1, .y = 2 }).?; - try testing.expectEqual(Selection{ - .start = .{ .x = 0, .y = 2 }, - .end = .{ .x = 9, .y = 3 }, - }, sel); - } -} - -// X -test "Screen: promptPath" { - const testing = std.testing; - const alloc = testing.allocator; - - var s = try init(alloc, 15, 10, 0); - defer s.deinit(); - - // zig fmt: off - { - // line number: - try s.testWriteString("output1\n"); // 0 - try s.testWriteString("output1\n"); // 1 - try s.testWriteString("prompt2\n"); // 2 - try s.testWriteString("input2\n"); // 3 - try s.testWriteString("output2\n"); // 4 - try s.testWriteString("output2\n"); // 5 - try s.testWriteString("prompt3$ input3\n"); // 6 - try s.testWriteString("output3\n"); // 7 - try s.testWriteString("output3\n"); // 8 - try s.testWriteString("output3"); // 9 - } - // zig fmt: on - - var row = s.getRow(.{ .screen = 2 }); - row.setSemanticPrompt(.prompt); - row = s.getRow(.{ .screen = 3 }); - row.setSemanticPrompt(.input); - row = s.getRow(.{ .screen = 4 }); - row.setSemanticPrompt(.command); - row = s.getRow(.{ .screen = 6 }); - row.setSemanticPrompt(.input); - row = s.getRow(.{ .screen = 7 }); - row.setSemanticPrompt(.command); - - // From is not in the prompt - { - const path = s.promptPath( - .{ .x = 0, .y = 1 }, - .{ .x = 0, .y = 2 }, - ); - try testing.expectEqual(@as(isize, 0), path.x); - try testing.expectEqual(@as(isize, 0), path.y); - } - - // Same line - { - const path = s.promptPath( - .{ .x = 6, .y = 2 }, - .{ .x = 3, .y = 2 }, - ); - try testing.expectEqual(@as(isize, -3), path.x); - try testing.expectEqual(@as(isize, 0), path.y); - } - - // Different lines - { - const path = s.promptPath( - .{ .x = 6, .y = 2 }, - .{ .x = 3, .y = 3 }, - ); - try testing.expectEqual(@as(isize, -3), path.x); - try testing.expectEqual(@as(isize, 1), path.y); - } - - // To is out of bounds before - { - const path = s.promptPath( - .{ .x = 6, .y = 2 }, - .{ .x = 3, .y = 1 }, - ); - try testing.expectEqual(@as(isize, -6), path.x); - try testing.expectEqual(@as(isize, 0), path.y); - } - - // To is out of bounds after - { - const path = s.promptPath( - .{ .x = 6, .y = 2 }, - .{ .x = 3, .y = 9 }, - ); - try testing.expectEqual(@as(isize, 3), path.x); - try testing.expectEqual(@as(isize, 1), path.y); - } -} - -// X - we don't use this in new terminal -test "Screen: scrollRegionUp single" { - const testing = std.testing; - const alloc = testing.allocator; - - var s = try init(alloc, 4, 5, 0); - defer s.deinit(); - try s.testWriteString("1ABCD\n2EFGH\n3IJKL\n4ABCD"); - - s.scrollRegionUp(.{ .active = 1 }, .{ .active = 2 }, 1); - { - // Test our contents rotated - const contents = try s.testString(alloc, .screen); - defer alloc.free(contents); - try testing.expectEqualStrings("1ABCD\n3IJKL\n\n4ABCD", contents); - } -} - -// X - we don't use this in new terminal -test "Screen: scrollRegionUp same line" { - const testing = std.testing; - const alloc = testing.allocator; - - var s = try init(alloc, 4, 5, 0); - defer s.deinit(); - try s.testWriteString("1ABCD\n2EFGH\n3IJKL\n4ABCD"); - - s.scrollRegionUp(.{ .active = 1 }, .{ .active = 1 }, 1); - { - // Test our contents rotated - const contents = try s.testString(alloc, .screen); - defer alloc.free(contents); - try testing.expectEqualStrings("1ABCD\n2EFGH\n3IJKL\n4ABCD", contents); - } -} - -// X - we don't use this in new terminal -test "Screen: scrollRegionUp single with pen" { - const testing = std.testing; - const alloc = testing.allocator; - - var s = try init(alloc, 4, 5, 0); - defer s.deinit(); - try s.testWriteString("1ABCD\n2EFGH\n3IJKL\n4ABCD"); - - s.cursor.pen = .{ .char = 'X' }; - s.cursor.pen.bg = .{ .rgb = .{ .r = 155 } }; - s.cursor.pen.attrs.bold = true; - s.scrollRegionUp(.{ .active = 1 }, .{ .active = 2 }, 1); - { - // Test our contents rotated - const contents = try s.testString(alloc, .screen); - defer alloc.free(contents); - try testing.expectEqualStrings("1ABCD\n3IJKL\n\n4ABCD", contents); - const cell = s.getCell(.active, 2, 0); - try testing.expectEqual(@as(u8, 155), cell.bg.rgb.r); - try testing.expect(!cell.attrs.bold); - try testing.expect(s.cursor.pen.attrs.bold); - } -} - -// X - we don't use this in new terminal -test "Screen: scrollRegionUp multiple" { - const testing = std.testing; - const alloc = testing.allocator; - - var s = try init(alloc, 4, 5, 0); - defer s.deinit(); - try s.testWriteString("1ABCD\n2EFGH\n3IJKL\n4ABCD"); - - s.scrollRegionUp(.{ .active = 1 }, .{ .active = 3 }, 1); - { - // Test our contents rotated - const contents = try s.testString(alloc, .screen); - defer alloc.free(contents); - try testing.expectEqualStrings("1ABCD\n3IJKL\n4ABCD", contents); - } -} - -// X - we don't use this in new terminal -test "Screen: scrollRegionUp multiple count" { - const testing = std.testing; - const alloc = testing.allocator; - - var s = try init(alloc, 4, 5, 0); - defer s.deinit(); - try s.testWriteString("1ABCD\n2EFGH\n3IJKL\n4ABCD"); - - s.scrollRegionUp(.{ .active = 1 }, .{ .active = 3 }, 2); - { - // Test our contents rotated - const contents = try s.testString(alloc, .screen); - defer alloc.free(contents); - try testing.expectEqualStrings("1ABCD\n4ABCD", contents); - } -} - -// X - we don't use this in new terminal -test "Screen: scrollRegionUp count greater than available lines" { - const testing = std.testing; - const alloc = testing.allocator; - - var s = try init(alloc, 4, 5, 0); - defer s.deinit(); - try s.testWriteString("1ABCD\n2EFGH\n3IJKL\n4ABCD"); - - s.scrollRegionUp(.{ .active = 1 }, .{ .active = 2 }, 10); - { - // Test our contents rotated - const contents = try s.testString(alloc, .screen); - defer alloc.free(contents); - try testing.expectEqualStrings("1ABCD\n\n\n4ABCD", contents); - } -} -// X - we don't use this in new terminal -test "Screen: scrollRegionUp fills with pen" { - const testing = std.testing; - const alloc = testing.allocator; - - var s = try init(alloc, 4, 5, 0); - defer s.deinit(); - try s.testWriteString("A\nB\nC\nD"); - - s.cursor.pen = .{ .char = 'X' }; - s.cursor.pen.bg = .{ .rgb = .{ .r = 155 } }; - s.cursor.pen.attrs.bold = true; - s.scrollRegionUp(.{ .active = 0 }, .{ .active = 2 }, 1); - { - // Test our contents rotated - const contents = try s.testString(alloc, .screen); - defer alloc.free(contents); - try testing.expectEqualStrings("B\nC\n\nD", contents); - const cell = s.getCell(.active, 2, 0); - try testing.expectEqual(@as(u8, 155), cell.bg.rgb.r); - try testing.expect(!cell.attrs.bold); - try testing.expect(s.cursor.pen.attrs.bold); - } -} - -// X - we don't use this in new terminal -test "Screen: scrollRegionUp buffer wrap" { - const testing = std.testing; - const alloc = testing.allocator; - - var s = try init(alloc, 3, 5, 0); - defer s.deinit(); - try s.testWriteString("1ABCD\n2EFGH\n3IJKL"); - - // Scroll down, should still be bottom, but should wrap because - // we're out of space. - try s.scroll(.{ .screen = 1 }); - s.cursor.x = 0; - try s.testWriteString("1ABCD\n2EFGH\n3IJKL\n4ABCD"); - - // Scroll - s.cursor.pen = .{ .char = 'X' }; - s.cursor.pen.bg = .{ .rgb = .{ .r = 155 } }; - s.cursor.pen.attrs.bold = true; - s.scrollRegionUp(.{ .screen = 0 }, .{ .screen = 2 }, 1); - - { - // Test our contents rotated - const contents = try s.testString(alloc, .screen); - defer alloc.free(contents); - try testing.expectEqualStrings("3IJKL\n4ABCD", contents); - const cell = s.getCell(.active, 2, 0); - try testing.expectEqual(@as(u8, 155), cell.bg.rgb.r); - try testing.expect(!cell.attrs.bold); - try testing.expect(s.cursor.pen.attrs.bold); - } -} - -// X - we don't use this in new terminal -test "Screen: scrollRegionUp buffer wrap alternate" { - const testing = std.testing; - const alloc = testing.allocator; - - var s = try init(alloc, 3, 5, 0); - defer s.deinit(); - try s.testWriteString("1ABCD\n2EFGH\n3IJKL"); - - // Scroll down, should still be bottom, but should wrap because - // we're out of space. - try s.scroll(.{ .screen = 1 }); - s.cursor.x = 0; - try s.testWriteString("1ABCD\n2EFGH\n3IJKL\n4ABCD"); - - // Scroll - s.cursor.pen = .{ .char = 'X' }; - s.cursor.pen.bg = .{ .rgb = .{ .r = 155 } }; - s.cursor.pen.attrs.bold = true; - s.scrollRegionUp(.{ .screen = 0 }, .{ .screen = 2 }, 2); - - { - // Test our contents rotated - const contents = try s.testString(alloc, .screen); - defer alloc.free(contents); - try testing.expectEqualStrings("4ABCD", contents); - const cell = s.getCell(.active, 2, 0); - try testing.expectEqual(@as(u8, 155), cell.bg.rgb.r); - try testing.expect(!cell.attrs.bold); - try testing.expect(s.cursor.pen.attrs.bold); - } -} - -// X - we don't use this in new terminal -test "Screen: scrollRegionUp buffer wrap alternative with extra lines" { - const testing = std.testing; - const alloc = testing.allocator; - - var s = try init(alloc, 5, 5, 0); - defer s.deinit(); - - // We artificially mess with the circular buffer here. This was discovered - // when debugging https://github.com/mitchellh/ghostty/issues/315. I - // don't know how to "naturally" get the circular buffer into this state - // although it is obviously possible, verified through various - // asciinema casts. - // - // I think the proper way to recreate this state would be to fill - // the screen, scroll the correct number of times, clear the screen - // with a fill. I can try that later to ensure we're hitting the same - // code path. - s.storage.head = 24; - s.storage.tail = 24; - s.storage.full = true; - - // Scroll down, should still be bottom, but should wrap because - // we're out of space. - // try s.scroll(.{ .screen = 2 }); - // s.cursor.x = 0; - try s.testWriteString("1ABCD\n2EFGH\n3IJKL\n4ABCD\n5EFGH"); - - // Scroll - s.scrollRegionUp(.{ .screen = 0 }, .{ .screen = 3 }, 2); - - { - // Test our contents rotated - const contents = try s.testString(alloc, .screen); - defer alloc.free(contents); - try testing.expectEqualStrings("3IJKL\n4ABCD\n\n\n5EFGH", contents); - } -} - -// X -test "Screen: clear history with no history" { - const testing = std.testing; - const alloc = testing.allocator; - - var s = try init(alloc, 3, 5, 3); - defer s.deinit(); - try s.testWriteString("4ABCD\n5EFGH\n6IJKL"); - try testing.expect(s.viewportIsBottom()); - try s.clear(.history); - try testing.expect(s.viewportIsBottom()); - { - // Test our contents rotated - const contents = try s.testString(alloc, .viewport); - defer alloc.free(contents); - try testing.expectEqualStrings("4ABCD\n5EFGH\n6IJKL", contents); - } - { - // Test our contents rotated - const contents = try s.testString(alloc, .screen); - defer alloc.free(contents); - try testing.expectEqualStrings("4ABCD\n5EFGH\n6IJKL", contents); - } -} - -// X -test "Screen: clear history" { - const testing = std.testing; - const alloc = testing.allocator; - - var s = try init(alloc, 3, 5, 3); - defer s.deinit(); - try s.testWriteString("1ABCD\n2EFGH\n3IJKL\n4ABCD\n5EFGH\n6IJKL"); - try testing.expect(s.viewportIsBottom()); - - // Scroll to top - try s.scroll(.{ .top = {} }); - { - // Test our contents rotated - const contents = try s.testString(alloc, .viewport); - defer alloc.free(contents); - try testing.expectEqualStrings("1ABCD\n2EFGH\n3IJKL", contents); - } - - try s.clear(.history); - try testing.expect(s.viewportIsBottom()); - { - // Test our contents rotated - const contents = try s.testString(alloc, .viewport); - defer alloc.free(contents); - try testing.expectEqualStrings("4ABCD\n5EFGH\n6IJKL", contents); - } - { - // Test our contents rotated - const contents = try s.testString(alloc, .screen); - defer alloc.free(contents); - try testing.expectEqualStrings("4ABCD\n5EFGH\n6IJKL", contents); - } -} - -// X -test "Screen: clear above cursor" { - const testing = std.testing; - const alloc = testing.allocator; - - var s = try init(alloc, 10, 10, 3); - defer s.deinit(); - try s.testWriteString("4ABCD\n5EFGH\n6IJKL"); - try testing.expect(s.viewportIsBottom()); - try s.clear(.above_cursor); - try testing.expect(s.viewportIsBottom()); - { - const contents = try s.testString(alloc, .viewport); - defer alloc.free(contents); - try testing.expectEqualStrings("6IJKL", contents); - } - { - const contents = try s.testString(alloc, .screen); - defer alloc.free(contents); - try testing.expectEqualStrings("6IJKL", contents); - } - - try testing.expectEqual(@as(usize, 5), s.cursor.x); - try testing.expectEqual(@as(usize, 0), s.cursor.y); -} - -// X -test "Screen: clear above cursor with history" { - const testing = std.testing; - const alloc = testing.allocator; - - var s = try init(alloc, 3, 10, 3); - defer s.deinit(); - try s.testWriteString("1ABCD\n2EFGH\n3IJKL\n"); - try s.testWriteString("4ABCD\n5EFGH\n6IJKL"); - try testing.expect(s.viewportIsBottom()); - try s.clear(.above_cursor); - try testing.expect(s.viewportIsBottom()); - { - const contents = try s.testString(alloc, .viewport); - defer alloc.free(contents); - try testing.expectEqualStrings("6IJKL", contents); - } - { - const contents = try s.testString(alloc, .screen); - defer alloc.free(contents); - try testing.expectEqualStrings("1ABCD\n2EFGH\n3IJKL\n6IJKL", contents); - } - - try testing.expectEqual(@as(usize, 5), s.cursor.x); - try testing.expectEqual(@as(usize, 0), s.cursor.y); -} - -// X -test "Screen: selectionString basic" { - const testing = std.testing; - const alloc = testing.allocator; - - var s = try init(alloc, 3, 5, 0); - defer s.deinit(); - const str = "1ABCD\n2EFGH\n3IJKL"; - try s.testWriteString(str); - - { - const contents = try s.selectionString(alloc, .{ - .start = .{ .x = 0, .y = 1 }, - .end = .{ .x = 2, .y = 2 }, - }, true); - defer alloc.free(contents); - const expected = "2EFGH\n3IJ"; - try testing.expectEqualStrings(expected, contents); - } -} - -// X -test "Screen: selectionString start outside of written area" { - const testing = std.testing; - const alloc = testing.allocator; - - var s = try init(alloc, 10, 5, 0); - defer s.deinit(); - const str = "1ABCD\n2EFGH\n3IJKL"; - try s.testWriteString(str); - - { - const contents = try s.selectionString(alloc, .{ - .start = .{ .x = 0, .y = 5 }, - .end = .{ .x = 2, .y = 6 }, - }, true); - defer alloc.free(contents); - const expected = ""; - try testing.expectEqualStrings(expected, contents); - } -} - -// X -test "Screen: selectionString end outside of written area" { - const testing = std.testing; - const alloc = testing.allocator; - - var s = try init(alloc, 10, 5, 0); - defer s.deinit(); - const str = "1ABCD\n2EFGH\n3IJKL"; - try s.testWriteString(str); - - { - const contents = try s.selectionString(alloc, .{ - .start = .{ .x = 0, .y = 2 }, - .end = .{ .x = 2, .y = 6 }, - }, true); - defer alloc.free(contents); - const expected = "3IJKL"; - try testing.expectEqualStrings(expected, contents); - } -} - -// X -test "Screen: selectionString trim space" { - const testing = std.testing; - const alloc = testing.allocator; - - var s = try init(alloc, 3, 5, 0); - defer s.deinit(); - const str = "1AB \n2EFGH\n3IJKL"; - try s.testWriteString(str); - - { - const contents = try s.selectionString(alloc, .{ - .start = .{ .x = 0, .y = 0 }, - .end = .{ .x = 2, .y = 1 }, - }, true); - defer alloc.free(contents); - const expected = "1AB\n2EF"; - try testing.expectEqualStrings(expected, contents); - } - - // No trim - { - const contents = try s.selectionString(alloc, .{ - .start = .{ .x = 0, .y = 0 }, - .end = .{ .x = 2, .y = 1 }, - }, false); - defer alloc.free(contents); - const expected = "1AB \n2EF"; - try testing.expectEqualStrings(expected, contents); - } -} - -// X -test "Screen: selectionString trim empty line" { - const testing = std.testing; - const alloc = testing.allocator; - - var s = try init(alloc, 5, 5, 0); - defer s.deinit(); - const str = "1AB \n\n2EFGH\n3IJKL"; - try s.testWriteString(str); - - { - const contents = try s.selectionString(alloc, .{ - .start = .{ .x = 0, .y = 0 }, - .end = .{ .x = 2, .y = 2 }, - }, true); - defer alloc.free(contents); - const expected = "1AB\n\n2EF"; - try testing.expectEqualStrings(expected, contents); - } - - // No trim - { - const contents = try s.selectionString(alloc, .{ - .start = .{ .x = 0, .y = 0 }, - .end = .{ .x = 2, .y = 2 }, - }, false); - defer alloc.free(contents); - const expected = "1AB \n \n2EF"; - try testing.expectEqualStrings(expected, contents); - } -} - -// X -test "Screen: selectionString soft wrap" { - const testing = std.testing; - const alloc = testing.allocator; - - var s = try init(alloc, 3, 5, 0); - defer s.deinit(); - const str = "1ABCD2EFGH3IJKL"; - try s.testWriteString(str); - - { - const contents = try s.selectionString(alloc, .{ - .start = .{ .x = 0, .y = 1 }, - .end = .{ .x = 2, .y = 2 }, - }, true); - defer alloc.free(contents); - const expected = "2EFGH3IJ"; - try testing.expectEqualStrings(expected, contents); - } -} - -// X - can't happen in new terminal -test "Screen: selectionString wrap around" { - const testing = std.testing; - const alloc = testing.allocator; - - var s = try init(alloc, 3, 5, 0); - defer s.deinit(); - try s.testWriteString("1ABCD\n2EFGH\n3IJKL"); - try testing.expect(s.viewportIsBottom()); - - // Scroll down, should still be bottom, but should wrap because - // we're out of space. - try s.scroll(.{ .screen = 1 }); - try testing.expect(s.viewportIsBottom()); - try s.testWriteString("1ABCD\n2EFGH\n3IJKL"); - - { - const contents = try s.selectionString(alloc, .{ - .start = .{ .x = 0, .y = 1 }, - .end = .{ .x = 2, .y = 2 }, - }, true); - defer alloc.free(contents); - const expected = "2EFGH\n3IJ"; - try testing.expectEqualStrings(expected, contents); - } -} - -// X -test "Screen: selectionString wide char" { - const testing = std.testing; - const alloc = testing.allocator; - - var s = try init(alloc, 3, 5, 0); - defer s.deinit(); - const str = "1A⚡"; - try s.testWriteString(str); - - { - const contents = try s.selectionString(alloc, .{ - .start = .{ .x = 0, .y = 0 }, - .end = .{ .x = 3, .y = 0 }, - }, true); - defer alloc.free(contents); - const expected = str; - try testing.expectEqualStrings(expected, contents); - } - - { - const contents = try s.selectionString(alloc, .{ - .start = .{ .x = 0, .y = 0 }, - .end = .{ .x = 2, .y = 0 }, - }, true); - defer alloc.free(contents); - const expected = str; - try testing.expectEqualStrings(expected, contents); - } - - { - const contents = try s.selectionString(alloc, .{ - .start = .{ .x = 3, .y = 0 }, - .end = .{ .x = 3, .y = 0 }, - }, true); - defer alloc.free(contents); - const expected = "⚡"; - try testing.expectEqualStrings(expected, contents); - } -} - -// X -test "Screen: selectionString wide char with header" { - const testing = std.testing; - const alloc = testing.allocator; - - var s = try init(alloc, 3, 5, 0); - defer s.deinit(); - const str = "1ABC⚡"; - try s.testWriteString(str); - - { - const contents = try s.selectionString(alloc, .{ - .start = .{ .x = 0, .y = 0 }, - .end = .{ .x = 4, .y = 0 }, - }, true); - defer alloc.free(contents); - const expected = str; - try testing.expectEqualStrings(expected, contents); - } -} - -// X -// https://github.com/mitchellh/ghostty/issues/289 -test "Screen: selectionString empty with soft wrap" { - const testing = std.testing; - const alloc = testing.allocator; - - var s = try init(alloc, 2, 5, 0); - defer s.deinit(); - - // Let me describe the situation that caused this because this - // test is not obvious. By writing an emoji below, we introduce - // one cell with the emoji and one cell as a "wide char spacer". - // We then soft wrap the line by writing spaces. - // - // By selecting only the tail, we'd select nothing and we had - // a logic error that would cause a crash. - try s.testWriteString("👨"); - try s.testWriteString(" "); - - { - const contents = try s.selectionString(alloc, .{ - .start = .{ .x = 1, .y = 0 }, - .end = .{ .x = 2, .y = 0 }, - }, true); - defer alloc.free(contents); - const expected = "👨"; - try testing.expectEqualStrings(expected, contents); - } -} - -// X -test "Screen: selectionString with zero width joiner" { - const testing = std.testing; - const alloc = testing.allocator; - - var s = try init(alloc, 1, 10, 0); - defer s.deinit(); - const str = "👨‍"; // this has a ZWJ - try s.testWriteString(str); - - // Integrity check - const row = s.getRow(.{ .screen = 0 }); - { - const cell = row.getCell(0); - try testing.expectEqual(@as(u32, 0x1F468), cell.char); - try testing.expect(cell.attrs.wide); - try testing.expectEqual(@as(usize, 2), row.codepointLen(0)); - } - { - const cell = row.getCell(1); - try testing.expectEqual(@as(u32, ' '), cell.char); - try testing.expect(cell.attrs.wide_spacer_tail); - try testing.expectEqual(@as(usize, 1), row.codepointLen(1)); - } - - // The real test - { - const contents = try s.selectionString(alloc, .{ - .start = .{ .x = 0, .y = 0 }, - .end = .{ .x = 1, .y = 0 }, - }, true); - defer alloc.free(contents); - const expected = "👨‍"; - try testing.expectEqualStrings(expected, contents); - } -} - -// X -test "Screen: selectionString, rectangle, basic" { - const testing = std.testing; - const alloc = testing.allocator; - - var s = try init(alloc, 5, 30, 0); - defer s.deinit(); - const str = - \\Lorem ipsum dolor - \\sit amet, consectetur - \\adipiscing elit, sed do - \\eiusmod tempor incididunt - \\ut labore et dolore - ; - const sel = Selection{ - .start = .{ .x = 2, .y = 1 }, - .end = .{ .x = 6, .y = 3 }, - .rectangle = true, - }; - const expected = - \\t ame - \\ipisc - \\usmod - ; - try s.testWriteString(str); - - const contents = try s.selectionString(alloc, sel, true); - defer alloc.free(contents); - try testing.expectEqualStrings(expected, contents); -} - -// X -test "Screen: selectionString, rectangle, w/EOL" { - const testing = std.testing; - const alloc = testing.allocator; - - var s = try init(alloc, 5, 30, 0); - defer s.deinit(); - const str = - \\Lorem ipsum dolor - \\sit amet, consectetur - \\adipiscing elit, sed do - \\eiusmod tempor incididunt - \\ut labore et dolore - ; - const sel = Selection{ - .start = .{ .x = 12, .y = 0 }, - .end = .{ .x = 26, .y = 4 }, - .rectangle = true, - }; - const expected = - \\dolor - \\nsectetur - \\lit, sed do - \\or incididunt - \\ dolore - ; - try s.testWriteString(str); - - const contents = try s.selectionString(alloc, sel, true); - defer alloc.free(contents); - try testing.expectEqualStrings(expected, contents); -} - -// X -test "Screen: selectionString, rectangle, more complex w/breaks" { - const testing = std.testing; - const alloc = testing.allocator; - - var s = try init(alloc, 8, 30, 0); - defer s.deinit(); - const str = - \\Lorem ipsum dolor - \\sit amet, consectetur - \\adipiscing elit, sed do - \\eiusmod tempor incididunt - \\ut labore et dolore - \\ - \\magna aliqua. Ut enim - \\ad minim veniam, quis - ; - const sel = Selection{ - .start = .{ .x = 11, .y = 2 }, - .end = .{ .x = 26, .y = 7 }, - .rectangle = true, - }; - const expected = - \\elit, sed do - \\por incididunt - \\t dolore - \\ - \\a. Ut enim - \\niam, quis - ; - try s.testWriteString(str); - - const contents = try s.selectionString(alloc, sel, true); - defer alloc.free(contents); - try testing.expectEqualStrings(expected, contents); -} - -test "Screen: dirty with getCellPtr" { - const testing = std.testing; - const alloc = testing.allocator; - - var s = try init(alloc, 3, 5, 0); - defer s.deinit(); - try s.testWriteString("1ABCD\n2EFGH\n3IJKL"); - try testing.expect(s.viewportIsBottom()); - - // Ensure all are dirty. Clear em. - var iter = s.rowIterator(.viewport); - while (iter.next()) |row| { - try testing.expect(row.isDirty()); - row.setDirty(false); - } - - // Reset our cursor onto the second row. - s.cursor.x = 0; - s.cursor.y = 1; - - try s.testWriteString("foo"); - { - const row = s.getRow(.{ .active = 0 }); - try testing.expect(!row.isDirty()); - } - { - const row = s.getRow(.{ .active = 1 }); - try testing.expect(row.isDirty()); - } - { - const row = s.getRow(.{ .active = 2 }); - try testing.expect(!row.isDirty()); - - _ = row.getCell(0); - try testing.expect(!row.isDirty()); - } -} - -test "Screen: dirty with clear, fill, fillSlice, copyRow" { - const testing = std.testing; - const alloc = testing.allocator; - - var s = try init(alloc, 3, 5, 0); - defer s.deinit(); - try s.testWriteString("1ABCD\n2EFGH\n3IJKL"); - try testing.expect(s.viewportIsBottom()); - - // Ensure all are dirty. Clear em. - var iter = s.rowIterator(.viewport); - while (iter.next()) |row| { - try testing.expect(row.isDirty()); - row.setDirty(false); - } - - { - const row = s.getRow(.{ .active = 0 }); - try testing.expect(!row.isDirty()); - row.clear(.{}); - try testing.expect(row.isDirty()); - row.setDirty(false); - } - - { - const row = s.getRow(.{ .active = 0 }); - try testing.expect(!row.isDirty()); - row.fill(.{ .char = 'A' }); - try testing.expect(row.isDirty()); - row.setDirty(false); - } - - { - const row = s.getRow(.{ .active = 0 }); - try testing.expect(!row.isDirty()); - row.fillSlice(.{ .char = 'A' }, 0, 2); - try testing.expect(row.isDirty()); - row.setDirty(false); - } - - { - const src = s.getRow(.{ .active = 0 }); - const row = s.getRow(.{ .active = 1 }); - try testing.expect(!row.isDirty()); - try row.copyRow(src); - try testing.expect(!src.isDirty()); - try testing.expect(row.isDirty()); - row.setDirty(false); - } -} - -test "Screen: dirty with graphemes" { - const testing = std.testing; - const alloc = testing.allocator; - - var s = try init(alloc, 3, 5, 0); - defer s.deinit(); - try s.testWriteString("1ABCD\n2EFGH\n3IJKL"); - try testing.expect(s.viewportIsBottom()); - - // Ensure all are dirty. Clear em. - var iter = s.rowIterator(.viewport); - while (iter.next()) |row| { - try testing.expect(row.isDirty()); - row.setDirty(false); - } - - { - const row = s.getRow(.{ .active = 0 }); - try testing.expect(!row.isDirty()); - try row.attachGrapheme(0, 0xFE0F); - try testing.expect(row.isDirty()); - row.setDirty(false); - row.clearGraphemes(0); - try testing.expect(row.isDirty()); - row.setDirty(false); - } -} - -// X -test "Screen: resize (no reflow) more rows" { - const testing = std.testing; - const alloc = testing.allocator; - - var s = try init(alloc, 3, 5, 0); - defer s.deinit(); - const str = "1ABCD\n2EFGH\n3IJKL"; - try s.testWriteString(str); - - // Clear dirty rows - var iter = s.rowIterator(.viewport); - while (iter.next()) |row| row.setDirty(false); - - // Resize - try s.resizeWithoutReflow(10, 5); - { - const contents = try s.testString(alloc, .viewport); - defer alloc.free(contents); - try testing.expectEqualStrings(str, contents); - } - - // Everything should be dirty - iter = s.rowIterator(.viewport); - while (iter.next()) |row| try testing.expect(row.isDirty()); -} - -// X -test "Screen: resize (no reflow) less rows" { - const testing = std.testing; - const alloc = testing.allocator; - - var s = try init(alloc, 3, 5, 0); - defer s.deinit(); - const str = "1ABCD\n2EFGH\n3IJKL"; - try s.testWriteString(str); - try s.resizeWithoutReflow(2, 5); - - { - const contents = try s.testString(alloc, .viewport); - defer alloc.free(contents); - try testing.expectEqualStrings("2EFGH\n3IJKL", contents); - } -} - -// X -test "Screen: resize (no reflow) less rows trims blank lines" { - const testing = std.testing; - const alloc = testing.allocator; - - var s = try init(alloc, 3, 5, 0); - defer s.deinit(); - const str = "1ABCD"; - try s.testWriteString(str); - - // Write only a background color into the remaining rows - for (1..s.rows) |y| { - const row = s.getRow(.{ .active = y }); - for (0..s.cols) |x| { - const cell = row.getCellPtr(x); - cell.*.bg = .{ .rgb = .{ .r = 0xFF, .g = 0, .b = 0 } }; - } - } - - // Make sure our cursor is at the end of the first line - s.cursor.x = 4; - s.cursor.y = 0; - const cursor = s.cursor; - - try s.resizeWithoutReflow(2, 5); - - // Cursor should not move - try testing.expectEqual(cursor, s.cursor); - - { - const contents = try s.testString(alloc, .viewport); - defer alloc.free(contents); - try testing.expectEqualStrings("1ABCD", contents); - } -} - -// X -test "Screen: resize (no reflow) more rows trims blank lines" { - const testing = std.testing; - const alloc = testing.allocator; - - var s = try init(alloc, 3, 5, 0); - defer s.deinit(); - const str = "1ABCD"; - try s.testWriteString(str); - - // Write only a background color into the remaining rows - for (1..s.rows) |y| { - const row = s.getRow(.{ .active = y }); - for (0..s.cols) |x| { - const cell = row.getCellPtr(x); - cell.*.bg = .{ .rgb = .{ .r = 0xFF, .g = 0, .b = 0 } }; - } - } - - // Make sure our cursor is at the end of the first line - s.cursor.x = 4; - s.cursor.y = 0; - const cursor = s.cursor; - - try s.resizeWithoutReflow(7, 5); - - // Cursor should not move - try testing.expectEqual(cursor, s.cursor); - - { - const contents = try s.testString(alloc, .viewport); - defer alloc.free(contents); - try testing.expectEqualStrings("1ABCD", contents); - } -} - -// X -test "Screen: resize (no reflow) more cols" { - const testing = std.testing; - const alloc = testing.allocator; - - var s = try init(alloc, 3, 5, 0); - defer s.deinit(); - const str = "1ABCD\n2EFGH\n3IJKL"; - try s.testWriteString(str); - try s.resizeWithoutReflow(3, 10); - - { - const contents = try s.testString(alloc, .viewport); - defer alloc.free(contents); - try testing.expectEqualStrings(str, contents); - } -} - -// X -test "Screen: resize (no reflow) less cols" { - const testing = std.testing; - const alloc = testing.allocator; - - var s = try init(alloc, 3, 5, 0); - defer s.deinit(); - const str = "1ABCD\n2EFGH\n3IJKL"; - try s.testWriteString(str); - try s.resizeWithoutReflow(3, 4); - - { - const contents = try s.testString(alloc, .viewport); - defer alloc.free(contents); - const expected = "1ABC\n2EFG\n3IJK"; - try testing.expectEqualStrings(expected, contents); - } -} - -// X -test "Screen: resize (no reflow) more rows with scrollback cursor end" { - const testing = std.testing; - const alloc = testing.allocator; - - var s = try init(alloc, 3, 5, 2); - defer s.deinit(); - const str = "1ABCD\n2EFGH\n3IJKL\n4ABCD\n5EFGH"; - try s.testWriteString(str); - try s.resizeWithoutReflow(10, 5); - - { - const contents = try s.testString(alloc, .viewport); - defer alloc.free(contents); - try testing.expectEqualStrings(str, contents); - } -} - -// X -test "Screen: resize (no reflow) less rows with scrollback" { - const testing = std.testing; - const alloc = testing.allocator; - - var s = try init(alloc, 3, 5, 2); - defer s.deinit(); - const str = "1ABCD\n2EFGH\n3IJKL\n4ABCD\n5EFGH"; - try s.testWriteString(str); - try s.resizeWithoutReflow(2, 5); - - { - const contents = try s.testString(alloc, .screen); - defer alloc.free(contents); - const expected = "2EFGH\n3IJKL\n4ABCD\n5EFGH"; - try testing.expectEqualStrings(expected, contents); - } -} - -// X -// https://github.com/mitchellh/ghostty/issues/1030 -test "Screen: resize (no reflow) less rows with empty trailing" { - const testing = std.testing; - const alloc = testing.allocator; - - var s = try init(alloc, 3, 5, 5); - defer s.deinit(); - const str = "1\n2\n3\n4\n5\n6\n7\n8"; - try s.testWriteString(str); - try s.scroll(.{ .clear = {} }); - s.cursor.x = 0; - s.cursor.y = 0; - try s.testWriteString("A\nB"); - - const cursor = s.cursor; - try s.resizeWithoutReflow(2, 5); - try testing.expectEqual(cursor, s.cursor); - - { - const contents = try s.testString(alloc, .viewport); - defer alloc.free(contents); - try testing.expectEqualStrings("A\nB", contents); - } -} - -// X -test "Screen: resize (no reflow) empty screen" { - const testing = std.testing; - const alloc = testing.allocator; - - var s = try init(alloc, 5, 5, 0); - defer s.deinit(); - try testing.expect(s.rowsWritten() == 0); - try testing.expectEqual(@as(usize, 5), s.rowsCapacity()); - - try s.resizeWithoutReflow(10, 10); - try testing.expect(s.rowsWritten() == 0); - - // This is the primary test for this test, we want to ensure we - // always have at least enough capacity for our rows. - try testing.expectEqual(@as(usize, 10), s.rowsCapacity()); -} - -// X -test "Screen: resize (no reflow) grapheme copy" { - const testing = std.testing; - const alloc = testing.allocator; - - var s = try init(alloc, 3, 5, 0); - defer s.deinit(); - const str = "1ABCD\n2EFGH\n3IJKL"; - try s.testWriteString(str); - - // Attach graphemes to all the columns - { - var iter = s.rowIterator(.viewport); - while (iter.next()) |row| { - var col: usize = 0; - while (col < s.cols) : (col += 1) { - try row.attachGrapheme(col, 0xFE0F); - } - } - } - - // Clear dirty rows - { - var iter = s.rowIterator(.viewport); - while (iter.next()) |row| row.setDirty(false); - } - - // Resize - try s.resizeWithoutReflow(10, 5); - { - const expected = "1️A️B️C️D️\n2️E️F️G️H️\n3️I️J️K️L️"; - const contents = try s.testString(alloc, .viewport); - defer alloc.free(contents); - try testing.expectEqualStrings(expected, contents); - } - - // Everything should be dirty - { - var iter = s.rowIterator(.viewport); - while (iter.next()) |row| try testing.expect(row.isDirty()); - } -} - -// X -test "Screen: resize (no reflow) more rows with soft wrapping" { - const testing = std.testing; - const alloc = testing.allocator; - - var s = try init(alloc, 3, 2, 3); - defer s.deinit(); - const str = "1A2B\n3C4E\n5F6G"; - try s.testWriteString(str); - - // Every second row should be wrapped - { - var y: usize = 0; - while (y < 6) : (y += 1) { - const row = s.getRow(.{ .screen = y }); - const wrapped = (y % 2 == 0); - try testing.expectEqual(wrapped, row.header().flags.wrap); - } - } - - // Resize - try s.resizeWithoutReflow(10, 2); - { - const contents = try s.testString(alloc, .viewport); - defer alloc.free(contents); - const expected = "1A\n2B\n3C\n4E\n5F\n6G"; - try testing.expectEqualStrings(expected, contents); - } - - // Every second row should be wrapped - { - var y: usize = 0; - while (y < 6) : (y += 1) { - const row = s.getRow(.{ .screen = y }); - const wrapped = (y % 2 == 0); - try testing.expectEqual(wrapped, row.header().flags.wrap); - } - } -} - -// X -test "Screen: resize more rows no scrollback" { - const testing = std.testing; - const alloc = testing.allocator; - - var s = try init(alloc, 3, 5, 0); - defer s.deinit(); - const str = "1ABCD\n2EFGH\n3IJKL"; - try s.testWriteString(str); - const cursor = s.cursor; - try s.resize(10, 5); - - // Cursor should not move - try testing.expectEqual(cursor, s.cursor); - - { - const contents = try s.testString(alloc, .viewport); - defer alloc.free(contents); - try testing.expectEqualStrings(str, contents); - } - { - const contents = try s.testString(alloc, .screen); - defer alloc.free(contents); - try testing.expectEqualStrings(str, contents); - } -} - -// X -test "Screen: resize more rows with empty scrollback" { - const testing = std.testing; - const alloc = testing.allocator; - - var s = try init(alloc, 3, 5, 10); - defer s.deinit(); - const str = "1ABCD\n2EFGH\n3IJKL"; - try s.testWriteString(str); - const cursor = s.cursor; - try s.resize(10, 5); - - // Cursor should not move - try testing.expectEqual(cursor, s.cursor); - - { - const contents = try s.testString(alloc, .viewport); - defer alloc.free(contents); - try testing.expectEqualStrings(str, contents); - } - { - const contents = try s.testString(alloc, .screen); - defer alloc.free(contents); - try testing.expectEqualStrings(str, contents); - } -} - -// X -test "Screen: resize more rows with populated scrollback" { - const testing = std.testing; - const alloc = testing.allocator; - - var s = try init(alloc, 3, 5, 5); - defer s.deinit(); - const str = "1ABCD\n2EFGH\n3IJKL\n4ABCD\n5EFGH"; - try s.testWriteString(str); - { - const contents = try s.testString(alloc, .viewport); - defer alloc.free(contents); - const expected = "3IJKL\n4ABCD\n5EFGH"; - try testing.expectEqualStrings(expected, contents); - } - - // Set our cursor to be on the "4" - s.cursor.x = 0; - s.cursor.y = 1; - try testing.expectEqual(@as(u32, '4'), s.getCell(.active, s.cursor.y, s.cursor.x).char); - - // Resize - try s.resize(10, 5); - - // Cursor should still be on the "4" - try testing.expectEqual(@as(u32, '4'), s.getCell(.active, s.cursor.y, s.cursor.x).char); - - { - const contents = try s.testString(alloc, .viewport); - defer alloc.free(contents); - const expected = "3IJKL\n4ABCD\n5EFGH"; - try testing.expectEqualStrings(expected, contents); - } -} - -// X -test "Screen: resize more rows and cols with wrapping" { - const testing = std.testing; - const alloc = testing.allocator; - - var s = try init(alloc, 4, 2, 0); - defer s.deinit(); - const str = "1A2B\n3C4D"; - try s.testWriteString(str); - { - const contents = try s.testString(alloc, .viewport); - defer alloc.free(contents); - const expected = "1A\n2B\n3C\n4D"; - try testing.expectEqualStrings(expected, contents); - } - - try s.resize(10, 5); - - // Cursor should move due to wrapping - try testing.expectEqual(@as(usize, 3), s.cursor.x); - try testing.expectEqual(@as(usize, 1), s.cursor.y); - - { - const contents = try s.testString(alloc, .viewport); - defer alloc.free(contents); - try testing.expectEqualStrings(str, contents); - } - { - const contents = try s.testString(alloc, .screen); - defer alloc.free(contents); - try testing.expectEqualStrings(str, contents); - } -} - -// X -test "Screen: resize more cols no reflow" { - const testing = std.testing; - const alloc = testing.allocator; - - var s = try init(alloc, 3, 5, 0); - defer s.deinit(); - const str = "1ABCD\n2EFGH\n3IJKL"; - try s.testWriteString(str); - const cursor = s.cursor; - try s.resize(3, 10); - - // Cursor should not move - try testing.expectEqual(cursor, s.cursor); - - { - const contents = try s.testString(alloc, .viewport); - defer alloc.free(contents); - try testing.expectEqualStrings(str, contents); - } - { - const contents = try s.testString(alloc, .screen); - defer alloc.free(contents); - try testing.expectEqualStrings(str, contents); - } -} - -// X -// https://github.com/mitchellh/ghostty/issues/272#issuecomment-1676038963 -test "Screen: resize more cols perfect split" { - const testing = std.testing; - const alloc = testing.allocator; - - var s = try init(alloc, 3, 5, 0); - defer s.deinit(); - const str = "1ABCD2EFGH3IJKL"; - try s.testWriteString(str); - try s.resize(3, 10); - - { - const contents = try s.testString(alloc, .screen); - defer alloc.free(contents); - try testing.expectEqualStrings("1ABCD2EFGH\n3IJKL", contents); - } -} - -// X -// https://github.com/mitchellh/ghostty/issues/1159 -test "Screen: resize (no reflow) more cols with scrollback scrolled up" { - const testing = std.testing; - const alloc = testing.allocator; - - var s = try init(alloc, 3, 5, 5); - defer s.deinit(); - const str = "1\n2\n3\n4\n5\n6\n7\n8"; - try s.testWriteString(str); - - // Cursor at bottom - try testing.expectEqual(@as(usize, 1), s.cursor.x); - try testing.expectEqual(@as(usize, 2), s.cursor.y); - - try s.scroll(.{ .viewport = -4 }); - { - const contents = try s.testString(alloc, .viewport); - defer alloc.free(contents); - try testing.expectEqualStrings("2\n3\n4", contents); - } - - try s.resize(3, 8); - { - const contents = try s.testString(alloc, .screen); - defer alloc.free(contents); - try testing.expectEqualStrings(str, contents); - } - - // Cursor remains at bottom - try testing.expectEqual(@as(usize, 1), s.cursor.x); - try testing.expectEqual(@as(usize, 2), s.cursor.y); -} - -// X -// https://github.com/mitchellh/ghostty/issues/1159 -test "Screen: resize (no reflow) less cols with scrollback scrolled up" { - const testing = std.testing; - const alloc = testing.allocator; - - var s = try init(alloc, 3, 5, 5); - defer s.deinit(); - const str = "1\n2\n3\n4\n5\n6\n7\n8"; - try s.testWriteString(str); - - // Cursor at bottom - try testing.expectEqual(@as(usize, 1), s.cursor.x); - try testing.expectEqual(@as(usize, 2), s.cursor.y); - - try s.scroll(.{ .viewport = -4 }); - { - const contents = try s.testString(alloc, .viewport); - defer alloc.free(contents); - try testing.expectEqualStrings("2\n3\n4", contents); - } - - try s.resize(3, 4); - { - const contents = try s.testString(alloc, .screen); - defer alloc.free(contents); - try testing.expectEqualStrings(str, contents); - } - { - const contents = try s.testString(alloc, .active); - defer alloc.free(contents); - try testing.expectEqualStrings("6\n7\n8", contents); - } - - // Cursor remains at bottom - try testing.expectEqual(@as(usize, 1), s.cursor.x); - try testing.expectEqual(@as(usize, 2), s.cursor.y); -} - -// X -test "Screen: resize more cols no reflow preserves semantic prompt" { - const testing = std.testing; - const alloc = testing.allocator; - - var s = try init(alloc, 3, 5, 0); - defer s.deinit(); - const str = "1ABCD\n2EFGH\n3IJKL"; - try s.testWriteString(str); - - // Set one of the rows to be a prompt - { - const row = s.getRow(.{ .active = 1 }); - row.setSemanticPrompt(.prompt); - } - - const cursor = s.cursor; - try s.resize(3, 10); - - // Cursor should not move - try testing.expectEqual(cursor, s.cursor); - - { - const contents = try s.testString(alloc, .viewport); - defer alloc.free(contents); - try testing.expectEqualStrings(str, contents); - } - { - const contents = try s.testString(alloc, .screen); - defer alloc.free(contents); - try testing.expectEqualStrings(str, contents); - } - - // Our one row should still be a semantic prompt, the others should not. - { - const row = s.getRow(.{ .active = 0 }); - try testing.expect(row.getSemanticPrompt() == .unknown); - } - { - const row = s.getRow(.{ .active = 1 }); - try testing.expect(row.getSemanticPrompt() == .prompt); - } - { - const row = s.getRow(.{ .active = 2 }); - try testing.expect(row.getSemanticPrompt() == .unknown); - } -} - -// X -test "Screen: resize more cols grapheme map" { - const testing = std.testing; - const alloc = testing.allocator; - - var s = try init(alloc, 3, 5, 0); - defer s.deinit(); - const str = "1ABCD\n2EFGH\n3IJKL"; - try s.testWriteString(str); - - // Attach graphemes to all the columns - { - var iter = s.rowIterator(.viewport); - while (iter.next()) |row| { - var col: usize = 0; - while (col < s.cols) : (col += 1) { - try row.attachGrapheme(col, 0xFE0F); - } - } - } - - const cursor = s.cursor; - try s.resize(3, 10); - - // Cursor should not move - try testing.expectEqual(cursor, s.cursor); - - { - const expected = "1️A️B️C️D️\n2️E️F️G️H️\n3️I️J️K️L️"; - const contents = try s.testString(alloc, .viewport); - defer alloc.free(contents); - try testing.expectEqualStrings(expected, contents); - } - { - const expected = "1️A️B️C️D️\n2️E️F️G️H️\n3️I️J️K️L️"; - const contents = try s.testString(alloc, .screen); - defer alloc.free(contents); - try testing.expectEqualStrings(expected, contents); - } -} - -// X -test "Screen: resize more cols with reflow that fits full width" { - const testing = std.testing; - const alloc = testing.allocator; - - var s = try init(alloc, 3, 5, 0); - defer s.deinit(); - const str = "1ABCD2EFGH\n3IJKL"; - try s.testWriteString(str); - - // Verify we soft wrapped - { - const contents = try s.testString(alloc, .viewport); - defer alloc.free(contents); - const expected = "1ABCD\n2EFGH\n3IJKL"; - try testing.expectEqualStrings(expected, contents); - } - - // Let's put our cursor on row 2, where the soft wrap is - s.cursor.x = 0; - s.cursor.y = 1; - try testing.expectEqual(@as(u32, '2'), s.getCell(.active, s.cursor.y, s.cursor.x).char); - - // Resize and verify we undid the soft wrap because we have space now - try s.resize(3, 10); - { - const contents = try s.testString(alloc, .viewport); - defer alloc.free(contents); - try testing.expectEqualStrings(str, contents); - } - - // Our cursor should've moved - try testing.expectEqual(@as(usize, 5), s.cursor.x); - try testing.expectEqual(@as(usize, 0), s.cursor.y); -} - -// X -test "Screen: resize more cols with reflow that ends in newline" { - const testing = std.testing; - const alloc = testing.allocator; - - var s = try init(alloc, 3, 6, 0); - defer s.deinit(); - const str = "1ABCD2EFGH\n3IJKL"; - try s.testWriteString(str); - - // Verify we soft wrapped - { - const contents = try s.testString(alloc, .viewport); - defer alloc.free(contents); - const expected = "1ABCD2\nEFGH\n3IJKL"; - try testing.expectEqualStrings(expected, contents); - } - - // Let's put our cursor on the last row - s.cursor.x = 0; - s.cursor.y = 2; - try testing.expectEqual(@as(u32, '3'), s.getCell(.active, s.cursor.y, s.cursor.x).char); - - // Resize and verify we undid the soft wrap because we have space now - try s.resize(3, 10); - { - const contents = try s.testString(alloc, .viewport); - defer alloc.free(contents); - try testing.expectEqualStrings(str, contents); - } - - // Our cursor should still be on the 3 - try testing.expectEqual(@as(u32, '3'), s.getCell(.active, s.cursor.y, s.cursor.x).char); -} - -// X -test "Screen: resize more cols with reflow that forces more wrapping" { - const testing = std.testing; - const alloc = testing.allocator; - - var s = try init(alloc, 3, 5, 0); - defer s.deinit(); - const str = "1ABCD2EFGH\n3IJKL"; - try s.testWriteString(str); - - // Let's put our cursor on row 2, where the soft wrap is - s.cursor.x = 0; - s.cursor.y = 1; - try testing.expectEqual(@as(u32, '2'), s.getCell(.active, s.cursor.y, s.cursor.x).char); - - // Verify we soft wrapped - { - const contents = try s.testString(alloc, .viewport); - defer alloc.free(contents); - const expected = "1ABCD\n2EFGH\n3IJKL"; - try testing.expectEqualStrings(expected, contents); - } - - // Resize and verify we undid the soft wrap because we have space now - try s.resize(3, 7); - { - const contents = try s.testString(alloc, .viewport); - defer alloc.free(contents); - const expected = "1ABCD2E\nFGH\n3IJKL"; - try testing.expectEqualStrings(expected, contents); - } - - // Our cursor should've moved - try testing.expectEqual(@as(usize, 5), s.cursor.x); - try testing.expectEqual(@as(usize, 0), s.cursor.y); -} - -// X -test "Screen: resize more cols with reflow that unwraps multiple times" { - const testing = std.testing; - const alloc = testing.allocator; - - var s = try init(alloc, 3, 5, 0); - defer s.deinit(); - const str = "1ABCD2EFGH3IJKL"; - try s.testWriteString(str); - - // Let's put our cursor on row 2, where the soft wrap is - s.cursor.x = 0; - s.cursor.y = 2; - try testing.expectEqual(@as(u32, '3'), s.getCell(.active, s.cursor.y, s.cursor.x).char); - - // Verify we soft wrapped - { - const contents = try s.testString(alloc, .viewport); - defer alloc.free(contents); - const expected = "1ABCD\n2EFGH\n3IJKL"; - try testing.expectEqualStrings(expected, contents); - } - - // Resize and verify we undid the soft wrap because we have space now - try s.resize(3, 15); - { - const contents = try s.testString(alloc, .viewport); - defer alloc.free(contents); - const expected = "1ABCD2EFGH3IJKL"; - try testing.expectEqualStrings(expected, contents); - } - - // Our cursor should've moved - try testing.expectEqual(@as(usize, 10), s.cursor.x); - try testing.expectEqual(@as(usize, 0), s.cursor.y); -} - -// X -test "Screen: resize more cols with populated scrollback" { - const testing = std.testing; - const alloc = testing.allocator; - - var s = try init(alloc, 3, 5, 5); - defer s.deinit(); - const str = "1ABCD\n2EFGH\n3IJKL\n4ABCD5EFGH"; - try s.testWriteString(str); - { - const contents = try s.testString(alloc, .viewport); - defer alloc.free(contents); - const expected = "3IJKL\n4ABCD\n5EFGH"; - try testing.expectEqualStrings(expected, contents); - } - - // // Set our cursor to be on the "5" - s.cursor.x = 0; - s.cursor.y = 2; - try testing.expectEqual(@as(u32, '5'), s.getCell(.active, s.cursor.y, s.cursor.x).char); - - // Resize - try s.resize(3, 10); - - // Cursor should still be on the "5" - try testing.expectEqual(@as(u32, '5'), s.getCell(.active, s.cursor.y, s.cursor.x).char); - - { - const contents = try s.testString(alloc, .viewport); - defer alloc.free(contents); - const expected = "2EFGH\n3IJKL\n4ABCD5EFGH"; - try testing.expectEqualStrings(expected, contents); - } -} - -// X -test "Screen: resize more cols with reflow" { - const testing = std.testing; - const alloc = testing.allocator; - - var s = try init(alloc, 3, 2, 5); - defer s.deinit(); - const str = "1ABC\n2DEF\n3ABC\n4DEF"; - try s.testWriteString(str); - - // Let's put our cursor on row 2, where the soft wrap is - s.cursor.x = 0; - s.cursor.y = 2; - try testing.expectEqual(@as(u32, 'E'), s.getCell(.active, s.cursor.y, s.cursor.x).char); - - // Verify we soft wrapped - { - const contents = try s.testString(alloc, .viewport); - defer alloc.free(contents); - const expected = "BC\n4D\nEF"; - try testing.expectEqualStrings(expected, contents); - } - - // Resize and verify we undid the soft wrap because we have space now - try s.resize(3, 7); - - { - const contents = try s.testString(alloc, .screen); - defer alloc.free(contents); - const expected = "1ABC\n2DEF\n3ABC\n4DEF"; - try testing.expectEqualStrings(expected, contents); - } - - // Our cursor should've moved - try testing.expectEqual(@as(usize, 2), s.cursor.x); - try testing.expectEqual(@as(usize, 2), s.cursor.y); -} - -// X -test "Screen: resize less rows no scrollback" { - const testing = std.testing; - const alloc = testing.allocator; - - var s = try init(alloc, 3, 5, 0); - defer s.deinit(); - const str = "1ABCD\n2EFGH\n3IJKL"; - try s.testWriteString(str); - s.cursor.x = 0; - s.cursor.y = 0; - const cursor = s.cursor; - try s.resize(1, 5); - - // Cursor should not move - try testing.expectEqual(cursor, s.cursor); - - { - const contents = try s.testString(alloc, .viewport); - defer alloc.free(contents); - const expected = "3IJKL"; - try testing.expectEqualStrings(expected, contents); - } - { - const contents = try s.testString(alloc, .screen); - defer alloc.free(contents); - const expected = "3IJKL"; - try testing.expectEqualStrings(expected, contents); - } -} - -// X -test "Screen: resize less rows moving cursor" { - const testing = std.testing; - const alloc = testing.allocator; - - var s = try init(alloc, 3, 5, 0); - defer s.deinit(); - const str = "1ABCD\n2EFGH\n3IJKL"; - try s.testWriteString(str); - - // Put our cursor on the last line - s.cursor.x = 1; - s.cursor.y = 2; - try testing.expectEqual(@as(u32, 'I'), s.getCell(.active, s.cursor.y, s.cursor.x).char); - - // Resize - try s.resize(1, 5); - - // Cursor should be on the last line - try testing.expectEqual(@as(usize, 1), s.cursor.x); - try testing.expectEqual(@as(usize, 0), s.cursor.y); - - { - const contents = try s.testString(alloc, .viewport); - defer alloc.free(contents); - const expected = "3IJKL"; - try testing.expectEqualStrings(expected, contents); - } - { - const contents = try s.testString(alloc, .screen); - defer alloc.free(contents); - const expected = "3IJKL"; - try testing.expectEqualStrings(expected, contents); - } -} - -// X -test "Screen: resize less rows with empty scrollback" { - const testing = std.testing; - const alloc = testing.allocator; - - var s = try init(alloc, 3, 5, 10); - defer s.deinit(); - const str = "1ABCD\n2EFGH\n3IJKL"; - try s.testWriteString(str); - try s.resize(1, 5); - - { - const contents = try s.testString(alloc, .screen); - defer alloc.free(contents); - try testing.expectEqualStrings(str, contents); - } - { - const contents = try s.testString(alloc, .viewport); - defer alloc.free(contents); - const expected = "3IJKL"; - try testing.expectEqualStrings(expected, contents); - } -} - -// X -test "Screen: resize less rows with populated scrollback" { - const testing = std.testing; - const alloc = testing.allocator; - - var s = try init(alloc, 3, 5, 5); - defer s.deinit(); - const str = "1ABCD\n2EFGH\n3IJKL\n4ABCD\n5EFGH"; - try s.testWriteString(str); - { - const contents = try s.testString(alloc, .viewport); - defer alloc.free(contents); - const expected = "3IJKL\n4ABCD\n5EFGH"; - try testing.expectEqualStrings(expected, contents); - } - - // Resize - try s.resize(1, 5); - - { - const contents = try s.testString(alloc, .screen); - defer alloc.free(contents); - try testing.expectEqualStrings(str, contents); - } - { - const contents = try s.testString(alloc, .viewport); - defer alloc.free(contents); - const expected = "5EFGH"; - try testing.expectEqualStrings(expected, contents); - } -} - -// X -test "Screen: resize less rows with full scrollback" { - const testing = std.testing; - const alloc = testing.allocator; - - var s = try init(alloc, 3, 5, 3); - defer s.deinit(); - const str = "00000\n1ABCD\n2EFGH\n3IJKL\n4ABCD\n5EFGH"; - try s.testWriteString(str); - { - const contents = try s.testString(alloc, .viewport); - defer alloc.free(contents); - const expected = "3IJKL\n4ABCD\n5EFGH"; - try testing.expectEqualStrings(expected, contents); - } - - const cursor = s.cursor; - try testing.expectEqual(Cursor{ .x = 4, .y = 2 }, cursor); - - // Resize - try s.resize(2, 5); - - // Cursor should stay in the same relative place (bottom of the - // screen, same character). - try testing.expectEqual(Cursor{ .x = 4, .y = 1 }, s.cursor); - - { - const contents = try s.testString(alloc, .screen); - defer alloc.free(contents); - const expected = "1ABCD\n2EFGH\n3IJKL\n4ABCD\n5EFGH"; - try testing.expectEqualStrings(expected, contents); - } - { - const contents = try s.testString(alloc, .viewport); - defer alloc.free(contents); - const expected = "4ABCD\n5EFGH"; - try testing.expectEqualStrings(expected, contents); - } -} - -// X -test "Screen: resize less cols no reflow" { - const testing = std.testing; - const alloc = testing.allocator; - - var s = try init(alloc, 3, 5, 0); - defer s.deinit(); - const str = "1AB\n2EF\n3IJ"; - try s.testWriteString(str); - s.cursor.x = 0; - s.cursor.y = 0; - const cursor = s.cursor; - try s.resize(3, 3); - - // Cursor should not move - try testing.expectEqual(cursor, s.cursor); - - { - const contents = try s.testString(alloc, .viewport); - defer alloc.free(contents); - try testing.expectEqualStrings(str, contents); - } - { - const contents = try s.testString(alloc, .screen); - defer alloc.free(contents); - try testing.expectEqualStrings(str, contents); - } -} - -test "Screen: resize less cols trailing background colors" { - const testing = std.testing; - const alloc = testing.allocator; - - var s = try init(alloc, 3, 10, 0); - defer s.deinit(); - const str = "1AB"; - try s.testWriteString(str); - const cursor = s.cursor; - - // Color our cells red - const pen: Cell = .{ .bg = .{ .rgb = .{ .r = 0xFF } } }; - for (s.cursor.x..s.cols) |x| { - const row = s.getRow(.{ .active = s.cursor.y }); - const cell = row.getCellPtr(x); - cell.* = pen; - } - for ((s.cursor.y + 1)..s.rows) |y| { - const row = s.getRow(.{ .active = y }); - row.fill(pen); - } - - try s.resize(3, 5); - - // Cursor should not move - try testing.expectEqual(cursor, s.cursor); - - { - const contents = try s.testString(alloc, .viewport); - defer alloc.free(contents); - try testing.expectEqualStrings(str, contents); - } - { - const contents = try s.testString(alloc, .screen); - defer alloc.free(contents); - try testing.expectEqualStrings(str, contents); - } - - // Verify all our trailing cells have the color - for (s.cursor.x..s.cols) |x| { - const row = s.getRow(.{ .active = s.cursor.y }); - const cell = row.getCellPtr(x); - try testing.expectEqual(pen, cell.*); - } -} - -// X -test "Screen: resize less cols with graphemes" { - const testing = std.testing; - const alloc = testing.allocator; - - var s = try init(alloc, 3, 5, 0); - defer s.deinit(); - const str = "1AB\n2EF\n3IJ"; - try s.testWriteString(str); - - // Attach graphemes to all the columns - { - var iter = s.rowIterator(.viewport); - while (iter.next()) |row| { - var col: usize = 0; - while (col < 3) : (col += 1) { - try row.attachGrapheme(col, 0xFE0F); - } - } - } - - s.cursor.x = 0; - s.cursor.y = 0; - const cursor = s.cursor; - try s.resize(3, 3); - - // Cursor should not move - try testing.expectEqual(cursor, s.cursor); - - { - const expected = "1️A️B️\n2️E️F️\n3️I️J️"; - const contents = try s.testString(alloc, .viewport); - defer alloc.free(contents); - try testing.expectEqualStrings(expected, contents); - } - { - const expected = "1️A️B️\n2️E️F️\n3️I️J️"; - const contents = try s.testString(alloc, .screen); - defer alloc.free(contents); - try testing.expectEqualStrings(expected, contents); - } -} - -// X -test "Screen: resize less cols no reflow preserves semantic prompt" { - const testing = std.testing; - const alloc = testing.allocator; - - var s = try init(alloc, 3, 5, 0); - defer s.deinit(); - const str = "1AB\n2EF\n3IJ"; - try s.testWriteString(str); - - // Set one of the rows to be a prompt - { - const row = s.getRow(.{ .active = 1 }); - row.setSemanticPrompt(.prompt); - } - - s.cursor.x = 0; - s.cursor.y = 0; - const cursor = s.cursor; - try s.resize(3, 3); - - // Cursor should not move - try testing.expectEqual(cursor, s.cursor); - - { - const contents = try s.testString(alloc, .viewport); - defer alloc.free(contents); - try testing.expectEqualStrings(str, contents); - } - { - const contents = try s.testString(alloc, .screen); - defer alloc.free(contents); - try testing.expectEqualStrings(str, contents); - } - - // Our one row should still be a semantic prompt, the others should not. - { - const row = s.getRow(.{ .active = 0 }); - try testing.expect(row.getSemanticPrompt() == .unknown); - } - { - const row = s.getRow(.{ .active = 1 }); - try testing.expect(row.getSemanticPrompt() == .prompt); - } - { - const row = s.getRow(.{ .active = 2 }); - try testing.expect(row.getSemanticPrompt() == .unknown); - } -} - -// X -test "Screen: resize less cols with reflow but row space" { - const testing = std.testing; - const alloc = testing.allocator; - - var s = try init(alloc, 3, 5, 0); - defer s.deinit(); - const str = "1ABCD"; - try s.testWriteString(str); - - // Put our cursor on the end - s.cursor.x = 4; - s.cursor.y = 0; - try testing.expectEqual(@as(u32, 'D'), s.getCell(.active, s.cursor.y, s.cursor.x).char); - - try s.resize(3, 3); - { - const contents = try s.testString(alloc, .viewport); - defer alloc.free(contents); - const expected = "1AB\nCD"; - try testing.expectEqualStrings(expected, contents); - } - { - const contents = try s.testString(alloc, .screen); - defer alloc.free(contents); - const expected = "1AB\nCD"; - try testing.expectEqualStrings(expected, contents); - } - - // Cursor should be on the last line - try testing.expectEqual(@as(usize, 1), s.cursor.x); - try testing.expectEqual(@as(usize, 1), s.cursor.y); -} - -// X -test "Screen: resize less cols with reflow with trimmed rows" { - const testing = std.testing; - const alloc = testing.allocator; - - var s = try init(alloc, 3, 5, 0); - defer s.deinit(); - const str = "3IJKL\n4ABCD\n5EFGH"; - try s.testWriteString(str); - try s.resize(3, 3); - - { - const contents = try s.testString(alloc, .viewport); - defer alloc.free(contents); - const expected = "CD\n5EF\nGH"; - try testing.expectEqualStrings(expected, contents); - } - { - const contents = try s.testString(alloc, .screen); - defer alloc.free(contents); - const expected = "CD\n5EF\nGH"; - try testing.expectEqualStrings(expected, contents); - } -} - -// X -test "Screen: resize less cols with reflow with trimmed rows and scrollback" { - const testing = std.testing; - const alloc = testing.allocator; - - var s = try init(alloc, 3, 5, 1); - defer s.deinit(); - const str = "3IJKL\n4ABCD\n5EFGH"; - try s.testWriteString(str); - try s.resize(3, 3); - - { - const contents = try s.testString(alloc, .viewport); - defer alloc.free(contents); - const expected = "CD\n5EF\nGH"; - try testing.expectEqualStrings(expected, contents); - } - { - const contents = try s.testString(alloc, .screen); - defer alloc.free(contents); - const expected = "4AB\nCD\n5EF\nGH"; - try testing.expectEqualStrings(expected, contents); - } -} - -// X -test "Screen: resize less cols with reflow previously wrapped" { - const testing = std.testing; - const alloc = testing.allocator; - - var s = try init(alloc, 3, 5, 0); - defer s.deinit(); - const str = "3IJKL4ABCD5EFGH"; - try s.testWriteString(str); - - // Check - { - const contents = try s.testString(alloc, .screen); - defer alloc.free(contents); - const expected = "3IJKL\n4ABCD\n5EFGH"; - try testing.expectEqualStrings(expected, contents); - } - - try s.resize(3, 3); - - // { - // const contents = try s.testString(alloc, .viewport); - // defer alloc.free(contents); - // const expected = "CD\n5EF\nGH"; - // try testing.expectEqualStrings(expected, contents); - // } - { - const contents = try s.testString(alloc, .screen); - defer alloc.free(contents); - const expected = "ABC\nD5E\nFGH"; - try testing.expectEqualStrings(expected, contents); - } -} - -// X -test "Screen: resize less cols with reflow and scrollback" { - const testing = std.testing; - const alloc = testing.allocator; - - var s = try init(alloc, 3, 5, 5); - defer s.deinit(); - const str = "1A\n2B\n3C\n4D\n5E"; - try s.testWriteString(str); - - // Put our cursor on the end - s.cursor.x = 1; - s.cursor.y = s.rows - 1; - try testing.expectEqual(@as(u32, 'E'), s.getCell(.active, s.cursor.y, s.cursor.x).char); - - try s.resize(3, 3); - - { - const contents = try s.testString(alloc, .viewport); - defer alloc.free(contents); - const expected = "3C\n4D\n5E"; - try testing.expectEqualStrings(expected, contents); - } - - // Cursor should be on the last line - try testing.expectEqual(@as(usize, 1), s.cursor.x); - try testing.expectEqual(@as(usize, 2), s.cursor.y); -} - -// X -test "Screen: resize less cols with reflow previously wrapped and scrollback" { - const testing = std.testing; - const alloc = testing.allocator; - - var s = try init(alloc, 3, 5, 2); - defer s.deinit(); - const str = "1ABCD2EFGH3IJKL4ABCD5EFGH"; - try s.testWriteString(str); - - // Check - { - const contents = try s.testString(alloc, .viewport); - defer alloc.free(contents); - const expected = "3IJKL\n4ABCD\n5EFGH"; - try testing.expectEqualStrings(expected, contents); - } - - // Put our cursor on the end - s.cursor.x = s.cols - 1; - s.cursor.y = s.rows - 1; - try testing.expectEqual(@as(u32, 'H'), s.getCell(.active, s.cursor.y, s.cursor.x).char); - - try s.resize(3, 3); - - { - const contents = try s.testString(alloc, .viewport); - defer alloc.free(contents); - const expected = "CD5\nEFG\nH"; - try testing.expectEqualStrings(expected, contents); - } - { - const contents = try s.testString(alloc, .screen); - defer alloc.free(contents); - const expected = "JKL\n4AB\nCD5\nEFG\nH"; - try testing.expectEqualStrings(expected, contents); - } - - // Cursor should be on the last line - try testing.expectEqual(@as(u32, 'H'), s.getCell(.active, s.cursor.y, s.cursor.x).char); - try testing.expectEqual(@as(usize, 0), s.cursor.x); - try testing.expectEqual(@as(usize, 2), s.cursor.y); -} - -// X -test "Screen: resize less cols with scrollback keeps cursor row" { - const testing = std.testing; - const alloc = testing.allocator; - - var s = try init(alloc, 3, 5, 5); - defer s.deinit(); - const str = "1A\n2B\n3C\n4D\n5E"; - try s.testWriteString(str); - - // Lets do a scroll and clear operation - try s.scroll(.{ .clear = {} }); - - // Move our cursor to the beginning - s.cursor.x = 0; - s.cursor.y = 0; - - try s.resize(3, 3); - - { - const contents = try s.testString(alloc, .viewport); - defer alloc.free(contents); - const expected = ""; - try testing.expectEqualStrings(expected, contents); - } - - // Cursor should be on the last line - try testing.expectEqual(@as(usize, 0), s.cursor.x); - try testing.expectEqual(@as(usize, 0), s.cursor.y); -} - -// X -test "Screen: resize more rows, less cols with reflow with scrollback" { - const testing = std.testing; - const alloc = testing.allocator; - - var s = try init(alloc, 3, 5, 3); - defer s.deinit(); - const str = "1ABCD\n2EFGH3IJKL\n4MNOP"; - try s.testWriteString(str); - - { - const contents = try s.testString(alloc, .screen); - defer alloc.free(contents); - const expected = "1ABCD\n2EFGH\n3IJKL\n4MNOP"; - try testing.expectEqualStrings(expected, contents); - } - { - const contents = try s.testString(alloc, .viewport); - defer alloc.free(contents); - const expected = "2EFGH\n3IJKL\n4MNOP"; - try testing.expectEqualStrings(expected, contents); - } - - try s.resize(10, 2); - - { - const contents = try s.testString(alloc, .viewport); - defer alloc.free(contents); - const expected = "BC\nD\n2E\nFG\nH3\nIJ\nKL\n4M\nNO\nP"; - try testing.expectEqualStrings(expected, contents); - } - { - const contents = try s.testString(alloc, .screen); - defer alloc.free(contents); - const expected = "1A\nBC\nD\n2E\nFG\nH3\nIJ\nKL\n4M\nNO\nP"; - try testing.expectEqualStrings(expected, contents); - } -} - -// X -// This seems like it should work fine but for some reason in practice -// in the initial implementation I found this bug! This is a regression -// test for that. -test "Screen: resize more rows then shrink again" { - const testing = std.testing; - const alloc = testing.allocator; - - var s = try init(alloc, 3, 5, 10); - defer s.deinit(); - const str = "1ABC"; - try s.testWriteString(str); - - // Grow - try s.resize(10, 5); - { - const contents = try s.testString(alloc, .viewport); - defer alloc.free(contents); - try testing.expectEqualStrings(str, contents); - } - { - const contents = try s.testString(alloc, .screen); - defer alloc.free(contents); - try testing.expectEqualStrings(str, contents); - } - - // Shrink - try s.resize(3, 5); - { - const contents = try s.testString(alloc, .screen); - defer alloc.free(contents); - try testing.expectEqualStrings(str, contents); - } - { - const contents = try s.testString(alloc, .viewport); - defer alloc.free(contents); - try testing.expectEqualStrings(str, contents); - } - - // Grow again - try s.resize(10, 5); - { - const contents = try s.testString(alloc, .viewport); - defer alloc.free(contents); - try testing.expectEqualStrings(str, contents); - } - { - const contents = try s.testString(alloc, .screen); - defer alloc.free(contents); - try testing.expectEqualStrings(str, contents); - } -} - -// X -test "Screen: resize less cols to eliminate wide char" { - const testing = std.testing; - const alloc = testing.allocator; - - var s = try init(alloc, 1, 2, 0); - defer s.deinit(); - const str = "😀"; - try s.testWriteString(str); - { - const contents = try s.testString(alloc, .screen); - defer alloc.free(contents); - try testing.expectEqualStrings(str, contents); - } - { - const cell = s.getCell(.screen, 0, 0); - try testing.expectEqual(@as(u32, '😀'), cell.char); - try testing.expect(cell.attrs.wide); - } - - // Resize to 1 column can't fit a wide char. So it should be deleted. - try s.resize(1, 1); - { - const contents = try s.testString(alloc, .screen); - defer alloc.free(contents); - try testing.expectEqualStrings(" ", contents); - } - - const cell = s.getCell(.screen, 0, 0); - try testing.expectEqual(@as(u32, ' '), cell.char); - try testing.expect(!cell.attrs.wide); - try testing.expect(!cell.attrs.wide_spacer_tail); - try testing.expect(!cell.attrs.wide_spacer_head); -} - -// X -test "Screen: resize less cols to wrap wide char" { - const testing = std.testing; - const alloc = testing.allocator; - - var s = try init(alloc, 3, 3, 0); - defer s.deinit(); - const str = "x😀"; - try s.testWriteString(str); - { - const contents = try s.testString(alloc, .screen); - defer alloc.free(contents); - try testing.expectEqualStrings(str, contents); - } - { - const cell = s.getCell(.screen, 0, 1); - try testing.expectEqual(@as(u32, '😀'), cell.char); - try testing.expect(cell.attrs.wide); - try testing.expect(s.getCell(.screen, 0, 2).attrs.wide_spacer_tail); - } - - try s.resize(3, 2); - { - const contents = try s.testString(alloc, .screen); - defer alloc.free(contents); - try testing.expectEqualStrings("x\n😀", contents); - } - { - const cell = s.getCell(.screen, 0, 1); - try testing.expectEqual(@as(u32, ' '), cell.char); - try testing.expect(!cell.attrs.wide); - try testing.expect(!cell.attrs.wide_spacer_tail); - try testing.expect(cell.attrs.wide_spacer_head); - } -} - -// X -test "Screen: resize less cols to eliminate wide char with row space" { - const testing = std.testing; - const alloc = testing.allocator; - - var s = try init(alloc, 2, 2, 0); - defer s.deinit(); - const str = "😀"; - try s.testWriteString(str); - { - const contents = try s.testString(alloc, .screen); - defer alloc.free(contents); - try testing.expectEqualStrings(str, contents); - } - { - const cell = s.getCell(.screen, 0, 0); - try testing.expectEqual(@as(u32, '😀'), cell.char); - try testing.expect(cell.attrs.wide); - try testing.expect(s.getCell(.screen, 0, 1).attrs.wide_spacer_tail); - } - - try s.resize(2, 1); - { - const contents = try s.testString(alloc, .screen); - defer alloc.free(contents); - try testing.expectEqualStrings(" \n ", contents); - } - { - const cell = s.getCell(.screen, 0, 0); - try testing.expectEqual(@as(u32, ' '), cell.char); - try testing.expect(!cell.attrs.wide); - try testing.expect(!cell.attrs.wide_spacer_tail); - try testing.expect(!cell.attrs.wide_spacer_head); - } -} - -// X -test "Screen: resize more cols with wide spacer head" { - const testing = std.testing; - const alloc = testing.allocator; - - var s = try init(alloc, 2, 3, 0); - defer s.deinit(); - const str = " 😀"; - try s.testWriteString(str); - { - const contents = try s.testString(alloc, .screen); - defer alloc.free(contents); - try testing.expectEqualStrings(" \n😀", contents); - } - - // So this is the key point: we end up with a wide spacer head at - // the end of row 1, then the emoji, then a wide spacer tail on row 2. - // We should expect that if we resize to more cols, the wide spacer - // head is replaced with the emoji. - { - const cell = s.getCell(.screen, 0, 2); - try testing.expectEqual(@as(u32, ' '), cell.char); - try testing.expect(cell.attrs.wide_spacer_head); - try testing.expect(s.getCell(.screen, 1, 0).attrs.wide); - try testing.expect(s.getCell(.screen, 1, 1).attrs.wide_spacer_tail); - } - - try s.resize(2, 4); - { - const contents = try s.testString(alloc, .screen); - defer alloc.free(contents); - try testing.expectEqualStrings(str, contents); - } - { - const cell = s.getCell(.screen, 0, 2); - try testing.expectEqual(@as(u32, '😀'), cell.char); - try testing.expect(!cell.attrs.wide_spacer_head); - try testing.expect(cell.attrs.wide); - try testing.expect(s.getCell(.screen, 0, 3).attrs.wide_spacer_tail); - } -} - -// X -test "Screen: resize less cols preserves grapheme cluster" { - const testing = std.testing; - const alloc = testing.allocator; - - var s = try init(alloc, 1, 5, 0); - defer s.deinit(); - const str: []const u8 = &.{ 0x43, 0xE2, 0x83, 0x90 }; // C⃐ (C with combining left arrow) - try s.testWriteString(str); - - // We should have a single cell with all the codepoints - { - const row = s.getRow(.{ .screen = 0 }); - try testing.expectEqual(@as(usize, 2), row.codepointLen(0)); - } - { - const contents = try s.testString(alloc, .screen); - defer alloc.free(contents); - try testing.expectEqualStrings(str, contents); - } - - // Resize to less columns. No wrapping, but we should still have - // the same grapheme cluster. - try s.resize(1, 4); - { - const contents = try s.testString(alloc, .screen); - defer alloc.free(contents); - try testing.expectEqualStrings(str, contents); - } -} - -// X -test "Screen: resize more cols with wide spacer head multiple lines" { - const testing = std.testing; - const alloc = testing.allocator; - - var s = try init(alloc, 3, 3, 0); - defer s.deinit(); - const str = "xxxyy😀"; - try s.testWriteString(str); - { - const contents = try s.testString(alloc, .screen); - defer alloc.free(contents); - try testing.expectEqualStrings("xxx\nyy\n😀", contents); - } - - // Similar to the "wide spacer head" test, but this time we'er going - // to increase our columns such that multiple rows are unwrapped. - { - const cell = s.getCell(.screen, 1, 2); - try testing.expectEqual(@as(u32, ' '), cell.char); - try testing.expect(cell.attrs.wide_spacer_head); - try testing.expect(s.getCell(.screen, 2, 0).attrs.wide); - try testing.expect(s.getCell(.screen, 2, 1).attrs.wide_spacer_tail); - } - - try s.resize(2, 8); - { - const contents = try s.testString(alloc, .screen); - defer alloc.free(contents); - try testing.expectEqualStrings(str, contents); - } - { - const cell = s.getCell(.screen, 0, 5); - try testing.expect(!cell.attrs.wide_spacer_head); - try testing.expectEqual(@as(u32, '😀'), cell.char); - try testing.expect(cell.attrs.wide); - try testing.expect(s.getCell(.screen, 0, 6).attrs.wide_spacer_tail); - } -} - -// X -test "Screen: resize more cols requiring a wide spacer head" { - const testing = std.testing; - const alloc = testing.allocator; - - var s = try init(alloc, 2, 2, 0); - defer s.deinit(); - const str = "xx😀"; - try s.testWriteString(str); - { - const contents = try s.testString(alloc, .screen); - defer alloc.free(contents); - try testing.expectEqualStrings("xx\n😀", contents); - } - { - try testing.expect(s.getCell(.screen, 1, 0).attrs.wide); - try testing.expect(s.getCell(.screen, 1, 1).attrs.wide_spacer_tail); - } - - // This resizes to 3 columns, which isn't enough space for our wide - // char to enter row 1. But we need to mark the wide spacer head on the - // end of the first row since we're wrapping to the next row. - try s.resize(2, 3); - { - const contents = try s.testString(alloc, .screen); - defer alloc.free(contents); - try testing.expectEqualStrings("xx\n😀", contents); - } - { - const cell = s.getCell(.screen, 0, 2); - try testing.expectEqual(@as(u32, ' '), cell.char); - try testing.expect(cell.attrs.wide_spacer_head); - try testing.expect(s.getCell(.screen, 1, 0).attrs.wide); - try testing.expect(s.getCell(.screen, 1, 1).attrs.wide_spacer_tail); - } - { - const cell = s.getCell(.screen, 1, 0); - try testing.expectEqual(@as(u32, '😀'), cell.char); - try testing.expect(cell.attrs.wide); - try testing.expect(s.getCell(.screen, 1, 1).attrs.wide_spacer_tail); - } -} - -test "Screen: jump zero" { - const testing = std.testing; - const alloc = testing.allocator; - - var s = try init(alloc, 3, 5, 10); - defer s.deinit(); - try s.testWriteString("1ABCD\n2EFGH\n3IJKL\n"); - try s.testWriteString("4ABCD\n5EFGH\n6IJKL"); - try testing.expect(s.viewportIsBottom()); - - // Set semantic prompts - { - const row = s.getRow(.{ .screen = 1 }); - row.setSemanticPrompt(.prompt); - } - { - const row = s.getRow(.{ .screen = 5 }); - row.setSemanticPrompt(.prompt); - } - - try testing.expect(!s.jump(.{ .prompt_delta = 0 })); - try testing.expectEqual(@as(usize, 3), s.viewport); -} - -test "Screen: jump to prompt" { - const testing = std.testing; - const alloc = testing.allocator; - - var s = try init(alloc, 3, 5, 10); - defer s.deinit(); - try s.testWriteString("1ABCD\n2EFGH\n3IJKL\n"); - try s.testWriteString("4ABCD\n5EFGH\n6IJKL"); - try testing.expect(s.viewportIsBottom()); - - // Set semantic prompts - { - const row = s.getRow(.{ .screen = 1 }); - row.setSemanticPrompt(.prompt); - } - { - const row = s.getRow(.{ .screen = 5 }); - row.setSemanticPrompt(.prompt); - } - - // Jump back - try testing.expect(s.jump(.{ .prompt_delta = -1 })); - try testing.expectEqual(@as(usize, 1), s.viewport); - - // Jump back - try testing.expect(!s.jump(.{ .prompt_delta = -1 })); - try testing.expectEqual(@as(usize, 1), s.viewport); - - // Jump forward - try testing.expect(s.jump(.{ .prompt_delta = 1 })); - try testing.expectEqual(@as(usize, 3), s.viewport); - - // Jump forward - try testing.expect(!s.jump(.{ .prompt_delta = 1 })); - try testing.expectEqual(@as(usize, 3), s.viewport); -} - -test "Screen: row graphemeBreak" { - const testing = std.testing; - const alloc = testing.allocator; - - var s = try init(alloc, 1, 10, 0); - defer s.deinit(); - try s.testWriteString("x"); - try s.testWriteString("👨‍A"); - - const row = s.getRow(.{ .screen = 0 }); - - // Normal char is a break - try testing.expect(row.graphemeBreak(0)); - - // Emoji with ZWJ is not - try testing.expect(!row.graphemeBreak(1)); -} diff --git a/src/terminal-old/Selection.zig b/src/terminal-old/Selection.zig deleted file mode 100644 index d29513d73..000000000 --- a/src/terminal-old/Selection.zig +++ /dev/null @@ -1,1165 +0,0 @@ -/// Represents a single selection within the terminal -/// (i.e. a highlight region). -const Selection = @This(); - -const std = @import("std"); -const assert = std.debug.assert; -const point = @import("point.zig"); -const Screen = @import("Screen.zig"); -const ScreenPoint = point.ScreenPoint; - -/// Start and end of the selection. There is no guarantee that -/// start is before end or vice versa. If a user selects backwards, -/// start will be after end, and vice versa. Use the struct functions -/// to not have to worry about this. -start: ScreenPoint, -end: ScreenPoint, - -/// Whether or not this selection refers to a rectangle, rather than whole -/// lines of a buffer. In this mode, start and end refer to the top left and -/// bottom right of the rectangle, or vice versa if the selection is backwards. -rectangle: bool = false, - -/// Converts a selection screen points to viewport points (still typed -/// as ScreenPoints) if the selection is present within the viewport -/// of the screen. -pub fn toViewport(self: Selection, screen: *const Screen) ?Selection { - const top = (point.Viewport{ .x = 0, .y = 0 }).toScreen(screen); - const bot = (point.Viewport{ .x = screen.cols - 1, .y = screen.rows - 1 }).toScreen(screen); - - // If our selection isn't within the viewport, do nothing. - if (!self.within(top, bot)) return null; - - // Convert - const start = self.start.toViewport(screen); - const end = self.end.toViewport(screen); - return Selection{ - .start = .{ .x = if (self.rectangle) self.start.x else start.x, .y = start.y }, - .end = .{ .x = if (self.rectangle) self.end.x else end.x, .y = end.y }, - .rectangle = self.rectangle, - }; -} - -/// Returns true if the selection is empty. -pub fn empty(self: Selection) bool { - return self.start.x == self.end.x and self.start.y == self.end.y; -} - -/// Returns true if the selection contains the given point. -/// -/// This recalculates top left and bottom right each call. If you have -/// many points to check, it is cheaper to do the containment logic -/// yourself and cache the topleft/bottomright. -pub fn contains(self: Selection, p: ScreenPoint) bool { - const tl = self.topLeft(); - const br = self.bottomRight(); - - // Honestly there is probably way more efficient boolean logic here. - // Look back at this in the future... - - // If we're in rectangle select, we can short-circuit with an easy check - // here - if (self.rectangle) - return p.y >= tl.y and p.y <= br.y and p.x >= tl.x and p.x <= br.x; - - // If tl/br are same line - if (tl.y == br.y) return p.y == tl.y and - p.x >= tl.x and - p.x <= br.x; - - // If on top line, just has to be left of X - if (p.y == tl.y) return p.x >= tl.x; - - // If on bottom line, just has to be right of X - if (p.y == br.y) return p.x <= br.x; - - // If between the top/bottom, always good. - return p.y > tl.y and p.y < br.y; -} - -/// Returns true if the selection contains any of the points between -/// (and including) the start and end. The x values are ignored this is -/// just a section match -pub fn within(self: Selection, start: ScreenPoint, end: ScreenPoint) bool { - const tl = self.topLeft(); - const br = self.bottomRight(); - - // Bottom right is before start, no way we are in it. - if (br.y < start.y) return false; - // Bottom right is the first line, only if our x is in it. - if (br.y == start.y) return br.x >= start.x; - - // If top left is beyond the end, we're not in it. - if (tl.y > end.y) return false; - // If top left is on the end, only if our x is in it. - if (tl.y == end.y) return tl.x <= end.x; - - return true; -} - -/// Returns true if the selection contains the row of the given point, -/// regardless of the x value. -pub fn containsRow(self: Selection, p: ScreenPoint) bool { - const tl = self.topLeft(); - const br = self.bottomRight(); - return p.y >= tl.y and p.y <= br.y; -} - -/// Get a selection for a single row in the screen. This will return null -/// if the row is not included in the selection. -pub fn containedRow(self: Selection, screen: *const Screen, p: ScreenPoint) ?Selection { - const tl = self.topLeft(); - const br = self.bottomRight(); - if (p.y < tl.y or p.y > br.y) return null; - - // Rectangle case: we can return early as the x range will always be the - // same. We've already validated that the row is in the selection. - if (self.rectangle) return .{ - .start = .{ .y = p.y, .x = tl.x }, - .end = .{ .y = p.y, .x = br.x }, - .rectangle = true, - }; - - if (p.y == tl.y) { - // If the selection is JUST this line, return it as-is. - if (p.y == br.y) { - return self; - } - - // Selection top-left line matches only. - return .{ - .start = tl, - .end = .{ .y = tl.y, .x = screen.cols - 1 }, - }; - } - - // Row is our bottom selection, so we return the selection from the - // beginning of the line to the br. We know our selection is more than - // one line (due to conditionals above) - if (p.y == br.y) { - assert(p.y != tl.y); - return .{ - .start = .{ .y = br.y, .x = 0 }, - .end = br, - }; - } - - // Row is somewhere between our selection lines so we return the full line. - return .{ - .start = .{ .y = p.y, .x = 0 }, - .end = .{ .y = p.y, .x = screen.cols - 1 }, - }; -} - -/// Returns the top left point of the selection. -pub fn topLeft(self: Selection) ScreenPoint { - return switch (self.order()) { - .forward => self.start, - .reverse => self.end, - .mirrored_forward => .{ .x = self.end.x, .y = self.start.y }, - .mirrored_reverse => .{ .x = self.start.x, .y = self.end.y }, - }; -} - -/// Returns the bottom right point of the selection. -pub fn bottomRight(self: Selection) ScreenPoint { - return switch (self.order()) { - .forward => self.end, - .reverse => self.start, - .mirrored_forward => .{ .x = self.start.x, .y = self.end.y }, - .mirrored_reverse => .{ .x = self.end.x, .y = self.start.y }, - }; -} - -/// Returns the selection in the given order. -/// -/// Note that only forward and reverse are useful desired orders for this -/// function. All other orders act as if forward order was desired. -pub fn ordered(self: Selection, desired: Order) Selection { - if (self.order() == desired) return self; - const tl = self.topLeft(); - const br = self.bottomRight(); - return switch (desired) { - .forward => .{ .start = tl, .end = br, .rectangle = self.rectangle }, - .reverse => .{ .start = br, .end = tl, .rectangle = self.rectangle }, - else => .{ .start = tl, .end = br, .rectangle = self.rectangle }, - }; -} - -/// The order of the selection: -/// -/// * forward: start(x, y) is before end(x, y) (top-left to bottom-right). -/// * reverse: end(x, y) is before start(x, y) (bottom-right to top-left). -/// * mirrored_[forward|reverse]: special, rectangle selections only (see below). -/// -/// For regular selections, the above also holds for top-right to bottom-left -/// (forward) and bottom-left to top-right (reverse). However, for rectangle -/// selections, both of these selections are *mirrored* as orientation -/// operations only flip the x or y axis, not both. Depending on the y axis -/// direction, this is either mirrored_forward or mirrored_reverse. -/// -pub const Order = enum { forward, reverse, mirrored_forward, mirrored_reverse }; - -pub fn order(self: Selection) Order { - if (self.rectangle) { - // Reverse (also handles single-column) - if (self.start.y > self.end.y and self.start.x >= self.end.x) return .reverse; - if (self.start.y >= self.end.y and self.start.x > self.end.x) return .reverse; - - // Mirror, bottom-left to top-right - if (self.start.y > self.end.y and self.start.x < self.end.x) return .mirrored_reverse; - - // Mirror, top-right to bottom-left - if (self.start.y < self.end.y and self.start.x > self.end.x) return .mirrored_forward; - - // Forward - return .forward; - } - - if (self.start.y < self.end.y) return .forward; - if (self.start.y > self.end.y) return .reverse; - if (self.start.x <= self.end.x) return .forward; - return .reverse; -} - -/// Possible adjustments to the selection. -pub const Adjustment = enum { - left, - right, - up, - down, - home, - end, - page_up, - page_down, -}; - -/// Adjust the selection by some given adjustment. An adjustment allows -/// a selection to be expanded slightly left, right, up, down, etc. -pub fn adjust(self: Selection, screen: *Screen, adjustment: Adjustment) Selection { - const screen_end = Screen.RowIndexTag.screen.maxLen(screen) - 1; - - // Make an editable one because its so much easier to use modification - // logic below than it is to reconstruct the selection every time. - var result = self; - - // Note that we always adjusts "end" because end always represents - // the last point of the selection by mouse, not necessarilly the - // top/bottom visually. So this results in the right behavior - // whether the user drags up or down. - switch (adjustment) { - .up => if (result.end.y == 0) { - result.end.x = 0; - } else { - result.end.y -= 1; - }, - - .down => if (result.end.y >= screen_end) { - result.end.y = screen_end; - result.end.x = screen.cols - 1; - } else { - result.end.y += 1; - }, - - .left => { - // Step left, wrapping to the next row up at the start of each new line, - // until we find a non-empty cell. - // - // This iterator emits the start point first, throw it out. - var iterator = result.end.iterator(screen, .left_up); - _ = iterator.next(); - while (iterator.next()) |next| { - if (screen.getCell( - .screen, - next.y, - next.x, - ).char != 0) { - result.end = next; - break; - } - } - }, - - .right => { - // Step right, wrapping to the next row down at the start of each new line, - // until we find a non-empty cell. - var iterator = result.end.iterator(screen, .right_down); - _ = iterator.next(); - while (iterator.next()) |next| { - if (next.y > screen_end) break; - if (screen.getCell( - .screen, - next.y, - next.x, - ).char != 0) { - if (next.y > screen_end) { - result.end.y = screen_end; - } else { - result.end = next; - } - break; - } - } - }, - - .page_up => if (screen.rows > result.end.y) { - result.end.y = 0; - result.end.x = 0; - } else { - result.end.y -= screen.rows; - }, - - .page_down => if (screen.rows > screen_end - result.end.y) { - result.end.y = screen_end; - result.end.x = screen.cols - 1; - } else { - result.end.y += screen.rows; - }, - - .home => { - result.end.y = 0; - result.end.x = 0; - }, - - .end => { - result.end.y = screen_end; - result.end.x = screen.cols - 1; - }, - } - - return result; -} - -// X -test "Selection: adjust right" { - const testing = std.testing; - var screen = try Screen.init(testing.allocator, 5, 10, 0); - defer screen.deinit(); - try screen.testWriteString("A1234\nB5678\nC1234\nD5678"); - - // Simple movement right - { - const sel = (Selection{ - .start = .{ .x = 5, .y = 1 }, - .end = .{ .x = 3, .y = 3 }, - }).adjust(&screen, .right); - - try testing.expectEqual(Selection{ - .start = .{ .x = 5, .y = 1 }, - .end = .{ .x = 4, .y = 3 }, - }, sel); - } - - // Already at end of the line. - { - const sel = (Selection{ - .start = .{ .x = 5, .y = 1 }, - .end = .{ .x = 4, .y = 2 }, - }).adjust(&screen, .right); - - try testing.expectEqual(Selection{ - .start = .{ .x = 5, .y = 1 }, - .end = .{ .x = 0, .y = 3 }, - }, sel); - } - - // Already at end of the screen - { - const sel = (Selection{ - .start = .{ .x = 5, .y = 1 }, - .end = .{ .x = 4, .y = 3 }, - }).adjust(&screen, .right); - - try testing.expectEqual(Selection{ - .start = .{ .x = 5, .y = 1 }, - .end = .{ .x = 4, .y = 3 }, - }, sel); - } -} - -// X -test "Selection: adjust left" { - const testing = std.testing; - var screen = try Screen.init(testing.allocator, 5, 10, 0); - defer screen.deinit(); - try screen.testWriteString("A1234\nB5678\nC1234\nD5678"); - - // Simple movement left - { - const sel = (Selection{ - .start = .{ .x = 5, .y = 1 }, - .end = .{ .x = 3, .y = 3 }, - }).adjust(&screen, .left); - - // Start line - try testing.expectEqual(Selection{ - .start = .{ .x = 5, .y = 1 }, - .end = .{ .x = 2, .y = 3 }, - }, sel); - } - - // Already at beginning of the line. - { - const sel = (Selection{ - .start = .{ .x = 5, .y = 1 }, - .end = .{ .x = 0, .y = 3 }, - }).adjust(&screen, .left); - - // Start line - try testing.expectEqual(Selection{ - .start = .{ .x = 5, .y = 1 }, - .end = .{ .x = 4, .y = 2 }, - }, sel); - } -} - -// X -test "Selection: adjust left skips blanks" { - const testing = std.testing; - var screen = try Screen.init(testing.allocator, 5, 10, 0); - defer screen.deinit(); - try screen.testWriteString("A1234\nB5678\nC12\nD56"); - - // Same line - { - const sel = (Selection{ - .start = .{ .x = 5, .y = 1 }, - .end = .{ .x = 4, .y = 3 }, - }).adjust(&screen, .left); - - // Start line - try testing.expectEqual(Selection{ - .start = .{ .x = 5, .y = 1 }, - .end = .{ .x = 2, .y = 3 }, - }, sel); - } - - // Edge - { - const sel = (Selection{ - .start = .{ .x = 5, .y = 1 }, - .end = .{ .x = 0, .y = 3 }, - }).adjust(&screen, .left); - - // Start line - try testing.expectEqual(Selection{ - .start = .{ .x = 5, .y = 1 }, - .end = .{ .x = 2, .y = 2 }, - }, sel); - } -} - -// X -test "Selection: adjust up" { - const testing = std.testing; - var screen = try Screen.init(testing.allocator, 5, 10, 0); - defer screen.deinit(); - try screen.testWriteString("A\nB\nC\nD\nE"); - - // Not on the first line - { - const sel = (Selection{ - .start = .{ .x = 5, .y = 1 }, - .end = .{ .x = 3, .y = 3 }, - }).adjust(&screen, .up); - - // Start line - try testing.expectEqual(Selection{ - .start = .{ .x = 5, .y = 1 }, - .end = .{ .x = 3, .y = 2 }, - }, sel); - } - - // On the first line - { - const sel = (Selection{ - .start = .{ .x = 5, .y = 1 }, - .end = .{ .x = 3, .y = 0 }, - }).adjust(&screen, .up); - - // Start line - try testing.expectEqual(Selection{ - .start = .{ .x = 5, .y = 1 }, - .end = .{ .x = 0, .y = 0 }, - }, sel); - } -} - -// X -test "Selection: adjust down" { - const testing = std.testing; - var screen = try Screen.init(testing.allocator, 5, 10, 0); - defer screen.deinit(); - try screen.testWriteString("A\nB\nC\nD\nE"); - - // Not on the first line - { - const sel = (Selection{ - .start = .{ .x = 5, .y = 1 }, - .end = .{ .x = 3, .y = 3 }, - }).adjust(&screen, .down); - - // Start line - try testing.expectEqual(Selection{ - .start = .{ .x = 5, .y = 1 }, - .end = .{ .x = 3, .y = 4 }, - }, sel); - } - - // On the last line - { - const sel = (Selection{ - .start = .{ .x = 5, .y = 1 }, - .end = .{ .x = 3, .y = 4 }, - }).adjust(&screen, .down); - - // Start line - try testing.expectEqual(Selection{ - .start = .{ .x = 5, .y = 1 }, - .end = .{ .x = 9, .y = 4 }, - }, sel); - } -} - -// X -test "Selection: adjust down with not full screen" { - const testing = std.testing; - var screen = try Screen.init(testing.allocator, 5, 10, 0); - defer screen.deinit(); - try screen.testWriteString("A\nB\nC"); - - // On the last line - { - const sel = (Selection{ - .start = .{ .x = 5, .y = 1 }, - .end = .{ .x = 3, .y = 2 }, - }).adjust(&screen, .down); - - // Start line - try testing.expectEqual(Selection{ - .start = .{ .x = 5, .y = 1 }, - .end = .{ .x = 9, .y = 2 }, - }, sel); - } -} - -// X -test "Selection: contains" { - const testing = std.testing; - { - const sel: Selection = .{ - .start = .{ .x = 5, .y = 1 }, - .end = .{ .x = 3, .y = 2 }, - }; - - try testing.expect(sel.contains(.{ .x = 6, .y = 1 })); - try testing.expect(sel.contains(.{ .x = 1, .y = 2 })); - try testing.expect(!sel.contains(.{ .x = 1, .y = 1 })); - try testing.expect(!sel.contains(.{ .x = 5, .y = 2 })); - try testing.expect(!sel.containsRow(.{ .x = 1, .y = 3 })); - try testing.expect(sel.containsRow(.{ .x = 1, .y = 1 })); - try testing.expect(sel.containsRow(.{ .x = 5, .y = 2 })); - } - - // Reverse - { - const sel: Selection = .{ - .start = .{ .x = 3, .y = 2 }, - .end = .{ .x = 5, .y = 1 }, - }; - - try testing.expect(sel.contains(.{ .x = 6, .y = 1 })); - try testing.expect(sel.contains(.{ .x = 1, .y = 2 })); - try testing.expect(!sel.contains(.{ .x = 1, .y = 1 })); - try testing.expect(!sel.contains(.{ .x = 5, .y = 2 })); - } - - // Single line - { - const sel: Selection = .{ - .start = .{ .x = 5, .y = 1 }, - .end = .{ .x = 10, .y = 1 }, - }; - - try testing.expect(sel.contains(.{ .x = 6, .y = 1 })); - try testing.expect(!sel.contains(.{ .x = 2, .y = 1 })); - try testing.expect(!sel.contains(.{ .x = 12, .y = 1 })); - } -} - -// X -test "Selection: contains, rectangle" { - const testing = std.testing; - { - const sel: Selection = .{ - .start = .{ .x = 3, .y = 3 }, - .end = .{ .x = 7, .y = 9 }, - .rectangle = true, - }; - - try testing.expect(sel.contains(.{ .x = 5, .y = 6 })); // Center - try testing.expect(sel.contains(.{ .x = 3, .y = 6 })); // Left border - try testing.expect(sel.contains(.{ .x = 7, .y = 6 })); // Right border - try testing.expect(sel.contains(.{ .x = 5, .y = 3 })); // Top border - try testing.expect(sel.contains(.{ .x = 5, .y = 9 })); // Bottom border - - try testing.expect(!sel.contains(.{ .x = 5, .y = 2 })); // Above center - try testing.expect(!sel.contains(.{ .x = 5, .y = 10 })); // Below center - try testing.expect(!sel.contains(.{ .x = 2, .y = 6 })); // Left center - try testing.expect(!sel.contains(.{ .x = 8, .y = 6 })); // Right center - try testing.expect(!sel.contains(.{ .x = 8, .y = 3 })); // Just right of top right - try testing.expect(!sel.contains(.{ .x = 2, .y = 9 })); // Just left of bottom left - - try testing.expect(!sel.containsRow(.{ .x = 1, .y = 1 })); - try testing.expect(sel.containsRow(.{ .x = 1, .y = 3 })); // x does not matter - try testing.expect(sel.containsRow(.{ .x = 1, .y = 6 })); - try testing.expect(sel.containsRow(.{ .x = 5, .y = 9 })); - try testing.expect(!sel.containsRow(.{ .x = 5, .y = 10 })); - } - - // Reverse - { - const sel: Selection = .{ - .start = .{ .x = 7, .y = 9 }, - .end = .{ .x = 3, .y = 3 }, - .rectangle = true, - }; - - try testing.expect(sel.contains(.{ .x = 5, .y = 6 })); // Center - try testing.expect(sel.contains(.{ .x = 3, .y = 6 })); // Left border - try testing.expect(sel.contains(.{ .x = 7, .y = 6 })); // Right border - try testing.expect(sel.contains(.{ .x = 5, .y = 3 })); // Top border - try testing.expect(sel.contains(.{ .x = 5, .y = 9 })); // Bottom border - - try testing.expect(!sel.contains(.{ .x = 5, .y = 2 })); // Above center - try testing.expect(!sel.contains(.{ .x = 5, .y = 10 })); // Below center - try testing.expect(!sel.contains(.{ .x = 2, .y = 6 })); // Left center - try testing.expect(!sel.contains(.{ .x = 8, .y = 6 })); // Right center - try testing.expect(!sel.contains(.{ .x = 8, .y = 3 })); // Just right of top right - try testing.expect(!sel.contains(.{ .x = 2, .y = 9 })); // Just left of bottom left - - try testing.expect(!sel.containsRow(.{ .x = 1, .y = 1 })); - try testing.expect(sel.containsRow(.{ .x = 1, .y = 3 })); // x does not matter - try testing.expect(sel.containsRow(.{ .x = 1, .y = 6 })); - try testing.expect(sel.containsRow(.{ .x = 5, .y = 9 })); - try testing.expect(!sel.containsRow(.{ .x = 5, .y = 10 })); - } - - // Single line - // NOTE: This is the same as normal selection but we just do it for brevity - { - const sel: Selection = .{ - .start = .{ .x = 5, .y = 1 }, - .end = .{ .x = 10, .y = 1 }, - .rectangle = true, - }; - - try testing.expect(sel.contains(.{ .x = 6, .y = 1 })); - try testing.expect(!sel.contains(.{ .x = 2, .y = 1 })); - try testing.expect(!sel.contains(.{ .x = 12, .y = 1 })); - } -} - -test "Selection: containedRow" { - const testing = std.testing; - var screen = try Screen.init(testing.allocator, 5, 10, 0); - defer screen.deinit(); - - { - const sel: Selection = .{ - .start = .{ .x = 5, .y = 1 }, - .end = .{ .x = 3, .y = 3 }, - }; - - // Not contained - try testing.expect(sel.containedRow(&screen, .{ .x = 1, .y = 4 }) == null); - - // Start line - try testing.expectEqual(Selection{ - .start = sel.start, - .end = .{ .x = screen.cols - 1, .y = 1 }, - }, sel.containedRow(&screen, .{ .x = 1, .y = 1 }).?); - - // End line - try testing.expectEqual(Selection{ - .start = .{ .x = 0, .y = 3 }, - .end = sel.end, - }, sel.containedRow(&screen, .{ .x = 2, .y = 3 }).?); - - // Middle line - try testing.expectEqual(Selection{ - .start = .{ .x = 0, .y = 2 }, - .end = .{ .x = screen.cols - 1, .y = 2 }, - }, sel.containedRow(&screen, .{ .x = 2, .y = 2 }).?); - } - - // Rectangle - { - const sel: Selection = .{ - .start = .{ .x = 3, .y = 1 }, - .end = .{ .x = 6, .y = 3 }, - .rectangle = true, - }; - - // Not contained - try testing.expect(sel.containedRow(&screen, .{ .x = 1, .y = 4 }) == null); - - // Start line - try testing.expectEqual(Selection{ - .start = .{ .x = 3, .y = 1 }, - .end = .{ .x = 6, .y = 1 }, - .rectangle = true, - }, sel.containedRow(&screen, .{ .x = 1, .y = 1 }).?); - - // End line - try testing.expectEqual(Selection{ - .start = .{ .x = 3, .y = 3 }, - .end = .{ .x = 6, .y = 3 }, - .rectangle = true, - }, sel.containedRow(&screen, .{ .x = 2, .y = 3 }).?); - - // Middle line - try testing.expectEqual(Selection{ - .start = .{ .x = 3, .y = 2 }, - .end = .{ .x = 6, .y = 2 }, - .rectangle = true, - }, sel.containedRow(&screen, .{ .x = 2, .y = 2 }).?); - } - - // Single-line selection - { - const sel: Selection = .{ - .start = .{ .x = 2, .y = 1 }, - .end = .{ .x = 6, .y = 1 }, - }; - - // Not contained - try testing.expect(sel.containedRow(&screen, .{ .x = 1, .y = 0 }) == null); - try testing.expect(sel.containedRow(&screen, .{ .x = 1, .y = 2 }) == null); - - // Contained - try testing.expectEqual(Selection{ - .start = sel.start, - .end = sel.end, - }, sel.containedRow(&screen, .{ .x = 1, .y = 1 }).?); - } -} - -test "Selection: within" { - const testing = std.testing; - { - const sel: Selection = .{ - .start = .{ .x = 5, .y = 1 }, - .end = .{ .x = 3, .y = 2 }, - }; - - // Fully within - try testing.expect(sel.within(.{ .x = 6, .y = 0 }, .{ .x = 6, .y = 3 })); - try testing.expect(sel.within(.{ .x = 3, .y = 1 }, .{ .x = 6, .y = 3 })); - try testing.expect(sel.within(.{ .x = 3, .y = 0 }, .{ .x = 6, .y = 2 })); - - // Partially within - try testing.expect(sel.within(.{ .x = 1, .y = 2 }, .{ .x = 6, .y = 3 })); - try testing.expect(sel.within(.{ .x = 1, .y = 0 }, .{ .x = 6, .y = 1 })); - - // Not within at all - try testing.expect(!sel.within(.{ .x = 0, .y = 0 }, .{ .x = 4, .y = 1 })); - } -} - -// X -test "Selection: order, standard" { - const testing = std.testing; - { - // forward, multi-line - const sel: Selection = .{ - .start = .{ .x = 2, .y = 1 }, - .end = .{ .x = 2, .y = 2 }, - }; - - try testing.expect(sel.order() == .forward); - } - { - // reverse, multi-line - const sel: Selection = .{ - .start = .{ .x = 2, .y = 2 }, - .end = .{ .x = 2, .y = 1 }, - }; - - try testing.expect(sel.order() == .reverse); - } - { - // forward, same-line - const sel: Selection = .{ - .start = .{ .x = 2, .y = 1 }, - .end = .{ .x = 3, .y = 1 }, - }; - - try testing.expect(sel.order() == .forward); - } - { - // forward, single char - const sel: Selection = .{ - .start = .{ .x = 2, .y = 1 }, - .end = .{ .x = 2, .y = 1 }, - }; - - try testing.expect(sel.order() == .forward); - } - { - // reverse, single line - const sel: Selection = .{ - .start = .{ .x = 2, .y = 1 }, - .end = .{ .x = 1, .y = 1 }, - }; - - try testing.expect(sel.order() == .reverse); - } -} - -// X -test "Selection: order, rectangle" { - const testing = std.testing; - // Conventions: - // TL - top left - // BL - bottom left - // TR - top right - // BR - bottom right - { - // forward (TL -> BR) - const sel: Selection = .{ - .start = .{ .x = 1, .y = 1 }, - .end = .{ .x = 2, .y = 2 }, - .rectangle = true, - }; - - try testing.expect(sel.order() == .forward); - } - { - // reverse (BR -> TL) - const sel: Selection = .{ - .start = .{ .x = 2, .y = 2 }, - .end = .{ .x = 1, .y = 1 }, - .rectangle = true, - }; - - try testing.expect(sel.order() == .reverse); - } - { - // mirrored_forward (TR -> BL) - const sel: Selection = .{ - .start = .{ .x = 3, .y = 1 }, - .end = .{ .x = 1, .y = 3 }, - .rectangle = true, - }; - - try testing.expect(sel.order() == .mirrored_forward); - } - { - // mirrored_reverse (BL -> TR) - const sel: Selection = .{ - .start = .{ .x = 1, .y = 3 }, - .end = .{ .x = 3, .y = 1 }, - .rectangle = true, - }; - - try testing.expect(sel.order() == .mirrored_reverse); - } - { - // forward, single line (left -> right ) - const sel: Selection = .{ - .start = .{ .x = 1, .y = 1 }, - .end = .{ .x = 3, .y = 1 }, - .rectangle = true, - }; - - try testing.expect(sel.order() == .forward); - } - { - // reverse, single line (right -> left) - const sel: Selection = .{ - .start = .{ .x = 3, .y = 1 }, - .end = .{ .x = 1, .y = 1 }, - .rectangle = true, - }; - - try testing.expect(sel.order() == .reverse); - } - { - // forward, single column (top -> bottom) - const sel: Selection = .{ - .start = .{ .x = 2, .y = 1 }, - .end = .{ .x = 2, .y = 3 }, - .rectangle = true, - }; - - try testing.expect(sel.order() == .forward); - } - { - // reverse, single column (bottom -> top) - const sel: Selection = .{ - .start = .{ .x = 2, .y = 3 }, - .end = .{ .x = 2, .y = 1 }, - .rectangle = true, - }; - - try testing.expect(sel.order() == .reverse); - } - { - // forward, single cell - const sel: Selection = .{ - .start = .{ .x = 1, .y = 1 }, - .end = .{ .x = 1, .y = 1 }, - .rectangle = true, - }; - - try testing.expect(sel.order() == .forward); - } -} - -// X -test "topLeft" { - const testing = std.testing; - { - // forward - const sel: Selection = .{ - .start = .{ .x = 1, .y = 1 }, - .end = .{ .x = 3, .y = 1 }, - }; - const expected: ScreenPoint = .{ .x = 1, .y = 1 }; - try testing.expectEqual(sel.topLeft(), expected); - } - { - // reverse - const sel: Selection = .{ - .start = .{ .x = 3, .y = 1 }, - .end = .{ .x = 1, .y = 1 }, - }; - const expected: ScreenPoint = .{ .x = 1, .y = 1 }; - try testing.expectEqual(sel.topLeft(), expected); - } - { - // mirrored_forward - const sel: Selection = .{ - .start = .{ .x = 3, .y = 1 }, - .end = .{ .x = 1, .y = 3 }, - .rectangle = true, - }; - const expected: ScreenPoint = .{ .x = 1, .y = 1 }; - try testing.expectEqual(sel.topLeft(), expected); - } - { - // mirrored_reverse - const sel: Selection = .{ - .start = .{ .x = 1, .y = 3 }, - .end = .{ .x = 3, .y = 1 }, - .rectangle = true, - }; - const expected: ScreenPoint = .{ .x = 1, .y = 1 }; - try testing.expectEqual(sel.topLeft(), expected); - } -} - -// X -test "bottomRight" { - const testing = std.testing; - { - // forward - const sel: Selection = .{ - .start = .{ .x = 1, .y = 1 }, - .end = .{ .x = 3, .y = 1 }, - }; - const expected: ScreenPoint = .{ .x = 3, .y = 1 }; - try testing.expectEqual(sel.bottomRight(), expected); - } - { - // reverse - const sel: Selection = .{ - .start = .{ .x = 3, .y = 1 }, - .end = .{ .x = 1, .y = 1 }, - }; - const expected: ScreenPoint = .{ .x = 3, .y = 1 }; - try testing.expectEqual(sel.bottomRight(), expected); - } - { - // mirrored_forward - const sel: Selection = .{ - .start = .{ .x = 3, .y = 1 }, - .end = .{ .x = 1, .y = 3 }, - .rectangle = true, - }; - const expected: ScreenPoint = .{ .x = 3, .y = 3 }; - try testing.expectEqual(sel.bottomRight(), expected); - } - { - // mirrored_reverse - const sel: Selection = .{ - .start = .{ .x = 1, .y = 3 }, - .end = .{ .x = 3, .y = 1 }, - .rectangle = true, - }; - const expected: ScreenPoint = .{ .x = 3, .y = 3 }; - try testing.expectEqual(sel.bottomRight(), expected); - } -} - -// X -test "ordered" { - const testing = std.testing; - { - // forward - const sel: Selection = .{ - .start = .{ .x = 1, .y = 1 }, - .end = .{ .x = 3, .y = 1 }, - }; - const sel_reverse: Selection = .{ - .start = .{ .x = 3, .y = 1 }, - .end = .{ .x = 1, .y = 1 }, - }; - try testing.expectEqual(sel.ordered(.forward), sel); - try testing.expectEqual(sel.ordered(.reverse), sel_reverse); - try testing.expectEqual(sel.ordered(.mirrored_reverse), sel); - } - { - // reverse - const sel: Selection = .{ - .start = .{ .x = 3, .y = 1 }, - .end = .{ .x = 1, .y = 1 }, - }; - const sel_forward: Selection = .{ - .start = .{ .x = 1, .y = 1 }, - .end = .{ .x = 3, .y = 1 }, - }; - try testing.expectEqual(sel.ordered(.forward), sel_forward); - try testing.expectEqual(sel.ordered(.reverse), sel); - try testing.expectEqual(sel.ordered(.mirrored_forward), sel_forward); - } - { - // mirrored_forward - const sel: Selection = .{ - .start = .{ .x = 3, .y = 1 }, - .end = .{ .x = 1, .y = 3 }, - .rectangle = true, - }; - const sel_forward: Selection = .{ - .start = .{ .x = 1, .y = 1 }, - .end = .{ .x = 3, .y = 3 }, - .rectangle = true, - }; - const sel_reverse: Selection = .{ - .start = .{ .x = 3, .y = 3 }, - .end = .{ .x = 1, .y = 1 }, - .rectangle = true, - }; - try testing.expectEqual(sel.ordered(.forward), sel_forward); - try testing.expectEqual(sel.ordered(.reverse), sel_reverse); - try testing.expectEqual(sel.ordered(.mirrored_reverse), sel_forward); - } - { - // mirrored_reverse - const sel: Selection = .{ - .start = .{ .x = 1, .y = 3 }, - .end = .{ .x = 3, .y = 1 }, - .rectangle = true, - }; - const sel_forward: Selection = .{ - .start = .{ .x = 1, .y = 1 }, - .end = .{ .x = 3, .y = 3 }, - .rectangle = true, - }; - const sel_reverse: Selection = .{ - .start = .{ .x = 3, .y = 3 }, - .end = .{ .x = 1, .y = 1 }, - .rectangle = true, - }; - try testing.expectEqual(sel.ordered(.forward), sel_forward); - try testing.expectEqual(sel.ordered(.reverse), sel_reverse); - try testing.expectEqual(sel.ordered(.mirrored_forward), sel_forward); - } -} - -test "toViewport" { - const testing = std.testing; - var screen = try Screen.init(testing.allocator, 24, 80, 0); - defer screen.deinit(); - screen.viewport = 11; // Scroll us down a bit - { - // Not in viewport (null) - const sel: Selection = .{ - .start = .{ .x = 10, .y = 1 }, - .end = .{ .x = 3, .y = 3 }, - .rectangle = false, - }; - try testing.expectEqual(null, sel.toViewport(&screen)); - } - { - // In viewport - const sel: Selection = .{ - .start = .{ .x = 10, .y = 11 }, - .end = .{ .x = 3, .y = 13 }, - .rectangle = false, - }; - const want: Selection = .{ - .start = .{ .x = 10, .y = 0 }, - .end = .{ .x = 3, .y = 2 }, - .rectangle = false, - }; - try testing.expectEqual(want, sel.toViewport(&screen)); - } - { - // Top off viewport - const sel: Selection = .{ - .start = .{ .x = 10, .y = 1 }, - .end = .{ .x = 3, .y = 13 }, - .rectangle = false, - }; - const want: Selection = .{ - .start = .{ .x = 0, .y = 0 }, - .end = .{ .x = 3, .y = 2 }, - .rectangle = false, - }; - try testing.expectEqual(want, sel.toViewport(&screen)); - } - { - // Bottom off viewport - const sel: Selection = .{ - .start = .{ .x = 10, .y = 11 }, - .end = .{ .x = 3, .y = 40 }, - .rectangle = false, - }; - const want: Selection = .{ - .start = .{ .x = 10, .y = 0 }, - .end = .{ .x = 79, .y = 23 }, - .rectangle = false, - }; - try testing.expectEqual(want, sel.toViewport(&screen)); - } - { - // Both off viewport - const sel: Selection = .{ - .start = .{ .x = 10, .y = 1 }, - .end = .{ .x = 3, .y = 40 }, - .rectangle = false, - }; - const want: Selection = .{ - .start = .{ .x = 0, .y = 0 }, - .end = .{ .x = 79, .y = 23 }, - .rectangle = false, - }; - try testing.expectEqual(want, sel.toViewport(&screen)); - } - { - // Both off viewport (rectangle) - const sel: Selection = .{ - .start = .{ .x = 10, .y = 1 }, - .end = .{ .x = 3, .y = 40 }, - .rectangle = true, - }; - const want: Selection = .{ - .start = .{ .x = 10, .y = 0 }, - .end = .{ .x = 3, .y = 23 }, - .rectangle = true, - }; - try testing.expectEqual(want, sel.toViewport(&screen)); - } -} diff --git a/src/terminal-old/StringMap.zig b/src/terminal-old/StringMap.zig deleted file mode 100644 index 588013d9d..000000000 --- a/src/terminal-old/StringMap.zig +++ /dev/null @@ -1,124 +0,0 @@ -/// A string along with the mapping of each individual byte in the string -/// to the point in the screen. -const StringMap = @This(); - -const std = @import("std"); -const oni = @import("oniguruma"); -const point = @import("point.zig"); -const Selection = @import("Selection.zig"); -const Screen = @import("Screen.zig"); -const Allocator = std.mem.Allocator; - -string: [:0]const u8, -map: []point.ScreenPoint, - -pub fn deinit(self: StringMap, alloc: Allocator) void { - alloc.free(self.string); - alloc.free(self.map); -} - -/// Returns an iterator that yields the next match of the given regex. -pub fn searchIterator( - self: StringMap, - regex: oni.Regex, -) SearchIterator { - return .{ .map = self, .regex = regex }; -} - -/// Iterates over the regular expression matches of the string. -pub const SearchIterator = struct { - map: StringMap, - regex: oni.Regex, - offset: usize = 0, - - /// Returns the next regular expression match or null if there are - /// no more matches. - pub fn next(self: *SearchIterator) !?Match { - if (self.offset >= self.map.string.len) return null; - - var region = self.regex.search( - self.map.string[self.offset..], - .{}, - ) catch |err| switch (err) { - error.Mismatch => { - self.offset = self.map.string.len; - return null; - }, - - else => return err, - }; - errdefer region.deinit(); - - // Increment our offset by the number of bytes in the match. - // We defer this so that we can return the match before - // modifying the offset. - const end_idx: usize = @intCast(region.ends()[0]); - defer self.offset += end_idx; - - return .{ - .map = self.map, - .offset = self.offset, - .region = region, - }; - } -}; - -/// A single regular expression match. -pub const Match = struct { - map: StringMap, - offset: usize, - region: oni.Region, - - pub fn deinit(self: *Match) void { - self.region.deinit(); - } - - /// Returns the selection containing the full match. - pub fn selection(self: Match) Selection { - const start_idx: usize = @intCast(self.region.starts()[0]); - const end_idx: usize = @intCast(self.region.ends()[0] - 1); - const start_pt = self.map.map[self.offset + start_idx]; - const end_pt = self.map.map[self.offset + end_idx]; - return .{ .start = start_pt, .end = end_pt }; - } -}; - -test "searchIterator" { - const testing = std.testing; - const alloc = testing.allocator; - - // Initialize our regex - try oni.testing.ensureInit(); - var re = try oni.Regex.init( - "[A-B]{2}", - .{}, - oni.Encoding.utf8, - oni.Syntax.default, - null, - ); - defer re.deinit(); - - // Initialize our screen - var s = try Screen.init(alloc, 5, 5, 0); - defer s.deinit(); - const str = "1ABCD2EFGH\n3IJKL"; - try s.testWriteString(str); - const line = s.getLine(.{ .x = 2, .y = 1 }).?; - const map = try line.stringMap(alloc); - defer map.deinit(alloc); - - // Get our iterator - var it = map.searchIterator(re); - { - var match = (try it.next()).?; - defer match.deinit(); - - const sel = match.selection(); - try testing.expectEqual(Selection{ - .start = .{ .x = 1, .y = 0 }, - .end = .{ .x = 2, .y = 0 }, - }, sel); - } - - try testing.expect(try it.next() == null); -} diff --git a/src/terminal-old/Tabstops.zig b/src/terminal-old/Tabstops.zig deleted file mode 100644 index 5a54fb28b..000000000 --- a/src/terminal-old/Tabstops.zig +++ /dev/null @@ -1,231 +0,0 @@ -//! Keep track of the location of tabstops. -//! -//! This is implemented as a bit set. There is a preallocation segment that -//! is used for almost all screen sizes. Then there is a dynamically allocated -//! segment if the screen is larger than the preallocation amount. -//! -//! In reality, tabstops don't need to be the most performant in any metric. -//! This implementation tries to balance denser memory usage (by using a bitset) -//! and minimizing unnecessary allocations. -const Tabstops = @This(); - -const std = @import("std"); -const Allocator = std.mem.Allocator; -const testing = std.testing; -const assert = std.debug.assert; -const fastmem = @import("../fastmem.zig"); - -/// Unit is the type we use per tabstop unit (see file docs). -const Unit = u8; -const unit_bits = @bitSizeOf(Unit); - -/// The number of columns we preallocate for. This is kind of high which -/// costs us some memory, but this is more columns than my 6k monitor at -/// 12-point font size, so this should prevent allocation in almost all -/// real world scenarios for the price of wasting at most -/// (columns / sizeOf(Unit)) bytes. -const prealloc_columns = 512; - -/// The number of entries we need for our preallocation. -const prealloc_count = prealloc_columns / unit_bits; - -/// We precompute all the possible masks since we never use a huge bit size. -const masks = blk: { - var res: [unit_bits]Unit = undefined; - for (res, 0..) |_, i| { - res[i] = @shlExact(@as(Unit, 1), @as(u3, @intCast(i))); - } - - break :blk res; -}; - -/// The number of columns this tabstop is set to manage. Use resize() -/// to change this number. -cols: usize = 0, - -/// Preallocated tab stops. -prealloc_stops: [prealloc_count]Unit = [1]Unit{0} ** prealloc_count, - -/// Dynamically expanded stops above prealloc stops. -dynamic_stops: []Unit = &[0]Unit{}, - -/// Returns the entry in the stops array that would contain this column. -inline fn entry(col: usize) usize { - return col / unit_bits; -} - -inline fn index(col: usize) usize { - return @mod(col, unit_bits); -} - -pub fn init(alloc: Allocator, cols: usize, interval: usize) !Tabstops { - var res: Tabstops = .{}; - try res.resize(alloc, cols); - res.reset(interval); - return res; -} - -pub fn deinit(self: *Tabstops, alloc: Allocator) void { - if (self.dynamic_stops.len > 0) alloc.free(self.dynamic_stops); - self.* = undefined; -} - -/// Set the tabstop at a certain column. The columns are 0-indexed. -pub fn set(self: *Tabstops, col: usize) void { - const i = entry(col); - const idx = index(col); - if (i < prealloc_count) { - self.prealloc_stops[i] |= masks[idx]; - return; - } - - const dynamic_i = i - prealloc_count; - assert(dynamic_i < self.dynamic_stops.len); - self.dynamic_stops[dynamic_i] |= masks[idx]; -} - -/// Unset the tabstop at a certain column. The columns are 0-indexed. -pub fn unset(self: *Tabstops, col: usize) void { - const i = entry(col); - const idx = index(col); - if (i < prealloc_count) { - self.prealloc_stops[i] ^= masks[idx]; - return; - } - - const dynamic_i = i - prealloc_count; - assert(dynamic_i < self.dynamic_stops.len); - self.dynamic_stops[dynamic_i] ^= masks[idx]; -} - -/// Get the value of a tabstop at a specific column. The columns are 0-indexed. -pub fn get(self: Tabstops, col: usize) bool { - const i = entry(col); - const idx = index(col); - const mask = masks[idx]; - const unit = if (i < prealloc_count) - self.prealloc_stops[i] - else unit: { - const dynamic_i = i - prealloc_count; - assert(dynamic_i < self.dynamic_stops.len); - break :unit self.dynamic_stops[dynamic_i]; - }; - - return unit & mask == mask; -} - -/// Resize this to support up to cols columns. -// TODO: needs interval to set new tabstops -pub fn resize(self: *Tabstops, alloc: Allocator, cols: usize) !void { - // Set our new value - self.cols = cols; - - // Do nothing if it fits. - if (cols <= prealloc_columns) return; - - // What we need in the dynamic size - const size = cols - prealloc_columns; - if (size < self.dynamic_stops.len) return; - - // Note: we can probably try to realloc here but I'm not sure it matters. - const new = try alloc.alloc(Unit, size); - if (self.dynamic_stops.len > 0) { - fastmem.copy(Unit, new, self.dynamic_stops); - alloc.free(self.dynamic_stops); - } - - self.dynamic_stops = new; -} - -/// Return the maximum number of columns this can support currently. -pub fn capacity(self: Tabstops) usize { - return (prealloc_count + self.dynamic_stops.len) * unit_bits; -} - -/// Unset all tabstops and then reset the initial tabstops to the given -/// interval. An interval of 0 sets no tabstops. -pub fn reset(self: *Tabstops, interval: usize) void { - @memset(&self.prealloc_stops, 0); - @memset(self.dynamic_stops, 0); - - if (interval > 0) { - var i: usize = interval; - while (i < self.cols - 1) : (i += interval) { - self.set(i); - } - } -} - -test "Tabstops: basic" { - var t: Tabstops = .{}; - defer t.deinit(testing.allocator); - try testing.expectEqual(@as(usize, 0), entry(4)); - try testing.expectEqual(@as(usize, 1), entry(8)); - try testing.expectEqual(@as(usize, 0), index(0)); - try testing.expectEqual(@as(usize, 1), index(1)); - try testing.expectEqual(@as(usize, 1), index(9)); - - try testing.expectEqual(@as(Unit, 0b00001000), masks[3]); - try testing.expectEqual(@as(Unit, 0b00010000), masks[4]); - - try testing.expect(!t.get(4)); - t.set(4); - try testing.expect(t.get(4)); - try testing.expect(!t.get(3)); - - t.reset(0); - try testing.expect(!t.get(4)); - - t.set(4); - try testing.expect(t.get(4)); - t.unset(4); - try testing.expect(!t.get(4)); -} - -test "Tabstops: dynamic allocations" { - var t: Tabstops = .{}; - defer t.deinit(testing.allocator); - - // Grow the capacity by 2. - const cap = t.capacity(); - try t.resize(testing.allocator, cap * 2); - - // Set something that was out of range of the first - t.set(cap + 5); - try testing.expect(t.get(cap + 5)); - try testing.expect(!t.get(cap + 4)); - - // Prealloc still works - try testing.expect(!t.get(5)); -} - -test "Tabstops: interval" { - var t: Tabstops = try init(testing.allocator, 80, 4); - defer t.deinit(testing.allocator); - try testing.expect(!t.get(0)); - try testing.expect(t.get(4)); - try testing.expect(!t.get(5)); - try testing.expect(t.get(8)); -} - -test "Tabstops: count on 80" { - // https://superuser.com/questions/710019/why-there-are-11-tabstops-on-a-80-column-console - - var t: Tabstops = try init(testing.allocator, 80, 8); - defer t.deinit(testing.allocator); - - // Count the tabstops - const count: usize = count: { - var v: usize = 0; - var i: usize = 0; - while (i < 80) : (i += 1) { - if (t.get(i)) { - v += 1; - } - } - - break :count v; - }; - - try testing.expectEqual(@as(usize, 9), count); -} diff --git a/src/terminal-old/Terminal.zig b/src/terminal-old/Terminal.zig deleted file mode 100644 index 5ff2591cb..000000000 --- a/src/terminal-old/Terminal.zig +++ /dev/null @@ -1,7632 +0,0 @@ -//! The primary terminal emulation structure. This represents a single -//! -//! "terminal" containing a grid of characters and exposes various operations -//! on that grid. This also maintains the scrollback buffer. -const Terminal = @This(); - -const std = @import("std"); -const builtin = @import("builtin"); -const testing = std.testing; -const assert = std.debug.assert; -const Allocator = std.mem.Allocator; -const simd = @import("../simd/main.zig"); -const unicode = @import("../unicode/main.zig"); - -const ansi = @import("ansi.zig"); -const modes = @import("modes.zig"); -const charsets = @import("charsets.zig"); -const csi = @import("csi.zig"); -const kitty = @import("kitty.zig"); -const sgr = @import("sgr.zig"); -const Tabstops = @import("Tabstops.zig"); -const color = @import("color.zig"); -const Screen = @import("Screen.zig"); -const mouse_shape = @import("mouse_shape.zig"); - -const log = std.log.scoped(.terminal); - -/// Default tabstop interval -const TABSTOP_INTERVAL = 8; - -/// Screen type is an enum that tracks whether a screen is primary or alternate. -pub const ScreenType = enum { - primary, - alternate, -}; - -/// The semantic prompt type. This is used when tracking a line type and -/// requires integration with the shell. By default, we mark a line as "none" -/// meaning we don't know what type it is. -/// -/// See: https://gitlab.freedesktop.org/Per_Bothner/specifications/blob/master/proposals/semantic-prompts.md -pub const SemanticPrompt = enum { - prompt, - prompt_continuation, - input, - command, -}; - -/// Screen is the current screen state. The "active_screen" field says what -/// the current screen is. The backup screen is the opposite of the active -/// screen. -active_screen: ScreenType, -screen: Screen, -secondary_screen: Screen, - -/// Whether we're currently writing to the status line (DECSASD and DECSSDT). -/// We don't support a status line currently so we just black hole this -/// data so that it doesn't mess up our main display. -status_display: ansi.StatusDisplay = .main, - -/// Where the tabstops are. -tabstops: Tabstops, - -/// The size of the terminal. -rows: usize, -cols: usize, - -/// The size of the screen in pixels. This is used for pty events and images -width_px: u32 = 0, -height_px: u32 = 0, - -/// The current scrolling region. -scrolling_region: ScrollingRegion, - -/// The last reported pwd, if any. -pwd: std.ArrayList(u8), - -/// The default color palette. This is only modified by changing the config file -/// and is used to reset the palette when receiving an OSC 104 command. -default_palette: color.Palette = color.default, - -/// The color palette to use. The mask indicates which palette indices have been -/// modified with OSC 4 -color_palette: struct { - const Mask = std.StaticBitSet(@typeInfo(color.Palette).Array.len); - colors: color.Palette = color.default, - mask: Mask = Mask.initEmpty(), -} = .{}, - -/// The previous printed character. This is used for the repeat previous -/// char CSI (ESC [ b). -previous_char: ?u21 = null, - -/// The modes that this terminal currently has active. -modes: modes.ModeState = .{}, - -/// The most recently set mouse shape for the terminal. -mouse_shape: mouse_shape.MouseShape = .text, - -/// 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, - - // 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, - - /// Set via the XTSHIFTESCAPE sequence. If true (XTSHIFTESCAPE = 1) - /// then we want to capture the shift key for the mouse protocol - /// if the configuration allows it. - mouse_shift_capture: enum { null, false, true } = .null, -} = .{}, - -/// The event types that can be reported for mouse-related activities. -/// These are all mutually exclusive (hence in a single enum). -pub const MouseEvents = enum(u3) { - none = 0, - x10 = 1, // 9 - normal = 2, // 1000 - button = 3, // 1002 - any = 4, // 1003 - - /// Returns true if this event sends motion events. - pub fn motion(self: MouseEvents) bool { - return self == .button or self == .any; - } -}; - -/// The format of mouse events when enabled. -/// These are all mutually exclusive (hence in a single enum). -pub const MouseFormat = enum(u3) { - x10 = 0, - utf8 = 1, // 1005 - sgr = 2, // 1006 - urxvt = 3, // 1015 - sgr_pixels = 4, // 1016 -}; - -/// Scrolling region is the area of the screen designated where scrolling -/// occurs. When scrolling the screen, only this viewport is scrolled. -pub const ScrollingRegion = struct { - // Top and bottom of the scroll region (0-indexed) - // Precondition: top < bottom - top: usize, - bottom: usize, - - // Left/right scroll regions. - // Precondition: right > left - // Precondition: right <= cols - 1 - left: usize, - right: usize, -}; - -/// Initialize a new terminal. -pub fn init(alloc: Allocator, cols: usize, rows: usize) !Terminal { - return Terminal{ - .cols = cols, - .rows = rows, - .active_screen = .primary, - // TODO: configurable scrollback - .screen = try Screen.init(alloc, rows, cols, 10000), - // No scrollback for the alternate screen - .secondary_screen = try Screen.init(alloc, rows, cols, 0), - .tabstops = try Tabstops.init(alloc, cols, TABSTOP_INTERVAL), - .scrolling_region = .{ - .top = 0, - .bottom = rows - 1, - .left = 0, - .right = cols - 1, - }, - .pwd = std.ArrayList(u8).init(alloc), - }; -} - -pub fn deinit(self: *Terminal, alloc: Allocator) void { - self.tabstops.deinit(alloc); - self.screen.deinit(); - self.secondary_screen.deinit(); - self.pwd.deinit(); - self.* = undefined; -} - -/// Options for switching to the alternate screen. -pub const AlternateScreenOptions = struct { - cursor_save: bool = false, - clear_on_enter: bool = false, - clear_on_exit: bool = false, -}; - -/// Switch to the alternate screen buffer. -/// -/// The alternate screen buffer: -/// * has its own grid -/// * has its own cursor state (included saved cursor) -/// * does not support scrollback -/// -pub fn alternateScreen( - self: *Terminal, - alloc: Allocator, - options: AlternateScreenOptions, -) void { - //log.info("alt screen active={} options={} cursor={}", .{ self.active_screen, options, self.screen.cursor }); - - // TODO: test - // TODO(mitchellh): what happens if we enter alternate screen multiple times? - // for now, we ignore... - if (self.active_screen == .alternate) return; - - // If we requested cursor save, we save the cursor in the primary screen - if (options.cursor_save) self.saveCursor(); - - // Switch the screens - const old = self.screen; - self.screen = self.secondary_screen; - self.secondary_screen = old; - self.active_screen = .alternate; - - // Bring our pen with us - self.screen.cursor = old.cursor; - - // Bring our charset state with us - self.screen.charset = old.charset; - - // Clear our selection - self.screen.selection = null; - - // Mark kitty images as dirty so they redraw - self.screen.kitty_images.dirty = true; - - if (options.clear_on_enter) { - self.eraseDisplay(alloc, .complete, false); - } -} - -/// Switch back to the primary screen (reset alternate screen mode). -pub fn primaryScreen( - self: *Terminal, - alloc: Allocator, - options: AlternateScreenOptions, -) void { - //log.info("primary screen active={} options={}", .{ self.active_screen, options }); - - // TODO: test - // TODO(mitchellh): what happens if we enter alternate screen multiple times? - if (self.active_screen == .primary) return; - - if (options.clear_on_exit) self.eraseDisplay(alloc, .complete, false); - - // Switch the screens - const old = self.screen; - self.screen = self.secondary_screen; - self.secondary_screen = old; - self.active_screen = .primary; - - // Clear our selection - self.screen.selection = null; - - // Mark kitty images as dirty so they redraw - self.screen.kitty_images.dirty = true; - - // Restore the cursor from the primary screen - if (options.cursor_save) self.restoreCursor(); -} - -/// The modes for DECCOLM. -pub const DeccolmMode = enum(u1) { - @"80_cols" = 0, - @"132_cols" = 1, -}; - -/// DECCOLM changes the terminal width between 80 and 132 columns. This -/// function call will do NOTHING unless `setDeccolmSupported` has been -/// called with "true". -/// -/// This breaks the expectation around modern terminals that they resize -/// with the window. This will fix the grid at either 80 or 132 columns. -/// The rows will continue to be variable. -pub fn deccolm(self: *Terminal, alloc: Allocator, mode: DeccolmMode) !void { - // If DEC mode 40 isn't enabled, then this is ignored. We also make - // sure that we don't have deccolm set because we want to fully ignore - // set mode. - if (!self.modes.get(.enable_mode_3)) { - self.modes.set(.@"132_column", false); - return; - } - - // Enable it - self.modes.set(.@"132_column", mode == .@"132_cols"); - - // Resize to the requested size - try self.resize( - alloc, - switch (mode) { - .@"132_cols" => 132, - .@"80_cols" => 80, - }, - self.rows, - ); - - // Erase our display and move our cursor. - self.eraseDisplay(alloc, .complete, false); - self.setCursorPos(1, 1); -} - -/// Resize the underlying terminal. -pub fn resize(self: *Terminal, alloc: Allocator, cols: usize, rows: usize) !void { - // If our cols/rows didn't change then we're done - if (self.cols == cols and self.rows == rows) return; - - // Resize our tabstops - // TODO: use resize, but it doesn't set new tabstops - if (self.cols != cols) { - self.tabstops.deinit(alloc); - self.tabstops = try Tabstops.init(alloc, cols, 8); - } - - // If we're making the screen smaller, dealloc the unused items. - if (self.active_screen == .primary) { - self.clearPromptForResize(); - if (self.modes.get(.wraparound)) { - try self.screen.resize(rows, cols); - } else { - try self.screen.resizeWithoutReflow(rows, cols); - } - try self.secondary_screen.resizeWithoutReflow(rows, cols); - } else { - try self.screen.resizeWithoutReflow(rows, cols); - if (self.modes.get(.wraparound)) { - try self.secondary_screen.resize(rows, cols); - } else { - try self.secondary_screen.resizeWithoutReflow(rows, cols); - } - } - - // Set our size - self.cols = cols; - self.rows = rows; - - // Reset the scrolling region - self.scrolling_region = .{ - .top = 0, - .bottom = rows - 1, - .left = 0, - .right = cols - 1, - }; -} - -/// 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.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 - // we are not at a prompt and we can exit this function. - const prompt_y: usize = prompt_y: { - // Keep track of the found value, because we want to find the START - var found: ?usize = null; - - // Search from the cursor up - var y: usize = 0; - while (y <= self.screen.cursor.y) : (y += 1) { - const real_y = self.screen.cursor.y - y; - const row = self.screen.getRow(.{ .active = real_y }); - switch (row.getSemanticPrompt()) { - // We are at a prompt but we're not at the start of the prompt. - // We mark our found value and continue because the prompt - // may be multi-line. - .input => found = real_y, - - // If we find the prompt then we're done. We are also done - // if we find any prompt continuation, because the shells - // that send this currently (zsh) cannot redraw every line. - .prompt, .prompt_continuation => { - found = real_y; - break; - }, - - // If we have command output, then we're most certainly not - // at a prompt. Break out of the loop. - .command => break, - - // If we don't know, we keep searching. - .unknown => {}, - } - } - - if (found) |found_y| break :prompt_y found_y; - return; - }; - assert(prompt_y < self.rows); - - // We want to clear all the lines from prompt_y downwards because - // the shell will redraw the prompt. - for (prompt_y..self.rows) |y| { - const row = self.screen.getRow(.{ .active = y }); - row.setWrapped(false); - row.setDirty(true); - row.clear(.{}); - } -} - -/// Return the current string value of the terminal. Newlines are -/// encoded as "\n". This omits any formatting such as fg/bg. -/// -/// The caller must free the string. -pub fn plainString(self: *Terminal, alloc: Allocator) ![]const u8 { - return try self.screen.testString(alloc, .viewport); -} - -/// Save cursor position and further state. -/// -/// The primary and alternate screen have distinct save state. One saved state -/// is kept per screen (main / alternative). If for the current screen state -/// was already saved it is overwritten. -pub fn saveCursor(self: *Terminal) void { - self.screen.saved_cursor = .{ - .x = self.screen.cursor.x, - .y = self.screen.cursor.y, - .pen = self.screen.cursor.pen, - .pending_wrap = self.screen.cursor.pending_wrap, - .origin = self.modes.get(.origin), - .charset = self.screen.charset, - }; -} - -/// Restore cursor position and other state. -/// -/// The primary and alternate screen have distinct save state. -/// If no save was done before values are reset to their initial values. -pub fn restoreCursor(self: *Terminal) void { - const saved: Screen.Cursor.Saved = self.screen.saved_cursor orelse .{ - .x = 0, - .y = 0, - .pen = .{}, - .pending_wrap = false, - .origin = false, - .charset = .{}, - }; - - self.screen.cursor.pen = saved.pen; - self.screen.charset = saved.charset; - self.modes.set(.origin, saved.origin); - self.screen.cursor.x = @min(saved.x, self.cols - 1); - self.screen.cursor.y = @min(saved.y, self.rows - 1); - self.screen.cursor.pending_wrap = saved.pending_wrap; -} - -/// TODO: test -pub fn setAttribute(self: *Terminal, attr: sgr.Attribute) !void { - switch (attr) { - .unset => { - self.screen.cursor.pen.fg = .none; - self.screen.cursor.pen.bg = .none; - self.screen.cursor.pen.attrs = .{}; - }, - - .bold => { - self.screen.cursor.pen.attrs.bold = true; - }, - - .reset_bold => { - // Bold and faint share the same SGR code for this - self.screen.cursor.pen.attrs.bold = false; - self.screen.cursor.pen.attrs.faint = false; - }, - - .italic => { - self.screen.cursor.pen.attrs.italic = true; - }, - - .reset_italic => { - self.screen.cursor.pen.attrs.italic = false; - }, - - .faint => { - self.screen.cursor.pen.attrs.faint = true; - }, - - .underline => |v| { - self.screen.cursor.pen.attrs.underline = v; - }, - - .reset_underline => { - self.screen.cursor.pen.attrs.underline = .none; - }, - - .underline_color => |rgb| { - self.screen.cursor.pen.attrs.underline_color = true; - self.screen.cursor.pen.underline_fg = .{ - .r = rgb.r, - .g = rgb.g, - .b = rgb.b, - }; - }, - - .@"256_underline_color" => |idx| { - self.screen.cursor.pen.attrs.underline_color = true; - self.screen.cursor.pen.underline_fg = self.color_palette.colors[idx]; - }, - - .reset_underline_color => { - self.screen.cursor.pen.attrs.underline_color = false; - }, - - .blink => { - log.warn("blink requested, but not implemented", .{}); - self.screen.cursor.pen.attrs.blink = true; - }, - - .reset_blink => { - self.screen.cursor.pen.attrs.blink = false; - }, - - .inverse => { - self.screen.cursor.pen.attrs.inverse = true; - }, - - .reset_inverse => { - self.screen.cursor.pen.attrs.inverse = false; - }, - - .invisible => { - self.screen.cursor.pen.attrs.invisible = true; - }, - - .reset_invisible => { - self.screen.cursor.pen.attrs.invisible = false; - }, - - .strikethrough => { - self.screen.cursor.pen.attrs.strikethrough = true; - }, - - .reset_strikethrough => { - self.screen.cursor.pen.attrs.strikethrough = false; - }, - - .direct_color_fg => |rgb| { - self.screen.cursor.pen.fg = .{ - .rgb = .{ - .r = rgb.r, - .g = rgb.g, - .b = rgb.b, - }, - }; - }, - - .direct_color_bg => |rgb| { - self.screen.cursor.pen.bg = .{ - .rgb = .{ - .r = rgb.r, - .g = rgb.g, - .b = rgb.b, - }, - }; - }, - - .@"8_fg" => |n| { - self.screen.cursor.pen.fg = .{ .indexed = @intFromEnum(n) }; - }, - - .@"8_bg" => |n| { - self.screen.cursor.pen.bg = .{ .indexed = @intFromEnum(n) }; - }, - - .reset_fg => self.screen.cursor.pen.fg = .none, - - .reset_bg => self.screen.cursor.pen.bg = .none, - - .@"8_bright_fg" => |n| { - self.screen.cursor.pen.fg = .{ .indexed = @intFromEnum(n) }; - }, - - .@"8_bright_bg" => |n| { - self.screen.cursor.pen.bg = .{ .indexed = @intFromEnum(n) }; - }, - - .@"256_fg" => |idx| { - self.screen.cursor.pen.fg = .{ .indexed = idx }; - }, - - .@"256_bg" => |idx| { - self.screen.cursor.pen.bg = .{ .indexed = idx }; - }, - - .unknown => return error.InvalidAttribute, - } -} - -/// Print the active attributes as a string. This is used to respond to DECRQSS -/// requests. -/// -/// Boolean attributes are printed first, followed by foreground color, then -/// background color. Each attribute is separated by a semicolon. -pub fn printAttributes(self: *Terminal, buf: []u8) ![]const u8 { - var stream = std.io.fixedBufferStream(buf); - const writer = stream.writer(); - - // The SGR response always starts with a 0. See https://vt100.net/docs/vt510-rm/DECRPSS - try writer.writeByte('0'); - - const pen = self.screen.cursor.pen; - var attrs = [_]u8{0} ** 8; - var i: usize = 0; - - if (pen.attrs.bold) { - attrs[i] = '1'; - i += 1; - } - - if (pen.attrs.faint) { - attrs[i] = '2'; - i += 1; - } - - if (pen.attrs.italic) { - attrs[i] = '3'; - i += 1; - } - - if (pen.attrs.underline != .none) { - attrs[i] = '4'; - i += 1; - } - - if (pen.attrs.blink) { - attrs[i] = '5'; - i += 1; - } - - if (pen.attrs.inverse) { - attrs[i] = '7'; - i += 1; - } - - if (pen.attrs.invisible) { - attrs[i] = '8'; - i += 1; - } - - if (pen.attrs.strikethrough) { - attrs[i] = '9'; - i += 1; - } - - for (attrs[0..i]) |c| { - try writer.print(";{c}", .{c}); - } - - switch (pen.fg) { - .none => {}, - .indexed => |idx| if (idx >= 16) - try writer.print(";38:5:{}", .{idx}) - else if (idx >= 8) - try writer.print(";9{}", .{idx - 8}) - else - try writer.print(";3{}", .{idx}), - .rgb => |rgb| try writer.print(";38:2::{[r]}:{[g]}:{[b]}", rgb), - } - - switch (pen.bg) { - .none => {}, - .indexed => |idx| if (idx >= 16) - try writer.print(";48:5:{}", .{idx}) - else if (idx >= 8) - try writer.print(";10{}", .{idx - 8}) - else - try writer.print(";4{}", .{idx}), - .rgb => |rgb| try writer.print(";48:2::{[r]}:{[g]}:{[b]}", rgb), - } - - return stream.getWritten(); -} - -/// Set the charset into the given slot. -pub fn configureCharset(self: *Terminal, slot: charsets.Slots, set: charsets.Charset) void { - self.screen.charset.charsets.set(slot, set); -} - -/// Invoke the charset in slot into the active slot. If single is true, -/// then this will only be invoked for a single character. -pub fn invokeCharset( - self: *Terminal, - active: charsets.ActiveSlot, - slot: charsets.Slots, - single: bool, -) void { - if (single) { - assert(active == .GL); - self.screen.charset.single_shift = slot; - return; - } - - switch (active) { - .GL => self.screen.charset.gl = slot, - .GR => self.screen.charset.gr = slot, - } -} - -/// Print UTF-8 encoded string to the terminal. -pub fn printString(self: *Terminal, str: []const u8) !void { - const view = try std.unicode.Utf8View.init(str); - var it = view.iterator(); - while (it.nextCodepoint()) |cp| { - switch (cp) { - '\n' => { - self.carriageReturn(); - try self.linefeed(); - }, - - else => try self.print(cp), - } - } -} - -pub fn print(self: *Terminal, c: u21) !void { - // log.debug("print={x} y={} x={}", .{ c, self.screen.cursor.y, self.screen.cursor.x }); - - // If we're not on the main display, do nothing for now - if (self.status_display != .main) return; - - // Our right margin depends where our cursor is now. - const right_limit = if (self.screen.cursor.x > self.scrolling_region.right) - self.cols - else - self.scrolling_region.right + 1; - - // Perform grapheme clustering if grapheme support is enabled (mode 2027). - // This is MUCH slower than the normal path so the conditional below is - // purposely ordered in least-likely to most-likely so we can drop out - // as quickly as possible. - if (c > 255 and self.modes.get(.grapheme_cluster) and self.screen.cursor.x > 0) grapheme: { - const row = self.screen.getRow(.{ .active = self.screen.cursor.y }); - - // We need the previous cell to determine if we're at a grapheme - // break or not. If we are NOT, then we are still combining the - // same grapheme. Otherwise, we can stay in this cell. - const Prev = struct { cell: *Screen.Cell, x: usize }; - const prev: Prev = prev: { - const x = x: { - // If we have wraparound, then we always use the prev col - if (self.modes.get(.wraparound)) break :x self.screen.cursor.x - 1; - - // If we do not have wraparound, the logic is trickier. If - // we're not on the last column, then we just use the previous - // column. Otherwise, we need to check if there is text to - // figure out if we're attaching to the prev or current. - if (self.screen.cursor.x != right_limit - 1) break :x self.screen.cursor.x - 1; - const current = row.getCellPtr(self.screen.cursor.x); - break :x self.screen.cursor.x - @intFromBool(current.char == 0); - }; - const immediate = row.getCellPtr(x); - - // If the previous cell is a wide spacer tail, then we actually - // want to use the cell before that because that has the actual - // content. - if (!immediate.attrs.wide_spacer_tail) break :prev .{ - .cell = immediate, - .x = x, - }; - - break :prev .{ - .cell = row.getCellPtr(x - 1), - .x = x - 1, - }; - }; - - // If our cell has no content, then this is a new cell and - // necessarily a grapheme break. - if (prev.cell.char == 0) break :grapheme; - - const grapheme_break = brk: { - var state: unicode.GraphemeBreakState = .{}; - var cp1: u21 = @intCast(prev.cell.char); - if (prev.cell.attrs.grapheme) { - var it = row.codepointIterator(prev.x); - while (it.next()) |cp2| { - // log.debug("cp1={x} cp2={x}", .{ cp1, cp2 }); - assert(!unicode.graphemeBreak(cp1, cp2, &state)); - cp1 = cp2; - } - } - - // log.debug("cp1={x} cp2={x} end", .{ cp1, c }); - break :brk unicode.graphemeBreak(cp1, c, &state); - }; - - // If we can NOT break, this means that "c" is part of a grapheme - // with the previous char. - if (!grapheme_break) { - // If this is an emoji variation selector then we need to modify - // the cell width accordingly. VS16 makes the character wide and - // VS15 makes it narrow. - if (c == 0xFE0F or c == 0xFE0E) { - // This only applies to emoji - const prev_props = unicode.getProperties(@intCast(prev.cell.char)); - const emoji = prev_props.grapheme_boundary_class == .extended_pictographic; - if (!emoji) return; - - switch (c) { - 0xFE0F => wide: { - if (prev.cell.attrs.wide) break :wide; - - // Move our cursor back to the previous. We'll move - // the cursor within this block to the proper location. - self.screen.cursor.x = prev.x; - - // If we don't have space for the wide char, we need - // to insert spacers and wrap. Then we just print the wide - // char as normal. - if (prev.x == right_limit - 1) { - if (!self.modes.get(.wraparound)) return; - const spacer_head = self.printCell(' '); - spacer_head.attrs.wide_spacer_head = true; - try self.printWrap(); - } - - const wide_cell = self.printCell(@intCast(prev.cell.char)); - wide_cell.attrs.wide = true; - - // Write our spacer - self.screen.cursor.x += 1; - const spacer = self.printCell(' '); - spacer.attrs.wide_spacer_tail = true; - - // Move the cursor again so we're beyond our spacer - self.screen.cursor.x += 1; - if (self.screen.cursor.x == right_limit) { - self.screen.cursor.x -= 1; - self.screen.cursor.pending_wrap = true; - } - }, - - 0xFE0E => narrow: { - // Prev cell is no longer wide - if (!prev.cell.attrs.wide) break :narrow; - prev.cell.attrs.wide = false; - - // Remove the wide spacer tail - const cell = row.getCellPtr(prev.x + 1); - cell.attrs.wide_spacer_tail = false; - - break :narrow; - }, - - else => unreachable, - } - } - - log.debug("c={x} grapheme attach to x={}", .{ c, prev.x }); - try row.attachGrapheme(prev.x, c); - return; - } - } - - // Determine the width of this character so we can handle - // non-single-width characters properly. We have a fast-path for - // byte-sized characters since they're so common. We can ignore - // control characters because they're always filtered prior. - const width: usize = if (c <= 0xFF) 1 else @intCast(unicode.table.get(c).width); - - // Note: it is possible to have a width of "3" and a width of "-1" - // from ziglyph. We should look into those cases and handle them - // appropriately. - assert(width <= 2); - // log.debug("c={x} width={}", .{ c, width }); - - // Attach zero-width characters to our cell as grapheme data. - if (width == 0) { - // If we have grapheme clustering enabled, we don't blindly attach - // any zero width character to our cells and we instead just ignore - // it. - if (self.modes.get(.grapheme_cluster)) return; - - // If we're at cell zero, then this is malformed data and we don't - // print anything or even store this. Zero-width characters are ALWAYS - // attached to some other non-zero-width character at the time of - // writing. - if (self.screen.cursor.x == 0) { - log.warn("zero-width character with no prior character, ignoring", .{}); - return; - } - - // Find our previous cell - const row = self.screen.getRow(.{ .active = self.screen.cursor.y }); - const prev: usize = prev: { - const x = self.screen.cursor.x - 1; - const immediate = row.getCellPtr(x); - if (!immediate.attrs.wide_spacer_tail) break :prev x; - break :prev x - 1; - }; - - // If this is a emoji variation selector, prev must be an emoji - if (c == 0xFE0F or c == 0xFE0E) { - const prev_cell = row.getCellPtr(prev); - const prev_props = unicode.getProperties(@intCast(prev_cell.char)); - const emoji = prev_props.grapheme_boundary_class == .extended_pictographic; - if (!emoji) return; - } - - try row.attachGrapheme(prev, c); - return; - } - - // We have a printable character, save it - self.previous_char = c; - - // If we're soft-wrapping, then handle that first. - if (self.screen.cursor.pending_wrap and self.modes.get(.wraparound)) - 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.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 - 1 => _ = @call(.always_inline, printCell, .{ self, c }), - - // Wide character requires a spacer. We print this by - // using two cells: the first is flagged "wide" and has the - // wide char. The second is guaranteed to be a spacer if - // we're not at the end of the line. - 2 => if ((right_limit - self.scrolling_region.left) > 1) { - // If we don't have space for the wide char, we need - // to insert spacers and wrap. Then we just print the wide - // char as normal. - if (self.screen.cursor.x == right_limit - 1) { - // If we don't have wraparound enabled then we don't print - // this character at all and don't move the cursor. This is - // how xterm behaves. - if (!self.modes.get(.wraparound)) return; - - const spacer_head = self.printCell(' '); - spacer_head.attrs.wide_spacer_head = true; - try self.printWrap(); - } - - const wide_cell = self.printCell(c); - wide_cell.attrs.wide = true; - - // Write our spacer - self.screen.cursor.x += 1; - const spacer = self.printCell(' '); - spacer.attrs.wide_spacer_tail = true; - } else { - // This is pretty broken, terminals should never be only 1-wide. - // We sould prevent this downstream. - _ = self.printCell(' '); - }, - - else => unreachable, - } - - // Move the cursor - self.screen.cursor.x += 1; - - // If we're at the column limit, then we need to wrap the next time. - // This is unlikely so we do the increment above and decrement here - // if we need to rather than check once. - if (self.screen.cursor.x == right_limit) { - self.screen.cursor.x -= 1; - self.screen.cursor.pending_wrap = true; - } -} - -fn printCell(self: *Terminal, unmapped_c: u21) *Screen.Cell { - const c: u21 = c: { - // TODO: non-utf8 handling, gr - - // If we're single shifting, then we use the key exactly once. - const key = if (self.screen.charset.single_shift) |key_once| blk: { - self.screen.charset.single_shift = null; - break :blk key_once; - } else self.screen.charset.gl; - const set = self.screen.charset.charsets.get(key); - - // UTF-8 or ASCII is used as-is - if (set == .utf8 or set == .ascii) break :c unmapped_c; - - // If we're outside of ASCII range this is an invalid value in - // this table so we just return space. - if (unmapped_c > std.math.maxInt(u8)) break :c ' '; - - // Get our lookup table and map it - const table = set.table(); - break :c @intCast(table[@intCast(unmapped_c)]); - }; - - const row = self.screen.getRow(.{ .active = self.screen.cursor.y }); - const cell = row.getCellPtr(self.screen.cursor.x); - - // If this cell is wide char then we need to clear it. - // We ignore wide spacer HEADS because we can just write - // single-width characters into that. - if (cell.attrs.wide) { - const x = self.screen.cursor.x + 1; - if (x < self.cols) { - const spacer_cell = row.getCellPtr(x); - spacer_cell.* = self.screen.cursor.pen; - } - - if (self.screen.cursor.y > 0 and self.screen.cursor.x <= 1) { - self.clearWideSpacerHead(); - } - } else if (cell.attrs.wide_spacer_tail) { - assert(self.screen.cursor.x > 0); - const x = self.screen.cursor.x - 1; - - const wide_cell = row.getCellPtr(x); - wide_cell.* = self.screen.cursor.pen; - - if (self.screen.cursor.y > 0 and self.screen.cursor.x <= 1) { - self.clearWideSpacerHead(); - } - } - - // If the prior value had graphemes, clear those - if (cell.attrs.grapheme) row.clearGraphemes(self.screen.cursor.x); - - // Write - cell.* = self.screen.cursor.pen; - cell.char = @intCast(c); - return cell; -} - -fn printWrap(self: *Terminal) !void { - const row = self.screen.getRow(.{ .active = self.screen.cursor.y }); - row.setWrapped(true); - - // Get the old semantic prompt so we can extend it to the next - // line. We need to do this before we index() because we may - // modify memory. - const old_prompt = row.getSemanticPrompt(); - - // Move to the next line - try self.index(); - self.screen.cursor.x = self.scrolling_region.left; - - // New line must inherit semantic prompt of the old line - const new_row = self.screen.getRow(.{ .active = self.screen.cursor.y }); - new_row.setSemanticPrompt(old_prompt); -} - -fn clearWideSpacerHead(self: *Terminal) void { - // TODO: handle deleting wide char on row 0 of active - assert(self.screen.cursor.y >= 1); - const cell = self.screen.getCellPtr( - .active, - self.screen.cursor.y - 1, - self.cols - 1, - ); - cell.attrs.wide_spacer_head = false; -} - -/// Print the previous printed character a repeated amount of times. -pub fn printRepeat(self: *Terminal, count_req: usize) !void { - if (self.previous_char) |c| { - const count = @max(count_req, 1); - for (0..count) |_| try self.print(c); - } -} - -/// Resets all margins and fills the whole screen with the character 'E' -/// -/// Sets the cursor to the top left corner. -pub fn decaln(self: *Terminal) !void { - // Reset margins, also sets cursor to top-left - self.scrolling_region = .{ - .top = 0, - .bottom = self.rows - 1, - .left = 0, - .right = self.cols - 1, - }; - - // Origin mode is disabled - self.modes.set(.origin, false); - - // Move our cursor to the top-left - self.setCursorPos(1, 1); - - // Clear our stylistic attributes - self.screen.cursor.pen = .{ - .bg = self.screen.cursor.pen.bg, - .fg = self.screen.cursor.pen.fg, - .attrs = .{ - .protected = self.screen.cursor.pen.attrs.protected, - }, - }; - - // Our pen has the letter E - const pen: Screen.Cell = .{ .char = 'E' }; - - // Fill with Es, does not move cursor. - for (0..self.rows) |y| { - const filled = self.screen.getRow(.{ .active = y }); - filled.fill(pen); - } -} - -/// Move the cursor to the next line in the scrolling region, possibly scrolling. -/// -/// If the cursor is outside of the scrolling region: move the cursor one line -/// down if it is not on the bottom-most line of the screen. -/// -/// If the cursor is inside the scrolling region: -/// If the cursor is on the bottom-most line of the scrolling region: -/// invoke scroll up with amount=1 -/// If the cursor is not on the bottom-most line of the scrolling region: -/// move the cursor one line down -/// -/// This unsets the pending wrap state without wrapping. -pub fn index(self: *Terminal) !void { - // Unset pending wrap state - self.screen.cursor.pending_wrap = false; - - // Outside of the scroll region we move the cursor one line down. - if (self.screen.cursor.y < self.scrolling_region.top or - self.screen.cursor.y > self.scrolling_region.bottom) - { - self.screen.cursor.y = @min(self.screen.cursor.y + 1, self.rows - 1); - return; - } - - // If the cursor is inside the scrolling region and on the bottom-most - // line, then we scroll up. If our scrolling region is the full screen - // we create scrollback. - if (self.screen.cursor.y == self.scrolling_region.bottom and - self.screen.cursor.x >= self.scrolling_region.left and - self.screen.cursor.x <= self.scrolling_region.right) - { - // If our scrolling region is the full screen, we create scrollback. - // Otherwise, we simply scroll the region. - if (self.scrolling_region.top == 0 and - self.scrolling_region.bottom == self.rows - 1 and - self.scrolling_region.left == 0 and - self.scrolling_region.right == self.cols - 1) - { - try self.screen.scroll(.{ .screen = 1 }); - } else { - try self.scrollUp(1); - } - - return; - } - - // Increase cursor by 1, maximum to bottom of scroll region - self.screen.cursor.y = @min(self.screen.cursor.y + 1, self.scrolling_region.bottom); -} - -/// Move the cursor to the previous line in the scrolling region, possibly -/// scrolling. -/// -/// If the cursor is outside of the scrolling region, move the cursor one -/// line up if it is not on the top-most line of the screen. -/// -/// If the cursor is inside the scrolling region: -/// -/// * If the cursor is on the top-most line of the scrolling region: -/// invoke scroll down with amount=1 -/// * If the cursor is not on the top-most line of the scrolling region: -/// move the cursor one line up -pub fn reverseIndex(self: *Terminal) !void { - if (self.screen.cursor.y != self.scrolling_region.top or - self.screen.cursor.x < self.scrolling_region.left or - self.screen.cursor.x > self.scrolling_region.right) - { - self.cursorUp(1); - return; - } - - try self.scrollDown(1); -} - -// Set Cursor Position. Move cursor to the position indicated -// by row and column (1-indexed). If column is 0, it is adjusted to 1. -// If column is greater than the right-most column it is adjusted to -// the right-most column. If row is 0, it is adjusted to 1. If row is -// greater than the bottom-most row it is adjusted to the bottom-most -// row. -pub fn setCursorPos(self: *Terminal, row_req: usize, col_req: usize) void { - // If cursor origin mode is set the cursor row will be moved relative to - // the top margin row and adjusted to be above or at bottom-most row in - // the current scroll region. - // - // If origin mode is set and left and right margin mode is set the cursor - // will be moved relative to the left margin column and adjusted to be on - // or left of the right margin column. - const params: struct { - x_offset: usize = 0, - y_offset: usize = 0, - x_max: usize, - y_max: usize, - } = if (self.modes.get(.origin)) .{ - .x_offset = self.scrolling_region.left, - .y_offset = self.scrolling_region.top, - .x_max = self.scrolling_region.right + 1, // We need this 1-indexed - .y_max = self.scrolling_region.bottom + 1, // We need this 1-indexed - } else .{ - .x_max = self.cols, - .y_max = self.rows, - }; - - const row = if (row_req == 0) 1 else row_req; - const col = if (col_req == 0) 1 else col_req; - self.screen.cursor.x = @min(params.x_max, col + params.x_offset) -| 1; - self.screen.cursor.y = @min(params.y_max, row + params.y_offset) -| 1; - // log.info("set cursor position: col={} row={}", .{ self.screen.cursor.x, self.screen.cursor.y }); - - // Unset pending wrap state - self.screen.cursor.pending_wrap = false; -} - -/// Erase the display. -pub fn eraseDisplay( - self: *Terminal, - alloc: Allocator, - mode: csi.EraseDisplay, - protected_req: bool, -) void { - // Erasing clears all attributes / colors _except_ the background - const pen: Screen.Cell = switch (self.screen.cursor.pen.bg) { - .none => .{}, - else => |bg| .{ .bg = bg }, - }; - - // We respect protected attributes if explicitly requested (probably - // a DECSEL sequence) or if our last protected mode was ISO even if its - // not currently set. - const protected = self.screen.protected_mode == .iso or protected_req; - - switch (mode) { - .scroll_complete => { - self.screen.scroll(.{ .clear = {} }) catch |err| { - log.warn("scroll clear failed, doing a normal clear err={}", .{err}); - self.eraseDisplay(alloc, .complete, protected_req); - return; - }; - - // Unsets pending wrap state - self.screen.cursor.pending_wrap = false; - - // Clear all Kitty graphics state for this screen - self.screen.kitty_images.delete(alloc, self, .{ .all = true }); - }, - - .complete => { - // If we're on the primary screen and our last non-empty row is - // a prompt, then we do a scroll_complete instead. This is a - // heuristic to get the generally desirable behavior that ^L - // at a prompt scrolls the screen contents prior to clearing. - // Most shells send `ESC [ H ESC [ 2 J` so we can't just check - // our current cursor position. See #905 - if (self.active_screen == .primary) at_prompt: { - // Go from the bottom of the viewport up and see if we're - // at a prompt. - const viewport_max = Screen.RowIndexTag.viewport.maxLen(&self.screen); - for (0..viewport_max) |y| { - const bottom_y = viewport_max - y - 1; - const row = self.screen.getRow(.{ .viewport = bottom_y }); - if (row.isEmpty()) continue; - switch (row.getSemanticPrompt()) { - // If we're at a prompt or input area, then we are at a prompt. - .prompt, - .prompt_continuation, - .input, - => break, - - // If we have command output, then we're most certainly not - // at a prompt. - .command => break :at_prompt, - - // If we don't know, we keep searching. - .unknown => {}, - } - } else break :at_prompt; - - self.screen.scroll(.{ .clear = {} }) catch { - // If we fail, we just fall back to doing a normal clear - // so we don't worry about the error. - }; - } - - var it = self.screen.rowIterator(.active); - while (it.next()) |row| { - row.setWrapped(false); - row.setDirty(true); - - if (!protected) { - row.clear(pen); - continue; - } - - // Protected mode erase - for (0..row.lenCells()) |x| { - const cell = row.getCellPtr(x); - if (cell.attrs.protected) continue; - cell.* = pen; - } - } - - // Unsets pending wrap state - self.screen.cursor.pending_wrap = false; - - // Clear all Kitty graphics state for this screen - self.screen.kitty_images.delete(alloc, self, .{ .all = true }); - }, - - .below => { - // All lines to the right (including the cursor) - { - self.eraseLine(.right, protected_req); - const row = self.screen.getRow(.{ .active = self.screen.cursor.y }); - row.setWrapped(false); - row.setDirty(true); - } - - // All lines below - for ((self.screen.cursor.y + 1)..self.rows) |y| { - const row = self.screen.getRow(.{ .active = y }); - row.setWrapped(false); - row.setDirty(true); - for (0..self.cols) |x| { - if (row.header().flags.grapheme) row.clearGraphemes(x); - const cell = row.getCellPtr(x); - if (protected and cell.attrs.protected) continue; - cell.* = pen; - cell.char = 0; - } - } - - // Unsets pending wrap state - self.screen.cursor.pending_wrap = false; - }, - - .above => { - // Erase to the left (including the cursor) - self.eraseLine(.left, protected_req); - - // All lines above - var y: usize = 0; - while (y < self.screen.cursor.y) : (y += 1) { - var x: usize = 0; - while (x < self.cols) : (x += 1) { - const cell = self.screen.getCellPtr(.active, y, x); - if (protected and cell.attrs.protected) continue; - cell.* = pen; - cell.char = 0; - } - } - - // Unsets pending wrap state - self.screen.cursor.pending_wrap = false; - }, - - .scrollback => self.screen.clear(.history) catch |err| { - // This isn't a huge issue, so just log it. - log.err("failed to clear scrollback: {}", .{err}); - }, - } -} - -/// Erase the line. -pub fn eraseLine( - self: *Terminal, - mode: csi.EraseLine, - protected_req: bool, -) void { - // We always fill with the background - const pen: Screen.Cell = switch (self.screen.cursor.pen.bg) { - .none => .{}, - else => |bg| .{ .bg = bg }, - }; - - // Get our start/end positions depending on mode. - const row = self.screen.getRow(.{ .active = self.screen.cursor.y }); - const start, const end = switch (mode) { - .right => right: { - var x = self.screen.cursor.x; - - // If our X is a wide spacer tail then we need to erase the - // previous cell too so we don't split a multi-cell character. - if (x > 0) { - const cell = row.getCellPtr(x); - if (cell.attrs.wide_spacer_tail) x -= 1; - } - - // This resets the soft-wrap of this line - row.setWrapped(false); - - break :right .{ x, row.lenCells() }; - }, - - .left => left: { - var x = self.screen.cursor.x; - - // If our x is a wide char we need to delete the tail too. - const cell = row.getCellPtr(x); - if (cell.attrs.wide) { - if (row.getCellPtr(x + 1).attrs.wide_spacer_tail) { - x += 1; - } - } - - break :left .{ 0, x + 1 }; - }, - - // Note that it seems like complete should reset the soft-wrap - // state of the line but in xterm it does not. - .complete => .{ 0, row.lenCells() }, - - else => { - log.err("unimplemented erase line mode: {}", .{mode}); - return; - }, - }; - - // All modes will clear the pending wrap state and we know we have - // a valid mode at this point. - self.screen.cursor.pending_wrap = false; - - // We respect protected attributes if explicitly requested (probably - // a DECSEL sequence) or if our last protected mode was ISO even if its - // not currently set. - const protected = self.screen.protected_mode == .iso or protected_req; - - // If we're not respecting protected attributes, we can use a fast-path - // to fill the entire line. - if (!protected) { - row.fillSlice(self.screen.cursor.pen, start, end); - return; - } - - for (start..end) |x| { - const cell = row.getCellPtr(x); - if (cell.attrs.protected) continue; - cell.* = pen; - } -} - -/// Removes amount characters from the current cursor position to the right. -/// The remaining characters are shifted to the left and space from the right -/// margin is filled with spaces. -/// -/// If amount is greater than the remaining number of characters in the -/// scrolling region, it is adjusted down. -/// -/// Does not change the cursor position. -pub fn deleteChars(self: *Terminal, count: usize) !void { - if (count == 0) return; - - // If our cursor is outside the margins then do nothing. We DO reset - // wrap state still so this must remain below the above logic. - if (self.screen.cursor.x < self.scrolling_region.left or - self.screen.cursor.x > self.scrolling_region.right) return; - - // This resets the pending wrap state - self.screen.cursor.pending_wrap = false; - - const pen: Screen.Cell = .{ - .bg = self.screen.cursor.pen.bg, - }; - - // If our X is a wide spacer tail then we need to erase the - // previous cell too so we don't split a multi-cell character. - const line = self.screen.getRow(.{ .active = self.screen.cursor.y }); - if (self.screen.cursor.x > 0) { - const cell = line.getCellPtr(self.screen.cursor.x); - if (cell.attrs.wide_spacer_tail) { - line.getCellPtr(self.screen.cursor.x - 1).* = pen; - } - } - - // We go from our cursor right to the end and either copy the cell - // "count" away or clear it. - for (self.screen.cursor.x..self.scrolling_region.right + 1) |x| { - const copy_x = x + count; - if (copy_x >= self.scrolling_region.right + 1) { - line.getCellPtr(x).* = pen; - continue; - } - - const copy_cell = line.getCellPtr(copy_x); - if (x == 0 and copy_cell.attrs.wide_spacer_tail) { - line.getCellPtr(x).* = pen; - continue; - } - line.getCellPtr(x).* = copy_cell.*; - copy_cell.char = 0; - } -} - -pub fn eraseChars(self: *Terminal, count_req: usize) void { - const count = @max(count_req, 1); - - // This resets the pending wrap state - self.screen.cursor.pending_wrap = false; - - // Our last index is at most the end of the number of chars we have - // in the current line. - const row = self.screen.getRow(.{ .active = self.screen.cursor.y }); - const end = end: { - var end = @min(self.cols, self.screen.cursor.x + count); - - // If our last cell is a wide char then we need to also clear the - // cell beyond it since we can't just split a wide char. - if (end != self.cols) { - const last = row.getCellPtr(end - 1); - if (last.attrs.wide) end += 1; - } - - break :end end; - }; - - // This resets the soft-wrap of this line - row.setWrapped(false); - - const pen: Screen.Cell = .{ - .bg = self.screen.cursor.pen.bg, - }; - - // If we never had a protection mode, then we can assume no cells - // are protected and go with the fast path. If the last protection - // mode was not ISO we also always ignore protection attributes. - if (self.screen.protected_mode != .iso) { - row.fillSlice(pen, self.screen.cursor.x, end); - } - - // We had a protection mode at some point. We must go through each - // cell and check its protection attribute. - for (self.screen.cursor.x..end) |x| { - const cell = row.getCellPtr(x); - if (cell.attrs.protected) continue; - cell.* = pen; - } -} - -/// Move the cursor to the left amount cells. If amount is 0, adjust it to 1. -pub fn cursorLeft(self: *Terminal, count_req: usize) void { - // Wrapping behavior depends on various terminal modes - const WrapMode = enum { none, reverse, reverse_extended }; - const wrap_mode: WrapMode = wrap_mode: { - if (!self.modes.get(.wraparound)) break :wrap_mode .none; - if (self.modes.get(.reverse_wrap_extended)) break :wrap_mode .reverse_extended; - if (self.modes.get(.reverse_wrap)) break :wrap_mode .reverse; - break :wrap_mode .none; - }; - - var count: usize = @max(count_req, 1); - - // If we are in no wrap mode, then we move the cursor left and exit - // since this is the fastest and most typical path. - if (wrap_mode == .none) { - self.screen.cursor.x -|= count; - self.screen.cursor.pending_wrap = false; - return; - } - - // If we have a pending wrap state and we are in either reverse wrap - // modes then we decrement the amount we move by one to match xterm. - if (self.screen.cursor.pending_wrap) { - count -= 1; - self.screen.cursor.pending_wrap = false; - } - - // The margins we can move to. - const top = self.scrolling_region.top; - const bottom = self.scrolling_region.bottom; - const right_margin = self.scrolling_region.right; - const left_margin = if (self.screen.cursor.x < self.scrolling_region.left) - 0 - else - self.scrolling_region.left; - - // Handle some edge cases when our cursor is already on the left margin. - if (self.screen.cursor.x == left_margin) { - switch (wrap_mode) { - // In reverse mode, if we're already before the top margin - // then we just set our cursor to the top-left and we're done. - .reverse => if (self.screen.cursor.y <= top) { - self.screen.cursor.x = left_margin; - self.screen.cursor.y = top; - return; - }, - - // Handled in while loop - .reverse_extended => {}, - - // Handled above - .none => unreachable, - } - } - - while (true) { - // We can move at most to the left margin. - const max = self.screen.cursor.x - left_margin; - - // We want to move at most the number of columns we have left - // or our remaining count. Do the move. - const amount = @min(max, count); - count -= amount; - self.screen.cursor.x -= amount; - - // If we have no more to move, then we're done. - if (count == 0) break; - - // If we are at the top, then we are done. - if (self.screen.cursor.y == top) { - if (wrap_mode != .reverse_extended) break; - - self.screen.cursor.y = bottom; - self.screen.cursor.x = right_margin; - count -= 1; - continue; - } - - // UNDEFINED TERMINAL BEHAVIOR. This situation is not handled in xterm - // and currently results in a crash in xterm. Given no other known - // terminal [to me] implements XTREVWRAP2, I decided to just mimick - // the behavior of xterm up and not including the crash by wrapping - // up to the (0, 0) and stopping there. My reasoning is that for an - // appropriately sized value of "count" this is the behavior that xterm - // would have. This is unit tested. - if (self.screen.cursor.y == 0) { - assert(self.screen.cursor.x == left_margin); - break; - } - - // If our previous line is not wrapped then we are done. - if (wrap_mode != .reverse_extended) { - const row = self.screen.getRow(.{ .active = self.screen.cursor.y - 1 }); - if (!row.isWrapped()) break; - } - - self.screen.cursor.y -= 1; - self.screen.cursor.x = right_margin; - count -= 1; - } -} - -/// Move the cursor right amount columns. If amount is greater than the -/// maximum move distance then it is internally adjusted to the maximum. -/// This sequence will not scroll the screen or scroll region. If amount is -/// 0, adjust it to 1. -pub fn cursorRight(self: *Terminal, count_req: usize) void { - // Always resets pending wrap - self.screen.cursor.pending_wrap = false; - - // The max the cursor can move to depends where the cursor currently is - const max = if (self.screen.cursor.x <= self.scrolling_region.right) - self.scrolling_region.right - else - self.cols - 1; - - const count = @max(count_req, 1); - self.screen.cursor.x = @min(max, self.screen.cursor.x +| count); -} - -/// Move the cursor down amount lines. If amount is greater than the maximum -/// move distance then it is internally adjusted to the maximum. This sequence -/// will not scroll the screen or scroll region. If amount is 0, adjust it to 1. -pub fn cursorDown(self: *Terminal, count_req: usize) void { - // Always resets pending wrap - self.screen.cursor.pending_wrap = false; - - // The max the cursor can move to depends where the cursor currently is - const max = if (self.screen.cursor.y <= self.scrolling_region.bottom) - self.scrolling_region.bottom - else - self.rows - 1; - - const count = @max(count_req, 1); - self.screen.cursor.y = @min(max, self.screen.cursor.y +| count); -} - -/// Move the cursor up amount lines. If amount is greater than the maximum -/// move distance then it is internally adjusted to the maximum. If amount is -/// 0, adjust it to 1. -pub fn cursorUp(self: *Terminal, count_req: usize) void { - // Always resets pending wrap - self.screen.cursor.pending_wrap = false; - - // The min the cursor can move to depends where the cursor currently is - const min = if (self.screen.cursor.y >= self.scrolling_region.top) - self.scrolling_region.top - else - 0; - - const count = @max(count_req, 1); - self.screen.cursor.y = @max(min, self.screen.cursor.y -| count); -} - -/// Backspace moves the cursor back a column (but not less than 0). -pub fn backspace(self: *Terminal) void { - self.cursorLeft(1); -} - -/// Horizontal tab moves the cursor to the next tabstop, clearing -/// the screen to the left the tabstop. -pub fn horizontalTab(self: *Terminal) !void { - while (self.screen.cursor.x < self.scrolling_region.right) { - // Move the cursor right - self.screen.cursor.x += 1; - - // If the last cursor position was a tabstop we return. We do - // "last cursor position" because we want a space to be written - // at the tabstop unless we're at the end (the while condition). - if (self.tabstops.get(self.screen.cursor.x)) return; - } -} - -// Same as horizontalTab but moves to the previous tabstop instead of the next. -pub fn horizontalTabBack(self: *Terminal) !void { - // With origin mode enabled, our leftmost limit is the left margin. - const left_limit = if (self.modes.get(.origin)) self.scrolling_region.left else 0; - - while (true) { - // If we're already at the edge of the screen, then we're done. - if (self.screen.cursor.x <= left_limit) return; - - // Move the cursor left - self.screen.cursor.x -= 1; - if (self.tabstops.get(self.screen.cursor.x)) return; - } -} - -/// Clear tab stops. -pub fn tabClear(self: *Terminal, cmd: csi.TabClear) void { - switch (cmd) { - .current => self.tabstops.unset(self.screen.cursor.x), - .all => self.tabstops.reset(0), - else => log.warn("invalid or unknown tab clear setting: {}", .{cmd}), - } -} - -/// Set a tab stop on the current cursor. -/// TODO: test -pub fn tabSet(self: *Terminal) void { - self.tabstops.set(self.screen.cursor.x); -} - -/// TODO: test -pub fn tabReset(self: *Terminal) void { - self.tabstops.reset(TABSTOP_INTERVAL); -} - -/// Carriage return moves the cursor to the first column. -pub fn carriageReturn(self: *Terminal) void { - // Always reset pending wrap state - self.screen.cursor.pending_wrap = false; - - // In origin mode we always move to the left margin - self.screen.cursor.x = if (self.modes.get(.origin)) - self.scrolling_region.left - else if (self.screen.cursor.x >= self.scrolling_region.left) - self.scrolling_region.left - else - 0; -} - -/// Linefeed moves the cursor to the next line. -pub fn linefeed(self: *Terminal) !void { - try self.index(); - if (self.modes.get(.linefeed)) self.carriageReturn(); -} - -/// Inserts spaces at current cursor position moving existing cell contents -/// to the right. The contents of the count right-most columns in the scroll -/// region are lost. The cursor position is not changed. -/// -/// This unsets the pending wrap state without wrapping. -/// -/// The inserted cells are colored according to the current SGR state. -pub fn insertBlanks(self: *Terminal, count: usize) void { - // Unset pending wrap state without wrapping. Note: this purposely - // happens BEFORE the scroll region check below, because that's what - // xterm does. - self.screen.cursor.pending_wrap = false; - - // If our cursor is outside the margins then do nothing. We DO reset - // wrap state still so this must remain below the above logic. - if (self.screen.cursor.x < self.scrolling_region.left or - self.screen.cursor.x > self.scrolling_region.right) return; - - // The limit we can shift to is our right margin. We add 1 since the - // math around this is 1-indexed. - const right_limit = self.scrolling_region.right + 1; - - // If our count is larger than the remaining amount, we just erase right. - // We only do this if we can erase the entire line (no right margin). - if (right_limit == self.cols and - count > right_limit - self.screen.cursor.x) - { - self.eraseLine(.right, false); - return; - } - - // Get the current row - const row = self.screen.getRow(.{ .active = self.screen.cursor.y }); - - // Determine our indexes. - const start = self.screen.cursor.x; - const pivot = @min(self.screen.cursor.x + count, right_limit); - - // This is the number of spaces we have left to shift existing data. - // If count is bigger than the available space left after the cursor, - // we may have no space at all for copying. - const copyable = right_limit - pivot; - if (copyable > 0) { - // This is the index of the final copyable value that we need to copy. - const copyable_end = start + copyable - 1; - - // If our last cell we're shifting is wide, then we need to clear - // it to be empty so we don't split the multi-cell char. - const cell = row.getCellPtr(copyable_end); - if (cell.attrs.wide) cell.char = 0; - - // Shift count cells. We have to do this backwards since we're not - // allocated new space, otherwise we'll copy duplicates. - var i: usize = 0; - while (i < copyable) : (i += 1) { - const to = right_limit - 1 - i; - const from = copyable_end - i; - const src = row.getCell(from); - const dst = row.getCellPtr(to); - dst.* = src; - } - } - - // Insert blanks. The blanks preserve the background color. - row.fillSlice(.{ - .bg = self.screen.cursor.pen.bg, - }, start, pivot); -} - -/// Insert amount lines at the current cursor row. The contents of the line -/// at the current cursor row and below (to the bottom-most line in the -/// scrolling region) are shifted down by amount lines. The contents of the -/// amount bottom-most lines in the scroll region are lost. -/// -/// This unsets the pending wrap state without wrapping. If the current cursor -/// position is outside of the current scroll region it does nothing. -/// -/// If amount is greater than the remaining number of lines in the scrolling -/// region it is adjusted down (still allowing for scrolling out every remaining -/// line in the scrolling region) -/// -/// In left and right margin mode the margins are respected; lines are only -/// scrolled in the scroll region. -/// -/// All cleared space is colored according to the current SGR state. -/// -/// Moves the cursor to the left margin. -pub fn insertLines(self: *Terminal, count: usize) !void { - // Rare, but happens - if (count == 0) return; - - // If the cursor is outside the scroll region we do nothing. - if (self.screen.cursor.y < self.scrolling_region.top or - self.screen.cursor.y > self.scrolling_region.bottom or - self.screen.cursor.x < self.scrolling_region.left or - self.screen.cursor.x > self.scrolling_region.right) return; - - // Move the cursor to the left margin - self.screen.cursor.x = self.scrolling_region.left; - self.screen.cursor.pending_wrap = false; - - // Remaining rows from our cursor - const rem = self.scrolling_region.bottom - self.screen.cursor.y + 1; - - // If count is greater than the amount of rows, adjust down. - const adjusted_count = @min(count, rem); - - // The the top `scroll_amount` lines need to move to the bottom - // scroll area. We may have nothing to scroll if we're clearing. - const scroll_amount = rem - adjusted_count; - var y: usize = self.scrolling_region.bottom; - const top = y - scroll_amount; - - // Ensure we have the lines populated to the end - while (y > top) : (y -= 1) { - const src = self.screen.getRow(.{ .active = y - adjusted_count }); - const dst = self.screen.getRow(.{ .active = y }); - for (self.scrolling_region.left..self.scrolling_region.right + 1) |x| { - try dst.copyCell(src, x); - } - } - - // Insert count blank lines - y = self.screen.cursor.y; - while (y < self.screen.cursor.y + adjusted_count) : (y += 1) { - const row = self.screen.getRow(.{ .active = y }); - row.fillSlice(.{ - .bg = self.screen.cursor.pen.bg, - }, self.scrolling_region.left, self.scrolling_region.right + 1); - } -} - -/// Removes amount lines from the current cursor row down. The remaining lines -/// to the bottom margin are shifted up and space from the bottom margin up is -/// filled with empty lines. -/// -/// If the current cursor position is outside of the current scroll region it -/// does nothing. If amount is greater than the remaining number of lines in the -/// scrolling region it is adjusted down. -/// -/// In left and right margin mode the margins are respected; lines are only -/// scrolled in the scroll region. -/// -/// If the cell movement splits a multi cell character that character cleared, -/// by replacing it by spaces, keeping its current attributes. All other -/// cleared space is colored according to the current SGR state. -/// -/// Moves the cursor to the left margin. -pub fn deleteLines(self: *Terminal, count: usize) !void { - // If the cursor is outside the scroll region we do nothing. - if (self.screen.cursor.y < self.scrolling_region.top or - self.screen.cursor.y > self.scrolling_region.bottom or - self.screen.cursor.x < self.scrolling_region.left or - self.screen.cursor.x > self.scrolling_region.right) return; - - // Move the cursor to the left margin - self.screen.cursor.x = self.scrolling_region.left; - self.screen.cursor.pending_wrap = false; - - // If this is a full line margin then we can do a faster scroll. - if (self.scrolling_region.left == 0 and - self.scrolling_region.right == self.cols - 1) - { - self.screen.scrollRegionUp( - .{ .active = self.screen.cursor.y }, - .{ .active = self.scrolling_region.bottom }, - @min(count, (self.scrolling_region.bottom - self.screen.cursor.y) + 1), - ); - return; - } - - // Left/right margin is set, we need to do a slower scroll. - // Remaining rows from our cursor in the region, 1-indexed. - const rem = self.scrolling_region.bottom - self.screen.cursor.y + 1; - - // If our count is greater than the remaining amount, we can just - // clear the region using insertLines. - if (count >= rem) { - try self.insertLines(count); - return; - } - - // The amount of lines we need to scroll up. - const scroll_amount = rem - count; - const scroll_end_y = self.screen.cursor.y + scroll_amount; - for (self.screen.cursor.y..scroll_end_y) |y| { - const src = self.screen.getRow(.{ .active = y + count }); - const dst = self.screen.getRow(.{ .active = y }); - for (self.scrolling_region.left..self.scrolling_region.right + 1) |x| { - try dst.copyCell(src, x); - } - } - - // Insert blank lines - for (scroll_end_y..self.scrolling_region.bottom + 1) |y| { - const row = self.screen.getRow(.{ .active = y }); - row.setWrapped(false); - row.fillSlice(.{ - .bg = self.screen.cursor.pen.bg, - }, self.scrolling_region.left, self.scrolling_region.right + 1); - } -} - -/// Scroll the text down by one row. -pub fn scrollDown(self: *Terminal, count: usize) !void { - // Preserve the cursor - const cursor = self.screen.cursor; - defer self.screen.cursor = cursor; - - // Move to the top of the scroll region - self.screen.cursor.y = self.scrolling_region.top; - self.screen.cursor.x = self.scrolling_region.left; - try self.insertLines(count); -} - -/// Removes amount lines from the top of the scroll region. The remaining lines -/// to the bottom margin are shifted up and space from the bottom margin up -/// is filled with empty lines. -/// -/// The new lines are created according to the current SGR state. -/// -/// Does not change the (absolute) cursor position. -pub fn scrollUp(self: *Terminal, count: usize) !void { - // Preserve the cursor - const cursor = self.screen.cursor; - defer self.screen.cursor = cursor; - - // Move to the top of the scroll region - self.screen.cursor.y = self.scrolling_region.top; - self.screen.cursor.x = self.scrolling_region.left; - try self.deleteLines(count); -} - -/// Options for scrolling the viewport of the terminal grid. -pub const ScrollViewport = union(enum) { - /// Scroll to the top of the scrollback - top: void, - - /// Scroll to the bottom, i.e. the top of the active area - bottom: void, - - /// Scroll by some delta amount, up is negative. - delta: isize, -}; - -/// Scroll the viewport of the terminal grid. -pub fn scrollViewport(self: *Terminal, behavior: ScrollViewport) !void { - try self.screen.scroll(switch (behavior) { - .top => .{ .top = {} }, - .bottom => .{ .bottom = {} }, - .delta => |delta| .{ .viewport = delta }, - }); -} - -/// Set Top and Bottom Margins If bottom is not specified, 0 or bigger than -/// the number of the bottom-most row, it is adjusted to the number of the -/// bottom most row. -/// -/// If top < bottom set the top and bottom row of the scroll region according -/// to top and bottom and move the cursor to the top-left cell of the display -/// (when in cursor origin mode is set to the top-left cell of the scroll region). -/// -/// Otherwise: Set the top and bottom row of the scroll region to the top-most -/// and bottom-most line of the screen. -/// -/// Top and bottom are 1-indexed. -pub fn setTopAndBottomMargin(self: *Terminal, top_req: usize, bottom_req: usize) void { - const top = @max(1, top_req); - const bottom = @min(self.rows, if (bottom_req == 0) self.rows else bottom_req); - if (top >= bottom) return; - - self.scrolling_region.top = top - 1; - self.scrolling_region.bottom = bottom - 1; - self.setCursorPos(1, 1); -} - -/// DECSLRM -pub fn setLeftAndRightMargin(self: *Terminal, left_req: usize, right_req: usize) void { - // We must have this mode enabled to do anything - if (!self.modes.get(.enable_left_and_right_margin)) return; - - const left = @max(1, left_req); - const right = @min(self.cols, if (right_req == 0) self.cols else right_req); - if (left >= right) return; - - self.scrolling_region.left = left - 1; - self.scrolling_region.right = right - 1; - self.setCursorPos(1, 1); -} - -/// Mark the current semantic prompt information. Current escape sequences -/// (OSC 133) only allow setting this for wherever the current active cursor -/// is located. -pub fn markSemanticPrompt(self: *Terminal, p: SemanticPrompt) void { - //log.debug("semantic_prompt y={} p={}", .{ self.screen.cursor.y, p }); - const row = self.screen.getRow(.{ .active = self.screen.cursor.y }); - row.setSemanticPrompt(switch (p) { - .prompt => .prompt, - .prompt_continuation => .prompt_continuation, - .input => .input, - .command => .command, - }); -} - -/// Returns true if the cursor is currently at a prompt. Another way to look -/// at this is it returns false if the shell is currently outputting something. -/// This requires shell integration (semantic prompt integration). -/// -/// If the shell integration doesn't exist, this will always return false. -pub fn cursorIsAtPrompt(self: *Terminal) bool { - // If we're on the secondary screen, we're never at a prompt. - if (self.active_screen == .alternate) return false; - - var y: usize = 0; - while (y <= self.screen.cursor.y) : (y += 1) { - // We want to go bottom up - const bottom_y = self.screen.cursor.y - y; - const row = self.screen.getRow(.{ .active = bottom_y }); - switch (row.getSemanticPrompt()) { - // If we're at a prompt or input area, then we are at a prompt. - .prompt, - .prompt_continuation, - .input, - => return true, - - // If we have command output, then we're most certainly not - // at a prompt. - .command => return false, - - // If we don't know, we keep searching. - .unknown => {}, - } - } - - return false; -} - -/// Set the pwd for the terminal. -pub fn setPwd(self: *Terminal, pwd: []const u8) !void { - self.pwd.clearRetainingCapacity(); - try self.pwd.appendSlice(pwd); -} - -/// Returns the pwd for the terminal, if any. The memory is owned by the -/// Terminal and is not copied. It is safe until a reset or setPwd. -pub fn getPwd(self: *const Terminal) ?[]const u8 { - if (self.pwd.items.len == 0) return null; - return self.pwd.items; -} - -/// Execute a kitty graphics command. The buf is used to populate with -/// the response that should be sent as an APC sequence. The response will -/// be a full, valid APC sequence. -/// -/// If an error occurs, the caller should response to the pty that a -/// an error occurred otherwise the behavior of the graphics protocol is -/// undefined. -pub fn kittyGraphics( - self: *Terminal, - alloc: Allocator, - cmd: *kitty.graphics.Command, -) ?kitty.graphics.Response { - return kitty.graphics.execute(alloc, self, cmd); -} - -/// Set the character protection mode for the terminal. -pub fn setProtectedMode(self: *Terminal, mode: ansi.ProtectedMode) void { - switch (mode) { - .off => { - self.screen.cursor.pen.attrs.protected = false; - - // screen.protected_mode is NEVER reset to ".off" because - // logic such as eraseChars depends on knowing what the - // _most recent_ mode was. - }, - - .iso => { - self.screen.cursor.pen.attrs.protected = true; - self.screen.protected_mode = .iso; - }, - - .dec => { - self.screen.cursor.pen.attrs.protected = true; - self.screen.protected_mode = .dec; - }, - } -} - -/// Full reset -pub fn fullReset(self: *Terminal, alloc: Allocator) void { - self.primaryScreen(alloc, .{ .clear_on_exit = true, .cursor_save = true }); - self.screen.charset = .{}; - self.modes = .{}; - self.flags = .{}; - self.tabstops.reset(TABSTOP_INTERVAL); - self.screen.cursor = .{}; - self.screen.saved_cursor = null; - self.screen.selection = null; - self.screen.kitty_keyboard = .{}; - self.screen.protected_mode = .off; - self.scrolling_region = .{ - .top = 0, - .bottom = self.rows - 1, - .left = 0, - .right = self.cols - 1, - }; - self.previous_char = null; - self.eraseDisplay(alloc, .scrollback, false); - self.eraseDisplay(alloc, .complete, false); - self.pwd.clearRetainingCapacity(); - self.status_display = .main; -} - -// X -test "Terminal: fullReset with a non-empty pen" { - var t = try init(testing.allocator, 80, 80); - defer t.deinit(testing.allocator); - - t.screen.cursor.pen.bg = .{ .rgb = .{ .r = 0xFF, .g = 0x00, .b = 0x7F } }; - t.screen.cursor.pen.fg = .{ .rgb = .{ .r = 0xFF, .g = 0x00, .b = 0x7F } }; - t.fullReset(testing.allocator); - - const cell = t.screen.getCell(.active, t.screen.cursor.y, t.screen.cursor.x); - try testing.expect(cell.bg == .none); - try testing.expect(cell.fg == .none); -} - -// X -test "Terminal: fullReset origin mode" { - var t = try init(testing.allocator, 10, 10); - defer t.deinit(testing.allocator); - - t.setCursorPos(3, 5); - t.modes.set(.origin, true); - t.fullReset(testing.allocator); - - // Origin mode should be reset and the cursor should be moved - try testing.expectEqual(@as(usize, 0), t.screen.cursor.y); - try testing.expectEqual(@as(usize, 0), t.screen.cursor.x); - try testing.expect(!t.modes.get(.origin)); -} - -// X -test "Terminal: fullReset status display" { - var t = try init(testing.allocator, 10, 10); - defer t.deinit(testing.allocator); - - t.status_display = .status_line; - t.fullReset(testing.allocator); - try testing.expect(t.status_display == .main); -} - -// X -test "Terminal: input with no control characters" { - var t = try init(testing.allocator, 80, 80); - defer t.deinit(testing.allocator); - - // Basic grid writing - for ("hello") |c| try t.print(c); - try testing.expectEqual(@as(usize, 0), t.screen.cursor.y); - try testing.expectEqual(@as(usize, 5), t.screen.cursor.x); - { - const str = try t.plainString(testing.allocator); - defer testing.allocator.free(str); - try testing.expectEqualStrings("hello", str); - } -} - -// X -test "Terminal: zero-width character at start" { - var t = try init(testing.allocator, 80, 80); - defer t.deinit(testing.allocator); - - // This used to crash the terminal. This is not allowed so we should - // just ignore it. - try t.print(0x200D); - - try testing.expectEqual(@as(usize, 0), t.screen.cursor.y); - try testing.expectEqual(@as(usize, 0), t.screen.cursor.x); -} - -// https://github.com/mitchellh/ghostty/issues/1400 -// X -test "Terminal: print single very long line" { - var t = try init(testing.allocator, 5, 5); - defer t.deinit(testing.allocator); - - // This would crash for issue 1400. So the assertion here is - // that we simply do not crash. - for (0..500) |_| try t.print('x'); -} - -// X -test "Terminal: print over wide char at 0,0" { - var t = try init(testing.allocator, 80, 80); - defer t.deinit(testing.allocator); - - try t.print(0x1F600); // Smiley face - t.setCursorPos(0, 0); - try t.print('A'); // Smiley face - - try testing.expectEqual(@as(usize, 0), t.screen.cursor.y); - try testing.expectEqual(@as(usize, 1), t.screen.cursor.x); - - const row = t.screen.getRow(.{ .screen = 0 }); - { - const cell = row.getCell(0); - try testing.expectEqual(@as(u32, 'A'), cell.char); - try testing.expect(!cell.attrs.wide); - try testing.expectEqual(@as(usize, 1), row.codepointLen(0)); - } - { - const cell = row.getCell(1); - try testing.expect(!cell.attrs.wide_spacer_tail); - } -} - -// X -test "Terminal: print over wide spacer tail" { - var t = try init(testing.allocator, 5, 5); - defer t.deinit(testing.allocator); - - try t.print('橋'); - t.setCursorPos(1, 2); - try t.print('X'); - - { - const str = try t.plainString(testing.allocator); - defer testing.allocator.free(str); - try testing.expectEqualStrings(" X", str); - } - - const row = t.screen.getRow(.{ .screen = 0 }); - { - const cell = row.getCell(0); - try testing.expectEqual(@as(u32, 0), cell.char); - try testing.expect(!cell.attrs.wide); - try testing.expectEqual(@as(usize, 1), row.codepointLen(0)); - } - { - const cell = row.getCell(1); - try testing.expectEqual(@as(u32, 'X'), cell.char); - try testing.expect(!cell.attrs.wide_spacer_tail); - try testing.expectEqual(@as(usize, 1), row.codepointLen(1)); - } -} - -// X -test "Terminal: VS15 to make narrow character" { - var t = try init(testing.allocator, 5, 5); - defer t.deinit(testing.allocator); - - // Enable grapheme clustering - t.modes.set(.grapheme_cluster, true); - - try t.print(0x26C8); // Thunder cloud and rain - try t.print(0xFE0E); // VS15 to make narrow - - { - const str = try t.plainString(testing.allocator); - defer testing.allocator.free(str); - try testing.expectEqualStrings("⛈︎", str); - } - - const row = t.screen.getRow(.{ .screen = 0 }); - { - const cell = row.getCell(0); - try testing.expectEqual(@as(u32, 0x26C8), cell.char); - try testing.expect(!cell.attrs.wide); - try testing.expectEqual(@as(usize, 2), row.codepointLen(0)); - } -} - -// X -test "Terminal: VS16 to make wide character with mode 2027" { - var t = try init(testing.allocator, 5, 5); - defer t.deinit(testing.allocator); - - // Enable grapheme clustering - t.modes.set(.grapheme_cluster, true); - - try t.print(0x2764); // Heart - try t.print(0xFE0F); // VS16 to make wide - - { - const str = try t.plainString(testing.allocator); - defer testing.allocator.free(str); - try testing.expectEqualStrings("❤️", str); - } - - const row = t.screen.getRow(.{ .screen = 0 }); - { - const cell = row.getCell(0); - try testing.expectEqual(@as(u32, 0x2764), cell.char); - try testing.expect(cell.attrs.wide); - try testing.expectEqual(@as(usize, 2), row.codepointLen(0)); - } -} - -// X -test "Terminal: VS16 repeated with mode 2027" { - var t = try init(testing.allocator, 5, 5); - defer t.deinit(testing.allocator); - - // Enable grapheme clustering - t.modes.set(.grapheme_cluster, true); - - try t.print(0x2764); // Heart - try t.print(0xFE0F); // VS16 to make wide - try t.print(0x2764); // Heart - try t.print(0xFE0F); // VS16 to make wide - - { - const str = try t.plainString(testing.allocator); - defer testing.allocator.free(str); - try testing.expectEqualStrings("❤️❤️", str); - } - - const row = t.screen.getRow(.{ .screen = 0 }); - { - const cell = row.getCell(0); - try testing.expectEqual(@as(u32, 0x2764), cell.char); - try testing.expect(cell.attrs.wide); - try testing.expectEqual(@as(usize, 2), row.codepointLen(0)); - } - { - const cell = row.getCell(2); - try testing.expectEqual(@as(u32, 0x2764), cell.char); - try testing.expect(cell.attrs.wide); - try testing.expectEqual(@as(usize, 2), row.codepointLen(2)); - } -} - -// X -test "Terminal: VS16 doesn't make character with 2027 disabled" { - var t = try init(testing.allocator, 5, 5); - defer t.deinit(testing.allocator); - - // Disable grapheme clustering - t.modes.set(.grapheme_cluster, false); - - try t.print(0x2764); // Heart - try t.print(0xFE0F); // VS16 to make wide - - { - const str = try t.plainString(testing.allocator); - defer testing.allocator.free(str); - try testing.expectEqualStrings("❤️", str); - } - - const row = t.screen.getRow(.{ .screen = 0 }); - { - const cell = row.getCell(0); - try testing.expectEqual(@as(u32, 0x2764), cell.char); - try testing.expect(!cell.attrs.wide); - try testing.expectEqual(@as(usize, 2), row.codepointLen(0)); - } -} - -// X -test "Terminal: print multicodepoint grapheme, disabled mode 2027" { - var t = try init(testing.allocator, 80, 80); - defer t.deinit(testing.allocator); - - // https://github.com/mitchellh/ghostty/issues/289 - // This is: 👨‍👩‍👧 (which may or may not render correctly) - try t.print(0x1F468); - try t.print(0x200D); - try t.print(0x1F469); - try t.print(0x200D); - try t.print(0x1F467); - - // We should have 6 cells taken up - try testing.expectEqual(@as(usize, 0), t.screen.cursor.y); - try testing.expectEqual(@as(usize, 6), t.screen.cursor.x); - - // Assert various properties about our screen to verify - // we have all expected cells. - const row = t.screen.getRow(.{ .screen = 0 }); - { - const cell = row.getCell(0); - try testing.expectEqual(@as(u32, 0x1F468), cell.char); - try testing.expect(cell.attrs.wide); - try testing.expectEqual(@as(usize, 2), row.codepointLen(0)); - } - { - const cell = row.getCell(1); - try testing.expectEqual(@as(u32, ' '), cell.char); - try testing.expect(cell.attrs.wide_spacer_tail); - try testing.expectEqual(@as(usize, 1), row.codepointLen(1)); - } - { - const cell = row.getCell(2); - try testing.expectEqual(@as(u32, 0x1F469), cell.char); - try testing.expect(cell.attrs.wide); - try testing.expectEqual(@as(usize, 2), row.codepointLen(2)); - } - { - const cell = row.getCell(3); - try testing.expectEqual(@as(u32, ' '), cell.char); - try testing.expect(cell.attrs.wide_spacer_tail); - } - { - const cell = row.getCell(4); - try testing.expectEqual(@as(u32, 0x1F467), cell.char); - try testing.expect(cell.attrs.wide); - try testing.expectEqual(@as(usize, 1), row.codepointLen(4)); - } - { - const cell = row.getCell(5); - try testing.expectEqual(@as(u32, ' '), cell.char); - try testing.expect(cell.attrs.wide_spacer_tail); - } -} - -// X -test "Terminal: print multicodepoint grapheme, mode 2027" { - var t = try init(testing.allocator, 80, 80); - defer t.deinit(testing.allocator); - - // Enable grapheme clustering - t.modes.set(.grapheme_cluster, true); - - // https://github.com/mitchellh/ghostty/issues/289 - // This is: 👨‍👩‍👧 (which may or may not render correctly) - try t.print(0x1F468); - try t.print(0x200D); - try t.print(0x1F469); - try t.print(0x200D); - try t.print(0x1F467); - - // We should have 2 cells taken up. It is one character but "wide". - try testing.expectEqual(@as(usize, 0), t.screen.cursor.y); - try testing.expectEqual(@as(usize, 2), t.screen.cursor.x); - - // Assert various properties about our screen to verify - // we have all expected cells. - const row = t.screen.getRow(.{ .screen = 0 }); - { - const cell = row.getCell(0); - try testing.expectEqual(@as(u32, 0x1F468), cell.char); - try testing.expect(cell.attrs.wide); - try testing.expectEqual(@as(usize, 5), row.codepointLen(0)); - } - { - const cell = row.getCell(1); - try testing.expectEqual(@as(u32, ' '), cell.char); - try testing.expect(cell.attrs.wide_spacer_tail); - try testing.expectEqual(@as(usize, 1), row.codepointLen(1)); - } -} - -// X -test "Terminal: print invalid VS16 non-grapheme" { - var t = try init(testing.allocator, 80, 80); - defer t.deinit(testing.allocator); - - // https://github.com/mitchellh/ghostty/issues/1482 - try t.print('x'); - try t.print(0xFE0F); - - // We should have 2 cells taken up. It is one character but "wide". - try testing.expectEqual(@as(usize, 0), t.screen.cursor.y); - try testing.expectEqual(@as(usize, 1), t.screen.cursor.x); - - // Assert various properties about our screen to verify - // we have all expected cells. - const row = t.screen.getRow(.{ .screen = 0 }); - { - const cell = row.getCell(0); - try testing.expectEqual(@as(u32, 'x'), cell.char); - try testing.expect(!cell.attrs.wide); - try testing.expectEqual(@as(usize, 1), row.codepointLen(0)); - } - { - const cell = row.getCell(1); - try testing.expectEqual(@as(u32, 0), cell.char); - } -} - -// X -test "Terminal: print invalid VS16 grapheme" { - var t = try init(testing.allocator, 80, 80); - defer t.deinit(testing.allocator); - - // Enable grapheme clustering - t.modes.set(.grapheme_cluster, true); - - // https://github.com/mitchellh/ghostty/issues/1482 - try t.print('x'); - try t.print(0xFE0F); - - // We should have 2 cells taken up. It is one character but "wide". - try testing.expectEqual(@as(usize, 0), t.screen.cursor.y); - try testing.expectEqual(@as(usize, 1), t.screen.cursor.x); - - // Assert various properties about our screen to verify - // we have all expected cells. - const row = t.screen.getRow(.{ .screen = 0 }); - { - const cell = row.getCell(0); - try testing.expectEqual(@as(u32, 'x'), cell.char); - try testing.expect(!cell.attrs.wide); - try testing.expectEqual(@as(usize, 1), row.codepointLen(0)); - } - { - const cell = row.getCell(1); - try testing.expectEqual(@as(u32, 0), cell.char); - } -} - -// X -test "Terminal: print invalid VS16 with second char" { - var t = try init(testing.allocator, 80, 80); - defer t.deinit(testing.allocator); - - // Enable grapheme clustering - t.modes.set(.grapheme_cluster, true); - - // https://github.com/mitchellh/ghostty/issues/1482 - try t.print('x'); - try t.print(0xFE0F); - try t.print('y'); - - // We should have 2 cells taken up. It is one character but "wide". - try testing.expectEqual(@as(usize, 0), t.screen.cursor.y); - try testing.expectEqual(@as(usize, 2), t.screen.cursor.x); - - // Assert various properties about our screen to verify - // we have all expected cells. - const row = t.screen.getRow(.{ .screen = 0 }); - { - const cell = row.getCell(0); - try testing.expectEqual(@as(u32, 'x'), cell.char); - try testing.expect(!cell.attrs.wide); - try testing.expectEqual(@as(usize, 1), row.codepointLen(0)); - } - { - const cell = row.getCell(1); - try testing.expectEqual(@as(u32, 'y'), cell.char); - try testing.expect(!cell.attrs.wide); - try testing.expectEqual(@as(usize, 1), row.codepointLen(0)); - } -} - -// X -test "Terminal: soft wrap" { - var t = try init(testing.allocator, 3, 80); - defer t.deinit(testing.allocator); - - // Basic grid writing - for ("hello") |c| try t.print(c); - try testing.expectEqual(@as(usize, 1), t.screen.cursor.y); - try testing.expectEqual(@as(usize, 2), t.screen.cursor.x); - { - const str = try t.plainString(testing.allocator); - defer testing.allocator.free(str); - try testing.expectEqualStrings("hel\nlo", str); - } -} - -// X -test "Terminal: soft wrap with semantic prompt" { - var t = try init(testing.allocator, 3, 80); - defer t.deinit(testing.allocator); - - t.markSemanticPrompt(.prompt); - for ("hello") |c| try t.print(c); - - { - const row = t.screen.getRow(.{ .active = 0 }); - try testing.expect(row.getSemanticPrompt() == .prompt); - } - { - const row = t.screen.getRow(.{ .active = 1 }); - try testing.expect(row.getSemanticPrompt() == .prompt); - } -} - -// X -test "Terminal: disabled wraparound with wide char and one space" { - var t = try init(testing.allocator, 5, 5); - defer t.deinit(testing.allocator); - - t.modes.set(.wraparound, false); - - // This puts our cursor at the end and there is NO SPACE for a - // wide character. - try t.printString("AAAA"); - try t.print(0x1F6A8); // Police car light - try testing.expectEqual(@as(usize, 0), t.screen.cursor.y); - try testing.expectEqual(@as(usize, 4), t.screen.cursor.x); - - { - const str = try t.plainString(testing.allocator); - defer testing.allocator.free(str); - try testing.expectEqualStrings("AAAA", str); - } - - // Make sure we printed nothing - const row = t.screen.getRow(.{ .screen = 0 }); - { - const cell = row.getCell(4); - try testing.expectEqual(@as(u32, 0), cell.char); - try testing.expect(!cell.attrs.wide); - } -} - -// X -test "Terminal: disabled wraparound with wide char and no space" { - var t = try init(testing.allocator, 5, 5); - defer t.deinit(testing.allocator); - - t.modes.set(.wraparound, false); - - // This puts our cursor at the end and there is NO SPACE for a - // wide character. - try t.printString("AAAAA"); - try t.print(0x1F6A8); // Police car light - try testing.expectEqual(@as(usize, 0), t.screen.cursor.y); - try testing.expectEqual(@as(usize, 4), t.screen.cursor.x); - - { - const str = try t.plainString(testing.allocator); - defer testing.allocator.free(str); - try testing.expectEqualStrings("AAAAA", str); - } - - // Make sure we printed nothing - const row = t.screen.getRow(.{ .screen = 0 }); - { - const cell = row.getCell(4); - try testing.expectEqual(@as(u32, 'A'), cell.char); - try testing.expect(!cell.attrs.wide); - } -} - -// X -test "Terminal: disabled wraparound with wide grapheme and half space" { - var t = try init(testing.allocator, 5, 5); - defer t.deinit(testing.allocator); - - t.modes.set(.grapheme_cluster, true); - t.modes.set(.wraparound, false); - - // This puts our cursor at the end and there is NO SPACE for a - // wide character. - try t.printString("AAAA"); - try t.print(0x2764); // Heart - try t.print(0xFE0F); // VS16 to make wide - try testing.expectEqual(@as(usize, 0), t.screen.cursor.y); - try testing.expectEqual(@as(usize, 4), t.screen.cursor.x); - - { - const str = try t.plainString(testing.allocator); - defer testing.allocator.free(str); - try testing.expectEqualStrings("AAAA❤", str); - } - - // Make sure we printed nothing - const row = t.screen.getRow(.{ .screen = 0 }); - { - const cell = row.getCell(4); - try testing.expectEqual(@as(u32, '❤'), cell.char); - try testing.expect(!cell.attrs.wide); - } -} - -// X -test "Terminal: print writes to bottom if scrolled" { - var t = try init(testing.allocator, 5, 2); - defer t.deinit(testing.allocator); - - // Basic grid writing - for ("hello") |c| try t.print(c); - t.setCursorPos(0, 0); - - // Make newlines so we create scrollback - // 3 pushes hello off the screen - try t.index(); - try t.index(); - try t.index(); - { - const str = try t.plainString(testing.allocator); - defer testing.allocator.free(str); - try testing.expectEqualStrings("", str); - } - - // Scroll to the top - try t.scrollViewport(.{ .top = {} }); - { - const str = try t.plainString(testing.allocator); - defer testing.allocator.free(str); - try testing.expectEqualStrings("hello", str); - } - - // Type - try t.print('A'); - try t.scrollViewport(.{ .bottom = {} }); - { - const str = try t.plainString(testing.allocator); - defer testing.allocator.free(str); - try testing.expectEqualStrings("\nA", str); - } -} - -// X -test "Terminal: print charset" { - var t = try init(testing.allocator, 80, 80); - defer t.deinit(testing.allocator); - - // G1 should have no effect - t.configureCharset(.G1, .dec_special); - t.configureCharset(.G2, .dec_special); - t.configureCharset(.G3, .dec_special); - - // Basic grid writing - try t.print('`'); - t.configureCharset(.G0, .utf8); - try t.print('`'); - t.configureCharset(.G0, .ascii); - try t.print('`'); - t.configureCharset(.G0, .dec_special); - try t.print('`'); - { - const str = try t.plainString(testing.allocator); - defer testing.allocator.free(str); - try testing.expectEqualStrings("```◆", str); - } -} - -// X -test "Terminal: print charset outside of ASCII" { - var t = try init(testing.allocator, 80, 80); - defer t.deinit(testing.allocator); - - // G1 should have no effect - t.configureCharset(.G1, .dec_special); - t.configureCharset(.G2, .dec_special); - t.configureCharset(.G3, .dec_special); - - // Basic grid writing - t.configureCharset(.G0, .dec_special); - try t.print('`'); - try t.print(0x1F600); - { - const str = try t.plainString(testing.allocator); - defer testing.allocator.free(str); - try testing.expectEqualStrings("◆ ", str); - } -} - -// X -test "Terminal: print invoke charset" { - var t = try init(testing.allocator, 80, 80); - defer t.deinit(testing.allocator); - - t.configureCharset(.G1, .dec_special); - - // Basic grid writing - try t.print('`'); - t.invokeCharset(.GL, .G1, false); - try t.print('`'); - try t.print('`'); - t.invokeCharset(.GL, .G0, false); - try t.print('`'); - { - const str = try t.plainString(testing.allocator); - defer testing.allocator.free(str); - try testing.expectEqualStrings("`◆◆`", str); - } -} - -// X -test "Terminal: print invoke charset single" { - var t = try init(testing.allocator, 80, 80); - defer t.deinit(testing.allocator); - - t.configureCharset(.G1, .dec_special); - - // Basic grid writing - try t.print('`'); - t.invokeCharset(.GL, .G1, true); - try t.print('`'); - try t.print('`'); - { - const str = try t.plainString(testing.allocator); - defer testing.allocator.free(str); - try testing.expectEqualStrings("`◆`", str); - } -} - -// X -test "Terminal: print right margin wrap" { - var t = try init(testing.allocator, 10, 5); - defer t.deinit(testing.allocator); - - try t.printString("123456789"); - t.modes.set(.enable_left_and_right_margin, true); - t.setLeftAndRightMargin(3, 5); - t.setCursorPos(1, 5); - try t.printString("XY"); - - { - const str = try t.plainString(testing.allocator); - defer testing.allocator.free(str); - try testing.expectEqualStrings("1234X6789\n Y", str); - } -} - -// X -test "Terminal: print right margin outside" { - var t = try init(testing.allocator, 10, 5); - defer t.deinit(testing.allocator); - - try t.printString("123456789"); - t.modes.set(.enable_left_and_right_margin, true); - t.setLeftAndRightMargin(3, 5); - t.setCursorPos(1, 6); - try t.printString("XY"); - - { - const str = try t.plainString(testing.allocator); - defer testing.allocator.free(str); - try testing.expectEqualStrings("12345XY89", str); - } -} - -// X -test "Terminal: print right margin outside wrap" { - var t = try init(testing.allocator, 10, 5); - defer t.deinit(testing.allocator); - - try t.printString("123456789"); - t.modes.set(.enable_left_and_right_margin, true); - t.setLeftAndRightMargin(3, 5); - t.setCursorPos(1, 10); - try t.printString("XY"); - - { - const str = try t.plainString(testing.allocator); - defer testing.allocator.free(str); - try testing.expectEqualStrings("123456789X\n Y", str); - } -} - -// X -test "Terminal: linefeed and carriage return" { - var t = try init(testing.allocator, 80, 80); - defer t.deinit(testing.allocator); - - // Basic grid writing - for ("hello") |c| try t.print(c); - t.carriageReturn(); - try t.linefeed(); - for ("world") |c| try t.print(c); - try testing.expectEqual(@as(usize, 1), t.screen.cursor.y); - try testing.expectEqual(@as(usize, 5), t.screen.cursor.x); - { - const str = try t.plainString(testing.allocator); - defer testing.allocator.free(str); - try testing.expectEqualStrings("hello\nworld", str); - } -} - -// X -test "Terminal: linefeed unsets pending wrap" { - var t = try init(testing.allocator, 5, 80); - defer t.deinit(testing.allocator); - - // Basic grid writing - for ("hello") |c| try t.print(c); - try testing.expect(t.screen.cursor.pending_wrap == true); - try t.linefeed(); - try testing.expect(t.screen.cursor.pending_wrap == false); -} - -// X -test "Terminal: linefeed mode automatic carriage return" { - var t = try init(testing.allocator, 10, 10); - defer t.deinit(testing.allocator); - - // Basic grid writing - t.modes.set(.linefeed, true); - try t.printString("123456"); - try t.linefeed(); - try t.print('X'); - { - const str = try t.plainString(testing.allocator); - defer testing.allocator.free(str); - try testing.expectEqualStrings("123456\nX", str); - } -} - -// X -test "Terminal: carriage return unsets pending wrap" { - var t = try init(testing.allocator, 5, 80); - defer t.deinit(testing.allocator); - - // Basic grid writing - for ("hello") |c| try t.print(c); - try testing.expect(t.screen.cursor.pending_wrap == true); - t.carriageReturn(); - try testing.expect(t.screen.cursor.pending_wrap == false); -} - -// X -test "Terminal: carriage return origin mode moves to left margin" { - var t = try init(testing.allocator, 5, 80); - defer t.deinit(testing.allocator); - - t.modes.set(.origin, true); - t.screen.cursor.x = 0; - t.scrolling_region.left = 2; - t.carriageReturn(); - try testing.expectEqual(@as(usize, 2), t.screen.cursor.x); -} - -// X -test "Terminal: carriage return left of left margin moves to zero" { - var t = try init(testing.allocator, 5, 80); - defer t.deinit(testing.allocator); - - t.screen.cursor.x = 1; - t.scrolling_region.left = 2; - t.carriageReturn(); - try testing.expectEqual(@as(usize, 0), t.screen.cursor.x); -} - -// X -test "Terminal: carriage return right of left margin moves to left margin" { - var t = try init(testing.allocator, 5, 80); - defer t.deinit(testing.allocator); - - t.screen.cursor.x = 3; - t.scrolling_region.left = 2; - t.carriageReturn(); - try testing.expectEqual(@as(usize, 2), t.screen.cursor.x); -} - -// X -test "Terminal: backspace" { - var t = try init(testing.allocator, 80, 80); - defer t.deinit(testing.allocator); - - // BS - for ("hello") |c| try t.print(c); - t.backspace(); - try t.print('y'); - try testing.expectEqual(@as(usize, 0), t.screen.cursor.y); - try testing.expectEqual(@as(usize, 5), t.screen.cursor.x); - { - const str = try t.plainString(testing.allocator); - defer testing.allocator.free(str); - try testing.expectEqualStrings("helly", str); - } -} - -// X -test "Terminal: horizontal tabs" { - const alloc = testing.allocator; - var t = try init(alloc, 20, 5); - defer t.deinit(alloc); - - // HT - try t.print('1'); - try t.horizontalTab(); - try testing.expectEqual(@as(usize, 8), t.screen.cursor.x); - - // HT - try t.horizontalTab(); - try testing.expectEqual(@as(usize, 16), t.screen.cursor.x); - - // HT at the end - try t.horizontalTab(); - try testing.expectEqual(@as(usize, 19), t.screen.cursor.x); - try t.horizontalTab(); - try testing.expectEqual(@as(usize, 19), t.screen.cursor.x); -} - -// X -test "Terminal: horizontal tabs starting on tabstop" { - const alloc = testing.allocator; - var t = try init(alloc, 20, 5); - defer t.deinit(alloc); - - t.screen.cursor.x = 8; - try t.print('X'); - t.screen.cursor.x = 8; - try t.horizontalTab(); - try t.print('A'); - - { - const str = try t.plainString(testing.allocator); - defer testing.allocator.free(str); - try testing.expectEqualStrings(" X A", str); - } -} - -// X -test "Terminal: horizontal tabs with right margin" { - const alloc = testing.allocator; - var t = try init(alloc, 20, 5); - defer t.deinit(alloc); - - t.scrolling_region.left = 2; - t.scrolling_region.right = 5; - t.screen.cursor.x = 0; - try t.print('X'); - try t.horizontalTab(); - try t.print('A'); - - { - const str = try t.plainString(testing.allocator); - defer testing.allocator.free(str); - try testing.expectEqualStrings("X A", str); - } -} - -// X -test "Terminal: horizontal tabs back" { - const alloc = testing.allocator; - var t = try init(alloc, 20, 5); - defer t.deinit(alloc); - - // Edge of screen - t.screen.cursor.x = 19; - - // HT - try t.horizontalTabBack(); - try testing.expectEqual(@as(usize, 16), t.screen.cursor.x); - - // HT - try t.horizontalTabBack(); - try testing.expectEqual(@as(usize, 8), t.screen.cursor.x); - - // HT - try t.horizontalTabBack(); - try testing.expectEqual(@as(usize, 0), t.screen.cursor.x); - try t.horizontalTabBack(); - try testing.expectEqual(@as(usize, 0), t.screen.cursor.x); -} - -// X -test "Terminal: horizontal tabs back starting on tabstop" { - const alloc = testing.allocator; - var t = try init(alloc, 20, 5); - defer t.deinit(alloc); - - t.screen.cursor.x = 8; - try t.print('X'); - t.screen.cursor.x = 8; - try t.horizontalTabBack(); - try t.print('A'); - - { - const str = try t.plainString(testing.allocator); - defer testing.allocator.free(str); - try testing.expectEqualStrings("A X", str); - } -} - -// X -test "Terminal: horizontal tabs with left margin in origin mode" { - const alloc = testing.allocator; - var t = try init(alloc, 20, 5); - defer t.deinit(alloc); - - t.modes.set(.origin, true); - t.scrolling_region.left = 2; - t.scrolling_region.right = 5; - t.screen.cursor.x = 3; - try t.print('X'); - try t.horizontalTabBack(); - try t.print('A'); - - { - const str = try t.plainString(testing.allocator); - defer testing.allocator.free(str); - try testing.expectEqualStrings(" AX", str); - } -} - -// X -test "Terminal: horizontal tab back with cursor before left margin" { - const alloc = testing.allocator; - var t = try init(alloc, 20, 5); - defer t.deinit(alloc); - - t.modes.set(.origin, true); - t.saveCursor(); - t.modes.set(.enable_left_and_right_margin, true); - t.setLeftAndRightMargin(5, 0); - t.restoreCursor(); - try t.horizontalTabBack(); - try t.print('X'); - - { - const str = try t.plainString(testing.allocator); - defer testing.allocator.free(str); - try testing.expectEqualStrings("X", str); - } -} - -// X -test "Terminal: cursorPos resets wrap" { - const alloc = testing.allocator; - var t = try init(alloc, 5, 5); - defer t.deinit(alloc); - - for ("ABCDE") |c| try t.print(c); - try testing.expect(t.screen.cursor.pending_wrap); - t.setCursorPos(1, 1); - try testing.expect(!t.screen.cursor.pending_wrap); - try t.print('X'); - - { - const str = try t.plainString(testing.allocator); - defer testing.allocator.free(str); - try testing.expectEqualStrings("XBCDE", str); - } -} - -// X -test "Terminal: cursorPos off the screen" { - const alloc = testing.allocator; - var t = try init(alloc, 5, 5); - defer t.deinit(alloc); - - t.setCursorPos(500, 500); - try t.print('X'); - - { - const str = try t.plainString(testing.allocator); - defer testing.allocator.free(str); - try testing.expectEqualStrings("\n\n\n\n X", str); - } -} - -// X -test "Terminal: cursorPos relative to origin" { - const alloc = testing.allocator; - var t = try init(alloc, 5, 5); - defer t.deinit(alloc); - - t.scrolling_region.top = 2; - t.scrolling_region.bottom = 3; - t.modes.set(.origin, true); - t.setCursorPos(1, 1); - try t.print('X'); - - { - const str = try t.plainString(testing.allocator); - defer testing.allocator.free(str); - try testing.expectEqualStrings("\n\nX", str); - } -} - -// X -test "Terminal: cursorPos relative to origin with left/right" { - const alloc = testing.allocator; - var t = try init(alloc, 5, 5); - defer t.deinit(alloc); - - t.scrolling_region.top = 2; - t.scrolling_region.bottom = 3; - t.scrolling_region.left = 2; - t.scrolling_region.right = 4; - t.modes.set(.origin, true); - t.setCursorPos(1, 1); - try t.print('X'); - - { - const str = try t.plainString(testing.allocator); - defer testing.allocator.free(str); - try testing.expectEqualStrings("\n\n X", str); - } -} - -// X -test "Terminal: cursorPos limits with full scroll region" { - const alloc = testing.allocator; - var t = try init(alloc, 5, 5); - defer t.deinit(alloc); - - t.scrolling_region.top = 2; - t.scrolling_region.bottom = 3; - t.scrolling_region.left = 2; - t.scrolling_region.right = 4; - t.modes.set(.origin, true); - t.setCursorPos(500, 500); - try t.print('X'); - - { - const str = try t.plainString(testing.allocator); - defer testing.allocator.free(str); - try testing.expectEqualStrings("\n\n\n X", str); - } -} - -// X -test "Terminal: setCursorPos (original test)" { - var t = try init(testing.allocator, 80, 80); - defer t.deinit(testing.allocator); - - try testing.expectEqual(@as(usize, 0), t.screen.cursor.x); - try testing.expectEqual(@as(usize, 0), t.screen.cursor.y); - - // Setting it to 0 should keep it zero (1 based) - t.setCursorPos(0, 0); - try testing.expectEqual(@as(usize, 0), t.screen.cursor.x); - try testing.expectEqual(@as(usize, 0), t.screen.cursor.y); - - // Should clamp to size - t.setCursorPos(81, 81); - try testing.expectEqual(@as(usize, 79), t.screen.cursor.x); - try testing.expectEqual(@as(usize, 79), t.screen.cursor.y); - - // Should reset pending wrap - t.setCursorPos(0, 80); - try t.print('c'); - try testing.expect(t.screen.cursor.pending_wrap); - t.setCursorPos(0, 80); - try testing.expect(!t.screen.cursor.pending_wrap); - - // Origin mode - t.modes.set(.origin, true); - - // No change without a scroll region - t.setCursorPos(81, 81); - try testing.expectEqual(@as(usize, 79), t.screen.cursor.x); - try testing.expectEqual(@as(usize, 79), t.screen.cursor.y); - - // Set the scroll region - t.setTopAndBottomMargin(10, t.rows); - t.setCursorPos(0, 0); - try testing.expectEqual(@as(usize, 0), t.screen.cursor.x); - try testing.expectEqual(@as(usize, 9), t.screen.cursor.y); - - t.setCursorPos(1, 1); - try testing.expectEqual(@as(usize, 0), t.screen.cursor.x); - try testing.expectEqual(@as(usize, 9), t.screen.cursor.y); - - t.setCursorPos(100, 0); - try testing.expectEqual(@as(usize, 0), t.screen.cursor.x); - try testing.expectEqual(@as(usize, 79), t.screen.cursor.y); - - t.setTopAndBottomMargin(10, 11); - t.setCursorPos(2, 0); - try testing.expectEqual(@as(usize, 0), t.screen.cursor.x); - try testing.expectEqual(@as(usize, 10), t.screen.cursor.y); -} - -// X -test "Terminal: setTopAndBottomMargin simple" { - const alloc = testing.allocator; - var t = try init(alloc, 5, 5); - defer t.deinit(alloc); - - try t.printString("ABC"); - t.carriageReturn(); - try t.linefeed(); - try t.printString("DEF"); - t.carriageReturn(); - try t.linefeed(); - try t.printString("GHI"); - t.setTopAndBottomMargin(0, 0); - try t.scrollDown(1); - - { - const str = try t.plainString(testing.allocator); - defer testing.allocator.free(str); - try testing.expectEqualStrings("\nABC\nDEF\nGHI", str); - } -} - -// X -test "Terminal: setTopAndBottomMargin top only" { - const alloc = testing.allocator; - var t = try init(alloc, 5, 5); - defer t.deinit(alloc); - - try t.printString("ABC"); - t.carriageReturn(); - try t.linefeed(); - try t.printString("DEF"); - t.carriageReturn(); - try t.linefeed(); - try t.printString("GHI"); - t.setTopAndBottomMargin(2, 0); - try t.scrollDown(1); - - { - const str = try t.plainString(testing.allocator); - defer testing.allocator.free(str); - try testing.expectEqualStrings("ABC\n\nDEF\nGHI", str); - } -} - -// X -test "Terminal: setTopAndBottomMargin top and bottom" { - const alloc = testing.allocator; - var t = try init(alloc, 5, 5); - defer t.deinit(alloc); - - try t.printString("ABC"); - t.carriageReturn(); - try t.linefeed(); - try t.printString("DEF"); - t.carriageReturn(); - try t.linefeed(); - try t.printString("GHI"); - t.setTopAndBottomMargin(1, 2); - try t.scrollDown(1); - - { - const str = try t.plainString(testing.allocator); - defer testing.allocator.free(str); - try testing.expectEqualStrings("\nABC\nGHI", str); - } -} - -// X -test "Terminal: setTopAndBottomMargin top equal to bottom" { - const alloc = testing.allocator; - var t = try init(alloc, 5, 5); - defer t.deinit(alloc); - - try t.printString("ABC"); - t.carriageReturn(); - try t.linefeed(); - try t.printString("DEF"); - t.carriageReturn(); - try t.linefeed(); - try t.printString("GHI"); - t.setTopAndBottomMargin(2, 2); - try t.scrollDown(1); - - { - const str = try t.plainString(testing.allocator); - defer testing.allocator.free(str); - try testing.expectEqualStrings("\nABC\nDEF\nGHI", str); - } -} - -// X -test "Terminal: setLeftAndRightMargin simple" { - const alloc = testing.allocator; - var t = try init(alloc, 5, 5); - defer t.deinit(alloc); - - try t.printString("ABC"); - t.carriageReturn(); - try t.linefeed(); - try t.printString("DEF"); - t.carriageReturn(); - try t.linefeed(); - try t.printString("GHI"); - t.modes.set(.enable_left_and_right_margin, true); - t.setLeftAndRightMargin(0, 0); - t.eraseChars(1); - - { - const str = try t.plainString(testing.allocator); - defer testing.allocator.free(str); - try testing.expectEqualStrings(" BC\nDEF\nGHI", str); - } -} - -// X -test "Terminal: setLeftAndRightMargin left only" { - const alloc = testing.allocator; - var t = try init(alloc, 5, 5); - defer t.deinit(alloc); - - try t.printString("ABC"); - t.carriageReturn(); - try t.linefeed(); - try t.printString("DEF"); - t.carriageReturn(); - try t.linefeed(); - try t.printString("GHI"); - t.modes.set(.enable_left_and_right_margin, true); - t.setLeftAndRightMargin(2, 0); - try testing.expectEqual(@as(usize, 1), t.scrolling_region.left); - try testing.expectEqual(@as(usize, t.cols - 1), t.scrolling_region.right); - t.setCursorPos(1, 2); - try t.insertLines(1); - - { - const str = try t.plainString(testing.allocator); - defer testing.allocator.free(str); - try testing.expectEqualStrings("A\nDBC\nGEF\n HI", str); - } -} - -// X -test "Terminal: setLeftAndRightMargin left and right" { - const alloc = testing.allocator; - var t = try init(alloc, 5, 5); - defer t.deinit(alloc); - - try t.printString("ABC"); - t.carriageReturn(); - try t.linefeed(); - try t.printString("DEF"); - t.carriageReturn(); - try t.linefeed(); - try t.printString("GHI"); - t.modes.set(.enable_left_and_right_margin, true); - t.setLeftAndRightMargin(1, 2); - t.setCursorPos(1, 2); - try t.insertLines(1); - - { - const str = try t.plainString(testing.allocator); - defer testing.allocator.free(str); - try testing.expectEqualStrings(" C\nABF\nDEI\nGH", str); - } -} - -// X -test "Terminal: setLeftAndRightMargin left equal right" { - const alloc = testing.allocator; - var t = try init(alloc, 5, 5); - defer t.deinit(alloc); - - try t.printString("ABC"); - t.carriageReturn(); - try t.linefeed(); - try t.printString("DEF"); - t.carriageReturn(); - try t.linefeed(); - try t.printString("GHI"); - t.modes.set(.enable_left_and_right_margin, true); - t.setLeftAndRightMargin(2, 2); - t.setCursorPos(1, 2); - try t.insertLines(1); - - { - const str = try t.plainString(testing.allocator); - defer testing.allocator.free(str); - try testing.expectEqualStrings("\nABC\nDEF\nGHI", str); - } -} - -// X -test "Terminal: setLeftAndRightMargin mode 69 unset" { - const alloc = testing.allocator; - var t = try init(alloc, 5, 5); - defer t.deinit(alloc); - - try t.printString("ABC"); - t.carriageReturn(); - try t.linefeed(); - try t.printString("DEF"); - t.carriageReturn(); - try t.linefeed(); - try t.printString("GHI"); - t.modes.set(.enable_left_and_right_margin, false); - t.setLeftAndRightMargin(1, 2); - t.setCursorPos(1, 2); - try t.insertLines(1); - - { - const str = try t.plainString(testing.allocator); - defer testing.allocator.free(str); - try testing.expectEqualStrings("\nABC\nDEF\nGHI", str); - } -} - -// X -test "Terminal: deleteLines" { - const alloc = testing.allocator; - var t = try init(alloc, 80, 80); - defer t.deinit(alloc); - - // Initial value - try t.print('A'); - t.carriageReturn(); - try t.linefeed(); - try t.print('B'); - t.carriageReturn(); - try t.linefeed(); - try t.print('C'); - t.carriageReturn(); - try t.linefeed(); - try t.print('D'); - - t.cursorUp(2); - try t.deleteLines(1); - - try t.print('E'); - t.carriageReturn(); - try t.linefeed(); - - // We should be - try testing.expectEqual(@as(usize, 0), t.screen.cursor.x); - try testing.expectEqual(@as(usize, 2), t.screen.cursor.y); - - { - const str = try t.plainString(testing.allocator); - defer testing.allocator.free(str); - try testing.expectEqualStrings("A\nE\nD", str); - } -} - -// X -test "Terminal: deleteLines with scroll region" { - const alloc = testing.allocator; - var t = try init(alloc, 80, 80); - defer t.deinit(alloc); - - // Initial value - try t.print('A'); - t.carriageReturn(); - try t.linefeed(); - try t.print('B'); - t.carriageReturn(); - try t.linefeed(); - try t.print('C'); - t.carriageReturn(); - try t.linefeed(); - try t.print('D'); - - t.setTopAndBottomMargin(1, 3); - t.setCursorPos(1, 1); - try t.deleteLines(1); - - try t.print('E'); - t.carriageReturn(); - try t.linefeed(); - - // We should be - // try testing.expectEqual(@as(usize, 0), t.screen.cursor.x); - // try testing.expectEqual(@as(usize, 2), t.screen.cursor.y); - - { - const str = try t.plainString(testing.allocator); - defer testing.allocator.free(str); - try testing.expectEqualStrings("E\nC\n\nD", str); - } -} - -// X -test "Terminal: deleteLines with scroll region, large count" { - const alloc = testing.allocator; - var t = try init(alloc, 80, 80); - defer t.deinit(alloc); - - // Initial value - try t.print('A'); - t.carriageReturn(); - try t.linefeed(); - try t.print('B'); - t.carriageReturn(); - try t.linefeed(); - try t.print('C'); - t.carriageReturn(); - try t.linefeed(); - try t.print('D'); - - t.setTopAndBottomMargin(1, 3); - t.setCursorPos(1, 1); - try t.deleteLines(5); - - try t.print('E'); - t.carriageReturn(); - try t.linefeed(); - - // We should be - // try testing.expectEqual(@as(usize, 0), t.screen.cursor.x); - // try testing.expectEqual(@as(usize, 2), t.screen.cursor.y); - - { - const str = try t.plainString(testing.allocator); - defer testing.allocator.free(str); - try testing.expectEqualStrings("E\n\n\nD", str); - } -} - -// X -test "Terminal: deleteLines with scroll region, cursor outside of region" { - const alloc = testing.allocator; - var t = try init(alloc, 80, 80); - defer t.deinit(alloc); - - // Initial value - try t.print('A'); - t.carriageReturn(); - try t.linefeed(); - try t.print('B'); - t.carriageReturn(); - try t.linefeed(); - try t.print('C'); - t.carriageReturn(); - try t.linefeed(); - try t.print('D'); - - t.setTopAndBottomMargin(1, 3); - t.setCursorPos(4, 1); - try t.deleteLines(1); - - { - const str = try t.plainString(testing.allocator); - defer testing.allocator.free(str); - try testing.expectEqualStrings("A\nB\nC\nD", str); - } -} - -// X -test "Terminal: deleteLines resets wrap" { - const alloc = testing.allocator; - var t = try init(alloc, 5, 5); - defer t.deinit(alloc); - - for ("ABCDE") |c| try t.print(c); - try testing.expect(t.screen.cursor.pending_wrap); - try t.deleteLines(1); - try testing.expect(!t.screen.cursor.pending_wrap); - try t.print('B'); - - { - const str = try t.plainString(testing.allocator); - defer testing.allocator.free(str); - try testing.expectEqualStrings("B", str); - } -} - -// X -test "Terminal: deleteLines simple" { - const alloc = testing.allocator; - var t = try init(alloc, 5, 5); - defer t.deinit(alloc); - - try t.printString("ABC"); - t.carriageReturn(); - try t.linefeed(); - try t.printString("DEF"); - t.carriageReturn(); - try t.linefeed(); - try t.printString("GHI"); - t.setCursorPos(2, 2); - try t.deleteLines(1); - - { - const str = try t.plainString(testing.allocator); - defer testing.allocator.free(str); - try testing.expectEqualStrings("ABC\nGHI", str); - } -} - -// X -test "Terminal: deleteLines left/right scroll region" { - const alloc = testing.allocator; - var t = try init(alloc, 10, 10); - defer t.deinit(alloc); - - try t.printString("ABC123"); - t.carriageReturn(); - try t.linefeed(); - try t.printString("DEF456"); - t.carriageReturn(); - try t.linefeed(); - try t.printString("GHI789"); - t.scrolling_region.left = 1; - t.scrolling_region.right = 3; - t.setCursorPos(2, 2); - try t.deleteLines(1); - - { - const str = try t.plainString(testing.allocator); - defer testing.allocator.free(str); - try testing.expectEqualStrings("ABC123\nDHI756\nG 89", str); - } -} - -test "Terminal: deleteLines left/right scroll region clears row wrap" { - const alloc = testing.allocator; - var t = try init(alloc, 5, 5); - defer t.deinit(alloc); - - try t.print('0'); - t.modes.set(.enable_left_and_right_margin, true); - t.setLeftAndRightMargin(2, 3); - try t.printRepeat(1000); - for (0..t.rows - 1) |y| { - const row = t.screen.getRow(.{ .active = y }); - try testing.expect(row.isWrapped()); - } - { - const row = t.screen.getRow(.{ .active = t.rows - 1 }); - try testing.expect(!row.isWrapped()); - } -} - -// X -test "Terminal: deleteLines left/right scroll region from top" { - const alloc = testing.allocator; - var t = try init(alloc, 10, 10); - defer t.deinit(alloc); - - try t.printString("ABC123"); - t.carriageReturn(); - try t.linefeed(); - try t.printString("DEF456"); - t.carriageReturn(); - try t.linefeed(); - try t.printString("GHI789"); - t.scrolling_region.left = 1; - t.scrolling_region.right = 3; - t.setCursorPos(1, 2); - try t.deleteLines(1); - - { - const str = try t.plainString(testing.allocator); - defer testing.allocator.free(str); - try testing.expectEqualStrings("AEF423\nDHI756\nG 89", str); - } -} - -// X -test "Terminal: deleteLines left/right scroll region high count" { - const alloc = testing.allocator; - var t = try init(alloc, 10, 10); - defer t.deinit(alloc); - - try t.printString("ABC123"); - t.carriageReturn(); - try t.linefeed(); - try t.printString("DEF456"); - t.carriageReturn(); - try t.linefeed(); - try t.printString("GHI789"); - t.scrolling_region.left = 1; - t.scrolling_region.right = 3; - t.setCursorPos(2, 2); - try t.deleteLines(100); - - { - const str = try t.plainString(testing.allocator); - defer testing.allocator.free(str); - try testing.expectEqualStrings("ABC123\nD 56\nG 89", str); - } -} - -// X -test "Terminal: insertLines simple" { - const alloc = testing.allocator; - var t = try init(alloc, 5, 5); - defer t.deinit(alloc); - - try t.printString("ABC"); - t.carriageReturn(); - try t.linefeed(); - try t.printString("DEF"); - t.carriageReturn(); - try t.linefeed(); - try t.printString("GHI"); - t.setCursorPos(2, 2); - try t.insertLines(1); - - { - const str = try t.plainString(testing.allocator); - defer testing.allocator.free(str); - try testing.expectEqualStrings("ABC\n\nDEF\nGHI", str); - } -} - -// X -test "Terminal: insertLines outside of scroll region" { - const alloc = testing.allocator; - var t = try init(alloc, 5, 5); - defer t.deinit(alloc); - - try t.printString("ABC"); - t.carriageReturn(); - try t.linefeed(); - try t.printString("DEF"); - t.carriageReturn(); - try t.linefeed(); - try t.printString("GHI"); - t.setTopAndBottomMargin(3, 4); - t.setCursorPos(2, 2); - try t.insertLines(1); - - { - const str = try t.plainString(testing.allocator); - defer testing.allocator.free(str); - try testing.expectEqualStrings("ABC\nDEF\nGHI", str); - } -} - -// X -test "Terminal: insertLines top/bottom scroll region" { - const alloc = testing.allocator; - var t = try init(alloc, 5, 5); - defer t.deinit(alloc); - - try t.printString("ABC"); - t.carriageReturn(); - try t.linefeed(); - try t.printString("DEF"); - t.carriageReturn(); - try t.linefeed(); - try t.printString("GHI"); - t.carriageReturn(); - try t.linefeed(); - try t.printString("123"); - t.setTopAndBottomMargin(1, 3); - t.setCursorPos(2, 2); - try t.insertLines(1); - - { - const str = try t.plainString(testing.allocator); - defer testing.allocator.free(str); - try testing.expectEqualStrings("ABC\n\nDEF\n123", str); - } -} - -// X -test "Terminal: insertLines left/right scroll region" { - const alloc = testing.allocator; - var t = try init(alloc, 10, 10); - defer t.deinit(alloc); - - try t.printString("ABC123"); - t.carriageReturn(); - try t.linefeed(); - try t.printString("DEF456"); - t.carriageReturn(); - try t.linefeed(); - try t.printString("GHI789"); - t.scrolling_region.left = 1; - t.scrolling_region.right = 3; - t.setCursorPos(2, 2); - try t.insertLines(1); - - { - const str = try t.plainString(testing.allocator); - defer testing.allocator.free(str); - try testing.expectEqualStrings("ABC123\nD 56\nGEF489\n HI7", str); - } -} - -// X -test "Terminal: insertLines" { - const alloc = testing.allocator; - var t = try init(alloc, 2, 5); - defer t.deinit(alloc); - - // Initial value - try t.print('A'); - t.carriageReturn(); - try t.linefeed(); - try t.print('B'); - t.carriageReturn(); - try t.linefeed(); - try t.print('C'); - t.carriageReturn(); - try t.linefeed(); - try t.print('D'); - t.carriageReturn(); - try t.linefeed(); - try t.print('E'); - - // Move to row 2 - t.setCursorPos(2, 1); - - // Insert two lines - try t.insertLines(2); - - { - const str = try t.plainString(testing.allocator); - defer testing.allocator.free(str); - try testing.expectEqualStrings("A\n\n\nB\nC", str); - } -} - -// X -test "Terminal: insertLines zero" { - const alloc = testing.allocator; - var t = try init(alloc, 2, 5); - defer t.deinit(alloc); - - // This should do nothing - t.setCursorPos(1, 1); - try t.insertLines(0); -} - -// X -test "Terminal: insertLines with scroll region" { - const alloc = testing.allocator; - var t = try init(alloc, 2, 6); - defer t.deinit(alloc); - - // Initial value - try t.print('A'); - t.carriageReturn(); - try t.linefeed(); - try t.print('B'); - t.carriageReturn(); - try t.linefeed(); - try t.print('C'); - t.carriageReturn(); - try t.linefeed(); - try t.print('D'); - t.carriageReturn(); - try t.linefeed(); - try t.print('E'); - - t.setTopAndBottomMargin(1, 2); - t.setCursorPos(1, 1); - try t.insertLines(1); - - try t.print('X'); - - { - const str = try t.plainString(testing.allocator); - defer testing.allocator.free(str); - try testing.expectEqualStrings("X\nA\nC\nD\nE", str); - } -} - -// X -test "Terminal: insertLines more than remaining" { - const alloc = testing.allocator; - var t = try init(alloc, 2, 5); - defer t.deinit(alloc); - - // Initial value - try t.print('A'); - t.carriageReturn(); - try t.linefeed(); - try t.print('B'); - t.carriageReturn(); - try t.linefeed(); - try t.print('C'); - t.carriageReturn(); - try t.linefeed(); - try t.print('D'); - t.carriageReturn(); - try t.linefeed(); - try t.print('E'); - - // Move to row 2 - t.setCursorPos(2, 1); - - // Insert a bunch of lines - try t.insertLines(20); - - { - const str = try t.plainString(testing.allocator); - defer testing.allocator.free(str); - try testing.expectEqualStrings("A", str); - } -} - -// X -test "Terminal: insertLines resets wrap" { - const alloc = testing.allocator; - var t = try init(alloc, 5, 5); - defer t.deinit(alloc); - - for ("ABCDE") |c| try t.print(c); - try testing.expect(t.screen.cursor.pending_wrap); - try t.insertLines(1); - try testing.expect(!t.screen.cursor.pending_wrap); - try t.print('B'); - - { - const str = try t.plainString(testing.allocator); - defer testing.allocator.free(str); - try testing.expectEqualStrings("B\nABCDE", str); - } -} - -// X -test "Terminal: reverseIndex" { - const alloc = testing.allocator; - var t = try init(alloc, 2, 5); - defer t.deinit(alloc); - - // Initial value - try t.print('A'); - t.carriageReturn(); - try t.linefeed(); - try t.print('B'); - t.carriageReturn(); - try t.linefeed(); - try t.print('C'); - try t.reverseIndex(); - try t.print('D'); - t.carriageReturn(); - try t.linefeed(); - t.carriageReturn(); - try t.linefeed(); - - { - const str = try t.plainString(testing.allocator); - defer testing.allocator.free(str); - try testing.expectEqualStrings("A\nBD\nC", str); - } -} - -// X -test "Terminal: reverseIndex from the top" { - const alloc = testing.allocator; - var t = try init(alloc, 2, 5); - defer t.deinit(alloc); - - try t.print('A'); - t.carriageReturn(); - try t.linefeed(); - try t.print('B'); - t.carriageReturn(); - try t.linefeed(); - t.carriageReturn(); - try t.linefeed(); - - t.setCursorPos(1, 1); - try t.reverseIndex(); - try t.print('D'); - - t.carriageReturn(); - try t.linefeed(); - t.setCursorPos(1, 1); - try t.reverseIndex(); - try t.print('E'); - t.carriageReturn(); - try t.linefeed(); - - { - const str = try t.plainString(testing.allocator); - defer testing.allocator.free(str); - try testing.expectEqualStrings("E\nD\nA\nB", str); - } -} - -// X -test "Terminal: reverseIndex top of scrolling region" { - const alloc = testing.allocator; - var t = try init(alloc, 2, 10); - defer t.deinit(alloc); - - // Initial value - t.setCursorPos(2, 1); - try t.print('A'); - t.carriageReturn(); - try t.linefeed(); - try t.print('B'); - t.carriageReturn(); - try t.linefeed(); - try t.print('C'); - t.carriageReturn(); - try t.linefeed(); - try t.print('D'); - t.carriageReturn(); - try t.linefeed(); - - // Set our scroll region - t.setTopAndBottomMargin(2, 5); - t.setCursorPos(2, 1); - try t.reverseIndex(); - try t.print('X'); - - { - const str = try t.plainString(testing.allocator); - defer testing.allocator.free(str); - try testing.expectEqualStrings("\nX\nA\nB\nC", str); - } -} - -// X -test "Terminal: reverseIndex top of screen" { - const alloc = testing.allocator; - var t = try init(alloc, 5, 5); - defer t.deinit(alloc); - - try t.print('A'); - t.setCursorPos(2, 1); - try t.print('B'); - t.setCursorPos(3, 1); - try t.print('C'); - t.setCursorPos(1, 1); - try t.reverseIndex(); - try t.print('X'); - - { - const str = try t.plainString(testing.allocator); - defer testing.allocator.free(str); - try testing.expectEqualStrings("X\nA\nB\nC", str); - } -} - -// X -test "Terminal: reverseIndex not top of screen" { - const alloc = testing.allocator; - var t = try init(alloc, 5, 5); - defer t.deinit(alloc); - - try t.print('A'); - t.setCursorPos(2, 1); - try t.print('B'); - t.setCursorPos(3, 1); - try t.print('C'); - t.setCursorPos(2, 1); - try t.reverseIndex(); - try t.print('X'); - - { - const str = try t.plainString(testing.allocator); - defer testing.allocator.free(str); - try testing.expectEqualStrings("X\nB\nC", str); - } -} - -// X -test "Terminal: reverseIndex top/bottom margins" { - const alloc = testing.allocator; - var t = try init(alloc, 5, 5); - defer t.deinit(alloc); - - try t.print('A'); - t.setCursorPos(2, 1); - try t.print('B'); - t.setCursorPos(3, 1); - try t.print('C'); - t.setTopAndBottomMargin(2, 3); - t.setCursorPos(2, 1); - try t.reverseIndex(); - - { - const str = try t.plainString(testing.allocator); - defer testing.allocator.free(str); - try testing.expectEqualStrings("A\n\nB", str); - } -} - -// X -test "Terminal: reverseIndex outside top/bottom margins" { - const alloc = testing.allocator; - var t = try init(alloc, 5, 5); - defer t.deinit(alloc); - - try t.print('A'); - t.setCursorPos(2, 1); - try t.print('B'); - t.setCursorPos(3, 1); - try t.print('C'); - t.setTopAndBottomMargin(2, 3); - t.setCursorPos(1, 1); - try t.reverseIndex(); - - { - const str = try t.plainString(testing.allocator); - defer testing.allocator.free(str); - try testing.expectEqualStrings("A\nB\nC", str); - } -} - -// X -test "Terminal: reverseIndex left/right margins" { - const alloc = testing.allocator; - var t = try init(alloc, 5, 5); - defer t.deinit(alloc); - - try t.printString("ABC"); - t.setCursorPos(2, 1); - try t.printString("DEF"); - t.setCursorPos(3, 1); - try t.printString("GHI"); - t.modes.set(.enable_left_and_right_margin, true); - t.setLeftAndRightMargin(2, 3); - t.setCursorPos(1, 2); - try t.reverseIndex(); - - { - const str = try t.plainString(testing.allocator); - defer testing.allocator.free(str); - try testing.expectEqualStrings("A\nDBC\nGEF\n HI", str); - } -} - -// X -test "Terminal: reverseIndex outside left/right margins" { - const alloc = testing.allocator; - var t = try init(alloc, 5, 5); - defer t.deinit(alloc); - - try t.printString("ABC"); - t.setCursorPos(2, 1); - try t.printString("DEF"); - t.setCursorPos(3, 1); - try t.printString("GHI"); - t.modes.set(.enable_left_and_right_margin, true); - t.setLeftAndRightMargin(2, 3); - t.setCursorPos(1, 1); - try t.reverseIndex(); - - { - const str = try t.plainString(testing.allocator); - defer testing.allocator.free(str); - try testing.expectEqualStrings("ABC\nDEF\nGHI", str); - } -} - -// X -test "Terminal: index" { - const alloc = testing.allocator; - var t = try init(alloc, 2, 5); - defer t.deinit(alloc); - - try t.index(); - try t.print('A'); - - { - const str = try t.plainString(testing.allocator); - defer testing.allocator.free(str); - try testing.expectEqualStrings("\nA", str); - } -} - -// X -test "Terminal: index from the bottom" { - const alloc = testing.allocator; - var t = try init(alloc, 2, 5); - defer t.deinit(alloc); - - t.setCursorPos(5, 1); - try t.print('A'); - t.cursorLeft(1); // undo moving right from 'A' - try t.index(); - - try t.print('B'); - - { - const str = try t.plainString(testing.allocator); - defer testing.allocator.free(str); - try testing.expectEqualStrings("\n\n\nA\nB", str); - } -} - -// X -test "Terminal: index outside of scrolling region" { - const alloc = testing.allocator; - var t = try init(alloc, 2, 5); - defer t.deinit(alloc); - - try testing.expectEqual(@as(usize, 0), t.screen.cursor.y); - t.setTopAndBottomMargin(2, 5); - try t.index(); - try testing.expectEqual(@as(usize, 1), t.screen.cursor.y); -} - -// X -test "Terminal: index from the bottom outside of scroll region" { - const alloc = testing.allocator; - var t = try init(alloc, 2, 5); - defer t.deinit(alloc); - - t.setTopAndBottomMargin(1, 2); - t.setCursorPos(5, 1); - try t.print('A'); - try t.index(); - try t.print('B'); - - { - const str = try t.plainString(testing.allocator); - defer testing.allocator.free(str); - try testing.expectEqualStrings("\n\n\n\nAB", str); - } -} - -// X -test "Terminal: index no scroll region, top of screen" { - const alloc = testing.allocator; - var t = try init(alloc, 5, 5); - defer t.deinit(alloc); - - try t.print('A'); - try t.index(); - try t.print('X'); - - { - const str = try t.plainString(testing.allocator); - defer testing.allocator.free(str); - try testing.expectEqualStrings("A\n X", str); - } -} - -// X -test "Terminal: index bottom of primary screen" { - const alloc = testing.allocator; - var t = try init(alloc, 5, 5); - defer t.deinit(alloc); - - t.setCursorPos(5, 1); - try t.print('A'); - try t.index(); - try t.print('X'); - - { - const str = try t.plainString(testing.allocator); - defer testing.allocator.free(str); - try testing.expectEqualStrings("\n\n\nA\n X", str); - } -} - -// X -test "Terminal: index bottom of primary screen background sgr" { - const alloc = testing.allocator; - var t = try init(alloc, 5, 5); - defer t.deinit(alloc); - - const pen: Screen.Cell = .{ - .bg = .{ .rgb = .{ .r = 0xFF, .g = 0x00, .b = 0x00 } }, - }; - - t.setCursorPos(5, 1); - try t.print('A'); - t.screen.cursor.pen = pen; - try t.index(); - - { - const str = try t.plainString(testing.allocator); - defer testing.allocator.free(str); - try testing.expectEqualStrings("\n\n\nA", str); - for (0..5) |x| { - const cell = t.screen.getCell(.active, 4, x); - try testing.expectEqual(pen, cell); - } - } -} - -// X -test "Terminal: index inside scroll region" { - const alloc = testing.allocator; - var t = try init(alloc, 5, 5); - defer t.deinit(alloc); - - t.setTopAndBottomMargin(1, 3); - try t.print('A'); - try t.index(); - try t.print('X'); - - { - const str = try t.plainString(testing.allocator); - defer testing.allocator.free(str); - try testing.expectEqualStrings("A\n X", str); - } -} - -// X -test "Terminal: index bottom of scroll region" { - const alloc = testing.allocator; - var t = try init(alloc, 5, 5); - defer t.deinit(alloc); - - t.setTopAndBottomMargin(1, 3); - t.setCursorPos(4, 1); - try t.print('B'); - t.setCursorPos(3, 1); - try t.print('A'); - try t.index(); - try t.print('X'); - - { - const str = try t.plainString(testing.allocator); - defer testing.allocator.free(str); - try testing.expectEqualStrings("\nA\n X\nB", str); - } -} - -// X -test "Terminal: index bottom of primary screen with scroll region" { - const alloc = testing.allocator; - var t = try init(alloc, 5, 5); - defer t.deinit(alloc); - - t.setTopAndBottomMargin(1, 3); - t.setCursorPos(3, 1); - try t.print('A'); - t.setCursorPos(5, 1); - try t.index(); - try t.index(); - try t.index(); - try t.print('X'); - - { - const str = try t.plainString(testing.allocator); - defer testing.allocator.free(str); - try testing.expectEqualStrings("\n\nA\n\nX", str); - } -} - -// X -test "Terminal: index outside left/right margin" { - const alloc = testing.allocator; - var t = try init(alloc, 10, 5); - defer t.deinit(alloc); - - t.setTopAndBottomMargin(1, 3); - t.scrolling_region.left = 3; - t.scrolling_region.right = 5; - t.setCursorPos(3, 3); - try t.print('A'); - t.setCursorPos(3, 1); - try t.index(); - try t.print('X'); - - { - const str = try t.plainString(testing.allocator); - defer testing.allocator.free(str); - try testing.expectEqualStrings("\n\nX A", str); - } -} - -// X -test "Terminal: index inside left/right margin" { - const alloc = testing.allocator; - var t = try init(alloc, 10, 5); - defer t.deinit(alloc); - - try t.printString("AAAAAA"); - t.carriageReturn(); - try t.linefeed(); - try t.printString("AAAAAA"); - t.carriageReturn(); - try t.linefeed(); - try t.printString("AAAAAA"); - t.modes.set(.enable_left_and_right_margin, true); - t.setTopAndBottomMargin(1, 3); - t.setLeftAndRightMargin(1, 3); - t.setCursorPos(3, 1); - try t.index(); - - try testing.expectEqual(@as(usize, 2), t.screen.cursor.y); - try testing.expectEqual(@as(usize, 0), t.screen.cursor.x); - - { - const str = try t.plainString(testing.allocator); - defer testing.allocator.free(str); - try testing.expectEqualStrings("AAAAAA\nAAAAAA\n AAA", str); - } -} - -// X -test "Terminal: DECALN" { - const alloc = testing.allocator; - var t = try init(alloc, 2, 2); - defer t.deinit(alloc); - - // Initial value - try t.print('A'); - t.carriageReturn(); - try t.linefeed(); - try t.print('B'); - try t.decaln(); - - try testing.expectEqual(@as(usize, 0), t.screen.cursor.y); - try testing.expectEqual(@as(usize, 0), t.screen.cursor.x); - - { - const str = try t.plainString(testing.allocator); - defer testing.allocator.free(str); - try testing.expectEqualStrings("EE\nEE", str); - } -} - -// X -test "Terminal: decaln reset margins" { - const alloc = testing.allocator; - var t = try init(alloc, 3, 3); - defer t.deinit(alloc); - - // Initial value - t.modes.set(.origin, true); - t.setTopAndBottomMargin(2, 3); - try t.decaln(); - try t.scrollDown(1); - - { - const str = try t.plainString(testing.allocator); - defer testing.allocator.free(str); - try testing.expectEqualStrings("\nEEE\nEEE", str); - } -} - -// X -test "Terminal: decaln preserves color" { - const alloc = testing.allocator; - var t = try init(alloc, 3, 3); - defer t.deinit(alloc); - - const pen: Screen.Cell = .{ - .bg = .{ .rgb = .{ .r = 0xFF, .g = 0x00, .b = 0x00 } }, - }; - - // Initial value - t.screen.cursor.pen = pen; - t.modes.set(.origin, true); - t.setTopAndBottomMargin(2, 3); - try t.decaln(); - try t.scrollDown(1); - - { - const str = try t.plainString(testing.allocator); - defer testing.allocator.free(str); - try testing.expectEqualStrings("\nEEE\nEEE", str); - const cell = t.screen.getCell(.active, 0, 0); - try testing.expectEqual(pen, cell); - } -} - -// X -test "Terminal: insertBlanks" { - // NOTE: this is not verified with conformance tests, so these - // tests might actually be verifying wrong behavior. - const alloc = testing.allocator; - var t = try init(alloc, 5, 2); - defer t.deinit(alloc); - - try t.print('A'); - try t.print('B'); - try t.print('C'); - t.screen.cursor.pen.attrs.bold = true; - t.setCursorPos(1, 1); - t.insertBlanks(2); - - { - const str = try t.plainString(testing.allocator); - defer testing.allocator.free(str); - try testing.expectEqualStrings(" ABC", str); - const cell = t.screen.getCell(.active, 0, 0); - try testing.expect(!cell.attrs.bold); - } -} - -// X -test "Terminal: insertBlanks pushes off end" { - // NOTE: this is not verified with conformance tests, so these - // tests might actually be verifying wrong behavior. - const alloc = testing.allocator; - var t = try init(alloc, 3, 2); - defer t.deinit(alloc); - - try t.print('A'); - try t.print('B'); - try t.print('C'); - t.setCursorPos(1, 1); - t.insertBlanks(2); - - { - const str = try t.plainString(testing.allocator); - defer testing.allocator.free(str); - try testing.expectEqualStrings(" A", str); - } -} - -// X -test "Terminal: insertBlanks more than size" { - // NOTE: this is not verified with conformance tests, so these - // tests might actually be verifying wrong behavior. - const alloc = testing.allocator; - var t = try init(alloc, 3, 2); - defer t.deinit(alloc); - - try t.print('A'); - try t.print('B'); - try t.print('C'); - t.setCursorPos(1, 1); - t.insertBlanks(5); - - { - const str = try t.plainString(testing.allocator); - defer testing.allocator.free(str); - try testing.expectEqualStrings("", str); - } -} - -// X -test "Terminal: insertBlanks no scroll region, fits" { - const alloc = testing.allocator; - var t = try init(alloc, 10, 10); - defer t.deinit(alloc); - - for ("ABC") |c| try t.print(c); - t.setCursorPos(1, 1); - t.insertBlanks(2); - - { - const str = try t.plainString(testing.allocator); - defer testing.allocator.free(str); - try testing.expectEqualStrings(" ABC", str); - } -} - -// X -test "Terminal: insertBlanks preserves background sgr" { - const alloc = testing.allocator; - var t = try init(alloc, 10, 10); - defer t.deinit(alloc); - - const pen: Screen.Cell = .{ - .bg = .{ .rgb = .{ .r = 0xFF, .g = 0x00, .b = 0x00 } }, - }; - - for ("ABC") |c| try t.print(c); - t.setCursorPos(1, 1); - t.screen.cursor.pen = pen; - t.insertBlanks(2); - - { - const str = try t.plainString(testing.allocator); - defer testing.allocator.free(str); - try testing.expectEqualStrings(" ABC", str); - const cell = t.screen.getCell(.active, 0, 0); - try testing.expectEqual(pen, cell); - } -} - -// X -test "Terminal: insertBlanks shift off screen" { - const alloc = testing.allocator; - var t = try init(alloc, 5, 10); - defer t.deinit(alloc); - - for (" ABC") |c| try t.print(c); - t.setCursorPos(1, 3); - t.insertBlanks(2); - try t.print('X'); - - { - const str = try t.plainString(testing.allocator); - defer testing.allocator.free(str); - try testing.expectEqualStrings(" X A", str); - } -} - -// X -test "Terminal: insertBlanks split multi-cell character" { - const alloc = testing.allocator; - var t = try init(alloc, 5, 10); - defer t.deinit(alloc); - - for ("123") |c| try t.print(c); - try t.print('橋'); - t.setCursorPos(1, 1); - t.insertBlanks(1); - - { - const str = try t.plainString(testing.allocator); - defer testing.allocator.free(str); - try testing.expectEqualStrings(" 123", str); - } -} - -// X -test "Terminal: insertBlanks inside left/right scroll region" { - const alloc = testing.allocator; - var t = try init(alloc, 10, 10); - defer t.deinit(alloc); - - t.scrolling_region.left = 2; - t.scrolling_region.right = 4; - t.setCursorPos(1, 3); - for ("ABC") |c| try t.print(c); - t.setCursorPos(1, 3); - t.insertBlanks(2); - try t.print('X'); - - { - const str = try t.plainString(testing.allocator); - defer testing.allocator.free(str); - try testing.expectEqualStrings(" X A", str); - } -} - -// X -test "Terminal: insertBlanks outside left/right scroll region" { - const alloc = testing.allocator; - var t = try init(alloc, 6, 10); - defer t.deinit(alloc); - - t.setCursorPos(1, 4); - for ("ABC") |c| try t.print(c); - t.scrolling_region.left = 2; - t.scrolling_region.right = 4; - try testing.expect(t.screen.cursor.pending_wrap); - t.insertBlanks(2); - try testing.expect(!t.screen.cursor.pending_wrap); - try t.print('X'); - - { - const str = try t.plainString(testing.allocator); - defer testing.allocator.free(str); - try testing.expectEqualStrings(" ABX", str); - } -} - -// X -test "Terminal: insertBlanks left/right scroll region large count" { - const alloc = testing.allocator; - var t = try init(alloc, 10, 10); - defer t.deinit(alloc); - - t.modes.set(.origin, true); - t.modes.set(.enable_left_and_right_margin, true); - t.setLeftAndRightMargin(3, 5); - t.setCursorPos(1, 1); - t.insertBlanks(140); - try t.print('X'); - - { - const str = try t.plainString(testing.allocator); - defer testing.allocator.free(str); - try testing.expectEqualStrings(" X", str); - } -} - -// X -test "Terminal: insert mode with space" { - const alloc = testing.allocator; - var t = try init(alloc, 10, 2); - defer t.deinit(alloc); - - for ("hello") |c| try t.print(c); - t.setCursorPos(1, 2); - t.modes.set(.insert, true); - try t.print('X'); - - { - const str = try t.plainString(testing.allocator); - defer testing.allocator.free(str); - try testing.expectEqualStrings("hXello", str); - } -} - -// X -test "Terminal: insert mode doesn't wrap pushed characters" { - const alloc = testing.allocator; - var t = try init(alloc, 5, 2); - defer t.deinit(alloc); - - for ("hello") |c| try t.print(c); - t.setCursorPos(1, 2); - t.modes.set(.insert, true); - try t.print('X'); - - { - const str = try t.plainString(testing.allocator); - defer testing.allocator.free(str); - try testing.expectEqualStrings("hXell", str); - } -} - -// X -test "Terminal: insert mode does nothing at the end of the line" { - const alloc = testing.allocator; - var t = try init(alloc, 5, 2); - defer t.deinit(alloc); - - for ("hello") |c| try t.print(c); - t.modes.set(.insert, true); - try t.print('X'); - - { - const str = try t.plainString(testing.allocator); - defer testing.allocator.free(str); - try testing.expectEqualStrings("hello\nX", str); - } -} - -// X -test "Terminal: insert mode with wide characters" { - const alloc = testing.allocator; - var t = try init(alloc, 5, 2); - defer t.deinit(alloc); - - for ("hello") |c| try t.print(c); - t.setCursorPos(1, 2); - t.modes.set(.insert, true); - try t.print('😀'); // 0x1F600 - - { - const str = try t.plainString(testing.allocator); - defer testing.allocator.free(str); - try testing.expectEqualStrings("h😀el", str); - } -} - -// X -test "Terminal: insert mode with wide characters at end" { - const alloc = testing.allocator; - var t = try init(alloc, 5, 2); - defer t.deinit(alloc); - - for ("well") |c| try t.print(c); - t.modes.set(.insert, true); - try t.print('😀'); // 0x1F600 - - { - const str = try t.plainString(testing.allocator); - defer testing.allocator.free(str); - try testing.expectEqualStrings("well\n😀", str); - } -} - -// X -test "Terminal: insert mode pushing off wide character" { - const alloc = testing.allocator; - var t = try init(alloc, 5, 2); - defer t.deinit(alloc); - - for ("123") |c| try t.print(c); - try t.print('😀'); // 0x1F600 - t.modes.set(.insert, true); - t.setCursorPos(1, 1); - try t.print('X'); - - { - const str = try t.plainString(testing.allocator); - defer testing.allocator.free(str); - try testing.expectEqualStrings("X123", str); - } -} - -// X -test "Terminal: cursorIsAtPrompt" { - const alloc = testing.allocator; - var t = try init(alloc, 3, 2); - defer t.deinit(alloc); - - try testing.expect(!t.cursorIsAtPrompt()); - t.markSemanticPrompt(.prompt); - try testing.expect(t.cursorIsAtPrompt()); - - // Input is also a prompt - t.markSemanticPrompt(.input); - try testing.expect(t.cursorIsAtPrompt()); - - // Newline -- we expect we're still at a prompt if we received - // prompt stuff before. - try t.linefeed(); - try testing.expect(t.cursorIsAtPrompt()); - - // But once we say we're starting output, we're not a prompt - t.markSemanticPrompt(.command); - try testing.expect(!t.cursorIsAtPrompt()); - try t.linefeed(); - try testing.expect(!t.cursorIsAtPrompt()); - - // Until we know we're at a prompt again - try t.linefeed(); - t.markSemanticPrompt(.prompt); - try testing.expect(t.cursorIsAtPrompt()); -} - -// X -test "Terminal: cursorIsAtPrompt alternate screen" { - const alloc = testing.allocator; - var t = try init(alloc, 3, 2); - defer t.deinit(alloc); - - try testing.expect(!t.cursorIsAtPrompt()); - t.markSemanticPrompt(.prompt); - try testing.expect(t.cursorIsAtPrompt()); - - // Secondary screen is never a prompt - t.alternateScreen(alloc, .{}); - try testing.expect(!t.cursorIsAtPrompt()); - t.markSemanticPrompt(.prompt); - try testing.expect(!t.cursorIsAtPrompt()); -} - -// X -test "Terminal: print wide char with 1-column width" { - const alloc = testing.allocator; - var t = try init(alloc, 1, 2); - defer t.deinit(alloc); - - try t.print('😀'); // 0x1F600 -} - -// X -test "Terminal: deleteChars" { - const alloc = testing.allocator; - var t = try init(alloc, 5, 5); - defer t.deinit(alloc); - - for ("ABCDE") |c| try t.print(c); - t.setCursorPos(1, 2); - - // the cells that shifted in should not have this attribute set - t.screen.cursor.pen = .{ .attrs = .{ .bold = true } }; - - try t.deleteChars(2); - { - const str = try t.plainString(testing.allocator); - defer testing.allocator.free(str); - try testing.expectEqualStrings("ADE", str); - - const cell = t.screen.getCell(.active, 0, 4); - try testing.expect(!cell.attrs.bold); - } -} - -// X -test "Terminal: deleteChars zero count" { - const alloc = testing.allocator; - var t = try init(alloc, 5, 5); - defer t.deinit(alloc); - - for ("ABCDE") |c| try t.print(c); - t.setCursorPos(1, 2); - - try t.deleteChars(0); - { - const str = try t.plainString(testing.allocator); - defer testing.allocator.free(str); - try testing.expectEqualStrings("ABCDE", str); - } -} - -// X -test "Terminal: deleteChars more than half" { - const alloc = testing.allocator; - var t = try init(alloc, 5, 5); - defer t.deinit(alloc); - - for ("ABCDE") |c| try t.print(c); - t.setCursorPos(1, 2); - - try t.deleteChars(3); - { - const str = try t.plainString(testing.allocator); - defer testing.allocator.free(str); - try testing.expectEqualStrings("AE", str); - } -} - -// X -test "Terminal: deleteChars more than line width" { - const alloc = testing.allocator; - var t = try init(alloc, 5, 5); - defer t.deinit(alloc); - - for ("ABCDE") |c| try t.print(c); - t.setCursorPos(1, 2); - - try t.deleteChars(10); - { - const str = try t.plainString(testing.allocator); - defer testing.allocator.free(str); - try testing.expectEqualStrings("A", str); - } -} - -// X -test "Terminal: deleteChars should shift left" { - const alloc = testing.allocator; - var t = try init(alloc, 5, 5); - defer t.deinit(alloc); - - for ("ABCDE") |c| try t.print(c); - t.setCursorPos(1, 2); - - try t.deleteChars(1); - { - const str = try t.plainString(testing.allocator); - defer testing.allocator.free(str); - try testing.expectEqualStrings("ACDE", str); - } -} - -// X -test "Terminal: deleteChars resets wrap" { - const alloc = testing.allocator; - var t = try init(alloc, 5, 5); - defer t.deinit(alloc); - - for ("ABCDE") |c| try t.print(c); - try testing.expect(t.screen.cursor.pending_wrap); - try t.deleteChars(1); - try testing.expect(!t.screen.cursor.pending_wrap); - try t.print('X'); - - { - const str = try t.plainString(testing.allocator); - defer testing.allocator.free(str); - try testing.expectEqualStrings("ABCDX", str); - } -} - -// X -test "Terminal: deleteChars simple operation" { - const alloc = testing.allocator; - var t = try init(alloc, 10, 10); - defer t.deinit(alloc); - - try t.printString("ABC123"); - t.setCursorPos(1, 3); - try t.deleteChars(2); - - { - const str = try t.plainString(testing.allocator); - defer testing.allocator.free(str); - try testing.expectEqualStrings("AB23", str); - } -} - -// X -test "Terminal: deleteChars background sgr" { - const alloc = testing.allocator; - var t = try init(alloc, 10, 10); - defer t.deinit(alloc); - - const pen: Screen.Cell = .{ - .bg = .{ .rgb = .{ .r = 0xFF, .g = 0x00, .b = 0x00 } }, - }; - - try t.printString("ABC123"); - t.setCursorPos(1, 3); - t.screen.cursor.pen = pen; - try t.deleteChars(2); - - { - const str = try t.plainString(testing.allocator); - defer testing.allocator.free(str); - try testing.expectEqualStrings("AB23", str); - for (t.cols - 2..t.cols) |x| { - const cell = t.screen.getCell(.active, 0, x); - try testing.expectEqual(pen, cell); - } - } -} - -// X -test "Terminal: deleteChars outside scroll region" { - const alloc = testing.allocator; - var t = try init(alloc, 6, 10); - defer t.deinit(alloc); - - try t.printString("ABC123"); - t.scrolling_region.left = 2; - t.scrolling_region.right = 4; - try testing.expect(t.screen.cursor.pending_wrap); - try t.deleteChars(2); - try testing.expect(t.screen.cursor.pending_wrap); - - { - const str = try t.plainString(testing.allocator); - defer testing.allocator.free(str); - try testing.expectEqualStrings("ABC123", str); - } -} - -// X -test "Terminal: deleteChars inside scroll region" { - const alloc = testing.allocator; - var t = try init(alloc, 6, 10); - defer t.deinit(alloc); - - try t.printString("ABC123"); - t.scrolling_region.left = 2; - t.scrolling_region.right = 4; - t.setCursorPos(1, 4); - try t.deleteChars(1); - - { - const str = try t.plainString(testing.allocator); - defer testing.allocator.free(str); - try testing.expectEqualStrings("ABC2 3", str); - } -} - -// X -test "Terminal: deleteChars split wide character" { - const alloc = testing.allocator; - var t = try init(alloc, 6, 10); - defer t.deinit(alloc); - - try t.printString("A橋123"); - t.setCursorPos(1, 3); - try t.deleteChars(1); - - { - const str = try t.plainString(testing.allocator); - defer testing.allocator.free(str); - try testing.expectEqualStrings("A 123", str); - } -} - -// X -test "Terminal: deleteChars split wide character tail" { - const alloc = testing.allocator; - var t = try init(alloc, 5, 5); - defer t.deinit(alloc); - - t.setCursorPos(1, t.cols - 1); - try t.print(0x6A4B); // 橋 - t.carriageReturn(); - try t.deleteChars(t.cols - 1); - try t.print('0'); - - { - const str = try t.plainString(testing.allocator); - defer testing.allocator.free(str); - try testing.expectEqualStrings("0", str); - } -} - -// X -test "Terminal: eraseChars resets pending wrap" { - const alloc = testing.allocator; - var t = try init(alloc, 5, 5); - defer t.deinit(alloc); - - for ("ABCDE") |c| try t.print(c); - try testing.expect(t.screen.cursor.pending_wrap); - t.eraseChars(1); - try testing.expect(!t.screen.cursor.pending_wrap); - try t.print('X'); - - { - const str = try t.plainString(testing.allocator); - defer testing.allocator.free(str); - try testing.expectEqualStrings("ABCDX", str); - } -} - -// X -test "Terminal: eraseChars resets wrap" { - const alloc = testing.allocator; - var t = try init(alloc, 5, 5); - defer t.deinit(alloc); - - for ("ABCDE123") |c| try t.print(c); - { - const row = t.screen.getRow(.{ .active = 0 }); - try testing.expect(row.isWrapped()); - } - - t.setCursorPos(1, 1); - t.eraseChars(1); - - { - const row = t.screen.getRow(.{ .active = 0 }); - try testing.expect(!row.isWrapped()); - } - try t.print('X'); - - { - const str = try t.plainString(testing.allocator); - defer testing.allocator.free(str); - try testing.expectEqualStrings("XBCDE\n123", str); - } -} - -// X -test "Terminal: eraseChars simple operation" { - const alloc = testing.allocator; - var t = try init(alloc, 5, 5); - defer t.deinit(alloc); - - for ("ABC") |c| try t.print(c); - t.setCursorPos(1, 1); - t.eraseChars(2); - try t.print('X'); - - { - const str = try t.plainString(testing.allocator); - defer testing.allocator.free(str); - try testing.expectEqualStrings("X C", str); - } -} - -// X -test "Terminal: eraseChars minimum one" { - const alloc = testing.allocator; - var t = try init(alloc, 5, 5); - defer t.deinit(alloc); - - for ("ABC") |c| try t.print(c); - t.setCursorPos(1, 1); - t.eraseChars(0); - try t.print('X'); - - { - const str = try t.plainString(testing.allocator); - defer testing.allocator.free(str); - try testing.expectEqualStrings("XBC", str); - } -} - -// X -test "Terminal: eraseChars beyond screen edge" { - const alloc = testing.allocator; - var t = try init(alloc, 5, 5); - defer t.deinit(alloc); - - for (" ABC") |c| try t.print(c); - t.setCursorPos(1, 4); - t.eraseChars(10); - - { - const str = try t.plainString(testing.allocator); - defer testing.allocator.free(str); - try testing.expectEqualStrings(" A", str); - } -} - -// X -test "Terminal: eraseChars preserves background sgr" { - const alloc = testing.allocator; - var t = try init(alloc, 10, 10); - defer t.deinit(alloc); - - const pen: Screen.Cell = .{ - .bg = .{ .rgb = .{ .r = 0xFF, .g = 0x00, .b = 0x00 } }, - }; - - for ("ABC") |c| try t.print(c); - t.setCursorPos(1, 1); - t.screen.cursor.pen = pen; - t.eraseChars(2); - - { - const str = try t.plainString(testing.allocator); - defer testing.allocator.free(str); - try testing.expectEqualStrings(" C", str); - { - const cell = t.screen.getCell(.active, 0, 0); - try testing.expectEqual(pen, cell); - } - { - const cell = t.screen.getCell(.active, 0, 1); - try testing.expectEqual(pen, cell); - } - } -} - -// X -test "Terminal: eraseChars wide character" { - const alloc = testing.allocator; - var t = try init(alloc, 5, 5); - defer t.deinit(alloc); - - try t.print('橋'); - for ("BC") |c| try t.print(c); - t.setCursorPos(1, 1); - t.eraseChars(1); - try t.print('X'); - - { - const str = try t.plainString(testing.allocator); - defer testing.allocator.free(str); - try testing.expectEqualStrings("X BC", str); - } -} - -// X -test "Terminal: eraseChars protected attributes respected with iso" { - const alloc = testing.allocator; - var t = try init(alloc, 5, 5); - defer t.deinit(alloc); - - t.setProtectedMode(.iso); - for ("ABC") |c| try t.print(c); - t.setCursorPos(1, 1); - t.eraseChars(2); - - { - const str = try t.plainString(testing.allocator); - defer testing.allocator.free(str); - try testing.expectEqualStrings("ABC", str); - } -} - -// X -test "Terminal: eraseChars protected attributes ignored with dec most recent" { - const alloc = testing.allocator; - var t = try init(alloc, 5, 5); - defer t.deinit(alloc); - - t.setProtectedMode(.iso); - for ("ABC") |c| try t.print(c); - t.setProtectedMode(.dec); - t.setProtectedMode(.off); - t.setCursorPos(1, 1); - t.eraseChars(2); - - { - const str = try t.plainString(testing.allocator); - defer testing.allocator.free(str); - try testing.expectEqualStrings(" C", str); - } -} - -// X -test "Terminal: eraseChars protected attributes ignored with dec set" { - const alloc = testing.allocator; - var t = try init(alloc, 5, 5); - defer t.deinit(alloc); - - t.setProtectedMode(.dec); - for ("ABC") |c| try t.print(c); - t.setCursorPos(1, 1); - t.eraseChars(2); - - { - const str = try t.plainString(testing.allocator); - defer testing.allocator.free(str); - try testing.expectEqualStrings(" C", str); - } -} - -// X -// https://github.com/mitchellh/ghostty/issues/272 -// This is also tested in depth in screen resize tests but I want to keep -// this test around to ensure we don't regress at multiple layers. -test "Terminal: resize less cols with wide char then print" { - const alloc = testing.allocator; - var t = try init(alloc, 3, 3); - defer t.deinit(alloc); - - try t.print('x'); - try t.print('😀'); // 0x1F600 - try t.resize(alloc, 2, 3); - t.setCursorPos(1, 2); - try t.print('😀'); // 0x1F600 -} - -// X -// https://github.com/mitchellh/ghostty/issues/723 -// This was found via fuzzing so its highly specific. -test "Terminal: resize with left and right margin set" { - const alloc = testing.allocator; - const cols = 70; - const rows = 23; - var t = try init(alloc, cols, rows); - defer t.deinit(alloc); - - t.modes.set(.enable_left_and_right_margin, true); - try t.print('0'); - t.modes.set(.enable_mode_3, true); - try t.resize(alloc, cols, rows); - t.setLeftAndRightMargin(2, 0); - try t.printRepeat(1850); - _ = t.modes.restore(.enable_mode_3); - try t.resize(alloc, cols, rows); -} - -// X -// https://github.com/mitchellh/ghostty/issues/1343 -test "Terminal: resize with wraparound off" { - const alloc = testing.allocator; - const cols = 4; - const rows = 2; - var t = try init(alloc, cols, rows); - defer t.deinit(alloc); - - t.modes.set(.wraparound, false); - try t.print('0'); - try t.print('1'); - try t.print('2'); - try t.print('3'); - const new_cols = 2; - try t.resize(alloc, new_cols, rows); - - const str = try t.plainString(testing.allocator); - defer testing.allocator.free(str); - try testing.expectEqualStrings("01", str); -} - -// X -test "Terminal: resize with wraparound on" { - const alloc = testing.allocator; - const cols = 4; - const rows = 2; - var t = try init(alloc, cols, rows); - defer t.deinit(alloc); - - t.modes.set(.wraparound, true); - try t.print('0'); - try t.print('1'); - try t.print('2'); - try t.print('3'); - const new_cols = 2; - try t.resize(alloc, new_cols, rows); - - const str = try t.plainString(testing.allocator); - defer testing.allocator.free(str); - try testing.expectEqualStrings("01\n23", str); -} - -// X -test "Terminal: saveCursor" { - const alloc = testing.allocator; - var t = try init(alloc, 3, 3); - defer t.deinit(alloc); - - t.screen.cursor.pen.attrs.bold = true; - t.screen.charset.gr = .G3; - t.modes.set(.origin, true); - t.saveCursor(); - t.screen.charset.gr = .G0; - t.screen.cursor.pen.attrs.bold = false; - t.modes.set(.origin, false); - t.restoreCursor(); - try testing.expect(t.screen.cursor.pen.attrs.bold); - try testing.expect(t.screen.charset.gr == .G3); - try testing.expect(t.modes.get(.origin)); -} - -// X -test "Terminal: saveCursor with screen change" { - const alloc = testing.allocator; - var t = try init(alloc, 3, 3); - defer t.deinit(alloc); - - t.screen.cursor.pen.attrs.bold = true; - t.screen.cursor.x = 2; - t.screen.charset.gr = .G3; - t.modes.set(.origin, true); - t.alternateScreen(alloc, .{ - .cursor_save = true, - .clear_on_enter = true, - }); - // make sure our cursor and charset have come with us - try testing.expect(t.screen.cursor.pen.attrs.bold); - try testing.expect(t.screen.cursor.x == 2); - try testing.expect(t.screen.charset.gr == .G3); - try testing.expect(t.modes.get(.origin)); - t.screen.charset.gr = .G0; - t.screen.cursor.pen.attrs.bold = false; - t.modes.set(.origin, false); - t.primaryScreen(alloc, .{ - .cursor_save = true, - .clear_on_enter = true, - }); - try testing.expect(t.screen.cursor.pen.attrs.bold); - try testing.expect(t.screen.charset.gr == .G3); - try testing.expect(t.modes.get(.origin)); -} - -// X -test "Terminal: saveCursor position" { - const alloc = testing.allocator; - var t = try init(alloc, 10, 5); - defer t.deinit(alloc); - - t.setCursorPos(1, 5); - try t.print('A'); - t.saveCursor(); - t.setCursorPos(1, 1); - try t.print('B'); - t.restoreCursor(); - try t.print('X'); - - { - const str = try t.plainString(testing.allocator); - defer testing.allocator.free(str); - try testing.expectEqualStrings("B AX", str); - } -} - -// X -test "Terminal: saveCursor pending wrap state" { - const alloc = testing.allocator; - var t = try init(alloc, 5, 5); - defer t.deinit(alloc); - - t.setCursorPos(1, 5); - try t.print('A'); - t.saveCursor(); - t.setCursorPos(1, 1); - try t.print('B'); - t.restoreCursor(); - try t.print('X'); - - { - const str = try t.plainString(testing.allocator); - defer testing.allocator.free(str); - try testing.expectEqualStrings("B A\nX", str); - } -} - -// X -test "Terminal: saveCursor origin mode" { - const alloc = testing.allocator; - var t = try init(alloc, 10, 5); - defer t.deinit(alloc); - - t.modes.set(.origin, true); - t.saveCursor(); - t.modes.set(.enable_left_and_right_margin, true); - t.setLeftAndRightMargin(3, 5); - t.setTopAndBottomMargin(2, 4); - t.restoreCursor(); - try t.print('X'); - - { - const str = try t.plainString(testing.allocator); - defer testing.allocator.free(str); - try testing.expectEqualStrings("X", str); - } -} - -// X -test "Terminal: saveCursor resize" { - const alloc = testing.allocator; - var t = try init(alloc, 10, 5); - defer t.deinit(alloc); - - t.setCursorPos(1, 10); - t.saveCursor(); - try t.resize(alloc, 5, 5); - t.restoreCursor(); - try t.print('X'); - - { - const str = try t.plainString(testing.allocator); - defer testing.allocator.free(str); - try testing.expectEqualStrings(" X", str); - } -} - -// X -test "Terminal: saveCursor protected pen" { - const alloc = testing.allocator; - var t = try init(alloc, 10, 5); - defer t.deinit(alloc); - - t.setProtectedMode(.iso); - try testing.expect(t.screen.cursor.pen.attrs.protected); - t.setCursorPos(1, 10); - t.saveCursor(); - t.setProtectedMode(.off); - try testing.expect(!t.screen.cursor.pen.attrs.protected); - t.restoreCursor(); - try testing.expect(t.screen.cursor.pen.attrs.protected); -} - -// X -test "Terminal: setProtectedMode" { - const alloc = testing.allocator; - var t = try init(alloc, 3, 3); - defer t.deinit(alloc); - - try testing.expect(!t.screen.cursor.pen.attrs.protected); - t.setProtectedMode(.off); - try testing.expect(!t.screen.cursor.pen.attrs.protected); - t.setProtectedMode(.iso); - try testing.expect(t.screen.cursor.pen.attrs.protected); - t.setProtectedMode(.dec); - try testing.expect(t.screen.cursor.pen.attrs.protected); - t.setProtectedMode(.off); - try testing.expect(!t.screen.cursor.pen.attrs.protected); -} - -// X -test "Terminal: eraseLine simple erase right" { - const alloc = testing.allocator; - var t = try init(alloc, 5, 5); - defer t.deinit(alloc); - - for ("ABCDE") |c| try t.print(c); - t.setCursorPos(1, 3); - t.eraseLine(.right, false); - - { - const str = try t.plainString(testing.allocator); - defer testing.allocator.free(str); - try testing.expectEqualStrings("AB", str); - } -} - -// X -test "Terminal: eraseLine resets pending wrap" { - const alloc = testing.allocator; - var t = try init(alloc, 5, 5); - defer t.deinit(alloc); - - for ("ABCDE") |c| try t.print(c); - try testing.expect(t.screen.cursor.pending_wrap); - t.eraseLine(.right, false); - try testing.expect(!t.screen.cursor.pending_wrap); - try t.print('B'); - - { - const str = try t.plainString(testing.allocator); - defer testing.allocator.free(str); - try testing.expectEqualStrings("ABCDB", str); - } -} - -// X -test "Terminal: eraseLine resets wrap" { - const alloc = testing.allocator; - var t = try init(alloc, 5, 5); - defer t.deinit(alloc); - - for ("ABCDE123") |c| try t.print(c); - { - const row = t.screen.getRow(.{ .active = 0 }); - try testing.expect(row.isWrapped()); - } - - t.setCursorPos(1, 1); - t.eraseLine(.right, false); - - { - const row = t.screen.getRow(.{ .active = 0 }); - try testing.expect(!row.isWrapped()); - } - try t.print('X'); - - { - const str = try t.plainString(testing.allocator); - defer testing.allocator.free(str); - try testing.expectEqualStrings("X\n123", str); - } -} - -// X -test "Terminal: eraseLine right preserves background sgr" { - const alloc = testing.allocator; - var t = try init(alloc, 5, 5); - defer t.deinit(alloc); - - const pen: Screen.Cell = .{ - .bg = .{ .rgb = .{ .r = 0xFF, .g = 0x00, .b = 0x00 } }, - }; - - for ("ABCDE") |c| try t.print(c); - t.setCursorPos(1, 2); - t.screen.cursor.pen = pen; - t.eraseLine(.right, false); - - { - const str = try t.plainString(testing.allocator); - defer testing.allocator.free(str); - try testing.expectEqualStrings("A", str); - for (1..5) |x| { - const cell = t.screen.getCell(.active, 0, x); - try testing.expectEqual(pen, cell); - } - } -} - -// X -test "Terminal: eraseLine right wide character" { - const alloc = testing.allocator; - var t = try init(alloc, 10, 5); - defer t.deinit(alloc); - - for ("AB") |c| try t.print(c); - try t.print('橋'); - for ("DE") |c| try t.print(c); - t.setCursorPos(1, 4); - t.eraseLine(.right, false); - - { - const str = try t.plainString(testing.allocator); - defer testing.allocator.free(str); - try testing.expectEqualStrings("AB", str); - } -} - -// X -test "Terminal: eraseLine right protected attributes respected with iso" { - const alloc = testing.allocator; - var t = try init(alloc, 5, 5); - defer t.deinit(alloc); - - t.setProtectedMode(.iso); - for ("ABC") |c| try t.print(c); - t.setCursorPos(1, 1); - t.eraseLine(.right, false); - - { - const str = try t.plainString(testing.allocator); - defer testing.allocator.free(str); - try testing.expectEqualStrings("ABC", str); - } -} - -// X -test "Terminal: eraseLine right protected attributes ignored with dec most recent" { - const alloc = testing.allocator; - var t = try init(alloc, 5, 5); - defer t.deinit(alloc); - - t.setProtectedMode(.iso); - for ("ABC") |c| try t.print(c); - t.setProtectedMode(.dec); - t.setProtectedMode(.off); - t.setCursorPos(1, 2); - t.eraseLine(.right, false); - - { - const str = try t.plainString(testing.allocator); - defer testing.allocator.free(str); - try testing.expectEqualStrings("A", str); - } -} - -// X -test "Terminal: eraseLine right protected attributes ignored with dec set" { - const alloc = testing.allocator; - var t = try init(alloc, 5, 5); - defer t.deinit(alloc); - - t.setProtectedMode(.dec); - for ("ABC") |c| try t.print(c); - t.setCursorPos(1, 2); - t.eraseLine(.right, false); - - { - const str = try t.plainString(testing.allocator); - defer testing.allocator.free(str); - try testing.expectEqualStrings("A", str); - } -} - -// X -test "Terminal: eraseLine right protected requested" { - const alloc = testing.allocator; - var t = try init(alloc, 10, 5); - defer t.deinit(alloc); - - for ("12345678") |c| try t.print(c); - t.setCursorPos(t.screen.cursor.y + 1, 6); - t.setProtectedMode(.dec); - try t.print('X'); - t.setCursorPos(t.screen.cursor.y + 1, 4); - t.eraseLine(.right, true); - - { - const str = try t.plainString(testing.allocator); - defer testing.allocator.free(str); - try testing.expectEqualStrings("123 X", str); - } -} - -// X -test "Terminal: eraseLine simple erase left" { - const alloc = testing.allocator; - var t = try init(alloc, 5, 5); - defer t.deinit(alloc); - - for ("ABCDE") |c| try t.print(c); - t.setCursorPos(1, 3); - t.eraseLine(.left, false); - - { - const str = try t.plainString(testing.allocator); - defer testing.allocator.free(str); - try testing.expectEqualStrings(" DE", str); - } -} - -// X -test "Terminal: eraseLine left resets wrap" { - const alloc = testing.allocator; - var t = try init(alloc, 5, 5); - defer t.deinit(alloc); - - for ("ABCDE") |c| try t.print(c); - try testing.expect(t.screen.cursor.pending_wrap); - t.eraseLine(.left, false); - try testing.expect(!t.screen.cursor.pending_wrap); - try t.print('B'); - - { - const str = try t.plainString(testing.allocator); - defer testing.allocator.free(str); - try testing.expectEqualStrings(" B", str); - } -} - -// X -test "Terminal: eraseLine left preserves background sgr" { - const alloc = testing.allocator; - var t = try init(alloc, 5, 5); - defer t.deinit(alloc); - - const pen: Screen.Cell = .{ - .bg = .{ .rgb = .{ .r = 0xFF, .g = 0x00, .b = 0x00 } }, - }; - - for ("ABCDE") |c| try t.print(c); - t.setCursorPos(1, 2); - t.screen.cursor.pen = pen; - t.eraseLine(.left, false); - - { - const str = try t.plainString(testing.allocator); - defer testing.allocator.free(str); - try testing.expectEqualStrings(" CDE", str); - for (0..2) |x| { - const cell = t.screen.getCell(.active, 0, x); - try testing.expectEqual(pen, cell); - } - } -} - -// X -test "Terminal: eraseLine left wide character" { - const alloc = testing.allocator; - var t = try init(alloc, 10, 5); - defer t.deinit(alloc); - - for ("AB") |c| try t.print(c); - try t.print('橋'); - for ("DE") |c| try t.print(c); - t.setCursorPos(1, 3); - t.eraseLine(.left, false); - - { - const str = try t.plainString(testing.allocator); - defer testing.allocator.free(str); - try testing.expectEqualStrings(" DE", str); - } -} - -// X -test "Terminal: eraseLine left protected attributes respected with iso" { - const alloc = testing.allocator; - var t = try init(alloc, 5, 5); - defer t.deinit(alloc); - - t.setProtectedMode(.iso); - for ("ABC") |c| try t.print(c); - t.setCursorPos(1, 1); - t.eraseLine(.left, false); - - { - const str = try t.plainString(testing.allocator); - defer testing.allocator.free(str); - try testing.expectEqualStrings("ABC", str); - } -} - -// X -test "Terminal: eraseLine left protected attributes ignored with dec most recent" { - const alloc = testing.allocator; - var t = try init(alloc, 5, 5); - defer t.deinit(alloc); - - t.setProtectedMode(.iso); - for ("ABC") |c| try t.print(c); - t.setProtectedMode(.dec); - t.setProtectedMode(.off); - t.setCursorPos(1, 2); - t.eraseLine(.left, false); - - { - const str = try t.plainString(testing.allocator); - defer testing.allocator.free(str); - try testing.expectEqualStrings(" C", str); - } -} - -// X -test "Terminal: eraseLine left protected attributes ignored with dec set" { - const alloc = testing.allocator; - var t = try init(alloc, 5, 5); - defer t.deinit(alloc); - - t.setProtectedMode(.dec); - for ("ABC") |c| try t.print(c); - t.setCursorPos(1, 2); - t.eraseLine(.left, false); - - { - const str = try t.plainString(testing.allocator); - defer testing.allocator.free(str); - try testing.expectEqualStrings(" C", str); - } -} - -// X -test "Terminal: eraseLine left protected requested" { - const alloc = testing.allocator; - var t = try init(alloc, 10, 5); - defer t.deinit(alloc); - - for ("123456789") |c| try t.print(c); - t.setCursorPos(t.screen.cursor.y + 1, 6); - t.setProtectedMode(.dec); - try t.print('X'); - t.setCursorPos(t.screen.cursor.y + 1, 8); - t.eraseLine(.left, true); - - { - const str = try t.plainString(testing.allocator); - defer testing.allocator.free(str); - try testing.expectEqualStrings(" X 9", str); - } -} - -// X -test "Terminal: eraseLine complete preserves background sgr" { - const alloc = testing.allocator; - var t = try init(alloc, 5, 5); - defer t.deinit(alloc); - - const pen: Screen.Cell = .{ - .bg = .{ .rgb = .{ .r = 0xFF, .g = 0x00, .b = 0x00 } }, - }; - - for ("ABCDE") |c| try t.print(c); - t.setCursorPos(1, 2); - t.screen.cursor.pen = pen; - t.eraseLine(.complete, false); - - { - const str = try t.plainString(testing.allocator); - defer testing.allocator.free(str); - try testing.expectEqualStrings("", str); - for (0..5) |x| { - const cell = t.screen.getCell(.active, 0, x); - try testing.expectEqual(pen, cell); - } - } -} - -// X -test "Terminal: eraseLine complete protected attributes respected with iso" { - const alloc = testing.allocator; - var t = try init(alloc, 5, 5); - defer t.deinit(alloc); - - t.setProtectedMode(.iso); - for ("ABC") |c| try t.print(c); - t.setCursorPos(1, 1); - t.eraseLine(.complete, false); - - { - const str = try t.plainString(testing.allocator); - defer testing.allocator.free(str); - try testing.expectEqualStrings("ABC", str); - } -} - -// X -test "Terminal: eraseLine complete protected attributes ignored with dec most recent" { - const alloc = testing.allocator; - var t = try init(alloc, 5, 5); - defer t.deinit(alloc); - - t.setProtectedMode(.iso); - for ("ABC") |c| try t.print(c); - t.setProtectedMode(.dec); - t.setProtectedMode(.off); - t.setCursorPos(1, 2); - t.eraseLine(.complete, false); - - { - const str = try t.plainString(testing.allocator); - defer testing.allocator.free(str); - try testing.expectEqualStrings("", str); - } -} - -// X -test "Terminal: eraseLine complete protected attributes ignored with dec set" { - const alloc = testing.allocator; - var t = try init(alloc, 5, 5); - defer t.deinit(alloc); - - t.setProtectedMode(.dec); - for ("ABC") |c| try t.print(c); - t.setCursorPos(1, 2); - t.eraseLine(.complete, false); - - { - const str = try t.plainString(testing.allocator); - defer testing.allocator.free(str); - try testing.expectEqualStrings("", str); - } -} - -// X -test "Terminal: eraseLine complete protected requested" { - const alloc = testing.allocator; - var t = try init(alloc, 10, 5); - defer t.deinit(alloc); - - for ("123456789") |c| try t.print(c); - t.setCursorPos(t.screen.cursor.y + 1, 6); - t.setProtectedMode(.dec); - try t.print('X'); - t.setCursorPos(t.screen.cursor.y + 1, 8); - t.eraseLine(.complete, true); - - { - const str = try t.plainString(testing.allocator); - defer testing.allocator.free(str); - try testing.expectEqualStrings(" X", str); - } -} - -// X -test "Terminal: eraseDisplay simple erase below" { - const alloc = testing.allocator; - var t = try init(alloc, 5, 5); - defer t.deinit(alloc); - - for ("ABC") |c| try t.print(c); - t.carriageReturn(); - try t.linefeed(); - for ("DEF") |c| try t.print(c); - t.carriageReturn(); - try t.linefeed(); - for ("GHI") |c| try t.print(c); - t.setCursorPos(2, 2); - t.eraseDisplay(alloc, .below, false); - - { - const str = try t.plainString(testing.allocator); - defer testing.allocator.free(str); - try testing.expectEqualStrings("ABC\nD", str); - } -} - -// X -test "Terminal: eraseDisplay erase below preserves SGR bg" { - const alloc = testing.allocator; - var t = try init(alloc, 5, 5); - defer t.deinit(alloc); - - for ("ABC") |c| try t.print(c); - t.carriageReturn(); - try t.linefeed(); - for ("DEF") |c| try t.print(c); - t.carriageReturn(); - try t.linefeed(); - for ("GHI") |c| try t.print(c); - t.setCursorPos(2, 2); - - const pen: Screen.Cell = .{ - .bg = .{ .rgb = .{ .r = 0xFF, .g = 0x00, .b = 0x00 } }, - }; - - t.screen.cursor.pen = pen; - t.eraseDisplay(alloc, .below, false); - - { - const str = try t.plainString(testing.allocator); - defer testing.allocator.free(str); - try testing.expectEqualStrings("ABC\nD", str); - for (1..5) |x| { - const cell = t.screen.getCell(.active, 1, x); - try testing.expectEqual(pen, cell); - } - } -} - -// X -test "Terminal: eraseDisplay below split multi-cell" { - const alloc = testing.allocator; - var t = try init(alloc, 5, 5); - defer t.deinit(alloc); - - try t.printString("AB橋C"); - t.carriageReturn(); - try t.linefeed(); - try t.printString("DE橋F"); - t.carriageReturn(); - try t.linefeed(); - try t.printString("GH橋I"); - t.setCursorPos(2, 4); - t.eraseDisplay(alloc, .below, false); - - { - const str = try t.plainString(testing.allocator); - defer testing.allocator.free(str); - try testing.expectEqualStrings("AB橋C\nDE", str); - } -} - -// X -test "Terminal: eraseDisplay below protected attributes respected with iso" { - const alloc = testing.allocator; - var t = try init(alloc, 5, 5); - defer t.deinit(alloc); - - t.setProtectedMode(.iso); - for ("ABC") |c| try t.print(c); - t.carriageReturn(); - try t.linefeed(); - for ("DEF") |c| try t.print(c); - t.carriageReturn(); - try t.linefeed(); - for ("GHI") |c| try t.print(c); - t.setCursorPos(2, 2); - t.eraseDisplay(alloc, .below, false); - - { - const str = try t.plainString(testing.allocator); - defer testing.allocator.free(str); - try testing.expectEqualStrings("ABC\nDEF\nGHI", str); - } -} - -// X -test "Terminal: eraseDisplay below protected attributes ignored with dec most recent" { - const alloc = testing.allocator; - var t = try init(alloc, 5, 5); - defer t.deinit(alloc); - - t.setProtectedMode(.iso); - for ("ABC") |c| try t.print(c); - t.carriageReturn(); - try t.linefeed(); - for ("DEF") |c| try t.print(c); - t.carriageReturn(); - try t.linefeed(); - for ("GHI") |c| try t.print(c); - t.setProtectedMode(.dec); - t.setProtectedMode(.off); - t.setCursorPos(2, 2); - t.eraseDisplay(alloc, .below, false); - - { - const str = try t.plainString(testing.allocator); - defer testing.allocator.free(str); - try testing.expectEqualStrings("ABC\nD", str); - } -} - -// X -test "Terminal: eraseDisplay below protected attributes ignored with dec set" { - const alloc = testing.allocator; - var t = try init(alloc, 5, 5); - defer t.deinit(alloc); - - t.setProtectedMode(.dec); - for ("ABC") |c| try t.print(c); - t.carriageReturn(); - try t.linefeed(); - for ("DEF") |c| try t.print(c); - t.carriageReturn(); - try t.linefeed(); - for ("GHI") |c| try t.print(c); - t.setCursorPos(2, 2); - t.eraseDisplay(alloc, .below, false); - - { - const str = try t.plainString(testing.allocator); - defer testing.allocator.free(str); - try testing.expectEqualStrings("ABC\nD", str); - } -} - -// X -test "Terminal: eraseDisplay simple erase above" { - const alloc = testing.allocator; - var t = try init(alloc, 5, 5); - defer t.deinit(alloc); - - for ("ABC") |c| try t.print(c); - t.carriageReturn(); - try t.linefeed(); - for ("DEF") |c| try t.print(c); - t.carriageReturn(); - try t.linefeed(); - for ("GHI") |c| try t.print(c); - t.setCursorPos(2, 2); - t.eraseDisplay(alloc, .above, false); - - { - const str = try t.plainString(testing.allocator); - defer testing.allocator.free(str); - try testing.expectEqualStrings("\n F\nGHI", str); - } -} - -// X -test "Terminal: eraseDisplay below protected attributes respected with force" { - const alloc = testing.allocator; - var t = try init(alloc, 5, 5); - defer t.deinit(alloc); - - t.setProtectedMode(.dec); - for ("ABC") |c| try t.print(c); - t.carriageReturn(); - try t.linefeed(); - for ("DEF") |c| try t.print(c); - t.carriageReturn(); - try t.linefeed(); - for ("GHI") |c| try t.print(c); - t.setCursorPos(2, 2); - t.eraseDisplay(alloc, .below, true); - - { - const str = try t.plainString(testing.allocator); - defer testing.allocator.free(str); - try testing.expectEqualStrings("ABC\nDEF\nGHI", str); - } -} - -// X -test "Terminal: eraseDisplay erase above preserves SGR bg" { - const alloc = testing.allocator; - var t = try init(alloc, 5, 5); - defer t.deinit(alloc); - - for ("ABC") |c| try t.print(c); - t.carriageReturn(); - try t.linefeed(); - for ("DEF") |c| try t.print(c); - t.carriageReturn(); - try t.linefeed(); - for ("GHI") |c| try t.print(c); - t.setCursorPos(2, 2); - - const pen: Screen.Cell = .{ - .bg = .{ .rgb = .{ .r = 0xFF, .g = 0x00, .b = 0x00 } }, - }; - - t.screen.cursor.pen = pen; - t.eraseDisplay(alloc, .above, false); - - { - const str = try t.plainString(testing.allocator); - defer testing.allocator.free(str); - try testing.expectEqualStrings("\n F\nGHI", str); - for (0..2) |x| { - const cell = t.screen.getCell(.active, 1, x); - try testing.expectEqual(pen, cell); - } - } -} - -// X -test "Terminal: eraseDisplay above split multi-cell" { - const alloc = testing.allocator; - var t = try init(alloc, 5, 5); - defer t.deinit(alloc); - - try t.printString("AB橋C"); - t.carriageReturn(); - try t.linefeed(); - try t.printString("DE橋F"); - t.carriageReturn(); - try t.linefeed(); - try t.printString("GH橋I"); - t.setCursorPos(2, 3); - t.eraseDisplay(alloc, .above, false); - - { - const str = try t.plainString(testing.allocator); - defer testing.allocator.free(str); - try testing.expectEqualStrings("\n F\nGH橋I", str); - } -} - -// X -test "Terminal: eraseDisplay above protected attributes respected with iso" { - const alloc = testing.allocator; - var t = try init(alloc, 5, 5); - defer t.deinit(alloc); - - t.setProtectedMode(.iso); - for ("ABC") |c| try t.print(c); - t.carriageReturn(); - try t.linefeed(); - for ("DEF") |c| try t.print(c); - t.carriageReturn(); - try t.linefeed(); - for ("GHI") |c| try t.print(c); - t.setCursorPos(2, 2); - t.eraseDisplay(alloc, .above, false); - - { - const str = try t.plainString(testing.allocator); - defer testing.allocator.free(str); - try testing.expectEqualStrings("ABC\nDEF\nGHI", str); - } -} - -// X -test "Terminal: eraseDisplay above protected attributes ignored with dec most recent" { - const alloc = testing.allocator; - var t = try init(alloc, 5, 5); - defer t.deinit(alloc); - - t.setProtectedMode(.iso); - for ("ABC") |c| try t.print(c); - t.carriageReturn(); - try t.linefeed(); - for ("DEF") |c| try t.print(c); - t.carriageReturn(); - try t.linefeed(); - for ("GHI") |c| try t.print(c); - t.setProtectedMode(.dec); - t.setProtectedMode(.off); - t.setCursorPos(2, 2); - t.eraseDisplay(alloc, .above, false); - - { - const str = try t.plainString(testing.allocator); - defer testing.allocator.free(str); - try testing.expectEqualStrings("\n F\nGHI", str); - } -} - -// X -test "Terminal: eraseDisplay above protected attributes ignored with dec set" { - const alloc = testing.allocator; - var t = try init(alloc, 5, 5); - defer t.deinit(alloc); - - t.setProtectedMode(.dec); - for ("ABC") |c| try t.print(c); - t.carriageReturn(); - try t.linefeed(); - for ("DEF") |c| try t.print(c); - t.carriageReturn(); - try t.linefeed(); - for ("GHI") |c| try t.print(c); - t.setCursorPos(2, 2); - t.eraseDisplay(alloc, .above, false); - - { - const str = try t.plainString(testing.allocator); - defer testing.allocator.free(str); - try testing.expectEqualStrings("\n F\nGHI", str); - } -} - -// X -test "Terminal: eraseDisplay above protected attributes respected with force" { - const alloc = testing.allocator; - var t = try init(alloc, 5, 5); - defer t.deinit(alloc); - - t.setProtectedMode(.dec); - for ("ABC") |c| try t.print(c); - t.carriageReturn(); - try t.linefeed(); - for ("DEF") |c| try t.print(c); - t.carriageReturn(); - try t.linefeed(); - for ("GHI") |c| try t.print(c); - t.setCursorPos(2, 2); - t.eraseDisplay(alloc, .above, true); - - { - const str = try t.plainString(testing.allocator); - defer testing.allocator.free(str); - try testing.expectEqualStrings("ABC\nDEF\nGHI", str); - } -} - -// X -test "Terminal: eraseDisplay above" { - var t = try init(testing.allocator, 80, 80); - defer t.deinit(testing.allocator); - - const pink = color.RGB{ .r = 0xFF, .g = 0x00, .b = 0x7F }; - t.screen.cursor.pen = Screen.Cell{ - .char = 'a', - .bg = .{ .rgb = pink }, - .fg = .{ .rgb = pink }, - .attrs = .{ .bold = true }, - }; - const cell_ptr = t.screen.getCellPtr(.active, 0, 0); - cell_ptr.* = t.screen.cursor.pen; - // verify the cell was set - var cell = t.screen.getCell(.active, 0, 0); - try testing.expect(cell.bg.rgb.eql(pink)); - try testing.expect(cell.fg.rgb.eql(pink)); - try testing.expect(cell.char == 'a'); - try testing.expect(cell.attrs.bold); - // move the cursor below it - t.screen.cursor.y = 40; - t.screen.cursor.x = 40; - // erase above the cursor - t.eraseDisplay(testing.allocator, .above, false); - // check it was erased - cell = t.screen.getCell(.active, 0, 0); - try testing.expect(cell.bg.rgb.eql(pink)); - try testing.expect(cell.fg == .none); - try testing.expect(cell.char == 0); - try testing.expect(!cell.attrs.bold); - - // Check that our pen hasn't changed - try testing.expect(t.screen.cursor.pen.attrs.bold); - - // check that another cell got the correct bg - cell = t.screen.getCell(.active, 0, 1); - try testing.expect(cell.bg.rgb.eql(pink)); -} - -// X -test "Terminal: eraseDisplay below" { - var t = try init(testing.allocator, 80, 80); - defer t.deinit(testing.allocator); - - const pink = color.RGB{ .r = 0xFF, .g = 0x00, .b = 0x7F }; - t.screen.cursor.pen = Screen.Cell{ - .char = 'a', - .bg = .{ .rgb = pink }, - .fg = .{ .rgb = pink }, - .attrs = .{ .bold = true }, - }; - const cell_ptr = t.screen.getCellPtr(.active, 60, 60); - cell_ptr.* = t.screen.cursor.pen; - // verify the cell was set - var cell = t.screen.getCell(.active, 60, 60); - try testing.expect(cell.bg.rgb.eql(pink)); - try testing.expect(cell.fg.rgb.eql(pink)); - try testing.expect(cell.char == 'a'); - try testing.expect(cell.attrs.bold); - // erase below the cursor - t.eraseDisplay(testing.allocator, .below, false); - // check it was erased - cell = t.screen.getCell(.active, 60, 60); - try testing.expect(cell.bg.rgb.eql(pink)); - try testing.expect(cell.fg == .none); - try testing.expect(cell.char == 0); - try testing.expect(!cell.attrs.bold); - - // check that another cell got the correct bg - cell = t.screen.getCell(.active, 0, 1); - try testing.expect(cell.bg.rgb.eql(pink)); -} - -// X -test "Terminal: eraseDisplay complete" { - var t = try init(testing.allocator, 80, 80); - defer t.deinit(testing.allocator); - - const pink = color.RGB{ .r = 0xFF, .g = 0x00, .b = 0x7F }; - t.screen.cursor.pen = Screen.Cell{ - .char = 'a', - .bg = .{ .rgb = pink }, - .fg = .{ .rgb = pink }, - .attrs = .{ .bold = true }, - }; - var cell_ptr = t.screen.getCellPtr(.active, 60, 60); - cell_ptr.* = t.screen.cursor.pen; - cell_ptr = t.screen.getCellPtr(.active, 0, 0); - cell_ptr.* = t.screen.cursor.pen; - // verify the cell was set - var cell = t.screen.getCell(.active, 60, 60); - try testing.expect(cell.bg.rgb.eql(pink)); - try testing.expect(cell.fg.rgb.eql(pink)); - try testing.expect(cell.char == 'a'); - try testing.expect(cell.attrs.bold); - // verify the cell was set - cell = t.screen.getCell(.active, 0, 0); - try testing.expect(cell.bg.rgb.eql(pink)); - try testing.expect(cell.fg.rgb.eql(pink)); - try testing.expect(cell.char == 'a'); - try testing.expect(cell.attrs.bold); - // position our cursor between the cells - t.screen.cursor.y = 30; - // erase everything - t.eraseDisplay(testing.allocator, .complete, false); - // check they were erased - cell = t.screen.getCell(.active, 60, 60); - try testing.expect(cell.bg.rgb.eql(pink)); - try testing.expect(cell.fg == .none); - try testing.expect(cell.char == 0); - try testing.expect(!cell.attrs.bold); - cell = t.screen.getCell(.active, 0, 0); - try testing.expect(cell.bg.rgb.eql(pink)); - try testing.expect(cell.fg == .none); - try testing.expect(cell.char == 0); - try testing.expect(!cell.attrs.bold); -} - -// X -test "Terminal: eraseDisplay protected complete" { - const alloc = testing.allocator; - var t = try init(alloc, 10, 5); - defer t.deinit(alloc); - - try t.print('A'); - t.carriageReturn(); - try t.linefeed(); - for ("123456789") |c| try t.print(c); - t.setCursorPos(t.screen.cursor.y + 1, 6); - t.setProtectedMode(.dec); - try t.print('X'); - t.setCursorPos(t.screen.cursor.y + 1, 4); - t.eraseDisplay(alloc, .complete, true); - - { - const str = try t.plainString(testing.allocator); - defer testing.allocator.free(str); - try testing.expectEqualStrings("\n X", str); - } -} - -// X -test "Terminal: eraseDisplay protected below" { - const alloc = testing.allocator; - var t = try init(alloc, 10, 5); - defer t.deinit(alloc); - - try t.print('A'); - t.carriageReturn(); - try t.linefeed(); - for ("123456789") |c| try t.print(c); - t.setCursorPos(t.screen.cursor.y + 1, 6); - t.setProtectedMode(.dec); - try t.print('X'); - t.setCursorPos(t.screen.cursor.y + 1, 4); - t.eraseDisplay(alloc, .below, true); - - { - const str = try t.plainString(testing.allocator); - defer testing.allocator.free(str); - try testing.expectEqualStrings("A\n123 X", str); - } -} - -// X -test "Terminal: eraseDisplay protected above" { - const alloc = testing.allocator; - var t = try init(alloc, 10, 5); - defer t.deinit(alloc); - - try t.print('A'); - t.carriageReturn(); - try t.linefeed(); - t.eraseDisplay(alloc, .scroll_complete, false); - - { - const str = try t.plainString(testing.allocator); - defer testing.allocator.free(str); - try testing.expectEqualStrings("", str); - } -} - -// X -test "Terminal: eraseDisplay scroll complete" { - const alloc = testing.allocator; - var t = try init(alloc, 10, 3); - defer t.deinit(alloc); - - try t.print('A'); - t.carriageReturn(); - try t.linefeed(); - for ("123456789") |c| try t.print(c); - t.setCursorPos(t.screen.cursor.y + 1, 6); - t.setProtectedMode(.dec); - try t.print('X'); - t.setCursorPos(t.screen.cursor.y + 1, 8); - t.eraseDisplay(alloc, .above, true); - - { - const str = try t.plainString(testing.allocator); - defer testing.allocator.free(str); - try testing.expectEqualStrings("\n X 9", str); - } -} - -// X -test "Terminal: cursorLeft no wrap" { - const alloc = testing.allocator; - var t = try init(alloc, 10, 5); - defer t.deinit(alloc); - - try t.print('A'); - t.carriageReturn(); - try t.linefeed(); - try t.print('B'); - t.cursorLeft(10); - - { - const str = try t.plainString(testing.allocator); - defer testing.allocator.free(str); - try testing.expectEqualStrings("A\nB", str); - } -} - -// X -test "Terminal: cursorLeft unsets pending wrap state" { - const alloc = testing.allocator; - var t = try init(alloc, 5, 5); - defer t.deinit(alloc); - - for ("ABCDE") |c| try t.print(c); - try testing.expect(t.screen.cursor.pending_wrap); - t.cursorLeft(1); - try testing.expect(!t.screen.cursor.pending_wrap); - try t.print('X'); - - { - const str = try t.plainString(testing.allocator); - defer testing.allocator.free(str); - try testing.expectEqualStrings("ABCXE", str); - } -} - -// X -test "Terminal: cursorLeft unsets pending wrap state with longer jump" { - const alloc = testing.allocator; - var t = try init(alloc, 5, 5); - defer t.deinit(alloc); - - for ("ABCDE") |c| try t.print(c); - try testing.expect(t.screen.cursor.pending_wrap); - t.cursorLeft(3); - try testing.expect(!t.screen.cursor.pending_wrap); - try t.print('X'); - - { - const str = try t.plainString(testing.allocator); - defer testing.allocator.free(str); - try testing.expectEqualStrings("AXCDE", str); - } -} - -// X -test "Terminal: cursorLeft reverse wrap with pending wrap state" { - const alloc = testing.allocator; - var t = try init(alloc, 5, 5); - defer t.deinit(alloc); - - t.modes.set(.wraparound, true); - t.modes.set(.reverse_wrap, true); - - for ("ABCDE") |c| try t.print(c); - try testing.expect(t.screen.cursor.pending_wrap); - t.cursorLeft(1); - try testing.expect(!t.screen.cursor.pending_wrap); - try t.print('X'); - - { - const str = try t.plainString(testing.allocator); - defer testing.allocator.free(str); - try testing.expectEqualStrings("ABCDX", str); - } -} - -// X -test "Terminal: cursorLeft reverse wrap extended with pending wrap state" { - const alloc = testing.allocator; - var t = try init(alloc, 5, 5); - defer t.deinit(alloc); - - t.modes.set(.wraparound, true); - t.modes.set(.reverse_wrap_extended, true); - - for ("ABCDE") |c| try t.print(c); - try testing.expect(t.screen.cursor.pending_wrap); - t.cursorLeft(1); - try testing.expect(!t.screen.cursor.pending_wrap); - try t.print('X'); - - { - const str = try t.plainString(testing.allocator); - defer testing.allocator.free(str); - try testing.expectEqualStrings("ABCDX", str); - } -} - -// X -test "Terminal: cursorLeft reverse wrap" { - const alloc = testing.allocator; - var t = try init(alloc, 5, 5); - defer t.deinit(alloc); - - t.modes.set(.wraparound, true); - t.modes.set(.reverse_wrap, true); - - for ("ABCDE1") |c| try t.print(c); - t.cursorLeft(2); - try t.print('X'); - try testing.expect(t.screen.cursor.pending_wrap); - - { - const str = try t.plainString(testing.allocator); - defer testing.allocator.free(str); - try testing.expectEqualStrings("ABCDX\n1", str); - } -} - -// X -test "Terminal: cursorLeft reverse wrap with no soft wrap" { - const alloc = testing.allocator; - var t = try init(alloc, 5, 5); - defer t.deinit(alloc); - - t.modes.set(.wraparound, true); - t.modes.set(.reverse_wrap, true); - - for ("ABCDE") |c| try t.print(c); - t.carriageReturn(); - try t.linefeed(); - try t.print('1'); - t.cursorLeft(2); - try t.print('X'); - - { - const str = try t.plainString(testing.allocator); - defer testing.allocator.free(str); - try testing.expectEqualStrings("ABCDE\nX", str); - } -} - -// X -test "Terminal: cursorLeft reverse wrap before left margin" { - const alloc = testing.allocator; - var t = try init(alloc, 5, 5); - defer t.deinit(alloc); - - t.modes.set(.wraparound, true); - t.modes.set(.reverse_wrap, true); - t.setTopAndBottomMargin(3, 0); - t.cursorLeft(1); - try t.print('X'); - - { - const str = try t.plainString(testing.allocator); - defer testing.allocator.free(str); - try testing.expectEqualStrings("\n\nX", str); - } -} - -// X -test "Terminal: cursorLeft extended reverse wrap" { - const alloc = testing.allocator; - var t = try init(alloc, 5, 5); - defer t.deinit(alloc); - - t.modes.set(.wraparound, true); - t.modes.set(.reverse_wrap_extended, true); - - for ("ABCDE") |c| try t.print(c); - t.carriageReturn(); - try t.linefeed(); - try t.print('1'); - t.cursorLeft(2); - try t.print('X'); - - { - const str = try t.plainString(testing.allocator); - defer testing.allocator.free(str); - try testing.expectEqualStrings("ABCDX\n1", str); - } -} - -// X -test "Terminal: cursorLeft extended reverse wrap bottom wraparound" { - const alloc = testing.allocator; - var t = try init(alloc, 5, 3); - defer t.deinit(alloc); - - t.modes.set(.wraparound, true); - t.modes.set(.reverse_wrap_extended, true); - - for ("ABCDE") |c| try t.print(c); - t.carriageReturn(); - try t.linefeed(); - try t.print('1'); - t.cursorLeft(1 + t.cols + 1); - try t.print('X'); - - { - const str = try t.plainString(testing.allocator); - defer testing.allocator.free(str); - try testing.expectEqualStrings("ABCDE\n1\n X", str); - } -} - -// X -test "Terminal: cursorLeft extended reverse wrap is priority if both set" { - const alloc = testing.allocator; - var t = try init(alloc, 5, 3); - defer t.deinit(alloc); - - t.modes.set(.wraparound, true); - t.modes.set(.reverse_wrap, true); - t.modes.set(.reverse_wrap_extended, true); - - for ("ABCDE") |c| try t.print(c); - t.carriageReturn(); - try t.linefeed(); - try t.print('1'); - t.cursorLeft(1 + t.cols + 1); - try t.print('X'); - - { - const str = try t.plainString(testing.allocator); - defer testing.allocator.free(str); - try testing.expectEqualStrings("ABCDE\n1\n X", str); - } -} - -// X -test "Terminal: cursorLeft extended reverse wrap above top scroll region" { - const alloc = testing.allocator; - var t = try init(alloc, 5, 5); - defer t.deinit(alloc); - - t.modes.set(.wraparound, true); - t.modes.set(.reverse_wrap_extended, true); - - t.setTopAndBottomMargin(3, 0); - t.setCursorPos(2, 1); - t.cursorLeft(1000); - - try testing.expectEqual(@as(usize, 0), t.screen.cursor.x); - try testing.expectEqual(@as(usize, 0), t.screen.cursor.y); -} - -// X -test "Terminal: cursorLeft reverse wrap on first row" { - const alloc = testing.allocator; - var t = try init(alloc, 5, 5); - defer t.deinit(alloc); - - t.modes.set(.wraparound, true); - t.modes.set(.reverse_wrap, true); - - t.setTopAndBottomMargin(3, 0); - t.setCursorPos(1, 2); - t.cursorLeft(1000); - - try testing.expectEqual(@as(usize, 0), t.screen.cursor.x); - try testing.expectEqual(@as(usize, 0), t.screen.cursor.y); -} - -// X -test "Terminal: cursorDown basic" { - const alloc = testing.allocator; - var t = try init(alloc, 5, 5); - defer t.deinit(alloc); - - try t.print('A'); - t.cursorDown(10); - try t.print('X'); - - { - const str = try t.plainString(testing.allocator); - defer testing.allocator.free(str); - try testing.expectEqualStrings("A\n\n\n\n X", str); - } -} - -// X -test "Terminal: cursorDown above bottom scroll margin" { - const alloc = testing.allocator; - var t = try init(alloc, 5, 5); - defer t.deinit(alloc); - - t.setTopAndBottomMargin(1, 3); - try t.print('A'); - t.cursorDown(10); - try t.print('X'); - - { - const str = try t.plainString(testing.allocator); - defer testing.allocator.free(str); - try testing.expectEqualStrings("A\n\n X", str); - } -} - -// X -test "Terminal: cursorDown below bottom scroll margin" { - const alloc = testing.allocator; - var t = try init(alloc, 5, 5); - defer t.deinit(alloc); - - t.setTopAndBottomMargin(1, 3); - try t.print('A'); - t.setCursorPos(4, 1); - t.cursorDown(10); - try t.print('X'); - - { - const str = try t.plainString(testing.allocator); - defer testing.allocator.free(str); - try testing.expectEqualStrings("A\n\n\n\nX", str); - } -} - -// X -test "Terminal: cursorDown resets wrap" { - const alloc = testing.allocator; - var t = try init(alloc, 5, 5); - defer t.deinit(alloc); - - for ("ABCDE") |c| try t.print(c); - try testing.expect(t.screen.cursor.pending_wrap); - t.cursorDown(1); - try testing.expect(!t.screen.cursor.pending_wrap); - try t.print('X'); - - { - const str = try t.plainString(testing.allocator); - defer testing.allocator.free(str); - try testing.expectEqualStrings("ABCDE\n X", str); - } -} - -// X -test "Terminal: cursorUp basic" { - const alloc = testing.allocator; - var t = try init(alloc, 5, 5); - defer t.deinit(alloc); - - t.setCursorPos(3, 1); - try t.print('A'); - t.cursorUp(10); - try t.print('X'); - - { - const str = try t.plainString(testing.allocator); - defer testing.allocator.free(str); - try testing.expectEqualStrings(" X\n\nA", str); - } -} - -// X -test "Terminal: cursorUp below top scroll margin" { - const alloc = testing.allocator; - var t = try init(alloc, 5, 5); - defer t.deinit(alloc); - - t.setTopAndBottomMargin(2, 4); - t.setCursorPos(3, 1); - try t.print('A'); - t.cursorUp(5); - try t.print('X'); - - { - const str = try t.plainString(testing.allocator); - defer testing.allocator.free(str); - try testing.expectEqualStrings("\n X\nA", str); - } -} - -// X -test "Terminal: cursorUp above top scroll margin" { - const alloc = testing.allocator; - var t = try init(alloc, 5, 5); - defer t.deinit(alloc); - - t.setTopAndBottomMargin(3, 5); - t.setCursorPos(3, 1); - try t.print('A'); - t.setCursorPos(2, 1); - t.cursorUp(10); - try t.print('X'); - - { - const str = try t.plainString(testing.allocator); - defer testing.allocator.free(str); - try testing.expectEqualStrings("X\n\nA", str); - } -} - -// X -test "Terminal: cursorUp resets wrap" { - const alloc = testing.allocator; - var t = try init(alloc, 5, 5); - defer t.deinit(alloc); - - for ("ABCDE") |c| try t.print(c); - try testing.expect(t.screen.cursor.pending_wrap); - t.cursorUp(1); - try testing.expect(!t.screen.cursor.pending_wrap); - try t.print('X'); - - { - const str = try t.plainString(testing.allocator); - defer testing.allocator.free(str); - try testing.expectEqualStrings("ABCDX", str); - } -} - -// X -test "Terminal: cursorRight resets wrap" { - const alloc = testing.allocator; - var t = try init(alloc, 5, 5); - defer t.deinit(alloc); - - for ("ABCDE") |c| try t.print(c); - try testing.expect(t.screen.cursor.pending_wrap); - t.cursorRight(1); - try testing.expect(!t.screen.cursor.pending_wrap); - try t.print('X'); - - { - const str = try t.plainString(testing.allocator); - defer testing.allocator.free(str); - try testing.expectEqualStrings("ABCDX", str); - } -} - -// X -test "Terminal: cursorRight to the edge of screen" { - const alloc = testing.allocator; - var t = try init(alloc, 5, 5); - defer t.deinit(alloc); - - t.cursorRight(100); - try t.print('X'); - - { - const str = try t.plainString(testing.allocator); - defer testing.allocator.free(str); - try testing.expectEqualStrings(" X", str); - } -} - -// X -test "Terminal: cursorRight left of right margin" { - const alloc = testing.allocator; - var t = try init(alloc, 5, 5); - defer t.deinit(alloc); - - t.scrolling_region.right = 2; - t.cursorRight(100); - try t.print('X'); - - { - const str = try t.plainString(testing.allocator); - defer testing.allocator.free(str); - try testing.expectEqualStrings(" X", str); - } -} - -// X -test "Terminal: cursorRight right of right margin" { - const alloc = testing.allocator; - var t = try init(alloc, 5, 5); - defer t.deinit(alloc); - - t.scrolling_region.right = 2; - t.screen.cursor.x = 3; - t.cursorRight(100); - try t.print('X'); - - { - const str = try t.plainString(testing.allocator); - defer testing.allocator.free(str); - try testing.expectEqualStrings(" X", str); - } -} - -// X -test "Terminal: scrollDown simple" { - const alloc = testing.allocator; - var t = try init(alloc, 5, 5); - defer t.deinit(alloc); - - try t.printString("ABC"); - t.carriageReturn(); - try t.linefeed(); - try t.printString("DEF"); - t.carriageReturn(); - try t.linefeed(); - try t.printString("GHI"); - t.setCursorPos(2, 2); - const cursor = t.screen.cursor; - try t.scrollDown(1); - try testing.expectEqual(cursor, t.screen.cursor); - - { - const str = try t.plainString(testing.allocator); - defer testing.allocator.free(str); - try testing.expectEqualStrings("\nABC\nDEF\nGHI", str); - } -} - -// X -test "Terminal: scrollDown outside of scroll region" { - const alloc = testing.allocator; - var t = try init(alloc, 5, 5); - defer t.deinit(alloc); - - try t.printString("ABC"); - t.carriageReturn(); - try t.linefeed(); - try t.printString("DEF"); - t.carriageReturn(); - try t.linefeed(); - try t.printString("GHI"); - t.setTopAndBottomMargin(3, 4); - t.setCursorPos(2, 2); - const cursor = t.screen.cursor; - try t.scrollDown(1); - try testing.expectEqual(cursor, t.screen.cursor); - - { - const str = try t.plainString(testing.allocator); - defer testing.allocator.free(str); - try testing.expectEqualStrings("ABC\nDEF\n\nGHI", str); - } -} - -// X -test "Terminal: scrollDown left/right scroll region" { - const alloc = testing.allocator; - var t = try init(alloc, 10, 10); - defer t.deinit(alloc); - - try t.printString("ABC123"); - t.carriageReturn(); - try t.linefeed(); - try t.printString("DEF456"); - t.carriageReturn(); - try t.linefeed(); - try t.printString("GHI789"); - t.scrolling_region.left = 1; - t.scrolling_region.right = 3; - t.setCursorPos(2, 2); - const cursor = t.screen.cursor; - try t.scrollDown(1); - try testing.expectEqual(cursor, t.screen.cursor); - - { - const str = try t.plainString(testing.allocator); - defer testing.allocator.free(str); - try testing.expectEqualStrings("A 23\nDBC156\nGEF489\n HI7", str); - } -} - -// X -test "Terminal: scrollDown outside of left/right scroll region" { - const alloc = testing.allocator; - var t = try init(alloc, 10, 10); - defer t.deinit(alloc); - - try t.printString("ABC123"); - t.carriageReturn(); - try t.linefeed(); - try t.printString("DEF456"); - t.carriageReturn(); - try t.linefeed(); - try t.printString("GHI789"); - t.scrolling_region.left = 1; - t.scrolling_region.right = 3; - t.setCursorPos(1, 1); - const cursor = t.screen.cursor; - try t.scrollDown(1); - try testing.expectEqual(cursor, t.screen.cursor); - - { - const str = try t.plainString(testing.allocator); - defer testing.allocator.free(str); - try testing.expectEqualStrings("A 23\nDBC156\nGEF489\n HI7", str); - } -} - -// X -test "Terminal: scrollDown preserves pending wrap" { - const alloc = testing.allocator; - var t = try init(alloc, 5, 10); - defer t.deinit(alloc); - - t.setCursorPos(1, 5); - try t.print('A'); - t.setCursorPos(2, 5); - try t.print('B'); - t.setCursorPos(3, 5); - try t.print('C'); - try t.scrollDown(1); - try t.print('X'); - - { - const str = try t.plainString(testing.allocator); - defer testing.allocator.free(str); - try testing.expectEqualStrings("\n A\n B\nX C", str); - } -} - -// X -test "Terminal: scrollUp simple" { - const alloc = testing.allocator; - var t = try init(alloc, 5, 5); - defer t.deinit(alloc); - - try t.printString("ABC"); - t.carriageReturn(); - try t.linefeed(); - try t.printString("DEF"); - t.carriageReturn(); - try t.linefeed(); - try t.printString("GHI"); - t.setCursorPos(2, 2); - const cursor = t.screen.cursor; - try t.scrollUp(1); - try testing.expectEqual(cursor, t.screen.cursor); - - { - const str = try t.plainString(testing.allocator); - defer testing.allocator.free(str); - try testing.expectEqualStrings("DEF\nGHI", str); - } -} - -// X -test "Terminal: scrollUp top/bottom scroll region" { - const alloc = testing.allocator; - var t = try init(alloc, 5, 5); - defer t.deinit(alloc); - - try t.printString("ABC"); - t.carriageReturn(); - try t.linefeed(); - try t.printString("DEF"); - t.carriageReturn(); - try t.linefeed(); - try t.printString("GHI"); - t.setTopAndBottomMargin(2, 3); - t.setCursorPos(1, 1); - try t.scrollUp(1); - - { - const str = try t.plainString(testing.allocator); - defer testing.allocator.free(str); - try testing.expectEqualStrings("ABC\nGHI", str); - } -} - -// X -test "Terminal: scrollUp left/right scroll region" { - const alloc = testing.allocator; - var t = try init(alloc, 10, 10); - defer t.deinit(alloc); - - try t.printString("ABC123"); - t.carriageReturn(); - try t.linefeed(); - try t.printString("DEF456"); - t.carriageReturn(); - try t.linefeed(); - try t.printString("GHI789"); - t.scrolling_region.left = 1; - t.scrolling_region.right = 3; - t.setCursorPos(2, 2); - const cursor = t.screen.cursor; - try t.scrollUp(1); - try testing.expectEqual(cursor, t.screen.cursor); - - { - const str = try t.plainString(testing.allocator); - defer testing.allocator.free(str); - try testing.expectEqualStrings("AEF423\nDHI756\nG 89", str); - } -} - -// X -test "Terminal: scrollUp preserves pending wrap" { - const alloc = testing.allocator; - var t = try init(alloc, 5, 5); - defer t.deinit(alloc); - - t.setCursorPos(1, 5); - try t.print('A'); - t.setCursorPos(2, 5); - try t.print('B'); - t.setCursorPos(3, 5); - try t.print('C'); - try t.scrollUp(1); - try t.print('X'); - - { - const str = try t.plainString(testing.allocator); - defer testing.allocator.free(str); - try testing.expectEqualStrings(" B\n C\n\nX", str); - } -} - -// X -test "Terminal: scrollUp full top/bottom region" { - const alloc = testing.allocator; - var t = try init(alloc, 5, 5); - defer t.deinit(alloc); - - try t.printString("top"); - t.setCursorPos(5, 1); - try t.printString("ABCDE"); - t.setTopAndBottomMargin(2, 5); - try t.scrollUp(4); - - { - const str = try t.plainString(testing.allocator); - defer testing.allocator.free(str); - try testing.expectEqualStrings("top", str); - } -} - -// X -test "Terminal: scrollUp full top/bottomleft/right scroll region" { - const alloc = testing.allocator; - var t = try init(alloc, 5, 5); - defer t.deinit(alloc); - - try t.printString("top"); - t.setCursorPos(5, 1); - try t.printString("ABCDE"); - t.modes.set(.enable_left_and_right_margin, true); - t.setTopAndBottomMargin(2, 5); - t.setLeftAndRightMargin(2, 4); - try t.scrollUp(4); - - { - const str = try t.plainString(testing.allocator); - defer testing.allocator.free(str); - try testing.expectEqualStrings("top\n\n\n\nA E", str); - } -} - -// X -test "Terminal: tabClear single" { - const alloc = testing.allocator; - var t = try init(alloc, 30, 5); - defer t.deinit(alloc); - - try t.horizontalTab(); - t.tabClear(.current); - t.setCursorPos(1, 1); - try t.horizontalTab(); - try testing.expectEqual(@as(usize, 16), t.screen.cursor.x); -} - -// X -test "Terminal: tabClear all" { - const alloc = testing.allocator; - var t = try init(alloc, 30, 5); - defer t.deinit(alloc); - - t.tabClear(.all); - t.setCursorPos(1, 1); - try t.horizontalTab(); - try testing.expectEqual(@as(usize, 29), t.screen.cursor.x); -} - -// X -test "Terminal: printRepeat simple" { - const alloc = testing.allocator; - var t = try init(alloc, 5, 5); - defer t.deinit(alloc); - - try t.printString("A"); - try t.printRepeat(1); - - { - const str = try t.plainString(testing.allocator); - defer testing.allocator.free(str); - try testing.expectEqualStrings("AA", str); - } -} - -// X -test "Terminal: printRepeat wrap" { - const alloc = testing.allocator; - var t = try init(alloc, 5, 5); - defer t.deinit(alloc); - - try t.printString(" A"); - try t.printRepeat(1); - - { - const str = try t.plainString(testing.allocator); - defer testing.allocator.free(str); - try testing.expectEqualStrings(" A\nA", str); - } -} - -// X -test "Terminal: printRepeat no previous character" { - const alloc = testing.allocator; - var t = try init(alloc, 5, 5); - defer t.deinit(alloc); - - try t.printRepeat(1); - - { - const str = try t.plainString(testing.allocator); - defer testing.allocator.free(str); - try testing.expectEqualStrings("", str); - } -} - -// X -test "Terminal: DECCOLM without DEC mode 40" { - const alloc = testing.allocator; - var t = try init(alloc, 5, 5); - defer t.deinit(alloc); - - t.modes.set(.@"132_column", true); - try t.deccolm(alloc, .@"132_cols"); - try testing.expectEqual(@as(usize, 5), t.cols); - try testing.expectEqual(@as(usize, 5), t.rows); - try testing.expect(!t.modes.get(.@"132_column")); -} - -// X -test "Terminal: DECCOLM unset" { - const alloc = testing.allocator; - var t = try init(alloc, 5, 5); - defer t.deinit(alloc); - - t.modes.set(.enable_mode_3, true); - try t.deccolm(alloc, .@"80_cols"); - try testing.expectEqual(@as(usize, 80), t.cols); - try testing.expectEqual(@as(usize, 5), t.rows); -} - -// X -test "Terminal: DECCOLM resets pending wrap" { - const alloc = testing.allocator; - var t = try init(alloc, 5, 5); - defer t.deinit(alloc); - - for ("ABCDE") |c| try t.print(c); - try testing.expect(t.screen.cursor.pending_wrap); - - t.modes.set(.enable_mode_3, true); - try t.deccolm(alloc, .@"80_cols"); - try testing.expectEqual(@as(usize, 80), t.cols); - try testing.expectEqual(@as(usize, 5), t.rows); - try testing.expect(!t.screen.cursor.pending_wrap); -} - -// X -test "Terminal: DECCOLM preserves SGR bg" { - const alloc = testing.allocator; - var t = try init(alloc, 5, 5); - defer t.deinit(alloc); - - const pen: Screen.Cell = .{ - .bg = .{ .rgb = .{ .r = 0xFF, .g = 0x00, .b = 0x00 } }, - }; - - t.screen.cursor.pen = pen; - t.modes.set(.enable_mode_3, true); - try t.deccolm(alloc, .@"80_cols"); - - { - const cell = t.screen.getCell(.active, 0, 0); - try testing.expectEqual(pen, cell); - } -} - -// X -test "Terminal: DECCOLM resets scroll region" { - const alloc = testing.allocator; - var t = try init(alloc, 5, 5); - defer t.deinit(alloc); - - t.modes.set(.enable_left_and_right_margin, true); - t.setTopAndBottomMargin(2, 3); - t.setLeftAndRightMargin(3, 5); - - t.modes.set(.enable_mode_3, true); - try t.deccolm(alloc, .@"80_cols"); - - try testing.expect(t.modes.get(.enable_left_and_right_margin)); - try testing.expectEqual(@as(usize, 0), t.scrolling_region.top); - try testing.expectEqual(@as(usize, 4), t.scrolling_region.bottom); - try testing.expectEqual(@as(usize, 0), t.scrolling_region.left); - try testing.expectEqual(@as(usize, 79), t.scrolling_region.right); -} - -// X -test "Terminal: printAttributes" { - const alloc = testing.allocator; - var t = try init(alloc, 5, 5); - defer t.deinit(alloc); - - var storage: [64]u8 = undefined; - - { - try t.setAttribute(.{ .direct_color_fg = .{ .r = 1, .g = 2, .b = 3 } }); - defer t.setAttribute(.unset) catch unreachable; - const buf = try t.printAttributes(&storage); - try testing.expectEqualStrings("0;38:2::1:2:3", buf); - } - - { - try t.setAttribute(.bold); - try t.setAttribute(.{ .direct_color_bg = .{ .r = 1, .g = 2, .b = 3 } }); - defer t.setAttribute(.unset) catch unreachable; - const buf = try t.printAttributes(&storage); - try testing.expectEqualStrings("0;1;48:2::1:2:3", buf); - } - - { - try t.setAttribute(.bold); - try t.setAttribute(.faint); - try t.setAttribute(.italic); - try t.setAttribute(.{ .underline = .single }); - try t.setAttribute(.blink); - try t.setAttribute(.inverse); - try t.setAttribute(.invisible); - try t.setAttribute(.strikethrough); - try t.setAttribute(.{ .direct_color_fg = .{ .r = 100, .g = 200, .b = 255 } }); - try t.setAttribute(.{ .direct_color_bg = .{ .r = 101, .g = 102, .b = 103 } }); - defer t.setAttribute(.unset) catch unreachable; - const buf = try t.printAttributes(&storage); - try testing.expectEqualStrings("0;1;2;3;4;5;7;8;9;38:2::100:200:255;48:2::101:102:103", buf); - } - - { - try t.setAttribute(.{ .underline = .single }); - defer t.setAttribute(.unset) catch unreachable; - const buf = try t.printAttributes(&storage); - try testing.expectEqualStrings("0;4", buf); - } - - { - const buf = try t.printAttributes(&storage); - try testing.expectEqualStrings("0", buf); - } -} - -test "Terminal: preserve grapheme cluster on large scrollback" { - const alloc = testing.allocator; - var t = try init(alloc, 5, 3); - defer t.deinit(alloc); - - // This is the label emoji + the VS16 variant selector - const label = "\u{1F3F7}\u{FE0F}"; - - // This bug required a certain behavior around scrollback interacting - // with the circular buffer that we use at the time of writing this test. - // Mainly, we want to verify that in certain scroll scenarios we preserve - // grapheme clusters. This test is admittedly somewhat brittle but we - // should keep it around to prevent this regression. - for (0..t.screen.max_scrollback * 2) |_| { - try t.printString(label ++ "\n"); - } - - try t.scrollViewport(.{ .delta = -1 }); - { - const str = try t.screen.testString(alloc, .viewport); - defer testing.allocator.free(str); - try testing.expectEqualStrings("🏷️\n🏷️\n🏷️", str); - } -} diff --git a/src/terminal-old/UTF8Decoder.zig b/src/terminal-old/UTF8Decoder.zig deleted file mode 100644 index 6bb0d9815..000000000 --- a/src/terminal-old/UTF8Decoder.zig +++ /dev/null @@ -1,142 +0,0 @@ -//! DFA-based non-allocating error-replacing UTF-8 decoder. -//! -//! This implementation is based largely on the excellent work of -//! Bjoern Hoehrmann, with slight modifications to support error- -//! replacement. -//! -//! For details on Bjoern's DFA-based UTF-8 decoder, see -//! http://bjoern.hoehrmann.de/utf-8/decoder/dfa (MIT licensed) -const UTF8Decoder = @This(); - -const std = @import("std"); -const testing = std.testing; - -const log = std.log.scoped(.utf8decoder); - -// zig fmt: off -const char_classes = [_]u4{ - 0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0, 0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0, - 0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0, 0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0, - 0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0, 0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0, - 0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0, 0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0, - 1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1, 9,9,9,9,9,9,9,9,9,9,9,9,9,9,9,9, - 7,7,7,7,7,7,7,7,7,7,7,7,7,7,7,7, 7,7,7,7,7,7,7,7,7,7,7,7,7,7,7,7, - 8,8,2,2,2,2,2,2,2,2,2,2,2,2,2,2, 2,2,2,2,2,2,2,2,2,2,2,2,2,2,2,2, - 10,3,3,3,3,3,3,3,3,3,3,3,3,4,3,3, 11,6,6,6,5,8,8,8,8,8,8,8,8,8,8,8, -}; - -const transitions = [_]u8 { - 0,12,24,36,60,96,84,12,12,12,48,72, 12,12,12,12,12,12,12,12,12,12,12,12, - 12, 0,12,12,12,12,12, 0,12, 0,12,12, 12,24,12,12,12,12,12,24,12,24,12,12, - 12,12,12,12,12,12,12,24,12,12,12,12, 12,24,12,12,12,12,12,12,12,24,12,12, - 12,12,12,12,12,12,12,36,12,36,12,12, 12,36,12,12,12,12,12,36,12,36,12,12, - 12,36,12,12,12,12,12,12,12,12,12,12, -}; -// zig fmt: on - -// DFA states -const ACCEPT_STATE = 0; -const REJECT_STATE = 12; - -// This is where we accumulate our current codepoint. -accumulator: u21 = 0, -// The internal state of the DFA. -state: u8 = ACCEPT_STATE, - -/// Takes the next byte in the utf-8 sequence and emits a tuple of -/// - The codepoint that was generated, if there is one. -/// - A boolean that indicates whether the provided byte was consumed. -/// -/// The only case where the byte is not consumed is if an ill-formed -/// sequence is reached, in which case a replacement character will be -/// emitted and the byte will not be consumed. -/// -/// If the byte is not consumed, the caller is responsible for calling -/// again with the same byte before continuing. -pub inline fn next(self: *UTF8Decoder, byte: u8) struct { ?u21, bool } { - const char_class = char_classes[byte]; - - const initial_state = self.state; - - if (self.state != ACCEPT_STATE) { - self.accumulator <<= 6; - self.accumulator |= (byte & 0x3F); - } else { - self.accumulator = (@as(u21, 0xFF) >> char_class) & (byte); - } - - self.state = transitions[self.state + char_class]; - - if (self.state == ACCEPT_STATE) { - defer self.accumulator = 0; - - // Emit the fully decoded codepoint. - return .{ self.accumulator, true }; - } else if (self.state == REJECT_STATE) { - self.accumulator = 0; - self.state = ACCEPT_STATE; - // Emit a replacement character. If we rejected the first byte - // in a sequence, then it was consumed, otherwise it was not. - return .{ 0xFFFD, initial_state == ACCEPT_STATE }; - } else { - // Emit nothing, we're in the middle of a sequence. - return .{ null, true }; - } -} - -test "ASCII" { - var d: UTF8Decoder = .{}; - var out: [13]u8 = undefined; - for ("Hello, World!", 0..) |byte, i| { - const res = d.next(byte); - try testing.expect(res[1]); - if (res[0]) |codepoint| { - out[i] = @intCast(codepoint); - } - } - - try testing.expect(std.mem.eql(u8, &out, "Hello, World!")); -} - -test "Well formed utf-8" { - var d: UTF8Decoder = .{}; - var out: [4]u21 = undefined; - var i: usize = 0; - // 4 bytes, 3 bytes, 2 bytes, 1 byte - for ("😄✤ÁA") |byte| { - var consumed = false; - while (!consumed) { - const res = d.next(byte); - consumed = res[1]; - // There are no errors in this sequence, so - // every byte should be consumed first try. - try testing.expect(consumed == true); - if (res[0]) |codepoint| { - out[i] = codepoint; - i += 1; - } - } - } - - try testing.expect(std.mem.eql(u21, &out, &[_]u21{ 0x1F604, 0x2724, 0xC1, 0x41 })); -} - -test "Partially invalid utf-8" { - var d: UTF8Decoder = .{}; - var out: [5]u21 = undefined; - var i: usize = 0; - // Illegally terminated sequence, valid sequence, illegal surrogate pair. - for ("\xF0\x9F😄\xED\xA0\x80") |byte| { - var consumed = false; - while (!consumed) { - const res = d.next(byte); - consumed = res[1]; - if (res[0]) |codepoint| { - out[i] = codepoint; - i += 1; - } - } - } - - try testing.expect(std.mem.eql(u21, &out, &[_]u21{ 0xFFFD, 0x1F604, 0xFFFD, 0xFFFD, 0xFFFD })); -} diff --git a/src/terminal-old/ansi.zig b/src/terminal-old/ansi.zig deleted file mode 100644 index 43c2a9a1c..000000000 --- a/src/terminal-old/ansi.zig +++ /dev/null @@ -1,114 +0,0 @@ -/// C0 (7-bit) control characters from ANSI. -/// -/// This is not complete, control characters are only added to this -/// as the terminal emulator handles them. -pub const C0 = enum(u7) { - /// Null - NUL = 0x00, - /// Start of heading - SOH = 0x01, - /// Start of text - STX = 0x02, - /// Enquiry - ENQ = 0x05, - /// Bell - BEL = 0x07, - /// Backspace - BS = 0x08, - // Horizontal tab - HT = 0x09, - /// Line feed - LF = 0x0A, - /// Vertical Tab - VT = 0x0B, - /// Form feed - FF = 0x0C, - /// Carriage return - CR = 0x0D, - /// Shift out - SO = 0x0E, - /// Shift in - SI = 0x0F, - - // Non-exhaustive so that @intToEnum never fails since the inputs are - // user-generated. - _, -}; - -/// The SGR rendition aspects that can be set, sometimes known as attributes. -/// The value corresponds to the parameter value for the SGR command (ESC [ m). -pub const RenditionAspect = enum(u16) { - default = 0, - bold = 1, - default_fg = 39, - default_bg = 49, - - // Non-exhaustive so that @intToEnum never fails since the inputs are - // user-generated. - _, -}; - -/// The device attribute request type (ESC [ c). -pub const DeviceAttributeReq = enum { - primary, // Blank - secondary, // > - tertiary, // = -}; - -/// Possible cursor styles (ESC [ q) -pub const CursorStyle = enum(u16) { - default = 0, - blinking_block = 1, - steady_block = 2, - blinking_underline = 3, - steady_underline = 4, - blinking_bar = 5, - steady_bar = 6, - - // Non-exhaustive so that @intToEnum never fails for unsupported modes. - _, - - /// True if the cursor should blink. - pub fn blinking(self: CursorStyle) bool { - return switch (self) { - .blinking_block, .blinking_underline, .blinking_bar => true, - else => false, - }; - } -}; - -/// The status line type for DECSSDT. -pub const StatusLineType = enum(u16) { - none = 0, - indicator = 1, - host_writable = 2, - - // Non-exhaustive so that @intToEnum never fails for unsupported values. - _, -}; - -/// The display to target for status updates (DECSASD). -pub const StatusDisplay = enum(u16) { - main = 0, - status_line = 1, - - // Non-exhaustive so that @intToEnum never fails for unsupported values. - _, -}; - -/// The possible modify key formats to ESC[>{a};{b}m -/// Note: this is not complete, we should add more as we support more -pub const ModifyKeyFormat = union(enum) { - legacy: void, - cursor_keys: void, - function_keys: void, - other_keys: enum { none, numeric_except, numeric }, -}; - -/// The protection modes that can be set for the terminal. See DECSCA and -/// ESC V, W. -pub const ProtectedMode = enum { - off, - iso, // ESC V, W - dec, // CSI Ps " q -}; diff --git a/src/terminal-old/apc.zig b/src/terminal-old/apc.zig deleted file mode 100644 index 6a6b8cc36..000000000 --- a/src/terminal-old/apc.zig +++ /dev/null @@ -1,137 +0,0 @@ -const std = @import("std"); -const assert = std.debug.assert; -const Allocator = std.mem.Allocator; - -const kitty_gfx = @import("kitty/graphics.zig"); - -const log = std.log.scoped(.terminal_apc); - -/// APC command handler. This should be hooked into a terminal.Stream handler. -/// The start/feed/end functions are meant to be called from the terminal.Stream -/// apcStart, apcPut, and apcEnd functions, respectively. -pub const Handler = struct { - state: State = .{ .inactive = {} }, - - pub fn deinit(self: *Handler) void { - self.state.deinit(); - } - - pub fn start(self: *Handler) void { - self.state.deinit(); - self.state = .{ .identify = {} }; - } - - pub fn feed(self: *Handler, alloc: Allocator, byte: u8) void { - switch (self.state) { - .inactive => unreachable, - - // We're ignoring this APC command, likely because we don't - // recognize it so there is no need to store the data in memory. - .ignore => return, - - // We identify the APC command by the first byte. - .identify => { - switch (byte) { - // Kitty graphics protocol - 'G' => self.state = .{ .kitty = kitty_gfx.CommandParser.init(alloc) }, - - // Unknown - else => self.state = .{ .ignore = {} }, - } - }, - - .kitty => |*p| p.feed(byte) catch |err| { - log.warn("kitty graphics protocol error: {}", .{err}); - self.state = .{ .ignore = {} }; - }, - } - } - - pub fn end(self: *Handler) ?Command { - defer { - self.state.deinit(); - self.state = .{ .inactive = {} }; - } - - return switch (self.state) { - .inactive => unreachable, - .ignore, .identify => null, - .kitty => |*p| kitty: { - const command = p.complete() catch |err| { - log.warn("kitty graphics protocol error: {}", .{err}); - break :kitty null; - }; - - break :kitty .{ .kitty = command }; - }, - }; - } -}; - -pub const State = union(enum) { - /// We're not in the middle of an APC command yet. - inactive: void, - - /// We got an unrecognized APC sequence or the APC sequence we - /// recognized became invalid. We're just dropping bytes. - ignore: void, - - /// We're waiting to identify the APC sequence. This is done by - /// inspecting the first byte of the sequence. - identify: void, - - /// Kitty graphics protocol - kitty: kitty_gfx.CommandParser, - - pub fn deinit(self: *State) void { - switch (self.*) { - .inactive, .ignore, .identify => {}, - .kitty => |*v| v.deinit(), - } - } -}; - -/// Possible APC commands. -pub const Command = union(enum) { - kitty: kitty_gfx.Command, - - pub fn deinit(self: *Command, alloc: Allocator) void { - switch (self.*) { - .kitty => |*v| v.deinit(alloc), - } - } -}; - -test "unknown APC command" { - const testing = std.testing; - const alloc = testing.allocator; - - var h: Handler = .{}; - h.start(); - for ("Xabcdef1234") |c| h.feed(alloc, c); - try testing.expect(h.end() == null); -} - -test "garbage Kitty command" { - const testing = std.testing; - const alloc = testing.allocator; - - var h: Handler = .{}; - h.start(); - for ("Gabcdef1234") |c| h.feed(alloc, c); - try testing.expect(h.end() == null); -} - -test "valid Kitty command" { - const testing = std.testing; - const alloc = testing.allocator; - - var h: Handler = .{}; - h.start(); - const input = "Gf=24,s=10,v=20,hello=world"; - for (input) |c| h.feed(alloc, c); - - var cmd = h.end().?; - defer cmd.deinit(alloc); - try testing.expect(cmd == .kitty); -} diff --git a/src/terminal-old/charsets.zig b/src/terminal-old/charsets.zig deleted file mode 100644 index 316238458..000000000 --- a/src/terminal-old/charsets.zig +++ /dev/null @@ -1,114 +0,0 @@ -const std = @import("std"); -const assert = std.debug.assert; - -/// The available charset slots for a terminal. -pub const Slots = enum(u3) { - G0 = 0, - G1 = 1, - G2 = 2, - G3 = 3, -}; - -/// The name of the active slots. -pub const ActiveSlot = enum { GL, GR }; - -/// The list of supported character sets and their associated tables. -pub const Charset = enum { - utf8, - ascii, - british, - dec_special, - - /// The table for the given charset. This returns a pointer to a - /// slice that is guaranteed to be 255 chars that can be used to map - /// ASCII to the given charset. - pub fn table(set: Charset) []const u16 { - return switch (set) { - .british => &british, - .dec_special => &dec_special, - - // utf8 is not a table, callers should double-check if the - // charset is utf8 and NOT use tables. - .utf8 => unreachable, - - // recommended that callers just map ascii directly but we can - // support a table - .ascii => &ascii, - }; - } -}; - -/// Just a basic c => c ascii table -const ascii = initTable(); - -/// https://vt100.net/docs/vt220-rm/chapter2.html -const british = british: { - var table = initTable(); - table[0x23] = 0x00a3; - break :british table; -}; - -/// https://en.wikipedia.org/wiki/DEC_Special_Graphics -const dec_special = tech: { - var table = initTable(); - table[0x60] = 0x25C6; - table[0x61] = 0x2592; - table[0x62] = 0x2409; - table[0x63] = 0x240C; - table[0x64] = 0x240D; - table[0x65] = 0x240A; - table[0x66] = 0x00B0; - table[0x67] = 0x00B1; - table[0x68] = 0x2424; - table[0x69] = 0x240B; - table[0x6a] = 0x2518; - table[0x6b] = 0x2510; - table[0x6c] = 0x250C; - table[0x6d] = 0x2514; - table[0x6e] = 0x253C; - table[0x6f] = 0x23BA; - table[0x70] = 0x23BB; - table[0x71] = 0x2500; - table[0x72] = 0x23BC; - table[0x73] = 0x23BD; - table[0x74] = 0x251C; - table[0x75] = 0x2524; - table[0x76] = 0x2534; - table[0x77] = 0x252C; - table[0x78] = 0x2502; - table[0x79] = 0x2264; - table[0x7a] = 0x2265; - table[0x7b] = 0x03C0; - table[0x7c] = 0x2260; - table[0x7d] = 0x00A3; - table[0x7e] = 0x00B7; - break :tech table; -}; - -/// Our table length is 256 so we can contain all ASCII chars. -const table_len = std.math.maxInt(u8) + 1; - -/// Creates a table that maps ASCII to ASCII as a getting started point. -fn initTable() [table_len]u16 { - var result: [table_len]u16 = undefined; - var i: usize = 0; - while (i < table_len) : (i += 1) result[i] = @intCast(i); - assert(i == table_len); - return result; -} - -test { - const testing = std.testing; - const info = @typeInfo(Charset).Enum; - inline for (info.fields) |field| { - // utf8 has no table - if (@field(Charset, field.name) == .utf8) continue; - - const table = @field(Charset, field.name).table(); - - // Yes, I could use `table_len` here, but I want to explicitly use a - // hardcoded constant so that if there are miscompilations or a comptime - // issue, we catch it. - try testing.expectEqual(@as(usize, 256), table.len); - } -} diff --git a/src/terminal-old/color.zig b/src/terminal-old/color.zig deleted file mode 100644 index 194cee8b1..000000000 --- a/src/terminal-old/color.zig +++ /dev/null @@ -1,339 +0,0 @@ -const std = @import("std"); -const assert = std.debug.assert; -const x11_color = @import("x11_color.zig"); - -/// The default palette. -pub const default: Palette = default: { - var result: Palette = undefined; - - // Named values - var i: u8 = 0; - while (i < 16) : (i += 1) { - result[i] = Name.default(@enumFromInt(i)) catch unreachable; - } - - // Cube - assert(i == 16); - var r: u8 = 0; - while (r < 6) : (r += 1) { - var g: u8 = 0; - while (g < 6) : (g += 1) { - var b: u8 = 0; - while (b < 6) : (b += 1) { - result[i] = .{ - .r = if (r == 0) 0 else (r * 40 + 55), - .g = if (g == 0) 0 else (g * 40 + 55), - .b = if (b == 0) 0 else (b * 40 + 55), - }; - - i += 1; - } - } - } - - // Grey ramp - assert(i == 232); - assert(@TypeOf(i) == u8); - while (i > 0) : (i +%= 1) { - const value = ((i - 232) * 10) + 8; - result[i] = .{ .r = value, .g = value, .b = value }; - } - - break :default result; -}; - -/// Palette is the 256 color palette. -pub const Palette = [256]RGB; - -/// Color names in the standard 8 or 16 color palette. -pub const Name = enum(u8) { - black = 0, - red = 1, - green = 2, - yellow = 3, - blue = 4, - magenta = 5, - cyan = 6, - white = 7, - - bright_black = 8, - bright_red = 9, - bright_green = 10, - bright_yellow = 11, - bright_blue = 12, - bright_magenta = 13, - bright_cyan = 14, - bright_white = 15, - - // Remainders are valid unnamed values in the 256 color palette. - _, - - /// Default colors for tagged values. - pub fn default(self: Name) !RGB { - return switch (self) { - .black => RGB{ .r = 0x1D, .g = 0x1F, .b = 0x21 }, - .red => RGB{ .r = 0xCC, .g = 0x66, .b = 0x66 }, - .green => RGB{ .r = 0xB5, .g = 0xBD, .b = 0x68 }, - .yellow => RGB{ .r = 0xF0, .g = 0xC6, .b = 0x74 }, - .blue => RGB{ .r = 0x81, .g = 0xA2, .b = 0xBE }, - .magenta => RGB{ .r = 0xB2, .g = 0x94, .b = 0xBB }, - .cyan => RGB{ .r = 0x8A, .g = 0xBE, .b = 0xB7 }, - .white => RGB{ .r = 0xC5, .g = 0xC8, .b = 0xC6 }, - - .bright_black => RGB{ .r = 0x66, .g = 0x66, .b = 0x66 }, - .bright_red => RGB{ .r = 0xD5, .g = 0x4E, .b = 0x53 }, - .bright_green => RGB{ .r = 0xB9, .g = 0xCA, .b = 0x4A }, - .bright_yellow => RGB{ .r = 0xE7, .g = 0xC5, .b = 0x47 }, - .bright_blue => RGB{ .r = 0x7A, .g = 0xA6, .b = 0xDA }, - .bright_magenta => RGB{ .r = 0xC3, .g = 0x97, .b = 0xD8 }, - .bright_cyan => RGB{ .r = 0x70, .g = 0xC0, .b = 0xB1 }, - .bright_white => RGB{ .r = 0xEA, .g = 0xEA, .b = 0xEA }, - - else => error.NoDefaultValue, - }; - } -}; - -/// RGB -pub const RGB = struct { - r: u8 = 0, - g: u8 = 0, - b: u8 = 0, - - pub fn eql(self: RGB, other: RGB) bool { - return self.r == other.r and self.g == other.g and self.b == other.b; - } - - /// Calculates the contrast ratio between two colors. The contrast - /// ration is a value between 1 and 21 where 1 is the lowest contrast - /// and 21 is the highest contrast. - /// - /// https://www.w3.org/TR/WCAG20/#contrast-ratiodef - pub fn contrast(self: RGB, other: RGB) f64 { - // pair[0] = lighter, pair[1] = darker - const pair: [2]f64 = pair: { - const self_lum = self.luminance(); - const other_lum = other.luminance(); - if (self_lum > other_lum) break :pair .{ self_lum, other_lum }; - break :pair .{ other_lum, self_lum }; - }; - - return (pair[0] + 0.05) / (pair[1] + 0.05); - } - - /// Calculates luminance based on the W3C formula. This returns a - /// normalized value between 0 and 1 where 0 is black and 1 is white. - /// - /// https://www.w3.org/TR/WCAG20/#relativeluminancedef - pub fn luminance(self: RGB) f64 { - const r_lum = componentLuminance(self.r); - const g_lum = componentLuminance(self.g); - const b_lum = componentLuminance(self.b); - return 0.2126 * r_lum + 0.7152 * g_lum + 0.0722 * b_lum; - } - - /// Calculates single-component luminance based on the W3C formula. - /// - /// Expects sRGB color space which at the time of writing we don't - /// generally use but it's a good enough approximation until we fix that. - /// https://www.w3.org/TR/WCAG20/#relativeluminancedef - fn componentLuminance(c: u8) f64 { - const c_f64: f64 = @floatFromInt(c); - const normalized: f64 = c_f64 / 255; - if (normalized <= 0.03928) return normalized / 12.92; - return std.math.pow(f64, (normalized + 0.055) / 1.055, 2.4); - } - - /// Calculates "perceived luminance" which is better for determining - /// light vs dark. - /// - /// Source: https://www.w3.org/TR/AERT/#color-contrast - pub fn perceivedLuminance(self: RGB) f64 { - const r_f64: f64 = @floatFromInt(self.r); - const g_f64: f64 = @floatFromInt(self.g); - const b_f64: f64 = @floatFromInt(self.b); - return 0.299 * (r_f64 / 255) + 0.587 * (g_f64 / 255) + 0.114 * (b_f64 / 255); - } - - test "size" { - try std.testing.expectEqual(@as(usize, 24), @bitSizeOf(RGB)); - try std.testing.expectEqual(@as(usize, 3), @sizeOf(RGB)); - } - - /// Parse a color from a floating point intensity value. - /// - /// The value should be between 0.0 and 1.0, inclusive. - fn fromIntensity(value: []const u8) !u8 { - const i = std.fmt.parseFloat(f64, value) catch return error.InvalidFormat; - if (i < 0.0 or i > 1.0) { - return error.InvalidFormat; - } - - return @intFromFloat(i * std.math.maxInt(u8)); - } - - /// Parse a color from a string of hexadecimal digits - /// - /// The string can contain 1, 2, 3, or 4 characters and represents the color - /// value scaled in 4, 8, 12, or 16 bits, respectively. - fn fromHex(value: []const u8) !u8 { - if (value.len == 0 or value.len > 4) { - return error.InvalidFormat; - } - - const color = std.fmt.parseUnsigned(u16, value, 16) catch return error.InvalidFormat; - const divisor: usize = switch (value.len) { - 1 => std.math.maxInt(u4), - 2 => std.math.maxInt(u8), - 3 => std.math.maxInt(u12), - 4 => std.math.maxInt(u16), - else => unreachable, - }; - - return @intCast(@as(usize, color) * std.math.maxInt(u8) / divisor); - } - - /// Parse a color specification. - /// - /// Any of the following forms are accepted: - /// - /// 1. rgb:// - /// - /// , , := h | hh | hhh | hhhh - /// - /// where `h` is a single hexadecimal digit. - /// - /// 2. rgbi:// - /// - /// where , , and are floating point values between - /// 0.0 and 1.0 (inclusive). - /// - /// 3. #hhhhhh - /// - /// where `h` is a single hexadecimal digit. - pub fn parse(value: []const u8) !RGB { - if (value.len == 0) { - return error.InvalidFormat; - } - - if (value[0] == '#') { - if (value.len != 7) { - return error.InvalidFormat; - } - - return RGB{ - .r = try RGB.fromHex(value[1..3]), - .g = try RGB.fromHex(value[3..5]), - .b = try RGB.fromHex(value[5..7]), - }; - } - - // Check for X11 named colors. We allow whitespace around the edges - // of the color because Kitty allows whitespace. This is not part of - // any spec I could find. - if (x11_color.map.get(std.mem.trim(u8, value, " "))) |rgb| return rgb; - - if (value.len < "rgb:a/a/a".len or !std.mem.eql(u8, value[0..3], "rgb")) { - return error.InvalidFormat; - } - - var i: usize = 3; - - const use_intensity = if (value[i] == 'i') blk: { - i += 1; - break :blk true; - } else false; - - if (value[i] != ':') { - return error.InvalidFormat; - } - - i += 1; - - const r = r: { - const slice = if (std.mem.indexOfScalarPos(u8, value, i, '/')) |end| - value[i..end] - else - return error.InvalidFormat; - - i += slice.len + 1; - - break :r if (use_intensity) - try RGB.fromIntensity(slice) - else - try RGB.fromHex(slice); - }; - - const g = g: { - const slice = if (std.mem.indexOfScalarPos(u8, value, i, '/')) |end| - value[i..end] - else - return error.InvalidFormat; - - i += slice.len + 1; - - break :g if (use_intensity) - try RGB.fromIntensity(slice) - else - try RGB.fromHex(slice); - }; - - const b = if (use_intensity) - try RGB.fromIntensity(value[i..]) - else - try RGB.fromHex(value[i..]); - - return RGB{ - .r = r, - .g = g, - .b = b, - }; - } -}; - -test "palette: default" { - const testing = std.testing; - - // Safety check - var i: u8 = 0; - while (i < 16) : (i += 1) { - try testing.expectEqual(Name.default(@as(Name, @enumFromInt(i))), default[i]); - } -} - -test "RGB.parse" { - const testing = std.testing; - - try testing.expectEqual(RGB{ .r = 255, .g = 0, .b = 0 }, try RGB.parse("rgbi:1.0/0/0")); - try testing.expectEqual(RGB{ .r = 127, .g = 160, .b = 0 }, try RGB.parse("rgb:7f/a0a0/0")); - try testing.expectEqual(RGB{ .r = 255, .g = 255, .b = 255 }, try RGB.parse("rgb:f/ff/fff")); - try testing.expectEqual(RGB{ .r = 255, .g = 255, .b = 255 }, try RGB.parse("#ffffff")); - try testing.expectEqual(RGB{ .r = 255, .g = 0, .b = 16 }, try RGB.parse("#ff0010")); - - try testing.expectEqual(RGB{ .r = 0, .g = 0, .b = 0 }, try RGB.parse("black")); - try testing.expectEqual(RGB{ .r = 255, .g = 0, .b = 0 }, try RGB.parse("red")); - try testing.expectEqual(RGB{ .r = 0, .g = 255, .b = 0 }, try RGB.parse("green")); - try testing.expectEqual(RGB{ .r = 0, .g = 0, .b = 255 }, try RGB.parse("blue")); - try testing.expectEqual(RGB{ .r = 255, .g = 255, .b = 255 }, try RGB.parse("white")); - - try testing.expectEqual(RGB{ .r = 124, .g = 252, .b = 0 }, try RGB.parse("LawnGreen")); - try testing.expectEqual(RGB{ .r = 0, .g = 250, .b = 154 }, try RGB.parse("medium spring green")); - try testing.expectEqual(RGB{ .r = 34, .g = 139, .b = 34 }, try RGB.parse(" Forest Green ")); - - // Invalid format - try testing.expectError(error.InvalidFormat, RGB.parse("rgb;")); - try testing.expectError(error.InvalidFormat, RGB.parse("rgb:")); - try testing.expectError(error.InvalidFormat, RGB.parse(":a/a/a")); - try testing.expectError(error.InvalidFormat, RGB.parse("a/a/a")); - try testing.expectError(error.InvalidFormat, RGB.parse("rgb:a/a/a/")); - try testing.expectError(error.InvalidFormat, RGB.parse("rgb:00000///")); - try testing.expectError(error.InvalidFormat, RGB.parse("rgb:000/")); - try testing.expectError(error.InvalidFormat, RGB.parse("rgbi:a/a/a")); - try testing.expectError(error.InvalidFormat, RGB.parse("rgb:0.5/0.0/1.0")); - try testing.expectError(error.InvalidFormat, RGB.parse("rgb:not/hex/zz")); - try testing.expectError(error.InvalidFormat, RGB.parse("#")); - try testing.expectError(error.InvalidFormat, RGB.parse("#ff")); - try testing.expectError(error.InvalidFormat, RGB.parse("#ffff")); - try testing.expectError(error.InvalidFormat, RGB.parse("#fffff")); - try testing.expectError(error.InvalidFormat, RGB.parse("#gggggg")); -} diff --git a/src/terminal-old/csi.zig b/src/terminal-old/csi.zig deleted file mode 100644 index 877f5986e..000000000 --- a/src/terminal-old/csi.zig +++ /dev/null @@ -1,33 +0,0 @@ -// Modes for the ED CSI command. -pub const EraseDisplay = enum(u8) { - below = 0, - above = 1, - complete = 2, - scrollback = 3, - - /// This is an extension added by Kitty to move the viewport into the - /// scrollback and then erase the display. - scroll_complete = 22, -}; - -// Modes for the EL CSI command. -pub const EraseLine = enum(u8) { - right = 0, - left = 1, - complete = 2, - right_unless_pending_wrap = 4, - - // Non-exhaustive so that @intToEnum never fails since the inputs are - // user-generated. - _, -}; - -// Modes for the TBC (tab clear) command. -pub const TabClear = enum(u8) { - current = 0, - all = 3, - - // Non-exhaustive so that @intToEnum never fails since the inputs are - // user-generated. - _, -}; diff --git a/src/terminal-old/dcs.zig b/src/terminal-old/dcs.zig deleted file mode 100644 index cde00d218..000000000 --- a/src/terminal-old/dcs.zig +++ /dev/null @@ -1,309 +0,0 @@ -const std = @import("std"); -const assert = std.debug.assert; -const Allocator = std.mem.Allocator; -const terminal = @import("main.zig"); -const DCS = terminal.DCS; - -const log = std.log.scoped(.terminal_dcs); - -/// DCS command handler. This should be hooked into a terminal.Stream handler. -/// The hook/put/unhook functions are meant to be called from the -/// terminal.stream dcsHook, dcsPut, and dcsUnhook functions, respectively. -pub const Handler = struct { - state: State = .{ .inactive = {} }, - - /// Maximum bytes any DCS command can take. This is to prevent - /// malicious input from causing us to allocate too much memory. - /// This is arbitrarily set to 1MB today, increase if needed. - max_bytes: usize = 1024 * 1024, - - pub fn deinit(self: *Handler) void { - self.discard(); - } - - pub fn hook(self: *Handler, alloc: Allocator, dcs: DCS) void { - assert(self.state == .inactive); - self.state = if (tryHook(alloc, dcs)) |state_| state: { - if (state_) |state| break :state state else { - log.info("unknown DCS hook: {}", .{dcs}); - break :state .{ .ignore = {} }; - } - } else |err| state: { - log.info( - "error initializing DCS hook, will ignore hook err={}", - .{err}, - ); - break :state .{ .ignore = {} }; - }; - } - - fn tryHook(alloc: Allocator, dcs: DCS) !?State { - return switch (dcs.intermediates.len) { - 1 => switch (dcs.intermediates[0]) { - '+' => switch (dcs.final) { - // XTGETTCAP - // https://github.com/mitchellh/ghostty/issues/517 - 'q' => .{ - .xtgettcap = try std.ArrayList(u8).initCapacity( - alloc, - 128, // Arbitrary choice - ), - }, - - else => null, - }, - - '$' => switch (dcs.final) { - // DECRQSS - 'q' => .{ - .decrqss = .{}, - }, - - else => null, - }, - - else => null, - }, - - else => null, - }; - } - - pub fn put(self: *Handler, byte: u8) void { - self.tryPut(byte) catch |err| { - // On error we just discard our state and ignore the rest - log.info("error putting byte into DCS handler err={}", .{err}); - self.discard(); - self.state = .{ .ignore = {} }; - }; - } - - fn tryPut(self: *Handler, byte: u8) !void { - switch (self.state) { - .inactive, - .ignore, - => {}, - - .xtgettcap => |*list| { - if (list.items.len >= self.max_bytes) { - return error.OutOfMemory; - } - - try list.append(byte); - }, - - .decrqss => |*buffer| { - if (buffer.len >= buffer.data.len) { - return error.OutOfMemory; - } - - buffer.data[buffer.len] = byte; - buffer.len += 1; - }, - } - } - - pub fn unhook(self: *Handler) ?Command { - defer self.state = .{ .inactive = {} }; - return switch (self.state) { - .inactive, - .ignore, - => null, - - .xtgettcap => |list| .{ .xtgettcap = .{ .data = list } }, - - .decrqss => |buffer| .{ .decrqss = switch (buffer.len) { - 0 => .none, - 1 => switch (buffer.data[0]) { - 'm' => .sgr, - 'r' => .decstbm, - 's' => .decslrm, - else => .none, - }, - 2 => switch (buffer.data[0]) { - ' ' => switch (buffer.data[1]) { - 'q' => .decscusr, - else => .none, - }, - else => .none, - }, - else => unreachable, - } }, - }; - } - - fn discard(self: *Handler) void { - switch (self.state) { - .inactive, - .ignore, - => {}, - - .xtgettcap => |*list| list.deinit(), - - .decrqss => {}, - } - - self.state = .{ .inactive = {} }; - } -}; - -pub const Command = union(enum) { - /// XTGETTCAP - xtgettcap: XTGETTCAP, - - /// DECRQSS - decrqss: DECRQSS, - - pub fn deinit(self: Command) void { - switch (self) { - .xtgettcap => |*v| { - v.data.deinit(); - }, - .decrqss => {}, - } - } - - pub const XTGETTCAP = struct { - data: std.ArrayList(u8), - i: usize = 0, - - /// Returns the next terminfo key being requested and null - /// when there are no more keys. The returned value is NOT hex-decoded - /// because we expect to use a comptime lookup table. - pub fn next(self: *XTGETTCAP) ?[]const u8 { - if (self.i >= self.data.items.len) return null; - - var rem = self.data.items[self.i..]; - const idx = std.mem.indexOf(u8, rem, ";") orelse rem.len; - - // Note that if we're at the end, idx + 1 is len + 1 so we're over - // the end but that's okay because our check above is >= so we'll - // never read. - self.i += idx + 1; - - return rem[0..idx]; - } - }; - - /// Supported DECRQSS settings - pub const DECRQSS = enum { - none, - sgr, - decscusr, - decstbm, - decslrm, - }; -}; - -const State = union(enum) { - /// We're not in a DCS state at the moment. - inactive: void, - - /// We're hooked, but its an unknown DCS command or one that went - /// invalid due to some bad input, so we're ignoring the rest. - ignore: void, - - /// XTGETTCAP - xtgettcap: std.ArrayList(u8), - - /// DECRQSS - decrqss: struct { - data: [2]u8 = undefined, - len: u2 = 0, - }, -}; - -test "unknown DCS command" { - const testing = std.testing; - const alloc = testing.allocator; - - var h: Handler = .{}; - defer h.deinit(); - h.hook(alloc, .{ .final = 'A' }); - try testing.expect(h.state == .ignore); - try testing.expect(h.unhook() == null); - try testing.expect(h.state == .inactive); -} - -test "XTGETTCAP command" { - const testing = std.testing; - const alloc = testing.allocator; - - var h: Handler = .{}; - defer h.deinit(); - h.hook(alloc, .{ .intermediates = "+", .final = 'q' }); - for ("536D756C78") |byte| h.put(byte); - var cmd = h.unhook().?; - defer cmd.deinit(); - try testing.expect(cmd == .xtgettcap); - try testing.expectEqualStrings("536D756C78", cmd.xtgettcap.next().?); - try testing.expect(cmd.xtgettcap.next() == null); -} - -test "XTGETTCAP command multiple keys" { - const testing = std.testing; - const alloc = testing.allocator; - - var h: Handler = .{}; - defer h.deinit(); - h.hook(alloc, .{ .intermediates = "+", .final = 'q' }); - for ("536D756C78;536D756C78") |byte| h.put(byte); - var cmd = h.unhook().?; - defer cmd.deinit(); - try testing.expect(cmd == .xtgettcap); - try testing.expectEqualStrings("536D756C78", cmd.xtgettcap.next().?); - try testing.expectEqualStrings("536D756C78", cmd.xtgettcap.next().?); - try testing.expect(cmd.xtgettcap.next() == null); -} - -test "XTGETTCAP command invalid data" { - const testing = std.testing; - const alloc = testing.allocator; - - var h: Handler = .{}; - defer h.deinit(); - h.hook(alloc, .{ .intermediates = "+", .final = 'q' }); - for ("who;536D756C78") |byte| h.put(byte); - var cmd = h.unhook().?; - defer cmd.deinit(); - try testing.expect(cmd == .xtgettcap); - try testing.expectEqualStrings("who", cmd.xtgettcap.next().?); - try testing.expectEqualStrings("536D756C78", cmd.xtgettcap.next().?); - try testing.expect(cmd.xtgettcap.next() == null); -} - -test "DECRQSS command" { - const testing = std.testing; - const alloc = testing.allocator; - - var h: Handler = .{}; - defer h.deinit(); - h.hook(alloc, .{ .intermediates = "$", .final = 'q' }); - h.put('m'); - var cmd = h.unhook().?; - defer cmd.deinit(); - try testing.expect(cmd == .decrqss); - try testing.expect(cmd.decrqss == .sgr); -} - -test "DECRQSS invalid command" { - const testing = std.testing; - const alloc = testing.allocator; - - var h: Handler = .{}; - defer h.deinit(); - h.hook(alloc, .{ .intermediates = "$", .final = 'q' }); - h.put('z'); - var cmd = h.unhook().?; - defer cmd.deinit(); - try testing.expect(cmd == .decrqss); - try testing.expect(cmd.decrqss == .none); - - h.discard(); - - h.hook(alloc, .{ .intermediates = "$", .final = 'q' }); - h.put('"'); - h.put(' '); - h.put('q'); - try testing.expect(h.unhook() == null); -} diff --git a/src/terminal-old/device_status.zig b/src/terminal-old/device_status.zig deleted file mode 100644 index 78147ddd4..000000000 --- a/src/terminal-old/device_status.zig +++ /dev/null @@ -1,67 +0,0 @@ -const std = @import("std"); - -/// An enum(u16) of the available device status requests. -pub const Request = dsr_enum: { - const EnumField = std.builtin.Type.EnumField; - var fields: [entries.len]EnumField = undefined; - for (entries, 0..) |entry, i| { - fields[i] = .{ - .name = entry.name, - .value = @as(Tag.Backing, @bitCast(Tag{ - .value = entry.value, - .question = entry.question, - })), - }; - } - - break :dsr_enum @Type(.{ .Enum = .{ - .tag_type = Tag.Backing, - .fields = &fields, - .decls = &.{}, - .is_exhaustive = true, - } }); -}; - -/// The tag type for our enum is a u16 but we use a packed struct -/// in order to pack the question bit into the tag. The "u16" size is -/// chosen somewhat arbitrarily to match the largest expected size -/// we see as a multiple of 8 bits. -pub const Tag = packed struct(u16) { - pub const Backing = @typeInfo(@This()).Struct.backing_integer.?; - value: u15, - question: bool = false, - - test "order" { - const t: Tag = .{ .value = 1 }; - const int: Backing = @bitCast(t); - try std.testing.expectEqual(@as(Backing, 1), int); - } -}; - -pub fn reqFromInt(v: u16, question: bool) ?Request { - inline for (entries) |entry| { - if (entry.value == v and entry.question == question) { - const tag: Tag = .{ .question = question, .value = entry.value }; - const int: Tag.Backing = @bitCast(tag); - return @enumFromInt(int); - } - } - - return null; -} - -/// A single entry of a possible device status request we support. The -/// "question" field determines if it is valid with or without the "?" -/// prefix. -const Entry = struct { - name: [:0]const u8, - value: comptime_int, - question: bool = false, // "?" request -}; - -/// The full list of device status request entries. -const entries: []const Entry = &.{ - .{ .name = "operating_status", .value = 5 }, - .{ .name = "cursor_position", .value = 6 }, - .{ .name = "color_scheme", .value = 996, .question = true }, -}; diff --git a/src/terminal-old/kitty.zig b/src/terminal-old/kitty.zig deleted file mode 100644 index 497dd4aba..000000000 --- a/src/terminal-old/kitty.zig +++ /dev/null @@ -1,8 +0,0 @@ -//! Types and functions related to Kitty protocols. - -pub const graphics = @import("kitty/graphics.zig"); -pub usingnamespace @import("kitty/key.zig"); - -test { - @import("std").testing.refAllDecls(@This()); -} diff --git a/src/terminal-old/kitty/graphics.zig b/src/terminal-old/kitty/graphics.zig deleted file mode 100644 index cfc45adbc..000000000 --- a/src/terminal-old/kitty/graphics.zig +++ /dev/null @@ -1,22 +0,0 @@ -//! Kitty graphics protocol support. -//! -//! Documentation: -//! https://sw.kovidgoyal.net/kitty/graphics-protocol -//! -//! Unimplemented features that are still todo: -//! - shared memory transmit -//! - virtual placement w/ unicode -//! - animation -//! -//! Performance: -//! The performance of this particular subsystem of Ghostty is not great. -//! We can avoid a lot more allocations, we can replace some C code (which -//! implicitly allocates) with native Zig, we can improve the data structures -//! to avoid repeated lookups, etc. I tried to avoid pessimization but my -//! aim to ship a v1 of this implementation came at some cost. I learned a lot -//! though and I think we can go back through and fix this up. - -pub usingnamespace @import("graphics_command.zig"); -pub usingnamespace @import("graphics_exec.zig"); -pub usingnamespace @import("graphics_image.zig"); -pub usingnamespace @import("graphics_storage.zig"); diff --git a/src/terminal-old/kitty/graphics_command.zig b/src/terminal-old/kitty/graphics_command.zig deleted file mode 100644 index ca7a4d674..000000000 --- a/src/terminal-old/kitty/graphics_command.zig +++ /dev/null @@ -1,984 +0,0 @@ -const std = @import("std"); -const assert = std.debug.assert; -const Allocator = std.mem.Allocator; -const ArenaAllocator = std.heap.ArenaAllocator; - -/// The key-value pairs for the control information for a command. The -/// keys are always single characters and the values are either single -/// characters or 32-bit unsigned integers. -/// -/// For the value of this: if the value is a single printable ASCII character -/// it is the ASCII code. Otherwise, it is parsed as a 32-bit unsigned integer. -const KV = std.AutoHashMapUnmanaged(u8, u32); - -/// Command parser parses the Kitty graphics protocol escape sequence. -pub const CommandParser = struct { - /// The memory used by the parser is stored in an arena because it is - /// all freed at the end of the command. - arena: ArenaAllocator, - - /// This is the list of KV pairs that we're building up. - kv: KV = .{}, - - /// This is used as a buffer to store the key/value of a KV pair. - /// The value of a KV pair is at most a 32-bit integer which at most - /// is 10 characters (4294967295). - kv_temp: [10]u8 = undefined, - kv_temp_len: u4 = 0, - kv_current: u8 = 0, // Current kv key - - /// This is the list of bytes that contains both KV data and final - /// data. You shouldn't access this directly. - data: std.ArrayList(u8), - - /// Internal state for parsing. - state: State = .control_key, - - const State = enum { - /// Parsing k/v pairs. The "ignore" variants are in that state - /// but ignore any data because we know they're invalid. - control_key, - control_key_ignore, - control_value, - control_value_ignore, - - /// We're parsing the data blob. - data, - }; - - /// Initialize the parser. The allocator given will be used for both - /// temporary data and long-lived values such as the final image blob. - pub fn init(alloc: Allocator) CommandParser { - var arena = ArenaAllocator.init(alloc); - errdefer arena.deinit(); - return .{ - .arena = arena, - .data = std.ArrayList(u8).init(alloc), - }; - } - - pub fn deinit(self: *CommandParser) void { - // We don't free the hash map because its in the arena - self.arena.deinit(); - self.data.deinit(); - } - - /// Feed a single byte to the parser. - /// - /// The first byte to start parsing should be the byte immediately following - /// the "G" in the APC sequence, i.e. "\x1b_G123" the first byte should - /// be "1". - pub fn feed(self: *CommandParser, c: u8) !void { - switch (self.state) { - .control_key => switch (c) { - // '=' means the key is complete and we're moving to the value. - '=' => if (self.kv_temp_len != 1) { - // All control keys are a single character right now so - // if we're not a single character just ignore follow-up - // data. - self.state = .control_value_ignore; - self.kv_temp_len = 0; - } else { - self.kv_current = self.kv_temp[0]; - self.kv_temp_len = 0; - self.state = .control_value; - }, - - else => try self.accumulateValue(c, .control_key_ignore), - }, - - .control_key_ignore => switch (c) { - '=' => self.state = .control_value_ignore, - else => {}, - }, - - .control_value => switch (c) { - ',' => try self.finishValue(.control_key), // move to next key - ';' => try self.finishValue(.data), // move to data - else => try self.accumulateValue(c, .control_value_ignore), - }, - - .control_value_ignore => switch (c) { - ',' => self.state = .control_key_ignore, - ';' => self.state = .data, - else => {}, - }, - - .data => try self.data.append(c), - } - - // We always add to our data list because this is our stable - // array of bytes that we'll reference everywhere else. - } - - /// Complete the parsing. This must be called after all the - /// bytes have been fed to the parser. - /// - /// The allocator given will be used for the long-lived data - /// of the final command. - pub fn complete(self: *CommandParser) !Command { - switch (self.state) { - // We can't ever end in the control key state and be valid. - // This means the command looked something like "a=1,b" - .control_key, .control_key_ignore => return error.InvalidFormat, - - // Some commands (i.e. placements) end without extra data so - // we end in the value state. i.e. "a=1,b=2" - .control_value => try self.finishValue(.data), - .control_value_ignore => {}, - - // Most commands end in data, i.e. "a=1,b=2;1234" - .data => {}, - } - - // Determine our action, which is always a single character. - const action: u8 = action: { - const value = self.kv.get('a') orelse break :action 't'; - const c = std.math.cast(u8, value) orelse return error.InvalidFormat; - break :action c; - }; - const control: Command.Control = switch (action) { - 'q' => .{ .query = try Transmission.parse(self.kv) }, - 't' => .{ .transmit = try Transmission.parse(self.kv) }, - 'T' => .{ .transmit_and_display = .{ - .transmission = try Transmission.parse(self.kv), - .display = try Display.parse(self.kv), - } }, - 'p' => .{ .display = try Display.parse(self.kv) }, - 'd' => .{ .delete = try Delete.parse(self.kv) }, - 'f' => .{ .transmit_animation_frame = try AnimationFrameLoading.parse(self.kv) }, - 'a' => .{ .control_animation = try AnimationControl.parse(self.kv) }, - 'c' => .{ .compose_animation = try AnimationFrameComposition.parse(self.kv) }, - else => return error.InvalidFormat, - }; - - // Determine our quiet value - const quiet: Command.Quiet = if (self.kv.get('q')) |v| quiet: { - break :quiet switch (v) { - 0 => .no, - 1 => .ok, - 2 => .failures, - else => return error.InvalidFormat, - }; - } else .no; - - return .{ - .control = control, - .quiet = quiet, - .data = if (self.data.items.len == 0) "" else data: { - break :data try self.data.toOwnedSlice(); - }, - }; - } - - fn accumulateValue(self: *CommandParser, c: u8, overflow_state: State) !void { - const idx = self.kv_temp_len; - self.kv_temp_len += 1; - if (self.kv_temp_len > self.kv_temp.len) { - self.state = overflow_state; - self.kv_temp_len = 0; - return; - } - self.kv_temp[idx] = c; - } - - fn finishValue(self: *CommandParser, next_state: State) !void { - const alloc = self.arena.allocator(); - - // We can move states right away, we don't use it. - self.state = next_state; - - // Check for ASCII chars first - if (self.kv_temp_len == 1) { - const c = self.kv_temp[0]; - if (c < '0' or c > '9') { - try self.kv.put(alloc, self.kv_current, @intCast(c)); - self.kv_temp_len = 0; - return; - } - } - - // Only "z" is currently signed. This is a bit of a kloodge; if more - // fields become signed we can rethink this but for now we parse - // "z" as i32 then bitcast it to u32 then bitcast it back later. - if (self.kv_current == 'z') { - const v = try std.fmt.parseInt(i32, self.kv_temp[0..self.kv_temp_len], 10); - try self.kv.put(alloc, self.kv_current, @bitCast(v)); - } else { - const v = try std.fmt.parseInt(u32, self.kv_temp[0..self.kv_temp_len], 10); - try self.kv.put(alloc, self.kv_current, v); - } - - // Clear our temp buffer - self.kv_temp_len = 0; - } -}; - -/// Represents a possible response to a command. -pub const Response = struct { - id: u32 = 0, - image_number: u32 = 0, - placement_id: u32 = 0, - message: []const u8 = "OK", - - pub fn encode(self: Response, writer: anytype) !void { - // We only encode a result if we have either an id or an image number. - if (self.id == 0 and self.image_number == 0) return; - - try writer.writeAll("\x1b_G"); - if (self.id > 0) { - try writer.print("i={}", .{self.id}); - } - if (self.image_number > 0) { - if (self.id > 0) try writer.writeByte(','); - try writer.print("I={}", .{self.image_number}); - } - if (self.placement_id > 0) { - try writer.print(",p={}", .{self.placement_id}); - } - try writer.writeByte(';'); - try writer.writeAll(self.message); - try writer.writeAll("\x1b\\"); - } - - /// Returns true if this response is not an error. - pub fn ok(self: Response) bool { - return std.mem.eql(u8, self.message, "OK"); - } -}; - -pub const Command = struct { - control: Control, - quiet: Quiet = .no, - data: []const u8 = "", - - pub const Action = enum { - query, // q - transmit, // t - transmit_and_display, // T - display, // p - delete, // d - transmit_animation_frame, // f - control_animation, // a - compose_animation, // c - }; - - pub const Quiet = enum { - no, // 0 - ok, // 1 - failures, // 2 - }; - - pub const Control = union(Action) { - query: Transmission, - transmit: Transmission, - transmit_and_display: struct { - transmission: Transmission, - display: Display, - }, - display: Display, - delete: Delete, - transmit_animation_frame: AnimationFrameLoading, - control_animation: AnimationControl, - compose_animation: AnimationFrameComposition, - }; - - /// Take ownership over the data in this command. If the returned value - /// has a length of zero, then the data was empty and need not be freed. - pub fn toOwnedData(self: *Command) []const u8 { - const result = self.data; - self.data = ""; - return result; - } - - /// Returns the transmission data if it has any. - pub fn transmission(self: Command) ?Transmission { - return switch (self.control) { - .query => |t| t, - .transmit => |t| t, - .transmit_and_display => |t| t.transmission, - else => null, - }; - } - - /// Returns the display data if it has any. - pub fn display(self: Command) ?Display { - return switch (self.control) { - .display => |d| d, - .transmit_and_display => |t| t.display, - else => null, - }; - } - - pub fn deinit(self: Command, alloc: Allocator) void { - if (self.data.len > 0) alloc.free(self.data); - } -}; - -pub const Transmission = struct { - format: Format = .rgb, // f - medium: Medium = .direct, // t - width: u32 = 0, // s - height: u32 = 0, // v - size: u32 = 0, // S - offset: u32 = 0, // O - image_id: u32 = 0, // i - image_number: u32 = 0, // I - placement_id: u32 = 0, // p - compression: Compression = .none, // o - more_chunks: bool = false, // m - - pub const Format = enum { - rgb, // 24 - rgba, // 32 - png, // 100 - - // The following are not supported directly via the protocol - // but they are formats that a png may decode to that we - // support. - grey_alpha, - }; - - pub const Medium = enum { - direct, // d - file, // f - temporary_file, // t - shared_memory, // s - }; - - pub const Compression = enum { - none, - zlib_deflate, // z - }; - - fn parse(kv: KV) !Transmission { - var result: Transmission = .{}; - if (kv.get('f')) |v| { - result.format = switch (v) { - 24 => .rgb, - 32 => .rgba, - 100 => .png, - else => return error.InvalidFormat, - }; - } - - if (kv.get('t')) |v| { - const c = std.math.cast(u8, v) orelse return error.InvalidFormat; - result.medium = switch (c) { - 'd' => .direct, - 'f' => .file, - 't' => .temporary_file, - 's' => .shared_memory, - else => return error.InvalidFormat, - }; - } - - if (kv.get('s')) |v| { - result.width = v; - } - - if (kv.get('v')) |v| { - result.height = v; - } - - if (kv.get('S')) |v| { - result.size = v; - } - - if (kv.get('O')) |v| { - result.offset = v; - } - - if (kv.get('i')) |v| { - result.image_id = v; - } - - if (kv.get('I')) |v| { - result.image_number = v; - } - - if (kv.get('p')) |v| { - result.placement_id = v; - } - - if (kv.get('o')) |v| { - const c = std.math.cast(u8, v) orelse return error.InvalidFormat; - result.compression = switch (c) { - 'z' => .zlib_deflate, - else => return error.InvalidFormat, - }; - } - - if (kv.get('m')) |v| { - result.more_chunks = v > 0; - } - - return result; - } -}; - -pub const Display = struct { - image_id: u32 = 0, // i - image_number: u32 = 0, // I - placement_id: u32 = 0, // p - x: u32 = 0, // x - y: u32 = 0, // y - width: u32 = 0, // w - height: u32 = 0, // h - x_offset: u32 = 0, // X - y_offset: u32 = 0, // Y - columns: u32 = 0, // c - rows: u32 = 0, // r - cursor_movement: CursorMovement = .after, // C - virtual_placement: bool = false, // U - z: i32 = 0, // z - - pub const CursorMovement = enum { - after, // 0 - none, // 1 - }; - - fn parse(kv: KV) !Display { - var result: Display = .{}; - - if (kv.get('i')) |v| { - result.image_id = v; - } - - if (kv.get('I')) |v| { - result.image_number = v; - } - - if (kv.get('p')) |v| { - result.placement_id = v; - } - - if (kv.get('x')) |v| { - result.x = v; - } - - if (kv.get('y')) |v| { - result.y = v; - } - - if (kv.get('w')) |v| { - result.width = v; - } - - if (kv.get('h')) |v| { - result.height = v; - } - - if (kv.get('X')) |v| { - result.x_offset = v; - } - - if (kv.get('Y')) |v| { - result.y_offset = v; - } - - if (kv.get('c')) |v| { - result.columns = v; - } - - if (kv.get('r')) |v| { - result.rows = v; - } - - if (kv.get('C')) |v| { - result.cursor_movement = switch (v) { - 0 => .after, - 1 => .none, - else => return error.InvalidFormat, - }; - } - - if (kv.get('U')) |v| { - result.virtual_placement = switch (v) { - 0 => false, - 1 => true, - else => return error.InvalidFormat, - }; - } - - if (kv.get('z')) |v| { - // We can bitcast here because of how we parse it earlier. - result.z = @bitCast(v); - } - - return result; - } -}; - -pub const AnimationFrameLoading = struct { - x: u32 = 0, // x - y: u32 = 0, // y - create_frame: u32 = 0, // c - edit_frame: u32 = 0, // r - gap_ms: u32 = 0, // z - composition_mode: CompositionMode = .alpha_blend, // X - background: Background = .{}, // Y - - pub const Background = packed struct(u32) { - r: u8 = 0, - g: u8 = 0, - b: u8 = 0, - a: u8 = 0, - }; - - fn parse(kv: KV) !AnimationFrameLoading { - var result: AnimationFrameLoading = .{}; - - if (kv.get('x')) |v| { - result.x = v; - } - - if (kv.get('y')) |v| { - result.y = v; - } - - if (kv.get('c')) |v| { - result.create_frame = v; - } - - if (kv.get('r')) |v| { - result.edit_frame = v; - } - - if (kv.get('z')) |v| { - result.gap_ms = v; - } - - if (kv.get('X')) |v| { - result.composition_mode = switch (v) { - 0 => .alpha_blend, - 1 => .overwrite, - else => return error.InvalidFormat, - }; - } - - if (kv.get('Y')) |v| { - result.background = @bitCast(v); - } - - return result; - } -}; - -pub const AnimationFrameComposition = struct { - frame: u32 = 0, // c - edit_frame: u32 = 0, // r - x: u32 = 0, // x - y: u32 = 0, // y - width: u32 = 0, // w - height: u32 = 0, // h - left_edge: u32 = 0, // X - top_edge: u32 = 0, // Y - composition_mode: CompositionMode = .alpha_blend, // C - - fn parse(kv: KV) !AnimationFrameComposition { - var result: AnimationFrameComposition = .{}; - - if (kv.get('c')) |v| { - result.frame = v; - } - - if (kv.get('r')) |v| { - result.edit_frame = v; - } - - if (kv.get('x')) |v| { - result.x = v; - } - - if (kv.get('y')) |v| { - result.y = v; - } - - if (kv.get('w')) |v| { - result.width = v; - } - - if (kv.get('h')) |v| { - result.height = v; - } - - if (kv.get('X')) |v| { - result.left_edge = v; - } - - if (kv.get('Y')) |v| { - result.top_edge = v; - } - - if (kv.get('C')) |v| { - result.composition_mode = switch (v) { - 0 => .alpha_blend, - 1 => .overwrite, - else => return error.InvalidFormat, - }; - } - - return result; - } -}; - -pub const AnimationControl = struct { - action: AnimationAction = .invalid, // s - frame: u32 = 0, // r - gap_ms: u32 = 0, // z - current_frame: u32 = 0, // c - loops: u32 = 0, // v - - pub const AnimationAction = enum { - invalid, // 0 - stop, // 1 - run_wait, // 2 - run, // 3 - }; - - fn parse(kv: KV) !AnimationControl { - var result: AnimationControl = .{}; - - if (kv.get('s')) |v| { - result.action = switch (v) { - 0 => .invalid, - 1 => .stop, - 2 => .run_wait, - 3 => .run, - else => return error.InvalidFormat, - }; - } - - if (kv.get('r')) |v| { - result.frame = v; - } - - if (kv.get('z')) |v| { - result.gap_ms = v; - } - - if (kv.get('c')) |v| { - result.current_frame = v; - } - - if (kv.get('v')) |v| { - result.loops = v; - } - - return result; - } -}; - -pub const Delete = union(enum) { - // a/A - all: bool, - - // i/I - id: struct { - delete: bool = false, // uppercase - image_id: u32 = 0, // i - placement_id: u32 = 0, // p - }, - - // n/N - newest: struct { - delete: bool = false, // uppercase - image_number: u32 = 0, // I - placement_id: u32 = 0, // p - }, - - // c/C, - intersect_cursor: bool, - - // f/F - animation_frames: bool, - - // p/P - intersect_cell: struct { - delete: bool = false, // uppercase - x: u32 = 0, // x - y: u32 = 0, // y - }, - - // q/Q - intersect_cell_z: struct { - delete: bool = false, // uppercase - x: u32 = 0, // x - y: u32 = 0, // y - z: i32 = 0, // z - }, - - // x/X - column: struct { - delete: bool = false, // uppercase - x: u32 = 0, // x - }, - - // y/Y - row: struct { - delete: bool = false, // uppercase - y: u32 = 0, // y - }, - - // z/Z - z: struct { - delete: bool = false, // uppercase - z: i32 = 0, // z - }, - - fn parse(kv: KV) !Delete { - const what: u8 = what: { - const value = kv.get('d') orelse break :what 'a'; - const c = std.math.cast(u8, value) orelse return error.InvalidFormat; - break :what c; - }; - - return switch (what) { - 'a', 'A' => .{ .all = what == 'A' }, - - 'i', 'I' => blk: { - var result: Delete = .{ .id = .{ .delete = what == 'I' } }; - if (kv.get('i')) |v| { - result.id.image_id = v; - } - if (kv.get('p')) |v| { - result.id.placement_id = v; - } - - break :blk result; - }, - - 'n', 'N' => blk: { - var result: Delete = .{ .newest = .{ .delete = what == 'N' } }; - if (kv.get('I')) |v| { - result.newest.image_number = v; - } - if (kv.get('p')) |v| { - result.newest.placement_id = v; - } - - break :blk result; - }, - - 'c', 'C' => .{ .intersect_cursor = what == 'C' }, - - 'f', 'F' => .{ .animation_frames = what == 'F' }, - - 'p', 'P' => blk: { - var result: Delete = .{ .intersect_cell = .{ .delete = what == 'P' } }; - if (kv.get('x')) |v| { - result.intersect_cell.x = v; - } - if (kv.get('y')) |v| { - result.intersect_cell.y = v; - } - - break :blk result; - }, - - 'q', 'Q' => blk: { - var result: Delete = .{ .intersect_cell_z = .{ .delete = what == 'Q' } }; - if (kv.get('x')) |v| { - result.intersect_cell_z.x = v; - } - if (kv.get('y')) |v| { - result.intersect_cell_z.y = v; - } - if (kv.get('z')) |v| { - // We can bitcast here because of how we parse it earlier. - result.intersect_cell_z.z = @bitCast(v); - } - - break :blk result; - }, - - 'x', 'X' => blk: { - var result: Delete = .{ .column = .{ .delete = what == 'X' } }; - if (kv.get('x')) |v| { - result.column.x = v; - } - - break :blk result; - }, - - 'y', 'Y' => blk: { - var result: Delete = .{ .row = .{ .delete = what == 'Y' } }; - if (kv.get('y')) |v| { - result.row.y = v; - } - - break :blk result; - }, - - 'z', 'Z' => blk: { - var result: Delete = .{ .z = .{ .delete = what == 'Z' } }; - if (kv.get('z')) |v| { - // We can bitcast here because of how we parse it earlier. - result.z.z = @bitCast(v); - } - - break :blk result; - }, - - else => return error.InvalidFormat, - }; - } -}; - -pub const CompositionMode = enum { - alpha_blend, // 0 - overwrite, // 1 -}; - -test "transmission command" { - const testing = std.testing; - const alloc = testing.allocator; - var p = CommandParser.init(alloc); - defer p.deinit(); - - const input = "f=24,s=10,v=20"; - for (input) |c| try p.feed(c); - const command = try p.complete(); - defer command.deinit(alloc); - - try testing.expect(command.control == .transmit); - const v = command.control.transmit; - try testing.expectEqual(Transmission.Format.rgb, v.format); - try testing.expectEqual(@as(u32, 10), v.width); - try testing.expectEqual(@as(u32, 20), v.height); -} - -test "query command" { - const testing = std.testing; - const alloc = testing.allocator; - var p = CommandParser.init(alloc); - defer p.deinit(); - - const input = "i=31,s=1,v=1,a=q,t=d,f=24;AAAA"; - for (input) |c| try p.feed(c); - const command = try p.complete(); - defer command.deinit(alloc); - - try testing.expect(command.control == .query); - const v = command.control.query; - try testing.expectEqual(Transmission.Medium.direct, v.medium); - try testing.expectEqual(@as(u32, 1), v.width); - try testing.expectEqual(@as(u32, 1), v.height); - try testing.expectEqual(@as(u32, 31), v.image_id); - try testing.expectEqualStrings("AAAA", command.data); -} - -test "display command" { - const testing = std.testing; - const alloc = testing.allocator; - var p = CommandParser.init(alloc); - defer p.deinit(); - - const input = "a=p,U=1,i=31,c=80,r=120"; - for (input) |c| try p.feed(c); - const command = try p.complete(); - defer command.deinit(alloc); - - try testing.expect(command.control == .display); - const v = command.control.display; - try testing.expectEqual(@as(u32, 80), v.columns); - try testing.expectEqual(@as(u32, 120), v.rows); - try testing.expectEqual(@as(u32, 31), v.image_id); -} - -test "delete command" { - const testing = std.testing; - const alloc = testing.allocator; - var p = CommandParser.init(alloc); - defer p.deinit(); - - const input = "a=d,d=p,x=3,y=4"; - for (input) |c| try p.feed(c); - const command = try p.complete(); - defer command.deinit(alloc); - - try testing.expect(command.control == .delete); - const v = command.control.delete; - try testing.expect(v == .intersect_cell); - const dv = v.intersect_cell; - try testing.expect(!dv.delete); - try testing.expectEqual(@as(u32, 3), dv.x); - try testing.expectEqual(@as(u32, 4), dv.y); -} - -test "ignore unknown keys (long)" { - const testing = std.testing; - const alloc = testing.allocator; - var p = CommandParser.init(alloc); - defer p.deinit(); - - const input = "f=24,s=10,v=20,hello=world"; - for (input) |c| try p.feed(c); - const command = try p.complete(); - defer command.deinit(alloc); - - try testing.expect(command.control == .transmit); - const v = command.control.transmit; - try testing.expectEqual(Transmission.Format.rgb, v.format); - try testing.expectEqual(@as(u32, 10), v.width); - try testing.expectEqual(@as(u32, 20), v.height); -} - -test "ignore very long values" { - const testing = std.testing; - const alloc = testing.allocator; - var p = CommandParser.init(alloc); - defer p.deinit(); - - const input = "f=24,s=10,v=2000000000000000000000000000000000000000"; - for (input) |c| try p.feed(c); - const command = try p.complete(); - defer command.deinit(alloc); - - try testing.expect(command.control == .transmit); - const v = command.control.transmit; - try testing.expectEqual(Transmission.Format.rgb, v.format); - try testing.expectEqual(@as(u32, 10), v.width); - try testing.expectEqual(@as(u32, 0), v.height); -} - -test "response: encode nothing without ID or image number" { - const testing = std.testing; - var buf: [1024]u8 = undefined; - var fbs = std.io.fixedBufferStream(&buf); - - var r: Response = .{}; - try r.encode(fbs.writer()); - try testing.expectEqualStrings("", fbs.getWritten()); -} - -test "response: encode with only image id" { - const testing = std.testing; - var buf: [1024]u8 = undefined; - var fbs = std.io.fixedBufferStream(&buf); - - var r: Response = .{ .id = 4 }; - try r.encode(fbs.writer()); - try testing.expectEqualStrings("\x1b_Gi=4;OK\x1b\\", fbs.getWritten()); -} - -test "response: encode with only image number" { - const testing = std.testing; - var buf: [1024]u8 = undefined; - var fbs = std.io.fixedBufferStream(&buf); - - var r: Response = .{ .image_number = 4 }; - try r.encode(fbs.writer()); - try testing.expectEqualStrings("\x1b_GI=4;OK\x1b\\", fbs.getWritten()); -} - -test "response: encode with image ID and number" { - const testing = std.testing; - var buf: [1024]u8 = undefined; - var fbs = std.io.fixedBufferStream(&buf); - - var r: Response = .{ .id = 12, .image_number = 4 }; - try r.encode(fbs.writer()); - try testing.expectEqualStrings("\x1b_Gi=12,I=4;OK\x1b\\", fbs.getWritten()); -} diff --git a/src/terminal-old/kitty/graphics_exec.zig b/src/terminal-old/kitty/graphics_exec.zig deleted file mode 100644 index b4047c1d5..000000000 --- a/src/terminal-old/kitty/graphics_exec.zig +++ /dev/null @@ -1,344 +0,0 @@ -const std = @import("std"); -const assert = std.debug.assert; -const Allocator = std.mem.Allocator; - -const renderer = @import("../../renderer.zig"); -const point = @import("../point.zig"); -const Terminal = @import("../Terminal.zig"); -const command = @import("graphics_command.zig"); -const image = @import("graphics_image.zig"); -const Command = command.Command; -const Response = command.Response; -const LoadingImage = image.LoadingImage; -const Image = image.Image; -const ImageStorage = @import("graphics_storage.zig").ImageStorage; - -const log = std.log.scoped(.kitty_gfx); - -/// Execute a Kitty graphics command against the given terminal. This -/// will never fail, but the response may indicate an error and the -/// terminal state may not be updated to reflect the command. This will -/// never put the terminal in an unrecoverable state, however. -/// -/// The allocator must be the same allocator that was used to build -/// the command. -pub fn execute( - alloc: Allocator, - terminal: *Terminal, - cmd: *Command, -) ?Response { - // If storage is disabled then we disable the full protocol. This means - // we don't even respond to queries so the terminal completely acts as - // if this feature is not supported. - if (!terminal.screen.kitty_images.enabled()) { - log.debug("kitty graphics requested but disabled", .{}); - return null; - } - - log.debug("executing kitty graphics command: quiet={} control={}", .{ - cmd.quiet, - cmd.control, - }); - - const resp_: ?Response = switch (cmd.control) { - .query => query(alloc, cmd), - .transmit, .transmit_and_display => transmit(alloc, terminal, cmd), - .display => display(alloc, terminal, cmd), - .delete => delete(alloc, terminal, cmd), - - .transmit_animation_frame, - .control_animation, - .compose_animation, - => .{ .message = "ERROR: unimplemented action" }, - }; - - // Handle the quiet settings - if (resp_) |resp| { - if (!resp.ok()) { - log.warn("erroneous kitty graphics response: {s}", .{resp.message}); - } - - return switch (cmd.quiet) { - .no => resp, - .ok => if (resp.ok()) null else resp, - .failures => null, - }; - } - - return null; -} -/// Execute a "query" command. -/// -/// This command is used to attempt to load an image and respond with -/// success/error but does not persist any of the command to the terminal -/// state. -fn query(alloc: Allocator, cmd: *Command) Response { - const t = cmd.control.query; - - // Query requires image ID. We can't actually send a response without - // an image ID either but we return an error and this will be logged - // downstream. - if (t.image_id == 0) { - return .{ .message = "EINVAL: image ID required" }; - } - - // Build a partial response to start - var result: Response = .{ - .id = t.image_id, - .image_number = t.image_number, - .placement_id = t.placement_id, - }; - - // Attempt to load the image. If we cannot, then set an appropriate error. - var loading = LoadingImage.init(alloc, cmd) catch |err| { - encodeError(&result, err); - return result; - }; - loading.deinit(alloc); - - return result; -} - -/// Transmit image data. -/// -/// This loads the image, validates it, and puts it into the terminal -/// screen storage. It does not display the image. -fn transmit( - alloc: Allocator, - terminal: *Terminal, - cmd: *Command, -) Response { - const t = cmd.transmission().?; - var result: Response = .{ - .id = t.image_id, - .image_number = t.image_number, - .placement_id = t.placement_id, - }; - if (t.image_id > 0 and t.image_number > 0) { - return .{ .message = "EINVAL: image ID and number are mutually exclusive" }; - } - - const load = loadAndAddImage(alloc, terminal, cmd) catch |err| { - encodeError(&result, err); - return result; - }; - errdefer load.image.deinit(alloc); - - // If we're also displaying, then do that now. This function does - // both transmit and transmit and display. The display might also be - // deferred if it is multi-chunk. - if (load.display) |d| { - assert(!load.more); - var d_copy = d; - d_copy.image_id = load.image.id; - return display(alloc, terminal, &.{ - .control = .{ .display = d_copy }, - .quiet = cmd.quiet, - }); - } - - // If there are more chunks expected we do not respond. - if (load.more) return .{}; - - // After the image is added, set the ID in case it changed - result.id = load.image.id; - - // If the original request had an image number, then we respond. - // Otherwise, we don't respond. - if (load.image.number == 0) return .{}; - - return result; -} - -/// Display a previously transmitted image. -fn display( - alloc: Allocator, - terminal: *Terminal, - cmd: *const Command, -) Response { - const d = cmd.display().?; - - // Display requires image ID or number. - if (d.image_id == 0 and d.image_number == 0) { - return .{ .message = "EINVAL: image ID or number required" }; - } - - // Build up our response - var result: Response = .{ - .id = d.image_id, - .image_number = d.image_number, - .placement_id = d.placement_id, - }; - - // Verify the requested image exists if we have an ID - const storage = &terminal.screen.kitty_images; - const img_: ?Image = if (d.image_id != 0) - storage.imageById(d.image_id) - else - storage.imageByNumber(d.image_number); - const img = img_ orelse { - result.message = "EINVAL: image not found"; - return result; - }; - - // Make sure our response has the image id in case we looked up by number - result.id = img.id; - - // Determine the screen point for the placement. - const placement_point = (point.Viewport{ - .x = terminal.screen.cursor.x, - .y = terminal.screen.cursor.y, - }).toScreen(&terminal.screen); - - // Add the placement - const p: ImageStorage.Placement = .{ - .point = placement_point, - .x_offset = d.x_offset, - .y_offset = d.y_offset, - .source_x = d.x, - .source_y = d.y, - .source_width = d.width, - .source_height = d.height, - .columns = d.columns, - .rows = d.rows, - .z = d.z, - }; - storage.addPlacement( - alloc, - img.id, - result.placement_id, - p, - ) catch |err| { - encodeError(&result, err); - return result; - }; - - // Cursor needs to move after placement - switch (d.cursor_movement) { - .none => {}, - .after => { - const rect = p.rect(img, terminal); - - // We can do better by doing this with pure internal screen state - // but this handles scroll regions. - const height = rect.bottom_right.y - rect.top_left.y; - for (0..height) |_| terminal.index() catch |err| { - log.warn("failed to move cursor: {}", .{err}); - break; - }; - - terminal.setCursorPos( - terminal.screen.cursor.y, - rect.bottom_right.x + 1, - ); - }, - } - - // Display does not result in a response on success - return .{}; -} - -/// Display a previously transmitted image. -fn delete( - alloc: Allocator, - terminal: *Terminal, - cmd: *Command, -) Response { - const storage = &terminal.screen.kitty_images; - storage.delete(alloc, terminal, cmd.control.delete); - - // Delete never responds on success - return .{}; -} - -fn loadAndAddImage( - alloc: Allocator, - terminal: *Terminal, - cmd: *Command, -) !struct { - image: Image, - more: bool = false, - display: ?command.Display = null, -} { - const t = cmd.transmission().?; - const storage = &terminal.screen.kitty_images; - - // Determine our image. This also handles chunking and early exit. - var loading: LoadingImage = if (storage.loading) |loading| loading: { - // Note: we do NOT want to call "cmd.toOwnedData" here because - // we're _copying_ the data. We want the command data to be freed. - try loading.addData(alloc, cmd.data); - - // If we have more then we're done - if (t.more_chunks) return .{ .image = loading.image, .more = true }; - - // We have no more chunks. We're going to be completing the - // image so we want to destroy the pointer to the loading - // image and copy it out. - defer { - alloc.destroy(loading); - storage.loading = null; - } - - break :loading loading.*; - } else try LoadingImage.init(alloc, cmd); - - // We only want to deinit on error. If we're chunking, then we don't - // want to deinit at all. If we're not chunking, then we'll deinit - // after we've copied the image out. - errdefer loading.deinit(alloc); - - // If the image has no ID, we assign one - if (loading.image.id == 0) { - loading.image.id = storage.next_image_id; - storage.next_image_id +%= 1; - } - - // If this is chunked, this is the beginning of a new chunked transmission. - // (We checked for an in-progress chunk above.) - if (t.more_chunks) { - // We allocate the pointer on the heap because its rare and we - // don't want to always pay the memory cost to keep it around. - const loading_ptr = try alloc.create(LoadingImage); - errdefer alloc.destroy(loading_ptr); - loading_ptr.* = loading; - storage.loading = loading_ptr; - return .{ .image = loading.image, .more = true }; - } - - // Dump the image data before it is decompressed - // loading.debugDump() catch unreachable; - - // Validate and store our image - var img = try loading.complete(alloc); - errdefer img.deinit(alloc); - try storage.addImage(alloc, img); - - // Get our display settings - const display_ = loading.display; - - // Ensure we deinit the loading state because we're done. The image - // won't be deinit because of "complete" above. - loading.deinit(alloc); - - return .{ .image = img, .display = display_ }; -} - -const EncodeableError = Image.Error || Allocator.Error; - -/// Encode an error code into a message for a response. -fn encodeError(r: *Response, err: EncodeableError) void { - switch (err) { - error.OutOfMemory => r.message = "ENOMEM: out of memory", - error.InternalError => r.message = "EINVAL: internal error", - error.InvalidData => r.message = "EINVAL: invalid data", - error.DecompressionFailed => r.message = "EINVAL: decompression failed", - error.FilePathTooLong => r.message = "EINVAL: file path too long", - error.TemporaryFileNotInTempDir => r.message = "EINVAL: temporary file not in temp dir", - error.UnsupportedFormat => r.message = "EINVAL: unsupported format", - error.UnsupportedMedium => r.message = "EINVAL: unsupported medium", - error.UnsupportedDepth => r.message = "EINVAL: unsupported pixel depth", - error.DimensionsRequired => r.message = "EINVAL: dimensions required", - error.DimensionsTooLarge => r.message = "EINVAL: dimensions too large", - } -} diff --git a/src/terminal-old/kitty/graphics_image.zig b/src/terminal-old/kitty/graphics_image.zig deleted file mode 100644 index 249d8878f..000000000 --- a/src/terminal-old/kitty/graphics_image.zig +++ /dev/null @@ -1,776 +0,0 @@ -const std = @import("std"); -const builtin = @import("builtin"); -const assert = std.debug.assert; -const Allocator = std.mem.Allocator; -const ArenaAllocator = std.heap.ArenaAllocator; - -const command = @import("graphics_command.zig"); -const point = @import("../point.zig"); -const internal_os = @import("../../os/main.zig"); -const stb = @import("../../stb/main.zig"); - -const log = std.log.scoped(.kitty_gfx); - -/// Maximum width or height of an image. Taken directly from Kitty. -const max_dimension = 10000; - -/// Maximum size in bytes, taken from Kitty. -const max_size = 400 * 1024 * 1024; // 400MB - -/// An image that is still being loaded. The image should be initialized -/// using init on the first chunk and then addData for each subsequent -/// chunk. Once all chunks have been added, complete should be called -/// to finalize the image. -pub const LoadingImage = struct { - /// The in-progress image. The first chunk must have all the metadata - /// so this comes from that initially. - image: Image, - - /// The data that is being built up. - data: std.ArrayListUnmanaged(u8) = .{}, - - /// This is non-null when a transmit and display command is given - /// so that we display the image after it is fully loaded. - display: ?command.Display = null, - - /// Initialize a chunked immage from the first image transmission. - /// If this is a multi-chunk image, this should only be the FIRST - /// chunk. - pub fn init(alloc: Allocator, cmd: *command.Command) !LoadingImage { - // Build our initial image from the properties sent via the control. - // These can be overwritten by the data loading process. For example, - // PNG loading sets the width/height from the data. - const t = cmd.transmission().?; - var result: LoadingImage = .{ - .image = .{ - .id = t.image_id, - .number = t.image_number, - .width = t.width, - .height = t.height, - .compression = t.compression, - .format = t.format, - }, - - .display = cmd.display(), - }; - - // Special case for the direct medium, we just add it directly - // which will handle copying the data, base64 decoding, etc. - if (t.medium == .direct) { - try result.addData(alloc, cmd.data); - return result; - } - - // For every other medium, we'll need to at least base64 decode - // the data to make it useful so let's do that. Also, all the data - // has to be path data so we can put it in a stack-allocated buffer. - var buf: [std.fs.MAX_PATH_BYTES]u8 = undefined; - const Base64Decoder = std.base64.standard.Decoder; - const size = Base64Decoder.calcSizeForSlice(cmd.data) catch |err| { - log.warn("failed to calculate base64 size for file path: {}", .{err}); - return error.InvalidData; - }; - if (size > buf.len) return error.FilePathTooLong; - Base64Decoder.decode(&buf, cmd.data) catch |err| { - log.warn("failed to decode base64 data: {}", .{err}); - return error.InvalidData; - }; - - if (comptime builtin.os.tag != .windows) { - if (std.mem.indexOfScalar(u8, buf[0..size], 0) != null) { - // std.posix.realpath *asserts* that the path does not have - // internal nulls instead of erroring. - log.warn("failed to get absolute path: BadPathName", .{}); - return error.InvalidData; - } - } - - var abs_buf: [std.fs.MAX_PATH_BYTES]u8 = undefined; - const path = std.posix.realpath(buf[0..size], &abs_buf) catch |err| { - log.warn("failed to get absolute path: {}", .{err}); - return error.InvalidData; - }; - - // Depending on the medium, load the data from the path. - switch (t.medium) { - .direct => unreachable, // handled above - .file => try result.readFile(.file, alloc, t, path), - .temporary_file => try result.readFile(.temporary_file, alloc, t, path), - .shared_memory => try result.readSharedMemory(alloc, t, path), - } - - return result; - } - - /// Reads the data from a shared memory segment. - fn readSharedMemory( - self: *LoadingImage, - alloc: Allocator, - t: command.Transmission, - path: []const u8, - ) !void { - // We require libc for this for shm_open - if (comptime !builtin.link_libc) return error.UnsupportedMedium; - - // Todo: support shared memory - _ = self; - _ = alloc; - _ = t; - _ = path; - return error.UnsupportedMedium; - } - - /// Reads the data from a temporary file and returns it. This allocates - /// and does not free any of the data, so the caller must free it. - /// - /// This will also delete the temporary file if it is in a safe location. - fn readFile( - self: *LoadingImage, - comptime medium: command.Transmission.Medium, - alloc: Allocator, - t: command.Transmission, - path: []const u8, - ) !void { - switch (medium) { - .file, .temporary_file => {}, - else => @compileError("readFile only supports file and temporary_file"), - } - - // Verify file seems "safe". This is logic copied directly from Kitty, - // mostly. This is really rough but it will catch obvious bad actors. - if (std.mem.startsWith(u8, path, "/proc/") or - std.mem.startsWith(u8, path, "/sys/") or - (std.mem.startsWith(u8, path, "/dev/") and - !std.mem.startsWith(u8, path, "/dev/shm/"))) - { - return error.InvalidData; - } - - // Temporary file logic - if (medium == .temporary_file) { - if (!isPathInTempDir(path)) return error.TemporaryFileNotInTempDir; - } - defer if (medium == .temporary_file) { - std.posix.unlink(path) catch |err| { - log.warn("failed to delete temporary file: {}", .{err}); - }; - }; - - var file = std.fs.cwd().openFile(path, .{}) catch |err| { - log.warn("failed to open temporary file: {}", .{err}); - return error.InvalidData; - }; - defer file.close(); - - // File must be a regular file - if (file.stat()) |stat| { - if (stat.kind != .file) { - log.warn("file is not a regular file kind={}", .{stat.kind}); - return error.InvalidData; - } - } else |err| { - log.warn("failed to stat file: {}", .{err}); - return error.InvalidData; - } - - if (t.offset > 0) { - file.seekTo(@intCast(t.offset)) catch |err| { - log.warn("failed to seek to offset {}: {}", .{ t.offset, err }); - return error.InvalidData; - }; - } - - var buf_reader = std.io.bufferedReader(file.reader()); - const reader = buf_reader.reader(); - - // Read the file - var managed = std.ArrayList(u8).init(alloc); - errdefer managed.deinit(); - const size: usize = if (t.size > 0) @min(t.size, max_size) else max_size; - reader.readAllArrayList(&managed, size) catch |err| { - log.warn("failed to read temporary file: {}", .{err}); - return error.InvalidData; - }; - - // Set our data - assert(self.data.items.len == 0); - self.data = .{ .items = managed.items, .capacity = managed.capacity }; - } - - /// Returns true if path appears to be in a temporary directory. - /// Copies logic from Kitty. - fn isPathInTempDir(path: []const u8) bool { - if (std.mem.startsWith(u8, path, "/tmp")) return true; - if (std.mem.startsWith(u8, path, "/dev/shm")) return true; - if (internal_os.allocTmpDir(std.heap.page_allocator)) |dir| { - defer internal_os.freeTmpDir(std.heap.page_allocator, dir); - if (std.mem.startsWith(u8, path, dir)) return true; - - // The temporary dir is sometimes a symlink. On macOS for - // example /tmp is /private/var/... - var buf: [std.fs.MAX_PATH_BYTES]u8 = undefined; - if (std.posix.realpath(dir, &buf)) |real_dir| { - if (std.mem.startsWith(u8, path, real_dir)) return true; - } else |_| {} - } - - return false; - } - - pub fn deinit(self: *LoadingImage, alloc: Allocator) void { - self.image.deinit(alloc); - self.data.deinit(alloc); - } - - pub fn destroy(self: *LoadingImage, alloc: Allocator) void { - self.deinit(alloc); - alloc.destroy(self); - } - - /// Adds a chunk of base64-encoded data to the image. Use this if the - /// image is coming in chunks (the "m" parameter in the protocol). - pub fn addData(self: *LoadingImage, alloc: Allocator, data: []const u8) !void { - // If no data, skip - if (data.len == 0) return; - - // Grow our array list by size capacity if it needs it - const Base64Decoder = std.base64.standard.Decoder; - const size = Base64Decoder.calcSizeForSlice(data) catch |err| { - log.warn("failed to calculate size for base64 data: {}", .{err}); - return error.InvalidData; - }; - - // If our data would get too big, return an error - if (self.data.items.len + size > max_size) { - log.warn("image data too large max_size={}", .{max_size}); - return error.InvalidData; - } - - try self.data.ensureUnusedCapacity(alloc, size); - - // We decode directly into the arraylist - const start_i = self.data.items.len; - self.data.items.len = start_i + size; - const buf = self.data.items[start_i..]; - Base64Decoder.decode(buf, data) catch |err| switch (err) { - // We have to ignore invalid padding because lots of encoders - // add the wrong padding. Since we validate image data later - // (PNG decode or simple dimensions check), we can ignore this. - error.InvalidPadding => {}, - - else => { - log.warn("failed to decode base64 data: {}", .{err}); - return error.InvalidData; - }, - }; - } - - /// Complete the chunked image, returning a completed image. - pub fn complete(self: *LoadingImage, alloc: Allocator) !Image { - const img = &self.image; - - // Decompress the data if it is compressed. - try self.decompress(alloc); - - // Decode the png if we have to - if (img.format == .png) try self.decodePng(alloc); - - // Validate our dimensions. - if (img.width == 0 or img.height == 0) return error.DimensionsRequired; - if (img.width > max_dimension or img.height > max_dimension) return error.DimensionsTooLarge; - - // Data length must be what we expect - const bpp: u32 = switch (img.format) { - .grey_alpha => 2, - .rgb => 3, - .rgba => 4, - .png => unreachable, // png should be decoded by here - }; - const expected_len = img.width * img.height * bpp; - const actual_len = self.data.items.len; - if (actual_len != expected_len) { - std.log.warn( - "unexpected length image id={} width={} height={} bpp={} expected_len={} actual_len={}", - .{ img.id, img.width, img.height, bpp, expected_len, actual_len }, - ); - return error.InvalidData; - } - - // Set our time - self.image.transmit_time = std.time.Instant.now() catch |err| { - log.warn("failed to get time: {}", .{err}); - return error.InternalError; - }; - - // Everything looks good, copy the image data over. - var result = self.image; - result.data = try self.data.toOwnedSlice(alloc); - errdefer result.deinit(alloc); - self.image = .{}; - return result; - } - - /// Debug function to write the data to a file. This is useful for - /// capturing some test data for unit tests. - pub fn debugDump(self: LoadingImage) !void { - if (comptime builtin.mode != .Debug) @compileError("debugDump in non-debug"); - - var buf: [1024]u8 = undefined; - const filename = try std.fmt.bufPrint( - &buf, - "image-{s}-{s}-{d}x{d}-{}.data", - .{ - @tagName(self.image.format), - @tagName(self.image.compression), - self.image.width, - self.image.height, - self.image.id, - }, - ); - const cwd = std.fs.cwd(); - const f = try cwd.createFile(filename, .{}); - defer f.close(); - - const writer = f.writer(); - try writer.writeAll(self.data.items); - } - - /// Decompress the data in-place. - fn decompress(self: *LoadingImage, alloc: Allocator) !void { - return switch (self.image.compression) { - .none => {}, - .zlib_deflate => self.decompressZlib(alloc), - }; - } - - fn decompressZlib(self: *LoadingImage, alloc: Allocator) !void { - // Open our zlib stream - var fbs = std.io.fixedBufferStream(self.data.items); - var stream = std.compress.zlib.decompressor(fbs.reader()); - - // Write it to an array list - var list = std.ArrayList(u8).init(alloc); - errdefer list.deinit(); - stream.reader().readAllArrayList(&list, max_size) catch |err| { - log.warn("failed to read decompressed data: {}", .{err}); - return error.DecompressionFailed; - }; - - // Empty our current data list, take ownership over managed array list - self.data.deinit(alloc); - self.data = .{ .items = list.items, .capacity = list.capacity }; - - // Make sure we note that our image is no longer compressed - self.image.compression = .none; - } - - /// Decode the data as PNG. This will also updated the image dimensions. - fn decodePng(self: *LoadingImage, alloc: Allocator) !void { - assert(self.image.format == .png); - - // Decode PNG - var width: c_int = 0; - var height: c_int = 0; - var bpp: c_int = 0; - const data = stb.stbi_load_from_memory( - self.data.items.ptr, - @intCast(self.data.items.len), - &width, - &height, - &bpp, - 0, - ) orelse return error.InvalidData; - defer stb.stbi_image_free(data); - const len: usize = @intCast(width * height * bpp); - if (len > max_size) { - log.warn("png image too large size={} max_size={}", .{ len, max_size }); - return error.InvalidData; - } - - // Validate our bpp - if (bpp < 2 or bpp > 4) { - log.warn("png with unsupported bpp={}", .{bpp}); - return error.UnsupportedDepth; - } - - // Replace our data - self.data.deinit(alloc); - self.data = .{}; - try self.data.ensureUnusedCapacity(alloc, len); - try self.data.appendSlice(alloc, data[0..len]); - - // Store updated image dimensions - self.image.width = @intCast(width); - self.image.height = @intCast(height); - self.image.format = switch (bpp) { - 2 => .grey_alpha, - 3 => .rgb, - 4 => .rgba, - else => unreachable, // validated above - }; - } -}; - -/// Image represents a single fully loaded image. -pub const Image = struct { - id: u32 = 0, - number: u32 = 0, - width: u32 = 0, - height: u32 = 0, - format: command.Transmission.Format = .rgb, - compression: command.Transmission.Compression = .none, - data: []const u8 = "", - transmit_time: std.time.Instant = undefined, - - pub const Error = error{ - InternalError, - InvalidData, - DecompressionFailed, - DimensionsRequired, - DimensionsTooLarge, - FilePathTooLong, - TemporaryFileNotInTempDir, - UnsupportedFormat, - UnsupportedMedium, - UnsupportedDepth, - }; - - pub fn deinit(self: *Image, alloc: Allocator) void { - if (self.data.len > 0) alloc.free(self.data); - } - - /// Mostly for logging - pub fn withoutData(self: *const Image) Image { - var copy = self.*; - copy.data = ""; - return copy; - } -}; - -/// The rect taken up by some image placement, in grid cells. This will -/// be rounded up to the nearest grid cell since we can't place images -/// in partial grid cells. -pub const Rect = struct { - top_left: point.ScreenPoint = .{}, - bottom_right: point.ScreenPoint = .{}, - - /// True if the rect contains a given screen point. - pub fn contains(self: Rect, p: point.ScreenPoint) bool { - return p.y >= self.top_left.y and - p.y <= self.bottom_right.y and - p.x >= self.top_left.x and - p.x <= self.bottom_right.x; - } -}; - -/// Easy base64 encoding function. -fn testB64(alloc: Allocator, data: []const u8) ![]const u8 { - const B64Encoder = std.base64.standard.Encoder; - const b64 = try alloc.alloc(u8, B64Encoder.calcSize(data.len)); - errdefer alloc.free(b64); - return B64Encoder.encode(b64, data); -} - -/// Easy base64 decoding function. -fn testB64Decode(alloc: Allocator, data: []const u8) ![]const u8 { - const B64Decoder = std.base64.standard.Decoder; - const result = try alloc.alloc(u8, try B64Decoder.calcSizeForSlice(data)); - errdefer alloc.free(result); - try B64Decoder.decode(result, data); - return result; -} - -// This specifically tests we ALLOW invalid RGB data because Kitty -// documents that this should work. -test "image load with invalid RGB data" { - const testing = std.testing; - const alloc = testing.allocator; - - // _Gi=31,s=1,v=1,a=q,t=d,f=24;AAAA\ - var cmd: command.Command = .{ - .control = .{ .transmit = .{ - .format = .rgb, - .width = 1, - .height = 1, - .image_id = 31, - } }, - .data = try alloc.dupe(u8, "AAAA"), - }; - defer cmd.deinit(alloc); - var loading = try LoadingImage.init(alloc, &cmd); - defer loading.deinit(alloc); -} - -test "image load with image too wide" { - const testing = std.testing; - const alloc = testing.allocator; - - var cmd: command.Command = .{ - .control = .{ .transmit = .{ - .format = .rgb, - .width = max_dimension + 1, - .height = 1, - .image_id = 31, - } }, - .data = try alloc.dupe(u8, "AAAA"), - }; - defer cmd.deinit(alloc); - var loading = try LoadingImage.init(alloc, &cmd); - defer loading.deinit(alloc); - try testing.expectError(error.DimensionsTooLarge, loading.complete(alloc)); -} - -test "image load with image too tall" { - const testing = std.testing; - const alloc = testing.allocator; - - var cmd: command.Command = .{ - .control = .{ .transmit = .{ - .format = .rgb, - .height = max_dimension + 1, - .width = 1, - .image_id = 31, - } }, - .data = try alloc.dupe(u8, "AAAA"), - }; - defer cmd.deinit(alloc); - var loading = try LoadingImage.init(alloc, &cmd); - defer loading.deinit(alloc); - try testing.expectError(error.DimensionsTooLarge, loading.complete(alloc)); -} - -test "image load: rgb, zlib compressed, direct" { - const testing = std.testing; - const alloc = testing.allocator; - - var cmd: command.Command = .{ - .control = .{ .transmit = .{ - .format = .rgb, - .medium = .direct, - .compression = .zlib_deflate, - .height = 96, - .width = 128, - .image_id = 31, - } }, - .data = try alloc.dupe( - u8, - @embedFile("testdata/image-rgb-zlib_deflate-128x96-2147483647.data"), - ), - }; - defer cmd.deinit(alloc); - var loading = try LoadingImage.init(alloc, &cmd); - defer loading.deinit(alloc); - var img = try loading.complete(alloc); - defer img.deinit(alloc); - - // should be decompressed - try testing.expect(img.compression == .none); -} - -test "image load: rgb, not compressed, direct" { - const testing = std.testing; - const alloc = testing.allocator; - - var cmd: command.Command = .{ - .control = .{ .transmit = .{ - .format = .rgb, - .medium = .direct, - .compression = .none, - .width = 20, - .height = 15, - .image_id = 31, - } }, - .data = try alloc.dupe( - u8, - @embedFile("testdata/image-rgb-none-20x15-2147483647.data"), - ), - }; - defer cmd.deinit(alloc); - var loading = try LoadingImage.init(alloc, &cmd); - defer loading.deinit(alloc); - var img = try loading.complete(alloc); - defer img.deinit(alloc); - - // should be decompressed - try testing.expect(img.compression == .none); -} - -test "image load: rgb, zlib compressed, direct, chunked" { - const testing = std.testing; - const alloc = testing.allocator; - - const data = @embedFile("testdata/image-rgb-zlib_deflate-128x96-2147483647.data"); - - // Setup our initial chunk - var cmd: command.Command = .{ - .control = .{ .transmit = .{ - .format = .rgb, - .medium = .direct, - .compression = .zlib_deflate, - .height = 96, - .width = 128, - .image_id = 31, - .more_chunks = true, - } }, - .data = try alloc.dupe(u8, data[0..1024]), - }; - defer cmd.deinit(alloc); - var loading = try LoadingImage.init(alloc, &cmd); - defer loading.deinit(alloc); - - // Read our remaining chunks - var fbs = std.io.fixedBufferStream(data[1024..]); - var buf: [1024]u8 = undefined; - while (fbs.reader().readAll(&buf)) |size| { - try loading.addData(alloc, buf[0..size]); - if (size < buf.len) break; - } else |err| return err; - - // Complete - var img = try loading.complete(alloc); - defer img.deinit(alloc); - try testing.expect(img.compression == .none); -} - -test "image load: rgb, zlib compressed, direct, chunked with zero initial chunk" { - const testing = std.testing; - const alloc = testing.allocator; - - const data = @embedFile("testdata/image-rgb-zlib_deflate-128x96-2147483647.data"); - - // Setup our initial chunk - var cmd: command.Command = .{ - .control = .{ .transmit = .{ - .format = .rgb, - .medium = .direct, - .compression = .zlib_deflate, - .height = 96, - .width = 128, - .image_id = 31, - .more_chunks = true, - } }, - }; - defer cmd.deinit(alloc); - var loading = try LoadingImage.init(alloc, &cmd); - defer loading.deinit(alloc); - - // Read our remaining chunks - var fbs = std.io.fixedBufferStream(data); - var buf: [1024]u8 = undefined; - while (fbs.reader().readAll(&buf)) |size| { - try loading.addData(alloc, buf[0..size]); - if (size < buf.len) break; - } else |err| return err; - - // Complete - var img = try loading.complete(alloc); - defer img.deinit(alloc); - try testing.expect(img.compression == .none); -} - -test "image load: rgb, not compressed, temporary file" { - const testing = std.testing; - const alloc = testing.allocator; - - var tmp_dir = try internal_os.TempDir.init(); - defer tmp_dir.deinit(); - const data = try testB64Decode( - alloc, - @embedFile("testdata/image-rgb-none-20x15-2147483647.data"), - ); - defer alloc.free(data); - try tmp_dir.dir.writeFile("image.data", data); - - var buf: [std.fs.MAX_PATH_BYTES]u8 = undefined; - const path = try tmp_dir.dir.realpath("image.data", &buf); - - var cmd: command.Command = .{ - .control = .{ .transmit = .{ - .format = .rgb, - .medium = .temporary_file, - .compression = .none, - .width = 20, - .height = 15, - .image_id = 31, - } }, - .data = try testB64(alloc, path), - }; - defer cmd.deinit(alloc); - var loading = try LoadingImage.init(alloc, &cmd); - defer loading.deinit(alloc); - var img = try loading.complete(alloc); - defer img.deinit(alloc); - try testing.expect(img.compression == .none); - - // Temporary file should be gone - try testing.expectError(error.FileNotFound, tmp_dir.dir.access(path, .{})); -} - -test "image load: rgb, not compressed, regular file" { - const testing = std.testing; - const alloc = testing.allocator; - - var tmp_dir = try internal_os.TempDir.init(); - defer tmp_dir.deinit(); - const data = try testB64Decode( - alloc, - @embedFile("testdata/image-rgb-none-20x15-2147483647.data"), - ); - defer alloc.free(data); - try tmp_dir.dir.writeFile("image.data", data); - - var buf: [std.fs.MAX_PATH_BYTES]u8 = undefined; - const path = try tmp_dir.dir.realpath("image.data", &buf); - - var cmd: command.Command = .{ - .control = .{ .transmit = .{ - .format = .rgb, - .medium = .file, - .compression = .none, - .width = 20, - .height = 15, - .image_id = 31, - } }, - .data = try testB64(alloc, path), - }; - defer cmd.deinit(alloc); - var loading = try LoadingImage.init(alloc, &cmd); - defer loading.deinit(alloc); - var img = try loading.complete(alloc); - defer img.deinit(alloc); - try testing.expect(img.compression == .none); - try tmp_dir.dir.access(path, .{}); -} - -test "image load: png, not compressed, regular file" { - const testing = std.testing; - const alloc = testing.allocator; - - var tmp_dir = try internal_os.TempDir.init(); - defer tmp_dir.deinit(); - const data = @embedFile("testdata/image-png-none-50x76-2147483647-raw.data"); - try tmp_dir.dir.writeFile("image.data", data); - - var buf: [std.fs.MAX_PATH_BYTES]u8 = undefined; - const path = try tmp_dir.dir.realpath("image.data", &buf); - - var cmd: command.Command = .{ - .control = .{ .transmit = .{ - .format = .png, - .medium = .file, - .compression = .none, - .width = 0, - .height = 0, - .image_id = 31, - } }, - .data = try testB64(alloc, path), - }; - defer cmd.deinit(alloc); - var loading = try LoadingImage.init(alloc, &cmd); - defer loading.deinit(alloc); - var img = try loading.complete(alloc); - defer img.deinit(alloc); - try testing.expect(img.compression == .none); - try testing.expect(img.format == .rgb); - try tmp_dir.dir.access(path, .{}); -} diff --git a/src/terminal-old/kitty/graphics_storage.zig b/src/terminal-old/kitty/graphics_storage.zig deleted file mode 100644 index 6e4efc55b..000000000 --- a/src/terminal-old/kitty/graphics_storage.zig +++ /dev/null @@ -1,865 +0,0 @@ -const std = @import("std"); -const assert = std.debug.assert; -const Allocator = std.mem.Allocator; -const ArenaAllocator = std.heap.ArenaAllocator; - -const terminal = @import("../main.zig"); -const point = @import("../point.zig"); -const command = @import("graphics_command.zig"); -const Screen = @import("../Screen.zig"); -const LoadingImage = @import("graphics_image.zig").LoadingImage; -const Image = @import("graphics_image.zig").Image; -const Rect = @import("graphics_image.zig").Rect; -const Command = command.Command; -const ScreenPoint = point.ScreenPoint; - -const log = std.log.scoped(.kitty_gfx); - -/// An image storage is associated with a terminal screen (i.e. main -/// screen, alt screen) and contains all the transmitted images and -/// placements. -pub const ImageStorage = struct { - const ImageMap = std.AutoHashMapUnmanaged(u32, Image); - const PlacementMap = std.AutoHashMapUnmanaged(PlacementKey, Placement); - - /// Dirty is set to true if placements or images change. This is - /// purely informational for the renderer and doesn't affect the - /// correctness of the program. The renderer must set this to false - /// if it cares about this value. - dirty: bool = false, - - /// This is the next automatically assigned image ID. We start mid-way - /// through the u32 range to avoid collisions with buggy programs. - next_image_id: u32 = 2147483647, - - /// This is the next automatically assigned placement ID. This is never - /// user-facing so we can start at 0. This is 32-bits because we use - /// the same space for external placement IDs. We can start at zero - /// because any number is valid. - next_internal_placement_id: u32 = 0, - - /// The set of images that are currently known. - images: ImageMap = .{}, - - /// The set of placements for loaded images. - placements: PlacementMap = .{}, - - /// Non-null if there is an in-progress loading image. - loading: ?*LoadingImage = null, - - /// The total bytes of image data that have been loaded and the limit. - /// If the limit is reached, the oldest images will be evicted to make - /// space. Unused images take priority. - total_bytes: usize = 0, - total_limit: usize = 320 * 1000 * 1000, // 320MB - - pub fn deinit(self: *ImageStorage, alloc: Allocator) void { - if (self.loading) |loading| loading.destroy(alloc); - - var it = self.images.iterator(); - while (it.next()) |kv| kv.value_ptr.deinit(alloc); - self.images.deinit(alloc); - - self.placements.deinit(alloc); - } - - /// Kitty image protocol is enabled if we have a non-zero limit. - pub fn enabled(self: *const ImageStorage) bool { - return self.total_limit != 0; - } - - /// Sets the limit in bytes for the total amount of image data that - /// can be loaded. If this limit is lower, this will do an eviction - /// if necessary. If the value is zero, then Kitty image protocol will - /// be disabled. - pub fn setLimit(self: *ImageStorage, alloc: Allocator, limit: usize) !void { - // Special case disabling by quickly deleting all - if (limit == 0) { - self.deinit(alloc); - self.* = .{}; - } - - // If we re lowering our limit, check if we need to evict. - if (limit < self.total_bytes) { - const req_bytes = self.total_bytes - limit; - log.info("evicting images to lower limit, evicting={}", .{req_bytes}); - if (!try self.evictImage(alloc, req_bytes)) { - log.warn("failed to evict enough images for required bytes", .{}); - } - } - - self.total_limit = limit; - } - - /// Add an already-loaded image to the storage. This will automatically - /// free any existing image with the same ID. - pub fn addImage(self: *ImageStorage, alloc: Allocator, img: Image) Allocator.Error!void { - // If the image itself is over the limit, then error immediately - if (img.data.len > self.total_limit) return error.OutOfMemory; - - // If this would put us over the limit, then evict. - const total_bytes = self.total_bytes + img.data.len; - if (total_bytes > self.total_limit) { - const req_bytes = total_bytes - self.total_limit; - log.info("evicting images to make space for {} bytes", .{req_bytes}); - if (!try self.evictImage(alloc, req_bytes)) { - log.warn("failed to evict enough images for required bytes", .{}); - return error.OutOfMemory; - } - } - - // Do the gop op first so if it fails we don't get a partial state - const gop = try self.images.getOrPut(alloc, img.id); - - log.debug("addImage image={}", .{img: { - var copy = img; - copy.data = ""; - break :img copy; - }}); - - // Write our new image - if (gop.found_existing) { - self.total_bytes -= gop.value_ptr.data.len; - gop.value_ptr.deinit(alloc); - } - - gop.value_ptr.* = img; - self.total_bytes += img.data.len; - - self.dirty = true; - } - - /// Add a placement for a given image. The caller must verify in advance - /// the image exists to prevent memory corruption. - pub fn addPlacement( - self: *ImageStorage, - alloc: Allocator, - image_id: u32, - placement_id: u32, - p: Placement, - ) !void { - assert(self.images.get(image_id) != null); - log.debug("placement image_id={} placement_id={} placement={}\n", .{ - image_id, - placement_id, - p, - }); - - // The important piece here is that the placement ID needs to - // be marked internal if it is zero. This allows multiple placements - // to be added for the same image. If it is non-zero, then it is - // an external placement ID and we can only have one placement - // per (image id, placement id) pair. - const key: PlacementKey = .{ - .image_id = image_id, - .placement_id = if (placement_id == 0) .{ - .tag = .internal, - .id = id: { - defer self.next_internal_placement_id +%= 1; - break :id self.next_internal_placement_id; - }, - } else .{ - .tag = .external, - .id = placement_id, - }, - }; - - const gop = try self.placements.getOrPut(alloc, key); - gop.value_ptr.* = p; - - self.dirty = true; - } - - /// Get an image by its ID. If the image doesn't exist, null is returned. - pub fn imageById(self: *const ImageStorage, image_id: u32) ?Image { - return self.images.get(image_id); - } - - /// Get an image by its number. If the image doesn't exist, return null. - pub fn imageByNumber(self: *const ImageStorage, image_number: u32) ?Image { - var newest: ?Image = null; - - var it = self.images.iterator(); - while (it.next()) |kv| { - if (kv.value_ptr.number == image_number) { - if (newest == null or - kv.value_ptr.transmit_time.order(newest.?.transmit_time) == .gt) - { - newest = kv.value_ptr.*; - } - } - } - - return newest; - } - - /// Delete placements, images. - pub fn delete( - self: *ImageStorage, - alloc: Allocator, - t: *const terminal.Terminal, - cmd: command.Delete, - ) void { - switch (cmd) { - .all => |delete_images| if (delete_images) { - // We just reset our entire state. - self.deinit(alloc); - self.* = .{ - .dirty = true, - .total_limit = self.total_limit, - }; - } else { - // Delete all our placements - self.placements.deinit(alloc); - self.placements = .{}; - self.dirty = true; - }, - - .id => |v| self.deleteById( - alloc, - v.image_id, - v.placement_id, - v.delete, - ), - - .newest => |v| newest: { - const img = self.imageByNumber(v.image_number) orelse break :newest; - self.deleteById(alloc, img.id, v.placement_id, v.delete); - }, - - .intersect_cursor => |delete_images| { - const target = (point.Viewport{ - .x = t.screen.cursor.x, - .y = t.screen.cursor.y, - }).toScreen(&t.screen); - self.deleteIntersecting(alloc, t, target, delete_images, {}, null); - }, - - .intersect_cell => |v| { - const target = (point.Viewport{ .x = v.x, .y = v.y }).toScreen(&t.screen); - self.deleteIntersecting(alloc, t, target, v.delete, {}, null); - }, - - .intersect_cell_z => |v| { - const target = (point.Viewport{ .x = v.x, .y = v.y }).toScreen(&t.screen); - self.deleteIntersecting(alloc, t, target, v.delete, v.z, struct { - fn filter(ctx: i32, p: Placement) bool { - return p.z == ctx; - } - }.filter); - }, - - .column => |v| { - var it = self.placements.iterator(); - while (it.next()) |entry| { - const img = self.imageById(entry.key_ptr.image_id) orelse continue; - const rect = entry.value_ptr.rect(img, t); - if (rect.top_left.x <= v.x and rect.bottom_right.x >= v.x) { - self.placements.removeByPtr(entry.key_ptr); - if (v.delete) self.deleteIfUnused(alloc, img.id); - } - } - - // Mark dirty to force redraw - self.dirty = true; - }, - - .row => |v| { - // Get the screenpoint y - const y = (point.Viewport{ .x = 0, .y = v.y }).toScreen(&t.screen).y; - - var it = self.placements.iterator(); - while (it.next()) |entry| { - const img = self.imageById(entry.key_ptr.image_id) orelse continue; - const rect = entry.value_ptr.rect(img, t); - if (rect.top_left.y <= y and rect.bottom_right.y >= y) { - self.placements.removeByPtr(entry.key_ptr); - if (v.delete) self.deleteIfUnused(alloc, img.id); - } - } - - // Mark dirty to force redraw - self.dirty = true; - }, - - .z => |v| { - var it = self.placements.iterator(); - while (it.next()) |entry| { - if (entry.value_ptr.z == v.z) { - const image_id = entry.key_ptr.image_id; - self.placements.removeByPtr(entry.key_ptr); - if (v.delete) self.deleteIfUnused(alloc, image_id); - } - } - - // Mark dirty to force redraw - self.dirty = true; - }, - - // We don't support animation frames yet so they are successfully - // deleted! - .animation_frames => {}, - } - } - - fn deleteById( - self: *ImageStorage, - alloc: Allocator, - image_id: u32, - placement_id: u32, - delete_unused: bool, - ) void { - // If no placement, we delete all placements with the ID - if (placement_id == 0) { - var it = self.placements.iterator(); - while (it.next()) |entry| { - if (entry.key_ptr.image_id == image_id) { - self.placements.removeByPtr(entry.key_ptr); - } - } - } else { - _ = self.placements.remove(.{ - .image_id = image_id, - .placement_id = .{ .tag = .external, .id = placement_id }, - }); - } - - // If this is specified, then we also delete the image - // if it is no longer in use. - if (delete_unused) self.deleteIfUnused(alloc, image_id); - - // Mark dirty to force redraw - self.dirty = true; - } - - /// Delete an image if it is unused. - fn deleteIfUnused(self: *ImageStorage, alloc: Allocator, image_id: u32) void { - var it = self.placements.iterator(); - while (it.next()) |kv| { - if (kv.key_ptr.image_id == image_id) { - return; - } - } - - // If we get here, we can delete the image. - if (self.images.getEntry(image_id)) |entry| { - self.total_bytes -= entry.value_ptr.data.len; - entry.value_ptr.deinit(alloc); - self.images.removeByPtr(entry.key_ptr); - } - } - - /// Deletes all placements intersecting a screen point. - fn deleteIntersecting( - self: *ImageStorage, - alloc: Allocator, - t: *const terminal.Terminal, - p: point.ScreenPoint, - delete_unused: bool, - filter_ctx: anytype, - comptime filter: ?fn (@TypeOf(filter_ctx), Placement) bool, - ) void { - var it = self.placements.iterator(); - while (it.next()) |entry| { - const img = self.imageById(entry.key_ptr.image_id) orelse continue; - const rect = entry.value_ptr.rect(img, t); - if (rect.contains(p)) { - if (filter) |f| if (!f(filter_ctx, entry.value_ptr.*)) continue; - self.placements.removeByPtr(entry.key_ptr); - if (delete_unused) self.deleteIfUnused(alloc, img.id); - } - } - - // Mark dirty to force redraw - self.dirty = true; - } - - /// Evict image to make space. This will evict the oldest image, - /// prioritizing unused images first, as recommended by the published - /// Kitty spec. - /// - /// This will evict as many images as necessary to make space for - /// req bytes. - fn evictImage(self: *ImageStorage, alloc: Allocator, req: usize) !bool { - assert(req <= self.total_limit); - - // Ironically we allocate to evict. We should probably redesign the - // data structures to avoid this but for now allocating a little - // bit is fine compared to the megabytes we're looking to save. - const Candidate = struct { - id: u32, - time: std.time.Instant, - used: bool, - }; - - var candidates = std.ArrayList(Candidate).init(alloc); - defer candidates.deinit(); - - var it = self.images.iterator(); - while (it.next()) |kv| { - const img = kv.value_ptr; - - // This is a huge waste. See comment above about redesigning - // our data structures to avoid this. Eviction should be very - // rare though and we never have that many images/placements - // so hopefully this will last a long time. - const used = used: { - var p_it = self.placements.iterator(); - while (p_it.next()) |p_kv| { - if (p_kv.key_ptr.image_id == img.id) { - break :used true; - } - } - - break :used false; - }; - - try candidates.append(.{ - .id = img.id, - .time = img.transmit_time, - .used = used, - }); - } - - // Sort - std.mem.sortUnstable( - Candidate, - candidates.items, - {}, - struct { - fn lessThan( - ctx: void, - lhs: Candidate, - rhs: Candidate, - ) bool { - _ = ctx; - - // If they're usage matches, then its based on time. - if (lhs.used == rhs.used) return switch (lhs.time.order(rhs.time)) { - .lt => true, - .gt => false, - .eq => lhs.id < rhs.id, - }; - - // If not used, then its a better candidate - return !lhs.used; - } - }.lessThan, - ); - - // They're in order of best to evict. - var evicted: usize = 0; - for (candidates.items) |c| { - // Delete all the placements for this image and the image. - var p_it = self.placements.iterator(); - while (p_it.next()) |entry| { - if (entry.key_ptr.image_id == c.id) { - self.placements.removeByPtr(entry.key_ptr); - } - } - - if (self.images.getEntry(c.id)) |entry| { - log.info("evicting image id={} bytes={}", .{ c.id, entry.value_ptr.data.len }); - - evicted += entry.value_ptr.data.len; - self.total_bytes -= entry.value_ptr.data.len; - - entry.value_ptr.deinit(alloc); - self.images.removeByPtr(entry.key_ptr); - - if (evicted > req) return true; - } - } - - return false; - } - - /// Every placement is uniquely identified by the image ID and the - /// placement ID. If an image ID isn't specified it is assumed to be 0. - /// Likewise, if a placement ID isn't specified it is assumed to be 0. - pub const PlacementKey = struct { - image_id: u32, - placement_id: packed struct { - tag: enum(u1) { internal, external }, - id: u32, - }, - }; - - pub const Placement = struct { - /// The location of the image on the screen. - point: ScreenPoint, - - /// Offset of the x/y from the top-left of the cell. - x_offset: u32 = 0, - y_offset: u32 = 0, - - /// Source rectangle for the image to pull from - source_x: u32 = 0, - source_y: u32 = 0, - source_width: u32 = 0, - source_height: u32 = 0, - - /// The columns/rows this image occupies. - columns: u32 = 0, - rows: u32 = 0, - - /// The z-index for this placement. - z: i32 = 0, - - /// Returns a selection of the entire rectangle this placement - /// occupies within the screen. - pub fn rect( - self: Placement, - image: Image, - t: *const terminal.Terminal, - ) Rect { - // If we have columns/rows specified we can simplify this whole thing. - if (self.columns > 0 and self.rows > 0) { - return .{ - .top_left = self.point, - .bottom_right = .{ - .x = @min(self.point.x + self.columns, t.cols - 1), - .y = self.point.y + self.rows, - }, - }; - } - - // Calculate our cell size. - const terminal_width_f64: f64 = @floatFromInt(t.width_px); - const terminal_height_f64: f64 = @floatFromInt(t.height_px); - const grid_columns_f64: f64 = @floatFromInt(t.cols); - const grid_rows_f64: f64 = @floatFromInt(t.rows); - const cell_width_f64 = terminal_width_f64 / grid_columns_f64; - const cell_height_f64 = terminal_height_f64 / grid_rows_f64; - - // Our image width - const width_px = if (self.source_width > 0) self.source_width else image.width; - const height_px = if (self.source_height > 0) self.source_height else image.height; - - // Calculate our image size in grid cells - const width_f64: f64 = @floatFromInt(width_px); - const height_f64: f64 = @floatFromInt(height_px); - const width_cells: u32 = @intFromFloat(@ceil(width_f64 / cell_width_f64)); - const height_cells: u32 = @intFromFloat(@ceil(height_f64 / cell_height_f64)); - - return .{ - .top_left = self.point, - .bottom_right = .{ - .x = @min(self.point.x + width_cells, t.cols - 1), - .y = self.point.y + height_cells, - }, - }; - } - }; -}; - -test "storage: add placement with zero placement id" { - const testing = std.testing; - const alloc = testing.allocator; - var t = try terminal.Terminal.init(alloc, 100, 100); - defer t.deinit(alloc); - t.width_px = 100; - t.height_px = 100; - - var s: ImageStorage = .{}; - defer s.deinit(alloc); - try s.addImage(alloc, .{ .id = 1, .width = 50, .height = 50 }); - try s.addImage(alloc, .{ .id = 2, .width = 25, .height = 25 }); - try s.addPlacement(alloc, 1, 0, .{ .point = .{ .x = 25, .y = 25 } }); - try s.addPlacement(alloc, 1, 0, .{ .point = .{ .x = 25, .y = 25 } }); - - try testing.expectEqual(@as(usize, 2), s.placements.count()); - try testing.expectEqual(@as(usize, 2), s.images.count()); - - // verify the placement is what we expect - try testing.expect(s.placements.get(.{ - .image_id = 1, - .placement_id = .{ .tag = .internal, .id = 0 }, - }) != null); - try testing.expect(s.placements.get(.{ - .image_id = 1, - .placement_id = .{ .tag = .internal, .id = 1 }, - }) != null); -} - -test "storage: delete all placements and images" { - const testing = std.testing; - const alloc = testing.allocator; - var t = try terminal.Terminal.init(alloc, 3, 3); - defer t.deinit(alloc); - - var s: ImageStorage = .{}; - defer s.deinit(alloc); - try s.addImage(alloc, .{ .id = 1 }); - try s.addImage(alloc, .{ .id = 2 }); - try s.addImage(alloc, .{ .id = 3 }); - try s.addPlacement(alloc, 1, 1, .{ .point = .{ .x = 1, .y = 1 } }); - try s.addPlacement(alloc, 2, 1, .{ .point = .{ .x = 1, .y = 1 } }); - - s.dirty = false; - s.delete(alloc, &t, .{ .all = true }); - try testing.expect(s.dirty); - try testing.expectEqual(@as(usize, 0), s.images.count()); - try testing.expectEqual(@as(usize, 0), s.placements.count()); -} - -test "storage: delete all placements and images preserves limit" { - const testing = std.testing; - const alloc = testing.allocator; - var t = try terminal.Terminal.init(alloc, 3, 3); - defer t.deinit(alloc); - - var s: ImageStorage = .{}; - defer s.deinit(alloc); - s.total_limit = 5000; - try s.addImage(alloc, .{ .id = 1 }); - try s.addImage(alloc, .{ .id = 2 }); - try s.addImage(alloc, .{ .id = 3 }); - try s.addPlacement(alloc, 1, 1, .{ .point = .{ .x = 1, .y = 1 } }); - try s.addPlacement(alloc, 2, 1, .{ .point = .{ .x = 1, .y = 1 } }); - - s.dirty = false; - s.delete(alloc, &t, .{ .all = true }); - try testing.expect(s.dirty); - try testing.expectEqual(@as(usize, 0), s.images.count()); - try testing.expectEqual(@as(usize, 0), s.placements.count()); - try testing.expectEqual(@as(usize, 5000), s.total_limit); -} - -test "storage: delete all placements" { - const testing = std.testing; - const alloc = testing.allocator; - var t = try terminal.Terminal.init(alloc, 3, 3); - defer t.deinit(alloc); - - var s: ImageStorage = .{}; - defer s.deinit(alloc); - try s.addImage(alloc, .{ .id = 1 }); - try s.addImage(alloc, .{ .id = 2 }); - try s.addImage(alloc, .{ .id = 3 }); - try s.addPlacement(alloc, 1, 1, .{ .point = .{ .x = 1, .y = 1 } }); - try s.addPlacement(alloc, 2, 1, .{ .point = .{ .x = 1, .y = 1 } }); - - s.dirty = false; - s.delete(alloc, &t, .{ .all = false }); - try testing.expect(s.dirty); - try testing.expectEqual(@as(usize, 0), s.placements.count()); - try testing.expectEqual(@as(usize, 3), s.images.count()); -} - -test "storage: delete all placements by image id" { - const testing = std.testing; - const alloc = testing.allocator; - var t = try terminal.Terminal.init(alloc, 3, 3); - defer t.deinit(alloc); - - var s: ImageStorage = .{}; - defer s.deinit(alloc); - try s.addImage(alloc, .{ .id = 1 }); - try s.addImage(alloc, .{ .id = 2 }); - try s.addImage(alloc, .{ .id = 3 }); - try s.addPlacement(alloc, 1, 1, .{ .point = .{ .x = 1, .y = 1 } }); - try s.addPlacement(alloc, 2, 1, .{ .point = .{ .x = 1, .y = 1 } }); - - s.dirty = false; - s.delete(alloc, &t, .{ .id = .{ .image_id = 2 } }); - try testing.expect(s.dirty); - try testing.expectEqual(@as(usize, 1), s.placements.count()); - try testing.expectEqual(@as(usize, 3), s.images.count()); -} - -test "storage: delete all placements by image id and unused images" { - const testing = std.testing; - const alloc = testing.allocator; - var t = try terminal.Terminal.init(alloc, 3, 3); - defer t.deinit(alloc); - - var s: ImageStorage = .{}; - defer s.deinit(alloc); - try s.addImage(alloc, .{ .id = 1 }); - try s.addImage(alloc, .{ .id = 2 }); - try s.addImage(alloc, .{ .id = 3 }); - try s.addPlacement(alloc, 1, 1, .{ .point = .{ .x = 1, .y = 1 } }); - try s.addPlacement(alloc, 2, 1, .{ .point = .{ .x = 1, .y = 1 } }); - - s.dirty = false; - s.delete(alloc, &t, .{ .id = .{ .delete = true, .image_id = 2 } }); - try testing.expect(s.dirty); - try testing.expectEqual(@as(usize, 1), s.placements.count()); - try testing.expectEqual(@as(usize, 2), s.images.count()); -} - -test "storage: delete placement by specific id" { - const testing = std.testing; - const alloc = testing.allocator; - var t = try terminal.Terminal.init(alloc, 3, 3); - defer t.deinit(alloc); - - var s: ImageStorage = .{}; - defer s.deinit(alloc); - try s.addImage(alloc, .{ .id = 1 }); - try s.addImage(alloc, .{ .id = 2 }); - try s.addImage(alloc, .{ .id = 3 }); - try s.addPlacement(alloc, 1, 1, .{ .point = .{ .x = 1, .y = 1 } }); - try s.addPlacement(alloc, 1, 2, .{ .point = .{ .x = 1, .y = 1 } }); - try s.addPlacement(alloc, 2, 1, .{ .point = .{ .x = 1, .y = 1 } }); - - s.dirty = false; - s.delete(alloc, &t, .{ .id = .{ - .delete = true, - .image_id = 1, - .placement_id = 2, - } }); - try testing.expect(s.dirty); - try testing.expectEqual(@as(usize, 2), s.placements.count()); - try testing.expectEqual(@as(usize, 3), s.images.count()); -} - -test "storage: delete intersecting cursor" { - const testing = std.testing; - const alloc = testing.allocator; - var t = try terminal.Terminal.init(alloc, 100, 100); - defer t.deinit(alloc); - t.width_px = 100; - t.height_px = 100; - - var s: ImageStorage = .{}; - defer s.deinit(alloc); - try s.addImage(alloc, .{ .id = 1, .width = 50, .height = 50 }); - try s.addImage(alloc, .{ .id = 2, .width = 25, .height = 25 }); - try s.addPlacement(alloc, 1, 1, .{ .point = .{ .x = 0, .y = 0 } }); - try s.addPlacement(alloc, 1, 2, .{ .point = .{ .x = 25, .y = 25 } }); - - t.screen.cursor.x = 12; - t.screen.cursor.y = 12; - - s.dirty = false; - s.delete(alloc, &t, .{ .intersect_cursor = false }); - try testing.expect(s.dirty); - try testing.expectEqual(@as(usize, 1), s.placements.count()); - try testing.expectEqual(@as(usize, 2), s.images.count()); - - // verify the placement is what we expect - try testing.expect(s.placements.get(.{ - .image_id = 1, - .placement_id = .{ .tag = .external, .id = 2 }, - }) != null); -} - -test "storage: delete intersecting cursor plus unused" { - const testing = std.testing; - const alloc = testing.allocator; - var t = try terminal.Terminal.init(alloc, 100, 100); - defer t.deinit(alloc); - t.width_px = 100; - t.height_px = 100; - - var s: ImageStorage = .{}; - defer s.deinit(alloc); - try s.addImage(alloc, .{ .id = 1, .width = 50, .height = 50 }); - try s.addImage(alloc, .{ .id = 2, .width = 25, .height = 25 }); - try s.addPlacement(alloc, 1, 1, .{ .point = .{ .x = 0, .y = 0 } }); - try s.addPlacement(alloc, 1, 2, .{ .point = .{ .x = 25, .y = 25 } }); - - t.screen.cursor.x = 12; - t.screen.cursor.y = 12; - - s.dirty = false; - s.delete(alloc, &t, .{ .intersect_cursor = true }); - try testing.expect(s.dirty); - try testing.expectEqual(@as(usize, 1), s.placements.count()); - try testing.expectEqual(@as(usize, 2), s.images.count()); - - // verify the placement is what we expect - try testing.expect(s.placements.get(.{ - .image_id = 1, - .placement_id = .{ .tag = .external, .id = 2 }, - }) != null); -} - -test "storage: delete intersecting cursor hits multiple" { - const testing = std.testing; - const alloc = testing.allocator; - var t = try terminal.Terminal.init(alloc, 100, 100); - defer t.deinit(alloc); - t.width_px = 100; - t.height_px = 100; - - var s: ImageStorage = .{}; - defer s.deinit(alloc); - try s.addImage(alloc, .{ .id = 1, .width = 50, .height = 50 }); - try s.addImage(alloc, .{ .id = 2, .width = 25, .height = 25 }); - try s.addPlacement(alloc, 1, 1, .{ .point = .{ .x = 0, .y = 0 } }); - try s.addPlacement(alloc, 1, 2, .{ .point = .{ .x = 25, .y = 25 } }); - - t.screen.cursor.x = 26; - t.screen.cursor.y = 26; - - s.dirty = false; - s.delete(alloc, &t, .{ .intersect_cursor = true }); - try testing.expect(s.dirty); - try testing.expectEqual(@as(usize, 0), s.placements.count()); - try testing.expectEqual(@as(usize, 1), s.images.count()); -} - -test "storage: delete by column" { - const testing = std.testing; - const alloc = testing.allocator; - var t = try terminal.Terminal.init(alloc, 100, 100); - defer t.deinit(alloc); - t.width_px = 100; - t.height_px = 100; - - var s: ImageStorage = .{}; - defer s.deinit(alloc); - try s.addImage(alloc, .{ .id = 1, .width = 50, .height = 50 }); - try s.addImage(alloc, .{ .id = 2, .width = 25, .height = 25 }); - try s.addPlacement(alloc, 1, 1, .{ .point = .{ .x = 0, .y = 0 } }); - try s.addPlacement(alloc, 1, 2, .{ .point = .{ .x = 25, .y = 25 } }); - - s.dirty = false; - s.delete(alloc, &t, .{ .column = .{ - .delete = false, - .x = 60, - } }); - try testing.expect(s.dirty); - try testing.expectEqual(@as(usize, 1), s.placements.count()); - try testing.expectEqual(@as(usize, 2), s.images.count()); - - // verify the placement is what we expect - try testing.expect(s.placements.get(.{ - .image_id = 1, - .placement_id = .{ .tag = .external, .id = 1 }, - }) != null); -} - -test "storage: delete by row" { - const testing = std.testing; - const alloc = testing.allocator; - var t = try terminal.Terminal.init(alloc, 100, 100); - defer t.deinit(alloc); - t.width_px = 100; - t.height_px = 100; - - var s: ImageStorage = .{}; - defer s.deinit(alloc); - try s.addImage(alloc, .{ .id = 1, .width = 50, .height = 50 }); - try s.addImage(alloc, .{ .id = 2, .width = 25, .height = 25 }); - try s.addPlacement(alloc, 1, 1, .{ .point = .{ .x = 0, .y = 0 } }); - try s.addPlacement(alloc, 1, 2, .{ .point = .{ .x = 25, .y = 25 } }); - - s.dirty = false; - s.delete(alloc, &t, .{ .row = .{ - .delete = false, - .y = 60, - } }); - try testing.expect(s.dirty); - try testing.expectEqual(@as(usize, 1), s.placements.count()); - try testing.expectEqual(@as(usize, 2), s.images.count()); - - // verify the placement is what we expect - try testing.expect(s.placements.get(.{ - .image_id = 1, - .placement_id = .{ .tag = .external, .id = 1 }, - }) != null); -} diff --git a/src/terminal-old/kitty/key.zig b/src/terminal-old/kitty/key.zig deleted file mode 100644 index 938bf65b5..000000000 --- a/src/terminal-old/kitty/key.zig +++ /dev/null @@ -1,151 +0,0 @@ -//! Kitty keyboard protocol support. - -const std = @import("std"); - -/// Stack for the key flags. This implements the push/pop behavior -/// of the CSI > u and CSI < u sequences. We implement the stack as -/// fixed size to avoid heap allocation. -pub const KeyFlagStack = struct { - const len = 8; - - flags: [len]KeyFlags = .{.{}} ** len, - idx: u3 = 0, - - /// Return the current stack value - pub fn current(self: KeyFlagStack) KeyFlags { - return self.flags[self.idx]; - } - - /// Perform the "set" operation as described in the spec for - /// the CSI = u sequence. - pub fn set( - self: *KeyFlagStack, - mode: KeySetMode, - v: KeyFlags, - ) void { - switch (mode) { - .set => self.flags[self.idx] = v, - .@"or" => self.flags[self.idx] = @bitCast( - self.flags[self.idx].int() | v.int(), - ), - .not => self.flags[self.idx] = @bitCast( - self.flags[self.idx].int() & ~v.int(), - ), - } - } - - /// Push a new set of flags onto the stack. If the stack is full - /// then the oldest entry is evicted. - pub fn push(self: *KeyFlagStack, flags: KeyFlags) void { - // Overflow and wrap around if we're full, which evicts - // the oldest entry. - self.idx +%= 1; - self.flags[self.idx] = flags; - } - - /// Pop `n` entries from the stack. This will just wrap around - /// if `n` is greater than the amount in the stack. - pub fn pop(self: *KeyFlagStack, n: usize) void { - // If n is more than our length then we just reset the stack. - // This also avoids a DoS vector where a malicious client - // could send a huge number of pop commands to waste cpu. - if (n >= self.flags.len) { - self.idx = 0; - self.flags = .{.{}} ** len; - return; - } - - for (0..n) |_| { - self.flags[self.idx] = .{}; - self.idx -%= 1; - } - } - - // Make sure we the overflow works as expected - test { - const testing = std.testing; - var stack: KeyFlagStack = .{}; - stack.idx = stack.flags.len - 1; - stack.idx +%= 1; - try testing.expect(stack.idx == 0); - - stack.idx = 0; - stack.idx -%= 1; - try testing.expect(stack.idx == stack.flags.len - 1); - } -}; - -/// The possible flags for the Kitty keyboard protocol. -pub const KeyFlags = packed struct(u5) { - disambiguate: bool = false, - report_events: bool = false, - report_alternates: bool = false, - report_all: bool = false, - report_associated: bool = false, - - pub fn int(self: KeyFlags) u5 { - return @bitCast(self); - } - - // Its easy to get packed struct ordering wrong so this test checks. - test { - const testing = std.testing; - - try testing.expectEqual( - @as(u5, 0b1), - (KeyFlags{ .disambiguate = true }).int(), - ); - try testing.expectEqual( - @as(u5, 0b10), - (KeyFlags{ .report_events = true }).int(), - ); - } -}; - -/// The possible modes for setting the key flags. -pub const KeySetMode = enum { set, @"or", not }; - -test "KeyFlagStack: push pop" { - const testing = std.testing; - var stack: KeyFlagStack = .{}; - stack.push(.{ .disambiguate = true }); - try testing.expectEqual( - KeyFlags{ .disambiguate = true }, - stack.current(), - ); - - stack.pop(1); - try testing.expectEqual(KeyFlags{}, stack.current()); -} - -test "KeyFlagStack: pop big number" { - const testing = std.testing; - var stack: KeyFlagStack = .{}; - stack.pop(100); - try testing.expectEqual(KeyFlags{}, stack.current()); -} - -test "KeyFlagStack: set" { - const testing = std.testing; - var stack: KeyFlagStack = .{}; - stack.set(.set, .{ .disambiguate = true }); - try testing.expectEqual( - KeyFlags{ .disambiguate = true }, - stack.current(), - ); - - stack.set(.@"or", .{ .report_events = true }); - try testing.expectEqual( - KeyFlags{ - .disambiguate = true, - .report_events = true, - }, - stack.current(), - ); - - stack.set(.not, .{ .report_events = true }); - try testing.expectEqual( - KeyFlags{ .disambiguate = true }, - stack.current(), - ); -} diff --git a/src/terminal-old/kitty/testdata/image-png-none-50x76-2147483647-raw.data b/src/terminal-old/kitty/testdata/image-png-none-50x76-2147483647-raw.data deleted file mode 100644 index 032cb07c7..000000000 Binary files a/src/terminal-old/kitty/testdata/image-png-none-50x76-2147483647-raw.data and /dev/null differ diff --git a/src/terminal-old/kitty/testdata/image-rgb-none-20x15-2147483647.data b/src/terminal-old/kitty/testdata/image-rgb-none-20x15-2147483647.data deleted file mode 100644 index f65d40ce8..000000000 --- a/src/terminal-old/kitty/testdata/image-rgb-none-20x15-2147483647.data +++ /dev/null @@ -1 +0,0 @@ -DRoeCxgcCxcjEh4qDBgkCxcjChYiCxcjCRclBRMhBxIXHysvTVNRbHJwcXB2Li0zCBYXEyEiCxkaDBobChcbCBUZDxsnBBAcEBwoChYiCxcjDBgkDhwqBxUjDBccm6aqy9HP1NrYzs3UsK+2IjAxCBYXCBYXBxUWFBoaDxUVICYqIyktERcZDxUXDxUVEhgYDhUTCxIQGh8XusC4zM7FvL61q6elmZWTTVtcDBobDRscCxkaKS8vaW9vxMnOur/EiY+RaW5wICYmW2FhfYOBQEZEnqSc4ebeqauilZaOsa2rm5eVcH5/GigpChgZCBYX0NHP3d7c3tzbx8XExsTEvry8wL241dLN0tDF0tDF29nM4d/StbKpzMrAUk5DZmJXeYSGKTU3ER0fDRkb1tfVysvJ0tDPsa+tr6ytop+gmZaRqaahuritw8G2urirqKaZiYZ9paKZZmJXamZbOkZIDhocBxMVBBASxMDBtrKzqqanoZ2ejYeLeHF2eXFvhn58npePta6ml5CKgXp0W1hPaWZdZWdSYmRPFiYADR0AFCQAEyMAt7O0lJCRf3t8eHR1Zl9kY1xhYVpYbGRieXJqeHFpdW1oc2tmcG1kX1xTbW9ajY96jp55kaF8kKB7kaF8sK6rcnFtX11cXFpZW1pWWFdTXVpTXltUaGJgY11bY11da2Vla25dam1ccHtTnqmBorVtp7pypLdvobRsh4aCaGdjWFZVXFpZYWBcZ2ZiaGVeZGFaY11bYlxaV1FRZ2FhdHdmbG9egItjo66GpLdvq752rL93rsF5kpKIZ2ddWFxTW19WbnZdipJ6cnhaaW9RaGhgV1ZPY2Jga2poanFQd35dk6Vpn7B0oLFvorNxm6xqmKlnv760enpwVlpRW19Wc3til5+Hl55/k5p7iIiAcnJqd3Z0bm1rcHdWh45tipxgladrkaJglKVjkaJgkqNh09DJiYZ/YmZdY2deeYZYjJlrj51ijpxhztHClJaIdHNvdHNvanNHi5RpmaxnjKBbmqhrmadqkJ5hi5lcxsO8jImCaGtiYmZdg5Bikp9xjJpfjpxh1djJqq2eamllZ2Zid4BVmKF2kqZhh5tWlaNmlaNmjpxfjJpdw729rqiodnZ0cHBuiplij55nj6FVjJ5SzdC9t7qncW1sXlpZh45iqbCEmKllmapmmqlqnq1unaxtoK9w \ No newline at end of file diff --git a/src/terminal-old/kitty/testdata/image-rgb-zlib_deflate-128x96-2147483647.data b/src/terminal-old/kitty/testdata/image-rgb-zlib_deflate-128x96-2147483647.data deleted file mode 100644 index c1d6d87b3..000000000 --- a/src/terminal-old/kitty/testdata/image-rgb-zlib_deflate-128x96-2147483647.data +++ /dev/null @@ -1 +0,0 @@ -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 \ No newline at end of file diff --git a/src/terminal-old/main.zig b/src/terminal-old/main.zig deleted file mode 100644 index 0e6856a10..000000000 --- a/src/terminal-old/main.zig +++ /dev/null @@ -1,54 +0,0 @@ -const builtin = @import("builtin"); - -pub usingnamespace @import("sanitize.zig"); - -const charsets = @import("charsets.zig"); -const stream = @import("stream.zig"); -const ansi = @import("ansi.zig"); -const csi = @import("csi.zig"); -const sgr = @import("sgr.zig"); -pub const apc = @import("apc.zig"); -pub const dcs = @import("dcs.zig"); -pub const osc = @import("osc.zig"); -pub const point = @import("point.zig"); -pub const color = @import("color.zig"); -pub const device_status = @import("device_status.zig"); -pub const kitty = @import("kitty.zig"); -pub const modes = @import("modes.zig"); -pub const parse_table = @import("parse_table.zig"); -pub const x11_color = @import("x11_color.zig"); - -pub const Charset = charsets.Charset; -pub const CharsetSlot = charsets.Slots; -pub const CharsetActiveSlot = charsets.ActiveSlot; -pub const CSI = Parser.Action.CSI; -pub const DCS = Parser.Action.DCS; -pub const MouseShape = @import("mouse_shape.zig").MouseShape; -pub const Parser = @import("Parser.zig"); -pub const Selection = @import("Selection.zig"); -pub const Screen = @import("Screen.zig"); -pub const Terminal = @import("Terminal.zig"); -pub const Stream = stream.Stream; -pub const Cursor = Screen.Cursor; -pub const CursorStyleReq = ansi.CursorStyle; -pub const DeviceAttributeReq = ansi.DeviceAttributeReq; -pub const Mode = modes.Mode; -pub const ModifyKeyFormat = ansi.ModifyKeyFormat; -pub const ProtectedMode = ansi.ProtectedMode; -pub const StatusLineType = ansi.StatusLineType; -pub const StatusDisplay = ansi.StatusDisplay; -pub const EraseDisplay = csi.EraseDisplay; -pub const EraseLine = csi.EraseLine; -pub const TabClear = csi.TabClear; -pub const Attribute = sgr.Attribute; - -pub const StringMap = @import("StringMap.zig"); - -/// If we're targeting wasm then we export some wasm APIs. -pub usingnamespace if (builtin.target.isWasm()) struct { - pub usingnamespace @import("wasm.zig"); -} else struct {}; - -test { - @import("std").testing.refAllDecls(@This()); -} diff --git a/src/terminal-old/modes.zig b/src/terminal-old/modes.zig deleted file mode 100644 index c9ed84cbd..000000000 --- a/src/terminal-old/modes.zig +++ /dev/null @@ -1,247 +0,0 @@ -//! 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(8, @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 = @as(ModeTag.Backing, @bitCast(ModeTag{ - .value = entry.value, - .ansi = entry.ansi, - })), - }; - } - - break :mode_enum @Type(.{ .Enum = .{ - .tag_type = ModeTag.Backing, - .fields = &fields, - .decls = &.{}, - .is_exhaustive = true, - } }); -}; - -/// The tag type for our enum is a u16 but we use a packed struct -/// in order to pack the ansi bit into the tag. -pub const ModeTag = packed struct(u16) { - pub const Backing = @typeInfo(@This()).Struct.backing_integer.?; - value: u15, - ansi: bool = false, - - test "order" { - const t: ModeTag = .{ .value = 1 }; - const int: Backing = @bitCast(t); - try std.testing.expectEqual(@as(Backing, 1), int); - } -}; - -pub fn modeFromInt(v: u16, ansi: bool) ?Mode { - inline for (entries) |entry| { - if (comptime !entry.disabled) { - if (entry.value == v and entry.ansi == ansi) { - const tag: ModeTag = .{ .ansi = ansi, .value = entry.value }; - const int: ModeTag.Backing = @bitCast(tag); - return @enumFromInt(int); - } - } - } - - return null; -} - -fn entryForMode(comptime mode: Mode) ModeEntry { - @setEvalBranchQuota(10_000); - 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: [:0]const u8, - value: comptime_int, - default: bool = false, - - /// True if this is an ANSI mode, false if its a DEC mode (?-prefixed). - ansi: bool = false, - - /// If true, this mode is disabled and Ghostty will not allow it to be - /// set or queried. The mode enum still has it, allowing Ghostty developers - /// to develop a mode without exposing it to real users. - disabled: 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 = &.{ - // ANSI - .{ .name = "disable_keyboard", .value = 2, .ansi = true }, // KAM - .{ .name = "insert", .value = 4, .ansi = true }, - .{ .name = "send_receive_mode", .value = 12, .ansi = true, .default = true }, // SRM - .{ .name = "linefeed", .value = 20, .ansi = true }, - - // DEC - .{ .name = "cursor_keys", .value = 1 }, // DECCKM - .{ .name = "132_column", .value = 3 }, - .{ .name = "slow_scroll", .value = 4 }, - .{ .name = "reverse_colors", .value = 5 }, - .{ .name = "origin", .value = 6 }, - .{ .name = "wraparound", .value = 7, .default = true }, - .{ .name = "autorepeat", .value = 8 }, - .{ .name = "mouse_event_x10", .value = 9 }, - .{ .name = "cursor_blinking", .value = 12 }, - .{ .name = "cursor_visible", .value = 25, .default = true }, - .{ .name = "enable_mode_3", .value = 40 }, - .{ .name = "reverse_wrap", .value = 45 }, - .{ .name = "keypad_keys", .value = 66 }, - .{ .name = "enable_left_and_right_margin", .value = 69 }, - .{ .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 = "ignore_keypad_with_numlock", .value = 1035, .default = true }, - .{ .name = "alt_esc_prefix", .value = 1036, .default = true }, - .{ .name = "alt_sends_escape", .value = 1039 }, - .{ .name = "reverse_wrap_extended", .value = 1045 }, - .{ .name = "alt_screen", .value = 1047 }, - .{ .name = "alt_screen_save_cursor_clear_enter", .value = 1049 }, - .{ .name = "bracketed_paste", .value = 2004 }, - .{ .name = "synchronized_output", .value = 2026 }, - .{ .name = "grapheme_cluster", .value = 2027 }, - .{ .name = "report_color_scheme", .value = 2031 }, -}; - -test { - _ = Mode; - _ = ModePacked; -} - -test modeFromInt { - try testing.expect(modeFromInt(4, true).? == .insert); - try testing.expect(modeFromInt(9, true) == null); - try testing.expect(modeFromInt(9, false).? == .mouse_event_x10); - try testing.expect(modeFromInt(14, true) == null); -} - -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)); -} diff --git a/src/terminal-old/mouse_shape.zig b/src/terminal-old/mouse_shape.zig deleted file mode 100644 index cf8f42c4b..000000000 --- a/src/terminal-old/mouse_shape.zig +++ /dev/null @@ -1,115 +0,0 @@ -const std = @import("std"); - -/// The possible cursor shapes. Not all app runtimes support these shapes. -/// The shapes are always based on the W3C supported cursor styles so we -/// can have a cross platform list. -// -// Must be kept in sync with ghostty_cursor_shape_e -pub const MouseShape = enum(c_int) { - default, - context_menu, - help, - pointer, - progress, - wait, - cell, - crosshair, - text, - vertical_text, - alias, - copy, - move, - no_drop, - not_allowed, - grab, - grabbing, - all_scroll, - col_resize, - row_resize, - n_resize, - e_resize, - s_resize, - w_resize, - ne_resize, - nw_resize, - se_resize, - sw_resize, - ew_resize, - ns_resize, - nesw_resize, - nwse_resize, - zoom_in, - zoom_out, - - /// Build cursor shape from string or null if its unknown. - pub fn fromString(v: []const u8) ?MouseShape { - return string_map.get(v); - } -}; - -const string_map = std.ComptimeStringMap(MouseShape, .{ - // W3C - .{ "default", .default }, - .{ "context-menu", .context_menu }, - .{ "help", .help }, - .{ "pointer", .pointer }, - .{ "progress", .progress }, - .{ "wait", .wait }, - .{ "cell", .cell }, - .{ "crosshair", .crosshair }, - .{ "text", .text }, - .{ "vertical-text", .vertical_text }, - .{ "alias", .alias }, - .{ "copy", .copy }, - .{ "move", .move }, - .{ "no-drop", .no_drop }, - .{ "not-allowed", .not_allowed }, - .{ "grab", .grab }, - .{ "grabbing", .grabbing }, - .{ "all-scroll", .all_scroll }, - .{ "col-resize", .col_resize }, - .{ "row-resize", .row_resize }, - .{ "n-resize", .n_resize }, - .{ "e-resize", .e_resize }, - .{ "s-resize", .s_resize }, - .{ "w-resize", .w_resize }, - .{ "ne-resize", .ne_resize }, - .{ "nw-resize", .nw_resize }, - .{ "se-resize", .se_resize }, - .{ "sw-resize", .sw_resize }, - .{ "ew-resize", .ew_resize }, - .{ "ns-resize", .ns_resize }, - .{ "nesw-resize", .nesw_resize }, - .{ "nwse-resize", .nwse_resize }, - .{ "zoom-in", .zoom_in }, - .{ "zoom-out", .zoom_out }, - - // xterm/foot - .{ "left_ptr", .default }, - .{ "question_arrow", .help }, - .{ "hand", .pointer }, - .{ "left_ptr_watch", .progress }, - .{ "watch", .wait }, - .{ "cross", .crosshair }, - .{ "xterm", .text }, - .{ "dnd-link", .alias }, - .{ "dnd-copy", .copy }, - .{ "dnd-move", .move }, - .{ "dnd-no-drop", .no_drop }, - .{ "crossed_circle", .not_allowed }, - .{ "hand1", .grab }, - .{ "right_side", .e_resize }, - .{ "top_side", .n_resize }, - .{ "top_right_corner", .ne_resize }, - .{ "top_left_corner", .nw_resize }, - .{ "bottom_side", .s_resize }, - .{ "bottom_right_corner", .se_resize }, - .{ "bottom_left_corner", .sw_resize }, - .{ "left_side", .w_resize }, - .{ "fleur", .all_scroll }, -}); - -test "cursor shape from string" { - const testing = std.testing; - try testing.expectEqual(MouseShape.default, MouseShape.fromString("default").?); -} diff --git a/src/terminal-old/osc.zig b/src/terminal-old/osc.zig deleted file mode 100644 index a220ea031..000000000 --- a/src/terminal-old/osc.zig +++ /dev/null @@ -1,1274 +0,0 @@ -//! OSC (Operating System Command) related functions and types. OSC is -//! another set of control sequences for terminal programs that start with -//! "ESC ]". Unlike CSI or standard ESC sequences, they may contain strings -//! and other irregular formatting so a dedicated parser is created to handle it. -const osc = @This(); - -const std = @import("std"); -const mem = std.mem; -const assert = std.debug.assert; -const Allocator = mem.Allocator; - -const log = std.log.scoped(.osc); - -pub const Command = union(enum) { - /// Set the window title of the terminal - /// - /// If title mode 0 is set text is expect to be hex encoded (i.e. utf-8 - /// with each code unit further encoded with two hex digets). - /// - /// If title mode 2 is set or the terminal is setup for unconditional - /// utf-8 titles text is interpreted as utf-8. Else text is interpreted - /// as latin1. - change_window_title: []const u8, - - /// Set the icon of the terminal window. The name of the icon is not - /// well defined, so this is currently ignored by Ghostty at the time - /// of writing this. We just parse it so that we don't get parse errors - /// in the log. - change_window_icon: []const u8, - - /// First do a fresh-line. Then start a new command, and enter prompt mode: - /// Subsequent text (until a OSC "133;B" or OSC "133;I" command) is a - /// prompt string (as if followed by OSC 133;P;k=i\007). Note: I've noticed - /// not all shells will send the prompt end code. - prompt_start: struct { - aid: ?[]const u8 = null, - kind: enum { primary, right, continuation } = .primary, - redraw: bool = true, - }, - - /// End of prompt and start of user input, terminated by a OSC "133;C" - /// or another prompt (OSC "133;P"). - prompt_end: void, - - /// The OSC "133;C" command can be used to explicitly end - /// the input area and begin the output area. However, some applications - /// don't provide a convenient way to emit that command. - /// That is why we also specify an implicit way to end the input area - /// at the end of the line. In the case of multiple input lines: If the - /// cursor is on a fresh (empty) line and we see either OSC "133;P" or - /// OSC "133;I" then this is the start of a continuation input line. - /// If we see anything else, it is the start of the output area (or end - /// of command). - end_of_input: void, - - /// End of current command. - /// - /// The exit-code need not be specified if if there are no options, - /// or if the command was cancelled (no OSC "133;C"), such as by typing - /// an interrupt/cancel character (typically ctrl-C) during line-editing. - /// Otherwise, it must be an integer code, where 0 means the command - /// succeeded, and other values indicate failure. In additing to the - /// exit-code there may be an err= option, which non-legacy terminals - /// should give precedence to. The err=_value_ option is more general: - /// an empty string is success, and any non-empty value (which need not - /// be an integer) is an error code. So to indicate success both ways you - /// could send OSC "133;D;0;err=\007", though `OSC "133;D;0\007" is shorter. - end_of_command: struct { - exit_code: ?u8 = null, - // TODO: err option - }, - - /// Set or get clipboard contents. If data is null, then the current - /// clipboard contents are sent to the pty. If data is set, this - /// contents is set on the clipboard. - clipboard_contents: struct { - kind: u8, - data: []const u8, - }, - - /// OSC 7. Reports the current working directory of the shell. This is - /// a moderately flawed escape sequence but one that many major terminals - /// support so we also support it. To understand the flaws, read through - /// this terminal-wg issue: https://gitlab.freedesktop.org/terminal-wg/specifications/-/issues/20 - report_pwd: struct { - /// The reported pwd value. This is not checked for validity. It should - /// be a file URL but it is up to the caller to utilize this value. - value: []const u8, - }, - - /// OSC 22. Set the mouse shape. There doesn't seem to be a standard - /// naming scheme for cursors but it looks like terminals such as Foot - /// are moving towards using the W3C CSS cursor names. For OSC parsing, - /// we just parse whatever string is given. - mouse_shape: struct { - value: []const u8, - }, - - /// OSC 4, OSC 10, and OSC 11 color report. - report_color: struct { - /// OSC 4 requests a palette color, OSC 10 requests the foreground - /// color, OSC 11 the background color. - kind: ColorKind, - - /// We must reply with the same string terminator (ST) as used in the - /// request. - terminator: Terminator = .st, - }, - - /// Modify the foreground (OSC 10) or background color (OSC 11), or a palette color (OSC 4) - set_color: struct { - /// OSC 4 sets a palette color, OSC 10 sets the foreground color, OSC 11 - /// the background color. - kind: ColorKind, - - /// The color spec as a string - value: []const u8, - }, - - /// Reset a palette color (OSC 104) or the foreground (OSC 110), background - /// (OSC 111), or cursor (OSC 112) color. - reset_color: struct { - kind: ColorKind, - - /// OSC 104 can have parameters indicating which palette colors to - /// reset. - value: []const u8, - }, - - /// Show a desktop notification (OSC 9 or OSC 777) - show_desktop_notification: struct { - title: []const u8, - body: []const u8, - }, - - pub const ColorKind = union(enum) { - palette: u8, - foreground, - background, - cursor, - - pub fn code(self: ColorKind) []const u8 { - return switch (self) { - .palette => "4", - .foreground => "10", - .background => "11", - .cursor => "12", - }; - } - }; -}; - -/// The terminator used to end an OSC command. For OSC commands that demand -/// a response, we try to match the terminator used in the request since that -/// is most likely to be accepted by the calling program. -pub const Terminator = enum { - /// The preferred string terminator is ESC followed by \ - st, - - /// Some applications and terminals use BELL (0x07) as the string terminator. - bel, - - /// Initialize the terminator based on the last byte seen. If the - /// last byte is a BEL then we use BEL, otherwise we just assume ST. - pub fn init(ch: ?u8) Terminator { - return switch (ch orelse return .st) { - 0x07 => .bel, - else => .st, - }; - } - - /// The terminator as a string. This is static memory so it doesn't - /// need to be freed. - pub fn string(self: Terminator) []const u8 { - return switch (self) { - .st => "\x1b\\", - .bel => "\x07", - }; - } -}; - -pub const Parser = struct { - /// Optional allocator used to accept data longer than MAX_BUF. - /// This only applies to some commands (e.g. OSC 52) that can - /// reasonably exceed MAX_BUF. - alloc: ?Allocator = null, - - /// Current state of the parser. - state: State = .empty, - - /// Current command of the parser, this accumulates. - command: Command = undefined, - - /// Buffer that stores the input we see for a single OSC command. - /// Slices in Command are offsets into this buffer. - buf: [MAX_BUF]u8 = undefined, - buf_start: usize = 0, - buf_idx: usize = 0, - buf_dynamic: ?*std.ArrayListUnmanaged(u8) = null, - - /// True when a command is complete/valid to return. - complete: bool = false, - - /// Temporary state that is dependent on the current state. - temp_state: union { - /// Current string parameter being populated - str: *[]const u8, - - /// Current numeric parameter being populated - num: u16, - - /// Temporary state for key/value pairs - key: []const u8, - } = undefined, - - // Maximum length of a single OSC command. This is the full OSC command - // sequence length (excluding ESC ]). This is arbitrary, I couldn't find - // any definitive resource on how long this should be. - const MAX_BUF = 2048; - - pub const State = enum { - empty, - invalid, - - // Command prefixes. We could just accumulate and compare (mem.eql) - // but the state space is small enough that we just build it up this way. - @"0", - @"1", - @"10", - @"11", - @"12", - @"13", - @"133", - @"2", - @"22", - @"4", - @"5", - @"52", - @"7", - @"77", - @"777", - @"9", - - // OSC 10 is used to query or set the current foreground color. - query_fg_color, - - // OSC 11 is used to query or set the current background color. - query_bg_color, - - // OSC 12 is used to query or set the current cursor color. - query_cursor_color, - - // We're in a semantic prompt OSC command but we aren't sure - // what the command is yet, i.e. `133;` - semantic_prompt, - semantic_option_start, - semantic_option_key, - semantic_option_value, - semantic_exit_code_start, - semantic_exit_code, - - // Get/set clipboard states - clipboard_kind, - clipboard_kind_end, - - // Get/set color palette index - color_palette_index, - color_palette_index_end, - - // Reset color palette index - reset_color_palette_index, - - // rxvt extension. Only used for OSC 777 and only the value "notify" is - // supported - rxvt_extension, - - // Title of a desktop notification - notification_title, - - // Expect a string parameter. param_str must be set as well as - // buf_start. - string, - - // A string that can grow beyond MAX_BUF. This uses the allocator. - // If the parser has no allocator then it is treated as if the - // buffer is full. - allocable_string, - }; - - /// This must be called to clean up any allocated memory. - pub fn deinit(self: *Parser) void { - self.reset(); - } - - /// Reset the parser start. - pub fn reset(self: *Parser) void { - self.state = .empty; - self.buf_start = 0; - self.buf_idx = 0; - self.complete = false; - if (self.buf_dynamic) |ptr| { - const alloc = self.alloc.?; - ptr.deinit(alloc); - alloc.destroy(ptr); - self.buf_dynamic = null; - } - } - - /// Consume the next character c and advance the parser state. - pub fn next(self: *Parser, c: u8) void { - // If our buffer is full then we're invalid. - if (self.buf_idx >= self.buf.len) { - self.state = .invalid; - return; - } - - // We store everything in the buffer so we can do a better job - // logging if we get to an invalid command. - self.buf[self.buf_idx] = c; - self.buf_idx += 1; - - // log.warn("state = {} c = {x}", .{ self.state, c }); - - switch (self.state) { - // If we get something during the invalid state, we've - // ruined our entry. - .invalid => self.complete = false, - - .empty => switch (c) { - '0' => self.state = .@"0", - '1' => self.state = .@"1", - '2' => self.state = .@"2", - '4' => self.state = .@"4", - '5' => self.state = .@"5", - '7' => self.state = .@"7", - '9' => self.state = .@"9", - else => self.state = .invalid, - }, - - .@"0" => switch (c) { - ';' => { - self.command = .{ .change_window_title = undefined }; - - self.state = .string; - self.temp_state = .{ .str = &self.command.change_window_title }; - self.buf_start = self.buf_idx; - }, - else => self.state = .invalid, - }, - - .@"1" => switch (c) { - ';' => { - self.command = .{ .change_window_icon = undefined }; - - self.state = .string; - self.temp_state = .{ .str = &self.command.change_window_icon }; - self.buf_start = self.buf_idx; - }, - '0' => self.state = .@"10", - '1' => self.state = .@"11", - '2' => self.state = .@"12", - '3' => self.state = .@"13", - else => self.state = .invalid, - }, - - .@"10" => switch (c) { - ';' => self.state = .query_fg_color, - '4' => { - self.command = .{ .reset_color = .{ - .kind = .{ .palette = 0 }, - .value = "", - } }; - - self.state = .reset_color_palette_index; - self.complete = true; - }, - else => self.state = .invalid, - }, - - .@"11" => switch (c) { - ';' => self.state = .query_bg_color, - '0' => { - self.command = .{ .reset_color = .{ .kind = .foreground, .value = undefined } }; - self.complete = true; - self.state = .invalid; - }, - '1' => { - self.command = .{ .reset_color = .{ .kind = .background, .value = undefined } }; - self.complete = true; - self.state = .invalid; - }, - '2' => { - self.command = .{ .reset_color = .{ .kind = .cursor, .value = undefined } }; - self.complete = true; - self.state = .invalid; - }, - else => self.state = .invalid, - }, - - .@"12" => switch (c) { - ';' => self.state = .query_cursor_color, - else => self.state = .invalid, - }, - - .@"13" => switch (c) { - '3' => self.state = .@"133", - else => self.state = .invalid, - }, - - .@"133" => switch (c) { - ';' => self.state = .semantic_prompt, - else => self.state = .invalid, - }, - - .@"2" => switch (c) { - '2' => self.state = .@"22", - ';' => { - self.command = .{ .change_window_title = undefined }; - - self.state = .string; - self.temp_state = .{ .str = &self.command.change_window_title }; - self.buf_start = self.buf_idx; - }, - else => self.state = .invalid, - }, - - .@"22" => switch (c) { - ';' => { - self.command = .{ .mouse_shape = undefined }; - - self.state = .string; - self.temp_state = .{ .str = &self.command.mouse_shape.value }; - self.buf_start = self.buf_idx; - }, - else => self.state = .invalid, - }, - - .@"4" => switch (c) { - ';' => { - self.state = .color_palette_index; - self.buf_start = self.buf_idx; - }, - else => self.state = .invalid, - }, - - .color_palette_index => switch (c) { - '0'...'9' => {}, - ';' => blk: { - const str = self.buf[self.buf_start .. self.buf_idx - 1]; - if (str.len == 0) { - self.state = .invalid; - break :blk; - } - - if (std.fmt.parseUnsigned(u8, str, 10)) |num| { - self.state = .color_palette_index_end; - self.temp_state = .{ .num = num }; - } else |err| switch (err) { - error.Overflow => self.state = .invalid, - error.InvalidCharacter => unreachable, - } - }, - else => self.state = .invalid, - }, - - .color_palette_index_end => switch (c) { - '?' => { - self.command = .{ .report_color = .{ - .kind = .{ .palette = @intCast(self.temp_state.num) }, - } }; - - self.complete = true; - }, - else => { - self.command = .{ .set_color = .{ - .kind = .{ .palette = @intCast(self.temp_state.num) }, - .value = "", - } }; - - self.state = .string; - self.temp_state = .{ .str = &self.command.set_color.value }; - self.buf_start = self.buf_idx - 1; - }, - }, - - .reset_color_palette_index => switch (c) { - ';' => { - self.state = .string; - self.temp_state = .{ .str = &self.command.reset_color.value }; - self.buf_start = self.buf_idx; - self.complete = false; - }, - else => { - self.state = .invalid; - self.complete = false; - }, - }, - - .@"5" => switch (c) { - '2' => self.state = .@"52", - else => self.state = .invalid, - }, - - .@"52" => switch (c) { - ';' => { - self.command = .{ .clipboard_contents = undefined }; - self.state = .clipboard_kind; - }, - else => self.state = .invalid, - }, - - .clipboard_kind => switch (c) { - ';' => { - self.command.clipboard_contents.kind = 'c'; - self.temp_state = .{ .str = &self.command.clipboard_contents.data }; - self.buf_start = self.buf_idx; - self.prepAllocableString(); - }, - else => { - self.command.clipboard_contents.kind = c; - self.state = .clipboard_kind_end; - }, - }, - - .clipboard_kind_end => switch (c) { - ';' => { - self.temp_state = .{ .str = &self.command.clipboard_contents.data }; - self.buf_start = self.buf_idx; - self.prepAllocableString(); - }, - else => self.state = .invalid, - }, - - .@"7" => switch (c) { - ';' => { - self.command = .{ .report_pwd = .{ .value = "" } }; - - self.state = .string; - self.temp_state = .{ .str = &self.command.report_pwd.value }; - self.buf_start = self.buf_idx; - }, - '7' => self.state = .@"77", - else => self.state = .invalid, - }, - - .@"77" => switch (c) { - '7' => self.state = .@"777", - else => self.state = .invalid, - }, - - .@"777" => switch (c) { - ';' => { - self.state = .rxvt_extension; - self.buf_start = self.buf_idx; - }, - else => self.state = .invalid, - }, - - .rxvt_extension => switch (c) { - 'a'...'z' => {}, - ';' => { - const ext = self.buf[self.buf_start .. self.buf_idx - 1]; - if (!std.mem.eql(u8, ext, "notify")) { - log.warn("unknown rxvt extension: {s}", .{ext}); - self.state = .invalid; - return; - } - - self.command = .{ .show_desktop_notification = undefined }; - self.buf_start = self.buf_idx; - self.state = .notification_title; - }, - else => self.state = .invalid, - }, - - .notification_title => switch (c) { - ';' => { - self.command.show_desktop_notification.title = self.buf[self.buf_start .. self.buf_idx - 1]; - self.temp_state = .{ .str = &self.command.show_desktop_notification.body }; - self.buf_start = self.buf_idx; - self.state = .string; - }, - else => {}, - }, - - .@"9" => switch (c) { - ';' => { - self.command = .{ .show_desktop_notification = .{ - .title = "", - .body = undefined, - } }; - - self.temp_state = .{ .str = &self.command.show_desktop_notification.body }; - self.buf_start = self.buf_idx; - self.state = .string; - }, - else => self.state = .invalid, - }, - - .query_fg_color => switch (c) { - '?' => { - self.command = .{ .report_color = .{ .kind = .foreground } }; - self.complete = true; - self.state = .invalid; - }, - else => { - self.command = .{ .set_color = .{ - .kind = .foreground, - .value = "", - } }; - - self.state = .string; - self.temp_state = .{ .str = &self.command.set_color.value }; - self.buf_start = self.buf_idx - 1; - }, - }, - - .query_bg_color => switch (c) { - '?' => { - self.command = .{ .report_color = .{ .kind = .background } }; - self.complete = true; - self.state = .invalid; - }, - else => { - self.command = .{ .set_color = .{ - .kind = .background, - .value = "", - } }; - - self.state = .string; - self.temp_state = .{ .str = &self.command.set_color.value }; - self.buf_start = self.buf_idx - 1; - }, - }, - - .query_cursor_color => switch (c) { - '?' => { - self.command = .{ .report_color = .{ .kind = .cursor } }; - self.complete = true; - self.state = .invalid; - }, - else => { - self.command = .{ .set_color = .{ - .kind = .cursor, - .value = "", - } }; - - self.state = .string; - self.temp_state = .{ .str = &self.command.set_color.value }; - self.buf_start = self.buf_idx - 1; - }, - }, - - .semantic_prompt => switch (c) { - 'A' => { - self.state = .semantic_option_start; - self.command = .{ .prompt_start = .{} }; - self.complete = true; - }, - - 'B' => { - self.state = .semantic_option_start; - self.command = .{ .prompt_end = {} }; - self.complete = true; - }, - - 'C' => { - self.state = .semantic_option_start; - self.command = .{ .end_of_input = {} }; - self.complete = true; - }, - - 'D' => { - self.state = .semantic_exit_code_start; - self.command = .{ .end_of_command = .{} }; - self.complete = true; - }, - - else => self.state = .invalid, - }, - - .semantic_option_start => switch (c) { - ';' => { - self.state = .semantic_option_key; - self.buf_start = self.buf_idx; - }, - else => self.state = .invalid, - }, - - .semantic_option_key => switch (c) { - '=' => { - self.temp_state = .{ .key = self.buf[self.buf_start .. self.buf_idx - 1] }; - self.state = .semantic_option_value; - self.buf_start = self.buf_idx; - }, - else => {}, - }, - - .semantic_option_value => switch (c) { - ';' => { - self.endSemanticOptionValue(); - self.state = .semantic_option_key; - self.buf_start = self.buf_idx; - }, - else => {}, - }, - - .semantic_exit_code_start => switch (c) { - ';' => { - // No longer complete, if ';' shows up we expect some code. - self.complete = false; - self.state = .semantic_exit_code; - self.temp_state = .{ .num = 0 }; - self.buf_start = self.buf_idx; - }, - else => self.state = .invalid, - }, - - .semantic_exit_code => switch (c) { - '0', '1', '2', '3', '4', '5', '6', '7', '8', '9' => { - self.complete = true; - - const idx = self.buf_idx - self.buf_start; - if (idx > 0) self.temp_state.num *|= 10; - self.temp_state.num +|= c - '0'; - }, - ';' => { - self.endSemanticExitCode(); - self.state = .semantic_option_key; - self.buf_start = self.buf_idx; - }, - else => self.state = .invalid, - }, - - .allocable_string => { - const alloc = self.alloc.?; - const list = self.buf_dynamic.?; - list.append(alloc, c) catch { - self.state = .invalid; - return; - }; - - // Never consume buffer space for allocable strings - self.buf_idx -= 1; - - // We can complete at any time - self.complete = true; - }, - - .string => self.complete = true, - } - } - - fn prepAllocableString(self: *Parser) void { - assert(self.buf_dynamic == null); - - // We need an allocator. If we don't have an allocator, we - // pretend we're just a fixed buffer string and hope we fit! - const alloc = self.alloc orelse { - self.state = .string; - return; - }; - - // Allocate our dynamic buffer - const list = alloc.create(std.ArrayListUnmanaged(u8)) catch { - self.state = .string; - return; - }; - list.* = .{}; - - self.buf_dynamic = list; - self.state = .allocable_string; - } - - fn endSemanticOptionValue(self: *Parser) void { - const value = self.buf[self.buf_start..self.buf_idx]; - - if (mem.eql(u8, self.temp_state.key, "aid")) { - switch (self.command) { - .prompt_start => |*v| v.aid = value, - else => {}, - } - } else if (mem.eql(u8, self.temp_state.key, "redraw")) { - // Kitty supports a "redraw" option for prompt_start. I can't find - // this documented anywhere but can see in the code that this is used - // by shell environments to tell the terminal that the shell will NOT - // redraw the prompt so we should attempt to resize it. - switch (self.command) { - .prompt_start => |*v| { - const valid = if (value.len == 1) valid: { - switch (value[0]) { - '0' => v.redraw = false, - '1' => v.redraw = true, - else => break :valid false, - } - - break :valid true; - } else false; - - if (!valid) { - log.info("OSC 133 A invalid redraw value: {s}", .{value}); - } - }, - else => {}, - } - } else if (mem.eql(u8, self.temp_state.key, "k")) { - // The "k" marks the kind of prompt, or "primary" if we don't know. - // This can be used to distinguish between the first prompt, - // a continuation, etc. - switch (self.command) { - .prompt_start => |*v| if (value.len == 1) { - v.kind = switch (value[0]) { - 'c', 's' => .continuation, - 'r' => .right, - 'i' => .primary, - else => .primary, - }; - }, - else => {}, - } - } else log.info("unknown semantic prompts option: {s}", .{self.temp_state.key}); - } - - fn endSemanticExitCode(self: *Parser) void { - switch (self.command) { - .end_of_command => |*v| v.exit_code = @truncate(self.temp_state.num), - else => {}, - } - } - - fn endString(self: *Parser) void { - self.temp_state.str.* = self.buf[self.buf_start..self.buf_idx]; - } - - fn endAllocableString(self: *Parser) void { - const list = self.buf_dynamic.?; - self.temp_state.str.* = list.items; - } - - /// End the sequence and return the command, if any. If the return value - /// is null, then no valid command was found. The optional terminator_ch - /// is the final character in the OSC sequence. This is used to determine - /// the response terminator. - pub fn end(self: *Parser, terminator_ch: ?u8) ?Command { - if (!self.complete) { - log.warn("invalid OSC command: {s}", .{self.buf[0..self.buf_idx]}); - return null; - } - - // Other cleanup we may have to do depending on state. - switch (self.state) { - .semantic_exit_code => self.endSemanticExitCode(), - .semantic_option_value => self.endSemanticOptionValue(), - .string => self.endString(), - .allocable_string => self.endAllocableString(), - else => {}, - } - - switch (self.command) { - .report_color => |*c| c.terminator = Terminator.init(terminator_ch), - else => {}, - } - - return self.command; - } -}; - -test "OSC: change_window_title" { - const testing = std.testing; - - var p: Parser = .{}; - p.next('0'); - p.next(';'); - p.next('a'); - p.next('b'); - const cmd = p.end(null).?; - try testing.expect(cmd == .change_window_title); - try testing.expectEqualStrings("ab", cmd.change_window_title); -} - -test "OSC: change_window_title with 2" { - const testing = std.testing; - - var p: Parser = .{}; - p.next('2'); - p.next(';'); - p.next('a'); - p.next('b'); - const cmd = p.end(null).?; - try testing.expect(cmd == .change_window_title); - try testing.expectEqualStrings("ab", cmd.change_window_title); -} - -test "OSC: change_window_title with utf8" { - const testing = std.testing; - - var p: Parser = .{}; - p.next('2'); - p.next(';'); - // '—' EM DASH U+2014 (E2 80 94) - p.next(0xE2); - p.next(0x80); - p.next(0x94); - - p.next(' '); - // '‐' HYPHEN U+2010 (E2 80 90) - // Intententionally chosen to conflict with the 0x90 C1 control - p.next(0xE2); - p.next(0x80); - p.next(0x90); - const cmd = p.end(null).?; - try testing.expect(cmd == .change_window_title); - try testing.expectEqualStrings("— ‐", cmd.change_window_title); -} - -test "OSC: change_window_icon" { - const testing = std.testing; - - var p: Parser = .{}; - p.next('1'); - p.next(';'); - p.next('a'); - p.next('b'); - const cmd = p.end(null).?; - try testing.expect(cmd == .change_window_icon); - try testing.expectEqualStrings("ab", cmd.change_window_icon); -} - -test "OSC: prompt_start" { - const testing = std.testing; - - var p: Parser = .{}; - - const input = "133;A"; - for (input) |ch| p.next(ch); - - const cmd = p.end(null).?; - try testing.expect(cmd == .prompt_start); - try testing.expect(cmd.prompt_start.aid == null); - try testing.expect(cmd.prompt_start.redraw); -} - -test "OSC: prompt_start with single option" { - const testing = std.testing; - - var p: Parser = .{}; - - const input = "133;A;aid=14"; - for (input) |ch| p.next(ch); - - const cmd = p.end(null).?; - try testing.expect(cmd == .prompt_start); - try testing.expectEqualStrings("14", cmd.prompt_start.aid.?); -} - -test "OSC: prompt_start with redraw disabled" { - const testing = std.testing; - - var p: Parser = .{}; - - const input = "133;A;redraw=0"; - for (input) |ch| p.next(ch); - - const cmd = p.end(null).?; - try testing.expect(cmd == .prompt_start); - try testing.expect(!cmd.prompt_start.redraw); -} - -test "OSC: prompt_start with redraw invalid value" { - const testing = std.testing; - - var p: Parser = .{}; - - const input = "133;A;redraw=42"; - for (input) |ch| p.next(ch); - - const cmd = p.end(null).?; - try testing.expect(cmd == .prompt_start); - try testing.expect(cmd.prompt_start.redraw); - try testing.expect(cmd.prompt_start.kind == .primary); -} - -test "OSC: prompt_start with continuation" { - const testing = std.testing; - - var p: Parser = .{}; - - const input = "133;A;k=c"; - for (input) |ch| p.next(ch); - - const cmd = p.end(null).?; - try testing.expect(cmd == .prompt_start); - try testing.expect(cmd.prompt_start.kind == .continuation); -} - -test "OSC: end_of_command no exit code" { - const testing = std.testing; - - var p: Parser = .{}; - - const input = "133;D"; - for (input) |ch| p.next(ch); - - const cmd = p.end(null).?; - try testing.expect(cmd == .end_of_command); -} - -test "OSC: end_of_command with exit code" { - const testing = std.testing; - - var p: Parser = .{}; - - const input = "133;D;25"; - for (input) |ch| p.next(ch); - - const cmd = p.end(null).?; - try testing.expect(cmd == .end_of_command); - try testing.expectEqual(@as(u8, 25), cmd.end_of_command.exit_code.?); -} - -test "OSC: prompt_end" { - const testing = std.testing; - - var p: Parser = .{}; - - const input = "133;B"; - for (input) |ch| p.next(ch); - - const cmd = p.end(null).?; - try testing.expect(cmd == .prompt_end); -} - -test "OSC: end_of_input" { - const testing = std.testing; - - var p: Parser = .{}; - - const input = "133;C"; - for (input) |ch| p.next(ch); - - const cmd = p.end(null).?; - try testing.expect(cmd == .end_of_input); -} - -test "OSC: reset cursor color" { - const testing = std.testing; - - var p: Parser = .{}; - - const input = "112"; - for (input) |ch| p.next(ch); - - const cmd = p.end(null).?; - try testing.expect(cmd == .reset_color); - try testing.expectEqual(cmd.reset_color.kind, .cursor); -} - -test "OSC: get/set clipboard" { - const testing = std.testing; - - var p: Parser = .{}; - - const input = "52;s;?"; - for (input) |ch| p.next(ch); - - const cmd = p.end(null).?; - try testing.expect(cmd == .clipboard_contents); - try testing.expect(cmd.clipboard_contents.kind == 's'); - try testing.expect(std.mem.eql(u8, "?", cmd.clipboard_contents.data)); -} - -test "OSC: get/set clipboard (optional parameter)" { - const testing = std.testing; - - var p: Parser = .{}; - - const input = "52;;?"; - for (input) |ch| p.next(ch); - - const cmd = p.end(null).?; - try testing.expect(cmd == .clipboard_contents); - try testing.expect(cmd.clipboard_contents.kind == 'c'); - try testing.expect(std.mem.eql(u8, "?", cmd.clipboard_contents.data)); -} - -test "OSC: get/set clipboard with allocator" { - const testing = std.testing; - const alloc = testing.allocator; - - var p: Parser = .{ .alloc = alloc }; - defer p.deinit(); - - const input = "52;s;?"; - for (input) |ch| p.next(ch); - - const cmd = p.end(null).?; - try testing.expect(cmd == .clipboard_contents); - try testing.expect(cmd.clipboard_contents.kind == 's'); - try testing.expect(std.mem.eql(u8, "?", cmd.clipboard_contents.data)); -} - -test "OSC: report pwd" { - const testing = std.testing; - - var p: Parser = .{}; - - const input = "7;file:///tmp/example"; - for (input) |ch| p.next(ch); - - const cmd = p.end(null).?; - try testing.expect(cmd == .report_pwd); - try testing.expect(std.mem.eql(u8, "file:///tmp/example", cmd.report_pwd.value)); -} - -test "OSC: pointer cursor" { - const testing = std.testing; - - var p: Parser = .{}; - - const input = "22;pointer"; - for (input) |ch| p.next(ch); - - const cmd = p.end(null).?; - try testing.expect(cmd == .mouse_shape); - try testing.expect(std.mem.eql(u8, "pointer", cmd.mouse_shape.value)); -} - -test "OSC: report pwd empty" { - const testing = std.testing; - - var p: Parser = .{}; - - const input = "7;"; - for (input) |ch| p.next(ch); - - try testing.expect(p.end(null) == null); -} - -test "OSC: longer than buffer" { - const testing = std.testing; - - var p: Parser = .{}; - - const input = "a" ** (Parser.MAX_BUF + 2); - for (input) |ch| p.next(ch); - - try testing.expect(p.end(null) == null); -} - -test "OSC: report default foreground color" { - const testing = std.testing; - - var p: Parser = .{}; - - const input = "10;?"; - for (input) |ch| p.next(ch); - - // This corresponds to ST = ESC followed by \ - const cmd = p.end('\x1b').?; - try testing.expect(cmd == .report_color); - try testing.expectEqual(cmd.report_color.kind, .foreground); - try testing.expectEqual(cmd.report_color.terminator, .st); -} - -test "OSC: set foreground color" { - const testing = std.testing; - - var p: Parser = .{}; - - const input = "10;rgbi:0.0/0.5/1.0"; - for (input) |ch| p.next(ch); - - const cmd = p.end('\x07').?; - try testing.expect(cmd == .set_color); - try testing.expectEqual(cmd.set_color.kind, .foreground); - try testing.expectEqualStrings(cmd.set_color.value, "rgbi:0.0/0.5/1.0"); -} - -test "OSC: report default background color" { - const testing = std.testing; - - var p: Parser = .{}; - - const input = "11;?"; - for (input) |ch| p.next(ch); - - // This corresponds to ST = BEL character - const cmd = p.end('\x07').?; - try testing.expect(cmd == .report_color); - try testing.expectEqual(cmd.report_color.kind, .background); - try testing.expectEqual(cmd.report_color.terminator, .bel); -} - -test "OSC: set background color" { - const testing = std.testing; - - var p: Parser = .{}; - - const input = "11;rgb:f/ff/ffff"; - for (input) |ch| p.next(ch); - - const cmd = p.end('\x1b').?; - try testing.expect(cmd == .set_color); - try testing.expectEqual(cmd.set_color.kind, .background); - try testing.expectEqualStrings(cmd.set_color.value, "rgb:f/ff/ffff"); -} - -test "OSC: get palette color" { - const testing = std.testing; - - var p: Parser = .{}; - - const input = "4;1;?"; - for (input) |ch| p.next(ch); - - const cmd = p.end('\x1b').?; - try testing.expect(cmd == .report_color); - try testing.expectEqual(Command.ColorKind{ .palette = 1 }, cmd.report_color.kind); - try testing.expectEqual(cmd.report_color.terminator, .st); -} - -test "OSC: set palette color" { - const testing = std.testing; - - var p: Parser = .{}; - - const input = "4;17;rgb:aa/bb/cc"; - for (input) |ch| p.next(ch); - - const cmd = p.end('\x1b').?; - try testing.expect(cmd == .set_color); - try testing.expectEqual(Command.ColorKind{ .palette = 17 }, cmd.set_color.kind); - try testing.expectEqualStrings(cmd.set_color.value, "rgb:aa/bb/cc"); -} - -test "OSC: show desktop notification" { - const testing = std.testing; - - var p: Parser = .{}; - - const input = "9;Hello world"; - for (input) |ch| p.next(ch); - - const cmd = p.end('\x1b').?; - try testing.expect(cmd == .show_desktop_notification); - try testing.expectEqualStrings(cmd.show_desktop_notification.title, ""); - try testing.expectEqualStrings(cmd.show_desktop_notification.body, "Hello world"); -} - -test "OSC: show desktop notification with title" { - const testing = std.testing; - - var p: Parser = .{}; - - const input = "777;notify;Title;Body"; - for (input) |ch| p.next(ch); - - const cmd = p.end('\x1b').?; - try testing.expect(cmd == .show_desktop_notification); - try testing.expectEqualStrings(cmd.show_desktop_notification.title, "Title"); - try testing.expectEqualStrings(cmd.show_desktop_notification.body, "Body"); -} - -test "OSC: empty param" { - const testing = std.testing; - - var p: Parser = .{}; - - const input = "4;;"; - for (input) |ch| p.next(ch); - - const cmd = p.end('\x1b'); - try testing.expect(cmd == null); -} diff --git a/src/terminal-old/parse_table.zig b/src/terminal-old/parse_table.zig deleted file mode 100644 index 66c443783..000000000 --- a/src/terminal-old/parse_table.zig +++ /dev/null @@ -1,389 +0,0 @@ -//! The primary export of this file is "table", which contains a -//! comptime-generated state transition table for VT emulation. -//! -//! This is based on the vt100.net state machine: -//! https://vt100.net/emu/dec_ansi_parser -//! But has some modifications: -//! -//! * csi_param accepts the colon character (':') since the SGR command -//! accepts colon as a valid parameter value. -//! - -const std = @import("std"); -const builtin = @import("builtin"); -const parser = @import("Parser.zig"); -const State = parser.State; -const Action = parser.TransitionAction; - -/// The state transition table. The type is [u8][State]Transition but -/// comptime-generated to be exactly-sized. -pub const table = genTable(); - -/// Table is the type of the state table. This is dynamically (comptime) -/// generated to be exactly sized. -pub const Table = genTableType(false); - -/// OptionalTable is private to this file. We use this to accumulate and -/// detect invalid transitions created. -const OptionalTable = genTableType(true); - -// Transition is the transition to take within the table -pub const Transition = struct { - state: State, - action: Action, -}; - -/// Table is the type of the state transition table. -fn genTableType(comptime optional: bool) type { - const max_u8 = std.math.maxInt(u8); - const stateInfo = @typeInfo(State); - const max_state = stateInfo.Enum.fields.len; - const Elem = if (optional) ?Transition else Transition; - return [max_u8 + 1][max_state]Elem; -} - -/// Function to generate the full state transition table for VT emulation. -fn genTable() Table { - @setEvalBranchQuota(20000); - - // We accumulate using an "optional" table so we can detect duplicates. - var result: OptionalTable = undefined; - for (0..result.len) |i| { - for (0..result[0].len) |j| { - result[i][j] = null; - } - } - - // anywhere transitions - const stateInfo = @typeInfo(State); - inline for (stateInfo.Enum.fields) |field| { - const source: State = @enumFromInt(field.value); - - // anywhere => ground - single(&result, 0x18, source, .ground, .execute); - single(&result, 0x1A, source, .ground, .execute); - range(&result, 0x80, 0x8F, source, .ground, .execute); - range(&result, 0x91, 0x97, source, .ground, .execute); - single(&result, 0x99, source, .ground, .execute); - single(&result, 0x9A, source, .ground, .execute); - single(&result, 0x9C, source, .ground, .none); - - // anywhere => escape - single(&result, 0x1B, source, .escape, .none); - - // anywhere => sos_pm_apc_string - single(&result, 0x98, source, .sos_pm_apc_string, .none); - single(&result, 0x9E, source, .sos_pm_apc_string, .none); - single(&result, 0x9F, source, .sos_pm_apc_string, .none); - - // anywhere => csi_entry - single(&result, 0x9B, source, .csi_entry, .none); - - // anywhere => dcs_entry - single(&result, 0x90, source, .dcs_entry, .none); - - // anywhere => osc_string - single(&result, 0x9D, source, .osc_string, .none); - } - - // ground - { - // events - single(&result, 0x19, .ground, .ground, .execute); - range(&result, 0, 0x17, .ground, .ground, .execute); - range(&result, 0x1C, 0x1F, .ground, .ground, .execute); - range(&result, 0x20, 0x7F, .ground, .ground, .print); - } - - // escape_intermediate - { - const source = State.escape_intermediate; - - single(&result, 0x19, source, source, .execute); - range(&result, 0, 0x17, source, source, .execute); - range(&result, 0x1C, 0x1F, source, source, .execute); - range(&result, 0x20, 0x2F, source, source, .collect); - single(&result, 0x7F, source, source, .ignore); - - // => ground - range(&result, 0x30, 0x7E, source, .ground, .esc_dispatch); - } - - // sos_pm_apc_string - { - const source = State.sos_pm_apc_string; - - // events - single(&result, 0x19, source, source, .apc_put); - range(&result, 0, 0x17, source, source, .apc_put); - range(&result, 0x1C, 0x1F, source, source, .apc_put); - range(&result, 0x20, 0x7F, source, source, .apc_put); - } - - // escape - { - const source = State.escape; - - // events - single(&result, 0x19, source, source, .execute); - range(&result, 0, 0x17, source, source, .execute); - range(&result, 0x1C, 0x1F, source, source, .execute); - single(&result, 0x7F, source, source, .ignore); - - // => ground - range(&result, 0x30, 0x4F, source, .ground, .esc_dispatch); - range(&result, 0x51, 0x57, source, .ground, .esc_dispatch); - range(&result, 0x60, 0x7E, source, .ground, .esc_dispatch); - single(&result, 0x59, source, .ground, .esc_dispatch); - single(&result, 0x5A, source, .ground, .esc_dispatch); - single(&result, 0x5C, source, .ground, .esc_dispatch); - - // => escape_intermediate - range(&result, 0x20, 0x2F, source, .escape_intermediate, .collect); - - // => sos_pm_apc_string - single(&result, 0x58, source, .sos_pm_apc_string, .none); - single(&result, 0x5E, source, .sos_pm_apc_string, .none); - single(&result, 0x5F, source, .sos_pm_apc_string, .none); - - // => dcs_entry - single(&result, 0x50, source, .dcs_entry, .none); - - // => csi_entry - single(&result, 0x5B, source, .csi_entry, .none); - - // => osc_string - single(&result, 0x5D, source, .osc_string, .none); - } - - // dcs_entry - { - const source = State.dcs_entry; - - // events - single(&result, 0x19, source, source, .ignore); - range(&result, 0, 0x17, source, source, .ignore); - range(&result, 0x1C, 0x1F, source, source, .ignore); - single(&result, 0x7F, source, source, .ignore); - - // => dcs_intermediate - range(&result, 0x20, 0x2F, source, .dcs_intermediate, .collect); - - // => dcs_ignore - single(&result, 0x3A, source, .dcs_ignore, .none); - - // => dcs_param - range(&result, 0x30, 0x39, source, .dcs_param, .param); - single(&result, 0x3B, source, .dcs_param, .param); - range(&result, 0x3C, 0x3F, source, .dcs_param, .collect); - - // => dcs_passthrough - range(&result, 0x40, 0x7E, source, .dcs_passthrough, .none); - } - - // dcs_intermediate - { - const source = State.dcs_intermediate; - - // events - single(&result, 0x19, source, source, .ignore); - range(&result, 0, 0x17, source, source, .ignore); - range(&result, 0x1C, 0x1F, source, source, .ignore); - range(&result, 0x20, 0x2F, source, source, .collect); - single(&result, 0x7F, source, source, .ignore); - - // => dcs_ignore - range(&result, 0x30, 0x3F, source, .dcs_ignore, .none); - - // => dcs_passthrough - range(&result, 0x40, 0x7E, source, .dcs_passthrough, .none); - } - - // dcs_ignore - { - const source = State.dcs_ignore; - - // events - single(&result, 0x19, source, source, .ignore); - range(&result, 0, 0x17, source, source, .ignore); - range(&result, 0x1C, 0x1F, source, source, .ignore); - } - - // dcs_param - { - const source = State.dcs_param; - - // events - single(&result, 0x19, source, source, .ignore); - range(&result, 0, 0x17, source, source, .ignore); - range(&result, 0x1C, 0x1F, source, source, .ignore); - range(&result, 0x30, 0x39, source, source, .param); - single(&result, 0x3B, source, source, .param); - single(&result, 0x7F, source, source, .ignore); - - // => dcs_ignore - single(&result, 0x3A, source, .dcs_ignore, .none); - range(&result, 0x3C, 0x3F, source, .dcs_ignore, .none); - - // => dcs_intermediate - range(&result, 0x20, 0x2F, source, .dcs_intermediate, .collect); - - // => dcs_passthrough - range(&result, 0x40, 0x7E, source, .dcs_passthrough, .none); - } - - // dcs_passthrough - { - const source = State.dcs_passthrough; - - // events - single(&result, 0x19, source, source, .put); - range(&result, 0, 0x17, source, source, .put); - range(&result, 0x1C, 0x1F, source, source, .put); - range(&result, 0x20, 0x7E, source, source, .put); - single(&result, 0x7F, source, source, .ignore); - } - - // csi_param - { - const source = State.csi_param; - - // events - single(&result, 0x19, source, source, .execute); - range(&result, 0, 0x17, source, source, .execute); - range(&result, 0x1C, 0x1F, source, source, .execute); - range(&result, 0x30, 0x39, source, source, .param); - single(&result, 0x3A, source, source, .param); - single(&result, 0x3B, source, source, .param); - single(&result, 0x7F, source, source, .ignore); - - // => ground - range(&result, 0x40, 0x7E, source, .ground, .csi_dispatch); - - // => csi_ignore - range(&result, 0x3C, 0x3F, source, .csi_ignore, .none); - - // => csi_intermediate - range(&result, 0x20, 0x2F, source, .csi_intermediate, .collect); - } - - // csi_ignore - { - const source = State.csi_ignore; - - // events - single(&result, 0x19, source, source, .execute); - range(&result, 0, 0x17, source, source, .execute); - range(&result, 0x1C, 0x1F, source, source, .execute); - range(&result, 0x20, 0x3F, source, source, .ignore); - single(&result, 0x7F, source, source, .ignore); - - // => ground - range(&result, 0x40, 0x7E, source, .ground, .none); - } - - // csi_intermediate - { - const source = State.csi_intermediate; - - // events - single(&result, 0x19, source, source, .execute); - range(&result, 0, 0x17, source, source, .execute); - range(&result, 0x1C, 0x1F, source, source, .execute); - range(&result, 0x20, 0x2F, source, source, .collect); - single(&result, 0x7F, source, source, .ignore); - - // => ground - range(&result, 0x40, 0x7E, source, .ground, .csi_dispatch); - - // => csi_ignore - range(&result, 0x30, 0x3F, source, .csi_ignore, .none); - } - - // csi_entry - { - const source = State.csi_entry; - - // events - single(&result, 0x19, source, source, .execute); - range(&result, 0, 0x17, source, source, .execute); - range(&result, 0x1C, 0x1F, source, source, .execute); - single(&result, 0x7F, source, source, .ignore); - - // => ground - range(&result, 0x40, 0x7E, source, .ground, .csi_dispatch); - - // => csi_ignore - single(&result, 0x3A, source, .csi_ignore, .none); - - // => csi_intermediate - range(&result, 0x20, 0x2F, source, .csi_intermediate, .collect); - - // => csi_param - range(&result, 0x30, 0x39, source, .csi_param, .param); - single(&result, 0x3B, source, .csi_param, .param); - range(&result, 0x3C, 0x3F, source, .csi_param, .collect); - } - - // osc_string - { - const source = State.osc_string; - - // events - single(&result, 0x19, source, source, .ignore); - range(&result, 0, 0x06, source, source, .ignore); - range(&result, 0x08, 0x17, source, source, .ignore); - range(&result, 0x1C, 0x1F, source, source, .ignore); - range(&result, 0x20, 0xFF, source, source, .osc_put); - - // XTerm accepts either BEL or ST for terminating OSC - // sequences, and when returning information, uses the same - // terminator used in a query. - single(&result, 0x07, source, .ground, .none); - } - - // Create our immutable version - var final: Table = undefined; - for (0..final.len) |i| { - for (0..final[0].len) |j| { - final[i][j] = result[i][j] orelse transition(@enumFromInt(j), .none); - } - } - - return final; -} - -fn single(t: *OptionalTable, c: u8, s0: State, s1: State, a: Action) void { - const s0_int = @intFromEnum(s0); - - // TODO: enable this but it thinks we're in runtime right now - // if (t[c][s0_int]) |existing| { - // @compileLog(c); - // @compileLog(s0); - // @compileLog(s1); - // @compileLog(existing); - // @compileError("transition set multiple times"); - // } - - t[c][s0_int] = transition(s1, a); -} - -fn range(t: *OptionalTable, from: u8, to: u8, s0: State, s1: State, a: Action) void { - var i = from; - while (i <= to) : (i += 1) { - single(t, i, s0, s1, a); - // If 'to' is 0xFF, our next pass will overflow. Return early to prevent - // the loop from executing it's continue expression - if (i == to) break; - } -} - -fn transition(state: State, action: Action) Transition { - return .{ .state = state, .action = action }; -} - -test { - // This forces comptime-evaluation of table, so we're just testing - // that it succeeds in creation. - _ = table; -} diff --git a/src/terminal-old/point.zig b/src/terminal-old/point.zig deleted file mode 100644 index 8c694f992..000000000 --- a/src/terminal-old/point.zig +++ /dev/null @@ -1,254 +0,0 @@ -const std = @import("std"); -const terminal = @import("main.zig"); -const Screen = terminal.Screen; - -// This file contains various types to represent x/y coordinates. We -// use different types so that we can lean on type-safety to get the -// exact expected type of point. - -/// Active is a point within the active part of the screen. -pub const Active = struct { - x: usize = 0, - y: usize = 0, - - pub fn toScreen(self: Active, screen: *const Screen) ScreenPoint { - return .{ - .x = self.x, - .y = screen.history + self.y, - }; - } - - test "toScreen with scrollback" { - const testing = std.testing; - const alloc = testing.allocator; - - var s = try Screen.init(alloc, 3, 5, 3); - defer s.deinit(); - const str = "1\n2\n3\n4\n5\n6\n7\n8"; - try s.testWriteString(str); - - try testing.expectEqual(ScreenPoint{ - .x = 1, - .y = 5, - }, (Active{ .x = 1, .y = 2 }).toScreen(&s)); - } -}; - -/// Viewport is a point within the viewport of the screen. -pub const Viewport = struct { - x: usize = 0, - y: usize = 0, - - pub fn toScreen(self: Viewport, screen: *const Screen) ScreenPoint { - // x is unchanged, y we have to add the visible offset to - // get the full offset from the top. - return .{ - .x = self.x, - .y = screen.viewport + self.y, - }; - } - - pub fn eql(self: Viewport, other: Viewport) bool { - return self.x == other.x and self.y == other.y; - } - - test "toScreen with no scrollback" { - const testing = std.testing; - const alloc = testing.allocator; - - var s = try Screen.init(alloc, 3, 5, 0); - defer s.deinit(); - - try testing.expectEqual(ScreenPoint{ - .x = 1, - .y = 1, - }, (Viewport{ .x = 1, .y = 1 }).toScreen(&s)); - } - - test "toScreen with scrollback" { - const testing = std.testing; - const alloc = testing.allocator; - - var s = try Screen.init(alloc, 3, 5, 3); - defer s.deinit(); - - // At the bottom - try s.scroll(.{ .screen = 6 }); - try testing.expectEqual(ScreenPoint{ - .x = 0, - .y = 3, - }, (Viewport{ .x = 0, .y = 0 }).toScreen(&s)); - - // Move the viewport a bit up - try s.scroll(.{ .screen = -1 }); - try testing.expectEqual(ScreenPoint{ - .x = 0, - .y = 2, - }, (Viewport{ .x = 0, .y = 0 }).toScreen(&s)); - - // Move the viewport to top - try s.scroll(.{ .top = {} }); - try testing.expectEqual(ScreenPoint{ - .x = 0, - .y = 0, - }, (Viewport{ .x = 0, .y = 0 }).toScreen(&s)); - } -}; - -/// A screen point. This is offset from the top of the scrollback -/// buffer. If the screen is scrolled or resized, this will have to -/// be recomputed. -pub const ScreenPoint = struct { - x: usize = 0, - y: usize = 0, - - /// Returns if this point is before another point. - pub fn before(self: ScreenPoint, other: ScreenPoint) bool { - return self.y < other.y or - (self.y == other.y and self.x < other.x); - } - - /// Returns if two points are equal. - pub fn eql(self: ScreenPoint, other: ScreenPoint) bool { - return self.x == other.x and self.y == other.y; - } - - /// Returns true if this screen point is currently in the active viewport. - pub fn inViewport(self: ScreenPoint, screen: *const Screen) bool { - return self.y >= screen.viewport and - self.y < screen.viewport + screen.rows; - } - - /// Converts this to a viewport point. If the point is above the - /// viewport this will move the point to (0, 0) and if it is below - /// the viewport it'll move it to (cols - 1, rows - 1). - pub fn toViewport(self: ScreenPoint, screen: *const Screen) Viewport { - // TODO: test - - // Before viewport - if (self.y < screen.viewport) return .{ .x = 0, .y = 0 }; - - // After viewport - if (self.y > screen.viewport + screen.rows) return .{ - .x = screen.cols - 1, - .y = screen.rows - 1, - }; - - return .{ .x = self.x, .y = self.y - screen.viewport }; - } - - /// Returns a screen point iterator. This will iterate over all of - /// of the points in a screen in a given direction one by one. - /// - /// The iterator is only valid as long as the screen is not resized. - pub fn iterator( - self: ScreenPoint, - screen: *const Screen, - dir: Direction, - ) Iterator { - return .{ .screen = screen, .current = self, .direction = dir }; - } - - pub const Iterator = struct { - screen: *const Screen, - current: ?ScreenPoint, - direction: Direction, - - pub fn next(self: *Iterator) ?ScreenPoint { - const current = self.current orelse return null; - self.current = switch (self.direction) { - .left_up => left_up: { - if (current.x == 0) { - if (current.y == 0) break :left_up null; - break :left_up .{ - .x = self.screen.cols - 1, - .y = current.y - 1, - }; - } - - break :left_up .{ - .x = current.x - 1, - .y = current.y, - }; - }, - - .right_down => right_down: { - if (current.x == self.screen.cols - 1) { - const max = self.screen.rows + self.screen.max_scrollback; - if (current.y == max - 1) break :right_down null; - break :right_down .{ - .x = 0, - .y = current.y + 1, - }; - } - - break :right_down .{ - .x = current.x + 1, - .y = current.y, - }; - }, - }; - - return current; - } - }; - - test "before" { - const testing = std.testing; - - const p: ScreenPoint = .{ .x = 5, .y = 2 }; - try testing.expect(p.before(.{ .x = 6, .y = 2 })); - try testing.expect(p.before(.{ .x = 3, .y = 3 })); - } - - test "iterator" { - const testing = std.testing; - const alloc = testing.allocator; - - var s = try Screen.init(alloc, 5, 5, 0); - defer s.deinit(); - - // Back from the first line - { - var pt: ScreenPoint = .{ .x = 1, .y = 0 }; - var it = pt.iterator(&s, .left_up); - try testing.expectEqual(ScreenPoint{ .x = 1, .y = 0 }, it.next().?); - try testing.expectEqual(ScreenPoint{ .x = 0, .y = 0 }, it.next().?); - try testing.expect(it.next() == null); - } - - // Back from second line - { - var pt: ScreenPoint = .{ .x = 1, .y = 1 }; - var it = pt.iterator(&s, .left_up); - try testing.expectEqual(ScreenPoint{ .x = 1, .y = 1 }, it.next().?); - try testing.expectEqual(ScreenPoint{ .x = 0, .y = 1 }, it.next().?); - try testing.expectEqual(ScreenPoint{ .x = 4, .y = 0 }, it.next().?); - } - - // Forward last line - { - var pt: ScreenPoint = .{ .x = 3, .y = 4 }; - var it = pt.iterator(&s, .right_down); - try testing.expectEqual(ScreenPoint{ .x = 3, .y = 4 }, it.next().?); - try testing.expectEqual(ScreenPoint{ .x = 4, .y = 4 }, it.next().?); - try testing.expect(it.next() == null); - } - - // Forward not last line - { - var pt: ScreenPoint = .{ .x = 3, .y = 3 }; - var it = pt.iterator(&s, .right_down); - try testing.expectEqual(ScreenPoint{ .x = 3, .y = 3 }, it.next().?); - try testing.expectEqual(ScreenPoint{ .x = 4, .y = 3 }, it.next().?); - try testing.expectEqual(ScreenPoint{ .x = 0, .y = 4 }, it.next().?); - } - } -}; - -/// Direction that points can go. -pub const Direction = enum { left_up, right_down }; - -test { - std.testing.refAllDecls(@This()); -} diff --git a/src/terminal-old/res/rgb.txt b/src/terminal-old/res/rgb.txt deleted file mode 100644 index 709664376..000000000 --- a/src/terminal-old/res/rgb.txt +++ /dev/null @@ -1,782 +0,0 @@ -255 250 250 snow -248 248 255 ghost white -248 248 255 GhostWhite -245 245 245 white smoke -245 245 245 WhiteSmoke -220 220 220 gainsboro -255 250 240 floral white -255 250 240 FloralWhite -253 245 230 old lace -253 245 230 OldLace -250 240 230 linen -250 235 215 antique white -250 235 215 AntiqueWhite -255 239 213 papaya whip -255 239 213 PapayaWhip -255 235 205 blanched almond -255 235 205 BlanchedAlmond -255 228 196 bisque -255 218 185 peach puff -255 218 185 PeachPuff -255 222 173 navajo white -255 222 173 NavajoWhite -255 228 181 moccasin -255 248 220 cornsilk -255 255 240 ivory -255 250 205 lemon chiffon -255 250 205 LemonChiffon -255 245 238 seashell -240 255 240 honeydew -245 255 250 mint cream -245 255 250 MintCream -240 255 255 azure -240 248 255 alice blue -240 248 255 AliceBlue -230 230 250 lavender -255 240 245 lavender blush -255 240 245 LavenderBlush -255 228 225 misty rose -255 228 225 MistyRose -255 255 255 white - 0 0 0 black - 47 79 79 dark slate gray - 47 79 79 DarkSlateGray - 47 79 79 dark slate grey - 47 79 79 DarkSlateGrey -105 105 105 dim gray -105 105 105 DimGray -105 105 105 dim grey -105 105 105 DimGrey -112 128 144 slate gray -112 128 144 SlateGray -112 128 144 slate grey -112 128 144 SlateGrey -119 136 153 light slate gray -119 136 153 LightSlateGray -119 136 153 light slate grey -119 136 153 LightSlateGrey -190 190 190 gray -190 190 190 grey -190 190 190 x11 gray -190 190 190 X11Gray -190 190 190 x11 grey -190 190 190 X11Grey -128 128 128 web gray -128 128 128 WebGray -128 128 128 web grey -128 128 128 WebGrey -211 211 211 light grey -211 211 211 LightGrey -211 211 211 light gray -211 211 211 LightGray - 25 25 112 midnight blue - 25 25 112 MidnightBlue - 0 0 128 navy - 0 0 128 navy blue - 0 0 128 NavyBlue -100 149 237 cornflower blue -100 149 237 CornflowerBlue - 72 61 139 dark slate blue - 72 61 139 DarkSlateBlue -106 90 205 slate blue -106 90 205 SlateBlue -123 104 238 medium slate blue -123 104 238 MediumSlateBlue -132 112 255 light slate blue -132 112 255 LightSlateBlue - 0 0 205 medium blue - 0 0 205 MediumBlue - 65 105 225 royal blue - 65 105 225 RoyalBlue - 0 0 255 blue - 30 144 255 dodger blue - 30 144 255 DodgerBlue - 0 191 255 deep sky blue - 0 191 255 DeepSkyBlue -135 206 235 sky blue -135 206 235 SkyBlue -135 206 250 light sky blue -135 206 250 LightSkyBlue - 70 130 180 steel blue - 70 130 180 SteelBlue -176 196 222 light steel blue -176 196 222 LightSteelBlue -173 216 230 light blue -173 216 230 LightBlue -176 224 230 powder blue -176 224 230 PowderBlue -175 238 238 pale turquoise -175 238 238 PaleTurquoise - 0 206 209 dark turquoise - 0 206 209 DarkTurquoise - 72 209 204 medium turquoise - 72 209 204 MediumTurquoise - 64 224 208 turquoise - 0 255 255 cyan - 0 255 255 aqua -224 255 255 light cyan -224 255 255 LightCyan - 95 158 160 cadet blue - 95 158 160 CadetBlue -102 205 170 medium aquamarine -102 205 170 MediumAquamarine -127 255 212 aquamarine - 0 100 0 dark green - 0 100 0 DarkGreen - 85 107 47 dark olive green - 85 107 47 DarkOliveGreen -143 188 143 dark sea green -143 188 143 DarkSeaGreen - 46 139 87 sea green - 46 139 87 SeaGreen - 60 179 113 medium sea green - 60 179 113 MediumSeaGreen - 32 178 170 light sea green - 32 178 170 LightSeaGreen -152 251 152 pale green -152 251 152 PaleGreen - 0 255 127 spring green - 0 255 127 SpringGreen -124 252 0 lawn green -124 252 0 LawnGreen - 0 255 0 green - 0 255 0 lime - 0 255 0 x11 green - 0 255 0 X11Green - 0 128 0 web green - 0 128 0 WebGreen -127 255 0 chartreuse - 0 250 154 medium spring green - 0 250 154 MediumSpringGreen -173 255 47 green yellow -173 255 47 GreenYellow - 50 205 50 lime green - 50 205 50 LimeGreen -154 205 50 yellow green -154 205 50 YellowGreen - 34 139 34 forest green - 34 139 34 ForestGreen -107 142 35 olive drab -107 142 35 OliveDrab -189 183 107 dark khaki -189 183 107 DarkKhaki -240 230 140 khaki -238 232 170 pale goldenrod -238 232 170 PaleGoldenrod -250 250 210 light goldenrod yellow -250 250 210 LightGoldenrodYellow -255 255 224 light yellow -255 255 224 LightYellow -255 255 0 yellow -255 215 0 gold -238 221 130 light goldenrod -238 221 130 LightGoldenrod -218 165 32 goldenrod -184 134 11 dark goldenrod -184 134 11 DarkGoldenrod -188 143 143 rosy brown -188 143 143 RosyBrown -205 92 92 indian red -205 92 92 IndianRed -139 69 19 saddle brown -139 69 19 SaddleBrown -160 82 45 sienna -205 133 63 peru -222 184 135 burlywood -245 245 220 beige -245 222 179 wheat -244 164 96 sandy brown -244 164 96 SandyBrown -210 180 140 tan -210 105 30 chocolate -178 34 34 firebrick -165 42 42 brown -233 150 122 dark salmon -233 150 122 DarkSalmon -250 128 114 salmon -255 160 122 light salmon -255 160 122 LightSalmon -255 165 0 orange -255 140 0 dark orange -255 140 0 DarkOrange -255 127 80 coral -240 128 128 light coral -240 128 128 LightCoral -255 99 71 tomato -255 69 0 orange red -255 69 0 OrangeRed -255 0 0 red -255 105 180 hot pink -255 105 180 HotPink -255 20 147 deep pink -255 20 147 DeepPink -255 192 203 pink -255 182 193 light pink -255 182 193 LightPink -219 112 147 pale violet red -219 112 147 PaleVioletRed -176 48 96 maroon -176 48 96 x11 maroon -176 48 96 X11Maroon -128 0 0 web maroon -128 0 0 WebMaroon -199 21 133 medium violet red -199 21 133 MediumVioletRed -208 32 144 violet red -208 32 144 VioletRed -255 0 255 magenta -255 0 255 fuchsia -238 130 238 violet -221 160 221 plum -218 112 214 orchid -186 85 211 medium orchid -186 85 211 MediumOrchid -153 50 204 dark orchid -153 50 204 DarkOrchid -148 0 211 dark violet -148 0 211 DarkViolet -138 43 226 blue violet -138 43 226 BlueViolet -160 32 240 purple -160 32 240 x11 purple -160 32 240 X11Purple -128 0 128 web purple -128 0 128 WebPurple -147 112 219 medium purple -147 112 219 MediumPurple -216 191 216 thistle -255 250 250 snow1 -238 233 233 snow2 -205 201 201 snow3 -139 137 137 snow4 -255 245 238 seashell1 -238 229 222 seashell2 -205 197 191 seashell3 -139 134 130 seashell4 -255 239 219 AntiqueWhite1 -238 223 204 AntiqueWhite2 -205 192 176 AntiqueWhite3 -139 131 120 AntiqueWhite4 -255 228 196 bisque1 -238 213 183 bisque2 -205 183 158 bisque3 -139 125 107 bisque4 -255 218 185 PeachPuff1 -238 203 173 PeachPuff2 -205 175 149 PeachPuff3 -139 119 101 PeachPuff4 -255 222 173 NavajoWhite1 -238 207 161 NavajoWhite2 -205 179 139 NavajoWhite3 -139 121 94 NavajoWhite4 -255 250 205 LemonChiffon1 -238 233 191 LemonChiffon2 -205 201 165 LemonChiffon3 -139 137 112 LemonChiffon4 -255 248 220 cornsilk1 -238 232 205 cornsilk2 -205 200 177 cornsilk3 -139 136 120 cornsilk4 -255 255 240 ivory1 -238 238 224 ivory2 -205 205 193 ivory3 -139 139 131 ivory4 -240 255 240 honeydew1 -224 238 224 honeydew2 -193 205 193 honeydew3 -131 139 131 honeydew4 -255 240 245 LavenderBlush1 -238 224 229 LavenderBlush2 -205 193 197 LavenderBlush3 -139 131 134 LavenderBlush4 -255 228 225 MistyRose1 -238 213 210 MistyRose2 -205 183 181 MistyRose3 -139 125 123 MistyRose4 -240 255 255 azure1 -224 238 238 azure2 -193 205 205 azure3 -131 139 139 azure4 -131 111 255 SlateBlue1 -122 103 238 SlateBlue2 -105 89 205 SlateBlue3 - 71 60 139 SlateBlue4 - 72 118 255 RoyalBlue1 - 67 110 238 RoyalBlue2 - 58 95 205 RoyalBlue3 - 39 64 139 RoyalBlue4 - 0 0 255 blue1 - 0 0 238 blue2 - 0 0 205 blue3 - 0 0 139 blue4 - 30 144 255 DodgerBlue1 - 28 134 238 DodgerBlue2 - 24 116 205 DodgerBlue3 - 16 78 139 DodgerBlue4 - 99 184 255 SteelBlue1 - 92 172 238 SteelBlue2 - 79 148 205 SteelBlue3 - 54 100 139 SteelBlue4 - 0 191 255 DeepSkyBlue1 - 0 178 238 DeepSkyBlue2 - 0 154 205 DeepSkyBlue3 - 0 104 139 DeepSkyBlue4 -135 206 255 SkyBlue1 -126 192 238 SkyBlue2 -108 166 205 SkyBlue3 - 74 112 139 SkyBlue4 -176 226 255 LightSkyBlue1 -164 211 238 LightSkyBlue2 -141 182 205 LightSkyBlue3 - 96 123 139 LightSkyBlue4 -198 226 255 SlateGray1 -185 211 238 SlateGray2 -159 182 205 SlateGray3 -108 123 139 SlateGray4 -202 225 255 LightSteelBlue1 -188 210 238 LightSteelBlue2 -162 181 205 LightSteelBlue3 -110 123 139 LightSteelBlue4 -191 239 255 LightBlue1 -178 223 238 LightBlue2 -154 192 205 LightBlue3 -104 131 139 LightBlue4 -224 255 255 LightCyan1 -209 238 238 LightCyan2 -180 205 205 LightCyan3 -122 139 139 LightCyan4 -187 255 255 PaleTurquoise1 -174 238 238 PaleTurquoise2 -150 205 205 PaleTurquoise3 -102 139 139 PaleTurquoise4 -152 245 255 CadetBlue1 -142 229 238 CadetBlue2 -122 197 205 CadetBlue3 - 83 134 139 CadetBlue4 - 0 245 255 turquoise1 - 0 229 238 turquoise2 - 0 197 205 turquoise3 - 0 134 139 turquoise4 - 0 255 255 cyan1 - 0 238 238 cyan2 - 0 205 205 cyan3 - 0 139 139 cyan4 -151 255 255 DarkSlateGray1 -141 238 238 DarkSlateGray2 -121 205 205 DarkSlateGray3 - 82 139 139 DarkSlateGray4 -127 255 212 aquamarine1 -118 238 198 aquamarine2 -102 205 170 aquamarine3 - 69 139 116 aquamarine4 -193 255 193 DarkSeaGreen1 -180 238 180 DarkSeaGreen2 -155 205 155 DarkSeaGreen3 -105 139 105 DarkSeaGreen4 - 84 255 159 SeaGreen1 - 78 238 148 SeaGreen2 - 67 205 128 SeaGreen3 - 46 139 87 SeaGreen4 -154 255 154 PaleGreen1 -144 238 144 PaleGreen2 -124 205 124 PaleGreen3 - 84 139 84 PaleGreen4 - 0 255 127 SpringGreen1 - 0 238 118 SpringGreen2 - 0 205 102 SpringGreen3 - 0 139 69 SpringGreen4 - 0 255 0 green1 - 0 238 0 green2 - 0 205 0 green3 - 0 139 0 green4 -127 255 0 chartreuse1 -118 238 0 chartreuse2 -102 205 0 chartreuse3 - 69 139 0 chartreuse4 -192 255 62 OliveDrab1 -179 238 58 OliveDrab2 -154 205 50 OliveDrab3 -105 139 34 OliveDrab4 -202 255 112 DarkOliveGreen1 -188 238 104 DarkOliveGreen2 -162 205 90 DarkOliveGreen3 -110 139 61 DarkOliveGreen4 -255 246 143 khaki1 -238 230 133 khaki2 -205 198 115 khaki3 -139 134 78 khaki4 -255 236 139 LightGoldenrod1 -238 220 130 LightGoldenrod2 -205 190 112 LightGoldenrod3 -139 129 76 LightGoldenrod4 -255 255 224 LightYellow1 -238 238 209 LightYellow2 -205 205 180 LightYellow3 -139 139 122 LightYellow4 -255 255 0 yellow1 -238 238 0 yellow2 -205 205 0 yellow3 -139 139 0 yellow4 -255 215 0 gold1 -238 201 0 gold2 -205 173 0 gold3 -139 117 0 gold4 -255 193 37 goldenrod1 -238 180 34 goldenrod2 -205 155 29 goldenrod3 -139 105 20 goldenrod4 -255 185 15 DarkGoldenrod1 -238 173 14 DarkGoldenrod2 -205 149 12 DarkGoldenrod3 -139 101 8 DarkGoldenrod4 -255 193 193 RosyBrown1 -238 180 180 RosyBrown2 -205 155 155 RosyBrown3 -139 105 105 RosyBrown4 -255 106 106 IndianRed1 -238 99 99 IndianRed2 -205 85 85 IndianRed3 -139 58 58 IndianRed4 -255 130 71 sienna1 -238 121 66 sienna2 -205 104 57 sienna3 -139 71 38 sienna4 -255 211 155 burlywood1 -238 197 145 burlywood2 -205 170 125 burlywood3 -139 115 85 burlywood4 -255 231 186 wheat1 -238 216 174 wheat2 -205 186 150 wheat3 -139 126 102 wheat4 -255 165 79 tan1 -238 154 73 tan2 -205 133 63 tan3 -139 90 43 tan4 -255 127 36 chocolate1 -238 118 33 chocolate2 -205 102 29 chocolate3 -139 69 19 chocolate4 -255 48 48 firebrick1 -238 44 44 firebrick2 -205 38 38 firebrick3 -139 26 26 firebrick4 -255 64 64 brown1 -238 59 59 brown2 -205 51 51 brown3 -139 35 35 brown4 -255 140 105 salmon1 -238 130 98 salmon2 -205 112 84 salmon3 -139 76 57 salmon4 -255 160 122 LightSalmon1 -238 149 114 LightSalmon2 -205 129 98 LightSalmon3 -139 87 66 LightSalmon4 -255 165 0 orange1 -238 154 0 orange2 -205 133 0 orange3 -139 90 0 orange4 -255 127 0 DarkOrange1 -238 118 0 DarkOrange2 -205 102 0 DarkOrange3 -139 69 0 DarkOrange4 -255 114 86 coral1 -238 106 80 coral2 -205 91 69 coral3 -139 62 47 coral4 -255 99 71 tomato1 -238 92 66 tomato2 -205 79 57 tomato3 -139 54 38 tomato4 -255 69 0 OrangeRed1 -238 64 0 OrangeRed2 -205 55 0 OrangeRed3 -139 37 0 OrangeRed4 -255 0 0 red1 -238 0 0 red2 -205 0 0 red3 -139 0 0 red4 -255 20 147 DeepPink1 -238 18 137 DeepPink2 -205 16 118 DeepPink3 -139 10 80 DeepPink4 -255 110 180 HotPink1 -238 106 167 HotPink2 -205 96 144 HotPink3 -139 58 98 HotPink4 -255 181 197 pink1 -238 169 184 pink2 -205 145 158 pink3 -139 99 108 pink4 -255 174 185 LightPink1 -238 162 173 LightPink2 -205 140 149 LightPink3 -139 95 101 LightPink4 -255 130 171 PaleVioletRed1 -238 121 159 PaleVioletRed2 -205 104 137 PaleVioletRed3 -139 71 93 PaleVioletRed4 -255 52 179 maroon1 -238 48 167 maroon2 -205 41 144 maroon3 -139 28 98 maroon4 -255 62 150 VioletRed1 -238 58 140 VioletRed2 -205 50 120 VioletRed3 -139 34 82 VioletRed4 -255 0 255 magenta1 -238 0 238 magenta2 -205 0 205 magenta3 -139 0 139 magenta4 -255 131 250 orchid1 -238 122 233 orchid2 -205 105 201 orchid3 -139 71 137 orchid4 -255 187 255 plum1 -238 174 238 plum2 -205 150 205 plum3 -139 102 139 plum4 -224 102 255 MediumOrchid1 -209 95 238 MediumOrchid2 -180 82 205 MediumOrchid3 -122 55 139 MediumOrchid4 -191 62 255 DarkOrchid1 -178 58 238 DarkOrchid2 -154 50 205 DarkOrchid3 -104 34 139 DarkOrchid4 -155 48 255 purple1 -145 44 238 purple2 -125 38 205 purple3 - 85 26 139 purple4 -171 130 255 MediumPurple1 -159 121 238 MediumPurple2 -137 104 205 MediumPurple3 - 93 71 139 MediumPurple4 -255 225 255 thistle1 -238 210 238 thistle2 -205 181 205 thistle3 -139 123 139 thistle4 - 0 0 0 gray0 - 0 0 0 grey0 - 3 3 3 gray1 - 3 3 3 grey1 - 5 5 5 gray2 - 5 5 5 grey2 - 8 8 8 gray3 - 8 8 8 grey3 - 10 10 10 gray4 - 10 10 10 grey4 - 13 13 13 gray5 - 13 13 13 grey5 - 15 15 15 gray6 - 15 15 15 grey6 - 18 18 18 gray7 - 18 18 18 grey7 - 20 20 20 gray8 - 20 20 20 grey8 - 23 23 23 gray9 - 23 23 23 grey9 - 26 26 26 gray10 - 26 26 26 grey10 - 28 28 28 gray11 - 28 28 28 grey11 - 31 31 31 gray12 - 31 31 31 grey12 - 33 33 33 gray13 - 33 33 33 grey13 - 36 36 36 gray14 - 36 36 36 grey14 - 38 38 38 gray15 - 38 38 38 grey15 - 41 41 41 gray16 - 41 41 41 grey16 - 43 43 43 gray17 - 43 43 43 grey17 - 46 46 46 gray18 - 46 46 46 grey18 - 48 48 48 gray19 - 48 48 48 grey19 - 51 51 51 gray20 - 51 51 51 grey20 - 54 54 54 gray21 - 54 54 54 grey21 - 56 56 56 gray22 - 56 56 56 grey22 - 59 59 59 gray23 - 59 59 59 grey23 - 61 61 61 gray24 - 61 61 61 grey24 - 64 64 64 gray25 - 64 64 64 grey25 - 66 66 66 gray26 - 66 66 66 grey26 - 69 69 69 gray27 - 69 69 69 grey27 - 71 71 71 gray28 - 71 71 71 grey28 - 74 74 74 gray29 - 74 74 74 grey29 - 77 77 77 gray30 - 77 77 77 grey30 - 79 79 79 gray31 - 79 79 79 grey31 - 82 82 82 gray32 - 82 82 82 grey32 - 84 84 84 gray33 - 84 84 84 grey33 - 87 87 87 gray34 - 87 87 87 grey34 - 89 89 89 gray35 - 89 89 89 grey35 - 92 92 92 gray36 - 92 92 92 grey36 - 94 94 94 gray37 - 94 94 94 grey37 - 97 97 97 gray38 - 97 97 97 grey38 - 99 99 99 gray39 - 99 99 99 grey39 -102 102 102 gray40 -102 102 102 grey40 -105 105 105 gray41 -105 105 105 grey41 -107 107 107 gray42 -107 107 107 grey42 -110 110 110 gray43 -110 110 110 grey43 -112 112 112 gray44 -112 112 112 grey44 -115 115 115 gray45 -115 115 115 grey45 -117 117 117 gray46 -117 117 117 grey46 -120 120 120 gray47 -120 120 120 grey47 -122 122 122 gray48 -122 122 122 grey48 -125 125 125 gray49 -125 125 125 grey49 -127 127 127 gray50 -127 127 127 grey50 -130 130 130 gray51 -130 130 130 grey51 -133 133 133 gray52 -133 133 133 grey52 -135 135 135 gray53 -135 135 135 grey53 -138 138 138 gray54 -138 138 138 grey54 -140 140 140 gray55 -140 140 140 grey55 -143 143 143 gray56 -143 143 143 grey56 -145 145 145 gray57 -145 145 145 grey57 -148 148 148 gray58 -148 148 148 grey58 -150 150 150 gray59 -150 150 150 grey59 -153 153 153 gray60 -153 153 153 grey60 -156 156 156 gray61 -156 156 156 grey61 -158 158 158 gray62 -158 158 158 grey62 -161 161 161 gray63 -161 161 161 grey63 -163 163 163 gray64 -163 163 163 grey64 -166 166 166 gray65 -166 166 166 grey65 -168 168 168 gray66 -168 168 168 grey66 -171 171 171 gray67 -171 171 171 grey67 -173 173 173 gray68 -173 173 173 grey68 -176 176 176 gray69 -176 176 176 grey69 -179 179 179 gray70 -179 179 179 grey70 -181 181 181 gray71 -181 181 181 grey71 -184 184 184 gray72 -184 184 184 grey72 -186 186 186 gray73 -186 186 186 grey73 -189 189 189 gray74 -189 189 189 grey74 -191 191 191 gray75 -191 191 191 grey75 -194 194 194 gray76 -194 194 194 grey76 -196 196 196 gray77 -196 196 196 grey77 -199 199 199 gray78 -199 199 199 grey78 -201 201 201 gray79 -201 201 201 grey79 -204 204 204 gray80 -204 204 204 grey80 -207 207 207 gray81 -207 207 207 grey81 -209 209 209 gray82 -209 209 209 grey82 -212 212 212 gray83 -212 212 212 grey83 -214 214 214 gray84 -214 214 214 grey84 -217 217 217 gray85 -217 217 217 grey85 -219 219 219 gray86 -219 219 219 grey86 -222 222 222 gray87 -222 222 222 grey87 -224 224 224 gray88 -224 224 224 grey88 -227 227 227 gray89 -227 227 227 grey89 -229 229 229 gray90 -229 229 229 grey90 -232 232 232 gray91 -232 232 232 grey91 -235 235 235 gray92 -235 235 235 grey92 -237 237 237 gray93 -237 237 237 grey93 -240 240 240 gray94 -240 240 240 grey94 -242 242 242 gray95 -242 242 242 grey95 -245 245 245 gray96 -245 245 245 grey96 -247 247 247 gray97 -247 247 247 grey97 -250 250 250 gray98 -250 250 250 grey98 -252 252 252 gray99 -252 252 252 grey99 -255 255 255 gray100 -255 255 255 grey100 -169 169 169 dark grey -169 169 169 DarkGrey -169 169 169 dark gray -169 169 169 DarkGray - 0 0 139 dark blue - 0 0 139 DarkBlue - 0 139 139 dark cyan - 0 139 139 DarkCyan -139 0 139 dark magenta -139 0 139 DarkMagenta -139 0 0 dark red -139 0 0 DarkRed -144 238 144 light green -144 238 144 LightGreen -220 20 60 crimson - 75 0 130 indigo -128 128 0 olive -102 51 153 rebecca purple -102 51 153 RebeccaPurple -192 192 192 silver - 0 128 128 teal diff --git a/src/terminal-old/sanitize.zig b/src/terminal-old/sanitize.zig deleted file mode 100644 index f492291aa..000000000 --- a/src/terminal-old/sanitize.zig +++ /dev/null @@ -1,13 +0,0 @@ -const std = @import("std"); - -/// Returns true if the data looks safe to paste. -pub fn isSafePaste(data: []const u8) bool { - return std.mem.indexOf(u8, data, "\n") == null; -} - -test isSafePaste { - const testing = std.testing; - try testing.expect(isSafePaste("hello")); - try testing.expect(!isSafePaste("hello\n")); - try testing.expect(!isSafePaste("hello\nworld")); -} diff --git a/src/terminal-old/sgr.zig b/src/terminal-old/sgr.zig deleted file mode 100644 index b23bd1514..000000000 --- a/src/terminal-old/sgr.zig +++ /dev/null @@ -1,559 +0,0 @@ -//! SGR (Select Graphic Rendition) attrinvbute parsing and types. - -const std = @import("std"); -const testing = std.testing; -const color = @import("color.zig"); - -/// Attribute type for SGR -pub const Attribute = union(enum) { - /// Unset all attributes - unset: void, - - /// Unknown attribute, the raw CSI command parameters are here. - unknown: struct { - /// Full is the full SGR input. - full: []const u16, - - /// Partial is the remaining, where we got hung up. - partial: []const u16, - }, - - /// Bold the text. - bold: void, - reset_bold: void, - - /// Italic text. - italic: void, - reset_italic: void, - - /// Faint/dim text. - /// Note: reset faint is the same SGR code as reset bold - faint: void, - - /// Underline the text - underline: Underline, - reset_underline: void, - underline_color: color.RGB, - @"256_underline_color": u8, - reset_underline_color: void, - - /// Blink the text - blink: void, - reset_blink: void, - - /// Invert fg/bg colors. - inverse: void, - reset_inverse: void, - - /// Invisible - invisible: void, - reset_invisible: void, - - /// Strikethrough the text. - strikethrough: void, - reset_strikethrough: void, - - /// Set foreground color as RGB values. - direct_color_fg: color.RGB, - - /// Set background color as RGB values. - direct_color_bg: color.RGB, - - /// Set the background/foreground as a named color attribute. - @"8_bg": color.Name, - @"8_fg": color.Name, - - /// Reset the fg/bg to their default values. - reset_fg: void, - reset_bg: void, - - /// Set the background/foreground as a named bright color attribute. - @"8_bright_bg": color.Name, - @"8_bright_fg": color.Name, - - /// Set background color as 256-color palette. - @"256_bg": u8, - - /// Set foreground color as 256-color palette. - @"256_fg": u8, - - pub const Underline = enum(u3) { - none = 0, - single = 1, - double = 2, - curly = 3, - dotted = 4, - dashed = 5, - }; -}; - -/// Parser parses the attributes from a list of SGR parameters. -pub const Parser = struct { - params: []const u16, - idx: usize = 0, - - /// True if the separator is a colon - colon: bool = false, - - /// Next returns the next attribute or null if there are no more attributes. - pub fn next(self: *Parser) ?Attribute { - if (self.idx > self.params.len) return null; - - // Implicitly means unset - if (self.params.len == 0) { - self.idx += 1; - return Attribute{ .unset = {} }; - } - - const slice = self.params[self.idx..self.params.len]; - self.idx += 1; - - // Our last one will have an idx be the last value. - if (slice.len == 0) return null; - - switch (slice[0]) { - 0 => return Attribute{ .unset = {} }, - - 1 => return Attribute{ .bold = {} }, - - 2 => return Attribute{ .faint = {} }, - - 3 => return Attribute{ .italic = {} }, - - 4 => blk: { - if (self.colon) { - switch (slice.len) { - // 0 is unreachable because we're here and we read - // an element to get here. - 0 => unreachable, - - // 1 is possible if underline is the last element. - 1 => return Attribute{ .underline = .single }, - - // 2 means we have a specific underline style. - 2 => { - self.idx += 1; - switch (slice[1]) { - 0 => return Attribute{ .reset_underline = {} }, - 1 => return Attribute{ .underline = .single }, - 2 => return Attribute{ .underline = .double }, - 3 => return Attribute{ .underline = .curly }, - 4 => return Attribute{ .underline = .dotted }, - 5 => return Attribute{ .underline = .dashed }, - - // For unknown underline styles, just render - // a single underline. - else => return Attribute{ .underline = .single }, - } - }, - - // Colon-separated must only be 2. - else => break :blk, - } - } - - return Attribute{ .underline = .single }; - }, - - 5 => return Attribute{ .blink = {} }, - - 6 => return Attribute{ .blink = {} }, - - 7 => return Attribute{ .inverse = {} }, - - 8 => return Attribute{ .invisible = {} }, - - 9 => return Attribute{ .strikethrough = {} }, - - 22 => return Attribute{ .reset_bold = {} }, - - 23 => return Attribute{ .reset_italic = {} }, - - 24 => return Attribute{ .reset_underline = {} }, - - 25 => return Attribute{ .reset_blink = {} }, - - 27 => return Attribute{ .reset_inverse = {} }, - - 28 => return Attribute{ .reset_invisible = {} }, - - 29 => return Attribute{ .reset_strikethrough = {} }, - - 30...37 => return Attribute{ - .@"8_fg" = @enumFromInt(slice[0] - 30), - }, - - 38 => if (slice.len >= 5 and slice[1] == 2) { - self.idx += 4; - - // In the 6-len form, ignore the 3rd param. - const rgb = slice[2..5]; - - // We use @truncate because the value should be 0 to 255. If - // it isn't, the behavior is undefined so we just... truncate it. - return Attribute{ - .direct_color_fg = .{ - .r = @truncate(rgb[0]), - .g = @truncate(rgb[1]), - .b = @truncate(rgb[2]), - }, - }; - } else if (slice.len >= 3 and slice[1] == 5) { - self.idx += 2; - return Attribute{ - .@"256_fg" = @truncate(slice[2]), - }; - }, - - 39 => return Attribute{ .reset_fg = {} }, - - 40...47 => return Attribute{ - .@"8_bg" = @enumFromInt(slice[0] - 40), - }, - - 48 => if (slice.len >= 5 and slice[1] == 2) { - self.idx += 4; - - // We only support the 5-len form. - const rgb = slice[2..5]; - - // We use @truncate because the value should be 0 to 255. If - // it isn't, the behavior is undefined so we just... truncate it. - return Attribute{ - .direct_color_bg = .{ - .r = @truncate(rgb[0]), - .g = @truncate(rgb[1]), - .b = @truncate(rgb[2]), - }, - }; - } else if (slice.len >= 3 and slice[1] == 5) { - self.idx += 2; - return Attribute{ - .@"256_bg" = @truncate(slice[2]), - }; - }, - - 49 => return Attribute{ .reset_bg = {} }, - - 58 => if (slice.len >= 5 and slice[1] == 2) { - self.idx += 4; - - // In the 6-len form, ignore the 3rd param. Otherwise, use it. - const rgb = if (slice.len == 5) slice[2..5] else rgb: { - // Consume one more element - self.idx += 1; - break :rgb slice[3..6]; - }; - - // We use @truncate because the value should be 0 to 255. If - // it isn't, the behavior is undefined so we just... truncate it. - return Attribute{ - .underline_color = .{ - .r = @truncate(rgb[0]), - .g = @truncate(rgb[1]), - .b = @truncate(rgb[2]), - }, - }; - } else if (slice.len >= 3 and slice[1] == 5) { - self.idx += 2; - return Attribute{ - .@"256_underline_color" = @truncate(slice[2]), - }; - }, - - 59 => return Attribute{ .reset_underline_color = {} }, - - 90...97 => return Attribute{ - // 82 instead of 90 to offset to "bright" colors - .@"8_bright_fg" = @enumFromInt(slice[0] - 82), - }, - - 100...107 => return Attribute{ - .@"8_bright_bg" = @enumFromInt(slice[0] - 92), - }, - - else => {}, - } - - return Attribute{ .unknown = .{ .full = self.params, .partial = slice } }; - } -}; - -fn testParse(params: []const u16) Attribute { - var p: Parser = .{ .params = params }; - return p.next().?; -} - -fn testParseColon(params: []const u16) Attribute { - var p: Parser = .{ .params = params, .colon = true }; - return p.next().?; -} - -test "sgr: Parser" { - try testing.expect(testParse(&[_]u16{}) == .unset); - try testing.expect(testParse(&[_]u16{0}) == .unset); - - { - const v = testParse(&[_]u16{ 38, 2, 40, 44, 52 }); - try testing.expect(v == .direct_color_fg); - try testing.expectEqual(@as(u8, 40), v.direct_color_fg.r); - try testing.expectEqual(@as(u8, 44), v.direct_color_fg.g); - try testing.expectEqual(@as(u8, 52), v.direct_color_fg.b); - } - - try testing.expect(testParse(&[_]u16{ 38, 2, 44, 52 }) == .unknown); - - { - const v = testParse(&[_]u16{ 48, 2, 40, 44, 52 }); - try testing.expect(v == .direct_color_bg); - try testing.expectEqual(@as(u8, 40), v.direct_color_bg.r); - try testing.expectEqual(@as(u8, 44), v.direct_color_bg.g); - try testing.expectEqual(@as(u8, 52), v.direct_color_bg.b); - } - - try testing.expect(testParse(&[_]u16{ 48, 2, 44, 52 }) == .unknown); -} - -test "sgr: Parser multiple" { - var p: Parser = .{ .params = &[_]u16{ 0, 38, 2, 40, 44, 52 } }; - try testing.expect(p.next().? == .unset); - try testing.expect(p.next().? == .direct_color_fg); - try testing.expect(p.next() == null); - try testing.expect(p.next() == null); -} - -test "sgr: bold" { - { - const v = testParse(&[_]u16{1}); - try testing.expect(v == .bold); - } - - { - const v = testParse(&[_]u16{22}); - try testing.expect(v == .reset_bold); - } -} - -test "sgr: italic" { - { - const v = testParse(&[_]u16{3}); - try testing.expect(v == .italic); - } - - { - const v = testParse(&[_]u16{23}); - try testing.expect(v == .reset_italic); - } -} - -test "sgr: underline" { - { - const v = testParse(&[_]u16{4}); - try testing.expect(v == .underline); - } - - { - const v = testParse(&[_]u16{24}); - try testing.expect(v == .reset_underline); - } -} - -test "sgr: underline styles" { - { - const v = testParseColon(&[_]u16{ 4, 2 }); - try testing.expect(v == .underline); - try testing.expect(v.underline == .double); - } - - { - const v = testParseColon(&[_]u16{ 4, 0 }); - try testing.expect(v == .reset_underline); - } - - { - const v = testParseColon(&[_]u16{ 4, 1 }); - try testing.expect(v == .underline); - try testing.expect(v.underline == .single); - } - - { - const v = testParseColon(&[_]u16{ 4, 3 }); - try testing.expect(v == .underline); - try testing.expect(v.underline == .curly); - } - - { - const v = testParseColon(&[_]u16{ 4, 4 }); - try testing.expect(v == .underline); - try testing.expect(v.underline == .dotted); - } - - { - const v = testParseColon(&[_]u16{ 4, 5 }); - try testing.expect(v == .underline); - try testing.expect(v.underline == .dashed); - } -} - -test "sgr: blink" { - { - const v = testParse(&[_]u16{5}); - try testing.expect(v == .blink); - } - - { - const v = testParse(&[_]u16{6}); - try testing.expect(v == .blink); - } - - { - const v = testParse(&[_]u16{25}); - try testing.expect(v == .reset_blink); - } -} - -test "sgr: inverse" { - { - const v = testParse(&[_]u16{7}); - try testing.expect(v == .inverse); - } - - { - const v = testParse(&[_]u16{27}); - try testing.expect(v == .reset_inverse); - } -} - -test "sgr: strikethrough" { - { - const v = testParse(&[_]u16{9}); - try testing.expect(v == .strikethrough); - } - - { - const v = testParse(&[_]u16{29}); - try testing.expect(v == .reset_strikethrough); - } -} - -test "sgr: 8 color" { - var p: Parser = .{ .params = &[_]u16{ 31, 43, 90, 103 } }; - - { - const v = p.next().?; - try testing.expect(v == .@"8_fg"); - try testing.expect(v.@"8_fg" == .red); - } - - { - const v = p.next().?; - try testing.expect(v == .@"8_bg"); - try testing.expect(v.@"8_bg" == .yellow); - } - - { - const v = p.next().?; - try testing.expect(v == .@"8_bright_fg"); - try testing.expect(v.@"8_bright_fg" == .bright_black); - } - - { - const v = p.next().?; - try testing.expect(v == .@"8_bright_bg"); - try testing.expect(v.@"8_bright_bg" == .bright_yellow); - } -} - -test "sgr: 256 color" { - var p: Parser = .{ .params = &[_]u16{ 38, 5, 161, 48, 5, 236 } }; - try testing.expect(p.next().? == .@"256_fg"); - try testing.expect(p.next().? == .@"256_bg"); - try testing.expect(p.next() == null); -} - -test "sgr: 256 color underline" { - var p: Parser = .{ .params = &[_]u16{ 58, 5, 9 } }; - try testing.expect(p.next().? == .@"256_underline_color"); - try testing.expect(p.next() == null); -} - -test "sgr: 24-bit bg color" { - { - const v = testParseColon(&[_]u16{ 48, 2, 1, 2, 3 }); - try testing.expect(v == .direct_color_bg); - try testing.expectEqual(@as(u8, 1), v.direct_color_bg.r); - try testing.expectEqual(@as(u8, 2), v.direct_color_bg.g); - try testing.expectEqual(@as(u8, 3), v.direct_color_bg.b); - } -} - -test "sgr: underline color" { - { - const v = testParseColon(&[_]u16{ 58, 2, 1, 2, 3 }); - try testing.expect(v == .underline_color); - try testing.expectEqual(@as(u8, 1), v.underline_color.r); - try testing.expectEqual(@as(u8, 2), v.underline_color.g); - try testing.expectEqual(@as(u8, 3), v.underline_color.b); - } - - { - const v = testParseColon(&[_]u16{ 58, 2, 0, 1, 2, 3 }); - try testing.expect(v == .underline_color); - try testing.expectEqual(@as(u8, 1), v.underline_color.r); - try testing.expectEqual(@as(u8, 2), v.underline_color.g); - try testing.expectEqual(@as(u8, 3), v.underline_color.b); - } -} - -test "sgr: reset underline color" { - var p: Parser = .{ .params = &[_]u16{59} }; - try testing.expect(p.next().? == .reset_underline_color); -} - -test "sgr: invisible" { - var p: Parser = .{ .params = &[_]u16{ 8, 28 } }; - try testing.expect(p.next().? == .invisible); - try testing.expect(p.next().? == .reset_invisible); -} - -test "sgr: underline, bg, and fg" { - var p: Parser = .{ - .params = &[_]u16{ 4, 38, 2, 255, 247, 219, 48, 2, 242, 93, 147, 4 }, - }; - { - const v = p.next().?; - try testing.expect(v == .underline); - try testing.expectEqual(Attribute.Underline.single, v.underline); - } - { - const v = p.next().?; - try testing.expect(v == .direct_color_fg); - try testing.expectEqual(@as(u8, 255), v.direct_color_fg.r); - try testing.expectEqual(@as(u8, 247), v.direct_color_fg.g); - try testing.expectEqual(@as(u8, 219), v.direct_color_fg.b); - } - { - const v = p.next().?; - try testing.expect(v == .direct_color_bg); - try testing.expectEqual(@as(u8, 242), v.direct_color_bg.r); - try testing.expectEqual(@as(u8, 93), v.direct_color_bg.g); - try testing.expectEqual(@as(u8, 147), v.direct_color_bg.b); - } - { - const v = p.next().?; - try testing.expect(v == .underline); - try testing.expectEqual(Attribute.Underline.single, v.underline); - } -} - -test "sgr: direct color fg missing color" { - // This used to crash - var p: Parser = .{ .params = &[_]u16{ 38, 5 }, .colon = false }; - while (p.next()) |_| {} -} - -test "sgr: direct color bg missing color" { - // This used to crash - var p: Parser = .{ .params = &[_]u16{ 48, 5 }, .colon = false }; - while (p.next()) |_| {} -} diff --git a/src/terminal-old/simdvt.zig b/src/terminal-old/simdvt.zig deleted file mode 100644 index be5e4fcb7..000000000 --- a/src/terminal-old/simdvt.zig +++ /dev/null @@ -1,5 +0,0 @@ -pub usingnamespace @import("simdvt/parser.zig"); - -test { - @import("std").testing.refAllDecls(@This()); -} diff --git a/src/terminal-old/stream.zig b/src/terminal-old/stream.zig deleted file mode 100644 index fc97d3685..000000000 --- a/src/terminal-old/stream.zig +++ /dev/null @@ -1,2014 +0,0 @@ -const std = @import("std"); -const assert = std.debug.assert; -const testing = std.testing; -const simd = @import("../simd/main.zig"); -const Parser = @import("Parser.zig"); -const ansi = @import("ansi.zig"); -const charsets = @import("charsets.zig"); -const device_status = @import("device_status.zig"); -const csi = @import("csi.zig"); -const kitty = @import("kitty.zig"); -const modes = @import("modes.zig"); -const osc = @import("osc.zig"); -const sgr = @import("sgr.zig"); -const UTF8Decoder = @import("UTF8Decoder.zig"); -const MouseShape = @import("mouse_shape.zig").MouseShape; - -const log = std.log.scoped(.stream); - -/// Returns a type that can process a stream of tty control characters. -/// This will call various callback functions on type T. Type T only has to -/// implement the callbacks it cares about; any unimplemented callbacks will -/// logged at runtime. -/// -/// To figure out what callbacks exist, search the source for "hasDecl". This -/// isn't ideal but for now that's the best approach. -/// -/// This is implemented this way because we purposely do NOT want dynamic -/// dispatch for performance reasons. The way this is implemented forces -/// comptime resolution for all function calls. -pub fn Stream(comptime Handler: type) type { - return struct { - const Self = @This(); - - // We use T with @hasDecl so it needs to be a struct. Unwrap the - // pointer if we were given one. - const T = switch (@typeInfo(Handler)) { - .Pointer => |p| p.child, - else => Handler, - }; - - handler: Handler, - parser: Parser = .{}, - utf8decoder: UTF8Decoder = .{}, - - pub fn deinit(self: *Self) void { - self.parser.deinit(); - } - - /// Process a string of characters. - pub fn nextSlice(self: *Self, input: []const u8) !void { - // This is the maximum number of codepoints we can decode - // at one time for this function call. This is somewhat arbitrary - // so if someone can demonstrate a better number then we can switch. - var cp_buf: [4096]u32 = undefined; - - // Split the input into chunks that fit into cp_buf. - var i: usize = 0; - while (true) { - const len = @min(cp_buf.len, input.len - i); - try self.nextSliceCapped(input[i .. i + len], &cp_buf); - i += len; - if (i >= input.len) break; - } - } - - fn nextSliceCapped(self: *Self, input: []const u8, cp_buf: []u32) !void { - assert(input.len <= cp_buf.len); - - var offset: usize = 0; - - // If the scalar UTF-8 decoder was in the middle of processing - // a code sequence, we continue until it's not. - while (self.utf8decoder.state != 0) { - if (offset >= input.len) return; - try self.nextUtf8(input[offset]); - offset += 1; - } - if (offset >= input.len) return; - - // If we're not in the ground state then we process until - // we are. This can happen if the last chunk of input put us - // in the middle of a control sequence. - offset += try self.consumeUntilGround(input[offset..]); - if (offset >= input.len) return; - offset += try self.consumeAllEscapes(input[offset..]); - - // If we're in the ground state then we can use SIMD to process - // input until we see an ESC (0x1B), since all other characters - // up to that point are just UTF-8. - while (self.parser.state == .ground and offset < input.len) { - const res = simd.vt.utf8DecodeUntilControlSeq(input[offset..], cp_buf); - for (cp_buf[0..res.decoded]) |cp| { - if (cp <= 0xF) { - try self.execute(@intCast(cp)); - } else { - try self.print(@intCast(cp)); - } - } - // Consume the bytes we just processed. - offset += res.consumed; - - if (offset >= input.len) return; - - // If our offset is NOT an escape then we must have a - // partial UTF-8 sequence. In that case, we pass it off - // to the scalar parser. - if (input[offset] != 0x1B) { - const rem = input[offset..]; - for (rem) |c| try self.nextUtf8(c); - return; - } - - // Process control sequences until we run out. - offset += try self.consumeAllEscapes(input[offset..]); - } - } - - /// Parses back-to-back escape sequences until none are left. - /// Returns the number of bytes consumed from the provided input. - /// - /// Expects input to start with 0x1B, use consumeUntilGround first - /// if the stream may be in the middle of an escape sequence. - fn consumeAllEscapes(self: *Self, input: []const u8) !usize { - var offset: usize = 0; - while (input[offset] == 0x1B) { - self.parser.state = .escape; - self.parser.clear(); - offset += 1; - offset += try self.consumeUntilGround(input[offset..]); - if (offset >= input.len) return input.len; - } - return offset; - } - - /// Parses escape sequences until the parser reaches the ground state. - /// Returns the number of bytes consumed from the provided input. - fn consumeUntilGround(self: *Self, input: []const u8) !usize { - var offset: usize = 0; - while (self.parser.state != .ground) { - if (offset >= input.len) return input.len; - try self.nextNonUtf8(input[offset]); - offset += 1; - } - return offset; - } - - /// Like nextSlice but takes one byte and is necessarilly a scalar - /// operation that can't use SIMD. Prefer nextSlice if you can and - /// try to get multiple bytes at once. - pub fn next(self: *Self, c: u8) !void { - // The scalar path can be responsible for decoding UTF-8. - if (self.parser.state == .ground and c != 0x1B) { - try self.nextUtf8(c); - return; - } - - try self.nextNonUtf8(c); - } - - /// Process the next byte and print as necessary. - /// - /// This assumes we're in the UTF-8 decoding state. If we may not - /// be in the UTF-8 decoding state call nextSlice or next. - fn nextUtf8(self: *Self, c: u8) !void { - assert(self.parser.state == .ground and c != 0x1B); - - const res = self.utf8decoder.next(c); - const consumed = res[1]; - if (res[0]) |codepoint| { - if (codepoint <= 0xF) { - try self.execute(@intCast(codepoint)); - } else { - try self.print(@intCast(codepoint)); - } - } - if (!consumed) { - const retry = self.utf8decoder.next(c); - // It should be impossible for the decoder - // to not consume the byte twice in a row. - assert(retry[1] == true); - if (retry[0]) |codepoint| { - if (codepoint <= 0xF) { - try self.execute(@intCast(codepoint)); - } else { - try self.print(@intCast(codepoint)); - } - } - } - } - - /// Process the next character and call any callbacks if necessary. - /// - /// This assumes that we're not in the UTF-8 decoding state. If - /// we may be in the UTF-8 decoding state call nextSlice or next. - fn nextNonUtf8(self: *Self, c: u8) !void { - assert(self.parser.state != .ground or c == 0x1B); - - // Fast path for ESC - if (self.parser.state == .ground and c == 0x1B) { - self.parser.state = .escape; - self.parser.clear(); - return; - } - // Fast path for CSI entry. - if (self.parser.state == .escape and c == '[') { - self.parser.state = .csi_entry; - return; - } - // Fast path for CSI params. - if (self.parser.state == .csi_param) csi_param: { - switch (c) { - // A C0 escape (yes, this is valid): - 0x00...0x0F => try self.execute(c), - // We ignore C0 escapes > 0xF since execute - // doesn't have processing for them anyway: - 0x10...0x17, 0x19, 0x1C...0x1F => {}, - // We don't currently have any handling for - // 0x18 or 0x1A, but they should still move - // the parser state to ground. - 0x18, 0x1A => self.parser.state = .ground, - // A parameter digit: - '0'...'9' => if (self.parser.params_idx < 16) { - self.parser.param_acc *|= 10; - self.parser.param_acc +|= c - '0'; - // The parser's CSI param action uses param_acc_idx - // to decide if there's a final param that needs to - // be consumed or not, but it doesn't matter really - // what it is as long as it's not 0. - self.parser.param_acc_idx |= 1; - }, - // A parameter separator: - ':', ';' => if (self.parser.params_idx < 16) { - self.parser.params[self.parser.params_idx] = self.parser.param_acc; - self.parser.params_idx += 1; - - self.parser.param_acc = 0; - self.parser.param_acc_idx = 0; - - // Keep track of separator state. - const sep: Parser.ParamSepState = @enumFromInt(c); - if (self.parser.params_idx == 1) self.parser.params_sep = sep; - if (self.parser.params_sep != sep) self.parser.params_sep = .mixed; - }, - // Explicitly ignored: - 0x7F => {}, - // Defer to the state machine to - // handle any other characters: - else => break :csi_param, - } - return; - } - - const actions = self.parser.next(c); - for (actions) |action_opt| { - const action = action_opt orelse continue; - - // if (action != .print) { - // log.info("action: {}", .{action}); - // } - - // If this handler handles everything manually then we do nothing - // if it can be processed. - if (@hasDecl(T, "handleManually")) { - const processed = self.handler.handleManually(action) catch |err| err: { - log.warn("error handling action manually err={} action={}", .{ - err, - action, - }); - - break :err false; - }; - - if (processed) continue; - } - - switch (action) { - .print => |p| if (@hasDecl(T, "print")) try self.handler.print(p), - .execute => |code| try self.execute(code), - .csi_dispatch => |csi_action| try self.csiDispatch(csi_action), - .esc_dispatch => |esc| try self.escDispatch(esc), - .osc_dispatch => |cmd| try self.oscDispatch(cmd), - .dcs_hook => |dcs| if (@hasDecl(T, "dcsHook")) { - try self.handler.dcsHook(dcs); - } else log.warn("unimplemented DCS hook", .{}), - .dcs_put => |code| if (@hasDecl(T, "dcsPut")) { - try self.handler.dcsPut(code); - } else log.warn("unimplemented DCS put: {x}", .{code}), - .dcs_unhook => if (@hasDecl(T, "dcsUnhook")) { - try self.handler.dcsUnhook(); - } else log.warn("unimplemented DCS unhook", .{}), - .apc_start => if (@hasDecl(T, "apcStart")) { - try self.handler.apcStart(); - } else log.warn("unimplemented APC start", .{}), - .apc_put => |code| if (@hasDecl(T, "apcPut")) { - try self.handler.apcPut(code); - } else log.warn("unimplemented APC put: {x}", .{code}), - .apc_end => if (@hasDecl(T, "apcEnd")) { - try self.handler.apcEnd(); - } else log.warn("unimplemented APC end", .{}), - } - } - } - - pub fn print(self: *Self, c: u21) !void { - if (@hasDecl(T, "print")) { - try self.handler.print(c); - } - } - - pub fn execute(self: *Self, c: u8) !void { - switch (@as(ansi.C0, @enumFromInt(c))) { - // We ignore SOH/STX: https://github.com/microsoft/terminal/issues/10786 - .NUL, .SOH, .STX => {}, - - .ENQ => if (@hasDecl(T, "enquiry")) - try self.handler.enquiry() - else - log.warn("unimplemented execute: {x}", .{c}), - - .BEL => if (@hasDecl(T, "bell")) - try self.handler.bell() - else - log.warn("unimplemented execute: {x}", .{c}), - - .BS => if (@hasDecl(T, "backspace")) - try self.handler.backspace() - else - log.warn("unimplemented execute: {x}", .{c}), - - .HT => if (@hasDecl(T, "horizontalTab")) - try self.handler.horizontalTab(1) - else - log.warn("unimplemented execute: {x}", .{c}), - - .LF, .VT, .FF => if (@hasDecl(T, "linefeed")) - try self.handler.linefeed() - else - log.warn("unimplemented execute: {x}", .{c}), - - .CR => if (@hasDecl(T, "carriageReturn")) - try self.handler.carriageReturn() - else - log.warn("unimplemented execute: {x}", .{c}), - - .SO => if (@hasDecl(T, "invokeCharset")) - try self.handler.invokeCharset(.GL, .G1, false) - else - log.warn("unimplemented invokeCharset: {x}", .{c}), - - .SI => if (@hasDecl(T, "invokeCharset")) - try self.handler.invokeCharset(.GL, .G0, false) - else - log.warn("unimplemented invokeCharset: {x}", .{c}), - - else => log.warn("invalid C0 character, ignoring: 0x{x}", .{c}), - } - } - - fn csiDispatch(self: *Self, input: Parser.Action.CSI) !void { - // Handles aliases first - const action = switch (input.final) { - // Alias for set cursor position - 'f' => blk: { - var copy = input; - copy.final = 'H'; - break :blk copy; - }, - - else => input, - }; - - switch (action.final) { - // CUU - Cursor Up - 'A', 'k' => if (@hasDecl(T, "setCursorUp")) try self.handler.setCursorUp( - switch (action.params.len) { - 0 => 1, - 1 => action.params[0], - else => { - log.warn("invalid cursor up command: {}", .{action}); - return; - }, - }, - false, - ) else log.warn("unimplemented CSI callback: {}", .{action}), - - // CUD - Cursor Down - 'B' => if (@hasDecl(T, "setCursorDown")) try self.handler.setCursorDown( - switch (action.params.len) { - 0 => 1, - 1 => action.params[0], - else => { - log.warn("invalid cursor down command: {}", .{action}); - return; - }, - }, - false, - ) else log.warn("unimplemented CSI callback: {}", .{action}), - - // CUF - Cursor Right - 'C' => if (@hasDecl(T, "setCursorRight")) try self.handler.setCursorRight( - switch (action.params.len) { - 0 => 1, - 1 => action.params[0], - else => { - log.warn("invalid cursor right command: {}", .{action}); - return; - }, - }, - ) else log.warn("unimplemented CSI callback: {}", .{action}), - - // CUB - Cursor Left - 'D', 'j' => if (@hasDecl(T, "setCursorLeft")) try self.handler.setCursorLeft( - switch (action.params.len) { - 0 => 1, - 1 => action.params[0], - else => { - log.warn("invalid cursor left command: {}", .{action}); - return; - }, - }, - ) else log.warn("unimplemented CSI callback: {}", .{action}), - - // CNL - Cursor Next Line - 'E' => if (@hasDecl(T, "setCursorDown")) try self.handler.setCursorDown( - switch (action.params.len) { - 0 => 1, - 1 => action.params[0], - else => { - log.warn("invalid cursor up command: {}", .{action}); - return; - }, - }, - true, - ) else log.warn("unimplemented CSI callback: {}", .{action}), - - // CPL - Cursor Previous Line - 'F' => if (@hasDecl(T, "setCursorUp")) try self.handler.setCursorUp( - switch (action.params.len) { - 0 => 1, - 1 => action.params[0], - else => { - log.warn("invalid cursor down command: {}", .{action}); - return; - }, - }, - true, - ) else log.warn("unimplemented CSI callback: {}", .{action}), - - // HPA - Cursor Horizontal Position Absolute - // TODO: test - 'G', '`' => if (@hasDecl(T, "setCursorCol")) switch (action.params.len) { - 0 => try self.handler.setCursorCol(1), - 1 => try self.handler.setCursorCol(action.params[0]), - else => log.warn("invalid HPA command: {}", .{action}), - } else log.warn("unimplemented CSI callback: {}", .{action}), - - // CUP - Set Cursor Position. - // TODO: test - 'H', 'f' => if (@hasDecl(T, "setCursorPos")) switch (action.params.len) { - 0 => try self.handler.setCursorPos(1, 1), - 1 => try self.handler.setCursorPos(action.params[0], 1), - 2 => try self.handler.setCursorPos(action.params[0], action.params[1]), - else => log.warn("invalid CUP command: {}", .{action}), - } else log.warn("unimplemented CSI callback: {}", .{action}), - - // CHT - Cursor Horizontal Tabulation - 'I' => if (@hasDecl(T, "horizontalTab")) try self.handler.horizontalTab( - switch (action.params.len) { - 0 => 1, - 1 => action.params[0], - else => { - log.warn("invalid horizontal tab command: {}", .{action}); - return; - }, - }, - ) else log.warn("unimplemented CSI callback: {}", .{action}), - - // Erase Display - 'J' => if (@hasDecl(T, "eraseDisplay")) { - const protected_: ?bool = switch (action.intermediates.len) { - 0 => false, - 1 => if (action.intermediates[0] == '?') true else null, - else => null, - }; - - const protected = protected_ orelse { - log.warn("invalid erase display command: {}", .{action}); - return; - }; - - const mode_: ?csi.EraseDisplay = switch (action.params.len) { - 0 => .below, - 1 => if (action.params[0] <= 3) - std.meta.intToEnum(csi.EraseDisplay, action.params[0]) catch null - else - null, - else => null, - }; - - const mode = mode_ orelse { - log.warn("invalid erase display command: {}", .{action}); - return; - }; - - try self.handler.eraseDisplay(mode, protected); - } else log.warn("unimplemented CSI callback: {}", .{action}), - - // Erase Line - 'K' => if (@hasDecl(T, "eraseLine")) { - const protected_: ?bool = switch (action.intermediates.len) { - 0 => false, - 1 => if (action.intermediates[0] == '?') true else null, - else => null, - }; - - const protected = protected_ orelse { - log.warn("invalid erase line command: {}", .{action}); - return; - }; - - const mode_: ?csi.EraseLine = switch (action.params.len) { - 0 => .right, - 1 => if (action.params[0] < 3) @enumFromInt(action.params[0]) else null, - else => null, - }; - - const mode = mode_ orelse { - log.warn("invalid erase line command: {}", .{action}); - return; - }; - - try self.handler.eraseLine(mode, protected); - } else log.warn("unimplemented CSI callback: {}", .{action}), - - // IL - Insert Lines - // TODO: test - 'L' => if (@hasDecl(T, "insertLines")) switch (action.params.len) { - 0 => try self.handler.insertLines(1), - 1 => try self.handler.insertLines(action.params[0]), - else => log.warn("invalid IL command: {}", .{action}), - } else log.warn("unimplemented CSI callback: {}", .{action}), - - // DL - Delete Lines - // TODO: test - 'M' => if (@hasDecl(T, "deleteLines")) switch (action.params.len) { - 0 => try self.handler.deleteLines(1), - 1 => try self.handler.deleteLines(action.params[0]), - else => log.warn("invalid DL command: {}", .{action}), - } else log.warn("unimplemented CSI callback: {}", .{action}), - - // Delete Character (DCH) - 'P' => if (@hasDecl(T, "deleteChars")) try self.handler.deleteChars( - switch (action.params.len) { - 0 => 1, - 1 => action.params[0], - else => { - log.warn("invalid delete characters command: {}", .{action}); - return; - }, - }, - ) else log.warn("unimplemented CSI callback: {}", .{action}), - - // Scroll Up (SD) - - 'S' => switch (action.intermediates.len) { - 0 => if (@hasDecl(T, "scrollUp")) try self.handler.scrollUp( - switch (action.params.len) { - 0 => 1, - 1 => action.params[0], - else => { - log.warn("invalid scroll up command: {}", .{action}); - return; - }, - }, - ) else log.warn("unimplemented CSI callback: {}", .{action}), - - else => log.warn( - "ignoring unimplemented CSI S with intermediates: {s}", - .{action.intermediates}, - ), - }, - - // Scroll Down (SD) - 'T' => if (@hasDecl(T, "scrollDown")) try self.handler.scrollDown( - switch (action.params.len) { - 0 => 1, - 1 => action.params[0], - else => { - log.warn("invalid scroll down command: {}", .{action}); - return; - }, - }, - ) else log.warn("unimplemented CSI callback: {}", .{action}), - - // Cursor Tabulation Control - 'W' => { - switch (action.params.len) { - 0 => if (action.intermediates.len == 1 and action.intermediates[0] == '?') { - if (@hasDecl(T, "tabReset")) - try self.handler.tabReset() - else - log.warn("unimplemented tab reset callback: {}", .{action}); - }, - - 1 => switch (action.params[0]) { - 0 => if (@hasDecl(T, "tabSet")) - try self.handler.tabSet() - else - log.warn("unimplemented tab set callback: {}", .{action}), - - 2 => if (@hasDecl(T, "tabClear")) - try self.handler.tabClear(.current) - else - log.warn("unimplemented tab clear callback: {}", .{action}), - - 5 => if (@hasDecl(T, "tabClear")) - try self.handler.tabClear(.all) - else - log.warn("unimplemented tab clear callback: {}", .{action}), - - else => {}, - }, - - else => {}, - } - - log.warn("invalid cursor tabulation control: {}", .{action}); - return; - }, - - // Erase Characters (ECH) - 'X' => if (@hasDecl(T, "eraseChars")) try self.handler.eraseChars( - switch (action.params.len) { - 0 => 1, - 1 => action.params[0], - else => { - log.warn("invalid erase characters command: {}", .{action}); - return; - }, - }, - ) else log.warn("unimplemented CSI callback: {}", .{action}), - - // CHT - Cursor Horizontal Tabulation Back - 'Z' => if (@hasDecl(T, "horizontalTabBack")) try self.handler.horizontalTabBack( - switch (action.params.len) { - 0 => 1, - 1 => action.params[0], - else => { - log.warn("invalid horizontal tab back command: {}", .{action}); - return; - }, - }, - ) else log.warn("unimplemented CSI callback: {}", .{action}), - - // HPR - Cursor Horizontal Position Relative - 'a' => if (@hasDecl(T, "setCursorColRelative")) try self.handler.setCursorColRelative( - switch (action.params.len) { - 0 => 1, - 1 => action.params[0], - else => { - log.warn("invalid HPR command: {}", .{action}); - return; - }, - }, - ) else log.warn("unimplemented CSI callback: {}", .{action}), - - // Repeat Previous Char (REP) - 'b' => if (@hasDecl(T, "printRepeat")) try self.handler.printRepeat( - switch (action.params.len) { - 0 => 1, - 1 => action.params[0], - else => { - log.warn("invalid print repeat command: {}", .{action}); - return; - }, - }, - ) else log.warn("unimplemented CSI callback: {}", .{action}), - - // c - Device Attributes (DA1) - 'c' => if (@hasDecl(T, "deviceAttributes")) { - const req: ansi.DeviceAttributeReq = switch (action.intermediates.len) { - 0 => ansi.DeviceAttributeReq.primary, - 1 => switch (action.intermediates[0]) { - '>' => ansi.DeviceAttributeReq.secondary, - '=' => ansi.DeviceAttributeReq.tertiary, - else => null, - }, - else => @as(?ansi.DeviceAttributeReq, null), - } orelse { - log.warn("invalid device attributes command: {}", .{action}); - return; - }; - - try self.handler.deviceAttributes(req, action.params); - } else log.warn("unimplemented CSI callback: {}", .{action}), - - // VPA - Cursor Vertical Position Absolute - 'd' => if (@hasDecl(T, "setCursorRow")) try self.handler.setCursorRow( - switch (action.params.len) { - 0 => 1, - 1 => action.params[0], - else => { - log.warn("invalid VPA command: {}", .{action}); - return; - }, - }, - ) else log.warn("unimplemented CSI callback: {}", .{action}), - - // VPR - Cursor Vertical Position Relative - 'e' => if (@hasDecl(T, "setCursorRowRelative")) try self.handler.setCursorRowRelative( - switch (action.params.len) { - 0 => 1, - 1 => action.params[0], - else => { - log.warn("invalid VPR command: {}", .{action}); - return; - }, - }, - ) else log.warn("unimplemented CSI callback: {}", .{action}), - - // TBC - Tab Clear - // TODO: test - 'g' => if (@hasDecl(T, "tabClear")) try self.handler.tabClear( - switch (action.params.len) { - 1 => @enumFromInt(action.params[0]), - else => { - log.warn("invalid tab clear command: {}", .{action}); - return; - }, - }, - ) else log.warn("unimplemented CSI callback: {}", .{action}), - - // SM - Set Mode - 'h' => if (@hasDecl(T, "setMode")) mode: { - const ansi_mode = ansi: { - if (action.intermediates.len == 0) break :ansi true; - if (action.intermediates.len == 1 and - action.intermediates[0] == '?') break :ansi false; - - log.warn("invalid set mode command: {}", .{action}); - break :mode; - }; - - for (action.params) |mode_int| { - if (modes.modeFromInt(mode_int, ansi_mode)) |mode| { - try self.handler.setMode(mode, true); - } else { - log.warn("unimplemented mode: {}", .{mode_int}); - } - } - } else log.warn("unimplemented CSI callback: {}", .{action}), - - // RM - Reset Mode - 'l' => if (@hasDecl(T, "setMode")) mode: { - const ansi_mode = ansi: { - if (action.intermediates.len == 0) break :ansi true; - if (action.intermediates.len == 1 and - action.intermediates[0] == '?') break :ansi false; - - log.warn("invalid set mode command: {}", .{action}); - break :mode; - }; - - for (action.params) |mode_int| { - if (modes.modeFromInt(mode_int, ansi_mode)) |mode| { - try self.handler.setMode(mode, false); - } else { - log.warn("unimplemented mode: {}", .{mode_int}); - } - } - } else log.warn("unimplemented CSI callback: {}", .{action}), - - // SGR - Select Graphic Rendition - 'm' => switch (action.intermediates.len) { - 0 => if (@hasDecl(T, "setAttribute")) { - // log.info("parse SGR params={any}", .{action.params}); - var p: sgr.Parser = .{ .params = action.params, .colon = action.sep == .colon }; - while (p.next()) |attr| { - // log.info("SGR attribute: {}", .{attr}); - try self.handler.setAttribute(attr); - } - } else log.warn("unimplemented CSI callback: {}", .{action}), - - 1 => switch (action.intermediates[0]) { - '>' => if (@hasDecl(T, "setModifyKeyFormat")) blk: { - if (action.params.len == 0) { - // Reset - try self.handler.setModifyKeyFormat(.{ .legacy = {} }); - break :blk; - } - - var format: ansi.ModifyKeyFormat = switch (action.params[0]) { - 0 => .{ .legacy = {} }, - 1 => .{ .cursor_keys = {} }, - 2 => .{ .function_keys = {} }, - 4 => .{ .other_keys = .none }, - else => { - log.warn("invalid setModifyKeyFormat: {}", .{action}); - break :blk; - }, - }; - - if (action.params.len > 2) { - log.warn("invalid setModifyKeyFormat: {}", .{action}); - break :blk; - } - - if (action.params.len == 2) { - switch (format) { - // We don't support any of the subparams yet for these. - .legacy => {}, - .cursor_keys => {}, - .function_keys => {}, - - // We only support the numeric form. - .other_keys => |*v| switch (action.params[1]) { - 2 => v.* = .numeric, - else => v.* = .none, - }, - } - } - - try self.handler.setModifyKeyFormat(format); - } else log.warn("unimplemented setModifyKeyFormat: {}", .{action}), - - else => log.warn( - "unknown CSI m with intermediate: {}", - .{action.intermediates[0]}, - ), - }, - - else => { - // Nothing, but I wanted a place to put this comment: - // there are others forms of CSI m that have intermediates. - // `vim --clean` uses `CSI ? 4 m` and I don't know what - // that means. And there is also `CSI > m` which is used - // to control modifier key reporting formats that we don't - // support yet. - log.warn( - "ignoring unimplemented CSI m with intermediates: {s}", - .{action.intermediates}, - ); - }, - }, - - // TODO: test - 'n' => { - // Handle deviceStatusReport first - if (action.intermediates.len == 0 or - action.intermediates[0] == '?') - { - if (!@hasDecl(T, "deviceStatusReport")) { - log.warn("unimplemented CSI callback: {}", .{action}); - return; - } - - if (action.params.len != 1) { - log.warn("invalid device status report command: {}", .{action}); - return; - } - - const question = question: { - if (action.intermediates.len == 0) break :question false; - if (action.intermediates.len == 1 and - action.intermediates[0] == '?') break :question true; - - log.warn("invalid set mode command: {}", .{action}); - return; - }; - - const req = device_status.reqFromInt(action.params[0], question) orelse { - log.warn("invalid device status report command: {}", .{action}); - return; - }; - - try self.handler.deviceStatusReport(req); - return; - } - - // Handle other forms of CSI n - switch (action.intermediates.len) { - 0 => unreachable, // handled above - - 1 => switch (action.intermediates[0]) { - '>' => if (@hasDecl(T, "setModifyKeyFormat")) { - // This isn't strictly correct. CSI > n has parameters that - // control what exactly is being disabled. However, we - // only support reverting back to modify other keys in - // numeric except format. - try self.handler.setModifyKeyFormat(.{ .other_keys = .numeric_except }); - } else log.warn("unimplemented setModifyKeyFormat: {}", .{action}), - - else => log.warn( - "unknown CSI n with intermediate: {}", - .{action.intermediates[0]}, - ), - }, - - else => log.warn( - "ignoring unimplemented CSI n with intermediates: {s}", - .{action.intermediates}, - ), - } - }, - - // DECRQM - Request Mode - 'p' => switch (action.intermediates.len) { - 2 => decrqm: { - const ansi_mode = ansi: { - switch (action.intermediates.len) { - 1 => if (action.intermediates[0] == '$') break :ansi true, - 2 => if (action.intermediates[0] == '?' and - action.intermediates[1] == '$') break :ansi false, - else => {}, - } - - log.warn( - "ignoring unimplemented CSI p with intermediates: {s}", - .{action.intermediates}, - ); - break :decrqm; - }; - - if (action.params.len != 1) { - log.warn("invalid DECRQM command: {}", .{action}); - break :decrqm; - } - - if (@hasDecl(T, "requestMode")) { - try self.handler.requestMode(action.params[0], ansi_mode); - } else log.warn("unimplemented DECRQM callback: {}", .{action}); - }, - - else => log.warn( - "ignoring unimplemented CSI p with intermediates: {s}", - .{action.intermediates}, - ), - }, - - 'q' => switch (action.intermediates.len) { - 1 => switch (action.intermediates[0]) { - // DECSCUSR - Select Cursor Style - // TODO: test - ' ' => { - if (@hasDecl(T, "setCursorStyle")) try self.handler.setCursorStyle( - switch (action.params.len) { - 0 => ansi.CursorStyle.default, - 1 => @enumFromInt(action.params[0]), - else => { - log.warn("invalid set curor style command: {}", .{action}); - return; - }, - }, - ) else log.warn("unimplemented CSI callback: {}", .{action}); - }, - - // DECSCA - '"' => { - if (@hasDecl(T, "setProtectedMode")) { - const mode_: ?ansi.ProtectedMode = switch (action.params.len) { - else => null, - 0 => .off, - 1 => switch (action.params[0]) { - 0, 2 => .off, - 1 => .dec, - else => null, - }, - }; - - const mode = mode_ orelse { - log.warn("invalid set protected mode command: {}", .{action}); - return; - }; - - try self.handler.setProtectedMode(mode); - } else log.warn("unimplemented CSI callback: {}", .{action}); - }, - - // XTVERSION - '>' => { - if (@hasDecl(T, "reportXtversion")) try self.handler.reportXtversion(); - }, - else => { - log.warn( - "ignoring unimplemented CSI q with intermediates: {s}", - .{action.intermediates}, - ); - }, - }, - - else => log.warn( - "ignoring unimplemented CSI p 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_int| { - if (modes.modeFromInt(mode_int, false)) |mode| { - try self.handler.restoreMode(mode); - } else { - log.warn( - "unimplemented restore mode: {}", - .{mode_int}, - ); - } - } - }, - - else => log.warn( - "unknown CSI s with intermediate: {}", - .{action}, - ), - }, - - else => log.warn( - "ignoring unimplemented CSI s with intermediates: {s}", - .{action}, - ), - }, - - 's' => switch (action.intermediates.len) { - // DECSLRM - 0 => if (@hasDecl(T, "setLeftAndRightMargin")) { - switch (action.params.len) { - // CSI S is ambiguous with zero params so we defer - // to our handler to do the proper logic. If mode 69 - // is set, then we should invoke DECSLRM, otherwise - // we should invoke SC. - 0 => try self.handler.setLeftAndRightMarginAmbiguous(), - 1 => try self.handler.setLeftAndRightMargin(action.params[0], 0), - 2 => try self.handler.setLeftAndRightMargin(action.params[0], action.params[1]), - else => log.warn("invalid DECSLRM command: {}", .{action}), - } - } else log.warn( - "unimplemented CSI callback: {}", - .{action}, - ), - - 1 => switch (action.intermediates[0]) { - '?' => if (@hasDecl(T, "saveMode")) { - for (action.params) |mode_int| { - if (modes.modeFromInt(mode_int, false)) |mode| { - try self.handler.saveMode(mode); - } else { - log.warn( - "unimplemented save mode: {}", - .{mode_int}, - ); - } - } - }, - - // XTSHIFTESCAPE - '>' => if (@hasDecl(T, "setMouseShiftCapture")) capture: { - const capture = switch (action.params.len) { - 0 => false, - 1 => switch (action.params[0]) { - 0 => false, - 1 => true, - else => { - log.warn("invalid XTSHIFTESCAPE command: {}", .{action}); - break :capture; - }, - }, - else => { - log.warn("invalid XTSHIFTESCAPE command: {}", .{action}); - break :capture; - }, - }; - - try self.handler.setMouseShiftCapture(capture); - } else log.warn( - "unimplemented CSI callback: {}", - .{action}, - ), - - else => log.warn( - "unknown CSI s with intermediate: {}", - .{action}, - ), - }, - - else => log.warn( - "ignoring unimplemented CSI s with intermediates: {s}", - .{action}, - ), - }, - - 'u' => switch (action.intermediates.len) { - 0 => if (@hasDecl(T, "restoreCursor")) - try self.handler.restoreCursor() - else - log.warn("unimplemented CSI callback: {}", .{action}), - - // Kitty keyboard protocol - 1 => switch (action.intermediates[0]) { - '?' => if (@hasDecl(T, "queryKittyKeyboard")) { - try self.handler.queryKittyKeyboard(); - }, - - '>' => if (@hasDecl(T, "pushKittyKeyboard")) push: { - const flags: u5 = if (action.params.len == 1) - std.math.cast(u5, action.params[0]) orelse { - log.warn("invalid pushKittyKeyboard command: {}", .{action}); - break :push; - } - else - 0; - - try self.handler.pushKittyKeyboard(@bitCast(flags)); - }, - - '<' => if (@hasDecl(T, "popKittyKeyboard")) { - const number: u16 = if (action.params.len == 1) - action.params[0] - else - 1; - - try self.handler.popKittyKeyboard(number); - }, - - '=' => if (@hasDecl(T, "setKittyKeyboard")) set: { - const flags: u5 = if (action.params.len >= 1) - std.math.cast(u5, action.params[0]) orelse { - log.warn("invalid setKittyKeyboard command: {}", .{action}); - break :set; - } - else - 0; - - const number: u16 = if (action.params.len >= 2) - action.params[1] - else - 1; - - const mode: kitty.KeySetMode = switch (number) { - 0 => .set, - 1 => .@"or", - 2 => .not, - else => { - log.warn("invalid setKittyKeyboard command: {}", .{action}); - break :set; - }, - }; - - try self.handler.setKittyKeyboard( - mode, - @bitCast(flags), - ); - }, - - else => log.warn( - "unknown CSI s with intermediate: {}", - .{action}, - ), - }, - - else => log.warn( - "ignoring unimplemented CSI u: {}", - .{action}, - ), - }, - - // ICH - Insert Blanks - '@' => switch (action.intermediates.len) { - 0 => if (@hasDecl(T, "insertBlanks")) switch (action.params.len) { - 0 => try self.handler.insertBlanks(1), - 1 => try self.handler.insertBlanks(action.params[0]), - else => log.warn("invalid ICH command: {}", .{action}), - } else log.warn("unimplemented CSI callback: {}", .{action}), - - else => log.warn( - "ignoring unimplemented CSI @: {}", - .{action}, - ), - }, - - // DECSASD - Select Active Status Display - '}' => { - const success = decsasd: { - // Verify we're getting a DECSASD command - if (action.intermediates.len != 1 or action.intermediates[0] != '$') - break :decsasd false; - if (action.params.len != 1) - break :decsasd false; - if (!@hasDecl(T, "setActiveStatusDisplay")) - break :decsasd false; - - try self.handler.setActiveStatusDisplay(@enumFromInt(action.params[0])); - break :decsasd true; - }; - - if (!success) log.warn("unimplemented CSI callback: {}", .{action}); - }, - - else => if (@hasDecl(T, "csiUnimplemented")) - try self.handler.csiUnimplemented(action) - else - log.warn("unimplemented CSI action: {}", .{action}), - } - } - - fn oscDispatch(self: *Self, cmd: osc.Command) !void { - switch (cmd) { - .change_window_title => |title| { - if (@hasDecl(T, "changeWindowTitle")) { - if (!std.unicode.utf8ValidateSlice(title)) { - log.warn("change title request: invalid utf-8, ignoring request", .{}); - return; - } - - try self.handler.changeWindowTitle(title); - return; - } else log.warn("unimplemented OSC callback: {}", .{cmd}); - }, - - .change_window_icon => |icon| { - log.info("OSC 1 (change icon) received and ignored icon={s}", .{icon}); - }, - - .clipboard_contents => |clip| { - if (@hasDecl(T, "clipboardContents")) { - try self.handler.clipboardContents(clip.kind, clip.data); - return; - } else log.warn("unimplemented OSC callback: {}", .{cmd}); - }, - - .prompt_start => |v| { - if (@hasDecl(T, "promptStart")) { - switch (v.kind) { - .primary, .right => try self.handler.promptStart(v.aid, v.redraw), - .continuation => try self.handler.promptContinuation(v.aid), - } - return; - } else log.warn("unimplemented OSC callback: {}", .{cmd}); - }, - - .prompt_end => { - if (@hasDecl(T, "promptEnd")) { - try self.handler.promptEnd(); - return; - } else log.warn("unimplemented OSC callback: {}", .{cmd}); - }, - - .end_of_input => { - if (@hasDecl(T, "endOfInput")) { - try self.handler.endOfInput(); - return; - } else log.warn("unimplemented OSC callback: {}", .{cmd}); - }, - - .end_of_command => |end| { - if (@hasDecl(T, "endOfCommand")) { - try self.handler.endOfCommand(end.exit_code); - return; - } else log.warn("unimplemented OSC callback: {}", .{cmd}); - }, - - .report_pwd => |v| { - if (@hasDecl(T, "reportPwd")) { - try self.handler.reportPwd(v.value); - return; - } else log.warn("unimplemented OSC callback: {}", .{cmd}); - }, - - .mouse_shape => |v| { - if (@hasDecl(T, "setMouseShape")) { - const shape = MouseShape.fromString(v.value) orelse { - log.warn("unknown cursor shape: {s}", .{v.value}); - return; - }; - - try self.handler.setMouseShape(shape); - return; - } else log.warn("unimplemented OSC callback: {}", .{cmd}); - }, - - .report_color => |v| { - if (@hasDecl(T, "reportColor")) { - try self.handler.reportColor(v.kind, v.terminator); - return; - } else log.warn("unimplemented OSC callback: {}", .{cmd}); - }, - - .set_color => |v| { - if (@hasDecl(T, "setColor")) { - try self.handler.setColor(v.kind, v.value); - return; - } else log.warn("unimplemented OSC callback: {}", .{cmd}); - }, - - .reset_color => |v| { - if (@hasDecl(T, "resetColor")) { - try self.handler.resetColor(v.kind, v.value); - return; - } else log.warn("unimplemented OSC callback: {}", .{cmd}); - }, - - .show_desktop_notification => |v| { - if (@hasDecl(T, "showDesktopNotification")) { - try self.handler.showDesktopNotification(v.title, v.body); - return; - } else log.warn("unimplemented OSC callback: {}", .{cmd}); - }, - } - - // Fall through for when we don't have a handler. - if (@hasDecl(T, "oscUnimplemented")) { - try self.handler.oscUnimplemented(cmd); - } else { - log.warn("unimplemented OSC command: {s}", .{@tagName(cmd)}); - } - } - - fn configureCharset( - self: *Self, - intermediates: []const u8, - set: charsets.Charset, - ) !void { - if (intermediates.len != 1) { - log.warn("invalid charset intermediate: {any}", .{intermediates}); - return; - } - - const slot: charsets.Slots = switch (intermediates[0]) { - // TODO: support slots '-', '.', '/' - - '(' => .G0, - ')' => .G1, - '*' => .G2, - '+' => .G3, - else => { - log.warn("invalid charset intermediate: {any}", .{intermediates}); - return; - }, - }; - - if (@hasDecl(T, "configureCharset")) { - try self.handler.configureCharset(slot, set); - return; - } - - log.warn("unimplemented configureCharset callback slot={} set={}", .{ - slot, - set, - }); - } - - fn escDispatch( - self: *Self, - action: Parser.Action.ESC, - ) !void { - switch (action.final) { - // Charsets - 'B' => try self.configureCharset(action.intermediates, .ascii), - 'A' => try self.configureCharset(action.intermediates, .british), - '0' => try self.configureCharset(action.intermediates, .dec_special), - - // DECSC - Save Cursor - '7' => if (@hasDecl(T, "saveCursor")) switch (action.intermediates.len) { - 0 => try self.handler.saveCursor(), - else => { - log.warn("invalid command: {}", .{action}); - return; - }, - } else log.warn("unimplemented ESC callback: {}", .{action}), - - '8' => blk: { - switch (action.intermediates.len) { - // DECRC - Restore Cursor - 0 => if (@hasDecl(T, "restoreCursor")) { - try self.handler.restoreCursor(); - break :blk {}; - } else log.warn("unimplemented restore cursor callback: {}", .{action}), - - 1 => switch (action.intermediates[0]) { - // DECALN - Fill Screen with E - '#' => if (@hasDecl(T, "decaln")) { - try self.handler.decaln(); - break :blk {}; - } else log.warn("unimplemented ESC callback: {}", .{action}), - - else => {}, - }, - - else => {}, // fall through - } - - log.warn("unimplemented ESC action: {}", .{action}); - }, - - // IND - Index - 'D' => if (@hasDecl(T, "index")) switch (action.intermediates.len) { - 0 => try self.handler.index(), - else => { - log.warn("invalid index command: {}", .{action}); - return; - }, - } else log.warn("unimplemented ESC callback: {}", .{action}), - - // NEL - Next Line - 'E' => if (@hasDecl(T, "nextLine")) switch (action.intermediates.len) { - 0 => try self.handler.nextLine(), - else => { - log.warn("invalid next line command: {}", .{action}); - return; - }, - } else log.warn("unimplemented ESC callback: {}", .{action}), - - // HTS - Horizontal Tab Set - 'H' => if (@hasDecl(T, "tabSet")) - try self.handler.tabSet() - else - log.warn("unimplemented tab set callback: {}", .{action}), - - // RI - Reverse Index - 'M' => if (@hasDecl(T, "reverseIndex")) switch (action.intermediates.len) { - 0 => try self.handler.reverseIndex(), - else => { - log.warn("invalid reverse index command: {}", .{action}); - return; - }, - } else log.warn("unimplemented ESC callback: {}", .{action}), - - // SS2 - Single Shift 2 - 'N' => if (@hasDecl(T, "invokeCharset")) switch (action.intermediates.len) { - 0 => try self.handler.invokeCharset(.GL, .G2, true), - else => { - log.warn("invalid single shift 2 command: {}", .{action}); - return; - }, - } else log.warn("unimplemented invokeCharset: {}", .{action}), - - // SS3 - Single Shift 3 - 'O' => if (@hasDecl(T, "invokeCharset")) switch (action.intermediates.len) { - 0 => try self.handler.invokeCharset(.GL, .G3, true), - else => { - log.warn("invalid single shift 3 command: {}", .{action}); - return; - }, - } else log.warn("unimplemented invokeCharset: {}", .{action}), - - // DECID - 'Z' => if (@hasDecl(T, "deviceAttributes")) { - try self.handler.deviceAttributes(.primary, &.{}); - } else log.warn("unimplemented ESC callback: {}", .{action}), - - // RIS - Full Reset - 'c' => if (@hasDecl(T, "fullReset")) switch (action.intermediates.len) { - 0 => try self.handler.fullReset(), - else => { - log.warn("invalid full reset command: {}", .{action}); - return; - }, - } else log.warn("unimplemented ESC callback: {}", .{action}), - - // LS2 - Locking Shift 2 - 'n' => if (@hasDecl(T, "invokeCharset")) switch (action.intermediates.len) { - 0 => try self.handler.invokeCharset(.GL, .G2, false), - else => { - log.warn("invalid single shift 2 command: {}", .{action}); - return; - }, - } else log.warn("unimplemented invokeCharset: {}", .{action}), - - // LS3 - Locking Shift 3 - 'o' => if (@hasDecl(T, "invokeCharset")) switch (action.intermediates.len) { - 0 => try self.handler.invokeCharset(.GL, .G3, false), - else => { - log.warn("invalid single shift 3 command: {}", .{action}); - return; - }, - } else log.warn("unimplemented invokeCharset: {}", .{action}), - - // LS1R - Locking Shift 1 Right - '~' => if (@hasDecl(T, "invokeCharset")) switch (action.intermediates.len) { - 0 => try self.handler.invokeCharset(.GR, .G1, false), - else => { - log.warn("invalid locking shift 1 right command: {}", .{action}); - return; - }, - } else log.warn("unimplemented invokeCharset: {}", .{action}), - - // LS2R - Locking Shift 2 Right - '}' => if (@hasDecl(T, "invokeCharset")) switch (action.intermediates.len) { - 0 => try self.handler.invokeCharset(.GR, .G2, false), - else => { - log.warn("invalid locking shift 2 right command: {}", .{action}); - return; - }, - } else log.warn("unimplemented invokeCharset: {}", .{action}), - - // LS3R - Locking Shift 3 Right - '|' => if (@hasDecl(T, "invokeCharset")) switch (action.intermediates.len) { - 0 => try self.handler.invokeCharset(.GR, .G3, false), - else => { - log.warn("invalid locking shift 3 right command: {}", .{action}); - return; - }, - } else log.warn("unimplemented invokeCharset: {}", .{action}), - - // Set application keypad mode - '=' => if (@hasDecl(T, "setMode")) { - try self.handler.setMode(.keypad_keys, true); - } else log.warn("unimplemented setMode: {}", .{action}), - - // Reset application keypad mode - '>' => if (@hasDecl(T, "setMode")) { - try self.handler.setMode(.keypad_keys, false); - } else log.warn("unimplemented setMode: {}", .{action}), - - else => if (@hasDecl(T, "escUnimplemented")) - try self.handler.escUnimplemented(action) - else - log.warn("unimplemented ESC action: {}", .{action}), - - // Sets ST (string terminator). We don't have to do anything - // because our parser always accepts ST. - '\\' => {}, - } - } - }; -} - -test "stream: print" { - const H = struct { - c: ?u21 = 0, - - pub fn print(self: *@This(), c: u21) !void { - self.c = c; - } - }; - - var s: Stream(H) = .{ .handler = .{} }; - try s.next('x'); - try testing.expectEqual(@as(u21, 'x'), s.handler.c.?); -} - -test "simd: print invalid utf-8" { - const H = struct { - c: ?u21 = 0, - - pub fn print(self: *@This(), c: u21) !void { - self.c = c; - } - }; - - var s: Stream(H) = .{ .handler = .{} }; - try s.nextSlice(&.{0xFF}); - try testing.expectEqual(@as(u21, 0xFFFD), s.handler.c.?); -} - -test "simd: complete incomplete utf-8" { - const H = struct { - c: ?u21 = null, - - pub fn print(self: *@This(), c: u21) !void { - self.c = c; - } - }; - - var s: Stream(H) = .{ .handler = .{} }; - try s.nextSlice(&.{0xE0}); // 3 byte - try testing.expect(s.handler.c == null); - try s.nextSlice(&.{0xA0}); // still incomplete - try testing.expect(s.handler.c == null); - try s.nextSlice(&.{0x80}); - try testing.expectEqual(@as(u21, 0x800), s.handler.c.?); -} - -test "stream: cursor right (CUF)" { - const H = struct { - amount: u16 = 0, - - pub fn setCursorRight(self: *@This(), v: u16) !void { - self.amount = v; - } - }; - - var s: Stream(H) = .{ .handler = .{} }; - try s.nextSlice("\x1B[C"); - try testing.expectEqual(@as(u16, 1), s.handler.amount); - - try s.nextSlice("\x1B[5C"); - try testing.expectEqual(@as(u16, 5), s.handler.amount); - - s.handler.amount = 0; - try s.nextSlice("\x1B[5;4C"); - try testing.expectEqual(@as(u16, 0), s.handler.amount); -} - -test "stream: dec set mode (SM) and reset mode (RM)" { - const H = struct { - 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(modes.Mode, .origin), s.handler.mode); - - try s.nextSlice("\x1B[?6l"); - try testing.expectEqual(@as(modes.Mode, @enumFromInt(1)), s.handler.mode); -} - -test "stream: ansi set mode (SM) and reset mode (RM)" { - const H = struct { - mode: ?modes.Mode = null, - - pub fn setMode(self: *@This(), mode: modes.Mode, v: bool) !void { - self.mode = null; - if (v) self.mode = mode; - } - }; - - var s: Stream(H) = .{ .handler = .{} }; - try s.nextSlice("\x1B[4h"); - try testing.expectEqual(@as(modes.Mode, .insert), s.handler.mode.?); - - try s.nextSlice("\x1B[4l"); - try testing.expect(s.handler.mode == null); -} - -test "stream: ansi set mode (SM) and reset mode (RM) with unknown value" { - const H = struct { - mode: ?modes.Mode = null, - - pub fn setMode(self: *@This(), mode: modes.Mode, v: bool) !void { - self.mode = null; - if (v) self.mode = mode; - } - }; - - var s: Stream(H) = .{ .handler = .{} }; - try s.nextSlice("\x1B[6h"); - try testing.expect(s.handler.mode == null); - - try s.nextSlice("\x1B[6l"); - try testing.expect(s.handler.mode == null); -} - -test "stream: restore mode" { - const H = struct { - const Self = @This(); - called: bool = false, - - pub fn setTopAndBottomMargin(self: *Self, t: u16, b: u16) !void { - _ = t; - _ = b; - self.called = true; - } - }; - - var s: Stream(H) = .{ .handler = .{} }; - for ("\x1B[?42r") |c| try s.next(c); - try testing.expect(!s.handler.called); -} - -test "stream: pop kitty keyboard with no params defaults to 1" { - const H = struct { - const Self = @This(); - n: u16 = 0, - - pub fn popKittyKeyboard(self: *Self, n: u16) !void { - self.n = n; - } - }; - - var s: Stream(H) = .{ .handler = .{} }; - for ("\x1B[2s"); - try testing.expect(s.handler.escape == null); - - try s.nextSlice("\x1B[>s"); - try testing.expect(s.handler.escape.? == false); - - try s.nextSlice("\x1B[>0s"); - try testing.expect(s.handler.escape.? == false); - - try s.nextSlice("\x1B[>1s"); - try testing.expect(s.handler.escape.? == true); -} - -test "stream: change window title with invalid utf-8" { - const H = struct { - seen: bool = false, - - pub fn changeWindowTitle(self: *@This(), title: []const u8) !void { - _ = title; - - self.seen = true; - } - }; - - { - var s: Stream(H) = .{ .handler = .{} }; - try s.nextSlice("\x1b]2;abc\x1b\\"); - try testing.expect(s.handler.seen); - } - - { - var s: Stream(H) = .{ .handler = .{} }; - try s.nextSlice("\x1b]2;abc\xc0\x1b\\"); - try testing.expect(!s.handler.seen); - } -} - -test "stream: insert characters" { - const H = struct { - const Self = @This(); - called: bool = false, - - pub fn insertBlanks(self: *Self, v: u16) !void { - _ = v; - self.called = true; - } - }; - - var s: Stream(H) = .{ .handler = .{} }; - for ("\x1B[42@") |c| try s.next(c); - try testing.expect(s.handler.called); - - s.handler.called = false; - for ("\x1B[?42@") |c| try s.next(c); - try testing.expect(!s.handler.called); -} - -test "stream: SCOSC" { - const H = struct { - const Self = @This(); - called: bool = false, - - pub fn setLeftAndRightMargin(self: *Self, left: u16, right: u16) !void { - _ = self; - _ = left; - _ = right; - @panic("bad"); - } - - pub fn setLeftAndRightMarginAmbiguous(self: *Self) !void { - self.called = true; - } - }; - - var s: Stream(H) = .{ .handler = .{} }; - for ("\x1B[s") |c| try s.next(c); - try testing.expect(s.handler.called); -} - -test "stream: SCORC" { - const H = struct { - const Self = @This(); - called: bool = false, - - pub fn restoreCursor(self: *Self) !void { - self.called = true; - } - }; - - var s: Stream(H) = .{ .handler = .{} }; - for ("\x1B[u") |c| try s.next(c); - try testing.expect(s.handler.called); -} - -test "stream: too many csi params" { - const H = struct { - pub fn setCursorRight(self: *@This(), v: u16) !void { - _ = v; - _ = self; - unreachable; - } - }; - - var s: Stream(H) = .{ .handler = .{} }; - try s.nextSlice("\x1B[1;1;1;1;1;1;1;1;1;1;1;1;1;1;1;1;1C"); -} - -test "stream: csi param too long" { - const H = struct { - pub fn setCursorRight(self: *@This(), v: u16) !void { - _ = v; - _ = self; - } - }; - - var s: Stream(H) = .{ .handler = .{} }; - try s.nextSlice("\x1B[1111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111C"); -} diff --git a/src/terminal-old/wasm.zig b/src/terminal-old/wasm.zig deleted file mode 100644 index 3450a6829..000000000 --- a/src/terminal-old/wasm.zig +++ /dev/null @@ -1,32 +0,0 @@ -// This is the C-ABI API for the terminal package. This isn't used -// by other Zig programs but by C or WASM interfacing. -// -// NOTE: This is far, far from complete. We did a very minimal amount to -// prove that compilation works, but we haven't completed coverage yet. - -const std = @import("std"); -const builtin = @import("builtin"); -const Allocator = std.mem.Allocator; -const Terminal = @import("main.zig").Terminal; -const wasm = @import("../os/wasm.zig"); -const alloc = wasm.alloc; - -export fn terminal_new(cols: usize, rows: usize) ?*Terminal { - const term = Terminal.init(alloc, cols, rows) catch return null; - const result = alloc.create(Terminal) catch return null; - result.* = term; - return result; -} - -export fn terminal_free(ptr: ?*Terminal) void { - if (ptr) |v| { - v.deinit(alloc); - alloc.destroy(v); - } -} - -export fn terminal_print(ptr: ?*Terminal, char: u32) void { - if (ptr) |t| { - t.print(@intCast(char)) catch return; - } -} diff --git a/src/terminal-old/x11_color.zig b/src/terminal-old/x11_color.zig deleted file mode 100644 index 9e4eda86b..000000000 --- a/src/terminal-old/x11_color.zig +++ /dev/null @@ -1,62 +0,0 @@ -const std = @import("std"); -const assert = std.debug.assert; -const RGB = @import("color.zig").RGB; - -/// The map of all available X11 colors. -pub const map = colorMap() catch @compileError("failed to parse rgb.txt"); - -fn colorMap() !type { - @setEvalBranchQuota(100_000); - - const KV = struct { []const u8, RGB }; - - // The length of our data is the number of lines in the rgb file. - const len = std.mem.count(u8, data, "\n"); - var kvs: [len]KV = undefined; - - // Parse the line. This is not very robust parsing, because we expect - // a very exact format for rgb.txt. However, this is all done at comptime - // so if our data is bad, we should hopefully get an error here or one - // of our unit tests will catch it. - var iter = std.mem.splitScalar(u8, data, '\n'); - var i: usize = 0; - while (iter.next()) |line| { - if (line.len == 0) continue; - const r = try std.fmt.parseInt(u8, std.mem.trim(u8, line[0..3], " "), 10); - const g = try std.fmt.parseInt(u8, std.mem.trim(u8, line[4..7], " "), 10); - const b = try std.fmt.parseInt(u8, std.mem.trim(u8, line[8..11], " "), 10); - const name = std.mem.trim(u8, line[12..], " \t\n"); - kvs[i] = .{ name, .{ .r = r, .g = g, .b = b } }; - i += 1; - } - assert(i == len); - - return std.ComptimeStringMapWithEql( - RGB, - kvs, - std.comptime_string_map.eqlAsciiIgnoreCase, - ); -} - -/// This is the rgb.txt file from the X11 project. This was last sourced -/// from this location: https://gitlab.freedesktop.org/xorg/app/rgb -/// This data is licensed under the MIT/X11 license while this Zig file is -/// licensed under the same license as Ghostty. -const data = @embedFile("res/rgb.txt"); - -test { - const testing = std.testing; - try testing.expectEqual(null, map.get("nosuchcolor")); - try testing.expectEqual(RGB{ .r = 255, .g = 255, .b = 255 }, map.get("white").?); - try testing.expectEqual(RGB{ .r = 0, .g = 250, .b = 154 }, map.get("medium spring green")); - try testing.expectEqual(RGB{ .r = 34, .g = 139, .b = 34 }, map.get("ForestGreen")); - try testing.expectEqual(RGB{ .r = 34, .g = 139, .b = 34 }, map.get("FoReStGReen")); - try testing.expectEqual(RGB{ .r = 0, .g = 0, .b = 0 }, map.get("black")); - try testing.expectEqual(RGB{ .r = 255, .g = 0, .b = 0 }, map.get("red")); - try testing.expectEqual(RGB{ .r = 0, .g = 255, .b = 0 }, map.get("green")); - try testing.expectEqual(RGB{ .r = 0, .g = 0, .b = 255 }, map.get("blue")); - try testing.expectEqual(RGB{ .r = 255, .g = 255, .b = 255 }, map.get("white")); - try testing.expectEqual(RGB{ .r = 124, .g = 252, .b = 0 }, map.get("lawngreen")); - try testing.expectEqual(RGB{ .r = 0, .g = 250, .b = 154 }, map.get("mediumspringgreen")); - try testing.expectEqual(RGB{ .r = 34, .g = 139, .b = 34 }, map.get("forestgreen")); -}