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