mirror of
https://github.com/ghostty-org/ghostty.git
synced 2025-07-14 15:56:13 +03:00
Merge pull request #1102 from mitchellh/macos-login
macOS: use login command, "command" now accepts arguments directly
This commit is contained in:
@ -336,28 +336,15 @@ palette: Palette = .{},
|
|||||||
/// - SHELL environment variable
|
/// - SHELL environment variable
|
||||||
/// - passwd entry (user information)
|
/// - passwd entry (user information)
|
||||||
///
|
///
|
||||||
/// The command is the path to only the binary to run. This cannot
|
/// This can contain additional arguments to run the command with.
|
||||||
/// also contain arguments, because Ghostty does not perform any
|
/// If additional arguments are provided, the command will be executed
|
||||||
/// shell string parsing. To provide additional arguments, use the
|
/// using "/bin/sh -c". Ghostty does not do any shell command parsing.
|
||||||
/// "command-arg" configuration (repeated for multiple arguments).
|
|
||||||
///
|
///
|
||||||
/// If you're using the `ghostty` CLI there is also a shortcut
|
/// If you're using the `ghostty` CLI there is also a shortcut
|
||||||
/// to run a command with argumens directly: you can use the `-e`
|
/// to run a command with argumens directly: you can use the `-e`
|
||||||
/// flag. For example: `ghostty -e fish --with --custom --args`.
|
/// flag. For example: `ghostty -e fish --with --custom --args`.
|
||||||
/// This is just shorthand for specifying "command" and
|
|
||||||
/// "command-arg" in the configuration.
|
|
||||||
command: ?[]const u8 = null,
|
command: ?[]const u8 = null,
|
||||||
|
|
||||||
/// A single argument to pass to the command. This can be repeated to
|
|
||||||
/// pass multiple arguments. This slightly clunky configuration style is
|
|
||||||
/// so that Ghostty doesn't have to perform any sort of shell parsing
|
|
||||||
/// to find argument boundaries.
|
|
||||||
///
|
|
||||||
/// This cannot be used to override argv[0]. argv[0] will always be
|
|
||||||
/// set by Ghostty to be the command (possibly with a hyphen-prefix to
|
|
||||||
/// indicate that it is a login shell, depending on the OS).
|
|
||||||
@"command-arg": RepeatableString = .{},
|
|
||||||
|
|
||||||
/// Match a regular expression against the terminal text and associate
|
/// Match a regular expression against the terminal text and associate
|
||||||
/// clicking it with an action. This can be used to match URLs, file paths,
|
/// clicking it with an action. This can be used to match URLs, file paths,
|
||||||
/// etc. Actions can be opening using the system opener (i.e. "open" or
|
/// etc. Actions can be opening using the system opener (i.e. "open" or
|
||||||
@ -1652,10 +1639,17 @@ pub fn parseManuallyHook(self: *Config, alloc: Allocator, arg: []const u8, iter:
|
|||||||
// If it isn't "-e" then we just continue parsing normally.
|
// 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")) return true;
|
||||||
|
|
||||||
// The first value is the command to run.
|
// Build up the command. We don't clean this up because we take
|
||||||
if (iter.next()) |command| {
|
// ownership in our allocator.
|
||||||
self.command = try alloc.dupe(u8, command);
|
var command = std.ArrayList(u8).init(alloc);
|
||||||
} else {
|
errdefer command.deinit();
|
||||||
|
|
||||||
|
while (iter.next()) |param| {
|
||||||
|
try command.appendSlice(param);
|
||||||
|
try command.append(' ');
|
||||||
|
}
|
||||||
|
|
||||||
|
if (command.items.len == 0) {
|
||||||
try self._errors.add(alloc, .{
|
try self._errors.add(alloc, .{
|
||||||
.message = try std.fmt.allocPrintZ(
|
.message = try std.fmt.allocPrintZ(
|
||||||
alloc,
|
alloc,
|
||||||
@ -1667,11 +1661,7 @@ pub fn parseManuallyHook(self: *Config, alloc: Allocator, arg: []const u8, iter:
|
|||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
// All further arguments are parameters
|
self.command = command.items[0 .. command.items.len - 1];
|
||||||
self.@"command-arg".list.clearRetainingCapacity();
|
|
||||||
while (iter.next()) |param| {
|
|
||||||
try self.@"command-arg".parseCLI(alloc, param);
|
|
||||||
}
|
|
||||||
|
|
||||||
// Do not continue, we consumed everything.
|
// Do not continue, we consumed everything.
|
||||||
return false;
|
return false;
|
||||||
@ -1856,25 +1846,7 @@ test "parse e: command and args" {
|
|||||||
|
|
||||||
var it: TestIterator = .{ .data = &.{ "echo", "foo", "bar baz" } };
|
var it: TestIterator = .{ .data = &.{ "echo", "foo", "bar baz" } };
|
||||||
try testing.expect(!try cfg.parseManuallyHook(alloc, "-e", &it));
|
try testing.expect(!try cfg.parseManuallyHook(alloc, "-e", &it));
|
||||||
try testing.expectEqualStrings("echo", cfg.command.?);
|
try testing.expectEqualStrings("echo foo bar baz", cfg.command.?);
|
||||||
try testing.expectEqual(@as(usize, 2), cfg.@"command-arg".list.items.len);
|
|
||||||
try testing.expectEqualStrings("foo", cfg.@"command-arg".list.items[0]);
|
|
||||||
try testing.expectEqualStrings("bar baz", cfg.@"command-arg".list.items[1]);
|
|
||||||
}
|
|
||||||
|
|
||||||
test "parse e: command replaces args" {
|
|
||||||
const testing = std.testing;
|
|
||||||
var cfg = try Config.default(testing.allocator);
|
|
||||||
defer cfg.deinit();
|
|
||||||
const alloc = cfg._arena.?.allocator();
|
|
||||||
|
|
||||||
try cfg.@"command-arg".parseCLI(alloc, "foo");
|
|
||||||
try testing.expectEqual(@as(usize, 1), cfg.@"command-arg".list.items.len);
|
|
||||||
|
|
||||||
var it: TestIterator = .{ .data = &.{"echo"} };
|
|
||||||
try testing.expect(!try cfg.parseManuallyHook(alloc, "-e", &it));
|
|
||||||
try testing.expectEqualStrings("echo", cfg.command.?);
|
|
||||||
try testing.expectEqual(@as(usize, 0), cfg.@"command-arg".list.items.len);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
test "clone default" {
|
test "clone default" {
|
||||||
|
@ -26,6 +26,7 @@ const c = if (builtin.os.tag != .windows) @cImport({
|
|||||||
pub const Entry = struct {
|
pub const Entry = struct {
|
||||||
shell: ?[]const u8 = null,
|
shell: ?[]const u8 = null,
|
||||||
home: ?[]const u8 = null,
|
home: ?[]const u8 = null,
|
||||||
|
name: ?[]const u8 = null,
|
||||||
};
|
};
|
||||||
|
|
||||||
/// Get the passwd entry for the currently executing user.
|
/// Get the passwd entry for the currently executing user.
|
||||||
@ -134,6 +135,13 @@ pub fn get(alloc: Allocator) !Entry {
|
|||||||
result.home = dir;
|
result.home = dir;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (pw.pw_name) |ptr| {
|
||||||
|
const source = std.mem.sliceTo(ptr, 0);
|
||||||
|
const name = try alloc.alloc(u8, source.len);
|
||||||
|
@memcpy(name, source);
|
||||||
|
result.name = name;
|
||||||
|
}
|
||||||
|
|
||||||
return result;
|
return result;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -709,7 +709,6 @@ const Subprocess = struct {
|
|||||||
arena: std.heap.ArenaAllocator,
|
arena: std.heap.ArenaAllocator,
|
||||||
cwd: ?[]const u8,
|
cwd: ?[]const u8,
|
||||||
env: EnvMap,
|
env: EnvMap,
|
||||||
path: []const u8,
|
|
||||||
args: [][]const u8,
|
args: [][]const u8,
|
||||||
grid_size: renderer.GridSize,
|
grid_size: renderer.GridSize,
|
||||||
screen_size: renderer.ScreenSize,
|
screen_size: renderer.ScreenSize,
|
||||||
@ -731,43 +730,6 @@ const Subprocess = struct {
|
|||||||
.windows => "cmd.exe",
|
.windows => "cmd.exe",
|
||||||
else => "sh",
|
else => "sh",
|
||||||
};
|
};
|
||||||
const path = try Command.expandPath(
|
|
||||||
alloc,
|
|
||||||
opts.full_config.command orelse default_path,
|
|
||||||
) orelse path: {
|
|
||||||
// If we had a command specified, try to at least fall
|
|
||||||
// back to a default value like "sh" so that Ghostty
|
|
||||||
// launches.
|
|
||||||
if (opts.full_config.command) |command| {
|
|
||||||
log.warn("unable to find command, fallbacking back to default command={s}", .{command});
|
|
||||||
if (try Command.expandPath(
|
|
||||||
alloc,
|
|
||||||
default_path,
|
|
||||||
)) |path| break :path path;
|
|
||||||
}
|
|
||||||
|
|
||||||
log.warn("unable to find default command to launch, exiting", .{});
|
|
||||||
return error.CommandNotFound;
|
|
||||||
};
|
|
||||||
|
|
||||||
// On macOS, we launch the program as a login shell. This is a Mac-specific
|
|
||||||
// behavior (see other terminals). Terminals in general should NOT be
|
|
||||||
// spawning login shells because well... we're not "logging in." The solution
|
|
||||||
// is to put dotfiles in "rc" variants rather than "_login" variants. But,
|
|
||||||
// history!
|
|
||||||
const argv0_override: ?[]const u8 = if (comptime builtin.target.isDarwin()) argv0: {
|
|
||||||
// Get rid of the path
|
|
||||||
const argv0 = if (std.mem.lastIndexOf(u8, path, "/")) |idx|
|
|
||||||
path[idx + 1 ..]
|
|
||||||
else
|
|
||||||
path;
|
|
||||||
|
|
||||||
// Copy it with a hyphen so its a login shell
|
|
||||||
const argv0_buf = try alloc.alloc(u8, argv0.len + 1);
|
|
||||||
argv0_buf[0] = '-';
|
|
||||||
@memcpy(argv0_buf[1..], argv0);
|
|
||||||
break :argv0 argv0_buf;
|
|
||||||
} else null;
|
|
||||||
|
|
||||||
// Set our env vars. For Flatpak builds running in Flatpak we don't
|
// Set our env vars. For Flatpak builds running in Flatpak we don't
|
||||||
// inherit our environment because the login shell on the host side
|
// inherit our environment because the login shell on the host side
|
||||||
@ -838,23 +800,102 @@ const Subprocess = struct {
|
|||||||
|
|
||||||
// Build our args list
|
// Build our args list
|
||||||
const args = args: {
|
const args = args: {
|
||||||
const cap = 1 + opts.full_config.@"command-arg".list.items.len;
|
const cap = 6; // the most we'll use on macOS
|
||||||
var args = try std.ArrayList([]const u8).initCapacity(alloc, cap);
|
var args = try std.ArrayList([]const u8).initCapacity(alloc, cap);
|
||||||
defer args.deinit();
|
defer args.deinit();
|
||||||
|
|
||||||
if (!internal_os.isFlatpak()) {
|
// If we're on macOS, we have to use `login(1)` to get all of
|
||||||
try args.append(argv0_override orelse path);
|
// the proper environment variables set, a login shell, and proper
|
||||||
} else {
|
// hushlogin behavior.
|
||||||
// We run our shell wrapped in a /bin/sh login shell because
|
if (comptime builtin.target.isDarwin()) darwin: {
|
||||||
// some systems do not properly initialize the env vars unless
|
const passwd = internal_os.passwd.get(alloc) catch |err| {
|
||||||
// we start this way (NixOS!)
|
log.warn("failed to read passwd, not using a login shell err={}", .{err});
|
||||||
try args.append("/bin/sh");
|
break :darwin;
|
||||||
try args.append("-l");
|
};
|
||||||
|
|
||||||
|
const username = passwd.name orelse {
|
||||||
|
log.warn("failed to get username, not using a login shell", .{});
|
||||||
|
break :darwin;
|
||||||
|
};
|
||||||
|
|
||||||
|
const hush = if (passwd.home) |home| hush: {
|
||||||
|
var dir = std.fs.openDirAbsolute(home, .{}) catch |err| {
|
||||||
|
log.warn(
|
||||||
|
"failed to open home dir, not checking for hushlogin err={}",
|
||||||
|
.{err},
|
||||||
|
);
|
||||||
|
break :hush false;
|
||||||
|
};
|
||||||
|
defer dir.close();
|
||||||
|
|
||||||
|
break :hush if (dir.access(".hushlogin", .{})) true else |_| false;
|
||||||
|
} else false;
|
||||||
|
|
||||||
|
const cmd = try std.fmt.allocPrint(
|
||||||
|
alloc,
|
||||||
|
"exec -l {s}",
|
||||||
|
.{opts.full_config.command orelse default_path},
|
||||||
|
);
|
||||||
|
|
||||||
|
// The reason for executing login this way is unclear. This
|
||||||
|
// comment will attempt to explain but prepare for a truly
|
||||||
|
// unhinged reality.
|
||||||
|
//
|
||||||
|
// The first major issue is that on macOS, a lot of users
|
||||||
|
// put shell configurations in ~/.bash_profile instead of
|
||||||
|
// ~/.bashrc (or equivalent for another shell). This file is only
|
||||||
|
// loaded for a login shell so macOS users expect all their terminals
|
||||||
|
// to be login shells. No other platform behaves this way and its
|
||||||
|
// totally braindead but somehow the entire dev community on
|
||||||
|
// macOS has cargo culted their way to this reality so we have to
|
||||||
|
// do it...
|
||||||
|
//
|
||||||
|
// To get a login shell, you COULD just prepend argv0 with a `-`
|
||||||
|
// but that doesn't fully work because `getlogin()` C API will
|
||||||
|
// return the wrong value, SHELL won't be set, and various
|
||||||
|
// other login behaviors that macOS users expect.
|
||||||
|
//
|
||||||
|
// The proper way is to use `login(1)`. But login(1) forces
|
||||||
|
// the working directory to change to the home directory,
|
||||||
|
// which we may not want. If we specify "-l" then we can avoid
|
||||||
|
// this behavior but now the shell isn't a login shell.
|
||||||
|
//
|
||||||
|
// There is another issue: `login(1)` only checks for ".hushlogin"
|
||||||
|
// in the working directory. This means that if we specify "-l"
|
||||||
|
// then we won't get hushlogin honored if its in the home
|
||||||
|
// directory (which is standard). To get around this, we
|
||||||
|
// check for hushlogin ourselves and if present specify the
|
||||||
|
// "-q" flag to login(1).
|
||||||
|
//
|
||||||
|
// So to get all the behaviors we want, we specify "-l" but
|
||||||
|
// execute "zsh" (which is built-in to macOS). We then use
|
||||||
|
// the zsh builtin "exec" to replace the process with a login
|
||||||
|
// shell ("-l" on exec) with the command we really want.
|
||||||
|
//
|
||||||
|
// To figure out a lot of this logic I read the login.c
|
||||||
|
// source code in the OSS distribution Apple provides for
|
||||||
|
// macOS.
|
||||||
|
//
|
||||||
|
// Awesome.
|
||||||
|
try args.append("/usr/bin/login");
|
||||||
|
if (hush) try args.append("-q");
|
||||||
|
try args.append("-flp");
|
||||||
|
try args.append(username);
|
||||||
|
try args.append("/bin/zsh");
|
||||||
try args.append("-c");
|
try args.append("-c");
|
||||||
try args.append(path);
|
try args.append(cmd);
|
||||||
|
break :args try args.toOwnedSlice();
|
||||||
}
|
}
|
||||||
|
|
||||||
try args.appendSlice(opts.full_config.@"command-arg".list.items);
|
// We run our shell wrapped in `/bin/sh` so that we don't have
|
||||||
|
// to parse the commadnd line ourselves if it has arguments.
|
||||||
|
// Additionally, some environments (NixOS, I found) use /bin/sh
|
||||||
|
// to setup some environment variables that are important to
|
||||||
|
// have set.
|
||||||
|
try args.append("/bin/sh");
|
||||||
|
if (internal_os.isFlatpak()) try args.append("-l");
|
||||||
|
try args.append("-c");
|
||||||
|
try args.append(opts.full_config.command orelse default_path);
|
||||||
break :args try args.toOwnedSlice();
|
break :args try args.toOwnedSlice();
|
||||||
};
|
};
|
||||||
|
|
||||||
@ -865,9 +906,6 @@ const Subprocess = struct {
|
|||||||
else
|
else
|
||||||
null;
|
null;
|
||||||
|
|
||||||
// The execution path
|
|
||||||
const final_path = if (internal_os.isFlatpak()) args[0] else path;
|
|
||||||
|
|
||||||
// Setup our shell integration, if we can.
|
// Setup our shell integration, if we can.
|
||||||
const shell_integrated: ?shell_integration.Shell = shell: {
|
const shell_integrated: ?shell_integration.Shell = shell: {
|
||||||
const force: ?shell_integration.Shell = switch (opts.full_config.@"shell-integration") {
|
const force: ?shell_integration.Shell = switch (opts.full_config.@"shell-integration") {
|
||||||
@ -877,10 +915,19 @@ const Subprocess = struct {
|
|||||||
.zsh => .zsh,
|
.zsh => .zsh,
|
||||||
};
|
};
|
||||||
|
|
||||||
|
// We have to get the path to the executing shell. The command
|
||||||
|
// can be a full shell string with arguments so we look for a space
|
||||||
|
// and take the first part.
|
||||||
|
const path = if (opts.full_config.command) |cmd| path: {
|
||||||
|
const idx = std.mem.indexOfScalar(u8, cmd, ' ') orelse cmd.len;
|
||||||
|
break :path cmd[0..idx];
|
||||||
|
} else default_path;
|
||||||
|
|
||||||
const dir = opts.resources_dir orelse break :shell null;
|
const dir = opts.resources_dir orelse break :shell null;
|
||||||
|
|
||||||
break :shell try shell_integration.setup(
|
break :shell try shell_integration.setup(
|
||||||
dir,
|
dir,
|
||||||
final_path,
|
path,
|
||||||
&env,
|
&env,
|
||||||
force,
|
force,
|
||||||
opts.full_config.@"shell-integration-features",
|
opts.full_config.@"shell-integration-features",
|
||||||
@ -902,7 +949,6 @@ const Subprocess = struct {
|
|||||||
.arena = arena,
|
.arena = arena,
|
||||||
.env = env,
|
.env = env,
|
||||||
.cwd = cwd,
|
.cwd = cwd,
|
||||||
.path = final_path,
|
|
||||||
.args = args,
|
.args = args,
|
||||||
.grid_size = opts.grid_size,
|
.grid_size = opts.grid_size,
|
||||||
.screen_size = padded_size,
|
.screen_size = padded_size,
|
||||||
@ -938,10 +984,7 @@ const Subprocess = struct {
|
|||||||
self.pty = null;
|
self.pty = null;
|
||||||
}
|
}
|
||||||
|
|
||||||
log.debug("starting command path={s} args={s}", .{
|
log.debug("starting command command={s}", .{self.args});
|
||||||
self.path,
|
|
||||||
self.args,
|
|
||||||
});
|
|
||||||
|
|
||||||
// In flatpak, we use the HostCommand to execute our shell.
|
// In flatpak, we use the HostCommand to execute our shell.
|
||||||
if (internal_os.isFlatpak()) flatpak: {
|
if (internal_os.isFlatpak()) flatpak: {
|
||||||
@ -950,10 +993,6 @@ const Subprocess = struct {
|
|||||||
break :flatpak;
|
break :flatpak;
|
||||||
}
|
}
|
||||||
|
|
||||||
// For flatpak our path and argv[0] must match because that is
|
|
||||||
// used for execution by the dbus API.
|
|
||||||
assert(std.mem.eql(u8, self.path, self.args[0]));
|
|
||||||
|
|
||||||
// Flatpak command must have a stable pointer.
|
// Flatpak command must have a stable pointer.
|
||||||
self.flatpak_command = .{
|
self.flatpak_command = .{
|
||||||
.argv = self.args,
|
.argv = self.args,
|
||||||
@ -967,7 +1006,7 @@ const Subprocess = struct {
|
|||||||
errdefer killCommandFlatpak(cmd);
|
errdefer killCommandFlatpak(cmd);
|
||||||
|
|
||||||
log.info("started subcommand on host via flatpak API path={s} pid={?}", .{
|
log.info("started subcommand on host via flatpak API path={s} pid={?}", .{
|
||||||
self.path,
|
self.args[0],
|
||||||
pid,
|
pid,
|
||||||
});
|
});
|
||||||
|
|
||||||
@ -996,7 +1035,7 @@ const Subprocess = struct {
|
|||||||
|
|
||||||
// Build our subcommand
|
// Build our subcommand
|
||||||
var cmd: Command = .{
|
var cmd: Command = .{
|
||||||
.path = self.path,
|
.path = self.args[0],
|
||||||
.args = self.args,
|
.args = self.args,
|
||||||
.env = &self.env,
|
.env = &self.env,
|
||||||
.cwd = cwd,
|
.cwd = cwd,
|
||||||
@ -1017,7 +1056,7 @@ const Subprocess = struct {
|
|||||||
errdefer killCommand(&cmd) catch |err| {
|
errdefer killCommand(&cmd) catch |err| {
|
||||||
log.warn("error killing command during cleanup err={}", .{err});
|
log.warn("error killing command during cleanup err={}", .{err});
|
||||||
};
|
};
|
||||||
log.info("started subcommand path={s} pid={?}", .{ self.path, cmd.pid });
|
log.info("started subcommand path={s} pid={?}", .{ self.args[0], cmd.pid });
|
||||||
|
|
||||||
self.command = cmd;
|
self.command = cmd;
|
||||||
return switch (builtin.os.tag) {
|
return switch (builtin.os.tag) {
|
||||||
|
Reference in New Issue
Block a user