From f2252bb1657fbb84ac256b05fa22e6224faf493a Mon Sep 17 00:00:00 2001 From: ddidderr Date: Mon, 12 Oct 2026 06:38:33 +0200 Subject: [PATCH] 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) --- build.rs | 16 + crates/ghostty-apprt-gtk/Cargo.toml | 14 +- crates/ghostty-apprt-gtk/src/adw_version.rs | 60 +- .../class/clipboard_confirmation_dialog.rs | 51 +- .../src/class/close_confirmation_dialog.rs | 57 +- .../src/class/config_errors_dialog.rs | 25 +- .../src/class/debug_warning.rs | 29 +- crates/ghostty-apprt-gtk/src/class/dialog.rs | 127 +- crates/ghostty-apprt-gtk/src/class/surface.rs | 7 + .../src/class/surface_child_exited.rs | 63 +- crates/ghostty-apprt-gtk/src/class/window.rs | 35 +- crates/ghostty-apprt-gtk/src/css.rs | 44 +- crates/ghostty-apprt-gtk/src/fake_version.rs | 37 + crates/ghostty-apprt-gtk/src/gtk_version.rs | 59 +- crates/ghostty-apprt-gtk/src/lib.rs | 2 + crates/ghostty-bench/Cargo.toml | 35 +- crates/ghostty-bench/src/benches.rs | 224 ++- crates/ghostty-bench/src/lib.rs | 1 + crates/ghostty-bench/src/simd_width.rs | 616 ++++++ crates/ghostty-cli/Cargo.toml | 3 + crates/ghostty-cli/build.rs | 37 + crates/ghostty-cli/src/boo.rs | 10 + crates/ghostty-cli/src/edit_config.rs | 31 +- crates/ghostty-cli/src/ghostty.rs | 20 +- crates/ghostty-cli/src/help/boo.txt | 1 - crates/ghostty-cli/src/help/crash-report.txt | 6 - crates/ghostty-cli/src/help/edit-config.txt | 20 - crates/ghostty-cli/src/help/help.txt | 5 - crates/ghostty-cli/src/help/list-actions.txt | 7 - crates/ghostty-cli/src/help/list-colors.txt | 7 - crates/ghostty-cli/src/help/list-fonts.txt | 24 - crates/ghostty-cli/src/help/list-keybinds.txt | 17 - crates/ghostty-cli/src/help/list-themes.txt | 37 - crates/ghostty-cli/src/help/new-window.txt | 62 - crates/ghostty-cli/src/help/show-config.txt | 27 - crates/ghostty-cli/src/help/show-face.txt | 21 - crates/ghostty-cli/src/help/ssh-cache.txt | 23 - .../ghostty-cli/src/help/validate-config.txt | 9 - crates/ghostty-cli/src/help/version.txt | 2 - .../ghostty-cli/src/ssh_cache/disk_cache.rs | 91 +- crates/ghostty-cli/src/tui.rs | 11 + crates/ghostty-helpgen/Cargo.toml | 9 + crates/ghostty-helpgen/src/lib.rs | 218 +++ crates/ghostty-input/Cargo.toml | 3 + crates/ghostty-input/build.rs | 29 + crates/ghostty-input/src/binding/action.rs | 1650 +++++------------ docs/rewrite/ARCHITECTURE.md | 15 +- docs/rewrite/notes/compat-GC-bench.md | 18 + docs/rewrite/notes/compat-GC-cli.md | 13 + docs/rewrite/notes/compat-GC-helpgen.md | 43 + docs/rewrite/notes/compat-GC.md | 76 + 51 files changed, 2426 insertions(+), 1621 deletions(-) create mode 100644 build.rs create mode 100644 crates/ghostty-apprt-gtk/src/fake_version.rs create mode 100644 crates/ghostty-bench/src/simd_width.rs delete mode 100644 crates/ghostty-cli/src/help/boo.txt delete mode 100644 crates/ghostty-cli/src/help/crash-report.txt delete mode 100644 crates/ghostty-cli/src/help/edit-config.txt delete mode 100644 crates/ghostty-cli/src/help/help.txt delete mode 100644 crates/ghostty-cli/src/help/list-actions.txt delete mode 100644 crates/ghostty-cli/src/help/list-colors.txt delete mode 100644 crates/ghostty-cli/src/help/list-fonts.txt delete mode 100644 crates/ghostty-cli/src/help/list-keybinds.txt delete mode 100644 crates/ghostty-cli/src/help/list-themes.txt delete mode 100644 crates/ghostty-cli/src/help/new-window.txt delete mode 100644 crates/ghostty-cli/src/help/show-config.txt delete mode 100644 crates/ghostty-cli/src/help/show-face.txt delete mode 100644 crates/ghostty-cli/src/help/ssh-cache.txt delete mode 100644 crates/ghostty-cli/src/help/validate-config.txt delete mode 100644 crates/ghostty-cli/src/help/version.txt create mode 100644 crates/ghostty-helpgen/Cargo.toml create mode 100644 crates/ghostty-helpgen/src/lib.rs create mode 100644 crates/ghostty-input/build.rs create mode 100644 docs/rewrite/notes/compat-GC-bench.md create mode 100644 docs/rewrite/notes/compat-GC-cli.md create mode 100644 docs/rewrite/notes/compat-GC-helpgen.md create mode 100644 docs/rewrite/notes/compat-GC.md diff --git a/build.rs b/build.rs new file mode 100644 index 0000000..4ce698d --- /dev/null +++ b/build.rs @@ -0,0 +1,16 @@ +//! Link the final binary with lazy symbol binding. +//! +//! Rust links executables with `-z now` (eager binding) by default. The GTK +//! apprt compiles in code for newer GTK/libadwaita API (e.g. the GTK 4.16 CSS +//! diagnostics) that is only ever called after a runtime `at_least` check, +//! exactly like the Zig build, which links GTK dynamically with lazy binding. +//! With eager binding the dynamic loader would refuse to start the program on +//! an older library that merely lacks such a symbol; with lazy binding the +//! symbol is only resolved if the guarded code actually runs. + +fn main() { + println!("cargo:rerun-if-changed=build.rs"); + if std::env::var("CARGO_CFG_TARGET_OS").as_deref() == Ok("linux") { + println!("cargo:rustc-link-arg-bins=-Wl,-z,lazy"); + } +} diff --git a/crates/ghostty-apprt-gtk/Cargo.toml b/crates/ghostty-apprt-gtk/Cargo.toml index 4037b7b..b58eb7a 100644 --- a/crates/ghostty-apprt-gtk/Cargo.toml +++ b/crates/ghostty-apprt-gtk/Cargo.toml @@ -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"] } diff --git a/crates/ghostty-apprt-gtk/src/adw_version.rs b/crates/ghostty-apprt-gtk/src/adw_version.rs index 4c1ad4a..3533d23 100644 --- a/crates/ghostty-apprt-gtk/src/adw_version.rs +++ b/crates/ghostty-apprt-gtk/src/adw_version.rs @@ -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")); + } } diff --git a/crates/ghostty-apprt-gtk/src/class/clipboard_confirmation_dialog.rs b/crates/ghostty-apprt-gtk/src/class/clipboard_confirmation_dialog.rs index 1686d31..8b6ae92 100644 --- a/crates/ghostty-apprt-gtk/src/class/clipboard_confirmation_dialog.rs +++ b/crates/ghostty-apprt-gtk/src/class/clipboard_confirmation_dialog.rs @@ -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, #[template_child] pub hide_button: TemplateChild, + // Only exists in the 1.4+ template (`adw::SwitchRow`). + #[cfg(feature = "adw_1_4")] #[template_child] pub remember_choice: TemplateChild, } @@ -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) - @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 { diff --git a/crates/ghostty-apprt-gtk/src/class/close_confirmation_dialog.rs b/crates/ghostty-apprt-gtk/src/class/close_confirmation_dialog.rs index e0d79f1..0b73b9b 100644 --- a/crates/ghostty-apprt-gtk/src/class/close_confirmation_dialog.rs +++ b/crates/ghostty-apprt-gtk/src/class/close_confirmation_dialog.rs @@ -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> = 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) - @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(&self, f: F) -> glib::SignalHandlerId { self.connect_closure( - "close-request", + CLOSE_REQUEST, false, glib::closure_local!(move |obj: &Self| f(obj)), ) diff --git a/crates/ghostty-apprt-gtk/src/class/config_errors_dialog.rs b/crates/ghostty-apprt-gtk/src/class/config_errors_dialog.rs index 1b132f8..29b7695 100644 --- a/crates/ghostty-apprt-gtk/src/class/config_errors_dialog.rs +++ b/crates/ghostty-apprt-gtk/src/class/config_errors_dialog.rs @@ -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) - @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 { diff --git a/crates/ghostty-apprt-gtk/src/class/debug_warning.rs b/crates/ghostty-apprt-gtk/src/class/debug_warning.rs index 1d984e3..4538834 100644 --- a/crates/ghostty-apprt-gtk/src/class/debug_warning.rs +++ b/crates/ghostty-apprt-gtk/src/class/debug_warning.rs @@ -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) @extends adw::Bin, gtk::Widget, @implements gtk::Accessible, gtk::Buildable, gtk::ConstraintTarget; } +#[cfg(not(feature = "adw_1_3"))] +glib::wrapper! { + pub struct DebugWarning(ObjectSubclass) + @extends gtk::Box, gtk::Widget, + @implements gtk::Accessible, gtk::Buildable, gtk::ConstraintTarget, gtk::Orientable; +} + impl DebugWarning { pub fn new() -> Self { crate::register_resources(); diff --git a/crates/ghostty-apprt-gtk/src/class/dialog.rs b/crates/ghostty-apprt-gtk/src/class/dialog.rs index 89f3d28..48f08e7 100644 --- a/crates/ghostty-apprt-gtk/src/class/dialog.rs +++ b/crates/ghostty-apprt-gtk/src/class/dialog.rs @@ -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) @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) + @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 IsSubclassable for Dialog { fn class_init(class: &mut glib::Class) { - >::class_init(class.upcast_ref_mut()); + >::class_init(class.upcast_ref_mut()); } fn instance_init(instance: &mut glib::subclass::InitializingObject) { - >::instance_init(instance); + >::instance_init(instance); } } impl Dialog { /// Present the dialog over `parent`. + #[cfg(feature = "adw_1_5")] pub fn present(&self, parent: Option<&impl IsA>) { 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>) { + // 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::().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; diff --git a/crates/ghostty-apprt-gtk/src/class/surface.rs b/crates/ghostty-apprt-gtk/src/class/surface.rs index e3ace17..faa21d7 100644 --- a/crates/ghostty-apprt-gtk/src/class/surface.rs +++ b/crates/ghostty-apprt-gtk/src/class/surface.rs @@ -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 } diff --git a/crates/ghostty-apprt-gtk/src/class/surface_child_exited.rs b/crates/ghostty-apprt-gtk/src/class/surface_child_exited.rs index 72821fd..c7faba1 100644 --- a/crates/ghostty-apprt-gtk/src/class/surface_child_exited.rs +++ b/crates/ghostty-apprt-gtk/src/class/surface_child_exited.rs @@ -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) @extends adw::Bin, gtk::Widget, @implements gtk::Accessible, gtk::Buildable, gtk::ConstraintTarget; } +#[cfg(not(feature = "adw_1_3"))] +glib::wrapper! { + pub struct SurfaceChildExited(ObjectSubclass) + @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(&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> = OnceLock::new(); + SIGNALS.get_or_init(|| vec![Signal::builder("close-request").build()]) + } + } + + impl WidgetImpl for SurfaceChildExited {} +} diff --git a/crates/ghostty-apprt-gtk/src/class/window.rs b/crates/ghostty-apprt-gtk/src/class/window.rs index ce51b5c..67c4007 100644 --- a/crates/ghostty-apprt-gtk/src/class/window.rs +++ b/crates/ghostty-apprt-gtk/src/class/window.rs @@ -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>) { diff --git a/crates/ghostty-apprt-gtk/src/css.rs b/crates/ghostty-apprt-gtk/src/css.rs index 1abb094..30fb8da 100644 --- a/crates/ghostty-apprt-gtk/src/css.rs +++ b/crates/ghostty-apprt-gtk/src/css.rs @@ -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(":1:2", &err, None), + "css parsing failed at :1:2: gtk-css-parser-error-quark 7 bad" + ); + assert_eq!( + parsing_error_message(":1:2", &err, Some(b"a { b }")), + "css parsing failed at :1:2: gtk-css-parser-error-quark 7 bad\na { b }" + ); + } } diff --git a/crates/ghostty-apprt-gtk/src/fake_version.rs b/crates/ghostty-apprt-gtk/src/fake_version.rs new file mode 100644 index 0000000..e4c1859 --- /dev/null +++ b/crates/ghostty-apprt-gtk/src/fake_version.rs @@ -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 { + parse(&std::env::var(var).ok()?) +} + +fn parse(s: &str) -> Option { + 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); + } +} diff --git a/crates/ghostty-apprt-gtk/src/gtk_version.rs b/crates/ghostty-apprt-gtk/src/gtk_version.rs index 42d0595..5a17537 100644 --- a/crates/ghostty-apprt-gtk/src/gtk_version.rs +++ b/crates/ghostty-apprt-gtk/src/gtk_version.rs @@ -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(); diff --git a/crates/ghostty-apprt-gtk/src/lib.rs b/crates/ghostty-apprt-gtk/src/lib.rs index c4f3401..85ad49b 100644 --- a/crates/ghostty-apprt-gtk/src/lib.rs +++ b/crates/ghostty-apprt-gtk/src/lib.rs @@ -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; diff --git a/crates/ghostty-bench/Cargo.toml b/crates/ghostty-bench/Cargo.toml index b0df767..c20cf04 100644 --- a/crates/ghostty-bench/Cargo.toml +++ b/crates/ghostty-bench/Cargo.toml @@ -19,5 +19,36 @@ ghostty-unicode.workspace = true ghostty-vt.workspace = true log = "0.4.34" -[lints] -workspace = true +# Not `workspace = true`: this crate needs a single audited FFI call (libc +# `wcwidth`) so `unsafe_code` is `deny` (locally allowed) instead of `forbid`. +[lints.rust] +unsafe_code = "deny" + +[lints.clippy] +pedantic = { level = "warn", priority = -1 } +todo = "warn" +unwrap_used = "warn" +# Porting a low-level terminal emulator involves a lot of intentional +# numeric casts and long state-machine functions; these pedantic lints are +# noise for this codebase. +cast_possible_truncation = "allow" +cast_possible_wrap = "allow" +cast_sign_loss = "allow" +cast_precision_loss = "allow" +cast_lossless = "allow" +module_name_repetitions = "allow" +too_many_lines = "allow" +missing_errors_doc = "allow" +missing_panics_doc = "allow" +similar_names = "allow" +struct_excessive_bools = "allow" +fn_params_excessive_bools = "allow" +match_same_arms = "allow" +unreadable_literal = "allow" +many_single_char_names = "allow" +doc_markdown = "allow" +must_use_candidate = "allow" +return_self_not_must_use = "allow" +used_underscore_binding = "allow" +large_stack_arrays = "allow" +inline_always = "allow" diff --git a/crates/ghostty-bench/src/benches.rs b/crates/ghostty-bench/src/benches.rs index 2c2b19f..3de8496 100644 --- a/crates/ghostty-bench/src/benches.rs +++ b/crates/ghostty-bench/src/benches.rs @@ -285,6 +285,43 @@ fn decode_each(d: &mut Utf8Decoder, chunk: &[u8], mut func: impl FnMut(u32)) { } } +/// libc `wcwidth` (`extern "c" fn wcwidth(c: u32) c_int`). +#[cfg(unix)] +#[allow(unsafe_code)] +mod libc_wcwidth { + unsafe extern "C" { + fn wcwidth(c: u32) -> i32; + } + + #[allow(unsafe_code)] + #[inline] + pub fn call(cp: u32) -> i32 { + // SAFETY: `wcwidth` is a pure libc function taking a plain integer + // (wchar_t) and returning an int; no pointers or preconditions. + unsafe { wcwidth(cp) } + } +} + +/// The `table` mode: generated two-stage lookup (`unicode.table.get(cp).width`), +/// with the same `<= 0xFF` shortcut as terminal.zig. +#[inline] +fn width_table( + table: &ghostty_unicode::lut::Tables<'_, ghostty_unicode::Properties>, + cp: u32, +) -> i32 { + if cp <= 0xFF { + 1 + } else { + i32::from(table.get(cp).width) + } +} + +/// `simd.codepointWidth` (scalar port of the highway implementation). +#[must_use] +pub fn simd_width(cp: u32) -> i32 { + i32::from(crate::simd_width::codepoint_width(cp)) +} + impl Benchmark for CodepointWidth { fn setup(&mut self) -> Result { self.data.open() @@ -298,19 +335,37 @@ impl Benchmark for CodepointWidth { match self.mode { WidthMode::Noop => Ok(()), WidthMode::Wcwidth => { - // libc wcwidth needs FFI, which this workspace forbids. - log::warn!("wcwidth mode is not available in the Rust port"); - Ok(()) + #[cfg(unix)] + { + let mut d = Utf8Decoder::new(); + self.data.for_each_chunk(|chunk| { + decode_each(&mut d, chunk, |cp| { + black_box(libc_wcwidth::call(cp)); + }); + Ok(()) + }) + } + #[cfg(not(unix))] + { + log::warn!("wcwidth is not available on this platform"); + Ok(()) + } } - // The Rust port has no separate SIMD width function: both use - // the lookup table. - WidthMode::Simd | WidthMode::Table => { + WidthMode::Simd => { + let mut d = Utf8Decoder::new(); + self.data.for_each_chunk(|chunk| { + decode_each(&mut d, chunk, |cp| { + black_box(simd_width(cp)); + }); + Ok(()) + }) + } + WidthMode::Table => { let mut d = Utf8Decoder::new(); let table = ghostty_unicode::table(); self.data.for_each_chunk(|chunk| { decode_each(&mut d, chunk, |cp| { - // Same trick as terminal.rs. - black_box(if cp <= 0xFF { 1 } else { table.get(cp).width }); + black_box(width_table(&table, cp)); }); Ok(()) }) @@ -375,19 +430,57 @@ impl Benchmark for GraphemeBreak { // --------------------------------------------------------------------------- // is-symbol +#[derive(Debug, Clone, Copy, PartialEq, Eq)] +pub enum SymbolMode { + /// uucode implementation (computed from general category + block). + Uucode, + /// Ghostty's table-based approach. + Table, +} + pub struct IsSymbol { data: Data, + mode: SymbolMode, +} + +/// Computed `is_symbol` (`computeIsSymbol` in uucode_config.zig): private +/// use (general category Co) or one of the symbol blocks, evaluated directly +/// from the ranges rather than via the generated table. +#[must_use] +pub fn is_symbol_computed(cp: u32) -> bool { + matches!( + cp, + // general_category == other_private_use + 0xE000..=0xF8FF | 0xF_0000..=0xF_FFFD | 0x10_0000..=0x10_FFFD + // arrows + | 0x2190..=0x21FF + // dingbats + | 0x2700..=0x27BF + // emoticons + | 0x1F600..=0x1F64F + // miscellaneous_symbols + | 0x2600..=0x26FF + // enclosed_alphanumerics + | 0x2460..=0x24FF + // enclosed_alphanumeric_supplement + | 0x1F100..=0x1F1FF + // miscellaneous_symbols_and_pictographs + | 0x1F300..=0x1F5FF + // transport_and_map_symbols + | 0x1F680..=0x1F6FF + ) } impl IsSymbol { pub fn create(args: &mut Args) -> std::result::Result { - // Modes `uucode` and `table` both use the generated table here. - match args.take_opt_string("mode")?.as_deref() { - None | Some("uucode" | "table") => {} + let mode = match args.take_opt_string("mode")?.as_deref() { + None | Some("uucode") => SymbolMode::Uucode, + Some("table") => SymbolMode::Table, Some(m) => return Err(format!("invalid value for --mode: {m}")), - } + }; Ok(IsSymbol { data: Data::new(args.take_opt_string("data")?), + mode, }) } } @@ -403,13 +496,23 @@ impl Benchmark for IsSymbol { fn step(&mut self) -> Result { let mut d = Utf8Decoder::new(); - let table = ghostty_unicode::symbols_table::table(); - self.data.for_each_chunk(|chunk| { - decode_each(&mut d, chunk, |cp| { - black_box(table.get(cp)); - }); - Ok(()) - }) + match self.mode { + SymbolMode::Uucode => self.data.for_each_chunk(|chunk| { + decode_each(&mut d, chunk, |cp| { + black_box(is_symbol_computed(cp)); + }); + Ok(()) + }), + SymbolMode::Table => { + let table = ghostty_unicode::symbols_table::table(); + self.data.for_each_chunk(|chunk| { + decode_each(&mut d, chunk, |cp| { + black_box(table.get(cp)); + }); + Ok(()) + }) + } + } } } @@ -564,3 +667,86 @@ pub fn main_action(action: &str, mut args: Args) -> std::result::Result Err(format!("invalid action: +{action}")), } } + +#[cfg(test)] +mod tests { + use super::*; + + fn run(action: &str, mode: &str, data: &std::path::Path) -> Result { + let mut args = Args::parse([ + format!("+{action}"), + format!("--mode={mode}"), + format!("--data={}", data.display()), + ]) + .unwrap(); + match action { + "codepoint-width" => crate::benchmark::run( + &mut CodepointWidth::create(&mut args).unwrap(), + crate::benchmark::RunMode::Once, + ), + _ => crate::benchmark::run( + &mut IsSymbol::create(&mut args).unwrap(), + crate::benchmark::RunMode::Once, + ), + } + .map(|_| ()) + } + + fn sample_file(tag: &str) -> std::path::PathBuf { + let p = + std::env::temp_dir().join(format!("ghostty-bench-{tag}-{}.txt", std::process::id())); + std::fs::write( + &p, + "hello \u{4e00}\u{1f600}\u{301}\u{e000} world \u{2190}\n", + ) + .unwrap(); + p + } + + #[test] + fn codepoint_width_all_modes_run() { + let p = sample_file("cw"); + for m in ["noop", "wcwidth", "simd", "table"] { + run("codepoint-width", m, &p).unwrap(); + } + let mut args = Args::parse(["--mode=bogus".to_owned()]).unwrap(); + assert!(CodepointWidth::create(&mut args).is_err()); + let _ = std::fs::remove_file(p); + } + + #[test] + fn is_symbol_modes_run() { + let p = sample_file("sym"); + for m in ["uucode", "table"] { + run("is-symbol", m, &p).unwrap(); + } + let _ = std::fs::remove_file(p); + } + + #[test] + fn is_symbol_computed_matches_table() { + let t = ghostty_unicode::symbols_table::table(); + for cp in 0..=0x11_0000u32 { + assert_eq!(is_symbol_computed(cp), t.get(cp), "cp={cp:x}"); + } + } + + #[test] + fn simd_matches_table_on_samples() { + let t = ghostty_unicode::table(); + for cp in [ + 0x61, 0x100, 0x3400, 0x2E3A, 0x1F1E6, 0x4E00, 0xF900, 0x1F600, 0x20000, 0x30000, 0x301, + 0x200B, 0x200D, 0xFE0F, 0x20DD, 0xE0100, 0xE0001, 0x1160, 0xAC00, + ] { + assert_eq!(simd_width(cp), width_table(&t, cp), "cp={cp:x}"); + } + } + + #[cfg(unix)] + #[test] + fn wcwidth_ascii() { + // The C locale is in effect (no setlocale, like Zig): ASCII only. + assert_eq!(libc_wcwidth::call(u32::from(b'a')), 1); + assert_eq!(libc_wcwidth::call(0), 0); + } +} diff --git a/crates/ghostty-bench/src/lib.rs b/crates/ghostty-bench/src/lib.rs index fe0b1bf..d6fe7f0 100644 --- a/crates/ghostty-bench/src/lib.rs +++ b/crates/ghostty-bench/src/lib.rs @@ -10,4 +10,5 @@ pub mod args; pub mod benches; pub mod benchmark; +mod simd_width; pub mod synthetic; diff --git a/crates/ghostty-bench/src/simd_width.rs b/crates/ghostty-bench/src/simd_width.rs new file mode 100644 index 0000000..ba9de3d --- /dev/null +++ b/crates/ghostty-bench/src/simd_width.rs @@ -0,0 +1,616 @@ +//! Scalar port of `src/simd/codepoint_width.cpp` (the highway SIMD +//! implementation behind `simd.codepointWidth`). The C++ tests N inclusive +//! `[gte, lte]` ranges per vector op; here each range table is scanned +//! sequentially, with identical tables and precedence order. +//! +//! The range tables are copied verbatim from the C++ source (the trailing +//! zero padding used for vector alignment is dropped). + +const EAW_16: &[(u32, u32)] = &[ + (0x3000, 0x3000), + (0xff01, 0xff60), + (0xffe0, 0xffe6), + (0x1100, 0x115f), + (0x231a, 0x231b), + (0x2329, 0x2329), + (0x232a, 0x232a), + (0x23e9, 0x23ec), + (0x23f0, 0x23f0), + (0x23f3, 0x23f3), + (0x25fd, 0x25fe), + (0x2614, 0x2615), + (0x2648, 0x2653), + (0x267f, 0x267f), + (0x2693, 0x2693), + (0x26a1, 0x26a1), + (0x26aa, 0x26ab), + (0x26bd, 0x26be), + (0x26c4, 0x26c5), + (0x26ce, 0x26ce), + (0x26d4, 0x26d4), + (0x26ea, 0x26ea), + (0x26f2, 0x26f3), + (0x26f5, 0x26f5), + (0x26fa, 0x26fa), + (0x26fd, 0x26fd), + (0x2705, 0x2705), + (0x270a, 0x270b), + (0x2728, 0x2728), + (0x274c, 0x274c), + (0x274e, 0x274e), + (0x2753, 0x2755), + (0x2757, 0x2757), + (0x2795, 0x2797), + (0x27b0, 0x27b0), + (0x27bf, 0x27bf), + (0x2b1b, 0x2b1c), + (0x2b50, 0x2b50), + (0x2b55, 0x2b55), + (0x2e80, 0x2e99), + (0x2e9b, 0x2ef3), + (0x2f00, 0x2fd5), + (0x2ff0, 0x2ffb), + (0x3001, 0x3029), + (0x302e, 0x303e), + (0x3041, 0x3096), + (0x309b, 0x309c), + (0x309d, 0x309e), + (0x309f, 0x309f), + (0x30a0, 0x30a0), + (0x30a1, 0x30fa), + (0x30fb, 0x30fb), + (0x30fc, 0x30fe), + (0x30ff, 0x30ff), + (0x3105, 0x312f), + (0x3131, 0x318e), + (0x3190, 0x3191), + (0x3192, 0x3195), + (0x3196, 0x319f), + (0x31a0, 0x31bf), + (0x31c0, 0x31e3), + (0x31f0, 0x31ff), + (0x3200, 0x321e), + (0x3220, 0x3229), + (0x322a, 0x3247), + (0x3250, 0x3250), + (0x3251, 0x325f), + (0x3260, 0x327f), + (0x3280, 0x3289), + (0x328a, 0x32b0), + (0x32b1, 0x32bf), + (0x32c0, 0x33ff), + (0x3400, 0x4bdf), + (0x4e00, 0xa014), + (0xa015, 0xa015), + (0xa016, 0xa48c), + (0xa490, 0xa4c6), + (0xa960, 0xa97c), + (0xac00, 0xd7a3), + (0xf900, 0xfa6d), + (0xfa70, 0xfad9), + (0xfe10, 0xfe19), + (0xfe30, 0xfe52), + (0xfe54, 0xfe66), + (0xfe68, 0xfe6b), +]; + +const EAW_32: &[(u32, u32)] = &[ + (0x16fe0, 0x16fe3), + (0x16ff0, 0x16ff1), + (0x17000, 0x187f7), + (0x18800, 0x18cd5), + (0x18d00, 0x18d08), + (0x1aff0, 0x1aff3), + (0x1aff5, 0x1affb), + (0x1affd, 0x1affe), + (0x1b000, 0x1b122), + (0x1b132, 0x1b132), + (0x1b150, 0x1b152), + (0x1b155, 0x1b155), + (0x1b164, 0x1b167), + (0x1b170, 0x1b2fb), + (0x1f004, 0x1f004), + (0x1f0cf, 0x1f0cf), + (0x1f18e, 0x1f18e), + (0x1f191, 0x1f19a), + (0x1f200, 0x1f202), + (0x1f210, 0x1f23b), + (0x1f240, 0x1f248), + (0x1f250, 0x1f251), + (0x1f260, 0x1f265), + (0x1f300, 0x1f320), + (0x1f32d, 0x1f335), + (0x1f337, 0x1f37c), + (0x1f37e, 0x1f393), + (0x1f3a0, 0x1f3ca), + (0x1f3cf, 0x1f3d3), + (0x1f3e0, 0x1f3f0), + (0x1f3f4, 0x1f3f4), + (0x1f3f8, 0x1f3fa), + (0x1f3fb, 0x1f3ff), + (0x1f400, 0x1f43e), + (0x1f440, 0x1f440), + (0x1f442, 0x1f4fc), + (0x1f4ff, 0x1f53d), + (0x1f54b, 0x1f54e), + (0x1f550, 0x1f567), + (0x1f57a, 0x1f57a), + (0x1f595, 0x1f596), + (0x1f5a4, 0x1f5a4), + (0x1f5fb, 0x1f64f), + (0x1f680, 0x1f6c5), + (0x1f6cc, 0x1f6cc), + (0x1f6d0, 0x1f6d2), + (0x1f6d5, 0x1f6d7), + (0x1f6dc, 0x1f6df), + (0x1f6eb, 0x1f6ec), + (0x1f6f4, 0x1f6fc), + (0x1f7e0, 0x1f7eb), + (0x1f7f0, 0x1f7f0), + (0x1f90c, 0x1f93a), + (0x1f93c, 0x1f945), + (0x1f947, 0x1f9ff), + (0x1fa70, 0x1fa7c), + (0x1fa80, 0x1fa88), + (0x1fa90, 0x1fabd), + (0x1fabf, 0x1fac5), + (0x1face, 0x1fadb), + (0x1fae0, 0x1fae8), + (0x1faf0, 0x1faf8), + (0x20000, 0x2a6df), + (0x2a700, 0x2b739), + (0x2b740, 0x2b81d), + (0x2b820, 0x2cea1), + (0x2ceb0, 0x2ebe0), + (0x2f800, 0x2fa1d), + (0x30000, 0x3134a), + (0x31350, 0x323af), +]; + +const ZERO_16: &[(u32, u32)] = &[ + (0xad, 0xad), + (0x70f, 0x70f), + (0x890, 0x891), + (0x180e, 0x180e), + (0x200b, 0x200f), + (0x202a, 0x202e), + (0x2060, 0x2064), + (0x2066, 0x206f), + (0xfeff, 0xfeff), + (0xfff9, 0xfffb), + (0x488, 0x489), + (0x1abe, 0x1abe), + (0x20dd, 0x20e0), + (0x20e2, 0x20e4), + (0xa670, 0xa672), +]; + +const ZERO_32: &[(u32, u32)] = &[ + (0x110bd, 0x110bd), + (0x110cd, 0x110cd), + (0x13430, 0x1343f), + (0x1bca0, 0x1bca3), + (0x1d173, 0x1d17a), + (0xe0001, 0xe0001), + (0xe0020, 0xe007f), +]; + +const NSM_16: &[(u32, u32)] = &[ + (0x300, 0x36f), + (0x483, 0x487), + (0x591, 0x5bd), + (0x5bf, 0x5bf), + (0x5c1, 0x5c2), + (0x5c4, 0x5c5), + (0x5c7, 0x5c7), + (0x610, 0x61a), + (0x64b, 0x65f), + (0x670, 0x670), + (0x6d6, 0x6dc), + (0x6df, 0x6e4), + (0x6e7, 0x6e8), + (0x6ea, 0x6ed), + (0x711, 0x711), + (0x730, 0x74a), + (0x7a6, 0x7b0), + (0x7eb, 0x7f3), + (0x7fd, 0x7fd), + (0x816, 0x819), + (0x81b, 0x823), + (0x825, 0x827), + (0x829, 0x82d), + (0x859, 0x85b), + (0x898, 0x89f), + (0x8ca, 0x8e1), + (0x8e3, 0x902), + (0x93a, 0x93a), + (0x93c, 0x93c), + (0x941, 0x948), + (0x94d, 0x94d), + (0x951, 0x957), + (0x962, 0x963), + (0x981, 0x981), + (0x9bc, 0x9bc), + (0x9c1, 0x9c4), + (0x9cd, 0x9cd), + (0x9e2, 0x9e3), + (0x9fe, 0x9fe), + (0xa01, 0xa02), + (0xa3c, 0xa3c), + (0xa41, 0xa42), + (0xa47, 0xa48), + (0xa4b, 0xa4d), + (0xa51, 0xa51), + (0xa70, 0xa71), + (0xa75, 0xa75), + (0xa81, 0xa82), + (0xabc, 0xabc), + (0xac1, 0xac5), + (0xac7, 0xac8), + (0xacd, 0xacd), + (0xae2, 0xae3), + (0xafa, 0xaff), + (0xb01, 0xb01), + (0xb3c, 0xb3c), + (0xb3f, 0xb3f), + (0xb41, 0xb44), + (0xb4d, 0xb4d), + (0xb55, 0xb56), + (0xb62, 0xb63), + (0xb82, 0xb82), + (0xbc0, 0xbc0), + (0xbcd, 0xbcd), + (0xc00, 0xc00), + (0xc04, 0xc04), + (0xc3c, 0xc3c), + (0xc3e, 0xc40), + (0xc46, 0xc48), + (0xc4a, 0xc4d), + (0xc55, 0xc56), + (0xc62, 0xc63), + (0xc81, 0xc81), + (0xcbc, 0xcbc), + (0xcbf, 0xcbf), + (0xcc6, 0xcc6), + (0xccc, 0xccd), + (0xce2, 0xce3), + (0xd00, 0xd01), + (0xd3b, 0xd3c), + (0xd41, 0xd44), + (0xd4d, 0xd4d), + (0xd62, 0xd63), + (0xd81, 0xd81), + (0xdca, 0xdca), + (0xdd2, 0xdd4), + (0xdd6, 0xdd6), + (0xe31, 0xe31), + (0xe34, 0xe3a), + (0xe47, 0xe4e), + (0xeb1, 0xeb1), + (0xeb4, 0xebc), + (0xec8, 0xece), + (0xf18, 0xf19), + (0xf35, 0xf35), + (0xf37, 0xf37), + (0xf39, 0xf39), + (0xf71, 0xf7e), + (0xf80, 0xf84), + (0xf86, 0xf87), + (0xf8d, 0xf97), + (0xf99, 0xfbc), + (0xfc6, 0xfc6), + (0x102d, 0x1030), + (0x1032, 0x1037), + (0x1039, 0x103a), + (0x103d, 0x103e), + (0x1058, 0x1059), + (0x105e, 0x1060), + (0x1071, 0x1074), + (0x1082, 0x1082), + (0x1085, 0x1086), + (0x108d, 0x108d), + (0x109d, 0x109d), + (0x135d, 0x135f), + (0x1712, 0x1714), + (0x1732, 0x1733), + (0x1752, 0x1753), + (0x1772, 0x1773), + (0x17b4, 0x17b5), + (0x17b7, 0x17bd), + (0x17c6, 0x17c6), + (0x17c9, 0x17d3), + (0x17dd, 0x17dd), + (0x180b, 0x180d), + (0x180f, 0x180f), + (0x1885, 0x1886), + (0x18a9, 0x18a9), + (0x1920, 0x1922), + (0x1927, 0x1928), + (0x1932, 0x1932), + (0x1939, 0x193b), + (0x1a17, 0x1a18), + (0x1a1b, 0x1a1b), + (0x1a56, 0x1a56), + (0x1a58, 0x1a5e), + (0x1a60, 0x1a60), + (0x1a62, 0x1a62), + (0x1a65, 0x1a6c), + (0x1a73, 0x1a7c), + (0x1a7f, 0x1a7f), + (0x1ab0, 0x1abd), + (0x1abf, 0x1ace), + (0x1b00, 0x1b03), + (0x1b34, 0x1b34), + (0x1b36, 0x1b3a), + (0x1b3c, 0x1b3c), + (0x1b42, 0x1b42), + (0x1b6b, 0x1b73), + (0x1b80, 0x1b81), + (0x1ba2, 0x1ba5), + (0x1ba8, 0x1ba9), + (0x1bab, 0x1bad), + (0x1be6, 0x1be6), + (0x1be8, 0x1be9), + (0x1bed, 0x1bed), + (0x1bef, 0x1bf1), + (0x1c2c, 0x1c33), + (0x1c36, 0x1c37), + (0x1cd0, 0x1cd2), + (0x1cd4, 0x1ce0), + (0x1ce2, 0x1ce8), + (0x1ced, 0x1ced), + (0x1cf4, 0x1cf4), + (0x1cf8, 0x1cf9), + (0x1dc0, 0x1dff), + (0x20d0, 0x20dc), + (0x20e1, 0x20e1), + (0x20e5, 0x20f0), + (0x2cef, 0x2cf1), + (0x2d7f, 0x2d7f), + (0x2de0, 0x2dff), + (0x302a, 0x302d), + (0x3099, 0x309a), + (0xa66f, 0xa66f), + (0xa674, 0xa67d), + (0xa69e, 0xa69f), + (0xa6f0, 0xa6f1), + (0xa802, 0xa802), + (0xa806, 0xa806), + (0xa80b, 0xa80b), + (0xa825, 0xa826), + (0xa82c, 0xa82c), + (0xa8c4, 0xa8c5), + (0xa8e0, 0xa8f1), + (0xa8ff, 0xa8ff), + (0xa926, 0xa92d), + (0xa947, 0xa951), + (0xa980, 0xa982), + (0xa9b3, 0xa9b3), + (0xa9b6, 0xa9b9), + (0xa9bc, 0xa9bd), + (0xa9e5, 0xa9e5), + (0xaa29, 0xaa2e), + (0xaa31, 0xaa32), + (0xaa35, 0xaa36), + (0xaa43, 0xaa43), + (0xaa4c, 0xaa4c), + (0xaa7c, 0xaa7c), + (0xaab0, 0xaab0), + (0xaab2, 0xaab4), + (0xaab7, 0xaab8), + (0xaabe, 0xaabf), + (0xaac1, 0xaac1), + (0xaaec, 0xaaed), + (0xaaf6, 0xaaf6), + (0xabe5, 0xabe5), + (0xabe8, 0xabe8), + (0xabed, 0xabed), + (0xfb1e, 0xfb1e), + (0xfe00, 0xfe0f), + (0xfe20, 0xfe2f), +]; + +const NSM_32: &[(u32, u32)] = &[ + (0x101fd, 0x101fd), + (0x102e0, 0x102e0), + (0x10376, 0x1037a), + (0x10a01, 0x10a03), + (0x10a05, 0x10a06), + (0x10a0c, 0x10a0f), + (0x10a38, 0x10a3a), + (0x10a3f, 0x10a3f), + (0x10ae5, 0x10ae6), + (0x10d24, 0x10d27), + (0x10eab, 0x10eac), + (0x10efd, 0x10eff), + (0x10f46, 0x10f50), + (0x10f82, 0x10f85), + (0x11001, 0x11001), + (0x11038, 0x11046), + (0x11070, 0x11070), + (0x11073, 0x11074), + (0x1107f, 0x11081), + (0x110b3, 0x110b6), + (0x110b9, 0x110ba), + (0x110c2, 0x110c2), + (0x11100, 0x11102), + (0x11127, 0x1112b), + (0x1112d, 0x11134), + (0x11173, 0x11173), + (0x11180, 0x11181), + (0x111b6, 0x111be), + (0x111c9, 0x111cc), + (0x111cf, 0x111cf), + (0x1122f, 0x11231), + (0x11234, 0x11234), + (0x11236, 0x11237), + (0x1123e, 0x1123e), + (0x11241, 0x11241), + (0x112df, 0x112df), + (0x112e3, 0x112ea), + (0x11300, 0x11301), + (0x1133b, 0x1133c), + (0x11340, 0x11340), + (0x11366, 0x1136c), + (0x11370, 0x11374), + (0x11438, 0x1143f), + (0x11442, 0x11444), + (0x11446, 0x11446), + (0x1145e, 0x1145e), + (0x114b3, 0x114b8), + (0x114ba, 0x114ba), + (0x114bf, 0x114c0), + (0x114c2, 0x114c3), + (0x115b2, 0x115b5), + (0x115bc, 0x115bd), + (0x115bf, 0x115c0), + (0x115dc, 0x115dd), + (0x11633, 0x1163a), + (0x1163d, 0x1163d), + (0x1163f, 0x11640), + (0x116ab, 0x116ab), + (0x116ad, 0x116ad), + (0x116b0, 0x116b5), + (0x116b7, 0x116b7), + (0x1171d, 0x1171f), + (0x11722, 0x11725), + (0x11727, 0x1172b), + (0x1182f, 0x11837), + (0x11839, 0x1183a), + (0x1193b, 0x1193c), + (0x1193e, 0x1193e), + (0x11943, 0x11943), + (0x119d4, 0x119d7), + (0x119da, 0x119db), + (0x119e0, 0x119e0), + (0x11a01, 0x11a0a), + (0x11a33, 0x11a38), + (0x11a3b, 0x11a3e), + (0x11a47, 0x11a47), + (0x11a51, 0x11a56), + (0x11a59, 0x11a5b), + (0x11a8a, 0x11a96), + (0x11a98, 0x11a99), + (0x11c30, 0x11c36), + (0x11c38, 0x11c3d), + (0x11c3f, 0x11c3f), + (0x11c92, 0x11ca7), + (0x11caa, 0x11cb0), + (0x11cb2, 0x11cb3), + (0x11cb5, 0x11cb6), + (0x11d31, 0x11d36), + (0x11d3a, 0x11d3a), + (0x11d3c, 0x11d3d), + (0x11d3f, 0x11d45), + (0x11d47, 0x11d47), + (0x11d90, 0x11d91), + (0x11d95, 0x11d95), + (0x11d97, 0x11d97), + (0x11ef3, 0x11ef4), + (0x11f00, 0x11f01), + (0x11f36, 0x11f3a), + (0x11f40, 0x11f40), + (0x11f42, 0x11f42), + (0x13440, 0x13440), + (0x13447, 0x13455), + (0x16af0, 0x16af4), + (0x16b30, 0x16b36), + (0x16f4f, 0x16f4f), + (0x16f8f, 0x16f92), + (0x16fe4, 0x16fe4), + (0x1bc9d, 0x1bc9e), + (0x1cf00, 0x1cf2d), + (0x1cf30, 0x1cf46), + (0x1d167, 0x1d169), + (0x1d17b, 0x1d182), + (0x1d185, 0x1d18b), + (0x1d1aa, 0x1d1ad), + (0x1d242, 0x1d244), + (0x1da00, 0x1da36), + (0x1da3b, 0x1da6c), + (0x1da75, 0x1da75), + (0x1da84, 0x1da84), + (0x1da9b, 0x1da9f), + (0x1daa1, 0x1daaf), + (0x1e000, 0x1e006), + (0x1e008, 0x1e018), + (0x1e01b, 0x1e021), + (0x1e023, 0x1e024), + (0x1e026, 0x1e02a), + (0x1e08f, 0x1e08f), + (0x1e130, 0x1e136), + (0x1e2ae, 0x1e2ae), + (0x1e2ec, 0x1e2ef), + (0x1e4ec, 0x1e4ef), + (0x1e8d0, 0x1e8d6), + (0x1e944, 0x1e94a), + (0xe0100, 0xe01ef), +]; + +#[inline] +fn in_ranges(ranges: &[(u32, u32)], cp: u32) -> bool { + ranges.iter().any(|&(lo, hi)| cp >= lo && cp <= hi) +} + +/// 16-bit leading ranges. Index >= 5 is zero width, otherwise wide. (2E3B is +/// technically width 3, but we clamp to 2.) +const LEAD_16: &[(u32, u32)] = &[ + (0x2E3A, 0x2E3A), + (0x3400, 0x4DBF), + (0x4E00, 0x9FFF), + (0xF900, 0xFAFF), + (0x2E3B, 0x2E3B), + (0x1160, 0x11FF), + (0x2060, 0x206F), + (0xFFF0, 0xFFF8), +]; +const LEAD_32_WIDE: &[(u32, u32)] = &[(0x1f1e6, 0x1f1ff), (0x20000, 0x2FFFD), (0x30000, 0x3FFFD)]; +const LEAD_32_ZERO: &[(u32, u32)] = &[(0xE0000, 0xE0FFF)]; + +fn width16(cp: u32) -> i8 { + if let Some(i) = LEAD_16.iter().position(|&(lo, hi)| cp >= lo && cp <= hi) { + return if i >= 5 { 0 } else { 2 }; + } + if in_ranges(ZERO_16, cp) { + return 0; + } + if in_ranges(EAW_16, cp) { + return 2; + } + if in_ranges(NSM_16, cp) { + return 0; + } + 1 +} + +fn width32(cp: u32) -> i8 { + if in_ranges(LEAD_32_WIDE, cp) { + return 2; + } + if in_ranges(LEAD_32_ZERO, cp) { + return 0; + } + if in_ranges(ZERO_32, cp) { + return 0; + } + if in_ranges(EAW_32, cp) { + return 2; + } + if in_ranges(NSM_32, cp) { + return 0; + } + 1 +} + +/// `simd.codepointWidth`. Control characters are not special-cased: input +/// `<= 0xFF` is always width 1. +#[must_use] +pub fn codepoint_width(cp: u32) -> i8 { + if cp <= 0xFF { + 1 + } else if cp <= 0xFFFF { + width16(cp) + } else { + width32(cp) + } +} diff --git a/crates/ghostty-cli/Cargo.toml b/crates/ghostty-cli/Cargo.toml index cf13756..643863c 100644 --- a/crates/ghostty-cli/Cargo.toml +++ b/crates/ghostty-cli/Cargo.toml @@ -19,6 +19,9 @@ termina = "0.3.3" thiserror = "2.0.21" unicode-width = "0.2.2" +[build-dependencies] +ghostty-helpgen.workspace = true + [lints] workspace = true diff --git a/crates/ghostty-cli/build.rs b/crates/ghostty-cli/build.rs index 0cb71a0..83c603e 100644 --- a/crates/ghostty-cli/build.rs +++ b/crates/ghostty-cli/build.rs @@ -125,8 +125,45 @@ fn build_mode() -> &'static str { } } +/// Generates the per-action help table (Zig: `helpgen.zig` `genActions`): +/// the doc comment of each action's `run` function becomes +/// `Action::help_text`. +fn gen_action_help(manifest_dir: &Path) { + let ghostty_rs = manifest_dir.join("src/ghostty.rs"); + println!("cargo:rerun-if-changed={}", ghostty_rs.display()); + let src = std::fs::read_to_string(&ghostty_rs).expect("read ghostty.rs"); + let mut table = Vec::new(); + for (variant, _) in ghostty_helpgen::enum_variant_docs(&src, "Action") { + // `ListFonts` -> `list_fonts` (file name, as Zig `Action.file`). + let mut file = String::new(); + for (i, c) in variant.chars().enumerate() { + if c.is_ascii_uppercase() && i > 0 { + file.push('_'); + } + file.push(c.to_ascii_lowercase()); + } + let path = manifest_dir.join(format!("src/{file}.rs")); + println!("cargo:rerun-if-changed={}", path.display()); + let text = std::fs::read_to_string(&path).expect("read action source"); + let help = ghostty_helpgen::run_fn_doc(&text).unwrap_or_else(|| { + panic!("doc comment must be present on run function of the {file} action!") + }); + table.push((variant, help)); + } + assert!(!table.is_empty(), "no actions found"); + let out = PathBuf::from(std::env::var_os("OUT_DIR").expect("OUT_DIR")); + std::fs::write( + out.join("action_help.rs"), + ghostty_helpgen::cli_help_match(&table), + ) + .expect("write action_help.rs"); +} + fn main() { println!("cargo:rerun-if-changed=build.rs"); + gen_action_help(&PathBuf::from( + std::env::var_os("CARGO_MANIFEST_DIR").unwrap_or_default(), + )); println!("cargo:rerun-if-env-changed=RUSTC"); let rustc = std::env::var("RUSTC").unwrap_or_else(|_| "rustc".into()); let version = Command::new(rustc) diff --git a/crates/ghostty-cli/src/boo.rs b/crates/ghostty-cli/src/boo.rs index 337b19b..7119cd6 100644 --- a/crates/ghostty-cli/src/boo.rs +++ b/crates/ghostty-cli/src/boo.rs @@ -180,6 +180,16 @@ impl Boo { /// The `boo` command is used to display the animation from the Ghostty website in the terminal pub fn run(args: &[String]) -> Result { + // Disable on non-desktop systems. + if cfg!(not(any( + windows, + target_os = "macos", + target_os = "linux", + target_os = "freebsd" + ))) { + return Ok(1); + } + let mut opts = Options; crate::parse_options(&mut opts, args)?; diff --git a/crates/ghostty-cli/src/edit_config.rs b/crates/ghostty-cli/src/edit_config.rs index eb6a80a..a6ef00a 100644 --- a/crates/ghostty-cli/src/edit_config.rs +++ b/crates/ghostty-cli/src/edit_config.rs @@ -1,6 +1,8 @@ //! `+edit-config` (ZIG/src/cli/edit_config.zig). -use std::{io::Write, os::unix::process::CommandExt}; +use std::io::Write; +#[cfg(unix)] +use std::os::unix::process::CommandExt; use ghostty_config::config::Config; @@ -62,6 +64,31 @@ fn run_inner(stderr: &mut dyn Write) -> Result { let path = ghostty_config::file_load::preferred_default_file_path() .map_err(|e| Error::Other(e.to_string()))?; + // We don't currently support Windows because we use the exec syscall. + #[cfg(windows)] + { + write_windows_unsupported(stderr, &path) + } + + #[cfg(unix)] + exec_editor(stderr, &path) +} + +/// Windows branch of `run`: print the path and fail. +#[cfg(windows)] +fn write_windows_unsupported(stderr: &mut dyn Write, path: &str) -> Result { + write!( + stderr, + "The `ghostty +edit-config` command is not supported on Windows.\n\ + Please edit the configuration file manually at the following path:\n\ + \n\ + {path}\n" + )?; + Ok(1) +} + +#[cfg(unix)] +fn exec_editor(stderr: &mut dyn Write, path: &str) -> Result { // Get our editor let Some(editor) = find_editor() else { // If we don't have `$EDITOR` set then we can't do anything @@ -87,7 +114,7 @@ fn run_inner(stderr: &mut dyn Write) -> Result { // Flush before exec so no output is lost. let _ = stderr.flush(); - let err = std::process::Command::new(&editor).arg(&path).exec(); + let err = std::process::Command::new(&editor).arg(path).exec(); // If we reached this point then exec failed. let err = match err.kind() { diff --git a/crates/ghostty-cli/src/ghostty.rs b/crates/ghostty-cli/src/ghostty.rs index 6a43dad..ee3b580 100644 --- a/crates/ghostty-cli/src/ghostty.rs +++ b/crates/ghostty-cli/src/ghostty.rs @@ -148,23 +148,9 @@ impl Action { /// function, i.e. `help_strings.Action.`), without trailing newline. #[must_use] pub fn help_text(self) -> &'static str { - match self { - Action::Version => include_str!("help/version.txt"), - Action::Help => include_str!("help/help.txt"), - Action::ListFonts => include_str!("help/list-fonts.txt"), - Action::ListKeybinds => include_str!("help/list-keybinds.txt"), - Action::ListThemes => include_str!("help/list-themes.txt"), - Action::ListColors => include_str!("help/list-colors.txt"), - Action::ListActions => include_str!("help/list-actions.txt"), - Action::SshCache => include_str!("help/ssh-cache.txt"), - Action::EditConfig => include_str!("help/edit-config.txt"), - Action::ShowConfig => include_str!("help/show-config.txt"), - Action::ValidateConfig => include_str!("help/validate-config.txt"), - Action::ShowFace => include_str!("help/show-face.txt"), - Action::CrashReport => include_str!("help/crash-report.txt"), - Action::Boo => include_str!("help/boo.txt"), - Action::NewWindow => include_str!("help/new-window.txt"), - } + // Generated by `build.rs` (via `ghostty-helpgen`) from the doc + // comment of each action's `run` function. + include!(concat!(env!("OUT_DIR"), "/action_help.rs")) } /// Run the action. `args` is the full argument vector (including diff --git a/crates/ghostty-cli/src/help/boo.txt b/crates/ghostty-cli/src/help/boo.txt deleted file mode 100644 index f64b856..0000000 --- a/crates/ghostty-cli/src/help/boo.txt +++ /dev/null @@ -1 +0,0 @@ -The `boo` command is used to display the animation from the Ghostty website in the terminal \ No newline at end of file diff --git a/crates/ghostty-cli/src/help/crash-report.txt b/crates/ghostty-cli/src/help/crash-report.txt deleted file mode 100644 index 6b3a294..0000000 --- a/crates/ghostty-cli/src/help/crash-report.txt +++ /dev/null @@ -1,6 +0,0 @@ -The `crash-report` command is used to inspect and send crash reports. - -When executed without any arguments, this will list existing crash reports. - -This command currently only supports listing crash reports. Viewing -and sending crash reports is unimplemented and will be added in the future. \ No newline at end of file diff --git a/crates/ghostty-cli/src/help/edit-config.txt b/crates/ghostty-cli/src/help/edit-config.txt deleted file mode 100644 index ac4114b..0000000 --- a/crates/ghostty-cli/src/help/edit-config.txt +++ /dev/null @@ -1,20 +0,0 @@ -The `edit-config` command opens the Ghostty configuration file in the -editor specified by the `$VISUAL` or `$EDITOR` environment variables. - -IMPORTANT: This command will not reload the configuration after -editing. You will need to manually reload the configuration using the -application menu, configured keybind, or by restarting Ghostty. We -plan to auto-reload in the future, but Ghostty isn't capable of -this yet. - -The filepath opened is the default user-specific configuration -file, which is typically located at `$XDG_CONFIG_HOME/ghostty/config.ghostty`. -On macOS, this may also be located at -`~/Library/Application Support/com.mitchellh.ghostty/config.ghostty`. -On macOS, whichever path exists and is non-empty will be prioritized, -prioritizing the Application Support directory if neither are -non-empty. - -This command prefers the `$VISUAL` environment variable over `$EDITOR`, -if both are set. If neither are set, it will print an error -and exit. \ No newline at end of file diff --git a/crates/ghostty-cli/src/help/help.txt b/crates/ghostty-cli/src/help/help.txt deleted file mode 100644 index 22550a8..0000000 --- a/crates/ghostty-cli/src/help/help.txt +++ /dev/null @@ -1,5 +0,0 @@ -The `help` command shows general help about Ghostty. Recognized as either -`-h, `--help`, or like other actions `+help`. - -You can also specify `--help` or `-h` along with any action such as -`+list-themes` to see help for a specific action. \ No newline at end of file diff --git a/crates/ghostty-cli/src/help/list-actions.txt b/crates/ghostty-cli/src/help/list-actions.txt deleted file mode 100644 index 7e3e943..0000000 --- a/crates/ghostty-cli/src/help/list-actions.txt +++ /dev/null @@ -1,7 +0,0 @@ -The `list-actions` command is used to list all the available keybind -actions for Ghostty. These are distinct from the CLI Actions which can -be listed via `+help` - -Flags: - - * `--docs`: will print out the documentation for each action. \ No newline at end of file diff --git a/crates/ghostty-cli/src/help/list-colors.txt b/crates/ghostty-cli/src/help/list-colors.txt deleted file mode 100644 index 767b4fd..0000000 --- a/crates/ghostty-cli/src/help/list-colors.txt +++ /dev/null @@ -1,7 +0,0 @@ -The `list-colors` command is used to list all the named RGB colors in -Ghostty. - -Flags: - - * `--plain`: will disable formatting and make the output more - friendly for Unix tooling. This is default when not printing to a tty. \ No newline at end of file diff --git a/crates/ghostty-cli/src/help/list-fonts.txt b/crates/ghostty-cli/src/help/list-fonts.txt deleted file mode 100644 index 7c9da45..0000000 --- a/crates/ghostty-cli/src/help/list-fonts.txt +++ /dev/null @@ -1,24 +0,0 @@ -The `list-fonts` command is used to list all the available fonts for -Ghostty. This uses the exact same font discovery mechanism Ghostty uses to -find fonts to use. - -When executed with no arguments, this will list all available fonts, sorted -by family name, then font name. If a family name is given with `--family`, -the sorting will be disabled and the results instead will be shown in the -same priority order Ghostty would use to pick a font. - -Flags: - - * `--bold`: Filter results to specific bold styles. It is not guaranteed - that only those styles are returned. They are only prioritized. - - * `--italic`: Filter results to specific italic styles. It is not guaranteed - that only those styles are returned. They are only prioritized. - - * `--style`: Filter results based on the style string advertised by a font. - It is not guaranteed that only those styles are returned. They are only - prioritized. - - * `--family`: Filter results to a specific font family. The family handling - is identical to the `font-family` set of Ghostty configuration values, so - this can be used to debug why your desired font may not be loading. \ No newline at end of file diff --git a/crates/ghostty-cli/src/help/list-keybinds.txt b/crates/ghostty-cli/src/help/list-keybinds.txt deleted file mode 100644 index 6b7f4e4..0000000 --- a/crates/ghostty-cli/src/help/list-keybinds.txt +++ /dev/null @@ -1,17 +0,0 @@ -The `list-keybinds` command is used to list all the available keybinds for -Ghostty. - -When executed without any arguments this will list the current keybinds -loaded by the config file. If no config file is found or there aren't any -changes to the keybinds it will print out the default ones configured for -Ghostty - -Flags: - - * `--default`: will print out all the default keybinds - - * `--docs`: currently does nothing, intended to print out documentation - about the action associated with the keybinds - - * `--plain`: will disable formatting and make the output more - friendly for Unix tooling. This is default when not printing to a tty. \ No newline at end of file diff --git a/crates/ghostty-cli/src/help/list-themes.txt b/crates/ghostty-cli/src/help/list-themes.txt deleted file mode 100644 index 5dfea0e..0000000 --- a/crates/ghostty-cli/src/help/list-themes.txt +++ /dev/null @@ -1,37 +0,0 @@ -The `list-themes` command is used to preview or list all the available -themes for Ghostty. - -If this command is run from a TTY, a TUI preview of the themes will be -shown. While in the preview, `F1` will bring up a help screen and `ESC` will -exit the preview. Other keys that can be used to navigate the preview are -listed in the help screen. - -If this command is not run from a TTY, or the output is piped to another -command, a plain list of theme names will be printed to the screen. A plain -list can be forced using the `--plain` CLI flag. - -Two different directories will be searched for themes. - -The first directory is the `themes` subdirectory of your Ghostty -configuration directory. This is `$XDG_CONFIG_HOME/ghostty/themes` or -`~/.config/ghostty/themes`. - -The second directory is the `themes` subdirectory of the Ghostty resources -directory. Ghostty ships with a multitude of themes that will be installed -into this directory. On macOS, this directory is the -`Ghostty.app/Contents/Resources/ghostty/themes`. On Linux, this directory -is the `share/ghostty/themes` (wherever you installed the Ghostty "share" -directory). If you're running Ghostty from the source, this is the -`zig-out/share/ghostty/themes` directory. - -You can also set the `GHOSTTY_RESOURCES_DIR` environment variable to point -to the resources directory. - -Flags: - - * `--path`: Show the full path to the theme. - - * `--plain`: Force a plain listing of themes. - - * `--color`: Specify the color scheme of the themes included in the list. - This can be `dark`, `light`, or `all`. The default is `all`. \ No newline at end of file diff --git a/crates/ghostty-cli/src/help/new-window.txt b/crates/ghostty-cli/src/help/new-window.txt deleted file mode 100644 index 853c1c5..0000000 --- a/crates/ghostty-cli/src/help/new-window.txt +++ /dev/null @@ -1,62 +0,0 @@ -The `new-window` will use native platform IPC to open up a new window in a -running instance of Ghostty. - -If the `--class` flag is not set, the `new-window` command will try and -connect to a running instance of Ghostty based on what optimizations the -Ghostty CLI was compiled with. Otherwise the `new-window` command will try -and contact a running Ghostty instance that was configured with the same -`class` as was given on the command line. - -All of the arguments after the `+new-window` argument (except for the -`--class` flag) will be sent to the remote Ghostty instance and will be -parsed as command line flags. These flags will override certain settings -when creating the first surface in the new window. Currently, only -`--working-directory`, `--command`, and `--title` are supported. `-e` will -also work as an alias for `--command`, except that if `-e` is found on the -command line all following arguments will become part of the command and no -more arguments will be parsed for configuration settings. - -If `--working-directory` is found on the command line and is a relative -path (i.e. doesn't start with `/`) it will be resolved to an absolute path -relative to the current working directory that the `ghostty +new-window` -command is run from. `~/` prefixes will also be expanded to the user's home -directory. - -If `--working-directory` is _not_ found on the command line, the working -directory that `ghostty +new-window` is run from will be passed to Ghostty. - -GTK uses an application ID to identify instances of applications. If Ghostty -is compiled with release optimizations, the default application ID will be -`com.mitchellh.ghostty`. If Ghostty is compiled with debug optimizations, -the default application ID will be `com.mitchellh.ghostty-debug`. The -`class` configuration entry can be used to set up a custom application -ID. The class name must follow the requirements defined [in the GTK -documentation](https://docs.gtk.org/gio/type_func.Application.id_is_valid.html) -or it will be ignored and Ghostty will use the default as defined above. - -On GTK, D-Bus activation must be properly configured. Ghostty does not need -to be running for this to open a new window, making it suitable for binding -to keys in your window manager (if other methods for configuring global -shortcuts are unavailable). D-Bus will handle launching a new instance -of Ghostty if it is not already running. See the Ghostty website for -information on properly configuring D-Bus activation. - -Only supported on GTK. - -Flags: - - * `--class=`: If set, open up a new window in a custom instance of - Ghostty. The class must be a valid GTK application ID. - - * `--command`: The command to be executed in the first surface of the new window. - - * `--working-directory=`: The working directory to pass to Ghostty. - - * `--title`: A title that will override the title of the first surface in - the new window. The title override may be edited or removed later. - - * `-e`: Any arguments after this will be interpreted as a command to - execute inside the first surface of the new window instead of the - default command. - -Available since: 1.2.0 \ No newline at end of file diff --git a/crates/ghostty-cli/src/help/show-config.txt b/crates/ghostty-cli/src/help/show-config.txt deleted file mode 100644 index 50b7fbf..0000000 --- a/crates/ghostty-cli/src/help/show-config.txt +++ /dev/null @@ -1,27 +0,0 @@ -The `show-config` command shows the current configuration in a valid Ghostty -configuration file format. - -When executed without any arguments this will output the current -configuration that is different from the default configuration. If you're -using the default configuration this will output nothing. - -If you are a new user and want to see all available options with -documentation, run `ghostty +show-config --default --docs`. - -The output is not in any specific order, but the order should be consistent -between runs. The output is not guaranteed to be exactly match the input -configuration files, but it will result in the same behavior. Comments, -whitespace, and other formatting is not preserved from user configuration -files. - -Flags: - - * `--default`: Show the default configuration instead of loading - the user configuration. - - * `--changes-only`: Only show the options that have been changed - from the default. This has no effect if `--default` is specified. - - * `--docs`: Print the documentation above each option as a comment, - This is very noisy but is very useful to learn about available - options, especially paired with `--default`. \ No newline at end of file diff --git a/crates/ghostty-cli/src/help/show-face.txt b/crates/ghostty-cli/src/help/show-face.txt deleted file mode 100644 index 52ab89a..0000000 --- a/crates/ghostty-cli/src/help/show-face.txt +++ /dev/null @@ -1,21 +0,0 @@ -The `show-face` command shows what font face Ghostty will use to render a -specific codepoint. Note that this command does not take into consideration -grapheme clustering or any other Unicode features that might modify the -presentation of a codepoint, so this may show a different font face than -Ghostty uses to render a codepoint in a terminal session. - -Flags: - - * `--cp`: Find the face for a single codepoint. The codepoint may be specified - in decimal (`--cp=65`), hexadecimal (`--cp=0x41`), octal (`--cp=0o101`), or - binary (`--cp=0b1000001`). - - * `--string`: Find the face for all of the codepoints in a string. The - string must be a valid UTF-8 sequence. - - * `--style`: Search for a specific style. Valid options are `regular`, `bold`, - `italic`, and `bold_italic`. - - * `--presentation`: If set, force searching for a specific presentation - style. Valid options are `text` and `emoji`. If unset, the presentation - style of a codepoint will be inferred from the Unicode standard. \ No newline at end of file diff --git a/crates/ghostty-cli/src/help/ssh-cache.txt b/crates/ghostty-cli/src/help/ssh-cache.txt deleted file mode 100644 index 0e5b44b..0000000 --- a/crates/ghostty-cli/src/help/ssh-cache.txt +++ /dev/null @@ -1,23 +0,0 @@ -Manage the SSH terminfo cache for automatic remote host setup. - -When SSH integration is enabled with `shell-integration-features = ssh-terminfo`, -Ghostty automatically installs its terminfo on remote hosts. This command -manages the cache of successful installations to avoid redundant uploads. - -The cache stores hostnames (or user@hostname combinations) along with timestamps. -Entries older than the expiration period are automatically removed during cache -operations. By default, entries never expire. - -Only one of `--clear`, `--add`, `--remove`, or `--host` can be specified. -If multiple are specified, one of the actions will be executed but -it isn't guaranteed which one. This is entirely unsafe so you should split -multiple actions into separate commands. - -Examples: - ghostty +ssh-cache # List all cached hosts - ghostty +ssh-cache --host=example.com # Check if host is cached - ghostty +ssh-cache --add=example.com # Manually add host to cache - ghostty +ssh-cache --add=user@example.com # Add user@host combination - ghostty +ssh-cache --remove=example.com # Remove host from cache - ghostty +ssh-cache --clear # Clear entire cache - ghostty +ssh-cache --expire-days=30 # Set custom expiration period \ No newline at end of file diff --git a/crates/ghostty-cli/src/help/validate-config.txt b/crates/ghostty-cli/src/help/validate-config.txt deleted file mode 100644 index 0340fa5..0000000 --- a/crates/ghostty-cli/src/help/validate-config.txt +++ /dev/null @@ -1,9 +0,0 @@ -The `validate-config` command is used to validate a Ghostty config file. - -When executed without any arguments, this will load the config from the default -location. - -Flags: - - * `--config-file`: can be passed to validate a specific target config file in - a non-default location \ No newline at end of file diff --git a/crates/ghostty-cli/src/help/version.txt b/crates/ghostty-cli/src/help/version.txt deleted file mode 100644 index 015832a..0000000 --- a/crates/ghostty-cli/src/help/version.txt +++ /dev/null @@ -1,2 +0,0 @@ -The `version` command is used to display information about Ghostty. Recognized as -either `+version` or `--version`. \ No newline at end of file diff --git a/crates/ghostty-cli/src/ssh_cache/disk_cache.rs b/crates/ghostty-cli/src/ssh_cache/disk_cache.rs index a18d778..d97041e 100644 --- a/crates/ghostty-cli/src/ssh_cache/disk_cache.rs +++ b/crates/ghostty-cli/src/ssh_cache/disk_cache.rs @@ -2,12 +2,13 @@ //! (ZIG/src/cli/ssh-cache/DiskCache.zig). The cache only stores metadata //! (hostname, terminfo value, etc.) and does not store any sensitive data. +#[cfg(unix)] +use std::os::unix::fs::{OpenOptionsExt, PermissionsExt}; use std::{ collections::BTreeMap, fs::{File, OpenOptions}, io::{Read, Write}, net::Ipv6Addr, - os::unix::fs::{OpenOptionsExt, PermissionsExt}, path::PathBuf, }; @@ -87,13 +88,11 @@ impl DiskCache { } // Open or create cache file with secure permissions - let file = OpenOptions::new() - .read(true) - .write(true) - .create(true) - .truncate(false) - .mode(0o600) - .open(&self.path)?; + let mut open = OpenOptions::new(); + open.read(true).write(true).create(true).truncate(false); + #[cfg(unix)] + open.mode(0o600); + let file = open.open(&self.path)?; fixup_permissions(&file)?; // Lock @@ -196,12 +195,11 @@ impl DiskCache { let tmp_path = cache_dir.join(tmp_name); let result = (|| -> std::io::Result<()> { - let mut f = OpenOptions::new() - .write(true) - .create(true) - .truncate(true) - .mode(0o600) - .open(&tmp_path)?; + let mut open = OpenOptions::new(); + open.write(true).create(true).truncate(true); + #[cfg(unix)] + open.mode(0o600); + let mut f = open.open(&tmp_path)?; f.write_all(data.as_bytes())?; f.sync_all()?; std::fs::rename(&tmp_path, &self.path) @@ -227,6 +225,18 @@ impl Drop for Lock<'_> { /// Zig: `_ = file.tryLock(.exclusive) catch return error.CacheIsLocked;` /// The boolean result is ignored upstream, so a lock held by someone else /// does not fail; only a real error does. +/// +/// Zig skips locking entirely on Windows +/// (`if (comptime builtin.os.tag != .windows)`), so we do too. On unix +/// `File::try_lock` is `flock(LOCK_EX | LOCK_NB)`, the same call as Zig's +/// `File.tryLock(.exclusive)`. +#[cfg(windows)] +#[allow(clippy::unnecessary_wraps)] +fn lock(_file: &File) -> Result, Error> { + Ok(Lock(None)) +} + +#[cfg(not(windows))] fn lock(file: &File) -> Result, Error> { match file.try_lock() { Ok(()) => Ok(Lock(Some(file))), @@ -235,7 +245,20 @@ fn lock(file: &File) -> Result, Error> { } } +#[cfg_attr(windows, allow(clippy::unnecessary_wraps))] fn fixup_permissions(file: &File) -> Result<(), Error> { + // Windows does not support chmod + #[cfg(windows)] + { + let _ = file; + Ok(()) + } + #[cfg(unix)] + fixup_permissions_unix(file) +} + +#[cfg(unix)] +fn fixup_permissions_unix(file: &File) -> Result<(), Error> { // Ensure file has correct permissions (readable/writable by // owner only) let meta = file.metadata()?; @@ -323,6 +346,34 @@ fn is_valid_user(user: &str) -> bool { #[cfg(test)] mod tests { #![allow(clippy::unwrap_used)] + #[cfg(not(windows))] + #[test] + fn lock_is_exclusive_until_dropped() { + let dir = tempfile::tempdir().unwrap(); + let path = dir.path().join("lock"); + let a = File::create(&path).unwrap(); + let b = File::options().read(true).write(true).open(&path).unwrap(); + + let held = super::lock(&a).unwrap(); + assert!(held.0.is_some()); + // A second handle can't take the lock; Zig ignores that result. + assert!(matches!( + b.try_lock(), + Err(std::fs::TryLockError::WouldBlock) + )); + assert!(super::lock(&b).unwrap().0.is_none()); + drop(held); + // Unlocked on drop. + assert!(b.try_lock().is_ok()); + } + + #[cfg(windows)] + #[test] + fn lock_is_noop_on_windows() { + let dir = tempfile::tempdir().unwrap(); + let a = File::create(dir.path().join("lock")).unwrap(); + assert!(super::lock(&a).unwrap().0.is_none()); + } use super::*; #[test] @@ -389,8 +440,9 @@ mod tests { assert_eq!(vec![std::ffi::OsString::from("cache")], names); } + #[cfg(unix)] #[test] - fn disk_cache_file_permissions_and_invalid_host() { + fn disk_cache_file_permissions() { let tmp = tempfile::tempdir().unwrap(); let cache = DiskCache { path: tmp.path().join("sub/cache"), @@ -398,6 +450,15 @@ mod tests { cache.add("example.com").unwrap(); let mode = std::fs::metadata(&cache.path).unwrap().permissions().mode(); assert_eq!(0o600, mode & 0o777); + } + + #[test] + fn disk_cache_invalid_host() { + let tmp = tempfile::tempdir().unwrap(); + let cache = DiskCache { + path: tmp.path().join("sub/cache"), + }; + cache.add("example.com").unwrap(); assert!(matches!( cache.add("bad host"), Err(Error::HostnameIsInvalid) diff --git a/crates/ghostty-cli/src/tui.rs b/crates/ghostty-cli/src/tui.rs index 6db03ed..4df7615 100644 --- a/crates/ghostty-cli/src/tui.rs +++ b/crates/ghostty-cli/src/tui.rs @@ -264,9 +264,20 @@ fn write_style_change(w: &mut dyn Write, cursor: &Style, next: &Style) -> std::i Ok(()) } +/// Fixed `(cols, rows)` used on Windows instead of querying the tty. +#[cfg(windows)] +const WINDOWS_WINSIZE: (u16, u16) = (120, 24); + /// The terminal size in columns and rows (`vaxis.Tty.getWinsize`). #[must_use] pub fn window_size() -> (u16, u16) { + // On Windows we use some default; it doesn't really matter for what we're + // doing because we don't do any wrapping (rows = 24, cols = 120). + #[cfg(windows)] + { + WINDOWS_WINSIZE + } + #[cfg(not(windows))] ratatui::crossterm::terminal::size().unwrap_or((80, 24)) } diff --git a/crates/ghostty-helpgen/Cargo.toml b/crates/ghostty-helpgen/Cargo.toml new file mode 100644 index 0000000..5d529a3 --- /dev/null +++ b/crates/ghostty-helpgen/Cargo.toml @@ -0,0 +1,9 @@ +[package] +name = "ghostty-helpgen" +version.workspace = true +edition.workspace = true +license.workspace = true +description = "Build-time help text generator (port of ghostty src/helpgen.zig)" + +[lints] +workspace = true diff --git a/crates/ghostty-helpgen/src/lib.rs b/crates/ghostty-helpgen/src/lib.rs new file mode 100644 index 0000000..53df815 --- /dev/null +++ b/crates/ghostty-helpgen/src/lib.rs @@ -0,0 +1,218 @@ +//! Build-time help text generation (ZIG/src/helpgen.zig). +//! +//! Zig's `helpgen.zig` parses the sources of the CLI actions and of +//! `input/Binding.zig` and turns the `///` doc comments into help strings +//! that are compiled into the binary. This crate is the Rust counterpart: +//! it is used as a build-dependency by the `build.rs` of `ghostty-cli` +//! (action help, from the `///` docs of each `pub fn run`) and of +//! `ghostty-input` (keybind action docs, from the `///` docs of the +//! variants of `enum Action`), which write Rust source tables into +//! `OUT_DIR` that are pulled in with `include!`. +//! +//! The scanner is deliberately line based (like Zig's token based one it +//! only looks at doc comment runs and the item that follows them), so it +//! needs no parser dependency. + +use std::fmt::Write as _; + +/// Returns the text of a doc comment line without the `///` marker, or +/// `None` if the line is not a doc comment (`////` is a plain comment in +/// Rust but is accepted by Zig; we follow Rust here). +fn strip_marker(line: &str) -> Option<&str> { + let rest = line.trim_start().strip_prefix("///")?; + if rest.starts_with('/') { + return None; + } + Some(rest) +} + +/// Port of `extractDocComments` + `findCommonPrefix`: removes the `///` +/// markers, then removes the common number of leading spaces of all +/// non-blank lines. Returns the lines (without line terminators). +/// +/// `comment_lines` are the raw source lines of one contiguous doc comment +/// run. Lines that are not doc comments are ignored. +#[must_use] +pub fn doc_lines(comment_lines: &[&str]) -> Vec { + let lines: Vec<&str> = comment_lines + .iter() + .filter_map(|l| strip_marker(l)) + .collect(); + // Blank lines don't participate in the common prefix. + let prefix = lines + .iter() + .filter_map(|l| l.find(|c| c != ' ')) + .min() + .unwrap_or(usize::MAX); + lines + .iter() + .map(|l| l[prefix.min(l.len())..].to_string()) + .collect() +} + +/// The doc comment lines directly above line `idx` (attributes such as +/// `#[cfg(..)]` between the comment and the item are skipped), in source +/// order. Empty if there is no doc comment. +fn doc_run_above<'a>(lines: &[&'a str], idx: usize) -> Vec<&'a str> { + let mut i = idx; + while i > 0 && lines[i - 1].trim_start().starts_with("#[") { + i -= 1; + } + let end = i; + while i > 0 && strip_marker(lines[i - 1]).is_some() { + i -= 1; + } + lines[i..end].to_vec() +} + +/// The doc comment of the `run` function of a CLI action source file +/// (Zig: the doc comment preceding `fn run`), joined with `\n` and with +/// no trailing newline. `None` if there is no `run` function or it has no +/// doc comment. +#[must_use] +pub fn run_fn_doc(source: &str) -> Option { + let lines: Vec<&str> = source.lines().collect(); + let idx = lines.iter().position(|l| { + let l = l.trim_start(); + l.strip_prefix("pub fn run") + .or_else(|| l.strip_prefix("fn run")) + .is_some_and(|r| r.starts_with(['(', '<'])) + })?; + let run = doc_run_above(&lines, idx); + if run.is_empty() { + return None; + } + Some(doc_lines(&run).join("\n")) +} + +/// The doc comments of the variants of `enum ` in `source`, as +/// `(variant, doc)` in declaration order. `doc` is the doc lines joined +/// with `\n` plus one trailing newline (Zig's multiline literal plus the +/// newline the generated table always carried). Variants without a doc +/// comment are returned with `None`. +#[must_use] +pub fn enum_variant_docs(source: &str, name: &str) -> Vec<(String, Option)> { + let lines: Vec<&str> = source.lines().collect(); + let header = format!("enum {name} {{"); + let Some(start) = lines + .iter() + .position(|l| (l.starts_with("pub ") && l.contains(&header)) || *l == header) + else { + return Vec::new(); + }; + let mut out = Vec::new(); + for (i, line) in lines.iter().enumerate().skip(start + 1) { + if *line == "}" { + break; + } + // Variants are exactly one indentation level deep. + let Some(rest) = line.strip_prefix(" ") else { + continue; + }; + if !rest.starts_with(|c: char| c.is_ascii_uppercase()) { + continue; + } + let ident: String = rest + .chars() + .take_while(|c| c.is_ascii_alphanumeric() || *c == '_') + .collect(); + let run = doc_run_above(&lines, i); + let doc = (!run.is_empty()).then(|| { + let mut d = doc_lines(&run).join("\n"); + d.push('\n'); + d + }); + out.push((ident, doc)); + } + out +} + +/// Generates the body of `ActionKey::docs`: a `match self` expression over +/// the given `(variant, doc)` pairs. +#[must_use] +pub fn keybind_docs_match(variants: &[(String, String)]) -> String { + let mut s = String::from("match self {\n"); + for (v, d) in variants { + let _ = writeln!(s, " ActionKey::{v} => {d:?},"); + } + s.push_str("}\n"); + s +} + +/// Generates the body of `Action::help_text`: a `match self` expression +/// over `(variant, help)` pairs. +#[must_use] +pub fn cli_help_match(actions: &[(String, String)]) -> String { + let mut s = String::from("match self {\n"); + for (v, d) in actions { + let _ = writeln!(s, " Action::{v} => {d:?},"); + } + s.push_str("}\n"); + s +} + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn strips_marker_and_common_prefix() { + let src = ["/// One", "/// two", " /// three"]; + assert_eq!(doc_lines(&src), ["One", " two", "three"]); + } + + #[test] + fn blank_lines_do_not_affect_prefix() { + let src = ["///", "/// a", "///", "/// b", "/// "]; + assert_eq!(doc_lines(&src), ["", "a", "", " b", ""]); + } + + #[test] + fn code_fences_and_indented_blocks_are_preserved() { + let src = ["/// ```", "/// indented", "/// ```"]; + assert_eq!(doc_lines(&src), ["```", " indented", "```"]); + } + + #[test] + fn four_slashes_is_not_a_doc_comment() { + assert_eq!(doc_lines(&["//// nope", "// plain"]), Vec::::new()); + } + + #[test] + fn run_doc_is_extracted() { + let src = + "use x;\n\n/// First.\n///\n/// Second.\n#[must_use]\npub fn run(a: &[String]) {}\n"; + assert_eq!(run_fn_doc(src).as_deref(), Some("First.\n\nSecond.")); + } + + #[test] + fn run_doc_ignores_other_comments_and_missing() { + assert_eq!(run_fn_doc("// x\npub fn run() {}"), None); + assert_eq!(run_fn_doc("/// a\npub fn runner() {}"), None); + assert_eq!(run_fn_doc("fn other() {}"), None); + } + + #[test] + fn enum_variants_are_extracted() { + let src = "pub enum Action {\n /// Ignore.\n ///\n /// More.\n Ignore,\n Undoc,\n /// Send.\n ///\n Csi(String),\n /// Multi\n Multi {\n a: u8,\n },\n}\nenum Other {\n /// no\n X,\n}\n"; + let v = enum_variant_docs(src, "Action"); + assert_eq!( + v, + [ + ("Ignore".into(), Some("Ignore.\n\nMore.\n".into())), + ("Undoc".into(), None), + ("Csi".into(), Some("Send.\n\n".into())), + ("Multi".into(), Some("Multi\n".into())), + ] + ); + } + + #[test] + fn generated_match_is_escaped() { + let m = keybind_docs_match(&[("A".into(), "x \"q\"\n".into())]); + assert_eq!( + m, + "match self {\n ActionKey::A => \"x \\\"q\\\"\\n\",\n}\n" + ); + } +} diff --git a/crates/ghostty-input/Cargo.toml b/crates/ghostty-input/Cargo.toml index 19d71dd..6f84b97 100644 --- a/crates/ghostty-input/Cargo.toml +++ b/crates/ghostty-input/Cargo.toml @@ -10,6 +10,9 @@ ghostty-unicode.workspace = true indexmap = "2.14.2" thiserror = "2.0.21" +[build-dependencies] +ghostty-helpgen.workspace = true + # Same as `[workspace.lints]` except `unsafe_code` is denied instead of # forbidden: the macOS Carbon keymap (`keymap_darwin.rs`) needs `unsafe` FFI. [lints.rust] diff --git a/crates/ghostty-input/build.rs b/crates/ghostty-input/build.rs new file mode 100644 index 0000000..4787f5b --- /dev/null +++ b/crates/ghostty-input/build.rs @@ -0,0 +1,29 @@ +//! Generates the keybind action docs table (Zig: `helpgen.zig` +//! `genKeybindActions`) from the `///` docs of `enum Action` in +//! `src/binding/action.rs`. + +use std::{env, fs, path::PathBuf}; + +fn main() { + let src_path = "src/binding/action.rs"; + println!("cargo:rerun-if-changed={src_path}"); + println!("cargo:rerun-if-changed=build.rs"); + let src = fs::read_to_string(src_path).expect("read action.rs"); + + let variants: Vec<(String, String)> = ghostty_helpgen::enum_variant_docs(&src, "Action") + .into_iter() + .map(|(name, doc)| { + let doc = doc + .unwrap_or_else(|| panic!("keybind action `{name}` must have a `///` doc comment")); + (name, doc) + }) + .collect(); + assert!(!variants.is_empty(), "no variants found in `enum Action`"); + + let out = PathBuf::from(env::var_os("OUT_DIR").expect("OUT_DIR")); + fs::write( + out.join("keybind_action_docs.rs"), + ghostty_helpgen::keybind_docs_match(&variants), + ) + .expect("write keybind_action_docs.rs"); +} diff --git a/crates/ghostty-input/src/binding/action.rs b/crates/ghostty-input/src/binding/action.rs index 8b357be..02d0c8d 100644 --- a/crates/ghostty-input/src/binding/action.rs +++ b/crates/ghostty-input/src/binding/action.rs @@ -406,553 +406,553 @@ impl Action { /// is available through [`Action::docs`] / [`ActionKey::docs`]. #[derive(Debug, Clone, PartialEq)] pub enum Action { - // Ignore this key combination. - // - // Ghostty will not process this combination nor forward it to the child - // process within the terminal, but it may still be processed by the OS or - // other applications. + /// Ignore this key combination. + /// + /// Ghostty will not process this combination nor forward it to the child + /// process within the terminal, but it may still be processed by the OS or + /// other applications. Ignore, - // Unbind a previously bound key binding. - // - // This cannot unbind bindings that were not bound by Ghostty or the user - // (e.g. bindings set by the OS or some other application). + /// Unbind a previously bound key binding. + /// + /// This cannot unbind bindings that were not bound by Ghostty or the user + /// (e.g. bindings set by the OS or some other application). Unbind, - // Send a CSI sequence. - // - // The value should be the CSI sequence without the CSI header (`ESC [` or - // `\x1b[`). - // - // For example, `csi:0m` can be sent to reset all styles of the current text. + /// Send a CSI sequence. + /// + /// The value should be the CSI sequence without the CSI header (`ESC [` or + /// `\x1b[`). + /// + /// For example, `csi:0m` can be sent to reset all styles of the current text. Csi(String), - // Send an `ESC` sequence. + /// Send an `ESC` sequence. Esc(String), - // Send the specified text. - // - // Uses Zig string literal syntax. This is currently not validated. - // If the text is invalid (i.e. contains an invalid escape sequence), - // the error will currently only show up in logs. + /// Send the specified text. + /// + /// Uses Zig string literal syntax. This is currently not validated. + /// If the text is invalid (i.e. contains an invalid escape sequence), + /// the error will currently only show up in logs. Text(String), - // Send data to the pty depending on whether cursor key mode is enabled - // (`application`) or disabled (`normal`). + /// Send data to the pty depending on whether cursor key mode is enabled + /// (`application`) or disabled (`normal`). CursorKey(CursorKey), - // Reset the terminal. - // - // This can fix a lot of issues when a running program puts the terminal - // into a broken state, equivalent to running the `reset` command. - // - // If you do this while in a TUI program such as vim, this may break - // the program. If you do this while in a shell, you may have to press - // enter after to get a new prompt. + /// Reset the terminal. + /// + /// This can fix a lot of issues when a running program puts the terminal + /// into a broken state, equivalent to running the `reset` command. + /// + /// If you do this while in a TUI program such as vim, this may break + /// the program. If you do this while in a shell, you may have to press + /// enter after to get a new prompt. Reset, - // Copy the selected text to the clipboard. + /// Copy the selected text to the clipboard. CopyToClipboard(CopyToClipboard), - // Paste the contents of the default clipboard. + /// Paste the contents of the default clipboard. PasteFromClipboard, - // Paste the contents of the selection clipboard. + /// Paste the contents of the selection clipboard. PasteFromSelection, - // If there is a URL under the cursor, copy it to the default clipboard. + /// If there is a URL under the cursor, copy it to the default clipboard. CopyUrlToClipboard, - // Copy the terminal title to the clipboard. If the terminal title is not - // set or is empty this has no effect. + /// Copy the terminal title to the clipboard. If the terminal title is not + /// set or is empty this has no effect. CopyTitleToClipboard, - // Increase the font size by the specified amount in points (pt). - // - // For example, `increase_font_size:1.5` will increase the font size - // by 1.5 points. + /// Increase the font size by the specified amount in points (pt). + /// + /// For example, `increase_font_size:1.5` will increase the font size + /// by 1.5 points. IncreaseFontSize(f32), - // Decrease the font size by the specified amount in points (pt). - // - // For example, `decrease_font_size:1.5` will decrease the font size - // by 1.5 points. + /// Decrease the font size by the specified amount in points (pt). + /// + /// For example, `decrease_font_size:1.5` will decrease the font size + /// by 1.5 points. DecreaseFontSize(f32), - // Reset the font size to the original configured size. + /// Reset the font size to the original configured size. ResetFontSize, - // Set the font size to the specified size in points (pt). - // - // For example, `set_font_size:14.5` will set the font size - // to 14.5 points. + /// Set the font size to the specified size in points (pt). + /// + /// For example, `set_font_size:14.5` will set the font size + /// to 14.5 points. SetFontSize(f32), - // Start a search for the given text. If the text is empty, then - // the search is canceled. A canceled search will not disable any GUI - // elements showing search. For that, the explicit end_search binding - // should be used. - // - // If a previous search is active, it is replaced. + /// Start a search for the given text. If the text is empty, then + /// the search is canceled. A canceled search will not disable any GUI + /// elements showing search. For that, the explicit end_search binding + /// should be used. + /// + /// If a previous search is active, it is replaced. Search(String), - // Start a search for the current text selection. If there is no - // selection, this does nothing. If a search is already active, this - // changes the search terms. + /// Start a search for the current text selection. If there is no + /// selection, this does nothing. If a search is already active, this + /// changes the search terms. SearchSelection, - // Navigate the search results. If there is no active search, this - // is not performed. + /// Navigate the search results. If there is no active search, this + /// is not performed. NavigateSearch(NavigateSearch), - // Start a search if it isn't started already. This doesn't set any - // search terms, but opens the UI for searching. + /// Start a search if it isn't started already. This doesn't set any + /// search terms, but opens the UI for searching. StartSearch, - // End the current search if any and hide any GUI elements. + /// End the current search if any and hide any GUI elements. EndSearch, - // Clear the screen and all scrollback. + /// Clear the screen and all scrollback. ClearScreen, - // Select all text on the screen. + /// Select all text on the screen. SelectAll, - // Scroll to the top of the screen. + /// Scroll to the top of the screen. ScrollToTop, - // Scroll to the bottom of the screen. + /// Scroll to the bottom of the screen. ScrollToBottom, - // Scroll to the selected text. + /// Scroll to the selected text. ScrollToSelection, - // Scroll to the given absolute row in the screen with 0 being - // the first row. + /// Scroll to the given absolute row in the screen with 0 being + /// the first row. ScrollToRow(usize), - // Scroll the screen up by one page. + /// Scroll the screen up by one page. ScrollPageUp, - // Scroll the screen down by one page. + /// Scroll the screen down by one page. ScrollPageDown, - // Scroll the screen by the specified fraction of a page. - // - // Positive values scroll downwards, and negative values scroll upwards. - // - // For example, `scroll_page_fractional:0.5` would scroll the screen - // downwards by half a page, while `scroll_page_fractional:-1.5` would - // scroll it upwards by one and a half pages. + /// Scroll the screen by the specified fraction of a page. + /// + /// Positive values scroll downwards, and negative values scroll upwards. + /// + /// For example, `scroll_page_fractional:0.5` would scroll the screen + /// downwards by half a page, while `scroll_page_fractional:-1.5` would + /// scroll it upwards by one and a half pages. ScrollPageFractional(f32), - // Scroll the screen by the specified amount of lines. - // - // Positive values scroll downwards, and negative values scroll upwards. - // - // For example, `scroll_page_lines:3` would scroll the screen downwards - // by 3 lines, while `scroll_page_lines:-10` would scroll it upwards by 10 - // lines. + /// Scroll the screen by the specified amount of lines. + /// + /// Positive values scroll downwards, and negative values scroll upwards. + /// + /// For example, `scroll_page_lines:3` would scroll the screen downwards + /// by 3 lines, while `scroll_page_lines:-10` would scroll it upwards by 10 + /// lines. ScrollPageLines(i16), - // Adjust the current selection in the given direction or position, - // relative to the cursor. - // - // WARNING: This does not create a new selection, and does nothing when - // there currently isn't one. - // - // Valid arguments are: - // - // - `left`, `right` - // - // Adjust the selection one cell to the left or right respectively. - // - // - `up`, `down` - // - // Adjust the selection one line upwards or downwards respectively. - // - // - `page_up`, `page_down` - // - // Adjust the selection one page upwards or downwards respectively. - // - // - `home`, `end` - // - // Adjust the selection to the top-left or the bottom-right corner - // of the screen respectively. - // - // - `beginning_of_line`, `end_of_line` - // - // Adjust the selection to the beginning or the end of the line - // respectively. - // + /// Adjust the current selection in the given direction or position, + /// relative to the cursor. + /// + /// WARNING: This does not create a new selection, and does nothing when + /// there currently isn't one. + /// + /// Valid arguments are: + /// + /// - `left`, `right` + /// + /// Adjust the selection one cell to the left or right respectively. + /// + /// - `up`, `down` + /// + /// Adjust the selection one line upwards or downwards respectively. + /// + /// - `page_up`, `page_down` + /// + /// Adjust the selection one page upwards or downwards respectively. + /// + /// - `home`, `end` + /// + /// Adjust the selection to the top-left or the bottom-right corner + /// of the screen respectively. + /// + /// - `beginning_of_line`, `end_of_line` + /// + /// Adjust the selection to the beginning or the end of the line + /// respectively. + /// AdjustSelection(AdjustSelection), - // Jump the viewport forward or back by the given number of prompts. - // - // Requires shell integration. - // - // Positive values scroll downwards, and negative values scroll upwards. + /// Jump the viewport forward or back by the given number of prompts. + /// + /// Requires shell integration. + /// + /// Positive values scroll downwards, and negative values scroll upwards. JumpToPrompt(i16), - // Write the entire scrollback into a temporary file with the specified - // action. The action determines what to do with the filepath. - // - // Valid actions are: - // - // - `copy` - // - // Copy the file path into the clipboard. - // - // - `paste` - // - // Paste the file path into the terminal. - // - // - `open` - // - // Open the file in the default OS editor for text files. - // - // The default OS editor is determined by using `open` on macOS - // and `xdg-open` on Linux. - // + /// Write the entire scrollback into a temporary file with the specified + /// action. The action determines what to do with the filepath. + /// + /// Valid actions are: + /// + /// - `copy` + /// + /// Copy the file path into the clipboard. + /// + /// - `paste` + /// + /// Paste the file path into the terminal. + /// + /// - `open` + /// + /// Open the file in the default OS editor for text files. + /// + /// The default OS editor is determined by using `open` on macOS + /// and `xdg-open` on Linux. + /// WriteScrollbackFile(WriteScreen), - // Write the contents of the screen into a temporary file with the - // specified action. - // - // See `write_scrollback_file` for possible actions. + /// Write the contents of the screen into a temporary file with the + /// specified action. + /// + /// See `write_scrollback_file` for possible actions. WriteScreenFile(WriteScreen), - // Write the currently selected text into a temporary file with the - // specified action. - // - // See `write_scrollback_file` for possible actions. - // - // Does nothing when no text is selected. + /// Write the currently selected text into a temporary file with the + /// specified action. + /// + /// See `write_scrollback_file` for possible actions. + /// + /// Does nothing when no text is selected. WriteSelectionFile(WriteScreen), - // Open a new window. - // - // If the application isn't currently focused, - // this will bring it to the front. + /// Open a new window. + /// + /// If the application isn't currently focused, + /// this will bring it to the front. NewWindow, - // Open a new tab. + /// Open a new tab. NewTab, - // Go to the previous tab. + /// Go to the previous tab. PreviousTab, - // Go to the next tab. + /// Go to the next tab. NextTab, - // Go to the last tab. + /// Go to the last tab. LastTab, - // Go to the tab with the specific index, starting from 1. - // - // If the tab number is higher than the number of tabs, - // this will go to the last tab. + /// Go to the tab with the specific index, starting from 1. + /// + /// If the tab number is higher than the number of tabs, + /// this will go to the last tab. GotoTab(usize), - // Moves a tab by a relative offset. - // - // Positive values move the tab forwards, and negative values move it - // backwards. If the new position is out of bounds, it is wrapped around - // cyclically within the tab list. - // - // For example, `move_tab:1` moves the tab one position forwards, and if - // it was already the last tab in the list, it wraps around and becomes - // the first tab in the list. Likewise, `move_tab:-1` moves the tab one - // position backwards, and if it was the first tab, then it will become - // the last tab. + /// Moves a tab by a relative offset. + /// + /// Positive values move the tab forwards, and negative values move it + /// backwards. If the new position is out of bounds, it is wrapped around + /// cyclically within the tab list. + /// + /// For example, `move_tab:1` moves the tab one position forwards, and if + /// it was already the last tab in the list, it wraps around and becomes + /// the first tab in the list. Likewise, `move_tab:-1` moves the tab one + /// position backwards, and if it was the first tab, then it will become + /// the last tab. MoveTab(isize), - // Toggle the tab overview. - // - // This is only supported on Linux and when the system's libadwaita - // version is 1.4 or newer. The current libadwaita version can be - // found by running `ghostty +version`. + /// Toggle the tab overview. + /// + /// This is only supported on Linux and when the system's libadwaita + /// version is 1.4 or newer. The current libadwaita version can be + /// found by running `ghostty +version`. ToggleTabOverview, - // Change the title of the current focused surface via a pop-up prompt. + /// Change the title of the current focused surface via a pop-up prompt. PromptSurfaceTitle, - // Change the title of the current tab via a pop-up prompt. The - // title set via this prompt overrides any title set by the terminal - // and persists across focus changes within the tab. + /// Change the title of the current tab via a pop-up prompt. The + /// title set via this prompt overrides any title set by the terminal + /// and persists across focus changes within the tab. PromptTabTitle, - // Set the title for the current focused surface. - // - // If the title is empty, the surface title is reset to an empty title. + /// Set the title for the current focused surface. + /// + /// If the title is empty, the surface title is reset to an empty title. SetSurfaceTitle(String), - // Set the title for the current focused tab. - // - // If the title is empty, the tab title override is cleared. + /// Set the title for the current focused tab. + /// + /// If the title is empty, the tab title override is cleared. SetTabTitle(String), - // Create a new split in the specified direction. - // - // Valid arguments: - // - // - `right`, `down`, `left`, `up` - // - // Creates a new split in the corresponding direction. - // - // - `auto` - // - // Creates a new split along the larger direction. - // For example, if the parent split is currently wider than it is tall, - // then a left-right split would be created, and vice versa. - // + /// Create a new split in the specified direction. + /// + /// Valid arguments: + /// + /// - `right`, `down`, `left`, `up` + /// + /// Creates a new split in the corresponding direction. + /// + /// - `auto` + /// + /// Creates a new split along the larger direction. + /// For example, if the parent split is currently wider than it is tall, + /// then a left-right split would be created, and vice versa. + /// NewSplit(SplitDirection), - // Focus on a split either in the specified direction (`right`, `down`, - // `left` and `up`), or in the adjacent split in the order of creation - // (`previous` and `next`). + /// Focus on a split either in the specified direction (`right`, `down`, + /// `left` and `up`), or in the adjacent split in the order of creation + /// (`previous` and `next`). GotoSplit(SplitFocusDirection), - // Focus on either the previous window or the next one ('previous', 'next') + /// Focus on either the previous window or the next one ('previous', 'next') GotoWindow(GotoWindow), - // Zoom in or out of the current split. - // - // When a split is zoomed into, it will take up the entire space in - // the current tab, hiding other splits. The tab or tab bar would also - // reflect this by displaying an icon indicating the zoomed state. + /// Zoom in or out of the current split. + /// + /// When a split is zoomed into, it will take up the entire space in + /// the current tab, hiding other splits. The tab or tab bar would also + /// reflect this by displaying an icon indicating the zoomed state. ToggleSplitZoom, - // Toggle read-only mode for the current surface. - // - // When a surface is in read-only mode: - // - No input is sent to the PTY (mouse events, key encoding) - // - Input can still be used at the terminal level to make selections, - // copy/paste (keybinds), scroll, etc. - // - Warn before quit is always enabled in this state even if an active - // process is not running + /// Toggle read-only mode for the current surface. + /// + /// When a surface is in read-only mode: + /// - No input is sent to the PTY (mouse events, key encoding) + /// - Input can still be used at the terminal level to make selections, + /// copy/paste (keybinds), scroll, etc. + /// - Warn before quit is always enabled in this state even if an active + /// process is not running ToggleReadonly, - // Resize the current split in the specified direction and amount in - // pixels. The two arguments should be joined with a comma (`,`), - // like in `resize_split:up,10`. + /// Resize the current split in the specified direction and amount in + /// pixels. The two arguments should be joined with a comma (`,`), + /// like in `resize_split:up,10`. ResizeSplit(SplitResizeParameter), - // Equalize the size of all splits in the current window. + /// Equalize the size of all splits in the current window. EqualizeSplits, - // Reset the window to the default size. The "default size" is the - // size that a new window would be created with. This has no effect - // if the window is fullscreen. - // - // Only implemented on macOS. + /// Reset the window to the default size. The "default size" is the + /// size that a new window would be created with. This has no effect + /// if the window is fullscreen. + /// + /// Only implemented on macOS. ResetWindowSize, - // Control the visibility of the terminal inspector. - // - // Valid arguments: `toggle`, `show`, `hide`. + /// Control the visibility of the terminal inspector. + /// + /// Valid arguments: `toggle`, `show`, `hide`. Inspector(InspectorMode), - // Show the GTK inspector. - // - // Has no effect on macOS. + /// Show the GTK inspector. + /// + /// Has no effect on macOS. ShowGtkInspector, - // Show the on-screen keyboard if one is present. - // - // Only implemented on Linux (GTK). On GNOME, the "Screen Keyboard" - // accessibility feature must be turned on, which can be found under - // Settings > Accessibility > Typing. Other platforms are as of now - // untested. + /// Show the on-screen keyboard if one is present. + /// + /// Only implemented on Linux (GTK). On GNOME, the "Screen Keyboard" + /// accessibility feature must be turned on, which can be found under + /// Settings > Accessibility > Typing. Other platforms are as of now + /// untested. ShowOnScreenKeyboard, - // Open the configuration file in the default OS editor. - // - // If your default OS editor isn't configured then this will fail. - // Currently, any failures to open the configuration will show up only in - // the logs. + /// Open the configuration file in the default OS editor. + /// + /// If your default OS editor isn't configured then this will fail. + /// Currently, any failures to open the configuration will show up only in + /// the logs. OpenConfig, - // Reload the configuration. - // - // The exact meaning depends on the app runtime in use, but this usually - // involves re-reading the configuration file and applying any changes - // Note that not all changes can be applied at runtime. + /// Reload the configuration. + /// + /// The exact meaning depends on the app runtime in use, but this usually + /// involves re-reading the configuration file and applying any changes + /// Note that not all changes can be applied at runtime. ReloadConfig, - // Close the current "surface", whether that is a window, tab, split, etc. - // - // This might trigger a close confirmation popup, depending on the value - // of the `confirm-close-surface` configuration setting. + /// Close the current "surface", whether that is a window, tab, split, etc. + /// + /// This might trigger a close confirmation popup, depending on the value + /// of the `confirm-close-surface` configuration setting. CloseSurface, - // Close the current tab and all splits therein, close all other tabs, or - // close every tab to the right of the current one depending on the mode. - // - // If the mode is not specified, defaults to closing the current tab. - // - // This might trigger a close confirmation popup, depending on the value - // of the `confirm-close-surface` configuration setting. + /// Close the current tab and all splits therein, close all other tabs, or + /// close every tab to the right of the current one depending on the mode. + /// + /// If the mode is not specified, defaults to closing the current tab. + /// + /// This might trigger a close confirmation popup, depending on the value + /// of the `confirm-close-surface` configuration setting. CloseTab(CloseTabMode), - // Close the current window and all tabs and splits therein. - // - // This might trigger a close confirmation popup, depending on the value - // of the `confirm-close-surface` configuration setting. + /// Close the current window and all tabs and splits therein. + /// + /// This might trigger a close confirmation popup, depending on the value + /// of the `confirm-close-surface` configuration setting. CloseWindow, - // Close all windows. - // - // WARNING: This action has been deprecated and has no effect on either - // Linux or macOS. Users are instead encouraged to use `all:close_window` - // instead. + /// Close all windows. + /// + /// WARNING: This action has been deprecated and has no effect on either + /// Linux or macOS. Users are instead encouraged to use `all:close_window` + /// instead. CloseAllWindows, - // Maximize or unmaximize the current window. - // - // This has no effect on macOS as it does not have the concept of - // maximized windows. + /// Maximize or unmaximize the current window. + /// + /// This has no effect on macOS as it does not have the concept of + /// maximized windows. ToggleMaximize, - // Fullscreen or unfullscreen the current window. + /// Fullscreen or unfullscreen the current window. ToggleFullscreen, - // Toggle window decorations (titlebar, buttons, etc.) for the current window. - // - // Only implemented on Linux. + /// Toggle window decorations (titlebar, buttons, etc.) for the current window. + /// + /// Only implemented on Linux. ToggleWindowDecorations, - // Toggle whether the terminal window should always float on top of other - // windows even when unfocused. - // - // Terminal windows always start as normal (not float-on-top) windows. - // - // Only implemented on macOS. + /// Toggle whether the terminal window should always float on top of other + /// windows even when unfocused. + /// + /// Terminal windows always start as normal (not float-on-top) windows. + /// + /// Only implemented on macOS. ToggleWindowFloatOnTop, - // Toggle secure input mode. - // - // This is used to prevent apps from monitoring your keyboard input - // when entering passwords or other sensitive information. - // - // This applies to the entire application, not just the focused terminal. - // You must manually untoggle it or quit Ghostty entirely to disable it. - // - // Only implemented on macOS, as this uses a built-in system API. + /// Toggle secure input mode. + /// + /// This is used to prevent apps from monitoring your keyboard input + /// when entering passwords or other sensitive information. + /// + /// This applies to the entire application, not just the focused terminal. + /// You must manually untoggle it or quit Ghostty entirely to disable it. + /// + /// Only implemented on macOS, as this uses a built-in system API. ToggleSecureInput, - // Toggle mouse reporting on or off. - // - // When mouse reporting is disabled, mouse events will not be reported to - // terminal applications even if they request it. This allows you to always - // use the mouse for selection and other terminal UI interactions without - // applications capturing mouse input. - // - // This can also be controlled via the `mouse-reporting` configuration - // option. + /// Toggle mouse reporting on or off. + /// + /// When mouse reporting is disabled, mouse events will not be reported to + /// terminal applications even if they request it. This allows you to always + /// use the mouse for selection and other terminal UI interactions without + /// applications capturing mouse input. + /// + /// This can also be controlled via the `mouse-reporting` configuration + /// option. ToggleMouseReporting, - // Toggle the command palette. - // - // The command palette is a popup that lets you see what actions - // you can perform, their associated keybindings (if any), a search bar - // to filter the actions, and the ability to then execute the action. - // - // This requires libadwaita 1.5 or newer on Linux. The current libadwaita - // version can be found by running `ghostty +version`. + /// Toggle the command palette. + /// + /// The command palette is a popup that lets you see what actions + /// you can perform, their associated keybindings (if any), a search bar + /// to filter the actions, and the ability to then execute the action. + /// + /// This requires libadwaita 1.5 or newer on Linux. The current libadwaita + /// version can be found by running `ghostty +version`. ToggleCommandPalette, - // Toggle the quick terminal. - // - // The quick terminal, also known as the "Quake-style" or drop-down - // terminal, is a terminal window that appears on demand from a keybinding, - // often sliding in from a screen edge such as the top. This is useful for - // quick access to a terminal without having to open a new window or tab. - // - // The terminal state is preserved between appearances, so showing the - // quick terminal after it was already hidden would display the same - // window instead of creating a new one. - // - // As quick terminals are often useful when other windows are currently - // focused, they are best used with *global* keybinds. For example, one - // can define the following key bind to toggle the quick terminal from - // anywhere within the system by pressing `` Cmd+` ``: - // - // ```ini - // keybind = global:cmd+backquote=toggle_quick_terminal - // ``` - // - // The quick terminal has some limitations: - // - // - Only one quick terminal instance can exist at a time. - // - // - Unlike normal terminal windows, the quick terminal will not be - // restored when the application is restarted on systems that support - // window restoration like macOS. - // - // - On Linux, the quick terminal is only supported on Wayland and not - // X11, and only on Wayland compositors that support the `wlr-layer-shell-v1` - // protocol. In practice, this means that only GNOME users would not be - // able to use this feature. - // - // - On Linux, slide-in animations are only supported on KDE, and when - // the "Sliding Popups" KWin plugin is enabled. - // - // If you do not have this plugin enabled, open System Settings > Apps - // & Windows > Window Management > Desktop Effects, and enable the - // plugin in the plugin list. Ghostty would then need to be restarted - // fully for this to take effect. - // - // - Quick terminal tabs are only supported on Linux and not on macOS. - // This is because tabs on macOS require a title bar. - // - // - On macOS, a fullscreened quick terminal will always be in non-native - // fullscreen mode. This is a requirement due to how the quick terminal - // is rendered. - // - // See the various configurations for the quick terminal in the - // configuration file to customize its behavior. + /// Toggle the quick terminal. + /// + /// The quick terminal, also known as the "Quake-style" or drop-down + /// terminal, is a terminal window that appears on demand from a keybinding, + /// often sliding in from a screen edge such as the top. This is useful for + /// quick access to a terminal without having to open a new window or tab. + /// + /// The terminal state is preserved between appearances, so showing the + /// quick terminal after it was already hidden would display the same + /// window instead of creating a new one. + /// + /// As quick terminals are often useful when other windows are currently + /// focused, they are best used with *global* keybinds. For example, one + /// can define the following key bind to toggle the quick terminal from + /// anywhere within the system by pressing `` Cmd+` ``: + /// + /// ```ini + /// keybind = global:cmd+backquote=toggle_quick_terminal + /// ``` + /// + /// The quick terminal has some limitations: + /// + /// - Only one quick terminal instance can exist at a time. + /// + /// - Unlike normal terminal windows, the quick terminal will not be + /// restored when the application is restarted on systems that support + /// window restoration like macOS. + /// + /// - On Linux, the quick terminal is only supported on Wayland and not + /// X11, and only on Wayland compositors that support the `wlr-layer-shell-v1` + /// protocol. In practice, this means that only GNOME users would not be + /// able to use this feature. + /// + /// - On Linux, slide-in animations are only supported on KDE, and when + /// the "Sliding Popups" KWin plugin is enabled. + /// + /// If you do not have this plugin enabled, open System Settings > Apps + /// & Windows > Window Management > Desktop Effects, and enable the + /// plugin in the plugin list. Ghostty would then need to be restarted + /// fully for this to take effect. + /// + /// - Quick terminal tabs are only supported on Linux and not on macOS. + /// This is because tabs on macOS require a title bar. + /// + /// - On macOS, a fullscreened quick terminal will always be in non-native + /// fullscreen mode. This is a requirement due to how the quick terminal + /// is rendered. + /// + /// See the various configurations for the quick terminal in the + /// configuration file to customize its behavior. ToggleQuickTerminal, - // Show or hide all windows. If all windows become shown, we also ensure - // Ghostty becomes focused. When hiding all windows, focus is yielded - // to the next application as determined by the OS. - // - // Note: When the focused surface is fullscreen, this method does nothing. - // - // Only implemented on macOS. + /// Show or hide all windows. If all windows become shown, we also ensure + /// Ghostty becomes focused. When hiding all windows, focus is yielded + /// to the next application as determined by the OS. + /// + /// Note: When the focused surface is fullscreen, this method does nothing. + /// + /// Only implemented on macOS. ToggleVisibility, - // Toggle the window background opacity between transparent and opaque. - // - // This does nothing when `background-opacity` is set to 1 or above. - // - // When `background-opacity` is less than 1, this action will either make - // the window transparent or not depending on its current transparency state. - // - // Only implemented on macOS. + /// Toggle the window background opacity between transparent and opaque. + /// + /// This does nothing when `background-opacity` is set to 1 or above. + /// + /// When `background-opacity` is less than 1, this action will either make + /// the window transparent or not depending on its current transparency state. + /// + /// Only implemented on macOS. ToggleBackgroundOpacity, - // Check for updates. - // - // Only implemented on macOS. + /// Check for updates. + /// + /// Only implemented on macOS. CheckForUpdates, - // Undo the last undoable action for the focused surface or terminal, - // if possible. This can undo actions such as closing tabs or - // windows. - // - // Not every action in Ghostty can be undone or redone. The list - // of actions support undo/redo is currently limited to: - // - // - New window, close window - // - New tab, close tab - // - New split, close split - // - // All actions are only undoable/redoable for a limited time. - // For example, restoring a closed split can only be done for - // some number of seconds since the split was closed. The exact - // amount is configured with the `undo-timeout` configuration settings. - // - // The undo/redo actions being limited ensures that there is - // bounded memory usage over time, closed surfaces don't continue running - // in the background indefinitely, and the keybinds become available - // for terminal applications to use. - // - // Only implemented on macOS. + /// Undo the last undoable action for the focused surface or terminal, + /// if possible. This can undo actions such as closing tabs or + /// windows. + /// + /// Not every action in Ghostty can be undone or redone. The list + /// of actions support undo/redo is currently limited to: + /// + /// - New window, close window + /// - New tab, close tab + /// - New split, close split + /// + /// All actions are only undoable/redoable for a limited time. + /// For example, restoring a closed split can only be done for + /// some number of seconds since the split was closed. The exact + /// amount is configured with the `undo-timeout` configuration settings. + /// + /// The undo/redo actions being limited ensures that there is + /// bounded memory usage over time, closed surfaces don't continue running + /// in the background indefinitely, and the keybinds become available + /// for terminal applications to use. + /// + /// Only implemented on macOS. Undo, - // Redo the last undoable action for the focused surface or terminal, - // if possible. See "undo" for more details on what can and cannot - // be undone or redone. + /// Redo the last undoable action for the focused surface or terminal, + /// if possible. See "undo" for more details on what can and cannot + /// be undone or redone. Redo, - // End the currently active key sequence, if any, and flush the - // keys up to this point to the terminal, excluding the key that - // triggered this action. - // - // For example: `ctrl+w>escape=end_key_sequence` would encode - // `ctrl+w` to the terminal and exit the key sequence. - // - // Normally, an invalid sequence will reset the key sequence and - // flush all data including the invalid key. This action allows - // you to flush only the prior keys, which is useful when you want - // to bind something like a control key (`ctrl+w`) but not send - // additional inputs. + /// End the currently active key sequence, if any, and flush the + /// keys up to this point to the terminal, excluding the key that + /// triggered this action. + /// + /// For example: `ctrl+w>escape=end_key_sequence` would encode + /// `ctrl+w` to the terminal and exit the key sequence. + /// + /// Normally, an invalid sequence will reset the key sequence and + /// flush all data including the invalid key. This action allows + /// you to flush only the prior keys, which is useful when you want + /// to bind something like a control key (`ctrl+w`) but not send + /// additional inputs. EndKeySequence, - // Activate a named key table (see `keybind` configuration documentation). - // The named key table will remain active until `deactivate_key_table` - // is called. If you want a one-shot key table activation, use the - // `activate_key_table_once` action instead. - // - // If the named key table does not exist, this action has no effect - // and performable will report false. - // - // If the named key table is already the currently active key table, - // this action has no effect and performable will report false. + /// Activate a named key table (see `keybind` configuration documentation). + /// The named key table will remain active until `deactivate_key_table` + /// is called. If you want a one-shot key table activation, use the + /// `activate_key_table_once` action instead. + /// + /// If the named key table does not exist, this action has no effect + /// and performable will report false. + /// + /// If the named key table is already the currently active key table, + /// this action has no effect and performable will report false. ActivateKeyTable(String), - // Same as activate_key_table, but the key table will only be active - // until the first valid keybinding from that table is used (including - // any defined `catch_all` bindings). - // - // The "once" check is only done if this is the currently active - // key table. If another key table is activated later, then this - // table will remain active until it pops back out to being the - // active key table. + /// Same as activate_key_table, but the key table will only be active + /// until the first valid keybinding from that table is used (including + /// any defined `catch_all` bindings). + /// + /// The "once" check is only done if this is the currently active + /// key table. If another key table is activated later, then this + /// table will remain active until it pops back out to being the + /// active key table. ActivateKeyTableOnce(String), - // Deactivate the currently active key table, if any. The next most - // recently activated key table (if any) will become active again. - // If no key table is active, this action has no effect. + /// Deactivate the currently active key table, if any. The next most + /// recently activated key table (if any) will become active again. + /// If no key table is active, this action has no effect. DeactivateKeyTable, - // Deactivate all active key tables. If no active key table exists, - // this will report performable as false. + /// Deactivate all active key tables. If no active key table exists, + /// this will report performable as false. DeactivateAllKeyTables, - // Quit Ghostty. + /// Quit Ghostty. Quit, - // Crash Ghostty in the desired thread for the focused surface. - // - // WARNING: This is a hard crash (panic) and data can be lost. - // - // The purpose of this action is to test crash handling. For some - // users, it may be useful to test crash reporting functionality in - // order to determine if it all works as expected. - // - // The value determines the crash location: - // - // - `main` - // - // Crash on the main (GUI) thread. - // - // - `io` - // - // Crash on the IO thread for the focused surface. - // - // - `render` - // - // Crash on the render thread for the focused surface. - // + /// Crash Ghostty in the desired thread for the focused surface. + /// + /// WARNING: This is a hard crash (panic) and data can be lost. + /// + /// The purpose of this action is to test crash handling. For some + /// users, it may be useful to test crash reporting functionality in + /// order to determine if it all works as expected. + /// + /// The value determines the crash location: + /// + /// - `main` + /// + /// Crash on the main (GUI) thread. + /// + /// - `io` + /// + /// Crash on the IO thread for the focused surface. + /// + /// - `render` + /// + /// Crash on the render thread for the focused surface. + /// Crash(CrashThread), } @@ -1236,729 +1236,11 @@ impl ActionKey { d.strip_suffix('\n').unwrap_or(d) } - /// The doc comment of the action (Zig `help_strings.KeybindAction`), + /// The doc comment of the action (Zig `help_strings.KeybindAction`, generated + /// at build time from the `///` docs of [`Action`] by `ghostty-helpgen`), /// each literal ending in one extra newline. pub fn docs(self) -> &'static str { - match self { - ActionKey::Ignore => { - r##"Ignore this key combination. - -Ghostty will not process this combination nor forward it to the child -process within the terminal, but it may still be processed by the OS or -other applications. -"## - } - ActionKey::Unbind => { - r##"Unbind a previously bound key binding. - -This cannot unbind bindings that were not bound by Ghostty or the user -(e.g. bindings set by the OS or some other application). -"## - } - ActionKey::Csi => { - r##"Send a CSI sequence. - -The value should be the CSI sequence without the CSI header (`ESC [` or -`\x1b[`). - -For example, `csi:0m` can be sent to reset all styles of the current text. -"## - } - ActionKey::Esc => { - r##"Send an `ESC` sequence. -"## - } - ActionKey::Text => { - r##"Send the specified text. - -Uses Zig string literal syntax. This is currently not validated. -If the text is invalid (i.e. contains an invalid escape sequence), -the error will currently only show up in logs. -"## - } - ActionKey::CursorKey => { - r##"Send data to the pty depending on whether cursor key mode is enabled -(`application`) or disabled (`normal`). -"## - } - ActionKey::Reset => { - r##"Reset the terminal. - -This can fix a lot of issues when a running program puts the terminal -into a broken state, equivalent to running the `reset` command. - -If you do this while in a TUI program such as vim, this may break -the program. If you do this while in a shell, you may have to press -enter after to get a new prompt. -"## - } - ActionKey::CopyToClipboard => { - r##"Copy the selected text to the clipboard. -"## - } - ActionKey::PasteFromClipboard => { - r##"Paste the contents of the default clipboard. -"## - } - ActionKey::PasteFromSelection => { - r##"Paste the contents of the selection clipboard. -"## - } - ActionKey::CopyUrlToClipboard => { - r##"If there is a URL under the cursor, copy it to the default clipboard. -"## - } - ActionKey::CopyTitleToClipboard => { - r##"Copy the terminal title to the clipboard. If the terminal title is not -set or is empty this has no effect. -"## - } - ActionKey::IncreaseFontSize => { - r##"Increase the font size by the specified amount in points (pt). - -For example, `increase_font_size:1.5` will increase the font size -by 1.5 points. -"## - } - ActionKey::DecreaseFontSize => { - r##"Decrease the font size by the specified amount in points (pt). - -For example, `decrease_font_size:1.5` will decrease the font size -by 1.5 points. -"## - } - ActionKey::ResetFontSize => { - r##"Reset the font size to the original configured size. -"## - } - ActionKey::SetFontSize => { - r##"Set the font size to the specified size in points (pt). - -For example, `set_font_size:14.5` will set the font size -to 14.5 points. -"## - } - ActionKey::Search => { - r##"Start a search for the given text. If the text is empty, then -the search is canceled. A canceled search will not disable any GUI -elements showing search. For that, the explicit end_search binding -should be used. - -If a previous search is active, it is replaced. -"## - } - ActionKey::SearchSelection => { - r##"Start a search for the current text selection. If there is no -selection, this does nothing. If a search is already active, this -changes the search terms. -"## - } - ActionKey::NavigateSearch => { - r##"Navigate the search results. If there is no active search, this -is not performed. -"## - } - ActionKey::StartSearch => { - r##"Start a search if it isn't started already. This doesn't set any -search terms, but opens the UI for searching. -"## - } - ActionKey::EndSearch => { - r##"End the current search if any and hide any GUI elements. -"## - } - ActionKey::ClearScreen => { - r##"Clear the screen and all scrollback. -"## - } - ActionKey::SelectAll => { - r##"Select all text on the screen. -"## - } - ActionKey::ScrollToTop => { - r##"Scroll to the top of the screen. -"## - } - ActionKey::ScrollToBottom => { - r##"Scroll to the bottom of the screen. -"## - } - ActionKey::ScrollToSelection => { - r##"Scroll to the selected text. -"## - } - ActionKey::ScrollToRow => { - r##"Scroll to the given absolute row in the screen with 0 being -the first row. -"## - } - ActionKey::ScrollPageUp => { - r##"Scroll the screen up by one page. -"## - } - ActionKey::ScrollPageDown => { - r##"Scroll the screen down by one page. -"## - } - ActionKey::ScrollPageFractional => { - r##"Scroll the screen by the specified fraction of a page. - -Positive values scroll downwards, and negative values scroll upwards. - -For example, `scroll_page_fractional:0.5` would scroll the screen -downwards by half a page, while `scroll_page_fractional:-1.5` would -scroll it upwards by one and a half pages. -"## - } - ActionKey::ScrollPageLines => { - r##"Scroll the screen by the specified amount of lines. - -Positive values scroll downwards, and negative values scroll upwards. - -For example, `scroll_page_lines:3` would scroll the screen downwards -by 3 lines, while `scroll_page_lines:-10` would scroll it upwards by 10 -lines. -"## - } - ActionKey::AdjustSelection => { - r##"Adjust the current selection in the given direction or position, -relative to the cursor. - -WARNING: This does not create a new selection, and does nothing when -there currently isn't one. - -Valid arguments are: - - - `left`, `right` - - Adjust the selection one cell to the left or right respectively. - - - `up`, `down` - - Adjust the selection one line upwards or downwards respectively. - - - `page_up`, `page_down` - - Adjust the selection one page upwards or downwards respectively. - - - `home`, `end` - - Adjust the selection to the top-left or the bottom-right corner - of the screen respectively. - - - `beginning_of_line`, `end_of_line` - - Adjust the selection to the beginning or the end of the line - respectively. - -"## - } - ActionKey::JumpToPrompt => { - r##"Jump the viewport forward or back by the given number of prompts. - -Requires shell integration. - -Positive values scroll downwards, and negative values scroll upwards. -"## - } - ActionKey::WriteScrollbackFile => { - r##"Write the entire scrollback into a temporary file with the specified -action. The action determines what to do with the filepath. - -Valid actions are: - - - `copy` - - Copy the file path into the clipboard. - - - `paste` - - Paste the file path into the terminal. - - - `open` - - Open the file in the default OS editor for text files. - - The default OS editor is determined by using `open` on macOS - and `xdg-open` on Linux. - -"## - } - ActionKey::WriteScreenFile => { - r##"Write the contents of the screen into a temporary file with the -specified action. - -See `write_scrollback_file` for possible actions. -"## - } - ActionKey::WriteSelectionFile => { - r##"Write the currently selected text into a temporary file with the -specified action. - -See `write_scrollback_file` for possible actions. - -Does nothing when no text is selected. -"## - } - ActionKey::NewWindow => { - r##"Open a new window. - -If the application isn't currently focused, -this will bring it to the front. -"## - } - ActionKey::NewTab => { - r##"Open a new tab. -"## - } - ActionKey::PreviousTab => { - r##"Go to the previous tab. -"## - } - ActionKey::NextTab => { - r##"Go to the next tab. -"## - } - ActionKey::LastTab => { - r##"Go to the last tab. -"## - } - ActionKey::GotoTab => { - r##"Go to the tab with the specific index, starting from 1. - -If the tab number is higher than the number of tabs, -this will go to the last tab. -"## - } - ActionKey::MoveTab => { - r##"Moves a tab by a relative offset. - -Positive values move the tab forwards, and negative values move it -backwards. If the new position is out of bounds, it is wrapped around -cyclically within the tab list. - -For example, `move_tab:1` moves the tab one position forwards, and if -it was already the last tab in the list, it wraps around and becomes -the first tab in the list. Likewise, `move_tab:-1` moves the tab one -position backwards, and if it was the first tab, then it will become -the last tab. -"## - } - ActionKey::ToggleTabOverview => { - r##"Toggle the tab overview. - -This is only supported on Linux and when the system's libadwaita -version is 1.4 or newer. The current libadwaita version can be -found by running `ghostty +version`. -"## - } - ActionKey::PromptSurfaceTitle => { - r##"Change the title of the current focused surface via a pop-up prompt. -"## - } - ActionKey::PromptTabTitle => { - r##"Change the title of the current tab via a pop-up prompt. The -title set via this prompt overrides any title set by the terminal -and persists across focus changes within the tab. -"## - } - ActionKey::SetSurfaceTitle => { - r##"Set the title for the current focused surface. - -If the title is empty, the surface title is reset to an empty title. -"## - } - ActionKey::SetTabTitle => { - r##"Set the title for the current focused tab. - -If the title is empty, the tab title override is cleared. -"## - } - ActionKey::NewSplit => { - r##"Create a new split in the specified direction. - -Valid arguments: - - - `right`, `down`, `left`, `up` - - Creates a new split in the corresponding direction. - - - `auto` - - Creates a new split along the larger direction. - For example, if the parent split is currently wider than it is tall, - then a left-right split would be created, and vice versa. - -"## - } - ActionKey::GotoSplit => { - r##"Focus on a split either in the specified direction (`right`, `down`, -`left` and `up`), or in the adjacent split in the order of creation -(`previous` and `next`). -"## - } - ActionKey::GotoWindow => { - r##"Focus on either the previous window or the next one ('previous', 'next') -"## - } - ActionKey::ToggleSplitZoom => { - r##"Zoom in or out of the current split. - -When a split is zoomed into, it will take up the entire space in -the current tab, hiding other splits. The tab or tab bar would also -reflect this by displaying an icon indicating the zoomed state. -"## - } - ActionKey::ToggleReadonly => { - r##"Toggle read-only mode for the current surface. - -When a surface is in read-only mode: - - No input is sent to the PTY (mouse events, key encoding) - - Input can still be used at the terminal level to make selections, - copy/paste (keybinds), scroll, etc. - - Warn before quit is always enabled in this state even if an active - process is not running -"## - } - ActionKey::ResizeSplit => { - r##"Resize the current split in the specified direction and amount in -pixels. The two arguments should be joined with a comma (`,`), -like in `resize_split:up,10`. -"## - } - ActionKey::EqualizeSplits => { - r##"Equalize the size of all splits in the current window. -"## - } - ActionKey::ResetWindowSize => { - r##"Reset the window to the default size. The "default size" is the -size that a new window would be created with. This has no effect -if the window is fullscreen. - -Only implemented on macOS. -"## - } - ActionKey::Inspector => { - r##"Control the visibility of the terminal inspector. - -Valid arguments: `toggle`, `show`, `hide`. -"## - } - ActionKey::ShowGtkInspector => { - r##"Show the GTK inspector. - -Has no effect on macOS. -"## - } - ActionKey::ShowOnScreenKeyboard => { - r##"Show the on-screen keyboard if one is present. - -Only implemented on Linux (GTK). On GNOME, the "Screen Keyboard" -accessibility feature must be turned on, which can be found under -Settings > Accessibility > Typing. Other platforms are as of now -untested. -"## - } - ActionKey::OpenConfig => { - r##"Open the configuration file in the default OS editor. - -If your default OS editor isn't configured then this will fail. -Currently, any failures to open the configuration will show up only in -the logs. -"## - } - ActionKey::ReloadConfig => { - r##"Reload the configuration. - -The exact meaning depends on the app runtime in use, but this usually -involves re-reading the configuration file and applying any changes -Note that not all changes can be applied at runtime. -"## - } - ActionKey::CloseSurface => { - r##"Close the current "surface", whether that is a window, tab, split, etc. - -This might trigger a close confirmation popup, depending on the value -of the `confirm-close-surface` configuration setting. -"## - } - ActionKey::CloseTab => { - r##"Close the current tab and all splits therein, close all other tabs, or -close every tab to the right of the current one depending on the mode. - -If the mode is not specified, defaults to closing the current tab. - -This might trigger a close confirmation popup, depending on the value -of the `confirm-close-surface` configuration setting. -"## - } - ActionKey::CloseWindow => { - r##"Close the current window and all tabs and splits therein. - -This might trigger a close confirmation popup, depending on the value -of the `confirm-close-surface` configuration setting. -"## - } - ActionKey::CloseAllWindows => { - r##"Close all windows. - -WARNING: This action has been deprecated and has no effect on either -Linux or macOS. Users are instead encouraged to use `all:close_window` -instead. -"## - } - ActionKey::ToggleMaximize => { - r##"Maximize or unmaximize the current window. - -This has no effect on macOS as it does not have the concept of -maximized windows. -"## - } - ActionKey::ToggleFullscreen => { - r##"Fullscreen or unfullscreen the current window. -"## - } - ActionKey::ToggleWindowDecorations => { - r##"Toggle window decorations (titlebar, buttons, etc.) for the current window. - -Only implemented on Linux. -"## - } - ActionKey::ToggleWindowFloatOnTop => { - r##"Toggle whether the terminal window should always float on top of other -windows even when unfocused. - -Terminal windows always start as normal (not float-on-top) windows. - -Only implemented on macOS. -"## - } - ActionKey::ToggleSecureInput => { - r##"Toggle secure input mode. - -This is used to prevent apps from monitoring your keyboard input -when entering passwords or other sensitive information. - -This applies to the entire application, not just the focused terminal. -You must manually untoggle it or quit Ghostty entirely to disable it. - -Only implemented on macOS, as this uses a built-in system API. -"## - } - ActionKey::ToggleMouseReporting => { - r##"Toggle mouse reporting on or off. - -When mouse reporting is disabled, mouse events will not be reported to -terminal applications even if they request it. This allows you to always -use the mouse for selection and other terminal UI interactions without -applications capturing mouse input. - -This can also be controlled via the `mouse-reporting` configuration -option. -"## - } - ActionKey::ToggleCommandPalette => { - r##"Toggle the command palette. - -The command palette is a popup that lets you see what actions -you can perform, their associated keybindings (if any), a search bar -to filter the actions, and the ability to then execute the action. - -This requires libadwaita 1.5 or newer on Linux. The current libadwaita -version can be found by running `ghostty +version`. -"## - } - ActionKey::ToggleQuickTerminal => { - r##"Toggle the quick terminal. - -The quick terminal, also known as the "Quake-style" or drop-down -terminal, is a terminal window that appears on demand from a keybinding, -often sliding in from a screen edge such as the top. This is useful for -quick access to a terminal without having to open a new window or tab. - -The terminal state is preserved between appearances, so showing the -quick terminal after it was already hidden would display the same -window instead of creating a new one. - -As quick terminals are often useful when other windows are currently -focused, they are best used with *global* keybinds. For example, one -can define the following key bind to toggle the quick terminal from -anywhere within the system by pressing `` Cmd+` ``: - -```ini -keybind = global:cmd+backquote=toggle_quick_terminal -``` - -The quick terminal has some limitations: - - - Only one quick terminal instance can exist at a time. - - - Unlike normal terminal windows, the quick terminal will not be - restored when the application is restarted on systems that support - window restoration like macOS. - - - On Linux, the quick terminal is only supported on Wayland and not - X11, and only on Wayland compositors that support the `wlr-layer-shell-v1` - protocol. In practice, this means that only GNOME users would not be - able to use this feature. - - - On Linux, slide-in animations are only supported on KDE, and when - the "Sliding Popups" KWin plugin is enabled. - - If you do not have this plugin enabled, open System Settings > Apps - & Windows > Window Management > Desktop Effects, and enable the - plugin in the plugin list. Ghostty would then need to be restarted - fully for this to take effect. - - - Quick terminal tabs are only supported on Linux and not on macOS. - This is because tabs on macOS require a title bar. - - - On macOS, a fullscreened quick terminal will always be in non-native - fullscreen mode. This is a requirement due to how the quick terminal - is rendered. - -See the various configurations for the quick terminal in the -configuration file to customize its behavior. -"## - } - ActionKey::ToggleVisibility => { - r##"Show or hide all windows. If all windows become shown, we also ensure -Ghostty becomes focused. When hiding all windows, focus is yielded -to the next application as determined by the OS. - -Note: When the focused surface is fullscreen, this method does nothing. - -Only implemented on macOS. -"## - } - ActionKey::ToggleBackgroundOpacity => { - r##"Toggle the window background opacity between transparent and opaque. - -This does nothing when `background-opacity` is set to 1 or above. - -When `background-opacity` is less than 1, this action will either make -the window transparent or not depending on its current transparency state. - -Only implemented on macOS. -"## - } - ActionKey::CheckForUpdates => { - r##"Check for updates. - -Only implemented on macOS. -"## - } - ActionKey::Undo => { - r##"Undo the last undoable action for the focused surface or terminal, -if possible. This can undo actions such as closing tabs or -windows. - -Not every action in Ghostty can be undone or redone. The list -of actions support undo/redo is currently limited to: - - - New window, close window - - New tab, close tab - - New split, close split - -All actions are only undoable/redoable for a limited time. -For example, restoring a closed split can only be done for -some number of seconds since the split was closed. The exact -amount is configured with the `undo-timeout` configuration settings. - -The undo/redo actions being limited ensures that there is -bounded memory usage over time, closed surfaces don't continue running -in the background indefinitely, and the keybinds become available -for terminal applications to use. - -Only implemented on macOS. -"## - } - ActionKey::Redo => { - r##"Redo the last undoable action for the focused surface or terminal, -if possible. See "undo" for more details on what can and cannot -be undone or redone. -"## - } - ActionKey::EndKeySequence => { - r##"End the currently active key sequence, if any, and flush the -keys up to this point to the terminal, excluding the key that -triggered this action. - -For example: `ctrl+w>escape=end_key_sequence` would encode -`ctrl+w` to the terminal and exit the key sequence. - -Normally, an invalid sequence will reset the key sequence and -flush all data including the invalid key. This action allows -you to flush only the prior keys, which is useful when you want -to bind something like a control key (`ctrl+w`) but not send -additional inputs. -"## - } - ActionKey::ActivateKeyTable => { - r##"Activate a named key table (see `keybind` configuration documentation). -The named key table will remain active until `deactivate_key_table` -is called. If you want a one-shot key table activation, use the -`activate_key_table_once` action instead. - -If the named key table does not exist, this action has no effect -and performable will report false. - -If the named key table is already the currently active key table, -this action has no effect and performable will report false. -"## - } - ActionKey::ActivateKeyTableOnce => { - r##"Same as activate_key_table, but the key table will only be active -until the first valid keybinding from that table is used (including -any defined `catch_all` bindings). - -The "once" check is only done if this is the currently active -key table. If another key table is activated later, then this -table will remain active until it pops back out to being the -active key table. -"## - } - ActionKey::DeactivateKeyTable => { - r##"Deactivate the currently active key table, if any. The next most -recently activated key table (if any) will become active again. -If no key table is active, this action has no effect. -"## - } - ActionKey::DeactivateAllKeyTables => { - r##"Deactivate all active key tables. If no active key table exists, -this will report performable as false. -"## - } - ActionKey::Quit => { - r##"Quit Ghostty. -"## - } - ActionKey::Crash => { - r##"Crash Ghostty in the desired thread for the focused surface. - -WARNING: This is a hard crash (panic) and data can be lost. - -The purpose of this action is to test crash handling. For some -users, it may be useful to test crash reporting functionality in -order to determine if it all works as expected. - -The value determines the crash location: - - - `main` - - Crash on the main (GUI) thread. - - - `io` - - Crash on the IO thread for the focused surface. - - - `render` - - Crash on the render thread for the focused surface. - -"## - } - } + include!(concat!(env!("OUT_DIR"), "/keybind_action_docs.rs")) } } diff --git a/docs/rewrite/ARCHITECTURE.md b/docs/rewrite/ARCHITECTURE.md index cad4c9d..8114757 100644 --- a/docs/rewrite/ARCHITECTURE.md +++ b/docs/rewrite/ARCHITECTURE.md @@ -62,6 +62,7 @@ ghostty-bench (benchmarks), ghostty-build-data (completions, man pages, syntax). | `ghostty-cli` | cli/* actions, build_config.zig (build info) | apprt, config, font, input, os, vt | | `ghostty-apprt-gtk` | apprt/gtk/* (+ ui/*.blp, css) | apprt, config, input, inspector, os, vt, datastruct | | `ghostty-bench` | benchmark/*, synthetic/* | terminal, unicode, vt | +| `ghostty-helpgen` | helpgen.zig (build-time doc-comment extraction for CLI-action and keybind-action help; used from `build.rs` of ghostty-cli / ghostty-input) | – | | `ghostty-build-data` | main_build_data.zig, extra/*, build/mdgen | cli, config, input, os, termio | | root `ghostty-rs` (bin `ghostty`) | main_ghostty.zig | apprt, apprt-gtk, cli, config, core, os | @@ -123,11 +124,19 @@ integration), `po/` (translations), crate-local `res/`/`shaders/`/`resources/`. - 2026-10-12: Sentry crash handling and GTK<4.14/libadwaita<1.5 fallbacks, previously omitted, are ported in Phase 8 for full parity. +- 2026-10-12 (GC): the GTK/libadwaita compatibility fallbacks are ported + (MessageDialog, SwitchRow-less clipboard template, pre-Banner debug warning + and child-exited, GtkAboutDialog, CSS error source bytes, all blueprint + variants in the gresource). Minimum versions: **GTK 4.14, libadwaita 1.5** -- + Ghostty's own floor (its build compiles every blueprint, incl. the `1.5/` + ones, and calls `gtk_css_provider_load_from_bytes` (4.12) and + `gtk_im_context_activate_osk` (4.14) unconditionally); the `gnome_44` + binding feature was dropped. Version gating mirrors Zig's `atLeast` + (build level AND runtime): cargo features `adw_1_3|1_4|1_5` and `gtk_4_16` + (all default) are the "header version"; building without them selects the + Zig fallbacks at compile time. See `notes/compat-GC.md`. ## Intentional omissions (decided by the orchestrator) - Sentry crash reporting (`crash/*`): optional and disabled by default in upstream Linux builds; `+crash-report` listing is ported. -- GTK < 4.14 / libadwaita < 1.5 compatibility fallbacks (MessageDialog, - pre-Banner widgets, old about dialog, 1.0–1.3 blueprint variants): the - Rust port requires GTK ≥ 4.14 and libadwaita ≥ 1.5. diff --git a/docs/rewrite/notes/compat-GC-bench.md b/docs/rewrite/notes/compat-GC-bench.md new file mode 100644 index 0000000..02d09ee --- /dev/null +++ b/docs/rewrite/notes/compat-GC-bench.md @@ -0,0 +1,18 @@ +# ghostty-bench: CodepointWidth / IsSymbol (compat-GC) + +- `codepoint-width` modes (CLI as Zig: noop|wcwidth|simd|table): + - `wcwidth`: real libc `wcwidth` via a local `extern "C"` (no libc crate), in + a module with `#[allow(unsafe_code)]` + SAFETY comment. Zig does not call + `setlocale`, so neither do we (C locale). Non-unix: warning no-op like Zig on Windows. + - `simd`: new `src/simd_width.rs`, scalar port of `simd/codepoint_width.cpp` + (same range tables and precedence; vector lanes replaced by a sequential scan). + - `table`: generated two-stage table with the `<= 0xFF => 1` shortcut. +- `is-symbol` modes: `uucode` (default) = `is_symbol_computed`, computed from + private-use + block ranges (`computeIsSymbol`); `table` = generated symbols table. + Test verifies equality with the table over all codepoints. +- Cargo.toml: `[lints]` no longer `workspace = true` (workspace `forbid` cannot be + locally allowed); `unsafe_code = "deny"` and the clippy set is copied. Keep in sync. +- Deviation: Zig's `simd` falls back to uucode when built without SIMD; there is no + separate uucode width mode in the CLI (none in Zig), so none added. +- Tests: all modes run on a sample file; computed vs table symbols; simd vs table + samples; wcwidth ASCII. Zig `test CodepointWidth/IsSymbol` (run once, no data) covered by the run tests. diff --git a/docs/rewrite/notes/compat-GC-cli.md b/docs/rewrite/notes/compat-GC-cli.md new file mode 100644 index 0000000..55e6d88 --- /dev/null +++ b/docs/rewrite/notes/compat-GC-cli.md @@ -0,0 +1,13 @@ +# compat-GC: ghostty-cli Windows branches and DiskCache locking + +Ported (all `cfg(windows)` / cfg-gated, no unsafe, no new deps): +* `edit_config.rs`: Windows prints "not supported on Windows ... edit manually at " to stderr and returns 1 (after the path lookup, before the editor lookup, as in Zig). The `exec` path is now `cfg(unix)` (`exec_editor`). +* `list_colors.rs` / `list_keybinds.rs`: both use `tui::window_size()`; on Windows it returns the fixed Zig default (120 cols x 24 rows) instead of querying the tty. Pixel size (1024x768) is not modelled (unused). +* `boo.rs`: Zig allows windows/macos/linux/freebsd and returns 1 elsewhere; mirrored with a `cfg!` check. Windows needs no other change. +* `ssh_cache/disk_cache.rs`: made the module compile on Windows (unix-only `mode`/`PermissionsExt` gated; `fixup_permissions` is a no-op on Windows as in Zig "Windows does not support chmod"). + +Locking deviation from the task brief: Zig does NOT lock on Windows (`if (comptime builtin.os.tag != .windows) file.tryLock(.exclusive)` and the matching unlock). So the faithful port is a no-op lock on Windows rather than LockFileEx; hence no windows-sys/unsafe. On unix the existing implementation already matched Zig: `File::try_lock` (flock LOCK_EX|LOCK_NB), WouldBlock is ignored (proceeds unlocked, as Zig ignores the bool), other errors -> CacheIsLocked, unlock on drop. + +Tests: unix `lock_is_exclusive_until_dropped`; windows-only `lock_is_noop_on_windows` (compiled only); permission assertion split into unix-only `disk_cache_file_permissions`. + +Verified: build/test(92)/clippy -D warnings on linux; `cargo check --target x86_64-pc-windows-gnu --tests -p ghostty-cli` and windows clippy clean (stub fontconfig.pc, RUSTC_WRAPPER= as in windows-WIN.md). diff --git a/docs/rewrite/notes/compat-GC-helpgen.md b/docs/rewrite/notes/compat-GC-helpgen.md new file mode 100644 index 0000000..3a6299d --- /dev/null +++ b/docs/rewrite/notes/compat-GC-helpgen.md @@ -0,0 +1,43 @@ +# compat-GC: helpgen (ZIG/src/helpgen.zig) + +Zig generates help strings at build time from doc comments (`helpgen.zig`, +exposed as `help_strings`). The Rust port does the same instead of keeping +checked-in tables. + +## Design + +* New crate `crates/ghostty-helpgen` (no dependencies), used as a + `[build-dependencies]` of `ghostty-cli` and `ghostty-input`. + * `doc_lines`: port of `extractDocComments`/`findCommonPrefix` (strip + `///`, remove the common leading-space prefix of non-blank lines, keep + blank lines and code fences verbatim). + * `run_fn_doc`: doc comment of a module's `run` function (Zig `genActions`). + * `enum_variant_docs`: `///` docs per variant of an enum (Zig + `genKeybindActions`). + * `*_match`: emit Rust `match self { ... }` bodies with escaped literals. +* `ghostty-cli/build.rs` reads the variants of `enum Action` in + `src/ghostty.rs`, maps `ListFonts` -> `src/list_fonts.rs`, extracts the doc + of `pub fn run`, writes `$OUT_DIR/action_help.rs`; `Action::help_text` + `include!`s it. A missing doc comment fails the build (as in Zig). + The former `src/help/*.txt` files are deleted. +* `ghostty-input/build.rs` extracts the docs of every variant of + `enum Action` in `src/binding/action.rs` (now `///` instead of `//`), + writes `$OUT_DIR/keybind_action_docs.rs`, included by `ActionKey::docs` + (docs = lines joined by `\n` plus one trailing `\n`, as before; + `help_string` strips one). +* Config help already derives from field docs through `config_struct!` + (`ghostty-config/src/macros.rs`), so no generation was needed there. +* A line-based scanner is used rather than `syn`: it mirrors Zig's token + based approach (doc run + following item) and avoids a heavy build dep. + +## Verification + +Outputs of `+help`, `+list-actions [--docs]` and every `+ --help` +were captured before and after: byte identical. Differences vs +`/usr/bin/ghostty` exist only for `+version --help` and `+new-window --help` +(unchanged from the baseline). + +## Tests + +`ghostty-helpgen` unit tests (marker stripping, common prefix, blank lines, +code fences, `////`, run-doc and enum-variant extraction, escaping). diff --git a/docs/rewrite/notes/compat-GC.md b/docs/rewrite/notes/compat-GC.md new file mode 100644 index 0000000..4cee3e3 --- /dev/null +++ b/docs/rewrite/notes/compat-GC.md @@ -0,0 +1,76 @@ +# compat-GC: GTK/libadwaita compat fallbacks, bench modes, helpgen, CLI Windows + +Sub-notes: `compat-GC-bench.md`, `compat-GC-helpgen.md`, `compat-GC-cli.md`. + +## 1. GTK / libadwaita compatibility (crates/ghostty-apprt-gtk) + +### Ghostty's own minimum (finding) +`ZIG/src/build/SharedDeps.zig` runs `blueprint.zig` over *every* entry of +`gresource.blueprints`, which refuses to compile a `./x.blp` if the +libadwaita headers are older than that version; the list contains `1.5/` files +(window, tab, command palette, ...). So Ghostty 1.3.1 cannot be *built* against +libadwaita < 1.5, and the Zig code calls `gtk_css_provider_load_from_bytes` +(4.12) and `gtk_im_context_activate_osk` (4.14) unguarded. Ghostty's real +minimum is therefore **GTK 4.14 / libadwaita 1.5**, which is what the crate +already required; I verified this by compiling with `gtk4` features `v4_8`/`v4_10` +(fails on `load_from_bytes`/`activate_osk`) and `v4_14` (compiles). The only +change to the requirement is dropping the unneeded `gnome_44` feature. In Zig the +fallbacks below are only reachable in a (hypothetical) build against older +headers; they are ported with identical selection logic. + +### Version gating (gtk_version.rs, adw_version.rs) +Zig's `atLeast` = comptime header version AND runtime library version. Rust: +* cargo features are the "header version": `adw_1_3` < `adw_1_4` < `adw_1_5` + and `gtk_4_16` (`gtk/v4_16`); all are default. `BUILD_LEVEL` / + `const fn comptime_at_least` read them; `at_least` = `comptime_at_least && + runtime_at_least`; `runtime_at_least`/`runtime_until` unchanged. + `COMPTIME_VERSION` (pkg-config) is kept for `+version` output. +* Types that Zig selects at comptime are selected with `cfg(feature = ...)`. +* Debug builds only: `GHOSTTY_DEBUG_FAKE_GTK_VERSION` / `GHOSTTY_DEBUG_FAKE_ADW_VERSION` + (e.g. `1.4.0`) override the *runtime* version answers (`fake_version.rs`; no Zig + counterpart) so runtime branches can be exercised on new libraries. +* Root `build.rs` links the binary with `-z lazy` (Rust defaults to `-z now`) so + symbols only used behind a runtime check (GTK 4.16 `gtk_css_section_get_bytes`) + don't stop an older library from loading the program, like Zig's lazy binding. + +### Fallbacks ported +| Zig | Rust | +|---|---| +| `class/dialog.zig` `Parent = AlertDialog / MessageDialog`, `present`/`close` | `class/dialog.rs`: `DialogParent` + `present`/`close` per cfg `adw_1_5`; macros `dialog_subclass_wrapper!`/`impl_dialog_parent!` give the three subclasses (close-confirmation, config-errors, clipboard) the right `@extends` chain and `response` override for both parents | +| `clipboard_confirmation_dialog.zig` `can_remember`, template 1.4 vs 1.0 | `cfg_attr` template resource `ui/1.4` (adw_1_4) vs `ui/1.0`; `remember_choice` child only with SwitchRow; `CAN_REMEMBER`; `remember=false` without it | +| `debug_warning.zig` Parent `adw.Bin`/`gtk.Box`, template 1.3/1.2 | `class/debug_warning.rs` cfg `adw_1_3` | +| `surface_child_exited.zig` Banner / `SurfaceChildExitedNoop` | `class/surface_child_exited.rs`: noop widget (no template, `NOOP`, `close-request` emitted from `set_child_exited`); `Surface::child_exited` returns false for the noop, as in Zig. NB: Zig has no `1.2/surface-child-exited.blp` (the task text assumed one); the pre-Banner implementation is the Noop | +| `window.zig` `actionAbout` | `adw::AboutDialog` if `supports_dialogs()` else `gtk::show_about_dialog` | +| `application.zig` `signalCssParsingError` (GTK >= 4.16 branch) | `css.rs`: `domain code message` + `\n` (`CssSection::bytes`) behind `gtk_4_16` + runtime check, plain line otherwise; tested via `parsing_error_message` | +| blueprint variants | `build.rs` already compiles 1.0/1.2/1.3/1.4/1.5 variants (same list as `gresource.zig`) into the gresource; the class selects at build level (as Zig) | + +### Verification +* `cargo clippy -p ghostty-apprt-gtk --all-targets -D warnings` clean with default + features AND with `default = []` (temporarily edited), i.e. both the new and + the fallback code compile. +* Default build: GTK app runs with the debug replay hook (paste dialog shows + `GhosttyClipboardConfirmationDialog` with the remember switch; no GTK criticals). +* Fallback build (`default = []`, run via the replay hook): the paste confirmation + and the close confirmation come up as `adw::MessageDialog`-based + `GhosttyClipboardConfirmationDialog` / `GhosttyCloseConfirmationDialog` + (transient for the window), the binary has no `BIND_NOW`. +* Workspace gate (excluding L1's in-progress `ghostty-lib`): `cargo clippy --workspace + --all-targets -D warnings`, `cargo test --workspace`, `cargo +nightly fmt --all` pass. + `+help`, `+list-actions [--docs]`, `+list-keybinds --help`, `+show-config --help` + are byte-identical to `/usr/bin/ghostty`. + +### Deviation found by the runtime check +Zig's `GhosttyCloseConfirmationDialog` registers a `close-request` signal; with +the `MessageDialog` (a `GtkWindow`) parent that name already exists in the class +ancestry and GObject aborts. Zig's fallback has the same latent bug (dead code +upstream). The fallback build here names it `dialog-close-request` +(`close_confirmation_dialog.rs::CLOSE_REQUEST`); default build unchanged. + +### Not testable here +Real old libraries (GTK < 4.16 / libadwaita < 1.5) are not installed; only the +compile-time fallback selection and the version arithmetic are exercised. + +## 2-4 +See the sub-notes. Deviations vs the task text: Zig `CodepointWidth` has no +`uucode` mode (bench); Zig does no file locking on Windows, so the faithful +port is a no-op there (cli).