apprt/gtk-ng: hook up title, pwd surface properties (#8005)

Continuing to plumb along the APIs necessary basic surface functionality
before moving onto windowing functionality. This adds title/pwd as
properties to Surface, adds abstractions necessary to manage that memory
correctly, and also adds a tiny change to renderers to make everything
slightly more usable under Valgrind.
This commit is contained in:
Mitchell Hashimoto
2025-07-21 08:35:55 -07:00
committed by GitHub
5 changed files with 180 additions and 10 deletions

View File

@ -36,8 +36,7 @@ pub fn close(self: *Self, process_active: bool) void {
} }
pub fn getTitle(self: *Self) ?[:0]const u8 { pub fn getTitle(self: *Self) ?[:0]const u8 {
_ = self; return self.surface.getTitle();
return null;
} }
pub fn getContentScale(self: *const Self) !apprt.ContentScale { pub fn getContentScale(self: *const Self) !apprt.ContentScale {

View File

@ -46,6 +46,52 @@ pub fn Common(
} }
}).private else {}; }).private else {};
/// A helper that can be used to create a property that reads and
/// writes a private `?[:0]const u8` field type.
///
/// Reading the property will result in a copy of the string
/// and callers are responsible for freeing it.
///
/// Writing the property will free the previous value and copy
/// the new value into the private field.
///
/// The object class (Self) must still free the private field
/// in finalize!
pub fn privateStringFieldAccessor(
comptime name: []const u8,
) gobject.ext.Accessor(
Self,
@FieldType(Private.?, name),
) {
const S = struct {
fn getter(self: *Self) ?[:0]const u8 {
return @field(private(self), name);
}
fn setter(self: *Self, value: ?[:0]const u8) void {
const priv = private(self);
if (@field(priv, name)) |v| {
glib.free(@constCast(@ptrCast(v)));
}
// We don't need to copy this because it was already
// copied by the typedAccessor.
@field(priv, name) = value;
}
};
return gobject.ext.typedAccessor(
Self,
?[:0]const u8,
.{
.getter = S.getter,
.getter_transfer = .none,
.setter = S.setter,
.setter_transfer = .full,
},
);
}
/// Common class functions. /// Common class functions.
pub const Class = struct { pub const Class = struct {
pub fn as(class: *Self.Class, comptime T: type) *T { pub fn as(class: *Self.Class, comptime T: type) *T {

View File

@ -416,11 +416,15 @@ pub const Application = extern struct {
}, },
), ),
.pwd => Action.pwd(target, value),
.quit_timer => try Action.quitTimer(self, value), .quit_timer => try Action.quitTimer(self, value),
.render => Action.render(self, target), .render => Action.render(self, target),
// Unimplemented .set_title => Action.setTitle(target, value),
// Unimplemented but todo on gtk-ng branch
.quit, .quit,
.close_window, .close_window,
.toggle_maximize, .toggle_maximize,
@ -438,8 +442,6 @@ pub const Application = extern struct {
.inspector, .inspector,
.show_gtk_inspector, .show_gtk_inspector,
.desktop_notification, .desktop_notification,
.set_title,
.pwd,
.present_terminal, .present_terminal,
.initial_size, .initial_size,
.size_limit, .size_limit,
@ -449,7 +451,6 @@ pub const Application = extern struct {
.toggle_window_decorations, .toggle_window_decorations,
.prompt_title, .prompt_title,
.toggle_quick_terminal, .toggle_quick_terminal,
.secure_input,
.ring_bell, .ring_bell,
.toggle_command_palette, .toggle_command_palette,
.open_url, .open_url,
@ -471,6 +472,13 @@ pub const Application = extern struct {
log.warn("unimplemented action={}", .{action}); log.warn("unimplemented action={}", .{action});
return false; return false;
}, },
// Unimplemented
.secure_input,
=> {
log.warn("unimplemented action={}", .{action});
return false;
},
} }
// Assume it was handled. The unhandled case must be explicit // Assume it was handled. The unhandled case must be explicit
@ -937,6 +945,24 @@ const Action = struct {
gtk.Window.present(win.as(gtk.Window)); gtk.Window.present(win.as(gtk.Window));
} }
pub fn pwd(
target: apprt.Target,
value: apprt.action.Pwd,
) void {
switch (target) {
.app => log.warn("pwd to app is unexpected", .{}),
.surface => |surface| {
var v = gobject.ext.Value.newFrom(value.pwd);
defer v.unset();
gobject.Object.setProperty(
surface.rt_surface.gobj().as(gobject.Object),
"pwd",
&v,
);
},
}
}
pub fn quitTimer( pub fn quitTimer(
self: *Application, self: *Application,
mode: apprt.action.QuitTimer, mode: apprt.action.QuitTimer,
@ -958,6 +984,24 @@ const Action = struct {
.surface => |v| v.rt_surface.surface.redraw(), .surface => |v| v.rt_surface.surface.redraw(),
} }
} }
pub fn setTitle(
target: apprt.Target,
value: apprt.action.SetTitle,
) void {
switch (target) {
.app => log.warn("set_title to app is unexpected", .{}),
.surface => |surface| {
var v = gobject.ext.Value.newFrom(value.title);
defer v.unset();
gobject.Object.setProperty(
surface.rt_surface.gobj().as(gobject.Object),
"title",
&v,
);
},
}
}
}; };
/// This sets various GTK-related environment variables as necessary /// This sets various GTK-related environment variables as necessary

View File

@ -93,6 +93,40 @@ 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,
?[:0]const u8,
.{
.nick = "Working Directory",
.blurb = "The current working directory as reported by core.",
.default = null,
.accessor = C.privateStringFieldAccessor("pwd"),
},
);
};
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,
?[:0]const u8,
.{
.nick = "Title",
.blurb = "The title of the surface.",
.default = null,
.accessor = C.privateStringFieldAccessor("title"),
},
);
};
}; };
pub const signals = struct { pub const signals = struct {
@ -128,6 +162,14 @@ pub const Surface = extern struct {
/// Whether the mouse should be hidden or not as requested externally. /// Whether the mouse should be hidden or not as requested externally.
mouse_hidden: bool = false, mouse_hidden: bool = false,
/// The current working directory. This has to be reported externally,
/// usually by shell integration which then talks to libghostty
/// which triggers this property.
pwd: ?[:0]const u8 = null,
/// The title of this surface, if any has been set.
title: ?[:0]const u8 = null,
/// The GLAarea that renders the actual surface. This is a binding /// The GLAarea that renders the actual surface. This is a binding
/// to the template so it doesn't have to be unrefed manually. /// to the template so it doesn't have to be unrefed manually.
gl_area: *gtk.GLArea = undefined, gl_area: *gtk.GLArea = undefined,
@ -821,6 +863,15 @@ pub const Surface = extern struct {
priv.core_surface = null; priv.core_surface = null;
} }
if (priv.pwd) |v| {
glib.free(@constCast(@ptrCast(v)));
priv.pwd = null;
}
if (priv.title) |v| {
glib.free(@constCast(@ptrCast(v)));
priv.title = null;
}
gobject.Object.virtual_methods.finalize.call( gobject.Object.virtual_methods.finalize.call(
Class.parent, Class.parent,
self.as(Parent), self.as(Parent),
@ -830,6 +881,11 @@ pub const Surface = extern struct {
//--------------------------------------------------------------- //---------------------------------------------------------------
// Properties // Properties
/// Returns the title property without a copy.
pub fn getTitle(self: *Self) ?[:0]const u8 {
return self.private().title;
}
fn propMouseHidden( fn propMouseHidden(
self: *Self, self: *Self,
_: *gobject.ParamSpec, _: *gobject.ParamSpec,
@ -1512,6 +1568,8 @@ pub const Surface = extern struct {
properties.config.impl, properties.config.impl,
properties.@"mouse-shape".impl, properties.@"mouse-shape".impl,
properties.@"mouse-hidden".impl, properties.@"mouse-hidden".impl,
properties.pwd.impl,
properties.title.impl,
}); });
// Signals // Signals

View File

@ -248,7 +248,7 @@ fn threadMain_(self: *Thread) !void {
self.cursor_h.run( self.cursor_h.run(
&self.loop, &self.loop,
&self.cursor_c, &self.cursor_c,
CURSOR_BLINK_INTERVAL, cursorBlinkInterval(),
Thread, Thread,
self, self,
cursorTimerCallback, cursorTimerCallback,
@ -408,7 +408,7 @@ fn drainMailbox(self: *Thread) !void {
self.cursor_h.run( self.cursor_h.run(
&self.loop, &self.loop,
&self.cursor_c, &self.cursor_c,
CURSOR_BLINK_INTERVAL, cursorBlinkInterval(),
Thread, Thread,
self, self,
cursorTimerCallback, cursorTimerCallback,
@ -424,7 +424,7 @@ fn drainMailbox(self: *Thread) !void {
&self.loop, &self.loop,
&self.cursor_c, &self.cursor_c,
&self.cursor_c_cancel, &self.cursor_c_cancel,
CURSOR_BLINK_INTERVAL, cursorBlinkInterval(),
Thread, Thread,
self, self,
cursorTimerCallback, cursorTimerCallback,
@ -641,7 +641,14 @@ fn cursorTimerCallback(
t.flags.cursor_blink_visible = !t.flags.cursor_blink_visible; t.flags.cursor_blink_visible = !t.flags.cursor_blink_visible;
t.wakeup.notify() catch {}; t.wakeup.notify() catch {};
t.cursor_h.run(&t.loop, &t.cursor_c, CURSOR_BLINK_INTERVAL, Thread, t, cursorTimerCallback); t.cursor_h.run(
&t.loop,
&t.cursor_c,
cursorBlinkInterval(),
Thread,
t,
cursorTimerCallback,
);
return .disarm; return .disarm;
} }
@ -687,3 +694,19 @@ fn stopCallback(
self_.?.loop.stop(); self_.?.loop.stop();
return .disarm; return .disarm;
} }
/// Returns the interval for the blinking cursor in milliseconds.
fn cursorBlinkInterval() u64 {
if (std.valgrind.runningOnValgrind() > 0) {
// If we're running under Valgrind, the cursor blink adds enough
// churn that it makes some stalls annoying unless you're on a
// super powerful computer, so we delay it.
//
// This is a hack, we should change some of our cursor timer
// logic to be more efficient:
// https://github.com/ghostty-org/ghostty/issues/8003
return CURSOR_BLINK_INTERVAL * 5;
}
return CURSOR_BLINK_INTERVAL;
}