1use anyhow::{anyhow, Result};
4use crossterm::event::{
5 self, DisableMouseCapture, EnableMouseCapture, Event, KeyCode, KeyEventKind,
6};
7use crossterm::execute;
8use crossterm::terminal::{
9 disable_raw_mode, enable_raw_mode, EnterAlternateScreen, LeaveAlternateScreen,
10};
11use ratatui::backend::CrosstermBackend;
12use ratatui::layout::{Constraint, Direction, Layout};
13use ratatui::style::{Color, Modifier, Style};
14use ratatui::widgets::{Block, Borders, List, ListItem, ListState, Paragraph};
15use ratatui::Terminal;
16use std::io::stdout;
17
18use crate::client::RommClient;
19use crate::config::{
20 load_config, load_layered_env, normalize_romm_origin, persist_user_config, AuthConfig, Config,
21};
22
23#[derive(Clone, Copy, PartialEq, Eq)]
24enum AuthKind {
25 None,
26 Basic,
27 Bearer,
28 ApiKey,
29}
30
31#[derive(Clone, Copy, PartialEq, Eq)]
32enum Step {
33 Url,
34 Https,
35 Download,
36 AuthMenu,
37 BasicUser,
38 BasicPass,
39 Bearer,
40 ApiHeader,
41 ApiKey,
42 Summary,
43}
44
45pub struct SetupWizard {
47 step: Step,
48 auth_kind: AuthKind,
49 auth_menu_selected: usize,
50 url: String,
51 url_cursor: usize,
52 download_dir: String,
53 dl_cursor: usize,
54 username: String,
55 user_cursor: usize,
56 password: String,
57 bearer_token: String,
58 bearer_cursor: usize,
59 api_header: String,
60 header_cursor: usize,
61 api_key: String,
62 api_key_cursor: usize,
63 pub testing: bool,
64 pub use_https: bool,
65 pub error: Option<String>,
66}
67
68impl SetupWizard {
69 pub fn new() -> Self {
70 let default_dl = dirs::download_dir()
71 .unwrap_or_else(|| dirs::home_dir().unwrap_or_default().join("Downloads"))
72 .join("romm-cli")
73 .display()
74 .to_string();
75 Self {
76 step: Step::Url,
77 auth_kind: AuthKind::None,
78 auth_menu_selected: 0,
79 url: "https://".to_string(),
80 url_cursor: "https://".len(),
81 download_dir: default_dl,
82 dl_cursor: 0,
83 username: String::new(),
84 user_cursor: 0,
85 password: String::new(),
86 bearer_token: String::new(),
87 bearer_cursor: 0,
88 api_header: String::new(),
89 header_cursor: 0,
90 api_key: String::new(),
91 api_key_cursor: 0,
92 testing: false,
93 use_https: true,
94 error: None,
95 }
96 }
97
98 pub fn new_auth_only(config: &Config) -> Self {
99 let mut wizard = Self::new();
100 wizard.step = Step::AuthMenu;
101 wizard.url = config.base_url.clone();
102 wizard.download_dir = config.download_dir.clone();
103 wizard.use_https = config.use_https;
104
105 match &config.auth {
106 Some(AuthConfig::Basic { username, .. }) => {
107 wizard.auth_kind = AuthKind::Basic;
108 wizard.auth_menu_selected = 1;
109 wizard.username = username.clone();
110 wizard.user_cursor = username.len();
111 }
112 Some(AuthConfig::Bearer { token }) => {
113 wizard.auth_kind = AuthKind::Bearer;
114 wizard.auth_menu_selected = 2;
115 wizard.bearer_token = token.clone();
116 wizard.bearer_cursor = token.len();
117 }
118 Some(AuthConfig::ApiKey { header, key }) => {
119 wizard.auth_kind = AuthKind::ApiKey;
120 wizard.auth_menu_selected = 3;
121 wizard.api_header = header.clone();
122 wizard.header_cursor = header.len();
123 wizard.api_key = key.clone();
124 wizard.api_key_cursor = key.len();
125 }
126 None => {
127 wizard.auth_kind = AuthKind::None;
128 wizard.auth_menu_selected = 0;
129 }
130 }
131 wizard
132 }
133
134 fn auth_labels() -> [&'static str; 4] {
135 [
136 "No authentication",
137 "Basic (username + password)",
138 "API Token (Bearer)",
139 "API key in custom header",
140 ]
141 }
142
143 fn auth_kind_from_index(i: usize) -> AuthKind {
144 match i {
145 1 => AuthKind::Basic,
146 2 => AuthKind::Bearer,
147 3 => AuthKind::ApiKey,
148 _ => AuthKind::None,
149 }
150 }
151
152 fn build_config(&self) -> Result<Config> {
153 let base_url = normalize_romm_origin(self.url.trim());
154 if base_url.is_empty() {
155 return Err(anyhow!("Server URL cannot be empty"));
156 }
157 let auth: Option<AuthConfig> = match self.auth_kind {
158 AuthKind::None => None,
159 AuthKind::Basic => {
160 let u = self.username.trim();
161 if u.is_empty() {
162 return Err(anyhow!("Username cannot be empty"));
163 }
164 if self.password.is_empty() {
165 return Err(anyhow!("Password cannot be empty"));
166 }
167 Some(AuthConfig::Basic {
168 username: u.to_string(),
169 password: self.password.clone(),
170 })
171 }
172 AuthKind::Bearer => {
173 if self.bearer_token.trim().is_empty() {
174 return Err(anyhow!("Bearer token cannot be empty"));
175 }
176 Some(AuthConfig::Bearer {
177 token: self.bearer_token.trim().to_string(),
178 })
179 }
180 AuthKind::ApiKey => {
181 let h = self.api_header.trim();
182 if h.is_empty() {
183 return Err(anyhow!("Header name cannot be empty"));
184 }
185 if self.api_key.is_empty() {
186 return Err(anyhow!("API key cannot be empty"));
187 }
188 Some(AuthConfig::ApiKey {
189 header: h.to_string(),
190 key: self.api_key.clone(),
191 })
192 }
193 };
194 Ok(Config {
195 base_url,
196 download_dir: self.download_dir.trim().to_string(),
197 use_https: self.use_https,
198 auth,
199 })
200 }
201
202 pub fn render(&mut self, f: &mut ratatui::Frame, area: ratatui::layout::Rect) {
203 let title = match self.step {
204 Step::Url => "Step 1/5 — RomM server URL",
205 Step::Https => "Step 2/5 — Secure connection",
206 Step::Download => "Step 3/5 — Download directory",
207 Step::AuthMenu => "Step 4/5 — Authentication",
208 Step::BasicUser | Step::BasicPass => "Step 5/5 — Basic auth",
209 Step::Bearer => "Step 5/5 — API Token",
210 Step::ApiHeader | Step::ApiKey => "Step 5/5 — API key",
211 Step::Summary => "Review & connect",
212 };
213
214 let main = Layout::default()
215 .direction(Direction::Vertical)
216 .constraints([
217 Constraint::Length(3),
218 Constraint::Min(6),
219 Constraint::Length(4),
220 ])
221 .split(area);
222
223 let hint_top = "Same origin as in your browser (no trailing /api). Esc: quit";
224 let p = Paragraph::new(hint_top).style(Style::default().fg(Color::DarkGray));
225 f.render_widget(p, main[0]);
226
227 match self.step {
228 Step::Url => {
229 let line = format!(
230 "{}▏",
231 self.url.chars().take(self.url_cursor).collect::<String>()
232 );
233 let rest: String = self.url.chars().skip(self.url_cursor).collect();
234 let text = format!("{line}{rest}");
235 let block = Block::default().title(title).borders(Borders::ALL);
236 let p = Paragraph::new(text).block(block);
237 f.render_widget(p, main[1]);
238 }
239 Step::Https => {
240 let text = if self.use_https {
241 "[X] Use HTTPS (Recommended)"
242 } else {
243 "[ ] Use HTTPS (Insecure)"
244 };
245 let block = Block::default().title(title).borders(Borders::ALL);
246 let p = Paragraph::new(format!("\n {}\n\n Space: toggle Enter: next", text))
247 .block(block);
248 f.render_widget(p, main[1]);
249 }
250 Step::Download => {
251 let line = format!(
252 "{}▏",
253 self.download_dir
254 .chars()
255 .take(self.dl_cursor)
256 .collect::<String>()
257 );
258 let rest: String = self.download_dir.chars().skip(self.dl_cursor).collect();
259 let text = format!("{line}{rest}");
260 let block = Block::default().title(title).borders(Borders::ALL);
261 let p = Paragraph::new(text).block(block);
262 f.render_widget(p, main[1]);
263 }
264 Step::AuthMenu => {
265 let items: Vec<ListItem> = Self::auth_labels()
266 .iter()
267 .map(|s| ListItem::new(*s))
268 .collect();
269 let mut state = ListState::default();
270 state.select(Some(self.auth_menu_selected));
271 let list = List::new(items)
272 .block(Block::default().title(title).borders(Borders::ALL))
273 .highlight_style(
274 Style::default()
275 .fg(Color::Yellow)
276 .add_modifier(Modifier::BOLD),
277 )
278 .highlight_symbol(">> ");
279 f.render_stateful_widget(list, main[1], &mut state);
280 }
281 Step::BasicUser | Step::BasicPass => {
282 let user_line = if self.step == Step::BasicUser {
283 format!(
284 "{}▏{}",
285 self.username
286 .chars()
287 .take(self.user_cursor)
288 .collect::<String>(),
289 self.username
290 .chars()
291 .skip(self.user_cursor)
292 .collect::<String>()
293 )
294 } else {
295 self.username.clone()
296 };
297 let pass_display: String = if self.step == Step::BasicPass {
298 "•".repeat(self.password.len()) + "▏"
299 } else {
300 "•".repeat(self.password.len())
301 };
302 let block = Block::default().title(title).borders(Borders::ALL);
303 let body = format!("Username\n{user_line}\n\nPassword (hidden)\n{pass_display}\n\nTab: switch field");
304 let p = Paragraph::new(body).block(block);
305 f.render_widget(p, main[1]);
306 }
307 Step::Bearer => {
308 let line = format!(
309 "{}▏{}",
310 self.bearer_token
311 .chars()
312 .take(self.bearer_cursor)
313 .collect::<String>(),
314 self.bearer_token
315 .chars()
316 .skip(self.bearer_cursor)
317 .collect::<String>()
318 );
319 let block = Block::default().title(title).borders(Borders::ALL);
320 let p = Paragraph::new(line).block(block);
321 f.render_widget(p, main[1]);
322 }
323 Step::ApiHeader | Step::ApiKey => {
324 let header_line = if self.step == Step::ApiHeader {
325 format!(
326 "{}▏{}",
327 self.api_header
328 .chars()
329 .take(self.header_cursor)
330 .collect::<String>(),
331 self.api_header
332 .chars()
333 .skip(self.header_cursor)
334 .collect::<String>()
335 )
336 } else {
337 self.api_header.clone()
338 };
339 let key_line = if self.step == Step::ApiKey {
340 "•".repeat(self.api_key.len()) + "▏"
341 } else {
342 "•".repeat(self.api_key.len())
343 };
344 let body = format!(
345 "Header name\n{header_line}\n\nKey (hidden)\n{key_line}\n\nTab: switch field"
346 );
347 let block = Block::default().title(title).borders(Borders::ALL);
348 let p = Paragraph::new(body).block(block);
349 f.render_widget(p, main[1]);
350 }
351 Step::Summary => {
352 let url_line = normalize_romm_origin(self.url.trim());
353 let auth_desc = match self.auth_kind {
354 AuthKind::None => "None",
355 AuthKind::Basic => "Basic",
356 AuthKind::Bearer => "API Token",
357 AuthKind::ApiKey => "API key header",
358 };
359 let mut lines = vec![
360 format!("Server: {url_line}"),
361 format!("Downloads: {}", self.download_dir.trim()),
362 format!("Use HTTPS: {}", if self.use_https { "Yes" } else { "No" }),
363 format!("Auth: {auth_desc}"),
364 String::new(),
365 ];
366 if self.testing {
367 lines.push("Connecting to server…".to_string());
368 } else if let Some(ref e) = self.error {
369 lines.push(format!("Last error: {e}"));
370 } else {
371 lines.push("Enter: test connection and save Esc: quit".to_string());
372 }
373 let block = Block::default().title(title).borders(Borders::ALL);
374 let p = Paragraph::new(lines.join("\n")).block(block);
375 f.render_widget(p, main[1]);
376 }
377 }
378
379 let footer = match self.step {
380 Step::Url => "Enter: next Backspace: delete Esc: quit",
381 Step::Https => "Space: toggle Enter: next Esc: quit",
382 Step::Download => "Enter: next Backspace: delete Esc: quit",
383 Step::AuthMenu => "↑/↓: choose Enter: next Esc: quit",
384 Step::BasicUser | Step::BasicPass => {
385 "Type text Tab: switch field Enter: next step Esc: quit"
386 }
387 Step::Bearer => "Enter: next step Esc: quit",
388 Step::ApiHeader | Step::ApiKey => "Tab: switch field Enter: next step Esc: quit",
389 Step::Summary => {
390 if self.testing {
391 "Please wait…"
392 } else {
393 "Enter: connect & save"
394 }
395 }
396 };
397 let p = Paragraph::new(footer)
398 .style(Style::default().fg(Color::Cyan))
399 .block(Block::default().borders(Borders::ALL));
400 f.render_widget(p, main[2]);
401 }
402
403 pub fn cursor_pos(&self, area: ratatui::layout::Rect) -> Option<(u16, u16)> {
404 let main = Layout::default()
405 .direction(Direction::Vertical)
406 .constraints([
407 Constraint::Length(3),
408 Constraint::Min(6),
409 Constraint::Length(4),
410 ])
411 .split(area);
412 let inner = main[1];
413 match self.step {
414 Step::Url => {
415 let x = inner.x + 1 + self.url_cursor.min(self.url.len()) as u16;
416 Some((x, inner.y + 1))
417 }
418 Step::Download => {
419 let x = inner.x + 1 + self.dl_cursor.min(self.download_dir.len()) as u16;
420 Some((x, inner.y + 1))
421 }
422 Step::Bearer => {
423 let x = inner.x + 1 + self.bearer_cursor.min(self.bearer_token.len()) as u16;
424 Some((x, inner.y + 1))
425 }
426 Step::BasicUser => {
427 let x = inner.x + 1 + self.user_cursor.min(self.username.len()) as u16;
428 Some((x, inner.y + 2))
429 }
430 Step::BasicPass => {
431 let x = inner.x + 1 + "•".repeat(self.password.len()).len() as u16;
432 Some((x, inner.y + 6))
433 }
434 Step::ApiHeader => {
435 let x = inner.x + 1 + self.header_cursor.min(self.api_header.len()) as u16;
436 Some((x, inner.y + 2))
437 }
438 Step::ApiKey => {
439 let x = inner.x + 1 + self.api_key_cursor.min(self.api_key.len()) as u16;
440 Some((x, inner.y + 6))
441 }
442 Step::Https | Step::AuthMenu | Step::Summary => None,
443 }
444 }
445
446 fn add_char_url(&mut self, c: char) {
447 let pos = self.url_cursor.min(self.url.len());
448 self.url.insert(pos, c);
449 self.url_cursor = pos + 1;
450 }
451
452 fn del_char_url(&mut self) {
453 if self.url_cursor > 0 && self.url_cursor <= self.url.len() {
454 self.url.remove(self.url_cursor - 1);
455 self.url_cursor -= 1;
456 }
457 }
458
459 fn add_char_dl(&mut self, c: char) {
460 let pos = self.dl_cursor.min(self.download_dir.len());
461 self.download_dir.insert(pos, c);
462 self.dl_cursor = pos + 1;
463 }
464
465 fn del_char_dl(&mut self) {
466 if self.dl_cursor > 0 && self.dl_cursor <= self.download_dir.len() {
467 self.download_dir.remove(self.dl_cursor - 1);
468 self.dl_cursor -= 1;
469 }
470 }
471
472 fn advance_from_auth_menu(&mut self) {
473 self.auth_kind = Self::auth_kind_from_index(self.auth_menu_selected);
474 self.step = match self.auth_kind {
475 AuthKind::None => Step::Summary,
476 AuthKind::Basic => Step::BasicUser,
477 AuthKind::Bearer => Step::Bearer,
478 AuthKind::ApiKey => Step::ApiHeader,
479 };
480 }
481
482 fn advance_step(&mut self) -> Result<()> {
483 self.error = None;
484 match self.step {
485 Step::Url => {
486 if normalize_romm_origin(self.url.trim()).is_empty() {
487 self.error = Some("Enter a valid server URL".to_string());
488 return Ok(());
489 }
490 self.step = Step::Https;
491 }
492 Step::Https => {
493 self.step = Step::Download;
494 self.dl_cursor = self.download_dir.len();
495 }
496 Step::Download => {
497 if self.download_dir.trim().is_empty() {
498 self.error = Some("Download path cannot be empty".to_string());
499 return Ok(());
500 }
501 self.step = Step::AuthMenu;
502 }
503 Step::AuthMenu => self.advance_from_auth_menu(),
504 Step::BasicUser => self.step = Step::BasicPass,
505 Step::BasicPass => self.step = Step::Summary,
506 Step::Bearer => self.step = Step::Summary,
507 Step::ApiHeader => self.step = Step::ApiKey,
508 Step::ApiKey => self.step = Step::Summary,
509 Step::Summary => {}
510 }
511 Ok(())
512 }
513
514 pub async fn try_connect_and_persist(&mut self, verbose: bool) -> Result<Config> {
515 let cfg = self.build_config()?;
516 let client = RommClient::new(&cfg, verbose)?;
517 client.fetch_openapi_json().await?;
518 let base = cfg.base_url.clone();
519 let download = self.download_dir.trim().to_string();
520 persist_user_config(&base, &download, self.use_https, cfg.auth.clone())?;
521 load_layered_env();
522 load_config()
523 }
524
525 pub fn handle_key(&mut self, key: crossterm::event::KeyEvent) -> Result<bool> {
526 if key.kind != KeyEventKind::Press {
527 return Ok(false);
528 }
529 if key.code == KeyCode::Esc {
530 return Ok(true); }
532
533 if self.testing {
534 return Ok(false);
535 }
536
537 match self.step {
538 Step::Url => match key.code {
539 KeyCode::Enter => {
540 let _ = self.advance_step();
541 }
542 KeyCode::Char(c) => self.add_char_url(c),
543 KeyCode::Backspace => self.del_char_url(),
544 KeyCode::Left => {
545 if self.url_cursor > 0 {
546 self.url_cursor -= 1;
547 }
548 }
549 KeyCode::Right => {
550 if self.url_cursor < self.url.len() {
551 self.url_cursor += 1;
552 }
553 }
554 _ => {}
555 },
556 Step::Https => match key.code {
557 KeyCode::Enter => {
558 let _ = self.advance_step();
559 }
560 KeyCode::Char(' ') => self.use_https = !self.use_https,
561 _ => {}
562 },
563 Step::Download => match key.code {
564 KeyCode::Enter => {
565 let _ = self.advance_step();
566 }
567 KeyCode::Char(c) => self.add_char_dl(c),
568 KeyCode::Backspace => self.del_char_dl(),
569 KeyCode::Left => {
570 if self.dl_cursor > 0 {
571 self.dl_cursor -= 1;
572 }
573 }
574 KeyCode::Right => {
575 if self.dl_cursor < self.download_dir.len() {
576 self.dl_cursor += 1;
577 }
578 }
579 _ => {}
580 },
581 Step::AuthMenu => match key.code {
582 KeyCode::Up | KeyCode::Char('k') => {
583 if self.auth_menu_selected > 0 {
584 self.auth_menu_selected -= 1;
585 }
586 }
587 KeyCode::Down | KeyCode::Char('j') => {
588 if self.auth_menu_selected < 3 {
589 self.auth_menu_selected += 1;
590 }
591 }
592 KeyCode::Enter => {
593 let _ = self.advance_step();
594 }
595 _ => {}
596 },
597 Step::BasicUser => match key.code {
598 KeyCode::Tab => self.step = Step::BasicPass,
599 KeyCode::Enter => {
600 let _ = self.advance_step();
601 }
602 KeyCode::Char(c) => {
603 let pos = self.user_cursor.min(self.username.len());
604 self.username.insert(pos, c);
605 self.user_cursor = pos + 1;
606 }
607 KeyCode::Backspace => {
608 if self.user_cursor > 0 && self.user_cursor <= self.username.len() {
609 self.username.remove(self.user_cursor - 1);
610 self.user_cursor -= 1;
611 }
612 }
613 KeyCode::Left => {
614 if self.user_cursor > 0 {
615 self.user_cursor -= 1;
616 }
617 }
618 KeyCode::Right => {
619 if self.user_cursor < self.username.len() {
620 self.user_cursor += 1;
621 }
622 }
623 _ => {}
624 },
625 Step::BasicPass => match key.code {
626 KeyCode::Tab => self.step = Step::BasicUser,
627 KeyCode::Enter => {
628 let _ = self.advance_step();
629 }
630 KeyCode::Char(c) => self.password.push(c),
631 KeyCode::Backspace => {
632 self.password.pop();
633 }
634 _ => {}
635 },
636 Step::Bearer => match key.code {
637 KeyCode::Enter => {
638 let _ = self.advance_step();
639 }
640 KeyCode::Char(c) => {
641 let pos = self.bearer_cursor.min(self.bearer_token.len());
642 self.bearer_token.insert(pos, c);
643 self.bearer_cursor = pos + 1;
644 }
645 KeyCode::Backspace => {
646 if self.bearer_cursor > 0 && self.bearer_cursor <= self.bearer_token.len() {
647 self.bearer_token.remove(self.bearer_cursor - 1);
648 self.bearer_cursor -= 1;
649 }
650 }
651 KeyCode::Left => {
652 if self.bearer_cursor > 0 {
653 self.bearer_cursor -= 1;
654 }
655 }
656 KeyCode::Right => {
657 if self.bearer_cursor < self.bearer_token.len() {
658 self.bearer_cursor += 1;
659 }
660 }
661 _ => {}
662 },
663 Step::ApiHeader => match key.code {
664 KeyCode::Tab => self.step = Step::ApiKey,
665 KeyCode::Enter => {
666 let _ = self.advance_step();
667 }
668 KeyCode::Char(c) => {
669 let pos = self.header_cursor.min(self.api_header.len());
670 self.api_header.insert(pos, c);
671 self.header_cursor = pos + 1;
672 }
673 KeyCode::Backspace => {
674 if self.header_cursor > 0 && self.header_cursor <= self.api_header.len() {
675 self.api_header.remove(self.header_cursor - 1);
676 self.header_cursor -= 1;
677 }
678 }
679 KeyCode::Left => {
680 if self.header_cursor > 0 {
681 self.header_cursor -= 1;
682 }
683 }
684 KeyCode::Right => {
685 if self.header_cursor < self.api_header.len() {
686 self.header_cursor += 1;
687 }
688 }
689 _ => {}
690 },
691 Step::ApiKey => match key.code {
692 KeyCode::Tab => self.step = Step::ApiHeader,
693 KeyCode::Enter => {
694 let _ = self.advance_step();
695 }
696 KeyCode::Char(c) => {
697 let pos = self.api_key_cursor.min(self.api_key.len());
698 self.api_key.insert(pos, c);
699 self.api_key_cursor = pos + 1;
700 }
701 KeyCode::Backspace => {
702 if self.api_key_cursor > 0 && self.api_key_cursor <= self.api_key.len() {
703 self.api_key.remove(self.api_key_cursor - 1);
704 self.api_key_cursor -= 1;
705 }
706 }
707 KeyCode::Left => {
708 if self.api_key_cursor > 0 {
709 self.api_key_cursor -= 1;
710 }
711 }
712 KeyCode::Right => {
713 if self.api_key_cursor < self.api_key.len() {
714 self.api_key_cursor += 1;
715 }
716 }
717 _ => {}
718 },
719 Step::Summary => {
720 if key.code == KeyCode::Enter {
721 self.testing = true;
722 self.error = None;
723 }
726 }
727 }
728 Ok(false)
729 }
730
731 pub async fn run(mut self, verbose: bool) -> Result<Config> {
732 enable_raw_mode()?;
733 let mut stdout = stdout();
734 execute!(stdout, EnterAlternateScreen, EnableMouseCapture)?;
735 let backend = CrosstermBackend::new(stdout);
736 let mut terminal = Terminal::new(backend)?;
737
738 loop {
739 terminal.draw(|f| {
740 let area = f.size();
741 self.render(f, area);
742 if let Some((x, y)) = self.cursor_pos(area) {
743 f.set_cursor(x, y);
744 }
745 })?;
746
747 if event::poll(std::time::Duration::from_millis(100))? {
748 if let Event::Key(key) = event::read()? {
749 if self.handle_key(key)? {
750 disable_raw_mode()?;
751 execute!(
752 terminal.backend_mut(),
753 LeaveAlternateScreen,
754 DisableMouseCapture
755 )?;
756 terminal.show_cursor()?;
757 return Err(anyhow!("setup cancelled"));
758 }
759
760 if self.testing {
761 terminal.draw(|f| {
762 let area = f.size();
763 self.render(f, area);
764 })?;
765 let result = self.try_connect_and_persist(verbose).await;
766 self.testing = false;
767 match result {
768 Ok(cfg) => {
769 disable_raw_mode()?;
770 execute!(
771 terminal.backend_mut(),
772 LeaveAlternateScreen,
773 DisableMouseCapture
774 )?;
775 terminal.show_cursor()?;
776 return Ok(cfg);
777 }
778 Err(e) => {
779 self.error = Some(format!("{e:#}"));
780 }
781 }
782 }
783 }
784 }
785 }
786 }
787}
788
789impl Default for SetupWizard {
790 fn default() -> Self {
791 Self::new()
792 }
793}