termio: report color scheme synchronously

The reporting of color scheme was handled asynchronously by queuing a
handler in the surface. This could lead to race conditions where the
DSR is reported after subsequent VT sequences.

Fixes #5922
This commit is contained in:
Tobias Kohlbau
2025-11-25 22:19:57 +01:00
parent 250877eff6
commit 836d794b9e
6 changed files with 32 additions and 31 deletions

View File

@@ -1073,8 +1073,6 @@ pub fn handleMessage(self: *Surface, msg: Message) !void {
.scrollbar => |scrollbar| self.updateScrollbar(scrollbar),
.report_color_scheme => |force| self.reportColorScheme(force),
.present_surface => try self.presentSurface(),
.password_input => |v| try self.passwordInput(v),
@@ -1386,26 +1384,6 @@ fn passwordInput(self: *Surface, v: bool) !void {
try self.queueRender();
}
/// Sends a DSR response for the current color scheme to the pty. If
/// force is false then we only send the response if the terminal mode
/// 2031 is enabled.
fn reportColorScheme(self: *Surface, force: bool) void {
if (!force) {
self.renderer_state.mutex.lock();
defer self.renderer_state.mutex.unlock();
if (!self.renderer_state.terminal.modes.get(.report_color_scheme)) {
return;
}
}
const output = switch (self.config_conditional_state.theme) {
.light => "\x1B[?997;2n",
.dark => "\x1B[?997;1n",
};
self.queueIo(.{ .write_stable = output }, .unlocked);
}
fn searchCallback(event: terminal.search.Thread.Event, ud: ?*anyopaque) void {
// IMPORTANT: This function is run on the SEARCH THREAD! It is NOT SAFE
// to access anything other than values that never change on the surface.
@@ -5039,7 +5017,7 @@ pub fn colorSchemeCallback(self: *Surface, scheme: apprt.ColorScheme) !void {
self.notifyConfigConditionalState();
// If mode 2031 is on, then we report the change live.
self.reportColorScheme(false);
self.queueIo(.{ .color_scheme_report = .{ .force = false } }, .unlocked);
}
pub fn posToViewport(self: Surface, xpos: f64, ypos: f64) terminal.point.Coordinate {

View File

@@ -63,11 +63,6 @@ pub const Message = union(enum) {
/// Health status change for the renderer.
renderer_health: renderer.Health,
/// Report the color scheme. The bool parameter is whether to force or not.
/// If force is true, the color scheme should be reported even if mode
/// 2031 is not set.
report_color_scheme: bool,
/// Tell the surface to present itself to the user. This may require raising
/// a window and switching tabs.
present_surface: void,

View File

@@ -165,6 +165,7 @@ 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,
@@ -185,6 +186,7 @@ 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).
@@ -712,6 +714,25 @@ fn processOutputLocked(self: *Termio, buf: []const u8) void {
}
}
/// Sends a DSR response for the current color scheme to the pty.
pub fn colorSchemeReport(self: *Termio, td: *ThreadData, force: bool) !void {
self.renderer_state.mutex.lock();
defer self.renderer_state.mutex.unlock();
try self.colorSchemeReportLocked(td, force);
}
pub fn colorSchemeReportLocked(self: *Termio, td: *ThreadData, force: bool) !void {
if (!force and !self.renderer_state.terminal.modes.get(.report_color_scheme)) {
return;
}
const output = switch (self.config.conditional_state.theme) {
.light => "\x1B[?997;2n",
.dark => "\x1B[?997;1n",
};
try self.queueWrite(td, output, false);
}
/// ThreadData is the data created and stored in the termio thread
/// when the thread is started and destroyed when the thread is
/// stopped.

View File

@@ -311,6 +311,7 @@ fn drainMailbox(
log.debug("mailbox message={s}", .{@tagName(message)});
switch (message) {
.color_scheme_report => |v| try io.colorSchemeReport(data, v.force),
.crash => @panic("crash request, crashing intentionally"),
.change_config => |config| {
defer config.alloc.destroy(config.ptr);

View File

@@ -16,6 +16,12 @@ pub const Message = union(enum) {
/// in the future.
pub const WriteReq = MessageData(u8, 38);
/// Request a color scheme report is sent to the pty.
color_scheme_report: struct {
/// Force write the current color scheme
force: bool,
},
/// Purposely crash the renderer. This is used for testing and debugging.
/// See the "crash" binding action.
crash: void,

View File

@@ -119,7 +119,7 @@ pub const StreamHandler = struct {
};
// The config could have changed any of our colors so update mode 2031
self.surfaceMessageWriter(.{ .report_color_scheme = false });
self.messageWriter(.{ .color_scheme_report = .{ .force = false } });
}
inline fn surfaceMessageWriter(
@@ -871,7 +871,7 @@ pub const StreamHandler = struct {
self.messageWriter(msg);
},
.color_scheme => self.surfaceMessageWriter(.{ .report_color_scheme = true }),
.color_scheme => self.messageWriter(.{ .color_scheme_report = .{ .force = true } }),
}
}
@@ -956,7 +956,7 @@ pub const StreamHandler = struct {
try self.setMouseShape(.text);
// Reset resets our palette so we report it for mode 2031.
self.surfaceMessageWriter(.{ .report_color_scheme = false });
self.messageWriter(.{ .color_scheme_report = .{ .force = false } });
// Clear the progress bar
self.progressReport(.{ .state = .remove });