rush_sync_server/ui/
cursor.rs

1// =====================================================
2// FILE: src/ui/cursor.rs - FIXED INPUT CURSOR IMPLEMENTATION
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    Default,    // ✅ FIXED: Jetzt wird das korrekte Symbol verwendet
25}
26
27impl CursorType {
28    pub fn from_str(s: &str) -> CursorType {
29        let result = match s.to_uppercase().as_str() {
30            "BLOCK" => CursorType::Block,
31            "PIPE" => CursorType::Pipe,
32            "UNDERSCORE" => CursorType::Underscore,
33            _ => CursorType::Default, // ✅ FIXED: Default wird korrekt behandelt
34        };
35
36        // ✅ DEBUG für Parsing-Verifizierung
37        if std::env::var("RUST_LOG")
38            .unwrap_or_default()
39            .contains("debug")
40        {
41            eprintln!("🔍 CursorType::from_str('{}') → {:?}", s, result);
42        }
43
44        result
45    }
46
47    pub fn symbol(self) -> &'static str {
48        match self {
49            CursorType::Block => "█",
50            CursorType::Pipe => "|",
51            CursorType::Underscore => "_",
52            CursorType::Default => "|", // ✅ FIXED: Default = PIPE Symbol statt "///"
53        }
54    }
55}
56
57/// ✅ ZENTRALE CURSOR-IMPLEMENTIERUNG
58/// Funktioniert für beide: Input & Output
59#[derive(Debug, Clone)]
60pub struct UiCursor {
61    pub kind: CursorKind,
62    pub ctype: CursorType,
63    pub color: AppColor,
64    pub fg: AppColor,
65    pub position: usize,
66    pub text_length: usize,
67    pub blink_visible: bool,
68    last_blink: Instant,
69    blink_interval: Duration,
70}
71
72impl UiCursor {
73    /// ✅ ZENTRALE FACTORY-METHODE - Erstellt Cursor basierend auf Config
74    pub fn from_config(config: &Config, kind: CursorKind) -> Self {
75        let (cursor_type_str, color, fg) = match kind {
76            CursorKind::Input => (
77                &config.theme.input_cursor,
78                config.theme.input_cursor_color, // ✅ FIXED: Richtige Farbe für Input-Cursor
79                config.theme.input_text,
80            ),
81            CursorKind::Output => (
82                &config.theme.output_cursor,
83                config.theme.output_cursor_color,
84                config.theme.output_text,
85            ),
86        };
87
88        // ✅ DEBUG: Verifiziere Config-Werte
89        if std::env::var("RUST_LOG")
90            .unwrap_or_default()
91            .contains("debug")
92        {
93            eprintln!(
94                "🔧 UiCursor::from_config({:?}): type='{}', color='{}', fg='{}'",
95                kind,
96                cursor_type_str,
97                color.to_name(),
98                fg.to_name()
99            );
100        }
101
102        Self {
103            kind,
104            ctype: CursorType::from_str(cursor_type_str),
105            color, // ✅ FIXED: Jetzt wird die richtige Farbe verwendet
106            fg,
107            position: 0,
108            text_length: 0,
109            blink_visible: true,
110            last_blink: Instant::now(),
111            blink_interval: Duration::from_millis(530),
112        }
113    }
114
115    /// ✅ TYPEWRITER-FACTORY (Legacy-Support)
116    pub fn for_typewriter() -> Self {
117        Self {
118            kind: CursorKind::Output,
119            ctype: CursorType::Default,
120            color: AppColor::default(),
121            fg: AppColor::default(),
122            position: 0,
123            text_length: 0,
124            blink_visible: true,
125            last_blink: Instant::now(),
126            blink_interval: Duration::from_millis(530),
127        }
128    }
129
130    /// ✅ ZENTRALE CONFIG-UPDATE-METHODE
131    pub fn update_from_config(&mut self, config: &Config) {
132        let (cursor_type_str, color, fg) = match self.kind {
133            CursorKind::Input => (
134                &config.theme.input_cursor,
135                config.theme.input_cursor_color, // ✅ FIXED: Richtige Farbe
136                config.theme.input_text,
137            ),
138            CursorKind::Output => (
139                &config.theme.output_cursor,
140                config.theme.output_cursor_color,
141                config.theme.output_text,
142            ),
143        };
144
145        self.ctype = CursorType::from_str(cursor_type_str);
146        self.color = color; // ✅ FIXED: Jetzt wird die richtige Farbe gesetzt
147        self.fg = fg;
148
149        log::debug!(
150            "🔄 Cursor updated: {:?} → type='{}', color='{}', symbol='{}'",
151            self.kind,
152            cursor_type_str,
153            color.to_name(),
154            self.get_symbol()
155        );
156    }
157
158    // ==================== BLINK-VERWALTUNG ====================
159
160    /// ✅ FIXED: Blink-Status aktualisieren
161    pub fn update_blink(&mut self) {
162        if self.last_blink.elapsed() >= self.blink_interval {
163            self.blink_visible = !self.blink_visible;
164            self.last_blink = Instant::now();
165        }
166    }
167
168    /// Cursor sichtbar machen (nach Screen-Clear)
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_to_start(&mut self) {
181        self.position = 0;
182    }
183
184    pub fn move_to_end(&mut self) {
185        self.position = self.text_length;
186    }
187
188    pub fn move_left(&mut self) {
189        if self.position > 0 {
190            self.position -= 1;
191        }
192    }
193
194    pub fn move_right(&mut self) {
195        if self.position < self.text_length {
196            self.position += 1;
197        }
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    /// ✅ FIXED: INTELLIGENTE SPAN-ERSTELLUNG für Input-Bereich
251    /// Nur für BLOCK-Cursor (wird nur noch für Block verwendet)
252    pub fn as_span(&self, text: &str, blink: bool) -> Span<'static> {
253        if !blink || !self.blink_visible {
254            // Cursor nicht sichtbar → normales Zeichen anzeigen
255            let graphemes: Vec<&str> = text.graphemes(true).collect();
256            let ch = graphemes.get(self.position).copied().unwrap_or(" ");
257            return Span::styled(ch.to_string(), Style::default().fg(self.fg.into()));
258        }
259
260        // BLOCK: Zeichen unter Cursor invertieren
261        let graphemes: Vec<&str> = text.graphemes(true).collect();
262        let ch = graphemes.get(self.position).copied().unwrap_or(" ");
263        Span::styled(
264            ch.to_string(),
265            Style::default().fg(self.fg.into()).bg(self.color.into()),
266        )
267    }
268
269    /// ✅ ZENTRALE CURSOR-SYMBOL-ERSTELLUNG
270    /// Rendert den Cursor als separates Symbol (für Output-Bereich UND Input-non-BLOCK)
271    pub fn create_cursor_span(&self, config: &Config) -> Span<'static> {
272        let symbol = self.get_symbol();
273
274        Span::styled(
275            symbol.to_string(),
276            Style::default()
277                .fg(self.color.into()) // ✅ FIXED: Richtige Cursor-Farbe
278                .bg(config.theme.input_bg.into()), // ✅ FIXED: Input-Hintergrund für Input-Cursor
279        )
280    }
281
282    pub fn get_symbol(&self) -> &'static str {
283        self.ctype.symbol()
284    }
285
286    // ==================== DEBUG & INFO ====================
287
288    pub fn debug_info(&self) -> String {
289        format!(
290            "UiCursor({:?}): type={}, pos={}/{}, visible={}, symbol='{}', color='{}'",
291            self.kind,
292            match self.ctype {
293                CursorType::Block => "BLOCK",
294                CursorType::Pipe => "PIPE",
295                CursorType::Underscore => "UNDERSCORE",
296                CursorType::Default => "DEFAULT",
297            },
298            self.position,
299            self.text_length,
300            self.blink_visible,
301            self.get_symbol(),
302            self.color.to_name()
303        )
304    }
305}
306
307// ==================== FACTORY-FUNKTIONEN ====================
308
309/// ✅ Erstelle Input-Cursor
310pub fn create_input_cursor(config: &Config) -> UiCursor {
311    UiCursor::from_config(config, CursorKind::Input)
312}
313
314/// ✅ Erstelle Output-Cursor
315pub fn create_output_cursor(config: &Config) -> UiCursor {
316    UiCursor::from_config(config, CursorKind::Output)
317}
318
319#[cfg(test)]
320mod tests {
321    use super::*;
322
323    #[test]
324    fn test_cursor_types() {
325        assert_eq!(CursorType::from_str("BLOCK").symbol(), "█");
326        assert_eq!(CursorType::from_str("PIPE").symbol(), "|");
327        assert_eq!(CursorType::from_str("UNDERSCORE").symbol(), "_");
328        assert_eq!(CursorType::from_str("DEFAULT").symbol(), "|"); // ✅ FIXED
329        assert_eq!(CursorType::from_str("unknown").symbol(), "|"); // ✅ FIXED
330    }
331
332    #[test]
333    fn test_cursor_position() {
334        let config = crate::core::config::Config::default();
335        let mut cursor = UiCursor::from_config(&config, CursorKind::Input);
336
337        cursor.update_text_length("hello");
338        assert_eq!(cursor.text_length, 5);
339
340        cursor.move_right();
341        cursor.move_right();
342        assert_eq!(cursor.position, 2);
343
344        cursor.move_to_end();
345        assert_eq!(cursor.position, 5);
346
347        cursor.move_to_start();
348        assert_eq!(cursor.position, 0);
349    }
350
351    #[test]
352    fn test_input_cursor_color() {
353        let config = crate::core::config::Config::default();
354        let cursor = UiCursor::from_config(&config, CursorKind::Input);
355
356        // ✅ FIXED: Test dass Input-Cursor die richtige Farbe bekommt
357        assert_eq!(
358            cursor.color.to_name(),
359            config.theme.input_cursor_color.to_name()
360        );
361    }
362}