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