apprt/gtk-ng: prompt surface title
parent
a843929d5a
commit
8edc041eaf
|
|
@ -43,6 +43,7 @@ pub const blueprints: []const Blueprint = &.{
|
|||
.{ .major = 1, .minor = 5, .name = "split-tree" },
|
||||
.{ .major = 1, .minor = 5, .name = "split-tree-split" },
|
||||
.{ .major = 1, .minor = 2, .name = "surface" },
|
||||
.{ .major = 1, .minor = 5, .name = "surface-title-dialog" },
|
||||
.{ .major = 1, .minor = 3, .name = "surface-child-exited" },
|
||||
.{ .major = 1, .minor = 5, .name = "tab" },
|
||||
.{ .major = 1, .minor = 5, .name = "window" },
|
||||
|
|
|
|||
|
|
@ -589,6 +589,8 @@ pub const Application = extern struct {
|
|||
|
||||
.progress_report => return Action.progressReport(target, value),
|
||||
|
||||
.prompt_title => return Action.promptTitle(target),
|
||||
|
||||
.quit => self.quit(),
|
||||
|
||||
.quit_timer => try Action.quitTimer(self, value),
|
||||
|
|
@ -618,7 +620,6 @@ pub const Application = extern struct {
|
|||
.toggle_split_zoom => return Action.toggleSplitZoom(target),
|
||||
|
||||
// Unimplemented but todo on gtk-ng branch
|
||||
.prompt_title,
|
||||
.inspector,
|
||||
=> {
|
||||
log.warn("unimplemented action={}", .{action});
|
||||
|
|
@ -1955,6 +1956,16 @@ const Action = struct {
|
|||
};
|
||||
}
|
||||
|
||||
pub fn promptTitle(target: apprt.Target) bool {
|
||||
switch (target) {
|
||||
.app => return false,
|
||||
.surface => |v| {
|
||||
v.rt_surface.surface.promptTitle();
|
||||
return true;
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
/// Reload the configuration for the application and propagate it
|
||||
/// across the entire application and all terminals.
|
||||
pub fn reloadConfig(
|
||||
|
|
|
|||
|
|
@ -27,6 +27,7 @@ const Config = @import("config.zig").Config;
|
|||
const ResizeOverlay = @import("resize_overlay.zig").ResizeOverlay;
|
||||
const ChildExited = @import("surface_child_exited.zig").SurfaceChildExited;
|
||||
const ClipboardConfirmationDialog = @import("clipboard_confirmation_dialog.zig").ClipboardConfirmationDialog;
|
||||
const TitleDialog = @import("surface_title_dialog.zig").SurfaceTitleDialog;
|
||||
const Window = @import("window.zig").Window;
|
||||
|
||||
const log = std.log.scoped(.gtk_ghostty_surface);
|
||||
|
|
@ -186,8 +187,6 @@ pub const Surface = extern struct {
|
|||
|
||||
pub const @"mouse-hover-url" = struct {
|
||||
pub const name = "mouse-hover-url";
|
||||
pub const get = impl.get;
|
||||
pub const set = impl.set;
|
||||
const impl = gobject.ext.defineProperty(
|
||||
name,
|
||||
Self,
|
||||
|
|
@ -201,8 +200,6 @@ pub const Surface = extern struct {
|
|||
|
||||
pub const pwd = struct {
|
||||
pub const name = "pwd";
|
||||
pub const get = impl.get;
|
||||
pub const set = impl.set;
|
||||
const impl = gobject.ext.defineProperty(
|
||||
name,
|
||||
Self,
|
||||
|
|
@ -216,8 +213,6 @@ pub const Surface = extern struct {
|
|||
|
||||
pub const title = struct {
|
||||
pub const name = "title";
|
||||
pub const get = impl.get;
|
||||
pub const set = impl.set;
|
||||
const impl = gobject.ext.defineProperty(
|
||||
name,
|
||||
Self,
|
||||
|
|
@ -229,6 +224,19 @@ pub const Surface = extern struct {
|
|||
);
|
||||
};
|
||||
|
||||
pub const @"title-override" = struct {
|
||||
pub const name = "title-override";
|
||||
const impl = gobject.ext.defineProperty(
|
||||
name,
|
||||
Self,
|
||||
?[:0]const u8,
|
||||
.{
|
||||
.default = null,
|
||||
.accessor = C.privateStringFieldAccessor("title_override"),
|
||||
},
|
||||
);
|
||||
};
|
||||
|
||||
pub const zoom = struct {
|
||||
pub const name = "zoom";
|
||||
const impl = gobject.ext.defineProperty(
|
||||
|
|
@ -401,6 +409,9 @@ pub const Surface = extern struct {
|
|||
/// The title of this surface, if any has been set.
|
||||
title: ?[:0]const u8 = null,
|
||||
|
||||
/// The manually overridden title of this surface from `promptTitle`.
|
||||
title_override: ?[:0]const u8 = null,
|
||||
|
||||
/// The current focus state of the terminal based on the
|
||||
/// focus events.
|
||||
focused: bool = true,
|
||||
|
|
@ -883,6 +894,26 @@ pub const Surface = extern struct {
|
|||
return false;
|
||||
}
|
||||
|
||||
/// Prompt for a manual title change for the surface.
|
||||
pub fn promptTitle(self: *Self) void {
|
||||
const priv = self.private();
|
||||
const dialog = gobject.ext.newInstance(
|
||||
TitleDialog,
|
||||
.{
|
||||
.@"initial-value" = priv.title_override orelse priv.title,
|
||||
},
|
||||
);
|
||||
_ = TitleDialog.signals.set.connect(
|
||||
dialog,
|
||||
*Self,
|
||||
titleDialogSet,
|
||||
self,
|
||||
.{},
|
||||
);
|
||||
|
||||
dialog.present(self.as(gtk.Widget));
|
||||
}
|
||||
|
||||
/// Scale x/y by the GDK device scale.
|
||||
fn scaledCoordinates(
|
||||
self: *Self,
|
||||
|
|
@ -1145,6 +1176,9 @@ pub const Surface = extern struct {
|
|||
fn init(self: *Self, _: *Class) callconv(.c) void {
|
||||
gtk.Widget.initTemplate(self.as(gtk.Widget));
|
||||
|
||||
// Initialize our actions
|
||||
self.initActions();
|
||||
|
||||
const priv = self.private();
|
||||
|
||||
// Initialize some private fields so they aren't undefined
|
||||
|
|
@ -1191,6 +1225,45 @@ pub const Surface = extern struct {
|
|||
self.propConfig(undefined, null);
|
||||
}
|
||||
|
||||
fn initActions(self: *Self) void {
|
||||
// The set of actions. Each action has (in order):
|
||||
// [0] The action name
|
||||
// [1] The callback function
|
||||
// [2] The glib.VariantType of the parameter
|
||||
//
|
||||
// For action names:
|
||||
// https://docs.gtk.org/gio/type_func.Action.name_is_valid.html
|
||||
const actions = .{
|
||||
.{ "prompt-title", actionPromptTitle, null },
|
||||
};
|
||||
|
||||
// We need to collect our actions into a group since we're just
|
||||
// a plain widget that doesn't implement ActionGroup directly.
|
||||
const group = gio.SimpleActionGroup.new();
|
||||
errdefer group.unref();
|
||||
const map = group.as(gio.ActionMap);
|
||||
inline for (actions) |entry| {
|
||||
const action = gio.SimpleAction.new(
|
||||
entry[0],
|
||||
entry[2],
|
||||
);
|
||||
defer action.unref();
|
||||
_ = gio.SimpleAction.signals.activate.connect(
|
||||
action,
|
||||
*Self,
|
||||
entry[1],
|
||||
self,
|
||||
.{},
|
||||
);
|
||||
map.addAction(action.as(gio.Action));
|
||||
}
|
||||
|
||||
self.as(gtk.Widget).insertActionGroup(
|
||||
"surface",
|
||||
group.as(gio.ActionGroup),
|
||||
);
|
||||
}
|
||||
|
||||
fn dispose(self: *Self) callconv(.c) void {
|
||||
const priv = self.private();
|
||||
if (priv.config) |v| {
|
||||
|
|
@ -1254,6 +1327,10 @@ pub const Surface = extern struct {
|
|||
glib.free(@constCast(@ptrCast(v)));
|
||||
priv.title = null;
|
||||
}
|
||||
if (priv.title_override) |v| {
|
||||
glib.free(@constCast(@ptrCast(v)));
|
||||
priv.title_override = null;
|
||||
}
|
||||
self.clearCgroup();
|
||||
|
||||
gobject.Object.virtual_methods.finalize.call(
|
||||
|
|
@ -1270,7 +1347,9 @@ pub const Surface = extern struct {
|
|||
return self.private().title;
|
||||
}
|
||||
|
||||
/// Set the title for this surface, copies the value.
|
||||
/// Set the title for this surface, copies the value. This should always
|
||||
/// be the title as set by the terminal program, not any manually set
|
||||
/// title. For manually set titles see `setTitleOverride`.
|
||||
pub fn setTitle(self: *Self, title: ?[:0]const u8) void {
|
||||
const priv = self.private();
|
||||
if (priv.title) |v| glib.free(@constCast(@ptrCast(v)));
|
||||
|
|
@ -1279,6 +1358,16 @@ pub const Surface = extern struct {
|
|||
self.as(gobject.Object).notifyByPspec(properties.title.impl.param_spec);
|
||||
}
|
||||
|
||||
/// Overridden title. This will be generally be shown over the title
|
||||
/// unless this is unset (null).
|
||||
pub fn setTitleOverride(self: *Self, title: ?[:0]const u8) void {
|
||||
const priv = self.private();
|
||||
if (priv.title_override) |v| glib.free(@constCast(@ptrCast(v)));
|
||||
priv.title_override = null;
|
||||
if (title) |v| priv.title_override = glib.ext.dupeZ(u8, v);
|
||||
self.as(gobject.Object).notifyByPspec(properties.@"title-override".impl.param_spec);
|
||||
}
|
||||
|
||||
/// Returns the pwd property without a copy.
|
||||
pub fn getPwd(self: *Self) ?[:0]const u8 {
|
||||
return self.private().pwd;
|
||||
|
|
@ -1582,6 +1671,17 @@ pub const Surface = extern struct {
|
|||
//---------------------------------------------------------------
|
||||
// Signal Handlers
|
||||
|
||||
pub fn actionPromptTitle(
|
||||
_: *gio.SimpleAction,
|
||||
_: ?*glib.Variant,
|
||||
self: *Self,
|
||||
) callconv(.c) void {
|
||||
const surface = self.core() orelse return;
|
||||
_ = surface.performBindingAction(.prompt_surface_title) catch |err| {
|
||||
log.warn("unable to perform prompt title action err={}", .{err});
|
||||
};
|
||||
}
|
||||
|
||||
fn childExitedClose(
|
||||
_: *ChildExited,
|
||||
self: *Self,
|
||||
|
|
@ -2413,6 +2513,15 @@ pub const Surface = extern struct {
|
|||
media_file.unref();
|
||||
}
|
||||
|
||||
fn titleDialogSet(
|
||||
_: *TitleDialog,
|
||||
title_ptr: [*:0]const u8,
|
||||
self: *Self,
|
||||
) callconv(.c) void {
|
||||
const title = std.mem.span(title_ptr);
|
||||
self.setTitleOverride(if (title.len == 0) null else title);
|
||||
}
|
||||
|
||||
const C = Common(Self, Private);
|
||||
pub const as = C.as;
|
||||
pub const ref = C.ref;
|
||||
|
|
@ -2493,6 +2602,7 @@ pub const Surface = extern struct {
|
|||
properties.@"mouse-hover-url".impl,
|
||||
properties.pwd.impl,
|
||||
properties.title.impl,
|
||||
properties.@"title-override".impl,
|
||||
properties.zoom.impl,
|
||||
});
|
||||
|
||||
|
|
|
|||
|
|
@ -0,0 +1,190 @@
|
|||
const std = @import("std");
|
||||
const adw = @import("adw");
|
||||
const gio = @import("gio");
|
||||
const glib = @import("glib");
|
||||
const gobject = @import("gobject");
|
||||
const gtk = @import("gtk");
|
||||
|
||||
const gresource = @import("../build/gresource.zig");
|
||||
const adw_version = @import("../adw_version.zig");
|
||||
const ext = @import("../ext.zig");
|
||||
const Common = @import("../class.zig").Common;
|
||||
|
||||
const log = std.log.scoped(.gtk_ghostty_surface_title_dialog);
|
||||
|
||||
pub const SurfaceTitleDialog = extern struct {
|
||||
const Self = @This();
|
||||
parent_instance: Parent,
|
||||
pub const Parent = adw.AlertDialog;
|
||||
pub const getGObjectType = gobject.ext.defineClass(Self, .{
|
||||
.name = "GhosttySurfaceTitleDialog",
|
||||
.instanceInit = &init,
|
||||
.classInit = &Class.init,
|
||||
.parent_class = &Class.parent,
|
||||
.private = .{ .Type = Private, .offset = &Private.offset },
|
||||
});
|
||||
|
||||
pub const properties = struct {
|
||||
pub const @"initial-value" = struct {
|
||||
pub const name = "initial-value";
|
||||
pub const get = impl.get;
|
||||
pub const set = impl.set;
|
||||
const impl = gobject.ext.defineProperty(
|
||||
name,
|
||||
Self,
|
||||
?[:0]const u8,
|
||||
.{
|
||||
.default = null,
|
||||
.accessor = C.privateStringFieldAccessor("initial_value"),
|
||||
},
|
||||
);
|
||||
};
|
||||
};
|
||||
|
||||
pub const signals = struct {
|
||||
/// Set the title to the given value.
|
||||
pub const set = struct {
|
||||
pub const name = "set";
|
||||
pub const connect = impl.connect;
|
||||
const impl = gobject.ext.defineSignal(
|
||||
name,
|
||||
Self,
|
||||
&.{[*:0]const u8},
|
||||
void,
|
||||
);
|
||||
};
|
||||
};
|
||||
|
||||
const Private = struct {
|
||||
/// The initial value of the entry field.
|
||||
initial_value: ?[:0]const u8 = null,
|
||||
|
||||
// Template bindings
|
||||
entry: *gtk.Entry,
|
||||
|
||||
pub var offset: c_int = 0;
|
||||
};
|
||||
|
||||
fn init(self: *Self, _: *Class) callconv(.c) void {
|
||||
gtk.Widget.initTemplate(self.as(gtk.Widget));
|
||||
}
|
||||
|
||||
pub fn present(self: *Self, parent_: *gtk.Widget) void {
|
||||
// If we have a window we can attach to, we prefer that.
|
||||
const parent: *gtk.Widget = if (ext.getAncestor(
|
||||
adw.ApplicationWindow,
|
||||
parent_,
|
||||
)) |window|
|
||||
window.as(gtk.Widget)
|
||||
else if (ext.getAncestor(
|
||||
adw.Window,
|
||||
parent_,
|
||||
)) |window|
|
||||
window.as(gtk.Widget)
|
||||
else
|
||||
parent_;
|
||||
|
||||
// Set our initial value
|
||||
const priv = self.private();
|
||||
if (priv.initial_value) |v| {
|
||||
priv.entry.getBuffer().setText(v, -1);
|
||||
}
|
||||
|
||||
// Show it. We could also just use virtual methods to bind to
|
||||
// response but this is pretty simple.
|
||||
self.as(adw.AlertDialog).choose(
|
||||
parent,
|
||||
null,
|
||||
alertDialogReady,
|
||||
self,
|
||||
);
|
||||
}
|
||||
|
||||
fn alertDialogReady(
|
||||
_: ?*gobject.Object,
|
||||
result: *gio.AsyncResult,
|
||||
ud: ?*anyopaque,
|
||||
) callconv(.c) void {
|
||||
const self: *Self = @ptrCast(@alignCast(ud));
|
||||
const response = self.as(adw.AlertDialog).chooseFinish(result);
|
||||
|
||||
// If we didn't hit "okay" then we do nothing.
|
||||
if (std.mem.orderZ(u8, "ok", response) != .eq) return;
|
||||
|
||||
// Emit our signal with the new title.
|
||||
const title = std.mem.span(self.private().entry.getBuffer().getText());
|
||||
signals.set.impl.emit(
|
||||
self,
|
||||
null,
|
||||
.{title.ptr},
|
||||
null,
|
||||
);
|
||||
}
|
||||
|
||||
fn dispose(self: *Self) callconv(.c) void {
|
||||
gtk.Widget.disposeTemplate(
|
||||
self.as(gtk.Widget),
|
||||
getGObjectType(),
|
||||
);
|
||||
|
||||
gobject.Object.virtual_methods.dispose.call(
|
||||
Class.parent,
|
||||
self.as(Parent),
|
||||
);
|
||||
}
|
||||
|
||||
fn finalize(self: *Self) callconv(.c) void {
|
||||
const priv = self.private();
|
||||
if (priv.initial_value) |v| {
|
||||
glib.free(@constCast(@ptrCast(v)));
|
||||
priv.initial_value = null;
|
||||
}
|
||||
|
||||
gobject.Object.virtual_methods.finalize.call(
|
||||
Class.parent,
|
||||
self.as(Parent),
|
||||
);
|
||||
}
|
||||
|
||||
const C = Common(Self, Private);
|
||||
pub const as = C.as;
|
||||
pub const ref = C.ref;
|
||||
pub const unref = C.unref;
|
||||
const private = C.private;
|
||||
|
||||
pub const Class = extern struct {
|
||||
parent_class: Parent.Class,
|
||||
var parent: *Parent.Class = undefined;
|
||||
pub const Instance = Self;
|
||||
|
||||
fn init(class: *Class) callconv(.c) void {
|
||||
gtk.Widget.Class.setTemplateFromResource(
|
||||
class.as(gtk.Widget.Class),
|
||||
comptime gresource.blueprint(.{
|
||||
.major = 1,
|
||||
.minor = 5,
|
||||
.name = "surface-title-dialog",
|
||||
}),
|
||||
);
|
||||
|
||||
// Signals
|
||||
signals.set.impl.register(.{});
|
||||
|
||||
// Bindings
|
||||
class.bindTemplateChildPrivate("entry", .{});
|
||||
|
||||
// Properties
|
||||
gobject.ext.registerProperties(class, &.{
|
||||
properties.@"initial-value".impl,
|
||||
});
|
||||
|
||||
// Virtual methods
|
||||
gobject.Object.virtual_methods.dispose.implement(class, &dispose);
|
||||
gobject.Object.virtual_methods.finalize.implement(class, &finalize);
|
||||
}
|
||||
|
||||
pub const as = C.Class.as;
|
||||
pub const bindTemplateChildPrivate = C.Class.bindTemplateChildPrivate;
|
||||
pub const bindTemplateCallback = C.Class.bindTemplateCallback;
|
||||
};
|
||||
};
|
||||
|
|
@ -364,7 +364,8 @@ pub const Tab = extern struct {
|
|||
fn closureComputedTitle(
|
||||
_: *Self,
|
||||
config_: ?*Config,
|
||||
plain_: ?[*:0]const u8,
|
||||
terminal_: ?[*:0]const u8,
|
||||
override_: ?[*:0]const u8,
|
||||
zoomed_: c_int,
|
||||
bell_ringing_: c_int,
|
||||
_: *gobject.ParamSpec,
|
||||
|
|
@ -372,9 +373,13 @@ pub const Tab = extern struct {
|
|||
const zoomed = zoomed_ != 0;
|
||||
const bell_ringing = bell_ringing_ != 0;
|
||||
|
||||
// Our plain title is the overridden title if it exists, otherwise
|
||||
// the terminal title if it exists, otherwise a default string.
|
||||
const plain = plain: {
|
||||
const default = "Ghostty";
|
||||
const plain = plain_ orelse break :plain default;
|
||||
const plain = override_ orelse
|
||||
terminal_ orelse
|
||||
break :plain default;
|
||||
break :plain std.mem.span(plain);
|
||||
};
|
||||
|
||||
|
|
|
|||
|
|
@ -168,7 +168,7 @@ menu context_menu_model {
|
|||
|
||||
item {
|
||||
label: _("Change Title…");
|
||||
action: "win.prompt-title";
|
||||
action: "surface.prompt-title";
|
||||
}
|
||||
|
||||
item {
|
||||
|
|
|
|||
|
|
@ -0,0 +1,16 @@
|
|||
using Gtk 4.0;
|
||||
using Adw 1;
|
||||
|
||||
template $GhosttySurfaceTitleDialog: Adw.AlertDialog {
|
||||
heading: _("Change Terminal Title");
|
||||
body: _("Leave blank to restore the default title.");
|
||||
|
||||
responses [
|
||||
cancel: _("Cancel") suggested,
|
||||
ok: _("OK") destructive,
|
||||
]
|
||||
|
||||
focus-widget: entry;
|
||||
|
||||
extra-child: Entry entry {};
|
||||
}
|
||||
|
|
@ -8,7 +8,7 @@ template $GhosttyTab: Box {
|
|||
orientation: vertical;
|
||||
hexpand: true;
|
||||
vexpand: true;
|
||||
title: bind $computed_title(template.config, split_tree.active-surface as <$GhosttySurface>.title, split_tree.is-zoomed, split_tree.active-surface as <$GhosttySurface>.bell-ringing) as <string>;
|
||||
title: bind $computed_title(template.config, split_tree.active-surface as <$GhosttySurface>.title, split_tree.active-surface as <$GhosttySurface>.title-override, split_tree.is-zoomed, split_tree.active-surface as <$GhosttySurface>.bell-ringing) as <string>;
|
||||
tooltip: bind split_tree.active-surface as <$GhosttySurface>.pwd;
|
||||
|
||||
$GhosttySplitTree split_tree {
|
||||
|
|
|
|||
Loading…
Reference in New Issue