Skip to content

Commit adcaff7

Browse files
committed
config: edit opens AppSupport over XDG on macOS, prefers non-empty paths
Fixes ghostty-org#3953 Fixes ghostty-org#3284 This fixes two issues. In fixing one issue, the other became apparent so I fixed both in this one commit. The first issue is that on macOS, the `open` command should take the `-t` flag to open text files in a text editor. To do this, the `os.open` function now takes a type hint that is used to better do the right thing. Second, the order of the paths that we attempt to open when editing a config on macOS is wrong. Our priority when loading configs is well documented: https://ghostty.org/docs/config#macos-specific-path-(macos-only). But open_config does the opposite. This makes it too easy for people to have configs that are being overridden without them realizing it. This commit changes the order of the paths to match the documented order. If neither path exists, we prefer AppSupport.
1 parent 3059d90 commit adcaff7

File tree

5 files changed

+140
-39
lines changed

5 files changed

+140
-39
lines changed

Diff for: src/Surface.zig

+3-3
Original file line numberDiff line numberDiff line change
@@ -3195,15 +3195,15 @@ fn processLinks(self: *Surface, pos: apprt.CursorPos) !bool {
31953195
.trim = false,
31963196
});
31973197
defer self.alloc.free(str);
3198-
try internal_os.open(self.alloc, str);
3198+
try internal_os.open(self.alloc, .unknown, str);
31993199
},
32003200

32013201
._open_osc8 => {
32023202
const uri = self.osc8URI(sel.start()) orelse {
32033203
log.warn("failed to get URI for OSC8 hyperlink", .{});
32043204
return false;
32053205
};
3206-
try internal_os.open(self.alloc, uri);
3206+
try internal_os.open(self.alloc, .unknown, uri);
32073207
},
32083208
}
32093209

@@ -4303,7 +4303,7 @@ fn writeScreenFile(
43034303
const path = try tmp_dir.dir.realpath(filename, &path_buf);
43044304

43054305
switch (write_action) {
4306-
.open => try internal_os.open(self.alloc, path),
4306+
.open => try internal_os.open(self.alloc, .text, path),
43074307
.paste => self.io.queueMessage(try termio.Message.writeReq(
43084308
self.alloc,
43094309
path,

Diff for: src/config/edit.zig

+86-20
Original file line numberDiff line numberDiff line change
@@ -1,31 +1,29 @@
11
const std = @import("std");
22
const builtin = @import("builtin");
3+
const assert = std.debug.assert;
34
const Allocator = std.mem.Allocator;
5+
const ArenaAllocator = std.heap.ArenaAllocator;
46
const internal_os = @import("../os/main.zig");
57

68
/// Open the configuration in the OS default editor according to the default
79
/// paths the main config file could be in.
10+
///
11+
/// On Linux, this will open the file at the XDG config path. This is the
12+
/// only valid path for Linux so we don't need to check for other paths.
13+
///
14+
/// On macOS, both XDG and AppSupport paths are valid. Because Ghostty
15+
/// prioritizes AppSupport over XDG, we will open AppSupport if it exists,
16+
/// followed by XDG if it exists, and finally AppSupport if neither exist.
17+
/// For the existence check, we also prefer non-empty files over empty
18+
/// files.
819
pub fn open(alloc_gpa: Allocator) !void {
9-
// default location
10-
const config_path = config_path: {
11-
const xdg_config_path = try internal_os.xdg.config(alloc_gpa, .{ .subdir = "ghostty/config" });
12-
13-
if (comptime builtin.os.tag == .macos) macos: {
14-
// On macOS, use the application support path if the XDG path doesn't exists.
15-
if (std.fs.accessAbsolute(xdg_config_path, .{})) {
16-
break :macos;
17-
} else |err| switch (err) {
18-
error.BadPathName, error.FileNotFound => {},
19-
else => break :macos,
20-
}
21-
22-
alloc_gpa.free(xdg_config_path);
23-
break :config_path try internal_os.macos.appSupportDir(alloc_gpa, "config");
24-
}
20+
// Use an arena to make memory management easier in here.
21+
var arena = ArenaAllocator.init(alloc_gpa);
22+
defer arena.deinit();
23+
const alloc = arena.allocator();
2524

26-
break :config_path xdg_config_path;
27-
};
28-
defer alloc_gpa.free(config_path);
25+
// Get the path we should open
26+
const config_path = try configPath(alloc);
2927

3028
// Create config directory recursively.
3129
if (std.fs.path.dirname(config_path)) |config_dir| {
@@ -43,5 +41,73 @@ pub fn open(alloc_gpa: Allocator) !void {
4341
}
4442
};
4543

46-
try internal_os.open(alloc_gpa, config_path);
44+
try internal_os.open(alloc, .text, config_path);
45+
}
46+
47+
/// Returns the config path to use for open for the current OS.
48+
///
49+
/// The allocator must be an arena allocator. No memory is freed by this
50+
/// function and the resulting path is not all the memory that is allocated.
51+
///
52+
/// NOTE: WHY IS THIS INLINE? This is inline because when this is not
53+
/// inline then Zig 0.13 crashes [most of the time] when trying to compile
54+
/// this file. This is a workaround for that issue. This function is only
55+
/// called from one place that is not performance critical so it is fine
56+
/// to be inline.
57+
inline fn configPath(alloc_arena: Allocator) ![]const u8 {
58+
const paths: []const []const u8 = try configPathCandidates(alloc_arena);
59+
assert(paths.len > 0);
60+
61+
// Find the first path that exists and is non-empty. If no paths are
62+
// non-empty but at least one exists, we will return the first path that
63+
// exists.
64+
var exists: ?[]const u8 = null;
65+
for (paths) |path| {
66+
const f = std.fs.openFileAbsolute(path, .{}) catch |err| {
67+
switch (err) {
68+
// File doesn't exist, continue.
69+
error.BadPathName, error.FileNotFound => continue,
70+
71+
// Some other error, assume it exists and return it.
72+
else => return err,
73+
}
74+
};
75+
defer f.close();
76+
77+
// We expect stat to succeed because we just opened the file.
78+
const stat = try f.stat();
79+
80+
// If the file is non-empty, return it.
81+
if (stat.size > 0) return path;
82+
83+
// If the file is empty, remember it exists.
84+
if (exists == null) exists = path;
85+
}
86+
87+
// No paths are non-empty, return the first path that exists.
88+
if (exists) |v| return v;
89+
90+
// No paths are non-empty or exist, return the first path.
91+
return paths[0];
92+
}
93+
94+
/// Returns a const list of possible paths the main config file could be
95+
/// in for the current OS.
96+
fn configPathCandidates(alloc_arena: Allocator) ![]const []const u8 {
97+
var paths = try std.ArrayList([]const u8).initCapacity(alloc_arena, 2);
98+
errdefer paths.deinit();
99+
100+
if (comptime builtin.os.tag == .macos) {
101+
paths.appendAssumeCapacity(try internal_os.macos.appSupportDir(
102+
alloc_arena,
103+
"config",
104+
));
105+
}
106+
107+
paths.appendAssumeCapacity(try internal_os.xdg.config(
108+
alloc_arena,
109+
.{ .subdir = "ghostty/config" },
110+
));
111+
112+
return paths.items;
47113
}

Diff for: src/os/macos.zig

+1-1
Original file line numberDiff line numberDiff line change
@@ -24,7 +24,7 @@ pub const AppSupportDirError = Allocator.Error || error{AppleAPIFailed};
2424
pub fn appSupportDir(
2525
alloc: Allocator,
2626
sub_path: []const u8,
27-
) AppSupportDirError![]u8 {
27+
) AppSupportDirError![]const u8 {
2828
comptime assert(builtin.target.isDarwin());
2929

3030
const NSFileManager = objc.getClass("NSFileManager").?;

Diff for: src/os/main.zig

+1
Original file line numberDiff line numberDiff line change
@@ -41,5 +41,6 @@ pub const home = homedir.home;
4141
pub const ensureLocale = locale.ensureLocale;
4242
pub const clickInterval = mouse.clickInterval;
4343
pub const open = openpkg.open;
44+
pub const OpenType = openpkg.Type;
4445
pub const pipe = pipepkg.pipe;
4546
pub const resourcesDir = resourcesdir.resourcesDir;

Diff for: src/os/open.zig

+49-15
Original file line numberDiff line numberDiff line change
@@ -2,33 +2,34 @@ const std = @import("std");
22
const builtin = @import("builtin");
33
const Allocator = std.mem.Allocator;
44

5+
/// The type of the data at the URL to open. This is used as a hint
6+
/// to potentially open the URL in a different way.
7+
pub const Type = enum {
8+
text,
9+
unknown,
10+
};
11+
512
/// Open a URL in the default handling application.
613
///
714
/// Any output on stderr is logged as a warning in the application logs.
815
/// Output on stdout is ignored.
9-
pub fn open(alloc: Allocator, url: []const u8) !void {
10-
// Some opener commands terminate after opening (macOS open) and some do not
11-
// (xdg-open). For those which do not terminate, we do not want to wait for
12-
// the process to exit to collect stderr.
13-
const argv, const wait = switch (builtin.os.tag) {
14-
.linux => .{ &.{ "xdg-open", url }, false },
15-
.macos => .{ &.{ "open", url }, true },
16-
.windows => .{ &.{ "rundll32", "url.dll,FileProtocolHandler", url }, false },
17-
.ios => return error.Unimplemented,
18-
else => @compileError("unsupported OS"),
19-
};
16+
pub fn open(
17+
alloc: Allocator,
18+
typ: Type,
19+
url: []const u8,
20+
) !void {
21+
const cmd = try openCommand(alloc, typ, url);
2022

21-
var exe = std.process.Child.init(argv, alloc);
22-
23-
if (comptime wait) {
23+
var exe = cmd.child;
24+
if (cmd.wait) {
2425
// Pipe stdout/stderr so we can collect output from the command
2526
exe.stdout_behavior = .Pipe;
2627
exe.stderr_behavior = .Pipe;
2728
}
2829

2930
try exe.spawn();
3031

31-
if (comptime wait) {
32+
if (cmd.wait) {
3233
// 50 KiB is the default value used by std.process.Child.run
3334
const output_max_size = 50 * 1024;
3435

@@ -47,3 +48,36 @@ pub fn open(alloc: Allocator, url: []const u8) !void {
4748
if (stderr.items.len > 0) std.log.err("open stderr={s}", .{stderr.items});
4849
}
4950
}
51+
52+
const OpenCommand = struct {
53+
child: std.process.Child,
54+
wait: bool = false,
55+
};
56+
57+
fn openCommand(alloc: Allocator, typ: Type, url: []const u8) !OpenCommand {
58+
return switch (builtin.os.tag) {
59+
.linux => .{ .child = std.process.Child.init(
60+
&.{ "xdg-open", url },
61+
alloc,
62+
) },
63+
64+
.windows => .{ .child = std.process.Child.init(
65+
&.{ "rundll32", "url.dll,FileProtocolHandler", url },
66+
alloc,
67+
) },
68+
69+
.macos => .{
70+
.child = std.process.Child.init(
71+
switch (typ) {
72+
.text => &.{ "open", "-t", url },
73+
.unknown => &.{ "open", url },
74+
},
75+
alloc,
76+
),
77+
.wait = true,
78+
},
79+
80+
.ios => return error.Unimplemented,
81+
else => @compileError("unsupported OS"),
82+
};
83+
}

0 commit comments

Comments
 (0)