use super::io::{load_todo_list, save_todo_list};
use super::types::{AppMode, CMD_POPUP_ITEMS, TodoItem, TodoList};
use crate::command::chat::storage::load_agent_config;
use crate::constants::todo_filter;
use crate::theme::Theme;
use chrono::Local;
use ratatui::widgets::ListState;
pub struct TodoApp {
pub list: TodoList,
pub snapshot: TodoList,
pub state: ListState,
pub mode: AppMode,
pub input: String,
pub edit_index: Option<usize>,
pub message: Option<String>,
pub filter: usize,
pub quit_input: String,
pub cursor_pos: usize,
pub report_pending_content: Option<String>,
pub cmd_popup_filter: String,
pub cmd_popup_selected: usize,
pub theme: Theme,
}
impl Default for TodoApp {
fn default() -> Self {
Self::new()
}
}
impl TodoApp {
pub fn new() -> Self {
let list = load_todo_list();
let snapshot = list.clone();
let mut state = ListState::default();
if !list.items.is_empty() {
state.select(Some(0));
}
let agent_config = load_agent_config();
let theme = Theme::from_name(&agent_config.theme);
Self {
list,
snapshot,
state,
mode: AppMode::Normal,
input: String::new(),
edit_index: None,
message: None,
filter: todo_filter::DEFAULT,
quit_input: String::new(),
cursor_pos: 0,
report_pending_content: None,
cmd_popup_filter: String::new(),
cmd_popup_selected: 0,
theme,
}
}
pub fn filtered_cmd_items(&self) -> Vec<(usize, &'static str, &'static str)> {
let filter = self.cmd_popup_filter.to_lowercase();
CMD_POPUP_ITEMS
.iter()
.enumerate()
.filter(|(_, (key, label))| {
filter.is_empty()
|| key.contains(filter.as_str())
|| label.contains(filter.as_str())
})
.map(|(i, (key, label))| (i, *key, *label))
.collect()
}
pub fn is_dirty(&self) -> bool {
self.list != self.snapshot
}
pub fn filtered_indices(&self) -> Vec<usize> {
self.list
.items
.iter()
.enumerate()
.filter(|(_, item)| match self.filter {
todo_filter::UNDONE => !item.done,
todo_filter::DONE => item.done,
todo_filter::ALL => true,
_ => true,
})
.map(|(i, _)| i)
.collect()
}
pub fn selected_real_index(&self) -> Option<usize> {
let indices = self.filtered_indices();
self.state
.selected()
.and_then(|sel| indices.get(sel).copied())
}
pub fn move_down(&mut self) {
let count = self.filtered_indices().len();
if count == 0 {
return;
}
let i = match self.state.selected() {
Some(i) => (i + 1) % count,
None => 0,
};
self.state.select(Some(i));
}
pub fn move_up(&mut self) {
let count = self.filtered_indices().len();
if count == 0 {
return;
}
let i = match self.state.selected() {
Some(i) => (i + count - 1) % count,
None => 0,
};
self.state.select(Some(i));
}
pub fn toggle_done(&mut self) {
if let Some(real_idx) = self.selected_real_index() {
let item = &mut self.list.items[real_idx];
item.done = !item.done;
if item.done {
item.done_at = Some(Local::now().format("%Y-%m-%d %H:%M:%S").to_string());
self.report_pending_content = Some(item.content.clone());
self.mode = AppMode::ConfirmReport;
} else {
item.done_at = None;
self.message = Some("⬜ 已标记为未完成".to_string());
}
}
}
pub fn add_item(&mut self) {
let text = self.input.trim().to_string();
if text.is_empty() {
self.message = Some("⚠️ 内容为空,已取消".to_string());
self.mode = AppMode::Normal;
self.input.clear();
return;
}
self.list.items.push(TodoItem {
content: text,
done: false,
created_at: Local::now().format("%Y-%m-%d %H:%M:%S").to_string(),
done_at: None,
});
self.input.clear();
self.mode = AppMode::Normal;
let count = self.filtered_indices().len();
if count > 0 {
self.state.select(Some(count - 1));
}
if save_todo_list(&self.list) {
self.snapshot = self.list.clone();
self.message = Some("☑️ 已添加并保存".to_string());
} else {
self.message = Some("☑️ 已添加(保存失败)".to_string());
}
}
pub fn confirm_edit(&mut self) {
let text = self.input.trim().to_string();
if text.is_empty() {
self.message = Some("⚠️ 内容为空,已取消编辑".to_string());
self.mode = AppMode::Normal;
self.input.clear();
self.edit_index = None;
return;
}
if let Some(idx) = self.edit_index
&& idx < self.list.items.len()
{
self.list.items[idx].content = text;
if save_todo_list(&self.list) {
self.snapshot = self.list.clone();
self.message = Some("☑️ 已更新并保存".to_string());
} else {
self.message = Some("☑️ 已更新(保存失败)".to_string());
}
}
self.input.clear();
self.edit_index = None;
self.mode = AppMode::Normal;
}
pub fn delete_selected(&mut self) {
if let Some(real_idx) = self.selected_real_index() {
let removed = self.list.items.remove(real_idx);
self.message = Some(format!("🗑️ 已删除: {}", removed.content));
let count = self.filtered_indices().len();
if count == 0 {
self.state.select(None);
} else if let Some(sel) = self.state.selected()
&& sel >= count
{
self.state.select(Some(count - 1));
}
}
self.mode = AppMode::Normal;
}
pub fn move_item_up(&mut self) {
if let Some(real_idx) = self.selected_real_index()
&& real_idx > 0
{
self.list.items.swap(real_idx, real_idx - 1);
self.move_up();
}
}
pub fn move_item_down(&mut self) {
if let Some(real_idx) = self.selected_real_index()
&& real_idx < self.list.items.len() - 1
{
self.list.items.swap(real_idx, real_idx + 1);
self.move_down();
}
}
pub fn toggle_filter(&mut self) {
self.filter = (self.filter + 1) % todo_filter::COUNT;
let count = self.filtered_indices().len();
if count > 0 {
self.state.select(Some(0));
} else {
self.state.select(None);
}
let label = todo_filter::label(self.filter);
self.message = Some(format!("🔍 过滤: {}", label));
}
pub fn save(&mut self) {
if self.is_dirty() {
if save_todo_list(&self.list) {
self.snapshot = self.list.clone();
self.message = Some("💾 已保存".to_string());
}
} else {
self.message = Some("📋 无需保存,没有修改".to_string());
}
}
}