mirror of
https://github.com/ghostty-org/ghostty.git
synced 2025-07-24 04:36:10 +03:00
789 lines
22 KiB
Zig
789 lines
22 KiB
Zig
//! Kitty graphics protocol support.
|
|
//!
|
|
//! Documentation:
|
|
//! https://sw.kovidgoyal.net/kitty/graphics-protocol
|
|
|
|
const std = @import("std");
|
|
const Allocator = std.mem.Allocator;
|
|
const ArenaAllocator = std.heap.ArenaAllocator;
|
|
|
|
const KV = std.StringHashMapUnmanaged([]const u8);
|
|
|
|
/// Command parser parses the Kitty graphics protocol escape sequence.
|
|
pub const CommandParser = struct {
|
|
/// General purpose allocator used for long-lived allocations. The
|
|
/// data list is long-lived.
|
|
alloc: Allocator,
|
|
|
|
/// The arena is used for allocations that only exist for the lifetime
|
|
/// of the parser. This is used for the KV data at the time of writing.
|
|
arena: ArenaAllocator,
|
|
|
|
kv: KV = .{},
|
|
data: std.ArrayListUnmanaged(u8) = .{},
|
|
data_i: usize = 0,
|
|
value_ptr: *[]const u8 = undefined,
|
|
state: State = .control_key,
|
|
|
|
const State = enum {
|
|
control_key,
|
|
control_value,
|
|
data,
|
|
};
|
|
|
|
pub fn init(alloc: Allocator) CommandParser {
|
|
return .{
|
|
.alloc = alloc,
|
|
.arena = ArenaAllocator.init(alloc),
|
|
};
|
|
}
|
|
|
|
pub fn deinit(self: *CommandParser) void {
|
|
// We don't free the hash map or array list because its in the arena
|
|
self.arena.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.
|
|
'=' => {
|
|
// We need to copy the key into the arena so that the
|
|
// pointer is stable.
|
|
const alloc = self.arena.allocator();
|
|
const gop = try self.kv.getOrPut(alloc, try alloc.dupe(
|
|
u8,
|
|
self.data.items[self.data_i..],
|
|
));
|
|
|
|
self.state = .control_value;
|
|
self.value_ptr = gop.value_ptr;
|
|
self.data_i = self.data.items.len;
|
|
},
|
|
|
|
else => try self.data.append(self.arena.allocator(), c),
|
|
},
|
|
|
|
.control_value => switch (c) {
|
|
// ',' means we're moving to another kv
|
|
',' => {
|
|
try self.finishValue();
|
|
self.state = .control_key;
|
|
},
|
|
|
|
// ';' means we're moving to the data
|
|
';' => {
|
|
try self.finishValue();
|
|
self.state = .data;
|
|
},
|
|
|
|
else => try self.data.append(self.arena.allocator(), c),
|
|
},
|
|
|
|
.data => try self.data.append(self.arena.allocator(), 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.
|
|
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 => 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(),
|
|
|
|
// 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 str = self.kv.get("a") orelse break :action 't';
|
|
if (str.len != 1) return error.InvalidFormat;
|
|
break :action str[0];
|
|
};
|
|
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")) |str| quiet: {
|
|
break :quiet switch (try std.fmt.parseInt(u32, str, 10)) {
|
|
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: {
|
|
// This is not the most efficient thing to do but it's easy
|
|
// and we can always optimize this later. Images are not super
|
|
// common, especially large ones.
|
|
break :data try self.alloc.dupe(u8, self.data.items[self.data_i..]);
|
|
},
|
|
};
|
|
}
|
|
|
|
fn finishValue(self: *CommandParser) !void {
|
|
self.value_ptr.* = try self.arena.allocator().dupe(
|
|
u8,
|
|
self.data.items[self.data_i..self.data.items.len],
|
|
);
|
|
self.data_i = self.data.items.len;
|
|
}
|
|
};
|
|
|
|
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,
|
|
};
|
|
|
|
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
|
|
};
|
|
|
|
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")) |str| {
|
|
const v = try std.fmt.parseInt(u32, str, 10);
|
|
result.format = switch (v) {
|
|
24 => .rgb,
|
|
32 => .rgba,
|
|
100 => .png,
|
|
else => return error.InvalidFormat,
|
|
};
|
|
}
|
|
|
|
if (kv.get("t")) |str| {
|
|
if (str.len != 1) return error.InvalidFormat;
|
|
result.medium = switch (str[0]) {
|
|
'd' => .direct,
|
|
'f' => .file,
|
|
't' => .temporary_file,
|
|
's' => .shared_memory,
|
|
else => return error.InvalidFormat,
|
|
};
|
|
}
|
|
|
|
if (kv.get("s")) |str| {
|
|
result.width = try std.fmt.parseInt(u32, str, 10);
|
|
}
|
|
|
|
if (kv.get("v")) |str| {
|
|
result.height = try std.fmt.parseInt(u32, str, 10);
|
|
}
|
|
|
|
if (kv.get("S")) |str| {
|
|
result.size = try std.fmt.parseInt(u32, str, 10);
|
|
}
|
|
|
|
if (kv.get("O")) |str| {
|
|
result.offset = try std.fmt.parseInt(u32, str, 10);
|
|
}
|
|
|
|
if (kv.get("i")) |str| {
|
|
result.image_id = try std.fmt.parseInt(u32, str, 10);
|
|
}
|
|
|
|
if (kv.get("I")) |str| {
|
|
result.image_number = try std.fmt.parseInt(u32, str, 10);
|
|
}
|
|
|
|
if (kv.get("p")) |str| {
|
|
result.placement_id = try std.fmt.parseInt(u32, str, 10);
|
|
}
|
|
|
|
if (kv.get("o")) |str| {
|
|
if (str.len != 1) return error.InvalidFormat;
|
|
result.compression = switch (str[0]) {
|
|
'z' => .zlib_deflate,
|
|
else => return error.InvalidFormat,
|
|
};
|
|
}
|
|
|
|
if (kv.get("m")) |str| {
|
|
const v = try std.fmt.parseInt(u32, str, 10);
|
|
result.more_chunks = v > 0;
|
|
}
|
|
|
|
return result;
|
|
}
|
|
};
|
|
|
|
pub const Display = struct {
|
|
image_id: u32 = 0, // i
|
|
image_number: u32 = 0, // I
|
|
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: u32 = 0, // z
|
|
|
|
pub const CursorMovement = enum {
|
|
after, // 0
|
|
none, // 1
|
|
};
|
|
|
|
fn parse(kv: KV) !Display {
|
|
var result: Display = .{};
|
|
|
|
if (kv.get("i")) |str| {
|
|
result.image_id = try std.fmt.parseInt(u32, str, 10);
|
|
}
|
|
|
|
if (kv.get("I")) |str| {
|
|
result.image_number = try std.fmt.parseInt(u32, str, 10);
|
|
}
|
|
|
|
if (kv.get("x")) |str| {
|
|
result.x = try std.fmt.parseInt(u32, str, 10);
|
|
}
|
|
|
|
if (kv.get("y")) |str| {
|
|
result.y = try std.fmt.parseInt(u32, str, 10);
|
|
}
|
|
|
|
if (kv.get("w")) |str| {
|
|
result.width = try std.fmt.parseInt(u32, str, 10);
|
|
}
|
|
|
|
if (kv.get("h")) |str| {
|
|
result.height = try std.fmt.parseInt(u32, str, 10);
|
|
}
|
|
|
|
if (kv.get("X")) |str| {
|
|
result.x_offset = try std.fmt.parseInt(u32, str, 10);
|
|
}
|
|
|
|
if (kv.get("Y")) |str| {
|
|
result.y_offset = try std.fmt.parseInt(u32, str, 10);
|
|
}
|
|
|
|
if (kv.get("c")) |str| {
|
|
result.columns = try std.fmt.parseInt(u32, str, 10);
|
|
}
|
|
|
|
if (kv.get("r")) |str| {
|
|
result.rows = try std.fmt.parseInt(u32, str, 10);
|
|
}
|
|
|
|
if (kv.get("C")) |str| {
|
|
if (str.len != 1) return error.InvalidFormat;
|
|
result.cursor_movement = switch (str[0]) {
|
|
'0' => .after,
|
|
'1' => .none,
|
|
else => return error.InvalidFormat,
|
|
};
|
|
}
|
|
|
|
if (kv.get("U")) |str| {
|
|
if (str.len != 1) return error.InvalidFormat;
|
|
result.virtual_placement = switch (str[0]) {
|
|
'0' => false,
|
|
'1' => true,
|
|
else => return error.InvalidFormat,
|
|
};
|
|
}
|
|
|
|
if (kv.get("z")) |str| {
|
|
result.z = try std.fmt.parseInt(u32, str, 10);
|
|
}
|
|
|
|
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")) |str| {
|
|
result.x = try std.fmt.parseInt(u32, str, 10);
|
|
}
|
|
|
|
if (kv.get("y")) |str| {
|
|
result.y = try std.fmt.parseInt(u32, str, 10);
|
|
}
|
|
|
|
if (kv.get("c")) |str| {
|
|
result.create_frame = try std.fmt.parseInt(u32, str, 10);
|
|
}
|
|
|
|
if (kv.get("r")) |str| {
|
|
result.edit_frame = try std.fmt.parseInt(u32, str, 10);
|
|
}
|
|
|
|
if (kv.get("z")) |str| {
|
|
result.gap_ms = try std.fmt.parseInt(u32, str, 10);
|
|
}
|
|
|
|
if (kv.get("X")) |str| {
|
|
const v = try std.fmt.parseInt(u32, str, 10);
|
|
result.composition_mode = switch (v) {
|
|
0 => .alpha_blend,
|
|
1 => .overwrite,
|
|
else => return error.InvalidFormat,
|
|
};
|
|
}
|
|
|
|
if (kv.get("Y")) |str| {
|
|
result.background = @bitCast(try std.fmt.parseInt(u32, str, 10));
|
|
}
|
|
|
|
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")) |str| {
|
|
result.frame = try std.fmt.parseInt(u32, str, 10);
|
|
}
|
|
|
|
if (kv.get("r")) |str| {
|
|
result.edit_frame = try std.fmt.parseInt(u32, str, 10);
|
|
}
|
|
|
|
if (kv.get("x")) |str| {
|
|
result.x = try std.fmt.parseInt(u32, str, 10);
|
|
}
|
|
|
|
if (kv.get("y")) |str| {
|
|
result.y = try std.fmt.parseInt(u32, str, 10);
|
|
}
|
|
|
|
if (kv.get("w")) |str| {
|
|
result.width = try std.fmt.parseInt(u32, str, 10);
|
|
}
|
|
|
|
if (kv.get("h")) |str| {
|
|
result.height = try std.fmt.parseInt(u32, str, 10);
|
|
}
|
|
|
|
if (kv.get("X")) |str| {
|
|
result.left_edge = try std.fmt.parseInt(u32, str, 10);
|
|
}
|
|
|
|
if (kv.get("Y")) |str| {
|
|
result.top_edge = try std.fmt.parseInt(u32, str, 10);
|
|
}
|
|
|
|
if (kv.get("C")) |str| {
|
|
const v = try std.fmt.parseInt(u32, str, 10);
|
|
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")) |str| {
|
|
const v = try std.fmt.parseInt(u32, str, 10);
|
|
result.action = switch (v) {
|
|
0 => .invalid,
|
|
1 => .stop,
|
|
2 => .run_wait,
|
|
3 => .run,
|
|
else => return error.InvalidFormat,
|
|
};
|
|
}
|
|
|
|
if (kv.get("r")) |str| {
|
|
result.frame = try std.fmt.parseInt(u32, str, 10);
|
|
}
|
|
|
|
if (kv.get("z")) |str| {
|
|
result.gap_ms = try std.fmt.parseInt(u32, str, 10);
|
|
}
|
|
|
|
if (kv.get("c")) |str| {
|
|
result.current_frame = try std.fmt.parseInt(u32, str, 10);
|
|
}
|
|
|
|
if (kv.get("v")) |str| {
|
|
result.loops = try std.fmt.parseInt(u32, str, 10);
|
|
}
|
|
|
|
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
|
|
count: 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: u32 = 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: u32 = 0, // z
|
|
},
|
|
|
|
fn parse(kv: KV) !Delete {
|
|
const what: u8 = what: {
|
|
const str = kv.get("d") orelse break :what 'a';
|
|
if (str.len != 1) return error.InvalidFormat;
|
|
break :what str[0];
|
|
};
|
|
|
|
return switch (what) {
|
|
'a', 'A' => .{ .all = what == 'A' },
|
|
|
|
'i', 'I' => blk: {
|
|
var result: Delete = .{ .id = .{ .delete = what == 'I' } };
|
|
if (kv.get("i")) |str| {
|
|
result.id.image_id = try std.fmt.parseInt(u32, str, 10);
|
|
}
|
|
if (kv.get("p")) |str| {
|
|
result.id.placement_id = try std.fmt.parseInt(u32, str, 10);
|
|
}
|
|
|
|
break :blk result;
|
|
},
|
|
|
|
'n', 'N' => blk: {
|
|
var result: Delete = .{ .newest = .{ .delete = what == 'N' } };
|
|
if (kv.get("I")) |str| {
|
|
result.newest.count = try std.fmt.parseInt(u32, str, 10);
|
|
}
|
|
if (kv.get("p")) |str| {
|
|
result.newest.placement_id = try std.fmt.parseInt(u32, str, 10);
|
|
}
|
|
|
|
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")) |str| {
|
|
result.intersect_cell.x = try std.fmt.parseInt(u32, str, 10);
|
|
}
|
|
if (kv.get("y")) |str| {
|
|
result.intersect_cell.y = try std.fmt.parseInt(u32, str, 10);
|
|
}
|
|
|
|
break :blk result;
|
|
},
|
|
|
|
'q', 'Q' => blk: {
|
|
var result: Delete = .{ .intersect_cell_z = .{ .delete = what == 'Q' } };
|
|
if (kv.get("x")) |str| {
|
|
result.intersect_cell_z.x = try std.fmt.parseInt(u32, str, 10);
|
|
}
|
|
if (kv.get("y")) |str| {
|
|
result.intersect_cell_z.y = try std.fmt.parseInt(u32, str, 10);
|
|
}
|
|
if (kv.get("z")) |str| {
|
|
result.intersect_cell_z.z = try std.fmt.parseInt(u32, str, 10);
|
|
}
|
|
|
|
break :blk result;
|
|
},
|
|
|
|
'x', 'X' => blk: {
|
|
var result: Delete = .{ .column = .{ .delete = what == 'X' } };
|
|
if (kv.get("x")) |str| {
|
|
result.column.x = try std.fmt.parseInt(u32, str, 10);
|
|
}
|
|
|
|
break :blk result;
|
|
},
|
|
|
|
'y', 'Y' => blk: {
|
|
var result: Delete = .{ .row = .{ .delete = what == 'Y' } };
|
|
if (kv.get("y")) |str| {
|
|
result.row.y = try std.fmt.parseInt(u32, str, 10);
|
|
}
|
|
|
|
break :blk result;
|
|
},
|
|
|
|
'z', 'Z' => blk: {
|
|
var result: Delete = .{ .z = .{ .delete = what == 'Z' } };
|
|
if (kv.get("z")) |str| {
|
|
result.z.z = try std.fmt.parseInt(u32, str, 10);
|
|
}
|
|
|
|
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);
|
|
}
|