Zig parses config values (cli/args.zig, Config.zig field parsers, config/string.zig) from raw byte slices, and each field type decides whether it requires UTF-8 (InvalidValue otherwise) or stores bytes. The port escape-encoded raw bytes into Strings at the line reader and let fields parse the escaped text, so fields could observe encoding artifacts; successive audits kept finding such leaks (RV5-RV8). Field parsers now receive the original bytes: text fields validate UTF-8 where Zig does, byte/path fields store bytes (EncodedOs/OsString). This resolves all 9 RV8 findings (docs/rewrite/notes/review-rv8-codex.md), each with a regression test: Windows path conversion fails with BadPathName instead of substituting U+FFFD; Windows process launch uses strict UTF-8 like Zig's Command; the WTF-8 decoder accepts paired- surrogate encodings like Zig; literal U+FDD0 Unicode key triggers; selection-word-chars and clipboard-codepoint-map reject invalid UTF-8; font style / codepoint-map family names use original bytes; C title actions forward bytes; D-Bus new-window/Flatpak conversions match Zig. Design, sweep and validation: docs/rewrite/notes/final-bytes-refactor.md. Also moves the bundled iTerm2-Color-Schemes license out of resources/themes/ so +list-themes no longer lists it as a theme (output is now byte-identical to Ghostty 1.3.1). Done by Codex (gpt-6-astra), reviewed and verified by the orchestrator. Test Plan: - cargo test --workspace --no-fail-fast (3428 passed, exit 0) - just check-macos (both), just check-windows, just check-wasm - +show-config --default --docs, +list-keybinds, +list-actions --docs, +list-themes --plain compared with /usr/bin/ghostty
333 lines
12 KiB
Rust
333 lines
12 KiB
Rust
//! Declarative macros used to define config structs, enums and packed
|
|
//! (bitfield-like) structs with Zig-compatible names and parsing.
|
|
|
|
use std::fmt;
|
|
|
|
/// Compute the help string for a field from its raw doc lines
|
|
/// (Zig: `helpgen.zig` `extractDocComments`). The `///` prefix has already
|
|
/// been removed (rustc's `#[doc]` value keeps the leading space just like the
|
|
/// Zig token slice `[3..]`). The common leading-space prefix is stripped and
|
|
/// the lines are joined with `\n`.
|
|
#[must_use]
|
|
pub fn help_string(lines: &[&str]) -> String {
|
|
let mut m = usize::MAX;
|
|
for line in lines {
|
|
let n = line.bytes().position(|c| c != b' ').unwrap_or(usize::MAX);
|
|
m = m.min(n);
|
|
}
|
|
let mut out = String::new();
|
|
for (i, line) in lines.iter().enumerate() {
|
|
if i > 0 {
|
|
out.push('\n');
|
|
}
|
|
out.push_str(&line[m.min(line.len())..]);
|
|
}
|
|
out
|
|
}
|
|
|
|
/// Metadata and accessors for a single field of a config struct.
|
|
pub struct FieldInfo<T> {
|
|
/// The field name as used in config files (e.g. `font-family`).
|
|
pub name: &'static str,
|
|
/// Raw doc comment lines.
|
|
pub doc: &'static [&'static str],
|
|
/// Classification of the field's type (for completion generation).
|
|
pub kind: crate::cli_args::FieldKind,
|
|
/// Deep equality of this field between two structs.
|
|
pub eq: fn(&T, &T) -> bool,
|
|
/// Format this field as in `+show-config`.
|
|
pub format: fn(&T, &str, &mut dyn fmt::Write) -> fmt::Result,
|
|
/// The value in the form the C API exposes it.
|
|
pub c_value: fn(&T) -> crate::c_value::CValue,
|
|
}
|
|
|
|
impl<T> FieldInfo<T> {
|
|
/// The help text for this field (empty string if undocumented).
|
|
#[must_use]
|
|
pub fn help(&self) -> String {
|
|
help_string(self.doc)
|
|
}
|
|
}
|
|
|
|
/// Implemented by structs declared with [`config_struct!`](crate::config_struct).
|
|
pub trait ConfigStruct: Sized + 'static {
|
|
const FIELDS: &'static [FieldInfo<Self>];
|
|
}
|
|
|
|
/// Declare a configuration struct.
|
|
///
|
|
/// ```ignore
|
|
/// config_struct! {
|
|
/// pub struct Foo {
|
|
/// /// doc comment
|
|
/// "some-key" => some_key: bool = false,
|
|
/// }
|
|
/// extra { diags: DiagnosticList = DiagnosticList::default(), }
|
|
/// }
|
|
/// ```
|
|
///
|
|
/// Generates the struct (all fields `pub`), `raw_default()`,
|
|
/// `parse_field(key, value)` (Zig: `parseIntoField`), `valid_values_for(key)`,
|
|
/// and a [`ConfigStruct`] impl with per-field metadata.
|
|
#[macro_export]
|
|
macro_rules! config_struct {
|
|
(
|
|
$(#[$meta:meta])*
|
|
$vis:vis struct $name:ident {
|
|
$(
|
|
$(#[doc = $doc:literal])*
|
|
$key:literal => $field:ident : $ty:ty = $default:expr
|
|
),* $(,)?
|
|
}
|
|
$(
|
|
extra {
|
|
$( $(#[$emeta:meta])* $efield:ident : $ety:ty = $edefault:expr ),* $(,)?
|
|
}
|
|
)?
|
|
) => {
|
|
$(#[$meta])*
|
|
#[derive(Clone, Debug)]
|
|
#[allow(dead_code)]
|
|
$vis struct $name {
|
|
$( pub $field: $ty, )*
|
|
$( $( $(#[$emeta])* pub $efield: $ety, )* )?
|
|
}
|
|
|
|
impl $name {
|
|
/// The struct with every field set to its declared default.
|
|
#[allow(clippy::too_many_lines, dead_code)]
|
|
pub fn raw_default() -> Self {
|
|
Self {
|
|
$( $field: $default, )*
|
|
$( $( $efield: $edefault, )* )?
|
|
}
|
|
}
|
|
|
|
/// Parse a plain UTF-8 value supplied programmatically.
|
|
#[allow(dead_code)]
|
|
pub fn parse_field(&mut self, key: &str, value: Option<&str>) -> Result<(), $crate::cli_args::ArgError> {
|
|
self.parse_field_bytes(key, value.map(str::as_bytes))
|
|
}
|
|
|
|
#[allow(dead_code)]
|
|
/// Parse a single key/value into the matching field. Empty
|
|
/// values reset the field to its default.
|
|
pub fn parse_field_bytes(
|
|
&mut self,
|
|
key: &str,
|
|
value: ::core::option::Option<&[u8]>,
|
|
) -> ::core::result::Result<(), $crate::cli_args::ArgError> {
|
|
match key {
|
|
$(
|
|
$key => {
|
|
if value == ::core::option::Option::Some(b"".as_slice()) {
|
|
$crate::cli_args::ConfigField::reset(&mut self.$field, $default);
|
|
return ::core::result::Result::Ok(());
|
|
}
|
|
$crate::cli_args::ConfigField::parse_bytes(&mut self.$field, value)
|
|
}
|
|
)*
|
|
_ => ::core::result::Result::Err($crate::cli_args::ArgError::InvalidField),
|
|
}
|
|
}
|
|
|
|
/// For enum-typed fields, the valid values.
|
|
#[allow(dead_code)]
|
|
pub fn valid_values_for(key: &str) -> ::core::option::Option<&'static [&'static str]> {
|
|
match key {
|
|
$(
|
|
$key => <$ty as $crate::cli_args::ConfigField>::VALID_VALUES,
|
|
)*
|
|
_ => ::core::option::Option::None,
|
|
}
|
|
}
|
|
}
|
|
|
|
impl $crate::macros::ConfigStruct for $name {
|
|
const FIELDS: &'static [$crate::macros::FieldInfo<$name>] = &[
|
|
$(
|
|
$crate::macros::FieldInfo {
|
|
name: $key,
|
|
doc: &[$($doc),*],
|
|
kind: <$ty as $crate::cli_args::ConfigField>::KIND,
|
|
eq: |a: &$name, b: &$name| a.$field == b.$field,
|
|
format: |a: &$name, name: &str, w: &mut dyn ::core::fmt::Write| {
|
|
let mut f = $crate::formatter::EntryFormatter::new(name, w);
|
|
$crate::cli_args::ConfigField::format_entry(&a.$field, &mut f)
|
|
},
|
|
c_value: |a: &$name| $crate::cli_args::ConfigField::c_value(&a.$field),
|
|
},
|
|
)*
|
|
];
|
|
}
|
|
};
|
|
}
|
|
|
|
/// Declare a Zig-style enum whose variants parse from / print as the given
|
|
/// strings.
|
|
#[macro_export]
|
|
macro_rules! config_enum {
|
|
(
|
|
$(#[$meta:meta])*
|
|
$vis:vis enum $name:ident {
|
|
$(
|
|
$(#[doc = $doc:literal])*
|
|
$variant:ident => $str:literal
|
|
),* $(,)?
|
|
}
|
|
) => {
|
|
$(#[$meta])*
|
|
#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash)]
|
|
#[allow(clippy::enum_variant_names)]
|
|
$vis enum $name {
|
|
$( $(#[doc = $doc])* $variant, )*
|
|
}
|
|
|
|
impl $name {
|
|
/// All variants in declaration order.
|
|
pub const ALL: &'static [$name] = &[ $( $name::$variant, )* ];
|
|
/// All variant names in declaration order.
|
|
pub const NAMES: &'static [&'static str] = &[ $( $str, )* ];
|
|
|
|
/// The user-facing name of this variant.
|
|
#[must_use]
|
|
pub fn as_str(&self) -> &'static str {
|
|
match self { $( $name::$variant => $str, )* }
|
|
}
|
|
|
|
/// Zig: `std.meta.stringToEnum`.
|
|
#[must_use]
|
|
pub fn from_name(s: &str) -> ::core::option::Option<Self> {
|
|
match s {
|
|
$( $str => ::core::option::Option::Some($name::$variant), )*
|
|
_ => ::core::option::Option::None,
|
|
}
|
|
}
|
|
}
|
|
|
|
impl ::core::default::Default for $name {
|
|
fn default() -> Self {
|
|
Self::ALL[0]
|
|
}
|
|
}
|
|
|
|
impl ::core::fmt::Display for $name {
|
|
fn fmt(&self, f: &mut ::core::fmt::Formatter<'_>) -> ::core::fmt::Result {
|
|
f.write_str(self.as_str())
|
|
}
|
|
}
|
|
|
|
impl $crate::cli_args::ConfigField for $name {
|
|
const VALID_VALUES: ::core::option::Option<&'static [&'static str]> =
|
|
::core::option::Option::Some(Self::NAMES);
|
|
|
|
fn parse_bytes(
|
|
&mut self,
|
|
value: ::core::option::Option<&[u8]>,
|
|
) -> ::core::result::Result<(), $crate::cli_args::ArgError> {
|
|
let v = $crate::cli_args::text_value(value)?.ok_or($crate::cli_args::ArgError::ValueRequired)?;
|
|
*self = Self::from_name(v).ok_or($crate::cli_args::ArgError::InvalidValue)?;
|
|
::core::result::Result::Ok(())
|
|
}
|
|
|
|
fn c_value(&self) -> $crate::c_value::CValue {
|
|
$crate::c_value::CValue::EnumName(self.as_str())
|
|
}
|
|
|
|
fn format_entry(
|
|
&self,
|
|
f: &mut $crate::formatter::EntryFormatter<'_>,
|
|
) -> ::core::fmt::Result {
|
|
::core::writeln!(f.writer, "{} = {}", f.name, self.as_str())
|
|
}
|
|
}
|
|
};
|
|
}
|
|
|
|
/// Declare a Zig `packed struct` of bools: parsed from a comma separated
|
|
/// list of names (optionally prefixed with `no-`) or a plain boolean.
|
|
#[macro_export]
|
|
macro_rules! config_packed {
|
|
(
|
|
$(#[$meta:meta])*
|
|
$vis:vis struct $name:ident {
|
|
$(
|
|
$(#[doc = $doc:literal])*
|
|
$key:literal => $field:ident : $default:expr
|
|
),* $(,)?
|
|
}
|
|
) => {
|
|
$(#[$meta])*
|
|
#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash)]
|
|
$vis struct $name {
|
|
$( $(#[doc = $doc])* pub $field: bool, )*
|
|
}
|
|
|
|
impl $name {
|
|
/// Field names (user-facing) in declaration order.
|
|
pub const NAMES: &'static [&'static str] = &[ $( $key, )* ];
|
|
|
|
fn set_index(&mut self, idx: usize, value: bool) {
|
|
let mut i = 0usize;
|
|
$(
|
|
if idx == i { self.$field = value; return; }
|
|
i += 1;
|
|
)*
|
|
let _ = i;
|
|
}
|
|
}
|
|
|
|
impl ::core::default::Default for $name {
|
|
fn default() -> Self {
|
|
Self { $( $field: $default, )* }
|
|
}
|
|
}
|
|
|
|
impl $crate::cli_args::ConfigField for $name {
|
|
const KIND: $crate::cli_args::FieldKind =
|
|
$crate::cli_args::FieldKind::Packed(Self::NAMES);
|
|
|
|
fn c_value(&self) -> $crate::c_value::CValue {
|
|
// Packed bools: the first field is bit 0.
|
|
let mut bits = 0u32;
|
|
let mut _i = 0u32;
|
|
$(
|
|
if self.$field && _i < 32 { bits |= 1 << _i; }
|
|
_i += 1;
|
|
)*
|
|
if _i > 32 {
|
|
return $crate::c_value::CValue::Unsupported;
|
|
}
|
|
$crate::c_value::CValue::Packed(bits)
|
|
}
|
|
|
|
fn parse_bytes(
|
|
&mut self,
|
|
value: ::core::option::Option<&[u8]>,
|
|
) -> ::core::result::Result<(), $crate::cli_args::ArgError> {
|
|
let v = $crate::cli_args::text_value(value)?.ok_or($crate::cli_args::ArgError::ValueRequired)?;
|
|
let mut result = <$name as ::core::default::Default>::default();
|
|
$crate::cli_args::parse_packed_struct_with(v, Self::NAMES, |i, b| {
|
|
result.set_index(i, b);
|
|
})?;
|
|
*self = result;
|
|
::core::result::Result::Ok(())
|
|
}
|
|
|
|
fn format_entry(
|
|
&self,
|
|
f: &mut $crate::formatter::EntryFormatter<'_>,
|
|
) -> ::core::fmt::Result {
|
|
::core::write!(f.writer, "{} = ", f.name)?;
|
|
let mut _first = true;
|
|
$(
|
|
if !_first { ::core::write!(f.writer, ",")?; }
|
|
_first = false;
|
|
::core::write!(f.writer, "{}{}", if self.$field { "" } else { "no-" }, $key)?;
|
|
)*
|
|
::core::writeln!(f.writer)
|
|
}
|
|
}
|
|
};
|
|
}
|