feat: port GTK version fallbacks, helpgen, bench modes, CLI leftovers

- apprt-gtk: Ghostty's runtime+build-time version gating (at_least)
  with cargo features adw_1_3/adw_1_4/adw_1_5/gtk_4_16 (default on) and
  the fallbacks for older libraries: MessageDialog (adw < 1.5),
  clipboard dialog without SwitchRow, pre-Banner debug warning and
  child-exited widgets, the old about dialog, GTK 4.16 CSS error source
  bytes. The minimum stays GTK 4.14 / libadwaita 1.5 (Ghostty's floor).
  The root build links with -z lazy so version-gated symbols don't keep
  older libraries from loading.
- ghostty-helpgen (port of helpgen.zig) generates CLI action help and
  keybinding action docs from doc comments at build time, replacing the
  checked-in help/*.txt; +help and +list-actions --docs stay
  byte-identical to Ghostty 1.3.1.
- ghostty-bench: libc wcwidth mode and separate simd/table and
  uucode/table code paths like Zig.
- ghostty-cli: Windows branches of edit-config, list-colors,
  list-keybinds and boo; ssh-cache DiskCache flock locking (no-op on
  Windows, as in Zig) with an exclusivity test.

Test Plan:
- cargo test --workspace
- diff <(cargo run -q -- +list-actions --docs) \
  <(ghostty +list-actions --docs)
This commit is contained in:
ddidderr committed 2026-10-12 06:38:33 +02:00
1 parent 020beb2d75
commit 4787565397
51 files changed
+2426 -1621

No files matched your search

+13 -1
View File
@@ -5,6 +5,18 @@ edition.workspace = true
license.workspace = true
build = "build.rs"
[features]
# Build-time ("header") capability levels, the Rust counterpart of the C
# headers' version macros that Zig's `gtk_version.atLeast`/`adw_version.atLeast`
# check at comptime. Every `atLeast` is "build level >= requested AND runtime
# library >= requested", so with a lower build level the code that needs newer
# API (and the Zig compatibility fallbacks) is compiled instead.
default = ["adw_1_5", "gtk_4_16"]
adw_1_3 = []
adw_1_4 = ["adw_1_3"]
adw_1_5 = ["adw_1_4"]
gtk_4_16 = ["gtk/v4_16"]
[dependencies]
ghostty-apprt.workspace = true
ghostty-config.workspace = true
@@ -16,7 +28,7 @@ ghostty-vt.workspace = true
log = "0.4"
thiserror = "2"
bitflags = "2"
gtk = { package = "gtk4", version = "0.11", features = ["v4_14", "gnome_44"] }
gtk = { package = "gtk4", version = "0.11", features = ["v4_14"] }
adw = { package = "libadwaita", version = "0.9", features = ["v1_5"] }
gtk4-layer-shell = "0.8"
gdk4-x11 = { version = "0.11.5", features = ["x11", "xlib"] }
+57 -3
View File
@@ -10,6 +10,10 @@ pub const COMPTIME_VERSION: Version =
/// Returns the runtime libadwaita version.
#[allow(unsafe_code)]
pub fn get_runtime_version() -> Version {
#[cfg(debug_assertions)]
if let Some(v) = crate::fake_version::get("GHOSTTY_DEBUG_FAKE_ADW_VERSION") {
return v;
}
// The safe `adw::major_version()` etc. assert that libadwaita was
// initialized, but the C functions don't need it (and `+version` has no
// display to initialize GTK with).
@@ -31,9 +35,36 @@ pub fn log_version() {
log::info!("libadwaita version build={a}.{b}.{c} runtime={x}.{y}.{z}");
}
/// Verifies that the libadwaita version at runtime is at least the given
/// version (callers needing newer API must also gate on a cargo feature).
/// The libadwaita API level this build uses, i.e. the highest `adw_1_*`
/// cargo feature (the equivalent of the version macros in `adwaita.h` that
/// Zig's `comptime_version` reads).
pub const BUILD_LEVEL: Version = if cfg!(feature = "adw_1_5") {
(1, 5, 0)
} else if cfg!(feature = "adw_1_4") {
(1, 4, 0)
} else if cfg!(feature = "adw_1_3") {
(1, 3, 0)
} else {
(1, 2, 0)
};
/// Compile-time half of [`at_least`]: whether this build was compiled with at
/// least the given libadwaita API level.
#[must_use]
pub const fn comptime_at_least(major: u32, minor: u32, micro: u32) -> bool {
let (a, b, c) = BUILD_LEVEL;
a > major || (a == major && (b > minor || (b == minor && c >= micro)))
}
/// Verifies that the running libadwaita version is at least the given
/// version, for both the build level and the library we are running against
/// (so generally you want both checks, like Zig's `atLeast`).
#[inline]
pub fn at_least(major: u32, minor: u32, micro: u32) -> bool {
// A lower build level means we must not call newer symbols at all.
if !comptime_at_least(major, minor, micro) {
return false;
}
runtime_at_least(major, minor, micro)
}
@@ -63,6 +94,14 @@ pub fn supports_banner() -> bool {
at_least(1, 3, 0)
}
/// Compile-time (build level only) variants of the `supports_*` checks. Zig
/// uses `supportsX()` at comptime to choose a class's parent type; Rust
/// picks types with the matching `cfg(feature = "adw_1_*")` and these const
/// functions document/assert the same decision.
pub const COMPTIME_SUPPORTS_DIALOGS: bool = comptime_at_least(1, 5, 0);
pub const COMPTIME_SUPPORTS_SWITCH_ROW: bool = comptime_at_least(1, 4, 0);
pub const COMPTIME_SUPPORTS_BANNER: bool = comptime_at_least(1, 3, 0);
#[cfg(test)]
mod tests {
use super::*;
@@ -75,7 +114,8 @@ mod tests {
return;
}
let (a, b, c) = get_runtime_version();
for f in [at_least, runtime_at_least] {
{
let f = runtime_at_least;
assert!(f(a, b, c));
assert!(!f(a, b, c + 1));
assert!(!f(a, b + 1, c));
@@ -86,4 +126,18 @@ mod tests {
assert!(f(a, b - 1, c + 1));
}
}
#[test]
fn version_at_least_build_level() {
let (a, b, c) = BUILD_LEVEL;
assert!(at_least(a, b, c) || get_runtime_version() < BUILD_LEVEL);
assert!(!at_least(a, b, c + 1));
assert!(!at_least(a, b + 1, c));
assert!(!at_least(a + 1, b, c));
assert!(comptime_at_least(a, b, c));
assert!(!comptime_at_least(a, b, c + 1));
assert_eq!(COMPTIME_SUPPORTS_DIALOGS, cfg!(feature = "adw_1_5"));
assert_eq!(COMPTIME_SUPPORTS_SWITCH_ROW, cfg!(feature = "adw_1_4"));
assert_eq!(COMPTIME_SUPPORTS_BANNER, cfg!(feature = "adw_1_3"));
}
}
@@ -15,7 +15,10 @@ use ghostty_apprt::structs::ClipboardRequest;
use ghostty_os::i18n::tr;
use gtk::{glib, glib::subclass::Signal};
use super::dialog::{Dialog, DialogImpl};
use super::dialog::{Dialog, dialog_subclass_wrapper, impl_dialog_parent};
/// Whether we're able to have the remember switch (Zig: `can_remember`).
pub const CAN_REMEMBER: bool = crate::adw_version::COMPTIME_SUPPORTS_SWITCH_ROW;
/// GBoxed wrapper for [`ClipboardRequest`] (Zig: boxed `apprt.ClipboardRequest`).
#[derive(Debug, Clone, Copy, PartialEq, Eq, glib::Boxed)]
@@ -27,13 +30,10 @@ mod imp {
use gtk::{glib, subclass::prelude::*};
use super::{
AdwAlertDialogImpl,
AdwDialogImpl,
Cell,
ClipboardRequest,
ClipboardRequestBoxed,
Dialog,
DialogImpl,
ObjectImpl,
ObjectSubclass,
OnceLock,
@@ -45,7 +45,14 @@ mod imp {
#[derive(Default, glib::Properties, gtk::CompositeTemplate)]
#[properties(wrapper_type = super::ClipboardConfirmationDialog)]
#[template(resource = "/com/mitchellh/ghostty/ui/1.4/clipboard-confirmation-dialog.ui")]
#[cfg_attr(
feature = "adw_1_4",
template(resource = "/com/mitchellh/ghostty/ui/1.4/clipboard-confirmation-dialog.ui")
)]
#[cfg_attr(
not(feature = "adw_1_4"),
template(resource = "/com/mitchellh/ghostty/ui/1.0/clipboard-confirmation-dialog.ui")
)]
pub struct ClipboardConfirmationDialog {
/// The request that this dialog is for.
#[property(get, set, nullable)]
@@ -71,6 +78,8 @@ mod imp {
pub reveal_button: TemplateChild<gtk::Button>,
#[template_child]
pub hide_button: TemplateChild<gtk::Button>,
// Only exists in the 1.4+ template (`adw::SwitchRow`).
#[cfg(feature = "adw_1_4")]
#[template_child]
pub remember_choice: TemplateChild<adw::SwitchRow>,
}
@@ -190,26 +199,24 @@ mod imp {
}
impl WidgetImpl for ClipboardConfirmationDialog {}
impl AdwDialogImpl for ClipboardConfirmationDialog {}
impl AdwAlertDialogImpl for ClipboardConfirmationDialog {
fn response(&self, response: &str) {
let remember = self.remember_choice.is_active();
let obj = self.obj();
if response == "cancel" {
obj.emit_by_name::<()>("deny", &[&remember]);
} else if response == "ok" {
obj.emit_by_name::<()>("confirm", &[&remember]);
}
super::impl_dialog_parent!(ClipboardConfirmationDialog, |this, response| {
#[cfg(feature = "adw_1_4")]
let remember = this.remember_choice.is_active();
#[cfg(not(feature = "adw_1_4"))]
let remember = false;
let obj = this.obj();
if response == "cancel" {
obj.emit_by_name::<()>("deny", &[&remember]);
} else if response == "ok" {
obj.emit_by_name::<()>("confirm", &[&remember]);
}
}
impl DialogImpl for ClipboardConfirmationDialog {}
});
}
glib::wrapper! {
pub struct ClipboardConfirmationDialog(ObjectSubclass<imp::ClipboardConfirmationDialog>)
@extends Dialog, adw::AlertDialog, adw::Dialog, gtk::Widget,
@implements gtk::Accessible, gtk::Buildable, gtk::ConstraintTarget, gtk::ShortcutManager;
}
dialog_subclass_wrapper!(
ClipboardConfirmationDialog,
imp::ClipboardConfirmationDialog
);
impl ClipboardConfirmationDialog {
pub fn new() -> Self {
@@ -8,7 +8,18 @@ use adw::{prelude::*, subclass::prelude::*};
use ghostty_os::i18n::tr;
use gtk::{glib, glib::subclass::Signal};
use super::dialog::{Dialog, DialogImpl};
use super::dialog::{Dialog, dialog_subclass_wrapper, impl_dialog_parent};
/// Name of the "user confirmed the close" signal. Zig always uses
/// `close-request`, but with the `adw::MessageDialog` parent (a `GtkWindow`)
/// that name already exists in the class ancestry, which makes GObject
/// refuse to register it (and would fire it on every window close). Zig's
/// fallback hits that, so the fallback build renames the signal.
const CLOSE_REQUEST: &str = if cfg!(feature = "adw_1_5") {
"close-request"
} else {
"dialog-close-request"
};
/// The target of a close dialog (`GhosttyCloseConfirmationDialogTarget`).
#[derive(Debug, Default, Clone, Copy, PartialEq, Eq, glib::Enum)]
@@ -47,19 +58,7 @@ impl Target {
mod imp {
use gtk::{glib, glib::prelude::*, subclass::prelude::*};
use super::{
AdwAlertDialogImpl,
AdwDialogImpl,
Cell,
Dialog,
DialogImpl,
ObjectImpl,
ObjectSubclass,
OnceLock,
Signal,
Target,
WidgetImpl,
};
use super::{Cell, Dialog, ObjectImpl, ObjectSubclass, OnceLock, Signal, Target, WidgetImpl};
#[derive(Default, glib::Properties, gtk::CompositeTemplate)]
#[properties(wrapper_type = super::CloseConfirmationDialog)]
@@ -91,7 +90,7 @@ mod imp {
static SIGNALS: OnceLock<Vec<Signal>> = OnceLock::new();
SIGNALS.get_or_init(|| {
vec![
Signal::builder("close-request").build(),
Signal::builder(super::CLOSE_REQUEST).build(),
Signal::builder("cancel").build(),
]
})
@@ -102,25 +101,17 @@ mod imp {
}
}
impl WidgetImpl for CloseConfirmationDialog {}
impl AdwDialogImpl for CloseConfirmationDialog {}
impl AdwAlertDialogImpl for CloseConfirmationDialog {
fn response(&self, response: &str) {
let name = if response == "close" {
"close-request"
} else {
"cancel"
};
self.obj().emit_by_name::<()>(name, &[]);
}
}
impl DialogImpl for CloseConfirmationDialog {}
super::impl_dialog_parent!(CloseConfirmationDialog, |this, response| {
let name = if response == "close" {
super::CLOSE_REQUEST
} else {
"cancel"
};
this.obj().emit_by_name::<()>(name, &[]);
});
}
glib::wrapper! {
pub struct CloseConfirmationDialog(ObjectSubclass<imp::CloseConfirmationDialog>)
@extends Dialog, adw::AlertDialog, adw::Dialog, gtk::Widget,
@implements gtk::Accessible, gtk::Buildable, gtk::ConstraintTarget, gtk::ShortcutManager;
}
dialog_subclass_wrapper!(CloseConfirmationDialog, imp::CloseConfirmationDialog);
impl CloseConfirmationDialog {
pub fn new(target: Target) -> Self {
@@ -142,7 +133,7 @@ impl CloseConfirmationDialog {
pub fn connect_close_request<F: Fn(&Self) + 'static>(&self, f: F) -> glib::SignalHandlerId {
self.connect_closure(
"close-request",
CLOSE_REQUEST,
false,
glib::closure_local!(move |obj: &Self| f(obj)),
)
@@ -9,18 +9,15 @@ use gtk::{glib, glib::subclass::Signal};
use super::{
config::Config,
dialog::{Dialog, DialogImpl},
dialog::{Dialog, dialog_subclass_wrapper, impl_dialog_parent},
};
mod imp {
use gtk::{glib, glib::prelude::*, subclass::prelude::*};
use super::{
AdwAlertDialogImpl,
AdwDialogImpl,
Config,
Dialog,
DialogImpl,
ObjectImpl,
ObjectSubclass,
ObjectSubclassExt,
@@ -69,23 +66,15 @@ mod imp {
}
}
impl WidgetImpl for ConfigErrorsDialog {}
impl AdwDialogImpl for ConfigErrorsDialog {}
impl AdwAlertDialogImpl for ConfigErrorsDialog {
fn response(&self, response: &str) {
if response != "reload" {
return;
}
self.obj().emit_by_name::<()>("reload-config", &[]);
super::impl_dialog_parent!(ConfigErrorsDialog, |this, response| {
if response != "reload" {
return;
}
}
impl DialogImpl for ConfigErrorsDialog {}
this.obj().emit_by_name::<()>("reload-config", &[]);
});
}
glib::wrapper! {
pub struct ConfigErrorsDialog(ObjectSubclass<imp::ConfigErrorsDialog>)
@extends Dialog, adw::AlertDialog, adw::Dialog, gtk::Widget,
@implements gtk::Accessible, gtk::Buildable, gtk::ConstraintTarget, gtk::ShortcutManager;
}
dialog_subclass_wrapper!(ConfigErrorsDialog, imp::ConfigErrorsDialog);
impl ConfigErrorsDialog {
pub fn new(config: &Config) -> Self {
@@ -1,5 +1,9 @@
//! Port of apprt/gtk/class/debug_warning.zig (`GhosttyDebugWarning`).
//! Uses the `adw::Banner` variant (libadwaita >= 1.3).
//!
//! Debug warning banner. It is based on `adw::Banner` (a `adw::Bin` with the
//! 1.3 template) if we're using Adwaita 1.3 or newer (the `adw_1_3` build
//! level, Zig: `adw_version.supportsBanner()`). Otherwise it is a `gtk::Box`
//! holding a label (1.2 template).
use gtk::glib;
@@ -7,14 +11,24 @@ mod imp {
use gtk::{glib, subclass::prelude::*};
#[derive(Default, gtk::CompositeTemplate)]
#[template(resource = "/com/mitchellh/ghostty/ui/1.3/debug-warning.ui")]
#[cfg_attr(
feature = "adw_1_3",
template(resource = "/com/mitchellh/ghostty/ui/1.3/debug-warning.ui")
)]
#[cfg_attr(
not(feature = "adw_1_3"),
template(resource = "/com/mitchellh/ghostty/ui/1.2/debug-warning.ui")
)]
pub struct DebugWarning;
#[glib::object_subclass]
impl ObjectSubclass for DebugWarning {
const NAME: &'static str = "GhosttyDebugWarning";
type Type = super::DebugWarning;
#[cfg(feature = "adw_1_3")]
type ParentType = adw::Bin;
#[cfg(not(feature = "adw_1_3"))]
type ParentType = gtk::Box;
fn class_init(klass: &mut Self::Class) {
klass.bind_template();
@@ -31,15 +45,26 @@ mod imp {
}
}
impl WidgetImpl for DebugWarning {}
#[cfg(feature = "adw_1_3")]
impl adw::subclass::prelude::BinImpl for DebugWarning {}
#[cfg(not(feature = "adw_1_3"))]
impl BoxImpl for DebugWarning {}
}
#[cfg(feature = "adw_1_3")]
glib::wrapper! {
pub struct DebugWarning(ObjectSubclass<imp::DebugWarning>)
@extends adw::Bin, gtk::Widget,
@implements gtk::Accessible, gtk::Buildable, gtk::ConstraintTarget;
}
#[cfg(not(feature = "adw_1_3"))]
glib::wrapper! {
pub struct DebugWarning(ObjectSubclass<imp::DebugWarning>)
@extends gtk::Box, gtk::Widget,
@implements gtk::Accessible, gtk::Buildable, gtk::ConstraintTarget, gtk::Orientable;
}
impl DebugWarning {
pub fn new() -> Self {
crate::register_resources();
+119 -8
View File
@@ -1,12 +1,25 @@
//! Port of apprt/gtk/class/dialog.zig (`GhosttyDialog`).
//!
//! Thin `adw::AlertDialog` subclass (we require libadwaita >= 1.5 so the
//! `adw::MessageDialog` fallback is not needed). Subclasses implement
//! [`DialogImpl`] and may override `response` via `AlertDialogImpl`.
//! Thin abstraction over `adw::AlertDialog` and `adw::MessageDialog`, chosen
//! at compile time from the libadwaita build level exactly like Zig's
//! `Parent = if (adw_version.supportsDialogs()) adw.AlertDialog else
//! adw.MessageDialog` (the `adw_1_5` cargo feature plays the role of the
//! header version). Subclasses implement [`DialogImpl`] and override
//! `response` through [`impl_dialog_parent!`].
//!
//! Once we drop support for Adwaita <= 1.2, this can be fully removed and we
//! can use `adw::AlertDialog` directly.
use adw::{prelude::*, subclass::prelude::*};
use gtk::glib;
/// The parent class of [`Dialog`] (Zig: `Dialog.Parent`).
#[cfg(feature = "adw_1_5")]
pub type DialogParent = adw::AlertDialog;
/// The parent class of [`Dialog`] (Zig: `Dialog.Parent`).
#[cfg(not(feature = "adw_1_5"))]
pub type DialogParent = adw::MessageDialog;
mod imp {
use adw::subclass::prelude::*;
use gtk::glib;
@@ -18,46 +31,144 @@ mod imp {
impl ObjectSubclass for Dialog {
const NAME: &'static str = "GhosttyDialog";
type Type = super::Dialog;
type ParentType = adw::AlertDialog;
type ParentType = super::DialogParent;
}
impl ObjectImpl for Dialog {}
impl WidgetImpl for Dialog {}
#[cfg(feature = "adw_1_5")]
impl AdwDialogImpl for Dialog {}
#[cfg(feature = "adw_1_5")]
impl AdwAlertDialogImpl for Dialog {}
#[cfg(not(feature = "adw_1_5"))]
impl WindowImpl for Dialog {}
#[cfg(not(feature = "adw_1_5"))]
impl MessageDialogImpl for Dialog {}
}
#[cfg(feature = "adw_1_5")]
glib::wrapper! {
pub struct Dialog(ObjectSubclass<imp::Dialog>)
@extends adw::AlertDialog, adw::Dialog, gtk::Widget,
@implements gtk::Accessible, gtk::Buildable, gtk::ConstraintTarget, gtk::ShortcutManager;
}
#[cfg(not(feature = "adw_1_5"))]
glib::wrapper! {
pub struct Dialog(ObjectSubclass<imp::Dialog>)
@extends adw::MessageDialog, gtk::Window, gtk::Widget,
@implements gtk::Accessible, gtk::Buildable, gtk::ConstraintTarget, gtk::Native,
gtk::Root, gtk::ShortcutManager;
}
/// Implemented by subclasses of [`Dialog`].
#[cfg(feature = "adw_1_5")]
pub trait DialogImpl: AdwAlertDialogImpl {}
/// Implemented by subclasses of [`Dialog`].
#[cfg(not(feature = "adw_1_5"))]
pub trait DialogImpl: MessageDialogImpl {}
#[allow(unsafe_code)]
// SAFETY: `Dialog` adds no class/instance data of its own; we simply forward
// to the parent (`adw::AlertDialog`) class/instance initialization, which is
// valid for any `T: DialogImpl` (a subtype of `AdwAlertDialogImpl`).
// to the parent class/instance initialization, which is valid for any
// `T: DialogImpl` (a subtype of the parent's `*Impl` trait).
unsafe impl<T: DialogImpl> IsSubclassable<T> for Dialog {
fn class_init(class: &mut glib::Class<Self>) {
<adw::AlertDialog as IsSubclassable<T>>::class_init(class.upcast_ref_mut());
<DialogParent as IsSubclassable<T>>::class_init(class.upcast_ref_mut());
}
fn instance_init(instance: &mut glib::subclass::InitializingObject<T>) {
<adw::AlertDialog as IsSubclassable<T>>::instance_init(instance);
<DialogParent as IsSubclassable<T>>::instance_init(instance);
}
}
impl Dialog {
/// Present the dialog over `parent`.
#[cfg(feature = "adw_1_5")]
pub fn present(&self, parent: Option<&impl IsA<gtk::Widget>>) {
adw::prelude::AdwDialogExt::present(self, parent);
}
/// Present the dialog over `parent`.
#[cfg(not(feature = "adw_1_5"))]
pub fn present(&self, parent: Option<&impl IsA<gtk::Widget>>) {
// Reset the previous parent window
self.set_transient_for(None::<&gtk::Window>);
// We need to get the window for the parent in order to set the
// transient-for property on the MessageDialog.
if let Some(window) = parent
.and_then(gtk::prelude::WidgetExt::root)
.and_then(|root| root.downcast::<gtk::Window>().ok())
{
self.set_transient_for(Some(&window));
}
gtk::prelude::GtkWindowExt::present(self);
}
/// Close the dialog without emitting `close-attempt`.
#[cfg(feature = "adw_1_5")]
pub fn close(&self) {
self.force_close();
}
/// Close the dialog.
#[cfg(not(feature = "adw_1_5"))]
pub fn close(&self) {
gtk::prelude::GtkWindowExt::close(self);
}
}
/// Declares the `glib::wrapper!` of a [`Dialog`] subclass with the right
/// `@extends` chain for the libadwaita build level.
macro_rules! dialog_subclass_wrapper {
($name:ident, $imp:ty) => {
#[cfg(feature = "adw_1_5")]
glib::wrapper! {
pub struct $name(ObjectSubclass<$imp>)
@extends $crate::class::dialog::Dialog, adw::AlertDialog, adw::Dialog, gtk::Widget,
@implements gtk::Accessible, gtk::Buildable, gtk::ConstraintTarget,
gtk::ShortcutManager;
}
#[cfg(not(feature = "adw_1_5"))]
glib::wrapper! {
pub struct $name(ObjectSubclass<$imp>)
@extends $crate::class::dialog::Dialog, adw::MessageDialog, gtk::Window,
gtk::Widget,
@implements gtk::Accessible, gtk::Buildable, gtk::ConstraintTarget,
gtk::Native, gtk::Root, gtk::ShortcutManager;
}
};
}
/// Implements the parent-class traits of a [`Dialog`] subclass (the
/// `Adw{Dialog,AlertDialog}Impl` or `{Window,MessageDialog}Impl` pair),
/// overriding `response` with `|this, response| body`.
macro_rules! impl_dialog_parent {
($ty:ty, |$this:ident, $response:ident| $body:block) => {
#[cfg(feature = "adw_1_5")]
impl adw::subclass::prelude::AdwDialogImpl for $ty {}
#[cfg(feature = "adw_1_5")]
impl adw::subclass::prelude::AdwAlertDialogImpl for $ty {
fn response(&self, $response: &str) {
let $this = self;
$body
}
}
#[cfg(not(feature = "adw_1_5"))]
impl gtk::subclass::prelude::WindowImpl for $ty {}
#[cfg(not(feature = "adw_1_5"))]
impl adw::subclass::prelude::MessageDialogImpl for $ty {
fn response(&self, $response: &str) {
let $this = self;
$body
}
}
impl $crate::class::dialog::DialogImpl for $ty {}
};
}
pub(crate) use dialog_subclass_wrapper;
pub(crate) use impl_dialog_parent;
@@ -1371,6 +1371,13 @@ impl Surface {
self.imp().child_exited.set(true);
self.notify("child-exited");
// If we have the noop child exited overlay then we don't do anything
// for child exited. The false return will force libghostty to show
// the normal text-based message.
if SurfaceChildExited::NOOP {
return false;
}
self.imp().child_exited_overlay.set_child_exited(Some(data));
true
}
@@ -1,26 +1,41 @@
//! Port of apprt/gtk/class/surface_child_exited.zig.
//!
//! Child exited overlay based on `adw.Banner` (Adwaita 1.3+; the crate
//! requires 1.5 so the Zig `SurfaceChildExitedNoop` fallback is omitted).
//! Emits `close-request` when the banner button is clicked.
//! Child exited overlay based on `adw.Banner` (Adwaita 1.3+, the `adw_1_3`
//! build level; emits `close-request` when the banner button is clicked).
//! Without it Zig compiles `SurfaceChildExitedNoop`, an empty widget that
//! immediately asks to be closed (`NOOP == true`); so does this port.
#[cfg(feature = "adw_1_3")]
use std::sync::OnceLock;
#[cfg(feature = "adw_1_3")]
use adw::{prelude::*, subclass::prelude::*};
use ghostty_apprt::surface::ChildExited;
use gtk::{glib, glib::subclass::Signal};
use gtk::glib;
#[cfg(feature = "adw_1_3")]
use gtk::glib::subclass::Signal;
#[cfg(not(feature = "adw_1_3"))]
use gtk::prelude::*;
/// Boxed wrapper so `ChildExited` can be a GObject property.
#[derive(Debug, Clone, Copy, PartialEq, Eq, glib::Boxed)]
#[boxed_type(name = "GhosttyChildExited", nullable)]
pub struct ChildExitedBoxed(pub ChildExited);
#[cfg(feature = "adw_1_3")]
glib::wrapper! {
pub struct SurfaceChildExited(ObjectSubclass<imp::SurfaceChildExited>)
@extends adw::Bin, gtk::Widget,
@implements gtk::Accessible, gtk::Buildable, gtk::ConstraintTarget;
}
#[cfg(not(feature = "adw_1_3"))]
glib::wrapper! {
pub struct SurfaceChildExited(ObjectSubclass<imp::SurfaceChildExited>)
@extends gtk::Widget,
@implements gtk::Accessible, gtk::Buildable, gtk::ConstraintTarget;
}
impl Default for SurfaceChildExited {
fn default() -> Self {
Self::new()
@@ -28,16 +43,27 @@ impl Default for SurfaceChildExited {
}
impl SurfaceChildExited {
/// Can be detected at compile time (Zig: `pub const noop = true`).
pub const NOOP: bool = !cfg!(feature = "adw_1_3");
#[must_use]
pub fn new() -> Self {
glib::Object::new()
}
/// Set the child exited data (Zig: `setData`).
#[cfg(feature = "adw_1_3")]
pub fn set_child_exited(&self, data: Option<&ChildExited>) {
self.set_data(data.map(|d| ChildExitedBoxed(*d)));
}
/// Set the child exited data (Zig: `setData`); the noop widget just asks
/// to be closed.
#[cfg(not(feature = "adw_1_3"))]
pub fn set_child_exited(&self, _data: Option<&ChildExited>) {
self.emit_by_name::<()>("close-request", &[]);
}
pub fn connect_close_request<F: Fn(&Self) + 'static>(&self, f: F) -> glib::SignalHandlerId {
self.connect_closure(
"close-request",
@@ -47,6 +73,7 @@ impl SurfaceChildExited {
}
}
#[cfg(feature = "adw_1_3")]
mod imp {
use std::cell::RefCell;
@@ -141,3 +168,31 @@ mod imp {
impl WidgetImpl for SurfaceChildExited {}
impl BinImpl for SurfaceChildExited {}
}
/// Empty widget that does nothing if we don't have a new enough Adwaita
/// version to support the child exited banner.
#[cfg(not(feature = "adw_1_3"))]
mod imp {
use std::sync::OnceLock;
use gtk::{glib, glib::subclass::Signal, subclass::prelude::*};
#[derive(Default)]
pub struct SurfaceChildExited;
#[glib::object_subclass]
impl ObjectSubclass for SurfaceChildExited {
const NAME: &'static str = "GhosttySurfaceChildExited";
type Type = super::SurfaceChildExited;
type ParentType = gtk::Widget;
}
impl ObjectImpl for SurfaceChildExited {
fn signals() -> &'static [Signal] {
static SIGNALS: OnceLock<Vec<Signal>> = OnceLock::new();
SIGNALS.get_or_init(|| vec![Signal::builder("close-request").build()])
}
}
impl WidgetImpl for SurfaceChildExited {}
}
+23 -12
View File
@@ -1657,18 +1657,29 @@ impl Window {
let icon = "com.mitchellh.ghostty";
let website = "https://ghostty.org";
// libadwaita >= 1.5 is assumed, so AdwAboutDialog is always used
// (the Zig GtkAboutDialog fallback is unreachable here).
let _ = adw_version::supports_dialogs();
let dialog = adw::AboutDialog::builder()
.application_name(name)
.developer_name(tr("Ghostty Developers"))
.application_icon(icon)
.version(env!("CARGO_PKG_VERSION"))
.issue_url("https://github.com/ghostty-org/ghostty/issues")
.website(website)
.build();
dialog.present(Some(self));
if adw_version::supports_dialogs() {
let dialog = adw::AboutDialog::builder()
.application_name(name)
.developer_name(tr("Ghostty Developers"))
.application_icon(icon)
.version(env!("CARGO_PKG_VERSION"))
.issue_url("https://github.com/ghostty-org/ghostty/issues")
.website(website)
.build();
dialog.present(Some(self));
} else {
// libadwaita < 1.5 has no AdwAboutDialog: use the GTK one.
gtk::show_about_dialog(
Some(self),
&[
("program-name", &name),
("logo-icon-name", &icon),
("title", &tr("About Ghostty")),
("version", &env!("CARGO_PKG_VERSION")),
("website", &website),
],
);
}
}
fn action_close_tab(&self, param: Option<&glib::Variant>) {
+42 -2
View File
@@ -31,13 +31,36 @@ pub struct CssProviders {
}
fn at_least_gtk(major: u32, minor: u32) -> bool {
(gtk::major_version(), gtk::minor_version()) >= (major, minor)
crate::gtk_version::runtime_at_least(major, minor, 0)
}
/// Log a CSS parsing error (Zig: `signalCssParsingError`).
fn log_parsing_error(section: &gtk::CssSection, err: &glib::Error) {
let location = section.to_str();
log::warn!("css parsing failed at {location}: {err}");
// GTK >= 4.16 can also hand us the offending source bytes.
#[cfg(feature = "gtk_4_16")]
if crate::gtk_version::at_least(4, 16, 0)
&& let Some(bytes) = section.bytes()
{
log::warn!("{}", parsing_error_message(&location, err, Some(&bytes)));
return;
}
log::warn!("{}", parsing_error_message(&location, err, None));
}
/// Builds the log line for [`log_parsing_error`]; with `data` the source
/// bytes follow on their own line (`css parsing failed at {s}: {s} {d} {s}\n{s}`).
fn parsing_error_message(location: &str, err: &glib::Error, data: Option<&[u8]>) -> String {
let base = format!(
"css parsing failed at {location}: {} {} {}",
err.domain().as_str(),
err.code(),
err.message(),
);
match data {
Some(data) => format!("{base}\n{}", String::from_utf8_lossy(data)),
None => base,
}
}
fn new_provider() -> gtk::CssProvider {
@@ -330,4 +353,21 @@ mod tests {
assert!(runtime_css(&c, true).contains("--ghostty-bg"));
assert!(runtime_css(&c, false).contains("windowhandle:backdrop"));
}
#[test]
fn parsing_error_message_includes_source_bytes() {
let err = glib::Error::with_domain(
glib::Quark::from_str("gtk-css-parser-error-quark"),
7,
"bad",
);
assert_eq!(
parsing_error_message("<data>:1:2", &err, None),
"css parsing failed at <data>:1:2: gtk-css-parser-error-quark 7 bad"
);
assert_eq!(
parsing_error_message("<data>:1:2", &err, Some(b"a { b }")),
"css parsing failed at <data>:1:2: gtk-css-parser-error-quark 7 bad\na { b }"
);
}
}
@@ -0,0 +1,37 @@
//! Debug-build-only override of the runtime library versions, used to
//! exercise the version-gated compatibility fallbacks on a machine that only
//! has new GTK/libadwaita installed (there is no Zig counterpart).
//!
//! `GHOSTTY_DEBUG_FAKE_GTK_VERSION=4.14.0` / `GHOSTTY_DEBUG_FAKE_ADW_VERSION=1.4.0`
//! make `gtk_version::get_runtime_version` / `adw_version::get_runtime_version`
//! report that version. It only changes what the `at_least` family answers;
//! the real libraries are still the ones that are linked. Compiled out of
//! release builds.
use crate::gtk_version::Version;
/// Parses `major.minor[.micro]` from the environment variable `var`.
pub(crate) fn get(var: &str) -> Option<Version> {
parse(&std::env::var(var).ok()?)
}
fn parse(s: &str) -> Option<Version> {
let mut it = s.trim().split('.');
let major = it.next()?.parse().ok()?;
let minor = it.next()?.parse().ok()?;
let micro = it.next().map_or(Some(0), |v| v.parse().ok())?;
Some((major, minor, micro))
}
#[cfg(test)]
mod tests {
use super::parse;
#[test]
fn parses_versions() {
assert_eq!(parse("1.4"), Some((1, 4, 0)));
assert_eq!(parse("4.14.2"), Some((4, 14, 2)));
assert_eq!(parse("x"), None);
assert_eq!(parse("1"), None);
}
}
+53 -6
View File
@@ -31,8 +31,22 @@ pub(crate) const fn parse_version(s: &str) -> (u32, u32, u32) {
(parts[0], parts[1], parts[2])
}
/// The GTK API level this build uses, i.e. the highest `gtk_4_*` cargo
/// feature (the equivalent of the version macros in the C headers that Zig's
/// `comptime_version` reads). Code calling newer symbols must be gated with
/// the matching `cfg(feature = ...)` as well as [`at_least`].
pub const BUILD_LEVEL: Version = if cfg!(feature = "gtk_4_16") {
(4, 16, 0)
} else {
(4, 14, 0)
};
/// Returns the runtime GTK version.
pub fn get_runtime_version() -> Version {
#[cfg(debug_assertions)]
if let Some(v) = crate::fake_version::get("GHOSTTY_DEBUG_FAKE_GTK_VERSION") {
return v;
}
(
gtk::major_version(),
gtk::minor_version(),
@@ -47,12 +61,28 @@ pub fn log_version() {
log::info!("GTK version build={a}.{b}.{c} runtime={x}.{y}.{z}");
}
/// Verifies that the GTK version at runtime is at least the given version.
/// Unlike Zig this can't compile out unknown symbols; callers needing newer
/// API must gate it on a cargo feature too.
/// Compile-time half of [`at_least`]: whether this build was compiled with
/// at least the given GTK API level. `const` so that, like Zig's `comptime`
/// check, it folds away the runtime check when it fails.
#[must_use]
pub const fn comptime_at_least(major: u32, minor: u32, micro: u32) -> bool {
let (a, b, c) = BUILD_LEVEL;
a > major || (a == major && (b > minor || (b == minor && c >= micro)))
}
/// Verifies that the GTK version is at least the given version.
///
/// Both the build level ([`comptime_at_least`]) and the library we are
/// running against must satisfy it. Use this where the check affects which
/// symbols are called; for checks that only depend on GTK's runtime behavior
/// use [`runtime_at_least`].
#[inline]
pub fn at_least(major: u32, minor: u32, micro: u32) -> bool {
// Anything below the build level is trivially satisfied by the runtime
// requirement of the build level, but we still check the runtime.
// If our build level is lower than the given version we return false
// immediately; this prevents us from calling unknown symbols.
if !comptime_at_least(major, minor, micro) {
return false;
}
runtime_at_least(major, minor, micro)
}
@@ -73,7 +103,8 @@ mod tests {
#[test]
fn at_least_test() {
let (a, b, c) = get_runtime_version();
for f in [at_least, runtime_at_least] {
{
let f = runtime_at_least;
assert!(f(a, b, c));
assert!(!f(a, b, c + 1));
assert!(!f(a, b + 1, c));
@@ -85,6 +116,22 @@ mod tests {
}
}
#[test]
fn at_least_build_level_test() {
// Zig's `atLeast` test, against the build level (our "headers").
let (a, b, c) = BUILD_LEVEL;
assert!(at_least(a, b, c));
assert!(!at_least(a, b, c + 1));
assert!(!at_least(a, b + 1, c));
assert!(!at_least(a + 1, b, c));
assert!(at_least(a - 1, b, c));
assert!(at_least(a - 1, b + 1, c));
assert!(at_least(a - 1, b, c + 1));
assert!(at_least(a, b - 1, c + 1));
assert!(comptime_at_least(a, b, c));
assert!(!comptime_at_least(a, b, c + 1));
}
#[test]
fn runtime_until_test() {
let (a, b, c) = get_runtime_version();
+2
View File
@@ -8,6 +8,8 @@ pub mod class;
pub mod css;
pub mod debug_input;
pub mod ext;
#[cfg(debug_assertions)]
mod fake_version;
pub mod flatpak;
pub mod gl;
pub mod gsettings;