1use 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#[derive(Debug, Clone, Copy, PartialEq, Eq)]
13pub enum CursorKind {
14 Input, Output, }
17
18#[derive(Debug, Clone, Copy, PartialEq, Eq)]
20pub enum CursorType {
21 Block, Pipe, Underscore, Default, }
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, };
35
36 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 => "|", }
54 }
55}
56
57#[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 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, 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 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, 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 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 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, 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; 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 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 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 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 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 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 pub fn as_span(&self, text: &str, blink: bool) -> Span<'static> {
253 if !blink || !self.blink_visible {
254 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 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 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()) .bg(config.theme.input_bg.into()), )
280 }
281
282 pub fn get_symbol(&self) -> &'static str {
283 self.ctype.symbol()
284 }
285
286 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
307pub fn create_input_cursor(config: &Config) -> UiCursor {
311 UiCursor::from_config(config, CursorKind::Input)
312}
313
314pub 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(), "|"); assert_eq!(CursorType::from_str("unknown").symbol(), "|"); }
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 assert_eq!(
358 cursor.color.to_name(),
359 config.theme.input_cursor_color.to_name()
360 );
361 }
362}