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 5d64cb903..181259ee7 100644 --- a/src/apprt/gtk-ng/class.zig +++ b/src/apprt/gtk-ng/class.zig @@ -46,6 +46,52 @@ 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. + /// + /// 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. pub const Class = struct { pub fn as(class: *Self.Class, comptime T: type) *T { diff --git a/src/apprt/gtk-ng/class/application.zig b/src/apprt/gtk-ng/class/application.zig index a170afc74..8865fd137 100644 --- a/src/apprt/gtk-ng/class/application.zig +++ b/src/apprt/gtk-ng/class/application.zig @@ -416,11 +416,15 @@ pub const Application = extern struct { }, ), + .pwd => Action.pwd(target, value), + .quit_timer => try Action.quitTimer(self, value), .render => Action.render(self, target), - // Unimplemented + .set_title => Action.setTitle(target, value), + + // Unimplemented but todo on gtk-ng branch .quit, .close_window, .toggle_maximize, @@ -438,8 +442,6 @@ pub const Application = extern struct { .inspector, .show_gtk_inspector, .desktop_notification, - .set_title, - .pwd, .present_terminal, .initial_size, .size_limit, @@ -449,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, @@ -471,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 @@ -937,6 +945,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, @@ -958,6 +984,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 0100f61c4..535087d9a 100644 --- a/src/apprt/gtk-ng/class/surface.zig +++ b/src/apprt/gtk-ng/class/surface.zig @@ -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 { @@ -128,6 +162,14 @@ 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 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, @@ -821,6 +863,15 @@ pub const Surface = extern struct { 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( Class.parent, self.as(Parent), @@ -830,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, @@ -1512,6 +1568,8 @@ pub const Surface = extern struct { properties.config.impl, properties.@"mouse-shape".impl, properties.@"mouse-hidden".impl, + properties.pwd.impl, + properties.title.impl, }); // Signals 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; +}