rush_sync_server/ui/
cursor.rs

1// =====================================================
2// FILE: src/ui/cursor.rs - FIXED CLIPPY WARNING
3// =====================================================
4
5use 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/// Cursor-Typ unterscheidet wo der Cursor verwendet wird
12#[derive(Debug, Clone, Copy, PartialEq, Eq)]
13pub enum CursorKind {
14    Input,  // Eingabebereich
15    Output, // Ausgabebereich (Typewriter)
16}
17
18/// Cursor-Darstellung - einheitlich für beide Bereiche
19#[derive(Debug, Clone, Copy, PartialEq, Eq)]
20pub enum CursorType {
21    Block,
22    Pipe,
23    Underscore,
24}
25
26// ✅ PROPER IMPLEMENTATION of FromStr trait
27impl 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), // Default fallback
36        }
37    }
38}
39
40impl CursorType {
41    // ✅ RENAMED to avoid confusion with FromStr::from_str
42    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/// ✅ ZENTRALE CURSOR-IMPLEMENTIERUNG
56/// Funktioniert für beide: Input & Output
57#[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    /// ✅ ZENTRALE FACTORY-METHODE - Erstellt Cursor basierend auf Config
72    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    /// ✅ TYPEWRITER-FACTORY (Legacy-Support)
112    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    /// ✅ ZENTRALE CONFIG-UPDATE-METHODE
127    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    /// ✅ NEUE METHODE: Update mit explizitem CursorKind (für Klarheit)
155    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    // ==================== BLINK-VERWALTUNG ====================
161
162    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    // ==================== POSITION-VERWALTUNG ====================
179
180    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    // ==================== TEXT-LÄNGEN-VERWALTUNG ====================
209
210    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    // ==================== BYTE-POSITION FÜR TEXT-EDITING ====================
223
224    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    // ==================== RENDERING ====================
249
250    /// ✅ BLOCK-CURSOR: Zeichen unter Cursor invertieren
251    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        // BLOCK: Zeichen unter Cursor invertieren
259        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    /// ✅ CURSOR-SYMBOL-ERSTELLUNG für PIPE und UNDERSCORE
268    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    // ==================== DEBUG & INFO ====================
288
289    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(), // ⬅️ Das sollte "lightblue" sein!
338            self.fg.to_name(),
339            self.position,
340            self.text_length,
341            self.blink_visible,
342            self.last_blink.elapsed()
343        )
344    }
345}
346
347// ==================== FACTORY-FUNKTIONEN ====================
348
349pub 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(), "|"); // Fallback to PIPE
367    }
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); // Fallback
378    }
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}