mirror of
https://github.com/ghostty-org/ghostty.git
synced 2025-07-17 01:06:08 +03:00
remove old terminal implementation
This commit is contained in:
@ -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 {
|
||||
|
@ -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}"
|
||||
|
@ -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));
|
||||
}
|
||||
}
|
@ -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}"
|
||||
|
@ -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();
|
||||
}
|
||||
}
|
@ -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 </tmp/ghostty_bench_data" \
|
||||
-n new \
|
||||
"./zig-out/bin/bench-stream --mode=simd --terminal=new </tmp/ghostty_bench_data" \
|
||||
-n old \
|
||||
"./zig-out/bin/bench-stream --mode=simd --terminal=old </tmp/ghostty_bench_data"
|
||||
|
@ -14,8 +14,7 @@ const Allocator = std.mem.Allocator;
|
||||
const ArenaAllocator = std.heap.ArenaAllocator;
|
||||
const ziglyph = @import("ziglyph");
|
||||
const cli = @import("../cli.zig");
|
||||
const terminal = @import("../terminal-old/main.zig");
|
||||
const terminalnew = @import("../terminal/main.zig");
|
||||
const terminal = @import("../terminal/main.zig");
|
||||
|
||||
const Args = struct {
|
||||
mode: Mode = .noop,
|
||||
@ -44,7 +43,7 @@ const Args = struct {
|
||||
self.* = undefined;
|
||||
}
|
||||
|
||||
const Terminal = enum { none, old, new };
|
||||
const Terminal = enum { none, new };
|
||||
};
|
||||
|
||||
const Mode = enum {
|
||||
@ -94,21 +93,6 @@ pub fn main() !void {
|
||||
const writer = std.io.getStdOut().writer();
|
||||
const buf = try alloc.alloc(u8, args.@"buffer-size");
|
||||
|
||||
if (false) {
|
||||
const f = try std.fs.cwd().openFile("/tmp/ghostty_bench_data", .{});
|
||||
defer f.close();
|
||||
const r = f.reader();
|
||||
const TerminalStream = terminal.Stream(*NewTerminalHandler);
|
||||
var t = try terminalnew.Terminal.init(alloc, .{
|
||||
.cols = @intCast(args.@"terminal-cols"),
|
||||
.rows = @intCast(args.@"terminal-rows"),
|
||||
});
|
||||
var handler: NewTerminalHandler = .{ .t = &t };
|
||||
var stream: TerminalStream = .{ .handler = &handler };
|
||||
try benchSimd(r, &stream, buf);
|
||||
return;
|
||||
}
|
||||
|
||||
const seed: u64 = if (args.seed >= 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);
|
||||
}
|
||||
};
|
||||
|
@ -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}"
|
||||
|
@ -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);
|
||||
}
|
||||
}
|
||||
}
|
@ -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,
|
||||
};
|
||||
|
@ -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"),
|
||||
};
|
||||
|
@ -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");
|
||||
|
@ -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);
|
||||
}
|
||||
}
|
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
@ -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);
|
||||
}
|
@ -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);
|
||||
}
|
File diff suppressed because it is too large
Load Diff
@ -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 }));
|
||||
}
|
@ -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
|
||||
};
|
@ -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);
|
||||
}
|
@ -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);
|
||||
}
|
||||
}
|
@ -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:<red>/<green>/<blue>
|
||||
///
|
||||
/// <red>, <green>, <blue> := h | hh | hhh | hhhh
|
||||
///
|
||||
/// where `h` is a single hexadecimal digit.
|
||||
///
|
||||
/// 2. rgbi:<red>/<green>/<blue>
|
||||
///
|
||||
/// where <red>, <green>, and <blue> 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"));
|
||||
}
|
@ -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.
|
||||
_,
|
||||
};
|
@ -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);
|
||||
}
|
@ -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 },
|
||||
};
|
@ -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());
|
||||
}
|
@ -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");
|
@ -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());
|
||||
}
|
@ -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",
|
||||
}
|
||||
}
|
@ -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;
|
||||
|
||||
// <ESC>_Gi=31,s=1,v=1,a=q,t=d,f=24;AAAA<ESC>\
|
||||
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, .{});
|
||||
}
|
@ -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);
|
||||
}
|
@ -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(),
|
||||
);
|
||||
}
|
Binary file not shown.
Before Width: | Height: | Size: 86 B |
@ -1 +0,0 @@
|
||||
DRoeCxgcCxcjEh4qDBgkCxcjChYiCxcjCRclBRMhBxIXHysvTVNRbHJwcXB2Li0zCBYXEyEiCxkaDBobChcbCBUZDxsnBBAcEBwoChYiCxcjDBgkDhwqBxUjDBccm6aqy9HP1NrYzs3UsK+2IjAxCBYXCBYXBxUWFBoaDxUVICYqIyktERcZDxUXDxUVEhgYDhUTCxIQGh8XusC4zM7FvL61q6elmZWTTVtcDBobDRscCxkaKS8vaW9vxMnOur/EiY+RaW5wICYmW2FhfYOBQEZEnqSc4ebeqauilZaOsa2rm5eVcH5/GigpChgZCBYX0NHP3d7c3tzbx8XExsTEvry8wL241dLN0tDF0tDF29nM4d/StbKpzMrAUk5DZmJXeYSGKTU3ER0fDRkb1tfVysvJ0tDPsa+tr6ytop+gmZaRqaahuritw8G2urirqKaZiYZ9paKZZmJXamZbOkZIDhocBxMVBBASxMDBtrKzqqanoZ2ejYeLeHF2eXFvhn58npePta6ml5CKgXp0W1hPaWZdZWdSYmRPFiYADR0AFCQAEyMAt7O0lJCRf3t8eHR1Zl9kY1xhYVpYbGRieXJqeHFpdW1oc2tmcG1kX1xTbW9ajY96jp55kaF8kKB7kaF8sK6rcnFtX11cXFpZW1pWWFdTXVpTXltUaGJgY11bY11da2Vla25dam1ccHtTnqmBorVtp7pypLdvobRsh4aCaGdjWFZVXFpZYWBcZ2ZiaGVeZGFaY11bYlxaV1FRZ2FhdHdmbG9egItjo66GpLdvq752rL93rsF5kpKIZ2ddWFxTW19WbnZdipJ6cnhaaW9RaGhgV1ZPY2Jga2poanFQd35dk6Vpn7B0oLFvorNxm6xqmKlnv760enpwVlpRW19Wc3til5+Hl55/k5p7iIiAcnJqd3Z0bm1rcHdWh45tipxgladrkaJglKVjkaJgkqNh09DJiYZ/YmZdY2deeYZYjJlrj51ijpxhztHClJaIdHNvdHNvanNHi5RpmaxnjKBbmqhrmadqkJ5hi5lcxsO8jImCaGtiYmZdg5Bikp9xjJpfjpxh1djJqq2eamllZ2Zid4BVmKF2kqZhh5tWlaNmlaNmjpxfjJpdw729rqiodnZ0cHBuiplij55nj6FVjJ5SzdC9t7qncW1sXlpZh45iqbCEmKllmapmmqlqnq1unaxtoK9w
|
File diff suppressed because one or more lines are too long
@ -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());
|
||||
}
|
@ -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));
|
||||
}
|
@ -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").?);
|
||||
}
|
File diff suppressed because it is too large
Load Diff
@ -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;
|
||||
}
|
@ -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());
|
||||
}
|
@ -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
|
@ -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"));
|
||||
}
|
@ -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()) |_| {}
|
||||
}
|
@ -1,5 +0,0 @@
|
||||
pub usingnamespace @import("simdvt/parser.zig");
|
||||
|
||||
test {
|
||||
@import("std").testing.refAllDecls(@This());
|
||||
}
|
File diff suppressed because it is too large
Load Diff
@ -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;
|
||||
}
|
||||
}
|
@ -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"));
|
||||
}
|
Reference in New Issue
Block a user