diff --git a/dist/linux/app.desktop b/dist/linux/app.desktop index a06032d78..99a005750 100644 --- a/dist/linux/app.desktop +++ b/dist/linux/app.desktop @@ -2,7 +2,7 @@ Name=Ghostty Type=Application Comment=A terminal emulator -Exec=/usr/bin/ghostty +Exec=/app/bin/ghostty Icon=com.mitchellh.ghostty Keywords=terminal;tty;pty; StartupNotify=true diff --git a/src/Command.zig b/src/Command.zig index ba11090d5..f5607b200 100644 --- a/src/Command.zig +++ b/src/Command.zig @@ -24,6 +24,7 @@ const Command = @This(); const std = @import("std"); const builtin = @import("builtin"); const TempDir = @import("TempDir.zig"); +const internal_os = @import("os/main.zig"); const mem = std.mem; const os = std.os; const debug = std.debug; diff --git a/src/Pty.zig b/src/Pty.zig index c61c9feb9..230aa67ff 100644 --- a/src/Pty.zig +++ b/src/Pty.zig @@ -104,12 +104,15 @@ pub fn childPreExec(self: Pty) !void { if (setsid() < 0) return error.ProcessGroupFailed; // Set controlling terminal - switch (std.os.system.getErrno(c.ioctl(self.slave, TIOCSCTTY, @as(c_ulong, 0)))) { - .SUCCESS => {}, - else => |err| { - log.err("error setting controlling terminal errno={}", .{err}); - return error.SetControllingTerminalFailed; - }, + // TODO: maybe + if (!@import("os/main.zig").isFlatpak()) { + switch (std.os.system.getErrno(c.ioctl(self.slave, TIOCSCTTY, @as(c_ulong, 0)))) { + .SUCCESS => {}, + else => |err| { + log.err("error setting controlling terminal errno={}", .{err}); + return error.SetControllingTerminalFailed; + }, + } } // Can close master/slave pair now diff --git a/src/os/main.zig b/src/os/main.zig index 132764dd7..070e7be1d 100644 --- a/src/os/main.zig +++ b/src/os/main.zig @@ -2,6 +2,7 @@ //! system. pub usingnamespace @import("file.zig"); +pub usingnamespace @import("flatpak.zig"); pub usingnamespace @import("locale.zig"); pub usingnamespace @import("macos_version.zig"); pub usingnamespace @import("mouse.zig"); diff --git a/src/termio/Exec.zig b/src/termio/Exec.zig index 86dcf71e8..1d3c1c195 100644 --- a/src/termio/Exec.zig +++ b/src/termio/Exec.zig @@ -6,6 +6,7 @@ const builtin = @import("builtin"); const build_config = @import("../build_config.zig"); const assert = std.debug.assert; const Allocator = std.mem.Allocator; +const EnvMap = std.process.EnvMap; const termio = @import("../termio.zig"); const Command = @import("../Command.zig"); const Pty = @import("../Pty.zig"); @@ -17,6 +18,7 @@ const tracy = @import("tracy"); const trace = tracy.trace; const apprt = @import("../apprt.zig"); const fastmem = @import("../fastmem.zig"); +const internal_os = @import("../os/main.zig"); const log = std.log.scoped(.io_exec); @@ -90,7 +92,7 @@ pub fn init(alloc: Allocator, opts: termio.Options) !Exec { } pub fn deinit(self: *Exec) void { - self.subprocess.deinit(self.alloc); + self.subprocess.deinit(); // Clean up our other members self.terminal.deinit(self.alloc); @@ -139,6 +141,7 @@ pub fn threadEnter(self: *Exec, thread: *termio.Thread) !ThreadData { // Store our data so our callbacks can access it self.data = ev_data_ptr; + errdefer self.data = null; // Start our reader thread const read_thread = try std.Thread.spawn( @@ -319,10 +322,11 @@ fn ttyWrite( /// Subprocess manages the lifecycle of the shell subprocess. const Subprocess = struct { + arena: std.heap.ArenaAllocator, cwd: ?[]const u8, - env: std.process.EnvMap, + env: EnvMap, path: []const u8, - argv0_override: ?[]const u8, + args: [][]const u8, grid_size: renderer.GridSize, screen_size: renderer.ScreenSize, pty: ?Pty = null, @@ -330,11 +334,16 @@ const Subprocess = struct { /// Initialize the subprocess. This will NOT start it, this only sets /// up the internal state necessary to start it later. - pub fn init(alloc: Allocator, opts: termio.Options) !Subprocess { + pub fn init(gpa: Allocator, opts: termio.Options) !Subprocess { + // We have a lot of maybe-allocations that all share the same lifetime + // so use an arena so we don't end up in an accounting nightmare. + var arena = std.heap.ArenaAllocator.init(gpa); + errdefer arena.deinit(); + const alloc = arena.allocator(); + // Determine the path to the binary we're executing const path = (try Command.expandPath(alloc, opts.config.command orelse "sh")) orelse return error.CommandNotFound; - errdefer alloc.free(path); // 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 @@ -354,10 +363,12 @@ const Subprocess = struct { std.mem.copy(u8, argv0_buf[1..], argv0); break :argv0 argv0_buf; } else null; - errdefer if (argv0_override) |buf| alloc.free(buf); // Set our env vars - var env = try std.process.getEnvMap(alloc); + var env = if (internal_os.isFlatpak()) + EnvMap.init(alloc) + else + try std.process.getEnvMap(alloc); errdefer env.deinit(); try env.put("TERM", "xterm-256color"); try env.put("COLORTERM", "truecolor"); @@ -377,22 +388,47 @@ const Subprocess = struct { } } + // If we're NOT in a flatpak (usually!), then we just exec the + // process directly. If we are in a flatpak, we use flatpak-spawn + // to escape the sandbox. + const args = if (!internal_os.isFlatpak()) &[_][]const u8{ + argv0_override orelse path, + } else args: { + var args = try std.ArrayList([]const u8).initCapacity(alloc, 8); + defer args.deinit(); + + try args.append("/usr/bin/flatpak-spawn"); + try args.append("--host"); + try args.append("--watch-bus"); + var env_it = env.iterator(); + while (env_it.next()) |pair| { + try args.append(try std.fmt.allocPrint( + alloc, + "--env={s}={s}", + .{ pair.key_ptr.*, pair.value_ptr.* }, + )); + } + try args.append(path); + + break :args try args.toOwnedSlice(); + }; + return .{ + .arena = arena, .env = env, .cwd = opts.config.@"working-directory", - .path = path, - .argv0_override = argv0_override, + .path = if (internal_os.isFlatpak()) "/usr/bin/flatpak-spawn" else path, + .args = args, .grid_size = opts.grid_size, .screen_size = opts.screen_size, }; } /// Clean up the subprocess. This will stop the subprocess if it is started. - pub fn deinit(self: *Subprocess, alloc: Allocator) void { + pub fn deinit(self: *Subprocess) void { self.stop(); self.env.deinit(); - alloc.free(self.path); - if (self.argv0_override) |v| alloc.free(v); + self.arena.deinit(); self.* = undefined; } @@ -414,12 +450,15 @@ const Subprocess = struct { self.pty = null; } - const args = &[_][]const u8{self.argv0_override orelse self.path}; + log.debug("starting command path={s} args={s}", .{ + self.path, + self.args, + }); // Build our subcommand var cmd: Command = .{ .path = self.path, - .args = args, + .args = self.args, .env = &self.env, .cwd = self.cwd, .stdin = .{ .handle = pty.slave },