feat(dist): generate completions, editor syntax, man pages, translations

Ports Ghostty's build-data helpers (main_build_data.zig, src/extra/*,
build/mdgen) as the ghostty-build-data binary: bash/zsh/fish
completions, Vim syntax/ftdetect/ftplugin/compiler, Sublime syntax (also
used by bat), terminfo source and ghostty(1)/ghostty(5) markdown. With
the version set to the installed 1.3.1-arch2, all generated files are
byte-identical to the installed Ghostty's.

- `just docs` renders man pages and HTML with pandoc.
- po/: Ghostty's 29 translation catalogs; `just locales` compiles them
  into the com.mitchellh.ghostty gettext domain.
- `just install PREFIX FLATPAK SYSTEM_PACKAGE` installs everything incl.
  terminfo text/termcap outputs and mirrors GhosttyResources.zig install
  modes (Flatpak D-Bus template without systemd; lib/systemd/user for
  system packages).
- Build metadata (version, tip/stable channel, commit, build mode) is
  derived at build time like build_config.zig; GTK/libadwaita
  compile-time versions come from pkg-config instead of constants.

Test Plan:
- cargo run -p ghostty-build-data -- +bash | diff - \
  /usr/share/bash-completion/completions/ghostty (same for zsh/fish/vim)
- just install /tmp/prefix
This commit is contained in:
ddidderr committed 2026-10-12 04:39:14 +02:00
1 parent 79486601b5
commit 2dc1c6028d
56 files changed
+12614 -16

No files matched your search

+129
View File
@@ -0,0 +1,129 @@
//! Bash completions (port of `extra/bash.zig`).
use std::fmt::Write as _;
use ghostty_config::cli_args::FieldKind;
use crate::chunks_bash::{C0, C1, C2, C3, C4, C5, C6};
use crate::{actions, config_fields, join_names, join_packed};
const COMPGEN_PREFIX: &str = "_compreply compgen -W \"";
const COMPGEN_SUFFIX: &str = "\" -- \"$cur\"; _add_spaces ;;";
fn is_flag(kind: FieldKind) -> bool {
matches!(kind, FieldKind::Bool | FieldKind::OptionalBool)
}
#[must_use]
pub fn completions() -> String {
let pad1 = " ";
let pad2 = " ";
let pad3 = " ";
let pad4 = " ";
let pad5 = " ";
let mut w = String::new();
let fields = config_fields();
w.push_str(C0);
for f in &fields {
if is_flag(f.kind) {
let _ = writeln!(w, "{pad2}config+=\" '--{} '\"", f.name);
} else {
let _ = writeln!(w, "{pad2}config+=\" --{}=\"", f.name);
}
}
w.push_str(C1);
for f in &fields {
let _ = write!(w, "{pad3}--{}) ", f.name);
if f.name.starts_with("font-family") {
w.push_str("_fonts ;;");
} else if f.name == "theme" {
w.push_str("_themes ;;");
} else if f.name == "working-directory" {
w.push_str("_dirs ;;");
} else if f.kind == FieldKind::RepeatablePath {
w.push_str("_files ;;");
} else {
match f.kind {
FieldKind::Enum(names) => {
let _ = write!(w, "{COMPGEN_PREFIX}{}{COMPGEN_SUFFIX}", join_names(names));
}
FieldKind::Packed(names) => {
let _ = write!(w, "{COMPGEN_PREFIX}{}{COMPGEN_SUFFIX}", join_packed(names));
}
_ => w.push_str("return ;;"),
}
}
w.push('\n');
}
w.push_str(C2);
let acts = actions();
for a in acts {
let opts = a.option_fields();
if opts.is_empty() {
continue;
}
let bash_name = a.name().replace('-', "_");
let _ = write!(w, "{pad2}local {bash_name}=\"");
for (count, (name, kind)) in opts.iter().enumerate() {
if count > 0 {
w.push(' ');
}
if is_flag(*kind) {
let _ = write!(w, "'--{name} '");
} else {
let _ = write!(w, "--{name}=");
}
}
w.push_str(" --help\"\n");
}
w.push_str(C3);
for a in acts {
let opts = a.option_fields();
if opts.is_empty() {
continue;
}
let bash_name = a.name().replace('-', "_");
let _ = writeln!(w, "{pad3}+{})", a.name());
let _ = writeln!(w, "{pad4}case $prev in");
for (name, kind) in &opts {
let _ = write!(w, "{pad5}--{name}) ");
match kind {
FieldKind::Bool => w.push_str("return ;;"),
FieldKind::Enum(names) | FieldKind::OptionalEnum(names) => {
let _ = write!(w, "{COMPGEN_PREFIX}{}{COMPGEN_SUFFIX}", join_names(names));
}
FieldKind::OptionalBool | FieldKind::OptionalOther => {
if *name == "config-file" {
w.push_str("return ;;");
} else {
w.push_str("return;;");
}
}
_ => {
if *name == "config-file" {
w.push_str("_files ;;");
} else {
w.push_str("return;;");
}
}
}
w.push('\n');
}
let _ = writeln!(
w,
"{pad5}*) _compreply compgen -W \"${bash_name}\" -- \"$cur\" ;;"
);
w.push_str(C4);
}
w.push_str(C5);
for a in acts {
let _ = writeln!(w, "{pad1}topLevel+=\" +{}\"", a.name());
}
w.push_str(C6);
w
}
@@ -0,0 +1,140 @@
// Generated from the Zig multiline strings of extra/bash.zig. Do not edit.
pub const C0: &str = r####"_ghostty() {
# compat: mapfile -t COMPREPLY < <( "$@" )
_compreply() {
COMPREPLY=()
while IFS='' read -r line; do COMPREPLY+=("$line"); done < <( "$@" )
}
# -o nospace requires we add back a space when a completion is finished
# and not part of a --key= completion
_add_spaces() {
for idx in "${!COMPREPLY[@]}"; do
[ -n "${COMPREPLY[idx]}" ] && COMPREPLY[idx]="${COMPREPLY[idx]} ";
done
}
_fonts() {
local IFS=$'\n'
COMPREPLY=()
while read -r line; do COMPREPLY+=("$line"); done < <( compgen -P '"' -S '"' -W "$($ghostty +list-fonts | grep '^[A-Z]' )" -- "$cur")
}
_themes() {
local IFS=$'\n'
COMPREPLY=()
while read -r line; do COMPREPLY+=("$line"); done < <( compgen -P '"' -S '"' -W "$($ghostty +list-themes | sed -E 's/^(.*) \(.*$/\1/')" -- "$cur")
}
_files() {
_compreply compgen -o filenames -f -- "$cur"
for i in "${!COMPREPLY[@]}"; do
if [[ -d "${COMPREPLY[i]}" ]]; then
COMPREPLY[i]="${COMPREPLY[i]}/";
fi
if [[ -f "${COMPREPLY[i]}" ]]; then
COMPREPLY[i]="${COMPREPLY[i]} ";
fi
done
}
_dirs() {
_compreply compgen -o dirnames -d -- "$cur"
for i in "${!COMPREPLY[@]}"; do
if [[ -d "${COMPREPLY[i]}" ]]; then
COMPREPLY[i]="${COMPREPLY[i]}/";
fi
done
if [[ "${#COMPREPLY[@]}" == 0 && -d "$cur" ]]; then
COMPREPLY=( "$cur " )
fi
}
_handle_config() {
local config="--help"
config+=" --version"
"####;
pub const C1: &str = r####"
case "$prev" in
"####;
pub const C2: &str = r####" *) _compreply compgen -W "$config" -- "$cur" ;;
esac
return 0
}
_handle_actions() {
"####;
pub const C3: &str = r####"
case "${COMP_WORDS[1]}" in
"####;
pub const C4: &str = r####" esac
;;
"####;
pub const C5: &str = r####" *) _compreply compgen -W "--help" -- "$cur" ;;
esac
return 0
}
# begin main logic
local topLevel="-e"
topLevel+=" --help"
topLevel+=" --version"
"####;
pub const C6: &str = r####"
local cur=""; local prev=""; local prevWasEq=false; COMPREPLY=()
local ghostty="$1"
# script assumes default COMP_WORDBREAKS of roughly $' \t\n"\'><=;|&(:'
# if = is missing this script will degrade to matching on keys only.
# eg: --key=
# this can be improved if needed see: https://github.com/ghostty-org/ghostty/discussions/2994
if [ "$2" = "=" ]; then cur=""
else cur="$2"
fi
if [ "$3" = "=" ]; then prev="${COMP_WORDS[COMP_CWORD-2]}"; prevWasEq=true;
else prev="${COMP_WORDS[COMP_CWORD-1]}"
fi
# current completion is double quoted add a space so the cursor progresses
if [[ "$2" == \"*\" ]]; then
COMPREPLY=( "$cur " );
return;
fi
case "$COMP_CWORD" in
1)
case "${COMP_WORDS[1]}" in
-e | --help | --version) return 0 ;;
--*) _handle_config ;;
*) _compreply compgen -W "${topLevel}" -- "$cur"; _add_spaces ;;
esac
;;
*)
case "$prev" in
-e | --help | --version) return 0 ;;
*)
if [[ "=" != "${COMP_WORDS[COMP_CWORD]}" && $prevWasEq != true ]]; then
# must be completing with a space after the key eg: '--<key> '
# clear out prev so we don't run any of the key specific completions
prev=""
fi
case "${COMP_WORDS[1]}" in
--*) _handle_config ;;
+*) _handle_actions ;;
esac
;;
esac
;;
esac
return 0
}
complete -o nospace -o bashdefault -F _ghostty ghostty
"####;
@@ -0,0 +1 @@
// Generated from the Zig multiline strings of extra/fish.zig. Do not edit.
@@ -0,0 +1,20 @@
// Generated from the Zig multiline strings of extra/sublime.zig. Do not edit.
pub const C0: &str = r####"%YAML 1.2
---
# See http://www.sublimetext.com/docs/syntax.html
name: Ghostty Config
file_extensions:
- ghostty
scope: source.ghostty
contexts:
main:
# Comments
- match: '^\s*#.*$'
scope: comment.line.number-sign.ghostty
# Keywords
- match: '\b("####;
pub const C1: &str = r####")\b'
scope: keyword.other.ghostty
"####;
@@ -0,0 +1,74 @@
// Generated from the Zig multiline strings of extra/vim.zig. Do not edit.
pub const C0: &str = r####"" Vim filetype detect file
" Language: Ghostty config file
" Maintainer: Ghostty <https://github.com/ghostty-org/ghostty>
"
" THIS FILE IS AUTO-GENERATED
au BufRead,BufNewFile */ghostty/config,*/*.ghostty/config,*/ghostty/themes/*,*.ghostty setf ghostty
"####;
pub const C1: &str = r####"" Vim filetype plugin file
" Language: Ghostty config file
" Maintainer: Ghostty <https://github.com/ghostty-org/ghostty>
"
" THIS FILE IS AUTO-GENERATED
if exists('b:did_ftplugin')
finish
endif
let b:did_ftplugin = 1
setlocal commentstring=#\ %s
setlocal iskeyword+=-
" Use syntax keywords for completion
setlocal omnifunc=syntaxcomplete#Complete
let b:undo_ftplugin = 'setl cms< isk< ofu<'
if !exists('current_compiler')
compiler ghostty
let b:undo_ftplugin .= " makeprg< errorformat<"
endif
"####;
pub const C2: &str = r####"" Vim compiler file
" Language: Ghostty config file
" Maintainer: Ghostty <https://github.com/ghostty-org/ghostty>
"
" THIS FILE IS AUTO-GENERATED
if exists("current_compiler")
finish
endif
let current_compiler = "ghostty"
CompilerSet makeprg=ghostty\ +validate-config\ --config-file=%:S
CompilerSet errorformat=%f:%l:%m,%m
"####;
pub const C3: &str = r####"" Vim syntax file
" Language: Ghostty config file
" Maintainer: Ghostty <https://github.com/ghostty-org/ghostty>
"
" THIS FILE IS AUTO-GENERATED
if exists('b:current_syntax')
finish
endif
let b:current_syntax = 'ghostty'
let s:cpo_save = &cpo
set cpo&vim
syn iskeyword @,48-57,-
syn keyword ghosttyConfigKeyword"####;
pub const C4: &str = r####"
syn match ghosttyConfigComment /^\s*#.*/ contains=@Spell
hi def link ghosttyConfigComment Comment
hi def link ghosttyConfigKeyword Keyword
let &cpo = s:cpo_save
unlet s:cpo_save
"####;
@@ -0,0 +1,58 @@
// Generated from the Zig multiline strings of extra/zsh.zig. Do not edit.
pub const C0: &str = r####"#compdef ghostty
_fonts () {
local font_list=$(ghostty +list-fonts | grep -Z '^[A-Z]')
local fonts=(${(f)font_list})
_describe -t fonts 'fonts' fonts
}
_themes() {
local theme_list=$(ghostty +list-themes | sed -E 's/^(.*) \(.*$/\1/')
local themes=(${(f)theme_list})
_describe -t themes 'themes' themes
}
"####;
pub const C1: &str = r####"_ghostty() {
typeset -A opt_args
local context state line
local opt=('-e' '--help' '--version')
_arguments -C \
'1:actions:->actions' \
'*:: :->rest' \
if [[ "$line[1]" == "--help" || "$line[1]" == "--version" || "$line[1]" == "-e" ]]; then
return
fi
if [[ "$line[1]" == -* ]]; then
_config
return
fi
case "$state" in
(actions)
local actions; actions=(
"####;
pub const C2: &str = r####" )
_describe '' opt
_describe -t action 'action' actions
;;
(rest)
if [[ "$line[2]" == "--help" ]]; then
return
fi
local help=('--help')
_describe '' help
case $line[1] in
"####;
pub const C3: &str = r####" esac
;;
esac
}
_ghostty "$@"
"####;
+146
View File
@@ -0,0 +1,146 @@
//! Fish completions (port of `extra/fish.zig`).
use std::fmt::Write as _;
use ghostty_cli::Action;
use ghostty_config::cli_args::FieldKind;
use crate::{actions, config_fields, join_names, join_packed};
#[must_use]
pub fn completions() -> String {
let mut w = String::new();
let acts = actions();
let plus = |sep: &str| {
acts.iter()
.map(|a| format!("+{}", a.name()))
.collect::<Vec<_>>()
.join(sep)
};
let _ = writeln!(w, "set -l commands \"{}\"", plus(" "));
w.push_str("complete -c ghostty -f\n");
w.push_str("complete -c ghostty -s e -l help -f\n");
w.push_str(
"complete -c ghostty -n \"not __fish_seen_subcommand_from $commands\" -l version -f\n",
);
for f in config_fields() {
w.push_str("complete -c ghostty -n \"not __fish_seen_subcommand_from $commands\" -l ");
w.push_str(f.name);
w.push_str(if f.kind == FieldKind::Bool {
" "
} else {
" -r"
});
if f.name.starts_with("font-family") {
w.push_str(" -f -a \"(ghostty +list-fonts | grep '^[A-Z]')\"");
} else if f.name == "theme" {
w.push_str(" -f -a \"(ghostty +list-themes | sed -E 's/^(.*) \\(.*\\$/\\1/')\"");
} else if f.name == "working-directory" {
w.push_str(" -f -k -a \"(__fish_complete_directories)\"");
} else {
w.push_str(if f.kind == FieldKind::RepeatablePath {
" -F"
} else {
" -f"
});
match f.kind {
FieldKind::Enum(names) => {
let _ = write!(w, " -a \"{}\"", join_names(names));
}
FieldKind::Packed(names) => {
let _ = write!(w, " -a \"{}\"", join_packed(names));
}
_ => {}
}
}
if !f.help.is_empty() {
let _ = write!(w, " -d \"{}\"", get_description(&f.help));
}
w.push('\n');
}
let _ = writeln!(
w,
"complete -c ghostty -n \"string match -q -- '+*' (commandline -pt)\" -f -a \"{}\"",
plus(" ")
);
for a in acts {
if matches!(a, Action::Help | Action::Version) {
continue;
}
for (name, kind) in a.option_fields() {
let _ = write!(
w,
"complete -c ghostty -n \"__fish_seen_subcommand_from +{}\" -l {name}",
a.name()
);
if kind != FieldKind::Bool {
w.push_str(" -r");
}
w.push_str(if name == "config-file" { " -F" } else { " -f" });
match kind {
FieldKind::Enum(names) | FieldKind::OptionalEnum(names) => {
let _ = write!(w, " -a \"{}\"", join_names(names));
}
_ => {}
}
w.push('\n');
}
}
w
}
/// First sentence of the help text on one line, with quotes escaped
/// (Zig: `getDescription`).
#[must_use]
pub fn get_description(help: &str) -> String {
let b = help.as_bytes();
let mut out = Vec::with_capacity(b.len() * 2);
let mut prev_was_space = false;
for (i, &c) in b.iter().enumerate() {
match c {
b'.' => {
out.push(b'.');
if i + 1 >= b.len() {
break;
}
let next = b[i + 1];
if next == b' ' || next == b'\n' {
break;
}
}
b'\n' => {
if !prev_was_space && !out.is_empty() {
out.push(b' ');
prev_was_space = true;
}
}
b'"' => {
out.extend_from_slice(b"\\\"");
prev_was_space = false;
}
_ => {
out.push(c);
prev_was_space = c == b' ';
}
}
}
String::from_utf8_lossy(&out).into_owned()
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn get_description_test() {
let input = "First sentence with \"quotes\"\nand newlines. Second sentence.";
assert_eq!(
get_description(input),
"First sentence with \\\"quotes\\\" and newlines."
);
}
}
+200
View File
@@ -0,0 +1,200 @@
//! Build-data generators (port of `src/main_build_data.zig`, `src/extra/*.zig`
//! and `src/build/mdgen/*`).
//!
//! The binary prints the requested artifact to stdout; the `just install`
//! recipe (mirroring `GhosttyResources.zig` / `GhosttyDocs.zig`) captures it.
#![allow(clippy::too_many_lines)]
#![cfg_attr(test, allow(clippy::expect_used))]
use ghostty_cli::Action;
use ghostty_config::cli_args::FieldKind;
use ghostty_config::config::Config;
use ghostty_config::macros::ConfigStruct;
pub mod bash;
pub mod fish;
pub mod mdgen;
pub mod sublime;
pub mod vim;
pub mod zsh;
#[allow(clippy::needless_raw_string_hashes)]
mod chunks_bash;
#[allow(clippy::needless_raw_string_hashes)]
mod chunks_sublime;
#[allow(clippy::needless_raw_string_hashes)]
mod chunks_vim;
#[allow(clippy::needless_raw_string_hashes)]
mod chunks_zsh;
/// The modes of the generator (Zig: `main_build_data.Action`, plus the two
/// `mdgen` executables which are separate binaries upstream).
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum Mode {
Bash,
Fish,
Zsh,
Sublime,
VimSyntax,
VimFtdetect,
VimFtplugin,
VimCompiler,
Terminfo,
/// `ghostty(1)` man page markdown (Zig: `mdgen_ghostty_1`).
Man1,
/// `ghostty(5)` man page markdown (Zig: `mdgen_ghostty_5`).
Man5,
}
impl Mode {
pub const ALL: &'static [(&'static str, Mode)] = &[
("bash", Mode::Bash),
("fish", Mode::Fish),
("zsh", Mode::Zsh),
("sublime", Mode::Sublime),
("vim-syntax", Mode::VimSyntax),
("vim-ftdetect", Mode::VimFtdetect),
("vim-ftplugin", Mode::VimFtplugin),
("vim-compiler", Mode::VimCompiler),
("terminfo", Mode::Terminfo),
("mdgen-ghostty-1", Mode::Man1),
("mdgen-ghostty-5", Mode::Man5),
];
/// Parse `+name` (Zig action syntax) or a bare `name`.
#[must_use]
pub fn parse(arg: &str) -> Option<Mode> {
let name = arg.strip_prefix('+').unwrap_or(arg);
Mode::ALL.iter().find(|(n, _)| *n == name).map(|(_, m)| *m)
}
/// Generate the output of this mode.
#[must_use]
pub fn generate(self) -> String {
match self {
Mode::Bash => bash::completions(),
Mode::Fish => fish::completions(),
Mode::Zsh => zsh::completions(),
Mode::Sublime => sublime::syntax(),
Mode::VimSyntax => vim::syntax(),
Mode::VimFtdetect => vim::FTDETECT.to_string(),
Mode::VimFtplugin => vim::FTPLUGIN.to_string(),
Mode::VimCompiler => vim::COMPILER.to_string(),
Mode::Terminfo => ghostty_termio::terminfo::ghostty.encode_to_string(),
Mode::Man1 => mdgen::ghostty_1(),
Mode::Man5 => mdgen::ghostty_5(),
}
}
}
/// A public (non-`_`) config field.
pub(crate) struct CfgField {
pub name: &'static str,
pub kind: FieldKind,
pub help: String,
}
/// The non-internal fields of `Config` in declaration order.
pub(crate) fn config_fields() -> Vec<CfgField> {
Config::FIELDS
.iter()
.filter(|f| !f.name.starts_with('_'))
.map(|f| CfgField {
name: f.name,
kind: f.kind,
help: f.help(),
})
.collect()
}
/// Space separated enum/packed names for a completion word list.
pub(crate) fn join_names(names: &[&str]) -> String {
names.join(" ")
}
/// Space separated `name no-name` pairs (packed structs).
pub(crate) fn join_packed(names: &[&str]) -> String {
names
.iter()
.map(|n| format!("{n} no-{n}"))
.collect::<Vec<_>>()
.join(" ")
}
/// All CLI actions in declaration order.
pub(crate) fn actions() -> &'static [Action] {
Action::ALL
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn every_mode_generates_output() {
for (name, mode) in Mode::ALL {
assert!(!mode.generate().is_empty(), "{name}");
}
assert_eq!(Mode::parse("+bash"), Some(Mode::Bash));
assert_eq!(Mode::parse("vim-syntax"), Some(Mode::VimSyntax));
assert_eq!(Mode::parse("nope"), None);
}
#[test]
fn completions_cover_config_and_actions() {
let bash = bash::completions();
assert!(bash.starts_with("_ghostty() {"));
assert!(bash.contains("--font-family) _fonts ;;"));
assert!(bash.contains("config+=\" '--maximize '\""));
assert!(bash.contains("local list_themes=\""));
assert!(bash.contains("topLevel+=\" +new-window\""));
let zsh = zsh::completions();
assert!(zsh.starts_with("#compdef ghostty"));
assert!(zsh.contains("\"--working-directory=-:::{_files -/}\""));
let fish = fish::completions();
assert!(fish.starts_with("set -l commands \"+version +help"));
assert!(fish.contains(" -l theme -r -f -a \"(ghostty +list-themes"));
}
#[test]
fn editor_syntax_lists_config_keys() {
assert!(vim::syntax().contains("\n\t\\ font-family"));
assert!(sublime::syntax().contains("|font-family|"));
assert!(!sublime::syntax().contains("|_"));
}
#[test]
fn man_pages_have_sections() {
let one = mdgen::ghostty_1();
assert!(one.starts_with("% GHOSTTY(1) Version "));
assert!(!one.contains("@@VERSION@@"));
assert!(one.contains("**`+list-fonts`**\n\n: "));
assert!(one.contains("**`--font-family`**"));
let five = mdgen::ghostty_5();
assert!(five.contains("# KEYBIND ACTIONS"));
assert!(five.contains("**`new_split`**"));
}
/// Every supported locale has a `.po` catalog and vice versa
/// (Zig: `GhosttyI18n.zig` iterates `os/i18n_locales.zig`).
#[test]
fn po_catalogs_match_locales() {
let dir = std::path::Path::new(env!("CARGO_MANIFEST_DIR")).join("../../po");
let mut found: Vec<String> = std::fs::read_dir(dir)
.expect("po dir")
.filter_map(Result::ok)
.filter_map(|e| {
let n = e.file_name().into_string().ok()?;
n.strip_suffix(".po").map(str::to_owned)
})
.collect();
found.sort();
let mut want: Vec<String> = ghostty_os::i18n_locales::LOCALES
.iter()
.map(|s| (*s).to_owned())
.collect();
want.sort();
assert_eq!(found, want);
}
}
+29
View File
@@ -0,0 +1,29 @@
//! `ghostty-build-data`: prints generated build data to stdout
//! (port of `src/main_build_data.zig`).
//!
//! Usage: `ghostty-build-data +bash|+zsh|+fish|+sublime|+vim-syntax|
//! +vim-ftdetect|+vim-ftplugin|+vim-compiler|+terminfo|+mdgen-ghostty-1|
//! +mdgen-ghostty-5`
use std::io::Write;
use std::process::ExitCode;
use ghostty_build_data::Mode;
fn main() -> ExitCode {
let Some(mode) = std::env::args().nth(1).and_then(|a| Mode::parse(&a)) else {
let names: Vec<_> = Mode::ALL.iter().map(|(n, _)| format!("+{n}")).collect();
eprintln!("usage: ghostty-build-data <{}>", names.join("|"));
return ExitCode::FAILURE;
};
let out = mode.generate();
let mut stdout = std::io::stdout().lock();
if stdout
.write_all(out.as_bytes())
.and_then(|()| stdout.flush())
.is_err()
{
return ExitCode::FAILURE;
}
ExitCode::SUCCESS
}
+94
View File
@@ -0,0 +1,94 @@
//! Man page markdown generator (port of `build/mdgen/{mdgen,main_ghostty_1,
//! main_ghostty_5}.zig`). The markdown is converted to roff/html with pandoc
//! by `just docs` / `just install` (Zig: `GhosttyDocs.zig`).
use ghostty_cli::build_info;
use ghostty_input::binding::ActionKey;
use crate::config_fields;
const GHOSTTY_1_HEADER: &str = include_str!("mdgen/ghostty_1_header.md");
const GHOSTTY_1_FOOTER: &str = include_str!("mdgen/ghostty_1_footer.md");
const GHOSTTY_5_HEADER: &str = include_str!("mdgen/ghostty_5_header.md");
const GHOSTTY_5_FOOTER: &str = include_str!("mdgen/ghostty_5_footer.md");
/// Replaces `@@VERSION@@` with the build's version string.
#[must_use]
pub fn substitute(input: &str) -> String {
input.replace("@@VERSION@@", &build_info::get().version_string)
}
/// Write a definition-list body: first line `: `, others indented.
fn write_help(w: &mut String, help: &str) {
for (i, line) in help.split('\n').enumerate() {
w.push_str(if i == 0 { ": " } else { " " });
w.push_str(line);
w.push('\n');
}
w.push_str("\n\n");
}
pub fn gen_config(w: &mut String, cli: bool) {
w.push_str("\n# CONFIGURATION OPTIONS\n\n");
for f in config_fields() {
w.push_str("**`");
if cli {
w.push_str("--");
}
w.push_str(f.name);
w.push_str("`**\n\n");
if !f.help.is_empty() {
write_help(w, &f.help);
}
}
}
pub fn gen_actions(w: &mut String) {
w.push_str("\n# COMMAND LINE ACTIONS\n\n");
for &action in ghostty_cli::Action::ALL {
match action {
ghostty_cli::Action::Help => w.push_str("**`--help`**\n\n"),
ghostty_cli::Action::Version => w.push_str("**`--version`**\n\n"),
_ => {
w.push_str("**`+");
w.push_str(action.name());
w.push_str("`**\n\n");
}
}
write_help(w, action.help_text());
}
}
pub fn gen_keybind_actions(w: &mut String) {
w.push_str("\n# KEYBIND ACTIONS\n\n");
for &key in ActionKey::ALL {
let name = key.name();
if name.starts_with('_') {
continue;
}
w.push_str("**`");
w.push_str(name);
w.push_str("`**\n\n");
write_help(w, key.help_string());
}
}
/// `ghostty(1)` markdown.
#[must_use]
pub fn ghostty_1() -> String {
let mut w = substitute(GHOSTTY_1_HEADER);
gen_actions(&mut w);
gen_config(&mut w, true);
w.push_str(&substitute(GHOSTTY_1_FOOTER));
w
}
/// `ghostty(5)` markdown.
#[must_use]
pub fn ghostty_5() -> String {
let mut w = substitute(GHOSTTY_5_HEADER);
gen_config(&mut w, false);
gen_keybind_actions(&mut w);
w.push_str(&substitute(GHOSTTY_5_FOOTER));
w
}
@@ -0,0 +1,64 @@
# FILES
_\$XDG_CONFIG_HOME/ghostty/config.ghostty_
: Location of the default configuration file.
_\$HOME/Library/Application Support/com.mitchellh.ghostty/config.ghostty_
: **On macOS**, location of the default configuration file. This location takes
precedence over the XDG environment locations.
_\$LOCALAPPDATA/ghostty/config.ghostty_
: **On Windows**, if _\$XDG_CONFIG_HOME_ is not set, _\$LOCALAPPDATA_ will be searched
for configuration files.
# ENVIRONMENT
**TERM**
: Defaults to `xterm-ghostty`. Can be configured with the `term` configuration option.
**GHOSTTY_RESOURCES_DIR**
: Where the Ghostty resources can be found.
**XDG_CONFIG_HOME**
: Default location for configuration files.
**$HOME/Library/Application Support/com.mitchellh.ghostty**
: **MACOS ONLY** default location for configuration files. This location takes
precedence over the XDG environment locations.
**LOCALAPPDATA**
: **WINDOWS ONLY:** alternate location to search for configuration files.
**GHOSTTY_LOG**
: The `GHOSTTY_LOG` environment variable can be used to control which
destinations receive logs. Ghostty currently defines two destinations:
: - `stderr` - logging to `stderr`.
: - `macos` - logging to macOS's unified log (has no effect on non-macOS platforms).
: Combine values with a comma to enable multiple destinations. Prefix a
destination with `no-` to disable it. Enabling and disabling destinations
can be done at the same time. Setting `GHOSTTY_LOG` to `true` will enable all
destinations. Setting `GHOSTTY_LOG` to `false` will disable all destinations.
# BUGS
See GitHub issues: <https://github.com/ghostty-org/ghostty/issues>
# AUTHOR
Mitchell Hashimoto <m@mitchellh.com>
Ghostty contributors <https://github.com/ghostty-org/ghostty/graphs/contributors>
# SEE ALSO
**ghostty(5)**
@@ -0,0 +1,23 @@
% GHOSTTY(1) Version @@VERSION@@ | Ghostty terminal emulator
# NAME
**ghostty** - Ghostty terminal emulator
# DESCRIPTION
Ghostty is a cross-platform, GPU-accelerated terminal emulator that aims to push
the boundaries of what is possible with a terminal emulator by exposing modern,
opt-in features that enable CLI tool developers to build more feature rich,
interactive applications.
There are a number of excellent terminal emulator options that exist today.
The unique goal of Ghostty is to have a platform for experimenting with modern,
optional, non-standards-compliant features to enhance the capabilities of CLI
applications. We aim to be the best in this category, and competitive in the
rest.
While aiming for this ambitious goal, Ghostty is a fully standards compliant
terminal emulator that aims to remain compatible with all existing shells and
software. You can use this as a drop-in replacement for your existing terminal
emulator.
@@ -0,0 +1,43 @@
# FILES
_\$XDG_CONFIG_HOME/ghostty/config.ghostty_
: Location of the default configuration file.
_\$HOME/Library/Application Support/com.mitchellh.ghostty/config.ghostty_
: **On macOS**, location of the default configuration file. This location takes
precedence over the XDG environment locations.
_\$LOCALAPPDATA/ghostty/config.ghostty_
: **On Windows**, if _\$XDG_CONFIG_HOME_ is not set, _\$LOCALAPPDATA_ will be searched
for configuration files.
# ENVIRONMENT
**XDG_CONFIG_HOME**
: Default location for configuration files.
**$HOME/Library/Application Support/com.mitchellh.ghostty**
: **MACOS ONLY** default location for configuration files. This location takes
precedence over the XDG environment locations.
**LOCALAPPDATA**
: **WINDOWS ONLY:** alternate location to search for configuration files.
# BUGS
See GitHub issues: <https://github.com/ghostty-org/ghostty/issues>
# AUTHOR
Mitchell Hashimoto <m@mitchellh.com>
Ghostty contributors <https://github.com/ghostty-org/ghostty/graphs/contributors>
# SEE ALSO
**ghostty(1)**
@@ -0,0 +1,122 @@
% GHOSTTY(5) Version @@VERSION@@ | Ghostty terminal emulator configuration file
# NAME
**ghostty** - Ghostty terminal emulator configuration file
# DESCRIPTION
To configure Ghostty, you must use a configuration file. GUI-based configuration
is on the roadmap but not yet supported. The configuration file must be placed
at `$XDG_CONFIG_HOME/ghostty/config.ghostty`, which defaults to `~/.config/ghostty/config.ghostty`
if the [XDG environment is not set](https://specifications.freedesktop.org/basedir-spec/basedir-spec-latest.html).
**If you are using macOS, the configuration file can also be placed at
`$HOME/Library/Application Support/com.mitchellh.ghostty/config.ghostty`.** This is the
default configuration location for macOS. It will be searched before any of the
XDG environment locations listed above.
The file format is documented below as an example:
# The syntax is "key = value". The whitespace around the equals doesn't matter.
background = 282c34
foreground= ffffff
# Blank lines are ignored!
keybind = ctrl+z=close_surface
keybind = ctrl+d=new_split:right
# Colors can be changed by setting the 16 colors of `palette`, which each color
# being defined as regular and bold.
#
# black
palette = 0=#1d2021
palette = 8=#7c6f64
# red
palette = 1=#cc241d
palette = 9=#fb4934
# green
palette = 2=#98971a
palette = 10=#b8bb26
# yellow
palette = 3=#d79921
palette = 11=#fabd2f
# blue
palette = 4=#458588
palette = 12=#83a598
# purple
palette = 5=#b16286
palette = 13=#d3869b
# aqua
palette = 6=#689d6a
palette = 14=#8ec07c
# white
palette = 7=#a89984
palette = 15=#fbf1c7
You can view all available configuration options and their documentation by
executing the command `ghostty +show-config --default --docs`. Note that this will
output the full default configuration with docs to stdout, so you may want to
pipe that through a pager, an editor, etc.
Note: You'll see a lot of weird blank configurations like `font-family =`. This
is a valid syntax to specify the default behavior (no value). The `+show-config`
outputs it so it's clear that key is defaulting and also to have something to
attach the doc comment to.
You can also see and read all available configuration options in the source
Config structure. The available keys are the keys verbatim, and their possible
values are typically documented in the comments. You also can search for
the public config files of many Ghostty users for examples and inspiration.
## Configuration Errors
If your configuration file has any errors, Ghostty does its best to ignore
them and move on. Configuration errors will be logged.
## Debugging Configuration
You can verify that configuration is being properly loaded by looking at the
debug output of Ghostty.
In the debug output, you should see in the first 20 lines or so messages about
loading (or not loading) a configuration file, as well as any errors it may have
encountered. Configuration errors are also shown in a dedicated window on both
macOS and Linux (GTK). Ghostty does not treat configuration errors as fatal and
will fall back to default values for erroneous keys.
You can also view the full configuration Ghostty is loading using `ghostty
+show-config` from the command-line. Use the `--help` flag to additional options
for that command.
## Logging
Ghostty can write logs to a number of destinations. On all platforms, logging to
`stderr` is available. Depending on the platform and how Ghostty was launched,
logs sent to `stderr` may be stored by the system and made available for later
retrieval.
On Linux if Ghostty is launched by the default `systemd` user service, you can use
`journald` to see Ghostty's logs: `journalctl --user --unit app-com.mitchellh.ghostty.service`.
On macOS logging to the macOS unified log is available and enabled by default.
--Use the system `log` CLI to view Ghostty's logs: `sudo log stream --level debug
--predicate 'subsystem=="com.mitchellh.ghostty"'`.
Ghostty's logging can be configured in two ways. The first is by what
optimization level Ghostty is compiled with. If Ghostty is compiled with `Debug`
optimizations debug logs will be output to `stderr`. If Ghostty is compiled with
any other optimization the debug logs will not be output to `stderr`.
Ghostty also checks the `GHOSTTY_LOG` environment variable. It can be used
to control which destinations receive logs. Ghostty currently defines two
destinations:
- `stderr` - logging to `stderr`.
- `macos` - logging to macOS's unified log (has no effect on non-macOS platforms).
Combine values with a comma to enable multiple destinations. Prefix a
destination with `no-` to disable it. Enabling and disabling destinations
can be done at the same time. Setting `GHOSTTY_LOG` to `true` will enable all
destinations. Setting `GHOSTTY_LOG` to `false` will disable all destinations.
+10
View File
@@ -0,0 +1,10 @@
//! Sublime syntax (also used by `bat`) (port of `extra/sublime.zig`).
use crate::chunks_sublime::{C0, C1};
use crate::config_fields;
#[must_use]
pub fn syntax() -> String {
let keywords: Vec<_> = config_fields().iter().map(|f| f.name).collect();
format!("{C0}{}{C1}", keywords.join("|"))
}
+20
View File
@@ -0,0 +1,20 @@
//! Vim/Neovim plugin files (port of `extra/vim.zig`).
use std::fmt::Write as _;
use crate::chunks_vim::{C0, C1, C2, C3, C4};
use crate::config_fields;
pub const FTDETECT: &str = C0;
pub const FTPLUGIN: &str = C1;
pub const COMPILER: &str = C2;
#[must_use]
pub fn syntax() -> String {
let mut w = String::from(C3);
for f in config_fields() {
let _ = write!(w, "\n\t\\ {}", f.name);
}
w.push_str(C4);
w
}
+95
View File
@@ -0,0 +1,95 @@
//! Zsh completions (port of `extra/zsh.zig`).
use std::fmt::Write as _;
use ghostty_config::cli_args::FieldKind;
use crate::chunks_zsh::{C0, C1, C2, C3};
use crate::{actions, config_fields, join_names, join_packed};
const EQUALS_REQUIRED: &str = "=-:::";
#[must_use]
pub fn completions() -> String {
let mut w = String::new();
w.push_str(C0);
w.push_str("_config() {\n");
w.push_str(" _arguments \\\n");
w.push_str(" \"--help\" \\\n");
w.push_str(" \"--version\" \\\n");
for f in config_fields() {
let _ = write!(w, " \"--{}", f.name);
if f.name.starts_with("font-family") {
let _ = write!(w, "{EQUALS_REQUIRED}_fonts");
} else if f.name == "theme" {
let _ = write!(w, "{EQUALS_REQUIRED}_themes");
} else if f.name == "working-directory" {
let _ = write!(w, "{EQUALS_REQUIRED}{{_files -/}}");
} else if f.kind == FieldKind::RepeatablePath {
let _ = write!(w, "{EQUALS_REQUIRED}_files");
} else {
match f.kind {
FieldKind::Bool => {}
FieldKind::Enum(names) => {
let _ = write!(w, "{EQUALS_REQUIRED}({})", join_names(names));
}
FieldKind::Packed(names) => {
let _ = write!(w, "{EQUALS_REQUIRED}({})", join_packed(names));
}
_ => {
let _ = write!(w, "{EQUALS_REQUIRED}( )");
}
}
}
w.push_str("\" \\\n");
}
w.push_str("\n}\n\n");
w.push_str(C1);
let padding = " ";
for a in actions() {
let _ = writeln!(w, "{padding}'+{}'", a.name());
}
w.push_str(C2);
for a in actions() {
let opts = a.option_fields();
if opts.is_empty() {
continue;
}
let _ = writeln!(w, "{padding}(+{})", a.name());
let _ = writeln!(w, "{padding} _arguments \\");
for (name, kind) in &opts {
let _ = write!(w, "{padding} '--{name}");
match kind {
FieldKind::Bool => {}
FieldKind::Enum(names) => {
let _ = write!(w, "{EQUALS_REQUIRED}({})", join_names(names));
}
FieldKind::OptionalEnum(names) => {
let _ = write!(w, "{EQUALS_REQUIRED}({})", join_names(names));
}
FieldKind::OptionalBool | FieldKind::OptionalOther => {
w.push_str(EQUALS_REQUIRED);
if *name == "config-file" {
w.push_str("_files");
} else {
w.push_str("( )");
}
}
_ => {
w.push_str(EQUALS_REQUIRED);
if *name == "config-file" {
w.push_str("_files");
} else {
w.push_str("( )");
}
}
}
w.push_str("' \\\n");
}
let _ = writeln!(w, "{padding};;");
}
w.push_str(C3);
w
}