Key events are encoded before crossing the thread boundary
`Surface.encodeKey` builds key encoding options, asks `input.key_encode.encode` to serialize the event, stores small outputs inline when possible, and allocates only for larger data.
var data: termio.Message.WriteReq.Small.Array = undefined;
var writer: std.Io.Writer = .fixed(&data);
if (input.key_encode.encode(&writer, event, encoding_opts)) {
const written = writer.buffered();
if (written.len == 0) return null;
break :req .{ .small = .{ .data = data, .len = @intCast(written.len) } };
}
Surface.queueIo centralizes outgoing IO messages
`queueIo` is the surface-side choke point. It can block writes in read-only mode, then delegates to `Termio.queueMessage`.
if (self.readonly) {
switch (msg) {
.write_small, .write_stable, .write_alloc => return,
else => {},
}
}
self.io.queueMessage(msg, mutex);
The mailbox is SPSC and wakes the IO loop
The mailbox uses a fixed-capacity SPSC queue plus an async wakeup handle. `send` tries the fast nonblocking path first. If the queue is full and a renderer mutex is held, it temporarily unlocks the mutex while waiting to avoid a deadlock between producers and the IO thread.
if (mb.queue.push(msg, .{ .instant = {} }) > 0) break :send;
mb.wakeup.notify() catch |err| {
log.warn("failed to wake up writer, data will be dropped err={}", .{err});
return;
};
if (mutex) |m| m.unlock();
defer if (mutex) |m| m.lock();
_ = mb.queue.push(msg, .{ .forever = {} });
The IO thread drains messages and dispatches writes or state changes
`termio.Thread` registers the mailbox wakeup with its event loop. Each wakeup drains queued messages, dispatches resizes, focus, config, selection, and write messages, and triggers a renderer wakeup after draining if anything changed.
while (mailbox.pop()) |message| {
redraw = true;
switch (message) {
.resize => |v| self.handleResize(cb, v),
.write_small => |v| try io.queueWrite(data, v.data[0..v.len], self.flags.linefeed_mode),
.write_stable => |v| try io.queueWrite(data, v, self.flags.linefeed_mode),
.write_alloc => |v| { defer v.alloc.free(v.data); try io.queueWrite(data, v.data, self.flags.linefeed_mode); },
else => {},
}
}
if (redraw) try io.renderer_wakeup.notify();
Read next: PTY and Stream follows the return path from subprocess output into terminal state.