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