terminal: make stream processing infallible

The terminal.Stream next/nextSlice functions can now no longer fail.
All prior failure modes were fully isolated in the handler `vt`
callbacks. As such, vt callbacks are now required to not return an error
and handle their own errors somehow.

Allowing streams to be fallible before was an incorrect design. It
caused problematic scenarios like in `nextSlice` early terminating
processing due to handler errors. This should not be possible.

There is no safe way to bubble up vt errors through the stream because
if nextSlice is called and multiple errors are returned, we can't
coalesce them. We could modify that to return a partial result but its
just more work for stream that is unnecessary. The handler can do all of
this.

This work was discovered due to cleanups to prepare for more C APIs.
Less errors make C APIs easier to implement! And, it helps clean up our
Zig, too.
This commit is contained in:
Mitchell Hashimoto
2026-03-13 13:14:03 -07:00
parent 04fa71e237
commit 2044e5030f
23 changed files with 791 additions and 782 deletions

View File

@@ -908,7 +908,7 @@ test "basic text" {
var s = t.vtStream();
defer s.deinit();
try s.nextSlice("ABCD");
s.nextSlice("ABCD");
var state: RenderState = .empty;
defer state.deinit(alloc);
@@ -944,9 +944,9 @@ test "styled text" {
var s = t.vtStream();
defer s.deinit();
try s.nextSlice("\x1b[1mA"); // Bold
try s.nextSlice("\x1b[0;3mB"); // Italic
try s.nextSlice("\x1b[0;4mC"); // Underline
s.nextSlice("\x1b[1mA"); // Bold
s.nextSlice("\x1b[0;3mB"); // Italic
s.nextSlice("\x1b[0;4mC"); // Underline
var state: RenderState = .empty;
defer state.deinit(alloc);
@@ -990,8 +990,8 @@ test "grapheme" {
var s = t.vtStream();
defer s.deinit();
try s.nextSlice("A");
try s.nextSlice("👨‍"); // this has a ZWJ
s.nextSlice("A");
s.nextSlice("👨‍"); // this has a ZWJ
var state: RenderState = .empty;
defer state.deinit(alloc);
@@ -1037,7 +1037,7 @@ test "cursor state in viewport" {
var s = t.vtStream();
defer s.deinit();
try s.nextSlice("A\x1b[H");
s.nextSlice("A\x1b[H");
var state: RenderState = .empty;
defer state.deinit(alloc);
@@ -1052,14 +1052,14 @@ test "cursor state in viewport" {
try testing.expect(state.cursor.style.default());
// Set a style on the cursor
try s.nextSlice("\x1b[1m"); // Bold
s.nextSlice("\x1b[1m"); // Bold
try state.update(alloc, &t);
try testing.expect(!state.cursor.style.default());
try testing.expect(state.cursor.style.flags.bold);
try s.nextSlice("\x1b[0m"); // Reset style
s.nextSlice("\x1b[0m"); // Reset style
// Move cursor to 2,1
try s.nextSlice("\x1b[2;3H");
s.nextSlice("\x1b[2;3H");
try state.update(alloc, &t);
try testing.expectEqual(2, state.cursor.active.x);
try testing.expectEqual(1, state.cursor.active.y);
@@ -1079,7 +1079,7 @@ test "cursor state out of viewport" {
var s = t.vtStream();
defer s.deinit();
try s.nextSlice("A\r\nB\r\nC\r\nD\r\n");
s.nextSlice("A\r\nB\r\nC\r\nD\r\n");
var state: RenderState = .empty;
defer state.deinit(alloc);
@@ -1139,7 +1139,7 @@ test "dirty state" {
}
// Write to first line
try s.nextSlice("A");
s.nextSlice("A");
try state.update(alloc, &t);
try testing.expectEqual(.partial, state.dirty);
{
@@ -1170,7 +1170,7 @@ test "colors" {
try state.update(alloc, &t);
// Change cursor color
try s.nextSlice("\x1b]12;#FF0000\x07");
s.nextSlice("\x1b]12;#FF0000\x07");
try state.update(alloc, &t);
const c = state.colors.cursor.?;
@@ -1179,7 +1179,7 @@ test "colors" {
try testing.expectEqual(0, c.b);
// Change palette color 0 to White
try s.nextSlice("\x1b]4;0;#FFFFFF\x07");
s.nextSlice("\x1b]4;0;#FFFFFF\x07");
try state.update(alloc, &t);
const p0 = state.colors.palette[0];
try testing.expectEqual(0xFF, p0.r);
@@ -1275,7 +1275,7 @@ test "linkCells" {
defer state.deinit(alloc);
// Create a hyperlink
try s.nextSlice("\x1b]8;;http://example.com\x1b\\LINK\x1b]8;;\x1b\\");
s.nextSlice("\x1b]8;;http://example.com\x1b\\LINK\x1b]8;;\x1b\\");
try state.update(alloc, &t);
// Query link at 0,0
@@ -1306,7 +1306,7 @@ test "string" {
var s = t.vtStream();
defer s.deinit();
try s.nextSlice("AB");
s.nextSlice("AB");
var state: RenderState = .empty;
defer state.deinit(alloc);
@@ -1345,12 +1345,12 @@ test "linkCells with scrollback spanning pages" {
const first_page_cap = pages.pages.first.?.data.capacity.rows;
// Fill first page
for (0..first_page_cap - 1) |_| try s.nextSlice("\r\n");
for (0..first_page_cap - 1) |_| s.nextSlice("\r\n");
// Create second page with hyperlink
try s.nextSlice("\r\n");
try s.nextSlice("\x1b]8;;http://example.com\x1b\\LINK\x1b]8;;\x1b\\");
for (0..(tail_rows - 1)) |_| try s.nextSlice("\r\n");
s.nextSlice("\r\n");
s.nextSlice("\x1b]8;;http://example.com\x1b\\LINK\x1b]8;;\x1b\\");
for (0..(tail_rows - 1)) |_| s.nextSlice("\r\n");
var state: RenderState = .empty;
defer state.deinit(alloc);
@@ -1416,7 +1416,7 @@ test "dirty row resets highlights" {
var s = t.vtStream();
defer s.deinit();
try s.nextSlice("ABC");
s.nextSlice("ABC");
var state: RenderState = .empty;
defer state.deinit(alloc);
@@ -1451,8 +1451,8 @@ test "dirty row resets highlights" {
}
// Write to row 0 to make it dirty
try s.nextSlice("\x1b[H"); // Move to home
try s.nextSlice("X");
s.nextSlice("\x1b[H"); // Move to home
s.nextSlice("X");
try state.update(alloc, &t);
// Verify the highlight was reset on the dirty row