diff --git a/src/terminal/Terminal.zig b/src/terminal/Terminal.zig index b3e3b33a7..479dea21d 100644 --- a/src/terminal/Terminal.zig +++ b/src/terminal/Terminal.zig @@ -1801,6 +1801,23 @@ pub fn setTopAndBottomMargin(self: *Terminal, top_req: usize, bottom_req: usize) self.setCursorPos(1, 1); } +/// DECSLRM +pub fn setLeftAndRightMargin(self: *Terminal, left_req: usize, right_req: usize) void { + const tracy = trace(@src()); + defer tracy.end(); + + // We must have this mode enabled to do anything + if (!self.modes.get(.enable_left_and_right_margin)) return; + + const left = @max(1, left_req); + const right = @min(self.rows, if (right_req == 0) self.rows else right_req); + if (left >= right) return; + + self.scrolling_region.left = left - 1; + self.scrolling_region.right = right - 1; + self.setCursorPos(1, 1); +} + /// Mark the current semantic prompt information. Current escape sequences /// (OSC 133) only allow setting this for wherever the current active cursor /// is located. @@ -2637,6 +2654,126 @@ test "Terminal: setTopAndBottomMargin top equal to bottom" { try testing.expectEqualStrings("\nABC\nDEF\nGHI", str); } } + +test "Terminal: setLeftAndRightMargin simple" { + const alloc = testing.allocator; + var t = try init(alloc, 5, 5); + defer t.deinit(alloc); + + try t.printString("ABC"); + t.carriageReturn(); + try t.linefeed(); + try t.printString("DEF"); + t.carriageReturn(); + try t.linefeed(); + try t.printString("GHI"); + t.modes.set(.enable_left_and_right_margin, true); + t.setLeftAndRightMargin(0, 0); + t.eraseChars(1); + + { + var str = try t.plainString(testing.allocator); + defer testing.allocator.free(str); + try testing.expectEqualStrings(" BC\nDEF\nGHI", str); + } +} + +test "Terminal: setLeftAndRightMargin left only" { + const alloc = testing.allocator; + var t = try init(alloc, 5, 5); + defer t.deinit(alloc); + + try t.printString("ABC"); + t.carriageReturn(); + try t.linefeed(); + try t.printString("DEF"); + t.carriageReturn(); + try t.linefeed(); + try t.printString("GHI"); + t.modes.set(.enable_left_and_right_margin, true); + t.setLeftAndRightMargin(2, 0); + t.setCursorPos(1, 2); + try t.insertLines(1); + + { + var str = try t.plainString(testing.allocator); + defer testing.allocator.free(str); + try testing.expectEqualStrings("A\nDBC\nGEF\n HI", str); + } +} + +test "Terminal: setLeftAndRightMargin left and right" { + const alloc = testing.allocator; + var t = try init(alloc, 5, 5); + defer t.deinit(alloc); + + try t.printString("ABC"); + t.carriageReturn(); + try t.linefeed(); + try t.printString("DEF"); + t.carriageReturn(); + try t.linefeed(); + try t.printString("GHI"); + t.modes.set(.enable_left_and_right_margin, true); + t.setLeftAndRightMargin(1, 2); + t.setCursorPos(1, 2); + try t.insertLines(1); + + { + var str = try t.plainString(testing.allocator); + defer testing.allocator.free(str); + try testing.expectEqualStrings(" C\nABF\nDEI\nGH", str); + } +} + +test "Terminal: setLeftAndRightMargin left equal right" { + const alloc = testing.allocator; + var t = try init(alloc, 5, 5); + defer t.deinit(alloc); + + try t.printString("ABC"); + t.carriageReturn(); + try t.linefeed(); + try t.printString("DEF"); + t.carriageReturn(); + try t.linefeed(); + try t.printString("GHI"); + t.modes.set(.enable_left_and_right_margin, true); + t.setLeftAndRightMargin(2, 2); + t.setCursorPos(1, 2); + try t.insertLines(1); + + { + var str = try t.plainString(testing.allocator); + defer testing.allocator.free(str); + try testing.expectEqualStrings("\nABC\nDEF\nGHI", str); + } +} + +test "Terminal: setLeftAndRightMargin mode 69 unset" { + const alloc = testing.allocator; + var t = try init(alloc, 5, 5); + defer t.deinit(alloc); + + try t.printString("ABC"); + t.carriageReturn(); + try t.linefeed(); + try t.printString("DEF"); + t.carriageReturn(); + try t.linefeed(); + try t.printString("GHI"); + t.modes.set(.enable_left_and_right_margin, false); + t.setLeftAndRightMargin(1, 2); + t.setCursorPos(1, 2); + try t.insertLines(1); + + { + var str = try t.plainString(testing.allocator); + defer testing.allocator.free(str); + try testing.expectEqualStrings("\nABC\nDEF\nGHI", str); + } +} + test "Terminal: deleteLines" { const alloc = testing.allocator; var t = try init(alloc, 80, 80); diff --git a/src/terminal/modes.zig b/src/terminal/modes.zig index 5d2064c7d..5adebb93b 100644 --- a/src/terminal/modes.zig +++ b/src/terminal/modes.zig @@ -134,10 +134,12 @@ const ModeTag = packed struct(u16) { pub fn modeFromInt(v: u16, ansi: bool) ?Mode { inline for (entries) |entry| { - if (entry.value == v and entry.ansi == ansi) { - const tag: ModeTag = .{ .ansi = ansi, .value = entry.value }; - const int: ModeTag.Backing = @bitCast(tag); - return @enumFromInt(int); + if (comptime !entry.disabled) { + if (entry.value == v and entry.ansi == ansi) { + const tag: ModeTag = .{ .ansi = ansi, .value = entry.value }; + const int: ModeTag.Backing = @bitCast(tag); + return @enumFromInt(int); + } } } @@ -160,6 +162,7 @@ const ModeEntry = struct { value: comptime_int, default: bool = false, ansi: bool = false, + disabled: bool = false, }; /// The full list of available entries. For documentation see how @@ -195,6 +198,10 @@ const entries: []const ModeEntry = &.{ .{ .name = "bracketed_paste", .value = 2004 }, .{ .name = "synchronized_output", .value = 2026 }, .{ .name = "grapheme_cluster", .value = 2027 }, + + // Disabled for now until we ensure we get left/right margins working + // correctly in all sequences. + .{ .name = "enable_left_and_right_margin", .value = 69, .disabled = true }, }; test { diff --git a/src/terminal/stream.zig b/src/terminal/stream.zig index a2500fce0..71c74191d 100644 --- a/src/terminal/stream.zig +++ b/src/terminal/stream.zig @@ -787,8 +787,20 @@ pub fn Stream(comptime Handler: type) type { ), }, - // Save Mode 's' => switch (action.intermediates.len) { + // DECSLRM + 0 => if (@hasDecl(T, "setLeftAndRightMargin")) { + switch (action.params.len) { + 0 => try self.handler.setLeftAndRightMargin(0, 0), + 1 => try self.handler.setLeftAndRightMargin(action.params[0], 0), + 2 => try self.handler.setLeftAndRightMargin(action.params[0], action.params[1]), + else => log.warn("invalid DECSLRM command: {}", .{action}), + } + } else log.warn( + "unimplemented CSI callback: {}", + .{action}, + ), + 1 => switch (action.intermediates[0]) { '?' => if (@hasDecl(T, "saveMode")) { for (action.params) |mode_int| { diff --git a/src/termio/Exec.zig b/src/termio/Exec.zig index 96303e8bd..4c054b739 100644 --- a/src/termio/Exec.zig +++ b/src/termio/Exec.zig @@ -1397,6 +1397,10 @@ const StreamHandler = struct { self.terminal.setTopAndBottomMargin(top, bot); } + pub 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) { @@ -1460,6 +1464,13 @@ const StreamHandler = struct { // Origin resets cursor pos .origin => self.terminal.setCursorPos(1, 1), + .enable_left_and_right_margin => if (!enabled) { + // When we disable left/right margin mode we need to + // reset the left/right margins. + self.terminal.scrolling_region.left = 0; + self.terminal.scrolling_region.right = self.terminal.cols - 1; + }, + .alt_screen_save_cursor_clear_enter => { const opts: terminal.Terminal.AlternateScreenOptions = .{ .cursor_save = true,