From 099dcbe04dfeee7bbd3808877d6e1cb8d9b0e765 Mon Sep 17 00:00:00 2001 From: Mitchell Hashimoto Date: Thu, 23 Oct 2025 14:03:34 -0700 Subject: [PATCH 01/31] lib: add a `TaggedUnion` helper to create C ABI compatible tagged unions --- src/lib/enum.zig | 6 +- src/lib/main.zig | 5 +- src/lib/struct.zig | 31 ++++++++ src/lib/target.zig | 6 ++ src/lib/union.zig | 171 +++++++++++++++++++++++++++++++++++++++++++ src/main_ghostty.zig | 1 + 6 files changed, 214 insertions(+), 6 deletions(-) create mode 100644 src/lib/struct.zig create mode 100644 src/lib/target.zig create mode 100644 src/lib/union.zig diff --git a/src/lib/enum.zig b/src/lib/enum.zig index c3971ebde..6fc759846 100644 --- a/src/lib/enum.zig +++ b/src/lib/enum.zig @@ -1,4 +1,5 @@ const std = @import("std"); +const Target = @import("target.zig").Target; /// Create an enum type with the given keys that is C ABI compatible /// if we're targeting C, otherwise a Zig enum with smallest possible @@ -58,11 +59,6 @@ pub fn Enum( return Result; } -pub const Target = union(enum) { - c, - zig, -}; - test "zig" { const testing = std.testing; const T = Enum(.zig, &.{ "a", "b", "c", "d" }); diff --git a/src/lib/main.zig b/src/lib/main.zig index 4ef8dcb2d..cdddade09 100644 --- a/src/lib/main.zig +++ b/src/lib/main.zig @@ -1,9 +1,12 @@ const std = @import("std"); const enumpkg = @import("enum.zig"); +const unionpkg = @import("union.zig"); pub const allocator = @import("allocator.zig"); pub const Enum = enumpkg.Enum; -pub const EnumTarget = enumpkg.Target; +pub const Struct = @import("struct.zig").Struct; +pub const Target = @import("target.zig").Target; +pub const TaggedUnion = unionpkg.TaggedUnion; test { std.testing.refAllDecls(@This()); diff --git a/src/lib/struct.zig b/src/lib/struct.zig new file mode 100644 index 000000000..d494da2e6 --- /dev/null +++ b/src/lib/struct.zig @@ -0,0 +1,31 @@ +const std = @import("std"); +const Target = @import("target.zig").Target; + +pub fn Struct( + comptime target: Target, + comptime Zig: type, +) type { + return switch (target) { + .zig => Zig, + .c => c: { + const info = @typeInfo(Zig).@"struct"; + var fields: [info.fields.len]std.builtin.Type.StructField = undefined; + for (info.fields, 0..) |field, i| { + fields[i] = .{ + .name = field.name, + .type = field.type, + .default_value_ptr = field.default_value_ptr, + .is_comptime = field.is_comptime, + .alignment = field.alignment, + }; + } + + break :c @Type(.{ .@"struct" = .{ + .layout = .@"extern", + .fields = &fields, + .decls = &.{}, + .is_tuple = info.is_tuple, + } }); + }, + }; +} diff --git a/src/lib/target.zig b/src/lib/target.zig new file mode 100644 index 000000000..8d7a7fb89 --- /dev/null +++ b/src/lib/target.zig @@ -0,0 +1,6 @@ +/// The target for ABI generation. The detection of this is left to the +/// caller since there are multiple ways to do that. +pub const Target = union(enum) { + c, + zig, +}; diff --git a/src/lib/union.zig b/src/lib/union.zig new file mode 100644 index 000000000..f19cd3c7f --- /dev/null +++ b/src/lib/union.zig @@ -0,0 +1,171 @@ +const std = @import("std"); +const assert = std.debug.assert; +const testing = std.testing; +const Target = @import("target.zig").Target; + +/// Create a tagged union type that supports a C ABI and maintains +/// C ABI compatibility when adding new tags. This returns a set of types +/// and functions to augment the given Union type, not create a wholly new +/// union type. +/// +/// The C ABI compatible types and functions are only available when the +/// target produces C values. +/// +/// The `Union` type should be a standard Zig tagged union. The tag type +/// should be explicit (i.e. not `union(enum)`) and the tag type should +/// be an enum created with the `Enum` function in this library, so that +/// automatic C ABI compatibility is ensured. +/// +/// The `Padding` type is a type that is always added to the C union +/// with the key `_padding`. This should be set to a type that has the size +/// and alignment needed to pad the C union to the expected size. This +/// should never change to ensure ABI compatibility. +pub fn TaggedUnion( + comptime target: Target, + comptime Union: type, + comptime Padding: type, +) type { + return struct { + comptime { + switch (target) { + .zig => {}, + + // For ABI compatibility, we expect that this is our union size. + .c => if (@sizeOf(CValue) != @sizeOf(Padding)) { + @compileLog(@sizeOf(CValue)); + @compileError("TaggedUnion CValue size does not match expected fixed size"); + }, + } + } + + /// The tag type. + pub const Tag = @typeInfo(Union).@"union".tag_type.?; + + /// The Zig union. + pub const Zig = Union; + + /// The C ABI compatible tagged union type. + pub const C = switch (target) { + .zig => struct {}, + .c => extern struct { + tag: Tag, + value: CValue, + }, + }; + + /// The C ABI compatible union value type. + pub const CValue = cvalue: { + switch (target) { + .zig => break :cvalue extern struct {}, + .c => {}, + } + + const tag_fields = @typeInfo(Tag).@"enum".fields; + var union_fields: [tag_fields.len + 1]std.builtin.Type.UnionField = undefined; + for (tag_fields, 0..) |field, i| { + const action = @unionInit(Union, field.name, undefined); + const Type = t: { + const Type = @TypeOf(@field(action, field.name)); + // Types can provide custom types for their CValue. + switch (@typeInfo(Type)) { + .@"enum", .@"struct", .@"union" => if (@hasDecl(Type, "C")) break :t Type.C, + else => {}, + } + + break :t Type; + }; + + union_fields[i] = .{ + .name = field.name, + .type = Type, + .alignment = @alignOf(Type), + }; + } + + union_fields[tag_fields.len] = .{ + .name = "_padding", + .type = Padding, + .alignment = @alignOf(Padding), + }; + + break :cvalue @Type(.{ .@"union" = .{ + .layout = .@"extern", + .tag_type = null, + .fields = &union_fields, + .decls = &.{}, + } }); + }; + + /// Convert to C union. + pub fn cval(self: Union) C { + const value: CValue = switch (self) { + inline else => |v, tag| @unionInit( + CValue, + @tagName(tag), + value: { + switch (@typeInfo(@TypeOf(v))) { + .@"enum", .@"struct", .@"union" => if (@hasDecl(@TypeOf(v), "cval")) v.cval(), + else => {}, + } + + break :value v; + }, + ), + }; + + return .{ + .tag = @as(Tag, self), + .value = value, + }; + } + + /// Returns the value type for the given tag. + pub fn Value(comptime tag: Tag) type { + inline for (@typeInfo(Union).@"union".fields) |field| { + const field_tag = @field(Tag, field.name); + if (field_tag == tag) return field.type; + } + + unreachable; + } + }; +} + +test "TaggedUnion: matching size" { + const Tag = enum(c_int) { a, b }; + const U = TaggedUnion( + .c, + union(Tag) { + a: u32, + b: u64, + }, + u64, + ); + + try testing.expectEqual(8, @sizeOf(U.CValue)); +} + +test "TaggedUnion: padded size" { + const Tag = enum(c_int) { a }; + const U = TaggedUnion( + .c, + union(Tag) { + a: u32, + }, + u64, + ); + + try testing.expectEqual(8, @sizeOf(U.CValue)); +} + +test "TaggedUnion: c conversion" { + const Tag = enum(c_int) { a, b }; + const U = TaggedUnion(.c, union(Tag) { + a: u32, + b: u64, + }, u64); + + const c = U.cval(.{ .a = 42 }); + try testing.expectEqual(Tag.a, c.tag); + try testing.expectEqual(42, c.value.a); +} diff --git a/src/main_ghostty.zig b/src/main_ghostty.zig index decfc609c..77b7f3ef4 100644 --- a/src/main_ghostty.zig +++ b/src/main_ghostty.zig @@ -193,6 +193,7 @@ test { _ = @import("crash/main.zig"); _ = @import("datastruct/main.zig"); _ = @import("inspector/main.zig"); + _ = @import("lib/main.zig"); _ = @import("terminal/main.zig"); _ = @import("terminfo/main.zig"); _ = @import("simd/main.zig"); From f7189d14b974ce68fd213cca67897ee6dae2cf8c Mon Sep 17 00:00:00 2001 From: Mitchell Hashimoto Date: Thu, 23 Oct 2025 14:03:34 -0700 Subject: [PATCH 02/31] terminal: convert Stream to use Action tagged union --- src/benchmark/TerminalStream.zig | 10 +- src/inspector/termio.zig | 9 + src/terminal/stream.zig | 398 ++++++++++++++++++++++++++++++- src/termio/Termio.zig | 2 +- src/termio/stream_handler.zig | 16 +- 5 files changed, 418 insertions(+), 17 deletions(-) diff --git a/src/benchmark/TerminalStream.zig b/src/benchmark/TerminalStream.zig index ecce509f3..23356ba22 100644 --- a/src/benchmark/TerminalStream.zig +++ b/src/benchmark/TerminalStream.zig @@ -138,8 +138,14 @@ fn step(ptr: *anyopaque) Benchmark.Error!void { const Handler = struct { t: *Terminal, - pub fn print(self: *Handler, cp: u21) !void { - try self.t.print(cp); + pub fn vt( + self: *Handler, + comptime action: Stream.Action.Tag, + value: Stream.Action.Value(action), + ) !void { + switch (action) { + .print => try self.t.print(value.cp), + } } }; diff --git a/src/inspector/termio.zig b/src/inspector/termio.zig index 212f0ea4a..840a587bf 100644 --- a/src/inspector/termio.zig +++ b/src/inspector/termio.zig @@ -333,6 +333,15 @@ pub const VTHandler = struct { cimgui.c.ImGuiTextFilter_destroy(self.filter_text); } + pub fn vt( + self: *VTHandler, + comptime action: Stream.Action.Tag, + value: Stream.Action.Value(action), + ) !void { + _ = self; + _ = value; + } + /// This is called with every single terminal action. pub fn handleManually(self: *VTHandler, action: terminal.Parser.Action) !bool { const insp = self.surface.inspector orelse return false; diff --git a/src/terminal/stream.zig b/src/terminal/stream.zig index c85e72f0f..a26118f3f 100644 --- a/src/terminal/stream.zig +++ b/src/terminal/stream.zig @@ -1,8 +1,11 @@ +const streampkg = @This(); const std = @import("std"); const build_options = @import("terminal_options"); const assert = std.debug.assert; const testing = std.testing; const simd = @import("../simd/main.zig"); +const LibEnum = @import("../lib/enum.zig").Enum; +const LibUnion = @import("../lib/union.zig").TaggedUnion; const Parser = @import("Parser.zig"); const ansi = @import("ansi.zig"); const charsets = @import("charsets.zig"); @@ -25,6 +28,40 @@ const log = std.log.scoped(.stream); /// do something else. const debug = false; +pub const Action = union(Key) { + print: Print, + + pub const Key = LibEnum( + if (build_options.c_abi) .c else .zig, + &.{ + "print", + }, + ); + + /// C ABI functions. + const c_union = LibUnion(@This(), extern struct { + x: u64, + }); + pub const Tag = c_union.Tag; + pub const Value = c_union.Value; + pub const C = c_union.C; + pub const CValue = c_union.CValue; + pub const cval = c_union.cval; + + /// Field types + pub const Print = struct { + cp: u21, + + pub const C = extern struct { + cp: u32, + }; + + pub fn cval(self: Print) Print.C { + return .{ .cp = @intCast(self.cp) }; + } + }; +}; + /// Returns a type that can process a stream of tty control characters. /// This will call various callback functions on type T. Type T only has to /// implement the callbacks it cares about; any unimplemented callbacks will @@ -40,6 +77,8 @@ pub fn Stream(comptime Handler: type) type { return struct { const Self = @This(); + pub const Action = streampkg.Action; + // We use T with @hasDecl so it needs to be a struct. Unwrap the // pointer if we were given one. const T = switch (@typeInfo(Handler)) { @@ -306,7 +345,7 @@ pub fn Stream(comptime Handler: type) type { } switch (action) { - .print => |p| if (@hasDecl(T, "print")) try self.handler.print(p), + .print => |p| try self.print(p), .execute => |code| try self.execute(code), .csi_dispatch => |csi_action| try self.csiDispatch(csi_action), .esc_dispatch => |esc| try self.escDispatch(esc), @@ -334,9 +373,7 @@ pub fn Stream(comptime Handler: type) type { } pub inline fn print(self: *Self, c: u21) !void { - if (@hasDecl(T, "print")) { - try self.handler.print(c); - } + try self.handler.vt(.print, .{ .cp = c }); } pub inline fn execute(self: *Self, c: u8) !void { @@ -1869,8 +1906,15 @@ test "stream: print" { const H = struct { c: ?u21 = 0, - pub fn print(self: *@This(), c: u21) !void { - self.c = c; + pub fn vt( + self: *@This(), + comptime action: anytype, + value: anytype, + ) !void { + switch (action) { + .print => self.c = value.cp, + else => {}, + } } }; @@ -1883,8 +1927,15 @@ test "simd: print invalid utf-8" { const H = struct { c: ?u21 = 0, - pub fn print(self: *@This(), c: u21) !void { - self.c = c; + pub fn vt( + self: *@This(), + comptime action: anytype, + value: anytype, + ) !void { + switch (action) { + .print => self.c = value.cp, + else => {}, + } } }; @@ -1897,8 +1948,15 @@ test "simd: complete incomplete utf-8" { const H = struct { c: ?u21 = null, - pub fn print(self: *@This(), c: u21) !void { - self.c = c; + pub fn vt( + self: *@This(), + comptime action: anytype, + value: anytype, + ) !void { + switch (action) { + .print => self.c = value.cp, + else => {}, + } } }; @@ -1918,6 +1976,16 @@ test "stream: cursor right (CUF)" { pub fn setCursorRight(self: *@This(), v: u16) !void { self.amount = v; } + + pub fn vt( + self: *@This(), + comptime action: anytype, + value: anytype, + ) !void { + _ = self; + _ = action; + _ = value; + } }; var s: Stream(H) = .init(.{}); @@ -1943,6 +2011,16 @@ test "stream: dec set mode (SM) and reset mode (RM)" { self.mode = @as(modes.Mode, @enumFromInt(1)); if (v) self.mode = mode; } + + pub fn vt( + self: *@This(), + comptime action: anytype, + value: anytype, + ) !void { + _ = self; + _ = action; + _ = value; + } }; var s: Stream(H) = .init(.{}); @@ -1965,6 +2043,16 @@ test "stream: ansi set mode (SM) and reset mode (RM)" { self.mode = null; if (v) self.mode = mode; } + + pub fn vt( + self: *@This(), + comptime action: anytype, + value: anytype, + ) !void { + _ = self; + _ = action; + _ = value; + } }; var s: Stream(H) = .init(.{}); @@ -1987,6 +2075,16 @@ test "stream: ansi set mode (SM) and reset mode (RM) with unknown value" { self.mode = null; if (v) self.mode = mode; } + + pub fn vt( + self: *@This(), + comptime action: anytype, + value: anytype, + ) !void { + _ = self; + _ = action; + _ = value; + } }; var s: Stream(H) = .init(.{}); @@ -2007,6 +2105,16 @@ test "stream: restore mode" { _ = b; self.called = true; } + + pub fn vt( + self: *@This(), + comptime action: anytype, + value: anytype, + ) !void { + _ = self; + _ = action; + _ = value; + } }; var s: Stream(H) = .init(.{}); @@ -2022,6 +2130,16 @@ test "stream: pop kitty keyboard with no params defaults to 1" { pub fn popKittyKeyboard(self: *Self, n: u16) !void { self.n = n; } + + pub fn vt( + self: *@This(), + comptime action: anytype, + value: anytype, + ) !void { + _ = self; + _ = action; + _ = value; + } }; var s: Stream(H) = .init(.{}); @@ -2037,6 +2155,16 @@ test "stream: DECSCA" { pub fn setProtectedMode(self: *Self, v: ansi.ProtectedMode) !void { self.v = v; } + + pub fn vt( + self: *@This(), + comptime action: anytype, + value: anytype, + ) !void { + _ = self; + _ = action; + _ = value; + } }; var s: Stream(H) = .init(.{}); @@ -2072,6 +2200,16 @@ test "stream: DECED, DECSED" { self.mode = mode; self.protected = protected; } + + pub fn vt( + self: *@This(), + comptime action: anytype, + value: anytype, + ) !void { + _ = self; + _ = action; + _ = value; + } }; var s: Stream(H) = .init(.{}); @@ -2148,6 +2286,16 @@ test "stream: DECEL, DECSEL" { self.mode = mode; self.protected = protected; } + + pub fn vt( + self: *@This(), + comptime action: anytype, + value: anytype, + ) !void { + _ = self; + _ = action; + _ = value; + } }; var s: Stream(H) = .init(.{}); @@ -2207,6 +2355,16 @@ test "stream: DECSCUSR" { pub fn setCursorStyle(self: *@This(), style: ansi.CursorStyle) !void { self.style = style; } + + pub fn vt( + self: *@This(), + comptime action: anytype, + value: anytype, + ) !void { + _ = self; + _ = action; + _ = value; + } }; var s: Stream(H) = .init(.{}); @@ -2228,6 +2386,16 @@ test "stream: DECSCUSR without space" { pub fn setCursorStyle(self: *@This(), style: ansi.CursorStyle) !void { self.style = style; } + + pub fn vt( + self: *@This(), + comptime action: anytype, + value: anytype, + ) !void { + _ = self; + _ = action; + _ = value; + } }; var s: Stream(H) = .init(.{}); @@ -2245,6 +2413,16 @@ test "stream: XTSHIFTESCAPE" { pub fn setMouseShiftCapture(self: *@This(), v: bool) !void { self.escape = v; } + + pub fn vt( + self: *@This(), + comptime action: anytype, + value: anytype, + ) !void { + _ = self; + _ = action; + _ = value; + } }; var s: Stream(H) = .init(.{}); @@ -2274,6 +2452,16 @@ test "stream: change window title with invalid utf-8" { self.seen = true; } + + pub fn vt( + self: *@This(), + comptime action: anytype, + value: anytype, + ) !void { + _ = self; + _ = action; + _ = value; + } }; { @@ -2298,6 +2486,16 @@ test "stream: insert characters" { _ = v; self.called = true; } + + pub fn vt( + self: *@This(), + comptime action: anytype, + value: anytype, + ) !void { + _ = self; + _ = action; + _ = value; + } }; var s: Stream(H) = .init(.{}); @@ -2324,6 +2522,16 @@ test "stream: SCOSC" { pub fn setLeftAndRightMarginAmbiguous(self: *Self) !void { self.called = true; } + + pub fn vt( + self: *@This(), + comptime action: anytype, + value: anytype, + ) !void { + _ = self; + _ = action; + _ = value; + } }; var s: Stream(H) = .init(.{}); @@ -2339,6 +2547,16 @@ test "stream: SCORC" { pub fn restoreCursor(self: *Self) !void { self.called = true; } + + pub fn vt( + self: *@This(), + comptime action: anytype, + value: anytype, + ) !void { + _ = self; + _ = action; + _ = value; + } }; var s: Stream(H) = .init(.{}); @@ -2353,6 +2571,16 @@ test "stream: too many csi params" { _ = self; unreachable; } + + pub fn vt( + self: *@This(), + comptime action: anytype, + value: anytype, + ) !void { + _ = self; + _ = action; + _ = value; + } }; var s: Stream(H) = .init(.{}); @@ -2365,6 +2593,16 @@ test "stream: csi param too long" { _ = v; _ = self; } + + pub fn vt( + self: *@This(), + comptime action: anytype, + value: anytype, + ) !void { + _ = self; + _ = action; + _ = value; + } }; var s: Stream(H) = .init(.{}); @@ -2378,6 +2616,16 @@ test "stream: send report with CSI t" { pub fn sendSizeReport(self: *@This(), style: csi.SizeReportStyle) void { self.style = style; } + + pub fn vt( + self: *@This(), + comptime action: anytype, + value: anytype, + ) !void { + _ = self; + _ = action; + _ = value; + } }; var s: Stream(H) = .init(.{}); @@ -2402,6 +2650,16 @@ test "stream: invalid CSI t" { pub fn sendSizeReport(self: *@This(), style: csi.SizeReportStyle) void { self.style = style; } + + pub fn vt( + self: *@This(), + comptime action: anytype, + value: anytype, + ) !void { + _ = self; + _ = action; + _ = value; + } }; var s: Stream(H) = .init(.{}); @@ -2417,6 +2675,16 @@ test "stream: CSI t push title" { pub fn pushPopTitle(self: *@This(), op: csi.TitlePushPop) void { self.op = op; } + + pub fn vt( + self: *@This(), + comptime action: anytype, + value: anytype, + ) !void { + _ = self; + _ = action; + _ = value; + } }; var s: Stream(H) = .init(.{}); @@ -2435,6 +2703,16 @@ test "stream: CSI t push title with explicit window" { pub fn pushPopTitle(self: *@This(), op: csi.TitlePushPop) void { self.op = op; } + + pub fn vt( + self: *@This(), + comptime action: anytype, + value: anytype, + ) !void { + _ = self; + _ = action; + _ = value; + } }; var s: Stream(H) = .init(.{}); @@ -2453,6 +2731,16 @@ test "stream: CSI t push title with explicit icon" { pub fn pushPopTitle(self: *@This(), op: csi.TitlePushPop) void { self.op = op; } + + pub fn vt( + self: *@This(), + comptime action: anytype, + value: anytype, + ) !void { + _ = self; + _ = action; + _ = value; + } }; var s: Stream(H) = .init(.{}); @@ -2468,6 +2756,16 @@ test "stream: CSI t push title with index" { pub fn pushPopTitle(self: *@This(), op: csi.TitlePushPop) void { self.op = op; } + + pub fn vt( + self: *@This(), + comptime action: anytype, + value: anytype, + ) !void { + _ = self; + _ = action; + _ = value; + } }; var s: Stream(H) = .init(.{}); @@ -2486,6 +2784,16 @@ test "stream: CSI t pop title" { pub fn pushPopTitle(self: *@This(), op: csi.TitlePushPop) void { self.op = op; } + + pub fn vt( + self: *@This(), + comptime action: anytype, + value: anytype, + ) !void { + _ = self; + _ = action; + _ = value; + } }; var s: Stream(H) = .init(.{}); @@ -2504,6 +2812,16 @@ test "stream: CSI t pop title with explicit window" { pub fn pushPopTitle(self: *@This(), op: csi.TitlePushPop) void { self.op = op; } + + pub fn vt( + self: *@This(), + comptime action: anytype, + value: anytype, + ) !void { + _ = self; + _ = action; + _ = value; + } }; var s: Stream(H) = .init(.{}); @@ -2522,6 +2840,16 @@ test "stream: CSI t pop title with explicit icon" { pub fn pushPopTitle(self: *@This(), op: csi.TitlePushPop) void { self.op = op; } + + pub fn vt( + self: *@This(), + comptime action: anytype, + value: anytype, + ) !void { + _ = self; + _ = action; + _ = value; + } }; var s: Stream(H) = .init(.{}); @@ -2537,6 +2865,16 @@ test "stream: CSI t pop title with index" { pub fn pushPopTitle(self: *@This(), op: csi.TitlePushPop) void { self.op = op; } + + pub fn vt( + self: *@This(), + comptime action: anytype, + value: anytype, + ) !void { + _ = self; + _ = action; + _ = value; + } }; var s: Stream(H) = .init(.{}); @@ -2555,6 +2893,16 @@ test "stream CSI W clear tab stops" { pub fn tabClear(self: *@This(), op: csi.TabClear) !void { self.op = op; } + + pub fn vt( + self: *@This(), + comptime action: anytype, + value: anytype, + ) !void { + _ = self; + _ = action; + _ = value; + } }; var s: Stream(H) = .init(.{}); @@ -2573,6 +2921,16 @@ test "stream CSI W tab set" { pub fn tabSet(self: *@This()) !void { self.called = true; } + + pub fn vt( + self: *@This(), + comptime action: anytype, + value: anytype, + ) !void { + _ = self; + _ = action; + _ = value; + } }; var s: Stream(H) = .init(.{}); @@ -2600,6 +2958,16 @@ test "stream CSI ? W reset tab stops" { pub fn tabReset(self: *@This()) !void { self.reset = true; } + + pub fn vt( + self: *@This(), + comptime action: anytype, + value: anytype, + ) !void { + _ = self; + _ = action; + _ = value; + } }; var s: Stream(H) = .init(.{}); @@ -2624,6 +2992,16 @@ test "stream: SGR with 17+ parameters for underline color" { self.attrs = attr; self.called = true; } + + pub fn vt( + self: *@This(), + comptime action: anytype, + value: anytype, + ) !void { + _ = self; + _ = action; + _ = value; + } }; var s: Stream(H) = .init(.{}); diff --git a/src/termio/Termio.zig b/src/termio/Termio.zig index e41fe33a9..2f1bf227d 100644 --- a/src/termio/Termio.zig +++ b/src/termio/Termio.zig @@ -64,7 +64,7 @@ mailbox: termio.Mailbox, /// The stream parser. This parses the stream of escape codes and so on /// from the child process and calls callbacks in the stream handler. -terminal_stream: terminalpkg.Stream(StreamHandler), +terminal_stream: StreamHandler.Stream, /// Last time the cursor was reset. This is used to prevent message /// flooding with cursor resets. diff --git a/src/termio/stream_handler.zig b/src/termio/stream_handler.zig index dd8669d90..b9ab7a2b4 100644 --- a/src/termio/stream_handler.zig +++ b/src/termio/stream_handler.zig @@ -95,6 +95,8 @@ pub const StreamHandler = struct { /// this to determine if we need to default the window title. seen_title: bool = false, + pub const Stream = terminal.Stream(StreamHandler); + pub fn deinit(self: *StreamHandler) void { self.apc.deinit(); self.dcs.deinit(); @@ -186,6 +188,16 @@ pub const StreamHandler = struct { _ = self.renderer_mailbox.push(msg, .{ .forever = {} }); } + pub fn vt( + self: *StreamHandler, + comptime action: Stream.Action.Tag, + value: Stream.Action.Value(action), + ) !void { + switch (action) { + .print => try self.terminal.print(value.cp), + } + } + pub inline fn dcsHook(self: *StreamHandler, dcs: terminal.DCS) !void { var cmd = self.dcs.hook(self.alloc, dcs) orelse return; defer cmd.deinit(); @@ -322,10 +334,6 @@ pub const StreamHandler = struct { } } - pub inline fn print(self: *StreamHandler, ch: u21) !void { - try self.terminal.print(ch); - } - pub inline fn printRepeat(self: *StreamHandler, count: usize) !void { try self.terminal.printRepeat(count); } From 2ef89c153abe0e5115472907c96301dbd422a695 Mon Sep 17 00:00:00 2001 From: Mitchell Hashimoto Date: Thu, 23 Oct 2025 15:09:30 -0700 Subject: [PATCH 03/31] terminal: convert C0 --- src/benchmark/TerminalStream.zig | 1 + src/terminal/charsets.zig | 17 +++--- src/terminal/stream.zig | 96 +++++++++++++++++--------------- src/termio/stream_handler.zig | 30 ++++------ 4 files changed, 71 insertions(+), 73 deletions(-) diff --git a/src/benchmark/TerminalStream.zig b/src/benchmark/TerminalStream.zig index 23356ba22..0a993c42b 100644 --- a/src/benchmark/TerminalStream.zig +++ b/src/benchmark/TerminalStream.zig @@ -145,6 +145,7 @@ const Handler = struct { ) !void { switch (action) { .print => try self.t.print(value.cp), + else => {}, } } }; diff --git a/src/terminal/charsets.zig b/src/terminal/charsets.zig index 66d6566e3..9d49832df 100644 --- a/src/terminal/charsets.zig +++ b/src/terminal/charsets.zig @@ -1,16 +1,19 @@ const std = @import("std"); +const build_options = @import("terminal_options"); const assert = std.debug.assert; +const LibEnum = @import("../lib/enum.zig").Enum; /// The available charset slots for a terminal. -pub const Slots = enum(u3) { - G0 = 0, - G1 = 1, - G2 = 2, - G3 = 3, -}; +pub const Slots = LibEnum( + if (build_options.c_abi) .c else .zig, + &.{ "G0", "G1", "G2", "G3" }, +); /// The name of the active slots. -pub const ActiveSlot = enum { GL, GR }; +pub const ActiveSlot = LibEnum( + if (build_options.c_abi) .c else .zig, + &.{ "GL", "GR" }, +); /// The list of supported character sets and their associated tables. pub const Charset = enum { diff --git a/src/terminal/stream.zig b/src/terminal/stream.zig index a26118f3f..555326607 100644 --- a/src/terminal/stream.zig +++ b/src/terminal/stream.zig @@ -4,8 +4,7 @@ const build_options = @import("terminal_options"); const assert = std.debug.assert; const testing = std.testing; const simd = @import("../simd/main.zig"); -const LibEnum = @import("../lib/enum.zig").Enum; -const LibUnion = @import("../lib/union.zig").TaggedUnion; +const lib = @import("../lib/main.zig"); const Parser = @import("Parser.zig"); const ansi = @import("ansi.zig"); const charsets = @import("charsets.zig"); @@ -28,20 +27,40 @@ const log = std.log.scoped(.stream); /// do something else. const debug = false; +const lib_target: lib.Target = if (build_options.c_abi) .c else .zig; + pub const Action = union(Key) { print: Print, + bell, + backspace, + horizontal_tab: HorizontalTab, + linefeed, + carriage_return, + enquiry, + invoke_charset: InvokeCharset, - pub const Key = LibEnum( - if (build_options.c_abi) .c else .zig, + pub const Key = lib.Enum( + lib_target, &.{ "print", + "bell", + "backspace", + "horizontal_tab", + "linefeed", + "carriage_return", + "enquiry", + "invoke_charset", }, ); /// C ABI functions. - const c_union = LibUnion(@This(), extern struct { - x: u64, - }); + const c_union = lib.TaggedUnion( + lib_target, + @This(), + // TODO: Before shipping an ABI-compatible libghostty, verify this. + // This was just arbitrarily chosen for now. + [8]u64, + ); pub const Tag = c_union.Tag; pub const Value = c_union.Value; pub const C = c_union.C; @@ -60,6 +79,16 @@ pub const Action = union(Key) { return .{ .cp = @intCast(self.cp) }; } }; + + pub const HorizontalTab = lib.Struct(lib_target, struct { + count: u16, + }); + + pub const InvokeCharset = lib.Struct(lib_target, struct { + bank: charsets.ActiveSlot, + charset: charsets.Slots, + locking: bool, + }); }; /// Returns a type that can process a stream of tty control characters. @@ -383,45 +412,14 @@ pub fn Stream(comptime Handler: type) type { // We ignore SOH/STX: https://github.com/microsoft/terminal/issues/10786 .NUL, .SOH, .STX => {}, - .ENQ => if (@hasDecl(T, "enquiry")) - try self.handler.enquiry() - else - log.warn("unimplemented execute: {x}", .{c}), - - .BEL => if (@hasDecl(T, "bell")) - try self.handler.bell() - else - log.warn("unimplemented execute: {x}", .{c}), - - .BS => if (@hasDecl(T, "backspace")) - try self.handler.backspace() - else - log.warn("unimplemented execute: {x}", .{c}), - - .HT => if (@hasDecl(T, "horizontalTab")) - try self.handler.horizontalTab(1) - else - log.warn("unimplemented execute: {x}", .{c}), - - .LF, .VT, .FF => if (@hasDecl(T, "linefeed")) - try self.handler.linefeed() - else - log.warn("unimplemented execute: {x}", .{c}), - - .CR => if (@hasDecl(T, "carriageReturn")) - try self.handler.carriageReturn() - else - log.warn("unimplemented execute: {x}", .{c}), - - .SO => if (@hasDecl(T, "invokeCharset")) - try self.handler.invokeCharset(.GL, .G1, false) - else - log.warn("unimplemented invokeCharset: {x}", .{c}), - - .SI => if (@hasDecl(T, "invokeCharset")) - try self.handler.invokeCharset(.GL, .G0, false) - else - log.warn("unimplemented invokeCharset: {x}", .{c}), + .ENQ => try self.handler.vt(.enquiry, {}), + .BEL => try self.handler.vt(.bell, {}), + .BS => try self.handler.vt(.backspace, {}), + .HT => try self.handler.vt(.horizontal_tab, .{ .count = 1 }), + .LF, .VT, .FF => try self.handler.vt(.linefeed, {}), + .CR => try self.handler.vt(.carriage_return, {}), + .SO => try self.handler.vt(.invoke_charset, .{ .bank = .GL, .charset = .G1, .locking = false }), + .SI => try self.handler.vt(.invoke_charset, .{ .bank = .GL, .charset = .G0, .locking = false }), else => log.warn("invalid C0 character, ignoring: 0x{x}", .{c}), } @@ -1902,6 +1900,12 @@ pub fn Stream(comptime Handler: type) type { }; } +test Action { + // Forces the C type to be reified when the target is C, ensuring + // all our types are C ABI compatible. + _ = Action.C; +} + test "stream: print" { const H = struct { c: ?u21 = 0, diff --git a/src/termio/stream_handler.zig b/src/termio/stream_handler.zig index b9ab7a2b4..d8c05f2f2 100644 --- a/src/termio/stream_handler.zig +++ b/src/termio/stream_handler.zig @@ -195,6 +195,13 @@ pub const StreamHandler = struct { ) !void { switch (action) { .print => try self.terminal.print(value.cp), + .bell => self.bell(), + .backspace => self.terminal.backspace(), + .horizontal_tab => try self.horizontalTab(value.count), + .linefeed => try self.linefeed(), + .carriage_return => self.terminal.carriageReturn(), + .enquiry => try self.enquiry(), + .invoke_charset => self.terminal.invokeCharset(value.bank, value.charset, value.locking), } } @@ -338,15 +345,11 @@ pub const StreamHandler = struct { try self.terminal.printRepeat(count); } - pub inline fn bell(self: *StreamHandler) !void { + inline fn bell(self: *StreamHandler) void { self.surfaceMessageWriter(.ring_bell); } - pub inline fn backspace(self: *StreamHandler) !void { - self.terminal.backspace(); - } - - pub inline fn horizontalTab(self: *StreamHandler, count: u16) !void { + inline fn horizontalTab(self: *StreamHandler, count: u16) !void { for (0..count) |_| { const x = self.terminal.screen.cursor.x; try self.terminal.horizontalTab(); @@ -362,16 +365,12 @@ pub const StreamHandler = struct { } } - pub inline fn linefeed(self: *StreamHandler) !void { + inline fn linefeed(self: *StreamHandler) !void { // Small optimization: call index instead of linefeed because they're // identical and this avoids one layer of function call overhead. try self.terminal.index(); } - pub inline fn carriageReturn(self: *StreamHandler) !void { - self.terminal.carriageReturn(); - } - pub inline fn setCursorLeft(self: *StreamHandler, amount: u16) !void { self.terminal.cursorLeft(amount); } @@ -896,15 +895,6 @@ pub const StreamHandler = struct { self.terminal.configureCharset(slot, set); } - pub fn invokeCharset( - self: *StreamHandler, - active: terminal.CharsetActiveSlot, - slot: terminal.CharsetSlot, - single: bool, - ) !void { - self.terminal.invokeCharset(active, slot, single); - } - pub fn fullReset( self: *StreamHandler, ) !void { From ccd821a0ff942b30dc415f14bbc6c8988103aa9e Mon Sep 17 00:00:00 2001 From: Mitchell Hashimoto Date: Thu, 23 Oct 2025 16:02:17 -0700 Subject: [PATCH 04/31] terminal: convert cursor movements --- src/terminal/stream.zig | 179 ++++++++++++++++++++-------------- src/termio/stream_handler.zig | 57 +++-------- 2 files changed, 121 insertions(+), 115 deletions(-) diff --git a/src/terminal/stream.zig b/src/terminal/stream.zig index 555326607..c48eca973 100644 --- a/src/terminal/stream.zig +++ b/src/terminal/stream.zig @@ -38,6 +38,15 @@ pub const Action = union(Key) { carriage_return, enquiry, invoke_charset: InvokeCharset, + cursor_up: CursorMovement, + cursor_down: CursorMovement, + cursor_left: CursorMovement, + cursor_right: CursorMovement, + cursor_col: CursorMovement, + cursor_row: CursorMovement, + cursor_col_relative: CursorMovement, + cursor_row_relative: CursorMovement, + cursor_pos: CursorPos, pub const Key = lib.Enum( lib_target, @@ -50,6 +59,15 @@ pub const Action = union(Key) { "carriage_return", "enquiry", "invoke_charset", + "cursor_up", + "cursor_down", + "cursor_left", + "cursor_right", + "cursor_col", + "cursor_row", + "cursor_col_relative", + "cursor_row_relative", + "cursor_pos", }, ); @@ -89,6 +107,19 @@ pub const Action = union(Key) { charset: charsets.Slots, locking: bool, }); + + pub const CursorMovement = extern struct { + /// The value of the cursor movement. Depending on the tag of this + /// union this may be an absolute value or it may be a relative + /// value. For example, `cursor_up` is relative, but `cursor_row` + /// is absolute. + value: u16, + }; + + pub const CursorPos = extern struct { + row: u16, + col: u16, + }; }; /// Returns a type that can process a stream of tty control characters. @@ -429,8 +460,8 @@ pub fn Stream(comptime Handler: type) type { switch (input.final) { // CUU - Cursor Up 'A', 'k' => switch (input.intermediates.len) { - 0 => if (@hasDecl(T, "setCursorUp")) try self.handler.setCursorUp( - switch (input.params.len) { + 0 => try self.handler.vt(.cursor_up, .{ + .value = switch (input.params.len) { 0 => 1, 1 => input.params[0], else => { @@ -438,8 +469,7 @@ pub fn Stream(comptime Handler: type) type { return; }, }, - false, - ) else log.warn("unimplemented CSI callback: {f}", .{input}), + }), else => log.warn( "ignoring unimplemented CSI A with intermediates: {s}", @@ -449,8 +479,8 @@ pub fn Stream(comptime Handler: type) type { // CUD - Cursor Down 'B' => switch (input.intermediates.len) { - 0 => if (@hasDecl(T, "setCursorDown")) try self.handler.setCursorDown( - switch (input.params.len) { + 0 => try self.handler.vt(.cursor_down, .{ + .value = switch (input.params.len) { 0 => 1, 1 => input.params[0], else => { @@ -458,8 +488,7 @@ pub fn Stream(comptime Handler: type) type { return; }, }, - false, - ) else log.warn("unimplemented CSI callback: {f}", .{input}), + }), else => log.warn( "ignoring unimplemented CSI B with intermediates: {s}", @@ -469,8 +498,8 @@ pub fn Stream(comptime Handler: type) type { // CUF - Cursor Right 'C' => switch (input.intermediates.len) { - 0 => if (@hasDecl(T, "setCursorRight")) try self.handler.setCursorRight( - switch (input.params.len) { + 0 => try self.handler.vt(.cursor_right, .{ + .value = switch (input.params.len) { 0 => 1, 1 => input.params[0], else => { @@ -478,7 +507,7 @@ pub fn Stream(comptime Handler: type) type { return; }, }, - ) else log.warn("unimplemented CSI callback: {f}", .{input}), + }), else => log.warn( "ignoring unimplemented CSI C with intermediates: {s}", @@ -488,8 +517,8 @@ pub fn Stream(comptime Handler: type) type { // CUB - Cursor Left 'D', 'j' => switch (input.intermediates.len) { - 0 => if (@hasDecl(T, "setCursorLeft")) try self.handler.setCursorLeft( - switch (input.params.len) { + 0 => try self.handler.vt(.cursor_left, .{ + .value = switch (input.params.len) { 0 => 1, 1 => input.params[0], else => { @@ -497,7 +526,7 @@ pub fn Stream(comptime Handler: type) type { return; }, }, - ) else log.warn("unimplemented CSI callback: {f}", .{input}), + }), else => log.warn( "ignoring unimplemented CSI D with intermediates: {s}", @@ -507,17 +536,19 @@ pub fn Stream(comptime Handler: type) type { // CNL - Cursor Next Line 'E' => switch (input.intermediates.len) { - 0 => if (@hasDecl(T, "setCursorDown")) try self.handler.setCursorDown( - switch (input.params.len) { - 0 => 1, - 1 => input.params[0], - else => { - log.warn("invalid cursor up command: {f}", .{input}); - return; + 0 => { + try self.handler.vt(.cursor_down, .{ + .value = switch (input.params.len) { + 0 => 1, + 1 => input.params[0], + else => { + log.warn("invalid cursor up command: {f}", .{input}); + return; + }, }, - }, - true, - ) else log.warn("unimplemented CSI callback: {f}", .{input}), + }); + try self.handler.vt(.carriage_return, {}); + }, else => log.warn( "ignoring unimplemented CSI E with intermediates: {s}", @@ -527,17 +558,19 @@ pub fn Stream(comptime Handler: type) type { // CPL - Cursor Previous Line 'F' => switch (input.intermediates.len) { - 0 => if (@hasDecl(T, "setCursorUp")) try self.handler.setCursorUp( - switch (input.params.len) { - 0 => 1, - 1 => input.params[0], - else => { - log.warn("invalid cursor down command: {f}", .{input}); - return; + 0 => { + try self.handler.vt(.cursor_up, .{ + .value = switch (input.params.len) { + 0 => 1, + 1 => input.params[0], + else => { + log.warn("invalid cursor down command: {f}", .{input}); + return; + }, }, - }, - true, - ) else log.warn("unimplemented CSI callback: {f}", .{input}), + }); + try self.handler.vt(.carriage_return, {}); + }, else => log.warn( "ignoring unimplemented CSI F with intermediates: {s}", @@ -548,11 +581,16 @@ pub fn Stream(comptime Handler: type) type { // HPA - Cursor Horizontal Position Absolute // TODO: test 'G', '`' => switch (input.intermediates.len) { - 0 => if (@hasDecl(T, "setCursorCol")) switch (input.params.len) { - 0 => try self.handler.setCursorCol(1), - 1 => try self.handler.setCursorCol(input.params[0]), - else => log.warn("invalid HPA command: {f}", .{input}), - } else log.warn("unimplemented CSI callback: {f}", .{input}), + 0 => try self.handler.vt(.cursor_col, .{ + .value = switch (input.params.len) { + 0 => 1, + 1 => input.params[0], + else => { + log.warn("invalid HPA command: {f}", .{input}); + return; + }, + }, + }), else => log.warn( "ignoring unimplemented CSI G with intermediates: {s}", @@ -563,12 +601,18 @@ pub fn Stream(comptime Handler: type) type { // CUP - Set Cursor Position. // TODO: test 'H', 'f' => switch (input.intermediates.len) { - 0 => if (@hasDecl(T, "setCursorPos")) switch (input.params.len) { - 0 => try self.handler.setCursorPos(1, 1), - 1 => try self.handler.setCursorPos(input.params[0], 1), - 2 => try self.handler.setCursorPos(input.params[0], input.params[1]), - else => log.warn("invalid CUP command: {f}", .{input}), - } else log.warn("unimplemented CSI callback: {f}", .{input}), + 0 => { + const pos: streampkg.Action.CursorPos = switch (input.params.len) { + 0 => .{ .row = 1, .col = 1 }, + 1 => .{ .row = input.params[0], .col = 1 }, + 2 => .{ .row = input.params[0], .col = input.params[1] }, + else => { + log.warn("invalid CUP command: {f}", .{input}); + return; + }, + }; + try self.handler.vt(.cursor_pos, pos); + }, else => log.warn( "ignoring unimplemented CSI H with intermediates: {s}", @@ -830,8 +874,8 @@ pub fn Stream(comptime Handler: type) type { // HPR - Cursor Horizontal Position Relative 'a' => switch (input.intermediates.len) { - 0 => if (@hasDecl(T, "setCursorColRelative")) try self.handler.setCursorColRelative( - switch (input.params.len) { + 0 => try self.handler.vt(.cursor_col_relative, .{ + .value = switch (input.params.len) { 0 => 1, 1 => input.params[0], else => { @@ -839,7 +883,7 @@ pub fn Stream(comptime Handler: type) type { return; }, }, - ) else log.warn("unimplemented CSI callback: {f}", .{input}), + }), else => log.warn( "ignoring unimplemented CSI a with intermediates: {s}", @@ -886,8 +930,8 @@ pub fn Stream(comptime Handler: type) type { // VPA - Cursor Vertical Position Absolute 'd' => switch (input.intermediates.len) { - 0 => if (@hasDecl(T, "setCursorRow")) try self.handler.setCursorRow( - switch (input.params.len) { + 0 => try self.handler.vt(.cursor_row, .{ + .value = switch (input.params.len) { 0 => 1, 1 => input.params[0], else => { @@ -895,7 +939,7 @@ pub fn Stream(comptime Handler: type) type { return; }, }, - ) else log.warn("unimplemented CSI callback: {f}", .{input}), + }), else => log.warn( "ignoring unimplemented CSI d with intermediates: {s}", @@ -905,8 +949,8 @@ pub fn Stream(comptime Handler: type) type { // VPR - Cursor Vertical Position Relative 'e' => switch (input.intermediates.len) { - 0 => if (@hasDecl(T, "setCursorRowRelative")) try self.handler.setCursorRowRelative( - switch (input.params.len) { + 0 => try self.handler.vt(.cursor_row_relative, .{ + .value = switch (input.params.len) { 0 => 1, 1 => input.params[0], else => { @@ -914,7 +958,7 @@ pub fn Stream(comptime Handler: type) type { return; }, }, - ) else log.warn("unimplemented CSI callback: {f}", .{input}), + }), else => log.warn( "ignoring unimplemented CSI e with intermediates: {s}", @@ -1977,18 +2021,15 @@ test "stream: cursor right (CUF)" { const H = struct { amount: u16 = 0, - pub fn setCursorRight(self: *@This(), v: u16) !void { - self.amount = v; - } - pub fn vt( self: *@This(), comptime action: anytype, value: anytype, ) !void { - _ = self; - _ = action; - _ = value; + switch (action) { + .cursor_right => self.amount = value.value, + else => {}, + } } }; @@ -2570,20 +2611,17 @@ test "stream: SCORC" { test "stream: too many csi params" { const H = struct { - pub fn setCursorRight(self: *@This(), v: u16) !void { - _ = v; - _ = self; - unreachable; - } - pub fn vt( self: *@This(), comptime action: anytype, value: anytype, ) !void { _ = self; - _ = action; _ = value; + switch (action) { + .cursor_right => unreachable, + else => {}, + } } }; @@ -2593,11 +2631,6 @@ test "stream: too many csi params" { test "stream: csi param too long" { const H = struct { - pub fn setCursorRight(self: *@This(), v: u16) !void { - _ = v; - _ = self; - } - pub fn vt( self: *@This(), comptime action: anytype, diff --git a/src/termio/stream_handler.zig b/src/termio/stream_handler.zig index d8c05f2f2..a18e669f6 100644 --- a/src/termio/stream_handler.zig +++ b/src/termio/stream_handler.zig @@ -202,6 +202,21 @@ pub const StreamHandler = struct { .carriage_return => self.terminal.carriageReturn(), .enquiry => try self.enquiry(), .invoke_charset => self.terminal.invokeCharset(value.bank, value.charset, value.locking), + .cursor_up => self.terminal.cursorUp(value.value), + .cursor_down => self.terminal.cursorDown(value.value), + .cursor_left => self.terminal.cursorLeft(value.value), + .cursor_right => self.terminal.cursorRight(value.value), + .cursor_pos => self.terminal.setCursorPos(value.row, value.col), + .cursor_col => self.terminal.setCursorPos(self.terminal.screen.cursor.y + 1, value.value), + .cursor_row => self.terminal.setCursorPos(value.value, self.terminal.screen.cursor.x + 1), + .cursor_col_relative => self.terminal.setCursorPos( + self.terminal.screen.cursor.y + 1, + self.terminal.screen.cursor.x + 1 +| value.value, + ), + .cursor_row_relative => self.terminal.setCursorPos( + self.terminal.screen.cursor.y + 1 +| value.value, + self.terminal.screen.cursor.x + 1, + ), } } @@ -371,49 +386,7 @@ pub const StreamHandler = struct { try self.terminal.index(); } - pub inline fn setCursorLeft(self: *StreamHandler, amount: u16) !void { - self.terminal.cursorLeft(amount); - } - pub inline fn setCursorRight(self: *StreamHandler, amount: u16) !void { - self.terminal.cursorRight(amount); - } - - pub inline fn setCursorDown(self: *StreamHandler, amount: u16, carriage: bool) !void { - self.terminal.cursorDown(amount); - if (carriage) self.terminal.carriageReturn(); - } - - pub inline fn setCursorUp(self: *StreamHandler, amount: u16, carriage: bool) !void { - self.terminal.cursorUp(amount); - if (carriage) self.terminal.carriageReturn(); - } - - pub inline fn setCursorCol(self: *StreamHandler, col: u16) !void { - self.terminal.setCursorPos(self.terminal.screen.cursor.y + 1, col); - } - - pub inline fn setCursorColRelative(self: *StreamHandler, offset: u16) !void { - self.terminal.setCursorPos( - self.terminal.screen.cursor.y + 1, - self.terminal.screen.cursor.x + 1 +| offset, - ); - } - - pub inline fn setCursorRow(self: *StreamHandler, row: u16) !void { - self.terminal.setCursorPos(row, self.terminal.screen.cursor.x + 1); - } - - pub inline fn setCursorRowRelative(self: *StreamHandler, offset: u16) !void { - self.terminal.setCursorPos( - self.terminal.screen.cursor.y + 1 +| offset, - self.terminal.screen.cursor.x + 1, - ); - } - - pub inline fn setCursorPos(self: *StreamHandler, row: u16, col: u16) !void { - self.terminal.setCursorPos(row, col); - } pub inline fn eraseDisplay(self: *StreamHandler, mode: terminal.EraseDisplay, protected: bool) !void { if (mode == .complete) { From b5da54d92538f9d58ec1e8a45d9110291f831295 Mon Sep 17 00:00:00 2001 From: Mitchell Hashimoto Date: Thu, 23 Oct 2025 16:27:02 -0700 Subject: [PATCH 05/31] terminal: horizontal tab --- src/terminal/stream.zig | 42 +++++++++++++++-------------------- src/termio/stream_handler.zig | 7 +++--- 2 files changed, 21 insertions(+), 28 deletions(-) diff --git a/src/terminal/stream.zig b/src/terminal/stream.zig index c48eca973..c6621df00 100644 --- a/src/terminal/stream.zig +++ b/src/terminal/stream.zig @@ -33,7 +33,8 @@ pub const Action = union(Key) { print: Print, bell, backspace, - horizontal_tab: HorizontalTab, + horizontal_tab: u16, + horizontal_tab_back: u16, linefeed, carriage_return, enquiry, @@ -55,6 +56,7 @@ pub const Action = union(Key) { "bell", "backspace", "horizontal_tab", + "horizontal_tab_back", "linefeed", "carriage_return", "enquiry", @@ -98,10 +100,6 @@ pub const Action = union(Key) { } }; - pub const HorizontalTab = lib.Struct(lib_target, struct { - count: u16, - }); - pub const InvokeCharset = lib.Struct(lib_target, struct { bank: charsets.ActiveSlot, charset: charsets.Slots, @@ -446,7 +444,7 @@ pub fn Stream(comptime Handler: type) type { .ENQ => try self.handler.vt(.enquiry, {}), .BEL => try self.handler.vt(.bell, {}), .BS => try self.handler.vt(.backspace, {}), - .HT => try self.handler.vt(.horizontal_tab, .{ .count = 1 }), + .HT => try self.handler.vt(.horizontal_tab, 1), .LF, .VT, .FF => try self.handler.vt(.linefeed, {}), .CR => try self.handler.vt(.carriage_return, {}), .SO => try self.handler.vt(.invoke_charset, .{ .bank = .GL, .charset = .G1, .locking = false }), @@ -622,16 +620,14 @@ pub fn Stream(comptime Handler: type) type { // CHT - Cursor Horizontal Tabulation 'I' => switch (input.intermediates.len) { - 0 => if (@hasDecl(T, "horizontalTab")) try self.handler.horizontalTab( - switch (input.params.len) { - 0 => 1, - 1 => input.params[0], - else => { - log.warn("invalid horizontal tab command: {f}", .{input}); - return; - }, + 0 => try self.handler.vt(.horizontal_tab, switch (input.params.len) { + 0 => 1, + 1 => input.params[0], + else => { + log.warn("invalid horizontal tab command: {f}", .{input}); + return; }, - ) else log.warn("unimplemented CSI callback: {f}", .{input}), + }), else => log.warn( "ignoring unimplemented CSI I with intermediates: {s}", @@ -855,16 +851,14 @@ pub fn Stream(comptime Handler: type) type { // CHT - Cursor Horizontal Tabulation Back 'Z' => switch (input.intermediates.len) { - 0 => if (@hasDecl(T, "horizontalTabBack")) try self.handler.horizontalTabBack( - switch (input.params.len) { - 0 => 1, - 1 => input.params[0], - else => { - log.warn("invalid horizontal tab back command: {f}", .{input}); - return; - }, + 0 => try self.handler.vt(.horizontal_tab_back, switch (input.params.len) { + 0 => 1, + 1 => input.params[0], + else => { + log.warn("invalid horizontal tab back command: {f}", .{input}); + return; }, - ) else log.warn("unimplemented CSI callback: {f}", .{input}), + }), else => log.warn( "ignoring unimplemented CSI Z with intermediates: {s}", diff --git a/src/termio/stream_handler.zig b/src/termio/stream_handler.zig index a18e669f6..cbd4ef281 100644 --- a/src/termio/stream_handler.zig +++ b/src/termio/stream_handler.zig @@ -197,7 +197,8 @@ pub const StreamHandler = struct { .print => try self.terminal.print(value.cp), .bell => self.bell(), .backspace => self.terminal.backspace(), - .horizontal_tab => try self.horizontalTab(value.count), + .horizontal_tab => try self.horizontalTab(value), + .horizontal_tab_back => try self.horizontalTabBack(value), .linefeed => try self.linefeed(), .carriage_return => self.terminal.carriageReturn(), .enquiry => try self.enquiry(), @@ -372,7 +373,7 @@ pub const StreamHandler = struct { } } - pub inline fn horizontalTabBack(self: *StreamHandler, count: u16) !void { + inline fn horizontalTabBack(self: *StreamHandler, count: u16) !void { for (0..count) |_| { const x = self.terminal.screen.cursor.x; try self.terminal.horizontalTabBack(); @@ -386,8 +387,6 @@ pub const StreamHandler = struct { try self.terminal.index(); } - - pub inline fn eraseDisplay(self: *StreamHandler, mode: terminal.EraseDisplay, protected: bool) !void { if (mode == .complete) { // Whenever we erase the full display, scroll to bottom. From b0fb3ef9a90a1ad15051f0669841aed6eadb3c6d Mon Sep 17 00:00:00 2001 From: Mitchell Hashimoto Date: Thu, 23 Oct 2025 16:31:42 -0700 Subject: [PATCH 06/31] terminal: erase display conversion --- src/terminal/stream.zig | 112 ++++++++++++++++++++++++---------- src/termio/stream_handler.zig | 27 ++++---- 2 files changed, 93 insertions(+), 46 deletions(-) diff --git a/src/terminal/stream.zig b/src/terminal/stream.zig index c6621df00..25ce76f4b 100644 --- a/src/terminal/stream.zig +++ b/src/terminal/stream.zig @@ -48,6 +48,15 @@ pub const Action = union(Key) { cursor_col_relative: CursorMovement, cursor_row_relative: CursorMovement, cursor_pos: CursorPos, + erase_display_below: bool, + erase_display_above: bool, + erase_display_complete: bool, + erase_display_scrollback: bool, + erase_display_scroll_complete: bool, + erase_line_right: bool, + erase_line_left: bool, + erase_line_complete: bool, + erase_line_right_unless_pending_wrap: bool, pub const Key = lib.Enum( lib_target, @@ -70,6 +79,15 @@ pub const Action = union(Key) { "cursor_col_relative", "cursor_row_relative", "cursor_pos", + "erase_display_below", + "erase_display_above", + "erase_display_complete", + "erase_display_scrollback", + "erase_display_scroll_complete", + "erase_line_right", + "erase_line_left", + "erase_line_complete", + "erase_line_right_unless_pending_wrap", }, ); @@ -636,7 +654,7 @@ pub fn Stream(comptime Handler: type) type { }, // Erase Display - 'J' => if (@hasDecl(T, "eraseDisplay")) { + 'J' => { const protected_: ?bool = switch (input.intermediates.len) { 0 => false, 1 => if (input.intermediates[0] == '?') true else null, @@ -659,11 +677,17 @@ pub fn Stream(comptime Handler: type) type { return; }; - try self.handler.eraseDisplay(mode, protected); - } else log.warn("unimplemented CSI callback: {f}", .{input}), + switch (mode) { + .below => try self.handler.vt(.erase_display_below, protected), + .above => try self.handler.vt(.erase_display_above, protected), + .complete => try self.handler.vt(.erase_display_complete, protected), + .scrollback => try self.handler.vt(.erase_display_scrollback, protected), + .scroll_complete => try self.handler.vt(.erase_display_scroll_complete, protected), + } + }, // Erase Line - 'K' => if (@hasDecl(T, "eraseLine")) { + 'K' => { const protected_: ?bool = switch (input.intermediates.len) { 0 => false, 1 => if (input.intermediates[0] == '?') true else null, @@ -686,8 +710,14 @@ pub fn Stream(comptime Handler: type) type { return; }; - try self.handler.eraseLine(mode, protected); - } else log.warn("unimplemented CSI callback: {f}", .{input}), + switch (mode) { + .right => try self.handler.vt(.erase_line_right, protected), + .left => try self.handler.vt(.erase_line_left, protected), + .complete => try self.handler.vt(.erase_line_complete, protected), + .right_unless_pending_wrap => try self.handler.vt(.erase_line_right_unless_pending_wrap, protected), + _ => log.warn("invalid erase line mode: {}", .{mode}), + } + }, // IL - Insert Lines // TODO: test @@ -2231,23 +2261,34 @@ test "stream: DECED, DECSED" { mode: ?csi.EraseDisplay = null, protected: ?bool = null, - pub fn eraseDisplay( - self: *Self, - mode: csi.EraseDisplay, - protected: bool, - ) !void { - self.mode = mode; - self.protected = protected; - } - pub fn vt( - self: *@This(), + self: *Self, comptime action: anytype, value: anytype, ) !void { - _ = self; - _ = action; - _ = value; + switch (action) { + .erase_display_below => { + self.mode = .below; + self.protected = value; + }, + .erase_display_above => { + self.mode = .above; + self.protected = value; + }, + .erase_display_complete => { + self.mode = .complete; + self.protected = value; + }, + .erase_display_scrollback => { + self.mode = .scrollback; + self.protected = value; + }, + .erase_display_scroll_complete => { + self.mode = .scroll_complete; + self.protected = value; + }, + else => {}, + } } }; @@ -2317,23 +2358,30 @@ test "stream: DECEL, DECSEL" { mode: ?csi.EraseLine = null, protected: ?bool = null, - pub fn eraseLine( - self: *Self, - mode: csi.EraseLine, - protected: bool, - ) !void { - self.mode = mode; - self.protected = protected; - } - pub fn vt( - self: *@This(), + self: *Self, comptime action: anytype, value: anytype, ) !void { - _ = self; - _ = action; - _ = value; + switch (action) { + .erase_line_right => { + self.mode = .right; + self.protected = value; + }, + .erase_line_left => { + self.mode = .left; + self.protected = value; + }, + .erase_line_complete => { + self.mode = .complete; + self.protected = value; + }, + .erase_line_right_unless_pending_wrap => { + self.mode = .right_unless_pending_wrap; + self.protected = value; + }, + else => {}, + } } }; diff --git a/src/termio/stream_handler.zig b/src/termio/stream_handler.zig index cbd4ef281..4cee8c1b3 100644 --- a/src/termio/stream_handler.zig +++ b/src/termio/stream_handler.zig @@ -218,6 +218,19 @@ pub const StreamHandler = struct { self.terminal.screen.cursor.y + 1 +| value.value, self.terminal.screen.cursor.x + 1, ), + .erase_display_below => self.terminal.eraseDisplay(.below, value), + .erase_display_above => self.terminal.eraseDisplay(.above, value), + .erase_display_complete => { + try self.terminal.scrollViewport(.{ .bottom = {} }); + try self.queueRender(); + self.terminal.eraseDisplay(.complete, value); + }, + .erase_display_scrollback => self.terminal.eraseDisplay(.scrollback, value), + .erase_display_scroll_complete => self.terminal.eraseDisplay(.scroll_complete, value), + .erase_line_right => self.terminal.eraseLine(.right, value), + .erase_line_left => self.terminal.eraseLine(.left, value), + .erase_line_complete => self.terminal.eraseLine(.complete, value), + .erase_line_right_unless_pending_wrap => self.terminal.eraseLine(.right_unless_pending_wrap, value), } } @@ -387,20 +400,6 @@ pub const StreamHandler = struct { try self.terminal.index(); } - pub inline fn eraseDisplay(self: *StreamHandler, mode: terminal.EraseDisplay, protected: bool) !void { - if (mode == .complete) { - // Whenever we erase the full display, scroll to bottom. - try self.terminal.scrollViewport(.{ .bottom = {} }); - try self.queueRender(); - } - - self.terminal.eraseDisplay(mode, protected); - } - - pub inline fn eraseLine(self: *StreamHandler, mode: terminal.EraseLine, protected: bool) !void { - self.terminal.eraseLine(mode, protected); - } - pub inline fn deleteChars(self: *StreamHandler, count: usize) !void { self.terminal.deleteChars(count); } From 37016d8b89aa3dc25da33fab42b9f850aac8c155 Mon Sep 17 00:00:00 2001 From: Mitchell Hashimoto Date: Thu, 23 Oct 2025 16:44:46 -0700 Subject: [PATCH 07/31] terminal: erase/insert lines, characters, etc. --- src/terminal/stream.zig | 130 +++++++++++++++++++--------------- src/termio/stream_handler.zig | 35 ++------- 2 files changed, 78 insertions(+), 87 deletions(-) diff --git a/src/terminal/stream.zig b/src/terminal/stream.zig index 25ce76f4b..9a3551491 100644 --- a/src/terminal/stream.zig +++ b/src/terminal/stream.zig @@ -57,6 +57,13 @@ pub const Action = union(Key) { erase_line_left: bool, erase_line_complete: bool, erase_line_right_unless_pending_wrap: bool, + delete_chars: usize, + erase_chars: usize, + insert_lines: usize, + insert_blanks: usize, + delete_lines: usize, + scroll_up: usize, + scroll_down: usize, pub const Key = lib.Enum( lib_target, @@ -88,6 +95,13 @@ pub const Action = union(Key) { "erase_line_left", "erase_line_complete", "erase_line_right_unless_pending_wrap", + "delete_chars", + "erase_chars", + "insert_lines", + "insert_blanks", + "delete_lines", + "scroll_up", + "scroll_down", }, ); @@ -722,11 +736,14 @@ pub fn Stream(comptime Handler: type) type { // IL - Insert Lines // TODO: test 'L' => switch (input.intermediates.len) { - 0 => if (@hasDecl(T, "insertLines")) switch (input.params.len) { - 0 => try self.handler.insertLines(1), - 1 => try self.handler.insertLines(input.params[0]), - else => log.warn("invalid IL command: {f}", .{input}), - } else log.warn("unimplemented CSI callback: {f}", .{input}), + 0 => try self.handler.vt(.insert_lines, switch (input.params.len) { + 0 => 1, + 1 => input.params[0], + else => { + log.warn("invalid IL command: {f}", .{input}); + return; + }, + }), else => log.warn( "ignoring unimplemented CSI L with intermediates: {s}", @@ -737,11 +754,14 @@ pub fn Stream(comptime Handler: type) type { // DL - Delete Lines // TODO: test 'M' => switch (input.intermediates.len) { - 0 => if (@hasDecl(T, "deleteLines")) switch (input.params.len) { - 0 => try self.handler.deleteLines(1), - 1 => try self.handler.deleteLines(input.params[0]), - else => log.warn("invalid DL command: {f}", .{input}), - } else log.warn("unimplemented CSI callback: {f}", .{input}), + 0 => try self.handler.vt(.delete_lines, switch (input.params.len) { + 0 => 1, + 1 => input.params[0], + else => { + log.warn("invalid DL command: {f}", .{input}); + return; + }, + }), else => log.warn( "ignoring unimplemented CSI M with intermediates: {s}", @@ -751,16 +771,14 @@ pub fn Stream(comptime Handler: type) type { // Delete Character (DCH) 'P' => switch (input.intermediates.len) { - 0 => if (@hasDecl(T, "deleteChars")) try self.handler.deleteChars( - switch (input.params.len) { - 0 => 1, - 1 => input.params[0], - else => { - log.warn("invalid delete characters command: {f}", .{input}); - return; - }, + 0 => try self.handler.vt(.delete_chars, switch (input.params.len) { + 0 => 1, + 1 => input.params[0], + else => { + log.warn("invalid delete characters command: {f}", .{input}); + return; }, - ) else log.warn("unimplemented CSI callback: {f}", .{input}), + }), else => log.warn( "ignoring unimplemented CSI P with intermediates: {s}", @@ -771,16 +789,14 @@ pub fn Stream(comptime Handler: type) type { // Scroll Up (SD) 'S' => switch (input.intermediates.len) { - 0 => if (@hasDecl(T, "scrollUp")) try self.handler.scrollUp( - switch (input.params.len) { - 0 => 1, - 1 => input.params[0], - else => { - log.warn("invalid scroll up command: {f}", .{input}); - return; - }, + 0 => try self.handler.vt(.scroll_up, switch (input.params.len) { + 0 => 1, + 1 => input.params[0], + else => { + log.warn("invalid scroll up command: {f}", .{input}); + return; }, - ) else log.warn("unimplemented CSI callback: {f}", .{input}), + }), else => log.warn( "ignoring unimplemented CSI S with intermediates: {s}", @@ -790,16 +806,14 @@ pub fn Stream(comptime Handler: type) type { // Scroll Down (SD) 'T' => switch (input.intermediates.len) { - 0 => if (@hasDecl(T, "scrollDown")) try self.handler.scrollDown( - switch (input.params.len) { - 0 => 1, - 1 => input.params[0], - else => { - log.warn("invalid scroll down command: {f}", .{input}); - return; - }, + 0 => try self.handler.vt(.scroll_down, switch (input.params.len) { + 0 => 1, + 1 => input.params[0], + else => { + log.warn("invalid scroll down command: {f}", .{input}); + return; }, - ) else log.warn("unimplemented CSI callback: {f}", .{input}), + }), else => log.warn( "ignoring unimplemented CSI T with intermediates: {s}", @@ -862,16 +876,14 @@ pub fn Stream(comptime Handler: type) type { // Erase Characters (ECH) 'X' => switch (input.intermediates.len) { - 0 => if (@hasDecl(T, "eraseChars")) try self.handler.eraseChars( - switch (input.params.len) { - 0 => 1, - 1 => input.params[0], - else => { - log.warn("invalid erase characters command: {f}", .{input}); - return; - }, + 0 => try self.handler.vt(.erase_chars, switch (input.params.len) { + 0 => 1, + 1 => input.params[0], + else => { + log.warn("invalid erase characters command: {f}", .{input}); + return; }, - ) else log.warn("unimplemented CSI callback: {f}", .{input}), + }), else => log.warn( "ignoring unimplemented CSI X with intermediates: {s}", @@ -1558,11 +1570,14 @@ pub fn Stream(comptime Handler: type) type { // ICH - Insert Blanks '@' => switch (input.intermediates.len) { - 0 => if (@hasDecl(T, "insertBlanks")) switch (input.params.len) { - 0 => try self.handler.insertBlanks(1), - 1 => try self.handler.insertBlanks(input.params[0]), - else => log.warn("invalid ICH command: {f}", .{input}), - } else log.warn("unimplemented CSI callback: {f}", .{input}), + 0 => try self.handler.vt(.insert_blanks, switch (input.params.len) { + 0 => 1, + 1 => input.params[0], + else => { + log.warn("invalid ICH command: {f}", .{input}); + return; + }, + }), else => log.warn( "ignoring unimplemented CSI @: {f}", @@ -2569,19 +2584,16 @@ test "stream: insert characters" { const Self = @This(); called: bool = false, - pub fn insertBlanks(self: *Self, v: u16) !void { - _ = v; - self.called = true; - } - pub fn vt( - self: *@This(), + self: *Self, comptime action: anytype, value: anytype, ) !void { - _ = self; - _ = action; _ = value; + switch (action) { + .insert_blanks => self.called = true, + else => {}, + } } }; diff --git a/src/termio/stream_handler.zig b/src/termio/stream_handler.zig index 4cee8c1b3..a05949424 100644 --- a/src/termio/stream_handler.zig +++ b/src/termio/stream_handler.zig @@ -231,6 +231,13 @@ pub const StreamHandler = struct { .erase_line_left => self.terminal.eraseLine(.left, value), .erase_line_complete => self.terminal.eraseLine(.complete, value), .erase_line_right_unless_pending_wrap => self.terminal.eraseLine(.right_unless_pending_wrap, value), + .delete_chars => self.terminal.deleteChars(value), + .erase_chars => self.terminal.eraseChars(value), + .insert_lines => self.terminal.insertLines(value), + .insert_blanks => self.terminal.insertBlanks(value), + .delete_lines => self.terminal.deleteLines(value), + .scroll_up => self.terminal.scrollUp(value), + .scroll_down => self.terminal.scrollDown(value), } } @@ -400,26 +407,6 @@ pub const StreamHandler = struct { try self.terminal.index(); } - pub inline fn deleteChars(self: *StreamHandler, count: usize) !void { - self.terminal.deleteChars(count); - } - - pub inline fn eraseChars(self: *StreamHandler, count: usize) !void { - self.terminal.eraseChars(count); - } - - pub inline fn insertLines(self: *StreamHandler, count: usize) !void { - self.terminal.insertLines(count); - } - - pub inline fn insertBlanks(self: *StreamHandler, count: usize) !void { - self.terminal.insertBlanks(count); - } - - pub inline fn deleteLines(self: *StreamHandler, count: usize) !void { - self.terminal.deleteLines(count); - } - pub inline fn reverseIndex(self: *StreamHandler) !void { self.terminal.reverseIndex(); } @@ -843,14 +830,6 @@ pub const StreamHandler = struct { self.messageWriter(try termio.Message.writeReq(self.alloc, self.enquiry_response)); } - pub inline fn scrollDown(self: *StreamHandler, count: usize) !void { - self.terminal.scrollDown(count); - } - - pub inline fn scrollUp(self: *StreamHandler, count: usize) !void { - self.terminal.scrollUp(count); - } - pub fn setActiveStatusDisplay( self: *StreamHandler, req: terminal.StatusDisplay, From dc5406781f19819c616de0e96bc08c109476c802 Mon Sep 17 00:00:00 2001 From: Mitchell Hashimoto Date: Thu, 23 Oct 2025 19:53:35 -0700 Subject: [PATCH 08/31] terminal: many more conversions --- src/lib/union.zig | 1 + src/terminal/stream.zig | 118 +++++++++++++++------------------- src/termio/stream_handler.zig | 21 ++---- 3 files changed, 58 insertions(+), 82 deletions(-) diff --git a/src/lib/union.zig b/src/lib/union.zig index f19cd3c7f..8e9f09049 100644 --- a/src/lib/union.zig +++ b/src/lib/union.zig @@ -121,6 +121,7 @@ pub fn TaggedUnion( /// Returns the value type for the given tag. pub fn Value(comptime tag: Tag) type { + @setEvalBranchQuota(2000); inline for (@typeInfo(Union).@"union".fields) |field| { const field_tag = @field(Tag, field.name); if (field_tag == tag) return field.type; diff --git a/src/terminal/stream.zig b/src/terminal/stream.zig index 9a3551491..71d54e4f3 100644 --- a/src/terminal/stream.zig +++ b/src/terminal/stream.zig @@ -31,6 +31,7 @@ const lib_target: lib.Target = if (build_options.c_abi) .c else .zig; pub const Action = union(Key) { print: Print, + print_repeat: usize, bell, backspace, horizontal_tab: u16, @@ -64,11 +65,16 @@ pub const Action = union(Key) { delete_lines: usize, scroll_up: usize, scroll_down: usize, + tab_clear_current, + tab_clear_all, + tab_set, + tab_reset, pub const Key = lib.Enum( lib_target, &.{ "print", + "print_repeat", "bell", "backspace", "horizontal_tab", @@ -102,6 +108,10 @@ pub const Action = union(Key) { "delete_lines", "scroll_up", "scroll_down", + "tab_clear_current", + "tab_clear_all", + "tab_set", + "tab_reset", }, ); @@ -827,11 +837,7 @@ pub fn Stream(comptime Handler: type) type { if (input.params.len == 0 or (input.params.len == 1 and input.params[0] == 0)) { - if (@hasDecl(T, "tabSet")) - try self.handler.tabSet() - else - log.warn("unimplemented tab set callback: {f}", .{input}); - + try self.handler.vt(.tab_set, {}); return; } @@ -841,15 +847,9 @@ pub fn Stream(comptime Handler: type) type { 1 => switch (input.params[0]) { 0 => unreachable, - 2 => if (@hasDecl(T, "tabClear")) - try self.handler.tabClear(.current) - else - log.warn("unimplemented tab clear callback: {f}", .{input}), + 2 => try self.handler.vt(.tab_clear_current, {}), - 5 => if (@hasDecl(T, "tabClear")) - try self.handler.tabClear(.all) - else - log.warn("unimplemented tab clear callback: {f}", .{input}), + 5 => try self.handler.vt(.tab_clear_all, {}), else => {}, }, @@ -862,10 +862,7 @@ pub fn Stream(comptime Handler: type) type { }, 1 => if (input.intermediates[0] == '?' and input.params[0] == 5) { - if (@hasDecl(T, "tabReset")) - try self.handler.tabReset() - else - log.warn("unimplemented tab reset callback: {f}", .{input}); + try self.handler.vt(.tab_reset, {}); } else log.warn("invalid cursor tabulation control: {f}", .{input}), else => log.warn( @@ -929,16 +926,14 @@ pub fn Stream(comptime Handler: type) type { // Repeat Previous Char (REP) 'b' => switch (input.intermediates.len) { - 0 => if (@hasDecl(T, "printRepeat")) try self.handler.printRepeat( - switch (input.params.len) { - 0 => 1, - 1 => input.params[0], - else => { - log.warn("invalid print repeat command: {f}", .{input}); - return; - }, + 0 => try self.handler.vt(.print_repeat, switch (input.params.len) { + 0 => 1, + 1 => input.params[0], + else => { + log.warn("invalid print repeat command: {f}", .{input}); + return; }, - ) else log.warn("unimplemented CSI callback: {f}", .{input}), + }), else => log.warn( "ignoring unimplemented CSI b with intermediates: {s}", @@ -1005,15 +1000,20 @@ pub fn Stream(comptime Handler: type) type { // TBC - Tab Clear // TODO: test 'g' => switch (input.intermediates.len) { - 0 => if (@hasDecl(T, "tabClear")) try self.handler.tabClear( - switch (input.params.len) { + 0 => { + const mode: csi.TabClear = switch (input.params.len) { 1 => @enumFromInt(input.params[0]), else => { log.warn("invalid tab clear command: {f}", .{input}); return; }, - }, - ) else log.warn("unimplemented CSI callback: {f}", .{input}), + }; + switch (mode) { + .current => try self.handler.vt(.tab_clear_current, {}), + .all => try self.handler.vt(.tab_clear_all, {}), + _ => log.warn("unknown tab clear mode: {}", .{mode}), + } + }, else => log.warn( "ignoring unimplemented CSI g with intermediates: {s}", @@ -1856,13 +1856,13 @@ pub fn Stream(comptime Handler: type) type { } else log.warn("unimplemented ESC callback: {f}", .{action}), // HTS - Horizontal Tab Set - 'H' => if (@hasDecl(T, "tabSet")) switch (action.intermediates.len) { - 0 => try self.handler.tabSet(), + 'H' => switch (action.intermediates.len) { + 0 => try self.handler.vt(.tab_set, {}), else => { log.warn("invalid tab set command: {f}", .{action}); return; }, - } else log.warn("unimplemented tab set callback: {f}", .{action}), + }, // RI - Reverse Index 'M' => if (@hasDecl(T, "reverseIndex")) switch (action.intermediates.len) { @@ -2979,99 +2979,85 @@ test "stream: CSI t pop title with index" { test "stream CSI W clear tab stops" { const H = struct { - op: ?csi.TabClear = null, - - pub fn tabClear(self: *@This(), op: csi.TabClear) !void { - self.op = op; - } + action: ?Action.Key = null, pub fn vt( self: *@This(), comptime action: anytype, value: anytype, ) !void { - _ = self; - _ = action; _ = value; + self.action = action; } }; var s: Stream(H) = .init(.{}); try s.nextSlice("\x1b[2W"); - try testing.expectEqual(csi.TabClear.current, s.handler.op.?); + try testing.expectEqual(Action.Key.tab_clear_current, s.handler.action.?); try s.nextSlice("\x1b[5W"); - try testing.expectEqual(csi.TabClear.all, s.handler.op.?); + try testing.expectEqual(Action.Key.tab_clear_all, s.handler.action.?); } test "stream CSI W tab set" { const H = struct { - called: bool = false, - - pub fn tabSet(self: *@This()) !void { - self.called = true; - } + action: ?Action.Key = null, pub fn vt( self: *@This(), comptime action: anytype, value: anytype, ) !void { - _ = self; - _ = action; _ = value; + self.action = action; } }; var s: Stream(H) = .init(.{}); try s.nextSlice("\x1b[W"); - try testing.expect(s.handler.called); + try testing.expectEqual(Action.Key.tab_set, s.handler.action.?); - s.handler.called = false; + s.handler.action = null; try s.nextSlice("\x1b[0W"); - try testing.expect(s.handler.called); + try testing.expectEqual(Action.Key.tab_set, s.handler.action.?); - s.handler.called = false; + s.handler.action = null; try s.nextSlice("\x1b[>W"); - try testing.expect(!s.handler.called); + try testing.expect(s.handler.action == null); - s.handler.called = false; + s.handler.action = null; try s.nextSlice("\x1b[99W"); - try testing.expect(!s.handler.called); + try testing.expect(s.handler.action == null); } test "stream CSI ? W reset tab stops" { const H = struct { - reset: bool = false, - - pub fn tabReset(self: *@This()) !void { - self.reset = true; - } + action: ?Action.Key = null, pub fn vt( self: *@This(), comptime action: anytype, value: anytype, ) !void { - _ = self; - _ = action; _ = value; + self.action = action; } }; var s: Stream(H) = .init(.{}); try s.nextSlice("\x1b[?2W"); - try testing.expect(!s.handler.reset); + try testing.expect(s.handler.action == null); try s.nextSlice("\x1b[?5W"); - try testing.expect(s.handler.reset); + try testing.expectEqual(Action.Key.tab_reset, s.handler.action.?); // Invalid and ignored by the handler + s.handler.action = null; try s.nextSlice("\x1b[?1;2;3W"); - try testing.expect(s.handler.reset); + try testing.expect(s.handler.action == null); } test "stream: SGR with 17+ parameters for underline color" { diff --git a/src/termio/stream_handler.zig b/src/termio/stream_handler.zig index a05949424..53da93f82 100644 --- a/src/termio/stream_handler.zig +++ b/src/termio/stream_handler.zig @@ -195,6 +195,7 @@ pub const StreamHandler = struct { ) !void { switch (action) { .print => try self.terminal.print(value.cp), + .print_repeat => try self.terminal.printRepeat(value), .bell => self.bell(), .backspace => self.terminal.backspace(), .horizontal_tab => try self.horizontalTab(value), @@ -238,6 +239,10 @@ pub const StreamHandler = struct { .delete_lines => self.terminal.deleteLines(value), .scroll_up => self.terminal.scrollUp(value), .scroll_down => self.terminal.scrollDown(value), + .tab_clear_current => self.terminal.tabClear(.current), + .tab_clear_all => self.terminal.tabClear(.all), + .tab_set => self.terminal.tabSet(), + .tab_reset => self.terminal.tabReset(), } } @@ -377,10 +382,6 @@ pub const StreamHandler = struct { } } - pub inline fn printRepeat(self: *StreamHandler, count: usize) !void { - try self.terminal.printRepeat(count); - } - inline fn bell(self: *StreamHandler) void { self.surfaceMessageWriter(.ring_bell); } @@ -805,18 +806,6 @@ pub const StreamHandler = struct { try self.terminal.decaln(); } - pub inline fn tabClear(self: *StreamHandler, cmd: terminal.TabClear) !void { - self.terminal.tabClear(cmd); - } - - pub inline fn tabSet(self: *StreamHandler) !void { - self.terminal.tabSet(); - } - - pub inline fn tabReset(self: *StreamHandler) !void { - self.terminal.tabReset(); - } - pub inline fn saveCursor(self: *StreamHandler) !void { self.terminal.saveCursor(); } From 94a8fa05cbaf6b234d612385a5670eae758a50d1 Mon Sep 17 00:00:00 2001 From: Mitchell Hashimoto Date: Thu, 23 Oct 2025 20:07:39 -0700 Subject: [PATCH 09/31] terminal: convert modes --- src/terminal/stream.zig | 63 +++++++++++++++++++++-------------- src/termio/stream_handler.zig | 24 ++++++------- 2 files changed, 48 insertions(+), 39 deletions(-) diff --git a/src/terminal/stream.zig b/src/terminal/stream.zig index 71d54e4f3..7ecd67750 100644 --- a/src/terminal/stream.zig +++ b/src/terminal/stream.zig @@ -69,6 +69,10 @@ pub const Action = union(Key) { tab_clear_all, tab_set, tab_reset, + set_mode: Mode, + reset_mode: Mode, + save_mode: Mode, + restore_mode: Mode, pub const Key = lib.Enum( lib_target, @@ -112,6 +116,10 @@ pub const Action = union(Key) { "tab_clear_all", "tab_set", "tab_reset", + "set_mode", + "reset_mode", + "save_mode", + "restore_mode", }, ); @@ -160,6 +168,16 @@ pub const Action = union(Key) { row: u16, col: u16, }; + + pub const Mode = struct { + mode: modes.Mode, + + pub const C = u16; + + pub fn cval(self: Mode) Mode.C { + return @bitCast(self.mode); + } + }; }; /// Returns a type that can process a stream of tty control characters. @@ -1022,7 +1040,7 @@ pub fn Stream(comptime Handler: type) type { }, // SM - Set Mode - 'h' => if (@hasDecl(T, "setMode")) mode: { + 'h' => mode: { const ansi_mode = ansi: { if (input.intermediates.len == 0) break :ansi true; if (input.intermediates.len == 1 and @@ -1034,15 +1052,15 @@ pub fn Stream(comptime Handler: type) type { for (input.params) |mode_int| { if (modes.modeFromInt(mode_int, ansi_mode)) |mode| { - try self.handler.setMode(mode, true); + try self.handler.vt(.set_mode, .{ .mode = mode }); } else { log.warn("unimplemented mode: {}", .{mode_int}); } } - } else log.warn("unimplemented CSI callback: {f}", .{input}), + }, // RM - Reset Mode - 'l' => if (@hasDecl(T, "setMode")) mode: { + 'l' => mode: { const ansi_mode = ansi: { if (input.intermediates.len == 0) break :ansi true; if (input.intermediates.len == 1 and @@ -1054,12 +1072,12 @@ pub fn Stream(comptime Handler: type) type { for (input.params) |mode_int| { if (modes.modeFromInt(mode_int, ansi_mode)) |mode| { - try self.handler.setMode(mode, false); + try self.handler.vt(.reset_mode, .{ .mode = mode }); } else { log.warn("unimplemented mode: {}", .{mode_int}); } } - } else log.warn("unimplemented CSI callback: {f}", .{input}), + }, // SGR - Select Graphic Rendition 'm' => switch (input.intermediates.len) { @@ -1304,10 +1322,10 @@ pub fn Stream(comptime Handler: type) type { 1 => switch (input.intermediates[0]) { // Restore Mode - '?' => if (@hasDecl(T, "restoreMode")) { + '?' => { for (input.params) |mode_int| { if (modes.modeFromInt(mode_int, false)) |mode| { - try self.handler.restoreMode(mode); + try self.handler.vt(.restore_mode, .{ .mode = mode }); } else { log.warn( "unimplemented restore mode: {}", @@ -1348,10 +1366,10 @@ pub fn Stream(comptime Handler: type) type { ), 1 => switch (input.intermediates[0]) { - '?' => if (@hasDecl(T, "saveMode")) { + '?' => { for (input.params) |mode_int| { if (modes.modeFromInt(mode_int, false)) |mode| { - try self.handler.saveMode(mode); + try self.handler.vt(.save_mode, .{ .mode = mode }); } else { log.warn( "unimplemented save mode: {}", @@ -2091,19 +2109,17 @@ test "stream: cursor right (CUF)" { test "stream: dec set mode (SM) and reset mode (RM)" { const H = struct { mode: modes.Mode = @as(modes.Mode, @enumFromInt(1)), - pub fn setMode(self: *@This(), mode: modes.Mode, v: bool) !void { - self.mode = @as(modes.Mode, @enumFromInt(1)); - if (v) self.mode = mode; - } pub fn vt( self: *@This(), comptime action: anytype, value: anytype, ) !void { - _ = self; - _ = action; - _ = value; + switch (action) { + .set_mode => self.mode = value.mode, + .reset_mode => self.mode = @as(modes.Mode, @enumFromInt(1)), + else => {}, + } } }; @@ -2123,19 +2139,16 @@ test "stream: ansi set mode (SM) and reset mode (RM)" { const H = struct { mode: ?modes.Mode = null, - pub fn setMode(self: *@This(), mode: modes.Mode, v: bool) !void { - self.mode = null; - if (v) self.mode = mode; - } - pub fn vt( self: *@This(), comptime action: anytype, value: anytype, ) !void { - _ = self; - _ = action; - _ = value; + switch (action) { + .set_mode => self.mode = value.mode, + .reset_mode => self.mode = null, + else => {}, + } } }; diff --git a/src/termio/stream_handler.zig b/src/termio/stream_handler.zig index 53da93f82..226a6fde9 100644 --- a/src/termio/stream_handler.zig +++ b/src/termio/stream_handler.zig @@ -243,6 +243,16 @@ pub const StreamHandler = struct { .tab_clear_all => self.terminal.tabClear(.all), .tab_set => self.terminal.tabSet(), .tab_reset => self.terminal.tabReset(), + .set_mode => try self.setMode(value.mode, true), + .reset_mode => try self.setMode(value.mode, false), + .save_mode => self.terminal.modes.save(value.mode), + .restore_mode => { + // For restore mode we have to restore but if we set it, we + // always have to call setMode because setting some modes have + // side effects and we want to make sure we process those. + const v = self.terminal.modes.restore(value.mode); + try self.setMode(value.mode, v); + }, } } @@ -470,20 +480,6 @@ pub const StreamHandler = struct { self.messageWriter(msg); } - pub inline fn saveMode(self: *StreamHandler, mode: terminal.Mode) !void { - // log.debug("save mode={}", .{mode}); - self.terminal.modes.save(mode); - } - - pub inline fn restoreMode(self: *StreamHandler, mode: terminal.Mode) !void { - // For restore mode we have to restore but if we set it, we - // always have to call setMode because setting some modes have - // side effects and we want to make sure we process those. - const v = self.terminal.modes.restore(mode); - // log.debug("restore mode={} v={}", .{ mode, v }); - try self.setMode(mode, v); - } - pub fn setMode(self: *StreamHandler, mode: terminal.Mode, enabled: bool) !void { // Note: this function doesn't need to grab the render state or // terminal locks because it is only called from process() which From b6ac4c764f41db8dfcca4ac8a40053148c30b790 Mon Sep 17 00:00:00 2001 From: Mitchell Hashimoto Date: Thu, 23 Oct 2025 20:15:05 -0700 Subject: [PATCH 10/31] terminal: modify_other_keys --- src/terminal/ansi.zig | 21 +++++++++++++++------ src/terminal/stream.zig | 32 ++++++++++++++++++-------------- src/termio/stream_handler.zig | 6 ++---- 3 files changed, 35 insertions(+), 24 deletions(-) diff --git a/src/terminal/ansi.zig b/src/terminal/ansi.zig index 590e9885a..7c18d933e 100644 --- a/src/terminal/ansi.zig +++ b/src/terminal/ansi.zig @@ -1,3 +1,7 @@ +const build_options = @import("terminal_options"); +const lib = @import("../lib/main.zig"); +const lib_target: lib.Target = if (build_options.c_abi) .c else .zig; + /// C0 (7-bit) control characters from ANSI. /// /// This is not complete, control characters are only added to this @@ -95,12 +99,17 @@ pub const StatusDisplay = enum(u16) { /// 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 }, -}; +pub const ModifyKeyFormat = lib.Enum( + lib_target, + &.{ + "legacy", + "cursor_keys", + "function_keys", + "other_keys_none", + "other_keys_numeric_except", + "other_keys_numeric", + }, +); /// The protection modes that can be set for the terminal. See DECSCA and /// ESC V, W. diff --git a/src/terminal/stream.zig b/src/terminal/stream.zig index 7ecd67750..2c237fbb0 100644 --- a/src/terminal/stream.zig +++ b/src/terminal/stream.zig @@ -73,6 +73,7 @@ pub const Action = union(Key) { reset_mode: Mode, save_mode: Mode, restore_mode: Mode, + modify_key_format: ansi.ModifyKeyFormat, pub const Key = lib.Enum( lib_target, @@ -120,6 +121,7 @@ pub const Action = union(Key) { "reset_mode", "save_mode", "restore_mode", + "modify_key_format", }, ); @@ -1094,18 +1096,18 @@ pub fn Stream(comptime Handler: type) type { } else log.warn("unimplemented CSI callback: {f}", .{input}), 1 => switch (input.intermediates[0]) { - '>' => if (@hasDecl(T, "setModifyKeyFormat")) blk: { + '>' => blk: { if (input.params.len == 0) { // Reset - try self.handler.setModifyKeyFormat(.{ .legacy = {} }); + try self.handler.vt(.modify_key_format, .legacy); break :blk; } var format: ansi.ModifyKeyFormat = switch (input.params[0]) { - 0 => .{ .legacy = {} }, - 1 => .{ .cursor_keys = {} }, - 2 => .{ .function_keys = {} }, - 4 => .{ .other_keys = .none }, + 0 => .legacy, + 1 => .cursor_keys, + 2 => .function_keys, + 4 => .other_keys_none, else => { log.warn("invalid setModifyKeyFormat: {f}", .{input}); break :blk; @@ -1125,15 +1127,17 @@ pub fn Stream(comptime Handler: type) type { .function_keys => {}, // We only support the numeric form. - .other_keys => |*v| switch (input.params[1]) { - 2 => v.* = .numeric, - else => v.* = .none, + .other_keys_none => switch (input.params[1]) { + 2 => format = .other_keys_numeric, + else => {}, }, + .other_keys_numeric_except => {}, + .other_keys_numeric => {}, } } - try self.handler.setModifyKeyFormat(format); - } else log.warn("unimplemented setModifyKeyFormat: {f}", .{input}), + try self.handler.vt(.modify_key_format, format); + }, else => log.warn( "unknown CSI m with intermediate: {}", @@ -1194,13 +1198,13 @@ pub fn Stream(comptime Handler: type) type { 0 => unreachable, // handled above 1 => switch (input.intermediates[0]) { - '>' => if (@hasDecl(T, "setModifyKeyFormat")) { + '>' => { // This isn't strictly correct. CSI > n has parameters that // control what exactly is being disabled. However, we // only support reverting back to modify other keys in // numeric except format. - try self.handler.setModifyKeyFormat(.{ .other_keys = .numeric_except }); - } else log.warn("unimplemented setModifyKeyFormat: {f}", .{input}), + try self.handler.vt(.modify_key_format, .other_keys_numeric_except); + }, else => log.warn( "unknown CSI n with intermediate: {}", diff --git a/src/termio/stream_handler.zig b/src/termio/stream_handler.zig index 226a6fde9..cb78ff264 100644 --- a/src/termio/stream_handler.zig +++ b/src/termio/stream_handler.zig @@ -253,6 +253,7 @@ pub const StreamHandler = struct { const v = self.terminal.modes.restore(value.mode); try self.setMode(value.mode, v); }, + .modify_key_format => try self.setModifyKeyFormat(value), } } @@ -450,10 +451,7 @@ pub const StreamHandler = struct { pub fn setModifyKeyFormat(self: *StreamHandler, format: terminal.ModifyKeyFormat) !void { self.terminal.flags.modify_other_keys_2 = false; switch (format) { - .other_keys => |v| switch (v) { - .numeric => self.terminal.flags.modify_other_keys_2 = true, - else => {}, - }, + .other_keys_numeric => self.terminal.flags.modify_other_keys_2 = true, else => {}, } } From 25eee9379db02e8eae33c0551dbb0a4eb7d8eb7c Mon Sep 17 00:00:00 2001 From: Mitchell Hashimoto Date: Thu, 23 Oct 2025 20:17:23 -0700 Subject: [PATCH 11/31] terminal: request mode --- src/terminal/stream.zig | 22 +++++++++++++++++++--- src/termio/stream_handler.zig | 28 ++++++++++++++++++++-------- 2 files changed, 39 insertions(+), 11 deletions(-) diff --git a/src/terminal/stream.zig b/src/terminal/stream.zig index 2c237fbb0..d9ec4c59b 100644 --- a/src/terminal/stream.zig +++ b/src/terminal/stream.zig @@ -73,6 +73,8 @@ pub const Action = union(Key) { reset_mode: Mode, save_mode: Mode, restore_mode: Mode, + request_mode: Mode, + request_mode_unknown: RawMode, modify_key_format: ansi.ModifyKeyFormat, pub const Key = lib.Enum( @@ -121,6 +123,8 @@ pub const Action = union(Key) { "reset_mode", "save_mode", "restore_mode", + "request_mode", + "request_mode_unknown", "modify_key_format", }, ); @@ -180,6 +184,11 @@ pub const Action = union(Key) { return @bitCast(self.mode); } }; + + pub const RawMode = extern struct { + mode: u16, + ansi: bool, + }; }; /// Returns a type that can process a stream of tty control characters. @@ -1242,9 +1251,16 @@ pub fn Stream(comptime Handler: type) type { break :decrqm; } - if (@hasDecl(T, "requestMode")) { - try self.handler.requestMode(input.params[0], ansi_mode); - } else log.warn("unimplemented DECRQM callback: {f}", .{input}); + const mode_raw = input.params[0]; + const mode = modes.modeFromInt(mode_raw, ansi_mode); + if (mode) |m| { + try self.handler.vt(.request_mode, .{ .mode = m }); + } else { + try self.handler.vt(.request_mode_unknown, .{ + .mode = mode_raw, + .ansi = ansi_mode, + }); + } }, else => log.warn( diff --git a/src/termio/stream_handler.zig b/src/termio/stream_handler.zig index cb78ff264..644613626 100644 --- a/src/termio/stream_handler.zig +++ b/src/termio/stream_handler.zig @@ -253,6 +253,8 @@ pub const StreamHandler = struct { const v = self.terminal.modes.restore(value.mode); try self.setMode(value.mode, v); }, + .request_mode => try self.requestMode(value.mode), + .request_mode_unknown => try self.requestModeUnknown(value.mode, value.ansi), .modify_key_format => try self.setModifyKeyFormat(value), } } @@ -456,22 +458,32 @@ pub const StreamHandler = struct { } } - pub fn requestMode(self: *StreamHandler, mode_raw: u16, ansi: bool) !void { - // Get the mode value and respond. - const code: u8 = code: { - const mode = terminal.modes.modeFromInt(mode_raw, ansi) orelse break :code 0; - if (self.terminal.modes.get(mode)) break :code 1; - break :code 2; - }; + fn requestMode(self: *StreamHandler, mode: terminal.Mode) !void { + const tag: terminal.modes.ModeTag = @bitCast(@intFromEnum(mode)); + const code: u8 = if (self.terminal.modes.get(mode)) 1 else 2; var msg: termio.Message = .{ .write_small = .{} }; const resp = try std.fmt.bufPrint( &msg.write_small.data, "\x1B[{s}{};{}$y", + .{ + if (tag.ansi) "" else "?", + tag.value, + code, + }, + ); + msg.write_small.len = @intCast(resp.len); + self.messageWriter(msg); + } + + fn requestModeUnknown(self: *StreamHandler, mode_raw: u16, ansi: bool) !void { + var msg: termio.Message = .{ .write_small = .{} }; + const resp = try std.fmt.bufPrint( + &msg.write_small.data, + "\x1B[{s}{};0$y", .{ if (ansi) "" else "?", mode_raw, - code, }, ); msg.write_small.len = @intCast(resp.len); From c1e57dd3304bf635c32e2d9069cbafd7cfcd70cd Mon Sep 17 00:00:00 2001 From: Mitchell Hashimoto Date: Thu, 23 Oct 2025 20:27:52 -0700 Subject: [PATCH 12/31] terminal: setprotectedmode --- src/terminal/stream.zig | 70 ++++++++++++++++++++--------------- src/termio/stream_handler.zig | 7 ++-- 2 files changed, 43 insertions(+), 34 deletions(-) diff --git a/src/terminal/stream.zig b/src/terminal/stream.zig index d9ec4c59b..21e7d1f51 100644 --- a/src/terminal/stream.zig +++ b/src/terminal/stream.zig @@ -76,6 +76,9 @@ pub const Action = union(Key) { request_mode: Mode, request_mode_unknown: RawMode, modify_key_format: ansi.ModifyKeyFormat, + protected_mode_off, + protected_mode_iso, + protected_mode_dec, pub const Key = lib.Enum( lib_target, @@ -126,6 +129,9 @@ pub const Action = union(Key) { "request_mode", "request_mode_unknown", "modify_key_format", + "protected_mode_off", + "protected_mode_iso", + "protected_mode_dec", }, ); @@ -1288,24 +1294,26 @@ pub fn Stream(comptime Handler: type) type { // DECSCA '"' => { - if (@hasDecl(T, "setProtectedMode")) { - const mode_: ?ansi.ProtectedMode = switch (input.params.len) { + const mode_: ?ansi.ProtectedMode = switch (input.params.len) { + else => null, + 0 => .off, + 1 => switch (input.params[0]) { + 0, 2 => .off, + 1 => .dec, else => null, - 0 => .off, - 1 => switch (input.params[0]) { - 0, 2 => .off, - 1 => .dec, - else => null, - }, - }; + }, + }; - const mode = mode_ orelse { - log.warn("invalid set protected mode command: {f}", .{input}); - return; - }; + const mode = mode_ orelse { + log.warn("invalid set protected mode command: {f}", .{input}); + return; + }; - try self.handler.setProtectedMode(mode); - } else log.warn("unimplemented CSI callback: {f}", .{input}); + switch (mode) { + .off => try self.handler.vt(.protected_mode_off, {}), + .iso => try self.handler.vt(.protected_mode_iso, {}), + .dec => try self.handler.vt(.protected_mode_dec, {}), + } }, // XTVERSION @@ -1930,14 +1938,16 @@ pub fn Stream(comptime Handler: type) type { } else log.warn("unimplemented invokeCharset: {f}", .{action}), // SPA - Start of Guarded Area - 'V' => if (@hasDecl(T, "setProtectedMode") and action.intermediates.len == 0) { - try self.handler.setProtectedMode(ansi.ProtectedMode.iso); - } else log.warn("unimplemented ESC callback: {f}", .{action}), + 'V' => switch (action.intermediates.len) { + 0 => try self.handler.vt(.protected_mode_iso, {}), + else => log.warn("unimplemented ESC callback: {f}", .{action}), + }, // EPA - End of Guarded Area - 'W' => if (@hasDecl(T, "setProtectedMode") and action.intermediates.len == 0) { - try self.handler.setProtectedMode(ansi.ProtectedMode.off); - } else log.warn("unimplemented ESC callback: {f}", .{action}), + 'W' => switch (action.intermediates.len) { + 0 => try self.handler.vt(.protected_mode_off, {}), + else => log.warn("unimplemented ESC callback: {f}", .{action}), + }, // DECID 'Z' => if (@hasDecl(T, "deviceAttributes") and action.intermediates.len == 0) { @@ -2269,18 +2279,18 @@ test "stream: DECSCA" { const Self = @This(); v: ?ansi.ProtectedMode = null, - pub fn setProtectedMode(self: *Self, v: ansi.ProtectedMode) !void { - self.v = v; - } - pub fn vt( - self: *@This(), - comptime action: anytype, - value: anytype, + self: *Self, + comptime action: Stream(Self).Action.Tag, + value: Stream(Self).Action.Value(action), ) !void { - _ = self; - _ = action; _ = value; + switch (action) { + .protected_mode_off => self.v = .off, + .protected_mode_iso => self.v = .iso, + .protected_mode_dec => self.v = .dec, + else => {}, + } } }; diff --git a/src/termio/stream_handler.zig b/src/termio/stream_handler.zig index 644613626..d66223081 100644 --- a/src/termio/stream_handler.zig +++ b/src/termio/stream_handler.zig @@ -256,6 +256,9 @@ pub const StreamHandler = struct { .request_mode => try self.requestMode(value.mode), .request_mode_unknown => try self.requestModeUnknown(value.mode, value.ansi), .modify_key_format => try self.setModifyKeyFormat(value), + .protected_mode_off => self.terminal.setProtectedMode(.off), + .protected_mode_iso => self.terminal.setProtectedMode(.iso), + .protected_mode_dec => self.terminal.setProtectedMode(.dec), } } @@ -804,10 +807,6 @@ pub const StreamHandler = struct { } } - pub inline fn setProtectedMode(self: *StreamHandler, mode: terminal.ProtectedMode) !void { - self.terminal.setProtectedMode(mode); - } - pub inline fn decaln(self: *StreamHandler) !void { try self.terminal.decaln(); } From 0df4d5c5a45846845468fb1ea7c75ed72e311027 Mon Sep 17 00:00:00 2001 From: Mitchell Hashimoto Date: Thu, 23 Oct 2025 20:37:32 -0700 Subject: [PATCH 13/31] terminal: margins --- src/lib/union.zig | 2 +- src/terminal/stream.zig | 95 ++++++++++++++++------------------- src/termio/stream_handler.zig | 25 ++++----- 3 files changed, 52 insertions(+), 70 deletions(-) diff --git a/src/lib/union.zig b/src/lib/union.zig index 8e9f09049..7e15aa84d 100644 --- a/src/lib/union.zig +++ b/src/lib/union.zig @@ -121,7 +121,7 @@ pub fn TaggedUnion( /// Returns the value type for the given tag. pub fn Value(comptime tag: Tag) type { - @setEvalBranchQuota(2000); + @setEvalBranchQuota(10000); inline for (@typeInfo(Union).@"union".fields) |field| { const field_tag = @field(Tag, field.name); if (field_tag == tag) return field.type; diff --git a/src/terminal/stream.zig b/src/terminal/stream.zig index 21e7d1f51..4b92e38f9 100644 --- a/src/terminal/stream.zig +++ b/src/terminal/stream.zig @@ -75,6 +75,9 @@ pub const Action = union(Key) { restore_mode: Mode, request_mode: Mode, request_mode_unknown: RawMode, + top_and_bottom_margin: Margin, + left_and_right_margin: Margin, + left_and_right_margin_ambiguous, modify_key_format: ansi.ModifyKeyFormat, protected_mode_off, protected_mode_iso, @@ -128,6 +131,9 @@ pub const Action = union(Key) { "restore_mode", "request_mode", "request_mode_unknown", + "top_and_bottom_margin", + "left_and_right_margin", + "left_and_right_margin_ambiguous", "modify_key_format", "protected_mode_off", "protected_mode_iso", @@ -195,6 +201,11 @@ pub const Action = union(Key) { mode: u16, ansi: bool, }; + + pub const Margin = extern struct { + top_left: u16, + bottom_right: u16, + }; }; /// Returns a type that can process a stream of tty control characters. @@ -1336,17 +1347,12 @@ pub fn Stream(comptime Handler: type) type { 'r' => switch (input.intermediates.len) { // DECSTBM - Set Top and Bottom Margins - 0 => if (@hasDecl(T, "setTopAndBottomMargin")) { - switch (input.params.len) { - 0 => try self.handler.setTopAndBottomMargin(0, 0), - 1 => try self.handler.setTopAndBottomMargin(input.params[0], 0), - 2 => try self.handler.setTopAndBottomMargin(input.params[0], input.params[1]), - else => log.warn("invalid DECSTBM command: {f}", .{input}), - } - } else log.warn( - "unimplemented CSI callback: {f}", - .{input}, - ), + 0 => switch (input.params.len) { + 0 => try self.handler.vt(.top_and_bottom_margin, .{ .top_left = 0, .bottom_right = 0 }), + 1 => try self.handler.vt(.top_and_bottom_margin, .{ .top_left = input.params[0], .bottom_right = 0 }), + 2 => try self.handler.vt(.top_and_bottom_margin, .{ .top_left = input.params[0], .bottom_right = input.params[1] }), + else => log.warn("invalid DECSTBM command: {f}", .{input}), + }, 1 => switch (input.intermediates[0]) { // Restore Mode @@ -1377,21 +1383,16 @@ pub fn Stream(comptime Handler: type) type { 's' => switch (input.intermediates.len) { // DECSLRM - 0 => if (@hasDecl(T, "setLeftAndRightMargin")) { - switch (input.params.len) { - // CSI S is ambiguous with zero params so we defer - // to our handler to do the proper logic. If mode 69 - // is set, then we should invoke DECSLRM, otherwise - // we should invoke SC. - 0 => try self.handler.setLeftAndRightMarginAmbiguous(), - 1 => try self.handler.setLeftAndRightMargin(input.params[0], 0), - 2 => try self.handler.setLeftAndRightMargin(input.params[0], input.params[1]), - else => log.warn("invalid DECSLRM command: {f}", .{input}), - } - } else log.warn( - "unimplemented CSI callback: {f}", - .{input}, - ), + 0 => switch (input.params.len) { + // CSI S is ambiguous with zero params so we defer + // to our handler to do the proper logic. If mode 69 + // is set, then we should invoke DECSLRM, otherwise + // we should invoke SC. + 0 => try self.handler.vt(.left_and_right_margin_ambiguous, {}), + 1 => try self.handler.vt(.left_and_right_margin, .{ .top_left = input.params[0], .bottom_right = 0 }), + 2 => try self.handler.vt(.left_and_right_margin, .{ .top_left = input.params[0], .bottom_right = input.params[1] }), + else => log.warn("invalid DECSLRM command: {f}", .{input}), + }, 1 => switch (input.intermediates[0]) { '?' => { @@ -2227,20 +2228,16 @@ test "stream: restore mode" { const Self = @This(); called: bool = false, - pub fn setTopAndBottomMargin(self: *Self, t: u16, b: u16) !void { - _ = t; - _ = b; - self.called = true; - } - pub fn vt( - self: *@This(), - comptime action: anytype, - value: anytype, + self: *Self, + comptime action: Stream(Self).Action.Tag, + value: Stream(Self).Action.Value(action), ) !void { - _ = self; - _ = action; _ = value; + switch (action) { + .top_and_bottom_margin => self.called = true, + else => {}, + } } }; @@ -2654,25 +2651,17 @@ test "stream: SCOSC" { const Self = @This(); called: bool = false, - pub fn setLeftAndRightMargin(self: *Self, left: u16, right: u16) !void { - _ = self; - _ = left; - _ = right; - @panic("bad"); - } - - pub fn setLeftAndRightMarginAmbiguous(self: *Self) !void { - self.called = true; - } - pub fn vt( - self: *@This(), - comptime action: anytype, - value: anytype, + self: *Self, + comptime action: Stream(Self).Action.Tag, + value: Stream(Self).Action.Value(action), ) !void { - _ = self; - _ = action; _ = value; + switch (action) { + .left_and_right_margin => @panic("bad"), + .left_and_right_margin_ambiguous => self.called = true, + else => {}, + } } }; diff --git a/src/termio/stream_handler.zig b/src/termio/stream_handler.zig index d66223081..e7e965e25 100644 --- a/src/termio/stream_handler.zig +++ b/src/termio/stream_handler.zig @@ -255,6 +255,15 @@ pub const StreamHandler = struct { }, .request_mode => try self.requestMode(value.mode), .request_mode_unknown => try self.requestModeUnknown(value.mode, value.ansi), + .top_and_bottom_margin => self.terminal.setTopAndBottomMargin(value.top_left, value.bottom_right), + .left_and_right_margin => self.terminal.setLeftAndRightMargin(value.top_left, value.bottom_right), + .left_and_right_margin_ambiguous => { + if (self.terminal.modes.get(.enable_left_and_right_margin)) { + self.terminal.setLeftAndRightMargin(0, 0); + } else { + self.terminal.saveCursor(); + } + }, .modify_key_format => try self.setModifyKeyFormat(value), .protected_mode_off => self.terminal.setProtectedMode(.off), .protected_mode_iso => self.terminal.setProtectedMode(.iso), @@ -437,22 +446,6 @@ pub const StreamHandler = struct { self.terminal.carriageReturn(); } - pub inline fn setTopAndBottomMargin(self: *StreamHandler, top: u16, bot: u16) !void { - self.terminal.setTopAndBottomMargin(top, bot); - } - - pub inline fn setLeftAndRightMarginAmbiguous(self: *StreamHandler) !void { - if (self.terminal.modes.get(.enable_left_and_right_margin)) { - try self.setLeftAndRightMargin(0, 0); - } else { - try self.saveCursor(); - } - } - - pub inline fn setLeftAndRightMargin(self: *StreamHandler, left: u16, right: u16) !void { - self.terminal.setLeftAndRightMargin(left, right); - } - pub fn setModifyKeyFormat(self: *StreamHandler, format: terminal.ModifyKeyFormat) !void { self.terminal.flags.modify_other_keys_2 = false; switch (format) { From b7ea979f38e7255fcdbe77430bdc6071ba6cd778 Mon Sep 17 00:00:00 2001 From: Mitchell Hashimoto Date: Thu, 23 Oct 2025 20:41:24 -0700 Subject: [PATCH 14/31] terminal: zero-arg actions --- src/terminal/stream.zig | 57 ++++++++++++++++++----------------- src/termio/stream_handler.zig | 8 +++++ 2 files changed, 38 insertions(+), 27 deletions(-) diff --git a/src/terminal/stream.zig b/src/terminal/stream.zig index 4b92e38f9..e85176dbd 100644 --- a/src/terminal/stream.zig +++ b/src/terminal/stream.zig @@ -78,10 +78,18 @@ pub const Action = union(Key) { top_and_bottom_margin: Margin, left_and_right_margin: Margin, left_and_right_margin_ambiguous, + save_cursor, + restore_cursor, modify_key_format: ansi.ModifyKeyFormat, protected_mode_off, protected_mode_iso, protected_mode_dec, + xtversion, + kitty_keyboard_query, + prompt_end, + end_of_input, + end_hyperlink, + decaln, pub const Key = lib.Enum( lib_target, @@ -134,10 +142,18 @@ pub const Action = union(Key) { "top_and_bottom_margin", "left_and_right_margin", "left_and_right_margin_ambiguous", + "save_cursor", + "restore_cursor", "modify_key_format", "protected_mode_off", "protected_mode_iso", "protected_mode_dec", + "xtversion", + "kitty_keyboard_query", + "prompt_end", + "end_of_input", + "end_hyperlink", + "decaln", }, ); @@ -1328,9 +1344,7 @@ pub fn Stream(comptime Handler: type) type { }, // XTVERSION - '>' => { - if (@hasDecl(T, "reportXtversion")) try self.handler.reportXtversion(); - }, + '>' => try self.handler.vt(.xtversion, {}), else => { log.warn( "ignoring unimplemented CSI q with intermediates: {s}", @@ -1548,9 +1562,7 @@ pub fn Stream(comptime Handler: type) type { // Kitty keyboard protocol 1 => switch (input.intermediates[0]) { - '?' => if (@hasDecl(T, "queryKittyKeyboard")) { - try self.handler.queryKittyKeyboard(); - }, + '?' => try self.handler.vt(.kitty_keyboard_query, {}), '>' => if (@hasDecl(T, "pushKittyKeyboard")) push: { const flags: u5 = if (input.params.len == 1) @@ -1698,17 +1710,11 @@ pub fn Stream(comptime Handler: type) type { }, .prompt_end => { - if (@hasDecl(T, "promptEnd")) { - try self.handler.promptEnd(); - return; - } else log.warn("unimplemented OSC callback: {}", .{cmd}); + try self.handler.vt(.prompt_end, {}); }, .end_of_input => { - if (@hasDecl(T, "endOfInput")) { - try self.handler.endOfInput(); - return; - } else log.warn("unimplemented OSC callback: {}", .{cmd}); + try self.handler.vt(.end_of_input, {}); }, .end_of_command => |end| { @@ -1770,10 +1776,7 @@ pub fn Stream(comptime Handler: type) type { }, .hyperlink_end => { - if (@hasDecl(T, "endHyperlink")) { - try self.handler.endHyperlink(); - return; - } else log.warn("unimplemented OSC callback: {}", .{cmd}); + try self.handler.vt(.end_hyperlink, {}); }, .conemu_progress_report => |v| { @@ -1852,28 +1855,28 @@ pub fn Stream(comptime Handler: type) type { '0' => try self.configureCharset(action.intermediates, .dec_special), // DECSC - Save Cursor - '7' => if (@hasDecl(T, "saveCursor")) switch (action.intermediates.len) { - 0 => try self.handler.saveCursor(), + '7' => switch (action.intermediates.len) { + 0 => try self.handler.vt(.save_cursor, {}), else => { log.warn("invalid command: {f}", .{action}); return; }, - } else log.warn("unimplemented ESC callback: {f}", .{action}), + }, '8' => blk: { switch (action.intermediates.len) { // DECRC - Restore Cursor - 0 => if (@hasDecl(T, "restoreCursor")) { - try self.handler.restoreCursor(); + 0 => { + try self.handler.vt(.restore_cursor, {}); break :blk {}; - } else log.warn("unimplemented restore cursor callback: {f}", .{action}), + }, 1 => switch (action.intermediates[0]) { // DECALN - Fill Screen with E - '#' => if (@hasDecl(T, "decaln")) { - try self.handler.decaln(); + '#' => { + try self.handler.vt(.decaln, {}); break :blk {}; - } else log.warn("unimplemented ESC callback: {f}", .{action}), + }, else => {}, }, diff --git a/src/termio/stream_handler.zig b/src/termio/stream_handler.zig index e7e965e25..68c7d00b7 100644 --- a/src/termio/stream_handler.zig +++ b/src/termio/stream_handler.zig @@ -264,10 +264,18 @@ pub const StreamHandler = struct { self.terminal.saveCursor(); } }, + .save_cursor => try self.saveCursor(), + .restore_cursor => try self.restoreCursor(), .modify_key_format => try self.setModifyKeyFormat(value), .protected_mode_off => self.terminal.setProtectedMode(.off), .protected_mode_iso => self.terminal.setProtectedMode(.iso), .protected_mode_dec => self.terminal.setProtectedMode(.dec), + .xtversion => try self.reportXtversion(), + .kitty_keyboard_query => try self.queryKittyKeyboard(), + .prompt_end => try self.promptEnd(), + .end_of_input => try self.endOfInput(), + .end_hyperlink => try self.endHyperlink(), + .decaln => try self.decaln(), } } From 2520e27aefddfb681c7e306ffac26c470c494fa6 Mon Sep 17 00:00:00 2001 From: Mitchell Hashimoto Date: Thu, 23 Oct 2025 20:49:50 -0700 Subject: [PATCH 15/31] terminal: kitty keyboard actions --- src/terminal/stream.zig | 66 +++++++++++++++++++++++------------ src/termio/stream_handler.zig | 42 +++++++++++----------- 2 files changed, 63 insertions(+), 45 deletions(-) diff --git a/src/terminal/stream.zig b/src/terminal/stream.zig index e85176dbd..b74764fb4 100644 --- a/src/terminal/stream.zig +++ b/src/terminal/stream.zig @@ -86,6 +86,11 @@ pub const Action = union(Key) { protected_mode_dec, xtversion, kitty_keyboard_query, + kitty_keyboard_push: KittyKeyboardFlags, + kitty_keyboard_pop: u16, + kitty_keyboard_set: KittyKeyboardFlags, + kitty_keyboard_set_or: KittyKeyboardFlags, + kitty_keyboard_set_not: KittyKeyboardFlags, prompt_end, end_of_input, end_hyperlink, @@ -150,6 +155,11 @@ pub const Action = union(Key) { "protected_mode_dec", "xtversion", "kitty_keyboard_query", + "kitty_keyboard_push", + "kitty_keyboard_pop", + "kitty_keyboard_set", + "kitty_keyboard_set_or", + "kitty_keyboard_set_not", "prompt_end", "end_of_input", "end_hyperlink", @@ -222,6 +232,16 @@ pub const Action = union(Key) { top_left: u16, bottom_right: u16, }; + + pub const KittyKeyboardFlags = struct { + flags: kitty.KeyFlags, + + pub const C = u8; + + pub fn cval(self: KittyKeyboardFlags) KittyKeyboardFlags.C { + return @intCast(self.flags.int()); + } + }; }; /// Returns a type that can process a stream of tty control characters. @@ -1564,7 +1584,7 @@ pub fn Stream(comptime Handler: type) type { 1 => switch (input.intermediates[0]) { '?' => try self.handler.vt(.kitty_keyboard_query, {}), - '>' => if (@hasDecl(T, "pushKittyKeyboard")) push: { + '>' => push: { const flags: u5 = if (input.params.len == 1) std.math.cast(u5, input.params[0]) orelse { log.warn("invalid pushKittyKeyboard command: {f}", .{input}); @@ -1573,19 +1593,19 @@ pub fn Stream(comptime Handler: type) type { else 0; - try self.handler.pushKittyKeyboard(@bitCast(flags)); + try self.handler.vt(.kitty_keyboard_push, .{ .flags = @as(kitty.KeyFlags, @bitCast(flags)) }); }, - '<' => if (@hasDecl(T, "popKittyKeyboard")) { + '<' => { const number: u16 = if (input.params.len == 1) input.params[0] else 1; - try self.handler.popKittyKeyboard(number); + try self.handler.vt(.kitty_keyboard_pop, number); }, - '=' => if (@hasDecl(T, "setKittyKeyboard")) set: { + '=' => set: { const flags: u5 = if (input.params.len >= 1) std.math.cast(u5, input.params[0]) orelse { log.warn("invalid setKittyKeyboard command: {f}", .{input}); @@ -1599,20 +1619,23 @@ pub fn Stream(comptime Handler: type) type { else 1; - const mode: kitty.KeySetMode = switch (number) { - 1 => .set, - 2 => .@"or", - 3 => .not, + const action_tag: streampkg.Action.Tag = switch (number) { + 1 => .kitty_keyboard_set, + 2 => .kitty_keyboard_set_or, + 3 => .kitty_keyboard_set_not, else => { log.warn("invalid setKittyKeyboard command: {f}", .{input}); break :set; }, }; - try self.handler.setKittyKeyboard( - mode, - @bitCast(flags), - ); + const kitty_flags: streampkg.Action.KittyKeyboardFlags = .{ .flags = @as(kitty.KeyFlags, @bitCast(flags)) }; + switch (action_tag) { + .kitty_keyboard_set => try self.handler.vt(.kitty_keyboard_set, kitty_flags), + .kitty_keyboard_set_or => try self.handler.vt(.kitty_keyboard_set_or, kitty_flags), + .kitty_keyboard_set_not => try self.handler.vt(.kitty_keyboard_set_not, kitty_flags), + else => unreachable, + } }, else => log.warn( @@ -2254,18 +2277,15 @@ test "stream: pop kitty keyboard with no params defaults to 1" { const Self = @This(); n: u16 = 0, - pub fn popKittyKeyboard(self: *Self, n: u16) !void { - self.n = n; - } - pub fn vt( - self: *@This(), - comptime action: anytype, - value: anytype, + self: *Self, + comptime action: streampkg.Action.Tag, + value: streampkg.Action.Value(action), ) !void { - _ = self; - _ = action; - _ = value; + switch (action) { + .kitty_keyboard_pop => self.n = value, + else => {}, + } } }; diff --git a/src/termio/stream_handler.zig b/src/termio/stream_handler.zig index 68c7d00b7..ba9746ca2 100644 --- a/src/termio/stream_handler.zig +++ b/src/termio/stream_handler.zig @@ -272,6 +272,26 @@ pub const StreamHandler = struct { .protected_mode_dec => self.terminal.setProtectedMode(.dec), .xtversion => try self.reportXtversion(), .kitty_keyboard_query => try self.queryKittyKeyboard(), + .kitty_keyboard_push => { + log.debug("pushing kitty keyboard mode: {}", .{value.flags}); + self.terminal.screen.kitty_keyboard.push(value.flags); + }, + .kitty_keyboard_pop => { + log.debug("popping kitty keyboard mode n={}", .{value}); + self.terminal.screen.kitty_keyboard.pop(@intCast(value)); + }, + .kitty_keyboard_set => { + log.debug("setting kitty keyboard mode: set {}", .{value.flags}); + self.terminal.screen.kitty_keyboard.set(.set, value.flags); + }, + .kitty_keyboard_set_or => { + log.debug("setting kitty keyboard mode: or {}", .{value.flags}); + self.terminal.screen.kitty_keyboard.set(.@"or", value.flags); + }, + .kitty_keyboard_set_not => { + log.debug("setting kitty keyboard mode: not {}", .{value.flags}); + self.terminal.screen.kitty_keyboard.set(.not, value.flags); + }, .prompt_end => try self.promptEnd(), .end_of_input => try self.endOfInput(), .end_hyperlink => try self.endHyperlink(), @@ -865,28 +885,6 @@ pub const StreamHandler = struct { }); } - pub fn pushKittyKeyboard( - self: *StreamHandler, - flags: terminal.kitty.KeyFlags, - ) !void { - log.debug("pushing kitty keyboard mode: {}", .{flags}); - self.terminal.screen.kitty_keyboard.push(flags); - } - - pub fn popKittyKeyboard(self: *StreamHandler, n: u16) !void { - log.debug("popping kitty keyboard mode n={}", .{n}); - self.terminal.screen.kitty_keyboard.pop(@intCast(n)); - } - - pub fn setKittyKeyboard( - self: *StreamHandler, - mode: terminal.kitty.KeySetMode, - flags: terminal.kitty.KeyFlags, - ) !void { - log.debug("setting kitty keyboard mode: {} {}", .{ mode, flags }); - self.terminal.screen.kitty_keyboard.set(mode, flags); - } - pub fn reportXtversion( self: *StreamHandler, ) !void { From f68ea7c907d93cb22d2c3c457bb79b60da1bc8a3 Mon Sep 17 00:00:00 2001 From: Mitchell Hashimoto Date: Thu, 23 Oct 2025 20:59:57 -0700 Subject: [PATCH 16/31] terminal: many more conversions --- src/terminal/csi.zig | 21 ++- src/terminal/stream.zig | 291 +++++++++++++--------------------- src/termio/stream_handler.zig | 11 +- 3 files changed, 129 insertions(+), 194 deletions(-) diff --git a/src/terminal/csi.zig b/src/terminal/csi.zig index 0cab9ed52..d2f4bd6f8 100644 --- a/src/terminal/csi.zig +++ b/src/terminal/csi.zig @@ -1,3 +1,7 @@ +const build_options = @import("terminal_options"); +const lib = @import("../lib/main.zig"); +const lib_target: lib.Target = if (build_options.c_abi) .c else .zig; + /// Modes for the ED CSI command. pub const EraseDisplay = enum(u8) { below = 0, @@ -33,13 +37,16 @@ pub const TabClear = enum(u8) { }; /// Style formats for terminal size reports. -pub const SizeReportStyle = enum { - // XTWINOPS - csi_14_t, - csi_16_t, - csi_18_t, - csi_21_t, -}; +pub const SizeReportStyle = lib.Enum( + lib_target, + &.{ + // XTWINOPS + "csi_14_t", + "csi_16_t", + "csi_18_t", + "csi_21_t", + }, +); /// XTWINOPS CSI 22/23 pub const TitlePushPop = struct { diff --git a/src/terminal/stream.zig b/src/terminal/stream.zig index b74764fb4..7ccb597b8 100644 --- a/src/terminal/stream.zig +++ b/src/terminal/stream.zig @@ -81,9 +81,13 @@ pub const Action = union(Key) { save_cursor, restore_cursor, modify_key_format: ansi.ModifyKeyFormat, + mouse_shift_capture: bool, protected_mode_off, protected_mode_iso, protected_mode_dec, + size_report: csi.SizeReportStyle, + title_push: u16, + title_pop: u16, xtversion, kitty_keyboard_query, kitty_keyboard_push: KittyKeyboardFlags, @@ -150,9 +154,13 @@ pub const Action = union(Key) { "save_cursor", "restore_cursor", "modify_key_format", + "mouse_shift_capture", "protected_mode_off", "protected_mode_iso", "protected_mode_dec", + "size_report", + "title_push", + "title_pop", "xtversion", "kitty_keyboard_query", "kitty_keyboard_push", @@ -1443,7 +1451,7 @@ pub fn Stream(comptime Handler: type) type { }, // XTSHIFTESCAPE - '>' => if (@hasDecl(T, "setMouseShiftCapture")) capture: { + '>' => capture: { const capture = switch (input.params.len) { 0 => false, 1 => switch (input.params[0]) { @@ -1460,11 +1468,8 @@ pub fn Stream(comptime Handler: type) type { }, }; - try self.handler.setMouseShiftCapture(capture); - } else log.warn( - "unimplemented CSI callback: {f}", - .{input}, - ), + try self.handler.vt(.mouse_shift_capture, capture); + }, else => log.warn( "unknown CSI s with intermediate: {f}", @@ -1485,48 +1490,28 @@ pub fn Stream(comptime Handler: type) type { switch (input.params[0]) { 14 => if (input.params.len == 1) { // report the text area size in pixels - if (@hasDecl(T, "sendSizeReport")) { - self.handler.sendSizeReport(.csi_14_t); - } else log.warn( - "ignoring unimplemented CSI 14 t", - .{}, - ); + try self.handler.vt(.size_report, .csi_14_t); } else log.warn( "ignoring CSI 14 t with extra parameters: {f}", .{input}, ), 16 => if (input.params.len == 1) { // report cell size in pixels - if (@hasDecl(T, "sendSizeReport")) { - self.handler.sendSizeReport(.csi_16_t); - } else log.warn( - "ignoring unimplemented CSI 16 t", - .{}, - ); + try self.handler.vt(.size_report, .csi_16_t); } else log.warn( "ignoring CSI 16 t with extra parameters: {f}", .{input}, ), 18 => if (input.params.len == 1) { // report screen size in characters - if (@hasDecl(T, "sendSizeReport")) { - self.handler.sendSizeReport(.csi_18_t); - } else log.warn( - "ignoring unimplemented CSI 18 t", - .{}, - ); + try self.handler.vt(.size_report, .csi_18_t); } else log.warn( "ignoring CSI 18 t with extra parameters: {f}", .{input}, ), 21 => if (input.params.len == 1) { // report window title - if (@hasDecl(T, "sendSizeReport")) { - self.handler.sendSizeReport(.csi_21_t); - } else log.warn( - "ignoring unimplemented CSI 21 t", - .{}, - ); + try self.handler.vt(.size_report, .csi_21_t); } else log.warn( "ignoring CSI 21 t with extra parameters: {f}", .{input}, @@ -1538,22 +1523,15 @@ pub fn Stream(comptime Handler: type) type { input.params[1] == 2)) { // push/pop title - if (@hasDecl(T, "pushPopTitle")) { - self.handler.pushPopTitle(.{ - .op = switch (number) { - 22 => .push, - 23 => .pop, - else => @compileError("unreachable"), - }, - .index = if (input.params.len == 3) - input.params[2] - else - 0, - }); - } else log.warn( - "ignoring unimplemented CSI 22/23 t", - .{}, - ); + const index: u16 = if (input.params.len == 3) + input.params[2] + else + 0; + switch (number) { + 22 => try self.handler.vt(.title_push, index), + 23 => try self.handler.vt(.title_pop, index), + else => @compileError("unreachable"), + } } else log.warn( "ignoring CSI 22/23 t with extra parameters: {f}", .{input}, @@ -1575,10 +1553,7 @@ pub fn Stream(comptime Handler: type) type { }, 'u' => switch (input.intermediates.len) { - 0 => if (@hasDecl(T, "restoreCursor")) - try self.handler.restoreCursor() - else - log.warn("unimplemented CSI callback: {f}", .{input}), + 0 => try self.handler.vt(.restore_cursor, {}), // Kitty keyboard protocol 1 => switch (input.intermediates[0]) { @@ -2575,18 +2550,15 @@ test "stream: XTSHIFTESCAPE" { const H = struct { escape: ?bool = null, - pub fn setMouseShiftCapture(self: *@This(), v: bool) !void { - self.escape = v; - } - pub fn vt( self: *@This(), - comptime action: anytype, - value: anytype, + comptime action: streampkg.Action.Tag, + value: streampkg.Action.Value(action), ) !void { - _ = self; - _ = action; - _ = value; + switch (action) { + .mouse_shift_capture => self.escape = value, + else => {}, + } } }; @@ -2698,18 +2670,16 @@ test "stream: SCORC" { const Self = @This(); called: bool = false, - pub fn restoreCursor(self: *Self) !void { - self.called = true; - } - pub fn vt( - self: *@This(), - comptime action: anytype, - value: anytype, + self: *Self, + comptime action: streampkg.Action.Tag, + value: streampkg.Action.Value(action), ) !void { - _ = self; - _ = action; _ = value; + switch (action) { + .restore_cursor => self.called = true, + else => {}, + } } }; @@ -2759,18 +2729,15 @@ test "stream: send report with CSI t" { const H = struct { style: ?csi.SizeReportStyle = null, - pub fn sendSizeReport(self: *@This(), style: csi.SizeReportStyle) void { - self.style = style; - } - pub fn vt( self: *@This(), - comptime action: anytype, - value: anytype, + comptime action: streampkg.Action.Tag, + value: streampkg.Action.Value(action), ) !void { - _ = self; - _ = action; - _ = value; + switch (action) { + .size_report => self.style = value, + else => {}, + } } }; @@ -2816,220 +2783,178 @@ test "stream: invalid CSI t" { test "stream: CSI t push title" { const H = struct { - op: ?csi.TitlePushPop = null, - - pub fn pushPopTitle(self: *@This(), op: csi.TitlePushPop) void { - self.op = op; - } + index: ?u16 = null, pub fn vt( self: *@This(), - comptime action: anytype, - value: anytype, + comptime action: streampkg.Action.Tag, + value: streampkg.Action.Value(action), ) !void { - _ = self; - _ = action; - _ = value; + switch (action) { + .title_push => self.index = value, + else => {}, + } } }; var s: Stream(H) = .init(.{}); try s.nextSlice("\x1b[22;0t"); - try testing.expectEqual(csi.TitlePushPop{ - .op = .push, - .index = 0, - }, s.handler.op.?); + try testing.expectEqual(@as(u16, 0), s.handler.index.?); } test "stream: CSI t push title with explicit window" { const H = struct { - op: ?csi.TitlePushPop = null, - - pub fn pushPopTitle(self: *@This(), op: csi.TitlePushPop) void { - self.op = op; - } + index: ?u16 = null, pub fn vt( self: *@This(), - comptime action: anytype, - value: anytype, + comptime action: streampkg.Action.Tag, + value: streampkg.Action.Value(action), ) !void { - _ = self; - _ = action; - _ = value; + switch (action) { + .title_push => self.index = value, + else => {}, + } } }; var s: Stream(H) = .init(.{}); try s.nextSlice("\x1b[22;2t"); - try testing.expectEqual(csi.TitlePushPop{ - .op = .push, - .index = 0, - }, s.handler.op.?); + try testing.expectEqual(@as(u16, 0), s.handler.index.?); } test "stream: CSI t push title with explicit icon" { const H = struct { - op: ?csi.TitlePushPop = null, - - pub fn pushPopTitle(self: *@This(), op: csi.TitlePushPop) void { - self.op = op; - } + index: ?u16 = null, pub fn vt( self: *@This(), - comptime action: anytype, - value: anytype, + comptime action: streampkg.Action.Tag, + value: streampkg.Action.Value(action), ) !void { - _ = self; - _ = action; - _ = value; + switch (action) { + .title_push => self.index = value, + else => {}, + } } }; var s: Stream(H) = .init(.{}); try s.nextSlice("\x1b[22;1t"); - try testing.expectEqual(null, s.handler.op); + try testing.expectEqual(null, s.handler.index); } test "stream: CSI t push title with index" { const H = struct { - op: ?csi.TitlePushPop = null, - - pub fn pushPopTitle(self: *@This(), op: csi.TitlePushPop) void { - self.op = op; - } + index: ?u16 = null, pub fn vt( self: *@This(), - comptime action: anytype, - value: anytype, + comptime action: streampkg.Action.Tag, + value: streampkg.Action.Value(action), ) !void { - _ = self; - _ = action; - _ = value; + switch (action) { + .title_push => self.index = value, + else => {}, + } } }; var s: Stream(H) = .init(.{}); try s.nextSlice("\x1b[22;0;5t"); - try testing.expectEqual(csi.TitlePushPop{ - .op = .push, - .index = 5, - }, s.handler.op.?); + try testing.expectEqual(@as(u16, 5), s.handler.index.?); } test "stream: CSI t pop title" { const H = struct { - op: ?csi.TitlePushPop = null, - - pub fn pushPopTitle(self: *@This(), op: csi.TitlePushPop) void { - self.op = op; - } + index: ?u16 = null, pub fn vt( self: *@This(), - comptime action: anytype, - value: anytype, + comptime action: streampkg.Action.Tag, + value: streampkg.Action.Value(action), ) !void { - _ = self; - _ = action; - _ = value; + switch (action) { + .title_pop => self.index = value, + else => {}, + } } }; var s: Stream(H) = .init(.{}); try s.nextSlice("\x1b[23;0t"); - try testing.expectEqual(csi.TitlePushPop{ - .op = .pop, - .index = 0, - }, s.handler.op.?); + try testing.expectEqual(@as(u16, 0), s.handler.index.?); } test "stream: CSI t pop title with explicit window" { const H = struct { - op: ?csi.TitlePushPop = null, - - pub fn pushPopTitle(self: *@This(), op: csi.TitlePushPop) void { - self.op = op; - } + index: ?u16 = null, pub fn vt( self: *@This(), - comptime action: anytype, - value: anytype, + comptime action: streampkg.Action.Tag, + value: streampkg.Action.Value(action), ) !void { - _ = self; - _ = action; - _ = value; + switch (action) { + .title_pop => self.index = value, + else => {}, + } } }; var s: Stream(H) = .init(.{}); try s.nextSlice("\x1b[23;2t"); - try testing.expectEqual(csi.TitlePushPop{ - .op = .pop, - .index = 0, - }, s.handler.op.?); + try testing.expectEqual(@as(u16, 0), s.handler.index.?); } test "stream: CSI t pop title with explicit icon" { const H = struct { - op: ?csi.TitlePushPop = null, - - pub fn pushPopTitle(self: *@This(), op: csi.TitlePushPop) void { - self.op = op; - } + index: ?u16 = null, pub fn vt( self: *@This(), - comptime action: anytype, - value: anytype, + comptime action: streampkg.Action.Tag, + value: streampkg.Action.Value(action), ) !void { - _ = self; - _ = action; - _ = value; + switch (action) { + .title_pop => self.index = value, + else => {}, + } } }; var s: Stream(H) = .init(.{}); try s.nextSlice("\x1b[23;1t"); - try testing.expectEqual(null, s.handler.op); + try testing.expectEqual(null, s.handler.index); } test "stream: CSI t pop title with index" { const H = struct { - op: ?csi.TitlePushPop = null, - - pub fn pushPopTitle(self: *@This(), op: csi.TitlePushPop) void { - self.op = op; - } + index: ?u16 = null, pub fn vt( self: *@This(), - comptime action: anytype, - value: anytype, + comptime action: streampkg.Action.Tag, + value: streampkg.Action.Value(action), ) !void { - _ = self; - _ = action; - _ = value; + switch (action) { + .title_pop => self.index = value, + else => {}, + } } }; var s: Stream(H) = .init(.{}); try s.nextSlice("\x1b[23;0;5t"); - try testing.expectEqual(csi.TitlePushPop{ - .op = .pop, - .index = 5, - }, s.handler.op.?); + try testing.expectEqual(@as(u16, 5), s.handler.index.?); } test "stream CSI W clear tab stops" { diff --git a/src/termio/stream_handler.zig b/src/termio/stream_handler.zig index ba9746ca2..b7fcd5834 100644 --- a/src/termio/stream_handler.zig +++ b/src/termio/stream_handler.zig @@ -270,6 +270,8 @@ pub const StreamHandler = struct { .protected_mode_off => self.terminal.setProtectedMode(.off), .protected_mode_iso => self.terminal.setProtectedMode(.iso), .protected_mode_dec => self.terminal.setProtectedMode(.dec), + .mouse_shift_capture => self.terminal.flags.mouse_shift_capture = if (value) .true else .false, + .size_report => self.sendSizeReport(value), .xtversion => try self.reportXtversion(), .kitty_keyboard_query => try self.queryKittyKeyboard(), .kitty_keyboard_push => { @@ -296,6 +298,11 @@ pub const StreamHandler = struct { .end_of_input => try self.endOfInput(), .end_hyperlink => try self.endHyperlink(), .decaln => try self.decaln(), + + // Unimplemented + .title_push, + .title_pop, + => {}, } } @@ -692,10 +699,6 @@ pub const StreamHandler = struct { } } - pub inline fn setMouseShiftCapture(self: *StreamHandler, v: bool) !void { - self.terminal.flags.mouse_shift_capture = if (v) .true else .false; - } - pub inline fn setAttribute(self: *StreamHandler, attr: terminal.Attribute) !void { switch (attr) { .unknown => |unk| log.warn("unimplemented or unknown SGR attribute: {any}", .{unk}), From 9cd45943568f102ff8db9ea7866e7ac103215d8e Mon Sep 17 00:00:00 2001 From: Mitchell Hashimoto Date: Thu, 23 Oct 2025 21:11:21 -0700 Subject: [PATCH 17/31] terminal: active status display --- src/terminal/ansi.zig | 11 +++++++---- src/terminal/stream.zig | 37 +++++++++++++++++++---------------- src/termio/stream_handler.zig | 8 +------- 3 files changed, 28 insertions(+), 28 deletions(-) diff --git a/src/terminal/ansi.zig b/src/terminal/ansi.zig index 7c18d933e..e4b2613d8 100644 --- a/src/terminal/ansi.zig +++ b/src/terminal/ansi.zig @@ -92,10 +92,13 @@ pub const StatusLineType = enum(u16) { }; /// The display to target for status updates (DECSASD). -pub const StatusDisplay = enum(u16) { - main = 0, - status_line = 1, -}; +pub const StatusDisplay = lib.Enum( + lib_target, + &.{ + "main", + "status_line", + }, +); /// The possible modify key formats to ESC[>{a};{b}m /// Note: this is not complete, we should add more as we support more diff --git a/src/terminal/stream.zig b/src/terminal/stream.zig index 7ccb597b8..45bf1e25d 100644 --- a/src/terminal/stream.zig +++ b/src/terminal/stream.zig @@ -98,6 +98,7 @@ pub const Action = union(Key) { prompt_end, end_of_input, end_hyperlink, + active_status_display: ansi.StatusDisplay, decaln, pub const Key = lib.Enum( @@ -171,6 +172,7 @@ pub const Action = union(Key) { "prompt_end", "end_of_input", "end_hyperlink", + "active_status_display", "decaln", }, ); @@ -1643,26 +1645,27 @@ pub fn Stream(comptime Handler: type) type { }, // DECSASD - Select Active Status Display - '}' => { - const success = decsasd: { - // Verify we're getting a DECSASD command - if (input.intermediates.len != 1 or input.intermediates[0] != '$') - break :decsasd false; - if (input.params.len != 1) - break :decsasd false; - if (!@hasDecl(T, "setActiveStatusDisplay")) - break :decsasd false; + '}' => decsasd: { + // Verify we're getting a DECSASD command + if (input.intermediates.len != 1 or input.intermediates[0] != '$') { + log.warn("unimplemented CSI callback: {f}", .{input}); + break :decsasd; + } + if (input.params.len != 1) { + log.warn("unimplemented CSI callback: {f}", .{input}); + break :decsasd; + } - const display = std.meta.intToEnum( - ansi.StatusDisplay, - input.params[0], - ) catch break :decsasd false; - - try self.handler.setActiveStatusDisplay(display); - break :decsasd true; + const display: ansi.StatusDisplay = switch (input.params[0]) { + 0 => .main, + 1 => .status_line, + else => { + log.warn("unimplemented CSI callback: {f}", .{input}); + break :decsasd; + }, }; - if (!success) log.warn("unimplemented CSI callback: {f}", .{input}); + try self.handler.vt(.active_status_display, display); }, else => if (@hasDecl(T, "csiUnimplemented")) diff --git a/src/termio/stream_handler.zig b/src/termio/stream_handler.zig index b7fcd5834..3f08610b7 100644 --- a/src/termio/stream_handler.zig +++ b/src/termio/stream_handler.zig @@ -297,6 +297,7 @@ pub const StreamHandler = struct { .prompt_end => try self.promptEnd(), .end_of_input => try self.endOfInput(), .end_hyperlink => try self.endHyperlink(), + .active_status_display => self.terminal.status_display = value, .decaln => try self.decaln(), // Unimplemented @@ -848,13 +849,6 @@ pub const StreamHandler = struct { self.messageWriter(try termio.Message.writeReq(self.alloc, self.enquiry_response)); } - pub fn setActiveStatusDisplay( - self: *StreamHandler, - req: terminal.StatusDisplay, - ) !void { - self.terminal.status_display = req; - } - pub fn configureCharset( self: *StreamHandler, slot: terminal.CharsetSlot, From b91149f0fe6fd826ba3bfb4a330a72f18a48fcff Mon Sep 17 00:00:00 2001 From: Mitchell Hashimoto Date: Thu, 23 Oct 2025 21:17:49 -0700 Subject: [PATCH 18/31] terminal: simple esc dispatch --- src/terminal/stream.zig | 46 +++++++++++++++++++++-------------- src/termio/stream_handler.zig | 4 +++ 2 files changed, 32 insertions(+), 18 deletions(-) diff --git a/src/terminal/stream.zig b/src/terminal/stream.zig index 45bf1e25d..fa58cb69d 100644 --- a/src/terminal/stream.zig +++ b/src/terminal/stream.zig @@ -69,6 +69,10 @@ pub const Action = union(Key) { tab_clear_all, tab_set, tab_reset, + index, + next_line, + reverse_index, + full_reset, set_mode: Mode, reset_mode: Mode, save_mode: Mode, @@ -143,6 +147,10 @@ pub const Action = union(Key) { "tab_clear_all", "tab_set", "tab_reset", + "index", + "next_line", + "reverse_index", + "full_reset", "set_mode", "reset_mode", "save_mode", @@ -1889,22 +1897,22 @@ pub fn Stream(comptime Handler: type) type { }, // IND - Index - 'D' => if (@hasDecl(T, "index")) switch (action.intermediates.len) { - 0 => try self.handler.index(), + 'D' => switch (action.intermediates.len) { + 0 => try self.handler.vt(.index, {}), else => { log.warn("invalid index command: {f}", .{action}); return; }, - } else log.warn("unimplemented ESC callback: {f}", .{action}), + }, // NEL - Next Line - 'E' => if (@hasDecl(T, "nextLine")) switch (action.intermediates.len) { - 0 => try self.handler.nextLine(), + 'E' => switch (action.intermediates.len) { + 0 => try self.handler.vt(.next_line, {}), else => { log.warn("invalid next line command: {f}", .{action}); return; }, - } else log.warn("unimplemented ESC callback: {f}", .{action}), + }, // HTS - Horizontal Tab Set 'H' => switch (action.intermediates.len) { @@ -1916,13 +1924,13 @@ pub fn Stream(comptime Handler: type) type { }, // RI - Reverse Index - 'M' => if (@hasDecl(T, "reverseIndex")) switch (action.intermediates.len) { - 0 => try self.handler.reverseIndex(), + 'M' => switch (action.intermediates.len) { + 0 => try self.handler.vt(.reverse_index, {}), else => { log.warn("invalid reverse index command: {f}", .{action}); return; }, - } else log.warn("unimplemented ESC callback: {f}", .{action}), + }, // SS2 - Single Shift 2 'N' => if (@hasDecl(T, "invokeCharset")) switch (action.intermediates.len) { @@ -1960,13 +1968,13 @@ pub fn Stream(comptime Handler: type) type { } else log.warn("unimplemented ESC callback: {f}", .{action}), // RIS - Full Reset - 'c' => if (@hasDecl(T, "fullReset")) switch (action.intermediates.len) { - 0 => try self.handler.fullReset(), + 'c' => switch (action.intermediates.len) { + 0 => try self.handler.vt(.full_reset, {}), else => { log.warn("invalid full reset command: {f}", .{action}); return; }, - } else log.warn("unimplemented ESC callback: {f}", .{action}), + }, // LS2 - Locking Shift 2 'n' => if (@hasDecl(T, "invokeCharset")) switch (action.intermediates.len) { @@ -2014,14 +2022,16 @@ pub fn Stream(comptime Handler: type) type { } else log.warn("unimplemented invokeCharset: {f}", .{action}), // Set application keypad mode - '=' => if (@hasDecl(T, "setMode") and action.intermediates.len == 0) { - try self.handler.setMode(.keypad_keys, true); - } else log.warn("unimplemented setMode: {f}", .{action}), + '=' => switch (action.intermediates.len) { + 0 => try self.handler.vt(.set_mode, .{ .mode = .keypad_keys }), + else => log.warn("unimplemented setMode: {f}", .{action}), + }, // Reset application keypad mode - '>' => if (@hasDecl(T, "setMode") and action.intermediates.len == 0) { - try self.handler.setMode(.keypad_keys, false); - } else log.warn("unimplemented setMode: {f}", .{action}), + '>' => switch (action.intermediates.len) { + 0 => try self.handler.vt(.reset_mode, .{ .mode = .keypad_keys }), + else => log.warn("unimplemented setMode: {f}", .{action}), + }, else => if (@hasDecl(T, "escUnimplemented")) try self.handler.escUnimplemented(action) diff --git a/src/termio/stream_handler.zig b/src/termio/stream_handler.zig index 3f08610b7..8e3722649 100644 --- a/src/termio/stream_handler.zig +++ b/src/termio/stream_handler.zig @@ -243,6 +243,10 @@ pub const StreamHandler = struct { .tab_clear_all => self.terminal.tabClear(.all), .tab_set => self.terminal.tabSet(), .tab_reset => self.terminal.tabReset(), + .index => try self.index(), + .next_line => try self.nextLine(), + .reverse_index => try self.reverseIndex(), + .full_reset => try self.fullReset(), .set_mode => try self.setMode(value.mode, true), .reset_mode => try self.setMode(value.mode, false), .save_mode => self.terminal.modes.save(value.mode), From 6902d89d9123484762f8610c27221ca64e778525 Mon Sep 17 00:00:00 2001 From: Mitchell Hashimoto Date: Thu, 23 Oct 2025 21:21:39 -0700 Subject: [PATCH 19/31] terminal: convert APC --- src/terminal/stream.zig | 18 +++++++++--------- src/termio/stream_handler.zig | 11 +++-------- 2 files changed, 12 insertions(+), 17 deletions(-) diff --git a/src/terminal/stream.zig b/src/terminal/stream.zig index fa58cb69d..1a8e983b5 100644 --- a/src/terminal/stream.zig +++ b/src/terminal/stream.zig @@ -99,6 +99,9 @@ pub const Action = union(Key) { kitty_keyboard_set: KittyKeyboardFlags, kitty_keyboard_set_or: KittyKeyboardFlags, kitty_keyboard_set_not: KittyKeyboardFlags, + apc_start, + apc_end, + apc_put: u8, prompt_end, end_of_input, end_hyperlink, @@ -177,6 +180,9 @@ pub const Action = union(Key) { "kitty_keyboard_set", "kitty_keyboard_set_or", "kitty_keyboard_set_not", + "apc_start", + "apc_end", + "apc_put", "prompt_end", "end_of_input", "end_hyperlink", @@ -559,15 +565,9 @@ pub fn Stream(comptime Handler: type) type { .dcs_unhook => if (@hasDecl(T, "dcsUnhook")) { try self.handler.dcsUnhook(); } else log.warn("unimplemented DCS unhook", .{}), - .apc_start => if (@hasDecl(T, "apcStart")) { - try self.handler.apcStart(); - } else log.warn("unimplemented APC start", .{}), - .apc_put => |code| if (@hasDecl(T, "apcPut")) { - try self.handler.apcPut(code); - } else log.warn("unimplemented APC put: {x}", .{code}), - .apc_end => if (@hasDecl(T, "apcEnd")) { - try self.handler.apcEnd(); - } else log.warn("unimplemented APC end", .{}), + .apc_start => try self.handler.vt(.apc_start, {}), + .apc_put => |code| try self.handler.vt(.apc_put, code), + .apc_end => try self.handler.vt(.apc_end, {}), } } } diff --git a/src/termio/stream_handler.zig b/src/termio/stream_handler.zig index 8e3722649..dbfa9ddb2 100644 --- a/src/termio/stream_handler.zig +++ b/src/termio/stream_handler.zig @@ -303,6 +303,9 @@ pub const StreamHandler = struct { .end_hyperlink => try self.endHyperlink(), .active_status_display => self.terminal.status_display = value, .decaln => try self.decaln(), + .apc_start => self.apc.start(), + .apc_end => try self.apcEnd(), + .apc_put => self.apc.feed(self.alloc, value), // Unimplemented .title_push, @@ -418,14 +421,6 @@ pub const StreamHandler = struct { } } - pub inline fn apcStart(self: *StreamHandler) !void { - self.apc.start(); - } - - pub inline fn apcPut(self: *StreamHandler, byte: u8) !void { - self.apc.feed(self.alloc, byte); - } - pub fn apcEnd(self: *StreamHandler) !void { var cmd = self.apc.end() orelse return; defer cmd.deinit(self.alloc); From 109376115bb2ff70517cca79019437501d5e0fe8 Mon Sep 17 00:00:00 2001 From: Mitchell Hashimoto Date: Thu, 23 Oct 2025 21:29:16 -0700 Subject: [PATCH 20/31] terminal: convert dcs --- src/terminal/Parser.zig | 8 ++++++++ src/terminal/stream.zig | 18 +++++++++--------- src/termio/stream_handler.zig | 3 +++ 3 files changed, 20 insertions(+), 9 deletions(-) diff --git a/src/terminal/Parser.zig b/src/terminal/Parser.zig index 625591d3f..4a02e2b13 100644 --- a/src/terminal/Parser.zig +++ b/src/terminal/Parser.zig @@ -127,6 +127,14 @@ pub const Action = union(enum) { intermediates: []const u8 = "", params: []const u16 = &.{}, final: u8, + + pub const C = extern struct { + intermediates: [*]const u8, + intermediates_len: usize, + params: [*]const u16, + params_len: usize, + final: u8, + }; }; // Implement formatter for logging. This is mostly copied from the diff --git a/src/terminal/stream.zig b/src/terminal/stream.zig index 1a8e983b5..2ef7c256b 100644 --- a/src/terminal/stream.zig +++ b/src/terminal/stream.zig @@ -99,6 +99,9 @@ pub const Action = union(Key) { kitty_keyboard_set: KittyKeyboardFlags, kitty_keyboard_set_or: KittyKeyboardFlags, kitty_keyboard_set_not: KittyKeyboardFlags, + dcs_hook: Parser.Action.DCS, + dcs_put: u8, + dcs_unhook, apc_start, apc_end, apc_put: u8, @@ -180,6 +183,9 @@ pub const Action = union(Key) { "kitty_keyboard_set", "kitty_keyboard_set_or", "kitty_keyboard_set_not", + "dcs_hook", + "dcs_put", + "dcs_unhook", "apc_start", "apc_end", "apc_put", @@ -556,15 +562,9 @@ pub fn Stream(comptime Handler: type) type { .csi_dispatch => |csi_action| try self.csiDispatch(csi_action), .esc_dispatch => |esc| try self.escDispatch(esc), .osc_dispatch => |cmd| try self.oscDispatch(cmd), - .dcs_hook => |dcs| if (@hasDecl(T, "dcsHook")) { - try self.handler.dcsHook(dcs); - } else log.warn("unimplemented DCS hook", .{}), - .dcs_put => |code| if (@hasDecl(T, "dcsPut")) { - try self.handler.dcsPut(code); - } else log.warn("unimplemented DCS put: {x}", .{code}), - .dcs_unhook => if (@hasDecl(T, "dcsUnhook")) { - try self.handler.dcsUnhook(); - } else log.warn("unimplemented DCS unhook", .{}), + .dcs_hook => |dcs| try self.handler.vt(.dcs_hook, dcs), + .dcs_put => |code| try self.handler.vt(.dcs_put, code), + .dcs_unhook => try self.handler.vt(.dcs_unhook, {}), .apc_start => try self.handler.vt(.apc_start, {}), .apc_put => |code| try self.handler.vt(.apc_put, code), .apc_end => try self.handler.vt(.apc_end, {}), diff --git a/src/termio/stream_handler.zig b/src/termio/stream_handler.zig index dbfa9ddb2..a44522573 100644 --- a/src/termio/stream_handler.zig +++ b/src/termio/stream_handler.zig @@ -303,6 +303,9 @@ pub const StreamHandler = struct { .end_hyperlink => try self.endHyperlink(), .active_status_display => self.terminal.status_display = value, .decaln => try self.decaln(), + .dcs_hook => try self.dcsHook(value), + .dcs_put => try self.dcsPut(value), + .dcs_unhook => try self.dcsUnhook(), .apc_start => self.apc.start(), .apc_end => try self.apcEnd(), .apc_put => self.apc.feed(self.alloc, value), From e347ab6915e7c3e0a3629a27dd672bce9613559b Mon Sep 17 00:00:00 2001 From: Mitchell Hashimoto Date: Fri, 24 Oct 2025 07:08:09 -0700 Subject: [PATCH 21/31] terminal: device attributes --- src/terminal/ansi.zig | 13 ++++++++----- src/terminal/stream.zig | 30 +++++++++++++++++------------- src/termio/stream_handler.zig | 4 +--- 3 files changed, 26 insertions(+), 21 deletions(-) diff --git a/src/terminal/ansi.zig b/src/terminal/ansi.zig index e4b2613d8..357910d54 100644 --- a/src/terminal/ansi.zig +++ b/src/terminal/ansi.zig @@ -53,11 +53,14 @@ pub const RenditionAspect = enum(u16) { }; /// The device attribute request type (ESC [ c). -pub const DeviceAttributeReq = enum { - primary, // Blank - secondary, // > - tertiary, // = -}; +pub const DeviceAttributeReq = lib.Enum( + lib_target, + &.{ + "primary", // Blank + "secondary", // > + "tertiary", // = + }, +); /// Possible cursor styles (ESC [ q) pub const CursorStyle = enum(u16) { diff --git a/src/terminal/stream.zig b/src/terminal/stream.zig index 2ef7c256b..a63665a92 100644 --- a/src/terminal/stream.zig +++ b/src/terminal/stream.zig @@ -93,6 +93,7 @@ pub const Action = union(Key) { title_push: u16, title_pop: u16, xtversion, + device_attributes: ansi.DeviceAttributeReq, kitty_keyboard_query, kitty_keyboard_push: KittyKeyboardFlags, kitty_keyboard_pop: u16, @@ -177,6 +178,7 @@ pub const Action = union(Key) { "title_push", "title_pop", "xtversion", + "device_attributes", "kitty_keyboard_query", "kitty_keyboard_push", "kitty_keyboard_pop", @@ -1042,22 +1044,24 @@ pub fn Stream(comptime Handler: type) type { }, // c - Device Attributes (DA1) - 'c' => if (@hasDecl(T, "deviceAttributes")) { - const req: ansi.DeviceAttributeReq = switch (input.intermediates.len) { - 0 => ansi.DeviceAttributeReq.primary, + 'c' => { + const req: ?ansi.DeviceAttributeReq = switch (input.intermediates.len) { + 0 => .primary, 1 => switch (input.intermediates[0]) { - '>' => ansi.DeviceAttributeReq.secondary, - '=' => ansi.DeviceAttributeReq.tertiary, + '>' => .secondary, + '=' => .tertiary, else => null, }, - else => @as(?ansi.DeviceAttributeReq, null), - } orelse { + else => null, + }; + + if (req) |r| { + try self.handler.vt(.device_attributes, r); + } else { log.warn("invalid device attributes command: {f}", .{input}); return; - }; - - try self.handler.deviceAttributes(req, input.params); - } else log.warn("unimplemented CSI callback: {f}", .{input}), + } + }, // VPA - Cursor Vertical Position Absolute 'd' => switch (input.intermediates.len) { @@ -1963,8 +1967,8 @@ pub fn Stream(comptime Handler: type) type { }, // DECID - 'Z' => if (@hasDecl(T, "deviceAttributes") and action.intermediates.len == 0) { - try self.handler.deviceAttributes(.primary, &.{}); + 'Z' => if (action.intermediates.len == 0) { + try self.handler.vt(.device_attributes, .primary); } else log.warn("unimplemented ESC callback: {f}", .{action}), // RIS - Full Reset diff --git a/src/termio/stream_handler.zig b/src/termio/stream_handler.zig index a44522573..11ca15ea9 100644 --- a/src/termio/stream_handler.zig +++ b/src/termio/stream_handler.zig @@ -277,6 +277,7 @@ pub const StreamHandler = struct { .mouse_shift_capture => self.terminal.flags.mouse_shift_capture = if (value) .true else .false, .size_report => self.sendSizeReport(value), .xtversion => try self.reportXtversion(), + .device_attributes => try self.deviceAttributes(value), .kitty_keyboard_query => try self.queryKittyKeyboard(), .kitty_keyboard_push => { log.debug("pushing kitty keyboard mode: {}", .{value.flags}); @@ -722,10 +723,7 @@ pub const StreamHandler = struct { pub fn deviceAttributes( self: *StreamHandler, req: terminal.DeviceAttributeReq, - params: []const u16, ) !void { - _ = params; - // For the below, we quack as a VT220. We don't quack as // a 420 because we don't support DCS sequences. switch (req) { From a4a37534d7a1a7bbe70fbadc8d42f49abf305ab0 Mon Sep 17 00:00:00 2001 From: Mitchell Hashimoto Date: Fri, 24 Oct 2025 07:11:06 -0700 Subject: [PATCH 22/31] terminal: missed some invoke charsets --- src/terminal/stream.zig | 70 ++++++++++++++++++++++++++++------------- 1 file changed, 49 insertions(+), 21 deletions(-) diff --git a/src/terminal/stream.zig b/src/terminal/stream.zig index a63665a92..252c50178 100644 --- a/src/terminal/stream.zig +++ b/src/terminal/stream.zig @@ -1937,22 +1937,30 @@ pub fn Stream(comptime Handler: type) type { }, // SS2 - Single Shift 2 - 'N' => if (@hasDecl(T, "invokeCharset")) switch (action.intermediates.len) { - 0 => try self.handler.invokeCharset(.GL, .G2, true), + 'N' => switch (action.intermediates.len) { + 0 => try self.handler.vt(.invoke_charset, .{ + .bank = .GL, + .charset = .G2, + .locking = true, + }), else => { log.warn("invalid single shift 2 command: {f}", .{action}); return; }, - } else log.warn("unimplemented invokeCharset: {f}", .{action}), + }, // SS3 - Single Shift 3 - 'O' => if (@hasDecl(T, "invokeCharset")) switch (action.intermediates.len) { - 0 => try self.handler.invokeCharset(.GL, .G3, true), + 'O' => switch (action.intermediates.len) { + 0 => try self.handler.vt(.invoke_charset, .{ + .bank = .GL, + .charset = .G3, + .locking = true, + }), else => { log.warn("invalid single shift 3 command: {f}", .{action}); return; }, - } else log.warn("unimplemented invokeCharset: {f}", .{action}), + }, // SPA - Start of Guarded Area 'V' => switch (action.intermediates.len) { @@ -1981,49 +1989,69 @@ pub fn Stream(comptime Handler: type) type { }, // LS2 - Locking Shift 2 - 'n' => if (@hasDecl(T, "invokeCharset")) switch (action.intermediates.len) { - 0 => try self.handler.invokeCharset(.GL, .G2, false), + 'n' => switch (action.intermediates.len) { + 0 => try self.handler.vt(.invoke_charset, .{ + .bank = .GL, + .charset = .G2, + .locking = false, + }), else => { log.warn("invalid single shift 2 command: {f}", .{action}); return; }, - } else log.warn("unimplemented invokeCharset: {f}", .{action}), + }, // LS3 - Locking Shift 3 - 'o' => if (@hasDecl(T, "invokeCharset")) switch (action.intermediates.len) { - 0 => try self.handler.invokeCharset(.GL, .G3, false), + 'o' => switch (action.intermediates.len) { + 0 => try self.handler.vt(.invoke_charset, .{ + .bank = .GL, + .charset = .G3, + .locking = false, + }), else => { log.warn("invalid single shift 3 command: {f}", .{action}); return; }, - } else log.warn("unimplemented invokeCharset: {f}", .{action}), + }, // LS1R - Locking Shift 1 Right - '~' => if (@hasDecl(T, "invokeCharset")) switch (action.intermediates.len) { - 0 => try self.handler.invokeCharset(.GR, .G1, false), + '~' => switch (action.intermediates.len) { + 0 => try self.handler.vt(.invoke_charset, .{ + .bank = .GR, + .charset = .G1, + .locking = false, + }), else => { log.warn("invalid locking shift 1 right command: {f}", .{action}); return; }, - } else log.warn("unimplemented invokeCharset: {f}", .{action}), + }, // LS2R - Locking Shift 2 Right - '}' => if (@hasDecl(T, "invokeCharset")) switch (action.intermediates.len) { - 0 => try self.handler.invokeCharset(.GR, .G2, false), + '}' => switch (action.intermediates.len) { + 0 => try self.handler.vt(.invoke_charset, .{ + .bank = .GR, + .charset = .G2, + .locking = false, + }), else => { log.warn("invalid locking shift 2 right command: {f}", .{action}); return; }, - } else log.warn("unimplemented invokeCharset: {f}", .{action}), + }, // LS3R - Locking Shift 3 Right - '|' => if (@hasDecl(T, "invokeCharset")) switch (action.intermediates.len) { - 0 => try self.handler.invokeCharset(.GR, .G3, false), + '|' => switch (action.intermediates.len) { + 0 => try self.handler.vt(.invoke_charset, .{ + .bank = .GR, + .charset = .G3, + .locking = false, + }), else => { log.warn("invalid locking shift 3 right command: {f}", .{action}); return; }, - } else log.warn("unimplemented invokeCharset: {f}", .{action}), + }, // Set application keypad mode '=' => switch (action.intermediates.len) { From fd0f9bb84307452d096f8fd1a4d370a85fabcf6e Mon Sep 17 00:00:00 2001 From: Mitchell Hashimoto Date: Fri, 24 Oct 2025 07:17:43 -0700 Subject: [PATCH 23/31] terminal: device attributes --- src/terminal/stream.zig | 21 ++++++++++++++------- src/termio/stream_handler.zig | 1 + 2 files changed, 15 insertions(+), 7 deletions(-) diff --git a/src/terminal/stream.zig b/src/terminal/stream.zig index 252c50178..72c2c8532 100644 --- a/src/terminal/stream.zig +++ b/src/terminal/stream.zig @@ -94,6 +94,7 @@ pub const Action = union(Key) { title_pop: u16, xtversion, device_attributes: ansi.DeviceAttributeReq, + device_status: DeviceStatus, kitty_keyboard_query, kitty_keyboard_push: KittyKeyboardFlags, kitty_keyboard_pop: u16, @@ -179,6 +180,7 @@ pub const Action = union(Key) { "title_pop", "xtversion", "device_attributes", + "device_status", "kitty_keyboard_query", "kitty_keyboard_push", "kitty_keyboard_pop", @@ -245,6 +247,16 @@ pub const Action = union(Key) { col: u16, }; + pub const DeviceStatus = struct { + request: device_status.Request, + + pub const C = u16; + + pub fn cval(self: DeviceStatus) DeviceStatus.C { + return @bitCast(self.request); + } + }; + pub const Mode = struct { mode: modes.Mode, @@ -1054,7 +1066,7 @@ pub fn Stream(comptime Handler: type) type { }, else => null, }; - + if (req) |r| { try self.handler.vt(.device_attributes, r); } else { @@ -1249,11 +1261,6 @@ pub fn Stream(comptime Handler: type) type { if (input.intermediates.len == 0 or input.intermediates[0] == '?') { - if (!@hasDecl(T, "deviceStatusReport")) { - log.warn("unimplemented CSI callback: {f}", .{input}); - return; - } - if (input.params.len != 1) { log.warn("invalid device status report command: {f}", .{input}); return; @@ -1273,7 +1280,7 @@ pub fn Stream(comptime Handler: type) type { return; }; - try self.handler.deviceStatusReport(req); + try self.handler.vt(.device_status, .{ .request = req }); return; } diff --git a/src/termio/stream_handler.zig b/src/termio/stream_handler.zig index 11ca15ea9..9d754158a 100644 --- a/src/termio/stream_handler.zig +++ b/src/termio/stream_handler.zig @@ -278,6 +278,7 @@ pub const StreamHandler = struct { .size_report => self.sendSizeReport(value), .xtversion => try self.reportXtversion(), .device_attributes => try self.deviceAttributes(value), + .device_status => try self.deviceStatusReport(value.request), .kitty_keyboard_query => try self.queryKittyKeyboard(), .kitty_keyboard_push => { log.debug("pushing kitty keyboard mode: {}", .{value.flags}); From bce1164ae6ce77a990999123160027e8ff190b20 Mon Sep 17 00:00:00 2001 From: Mitchell Hashimoto Date: Fri, 24 Oct 2025 07:25:14 -0700 Subject: [PATCH 24/31] terminal: cursor style --- src/terminal/ansi.zig | 32 ++++++++------------ src/terminal/stream.zig | 55 ++++++++++++++++++++--------------- src/termio/stream_handler.zig | 3 +- 3 files changed, 44 insertions(+), 46 deletions(-) diff --git a/src/terminal/ansi.zig b/src/terminal/ansi.zig index 357910d54..c9cd53666 100644 --- a/src/terminal/ansi.zig +++ b/src/terminal/ansi.zig @@ -63,26 +63,18 @@ pub const DeviceAttributeReq = lib.Enum( ); /// 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, - }; - } -}; +pub const CursorStyle = lib.Enum( + lib_target, + &.{ + "default", + "blinking_block", + "steady_block", + "blinking_underline", + "steady_underline", + "blinking_bar", + "steady_bar", + }, +); /// The status line type for DECSSDT. pub const StatusLineType = enum(u16) { diff --git a/src/terminal/stream.zig b/src/terminal/stream.zig index 72c2c8532..7442fb21c 100644 --- a/src/terminal/stream.zig +++ b/src/terminal/stream.zig @@ -49,6 +49,7 @@ pub const Action = union(Key) { cursor_col_relative: CursorMovement, cursor_row_relative: CursorMovement, cursor_pos: CursorPos, + cursor_style: ansi.CursorStyle, erase_display_below: bool, erase_display_above: bool, erase_display_complete: bool, @@ -135,6 +136,7 @@ pub const Action = union(Key) { "cursor_col_relative", "cursor_row_relative", "cursor_pos", + "cursor_style", "erase_display_below", "erase_display_above", "erase_display_complete", @@ -1356,16 +1358,27 @@ pub fn Stream(comptime Handler: type) type { // DECSCUSR - Select Cursor Style // TODO: test ' ' => { - if (@hasDecl(T, "setCursorStyle")) try self.handler.setCursorStyle( - switch (input.params.len) { - 0 => ansi.CursorStyle.default, - 1 => @enumFromInt(input.params[0]), + const style: ansi.CursorStyle = switch (input.params.len) { + 0 => .default, + 1 => switch (input.params[0]) { + 0 => .default, + 1 => .blinking_block, + 2 => .steady_block, + 3 => .blinking_underline, + 4 => .steady_underline, + 5 => .blinking_bar, + 6 => .steady_bar, else => { - log.warn("invalid set curor style command: {f}", .{input}); + log.warn("invalid cursor style value: {}", .{input.params[0]}); return; }, }, - ) else log.warn("unimplemented CSI callback: {f}", .{input}); + else => { + log.warn("invalid set curor style command: {f}", .{input}); + return; + }, + }; + try self.handler.vt(.cursor_style, style); }, // DECSCA @@ -2544,18 +2557,15 @@ test "stream: DECSCUSR" { const H = struct { style: ?ansi.CursorStyle = null, - pub fn setCursorStyle(self: *@This(), style: ansi.CursorStyle) !void { - self.style = style; - } - pub fn vt( self: *@This(), - comptime action: anytype, - value: anytype, + comptime action: Stream(@This()).Action.Tag, + value: Stream(@This()).Action.Value(action), ) !void { - _ = self; - _ = action; - _ = value; + switch (action) { + .cursor_style => self.style = value, + else => {}, + } } }; @@ -2575,18 +2585,15 @@ test "stream: DECSCUSR without space" { const H = struct { style: ?ansi.CursorStyle = null, - pub fn setCursorStyle(self: *@This(), style: ansi.CursorStyle) !void { - self.style = style; - } - pub fn vt( self: *@This(), - comptime action: anytype, - value: anytype, + comptime action: Stream(@This()).Action.Tag, + value: Stream(@This()).Action.Value(action), ) !void { - _ = self; - _ = action; - _ = value; + switch (action) { + .cursor_style => self.style = value, + else => {}, + } } }; diff --git a/src/termio/stream_handler.zig b/src/termio/stream_handler.zig index 9d754158a..32696c096 100644 --- a/src/termio/stream_handler.zig +++ b/src/termio/stream_handler.zig @@ -219,6 +219,7 @@ pub const StreamHandler = struct { self.terminal.screen.cursor.y + 1 +| value.value, self.terminal.screen.cursor.x + 1, ), + .cursor_style => try self.setCursorStyle(value), .erase_display_below => self.terminal.eraseDisplay(.below, value), .erase_display_above => self.terminal.eraseDisplay(.above, value), .erase_display_complete => { @@ -828,8 +829,6 @@ pub const StreamHandler = struct { self.terminal.screen.cursor.cursor_style = .bar; self.terminal.modes.set(.cursor_blinking, false); }, - - else => log.warn("unimplemented cursor style: {}", .{style}), } } From 4d028dac1f8f8e2a8d886ec9b37b0d71892ff051 Mon Sep 17 00:00:00 2001 From: Mitchell Hashimoto Date: Fri, 24 Oct 2025 10:53:57 -0700 Subject: [PATCH 25/31] terminal: some osc types --- src/lib/main.zig | 2 + src/lib/types.zig | 13 ++ src/terminal/stream.zig | 233 +++++++++++++++++++++++++--------- src/termio/stream_handler.zig | 34 +++-- 4 files changed, 209 insertions(+), 73 deletions(-) create mode 100644 src/lib/types.zig diff --git a/src/lib/main.zig b/src/lib/main.zig index cdddade09..5a626b1e8 100644 --- a/src/lib/main.zig +++ b/src/lib/main.zig @@ -1,9 +1,11 @@ const std = @import("std"); const enumpkg = @import("enum.zig"); +const types = @import("types.zig"); const unionpkg = @import("union.zig"); pub const allocator = @import("allocator.zig"); pub const Enum = enumpkg.Enum; +pub const String = types.String; pub const Struct = @import("struct.zig").Struct; pub const Target = @import("target.zig").Target; pub const TaggedUnion = unionpkg.TaggedUnion; diff --git a/src/lib/types.zig b/src/lib/types.zig new file mode 100644 index 000000000..758540d12 --- /dev/null +++ b/src/lib/types.zig @@ -0,0 +1,13 @@ +pub const String = extern struct { + ptr: [*]const u8, + len: usize, + + pub fn init(zig: anytype) String { + return switch (@TypeOf(zig)) { + []u8, []const u8 => .{ + .ptr = zig.ptr, + .len = zig.len, + }, + }; + } +}; diff --git a/src/terminal/stream.zig b/src/terminal/stream.zig index 7442fb21c..025e995c1 100644 --- a/src/terminal/stream.zig +++ b/src/terminal/stream.zig @@ -113,6 +113,16 @@ pub const Action = union(Key) { end_hyperlink, active_status_display: ansi.StatusDisplay, decaln, + window_title: WindowTitle, + report_pwd: ReportPwd, + show_desktop_notification: ShowDesktopNotification, + progress_report: osc.Command.ProgressReport, + start_hyperlink: StartHyperlink, + clipboard_contents: ClipboardContents, + prompt_start: PromptStart, + prompt_continuation: PromptContinuation, + end_of_command: EndOfCommand, + mouse_shape: MouseShape, pub const Key = lib.Enum( lib_target, @@ -200,6 +210,16 @@ pub const Action = union(Key) { "end_hyperlink", "active_status_display", "decaln", + "window_title", + "report_pwd", + "show_desktop_notification", + "progress_report", + "start_hyperlink", + "clipboard_contents", + "prompt_start", + "prompt_continuation", + "end_of_command", + "mouse_shape", }, ); @@ -288,6 +308,118 @@ pub const Action = union(Key) { return @intCast(self.flags.int()); } }; + + pub const WindowTitle = struct { + title: []const u8, + + pub const C = lib.String; + + pub fn cval(self: WindowTitle) WindowTitle.C { + return .init(self.title); + } + }; + + pub const ReportPwd = struct { + url: []const u8, + + pub const C = lib.String; + + pub fn cval(self: ReportPwd) ReportPwd.C { + return .init(self.url); + } + }; + + pub const ShowDesktopNotification = struct { + title: []const u8, + body: []const u8, + + pub const C = extern struct { + title: lib.String, + body: lib.String, + }; + + pub fn cval(self: ShowDesktopNotification) ShowDesktopNotification.C { + return .{ + .title = .init(self.title), + .body = .init(self.body), + }; + } + }; + + pub const StartHyperlink = struct { + uri: []const u8, + id: ?[]const u8, + + pub const C = extern struct { + uri: lib.String, + id: lib.String, + }; + + pub fn cval(self: StartHyperlink) StartHyperlink.C { + return .{ + .uri = .init(self.uri), + .id = .init(self.id orelse ""), + }; + } + }; + + pub const ClipboardContents = struct { + kind: u8, + data: []const u8, + + pub const C = extern struct { + kind: u8, + data: lib.String, + }; + + pub fn cval(self: ClipboardContents) ClipboardContents.C { + return .{ + .kind = self.kind, + .data = .init(self.data), + }; + } + }; + + pub const PromptStart = struct { + aid: ?[]const u8, + redraw: bool, + + pub const C = extern struct { + aid: lib.String, + redraw: bool, + }; + + pub fn cval(self: PromptStart) PromptStart.C { + return .{ + .aid = .init(self.aid orelse ""), + .redraw = self.redraw, + }; + } + }; + + pub const PromptContinuation = struct { + aid: ?[]const u8, + + pub const C = lib.String; + + pub fn cval(self: PromptContinuation) PromptContinuation.C { + return .init(self.aid orelse ""); + } + }; + + pub const EndOfCommand = struct { + exit_code: ?u8, + + pub const C = extern struct { + exit_code: i16, + }; + + pub fn cval(self: EndOfCommand) EndOfCommand.C { + return .{ + .exit_code = if (self.exit_code) |code| @intCast(code) else -1, + }; + } + }; }; /// Returns a type that can process a stream of tty control characters. @@ -1710,15 +1842,12 @@ pub fn Stream(comptime Handler: type) type { inline fn oscDispatch(self: *Self, cmd: osc.Command) !void { switch (cmd) { .change_window_title => |title| { - if (@hasDecl(T, "changeWindowTitle")) { - if (!std.unicode.utf8ValidateSlice(title)) { - log.warn("change title request: invalid utf-8, ignoring request", .{}); - return; - } - - try self.handler.changeWindowTitle(title); + if (!std.unicode.utf8ValidateSlice(title)) { + log.warn("change title request: invalid utf-8, ignoring request", .{}); return; - } else log.warn("unimplemented OSC callback: {}", .{cmd}); + } + + try self.handler.vt(.window_title, .{ .title = title }); }, .change_window_icon => |icon| { @@ -1726,54 +1855,43 @@ pub fn Stream(comptime Handler: type) type { }, .clipboard_contents => |clip| { - if (@hasDecl(T, "clipboardContents")) { - try self.handler.clipboardContents(clip.kind, clip.data); - return; - } else log.warn("unimplemented OSC callback: {}", .{cmd}); + try self.handler.vt(.clipboard_contents, .{ + .kind = clip.kind, + .data = clip.data, + }); }, .prompt_start => |v| { - if (@hasDecl(T, "promptStart")) { - switch (v.kind) { - .primary, .right => try self.handler.promptStart(v.aid, v.redraw), - .continuation, .secondary => try self.handler.promptContinuation(v.aid), - } - return; - } else log.warn("unimplemented OSC callback: {}", .{cmd}); + switch (v.kind) { + .primary, .right => try self.handler.vt(.prompt_start, .{ + .aid = v.aid, + .redraw = v.redraw, + }), + .continuation, .secondary => try self.handler.vt(.prompt_continuation, .{ + .aid = v.aid, + }), + } }, - .prompt_end => { - try self.handler.vt(.prompt_end, {}); - }, + .prompt_end => try self.handler.vt(.prompt_end, {}), - .end_of_input => { - try self.handler.vt(.end_of_input, {}); - }, + .end_of_input => try self.handler.vt(.end_of_input, {}), .end_of_command => |end| { - if (@hasDecl(T, "endOfCommand")) { - try self.handler.endOfCommand(end.exit_code); - return; - } else log.warn("unimplemented OSC callback: {}", .{cmd}); + try self.handler.vt(.end_of_command, .{ .exit_code = end.exit_code }); }, .report_pwd => |v| { - if (@hasDecl(T, "reportPwd")) { - try self.handler.reportPwd(v.value); - return; - } else log.warn("unimplemented OSC callback: {}", .{cmd}); + try self.handler.vt(.report_pwd, .{ .url = v.value }); }, .mouse_shape => |v| { - if (@hasDecl(T, "setMouseShape")) { - const shape = MouseShape.fromString(v.value) orelse { - log.warn("unknown cursor shape: {s}", .{v.value}); - return; - }; - - try self.handler.setMouseShape(shape); + const shape = MouseShape.fromString(v.value) orelse { + log.warn("unknown cursor shape: {s}", .{v.value}); return; - } else log.warn("unimplemented OSC callback: {}", .{cmd}); + }; + + try self.handler.vt(.mouse_shape, shape); }, .color_operation => |v| { @@ -1795,17 +1913,17 @@ pub fn Stream(comptime Handler: type) type { }, .show_desktop_notification => |v| { - if (@hasDecl(T, "showDesktopNotification")) { - try self.handler.showDesktopNotification(v.title, v.body); - return; - } else log.warn("unimplemented OSC callback: {}", .{cmd}); + try self.handler.vt(.show_desktop_notification, .{ + .title = v.title, + .body = v.body, + }); }, .hyperlink_start => |v| { - if (@hasDecl(T, "startHyperlink")) { - try self.handler.startHyperlink(v.uri, v.id); - return; - } else log.warn("unimplemented OSC callback: {}", .{cmd}); + try self.handler.vt(.start_hyperlink, .{ + .uri = v.uri, + .id = v.id, + }); }, .hyperlink_end => { @@ -1813,10 +1931,7 @@ pub fn Stream(comptime Handler: type) type { }, .conemu_progress_report => |v| { - if (@hasDecl(T, "handleProgressReport")) { - try self.handler.handleProgressReport(v); - return; - } else log.warn("unimplemented OSC callback: {}", .{cmd}); + try self.handler.vt(.progress_report, v); }, .conemu_sleep, @@ -2643,20 +2758,16 @@ test "stream: change window title with invalid utf-8" { const H = struct { seen: bool = false, - pub fn changeWindowTitle(self: *@This(), title: []const u8) !void { - _ = title; - - self.seen = true; - } - pub fn vt( self: *@This(), comptime action: anytype, value: anytype, ) !void { - _ = self; - _ = action; _ = value; + switch (action) { + .window_title => self.seen = true, + else => {}, + } } }; diff --git a/src/termio/stream_handler.zig b/src/termio/stream_handler.zig index 32696c096..d23e7606e 100644 --- a/src/termio/stream_handler.zig +++ b/src/termio/stream_handler.zig @@ -306,6 +306,16 @@ pub const StreamHandler = struct { .end_hyperlink => try self.endHyperlink(), .active_status_display => self.terminal.status_display = value, .decaln => try self.decaln(), + .window_title => try self.windowTitle(value.title), + .report_pwd => try self.reportPwd(value.url), + .show_desktop_notification => try self.showDesktopNotification(value.title, value.body), + .progress_report => self.progressReport(value), + .start_hyperlink => try self.startHyperlink(value.uri, value.id), + .clipboard_contents => try self.clipboardContents(value.kind, value.data), + .prompt_start => self.promptStart(value.aid, value.redraw), + .prompt_continuation => self.promptContinuation(value.aid), + .end_of_command => self.endOfCommand(value.exit_code), + .mouse_shape => try self.setMouseShape(value), .dcs_hook => try self.dcsHook(value), .dcs_put => try self.dcsPut(value), .dcs_unhook => try self.dcsUnhook(), @@ -714,7 +724,7 @@ pub const StreamHandler = struct { } } - pub inline fn startHyperlink(self: *StreamHandler, uri: []const u8, id: ?[]const u8) !void { + inline fn startHyperlink(self: *StreamHandler, uri: []const u8, id: ?[]const u8) !void { try self.terminal.screen.startHyperlink(uri, id); } @@ -902,7 +912,7 @@ pub const StreamHandler = struct { //------------------------------------------------------------------------- // OSC - pub fn changeWindowTitle(self: *StreamHandler, title: []const u8) !void { + fn windowTitle(self: *StreamHandler, title: []const u8) !void { var buf: [256]u8 = undefined; if (title.len >= buf.len) { log.warn("change title requested larger than our buffer size, ignoring", .{}); @@ -933,7 +943,7 @@ pub const StreamHandler = struct { self.surfaceMessageWriter(.{ .set_title = buf }); } - pub inline fn setMouseShape( + inline fn setMouseShape( self: *StreamHandler, shape: terminal.MouseShape, ) !void { @@ -945,7 +955,7 @@ pub const StreamHandler = struct { self.surfaceMessageWriter(.{ .set_mouse_shape = shape }); } - pub fn clipboardContents(self: *StreamHandler, kind: u8, data: []const u8) !void { + fn clipboardContents(self: *StreamHandler, kind: u8, data: []const u8) !void { // Note: we ignore the "kind" field and always use the standard clipboard. // iTerm also appears to do this but other terminals seem to only allow // certain. Let's investigate more. @@ -975,13 +985,13 @@ pub const StreamHandler = struct { }); } - pub inline fn promptStart(self: *StreamHandler, aid: ?[]const u8, redraw: bool) !void { + inline fn promptStart(self: *StreamHandler, aid: ?[]const u8, redraw: bool) void { _ = aid; self.terminal.markSemanticPrompt(.prompt); self.terminal.flags.shell_redraws_prompt = redraw; } - pub inline fn promptContinuation(self: *StreamHandler, aid: ?[]const u8) !void { + inline fn promptContinuation(self: *StreamHandler, aid: ?[]const u8) void { _ = aid; self.terminal.markSemanticPrompt(.prompt_continuation); } @@ -995,11 +1005,11 @@ pub const StreamHandler = struct { self.surfaceMessageWriter(.start_command); } - pub inline fn endOfCommand(self: *StreamHandler, exit_code: ?u8) !void { + inline fn endOfCommand(self: *StreamHandler, exit_code: ?u8) void { self.surfaceMessageWriter(.{ .stop_command = exit_code }); } - pub fn reportPwd(self: *StreamHandler, url: []const u8) !void { + fn reportPwd(self: *StreamHandler, url: []const u8) !void { // Special handling for the empty URL. We treat the empty URL // as resetting the pwd as if we never saw a pwd. I can't find any // other terminal that does this but it seems like a reasonable @@ -1013,7 +1023,7 @@ pub const StreamHandler = struct { // If we haven't seen a title, we're using the pwd as our title. // Set it to blank which will reset our title behavior. if (!self.seen_title) { - try self.changeWindowTitle(""); + try self.windowTitle(""); assert(!self.seen_title); } @@ -1093,7 +1103,7 @@ pub const StreamHandler = struct { // If we haven't seen a title, use our pwd as the title. if (!self.seen_title) { - try self.changeWindowTitle(path); + try self.windowTitle(path); self.seen_title = false; } } @@ -1347,7 +1357,7 @@ pub const StreamHandler = struct { } } - pub fn showDesktopNotification( + fn showDesktopNotification( self: *StreamHandler, title: []const u8, body: []const u8, @@ -1500,7 +1510,7 @@ pub const StreamHandler = struct { } /// Display a GUI progress report. - pub fn handleProgressReport(self: *StreamHandler, report: terminal.osc.Command.ProgressReport) error{}!void { + fn progressReport(self: *StreamHandler, report: terminal.osc.Command.ProgressReport) void { self.surfaceMessageWriter(.{ .progress_report = report }); } }; From 5ba451d073acacaed371fad90cce9ee799f73136 Mon Sep 17 00:00:00 2001 From: Mitchell Hashimoto Date: Fri, 24 Oct 2025 11:27:47 -0700 Subject: [PATCH 26/31] terminal: configureCharset --- src/terminal/Terminal.zig | 2 +- src/terminal/charsets.zig | 116 +++++++++++++++++----------------- src/terminal/main.zig | 1 + src/terminal/stream.zig | 18 +++--- src/termio/stream_handler.zig | 5 +- 5 files changed, 72 insertions(+), 70 deletions(-) diff --git a/src/terminal/Terminal.zig b/src/terminal/Terminal.zig index 69bcbcb84..a8cee90fb 100644 --- a/src/terminal/Terminal.zig +++ b/src/terminal/Terminal.zig @@ -581,7 +581,7 @@ fn printCell( if (unmapped_c > std.math.maxInt(u8)) break :c ' '; // Get our lookup table and map it - const table = set.table(); + const table = charsets.table(set); break :c @intCast(table[@intCast(unmapped_c)]); }; diff --git a/src/terminal/charsets.zig b/src/terminal/charsets.zig index 9d49832df..b4fd58efc 100644 --- a/src/terminal/charsets.zig +++ b/src/terminal/charsets.zig @@ -16,76 +16,74 @@ pub const ActiveSlot = LibEnum( ); /// The list of supported character sets and their associated tables. -pub const Charset = enum { - utf8, - ascii, - british, - dec_special, +pub const Charset = LibEnum( + if (build_options.c_abi) .c else .zig, + &.{ "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, +/// 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, + // 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, - }; - } -}; + // 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; + var tbl = initTable(); + tbl[0x23] = 0x00a3; + break :british tbl; }; /// 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; + var tbl = initTable(); + tbl[0x60] = 0x25C6; + tbl[0x61] = 0x2592; + tbl[0x62] = 0x2409; + tbl[0x63] = 0x240C; + tbl[0x64] = 0x240D; + tbl[0x65] = 0x240A; + tbl[0x66] = 0x00B0; + tbl[0x67] = 0x00B1; + tbl[0x68] = 0x2424; + tbl[0x69] = 0x240B; + tbl[0x6a] = 0x2518; + tbl[0x6b] = 0x2510; + tbl[0x6c] = 0x250C; + tbl[0x6d] = 0x2514; + tbl[0x6e] = 0x253C; + tbl[0x6f] = 0x23BA; + tbl[0x70] = 0x23BB; + tbl[0x71] = 0x2500; + tbl[0x72] = 0x23BC; + tbl[0x73] = 0x23BD; + tbl[0x74] = 0x251C; + tbl[0x75] = 0x2524; + tbl[0x76] = 0x2534; + tbl[0x77] = 0x252C; + tbl[0x78] = 0x2502; + tbl[0x79] = 0x2264; + tbl[0x7a] = 0x2265; + tbl[0x7b] = 0x03C0; + tbl[0x7c] = 0x2260; + tbl[0x7d] = 0x00A3; + tbl[0x7e] = 0x00B7; + break :tech tbl; }; /// Our table length is 256 so we can contain all ASCII chars. @@ -107,11 +105,11 @@ test { // utf8 has no table if (@field(Charset, field.name) == .utf8) continue; - const table = @field(Charset, field.name).table(); + const tbl = table(@field(Charset, field.name)); // 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); + try testing.expectEqual(@as(usize, 256), tbl.len); } } diff --git a/src/terminal/main.zig b/src/terminal/main.zig index 59b5d0d53..5c19af023 100644 --- a/src/terminal/main.zig +++ b/src/terminal/main.zig @@ -25,6 +25,7 @@ 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 charsetTable = charsets.table; pub const Cell = page.Cell; pub const Coordinate = point.Coordinate; pub const CSI = Parser.Action.CSI; diff --git a/src/terminal/stream.zig b/src/terminal/stream.zig index 025e995c1..d4d61f62b 100644 --- a/src/terminal/stream.zig +++ b/src/terminal/stream.zig @@ -123,6 +123,7 @@ pub const Action = union(Key) { prompt_continuation: PromptContinuation, end_of_command: EndOfCommand, mouse_shape: MouseShape, + configure_charset: ConfigureCharset, pub const Key = lib.Enum( lib_target, @@ -220,6 +221,7 @@ pub const Action = union(Key) { "prompt_continuation", "end_of_command", "mouse_shape", + "configure_charset", }, ); @@ -420,6 +422,11 @@ pub const Action = union(Key) { }; } }; + + pub const ConfigureCharset = lib.Struct(lib_target, struct { + slot: charsets.Slots, + charset: charsets.Charset, + }); }; /// Returns a type that can process a stream of tty control characters. @@ -1981,14 +1988,9 @@ pub fn Stream(comptime Handler: type) type { }, }; - if (@hasDecl(T, "configureCharset")) { - try self.handler.configureCharset(slot, set); - return; - } - - log.warn("unimplemented configureCharset callback slot={} set={}", .{ - slot, - set, + try self.handler.vt(.configure_charset, .{ + .slot = slot, + .charset = set, }); } diff --git a/src/termio/stream_handler.zig b/src/termio/stream_handler.zig index d23e7606e..4e5795a10 100644 --- a/src/termio/stream_handler.zig +++ b/src/termio/stream_handler.zig @@ -316,6 +316,7 @@ pub const StreamHandler = struct { .prompt_continuation => self.promptContinuation(value.aid), .end_of_command => self.endOfCommand(value.exit_code), .mouse_shape => try self.setMouseShape(value), + .configure_charset => self.configureCharset(value.slot, value.charset), .dcs_hook => try self.dcsHook(value), .dcs_put => try self.dcsPut(value), .dcs_unhook => try self.dcsUnhook(), @@ -859,11 +860,11 @@ pub const StreamHandler = struct { self.messageWriter(try termio.Message.writeReq(self.alloc, self.enquiry_response)); } - pub fn configureCharset( + fn configureCharset( self: *StreamHandler, slot: terminal.CharsetSlot, set: terminal.Charset, - ) !void { + ) void { self.terminal.configureCharset(slot, set); } From 56376a8a384adc7a7f28011922561a8d9f596305 Mon Sep 17 00:00:00 2001 From: Mitchell Hashimoto Date: Fri, 24 Oct 2025 11:40:10 -0700 Subject: [PATCH 27/31] sgr: make C compat --- src/lib/union.zig | 2 +- src/terminal/color.zig | 20 ++++++++ src/terminal/sgr.zig | 103 +++++++++++++++++++++++++++++++++++++---- 3 files changed, 114 insertions(+), 11 deletions(-) diff --git a/src/lib/union.zig b/src/lib/union.zig index 7e15aa84d..9fe5e999c 100644 --- a/src/lib/union.zig +++ b/src/lib/union.zig @@ -104,7 +104,7 @@ pub fn TaggedUnion( @tagName(tag), value: { switch (@typeInfo(@TypeOf(v))) { - .@"enum", .@"struct", .@"union" => if (@hasDecl(@TypeOf(v), "cval")) v.cval(), + .@"enum", .@"struct", .@"union" => if (@hasDecl(@TypeOf(v), "cval")) break :value v.cval(), else => {}, } diff --git a/src/terminal/color.zig b/src/terminal/color.zig index d108e205b..b71279dbb 100644 --- a/src/terminal/color.zig +++ b/src/terminal/color.zig @@ -68,6 +68,12 @@ pub const Name = enum(u8) { // Remainders are valid unnamed values in the 256 color palette. _, + pub const C = u8; + + pub fn cval(self: Name) C { + return @intFromEnum(self); + } + /// Default colors for tagged values. pub fn default(self: Name) !RGB { return switch (self) { @@ -179,6 +185,20 @@ pub const RGB = packed struct(u24) { g: u8 = 0, b: u8 = 0, + pub const C = extern struct { + r: u8, + g: u8, + b: u8, + }; + + pub fn cval(self: RGB) C { + return .{ + .r = self.r, + .g = self.g, + .b = self.b, + }; + } + pub fn eql(self: RGB, other: RGB) bool { return self.r == other.r and self.g == other.g and self.b == other.b; } diff --git a/src/terminal/sgr.zig b/src/terminal/sgr.zig index d589172ad..a345a7a90 100644 --- a/src/terminal/sgr.zig +++ b/src/terminal/sgr.zig @@ -1,26 +1,22 @@ //! SGR (Select Graphic Rendition) attrinvbute parsing and types. const std = @import("std"); +const build_options = @import("terminal_options"); const assert = std.debug.assert; const testing = std.testing; +const lib = @import("../lib/main.zig"); const color = @import("color.zig"); const SepList = @import("Parser.zig").Action.CSI.SepList; -/// Attribute type for SGR -pub const Attribute = union(enum) { - pub const Tag = std.meta.FieldEnum(Attribute); +const lib_target: lib.Target = if (build_options.c_abi) .c else .zig; +/// Attribute type for SGR +pub const Attribute = union(Tag) { /// Unset all attributes unset, /// 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, - }, + unknown: Unknown, /// Bold the text. bold, @@ -85,6 +81,68 @@ pub const Attribute = union(enum) { /// Set foreground color as 256-color palette. @"256_fg": u8, + pub const Tag = lib.Enum( + lib_target, + &.{ + "unset", + "unknown", + "bold", + "reset_bold", + "italic", + "reset_italic", + "faint", + "underline", + "reset_underline", + "underline_color", + "256_underline_color", + "reset_underline_color", + "overline", + "reset_overline", + "blink", + "reset_blink", + "inverse", + "reset_inverse", + "invisible", + "reset_invisible", + "strikethrough", + "reset_strikethrough", + "direct_color_fg", + "direct_color_bg", + "8_bg", + "8_fg", + "reset_fg", + "reset_bg", + "8_bright_bg", + "8_bright_fg", + "256_bg", + "256_fg", + }, + ); + + pub const Unknown = struct { + /// Full is the full SGR input. + full: []const u16, + + /// Partial is the remaining, where we got hung up. + partial: []const u16, + + pub const C = extern struct { + full_ptr: [*]const u16, + full_len: usize, + partial_ptr: [*]const u16, + partial_len: usize, + }; + + pub fn cval(self: Unknown) Unknown.C { + return .{ + .full_ptr = self.full.ptr, + .full_len = self.full.len, + .partial_ptr = self.partial.ptr, + .partial_len = self.partial.len, + }; + } + }; + pub const Underline = enum(u3) { none = 0, single = 1, @@ -92,7 +150,28 @@ pub const Attribute = union(enum) { curly = 3, dotted = 4, dashed = 5, + + pub const C = u8; + + pub fn cval(self: Underline) Underline.C { + return @intFromEnum(self); + } }; + + /// C ABI functions. + const c_union = lib.TaggedUnion( + lib_target, + @This(), + // Padding size for C ABI compatibility. + // Largest variant is Unknown.C: 2 pointers + 2 usize = 32 bytes on 64-bit. + // We use [8]u64 (64 bytes) to allow room for future expansion while + // maintaining ABI compatibility. + [8]u64, + ); + pub const Value = c_union.Value; + pub const C = c_union.C; + pub const CValue = c_union.CValue; + pub const cval = c_union.cval; }; /// Parser parses the attributes from a list of SGR parameters. @@ -380,6 +459,10 @@ fn testParseColon(params: []const u16) Attribute { return p.next().?; } +test "sgr: Attribute C compat" { + _ = Attribute.C; +} + test "sgr: Parser" { try testing.expect(testParse(&[_]u16{}) == .unset); try testing.expect(testParse(&[_]u16{0}) == .unset); From e49694439c2a0412ea35c506692f82fdb893d7d4 Mon Sep 17 00:00:00 2001 From: Mitchell Hashimoto Date: Fri, 24 Oct 2025 11:51:30 -0700 Subject: [PATCH 28/31] terminal: setAttribute --- src/terminal/stream.zig | 25 +++++++++++++------------ src/termio/stream_handler.zig | 14 +++++--------- 2 files changed, 18 insertions(+), 21 deletions(-) diff --git a/src/terminal/stream.zig b/src/terminal/stream.zig index d4d61f62b..569144537 100644 --- a/src/terminal/stream.zig +++ b/src/terminal/stream.zig @@ -124,6 +124,7 @@ pub const Action = union(Key) { end_of_command: EndOfCommand, mouse_shape: MouseShape, configure_charset: ConfigureCharset, + set_attribute: sgr.Attribute, pub const Key = lib.Enum( lib_target, @@ -222,6 +223,7 @@ pub const Action = union(Key) { "end_of_command", "mouse_shape", "configure_charset", + "set_attribute", }, ); @@ -231,7 +233,7 @@ pub const Action = union(Key) { @This(), // TODO: Before shipping an ABI-compatible libghostty, verify this. // This was just arbitrarily chosen for now. - [8]u64, + [16]u64, ); pub const Tag = c_union.Tag; pub const Value = c_union.Value; @@ -1320,7 +1322,7 @@ pub fn Stream(comptime Handler: type) type { // SGR - Select Graphic Rendition 'm' => switch (input.intermediates.len) { - 0 => if (@hasDecl(T, "setAttribute")) { + 0 => { // log.info("parse SGR params={any}", .{input.params}); var p: sgr.Parser = .{ .params = input.params, @@ -1328,9 +1330,9 @@ pub fn Stream(comptime Handler: type) type { }; while (p.next()) |attr| { // log.info("SGR attribute: {}", .{attr}); - try self.handler.setAttribute(attr); + try self.handler.vt(.set_attribute, attr); } - } else log.warn("unimplemented CSI callback: {f}", .{input}), + }, 1 => switch (input.intermediates[0]) { '>' => blk: { @@ -3217,19 +3219,18 @@ test "stream: SGR with 17+ parameters for underline color" { attrs: ?sgr.Attribute = null, called: bool = false, - pub fn setAttribute(self: *@This(), attr: sgr.Attribute) !void { - self.attrs = attr; - self.called = true; - } - pub fn vt( self: *@This(), comptime action: anytype, value: anytype, ) !void { - _ = self; - _ = action; - _ = value; + switch (action) { + .set_attribute => { + self.attrs = value; + self.called = true; + }, + else => {}, + } } }; diff --git a/src/termio/stream_handler.zig b/src/termio/stream_handler.zig index 4e5795a10..3fd074cf9 100644 --- a/src/termio/stream_handler.zig +++ b/src/termio/stream_handler.zig @@ -317,6 +317,11 @@ pub const StreamHandler = struct { .end_of_command => self.endOfCommand(value.exit_code), .mouse_shape => try self.setMouseShape(value), .configure_charset => self.configureCharset(value.slot, value.charset), + .set_attribute => switch (value) { + .unknown => |unk| log.warn("unimplemented or unknown SGR attribute: {any}", .{unk}), + else => self.terminal.setAttribute(value) catch |err| + log.warn("error setting attribute {}: {}", .{ value, err }), + }, .dcs_hook => try self.dcsHook(value), .dcs_put => try self.dcsPut(value), .dcs_unhook => try self.dcsUnhook(), @@ -716,15 +721,6 @@ pub const StreamHandler = struct { } } - pub inline fn setAttribute(self: *StreamHandler, attr: terminal.Attribute) !void { - switch (attr) { - .unknown => |unk| log.warn("unimplemented or unknown SGR attribute: {any}", .{unk}), - - else => self.terminal.setAttribute(attr) catch |err| - log.warn("error setting attribute {}: {}", .{ attr, err }), - } - } - inline fn startHyperlink(self: *StreamHandler, uri: []const u8, id: ?[]const u8) !void { try self.terminal.screen.startHyperlink(uri, id); } From a85ad0e4f82bff7110b3c500926ffead4b301ed0 Mon Sep 17 00:00:00 2001 From: Mitchell Hashimoto Date: Fri, 24 Oct 2025 15:20:12 -0700 Subject: [PATCH 29/31] terminal: unused decls --- src/terminal/stream.zig | 19 ++++--------------- 1 file changed, 4 insertions(+), 15 deletions(-) diff --git a/src/terminal/stream.zig b/src/terminal/stream.zig index 569144537..a9b00bf3d 100644 --- a/src/terminal/stream.zig +++ b/src/terminal/stream.zig @@ -1841,10 +1841,7 @@ pub fn Stream(comptime Handler: type) type { try self.handler.vt(.active_status_display, display); }, - else => if (@hasDecl(T, "csiUnimplemented")) - try self.handler.csiUnimplemented(input) - else - log.warn("unimplemented CSI action: {f}", .{input}), + else => log.warn("unimplemented CSI action: {f}", .{input}), } } @@ -1959,12 +1956,7 @@ pub fn Stream(comptime Handler: type) type { }, } - // Fall through for when we don't have a handler. - if (@hasDecl(T, "oscUnimplemented")) { - try self.handler.oscUnimplemented(cmd); - } else { - log.warn("unimplemented OSC command: {s}", .{@tagName(cmd)}); - } + log.warn("unimplemented OSC command: {s}", .{@tagName(cmd)}); } inline fn configureCharset( @@ -2204,14 +2196,11 @@ pub fn Stream(comptime Handler: type) type { else => log.warn("unimplemented setMode: {f}", .{action}), }, - else => if (@hasDecl(T, "escUnimplemented")) - try self.handler.escUnimplemented(action) - else - log.warn("unimplemented ESC action: {f}", .{action}), - // Sets ST (string terminator). We don't have to do anything // because our parser always accepts ST. '\\' => {}, + + else => log.warn("unimplemented ESC action: {f}", .{action}), } } }; From e13f9b9e8c3137a675608cadd7a56a62f446be65 Mon Sep 17 00:00:00 2001 From: Mitchell Hashimoto Date: Sat, 25 Oct 2025 06:40:14 -0700 Subject: [PATCH 30/31] terminal: kitty color --- src/terminal/kitty/color.zig | 9 +++++++++ src/terminal/osc.zig | 9 +++++++++ src/terminal/stream.zig | 28 ++++++++++++++-------------- src/termio/stream_handler.zig | 3 ++- 4 files changed, 34 insertions(+), 15 deletions(-) diff --git a/src/terminal/kitty/color.zig b/src/terminal/kitty/color.zig index 099002f39..dface5723 100644 --- a/src/terminal/kitty/color.zig +++ b/src/terminal/kitty/color.zig @@ -1,4 +1,6 @@ const std = @import("std"); +const build_options = @import("terminal_options"); +const LibEnum = @import("../../lib/enum.zig").Enum; const terminal = @import("../main.zig"); const RGB = terminal.color.RGB; const Terminator = terminal.osc.Terminator; @@ -16,6 +18,13 @@ pub const OSC = struct { /// We must reply with the same string terminator (ST) as used in the /// request. terminator: Terminator = .st, + + /// We don't currently support encoding this to C in any way. + pub const C = void; + + pub fn cval(_: OSC) C { + return {}; + } }; pub const Special = enum { diff --git a/src/terminal/osc.zig b/src/terminal/osc.zig index f7324636a..effdfbd62 100644 --- a/src/terminal/osc.zig +++ b/src/terminal/osc.zig @@ -271,6 +271,8 @@ pub const Terminator = enum { /// Some applications and terminals use BELL (0x07) as the string terminator. bel, + pub const C = LibEnum(.c, &.{ "st", "bel" }); + /// Initialize the terminator based on the last byte seen. If the /// last byte is a BEL then we use BEL, otherwise we just assume ST. pub fn init(ch: ?u8) Terminator { @@ -289,6 +291,13 @@ pub const Terminator = enum { }; } + pub fn cval(self: Terminator) C { + return switch (self) { + .st => .st, + .bel => .bel, + }; + } + pub fn format( self: Terminator, comptime _: []const u8, diff --git a/src/terminal/stream.zig b/src/terminal/stream.zig index a9b00bf3d..2fb897d86 100644 --- a/src/terminal/stream.zig +++ b/src/terminal/stream.zig @@ -125,6 +125,7 @@ pub const Action = union(Key) { mouse_shape: MouseShape, configure_charset: ConfigureCharset, set_attribute: sgr.Attribute, + kitty_color_report: kitty.color.OSC, pub const Key = lib.Enum( lib_target, @@ -224,6 +225,7 @@ pub const Action = union(Key) { "mouse_shape", "configure_charset", "set_attribute", + "kitty_color_report", }, ); @@ -432,24 +434,25 @@ pub const Action = union(Key) { }; /// Returns a type that can process a stream of tty control characters. -/// This will call various callback functions on type T. Type T only has to -/// implement the callbacks it cares about; any unimplemented callbacks will -/// logged at runtime. +/// This will call the `vt` function on type T with the following signature: /// -/// To figure out what callbacks exist, search the source for "hasDecl". This -/// isn't ideal but for now that's the best approach. +/// fn(comptime action: Action.Key, value: Action.Value(action)) !void /// -/// This is implemented this way because we purposely do NOT want dynamic -/// dispatch for performance reasons. The way this is implemented forces -/// comptime resolution for all function calls. +/// The handler type T can choose to react to whatever actions it cares +/// about in its pursuit of implementing a terminal emulator or other +/// functionality. +/// +/// The "comptime" key is on purpose (vs. a standard Zig tagged union) +/// because it allows the compiler to optimize away unimplemented actions. +/// e.g. you don't need to pay a conditional branching cost on every single +/// action because the Zig compiler codegens separate code paths for every +/// single action at comptime. pub fn Stream(comptime Handler: type) type { return struct { const Self = @This(); pub const Action = streampkg.Action; - // We use T with @hasDecl so it needs to be a struct. Unwrap the - // pointer if we were given one. const T = switch (@typeInfo(Handler)) { .pointer => |p| p.child, else => Handler, @@ -1912,10 +1915,7 @@ pub fn Stream(comptime Handler: type) type { }, .kitty_color_protocol => |v| { - if (@hasDecl(T, "sendKittyColorReport")) { - try self.handler.sendKittyColorReport(v); - return; - } else log.warn("unimplemented OSC callback: {}", .{cmd}); + try self.handler.vt(.kitty_color_report, v); }, .show_desktop_notification => |v| { diff --git a/src/termio/stream_handler.zig b/src/termio/stream_handler.zig index 3fd074cf9..8d6c5c92d 100644 --- a/src/termio/stream_handler.zig +++ b/src/termio/stream_handler.zig @@ -301,6 +301,7 @@ pub const StreamHandler = struct { log.debug("setting kitty keyboard mode: not {}", .{value.flags}); self.terminal.screen.kitty_keyboard.set(.not, value.flags); }, + .kitty_color_report => try self.kittyColorReport(value), .prompt_end => try self.promptEnd(), .end_of_input => try self.endOfInput(), .end_hyperlink => try self.endHyperlink(), @@ -1382,7 +1383,7 @@ pub const StreamHandler = struct { } } - pub fn sendKittyColorReport( + fn kittyColorReport( self: *StreamHandler, request: terminal.kitty.color.OSC, ) !void { From 1d03451d4f861c87706780ad9dc3d8038a684489 Mon Sep 17 00:00:00 2001 From: Mitchell Hashimoto Date: Sat, 25 Oct 2025 07:02:54 -0700 Subject: [PATCH 31/31] terminal: OSC color operations --- src/terminal/stream.zig | 27 +++++++++++++++++++-------- src/termio/stream_handler.zig | 3 ++- 2 files changed, 21 insertions(+), 9 deletions(-) diff --git a/src/terminal/stream.zig b/src/terminal/stream.zig index 2fb897d86..95062c6cd 100644 --- a/src/terminal/stream.zig +++ b/src/terminal/stream.zig @@ -126,6 +126,7 @@ pub const Action = union(Key) { configure_charset: ConfigureCharset, set_attribute: sgr.Attribute, kitty_color_report: kitty.color.OSC, + color_operation: ColorOperation, pub const Key = lib.Enum( lib_target, @@ -226,6 +227,7 @@ pub const Action = union(Key) { "configure_charset", "set_attribute", "kitty_color_report", + "color_operation", }, ); @@ -431,6 +433,18 @@ pub const Action = union(Key) { slot: charsets.Slots, charset: charsets.Charset, }); + + pub const ColorOperation = struct { + op: osc.color.Operation, + requests: osc.color.List, + terminator: osc.Terminator, + + pub const C = void; + + pub fn cval(_: ColorOperation) ColorOperation.C { + return {}; + } + }; }; /// Returns a type that can process a stream of tty control characters. @@ -1904,14 +1918,11 @@ pub fn Stream(comptime Handler: type) type { }, .color_operation => |v| { - if (@hasDecl(T, "handleColorOperation")) { - try self.handler.handleColorOperation( - v.op, - &v.requests, - v.terminator, - ); - return; - } else log.warn("unimplemented OSC callback: {}", .{cmd}); + try self.handler.vt(.color_operation, .{ + .op = v.op, + .requests = v.requests, + .terminator = v.terminator, + }); }, .kitty_color_protocol => |v| { diff --git a/src/termio/stream_handler.zig b/src/termio/stream_handler.zig index 8d6c5c92d..7f241f42c 100644 --- a/src/termio/stream_handler.zig +++ b/src/termio/stream_handler.zig @@ -302,6 +302,7 @@ pub const StreamHandler = struct { self.terminal.screen.kitty_keyboard.set(.not, value.flags); }, .kitty_color_report => try self.kittyColorReport(value), + .color_operation => try self.colorOperation(value.op, &value.requests, value.terminator), .prompt_end => try self.promptEnd(), .end_of_input => try self.endOfInput(), .end_hyperlink => try self.endHyperlink(), @@ -1106,7 +1107,7 @@ pub const StreamHandler = struct { } } - pub fn handleColorOperation( + fn colorOperation( self: *StreamHandler, op: terminal.osc.color.Operation, requests: *const terminal.osc.color.List,