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> {
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}