diff --git a/src/terminal/Screen.zig b/src/terminal/Screen.zig index 93194393b..fa299efd0 100644 --- a/src/terminal/Screen.zig +++ b/src/terminal/Screen.zig @@ -2887,10 +2887,6 @@ pub fn dumpString(self: *Screen, writer: anytype, opts: Dump) !void { // Handle blank rows if (row.isEmpty()) { - // Blank rows should never have wrap set. A blank row doesn't - // include explicit spaces so there should never be a scenario - // it's wrapped. - assert(!row.header().flags.wrap); blank_rows += 1; continue; } diff --git a/src/terminal/Terminal.zig b/src/terminal/Terminal.zig index ed48b3752..5d66d72d5 100644 --- a/src/terminal/Terminal.zig +++ b/src/terminal/Terminal.zig @@ -1707,8 +1707,8 @@ pub fn deleteLines(self: *Terminal, count: usize) !void { // The amount of lines we need to scroll up. const scroll_amount = rem - count; - const scroll_top = self.scrolling_region.bottom - scroll_amount; - for (self.screen.cursor.y..scroll_top + 1) |y| { + const scroll_end_y = self.screen.cursor.y + scroll_amount; + for (self.screen.cursor.y..scroll_end_y) |y| { const src = self.screen.getRow(.{ .active = y + count }); const dst = self.screen.getRow(.{ .active = y }); for (self.scrolling_region.left..self.scrolling_region.right + 1) |x| { @@ -1717,7 +1717,7 @@ pub fn deleteLines(self: *Terminal, count: usize) !void { } // Insert blank lines - for (scroll_top + 1..self.scrolling_region.bottom + 1) |y| { + for (scroll_end_y..self.scrolling_region.bottom + 1) |y| { const row = self.screen.getRow(.{ .active = y }); row.fillSlice(.{ .bg = self.screen.cursor.pen.bg, @@ -1748,13 +1748,18 @@ pub fn scrollDown(self: *Terminal, count: usize) !void { /// The new lines are created according to the current SGR state. /// /// Does not change the (absolute) cursor position. -// TODO: test pub fn scrollUp(self: *Terminal, count: usize) !void { - self.screen.scrollRegionUp( - .{ .active = self.scrolling_region.top }, - .{ .active = self.scrolling_region.bottom }, - count, - ); + const tracy = trace(@src()); + defer tracy.end(); + + // Preserve the cursor + const cursor = self.screen.cursor; + defer self.screen.cursor = cursor; + + // Move to the top of the scroll region + self.screen.cursor.y = self.scrolling_region.top; + self.screen.cursor.x = self.scrolling_region.left; + try self.deleteLines(count); } /// Options for scrolling the viewport of the terminal grid. @@ -2978,6 +2983,30 @@ test "Terminal: deleteLines left/right scroll region" { } } +test "Terminal: deleteLines left/right scroll region from top" { + const alloc = testing.allocator; + var t = try init(alloc, 10, 10); + defer t.deinit(alloc); + + try t.printString("ABC123"); + t.carriageReturn(); + try t.linefeed(); + try t.printString("DEF456"); + t.carriageReturn(); + try t.linefeed(); + try t.printString("GHI789"); + t.scrolling_region.left = 1; + t.scrolling_region.right = 3; + t.setCursorPos(1, 2); + try t.deleteLines(1); + + { + var str = try t.plainString(testing.allocator); + defer testing.allocator.free(str); + try testing.expectEqualStrings("AEF423\nDHI756\nG 89", str); + } +} + test "Terminal: deleteLines left/right scroll region high count" { const alloc = testing.allocator; var t = try init(alloc, 10, 10); @@ -5728,6 +5757,121 @@ test "Terminal: scrollDown outside of left/right scroll region" { } } +test "Terminal: scrollDown preserves pending wrap" { + const alloc = testing.allocator; + var t = try init(alloc, 5, 10); + defer t.deinit(alloc); + + t.setCursorPos(1, 5); + try t.print('A'); + t.setCursorPos(2, 5); + try t.print('B'); + t.setCursorPos(3, 5); + try t.print('C'); + try t.scrollDown(1); + try t.print('X'); + + { + var str = try t.plainString(testing.allocator); + defer testing.allocator.free(str); + try testing.expectEqualStrings("\n A\n B\nX C", str); + } +} + +test "Terminal: scrollUp 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.setCursorPos(2, 2); + const cursor = t.screen.cursor; + try t.scrollUp(1); + try testing.expectEqual(cursor, t.screen.cursor); + + { + var str = try t.plainString(testing.allocator); + defer testing.allocator.free(str); + try testing.expectEqualStrings("DEF\nGHI", str); + } +} + +test "Terminal: scrollUp top/bottom scroll region" { + 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, 3); + t.setCursorPos(1, 1); + try t.scrollUp(1); + + { + var str = try t.plainString(testing.allocator); + defer testing.allocator.free(str); + try testing.expectEqualStrings("ABC\nGHI", str); + } +} + +test "Terminal: scrollUp left/right scroll region" { + const alloc = testing.allocator; + var t = try init(alloc, 10, 10); + defer t.deinit(alloc); + + try t.printString("ABC123"); + t.carriageReturn(); + try t.linefeed(); + try t.printString("DEF456"); + t.carriageReturn(); + try t.linefeed(); + try t.printString("GHI789"); + t.scrolling_region.left = 1; + t.scrolling_region.right = 3; + t.setCursorPos(2, 2); + const cursor = t.screen.cursor; + try t.scrollUp(1); + try testing.expectEqual(cursor, t.screen.cursor); + + { + var str = try t.plainString(testing.allocator); + defer testing.allocator.free(str); + try testing.expectEqualStrings("AEF423\nDHI756\nG 89", str); + } +} + +test "Terminal: scrollUp preserves pending wrap" { + const alloc = testing.allocator; + var t = try init(alloc, 5, 5); + defer t.deinit(alloc); + + t.setCursorPos(1, 5); + try t.print('A'); + t.setCursorPos(2, 5); + try t.print('B'); + t.setCursorPos(3, 5); + try t.print('C'); + try t.scrollUp(1); + try t.print('X'); + + { + var str = try t.plainString(testing.allocator); + defer testing.allocator.free(str); + try testing.expectEqualStrings(" B\n C\n\nX", str); + } +} + test "Terminal: tabClear single" { const alloc = testing.allocator; var t = try init(alloc, 30, 5); diff --git a/src/terminal/stream.zig b/src/terminal/stream.zig index 0abe37182..5d6f5cdae 100644 --- a/src/terminal/stream.zig +++ b/src/terminal/stream.zig @@ -1487,3 +1487,37 @@ test "stream: DECEL, DECSEL" { try testing.expect(!s.handler.protected.?); } } + +test "stream: DECSCUSR" { + const H = struct { + style: ?ansi.CursorStyle = null, + + pub fn setCursorStyle(self: *@This(), style: ansi.CursorStyle) !void { + self.style = style; + } + }; + + var s: Stream(H) = .{ .handler = .{} }; + try s.nextSlice("\x1B[ q"); + try testing.expect(s.handler.style.? == .default); + + try s.nextSlice("\x1B[1 q"); + try testing.expect(s.handler.style.? == .blinking_block); +} + +test "stream: DECSCUSR without space" { + const H = struct { + style: ?ansi.CursorStyle = null, + + pub fn setCursorStyle(self: *@This(), style: ansi.CursorStyle) !void { + self.style = style; + } + }; + + var s: Stream(H) = .{ .handler = .{} }; + try s.nextSlice("\x1B[q"); + try testing.expect(s.handler.style == null); + + try s.nextSlice("\x1B[1q"); + try testing.expect(s.handler.style == null); +} diff --git a/website/app/vt/deckpam/page.mdx b/website/app/vt/deckpam/page.mdx new file mode 100644 index 000000000..06abae27e --- /dev/null +++ b/website/app/vt/deckpam/page.mdx @@ -0,0 +1,7 @@ +import VTSequence from "@/components/VTSequence"; + +# Keypad Application Mode (DECKPAM) + +"]} /> + +Sets keypad numeric mode. diff --git a/website/app/vt/decscusr/page.mdx b/website/app/vt/decscusr/page.mdx new file mode 100644 index 000000000..ffa1d963f --- /dev/null +++ b/website/app/vt/decscusr/page.mdx @@ -0,0 +1,24 @@ +import VTSequence from "@/components/VTSequence"; + +# Set Cursor Style (DECSCUSR) + + + +Set the mouse cursor style. + +If `n` is omitted, `n` defaults to `0`. `n` must be an integer between +0 and 6 (inclusive). The mapping of `n` to cursor style is below: + +| n | style | +| --- | --------------------- | +| 0 | terminal default | +| 1 | blinking block | +| 2 | steady block | +| 3 | blinking underline | +| 4 | steady underline | +| 5 | blinking vertical bar | +| 6 | steady vertical bar | + +For `n = 0`, the terminal default is up to the terminal and is inconsistent +across terminal implementations. The default may also be impacted by terminal +configuration. diff --git a/website/app/vt/su/page.mdx b/website/app/vt/su/page.mdx new file mode 100644 index 000000000..58e2a8c0d --- /dev/null +++ b/website/app/vt/su/page.mdx @@ -0,0 +1,97 @@ +import VTSequence from "@/components/VTSequence"; + +# Scroll Up (SU) + + + +Remove `n` lines from the top of the scroll region and shift existing +lines up. + +The parameter `n` must be an integer greater than or equal to 1. If `n` is less than +or equal to 0, adjust `n` to be 1. If `n` is omitted, `n` defaults to 1. + +This sequence executes [Delete Line (DL)](/vt/dl) with the cursor position +set to the top of the scroll region. There are some differences from DL +which are explained below. + +The cursor position after the operation must be unchanged from when SU was +invoked. The pending wrap state is _not_ reset. + +## Validation + +### SU V-1: Simple Usage + +```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;2H" +printf "\033[S" +``` + +``` +|DEF_____| +|GHI_____| +``` + +### SU V-2: Top/Bottom Scroll Region + +```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;3r" # scroll region top/bottom +printf "\033[1;1H" +printf "\033[S" +``` + +``` +|ABC_____| +|GHI_____| +``` + +### SU V-3: Left/Right Scroll Regions + +```bash +printf "\033[1;1H" # move to top-left +printf "\033[0J" # clear screen +printf "ABC123\n" +printf "DEF456\n" +printf "GHI789\n" +printf "\033[?69h" # enable left/right margins +printf "\033[2;4s" # scroll region left/right +printf "\033[2;2H" +printf "\033[S" +``` + +``` +|AEF423__| +|DHI756__| +|G___89__| +``` + +### SU V-4: Preserves Pending Wrap + +```bash +cols=$(tput cols) +printf "\033[1;${cols}H" # move to top-right +printf "\033[2J" # clear screen +printf "A" +printf "\033[2;${cols}H" +printf "B" +printf "\033[3;${cols}H" +printf "C" +printf "\033[S" +printf "X" +``` + +``` +|_______B| +|_______C| +|________| +|X_______| +```