1use std::io::{self, stdout};
9use std::time::Duration;
10
11use anyhow::Result;
12use crossterm::{
13 ExecutableCommand,
14 event::{self, Event, KeyCode, KeyEventKind},
15 terminal::{EnterAlternateScreen, LeaveAlternateScreen, disable_raw_mode, enable_raw_mode},
16};
17use ratatui::{
18 prelude::*,
19 widgets::{Block, Borders, Paragraph},
20};
21
22pub struct App {
24 should_quit: bool,
25}
26
27impl App {
28 pub fn new() -> Self {
30 Self { should_quit: false }
31 }
32
33 pub fn handle_key(&mut self, key: KeyCode) {
35 if key == KeyCode::Char('q') {
36 self.should_quit = true;
37 }
38 }
39
40 pub fn should_quit(&self) -> bool {
42 self.should_quit
43 }
44}
45
46impl Default for App {
47 fn default() -> Self {
48 Self::new()
49 }
50}
51
52pub fn setup_terminal() -> Result<Terminal<CrosstermBackend<io::Stdout>>> {
54 enable_raw_mode()?;
55 stdout().execute(EnterAlternateScreen)?;
56 let backend = CrosstermBackend::new(stdout());
57 let terminal = Terminal::new(backend)?;
58 Ok(terminal)
59}
60
61pub fn restore_terminal() -> Result<()> {
63 disable_raw_mode()?;
64 stdout().execute(LeaveAlternateScreen)?;
65 Ok(())
66}
67
68pub fn draw(frame: &mut Frame) {
70 let area = frame.area();
71
72 let block = Block::default()
73 .title(" Aranet TUI ")
74 .borders(Borders::ALL)
75 .border_style(Style::default().fg(Color::Cyan));
76
77 let message = Paragraph::new("Aranet TUI - Coming Soon\n\nPress 'q' to quit")
78 .alignment(Alignment::Center)
79 .block(block);
80
81 let vertical_center = Layout::default()
83 .direction(Direction::Vertical)
84 .constraints([
85 Constraint::Percentage(40),
86 Constraint::Length(5),
87 Constraint::Percentage(40),
88 ])
89 .split(area);
90
91 frame.render_widget(message, vertical_center[1]);
92}
93
94pub fn run_loop(terminal: &mut Terminal<CrosstermBackend<io::Stdout>>) -> Result<()> {
96 let mut app = App::new();
97
98 while !app.should_quit() {
99 terminal.draw(draw)?;
100
101 if event::poll(Duration::from_millis(100))?
103 && let Event::Key(key) = event::read()?
104 && key.kind == KeyEventKind::Press
105 {
106 app.handle_key(key.code);
107 }
108 }
109
110 Ok(())
111}
112
113pub async fn run() -> Result<()> {
118 let mut terminal = setup_terminal()?;
119
120 let result = run_loop(&mut terminal);
122
123 restore_terminal()?;
124
125 result
126}
127
128#[cfg(test)]
129mod tests {
130 use super::*;
131
132 #[test]
133 fn test_app_new() {
134 let app = App::new();
135 assert!(!app.should_quit());
136 }
137
138 #[test]
139 fn test_app_default() {
140 let app = App::default();
141 assert!(!app.should_quit());
142 }
143
144 #[test]
145 fn test_app_handle_key_q_quits() {
146 let mut app = App::new();
147 assert!(!app.should_quit());
148
149 app.handle_key(KeyCode::Char('q'));
150 assert!(app.should_quit());
151 }
152
153 #[test]
154 fn test_app_handle_key_other_does_not_quit() {
155 let mut app = App::new();
156
157 app.handle_key(KeyCode::Char('a'));
158 assert!(!app.should_quit());
159
160 app.handle_key(KeyCode::Enter);
161 assert!(!app.should_quit());
162
163 app.handle_key(KeyCode::Esc);
164 assert!(!app.should_quit());
165
166 app.handle_key(KeyCode::Up);
167 assert!(!app.should_quit());
168 }
169
170 #[test]
171 fn test_app_handle_key_uppercase_q_does_not_quit() {
172 let mut app = App::new();
173
174 app.handle_key(KeyCode::Char('Q'));
176 assert!(!app.should_quit());
177 }
178
179 #[test]
180 fn test_app_should_quit_returns_correct_state() {
181 let mut app = App::new();
182
183 assert!(!app.should_quit);
185 assert!(!app.should_quit());
186
187 app.should_quit = true;
189 assert!(app.should_quit);
190 assert!(app.should_quit());
191 }
192}