From f462ca8d03848461339c71bb090f3a95fa21647b Mon Sep 17 00:00:00 2001 From: Your Name Date: Sat, 14 Feb 2026 19:45:58 +0200 Subject: [PATCH] Handle OS file association: open .md files passed as CLI args --- package.json | 2 +- src-tauri/Cargo.toml | 2 +- src-tauri/src/lib.rs | 23 ++++++++++++++++++++++- src-tauri/tauri.conf.json | 2 +- src/App.tsx | 26 ++++++++++++++++++++++++++ 5 files changed, 51 insertions(+), 4 deletions(-) diff --git a/package.json b/package.json index d90b5bb..682a7af 100644 --- a/package.json +++ b/package.json @@ -1 +1 @@ -{"name":"vesper","private":true,"version":"1.0.0","type":"module","scripts":{"dev":"vite","build":"tsc && vite build","preview":"vite preview","tauri":"tauri"},"dependencies":{"@fontsource-variable/inter":"^5.2.8","@fontsource-variable/jetbrains-mono":"^5.2.8","@tailwindcss/typography":"^0.5.19","@tailwindcss/vite":"^4.1.18","@tauri-apps/api":"^2","@tauri-apps/plugin-dialog":"^2.6.0","@tauri-apps/plugin-fs":"^2.4.5","@tauri-apps/plugin-opener":"^2","daisyui":"^5.5.18","framer-motion":"^12.34.0","highlight.js":"^11.11.1","lucide-react":"^0.564.0","markdown-it":"^14.1.1","markdown-it-mark":"^4.0.0","markdown-it-sub":"^2.0.0","markdown-it-sup":"^2.0.0","markdown-it-task-lists":"^2.1.1","overlayscrollbars":"^2.14.0","overlayscrollbars-react":"^0.5.6","react":"^19.1.0","react-dom":"^19.1.0","tailwindcss":"^4.1.18"},"devDependencies":{"@tauri-apps/cli":"^2","@types/markdown-it":"^14.1.2","@types/react":"^19.1.8","@types/react-dom":"^19.1.6","@vitejs/plugin-react":"^4.6.0","png-to-ico":"^3.0.1","puppeteer-core":"^24.37.3","sharp":"^0.34.5","typescript":"~5.8.3","vite":"^7.0.4"}} \ No newline at end of file +{"name":"vesper","private":true,"version":"1.0.1","type":"module","scripts":{"dev":"vite","build":"tsc && vite build","preview":"vite preview","tauri":"tauri"},"dependencies":{"@fontsource-variable/inter":"^5.2.8","@fontsource-variable/jetbrains-mono":"^5.2.8","@tailwindcss/typography":"^0.5.19","@tailwindcss/vite":"^4.1.18","@tauri-apps/api":"^2","@tauri-apps/plugin-dialog":"^2.6.0","@tauri-apps/plugin-fs":"^2.4.5","@tauri-apps/plugin-opener":"^2","daisyui":"^5.5.18","framer-motion":"^12.34.0","highlight.js":"^11.11.1","lucide-react":"^0.564.0","markdown-it":"^14.1.1","markdown-it-mark":"^4.0.0","markdown-it-sub":"^2.0.0","markdown-it-sup":"^2.0.0","markdown-it-task-lists":"^2.1.1","overlayscrollbars":"^2.14.0","overlayscrollbars-react":"^0.5.6","react":"^19.1.0","react-dom":"^19.1.0","tailwindcss":"^4.1.18"},"devDependencies":{"@tauri-apps/cli":"^2","@types/markdown-it":"^14.1.2","@types/react":"^19.1.8","@types/react-dom":"^19.1.6","@vitejs/plugin-react":"^4.6.0","png-to-ico":"^3.0.1","puppeteer-core":"^24.37.3","sharp":"^0.34.5","typescript":"~5.8.3","vite":"^7.0.4"}} \ No newline at end of file diff --git a/src-tauri/Cargo.toml b/src-tauri/Cargo.toml index 6dd7da0..4894863 100644 --- a/src-tauri/Cargo.toml +++ b/src-tauri/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "vesper" -version = "0.1.0" +version = "1.0.1" description = "A beautiful markdown reader" authors = ["you"] edition = "2021" diff --git a/src-tauri/src/lib.rs b/src-tauri/src/lib.rs index 3e44904..7068e58 100644 --- a/src-tauri/src/lib.rs +++ b/src-tauri/src/lib.rs @@ -3,7 +3,7 @@ use std::fs; use std::path::PathBuf; use serde::{Deserialize, Serialize}; -use tauri::Manager; +use tauri::{Emitter, Manager}; #[derive(Serialize, Deserialize, Default)] struct WindowState { @@ -72,6 +72,27 @@ pub fn run() { let _ = window.maximize(); } + // If launched with a file argument (e.g. double-clicking a .md file), + // emit the path to the frontend so it can open it as a tab. + let args: Vec = env::args().collect(); + if let Some(file_arg) = args.get(1) { + let path = PathBuf::from(file_arg); + if path.is_file() { + if let Some(ext) = path.extension().and_then(|e| e.to_str()) { + let ext_lower = ext.to_lowercase(); + if ext_lower == "md" || ext_lower == "markdown" || ext_lower == "txt" { + let path_str = path.to_string_lossy().to_string(); + let win = window.clone(); + // Emit after a short delay so the frontend has time to set up listeners + std::thread::spawn(move || { + std::thread::sleep(std::time::Duration::from_millis(500)); + let _ = win.emit("open-file", path_str); + }); + } + } + } + } + Ok(()) }) .on_window_event(|window, event| { diff --git a/src-tauri/tauri.conf.json b/src-tauri/tauri.conf.json index 44e6c15..05c5b2f 100644 --- a/src-tauri/tauri.conf.json +++ b/src-tauri/tauri.conf.json @@ -1,7 +1,7 @@ { "$schema": "https://schema.tauri.app/config/2", "productName": "Vesper", - "version": "1.0.0", + "version": "1.0.1", "identifier": "com.vesper.reader", "build": { "beforeDevCommand": "npm run dev", diff --git a/src/App.tsx b/src/App.tsx index 141a926..7ce4a40 100644 --- a/src/App.tsx +++ b/src/App.tsx @@ -3,6 +3,7 @@ import { open } from "@tauri-apps/plugin-dialog"; import { readTextFile } from "@tauri-apps/plugin-fs"; import { getCurrentWindow } from "@tauri-apps/api/window"; import { getCurrentWebview } from "@tauri-apps/api/webview"; +import { listen } from "@tauri-apps/api/event"; import MarkdownIt from "markdown-it"; import hljs from "highlight.js"; import { motion, AnimatePresence } from "framer-motion"; @@ -512,6 +513,31 @@ function App() { return () => { unlisten?.(); }; }, [parseHeadings]); + // Handle file opened via OS file association (e.g. double-clicking a .md file) + useEffect(() => { + let unlisten: (() => void) | undefined; + listen('open-file', async (event) => { + const filePath = event.payload; + try { + const existing = tabsRef.current.find(t => t.path === filePath); + if (existing) { + setActiveTabId(existing.id); + parseHeadings(existing.content); + } else { + const content = await readTextFile(filePath); + const title = filePath.split(/[/\\]/).pop() || 'Untitled'; + const newTab: Tab = { id: Date.now().toString(), title, content, path: filePath }; + setTabs(prev => [...prev, newTab]); + setActiveTabId(newTab.id); + parseHeadings(content); + } + } catch (err) { + console.error('Failed to open file from OS:', err); + } + }).then(fn => { unlisten = fn; }); + return () => { unlisten?.(); }; + }, [parseHeadings]); + const handleOpenDialog = useCallback(async () => { try { const selected = await open({ multiple: false, filters: [{ name: 'Markdown', extensions: ['md', 'markdown', 'txt'] }] });