1use crate::core::config::Config;
6use crate::ui::color::AppColor;
7use ratatui::prelude::{Span, Style};
8use std::time::{Duration, Instant};
9use unicode_segmentation::UnicodeSegmentation;
10
11#[derive(Debug, Clone, Copy, PartialEq, Eq)]
13pub enum CursorKind {
14 Input, Output, }
17
18#[derive(Debug, Clone, Copy, PartialEq, Eq)]
20pub enum CursorType {
21 Block,
22 Pipe,
23 Underscore,
24}
25
26impl std::str::FromStr for CursorType {
28 type Err = ();
29
30 fn from_str(s: &str) -> Result<Self, Self::Err> {
31 match s.to_uppercase().as_str() {
32 "BLOCK" => Ok(CursorType::Block),
33 "PIPE" => Ok(CursorType::Pipe),
34 "UNDERSCORE" => Ok(CursorType::Underscore),
35 _ => Ok(CursorType::Pipe), }
37 }
38}
39
40impl CursorType {
41 pub fn parse_type(s: &str) -> CursorType {
43 s.parse().unwrap_or(CursorType::Pipe)
44 }
45
46 pub fn symbol(self) -> &'static str {
47 match self {
48 CursorType::Block => "█",
49 CursorType::Pipe => "|",
50 CursorType::Underscore => "_",
51 }
52 }
53}
54
55#[derive(Debug, Clone)]
58pub struct UiCursor {
59 pub kind: CursorKind,
60 pub ctype: CursorType,
61 pub color: AppColor,
62 pub fg: AppColor,
63 pub position: usize,
64 pub text_length: usize,
65 pub blink_visible: bool,
66 last_blink: Instant,
67 blink_interval: Duration,
68}
69
70impl UiCursor {
71 pub fn from_config(config: &Config, kind: CursorKind) -> Self {
73 let (cursor_type_str, color, fg) = match kind {
74 CursorKind::Input => (
75 &config.theme.input_cursor,
76 config.theme.input_cursor_color,
77 config.theme.input_text,
78 ),
79 CursorKind::Output => (
80 &config.theme.output_cursor,
81 config.theme.output_cursor_color,
82 config.theme.output_text,
83 ),
84 };
85
86 let cursor_type = CursorType::parse_type(cursor_type_str);
87
88 Self {
89 kind,
90 ctype: cursor_type,
91 color,
92 fg,
93 position: 0,
94 text_length: 0,
95 blink_visible: true,
96 last_blink: Instant::now(),
97 blink_interval: Duration::from_millis(530),
98 }
99 }
100
101 pub fn for_typewriter() -> Self {
103 Self {
104 kind: CursorKind::Output,
105 ctype: CursorType::Pipe,
106 color: AppColor::default(),
107 fg: AppColor::default(),
108 position: 0,
109 text_length: 0,
110 blink_visible: true,
111 last_blink: Instant::now(),
112 blink_interval: Duration::from_millis(530),
113 }
114 }
115
116 pub fn update_from_config(&mut self, config: &Config) {
118 let (cursor_type_str, color, fg) = match self.kind {
119 CursorKind::Input => (
120 &config.theme.input_cursor,
121 config.theme.input_cursor_color,
122 config.theme.input_text,
123 ),
124 CursorKind::Output => (
125 &config.theme.output_cursor,
126 config.theme.output_cursor_color,
127 config.theme.output_text,
128 ),
129 };
130
131 self.ctype = CursorType::parse_type(cursor_type_str);
132 self.color = color;
133 self.fg = fg;
134 }
135
136 pub fn update_from_config_explicit(&mut self, config: &Config, kind: CursorKind) {
138 self.kind = kind;
139 self.update_from_config(config);
140 }
141
142 pub fn update_blink(&mut self) {
145 if self.last_blink.elapsed() >= self.blink_interval {
146 self.blink_visible = !self.blink_visible;
147 self.last_blink = Instant::now();
148 }
149 }
150
151 pub fn show_cursor(&mut self) {
152 self.blink_visible = true;
153 self.last_blink = Instant::now();
154 }
155
156 pub fn is_visible(&self) -> bool {
157 self.blink_visible
158 }
159
160 pub fn move_left(&mut self) {
163 if self.position > 0 {
164 self.position -= 1;
165 }
166 }
167
168 pub fn move_right(&mut self) {
169 if self.position < self.text_length {
170 self.position += 1;
171 }
172 }
173
174 pub fn move_to_start(&mut self) {
175 self.position = 0;
176 }
177
178 pub fn move_to_end(&mut self) {
179 self.position = self.text_length;
180 }
181
182 pub fn get_position(&self) -> usize {
183 self.position
184 }
185
186 pub fn get_current_position(&self) -> usize {
187 self.position
188 }
189
190 pub fn update_text_length(&mut self, text: &str) {
193 self.text_length = text.graphemes(true).count();
194 if self.position > self.text_length {
195 self.position = self.text_length;
196 }
197 }
198
199 pub fn reset_for_empty_text(&mut self) {
200 self.position = 0;
201 self.text_length = 0;
202 }
203
204 pub fn get_byte_position(&self, text: &str) -> usize {
207 text.grapheme_indices(true)
208 .nth(self.position)
209 .map(|(i, _)| i)
210 .unwrap_or_else(|| text.len())
211 }
212
213 pub fn get_prev_byte_position(&self, text: &str) -> usize {
214 if self.position == 0 {
215 return 0;
216 }
217 text.grapheme_indices(true)
218 .nth(self.position.saturating_sub(1))
219 .map(|(i, _)| i)
220 .unwrap_or(0)
221 }
222
223 pub fn get_next_byte_position(&self, text: &str) -> usize {
224 text.grapheme_indices(true)
225 .nth(self.position + 1)
226 .map(|(i, _)| i)
227 .unwrap_or_else(|| text.len())
228 }
229
230 pub fn as_span(&self, text: &str, blink: bool) -> Span<'static> {
234 if !blink || !self.blink_visible {
235 let graphemes: Vec<&str> = text.graphemes(true).collect();
236 let ch = graphemes.get(self.position).copied().unwrap_or(" ");
237 return Span::styled(ch.to_string(), Style::default().fg(self.fg.into()));
238 }
239
240 let graphemes: Vec<&str> = text.graphemes(true).collect();
242 let ch = graphemes.get(self.position).copied().unwrap_or(" ");
243 Span::styled(
244 ch.to_string(),
245 Style::default().fg(self.fg.into()).bg(self.color.into()),
246 )
247 }
248
249 pub fn create_cursor_span(&self, config: &Config) -> Span<'static> {
251 let symbol = self.get_symbol();
252 let cursor_color = self.color;
253
254 let bg_color = match self.kind {
255 CursorKind::Input => config.theme.input_bg.into(),
256 CursorKind::Output => config.theme.output_bg.into(),
257 };
258
259 Span::styled(
260 symbol.to_string(),
261 Style::default().fg(cursor_color.into()).bg(bg_color),
262 )
263 }
264
265 pub fn get_symbol(&self) -> &'static str {
266 self.ctype.symbol()
267 }
268
269 pub fn debug_info(&self) -> String {
272 format!(
273 "UiCursor({:?}): type={:?}, pos={}/{}, visible={}, symbol='{}', color='{}', fg='{}'",
274 self.kind,
275 self.ctype,
276 self.position,
277 self.text_length,
278 self.blink_visible,
279 self.get_symbol(),
280 self.color.to_name(),
281 self.fg.to_name()
282 )
283 }
284
285 pub fn full_debug(&self) -> String {
286 format!(
287 "🔍 FULL CURSOR DEBUG:\n\
288 Kind: {:?}\n\
289 Type: {:?}\n\
290 Symbol: '{}'\n\
291 Cursor Color: '{}'\n\
292 Text Color: '{}'\n\
293 Position: {}/{}\n\
294 Visible: {}",
295 self.kind,
296 self.ctype,
297 self.get_symbol(),
298 self.color.to_name(),
299 self.fg.to_name(),
300 self.position,
301 self.text_length,
302 self.blink_visible,
303 )
304 }
305
306 pub fn detailed_debug(&self) -> String {
307 format!(
308 "🔍 DETAILED CURSOR DEBUG:\n\
309 🏷️ Kind: {:?}\n\
310 🎯 Type: {:?} (symbol: '{}')\n\
311 🎨 Cursor Color: '{}' ⬅️ IST DAS RICHTIG?\n\
312 🎨 Text Color (fg): '{}'\n\
313 📍 Position: {}/{}\n\
314 👁️ Visible: {}\n\
315 ⏱️ Last Blink: {:?}",
316 self.kind,
317 self.ctype,
318 self.get_symbol(),
319 self.color.to_name(), self.fg.to_name(),
321 self.position,
322 self.text_length,
323 self.blink_visible,
324 self.last_blink.elapsed()
325 )
326 }
327}
328
329pub fn create_input_cursor(config: &Config) -> UiCursor {
332 UiCursor::from_config(config, CursorKind::Input)
333}
334
335pub fn create_output_cursor(config: &Config) -> UiCursor {
336 UiCursor::from_config(config, CursorKind::Output)
337}
338
339#[cfg(test)]
340mod tests {
341 use super::*;
342
343 #[test]
344 fn test_cursor_types() {
345 assert_eq!(CursorType::parse_type("BLOCK").symbol(), "█");
346 assert_eq!(CursorType::parse_type("PIPE").symbol(), "|");
347 assert_eq!(CursorType::parse_type("UNDERSCORE").symbol(), "_");
348 assert_eq!(CursorType::parse_type("unknown").symbol(), "|"); }
350
351 #[test]
352 fn test_fromstr_trait() {
353 assert_eq!("BLOCK".parse::<CursorType>().unwrap(), CursorType::Block);
354 assert_eq!("PIPE".parse::<CursorType>().unwrap(), CursorType::Pipe);
355 assert_eq!(
356 "UNDERSCORE".parse::<CursorType>().unwrap(),
357 CursorType::Underscore
358 );
359 assert_eq!("unknown".parse::<CursorType>().unwrap(), CursorType::Pipe); }
361
362 #[test]
363 fn test_cursor_position() {
364 let config = crate::core::config::Config::default();
365 let mut cursor = UiCursor::from_config(&config, CursorKind::Input);
366
367 cursor.update_text_length("hello");
368 assert_eq!(cursor.text_length, 5);
369
370 cursor.move_right();
371 cursor.move_right();
372 assert_eq!(cursor.position, 2);
373
374 cursor.move_to_end();
375 assert_eq!(cursor.position, 5);
376
377 cursor.move_to_start();
378 assert_eq!(cursor.position, 0);
379 }
380
381 #[test]
382 fn test_input_cursor_color() {
383 let config = crate::core::config::Config::default();
384 let cursor = UiCursor::from_config(&config, CursorKind::Input);
385
386 assert_eq!(
387 cursor.color.to_name(),
388 config.theme.input_cursor_color.to_name()
389 );
390 }
391}