Merge branch 'main' into vi_VN

This commit is contained in:
Anh Thang
2026-03-05 08:56:57 +07:00
committed by GitHub
28 changed files with 1036 additions and 232 deletions

View File

@@ -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

View File

@@ -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;

View File

@@ -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;
}

View File

@@ -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");
@@ -157,11 +158,6 @@ pub const SplitTree = extern struct {
/// used to debounce updates.
rebuild_source: ?c_uint = null,
/// Tracks whether we want a rebuild to happen at the next tick
/// that our surface tree has no surfaces with parents. See the
/// propTree function for a lot more details.
rebuild_pending: bool,
/// Used to store state about a pending surface close for the
/// close dialog.
pending_close: ?Surface.Tree.Node.Handle,
@@ -208,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();
@@ -408,13 +415,6 @@ pub const SplitTree = extern struct {
self,
.{ .detail = "focused" },
);
_ = gobject.Object.signals.notify.connect(
surface.as(gtk.Widget),
*Self,
propSurfaceParent,
self,
.{ .detail = "parent" },
);
}
}
@@ -478,20 +478,6 @@ pub const SplitTree = extern struct {
return surface;
}
/// Returns whether any of the surfaces in the tree have a parent.
/// This is important because we can only rebuild the widget tree
/// when every surface has no parent.
fn getTreeHasParents(self: *Self) bool {
const tree: *const Surface.Tree = self.getTree() orelse &.empty;
var it = tree.iterator();
while (it.next()) |entry| {
const surface = entry.view;
if (surface.as(gtk.Widget).getParent() != null) return true;
}
return false;
}
pub fn getHasSurfaces(self: *Self) bool {
const tree: *const Surface.Tree = self.private().tree orelse &.empty;
return !tree.isEmpty();
@@ -638,6 +624,7 @@ pub const SplitTree = extern struct {
self.newSplit(
direction,
self.getActiveSurface(),
.none,
) catch |err| {
log.warn("new split failed error={}", .{err});
};
@@ -779,27 +766,6 @@ pub const SplitTree = extern struct {
self.as(gobject.Object).notifyByPspec(properties.@"active-surface".impl.param_spec);
}
fn propSurfaceParent(
_: *gtk.Widget,
_: *gobject.ParamSpec,
self: *Self,
) callconv(.c) void {
const priv = self.private();
// If we're not waiting to rebuild then ignore this.
if (!priv.rebuild_pending) return;
// If any parents still exist in our tree then don't do anything.
if (self.getTreeHasParents()) return;
// Schedule the rebuild. Note, I tried to do this immediately (not
// on an idle tick) and it didn't work and had obvious rendering
// glitches. Something to look into in the future.
assert(priv.rebuild_source == null);
priv.rebuild_pending = false;
priv.rebuild_source = glib.idleAdd(onRebuild, self);
}
fn propTree(
self: *Self,
_: *gobject.ParamSpec,
@@ -807,6 +773,12 @@ pub const SplitTree = extern struct {
) callconv(.c) void {
const priv = self.private();
// No matter what we notify
self.as(gobject.Object).freezeNotify();
defer self.as(gobject.Object).thawNotify();
self.as(gobject.Object).notifyByPspec(properties.@"has-surfaces".impl.param_spec);
self.as(gobject.Object).notifyByPspec(properties.@"is-zoomed".impl.param_spec);
// If we were planning a rebuild, always remove that so we can
// start from a clean slate.
if (priv.rebuild_source) |v| {
@@ -816,38 +788,22 @@ pub const SplitTree = extern struct {
priv.rebuild_source = null;
}
// We need to wait for all our previous surfaces to lose their
// parent before adding them to a new one. I'm not sure if its a GTK
// bug, but manually forcing an unparent of all prior surfaces AND
// adding them to a new parent in the same tick causes the GLArea
// to break (it seems). I didn't investigate too deeply.
//
// Note, we also can't just defer to an idle tick (via idleAdd) because
// sometimes it takes more than one tick for all our surfaces to
// lose their parent.
//
// To work around this issue, if we have any surfaces that have
// a parent, we set the build pending flag and wait for the tree
// to be fully parent-free before building.
priv.rebuild_pending = self.getTreeHasParents();
// Reset our prior bin. This will force all prior surfaces to
// unparent... eventually.
priv.tree_bin.setChild(null);
// If none of the surfaces we plan on drawing require an unparent
// then we can setup our tree immediately. Otherwise, it'll happen
// via the `propSurfaceParent` callback.
if (!priv.rebuild_pending and priv.rebuild_source == null) {
priv.rebuild_source = glib.idleAdd(
onRebuild,
self,
);
// If we transitioned to an empty tree, clear immediately instead of
// waiting for an idle callback. Delaying teardown can keep the last
// surface alive during shutdown if the main loop exits first.
if (priv.tree == null) {
priv.tree_bin.setChild(null);
return;
}
// Dependent properties
self.as(gobject.Object).notifyByPspec(properties.@"has-surfaces".impl.param_spec);
self.as(gobject.Object).notifyByPspec(properties.@"is-zoomed".impl.param_spec);
// Build on an idle callback so rapid tree changes are debounced.
// We keep the existing tree attached until the rebuild runs,
// which avoids transient empty frames.
assert(priv.rebuild_source == null);
priv.rebuild_source = glib.idleAdd(
onRebuild,
self,
);
}
fn onRebuild(ud: ?*anyopaque) callconv(.c) c_int {
@@ -857,22 +813,21 @@ pub const SplitTree = extern struct {
const priv = self.private();
priv.rebuild_source = null;
// Prior to rebuilding the tree, our surface tree must be
// comprised of fully orphaned surfaces.
assert(!self.getTreeHasParents());
// Rebuild our tree
const tree: *const Surface.Tree = self.private().tree orelse &.empty;
if (!tree.isEmpty()) {
priv.tree_bin.setChild(self.buildTree(
if (tree.isEmpty()) {
priv.tree_bin.setChild(null);
} else {
const built = self.buildTree(
tree,
tree.zoomed orelse .root,
));
);
defer built.deinit();
priv.tree_bin.setChild(built.widget);
}
// If we have a last focused surface, we need to refocus it, because
// during the frame between setting the bin to null and rebuilding,
// GTK will reset our focus state (as it should!)
// Replacing our tree widget hierarchy can reset focus state.
// If we have a last-focused surface, restore focus to it.
if (priv.last_focused.get()) |v| {
defer v.unref();
v.grabFocus();
@@ -889,26 +844,120 @@ pub const SplitTree = extern struct {
/// Builds the widget tree associated with a surface split tree.
///
/// The final returned widget is expected to be a floating reference,
/// ready to be attached to a parent widget.
/// Returned widgets are expected to be attached to a parent by the caller.
///
/// If `release_ref` is true then `widget` has an extra temporary
/// reference that must be released once it is parented in the rebuilt
/// tree.
const BuildTreeResult = struct {
widget: *gtk.Widget,
release_ref: bool,
pub fn initNew(widget: *gtk.Widget) BuildTreeResult {
return .{ .widget = widget, .release_ref = false };
}
pub fn initReused(widget: *gtk.Widget) BuildTreeResult {
// We add a temporary ref to the widget to ensure it doesn't
// get destroyed while we're rebuilding the tree and detaching
// it from its old parent. The caller is expected to release
// this ref once the widget is attached to its new parent.
_ = widget.as(gobject.Object).ref();
// Detach after we ref it so that this doesn't mark the
// widget for destruction.
detachWidget(widget);
return .{ .widget = widget, .release_ref = true };
}
pub fn deinit(self: BuildTreeResult) void {
// If we have to release a ref, do it.
if (self.release_ref) self.widget.as(gobject.Object).unref();
}
};
fn buildTree(
self: *Self,
tree: *const Surface.Tree,
current: Surface.Tree.Node.Handle,
) *gtk.Widget {
) BuildTreeResult {
return switch (tree.nodes[current.idx()]) {
.leaf => |v| gobject.ext.newInstance(SurfaceScrolledWindow, .{
.surface = v,
}).as(gtk.Widget),
.split => |s| SplitTreeSplit.new(
current,
&s,
self.buildTree(tree, s.left),
self.buildTree(tree, s.right),
).as(gtk.Widget),
.leaf => |v| leaf: {
const window = ext.getAncestor(
SurfaceScrolledWindow,
v.as(gtk.Widget),
) orelse {
// The surface isn't in a window already so we don't
// have to worry about reuse.
break :leaf .initNew(gobject.ext.newInstance(
SurfaceScrolledWindow,
.{ .surface = v },
).as(gtk.Widget));
};
// Keep this widget alive while we detach it from the
// old tree and adopt it into the new one.
break :leaf .initReused(window.as(gtk.Widget));
},
.split => |s| split: {
const left = self.buildTree(tree, s.left);
defer left.deinit();
const right = self.buildTree(tree, s.right);
defer right.deinit();
break :split .initNew(SplitTreeSplit.new(
current,
&s,
left.widget,
right.widget,
).as(gtk.Widget));
},
};
}
/// Detach a split widget from its current parent.
///
/// We intentionally use parent-specific child APIs when possible
/// (`GtkPaned.setStartChild/setEndChild`, `AdwBin.setChild`) instead of
/// calling `gtk.Widget.unparent` directly. Container implementations track
/// child pointers/properties internally, and those setters are the path
/// that keeps container state and notifications in sync.
fn detachWidget(widget: *gtk.Widget) void {
const parent = widget.getParent() orelse return;
// Surface will be in a paned when it is split.
if (gobject.ext.cast(gtk.Paned, parent)) |paned| {
if (paned.getStartChild()) |child| {
if (child == widget) {
paned.setStartChild(null);
return;
}
}
if (paned.getEndChild()) |child| {
if (child == widget) {
paned.setEndChild(null);
return;
}
}
}
// Surface will be in a bin when it is not split.
if (gobject.ext.cast(adw.Bin, parent)) |bin| {
if (bin.getChild()) |child| {
if (child == widget) {
bin.setChild(null);
return;
}
}
}
// Fallback for unexpected parents where we don't have a typed
// container API available.
widget.unparent();
}
//---------------------------------------------------------------
// Class

View File

@@ -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");
@@ -693,6 +694,10 @@ pub const Surface = extern struct {
/// Whether primary paste (middle-click paste) is enabled.
gtk_enable_primary_paste: bool = true,
/// True when a left mouse down was consumed purely for a focus change,
/// and the matching left mouse release should also be suppressed.
suppress_left_mouse_release: bool = false,
/// How much pending horizontal scroll do we have?
pending_horizontal_scroll: f64 = 0.0,
@@ -700,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 {
@@ -1849,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.
@@ -1862,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;
@@ -1895,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);
@@ -2733,13 +2767,21 @@ pub const Surface = extern struct {
// If we don't have focus, grab it.
const gl_area_widget = priv.gl_area.as(gtk.Widget);
if (gl_area_widget.hasFocus() == 0) {
const had_focus = gl_area_widget.hasFocus() != 0;
if (!had_focus) {
_ = gl_area_widget.grabFocus();
}
// Report the event
const button = translateMouseButton(gesture.as(gtk.GestureSingle).getCurrentButton());
// If this click is only transitioning split focus, suppress it so
// it doesn't get forwarded to the terminal as a mouse event.
if (!had_focus and button == .left) {
priv.suppress_left_mouse_release = true;
return;
}
if (button == .middle and !priv.gtk_enable_primary_paste) {
return;
}
@@ -2795,6 +2837,11 @@ pub const Surface = extern struct {
const gtk_mods = event.getModifierState();
const button = translateMouseButton(gesture.as(gtk.GestureSingle).getCurrentButton());
if (button == .left and priv.suppress_left_mouse_release) {
priv.suppress_left_mouse_release = false;
return;
}
if (button == .middle and !priv.gtk_enable_primary_paste) {
return;
}
@@ -3296,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;
@@ -3329,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;

View File

@@ -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 {

View File

@@ -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(

View File

@@ -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();

View File

@@ -424,19 +424,6 @@ pub fn init(b: *std.Build, appVersion: []const u8) !Config {
// show up properly in `--help`.
{
// These should default to `true` except on macOS because linking them
// to Ghostty statically when GTK is dynamically linked to them can
// cause crashes.
for (&[_][]const u8{
"fontconfig",
}) |dep| {
_ = b.systemIntegrationOption(
dep,
.{
.default = if (target.result.os.tag.isDarwin()) false else true,
},
);
}
// These dependencies we want to default false if we're on macOS.
// On macOS we don't want to use system libraries because we
// generally want a fat binary. This can be overridden with the
@@ -444,6 +431,7 @@ pub fn init(b: *std.Build, appVersion: []const u8) !Config {
for (&[_][]const u8{
"freetype",
"harfbuzz",
"fontconfig",
"libpng",
"zlib",
"oniguruma",

View File

@@ -200,13 +200,6 @@ pub fn add(
if (b.systemIntegrationOption("fontconfig", .{})) {
step.linkSystemLibrary2("fontconfig", dynamic_link_opts);
} else {
if (self.config.app_runtime == .gtk)
std.debug.print(
\\WARNING: Statically linking FontConfig when using the GTK app runtime is known
\\to cause crashes! It is HIGHLY recommended that Ghostty be dynamically linked
\\to the system FontConfig library.
\\
, .{});
step.linkLibrary(fontconfig_dep.artifact("fontconfig"));
try static_libs.append(
b.allocator,

View File

@@ -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 => {

View File

@@ -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;
@@ -3087,7 +3087,7 @@ keybind: Keybinds = .{},
/// the path is not absolute, it is considered relative to the directory of the
/// configuration file that it is referenced from, or from the current working
/// directory if this is used as a CLI flag. The path may be prefixed with `~/`
/// to reference the user's home directory. (GTK only)
/// to reference the user's home directory.
///
/// Available since: 1.2.0
@"bell-audio-path": ?Path = null,
@@ -3095,7 +3095,6 @@ keybind: Keybinds = .{},
/// If `audio` is an enabled bell feature, this is the volume to play the audio
/// file at (relative to the system volume). This is a floating point number
/// ranging from 0.0 (silence) to 1.0 (as loud as possible). The default is 0.5.
/// (GTK only)
///
/// Available since: 1.2.0
@"bell-audio-volume": f64 = 0.5,
@@ -4794,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;
}

View File

@@ -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),

View File

@@ -32,6 +32,20 @@ pub const Path = union(enum) {
return std.meta.eql(self, other);
}
/// ghostty_config_path_s
pub const C = extern struct {
path: [*:0]const u8,
optional: bool,
};
/// Returns the path as a C-compatible struct.
pub fn cval(self: Path) C {
return switch (self) {
.optional => |path| .{ .path = path.ptr, .optional = true },
.required => |path| .{ .path = path.ptr, .optional = false },
};
}
/// Parse the input and return a Path. A leading `?` indicates that the path
/// is _optional_ and an error should not be logged or displayed to the user
/// if that path does not exist. Otherwise the path is required and an error

View File

@@ -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
View 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="));
}

View File

@@ -55,4 +55,5 @@ pub const locales = [_][:0]const u8{
"lt",
"lv",
"vi",
"kk",
};

View File

@@ -141,10 +141,16 @@ _ghostty_deferred_init() {
# - False negative (with prompt_subst): PS1='$mark1'
[[ $PS1 == *$mark1* ]] || PS1=${mark1}${PS1}
[[ $PS1 == *$markB* ]] || PS1=${PS1}${markB}
# Handle multiline prompts by marking continuation lines as
# secondary by replacing newlines with being prefixed
# with k=s
if [[ $PS1 == *$'\n'* ]]; then
# Handle multiline prompts by marking newline-separated
# continuation lines with k=s (mark2). We skip the newline
# immediately after mark1 to avoid introducing a double
# newline due to OSC 133;A's fresh-line behavior.
if [[ $PS1 == ${mark1}$'\n'* ]]; then
builtin local rest=${PS1#${mark1}$'\n'}
if [[ $rest == *$'\n'* ]]; then
PS1=${mark1}$'\n'${rest//$'\n'/$'\n'${mark2}}
fi
elif [[ $PS1 == *$'\n'* ]]; then
PS1=${PS1//$'\n'/$'\n'${mark2}}
fi
@@ -239,6 +245,19 @@ _ghostty_deferred_init() {
builtin print -rnu $_ghostty_fd \$'\\e[0 q'"
fi
# Emit semantic prompt markers at line-init if PS1 doesn't contain our
# marks. This ensures the terminal sees prompt markers even if another
# plugin (like zinit or oh-my-posh) regenerated PS1 after our precmd ran.
# We use 133;P instead of 133;A to avoid fresh-line behavior which would
# disrupt the display since the prompt has already been drawn. We also
# emit 133;B to mark the input area, which is needed for click-to-move.
(( $+functions[_ghostty_zle_line_init] )) || _ghostty_zle_line_init() { builtin true; }
functions[_ghostty_zle_line_init]="
if [[ \$PS1 != *$'%{\\e]133;A'* ]]; then
builtin print -nu \$_ghostty_fd '\\e]133;P;k=i\\a\\e]133;B\\a'
fi
"${functions[_ghostty_zle_line_init]}
# Add Ghostty binary to PATH if the path feature is enabled
if [[ "$GHOSTTY_SHELL_FEATURES" == *"path"* ]] && [[ -n "$GHOSTTY_BIN_DIR" ]]; then
if [[ ":$PATH:" != *":$GHOSTTY_BIN_DIR:"* ]]; then