From ba94cdf711caf95d8e7c024c1737b909171338b0 Mon Sep 17 00:00:00 2001 From: UnsaltedScholar Date: Wed, 5 Aug 2026 23:35:03 -0400 Subject: [PATCH 1/3] Use performAllAction for color scheme reports --- src/App.zig | 10 ++++++++++ src/Surface.zig | 12 +++++++++--- src/apprt/structs.zig | 2 +- src/input/Binding.zig | 4 ++++ src/input/command.zig | 1 + src/termio/Options.zig | 4 ++++ src/termio/Termio.zig | 13 +++++++++---- 7 files changed, 38 insertions(+), 8 deletions(-) diff --git a/src/App.zig b/src/App.zig index cf7316e8c4..d7f48742bf 100644 --- a/src/App.zig +++ b/src/App.zig @@ -60,6 +60,11 @@ font_grid_set: font.SharedGridSet, last_notification_time: ?std.Io.Timestamp = null, last_notification_digest: u64 = 0, +/// The current system color scheme. This is kept separate from conditional +/// configuration state so mode 2031 remains correct for non-conditional +/// themes. +system_color_scheme: std.atomic.Value(apprt.ColorScheme) = .init(.light), + /// The conditional state of the configuration. See the equivalent field /// in the Surface struct for more information. In this case, this applies /// to the app-level config and as a default for new surfaces. @@ -392,6 +397,11 @@ pub fn colorSchemeEvent( rt_app: *apprt.App, scheme: apprt.ColorScheme, ) !void { + // Cache the system color scheme and notify every surface when it changes. + if (self.system_color_scheme.swap(scheme, .monotonic) != scheme) { + try self.performAllAction(rt_app, .system_color_scheme_changed); + } + const new_scheme: configpkg.ConditionalState.Theme = switch (scheme) { .light => .light, .dark => .dark, diff --git a/src/Surface.zig b/src/Surface.zig index 3002e9f580..91f81a39c2 100644 --- a/src/Surface.zig +++ b/src/Surface.zig @@ -678,6 +678,7 @@ pub fn init( .size = size, .full_config = config, .config = try termio.Termio.DerivedConfig.init(alloc, config), + .system_color_scheme = &app.system_color_scheme, .backend = .{ .exec = io_exec }, .mailbox = io_mailbox, .renderer_state = &self.renderer_state, @@ -4756,9 +4757,6 @@ 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; self.notifyConfigConditionalState(); - - // If mode 2031 is on, then we report the change live. - self.queueIo(.{ .color_scheme_report = .{ .force = false } }, .unlocked); } pub fn posToViewport(self: Surface, xpos: f64, ypos: f64) terminal.point.Coordinate { @@ -4845,6 +4843,14 @@ pub fn performBindingAction(self: *Surface, action: input.Binding.Action) !bool } switch (action.scoped(.surface).?) { + .system_color_scheme_changed => { + // Termio suppresses this report unless mode 2031 is enabled. + self.queueIo( + .{ .color_scheme_report = .{ .force = false } }, + .unlocked, + ); + }, + .csi, .esc => |data| { // We need to send the CSI/ESC sequence as a single write request. // If you split it across two then the shell can interpret it diff --git a/src/apprt/structs.zig b/src/apprt/structs.zig index 2c37dbd5ee..942d6d1453 100644 --- a/src/apprt/structs.zig +++ b/src/apprt/structs.zig @@ -93,7 +93,7 @@ pub const ClipboardRequest = union(ClipboardRequestType) { }; /// The color scheme in use (light vs dark). -pub const ColorScheme = enum(u2) { +pub const ColorScheme = enum(u8) { light = 0, dark = 1, }; diff --git a/src/input/Binding.zig b/src/input/Binding.zig index a2bae642a5..f6658420b3 100644 --- a/src/input/Binding.zig +++ b/src/input/Binding.zig @@ -949,6 +949,9 @@ pub const Action = union(enum) { /// this will report performable as false. deactivate_all_key_tables, + /// Notify every terminal that the system color scheme changed. + system_color_scheme_changed, + /// Quit Ghostty. quit, @@ -1365,6 +1368,7 @@ pub const Action = union(enum) { .start_search, .end_search, .reset, + .system_color_scheme_changed, .copy_to_clipboard, .copy_url_to_clipboard, .copy_title_to_clipboard, diff --git a/src/input/command.zig b/src/input/command.zig index 6560148bce..fcdb9a083f 100644 --- a/src/input/command.zig +++ b/src/input/command.zig @@ -734,6 +734,7 @@ fn actionCommands(action: Action.Key) []const Command { => comptime &.{}, // No commands for obvious reasons + .system_color_scheme_changed, .ignore, .unbind, => comptime &.{}, diff --git a/src/termio/Options.zig b/src/termio/Options.zig index a6bf8c4d44..e557f474c8 100644 --- a/src/termio/Options.zig +++ b/src/termio/Options.zig @@ -1,5 +1,6 @@ //! The options that are used to configure a terminal IO implementation. +const std = @import("std"); const xev = @import("../global.zig").xev; const apprt = @import("../apprt.zig"); const renderer = @import("../renderer.zig"); @@ -17,6 +18,9 @@ full_config: *const Config, /// The derived configuration for this termio implementation. config: termio.Termio.DerivedConfig, +/// The system color scheme used for mode 2031 reports. +system_color_scheme: *const std.atomic.Value(apprt.ColorScheme), + /// The backend for termio that implements where reads/writes are sourced. backend: termio.Backend, diff --git a/src/termio/Termio.zig b/src/termio/Termio.zig index 7d6e179d82..9253fed94a 100644 --- a/src/termio/Termio.zig +++ b/src/termio/Termio.zig @@ -37,6 +37,9 @@ backend: termio.Backend, /// The derived configuration for this termio implementation. config: DerivedConfig, +/// The system color scheme used for mode 2031 reports. +system_color_scheme: *const std.atomic.Value(apprt.ColorScheme), + /// 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. @@ -300,6 +303,7 @@ pub fn init(self: *Termio, alloc: Allocator, opts: termio.Options) !void { .alloc = alloc, .terminal = term, .config = opts.config, + .system_color_scheme = opts.system_color_scheme, .renderer_state = opts.renderer_state, .renderer_wakeup = opts.renderer_wakeup, .renderer_mailbox = opts.renderer_mailbox, @@ -715,10 +719,11 @@ 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) { - .light => .light, - .dark => .dark, - }; + const scheme: terminalpkg.device_status.ColorScheme = + switch (self.system_color_scheme.load(.monotonic)) { + .light => .light, + .dark => .dark, + }; var buf: [terminalpkg.device_status.max_color_scheme_report_encode_size]u8 = undefined; var writer: std.Io.Writer = .fixed(&buf); From 448c9d408c4771b40a17c972dc27681cea250c39 Mon Sep 17 00:00:00 2001 From: UnsaltedScholar Date: Wed, 5 Aug 2026 23:36:40 -0400 Subject: [PATCH 2/3] Update GTK window theme on config reload --- src/apprt/gtk/class/application.zig | 23 +++++++++++++++++------ 1 file changed, 17 insertions(+), 6 deletions(-) diff --git a/src/apprt/gtk/class/application.zig b/src/apprt/gtk/class/application.zig index 8cc5918ebb..e7c9c56ac0 100644 --- a/src/apprt/gtk/class/application.zig +++ b/src/apprt/gtk/class/application.zig @@ -1280,6 +1280,10 @@ pub const Application = extern struct { const priv = self.private(); priv.config.unref(); priv.config = config.ref(); + + // Apply GTK appearance settings from the new config. + self.syncStyleManager(); + self.as(gobject.Object).notifyByPspec(properties.config.impl.param_spec); // Show our errors if we have any @@ -1421,15 +1425,12 @@ pub const Application = extern struct { } } - /// Setup the style manager on startup. The primary task here is to - /// setup our initial light/dark mode based on the configuration and - /// setup listeners for changes to the style manager. - fn startupStyleManager(self: *Self) void { + /// Sync the configured window theme. + fn syncStyleManager(self: *Self) void { const priv = self.private(); const config = priv.config.get(); - - // Setup our initial light/dark const style = self.as(adw.Application).getStyleManager(); + style.setColorScheme(switch (config.@"window-theme") { .auto, .ghostty => auto: { const lum = config.background.toTerminalRGB().perceivedLuminance(); @@ -1442,6 +1443,16 @@ pub const Application = extern struct { .dark => .force_dark, .light => .force_light, }); + } + + /// Setup the style manager on startup. The primary task here is to + /// setup our initial light/dark mode based on the configuration and + /// setup listeners for changes to the style manager. + fn startupStyleManager(self: *Self) void { + const style = self.as(adw.Application).getStyleManager(); + + // Setup our initial light/dark. + self.syncStyleManager(); // Setup color change notifications _ = gobject.Object.signals.notify.connect( From 30a0ea804b748337e8e3f1152366b10921d64ef1 Mon Sep 17 00:00:00 2001 From: UnsaltedScholar Date: Wed, 5 Aug 2026 23:39:19 -0400 Subject: [PATCH 3/3] Prevent duplicate config reload toast on theme change --- src/apprt/gtk/class/application.zig | 15 +++++++++++++++ src/apprt/gtk/class/window.zig | 10 ++++++---- 2 files changed, 21 insertions(+), 4 deletions(-) diff --git a/src/apprt/gtk/class/application.zig b/src/apprt/gtk/class/application.zig index e7c9c56ac0..93f52e6064 100644 --- a/src/apprt/gtk/class/application.zig +++ b/src/apprt/gtk/class/application.zig @@ -193,6 +193,9 @@ pub const Application = extern struct { /// only be set by the main loop thread. running: bool = false, + /// Whether the current configuration reload is a soft reload. + config_reload_soft: bool = false, + /// The timer used to quit the application after the last window is /// closed. Even if there is no quit delay set, this is the state /// used to determine to close the app. @@ -243,6 +246,11 @@ pub const Application = extern struct { return gobject.ext.cast(Self, app).?; } + /// Returns whether the current configuration reload is soft. + pub fn configReloadIsSoft(self: *Self) bool { + return self.private().config_reload_soft; + } + /// Creates a new Application instance. /// /// This does a lot more work than a typical class instantiation, @@ -2732,6 +2740,13 @@ const Action = struct { }; defer config.unref(); + // Track whether this config propagation came from an internal soft + // reload so windows can avoid showing a duplicate reload toast. + const priv = self.private(); + const previous_soft = priv.config_reload_soft; + priv.config_reload_soft = opts.soft; + defer priv.config_reload_soft = previous_soft; + // Update the proper target. This will trigger a `config_change` // apprt action which will propagate the config properly to our // property system. diff --git a/src/apprt/gtk/class/window.zig b/src/apprt/gtk/class/window.zig index 3e127308f2..3de7550686 100644 --- a/src/apprt/gtk/class/window.zig +++ b/src/apprt/gtk/class/window.zig @@ -1119,10 +1119,12 @@ pub const Window = extern struct { self: *Self, ) callconv(.c) void { const priv = self.private(); - if (priv.config) |config_obj| { - const config = config_obj.get(); - if (config.@"app-notifications".@"config-reload") { - self.addToast(i18n._("Reloaded the configuration")); + if (!Application.default().configReloadIsSoft()) { + if (priv.config) |config_obj| { + const config = config_obj.get(); + if (config.@"app-notifications".@"config-reload") { + self.addToast(i18n._("Reloaded the configuration")); + } } }