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:
@@ -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
|
||||
}
|
||||
Reference in New Issue
Block a user