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 len(&self) -> usize {
64 self.input.len()
65 }
66 pub fn is_empty(&self) -> bool {
67 self.input.is_empty()
68 }
69
70 pub fn cursor(&self) -> u16 {
72 self.cursor as u16
73 }
74
75 pub fn left(&self) -> u16 {
76 self.config.border.left() + self.prompt.width() as u16
77 }
78
79 pub fn cursor_offset(&self, rect: &Rect) -> Position {
81 let top = self.config.border.top();
82
83 let offset_x: u16 = self.graphemes[self.before..self.cursor]
84 .iter()
85 .map(|(_, w)| *w)
86 .sum();
87
88 Position::new(rect.x + self.left() + offset_x, rect.y + top)
89 }
90
91 pub fn update_width(&mut self, width: u16) {
93 let text_width = width
94 .saturating_sub(self.prompt.width() as u16)
95 .saturating_sub(self.config.border.width());
96 if self.width != text_width {
97 self.width = text_width;
98 }
99 }
100
101 pub fn set(&mut self, input: impl Into<Option<String>>, cursor: u16) {
102 if let Some(input) = input.into() {
103 self.input = input;
104 self.recompute_graphemes();
105 }
106 self.cursor = (cursor as usize).min(self.graphemes.len());
107 }
108
109 pub fn push_char(&mut self, c: char) {
110 let byte_idx = self.byte_index(self.cursor);
111 self.input.insert(byte_idx, c);
112 self.recompute_graphemes();
113 self.cursor += 1;
114 }
115
116 pub fn push_str(&mut self, content: &str) {
117 let byte_idx = self.byte_index(self.cursor);
118 self.input.insert_str(byte_idx, content);
119 let added_graphemes = content.graphemes(true).count();
120 self.recompute_graphemes();
121 self.cursor += added_graphemes;
122 }
123
124 pub fn scroll_to_cursor(&mut self) {
125 if self.width == 0 {
126 return;
127 }
128 let padding = self.config.scroll_padding as usize;
129
130 if self.before >= self.cursor {
132 self.before = self.cursor.saturating_sub(padding);
133 return;
134 }
135
136 loop {
138 let visual_dist: u16 = self.graphemes
139 [self.before..=(self.cursor + padding).min(self.graphemes.len().saturating_sub(1))]
140 .iter()
141 .map(|(_, w)| *w)
142 .sum();
143
144 if visual_dist <= self.width {
147 break;
148 }
149
150 if self.before < self.cursor {
151 self.before += 1;
152 } else {
153 break;
155 }
156 }
157 }
158
159 pub fn cancel(&mut self) {
160 self.input.clear();
161 self.graphemes.clear();
162 self.cursor = 0;
163 self.before = 0;
164 }
165
166 pub fn prepare_column_change(&mut self) {
167 let trimmed = self.input.trim_end();
168 if let Some(pos) = trimmed.rfind(' ') {
169 let last_word = &trimmed[pos + 1..];
170 if last_word.starts_with('%') {
171 let bytes = trimmed[..pos].len();
172 self.input.truncate(bytes);
173 }
174 } else if trimmed.starts_with('%') {
175 self.input.clear();
176 }
177
178 if !self.input.is_empty() && !self.input.ends_with(' ') {
179 self.input.push(' ');
180 }
181 self.recompute_graphemes();
182 self.cursor = self.graphemes.len();
183 }
184
185 pub fn set_at_visual_offset(&mut self, visual_offset: u16) {
187 let mut current_width = 0;
188 let mut target_cursor = self.before;
189
190 for (i, &(_, width)) in self.graphemes.iter().enumerate().skip(self.before) {
191 if current_width + width > visual_offset {
192 if visual_offset - current_width > width / 2 {
194 target_cursor = i + 1;
195 } else {
196 target_cursor = i;
197 }
198 break;
199 }
200 current_width += width;
201 target_cursor = i + 1;
202 }
203
204 self.cursor = target_cursor;
205 }
206
207 pub fn forward_char(&mut self) {
209 if self.cursor < self.graphemes.len() {
210 self.cursor += 1;
211 }
212 }
213 pub fn backward_char(&mut self) {
214 if self.cursor > 0 {
215 self.cursor -= 1;
216 }
217 }
218
219 pub fn forward_word(&mut self) {
220 let mut in_word = false;
221 while self.cursor < self.graphemes.len() {
222 let byte_start = self.graphemes[self.cursor].0;
223 let byte_end = self
224 .graphemes
225 .get(self.cursor + 1)
226 .map(|(idx, _)| *idx)
227 .unwrap_or(self.input.len());
228 let g = &self.input[byte_start..byte_end];
229
230 if g.chars().all(|c| c.is_whitespace()) {
231 if in_word {
232 break;
233 }
234 } else {
235 in_word = true;
236 }
237 self.cursor += 1;
238 }
239 }
240
241 pub fn backward_word(&mut self) {
242 let mut in_word = false;
243 while self.cursor > 0 {
244 let byte_start = self.graphemes[self.cursor - 1].0;
245 let byte_end = self
246 .graphemes
247 .get(self.cursor)
248 .map(|(idx, _)| *idx)
249 .unwrap_or(self.input.len());
250 let g = &self.input[byte_start..byte_end];
251
252 if g.chars().all(|c| c.is_whitespace()) {
253 if in_word {
254 break;
255 }
256 } else {
257 in_word = true;
258 }
259 self.cursor -= 1;
260 }
261 }
262
263 pub fn delete(&mut self) {
264 if self.cursor > 0 {
265 let start = self.graphemes[self.cursor - 1].0;
266 let end = self.byte_index(self.cursor);
267 self.input.replace_range(start..end, "");
268 self.recompute_graphemes();
269 self.cursor -= 1;
270 }
271 }
272
273 pub fn delete_word(&mut self) {
274 let old_cursor = self.cursor;
275 self.backward_word();
276 let new_cursor = self.cursor;
277
278 let start = self.byte_index(new_cursor);
279 let end = self.byte_index(old_cursor);
280 self.input.replace_range(start..end, "");
281 self.recompute_graphemes();
282 }
283
284 pub fn delete_line_start(&mut self) {
285 let end = self.byte_index(self.cursor);
286 self.input.replace_range(0..end, "");
287 self.recompute_graphemes();
288 self.cursor = 0;
289 self.before = 0;
290 }
291
292 pub fn delete_line_end(&mut self) {
293 let start = self.byte_index(self.cursor);
294 self.input.truncate(start);
295 self.recompute_graphemes();
296 }
297
298 pub fn make_input(&self) -> Paragraph<'_> {
301 let mut visible_width = 0;
302 let mut end_idx = self.before;
303
304 while end_idx < self.graphemes.len() {
305 let g_width = self.graphemes[end_idx].1;
306 if self.width != 0 && visible_width + g_width > self.width {
307 break;
308 }
309 visible_width += g_width;
310 end_idx += 1;
311 }
312
313 let start_byte = self.byte_index(self.before);
314 let end_byte = self.byte_index(end_idx);
315 let visible_input = &self.input[start_byte..end_byte];
316
317 let mut line = self.prompt.clone();
318 line.push_span(Span::styled(visible_input, self.config.text_style()));
319
320 Paragraph::new(line).block(self.config.border.as_block())
321 }
322
323 pub fn set_prompt(&mut self, template: Option<Line<'static>>) {
325 let line = template
326 .unwrap_or_else(|| self.config.prompt.clone().into())
327 .style(self.config.prompt_style());
328 self.set_prompt_line(line);
329 }
330
331 pub fn set_prompt_line(&mut self, prompt: Line<'static>) {
333 let old_width = self.prompt.to_string().width();
334 let new_width = prompt.to_string().width();
335
336 if new_width > old_width {
337 self.width = self.width.saturating_sub((new_width - old_width) as u16);
338 } else if old_width > new_width {
339 self.width += (old_width - new_width) as u16;
340 }
341
342 self.prompt = prompt;
343 }
344}