diff --git a/src-tauri/src/commands.rs b/src-tauri/src/commands.rs index 2d652f7..030e0ab 100644 --- a/src-tauri/src/commands.rs +++ b/src-tauri/src/commands.rs @@ -2578,6 +2578,32 @@ pub async fn scan_project(working_dir: String) -> Result { }) } +#[tauri::command] +pub async fn open_binary_file(app: AppHandle, path: String) -> Result<(), String> { + use tauri_plugin_opener::OpenerExt; + + #[cfg(target_os = "windows")] + { + // Convert the WSL Linux path (e.g. /tmp/file.pdf) to a Windows UNC path + // (e.g. \\wsl.localhost\Ubuntu\tmp\file.pdf) so the Windows shell can open it. + let output = std::process::Command::new("wsl") + .args(["wslpath", "-w", &path]) + .output() + .map_err(|e| e.to_string())?; + let windows_path = String::from_utf8_lossy(&output.stdout).trim().to_string(); + app.opener() + .open_path(windows_path, None::<&str>) + .map_err(|e| e.to_string()) + } + + #[cfg(not(target_os = "windows"))] + { + app.opener() + .open_path(path, None::<&str>) + .map_err(|e| e.to_string()) + } +} + #[cfg(test)] mod tests { use super::*; @@ -3292,4 +3318,39 @@ gitea: gitea-mcp -t stdio (STDIO) - ✓ Connected"#; Some("Indented Heading".to_string()) ); } + + // ==================== open_binary_file E2E path conversion tests ==================== + + /// Build the wslpath command structure without executing it, for cross-platform CI testing. + #[cfg(test)] + fn build_wslpath_command(path: &str) -> (String, Vec) { + ( + "wsl".to_string(), + vec!["wslpath".to_string(), "-w".to_string(), path.to_string()], + ) + } + + #[test] + fn test_e2e_wslpath_command_structure_pdf() { + let (command, args) = build_wslpath_command("/tmp/mcp_output_abc123.pdf"); + assert_eq!(command, "wsl"); + assert_eq!(args.len(), 3); + assert_eq!(args[0], "wslpath"); + assert_eq!(args[1], "-w"); + assert_eq!(args[2], "/tmp/mcp_output_abc123.pdf"); + } + + #[test] + fn test_e2e_wslpath_command_structure_audio() { + let (command, args) = build_wslpath_command("/tmp/mcp_output_xyz789.mp3"); + assert_eq!(command, "wsl"); + assert_eq!(args[2], "/tmp/mcp_output_xyz789.mp3"); + } + + #[test] + fn test_e2e_wslpath_command_structure_preserves_path() { + let path = "/home/naomi/documents/report with spaces.pdf"; + let (_, args) = build_wslpath_command(path); + assert_eq!(args[2], path); + } } diff --git a/src-tauri/src/lib.rs b/src-tauri/src/lib.rs index 7cec226..0e1ec59 100644 --- a/src-tauri/src/lib.rs +++ b/src-tauri/src/lib.rs @@ -223,6 +223,7 @@ pub fn run() { delete_draft, delete_all_drafts, scan_project, + open_binary_file, ]) .run(tauri::generate_context!()) .expect("error while running tauri application"); diff --git a/src/lib/components/Markdown.svelte b/src/lib/components/Markdown.svelte index c0fa9ae..0117bfe 100644 --- a/src/lib/components/Markdown.svelte +++ b/src/lib/components/Markdown.svelte @@ -2,7 +2,8 @@ import { marked } from "marked"; import hljs from "highlight.js"; import { onMount } from "svelte"; - import { openUrl, openPath } from "@tauri-apps/plugin-opener"; + import { openUrl } from "@tauri-apps/plugin-opener"; + import { invoke } from "@tauri-apps/api/core"; import { clipboardStore } from "$lib/stores/clipboard"; import { linkifyFilePaths } from "$lib/utils/filePaths"; @@ -148,7 +149,7 @@ const filePath = anchor.dataset.filepath; if (filePath) { - void openPath(filePath); + void invoke("open_binary_file", { path: filePath }); return; } diff --git a/src/lib/components/Markdown.test.ts b/src/lib/components/Markdown.test.ts index 56b78c5..e2eb441 100644 --- a/src/lib/components/Markdown.test.ts +++ b/src/lib/components/Markdown.test.ts @@ -9,7 +9,8 @@ * - [ ] Code blocks render with syntax highlighting and a copy button * - [ ] ||spoiler text|| renders as a hidden span revealed on click * - [ ] Search query highlights matching text in non-code content - * - [ ] Links open in the system browser via the Tauri opener + * - [ ] Regular links open in the system browser via the Tauri opener + * - [ ] Binary file links invoke open_binary_file (WSL-path-aware) instead of openPath */ import { describe, it, expect } from "vitest";