From 7cc0bbe2114a76c297c67a3b27051bedd6b99a5a Mon Sep 17 00:00:00 2001 From: Mitchell Hashimoto Date: Mon, 9 Oct 2023 20:08:16 -0700 Subject: [PATCH 1/4] terminal: set top and bottom margin tests --- src/terminal/Terminal.zig | 169 +++++++++++++++++++++----------- src/termio/Exec.zig | 2 +- website/app/vt/decstbm/page.mdx | 111 +++++++++++++++++++++ 3 files changed, 223 insertions(+), 59 deletions(-) create mode 100644 website/app/vt/decstbm/page.mdx diff --git a/src/terminal/Terminal.zig b/src/terminal/Terminal.zig index 4945270f6..2b5d358c3 100644 --- a/src/terminal/Terminal.zig +++ b/src/terminal/Terminal.zig @@ -866,7 +866,7 @@ pub fn decaln(self: *Terminal) !void { defer tracy.end(); // Reset margins, also sets cursor to top-left - self.setScrollingRegion(0, 0); + self.setTopAndBottomMargin(0, 0); // Fill with Es, does not move cursor. We reset fg/bg so we can just // optimize here by doing row copies. @@ -1767,19 +1767,16 @@ pub fn scrollViewport(self: *Terminal, behavior: ScrollViewport) !void { /// and bottom-most line of the screen. /// /// Top and bottom are 1-indexed. -pub fn setScrollingRegion(self: *Terminal, top: usize, bottom: usize) void { +pub fn setTopAndBottomMargin(self: *Terminal, top_req: usize, bottom_req: usize) void { const tracy = trace(@src()); defer tracy.end(); - var t = if (top == 0) 1 else top; - var b = @min(bottom, self.rows); - if (t >= b) { - t = 1; - b = self.rows; - } + const top = @max(1, top_req); + const bottom = @min(self.rows, if (bottom_req == 0) self.rows else bottom_req); + if (top >= bottom) return; - self.scrolling_region.top = t - 1; - self.scrolling_region.bottom = b - 1; + self.scrolling_region.top = top - 1; + self.scrolling_region.bottom = bottom - 1; self.setCursorPos(1, 1); } @@ -2513,7 +2510,7 @@ test "Terminal: setCursorPos (original test)" { try testing.expectEqual(@as(usize, 79), t.screen.cursor.y); // Set the scroll region - t.setScrollingRegion(10, t.rows); + t.setTopAndBottomMargin(10, t.rows); t.setCursorPos(0, 0); try testing.expectEqual(@as(usize, 0), t.screen.cursor.x); try testing.expectEqual(@as(usize, 9), t.screen.cursor.y); @@ -2526,43 +2523,99 @@ test "Terminal: setCursorPos (original test)" { try testing.expectEqual(@as(usize, 0), t.screen.cursor.x); try testing.expectEqual(@as(usize, 79), t.screen.cursor.y); - t.setScrollingRegion(10, 11); + t.setTopAndBottomMargin(10, 11); t.setCursorPos(2, 0); try testing.expectEqual(@as(usize, 0), t.screen.cursor.x); try testing.expectEqual(@as(usize, 10), t.screen.cursor.y); } -test "Terminal: setScrollingRegion" { - var t = try init(testing.allocator, 80, 80); - defer t.deinit(testing.allocator); +test "Terminal: setTopAndBottomMargin simple" { + const alloc = testing.allocator; + var t = try init(alloc, 5, 5); + defer t.deinit(alloc); - // Initial value - try testing.expectEqual(@as(usize, 0), t.scrolling_region.top); - try testing.expectEqual(@as(usize, t.rows - 1), t.scrolling_region.bottom); + try t.printString("ABC"); + t.carriageReturn(); + try t.linefeed(); + try t.printString("DEF"); + t.carriageReturn(); + try t.linefeed(); + try t.printString("GHI"); + t.setTopAndBottomMargin(0, 0); + try t.scrollDown(1); - // Move our cusor so we can verify we move it back - t.setCursorPos(5, 5); - t.setScrollingRegion(3, 7); - - // Cursor should move back to top-left - try testing.expectEqual(@as(usize, 0), t.screen.cursor.x); - try testing.expectEqual(@as(usize, 0), t.screen.cursor.y); - - // Scroll region is set - try testing.expectEqual(@as(usize, 2), t.scrolling_region.top); - try testing.expectEqual(@as(usize, 6), t.scrolling_region.bottom); - - // Scroll region invalid - t.setScrollingRegion(7, 3); - try testing.expectEqual(@as(usize, 0), t.scrolling_region.top); - try testing.expectEqual(@as(usize, t.rows - 1), t.scrolling_region.bottom); - - // Scroll region with zero top and bottom - t.setScrollingRegion(0, 0); - try testing.expectEqual(@as(usize, 0), t.scrolling_region.top); - try testing.expectEqual(@as(usize, t.rows - 1), t.scrolling_region.bottom); + { + var str = try t.plainString(testing.allocator); + defer testing.allocator.free(str); + try testing.expectEqualStrings("\nABC\nDEF\nGHI", str); + } } +test "Terminal: setTopAndBottomMargin top 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.setTopAndBottomMargin(2, 0); + try t.scrollDown(1); + + { + var str = try t.plainString(testing.allocator); + defer testing.allocator.free(str); + try testing.expectEqualStrings("ABC\n\nDEF\nGHI", str); + } +} + +test "Terminal: setTopAndBottomMargin top and bottom" { + 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.setTopAndBottomMargin(1, 2); + try t.scrollDown(1); + + { + var str = try t.plainString(testing.allocator); + defer testing.allocator.free(str); + try testing.expectEqualStrings("\nABC\nGHI", str); + } +} + +test "Terminal: setTopAndBottomMargin top equal to bottom" { + 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.setTopAndBottomMargin(2, 2); + try t.scrollDown(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); @@ -2615,7 +2668,7 @@ test "Terminal: deleteLines with scroll region" { try t.linefeed(); try t.print('D'); - t.setScrollingRegion(1, 3); + t.setTopAndBottomMargin(1, 3); t.setCursorPos(1, 1); try t.deleteLines(1); @@ -2651,7 +2704,7 @@ test "Terminal: deleteLines with scroll region, large count" { try t.linefeed(); try t.print('D'); - t.setScrollingRegion(1, 3); + t.setTopAndBottomMargin(1, 3); t.setCursorPos(1, 1); try t.deleteLines(5); @@ -2687,7 +2740,7 @@ test "Terminal: deleteLines with scroll region, cursor outside of region" { try t.linefeed(); try t.print('D'); - t.setScrollingRegion(1, 3); + t.setTopAndBottomMargin(1, 3); t.setCursorPos(4, 1); try t.deleteLines(1); @@ -2820,7 +2873,7 @@ test "Terminal: insertLines outside of scroll region" { t.carriageReturn(); try t.linefeed(); try t.printString("GHI"); - t.setScrollingRegion(3, 4); + t.setTopAndBottomMargin(3, 4); t.setCursorPos(2, 2); try t.insertLines(1); @@ -2846,7 +2899,7 @@ test "Terminal: insertLines top/bottom scroll region" { t.carriageReturn(); try t.linefeed(); try t.printString("123"); - t.setScrollingRegion(1, 3); + t.setTopAndBottomMargin(1, 3); t.setCursorPos(2, 2); try t.insertLines(1); @@ -2944,7 +2997,7 @@ test "Terminal: insertLines with scroll region" { try t.linefeed(); try t.print('E'); - t.setScrollingRegion(1, 2); + t.setTopAndBottomMargin(1, 2); t.setCursorPos(1, 1); try t.insertLines(1); @@ -3089,7 +3142,7 @@ test "Terminal: reverseIndex top of scrolling region" { try t.linefeed(); // Set our scroll region - t.setScrollingRegion(2, 5); + t.setTopAndBottomMargin(2, 5); t.setCursorPos(2, 1); try t.reverseIndex(); try t.print('X'); @@ -3141,7 +3194,7 @@ test "Terminal: index outside of scrolling region" { defer t.deinit(alloc); try testing.expectEqual(@as(usize, 0), t.screen.cursor.y); - t.setScrollingRegion(2, 5); + t.setTopAndBottomMargin(2, 5); try t.index(); try testing.expectEqual(@as(usize, 1), t.screen.cursor.y); } @@ -3151,7 +3204,7 @@ test "Terminal: index from the bottom outside of scroll region" { var t = try init(alloc, 2, 5); defer t.deinit(alloc); - t.setScrollingRegion(1, 2); + t.setTopAndBottomMargin(1, 2); t.setCursorPos(5, 1); try t.print('A'); try t.index(); @@ -3228,7 +3281,7 @@ test "Terminal: index inside scroll region" { var t = try init(alloc, 5, 5); defer t.deinit(alloc); - t.setScrollingRegion(1, 3); + t.setTopAndBottomMargin(1, 3); try t.print('A'); try t.index(); try t.print('X'); @@ -3245,7 +3298,7 @@ test "Terminal: index bottom of scroll region" { var t = try init(alloc, 5, 5); defer t.deinit(alloc); - t.setScrollingRegion(1, 3); + t.setTopAndBottomMargin(1, 3); t.setCursorPos(4, 1); try t.print('B'); t.setCursorPos(3, 1); @@ -3265,7 +3318,7 @@ test "Terminal: index bottom of primary screen with scroll region" { var t = try init(alloc, 5, 5); defer t.deinit(alloc); - t.setScrollingRegion(1, 3); + t.setTopAndBottomMargin(1, 3); t.setCursorPos(3, 1); try t.print('A'); t.setCursorPos(5, 1); @@ -3286,7 +3339,7 @@ test "Terminal: index outside left/right margin" { var t = try init(alloc, 10, 5); defer t.deinit(alloc); - t.setScrollingRegion(1, 3); + t.setTopAndBottomMargin(1, 3); t.scrolling_region.left = 3; t.scrolling_region.right = 5; t.setCursorPos(3, 3); @@ -3307,7 +3360,7 @@ test "Terminal: index inside left/right margin" { var t = try init(alloc, 10, 5); defer t.deinit(alloc); - t.setScrollingRegion(1, 3); + t.setTopAndBottomMargin(1, 3); t.scrolling_region.left = 3; t.scrolling_region.right = 5; t.setCursorPos(3, 3); @@ -5181,7 +5234,7 @@ test "Terminal: cursorDown above bottom scroll margin" { var t = try init(alloc, 5, 5); defer t.deinit(alloc); - t.setScrollingRegion(1, 3); + t.setTopAndBottomMargin(1, 3); try t.print('A'); t.cursorDown(10); try t.print('X'); @@ -5198,7 +5251,7 @@ test "Terminal: cursorDown below bottom scroll margin" { var t = try init(alloc, 5, 5); defer t.deinit(alloc); - t.setScrollingRegion(1, 3); + t.setTopAndBottomMargin(1, 3); try t.print('A'); t.setCursorPos(4, 1); t.cursorDown(10); @@ -5251,7 +5304,7 @@ test "Terminal: cursorUp below top scroll margin" { var t = try init(alloc, 5, 5); defer t.deinit(alloc); - t.setScrollingRegion(2, 4); + t.setTopAndBottomMargin(2, 4); t.setCursorPos(3, 1); try t.print('A'); t.cursorUp(5); @@ -5269,7 +5322,7 @@ test "Terminal: cursorUp above top scroll margin" { var t = try init(alloc, 5, 5); defer t.deinit(alloc); - t.setScrollingRegion(3, 5); + t.setTopAndBottomMargin(3, 5); t.setCursorPos(3, 1); try t.print('A'); t.setCursorPos(2, 1); @@ -5403,7 +5456,7 @@ test "Terminal: scrollDown outside of scroll region" { t.carriageReturn(); try t.linefeed(); try t.printString("GHI"); - t.setScrollingRegion(3, 4); + t.setTopAndBottomMargin(3, 4); t.setCursorPos(2, 2); const cursor = t.screen.cursor; try t.scrollDown(1); diff --git a/src/termio/Exec.zig b/src/termio/Exec.zig index a929e3c95..96303e8bd 100644 --- a/src/termio/Exec.zig +++ b/src/termio/Exec.zig @@ -1394,7 +1394,7 @@ const StreamHandler = struct { } pub fn setTopAndBottomMargin(self: *StreamHandler, top: u16, bot: u16) !void { - self.terminal.setScrollingRegion(top, bot); + self.terminal.setTopAndBottomMargin(top, bot); } pub fn setModifyKeyFormat(self: *StreamHandler, format: terminal.ModifyKeyFormat) !void { diff --git a/website/app/vt/decstbm/page.mdx b/website/app/vt/decstbm/page.mdx new file mode 100644 index 000000000..071667a83 --- /dev/null +++ b/website/app/vt/decstbm/page.mdx @@ -0,0 +1,111 @@ +import VTSequence from "@/components/VTSequence"; + +# Set Top and Bottom Margins (DECSTBM) + + + +Sets the top and bottom margins, otherwise known as the scroll region. + +Parameters `t` and `b` are integer values. If either value is zero the +value will be reset to default values. The default value for `t` is `1` +and the default value of `b` is the number of rows in the screen. + +Values `t` and `b` can be omitted. If either value is omitted, their +default values will be used. Note that it is impossible to omit `t` +and not omit `b`. The only valid sequences are `CSI t ; b r`, +`CSI t r` and `CSI r`. + +If top is larger or equal to bottom, this sequence does nothing. A +scroll region must be at least two rows (`b` must be greater than `t`). +The rest of this sequence description assumes valid values for `t` and `b`. + +This sequence unsets the pending wrap state and moves the cursor to +the top-left of the screen. If [origin mode](#TODO) is set, the cursor is +moved to the top-left of the scroll region. + +To reset the scroll region, call this sequence with both values set to +"0". This will force the default values for both `t` and `b` which is +the full screen. + +The top and bottom margin constitute what is known as the _scroll region_. +The scroll region impacts the operation of many sequences such as +[insert line](/vt/il), [cursor down](/vt/cud), etc. Scroll regions are +an effective and efficient way to constraint terminal modifications to a +rectangular region of the screen. + +## Validation + +### DECSTBM V-1: Full Screen + +```bash +printf "\033[1;1H" # move to top-left +printf "\033[0J" # clear screen +printf "ABC\n" +printf "DEF\n" +printf "GHI\n" +printf "\033[r" # scroll region top/bottom +printf "\033[T" +``` + +``` +|c_______| +|ABC_____| +|DEF_____| +|GHI_____| +``` + +### DECSTBM V-2: Top Only + +```bash +printf "\033[1;1H" # move to top-left +printf "\033[0J" # clear screen +printf "ABC\n" +printf "DEF\n" +printf "GHI\n" +printf "\033[2r" # scroll region top/bottom +printf "\033[T" +``` + +``` +|ABC_____| +|________| +|DEF_____| +|GHI_____| +``` + +### DECSTBM V-3: Top and Bottom + +```bash +printf "\033[1;1H" # move to top-left +printf "\033[0J" # clear screen +printf "ABC\n" +printf "DEF\n" +printf "GHI\n" +printf "\033[1;2r" # scroll region top/bottom +printf "\033[T" +``` + +``` +|________| +|ABC_____| +|GHI_____| +``` + +### DECSTBM V-4: Top Equal to Bottom + +```bash +printf "\033[1;1H" # move to top-left +printf "\033[0J" # clear screen +printf "ABC\n" +printf "DEF\n" +printf "GHI\n" +printf "\033[2;2r" # scroll region top/bottom +printf "\033[T" +``` + +``` +|________| +|ABC_____| +|DEF_____| +|GHI_____| +``` From be99d8ffe16e2dca4561cc7fa6a7087cff72df82 Mon Sep 17 00:00:00 2001 From: Mitchell Hashimoto Date: Mon, 9 Oct 2023 20:33:52 -0700 Subject: [PATCH 2/4] terminal: decaln --- src/terminal/Terminal.zig | 81 ++++++++++++++++++++++++++++++---- website/app/vt/decaln/page.mdx | 45 +++++++++++++++++++ 2 files changed, 118 insertions(+), 8 deletions(-) create mode 100644 website/app/vt/decaln/page.mdx diff --git a/src/terminal/Terminal.zig b/src/terminal/Terminal.zig index 2b5d358c3..b3e3b33a7 100644 --- a/src/terminal/Terminal.zig +++ b/src/terminal/Terminal.zig @@ -866,16 +866,37 @@ pub fn decaln(self: *Terminal) !void { defer tracy.end(); // Reset margins, also sets cursor to top-left - self.setTopAndBottomMargin(0, 0); + self.scrolling_region = .{ + .top = 0, + .bottom = self.rows - 1, + .left = 0, + .right = self.cols - 1, + }; - // Fill with Es, does not move cursor. We reset fg/bg so we can just - // optimize here by doing row copies. - const filled = self.screen.getRow(.{ .active = 0 }); - filled.fill(.{ .char = 'E' }); + // Origin mode is disabled + self.modes.set(.origin, false); - var row: usize = 1; - while (row < self.rows) : (row += 1) { - try self.screen.getRow(.{ .active = row }).copyRow(filled); + // Move our cursor to the top-left + self.setCursorPos(1, 1); + + // Clear our stylistic attributes + self.screen.cursor.pen = .{ + .bg = self.screen.cursor.pen.bg, + .fg = self.screen.cursor.pen.fg, + .attrs = .{ + .has_bg = self.screen.cursor.pen.attrs.has_bg, + .has_fg = self.screen.cursor.pen.attrs.has_fg, + .protected = self.screen.cursor.pen.attrs.protected, + }, + }; + + // Our pen has the letter E + var pen: Screen.Cell = .{ .char = 'E' }; + + // Fill with Es, does not move cursor. + for (0..self.rows) |y| { + const filled = self.screen.getRow(.{ .active = y }); + filled.fill(pen); } } @@ -3397,6 +3418,50 @@ test "Terminal: DECALN" { } } +test "Terminal: decaln reset margins" { + const alloc = testing.allocator; + var t = try init(alloc, 3, 3); + defer t.deinit(alloc); + + // Initial value + t.modes.set(.origin, true); + t.setTopAndBottomMargin(2, 3); + try t.decaln(); + try t.scrollDown(1); + + { + var str = try t.plainString(testing.allocator); + defer testing.allocator.free(str); + try testing.expectEqualStrings("\nEEE\nEEE", str); + } +} + +test "Terminal: decaln preserves color" { + const alloc = testing.allocator; + var t = try init(alloc, 3, 3); + defer t.deinit(alloc); + + const pen: Screen.Cell = .{ + .bg = .{ .r = 0xFF, .g = 0x00, .b = 0x00 }, + .attrs = .{ .has_bg = true }, + }; + + // Initial value + t.screen.cursor.pen = pen; + t.modes.set(.origin, true); + t.setTopAndBottomMargin(2, 3); + try t.decaln(); + try t.scrollDown(1); + + { + var str = try t.plainString(testing.allocator); + defer testing.allocator.free(str); + try testing.expectEqualStrings("\nEEE\nEEE", str); + const cell = t.screen.getCell(.active, 0, 0); + try testing.expectEqual(pen, cell); + } +} + test "Terminal: insertBlanks" { // NOTE: this is not verified with conformance tests, so these // tests might actually be verifying wrong behavior. diff --git a/website/app/vt/decaln/page.mdx b/website/app/vt/decaln/page.mdx new file mode 100644 index 000000000..5b036c7e2 --- /dev/null +++ b/website/app/vt/decaln/page.mdx @@ -0,0 +1,45 @@ +import VTSequence from "@/components/VTSequence"; + +# Screen Alignment Test (DECALN) + + + +Reset margins, move cursor to the top left, and fill the screen with `E`. + +Reset the top, bottom, left, and right margins and unset [origin mode](#TODO). +The cursor is moved to the top-left corner of the screen. + +All stylistic SGR attributes are unset, such as bold, blink, etc. +SGR foreground and background colors are preserved. +The [protected attribute](#TODO) is not unset. + +The entire screen is filled with the character `E`. The letter `E` ignores +the current SGR settings and is written with no styling. + +## Validation + +### DECALN V-1: Simple Usage + +```bash +printf "\033#8" +``` + +``` +|EEEEEEEE| +|EEEEEEEE| +|EEEEEEEE| +``` + +### DECALN V-2: Reset Margins + +```bash +printf "\033[2;3r" # scroll region top/bottom +printf "\033#8" +printf "\033[T" +``` + +``` +|c_______| +|EEEEEEEE| +|EEEEEEEE| +``` From 2354454907a718f90a397a7f3f17165d79cf45b7 Mon Sep 17 00:00:00 2001 From: Mitchell Hashimoto Date: Mon, 9 Oct 2023 21:15:03 -0700 Subject: [PATCH 3/4] terminal: set left and right margins, left and right margin mode 69 --- src/terminal/Terminal.zig | 137 ++++++++++++++++++++++++++++++++++++++ src/terminal/modes.zig | 15 +++-- src/terminal/stream.zig | 14 +++- src/termio/Exec.zig | 11 +++ 4 files changed, 172 insertions(+), 5 deletions(-) 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, From d05e488cc428bba720deb677fde2cfa7e771042e Mon Sep 17 00:00:00 2001 From: Mitchell Hashimoto Date: Mon, 9 Oct 2023 21:20:30 -0700 Subject: [PATCH 4/4] website: left/right margins --- website/app/vt/decslrm/page.mdx | 120 ++++++++++++++++++++++++++++++++ 1 file changed, 120 insertions(+) create mode 100644 website/app/vt/decslrm/page.mdx diff --git a/website/app/vt/decslrm/page.mdx b/website/app/vt/decslrm/page.mdx new file mode 100644 index 000000000..2729d08c8 --- /dev/null +++ b/website/app/vt/decslrm/page.mdx @@ -0,0 +1,120 @@ +import VTSequence from "@/components/VTSequence"; + +# Set Left and Right Margins (DECSLRM) + + + +Sets the left and right margins, otherwise known as the scroll region. +To learn more about scroll regions in general, see +[Set Top and Bottom Margins](/vt/decstbm). + +Parameters `l` and `r` are integer values. If either value is zero the +value will be reset to default values. The default value for `l` is `1` +and the default value of `r` is the number of columns in the screen. + +Values `l` and `r` can be omitted. If either value is omitted, their +default values will be used. Note that it is impossible to omit `l` +and not omit `r`. + +This sequence requires [enable left and right margin (mode 69)](#TODO) +to be set. If mode 69 is not set, this sequence does nothing and left +and right margins will not be set. + +This sequence conflicts with [save cursor (`CSI s`)](#TODO). If +mode 69 is disabled, save cursor will be invoked. If mode 69 is enabled, +the `CSI s` save cursor sequence will be disabled, but save cursor is always +also available as `ESC 7`. + +If left is larger or equal to right, this sequence does nothing. A +scroll region must be at least two columns (`r` must be greater than `l`). +The rest of this sequence description assumes valid values for `l` and `r`. + +This sequence unsets the pending wrap state and moves the cursor to +the top-left of the screen. If [origin mode](#TODO) is set, the cursor is +moved to the top-left of the scroll region. + +To reset the left and right margins, call this sequence with both values set to +"0". This will force the default values for both `l` and `r` which is +the full screen. Unsetting mode 69 will also reset the left and right margins. + +## Validation + +### DECSLRM V-1: Full Screen + +```bash +printf "\033[1;1H" # move to top-left +printf "\033[0J" # clear screen +printf "ABC\n" +printf "DEF\n" +printf "GHI\n" +printf "\033[?69h" # enable left/right margins +printf "\033[s" # scroll region left/right +printf "\033[X" +``` + +``` +|cBC_____| +|DEF_____| +|GHI_____| +``` + +### DECSLRM V-2: Left Only + +```bash +printf "\033[1;1H" # move to top-left +printf "\033[0J" # clear screen +printf "ABC\n" +printf "DEF\n" +printf "GHI\n" +printf "\033[?69h" # enable left/right margins +printf "\033[2s" # scroll region left/right +printf "\033[2G" # move cursor to column 2 +printf "\033[L" +``` + +``` +|Ac______| +|DBC_____| +|GEF_____| +| HI_____| +``` + +### DECSLRM V-3: Left And Right + +```bash +printf "\033[1;1H" # move to top-left +printf "\033[0J" # clear screen +printf "ABC\n" +printf "DEF\n" +printf "GHI\n" +printf "\033[?69h" # enable left/right margins +printf "\033[1;2s" # scroll region left/right +printf "\033[2G" # move cursor to column 2 +printf "\033[L" +``` + +``` +|_cC_____| +|ABF_____| +|DEI_____| +|GH______| +``` + +### DECSLRM V-4: Left Equal to Right + +```bash +printf "\033[1;1H" # move to top-left +printf "\033[0J" # clear screen +printf "ABC\n" +printf "DEF\n" +printf "GHI\n" +printf "\033[?69h" # enable left/right margins +printf "\033[2;2s" # scroll region left/right +printf "\033[X" +``` + +``` +|cBC_____| +|DEF_____| +|GHI_____| +```