Handle OS file association: open .md files passed as CLI args
This commit is contained in:
@@ -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"}}
|
{"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"}}
|
||||||
@@ -1,6 +1,6 @@
|
|||||||
[package]
|
[package]
|
||||||
name = "vesper"
|
name = "vesper"
|
||||||
version = "0.1.0"
|
version = "1.0.1"
|
||||||
description = "A beautiful markdown reader"
|
description = "A beautiful markdown reader"
|
||||||
authors = ["you"]
|
authors = ["you"]
|
||||||
edition = "2021"
|
edition = "2021"
|
||||||
|
|||||||
@@ -3,7 +3,7 @@ use std::fs;
|
|||||||
use std::path::PathBuf;
|
use std::path::PathBuf;
|
||||||
|
|
||||||
use serde::{Deserialize, Serialize};
|
use serde::{Deserialize, Serialize};
|
||||||
use tauri::Manager;
|
use tauri::{Emitter, Manager};
|
||||||
|
|
||||||
#[derive(Serialize, Deserialize, Default)]
|
#[derive(Serialize, Deserialize, Default)]
|
||||||
struct WindowState {
|
struct WindowState {
|
||||||
@@ -72,6 +72,27 @@ pub fn run() {
|
|||||||
let _ = window.maximize();
|
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<String> = 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(())
|
Ok(())
|
||||||
})
|
})
|
||||||
.on_window_event(|window, event| {
|
.on_window_event(|window, event| {
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
{
|
{
|
||||||
"$schema": "https://schema.tauri.app/config/2",
|
"$schema": "https://schema.tauri.app/config/2",
|
||||||
"productName": "Vesper",
|
"productName": "Vesper",
|
||||||
"version": "1.0.0",
|
"version": "1.0.1",
|
||||||
"identifier": "com.vesper.reader",
|
"identifier": "com.vesper.reader",
|
||||||
"build": {
|
"build": {
|
||||||
"beforeDevCommand": "npm run dev",
|
"beforeDevCommand": "npm run dev",
|
||||||
|
|||||||
26
src/App.tsx
26
src/App.tsx
@@ -3,6 +3,7 @@ import { open } from "@tauri-apps/plugin-dialog";
|
|||||||
import { readTextFile } from "@tauri-apps/plugin-fs";
|
import { readTextFile } from "@tauri-apps/plugin-fs";
|
||||||
import { getCurrentWindow } from "@tauri-apps/api/window";
|
import { getCurrentWindow } from "@tauri-apps/api/window";
|
||||||
import { getCurrentWebview } from "@tauri-apps/api/webview";
|
import { getCurrentWebview } from "@tauri-apps/api/webview";
|
||||||
|
import { listen } from "@tauri-apps/api/event";
|
||||||
import MarkdownIt from "markdown-it";
|
import MarkdownIt from "markdown-it";
|
||||||
import hljs from "highlight.js";
|
import hljs from "highlight.js";
|
||||||
import { motion, AnimatePresence } from "framer-motion";
|
import { motion, AnimatePresence } from "framer-motion";
|
||||||
@@ -512,6 +513,31 @@ function App() {
|
|||||||
return () => { unlisten?.(); };
|
return () => { unlisten?.(); };
|
||||||
}, [parseHeadings]);
|
}, [parseHeadings]);
|
||||||
|
|
||||||
|
// Handle file opened via OS file association (e.g. double-clicking a .md file)
|
||||||
|
useEffect(() => {
|
||||||
|
let unlisten: (() => void) | undefined;
|
||||||
|
listen<string>('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 () => {
|
const handleOpenDialog = useCallback(async () => {
|
||||||
try {
|
try {
|
||||||
const selected = await open({ multiple: false, filters: [{ name: 'Markdown', extensions: ['md', 'markdown', 'txt'] }] });
|
const selected = await open({ multiple: false, filters: [{ name: 'Markdown', extensions: ['md', 'markdown', 'txt'] }] });
|
||||||
|
|||||||
Reference in New Issue
Block a user