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        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    /// ✅ TYPEWRITER-FACTORY (Legacy-Support)
102    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    /// ✅ ZENTRALE CONFIG-UPDATE-METHODE
117    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    /// ✅ NEUE METHODE: Update mit explizitem CursorKind (für Klarheit)
137    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    // ==================== BLINK-VERWALTUNG ====================
143
144    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    // ==================== POSITION-VERWALTUNG ====================
161
162    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    // ==================== TEXT-LÄNGEN-VERWALTUNG ====================
191
192    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    // ==================== BYTE-POSITION FÜR TEXT-EDITING ====================
205
206    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    // ==================== RENDERING ====================
231
232    /// ✅ BLOCK-CURSOR: Zeichen unter Cursor invertieren
233    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        // BLOCK: Zeichen unter Cursor invertieren
241        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    /// ✅ CURSOR-SYMBOL-ERSTELLUNG für PIPE und UNDERSCORE
250    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    // ==================== DEBUG & INFO ====================
270
271    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(), // ⬅️ Das sollte "lightblue" sein!
320            self.fg.to_name(),
321            self.position,
322            self.text_length,
323            self.blink_visible,
324            self.last_blink.elapsed()
325        )
326    }
327}
328
329// ==================== FACTORY-FUNKTIONEN ====================
330
331pub 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(), "|"); // Fallback to PIPE
349    }
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); // Fallback
360    }
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}