mirror of
https://github.com/ghostty-org/ghostty.git
synced 2026-06-07 12:24:24 +00:00
gtk: +new-window now respects --working-directory and -e (#10809)
Fixes: #8862 Fixes: #10716 This adds the machinery to pass configuration settings received over DBus down to the GObject Surface so that that configuration information can be used to override some settings from the current "live" config when creating a new window. Currently it's only possible to override `--working-directory`, `--command`, and `--title`. `-e` on the `ghostty +new-window` CLI works as well. Adding more overridable settings is possible, but being able to fully override any possible setting would better be served with a major revamp of how Ghostty handles configs, which is way out of scope at the moment.
This commit is contained in:
@@ -607,10 +607,14 @@ pub fn init(
|
||||
};
|
||||
|
||||
// The command we're going to execute
|
||||
const command: ?configpkg.Command = if (app.first)
|
||||
config.@"initial-command" orelse config.command
|
||||
else
|
||||
config.command;
|
||||
const command: ?configpkg.Command = command: {
|
||||
if (app.first) {
|
||||
if (config.@"initial-command") |command| {
|
||||
break :command command;
|
||||
}
|
||||
}
|
||||
break :command config.command;
|
||||
};
|
||||
|
||||
// Start our IO implementation
|
||||
// This separate block ({}) is important because our errdefers must
|
||||
|
||||
@@ -2,6 +2,7 @@ const Self = @This();
|
||||
|
||||
const std = @import("std");
|
||||
const apprt = @import("../../apprt.zig");
|
||||
const configpkg = @import("../../config.zig");
|
||||
const CoreSurface = @import("../../Surface.zig");
|
||||
const ApprtApp = @import("App.zig");
|
||||
const Application = @import("class/application.zig").Application;
|
||||
|
||||
@@ -22,6 +22,7 @@ const xev = @import("../../../global.zig").xev;
|
||||
const Binding = @import("../../../input.zig").Binding;
|
||||
const CoreConfig = configpkg.Config;
|
||||
const CoreSurface = @import("../../../Surface.zig");
|
||||
const lib = @import("../../../lib/main.zig");
|
||||
|
||||
const ext = @import("../ext.zig");
|
||||
const key = @import("../key.zig");
|
||||
@@ -709,6 +710,7 @@ pub const Application = extern struct {
|
||||
.app => null,
|
||||
.surface => |v| v,
|
||||
},
|
||||
.none,
|
||||
),
|
||||
|
||||
.open_config => return Action.openConfig(self),
|
||||
@@ -1669,17 +1671,30 @@ pub const Application = extern struct {
|
||||
) callconv(.c) void {
|
||||
log.debug("received new window action", .{});
|
||||
|
||||
parameter: {
|
||||
var arena: std.heap.ArenaAllocator = .init(Application.default().allocator());
|
||||
defer arena.deinit();
|
||||
|
||||
const alloc = arena.allocator();
|
||||
|
||||
var working_directory: ?[:0]const u8 = null;
|
||||
var title: ?[:0]const u8 = null;
|
||||
var command: ?configpkg.Command = null;
|
||||
var args: std.ArrayList([:0]const u8) = .empty;
|
||||
|
||||
overrides: {
|
||||
// were we given a parameter?
|
||||
const parameter = parameter_ orelse break :parameter;
|
||||
const parameter = parameter_ orelse break :overrides;
|
||||
|
||||
const as_variant_type = glib.VariantType.new("as");
|
||||
defer as_variant_type.free();
|
||||
|
||||
// ensure that the supplied parameter is an array of strings
|
||||
if (glib.Variant.isOfType(parameter, as_variant_type) == 0) {
|
||||
log.warn("parameter is of type {s}", .{parameter.getTypeString()});
|
||||
break :parameter;
|
||||
log.warn("parameter is of type '{s}', not '{s}'", .{
|
||||
parameter.getTypeString(),
|
||||
as_variant_type.peekString()[0..as_variant_type.getStringLength()],
|
||||
});
|
||||
break :overrides;
|
||||
}
|
||||
|
||||
const s_variant_type = glib.VariantType.new("s");
|
||||
@@ -1688,7 +1703,10 @@ pub const Application = extern struct {
|
||||
var it: glib.VariantIter = undefined;
|
||||
_ = it.init(parameter);
|
||||
|
||||
while (it.nextValue()) |value| {
|
||||
var e_seen: bool = false;
|
||||
var i: usize = 0;
|
||||
|
||||
while (it.nextValue()) |value| : (i += 1) {
|
||||
defer value.unref();
|
||||
|
||||
// just to be sure
|
||||
@@ -1698,13 +1716,64 @@ pub const Application = extern struct {
|
||||
const buf = value.getString(&len);
|
||||
const str = buf[0..len];
|
||||
|
||||
log.debug("new-window command argument: {s}", .{str});
|
||||
log.debug("new-window argument: {d} {s}", .{ i, str });
|
||||
|
||||
if (e_seen) {
|
||||
const cpy = alloc.dupeZ(u8, str) catch |err| {
|
||||
log.warn("unable to duplicate argument {d} {s}: {t}", .{ i, str, err });
|
||||
break :overrides;
|
||||
};
|
||||
args.append(alloc, cpy) catch |err| {
|
||||
log.warn("unable to append argument {d} {s}: {t}", .{ i, str, err });
|
||||
break :overrides;
|
||||
};
|
||||
continue;
|
||||
}
|
||||
|
||||
if (std.mem.eql(u8, str, "-e")) {
|
||||
e_seen = true;
|
||||
continue;
|
||||
}
|
||||
|
||||
if (lib.cutPrefix(u8, str, "--command=")) |v| {
|
||||
var cmd: configpkg.Command = undefined;
|
||||
cmd.parseCLI(alloc, v) catch |err| {
|
||||
log.warn("unable to parse command: {t}", .{err});
|
||||
continue;
|
||||
};
|
||||
command = cmd;
|
||||
continue;
|
||||
}
|
||||
if (lib.cutPrefix(u8, str, "--working-directory=")) |v| {
|
||||
working_directory = alloc.dupeZ(u8, std.mem.trim(u8, v, &std.ascii.whitespace)) catch |err| wd: {
|
||||
log.warn("unable to duplicate working directory: {t}", .{err});
|
||||
break :wd null;
|
||||
};
|
||||
continue;
|
||||
}
|
||||
if (lib.cutPrefix(u8, str, "--title=")) |v| {
|
||||
title = alloc.dupeZ(u8, std.mem.trim(u8, v, &std.ascii.whitespace)) catch |err| t: {
|
||||
log.warn("unable to duplicate title: {t}", .{err});
|
||||
break :t null;
|
||||
};
|
||||
continue;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
_ = self.core().mailbox.push(.{
|
||||
.new_window = .{},
|
||||
}, .{ .forever = {} });
|
||||
if (args.items.len > 0) {
|
||||
command = .{
|
||||
.direct = args.items,
|
||||
};
|
||||
}
|
||||
|
||||
Action.newWindow(self, null, .{
|
||||
.command = command,
|
||||
.working_directory = working_directory,
|
||||
.title = title,
|
||||
}) catch |err| {
|
||||
log.warn("unable to create new window: {t}", .{err});
|
||||
};
|
||||
}
|
||||
|
||||
pub fn actionOpenConfig(
|
||||
@@ -2151,6 +2220,13 @@ const Action = struct {
|
||||
pub fn newWindow(
|
||||
self: *Application,
|
||||
parent: ?*CoreSurface,
|
||||
overrides: struct {
|
||||
command: ?configpkg.Command = null,
|
||||
working_directory: ?[:0]const u8 = null,
|
||||
title: ?[:0]const u8 = null,
|
||||
|
||||
pub const none: @This() = .{};
|
||||
},
|
||||
) !void {
|
||||
// Note that we've requested a window at least once. This is used
|
||||
// to trigger quit on no windows. Note I'm not sure if this is REALLY
|
||||
@@ -2159,14 +2235,32 @@ const Action = struct {
|
||||
// was a delay in the event loop before we created a Window.
|
||||
self.private().requested_window = true;
|
||||
|
||||
const win = Window.new(self);
|
||||
initAndShowWindow(self, win, parent);
|
||||
const win = Window.new(self, .{
|
||||
.title = overrides.title,
|
||||
});
|
||||
initAndShowWindow(
|
||||
self,
|
||||
win,
|
||||
parent,
|
||||
.{
|
||||
.command = overrides.command,
|
||||
.working_directory = overrides.working_directory,
|
||||
.title = overrides.title,
|
||||
},
|
||||
);
|
||||
}
|
||||
|
||||
fn initAndShowWindow(
|
||||
self: *Application,
|
||||
win: *Window,
|
||||
parent: ?*CoreSurface,
|
||||
overrides: struct {
|
||||
command: ?configpkg.Command = null,
|
||||
working_directory: ?[:0]const u8 = null,
|
||||
title: ?[:0]const u8 = null,
|
||||
|
||||
pub const none: @This() = .{};
|
||||
},
|
||||
) void {
|
||||
// Setup a binding so that whenever our config changes so does the
|
||||
// window. There's never a time when the window config should be out
|
||||
@@ -2180,7 +2274,11 @@ const Action = struct {
|
||||
);
|
||||
|
||||
// Create a new tab with window context (first tab in new window)
|
||||
win.newTabForWindow(parent);
|
||||
win.newTabForWindow(parent, .{
|
||||
.command = overrides.command,
|
||||
.working_directory = overrides.working_directory,
|
||||
.title = overrides.title,
|
||||
});
|
||||
|
||||
// Estimate the initial window size before presenting so the window
|
||||
// manager can position it correctly.
|
||||
@@ -2506,7 +2604,7 @@ const Action = struct {
|
||||
.@"quick-terminal" = true,
|
||||
});
|
||||
assert(win.isQuickTerminal());
|
||||
initAndShowWindow(self, win, null);
|
||||
initAndShowWindow(self, win, null, .none);
|
||||
return true;
|
||||
}
|
||||
|
||||
|
||||
@@ -7,6 +7,7 @@ const glib = @import("glib");
|
||||
const gobject = @import("gobject");
|
||||
const gtk = @import("gtk");
|
||||
|
||||
const configpkg = @import("../../../config.zig");
|
||||
const apprt = @import("../../../apprt.zig");
|
||||
const ext = @import("../ext.zig");
|
||||
const gresource = @import("../build/gresource.zig");
|
||||
@@ -203,11 +204,22 @@ pub const SplitTree = extern struct {
|
||||
self: *Self,
|
||||
direction: Surface.Tree.Split.Direction,
|
||||
parent_: ?*Surface,
|
||||
overrides: struct {
|
||||
command: ?configpkg.Command = null,
|
||||
working_directory: ?[:0]const u8 = null,
|
||||
title: ?[:0]const u8 = null,
|
||||
|
||||
pub const none: @This() = .{};
|
||||
},
|
||||
) Allocator.Error!void {
|
||||
const alloc = Application.default().allocator();
|
||||
|
||||
// Create our new surface.
|
||||
const surface: *Surface = .new();
|
||||
const surface: *Surface = .new(.{
|
||||
.command = overrides.command,
|
||||
.working_directory = overrides.working_directory,
|
||||
.title = overrides.title,
|
||||
});
|
||||
defer surface.unref();
|
||||
_ = surface.refSink();
|
||||
|
||||
@@ -612,6 +624,7 @@ pub const SplitTree = extern struct {
|
||||
self.newSplit(
|
||||
direction,
|
||||
self.getActiveSurface(),
|
||||
.none,
|
||||
) catch |err| {
|
||||
log.warn("new split failed error={}", .{err});
|
||||
};
|
||||
|
||||
@@ -10,6 +10,7 @@ const gtk = @import("gtk");
|
||||
|
||||
const apprt = @import("../../../apprt.zig");
|
||||
const build_config = @import("../../../build_config.zig");
|
||||
const configpkg = @import("../../../config.zig");
|
||||
const datastruct = @import("../../../datastruct/main.zig");
|
||||
const font = @import("../../../font/main.zig");
|
||||
const input = @import("../../../input.zig");
|
||||
@@ -704,11 +705,33 @@ pub const Surface = extern struct {
|
||||
/// stops scrolling.
|
||||
pending_horizontal_scroll_reset: ?c_uint = null,
|
||||
|
||||
overrides: struct {
|
||||
command: ?configpkg.Command = null,
|
||||
working_directory: ?[:0]const u8 = null,
|
||||
|
||||
pub const none: @This() = .{};
|
||||
} = .none,
|
||||
|
||||
pub var offset: c_int = 0;
|
||||
};
|
||||
|
||||
pub fn new() *Self {
|
||||
return gobject.ext.newInstance(Self, .{});
|
||||
pub fn new(overrides: struct {
|
||||
command: ?configpkg.Command = null,
|
||||
working_directory: ?[:0]const u8 = null,
|
||||
title: ?[:0]const u8 = null,
|
||||
|
||||
pub const none: @This() = .{};
|
||||
}) *Self {
|
||||
const self = gobject.ext.newInstance(Self, .{
|
||||
.@"title-override" = overrides.title,
|
||||
});
|
||||
const alloc = Application.default().allocator();
|
||||
const priv: *Private = self.private();
|
||||
priv.overrides = .{
|
||||
.command = if (overrides.command) |c| c.clone(alloc) catch null else null,
|
||||
.working_directory = if (overrides.working_directory) |wd| alloc.dupeZ(u8, wd) catch null else null,
|
||||
};
|
||||
return self;
|
||||
}
|
||||
|
||||
pub fn core(self: *Self) ?*CoreSurface {
|
||||
@@ -1853,6 +1876,7 @@ pub const Surface = extern struct {
|
||||
}
|
||||
|
||||
fn finalize(self: *Self) callconv(.c) void {
|
||||
const alloc = Application.default().allocator();
|
||||
const priv = self.private();
|
||||
if (priv.core_surface) |v| {
|
||||
// Remove ourselves from the list of known surfaces in the app.
|
||||
@@ -1866,7 +1890,6 @@ pub const Surface = extern struct {
|
||||
|
||||
// Deinit the surface
|
||||
v.deinit();
|
||||
const alloc = Application.default().allocator();
|
||||
alloc.destroy(v);
|
||||
|
||||
priv.core_surface = null;
|
||||
@@ -1899,9 +1922,16 @@ pub const Surface = extern struct {
|
||||
glib.free(@ptrCast(@constCast(v)));
|
||||
priv.title_override = null;
|
||||
}
|
||||
if (priv.overrides.command) |c| {
|
||||
c.deinit(alloc);
|
||||
priv.overrides.command = null;
|
||||
}
|
||||
if (priv.overrides.working_directory) |wd| {
|
||||
alloc.free(wd);
|
||||
priv.overrides.working_directory = null;
|
||||
}
|
||||
|
||||
// Clean up key sequence and key table state
|
||||
const alloc = Application.default().allocator();
|
||||
for (priv.key_sequence.items) |s| alloc.free(s);
|
||||
priv.key_sequence.deinit(alloc);
|
||||
for (priv.key_tables.items) |s| alloc.free(s);
|
||||
@@ -3313,7 +3343,7 @@ pub const Surface = extern struct {
|
||||
};
|
||||
|
||||
fn initSurface(self: *Self) InitError!void {
|
||||
const priv = self.private();
|
||||
const priv: *Private = self.private();
|
||||
assert(priv.core_surface == null);
|
||||
const gl_area = priv.gl_area;
|
||||
|
||||
@@ -3346,6 +3376,13 @@ pub const Surface = extern struct {
|
||||
);
|
||||
defer config.deinit();
|
||||
|
||||
if (priv.overrides.command) |c| {
|
||||
config.command = try c.clone(config._arena.?.allocator());
|
||||
}
|
||||
if (priv.overrides.working_directory) |wd| {
|
||||
config.@"working-directory" = try config._arena.?.allocator().dupeZ(u8, wd);
|
||||
}
|
||||
|
||||
// Properties that can impact surface init
|
||||
if (priv.font_size_request) |size| config.@"font-size" = size.points;
|
||||
if (priv.pwd) |pwd| config.@"working-directory" = pwd;
|
||||
|
||||
@@ -5,6 +5,7 @@ const glib = @import("glib");
|
||||
const gobject = @import("gobject");
|
||||
const gtk = @import("gtk");
|
||||
|
||||
const configpkg = @import("../../../config.zig");
|
||||
const apprt = @import("../../../apprt.zig");
|
||||
const CoreSurface = @import("../../../Surface.zig");
|
||||
const ext = @import("../ext.zig");
|
||||
@@ -186,22 +187,34 @@ pub const Tab = extern struct {
|
||||
}
|
||||
}
|
||||
|
||||
fn init(self: *Self, _: *Class) callconv(.c) void {
|
||||
gtk.Widget.initTemplate(self.as(gtk.Widget));
|
||||
pub fn new(config: ?*Config, overrides: struct {
|
||||
command: ?configpkg.Command = null,
|
||||
working_directory: ?[:0]const u8 = null,
|
||||
title: ?[:0]const u8 = null,
|
||||
|
||||
// Init our actions
|
||||
self.initActionMap();
|
||||
pub const none: @This() = .{};
|
||||
}) *Self {
|
||||
const tab = gobject.ext.newInstance(Tab, .{});
|
||||
|
||||
const priv: *Private = tab.private();
|
||||
|
||||
if (config) |c| priv.config = c.ref();
|
||||
|
||||
// If our configuration is null then we get the configuration
|
||||
// from the application.
|
||||
const priv = self.private();
|
||||
if (priv.config == null) {
|
||||
const app = Application.default();
|
||||
priv.config = app.getConfig();
|
||||
}
|
||||
|
||||
tab.as(gobject.Object).notifyByPspec(properties.config.impl.param_spec);
|
||||
|
||||
// Create our initial surface in the split tree.
|
||||
priv.split_tree.newSplit(.right, null) catch |err| switch (err) {
|
||||
priv.split_tree.newSplit(.right, null, .{
|
||||
.command = overrides.command,
|
||||
.working_directory = overrides.working_directory,
|
||||
.title = overrides.title,
|
||||
}) catch |err| switch (err) {
|
||||
error.OutOfMemory => {
|
||||
// TODO: We should make our "no surfaces" state more aesthetically
|
||||
// pleasing and show something like an "Oops, something went wrong"
|
||||
@@ -209,6 +222,15 @@ pub const Tab = extern struct {
|
||||
@panic("oom");
|
||||
},
|
||||
};
|
||||
|
||||
return tab;
|
||||
}
|
||||
|
||||
fn init(self: *Self, _: *Class) callconv(.c) void {
|
||||
gtk.Widget.initTemplate(self.as(gtk.Widget));
|
||||
|
||||
// Init our actions
|
||||
self.initActionMap();
|
||||
}
|
||||
|
||||
fn initActionMap(self: *Self) void {
|
||||
|
||||
@@ -266,10 +266,27 @@ pub const Window = extern struct {
|
||||
pub var offset: c_int = 0;
|
||||
};
|
||||
|
||||
pub fn new(app: *Application) *Self {
|
||||
return gobject.ext.newInstance(Self, .{
|
||||
pub fn new(
|
||||
app: *Application,
|
||||
overrides: struct {
|
||||
title: ?[:0]const u8 = null,
|
||||
|
||||
pub const none: @This() = .{};
|
||||
},
|
||||
) *Self {
|
||||
const win = gobject.ext.newInstance(Self, .{
|
||||
.application = app,
|
||||
});
|
||||
|
||||
if (overrides.title) |title| {
|
||||
// If the overrides have a title set, we set that immediately
|
||||
// so that any applications inspecting the window states see an
|
||||
// immediate title set when the window appears, rather than waiting
|
||||
// possibly a few event loop ticks for it to sync from the surface.
|
||||
win.as(gtk.Window).setTitle(title);
|
||||
}
|
||||
|
||||
return win;
|
||||
}
|
||||
|
||||
fn init(self: *Self, _: *Class) callconv(.c) void {
|
||||
@@ -278,10 +295,14 @@ pub const Window = extern struct {
|
||||
// If our configuration is null then we get the configuration
|
||||
// from the application.
|
||||
const priv = self.private();
|
||||
if (priv.config == null) {
|
||||
|
||||
const config = config: {
|
||||
if (priv.config) |config| break :config config.get();
|
||||
const app = Application.default();
|
||||
priv.config = app.getConfig();
|
||||
}
|
||||
const config = app.getConfig();
|
||||
priv.config = config;
|
||||
break :config config.get();
|
||||
};
|
||||
|
||||
// We initialize our windowing protocol to none because we can't
|
||||
// actually initialize this until we get realized.
|
||||
@@ -305,17 +326,16 @@ pub const Window = extern struct {
|
||||
self.initActionMap();
|
||||
|
||||
// Start states based on config.
|
||||
if (priv.config) |config_obj| {
|
||||
const config = config_obj.get();
|
||||
if (config.maximize) self.as(gtk.Window).maximize();
|
||||
if (config.fullscreen != .false) self.as(gtk.Window).fullscreen();
|
||||
if (config.maximize) self.as(gtk.Window).maximize();
|
||||
if (config.fullscreen != .false) self.as(gtk.Window).fullscreen();
|
||||
|
||||
// If we have an explicit title set, we set that immediately
|
||||
// so that any applications inspecting the window states see
|
||||
// an immediate title set when the window appears, rather than
|
||||
// waiting possibly a few event loop ticks for it to sync from
|
||||
// the surface.
|
||||
if (config.title) |v| self.as(gtk.Window).setTitle(v);
|
||||
// If we have an explicit title set, we set that immediately
|
||||
// so that any applications inspecting the window states see
|
||||
// an immediate title set when the window appears, rather than
|
||||
// waiting possibly a few event loop ticks for it to sync from
|
||||
// the surface.
|
||||
if (config.title) |title| {
|
||||
self.as(gtk.Window).setTitle(title);
|
||||
}
|
||||
|
||||
// We always sync our appearance at the end because loading our
|
||||
@@ -368,21 +388,56 @@ pub const Window = extern struct {
|
||||
/// at the position dictated by the `window-new-tab-position` config.
|
||||
/// The new tab will be selected.
|
||||
pub fn newTab(self: *Self, parent_: ?*CoreSurface) void {
|
||||
_ = self.newTabPage(parent_, .tab);
|
||||
_ = self.newTabPage(parent_, .tab, .none);
|
||||
}
|
||||
|
||||
pub fn newTabForWindow(self: *Self, parent_: ?*CoreSurface) void {
|
||||
_ = self.newTabPage(parent_, .window);
|
||||
pub fn newTabForWindow(
|
||||
self: *Self,
|
||||
parent_: ?*CoreSurface,
|
||||
overrides: struct {
|
||||
command: ?configpkg.Command = null,
|
||||
working_directory: ?[:0]const u8 = null,
|
||||
title: ?[:0]const u8 = null,
|
||||
|
||||
pub const none: @This() = .{};
|
||||
},
|
||||
) void {
|
||||
_ = self.newTabPage(
|
||||
parent_,
|
||||
.window,
|
||||
.{
|
||||
.command = overrides.command,
|
||||
.working_directory = overrides.working_directory,
|
||||
.title = overrides.title,
|
||||
},
|
||||
);
|
||||
}
|
||||
|
||||
fn newTabPage(self: *Self, parent_: ?*CoreSurface, context: apprt.surface.NewSurfaceContext) *adw.TabPage {
|
||||
const priv = self.private();
|
||||
fn newTabPage(
|
||||
self: *Self,
|
||||
parent_: ?*CoreSurface,
|
||||
context: apprt.surface.NewSurfaceContext,
|
||||
overrides: struct {
|
||||
command: ?configpkg.Command = null,
|
||||
working_directory: ?[:0]const u8 = null,
|
||||
title: ?[:0]const u8 = null,
|
||||
|
||||
pub const none: @This() = .{};
|
||||
},
|
||||
) *adw.TabPage {
|
||||
const priv: *Private = self.private();
|
||||
const tab_view = priv.tab_view;
|
||||
|
||||
// Create our new tab object
|
||||
const tab = gobject.ext.newInstance(Tab, .{
|
||||
.config = priv.config,
|
||||
});
|
||||
const tab = Tab.new(
|
||||
priv.config,
|
||||
.{
|
||||
.command = overrides.command,
|
||||
.working_directory = overrides.working_directory,
|
||||
.title = overrides.title,
|
||||
},
|
||||
);
|
||||
|
||||
if (parent_) |p| {
|
||||
// For a new window's first tab, inherit the parent's initial size hints.
|
||||
if (context == .window) {
|
||||
@@ -1253,7 +1308,7 @@ pub const Window = extern struct {
|
||||
_: *adw.TabOverview,
|
||||
self: *Self,
|
||||
) callconv(.c) *adw.TabPage {
|
||||
return self.newTabPage(if (self.getActiveSurface()) |v| v.core() else null, .tab);
|
||||
return self.newTabPage(if (self.getActiveSurface()) |v| v.core() else null, .tab, .none);
|
||||
}
|
||||
|
||||
fn tabOverviewOpen(
|
||||
|
||||
@@ -18,7 +18,7 @@ const DBus = @import("DBus.zig");
|
||||
// `ghostty +new-window -e echo hello` would be equivalent to the following command (on a release build):
|
||||
//
|
||||
// ```
|
||||
// gdbus call --session --dest com.mitchellh.ghostty --object-path /com/mitchellh/ghostty --method org.gtk.Actions.Activate new-window-command '[<@as ["echo" "hello"]>]' []
|
||||
// gdbus call --session --dest com.mitchellh.ghostty --object-path /com/mitchellh/ghostty --method org.gtk.Actions.Activate new-window-command '[<@as ["-e" "echo" "hello"]>]' []
|
||||
// ```
|
||||
pub fn newWindow(alloc: Allocator, target: apprt.ipc.Target, value: apprt.ipc.Action.NewWindow) (Allocator.Error || std.Io.Writer.Error || apprt.ipc.Errors)!bool {
|
||||
var dbus = try DBus.init(
|
||||
@@ -32,10 +32,10 @@ pub fn newWindow(alloc: Allocator, target: apprt.ipc.Target, value: apprt.ipc.Ac
|
||||
defer dbus.deinit(alloc);
|
||||
|
||||
if (value.arguments) |arguments| {
|
||||
// If `-e` was specified on the command line, the first
|
||||
// parameter is an array of strings that contain the arguments
|
||||
// that came after `-e`, which will be interpreted as a command
|
||||
// to run.
|
||||
// If any arguments were specified on the command line, the first
|
||||
// parameter is an array of strings that contain the arguments. They
|
||||
// will be sent to the main Ghostty instance and interpreted as CLI
|
||||
// arguments.
|
||||
const as_variant_type = glib.VariantType.new("as");
|
||||
defer as_variant_type.free();
|
||||
|
||||
|
||||
@@ -5,6 +5,8 @@ const Action = @import("../cli.zig").ghostty.Action;
|
||||
const apprt = @import("../apprt.zig");
|
||||
const args = @import("args.zig");
|
||||
const diagnostics = @import("diagnostics.zig");
|
||||
const lib = @import("../lib/main.zig");
|
||||
const homedir = @import("../os/homedir.zig");
|
||||
|
||||
pub const Options = struct {
|
||||
/// This is set by the CLI parser for deinit.
|
||||
@@ -13,35 +15,63 @@ pub const Options = struct {
|
||||
/// If set, open up a new window in a custom instance of Ghostty.
|
||||
class: ?[:0]const u8 = null,
|
||||
|
||||
/// If `-e` is found in the arguments, this will contain all of the
|
||||
/// arguments to pass to Ghostty as the command.
|
||||
_arguments: ?[][: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 = .{},
|
||||
|
||||
/// Manual parse hook, used to deal with `-e`
|
||||
pub fn parseManuallyHook(self: *Options, alloc: Allocator, arg: []const u8, iter: anytype) Allocator.Error!bool {
|
||||
// If it's not `-e` continue with the standard argument parsning.
|
||||
if (!std.mem.eql(u8, arg, "-e")) return true;
|
||||
/// Manual parse hook, collect all of the arguments after `+new-window`.
|
||||
pub fn parseManuallyHook(self: *Options, alloc: Allocator, arg: []const u8, iter: anytype) (error{InvalidValue} || homedir.ExpandError || std.fs.Dir.RealPathAllocError || Allocator.Error)!bool {
|
||||
var e_seen: bool = std.mem.eql(u8, arg, "-e");
|
||||
|
||||
var arguments: std.ArrayList([:0]const u8) = .empty;
|
||||
errdefer {
|
||||
for (arguments.items) |argument| alloc.free(argument);
|
||||
arguments.deinit(alloc);
|
||||
}
|
||||
// Include the argument that triggered the manual parse hook.
|
||||
if (try self.checkArg(alloc, arg)) |a| try self._arguments.append(alloc, a);
|
||||
|
||||
// Otherwise gather up the rest of the arguments to use as the command.
|
||||
// Gather up the rest of the arguments to use as the command.
|
||||
while (iter.next()) |param| {
|
||||
try arguments.append(alloc, try alloc.dupeZ(u8, 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);
|
||||
}
|
||||
|
||||
self._arguments = try arguments.toOwnedSlice(alloc);
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
fn checkArg(self: *Options, alloc: Allocator, arg: []const u8) (error{InvalidValue} || homedir.ExpandError || std.fs.Dir.RealPathAllocError || Allocator.Error)!?[:0]const u8 {
|
||||
if (lib.cutPrefix(u8, arg, "--class=")) |rest| {
|
||||
self.class = try alloc.dupeZ(u8, std.mem.trim(u8, rest, &std.ascii.whitespace));
|
||||
return null;
|
||||
}
|
||||
|
||||
if (lib.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.fs.Dir = std.fs.cwd();
|
||||
var expandhome_buf: [std.fs.max_path_bytes]u8 = undefined;
|
||||
const expanded = try homedir.expandHome(stripped, &expandhome_buf);
|
||||
var realpath_buf: [std.fs.max_path_bytes]u8 = undefined;
|
||||
const realpath = try cwd.realpath(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;
|
||||
@@ -63,11 +93,23 @@ pub const Options = struct {
|
||||
/// and contact a running Ghostty instance that was configured with the same
|
||||
/// `class` as was given on the command line.
|
||||
///
|
||||
/// If the `-e` flag is included on the command line, any arguments that follow
|
||||
/// will be sent to the running Ghostty instance and used as the command to run
|
||||
/// in the new window rather than the default. If `-e` is not specified, Ghostty
|
||||
/// will use the default command (either specified with `command` in your config
|
||||
/// or your default shell as configured on your system).
|
||||
/// 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`, 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
|
||||
@@ -92,8 +134,16 @@ pub const Options = struct {
|
||||
/// * `--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.
|
||||
///
|
||||
/// * `--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 new window instead of the default command.
|
||||
/// 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 {
|
||||
@@ -143,11 +193,12 @@ fn runArgs(
|
||||
if (exit) return 1;
|
||||
}
|
||||
|
||||
if (opts._arguments) |arguments| {
|
||||
if (arguments.len == 0) {
|
||||
try stderr.print("The -e flag was specified on the command line, but no other arguments were found.\n", .{});
|
||||
return 1;
|
||||
}
|
||||
if (!opts._working_directory_seen) {
|
||||
const alloc = opts._arena.?.allocator();
|
||||
const cwd: std.fs.Dir = std.fs.cwd();
|
||||
var buf: [std.fs.max_path_bytes]u8 = undefined;
|
||||
const wd = try cwd.realpath(".", &buf);
|
||||
try opts._arguments.append(alloc, try std.fmt.allocPrintSentinel(alloc, "--working-directory={s}", .{wd}, 0));
|
||||
}
|
||||
|
||||
var arena = ArenaAllocator.init(alloc_gpa);
|
||||
@@ -159,7 +210,7 @@ fn runArgs(
|
||||
if (opts.class) |class| .{ .class = class } else .detect,
|
||||
.new_window,
|
||||
.{
|
||||
.arguments = opts._arguments,
|
||||
.arguments = if (opts._arguments.items.len == 0) null else opts._arguments.items,
|
||||
},
|
||||
) catch |err| switch (err) {
|
||||
error.IPCFailed => {
|
||||
|
||||
@@ -29,7 +29,7 @@ const file_load = @import("file_load.zig");
|
||||
const formatterpkg = @import("formatter.zig");
|
||||
const themepkg = @import("theme.zig");
|
||||
const url = @import("url.zig");
|
||||
const Key = @import("key.zig").Key;
|
||||
pub const Key = @import("key.zig").Key;
|
||||
const MetricModifier = fontpkg.Metrics.Modifier;
|
||||
const help_strings = @import("help_strings");
|
||||
pub const Command = @import("command.zig").Command;
|
||||
@@ -4793,8 +4793,8 @@ fn compatBoldIsBright(
|
||||
_ = alloc;
|
||||
assert(std.mem.eql(u8, key, "bold-is-bright"));
|
||||
|
||||
const set = cli.args.parseBool(value_ orelse "t") catch return false;
|
||||
if (set) {
|
||||
const isset = cli.args.parseBool(value_ orelse "t") catch return false;
|
||||
if (isset) {
|
||||
self.@"bold-color" = .bright;
|
||||
}
|
||||
|
||||
|
||||
@@ -165,6 +165,16 @@ pub const Command = union(enum) {
|
||||
};
|
||||
}
|
||||
|
||||
pub fn deinit(self: *const Self, alloc: Allocator) void {
|
||||
switch (self.*) {
|
||||
.shell => |v| alloc.free(v),
|
||||
.direct => |l| {
|
||||
for (l) |v| alloc.free(v);
|
||||
alloc.free(l);
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
pub fn formatEntry(self: Self, formatter: formatterpkg.EntryFormatter) !void {
|
||||
switch (self) {
|
||||
.shell => |v| try formatter.formatEntry([]const u8, v),
|
||||
|
||||
@@ -10,6 +10,7 @@ pub const String = types.String;
|
||||
pub const Struct = @import("struct.zig").Struct;
|
||||
pub const Target = @import("target.zig").Target;
|
||||
pub const TaggedUnion = unionpkg.TaggedUnion;
|
||||
pub const cutPrefix = @import("string.zig").cutPrefix;
|
||||
|
||||
test {
|
||||
std.testing.refAllDecls(@This());
|
||||
|
||||
15
src/lib/string.zig
Normal file
15
src/lib/string.zig
Normal file
@@ -0,0 +1,15 @@
|
||||
const std = @import("std");
|
||||
|
||||
// This is a copy of std.mem.cutPrefix from 0.16. Once Ghostty has been ported
|
||||
// to 0.16 this can be removed.
|
||||
|
||||
/// If slice starts with prefix, returns the rest of slice starting at
|
||||
/// prefix.len.
|
||||
pub fn cutPrefix(comptime T: type, slice: []const T, prefix: []const T) ?[]const T {
|
||||
return if (std.mem.startsWith(T, slice, prefix)) slice[prefix.len..] else null;
|
||||
}
|
||||
|
||||
test cutPrefix {
|
||||
try std.testing.expectEqualStrings("foo", cutPrefix(u8, "--example=foo", "--example=").?);
|
||||
try std.testing.expectEqual(null, cutPrefix(u8, "--example=foo", "-example="));
|
||||
}
|
||||
Reference in New Issue
Block a user