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#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
26pub struct TodoItem {
27 pub content: String,
29 pub done: bool,
31 pub created_at: String,
33 pub done_at: Option<String>,
35}
36
37#[derive(Debug, Clone, Serialize, Deserialize, Default, PartialEq)]
39pub struct TodoList {
40 pub items: Vec<TodoItem>,
41}
42
43fn todo_dir() -> PathBuf {
47 let dir = YamlConfig::data_dir().join("todo");
48 let _ = fs::create_dir_all(&dir);
49 dir
50}
51
52fn todo_file_path() -> PathBuf {
54 todo_dir().join("todo.json")
55}
56
57fn 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
77fn save_todo_list(list: &TodoList) -> bool {
79 let path = todo_file_path();
80 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
99pub fn handle_todo(content: &[String], _config: &YamlConfig) {
103 if content.is_empty() {
104 run_todo_tui();
106 return;
107 }
108
109 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 let undone = list.items.iter().filter(|i| !i.done).count();
130 info!("📋 当前未完成待办: {} 条", undone);
131 }
132}
133
134struct TodoApp {
138 list: TodoList,
140 snapshot: TodoList,
142 state: ListState,
144 mode: AppMode,
146 input: String,
148 edit_index: Option<usize>,
150 message: Option<String>,
152 filter: usize,
154 quit_input: String,
156 cursor_pos: usize,
158}
159
160#[derive(PartialEq)]
161enum AppMode {
162 Normal,
164 Adding,
166 Editing,
168 ConfirmDelete,
170 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 fn is_dirty(&self) -> bool {
198 self.list != self.snapshot
199 }
200
201 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 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 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 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 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 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 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 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 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 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 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 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 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 fn save(&mut self) {
380 if self.is_dirty() {
381 if save_todo_list(&self.list) {
382 self.snapshot = self.list.clone();
384 self.message = Some("💾 已保存".to_string());
385 }
386 } else {
387 self.message = Some("📋 无需保存,没有修改".to_string());
388 }
389 }
390}
391
392fn 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 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 terminal.draw(|f| draw_ui(f, &mut app))?;
416
417 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 terminal::disable_raw_mode()?;
439 execute!(terminal.backend_mut(), LeaveAlternateScreen)?;
440
441 Ok(())
442}
443
444fn draw_ui(f: &mut ratatui::Frame, app: &mut TodoApp) {
446 let size = f.area();
447
448 let chunks = Layout::default()
450 .direction(Direction::Vertical)
451 .constraints([
452 Constraint::Length(3), Constraint::Min(5), Constraint::Length(3), Constraint::Length(2), ])
457 .split(size);
458
459 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 if app.mode == AppMode::Help {
487 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 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 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 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 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
726fn handle_normal_mode(app: &mut TodoApp, key: KeyEvent) -> bool {
728 if key.modifiers.contains(KeyModifiers::CONTROL) && key.code == KeyCode::Char('c') {
730 return true;
731 }
732
733 match key.code {
734 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 KeyCode::Char('!') => {
757 if app.quit_input == "q" {
758 return true; }
760 app.quit_input.clear();
761 }
762
763 KeyCode::Char('n') | KeyCode::Down | KeyCode::Char('j') => app.move_down(),
765
766 KeyCode::Char('N') | KeyCode::Up | KeyCode::Char('k') => app.move_up(),
768
769 KeyCode::Char(' ') | KeyCode::Enter => app.toggle_done(),
771
772 KeyCode::Char('a') => {
774 app.mode = AppMode::Adding;
775 app.input.clear();
776 app.cursor_pos = 0;
777 app.message = None;
778 }
779
780 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 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 KeyCode::Char('d') => {
805 if app.selected_real_index().is_some() {
806 app.mode = AppMode::ConfirmDelete;
807 }
808 }
809
810 KeyCode::Char('f') => app.toggle_filter(),
812
813 KeyCode::Char('s') => app.save(),
815
816 KeyCode::Char('K') => app.move_item_up(),
818 KeyCode::Char('J') => app.move_item_down(),
819
820 KeyCode::Char('?') => {
822 app.mode = AppMode::Help;
823 }
824
825 _ => {}
826 }
827
828 if key.code != KeyCode::Char('q') && key.code != KeyCode::Char('!') {
830 app.quit_input.clear();
831 }
832
833 false
834}
835
836fn 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 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 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
922fn 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
936fn handle_help_mode(app: &mut TodoApp, _key: KeyEvent) {
938 app.mode = AppMode::Normal;
939 app.message = None;
940}
941
942fn 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() };
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
959fn copy_to_clipboard(content: &str) -> bool {
961 use std::io::Write;
962 use std::process::{Command, Stdio};
963
964 let (cmd, args): (&str, Vec<&str>) = if cfg!(target_os = "macos") {
966 ("pbcopy", vec![])
967 } else if cfg!(target_os = "linux") {
968 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; };
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}