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 {
91 kind,
92 ctype: cursor_type,
93 color,
94 fg,
95 position: 0,
96 text_length: 0,
97 blink_visible: true,
98 last_blink: Instant::now(),
99 blink_interval: Duration::from_millis(530),
100 }
101 }
102
103 pub fn for_typewriter() -> Self {
105 Self {
106 kind: CursorKind::Output,
107 ctype: CursorType::Pipe,
108 color: AppColor::default(),
109 fg: AppColor::default(),
110 position: 0,
111 text_length: 0,
112 blink_visible: true,
113 last_blink: Instant::now(),
114 blink_interval: Duration::from_millis(530),
115 }
116 }
117
118 pub fn update_from_config(&mut self, config: &Config) {
120 let (cursor_type_str, color, fg) = match self.kind {
121 CursorKind::Input => (
122 &config.theme.input_cursor,
123 config.theme.input_cursor_color,
124 config.theme.input_text,
125 ),
126 CursorKind::Output => (
127 &config.theme.output_cursor,
128 config.theme.output_cursor_color,
129 config.theme.output_text,
130 ),
131 };
132
133 self.ctype = CursorType::parse_type(cursor_type_str);
134 self.color = color;
135 self.fg = fg;
136 }
137
138 pub fn update_from_config_explicit(&mut self, config: &Config, kind: CursorKind) {
140 self.kind = kind;
141 self.update_from_config(config);
142 }
143
144 pub fn update_blink(&mut self) {
147 if self.last_blink.elapsed() >= self.blink_interval {
148 self.blink_visible = !self.blink_visible;
149 self.last_blink = Instant::now();
150 }
151 }
152
153 pub fn show_cursor(&mut self) {
154 self.blink_visible = true;
155 self.last_blink = Instant::now();
156 }
157
158 pub fn is_visible(&self) -> bool {
159 self.blink_visible
160 }
161
162 pub fn move_left(&mut self) {
165 if self.position > 0 {
166 self.position -= 1;
167 }
168 }
169
170 pub fn move_right(&mut self) {
171 if self.position < self.text_length {
172 self.position += 1;
173 }
174 }
175
176 pub fn move_to_start(&mut self) {
177 self.position = 0;
178 }
179
180 pub fn move_to_end(&mut self) {
181 self.position = self.text_length;
182 }
183
184 pub fn get_position(&self) -> usize {
185 self.position
186 }
187
188 pub fn get_current_position(&self) -> usize {
189 self.position
190 }
191
192 pub fn update_text_length(&mut self, text: &str) {
195 self.text_length = text.graphemes(true).count();
196 if self.position > self.text_length {
197 self.position = self.text_length;
198 }
199 }
200
201 pub fn reset_for_empty_text(&mut self) {
202 self.position = 0;
203 self.text_length = 0;
204 }
205
206 pub fn get_byte_position(&self, text: &str) -> usize {
209 text.grapheme_indices(true)
210 .nth(self.position)
211 .map(|(i, _)| i)
212 .unwrap_or_else(|| text.len())
213 }
214
215 pub fn get_prev_byte_position(&self, text: &str) -> usize {
216 if self.position == 0 {
217 return 0;
218 }
219 text.grapheme_indices(true)
220 .nth(self.position.saturating_sub(1))
221 .map(|(i, _)| i)
222 .unwrap_or(0)
223 }
224
225 pub fn get_next_byte_position(&self, text: &str) -> usize {
226 text.grapheme_indices(true)
227 .nth(self.position + 1)
228 .map(|(i, _)| i)
229 .unwrap_or_else(|| text.len())
230 }
231
232 pub fn as_span(&self, text: &str, blink: bool) -> Span<'static> {
236 if !blink || !self.blink_visible {
237 let graphemes: Vec<&str> = text.graphemes(true).collect();
238 let ch = graphemes.get(self.position).copied().unwrap_or(" ");
239 return Span::styled(ch.to_string(), Style::default().fg(self.fg.into()));
240 }
241
242 let graphemes: Vec<&str> = text.graphemes(true).collect();
244 let ch = graphemes.get(self.position).copied().unwrap_or(" ");
245 Span::styled(
246 ch.to_string(),
247 Style::default().fg(self.fg.into()).bg(self.color.into()),
248 )
249 }
250
251 pub fn create_cursor_span(&self, config: &Config) -> Span<'static> {
253 let symbol = self.get_symbol();
254 let cursor_color = self.color;
255
256 let bg_color = match self.kind {
257 CursorKind::Input => config.theme.input_bg.into(),
258 CursorKind::Output => config.theme.output_bg.into(),
259 };
260
261 Span::styled(
262 symbol.to_string(),
263 Style::default().fg(cursor_color.into()).bg(bg_color),
264 )
265 }
266
267 pub fn get_symbol(&self) -> &'static str {
268 self.ctype.symbol()
269 }
270
271 pub fn debug_info(&self) -> String {
274 format!(
275 "UiCursor({:?}): type={:?}, pos={}/{}, visible={}, symbol='{}', color='{}', fg='{}'",
276 self.kind,
277 self.ctype,
278 self.position,
279 self.text_length,
280 self.blink_visible,
281 self.get_symbol(),
282 self.color.to_name(),
283 self.fg.to_name()
284 )
285 }
286
287 pub fn full_debug(&self) -> String {
288 format!(
289 "🔍 FULL CURSOR DEBUG:\n\
290 Kind: {:?}\n\
291 Type: {:?}\n\
292 Symbol: '{}'\n\
293 Cursor Color: '{}'\n\
294 Text Color: '{}'\n\
295 Position: {}/{}\n\
296 Visible: {}",
297 self.kind,
298 self.ctype,
299 self.get_symbol(),
300 self.color.to_name(),
301 self.fg.to_name(),
302 self.position,
303 self.text_length,
304 self.blink_visible,
305 )
306 }
307
308 pub fn detailed_debug(&self) -> String {
309 format!(
310 "🔍 DETAILED CURSOR DEBUG:\n\
311 🏷️ Kind: {:?}\n\
312 🎯 Type: {:?} (symbol: '{}')\n\
313 🎨 Cursor Color: '{}' ⬅️ IST DAS RICHTIG?\n\
314 🎨 Text Color (fg): '{}'\n\
315 📍 Position: {}/{}\n\
316 👁️ Visible: {}\n\
317 ⏱️ Last Blink: {:?}",
318 self.kind,
319 self.ctype,
320 self.get_symbol(),
321 self.color.to_name(), self.fg.to_name(),
323 self.position,
324 self.text_length,
325 self.blink_visible,
326 self.last_blink.elapsed()
327 )
328 }
329}
330
331pub fn create_input_cursor(config: &Config) -> UiCursor {
334 UiCursor::from_config(config, CursorKind::Input)
335}
336
337pub fn create_output_cursor(config: &Config) -> UiCursor {
338 UiCursor::from_config(config, CursorKind::Output)
339}
340
341#[cfg(test)]
342mod tests {
343 use super::*;
344
345 #[test]
346 fn test_cursor_types() {
347 assert_eq!(CursorType::parse_type("BLOCK").symbol(), "█");
348 assert_eq!(CursorType::parse_type("PIPE").symbol(), "|");
349 assert_eq!(CursorType::parse_type("UNDERSCORE").symbol(), "_");
350 assert_eq!(CursorType::parse_type("unknown").symbol(), "|"); }
352
353 #[test]
354 fn test_fromstr_trait() {
355 assert_eq!("BLOCK".parse::<CursorType>().unwrap(), CursorType::Block);
356 assert_eq!("PIPE".parse::<CursorType>().unwrap(), CursorType::Pipe);
357 assert_eq!(
358 "UNDERSCORE".parse::<CursorType>().unwrap(),
359 CursorType::Underscore
360 );
361 assert_eq!("unknown".parse::<CursorType>().unwrap(), CursorType::Pipe); }
363
364 #[test]
365 fn test_cursor_position() {
366 let config = crate::core::config::Config::default();
367 let mut cursor = UiCursor::from_config(&config, CursorKind::Input);
368
369 cursor.update_text_length("hello");
370 assert_eq!(cursor.text_length, 5);
371
372 cursor.move_right();
373 cursor.move_right();
374 assert_eq!(cursor.position, 2);
375
376 cursor.move_to_end();
377 assert_eq!(cursor.position, 5);
378
379 cursor.move_to_start();
380 assert_eq!(cursor.position, 0);
381 }
382
383 #[test]
384 fn test_input_cursor_color() {
385 let config = crate::core::config::Config::default();
386 let cursor = UiCursor::from_config(&config, CursorKind::Input);
387
388 assert_eq!(
389 cursor.color.to_name(),
390 config.theme.input_cursor_color.to_name()
391 );
392 }
393}