Initial commit: md-manuscript editor

Rust/egui desktop app: draggable markdown file list, editor, git sync,
per-file chapter titles, and native ODT export. Includes README and
Debian 12 (Surface) install guide.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
landon
2026-07-25 15:17:42 -05:00
commit f473b63ee3
11 changed files with 6201 additions and 0 deletions
+84
View File
@@ -0,0 +1,84 @@
use std::collections::HashMap;
use std::path::Path;
/// Name of the file (inside the workspace) that stores the manual ordering.
/// It is committed to git so the order syncs across machines.
pub const ORDER_FILE: &str = "order.json";
/// Name of the file that stores per-file chapter title overrides
/// (a JSON object mapping `file name` -> `chapter title`). Also committed to git.
pub const TITLES_FILE: &str = "titles.json";
/// Read the chapter-title overrides. Missing/invalid -> empty map.
pub fn read_titles(workspace: &Path) -> HashMap<String, String> {
let path = workspace.join(TITLES_FILE);
match std::fs::read_to_string(&path) {
Ok(text) => serde_json::from_str(&text).unwrap_or_default(),
Err(_) => HashMap::new(),
}
}
/// Persist the chapter-title overrides as JSON into the workspace.
pub fn write_titles(workspace: &Path, titles: &HashMap<String, String>) -> std::io::Result<()> {
let path = workspace.join(TITLES_FILE);
let text = serde_json::to_string_pretty(titles).unwrap_or_else(|_| "{}".to_string());
std::fs::write(path, text)
}
/// Read the saved order (a JSON array of file names). Missing/invalid -> empty.
fn read_saved_order(workspace: &Path) -> Vec<String> {
let path = workspace.join(ORDER_FILE);
match std::fs::read_to_string(&path) {
Ok(text) => serde_json::from_str(&text).unwrap_or_default(),
Err(_) => Vec::new(),
}
}
/// Persist the given order as JSON into the workspace.
pub fn write_order(workspace: &Path, order: &[String]) -> std::io::Result<()> {
let path = workspace.join(ORDER_FILE);
let text = serde_json::to_string_pretty(order).unwrap_or_else(|_| "[]".to_string());
std::fs::write(path, text)
}
/// List all `*.md` files currently present in the workspace (file names only).
fn list_markdown_files(workspace: &Path) -> Vec<String> {
let mut files = Vec::new();
if let Ok(entries) = std::fs::read_dir(workspace) {
for entry in entries.flatten() {
let path = entry.path();
if path.is_file() {
if let Some(ext) = path.extension() {
if ext.eq_ignore_ascii_case("md") {
if let Some(name) = path.file_name().and_then(|n| n.to_str()) {
files.push(name.to_string());
}
}
}
}
}
}
files
}
/// Produce the effective ordered list of markdown files:
/// 1. saved-order entries that still exist on disk, in saved order
/// 2. any new `.md` files not yet in the saved order, appended alphabetically
pub fn resolve_order(workspace: &Path) -> Vec<String> {
let saved = read_saved_order(workspace);
let mut present = list_markdown_files(workspace);
present.sort_by(|a, b| a.to_lowercase().cmp(&b.to_lowercase()));
let mut result: Vec<String> = Vec::new();
for name in &saved {
if present.contains(name) && !result.contains(name) {
result.push(name.clone());
}
}
for name in present {
if !result.contains(&name) {
result.push(name);
}
}
result
}