1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
|
const std = @import("std");
const Allocator = std.mem.Allocator;
const ArenaAllocator = std.heap.ArenaAllocator;
const Action = @import("../cli.zig").ghostty.Action;
const apprt = @import("../apprt.zig");
const args = @import("args.zig");
const diagnostics = @import("diagnostics.zig");
const homedir = @import("../os/homedir.zig");
const global = @import("../global.zig");
pub const Options = struct {
/// We store an I/O implementation to make maintenance easier.
_io: std.Io,
/// This is set by the CLI parser for deinit.
_arena: ?ArenaAllocator = null,
/// If set, open up a new window in a custom instance of Ghostty.
class: ?[:0]const u8 = null,
/// Did the user specify a `--working-directory` argument on the command line?
_working_directory_seen: bool = false,
/// All of the arguments after `+new-window`. They will be sent to Ghosttty
/// for processing.
_arguments: std.ArrayList([:0]const u8) = .empty,
/// Enable arg parsing diagnostics so that we don't get an error if
/// there is a "normal" config setting on the cli.
_diagnostics: diagnostics.DiagnosticList = .{},
pub const ParseManuallyHookError = error{InvalidValue} ||
homedir.ExpandError ||
std.Io.Dir.RealPathFileAllocError ||
Allocator.Error;
/// Manual parse hook, collect all of the arguments after `+new-window`.
pub fn parseManuallyHook(
self: *Options,
alloc: Allocator,
arg: []const u8,
iter: anytype,
) ParseManuallyHookError!bool {
var e_seen: bool = std.mem.eql(u8, arg, "-e");
// Include the argument that triggered the manual parse hook.
if (try self.checkArg(alloc, arg)) |a| try self._arguments.append(alloc, a);
// Gather up the rest of the arguments to use as the command.
while (iter.next()) |param| {
if (e_seen) {
try self._arguments.append(alloc, try alloc.dupeZ(u8, param));
continue;
}
if (std.mem.eql(u8, param, "-e")) {
e_seen = true;
try self._arguments.append(alloc, try alloc.dupeZ(u8, param));
continue;
}
if (try self.checkArg(alloc, param)) |a| try self._arguments.append(alloc, a);
}
return false;
}
const CheckArgError = error{InvalidValue} ||
homedir.ExpandError ||
std.Io.Dir.RealPathFileAllocError ||
Allocator.Error;
fn checkArg(self: *Options, alloc: Allocator, arg: []const u8) CheckArgError!?[:0]const u8 {
if (std.mem.cutPrefix(u8, arg, "--class=")) |rest| {
self.class = try alloc.dupeZ(u8, std.mem.trim(u8, rest, &std.ascii.whitespace));
return null;
}
if (std.mem.cutPrefix(u8, arg, "--working-directory=")) |rest| {
const stripped = std.mem.trim(u8, rest, &std.ascii.whitespace);
if (std.mem.eql(u8, stripped, "home")) return try alloc.dupeZ(u8, arg);
if (std.mem.eql(u8, stripped, "inherit")) return try alloc.dupeZ(u8, arg);
const cwd: std.Io.Dir = .cwd();
var expandhome_buf: [std.fs.max_path_bytes]u8 = undefined;
const expanded = expanded: {
var environ_map = try global.environMap();
defer environ_map.deinit();
break :expanded try homedir.expandHome(global.io(), &environ_map, stripped, &expandhome_buf);
};
var realpath_buf: [std.fs.max_path_bytes]u8 = undefined;
const realpath = realpath_buf[0..try cwd.realPathFile(self._io, expanded, &realpath_buf)];
self._working_directory_seen = true;
return try std.fmt.allocPrintSentinel(alloc, "--working-directory={s}", .{realpath}, 0);
}
return try alloc.dupeZ(u8, arg);
}
pub fn deinit(self: *Options) void {
if (self._arena) |arena| arena.deinit();
self.* = undefined;
}
/// Enables "-h" and "--help" to work.
pub fn help(self: Options) !void {
_ = self;
return Action.help_error;
}
};
/// The `new-window` will use native platform IPC to open up a new window in a
/// running instance of Ghostty.
///
/// If the `--class` flag is not set, the `new-window` command will try and
/// connect to a running instance of Ghostty based on what optimizations the
/// Ghostty CLI was compiled with. Otherwise the `new-window` command will try
/// and contact a running Ghostty instance that was configured with the same
/// `class` as was given on the command line.
///
/// All of the arguments after the `+new-window` argument (except for the
/// `--class` flag) will be sent to the remote Ghostty instance and will be
/// parsed as command line flags. These flags will override certain settings
/// when creating the first surface in the new window. Currently, only
/// `--working-directory`, `--command`, `--shell-integration=<mode>`, and
/// `--title` are supported. `-e` will also work as an alias for `--command`, except that if
/// `-e` is found on the command line all following arguments will become part
/// of the command and no more arguments will be parsed for configuration
/// settings.
///
/// If `--working-directory` is found on the command line and is a relative
/// path (i.e. doesn't start with `/`) it will be resolved to an absolute path
/// relative to the current working directory that the `ghostty +new-window`
/// command is run from. `~/` prefixes will also be expanded to the user's home
/// directory.
///
/// If `--working-directory` is _not_ found on the command line, the working
/// directory that `ghostty +new-window` is run from will be passed to Ghostty.
///
/// GTK uses an application ID to identify instances of applications. If Ghostty
/// is compiled with release optimizations, the default application ID will be
/// `com.mitchellh.ghostty`. If Ghostty is compiled with debug optimizations,
/// the default application ID will be `com.mitchellh.ghostty-debug`. The
/// `class` configuration entry can be used to set up a custom application
/// ID. The class name must follow the requirements defined [in the GTK
/// documentation](https://docs.gtk.org/gio/type_func.Application.id_is_valid.html)
/// or it will be ignored and Ghostty will use the default as defined above.
///
/// On GTK, D-Bus activation must be properly configured. Ghostty does not need
/// to be running for this to open a new window, making it suitable for binding
/// to keys in your window manager (if other methods for configuring global
/// shortcuts are unavailable). D-Bus will handle launching a new instance
/// of Ghostty if it is not already running. See the Ghostty website for
/// information on properly configuring D-Bus activation.
///
/// Only supported on GTK.
///
/// Flags:
///
/// * `--class=<class>`: If set, open up a new window in a custom instance of
/// Ghostty. The class must be a valid GTK application ID.
///
/// * `--command`: The command to be executed in the first surface of the new window.
///
/// * `--shell-integration=<mode>`: Whether to enable shell integration and
/// which shell to use. See the main configuration documentation for
/// supported values.
///
/// * `--working-directory=<directory>`: The working directory to pass to Ghostty.
///
/// * `--title`: A title that will override the title of the first surface in
/// the new window. The title override may be edited or removed later.
///
/// * `-e`: Any arguments after this will be interpreted as a command to
/// execute inside the first surface of the new window instead of the
/// default command.
///
/// Available since: 1.2.0
pub fn run(alloc: Allocator) !u8 {
var iter = try args.argsIterator(alloc, global.args());
defer iter.deinit();
var buffer: [1024]u8 = undefined;
var stderr_writer = std.Io.File.stderr().writer(global.io(), &buffer);
const stderr = &stderr_writer.interface;
const result = runArgs(alloc, &iter, stderr);
stderr.flush() catch {};
return result;
}
fn runArgs(
alloc_gpa: Allocator,
argsIter: anytype,
stderr: *std.Io.Writer,
) !u8 {
var opts: Options = .{ ._io = global.io() };
defer opts.deinit();
args.parse(Options, alloc_gpa, &opts, argsIter) catch |err| switch (err) {
error.ActionHelpRequested => return err,
else => {
try stderr.print("Error parsing args: {}\n", .{err});
return 1;
},
};
// Print out any diagnostics, unless it's likely that the diagnostic was
// generated trying to parse a "normal" configuration setting. Exit with an
// error code if any diagnostics were printed.
if (!opts._diagnostics.empty()) {
var exit: bool = false;
outer: for (opts._diagnostics.items()) |diagnostic| {
if (diagnostic.location != .cli) continue :outer;
inner: inline for (@typeInfo(Options).@"struct".fields) |field| {
if (field.name[0] == '_') continue :inner;
if (std.mem.eql(u8, field.name, diagnostic.key)) {
try stderr.print("config error: {f}\n", .{diagnostic});
exit = true;
}
}
}
if (exit) return 1;
}
if (!opts._working_directory_seen) {
const alloc = opts._arena.?.allocator();
const cwd: std.Io.Dir = .cwd();
var buf: [std.fs.max_path_bytes]u8 = undefined;
const wd = buf[0..try cwd.realPathFile(global.io(), ".", &buf)];
// This should be inserted at the beginning of the list, just in case `-e` was used.
try opts._arguments.insert(alloc, 0, try std.fmt.allocPrintSentinel(
alloc,
"--working-directory={s}",
.{wd},
0,
));
}
var arena = ArenaAllocator.init(alloc_gpa);
defer arena.deinit();
const alloc = arena.allocator();
if (apprt.App.performIpc(
alloc,
if (opts.class) |class| .{ .class = class } else .detect,
.new_window,
.{
.arguments = if (opts._arguments.items.len == 0) null else opts._arguments.items,
},
) catch |err| switch (err) {
error.IPCFailed => {
// The apprt should have printed a more specific error message
// already.
return 1;
},
else => {
try stderr.print("Sending the IPC failed: {}", .{err});
return 1;
},
}) return 0;
// If we get here, the platform is not supported.
try stderr.print("+new-window is not supported on this platform.\n", .{});
return 1;
}
|