1use ratatui::{
2 layout::{Position, Rect},
3 text::{Line, Span},
4 widgets::Paragraph,
5};
6use unicode_segmentation::UnicodeSegmentation;
7use unicode_width::UnicodeWidthStr;
8
9use crate::config::InputConfig;
10
11#[derive(Debug)]
12pub struct InputUI {
13 cursor: usize, pub input: String, graphemes: Vec<(usize, u16)>,
17 prompt: Line<'static>,
18 before: usize, width: u16, pub config: InputConfig,
22}
23
24impl InputUI {
25 pub fn new(config: InputConfig) -> Self {
26 let mut ui = Self {
27 cursor: 0,
28 input: "".into(),
29 graphemes: Vec::new(),
30 prompt: Line::styled(config.prompt.clone(), config.prompt_style()),
31 config,
32 before: 0,
33 width: 0,
34 };
35
36 if !ui.config.initial.is_empty() {
37 ui.input = ui.config.initial.clone();
38 ui.recompute_graphemes();
39 ui.cursor = ui.graphemes.len();
40 }
41
42 ui
43 }
44
45 pub fn recompute_graphemes(&mut self) {
47 self.graphemes = self
48 .input
49 .grapheme_indices(true)
50 .map(|(idx, g)| (idx, g.width() as u16))
51 .collect();
52 }
53
54 pub fn byte_index(&self, grapheme_idx: usize) -> usize {
55 self.graphemes
56 .get(grapheme_idx)
57 .map(|(idx, _)| *idx)
58 .unwrap_or(self.input.len())
59 }
60
61 pub fn str_at_cursor(&self) -> &str {
62 &self.input[..self.byte_index(self.cursor)]
63 }
64
65 pub fn len(&self) -> usize {
68 self.input.len()
69 }
70 pub fn is_empty(&self) -> bool {
71 self.input.is_empty()
72 }
73
74 pub fn cursor(&self) -> u16 {
76 self.cursor as u16
77 }
78
79 pub fn left(&self) -> u16 {
80 self.config.border.left() + self.prompt.width() as u16
81 }
82
83 pub fn cursor_offset(&self, rect: &Rect) -> Position {
85 let top = self.config.border.top();
86
87 let offset_x: u16 = self.graphemes[self.before..self.cursor]
88 .iter()
89 .map(|(_, w)| *w)
90 .sum();
91
92 Position::new(rect.x + self.left() + offset_x, rect.y + top)
93 }
94
95 pub fn update_width(&mut self, width: u16) {
97 let text_width = width
98 .saturating_sub(self.prompt.width() as u16)
99 .saturating_sub(self.config.border.width());
100 if self.width != text_width {
101 self.width = text_width;
102 }
103 }
104
105 pub fn set(&mut self, input: impl Into<Option<String>>, cursor: u16) {
106 if let Some(input) = input.into() {
107 self.input = input;
108 self.recompute_graphemes();
109 }
110 self.cursor = (cursor as usize).min(self.graphemes.len());
111 }
112
113 pub fn push_char(&mut self, c: char) {
114 let byte_idx = self.byte_index(self.cursor);
115 self.input.insert(byte_idx, c);
116 self.recompute_graphemes();
117 self.cursor += 1;
118 }
119
120 pub fn insert_str(&mut self, content: &str) {
121 let byte_idx = self.byte_index(self.cursor);
122 self.input.insert_str(byte_idx, content);
123 let added_graphemes = content.graphemes(true).count();
124 self.recompute_graphemes();
125 self.cursor += added_graphemes;
126 }
127
128 pub fn push_str(&mut self, content: &str) {
129 self.input.push_str(content);
130 self.recompute_graphemes();
131 self.cursor = self.graphemes.len();
132 }
133
134 pub fn scroll_to_cursor(&mut self) {
135 if self.width == 0 {
136 return;
137 }
138 let padding = self.config.scroll_padding as usize;
139
140 if self.before >= self.cursor {
142 self.before = self.cursor.saturating_sub(padding);
143 return;
144 }
145
146 loop {
148 let visual_dist: u16 = self.graphemes
149 [self.before..=(self.cursor + padding).min(self.graphemes.len().saturating_sub(1))]
150 .iter()
151 .map(|(_, w)| *w)
152 .sum();
153
154 if visual_dist <= self.width {
157 break;
158 }
159
160 if self.before < self.cursor {
161 self.before += 1;
162 } else {
163 break;
165 }
166 }
167 }
168
169 pub fn cancel(&mut self) {
170 self.input.clear();
171 self.graphemes.clear();
172 self.cursor = 0;
173 self.before = 0;
174 }
175
176 pub fn prepare_column_change(&mut self) {
177 let trimmed = self.input.trim_end();
178 if let Some(pos) = trimmed.rfind(' ') {
179 let last_word = &trimmed[pos + 1..];
180 if last_word.starts_with('%') {
181 let bytes = trimmed[..pos].len();
182 self.input.truncate(bytes);
183 }
184 } else if trimmed.starts_with('%') {
185 self.input.clear();
186 }
187
188 if !self.input.is_empty() && !self.input.ends_with(' ') {
189 self.input.push(' ');
190 }
191 self.recompute_graphemes();
192 self.cursor = self.graphemes.len();
193 }
194
195 pub fn set_at_visual_offset(&mut self, visual_offset: u16) {
197 let mut current_width = 0;
198 let mut target_cursor = self.before;
199
200 for (i, &(_, width)) in self.graphemes.iter().enumerate().skip(self.before) {
201 if current_width + width > visual_offset {
202 if visual_offset - current_width > width / 2 {
204 target_cursor = i + 1;
205 } else {
206 target_cursor = i;
207 }
208 break;
209 }
210 current_width += width;
211 target_cursor = i + 1;
212 }
213
214 self.cursor = target_cursor;
215 }
216
217 pub fn forward_char(&mut self) {
219 if self.cursor < self.graphemes.len() {
220 self.cursor += 1;
221 }
222 }
223 pub fn backward_char(&mut self) {
224 if self.cursor > 0 {
225 self.cursor -= 1;
226 }
227 }
228
229 pub fn forward_word(&mut self) {
230 let mut in_word = false;
231 while self.cursor < self.graphemes.len() {
232 let byte_start = self.graphemes[self.cursor].0;
233 let byte_end = self
234 .graphemes
235 .get(self.cursor + 1)
236 .map(|(idx, _)| *idx)
237 .unwrap_or(self.input.len());
238 let g = &self.input[byte_start..byte_end];
239
240 if g.chars().all(|c| c.is_whitespace()) {
241 if in_word {
242 break;
243 }
244 } else {
245 in_word = true;
246 }
247 self.cursor += 1;
248 }
249 }
250
251 pub fn backward_word(&mut self) {
252 let mut in_word = false;
253 while self.cursor > 0 {
254 let byte_start = self.graphemes[self.cursor - 1].0;
255 let byte_end = self
256 .graphemes
257 .get(self.cursor)
258 .map(|(idx, _)| *idx)
259 .unwrap_or(self.input.len());
260 let g = &self.input[byte_start..byte_end];
261
262 if g.chars().all(|c| c.is_whitespace()) {
263 if in_word {
264 break;
265 }
266 } else {
267 in_word = true;
268 }
269 self.cursor -= 1;
270 }
271 }
272
273 pub fn delete(&mut self) {
274 if self.cursor > 0 {
275 let start = self.graphemes[self.cursor - 1].0;
276 let end = self.byte_index(self.cursor);
277 self.input.replace_range(start..end, "");
278 self.recompute_graphemes();
279 self.cursor -= 1;
280 }
281 }
282
283 pub fn delete_word(&mut self) {
284 let old_cursor = self.cursor;
285 self.backward_word();
286 let new_cursor = self.cursor;
287
288 let start = self.byte_index(new_cursor);
289 let end = self.byte_index(old_cursor);
290 self.input.replace_range(start..end, "");
291 self.recompute_graphemes();
292 }
293
294 pub fn delete_line_start(&mut self) {
295 let end = self.byte_index(self.cursor);
296 self.input.replace_range(0..end, "");
297 self.recompute_graphemes();
298 self.cursor = 0;
299 self.before = 0;
300 }
301
302 pub fn delete_line_end(&mut self) {
303 let start = self.byte_index(self.cursor);
304 self.input.truncate(start);
305 self.recompute_graphemes();
306 }
307
308 pub fn make_input(&self) -> Paragraph<'_> {
311 let mut visible_width = 0;
312 let mut end_idx = self.before;
313
314 while end_idx < self.graphemes.len() {
315 let g_width = self.graphemes[end_idx].1;
316 if self.width != 0 && visible_width + g_width > self.width {
317 break;
318 }
319 visible_width += g_width;
320 end_idx += 1;
321 }
322
323 let start_byte = self.byte_index(self.before);
324 let end_byte = self.byte_index(end_idx);
325 let visible_input = &self.input[start_byte..end_byte];
326
327 let mut line = self.prompt.clone();
328 line.push_span(Span::styled(visible_input, self.config.text_style()));
329
330 Paragraph::new(line).block(self.config.border.as_block())
331 }
332
333 pub fn set_prompt(&mut self, template: Option<Line<'static>>) {
335 let line = template
336 .unwrap_or_else(|| self.config.prompt.clone().into())
337 .style(self.config.prompt_style());
338 self.set_prompt_line(line);
339 }
340
341 pub fn set_prompt_line(&mut self, prompt: Line<'static>) {
343 let old_width = self.prompt.to_string().width();
344 let new_width = prompt.to_string().width();
345
346 if new_width > old_width {
347 self.width = self.width.saturating_sub((new_width - old_width) as u16);
348 } else if old_width > new_width {
349 self.width += (old_width - new_width) as u16;
350 }
351
352 self.prompt = prompt;
353 }
354}