mobius_cli/frontend/
reinitialize.rs1use 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
18pub 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}