From c2ca2b123feca1c7dd06b2b669c06ce6120073b2 Mon Sep 17 00:00:00 2001 From: zhao-oai Date: Sun, 26 Oct 2025 15:40:42 -0700 Subject: [PATCH] Fix image path normalization for mac screenshots --- codex-rs/tui/src/clipboard_paste.rs | 89 +++++++++++++++++++++++++++++ 1 file changed, 89 insertions(+) diff --git a/codex-rs/tui/src/clipboard_paste.rs b/codex-rs/tui/src/clipboard_paste.rs index 4945eea350..2f878b50dd 100644 --- a/codex-rs/tui/src/clipboard_paste.rs +++ b/codex-rs/tui/src/clipboard_paste.rs @@ -150,6 +150,9 @@ pub fn paste_image_to_temp_png() -> Result<(PathBuf, PastedImageInfo), PasteImag /// - shell-escaped single paths (via `shlex`) pub fn normalize_pasted_path(pasted: &str) -> Option { let pasted = pasted.trim(); + if pasted.is_empty() { + return None; + } // file:// URL → filesystem path if let Ok(url) = url::Url::parse(pasted) @@ -190,6 +193,10 @@ pub fn normalize_pasted_path(pasted: &str) -> Option { return parts.into_iter().next().map(PathBuf::from); } + if !contains_unescaped_ascii_whitespace(pasted) { + return shell_style_single_path(pasted); + } + None } @@ -207,6 +214,71 @@ pub fn pasted_image_format(path: &Path) -> EncodedImageFormat { } } +fn contains_unescaped_ascii_whitespace(pasted: &str) -> bool { + let mut escaped = false; + let mut in_single_quote = false; + let mut in_double_quote = false; + + for c in pasted.chars() { + if escaped { + escaped = false; + continue; + } + match c { + '\\' if !in_single_quote => { + escaped = true; + } + '\'' if !in_double_quote => { + in_single_quote = !in_single_quote; + } + '"' if !in_single_quote => { + in_double_quote = !in_double_quote; + } + ' ' | '\t' | '\r' | '\n' | '\u{000B}' | '\u{000C}' + if !in_single_quote && !in_double_quote => + { + return true; + } + _ => {} + } + } + + false +} + +fn shell_style_single_path(pasted: &str) -> Option { + let mut result = String::with_capacity(pasted.len()); + let mut chars = pasted.chars(); + let mut in_single_quote = false; + let mut in_double_quote = false; + + while let Some(c) = chars.next() { + if c == '\\' && !in_single_quote { + if let Some(next) = chars.next() { + result.push(next); + } else { + result.push('\\'); + } + continue; + } + if c == '\'' && !in_double_quote { + in_single_quote = !in_single_quote; + continue; + } + if c == '"' && !in_single_quote { + in_double_quote = !in_double_quote; + continue; + } + result.push(c); + } + + if in_single_quote || in_double_quote { + return None; + } + + Some(PathBuf::from(result)) +} + #[cfg(test)] mod pasted_paths_tests { use super::*; @@ -307,6 +379,23 @@ mod pasted_paths_tests { ); } + #[test] + fn normalize_path_with_narrow_no_break_space() { + let input = "/Users/zhao/Downloads/Screenshot\\ 2025-09-25\\ at\\ 10.47.39\u{202F}AM.png"; + let expected = + PathBuf::from("/Users/zhao/Downloads/Screenshot 2025-09-25 at 10.47.39\u{202F}AM.png"); + let result = normalize_pasted_path(input).expect("should handle narrow no-break space"); + assert_eq!(result, expected); + } + + #[test] + fn normalize_single_path_with_nbsp_and_quotes() { + let input = "'/Users/zhao/Documents/My\u{00A0}File.png'"; + let expected = PathBuf::from("/Users/zhao/Documents/My\u{00A0}File.png"); + let result = normalize_pasted_path(input).expect("should handle quoted non-breaking space"); + assert_eq!(result, expected); + } + #[test] fn pasted_image_format_with_windows_style_paths() { assert_eq!(