From 646e3c365c8e481b9c003fa5e5dab71a3ec8eb0f Mon Sep 17 00:00:00 2001 From: Borja Clemente Date: Sun, 17 Dec 2023 14:22:38 +0100 Subject: [PATCH 1/4] Add settings shortcut on MacOS - Settings shortcut opens the config file in the default editor. Signed-off-by: Borja Clemente --- include/ghostty.h | 4 +++ macos/Sources/AppDelegate.swift | 6 ++++ macos/Sources/Ghostty/AppState.swift | 10 +++++++ macos/Sources/MainMenu.xib | 7 ++++- src/App.zig | 10 +++++++ src/Surface.zig | 2 ++ src/apprt/embedded.zig | 15 ++++++++++ src/config/CAPI.zig | 6 ++++ src/config/Config.zig | 43 +++++++++++++++++++++++++++- src/input/Binding.zig | 3 ++ 10 files changed, 104 insertions(+), 2 deletions(-) diff --git a/include/ghostty.h b/include/ghostty.h index 357d40de5..8c19150b1 100644 --- a/include/ghostty.h +++ b/include/ghostty.h @@ -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); diff --git a/macos/Sources/AppDelegate.swift b/macos/Sources/AppDelegate.swift index b99d45ba8..8dc07095d 100644 --- a/macos/Sources/AppDelegate.swift +++ b/macos/Sources/AppDelegate.swift @@ -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() } diff --git a/macos/Sources/Ghostty/AppState.swift b/macos/Sources/Ghostty/AppState.swift index f9f4da8b8..df1bc56bd 100644 --- a/macos/Sources/Ghostty/AppState.swift +++ b/macos/Sources/Ghostty/AppState.swift @@ -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") diff --git a/macos/Sources/MainMenu.xib b/macos/Sources/MainMenu.xib index c9317efbb..e1d54bc59 100644 --- a/macos/Sources/MainMenu.xib +++ b/macos/Sources/MainMenu.xib @@ -27,6 +27,7 @@ + @@ -57,7 +58,11 @@ - + + + + + diff --git a/src/App.zig b/src/App.zig index c1917f79b..99949d9e1 100644 --- a/src/App.zig +++ b/src/App.zig @@ -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, diff --git a/src/Surface.zig b/src/Surface.zig index f79818738..2859a32f2 100644 --- a/src/Surface.zig +++ b/src/Surface.zig @@ -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| { diff --git a/src/apprt/embedded.zig b/src/apprt/embedded.zig index fbeccfe86..0a4a59678 100644 --- a/src/apprt/embedded.zig +++ b/src/apprt/embedded.zig @@ -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| { diff --git a/src/config/CAPI.zig b/src/config/CAPI.zig index 09ea27c70..9213f7cf8 100644 --- a/src/config/CAPI.zig +++ b/src/config/CAPI.zig @@ -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 = "", diff --git a/src/config/Config.zig b/src/config/Config.zig index 1275c8a82..d52546bed 100644 --- a/src/config/Config.zig +++ b/src/config/Config.zig @@ -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 } }, diff --git a/src/input/Binding.zig b/src/input/Binding.zig index 19bdcdc27..98279e17e 100644 --- a/src/input/Binding.zig +++ b/src/input/Binding.zig @@ -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 From 7600c761ef5f8415f0c8097eda3abde678c65368 Mon Sep 17 00:00:00 2001 From: Mitchell Hashimoto Date: Mon, 18 Dec 2023 08:00:40 -0800 Subject: [PATCH 2/4] fix callback struct ordering, use internal_os.open --- include/ghostty.h | 4 +-- macos/Sources/Ghostty/AppState.swift | 2 +- src/apprt/embedded.zig | 2 +- src/config.zig | 1 + src/config/CAPI.zig | 3 ++- src/config/Config.zig | 37 ---------------------------- src/config/edit.zig | 29 ++++++++++++++++++++++ 7 files changed, 36 insertions(+), 42 deletions(-) create mode 100644 src/config/edit.zig diff --git a/include/ghostty.h b/include/ghostty.h index 8c19150b1..0b70e2549 100644 --- a/include/ghostty.h +++ b/include/ghostty.h @@ -353,8 +353,8 @@ 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_open_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); typedef void (*ghostty_runtime_set_mouse_visibility_cb)(void *, bool); @@ -381,8 +381,8 @@ 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_open_config_cb open_config_cb; ghostty_runtime_set_title_cb set_title_cb; ghostty_runtime_set_mouse_shape_cb set_mouse_shape_cb; ghostty_runtime_set_mouse_visibility_cb set_mouse_visibility_cb; diff --git a/macos/Sources/Ghostty/AppState.swift b/macos/Sources/Ghostty/AppState.swift index df1bc56bd..ca457f211 100644 --- a/macos/Sources/Ghostty/AppState.swift +++ b/macos/Sources/Ghostty/AppState.swift @@ -146,8 +146,8 @@ 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) }, + open_config_cb: { userdata in AppState.openConfig(userdata) }, set_title_cb: { userdata, title in AppState.setTitle(userdata, title: title) }, set_mouse_shape_cb: { userdata, shape in AppState.setMouseShape(userdata, shape: shape) }, set_mouse_visibility_cb: { userdata, visible in AppState.setMouseVisibility(userdata, visible: visible) }, diff --git a/src/apprt/embedded.zig b/src/apprt/embedded.zig index 0a4a59678..724eb395f 100644 --- a/src/apprt/embedded.zig +++ b/src/apprt/embedded.zig @@ -164,7 +164,7 @@ pub const App = struct { } pub fn openConfig(self: *App) !void { - try Config.edit(self.core_app.alloc); + try configpkg.edit.open(self.core_app.alloc); } pub fn reloadConfig(self: *App) !?*const Config { diff --git a/src/config.zig b/src/config.zig index cd449fb38..57c4bcd88 100644 --- a/src/config.zig +++ b/src/config.zig @@ -3,6 +3,7 @@ const builtin = @import("builtin"); pub usingnamespace @import("config/key.zig"); pub const Config = @import("config/Config.zig"); pub const string = @import("config/string.zig"); +pub const edit = @import("config/edit.zig"); pub const url = @import("config/url.zig"); // Field types diff --git a/src/config/CAPI.zig b/src/config/CAPI.zig index 9213f7cf8..cff6a4ea3 100644 --- a/src/config/CAPI.zig +++ b/src/config/CAPI.zig @@ -5,6 +5,7 @@ const global = &@import("../main.zig").state; const Config = @import("Config.zig"); const c_get = @import("c_get.zig"); +const edit = @import("edit.zig"); const Key = @import("key.zig").Key; const log = std.log.scoped(.config); @@ -121,7 +122,7 @@ export fn ghostty_config_get_error(self: *Config, idx: u32) Error { } export fn ghostty_config_open() void { - Config.edit(global.alloc) catch |err| { + edit.open(global.alloc) catch |err| { log.err("error opening config in editor err={}", .{err}); }; } diff --git a/src/config/Config.zig b/src/config/Config.zig index d52546bed..f99cd989a 100644 --- a/src/config/Config.zig +++ b/src/config/Config.zig @@ -19,7 +19,6 @@ 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); @@ -820,42 +819,6 @@ 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 diff --git a/src/config/edit.zig b/src/config/edit.zig new file mode 100644 index 000000000..8f223afcc --- /dev/null +++ b/src/config/edit.zig @@ -0,0 +1,29 @@ +const std = @import("std"); +const builtin = @import("builtin"); +const assert = std.debug.assert; +const Allocator = std.mem.Allocator; +const internal_os = @import("../os/main.zig"); +const Command = @import("../Command.zig"); + +const log = std.log.scoped(.config); + +/// Open the configuration in the OS default editor according to the default +/// paths the main config file could be in. +pub fn open(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 => {}, + else => return err, + } + }; + + try internal_os.open(alloc_gpa, config_path); +} From 2c311ab369071986d9803b3ab71f810bc73196fd Mon Sep 17 00:00:00 2001 From: Mitchell Hashimoto Date: Mon, 18 Dec 2023 08:04:24 -0800 Subject: [PATCH 3/4] apprt/gtk: hook up open config --- src/apprt/gtk/App.zig | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/src/apprt/gtk/App.zig b/src/apprt/gtk/App.zig index caae5669e..d746f2d8a 100644 --- a/src/apprt/gtk/App.zig +++ b/src/apprt/gtk/App.zig @@ -217,6 +217,11 @@ pub fn terminate(self: *App) void { self.config.deinit(); } +/// Open the configuration in the system editor. +pub fn openConfig(self: *App) !void { + try configpkg.edit.open(self.core_app.alloc); +} + /// Reload the configuration. This should return the new configuration. /// The old value can be freed immediately at this point assuming a /// successful return. From aa9b7cd2e9193b97725d6e3d3587d9a3a537f3ed Mon Sep 17 00:00:00 2001 From: Mitchell Hashimoto Date: Mon, 18 Dec 2023 08:07:41 -0800 Subject: [PATCH 4/4] input: clarify some limitations of opening config --- src/input/Binding.zig | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/src/input/Binding.zig b/src/input/Binding.zig index 98279e17e..c4a35a647 100644 --- a/src/input/Binding.zig +++ b/src/input/Binding.zig @@ -214,7 +214,9 @@ pub const Action = union(enum) { /// focused terminal. inspector: InspectorMode, - /// Open the configuration file in the default OS editor. + /// Open the configuration file in the default OS editor. If your default + /// OS editor isn't configured then this will fail. Currently, any failures + /// to open the configuration will show up only in the logs. open_config: void, /// Reload the configuration. The exact meaning depends on the app runtime