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