fix-deviant(config): accept absolute Windows theme paths

Intentional deviation from Ghostty 1.3.1 (DEVIANT_FIXES.md row 38).
Zig's Theme.parseCLI (config/Config.zig:9788-9799) treats any value
containing ',', '=' or ':' as a light/dark pair. Every absolute Windows
path (C:\themes\x, \\?\C:\..., \\server\share\x) contains a colon, so
Ghostty on Windows rejects all absolute theme paths.

A value that is a single Windows absolute path is now taken as a theme
path; the light:/dark: pair syntax still works, including with drive
paths as values.

Test Plan:
- cargo test -p ghostty-config windows_absolute_path_detection
  theme_windows_absolute_path_and_pair_with_paths
This commit is contained in:
ddidderr committed 2026-10-12 17:23:38 +02:00
1 parent 5c5962ea95
commit e4a6ee7bf4
2 files changed
+70

No files matched your search

+69
View File
@@ -974,6 +974,19 @@ impl ConfigField for Theme {
// we're parsing a light/dark mode theme pair. Note that "=" isn't
// actually valid for setting a light/dark mode pair but I anticipate
// it'll be a common typo.
//
// DEVIANT (Windows, see DEVIANT_FIXES.md): an absolute Windows path
// (`C:\themes\x`, `\\?\C:\...`, `\\server\share\x`) contains a
// colon, so Ghostty would always parse it as a malformed pair and
// reject every absolute theme path on Windows.
if cfg!(windows) && is_windows_absolute_path(input) {
let trimmed = crate::cli_args::trim_bytes(input);
*self = Theme {
light: EncodedOs::from_bytes(trimmed),
dark: EncodedOs::from_bytes(trimmed),
};
return Ok(());
}
if input.iter().any(|b| matches!(b, b',' | b'=' | b':')) {
let mut light: Option<EncodedOs> = None;
let mut dark: Option<EncodedOs> = None;
@@ -1017,6 +1030,17 @@ impl ConfigField for Theme {
}
}
/// Whether `input` (after leading whitespace) is a drive-letter (`C:\` or
/// `C:/`) or UNC / device (`\\...`) absolute path.
fn is_windows_absolute_path(input: &[u8]) -> bool {
let input = crate::cli_args::trim_bytes(input);
match input {
[b'\\', b'\\', ..] => true,
[d, b':', b'\\' | b'/', ..] => d.is_ascii_alphabetic(),
_ => false,
}
}
/// A duration, stored in nanoseconds.
#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
pub struct Duration {
@@ -1289,6 +1313,51 @@ config_packed! {
#[cfg(test)]
#[allow(clippy::unwrap_used, clippy::approx_constant)]
mod tests {
#[test]
fn windows_absolute_path_detection() {
for p in [
r"C:\themes\x",
"d:/themes/x",
r"\\?\C:\t\x",
r"\\srv\share\x",
r" C:\x",
] {
assert!(is_windows_absolute_path(p.as_bytes()), "{p}");
}
for p in [
"foo",
"light:foo,dark:bar",
"light:C:\\x",
"/abs/x",
"C:",
"1:\\x",
"C:x",
] {
assert!(!is_windows_absolute_path(p.as_bytes()), "{p}");
}
}
#[test]
fn theme_windows_absolute_path_and_pair_with_paths() {
let mut t = Theme::default();
let r = t.parse_bytes(Some(br"C:\themes\my theme"));
if cfg!(windows) {
r.unwrap();
assert_eq!(t.light.as_encoded(), r"C:\themes\my theme");
assert_eq!(t.dark, t.light);
// The pair syntax still works with drive paths as values.
let mut t = Theme::default();
// (Backslashes are escapes in the pair syntax: quote them.)
t.parse_bytes(Some(br#"light:"C:\\a",dark:"C:\\b""#))
.unwrap();
assert_eq!(t.light.as_encoded(), r"C:\a");
assert_eq!(t.dark.as_encoded(), r"C:\b");
} else {
// Zig behaviour everywhere else: a colon means "pair".
assert!(r.is_err());
}
}
#[test]
fn quick_terminal_size_value_formats_nan_like_zig() {
let mut out = String::new();
+1
View File
@@ -63,6 +63,7 @@ when this file was written.
| 35 | Termio: PTY hangup drops unread output | `termio/Exec.zig` `ReadThread.threadMainPosix` (exits as soon as `POLL.HUP` is seen) | `POLLIN \| POLLHUP` can arrive together with unread final output (a Linux PTY check confirmed this); exiting immediately loses the tail of the child's output. | After HUP the read thread makes one more pass over the fd, so the remaining output is delivered before exit. | 6629fb0 | none (needs a live PTY; verified by RV18 on Linux) |
| 36 | Font (wasm/browser Canvas): atlas pixel format | `font/face/web_canvas.zig` (atlas write branch on `.rgba`) | The branch tests a stale `.rgba` format against an enum that contains `.bgra`, so BGRA atlases are never converted. | The BGRA atlas conversion is implemented. No claim of equivalence with a real browser (not testable here). | d89691b | `ghostty-font` web_canvas tests (compile/Node level) |
| 37 | Termio: subprocess cleanup | `termio/Exec.zig:1188-1224` (`getpgid`) | Loops forever while `getpgid(child) == our pgid`, i.e. also when the child died before `setsid` and is an unreaped zombie (its pgid stays ours), hanging the surface close. | Still waits without a cap (no 5 s limit) while the child is alive, but stops (no SIGHUP, nothing to kill) once `waitid(WNOWAIT)` shows the child exited. | uncommitted (P3) | `getpgid_gives_up_on_exited_child_without_setsid`, `getpgid_waits_for_late_setsid` (`ghostty-termio/src/exec.rs`) |
| 38 | Config (Windows): absolute theme paths | `config/Config.zig:9788-9799` (`Theme.parseCLI`) | Any value containing `,`, `=` or `:` is parsed as a `light:..,dark:..` pair, and a Windows absolute path (`C:\\themes\\x`, `\\\\?\\C:\\...`) always contains a colon, so `theme = <absolute path>` is rejected on Windows (found by running the config tests under Wine). | On Windows a value that starts like an absolute Windows path (drive letter + `:` + separator, or `\\\\`) is a single theme; the pair syntax is unchanged (quote backslashes). Other platforms are unchanged. | uncommitted (agent WINE) | `windows_absolute_path_detection`, `theme_windows_absolute_path_and_pair_with_paths` (`ghostty-config/src/types_b.rs`), `theme_loading` under Wine |
## Upstream quirks deliberately kept (not fixed)