Add settings shortcut on MacOS

- Settings shortcut opens the config file in the default editor.

Signed-off-by: Borja Clemente <borja.clemente@gmail.com>
This commit is contained in:
Borja Clemente
2023-12-17 14:22:38 +01:00
parent 5eea79ad0d
commit 646e3c365c
10 changed files with 104 additions and 2 deletions

View File

@ -353,6 +353,7 @@ typedef struct {
} ghostty_surface_config_s;
typedef void (*ghostty_runtime_wakeup_cb)(void *);
typedef void (*ghostty_runtime_open_config_cb)(void *);
typedef const ghostty_config_t (*ghostty_runtime_reload_config_cb)(void *);
typedef void (*ghostty_runtime_set_title_cb)(void *, const char *);
typedef void (*ghostty_runtime_set_mouse_shape_cb)(void *, ghostty_mouse_shape_e);
@ -380,6 +381,7 @@ typedef struct {
void *userdata;
bool supports_selection_clipboard;
ghostty_runtime_wakeup_cb wakeup_cb;
ghostty_runtime_open_config_cb open_config_cb;
ghostty_runtime_reload_config_cb reload_config_cb;
ghostty_runtime_set_title_cb set_title_cb;
ghostty_runtime_set_mouse_shape_cb set_mouse_shape_cb;
@ -422,12 +424,14 @@ bool ghostty_config_get(ghostty_config_t, void *, const char *, uintptr_t);
ghostty_input_trigger_s ghostty_config_trigger(ghostty_config_t, const char *, uintptr_t);
uint32_t ghostty_config_errors_count(ghostty_config_t);
ghostty_error_s ghostty_config_get_error(ghostty_config_t, uint32_t);
void ghostty_config_open();
ghostty_app_t ghostty_app_new(const ghostty_runtime_config_s *, ghostty_config_t);
void ghostty_app_free(ghostty_app_t);
bool ghostty_app_tick(ghostty_app_t);
void *ghostty_app_userdata(ghostty_app_t);
void ghostty_app_keyboard_changed(ghostty_app_t);
void ghostty_app_open_config(ghostty_app_t);
void ghostty_app_reload_config(ghostty_app_t);
bool ghostty_app_needs_confirm_quit(ghostty_app_t);

View File

@ -12,6 +12,7 @@ class AppDelegate: NSObject, ObservableObject, NSApplicationDelegate, UNUserNoti
)
/// Various menu items so that we can programmatically sync the keyboard shortcut with the Ghostty config.
@IBOutlet private var menuOpenConfig: NSMenuItem?
@IBOutlet private var menuReloadConfig: NSMenuItem?
@IBOutlet private var menuQuit: NSMenuItem?
@ -211,6 +212,7 @@ class AppDelegate: NSObject, ObservableObject, NSApplicationDelegate, UNUserNoti
private func syncMenuShortcuts() {
guard ghostty.config != nil else { return }
syncMenuShortcut(action: "open_config", menuItem: self.menuOpenConfig)
syncMenuShortcut(action: "reload_config", menuItem: self.menuReloadConfig)
syncMenuShortcut(action: "quit", menuItem: self.menuQuit)
@ -340,6 +342,10 @@ class AppDelegate: NSObject, ObservableObject, NSApplicationDelegate, UNUserNoti
//MARK: - IB Actions
@IBAction func openConfig(_ sender: Any?) {
ghostty.openConfig()
}
@IBAction func reloadConfig(_ sender: Any?) {
ghostty.reloadConfig()
}

View File

@ -146,6 +146,7 @@ extension Ghostty {
userdata: Unmanaged.passUnretained(self).toOpaque(),
supports_selection_clipboard: false,
wakeup_cb: { userdata in AppState.wakeup(userdata) },
open_config_cb: { userdata in AppState.openConfig(userdata) },
reload_config_cb: { userdata in AppState.reloadConfig(userdata) },
set_title_cb: { userdata, title in AppState.setTitle(userdata, title: title) },
set_mouse_shape_cb: { userdata, shape in AppState.setMouseShape(userdata, shape: shape) },
@ -267,6 +268,11 @@ extension Ghostty {
NSApplication.shared.terminate(nil)
}
func openConfig() {
guard let app = self.app else { return }
ghostty_app_open_config(app)
}
func reloadConfig() {
guard let app = self.app else { return }
ghostty_app_reload_config(app)
@ -489,6 +495,10 @@ extension Ghostty {
)
}
static func openConfig(_ userdata: UnsafeMutableRawPointer?) {
ghostty_config_open();
}
static func reloadConfig(_ userdata: UnsafeMutableRawPointer?) -> ghostty_config_t? {
guard let newConfig = Self.loadConfig() else {
AppDelegate.logger.warning("failed to reload configuration")

View File

@ -27,6 +27,7 @@
<outlet property="menuNewTab" destination="uTG-Vz-hJU" id="eMg-R3-SeS"/>
<outlet property="menuNewWindow" destination="Was-JA-tGl" id="lK7-3I-CPG"/>
<outlet property="menuNextSplit" destination="bD7-ei-wKU" id="LeT-xw-eh4"/>
<outlet property="menuOpenConfig" destination="BOF-NM-1cW" id="Nze-Go-glw"/>
<outlet property="menuPaste" destination="i27-pK-umN" id="ICc-X2-gV3"/>
<outlet property="menuPreviousSplit" destination="Lic-px-1wg" id="Rto-CG-yRe"/>
<outlet property="menuQuit" destination="4sb-4s-VLi" id="qYN-S1-6UW"/>
@ -57,7 +58,11 @@
</connections>
</menuItem>
<menuItem isSeparatorItem="YES" id="VOq-y0-SEH"/>
<menuItem title="Preferences…" keyEquivalent="," id="BOF-NM-1cW"/>
<menuItem title="Preferences…" keyEquivalent="," id="BOF-NM-1cW">
<connections>
<action selector="openConfig:" target="bbz-4X-AYv" id="X65-fg-iWU"/>
</connections>
</menuItem>
<menuItem title="Reload Configuration" id="KKH-XX-5py">
<modifierMask key="keyEquivalentModifierMask"/>
<connections>

View File

@ -186,6 +186,7 @@ fn drainMailbox(self: *App, rt_app: *apprt.App) !void {
log.debug("mailbox message={s}", .{@tagName(message)});
switch (message) {
.reload_config => try self.reloadConfig(rt_app),
.open_config => try self.openConfig(rt_app),
.new_window => |msg| try self.newWindow(rt_app, msg),
.close => |surface| try self.closeSurface(surface),
.quit => try self.setQuit(),
@ -196,6 +197,12 @@ fn drainMailbox(self: *App, rt_app: *apprt.App) !void {
}
}
pub fn openConfig(self: *App, rt_app: *apprt.App) !void {
_ = self;
log.debug("opening configuration", .{});
try rt_app.openConfig();
}
pub fn reloadConfig(self: *App, rt_app: *apprt.App) !void {
log.debug("reloading configuration", .{});
if (try rt_app.reloadConfig()) |new| {
@ -274,6 +281,9 @@ pub const Message = union(enum) {
/// all the active surfaces.
reload_config: void,
// Open the configuration file
open_config: void,
/// Create a new terminal window.
new_window: NewWindow,

View File

@ -2550,6 +2550,8 @@ pub fn performBindingAction(self: *Surface, action: input.Binding.Action) !bool
.unbind => unreachable,
.ignore => {},
.open_config => try self.app.openConfig(self.rt_app),
.reload_config => try self.app.reloadConfig(self.rt_app),
.csi, .esc => |data| {

View File

@ -47,6 +47,9 @@ pub const App = struct {
/// called.
reload_config: *const fn (AppUD) callconv(.C) ?*const Config,
/// Open the configuration file.
open_config: *const fn (AppUD) callconv(.C) void,
/// Called to set the title of the window.
set_title: *const fn (SurfaceUD, [*]const u8) callconv(.C) void,
@ -160,6 +163,10 @@ pub const App = struct {
}
}
pub fn openConfig(self: *App) !void {
try Config.edit(self.core_app.alloc);
}
pub fn reloadConfig(self: *App) !?*const Config {
// Reload
if (self.opts.reload_config(self.opts.userdata)) |new| {
@ -1285,6 +1292,14 @@ pub const CAPI = struct {
};
}
/// Open the configuration.
export fn ghostty_app_open_config(v: *App) void {
_ = v.core_app.openConfig(v) catch |err| {
log.err("error reloading config err={}", .{err});
return;
};
}
/// Reload the configuration.
export fn ghostty_app_reload_config(v: *App) void {
_ = v.core_app.reloadConfig(v) catch |err| {

View File

@ -120,6 +120,12 @@ export fn ghostty_config_get_error(self: *Config, idx: u32) Error {
return .{ .message = err.message.ptr };
}
export fn ghostty_config_open() void {
Config.edit(global.alloc) catch |err| {
log.err("error opening config in editor err={}", .{err});
};
}
/// Sync with ghostty_error_s
const Error = extern struct {
message: [*:0]const u8 = "",

View File

@ -19,6 +19,7 @@ const Key = @import("key.zig").Key;
const KeyValue = @import("key.zig").Value;
const ErrorList = @import("ErrorList.zig");
const MetricModifier = fontpkg.face.Metrics.Modifier;
const Command = @import("../Command.zig");
const log = std.log.scoped(.config);
@ -819,6 +820,42 @@ pub fn deinit(self: *Config) void {
self.* = undefined;
}
/// Open the configuration in the OS default editor according to the default paths the main config file could be in:
///
/// 1. XDG Config File
///
pub fn edit(alloc_gpa: Allocator) !void {
// default location
const config_path = try internal_os.xdg.config(alloc_gpa, .{ .subdir = "ghostty/config" });
defer alloc_gpa.free(config_path);
// Try to create file and go on if it already exists
_ = std.fs.createFileAbsolute(config_path, .{ .exclusive = true }) catch |err| {
switch (err) {
error.PathAlreadyExists => log.info("config file found at {s}", .{config_path}),
else => return err,
}
};
// TODO: maybe add editor config property to allow users to set the editor they want to use when opening a file.
const editor = try Command.expandPath(alloc_gpa, "open") orelse "/usr/bin/open"; // should always be found, but worse case we use a hardcoded absolute path.
defer alloc_gpa.free(editor);
// the command to run
const argv = [_][]const u8{ editor, config_path };
var proc = std.ChildProcess.init(&argv, alloc_gpa);
proc.stdin_behavior = .Ignore;
proc.stdout_behavior = .Ignore;
proc.stderr_behavior = .Ignore;
try proc.spawn();
log.info("started subcommand path={s} pid={?}", .{ editor, proc.id });
// the process only ends after this call returns.
if (try proc.wait() != std.ChildProcess.Term.Exited) return error.ExitError;
}
/// Load the configuration according to the default rules:
///
/// 1. Defaults
@ -1121,7 +1158,11 @@ pub fn default(alloc_gpa: Allocator) Allocator.Error!Config {
.{ .key = .comma, .mods = .{ .super = true, .shift = true } },
.{ .reload_config = {} },
);
try result.keybind.set.put(
alloc,
.{ .key = .comma, .mods = .{ .super = true } },
.{ .open_config = {} },
);
try result.keybind.set.put(
alloc,
.{ .key = .k, .mods = .{ .super = true } },

View File

@ -214,6 +214,9 @@ pub const Action = union(enum) {
/// focused terminal.
inspector: InspectorMode,
/// Open the configuration file in the default OS editor.
open_config: void,
/// Reload the configuration. The exact meaning depends on the app runtime
/// in use but this usually involves re-reading the configuration file
/// and applying any changes. Note that not all changes can be applied at