terminal/snapshot: use lib.Enum enums where possible

This commit is contained in:
Mitchell Hashimoto
2026-07-30 12:58:02 -07:00
parent 86ec146334
commit b867a0f59e
7 changed files with 370 additions and 560 deletions

View File

@@ -8,6 +8,7 @@ const ansi = @import("ansi.zig");
const charsets = @import("charsets.zig");
const fastmem = @import("../fastmem.zig");
const kitty = @import("kitty.zig");
const lib = @import("lib.zig");
const sgr = @import("sgr.zig");
const tripwire = @import("../tripwire.zig");
const unicode = @import("../unicode/main.zig");
@@ -114,7 +115,12 @@ pub const SemanticPrompt = struct {
.click = .none,
};
pub const SemanticClick = union(enum) {
pub const SemanticClickKind = lib.Enum(
lib.target,
&.{ "none", "click_events", "cl" },
);
pub const SemanticClick = union(SemanticClickKind) {
none,
click_events: osc.semantic_prompt.ClickEvents,
cl: osc.semantic_prompt.Click,

View File

@@ -99,8 +99,11 @@ pub const ModifyKeyFormat = lib.Enum(
/// 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
};
pub const ProtectedMode = lib.Enum(
lib.target,
&.{
"off",
"iso", // ESC V, W
"dec", // CSI Ps " q
},
);

View File

@@ -1,15 +1,25 @@
const lib = @import("lib.zig");
/// The visual style of the cursor. Whether or not it blinks
/// is determined by mode 12 (modes.zig). This mode is synchronized
/// with CSI q, the same as xterm.
pub const Style = enum {
bar, // DECSCUSR 5, 6
block, // DECSCUSR 1, 2
underline, // DECSCUSR 3, 4
///
/// Bar, block, and underline correspond to DECSCUSR 5/6, 1/2, and 3/4.
/// Hollow block is Ghostty-specific and is reported as DECSCUSR 1 or 2.
pub const Style = lib.Enum(lib.target, &.{
// DECSCUSR 5, 6
"bar",
/// The cursor styles below aren't known by DESCUSR and are custom
/// implemented in Ghostty. They are reported as some standard style
/// if requested, though.
/// Hollow block cursor. This is a block cursor with the center empty.
/// Reported as DECSCUSR 1 or 2 (block).
block_hollow,
};
// DECSCUSR 1, 2
"block",
// DECSCUSR 3, 4
"underline",
// The cursor styles below aren't known by DESCUSR and are custom
// implemented in Ghostty. They are reported as some standard style
// if requested, though.
// Hollow block cursor. This is a block cursor with the center empty.
// Reported as DECSCUSR 1 or 2 (block).
"block_hollow",
});

View File

@@ -1,6 +1,7 @@
//! https://gitlab.freedesktop.org/Per_Bothner/specifications/blob/master/proposals/semantic-prompts.md
const std = @import("std");
const lib = @import("../../lib.zig");
const Parser = @import("../../osc.zig").Parser;
const OSCCommand = @import("../../osc.zig").Command;
const string_encoding = @import("../../../os/string_encoding.zig");
@@ -68,7 +69,10 @@ pub const Command = struct {
// See https://github.com/ghostty-org/ghostty/issues/10865 and
// https://github.com/kovidgoyal/kitty/issues/9500
// for further details.
pub const ClickEvents = enum { absolute, relative };
pub const ClickEvents = lib.Enum(
lib.target,
&.{ "absolute", "relative" },
);
pub const Option = enum {
aid,
@@ -199,7 +203,7 @@ pub const Option = enum {
return switch (self) {
.aid => value,
.cl => .init(value),
.cl => parseClick(value),
.prompt_kind => if (value.len == 1) PromptKind.init(value[0]) else null,
.err => value,
.redraw => if (std.mem.eql(u8, value, "0"))
@@ -234,42 +238,50 @@ pub const Option = enum {
/// The `cl` option specifies what kind of cursor key sequences are handled
/// by the application for click-to-move-cursor functionality.
pub const Click = enum {
/// Value: "line". Allows motion within a single input line using standard
/// left/right arrow escape sequences. Only a single left/right sequence
/// should be emitted for double-width characters.
line,
///
/// `line` allows movement within one input line. `multiple` allows movement
/// across lines with left/right sequences. The two vertical modes additionally
/// allow up/down sequences, with `smart_vertical` permitting editor-aware
/// column clamping.
pub const Click = lib.Enum(
lib.target,
&.{
// Value: "line". Allows motion within a single input line using
// standard left/right arrow escape sequences. Only a single left/right
// sequence should be emitted for double-width characters.
"line",
/// Value: "m". Allows movement between different lines in the same group,
/// but only using left/right arrow escape sequences.
multiple,
// Value: "m". Allows movement between different lines in the same
// group, but only using left/right arrow escape sequences.
"multiple",
/// Value: "v". Like `multiple` but cursor up/down should be used. The
/// terminal should be conservative when moving between lines: move the
/// cursor left to the start of line, emit the needed up/down sequences,
/// then move the cursor right to the clicked destination.
conservative_vertical,
// Value: "v". Like `multiple` but cursor up/down should be used. The
// terminal should be conservative when moving between lines: move the
// cursor left to the start of line, emit the needed up/down sequences,
// then move the cursor right to the clicked destination.
"conservative_vertical",
/// Value: "w". Like `conservative_vertical` but specifies that there are
/// no spurious spaces at the end of the line, and the application editor
/// handles "smart vertical movement" (moving 2 lines up from position 20,
/// where the intermediate line is 15 chars wide and the destination is
/// 18 chars wide, ends at position 18).
smart_vertical,
// Value: "w". Like `conservative_vertical` but specifies that there
// are no spurious spaces at the end of the line, and the application
// editor handles "smart vertical movement" (moving 2 lines up from
// position 20, where the intermediate line is 15 chars wide and the
// destination is 18 chars wide, ends at position 18).
"smart_vertical",
},
);
pub fn init(value: []const u8) ?Click {
return if (value.len == 1) switch (value[0]) {
'm' => .multiple,
'v' => .conservative_vertical,
'w' => .smart_vertical,
else => null,
} else if (std.mem.eql(
u8,
value,
"line",
)) .line else null;
}
};
fn parseClick(value: []const u8) ?Click {
return if (value.len == 1) switch (value[0]) {
'm' => .multiple,
'v' => .conservative_vertical,
'w' => .smart_vertical,
else => null,
} else if (std.mem.eql(
u8,
value,
"line",
)) .line else null;
}
pub const PromptKind = enum {
initial,

View File

@@ -149,11 +149,7 @@ pub fn encode(
const row_header: RowHeader = .{
.wrap = row.wrap,
.wrap_continuation = row.wrap_continuation,
.semantic_prompt = switch (row.semantic_prompt) {
.none => .none,
.prompt => .prompt,
.prompt_continuation => .prompt_continuation,
},
.semantic_prompt = row.semantic_prompt,
};
try writer.writeByte(@bitCast(row_header));
@@ -262,20 +258,24 @@ pub fn decode(
hyperlink_remap: *const HyperlinkRemap,
) DecodeError!void {
for (0..page.size.rows) |y| {
const row_header: RowHeader = @bitCast(try reader.takeByte());
const row_raw = try reader.takeByte();
// Validate the enum field before bitcasting into the packed native
// representation. The remaining value is checked through its named
// padding field below.
const semantic_prompt_raw: u2 = @truncate(row_raw >> 2);
_ = std.enums.fromInt(
TerminalRow.SemanticPrompt,
semantic_prompt_raw,
) orelse return error.InvalidRow;
const row_header: RowHeader = @bitCast(row_raw);
if (row_header._padding != 0) return error.InvalidRow;
const semantic_prompt: TerminalRow.SemanticPrompt =
switch (row_header.semantic_prompt) {
.none => .none,
.prompt => .prompt,
.prompt_continuation => .prompt_continuation,
.invalid => return error.InvalidRow,
};
const row = page.getRow(y);
row.wrap = row_header.wrap;
row.wrap_continuation = row_header.wrap_continuation;
row.semantic_prompt = semantic_prompt;
row.semantic_prompt = row_header.semantic_prompt;
const cells = page.getCells(row);
for (cells, 0..) |*cell, x| {
@@ -396,15 +396,8 @@ pub fn decode(
const RowHeader = packed struct(u8) {
wrap: bool = false,
wrap_continuation: bool = false,
semantic_prompt: SemanticPrompt = .none,
semantic_prompt: TerminalRow.SemanticPrompt = .none,
_padding: u4 = 0,
const SemanticPrompt = enum(u2) {
none = 0,
prompt = 1,
prompt_continuation = 2,
invalid = 3,
};
};
/// The fixed fields that precede a cell's grapheme suffix codepoints.
@@ -451,14 +444,9 @@ const CellHeader = struct {
self: CellHeader,
writer: *std.Io.Writer,
) std.Io.Writer.Error!void {
const semantic_content: Flags.SemanticContent = switch (self.semantic_content) {
.output => .output,
.input => .input,
.prompt => .prompt,
};
const flags: Flags = .{
.protected = self.protected,
.semantic_content = semantic_content,
.semantic_content = self.semantic_content,
};
// 0 1 2 3 4 6 8 12 16
@@ -488,14 +476,18 @@ const CellHeader = struct {
try reader.takeByte(),
) orelse return error.InvalidCell;
const flags: Flags = @bitCast(try reader.takeByte());
const flags_raw = try reader.takeByte();
// As with the row header, validate the exhaustive native enum before
// bitcasting the complete packed byte.
const semantic_content_raw: u2 = @truncate(flags_raw >> 1);
_ = std.enums.fromInt(
TerminalCell.SemanticContent,
semantic_content_raw,
) orelse return error.InvalidCell;
const flags: Flags = @bitCast(flags_raw);
if (flags._padding != 0) return error.InvalidCell;
const semantic_content: TerminalCell.SemanticContent = switch (flags.semantic_content) {
.output => .output,
.input => .input,
.prompt => .prompt,
.invalid => return error.InvalidCell,
};
// This byte is reserved so the IDs and content value remain naturally
// aligned within the fixed cell header.
@@ -505,7 +497,7 @@ const CellHeader = struct {
.content_kind = content_kind,
.width = width,
.protected = flags.protected,
.semantic_content = semantic_content,
.semantic_content = flags.semantic_content,
.style_id = try io.readInt(reader, TerminalStyleId),
.hyperlink_id = try io.readInt(reader, TerminalHyperlinkId),
.value = @bitCast(try io.readInt(reader, u32)),
@@ -547,14 +539,7 @@ const CellHeader = struct {
const Flags = packed struct(u8) {
protected: bool = false,
semantic_content: SemanticContent = .output,
semantic_content: TerminalCell.SemanticContent = .output,
_padding: u5 = 0,
const SemanticContent = enum(u2) {
output = 0,
input = 1,
prompt = 2,
invalid = 3,
};
};
};

View File

@@ -86,7 +86,7 @@ pub const Header = struct {
}
/// Identifies the previously encoded screen that owns this history.
key: screen.Key,
key: TerminalScreenKey,
/// Number of complete PAGE records immediately following this record.
page_count: u32,
@@ -102,7 +102,7 @@ pub const Header = struct {
self: Header,
writer: *std.Io.Writer,
) std.Io.Writer.Error!void {
try io.writeInt(writer, u16, @intFromEnum(self.key));
try io.writeInt(writer, u16, @intCast(@intFromEnum(self.key)));
try io.writeInt(writer, u32, self.page_count);
try io.writeInt(writer, u64, self.total_rows);
try io.writeInt(writer, u16, self.screen_overlap_rows);
@@ -112,9 +112,13 @@ pub const Header = struct {
/// Decode and validate the fixed HISTORY payload.
pub fn decode(reader: *std.Io.Reader) Header.DecodeError!Header {
const raw = try io.readInt(reader, u16);
const Tag = @typeInfo(TerminalScreenKey).@"enum".tag_type;
const value = std.math.cast(Tag, raw) orelse
return error.InvalidKey;
const key = std.enums.fromInt(
screen.Key,
try io.readInt(reader, u16),
TerminalScreenKey,
value,
) orelse return error.InvalidKey;
return .{
.key = key,
@@ -186,10 +190,7 @@ pub fn encode(
};
const header: Header = .{
.key = switch (key) {
.primary => .primary,
.alternate => .alternate,
},
.key = key,
.page_count = std.math.cast(
u32,
page_count,
@@ -265,11 +266,7 @@ pub fn decode(
break :header header;
};
const decoded_key: TerminalScreenKey = switch (header.key) {
.primary => .primary,
.alternate => .alternate,
};
if (decoded_key != expected_key) return error.UnexpectedScreenKey;
if (header.key != expected_key) return error.UnexpectedScreenKey;
// A freshly restored SCREEN may carry overlap inside its first page, but
// cannot already contain a complete historical page before that boundary.
@@ -437,7 +434,7 @@ test "HISTORY encodes newest first and restores complete history" {
try std.testing.expectEqual(record.Tag.history, history_record.header.tag);
const header = try Header.decode(history_record.payloadReader());
try history_record.finish();
try std.testing.expectEqual(screen.Key.primary, header.key);
try std.testing.expectEqual(TerminalScreenKey.primary, header.key);
try std.testing.expectEqual(@as(u32, 2), header.page_count);
try std.testing.expectEqual(
@as(u16, active_top.y),

View File

@@ -193,9 +193,9 @@
//! bit 8 +---------------------------+
//! ```
//!
//! All enum values and bit assignments below are snapshot-version registries,
//! not native enum ordinals. Changing any of them requires a snapshot version
//! bump.
//! Native enum values used below are stable terminal registries. Their wire
//! widths are fixed independently by this payload. Changing those values or
//! any packed bit assignment requires a snapshot version bump.
const std = @import("std");
const build_options = @import("terminal_options");
@@ -209,6 +209,7 @@ const terminal_ansi = @import("../ansi.zig");
const terminal_charsets = @import("../charsets.zig");
const terminal_hyperlink = @import("../hyperlink.zig");
const terminal_kitty = @import("../kitty.zig");
const terminal_osc = @import("../osc.zig");
const terminal_page = @import("../page.zig");
const TerminalPageList = @import("../PageList.zig");
const TerminalScreen = @import("../Screen.zig");
@@ -221,7 +222,6 @@ const TerminalStyle = terminal_style.Style;
/// Errors possible while encoding fixed SCREEN payload fields.
const PayloadEncodeError = std.Io.Writer.Error || error{
InvalidCursorFlags,
InvalidSemanticContent,
InvalidCharsetState,
InvalidKittyKeyboardIndex,
InvalidKittyKeyboardFlags,
@@ -349,7 +349,7 @@ pub fn decode(
defer if (cursor_hyperlink) |*value| value.deinit(alloc);
try record_reader.finish();
const key = header.key.terminal();
const key = header.key;
if (header.page_count == 0) return error.InvalidPageCount;
// Dimensions are terminal-wide state supplied by the enclosing snapshot.
@@ -417,13 +417,12 @@ pub fn decode(
.cursor = .{
.x = header.cursor_x,
.y = header.cursor_y,
.cursor_style = header.cursor_style.terminal(),
.cursor_style = header.cursor_style,
.pending_wrap = header.cursor_flags.pending_wrap,
.protected = header.cursor_flags.protected,
.style = header.cursor_pen,
.hyperlink_implicit_id = header.hyperlink_implicit_id,
.semantic_content = header.cursor_flags.semantic_content
.terminal(),
.semantic_content = header.cursor_flags.semantic_content,
.semantic_content_clear_eol = header.cursor_flags
.semantic_content_clear_eol,
.page_pin = cursor_pin,
@@ -434,12 +433,12 @@ pub fn decode(
value.terminal()
else
null,
.charset = header.charset.terminal(),
.protected_mode = header.protected_mode.terminal(),
.charset = header.charset,
.protected_mode = header.protected_mode,
.kitty_keyboard = header.kitty_keyboard.terminal(),
.semantic_prompt = .{
.seen = false,
.click = header.semantic_click.terminal(),
.click = header.semantic_click,
},
};
};
@@ -527,86 +526,19 @@ const PayloadDecodeError = style.DecodeError || error{
InvalidSavedCursorFlags,
};
/// Identifies which terminal screen the record restores.
pub const Key = enum(u16) {
primary = 0,
alternate = 1,
fn init(value: TerminalScreenKey) Key {
return switch (value) {
.primary => .primary,
.alternate => .alternate,
};
}
fn terminal(self: Key) TerminalScreenKey {
return switch (self) {
.primary => .primary,
.alternate => .alternate,
};
}
};
/// Snapshot registry for the cursor's visual shape.
pub const CursorStyle = enum(u8) {
bar = 0,
block = 1,
underline = 2,
block_hollow = 3,
fn init(value: TerminalScreen.CursorStyle) CursorStyle {
return switch (value) {
.bar => .bar,
.block => .block,
.underline => .underline,
.block_hollow => .block_hollow,
};
}
fn terminal(self: CursorStyle) TerminalScreen.CursorStyle {
return switch (self) {
.bar => .bar,
.block => .block,
.underline => .underline,
.block_hollow => .block_hollow,
};
}
};
/// Flags encoded after the cursor's visual shape.
pub const CursorFlags = packed struct(u8) {
pending_wrap: bool = false,
protected: bool = false,
semantic_content: SemanticContent = .output,
semantic_content: terminal_page.Cell.SemanticContent = .output,
semantic_content_clear_eol: bool = false,
_padding: u3 = 0,
/// Snapshot registry for the cursor's semantic content.
pub const SemanticContent = enum(u2) {
output = 0,
input = 1,
prompt = 2,
invalid = 3,
fn terminal(self: SemanticContent) terminal_page.Cell.SemanticContent {
return switch (self) {
.output => .output,
.input => .input,
.prompt => .prompt,
.invalid => unreachable,
};
}
};
fn init(cursor: *const TerminalScreen.Cursor) CursorFlags {
return .{
.pending_wrap = cursor.pending_wrap,
.protected = cursor.protected,
.semantic_content = switch (cursor.semantic_content) {
.output => .output,
.input => .input,
.prompt => .prompt,
},
.semantic_content = cursor.semantic_content,
.semantic_content_clear_eol = cursor
.semantic_content_clear_eol,
};
@@ -614,166 +546,143 @@ pub const CursorFlags = packed struct(u8) {
/// Decode and validate the cursor flag registry.
pub fn decode(reader: *std.Io.Reader) PayloadDecodeError!CursorFlags {
const result: CursorFlags = @bitCast(try reader.takeByte());
const raw = try reader.takeByte();
const semantic_content_raw: u2 = @truncate(raw >> 2);
_ = std.enums.fromInt(
terminal_page.Cell.SemanticContent,
semantic_content_raw,
) orelse return error.InvalidSemanticContent;
const result: CursorFlags = @bitCast(raw);
if (result._padding != 0) return error.InvalidCursorFlags;
if (result.semantic_content == .invalid) {
return error.InvalidSemanticContent;
}
return result;
}
};
/// The packed charset state used by current and saved cursors.
pub const CharsetState = packed struct(u16) {
g0: Charset = .utf8,
g1: Charset = .utf8,
g2: Charset = .utf8,
g3: Charset = .utf8,
gl: Slot = .g0,
gr: Slot = .g2,
single_shift: SingleShift = .none,
/// The packed wire bits shared by current and saved cursor charset state.
///
/// Native enum values are stable, but their backing type is `c_int` in C ABI
/// builds. The wire therefore stores only checked integer values at its fixed
/// bit widths.
const CharsetBits = packed struct(u16) {
g0: u2 = 0,
g1: u2 = 0,
g2: u2 = 0,
g3: u2 = 0,
gl: u2 = 0,
gr: u2 = 2,
single_shift: u3 = 0,
_padding: u1 = 0,
/// Snapshot registry for one graphical character set.
pub const Charset = enum(u2) {
utf8 = 0,
ascii = 1,
british = 2,
dec_special = 3,
fn terminal(self: Charset) terminal_charsets.Charset {
return switch (self) {
.utf8 => .utf8,
.ascii => .ascii,
.british => .british,
.dec_special => .dec_special,
};
}
};
/// Snapshot registry for a G0 through G3 charset slot.
pub const Slot = enum(u2) {
g0 = 0,
g1 = 1,
g2 = 2,
g3 = 3,
fn terminal(self: Slot) terminal_charsets.Slots {
return switch (self) {
.g0 => .G0,
.g1 => .G1,
.g2 => .G2,
.g3 => .G3,
};
}
};
/// Snapshot registry for the single-shift charset slot.
pub const SingleShift = enum(u3) {
none = 0,
g0 = 1,
g1 = 2,
g2 = 3,
g3 = 4,
_,
fn terminal(self: SingleShift) ?terminal_charsets.Slots {
return switch (self) {
.none => null,
.g0 => .G0,
.g1 => .G1,
.g2 => .G2,
.g3 => .G3,
_ => unreachable,
};
}
};
fn init(value: TerminalScreen.CharsetState) CharsetState {
return .{
.g0 = charset(value.charsets.get(.G0)),
.g1 = charset(value.charsets.get(.G1)),
.g2 = charset(value.charsets.get(.G2)),
.g3 = charset(value.charsets.get(.G3)),
.gl = slot(value.gl),
.gr = slot(value.gr),
.single_shift = if (value.single_shift) |value_slot|
switch (value_slot) {
.G0 => .g0,
.G1 => .g1,
.G2 => .g2,
.G3 => .g3,
}
else
.none,
};
}
fn terminal(self: CharsetState) TerminalScreen.CharsetState {
var result: TerminalScreen.CharsetState = .{
.gl = self.gl.terminal(),
.gr = self.gr.terminal(),
.single_shift = self.single_shift.terminal(),
};
result.charsets.set(.G0, self.g0.terminal());
result.charsets.set(.G1, self.g1.terminal());
result.charsets.set(.G2, self.g2.terminal());
result.charsets.set(.G3, self.g3.terminal());
return result;
}
fn charset(value: terminal_charsets.Charset) Charset {
return switch (value) {
.utf8 => .utf8,
.ascii => .ascii,
.british => .british,
.dec_special => .dec_special,
};
}
fn slot(value: terminal_charsets.Slots) Slot {
return switch (value) {
.G0 => .g0,
.G1 => .g1,
.G2 => .g2,
.G3 => .g3,
};
}
/// Decode and validate the packed charset registry.
pub fn decode(reader: *std.Io.Reader) PayloadDecodeError!CharsetState {
const result: CharsetState = @bitCast(try io.readInt(reader, u16));
if (result._padding != 0) return error.InvalidCharsetState;
switch (result.single_shift) {
.none, .g0, .g1, .g2, .g3 => {},
_ => return error.InvalidCharsetState,
}
return result;
}
};
/// Snapshot registry for selective-erase protection behavior.
pub const ProtectedMode = enum(u8) {
off = 0,
iso = 1,
dec = 2,
fn enumFromInt(comptime T: type, raw: anytype) ?T {
const Tag = @typeInfo(T).@"enum".tag_type;
const value = std.math.cast(Tag, raw) orelse return null;
return std.enums.fromInt(T, value);
}
fn init(value: terminal_ansi.ProtectedMode) ProtectedMode {
return switch (value) {
.off => .off,
.iso => .iso,
.dec => .dec,
};
fn encodeCharsetState(
value: TerminalScreen.CharsetState,
) error{InvalidCharsetState}!u16 {
const g0 = std.math.cast(
u2,
@intFromEnum(value.charsets.get(.G0)),
) orelse return error.InvalidCharsetState;
const g1 = std.math.cast(
u2,
@intFromEnum(value.charsets.get(.G1)),
) orelse return error.InvalidCharsetState;
const g2 = std.math.cast(
u2,
@intFromEnum(value.charsets.get(.G2)),
) orelse return error.InvalidCharsetState;
const g3 = std.math.cast(
u2,
@intFromEnum(value.charsets.get(.G3)),
) orelse return error.InvalidCharsetState;
const gl = std.math.cast(
u2,
@intFromEnum(value.gl),
) orelse return error.InvalidCharsetState;
const gr = std.math.cast(
u2,
@intFromEnum(value.gr),
) orelse return error.InvalidCharsetState;
const single_shift: u3 = if (value.single_shift) |slot| single_shift: {
const raw = std.math.cast(
u2,
@intFromEnum(slot),
) orelse return error.InvalidCharsetState;
break :single_shift @as(u3, raw) + 1;
} else 0;
const bits: CharsetBits = .{
.g0 = g0,
.g1 = g1,
.g2 = g2,
.g3 = g3,
.gl = gl,
.gr = gr,
.single_shift = single_shift,
};
return @bitCast(bits);
}
fn decodeCharsetState(
raw: u16,
) error{InvalidCharsetState}!TerminalScreen.CharsetState {
const bits: CharsetBits = @bitCast(raw);
if (bits._padding != 0 or bits.single_shift > 4) {
return error.InvalidCharsetState;
}
fn terminal(self: ProtectedMode) terminal_ansi.ProtectedMode {
return switch (self) {
.off => .off,
.iso => .iso,
.dec => .dec,
};
}
};
var result: TerminalScreen.CharsetState = .{
.gl = enumFromInt(
terminal_charsets.Slots,
bits.gl,
) orelse return error.InvalidCharsetState,
.gr = enumFromInt(
terminal_charsets.Slots,
bits.gr,
) orelse return error.InvalidCharsetState,
.single_shift = if (bits.single_shift == 0)
null
else
enumFromInt(
terminal_charsets.Slots,
bits.single_shift - 1,
) orelse return error.InvalidCharsetState,
};
result.charsets.set(
.G0,
enumFromInt(
terminal_charsets.Charset,
bits.g0,
) orelse return error.InvalidCharsetState,
);
result.charsets.set(
.G1,
enumFromInt(
terminal_charsets.Charset,
bits.g1,
) orelse return error.InvalidCharsetState,
);
result.charsets.set(
.G2,
enumFromInt(
terminal_charsets.Charset,
bits.g2,
) orelse return error.InvalidCharsetState,
);
result.charsets.set(
.G3,
enumFromInt(
terminal_charsets.Charset,
bits.g3,
) orelse return error.InvalidCharsetState,
);
return result;
}
/// The complete fixed-size Kitty keyboard stack.
pub const KittyKeyboard = struct {
@@ -840,98 +749,31 @@ pub const KittyKeyboard = struct {
}
};
/// Selects the interpretation of the semantic-click value byte.
pub const SemanticClickKind = enum(u8) {
none = 0,
click_events = 1,
cl = 2,
};
/// Decode and validate the semantic-click kind and value bytes.
fn decodeSemanticClick(
reader: *std.Io.Reader,
) PayloadDecodeError!TerminalScreen.SemanticPrompt.SemanticClick {
const kind = enumFromInt(
TerminalScreen.SemanticPrompt.SemanticClickKind,
try reader.takeByte(),
) orelse return error.InvalidSemanticClick;
const value = try reader.takeByte();
/// Snapshot registry for the OSC 133 `click_events` option.
pub const ClickEvents = enum(u8) {
absolute = 0,
relative = 1,
};
/// Snapshot registry for the OSC 133 `cl` option.
pub const Click = enum(u8) {
line = 0,
multiple = 1,
conservative_vertical = 2,
smart_vertical = 3,
};
/// The typed semantic-click kind and value from the fixed header.
pub const SemanticClick = union(SemanticClickKind) {
none,
click_events: ClickEvents,
cl: Click,
fn init(
value: TerminalScreen.SemanticPrompt.SemanticClick,
) SemanticClick {
return switch (value) {
.none => .none,
.click_events => |click_events| .{
.click_events = switch (click_events) {
.absolute => .absolute,
.relative => .relative,
},
},
.cl => |click| .{
.cl = switch (click) {
.line => .line,
.multiple => .multiple,
.conservative_vertical => .conservative_vertical,
.smart_vertical => .smart_vertical,
},
},
};
}
fn terminal(
self: SemanticClick,
) TerminalScreen.SemanticPrompt.SemanticClick {
return switch (self) {
.none => .none,
.click_events => |value| .{ .click_events = switch (value) {
.absolute => .absolute,
.relative => .relative,
} },
.cl => |value| .{ .cl = switch (value) {
.line => .line,
.multiple => .multiple,
.conservative_vertical => .conservative_vertical,
.smart_vertical => .smart_vertical,
} },
};
}
/// Decode and validate the semantic-click kind and value bytes.
pub fn decode(reader: *std.Io.Reader) PayloadDecodeError!SemanticClick {
const kind_raw = try reader.takeByte();
const value_raw = try reader.takeByte();
const kind = std.enums.fromInt(
SemanticClickKind,
kind_raw,
) orelse return error.InvalidSemanticClick;
return switch (kind) {
.none => if (value_raw == 0)
.none
else
error.InvalidSemanticClick,
.click_events => .{ .click_events = std.enums.fromInt(
ClickEvents,
value_raw,
) orelse return error.InvalidSemanticClick },
.cl => .{ .cl = std.enums.fromInt(
Click,
value_raw,
) orelse return error.InvalidSemanticClick },
};
}
};
return switch (kind) {
.none => if (value == 0)
.none
else
error.InvalidSemanticClick,
.click_events => .{ .click_events = enumFromInt(
terminal_osc.semantic_prompt.ClickEvents,
value,
) orelse return error.InvalidSemanticClick },
.cl => .{ .cl = enumFromInt(
terminal_osc.semantic_prompt.Click,
value,
) orelse return error.InvalidSemanticClick },
};
}
/// The optional fixed-size saved cursor state.
pub const SavedCursor = struct {
@@ -946,7 +788,7 @@ pub const SavedCursor = struct {
y: u16,
pen: TerminalStyle,
flags: Flags,
charset: CharsetState,
charset: TerminalScreen.CharsetState,
/// Flags encoded with an optional saved cursor.
pub const Flags = packed struct(u8) {
@@ -973,7 +815,7 @@ pub const SavedCursor = struct {
.pending_wrap = value.pending_wrap,
.origin = value.origin,
},
.charset = .init(value.charset),
.charset = value.charset,
};
}
@@ -985,7 +827,7 @@ pub const SavedCursor = struct {
.protected = self.flags.protected,
.pending_wrap = self.flags.pending_wrap,
.origin = self.flags.origin,
.charset = self.charset.terminal(),
.charset = self.charset,
};
}
@@ -997,17 +839,13 @@ pub const SavedCursor = struct {
if (self.flags._padding != 0) {
return error.InvalidSavedCursorFlags;
}
if (self.charset._padding != 0) return error.InvalidCharsetState;
switch (self.charset.single_shift) {
.none, .g0, .g1, .g2, .g3 => {},
_ => return error.InvalidCharsetState,
}
const charset = try encodeCharsetState(self.charset);
try io.writeInt(writer, u16, self.x);
try io.writeInt(writer, u16, self.y);
try style.encode(self.pen, writer);
try writer.writeByte(@bitCast(self.flags));
try io.writeInt(writer, u16, @bitCast(self.charset));
try io.writeInt(writer, u16, charset);
}
/// Decode and validate one optional saved cursor value.
@@ -1017,7 +855,9 @@ pub const SavedCursor = struct {
.y = try io.readInt(reader, u16),
.pen = try style.decode(reader),
.flags = try Flags.decode(reader),
.charset = try CharsetState.decode(reader),
.charset = try decodeCharsetState(
try io.readInt(reader, u16),
),
};
}
@@ -1047,19 +887,19 @@ pub const Header = struct {
std.debug.assert(len == 45);
}
key: Key,
key: TerminalScreenKey,
/// Complete pages in the minimal suffix covering the active area.
page_count: u16,
cursor_x: u16,
cursor_y: u16,
cursor_style: CursorStyle,
cursor_style: TerminalScreen.CursorStyle,
cursor_flags: CursorFlags,
cursor_pen: TerminalStyle,
hyperlink_implicit_id: u32,
charset: CharsetState,
protected_mode: ProtectedMode,
charset: TerminalScreen.CharsetState,
protected_mode: terminal_ansi.ProtectedMode,
kitty_keyboard: KittyKeyboard,
semantic_click: SemanticClick,
semantic_click: TerminalScreen.SemanticPrompt.SemanticClick,
saved_cursor_present: bool,
/// Initialize the fixed header from one native screen.
@@ -1069,18 +909,18 @@ pub const Header = struct {
page_count: u16,
) Header {
return .{
.key = .init(key),
.key = key,
.page_count = page_count,
.cursor_x = screen.cursor.x,
.cursor_y = screen.cursor.y,
.cursor_style = .init(screen.cursor.cursor_style),
.cursor_style = screen.cursor.cursor_style,
.cursor_flags = .init(&screen.cursor),
.cursor_pen = screen.cursor.style,
.hyperlink_implicit_id = screen.cursor.hyperlink_implicit_id,
.charset = .init(screen.charset),
.protected_mode = .init(screen.protected_mode),
.charset = screen.charset,
.protected_mode = screen.protected_mode,
.kitty_keyboard = .init(screen.kitty_keyboard),
.semantic_click = .init(screen.semantic_prompt.click),
.semantic_click = screen.semantic_prompt.click,
.saved_cursor_present = screen.saved_cursor != null,
};
}
@@ -1094,16 +934,9 @@ pub const Header = struct {
if (self.cursor_flags._padding != 0) {
return error.InvalidCursorFlags;
}
if (self.cursor_flags.semantic_content == .invalid) {
return error.InvalidSemanticContent;
}
// Validate packed charset state.
if (self.charset._padding != 0) return error.InvalidCharsetState;
switch (self.charset.single_shift) {
.none, .g0, .g1, .g2, .g3 => {},
_ => return error.InvalidCharsetState,
}
// Validate and narrow native charset enum values before writing.
const charset = try encodeCharsetState(self.charset);
// Validate the complete Kitty keyboard stack.
if (self.kitty_keyboard.index >= self.kitty_keyboard.flags.len) {
@@ -1116,41 +949,37 @@ pub const Header = struct {
}
// Screen identity and cursor position.
try io.writeInt(writer, u16, @intFromEnum(self.key));
try io.writeInt(writer, u16, @intCast(@intFromEnum(self.key)));
try io.writeInt(writer, u16, self.page_count);
try io.writeInt(writer, u16, self.cursor_x);
try io.writeInt(writer, u16, self.cursor_y);
// Current cursor rendering state.
try writer.writeByte(@intFromEnum(self.cursor_style));
try writer.writeByte(@intCast(@intFromEnum(self.cursor_style)));
try writer.writeByte(@bitCast(self.cursor_flags));
try style.encode(self.cursor_pen, writer);
try io.writeInt(writer, u32, self.hyperlink_implicit_id);
// Charset and selective-erase state.
try io.writeInt(writer, u16, @bitCast(self.charset));
try writer.writeByte(@intFromEnum(self.protected_mode));
try io.writeInt(writer, u16, charset);
try writer.writeByte(@intCast(@intFromEnum(self.protected_mode)));
// Keyboard modes and semantic-click state.
try writer.writeByte(self.kitty_keyboard.index);
for (self.kitty_keyboard.flags) |flags| {
try writer.writeByte(@bitCast(flags));
}
try writer.writeByte(@intCast(@intFromEnum(
std.meta.activeTag(self.semantic_click),
)));
switch (self.semantic_click) {
.none => {
try writer.writeByte(@intFromEnum(SemanticClickKind.none));
try writer.writeByte(0);
},
.click_events => |value| {
try writer.writeByte(
@intFromEnum(SemanticClickKind.click_events),
);
try writer.writeByte(@intFromEnum(value));
},
.cl => |value| {
try writer.writeByte(@intFromEnum(SemanticClickKind.cl));
try writer.writeByte(@intFromEnum(value));
},
.none => try writer.writeByte(0),
.click_events => |value| try writer.writeByte(
@intCast(@intFromEnum(value)),
),
.cl => |value| try writer.writeByte(
@intCast(@intFromEnum(value)),
),
}
// Presence of the optional saved-cursor suffix.
@@ -1160,8 +989,8 @@ pub const Header = struct {
/// Decode and validate the fixed SCREEN payload header.
pub fn decode(reader: *std.Io.Reader) PayloadDecodeError!Header {
// Screen identity and cursor position.
const key = std.enums.fromInt(
Key,
const key = enumFromInt(
TerminalScreenKey,
try io.readInt(reader, u16),
) orelse return error.InvalidKey;
const page_count = try io.readInt(reader, u16);
@@ -1169,8 +998,8 @@ pub const Header = struct {
const cursor_y = try io.readInt(reader, u16);
// Current cursor rendering state.
const cursor_style = std.enums.fromInt(
CursorStyle,
const cursor_style = enumFromInt(
TerminalScreen.CursorStyle,
try reader.takeByte(),
) orelse return error.InvalidCursorStyle;
const cursor_flags = try CursorFlags.decode(reader);
@@ -1178,15 +1007,17 @@ pub const Header = struct {
const hyperlink_implicit_id = try io.readInt(reader, u32);
// Charset and selective-erase state.
const charset = try CharsetState.decode(reader);
const protected_mode = std.enums.fromInt(
ProtectedMode,
const charset = try decodeCharsetState(
try io.readInt(reader, u16),
);
const protected_mode = enumFromInt(
terminal_ansi.ProtectedMode,
try reader.takeByte(),
) orelse return error.InvalidProtectedMode;
// Keyboard modes and semantic-click state.
const kitty_keyboard = try KittyKeyboard.decode(reader);
const semantic_click = try SemanticClick.decode(reader);
const semantic_click = try decodeSemanticClick(reader);
// Presence of the optional saved-cursor suffix.
const saved_cursor_present = switch (try reader.takeByte()) {
@@ -1299,16 +1130,17 @@ const test_saved_cursor_fixture =
"\x00\x00\x00\x00\x00\x00\x00\x00" ++
"\x07\xe4\x3d";
fn testCharsetState() CharsetState {
return .{
.g0 = .utf8,
.g1 = .ascii,
.g2 = .british,
.g3 = .dec_special,
.gl = .g1,
.gr = .g3,
.single_shift = .g2,
fn testCharsetState() TerminalScreen.CharsetState {
var result: TerminalScreen.CharsetState = .{
.gl = .G1,
.gr = .G3,
.single_shift = .G2,
};
result.charsets.set(.G0, .utf8);
result.charsets.set(.G1, .ascii);
result.charsets.set(.G2, .british);
result.charsets.set(.G3, .dec_special);
return result;
}
fn testHeader() Header {
@@ -1443,14 +1275,14 @@ test "header decoding with a one-byte reader buffer" {
}
test "header registry values round trip" {
const keys = [_]Key{ .primary, .alternate };
const keys = [_]TerminalScreenKey{ .primary, .alternate };
for (keys) |value| {
var header = testHeader();
header.key = value;
try expectHeaderRoundTrip(header);
}
const cursor_styles = [_]CursorStyle{
const cursor_styles = [_]TerminalScreen.CursorStyle{
.bar,
.block,
.underline,
@@ -1462,7 +1294,7 @@ test "header registry values round trip" {
try expectHeaderRoundTrip(header);
}
const semantic_contents = [_]CursorFlags.SemanticContent{
const semantic_contents = [_]terminal_page.Cell.SemanticContent{
.output,
.input,
.prompt,
@@ -1473,7 +1305,7 @@ test "header registry values round trip" {
try expectHeaderRoundTrip(header);
}
const charsets = [_]CharsetState.Charset{
const charsets = [_]terminal_charsets.Charset{
.utf8,
.ascii,
.british,
@@ -1481,14 +1313,14 @@ test "header registry values round trip" {
};
for (charsets) |value| {
var header = testHeader();
header.charset.g0 = value;
header.charset.g1 = value;
header.charset.g2 = value;
header.charset.g3 = value;
header.charset.charsets.set(.G0, value);
header.charset.charsets.set(.G1, value);
header.charset.charsets.set(.G2, value);
header.charset.charsets.set(.G3, value);
try expectHeaderRoundTrip(header);
}
const slots = [_]CharsetState.Slot{ .g0, .g1, .g2, .g3 };
const slots = [_]terminal_charsets.Slots{ .G0, .G1, .G2, .G3 };
for (slots) |value| {
var header = testHeader();
header.charset.gl = value;
@@ -1496,12 +1328,12 @@ test "header registry values round trip" {
try expectHeaderRoundTrip(header);
}
const single_shifts = [_]CharsetState.SingleShift{
.none,
.g0,
.g1,
.g2,
.g3,
const single_shifts = [_]?terminal_charsets.Slots{
null,
.G0,
.G1,
.G2,
.G3,
};
for (single_shifts) |value| {
var header = testHeader();
@@ -1509,22 +1341,27 @@ test "header registry values round trip" {
try expectHeaderRoundTrip(header);
}
const protected_modes = [_]ProtectedMode{ .off, .iso, .dec };
const protected_modes = [_]terminal_ansi.ProtectedMode{
.off,
.iso,
.dec,
};
for (protected_modes) |value| {
var header = testHeader();
header.protected_mode = value;
try expectHeaderRoundTrip(header);
}
const semantic_clicks = [_]SemanticClick{
.none,
.{ .click_events = .absolute },
.{ .click_events = .relative },
.{ .cl = .line },
.{ .cl = .multiple },
.{ .cl = .conservative_vertical },
.{ .cl = .smart_vertical },
};
const semantic_clicks =
[_]TerminalScreen.SemanticPrompt.SemanticClick{
.none,
.{ .click_events = .absolute },
.{ .click_events = .relative },
.{ .cl = .line },
.{ .cl = .multiple },
.{ .cl = .conservative_vertical },
.{ .cl = .smart_vertical },
};
for (semantic_clicks) |value| {
var header = testHeader();
header.semantic_click = value;
@@ -1591,30 +1428,6 @@ test "header encoding rejects invalid state" {
invalid_cursor_flags.encode(&cursor_flags_writer),
);
var invalid_semantic_content = testHeader();
invalid_semantic_content.cursor_flags.semantic_content = .invalid;
var semantic_content_writer: std.Io.Writer = .fixed(&encoded);
try std.testing.expectError(
error.InvalidSemanticContent,
invalid_semantic_content.encode(&semantic_content_writer),
);
var invalid_charset = testHeader();
invalid_charset.charset._padding = 1;
var charset_writer: std.Io.Writer = .fixed(&encoded);
try std.testing.expectError(
error.InvalidCharsetState,
invalid_charset.encode(&charset_writer),
);
var invalid_single_shift = testHeader();
invalid_single_shift.charset.single_shift = @enumFromInt(5);
var single_shift_writer: std.Io.Writer = .fixed(&encoded);
try std.testing.expectError(
error.InvalidCharsetState,
invalid_single_shift.encode(&single_shift_writer),
);
var invalid_kitty_index = testHeader();
invalid_kitty_index.kitty_keyboard.index = 8;
var kitty_index_writer: std.Io.Writer = .fixed(&encoded);
@@ -1781,7 +1594,7 @@ test "native SCREEN payload omits absent optional state" {
var reader: std.Io.Reader = .fixed(writer.buffered());
const header = try Header.decode(&reader);
try std.testing.expectEqual(Key.primary, header.key);
try std.testing.expectEqual(TerminalScreenKey.primary, header.key);
try std.testing.expectEqual(@as(u16, 1), header.page_count);
try std.testing.expect(!header.saved_cursor_present);
try std.testing.expectEqual(
@@ -1824,22 +1637,6 @@ test "saved cursor rejects invalid state" {
invalid_flags.encode(&flags_writer),
);
var invalid_charset = testSavedCursor();
invalid_charset.charset._padding = 1;
var charset_writer: std.Io.Writer = .fixed(&encoded);
try std.testing.expectError(
error.InvalidCharsetState,
invalid_charset.encode(&charset_writer),
);
var invalid_single_shift_value = testSavedCursor();
invalid_single_shift_value.charset.single_shift = @enumFromInt(5);
var single_shift_value_writer: std.Io.Writer = .fixed(&encoded);
try std.testing.expectError(
error.InvalidCharsetState,
invalid_single_shift_value.encode(&single_shift_value_writer),
);
const valid = [_]u8{0} ** SavedCursor.len;
var invalid_flags_bytes = valid;
@@ -2176,7 +1973,7 @@ test "SCREEN encodes the minimal complete-page active suffix" {
const payload_reader = screen_record.payloadReader();
const header = try Header.decode(payload_reader);
try std.testing.expectEqual(Key.primary, header.key);
try std.testing.expectEqual(TerminalScreenKey.primary, header.key);
try std.testing.expectEqual(@as(u16, 2), header.page_count);
try std.testing.expect(!header.saved_cursor_present);
try std.testing.expectEqual(