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