Skip to main content

to/
lib.rs

1mod cli;
2mod error;
3mod project;
4mod scan;
5mod todo;
6
7use std::collections::HashSet;
8use std::env;
9use std::io::{self, Write};
10
11use cli::Command;
12pub use error::{AppError, Result};
13use project::{find_todo_file, init_todo_file};
14use scan::scan_project;
15use todo::TodoList;
16
17pub fn run() -> Result<()> {
18    let command = cli::parse_args(env::args_os().skip(1))?;
19    execute(command, &mut io::stdout())
20}
21
22fn execute<W: Write>(command: Command, writer: &mut W) -> Result<()> {
23    match command {
24        Command::Help => {
25            writer.write_all(cli::HELP_TEXT.as_bytes())?;
26        }
27        Command::Init => {
28            let cwd = env::current_dir()?;
29            let path = init_todo_file(&cwd)?;
30            writeln!(writer, "Initialized {}", path.display())?;
31        }
32        other => {
33            let cwd = env::current_dir()?;
34            let todo_path = find_todo_file(&cwd)?;
35            let mut todos = TodoList::load(&todo_path)?;
36
37            match other {
38                Command::List => write_task_list(writer, &todo_path, &todos)?,
39                Command::Add(text) => {
40                    let index = todos.add(text)?;
41                    let task = &todos.tasks()[index - 1];
42                    todos.save(&todo_path)?;
43                    writeln!(writer, "Added task {index}: {}", task.text)?;
44                }
45                Command::Done(index) => {
46                    let task = todos.mark_done(index)?.text.clone();
47                    todos.save(&todo_path)?;
48                    writeln!(writer, "Completed task {index}: {task}")?;
49                }
50                Command::Uncheck(index) => {
51                    let task = todos.mark_undone(index)?.text.clone();
52                    todos.save(&todo_path)?;
53                    writeln!(writer, "Unchecked task {index}: {task}")?;
54                }
55                Command::Scan => {
56                    let project_root = todo_path
57                        .parent()
58                        .unwrap_or_else(|| std::path::Path::new("."));
59                    let scanned_tasks = scan_project(project_root)?;
60                    let mut existing = todos
61                        .tasks()
62                        .iter()
63                        .map(|task| task.text.clone())
64                        .collect::<HashSet<_>>();
65                    let mut added = 0usize;
66
67                    for task in scanned_tasks {
68                        if existing.insert(task.clone()) {
69                            todos.add(task)?;
70                            added += 1;
71                        }
72                    }
73
74                    if added == 0 {
75                        writeln!(writer, "No new TODO comments found in git-tracked files.")?;
76                    } else {
77                        todos.save(&todo_path)?;
78                        writeln!(
79                            writer,
80                            "Added {added} task{} from git-tracked TODO comments.",
81                            if added == 1 { "" } else { "s" }
82                        )?;
83                    }
84                }
85                Command::Remove(index) => {
86                    let task = todos.remove(index)?;
87                    todos.save(&todo_path)?;
88                    writeln!(writer, "Removed task {index}: {}", task.text)?;
89                }
90                Command::Next => {
91                    if let Some((index, task)) = todos.next_open_task() {
92                        writeln!(writer, "Next task: {index}. {}", task.text)?;
93                    } else {
94                        writeln!(writer, "All tasks are complete.")?;
95                    }
96                }
97                Command::Help | Command::Init => unreachable!("handled above"),
98            }
99        }
100    }
101
102    Ok(())
103}
104
105fn write_task_list<W: Write>(
106    writer: &mut W,
107    todo_path: &std::path::Path,
108    todos: &TodoList,
109) -> Result<()> {
110    writeln!(writer, "Tasks from {}", todo_path.display())?;
111
112    if todos.tasks().is_empty() {
113        writeln!(writer, "No tasks yet.")?;
114        return Ok(());
115    }
116
117    for (index, task) in todos.tasks().iter().enumerate() {
118        let marker = if task.done { "[x]" } else { "[ ]" };
119        writeln!(writer, "{}. {} {}", index + 1, marker, task.text)?;
120    }
121
122    writeln!(
123        writer,
124        "Open: {}  Done: {}",
125        todos.open_count(),
126        todos.done_count()
127    )?;
128    Ok(())
129}
130
131#[cfg(test)]
132mod tests {
133    use super::*;
134
135    #[test]
136    fn help_command_writes_usage() {
137        let mut output = Vec::new();
138        execute(Command::Help, &mut output).unwrap();
139
140        let rendered = String::from_utf8(output).unwrap();
141        assert!(rendered.contains("to ls"));
142        assert!(rendered.contains("to init"));
143    }
144}