summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
-rw-r--r--include/ghostty.h8
-rw-r--r--macos/Ghostty.xcodeproj/project.pbxproj4
-rw-r--r--macos/Sources/App/macOS/AppDelegate.swift2
-rw-r--r--macos/Sources/Ghostty/Ghostty.Action.swift30
-rw-r--r--macos/Sources/Ghostty/Ghostty.App.swift51
-rw-r--r--macos/Sources/Ghostty/Package.swift20
-rw-r--r--macos/Sources/Helpers/Extensions/NSWorkspace+Extension.swift29
-rw-r--r--src/apprt/gtk/App.zig19
-rw-r--r--src/config/CAPI.zig31
-rw-r--r--src/config/edit.zig13
-rw-r--r--src/main_c.zig34
11 files changed, 212 insertions, 29 deletions
diff --git a/include/ghostty.h b/include/ghostty.h
index 2a4a7fb6e..312e6595a 100644
--- a/include/ghostty.h
+++ b/include/ghostty.h
@@ -351,6 +351,11 @@ typedef struct {
} ghostty_diagnostic_s;
typedef struct {
+ const char* ptr;
+ uintptr_t len;
+} ghostty_string_s;
+
+typedef struct {
double tl_px_x;
double tl_px_y;
uint32_t offset_start;
@@ -797,6 +802,7 @@ int ghostty_init(uintptr_t, char**);
void ghostty_cli_try_action(void);
ghostty_info_s ghostty_info(void);
const char* ghostty_translate(const char*);
+void ghostty_string_free(ghostty_string_s);
ghostty_config_t ghostty_config_new();
void ghostty_config_free(ghostty_config_t);
@@ -811,7 +817,7 @@ ghostty_input_trigger_s ghostty_config_trigger(ghostty_config_t,
uintptr_t);
uint32_t ghostty_config_diagnostics_count(ghostty_config_t);
ghostty_diagnostic_s ghostty_config_get_diagnostic(ghostty_config_t, uint32_t);
-void ghostty_config_open();
+ghostty_string_s ghostty_config_open_path(void);
ghostty_app_t ghostty_app_new(const ghostty_runtime_config_s*,
ghostty_config_t);
diff --git a/macos/Ghostty.xcodeproj/project.pbxproj b/macos/Ghostty.xcodeproj/project.pbxproj
index 08c3ef3b3..f6eedd864 100644
--- a/macos/Ghostty.xcodeproj/project.pbxproj
+++ b/macos/Ghostty.xcodeproj/project.pbxproj
@@ -14,6 +14,7 @@
9351BE8E3D22937F003B3499 /* nvim in Resources */ = {isa = PBXBuildFile; fileRef = 9351BE8E2D22937F003B3499 /* nvim */; };
A50297352DFA0F3400B4E924 /* Double+Extension.swift in Sources */ = {isa = PBXBuildFile; fileRef = A50297342DFA0F3300B4E924 /* Double+Extension.swift */; };
A505D21D2E1A2FA20018808F /* FileHandle+Extension.swift in Sources */ = {isa = PBXBuildFile; fileRef = A505D21C2E1A2F9E0018808F /* FileHandle+Extension.swift */; };
+ A505D21F2E1B6DE00018808F /* NSWorkspace+Extension.swift in Sources */ = {isa = PBXBuildFile; fileRef = A505D21E2E1B6DDC0018808F /* NSWorkspace+Extension.swift */; };
A511940F2E050595007258CC /* CloseTerminalIntent.swift in Sources */ = {isa = PBXBuildFile; fileRef = A511940E2E050590007258CC /* CloseTerminalIntent.swift */; };
A51194112E05A483007258CC /* QuickTerminalIntent.swift in Sources */ = {isa = PBXBuildFile; fileRef = A51194102E05A480007258CC /* QuickTerminalIntent.swift */; };
A51194132E05D006007258CC /* Optional+Extension.swift in Sources */ = {isa = PBXBuildFile; fileRef = A51194122E05D003007258CC /* Optional+Extension.swift */; };
@@ -160,6 +161,7 @@
9351BE8E2D22937F003B3499 /* nvim */ = {isa = PBXFileReference; lastKnownFileType = folder; name = nvim; path = "../zig-out/share/nvim"; sourceTree = "<group>"; };
A50297342DFA0F3300B4E924 /* Double+Extension.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "Double+Extension.swift"; sourceTree = "<group>"; };
A505D21C2E1A2F9E0018808F /* FileHandle+Extension.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "FileHandle+Extension.swift"; sourceTree = "<group>"; };
+ A505D21E2E1B6DDC0018808F /* NSWorkspace+Extension.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "NSWorkspace+Extension.swift"; sourceTree = "<group>"; };
A511940E2E050590007258CC /* CloseTerminalIntent.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CloseTerminalIntent.swift; sourceTree = "<group>"; };
A51194102E05A480007258CC /* QuickTerminalIntent.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = QuickTerminalIntent.swift; sourceTree = "<group>"; };
A51194122E05D003007258CC /* Optional+Extension.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "Optional+Extension.swift"; sourceTree = "<group>"; };
@@ -531,6 +533,7 @@
AEE8B3442B9AA39600260C5E /* NSPasteboard+Extension.swift */,
C1F26EA62B738B9900404083 /* NSView+Extension.swift */,
A5874D9C2DAD785F00E83852 /* NSWindow+Extension.swift */,
+ A505D21E2E1B6DDC0018808F /* NSWorkspace+Extension.swift */,
A5985CD62C320C4500C57AD3 /* String+Extension.swift */,
A58636722DF4813000E04A10 /* UndoManager+Extension.swift */,
A5CC36142C9CDA03004D6760 /* View+Extension.swift */,
@@ -819,6 +822,7 @@
A5CC36152C9CDA06004D6760 /* View+Extension.swift in Sources */,
A56D58892ACDE6CA00508D2C /* ServiceProvider.swift in Sources */,
A5CBD0602CA0C90A0017A1AE /* QuickTerminalWindow.swift in Sources */,
+ A505D21F2E1B6DE00018808F /* NSWorkspace+Extension.swift in Sources */,
A5CBD05E2CA0C5EC0017A1AE /* QuickTerminalController.swift in Sources */,
A5CF66D72D29DDB500139794 /* Ghostty.Event.swift in Sources */,
A511940F2E050595007258CC /* CloseTerminalIntent.swift in Sources */,
diff --git a/macos/Sources/App/macOS/AppDelegate.swift b/macos/Sources/App/macOS/AppDelegate.swift
index 53b6dce88..38500b7d3 100644
--- a/macos/Sources/App/macOS/AppDelegate.swift
+++ b/macos/Sources/App/macOS/AppDelegate.swift
@@ -932,7 +932,7 @@ class AppDelegate: NSObject,
//MARK: - IB Actions
@IBAction func openConfig(_ sender: Any?) {
- ghostty.openConfig()
+ Ghostty.App.openConfig()
}
@IBAction func reloadConfig(_ sender: Any?) {
diff --git a/macos/Sources/Ghostty/Ghostty.Action.swift b/macos/Sources/Ghostty/Ghostty.Action.swift
index dfdb0bff5..a6559600d 100644
--- a/macos/Sources/Ghostty/Ghostty.Action.swift
+++ b/macos/Sources/Ghostty/Ghostty.Action.swift
@@ -40,4 +40,34 @@ extension Ghostty.Action {
self.amount = c.amount
}
}
+
+ struct OpenURL {
+ enum Kind {
+ case unknown
+ case text
+
+ init(_ c: ghostty_action_open_url_kind_e) {
+ switch c {
+ case GHOSTTY_ACTION_OPEN_URL_KIND_TEXT:
+ self = .text
+ default:
+ self = .unknown
+ }
+ }
+ }
+
+ let kind: Kind
+ let url: String
+
+ init(c: ghostty_action_open_url_s) {
+ self.kind = Kind(c.kind)
+
+ if let urlCString = c.url {
+ let data = Data(bytes: urlCString, count: Int(c.len))
+ self.url = String(data: data, encoding: .utf8) ?? ""
+ } else {
+ self.url = ""
+ }
+ }
+ }
}
diff --git a/macos/Sources/Ghostty/Ghostty.App.swift b/macos/Sources/Ghostty/Ghostty.App.swift
index 17abe2b0e..0fdea1760 100644
--- a/macos/Sources/Ghostty/Ghostty.App.swift
+++ b/macos/Sources/Ghostty/Ghostty.App.swift
@@ -114,9 +114,21 @@ extension Ghostty {
ghostty_app_tick(app)
}
- func openConfig() {
- guard let app = self.app else { return }
- ghostty_app_open_config(app)
+ static func openConfig() {
+ let str = Ghostty.AllocatedString(ghostty_config_open_path()).string
+ guard !str.isEmpty else { return }
+ #if os(macOS)
+ let fileURL = URL(fileURLWithPath: str).absoluteString
+ var action = ghostty_action_open_url_s()
+ action.kind = GHOSTTY_ACTION_OPEN_URL_KIND_TEXT
+ fileURL.withCString { cStr in
+ action.url = cStr
+ action.len = UInt(fileURL.count)
+ _ = openURL(action)
+ }
+ #else
+ fatalError("Unsupported platform for opening config file")
+ #endif
}
/// Reload the configuration.
@@ -488,7 +500,7 @@ extension Ghostty {
pwdChanged(app, target: target, v: action.action.pwd)
case GHOSTTY_ACTION_OPEN_CONFIG:
- ghostty_config_open()
+ openConfig()
case GHOSTTY_ACTION_FLOAT_WINDOW:
toggleFloatWindow(app, target: target, mode: action.action.float_window)
@@ -546,6 +558,9 @@ extension Ghostty {
case GHOSTTY_ACTION_CHECK_FOR_UPDATES:
checkForUpdates(app)
+
+ case GHOSTTY_ACTION_OPEN_URL:
+ return openURL(action.action.open_url)
case GHOSTTY_ACTION_UNDO:
return undo(app, target: target)
@@ -598,6 +613,34 @@ extension Ghostty {
appDelegate.checkForUpdates(nil)
}
}
+
+ private static func openURL(
+ _ v: ghostty_action_open_url_s
+ ) -> Bool {
+ let action = Ghostty.Action.OpenURL(c: v)
+
+ // Convert the URL string to a URL object
+ guard let url = URL(string: action.url) else {
+ Ghostty.logger.warning("invalid URL for open URL action: \(action.url)")
+ return false
+ }
+
+ switch action.kind {
+ case .text:
+ // Open with the default text editor
+ if let textEditor = NSWorkspace.shared.defaultTextEditor {
+ NSWorkspace.shared.open([url], withApplicationAt: textEditor, configuration: NSWorkspace.OpenConfiguration())
+ return true
+ }
+
+ case .unknown:
+ break
+ }
+
+ // Open with the default application for the URL
+ NSWorkspace.shared.open(url)
+ return true
+ }
private static func undo(_ app: ghostty_app_t, target: ghostty_target_s) -> Bool {
let undoManager: UndoManager?
diff --git a/macos/Sources/Ghostty/Package.swift b/macos/Sources/Ghostty/Package.swift
index f30f2f6f9..9b05934df 100644
--- a/macos/Sources/Ghostty/Package.swift
+++ b/macos/Sources/Ghostty/Package.swift
@@ -74,6 +74,26 @@ extension Ghostty {
// MARK: Swift Types for C Types
extension Ghostty {
+ class AllocatedString {
+ private let cString: ghostty_string_s
+
+ init(_ c: ghostty_string_s) {
+ self.cString = c
+ }
+
+ var string: String {
+ guard let ptr = cString.ptr else { return "" }
+ let data = Data(bytes: ptr, count: Int(cString.len))
+ return String(data: data, encoding: .utf8) ?? ""
+ }
+
+ deinit {
+ ghostty_string_free(cString)
+ }
+ }
+}
+
+extension Ghostty {
enum SetFloatWIndow {
case on
case off
diff --git a/macos/Sources/Helpers/Extensions/NSWorkspace+Extension.swift b/macos/Sources/Helpers/Extensions/NSWorkspace+Extension.swift
new file mode 100644
index 000000000..bc2d028b5
--- /dev/null
+++ b/macos/Sources/Helpers/Extensions/NSWorkspace+Extension.swift
@@ -0,0 +1,29 @@
+import AppKit
+import UniformTypeIdentifiers
+
+extension NSWorkspace {
+ /// Returns the URL of the default text editor application.
+ /// - Returns: The URL of the default text editor, or nil if no default text editor is found.
+ var defaultTextEditor: URL? {
+ defaultApplicationURL(forContentType: UTType.plainText.identifier)
+ }
+
+ /// Returns the URL of the default application for opening files with the specified content type.
+ /// - Parameter contentType: The content type identifier (UTI) to find the default application for.
+ /// - Returns: The URL of the default application, or nil if no default application is found.
+ func defaultApplicationURL(forContentType contentType: String) -> URL? {
+ return LSCopyDefaultApplicationURLForContentType(
+ contentType as CFString,
+ .all,
+ nil
+ )?.takeRetainedValue() as? URL
+ }
+
+ /// Returns the URL of the default application for opening files with the specified file extension.
+ /// - Parameter ext: The file extension to find the default application for.
+ /// - Returns: The URL of the default application, or nil if no default application is found.
+ func defaultApplicationURL(forExtension ext: String) -> URL? {
+ guard let uti = UTType(filenameExtension: ext) else { return nil}
+ return defaultApplicationURL(forContentType: uti.identifier)
+ }
+}
diff --git a/src/apprt/gtk/App.zig b/src/apprt/gtk/App.zig
index 369090ee2..907f3a36d 100644
--- a/src/apprt/gtk/App.zig
+++ b/src/apprt/gtk/App.zig
@@ -496,7 +496,7 @@ pub fn performAction(
.resize_split => self.resizeSplit(target, value),
.equalize_splits => self.equalizeSplits(target),
.goto_split => return self.gotoSplit(target, value),
- .open_config => try configpkg.edit.open(self.core_app.alloc),
+ .open_config => return self.openConfig(),
.config_change => self.configChange(target, value.config),
.reload_config => try self.reloadConfig(target, value),
.inspector => self.controlInspector(target, value),
@@ -1759,7 +1759,22 @@ fn initActions(self: *App) void {
}
}
-pub fn openUrl(
+fn openConfig(self: *App) !bool {
+ // Get the config file path
+ const alloc = self.core_app.alloc;
+ const path = configpkg.edit.openPath(alloc) catch |err| {
+ log.warn("error getting config file path: {}", .{err});
+ return false;
+ };
+ defer alloc.free(path);
+
+ // Open it using openURL. "path" isn't actually a URL but
+ // at the time of writing that works just fine for GTK.
+ self.openUrl(.{ .kind = .text, .url = path });
+ return true;
+}
+
+fn openUrl(
app: *App,
value: apprt.action.OpenUrl,
) void {
diff --git a/src/config/CAPI.zig b/src/config/CAPI.zig
index 0b7108a59..bdc59797a 100644
--- a/src/config/CAPI.zig
+++ b/src/config/CAPI.zig
@@ -1,7 +1,9 @@
const std = @import("std");
+const assert = std.debug.assert;
const cli = @import("../cli.zig");
const inputpkg = @import("../input.zig");
-const global = &@import("../global.zig").state;
+const state = &@import("../global.zig").state;
+const c = @import("../main_c.zig");
const Config = @import("Config.zig");
const c_get = @import("c_get.zig");
@@ -12,14 +14,14 @@ const log = std.log.scoped(.config);
/// Create a new configuration filled with the initial default values.
export fn ghostty_config_new() ?*Config {
- const result = global.alloc.create(Config) catch |err| {
+ const result = state.alloc.create(Config) catch |err| {
log.err("error allocating config err={}", .{err});
return null;
};
- result.* = Config.default(global.alloc) catch |err| {
+ result.* = Config.default(state.alloc) catch |err| {
log.err("error creating config err={}", .{err});
- global.alloc.destroy(result);
+ state.alloc.destroy(result);
return null;
};
@@ -29,20 +31,20 @@ export fn ghostty_config_new() ?*Config {
export fn ghostty_config_free(ptr: ?*Config) void {
if (ptr) |v| {
v.deinit();
- global.alloc.destroy(v);
+ state.alloc.destroy(v);
}
}
/// Deep clone the configuration.
export fn ghostty_config_clone(self: *Config) ?*Config {
- const result = global.alloc.create(Config) catch |err| {
+ const result = state.alloc.create(Config) catch |err| {
log.err("error allocating config err={}", .{err});
return null;
};
- result.* = self.clone(global.alloc) catch |err| {
+ result.* = self.clone(state.alloc) catch |err| {
log.err("error cloning config err={}", .{err});
- global.alloc.destroy(result);
+ state.alloc.destroy(result);
return null;
};
@@ -51,7 +53,7 @@ export fn ghostty_config_clone(self: *Config) ?*Config {
/// Load the configuration from the CLI args.
export fn ghostty_config_load_cli_args(self: *Config) void {
- self.loadCliArgs(global.alloc) catch |err| {
+ self.loadCliArgs(state.alloc) catch |err| {
log.err("error loading config err={}", .{err});
};
}
@@ -60,7 +62,7 @@ export fn ghostty_config_load_cli_args(self: *Config) void {
/// is usually done first. The default file locations are locations
/// such as the home directory.
export fn ghostty_config_load_default_files(self: *Config) void {
- self.loadDefaultFiles(global.alloc) catch |err| {
+ self.loadDefaultFiles(state.alloc) catch |err| {
log.err("error loading config err={}", .{err});
};
}
@@ -69,7 +71,7 @@ export fn ghostty_config_load_default_files(self: *Config) void {
/// file locations in the previously loaded configuration. This will
/// recursively continue to load up to a built-in limit.
export fn ghostty_config_load_recursive_files(self: *Config) void {
- self.loadRecursiveFiles(global.alloc) catch |err| {
+ self.loadRecursiveFiles(state.alloc) catch |err| {
log.err("error loading config err={}", .{err});
};
}
@@ -122,10 +124,13 @@ export fn ghostty_config_get_diagnostic(self: *Config, idx: u32) Diagnostic {
return .{ .message = message.ptr };
}
-export fn ghostty_config_open() void {
- edit.open(global.alloc) catch |err| {
+export fn ghostty_config_open_path() c.String {
+ const path = edit.openPath(state.alloc) catch |err| {
log.err("error opening config in editor err={}", .{err});
+ return .empty;
};
+
+ return .fromSlice(path);
}
/// Sync with ghostty_diagnostic_s
diff --git a/src/config/edit.zig b/src/config/edit.zig
index ae4394942..38dc98169 100644
--- a/src/config/edit.zig
+++ b/src/config/edit.zig
@@ -5,18 +5,19 @@ const Allocator = std.mem.Allocator;
const ArenaAllocator = std.heap.ArenaAllocator;
const internal_os = @import("../os/main.zig");
-/// Open the configuration in the OS default editor according to the default
-/// paths the main config file could be in.
+/// The path to the configuration that should be opened for editing.
///
-/// On Linux, this will open the file at the XDG config path. This is the
+/// On Linux, this will use the file at the XDG config path. This is the
/// only valid path for Linux so we don't need to check for other paths.
///
/// On macOS, both XDG and AppSupport paths are valid. Because Ghostty
-/// prioritizes AppSupport over XDG, we will open AppSupport if it exists,
+/// prioritizes AppSupport over XDG, we will use AppSupport if it exists,
/// followed by XDG if it exists, and finally AppSupport if neither exist.
/// For the existence check, we also prefer non-empty files over empty
/// files.
-pub fn open(alloc_gpa: Allocator) !void {
+///
+/// The returned value is allocated using the provided allocator.
+pub fn openPath(alloc_gpa: Allocator) ![:0]const u8 {
// Use an arena to make memory management easier in here.
var arena = ArenaAllocator.init(alloc_gpa);
defer arena.deinit();
@@ -41,7 +42,7 @@ pub fn open(alloc_gpa: Allocator) !void {
}
};
- try internal_os.open(alloc_gpa, .text, config_path);
+ return try alloc_gpa.dupeZ(u8, config_path);
}
/// Returns the config path to use for open for the current OS.
diff --git a/src/main_c.zig b/src/main_c.zig
index 0722900e7..2c266cfb5 100644
--- a/src/main_c.zig
+++ b/src/main_c.zig
@@ -19,7 +19,12 @@ const internal_os = @import("os/main.zig");
// Some comptime assertions that our C API depends on.
comptime {
- assert(apprt.runtime == apprt.embedded);
+ // We allow tests to reference this file because we unit test
+ // some of the C API. At runtime though we should never get these
+ // functions unless we are building libghostty.
+ if (!builtin.is_test) {
+ assert(apprt.runtime == apprt.embedded);
+ }
}
/// Global options so we can log. This is identical to main.
@@ -29,7 +34,9 @@ comptime {
// These structs need to be referenced so the `export` functions
// are truly exported by the C API lib.
_ = @import("config.zig").CAPI;
- _ = apprt.runtime.CAPI;
+ if (@hasDecl(apprt.runtime, "CAPI")) {
+ _ = apprt.runtime.CAPI;
+ }
}
/// ghostty_info_s
@@ -46,6 +53,24 @@ const Info = extern struct {
};
};
+/// ghostty_string_s
+pub const String = extern struct {
+ ptr: ?[*]const u8,
+ len: usize,
+
+ pub const empty: String = .{
+ .ptr = null,
+ .len = 0,
+ };
+
+ pub fn fromSlice(slice: []const u8) String {
+ return .{
+ .ptr = slice.ptr,
+ .len = slice.len,
+ };
+ }
+};
+
/// Initialize ghostty global state.
export fn ghostty_init(argc: usize, argv: [*][*:0]u8) c_int {
assert(builtin.link_libc);
@@ -95,3 +120,8 @@ export fn ghostty_info() Info {
export fn ghostty_translate(msgid: [*:0]const u8) [*:0]const u8 {
return internal_os.i18n._(msgid);
}
+
+/// Free a string allocated by Ghostty.
+export fn ghostty_string_free(str: String) void {
+ state.alloc.free(str.ptr.?[0..str.len]);
+}