//! Command implementations for the parts todo system. //! //! This module contains all the core functionality for managing todo items, //! including adding, listing, prioritizing, archiving, and interactive review. use crate::content::{add_entry_to_content, archive_items_in_content, prioritize_items_in_content}; use crate::input::read_key_input; use std::collections::HashSet; use std::env; use std::fs::{self, File, OpenOptions}; use std::io::{self, BufRead, Write}; use std::path::Path; use std::process::Command; use termion::color; /// Adds a new entry to the top of the note file. /// Creates the file if it doesn't exist, otherwise prepends the text to existing content. pub fn add_entry(path: &Path, text: &str) -> io::Result<()> { let mut content = String::new(); if path.exists() { content = fs::read_to_string(path)?; } let new_content = add_entry_to_content(&content, text); fs::write(path, new_content) } /// Lists notes from the file with optional filtering and pagination. /// Displays numbered items with colored output, supports case-insensitive filtering. pub fn list_note(path: &Path, num: usize, all: bool, filter: Option<&str>) -> io::Result<()> { if !path.exists() { println!( "{}No notes yet.{}", color::Fg(color::Yellow), color::Fg(color::Reset) ); return Ok(()); } let file = File::open(path)?; let lines: Vec = io::BufReader::new(file).lines().collect::>()?; let filtered_lines: Vec<(usize, &String)> = if let Some(filter_text) = filter { lines .iter() .enumerate() .filter(|(_, line)| line.to_lowercase().contains(&filter_text.to_lowercase())) .collect() } else { lines.iter().enumerate().collect() }; if filtered_lines.is_empty() { if filter.is_some() { println!( "{}No items found matching filter: \"{}\"{}", color::Fg(color::Yellow), filter.unwrap(), color::Fg(color::Reset) ); } else { println!( "{}No notes yet.{}", color::Fg(color::Yellow), color::Fg(color::Reset) ); } return Ok(()); } let display_num = if all { filtered_lines.len() } else { num.min(filtered_lines.len()) }; for (original_idx, line) in filtered_lines.iter().take(display_num) { println!( "{}{}. {}{}", color::Fg(color::Blue), original_idx + 1, color::Fg(color::Reset), line ); } if !all && filter.is_some() && filtered_lines.len() > display_num { println!( "{}... and {} more matching items{}", color::Fg(color::Yellow), filtered_lines.len() - display_num, color::Fg(color::Reset) ); } Ok(()) } /// Moves specified items to the top of the note file to increase their priority. /// Takes 1-based item numbers and reorders them to appear at the beginning. pub fn prioritize_items(note_path: &Path, numbers: &[usize]) -> io::Result<()> { if !note_path.exists() { println!( "{}No notes found.{}", color::Fg(color::Yellow), color::Fg(color::Reset) ); return Ok(()); } let content = fs::read_to_string(note_path)?; let lines: Vec = content.lines().map(str::to_string).collect(); if lines.is_empty() { println!( "{}No notes to prioritize.{}", color::Fg(color::Yellow), color::Fg(color::Reset) ); return Ok(()); } match prioritize_items_in_content(&lines, numbers) { Ok((new_content, prioritized_items)) => { fs::write(note_path, new_content)?; println!( "{}✓ Prioritized {} item(s){}", color::Fg(color::Green), prioritized_items.len(), color::Fg(color::Reset) ); for item in prioritized_items { println!( " {}{}{}", color::Fg(color::Magenta), item, color::Fg(color::Reset) ); } Ok(()) } Err(msg) => { println!( "{}{}{}", color::Fg(color::Red), msg, color::Fg(color::Reset) ); Ok(()) } } } /// Removes specified items from the note file and appends them to the archive file. /// Takes 1-based item numbers, validates them, and handles file I/O operations. pub fn archive_items(note_path: &Path, archive_path: &Path, numbers: &[usize]) -> io::Result<()> { if !note_path.exists() { println!( "{}No notes found.{}", color::Fg(color::Yellow), color::Fg(color::Reset) ); return Ok(()); } let content = fs::read_to_string(note_path)?; let lines: Vec = content.lines().map(str::to_string).collect(); if lines.is_empty() { println!( "{}No notes to archive.{}", color::Fg(color::Yellow), color::Fg(color::Reset) ); return Ok(()); } match archive_items_in_content(&lines, numbers) { Ok((new_content, archived_items)) => { // Archive items if !archived_items.is_empty() { let mut archive_file = OpenOptions::new() .append(true) .create(true) .open(archive_path)?; for item in &archived_items { writeln!(archive_file, "{item}")?; } } fs::write(note_path, new_content)?; println!( "{}✓ Archived {} item(s){}", color::Fg(color::Green), archived_items.len(), color::Fg(color::Reset) ); for item in archived_items { println!( " {}{}{}", color::Fg(color::Red), item, color::Fg(color::Reset) ); } Ok(()) } Err(msg) => { println!( "{}{}{}", color::Fg(color::Red), msg, color::Fg(color::Reset) ); Ok(()) } } } /// Removes specified items from the note file without archiving them. /// Takes 1-based item numbers, validates them, and permanently deletes them. pub fn remove_items(note_path: &Path, numbers: &[usize]) -> io::Result<()> { if !note_path.exists() { println!( "{}No notes found.{}", color::Fg(color::Yellow), color::Fg(color::Reset) ); return Ok(()); } let content = fs::read_to_string(note_path)?; let lines: Vec = content.lines().map(str::to_string).collect(); if lines.is_empty() { println!( "{}No notes to remove.{}", color::Fg(color::Yellow), color::Fg(color::Reset) ); return Ok(()); } match archive_items_in_content(&lines, numbers) { Ok((new_content, removed_items)) => { fs::write(note_path, new_content)?; println!( "{}✓ Removed {} item(s){}", color::Fg(color::Green), removed_items.len(), color::Fg(color::Reset) ); for item in removed_items { println!( " {}{}{}", color::Fg(color::Red), item, color::Fg(color::Reset) ); } Ok(()) } Err(msg) => { println!( "{}{}{}", color::Fg(color::Red), msg, color::Fg(color::Reset) ); Ok(()) } } } /// Supports prioritizing, archiving, skipping items with keyboard navigation and undo functionality. pub fn review_note(note_path: &Path, archive_path: &Path) -> io::Result<()> { if !note_path.exists() { println!("No notes to review."); return Ok(()); } let content = fs::read_to_string(note_path)?; let lines: Vec = content.lines().map(str::to_string).collect(); let total = lines.len(); if total == 0 { println!("No notes to review."); return Ok(()); } let mut prioritized: Vec = Vec::new(); let mut to_archive: Vec = Vec::new(); let mut remove_indices: HashSet = HashSet::new(); let mut history: Vec<(usize, String, String)> = Vec::new(); // (index, action, item) let mut current_idx = 0; let indices: Vec = (0..total).collect(); while current_idx < indices.len() { let i = indices[current_idx]; let current = current_idx + 1; // Clear screen using ANSI escape sequence print!("\x1B[2J\x1B[H"); println!( "{}=== PARTS Review Mode ==={}", color::Fg(color::Cyan), color::Fg(color::Reset) ); println!( "{}Progress: {}/{} items remaining{}", color::Fg(color::Yellow), current, total, color::Fg(color::Reset) ); println!(); println!( "{}Item:{} {}", color::Fg(color::Green), color::Fg(color::Reset), lines[i] ); println!(); println!( "{}Actions:{}", color::Fg(color::Blue), color::Fg(color::Reset) ); println!( " {}[↑/p]{} Prioritize (move to top)", color::Fg(color::Magenta), color::Fg(color::Reset) ); println!( " {}[↓/a]{} Archive (remove from list)", color::Fg(color::Red), color::Fg(color::Reset) ); println!( " {}[→/Enter/s]{} Skip (keep in current position)", color::Fg(color::White), color::Fg(color::Reset) ); println!( " {}[←]{} Go back to previous item", color::Fg(color::Cyan), color::Fg(color::Reset) ); println!( " {}[q]{} Quit review", color::Fg(color::Yellow), color::Fg(color::Reset) ); println!(); print!( "{}Choose action:{} ", color::Fg(color::Blue), color::Fg(color::Reset) ); io::stdout().flush()?; // Read input (handle arrow keys) let action = read_key_input()?; match action.as_str() { "up" | "p" => { history.push((i, "prioritize".to_string(), lines[i].clone())); prioritized.push(lines[i].clone()); remove_indices.insert(i); println!( "↑\n{}✓ Prioritized{}", color::Fg(color::Magenta), color::Fg(color::Reset) ); current_idx += 1; } "down" | "a" => { history.push((i, "archive".to_string(), lines[i].clone())); to_archive.push(lines[i].clone()); remove_indices.insert(i); println!( "↓\n{}✓ Archived{}", color::Fg(color::Red), color::Fg(color::Reset) ); current_idx += 1; } "right" | "enter" | "s" => { history.push((i, "skip".to_string(), lines[i].clone())); println!( "→\n{}✓ Skipped{}", color::Fg(color::White), color::Fg(color::Reset) ); current_idx += 1; } "left" => { if let Some((prev_idx, prev_action, prev_item)) = history.pop() { // Undo the previous action match prev_action.as_str() { "prioritize" => { prioritized.retain(|item| item != &prev_item); remove_indices.remove(&prev_idx); } "archive" => { to_archive.retain(|item| item != &prev_item); remove_indices.remove(&prev_idx); } "skip" => { // Nothing to undo for skip } _ => {} } current_idx = current_idx.saturating_sub(1); println!( "←\n{}✓ Went back{}", color::Fg(color::Cyan), color::Fg(color::Reset) ); } else { println!( "←\n{}No previous item to go back to{}", color::Fg(color::Yellow), color::Fg(color::Reset) ); } } "q" => { println!( "q\n{}Quitting review and saving changes...{}", color::Fg(color::Yellow), color::Fg(color::Reset) ); break; } _ => { history.push((i, "skip".to_string(), lines[i].clone())); println!( "?\n{}✓ Skipped (invalid key){}", color::Fg(color::White), color::Fg(color::Reset) ); current_idx += 1; } } } // Clear screen for final summary print!("\x1B[2J\x1B[H"); println!( "{}=== Review Complete ==={}", color::Fg(color::Green), color::Fg(color::Reset) ); println!( "{}Prioritized:{} {} items", color::Fg(color::Magenta), color::Fg(color::Reset), prioritized.len() ); println!( "{}Archived:{} {} items", color::Fg(color::Red), color::Fg(color::Reset), to_archive.len() ); println!( "{}Remaining:{} {} items", color::Fg(color::Blue), color::Fg(color::Reset), total - remove_indices.len() ); println!(); // Archive items if !to_archive.is_empty() { let mut archive_file = OpenOptions::new() .append(true) .create(true) .open(archive_path)?; for arch in to_archive { writeln!(archive_file, "{arch}")?; } } // Build remaining items let mut remaining: Vec = Vec::new(); for (i, line) in lines.iter().enumerate() { if !remove_indices.contains(&i) { remaining.push(line.clone()); } } // Combine: prioritized items first, then remaining let mut new_lines = prioritized; new_lines.extend(remaining); // Write back to file let new_content = if new_lines.is_empty() { String::new() } else { new_lines.join("\n") + "\n" }; fs::write(note_path, new_content)?; println!( "{}✓ Changes saved successfully!{}", color::Fg(color::Green), color::Fg(color::Reset) ); Ok(()) } /// Opens the note file in the user's preferred editor (from $EDITOR environment variable). /// Creates the file if it doesn't exist, defaults to nano if $EDITOR is not set. pub fn edit_note(note_path: &Path) -> io::Result<()> { let editor = env::var("EDITOR").unwrap_or_else(|_| "nano".to_string()); // Create the file if it doesn't exist if !note_path.exists() { fs::write(note_path, "")?; } let status = Command::new(&editor).arg(note_path).status()?; if !status.success() { println!( "{}Editor exited with error code: {}{}", color::Fg(color::Red), status.code().unwrap_or(-1), color::Fg(color::Reset) ); } Ok(()) }