f473b63ee3
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>
85 lines
3.1 KiB
Rust
85 lines
3.1 KiB
Rust
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
|
|
}
|