Skip to main content

mobius_cli/frontend/
reinitialize.rs

1//! Confirmation before replacing existing gateway state.
2
3use std::io;
4use std::path::Path;
5
6use mobius::Result;
7use ratatui::Terminal;
8use ratatui::backend::CrosstermBackend;
9use ratatui::crossterm::event::{Event, KeyCode, KeyEvent, KeyEventKind, KeyModifiers};
10use ratatui::text::Line;
11use ratatui::widgets::{Block, Borders, Paragraph, Wrap};
12use tokio::time::MissedTickBehavior;
13
14use super::terminal::{INPUT_POLL, MAX_INPUT_BATCH, TerminalGuard, poll_event};
15use super::terminal_text;
16use super::theme::{Role, current};
17
18/// Asks whether existing gateway state may be permanently replaced.
19/// # Errors
20///
21/// Returns an error if validation or an operation required by this function fails.
22pub async fn confirm(state_dir: &Path) -> Result<bool> {
23    let mut guard = TerminalGuard::alternate()?;
24    guard.set_mouse_capture(false)?;
25    let mut terminal = Terminal::new(CrosstermBackend::new(io::stdout()))?;
26    terminal.clear()?;
27    let mut tick = tokio::time::interval(INPUT_POLL);
28    tick.set_missed_tick_behavior(MissedTickBehavior::Skip);
29    loop {
30        terminal.draw(|frame| render(frame, state_dir))?;
31        tick.tick().await;
32        for _ in 0..MAX_INPUT_BATCH {
33            let Some(event) = poll_event()? else {
34                break;
35            };
36            if let Event::Key(key) = event
37                && let Some(confirm) = decision(key)
38            {
39                return Ok(confirm);
40            }
41        }
42    }
43}
44
45fn decision(key: KeyEvent) -> Option<bool> {
46    if !matches!(key.kind, KeyEventKind::Press | KeyEventKind::Repeat) {
47        return None;
48    }
49    if key.modifiers.contains(KeyModifiers::CONTROL) && matches!(key.code, KeyCode::Char('c' | 'd'))
50    {
51        return Some(false);
52    }
53    match key.code {
54        KeyCode::Char('y' | 'Y') => Some(true),
55        KeyCode::Char('n' | 'N') | KeyCode::Esc => Some(false),
56        _ => None,
57    }
58}
59
60fn render(frame: &mut ratatui::Frame<'_>, state_dir: &Path) {
61    let theme = current();
62    let lines = vec![
63        Line::from(""),
64        Line::styled(
65            "  Gateway state already exists:",
66            theme.style(Role::Warning),
67        ),
68        Line::styled(
69            format!("  {}", terminal_text(&state_dir.display().to_string())),
70            theme.style(Role::Text),
71        ),
72        Line::from(""),
73        Line::styled(
74            "  Reinitialize it? This permanently deletes its configuration, chats, providers, and paired devices.",
75            theme.style(Role::Error),
76        ),
77        Line::from(""),
78        Line::styled(
79            "  y reinitialize · n/esc keep existing",
80            theme.style(Role::Muted),
81        ),
82    ];
83    frame.render_widget(
84        Paragraph::new(lines)
85            .block(
86                Block::default()
87                    .borders(Borders::ALL)
88                    .title(" Reinitialize möbius Gateway? "),
89            )
90            .style(theme.style(Role::Canvas))
91            .wrap(Wrap { trim: false }),
92        frame.area(),
93    );
94}
95
96#[cfg(test)]
97mod tests {
98    use super::*;
99
100    #[test]
101    fn only_an_explicit_yes_confirms_reinitialization() {
102        let key = KeyEvent::new(KeyCode::Char('y'), KeyModifiers::NONE);
103
104        assert_eq!(decision(key), Some(true));
105    }
106
107    #[test]
108    fn enter_does_not_confirm_reinitialization() {
109        let key = KeyEvent::new(KeyCode::Enter, KeyModifiers::NONE);
110
111        assert_eq!(decision(key), None);
112    }
113}