1use ratatui::{
2 layout::{Position, Rect},
3 style::Stylize,
4 text::{Line, Span},
5 widgets::Paragraph,
6};
7use unicode_segmentation::UnicodeSegmentation;
8use crate::{config::InputConfig, utils::text::grapheme_index_to_byte_index};
11
12#[derive(Debug, Clone)]
13pub struct InputUI {
14 pub cursor: u16, pub input: String,
16 pub config: InputConfig,
17 pub prompt: Span<'static>,
18}
19
20impl InputUI {
21 pub fn new(config: InputConfig) -> Self {
22 Self {
23 cursor: 0,
24 input: "".into(),
25 prompt: Span::from(config.prompt.clone()),
26 config,
27 }
28 }
29 pub fn len(&self) -> usize {
32 self.input.len()
33 }
34 pub fn is_empty(&self) -> bool {
35 self.input.is_empty()
36 }
37
38 pub fn cursor_offset(&self, rect: &Rect) -> Position {
39 let left = self.config.border.left();
40 let top = self.config.border.top();
41 Position::new(
42 rect.x + self.cursor + self.prompt.width() as u16 + left,
43 rect.y + top,
44 )
45 }
46
47 pub fn set(&mut self, input: String, cursor: u16) {
49 let grapheme_count = input.graphemes(true).count() as u16;
50 self.input = input;
51 self.cursor = cursor.min(grapheme_count);
52 }
53 pub fn cancel(&mut self) {
54 self.input.clear();
55 self.cursor = 0;
56 }
57
58 pub fn forward_char(&mut self) {
60 if self.cursor < self.input.graphemes(true).count() as u16 {
62 self.cursor += 1;
63 }
64 }
65 pub fn backward_char(&mut self) {
66 if self.cursor > 0 {
67 self.cursor -= 1;
68 }
69 }
70 pub fn insert_char(&mut self, c: char) {
71 let old_grapheme_count = self.input.graphemes(true).count() as u16;
72 let byte_index = grapheme_index_to_byte_index(&self.input, self.cursor);
73 self.input.insert(byte_index, c);
74 let new_grapheme_count = self.input.graphemes(true).count() as u16;
75 if new_grapheme_count > old_grapheme_count {
76 self.cursor += 1;
77 }
78 }
79
80 pub fn forward_word(&mut self) {
81 let post = self.input.graphemes(true).skip(self.cursor as usize);
82
83 let mut in_word = false;
84
85 for g in post {
86 self.cursor += 1;
87 if g.chars().all(|c| c.is_whitespace()) {
88 if in_word {
89 return;
90 }
91 } else {
92 in_word = true;
93 }
94 }
95 }
96
97 pub fn backward_word(&mut self) {
98 let mut in_word = false;
99
100 let pre: Vec<&str> = self
101 .input
102 .graphemes(true)
103 .take(self.cursor as usize)
104 .collect();
105
106 for g in pre.iter().rev() {
107 self.cursor -= 1;
108
109 if g.chars().all(|c| c.is_whitespace()) {
110 if in_word {
111 return;
112 }
113 } else {
114 in_word = true;
115 }
116 }
117
118 self.cursor = 0;
119 }
120
121 pub fn delete(&mut self) {
122 if self.cursor > 0 {
123 let byte_start = grapheme_index_to_byte_index(&self.input, self.cursor - 1);
124 let byte_end = grapheme_index_to_byte_index(&self.input, self.cursor);
125
126 self.input.replace_range(byte_start..byte_end, "");
127 self.cursor -= 1;
128 }
129 }
130
131 pub fn delete_word(&mut self) {
132 let old_cursor_grapheme = self.cursor;
133 self.backward_word();
134 let new_cursor_grapheme = self.cursor;
135
136 let byte_start = grapheme_index_to_byte_index(&self.input, new_cursor_grapheme);
137 let byte_end = grapheme_index_to_byte_index(&self.input, old_cursor_grapheme);
138
139 self.input.replace_range(byte_start..byte_end, "");
140 }
141
142 pub fn delete_line_start(&mut self) {
143 let byte_end = grapheme_index_to_byte_index(&self.input, self.cursor);
144
145 self.input.replace_range(0..byte_end, "");
146 self.cursor = 0;
147 }
148
149 pub fn delete_line_end(&mut self) {
150 let byte_index = grapheme_index_to_byte_index(&self.input, self.cursor);
151
152 self.input.truncate(byte_index);
154 }
155
156 pub fn make_input(&self) -> Paragraph<'_> {
158 let line = Line::from(vec![
159 self.prompt.clone(),
160 Span::raw(self.input.as_str())
161 .style(self.config.fg)
162 .add_modifier(self.config.modifier),
163 ]);
164
165 let mut input = Paragraph::new(line);
166
167 input = input.block(self.config.border.as_block());
168
169 input
170 }
171}