From 48368471a76a3298ca8b569c2376010f68e7b1dd Mon Sep 17 00:00:00 2001 From: Ayman Bagabas Date: Wed, 25 Dec 2024 22:45:18 +0300 Subject: [PATCH] fix: correct handling of CTC and DECST8C Cursor Tab Control (CTC) `CSI W` has a default value of 0, which is the same as setting a tab stop at the current cursor position. Set Tab at every 8th Column (DECST8C) `CSI ? 5 W` is a DEC private sequence that resets the tab stops to every 8th column. Reference: https://vt100.net/docs/vt510-rm/DECST8C.html Reference: https://wezfurlong.org/ecma48/07-control.html?highlight=ctc#7210-ctc---cursor-tabulation-control Reference: https://gitlab.gnome.org/GNOME/vte/-/blob/master/src/parser-seq.py#L596-L601 Fixes: https://github.com/ghostty-org/ghostty/issues/3120 --- src/terminal/stream.zig | 102 ++++++++++++++++++++++++++++++++-------- 1 file changed, 82 insertions(+), 20 deletions(-) diff --git a/src/terminal/stream.zig b/src/terminal/stream.zig index b8d60a13f..71d2bf9c2 100644 --- a/src/terminal/stream.zig +++ b/src/terminal/stream.zig @@ -601,30 +601,37 @@ pub fn Stream(comptime Handler: type) type { // Cursor Tabulation Control 'W' => { switch (input.params.len) { - 0 => if (input.intermediates.len == 1 and input.intermediates[0] == '?') { - if (@hasDecl(T, "tabReset")) - try self.handler.tabReset() - else - log.warn("unimplemented tab reset callback: {}", .{input}); - }, + 0 => if (@hasDecl(T, "tabSet")) + try self.handler.tabSet() + else + log.warn("unimplemented tab set callback: {}", .{input}), - 1 => switch (input.params[0]) { - 0 => if (@hasDecl(T, "tabSet")) - try self.handler.tabSet() - else - log.warn("unimplemented tab set callback: {}", .{input}), + 1 => if (input.intermediates.len == 1 and input.intermediates[0] == '?') { + if (input.params[0] == 5) { + if (@hasDecl(T, "tabReset")) + try self.handler.tabReset() + else + log.warn("unimplemented tab reset callback: {}", .{input}); + } else log.warn("invalid cursor tabulation control: {}", .{input}); + } else { + switch (input.params[0]) { + 0 => if (@hasDecl(T, "tabSet")) + try self.handler.tabSet() + else + log.warn("unimplemented tab set callback: {}", .{input}), - 2 => if (@hasDecl(T, "tabClear")) - try self.handler.tabClear(.current) - else - log.warn("unimplemented tab clear callback: {}", .{input}), + 2 => if (@hasDecl(T, "tabClear")) + try self.handler.tabClear(.current) + else + log.warn("unimplemented tab clear callback: {}", .{input}), - 5 => if (@hasDecl(T, "tabClear")) - try self.handler.tabClear(.all) - else - log.warn("unimplemented tab clear callback: {}", .{input}), + 5 => if (@hasDecl(T, "tabClear")) + try self.handler.tabClear(.all) + else + log.warn("unimplemented tab clear callback: {}", .{input}), - else => {}, + else => {}, + } }, else => {}, @@ -2327,3 +2334,58 @@ test "stream: CSI t pop title with index" { .index = 5, }, s.handler.op.?); } + +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; + } + }; + + var s: Stream(H) = .{ .handler = .{} }; + + try s.nextSlice("\x1b[2W"); + try testing.expectEqual(csi.TabClear.current, s.handler.op.?); + + try s.nextSlice("\x1b[5W"); + try testing.expectEqual(csi.TabClear.all, s.handler.op.?); +} + +test "stream CSI W tab set" { + const H = struct { + called: bool = false, + + pub fn tabSet(self: *@This()) !void { + self.called = true; + } + }; + + var s: Stream(H) = .{ .handler = .{} }; + + try s.nextSlice("\x1b[W"); + try testing.expect(s.handler.called); + + s.handler.called = false; + try s.nextSlice("\x1b[0W"); + try testing.expect(s.handler.called); +} + +test "stream CSI ? W reset tab stops" { + const H = struct { + reset: bool = false, + + pub fn tabReset(self: *@This()) !void { + self.reset = true; + } + }; + + var s: Stream(H) = .{ .handler = .{} }; + + try s.nextSlice("\x1b[?2W"); + try testing.expect(!s.handler.reset); + + try s.nextSlice("\x1b[?5W"); + try testing.expect(s.handler.reset); +}