Skip to main content

j_cli/command/todo/
mod.rs

1pub mod app;
2pub mod ui;
3
4use crate::config::YamlConfig;
5use crate::{error, info, usage};
6use app::{
7    AppMode, TodoApp, TodoItem, handle_confirm_cancel_input, handle_confirm_delete,
8    handle_confirm_report, handle_help_mode, handle_input_mode, handle_normal_mode, load_todo_list,
9    save_todo_list,
10};
11use chrono::Local;
12use crossterm::{
13    event::{self, Event, KeyCode, KeyModifiers},
14    execute,
15    terminal::{self, EnterAlternateScreen, LeaveAlternateScreen},
16};
17use ratatui::{Terminal, backend::CrosstermBackend};
18use std::io;
19use ui::draw_ui;
20
21/// 处理 todo 命令: j todo [list | add <content...>]
22pub fn handle_todo(content: &[String], config: &mut YamlConfig) {
23    if content.is_empty() {
24        run_todo_tui(config);
25        return;
26    }
27
28    match content[0].as_str() {
29        "list" => handle_todo_list(),
30        "add" => {
31            let text = content[1..].join(" ");
32            let text = text.trim().trim_matches('"').to_string();
33            if text.is_empty() {
34                error!("⚠️ 内容为空,无法添加待办");
35                return;
36            }
37            quick_add_todo(&text);
38        }
39        _ => {
40            usage!("j todo | j todo list | j todo add <内容>");
41        }
42    }
43}
44
45/// 快速添加一条待办(不进入 TUI)
46fn quick_add_todo(text: &str) {
47    let mut todo_list = load_todo_list();
48    todo_list.items.push(TodoItem {
49        content: text.to_string(),
50        done: false,
51        created_at: Local::now().format("%Y-%m-%d %H:%M:%S").to_string(),
52        done_at: None,
53    });
54
55    if save_todo_list(&todo_list) {
56        info!("✅ 已添加待办: {}", text);
57        let undone = todo_list.items.iter().filter(|i| !i.done).count();
58        info!("📋 当前未完成待办: {} 条", undone);
59    }
60}
61
62/// 列出所有待办,以 Markdown 格式渲染输出
63fn handle_todo_list() {
64    let todo_list = load_todo_list();
65
66    if todo_list.items.is_empty() {
67        info!("📋 暂无待办");
68        return;
69    }
70
71    let total = todo_list.items.len();
72    let done_count = todo_list.items.iter().filter(|i| i.done).count();
73    let undone_count = total - done_count;
74
75    let mut md = format!(
76        "## 待办备忘录 — 共 {} 条 | ✅ {} | ⬜ {}\n\n",
77        total, done_count, undone_count
78    );
79
80    for item in &todo_list.items {
81        if item.done {
82            md.push_str(&format!("- [x] {}\n", item.content));
83        } else {
84            md.push_str(&format!("- [ ] {}\n", item.content));
85        }
86    }
87
88    crate::md!("{}", md);
89}
90
91/// 启动 TUI 待办管理界面
92fn run_todo_tui(config: &mut YamlConfig) {
93    match run_todo_tui_internal(config) {
94        Ok(_) => {}
95        Err(e) => {
96            error!("❌ TUI 启动失败: {}", e);
97        }
98    }
99}
100
101fn run_todo_tui_internal(config: &mut YamlConfig) -> io::Result<()> {
102    terminal::enable_raw_mode()?;
103    let mut stdout = io::stdout();
104    execute!(stdout, EnterAlternateScreen)?;
105
106    let backend = CrosstermBackend::new(stdout);
107    let mut terminal = Terminal::new(backend)?;
108
109    let mut app = TodoApp::new();
110    let mut last_input_len: usize = 0;
111    // 记录进入 ConfirmCancelInput 前的模式,用于继续编辑时恢复
112    let mut prev_input_mode: Option<AppMode> = None;
113
114    loop {
115        terminal.draw(|f| draw_ui(f, &mut app))?;
116
117        let current_input_len = app.input.chars().count();
118        if current_input_len != last_input_len {
119            app.preview_scroll = 0;
120            last_input_len = current_input_len;
121        }
122
123        if event::poll(std::time::Duration::from_millis(100))? {
124            if let Event::Key(key) = event::read()? {
125                // Alt+↑/↓ 预览区滚动(在 Adding/Editing 模式下)
126                if (app.mode == AppMode::Adding || app.mode == AppMode::Editing)
127                    && key.modifiers.contains(KeyModifiers::ALT)
128                {
129                    match key.code {
130                        KeyCode::Down => {
131                            app.preview_scroll = app.preview_scroll.saturating_add(1);
132                            continue;
133                        }
134                        KeyCode::Up => {
135                            app.preview_scroll = app.preview_scroll.saturating_sub(1);
136                            continue;
137                        }
138                        _ => {}
139                    }
140                }
141
142                match app.mode {
143                    AppMode::Normal => {
144                        if handle_normal_mode(&mut app, key) {
145                            break;
146                        }
147                    }
148                    AppMode::Adding => {
149                        prev_input_mode = Some(AppMode::Adding);
150                        handle_input_mode(&mut app, key);
151                    }
152                    AppMode::Editing => {
153                        prev_input_mode = Some(AppMode::Editing);
154                        handle_input_mode(&mut app, key);
155                    }
156                    AppMode::ConfirmDelete => handle_confirm_delete(&mut app, key),
157                    AppMode::ConfirmReport => handle_confirm_report(&mut app, key, config),
158                    AppMode::ConfirmCancelInput => {
159                        let prev = prev_input_mode.clone().unwrap_or(AppMode::Adding);
160                        handle_confirm_cancel_input(&mut app, key, prev);
161                    }
162                    AppMode::Help => handle_help_mode(&mut app, key),
163                }
164            }
165        }
166    }
167
168    terminal::disable_raw_mode()?;
169    execute!(terminal.backend_mut(), LeaveAlternateScreen)?;
170
171    Ok(())
172}