tits_rs/
app.rs

1use std::error;
2
3use ratatui::{
4    style::{self, Color, Style, Stylize},
5    text::Span,
6};
7use tokio::sync::mpsc::UnboundedSender;
8
9use crate::{event::Event, utils::gen_text};
10
11/// Application result type.
12pub type AppResult<T> = std::result::Result<T, Box<dyn error::Error>>;
13
14#[derive(Debug, PartialEq)]
15pub enum Screen {
16    Start,
17    Typing,
18    Result,
19}
20
21enum CursorMovement {
22    Forwards,
23    Backwards,
24}
25
26/// Application.
27#[derive(Debug)]
28pub struct App {
29    pub screen: Screen,
30    pub is_typing: bool,
31    pub text: String,
32    pub span_vec: Vec<Span<'static>>,
33    pub cursor_idx: usize,
34    pub countdown: usize,
35    pub key_count: f64,
36    pub exit: bool,
37    pub _sender: UnboundedSender<Event>,
38}
39
40impl App {
41    /// Constructs a new instance of [`App`].
42    pub fn new(_sender: UnboundedSender<Event>) -> Self {
43        let text = gen_text();
44
45        let span_vec = string_to_spans(&text);
46
47        let mut app = Self {
48            // TODO: change this to start
49            screen: Screen::Typing,
50            text,
51            span_vec,
52            cursor_idx: 0,
53            key_count: 0.0,
54            countdown: 0,
55            exit: false,
56            is_typing: false,
57            _sender,
58        };
59
60        app.paint_cursor(0, true);
61
62        return app;
63    }
64
65    /// Handles timer tick event
66    pub fn tick(&mut self) {
67        self.countdown += 1;
68        if self.countdown == 30 {
69            self.screen = Screen::Result
70        }
71    }
72
73    /// Set running to false to quit the application.
74    pub fn quit(&mut self) {
75        self.exit = true;
76    }
77
78    pub fn enter_char(&mut self, ch: char) {
79        if let Some(equal) = self.char_match(ch) {
80            if equal {
81                self.key_count += 1.0;
82            }
83            self.update_span(self.cursor_idx, equal);
84            self.move_cursor(CursorMovement::Forwards);
85            self.paint_cursor(self.cursor_idx, true);
86        };
87    }
88
89    /// Move cursor position
90    fn move_cursor(&mut self, movement: CursorMovement) {
91        match movement {
92            CursorMovement::Forwards => {
93                self.cursor_idx = self.cursor_idx.saturating_add(1);
94            }
95            CursorMovement::Backwards => {
96                if self.cursor_idx == 0 {
97                    return;
98                }
99
100                self.cursor_idx = self.cursor_idx.saturating_sub(1);
101            }
102        }
103    }
104
105    /// basically handle backspace
106    pub fn remove_char(&mut self) {
107        self.paint_cursor(self.cursor_idx, false);
108        self.move_cursor(CursorMovement::Backwards);
109        self.paint_cursor(self.cursor_idx, true);
110    }
111
112    /// check if the input matches the character at current index
113    fn char_match(&self, want: char) -> Option<bool> {
114        self.span_vec.get(self.cursor_idx).map(|span| {
115            let got = span.to_string().chars().next().unwrap_or('\n');
116            if got == want {
117                return true;
118            } else {
119                return false;
120            }
121        })
122    }
123
124    fn update_span(&mut self, idx: usize, matched: bool) {
125        let color = if matched { Color::Green } else { Color::Red };
126        let span = self.span_vec.get(idx);
127        if let Some(span) = span {
128            let span = span.clone();
129            self.span_vec[idx] = span.style(Style::new().fg(color)).not_underlined();
130        }
131    }
132
133    fn paint_cursor(&mut self, idx: usize, paint: bool) {
134        if idx > self.span_vec.len() - 1 {
135            return;
136        }
137
138        let span = self.span_vec.get(idx);
139        if let Some(span) = span {
140            let span = span.clone();
141            match paint {
142                true => self.span_vec[idx] = span.style(Style::new()).underlined(),
143                false => self.span_vec[idx] = span.style(Style::new()).not_underlined(),
144            }
145        }
146    }
147
148    pub fn reset(&mut self) {
149        // TODO: change this to start
150        self.screen = Screen::Typing;
151        self.text = gen_text();
152        self.span_vec = string_to_spans(&self.text);
153        self.cursor_idx = 0;
154        self.key_count = 0.0;
155        self.countdown = 0;
156        self.is_typing = false;
157    }
158}
159
160fn string_to_spans(text: &String) -> Vec<Span<'static>> {
161    let span_vec: Vec<Span<'static>> = text
162        .chars()
163        .map(|f| Span::styled(f.to_string(), Style::new().fg(style::Color::DarkGray)))
164        .collect();
165
166    span_vec
167}