diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml new file mode 100644 index 0000000..4dfa29d --- /dev/null +++ b/.github/workflows/test.yml @@ -0,0 +1,54 @@ +name: Test Build + +on: + pull_request: + branches: [ main ] + push: + branches: [ main, 'fix/*', 'feature/*' ] + +jobs: + test-build: + strategy: + fail-fast: false + matrix: + platform: [windows-latest, macos-latest] + runs-on: ${{ matrix.platform }} + steps: + - uses: actions/checkout@v4 + + - name: Setup Node.js + uses: actions/setup-node@v4 + with: + node-version: '18' + cache: 'npm' + + - name: Install Rust + uses: dtolnay/rust-toolchain@stable + + + - name: Install npm dependencies + run: npm ci + + - name: Build Tauri app + run: npm run tauri build + + - name: Upload Windows artifacts + if: matrix.platform == 'windows-latest' + uses: actions/upload-artifact@v4 + with: + name: test-windows-artifacts + path: | + src-tauri/target/release/bundle/msi/*.msi + src-tauri/target/release/bundle/nsis/*.exe + src-tauri/target/release/markdown-viewer.exe + retention-days: 7 + + - name: Upload macOS artifacts + if: matrix.platform == 'macos-latest' + uses: actions/upload-artifact@v4 + with: + name: test-macos-artifacts + path: | + src-tauri/target/release/bundle/dmg/*.dmg + src-tauri/target/release/bundle/macos/*.app + retention-days: 7 \ No newline at end of file diff --git a/src-tauri/src/lib.rs b/src-tauri/src/lib.rs index ec7dd58..a585ec5 100644 --- a/src-tauri/src/lib.rs +++ b/src-tauri/src/lib.rs @@ -4,7 +4,9 @@ use std::env; use std::path::{Path, PathBuf}; use std::sync::{Arc, Mutex}; use notify::{Watcher, RecommendedWatcher, RecursiveMode, Event, EventKind}; -use tauri::{AppHandle, Emitter}; +use tauri::{AppHandle, Emitter, Manager}; +#[cfg(any(target_os = "macos", target_os = "ios"))] +use tauri::RunEvent; use syntect::parsing::SyntaxSet; use syntect::highlighting::ThemeSet; 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 // Global state for file watcher type WatcherState = Arc>>; +// App state to store file opened via "Open With" on macOS +#[derive(Default)] +struct OpenedFileState { + file_path: Arc>>, +} + +impl OpenedFileState { + fn set_file(&self, path: String) { + let mut file_path = self.file_path.lock().unwrap(); + *file_path = Some(path); + } + + fn get_file(&self) -> Option { + let file_path = self.file_path.lock().unwrap(); + file_path.clone() + } + + fn clear_file(&self) { + let mut file_path = self.file_path.lock().unwrap(); + *file_path = None; + } +} + // Security utilities for temp file handling fn create_secure_temp_file(content: &str) -> Result { // Basic size validation only @@ -386,6 +411,16 @@ fn get_launch_args() -> Vec { env::args().collect() } +#[tauri::command] +fn get_opened_file(state: tauri::State) -> Option { + let file = state.get_file(); + if file.is_some() { + // Clear the file after retrieving it so it's only opened once + state.clear_file(); + } + file +} + #[tauri::command] fn export_html(content: String, title: String) -> Result { let html_template = format!(r#" @@ -664,23 +699,81 @@ async fn save_temp_html_and_open(html_content: String) -> Result<(), String> { #[cfg_attr(mobile, tauri::mobile_entry_point)] pub fn run() { let watcher_state: WatcherState = Arc::new(Mutex::new(None)); + let opened_file_state = OpenedFileState::default(); tauri::Builder::default() .plugin(tauri_plugin_opener::init()) .plugin(tauri_plugin_dialog::init()) .plugin(tauri_plugin_fs::init()) .manage(watcher_state) + .manage(opened_file_state) .invoke_handler(tauri::generate_handler![ greet, parse_markdown, read_markdown_file, get_launch_args, + get_opened_file, start_watching_file, stop_watching_file, export_html, read_file_content, save_temp_html_and_open ]) - .run(tauri::generate_context!()) - .expect("error while running tauri application"); + .setup(|app| { + // Check command line args during setup (fallback for other platforms) + let setup_args = env::args().collect::>(); + + // Check for markdown files in args + for arg in setup_args.iter().skip(1) { + if arg.ends_with(".md") || arg.ends_with(".markdown") || + arg.ends_with(".mdown") || arg.ends_with(".mkd") { + // For command line arguments, store in the opened file state + let opened_file_state = app.state::(); + opened_file_state.set_file(arg.clone()); + break; + } + } + + Ok(()) + }) + .build(tauri::generate_context!()) + .expect("error while building tauri application") + .run(|_app_handle, event| { + match event { + // RunEvent::Opened is only available on macOS and iOS + #[cfg(any(target_os = "macos", target_os = "ios"))] + RunEvent::Opened { urls } => { + let app_handle = _app_handle; + // Find the first markdown file in the opened URLs + for url in urls { + // Convert URL to string and handle file:// URLs + let url_str = url.as_str(); + let file_path = if url_str.starts_with("file://") { + url_str.trim_start_matches("file://").to_string() + } else { + url_str.to_string() + }; + + if file_path.ends_with(".md") || file_path.ends_with(".markdown") || + file_path.ends_with(".mdown") || file_path.ends_with(".mkd") { + // Validate the file path for security + if let Ok(validated_path) = validate_file_path(&file_path) { + let validated_str = validated_path.to_string_lossy().to_string(); + + // Store the opened file in app state + let opened_file_state = app_handle.state::(); + opened_file_state.set_file(validated_str.clone()); + + // Also try to emit the event to the frontend if it's ready + let _ = app_handle.emit("file-opened-via-os", &validated_str); + } + break; + } + } + } + _ => { + // Handle other events as needed + } + } + }); } diff --git a/src/main.js b/src/main.js index c4c8faf..dffb410 100644 --- a/src/main.js +++ b/src/main.js @@ -1176,18 +1176,25 @@ async function loadMarkdownFile(filePath) { async function checkLaunchArgs() { try { + // First check if there's a file opened via "Open With" (macOS RunEvent::Opened) + const openedFile = await invoke('get_opened_file'); + if (openedFile) { + await loadMarkdownFile(openedFile); + return true; + } + + // Fallback: check command line arguments (for other platforms or direct execution) const args = await invoke('get_launch_args'); - console.log('Launch args:', args); // Look for markdown file in arguments (skip first arg which is the executable) for (let i = 1; i < args.length; i++) { const arg = args[i]; if (arg.match(/\.(md|markdown|mdown|mkd)$/i)) { - console.log('Found markdown file in args:', arg); await loadMarkdownFile(arg); return true; } } + return false; } catch (error) { console.error('Error checking launch args:', error); @@ -2158,24 +2165,6 @@ async function getOriginalMarkdownContent() { } window.addEventListener("DOMContentLoaded", async () => { - console.log('🚀 DOM Content Loaded'); - console.log('🔍 Checking Tauri availability...'); - console.log('window.__TAURI__:', !!window.__TAURI__); - console.log('window.__TAURI__.event:', !!window.__TAURI__?.event); - console.log('window.__TAURI__.window:', !!window.__TAURI__?.window); - console.log('window.__TAURI__.webview:', !!window.__TAURI__?.webview); - console.log('window.__TAURI__.core:', !!window.__TAURI__?.core); - - // Debug library availability - console.log('🔍 Checking libraries...'); - console.log('DOMPurify:', typeof DOMPurify !== 'undefined'); - console.log('mermaid:', typeof mermaid !== 'undefined'); - console.log('saveAs:', typeof saveAs !== 'undefined'); - console.log('window.docxReady:', window.docxReady); - console.log('window.generateDocxFromMarkdown:', typeof window.generateDocxFromMarkdown !== 'undefined'); - console.log('docx library:', typeof docx !== 'undefined'); - console.log('highlight.js:', typeof hljs !== 'undefined'); - console.log('window.highlightJsReady:', window.highlightJsReady); // Get DOM elements openFileBtn = document.querySelector('#open-file-btn'); @@ -2283,6 +2272,17 @@ window.addEventListener("DOMContentLoaded", async () => { handleFileChange(event.payload); }); + // Listen for file opened via OS "Open With" events + await listen('file-opened-via-os', async (event) => { + const filePath = event.payload; + if (filePath) { + // Small delay to ensure UI is ready + setTimeout(async () => { + await loadMarkdownFile(filePath); + }, 100); + } + }); + // Check for file associations (launch arguments) const foundFile = await checkLaunchArgs();