remove multilist support
This commit is contained in:
6 files changed
+171
-327
No files matched your search
+91
-105
@@ -6,7 +6,7 @@
|
|||||||
|
|
||||||
use crate::markdown::{
|
use crate::markdown::{
|
||||||
parse_todo_file, generate_todo_file, parse_archive_file, generate_archive_file,
|
parse_todo_file, generate_todo_file, parse_archive_file, generate_archive_file,
|
||||||
Task, TaskList, TodoFile, ArchiveFile, TaskRef
|
Task, TodoFile, ArchiveFile, TaskRef
|
||||||
};
|
};
|
||||||
use std::collections::HashSet;
|
use std::collections::HashSet;
|
||||||
use std::env;
|
use std::env;
|
||||||
@@ -163,37 +163,26 @@ impl ColorScheme {
|
|||||||
}
|
}
|
||||||
|
|
||||||
/// Adds a new entry to the todo file.
|
/// Adds a new entry to the todo file.
|
||||||
/// Creates the file if it doesn't exist, otherwise prepends to Default list or specified list.
|
/// Creates the file if it doesn't exist, otherwise prepends to the main list.
|
||||||
/// Can add as subtask if `under` is specified.
|
/// Can add as subtask if `under` is specified.
|
||||||
pub fn add_entry(path: &Path, text: &str, under: Option<usize>, list_name: Option<&str>) -> io::Result<()> {
|
pub fn add_entry(path: &Path, text: &str, under: Option<usize>) -> io::Result<()> {
|
||||||
let mut todo_file = if path.exists() {
|
let mut todo_file = if path.exists() {
|
||||||
let content = fs::read_to_string(path)?;
|
let content = fs::read_to_string(path)?;
|
||||||
parse_todo_file(&content).map_err(|e| io::Error::new(io::ErrorKind::InvalidData, e))?
|
parse_todo_file(&content).map_err(|e| io::Error::new(io::ErrorKind::InvalidData, e))?
|
||||||
} else {
|
} else {
|
||||||
let mut file = TodoFile::new("TODOs".to_string());
|
TodoFile::new("TODOs".to_string())
|
||||||
file.add_list(TaskList::new("Default".to_string()));
|
|
||||||
file
|
|
||||||
};
|
};
|
||||||
|
|
||||||
let target_list_name = list_name.unwrap_or("Default");
|
|
||||||
|
|
||||||
// Ensure the target list exists
|
|
||||||
if todo_file.get_list(target_list_name).is_none() {
|
|
||||||
todo_file.add_list(TaskList::new(target_list_name.to_string()));
|
|
||||||
}
|
|
||||||
|
|
||||||
let target_list = todo_file.get_list_mut(target_list_name).unwrap();
|
|
||||||
|
|
||||||
if let Some(task_num) = under {
|
if let Some(task_num) = under {
|
||||||
// Add as subtask
|
// Add as subtask
|
||||||
if task_num == 0 || task_num > target_list.tasks.len() {
|
if task_num == 0 || task_num > todo_file.tasks.len() {
|
||||||
return Err(io::Error::new(
|
return Err(io::Error::new(
|
||||||
io::ErrorKind::InvalidInput,
|
io::ErrorKind::InvalidInput,
|
||||||
format!("Invalid task number: {}. Valid range: 1-{}", task_num, target_list.tasks.len())
|
format!("Invalid task number: {}. Valid range: 1-{}", task_num, todo_file.tasks.len())
|
||||||
));
|
));
|
||||||
}
|
}
|
||||||
|
|
||||||
target_list.tasks[task_num - 1].add_subtask(text.to_string());
|
todo_file.tasks[task_num - 1].add_subtask(text.to_string());
|
||||||
println!(
|
println!(
|
||||||
"{}✓ Added subtask to task {}: {}{}",
|
"{}✓ Added subtask to task {}: {}{}",
|
||||||
color::Fg(color::Green),
|
color::Fg(color::Green),
|
||||||
@@ -204,11 +193,10 @@ pub fn add_entry(path: &Path, text: &str, under: Option<usize>, list_name: Optio
|
|||||||
} else {
|
} else {
|
||||||
// Add as new main task at top
|
// Add as new main task at top
|
||||||
let task = Task::new(text.to_string());
|
let task = Task::new(text.to_string());
|
||||||
target_list.prepend_task(task);
|
todo_file.prepend_task(task);
|
||||||
println!(
|
println!(
|
||||||
"{}✓ Added to {}: {}{}",
|
"{}✓ Added: {}{}",
|
||||||
color::Fg(color::Green),
|
color::Fg(color::Green),
|
||||||
target_list_name,
|
|
||||||
text,
|
text,
|
||||||
color::Fg(color::Reset)
|
color::Fg(color::Reset)
|
||||||
);
|
);
|
||||||
@@ -218,7 +206,7 @@ pub fn add_entry(path: &Path, text: &str, under: Option<usize>, list_name: Optio
|
|||||||
fs::write(path, content)
|
fs::write(path, content)
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Lists tasks from the Default list with numbered display including subtasks.
|
/// Lists tasks with numbered display including subtasks.
|
||||||
/// Displays task numbers and subtask letters, supports filtering.
|
/// Displays task numbers and subtask letters, supports filtering.
|
||||||
pub fn list_note(path: &Path, num: usize, all: bool, filter: Option<&str>) -> io::Result<()> {
|
pub fn list_note(path: &Path, num: usize, all: bool, filter: Option<&str>) -> io::Result<()> {
|
||||||
if !path.exists() {
|
if !path.exists() {
|
||||||
@@ -234,19 +222,7 @@ pub fn list_note(path: &Path, num: usize, all: bool, filter: Option<&str>) -> io
|
|||||||
let todo_file = parse_todo_file(&content)
|
let todo_file = parse_todo_file(&content)
|
||||||
.map_err(|e| io::Error::new(io::ErrorKind::InvalidData, e))?;
|
.map_err(|e| io::Error::new(io::ErrorKind::InvalidData, e))?;
|
||||||
|
|
||||||
let default_list = match todo_file.get_default_list() {
|
if todo_file.is_empty() {
|
||||||
Some(list) => list,
|
|
||||||
None => {
|
|
||||||
println!(
|
|
||||||
"{}No Default list found.{}",
|
|
||||||
color::Fg(color::Yellow),
|
|
||||||
color::Fg(color::Reset)
|
|
||||||
);
|
|
||||||
return Ok(());
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
if default_list.is_empty() {
|
|
||||||
println!(
|
println!(
|
||||||
"{}No notes yet.{}",
|
"{}No notes yet.{}",
|
||||||
color::Fg(color::Yellow),
|
color::Fg(color::Yellow),
|
||||||
@@ -255,31 +231,66 @@ pub fn list_note(path: &Path, num: usize, all: bool, filter: Option<&str>) -> io
|
|||||||
return Ok(());
|
return Ok(());
|
||||||
}
|
}
|
||||||
|
|
||||||
// Build flat list of items for filtering and display
|
// Build list of items for filtering and display
|
||||||
let mut display_items = Vec::new();
|
let display_items: Vec<_> = if let Some(filter_text) = filter {
|
||||||
for (task_idx, task) in default_list.tasks.iter().enumerate() {
|
let mut filtered = Vec::new();
|
||||||
let task_line = format!("{:3}. {}", task_idx + 1, task.text);
|
let filter_lower = filter_text.to_lowercase();
|
||||||
display_items.push((task_idx + 1, None, task_line.clone()));
|
|
||||||
|
for (task_idx, task) in todo_file.tasks.iter().enumerate() {
|
||||||
// Add subtasks if any
|
let task_num = task_idx + 1;
|
||||||
for (subtask_idx, subtask) in task.subtasks.iter().enumerate() {
|
let task_matches = task.text.to_lowercase().contains(&filter_lower);
|
||||||
let letter = (b'a' + subtask_idx as u8) as char;
|
|
||||||
let subtask_line = format!(" {}. {}", letter, subtask);
|
// Check which subtasks match
|
||||||
display_items.push((task_idx + 1, Some(subtask_idx), subtask_line));
|
let mut matching_subtasks = Vec::new();
|
||||||
|
for (subtask_idx, subtask) in task.subtasks.iter().enumerate() {
|
||||||
|
if subtask.to_lowercase().contains(&filter_lower) {
|
||||||
|
matching_subtasks.push(subtask_idx);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if task_matches {
|
||||||
|
// If task matches, include task and ALL its subtasks
|
||||||
|
let task_line = format!("{:3}. {}", task_num, task.text);
|
||||||
|
filtered.push((task_num, None, task_line));
|
||||||
|
|
||||||
|
for (subtask_idx, subtask) in task.subtasks.iter().enumerate() {
|
||||||
|
let letter = (b'a' + subtask_idx as u8) as char;
|
||||||
|
let subtask_line = format!(" {}. {}", letter, subtask);
|
||||||
|
filtered.push((task_num, Some(subtask_idx), subtask_line));
|
||||||
|
}
|
||||||
|
} else if !matching_subtasks.is_empty() {
|
||||||
|
// If only subtasks match, include task and only matching subtasks
|
||||||
|
let task_line = format!("{:3}. {}", task_num, task.text);
|
||||||
|
filtered.push((task_num, None, task_line));
|
||||||
|
|
||||||
|
for &subtask_idx in &matching_subtasks {
|
||||||
|
let letter = (b'a' + subtask_idx as u8) as char;
|
||||||
|
let subtask_line = format!(" {}. {}", letter, &task.subtasks[subtask_idx]);
|
||||||
|
filtered.push((task_num, Some(subtask_idx), subtask_line));
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
|
||||||
|
filtered
|
||||||
// Apply filter
|
|
||||||
let filtered_items: Vec<_> = if let Some(filter_text) = filter {
|
|
||||||
display_items
|
|
||||||
.into_iter()
|
|
||||||
.filter(|(_, _, line)| line.to_lowercase().contains(&filter_text.to_lowercase()))
|
|
||||||
.collect()
|
|
||||||
} else {
|
} else {
|
||||||
display_items
|
// No filter - include everything
|
||||||
|
let mut all_items = Vec::new();
|
||||||
|
for (task_idx, task) in todo_file.tasks.iter().enumerate() {
|
||||||
|
let task_num = task_idx + 1;
|
||||||
|
let task_line = format!("{:3}. {}", task_num, task.text);
|
||||||
|
all_items.push((task_num, None, task_line));
|
||||||
|
|
||||||
|
// Add subtasks if any
|
||||||
|
for (subtask_idx, subtask) in task.subtasks.iter().enumerate() {
|
||||||
|
let letter = (b'a' + subtask_idx as u8) as char;
|
||||||
|
let subtask_line = format!(" {}. {}", letter, subtask);
|
||||||
|
all_items.push((task_num, Some(subtask_idx), subtask_line));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
all_items
|
||||||
};
|
};
|
||||||
|
|
||||||
if filtered_items.is_empty() {
|
if display_items.is_empty() {
|
||||||
if filter.is_some() {
|
if filter.is_some() {
|
||||||
println!(
|
println!(
|
||||||
"{}No items found matching filter: \"{}\"{}",
|
"{}No items found matching filter: \"{}\"{}",
|
||||||
@@ -298,14 +309,14 @@ pub fn list_note(path: &Path, num: usize, all: bool, filter: Option<&str>) -> io
|
|||||||
}
|
}
|
||||||
|
|
||||||
let display_count = if all {
|
let display_count = if all {
|
||||||
filtered_items.len()
|
display_items.len()
|
||||||
} else {
|
} else {
|
||||||
num.min(filtered_items.len())
|
num.min(display_items.len())
|
||||||
};
|
};
|
||||||
|
|
||||||
let color_scheme = ColorScheme::new();
|
let color_scheme = ColorScheme::new();
|
||||||
|
|
||||||
for (task_num, subtask_idx, line) in filtered_items.iter().take(display_count) {
|
for (task_num, subtask_idx, line) in display_items.iter().take(display_count) {
|
||||||
if subtask_idx.is_none() {
|
if subtask_idx.is_none() {
|
||||||
// Main task - use HSV-based bright colors
|
// Main task - use HSV-based bright colors
|
||||||
let color = color_scheme.get_main_task_color(*task_num);
|
let color = color_scheme.get_main_task_color(*task_num);
|
||||||
@@ -327,11 +338,11 @@ pub fn list_note(path: &Path, num: usize, all: bool, filter: Option<&str>) -> io
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if !all && filtered_items.len() > display_count {
|
if !all && display_items.len() > display_count {
|
||||||
println!(
|
println!(
|
||||||
"{}... and {} more items{}",
|
"{}... and {} more items{}",
|
||||||
color::Fg(color::Yellow),
|
color::Fg(color::Yellow),
|
||||||
filtered_items.len() - display_count,
|
display_items.len() - display_count,
|
||||||
color::Fg(color::Reset)
|
color::Fg(color::Reset)
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
@@ -354,21 +365,9 @@ pub fn prioritize_items(todo_path: &Path, refs: &[String]) -> io::Result<()> {
|
|||||||
let mut todo_file = parse_todo_file(&content)
|
let mut todo_file = parse_todo_file(&content)
|
||||||
.map_err(|e| io::Error::new(io::ErrorKind::InvalidData, e))?;
|
.map_err(|e| io::Error::new(io::ErrorKind::InvalidData, e))?;
|
||||||
|
|
||||||
let default_list = match todo_file.get_default_list_mut() {
|
if todo_file.is_empty() {
|
||||||
Some(list) => list,
|
|
||||||
None => {
|
|
||||||
println!(
|
|
||||||
"{}No Default list found.{}",
|
|
||||||
color::Fg(color::Yellow),
|
|
||||||
color::Fg(color::Reset)
|
|
||||||
);
|
|
||||||
return Ok(());
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
if default_list.is_empty() {
|
|
||||||
println!(
|
println!(
|
||||||
"{}No notes to prioritize.{}",
|
"{}No notes found.{}",
|
||||||
color::Fg(color::Yellow),
|
color::Fg(color::Yellow),
|
||||||
color::Fg(color::Reset)
|
color::Fg(color::Reset)
|
||||||
);
|
);
|
||||||
@@ -380,19 +379,19 @@ pub fn prioritize_items(todo_path: &Path, refs: &[String]) -> io::Result<()> {
|
|||||||
for ref_str in refs {
|
for ref_str in refs {
|
||||||
match TaskRef::parse(ref_str) {
|
match TaskRef::parse(ref_str) {
|
||||||
Ok(task_ref) => {
|
Ok(task_ref) => {
|
||||||
if task_ref.task_index >= default_list.tasks.len() {
|
if task_ref.task_index >= todo_file.tasks.len() {
|
||||||
println!(
|
println!(
|
||||||
"{}Invalid task number: {}. Valid range: 1-{}{}",
|
"{}Invalid task number: {}. Valid range: 1-{}{}",
|
||||||
color::Fg(color::Red),
|
color::Fg(color::Red),
|
||||||
task_ref.task_index + 1,
|
task_ref.task_index + 1,
|
||||||
default_list.tasks.len(),
|
todo_file.tasks.len(),
|
||||||
color::Fg(color::Reset)
|
color::Fg(color::Reset)
|
||||||
);
|
);
|
||||||
return Ok(());
|
return Ok(());
|
||||||
}
|
}
|
||||||
|
|
||||||
if let Some(subtask_idx) = task_ref.subtask_index {
|
if let Some(subtask_idx) = task_ref.subtask_index {
|
||||||
let task = &default_list.tasks[task_ref.task_index];
|
let task = &todo_file.tasks[task_ref.task_index];
|
||||||
if subtask_idx >= task.subtasks.len() {
|
if subtask_idx >= task.subtasks.len() {
|
||||||
println!(
|
println!(
|
||||||
"{}Invalid subtask: {}{}. Task {} has {} subtasks{}",
|
"{}Invalid subtask: {}{}. Task {} has {} subtasks{}",
|
||||||
@@ -438,20 +437,20 @@ pub fn prioritize_items(todo_path: &Path, refs: &[String]) -> io::Result<()> {
|
|||||||
let mut moved_tasks = Vec::new();
|
let mut moved_tasks = Vec::new();
|
||||||
|
|
||||||
for &task_idx in &tasks_to_move {
|
for &task_idx in &tasks_to_move {
|
||||||
moved_tasks.push(default_list.tasks[task_idx].clone());
|
moved_tasks.push(todo_file.tasks[task_idx].clone());
|
||||||
}
|
}
|
||||||
|
|
||||||
// Add moved tasks first
|
// Add moved tasks first
|
||||||
new_tasks.extend(moved_tasks.clone());
|
new_tasks.extend(moved_tasks.clone());
|
||||||
|
|
||||||
// Add remaining tasks
|
// Add remaining tasks
|
||||||
for (idx, task) in default_list.tasks.iter().enumerate() {
|
for (idx, task) in todo_file.tasks.iter().enumerate() {
|
||||||
if !moved_task_indices.contains(&idx) {
|
if !moved_task_indices.contains(&idx) {
|
||||||
new_tasks.push(task.clone());
|
new_tasks.push(task.clone());
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
default_list.tasks = new_tasks;
|
todo_file.tasks = new_tasks;
|
||||||
|
|
||||||
let new_content = generate_todo_file(&todo_file);
|
let new_content = generate_todo_file(&todo_file);
|
||||||
fs::write(todo_path, new_content)?;
|
fs::write(todo_path, new_content)?;
|
||||||
@@ -490,19 +489,7 @@ pub fn archive_items(todo_path: &Path, archive_path: &Path, refs: &[String]) ->
|
|||||||
let mut todo_file = parse_todo_file(&content)
|
let mut todo_file = parse_todo_file(&content)
|
||||||
.map_err(|e| io::Error::new(io::ErrorKind::InvalidData, e))?;
|
.map_err(|e| io::Error::new(io::ErrorKind::InvalidData, e))?;
|
||||||
|
|
||||||
let default_list = match todo_file.get_default_list_mut() {
|
if todo_file.is_empty() {
|
||||||
Some(list) => list,
|
|
||||||
None => {
|
|
||||||
println!(
|
|
||||||
"{}No Default list found.{}",
|
|
||||||
color::Fg(color::Yellow),
|
|
||||||
color::Fg(color::Reset)
|
|
||||||
);
|
|
||||||
return Ok(());
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
if default_list.is_empty() {
|
|
||||||
println!(
|
println!(
|
||||||
"{}No notes to archive.{}",
|
"{}No notes to archive.{}",
|
||||||
color::Fg(color::Yellow),
|
color::Fg(color::Yellow),
|
||||||
@@ -535,13 +522,13 @@ pub fn archive_items(todo_path: &Path, archive_path: &Path, refs: &[String]) ->
|
|||||||
let mut whole_tasks_to_remove = HashSet::new();
|
let mut whole_tasks_to_remove = HashSet::new();
|
||||||
|
|
||||||
for (ref_str, task_ref) in &task_refs {
|
for (ref_str, task_ref) in &task_refs {
|
||||||
if task_ref.task_index >= default_list.tasks.len() {
|
if task_ref.task_index >= todo_file.tasks.len() {
|
||||||
println!(
|
println!(
|
||||||
"{}Invalid task number in '{}': {}. Valid range: 1-{}{}",
|
"{}Invalid task number in '{}': {}. Valid range: 1-{}{}",
|
||||||
color::Fg(color::Red),
|
color::Fg(color::Red),
|
||||||
ref_str,
|
ref_str,
|
||||||
task_ref.task_index + 1,
|
task_ref.task_index + 1,
|
||||||
default_list.tasks.len(),
|
todo_file.tasks.len(),
|
||||||
color::Fg(color::Reset)
|
color::Fg(color::Reset)
|
||||||
);
|
);
|
||||||
return Ok(());
|
return Ok(());
|
||||||
@@ -549,7 +536,7 @@ pub fn archive_items(todo_path: &Path, archive_path: &Path, refs: &[String]) ->
|
|||||||
|
|
||||||
if let Some(subtask_idx) = task_ref.subtask_index {
|
if let Some(subtask_idx) = task_ref.subtask_index {
|
||||||
// Archiving a subtask
|
// Archiving a subtask
|
||||||
let task = &default_list.tasks[task_ref.task_index];
|
let task = &todo_file.tasks[task_ref.task_index];
|
||||||
if subtask_idx >= task.subtasks.len() {
|
if subtask_idx >= task.subtasks.len() {
|
||||||
println!(
|
println!(
|
||||||
"{}Invalid subtask '{}': Task {} has {} subtasks{}",
|
"{}Invalid subtask '{}': Task {} has {} subtasks{}",
|
||||||
@@ -570,12 +557,12 @@ pub fn archive_items(todo_path: &Path, archive_path: &Path, refs: &[String]) ->
|
|||||||
|
|
||||||
// Collect items to archive
|
// Collect items to archive
|
||||||
for &task_idx in &whole_tasks_to_remove {
|
for &task_idx in &whole_tasks_to_remove {
|
||||||
tasks_to_archive.push(default_list.tasks[task_idx].clone());
|
tasks_to_archive.push(todo_file.tasks[task_idx].clone());
|
||||||
}
|
}
|
||||||
|
|
||||||
for &(task_idx, subtask_idx) in &subtasks_to_remove {
|
for &(task_idx, subtask_idx) in &subtasks_to_remove {
|
||||||
if !whole_tasks_to_remove.contains(&task_idx) {
|
if !whole_tasks_to_remove.contains(&task_idx) {
|
||||||
let subtask_text = default_list.tasks[task_idx].subtasks[subtask_idx].clone();
|
let subtask_text = todo_file.tasks[task_idx].subtasks[subtask_idx].clone();
|
||||||
tasks_to_archive.push(Task::new(subtask_text));
|
tasks_to_archive.push(Task::new(subtask_text));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -611,11 +598,11 @@ pub fn archive_items(todo_path: &Path, archive_path: &Path, refs: &[String]) ->
|
|||||||
for (task_idx, mut subtask_indices) in subtasks_by_task {
|
for (task_idx, mut subtask_indices) in subtasks_by_task {
|
||||||
subtask_indices.sort_by(|a, b| b.cmp(a)); // Sort in reverse order
|
subtask_indices.sort_by(|a, b| b.cmp(a)); // Sort in reverse order
|
||||||
for subtask_idx in subtask_indices {
|
for subtask_idx in subtask_indices {
|
||||||
default_list.tasks[task_idx].subtasks.remove(subtask_idx);
|
todo_file.tasks[task_idx].subtasks.remove(subtask_idx);
|
||||||
}
|
}
|
||||||
|
|
||||||
// Check if this task now has no subtasks left and should be auto-completed
|
// Check if this task now has no subtasks left and should be auto-completed
|
||||||
if default_list.tasks[task_idx].subtasks.is_empty() {
|
if todo_file.tasks[task_idx].subtasks.is_empty() {
|
||||||
tasks_to_auto_complete.push(task_idx);
|
tasks_to_auto_complete.push(task_idx);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -624,7 +611,7 @@ pub fn archive_items(todo_path: &Path, archive_path: &Path, refs: &[String]) ->
|
|||||||
let mut auto_completed_tasks = Vec::new();
|
let mut auto_completed_tasks = Vec::new();
|
||||||
if !tasks_to_auto_complete.is_empty() {
|
if !tasks_to_auto_complete.is_empty() {
|
||||||
for &task_idx in &tasks_to_auto_complete {
|
for &task_idx in &tasks_to_auto_complete {
|
||||||
auto_completed_tasks.push(default_list.tasks[task_idx].clone());
|
auto_completed_tasks.push(todo_file.tasks[task_idx].clone());
|
||||||
}
|
}
|
||||||
|
|
||||||
// Add auto-completed tasks to archive
|
// Add auto-completed tasks to archive
|
||||||
@@ -642,7 +629,7 @@ pub fn archive_items(todo_path: &Path, archive_path: &Path, refs: &[String]) ->
|
|||||||
whole_task_indices.dedup(); // Remove duplicates in case a task was both manually selected and auto-completed
|
whole_task_indices.dedup(); // Remove duplicates in case a task was both manually selected and auto-completed
|
||||||
|
|
||||||
for task_idx in whole_task_indices {
|
for task_idx in whole_task_indices {
|
||||||
default_list.tasks.remove(task_idx);
|
todo_file.tasks.remove(task_idx);
|
||||||
}
|
}
|
||||||
|
|
||||||
// Save updated todo file
|
// Save updated todo file
|
||||||
@@ -715,8 +702,7 @@ pub fn edit_note(todo_path: &Path) -> io::Result<()> {
|
|||||||
|
|
||||||
// Create the file if it doesn't exist
|
// Create the file if it doesn't exist
|
||||||
if !todo_path.exists() {
|
if !todo_path.exists() {
|
||||||
let mut empty_file = TodoFile::new("TODOs".to_string());
|
let empty_file = TodoFile::new("TODOs".to_string());
|
||||||
empty_file.add_list(TaskList::new("Default".to_string()));
|
|
||||||
let content = generate_todo_file(&empty_file);
|
let content = generate_todo_file(&empty_file);
|
||||||
fs::write(todo_path, content)?;
|
fs::write(todo_path, content)?;
|
||||||
}
|
}
|
||||||
|
|||||||
+1
-4
@@ -37,9 +37,6 @@ enum Commands {
|
|||||||
/// Add as subtask under this task number (e.g., --under 3)
|
/// Add as subtask under this task number (e.g., --under 3)
|
||||||
#[arg(long)]
|
#[arg(long)]
|
||||||
under: Option<usize>,
|
under: Option<usize>,
|
||||||
/// Add to specific list instead of Default
|
|
||||||
#[arg(long)]
|
|
||||||
list: Option<String>,
|
|
||||||
},
|
},
|
||||||
/// List the top N items (default 5)
|
/// List the top N items (default 5)
|
||||||
#[command(aliases = ["l", "list"])]
|
#[command(aliases = ["l", "list"])]
|
||||||
@@ -106,7 +103,7 @@ fn main() -> io::Result<()> {
|
|||||||
}
|
}
|
||||||
|
|
||||||
match cli.command {
|
match cli.command {
|
||||||
Commands::Add { text, under, list } => commands::add_entry(&todo_md_path, &text, under, list.as_deref()),
|
Commands::Add { text, under } => commands::add_entry(&todo_md_path, &text, under),
|
||||||
Commands::Ls { num, all, filter } => {
|
Commands::Ls { num, all, filter } => {
|
||||||
commands::list_note(&todo_md_path, num, all, filter.as_deref())
|
commands::list_note(&todo_md_path, num, all, filter.as_deref())
|
||||||
}
|
}
|
||||||
|
|||||||
+54
-185
@@ -40,17 +40,17 @@ impl Task {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Represents a named list of tasks
|
/// Represents the entire todo file with a single task list
|
||||||
#[derive(Debug, Clone, PartialEq)]
|
#[derive(Debug, Clone, PartialEq)]
|
||||||
pub struct TaskList {
|
pub struct TodoFile {
|
||||||
pub name: String,
|
pub title: String,
|
||||||
pub tasks: Vec<Task>,
|
pub tasks: Vec<Task>,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl TaskList {
|
impl TodoFile {
|
||||||
pub fn new(name: String) -> Self {
|
pub fn new(title: String) -> Self {
|
||||||
TaskList {
|
TodoFile {
|
||||||
name,
|
title,
|
||||||
tasks: Vec::new(),
|
tasks: Vec::new(),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -70,56 +70,6 @@ impl TaskList {
|
|||||||
pub fn task_count(&self) -> usize {
|
pub fn task_count(&self) -> usize {
|
||||||
self.tasks.len()
|
self.tasks.len()
|
||||||
}
|
}
|
||||||
|
|
||||||
#[allow(dead_code)]
|
|
||||||
pub fn total_item_count(&self) -> usize {
|
|
||||||
self.tasks
|
|
||||||
.iter()
|
|
||||||
.map(|t| 1 + t.subtask_count())
|
|
||||||
.sum()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Represents the entire todo file with multiple lists
|
|
||||||
#[derive(Debug, Clone, PartialEq)]
|
|
||||||
pub struct TodoFile {
|
|
||||||
pub title: String,
|
|
||||||
pub lists: Vec<TaskList>,
|
|
||||||
}
|
|
||||||
|
|
||||||
impl TodoFile {
|
|
||||||
pub fn new(title: String) -> Self {
|
|
||||||
TodoFile {
|
|
||||||
title,
|
|
||||||
lists: Vec::new(),
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn add_list(&mut self, list: TaskList) {
|
|
||||||
self.lists.push(list);
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn get_default_list_mut(&mut self) -> Option<&mut TaskList> {
|
|
||||||
self.lists.iter_mut().find(|list| list.name == "Default")
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn get_default_list(&self) -> Option<&TaskList> {
|
|
||||||
self.lists.iter().find(|list| list.name == "Default")
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn get_list_mut(&mut self, name: &str) -> Option<&mut TaskList> {
|
|
||||||
self.lists.iter_mut().find(|list| list.name == name)
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn get_list(&self, name: &str) -> Option<&TaskList> {
|
|
||||||
self.lists.iter().find(|list| list.name == name)
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn ensure_default_list(&mut self) {
|
|
||||||
if self.get_default_list().is_none() {
|
|
||||||
self.lists.insert(0, TaskList::new("Default".to_string()));
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Parse task reference in format "1", "2a", "10b", etc.
|
/// Parse task reference in format "1", "2a", "10b", etc.
|
||||||
@@ -185,7 +135,6 @@ pub fn parse_todo_file(content: &str) -> Result<TodoFile, String> {
|
|||||||
}
|
}
|
||||||
|
|
||||||
let mut todo_file = TodoFile::new("TODOs".to_string());
|
let mut todo_file = TodoFile::new("TODOs".to_string());
|
||||||
let mut current_list: Option<TaskList> = None;
|
|
||||||
let mut current_task: Option<Task> = None;
|
let mut current_task: Option<Task> = None;
|
||||||
|
|
||||||
for line in lines.iter() {
|
for line in lines.iter() {
|
||||||
@@ -203,28 +152,9 @@ pub fn parse_todo_file(content: &str) -> Result<TodoFile, String> {
|
|||||||
let title = &trimmed[1..];
|
let title = &trimmed[1..];
|
||||||
todo_file.title = title.trim().to_string();
|
todo_file.title = title.trim().to_string();
|
||||||
}
|
}
|
||||||
// Handle list headers - be flexible with spacing
|
// Skip any list headers - we ignore them now since we don't support multiple lists
|
||||||
else if let Some(list_name) = trimmed.strip_prefix("## ") {
|
else if trimmed.starts_with("##") {
|
||||||
// Save previous task if exists
|
continue;
|
||||||
if let (Some(mut list), Some(task)) = (current_list.take(), current_task.take()) {
|
|
||||||
list.add_task(task);
|
|
||||||
todo_file.add_list(list);
|
|
||||||
} else if let Some(list) = current_list.take() {
|
|
||||||
todo_file.add_list(list);
|
|
||||||
}
|
|
||||||
|
|
||||||
current_list = Some(TaskList::new(list_name.trim().to_string()));
|
|
||||||
} else if trimmed.starts_with("##") && !trimmed.starts_with("###") {
|
|
||||||
// Handle cases where user might not have space after ## (but not ###)
|
|
||||||
let list_name = &trimmed[2..];
|
|
||||||
if let (Some(mut list), Some(task)) = (current_list.take(), current_task.take()) {
|
|
||||||
list.add_task(task);
|
|
||||||
todo_file.add_list(list);
|
|
||||||
} else if let Some(list) = current_list.take() {
|
|
||||||
todo_file.add_list(list);
|
|
||||||
}
|
|
||||||
|
|
||||||
current_list = Some(TaskList::new(list_name.trim().to_string()));
|
|
||||||
}
|
}
|
||||||
// Handle subtasks - be flexible with indentation (2, 3, or 4 spaces, or tab)
|
// Handle subtasks - be flexible with indentation (2, 3, or 4 spaces, or tab)
|
||||||
else if let Some(subtask_text) = line.strip_prefix(" - ") {
|
else if let Some(subtask_text) = line.strip_prefix(" - ") {
|
||||||
@@ -232,82 +162,49 @@ pub fn parse_todo_file(content: &str) -> Result<TodoFile, String> {
|
|||||||
task.add_subtask(subtask_text.trim().to_string());
|
task.add_subtask(subtask_text.trim().to_string());
|
||||||
} else {
|
} else {
|
||||||
// If no current task, treat as main task (user error but be forgiving)
|
// If no current task, treat as main task (user error but be forgiving)
|
||||||
if current_list.is_none() {
|
current_task = Some(Task::new(subtask_text.trim().to_string()));
|
||||||
current_list = Some(TaskList::new("Default".to_string()));
|
|
||||||
}
|
|
||||||
if let Some(ref mut list) = current_list {
|
|
||||||
list.add_task(Task::new(subtask_text.trim().to_string()));
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
} else if let Some(subtask_text) = line.strip_prefix(" - ") {
|
} else if let Some(subtask_text) = line.strip_prefix(" - ") {
|
||||||
// Handle 3-space indentation
|
// Handle 3-space indentation
|
||||||
if let Some(ref mut task) = current_task {
|
if let Some(ref mut task) = current_task {
|
||||||
task.add_subtask(subtask_text.trim().to_string());
|
task.add_subtask(subtask_text.trim().to_string());
|
||||||
} else {
|
} else {
|
||||||
if current_list.is_none() {
|
current_task = Some(Task::new(subtask_text.trim().to_string()));
|
||||||
current_list = Some(TaskList::new("Default".to_string()));
|
|
||||||
}
|
|
||||||
if let Some(ref mut list) = current_list {
|
|
||||||
list.add_task(Task::new(subtask_text.trim().to_string()));
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
} else if let Some(subtask_text) = line.strip_prefix(" - ") {
|
} else if let Some(subtask_text) = line.strip_prefix(" - ") {
|
||||||
// Handle 4-space indentation (but not deeper nesting)
|
// Handle 4-space indentation (but not deeper nesting)
|
||||||
if let Some(ref mut task) = current_task {
|
if let Some(ref mut task) = current_task {
|
||||||
task.add_subtask(subtask_text.trim().to_string());
|
task.add_subtask(subtask_text.trim().to_string());
|
||||||
} else {
|
} else {
|
||||||
if current_list.is_none() {
|
current_task = Some(Task::new(subtask_text.trim().to_string()));
|
||||||
current_list = Some(TaskList::new("Default".to_string()));
|
|
||||||
}
|
|
||||||
if let Some(ref mut list) = current_list {
|
|
||||||
list.add_task(Task::new(subtask_text.trim().to_string()));
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
} else if let Some(subtask_text) = line.strip_prefix("\t- ") {
|
} else if let Some(subtask_text) = line.strip_prefix("\t- ") {
|
||||||
// Handle tab indentation
|
// Handle tab indentation
|
||||||
if let Some(ref mut task) = current_task {
|
if let Some(ref mut task) = current_task {
|
||||||
task.add_subtask(subtask_text.trim().to_string());
|
task.add_subtask(subtask_text.trim().to_string());
|
||||||
} else {
|
} else {
|
||||||
if current_list.is_none() {
|
current_task = Some(Task::new(subtask_text.trim().to_string()));
|
||||||
current_list = Some(TaskList::new("Default".to_string()));
|
|
||||||
}
|
|
||||||
if let Some(ref mut list) = current_list {
|
|
||||||
list.add_task(Task::new(subtask_text.trim().to_string()));
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
// Handle main tasks - flexible with spacing and different bullet styles
|
// Handle main tasks - flexible with spacing and different bullet styles
|
||||||
else if let Some(task_text) = trimmed.strip_prefix("- ") {
|
else if let Some(task_text) = trimmed.strip_prefix("- ") {
|
||||||
// Save previous task if exists
|
// Save previous task if exists
|
||||||
if let (Some(list), Some(task)) = (current_list.as_mut(), current_task.take()) {
|
if let Some(task) = current_task.take() {
|
||||||
list.add_task(task);
|
todo_file.add_task(task);
|
||||||
}
|
|
||||||
|
|
||||||
// Ensure we have a list
|
|
||||||
if current_list.is_none() {
|
|
||||||
current_list = Some(TaskList::new("Default".to_string()));
|
|
||||||
}
|
}
|
||||||
|
|
||||||
current_task = Some(Task::new(task_text.trim().to_string()));
|
current_task = Some(Task::new(task_text.trim().to_string()));
|
||||||
} else if let Some(task_text) = trimmed.strip_prefix("* ") {
|
} else if let Some(task_text) = trimmed.strip_prefix("* ") {
|
||||||
// Handle asterisk bullet points
|
// Handle asterisk bullet points
|
||||||
if let (Some(list), Some(task)) = (current_list.as_mut(), current_task.take()) {
|
if let Some(task) = current_task.take() {
|
||||||
list.add_task(task);
|
todo_file.add_task(task);
|
||||||
}
|
|
||||||
|
|
||||||
if current_list.is_none() {
|
|
||||||
current_list = Some(TaskList::new("Default".to_string()));
|
|
||||||
}
|
}
|
||||||
|
|
||||||
current_task = Some(Task::new(task_text.trim().to_string()));
|
current_task = Some(Task::new(task_text.trim().to_string()));
|
||||||
} else if let Some(task_text) = trimmed.strip_prefix("+ ") {
|
} else if let Some(task_text) = trimmed.strip_prefix("+ ") {
|
||||||
// Handle plus bullet points
|
// Handle plus bullet points
|
||||||
if let (Some(list), Some(task)) = (current_list.as_mut(), current_task.take()) {
|
if let Some(task) = current_task.take() {
|
||||||
list.add_task(task);
|
todo_file.add_task(task);
|
||||||
}
|
|
||||||
|
|
||||||
if current_list.is_none() {
|
|
||||||
current_list = Some(TaskList::new("Default".to_string()));
|
|
||||||
}
|
}
|
||||||
|
|
||||||
current_task = Some(Task::new(task_text.trim().to_string()));
|
current_task = Some(Task::new(task_text.trim().to_string()));
|
||||||
@@ -322,13 +219,8 @@ pub fn parse_todo_file(content: &str) -> Result<TodoFile, String> {
|
|||||||
// If it looks like it might be a task without proper formatting, treat it as one
|
// If it looks like it might be a task without proper formatting, treat it as one
|
||||||
if !trimmed.starts_with('#') && !trimmed.starts_with('<') && !trimmed.contains("```") {
|
if !trimmed.starts_with('#') && !trimmed.starts_with('<') && !trimmed.contains("```") {
|
||||||
// Save previous task if exists
|
// Save previous task if exists
|
||||||
if let (Some(list), Some(task)) = (current_list.as_mut(), current_task.take()) {
|
if let Some(task) = current_task.take() {
|
||||||
list.add_task(task);
|
todo_file.add_task(task);
|
||||||
}
|
|
||||||
|
|
||||||
// Ensure we have a list
|
|
||||||
if current_list.is_none() {
|
|
||||||
current_list = Some(TaskList::new("Default".to_string()));
|
|
||||||
}
|
}
|
||||||
|
|
||||||
current_task = Some(Task::new(trimmed.to_string()));
|
current_task = Some(Task::new(trimmed.to_string()));
|
||||||
@@ -337,17 +229,11 @@ pub fn parse_todo_file(content: &str) -> Result<TodoFile, String> {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Save final task and list
|
// Save final task
|
||||||
if let Some(mut list) = current_list {
|
if let Some(task) = current_task {
|
||||||
if let Some(task) = current_task {
|
todo_file.add_task(task);
|
||||||
list.add_task(task);
|
|
||||||
}
|
|
||||||
todo_file.add_list(list);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// Ensure there's always a Default list
|
|
||||||
todo_file.ensure_default_list();
|
|
||||||
|
|
||||||
Ok(todo_file)
|
Ok(todo_file)
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -356,19 +242,14 @@ pub fn generate_todo_file(todo_file: &TodoFile) -> String {
|
|||||||
let mut content = String::new();
|
let mut content = String::new();
|
||||||
content.push_str(&format!("# {}\n\n", todo_file.title));
|
content.push_str(&format!("# {}\n\n", todo_file.title));
|
||||||
|
|
||||||
for list in &todo_file.lists {
|
for task in &todo_file.tasks {
|
||||||
content.push_str(&format!("## {}\n", list.name));
|
content.push_str(&format!("- {}\n", task.text));
|
||||||
|
for subtask in &task.subtasks {
|
||||||
for task in &list.tasks {
|
content.push_str(&format!(" - {}\n", subtask));
|
||||||
content.push_str(&format!("- {}\n", task.text));
|
|
||||||
for subtask in &task.subtasks {
|
|
||||||
content.push_str(&format!(" - {}\n", subtask));
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
content.push('\n');
|
|
||||||
}
|
}
|
||||||
|
|
||||||
content.trim_end().to_string() + "\n"
|
content
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Parse an archive file with date-based sections
|
/// Parse an archive file with date-based sections
|
||||||
@@ -586,41 +467,33 @@ mod tests {
|
|||||||
fn test_parse_simple_todo_file() {
|
fn test_parse_simple_todo_file() {
|
||||||
let content = r#"# TODOs
|
let content = r#"# TODOs
|
||||||
|
|
||||||
## Default
|
|
||||||
- First task
|
- First task
|
||||||
- Second task
|
- Second task
|
||||||
- Subtask A
|
- Subtask A
|
||||||
- Subtask B
|
- Subtask B
|
||||||
|
- Third task
|
||||||
## Work
|
|
||||||
- Work task
|
|
||||||
"#;
|
"#;
|
||||||
|
|
||||||
let todo_file = parse_todo_file(content).unwrap();
|
let todo_file = parse_todo_file(content).unwrap();
|
||||||
assert_eq!(todo_file.title, "TODOs");
|
assert_eq!(todo_file.title, "TODOs");
|
||||||
assert_eq!(todo_file.lists.len(), 2);
|
assert_eq!(todo_file.tasks.len(), 3);
|
||||||
|
assert_eq!(todo_file.tasks[1].subtasks.len(), 2);
|
||||||
let default_list = todo_file.get_list("Default").unwrap();
|
assert_eq!(todo_file.tasks[1].subtasks[0], "Subtask A");
|
||||||
assert_eq!(default_list.tasks.len(), 2);
|
assert_eq!(todo_file.tasks[1].subtasks[1], "Subtask B");
|
||||||
assert_eq!(default_list.tasks[1].subtasks.len(), 2);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn test_generate_todo_file() {
|
fn test_generate_todo_file() {
|
||||||
let mut todo_file = TodoFile::new("TODOs".to_string());
|
let mut todo_file = TodoFile::new("TODOs".to_string());
|
||||||
let mut default_list = TaskList::new("Default".to_string());
|
|
||||||
|
|
||||||
let mut task = Task::new("Task with subtasks".to_string());
|
let mut task = Task::new("Task with subtasks".to_string());
|
||||||
task.add_subtask("Subtask 1".to_string());
|
task.add_subtask("Subtask 1".to_string());
|
||||||
task.add_subtask("Subtask 2".to_string());
|
task.add_subtask("Subtask 2".to_string());
|
||||||
default_list.add_task(task);
|
todo_file.add_task(task);
|
||||||
|
|
||||||
todo_file.add_list(default_list);
|
|
||||||
|
|
||||||
let generated = generate_todo_file(&todo_file);
|
let generated = generate_todo_file(&todo_file);
|
||||||
let expected = r#"# TODOs
|
let expected = r#"# TODOs
|
||||||
|
|
||||||
## Default
|
|
||||||
- Task with subtasks
|
- Task with subtasks
|
||||||
- Subtask 1
|
- Subtask 1
|
||||||
- Subtask 2
|
- Subtask 2
|
||||||
@@ -632,7 +505,6 @@ mod tests {
|
|||||||
fn test_handle_deep_nesting_gracefully() {
|
fn test_handle_deep_nesting_gracefully() {
|
||||||
let content = r#"# TODOs
|
let content = r#"# TODOs
|
||||||
|
|
||||||
## Default
|
|
||||||
- Task
|
- Task
|
||||||
- Subtask
|
- Subtask
|
||||||
- Deep subtask
|
- Deep subtask
|
||||||
@@ -640,21 +512,19 @@ mod tests {
|
|||||||
|
|
||||||
// Resilient parser should ignore deep nesting rather than error
|
// Resilient parser should ignore deep nesting rather than error
|
||||||
let todo_file = parse_todo_file(content).unwrap();
|
let todo_file = parse_todo_file(content).unwrap();
|
||||||
let default_list = todo_file.get_list("Default").unwrap();
|
|
||||||
// The resilient parser treats 4 spaces as valid subtask indentation
|
// The resilient parser treats 4 spaces as valid subtask indentation
|
||||||
// So "Deep subtask" becomes a subtask rather than being ignored
|
// So "Deep subtask" becomes a subtask rather than being ignored
|
||||||
assert_eq!(default_list.tasks.len(), 1);
|
assert_eq!(todo_file.tasks.len(), 1);
|
||||||
assert_eq!(default_list.tasks[0].text, "Task");
|
assert_eq!(todo_file.tasks[0].text, "Task");
|
||||||
assert_eq!(default_list.tasks[0].subtasks.len(), 2);
|
assert_eq!(todo_file.tasks[0].subtasks.len(), 2);
|
||||||
assert_eq!(default_list.tasks[0].subtasks[0], "Subtask");
|
assert_eq!(todo_file.tasks[0].subtasks[0], "Subtask");
|
||||||
assert_eq!(default_list.tasks[0].subtasks[1], "Deep subtask");
|
assert_eq!(todo_file.tasks[0].subtasks[1], "Deep subtask");
|
||||||
}
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn test_resilient_parsing_various_formats() {
|
fn test_resilient_parsing_various_formats() {
|
||||||
let content = r#"# TODOs
|
let content = r#"# TODOs
|
||||||
|
|
||||||
##Default
|
|
||||||
- Task with dash
|
- Task with dash
|
||||||
* Task with asterisk
|
* Task with asterisk
|
||||||
+ Task with plus
|
+ Task with plus
|
||||||
@@ -663,7 +533,6 @@ mod tests {
|
|||||||
- Subtask with 4 spaces
|
- Subtask with 4 spaces
|
||||||
- Subtask with tab
|
- Subtask with tab
|
||||||
|
|
||||||
## Work
|
|
||||||
Plain text task without bullet
|
Plain text task without bullet
|
||||||
- Normal task
|
- Normal task
|
||||||
|
|
||||||
@@ -676,25 +545,25 @@ This is a code block
|
|||||||
let todo_file = parse_todo_file(content).unwrap();
|
let todo_file = parse_todo_file(content).unwrap();
|
||||||
assert_eq!(todo_file.title, "TODOs");
|
assert_eq!(todo_file.title, "TODOs");
|
||||||
|
|
||||||
let default_list = todo_file.get_list("Default").unwrap();
|
// Now all tasks are in the single task list
|
||||||
assert_eq!(default_list.tasks.len(), 3); // 3 bullet tasks (all subtasks go under plus task)
|
assert_eq!(todo_file.tasks.len(), 6); // All tasks are top-level now (including code block content)
|
||||||
|
|
||||||
// First task (dash) has no subtasks
|
// First task (dash) has no subtasks
|
||||||
assert_eq!(default_list.tasks[0].text, "Task with dash");
|
assert_eq!(todo_file.tasks[0].text, "Task with dash");
|
||||||
assert_eq!(default_list.tasks[0].subtasks.len(), 0);
|
assert_eq!(todo_file.tasks[0].subtasks.len(), 0);
|
||||||
|
|
||||||
// Second task (asterisk) has no subtasks
|
// Second task (asterisk) has no subtasks
|
||||||
assert_eq!(default_list.tasks[1].text, "Task with asterisk");
|
assert_eq!(todo_file.tasks[1].text, "Task with asterisk");
|
||||||
assert_eq!(default_list.tasks[1].subtasks.len(), 0);
|
assert_eq!(todo_file.tasks[1].subtasks.len(), 0);
|
||||||
|
|
||||||
// Third task (plus) has all the subtasks (different indentation styles)
|
// Third task (plus) has all the subtasks (different indentation styles)
|
||||||
assert_eq!(default_list.tasks[2].text, "Task with plus");
|
assert_eq!(todo_file.tasks[2].text, "Task with plus");
|
||||||
assert_eq!(default_list.tasks[2].subtasks.len(), 4);
|
assert_eq!(todo_file.tasks[2].subtasks.len(), 4);
|
||||||
|
|
||||||
let work_list = todo_file.get_list("Work").unwrap();
|
// Plain text task and normal task
|
||||||
assert_eq!(work_list.tasks.len(), 3); // Plain text + normal task + code block content
|
assert_eq!(todo_file.tasks[3].text, "Plain text task without bullet");
|
||||||
assert_eq!(work_list.tasks[0].text, "Plain text task without bullet");
|
assert_eq!(todo_file.tasks[4].text, "Normal task");
|
||||||
assert_eq!(work_list.tasks[1].text, "Normal task");
|
assert_eq!(todo_file.tasks[5].text, "This is a code block");
|
||||||
// Comments are ignored, but code block content might be parsed as task
|
// Comments are ignored but code block content is parsed
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
+11
-17
@@ -3,7 +3,7 @@
|
|||||||
//! This module handles the one-time conversion from the old plain text format
|
//! This module handles the one-time conversion from the old plain text format
|
||||||
//! to the new Markdown format with Default list structure.
|
//! to the new Markdown format with Default list structure.
|
||||||
|
|
||||||
use crate::markdown::{Task, TaskList, TodoFile, ArchiveFile};
|
use crate::markdown::{Task, TodoFile, ArchiveFile};
|
||||||
use std::fs;
|
use std::fs;
|
||||||
use std::path::Path;
|
use std::path::Path;
|
||||||
|
|
||||||
@@ -25,18 +25,16 @@ pub fn migrate_note_file(note_path: &Path) -> Result<TodoFile, String> {
|
|||||||
.map_err(|e| format!("Failed to read note file: {}", e))?;
|
.map_err(|e| format!("Failed to read note file: {}", e))?;
|
||||||
|
|
||||||
let mut todo_file = TodoFile::new("TODOs".to_string());
|
let mut todo_file = TodoFile::new("TODOs".to_string());
|
||||||
let mut default_list = TaskList::new("Default".to_string());
|
|
||||||
|
|
||||||
// Split content into lines and create tasks
|
// Split content into lines and create tasks
|
||||||
for line in content.lines() {
|
for line in content.lines() {
|
||||||
let trimmed = line.trim();
|
let trimmed = line.trim();
|
||||||
if !trimmed.is_empty() {
|
if !trimmed.is_empty() {
|
||||||
let task = Task::new(trimmed.to_string());
|
let task = Task::new(trimmed.to_string());
|
||||||
default_list.add_task(task);
|
todo_file.add_task(task);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
todo_file.add_list(default_list);
|
|
||||||
Ok(todo_file)
|
Ok(todo_file)
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -68,11 +66,9 @@ pub fn migrate_archive_file(archive_path: &Path) -> Result<ArchiveFile, String>
|
|||||||
Ok(archive_file)
|
Ok(archive_file)
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Create an empty todo file with just a Default list
|
/// Create an empty todo file
|
||||||
pub fn create_empty_todo_file() -> TodoFile {
|
pub fn create_empty_todo_file() -> TodoFile {
|
||||||
let mut todo_file = TodoFile::new("TODOs".to_string());
|
TodoFile::new("TODOs".to_string())
|
||||||
todo_file.add_list(TaskList::new("Default".to_string()));
|
|
||||||
todo_file
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Create backup files before migration
|
/// Create backup files before migration
|
||||||
@@ -121,7 +117,7 @@ pub fn perform_migration(
|
|||||||
.map_err(|e| format!("Failed to write archive.md: {}", e))?;
|
.map_err(|e| format!("Failed to write archive.md: {}", e))?;
|
||||||
|
|
||||||
// Count migrated items
|
// Count migrated items
|
||||||
let todo_count = todo_file.get_default_list().map(|l| l.task_count()).unwrap_or(0);
|
let todo_count = todo_file.task_count();
|
||||||
let archive_count = archive_file.entries.first()
|
let archive_count = archive_file.entries.first()
|
||||||
.and_then(|e| e.lists.get("Default"))
|
.and_then(|e| e.lists.get("Default"))
|
||||||
.map(|tasks| tasks.len())
|
.map(|tasks| tasks.len())
|
||||||
@@ -157,11 +153,10 @@ mod tests {
|
|||||||
let result = migrate_note_file(file.path()).unwrap();
|
let result = migrate_note_file(file.path()).unwrap();
|
||||||
|
|
||||||
assert_eq!(result.title, "TODOs");
|
assert_eq!(result.title, "TODOs");
|
||||||
let default_list = result.get_list("Default").unwrap();
|
assert_eq!(result.tasks.len(), 3);
|
||||||
assert_eq!(default_list.tasks.len(), 3);
|
assert_eq!(result.tasks[0].text, "First task");
|
||||||
assert_eq!(default_list.tasks[0].text, "First task");
|
assert_eq!(result.tasks[1].text, "Second task");
|
||||||
assert_eq!(default_list.tasks[1].text, "Second task");
|
assert_eq!(result.tasks[2].text, "Third task");
|
||||||
assert_eq!(default_list.tasks[2].text, "Third task");
|
|
||||||
}
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
@@ -170,8 +165,7 @@ mod tests {
|
|||||||
let result = migrate_note_file(file.path()).unwrap();
|
let result = migrate_note_file(file.path()).unwrap();
|
||||||
|
|
||||||
assert_eq!(result.title, "TODOs");
|
assert_eq!(result.title, "TODOs");
|
||||||
let default_list = result.get_list("Default").unwrap();
|
assert_eq!(result.tasks.len(), 0);
|
||||||
assert_eq!(default_list.tasks.len(), 0);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
@@ -197,7 +191,7 @@ mod tests {
|
|||||||
let nonexistent = temp_dir.path().join("nonexistent.txt");
|
let nonexistent = temp_dir.path().join("nonexistent.txt");
|
||||||
|
|
||||||
let todo_result = migrate_note_file(&nonexistent).unwrap();
|
let todo_result = migrate_note_file(&nonexistent).unwrap();
|
||||||
assert!(todo_result.get_list("Default").unwrap().is_empty());
|
assert!(todo_result.is_empty());
|
||||||
|
|
||||||
let archive_result = migrate_archive_file(&nonexistent).unwrap();
|
let archive_result = migrate_archive_file(&nonexistent).unwrap();
|
||||||
assert!(archive_result.entries.is_empty());
|
assert!(archive_result.entries.is_empty());
|
||||||
|
|||||||
+12
-12
@@ -250,7 +250,7 @@ mod list_tests {
|
|||||||
|
|
||||||
/// Helper function to create Markdown todo files for new tests
|
/// Helper function to create Markdown todo files for new tests
|
||||||
fn create_markdown_test_file(tasks: &[&str]) -> NamedTempFile {
|
fn create_markdown_test_file(tasks: &[&str]) -> NamedTempFile {
|
||||||
let mut content = String::from("# TODOs\n\n## Default\n");
|
let mut content = String::from("# TODOs\n\n");
|
||||||
for task in tasks {
|
for task in tasks {
|
||||||
content.push_str(&format!("- {}\n", task));
|
content.push_str(&format!("- {}\n", task));
|
||||||
}
|
}
|
||||||
@@ -475,7 +475,7 @@ mod remove_tests {
|
|||||||
|
|
||||||
/// Helper function to create Markdown todo files for new tests
|
/// Helper function to create Markdown todo files for new tests
|
||||||
fn create_markdown_test_file(tasks: &[&str]) -> NamedTempFile {
|
fn create_markdown_test_file(tasks: &[&str]) -> NamedTempFile {
|
||||||
let mut content = String::from("# TODOs\n\n## Default\n");
|
let mut content = String::from("# TODOs\n\n");
|
||||||
for task in tasks {
|
for task in tasks {
|
||||||
content.push_str(&format!("- {}\n", task));
|
content.push_str(&format!("- {}\n", task));
|
||||||
}
|
}
|
||||||
@@ -487,7 +487,7 @@ mod remove_tests {
|
|||||||
|
|
||||||
/// Helper function to create Markdown todo files with subtasks
|
/// Helper function to create Markdown todo files with subtasks
|
||||||
fn create_markdown_test_file_with_subtasks(tasks_with_subtasks: &[(&str, &[&str])]) -> NamedTempFile {
|
fn create_markdown_test_file_with_subtasks(tasks_with_subtasks: &[(&str, &[&str])]) -> NamedTempFile {
|
||||||
let mut content = String::from("# TODOs\n\n## Default\n");
|
let mut content = String::from("# TODOs\n\n");
|
||||||
for (task, subtasks) in tasks_with_subtasks {
|
for (task, subtasks) in tasks_with_subtasks {
|
||||||
content.push_str(&format!("- {}\n", task));
|
content.push_str(&format!("- {}\n", task));
|
||||||
for subtask in *subtasks {
|
for subtask in *subtasks {
|
||||||
@@ -512,7 +512,7 @@ mod remove_tests {
|
|||||||
|
|
||||||
// Verify the item was removed from the file
|
// Verify the item was removed from the file
|
||||||
let updated_content = std::fs::read_to_string(file.path()).unwrap();
|
let updated_content = std::fs::read_to_string(file.path()).unwrap();
|
||||||
let expected_content = "# TODOs\n\n## Default\n- First item\n- Third item\n";
|
let expected_content = "# TODOs\n\n- First item\n- Third item\n";
|
||||||
assert_eq!(updated_content, expected_content);
|
assert_eq!(updated_content, expected_content);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -528,7 +528,7 @@ mod remove_tests {
|
|||||||
|
|
||||||
// Verify the items were removed from the file
|
// Verify the items were removed from the file
|
||||||
let updated_content = std::fs::read_to_string(file.path()).unwrap();
|
let updated_content = std::fs::read_to_string(file.path()).unwrap();
|
||||||
let expected_content = "# TODOs\n\n## Default\n- Second item\n- Fourth item\n";
|
let expected_content = "# TODOs\n\n- Second item\n- Fourth item\n";
|
||||||
assert_eq!(updated_content, expected_content);
|
assert_eq!(updated_content, expected_content);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -542,9 +542,9 @@ mod remove_tests {
|
|||||||
});
|
});
|
||||||
assert!(result.is_ok());
|
assert!(result.is_ok());
|
||||||
|
|
||||||
// Verify the file has only empty Default list
|
// Verify the file has only the header
|
||||||
let updated_content = std::fs::read_to_string(file.path()).unwrap();
|
let updated_content = std::fs::read_to_string(file.path()).unwrap();
|
||||||
let expected_content = "# TODOs\n\n## Default\n";
|
let expected_content = "# TODOs\n\n";
|
||||||
assert_eq!(updated_content, expected_content);
|
assert_eq!(updated_content, expected_content);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -576,7 +576,7 @@ mod remove_tests {
|
|||||||
|
|
||||||
// Verify only unique items were removed
|
// Verify only unique items were removed
|
||||||
let updated_content = std::fs::read_to_string(file.path()).unwrap();
|
let updated_content = std::fs::read_to_string(file.path()).unwrap();
|
||||||
let expected_content = "# TODOs\n\n## Default\n- Third item\n";
|
let expected_content = "# TODOs\n\n- Third item\n";
|
||||||
assert_eq!(updated_content, expected_content);
|
assert_eq!(updated_content, expected_content);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -605,9 +605,9 @@ mod remove_tests {
|
|||||||
});
|
});
|
||||||
assert!(result.is_ok());
|
assert!(result.is_ok());
|
||||||
|
|
||||||
// Verify the file remains with empty Default list
|
// Verify the file remains with just the header
|
||||||
let updated_content = std::fs::read_to_string(file.path()).unwrap();
|
let updated_content = std::fs::read_to_string(file.path()).unwrap();
|
||||||
let expected_content = "# TODOs\n\n## Default\n";
|
let expected_content = "# TODOs\n\n";
|
||||||
assert_eq!(updated_content, expected_content);
|
assert_eq!(updated_content, expected_content);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -628,7 +628,7 @@ mod remove_tests {
|
|||||||
|
|
||||||
// Verify the item was removed from the original file
|
// Verify the item was removed from the original file
|
||||||
let updated_content = std::fs::read_to_string(file.path()).unwrap();
|
let updated_content = std::fs::read_to_string(file.path()).unwrap();
|
||||||
let expected_content = "# TODOs\n\n## Default\n- Second item\n";
|
let expected_content = "# TODOs\n\n- Second item\n";
|
||||||
assert_eq!(updated_content, expected_content);
|
assert_eq!(updated_content, expected_content);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -652,7 +652,7 @@ mod remove_tests {
|
|||||||
|
|
||||||
// Verify Task B was auto-completed and removed from todo file
|
// Verify Task B was auto-completed and removed from todo file
|
||||||
let updated_content = std::fs::read_to_string(file.path()).unwrap();
|
let updated_content = std::fs::read_to_string(file.path()).unwrap();
|
||||||
let expected_content = "# TODOs\n\n## Default\n- Task A\n- Task C\n - Another subtask\n";
|
let expected_content = "# TODOs\n\n- Task A\n- Task C\n - Another subtask\n";
|
||||||
assert_eq!(updated_content, expected_content);
|
assert_eq!(updated_content, expected_content);
|
||||||
|
|
||||||
// Verify both subtasks AND the parent task are in the archive
|
// Verify both subtasks AND the parent task are in the archive
|
||||||
|
|||||||
@@ -118,7 +118,7 @@ mod basic_operations {
|
|||||||
// Add first item
|
// Add first item
|
||||||
let result = env.run_ldr(&["add", "First task"]);
|
let result = env.run_ldr(&["add", "First task"]);
|
||||||
result.assert_success();
|
result.assert_success();
|
||||||
assert!(result.stdout.contains("✓ Added to Default: First task"));
|
assert!(result.stdout.contains("✓ Added: First task"));
|
||||||
|
|
||||||
// Add second item (should go to top)
|
// Add second item (should go to top)
|
||||||
let result = env.run_ldr(&["add", "Second task"]);
|
let result = env.run_ldr(&["add", "Second task"]);
|
||||||
@@ -639,7 +639,6 @@ mod edit_functionality {
|
|||||||
// File should exist with proper structure
|
// File should exist with proper structure
|
||||||
let todos = env.read_todos();
|
let todos = env.read_todos();
|
||||||
assert!(todos.contains("# TODOs"));
|
assert!(todos.contains("# TODOs"));
|
||||||
assert!(todos.contains("## Default"));
|
|
||||||
}
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
@@ -694,7 +693,6 @@ mod migration_tests {
|
|||||||
// Verify content was migrated correctly
|
// Verify content was migrated correctly
|
||||||
let todos = env.read_todos();
|
let todos = env.read_todos();
|
||||||
assert!(todos.contains("# TODOs"));
|
assert!(todos.contains("# TODOs"));
|
||||||
assert!(todos.contains("## Default"));
|
|
||||||
assert!(todos.contains("- Task A"));
|
assert!(todos.contains("- Task A"));
|
||||||
assert!(todos.contains("- Task B with details"));
|
assert!(todos.contains("- Task B with details"));
|
||||||
assert!(todos.contains("- Task C"));
|
assert!(todos.contains("- Task C"));
|
||||||
@@ -715,7 +713,7 @@ mod migration_tests {
|
|||||||
|
|
||||||
// Create markdown file first
|
// Create markdown file first
|
||||||
fs::create_dir_all(env.todos_path().parent().unwrap()).unwrap();
|
fs::create_dir_all(env.todos_path().parent().unwrap()).unwrap();
|
||||||
fs::write(&env.todos_path(), "# TODOs\n\n## Default\n- Existing task\n").unwrap();
|
fs::write(&env.todos_path(), "# TODOs\n\n- Existing task\n").unwrap();
|
||||||
|
|
||||||
// Create old-style file
|
// Create old-style file
|
||||||
let old_note_path = env.data_dir.join("ldr/note.txt");
|
let old_note_path = env.data_dir.join("ldr/note.txt");
|
||||||
|
|||||||
Reference in new issue
Block a user