apprt: switch to reload_config action that calls update_config API

This commit is contained in:
Mitchell Hashimoto
2024-11-22 10:43:51 -08:00
parent 6371af0d8e
commit a191f3c396
6 changed files with 132 additions and 59 deletions

View File

@ -537,6 +537,11 @@ typedef struct {
ghostty_config_t config;
} ghostty_action_config_change_s;
// apprt.action.ReloadConfig
typedef struct {
bool soft;
} ghostty_action_reload_config_s;
// apprt.Action.Key
typedef enum {
GHOSTTY_ACTION_NEW_WINDOW,
@ -573,6 +578,7 @@ typedef enum {
GHOSTTY_ACTION_KEY_SEQUENCE,
GHOSTTY_ACTION_COLOR_CHANGE,
GHOSTTY_ACTION_CONFIG_CHANGE_CONDITIONAL_STATE,
GHOSTTY_ACTION_RELOAD_CONFIG,
GHOSTTY_ACTION_CONFIG_CHANGE,
} ghostty_action_tag_e;
@ -598,6 +604,7 @@ typedef union {
ghostty_action_secure_input_e secure_input;
ghostty_action_key_sequence_s key_sequence;
ghostty_action_color_change_s color_change;
ghostty_action_reload_config_s reload_config;
ghostty_action_config_change_s config_change;
} ghostty_action_u;
@ -607,7 +614,6 @@ typedef struct {
} ghostty_action_s;
typedef void (*ghostty_runtime_wakeup_cb)(void*);
typedef const ghostty_config_t (*ghostty_runtime_reload_config_cb)(void*);
typedef void (*ghostty_runtime_read_clipboard_cb)(void*,
ghostty_clipboard_e,
void*);
@ -630,7 +636,6 @@ typedef struct {
bool supports_selection_clipboard;
ghostty_runtime_wakeup_cb wakeup_cb;
ghostty_runtime_action_cb action_cb;
ghostty_runtime_reload_config_cb reload_config_cb;
ghostty_runtime_read_clipboard_cb read_clipboard_cb;
ghostty_runtime_confirm_read_clipboard_cb confirm_read_clipboard_cb;
ghostty_runtime_write_clipboard_cb write_clipboard_cb;
@ -668,7 +673,7 @@ void ghostty_app_set_focus(ghostty_app_t, bool);
bool ghostty_app_key(ghostty_app_t, ghostty_input_key_s);
void ghostty_app_keyboard_changed(ghostty_app_t);
void ghostty_app_open_config(ghostty_app_t);
void ghostty_app_reload_config(ghostty_app_t);
void ghostty_app_update_config(ghostty_app_t, ghostty_config_t);
bool ghostty_app_needs_confirm_quit(ghostty_app_t);
bool ghostty_app_has_global_keybinds(ghostty_app_t);
@ -679,6 +684,7 @@ void ghostty_surface_free(ghostty_surface_t);
void* ghostty_surface_userdata(ghostty_surface_t);
ghostty_app_t ghostty_surface_app(ghostty_surface_t);
ghostty_surface_config_s ghostty_surface_inherited_config(ghostty_surface_t);
void ghostty_surface_update_config(ghostty_surface_t, ghostty_config_t);
bool ghostty_surface_needs_confirm_quit(ghostty_surface_t);
void ghostty_surface_refresh(ghostty_surface_t);
void ghostty_surface_draw(ghostty_surface_t);

View File

@ -65,7 +65,6 @@ extension Ghostty {
supports_selection_clipboard: false,
wakeup_cb: { userdata in App.wakeup(userdata) },
action_cb: { app, target, action in App.action(app!, target: target, action: action) },
reload_config_cb: { userdata in App.reloadConfig(userdata) },
read_clipboard_cb: { userdata, loc, state in App.readClipboard(userdata, location: loc, state: state) },
confirm_read_clipboard_cb: { userdata, str, state, request in App.confirmReadClipboard(userdata, string: str, state: state, request: request ) },
write_clipboard_cb: { userdata, str, loc, confirm in App.writeClipboard(userdata, string: str, location: loc, confirm: confirm) },
@ -142,9 +141,47 @@ extension Ghostty {
ghostty_app_open_config(app)
}
func reloadConfig() {
/// Reload the configuration.
func reloadConfig(soft: Bool = false) {
guard let app = self.app else { return }
ghostty_app_reload_config(app)
// Soft updates just call with our existing config
if (soft) {
ghostty_app_update_config(app, config.config!)
return
}
// Hard or full updates have to reload the full configuration
let newConfig = Config()
guard newConfig.loaded else {
AppDelegate.logger.warning("failed to reload configuration")
return
}
ghostty_app_update_config(app, newConfig.config!)
// We can only set our config after updating it so that we don't free
// memory that may still be in use
self.config = newConfig
}
func reloadConfig(surface: ghostty_surface_t, soft: Bool = false) {
// Soft updates just call with our existing config
if (soft) {
ghostty_surface_update_config(surface, config.config!)
return
}
// Hard or full updates have to reload the full configuration.
// NOTE: We never set this on self.config because this is a surface-only
// config. We free it after the call.
let newConfig = Config()
guard newConfig.loaded else {
AppDelegate.logger.warning("failed to reload configuration")
return
}
ghostty_surface_update_config(surface, newConfig.config!)
}
/// Request that the given surface is closed. This will trigger the full normal surface close event
@ -237,7 +274,6 @@ extension Ghostty {
static func wakeup(_ userdata: UnsafeMutableRawPointer?) {}
static func action(_ app: ghostty_app_t, target: ghostty_target_s, action: ghostty_action_s) {}
static func reloadConfig(_ userdata: UnsafeMutableRawPointer?) -> ghostty_config_t? { return nil }
static func readClipboard(
_ userdata: UnsafeMutableRawPointer?,
location: ghostty_clipboard_e,
@ -365,21 +401,6 @@ extension Ghostty {
)
}
static func reloadConfig(_ userdata: UnsafeMutableRawPointer?) -> ghostty_config_t? {
let newConfig = Config()
guard newConfig.loaded else {
AppDelegate.logger.warning("failed to reload configuration")
return nil
}
// Assign the new config. This will automatically free the old config.
// It is safe to free the old config from within this function call.
let state = Unmanaged<Self>.fromOpaque(userdata!).takeUnretainedValue()
state.config = newConfig
return newConfig.config
}
static func wakeup(_ userdata: UnsafeMutableRawPointer?) {
let state = Unmanaged<App>.fromOpaque(userdata!).takeUnretainedValue()
@ -514,6 +535,9 @@ extension Ghostty {
case GHOSTTY_ACTION_CONFIG_CHANGE:
configChange(app, target: target, v: action.action.config_change)
case GHOSTTY_ACTION_RELOAD_CONFIG:
configReload(app, target: target, v: action.action.reload_config)
case GHOSTTY_ACTION_COLOR_CHANGE:
colorChange(app, target: target, change: action.action.color_change)
@ -1150,6 +1174,30 @@ extension Ghostty {
}
}
private static func configReload(
_ app: ghostty_app_t,
target: ghostty_target_s,
v: ghostty_action_reload_config_s)
{
logger.info("config reload notification")
guard let app_ud = ghostty_app_userdata(app) else { return }
let ghostty = Unmanaged<App>.fromOpaque(app_ud).takeUnretainedValue()
switch (target.tag) {
case GHOSTTY_TARGET_APP:
ghostty.reloadConfig(soft: v.soft)
return
case GHOSTTY_TARGET_SURFACE:
guard let surface = target.target.surface else { return }
ghostty.reloadConfig(surface: surface, soft: v.soft)
default:
assertionFailure()
}
}
private static func configChange(
_ app: ghostty_app_t,
target: ghostty_target_s,

View File

@ -12,7 +12,8 @@ const apprt = @import("apprt.zig");
const Surface = @import("Surface.zig");
const tracy = @import("tracy");
const input = @import("input.zig");
const Config = @import("config.zig").Config;
const configpkg = @import("config.zig");
const Config = configpkg.Config;
const BlockingQueue = @import("datastruct/main.zig").BlockingQueue;
const renderer = @import("renderer.zig");
const font = @import("font/main.zig");
@ -239,7 +240,6 @@ fn drainMailbox(self: *App, rt_app: *apprt.App) !void {
while (self.mailbox.pop()) |message| {
log.debug("mailbox message={s}", .{@tagName(message)});
switch (message) {
.reload_config => try self.reloadConfig(rt_app),
.open_config => try self.performAction(rt_app, .open_config),
.new_window => |msg| try self.newWindow(rt_app, msg),
.close => |surface| self.closeSurface(surface),
@ -260,14 +260,6 @@ fn drainMailbox(self: *App, rt_app: *apprt.App) !void {
}
}
pub fn reloadConfig(self: *App, rt_app: *apprt.App) !void {
log.debug("reloading configuration", .{});
if (try rt_app.reloadConfig()) |new| {
log.debug("new configuration received, applying", .{});
try self.updateConfig(rt_app, new);
}
}
pub fn closeSurface(self: *App, surface: *Surface) void {
if (!self.hasSurface(surface)) return;
surface.close();
@ -402,7 +394,7 @@ pub fn performAction(
.quit => self.setQuit(),
.new_window => try self.newWindow(rt_app, .{ .parent = null }),
.open_config => try rt_app.performAction(.app, .open_config, {}),
.reload_config => try self.reloadConfig(rt_app),
.reload_config => try rt_app.performAction(.app, .reload_config, .{}),
.close_all_windows => try rt_app.performAction(.app, .close_all_windows, {}),
.toggle_quick_terminal => try rt_app.performAction(.app, .toggle_quick_terminal, {}),
.toggle_visibility => try rt_app.performAction(.app, .toggle_visibility, {}),
@ -462,10 +454,6 @@ fn hasSurface(self: *const App, surface: *const Surface) bool {
/// The message types that can be sent to the app thread.
pub const Message = union(enum) {
/// Reload the configuration for the entire app and propagate it to
/// all the active surfaces.
reload_config: void,
// Open the configuration file
open_config: void,

View File

@ -1065,8 +1065,8 @@ fn updateRendererHealth(self: *Surface, health: renderer.Health) void {
fn notifyConfigConditionalState(self: *Surface) void {
self.rt_app.performAction(
.{ .surface = self },
.config_change_conditional_state,
{},
.reload_config,
.{ .soft = true },
) catch |err| {
log.warn("failed to notify app of config state change err={}", .{err});
};
@ -1077,8 +1077,22 @@ fn notifyConfigConditionalState(self: *Surface) void {
/// or other surfaces.
pub fn updateConfig(
self: *Surface,
config: *const configpkg.Config,
original: *const configpkg.Config,
) !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 = original.changeConditionalState(
self.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 original;
// 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

View File

@ -201,6 +201,15 @@ pub const Action = union(Key) {
/// on the app or surface.
config_change_conditional_state,
/// A request to reload the configuration. The reload request can be
/// from a user or for some internal reason. The reload request may
/// request it is a soft reload or a full reload. See the struct for
/// more documentation.
///
/// The configuration should be passed to updateConfig either at the
/// app or surface level depending on the target.
reload_config: ReloadConfig,
/// The configuration has changed. The value is a pointer to the new
/// configuration. The pointer is only valid for the duration of the
/// action and should not be stored.
@ -251,6 +260,7 @@ pub const Action = union(Key) {
key_sequence,
color_change,
config_change_conditional_state,
reload_config,
config_change,
};
@ -514,6 +524,13 @@ pub const ColorKind = enum(c_int) {
_,
};
pub const ReloadConfig = extern struct {
/// A soft reload means that the configuration doesn't need to be
/// read off disk, but libghostty needs the full config again so call
/// updateConfig with it.
soft: bool = false,
};
pub const ConfigChange = struct {
config: *const configpkg.Config,

View File

@ -47,11 +47,6 @@ pub const App = struct {
/// Callback called to handle an action.
action: *const fn (*App, apprt.Target.C, apprt.Action.C) callconv(.C) void,
/// Reload the configuration and return the new configuration.
/// The old configuration can be freed immediately when this is
/// called.
reload_config: *const fn (AppUD) callconv(.C) ?*const Config,
/// Read the clipboard value. The return value must be preserved
/// by the host until the next call. If there is no valid clipboard
/// value then this should return null.
@ -375,16 +370,6 @@ pub const App = struct {
}
}
pub fn reloadConfig(self: *App) !?*const Config {
// Reload
if (self.opts.reload_config(self.opts.userdata)) |new| {
self.config = new;
return self.config;
}
return null;
}
pub fn wakeup(self: App) void {
self.opts.wakeup(self.opts.userdata);
}
@ -1374,10 +1359,14 @@ pub const CAPI = struct {
};
}
/// Reload the configuration.
export fn ghostty_app_reload_config(v: *App) void {
_ = v.core_app.reloadConfig(v) catch |err| {
log.err("error reloading config err={}", .{err});
/// Update the configuration to the provided config. This will propagate
/// to all surfaces as well.
export fn ghostty_app_update_config(
v: *App,
config: *const Config,
) void {
v.core_app.updateConfig(v, config) catch |err| {
log.err("error updating config err={}", .{err});
return;
};
}
@ -1434,6 +1423,17 @@ pub const CAPI = struct {
return surface.newSurfaceOptions();
}
/// Update the configuration to the provided config for only this surface.
export fn ghostty_surface_update_config(
surface: *Surface,
config: *const Config,
) void {
surface.core_surface.updateConfig(config) catch |err| {
log.err("error updating config err={}", .{err});
return;
};
}
/// Returns true if the surface needs to confirm quitting.
export fn ghostty_surface_needs_confirm_quit(surface: *Surface) bool {
return surface.core_surface.needsConfirmQuit();