Skip to main content

j_cli/command/
todo.rs

1use crate::config::YamlConfig;
2use crate::{error, info};
3use chrono::Local;
4use crossterm::{
5    event::{self, Event, KeyCode, KeyEvent, KeyModifiers},
6    execute,
7    terminal::{self, EnterAlternateScreen, LeaveAlternateScreen},
8};
9use ratatui::{
10    Terminal,
11    backend::CrosstermBackend,
12    layout::{Constraint, Direction, Layout},
13    style::{Color, Modifier, Style},
14    text::{Line, Span},
15    widgets::{Block, Borders, List, ListItem, ListState, Paragraph},
16};
17use serde::{Deserialize, Serialize};
18use std::fs;
19use std::io;
20use std::path::PathBuf;
21
22// ========== 数据结构 ==========
23
24/// 单条待办事项
25#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
26pub struct TodoItem {
27    /// 待办内容
28    pub content: String,
29    /// 是否已完成
30    pub done: bool,
31    /// 创建时间
32    pub created_at: String,
33    /// 完成时间(可选)
34    pub done_at: Option<String>,
35}
36
37/// 待办列表(序列化到 JSON)
38#[derive(Debug, Clone, Serialize, Deserialize, Default, PartialEq)]
39pub struct TodoList {
40    pub items: Vec<TodoItem>,
41}
42
43// ========== 文件路径 ==========
44
45/// 获取 todo 数据目录: ~/.jdata/todo/
46fn todo_dir() -> PathBuf {
47    let dir = YamlConfig::data_dir().join("todo");
48    let _ = fs::create_dir_all(&dir);
49    dir
50}
51
52/// 获取 todo 数据文件路径: ~/.jdata/todo/todo.json
53fn todo_file_path() -> PathBuf {
54    todo_dir().join("todo.json")
55}
56
57// ========== 数据读写 ==========
58
59/// 从文件加载待办列表
60fn load_todo_list() -> TodoList {
61    let path = todo_file_path();
62    if !path.exists() {
63        return TodoList::default();
64    }
65    match fs::read_to_string(&path) {
66        Ok(content) => serde_json::from_str(&content).unwrap_or_else(|e| {
67            error!("❌ 解析 todo.json 失败: {}", e);
68            TodoList::default()
69        }),
70        Err(e) => {
71            error!("❌ 读取 todo.json 失败: {}", e);
72            TodoList::default()
73        }
74    }
75}
76
77/// 保存待办列表到文件
78fn save_todo_list(list: &TodoList) -> bool {
79    let path = todo_file_path();
80    // 确保目录存在
81    if let Some(parent) = path.parent() {
82        let _ = fs::create_dir_all(parent);
83    }
84    match serde_json::to_string_pretty(list) {
85        Ok(json) => match fs::write(&path, json) {
86            Ok(_) => true,
87            Err(e) => {
88                error!("❌ 保存 todo.json 失败: {}", e);
89                false
90            }
91        },
92        Err(e) => {
93            error!("❌ 序列化 todo 列表失败: {}", e);
94            false
95        }
96    }
97}
98
99// ========== 命令入口 ==========
100
101/// 处理 todo 命令: j todo [content...]
102pub fn handle_todo(content: &[String], _config: &YamlConfig) {
103    if content.is_empty() {
104        // 无参数:进入 TUI 待办管理界面
105        run_todo_tui();
106        return;
107    }
108
109    // 有参数:快速添加待办
110    let text = content.join(" ");
111    let text = text.trim().trim_matches('"').to_string();
112
113    if text.is_empty() {
114        error!("⚠️ 内容为空,无法添加待办");
115        return;
116    }
117
118    let mut list = load_todo_list();
119    list.items.push(TodoItem {
120        content: text.clone(),
121        done: false,
122        created_at: Local::now().format("%Y-%m-%d %H:%M:%S").to_string(),
123        done_at: None,
124    });
125
126    if save_todo_list(&list) {
127        info!("✅ 已添加待办: {}", text);
128        // 显示当前待办总数
129        let undone = list.items.iter().filter(|i| !i.done).count();
130        info!("📋 当前未完成待办: {} 条", undone);
131    }
132}
133
134// ========== TUI 界面 ==========
135
136/// TUI 应用状态
137struct TodoApp {
138    /// 待办列表数据
139    list: TodoList,
140    /// 加载时的快照(用于对比是否真正有修改)
141    snapshot: TodoList,
142    /// 列表选中状态
143    state: ListState,
144    /// 当前模式
145    mode: AppMode,
146    /// 输入缓冲区(添加/编辑模式使用)
147    input: String,
148    /// 编辑时记录的原始索引
149    edit_index: Option<usize>,
150    /// 状态栏消息
151    message: Option<String>,
152    /// 过滤模式: 0=全部, 1=未完成, 2=已完成
153    filter: usize,
154    /// 强制退出输入缓冲(用于 q! 退出)
155    quit_input: String,
156    /// 输入模式下的光标位置(字符索引)
157    cursor_pos: usize,
158}
159
160#[derive(PartialEq)]
161enum AppMode {
162    /// 正常浏览模式
163    Normal,
164    /// 输入添加模式
165    Adding,
166    /// 编辑模式
167    Editing,
168    /// 确认删除
169    ConfirmDelete,
170    /// 显示帮助
171    Help,
172}
173
174impl TodoApp {
175    fn new() -> Self {
176        let list = load_todo_list();
177        let snapshot = list.clone();
178        let mut state = ListState::default();
179        if !list.items.is_empty() {
180            state.select(Some(0));
181        }
182        Self {
183            list,
184            snapshot,
185            state,
186            mode: AppMode::Normal,
187            input: String::new(),
188            edit_index: None,
189            message: None,
190            filter: 0,
191            quit_input: String::new(),
192            cursor_pos: 0,
193        }
194    }
195
196    /// 通过对比快照判断是否有未保存的修改
197    fn is_dirty(&self) -> bool {
198        self.list != self.snapshot
199    }
200
201    /// 获取当前过滤后的索引列表(映射到 list.items 的真实索引)
202    fn filtered_indices(&self) -> Vec<usize> {
203        self.list
204            .items
205            .iter()
206            .enumerate()
207            .filter(|(_, item)| match self.filter {
208                1 => !item.done,
209                2 => item.done,
210                _ => true,
211            })
212            .map(|(i, _)| i)
213            .collect()
214    }
215
216    /// 获取当前选中项在原始列表中的真实索引
217    fn selected_real_index(&self) -> Option<usize> {
218        let indices = self.filtered_indices();
219        self.state
220            .selected()
221            .and_then(|sel| indices.get(sel).copied())
222    }
223
224    /// 向下移动
225    fn move_down(&mut self) {
226        let count = self.filtered_indices().len();
227        if count == 0 {
228            return;
229        }
230        let i = match self.state.selected() {
231            Some(i) => {
232                if i >= count - 1 {
233                    0
234                } else {
235                    i + 1
236                }
237            }
238            None => 0,
239        };
240        self.state.select(Some(i));
241    }
242
243    /// 向上移动
244    fn move_up(&mut self) {
245        let count = self.filtered_indices().len();
246        if count == 0 {
247            return;
248        }
249        let i = match self.state.selected() {
250            Some(i) => {
251                if i == 0 {
252                    count - 1
253                } else {
254                    i - 1
255                }
256            }
257            None => 0,
258        };
259        self.state.select(Some(i));
260    }
261
262    /// 切换当前选中项的完成状态
263    fn toggle_done(&mut self) {
264        if let Some(real_idx) = self.selected_real_index() {
265            let item = &mut self.list.items[real_idx];
266            item.done = !item.done;
267            if item.done {
268                item.done_at = Some(Local::now().format("%Y-%m-%d %H:%M:%S").to_string());
269                self.message = Some("✅ 已标记为完成".to_string());
270            } else {
271                item.done_at = None;
272                self.message = Some("⬜ 已标记为未完成".to_string());
273            }
274        }
275    }
276
277    /// 添加新待办
278    fn add_item(&mut self) {
279        let text = self.input.trim().to_string();
280        if text.is_empty() {
281            self.message = Some("⚠️ 内容为空,已取消".to_string());
282            self.mode = AppMode::Normal;
283            self.input.clear();
284            return;
285        }
286        self.list.items.push(TodoItem {
287            content: text,
288            done: false,
289            created_at: Local::now().format("%Y-%m-%d %H:%M:%S").to_string(),
290            done_at: None,
291        });
292        self.input.clear();
293        self.mode = AppMode::Normal;
294        // 选中新添加的项
295        let count = self.filtered_indices().len();
296        if count > 0 {
297            self.state.select(Some(count - 1));
298        }
299        self.message = Some("✅ 已添加新待办".to_string());
300    }
301
302    /// 确认编辑
303    fn confirm_edit(&mut self) {
304        let text = self.input.trim().to_string();
305        if text.is_empty() {
306            self.message = Some("⚠️ 内容为空,已取消编辑".to_string());
307            self.mode = AppMode::Normal;
308            self.input.clear();
309            self.edit_index = None;
310            return;
311        }
312        if let Some(idx) = self.edit_index {
313            if idx < self.list.items.len() {
314                self.list.items[idx].content = text;
315                self.message = Some("✅ 已更新待办内容".to_string());
316            }
317        }
318        self.input.clear();
319        self.edit_index = None;
320        self.mode = AppMode::Normal;
321    }
322
323    /// 删除当前选中项
324    fn delete_selected(&mut self) {
325        if let Some(real_idx) = self.selected_real_index() {
326            let removed = self.list.items.remove(real_idx);
327            self.message = Some(format!("🗑️ 已删除: {}", removed.content));
328            // 调整选中位置
329            let count = self.filtered_indices().len();
330            if count == 0 {
331                self.state.select(None);
332            } else if let Some(sel) = self.state.selected() {
333                if sel >= count {
334                    self.state.select(Some(count - 1));
335                }
336            }
337        }
338        self.mode = AppMode::Normal;
339    }
340
341    /// 移动选中项向上(调整顺序)
342    fn move_item_up(&mut self) {
343        if let Some(real_idx) = self.selected_real_index() {
344            if real_idx > 0 {
345                self.list.items.swap(real_idx, real_idx - 1);
346                self.move_up();
347            }
348        }
349    }
350
351    /// 移动选中项向下(调整顺序)
352    fn move_item_down(&mut self) {
353        if let Some(real_idx) = self.selected_real_index() {
354            if real_idx < self.list.items.len() - 1 {
355                self.list.items.swap(real_idx, real_idx + 1);
356                self.move_down();
357            }
358        }
359    }
360
361    /// 切换过滤模式
362    fn toggle_filter(&mut self) {
363        self.filter = (self.filter + 1) % 3;
364        let count = self.filtered_indices().len();
365        if count > 0 {
366            self.state.select(Some(0));
367        } else {
368            self.state.select(None);
369        }
370        let label = match self.filter {
371            1 => "未完成",
372            2 => "已完成",
373            _ => "全部",
374        };
375        self.message = Some(format!("🔍 过滤: {}", label));
376    }
377
378    /// 保存数据
379    fn save(&mut self) {
380        if self.is_dirty() {
381            if save_todo_list(&self.list) {
382                // 更新快照为当前状态
383                self.snapshot = self.list.clone();
384                self.message = Some("💾 已保存".to_string());
385            }
386        } else {
387            self.message = Some("📋 无需保存,没有修改".to_string());
388        }
389    }
390}
391
392/// 启动 TUI 待办管理界面
393fn run_todo_tui() {
394    match run_todo_tui_internal() {
395        Ok(_) => {}
396        Err(e) => {
397            error!("❌ TUI 启动失败: {}", e);
398        }
399    }
400}
401
402fn run_todo_tui_internal() -> io::Result<()> {
403    // 进入终端原始模式
404    terminal::enable_raw_mode()?;
405    let mut stdout = io::stdout();
406    execute!(stdout, EnterAlternateScreen)?;
407
408    let backend = CrosstermBackend::new(stdout);
409    let mut terminal = Terminal::new(backend)?;
410
411    let mut app = TodoApp::new();
412
413    loop {
414        // 渲染界面
415        terminal.draw(|f| draw_ui(f, &mut app))?;
416
417        // 处理输入事件
418        if event::poll(std::time::Duration::from_millis(100))? {
419            if let Event::Key(key) = event::read()? {
420                match app.mode {
421                    AppMode::Normal => {
422                        if handle_normal_mode(&mut app, key) {
423                            break;
424                        }
425                    }
426                    AppMode::Adding => handle_input_mode(&mut app, key),
427                    AppMode::Editing => handle_input_mode(&mut app, key),
428                    AppMode::ConfirmDelete => handle_confirm_delete(&mut app, key),
429                    AppMode::Help => handle_help_mode(&mut app, key),
430                }
431            }
432        }
433    }
434
435    // 退出前不自动保存(用户需要手动保存或用 q! 放弃修改)
436
437    // 恢复终端
438    terminal::disable_raw_mode()?;
439    execute!(terminal.backend_mut(), LeaveAlternateScreen)?;
440
441    Ok(())
442}
443
444/// 绘制 TUI 界面
445fn draw_ui(f: &mut ratatui::Frame, app: &mut TodoApp) {
446    let size = f.area();
447
448    // 整体布局: 标题栏 + 列表区 + 状态栏 + 帮助栏
449    let chunks = Layout::default()
450        .direction(Direction::Vertical)
451        .constraints([
452            Constraint::Length(3), // 标题栏
453            Constraint::Min(5),    // 列表区
454            Constraint::Length(3), // 状态/输入栏
455            Constraint::Length(2), // 帮助栏
456        ])
457        .split(size);
458
459    // ========== 标题栏 ==========
460    let filter_label = match app.filter {
461        1 => " [未完成]",
462        2 => " [已完成]",
463        _ => "",
464    };
465    let total = app.list.items.len();
466    let done = app.list.items.iter().filter(|i| i.done).count();
467    let undone = total - done;
468    let title = format!(
469        " 📋 待办备忘录{} — 共 {} 条 | ✅ {} | ⬜ {} ",
470        filter_label, total, done, undone
471    );
472    let title_block = Paragraph::new(Line::from(vec![Span::styled(
473        title,
474        Style::default()
475            .fg(Color::Cyan)
476            .add_modifier(Modifier::BOLD),
477    )]))
478    .block(
479        Block::default()
480            .borders(Borders::ALL)
481            .border_style(Style::default().fg(Color::Cyan)),
482    );
483    f.render_widget(title_block, chunks[0]);
484
485    // ========== 列表区 ==========
486    if app.mode == AppMode::Help {
487        // 帮助模式:显示完整帮助信息
488        let help_lines = vec![
489            Line::from(Span::styled(
490                "  📖 快捷键帮助",
491                Style::default()
492                    .fg(Color::Cyan)
493                    .add_modifier(Modifier::BOLD),
494            )),
495            Line::from(""),
496            Line::from(vec![
497                Span::styled("  n / ↓ / j    ", Style::default().fg(Color::Yellow)),
498                Span::raw("向下移动"),
499            ]),
500            Line::from(vec![
501                Span::styled("  N / ↑ / k    ", Style::default().fg(Color::Yellow)),
502                Span::raw("向上移动"),
503            ]),
504            Line::from(vec![
505                Span::styled("  空格 / 回车   ", Style::default().fg(Color::Yellow)),
506                Span::raw("切换完成状态 [x] / [ ]"),
507            ]),
508            Line::from(vec![
509                Span::styled("  a            ", Style::default().fg(Color::Yellow)),
510                Span::raw("添加新待办"),
511            ]),
512            Line::from(vec![
513                Span::styled("  e            ", Style::default().fg(Color::Yellow)),
514                Span::raw("编辑选中待办"),
515            ]),
516            Line::from(vec![
517                Span::styled("  d            ", Style::default().fg(Color::Yellow)),
518                Span::raw("删除待办(需确认)"),
519            ]),
520            Line::from(vec![
521                Span::styled("  f            ", Style::default().fg(Color::Yellow)),
522                Span::raw("过滤切换(全部 / 未完成 / 已完成)"),
523            ]),
524            Line::from(vec![
525                Span::styled("  J / K        ", Style::default().fg(Color::Yellow)),
526                Span::raw("调整待办顺序(下移 / 上移)"),
527            ]),
528            Line::from(vec![
529                Span::styled("  s            ", Style::default().fg(Color::Yellow)),
530                Span::raw("手动保存"),
531            ]),
532            Line::from(vec![
533                Span::styled("  y            ", Style::default().fg(Color::Yellow)),
534                Span::raw("复制选中待办到剪切板"),
535            ]),
536            Line::from(vec![
537                Span::styled("  q            ", Style::default().fg(Color::Yellow)),
538                Span::raw("退出(有未保存修改时需先保存或用 q! 强制退出)"),
539            ]),
540            Line::from(vec![
541                Span::styled("  q!           ", Style::default().fg(Color::Yellow)),
542                Span::raw("强制退出(丢弃未保存的修改)"),
543            ]),
544            Line::from(vec![
545                Span::styled("  Esc          ", Style::default().fg(Color::Yellow)),
546                Span::raw("退出(同 q)"),
547            ]),
548            Line::from(vec![
549                Span::styled("  Ctrl+C       ", Style::default().fg(Color::Yellow)),
550                Span::raw("强制退出(不保存)"),
551            ]),
552            Line::from(vec![
553                Span::styled("  ?            ", Style::default().fg(Color::Yellow)),
554                Span::raw("显示此帮助"),
555            ]),
556        ];
557        let help_block = Block::default()
558            .borders(Borders::ALL)
559            .border_style(Style::default().fg(Color::Cyan))
560            .title(" 帮助 ");
561        let help_widget = Paragraph::new(help_lines).block(help_block);
562        f.render_widget(help_widget, chunks[1]);
563    } else {
564        let indices = app.filtered_indices();
565        let items: Vec<ListItem> = indices
566            .iter()
567            .map(|&idx| {
568                let item = &app.list.items[idx];
569                let checkbox = if item.done { "[x]" } else { "[ ]" };
570                let style = if item.done {
571                    Style::default()
572                        .fg(Color::DarkGray)
573                        .add_modifier(Modifier::CROSSED_OUT)
574                } else {
575                    Style::default().fg(Color::White)
576                };
577
578                let mut spans = vec![
579                    Span::styled(
580                        format!(" {} ", checkbox),
581                        if item.done {
582                            Style::default().fg(Color::Green)
583                        } else {
584                            Style::default().fg(Color::Yellow)
585                        },
586                    ),
587                    Span::styled(&item.content, style),
588                ];
589
590                // 显示创建时间(缩短格式)
591                if let Some(short_date) = item.created_at.get(..10) {
592                    spans.push(Span::styled(
593                        format!("  ({})", short_date),
594                        Style::default().fg(Color::DarkGray),
595                    ));
596                }
597
598                ListItem::new(Line::from(spans))
599            })
600            .collect();
601
602        let list_block = Block::default()
603            .borders(Borders::ALL)
604            .border_style(Style::default().fg(Color::White))
605            .title(" 待办列表 ");
606
607        if items.is_empty() {
608            // 空列表提示
609            let empty_hint = List::new(vec![ListItem::new(Line::from(Span::styled(
610                "   (空) 按 a 添加新待办...",
611                Style::default().fg(Color::DarkGray),
612            )))])
613            .block(list_block);
614            f.render_widget(empty_hint, chunks[1]);
615        } else {
616            let list_widget = List::new(items)
617                .block(list_block)
618                .highlight_style(
619                    Style::default()
620                        .bg(Color::DarkGray)
621                        .add_modifier(Modifier::BOLD),
622                )
623                .highlight_symbol(" ▶ ");
624            f.render_stateful_widget(list_widget, chunks[1], &mut app.state);
625        };
626    }
627
628    // ========== 状态/输入栏 ==========
629    match &app.mode {
630        AppMode::Adding => {
631            let (before, cursor_ch, after) = split_input_at_cursor(&app.input, app.cursor_pos);
632            let input_widget = Paragraph::new(Line::from(vec![
633                Span::styled(" 新待办: ", Style::default().fg(Color::Green)),
634                Span::raw(before),
635                Span::styled(
636                    cursor_ch,
637                    Style::default().fg(Color::Black).bg(Color::White),
638                ),
639                Span::raw(after),
640            ]))
641            .block(
642                Block::default()
643                    .borders(Borders::ALL)
644                    .border_style(Style::default().fg(Color::Green))
645                    .title(" 添加模式 (Enter 确认 / Esc 取消 / ←→ 移动光标) "),
646            );
647            f.render_widget(input_widget, chunks[2]);
648        }
649        AppMode::Editing => {
650            let (before, cursor_ch, after) = split_input_at_cursor(&app.input, app.cursor_pos);
651            let input_widget = Paragraph::new(Line::from(vec![
652                Span::styled(" 编辑: ", Style::default().fg(Color::Yellow)),
653                Span::raw(before),
654                Span::styled(
655                    cursor_ch,
656                    Style::default().fg(Color::Black).bg(Color::White),
657                ),
658                Span::raw(after),
659            ]))
660            .block(
661                Block::default()
662                    .borders(Borders::ALL)
663                    .border_style(Style::default().fg(Color::Yellow))
664                    .title(" 编辑模式 (Enter 确认 / Esc 取消 / ←→ 移动光标) "),
665            );
666            f.render_widget(input_widget, chunks[2]);
667        }
668        AppMode::ConfirmDelete => {
669            let msg = if let Some(real_idx) = app.selected_real_index() {
670                format!(
671                    " 确认删除「{}」?(y 确认 / n 取消)",
672                    app.list.items[real_idx].content
673                )
674            } else {
675                " 没有选中的项目".to_string()
676            };
677            let confirm_widget = Paragraph::new(Line::from(Span::styled(
678                msg,
679                Style::default().fg(Color::Red),
680            )))
681            .block(
682                Block::default()
683                    .borders(Borders::ALL)
684                    .border_style(Style::default().fg(Color::Red))
685                    .title(" ⚠️ 确认删除 "),
686            );
687            f.render_widget(confirm_widget, chunks[2]);
688        }
689        AppMode::Normal | AppMode::Help => {
690            let msg = app.message.as_deref().unwrap_or("按 ? 查看完整帮助");
691            let dirty_indicator = if app.is_dirty() { " [未保存]" } else { "" };
692            let status_widget = Paragraph::new(Line::from(vec![
693                Span::styled(msg, Style::default().fg(Color::Gray)),
694                Span::styled(
695                    dirty_indicator,
696                    Style::default().fg(Color::Red).add_modifier(Modifier::BOLD),
697                ),
698            ]))
699            .block(
700                Block::default()
701                    .borders(Borders::ALL)
702                    .border_style(Style::default().fg(Color::DarkGray)),
703            );
704            f.render_widget(status_widget, chunks[2]);
705        }
706    }
707
708    // ========== 帮助栏 ==========
709    let help_text = match app.mode {
710        AppMode::Normal => {
711            " n/↓ 下移 | N/↑ 上移 | 空格/回车 切换完成 | a 添加 | e 编辑 | d 删除 | y 复制 | f 过滤 | s 保存 | ? 帮助 | q 退出"
712        }
713        AppMode::Adding | AppMode::Editing => {
714            " Enter 确认 | Esc 取消 | ←→ 移动光标 | Home/End 行首尾"
715        }
716        AppMode::ConfirmDelete => " y 确认删除 | n/Esc 取消",
717        AppMode::Help => " 按任意键返回",
718    };
719    let help_widget = Paragraph::new(Line::from(Span::styled(
720        help_text,
721        Style::default().fg(Color::DarkGray),
722    )));
723    f.render_widget(help_widget, chunks[3]);
724}
725
726/// 正常模式按键处理,返回 true 表示退出
727fn handle_normal_mode(app: &mut TodoApp, key: KeyEvent) -> bool {
728    // Ctrl+C 强制退出(不保存)
729    if key.modifiers.contains(KeyModifiers::CONTROL) && key.code == KeyCode::Char('c') {
730        return true;
731    }
732
733    match key.code {
734        // 退出:有未保存修改时拒绝,提示用 q! 或先保存
735        KeyCode::Char('q') => {
736            if app.is_dirty() {
737                app.message = Some(
738                    "⚠️ 有未保存的修改!请先 s 保存,或输入 q! 强制退出(丢弃修改)".to_string(),
739                );
740                app.quit_input = "q".to_string();
741                return false;
742            }
743            return true;
744        }
745        KeyCode::Esc => {
746            if app.is_dirty() {
747                app.message = Some(
748                    "⚠️ 有未保存的修改!请先 s 保存,或输入 q! 强制退出(丢弃修改)".to_string(),
749                );
750                return false;
751            }
752            return true;
753        }
754
755        // q! 强制退出(丢弃修改):通过 ! 键判断前一个输入是否为 q
756        KeyCode::Char('!') => {
757            if app.quit_input == "q" {
758                return true; // q! 强制退出
759            }
760            app.quit_input.clear();
761        }
762
763        // 向下移动
764        KeyCode::Char('n') | KeyCode::Down | KeyCode::Char('j') => app.move_down(),
765
766        // 向上移动
767        KeyCode::Char('N') | KeyCode::Up | KeyCode::Char('k') => app.move_up(),
768
769        // 切换完成状态
770        KeyCode::Char(' ') | KeyCode::Enter => app.toggle_done(),
771
772        // 添加
773        KeyCode::Char('a') => {
774            app.mode = AppMode::Adding;
775            app.input.clear();
776            app.cursor_pos = 0;
777            app.message = None;
778        }
779
780        // 编辑
781        KeyCode::Char('e') => {
782            if let Some(real_idx) = app.selected_real_index() {
783                app.input = app.list.items[real_idx].content.clone();
784                app.cursor_pos = app.input.chars().count();
785                app.edit_index = Some(real_idx);
786                app.mode = AppMode::Editing;
787                app.message = None;
788            }
789        }
790
791        // 复制选中待办到剪切板
792        KeyCode::Char('y') => {
793            if let Some(real_idx) = app.selected_real_index() {
794                let content = app.list.items[real_idx].content.clone();
795                if copy_to_clipboard(&content) {
796                    app.message = Some(format!("📋 已复制到剪切板: {}", content));
797                } else {
798                    app.message = Some("❌ 复制到剪切板失败".to_string());
799                }
800            }
801        }
802
803        // 删除(需确认)
804        KeyCode::Char('d') => {
805            if app.selected_real_index().is_some() {
806                app.mode = AppMode::ConfirmDelete;
807            }
808        }
809
810        // 过滤切换
811        KeyCode::Char('f') => app.toggle_filter(),
812
813        // 保存
814        KeyCode::Char('s') => app.save(),
815
816        // 调整顺序: Shift+↑ 上移 / Shift+↓ 下移
817        KeyCode::Char('K') => app.move_item_up(),
818        KeyCode::Char('J') => app.move_item_down(),
819
820        // 查看帮助
821        KeyCode::Char('?') => {
822            app.mode = AppMode::Help;
823        }
824
825        _ => {}
826    }
827
828    // 非 q 键时清空 quit_input 缓冲
829    if key.code != KeyCode::Char('q') && key.code != KeyCode::Char('!') {
830        app.quit_input.clear();
831    }
832
833    false
834}
835
836/// 输入模式按键处理(添加/编辑通用,支持光标移动和行内编辑)
837fn handle_input_mode(app: &mut TodoApp, key: KeyEvent) {
838    let char_count = app.input.chars().count();
839
840    match key.code {
841        KeyCode::Enter => {
842            if app.mode == AppMode::Adding {
843                app.add_item();
844            } else {
845                app.confirm_edit();
846            }
847        }
848        KeyCode::Esc => {
849            app.mode = AppMode::Normal;
850            app.input.clear();
851            app.cursor_pos = 0;
852            app.edit_index = None;
853            app.message = Some("已取消".to_string());
854        }
855        KeyCode::Left => {
856            if app.cursor_pos > 0 {
857                app.cursor_pos -= 1;
858            }
859        }
860        KeyCode::Right => {
861            if app.cursor_pos < char_count {
862                app.cursor_pos += 1;
863            }
864        }
865        KeyCode::Home => {
866            app.cursor_pos = 0;
867        }
868        KeyCode::End => {
869            app.cursor_pos = char_count;
870        }
871        KeyCode::Backspace => {
872            if app.cursor_pos > 0 {
873                // 找到第 cursor_pos-1 和 cursor_pos 个字符的字节偏移,删除该范围
874                let start = app
875                    .input
876                    .char_indices()
877                    .nth(app.cursor_pos - 1)
878                    .map(|(i, _)| i)
879                    .unwrap_or(0);
880                let end = app
881                    .input
882                    .char_indices()
883                    .nth(app.cursor_pos)
884                    .map(|(i, _)| i)
885                    .unwrap_or(app.input.len());
886                app.input.drain(start..end);
887                app.cursor_pos -= 1;
888            }
889        }
890        KeyCode::Delete => {
891            if app.cursor_pos < char_count {
892                let start = app
893                    .input
894                    .char_indices()
895                    .nth(app.cursor_pos)
896                    .map(|(i, _)| i)
897                    .unwrap_or(app.input.len());
898                let end = app
899                    .input
900                    .char_indices()
901                    .nth(app.cursor_pos + 1)
902                    .map(|(i, _)| i)
903                    .unwrap_or(app.input.len());
904                app.input.drain(start..end);
905            }
906        }
907        KeyCode::Char(c) => {
908            // 在光标位置插入字符(支持多字节字符)
909            let byte_idx = app
910                .input
911                .char_indices()
912                .nth(app.cursor_pos)
913                .map(|(i, _)| i)
914                .unwrap_or(app.input.len());
915            app.input.insert_str(byte_idx, &c.to_string());
916            app.cursor_pos += 1;
917        }
918        _ => {}
919    }
920}
921
922/// 确认删除按键处理
923fn handle_confirm_delete(app: &mut TodoApp, key: KeyEvent) {
924    match key.code {
925        KeyCode::Char('y') | KeyCode::Char('Y') => {
926            app.delete_selected();
927        }
928        KeyCode::Char('n') | KeyCode::Char('N') | KeyCode::Esc => {
929            app.mode = AppMode::Normal;
930            app.message = Some("已取消删除".to_string());
931        }
932        _ => {}
933    }
934}
935
936/// 帮助模式按键处理(按任意键返回)
937fn handle_help_mode(app: &mut TodoApp, _key: KeyEvent) {
938    app.mode = AppMode::Normal;
939    app.message = None;
940}
941
942/// 将输入字符串按光标位置分割为三部分:光标前、光标处字符、光标后
943fn split_input_at_cursor(input: &str, cursor_pos: usize) -> (String, String, String) {
944    let chars: Vec<char> = input.chars().collect();
945    let before: String = chars[..cursor_pos].iter().collect();
946    let cursor_ch = if cursor_pos < chars.len() {
947        chars[cursor_pos].to_string()
948    } else {
949        " ".to_string() // 光标在末尾时显示空格块
950    };
951    let after: String = if cursor_pos < chars.len() {
952        chars[cursor_pos + 1..].iter().collect()
953    } else {
954        String::new()
955    };
956    (before, cursor_ch, after)
957}
958
959/// 复制内容到系统剪切板(macOS 使用 pbcopy,Linux 使用 xclip)
960fn copy_to_clipboard(content: &str) -> bool {
961    use std::io::Write;
962    use std::process::{Command, Stdio};
963
964    // 根据平台选择剪切板命令
965    let (cmd, args): (&str, Vec<&str>) = if cfg!(target_os = "macos") {
966        ("pbcopy", vec![])
967    } else if cfg!(target_os = "linux") {
968        // 优先尝试 xclip,其次 xsel
969        if Command::new("which")
970            .arg("xclip")
971            .output()
972            .map(|o| o.status.success())
973            .unwrap_or(false)
974        {
975            ("xclip", vec!["-selection", "clipboard"])
976        } else {
977            ("xsel", vec!["--clipboard", "--input"])
978        }
979    } else {
980        return false; // 不支持的平台
981    };
982
983    let child = Command::new(cmd).args(&args).stdin(Stdio::piped()).spawn();
984
985    match child {
986        Ok(mut child) => {
987            if let Some(ref mut stdin) = child.stdin {
988                let _ = stdin.write_all(content.as_bytes());
989            }
990            child.wait().map(|s| s.success()).unwrap_or(false)
991        }
992        Err(_) => false,
993    }
994}