diff --git a/src/config/Config.zig b/src/config/Config.zig index ab0109da3..ac0289d3b 100644 --- a/src/config/Config.zig +++ b/src/config/Config.zig @@ -2290,8 +2290,12 @@ pub fn loadCliArgs(self: *Config, alloc_gpa: Allocator) !void { switch (builtin.os.tag) { .windows => {}, - // Fast-path if we are non-Windows and no args, do nothing. - else => if (std.os.argv.len <= 1) return, + // Fast-path if we are Linux and have no args. + .linux => if (std.os.argv.len <= 1) return, + + // Everything else we have to at least try because it may + // not use std.os.argv. + else => {}, } // On Linux, we have a special case where if the executing @@ -2360,9 +2364,13 @@ pub fn loadCliArgs(self: *Config, alloc_gpa: Allocator) !void { counter[i] = @field(self, field).list.items.len; } - // Parse the config from the CLI args - var iter = try std.process.argsWithAllocator(alloc_gpa); + // Initialize our CLI iterator. The first argument is always assumed + // to be the program name so we skip over that. + var iter = try internal_os.args.iterator(alloc_gpa); defer iter.deinit(); + if (iter.next()) |argv0| log.debug("skipping argv0 value={s}", .{argv0}); + + // Parse the config from the CLI args try self.loadIter(alloc_gpa, &iter); // If we are not loading the default files, then we need to diff --git a/src/os/args.zig b/src/os/args.zig new file mode 100644 index 000000000..9f7401c94 --- /dev/null +++ b/src/os/args.zig @@ -0,0 +1,131 @@ +const std = @import("std"); +const builtin = @import("builtin"); +const assert = std.debug.assert; +const Allocator = std.mem.Allocator; +const objc = @import("objc"); +const macos = @import("macos"); + +/// Returns an iterator over the command line arguments. This may or may +/// not allocate depending on the platform. +/// +/// For Zig-aware readers: this is the same as std.process.argsWithAllocator +/// but handles macOS using NSProcessInfo instead of libc argc/argv. +pub fn iterator(allocator: Allocator) ArgIterator.InitError!ArgIterator { + //if (true) return try std.process.argsWithAllocator(allocator); + return ArgIterator.initWithAllocator(allocator); +} + +/// Duck-typed to std.process.ArgIterator +pub const ArgIterator = switch (builtin.os.tag) { + .macos => IteratorMacOS, + else => std.process.ArgIterator, +}; + +/// This is an ArgIterator (duck-typed for std.process.ArgIterator) for +/// NSApplicationMain-based applications on macOS. It uses NSProcessInfo to +/// get the command line arguments since libc argc/argv pointers are not +/// valid. +/// +/// I believe this should work for all macOS applications even if +/// NSApplicationMain is not used, but I haven't tested that so I'm not +/// sure. If/when libghostty is ever used outside of NSApplicationMain +/// then we can revisit this. +const IteratorMacOS = struct { + alloc: Allocator, + index: usize, + count: usize, + buf: [:0]u8, + args: objc.Object, + + pub const InitError = Allocator.Error; + + pub fn initWithAllocator(alloc: Allocator) InitError!IteratorMacOS { + const NSProcessInfo = objc.getClass("NSProcessInfo").?; + const info = NSProcessInfo.msgSend(objc.Object, objc.sel("processInfo"), .{}); + const args = info.getProperty(objc.Object, "arguments"); + errdefer args.release(); + + // Determine our maximum length so we can allocate the buffer to + // fit all values. + var max: usize = 0; + const count: usize = @intCast(args.getProperty(c_ulong, "count")); + for (0..count) |i| { + const nsstr = args.msgSend( + objc.Object, + objc.sel("objectAtIndex:"), + .{@as(c_ulong, @intCast(i))}, + ); + + const maxlen: usize = @intCast(nsstr.msgSend( + c_ulong, + objc.sel("maximumLengthOfBytesUsingEncoding:"), + .{@as(c_ulong, 4)}, + )); + + max = @max(max, maxlen); + } + + // Allocate our buffer. We add 1 for the null terminator. + const buf = try alloc.allocSentinel(u8, max, 0); + errdefer alloc.free(buf); + + return .{ + .alloc = alloc, + .index = 0, + .count = count, + .buf = buf, + .args = args, + }; + } + + pub fn deinit(self: *IteratorMacOS) void { + self.alloc.free(self.buf); + + // Note: we don't release self.args because it is a pointer copy + // not a retained object. + } + + pub fn next(self: *IteratorMacOS) ?[:0]const u8 { + if (self.index == self.count) return null; + + // NSString. No release because not a copy. + const nsstr = self.args.msgSend( + objc.Object, + objc.sel("objectAtIndex:"), + .{@as(c_ulong, @intCast(self.index))}, + ); + self.index += 1; + + // Convert to string using getCString. Our buffer should always + // be big enough because we precomputed the maximum length. + if (!nsstr.msgSend( + bool, + objc.sel("getCString:maxLength:encoding:"), + .{ + @as([*]u8, @ptrCast(self.buf.ptr)), + @as(c_ulong, @intCast(self.buf.len)), + @as(c_ulong, 4), // NSUTF8StringEncoding + }, + )) { + // This should never happen... if it does, we just return empty. + return ""; + } + + return std.mem.sliceTo(self.buf, 0); + } + + pub fn skip(self: *IteratorMacOS) bool { + if (self.index == self.count) return false; + self.index += 1; + return true; + } +}; + +test "args" { + const testing = std.testing; + const alloc = testing.allocator; + + var iter = try iterator(alloc); + defer iter.deinit(); + try testing.expect(iter.next().?.len > 0); +} diff --git a/src/os/main.zig b/src/os/main.zig index 42bb0cdeb..7eed97445 100644 --- a/src/os/main.zig +++ b/src/os/main.zig @@ -15,6 +15,7 @@ const pipepkg = @import("pipe.zig"); const resourcesdir = @import("resourcesdir.zig"); // Namespaces +pub const args = @import("args.zig"); pub const cgroup = @import("cgroup.zig"); pub const passwd = @import("passwd.zig"); pub const xdg = @import("xdg.zig");