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 push_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 scroll_to_cursor(&mut self) {
129 if self.width == 0 {
130 return;
131 }
132 let padding = self.config.scroll_padding as usize;
133
134 if self.before >= self.cursor {
136 self.before = self.cursor.saturating_sub(padding);
137 return;
138 }
139
140 loop {
142 let visual_dist: u16 = self.graphemes
143 [self.before..=(self.cursor + padding).min(self.graphemes.len().saturating_sub(1))]
144 .iter()
145 .map(|(_, w)| *w)
146 .sum();
147
148 if visual_dist <= self.width {
151 break;
152 }
153
154 if self.before < self.cursor {
155 self.before += 1;
156 } else {
157 break;
159 }
160 }
161 }
162
163 pub fn cancel(&mut self) {
164 self.input.clear();
165 self.graphemes.clear();
166 self.cursor = 0;
167 self.before = 0;
168 }
169
170 pub fn prepare_column_change(&mut self) {
171 let trimmed = self.input.trim_end();
172 if let Some(pos) = trimmed.rfind(' ') {
173 let last_word = &trimmed[pos + 1..];
174 if last_word.starts_with('%') {
175 let bytes = trimmed[..pos].len();
176 self.input.truncate(bytes);
177 }
178 } else if trimmed.starts_with('%') {
179 self.input.clear();
180 }
181
182 if !self.input.is_empty() && !self.input.ends_with(' ') {
183 self.input.push(' ');
184 }
185 self.recompute_graphemes();
186 self.cursor = self.graphemes.len();
187 }
188
189 pub fn set_at_visual_offset(&mut self, visual_offset: u16) {
191 let mut current_width = 0;
192 let mut target_cursor = self.before;
193
194 for (i, &(_, width)) in self.graphemes.iter().enumerate().skip(self.before) {
195 if current_width + width > visual_offset {
196 if visual_offset - current_width > width / 2 {
198 target_cursor = i + 1;
199 } else {
200 target_cursor = i;
201 }
202 break;
203 }
204 current_width += width;
205 target_cursor = i + 1;
206 }
207
208 self.cursor = target_cursor;
209 }
210
211 pub fn forward_char(&mut self) {
213 if self.cursor < self.graphemes.len() {
214 self.cursor += 1;
215 }
216 }
217 pub fn backward_char(&mut self) {
218 if self.cursor > 0 {
219 self.cursor -= 1;
220 }
221 }
222
223 pub fn forward_word(&mut self) {
224 let mut in_word = false;
225 while self.cursor < self.graphemes.len() {
226 let byte_start = self.graphemes[self.cursor].0;
227 let byte_end = self
228 .graphemes
229 .get(self.cursor + 1)
230 .map(|(idx, _)| *idx)
231 .unwrap_or(self.input.len());
232 let g = &self.input[byte_start..byte_end];
233
234 if g.chars().all(|c| c.is_whitespace()) {
235 if in_word {
236 break;
237 }
238 } else {
239 in_word = true;
240 }
241 self.cursor += 1;
242 }
243 }
244
245 pub fn backward_word(&mut self) {
246 let mut in_word = false;
247 while self.cursor > 0 {
248 let byte_start = self.graphemes[self.cursor - 1].0;
249 let byte_end = self
250 .graphemes
251 .get(self.cursor)
252 .map(|(idx, _)| *idx)
253 .unwrap_or(self.input.len());
254 let g = &self.input[byte_start..byte_end];
255
256 if g.chars().all(|c| c.is_whitespace()) {
257 if in_word {
258 break;
259 }
260 } else {
261 in_word = true;
262 }
263 self.cursor -= 1;
264 }
265 }
266
267 pub fn delete(&mut self) {
268 if self.cursor > 0 {
269 let start = self.graphemes[self.cursor - 1].0;
270 let end = self.byte_index(self.cursor);
271 self.input.replace_range(start..end, "");
272 self.recompute_graphemes();
273 self.cursor -= 1;
274 }
275 }
276
277 pub fn delete_word(&mut self) {
278 let old_cursor = self.cursor;
279 self.backward_word();
280 let new_cursor = self.cursor;
281
282 let start = self.byte_index(new_cursor);
283 let end = self.byte_index(old_cursor);
284 self.input.replace_range(start..end, "");
285 self.recompute_graphemes();
286 }
287
288 pub fn delete_line_start(&mut self) {
289 let end = self.byte_index(self.cursor);
290 self.input.replace_range(0..end, "");
291 self.recompute_graphemes();
292 self.cursor = 0;
293 self.before = 0;
294 }
295
296 pub fn delete_line_end(&mut self) {
297 let start = self.byte_index(self.cursor);
298 self.input.truncate(start);
299 self.recompute_graphemes();
300 }
301
302 pub fn make_input(&self) -> Paragraph<'_> {
305 let mut visible_width = 0;
306 let mut end_idx = self.before;
307
308 while end_idx < self.graphemes.len() {
309 let g_width = self.graphemes[end_idx].1;
310 if self.width != 0 && visible_width + g_width > self.width {
311 break;
312 }
313 visible_width += g_width;
314 end_idx += 1;
315 }
316
317 let start_byte = self.byte_index(self.before);
318 let end_byte = self.byte_index(end_idx);
319 let visible_input = &self.input[start_byte..end_byte];
320
321 let mut line = self.prompt.clone();
322 line.push_span(Span::styled(visible_input, self.config.text_style()));
323
324 Paragraph::new(line).block(self.config.border.as_block())
325 }
326
327 pub fn set_prompt(&mut self, template: Option<Line<'static>>) {
329 let line = template
330 .unwrap_or_else(|| self.config.prompt.clone().into())
331 .style(self.config.prompt_style());
332 self.set_prompt_line(line);
333 }
334
335 pub fn set_prompt_line(&mut self, prompt: Line<'static>) {
337 let old_width = self.prompt.to_string().width();
338 let new_width = prompt.to_string().width();
339
340 if new_width > old_width {
341 self.width = self.width.saturating_sub((new_width - old_width) as u16);
342 } else if old_width > new_width {
343 self.width += (old_width - new_width) as u16;
344 }
345
346 self.prompt = prompt;
347 }
348}