apprt/gtk: use a subtitle to mark the current working directory (#3570)

If the title is already the current working directory, hide the
subtitle. Otherwise show the current working directory, like if a
command is running for instance.

This is a re-opening of my original PR because I had to delete my fork
and re-fork it.
This commit is contained in:
Mitchell Hashimoto
2025-01-08 10:10:19 -08:00
committed by GitHub
6 changed files with 87 additions and 6 deletions

View File

@ -347,6 +347,11 @@ cursor: ?*c.GdkCursor = null,
/// pass it to GTK. /// pass it to GTK.
title_text: ?[:0]const u8 = null, 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. /// The timer used to delay title updates in order to prevent flickering.
update_title_timer: ?c.guint = null, update_title_timer: ?c.guint = null,
@ -628,6 +633,7 @@ fn realize(self: *Surface) !void {
pub fn deinit(self: *Surface) void { pub fn deinit(self: *Surface) void {
self.init_config.deinit(self.app.core_app.alloc); self.init_config.deinit(self.app.core_app.alloc);
if (self.title_text) |title| self.app.core_app.alloc.free(title); 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. // We don't allocate anything if we aren't realized.
if (!self.realized) return; 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 // 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 // cursor after setting the title but it doesn't work, I think
// due to some gtk event loop things... // 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; 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 { 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| { if (self.container.tab()) |tab| {
tab.setTooltipText(pwd); 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( pub fn setMouseShape(
@ -1896,6 +1918,12 @@ fn gtkFocusEnter(_: *c.GtkEventControllerFocus, ud: ?*anyopaque) callconv(.C) vo
self.unfocused_widget = null; 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 // Notify our surface
self.core_surface.focusCallback(true) catch |err| { self.core_surface.focusCallback(true) catch |err| {
log.err("error in focus callback err={}", .{err}); log.err("error in focus callback err={}", .{err});

View File

@ -450,6 +450,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. /// Add a new tab to this window.
pub fn newTab(self: *Window, parent: ?*CoreSurface) !void { pub fn newTab(self: *Window, parent: ?*CoreSurface) !void {
const alloc = self.app.core_app.alloc; const alloc = self.app.core_app.alloc;

View File

@ -14,14 +14,15 @@ pub const HeaderBar = union(enum) {
if ((comptime adwaita.versionAtLeast(1, 4, 0)) and if ((comptime adwaita.versionAtLeast(1, 4, 0)) and
adwaita.enabled(&window.app.config)) adwaita.enabled(&window.app.config))
{ {
return initAdw(); return initAdw(window);
} }
return initGtk(); return initGtk();
} }
fn initAdw() HeaderBar { fn initAdw(window: *Window) HeaderBar {
const headerbar = c.adw_header_bar_new(); 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) }; 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,
}
}
}; };

View File

@ -159,5 +159,5 @@ fn adwSelectPage(_: *c.GObject, _: *c.GParamSpec, ud: ?*anyopaque) void {
const window: *Window = @ptrCast(@alignCast(ud.?)); const window: *Window = @ptrCast(@alignCast(ud.?));
const page = c.adw_tab_view_get_selected_page(window.notebook.adw.tab_view) orelse return; 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); const title = c.adw_tab_page_get_title(page);
c.gtk_window_set_title(window.window, title); window.setTitle(std.mem.span(title));
} }

View File

@ -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_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 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); 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( fn gtkNotebookCreateWindow(

View File

@ -1120,6 +1120,15 @@ keybind: Keybinds = .{},
/// required to be a fixed-width font. /// required to be a fixed-width font.
@"window-title-font-family": ?[:0]const u8 = null, @"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: /// The theme to use for the windows. Valid values:
/// ///
/// * `auto` - Determine the theme based on the configured terminal /// * `auto` - Determine the theme based on the configured terminal
@ -3974,6 +3983,11 @@ pub const WindowPaddingColor = enum {
@"extend-always", @"extend-always",
}; };
pub const WindowSubtitle = enum {
false,
@"working-directory",
};
/// Color represents a color using RGB. /// Color represents a color using RGB.
/// ///
/// This is a packed struct so that the C API to read color values just /// This is a packed struct so that the C API to read color values just