1use crate::core::config::Config;
2use crate::ui::color::AppColor;
3use ratatui::prelude::{Span, Style};
4use std::time::{Duration, Instant};
5use unicode_segmentation::UnicodeSegmentation;
6
7#[derive(Debug, Clone, Copy, PartialEq, Eq)]
8pub enum CursorKind {
9 Input,
10 Output,
11}
12
13#[derive(Debug, Clone, Copy, PartialEq, Eq)]
14pub enum CursorType {
15 Block,
16 Pipe,
17 Underscore,
18}
19
20impl std::str::FromStr for CursorType {
21 type Err = ();
22 fn from_str(s: &str) -> Result<Self, Self::Err> {
23 Ok(match s.to_uppercase().as_str() {
24 "BLOCK" => CursorType::Block,
25 "UNDERSCORE" => CursorType::Underscore,
26 _ => CursorType::Pipe, })
28 }
29}
30
31impl CursorType {
32 pub fn parse_type(s: &str) -> CursorType {
33 s.parse().unwrap_or(CursorType::Pipe)
34 }
35 pub fn symbol(self) -> &'static str {
36 match self {
37 CursorType::Block => "ā",
38 CursorType::Pipe => "|",
39 CursorType::Underscore => "_",
40 }
41 }
42}
43
44#[derive(Debug, Clone)]
45pub struct UiCursor {
46 pub kind: CursorKind,
47 pub ctype: CursorType,
48 pub color: AppColor,
49 pub fg: AppColor,
50 pub position: usize,
51 pub text_length: usize,
52 pub blink_visible: bool,
53 last_blink: Instant,
54 blink_interval: Duration,
55}
56
57impl UiCursor {
58 pub fn from_config(config: &Config, kind: CursorKind) -> Self {
60 let (cursor_str, color, fg) = match kind {
61 CursorKind::Input => (
62 &config.theme.input_cursor,
63 config.theme.input_cursor_color,
64 config.theme.input_text,
65 ),
66 CursorKind::Output => (
67 &config.theme.output_cursor,
68 config.theme.output_cursor_color,
69 config.theme.output_text,
70 ),
71 };
72
73 Self {
74 kind,
75 ctype: CursorType::parse_type(cursor_str),
76 color,
77 fg,
78 position: 0,
79 text_length: 0,
80 blink_visible: true,
81 last_blink: Instant::now(),
82 blink_interval: Duration::from_millis(530),
83 }
84 }
85
86 pub fn for_typewriter() -> Self {
87 Self {
88 kind: CursorKind::Output,
89 ctype: CursorType::Pipe,
90 color: AppColor::default(),
91 fg: AppColor::default(),
92 position: 0,
93 text_length: 0,
94 blink_visible: true,
95 last_blink: Instant::now(),
96 blink_interval: Duration::from_millis(530),
97 }
98 }
99
100 pub fn update_from_config(&mut self, config: &Config) {
102 let (cursor_str, color, fg) = match self.kind {
103 CursorKind::Input => (
104 &config.theme.input_cursor,
105 config.theme.input_cursor_color,
106 config.theme.input_text,
107 ),
108 CursorKind::Output => (
109 &config.theme.output_cursor,
110 config.theme.output_cursor_color,
111 config.theme.output_text,
112 ),
113 };
114 self.ctype = CursorType::parse_type(cursor_str);
115 self.color = color;
116 self.fg = fg;
117 }
118
119 pub fn update_from_config_explicit(&mut self, config: &Config, kind: CursorKind) {
120 self.kind = kind;
121 self.update_from_config(config);
122 }
123
124 pub fn update_blink(&mut self) {
126 if self.last_blink.elapsed() >= self.blink_interval {
127 self.blink_visible = !self.blink_visible;
128 self.last_blink = Instant::now();
129 }
130 }
131
132 pub fn show_cursor(&mut self) {
133 self.blink_visible = true;
134 self.last_blink = Instant::now();
135 }
136
137 pub fn is_visible(&self) -> bool {
138 self.blink_visible
139 }
140
141 pub fn move_left(&mut self) {
143 if self.position > 0 {
144 self.position -= 1;
145 }
146 }
147 pub fn move_right(&mut self) {
148 if self.position < self.text_length {
149 self.position += 1;
150 }
151 }
152 pub fn move_to_start(&mut self) {
153 self.position = 0;
154 }
155 pub fn move_to_end(&mut self) {
156 self.position = self.text_length;
157 }
158 pub fn get_position(&self) -> usize {
159 self.position
160 }
161 pub fn get_current_position(&self) -> usize {
162 self.position
163 }
164
165 pub fn update_text_length(&mut self, text: &str) {
167 self.text_length = text.graphemes(true).count();
168 if self.position > self.text_length {
169 self.position = self.text_length;
170 }
171 }
172
173 pub fn reset_for_empty_text(&mut self) {
174 self.position = 0;
175 self.text_length = 0;
176 }
177
178 pub fn get_byte_position(&self, text: &str) -> usize {
180 text.grapheme_indices(true)
181 .nth(self.position)
182 .map(|(i, _)| i)
183 .unwrap_or_else(|| text.len())
184 }
185
186 pub fn get_prev_byte_position(&self, text: &str) -> usize {
187 if self.position == 0 {
188 return 0;
189 }
190 text.grapheme_indices(true)
191 .nth(self.position.saturating_sub(1))
192 .map(|(i, _)| i)
193 .unwrap_or(0)
194 }
195
196 pub fn get_next_byte_position(&self, text: &str) -> usize {
197 text.grapheme_indices(true)
198 .nth(self.position + 1)
199 .map(|(i, _)| i)
200 .unwrap_or_else(|| text.len())
201 }
202
203 pub fn as_span(&self, text: &str, blink: bool) -> Span<'static> {
205 if !blink || !self.blink_visible {
206 let graphemes: Vec<&str> = text.graphemes(true).collect();
207 let ch = graphemes.get(self.position).copied().unwrap_or(" ");
208 return Span::styled(ch.to_string(), Style::default().fg(self.fg.into()));
209 }
210
211 let graphemes: Vec<&str> = text.graphemes(true).collect();
213 let ch = graphemes.get(self.position).copied().unwrap_or(" ");
214 Span::styled(
215 ch.to_string(),
216 Style::default().fg(self.fg.into()).bg(self.color.into()),
217 )
218 }
219
220 pub fn create_cursor_span(&self, config: &Config) -> Span<'static> {
221 let bg_color = match self.kind {
222 CursorKind::Input => config.theme.input_bg.into(),
223 CursorKind::Output => config.theme.output_bg.into(),
224 };
225 Span::styled(
226 self.get_symbol().to_string(),
227 Style::default().fg(self.color.into()).bg(bg_color),
228 )
229 }
230
231 pub fn get_symbol(&self) -> &'static str {
232 self.ctype.symbol()
233 }
234
235 pub fn debug_info(&self) -> String {
237 format!(
238 "UiCursor({:?}): type={:?}, pos={}/{}, visible={}, symbol='{}', color='{}', fg='{}'",
239 self.kind,
240 self.ctype,
241 self.position,
242 self.text_length,
243 self.blink_visible,
244 self.get_symbol(),
245 self.color.to_name(),
246 self.fg.to_name()
247 )
248 }
249
250 pub fn full_debug(&self) -> String {
251 format!("š FULL CURSOR DEBUG:\nKind: {:?}\nType: {:?}\nSymbol: '{}'\nCursor Color: '{}'\nText Color: '{}'\nPosition: {}/{}\nVisible: {}",
252 self.kind, self.ctype, self.get_symbol(), self.color.to_name(), self.fg.to_name(),
253 self.position, self.text_length, self.blink_visible)
254 }
255
256 pub fn detailed_debug(&self) -> String {
257 format!("š DETAILED CURSOR DEBUG:\nš·ļø Kind: {:?}\nšÆ Type: {:?} (symbol: '{}')\nšØ Cursor Color: '{}'\nšØ Text Color (fg): '{}'\nš Position: {}/{}\nšļø Visible: {}\nā±ļø Last Blink: {:?}",
258 self.kind, self.ctype, self.get_symbol(), self.color.to_name(), self.fg.to_name(),
259 self.position, self.text_length, self.blink_visible, self.last_blink.elapsed())
260 }
261}
262
263pub fn create_input_cursor(config: &Config) -> UiCursor {
265 UiCursor::from_config(config, CursorKind::Input)
266}
267pub fn create_output_cursor(config: &Config) -> UiCursor {
268 UiCursor::from_config(config, CursorKind::Output)
269}
270
271#[cfg(test)]
272mod tests {
273 use super::*;
274
275 #[test]
276 fn test_cursor_types() {
277 assert_eq!(CursorType::parse_type("BLOCK").symbol(), "ā");
278 assert_eq!(CursorType::parse_type("PIPE").symbol(), "|");
279 assert_eq!(CursorType::parse_type("UNDERSCORE").symbol(), "_");
280 assert_eq!(CursorType::parse_type("unknown").symbol(), "|"); }
282
283 #[test]
284 fn test_fromstr_trait() {
285 assert_eq!("BLOCK".parse::<CursorType>().unwrap(), CursorType::Block);
286 assert_eq!("PIPE".parse::<CursorType>().unwrap(), CursorType::Pipe);
287 assert_eq!(
288 "UNDERSCORE".parse::<CursorType>().unwrap(),
289 CursorType::Underscore
290 );
291 assert_eq!("unknown".parse::<CursorType>().unwrap(), CursorType::Pipe); }
293
294 #[test]
295 fn test_cursor_position() {
296 let config = crate::core::config::Config::default();
297 let mut cursor = UiCursor::from_config(&config, CursorKind::Input);
298
299 cursor.update_text_length("hello");
300 assert_eq!(cursor.text_length, 5);
301
302 cursor.move_right();
303 cursor.move_right();
304 assert_eq!(cursor.position, 2);
305
306 cursor.move_to_end();
307 assert_eq!(cursor.position, 5);
308
309 cursor.move_to_start();
310 assert_eq!(cursor.position, 0);
311 }
312
313 #[test]
314 fn test_input_cursor_color() {
315 let config = crate::core::config::Config::default();
316 let cursor = UiCursor::from_config(&config, CursorKind::Input);
317
318 assert_eq!(
319 cursor.color.to_name(),
320 config.theme.input_cursor_color.to_name()
321 );
322 }
323}