Skip to main content

stynx_code_tui/state/
input_state.rs

1#[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}