terminal: many more conversions

pull/9342/head
Mitchell Hashimoto 2025-10-23 20:59:57 -07:00
parent 2520e27aef
commit f68ea7c907
No known key found for this signature in database
GPG Key ID: 523D5DC389D273BC
3 changed files with 129 additions and 194 deletions

View File

@ -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 {

View File

@ -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" {

View File

@ -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}),