Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 4 additions & 0 deletions src/Surface.zig
Original file line number Diff line number Diff line change
Expand Up @@ -930,6 +930,7 @@ pub fn init(
.size = size,
.full_config = config,
.config = try termio.Termio.DerivedConfig.init(alloc, config),
.color_scheme = self.config_conditional_state.theme,
.backend = io_backend,
.suppress_terminal_responses = if (comptime @hasDecl(apprt.runtime.Surface, "suppressTerminalResponses"))
rt_surface.suppressTerminalResponses()
Expand Down Expand Up @@ -6847,6 +6848,9 @@ pub fn colorSchemeCallback(self: *Surface, scheme: apprt.ColorScheme) !void {

// Setup our conditional state which has the current color theme.
self.config_conditional_state.theme = new_scheme;
// Reports must follow the surface even if a plain config has no conditional
// theme to reload, or the embedder coalesces the reload notification.
self.io.updateColorScheme(new_scheme);
self.notifyConfigConditionalState();

// If mode 2031 is on, then we report the change live.
Expand Down
46 changes: 45 additions & 1 deletion src/terminal/formatter.zig
Original file line number Diff line number Diff line change
Expand Up @@ -89,6 +89,11 @@ pub const Options = struct {
/// screen contents as it is rendered on the page in the given size.
unwrap: bool = false,

/// Preserve blank rows at the end of VT selections when the caller also
/// requests cursor/state restoration. Replay consumers need those rows to
/// keep the active screen anchored after retained scrollback.
preserve_trailing_blank_rows: bool = false,

/// Trim trailing whitespace on lines with other text. Trailing blank
/// lines are always trimmed. This only affects trailing whitespace
/// on rows that have at least one other cell with text. Whitespace
Expand Down Expand Up @@ -335,7 +340,12 @@ pub const TerminalFormatter = struct {
}
}

var screen_formatter: ScreenFormatter = .init(self.terminal.screens.active, self.opts);
var screen_opts = self.opts;
screen_opts.preserve_trailing_blank_rows =
screen_opts.preserve_trailing_blank_rows or
(self.opts.emit == .vt and self.extra.screen.cursor);
var screen_formatter: ScreenFormatter =
.init(self.terminal.screens.active, screen_opts);
screen_formatter.content = self.content;
screen_formatter.extra = self.extra.screen;
screen_formatter.pin_map = self.pin_map;
Expand Down Expand Up @@ -1428,6 +1438,40 @@ pub const PageFormatter = struct {
}
}

// Plain dumps intentionally omit trailing blank rows because their
// meaning cannot be recovered without the cursor. VT replay asks for
// cursor restoration, so preserve those physical rows before the
// terminal formatter emits the cursor and other state footer.
if (self.opts.preserve_trailing_blank_rows and blank_rows > 0) {
const sequence: []const u8 = switch (self.opts.emit) {
.plain => "\n",
.vt => "\r\n",
.html => "\n",
};
for (0..blank_rows) |_| try writer.writeAll(sequence);
Comment thread
coderabbitai[bot] marked this conversation as resolved.

if (self.point_map) |*map| {
const start: Coordinate = if (map.map.items.len > 0)
map.map.items[map.map.items.len - 1]
else
.{ .x = 0, .y = 0 };
map.map.appendNTimes(
map.alloc,
.{ .x = start.x, .y = start.y },
sequence.len,
) catch return error.WriteFailed;
for (1..blank_rows) |y_offset_usize| {
const y_offset: size.CellCountInt = @intCast(y_offset_usize);
map.map.appendNTimes(
map.alloc,
.{ .x = 0, .y = start.y + y_offset },
sequence.len,
) catch return error.WriteFailed;
}
}
blank_rows = 0;
}

// If the style is non-default, we need to close our style tag.
if (!style.default()) try self.formatStyleClose(writer);

Expand Down
4 changes: 4 additions & 0 deletions src/termio/Options.zig
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,10 @@ full_config: *const Config,
/// The derived configuration for this termio implementation.
config: termio.Termio.DerivedConfig,

/// The surface's current runtime scheme. This is independent of whether the
/// configuration contains any theme-conditional settings.
color_scheme: @import("../config.zig").ConditionalState.Theme,

/// The backend for termio that implements where reads/writes are sourced.
backend: termio.Backend,

Expand Down
17 changes: 14 additions & 3 deletions src/termio/Termio.zig
Original file line number Diff line number Diff line change
Expand Up @@ -44,6 +44,10 @@ backend: termio.Backend,
/// The derived configuration for this termio implementation.
config: DerivedConfig,

/// Runtime scheme used by CSI 996 queries and mode 2031 reports. Guarded by
/// renderer_state.mutex; config swaps must never overwrite appearance state.
color_scheme: configpkg.ConditionalState.Theme,

/// The terminal emulator internal state. This is the abstract "terminal"
/// that manages input, grid updating, etc. and is renderer-agnostic. It
/// just stores internal state about a grid.
Expand Down Expand Up @@ -209,7 +213,6 @@ pub const DerivedConfig = struct {
osc_color_report_format: configpkg.Config.OSCColorReportFormat,
clipboard_write: configpkg.ClipboardAccess,
enquiry_response: []const u8,
conditional_state: configpkg.ConditionalState,

pub fn init(
alloc_gpa: Allocator,
Expand Down Expand Up @@ -249,7 +252,6 @@ pub const DerivedConfig = struct {
.osc_color_report_format = config.@"osc-color-report-format",
.clipboard_write = config.@"clipboard-write",
.enquiry_response = try alloc.dupe(u8, config.@"enquiry-response"),
.conditional_state = config._conditional_state,

// This has to be last so that we copy AFTER the arena allocations
// above happen (Zig assigns in order).
Expand Down Expand Up @@ -374,6 +376,7 @@ pub fn init(self: *Termio, alloc: Allocator, opts: termio.Options) !void {
.alloc = alloc,
.terminal = term,
.config = opts.config,
.color_scheme = opts.color_scheme,
.renderer_state = opts.renderer_state,
.renderer_wakeup = opts.renderer_wakeup,
.renderer_mailbox = opts.renderer_mailbox,
Expand Down Expand Up @@ -625,6 +628,14 @@ pub fn changeConfig(self: *Termio, td: *ThreadData, config: *DerivedConfig) !voi
self.terminal.setKittyGraphicsLoadingLimits(.allWithTempDir(global.tmpDirPath()));
}

/// Update report state synchronously with the surface appearance callback.
/// The IO thread reads it under the same lock when it handles a queued query.
pub fn updateColorScheme(self: *Termio, scheme: configpkg.ConditionalState.Theme) void {
self.renderer_state.mutex.lockUncancelable(global.io());
defer self.renderer_state.mutex.unlock(global.io());
self.color_scheme = scheme;
}

/// Update only the terminal color defaults used by OSC resets.
///
/// Manual-IO embedders call this on the same serial executor as processOutput.
Expand Down Expand Up @@ -972,7 +983,7 @@ pub fn colorSchemeReportLocked(self: *Termio, td: *ThreadData, force: bool) !voi
if (!force and !self.renderer_state.terminal.modes.get(.report_color_scheme)) {
return;
}
const scheme: terminalpkg.device_status.ColorScheme = switch (self.config.conditional_state.theme) {
const scheme: terminalpkg.device_status.ColorScheme = switch (self.color_scheme) {
.light => .light,
.dark => .dark,
};
Expand Down
Loading