1use crossterm::{
2 cursor,
3 event::{self, Event, KeyCode, KeyEvent, KeyModifiers},
4 execute,
5 style::{Color, Print, ResetColor, SetBackgroundColor, SetForegroundColor},
6 terminal::{self, ClearType, EnterAlternateScreen, LeaveAlternateScreen},
7};
8use snipt_core::{add_snippet, Result, SniptError};
9use std::sync::atomic::{AtomicBool, Ordering};
10use std::time::{Duration, Instant};
11use std::{
12 io::{self, stdout, Write},
13 thread,
14};
15
16const MAX_LINES: usize = 10000;
18const MAX_LINE_LENGTH: usize = 5000;
19const MAX_SHORTCUT_LENGTH: usize = 50;
20
21#[derive(PartialEq, Copy, Clone)]
22enum EditorMode {
23 Normal,
24 Insert,
25 Paste, }
27
28pub enum AddResult {
29 Added,
30 Cancelled,
31 Error(SniptError),
32}
33
34pub fn interactive_add() -> AddResult {
35 if let Err(e) = terminal::enable_raw_mode() {
37 return AddResult::Error(SniptError::Other(format!(
38 "Failed to enable raw mode: {}",
39 e
40 )));
41 }
42
43 let mut stdout = stdout();
44 if let Err(e) = execute!(stdout, EnterAlternateScreen) {
45 terminal::disable_raw_mode().ok();
46 return AddResult::Error(SniptError::Other(format!(
47 "Failed to enter alternate screen: {}",
48 e
49 )));
50 }
51
52 let result = run_interactive_ui(&mut stdout);
54
55 let _ = execute!(stdout, LeaveAlternateScreen);
57 let _ = terminal::disable_raw_mode();
58
59 match result {
60 Ok(true) => AddResult::Added,
61 Ok(false) => AddResult::Cancelled,
62 Err(e) => AddResult::Error(e),
63 }
64}
65
66fn run_interactive_ui(stdout: &mut io::Stdout) -> Result<bool> {
67 let mut shortcut = String::new();
68 let mut snippet = Vec::new();
69 snippet.push(String::new());
70 let mut current_field = 0; let mut cursor_pos = 0;
72 let mut current_line = 0;
73 let mut editor_mode = EditorMode::Insert;
74 let mut error_message = None;
75 let mut snippet_added = false;
76
77 let mut paste_buffer = String::new();
79
80 let mut last_render = Instant::now();
82 let mut force_render = true;
83
84 if let Err(e) = draw_ui(
86 stdout,
87 &shortcut,
88 &snippet,
89 current_field,
90 cursor_pos,
91 current_line,
92 editor_mode,
93 error_message.as_deref(),
94 ) {
95 error_message = Some(format!("UI Error: {}. Using minimal mode.", e));
96 }
97
98 const RENDER_THRESHOLD: Duration = Duration::from_millis(16); loop {
102 let now = Instant::now();
104 if force_render || now.duration_since(last_render) >= RENDER_THRESHOLD {
105 if let Err(e) = draw_ui(
106 stdout,
107 &shortcut,
108 &snippet,
109 current_field,
110 cursor_pos,
111 current_line,
112 editor_mode,
113 error_message.as_deref(),
114 ) {
115 error_message = Some(format!("UI Error: {}. Using minimal mode.", e));
117
118 if execute!(
119 stdout,
120 terminal::Clear(ClearType::All),
121 cursor::MoveTo(0, 0),
122 SetForegroundColor(Color::Red),
123 Print(&error_message.clone().unwrap_or_default()),
124 ResetColor,
125 cursor::MoveTo(0, 2),
126 SetForegroundColor(Color::White),
127 Print(format!("Shortcut: {}\n", shortcut)),
128 Print(format!(
129 "Editing {} lines, current: {}\n",
130 snippet.len(),
131 current_line + 1
132 )),
133 Print("Press Ctrl+W to save or Esc to cancel\n"),
134 ResetColor
135 )
136 .is_err()
137 {
138 return Err(SniptError::Other(
140 "Terminal display error. Try in a larger terminal.".to_string(),
141 ));
142 }
143 } else {
144 error_message = None;
145 }
146 last_render = now;
148 force_render = false;
149 }
150
151 if crossterm::event::poll(Duration::from_millis(1))? {
153 match event::read() {
154 Ok(Event::Key(KeyEvent {
155 code, modifiers, ..
156 })) => {
157 let mut state_changed = false;
159
160 if editor_mode == EditorMode::Paste {
162 match code {
163 KeyCode::Esc => {
164 paste_buffer.clear();
166 return Ok(false);
167 }
168 KeyCode::Enter => {
169 if !paste_buffer.is_empty() {
171 process_paste_buffer(
172 &mut snippet,
173 &mut current_line,
174 &mut cursor_pos,
175 &paste_buffer,
176 );
177 paste_buffer.clear();
178 }
179 editor_mode = EditorMode::Insert;
180 state_changed = true;
181 }
182 KeyCode::Char(c) => {
183 paste_buffer.push(c);
185 }
187 _ => {}
188 }
189 if state_changed {
190 force_render = true;
191 }
192 continue;
193 }
194
195 match code {
197 KeyCode::Tab | KeyCode::Down => {
198 if current_field == 0 {
199 current_field = 1;
200 current_line = 0;
201 cursor_pos = snippet[current_line].len();
202 state_changed = true;
203 } else if modifiers.contains(KeyModifiers::SHIFT) {
204 current_field = 0;
205 cursor_pos = shortcut.len();
206 state_changed = true;
207 } else if current_line < snippet.len() - 1 {
208 current_line += 1;
209 cursor_pos = snippet[current_line].len().min(cursor_pos);
210 state_changed = true;
211 }
212 }
213 KeyCode::BackTab | KeyCode::Up => {
214 if current_field == 1 {
215 if current_line > 0 {
216 current_line -= 1;
217 cursor_pos = snippet[current_line].len().min(cursor_pos);
218 state_changed = true;
219 } else {
220 current_field = 0;
221 cursor_pos = shortcut.len();
222 state_changed = true;
223 }
224 }
225 }
226 KeyCode::Esc => {
227 if shortcut.is_empty() && (snippet.len() == 1 && snippet[0].is_empty())
229 {
230 return Ok(false);
231 }
232
233 if editor_mode == EditorMode::Normal {
234 return Ok(snippet_added);
235 } else {
236 editor_mode = EditorMode::Normal;
237 state_changed = true;
238 }
239 }
240 KeyCode::Char('v') if modifiers.contains(KeyModifiers::CONTROL) => {
241 editor_mode = EditorMode::Paste;
243 paste_buffer.clear();
244 state_changed = true;
245 }
246
247 _ => {
248 if current_field == 0 {
249 if handle_shortcut_input(
251 &mut shortcut,
252 &mut cursor_pos,
253 code,
254 modifiers,
255 )? {
256 state_changed = true;
257 }
258 if code == KeyCode::Enter {
259 current_field = 1;
260 current_line = 0;
261 cursor_pos = snippet[current_line].len();
262 state_changed = true;
263 }
264 } else {
265 match editor_mode {
267 EditorMode::Normal => {
268 if handle_normal_mode(
269 &mut snippet,
270 &mut cursor_pos,
271 &mut current_line,
272 &mut editor_mode,
273 code,
274 modifiers,
275 stdout,
276 &shortcut,
277 &mut snippet_added,
278 )? {
279 state_changed = true;
280 }
281 }
282 EditorMode::Insert => {
283 if handle_insert_mode(
284 &mut snippet,
285 &mut cursor_pos,
286 &mut current_line,
287 &mut editor_mode,
288 code,
289 modifiers,
290 stdout,
291 &shortcut,
292 &mut snippet_added,
293 )? {
294 state_changed = true;
295 }
296 }
297 EditorMode::Paste => { }
298 }
299 }
300 }
301 }
302
303 if state_changed {
305 force_render = true;
306 }
307 }
308 Err(e) => {
309 error_message = Some(format!("Input error: {}. Press any key to continue.", e));
310 thread_sleep(1000);
311 force_render = true;
312 }
313 _ => {}
314 }
315 } else {
316 thread::sleep(Duration::from_millis(1));
318 }
319
320 if snippet_added {
321 return Ok(true);
322 }
323 }
324}
325
326fn process_paste_buffer(
328 snippet: &mut Vec<String>,
329 current_line: &mut usize,
330 cursor_pos: &mut usize,
331 buffer: &str,
332) {
333 let lines: Vec<&str> = buffer.split('\n').collect();
335
336 if lines.is_empty() {
337 return;
338 }
339
340 let current = snippet[*current_line].clone();
342
343 let before = ¤t[..(*cursor_pos).min(current.len())];
345 let after = ¤t[(*cursor_pos).min(current.len())..];
346
347 snippet[*current_line] = format!("{}{}", before, lines[0]);
349 *cursor_pos = before.len() + lines[0].len();
350
351 for (i, &line) in lines.iter().enumerate().skip(1) {
353 if snippet.len() >= MAX_LINES {
354 break;
355 }
356
357 let insertion_index = *current_line + i;
358
359 if i == lines.len() - 1 {
361 let combined_line = format!("{}{}", line, after);
362 if insertion_index < snippet.len() {
363 snippet.insert(insertion_index, combined_line);
364 } else {
365 snippet.push(combined_line);
366 }
367 } else if insertion_index < snippet.len() {
368 snippet.insert(insertion_index, line.to_string());
369 } else {
370 snippet.push(line.to_string());
371 }
372 }
373
374 if lines.len() > 1 {
376 *current_line += lines.len() - 1;
377 }
378}
379
380fn handle_shortcut_input(
382 shortcut: &mut String,
383 cursor_pos: &mut usize,
384 code: KeyCode,
385 _modifiers: KeyModifiers,
386) -> Result<bool> {
387 let mut state_changed = false;
388 match code {
389 KeyCode::Enter => {
390 return Ok(true);
392 }
393 KeyCode::Backspace => {
394 if *cursor_pos > 0 {
395 let mut chars: Vec<char> = shortcut.chars().collect();
397 let cursor_char_pos = (*cursor_pos).min(chars.len());
398
399 if cursor_char_pos > 0 {
400 chars.remove(cursor_char_pos - 1);
402 *shortcut = chars.into_iter().collect();
403 *cursor_pos -= 1;
404 state_changed = true;
405 }
406 }
407 }
408 KeyCode::Delete => {
409 let mut chars: Vec<char> = shortcut.chars().collect();
411 let cursor_char_pos = (*cursor_pos).min(chars.len());
412
413 if cursor_char_pos < chars.len() {
414 chars.remove(cursor_char_pos);
416 *shortcut = chars.into_iter().collect();
417 state_changed = true;
418 }
419 }
420 KeyCode::Left => {
421 if *cursor_pos > 0 {
422 *cursor_pos -= 1;
423 state_changed = true;
424 }
425 }
426 KeyCode::Right => {
427 let char_count = shortcut.chars().count();
428 if *cursor_pos < char_count {
429 *cursor_pos += 1;
430 state_changed = true;
431 }
432 }
433 KeyCode::Home => {
434 *cursor_pos = 0;
435 state_changed = true;
436 }
437 KeyCode::End => {
438 *cursor_pos = shortcut.chars().count(); state_changed = true;
440 }
441 KeyCode::Char(c) => {
442 if shortcut.len() < MAX_SHORTCUT_LENGTH {
443 let mut chars: Vec<char> = shortcut.chars().collect();
445 let cursor_char_pos = (*cursor_pos).min(chars.len());
446
447 chars.insert(cursor_char_pos, c);
449
450 *shortcut = chars.into_iter().collect();
452 *cursor_pos += 1;
453 state_changed = true;
454 }
455 }
456 _ => {}
457 }
458
459 Ok(state_changed)
460}
461
462#[allow(clippy::too_many_arguments)]
463fn handle_normal_mode(
464 snippet: &mut Vec<String>,
465 cursor_pos: &mut usize,
466 current_line: &mut usize,
467 editor_mode: &mut EditorMode,
468 code: KeyCode,
469 modifiers: KeyModifiers,
470 stdout: &mut io::Stdout,
471 shortcut: &str,
472 snippet_added: &mut bool,
473) -> Result<bool> {
474 let mut state_changed = false;
475 match code {
476 KeyCode::Char('i') => {
477 *editor_mode = EditorMode::Insert;
478 state_changed = true;
479 }
480 KeyCode::Char('a') => {
481 if *cursor_pos < snippet[*current_line].len() {
482 *cursor_pos = find_next_char_boundary(&snippet[*current_line], *cursor_pos)
483 .unwrap_or(*cursor_pos + 1)
484 .min(snippet[*current_line].len());
485 }
486 *editor_mode = EditorMode::Insert;
487 state_changed = true;
488 }
489 KeyCode::Char('A') => {
490 *cursor_pos = snippet[*current_line].len();
491 *editor_mode = EditorMode::Insert;
492 state_changed = true;
493 }
494 KeyCode::Char('o') => {
495 if snippet.len() < MAX_LINES {
496 snippet.insert(*current_line + 1, String::new());
497 *current_line += 1;
498 *cursor_pos = 0;
499 *editor_mode = EditorMode::Insert;
500 state_changed = true;
501 }
502 }
503 KeyCode::Char('O') => {
504 if snippet.len() < MAX_LINES {
505 snippet.insert(*current_line, String::new());
506 *cursor_pos = 0;
507 *editor_mode = EditorMode::Insert;
508 state_changed = true;
509 }
510 }
511 KeyCode::Char('h') => {
512 if *cursor_pos > 0 {
513 *cursor_pos = find_prev_char_boundary(&snippet[*current_line], *cursor_pos)
514 .unwrap_or(*cursor_pos - 1)
515 .min(*cursor_pos);
516 state_changed = true;
517 } else if *current_line > 0 {
518 *current_line -= 1;
519 *cursor_pos = snippet[*current_line].len();
520 state_changed = true;
521 }
522 }
523 KeyCode::Char('l') => {
524 if *cursor_pos < snippet[*current_line].len() {
525 *cursor_pos = find_next_char_boundary(&snippet[*current_line], *cursor_pos)
526 .unwrap_or(*cursor_pos + 1)
527 .min(snippet[*current_line].len());
528 state_changed = true;
529 } else if *current_line < snippet.len() - 1 {
530 *current_line += 1;
531 *cursor_pos = 0;
532 state_changed = true;
533 }
534 }
535 KeyCode::Char('j') => {
536 if *current_line < snippet.len() - 1 {
537 *current_line += 1;
538 *cursor_pos = (*cursor_pos).min(snippet[*current_line].len());
539 state_changed = true;
540 }
541 }
542 KeyCode::Char('k') => {
543 if *current_line > 0 {
544 *current_line -= 1;
545 *cursor_pos = (*cursor_pos).min(snippet[*current_line].len());
546 state_changed = true;
547 }
548 }
549 KeyCode::Char('0') => {
550 *cursor_pos = 0;
551 state_changed = true;
552 }
553 KeyCode::Char('$') => {
554 *cursor_pos = snippet[*current_line].len();
555 state_changed = true;
556 }
557 KeyCode::Char('d') if modifiers.contains(KeyModifiers::CONTROL) => {
558 if snippet.len() > 1 {
559 snippet.remove(*current_line);
560 if *current_line >= snippet.len() {
561 *current_line = snippet.len() - 1;
562 }
563 *cursor_pos = (*cursor_pos).min(snippet[*current_line].len());
564 state_changed = true;
565 } else {
566 snippet[0].clear();
567 *cursor_pos = 0;
568 state_changed = true;
569 }
570 }
571 KeyCode::Enter => {
572 if let Ok(added) = submit_snippet(stdout, shortcut, snippet) {
573 *snippet_added = added;
574 }
575 return Ok(true);
576 }
577 _ => {}
578 }
579
580 Ok(state_changed)
581}
582
583#[allow(clippy::too_many_arguments)]
585fn handle_insert_mode(
586 snippet: &mut Vec<String>,
587 cursor_pos: &mut usize,
588 current_line: &mut usize,
589 editor_mode: &mut EditorMode,
590 code: KeyCode,
591 modifiers: KeyModifiers,
592 stdout: &mut io::Stdout,
593 shortcut: &str,
594 snippet_added: &mut bool,
595) -> Result<bool> {
596 let mut state_changed = false;
597 match code {
598 KeyCode::Esc => {
599 if shortcut.is_empty() && (snippet.len() == 1 && snippet[0].is_empty()) {
601 *snippet_added = false;
602 return Ok(true);
603 }
604 *editor_mode = EditorMode::Normal;
605 state_changed = true;
606 }
607 KeyCode::Enter => {
608 if snippet.len() < MAX_LINES {
609 let current = &snippet[*current_line];
611
612 let chars: Vec<char> = current.chars().collect();
614 let cursor_char_pos = (*cursor_pos).min(chars.len());
615
616 let before: String = chars[..cursor_char_pos].iter().collect();
618 let after: String = chars[cursor_char_pos..].iter().collect();
619
620 snippet[*current_line] = before;
621 snippet.insert(*current_line + 1, after);
622 *current_line += 1;
623 *cursor_pos = 0;
624 state_changed = true;
625 }
626 }
627 KeyCode::Char('w') if modifiers.contains(KeyModifiers::CONTROL) => {
628 if let Ok(added) = submit_snippet(stdout, shortcut, snippet) {
629 *snippet_added = added;
630 }
631 return Ok(true);
632 }
633 KeyCode::Backspace => {
634 if *cursor_pos > 0 {
635 let mut chars: Vec<char> = snippet[*current_line].chars().collect();
637 let cursor_char_pos = (*cursor_pos).min(chars.len());
638
639 if cursor_char_pos > 0 {
640 chars.remove(cursor_char_pos - 1);
642 snippet[*current_line] = chars.into_iter().collect();
643 *cursor_pos -= 1;
644 state_changed = true;
645 }
646 } else if *current_line > 0 {
647 let content = snippet.remove(*current_line);
649 *current_line -= 1;
650 *cursor_pos = snippet[*current_line].chars().count(); snippet[*current_line].push_str(&content);
652 state_changed = true;
653 }
654 }
655 KeyCode::Delete => {
656 let mut chars: Vec<char> = snippet[*current_line].chars().collect();
658 let cursor_char_pos = (*cursor_pos).min(chars.len());
659
660 if cursor_char_pos < chars.len() {
661 chars.remove(cursor_char_pos);
663 snippet[*current_line] = chars.into_iter().collect();
664 state_changed = true;
665 } else if *current_line < snippet.len() - 1 {
666 let next = snippet.remove(*current_line + 1);
668 snippet[*current_line].push_str(&next);
669 state_changed = true;
670 }
671 }
672 KeyCode::Left => {
673 if *cursor_pos > 0 {
674 *cursor_pos -= 1;
675 state_changed = true;
676 } else if *current_line > 0 {
677 *current_line -= 1;
679 *cursor_pos = snippet[*current_line].chars().count(); state_changed = true;
681 }
682 }
683 KeyCode::Right => {
684 let char_count = snippet[*current_line].chars().count();
685 if *cursor_pos < char_count {
686 *cursor_pos += 1;
687 state_changed = true;
688 } else if *current_line < snippet.len() - 1 {
689 *current_line += 1;
691 *cursor_pos = 0;
692 state_changed = true;
693 }
694 }
695 KeyCode::Up => {
696 if *current_line > 0 {
697 *current_line -= 1;
698 let char_count = snippet[*current_line].chars().count();
699 *cursor_pos = (*cursor_pos).min(char_count);
700 state_changed = true;
701 }
702 }
703 KeyCode::Down => {
704 if *current_line < snippet.len() - 1 {
705 *current_line += 1;
706 let char_count = snippet[*current_line].chars().count();
707 *cursor_pos = (*cursor_pos).min(char_count);
708 state_changed = true;
709 }
710 }
711 KeyCode::Home => {
712 *cursor_pos = 0;
713 state_changed = true;
714 }
715 KeyCode::End => {
716 *cursor_pos = snippet[*current_line].chars().count(); state_changed = true;
718 }
719 KeyCode::Tab => {
720 if snippet[*current_line].len() < MAX_LINE_LENGTH - 4 {
722 for _ in 0..4 {
723 let mut chars: Vec<char> = snippet[*current_line].chars().collect();
725 let cursor_char_pos = (*cursor_pos).min(chars.len());
726
727 chars.insert(cursor_char_pos, ' ');
729
730 snippet[*current_line] = chars.into_iter().collect();
732 *cursor_pos += 1;
733 state_changed = true;
734 }
735 }
736 }
737 KeyCode::Char(c) => {
738 if snippet[*current_line].len() < MAX_LINE_LENGTH {
739 let mut chars: Vec<char> = snippet[*current_line].chars().collect();
741 let cursor_char_pos = (*cursor_pos).min(chars.len());
742
743 chars.insert(cursor_char_pos, c);
745
746 snippet[*current_line] = chars.into_iter().collect();
748 *cursor_pos += 1;
749 state_changed = true;
750 }
751 }
752 _ => {}
753 }
754
755 Ok(state_changed)
756}
757
758fn submit_snippet(stdout: &mut io::Stdout, shortcut: &str, snippet: &[String]) -> Result<bool> {
760 if shortcut.is_empty() || snippet.is_empty() || snippet[0].is_empty() {
761 show_error_message(stdout, "Both fields must be filled")?;
762 thread_sleep(1500);
763 return Ok(false); }
765
766 let full_snippet = snippet.join("\n");
768 match add_snippet(shortcut.to_string(), full_snippet) {
769 Ok(_) => {
770 show_success_message(stdout)?;
771 Ok(true)
773 }
774 Err(SniptError::Other(msg)) if msg.contains("already exists") => {
775 show_error_message(stdout, &msg)?;
776 thread_sleep(1500);
777 Ok(false)
778 }
779 Err(e) => Err(e),
780 }
781}
782
783#[allow(clippy::too_many_arguments)]
784fn draw_ui(
785 stdout: &mut io::Stdout,
786 shortcut: &str,
787 snippet: &[String],
788 current_field: usize,
789 cursor_pos: usize,
790 current_line: usize,
791 editor_mode: EditorMode,
792 error_msg: Option<&str>,
793) -> Result<()> {
794 static FIRST_DRAW: AtomicBool = AtomicBool::new(true);
796
797 let (width, height) = match terminal::size() {
799 Ok((w, h)) => (w, h),
800 Err(e) => {
801 return Err(SniptError::Other(format!(
802 "Failed to get terminal size: {}",
803 e
804 )))
805 }
806 };
807
808 if width < 40 || height < 15 {
810 return Err(SniptError::Other(format!(
811 "Terminal too small. Minimum size: 40x15, current: {}x{}",
812 width, height
813 )));
814 }
815
816 let panel_width = width.saturating_sub(8).max(40);
818 let panel_height = height.saturating_sub(6).max(15);
819 let start_x = (width - panel_width) / 2; let start_y = (height - panel_height) / 2; if FIRST_DRAW
824 .compare_exchange(true, false, Ordering::SeqCst, Ordering::SeqCst)
825 .is_ok()
826 {
827 if let Err(e) = execute!(
828 stdout,
829 terminal::Clear(ClearType::All),
830 cursor::Hide ) {
832 return Err(SniptError::Other(format!("Failed to clear screen: {}", e)));
833 }
834 }
835
836 let title = match editor_mode {
838 EditorMode::Paste => " ✏️ Paste Mode - Enter to confirm ",
839 EditorMode::Normal => " ✏️ Add New Snippet - Normal Mode ",
840 EditorMode::Insert => " ✏️ Add New Snippet - Insert Mode ",
841 };
842
843 let title_x = start_x + (panel_width - title.len() as u16) / 2;
844
845 if let Err(e) = execute!(
847 stdout,
848 cursor::Hide,
849 cursor::MoveTo(title_x, start_y - 1),
850 SetForegroundColor(if editor_mode == EditorMode::Paste {
851 Color::Green
852 } else if editor_mode == EditorMode::Normal {
853 Color::Blue
854 } else {
855 Color::Cyan
856 }),
857 SetBackgroundColor(Color::Black),
858 Print(title),
859 ResetColor
860 ) {
861 return Err(SniptError::Other(format!("Failed to draw title: {}", e)));
862 }
863
864 if let Err(e) = execute!(
866 stdout,
867 cursor::MoveTo(start_x, start_y),
868 SetForegroundColor(Color::Cyan),
869 Print("╭"),
870 Print("─".repeat((panel_width - 2) as usize)),
871 Print("╮")
872 ) {
873 return Err(SniptError::Other(format!("Failed to draw box top: {}", e)));
874 }
875
876 for i in 1..panel_height - 1 {
878 if let Err(e) = execute!(
879 stdout,
880 cursor::MoveTo(start_x, start_y + i),
881 Print("│"),
882 cursor::MoveTo(start_x + panel_width - 1, start_y + i),
883 Print("│")
884 ) {
885 return Err(SniptError::Other(format!(
886 "Failed to draw box sides at row {}: {}",
887 i, e
888 )));
889 }
890 }
891
892 if let Err(e) = execute!(
894 stdout,
895 cursor::MoveTo(start_x, start_y + panel_height - 1),
896 Print("╰"),
897 Print("─".repeat((panel_width - 2) as usize)),
898 Print("╯"),
899 ResetColor
900 ) {
901 return Err(SniptError::Other(format!(
902 "Failed to draw box bottom: {}",
903 e
904 )));
905 }
906
907 if let Err(e) = execute!(
909 stdout,
910 cursor::MoveTo(start_x + 3, start_y + 1),
911 SetForegroundColor(Color::Magenta),
912 Print("snipt"),
913 SetForegroundColor(Color::DarkGrey),
914 Print(" - Text Expansion Tool"),
915 ResetColor
916 ) {
917 return Err(SniptError::Other(format!("Failed to draw header: {}", e)));
918 }
919
920 if let Err(e) = execute!(
922 stdout,
923 cursor::MoveTo(start_x + 1, start_y + 2),
924 SetForegroundColor(Color::DarkGrey),
925 Print("─".repeat((panel_width - 3) as usize)),
926 ResetColor
927 ) {
928 return Err(SniptError::Other(format!(
929 "Failed to draw separator: {}",
930 e
931 )));
932 }
933
934 let field_x = start_x + 3;
936 if let Err(e) = draw_field(
937 stdout,
938 field_x,
939 start_y + 4,
940 panel_width - 8,
941 "Shortcut:",
942 shortcut,
943 current_field == 0,
944 ) {
945 return Err(SniptError::Other(format!(
946 "Failed to draw shortcut field: {}",
947 e
948 )));
949 }
950
951 let field_x = start_x + 3;
953 if let Err(e) = draw_multiline_field(
954 stdout,
955 field_x,
956 start_y + 8,
957 panel_width - 6,
958 panel_height - 14, "Snippet:",
960 snippet,
961 current_field == 1,
962 current_line,
963 ) {
964 return Err(SniptError::Other(format!(
965 "Failed to draw snippet field: {}",
966 e
967 )));
968 }
969 let help_text = match editor_mode {
970 EditorMode::Normal => {
971 "i/a: Insert | o/O: New line | h/j/k/l: Navigate | Ctrl+d: Delete line | Enter: Submit"
972 }
973 EditorMode::Insert => {
974 if current_field == 0 {
975 "Tab: Next field | Enter: Next field | Esc: Cancel"
976 } else {
977 "Esc: Normal mode | Enter: New line | Arrows: Navigate | Ctrl+v: Paste | Ctrl+w: Submit"
978 }
979 }
980 EditorMode::Paste => "Enter: Confirm paste | Esc: Cancel | Type or paste text",
981 };
982
983 let buttons_line = match editor_mode {
985 EditorMode::Insert if current_field == 1 => {
986 "[ Ctrl+W: Save ] [ Tab: Indent ] [ Esc: Normal Mode ]"
987 }
988 EditorMode::Normal => "[ Enter: Submit ] [ i: Insert Mode ] [ Esc: Cancel ]",
989 _ => "[ Ctrl+W: Save ] [ Esc: Cancel ]",
990 };
991
992 let buttons_x = start_x + (panel_width - buttons_line.len() as u16) / 2;
994
995 if let Err(e) = execute!(
996 stdout,
997 cursor::MoveTo(buttons_x, start_y + panel_height - 3),
998 SetForegroundColor(Color::White),
999 SetBackgroundColor(Color::DarkBlue),
1000 Print(buttons_line),
1001 ResetColor
1002 ) {
1003 return Err(SniptError::Other(format!("Failed to draw buttons: {}", e)));
1004 }
1005
1006 let help_x = if help_text.len() as u16 <= panel_width - 4 {
1008 start_x + (panel_width - help_text.len() as u16) / 2
1009 } else {
1010 start_x + 2
1011 };
1012 if let Err(e) = execute!(
1013 stdout,
1014 cursor::MoveTo(help_x, start_y + panel_height - 2),
1015 SetForegroundColor(Color::DarkGrey),
1016 Print(if help_text.len() as u16 <= panel_width - 4 {
1017 help_text
1018 } else {
1019 &help_text[0..(panel_width - 7) as usize]
1020 }),
1021 ResetColor
1022 ) {
1023 return Err(SniptError::Other(format!(
1024 "Failed to draw help text: {}",
1025 e
1026 )));
1027 }
1028
1029 if current_field == 1 && editor_mode != EditorMode::Paste {
1031 let mode_text = match editor_mode {
1032 EditorMode::Normal => "-- NORMAL --",
1033 EditorMode::Insert => "-- INSERT --",
1034 EditorMode::Paste => "-- PASTE --",
1035 };
1036
1037 if let Err(e) = execute!(
1038 stdout,
1039 cursor::MoveTo(field_x, start_y + 7),
1040 SetForegroundColor(if matches!(editor_mode, EditorMode::Normal) {
1041 Color::Blue
1042 } else {
1043 Color::Green
1044 }),
1045 Print(mode_text),
1046 ResetColor
1047 ) {
1048 return Err(SniptError::Other(format!(
1049 "Failed to draw mode text: {}",
1050 e
1051 )));
1052 }
1053 }
1054
1055 if let Some(msg) = error_msg {
1057 let err_x = start_x + 2;
1058 let err_y = start_y + panel_height;
1059
1060 let display_msg = if msg.len() > (panel_width - 4) as usize {
1062 &msg[0..(panel_width - 7) as usize]
1063 } else {
1064 msg
1065 };
1066
1067 if let Err(e) = execute!(
1068 stdout,
1069 cursor::MoveTo(err_x, err_y),
1070 SetForegroundColor(Color::White),
1071 SetBackgroundColor(Color::Red),
1072 Print(format!(" {} ", display_msg)),
1073 ResetColor
1074 ) {
1075 eprintln!("Failed to show error: {}", e);
1077 }
1078 }
1079
1080 let cursor_result = if editor_mode == EditorMode::Paste {
1082 execute!(stdout, cursor::MoveTo(0, 1), cursor::Show)
1084 } else if current_field == 0 {
1085 let visible_cursor_pos = cursor_pos.min(panel_width as usize - 9) as u16;
1086 execute!(
1087 stdout,
1088 cursor::MoveTo(field_x + 1 + visible_cursor_pos, start_y + 5),
1089 cursor::Show
1090 )
1091 } else {
1092 let visible_area_height = (panel_height - 14) as usize;
1094 let scroll_offset = if current_line >= visible_area_height {
1095 current_line - visible_area_height + 1
1096 } else {
1097 0
1098 };
1099
1100 let visible_line_idx = current_line - scroll_offset;
1101 let visible_cursor_pos = cursor_pos.min(panel_width as usize - 9) as u16;
1102 execute!(
1103 stdout,
1104 cursor::MoveTo(
1105 field_x + 1 + visible_cursor_pos,
1106 start_y + 9 + visible_line_idx as u16
1107 ),
1108 cursor::Show
1109 )
1110 };
1111
1112 if let Err(e) = cursor_result {
1113 return Err(SniptError::Other(format!(
1114 "Failed to position cursor: {}",
1115 e
1116 )));
1117 }
1118
1119 if let Err(e) = stdout.flush() {
1121 return Err(SniptError::Other(format!("Failed to flush output: {}", e)));
1122 }
1123
1124 Ok(())
1125}
1126
1127fn draw_field(
1128 stdout: &mut io::Stdout,
1129 x: u16,
1130 y: u16,
1131 width: u16,
1132 label: &str,
1133 value: &str,
1134 active: bool,
1135) -> Result<()> {
1136 if let Err(e) = execute!(
1138 stdout,
1139 cursor::MoveTo(x, y),
1140 SetForegroundColor(Color::Yellow),
1141 Print(label),
1142 ResetColor
1143 ) {
1144 return Err(SniptError::Other(format!(
1145 "Failed to draw field label: {}",
1146 e
1147 )));
1148 }
1149
1150 if let Err(e) = execute!(
1152 stdout,
1153 cursor::MoveTo(x, y + 1),
1154 SetForegroundColor(Color::Blue),
1155 Print("┌"),
1156 Print("─".repeat((width - 2) as usize)),
1157 Print("┐"),
1158 ResetColor
1159 ) {
1160 return Err(SniptError::Other(format!(
1161 "Failed to draw field box top: {}",
1162 e
1163 )));
1164 }
1165
1166 let bg_color = if active {
1167 Color::DarkBlue
1168 } else {
1169 Color::Black
1170 };
1171 let fg_color = if active { Color::White } else { Color::Grey };
1172
1173 let visible_text = safe_truncate_string(value, width as usize - 4, true);
1175
1176 if let Err(e) = execute!(
1178 stdout,
1179 cursor::MoveTo(x, y + 2),
1180 SetForegroundColor(Color::Blue),
1181 Print("│"),
1182 SetBackgroundColor(bg_color),
1183 SetForegroundColor(fg_color),
1184 Print(" "),
1185 Print(&visible_text),
1186 Print(" ".repeat((width as usize - 3 - visible_text.chars().count()).max(0))),
1187 ResetColor,
1188 SetForegroundColor(Color::Blue),
1189 Print("│"),
1190 ResetColor
1191 ) {
1192 return Err(SniptError::Other(format!(
1193 "Failed to draw field content: {}",
1194 e
1195 )));
1196 }
1197
1198 if let Err(e) = execute!(
1200 stdout,
1201 cursor::MoveTo(x, y + 3),
1202 SetForegroundColor(Color::Blue),
1203 Print("└"),
1204 Print("─".repeat((width - 2) as usize)),
1205 Print("┘"),
1206 ResetColor
1207 ) {
1208 return Err(SniptError::Other(format!(
1209 "Failed to draw field box bottom: {}",
1210 e
1211 )));
1212 }
1213
1214 Ok(())
1215}
1216
1217#[allow(clippy::too_many_arguments)]
1218fn draw_multiline_field(
1219 stdout: &mut io::Stdout,
1220 x: u16,
1221 y: u16,
1222 width: u16,
1223 height: u16,
1224 label: &str,
1225 lines: &[String],
1226 active: bool,
1227 current_line: usize,
1228) -> Result<()> {
1229 if let Err(e) = execute!(
1231 stdout,
1232 cursor::MoveTo(x, y),
1233 SetForegroundColor(Color::Yellow),
1234 Print(label),
1235 ResetColor
1236 ) {
1237 return Err(SniptError::Other(format!(
1238 "Failed to draw multiline field label: {}",
1239 e
1240 )));
1241 }
1242
1243 if let Err(e) = execute!(
1245 stdout,
1246 cursor::MoveTo(x, y + 1),
1247 SetForegroundColor(Color::Blue),
1248 Print("┌"),
1249 Print("─".repeat((width - 2) as usize)),
1250 Print("┐"),
1251 ResetColor
1252 ) {
1253 return Err(SniptError::Other(format!(
1254 "Failed to draw multiline field box top: {}",
1255 e
1256 )));
1257 }
1258
1259 let visible_area_height = height as usize;
1261 let scroll_offset = if current_line >= visible_area_height {
1262 current_line - visible_area_height + 1
1263 } else {
1264 0
1265 };
1266
1267 if lines.len() > 1 {
1269 let scroll_info = format!(" {}/{} ", current_line + 1, lines.len());
1270 let info_x = x + width - scroll_info.len() as u16 - 2;
1271
1272 if let Err(e) = execute!(
1273 stdout,
1274 cursor::MoveTo(info_x, y + 1),
1275 SetForegroundColor(Color::Yellow),
1276 Print(scroll_info),
1277 ResetColor
1278 ) {
1279 eprintln!("Failed to draw scroll info: {}", e);
1281 }
1282 }
1283
1284 let bg_color = if active {
1286 Color::DarkBlue
1287 } else {
1288 Color::Black
1289 };
1290 let fg_color = if active { Color::White } else { Color::Grey };
1291
1292 let max_visible_lines = height as usize;
1294 let end_line = (scroll_offset + max_visible_lines).min(lines.len());
1295
1296 for i in 0..(end_line - scroll_offset) {
1297 let line_idx = i + scroll_offset;
1298 let line = &lines[line_idx];
1299
1300 let visible_text = safe_truncate_string(line, width as usize - 4, true);
1302
1303 let is_current = line_idx == current_line && active;
1304 let line_bg = if is_current { bg_color } else { Color::Black };
1305 let line_fg = if is_current { Color::White } else { fg_color };
1306
1307 let padding_length = (width as usize - 3 - visible_text.chars().count()).max(0);
1309
1310 if let Err(e) = execute!(
1311 stdout,
1312 cursor::MoveTo(x, y + 2 + i as u16),
1313 SetForegroundColor(Color::Blue),
1314 Print("│"),
1315 SetBackgroundColor(line_bg),
1316 SetForegroundColor(line_fg),
1317 Print(" "),
1318 Print(&visible_text),
1319 Print(" ".repeat(padding_length)),
1320 ResetColor,
1321 SetForegroundColor(Color::Blue),
1322 Print("│"),
1323 ResetColor
1324 ) {
1325 return Err(SniptError::Other(format!(
1326 "Failed to draw line {} of multiline field: {}",
1327 i, e
1328 )));
1329 }
1330 }
1331
1332 for i in (end_line - scroll_offset)..max_visible_lines {
1334 if let Err(e) = execute!(
1335 stdout,
1336 cursor::MoveTo(x, y + 2 + i as u16),
1337 SetForegroundColor(Color::Blue),
1338 Print("│"),
1339 Print(" ".repeat((width - 2) as usize)),
1340 Print("│"),
1341 ResetColor
1342 ) {
1343 return Err(SniptError::Other(format!(
1344 "Failed to draw empty line {} of multiline field: {}",
1345 i, e
1346 )));
1347 }
1348 }
1349
1350 if let Err(e) = execute!(
1352 stdout,
1353 cursor::MoveTo(x, y + 2 + height),
1354 SetForegroundColor(Color::Blue),
1355 Print("└"),
1356 Print("─".repeat((width - 2) as usize)),
1357 Print("┘"),
1358 ResetColor
1359 ) {
1360 return Err(SniptError::Other(format!(
1361 "Failed to draw multiline field box bottom: {}",
1362 e
1363 )));
1364 }
1365
1366 Ok(())
1367}
1368
1369fn find_prev_char_boundary(s: &str, pos: usize) -> Option<usize> {
1371 if pos == 0 || pos > s.len() {
1372 return None;
1373 }
1374
1375 let mut idx = pos;
1377 while idx > 0 && !s.is_char_boundary(idx) {
1378 idx -= 1;
1379 }
1380
1381 if idx > 0 {
1383 let mut prev_idx = idx - 1;
1384 while prev_idx > 0 && !s.is_char_boundary(prev_idx) {
1385 prev_idx -= 1;
1386 }
1387 Some(prev_idx)
1388 } else {
1389 Some(0)
1390 }
1391}
1392
1393fn find_next_char_boundary(s: &str, pos: usize) -> Option<usize> {
1395 if pos >= s.len() {
1396 return None;
1397 }
1398
1399 let mut idx = pos + 1;
1401 while idx < s.len() && !s.is_char_boundary(idx) {
1402 idx += 1;
1403 }
1404
1405 if idx <= s.len() {
1406 Some(idx)
1407 } else {
1408 Some(s.len())
1409 }
1410}
1411
1412fn safe_truncate_string(s: &str, max_width: usize, add_ellipsis: bool) -> String {
1414 if s.is_empty() || max_width == 0 {
1415 return String::new();
1416 }
1417
1418 if s.chars().count() <= max_width {
1420 return s.to_string();
1421 }
1422
1423 let mut result = String::with_capacity(max_width + 3); let mut count = 0;
1426 let actual_max = if add_ellipsis {
1427 max_width - 3
1428 } else {
1429 max_width
1430 };
1431
1432 for c in s.chars() {
1433 if count >= actual_max {
1434 break;
1435 }
1436 result.push(c);
1437 count += 1;
1438 }
1439
1440 if add_ellipsis && count < s.chars().count() {
1441 result.push_str("...");
1442 }
1443
1444 result
1445}
1446
1447fn show_success_message(stdout: &mut io::Stdout) -> Result<()> {
1448 let (width, height) = terminal::size().unwrap_or((80, 24));
1450
1451 let message_lines = [
1453 "✓ Snippet added successfully!",
1454 "",
1455 "Your snippet is now ready to use.",
1456 "",
1457 "Press any key to view your snippets...",
1458 ];
1459
1460 let box_width = 50u16;
1462 let box_height = (message_lines.len() + 4) as u16;
1463 let x = (width.saturating_sub(box_width)) / 2;
1464 let y = (height.saturating_sub(box_height)) / 2;
1465
1466 if let Err(e) = execute!(stdout, terminal::Clear(ClearType::All)) {
1468 return Err(SniptError::Other(format!("Failed to clear screen: {}", e)));
1469 }
1470
1471 if let Err(e) = execute!(
1473 stdout,
1474 cursor::MoveTo(x, y),
1476 SetForegroundColor(Color::Green),
1477 Print("╭"),
1478 Print("─".repeat((box_width - 2) as usize)),
1479 Print("╮"),
1480 cursor::MoveTo(x + (box_width - 16) / 2, y),
1482 Print("╡ Success ╞"),
1483 ResetColor
1485 ) {
1486 return Err(SniptError::Other(format!("Failed to draw box top: {}", e)));
1487 }
1488
1489 for (i, line) in message_lines.iter().enumerate() {
1491 let line_y = y + i as u16 + 2; let text_x = if line.is_empty() {
1495 x + 2
1496 } else {
1497 x + (box_width - line.len() as u16) / 2
1498 };
1499
1500 let color = if i == 0 {
1501 Color::Green
1503 } else {
1504 Color::White
1505 };
1506
1507 if let Err(e) = execute!(
1508 stdout,
1509 cursor::MoveTo(x, line_y),
1511 SetForegroundColor(Color::Green),
1512 Print("│"),
1513 cursor::MoveTo(text_x, line_y),
1515 SetForegroundColor(color),
1516 Print(line),
1517 cursor::MoveTo(x + box_width - 1, line_y),
1519 SetForegroundColor(Color::Green),
1520 Print("│"),
1521 ResetColor
1522 ) {
1523 return Err(SniptError::Other(format!(
1524 "Failed to draw line {}: {}",
1525 i, e
1526 )));
1527 }
1528 }
1529
1530 if let Err(e) = execute!(
1532 stdout,
1533 cursor::MoveTo(x, y + box_height - 1),
1534 SetForegroundColor(Color::Green),
1535 Print("╰"),
1536 Print("─".repeat((box_width - 2) as usize)),
1537 Print("╯"),
1538 ResetColor
1539 ) {
1540 return Err(SniptError::Other(format!(
1541 "Failed to draw box bottom: {}",
1542 e
1543 )));
1544 }
1545
1546 if let Err(e) = stdout.flush() {
1547 return Err(SniptError::Other(format!("Failed to flush output: {}", e)));
1548 }
1549
1550 let exit_at = std::time::Instant::now() + Duration::from_millis(1000);
1552 while std::time::Instant::now() < exit_at {
1553 if crossterm::event::poll(Duration::from_millis(100))? {
1554 let _ = crossterm::event::read()?;
1555 break;
1556 }
1557 }
1558
1559 Ok(())
1560}
1561
1562fn show_error_message(stdout: &mut io::Stdout, message: &str) -> Result<()> {
1563 let (width, height) = terminal::size().unwrap_or((80, 24));
1565
1566 let display_msg = safe_truncate_string(message, width as usize - 10, true);
1568
1569 let x = (width.saturating_sub(display_msg.len() as u16)) / 2;
1570 let y = height - 3;
1571
1572 if let Err(e) = execute!(
1573 stdout,
1574 cursor::MoveTo(x, y),
1575 SetForegroundColor(Color::Red),
1576 Print("⚠ "),
1577 Print(display_msg),
1578 ResetColor
1579 ) {
1580 return Err(SniptError::Other(format!(
1581 "Failed to show error message: {}",
1582 e
1583 )));
1584 }
1585
1586 if let Err(e) = stdout.flush() {
1587 return Err(SniptError::Other(format!("Failed to flush output: {}", e)));
1588 }
1589
1590 Ok(())
1591}
1592
1593fn thread_sleep(ms: u64) {
1594 std::thread::sleep(std::time::Duration::from_millis(ms));
1595}