diff --git a/src/Session.zig b/src/Session.zig index f9355f9..6876888 100644 --- a/src/Session.zig +++ b/src/Session.zig @@ -38,6 +38,11 @@ watch: c_uint = 0, /// True once the child process has exited and the PTY hung up. exited: bool = false, +/// Cell size in pixels, as last set by a resize. Kept because the XTWINOPS +/// size queries report it and the answer has to come from somewhere; the +/// widget owns the real geometry and hands it down here. +cell_px: struct { w: u32 = 8, h: u32 = 16 } = .{}, + /// Latest state reported by the child. Kept so a repeated report of the /// state we are already in doesn't wake the UI for nothing. status: Status = .idle, @@ -139,13 +144,23 @@ pub fn create( errdefer self.stream.deinit(); // Wire up the side effects we care about. `readonly` handles terminal - // state but silently drops anything that needs to talk back to the - // child; we need the writes so that queries (cursor position, device - // attributes, in-band resize) get answers. + // state but silently drops anything that needs to talk back to the child. + // + // Every query below has to be answered, not merely permitted to write. + // `write_pty` is only the pipe; a query whose own effect is null is + // dropped before it ever reaches that pipe. And an unanswered query is + // not a missing feature, it is a stall: the standard way to test for a + // capability is to send the query followed by a device attributes + // request, and take the DA1 reply arriving alone as "unsupported". + // A terminal that answers neither leaves the program waiting out its + // own timeout, once per run, for every capability it probes. var effects = vt.TerminalStream.Handler.Effects.readonly; effects.write_pty = &effectWritePty; effects.title_changed = &effectTitleChanged; effects.progress_report = &effectProgressReport; + effects.device_attributes = &effectDeviceAttributes; + effects.color_scheme = &effectColorScheme; + effects.size = &effectSize; self.stream.handler.effects = effects; const shell = try defaultShell(alloc); @@ -268,6 +283,48 @@ fn effectWritePty(handler: *vt.TerminalStream.Handler, data: [:0]const u8) void } /// Effect callback: OSC 0/2 changed the window title. +/// The reply type an effect has to return, recovered from the effect's own +/// signature. libghostty-vt exports the device attributes *request* enum by +/// name but not the reply struct, and naming it this way can't drift from +/// what the callback is actually required to return. +fn EffectReply(comptime name: []const u8) type { + const Opt = @FieldType(vt.TerminalStream.Handler.Effects, name); + const Fn = @typeInfo(@typeInfo(Opt).optional.child).pointer.child; + return @typeInfo(Fn).@"fn".return_type.?; +} + +/// Effect callback: the child asked what kind of terminal this is (CSI c and +/// friends). The defaults describe a VT220 that does ANSI colour, which is +/// what this actually is — the answer matters less than there being one. +fn effectDeviceAttributes( + _: *vt.TerminalStream.Handler, +) EffectReply("device_attributes") { + return .{}; +} + +/// Effect callback: the child asked whether it is drawing on a light or dark +/// background (CSI ? 996 n), so it can pick readable colours. Answered from +/// the scheme the grid is actually painted in. +fn effectColorScheme( + _: *vt.TerminalStream.Handler, +) ?vt.device_status.ColorScheme { + return switch (theme.currentScheme()) { + .light => .light, + .dark => .dark, + }; +} + +/// Effect callback: the child asked for the terminal geometry (CSI 14/16/18 t). +fn effectSize(handler: *vt.TerminalStream.Handler) ?vt.size_report.Size { + const self = fromHandler(handler); + return .{ + .rows = self.term.rows, + .columns = self.term.cols, + .cell_width = self.cell_px.w, + .cell_height = self.cell_px.h, + }; +} + fn effectTitleChanged(handler: *vt.TerminalStream.Handler) void { const self = fromHandler(handler); self.on_title(self.ctx, self.term.title.items); @@ -320,6 +377,10 @@ pub fn write(self: *Session, bytes: []const u8) void { /// Resize the terminal grid and tell the child about it. pub fn resize(self: *Session, cols: u16, rows: u16, cell_w: u32, cell_h: u32) !void { + // Recorded before the early return: the grid can keep its dimensions + // across a font change that moves every pixel measurement. + self.cell_px = .{ .w = cell_w, .h = cell_h }; + if (cols == self.term.cols and rows == self.term.rows) return; try self.stream.handler.resize(.{