diff --git a/include/ghostty.h b/include/ghostty.h index 357d40de5..0b70e2549 100644 --- a/include/ghostty.h +++ b/include/ghostty.h @@ -354,6 +354,7 @@ typedef struct { typedef void (*ghostty_runtime_wakeup_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,6 +382,7 @@ typedef struct { bool supports_selection_clipboard; ghostty_runtime_wakeup_cb wakeup_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; @@ -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 44e11ef28..d1b16d176 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? @@ -212,6 +213,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) @@ -342,6 +344,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..ca457f211 100644 --- a/macos/Sources/Ghostty/AppState.swift +++ b/macos/Sources/Ghostty/AppState.swift @@ -147,6 +147,7 @@ extension Ghostty { supports_selection_clipboard: false, wakeup_cb: { userdata in AppState.wakeup(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) }, @@ -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 1b6c88fa3..df7635f9a 100644 --- a/macos/Sources/MainMenu.xib +++ b/macos/Sources/MainMenu.xib @@ -28,6 +28,7 @@ + @@ -58,7 +59,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 cb781fe0a..339b2c5b7 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..724eb395f 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 configpkg.edit.open(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/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. 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 09ea27c70..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); @@ -120,6 +121,12 @@ export fn ghostty_config_get_error(self: *Config, idx: u32) Error { return .{ .message = err.message.ptr }; } +export fn ghostty_config_open() void { + edit.open(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 5c4109223..e97b7b355 100644 --- a/src/config/Config.zig +++ b/src/config/Config.zig @@ -1121,7 +1121,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/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); +} diff --git a/src/input/Binding.zig b/src/input/Binding.zig index 95449b60a..59cdc0e47 100644 --- a/src/input/Binding.zig +++ b/src/input/Binding.zig @@ -214,6 +214,11 @@ pub const Action = union(enum) { /// focused terminal. inspector: InspectorMode, + /// 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 /// in use but this usually involves re-reading the configuration file /// and applying any changes. Note that not all changes can be applied at