Skip to main content

romm_cli/tui/screens/
setup_wizard.rs

1//! First-run setup: server URL, download directory, authentication, test connection, persist config.
2
3use 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
45/// Interactive setup run before the main TUI when `API_BASE_URL` is missing.
46pub 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); // Signal to caller that we should exit/cancel
531        }
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                    // The caller handles the actual async try_connect_and_persist call
724                    // when they see testing = true.
725                }
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}