diff --git a/src/terminal/Terminal.zig b/src/terminal/Terminal.zig
index 6991d8ad1..35e724aba 100644
--- a/src/terminal/Terminal.zig
+++ b/src/terminal/Terminal.zig
@@ -1497,7 +1497,7 @@ pub fn horizontalTab(self: *Terminal) !void {
const tracy = trace(@src());
defer tracy.end();
- while (self.screen.cursor.x < self.cols - 1) {
+ while (self.screen.cursor.x < self.scrolling_region.right) {
// Move the cursor right
self.screen.cursor.x += 1;
@@ -1513,9 +1513,12 @@ pub fn horizontalTabBack(self: *Terminal) !void {
const tracy = trace(@src());
defer tracy.end();
+ // With origin mode enabled, our leftmost limit is the left margin.
+ const left_limit = if (self.modes.get(.origin)) self.scrolling_region.left else 0;
+
while (true) {
// If we're already at the edge of the screen, then we're done.
- if (self.screen.cursor.x == 0) return;
+ if (self.screen.cursor.x == left_limit) return;
// Move the cursor left
self.screen.cursor.x -= 1;
@@ -2291,6 +2294,43 @@ test "Terminal: horizontal tabs" {
try testing.expectEqual(@as(usize, 19), t.screen.cursor.x);
}
+test "Terminal: horizontal tabs starting on tabstop" {
+ const alloc = testing.allocator;
+ var t = try init(alloc, 20, 5);
+ defer t.deinit(alloc);
+
+ t.screen.cursor.x = 8;
+ try t.print('X');
+ t.screen.cursor.x = 8;
+ try t.horizontalTab();
+ try t.print('A');
+
+ {
+ var str = try t.plainString(testing.allocator);
+ defer testing.allocator.free(str);
+ try testing.expectEqualStrings(" X A", str);
+ }
+}
+
+test "Terminal: horizontal tabs with right margin" {
+ const alloc = testing.allocator;
+ var t = try init(alloc, 20, 5);
+ defer t.deinit(alloc);
+
+ t.scrolling_region.left = 2;
+ t.scrolling_region.right = 5;
+ t.screen.cursor.x = 0;
+ try t.print('X');
+ try t.horizontalTab();
+ try t.print('A');
+
+ {
+ var str = try t.plainString(testing.allocator);
+ defer testing.allocator.free(str);
+ try testing.expectEqualStrings("X A", str);
+ }
+}
+
test "Terminal: horizontal tabs back" {
const alloc = testing.allocator;
var t = try init(alloc, 20, 5);
@@ -2314,6 +2354,44 @@ test "Terminal: horizontal tabs back" {
try testing.expectEqual(@as(usize, 0), t.screen.cursor.x);
}
+test "Terminal: horizontal tabs back starting on tabstop" {
+ const alloc = testing.allocator;
+ var t = try init(alloc, 20, 5);
+ defer t.deinit(alloc);
+
+ t.screen.cursor.x = 8;
+ try t.print('X');
+ t.screen.cursor.x = 8;
+ try t.horizontalTabBack();
+ try t.print('A');
+
+ {
+ var str = try t.plainString(testing.allocator);
+ defer testing.allocator.free(str);
+ try testing.expectEqualStrings("A X", str);
+ }
+}
+
+test "Terminal: horizontal tabs with left margin in origin mode" {
+ const alloc = testing.allocator;
+ var t = try init(alloc, 20, 5);
+ defer t.deinit(alloc);
+
+ t.modes.set(.origin, true);
+ t.scrolling_region.left = 2;
+ t.scrolling_region.right = 5;
+ t.screen.cursor.x = 3;
+ try t.print('X');
+ try t.horizontalTabBack();
+ try t.print('A');
+
+ {
+ var str = try t.plainString(testing.allocator);
+ defer testing.allocator.free(str);
+ try testing.expectEqualStrings(" AX", str);
+ }
+}
+
test "Terminal: setCursorPosition" {
var t = try init(testing.allocator, 80, 80);
defer t.deinit(testing.allocator);
diff --git a/website/app/vt/cbt/page.mdx b/website/app/vt/cbt/page.mdx
new file mode 100644
index 000000000..11ed98939
--- /dev/null
+++ b/website/app/vt/cbt/page.mdx
@@ -0,0 +1,83 @@
+import VTSequence from "@/components/VTSequence";
+
+# Cursor Backward Tabulation (CBT)
+
+
+
+Move the cursor `n` tabs left.
+
+The leftmost valid column for this operation is the first column. If
+[origin mode](#TODO) is enabled, then the leftmost valid column for this
+operatin is the [left margin](#TODO).
+
+Move the cursor left until the cursor position is on a tabstop. If the
+cursor would move past the leftmost valid column, the cursor remains at
+the leftmost valid column and the operation completes. Repeat this process
+`n` times.
+
+Tabstops are dynamic and can be set with escape sequences such as
+[horizontal tab set (HTS)](/vt/hts), [tab clear (TBC)](/vt/tbc), etc.
+A terminal emulator may default tabstops at any interval, though an interval
+of 8 spaces is most common.
+
+## Validation
+
+### CBT V-1: Left Beyond First Column
+
+```bash
+printf "\033[?W" # reset tab stops
+printf "\033[10Z"
+printf "A"
+```
+
+```
+|Ac________|
+```
+
+### CBT V-2: Left Starting After Tab Stop
+
+```bash
+printf "\033[?W" # reset tab stops
+printf "\033[1;10H"
+printf "X"
+printf "\033[Z"
+printf "A"
+```
+
+```
+|________AX|
+```
+
+### CBT V-3: Left Starting on Tabstop
+
+```bash
+printf "\033[?W" # reset tab stops
+printf "\033[1;9H"
+printf "X"
+printf "\033[1;9H"
+printf "\033[Z"
+printf "A"
+```
+
+```
+|A_______X_|
+```
+
+### CBT V-4: Left Margin with Origin Mode
+
+```bash
+printf "\033[1;1H" # move to top-left
+printf "\033[0J" # clear screen
+printf "\033[?W" # reset tab stops
+printf "\033[?6h" # enable origin mode
+printf "\033[?69h" # enable left/right margins
+printf "\033[3;6s" # scroll region left/right
+printf "\033[1;2H" # move cursor in region
+printf "X"
+printf "\033[Z"
+printf "A"
+```
+
+```
+|__AX______|
+```
diff --git a/website/app/vt/cht/page.mdx b/website/app/vt/cht/page.mdx
new file mode 100644
index 000000000..61ca2743f
--- /dev/null
+++ b/website/app/vt/cht/page.mdx
@@ -0,0 +1,70 @@
+import VTSequence from "@/components/VTSequence";
+
+# Cursor Horizontal Tabulation (CHT)
+
+
+
+Move the cursor `n` tabs right.
+
+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.
+
+The rightmost valid column for this operation is the rightmost column in
+the terminal screen or the [right margin](#TODO), whichever is smaller.
+This sequence does not change behavior with [origin mode](#TODO) set.
+
+Move the cursor right until the cursor position is on a tabstop. If the
+cursor would move past the rightmost valid column, the cursor remains at
+the rightmost valid column and the operation completes. Repeat this process
+`n` times.
+
+Tabstops are dynamic and can be set with escape sequences such as
+[horizontal tab set (HTS)](/vt/hts), [tab clear (TBC)](/vt/tbc), etc.
+A terminal emulator may default tabstops at any interval, though an interval
+of 8 spaces is most common.
+
+## Validation
+
+### CHT V-1: Right Beyond Last Column
+
+```bash
+printf "\033[?W" # reset tab stops
+printf "\033[100I" # assuming the test terminal has less than 800 columns
+printf "A"
+```
+
+```
+|_________A|
+```
+
+### CHT V-2: Right From Before a Tabstop
+
+```bash
+printf "\033[?W" # reset tab stops
+printf "\033[1;2H"
+printf "A"
+printf "\033[I"
+printf "X"
+```
+
+```
+|_A_____X__|
+```
+
+### CHT V-3: Right Margin
+
+```bash
+printf "\033[1;1H" # move to top-left
+printf "\033[0J" # clear screen
+printf "\033[?W" # reset tab stops
+printf "\033[?69h" # enable left/right margins
+printf "\033[3;6s" # scroll region left/right
+printf "\033[1;1H" # move cursor in region
+printf "X"
+printf "\033[I"
+printf "A"
+```
+
+```
+|__AX______|
+```
diff --git a/website/app/vt/tab/page.mdx b/website/app/vt/tab/page.mdx
new file mode 100644
index 000000000..95b1b3b41
--- /dev/null
+++ b/website/app/vt/tab/page.mdx
@@ -0,0 +1,8 @@
+import VTSequence from "@/components/VTSequence";
+
+# Tab (TAB)
+
+
+
+This is an alias for [cursor horizontal tabulation (CHT)](/vt/cht) with
+`n = 1`.
diff --git a/website/components/VTSequence.tsx b/website/components/VTSequence.tsx
index 471719562..23a69a77a 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,
+ TAB: 0x09,
LF: 0x0a,
CR: 0x0d,
ESC: 0x1b,