stynx_code_tui/state/
input_state.rs1#[derive(Debug, Clone, Copy, PartialEq, Eq)]
2pub enum InputMode {
3 Insert,
4 Normal,
5}
6
7pub struct InputState {
8 pub buffer: String,
9 pub cursor_pos: usize,
10 pub mode: InputMode,
11 pub history: Vec<String>,
12 pub history_index: Option<usize>,
13 pub suggestion: String,
14 pub pasted_buffer: Option<String>,
16 pub pasted_images: Vec<std::path::PathBuf>,
20 pub slash_matches: Vec<(String, String)>,
22 pub slash_selected: usize,
23}
24
25impl InputState {
26 pub fn new() -> Self {
27 Self {
28 buffer: String::new(),
29 cursor_pos: 0,
30 mode: InputMode::Insert,
31 history: Vec::new(),
32 history_index: None,
33 suggestion: String::new(),
34 pasted_buffer: None,
35 pasted_images: Vec::new(),
36 slash_matches: Vec::new(),
37 slash_selected: 0,
38 }
39 }
40
41 pub fn insert_image_paste(&mut self, path: std::path::PathBuf) -> usize {
44 self.pasted_images.push(path);
45 let idx = self.pasted_images.len();
46 let token = format!("[Image #{idx}]");
47 for c in token.chars() {
48 self.insert_char(c);
49 }
50 idx
51 }
52
53 pub fn expand_for_submit(&self) -> String {
58 let mut out = self.buffer.clone();
59 if let Some(real) = &self.pasted_buffer {
60 if let Some(start) = out.find("[Pasted ") {
61 if let Some(rel_end) = out[start..].find(']') {
62 let end = start + rel_end + 1;
63 let mut s = String::with_capacity(out.len() + real.len());
64 s.push_str(&out[..start]);
65 s.push_str(real);
66 s.push_str(&out[end..]);
67 out = s;
68 }
69 }
70 }
71 for (i, path) in self.pasted_images.iter().enumerate() {
72 let token = format!("[Image #{}]", i + 1);
73 let replacement = format!("@{}", path.display());
74 out = out.replace(&token, &replacement);
75 }
76 out
77 }
78
79 pub fn complete_suggestion(&mut self) {
80 if !self.suggestion.is_empty() {
81 self.buffer.push_str(&self.suggestion);
82 self.cursor_pos = self.buffer.len();
83 self.suggestion.clear();
84 }
85 }
86
87 pub fn insert_char(&mut self, c: char) {
88 if self.cursor_pos >= self.buffer.len() {
89 self.buffer.push(c);
90 } else {
91 self.buffer.insert(self.cursor_pos, c);
92 }
93 self.cursor_pos += c.len_utf8();
94 }
95
96 pub fn delete_char(&mut self) {
97 if self.cursor_pos > 0 {
98 let prev = self.buffer[..self.cursor_pos]
99 .char_indices()
100 .next_back()
101 .map(|(i, _)| i)
102 .unwrap_or(0);
103 self.buffer.drain(prev..self.cursor_pos);
104 self.cursor_pos = prev;
105 }
106 }
107
108 pub fn delete_char_forward(&mut self) {
109 if self.cursor_pos < self.buffer.len() {
110 let next = self.buffer[self.cursor_pos..]
111 .char_indices()
112 .nth(1)
113 .map(|(i, _)| self.cursor_pos + i)
114 .unwrap_or(self.buffer.len());
115 self.buffer.drain(self.cursor_pos..next);
116 }
117 }
118
119 pub fn move_cursor_left(&mut self) {
120 if self.cursor_pos > 0 {
121 self.cursor_pos = self.buffer[..self.cursor_pos]
122 .char_indices()
123 .next_back()
124 .map(|(i, _)| i)
125 .unwrap_or(0);
126 }
127 }
128
129 pub fn move_cursor_right(&mut self) {
130 if self.cursor_pos < self.buffer.len() {
131 self.cursor_pos = self.buffer[self.cursor_pos..]
132 .char_indices()
133 .nth(1)
134 .map(|(i, _)| self.cursor_pos + i)
135 .unwrap_or(self.buffer.len());
136 }
137 }
138
139 pub fn move_word_left(&mut self) {
140 let s = &self.buffer[..self.cursor_pos];
141 let trimmed = s.trim_end_matches(|c: char| !c.is_alphanumeric());
142 let word_end = trimmed.rfind(|c: char| !c.is_alphanumeric()).map(|i| i + 1).unwrap_or(0);
143 self.cursor_pos = word_end;
144 }
145
146 pub fn move_word_right(&mut self) {
147 let s = &self.buffer[self.cursor_pos..];
148 let skip = s.find(|c: char| c.is_alphanumeric()).unwrap_or(s.len());
149 let after = &s[skip..];
150 let word_end = after.find(|c: char| !c.is_alphanumeric()).unwrap_or(after.len());
151 self.cursor_pos = (self.cursor_pos + skip + word_end).min(self.buffer.len());
152 }
153
154 pub fn history_prev(&mut self) {
155 if self.history.is_empty() { return; }
156 let idx = match self.history_index {
157 None => self.history.len() - 1,
158 Some(i) if i > 0 => i - 1,
159 Some(i) => i,
160 };
161 self.history_index = Some(idx);
162 self.buffer = self.history[idx].clone();
163 self.cursor_pos = self.buffer.len();
164 }
165
166 pub fn history_next(&mut self) {
167 match self.history_index {
168 None => {}
169 Some(i) if i + 1 < self.history.len() => {
170 let idx = i + 1;
171 self.history_index = Some(idx);
172 self.buffer = self.history[idx].clone();
173 self.cursor_pos = self.buffer.len();
174 }
175 _ => { self.history_index = None; self.buffer.clear(); self.cursor_pos = 0; }
176 }
177 }
178
179 pub fn push_history(&mut self, text: String) {
180 if !text.is_empty() && self.history.last().map(|s| s.as_str()) != Some(&text) {
181 self.history.push(text);
182 }
183 self.history_index = None;
184 }
185
186 pub fn clear(&mut self) {
187 self.buffer.clear();
188 self.cursor_pos = 0;
189 self.history_index = None;
190 self.pasted_buffer = None;
191 self.pasted_images.clear();
192 }
193
194 pub fn get_display_text(&self) -> &str { &self.buffer }
195
196 pub fn cursor_line_col(&self) -> (usize, usize) {
200 use unicode_width::UnicodeWidthChar;
201 let mut line = 0;
202 let mut last_nl = 0;
203 for (i, b) in self.buffer.as_bytes().iter().enumerate() {
204 if i >= self.cursor_pos { break; }
205 if *b == b'\n' {
206 line += 1;
207 last_nl = i + 1;
208 }
209 }
210 let cursor = self.cursor_pos.min(self.buffer.len());
211 let col: usize = self.buffer[last_nl..cursor]
212 .chars()
213 .map(|c| c.width().unwrap_or(0))
214 .sum();
215 (line, col)
216 }
217
218 pub fn line_count(&self) -> usize {
219 self.buffer.lines().count().max(1)
220 + if self.buffer.ends_with('\n') { 1 } else { 0 }
221 }
222
223 pub fn cursor_up_line(&mut self) -> bool {
224 let (line, col) = self.cursor_line_col();
225 if line == 0 { return false; }
226 let lines: Vec<&str> = self.buffer.split('\n').collect();
227 let target_line = line - 1;
228 let target_col = col.min(lines[target_line].chars().count());
229 let mut pos = 0usize;
230 for l in lines.iter().take(target_line) {
231 pos += l.len() + 1;
232 }
233 pos += lines[target_line]
234 .char_indices()
235 .nth(target_col)
236 .map(|(i, _)| i)
237 .unwrap_or_else(|| lines[target_line].len());
238 self.cursor_pos = pos;
239 true
240 }
241
242 pub fn cursor_down_line(&mut self) -> bool {
243 let (line, col) = self.cursor_line_col();
244 let lines: Vec<&str> = self.buffer.split('\n').collect();
245 if line + 1 >= lines.len() { return false; }
246 let target_line = line + 1;
247 let target_col = col.min(lines[target_line].chars().count());
248 let mut pos = 0usize;
249 for l in lines.iter().take(target_line) {
250 pos += l.len() + 1;
251 }
252 pos += lines[target_line]
253 .char_indices()
254 .nth(target_col)
255 .map(|(i, _)| i)
256 .unwrap_or_else(|| lines[target_line].len());
257 self.cursor_pos = pos;
258 true
259 }
260}
261
262impl Default for InputState {
263 fn default() -> Self { Self::new() }
264}