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
9 changes: 5 additions & 4 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -28,8 +28,9 @@ A native macOS Markdown viewer built with [Tauri v2](https://v2.tauri.app/). Bro
- **Relative link navigation** — Click `.md` links to navigate between documents
- **Dark mode** — Follows macOS system appearance, toggleable manually
- **Session persistence** — Remembers your last opened folder across launches
- **Open single files** — Open `.md` files directly via CLI, Finder "Open With", or drag & drop
- **PDF export** — Export the current document as PDF with native rendering
- **CLI support** — Open a folder directly: `mdv ~/docs`
- **CLI support** — Open a folder or file directly: `mdv ~/docs` or `mdv ~/docs/README.md`
- **Native menu** — Cmd+O to open a folder, standard macOS app menu

### Markdown Rendering
Expand Down Expand Up @@ -106,10 +107,10 @@ The built app will be at `src-tauri/target/release/bundle/macos/Markdown Viewer.

```bash
# Open a specific folder
./src-tauri/target/release/mdv ~/my-docs
mdv ~/my-docs

# Or after installing the .app
/Applications/Markdown\ Viewer.app/Contents/MacOS/mdv ~/my-docs
# Open a single file (loads its parent directory in the sidebar)
mdv ~/my-docs/README.md
```

## Tech Stack
Expand Down
9 changes: 9 additions & 0 deletions docs/guide/features.md
Original file line number Diff line number Diff line change
Expand Up @@ -119,6 +119,15 @@ Click the PDF button in the sidebar header to export the current document as a P

The app remembers the last opened folder and restores it on next launch.

## Open Single Files

You can open individual `.md` files directly:

- **CLI**: `mdv ~/docs/README.md`
- **Finder**: Right-click a `.md` file → "Open With" → Markdown Viewer

The app loads the file and displays its parent directory in the sidebar for easy navigation to related documents. File associations are declared for `.md`, `.markdown`, and `.mdx` extensions.

## Relative Link Navigation

Click any `.md` link in a document to navigate to that file. Anchor links (`#section`) scroll to the target heading. External links open in the default browser.
6 changes: 3 additions & 3 deletions docs/guide/getting-started.md
Original file line number Diff line number Diff line change
Expand Up @@ -44,10 +44,10 @@ The built app will be at `src-tauri/target/release/bundle/macos/Markdown Viewer.

```bash
# Open a specific folder
./src-tauri/target/release/mdv ~/my-docs
mdv ~/my-docs

# Or after installing the .app
/Applications/Markdown\ Viewer.app/Contents/MacOS/mdv ~/my-docs
# Open a single file (loads its parent directory in the sidebar)
mdv ~/my-docs/README.md
```

## Examples
Expand Down
49 changes: 36 additions & 13 deletions src-tauri/src/lib.rs
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
use std::path::Path;
use tauri::Emitter;
use tauri::menu::{MenuBuilder, MenuItemBuilder, SubmenuBuilder};

Expand Down Expand Up @@ -57,8 +58,8 @@ async fn export_pdf(_output_path: String) -> Result<(), String> {
}

#[cfg_attr(mobile, tauri::mobile_entry_point)]
pub fn run(folder_arg: Option<String>) {
tauri::Builder::default()
pub fn run(path_arg: Option<String>) {
let app = tauri::Builder::default()
.invoke_handler(tauri::generate_handler![print_webview, export_pdf])
.plugin(tauri_plugin_store::Builder::new().build())
.plugin(tauri_plugin_dialog::init())
Expand Down Expand Up @@ -88,16 +89,24 @@ pub fn run(folder_arg: Option<String>) {
}
});

// Pass CLI arg to frontend
if let Some(ref folder) = folder_arg {
let folder = folder.clone();
// Pass CLI arg to frontend — detect file vs folder
if let Some(ref path_str) = path_arg {
let path = Path::new(path_str);
let handle = app.handle().clone();
// Emit after the window is ready
tauri::async_runtime::spawn(async move {
// Small delay to ensure frontend is loaded
tokio::time::sleep(std::time::Duration::from_millis(500)).await;
let _ = handle.emit("open-folder", folder);
});

if path.is_file() {
let file_path = path_str.clone();
tauri::async_runtime::spawn(async move {
tokio::time::sleep(std::time::Duration::from_millis(500)).await;
let _ = handle.emit("open-file", file_path);
});
} else if path.is_dir() {
let folder = path_str.clone();
tauri::async_runtime::spawn(async move {
tokio::time::sleep(std::time::Duration::from_millis(500)).await;
let _ = handle.emit("open-folder", folder);
});
}
}

if cfg!(debug_assertions) {
Expand All @@ -109,6 +118,20 @@ pub fn run(folder_arg: Option<String>) {
}
Ok(())
})
.run(tauri::generate_context!())
.expect("error while running tauri application");
.build(tauri::generate_context!())
.expect("error while building tauri application");

app.run(|app_handle, event| {
#[cfg(target_os = "macos")]
if let tauri::RunEvent::Opened { ref urls } = event {
for url in urls {
if let Ok(path) = url.to_file_path() {
if path.is_file() {
let _ = app_handle.emit("open-file", path.to_string_lossy().to_string());
}
}
}
}
let _ = (app_handle, event);
});
}
4 changes: 2 additions & 2 deletions src-tauri/src/main.rs
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,6 @@

fn main() {
let args: Vec<String> = std::env::args().collect();
let folder_arg = args.get(1).cloned();
app_lib::run(folder_arg);
let path_arg = args.get(1).cloned();
app_lib::run(path_arg);
}
9 changes: 8 additions & 1 deletion src-tauri/tauri.conf.json
Original file line number Diff line number Diff line change
Expand Up @@ -42,6 +42,13 @@
"macOS": {
"signingIdentity": "Developer ID Application: NICOLAS PATRICK PRUD'HOMME (44DTA694H9)",
"entitlements": "entitlements.plist"
}
},
"fileAssociations": [
{
"ext": ["md", "markdown", "mdx"],
"name": "Markdown Document",
"role": "Viewer"
}
]
}
}
20 changes: 18 additions & 2 deletions src/main.ts
Original file line number Diff line number Diff line change
Expand Up @@ -287,6 +287,15 @@ async function init(): Promise<void> {
setRootPath(event.payload);
});

// Listen for file open (via "Open With" or CLI with a file path)
appWindow.listen<string>("open-file", (event) => {
const filePath = event.payload;
const lastSep = filePath.lastIndexOf("/");
const parentDir = filePath.substring(0, lastSep);
const fileName = filePath.substring(lastSep + 1);
setRootPath(parentDir, fileName);
});

// Listen for menu "Open Folder" (Cmd+O)
appWindow.listen("menu-open-folder", () => {
openFolder();
Expand All @@ -299,14 +308,21 @@ async function init(): Promise<void> {
}
}

async function setRootPath(path: string): Promise<void> {
async function setRootPath(
path: string,
fileToOpen?: string
): Promise<void> {
rootPath = path;
rootName = extractRootName(path);
currentPath = [];
activeFile = null;
await saveRootPath(path);
await renderSidebar();
await autoSelectReadme();
if (fileToOpen) {
await loadFile(fileToOpen);
} else {
await autoSelectReadme();
}
}

async function openFolder(): Promise<void> {
Expand Down