From 32e4a3a4d79b3e8ae000ae8d9ab576438538188a Mon Sep 17 00:00:00 2001 From: Manoel Calixto Date: Mon, 1 Dec 2025 21:54:20 -0300 Subject: [PATCH] fix(tui): handle WSL clipboard image paths (#3990) Fixes #3939 Fixes #2803 ## Summary - convert Windows clipboard file paths into their `/mnt/` equivalents when running inside WSL so pasted images resolve correctly - add WSL detection helpers and share them with unit tests to cover both native Windows and WSL clipboard normalization cases - improve the test suite by exercising Windows path handling plus a dedicated WSL conversion scenario and keeping the code path guarded by targeted cfgs ## Testing - just fmt - cargo test -p codex-tui - cargo clippy -p codex-tui --tests - just fix -p codex-tui ## Screenshots _Codex TUI screenshot:_ describe this copied image _GitHub docs directory screenshot:_ image-copied Co-authored-by: Eric Traut --- codex-rs/tui/src/clipboard_paste.rs | 114 +++++++++++++++++++++++++++- 1 file changed, 110 insertions(+), 4 deletions(-) diff --git a/codex-rs/tui/src/clipboard_paste.rs b/codex-rs/tui/src/clipboard_paste.rs index 4945eea35..2a669f5c4 100644 --- a/codex-rs/tui/src/clipboard_paste.rs +++ b/codex-rs/tui/src/clipboard_paste.rs @@ -181,6 +181,14 @@ pub fn normalize_pasted_path(pasted: &str) -> Option { drive || unc }; if looks_like_windows_path { + #[cfg(target_os = "linux")] + { + if is_probably_wsl() + && let Some(converted) = convert_windows_path_to_wsl(pasted) + { + return Some(converted); + } + } return Some(PathBuf::from(pasted)); } @@ -193,6 +201,41 @@ pub fn normalize_pasted_path(pasted: &str) -> Option { None } +#[cfg(target_os = "linux")] +fn is_probably_wsl() -> bool { + std::env::var_os("WSL_DISTRO_NAME").is_some() + || std::env::var_os("WSL_INTEROP").is_some() + || std::env::var_os("WSLENV").is_some() +} + +#[cfg(target_os = "linux")] +fn convert_windows_path_to_wsl(input: &str) -> Option { + if input.starts_with("\\\\") { + return None; + } + + let drive_letter = input.chars().next()?.to_ascii_lowercase(); + if !drive_letter.is_ascii_lowercase() { + return None; + } + + if input.get(1..2) != Some(":") { + return None; + } + + let mut result = PathBuf::from(format!("/mnt/{drive_letter}")); + for component in input + .get(2..)? + .trim_start_matches(['\\', '/']) + .split(['\\', '/']) + .filter(|component| !component.is_empty()) + { + result.push(component); + } + + Some(result) +} + /// Infer an image format for the provided path based on its extension. pub fn pasted_image_format(path: &Path) -> EncodedImageFormat { match path @@ -210,6 +253,40 @@ pub fn pasted_image_format(path: &Path) -> EncodedImageFormat { #[cfg(test)] mod pasted_paths_tests { use super::*; + #[cfg(target_os = "linux")] + use std::ffi::OsString; + + #[cfg(target_os = "linux")] + struct EnvVarGuard { + key: &'static str, + original: Option, + } + + #[cfg(target_os = "linux")] + impl EnvVarGuard { + fn set(key: &'static str, value: &str) -> Self { + let original = std::env::var_os(key); + unsafe { + std::env::set_var(key, value); + } + Self { key, original } + } + } + + #[cfg(target_os = "linux")] + impl Drop for EnvVarGuard { + fn drop(&mut self) { + if let Some(original) = &self.original { + unsafe { + std::env::set_var(self.key, original); + } + } else { + unsafe { + std::env::remove_var(self.key); + } + } + } + } #[cfg(not(windows))] #[test] @@ -223,7 +300,17 @@ mod pasted_paths_tests { fn normalize_file_url_windows() { let input = r"C:\Temp\example.png"; let result = normalize_pasted_path(input).expect("should parse file URL"); - assert_eq!(result, PathBuf::from(r"C:\Temp\example.png")); + #[cfg(target_os = "linux")] + let expected = if is_probably_wsl() + && let Some(converted) = convert_windows_path_to_wsl(input) + { + converted + } else { + PathBuf::from(r"C:\Temp\example.png") + }; + #[cfg(not(target_os = "linux"))] + let expected = PathBuf::from(r"C:\Temp\example.png"); + assert_eq!(result, expected); } #[test] @@ -291,10 +378,17 @@ mod pasted_paths_tests { fn normalize_unquoted_windows_path_with_spaces() { let input = r"C:\\Users\\Alice\\My Pictures\\example image.png"; let result = normalize_pasted_path(input).expect("should accept unquoted windows path"); - assert_eq!( - result, + #[cfg(target_os = "linux")] + let expected = if is_probably_wsl() + && let Some(converted) = convert_windows_path_to_wsl(input) + { + converted + } else { PathBuf::from(r"C:\\Users\\Alice\\My Pictures\\example image.png") - ); + }; + #[cfg(not(target_os = "linux"))] + let expected = PathBuf::from(r"C:\\Users\\Alice\\My Pictures\\example image.png"); + assert_eq!(result, expected); } #[test] @@ -322,4 +416,16 @@ mod pasted_paths_tests { EncodedImageFormat::Other ); } + + #[cfg(target_os = "linux")] + #[test] + fn normalize_windows_path_in_wsl() { + let _guard = EnvVarGuard::set("WSL_DISTRO_NAME", "Ubuntu-24.04"); + let input = r"C:\\Users\\Alice\\Pictures\\example image.png"; + let result = normalize_pasted_path(input).expect("should convert windows path on wsl"); + assert_eq!( + result, + PathBuf::from("/mnt/c/Users/Alice/Pictures/example image.png") + ); + } }