Skip to content

Commit 32e4a3a

Browse files
fix(tui): handle WSL clipboard image paths (#3990)
Fixes #3939 Fixes #2803 ## Summary - convert Windows clipboard file paths into their `/mnt/<drive>` 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:_ <img width="1880" height="848" alt="describe this copied image" src="https://github.com/user-attachments/assets/c620d43c-f45c-451e-8893-e56ae85a5eea" /> _GitHub docs directory screenshot:_ <img width="1064" height="478" alt="image-copied" src="https://github.com/user-attachments/assets/eb5eef6c-eb43-45a0-8bfe-25c35bcae753" /> Co-authored-by: Eric Traut <[email protected]>
1 parent f443555 commit 32e4a3a

File tree

1 file changed

+110
-4
lines changed

1 file changed

+110
-4
lines changed

codex-rs/tui/src/clipboard_paste.rs

Lines changed: 110 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -181,6 +181,14 @@ pub fn normalize_pasted_path(pasted: &str) -> Option<PathBuf> {
181181
drive || unc
182182
};
183183
if looks_like_windows_path {
184+
#[cfg(target_os = "linux")]
185+
{
186+
if is_probably_wsl()
187+
&& let Some(converted) = convert_windows_path_to_wsl(pasted)
188+
{
189+
return Some(converted);
190+
}
191+
}
184192
return Some(PathBuf::from(pasted));
185193
}
186194

@@ -193,6 +201,41 @@ pub fn normalize_pasted_path(pasted: &str) -> Option<PathBuf> {
193201
None
194202
}
195203

204+
#[cfg(target_os = "linux")]
205+
fn is_probably_wsl() -> bool {
206+
std::env::var_os("WSL_DISTRO_NAME").is_some()
207+
|| std::env::var_os("WSL_INTEROP").is_some()
208+
|| std::env::var_os("WSLENV").is_some()
209+
}
210+
211+
#[cfg(target_os = "linux")]
212+
fn convert_windows_path_to_wsl(input: &str) -> Option<PathBuf> {
213+
if input.starts_with("\\\\") {
214+
return None;
215+
}
216+
217+
let drive_letter = input.chars().next()?.to_ascii_lowercase();
218+
if !drive_letter.is_ascii_lowercase() {
219+
return None;
220+
}
221+
222+
if input.get(1..2) != Some(":") {
223+
return None;
224+
}
225+
226+
let mut result = PathBuf::from(format!("/mnt/{drive_letter}"));
227+
for component in input
228+
.get(2..)?
229+
.trim_start_matches(['\\', '/'])
230+
.split(['\\', '/'])
231+
.filter(|component| !component.is_empty())
232+
{
233+
result.push(component);
234+
}
235+
236+
Some(result)
237+
}
238+
196239
/// Infer an image format for the provided path based on its extension.
197240
pub fn pasted_image_format(path: &Path) -> EncodedImageFormat {
198241
match path
@@ -210,6 +253,40 @@ pub fn pasted_image_format(path: &Path) -> EncodedImageFormat {
210253
#[cfg(test)]
211254
mod pasted_paths_tests {
212255
use super::*;
256+
#[cfg(target_os = "linux")]
257+
use std::ffi::OsString;
258+
259+
#[cfg(target_os = "linux")]
260+
struct EnvVarGuard {
261+
key: &'static str,
262+
original: Option<OsString>,
263+
}
264+
265+
#[cfg(target_os = "linux")]
266+
impl EnvVarGuard {
267+
fn set(key: &'static str, value: &str) -> Self {
268+
let original = std::env::var_os(key);
269+
unsafe {
270+
std::env::set_var(key, value);
271+
}
272+
Self { key, original }
273+
}
274+
}
275+
276+
#[cfg(target_os = "linux")]
277+
impl Drop for EnvVarGuard {
278+
fn drop(&mut self) {
279+
if let Some(original) = &self.original {
280+
unsafe {
281+
std::env::set_var(self.key, original);
282+
}
283+
} else {
284+
unsafe {
285+
std::env::remove_var(self.key);
286+
}
287+
}
288+
}
289+
}
213290

214291
#[cfg(not(windows))]
215292
#[test]
@@ -223,7 +300,17 @@ mod pasted_paths_tests {
223300
fn normalize_file_url_windows() {
224301
let input = r"C:\Temp\example.png";
225302
let result = normalize_pasted_path(input).expect("should parse file URL");
226-
assert_eq!(result, PathBuf::from(r"C:\Temp\example.png"));
303+
#[cfg(target_os = "linux")]
304+
let expected = if is_probably_wsl()
305+
&& let Some(converted) = convert_windows_path_to_wsl(input)
306+
{
307+
converted
308+
} else {
309+
PathBuf::from(r"C:\Temp\example.png")
310+
};
311+
#[cfg(not(target_os = "linux"))]
312+
let expected = PathBuf::from(r"C:\Temp\example.png");
313+
assert_eq!(result, expected);
227314
}
228315

229316
#[test]
@@ -291,10 +378,17 @@ mod pasted_paths_tests {
291378
fn normalize_unquoted_windows_path_with_spaces() {
292379
let input = r"C:\\Users\\Alice\\My Pictures\\example image.png";
293380
let result = normalize_pasted_path(input).expect("should accept unquoted windows path");
294-
assert_eq!(
295-
result,
381+
#[cfg(target_os = "linux")]
382+
let expected = if is_probably_wsl()
383+
&& let Some(converted) = convert_windows_path_to_wsl(input)
384+
{
385+
converted
386+
} else {
296387
PathBuf::from(r"C:\\Users\\Alice\\My Pictures\\example image.png")
297-
);
388+
};
389+
#[cfg(not(target_os = "linux"))]
390+
let expected = PathBuf::from(r"C:\\Users\\Alice\\My Pictures\\example image.png");
391+
assert_eq!(result, expected);
298392
}
299393

300394
#[test]
@@ -322,4 +416,16 @@ mod pasted_paths_tests {
322416
EncodedImageFormat::Other
323417
);
324418
}
419+
420+
#[cfg(target_os = "linux")]
421+
#[test]
422+
fn normalize_windows_path_in_wsl() {
423+
let _guard = EnvVarGuard::set("WSL_DISTRO_NAME", "Ubuntu-24.04");
424+
let input = r"C:\\Users\\Alice\\Pictures\\example image.png";
425+
let result = normalize_pasted_path(input).expect("should convert windows path on wsl");
426+
assert_eq!(
427+
result,
428+
PathBuf::from("/mnt/c/Users/Alice/Pictures/example image.png")
429+
);
430+
}
325431
}

0 commit comments

Comments
 (0)