diff --git a/src/terminal/Terminal.zig b/src/terminal/Terminal.zig
index 1cbcf82aa8..6991d8ad10 100644
--- a/src/terminal/Terminal.zig
+++ b/src/terminal/Terminal.zig
@@ -898,11 +898,16 @@ pub fn index(self: *Terminal) !void {
// If the cursor is inside the scrolling region and on the bottom-most
// line, then we scroll up. If our scrolling region is the full screen
// we create scrollback.
- if (self.screen.cursor.y == self.scrolling_region.bottom) {
+ if (self.screen.cursor.y == self.scrolling_region.bottom and
+ self.screen.cursor.x >= self.scrolling_region.left and
+ self.screen.cursor.x <= self.scrolling_region.right)
+ {
// If our scrolling region is the full screen, we create scrollback.
// Otherwise, we simply scroll the region.
if (self.scrolling_region.top == 0 and
- self.scrolling_region.bottom == self.rows - 1)
+ self.scrolling_region.bottom == self.rows - 1 and
+ self.scrolling_region.left == 0 and
+ self.scrolling_region.right == self.cols - 1)
{
try self.screen.scroll(.{ .screen = 1 });
} else {
@@ -1449,16 +1454,21 @@ pub fn cursorRight(self: *Terminal, count: usize) void {
/// Move the cursor down amount lines. If amount is greater than the maximum
/// move distance then it is internally adjusted to the maximum. This sequence
/// will not scroll the screen or scroll region. If amount is 0, adjust it to 1.
-// TODO: test
-pub fn cursorDown(self: *Terminal, count: usize) void {
+pub fn cursorDown(self: *Terminal, count_req: usize) void {
const tracy = trace(@src());
defer tracy.end();
+ // Always resets pending wrap
self.screen.cursor.pending_wrap = false;
- self.screen.cursor.y += if (count == 0) 1 else count;
- if (self.screen.cursor.y >= self.rows) {
- self.screen.cursor.y = self.rows - 1;
- }
+
+ // The max the cursor can move to depends where the cursor currently is
+ const max = if (self.screen.cursor.y <= self.scrolling_region.bottom)
+ self.scrolling_region.bottom
+ else
+ self.rows - 1;
+
+ const count = @max(count_req, 1);
+ self.screen.cursor.y = @min(max, self.screen.cursor.y +| count);
}
/// Move the cursor up amount lines. If amount is greater than the maximum
@@ -2823,6 +2833,138 @@ test "Terminal: index from the bottom outside of scroll region" {
}
}
+test "Terminal: index no scroll region, top of screen" {
+ const alloc = testing.allocator;
+ var t = try init(alloc, 5, 5);
+ defer t.deinit(alloc);
+
+ try t.print('A');
+ try t.index();
+ try t.print('X');
+
+ {
+ var str = try t.plainString(testing.allocator);
+ defer testing.allocator.free(str);
+ try testing.expectEqualStrings("A\n X", str);
+ }
+}
+
+test "Terminal: index bottom of primary screen" {
+ const alloc = testing.allocator;
+ var t = try init(alloc, 5, 5);
+ defer t.deinit(alloc);
+
+ t.setCursorPos(5, 1);
+ try t.print('A');
+ try t.index();
+ try t.print('X');
+
+ {
+ var str = try t.plainString(testing.allocator);
+ defer testing.allocator.free(str);
+ try testing.expectEqualStrings("\n\n\nA\n X", str);
+ }
+}
+
+test "Terminal: index inside scroll region" {
+ const alloc = testing.allocator;
+ var t = try init(alloc, 5, 5);
+ defer t.deinit(alloc);
+
+ t.setScrollingRegion(1, 3);
+ try t.print('A');
+ try t.index();
+ try t.print('X');
+
+ {
+ var str = try t.plainString(testing.allocator);
+ defer testing.allocator.free(str);
+ try testing.expectEqualStrings("A\n X", str);
+ }
+}
+
+test "Terminal: index bottom of scroll region" {
+ const alloc = testing.allocator;
+ var t = try init(alloc, 5, 5);
+ defer t.deinit(alloc);
+
+ t.setScrollingRegion(1, 3);
+ t.setCursorPos(4, 1);
+ try t.print('B');
+ t.setCursorPos(3, 1);
+ try t.print('A');
+ try t.index();
+ try t.print('X');
+
+ {
+ var str = try t.plainString(testing.allocator);
+ defer testing.allocator.free(str);
+ try testing.expectEqualStrings("\nA\n X\nB", str);
+ }
+}
+
+test "Terminal: index bottom of primary screen with scroll region" {
+ const alloc = testing.allocator;
+ var t = try init(alloc, 5, 5);
+ defer t.deinit(alloc);
+
+ t.setScrollingRegion(1, 3);
+ t.setCursorPos(3, 1);
+ try t.print('A');
+ t.setCursorPos(5, 1);
+ try t.index();
+ try t.index();
+ try t.index();
+ try t.print('X');
+
+ {
+ var str = try t.plainString(testing.allocator);
+ defer testing.allocator.free(str);
+ try testing.expectEqualStrings("\n\nA\n\nX", str);
+ }
+}
+
+test "Terminal: index outside left/right margin" {
+ const alloc = testing.allocator;
+ var t = try init(alloc, 10, 5);
+ defer t.deinit(alloc);
+
+ t.setScrollingRegion(1, 3);
+ t.scrolling_region.left = 3;
+ t.scrolling_region.right = 5;
+ t.setCursorPos(3, 3);
+ try t.print('A');
+ t.setCursorPos(3, 1);
+ try t.index();
+ try t.print('X');
+
+ {
+ var str = try t.plainString(testing.allocator);
+ defer testing.allocator.free(str);
+ try testing.expectEqualStrings("\n\nX A", str);
+ }
+}
+
+test "Terminal: index inside left/right margin" {
+ const alloc = testing.allocator;
+ var t = try init(alloc, 10, 5);
+ defer t.deinit(alloc);
+
+ t.setScrollingRegion(1, 3);
+ t.scrolling_region.left = 3;
+ t.scrolling_region.right = 5;
+ t.setCursorPos(3, 3);
+ try t.print('A');
+ try t.index();
+ try t.print('X');
+
+ {
+ var str = try t.plainString(testing.allocator);
+ defer testing.allocator.free(str);
+ try testing.expectEqualStrings("\n A\n X", str);
+ }
+}
+
test "Terminal: DECALN" {
const alloc = testing.allocator;
var t = try init(alloc, 2, 2);
@@ -3561,3 +3703,72 @@ test "Terminal: cursorLeft extended reverse wrap is priority if both set" {
try testing.expectEqualStrings("ABCDE\n1\n X", str);
}
}
+
+test "Terminal: cursorDown basic" {
+ const alloc = testing.allocator;
+ var t = try init(alloc, 5, 5);
+ defer t.deinit(alloc);
+
+ try t.print('A');
+ t.cursorDown(10);
+ try t.print('X');
+
+ {
+ var str = try t.plainString(testing.allocator);
+ defer testing.allocator.free(str);
+ try testing.expectEqualStrings("A\n\n\n\n X", str);
+ }
+}
+
+test "Terminal: cursorDown above bottom scroll margin" {
+ const alloc = testing.allocator;
+ var t = try init(alloc, 5, 5);
+ defer t.deinit(alloc);
+
+ t.setScrollingRegion(1, 3);
+ try t.print('A');
+ t.cursorDown(10);
+ try t.print('X');
+
+ {
+ var str = try t.plainString(testing.allocator);
+ defer testing.allocator.free(str);
+ try testing.expectEqualStrings("A\n\n X", str);
+ }
+}
+
+test "Terminal: cursorDown below bottom scroll margin" {
+ const alloc = testing.allocator;
+ var t = try init(alloc, 5, 5);
+ defer t.deinit(alloc);
+
+ t.setScrollingRegion(1, 3);
+ try t.print('A');
+ t.setCursorPos(4, 1);
+ t.cursorDown(10);
+ try t.print('X');
+
+ {
+ var str = try t.plainString(testing.allocator);
+ defer testing.allocator.free(str);
+ try testing.expectEqualStrings("A\n\n\n\nX", str);
+ }
+}
+
+test "Terminal: cursorDown resets wrap" {
+ const alloc = testing.allocator;
+ var t = try init(alloc, 5, 5);
+ defer t.deinit(alloc);
+
+ for ("ABCDE") |c| try t.print(c);
+ try testing.expect(t.screen.cursor.pending_wrap);
+ t.cursorDown(1);
+ try testing.expect(!t.screen.cursor.pending_wrap);
+ try t.print('X');
+
+ {
+ var str = try t.plainString(testing.allocator);
+ defer testing.allocator.free(str);
+ try testing.expectEqualStrings("ABCDE\n X", str);
+ }
+}
diff --git a/src/terminal/ansi.zig b/src/terminal/ansi.zig
index 96f4b9c0a6..27f9971aaf 100644
--- a/src/terminal/ansi.zig
+++ b/src/terminal/ansi.zig
@@ -21,6 +21,8 @@ pub const C0 = enum(u7) {
LF = 0x0A,
/// Vertical Tab
VT = 0x0B,
+ /// Form feed
+ FF = 0x0C,
/// Carriage return
CR = 0x0D,
/// Shift out
diff --git a/src/terminal/stream.zig b/src/terminal/stream.zig
index 6e4ffb2350..6940be0820 100644
--- a/src/terminal/stream.zig
+++ b/src/terminal/stream.zig
@@ -115,13 +115,7 @@ pub fn Stream(comptime Handler: type) type {
else
log.warn("unimplemented execute: {x}", .{c}),
- .LF => if (@hasDecl(T, "linefeed"))
- try self.handler.linefeed()
- else
- log.warn("unimplemented execute: {x}", .{c}),
-
- // VT is same as LF
- .VT => if (@hasDecl(T, "linefeed"))
+ .LF, .VT, .FF => if (@hasDecl(T, "linefeed"))
try self.handler.linefeed()
else
log.warn("unimplemented execute: {x}", .{c}),
diff --git a/website/app/vt/cud/page.mdx b/website/app/vt/cud/page.mdx
new file mode 100644
index 0000000000..63c51c5628
--- /dev/null
+++ b/website/app/vt/cud/page.mdx
@@ -0,0 +1,75 @@
+import VTSequence from "@/components/VTSequence";
+
+# Cursor Down (CUD)
+
+
+
+Move the cursor `n` cells down.
+
+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 always unsets the pending wrap state.
+
+If the current cursor position is at or above the [bottom margin](#TODO),
+the lowest point the cursor can move is the bottom margin. If the current
+cursor position is below the bottom margin, the lowest point the cursor
+can move is the final row.
+
+This sequence never triggers scrolling.
+
+## Validation
+
+### CUD V-1: Cursor Down
+
+```bash
+printf "A"
+printf "\033[2B" # cursor down
+printf "X"
+```
+
+```
+|A_________|
+|__________|
+|_Xc_______|
+```
+
+### CUD V-2: Cursor Down Above Bottom Margin
+
+```bash
+printf "\033[1;1H" # move to top-left
+printf "\033[0J" # clear screen
+printf "\n\n\n\n" # screen is 4 high
+printf "\033[1;3r" # set scrolling region
+printf "A"
+printf "\033[5B" # cursor down
+printf "X"
+```
+
+```
+|A_________|
+|__________|
+|_Xc_______|
+|__________|
+```
+
+### CUD V-3: Cursor Down Below Bottom Margin
+
+```bash
+printf "\033[1;1H" # move to top-left
+printf "\033[0J" # clear screen
+printf "\n\n\n\n\n" # screen is 5 high
+printf "\033[1;3r" # set scrolling region
+printf "A"
+printf "\033[4;1H" # move below region
+printf "\033[5B" # cursor down
+printf "X"
+```
+
+```
+|A_________|
+|__________|
+|__________|
+|__________|
+|_Xc_______|
+```
diff --git a/website/app/vt/ind/page.mdx b/website/app/vt/ind/page.mdx
new file mode 100644
index 0000000000..0710333e38
--- /dev/null
+++ b/website/app/vt/ind/page.mdx
@@ -0,0 +1,138 @@
+import VTSequence from "@/components/VTSequence";
+
+# Index (IND)
+
+
+
+Move the cursor down one cell, scrolling if necessary.
+
+This sequence always unsets the pending wrap state.
+
+If the cursor is exactly on the bottom margin and is at or within the
+[left](#TODO) and [right margin](#TODO), [scroll up](#TODO) one line.
+If the scroll region is the full terminal screen and the terminal is on
+the [primary screen](#TODO), this may create scrollback. See the
+[scroll](#TODO) documentation for more details.
+
+If the cursor is outside of the scroll region or not on the bottom
+margin of the scroll region, perform the [cursor down](/vt/cud) operation with
+`n = 1`.
+
+This sequence will only scroll when the cursor is exactly on the bottom
+margin and within the remaining scroll region. If the cursor is outside
+the scroll region and on the bottom line of the terminal, the cursor
+does not move.
+
+## Validation
+
+### IND V-1: No Scroll Region, Top of Screen
+
+```bash
+printf "\033[1;1H" # move to top-left
+printf "\033[0J" # clear screen
+printf "A"
+printf "\033D" # index
+printf "X"
+```
+
+```
+|A_________|
+|_Xc_______|
+```
+
+### IND V-2: Bottom of Primary Screen
+
+```bash
+lines=$(tput lines)
+printf "\033[1;1H" # move to top-left
+printf "\033[0J" # clear screen
+printf "\033[${lines};1H" # move to bottom-left
+printf "A"
+printf "\033D" # index
+printf "X"
+```
+
+```
+|A_________|
+|_Xc_______|
+```
+
+### IND V-3: Inside Scroll Region
+
+```bash
+printf "\033[1;1H" # move to top-left
+printf "\033[0J" # clear screen
+printf "\033[1;3r" # scroll region
+printf "A"
+printf "\033D" # index
+printf "X"
+```
+
+```
+|A_________|
+|_Xc_______|
+```
+
+### IND V-4: Bottom of Scroll Region
+
+```bash
+printf "\033[1;1H" # move to top-left
+printf "\033[0J" # clear screen
+printf "\033[1;3r" # scroll region
+printf "\033[4;1H" # below scroll region
+printf "B"
+printf "\033[3;1H" # move to last row of region
+printf "A"
+printf "\033D" # index
+printf "X"
+```
+
+```
+|__________|
+|A_________|
+|_Xc_______|
+|B_________|
+```
+
+### IND V-5: Bottom of Primary Screen with Scroll Region
+
+```bash
+lines=$(tput lines)
+printf "\033[1;1H" # move to top-left
+printf "\033[0J" # clear screen
+printf "\033[1;3r" # scroll region
+printf "\033[3;1H" # move to last row of region
+printf "A"
+printf "\033[${lines};1H" # move to bottom-left
+printf "\033D" # index
+printf "X"
+```
+
+```
+|__________|
+|__________|
+|A_________|
+|__________|
+|Xc________|
+```
+
+### IND V-6: Outside of Left/Right Scroll Region
+
+```bash
+printf "\033[1;1H" # move to top-left
+printf "\033[0J" # clear screen
+printf "\033[?69h" # enable left/right margins
+printf "\033[1;3r" # scroll region top/bottom
+printf "\033[3;5s" # scroll region left/right
+printf "\033[3;3H"
+printf "A"
+printf "\033[3;1H"
+printf "\033D" # index
+printf "X"
+```
+
+```
+|__________|
+|__________|
+|XcA_______|
+```
diff --git a/website/app/vt/lf/page.mdx b/website/app/vt/lf/page.mdx
new file mode 100644
index 0000000000..06b80eb0f7
--- /dev/null
+++ b/website/app/vt/lf/page.mdx
@@ -0,0 +1,10 @@
+import VTSequence from "@/components/VTSequence";
+
+# Linefeed (LF)
+
+
+
+This is an alias for [index (IND)](/vt/ind).
+
+If [linefeed mode (mode 20)](#TODO) is enabled, perform a
+[carriage return](/vt/cr) after the IND operation.
diff --git a/website/components/VTSequence.tsx b/website/components/VTSequence.tsx
index 7cb2c05593..4717195629 100644
--- a/website/components/VTSequence.tsx
+++ b/website/components/VTSequence.tsx
@@ -45,6 +45,7 @@ function VTElem({ elem }: { elem: string }) {
const special: { [key: string]: number } = {
BEL: 0x07,
BS: 0x08,
+ LF: 0x0a,
CR: 0x0d,
ESC: 0x1b,
};