Skip to main content

git_same/setup/
mod.rs

1//! Interactive setup wizard for creating workspace configurations.
2//!
3//! This module provides a self-contained ratatui mini-app that guides
4//! the user through setting up a workspace: selecting a provider,
5//! authenticating, selecting organizations, and choosing a base path.
6
7pub mod handler;
8pub mod screens;
9pub mod state;
10pub mod ui;
11
12use crate::errors::Result;
13use crossterm::{
14    event::{DisableMouseCapture, EnableMouseCapture, Event as CtEvent},
15    execute,
16    terminal::{disable_raw_mode, enable_raw_mode, EnterAlternateScreen, LeaveAlternateScreen},
17};
18use ratatui::backend::CrosstermBackend;
19use ratatui::Terminal;
20use state::{SetupOutcome, SetupState, SetupStep};
21use std::io;
22use std::time::Duration;
23
24/// Run the setup wizard.
25///
26/// Returns `Ok(true)` if the wizard completed (workspace saved),
27/// `Ok(false)` if the user cancelled.
28pub async fn run_setup() -> Result<bool> {
29    let default_path = std::env::current_dir()
30        .map(|p| state::tilde_collapse(&p.to_string_lossy()))
31        .unwrap_or_else(|_| "~/Git-Same/GitHub".to_string());
32    let mut state = SetupState::new(&default_path);
33
34    struct SetupTerminalGuard {
35        raw_enabled: bool,
36        alt_enabled: bool,
37    }
38    impl Drop for SetupTerminalGuard {
39        fn drop(&mut self) {
40            if self.alt_enabled {
41                let mut stdout = io::stdout();
42                let _ = execute!(stdout, LeaveAlternateScreen, DisableMouseCapture);
43            }
44            if self.raw_enabled {
45                let _ = disable_raw_mode();
46            }
47        }
48    }
49
50    // Setup terminal
51    enable_raw_mode()?;
52    let mut guard = SetupTerminalGuard {
53        raw_enabled: true,
54        alt_enabled: false,
55    };
56    let mut stdout = io::stdout();
57    execute!(stdout, EnterAlternateScreen, EnableMouseCapture)?;
58    guard.alt_enabled = true;
59    let backend = CrosstermBackend::new(stdout);
60    let mut terminal = Terminal::new(backend)?;
61
62    // Main loop
63    let result = run_wizard(&mut terminal, &mut state).await;
64
65    // Restore terminal (always, even on error)
66    let _ = disable_raw_mode();
67    guard.raw_enabled = false;
68    let _ = execute!(
69        terminal.backend_mut(),
70        LeaveAlternateScreen,
71        DisableMouseCapture
72    );
73    guard.alt_enabled = false;
74    let _ = terminal.show_cursor();
75
76    result?;
77
78    Ok(matches!(state.outcome, Some(SetupOutcome::Completed)))
79}
80
81async fn run_wizard(
82    terminal: &mut Terminal<CrosstermBackend<io::Stdout>>,
83    state: &mut SetupState,
84) -> Result<()> {
85    loop {
86        terminal.draw(|frame| ui::render(state, frame))?;
87
88        // If we're on the requirements step and checks have not run yet,
89        // run checks before waiting for key input.
90        if maybe_start_requirements_checks(state) {
91            terminal.draw(|frame| ui::render(state, frame))?;
92            run_requirements_checks(state).await;
93            continue;
94        }
95
96        // If we're on the orgs step and loading, trigger discovery before waiting for input
97        if state.step == SetupStep::SelectOrgs && state.org_loading {
98            // Render loading state first, then do discovery
99            terminal.draw(|frame| ui::render(state, frame))?;
100            handler::handle_key(
101                state,
102                crossterm::event::KeyEvent::new(
103                    crossterm::event::KeyCode::Null,
104                    crossterm::event::KeyModifiers::NONE,
105                ),
106            )
107            .await;
108            continue;
109        }
110
111        // Increment tick counter for animations
112        state.tick_count = state.tick_count.wrapping_add(1);
113
114        // Wait for input with a short timeout for responsive tick
115        if crossterm::event::poll(Duration::from_millis(100))? {
116            if let Ok(event) = crossterm::event::read() {
117                match event {
118                    CtEvent::Key(key) => {
119                        handler::handle_key(state, key).await;
120                    }
121                    CtEvent::Resize(_, _) => {
122                        // Terminal will re-render on next loop iteration
123                    }
124                    _ => {}
125                }
126            }
127        }
128
129        if state.should_quit {
130            break;
131        }
132    }
133    Ok(())
134}
135
136pub(crate) fn maybe_start_requirements_checks(state: &mut SetupState) -> bool {
137    if state.step != SetupStep::Requirements || state.checks_triggered {
138        return false;
139    }
140
141    state.checks_triggered = true;
142    state.checks_loading = true;
143    state.config_path_display = crate::config::Config::default_path()
144        .ok()
145        .map(|p| p.display().to_string());
146    true
147}
148
149pub(crate) fn apply_requirements_check_results(
150    state: &mut SetupState,
151    results: Vec<crate::checks::CheckResult>,
152) {
153    state.check_results = results;
154    state.checks_loading = false;
155}
156
157pub(crate) async fn run_requirements_checks(state: &mut SetupState) {
158    let results = crate::checks::check_requirements().await;
159    apply_requirements_check_results(state, results);
160}
161
162#[cfg(test)]
163#[path = "mod_tests.rs"]
164mod tests;