Skip to content

Commit 5ddbf04

Browse files
authored
Merge pull request #2 from vanzan01/fix/macos-file-association
Fix/macos file association
2 parents 09b06d3 + 62305a6 commit 5ddbf04

3 files changed

Lines changed: 170 additions & 23 deletions

File tree

.github/workflows/test.yml

Lines changed: 54 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,54 @@
1+
name: Test Build
2+
3+
on:
4+
pull_request:
5+
branches: [ main ]
6+
push:
7+
branches: [ main, 'fix/*', 'feature/*' ]
8+
9+
jobs:
10+
test-build:
11+
strategy:
12+
fail-fast: false
13+
matrix:
14+
platform: [windows-latest, macos-latest]
15+
runs-on: ${{ matrix.platform }}
16+
steps:
17+
- uses: actions/checkout@v4
18+
19+
- name: Setup Node.js
20+
uses: actions/setup-node@v4
21+
with:
22+
node-version: '18'
23+
cache: 'npm'
24+
25+
- name: Install Rust
26+
uses: dtolnay/rust-toolchain@stable
27+
28+
29+
- name: Install npm dependencies
30+
run: npm ci
31+
32+
- name: Build Tauri app
33+
run: npm run tauri build
34+
35+
- name: Upload Windows artifacts
36+
if: matrix.platform == 'windows-latest'
37+
uses: actions/upload-artifact@v4
38+
with:
39+
name: test-windows-artifacts
40+
path: |
41+
src-tauri/target/release/bundle/msi/*.msi
42+
src-tauri/target/release/bundle/nsis/*.exe
43+
src-tauri/target/release/markdown-viewer.exe
44+
retention-days: 7
45+
46+
- name: Upload macOS artifacts
47+
if: matrix.platform == 'macos-latest'
48+
uses: actions/upload-artifact@v4
49+
with:
50+
name: test-macos-artifacts
51+
path: |
52+
src-tauri/target/release/bundle/dmg/*.dmg
53+
src-tauri/target/release/bundle/macos/*.app
54+
retention-days: 7

src-tauri/src/lib.rs

Lines changed: 96 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,9 @@ use std::env;
44
use std::path::{Path, PathBuf};
55
use std::sync::{Arc, Mutex};
66
use notify::{Watcher, RecommendedWatcher, RecursiveMode, Event, EventKind};
7-
use tauri::{AppHandle, Emitter};
7+
use tauri::{AppHandle, Emitter, Manager};
8+
#[cfg(any(target_os = "macos", target_os = "ios"))]
9+
use tauri::RunEvent;
810
use syntect::parsing::SyntaxSet;
911
use syntect::highlighting::ThemeSet;
1012
use syntect::html::highlighted_html_for_string;
@@ -20,6 +22,29 @@ const MAX_HTML_SIZE: usize = 100 * 1024 * 1024; // 100MB HTML limit for temp fil
2022
// Global state for file watcher
2123
type WatcherState = Arc<Mutex<Option<RecommendedWatcher>>>;
2224

25+
// App state to store file opened via "Open With" on macOS
26+
#[derive(Default)]
27+
struct OpenedFileState {
28+
file_path: Arc<Mutex<Option<String>>>,
29+
}
30+
31+
impl OpenedFileState {
32+
fn set_file(&self, path: String) {
33+
let mut file_path = self.file_path.lock().unwrap();
34+
*file_path = Some(path);
35+
}
36+
37+
fn get_file(&self) -> Option<String> {
38+
let file_path = self.file_path.lock().unwrap();
39+
file_path.clone()
40+
}
41+
42+
fn clear_file(&self) {
43+
let mut file_path = self.file_path.lock().unwrap();
44+
*file_path = None;
45+
}
46+
}
47+
2348
// Security utilities for temp file handling
2449
fn create_secure_temp_file(content: &str) -> Result<PathBuf, String> {
2550
// Basic size validation only
@@ -386,6 +411,16 @@ fn get_launch_args() -> Vec<String> {
386411
env::args().collect()
387412
}
388413

414+
#[tauri::command]
415+
fn get_opened_file(state: tauri::State<OpenedFileState>) -> Option<String> {
416+
let file = state.get_file();
417+
if file.is_some() {
418+
// Clear the file after retrieving it so it's only opened once
419+
state.clear_file();
420+
}
421+
file
422+
}
423+
389424
#[tauri::command]
390425
fn export_html(content: String, title: String) -> Result<String, String> {
391426
let html_template = format!(r#"<!DOCTYPE html>
@@ -664,23 +699,81 @@ async fn save_temp_html_and_open(html_content: String) -> Result<(), String> {
664699
#[cfg_attr(mobile, tauri::mobile_entry_point)]
665700
pub fn run() {
666701
let watcher_state: WatcherState = Arc::new(Mutex::new(None));
702+
let opened_file_state = OpenedFileState::default();
667703

668704
tauri::Builder::default()
669705
.plugin(tauri_plugin_opener::init())
670706
.plugin(tauri_plugin_dialog::init())
671707
.plugin(tauri_plugin_fs::init())
672708
.manage(watcher_state)
709+
.manage(opened_file_state)
673710
.invoke_handler(tauri::generate_handler![
674711
greet,
675712
parse_markdown,
676713
read_markdown_file,
677714
get_launch_args,
715+
get_opened_file,
678716
start_watching_file,
679717
stop_watching_file,
680718
export_html,
681719
read_file_content,
682720
save_temp_html_and_open
683721
])
684-
.run(tauri::generate_context!())
685-
.expect("error while running tauri application");
722+
.setup(|app| {
723+
// Check command line args during setup (fallback for other platforms)
724+
let setup_args = env::args().collect::<Vec<String>>();
725+
726+
// Check for markdown files in args
727+
for arg in setup_args.iter().skip(1) {
728+
if arg.ends_with(".md") || arg.ends_with(".markdown") ||
729+
arg.ends_with(".mdown") || arg.ends_with(".mkd") {
730+
// For command line arguments, store in the opened file state
731+
let opened_file_state = app.state::<OpenedFileState>();
732+
opened_file_state.set_file(arg.clone());
733+
break;
734+
}
735+
}
736+
737+
Ok(())
738+
})
739+
.build(tauri::generate_context!())
740+
.expect("error while building tauri application")
741+
.run(|_app_handle, event| {
742+
match event {
743+
// RunEvent::Opened is only available on macOS and iOS
744+
#[cfg(any(target_os = "macos", target_os = "ios"))]
745+
RunEvent::Opened { urls } => {
746+
let app_handle = _app_handle;
747+
// Find the first markdown file in the opened URLs
748+
for url in urls {
749+
// Convert URL to string and handle file:// URLs
750+
let url_str = url.as_str();
751+
let file_path = if url_str.starts_with("file://") {
752+
url_str.trim_start_matches("file://").to_string()
753+
} else {
754+
url_str.to_string()
755+
};
756+
757+
if file_path.ends_with(".md") || file_path.ends_with(".markdown") ||
758+
file_path.ends_with(".mdown") || file_path.ends_with(".mkd") {
759+
// Validate the file path for security
760+
if let Ok(validated_path) = validate_file_path(&file_path) {
761+
let validated_str = validated_path.to_string_lossy().to_string();
762+
763+
// Store the opened file in app state
764+
let opened_file_state = app_handle.state::<OpenedFileState>();
765+
opened_file_state.set_file(validated_str.clone());
766+
767+
// Also try to emit the event to the frontend if it's ready
768+
let _ = app_handle.emit("file-opened-via-os", &validated_str);
769+
}
770+
break;
771+
}
772+
}
773+
}
774+
_ => {
775+
// Handle other events as needed
776+
}
777+
}
778+
});
686779
}

src/main.js

Lines changed: 20 additions & 20 deletions
Original file line numberDiff line numberDiff line change
@@ -1176,18 +1176,25 @@ async function loadMarkdownFile(filePath) {
11761176

11771177
async function checkLaunchArgs() {
11781178
try {
1179+
// First check if there's a file opened via "Open With" (macOS RunEvent::Opened)
1180+
const openedFile = await invoke('get_opened_file');
1181+
if (openedFile) {
1182+
await loadMarkdownFile(openedFile);
1183+
return true;
1184+
}
1185+
1186+
// Fallback: check command line arguments (for other platforms or direct execution)
11791187
const args = await invoke('get_launch_args');
1180-
console.log('Launch args:', args);
11811188

11821189
// Look for markdown file in arguments (skip first arg which is the executable)
11831190
for (let i = 1; i < args.length; i++) {
11841191
const arg = args[i];
11851192
if (arg.match(/\.(md|markdown|mdown|mkd)$/i)) {
1186-
console.log('Found markdown file in args:', arg);
11871193
await loadMarkdownFile(arg);
11881194
return true;
11891195
}
11901196
}
1197+
11911198
return false;
11921199
} catch (error) {
11931200
console.error('Error checking launch args:', error);
@@ -2158,24 +2165,6 @@ async function getOriginalMarkdownContent() {
21582165
}
21592166

21602167
window.addEventListener("DOMContentLoaded", async () => {
2161-
console.log('🚀 DOM Content Loaded');
2162-
console.log('🔍 Checking Tauri availability...');
2163-
console.log('window.__TAURI__:', !!window.__TAURI__);
2164-
console.log('window.__TAURI__.event:', !!window.__TAURI__?.event);
2165-
console.log('window.__TAURI__.window:', !!window.__TAURI__?.window);
2166-
console.log('window.__TAURI__.webview:', !!window.__TAURI__?.webview);
2167-
console.log('window.__TAURI__.core:', !!window.__TAURI__?.core);
2168-
2169-
// Debug library availability
2170-
console.log('🔍 Checking libraries...');
2171-
console.log('DOMPurify:', typeof DOMPurify !== 'undefined');
2172-
console.log('mermaid:', typeof mermaid !== 'undefined');
2173-
console.log('saveAs:', typeof saveAs !== 'undefined');
2174-
console.log('window.docxReady:', window.docxReady);
2175-
console.log('window.generateDocxFromMarkdown:', typeof window.generateDocxFromMarkdown !== 'undefined');
2176-
console.log('docx library:', typeof docx !== 'undefined');
2177-
console.log('highlight.js:', typeof hljs !== 'undefined');
2178-
console.log('window.highlightJsReady:', window.highlightJsReady);
21792168

21802169
// Get DOM elements
21812170
openFileBtn = document.querySelector('#open-file-btn');
@@ -2283,6 +2272,17 @@ window.addEventListener("DOMContentLoaded", async () => {
22832272
handleFileChange(event.payload);
22842273
});
22852274

2275+
// Listen for file opened via OS "Open With" events
2276+
await listen('file-opened-via-os', async (event) => {
2277+
const filePath = event.payload;
2278+
if (filePath) {
2279+
// Small delay to ensure UI is ready
2280+
setTimeout(async () => {
2281+
await loadMarkdownFile(filePath);
2282+
}, 100);
2283+
}
2284+
});
2285+
22862286
// Check for file associations (launch arguments)
22872287
const foundFile = await checkLaunchArgs();
22882288

0 commit comments

Comments
 (0)