From b7f1eaa14568bab988ea135dec98dc005b88ddbf Mon Sep 17 00:00:00 2001 From: Mitchell Hashimoto Date: Tue, 19 Nov 2024 15:35:42 -0800 Subject: [PATCH] apprt: action to change conditional state, implement for embedded --- include/ghostty.h | 1 + src/Surface.zig | 51 +++++++++++++++++++++++++++---------- src/apprt/action.zig | 8 ++++++ src/apprt/embedded.zig | 57 ++++++++++++++++++++++++++++++++++-------- src/apprt/glfw.zig | 1 + src/apprt/gtk/App.zig | 1 + src/config.zig | 1 + src/config/Config.zig | 2 +- 8 files changed, 97 insertions(+), 25 deletions(-) diff --git a/include/ghostty.h b/include/ghostty.h index 41e3b3fe2..d0426e995 100644 --- a/include/ghostty.h +++ b/include/ghostty.h @@ -567,6 +567,7 @@ typedef enum { GHOSTTY_ACTION_SECURE_INPUT, GHOSTTY_ACTION_KEY_SEQUENCE, GHOSTTY_ACTION_COLOR_CHANGE, + GHOSTTY_ACTION_CONFIG_CHANGE_CONDITIONAL_STATE, } ghostty_action_tag_e; typedef union { diff --git a/src/Surface.zig b/src/Surface.zig index fbb589638..27a3fb5a8 100644 --- a/src/Surface.zig +++ b/src/Surface.zig @@ -94,11 +94,6 @@ keyboard: Keyboard, /// less important. pressed_key: ?input.KeyEvent = null, -/// The current color scheme of the GUI element containing this surface. -/// This will default to light until the apprt sends us the actual color -/// scheme. This is used by mode 3031 and CSI 996 n. -color_scheme: apprt.ColorScheme = .light, - /// The hash value of the last keybinding trigger that we performed. This /// is only set if the last key input matched a keybinding, consumed it, /// and performed it. This is used to prevent sending release/repeat events @@ -121,6 +116,12 @@ size: renderer.Size, /// the lifetime of. This makes updating config at runtime easier. config: DerivedConfig, +/// The conditional state of the configuration. This can affect +/// how certain configurations take effect such as light/dark mode. +/// This is managed completely by Ghostty core but an apprt action +/// is sent whenever this changes. +config_conditional_state: configpkg.ConditionalState, + /// This is set to true if our IO thread notifies us our child exited. /// This is used to determine if we need to confirm, hold open, etc. child_exited: bool = false, @@ -480,6 +481,7 @@ pub fn init( .io_thr = undefined, .size = size, .config = derived_config, + .config_conditional_state = .{}, }; // The command we're going to execute @@ -777,7 +779,7 @@ pub fn needsConfirmQuit(self: *Surface) bool { /// surface. pub fn handleMessage(self: *Surface, msg: Message) !void { switch (msg) { - .change_config => |config| try self.changeConfig(config), + .change_config => |config| try self.updateConfig(config), .set_title => |*v| { // We ignore the message in case the title was set via config. @@ -935,7 +937,7 @@ fn passwordInput(self: *Surface, v: bool) !void { /// Sends a DSR response for the current color scheme to the pty. fn reportColorScheme(self: *Surface) !void { - const output = switch (self.color_scheme) { + const output = switch (self.config_conditional_state.theme) { .light => "\x1B[?997;2n", .dark => "\x1B[?997;1n", }; @@ -1058,8 +1060,25 @@ fn updateRendererHealth(self: *Surface, health: renderer.Health) void { }; } -/// Update our configuration at runtime. -fn changeConfig(self: *Surface, config: *const configpkg.Config) !void { +/// This should be called anytime `config_conditional_state` changes +/// so that the apprt can reload the configuration. +fn notifyConfigConditionalState(self: *Surface) void { + self.rt_app.performAction( + .{ .surface = self }, + .config_change_conditional_state, + {}, + ) catch |err| { + log.warn("failed to notify app of config state change err={}", .{err}); + }; +} + +/// Update our configuration at runtime. This can be called by the apprt +/// to set a surface-specific configuration that differs from the app +/// or other surfaces. +pub fn updateConfig( + self: *Surface, + config: *const configpkg.Config, +) !void { // Update our new derived config immediately const derived = DerivedConfig.init(self.alloc, config) catch |err| { // If the derivation fails then we just log and return. We don't @@ -3590,11 +3609,17 @@ pub fn colorSchemeCallback(self: *Surface, scheme: apprt.ColorScheme) !void { crash.sentry.thread_state = self.crashThreadState(); defer crash.sentry.thread_state = null; - // If our scheme didn't change, then we don't do anything. - if (self.color_scheme == scheme) return; + const new_scheme: configpkg.ConditionalState.Theme = switch (scheme) { + .light => .light, + .dark => .dark, + }; - // Set our new scheme - self.color_scheme = scheme; + // If our scheme didn't change, then we don't do anything. + if (self.config_conditional_state.theme == new_scheme) return; + + // 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. const report = report: { diff --git a/src/apprt/action.zig b/src/apprt/action.zig index 1f954c37c..aef6937a8 100644 --- a/src/apprt/action.zig +++ b/src/apprt/action.zig @@ -193,6 +193,13 @@ pub const Action = union(Key) { /// such as OSC 10/11. color_change: ColorChange, + /// The state of conditionals in the configuration has changed, so + /// the configuration should be reloaded. The apprt doesn't need + /// to do a full physical reload; it should call the + /// `changeConditionalState` function and then `updateConfig` + /// on the app or surface. + config_change_conditional_state, + /// Sync with: ghostty_action_tag_e pub const Key = enum(c_int) { new_window, @@ -228,6 +235,7 @@ pub const Action = union(Key) { secure_input, key_sequence, color_change, + config_change_conditional_state, }; /// Sync with: ghostty_action_u diff --git a/src/apprt/embedded.zig b/src/apprt/embedded.zig index 877bddf0d..5288c4a7b 100644 --- a/src/apprt/embedded.zig +++ b/src/apprt/embedded.zig @@ -430,6 +430,28 @@ pub const App = struct { comptime action: apprt.Action.Key, value: apprt.Action.Value(action), ) !void { + // Special case certain actions before they are sent to the + // embedded apprt. + self.performPreAction(target, action, value); + + log.debug("dispatching action target={s} action={} value={}", .{ + @tagName(target), + action, + value, + }); + self.opts.action( + self, + target.cval(), + @unionInit(apprt.Action, @tagName(action), value).cval(), + ); + } + + fn performPreAction( + self: *App, + target: apprt.Target, + comptime action: apprt.Action.Key, + value: apprt.Action.Value(action), + ) void { // Special case certain actions before they are sent to the embedder switch (action) { .set_title => switch (target) { @@ -443,19 +465,32 @@ pub const App = struct { }, }, + .config_change_conditional_state => switch (target) { + .app => {}, + .surface => |surface| action: { + // Build our new configuration. We can free the memory + // immediately after because the surface will derive any + // values it needs to. + var new_config = self.config.changeConditionalState( + surface.config_conditional_state, + ) catch |err| { + // Not a big deal if we error... we just don't update + // the config. We log the error and move on. + log.warn("error changing config conditional state err={}", .{err}); + break :action; + }; + defer new_config.deinit(); + + // Update our surface. + surface.updateConfig(&new_config) catch |err| { + log.warn("error updating surface config for state change err={}", .{err}); + break :action; + }; + }, + }, + else => {}, } - - log.debug("dispatching action target={s} action={} value={}", .{ - @tagName(target), - action, - value, - }); - self.opts.action( - self, - target.cval(), - @unionInit(apprt.Action, @tagName(action), value).cval(), - ); } }; diff --git a/src/apprt/glfw.zig b/src/apprt/glfw.zig index 54c53139c..3c866a1de 100644 --- a/src/apprt/glfw.zig +++ b/src/apprt/glfw.zig @@ -225,6 +225,7 @@ pub const App = struct { .renderer_health, .color_change, .pwd, + .config_change_conditional_state, => log.info("unimplemented action={}", .{action}), } } diff --git a/src/apprt/gtk/App.zig b/src/apprt/gtk/App.zig index b00d65bce..8815ef39f 100644 --- a/src/apprt/gtk/App.zig +++ b/src/apprt/gtk/App.zig @@ -487,6 +487,7 @@ pub fn performAction( .render_inspector, .renderer_health, .color_change, + .config_change_conditional_state, => log.warn("unimplemented action={}", .{action}), } } diff --git a/src/config.zig b/src/config.zig index 02268d4e3..b7e818f8e 100644 --- a/src/config.zig +++ b/src/config.zig @@ -7,6 +7,7 @@ pub const string = @import("config/string.zig"); pub const edit = @import("config/edit.zig"); pub const url = @import("config/url.zig"); +pub const ConditionalState = conditional.State; pub const FileFormatter = formatter.FileFormatter; pub const entryFormatter = formatter.entryFormatter; pub const formatEntry = formatter.formatEntry; diff --git a/src/config/Config.zig b/src/config/Config.zig index fa46ef04d..eb7346c97 100644 --- a/src/config/Config.zig +++ b/src/config/Config.zig @@ -2583,7 +2583,7 @@ pub fn loadRecursiveFiles(self: *Config, alloc_gpa: Allocator) !void { /// deleted a file that was referenced in the configuration, then the /// configuration can still be reloaded. pub fn changeConditionalState( - self: *Config, + self: *const Config, new: conditional.State, ) !Config { // Create our new configuration