From 97a262dbe635762885c40605ba6be54bb52d691d Mon Sep 17 00:00:00 2001 From: ddidderr Date: Mon, 12 Oct 2026 17:23:38 +0200 Subject: [PATCH] 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 --- crates/ghostty-config/src/types_b.rs | 69 ++++++++++++++++++++++++++++ docs/rewrite/DEVIANT_FIXES.md | 1 + 2 files changed, 70 insertions(+) diff --git a/crates/ghostty-config/src/types_b.rs b/crates/ghostty-config/src/types_b.rs index 16d3dd7..ebd5f9e 100644 --- a/crates/ghostty-config/src/types_b.rs +++ b/crates/ghostty-config/src/types_b.rs @@ -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 = None; let mut dark: Option = 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(); diff --git a/docs/rewrite/DEVIANT_FIXES.md b/docs/rewrite/DEVIANT_FIXES.md index b0109ad..bbb4bf5 100644 --- a/docs/rewrite/DEVIANT_FIXES.md +++ b/docs/rewrite/DEVIANT_FIXES.md @@ -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 = ` 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)