// synops-tasks — Parse tasks.md og vis oppgavestatus. // // Leser tasks.md fra repo-roten og parser oppgaver med status, // fase og detaljer. Støtter filtrering på fase og status. // // Output: formatert oppgaveliste til stdout. // // Ref: docs/retninger/unix_filosofi.md use clap::Parser; use regex::Regex; use std::path::PathBuf; use std::process; /// Parse tasks.md og vis oppgavestatus. #[derive(Parser)] #[command(name = "synops-tasks", about = "Vis oppgavestatus fra tasks.md")] struct Cli { /// Filtrer på fase (f.eks. 21 for "Fase 21") #[arg(long)] phase: Option, /// Filtrer på status: todo, done, blocked, inprogress, question, all #[arg(long, default_value = "all")] status: String, } #[derive(Debug, Clone)] struct Task { phase: u32, phase_title: String, id: String, title: String, status: TaskStatus, details: Vec, } #[derive(Debug, Clone, PartialEq)] enum TaskStatus { Todo, InProgress, Done, Question, Blocked, } impl TaskStatus { fn symbol(&self) -> &str { match self { TaskStatus::Todo => "[ ]", TaskStatus::InProgress => "[~]", TaskStatus::Done => "[x]", TaskStatus::Question => "[?]", TaskStatus::Blocked => "[!]", } } fn label(&self) -> &str { match self { TaskStatus::Todo => "Klar", TaskStatus::InProgress => "Pågår", TaskStatus::Done => "Ferdig", TaskStatus::Question => "Spørsmål", TaskStatus::Blocked => "Blokkert", } } fn matches_filter(&self, filter: &str) -> bool { match filter { "all" => true, "todo" => *self == TaskStatus::Todo, "done" => *self == TaskStatus::Done, "blocked" => *self == TaskStatus::Blocked || *self == TaskStatus::Question, "inprogress" => *self == TaskStatus::InProgress, "question" => *self == TaskStatus::Question, _ => true, } } } fn find_tasks_md() -> Result { // Prøv relativ sti først (repo-rot), deretter absolutt let candidates = [ PathBuf::from("tasks.md"), PathBuf::from("/home/vegard/synops/tasks.md"), ]; for path in &candidates { if path.exists() { return Ok(path.clone()); } } Err("Fant ikke tasks.md (prøvde ./ og /home/vegard/synops/)".to_string()) } fn parse_tasks(content: &str) -> Vec { let phase_re = Regex::new(r"^## Fase (\d+):\s*(.+)$").unwrap(); let task_re = Regex::new(r"^- \[(.)\] (\d+\.\d+)\s+(.+)$").unwrap(); let detail_re = Regex::new(r"^ > (.+)$").unwrap(); let mut tasks = Vec::new(); let mut current_phase: Option<(u32, String)> = None; for line in content.lines() { if let Some(caps) = phase_re.captures(line) { let num: u32 = caps[1].parse().unwrap_or(0); let title = caps[2].trim().to_string(); current_phase = Some((num, title)); continue; } if let Some(caps) = task_re.captures(line) { let status_char = &caps[1]; let id = caps[2].to_string(); let title = caps[3].to_string(); let status = match status_char { " " => TaskStatus::Todo, "~" => TaskStatus::InProgress, "x" => TaskStatus::Done, "?" => TaskStatus::Question, "!" => TaskStatus::Blocked, _ => TaskStatus::Todo, }; let (phase, phase_title) = current_phase .clone() .unwrap_or((0, "Ukjent".to_string())); tasks.push(Task { phase, phase_title, id, title, status, details: Vec::new(), }); continue; } if let Some(caps) = detail_re.captures(line) { if let Some(task) = tasks.last_mut() { task.details.push(caps[1].to_string()); } } } tasks } fn main() { let cli = Cli::parse(); let valid_statuses = ["all", "todo", "done", "blocked", "inprogress", "question"]; if !valid_statuses.contains(&cli.status.as_str()) { eprintln!( "Ugyldig status «{}». Gyldige verdier: {}", cli.status, valid_statuses.join(", ") ); process::exit(1); } let path = match find_tasks_md() { Ok(p) => p, Err(e) => { eprintln!("Feil: {e}"); process::exit(1); } }; let content = match std::fs::read_to_string(&path) { Ok(c) => c, Err(e) => { eprintln!("Kunne ikke lese {}: {e}", path.display()); process::exit(1); } }; let tasks = parse_tasks(&content); // Filtrer let filtered: Vec<&Task> = tasks .iter() .filter(|t| { if let Some(phase) = cli.phase { if t.phase != phase { return false; } } t.status.matches_filter(&cli.status) }) .collect(); if filtered.is_empty() { let mut msg = String::from("Ingen oppgaver funnet"); if let Some(phase) = cli.phase { msg.push_str(&format!(" i fase {phase}")); } if cli.status != "all" { msg.push_str(&format!(" med status «{}»", cli.status)); } println!("{msg}."); return; } // Grupper etter fase let mut current_phase: Option = None; // Sammendrag let total = filtered.len(); let done = filtered.iter().filter(|t| t.status == TaskStatus::Done).count(); let todo = filtered.iter().filter(|t| t.status == TaskStatus::Todo).count(); let progress = filtered .iter() .filter(|t| t.status == TaskStatus::InProgress) .count(); let blocked = filtered .iter() .filter(|t| t.status == TaskStatus::Blocked || t.status == TaskStatus::Question) .count(); if let Some(phase) = cli.phase { println!("# Fase {phase}\n"); } else { println!("# Oppgavestatus\n"); } println!( "Totalt: {total} | Ferdig: {done} | Klar: {todo} | Pågår: {progress} | Blokkert: {blocked}\n" ); for task in &filtered { if current_phase != Some(task.phase) { current_phase = Some(task.phase); println!("## Fase {}: {}\n", task.phase, task.phase_title); } println!( " {} {} {}", task.status.symbol(), task.id, task.title ); // Vis statuslabel for ikke-ferdige oppgaver if task.status != TaskStatus::Done { println!(" Status: {}", task.status.label()); } for detail in &task.details { println!(" > {detail}"); } } // Fremdriftsbar for filtrert sett if total > 0 { let pct = (done as f64 / total as f64 * 100.0) as u32; let bar_width = 30; let filled = (pct as usize * bar_width) / 100; let empty = bar_width - filled; println!( "\nFremdrift: [{}{}] {pct}% ({done}/{total})", "█".repeat(filled), "░".repeat(empty) ); } }