From 5946bc1a533ca05c4e4a1882560844fd71b42b4b Mon Sep 17 00:00:00 2001 From: Tim Culverhouse Date: Mon, 18 Dec 2023 12:34:32 -0600 Subject: [PATCH 1/5] cli: invert special case arg parsing logic Invert special case logic so that we can add additional cases. The previous logic bailed if we weren't the only special case ('-e'). --- src/config/Config.zig | 46 ++++++++++++++++++++++--------------------- 1 file changed, 24 insertions(+), 22 deletions(-) diff --git a/src/config/Config.zig b/src/config/Config.zig index e97b7b355..97a3b19a1 100644 --- a/src/config/Config.zig +++ b/src/config/Config.zig @@ -1630,35 +1630,37 @@ pub fn finalize(self: *Config) !void { /// Callback for src/cli/args.zig to allow us to handle special cases /// like `--help` or `-e`. Returns "false" if the CLI parsing should halt. pub fn parseManuallyHook(self: *Config, alloc: Allocator, arg: []const u8, iter: anytype) !bool { - // If it isn't "-e" then we just continue parsing normally. - if (!std.mem.eql(u8, arg, "-e")) return true; + if (std.mem.eql(u8, arg, "-e")) { + // Build up the command. We don't clean this up because we take + // ownership in our allocator. + var command = std.ArrayList(u8).init(alloc); + errdefer command.deinit(); - // Build up the command. We don't clean this up because we take - // ownership in our allocator. - var command = std.ArrayList(u8).init(alloc); - errdefer command.deinit(); + while (iter.next()) |param| { + try command.appendSlice(param); + try command.append(' '); + } - while (iter.next()) |param| { - try command.appendSlice(param); - try command.append(' '); - } + if (command.items.len == 0) { + try self._errors.add(alloc, .{ + .message = try std.fmt.allocPrintZ( + alloc, + "missing command after -e", + .{}, + ), + }); - if (command.items.len == 0) { - try self._errors.add(alloc, .{ - .message = try std.fmt.allocPrintZ( - alloc, - "missing command after -e", - .{}, - ), - }); + return false; + } + self.command = command.items[0 .. command.items.len - 1]; + + // Do not continue, we consumed everything. return false; } - self.command = command.items[0 .. command.items.len - 1]; - - // Do not continue, we consumed everything. - return false; + // If we didn't find a special case, continue parsing normally + return true; } /// Create a shallow copy of this config. This will share all the memory From e92f8b28d5cf57c2d17de91e744f79846ac1f988 Mon Sep 17 00:00:00 2001 From: Tim Culverhouse Date: Mon, 18 Dec 2023 12:38:14 -0600 Subject: [PATCH 2/5] cli: parse args as command when launched as 'xdg-terminal-exec' [xdg-terminal-exec](https://github.com/Vladimir-csp/xdg-terminal-exec) is a proposal to allow users to define a "default" terminal to use when launching applications which have `Terminal=true` in their desktop file. Users can symlink their terminal of choice to `xdg-terminal-exec`, which is the first option used in the GIO launch options, and is gaining traction elsewhere. When launched as `xdg-terminal-exec`, ghostty must parse any args as the command. Add a special case using the same logic as the '-e' flag to enable ghostty to be launched in this manner. Fixes: https://github.com/mitchellh/ghostty/issues/658 --- src/config/Config.zig | 8 +++++--- 1 file changed, 5 insertions(+), 3 deletions(-) diff --git a/src/config/Config.zig b/src/config/Config.zig index 97a3b19a1..28acfaade 100644 --- a/src/config/Config.zig +++ b/src/config/Config.zig @@ -1630,7 +1630,9 @@ pub fn finalize(self: *Config) !void { /// Callback for src/cli/args.zig to allow us to handle special cases /// like `--help` or `-e`. Returns "false" if the CLI parsing should halt. pub fn parseManuallyHook(self: *Config, alloc: Allocator, arg: []const u8, iter: anytype) !bool { - if (std.mem.eql(u8, arg, "-e")) { + if (std.mem.eql(u8, arg, "-e") or + std.mem.eql(u8, std.fs.path.basename(arg), "xdg-terminal-exec")) + { // Build up the command. We don't clean this up because we take // ownership in our allocator. var command = std.ArrayList(u8).init(alloc); @@ -1645,8 +1647,8 @@ pub fn parseManuallyHook(self: *Config, alloc: Allocator, arg: []const u8, iter: try self._errors.add(alloc, .{ .message = try std.fmt.allocPrintZ( alloc, - "missing command after -e", - .{}, + "missing command after {s}", + .{arg}, ), }); From 1137da9238bb203840ca1f2469d0082036073f8a Mon Sep 17 00:00:00 2001 From: Tim Culverhouse Date: Tue, 19 Dec 2023 08:41:29 -0600 Subject: [PATCH 3/5] cli: add xdg-terminal-exec parsing tests Add two tests for parsing of xdg-terminal-exec. --- src/config/Config.zig | 22 ++++++++++++++++++++++ 1 file changed, 22 insertions(+) diff --git a/src/config/Config.zig b/src/config/Config.zig index 28acfaade..c1984b7c8 100644 --- a/src/config/Config.zig +++ b/src/config/Config.zig @@ -1847,6 +1847,28 @@ test "parse e: command and args" { try testing.expectEqualStrings("echo foo bar baz", cfg.command.?); } +test "parse xdg-terminal-exec: command and args" { + const testing = std.testing; + var cfg = try Config.default(testing.allocator); + defer cfg.deinit(); + const alloc = cfg._arena.?.allocator(); + + var it: TestIterator = .{ .data = &.{ "echo", "foo", "bar baz" } }; + try testing.expect(!try cfg.parseManuallyHook(alloc, "xdg-terminal-exec", &it)); + try testing.expectEqualStrings("echo foo bar baz", cfg.command.?); +} + +test "parse xdg-terminal-exec: command and args with abs path" { + const testing = std.testing; + var cfg = try Config.default(testing.allocator); + defer cfg.deinit(); + const alloc = cfg._arena.?.allocator(); + + var it: TestIterator = .{ .data = &.{ "echo", "foo", "bar baz" } }; + try testing.expect(!try cfg.parseManuallyHook(alloc, "/home/ghostty/.local/bin/xdg-terminal-exec", &it)); + try testing.expectEqualStrings("echo foo bar baz", cfg.command.?); +} + test "clone default" { const testing = std.testing; const alloc = testing.allocator; From d9e4431800baab48353c8a70dc4474139906d423 Mon Sep 17 00:00:00 2001 From: Tim Culverhouse Date: Tue, 19 Dec 2023 09:38:11 -0600 Subject: [PATCH 4/5] cli: store manually parsed args for config replays CLI args are stored in the configuration `_inputs` field for replaying on configuration reload. When entering `parseManuallyHook`, we consume all args, preventing storage for replays. Store the args when parsing manually to allow replay of configuration. --- src/config/Config.zig | 1 + 1 file changed, 1 insertion(+) diff --git a/src/config/Config.zig b/src/config/Config.zig index c1984b7c8..8e1c54a7e 100644 --- a/src/config/Config.zig +++ b/src/config/Config.zig @@ -1639,6 +1639,7 @@ pub fn parseManuallyHook(self: *Config, alloc: Allocator, arg: []const u8, iter: errdefer command.deinit(); while (iter.next()) |param| { + try self._inputs.append(alloc, try alloc.dupe(u8, param)); try command.appendSlice(param); try command.append(' '); } From cea98d3afad14574cd69ee0c8600abbec220603f Mon Sep 17 00:00:00 2001 From: Mitchell Hashimoto Date: Tue, 19 Dec 2023 13:10:55 -0800 Subject: [PATCH 5/5] config: handle xdg-terminal-exec detection higher up --- src/config/Config.zig | 58 ++++++++++++++++++++++++------------------- 1 file changed, 33 insertions(+), 25 deletions(-) diff --git a/src/config/Config.zig b/src/config/Config.zig index 8e1c54a7e..2ca1c4a18 100644 --- a/src/config/Config.zig +++ b/src/config/Config.zig @@ -1345,6 +1345,38 @@ pub fn loadCliArgs(self: *Config, alloc_gpa: Allocator) !void { else => if (std.os.argv.len <= 1) return, } + // On Linux, we have a special case where if the executing + // program is "xdg-terminal-exec" then we treat all CLI + // args as if they are a command to execute. + if (comptime builtin.os.tag == .linux) xdg: { + if (!std.mem.eql( + u8, + std.fs.path.basename(std.mem.sliceTo(std.os.argv[0], 0)), + "xdg-terminal-exec", + )) break :xdg; + + const arena_alloc = self._arena.?.allocator(); + + // First, we add an artificial "-e" so that if we + // replay the inputs to rebuild the config (i.e. if + // a theme is set) then we will get the same behavior. + try self._inputs.append(arena_alloc, "-e"); + + // Next, take all remaining args and use that to build up + // a command to execute. + var command = std.ArrayList(u8).init(arena_alloc); + errdefer command.deinit(); + for (std.os.argv[1..]) |arg_raw| { + const arg = std.mem.sliceTo(arg_raw, 0); + try self._inputs.append(arena_alloc, try arena_alloc.dupe(u8, arg)); + try command.appendSlice(arg); + try command.append(' '); + } + + self.command = command.items[0 .. command.items.len - 1]; + return; + } + // Parse the config from the CLI args var iter = try std.process.argsWithAllocator(alloc_gpa); defer iter.deinit(); @@ -1630,9 +1662,7 @@ pub fn finalize(self: *Config) !void { /// Callback for src/cli/args.zig to allow us to handle special cases /// like `--help` or `-e`. Returns "false" if the CLI parsing should halt. pub fn parseManuallyHook(self: *Config, alloc: Allocator, arg: []const u8, iter: anytype) !bool { - if (std.mem.eql(u8, arg, "-e") or - std.mem.eql(u8, std.fs.path.basename(arg), "xdg-terminal-exec")) - { + if (std.mem.eql(u8, arg, "-e")) { // Build up the command. We don't clean this up because we take // ownership in our allocator. var command = std.ArrayList(u8).init(alloc); @@ -1848,28 +1878,6 @@ test "parse e: command and args" { try testing.expectEqualStrings("echo foo bar baz", cfg.command.?); } -test "parse xdg-terminal-exec: command and args" { - const testing = std.testing; - var cfg = try Config.default(testing.allocator); - defer cfg.deinit(); - const alloc = cfg._arena.?.allocator(); - - var it: TestIterator = .{ .data = &.{ "echo", "foo", "bar baz" } }; - try testing.expect(!try cfg.parseManuallyHook(alloc, "xdg-terminal-exec", &it)); - try testing.expectEqualStrings("echo foo bar baz", cfg.command.?); -} - -test "parse xdg-terminal-exec: command and args with abs path" { - const testing = std.testing; - var cfg = try Config.default(testing.allocator); - defer cfg.deinit(); - const alloc = cfg._arena.?.allocator(); - - var it: TestIterator = .{ .data = &.{ "echo", "foo", "bar baz" } }; - try testing.expect(!try cfg.parseManuallyHook(alloc, "/home/ghostty/.local/bin/xdg-terminal-exec", &it)); - try testing.expectEqualStrings("echo foo bar baz", cfg.command.?); -} - test "clone default" { const testing = std.testing; const alloc = testing.allocator;