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