use crossterm::{
cursor,
event::{self, Event, KeyCode, KeyEvent, KeyModifiers},
execute,
style::{Color, Print, ResetColor, SetBackgroundColor, SetForegroundColor},
terminal::{self, ClearType, EnterAlternateScreen, LeaveAlternateScreen},
};
use snipt_core::{add_snippet, Result, SniptError};
use std::sync::atomic::{AtomicBool, Ordering};
use std::time::{Duration, Instant};
use std::{
io::{self, stdout, Write},
thread,
};
const MAX_LINES: usize = 10000;
const MAX_LINE_LENGTH: usize = 5000;
const MAX_SHORTCUT_LENGTH: usize = 50;
#[derive(PartialEq, Copy, Clone)]
enum EditorMode {
Normal,
Insert,
Paste, }
pub enum AddResult {
Added,
Cancelled,
Error(SniptError),
}
pub fn interactive_add() -> AddResult {
if let Err(e) = terminal::enable_raw_mode() {
return AddResult::Error(SniptError::Other(format!(
"Failed to enable raw mode: {}",
e
)));
}
let mut stdout = stdout();
if let Err(e) = execute!(stdout, EnterAlternateScreen) {
terminal::disable_raw_mode().ok();
return AddResult::Error(SniptError::Other(format!(
"Failed to enter alternate screen: {}",
e
)));
}
let result = run_interactive_ui(&mut stdout);
let _ = execute!(stdout, LeaveAlternateScreen);
let _ = terminal::disable_raw_mode();
match result {
Ok(true) => AddResult::Added,
Ok(false) => AddResult::Cancelled,
Err(e) => AddResult::Error(e),
}
}
fn run_interactive_ui(stdout: &mut io::Stdout) -> Result<bool> {
let mut shortcut = String::new();
let mut snippet = Vec::new();
snippet.push(String::new());
let mut current_field = 0; let mut cursor_pos = 0;
let mut current_line = 0;
let mut editor_mode = EditorMode::Insert;
let mut error_message = None;
let mut snippet_added = false;
let mut paste_buffer = String::new();
let mut last_render = Instant::now();
let mut force_render = true;
if let Err(e) = draw_ui(
stdout,
&shortcut,
&snippet,
current_field,
cursor_pos,
current_line,
editor_mode,
error_message.as_deref(),
) {
error_message = Some(format!("UI Error: {}. Using minimal mode.", e));
}
const RENDER_THRESHOLD: Duration = Duration::from_millis(16);
loop {
let now = Instant::now();
if force_render || now.duration_since(last_render) >= RENDER_THRESHOLD {
if let Err(e) = draw_ui(
stdout,
&shortcut,
&snippet,
current_field,
cursor_pos,
current_line,
editor_mode,
error_message.as_deref(),
) {
error_message = Some(format!("UI Error: {}. Using minimal mode.", e));
if execute!(
stdout,
terminal::Clear(ClearType::All),
cursor::MoveTo(0, 0),
SetForegroundColor(Color::Red),
Print(&error_message.clone().unwrap_or_default()),
ResetColor,
cursor::MoveTo(0, 2),
SetForegroundColor(Color::White),
Print(format!("Shortcut: {}\n", shortcut)),
Print(format!(
"Editing {} lines, current: {}\n",
snippet.len(),
current_line + 1
)),
Print("Press Ctrl+W to save or Esc to cancel\n"),
ResetColor
)
.is_err()
{
return Err(SniptError::Other(
"Terminal display error. Try in a larger terminal.".to_string(),
));
}
} else {
error_message = None;
}
last_render = now;
force_render = false;
}
if crossterm::event::poll(Duration::from_millis(1))? {
match event::read() {
Ok(Event::Key(KeyEvent {
code, modifiers, ..
})) => {
let mut state_changed = false;
if editor_mode == EditorMode::Paste {
match code {
KeyCode::Esc => {
paste_buffer.clear();
return Ok(false);
}
KeyCode::Enter => {
if !paste_buffer.is_empty() {
process_paste_buffer(
&mut snippet,
&mut current_line,
&mut cursor_pos,
&paste_buffer,
);
paste_buffer.clear();
}
editor_mode = EditorMode::Insert;
state_changed = true;
}
KeyCode::Char(c) => {
paste_buffer.push(c);
}
_ => {}
}
if state_changed {
force_render = true;
}
continue;
}
match code {
KeyCode::Tab | KeyCode::Down => {
if current_field == 0 {
current_field = 1;
current_line = 0;
cursor_pos = snippet[current_line].len();
state_changed = true;
} else if modifiers.contains(KeyModifiers::SHIFT) {
current_field = 0;
cursor_pos = shortcut.len();
state_changed = true;
} else if current_line < snippet.len() - 1 {
current_line += 1;
cursor_pos = snippet[current_line].len().min(cursor_pos);
state_changed = true;
}
}
KeyCode::BackTab | KeyCode::Up => {
if current_field == 1 {
if current_line > 0 {
current_line -= 1;
cursor_pos = snippet[current_line].len().min(cursor_pos);
state_changed = true;
} else {
current_field = 0;
cursor_pos = shortcut.len();
state_changed = true;
}
}
}
KeyCode::Esc => {
if shortcut.is_empty() && (snippet.len() == 1 && snippet[0].is_empty())
{
return Ok(false);
}
if editor_mode == EditorMode::Normal {
return Ok(snippet_added);
} else {
editor_mode = EditorMode::Normal;
state_changed = true;
}
}
KeyCode::Char('v') if modifiers.contains(KeyModifiers::CONTROL) => {
editor_mode = EditorMode::Paste;
paste_buffer.clear();
state_changed = true;
}
_ => {
if current_field == 0 {
if handle_shortcut_input(
&mut shortcut,
&mut cursor_pos,
code,
modifiers,
)? {
state_changed = true;
}
if code == KeyCode::Enter {
current_field = 1;
current_line = 0;
cursor_pos = snippet[current_line].len();
state_changed = true;
}
} else {
match editor_mode {
EditorMode::Normal => {
if handle_normal_mode(
&mut snippet,
&mut cursor_pos,
&mut current_line,
&mut editor_mode,
code,
modifiers,
stdout,
&shortcut,
&mut snippet_added,
)? {
state_changed = true;
}
}
EditorMode::Insert => {
if handle_insert_mode(
&mut snippet,
&mut cursor_pos,
&mut current_line,
&mut editor_mode,
code,
modifiers,
stdout,
&shortcut,
&mut snippet_added,
)? {
state_changed = true;
}
}
EditorMode::Paste => { }
}
}
}
}
if state_changed {
force_render = true;
}
}
Err(e) => {
error_message = Some(format!("Input error: {}. Press any key to continue.", e));
thread_sleep(1000);
force_render = true;
}
_ => {}
}
} else {
thread::sleep(Duration::from_millis(1));
}
if snippet_added {
return Ok(true);
}
}
}
fn process_paste_buffer(
snippet: &mut Vec<String>,
current_line: &mut usize,
cursor_pos: &mut usize,
buffer: &str,
) {
let lines: Vec<&str> = buffer.split('\n').collect();
if lines.is_empty() {
return;
}
let current = snippet[*current_line].clone();
let before = ¤t[..(*cursor_pos).min(current.len())];
let after = ¤t[(*cursor_pos).min(current.len())..];
snippet[*current_line] = format!("{}{}", before, lines[0]);
*cursor_pos = before.len() + lines[0].len();
for (i, &line) in lines.iter().enumerate().skip(1) {
if snippet.len() >= MAX_LINES {
break;
}
let insertion_index = *current_line + i;
if i == lines.len() - 1 {
let combined_line = format!("{}{}", line, after);
if insertion_index < snippet.len() {
snippet.insert(insertion_index, combined_line);
} else {
snippet.push(combined_line);
}
} else if insertion_index < snippet.len() {
snippet.insert(insertion_index, line.to_string());
} else {
snippet.push(line.to_string());
}
}
if lines.len() > 1 {
*current_line += lines.len() - 1;
}
}
fn handle_shortcut_input(
shortcut: &mut String,
cursor_pos: &mut usize,
code: KeyCode,
_modifiers: KeyModifiers,
) -> Result<bool> {
let mut state_changed = false;
match code {
KeyCode::Enter => {
return Ok(true);
}
KeyCode::Backspace => {
if *cursor_pos > 0 {
let mut chars: Vec<char> = shortcut.chars().collect();
let cursor_char_pos = (*cursor_pos).min(chars.len());
if cursor_char_pos > 0 {
chars.remove(cursor_char_pos - 1);
*shortcut = chars.into_iter().collect();
*cursor_pos -= 1;
state_changed = true;
}
}
}
KeyCode::Delete => {
let mut chars: Vec<char> = shortcut.chars().collect();
let cursor_char_pos = (*cursor_pos).min(chars.len());
if cursor_char_pos < chars.len() {
chars.remove(cursor_char_pos);
*shortcut = chars.into_iter().collect();
state_changed = true;
}
}
KeyCode::Left => {
if *cursor_pos > 0 {
*cursor_pos -= 1;
state_changed = true;
}
}
KeyCode::Right => {
let char_count = shortcut.chars().count();
if *cursor_pos < char_count {
*cursor_pos += 1;
state_changed = true;
}
}
KeyCode::Home => {
*cursor_pos = 0;
state_changed = true;
}
KeyCode::End => {
*cursor_pos = shortcut.chars().count(); state_changed = true;
}
KeyCode::Char(c) => {
if shortcut.len() < MAX_SHORTCUT_LENGTH {
let mut chars: Vec<char> = shortcut.chars().collect();
let cursor_char_pos = (*cursor_pos).min(chars.len());
chars.insert(cursor_char_pos, c);
*shortcut = chars.into_iter().collect();
*cursor_pos += 1;
state_changed = true;
}
}
_ => {}
}
Ok(state_changed)
}
#[allow(clippy::too_many_arguments)]
fn handle_normal_mode(
snippet: &mut Vec<String>,
cursor_pos: &mut usize,
current_line: &mut usize,
editor_mode: &mut EditorMode,
code: KeyCode,
modifiers: KeyModifiers,
stdout: &mut io::Stdout,
shortcut: &str,
snippet_added: &mut bool,
) -> Result<bool> {
let mut state_changed = false;
match code {
KeyCode::Char('i') => {
*editor_mode = EditorMode::Insert;
state_changed = true;
}
KeyCode::Char('a') => {
if *cursor_pos < snippet[*current_line].len() {
*cursor_pos = find_next_char_boundary(&snippet[*current_line], *cursor_pos)
.unwrap_or(*cursor_pos + 1)
.min(snippet[*current_line].len());
}
*editor_mode = EditorMode::Insert;
state_changed = true;
}
KeyCode::Char('A') => {
*cursor_pos = snippet[*current_line].len();
*editor_mode = EditorMode::Insert;
state_changed = true;
}
KeyCode::Char('o') => {
if snippet.len() < MAX_LINES {
snippet.insert(*current_line + 1, String::new());
*current_line += 1;
*cursor_pos = 0;
*editor_mode = EditorMode::Insert;
state_changed = true;
}
}
KeyCode::Char('O') => {
if snippet.len() < MAX_LINES {
snippet.insert(*current_line, String::new());
*cursor_pos = 0;
*editor_mode = EditorMode::Insert;
state_changed = true;
}
}
KeyCode::Char('h') => {
if *cursor_pos > 0 {
*cursor_pos = find_prev_char_boundary(&snippet[*current_line], *cursor_pos)
.unwrap_or(*cursor_pos - 1)
.min(*cursor_pos);
state_changed = true;
} else if *current_line > 0 {
*current_line -= 1;
*cursor_pos = snippet[*current_line].len();
state_changed = true;
}
}
KeyCode::Char('l') => {
if *cursor_pos < snippet[*current_line].len() {
*cursor_pos = find_next_char_boundary(&snippet[*current_line], *cursor_pos)
.unwrap_or(*cursor_pos + 1)
.min(snippet[*current_line].len());
state_changed = true;
} else if *current_line < snippet.len() - 1 {
*current_line += 1;
*cursor_pos = 0;
state_changed = true;
}
}
KeyCode::Char('j') => {
if *current_line < snippet.len() - 1 {
*current_line += 1;
*cursor_pos = (*cursor_pos).min(snippet[*current_line].len());
state_changed = true;
}
}
KeyCode::Char('k') => {
if *current_line > 0 {
*current_line -= 1;
*cursor_pos = (*cursor_pos).min(snippet[*current_line].len());
state_changed = true;
}
}
KeyCode::Char('0') => {
*cursor_pos = 0;
state_changed = true;
}
KeyCode::Char('$') => {
*cursor_pos = snippet[*current_line].len();
state_changed = true;
}
KeyCode::Char('d') if modifiers.contains(KeyModifiers::CONTROL) => {
if snippet.len() > 1 {
snippet.remove(*current_line);
if *current_line >= snippet.len() {
*current_line = snippet.len() - 1;
}
*cursor_pos = (*cursor_pos).min(snippet[*current_line].len());
state_changed = true;
} else {
snippet[0].clear();
*cursor_pos = 0;
state_changed = true;
}
}
KeyCode::Enter => {
if let Ok(added) = submit_snippet(stdout, shortcut, snippet) {
*snippet_added = added;
}
return Ok(true);
}
_ => {}
}
Ok(state_changed)
}
#[allow(clippy::too_many_arguments)]
fn handle_insert_mode(
snippet: &mut Vec<String>,
cursor_pos: &mut usize,
current_line: &mut usize,
editor_mode: &mut EditorMode,
code: KeyCode,
modifiers: KeyModifiers,
stdout: &mut io::Stdout,
shortcut: &str,
snippet_added: &mut bool,
) -> Result<bool> {
let mut state_changed = false;
match code {
KeyCode::Esc => {
if shortcut.is_empty() && (snippet.len() == 1 && snippet[0].is_empty()) {
*snippet_added = false;
return Ok(true);
}
*editor_mode = EditorMode::Normal;
state_changed = true;
}
KeyCode::Enter => {
if snippet.len() < MAX_LINES {
let current = &snippet[*current_line];
let chars: Vec<char> = current.chars().collect();
let cursor_char_pos = (*cursor_pos).min(chars.len());
let before: String = chars[..cursor_char_pos].iter().collect();
let after: String = chars[cursor_char_pos..].iter().collect();
snippet[*current_line] = before;
snippet.insert(*current_line + 1, after);
*current_line += 1;
*cursor_pos = 0;
state_changed = true;
}
}
KeyCode::Char('w') if modifiers.contains(KeyModifiers::CONTROL) => {
if let Ok(added) = submit_snippet(stdout, shortcut, snippet) {
*snippet_added = added;
}
return Ok(true);
}
KeyCode::Backspace => {
if *cursor_pos > 0 {
let mut chars: Vec<char> = snippet[*current_line].chars().collect();
let cursor_char_pos = (*cursor_pos).min(chars.len());
if cursor_char_pos > 0 {
chars.remove(cursor_char_pos - 1);
snippet[*current_line] = chars.into_iter().collect();
*cursor_pos -= 1;
state_changed = true;
}
} else if *current_line > 0 {
let content = snippet.remove(*current_line);
*current_line -= 1;
*cursor_pos = snippet[*current_line].chars().count(); snippet[*current_line].push_str(&content);
state_changed = true;
}
}
KeyCode::Delete => {
let mut chars: Vec<char> = snippet[*current_line].chars().collect();
let cursor_char_pos = (*cursor_pos).min(chars.len());
if cursor_char_pos < chars.len() {
chars.remove(cursor_char_pos);
snippet[*current_line] = chars.into_iter().collect();
state_changed = true;
} else if *current_line < snippet.len() - 1 {
let next = snippet.remove(*current_line + 1);
snippet[*current_line].push_str(&next);
state_changed = true;
}
}
KeyCode::Left => {
if *cursor_pos > 0 {
*cursor_pos -= 1;
state_changed = true;
} else if *current_line > 0 {
*current_line -= 1;
*cursor_pos = snippet[*current_line].chars().count(); state_changed = true;
}
}
KeyCode::Right => {
let char_count = snippet[*current_line].chars().count();
if *cursor_pos < char_count {
*cursor_pos += 1;
state_changed = true;
} else if *current_line < snippet.len() - 1 {
*current_line += 1;
*cursor_pos = 0;
state_changed = true;
}
}
KeyCode::Up => {
if *current_line > 0 {
*current_line -= 1;
let char_count = snippet[*current_line].chars().count();
*cursor_pos = (*cursor_pos).min(char_count);
state_changed = true;
}
}
KeyCode::Down => {
if *current_line < snippet.len() - 1 {
*current_line += 1;
let char_count = snippet[*current_line].chars().count();
*cursor_pos = (*cursor_pos).min(char_count);
state_changed = true;
}
}
KeyCode::Home => {
*cursor_pos = 0;
state_changed = true;
}
KeyCode::End => {
*cursor_pos = snippet[*current_line].chars().count(); state_changed = true;
}
KeyCode::Tab => {
if snippet[*current_line].len() < MAX_LINE_LENGTH - 4 {
for _ in 0..4 {
let mut chars: Vec<char> = snippet[*current_line].chars().collect();
let cursor_char_pos = (*cursor_pos).min(chars.len());
chars.insert(cursor_char_pos, ' ');
snippet[*current_line] = chars.into_iter().collect();
*cursor_pos += 1;
state_changed = true;
}
}
}
KeyCode::Char(c) => {
if snippet[*current_line].len() < MAX_LINE_LENGTH {
let mut chars: Vec<char> = snippet[*current_line].chars().collect();
let cursor_char_pos = (*cursor_pos).min(chars.len());
chars.insert(cursor_char_pos, c);
snippet[*current_line] = chars.into_iter().collect();
*cursor_pos += 1;
state_changed = true;
}
}
_ => {}
}
Ok(state_changed)
}
fn submit_snippet(stdout: &mut io::Stdout, shortcut: &str, snippet: &[String]) -> Result<bool> {
if shortcut.is_empty() || snippet.is_empty() || snippet[0].is_empty() {
show_error_message(stdout, "Both fields must be filled")?;
thread_sleep(1500);
return Ok(false); }
let full_snippet = snippet.join("\n");
match add_snippet(shortcut.to_string(), full_snippet) {
Ok(_) => {
show_success_message(stdout)?;
Ok(true)
}
Err(SniptError::Other(msg)) if msg.contains("already exists") => {
show_error_message(stdout, &msg)?;
thread_sleep(1500);
Ok(false)
}
Err(e) => Err(e),
}
}
#[allow(clippy::too_many_arguments)]
fn draw_ui(
stdout: &mut io::Stdout,
shortcut: &str,
snippet: &[String],
current_field: usize,
cursor_pos: usize,
current_line: usize,
editor_mode: EditorMode,
error_msg: Option<&str>,
) -> Result<()> {
static FIRST_DRAW: AtomicBool = AtomicBool::new(true);
let (width, height) = match terminal::size() {
Ok((w, h)) => (w, h),
Err(e) => {
return Err(SniptError::Other(format!(
"Failed to get terminal size: {}",
e
)))
}
};
if width < 40 || height < 15 {
return Err(SniptError::Other(format!(
"Terminal too small. Minimum size: 40x15, current: {}x{}",
width, height
)));
}
let panel_width = width.saturating_sub(8).max(40);
let panel_height = height.saturating_sub(6).max(15);
let start_x = (width - panel_width) / 2; let start_y = (height - panel_height) / 2;
if FIRST_DRAW
.compare_exchange(true, false, Ordering::SeqCst, Ordering::SeqCst)
.is_ok()
{
if let Err(e) = execute!(
stdout,
terminal::Clear(ClearType::All),
cursor::Hide ) {
return Err(SniptError::Other(format!("Failed to clear screen: {}", e)));
}
}
let title = match editor_mode {
EditorMode::Paste => " ✏️ Paste Mode - Enter to confirm ",
EditorMode::Normal => " ✏️ Add New Snippet - Normal Mode ",
EditorMode::Insert => " ✏️ Add New Snippet - Insert Mode ",
};
let title_x = start_x + (panel_width - title.len() as u16) / 2;
if let Err(e) = execute!(
stdout,
cursor::Hide,
cursor::MoveTo(title_x, start_y - 1),
SetForegroundColor(if editor_mode == EditorMode::Paste {
Color::Green
} else if editor_mode == EditorMode::Normal {
Color::Blue
} else {
Color::Cyan
}),
SetBackgroundColor(Color::Black),
Print(title),
ResetColor
) {
return Err(SniptError::Other(format!("Failed to draw title: {}", e)));
}
if let Err(e) = execute!(
stdout,
cursor::MoveTo(start_x, start_y),
SetForegroundColor(Color::Cyan),
Print("╭"),
Print("─".repeat((panel_width - 2) as usize)),
Print("╮")
) {
return Err(SniptError::Other(format!("Failed to draw box top: {}", e)));
}
for i in 1..panel_height - 1 {
if let Err(e) = execute!(
stdout,
cursor::MoveTo(start_x, start_y + i),
Print("│"),
cursor::MoveTo(start_x + panel_width - 1, start_y + i),
Print("│")
) {
return Err(SniptError::Other(format!(
"Failed to draw box sides at row {}: {}",
i, e
)));
}
}
if let Err(e) = execute!(
stdout,
cursor::MoveTo(start_x, start_y + panel_height - 1),
Print("╰"),
Print("─".repeat((panel_width - 2) as usize)),
Print("╯"),
ResetColor
) {
return Err(SniptError::Other(format!(
"Failed to draw box bottom: {}",
e
)));
}
if let Err(e) = execute!(
stdout,
cursor::MoveTo(start_x + 3, start_y + 1),
SetForegroundColor(Color::Magenta),
Print("snipt"),
SetForegroundColor(Color::DarkGrey),
Print(" - Text Expansion Tool"),
ResetColor
) {
return Err(SniptError::Other(format!("Failed to draw header: {}", e)));
}
if let Err(e) = execute!(
stdout,
cursor::MoveTo(start_x + 1, start_y + 2),
SetForegroundColor(Color::DarkGrey),
Print("─".repeat((panel_width - 3) as usize)),
ResetColor
) {
return Err(SniptError::Other(format!(
"Failed to draw separator: {}",
e
)));
}
let field_x = start_x + 3;
if let Err(e) = draw_field(
stdout,
field_x,
start_y + 4,
panel_width - 8,
"Shortcut:",
shortcut,
current_field == 0,
) {
return Err(SniptError::Other(format!(
"Failed to draw shortcut field: {}",
e
)));
}
let field_x = start_x + 3;
if let Err(e) = draw_multiline_field(
stdout,
field_x,
start_y + 8,
panel_width - 6,
panel_height - 14, "Snippet:",
snippet,
current_field == 1,
current_line,
) {
return Err(SniptError::Other(format!(
"Failed to draw snippet field: {}",
e
)));
}
let help_text = match editor_mode {
EditorMode::Normal => {
"i/a: Insert | o/O: New line | h/j/k/l: Navigate | Ctrl+d: Delete line | Enter: Submit"
}
EditorMode::Insert => {
if current_field == 0 {
"Tab: Next field | Enter: Next field | Esc: Cancel"
} else {
"Esc: Normal mode | Enter: New line | Arrows: Navigate | Ctrl+v: Paste | Ctrl+w: Submit"
}
}
EditorMode::Paste => "Enter: Confirm paste | Esc: Cancel | Type or paste text",
};
let buttons_line = match editor_mode {
EditorMode::Insert if current_field == 1 => {
"[ Ctrl+W: Save ] [ Tab: Indent ] [ Esc: Normal Mode ]"
}
EditorMode::Normal => "[ Enter: Submit ] [ i: Insert Mode ] [ Esc: Cancel ]",
_ => "[ Ctrl+W: Save ] [ Esc: Cancel ]",
};
let buttons_x = start_x + (panel_width - buttons_line.len() as u16) / 2;
if let Err(e) = execute!(
stdout,
cursor::MoveTo(buttons_x, start_y + panel_height - 3),
SetForegroundColor(Color::White),
SetBackgroundColor(Color::DarkBlue),
Print(buttons_line),
ResetColor
) {
return Err(SniptError::Other(format!("Failed to draw buttons: {}", e)));
}
let help_x = if help_text.len() as u16 <= panel_width - 4 {
start_x + (panel_width - help_text.len() as u16) / 2
} else {
start_x + 2
};
if let Err(e) = execute!(
stdout,
cursor::MoveTo(help_x, start_y + panel_height - 2),
SetForegroundColor(Color::DarkGrey),
Print(if help_text.len() as u16 <= panel_width - 4 {
help_text
} else {
&help_text[0..(panel_width - 7) as usize]
}),
ResetColor
) {
return Err(SniptError::Other(format!(
"Failed to draw help text: {}",
e
)));
}
if current_field == 1 && editor_mode != EditorMode::Paste {
let mode_text = match editor_mode {
EditorMode::Normal => "-- NORMAL --",
EditorMode::Insert => "-- INSERT --",
EditorMode::Paste => "-- PASTE --",
};
if let Err(e) = execute!(
stdout,
cursor::MoveTo(field_x, start_y + 7),
SetForegroundColor(if matches!(editor_mode, EditorMode::Normal) {
Color::Blue
} else {
Color::Green
}),
Print(mode_text),
ResetColor
) {
return Err(SniptError::Other(format!(
"Failed to draw mode text: {}",
e
)));
}
}
if let Some(msg) = error_msg {
let err_x = start_x + 2;
let err_y = start_y + panel_height;
let display_msg = if msg.len() > (panel_width - 4) as usize {
&msg[0..(panel_width - 7) as usize]
} else {
msg
};
if let Err(e) = execute!(
stdout,
cursor::MoveTo(err_x, err_y),
SetForegroundColor(Color::White),
SetBackgroundColor(Color::Red),
Print(format!(" {} ", display_msg)),
ResetColor
) {
eprintln!("Failed to show error: {}", e);
}
}
let cursor_result = if editor_mode == EditorMode::Paste {
execute!(stdout, cursor::MoveTo(0, 1), cursor::Show)
} else if current_field == 0 {
let visible_cursor_pos = cursor_pos.min(panel_width as usize - 9) as u16;
execute!(
stdout,
cursor::MoveTo(field_x + 1 + visible_cursor_pos, start_y + 5),
cursor::Show
)
} else {
let visible_area_height = (panel_height - 14) as usize;
let scroll_offset = if current_line >= visible_area_height {
current_line - visible_area_height + 1
} else {
0
};
let visible_line_idx = current_line - scroll_offset;
let visible_cursor_pos = cursor_pos.min(panel_width as usize - 9) as u16;
execute!(
stdout,
cursor::MoveTo(
field_x + 1 + visible_cursor_pos,
start_y + 9 + visible_line_idx as u16
),
cursor::Show
)
};
if let Err(e) = cursor_result {
return Err(SniptError::Other(format!(
"Failed to position cursor: {}",
e
)));
}
if let Err(e) = stdout.flush() {
return Err(SniptError::Other(format!("Failed to flush output: {}", e)));
}
Ok(())
}
fn draw_field(
stdout: &mut io::Stdout,
x: u16,
y: u16,
width: u16,
label: &str,
value: &str,
active: bool,
) -> Result<()> {
if let Err(e) = execute!(
stdout,
cursor::MoveTo(x, y),
SetForegroundColor(Color::Yellow),
Print(label),
ResetColor
) {
return Err(SniptError::Other(format!(
"Failed to draw field label: {}",
e
)));
}
if let Err(e) = execute!(
stdout,
cursor::MoveTo(x, y + 1),
SetForegroundColor(Color::Blue),
Print("┌"),
Print("─".repeat((width - 2) as usize)),
Print("┐"),
ResetColor
) {
return Err(SniptError::Other(format!(
"Failed to draw field box top: {}",
e
)));
}
let bg_color = if active {
Color::DarkBlue
} else {
Color::Black
};
let fg_color = if active { Color::White } else { Color::Grey };
let visible_text = safe_truncate_string(value, width as usize - 4, true);
if let Err(e) = execute!(
stdout,
cursor::MoveTo(x, y + 2),
SetForegroundColor(Color::Blue),
Print("│"),
SetBackgroundColor(bg_color),
SetForegroundColor(fg_color),
Print(" "),
Print(&visible_text),
Print(" ".repeat((width as usize - 3 - visible_text.chars().count()).max(0))),
ResetColor,
SetForegroundColor(Color::Blue),
Print("│"),
ResetColor
) {
return Err(SniptError::Other(format!(
"Failed to draw field content: {}",
e
)));
}
if let Err(e) = execute!(
stdout,
cursor::MoveTo(x, y + 3),
SetForegroundColor(Color::Blue),
Print("└"),
Print("─".repeat((width - 2) as usize)),
Print("┘"),
ResetColor
) {
return Err(SniptError::Other(format!(
"Failed to draw field box bottom: {}",
e
)));
}
Ok(())
}
#[allow(clippy::too_many_arguments)]
fn draw_multiline_field(
stdout: &mut io::Stdout,
x: u16,
y: u16,
width: u16,
height: u16,
label: &str,
lines: &[String],
active: bool,
current_line: usize,
) -> Result<()> {
if let Err(e) = execute!(
stdout,
cursor::MoveTo(x, y),
SetForegroundColor(Color::Yellow),
Print(label),
ResetColor
) {
return Err(SniptError::Other(format!(
"Failed to draw multiline field label: {}",
e
)));
}
if let Err(e) = execute!(
stdout,
cursor::MoveTo(x, y + 1),
SetForegroundColor(Color::Blue),
Print("┌"),
Print("─".repeat((width - 2) as usize)),
Print("┐"),
ResetColor
) {
return Err(SniptError::Other(format!(
"Failed to draw multiline field box top: {}",
e
)));
}
let visible_area_height = height as usize;
let scroll_offset = if current_line >= visible_area_height {
current_line - visible_area_height + 1
} else {
0
};
if lines.len() > 1 {
let scroll_info = format!(" {}/{} ", current_line + 1, lines.len());
let info_x = x + width - scroll_info.len() as u16 - 2;
if let Err(e) = execute!(
stdout,
cursor::MoveTo(info_x, y + 1),
SetForegroundColor(Color::Yellow),
Print(scroll_info),
ResetColor
) {
eprintln!("Failed to draw scroll info: {}", e);
}
}
let bg_color = if active {
Color::DarkBlue
} else {
Color::Black
};
let fg_color = if active { Color::White } else { Color::Grey };
let max_visible_lines = height as usize;
let end_line = (scroll_offset + max_visible_lines).min(lines.len());
for i in 0..(end_line - scroll_offset) {
let line_idx = i + scroll_offset;
let line = &lines[line_idx];
let visible_text = safe_truncate_string(line, width as usize - 4, true);
let is_current = line_idx == current_line && active;
let line_bg = if is_current { bg_color } else { Color::Black };
let line_fg = if is_current { Color::White } else { fg_color };
let padding_length = (width as usize - 3 - visible_text.chars().count()).max(0);
if let Err(e) = execute!(
stdout,
cursor::MoveTo(x, y + 2 + i as u16),
SetForegroundColor(Color::Blue),
Print("│"),
SetBackgroundColor(line_bg),
SetForegroundColor(line_fg),
Print(" "),
Print(&visible_text),
Print(" ".repeat(padding_length)),
ResetColor,
SetForegroundColor(Color::Blue),
Print("│"),
ResetColor
) {
return Err(SniptError::Other(format!(
"Failed to draw line {} of multiline field: {}",
i, e
)));
}
}
for i in (end_line - scroll_offset)..max_visible_lines {
if let Err(e) = execute!(
stdout,
cursor::MoveTo(x, y + 2 + i as u16),
SetForegroundColor(Color::Blue),
Print("│"),
Print(" ".repeat((width - 2) as usize)),
Print("│"),
ResetColor
) {
return Err(SniptError::Other(format!(
"Failed to draw empty line {} of multiline field: {}",
i, e
)));
}
}
if let Err(e) = execute!(
stdout,
cursor::MoveTo(x, y + 2 + height),
SetForegroundColor(Color::Blue),
Print("└"),
Print("─".repeat((width - 2) as usize)),
Print("┘"),
ResetColor
) {
return Err(SniptError::Other(format!(
"Failed to draw multiline field box bottom: {}",
e
)));
}
Ok(())
}
fn find_prev_char_boundary(s: &str, pos: usize) -> Option<usize> {
if pos == 0 || pos > s.len() {
return None;
}
let mut idx = pos;
while idx > 0 && !s.is_char_boundary(idx) {
idx -= 1;
}
if idx > 0 {
let mut prev_idx = idx - 1;
while prev_idx > 0 && !s.is_char_boundary(prev_idx) {
prev_idx -= 1;
}
Some(prev_idx)
} else {
Some(0)
}
}
fn find_next_char_boundary(s: &str, pos: usize) -> Option<usize> {
if pos >= s.len() {
return None;
}
let mut idx = pos + 1;
while idx < s.len() && !s.is_char_boundary(idx) {
idx += 1;
}
if idx <= s.len() {
Some(idx)
} else {
Some(s.len())
}
}
fn safe_truncate_string(s: &str, max_width: usize, add_ellipsis: bool) -> String {
if s.is_empty() || max_width == 0 {
return String::new();
}
if s.chars().count() <= max_width {
return s.to_string();
}
let mut result = String::with_capacity(max_width + 3); let mut count = 0;
let actual_max = if add_ellipsis {
max_width - 3
} else {
max_width
};
for c in s.chars() {
if count >= actual_max {
break;
}
result.push(c);
count += 1;
}
if add_ellipsis && count < s.chars().count() {
result.push_str("...");
}
result
}
fn show_success_message(stdout: &mut io::Stdout) -> Result<()> {
let (width, height) = terminal::size().unwrap_or((80, 24));
let message_lines = [
"✓ Snippet added successfully!",
"",
"Your snippet is now ready to use.",
"",
"Press any key to view your snippets...",
];
let box_width = 50u16;
let box_height = (message_lines.len() + 4) as u16;
let x = (width.saturating_sub(box_width)) / 2;
let y = (height.saturating_sub(box_height)) / 2;
if let Err(e) = execute!(stdout, terminal::Clear(ClearType::All)) {
return Err(SniptError::Other(format!("Failed to clear screen: {}", e)));
}
if let Err(e) = execute!(
stdout,
cursor::MoveTo(x, y),
SetForegroundColor(Color::Green),
Print("╭"),
Print("─".repeat((box_width - 2) as usize)),
Print("╮"),
cursor::MoveTo(x + (box_width - 16) / 2, y),
Print("╡ Success ╞"),
ResetColor
) {
return Err(SniptError::Other(format!("Failed to draw box top: {}", e)));
}
for (i, line) in message_lines.iter().enumerate() {
let line_y = y + i as u16 + 2;
let text_x = if line.is_empty() {
x + 2
} else {
x + (box_width - line.len() as u16) / 2
};
let color = if i == 0 {
Color::Green
} else {
Color::White
};
if let Err(e) = execute!(
stdout,
cursor::MoveTo(x, line_y),
SetForegroundColor(Color::Green),
Print("│"),
cursor::MoveTo(text_x, line_y),
SetForegroundColor(color),
Print(line),
cursor::MoveTo(x + box_width - 1, line_y),
SetForegroundColor(Color::Green),
Print("│"),
ResetColor
) {
return Err(SniptError::Other(format!(
"Failed to draw line {}: {}",
i, e
)));
}
}
if let Err(e) = execute!(
stdout,
cursor::MoveTo(x, y + box_height - 1),
SetForegroundColor(Color::Green),
Print("╰"),
Print("─".repeat((box_width - 2) as usize)),
Print("╯"),
ResetColor
) {
return Err(SniptError::Other(format!(
"Failed to draw box bottom: {}",
e
)));
}
if let Err(e) = stdout.flush() {
return Err(SniptError::Other(format!("Failed to flush output: {}", e)));
}
let exit_at = std::time::Instant::now() + Duration::from_millis(1000);
while std::time::Instant::now() < exit_at {
if crossterm::event::poll(Duration::from_millis(100))? {
let _ = crossterm::event::read()?;
break;
}
}
Ok(())
}
fn show_error_message(stdout: &mut io::Stdout, message: &str) -> Result<()> {
let (width, height) = terminal::size().unwrap_or((80, 24));
let display_msg = safe_truncate_string(message, width as usize - 10, true);
let x = (width.saturating_sub(display_msg.len() as u16)) / 2;
let y = height - 3;
if let Err(e) = execute!(
stdout,
cursor::MoveTo(x, y),
SetForegroundColor(Color::Red),
Print("⚠ "),
Print(display_msg),
ResetColor
) {
return Err(SniptError::Other(format!(
"Failed to show error message: {}",
e
)));
}
if let Err(e) = stdout.flush() {
return Err(SniptError::Other(format!("Failed to flush output: {}", e)));
}
Ok(())
}
fn thread_sleep(ms: u64) {
std::thread::sleep(std::time::Duration::from_millis(ms));
}