rush_sync_server/ui/
cursor.rs

1// =====================================================
2// FILE: src/ui/cursor.rs - OHNE DEBUG LOGS
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        // ❌ REMOVED: log::debug!
89
90        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    /// ✅ TYPEWRITER-FACTORY (Legacy-Support)
104    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    /// ✅ ZENTRALE CONFIG-UPDATE-METHODE
119    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    /// ✅ NEUE METHODE: Update mit explizitem CursorKind (für Klarheit)
139    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    // ==================== BLINK-VERWALTUNG ====================
145
146    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    // ==================== POSITION-VERWALTUNG ====================
163
164    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    // ==================== TEXT-LÄNGEN-VERWALTUNG ====================
193
194    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    // ==================== BYTE-POSITION FÜR TEXT-EDITING ====================
207
208    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    // ==================== RENDERING ====================
233
234    /// ✅ BLOCK-CURSOR: Zeichen unter Cursor invertieren
235    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        // BLOCK: Zeichen unter Cursor invertieren
243        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    /// ✅ CURSOR-SYMBOL-ERSTELLUNG für PIPE und UNDERSCORE
252    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    // ==================== DEBUG & INFO ====================
272
273    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(), // ⬅️ Das sollte "lightblue" sein!
322            self.fg.to_name(),
323            self.position,
324            self.text_length,
325            self.blink_visible,
326            self.last_blink.elapsed()
327        )
328    }
329}
330
331// ==================== FACTORY-FUNKTIONEN ====================
332
333pub 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(), "|"); // Fallback to PIPE
351    }
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); // Fallback
362    }
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}