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