mirror of
https://github.com/ghostty-org/ghostty.git
synced 2025-08-02 14:57:31 +03:00
App applies conditional state, supports theme setting
The prior light/dark mode awareness work works on surface-level APIs. As a result, configurations used at the app-level (such as split divider colors, inactive split opacity, etc.) are not aware of the current theme configurations and default to the "light" theme. This commit adds APIs to specify app-level color scheme changes. This changes the configuration for the app and sets the default conditional state to use that new theme. This latter point makes it so that future surfaces use the correct theme on load rather than requiring some apprt event loop ticks. Some users have already reported a short "flicker" to load the correct theme, so this should help alleviate that.
This commit is contained in:
@ -675,6 +675,7 @@ void ghostty_app_open_config(ghostty_app_t);
|
|||||||
void ghostty_app_update_config(ghostty_app_t, ghostty_config_t);
|
void ghostty_app_update_config(ghostty_app_t, ghostty_config_t);
|
||||||
bool ghostty_app_needs_confirm_quit(ghostty_app_t);
|
bool ghostty_app_needs_confirm_quit(ghostty_app_t);
|
||||||
bool ghostty_app_has_global_keybinds(ghostty_app_t);
|
bool ghostty_app_has_global_keybinds(ghostty_app_t);
|
||||||
|
void ghostty_app_set_color_scheme(ghostty_app_t, ghostty_color_scheme_e);
|
||||||
|
|
||||||
ghostty_surface_config_s ghostty_surface_config_new();
|
ghostty_surface_config_s ghostty_surface_config_new();
|
||||||
|
|
||||||
|
@ -95,6 +95,9 @@ class AppDelegate: NSObject,
|
|||||||
/// makes our logic very easy.
|
/// makes our logic very easy.
|
||||||
private var isVisible: Bool = true
|
private var isVisible: Bool = true
|
||||||
|
|
||||||
|
/// The observer for the app appearance.
|
||||||
|
private var appearanceObserver: NSKeyValueObservation? = nil
|
||||||
|
|
||||||
override init() {
|
override init() {
|
||||||
terminalManager = TerminalManager(ghostty)
|
terminalManager = TerminalManager(ghostty)
|
||||||
updaterController = SPUStandardUpdaterController(
|
updaterController = SPUStandardUpdaterController(
|
||||||
@ -187,6 +190,23 @@ class AppDelegate: NSObject,
|
|||||||
)
|
)
|
||||||
])
|
])
|
||||||
center.delegate = self
|
center.delegate = self
|
||||||
|
|
||||||
|
// Observe our appearance so we can report the correct value to libghostty.
|
||||||
|
self.appearanceObserver = NSApplication.shared.observe(
|
||||||
|
\.effectiveAppearance,
|
||||||
|
options: [.new, .initial]
|
||||||
|
) { _, change in
|
||||||
|
guard let appearance = change.newValue else { return }
|
||||||
|
guard let app = self.ghostty.app else { return }
|
||||||
|
let scheme: ghostty_color_scheme_e
|
||||||
|
if (appearance.isDark) {
|
||||||
|
scheme = GHOSTTY_COLOR_SCHEME_DARK
|
||||||
|
} else {
|
||||||
|
scheme = GHOSTTY_COLOR_SCHEME_LIGHT
|
||||||
|
}
|
||||||
|
|
||||||
|
ghostty_app_set_color_scheme(app, scheme)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func applicationDidBecomeActive(_ notification: Notification) {
|
func applicationDidBecomeActive(_ notification: Notification) {
|
||||||
|
@ -1212,6 +1212,7 @@ extension Ghostty {
|
|||||||
|
|
||||||
switch (target.tag) {
|
switch (target.tag) {
|
||||||
case GHOSTTY_TARGET_APP:
|
case GHOSTTY_TARGET_APP:
|
||||||
|
// Notify the world that the app config changed
|
||||||
NotificationCenter.default.post(
|
NotificationCenter.default.post(
|
||||||
name: .ghosttyConfigDidChange,
|
name: .ghosttyConfigDidChange,
|
||||||
object: nil,
|
object: nil,
|
||||||
@ -1219,6 +1220,14 @@ extension Ghostty {
|
|||||||
SwiftUI.Notification.Name.GhosttyConfigChangeKey: config,
|
SwiftUI.Notification.Name.GhosttyConfigChangeKey: config,
|
||||||
]
|
]
|
||||||
)
|
)
|
||||||
|
|
||||||
|
// We also REPLACE our app-level config when this happens. This lets
|
||||||
|
// all the various things that depend on this but are still theme specific
|
||||||
|
// such as split border color work.
|
||||||
|
guard let app_ud = ghostty_app_userdata(app) else { return }
|
||||||
|
let ghostty = Unmanaged<App>.fromOpaque(app_ud).takeUnretainedValue()
|
||||||
|
ghostty.config = config
|
||||||
|
|
||||||
return
|
return
|
||||||
|
|
||||||
case GHOSTTY_TARGET_SURFACE:
|
case GHOSTTY_TARGET_SURFACE:
|
||||||
|
48
src/App.zig
48
src/App.zig
@ -67,6 +67,11 @@ font_grid_set: font.SharedGridSet,
|
|||||||
last_notification_time: ?std.time.Instant = null,
|
last_notification_time: ?std.time.Instant = null,
|
||||||
last_notification_digest: u64 = 0,
|
last_notification_digest: u64 = 0,
|
||||||
|
|
||||||
|
/// 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.
|
||||||
|
config_conditional_state: configpkg.ConditionalState,
|
||||||
|
|
||||||
/// Set to false once we've created at least one surface. This
|
/// Set to false once we've created at least one surface. This
|
||||||
/// never goes true again. This can be used by surfaces to determine
|
/// never goes true again. This can be used by surfaces to determine
|
||||||
/// if they are the first surface.
|
/// if they are the first surface.
|
||||||
@ -95,6 +100,7 @@ pub fn create(
|
|||||||
.mailbox = .{},
|
.mailbox = .{},
|
||||||
.quit = false,
|
.quit = false,
|
||||||
.font_grid_set = font_grid_set,
|
.font_grid_set = font_grid_set,
|
||||||
|
.config_conditional_state = .{},
|
||||||
};
|
};
|
||||||
errdefer app.surfaces.deinit(alloc);
|
errdefer app.surfaces.deinit(alloc);
|
||||||
|
|
||||||
@ -154,11 +160,24 @@ pub fn updateConfig(self: *App, rt_app: *apprt.App, config: *const Config) !void
|
|||||||
try surface.core_surface.handleMessage(.{ .change_config = config });
|
try surface.core_surface.handleMessage(.{ .change_config = config });
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Apply our conditional state. If we fail to apply the conditional state
|
||||||
|
// then we log and attempt to move forward with the old config.
|
||||||
|
// We only apply this to the app-level config because the surface
|
||||||
|
// config applies its own conditional state.
|
||||||
|
var applied_: ?configpkg.Config = config.changeConditionalState(
|
||||||
|
self.config_conditional_state,
|
||||||
|
) catch |err| err: {
|
||||||
|
log.warn("failed to apply conditional state to config err={}", .{err});
|
||||||
|
break :err null;
|
||||||
|
};
|
||||||
|
defer if (applied_) |*c| c.deinit();
|
||||||
|
const applied: *const configpkg.Config = if (applied_) |*c| c else config;
|
||||||
|
|
||||||
// Notify the apprt that the app has changed configuration.
|
// Notify the apprt that the app has changed configuration.
|
||||||
try rt_app.performAction(
|
try rt_app.performAction(
|
||||||
.app,
|
.app,
|
||||||
.config_change,
|
.config_change,
|
||||||
.{ .config = config },
|
.{ .config = applied },
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -380,6 +399,33 @@ pub fn keyEvent(
|
|||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Call to notify Ghostty that the color scheme for the app has changed.
|
||||||
|
/// "Color scheme" in this case refers to system themes such as "light/dark".
|
||||||
|
pub fn colorSchemeEvent(
|
||||||
|
self: *App,
|
||||||
|
rt_app: *apprt.App,
|
||||||
|
scheme: apprt.ColorScheme,
|
||||||
|
) !void {
|
||||||
|
const new_scheme: configpkg.ConditionalState.Theme = switch (scheme) {
|
||||||
|
.light => .light,
|
||||||
|
.dark => .dark,
|
||||||
|
};
|
||||||
|
|
||||||
|
// 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;
|
||||||
|
|
||||||
|
// Request our configuration be reloaded because the new scheme may
|
||||||
|
// impact the colors of the app.
|
||||||
|
try rt_app.performAction(
|
||||||
|
.app,
|
||||||
|
.reload_config,
|
||||||
|
.{ .soft = true },
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
/// Perform a binding action. This only accepts actions that are scoped
|
/// Perform a binding action. This only accepts actions that are scoped
|
||||||
/// to the app. Callers can use performAllAction to perform any action
|
/// to the app. Callers can use performAllAction to perform any action
|
||||||
/// and any non-app-scoped actions will be performed on all surfaces.
|
/// and any non-app-scoped actions will be performed on all surfaces.
|
||||||
|
@ -359,11 +359,25 @@ const DerivedConfig = struct {
|
|||||||
pub fn init(
|
pub fn init(
|
||||||
self: *Surface,
|
self: *Surface,
|
||||||
alloc: Allocator,
|
alloc: Allocator,
|
||||||
config: *const configpkg.Config,
|
config_original: *const configpkg.Config,
|
||||||
app: *App,
|
app: *App,
|
||||||
rt_app: *apprt.runtime.App,
|
rt_app: *apprt.runtime.App,
|
||||||
rt_surface: *apprt.runtime.Surface,
|
rt_surface: *apprt.runtime.Surface,
|
||||||
) !void {
|
) !void {
|
||||||
|
// Apply our conditional state. If we fail to apply the conditional state
|
||||||
|
// then we log and attempt to move forward with the old config.
|
||||||
|
var config_: ?configpkg.Config = config_original.changeConditionalState(
|
||||||
|
app.config_conditional_state,
|
||||||
|
) catch |err| err: {
|
||||||
|
log.warn("failed to apply conditional state to config err={}", .{err});
|
||||||
|
break :err null;
|
||||||
|
};
|
||||||
|
defer if (config_) |*c| c.deinit();
|
||||||
|
|
||||||
|
// We want a config pointer for everything so we get that either
|
||||||
|
// based on our conditional state or the original config.
|
||||||
|
const config: *const configpkg.Config = if (config_) |*c| c else config_original;
|
||||||
|
|
||||||
// Get our configuration
|
// Get our configuration
|
||||||
var derived_config = try DerivedConfig.init(alloc, config);
|
var derived_config = try DerivedConfig.init(alloc, config);
|
||||||
errdefer derived_config.deinit();
|
errdefer derived_config.deinit();
|
||||||
@ -481,7 +495,10 @@ pub fn init(
|
|||||||
.io_thr = undefined,
|
.io_thr = undefined,
|
||||||
.size = size,
|
.size = size,
|
||||||
.config = derived_config,
|
.config = derived_config,
|
||||||
.config_conditional_state = .{},
|
|
||||||
|
// Our conditional state is initialized to the app state. This
|
||||||
|
// lets us get the most likely correct color theme and so on.
|
||||||
|
.config_conditional_state = app.config_conditional_state,
|
||||||
};
|
};
|
||||||
|
|
||||||
// The command we're going to execute
|
// The command we're going to execute
|
||||||
|
@ -1357,6 +1357,22 @@ pub const CAPI = struct {
|
|||||||
return v.hasGlobalKeybinds();
|
return v.hasGlobalKeybinds();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Update the color scheme of the app.
|
||||||
|
export fn ghostty_app_set_color_scheme(v: *App, scheme_raw: c_int) void {
|
||||||
|
const scheme = std.meta.intToEnum(apprt.ColorScheme, scheme_raw) catch {
|
||||||
|
log.warn(
|
||||||
|
"invalid color scheme to ghostty_surface_set_color_scheme value={}",
|
||||||
|
.{scheme_raw},
|
||||||
|
);
|
||||||
|
return;
|
||||||
|
};
|
||||||
|
|
||||||
|
v.core_app.colorSchemeEvent(v, scheme) catch |err| {
|
||||||
|
log.err("error setting color scheme err={}", .{err});
|
||||||
|
return;
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
/// Returns initial surface options.
|
/// Returns initial surface options.
|
||||||
export fn ghostty_surface_config_new() apprt.Surface.Options {
|
export fn ghostty_surface_config_new() apprt.Surface.Options {
|
||||||
return .{};
|
return .{};
|
||||||
|
Reference in New Issue
Block a user