use std::path::PathBuf; use just_fmt::kebab_case; use mingling::{ Groupped, macros::{chain, dispatcher, renderer}, parser::AsPicker, }; use serde::Serialize; use crate::Constants; const SUGGEST_READ: &str = "Please use `memm read {}` to view the memory again"; const SUGGEST_EXPLORE: &str = "Please use `memm explore` to view available memories"; dispatcher!("remember", CMDRemember => EntryRemember); dispatcher!("rewrite", CMDRewrite => EntryRewrite); dispatcher!("forget", CMDForget => EntryForget); dispatcher!("explore", CMDExplore => EntryExplore); dispatcher!("read", CMDRead => EntryRead); dispatcher!("dumpall", CMDDumpAll => EntryDumpAll); #[chain] pub fn handle_remember(args: EntryRemember, constants: &Constants) -> Next { let (title, content) = args.pick(()).pick::>(()).unpack(); remember(constants, title, content.join(" ")).to_render() } #[chain] pub fn handle_rewrite(args: EntryRewrite, constants: &Constants) -> Next { let (title, content) = args.pick(()).pick::>(()).unpack(); rewrite(constants, title, content.join(" ")).to_render() } #[chain] pub fn handle_forget(args: EntryForget, constants: &Constants) -> Next { let title = args.pick(()).unpack(); forget(constants, title).to_render() } #[chain] pub fn handle_explore(_p: EntryExplore, constants: &Constants) -> Next { explore(constants).to_render() } #[chain] pub fn handle_read(args: EntryRead, constants: &Constants) -> Next { let title = args.pick(()).unpack(); read(constants, title).to_render() } #[chain] pub fn handle_dumpall(args: EntryDumpAll, constants: &Constants) -> Next { let about = args.pick::("--about").unpack(); dumpall(constants, about).to_render() } #[derive(Serialize, Groupped)] pub struct ResultExplore { pub titles: Vec, } #[renderer] pub fn phantom_render_result_explore(_p: ResultExplore) {} #[derive(Serialize, Groupped)] pub struct ResultRead { pub exist: bool, pub read_success: bool, pub content_lines: Vec, } #[renderer] pub fn phantom_render_result_read(_p: ResultRead) {} #[derive(Serialize, Groupped)] pub struct ResultWritten { pub exist: bool, pub write_success: bool, pub suggest: String, } #[renderer] pub fn phantom_render_result_written(_p: ResultWritten) {} #[derive(Serialize, Groupped)] pub struct ResultForgotten { pub exist: bool, pub forget_success: bool, pub suggest: String, } #[renderer] pub fn phantom_render_result_forgotten(_p: ResultForgotten) {} #[derive(Serialize, Groupped)] pub struct ResultDumpAll { pub entries: Vec, } #[derive(Serialize)] pub struct DumpEntry { pub title: String, pub content: String, } #[renderer] pub fn phantom_render_result_dumpall(_p: ResultDumpAll) {} fn explore(constants: &Constants) -> ResultExplore { let store_root = &constants.store_root; let mut titles = Vec::new(); if let Ok(entries) = std::fs::read_dir(store_root) { for entry in entries.flatten() { let path = entry.path(); if let Some(ext) = path.extension() { if ext == "md" { if let Some(stem) = path.file_stem() { titles.push(stem.to_string_lossy().to_string()); } } } } } ResultExplore { titles } } fn read(constants: &Constants, title: String) -> ResultRead { let target_file = item_path(constants, &title); if !target_file.exists() { return ResultRead { exist: false, read_success: false, content_lines: Vec::new(), }; } match std::fs::read_to_string(&target_file) { Ok(content) => ResultRead { exist: true, read_success: true, content_lines: content.lines().map(|line| line.to_string()).collect(), }, Err(_) => ResultRead { exist: true, read_success: false, content_lines: Vec::new(), }, } } fn rewrite(constants: &Constants, title: String, content: String) -> ResultWritten { let content = content.replace("\\n", "\n"); let target_file = item_path(constants, &title); match std::fs::write(&target_file, content) { Ok(_) => ResultWritten { exist: true, write_success: true, suggest: SUGGEST_READ.replace("{}", &title), }, Err(_) => ResultWritten { exist: true, write_success: false, suggest: SUGGEST_READ.replace("{}", &title), }, } } fn remember(constants: &Constants, title: String, content: String) -> ResultWritten { let content = content.replace("\\n", "\n"); let target_file = item_path(constants, &title); match std::fs::OpenOptions::new() .append(true) .create(true) .open(&target_file) { Ok(mut file) => { use std::io::Write; match writeln!(file, "{}", content) { Ok(_) => ResultWritten { exist: true, write_success: true, suggest: SUGGEST_READ.replace("{}", &title), }, Err(_) => ResultWritten { exist: true, write_success: false, suggest: SUGGEST_READ.replace("{}", &title), }, } } Err(_) => ResultWritten { exist: false, write_success: false, suggest: String::new(), }, } } fn forget(constants: &Constants, title: String) -> ResultForgotten { let target_file = item_path(constants, &title); if !target_file.exists() { return ResultForgotten { exist: false, forget_success: false, suggest: SUGGEST_EXPLORE.to_string(), }; } match std::fs::remove_file(&target_file) { Ok(_) => ResultForgotten { exist: true, forget_success: true, suggest: SUGGEST_EXPLORE.to_string(), }, Err(_) => ResultForgotten { exist: true, forget_success: false, suggest: SUGGEST_EXPLORE.to_string(), }, } } fn dumpall(constants: &Constants, about: String) -> ResultDumpAll { let store_root = &constants.store_root; let mut entries = Vec::new(); if let Ok(dir_entries) = std::fs::read_dir(store_root) { for entry in dir_entries.flatten() { let path = entry.path(); if let Some(ext) = path.extension() { if ext == "md" { if let Some(stem) = path.file_stem() { let title = stem.to_string_lossy().to_string(); let content = std::fs::read_to_string(&path).unwrap_or_default(); // If about is provided and not empty, do fuzzy matching on lowercase alphanumeric if !about.is_empty() { let filter: String = about .chars() .filter(|c| c.is_alphanumeric()) .collect::() .to_lowercase(); let title_clean: String = title .chars() .filter(|c| c.is_alphanumeric()) .collect::() .to_lowercase(); if !title_clean.contains(&filter) { continue; } } entries.push(DumpEntry { title, content }); } } } } } ResultDumpAll { entries } } fn item_path(constants: &Constants, title: &String) -> PathBuf { constants .store_root .join(format!("{}.md", kebab_case!(title))) }