From 793e2719891f985c27549fa9586822296652b47d Mon Sep 17 00:00:00 2001 From: Mitchell Hashimoto Date: Sun, 20 Jul 2025 15:02:49 -0700 Subject: [PATCH 1/7] apprt/gtk-ng: set pwd --- src/apprt/gtk-ng/class/application.zig | 21 +++++++++- src/apprt/gtk-ng/class/surface.zig | 53 ++++++++++++++++++++++++++ 2 files changed, 73 insertions(+), 1 deletion(-) diff --git a/src/apprt/gtk-ng/class/application.zig b/src/apprt/gtk-ng/class/application.zig index a170afc74..083c00e5a 100644 --- a/src/apprt/gtk-ng/class/application.zig +++ b/src/apprt/gtk-ng/class/application.zig @@ -416,6 +416,8 @@ pub const Application = extern struct { }, ), + .pwd => Action.pwd(target, value), + .quit_timer => try Action.quitTimer(self, value), .render => Action.render(self, target), @@ -439,7 +441,6 @@ pub const Application = extern struct { .show_gtk_inspector, .desktop_notification, .set_title, - .pwd, .present_terminal, .initial_size, .size_limit, @@ -937,6 +938,24 @@ const Action = struct { 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( self: *Application, mode: apprt.action.QuitTimer, diff --git a/src/apprt/gtk-ng/class/surface.zig b/src/apprt/gtk-ng/class/surface.zig index 0100f61c4..bbd47e7ae 100644 --- a/src/apprt/gtk-ng/class/surface.zig +++ b/src/apprt/gtk-ng/class/surface.zig @@ -93,6 +93,30 @@ pub const Surface = extern struct { }, ); }; + + pub const pwd = struct { + pub const name = "pwd"; + 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 = gobject.ext.typedAccessor( + Self, + ?[:0]const u8, + .{ + .getter = getPwd, + .getter_transfer = .none, + .setter = setPwd, + .setter_transfer = .full, + }, + ), + }, + ); + }; }; pub const signals = struct { @@ -128,6 +152,11 @@ pub const Surface = extern struct { /// Whether the mouse should be hidden or not as requested externally. 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 GLAarea that renders the actual surface. This is a binding /// to the template so it doesn't have to be unrefed manually. gl_area: *gtk.GLArea = undefined, @@ -820,6 +849,9 @@ pub const Surface = extern struct { priv.core_surface = null; } + if (priv.pwd != null) { + self.setPwd(null); + } gobject.Object.virtual_methods.finalize.call( Class.parent, @@ -830,6 +862,26 @@ pub const Surface = extern struct { //--------------------------------------------------------------- // Properties + fn getPwd( + self: *Self, + ) ?[:0]const u8 { + return self.private().pwd; + } + + fn setPwd( + self: *Self, + value: ?[:0]const u8, + ) void { + const priv = self.private(); + + // Free the previous value + if (priv.pwd) |v| glib.free(@constCast(@ptrCast(v.ptr))); + + // Set the new value, which is already copied since we + // set our setter_transfer value to full. + priv.pwd = value; + } + fn propMouseHidden( self: *Self, _: *gobject.ParamSpec, @@ -1512,6 +1564,7 @@ pub const Surface = extern struct { properties.config.impl, properties.@"mouse-shape".impl, properties.@"mouse-hidden".impl, + properties.pwd.impl, }); // Signals From 9440c775c73e53e9f2820e69f6d61980b961f90e Mon Sep 17 00:00:00 2001 From: Mitchell Hashimoto Date: Sun, 20 Jul 2025 15:23:40 -0700 Subject: [PATCH 2/7] apprt/gtk-ng: set title --- src/apprt/gtk-ng/class/application.zig | 21 ++++++++++- src/apprt/gtk-ng/class/surface.zig | 48 ++++++++++++++++++++++++-- 2 files changed, 65 insertions(+), 4 deletions(-) diff --git a/src/apprt/gtk-ng/class/application.zig b/src/apprt/gtk-ng/class/application.zig index 083c00e5a..49e77052b 100644 --- a/src/apprt/gtk-ng/class/application.zig +++ b/src/apprt/gtk-ng/class/application.zig @@ -422,6 +422,8 @@ pub const Application = extern struct { .render => Action.render(self, target), + .set_title => Action.setTitle(target, value), + // Unimplemented .quit, .close_window, @@ -440,7 +442,6 @@ pub const Application = extern struct { .inspector, .show_gtk_inspector, .desktop_notification, - .set_title, .present_terminal, .initial_size, .size_limit, @@ -977,6 +978,24 @@ const Action = struct { .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 diff --git a/src/apprt/gtk-ng/class/surface.zig b/src/apprt/gtk-ng/class/surface.zig index bbd47e7ae..353a1babb 100644 --- a/src/apprt/gtk-ng/class/surface.zig +++ b/src/apprt/gtk-ng/class/surface.zig @@ -117,6 +117,30 @@ pub const Surface = extern struct { }, ); }; + + pub const title = struct { + pub const name = "title"; + const impl = gobject.ext.defineProperty( + name, + Self, + ?[:0]const u8, + .{ + .nick = "Title", + .blurb = "The title of the surface.", + .default = null, + .accessor = gobject.ext.typedAccessor( + Self, + ?[:0]const u8, + .{ + .getter = getTitle, + .getter_transfer = .none, + .setter = setTitle, + .setter_transfer = .full, + }, + ), + }, + ); + }; }; pub const signals = struct { @@ -157,6 +181,9 @@ pub const Surface = extern struct { /// 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 /// to the template so it doesn't have to be unrefed manually. gl_area: *gtk.GLArea = undefined, @@ -849,9 +876,8 @@ pub const Surface = extern struct { priv.core_surface = null; } - if (priv.pwd != null) { - self.setPwd(null); - } + if (priv.pwd != null) self.setPwd(null); + if (priv.title != null) self.setTitle(null); gobject.Object.virtual_methods.finalize.call( Class.parent, @@ -882,6 +908,21 @@ pub const Surface = extern struct { priv.pwd = value; } + fn getTitle( + self: *Self, + ) ?[:0]const u8 { + return self.private().title; + } + + fn setTitle( + self: *Self, + value: ?[:0]const u8, + ) void { + const priv = self.private(); + if (priv.title) |v| glib.free(@constCast(@ptrCast(v.ptr))); + priv.title = value; + } + fn propMouseHidden( self: *Self, _: *gobject.ParamSpec, @@ -1565,6 +1606,7 @@ pub const Surface = extern struct { properties.@"mouse-shape".impl, properties.@"mouse-hidden".impl, properties.pwd.impl, + properties.title.impl, }); // Signals From cd82a610c3a482dbcb6d2c8c74089ddcf046e786 Mon Sep 17 00:00:00 2001 From: Mitchell Hashimoto Date: Mon, 21 Jul 2025 06:52:23 -0700 Subject: [PATCH 3/7] apprt/gtk-ng: abstract helper for private string fields --- src/apprt/gtk-ng/class.zig | 42 +++++++++++++++++++ src/apprt/gtk-ng/class/surface.zig | 67 +++++------------------------- 2 files changed, 52 insertions(+), 57 deletions(-) diff --git a/src/apprt/gtk-ng/class.zig b/src/apprt/gtk-ng/class.zig index 5d64cb903..7c8cfbc06 100644 --- a/src/apprt/gtk-ng/class.zig +++ b/src/apprt/gtk-ng/class.zig @@ -46,6 +46,48 @@ pub fn Common( } }).private else {}; + /// A helper that can be used to create a property that reads and + /// writes a private `?[:0]const u8` field type. + /// + /// This helper helps properly manage the memory to avoid memory leaks. + /// + /// 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. pub const Class = struct { pub fn as(class: *Self.Class, comptime T: type) *T { diff --git a/src/apprt/gtk-ng/class/surface.zig b/src/apprt/gtk-ng/class/surface.zig index 353a1babb..211deebfb 100644 --- a/src/apprt/gtk-ng/class/surface.zig +++ b/src/apprt/gtk-ng/class/surface.zig @@ -96,6 +96,8 @@ 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, @@ -104,22 +106,15 @@ pub const Surface = extern struct { .nick = "Working Directory", .blurb = "The current working directory as reported by core.", .default = null, - .accessor = gobject.ext.typedAccessor( - Self, - ?[:0]const u8, - .{ - .getter = getPwd, - .getter_transfer = .none, - .setter = setPwd, - .setter_transfer = .full, - }, - ), + .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, @@ -128,16 +123,7 @@ pub const Surface = extern struct { .nick = "Title", .blurb = "The title of the surface.", .default = null, - .accessor = gobject.ext.typedAccessor( - Self, - ?[:0]const u8, - .{ - .getter = getTitle, - .getter_transfer = .none, - .setter = setTitle, - .setter_transfer = .full, - }, - ), + .accessor = C.privateStringFieldAccessor("title"), }, ); }; @@ -876,8 +862,10 @@ pub const Surface = extern struct { priv.core_surface = null; } - if (priv.pwd != null) self.setPwd(null); - if (priv.title != null) self.setTitle(null); + + var @"null": gobject.Value = undefined; + if (priv.pwd != null) properties.pwd.set(self, &@"null"); + if (priv.title != null) properties.pwd.set(self, &@"null"); gobject.Object.virtual_methods.finalize.call( Class.parent, @@ -888,41 +876,6 @@ pub const Surface = extern struct { //--------------------------------------------------------------- // Properties - fn getPwd( - self: *Self, - ) ?[:0]const u8 { - return self.private().pwd; - } - - fn setPwd( - self: *Self, - value: ?[:0]const u8, - ) void { - const priv = self.private(); - - // Free the previous value - if (priv.pwd) |v| glib.free(@constCast(@ptrCast(v.ptr))); - - // Set the new value, which is already copied since we - // set our setter_transfer value to full. - priv.pwd = value; - } - - fn getTitle( - self: *Self, - ) ?[:0]const u8 { - return self.private().title; - } - - fn setTitle( - self: *Self, - value: ?[:0]const u8, - ) void { - const priv = self.private(); - if (priv.title) |v| glib.free(@constCast(@ptrCast(v.ptr))); - priv.title = value; - } - fn propMouseHidden( self: *Self, _: *gobject.ParamSpec, From ef686d62aa94e41612937e14c1bba71a87809974 Mon Sep 17 00:00:00 2001 From: Mitchell Hashimoto Date: Mon, 21 Jul 2025 07:04:24 -0700 Subject: [PATCH 4/7] apprt/gtk-ng: clear memory properly --- src/apprt/gtk-ng/class/surface.zig | 11 ++++++++--- 1 file changed, 8 insertions(+), 3 deletions(-) diff --git a/src/apprt/gtk-ng/class/surface.zig b/src/apprt/gtk-ng/class/surface.zig index 211deebfb..b719cb870 100644 --- a/src/apprt/gtk-ng/class/surface.zig +++ b/src/apprt/gtk-ng/class/surface.zig @@ -863,9 +863,14 @@ pub const Surface = extern struct { priv.core_surface = null; } - var @"null": gobject.Value = undefined; - if (priv.pwd != null) properties.pwd.set(self, &@"null"); - if (priv.title != null) properties.pwd.set(self, &@"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( Class.parent, From e911d53a2edd7d83d669d8c012ad145beab38f57 Mon Sep 17 00:00:00 2001 From: Mitchell Hashimoto Date: Mon, 21 Jul 2025 07:15:54 -0700 Subject: [PATCH 5/7] apprt/gtk-ng: get-title --- src/apprt/gtk-ng/Surface.zig | 3 +-- src/apprt/gtk-ng/class.zig | 6 +++++- src/apprt/gtk-ng/class/surface.zig | 5 +++++ 3 files changed, 11 insertions(+), 3 deletions(-) diff --git a/src/apprt/gtk-ng/Surface.zig b/src/apprt/gtk-ng/Surface.zig index ab9b6de52..bd40e828b 100644 --- a/src/apprt/gtk-ng/Surface.zig +++ b/src/apprt/gtk-ng/Surface.zig @@ -36,8 +36,7 @@ pub fn close(self: *Self, process_active: bool) void { } pub fn getTitle(self: *Self) ?[:0]const u8 { - _ = self; - return null; + return self.surface.getTitle(); } pub fn getContentScale(self: *const Self) !apprt.ContentScale { diff --git a/src/apprt/gtk-ng/class.zig b/src/apprt/gtk-ng/class.zig index 7c8cfbc06..181259ee7 100644 --- a/src/apprt/gtk-ng/class.zig +++ b/src/apprt/gtk-ng/class.zig @@ -49,7 +49,11 @@ pub fn Common( /// A helper that can be used to create a property that reads and /// writes a private `?[:0]const u8` field type. /// - /// This helper helps properly manage the memory to avoid memory leaks. + /// 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! diff --git a/src/apprt/gtk-ng/class/surface.zig b/src/apprt/gtk-ng/class/surface.zig index b719cb870..535087d9a 100644 --- a/src/apprt/gtk-ng/class/surface.zig +++ b/src/apprt/gtk-ng/class/surface.zig @@ -881,6 +881,11 @@ pub const Surface = extern struct { //--------------------------------------------------------------- // Properties + /// Returns the title property without a copy. + pub fn getTitle(self: *Self) ?[:0]const u8 { + return self.private().title; + } + fn propMouseHidden( self: *Self, _: *gobject.ParamSpec, From edb5f7c69d314fd5d2895983b3fdaf16d094d1fe Mon Sep 17 00:00:00 2001 From: Mitchell Hashimoto Date: Mon, 21 Jul 2025 07:28:05 -0700 Subject: [PATCH 6/7] slow down our cursor timer under valgrind --- src/renderer/Thread.zig | 31 +++++++++++++++++++++++++++---- 1 file changed, 27 insertions(+), 4 deletions(-) diff --git a/src/renderer/Thread.zig b/src/renderer/Thread.zig index b8884f2fb..210c2e337 100644 --- a/src/renderer/Thread.zig +++ b/src/renderer/Thread.zig @@ -248,7 +248,7 @@ fn threadMain_(self: *Thread) !void { self.cursor_h.run( &self.loop, &self.cursor_c, - CURSOR_BLINK_INTERVAL, + cursorBlinkInterval(), Thread, self, cursorTimerCallback, @@ -408,7 +408,7 @@ fn drainMailbox(self: *Thread) !void { self.cursor_h.run( &self.loop, &self.cursor_c, - CURSOR_BLINK_INTERVAL, + cursorBlinkInterval(), Thread, self, cursorTimerCallback, @@ -424,7 +424,7 @@ fn drainMailbox(self: *Thread) !void { &self.loop, &self.cursor_c, &self.cursor_c_cancel, - CURSOR_BLINK_INTERVAL, + cursorBlinkInterval(), Thread, self, cursorTimerCallback, @@ -641,7 +641,14 @@ fn cursorTimerCallback( t.flags.cursor_blink_visible = !t.flags.cursor_blink_visible; 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; } @@ -687,3 +694,19 @@ fn stopCallback( self_.?.loop.stop(); 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; +} From 7e834a1c3225a7beee92f81500dace902fb4ee4d Mon Sep 17 00:00:00 2001 From: Mitchell Hashimoto Date: Mon, 21 Jul 2025 08:15:34 -0700 Subject: [PATCH 7/7] apprt/gtk-ng: some actions --- src/apprt/gtk-ng/class/application.zig | 10 ++++++++-- 1 file changed, 8 insertions(+), 2 deletions(-) diff --git a/src/apprt/gtk-ng/class/application.zig b/src/apprt/gtk-ng/class/application.zig index 49e77052b..8865fd137 100644 --- a/src/apprt/gtk-ng/class/application.zig +++ b/src/apprt/gtk-ng/class/application.zig @@ -424,7 +424,7 @@ pub const Application = extern struct { .set_title => Action.setTitle(target, value), - // Unimplemented + // Unimplemented but todo on gtk-ng branch .quit, .close_window, .toggle_maximize, @@ -451,7 +451,6 @@ pub const Application = extern struct { .toggle_window_decorations, .prompt_title, .toggle_quick_terminal, - .secure_input, .ring_bell, .toggle_command_palette, .open_url, @@ -473,6 +472,13 @@ pub const Application = extern struct { log.warn("unimplemented action={}", .{action}); return false; }, + + // Unimplemented + .secure_input, + => { + log.warn("unimplemented action={}", .{action}); + return false; + }, } // Assume it was handled. The unhandled case must be explicit