tix-tracker 0.12.0

A terminal issue tracker: markdown + YAML frontmatter issues with a kanban TUI and CLI
Documentation
//! `tix layout`: inspect and migrate the configured on-disk issue layout.

use crate::cli::LayoutAction;
use crate::config::Config;
use crate::error::Result;
use crate::layout;
use crate::storage::Store;

/// Dispatch `tix layout show|migrate`.
pub fn run(config: &Config, store: &Store, action: &LayoutAction) -> Result<()> {
    match action {
        LayoutAction::Show => show(config, store),
        LayoutAction::Migrate { apply } => migrate(config, store, *apply),
    }
}

/// Print the effective templates and a worked example for the first issue.
fn show(config: &Config, store: &Store) -> Result<()> {
    let layout = &config.layout;
    println!("layout.directory = {}", layout.directory);
    println!("layout.filename  = {}", layout.filename);
    println!("layout.move_on_change = {}", layout.move_on_change);
    if let Some(issue) = store.list_all()?.into_iter().next() {
        println!(
            "example: #{} -> {}",
            issue.number,
            crate::layout::render(layout, &issue)
        );
    }
    Ok(())
}

/// Relocate every issue into the directory the configured template renders,
/// preserving each file's own name (and its archived-marker dot). Dry run by
/// default; `--apply` writes the moves. A collision is refused per issue (the
/// file is left in place), so a partial migration is reported rather than
/// silently skipped.
fn migrate(config: &Config, store: &Store, apply: bool) -> Result<()> {
    let mut moved = 0usize;
    let mut refused = 0usize;
    for issue in store.list_all()? {
        let dir = layout::render_directory(&config.layout, &issue);
        let Some((current_dir, base)) = issue.filename.rsplit_once('/') else {
            continue;
        };
        if current_dir == dir {
            continue;
        }
        let target = format!("{dir}/{base}");
        if apply {
            match store.migrate_issue(&issue.filename, &target) {
                Ok(()) => moved += 1,
                Err(e) => {
                    refused += 1;
                    eprintln!("{e}");
                }
            }
        } else {
            println!("{} -> {}", issue.filename, target);
            moved += 1;
        }
    }
    if apply {
        println!("migrated {moved} issue(s); {refused} refused.");
    } else {
        println!("would migrate {moved} issue(s) (dry run; pass --apply).");
    }
    Ok(())
}