rush_sync_server/ui/
cursor.rs

1use crate::core::config::Config;
2use crate::ui::color::AppColor;
3use ratatui::prelude::{Span, Style};
4use std::time::{Duration, Instant};
5use unicode_segmentation::UnicodeSegmentation;
6
7#[derive(Debug, Clone, Copy, PartialEq, Eq)]
8pub enum CursorKind {
9    Input,
10    Output,
11}
12
13#[derive(Debug, Clone, Copy, PartialEq, Eq)]
14pub enum CursorType {
15    Block,
16    Pipe,
17    Underscore,
18}
19
20impl std::str::FromStr for CursorType {
21    type Err = ();
22    fn from_str(s: &str) -> Result<Self, Self::Err> {
23        Ok(match s.to_uppercase().as_str() {
24            "BLOCK" => CursorType::Block,
25            "UNDERSCORE" => CursorType::Underscore,
26            _ => CursorType::Pipe, // Default fallback
27        })
28    }
29}
30
31impl CursorType {
32    pub fn parse_type(s: &str) -> CursorType {
33        s.parse().unwrap_or(CursorType::Pipe)
34    }
35    pub fn symbol(self) -> &'static str {
36        match self {
37            CursorType::Block => "ā–ˆ",
38            CursorType::Pipe => "|",
39            CursorType::Underscore => "_",
40        }
41    }
42}
43
44#[derive(Debug, Clone)]
45pub struct UiCursor {
46    pub kind: CursorKind,
47    pub ctype: CursorType,
48    pub color: AppColor,
49    pub fg: AppColor,
50    pub position: usize,
51    pub text_length: usize,
52    pub blink_visible: bool,
53    last_blink: Instant,
54    blink_interval: Duration,
55}
56
57impl UiCursor {
58    // Factory methods
59    pub fn from_config(config: &Config, kind: CursorKind) -> Self {
60        let (cursor_str, color, fg) = match kind {
61            CursorKind::Input => (
62                &config.theme.input_cursor,
63                config.theme.input_cursor_color,
64                config.theme.input_text,
65            ),
66            CursorKind::Output => (
67                &config.theme.output_cursor,
68                config.theme.output_cursor_color,
69                config.theme.output_text,
70            ),
71        };
72
73        Self {
74            kind,
75            ctype: CursorType::parse_type(cursor_str),
76            color,
77            fg,
78            position: 0,
79            text_length: 0,
80            blink_visible: true,
81            last_blink: Instant::now(),
82            blink_interval: Duration::from_millis(530),
83        }
84    }
85
86    pub fn for_typewriter() -> Self {
87        Self {
88            kind: CursorKind::Output,
89            ctype: CursorType::Pipe,
90            color: AppColor::default(),
91            fg: AppColor::default(),
92            position: 0,
93            text_length: 0,
94            blink_visible: true,
95            last_blink: Instant::now(),
96            blink_interval: Duration::from_millis(530),
97        }
98    }
99
100    // Config updates
101    pub fn update_from_config(&mut self, config: &Config) {
102        let (cursor_str, color, fg) = match self.kind {
103            CursorKind::Input => (
104                &config.theme.input_cursor,
105                config.theme.input_cursor_color,
106                config.theme.input_text,
107            ),
108            CursorKind::Output => (
109                &config.theme.output_cursor,
110                config.theme.output_cursor_color,
111                config.theme.output_text,
112            ),
113        };
114        self.ctype = CursorType::parse_type(cursor_str);
115        self.color = color;
116        self.fg = fg;
117    }
118
119    pub fn update_from_config_explicit(&mut self, config: &Config, kind: CursorKind) {
120        self.kind = kind;
121        self.update_from_config(config);
122    }
123
124    // Blink management
125    pub fn update_blink(&mut self) {
126        if self.last_blink.elapsed() >= self.blink_interval {
127            self.blink_visible = !self.blink_visible;
128            self.last_blink = Instant::now();
129        }
130    }
131
132    pub fn show_cursor(&mut self) {
133        self.blink_visible = true;
134        self.last_blink = Instant::now();
135    }
136
137    pub fn is_visible(&self) -> bool {
138        self.blink_visible
139    }
140
141    // Position management - streamlined
142    pub fn move_left(&mut self) {
143        if self.position > 0 {
144            self.position -= 1;
145        }
146    }
147    pub fn move_right(&mut self) {
148        if self.position < self.text_length {
149            self.position += 1;
150        }
151    }
152    pub fn move_to_start(&mut self) {
153        self.position = 0;
154    }
155    pub fn move_to_end(&mut self) {
156        self.position = self.text_length;
157    }
158    pub fn get_position(&self) -> usize {
159        self.position
160    }
161    pub fn get_current_position(&self) -> usize {
162        self.position
163    }
164
165    // Text length management
166    pub fn update_text_length(&mut self, text: &str) {
167        self.text_length = text.graphemes(true).count();
168        if self.position > self.text_length {
169            self.position = self.text_length;
170        }
171    }
172
173    pub fn reset_for_empty_text(&mut self) {
174        self.position = 0;
175        self.text_length = 0;
176    }
177
178    // Byte position calculations for text editing
179    pub fn get_byte_position(&self, text: &str) -> usize {
180        text.grapheme_indices(true)
181            .nth(self.position)
182            .map(|(i, _)| i)
183            .unwrap_or_else(|| text.len())
184    }
185
186    pub fn get_prev_byte_position(&self, text: &str) -> usize {
187        if self.position == 0 {
188            return 0;
189        }
190        text.grapheme_indices(true)
191            .nth(self.position.saturating_sub(1))
192            .map(|(i, _)| i)
193            .unwrap_or(0)
194    }
195
196    pub fn get_next_byte_position(&self, text: &str) -> usize {
197        text.grapheme_indices(true)
198            .nth(self.position + 1)
199            .map(|(i, _)| i)
200            .unwrap_or_else(|| text.len())
201    }
202
203    // Rendering methods
204    pub fn as_span(&self, text: &str, blink: bool) -> Span<'static> {
205        if !blink || !self.blink_visible {
206            let graphemes: Vec<&str> = text.graphemes(true).collect();
207            let ch = graphemes.get(self.position).copied().unwrap_or(" ");
208            return Span::styled(ch.to_string(), Style::default().fg(self.fg.into()));
209        }
210
211        // Block cursor: invert character under cursor
212        let graphemes: Vec<&str> = text.graphemes(true).collect();
213        let ch = graphemes.get(self.position).copied().unwrap_or(" ");
214        Span::styled(
215            ch.to_string(),
216            Style::default().fg(self.fg.into()).bg(self.color.into()),
217        )
218    }
219
220    pub fn create_cursor_span(&self, config: &Config) -> Span<'static> {
221        let bg_color = match self.kind {
222            CursorKind::Input => config.theme.input_bg.into(),
223            CursorKind::Output => config.theme.output_bg.into(),
224        };
225        Span::styled(
226            self.get_symbol().to_string(),
227            Style::default().fg(self.color.into()).bg(bg_color),
228        )
229    }
230
231    pub fn get_symbol(&self) -> &'static str {
232        self.ctype.symbol()
233    }
234
235    // Debug methods - consolidated
236    pub fn debug_info(&self) -> String {
237        format!(
238            "UiCursor({:?}): type={:?}, pos={}/{}, visible={}, symbol='{}', color='{}', fg='{}'",
239            self.kind,
240            self.ctype,
241            self.position,
242            self.text_length,
243            self.blink_visible,
244            self.get_symbol(),
245            self.color.to_name(),
246            self.fg.to_name()
247        )
248    }
249
250    pub fn full_debug(&self) -> String {
251        format!("šŸ” FULL CURSOR DEBUG:\nKind: {:?}\nType: {:?}\nSymbol: '{}'\nCursor Color: '{}'\nText Color: '{}'\nPosition: {}/{}\nVisible: {}",
252            self.kind, self.ctype, self.get_symbol(), self.color.to_name(), self.fg.to_name(),
253            self.position, self.text_length, self.blink_visible)
254    }
255
256    pub fn detailed_debug(&self) -> String {
257        format!("šŸ” DETAILED CURSOR DEBUG:\nšŸ·ļø Kind: {:?}\nšŸŽÆ Type: {:?} (symbol: '{}')\nšŸŽØ Cursor Color: '{}'\nšŸŽØ Text Color (fg): '{}'\nšŸ“ Position: {}/{}\nšŸ‘ļø Visible: {}\nā±ļø Last Blink: {:?}",
258            self.kind, self.ctype, self.get_symbol(), self.color.to_name(), self.fg.to_name(),
259            self.position, self.text_length, self.blink_visible, self.last_blink.elapsed())
260    }
261}
262
263// Factory functions - streamlined
264pub fn create_input_cursor(config: &Config) -> UiCursor {
265    UiCursor::from_config(config, CursorKind::Input)
266}
267pub fn create_output_cursor(config: &Config) -> UiCursor {
268    UiCursor::from_config(config, CursorKind::Output)
269}
270
271#[cfg(test)]
272mod tests {
273    use super::*;
274
275    #[test]
276    fn test_cursor_types() {
277        assert_eq!(CursorType::parse_type("BLOCK").symbol(), "ā–ˆ");
278        assert_eq!(CursorType::parse_type("PIPE").symbol(), "|");
279        assert_eq!(CursorType::parse_type("UNDERSCORE").symbol(), "_");
280        assert_eq!(CursorType::parse_type("unknown").symbol(), "|"); // Fallback to PIPE
281    }
282
283    #[test]
284    fn test_fromstr_trait() {
285        assert_eq!("BLOCK".parse::<CursorType>().unwrap(), CursorType::Block);
286        assert_eq!("PIPE".parse::<CursorType>().unwrap(), CursorType::Pipe);
287        assert_eq!(
288            "UNDERSCORE".parse::<CursorType>().unwrap(),
289            CursorType::Underscore
290        );
291        assert_eq!("unknown".parse::<CursorType>().unwrap(), CursorType::Pipe); // Fallback
292    }
293
294    #[test]
295    fn test_cursor_position() {
296        let config = crate::core::config::Config::default();
297        let mut cursor = UiCursor::from_config(&config, CursorKind::Input);
298
299        cursor.update_text_length("hello");
300        assert_eq!(cursor.text_length, 5);
301
302        cursor.move_right();
303        cursor.move_right();
304        assert_eq!(cursor.position, 2);
305
306        cursor.move_to_end();
307        assert_eq!(cursor.position, 5);
308
309        cursor.move_to_start();
310        assert_eq!(cursor.position, 0);
311    }
312
313    #[test]
314    fn test_input_cursor_color() {
315        let config = crate::core::config::Config::default();
316        let cursor = UiCursor::from_config(&config, CursorKind::Input);
317
318        assert_eq!(
319            cursor.color.to_name(),
320            config.theme.input_cursor_color.to_name()
321        );
322    }
323}