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

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

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

No files matched your search

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