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:
1 parent
cd86fe92a8
commit
f2252bb165
51 files changed
+2426
-1621
No files matched your search
@@ -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"] }
|
||||
|
||||
@@ -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();
|
||||
|
||||
@@ -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::<>k::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 {}
|
||||
}
|
||||
@@ -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>) {
|
||||
|
||||
@@ -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: >k::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);
|
||||
}
|
||||
}
|
||||
@@ -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();
|
||||
|
||||
@@ -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;
|
||||
|
||||
Reference in new issue
Block a user