diff --git a/src/terminal/Terminal.zig b/src/terminal/Terminal.zig index b4a2c1a8b..75ca871a3 100644 --- a/src/terminal/Terminal.zig +++ b/src/terminal/Terminal.zig @@ -717,6 +717,12 @@ pub fn print(self: *Terminal, c: u21) !void { self.insertBlanks(width); } + // Our right margin depends where our cursor is now. + const right_limit = if (self.screen.cursor.x > self.scrolling_region.right) + self.cols + else + self.scrolling_region.right + 1; + switch (width) { // Single cell is very easy: just write in the cell 1 => _ = @call(.always_inline, printCell, .{ self, c }), @@ -725,11 +731,11 @@ pub fn print(self: *Terminal, c: u21) !void { // using two cells: the first is flagged "wide" and has the // wide char. The second is guaranteed to be a spacer if // we're not at the end of the line. - 2 => if (self.cols > 1) { + 2 => if ((right_limit - self.scrolling_region.left) > 1) { // If we don't have space for the wide char, we need // to insert spacers and wrap. Then we just print the wide // char as normal. - if (self.screen.cursor.x == self.cols - 1) { + if (self.screen.cursor.x == right_limit - 1) { const spacer_head = self.printCell(' '); spacer_head.attrs.wide_spacer_head = true; try self.printWrap(); @@ -757,7 +763,7 @@ pub fn print(self: *Terminal, c: u21) !void { // If we're at the column limit, then we need to wrap the next time. // This is unlikely so we do the increment above and decrement here // if we need to rather than check once. - if (self.screen.cursor.x == self.cols) { + if (self.screen.cursor.x == right_limit) { self.screen.cursor.x -= 1; self.screen.cursor.pending_wrap = true; } @@ -835,7 +841,7 @@ fn printWrap(self: *Terminal) !void { // Move to the next line try self.index(); - self.screen.cursor.x = 0; + self.screen.cursor.x = self.scrolling_region.left; } fn clearWideSpacerHead(self: *Terminal) void { @@ -1329,12 +1335,13 @@ pub fn cursorLeft(self: *Terminal, count_req: usize) void { } // The margins we can move to. - // TODO: if cursor is left of the left margin, assume left margin to be 0. - // verified with xterm. don't forget when left margins are implemented! - const left_margin = 0; - const right_margin = self.cols - 1; const top = self.scrolling_region.top; const bottom = self.scrolling_region.bottom; + const right_margin = self.scrolling_region.right; + const left_margin = if (self.screen.cursor.x < self.scrolling_region.left) + 0 + else + self.scrolling_region.left; while (true) { // We can move at most to the left margin. @@ -1820,7 +1827,7 @@ pub fn setLeftAndRightMargin(self: *Terminal, left_req: usize, right_req: usize) 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); + const right = @min(self.cols, if (right_req == 0) self.cols else right_req); if (left >= right) return; self.scrolling_region.left = left - 1; @@ -2227,6 +2234,57 @@ test "Terminal: print invoke charset single" { } } +test "Terminal: print right margin wrap" { + var t = try init(testing.allocator, 10, 5); + defer t.deinit(testing.allocator); + + try t.printString("123456789"); + t.modes.set(.enable_left_and_right_margin, true); + t.setLeftAndRightMargin(3, 5); + t.setCursorPos(1, 5); + try t.printString("XY"); + + { + var str = try t.plainString(testing.allocator); + defer testing.allocator.free(str); + try testing.expectEqualStrings("1234X6789\n Y", str); + } +} + +test "Terminal: print right margin outside" { + var t = try init(testing.allocator, 10, 5); + defer t.deinit(testing.allocator); + + try t.printString("123456789"); + t.modes.set(.enable_left_and_right_margin, true); + t.setLeftAndRightMargin(3, 5); + t.setCursorPos(1, 6); + try t.printString("XY"); + + { + var str = try t.plainString(testing.allocator); + defer testing.allocator.free(str); + try testing.expectEqualStrings("12345XY89", str); + } +} + +test "Terminal: print right margin outside wrap" { + var t = try init(testing.allocator, 10, 5); + defer t.deinit(testing.allocator); + + try t.printString("123456789"); + t.modes.set(.enable_left_and_right_margin, true); + t.setLeftAndRightMargin(3, 5); + t.setCursorPos(1, 10); + try t.printString("XY"); + + { + var str = try t.plainString(testing.allocator); + defer testing.allocator.free(str); + try testing.expectEqualStrings("123456789X\n Y", str); + } +} + test "Terminal: linefeed and carriage return" { var t = try init(testing.allocator, 80, 80); defer t.deinit(testing.allocator); @@ -2702,6 +2760,8 @@ test "Terminal: setLeftAndRightMargin left only" { try t.printString("GHI"); t.modes.set(.enable_left_and_right_margin, true); t.setLeftAndRightMargin(2, 0); + try testing.expectEqual(@as(usize, 1), t.scrolling_region.left); + try testing.expectEqual(@as(usize, t.cols - 1), t.scrolling_region.right); t.setCursorPos(1, 2); try t.insertLines(1); diff --git a/src/terminal/modes.zig b/src/terminal/modes.zig index bd4cbe714..25a020e9d 100644 --- a/src/terminal/modes.zig +++ b/src/terminal/modes.zig @@ -184,6 +184,7 @@ const entries: []const ModeEntry = &.{ .{ .name = "enable_mode_3", .value = 40 }, .{ .name = "reverse_wrap", .value = 45 }, .{ .name = "keypad_keys", .value = 66 }, + .{ .name = "enable_left_and_right_margin", .value = 69 }, .{ .name = "mouse_event_normal", .value = 1000 }, .{ .name = "mouse_event_button", .value = 1002 }, .{ .name = "mouse_event_any", .value = 1003 }, @@ -200,10 +201,6 @@ 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/terminfo/ghostty.zig b/src/terminfo/ghostty.zig index ff187ad3c..119bae399 100644 --- a/src/terminfo/ghostty.zig +++ b/src/terminfo/ghostty.zig @@ -141,6 +141,12 @@ pub const ghostty: Source = .{ .{ .name = "XR", .value = .{ .string = "\\E[>0q" } }, .{ .name = "xr", .value = .{ .string = "\\EP>\\|[ -~]+a\\E\\\\" } }, + // DECSLRM (Left/Right Margins) + .{ .name = "Enmg", .value = .{ .string = "\\E[?69h" } }, + .{ .name = "Dsmg", .value = .{ .string = "\\E[?69l" } }, + .{ .name = "Clmg", .value = .{ .string = "\\E[s" } }, + .{ .name = "Cmg", .value = .{ .string = "\\E[%i%p1%d;%p2%ds" } }, + // These are all capabilities that should be pretty straightforward // and map to input sequences. .{ .name = "bel", .value = .{ .string = "^G" } },