diff --git a/src/apprt/gtk/Surface.zig b/src/apprt/gtk/Surface.zig index 056a3f40b..180f986ca 100644 --- a/src/apprt/gtk/Surface.zig +++ b/src/apprt/gtk/Surface.zig @@ -347,6 +347,11 @@ cursor: ?*c.GdkCursor = null, /// pass it to GTK. title_text: ?[:0]const u8 = null, +/// Our current working directory. We use this value for setting tooltips in +/// the headerbar subtitle if we have focus. When set, the text in this buf +/// will be null-terminated because we need to pass it to GTK. +pwd: ?[:0]const u8 = null, + /// The timer used to delay title updates in order to prevent flickering. update_title_timer: ?c.guint = null, @@ -628,6 +633,7 @@ fn realize(self: *Surface) !void { pub fn deinit(self: *Surface) void { self.init_config.deinit(self.app.core_app.alloc); if (self.title_text) |title| self.app.core_app.alloc.free(title); + if (self.pwd) |pwd| self.app.core_app.alloc.free(pwd); // We don't allocate anything if we aren't realized. if (!self.realized) return; @@ -876,7 +882,7 @@ fn updateTitleLabels(self: *Surface) void { // I don't know a way around this yet. I've tried re-hiding the // cursor after setting the title but it doesn't work, I think // due to some gtk event loop things... - c.gtk_window_set_title(window.window, title.ptr); + window.setTitle(title); } } } @@ -929,11 +935,27 @@ pub fn getTitle(self: *Surface) ?[:0]const u8 { return null; } +/// Set the current working directory of the surface. +/// +/// In addition, update the tab's tooltip text, and if we are the focused child, +/// update the subtitle of the containing window. pub fn setPwd(self: *Surface, pwd: [:0]const u8) !void { - // If we have a tab and are the focused child, then we have to update the tab if (self.container.tab()) |tab| { tab.setTooltipText(pwd); + + if (tab.focus_child == self) { + if (self.container.window()) |window| { + if (self.app.config.@"window-subtitle" == .@"working-directory") window.setSubtitle(pwd); + } + } } + + const alloc = self.app.core_app.alloc; + + // Failing to set the surface's current working directory is not a big + // deal since we just used our slice parameter which is the same value. + if (self.pwd) |old| alloc.free(old); + self.pwd = alloc.dupeZ(u8, pwd) catch null; } pub fn setMouseShape( @@ -1896,6 +1918,12 @@ fn gtkFocusEnter(_: *c.GtkEventControllerFocus, ud: ?*anyopaque) callconv(.C) vo self.unfocused_widget = null; } + if (self.pwd) |pwd| { + if (self.container.window()) |window| { + if (self.app.config.@"window-subtitle" == .@"working-directory") window.setSubtitle(pwd); + } + } + // Notify our surface self.core_surface.focusCallback(true) catch |err| { log.err("error in focus callback err={}", .{err}); diff --git a/src/apprt/gtk/Window.zig b/src/apprt/gtk/Window.zig index 63ee57d95..c2c69e281 100644 --- a/src/apprt/gtk/Window.zig +++ b/src/apprt/gtk/Window.zig @@ -453,6 +453,22 @@ pub fn deinit(self: *Window) void { } } +/// Set the title of the window. +pub fn setTitle(self: *Window, title: [:0]const u8) void { + if ((comptime adwaita.versionAtLeast(1, 4, 0)) and adwaita.versionAtLeast(1, 4, 0) and adwaita.enabled(&self.app.config) and self.app.config.@"gtk-titlebar") { + if (self.header) |header| header.setTitle(title); + } else { + c.gtk_window_set_title(self.window, title); + } +} + +/// Set the subtitle of the window if it has one. +pub fn setSubtitle(self: *Window, subtitle: [:0]const u8) void { + if ((comptime adwaita.versionAtLeast(1, 4, 0)) and adwaita.versionAtLeast(1, 4, 0) and adwaita.enabled(&self.app.config) and self.app.config.@"gtk-titlebar") { + if (self.header) |header| header.setSubtitle(subtitle); + } +} + /// Add a new tab to this window. pub fn newTab(self: *Window, parent: ?*CoreSurface) !void { const alloc = self.app.core_app.alloc; diff --git a/src/apprt/gtk/headerbar.zig b/src/apprt/gtk/headerbar.zig index 5bb92aca2..97c48a4c2 100644 --- a/src/apprt/gtk/headerbar.zig +++ b/src/apprt/gtk/headerbar.zig @@ -14,14 +14,15 @@ pub const HeaderBar = union(enum) { if ((comptime adwaita.versionAtLeast(1, 4, 0)) and adwaita.enabled(&window.app.config)) { - return initAdw(); + return initAdw(window); } return initGtk(); } - fn initAdw() HeaderBar { + fn initAdw(window: *Window) HeaderBar { const headerbar = c.adw_header_bar_new(); + c.adw_header_bar_set_title_widget(@ptrCast(headerbar), @ptrCast(c.adw_window_title_new(c.gtk_window_get_title(window.window) orelse "Ghostty", null))); return .{ .adw = @ptrCast(headerbar) }; } @@ -70,4 +71,26 @@ pub const HeaderBar = union(enum) { ), } } + + pub fn setTitle(self: HeaderBar, title: [:0]const u8) void { + switch (self) { + .adw => |headerbar| if (comptime adwaita.versionAtLeast(0, 0, 0)) { + const window_title: *c.AdwWindowTitle = @ptrCast(c.adw_header_bar_get_title_widget(@ptrCast(headerbar))); + c.adw_window_title_set_title(window_title, title); + }, + // The title is owned by the window when not using Adwaita + .gtk => unreachable, + } + } + + pub fn setSubtitle(self: HeaderBar, subtitle: [:0]const u8) void { + switch (self) { + .adw => |headerbar| if (comptime adwaita.versionAtLeast(0, 0, 0)) { + const window_title: *c.AdwWindowTitle = @ptrCast(c.adw_header_bar_get_title_widget(@ptrCast(headerbar))); + c.adw_window_title_set_subtitle(window_title, subtitle); + }, + // There is no subtitle unless Adwaita is used + .gtk => unreachable, + } + } }; diff --git a/src/apprt/gtk/notebook_adw.zig b/src/apprt/gtk/notebook_adw.zig index 85083a97e..48f005467 100644 --- a/src/apprt/gtk/notebook_adw.zig +++ b/src/apprt/gtk/notebook_adw.zig @@ -159,5 +159,5 @@ fn adwSelectPage(_: *c.GObject, _: *c.GParamSpec, ud: ?*anyopaque) void { const window: *Window = @ptrCast(@alignCast(ud.?)); const page = c.adw_tab_view_get_selected_page(window.notebook.adw.tab_view) orelse return; const title = c.adw_tab_page_get_title(page); - c.gtk_window_set_title(window.window, title); + window.setTitle(std.mem.span(title)); } diff --git a/src/apprt/gtk/notebook_gtk.zig b/src/apprt/gtk/notebook_gtk.zig index 6e8b016ba..a2c482500 100644 --- a/src/apprt/gtk/notebook_gtk.zig +++ b/src/apprt/gtk/notebook_gtk.zig @@ -259,7 +259,7 @@ fn gtkSwitchPage(_: *c.GtkNotebook, page: *c.GtkWidget, _: usize, ud: ?*anyopaqu const gtk_label_box = @as(*c.GtkWidget, @ptrCast(c.gtk_notebook_get_tab_label(self.notebook, page))); const gtk_label = @as(*c.GtkLabel, @ptrCast(c.gtk_widget_get_first_child(gtk_label_box))); const label_text = c.gtk_label_get_text(gtk_label); - c.gtk_window_set_title(window.window, label_text); + window.setTitle(std.mem.span(label_text)); } fn gtkNotebookCreateWindow( diff --git a/src/config/Config.zig b/src/config/Config.zig index 6cd6ad75e..6d2c026fe 100644 --- a/src/config/Config.zig +++ b/src/config/Config.zig @@ -1118,6 +1118,15 @@ keybind: Keybinds = .{}, /// required to be a fixed-width font. @"window-title-font-family": ?[:0]const u8 = null, +/// The text that will be displayed in the subtitle of the window. Valid values: +/// +/// * `false` - Disable the subtitle. +/// * `working-directory` - Set the subtitle to the working directory of the +/// surface. +/// +/// This feature is only supported on GTK with Adwaita enabled. +@"window-subtitle": WindowSubtitle = .false, + /// The theme to use for the windows. Valid values: /// /// * `auto` - Determine the theme based on the configured terminal @@ -3968,6 +3977,11 @@ pub const WindowPaddingColor = enum { @"extend-always", }; +pub const WindowSubtitle = enum { + false, + @"working-directory", +}; + /// Color represents a color using RGB. /// /// This is a packed struct so that the C API to read color values just