diff --git a/build.rs b/build.rs
index 4ce698d..3f87cff 100644
--- a/build.rs
+++ b/build.rs
@@ -1,16 +1,44 @@
-//! Link the final binary with lazy symbol binding.
+//! Root build script.
//!
-//! 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.
+//! * Linux: 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.
+//! * Windows (Zig: `GhosttyExe.zig`): use the GUI subsystem and embed
+//! `dist/windows/ghostty.rc` (icon, version resource and the manifest that
+//! declares per-monitor DPI awareness and the common-controls v6 activation
+//! context).
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");
+ match std::env::var("CARGO_CFG_TARGET_OS").as_deref() {
+ Ok("linux") => println!("cargo:rustc-link-arg-bins=-Wl,-z,lazy"),
+ Ok("windows") => windows(),
+ _ => {}
}
}
+
+fn windows() {
+ println!("cargo:rerun-if-changed=dist/windows/ghostty.rc");
+ println!("cargo:rerun-if-changed=dist/windows/ghostty.ico");
+ println!("cargo:rerun-if-changed=dist/windows/ghostty.manifest");
+
+ // `exe.subsystem = .Windows`.
+ if std::env::var("CARGO_CFG_TARGET_ENV").as_deref() == Ok("msvc") {
+ println!("cargo:rustc-link-arg-bins=/SUBSYSTEM:WINDOWS");
+ println!("cargo:rustc-link-arg-bins=/ENTRY:mainCRTStartup");
+ } else {
+ println!("cargo:rustc-link-arg-bins=-Wl,--subsystem,windows");
+ }
+
+ // `exe.addWin32ResourceFile(.{ .file = "dist/windows/ghostty.rc" })`.
+ // The resource is only linked into the binary targets, not the
+ // (nonexistent) library target of this package.
+ embed_resource::compile_for("dist/windows/ghostty.rc", ["ghostty"], embed_resource::NONE)
+ .manifest_required()
+ .expect("failed to embed the Windows resource file");
+}
diff --git a/crates/ghostty-build-data/src/lib.rs b/crates/ghostty-build-data/src/lib.rs
index e3fa2e6..ee5e2f3 100644
--- a/crates/ghostty-build-data/src/lib.rs
+++ b/crates/ghostty-build-data/src/lib.rs
@@ -15,6 +15,7 @@ pub mod fish;
pub mod mdgen;
pub mod sublime;
pub mod vim;
+pub mod webdata;
pub mod zsh;
#[allow(clippy::needless_raw_string_hashes)]
@@ -43,6 +44,14 @@ pub enum Mode {
Man1,
/// `ghostty(5)` man page markdown (Zig: `mdgen_ghostty_5`).
Man5,
+ /// Website `config.mdx` (Zig: `webgen_config`).
+ WebdataConfig,
+ /// Website `actions.mdx` (keybind actions; Zig: `webgen_actions`).
+ WebdataActions,
+ /// Website `commands.mdx` (CLI actions; Zig: `webgen_commands`).
+ WebdataCommands,
+ /// The Ghostty version string (embedded into source tarballs as `VERSION`).
+ Version,
}
impl Mode {
@@ -58,6 +67,10 @@ impl Mode {
("terminfo", Mode::Terminfo),
("mdgen-ghostty-1", Mode::Man1),
("mdgen-ghostty-5", Mode::Man5),
+ ("webgen-config", Mode::WebdataConfig),
+ ("webgen-actions", Mode::WebdataActions),
+ ("webgen-commands", Mode::WebdataCommands),
+ ("version", Mode::Version),
];
/// Parse `+name` (Zig action syntax) or a bare `name`.
@@ -82,6 +95,10 @@ impl Mode {
Mode::Terminfo => ghostty_termio::terminfo::ghostty.encode_to_string(),
Mode::Man1 => mdgen::ghostty_1(),
Mode::Man5 => mdgen::ghostty_5(),
+ Mode::WebdataConfig => webdata::config_mdx(),
+ Mode::WebdataActions => webdata::actions_mdx(),
+ Mode::WebdataCommands => webdata::commands_mdx(),
+ Mode::Version => ghostty_cli::build_info::get().version_string.clone(),
}
}
}
@@ -174,6 +191,27 @@ mod tests {
assert!(five.contains("**`new_split`**"));
}
+ #[test]
+ fn webdata_has_frontmatter_and_sections() {
+ let config = webdata::config_mdx();
+ assert!(config.starts_with(
+ "---\ntitle: Reference\ndescription: Reference of all Ghostty configuration options.\n"
+ ));
+ assert!(config.contains("organize these options.\n\n## `language`\n\n"));
+ assert!(config.contains("## `font-family`\n"));
+ assert!(!config.contains("## `_"));
+ // Callouts and indented code blocks are rewritten for the website.
+ assert!(config.contains("\n") || config.contains("\n"));
+ assert!(config.contains("```\n"));
+ let actions = webdata::actions_mdx();
+ assert!(actions.starts_with("---\ntitle: Keybinding Action Reference\n"));
+ assert!(actions.contains("\n## `ignore`\nIgnore this key combination.\n"));
+ let commands = webdata::commands_mdx();
+ assert!(commands.contains("## version\n"));
+ assert!(commands.contains("\n```\nghostty --version\n```\n\n"));
+ assert!(commands.contains("\n```\nghostty +list-fonts\n```\n\n"));
+ }
+
/// Every supported locale has a `.po` catalog and vice versa
/// (Zig: `GhosttyI18n.zig` iterates `os/i18n_locales.zig`).
#[test]
diff --git a/crates/ghostty-build-data/src/webdata.rs b/crates/ghostty-build-data/src/webdata.rs
new file mode 100644
index 0000000..31f97f7
--- /dev/null
+++ b/crates/ghostty-build-data/src/webdata.rs
@@ -0,0 +1,210 @@
+//! Website data generators (port of `build/GhosttyWebdata.zig` and
+//! `build/webgen/main_{config,actions,commands}.zig`). They produce the
+//! `config.mdx`, `actions.mdx` (keybind actions) and `commands.mdx` (CLI
+//! actions) files installed to `share/ghostty/webdata/`.
+
+use ghostty_input::binding::ActionKey;
+
+use crate::config_fields;
+
+/// `webgen/main_config.zig`: the configuration reference.
+#[must_use]
+pub fn config_mdx() -> String {
+ #[derive(Clone, Copy, PartialEq, Eq)]
+ enum Block {
+ Text,
+ Code,
+ CalloutNote,
+ CalloutWarning,
+ }
+
+ fn end_block(w: &mut String, block: Option) {
+ match block {
+ Some(Block::Code) => w.push_str("```\n"),
+ Some(Block::CalloutNote) => w.push_str("\n"),
+ Some(Block::CalloutWarning) => w.push_str("\n"),
+ Some(Block::Text) | None => {}
+ }
+ }
+
+ fn starts_with_ignore_case(s: &str, prefix: &str) -> bool {
+ s.len() >= prefix.len()
+ && s.as_bytes()[..prefix.len()].eq_ignore_ascii_case(prefix.as_bytes())
+ }
+
+ let mut w = String::from(
+ "---\n\
+title: Reference\n\
+description: Reference of all Ghostty configuration options.\n\
+editOnGithubLink: https://github.com/ghostty-org/ghostty/edit/main/src/config/Config.zig\n\
+---\n\
+\n\
+This is a reference of all Ghostty configuration options. These\n\
+options are ordered roughly by how common they are to be used\n\
+and grouped with related options. I recommend utilizing your\n\
+browser's search functionality to find the option you're looking\n\
+for.\n\
+\n\
+In the future, we'll have a more user-friendly way to view and\n\
+organize these options.\n\
+\n",
+ );
+
+ let fields = config_fields();
+ for (i, field) in fields.iter().enumerate() {
+ if field.help.is_empty() {
+ continue;
+ }
+ w.push_str("## `");
+ w.push_str(field.name);
+ w.push_str("`\n");
+
+ // For all subsequent fields with no docs, they are grouped with the
+ // previous field. (Zig breaks at `_` fields; those are already
+ // filtered out of `config_fields`, which would otherwise leave a
+ // gap, so the group may only extend to the next raw field.)
+ for next in &fields[i + 1..] {
+ if !next.help.is_empty() {
+ break;
+ }
+ w.push_str("## `");
+ w.push_str(next.name);
+ w.push_str("`\n");
+ }
+
+ w.push('\n');
+
+ let mut block: Option = None;
+ for s in field.help.split('\n') {
+ // Empty line resets our block.
+ if s.is_empty() {
+ end_block(&mut w, block);
+ block = None;
+ w.push('\n');
+ continue;
+ }
+
+ let first = block.is_none();
+ if block.is_none() {
+ if s.starts_with(" ") {
+ block = Some(Block::Code);
+ w.push_str("```\n");
+ } else if starts_with_ignore_case(s, "note:") {
+ block = Some(Block::CalloutNote);
+ w.push_str("\n");
+ } else if starts_with_ignore_case(s, "warning:") {
+ block = Some(Block::CalloutWarning);
+ w.push_str("\n");
+ } else {
+ block = Some(Block::Text);
+ }
+ }
+
+ w.push_str(match block {
+ Some(Block::CalloutNote) if first => &s["note:".len()..],
+ Some(Block::CalloutWarning) if first => &s["warning:".len()..],
+ Some(Block::Code) => s.strip_prefix(" ").unwrap_or(s),
+ _ => s,
+ });
+ w.push('\n');
+ }
+ end_block(&mut w, block);
+ w.push('\n');
+ }
+ w
+}
+
+/// `webgen/main_actions.zig` (`input/helpgen_actions.zig`, markdown with
+/// docs): the keybind action reference.
+#[must_use]
+pub fn actions_mdx() -> String {
+ let mut w = String::from(
+ "---\n\
+title: Keybinding Action Reference\n\
+description: Reference of all Ghostty keybinding actions.\n\
+editOnGithubLink: https://github.com/ghostty-org/ghostty/edit/main/src/input/Binding.zig\n\
+---\n\
+\n\
+This is a reference of all Ghostty keybinding actions.\n\
+\n",
+ );
+
+ // Doc comments are written below all related (consecutive undocumented)
+ // actions, so they are buffered until the next documented action.
+ let mut stream = String::new();
+ for &key in ActionKey::ALL {
+ let name = key.name();
+ if name.starts_with('_') {
+ continue;
+ }
+ let docs = key.help_string();
+ let has_docs = !key.docs().is_empty();
+
+ if has_docs {
+ w.push_str(&stream);
+ w.push('\n');
+ stream.clear();
+ }
+
+ w.push_str("## `");
+ w.push_str(name);
+ w.push_str("`\n");
+
+ if has_docs {
+ let mut iter = docs.split('\n').peekable();
+ while let Some(s) = iter.next() {
+ // If it is the last line and empty, then skip it.
+ if iter.peek().is_none() && s.is_empty() {
+ continue;
+ }
+ stream.push_str(s);
+ stream.push('\n');
+ }
+ }
+ }
+ if !stream.is_empty() {
+ w.push_str(&stream);
+ }
+ w
+}
+
+/// `webgen/main_commands.zig`: the CLI action reference.
+#[must_use]
+pub fn commands_mdx() -> String {
+ let mut w = String::from(
+ "---\n\
+title: Reference\n\
+description: Reference of all Ghostty action subcommands.\n\
+editOnGithubLink: https://github.com/ghostty-org/ghostty/tree/main/src/cli\n\
+---\n\
+Ghostty includes a number of utility actions that can be accessed as subcommands.\n\
+Actions provide utilities to work with config, list keybinds, list fonts, demo themes,\n\
+and debug.\n",
+ );
+
+ for &action in ghostty_cli::Action::ALL {
+ let name = action.name();
+ w.push_str("## ");
+ w.push_str(name);
+ w.push('\n');
+
+ let help = action.help_text();
+ if help.is_empty() {
+ continue;
+ }
+ for s in help.split('\n') {
+ w.push_str(s);
+ w.push('\n');
+ }
+ w.push_str("\n```\n");
+ match action {
+ ghostty_cli::Action::Help | ghostty_cli::Action::Version => {
+ w.push_str("ghostty --");
+ }
+ _ => w.push_str("ghostty +"),
+ }
+ w.push_str(name);
+ w.push_str("\n```\n\n");
+ }
+ w
+}
diff --git a/dist/windows/ghostty.ico b/dist/windows/ghostty.ico
new file mode 100644
index 0000000..1c5afc2
Binary files /dev/null and b/dist/windows/ghostty.ico differ
diff --git a/dist/windows/ghostty.manifest b/dist/windows/ghostty.manifest
new file mode 100644
index 0000000..9a07906
--- /dev/null
+++ b/dist/windows/ghostty.manifest
@@ -0,0 +1,14 @@
+
+
+
+
+ true/pm
+ PerMonitorV2
+
+
+
+
+
+
+
+
diff --git a/dist/windows/ghostty.rc b/dist/windows/ghostty.rc
new file mode 100644
index 0000000..0f3d12f
--- /dev/null
+++ b/dist/windows/ghostty.rc
@@ -0,0 +1,36 @@
+// LANG_NEUTRAL(0), SUBLANG_NEUTRAL(0)
+LANGUAGE 0, 0
+
+#define RT_MANIFEST 1
+RT_MANIFEST 24 "ghostty.manifest"
+
+#define ID_ICON_GHOSTTY 1
+ID_ICON_GHOSTTY ICON "ghostty.ico"
+
+VS_VERSION_INFO VERSIONINFO
+//FILEVERSION VERSION_MAJOR,VERSION_MINOR,VERSION_PATCH,VERSION_COMMIT_HEIGHT
+//PRODUCTVERSION VERSION_MAJOR,VERSION_MINOR,VERSION_PATCH,VERSION_COMMIT_HEIGHT
+//FILEFLAGSMASK VS_FFI_FILEFLAGSMASK
+//FILEFLAGS VER_DBG
+//FILEOS VOS_NT
+//FILETYPE VFT_APP
+//FILESUBTYPE VFT2_UNKNOWN
+BEGIN
+ BLOCK "StringFileInfo"
+ BEGIN
+ BLOCK "040904b0"
+ BEGIN
+ //VALUE "CompanyName", "???"
+ //VALUE "FileDescription", "???"
+ //VALUE "FileVersion", VERSION
+ //VALUE "LegalCopyright", "(C) 2024 ???"
+ VALUE "OriginalFilename", "ghostty.exe"
+ VALUE "ProductName", "Ghostty"
+ //VALUE "ProductVersion", VERSION
+ END
+ END
+ BLOCK "VarFileInfo"
+ BEGIN
+ VALUE "Translation", 0x0409,1200
+ END
+END
diff --git a/docs/rewrite/notes/final-Z4.md b/docs/rewrite/notes/final-Z4.md
new file mode 100644
index 0000000..caf89dd
--- /dev/null
+++ b/docs/rewrite/notes/final-Z4.md
@@ -0,0 +1,27 @@
+# Z4: artifact and distribution gaps
+
+- Webdata (`GhosttyWebdata.zig`): `ghostty-build-data +webgen-{config,actions,commands}` (`src/webdata.rs`);
+ `just webdata`, `just install-webdata PREFIX`. Byte-identical to Zig 0.15.2 output of a copy whose webgen mains
+ gained `stdout.flush()`. Upstream's mains never flush the 2048-byte buffer, so the real Zig files are truncated.
+ Rust emits the complete files. Also `+version` mode (build-info version string).
+- libghostty / vt install: `scripts/install-lib.sh`, `just install-lib PREFIX PROFILE WHAT`; `just install` installs vt
+ (as Zig's default install does). Layout, headers and `libghostty-vt.pc` match the Zig output (soname `.so.0` set via
+ patchelf, symlinks `.so`, `.so.0`, `.so.0.1.0`). libghostty adds `ghostty.h` + `module.modulemap`.
+- XCFramework: `scripts/xcframework.sh` / `just xcframework` (cargo staticlib per target, `lipo -create`,
+ `xcodebuild -create-xcframework -headers crates/ghostty-lib/include`, iOS + simulator slices). Verified only with
+ `DRY_RUN=1`: no macOS tools or SDK here; the darwin cross build was not attempted.
+- App id: `just install` defaults to `--profile production` (ReleaseFast); other profiles get `-debug` id/name, like
+ Zig Debug/ReleaseSafe. Desktop, D-Bus, systemd and metainfo files use it. DESTDIR install checked for production
+ (`com.mitchellh.ghostty`, build mode .ReleaseFast) and release (`-debug`).
+- i18n: `scripts/update-translations.sh` / `just update-translations` (xgettext with the same flags + `--keyword=tr`
+ over blueprints and apprt-gtk Rust sources, py.pot, merge, msgmerge). msgids identical to upstream's .pot.
+ Found and fixed: `surface_child_exited.rs` didn't translate "Command succeeded/failed" (now `tr`). Script accepts
+ an OUT_DIR; po/ was not rewritten in place (location comments would churn).
+- Dist: `scripts/dist.sh`, `just dist` / `just distcheck`: `ghostty-.tar.gz` with `VERSION` and the prebuilt
+ `crates/ghostty-apprt-gtk/dist/ghostty.gresource` (used by apprt-gtk/build.rs outside a git checkout, like Zig's
+ `ghostty_resources.c/h`). Framedata is already committed. As in Zig, VERSION is for packagers (`GHOSTTY_VERSION_STRING`).
+ Uses `git ls-files` (includes uncommitted files), not `git archive HEAD`. distcheck passes.
+- Windows: `dist/windows/{ghostty.rc,ico,manifest}` copied; root `build.rs` uses `embed-resource` plus the GUI subsystem.
+ The root binary can't cargo-check for Windows (GTK/wayland deps), so verified with a scratch crate using the same
+ build.rs for x86_64-pc-windows-gnu: PE subsystem 2, manifest (PerMonitorV2, Common-Controls 6) present.
+- Touched outside lane: apprt-gtk `build.rs` (prebuilt gresource) and `surface_child_exited.rs` (tr).
diff --git a/justfile b/justfile
index b024e4b..e9132a8 100644
--- a/justfile
+++ b/justfile
@@ -89,22 +89,43 @@ locales OUT="target/locale":
# Install bin + share/ghostty (shell-integration, themes, docs) +
# share/terminfo (compiled db, ghostty.terminfo, ghostty.termcap) + shell
# completions, vim/neovim/bat syntax, man pages, translations, desktop file,
-# D-Bus/systemd/metainfo and icons, mirroring Ghostty's GhosttyResources.zig,
-# GhosttyDocs.zig and GhosttyI18n.zig.
+# D-Bus/systemd/metainfo and icons, plus libghostty-vt (lib, headers,
+# libghostty-vt.pc), mirroring Ghostty's GhosttyResources.zig,
+# GhosttyDocs.zig, GhosttyI18n.zig and GhosttyLibVt.zig.
#
-# Usage: `just install [PREFIX] [FLATPAK] [SYSTEM_PACKAGE]`; honours DESTDIR.
+# Usage: `just install [PREFIX] [FLATPAK] [SYSTEM_PACKAGE] [PROFILE]`;
+# honours DESTDIR.
# PREFIX install prefix (default /usr/local)
# FLATPAK "true": use the Flatpak D-Bus template, no systemd unit
# and no 1024x1024 icon (Zig: -Dflatpak)
# SYSTEM_PACKAGE "true": install the systemd user unit to lib/systemd/user
# instead of share/systemd/user (Zig: --system / system
-# package mode)
-install PREFIX="/usr/local" FLATPAK="false" SYSTEM_PACKAGE="false": build-release build-data docs locales
+# package mode)
+# PROFILE cargo profile to build and install (default "production":
+# no debug assertions, the Zig ReleaseFast equivalent).
+# Like Zig's Debug/ReleaseSafe modes, any other profile
+# (dev, release) installs the "-debug" application id
+# (com.mitchellh.ghostty-debug, "Ghostty (Debug)"), which is
+# the id the binary uses at runtime when built with debug
+# assertions.
+install PREFIX="/usr/local" FLATPAK="false" SYSTEM_PACKAGE="false" PROFILE="production": build-data docs locales
#!/usr/bin/env bash
set -euo pipefail
+ cargo build --profile {{ PROFILE }}
+ just install-lib '{{ PREFIX }}' '{{ PROFILE }}' vt
P="${DESTDIR:-}{{ PREFIX }}"
BD=target/release/ghostty-build-data
- install -Dm755 target/release/ghostty "$P/bin/ghostty"
+ dir={{ PROFILE }}
+ if [ "$dir" = dev ]; then dir=debug; fi
+ # GhosttyResources.zig: Debug/ReleaseSafe get the "-debug" id and name.
+ if [ "{{ PROFILE }}" = production ]; then
+ APPID=com.mitchellh.ghostty
+ NAME=Ghostty
+ else
+ APPID=com.mitchellh.ghostty-debug
+ NAME="Ghostty (Debug)"
+ fi
+ install -Dm755 "target/$dir/ghostty" "$P/bin/ghostty"
mkdir -p "$P/share/ghostty" "$P/share/terminfo"
cp -R resources/shell-integration resources/themes "$P/share/ghostty/"
find "$P/share/ghostty/shell-integration" -name '*.md' -delete
@@ -150,20 +171,20 @@ install PREFIX="/usr/local" FLATPAK="false" SYSTEM_PACKAGE="false": build-releas
gen() { # template out
mkdir -p "$(dirname "$2")"
- sed -e 's|@NAME@|Ghostty|g' -e 's|@APPID@|com.mitchellh.ghostty|g' \
+ sed -e "s|@NAME@|$NAME|g" -e "s|@APPID@|$APPID|g" \
-e "s|@GHOSTTY@|{{ PREFIX }}/bin/ghostty|g" "$1" > "$2"
}
- gen dist/linux/app.desktop.in "$P/share/applications/com.mitchellh.ghostty.desktop"
+ gen dist/linux/app.desktop.in "$P/share/applications/$APPID.desktop"
if [ "{{ FLATPAK }}" = "true" ]; then
- gen dist/linux/dbus.service.flatpak.in "$P/share/dbus-1/services/com.mitchellh.ghostty.service"
+ gen dist/linux/dbus.service.flatpak.in "$P/share/dbus-1/services/$APPID.service"
else
- gen dist/linux/dbus.service.in "$P/share/dbus-1/services/com.mitchellh.ghostty.service"
+ gen dist/linux/dbus.service.in "$P/share/dbus-1/services/$APPID.service"
# systemd looks in lib/ for system packages and share/ otherwise.
sysd=share
if [ "{{ SYSTEM_PACKAGE }}" = "true" ]; then sysd=lib; fi
- gen dist/linux/systemd.service.in "$P/$sysd/systemd/user/app-com.mitchellh.ghostty.service"
+ gen dist/linux/systemd.service.in "$P/$sysd/systemd/user/app-$APPID.service"
fi
- gen dist/linux/com.mitchellh.ghostty.metainfo.xml.in "$P/share/metainfo/com.mitchellh.ghostty.metainfo.xml"
+ gen dist/linux/com.mitchellh.ghostty.metainfo.xml.in "$P/share/metainfo/$APPID.metainfo.xml"
install -Dm644 dist/linux/ghostty_dolphin.desktop "$P/share/kio/servicemenus/com.mitchellh.ghostty.desktop"
install -Dm755 dist/linux/ghostty_nautilus.py "$P/share/nautilus-python/extensions/ghostty.py"
ic="$P/share/icons/hicolor"
@@ -173,8 +194,62 @@ install PREFIX="/usr/local" FLATPAK="false" SYSTEM_PACKAGE="false": build-releas
if [ "{{ FLATPAK }}" = "true" ] && [ "${pair%%:*}" = "1024" ]; then continue; fi
install -Dm644 "dist/icons/${pair%%:*}.png" "$ic/${pair##*:}/apps/com.mitchellh.ghostty.png"
done
- echo "installed to $P"
+ echo "installed to $P (application id $APPID)"
# Regenerate resources/terminfo from the terminfo source in ghostty-termio.
terminfo:
scripts/gen-terminfo.sh
+
+# Install libghostty and libghostty-vt below PREFIX (honours DESTDIR):
+# lib/libghostty.{so,a}, include/ghostty.h, include/module.modulemap
+# (GhosttyLib.zig) and lib/libghostty-vt.so{,.0,.0.1.0},
+# include/ghostty/vt*.h, share/pkgconfig/libghostty-vt.pc (GhosttyLibVt.zig).
+# WHAT is one of: all, vt, ghostty.
+install-lib PREFIX="/usr/local" PROFILE="production" WHAT="all":
+ scripts/install-lib.sh '{{ PREFIX }}' '{{ PROFILE }}' '{{ WHAT }}'
+
+# Build libghostty (the embedded C API) as static and shared libraries.
+lib PROFILE="production":
+ cargo build -p ghostty-lib --profile {{ PROFILE }}
+
+# Build GhosttyKit.xcframework with lipo + xcodebuild (macOS only; Zig:
+# GhosttyXCFramework.zig). TARGET is "universal" (macOS arm64+x86_64, iOS,
+# iOS simulator) or "native". Set DRY_RUN=1 to only print the commands.
+xcframework TARGET="universal" PROFILE="production" OUT="macos/GhosttyKit.xcframework":
+ scripts/xcframework.sh '{{ TARGET }}' '{{ PROFILE }}' '{{ OUT }}'
+
+# Generate the website reference data (Zig: GhosttyWebdata.zig): config.mdx,
+# actions.mdx (keybind actions) and commands.mdx (CLI actions) in OUT.
+webdata OUT="target/webdata": build-data
+ #!/usr/bin/env bash
+ set -euo pipefail
+ mkdir -p "{{ OUT }}"
+ for m in config actions commands; do
+ target/release/ghostty-build-data "+webgen-$m" > "{{ OUT }}/$m.mdx"
+ done
+
+# Install the website data to PREFIX/share/ghostty/webdata (Zig:
+# `-Demit-webdata`); honours DESTDIR.
+install-webdata PREFIX="/usr/local": webdata
+ #!/usr/bin/env bash
+ set -euo pipefail
+ for m in config actions commands; do
+ install -Dm644 "target/webdata/$m.mdx" "${DESTDIR:-}{{ PREFIX }}/share/ghostty/webdata/$m.mdx"
+ done
+
+# Regenerate po/com.mitchellh.ghostty.pot (xgettext over the GTK blueprints,
+# the GTK apprt Rust sources and the Nautilus extension) and msgmerge every
+# po/*.po against it (Zig: `zig build update-translations`).
+update-translations:
+ scripts/update-translations.sh
+
+# Create the versioned source tarball target/dist/ghostty-.tar.gz with
+# the embedded VERSION file and the prebuilt dist resources (Zig: `zig build
+# dist`).
+dist:
+ scripts/dist.sh target/dist
+
+# Extract the dist tarball and check that it builds and tests (Zig:
+# `zig build distcheck`).
+distcheck: dist
+ scripts/dist.sh --check
diff --git a/scripts/dist.sh b/scripts/dist.sh
new file mode 100755
index 0000000..e817698
--- /dev/null
+++ b/scripts/dist.sh
@@ -0,0 +1,95 @@
+#!/usr/bin/env bash
+# Create the versioned source tarball (Zig: `zig build dist`,
+# build/GhosttyDist.zig) or check one (`zig build distcheck`).
+#
+# scripts/dist.sh [OUT_DIR] create OUT_DIR/ghostty-.tar.gz
+# scripts/dist.sh --check [TARBALL] extract it and verify it
+#
+# The tarball extracts into `ghostty-/` (expected by debhelper,
+# rpmbuild, ...) and contains, in addition to the checked-in sources:
+# * `VERSION`: the embedded Ghostty version (Zig: the `VERSION` file),
+# * the prebuilt dist resources that are not committed but built here to cut
+# the build-time dependencies of downstream packagers: the compiled GTK
+# GResource bundle `crates/ghostty-apprt-gtk/dist/ghostty.gresource`
+# (Zig: `src/apprt/gtk/ghostty_resources.{c,h}`; picked up by
+# ghostty-apprt-gtk/build.rs outside of a git checkout, replacing
+# blueprint-compiler + glib-compile-resources). The boo frame data
+# (Zig: `framedata.compressed`) is already committed
+# (`crates/ghostty-cli/src/boo_frames.deflate`).
+#
+# In a git checkout the file set is `git ls-files` (tracked files, plus
+# untracked files that are not ignored), otherwise all files except build
+# output. Files marked `export-ignore` in .gitattributes are dropped.
+set -euo pipefail
+
+cd "$(dirname "$0")/.."
+root=$PWD
+
+# Resources that are generated into the tarball (dist path relative to root).
+dist_resources=(crates/ghostty-apprt-gtk/dist/ghostty.gresource)
+
+version() {
+ cargo build -q --release -p ghostty-build-data >&2
+ "${CARGO_TARGET_DIR:-target}/release/ghostty-build-data" +version
+}
+
+if [ "${1:-}" = --check ]; then
+ version=$(version)
+ tarball=${2:-target/dist/ghostty-$version.tar.gz}
+ work=$(mktemp -d)
+ trap 'rm -rf "$work"' EXIT
+ tar -xzf "$tarball" -C "$work"
+ src="$work/ghostty-$version"
+ [ -f "$src/VERSION" ] || { echo "missing VERSION" >&2; exit 1; }
+ [ "$(cat "$src/VERSION")" = "$version" ] || { echo "VERSION mismatch" >&2; exit 1; }
+ for r in "${dist_resources[@]}"; do
+ [ -f "$src/$r" ] || { echo "missing dist resource $r" >&2; exit 1; }
+ done
+ # Check that the extracted tree builds and passes its tests.
+ (cd "$src" && cargo test --offline -q -p ghostty-build-data)
+ echo "distcheck ok: $tarball"
+ exit 0
+fi
+
+out=${1:-target/dist}
+version=$(version)
+name=ghostty-$version
+mkdir -p "$out"
+work=$(mktemp -d)
+trap 'rm -rf "$work"' EXIT
+
+# Generate the dist resources.
+cargo build -q -p ghostty-apprt-gtk
+gres=$(find "${CARGO_TARGET_DIR:-target}/debug/build" -path '*ghostty-apprt-gtk-*/out/ghostty.gresource' -printf '%T@ %p\n' |
+ sort -rn | head -1 | cut -d' ' -f2-)
+[ -n "$gres" ] || { echo "ghostty.gresource was not built" >&2; exit 1; }
+mkdir -p "$work/stage/crates/ghostty-apprt-gtk/dist"
+cp "$gres" "$work/stage/crates/ghostty-apprt-gtk/dist/ghostty.gresource"
+printf '%s' "$version" >"$work/stage/VERSION"
+
+# The file list.
+if [ -e .git ] && command -v git >/dev/null 2>&1; then
+ git ls-files -z --cached --others --exclude-standard
+else
+ find . -path ./target -prune -o -path ./.git -prune -o -path ./.zig-cache -prune \
+ -o -name '*.orig' -prune -o -type f -print0 | sed -z 's|^\./||'
+fi >"$work/files0"
+# export-ignore (git attribute).
+if [ -e .git ] && command -v git >/dev/null 2>&1; then
+ git check-attr -z export-ignore --stdin -z <"$work/files0" |
+ tr '\0' '\n' | awk 'NR%3==1{f=$0} NR%3==0 && $0=="set"{print f}' >"$work/ignored" || true
+ if [ -s "$work/ignored" ]; then
+ tr '\0' '\n' <"$work/files0" | grep -vxFf "$work/ignored" | tr '\n' '\0' >"$work/files0.new"
+ mv "$work/files0.new" "$work/files0"
+ fi
+fi
+# Drop files that no longer exist (deleted but still tracked).
+tr '\0' '\n' <"$work/files0" | while IFS= read -r f; do [ -e "$f" ] && printf '%s\0' "$f"; done >"$work/files"
+
+tar --create --null --files-from "$work/files" \
+ --transform "s,^,$name/," --sort=name --owner=0 --group=0 --numeric-owner \
+ -f "$work/src.tar"
+(cd "$work/stage" && tar --append -f "$work/src.tar" --transform "s,^,$name/," \
+ --owner=0 --group=0 --numeric-owner VERSION crates)
+gzip -n -c "$work/src.tar" >"$out/$name.tar.gz"
+echo "$out/$name.tar.gz"
diff --git a/scripts/install-lib.sh b/scripts/install-lib.sh
new file mode 100755
index 0000000..22aba13
--- /dev/null
+++ b/scripts/install-lib.sh
@@ -0,0 +1,92 @@
+#!/usr/bin/env bash
+# Install libghostty and/or libghostty-vt below a prefix, mirroring
+# build/GhosttyLib.zig (`install`, `installHeader`) and build/GhosttyLibVt.zig.
+#
+# Usage: scripts/install-lib.sh PREFIX PROFILE [all|vt|ghostty]
+# PREFIX install prefix; DESTDIR (env) is prepended for the file copies but
+# not for the paths recorded in the pkg-config file.
+# PROFILE cargo profile whose artifacts are installed (dev -> target/debug).
+# WHAT `vt`: libghostty-vt only (what `zig build` always installs);
+# `ghostty`: libghostty only (Zig: `-Dapp-runtime=none`, non-Darwin);
+# `all` (default): both.
+#
+# libghostty-vt (version 0.1.0):
+# lib/libghostty-vt.so.0.1.0 (+ libghostty-vt.so.0, libghostty-vt.so symlinks;
+# macOS: libghostty-vt.0.1.0.dylib, libghostty-vt.0.dylib, libghostty-vt.dylib)
+# include/ghostty/vt.h and include/ghostty/vt/**.h
+# share/pkgconfig/libghostty-vt.pc
+# libghostty:
+# lib/libghostty.so, lib/libghostty.a (macOS: .dylib), include/ghostty.h,
+# include/module.modulemap
+set -euo pipefail
+
+cd "$(dirname "$0")/.."
+prefix=${1:?usage: install-lib.sh PREFIX PROFILE [all|vt|ghostty]}
+profile=${2:-production}
+what=${3:-all}
+dest="${DESTDIR:-}$prefix"
+dir=$profile
+[ "$profile" = dev ] && dir=debug
+tdir=${CARGO_TARGET_DIR:-target}/$dir
+cargo_args=(--profile "$profile")
+vt_version=0.1.0
+vt_major=${vt_version%%.*}
+
+case $(uname -s) in
+Darwin) dylib=dylib ;;
+*) dylib=so ;;
+esac
+
+install_vt() {
+ cargo build "${cargo_args[@]}" -p ghostty-vt-capi
+ local src="$tdir/libghostty_vt_capi.$dylib"
+ install -d "$dest/lib" "$dest/share/pkgconfig"
+ if [ "$dylib" = so ]; then
+ local real="libghostty-vt.so.$vt_version"
+ install -m755 "$src" "$dest/lib/$real"
+ # The cargo-built soname is the unversioned name; Zig's is `.so.0`.
+ if command -v patchelf >/dev/null 2>&1; then
+ patchelf --set-soname "libghostty-vt.so.$vt_major" "$dest/lib/$real"
+ else
+ echo "patchelf not found: soname stays libghostty-vt.so" >&2
+ fi
+ ln -sf "$real" "$dest/lib/libghostty-vt.so.$vt_major"
+ ln -sf "libghostty-vt.so.$vt_major" "$dest/lib/libghostty-vt.so"
+ else
+ local real="libghostty-vt.$vt_version.dylib"
+ install -m755 "$src" "$dest/lib/$real"
+ if command -v install_name_tool >/dev/null 2>&1; then
+ install_name_tool -id "@rpath/libghostty-vt.$vt_major.dylib" "$dest/lib/$real"
+ fi
+ ln -sf "$real" "$dest/lib/libghostty-vt.$vt_major.dylib"
+ ln -sf "libghostty-vt.$vt_major.dylib" "$dest/lib/libghostty-vt.dylib"
+ fi
+
+ # Headers: include/ghostty/**.h (lib.installHeadersDirectory, .h only).
+ local inc=crates/ghostty-vt-capi/include/ghostty
+ while IFS= read -r h; do
+ install -Dm644 "$inc/$h" "$dest/include/ghostty/$h"
+ done < <(cd "$inc" && find . -type f -name '*.h' | sed 's|^\./||' | LC_ALL=C sort)
+
+ # pkg-config file (no trailing newline, like the Zig `b.fmt` literal).
+ printf 'prefix=%s\nincludedir=${prefix}/include\nlibdir=${prefix}/lib\n\nName: libghostty-vt\nURL: https://github.com/ghostty-org/ghostty\nDescription: Ghostty VT library\nVersion: %s\nCflags: -I${includedir}\nLibs: -L${libdir} -lghostty-vt' \
+ "$prefix" "$vt_version" >"$dest/share/pkgconfig/libghostty-vt.pc"
+ chmod 644 "$dest/share/pkgconfig/libghostty-vt.pc"
+}
+
+install_ghostty() {
+ cargo build "${cargo_args[@]}" -p ghostty-lib
+ install -d "$dest/lib" "$dest/include"
+ install -m755 "$tdir/libghostty.$dylib" "$dest/lib/libghostty.$dylib"
+ install -m644 "$tdir/libghostty.a" "$dest/lib/libghostty.a"
+ install -m644 crates/ghostty-lib/include/ghostty.h "$dest/include/ghostty.h"
+ install -m644 crates/ghostty-lib/include/module.modulemap "$dest/include/module.modulemap"
+}
+
+case $what in
+all) install_vt; install_ghostty ;;
+vt) install_vt ;;
+ghostty) install_ghostty ;;
+*) echo "unknown component: $what" >&2; exit 2 ;;
+esac
+echo "installed libraries to $dest"
diff --git a/scripts/update-translations.sh b/scripts/update-translations.sh
new file mode 100755
index 0000000..ab6d583
--- /dev/null
+++ b/scripts/update-translations.sh
@@ -0,0 +1,74 @@
+#!/usr/bin/env bash
+# Update the translation catalogs in po/ (Zig: `zig build update-translations`,
+# build/GhosttyI18n.zig `createUpdateStep`).
+#
+# 1. xgettext over the GTK blueprint files and the Rust sources of the GTK
+# apprt (Zig: src/apprt/gtk/**/*.zig) -> gtk.pot. The `tr(...)` function
+# of `ghostty_os::i18n` is the Rust counterpart of Zig's `_(...)`.
+# 2. xgettext over the Nautilus extension -> py.pot.
+# 3. xgettext merge (py.pot first, because of xgettext's charset behaviour
+# when merging) -> po/com.mitchellh.ghostty.pot.
+# 4. msgmerge every po/.po against the new template.
+#
+# Usage: scripts/update-translations.sh [OUT_DIR]
+# OUT_DIR where to write the .pot/.po files (default: po/, i.e. in place).
+# The existing catalogs are always read from po/.
+set -euo pipefail
+
+cd "$(dirname "$0")/.."
+domain=com.mitchellh.ghostty
+out=${1:-po}
+mkdir -p "$out"
+tmp=$(mktemp -d)
+trap 'rm -rf "$tmp"' EXIT
+
+gtk=crates/ghostty-apprt-gtk
+
+# Blueprint files in the order of the GResource list (build.rs BLUEPRINTS,
+# Zig: gresource.blueprints). The arguments to xgettext must be relative
+# paths in the repo root so the location comments don't depend on where the
+# checkout lives.
+mapfile -t blueprints < <(
+ sed -n 's/^[[:space:]]*(\([0-9]*\), \([0-9]*\), "\([^"]*\)"),.*/\1 \2 \3/p' "$gtk/build.rs" |
+ while read -r major minor name; do
+ echo "$gtk/resources/ui/$major.$minor/$name.blp"
+ done
+)
+# Rust sources of the GTK apprt, sorted for a deterministic order.
+mapfile -t sources < <(cd . && find "$gtk/src" -type f -name '*.rs' | LC_ALL=C sort)
+
+xgettext \
+ --language=C \
+ --from-code=UTF-8 \
+ --keyword=tr \
+ --keyword=_ \
+ --keyword=C_:1c,2 \
+ -o "$tmp/gtk.pot" \
+ "${blueprints[@]}" "${sources[@]}"
+
+xgettext \
+ --language=Python \
+ --from-code=UTF-8 \
+ -o "$tmp/py.pot" \
+ dist/linux/ghostty_nautilus.py
+
+xgettext \
+ --add-comments=Translators \
+ --package-name="$domain" \
+ --msgid-bugs-address=m@mitchellh.com \
+ '--copyright-holder="Mitchell Hashimoto, Ghostty contributors"' \
+ -o - \
+ "$tmp/py.pot" "$tmp/gtk.pot" >"$tmp/$domain.pot"
+
+for po in po/*.po; do
+ lang=$(basename "$po" .po)
+ msgmerge --quiet --no-fuzzy-matching "$po" "$tmp/$domain.pot" >"$tmp/$lang.po"
+done
+
+# Only write once everything succeeded (Zig: UpdateSourceFiles).
+cp "$tmp/$domain.pot" "$out/$domain.pot"
+for po in po/*.po; do
+ lang=$(basename "$po" .po)
+ cp "$tmp/$lang.po" "$out/$lang.po"
+done
+echo "updated $out/$domain.pot and $(ls po/*.po | wc -l) catalogs"
diff --git a/scripts/xcframework.sh b/scripts/xcframework.sh
new file mode 100755
index 0000000..04db917
--- /dev/null
+++ b/scripts/xcframework.sh
@@ -0,0 +1,83 @@
+#!/usr/bin/env bash
+# Build GhosttyKit.xcframework (macOS only; mirrors build/GhosttyLib.zig
+# `initMacOSUniversal`/`initStatic`, build/GhosttyXCFramework.zig,
+# build/LipoStep.zig and build/XCFrameworkStep.zig).
+#
+# Usage: scripts/xcframework.sh [universal|native] [PROFILE] [OUT]
+# universal (default) a universal (arm64 + x86_64, via lipo) macOS static
+# library, an iOS device library and an iOS simulator library.
+# native only the static library for the host macOS architecture.
+# PROFILE cargo profile (default production)
+# OUT output path (default macos/GhosttyKit.xcframework, like Zig)
+#
+# Each slice is `libghostty.a` (the Rust staticlib already bundles every
+# dependency, so there is no libtool step) plus the `include/` directory with
+# ghostty.h and module.modulemap. Static libraries have no dSYM.
+#
+# DRY_RUN=1 prints the commands instead of running them (usable on any host).
+set -euo pipefail
+
+cd "$(dirname "$0")/.."
+target=${1:-universal}
+profile=${2:-production}
+out=${3:-macos/GhosttyKit.xcframework}
+dir=$profile
+[ "$profile" = dev ] && dir=debug
+tdir=${CARGO_TARGET_DIR:-target}
+
+# Config.zig osVersionMin: macOS 13, iOS 17.
+export MACOSX_DEPLOYMENT_TARGET=13.0
+export IPHONEOS_DEPLOYMENT_TARGET=17.0
+
+run() {
+ if [ "${DRY_RUN:-}" = 1 ]; then
+ printf '+'
+ printf ' %q' "$@"
+ printf '\n'
+ else
+ "$@"
+ fi
+}
+
+# Build only the static library for a Rust target triple (the crate also
+# declares a cdylib, which needs no linking for the xcframework).
+build() {
+ run cargo rustc --profile "$profile" -p ghostty-lib --lib --crate-type staticlib --target "$1"
+}
+lib() { echo "$tdir/$1/$dir/libghostty.a"; }
+
+host_arch=$(uname -m)
+case $host_arch in
+arm64 | aarch64) native=aarch64-apple-darwin ;;
+*) native=x86_64-apple-darwin ;;
+esac
+
+args=()
+case $target in
+universal)
+ build aarch64-apple-darwin
+ build x86_64-apple-darwin
+ build aarch64-apple-ios
+ build aarch64-apple-ios-sim
+
+ universal_dir="$tdir/universal-apple-darwin/$dir"
+ run mkdir -p "$universal_dir"
+ run lipo -create -output "$universal_dir/libghostty.a" \
+ "$(lib aarch64-apple-darwin)" "$(lib x86_64-apple-darwin)"
+ args+=(-library "$universal_dir/libghostty.a" -headers crates/ghostty-lib/include)
+ args+=(-library "$(lib aarch64-apple-ios)" -headers crates/ghostty-lib/include)
+ args+=(-library "$(lib aarch64-apple-ios-sim)" -headers crates/ghostty-lib/include)
+ ;;
+native)
+ build "$native"
+ args+=(-library "$(lib "$native")" -headers crates/ghostty-lib/include)
+ ;;
+*)
+ echo "usage: $0 [universal|native] [PROFILE] [OUT]" >&2
+ exit 2
+ ;;
+esac
+
+# The output path is static, so delete the old framework first.
+run rm -rf "$out"
+run xcodebuild -create-xcframework "${args[@]}" -output "$out"