Skip to main content

snipt_ui/
editor.rs

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
16// Constants
17const 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, // New mode for pasting large text
26}
27
28pub enum AddResult {
29    Added,
30    Cancelled,
31    Error(SniptError),
32}
33
34pub fn interactive_add() -> AddResult {
35    // Setup terminal with error handling
36    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    // Run the interactive UI
53    let result = run_interactive_ui(&mut stdout);
54
55    // Cleanup terminal
56    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; // 0 for shortcut, 1 for snippet
71    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    // For paste handling
78    let mut paste_buffer = String::new();
79
80    // For performance optimization
81    let mut last_render = Instant::now();
82    let mut force_render = true;
83
84    // Initial draw to set up the UI
85    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    // Much shorter rendering interval - decreased to prevent flickering
99    const RENDER_THRESHOLD: Duration = Duration::from_millis(16); // ~60fps (smoother)
100
101    loop {
102        // Limit rendering frequency for better performance
103        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                // Try minimal UI if main UI fails
116                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                    // If even the minimal UI fails, return a usable error
139                    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            // Update the render time and reset the force render flag
147            last_render = now;
148            force_render = false;
149        }
150
151        // Handle UI events with a very short timeout to remain responsive
152        if crossterm::event::poll(Duration::from_millis(1))? {
153            match event::read() {
154                Ok(Event::Key(KeyEvent {
155                    code, modifiers, ..
156                })) => {
157                    // Only force render when the state actually changes
158                    let mut state_changed = false;
159
160                    // Special handling for paste mode
161                    if editor_mode == EditorMode::Paste {
162                        match code {
163                            KeyCode::Esc => {
164                                // Cancel paste operation
165                                paste_buffer.clear();
166                                return Ok(false);
167                            }
168                            KeyCode::Enter => {
169                                // Process paste buffer
170                                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                                // Add to paste buffer
184                                paste_buffer.push(c);
185                                // Don't redraw for each character in paste mode
186                            }
187                            _ => {}
188                        }
189                        if state_changed {
190                            force_render = true;
191                        }
192                        continue;
193                    }
194
195                    // Normal input handling
196                    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                            // Check for empty fields and return false to indicate cancel
228                            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                            // Enter paste mode
242                            editor_mode = EditorMode::Paste;
243                            paste_buffer.clear();
244                            state_changed = true;
245                        }
246
247                        _ => {
248                            if current_field == 0 {
249                                // Shortcut field handling
250                                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                                // Snippet field handling with vim-like modes
266                                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 => { /* Handled above */ }
298                                }
299                            }
300                        }
301                    }
302
303                    // Only redraw if state actually changed
304                    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            // Small sleep to prevent CPU usage when idle
317            thread::sleep(Duration::from_millis(1));
318        }
319
320        if snippet_added {
321            return Ok(true);
322        }
323    }
324}
325
326// Process a paste buffer efficiently by handling it all at once
327fn process_paste_buffer(
328    snippet: &mut Vec<String>,
329    current_line: &mut usize,
330    cursor_pos: &mut usize,
331    buffer: &str,
332) {
333    // Split the paste buffer by lines
334    let lines: Vec<&str> = buffer.split('\n').collect();
335
336    if lines.is_empty() {
337        return;
338    }
339
340    // Clone the current line to avoid borrowing issues
341    let current = snippet[*current_line].clone();
342
343    // Split at cursor position
344    let before = &current[..(*cursor_pos).min(current.len())];
345    let after = &current[(*cursor_pos).min(current.len())..];
346
347    // Replace current line with first part + first line of paste
348    snippet[*current_line] = format!("{}{}", before, lines[0]);
349    *cursor_pos = before.len() + lines[0].len();
350
351    // Insert the rest of the lines
352    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        // For the last line of paste, append the remainder of the original line
360        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    // Update current line position
375    if lines.len() > 1 {
376        *current_line += lines.len() - 1;
377    }
378}
379
380// Handle shortcut field input
381fn 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 true to indicate field change
391            return Ok(true);
392        }
393        KeyCode::Backspace => {
394            if *cursor_pos > 0 {
395                // Convert to chars for proper UTF-8 handling
396                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                    // Remove character before cursor
401                    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            // Convert to chars for proper UTF-8 handling
410            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                // Remove character at cursor
415                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(); // Count chars, not bytes
439            state_changed = true;
440        }
441        KeyCode::Char(c) => {
442            if shortcut.len() < MAX_SHORTCUT_LENGTH {
443                // Convert to chars for proper UTF-8 handling
444                let mut chars: Vec<char> = shortcut.chars().collect();
445                let cursor_char_pos = (*cursor_pos).min(chars.len());
446
447                // Insert character at cursor position
448                chars.insert(cursor_char_pos, c);
449
450                // Convert back to string
451                *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// Handle insert mode input
584#[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            // Check if both fields are empty - if so, return false for "canceled"
600            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                // Create a new line by splitting at cursor
610                let current = &snippet[*current_line];
611
612                // Convert to char array to handle UTF-8 correctly
613                let chars: Vec<char> = current.chars().collect();
614                let cursor_char_pos = (*cursor_pos).min(chars.len());
615
616                // Split string at character position
617                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                // Convert to chars for proper UTF-8 handling
636                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                    // Remove character before cursor
641                    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                // At start of line, merge with previous line
648                let content = snippet.remove(*current_line);
649                *current_line -= 1;
650                *cursor_pos = snippet[*current_line].chars().count(); // Important: count chars, not bytes
651                snippet[*current_line].push_str(&content);
652                state_changed = true;
653            }
654        }
655        KeyCode::Delete => {
656            // Convert to chars for proper UTF-8 handling
657            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                // Remove character at cursor
662                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                // At end of line, merge with next line
667                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                // Move to end of previous line
678                *current_line -= 1;
679                *cursor_pos = snippet[*current_line].chars().count(); // Count chars, not bytes
680                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                // Move to start of next line
690                *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(); // Count chars, not bytes
717            state_changed = true;
718        }
719        KeyCode::Tab => {
720            // Insert 4 spaces for tab
721            if snippet[*current_line].len() < MAX_LINE_LENGTH - 4 {
722                for _ in 0..4 {
723                    // Convert to chars
724                    let mut chars: Vec<char> = snippet[*current_line].chars().collect();
725                    let cursor_char_pos = (*cursor_pos).min(chars.len());
726
727                    // Insert space
728                    chars.insert(cursor_char_pos, ' ');
729
730                    // Convert back to string
731                    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                // Convert string to chars for proper UTF-8 handling
740                let mut chars: Vec<char> = snippet[*current_line].chars().collect();
741                let cursor_char_pos = (*cursor_pos).min(chars.len());
742
743                // Insert character at cursor position
744                chars.insert(cursor_char_pos, c);
745
746                // Convert back to string
747                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
758// Helper to submit a snippet
759fn 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); // Return false for "not completed"
764    }
765
766    // Join the lines with newlines
767    let full_snippet = snippet.join("\n");
768    match add_snippet(shortcut.to_string(), full_snippet) {
769        Ok(_) => {
770            show_success_message(stdout)?;
771            // Important: Return true to indicate success
772            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    // Use a static AtomicBool for tracking first draw
795    static FIRST_DRAW: AtomicBool = AtomicBool::new(true);
796
797    // Get terminal size safely
798    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    // Check if terminal is too small
809    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    // Calculate layout sizes using golden ratio-inspired proportions
817    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; // Center horizontally
820    let start_y = (height - panel_height) / 2; // Center vertically
821
822    // Clear the screen only on first draw - using atomic compare_exchange for thread safety
823    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 // Hide cursor during drawing to reduce flicker
831        ) {
832            return Err(SniptError::Other(format!("Failed to clear screen: {}", e)));
833        }
834    }
835
836    // Calculate title based on current mode
837    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    // Draw the title with better styling
846    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    // Draw the outer box with rounded corners for a nicer appearance
865    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    // Side borders
877    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    // Execute all the box drawing commands in one go
893    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    // Add app header/brand (new)
908    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    // Draw horizontal separator under header
921    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    // Draw shortcut field with improved style
935    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    // Draw the multiline field with improved style
952    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, // Adjust for better proportions
959        "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    // Add a distinctive button-like bottom bar for key actions
984    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    // Center the button bar
993    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    // Center the help text
1007    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    // Show editor mode if in snippet field
1030    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 there's an error message, display it with a red background for visibility
1056    if let Some(msg) = error_msg {
1057        let err_x = start_x + 2;
1058        let err_y = start_y + panel_height;
1059
1060        // Truncate message if needed
1061        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            // If we can't even print the error, just log it and continue
1076            eprintln!("Failed to show error: {}", e);
1077        }
1078    }
1079
1080    // Position cursor - with robust error handling
1081    let cursor_result = if editor_mode == EditorMode::Paste {
1082        // In paste mode, position cursor at top of screen where paste text appears
1083        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        // Position cursor in multiline field with scroll offset consideration
1093        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    // Flush output
1120    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    // Draw label
1137    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    // Draw field box top
1151    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    // Safely process value for display
1174    let visible_text = safe_truncate_string(value, width as usize - 4, true);
1175
1176    // Draw field content
1177    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    // Draw field box bottom
1199    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    // Draw label
1230    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    // Draw field box top
1244    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    // Calculate appropriate scroll position to keep current line visible
1260    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    // Show line numbers and scroll indicator if there are multiple lines
1268    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            // Non-critical error - just continue without scroll info
1280            eprintln!("Failed to draw scroll info: {}", e);
1281        }
1282    }
1283
1284    // Color settings
1285    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    // Draw each visible line
1293    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        // Safely process line for display
1301        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        // Padding to fill the line
1308        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    // Fill remaining lines with empty space
1333    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    // Draw field box bottom
1351    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
1369// Helper function to find previous character boundary
1370fn find_prev_char_boundary(s: &str, pos: usize) -> Option<usize> {
1371    if pos == 0 || pos > s.len() {
1372        return None;
1373    }
1374
1375    // Find the previous valid UTF-8 character boundary
1376    let mut idx = pos;
1377    while idx > 0 && !s.is_char_boundary(idx) {
1378        idx -= 1;
1379    }
1380
1381    // Keep going back to find the actual previous character
1382    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
1393// Helper function to find next character boundary
1394fn find_next_char_boundary(s: &str, pos: usize) -> Option<usize> {
1395    if pos >= s.len() {
1396        return None;
1397    }
1398
1399    // Find the next valid UTF-8 character boundary
1400    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
1412// Safe string truncation that respects UTF-8 boundaries
1413fn 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 the string is shorter than max_width, return it as is
1419    if s.chars().count() <= max_width {
1420        return s.to_string();
1421    }
1422
1423    // Otherwise truncate at character boundaries
1424    let mut result = String::with_capacity(max_width + 3); // +3 for possible ellipsis
1425    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    // Try to get terminal size, with fallback
1449    let (width, height) = terminal::size().unwrap_or((80, 24));
1450
1451    // Create an attractive success message box
1452    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    // Calculate box dimensions
1461    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    // Multiple commands with individual error handling
1467    if let Err(e) = execute!(stdout, terminal::Clear(ClearType::All)) {
1468        return Err(SniptError::Other(format!("Failed to clear screen: {}", e)));
1469    }
1470
1471    // Draw the success box
1472    if let Err(e) = execute!(
1473        stdout,
1474        // Draw top border
1475        cursor::MoveTo(x, y),
1476        SetForegroundColor(Color::Green),
1477        Print("╭"),
1478        Print("─".repeat((box_width - 2) as usize)),
1479        Print("╮"),
1480        // Draw title
1481        cursor::MoveTo(x + (box_width - 16) / 2, y),
1482        Print("╡ Success ╞"),
1483        // Reset for content
1484        ResetColor
1485    ) {
1486        return Err(SniptError::Other(format!("Failed to draw box top: {}", e)));
1487    }
1488
1489    // Draw message content
1490    for (i, line) in message_lines.iter().enumerate() {
1491        let line_y = y + i as u16 + 2; // +2 to account for top border and spacing
1492
1493        // Calculate position for centered text
1494        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            // Make the first line (success message) brighter
1502            Color::Green
1503        } else {
1504            Color::White
1505        };
1506
1507        if let Err(e) = execute!(
1508            stdout,
1509            // Draw border
1510            cursor::MoveTo(x, line_y),
1511            SetForegroundColor(Color::Green),
1512            Print("│"),
1513            // Draw content
1514            cursor::MoveTo(text_x, line_y),
1515            SetForegroundColor(color),
1516            Print(line),
1517            // Draw border
1518            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    // Draw bottom border
1531    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    // Wait for keypress but use cross-platform compatible wait instead of fixed sleep
1551    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    // Try to get terminal size, with fallback
1564    let (width, height) = terminal::size().unwrap_or((80, 24));
1565
1566    // Truncate message if too long
1567    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}