Skip to main content

mermaid_cli/tui/state/
input.rs

1//! Input state management
2//!
3//! User input buffer, cursor handling, and input history navigation.
4
5use std::collections::VecDeque;
6
7/// Input state - user input buffer, cursor, and history
8///
9/// All cursor positions are byte offsets that are guaranteed to sit on
10/// UTF-8 char boundaries. Methods navigate by whole characters, not
11/// raw bytes, so multi-byte input (emoji, CJK, accented chars) is safe.
12pub struct InputBuffer {
13    /// User input buffer
14    pub content: String,
15    /// Cursor position as a **byte offset** (always on a char boundary)
16    pub cursor_position: usize,
17    /// Input history for arrow key navigation (persisted across sessions)
18    pub history: VecDeque<String>,
19    /// Current position in history (None = editing current input, Some(i) = viewing history[i])
20    pub history_index: Option<usize>,
21    /// Saved input when navigating away from current draft
22    pub history_buffer: String,
23}
24
25impl InputBuffer {
26    /// Create a new empty input buffer
27    pub fn new() -> Self {
28        Self {
29            content: String::new(),
30            cursor_position: 0,
31            history: VecDeque::new(),
32            history_index: None,
33            history_buffer: String::new(),
34        }
35    }
36
37    /// Load persisted input history (from a resumed conversation)
38    pub fn load_history(&mut self, history: VecDeque<String>) {
39        self.history = history;
40    }
41
42    /// Clear the input buffer
43    pub fn clear(&mut self) {
44        self.content.clear();
45        self.cursor_position = 0;
46    }
47
48    /// Check if input is empty
49    pub fn is_empty(&self) -> bool {
50        self.content.is_empty()
51    }
52
53    /// Get the input content
54    pub fn get(&self) -> &str {
55        &self.content
56    }
57
58    /// Set the input content
59    pub fn set(&mut self, content: impl Into<String>) {
60        self.content = content.into();
61        self.cursor_position = self.content.len();
62    }
63
64    /// Insert a character at cursor position
65    pub fn insert(&mut self, c: char) {
66        self.content.insert(self.cursor_position, c);
67        self.cursor_position += c.len_utf8();
68    }
69
70    /// Insert a string at cursor position
71    pub fn insert_str(&mut self, s: &str) {
72        self.content.insert_str(self.cursor_position, s);
73        self.cursor_position += s.len();
74    }
75
76    /// Delete character before cursor (backspace)
77    pub fn backspace(&mut self) -> bool {
78        if self.cursor_position > 0 {
79            // Find the start of the previous character
80            let prev_boundary = self.content[..self.cursor_position]
81                .char_indices()
82                .next_back()
83                .map(|(idx, _)| idx)
84                .unwrap_or(0);
85            self.content.remove(prev_boundary);
86            self.cursor_position = prev_boundary;
87            true
88        } else {
89            false
90        }
91    }
92
93    /// Delete character at cursor (delete key)
94    pub fn delete(&mut self) -> bool {
95        if self.cursor_position < self.content.len() {
96            self.content.remove(self.cursor_position);
97            true
98        } else {
99            false
100        }
101    }
102
103    /// Move cursor left by one character
104    pub fn move_left(&mut self) {
105        if self.cursor_position > 0 {
106            // Find the previous char boundary
107            self.cursor_position = self.content[..self.cursor_position]
108                .char_indices()
109                .next_back()
110                .map(|(idx, _)| idx)
111                .unwrap_or(0);
112        }
113    }
114
115    /// Move cursor right by one character
116    pub fn move_right(&mut self) {
117        if self.cursor_position < self.content.len() {
118            // Find the next char boundary
119            self.cursor_position = self.content[self.cursor_position..]
120                .char_indices()
121                .nth(1)
122                .map(|(idx, _)| self.cursor_position + idx)
123                .unwrap_or(self.content.len());
124        }
125    }
126
127    /// Move cursor to start
128    pub fn move_home(&mut self) {
129        self.cursor_position = 0;
130    }
131
132    /// Move cursor to end
133    pub fn move_end(&mut self) {
134        self.cursor_position = self.content.len();
135    }
136}
137
138impl Default for InputBuffer {
139    fn default() -> Self {
140        Self::new()
141    }
142}