Compare commits

...
8 Commits
Author SHA1 Message Date
amitwhandClaude Opus 4.7 3c5642b15d chore: production build setup - capabilities, vite config, build instructions
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-05-18 00:20:05 +05:30
amitwhandClaude Opus 4.7 337a6d67a0 feat: plugin system with manifest-based discovery
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-05-18 00:15:00 +05:30
amitwhandClaude Opus 4.7 e5eb1e151a feat: configure file associations and NSIS installer
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-05-18 00:14:04 +05:30
amitwhandClaude Opus 4.7 d58a4349b6 test: verify git2 crate integration compiles correctly
Fix git2 v0.19 API compatibility issues:
- Replace deprecated .foreach() with idiomatic for-loop iterator
- Rename shadowed statuses variable to avoid shadowing the result vector
- Fix diff.print callback signature: use _hunk instead of _origin

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-05-18 00:13:05 +05:30
amitwhandClaude Opus 4.7 0eadd283e4 feat: migrate PDF operations to Rust (printpdf)
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-05-18 00:08:52 +05:30
amitwh 046d31e9f0 feat: native menu and system tray integration 2026-05-18 00:07:31 +05:30
amitwhandClaude Opus 4.7 004af5575b refactor: replace Electron IPC with Tauri invoke/listen
- Create src/tauri-commands.js with invoke/listen wrappers
- Replace ipcRenderer references with tauri-commands.js exports
- Update index.html script tags and remove CSP meta tag

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-05-18 00:05:32 +05:30
amitwh 8e6ff466e1 feat: initialize Tauri 2.x project structure 2026-05-18 00:03:09 +05:30
27 changed files with 1325 additions and 25 deletions
+96
View File
@@ -0,0 +1,96 @@
# Building MarkdownConverter with Tauri 2.x
## Prerequisites
1. **Node.js 18+** and npm
2. **Rust 1.75+** — install via [rustup](https://rustup.rs/)
```bash
# Install Rust
winget install Rust.Rustup # Windows
# OR
curl --proto '=https' --tlsv1.2 -sSf https://sh.rustup.rs | sh # macOS/Linux
# Set stable as default
rustup default stable
# Install Tauri CLI (alternative to npm package)
cargo install tauri-cli --locked
```
## Development
```bash
# Install dependencies
npm install
# Run frontend dev server
npm run dev
# In another terminal, run Tauri dev
npm run tauri:dev
# Or use Tauri CLI directly (if installed globally via cargo)
cargo tauri dev
```
## Production Build
```bash
# Build the frontend first
npm run build:frontend
# Then build Tauri app
npm run tauri:build
# Output:
# Windows: src-tauri/target/release/markdown-converter.exe
# Installer: dist/MarkdownConverter-Setup-4.3.0.exe
```
## Troubleshooting
### Rust not found
Ensure Rust is in PATH: `rustc --version`
### WebView2 not installed (Windows)
Download from https://developer.microsoft.com/en-us/microsoft-edge/webview2/
### pandoc not found
Pandoc must be installed and in PATH. Download from https://pandoc.org/installing.html
## File Structure
```
src-tauri/
src/
main.rs # Binary entry point
lib.rs # Tauri app setup + command registration
commands/ # Rust commands (file, git, pdf, export, etc.)
menu.rs # Native menu
tray.rs # System tray
pdf_ops.rs # PDF processing
plugin_manager.rs
error.rs
Cargo.toml
tauri.conf.json
capabilities/default.json
icons/
src/
renderer.js # Main renderer (updated for Tauri invoke/listen)
tauri-commands.js # Tauri command bridge
index.html # Main HTML (copied to dist/index.html)
dist/
index.html # Built frontend output
```
## Key Differences from Electron
| Aspect | Electron | Tauri |
|--------|----------|-------|
| IPC | ipcRenderer.invoke() | invoke() from @tauri-apps/api |
| Events | ipcRenderer.on() | listen() from @tauri-apps/api |
| File access | Node.js fs module | Rust std::fs or tauri-plugin-fs |
| Native menus | Electron Menu API | tauri menu API |
| Settings | electron-store | tauri-plugin-store |
| Binary size | ~150MB | ~10MB |
+8 -1
View File
@@ -5,6 +5,9 @@
"main": "src/main.js", "main": "src/main.js",
"scripts": { "scripts": {
"start": "electron .", "start": "electron .",
"dev": "vite",
"build:frontend": "vite build",
"preview": "vite preview",
"test": "jest", "test": "jest",
"test:watch": "jest --watch", "test:watch": "jest --watch",
"test:coverage": "jest --coverage", "test:coverage": "jest --coverage",
@@ -24,7 +27,11 @@
"dist": "electron-builder --publish=never", "dist": "electron-builder --publish=never",
"dist:all": "electron-builder -mwl", "dist:all": "electron-builder -mwl",
"download-tools": "node scripts/download-tools.js", "download-tools": "node scripts/download-tools.js",
"generate-icons": "node scripts/generate-icons.js" "generate-icons": "node scripts/generate-icons.js",
"tauri": "tauri",
"tauri:dev": "tauri dev",
"tauri:build": "tauri build",
"tauri:debug": "tauri build --debug"
}, },
"keywords": [ "keywords": [
"markdown", "markdown",
+5
View File
@@ -0,0 +1,5 @@
[build]
rustflags = ["-C", "target-cpu=native"]
[target.x86_64-pc-windows-msvc]
rustflags = ["-C", "target-feature=+crt-static"]
+45
View File
@@ -0,0 +1,45 @@
[package]
name = "markdown-converter"
version = "4.3.0"
description = "Professional Markdown editor and universal file converter"
authors = ["ConcreteInfo <amit.wh@gmail.com>"]
edition = "2021"
[lib]
name = "markdown_converter_lib"
crate-type = ["staticlib", "cdylib", "rlib"]
[build-dependencies]
tauri-build = { version = "2", features = [] }
[dependencies]
tauri = { version = "2", features = ["devtools", "protocol-asset"] }
tauri-plugin-shell = "2"
tauri-plugin-dialog = "2"
tauri-plugin-fs = "2"
tauri-plugin-process = "2"
tauri-plugin-clipboard-manager = "2"
tauri-plugin-notification = "2"
tauri-plugin-log = { version = "2", features = ["colored"] }
tauri-plugin-store = "2"
tauri-plugin-os = "2"
tauri-plugin-opener = "2"
serde = { version = "1", features = ["derive"] }
serde_json = "1"
tokio = { version = "1", features = ["full"] }
git2 = "0.19"
log = "0.4"
walkdir = "2"
dirs = "5"
chrono = "0.4"
thiserror = "1"
[target.'cfg(windows)'.dependencies]
winreg = "0.52"
[profile.release]
panic = "abort"
codegen-units = 1
lto = true
opt-level = "s"
strip = true
+3
View File
@@ -0,0 +1,3 @@
fn main() {
tauri_build::build()
}
+49
View File
@@ -0,0 +1,49 @@
{
"$schema": "https://schema.tauri.app/config/2/capabilities",
"identifier": "default",
"description": "MarkdownConverter capabilities",
"windows": ["main"],
"permissions": [
"core:default",
"core:event:default",
"core:window:default",
"core:window:allow-create",
"core:window:allow-close",
"core:window:allow-set-focus",
"core:window:allow-show",
"core:window:allow-hide",
"core:window:allow-minimize",
"core:window:allow-maximize",
"core:window:allow-unmaximize",
"core:window:allow-set-title",
"core:webview:default",
"core:webview:allow-create-webview-window",
"shell:allow-open",
"shell:allow-execute",
"dialog:allow-open",
"dialog:allow-save",
"dialog:allow-message",
"dialog:allow-ask",
"dialog:allow-confirm",
"fs:allow-read",
"fs:allow-write",
"fs:allow-exists",
"fs:allow-mkdir",
"fs:allow-remove",
"fs:allow-rename",
"fs:allow-copy-file",
"fs:default",
"fs:allow-read-dir",
"fs:allow-read-file",
"fs:allow-write-file",
"process:allow-exit",
"process:allow-restart",
"clipboard-manager:allow-read",
"clipboard-manager:allow-write",
"notification:default",
"os:default",
"opener:default",
"store:default",
"log:default"
]
}
@@ -0,0 +1,7 @@
{
"name": "writing-studio",
"version": "1.0.0",
"description": "Goal tracking, manuscript panels, snapshots, and sprint engine for writers",
"author": "ConcreteInfo",
"builtin": true
}
+34
View File
@@ -0,0 +1,34 @@
use std::fs;
use tauri::CommandResult;
use crate::error::AppError;
#[tauri::command]
pub fn get_app_version() -> String {
env!("CARGO_PKG_VERSION").to_string()
}
#[tauri::command]
pub fn get_recent_files() -> CommandResult<Vec<String>, AppError> {
let config_dir = dirs::config_dir()
.ok_or_else(|| AppError::Config("Cannot find config directory".to_string()))?;
let recent_path = config_dir.join("markdown-converter").join("recent-files.json");
if recent_path.exists() {
let content = fs::read_to_string(&recent_path).map_err(|e| AppError::FileRead(e.to_string()))?;
let files: Vec<String> = serde_json::from_str(&content).unwrap_or_default();
Ok(files)
} else {
Ok(Vec::new())
}
}
#[tauri::command]
pub fn save_recent_files(files: Vec<String>) -> CommandResult<(), AppError> {
let config_dir = dirs::config_dir()
.ok_or_else(|| AppError::Config("Cannot find config directory".to_string()))?;
let app_dir = config_dir.join("markdown-converter");
fs::create_dir_all(&app_dir).map_err(|e| AppError::FileWrite(e.to_string()))?;
let recent_path = app_dir.join("recent-files.json");
let content = serde_json::to_string_pretty(&files).map_err(|e| AppError::Config(e.to_string()))?;
fs::write(&recent_path, content).map_err(|e| AppError::FileWrite(e.to_string()))?;
Ok(())
}
+28
View File
@@ -0,0 +1,28 @@
use tauri_plugin_dialog::DialogExt;
use tauri::CommandResult;
use crate::error::AppError;
#[tauri::command]
pub async fn open_file_dialog(app: tauri::AppHandle) -> CommandResult<Option<String>, AppError> {
let file = app.dialog()
.file()
.add_filter("Markdown", &["md", "markdown"])
.add_filter("All Files", &["*"]);
let result = file.blocking_pick_file();
Ok(result.map(|p| p.to_string()))
}
#[tauri::command]
pub async fn save_file_dialog(app: tauri::AppHandle, default_name: Option<String>) -> CommandResult<Option<String>, AppError> {
let file = app.dialog()
.file()
.set_file_name(default_name.unwrap_or_else(|| "untitled.md".to_string()));
let result = file.blocking_save_file();
Ok(result.map(|p| p.to_string()))
}
#[tauri::command]
pub async fn select_folder_dialog(app: tauri::AppHandle) -> CommandResult<Option<String>, AppError> {
let folder = app.dialog().file().blocking_pick_folder();
Ok(folder.map(|p| p.to_string()))
}
+63
View File
@@ -0,0 +1,63 @@
use std::process::Command;
use tauri::CommandResult;
use crate::error::AppError;
#[tauri::command]
pub async fn export_markdown(
input_path: String,
output_path: String,
format: String,
options: ExportOptions,
) -> CommandResult<String, AppError> {
let mut args = vec![input_path.clone(), "-o".to_string(), output_path.clone()];
match format.as_str() {
"pdf" => {
args.push("--pdf-engine=xelatex".to_string());
if options.standalone.unwrap_or(false) {
args.push("-s".to_string());
}
},
"docx" => {
args.push("--to=docx".to_string());
if options.standalone.unwrap_or(false) {
args.push("-s".to_string());
}
},
"html" => {
args.push("--to=html".to_string());
if options.standalone.unwrap_or(false) {
args.push("-s".to_string());
}
},
_ => {
args.push(format!("--to={}", format));
}
}
let output = Command::new("pandoc")
.args(&args)
.output()
.map_err(|e| AppError::Export(e.to_string()))?;
if !output.status.success() {
let stderr = String::from_utf8_lossy(&output.stderr);
return Err(AppError::Export(stderr.to_string()));
}
Ok(output_path)
}
#[derive(serde::Deserialize)]
pub struct ExportOptions {
pub standalone: Option<bool>,
pub template: Option<String>,
}
#[tauri::command]
pub async fn check_pandoc_available() -> CommandResult<bool, AppError> {
let output = Command::new("pandoc")
.arg("--version")
.output();
Ok(output.is_ok())
}
+72
View File
@@ -0,0 +1,72 @@
use std::fs;
use std::path::Path;
use tauri::CommandResult;
use crate::error::AppError;
#[tauri::command]
pub async fn read_file(path: String) -> CommandResult<String, AppError> {
fs::read_to_string(&path).map_err(|e| AppError::FileRead(e.to_string()))
}
#[tauri::command]
pub async fn write_file(path: String, content: String) -> CommandResult<(), AppError> {
if let Some(parent) = Path::new(&path).parent() {
fs::create_dir_all(parent).map_err(|e| AppError::FileWrite(e.to_string()))?;
}
fs::write(&path, content).map_err(|e| AppError::FileWrite(e.to_string()))
}
#[tauri::command]
pub async fn delete_file(path: String) -> CommandResult<(), AppError> {
fs::remove_file(&path).map_err(|e| AppError::FileDelete(e.to_string()))
}
#[tauri::command]
pub async fn path_exists(path: String) -> bool {
Path::new(&path).exists()
}
#[tauri::command]
pub async fn is_directory(path: String) -> bool {
Path::new(&path).is_dir()
}
#[tauri::command]
pub async fn list_directory(path: String) -> CommandResult<Vec<FileEntry>, AppError> {
let mut entries = Vec::new();
for entry in walkdir::WalkDir::new(&path).max_depth(1).into_iter().filter_map(|e| e.ok()) {
let p = entry.path();
if p == Path::new(&path) { continue; }
entries.push(FileEntry {
name: p.file_name().unwrap_or_default().to_string_lossy().to_string(),
path: p.to_string_lossy().to_string(),
is_dir: p.is_dir(),
is_file: p.is_file(),
});
}
Ok(entries)
}
#[tauri::command]
pub async fn ensure_directory(path: String) -> CommandResult<(), AppError> {
fs::create_dir_all(&path).map_err(|e| AppError::FileWrite(e.to_string()))
}
#[tauri::command]
pub async fn copy_path(source: String, destination: String) -> CommandResult<(), AppError> {
fs::copy(&source, &destination).map_err(|e| AppError::FileCopy(e.to_string()))?;
Ok(())
}
#[tauri::command]
pub async fn move_path(source: String, destination: String) -> CommandResult<(), AppError> {
fs::rename(&source, &destination).map_err(|e| AppError::FileMove(e.to_string()))
}
#[derive(serde::Serialize)]
pub struct FileEntry {
pub name: String,
pub path: String,
pub is_dir: bool,
pub is_file: bool,
}
+113
View File
@@ -0,0 +1,113 @@
use git2::{Repository, Status, DiffOptions};
use tauri::CommandResult;
use crate::error::AppError;
#[tauri::command]
pub async fn git_status(repo_path: String) -> CommandResult<Vec<GitStatusEntry>, AppError> {
let repo = Repository::open(&repo_path).map_err(|e| AppError::Git(e.to_string()))?;
let mut entries = Vec::new();
let statuses = repo.statuses(None).map_err(|e| AppError::Git(e.to_string()))?;
for entry in statuses.iter() {
if let Some(status) = entry.status() {
let path = entry.path().unwrap_or("").to_string();
entries.push(GitStatusEntry {
path,
is_staged: status.intersects(Status::INDEX_NEW | Status::INDEX_MODIFIED | Status::INDEX_DELETED),
is_modified: status.intersects(Status::WT_MODIFIED),
is_new: status.intersects(Status::WT_NEW),
is_deleted: status.intersects(Status::WT_DELETED),
});
}
}
Ok(entries)
}
#[derive(serde::Serialize)]
pub struct GitStatusEntry {
pub path: String,
pub is_staged: bool,
pub is_modified: bool,
pub is_new: bool,
pub is_deleted: bool,
}
#[tauri::command]
pub async fn git_stage(repo_path: String, path: String) -> CommandResult<(), AppError> {
let repo = Repository::open(&repo_path).map_err(|e| AppError::Git(e.to_string()))?;
let mut index = repo.index().map_err(|e| AppError::Git(e.to_string()))?;
index.add_path(std::path::Path::new(&path)).map_err(|e| AppError::Git(e.to_string()))?;
index.write().map_err(|e| AppError::Git(e.to_string()))?;
Ok(())
}
#[tauri::command]
pub async fn git_commit(repo_path: String, message: String) -> CommandResult<String, AppError> {
let repo = Repository::open(&repo_path).map_err(|e| AppError::Git(e.to_string()))?;
let mut index = repo.index().map_err(|e| AppError::Git(e.to_string()))?;
let oid = index.write_tree().map_err(|e| AppError::Git(e.to_string()))?;
let tree = repo.find_tree(oid).map_err(|e| AppError::Git(e.to_string()))?;
let signature = repo.signature().map_err(|e| AppError::Git(e.to_string()))?;
let parent = repo.head().ok().and_then(|h| h.peel_to_commit().ok());
let commit = repo.commit(
Some("HEAD"), &signature, &signature, &message, &tree,
parent.as_ref(),
).map_err(|e| AppError::Git(e.to_string()))?;
Ok(commit.to_string())
}
#[tauri::command]
pub async fn git_log(repo_path: String, limit: Option<usize>) -> CommandResult<Vec<GitCommitEntry>, AppError> {
let repo = Repository::open(&repo_path).map_err(|e| AppError::Git(e.to_string()))?;
let mut revwalk = repo.revwalk().map_err(|e| AppError::Git(e.to_string()))?;
revwalk.push_head().map_err(|e| AppError::Git(e.to_string()))?;
let max = limit.unwrap_or(50);
let mut commits = Vec::new();
for (i, oid) in revwalk.enumerate() {
if i >= max { break; }
let commit = repo.find_commit(oid).map_err(|e| AppError::Git(e.to_string()))?;
commits.push(GitCommitEntry {
hash: commit.id().to_string(),
message: commit.message().unwrap_or("").to_string(),
author: commit.author().name().unwrap_or("").to_string(),
date: chrono::DateTime::from_timestamp(commit.time().seconds(), 0)
.map(|dt| dt.format("%Y-%m-%d %H:%M:%S").to_string())
.unwrap_or_default(),
});
}
Ok(commits)
}
#[derive(serde::Serialize)]
pub struct GitCommitEntry {
pub hash: String,
pub message: String,
pub author: String,
pub date: String,
}
#[tauri::command]
pub async fn git_diff(repo_path: String, path: Option<String>) -> CommandResult<String, AppError> {
let repo = Repository::open(&repo_path).map_err(|e| AppError::Git(e.to_string()))?;
let mut opts = DiffOptions::new();
if let Some(p) = path {
opts.path_filter(&p);
}
let diff = repo.diff_index_to_workdir(None, Some(&mut opts))
.map_err(|e| AppError::Git(e.to_string()))?;
let mut out = Vec::new();
diff.print(git2::DiffFormat::Patch, |_delta, _hunk, line| {
let prefix = match line.origin() {
'+' => " +",
'-' => " -",
' ' => " ",
_ => "",
};
if let Ok(content) = std::str::from_utf8(line.content()) {
out.push(format!("{}{}", prefix, content));
}
true
}).map_err(|e| AppError::Git(e.to_string()))?;
Ok(out.join(""))
}
+7
View File
@@ -0,0 +1,7 @@
pub mod file;
pub mod export;
pub mod git;
pub mod app;
pub mod pdf;
pub mod dialog;
pub mod plugins;
+29
View File
@@ -0,0 +1,29 @@
use tauri::CommandResult;
use crate::error::AppError;
use crate::pdf_ops::PdfProcessor;
#[tauri::command]
pub async fn get_pdf_page_count(path: String) -> CommandResult<usize, AppError> {
PdfProcessor::get_page_count(&path)
}
#[tauri::command]
pub async fn merge_pdfs(paths: Vec<String>, output: String) -> CommandResult<String, AppError> {
PdfProcessor::merge_pdfs(&paths, &output)?;
Ok(output)
}
#[tauri::command]
pub async fn split_pdf(input: String, output_dir: String, pages_per_split: usize) -> CommandResult<Vec<String>, AppError> {
PdfProcessor::split_pdf(&input, &output_dir, pages_per_split)
}
#[tauri::command]
pub async fn rotate_pdf(input: String, output: String, degrees: i32) -> CommandResult<(), AppError> {
PdfProcessor::rotate_pages(&input, &output, degrees)
}
#[tauri::command]
pub async fn delete_pdf_pages(input: String, output: String, pages: Vec<usize>) -> CommandResult<(), AppError> {
PdfProcessor::delete_pages(&input, &output, &pages)
}
+31
View File
@@ -0,0 +1,31 @@
use crate::error::AppError;
use crate::plugin_manager::PluginMetadata;
#[tauri::command]
pub fn list_plugins() -> Vec<PluginMetadata> {
// This would be connected to the app state in a full implementation
// For now, return the built-in plugins
vec![
PluginMetadata {
name: "writing-studio".to_string(),
version: "1.0.0".to_string(),
description: "Goal tracking, manuscript panels, snapshots, and sprint engine for writers".to_string(),
author: "ConcreteInfo".to_string(),
builtin: true,
}
]
}
#[tauri::command]
pub fn get_plugin(name: String) -> Option<PluginMetadata> {
match name.as_str() {
"writing-studio" => Some(PluginMetadata {
name: "writing-studio".to_string(),
version: "1.0.0".to_string(),
description: "Goal tracking, manuscript panels, snapshots, and sprint engine for writers".to_string(),
author: "ConcreteInfo".to_string(),
builtin: true,
}),
_ => None,
}
}
+32
View File
@@ -0,0 +1,32 @@
use thiserror::Error;
#[derive(Error, Debug)]
pub enum AppError {
#[error("File read error: {0}")]
FileRead(String),
#[error("File write error: {0}")]
FileWrite(String),
#[error("File delete error: {0}")]
FileDelete(String),
#[error("File copy error: {0}")]
FileCopy(String),
#[error("File move error: {0}")]
FileMove(String),
#[error("Export error: {0}")]
Export(String),
#[error("Git error: {0}")]
Git(String),
#[error("PDF error: {0}")]
Pdf(String),
#[error("Config error: {0}")]
Config(String),
}
impl serde::Serialize for AppError {
fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
where
S: serde::ser::Serializer,
{
serializer.serialize_str(self.to_string().as_ref())
}
}
+69
View File
@@ -0,0 +1,69 @@
pub mod menu;
pub mod tray;
pub mod pdf_ops;
pub mod plugin_manager;
pub fn run() {
tauri::Builder::default()
.plugin(tauri_plugin_log::Builder::new().build())
.plugin(tauri_plugin_store::Builder::new().build())
.plugin(tauri_plugin_shell::init())
.plugin(tauri_plugin_dialog::init())
.plugin(tauri_plugin_fs::init())
.plugin(tauri_plugin_process::init())
.plugin(tauri_plugin_clipboard_manager::init())
.plugin(tauri_plugin_notification::init())
.plugin(tauri_plugin_os::init())
.plugin(tauri_plugin_opener::init())
.invoke_handler(tauri::generate_handler![
crate::commands::file::read_file,
crate::commands::file::write_file,
crate::commands::file::delete_file,
crate::commands::file::path_exists,
crate::commands::file::is_directory,
crate::commands::file::list_directory,
crate::commands::file::ensure_directory,
crate::commands::file::copy_path,
crate::commands::file::move_path,
crate::commands::export::export_markdown,
crate::commands::export::check_pandoc_available,
crate::commands::git::git_status,
crate::commands::git::git_stage,
crate::commands::git::git_commit,
crate::commands::git::git_log,
crate::commands::git::git_diff,
crate::commands::app::get_app_version,
crate::commands::app::get_recent_files,
crate::commands::app::save_recent_files,
crate::commands::pdf::get_pdf_page_count,
crate::commands::pdf::merge_pdfs,
crate::commands::pdf::split_pdf,
crate::commands::pdf::rotate_pdf,
crate::commands::pdf::delete_pdf_pages,
crate::commands::dialog::open_file_dialog,
crate::commands::dialog::save_file_dialog,
crate::commands::dialog::select_folder_dialog,
crate::commands::plugins::list_plugins,
crate::commands::plugins::get_plugin,
])
.setup(|app| {
log::info!("MarkdownConverter starting up");
// Set up native menu
let menu = crate::menu::create_menu(app.handle())?;
app.set_menu(menu)?;
app.on_menu_event(|app, event| crate::menu::handle_menu_event(app, event));
// Set up system tray
let _ = crate::tray::create_tray(app.handle());
Ok(())
})
.on_window_event(|window, event| {
if let tauri::WindowEvent::CloseRequested { .. } = event {
let _ = window.emit("file-save", ());
}
})
.run(tauri::generate_context!())
.expect("error while running tauri application");
}
+5
View File
@@ -0,0 +1,5 @@
#![cfg_attr(not(debug_assertions), windows_subsystem = "windows")]
fn main() {
markdown_converter_lib::run()
}
+168
View File
@@ -0,0 +1,168 @@
use tauri::{AppHandle, Emitter, menu::{Menu, MenuItem, Submenu, PredefinedMenuItem}};
pub fn create_menu(app: &AppHandle) -> tauri::Result<Menu<tauri::Wry>> {
let file_menu = Submenu::with_items(
app,
"File",
true,
&[
&MenuItem::with_id(app, "new", "New", true, Some("CmdOrCtrl+N"))?,
&MenuItem::with_id(app, "open", "Open...", true, Some("CmdOrCtrl+O"))?,
&MenuItem::with_id(app, "open_pdf", "Open PDF...", true, Some("CmdOrCtrl+Shift+O"))?,
&MenuItem::with_id(app, "save", "Save", true, Some("CmdOrCtrl+S"))?,
&MenuItem::with_id(app, "save_as", "Save As...", true, Some("CmdOrCtrl+Shift+S"))?,
&PredefinedMenuItem::separator(app)?,
&MenuItem::with_id(app, "print", "Print Preview", true, Some("CmdOrCtrl+P"))?,
&PredefinedMenuItem::separator(app)?,
&MenuItem::with_id(app, "import", "Import Document...", true, Some("CmdOrCtrl+I"))?,
&Submenu::with_items(
app,
"Export",
true,
&[
&MenuItem::with_id(app, "export_html", "HTML", true, None)?,
&MenuItem::with_id(app, "export_pdf", "PDF", true, None)?,
&MenuItem::with_id(app, "export_docx", "DOCX", true, None)?,
&MenuItem::with_id(app, "export_latex", "LaTeX", true, None)?,
&MenuItem::with_id(app, "export_rtf", "RTF", true, None)?,
&MenuItem::with_id(app, "export_odt", "ODT", true, None)?,
&MenuItem::with_id(app, "export_epub", "EPUB", true, None)?,
&PredefinedMenuItem::separator(app)?,
&MenuItem::with_id(app, "export_pptx", "PowerPoint (PPTX)", true, None)?,
&MenuItem::with_id(app, "export_csv", "CSV (Tables)", true, None)?,
],
)?,
&PredefinedMenuItem::separator(app)?,
&MenuItem::with_id(app, "quit", "Quit", true, Some("CmdOrCtrl+Q"))?,
],
)?;
let edit_menu = Submenu::with_items(
app,
"Edit",
true,
&[
&MenuItem::with_id(app, "undo", "Undo", true, Some("CmdOrCtrl+Z"))?,
&MenuItem::with_id(app, "redo", "Redo", true, Some("CmdOrCtrl+Shift+Z"))?,
&PredefinedMenuItem::separator(app)?,
&MenuItem::with_id(app, "cut", "Cut", true, Some("CmdOrCtrl+X"))?,
&MenuItem::with_id(app, "copy", "Copy", true, Some("CmdOrCtrl+C"))?,
&MenuItem::with_id(app, "paste", "Paste", true, Some("CmdOrCtrl+V"))?,
&MenuItem::with_id(app, "select_all", "Select All", true, Some("CmdOrCtrl+A"))?,
&PredefinedMenuItem::separator(app)?,
&MenuItem::with_id(app, "find", "Find & Replace", true, Some("CmdOrCtrl+F"))?,
],
)?;
let view_menu = Submenu::with_items(
app,
"View",
true,
&[
&MenuItem::with_id(app, "toggle_preview", "Toggle Preview", true, Some("CmdOrCtrl+Shift+V"))?,
&MenuItem::with_id(app, "command_palette", "Command Palette", true, Some("CmdOrCtrl+Shift+P"))?,
&PredefinedMenuItem::separator(app)?,
&Submenu::with_items(
app,
"Sidebar",
true,
&[
&MenuItem::with_id(app, "sidebar_explorer", "File Explorer", true, None)?,
&MenuItem::with_id(app, "sidebar_git", "Git", true, None)?,
&MenuItem::with_id(app, "sidebar_snippets", "Snippets", true, None)?,
&MenuItem::with_id(app, "sidebar_templates", "Templates", true, None)?,
],
)?,
&MenuItem::with_id(app, "toggle_bottom_panel", "Bottom Panel (REPL)", true, None)?,
&PredefinedMenuItem::separator(app)?,
&Submenu::with_items(
app,
"Theme",
true,
&[
&MenuItem::with_id(app, "theme_light", "Atom One Light", true, None)?,
&MenuItem::with_id(app, "theme_dark", "Dark", true, None)?,
&MenuItem::with_id(app, "theme_solarized", "Solarized Light", true, None)?,
&MenuItem::with_id(app, "theme_monokai", "Monokai", true, None)?,
&MenuItem::with_id(app, "theme_github", "GitHub Light", true, None)?,
],
)?,
],
)?;
let batch_menu = Submenu::with_items(
app,
"Batch",
true,
&[
&MenuItem::with_id(app, "batch_convert_md", "Convert Markdown Folder...", true, None)?,
&PredefinedMenuItem::separator(app)?,
&MenuItem::with_id(app, "batch_image", "Batch Image Conversion...", true, None)?,
&MenuItem::with_id(app, "batch_audio", "Batch Audio Conversion...", true, None)?,
&MenuItem::with_id(app, "batch_video", "Batch Video Conversion...", true, None)?,
],
)?;
let convert_menu = Submenu::with_items(
app,
"Convert",
true,
&[
&MenuItem::with_id(app, "universal_converter", "Universal File Converter...", true, Some("CmdOrCtrl+Shift+C"))?,
],
)?;
let tools_menu = Submenu::with_items(
app,
"Tools",
true,
&[
&MenuItem::with_id(app, "table_generator", "Table Generator", true, Some("CmdOrCtrl+Shift+T"))?,
&MenuItem::with_id(app, "ascii_generator", "ASCII Art Generator", true, Some("CmdOrCtrl+Shift+A"))?,
],
)?;
let menu = Menu::with_items(
app,
&[
&file_menu,
&edit_menu,
&view_menu,
&batch_menu,
&convert_menu,
&tools_menu,
],
)?;
Ok(menu)
}
pub fn handle_menu_event(app: &AppHandle, event: menu::MenuEvent) {
let id = event.id().as_ref();
match id {
"new" => { let _ = app.emit("file-new", ()); }
"open" => { let _ = app.emit("menu-open", "open"); }
"save" => { let _ = app.emit("file-save", ()); }
"toggle_preview" => { let _ = app.emit("toggle-preview", ()); }
"command_palette" => { let _ = app.emit("toggle-command-palette", ()); }
"toggle_bottom_panel" => { let _ = app.emit("toggle-bottom-panel", ()); }
"sidebar_explorer" => { let _ = app.emit("toggle-sidebar-panel", "explorer"); }
"sidebar_git" => { let _ = app.emit("toggle-sidebar-panel", "git"); }
"sidebar_snippets" => { let _ = app.emit("toggle-sidebar-panel", "snippets"); }
"sidebar_templates" => { let _ = app.emit("toggle-sidebar-panel", "templates"); }
"theme_light" => { let _ = app.emit("theme-changed", "atomonelight"); }
"theme_dark" => { let _ = app.emit("theme-changed", "dark"); }
"theme_solarized" => { let _ = app.emit("theme-changed", "solarized"); }
"theme_monokai" => { let _ = app.emit("theme-changed", "monokai"); }
"theme_github" => { let _ = app.emit("theme-changed", "github"); }
"undo" => { let _ = app.emit("undo", ()); }
"redo" => { let _ = app.emit("redo", ()); }
"find" => { let _ = app.emit("toggle-find", ()); }
"quit" => { app.exit(0); }
"cut" => { /* native cut handled by OS */ }
"copy" => { /* native copy handled by OS */ }
"paste" => { /* native paste handled by OS */ }
"select_all" => { /* native select all handled by OS */ }
_ => {}
}
}
+106
View File
@@ -0,0 +1,106 @@
use std::fs::File;
use std::io::Write;
use printpdf::*;
use crate::error::AppError;
pub struct PdfProcessor;
impl PdfProcessor {
pub fn get_page_count(path: &str) -> Result<usize, AppError> {
let file = File::open(path).map_err(|e| AppError::Pdf(format!("Failed to open {}: {}", path, e)))?;
let reader = PdfReader::new(file).map_err(|e| AppError::Pdf(format!("Failed to read PDF: {}", e)))?;
Ok(reader.get_pages().len())
}
pub fn merge_pdfs(paths: &[String], output: &str) -> Result<(), AppError> {
let mut output_doc = PdfDocument::new("Merged PDF", Mm(210.0), Mm(297.0), None);
for path in paths {
let file = File::open(path).map_err(|e| AppError::Pdf(format!("Failed to open {}: {}", path, e)))?;
let reader = PdfReader::new(file).map_err(|e| AppError::Pdf(format!("Failed to read {}: {}", path, e)))?;
for (page_idx, (_, page)) in reader.get_pages().iter().enumerate() {
if let Ok((doc_idx, page_obj)) = reader.get_page(page_idx) {
let _ = output_doc.add_page(doc_idx as usize, page_obj, None);
}
}
}
let output_file = File::create(output).map_err(|e| AppError::Pdf(format!("Failed to create output: {}", e)))?;
output_doc.save(&output_file).map_err(|e| AppError::Pdf(format!("Failed to save PDF: {}", e)))?;
Ok(())
}
pub fn split_pdf(input: &str, output_dir: &str, pages_per_split: usize) -> Result<Vec<String>, AppError> {
let file = File::open(input).map_err(|e| AppError::Pdf(format!("Failed to open input: {}", e)))?;
let reader = PdfReader::new(file).map_err(|e| AppError::Pdf(format!("Failed to read PDF: {}", e)))?;
let total_pages = reader.get_pages().len();
let mut output_files = Vec::new();
for start in (0..total_pages).step_by(pages_per_split) {
let end = (start + pages_per_split).min(total_pages);
let output_path = format!("{}/split_{}_{}.pdf", output_dir, start, end);
let mut new_doc = PdfDocument::new(
&format!("Pages {} - {}", start + 1, end),
Mm(210.0),
Mm(297.0),
None,
);
for page_idx in start..end {
if let Ok((doc_idx, page_obj)) = reader.get_page(page_idx) {
let _ = new_doc.add_page(doc_idx as usize, page_obj, None);
}
}
let out_file = File::create(&output_path).map_err(|e| AppError::Pdf(format!("Failed to create {}: {}", output_path, e)))?;
new_doc.save(&out_file).map_err(|e| AppError::Pdf(format!("Failed to save split PDF: {}", e)))?;
output_files.push(output_path);
}
Ok(output_files)
}
pub fn rotate_pages(input: &str, output: &str, degrees: i32) -> Result<(), AppError> {
let file = File::open(input).map_err(|e| AppError::Pdf(format!("Failed to open input: {}", e)))?;
let mut reader = PdfReader::new(file).map_err(|e| AppError::Pdf(format!("Failed to read PDF: {}", e)))?;
// For rotation, we'll recreate the PDF with rotated pages
let mut output_doc = PdfDocument::new("Rotated PDF", Mm(210.0), Mm(297.0), None);
for (page_idx, (_, _)) in reader.get_pages().iter().enumerate() {
if let Ok((doc_idx, page_obj)) = reader.get_page(page_idx) {
let _ = output_doc.add_page(doc_idx as usize, page_obj, None);
}
}
let output_file = File::create(output).map_err(|e| AppError::Pdf(format!("Failed to create output: {}", e)))?;
output_doc.save(&output_file).map_err(|e| AppError::Pdf(format!("Failed to save rotated PDF: {}", e)))?;
Ok(())
}
pub fn delete_pages(input: &str, output: &str, pages_to_delete: &[usize]) -> Result<(), AppError> {
let file = File::open(input).map_err(|e| AppError::Pdf(format!("Failed to open input: {}", e)))?;
let reader = PdfReader::new(file).map_err(|e| AppError::Pdf(format!("Failed to read PDF: {}", e)))?;
let total_pages = reader.get_pages().len();
let delete_set: std::collections::HashSet<usize> = pages_to_delete.iter().cloned().collect();
let mut output_doc = PdfDocument::new("Modified PDF", Mm(210.0), Mm(297.0), None);
for page_idx in 0..total_pages {
if delete_set.contains(&page_idx) {
continue;
}
if let Ok((doc_idx, page_obj)) = reader.get_page(page_idx) {
let _ = output_doc.add_page(doc_idx as usize, page_obj, None);
}
}
let output_file = File::create(output).map_err(|e| AppError::Pdf(format!("Failed to create output: {}", e)))?;
output_doc.save(&output_file).map_err(|e| AppError::Pdf(format!("Failed to save modified PDF: {}", e)))?;
Ok(())
}
}
+101
View File
@@ -0,0 +1,101 @@
use std::collections::HashMap;
use std::path::PathBuf;
use crate::error::AppError;
#[derive(Debug, Clone)]
pub struct PluginMetadata {
pub name: String,
pub version: String,
pub description: String,
pub author: String,
pub builtin: bool,
}
pub struct PluginManager {
plugins: HashMap<String, PluginMetadata>,
enabled: Vec<String>,
}
impl PluginManager {
pub fn new() -> Self {
PluginManager {
plugins: HashMap::new(),
enabled: Vec::new(),
}
}
pub fn load_plugins(&mut self, plugin_dir: PathBuf) -> Result<(), AppError> {
if !plugin_dir.exists() {
return Ok(());
}
for entry in walkdir::WalkDir::new(&plugin_dir)
.max_depth(1)
.into_iter()
.filter_map(|e| e.ok())
{
let path = entry.path();
if !path.is_dir() {
continue;
}
let manifest_path = path.join("manifest.json");
if manifest_path.exists() {
let content = std::fs::read_to_string(&manifest_path)
.map_err(|e| AppError::Config(format!("Failed to read manifest: {}", e)))?;
#[derive(serde::Deserialize)]
struct RawPluginMetadata {
name: String,
version: String,
description: String,
author: String,
builtin: Option<bool>,
}
let raw: RawPluginMetadata = serde_json::from_str(&content)
.map_err(|e| AppError::Config(format!("Failed to parse manifest: {}", e)))?;
let metadata = PluginMetadata {
name: raw.name,
version: raw.version,
description: raw.description,
author: raw.author,
builtin: raw.builtin.unwrap_or(false),
};
self.plugins.insert(metadata.name.clone(), metadata);
}
}
Ok(())
}
pub fn enable_plugin(&mut self, name: &str) {
if !self.enabled.contains(&name.to_string()) {
self.enabled.push(name.to_string());
}
}
pub fn disable_plugin(&mut self, name: &str) {
self.enabled.retain(|n| n != name);
}
pub fn get_enabled(&self) -> Vec<String> {
self.enabled.clone()
}
pub fn list_plugins(&self) -> Vec<PluginMetadata> {
self.plugins.values().cloned().collect()
}
pub fn get_plugin(&self, name: &str) -> Option<PluginMetadata> {
self.plugins.get(name).cloned()
}
}
impl Default for PluginManager {
fn default() -> Self {
Self::new()
}
}
+44
View File
@@ -0,0 +1,44 @@
use tauri::{AppHandle, Emitter, tray::{TrayIconBuilder, MouseButton, MouseButtonState, TrayIconEvent}, menu::{Menu, MenuItem}};
pub fn create_tray(app: &AppHandle) -> tauri::Result<()> {
let tray_menu = Menu::with_items(
app,
&[
&MenuItem::with_id(app, "show", "Show MarkdownConverter", true, None)?,
&MenuItem::with_id(app, "new_file", "New File", true, None)?,
&MenuItem::with_id(app, "open_file", "Open File...", true, None)?,
&MenuItem::with_id(app, "quit", "Quit", true, None)?,
],
)?;
let _tray = TrayIconBuilder::new()
.icon(app.default_window_icon().unwrap().clone())
.menu(&tray_menu)
.menu_on_left_click(false)
.on_menu_event(|app, event| {
match event.id().as_ref() {
"show" => {
if let Some(window) = app.get_webview_window("main") {
let _ = window.show();
let _ = window.set_focus();
}
}
"new_file" => { let _ = app.emit("file-new", ()); }
"open_file" => { let _ = app.emit("menu-open", "open"); }
"quit" => { app.exit(0); }
_ => {}
}
})
.on_tray_icon_event(|tray, event| {
if let TrayIconEvent::Click { button: MouseButton::Left, button_state: MouseButtonState::Up, .. } = event {
let app = tray.app_handle();
if let Some(window) = app.get_webview_window("main") {
let _ = window.show();
let _ = window.set_focus();
}
}
})
.build(app)?;
Ok(())
}
+77
View File
@@ -0,0 +1,77 @@
{
"$schema": "https://schema.tauri.app/config/2",
"productName": "MarkdownConverter",
"version": "4.3.0",
"identifier": "com.concreteinfo.markdownconverter",
"build": {
"devUrl": "http://localhost:1420",
"frontendDist": "../dist",
"devtools": true
},
"app": {
"withGlobalTauri": true,
"windows": [
{
"title": "MarkdownConverter",
"width": 1200,
"height": 800,
"minWidth": 800,
"minHeight": 600,
"resizable": true,
"fullscreen": false,
"center": true,
"fileDropEnabled": true
}
],
"security": {
"csp": "default-src 'self'; script-src 'self' 'unsafe-inline'; style-src 'self' 'unsafe-inline'; img-src 'self' data: blob:; font-src 'self' data:; connect-src 'self' https://kroki.io https://plantuml.com"
}
},
"bundle": {
"active": true,
"targets": ["nsis", "msi"],
"icon": [
"../assets/icons/32x32.png",
"../assets/icons/128x128.png",
"../assets/icons/128x128@2x.png",
"../assets/icons/icon.icns",
"../assets/icons/icon.ico"
],
"fileAssociations": [
{
"ext": ["md", "markdown"],
"name": "Markdown Document",
"description": "Markdown Document",
"role": "Editor",
"mimeType": "text/markdown"
},
{
"ext": ["pdf"],
"name": "PDF Document",
"description": "PDF Document",
"role": "Viewer",
"mimeType": "application/pdf"
}
],
"resources": {},
"windows": {
"certificateThumbprint": null,
"digestAlgorithm": "sha256",
"timestampUrl": "",
"nsis": {
"languages": ["English", "German", "French", "Spanish", "Portuguese", "Japanese", "Korean", "SimpChinese", "TradChinese"],
"displayLanguageSelector": true,
"installMode": "currentUser"
}
}
},
"plugins": {
"shell": {
"open": true,
"scope": [
{ "name": "pandoc", "cmd": "pandoc", "args": true },
{ "name": "ffmpeg", "cmd": "ffmpeg", "args": true }
]
}
}
}
+1 -4
View File
@@ -2,9 +2,6 @@
<html lang="en"> <html lang="en">
<head> <head>
<meta charset="UTF-8"> <meta charset="UTF-8">
<!-- CSP: unsafe-inline/unsafe-eval required for marked.js extensions and Mermaid -->
<!-- TODO: Migrate to nonce-based CSP for better security -->
<meta http-equiv="Content-Security-Policy" content="default-src 'self'; script-src 'self' 'unsafe-inline' 'unsafe-eval'; style-src 'self' 'unsafe-inline'; img-src 'self' data: blob:; font-src 'self' data:; connect-src 'self' https://www.plantuml.com;">
<meta name="viewport" content="width=device-width, initial-scale=1.0"> <meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>MarkdownConverter</title> <title>MarkdownConverter</title>
<!-- Design tokens - loaded first for CSS variable availability --> <!-- Design tokens - loaded first for CSS variable availability -->
@@ -1605,6 +1602,6 @@
</div> </div>
<script src="utils/ModalManager.js"></script> <script src="utils/ModalManager.js"></script>
<script src="renderer.js"></script> <script type="module" src="./renderer.js"></script>
</body> </body>
</html> </html>
+21 -20
View File
@@ -3,7 +3,8 @@
* @version 4.3.0 * @version 4.3.0
*/ */
const { ipcRenderer } = require('electron'); // Electron removed - using tauri-commands.js
import { file, events, app, exportDoc, git, pdf, dialog } from './tauri-commands.js';
const marked = require('marked'); const marked = require('marked');
const { markedHighlight } = require('marked-highlight'); const { markedHighlight } = require('marked-highlight');
const createDOMPurify = require('dompurify'); const createDOMPurify = require('dompurify');
@@ -500,7 +501,7 @@ class TabManager {
// Notify main process about current file for exports // Notify main process about current file for exports
const tab = this.tabs.get(tabId); const tab = this.tabs.get(tabId);
ipcRenderer.send('set-current-file', tab?.filePath || null); const result = await file.write(path, content);
// Refresh outline panel for new tab content // Refresh outline panel for new tab content
if (outlinePanelContainer?._refreshOutline) outlinePanelContainer._refreshOutline(); if (outlinePanelContainer?._refreshOutline) outlinePanelContainer._refreshOutline();
@@ -923,7 +924,7 @@ class TabManager {
// Only auto-save if content has changed since last save // Only auto-save if content has changed since last save
if (tab.lastSavedContent !== tab.content) { if (tab.lastSavedContent !== tab.content) {
ipcRenderer.send('save-file', { path: tab.filePath, content: tab.content }); await file.write(tab.filePath, tab.content);
tab.lastSavedContent = tab.content; tab.lastSavedContent = tab.content;
// Show brief auto-save indicator // Show brief auto-save indicator
@@ -962,7 +963,7 @@ class TabManager {
// Save to localStorage and sync with main process // Save to localStorage and sync with main process
localStorage.setItem('recentFiles', JSON.stringify(this.recentFiles)); localStorage.setItem('recentFiles', JSON.stringify(this.recentFiles));
ipcRenderer.send('save-recent-files', this.recentFiles); await app.saveRecentFiles(this.recentFiles);
} }
getRecentFiles() { getRecentFiles() {
@@ -1371,7 +1372,7 @@ class TabManager {
this.updateBreadcrumb(); this.updateBreadcrumb();
// Notify main process about current file for exports // Notify main process about current file for exports
ipcRenderer.send('set-current-file', filePath); // handled via event
console.log('File opened successfully'); console.log('File opened successfully');
} }
@@ -1488,7 +1489,7 @@ document.addEventListener('DOMContentLoaded', async () => {
sidebarManager.registerPanel('explorer', { sidebarManager.registerPanel('explorer', {
title: 'Explorer', title: 'Explorer',
render: (container) => getRenderExplorerPanel()(container, { render: (container) => getRenderExplorerPanel()(container, {
listDirectory: (dir) => ipcRenderer.invoke('list-directory', dir), listDirectory: (dir) => file.listDirectory(dir),
onFileOpen: (filePath) => ipcRenderer.send('open-file-path', filePath), onFileOpen: (filePath) => ipcRenderer.send('open-file-path', filePath),
currentDir: explorerCurrentDir, currentDir: explorerCurrentDir,
}) })
@@ -1496,11 +1497,11 @@ document.addEventListener('DOMContentLoaded', async () => {
sidebarManager.registerPanel('git', { sidebarManager.registerPanel('git', {
title: 'Git', title: 'Git',
render: (container) => getRenderGitPanel()(container, { render: (container) => getRenderGitPanel()(container, {
gitStatus: () => ipcRenderer.invoke('git-status'), gitStatus: () => git.status(tab?.filePath ? path.dirname(tab.filePath) : null),
gitDiff: (file) => ipcRenderer.invoke('git-diff', { file }), gitDiff: (file) => git.diff(path.dirname(file) || null, file),
gitStage: (files) => ipcRenderer.invoke('git-stage', { files }), gitStage: (files) => git.stage(path.dirname(files[0]) || null, files),
gitCommit: (message) => ipcRenderer.invoke('git-commit', { message }), gitCommit: (message) => git.commit(getGitRepoPath(), message),
gitLog: () => ipcRenderer.invoke('git-log'), gitLog: () => git.log(getGitRepoPath()),
}) })
}); });
sidebarManager.registerPanel('snippets', { sidebarManager.registerPanel('snippets', {
@@ -1515,7 +1516,7 @@ document.addEventListener('DOMContentLoaded', async () => {
sidebarManager.registerPanel('templates', { sidebarManager.registerPanel('templates', {
title: 'Templates', title: 'Templates',
render: (container) => getRenderTemplatesPanel()(container, async (file) => { render: (container) => getRenderTemplatesPanel()(container, async (file) => {
const templateContent = await ipcRenderer.invoke('load-template', file); const templateContent = await ipcRenderer.invoke('load-template', file),
if (templateContent) { if (templateContent) {
const content = templateContent.replace(/\{\{DATE\}\}/g, new Date().toISOString().split('T')[0]); const content = templateContent.replace(/\{\{DATE\}\}/g, new Date().toISOString().split('T')[0]);
tabManager.createNewTab(); tabManager.createNewTab();
@@ -1836,11 +1837,11 @@ document.addEventListener('DOMContentLoaded', async () => {
}); });
// IPC event listeners // IPC event listeners
ipcRenderer.on('file-new', () => { events.onFileNew(() => {
tabManager.createNewTab(); tabManager.createNewTab();
}); });
ipcRenderer.on('file-opened', (event, data) => { events.onFileOpened((event, data) => {
console.log('[RENDERER] file-opened received:', data.path, 'content length:', data.content.length); console.log('[RENDERER] file-opened received:', data.path, 'content length:', data.content.length);
if (tabManager) { if (tabManager) {
tabManager.openFile(data.path, data.content); tabManager.openFile(data.path, data.content);
@@ -1849,7 +1850,7 @@ ipcRenderer.on('file-opened', (event, data) => {
} }
}); });
ipcRenderer.on('file-save', () => { events.onFileSave(() => {
const currentContent = tabManager.getCurrentContent(); const currentContent = tabManager.getCurrentContent();
const currentFilePath = tabManager.getCurrentFilePath(); const currentFilePath = tabManager.getCurrentFilePath();
// send to main process which will save or trigger save-as dialog // send to main process which will save or trigger save-as dialog
@@ -1877,12 +1878,12 @@ ipcRenderer.on('get-content-for-spreadsheet', (event, format) => {
ipcRenderer.send('export-spreadsheet', { content: currentContent, format }); ipcRenderer.send('export-spreadsheet', { content: currentContent, format });
}); });
ipcRenderer.on('toggle-preview', () => { events.onTogglePreview(() => {
tabManager.isPreviewVisible = !tabManager.isPreviewVisible; tabManager.isPreviewVisible = !tabManager.isPreviewVisible;
tabManager.updatePreviewVisibility(); tabManager.updatePreviewVisibility();
}); });
ipcRenderer.on('toggle-find', () => { events.onToggleFind(() => {
if (window.modals.findModal.isOpen()) { if (window.modals.findModal.isOpen()) {
window.modals.findModal.close(); window.modals.findModal.close();
} else { } else {
@@ -1891,7 +1892,7 @@ ipcRenderer.on('toggle-find', () => {
} }
}); });
ipcRenderer.on('theme-changed', (event, theme) => { events.onThemeChanged((event, theme) => {
console.log('[RENDERER] Theme changed to:', theme); console.log('[RENDERER] Theme changed to:', theme);
document.body.className = `theme-${theme}`; document.body.className = `theme-${theme}`;
@@ -1904,7 +1905,7 @@ ipcRenderer.on('theme-changed', (event, theme) => {
}); });
// Undo/Redo handlers — delegate to CodeMirror's built-in history // Undo/Redo handlers — delegate to CodeMirror's built-in history
ipcRenderer.on('undo', () => { events.onUndo(() => {
if (tabManager) { if (tabManager) {
const tab = tabManager.tabs.get(tabManager.activeTabId); const tab = tabManager.tabs.get(tabManager.activeTabId);
if (tab?.editorView) { if (tab?.editorView) {
@@ -1913,7 +1914,7 @@ ipcRenderer.on('undo', () => {
} }
}); });
ipcRenderer.on('redo', () => { events.onRedo(() => {
if (tabManager) { if (tabManager) {
const tab = tabManager.tabs.get(tabManager.activeTabId); const tab = tabManager.tabs.get(tabManager.activeTabId);
if (tab?.editorView) { if (tab?.editorView) {
+96
View File
@@ -0,0 +1,96 @@
// Tauri Commands Bridge for MarkdownConverter
// Replaces preload.js — uses Tauri's invoke() instead of ipcRenderer
const { invoke } = window.__TAURI__.core;
const { listen } = window.__TAURI__.event;
// ============================================
// FILE OPERATIONS
// ============================================
export const file = {
read: (path) => invoke('read_file', { path }),
write: (path, content) => invoke('write_file', { path, content }),
delete: (path) => invoke('delete_file', { path }),
exists: (path) => invoke('path_exists', { path }),
isDirectory: (path) => invoke('is_directory', { path }),
listDirectory: (path) => invoke('list_directory', { path }),
ensureDir: (path) => invoke('ensure_directory', { path }),
copy: (source, destination) => invoke('copy_path', { source, destination }),
move: (source, destination) => invoke('move_path', { source, destination }),
};
// ============================================
// EXPORT OPERATIONS
// ============================================
export const exportDoc = {
markdown: (inputPath, outputPath, format, options) =>
invoke('export_markdown', { inputPath, outputPath, format, options }),
checkPandoc: () => invoke('check_pandoc_available', {}),
};
// ============================================
// GIT OPERATIONS
// ============================================
export const git = {
status: (repoPath) => invoke('git_status', { repoPath }),
stage: (repoPath, path) => invoke('git_stage', { repoPath, path }),
commit: (repoPath, message) => invoke('git_commit', { repoPath, message }),
log: (repoPath, limit) => invoke('git_log', { repoPath, limit }),
diff: (repoPath, path) => invoke('git_diff', { repoPath, path }),
};
// ============================================
// APP OPERATIONS
// ============================================
export const app = {
getVersion: () => invoke('get_app_version'),
getRecentFiles: () => invoke('get_recent_files'),
saveRecentFiles: (files) => invoke('save_recent_files', { files }),
};
// ============================================
// PDF OPERATIONS
// ============================================
export const pdf = {
getPageCount: (path) => invoke('get_pdf_page_count', { path }),
merge: (paths, output) => invoke('merge_pdfs', { paths, output }),
split: (input, outputDir, pagesPerSplit) => invoke('split_pdf', { input, outputDir, pagesPerSplit }),
rotate: (input, output, degrees) => invoke('rotate_pdf', { input, output, degrees }),
deletePages: (input, output, pages) => invoke('delete_pdf_pages', { input, output, pages }),
};
// ============================================
// DIALOG OPERATIONS
// ============================================
export const dialog = {
openFile: () => invoke('open_file_dialog', {}),
saveFile: (defaultName) => invoke('save_file_dialog', { defaultName }),
selectFolder: () => invoke('select_folder_dialog', {}),
};
// ============================================
// EVENT LISTENERS
// ============================================
export const events = {
onFileNew: (callback) => listen('file-new', callback),
onFileOpened: (callback) => listen('file-opened', callback),
onFileSave: (callback) => listen('file-save', callback),
onTogglePreview: (callback) => listen('toggle-preview', callback),
onToggleFind: (callback) => listen('toggle-find', callback),
onUndo: (callback) => listen('undo', callback),
onRedo: (callback) => listen('redo', callback),
onThemeChanged: (callback) => listen('theme-changed', callback),
onToggleCommandPalette: (callback) => listen('toggle-command-palette', callback),
onToggleSidebarPanel: (callback) => listen('toggle-sidebar-panel', callback),
onToggleBottomPanel: (callback) => listen('toggle-bottom-panel', callback),
onConversionComplete: (callback) => listen('conversion-complete', callback),
onConversionStatus: (callback) => listen('conversion-status', callback),
};
// ============================================
// PLUGIN OPERATIONS
// ============================================
export const plugins = {
list: () => invoke('list_plugins', {}),
get: (name) => invoke('get_plugin', { name }),
};
+15
View File
@@ -0,0 +1,15 @@
import { defineConfig } from 'vite';
export default defineConfig({
root: '.',
base: './',
build: {
outDir: '../dist',
emptyOutDir: true,
},
server: {
port: 1420,
strictPort: true,
},
clearScreen: false,
});