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 config: InputConfig,
19 pub prompt: Span<'static>,
20 before: usize, pub width: u16,
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: Span::from(config.prompt.clone()),
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 fn recompute_graphemes(&mut self) {
46 self.graphemes = self
47 .input
48 .grapheme_indices(true)
49 .map(|(idx, g)| (idx, g.width() as u16))
50 .collect();
51 }
52
53 fn byte_index(&self, grapheme_idx: usize) -> usize {
54 self.graphemes
55 .get(grapheme_idx)
56 .map(|(idx, _)| *idx)
57 .unwrap_or(self.input.len())
58 }
59
60 pub fn len(&self) -> usize {
63 self.input.len()
64 }
65 pub fn is_empty(&self) -> bool {
66 self.input.is_empty()
67 }
68
69 pub fn cursor(&self) -> u16 {
71 self.cursor as u16
72 }
73
74 pub fn cursor_offset(&self, rect: &Rect) -> Position {
76 let left = self.config.border.left();
77 let top = self.config.border.top();
78
79 let offset_x: u16 = self.graphemes[self.before..self.cursor]
80 .iter()
81 .map(|(_, w)| *w)
82 .sum();
83
84 Position::new(
85 rect.x + self.prompt.width() as u16 + left + offset_x,
86 rect.y + top,
87 )
88 }
89
90 pub fn push_char(&mut self, c: char) {
91 let byte_idx = self.byte_index(self.cursor);
92 self.input.insert(byte_idx, c);
93 self.recompute_graphemes();
94 self.cursor += 1;
95 }
96
97 pub fn push_str(&mut self, content: &str) {
98 let byte_idx = self.byte_index(self.cursor);
99 self.input.insert_str(byte_idx, content);
100 let added_graphemes = content.graphemes(true).count();
101 self.recompute_graphemes();
102 self.cursor += added_graphemes;
103 }
104
105 pub fn set(&mut self, input: impl Into<Option<String>>, cursor: u16) {
107 if let Some(input) = input.into() {
108 self.input = input;
109 self.recompute_graphemes();
110 }
111 self.cursor = (cursor as usize).min(self.graphemes.len());
112 }
113
114 pub fn update_width(&mut self, width: u16) {
115 let text_width = width
116 .saturating_sub(self.prompt.width() as u16)
117 .saturating_sub(self.config.border.width());
118 if self.width != text_width {
119 self.width = text_width;
120 }
121 }
122
123 pub fn scroll_to_cursor(&mut self) {
124 if self.width == 0 {
125 return;
126 }
127 let padding = self.config.scroll_padding as usize;
128
129 if self.before >= self.cursor {
131 self.before = self.cursor.saturating_sub(padding);
132 return;
133 }
134
135 loop {
137 let visual_dist: u16 = self.graphemes
138 [self.before..=(self.cursor + padding).min(self.graphemes.len().saturating_sub(1))]
139 .iter()
140 .map(|(_, w)| *w)
141 .sum();
142
143 if visual_dist <= self.width {
146 break;
147 }
148
149 if self.before < self.cursor {
150 self.before += 1;
151 } else {
152 break;
154 }
155 }
156 }
157
158 pub fn set_at_visual_offset(&mut self, visual_offset: u16) {
159 let mut current_width = 0;
160 let mut target_cursor = self.before;
161
162 for (i, &(_, width)) in self.graphemes.iter().enumerate().skip(self.before) {
163 if current_width + width > visual_offset {
164 if visual_offset - current_width > width / 2 {
166 target_cursor = i + 1;
167 } else {
168 target_cursor = i;
169 }
170 break;
171 }
172 current_width += width;
173 target_cursor = i + 1;
174 }
175
176 self.cursor = target_cursor;
177 }
178
179 pub fn cancel(&mut self) {
180 self.input.clear();
181 self.graphemes.clear();
182 self.cursor = 0;
183 self.before = 0;
184 }
185 pub fn reset_prompt(&mut self) {
186 self.prompt = Span::from(self.config.prompt.clone());
187 }
188
189 pub fn forward_char(&mut self) {
191 if self.cursor < self.graphemes.len() {
192 self.cursor += 1;
193 }
194 }
195 pub fn backward_char(&mut self) {
196 if self.cursor > 0 {
197 self.cursor -= 1;
198 }
199 }
200
201 pub fn forward_word(&mut self) {
202 let mut in_word = false;
203 while self.cursor < self.graphemes.len() {
204 let byte_start = self.graphemes[self.cursor].0;
205 let byte_end = self
206 .graphemes
207 .get(self.cursor + 1)
208 .map(|(idx, _)| *idx)
209 .unwrap_or(self.input.len());
210 let g = &self.input[byte_start..byte_end];
211
212 if g.chars().all(|c| c.is_whitespace()) {
213 if in_word {
214 break;
215 }
216 } else {
217 in_word = true;
218 }
219 self.cursor += 1;
220 }
221 }
222
223 pub fn backward_word(&mut self) {
224 let mut in_word = false;
225 while self.cursor > 0 {
226 let byte_start = self.graphemes[self.cursor - 1].0;
227 let byte_end = self
228 .graphemes
229 .get(self.cursor)
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 delete(&mut self) {
246 if self.cursor > 0 {
247 let start = self.graphemes[self.cursor - 1].0;
248 let end = self.byte_index(self.cursor);
249 self.input.replace_range(start..end, "");
250 self.recompute_graphemes();
251 self.cursor -= 1;
252 }
253 }
254
255 pub fn delete_word(&mut self) {
256 let old_cursor = self.cursor;
257 self.backward_word();
258 let new_cursor = self.cursor;
259
260 let start = self.byte_index(new_cursor);
261 let end = self.byte_index(old_cursor);
262 self.input.replace_range(start..end, "");
263 self.recompute_graphemes();
264 }
265
266 pub fn delete_line_start(&mut self) {
267 let end = self.byte_index(self.cursor);
268 self.input.replace_range(0..end, "");
269 self.recompute_graphemes();
270 self.cursor = 0;
271 self.before = 0;
272 }
273
274 pub fn delete_line_end(&mut self) {
275 let start = self.byte_index(self.cursor);
276 self.input.truncate(start);
277 self.recompute_graphemes();
278 }
279
280 pub fn make_input(&self) -> Paragraph<'_> {
283 let mut visible_width = 0;
284 let mut end_idx = self.before;
285
286 while end_idx < self.graphemes.len() {
287 let g_width = self.graphemes[end_idx].1;
288 if visible_width + g_width > self.width {
289 break;
290 }
291 visible_width += g_width;
292 end_idx += 1;
293 }
294
295 let start_byte = self.byte_index(self.before);
296 let end_byte = self.byte_index(end_idx);
297 let visible_input = &self.input[start_byte..end_byte];
298
299 let line = Line::from(vec![
300 self.prompt.clone(),
301 Span::raw(visible_input)
302 .style(self.config.fg)
303 .add_modifier(self.config.modifier),
304 ]);
305
306 Paragraph::new(line).block(self.config.border.as_block())
307 }
308}