use std::path::{Path, PathBuf};
use anyhow::{Context, Result, bail};
use crate::layout::{self, Plan};
pub fn slug(name: &str) -> String {
let mut out = String::new();
for c in name.chars().flat_map(char::to_lowercase) {
if c.is_alphanumeric() {
out.push(c);
} else if !out.is_empty() && !out.ends_with('-') {
out.push('-');
}
}
let out = out.trim_end_matches('-').to_string();
if out.is_empty() { "plan".into() } else { out }
}
pub fn dir() -> PathBuf {
layout::plan_path().with_file_name("plans")
}
pub fn made_path(name: &str) -> PathBuf {
dir().join(format!("{}.toml", slug(name)))
}
pub fn summary_of(plan: &Path) -> PathBuf {
plan.with_extension("md")
}
pub fn parked(progress: &Path, slug: &str) -> PathBuf {
progress
.with_file_name("plans")
.join(format!("{slug}.json"))
}
pub fn fresh(progress: &Path) -> bool {
!layout::plan_path().exists() && !progress.exists()
}
pub fn active_slug() -> String {
slug(&layout::plan().name)
}
fn builtin_slug() -> String {
slug(
&Plan::parse(layout::EXAMPLE)
.expect("the example plan is valid")
.name,
)
}
pub fn list() -> Vec<String> {
let mut out: Vec<String> = std::fs::read_dir(dir())
.into_iter()
.flatten()
.flatten()
.filter_map(|e| {
let p = e.path();
(p.extension()? == "toml").then(|| p.file_stem()?.to_str().map(String::from))?
})
.collect();
let builtin = builtin_slug();
if !out.contains(&builtin) {
out.push(builtin);
}
out.sort();
out
}
pub fn activate(made: &Path) -> Result<()> {
let to = layout::plan_path();
if let Some(d) = to.parent() {
std::fs::create_dir_all(d)?;
}
std::fs::copy(made, &to).with_context(|| format!("writing {}", to.display()))?;
Ok(())
}
pub fn switch(target: &str, progress: &Path) -> Result<String> {
let current = active_slug();
let want = slug(target);
if want == current {
bail!("you're already practising {}", layout::plan().name);
}
let made = dir().join(format!("{want}.toml"));
let builtin = want == builtin_slug() && !made.exists();
if !builtin && !made.exists() {
bail!("no plan called {target}; you have: {}", list().join(", "));
}
let plan = if builtin {
Plan::parse(layout::EXAMPLE)?
} else {
Plan::load(&made)?
};
let active = layout::plan_path();
let keep = dir().join(format!("{current}.toml"));
if active.exists() && !keep.exists() {
std::fs::create_dir_all(dir())?;
std::fs::copy(&active, &keep).with_context(|| format!("keeping {}", keep.display()))?;
}
let back = parked(progress, &want);
let resumed = back.exists();
let carried = !resumed && progress.exists() && same_stages(layout::plan(), &plan);
if progress.exists() && !carried {
let to = parked(progress, ¤t);
std::fs::create_dir_all(to.parent().expect("has a parent"))?;
std::fs::rename(progress, &to).with_context(|| format!("parking {}", to.display()))?;
}
if resumed {
std::fs::rename(&back, progress)
.with_context(|| format!("restoring {}", back.display()))?;
}
if builtin {
if active.exists() {
std::fs::remove_file(&active)?;
}
} else {
activate(&made)?;
}
if carried {
return Ok(format!(
"Now practising {}. Its stages are the same as {current}'s, so your progress came with you.",
plan.name
));
}
Ok(format!(
"Now practising {}{}. Your progress on {current} is kept for when you switch back.",
plan.name,
if resumed {
", where you left off"
} else {
" from the start"
}
))
}
fn same_stages(a: &Plan, b: &Plan) -> bool {
a.stages.len() == b.stages.len()
&& a.stages
.iter()
.zip(&b.stages)
.all(|(x, y)| x.keys == y.keys)
}
pub fn remove(name: &str, progress: &Path) -> Result<String> {
let want = slug(name);
if want == active_slug() {
bail!(
"{} is the plan you're practising; switch to another with `very-normal use` first",
layout::plan().name
);
}
let made = dir().join(format!("{want}.toml"));
if !made.exists() {
if want == builtin_slug() {
bail!("the built-in plan can't be removed");
}
bail!("no plan called {name}; you have: {}", list().join(", "));
}
let mut gone = vec![made.clone()];
std::fs::remove_file(&made)?;
for extra in [summary_of(&made), parked(progress, &want)] {
if extra.exists() {
std::fs::remove_file(&extra)?;
gone.push(extra);
}
}
Ok(format!(
"Removed {}.",
gone.iter()
.map(|p| p.display().to_string())
.collect::<Vec<_>>()
.join(", ")
))
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn slugs_are_file_names() {
assert_eq!(slug("Home Row Hero"), "home-row-hero");
assert_eq!(slug(" Very Normal! "), "very-normal");
assert_eq!(slug("Mark's"), "mark-s");
assert_eq!(slug("!!"), "plan");
}
}