gromnie_cli/app.rs
1use crossterm::event::{self, Event, KeyCode, KeyModifiers};
2use ratatui::prelude::Backend;
3use ratatui::{Frame, Terminal};
4use std::error::Error;
5
6use gromnie_client::config::GromnieConfig;
7
8use super::draw::Draw;
9use super::wizards::{ConfigWizard, ConfigWizardStage, LaunchWizard, WizardStage};
10
11pub struct App {
12 pub app_screen: AppScreen,
13 pub launch_wizard: Option<LaunchWizard>,
14 pub config_wizard: Option<ConfigWizard>,
15}
16
17impl Default for App {
18 fn default() -> Self {
19 Self::new()
20 }
21}
22
23impl App {
24 pub fn new() -> Self {
25 Self {
26 app_screen: AppScreen::Config,
27 launch_wizard: None,
28 config_wizard: Some(ConfigWizard::new()),
29 }
30 }
31
32 pub fn new_with_config(config: GromnieConfig) -> Self {
33 Self {
34 app_screen: AppScreen::Launch,
35 launch_wizard: Some(LaunchWizard::new(config)),
36 config_wizard: None,
37 }
38 }
39
40 pub fn draw(&mut self, frame: &mut Frame) {
41 match &self.app_screen {
42 AppScreen::Setup => {
43 let _ = SetupWizard::new().draw(self, frame);
44 }
45 AppScreen::Launch => {
46 if let Some(wizard) = &self.launch_wizard {
47 let _ = wizard.clone().draw(self, frame);
48 }
49 }
50 AppScreen::Config => {
51 if let Some(wizard) = &self.config_wizard {
52 let _ = wizard.clone().draw(self, frame);
53 }
54 }
55 }
56 }
57
58 pub fn run(&mut self, terminal: &mut Terminal<impl Backend>) -> Result<(), Box<dyn Error>> {
59 loop {
60 terminal.draw(|frame| self.draw(frame))?;
61
62 // Handle input based on current screen
63 match &self.app_screen {
64 AppScreen::Setup => {
65 // TODO: Setup wizard input handling
66 break;
67 }
68 AppScreen::Config => {
69 if let Some(wizard) = &mut self.config_wizard {
70 // Auto-advance from Welcome stage
71 if wizard.stage == ConfigWizardStage::Welcome {
72 wizard.stage = ConfigWizardStage::EnteringServerName;
73 continue;
74 }
75
76 // Check if wizard is complete
77 if wizard.stage == ConfigWizardStage::Complete {
78 break;
79 }
80
81 // Poll for keyboard events
82 if event::poll(std::time::Duration::from_millis(16))?
83 && let Event::Key(key) = event::read()?
84 {
85 match key.code {
86 KeyCode::Char('c')
87 if key.modifiers.contains(KeyModifiers::CONTROL) =>
88 {
89 return Err("Configuration cancelled".into());
90 }
91 KeyCode::Char(c) => {
92 // Only accept input during text entry stages
93 if matches!(
94 wizard.stage,
95 ConfigWizardStage::EnteringServerName
96 | ConfigWizardStage::EnteringServerHost
97 | ConfigWizardStage::EnteringServerPort
98 | ConfigWizardStage::EnteringAccountUsername
99 | ConfigWizardStage::EnteringAccountPassword
100 ) {
101 wizard.current_input.push(c);
102 }
103 }
104 KeyCode::Backspace => {
105 // Remove last character during text entry
106 if matches!(
107 wizard.stage,
108 ConfigWizardStage::EnteringServerName
109 | ConfigWizardStage::EnteringServerHost
110 | ConfigWizardStage::EnteringServerPort
111 | ConfigWizardStage::EnteringAccountUsername
112 | ConfigWizardStage::EnteringAccountPassword
113 ) {
114 wizard.current_input.pop();
115 }
116 }
117 KeyCode::Enter => match wizard.stage {
118 ConfigWizardStage::EnteringServerName
119 if !wizard.current_input.is_empty() =>
120 {
121 wizard.server_name = wizard.current_input.clone();
122 wizard.current_input.clear();
123 wizard.stage = ConfigWizardStage::EnteringServerHost;
124 }
125 ConfigWizardStage::EnteringServerHost
126 if !wizard.current_input.is_empty() =>
127 {
128 wizard.server_host = wizard.current_input.clone();
129 wizard.current_input.clear();
130 wizard.stage = ConfigWizardStage::EnteringServerPort;
131 }
132 ConfigWizardStage::EnteringServerPort => {
133 wizard.server_port = if wizard.current_input.is_empty() {
134 "9000".to_string()
135 } else {
136 wizard.current_input.clone()
137 };
138 wizard.current_input.clear();
139 wizard.stage = ConfigWizardStage::EnteringAccountUsername;
140 }
141 ConfigWizardStage::EnteringAccountUsername
142 if !wizard.current_input.is_empty() =>
143 {
144 wizard.account_username = wizard.current_input.clone();
145 wizard.current_input.clear();
146 wizard.stage = ConfigWizardStage::EnteringAccountPassword;
147 }
148 ConfigWizardStage::EnteringAccountPassword
149 if !wizard.current_input.is_empty() =>
150 {
151 wizard.account_password = wizard.current_input.clone();
152 wizard.current_input.clear();
153 wizard.stage = ConfigWizardStage::EnteringCharacterName;
154 }
155 ConfigWizardStage::EnteringCharacterName => {
156 // Allow empty input (skip character config)
157 wizard.character_name = wizard.current_input.clone();
158 wizard.current_input.clear();
159 wizard.stage = ConfigWizardStage::Confirming;
160 }
161 ConfigWizardStage::Confirming => {
162 wizard.stage = ConfigWizardStage::Complete;
163 }
164 _ => {}
165 },
166 KeyCode::Esc => {
167 return Err("Configuration cancelled".into());
168 }
169 _ => {}
170 }
171 }
172 } else {
173 break;
174 }
175 }
176 AppScreen::Launch => {
177 if let Some(wizard) = &mut self.launch_wizard {
178 // Auto-advance from Welcome stage
179 if wizard.stage == WizardStage::Welcome {
180 std::thread::sleep(std::time::Duration::from_millis(500));
181 wizard.stage = WizardStage::SelectingServer;
182 continue;
183 }
184
185 // Check if wizard is complete
186 if wizard.stage == WizardStage::Complete {
187 break;
188 }
189
190 // Poll for keyboard events
191 if event::poll(std::time::Duration::from_millis(100))?
192 && let Event::Key(key) = event::read()?
193 {
194 match key.code {
195 KeyCode::Up => match wizard.stage {
196 WizardStage::SelectingServer
197 if wizard.selected_server_idx > 0 =>
198 {
199 wizard.selected_server_idx -= 1;
200 }
201 WizardStage::SelectingAccount
202 if wizard.selected_account_idx > 0 =>
203 {
204 wizard.selected_account_idx -= 1;
205 }
206 _ => {}
207 },
208 KeyCode::Down => match wizard.stage {
209 WizardStage::SelectingServer
210 if wizard.selected_server_idx
211 < wizard.server_list.len() - 1 =>
212 {
213 wizard.selected_server_idx += 1;
214 }
215 WizardStage::SelectingAccount
216 if wizard.selected_account_idx
217 < wizard.account_list.len() - 1 =>
218 {
219 wizard.selected_account_idx += 1;
220 }
221 _ => {}
222 },
223 KeyCode::Enter => match wizard.stage {
224 WizardStage::SelectingServer => {
225 wizard.stage = WizardStage::SelectingAccount;
226 }
227 WizardStage::SelectingAccount => {
228 wizard.stage = WizardStage::Confirming;
229 }
230 WizardStage::Confirming => {
231 wizard.stage = WizardStage::Complete;
232 }
233 _ => {}
234 },
235 KeyCode::Esc => {
236 return Err("Selection cancelled".into());
237 }
238 KeyCode::Char('c')
239 if key.modifiers.contains(KeyModifiers::CONTROL) =>
240 {
241 return Err("Selection cancelled".into());
242 }
243 _ => {}
244 }
245 }
246 } else {
247 break;
248 }
249 }
250 }
251 }
252
253 Ok(())
254 }
255}
256
257pub enum AppScreen {
258 #[allow(dead_code)]
259 Setup,
260 Launch,
261 Config,
262}
263
264struct SetupWizard {}
265
266impl SetupWizard {
267 fn new() -> Self {
268 Self {}
269 }
270}
271
272impl Draw for SetupWizard {
273 fn draw(self, _app: &mut App, frame: &mut Frame) -> Result<(), Box<dyn Error>> {
274 use ratatui::text::Line;
275 use ratatui::widgets::Block;
276
277 let area = frame.area();
278
279 let block = Block::new().title(Line::from("Progress").centered());
280 frame.render_widget(block, area);
281
282 Ok(())
283 }
284}