Termio creates the terminal and stream handler
`Termio.init` creates the `terminal.Terminal`, lets the backend initialize terminal-specific state, and builds `StreamHandler` with references to termio, surface, renderer, size, and terminal state.
const handler: StreamHandler = .{
.termio_mailbox = &self.mailbox,
.surface_mailbox = opts.surface_mailbox,
.renderer_state = opts.renderer_state,
.renderer_wakeup = opts.renderer_wakeup,
.renderer_mailbox = opts.renderer_mailbox,
.size = &self.size,
.terminal = &self.terminal,
};
self.* = .{
.terminal = term,
.terminal_stream = .initAlloc(alloc, handler),
};
The exec backend starts the subprocess on IO thread entry
`termio.Thread.threadMain_` calls `io.threadEnter`, which dispatches to the backend. For `Exec`, this starts the subprocess and sets up backend thread data before the IO loop begins processing mailbox wakeups.
try io.threadEnter(self, &cb.data);
defer cb.data.deinit();
defer io.threadExit(&cb.data);
try self.loop.run(.until_done);
Read threads feed raw bytes into Termio.processOutput
The POSIX read thread sets the PTY fd nonblocking, reads repeatedly until it would block, and calls `Termio.processOutput` for each buffer. The Windows path mirrors this using `ReadFile`.
while (true) {
const n = posix.read(fd, &buf) catch |err| {
switch (err) {
error.WouldBlock => break,
else => unreachable,
}
};
@call(.always_inline, termio.Termio.processOutput, .{ io, buf[0..n] });
}
Termio locks renderer state and feeds the terminal stream
Output processing is protected by `renderer_state.mutex` because it mutates terminal state that the renderer reads. The handler schedules a render before parsing, resets cursor blink as needed, records inspector data in slow mode, and feeds either byte-by-byte or as a slice.
self.renderer_state.mutex.lock();
defer self.renderer_state.mutex.unlock();
self.terminal_stream.handler.queueRender() catch unreachable;
self.terminal_stream.nextSlice(buf);
VT parser actions apply directly to terminal state
The stream handler receives parser actions and updates the terminal. The common path is intentionally branch-hinted around print, SGR attributes, cursor motion, line feed, and carriage return.
switch (action) {
.print => {
@branchHint(.likely);
try self.terminal.print(value.cp);
},
.cursor_pos => self.terminal.setCursorPos(value.row, value.col),
.set_attribute => self.terminal.setAttribute(value) catch |err| {
log.warn("error setting attribute {}: {}", .{ value, err });
},
else => {},
}
Read next: Renderer follows how these mutations become a new drawn frame.