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 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 reset_prompt(&mut self) {
169 self.prompt = Span::from(self.config.prompt.clone());
170 }
171
172 pub fn set_at_visual_offset(&mut self, visual_offset: u16) {
174 let mut current_width = 0;
175 let mut target_cursor = self.before;
176
177 for (i, &(_, width)) in self.graphemes.iter().enumerate().skip(self.before) {
178 if current_width + width > visual_offset {
179 if visual_offset - current_width > width / 2 {
181 target_cursor = i + 1;
182 } else {
183 target_cursor = i;
184 }
185 break;
186 }
187 current_width += width;
188 target_cursor = i + 1;
189 }
190
191 self.cursor = target_cursor;
192 }
193
194 pub fn forward_char(&mut self) {
196 if self.cursor < self.graphemes.len() {
197 self.cursor += 1;
198 }
199 }
200 pub fn backward_char(&mut self) {
201 if self.cursor > 0 {
202 self.cursor -= 1;
203 }
204 }
205
206 pub fn forward_word(&mut self) {
207 let mut in_word = false;
208 while self.cursor < self.graphemes.len() {
209 let byte_start = self.graphemes[self.cursor].0;
210 let byte_end = self
211 .graphemes
212 .get(self.cursor + 1)
213 .map(|(idx, _)| *idx)
214 .unwrap_or(self.input.len());
215 let g = &self.input[byte_start..byte_end];
216
217 if g.chars().all(|c| c.is_whitespace()) {
218 if in_word {
219 break;
220 }
221 } else {
222 in_word = true;
223 }
224 self.cursor += 1;
225 }
226 }
227
228 pub fn backward_word(&mut self) {
229 let mut in_word = false;
230 while self.cursor > 0 {
231 let byte_start = self.graphemes[self.cursor - 1].0;
232 let byte_end = self
233 .graphemes
234 .get(self.cursor)
235 .map(|(idx, _)| *idx)
236 .unwrap_or(self.input.len());
237 let g = &self.input[byte_start..byte_end];
238
239 if g.chars().all(|c| c.is_whitespace()) {
240 if in_word {
241 break;
242 }
243 } else {
244 in_word = true;
245 }
246 self.cursor -= 1;
247 }
248 }
249
250 pub fn delete(&mut self) {
251 if self.cursor > 0 {
252 let start = self.graphemes[self.cursor - 1].0;
253 let end = self.byte_index(self.cursor);
254 self.input.replace_range(start..end, "");
255 self.recompute_graphemes();
256 self.cursor -= 1;
257 }
258 }
259
260 pub fn delete_word(&mut self) {
261 let old_cursor = self.cursor;
262 self.backward_word();
263 let new_cursor = self.cursor;
264
265 let start = self.byte_index(new_cursor);
266 let end = self.byte_index(old_cursor);
267 self.input.replace_range(start..end, "");
268 self.recompute_graphemes();
269 }
270
271 pub fn delete_line_start(&mut self) {
272 let end = self.byte_index(self.cursor);
273 self.input.replace_range(0..end, "");
274 self.recompute_graphemes();
275 self.cursor = 0;
276 self.before = 0;
277 }
278
279 pub fn delete_line_end(&mut self) {
280 let start = self.byte_index(self.cursor);
281 self.input.truncate(start);
282 self.recompute_graphemes();
283 }
284
285 pub fn make_input(&self) -> Paragraph<'_> {
288 let mut visible_width = 0;
289 let mut end_idx = self.before;
290
291 while end_idx < self.graphemes.len() {
292 let g_width = self.graphemes[end_idx].1;
293 if self.width != 0 && visible_width + g_width > self.width {
294 break;
295 }
296 visible_width += g_width;
297 end_idx += 1;
298 }
299
300 let start_byte = self.byte_index(self.before);
301 let end_byte = self.byte_index(end_idx);
302 let visible_input = &self.input[start_byte..end_byte];
303
304 let line = Line::from(vec![
305 self.prompt.clone(),
306 Span::raw(visible_input)
307 .style(self.config.fg)
308 .add_modifier(self.config.modifier),
309 ]);
310
311 Paragraph::new(line).block(self.config.border.as_block())
312 }
313}