mirror of
https://github.com/ghostty-org/ghostty.git
synced 2025-07-14 15:56:13 +03:00
Merge pull request #2774 from ghostty-org/push-qtvoskmxulyu
App applies conditional state, supports theme setting
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