1use std::ops::{Deref, DerefMut};
2
3use ratatui::{
4 layout::{Position, Rect},
5 text::{Line, Span},
6 widgets::Paragraph,
7};
8use unicode_segmentation::UnicodeSegmentation;
9use unicode_width::UnicodeWidthStr;
10
11use crate::config::QueryConfig;
12
13#[derive(Debug, Default, Clone)]
14pub struct InputUI {
15 pub cursor: usize, pub input: String, pub graphemes: Vec<(usize, u16)>,
19 pub before: usize, pub width: u16, }
22
23impl InputUI {
24 pub fn new() -> Self {
25 Self::default()
26 }
27
28 pub fn recompute_graphemes(&mut self) {
30 self.graphemes = self
31 .input
32 .grapheme_indices(true)
33 .map(|(idx, g)| (idx, g.width() as u16))
34 .collect();
35 }
36
37 pub fn byte_index(&self, grapheme_idx: usize) -> usize {
38 self.graphemes
39 .get(grapheme_idx)
40 .map(|(idx, _)| *idx)
41 .unwrap_or(self.input.len())
42 }
43
44 pub fn str_at_cursor(&self) -> &str {
45 &self.input[..self.byte_index(self.cursor)]
46 }
47
48 pub fn len(&self) -> usize {
51 self.input.len()
52 }
53 pub fn is_empty(&self) -> bool {
54 self.input.is_empty()
55 }
56
57 pub fn cursor(&self) -> u16 {
59 self.cursor as u16
60 }
61
62 pub fn set(&mut self, input: impl Into<Option<String>>, cursor: u16) {
64 if let Some(input) = input.into() {
65 self.input = input;
66 self.recompute_graphemes();
67 }
68 self.cursor = (cursor as usize).min(self.graphemes.len());
69 }
70
71 pub fn push_char(&mut self, c: char) {
72 let byte_idx = self.byte_index(self.cursor);
73 self.input.insert(byte_idx, c);
74 self.recompute_graphemes();
75 self.cursor += 1;
76 }
77
78 pub fn insert_str(&mut self, content: &str) {
79 let byte_idx = self.byte_index(self.cursor);
80 self.input.insert_str(byte_idx, content);
81 let added_graphemes = content.graphemes(true).count();
82 self.recompute_graphemes();
83 self.cursor += added_graphemes;
84 }
85
86 pub fn push_str(&mut self, content: &str) {
87 self.input.push_str(content);
88 self.recompute_graphemes();
89 self.cursor = self.graphemes.len();
90 }
91
92 pub fn scroll_to_cursor(&mut self, padding: usize) {
93 if self.width == 0 {
94 return;
95 }
96
97 if self.before >= self.cursor {
99 self.before = self.cursor.saturating_sub(padding);
100 return;
101 }
102
103 loop {
105 let visual_dist: u16 = self.graphemes
106 [self.before..=(self.cursor + padding).min(self.graphemes.len().saturating_sub(1))]
107 .iter()
108 .map(|(_, w)| *w)
109 .sum();
110
111 if visual_dist <= self.width {
114 break;
115 }
116
117 if self.before < self.cursor {
118 self.before += 1;
119 } else {
120 break;
122 }
123 }
124 }
125
126 pub fn cancel(&mut self) {
127 self.input.clear();
128 self.graphemes.clear();
129 self.cursor = 0;
130 self.before = 0;
131 }
132
133 pub fn prepare_column_change(&mut self) {
134 let trimmed = self.input.trim_end();
135 if let Some(pos) = trimmed.rfind(' ') {
136 let last_word = &trimmed[pos + 1..];
137 if last_word.starts_with('%') {
138 let bytes = trimmed[..pos].len();
139 self.input.truncate(bytes);
140 }
141 } else if trimmed.starts_with('%') {
142 self.input.clear();
143 }
144
145 if !self.input.is_empty() && !self.input.ends_with(' ') {
146 self.input.push(' ');
147 }
148 self.recompute_graphemes();
149 self.cursor = self.graphemes.len();
150 }
151
152 pub fn set_at_visual_offset(&mut self, visual_offset: u16) {
154 let mut current_width = 0;
155 let mut target_cursor = self.before;
156
157 for (i, &(_, width)) in self.graphemes.iter().enumerate().skip(self.before) {
158 if current_width + width > visual_offset {
159 if visual_offset - current_width > width / 2 {
161 target_cursor = i + 1;
162 } else {
163 target_cursor = i;
164 }
165 break;
166 }
167 current_width += width;
168 target_cursor = i + 1;
169 }
170
171 self.cursor = target_cursor;
172 }
173
174 pub fn forward_char(&mut self) {
176 if self.cursor < self.graphemes.len() {
177 self.cursor += 1;
178 }
179 }
180 pub fn backward_char(&mut self) {
181 if self.cursor > 0 {
182 self.cursor -= 1;
183 }
184 }
185
186 pub fn forward_word(&mut self) {
187 let mut in_word = false;
188 while self.cursor < self.graphemes.len() {
189 let byte_start = self.graphemes[self.cursor].0;
190 let byte_end = self
191 .graphemes
192 .get(self.cursor + 1)
193 .map(|(idx, _)| *idx)
194 .unwrap_or(self.input.len());
195 let g = &self.input[byte_start..byte_end];
196
197 if g.chars().all(|c| c.is_whitespace()) {
198 if in_word {
199 break;
200 }
201 } else {
202 in_word = true;
203 }
204 self.cursor += 1;
205 }
206 }
207
208 pub fn backward_word(&mut self) {
209 let mut in_word = false;
210 while self.cursor > 0 {
211 let byte_start = self.graphemes[self.cursor - 1].0;
212 let byte_end = self
213 .graphemes
214 .get(self.cursor)
215 .map(|(idx, _)| *idx)
216 .unwrap_or(self.input.len());
217 let g = &self.input[byte_start..byte_end];
218
219 if g.chars().all(|c| c.is_whitespace()) {
220 if in_word {
221 break;
222 }
223 } else {
224 in_word = true;
225 }
226 self.cursor -= 1;
227 }
228 }
229
230 pub fn delete(&mut self) {
231 if self.cursor > 0 {
232 let start = self.graphemes[self.cursor - 1].0;
233 let end = self.byte_index(self.cursor);
234 self.input.replace_range(start..end, "");
235 self.recompute_graphemes();
236 self.cursor -= 1;
237 }
238 }
239
240 pub fn delete_word(&mut self) {
241 let old_cursor = self.cursor;
242 self.backward_word();
243 let new_cursor = self.cursor;
244
245 let start = self.byte_index(new_cursor);
246 let end = self.byte_index(old_cursor);
247 self.input.replace_range(start..end, "");
248 self.recompute_graphemes();
249 }
250
251 pub fn delete_line_start(&mut self) {
252 let end = self.byte_index(self.cursor);
253 self.input.replace_range(0..end, "");
254 self.recompute_graphemes();
255 self.cursor = 0;
256 self.before = 0;
257 }
258
259 pub fn delete_line_end(&mut self) {
260 let start = self.byte_index(self.cursor);
261 self.input.truncate(start);
262 self.recompute_graphemes();
263 }
264
265 pub fn render(&self) -> &str {
269 let mut visible_width = 0;
270 let mut end_idx = self.before;
271
272 while end_idx < self.graphemes.len() {
273 let g_width = self.graphemes[end_idx].1;
274 if self.width != 0 && visible_width + g_width > self.width {
275 break;
276 }
277 visible_width += g_width;
278 end_idx += 1;
279 }
280
281 let start_byte = self.byte_index(self.before);
282 let end_byte = self.byte_index(end_idx);
283 let visible_input = &self.input[start_byte..end_byte];
284
285 visible_input
286 }
287
288 pub fn cursor_rel_offset(&self) -> u16 {
289 self.graphemes[self.before..self.cursor]
290 .iter()
291 .map(|(_, w)| *w)
292 .sum()
293 }
294}
295
296#[derive(Debug)]
297pub struct QueryUI {
298 pub state: InputUI,
299 prompt: Line<'static>,
300 pub config: QueryConfig,
301}
302
303impl Deref for QueryUI {
304 type Target = InputUI;
305 fn deref(&self) -> &Self::Target {
306 &self.state
307 }
308}
309
310impl DerefMut for QueryUI {
311 fn deref_mut(&mut self) -> &mut Self::Target {
312 &mut self.state
313 }
314}
315
316impl QueryUI {
317 pub fn new(config: QueryConfig) -> Self {
318 let mut ui = Self {
319 state: InputUI::new(),
320 prompt: Line::styled(config.prompt.clone(), config.prompt_style),
321 config,
322 };
323
324 if !ui.config.initial.is_empty() {
325 ui.input = ui.config.initial.clone();
326 ui.recompute_graphemes();
327 ui.cursor = ui.graphemes.len();
328 }
329
330 ui
331 }
332
333 pub fn left(&self) -> u16 {
334 self.config.border.left() + self.prompt.width() as u16
335 }
336
337 pub fn cursor_offset(&self, rect: &Rect) -> Position {
339 let top = self.config.border.top();
340 Position::new(
341 rect.x + self.left() + self.cursor_rel_offset(),
342 rect.y + top,
343 )
344 }
345
346 pub fn update_width(&mut self, width: u16) {
348 let text_width = width
349 .saturating_sub(self.prompt.width() as u16)
350 .saturating_sub(self.config.border.width());
351 if self.width != text_width {
352 self.width = text_width;
353 }
354 }
355
356 pub fn scroll_to_cursor(&mut self) {
357 let padding = self.config.scroll_padding as usize;
358 self.state.scroll_to_cursor(padding);
359 }
360
361 pub fn make_input(&self) -> Paragraph<'_> {
365 let mut line = self.prompt.clone();
366 line.push_span(Span::styled(self.state.render(), self.config.style));
367
368 Paragraph::new(line).block(self.config.border.as_block())
369 }
370
371 pub fn set_prompt(&mut self, template: Option<Line<'static>>) {
373 let line = template
374 .unwrap_or_else(|| self.config.prompt.clone().into())
375 .style(self.config.prompt_style);
376 self.set_prompt_line(line);
377 }
378
379 pub fn set_prompt_line(&mut self, prompt: Line<'static>) {
381 let old_width = self.prompt.to_string().width();
382 let new_width = prompt.to_string().width();
383
384 if new_width > old_width {
385 self.width = self.width.saturating_sub((new_width - old_width) as u16);
386 } else if old_width > new_width {
387 self.width += (old_width - new_width) as u16;
388 }
389
390 self.prompt = prompt;
391 }
392}