Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
54 changes: 54 additions & 0 deletions .github/workflows/test.yml
Original file line number Diff line number Diff line change
@@ -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
99 changes: 96 additions & 3 deletions src-tauri/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand All @@ -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<Mutex<Option<RecommendedWatcher>>>;

// App state to store file opened via "Open With" on macOS
#[derive(Default)]
struct OpenedFileState {
file_path: Arc<Mutex<Option<String>>>,
}

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<String> {
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<PathBuf, String> {
// Basic size validation only
Expand Down Expand Up @@ -386,6 +411,16 @@ fn get_launch_args() -> Vec<String> {
env::args().collect()
}

#[tauri::command]
fn get_opened_file(state: tauri::State<OpenedFileState>) -> Option<String> {
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<String, String> {
let html_template = format!(r#"<!DOCTYPE html>
Expand Down Expand Up @@ -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::<Vec<String>>();

// 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::<OpenedFileState>();
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::<OpenedFileState>();
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
}
}
});
}
40 changes: 20 additions & 20 deletions src/main.js
Original file line number Diff line number Diff line change
Expand Up @@ -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);
Expand Down Expand Up @@ -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');
Expand Down Expand Up @@ -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();

Expand Down
Loading