Convert terminal.Stream to use a tagged union, remove `hasDecl` (#9342)
This removes our `@hasDecl` usage from `terminal.Stream` and instead uses a tagged union approach similar to what we already do for apprt actions. The reasons to do this: 1. It is less magic. You don't get new functionality by magically implementing a function. 2. It is safer. You can't typo a function name and Zig's exhaustive enum handling will force you to handle all cases (even if most cases are no-ops). This also helps you as at the implementor know when new functionality pops up. 3. It is easier to integrate into C (for libghostty-vt). We can expose a single tagged union type with a single callback rather than whatever the previous mess was. This PR doesn't do this yet. In addition, this PR adds in some helpers necessary to make it easier to make C ABI compatible tagged unions. This lays the groundwork for our libghostty-vt work but isn't exposed directly there yet. This PR has no functional changes. Everything should behave identically as before. I'm PRing this now because its already a huge diff, and I want to get this in before I make more meaningful changes such as exposing some of this to libghostty or adding a simpler Stream handler that maps to terminal state for the Zig module and so on. ## Benchmarks There's no meaningful impact on VT processing, I'd say all changes seen below are noise: <img width="2038" height="1392" alt="CleanShot 2025-10-25 at 07 10 04@2x" src="https://github.com/user-attachments/assets/af6fa611-5b35-44d0-91ae-26955b1f980a" /> ## One more `@hasDecl` There is one more `hasDecl` remaining for `handleManually`. This is a special case that's only used by our inspector. I think there is a better way to do this but I didn't want to bloat this PR with anything more! This doesn't impact our primary consumers of stream. ## AI Disclosure I used AI considerably in handling the rote tasks in refactoring this. I did the design myself manually but then prompted AI to help complete it step by step. I did review each manually and understand it but I want to take a careful review again...pull/9346/head
commit
83104ff27a
|
|
@ -138,8 +138,15 @@ 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),
|
||||
else => {},
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
|
|
|
|||
|
|
@ -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" });
|
||||
|
|
|
|||
|
|
@ -1,9 +1,14 @@
|
|||
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 EnumTarget = enumpkg.Target;
|
||||
pub const String = types.String;
|
||||
pub const Struct = @import("struct.zig").Struct;
|
||||
pub const Target = @import("target.zig").Target;
|
||||
pub const TaggedUnion = unionpkg.TaggedUnion;
|
||||
|
||||
test {
|
||||
std.testing.refAllDecls(@This());
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
} });
|
||||
},
|
||||
};
|
||||
}
|
||||
|
|
@ -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,
|
||||
};
|
||||
|
|
@ -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,
|
||||
},
|
||||
};
|
||||
}
|
||||
};
|
||||
|
|
@ -0,0 +1,172 @@
|
|||
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")) break :value 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 {
|
||||
@setEvalBranchQuota(10000);
|
||||
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);
|
||||
}
|
||||
|
|
@ -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");
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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)]);
|
||||
};
|
||||
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
@ -49,33 +53,28 @@ 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) {
|
||||
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) {
|
||||
|
|
@ -88,19 +87,27 @@ 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
|
||||
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.
|
||||
|
|
|
|||
|
|
@ -1,88 +1,89 @@
|
|||
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 {
|
||||
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.
|
||||
|
|
@ -104,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);
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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 {
|
||||
|
|
|
|||
|
|
@ -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 {
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
|
|
|
|||
|
|
@ -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);
|
||||
|
|
|
|||
File diff suppressed because it is too large
Load Diff
|
|
@ -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.
|
||||
|
|
|
|||
|
|
@ -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,156 @@ 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),
|
||||
.print_repeat => try self.terminal.printRepeat(value),
|
||||
.bell => self.bell(),
|
||||
.backspace => self.terminal.backspace(),
|
||||
.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(),
|
||||
.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,
|
||||
),
|
||||
.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 => {
|
||||
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),
|
||||
.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),
|
||||
.tab_clear_current => self.terminal.tabClear(.current),
|
||||
.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),
|
||||
.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);
|
||||
},
|
||||
.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();
|
||||
}
|
||||
},
|
||||
.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),
|
||||
.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),
|
||||
.device_status => try self.deviceStatusReport(value.request),
|
||||
.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);
|
||||
},
|
||||
.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(),
|
||||
.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),
|
||||
.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(),
|
||||
.apc_start => self.apc.start(),
|
||||
.apc_end => try self.apcEnd(),
|
||||
.apc_put => self.apc.feed(self.alloc, value),
|
||||
|
||||
// Unimplemented
|
||||
.title_push,
|
||||
.title_pop,
|
||||
=> {},
|
||||
}
|
||||
}
|
||||
|
||||
pub inline fn dcsHook(self: *StreamHandler, dcs: terminal.DCS) !void {
|
||||
var cmd = self.dcs.hook(self.alloc, dcs) orelse return;
|
||||
defer cmd.deinit();
|
||||
|
|
@ -293,14 +445,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);
|
||||
|
|
@ -322,23 +466,11 @@ 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);
|
||||
}
|
||||
|
||||
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();
|
||||
|
|
@ -346,7 +478,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();
|
||||
|
|
@ -354,94 +486,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);
|
||||
}
|
||||
|
||||
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) {
|
||||
// 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);
|
||||
}
|
||||
|
||||
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();
|
||||
}
|
||||
|
|
@ -455,48 +505,25 @@ 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) {
|
||||
.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 => {},
|
||||
}
|
||||
}
|
||||
|
||||
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 (ansi) "" else "?",
|
||||
mode_raw,
|
||||
if (tag.ansi) "" else "?",
|
||||
tag.value,
|
||||
code,
|
||||
},
|
||||
);
|
||||
|
|
@ -504,18 +531,18 @@ 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);
|
||||
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,
|
||||
},
|
||||
);
|
||||
msg.write_small.len = @intCast(resp.len);
|
||||
self.messageWriter(msg);
|
||||
}
|
||||
|
||||
pub fn setMode(self: *StreamHandler, mode: terminal.Mode, enabled: bool) !void {
|
||||
|
|
@ -696,20 +723,7 @@ 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}),
|
||||
|
||||
else => self.terminal.setAttribute(attr) catch |err|
|
||||
log.warn("error setting attribute {}: {}", .{ attr, err }),
|
||||
}
|
||||
}
|
||||
|
||||
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);
|
||||
}
|
||||
|
||||
|
|
@ -720,10 +734,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) {
|
||||
|
|
@ -827,31 +838,13 @@ 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}),
|
||||
}
|
||||
}
|
||||
|
||||
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();
|
||||
}
|
||||
|
||||
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();
|
||||
}
|
||||
|
|
@ -865,38 +858,14 @@ 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,
|
||||
) !void {
|
||||
self.terminal.status_display = req;
|
||||
}
|
||||
|
||||
pub fn configureCharset(
|
||||
fn configureCharset(
|
||||
self: *StreamHandler,
|
||||
slot: terminal.CharsetSlot,
|
||||
set: terminal.Charset,
|
||||
) !void {
|
||||
) void {
|
||||
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 {
|
||||
|
|
@ -922,28 +891,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 {
|
||||
|
|
@ -964,7 +911,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", .{});
|
||||
|
|
@ -995,7 +942,7 @@ pub const StreamHandler = struct {
|
|||
self.surfaceMessageWriter(.{ .set_title = buf });
|
||||
}
|
||||
|
||||
pub inline fn setMouseShape(
|
||||
inline fn setMouseShape(
|
||||
self: *StreamHandler,
|
||||
shape: terminal.MouseShape,
|
||||
) !void {
|
||||
|
|
@ -1007,7 +954,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.
|
||||
|
|
@ -1037,13 +984,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);
|
||||
}
|
||||
|
|
@ -1057,11 +1004,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
|
||||
|
|
@ -1075,7 +1022,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);
|
||||
}
|
||||
|
||||
|
|
@ -1155,12 +1102,12 @@ 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;
|
||||
}
|
||||
}
|
||||
|
||||
pub fn handleColorOperation(
|
||||
fn colorOperation(
|
||||
self: *StreamHandler,
|
||||
op: terminal.osc.color.Operation,
|
||||
requests: *const terminal.osc.color.List,
|
||||
|
|
@ -1409,7 +1356,7 @@ pub const StreamHandler = struct {
|
|||
}
|
||||
}
|
||||
|
||||
pub fn showDesktopNotification(
|
||||
fn showDesktopNotification(
|
||||
self: *StreamHandler,
|
||||
title: []const u8,
|
||||
body: []const u8,
|
||||
|
|
@ -1437,7 +1384,7 @@ pub const StreamHandler = struct {
|
|||
}
|
||||
}
|
||||
|
||||
pub fn sendKittyColorReport(
|
||||
fn kittyColorReport(
|
||||
self: *StreamHandler,
|
||||
request: terminal.kitty.color.OSC,
|
||||
) !void {
|
||||
|
|
@ -1562,7 +1509,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 });
|
||||
}
|
||||
};
|
||||
|
|
|
|||
Loading…
Reference in New Issue