feat(lib): port libghostty, Ghostty's embedded C API
Ports apprt/embedded.zig, main_c.zig and config/CApi.zig + c_get.zig as crates/ghostty-lib (cdylib/staticlib/rlib `libghostty`), exporting every function of the unchanged include/ghostty.h - the ABI Ghostty's Swift macOS/iOS app links against. The embedded runtime implements the runtime traits over the host's C callbacks, so the real core drives it. - ghostty-core selects the renderer per target: Metal on Apple (surface NSView/UIView via RtSurface::platform_view), OpenGL elsewhere. - ghostty_config_get follows Zig's c_get rules via a CValue type (143 supported keys, checked against the Zig field types). - The inspector C API is complete; its Metal backend replaces ImGui's Metal renderer and has not run. Verification: generated C ABI probe (sizeof/alignof/offsetof of every struct/union, 755 enum values, every function linked, nm exports, headers byte-identical to upstream); a C host on Linux covering init, CLI actions, config, app, keys, focus and color scheme; an opt-in test-platform feature creating real surfaces on Linux with headless EGL and a pty (13 tests). Apple targets are clippy-clean but not run; the Swift app has not been built against it. Test Plan: - cargo test -p ghostty-lib (and --features test-platform) - cargo clippy --target aarch64-apple-darwin -p ghostty-lib (cross env in docs/rewrite/notes/macos-M1.md)
This commit is contained in:
1 parent
f2252bb165
commit
75bf604375
47 files changed
+10296
-34
No files matched your search
@@ -0,0 +1,64 @@
|
||||
//! The C representation of config values (Zig: `config/c_get.zig` together
|
||||
//! with the `cval` functions of the config types).
|
||||
//!
|
||||
//! `ghostty_config_get` writes a value of a key-dependent C type into a
|
||||
//! caller supplied pointer. This crate cannot use `unsafe`, so the typed
|
||||
//! value is described by [`CValue`] and the C API crate (`ghostty-lib`) does
|
||||
//! the actual write.
|
||||
|
||||
use ghostty_input::command::CommandC;
|
||||
|
||||
/// A size of the quick terminal along one axis in C form
|
||||
/// (`ghostty_quick_terminal_size_s`).
|
||||
#[derive(Debug, Clone, Copy, PartialEq)]
|
||||
pub enum QuickTerminalSizeC {
|
||||
None,
|
||||
Percentage(f32),
|
||||
Pixels(u32),
|
||||
}
|
||||
|
||||
/// The value of a config field in the form the C API exposes it.
|
||||
#[derive(Debug, Clone, PartialEq)]
|
||||
pub enum CValue {
|
||||
/// The type of this field is not supported by the C API (Zig `getValue`
|
||||
/// returns false).
|
||||
Unsupported,
|
||||
/// An optional field without a value (returns false).
|
||||
Null,
|
||||
/// An optional string without a value: `ghostty_config_get` returns true
|
||||
/// and stores a null pointer.
|
||||
NullString,
|
||||
/// A string (`?[:0]const u8`): stored as a NUL terminated `const char*`.
|
||||
String(String),
|
||||
/// A boolean (`bool`).
|
||||
Bool(bool),
|
||||
/// `u8` / `u32`, stored as `c_uint`.
|
||||
Uint(u32),
|
||||
/// `i16`, stored as `c_short`.
|
||||
Short(i16),
|
||||
/// `f32`.
|
||||
F32(f32),
|
||||
/// `f64`.
|
||||
F64(f64),
|
||||
/// `usize` (`Duration.cval`).
|
||||
Usize(usize),
|
||||
/// An enum: the tag name, stored as a NUL terminated `const char*`.
|
||||
EnumName(&'static str),
|
||||
/// A packed struct of at most 32 bits: its bits stored as `c_uint`.
|
||||
Packed(u32),
|
||||
/// `ghostty_config_color_s`.
|
||||
Color { r: u8, g: u8, b: u8 },
|
||||
/// `ghostty_config_color_list_s`.
|
||||
ColorList(Vec<[u8; 3]>),
|
||||
/// `ghostty_config_palette_s`.
|
||||
Palette(Box<[[u8; 3]; 256]>),
|
||||
/// `ghostty_config_command_list_s`.
|
||||
Commands(Vec<CommandC>),
|
||||
/// `ghostty_config_quick_terminal_size_s`.
|
||||
QuickTerminalSize {
|
||||
primary: QuickTerminalSizeC,
|
||||
secondary: QuickTerminalSizeC,
|
||||
},
|
||||
/// `ghostty_config_path_s`.
|
||||
Path { path: String, optional: bool },
|
||||
}
|
||||
@@ -15,6 +15,7 @@
|
||||
use std::{fmt, io::BufRead};
|
||||
|
||||
use crate::{
|
||||
c_value::CValue,
|
||||
comma_splitter::CommaSplitter,
|
||||
diagnostics::{Diagnostic, DiagnosticList, Location},
|
||||
formatter::EntryFormatter,
|
||||
@@ -113,6 +114,28 @@ pub trait ConfigField: Clone + PartialEq + Default + fmt::Debug {
|
||||
|
||||
/// Write this value in the `+show-config` format.
|
||||
fn format_entry(&self, f: &mut EntryFormatter<'_>) -> fmt::Result;
|
||||
|
||||
/// The value in the form the C API exposes (Zig: `c_get.getValue`).
|
||||
/// Types the C API doesn't support yet return [`CValue::Unsupported`].
|
||||
fn c_value(&self) -> CValue {
|
||||
CValue::Unsupported
|
||||
}
|
||||
|
||||
/// [`ConfigField::c_value`] when the value is wrapped in an `Option`
|
||||
/// (only `String` differs: `?[:0]const u8` is supported, `[]const u8`
|
||||
/// is not).
|
||||
fn c_value_optional(&self) -> CValue {
|
||||
self.c_value()
|
||||
}
|
||||
|
||||
/// The C value of an `Option<Self>` that is `None`.
|
||||
fn c_value_none() -> CValue {
|
||||
if matches!(Self::default().c_value(), CValue::Unsupported) {
|
||||
CValue::Unsupported
|
||||
} else {
|
||||
CValue::Null
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// The destination of CLI parsing.
|
||||
@@ -456,6 +479,10 @@ pub fn parse_int_base0(s: &str) -> Option<i128> {
|
||||
impl ConfigField for bool {
|
||||
const KIND: FieldKind = FieldKind::Bool;
|
||||
|
||||
fn c_value(&self) -> CValue {
|
||||
CValue::Bool(*self)
|
||||
}
|
||||
|
||||
fn parse_cli(&mut self, value: Option<&str>) -> Result<(), ArgError> {
|
||||
*self = parse_bool(value.unwrap_or("t"))?;
|
||||
Ok(())
|
||||
@@ -467,8 +494,13 @@ impl ConfigField for bool {
|
||||
}
|
||||
|
||||
macro_rules! impl_int_field {
|
||||
($($t:ty),*) => {$(
|
||||
($($t:ty => $cv:expr),*) => {$(
|
||||
impl ConfigField for $t {
|
||||
fn c_value(&self) -> CValue {
|
||||
let f: fn($t) -> CValue = $cv;
|
||||
f(*self)
|
||||
}
|
||||
|
||||
fn parse_cli(&mut self, value: Option<&str>) -> Result<(), ArgError> {
|
||||
let v = value.ok_or(ArgError::ValueRequired)?;
|
||||
let n = parse_int_base0(v).ok_or(ArgError::InvalidValue)?;
|
||||
@@ -482,11 +514,27 @@ macro_rules! impl_int_field {
|
||||
}
|
||||
)*};
|
||||
}
|
||||
impl_int_field!(u8, u16, u32, u64, usize, i8, i16, i32, i64, isize);
|
||||
impl_int_field!(
|
||||
u8 => |v| CValue::Uint(u32::from(v)),
|
||||
u16 => |_| CValue::Unsupported,
|
||||
u32 => CValue::Uint,
|
||||
u64 => |_| CValue::Unsupported,
|
||||
usize => |_| CValue::Unsupported,
|
||||
i8 => |_| CValue::Unsupported,
|
||||
i16 => CValue::Short,
|
||||
i32 => |_| CValue::Unsupported,
|
||||
i64 => |_| CValue::Unsupported,
|
||||
isize => |_| CValue::Unsupported
|
||||
);
|
||||
|
||||
macro_rules! impl_float_field {
|
||||
($($t:ty),*) => {$(
|
||||
($($t:ty => $cv:expr),*) => {$(
|
||||
impl ConfigField for $t {
|
||||
fn c_value(&self) -> CValue {
|
||||
let f: fn($t) -> CValue = $cv;
|
||||
f(*self)
|
||||
}
|
||||
|
||||
fn parse_cli(&mut self, value: Option<&str>) -> Result<(), ArgError> {
|
||||
let v = value.ok_or(ArgError::ValueRequired)?;
|
||||
*self = v.parse::<$t>().map_err(|_| ArgError::InvalidValue)?;
|
||||
@@ -499,9 +547,17 @@ macro_rules! impl_float_field {
|
||||
}
|
||||
)*};
|
||||
}
|
||||
impl_float_field!(f32, f64);
|
||||
impl_float_field!(f32 => CValue::F32, f64 => CValue::F64);
|
||||
|
||||
impl ConfigField for String {
|
||||
fn c_value_optional(&self) -> CValue {
|
||||
CValue::String(self.clone())
|
||||
}
|
||||
|
||||
fn c_value_none() -> CValue {
|
||||
CValue::NullString
|
||||
}
|
||||
|
||||
fn parse_cli(&mut self, value: Option<&str>) -> Result<(), ArgError> {
|
||||
*self = value.ok_or(ArgError::ValueRequired)?.to_string();
|
||||
Ok(())
|
||||
@@ -519,6 +575,13 @@ impl<T: ConfigField> ConfigField for Option<T> {
|
||||
_ => FieldKind::OptionalOther,
|
||||
};
|
||||
|
||||
fn c_value(&self) -> CValue {
|
||||
match self {
|
||||
Some(v) => v.c_value_optional(),
|
||||
None => T::c_value_none(),
|
||||
}
|
||||
}
|
||||
|
||||
fn parse_cli(&mut self, value: Option<&str>) -> Result<(), ArgError> {
|
||||
// For optional fields, we just treat it as the child type.
|
||||
if let Some(v) = self {
|
||||
|
||||
@@ -3810,6 +3810,16 @@ impl Default for Config {
|
||||
}
|
||||
|
||||
impl Config {
|
||||
/// The value of the config key in the form the C API exposes it (Zig:
|
||||
/// `c_get.get`). `None` if there is no such key.
|
||||
#[must_use]
|
||||
pub fn c_value(&self, key: &str) -> Option<crate::c_value::CValue> {
|
||||
<Config as crate::macros::ConfigStruct>::FIELDS
|
||||
.iter()
|
||||
.find(|f| f.name == key)
|
||||
.map(|f| (f.c_value)(self))
|
||||
}
|
||||
|
||||
/// Build the default configuration (Zig: `Config.default`).
|
||||
#[must_use]
|
||||
pub fn new_default() -> Config {
|
||||
@@ -5412,4 +5422,85 @@ mod tests {
|
||||
assert!(out.contains("\nfont-size = 12\n"));
|
||||
assert!(out.contains("\nbackground = #282c34\n"));
|
||||
}
|
||||
|
||||
/// The keys `ghostty_config_get` supports, derived from the Zig field
|
||||
/// types and `c_get.getValue` (see `testdata/c_get_supported.txt`).
|
||||
#[test]
|
||||
fn c_value_supported_keys_match_zig() {
|
||||
use crate::{c_value::CValue, macros::ConfigStruct};
|
||||
let expected: std::collections::BTreeSet<&str> =
|
||||
include_str!("testdata/c_get_supported.txt")
|
||||
.lines()
|
||||
.filter(|l| !l.is_empty())
|
||||
.collect();
|
||||
let cfg = Config::default();
|
||||
let mut actual = std::collections::BTreeSet::new();
|
||||
for f in <Config as ConfigStruct>::FIELDS {
|
||||
if !matches!((f.c_value)(&cfg), CValue::Unsupported) {
|
||||
actual.insert(f.name);
|
||||
}
|
||||
}
|
||||
let missing: Vec<_> = expected.difference(&actual).collect();
|
||||
let extra: Vec<_> = actual.difference(&expected).collect();
|
||||
assert!(
|
||||
missing.is_empty() && extra.is_empty(),
|
||||
"missing={missing:?} extra={extra:?}"
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn c_value_basic_types() {
|
||||
use crate::c_value::CValue;
|
||||
let mut cfg = Config::default();
|
||||
cfg.maximize = true;
|
||||
assert_eq!(cfg.c_value("maximize"), Some(CValue::Bool(true)));
|
||||
cfg.window_theme = WindowTheme::Dark;
|
||||
assert_eq!(cfg.c_value("window-theme"), Some(CValue::EnumName("dark")));
|
||||
cfg.title = None;
|
||||
assert_eq!(cfg.c_value("title"), Some(CValue::NullString));
|
||||
cfg.unfocused_split_fill = None;
|
||||
assert_eq!(cfg.c_value("unfocused-split-fill"), Some(CValue::Null));
|
||||
cfg.background_opacity = 0.42;
|
||||
assert_eq!(cfg.c_value("background-opacity"), Some(CValue::F64(0.42)));
|
||||
cfg.background = crate::types_a::Color {
|
||||
r: 12,
|
||||
g: 34,
|
||||
b: 56,
|
||||
};
|
||||
assert_eq!(
|
||||
cfg.c_value("background"),
|
||||
Some(CValue::Color {
|
||||
r: 12,
|
||||
g: 34,
|
||||
b: 56
|
||||
})
|
||||
);
|
||||
assert_eq!(cfg.c_value("not-a-real-key"), None);
|
||||
assert_eq!(cfg.c_value("font-family"), Some(CValue::Unsupported));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn c_value_background_blur() {
|
||||
use crate::{c_value::CValue, types_b::BackgroundBlur};
|
||||
let mut cfg = Config::default();
|
||||
for (v, expect) in [
|
||||
(BackgroundBlur::False, 0),
|
||||
(BackgroundBlur::True, 20),
|
||||
(BackgroundBlur::Radius(42), 42),
|
||||
(BackgroundBlur::MacosGlassRegular, -1),
|
||||
(BackgroundBlur::MacosGlassClear, -2),
|
||||
] {
|
||||
cfg.background_blur = v;
|
||||
assert_eq!(cfg.c_value("background-blur"), Some(CValue::Short(expect)));
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn c_value_split_preserve_zoom() {
|
||||
use crate::c_value::CValue;
|
||||
let mut cfg = Config::default();
|
||||
assert_eq!(cfg.c_value("split-preserve-zoom"), Some(CValue::Packed(0)));
|
||||
cfg.split_preserve_zoom.navigation = true;
|
||||
assert_eq!(cfg.c_value("split-preserve-zoom"), Some(CValue::Packed(1)));
|
||||
}
|
||||
}
|
||||
@@ -6,6 +6,7 @@ use std::fmt;
|
||||
use ghostty_input::{KeyRemapSet, OptionAsAlt};
|
||||
|
||||
use crate::{
|
||||
c_value::CValue,
|
||||
cli_args::{ArgError, ConfigField},
|
||||
formatter::EntryFormatter,
|
||||
};
|
||||
@@ -30,6 +31,15 @@ impl ConfigField for KeyRemapSet {
|
||||
impl ConfigField for OptionAsAlt {
|
||||
const VALID_VALUES: Option<&'static [&'static str]> = None;
|
||||
|
||||
fn c_value(&self) -> CValue {
|
||||
CValue::EnumName(match self {
|
||||
OptionAsAlt::False => "false",
|
||||
OptionAsAlt::True => "true",
|
||||
OptionAsAlt::Left => "left",
|
||||
OptionAsAlt::Right => "right",
|
||||
})
|
||||
}
|
||||
|
||||
fn parse_cli(&mut self, value: Option<&str>) -> Result<(), ArgError> {
|
||||
let v = value.ok_or(ArgError::ValueRequired)?;
|
||||
*self = match v {
|
||||
|
||||
@@ -13,6 +13,7 @@
|
||||
)]
|
||||
|
||||
// Generic CLI parsing infrastructure.
|
||||
pub mod c_value;
|
||||
pub mod cli_args;
|
||||
pub mod comma_splitter;
|
||||
pub mod diagnostics;
|
||||
|
||||
@@ -37,6 +37,8 @@ pub struct FieldInfo<T> {
|
||||
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> {
|
||||
@@ -147,6 +149,7 @@ macro_rules! config_struct {
|
||||
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),
|
||||
},
|
||||
)*
|
||||
];
|
||||
@@ -221,6 +224,10 @@ macro_rules! config_enum {
|
||||
::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<'_>,
|
||||
@@ -274,6 +281,20 @@ macro_rules! config_packed {
|
||||
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_cli(
|
||||
&mut self,
|
||||
value: ::core::option::Option<&str>,
|
||||
|
||||
@@ -6,6 +6,7 @@ use std::{
|
||||
};
|
||||
|
||||
use crate::{
|
||||
c_value::CValue,
|
||||
cli_args::{ArgError, ConfigField},
|
||||
diagnostics::{Diagnostic, DiagnosticList},
|
||||
formatter::EntryFormatter,
|
||||
@@ -171,6 +172,19 @@ impl Path {
|
||||
}
|
||||
|
||||
impl ConfigField for Path {
|
||||
fn c_value(&self) -> CValue {
|
||||
match self {
|
||||
Path::Optional(p) => CValue::Path {
|
||||
path: p.clone(),
|
||||
optional: true,
|
||||
},
|
||||
Path::Required(p) => CValue::Path {
|
||||
path: p.clone(),
|
||||
optional: false,
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
/// Parse CLI option. The value will be overwritten.
|
||||
fn parse_cli(&mut self, input: Option<&str>) -> Result<(), ArgError> {
|
||||
let Some(item) = Path::parse(input)? else {
|
||||
|
||||
@@ -46,6 +46,14 @@ impl RepeatableStringMap {
|
||||
.map(|(_, v)| v.as_str())
|
||||
}
|
||||
|
||||
/// Insert or replace an entry (Zig: `map.put`).
|
||||
pub fn put(&mut self, key: &str, value: &str) {
|
||||
match self.map.iter_mut().find(|(k, _)| k == key) {
|
||||
Some((_, v)) => value.clone_into(v),
|
||||
None => self.map.push((key.to_owned(), value.to_owned())),
|
||||
}
|
||||
}
|
||||
|
||||
/// Iterator over the entries in the map (insertion order).
|
||||
pub fn iter(&self) -> impl Iterator<Item = (&str, &str)> {
|
||||
self.map.iter().map(|(k, v)| (k.as_str(), v.as_str()))
|
||||
|
||||
@@ -0,0 +1,143 @@
|
||||
language
|
||||
font-synthetic-style
|
||||
font-size
|
||||
font-thicken
|
||||
font-thicken-strength
|
||||
font-shaping-break
|
||||
alpha-blending
|
||||
grapheme-width-method
|
||||
freetype-load-flags
|
||||
background
|
||||
foreground
|
||||
background-image
|
||||
background-image-opacity
|
||||
background-image-position
|
||||
background-image-fit
|
||||
background-image-repeat
|
||||
selection-clear-on-typing
|
||||
selection-clear-on-copy
|
||||
minimum-contrast
|
||||
palette
|
||||
palette-generate
|
||||
palette-harmonious
|
||||
cursor-opacity
|
||||
cursor-style
|
||||
cursor-style-blink
|
||||
cursor-click-to-move
|
||||
mouse-hide-while-typing
|
||||
scroll-to-bottom
|
||||
mouse-shift-capture
|
||||
mouse-reporting
|
||||
background-opacity
|
||||
background-opacity-cells
|
||||
background-blur
|
||||
unfocused-split-opacity
|
||||
unfocused-split-fill
|
||||
split-divider-color
|
||||
split-preserve-zoom
|
||||
notify-on-command-finish
|
||||
notify-on-command-finish-action
|
||||
notify-on-command-finish-after
|
||||
wait-after-command
|
||||
abnormal-command-exit-runtime
|
||||
scrollbar
|
||||
link-url
|
||||
link-previews
|
||||
maximize
|
||||
fullscreen
|
||||
title
|
||||
class
|
||||
x11-instance-name
|
||||
window-padding-balance
|
||||
window-padding-color
|
||||
window-vsync
|
||||
window-inherit-working-directory
|
||||
tab-inherit-working-directory
|
||||
split-inherit-working-directory
|
||||
window-inherit-font-size
|
||||
window-decoration
|
||||
window-title-font-family
|
||||
window-subtitle
|
||||
window-theme
|
||||
window-colorspace
|
||||
window-height
|
||||
window-width
|
||||
window-position-x
|
||||
window-position-y
|
||||
window-save-state
|
||||
window-step-resize
|
||||
window-new-tab-position
|
||||
window-show-tab-bar
|
||||
window-titlebar-background
|
||||
window-titlebar-foreground
|
||||
resize-overlay
|
||||
resize-overlay-position
|
||||
resize-overlay-duration
|
||||
focus-follows-mouse
|
||||
clipboard-read
|
||||
clipboard-write
|
||||
clipboard-trim-trailing-spaces
|
||||
clipboard-paste-protection
|
||||
clipboard-paste-bracketed-safe
|
||||
title-report
|
||||
image-storage-limit
|
||||
copy-on-select
|
||||
right-click-action
|
||||
click-repeat-interval
|
||||
config-default-files
|
||||
confirm-close-surface
|
||||
quit-after-last-window-closed
|
||||
quit-after-last-window-closed-delay
|
||||
initial-window
|
||||
undo-timeout
|
||||
quick-terminal-position
|
||||
quick-terminal-size
|
||||
gtk-quick-terminal-layer
|
||||
quick-terminal-screen
|
||||
quick-terminal-animation-duration
|
||||
quick-terminal-autohide
|
||||
quick-terminal-space-behavior
|
||||
quick-terminal-keyboard-interactivity
|
||||
shell-integration
|
||||
shell-integration-features
|
||||
command-palette-entry
|
||||
osc-color-report-format
|
||||
vt-kam-allowed
|
||||
custom-shader-animation
|
||||
bell-features
|
||||
bell-audio-path
|
||||
bell-audio-volume
|
||||
app-notifications
|
||||
macos-non-native-fullscreen
|
||||
macos-window-buttons
|
||||
macos-titlebar-style
|
||||
macos-titlebar-proxy-icon
|
||||
macos-dock-drop-behavior
|
||||
macos-option-as-alt
|
||||
macos-window-shadow
|
||||
macos-hidden
|
||||
macos-auto-secure-input
|
||||
macos-secure-input-indication
|
||||
macos-applescript
|
||||
macos-icon
|
||||
macos-custom-icon
|
||||
macos-icon-frame
|
||||
macos-icon-ghost-color
|
||||
macos-icon-screen-color
|
||||
macos-shortcuts
|
||||
linux-cgroup
|
||||
linux-cgroup-hard-fail
|
||||
gtk-opengl-debug
|
||||
gtk-single-instance
|
||||
gtk-titlebar
|
||||
gtk-tabs-location
|
||||
gtk-titlebar-hide-when-maximized
|
||||
gtk-toolbar-style
|
||||
gtk-titlebar-style
|
||||
gtk-wide-tabs
|
||||
desktop-notifications
|
||||
progress-style
|
||||
faint-opacity
|
||||
async-backend
|
||||
auto-update
|
||||
auto-update-channel
|
||||
@@ -7,6 +7,7 @@ use std::fmt;
|
||||
use ghostty_vt::color::{DEFAULT as DEFAULT_PALETTE, Palette as TermPalette, PaletteMask, Rgb};
|
||||
|
||||
use crate::{
|
||||
c_value::CValue,
|
||||
cli_args::{ArgError, ConfigField, parse_int_base0, trim_ws},
|
||||
clipboard_codepoint_map::{ClipboardCodepointMap, Entry as ClipboardEntry, Replacement},
|
||||
config_enum,
|
||||
@@ -35,6 +36,15 @@ impl ConfigField for CursorStyle {
|
||||
const VALID_VALUES: Option<&'static [&'static str]> =
|
||||
Some(&["bar", "block", "underline", "block_hollow"]);
|
||||
|
||||
fn c_value(&self) -> CValue {
|
||||
CValue::EnumName(match self {
|
||||
CursorStyle::Bar => "bar",
|
||||
CursorStyle::Block => "block",
|
||||
CursorStyle::Underline => "underline",
|
||||
CursorStyle::BlockHollow => "block_hollow",
|
||||
})
|
||||
}
|
||||
|
||||
fn parse_cli(&mut self, value: Option<&str>) -> Result<(), ArgError> {
|
||||
let v = value.ok_or(ArgError::ValueRequired)?;
|
||||
*self = match v {
|
||||
@@ -290,6 +300,14 @@ impl Color {
|
||||
}
|
||||
|
||||
impl ConfigField for Color {
|
||||
fn c_value(&self) -> CValue {
|
||||
CValue::Color {
|
||||
r: self.r,
|
||||
g: self.g,
|
||||
b: self.b,
|
||||
}
|
||||
}
|
||||
|
||||
fn parse_cli(&mut self, input: Option<&str>) -> Result<(), ArgError> {
|
||||
*self = Color::from_cli(input)?;
|
||||
Ok(())
|
||||
@@ -394,6 +412,10 @@ pub struct ColorList {
|
||||
}
|
||||
|
||||
impl ConfigField for ColorList {
|
||||
fn c_value(&self) -> CValue {
|
||||
CValue::ColorList(self.colors.iter().map(|c| [c.r, c.g, c.b]).collect())
|
||||
}
|
||||
|
||||
fn parse_cli(&mut self, input: Option<&str>) -> Result<(), ArgError> {
|
||||
let input = input.ok_or(ArgError::ValueRequired)?;
|
||||
if input.is_empty() {
|
||||
@@ -454,6 +476,14 @@ impl Default for Palette {
|
||||
}
|
||||
|
||||
impl ConfigField for Palette {
|
||||
fn c_value(&self) -> CValue {
|
||||
let mut out = Box::new([[0u8; 3]; 256]);
|
||||
for (dst, c) in out.iter_mut().zip(self.value.iter()) {
|
||||
*dst = [c.r, c.g, c.b];
|
||||
}
|
||||
CValue::Palette(out)
|
||||
}
|
||||
|
||||
fn parse_cli(&mut self, input: Option<&str>) -> Result<(), ArgError> {
|
||||
let value = input.ok_or(ArgError::ValueRequired)?;
|
||||
let eql_idx = value.find('=').ok_or(ArgError::InvalidValue)?;
|
||||
|
||||
@@ -8,6 +8,7 @@ use std::fmt;
|
||||
use ghostty_input::binding::Action;
|
||||
|
||||
use crate::{
|
||||
c_value::{CValue, QuickTerminalSizeC},
|
||||
cli_args::{ArgError, ConfigField, parse_auto_struct_with, parse_bool, trim_ws},
|
||||
config_enum,
|
||||
config_packed,
|
||||
@@ -152,6 +153,15 @@ impl RepeatableCommand {
|
||||
}
|
||||
|
||||
impl ConfigField for RepeatableCommand {
|
||||
fn c_value(&self) -> CValue {
|
||||
CValue::Commands(
|
||||
self.value
|
||||
.iter()
|
||||
.map(ghostty_input::command::Command::cval)
|
||||
.collect(),
|
||||
)
|
||||
}
|
||||
|
||||
fn parse_cli(&mut self, input: Option<&str>) -> Result<(), ArgError> {
|
||||
// Unset or empty input clears the list
|
||||
let input = input.unwrap_or("");
|
||||
@@ -604,6 +614,18 @@ impl QuickTerminalSize {
|
||||
}
|
||||
|
||||
impl ConfigField for QuickTerminalSize {
|
||||
fn c_value(&self) -> CValue {
|
||||
let conv = |v: Option<QuickTerminalSizeValue>| match v {
|
||||
None => QuickTerminalSizeC::None,
|
||||
Some(QuickTerminalSizeValue::Percentage(p)) => QuickTerminalSizeC::Percentage(p),
|
||||
Some(QuickTerminalSizeValue::Pixels(p)) => QuickTerminalSizeC::Pixels(p),
|
||||
};
|
||||
CValue::QuickTerminalSize {
|
||||
primary: conv(self.primary),
|
||||
secondary: conv(self.secondary),
|
||||
}
|
||||
}
|
||||
|
||||
fn parse_cli(&mut self, input: Option<&str>) -> Result<(), ArgError> {
|
||||
let input = input.ok_or(ArgError::ValueRequired)?;
|
||||
let mut it = input.split(',');
|
||||
@@ -786,6 +808,17 @@ impl BackgroundBlur {
|
||||
}
|
||||
|
||||
impl ConfigField for BackgroundBlur {
|
||||
fn c_value(&self) -> CValue {
|
||||
// Sentinel values for the macOS glass styles, like Zig's `cval`.
|
||||
CValue::Short(match self {
|
||||
BackgroundBlur::False => 0,
|
||||
BackgroundBlur::True => 20,
|
||||
BackgroundBlur::Radius(v) => i16::from(*v),
|
||||
BackgroundBlur::MacosGlassRegular => -1,
|
||||
BackgroundBlur::MacosGlassClear => -2,
|
||||
})
|
||||
}
|
||||
|
||||
fn parse_cli(&mut self, input: Option<&str>) -> Result<(), ArgError> {
|
||||
let Some(input) = input else {
|
||||
// Emulate behavior for bools
|
||||
@@ -882,6 +915,10 @@ impl WindowDecoration {
|
||||
impl ConfigField for WindowDecoration {
|
||||
const VALID_VALUES: Option<&'static [&'static str]> = Some(Self::NAMES);
|
||||
|
||||
fn c_value(&self) -> CValue {
|
||||
CValue::EnumName(self.as_str())
|
||||
}
|
||||
|
||||
fn parse_cli(&mut self, input: Option<&str>) -> Result<(), ArgError> {
|
||||
*self = Self::parse(input)?;
|
||||
Ok(())
|
||||
@@ -1127,6 +1164,10 @@ impl fmt::Display for Duration {
|
||||
}
|
||||
|
||||
impl ConfigField for Duration {
|
||||
fn c_value(&self) -> CValue {
|
||||
CValue::Usize(self.as_milliseconds() as usize)
|
||||
}
|
||||
|
||||
fn parse_cli(&mut self, input: Option<&str>) -> Result<(), ArgError> {
|
||||
*self = Duration::parse(input)?;
|
||||
Ok(())
|
||||
@@ -1267,6 +1308,47 @@ mod tests {
|
||||
assert_eq!(list.value.len(), 0);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn repeatable_command_cval() {
|
||||
let mut list = RepeatableCommand::default();
|
||||
list.parse_cli(Some("title:Foo,action:ignore")).unwrap();
|
||||
list.parse_cli(Some("title:Bar,description:bobr,action:text:ale bydle"))
|
||||
.unwrap();
|
||||
assert_eq!(list.value.len(), 2);
|
||||
|
||||
let CValue::Commands(cv) = list.c_value() else {
|
||||
panic!("expected commands");
|
||||
};
|
||||
assert_eq!(cv.len(), 2);
|
||||
|
||||
// First entry
|
||||
assert_eq!(cv[0].title, "Foo");
|
||||
assert_eq!(cv[0].action_key, "ignore");
|
||||
assert_eq!(cv[0].action, "ignore");
|
||||
|
||||
// Second entry
|
||||
assert_eq!(cv[1].title, "Bar");
|
||||
assert_eq!(cv[1].description, "bobr");
|
||||
assert_eq!(cv[1].action_key, "text");
|
||||
assert_eq!(cv[1].action, "text:ale bydle");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn repeatable_command_cval_cleared() {
|
||||
let mut list = RepeatableCommand::default();
|
||||
list.parse_cli(Some("title:Foo,action:ignore")).unwrap();
|
||||
let CValue::Commands(cv) = list.c_value() else {
|
||||
panic!("expected commands");
|
||||
};
|
||||
assert_eq!(cv.len(), 1);
|
||||
|
||||
list.parse_cli(Some("")).unwrap();
|
||||
let CValue::Commands(cv) = list.c_value() else {
|
||||
panic!("expected commands");
|
||||
};
|
||||
assert_eq!(cv.len(), 0);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn repeatable_command_format_config_empty() {
|
||||
let list = RepeatableCommand::default();
|
||||
|
||||
Reference in new issue
Block a user