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