Skip to main content

git_same/setup/
state.rs

1//! Setup wizard state (the "Model" in Elm architecture).
2
3use crate::config::WorkspaceProvider;
4use crate::types::ProviderKind;
5
6/// Which step of the wizard is active.
7#[derive(Debug, Clone, Copy, PartialEq, Eq)]
8pub enum SetupStep {
9    /// Step 1: System requirements check.
10    Requirements,
11    /// Step 2: Select a provider.
12    SelectProvider,
13    /// Step 3: Authenticate and detect username.
14    Authenticate,
15    /// Step 4: Discover and select organizations.
16    SelectOrgs,
17    /// Step 5: Enter the base path.
18    SelectPath,
19    /// Step 6: Review and save.
20    Confirm,
21    /// Success / completion screen.
22    Complete,
23}
24
25/// An organization entry in the org selector.
26#[derive(Debug, Clone)]
27pub struct OrgEntry {
28    pub name: String,
29    pub repo_count: usize,
30    pub selected: bool,
31}
32
33/// The outcome of the setup wizard.
34#[derive(Debug, Clone)]
35pub enum SetupOutcome {
36    /// User completed the wizard.
37    Completed,
38    /// User cancelled.
39    Cancelled,
40}
41
42/// Represents one of the provider choices shown in step 2.
43#[derive(Debug, Clone)]
44pub struct ProviderChoice {
45    pub kind: ProviderKind,
46    pub label: String,
47    pub available: bool,
48}
49
50/// A suggested directory path for the path selector.
51#[derive(Debug, Clone)]
52pub struct PathSuggestion {
53    pub path: String,
54    pub label: String,
55}
56
57/// A selectable directory entry in the inline path navigator.
58#[derive(Debug, Clone)]
59pub struct PathBrowseEntry {
60    pub label: String,
61    pub path: String,
62    pub depth: u16,
63    pub expanded: bool,
64    pub has_children: bool,
65}
66
67/// The wizard state (model).
68pub struct SetupState {
69    /// Current wizard step.
70    pub step: SetupStep,
71    /// Whether to quit the wizard.
72    pub should_quit: bool,
73    /// Outcome when done.
74    pub outcome: Option<SetupOutcome>,
75
76    // Step 1: Requirements check
77    pub check_results: Vec<crate::checks::CheckResult>,
78    pub checks_loading: bool,
79    pub checks_triggered: bool,
80    pub config_path_display: Option<String>,
81    pub config_was_created: bool,
82
83    // Step 2: Provider selection
84    pub provider_choices: Vec<ProviderChoice>,
85    pub provider_index: usize,
86
87    // Step 3: Authentication
88    pub auth_status: AuthStatus,
89    pub username: Option<String>,
90    pub auth_token: Option<String>,
91
92    // Step 4: Org selection
93    pub orgs: Vec<OrgEntry>,
94    pub org_index: usize,
95    pub org_loading: bool,
96    pub org_discovery_in_progress: bool,
97    pub org_error: Option<String>,
98
99    // Step 5: Path
100    pub base_path: String,
101    pub path_cursor: usize,
102    pub path_suggestions_mode: bool,
103    pub path_suggestions: Vec<PathSuggestion>,
104    pub path_suggestion_index: usize,
105    pub path_completions: Vec<String>,
106    pub path_completion_index: usize,
107    pub path_browse_mode: bool,
108    pub path_browse_current_dir: String,
109    pub path_browse_entries: Vec<PathBrowseEntry>,
110    pub path_browse_index: usize,
111    pub path_browse_show_hidden: bool,
112    pub path_browse_error: Option<String>,
113    pub path_browse_info: Option<String>,
114
115    // General
116    pub error_message: Option<String>,
117
118    // Animation / UX
119    /// Tick counter for spinner and animation effects.
120    pub tick_count: u64,
121    /// Whether this is the first workspace setup (controls UI text).
122    pub is_first_setup: bool,
123}
124
125/// Authentication status during step 3.
126#[derive(Debug, Clone, PartialEq, Eq)]
127pub enum AuthStatus {
128    /// Haven't checked yet.
129    Pending,
130    /// Currently checking.
131    Checking,
132    /// Authenticated successfully.
133    Success,
134    /// Authentication failed.
135    Failed(String),
136}
137
138/// Collapse an absolute path's home directory prefix into `~`.
139pub fn tilde_collapse(path: &str) -> String {
140    let home_var = std::env::var("HOME").or_else(|_| std::env::var("USERPROFILE"));
141    if let Ok(home) = home_var {
142        let home_path = std::path::Path::new(&home);
143        let p = std::path::Path::new(path);
144        if let Ok(suffix) = p.strip_prefix(home_path) {
145            if suffix.as_os_str().is_empty() {
146                return "~".to_string();
147            }
148            return format!("~{}{}", std::path::MAIN_SEPARATOR, suffix.to_string_lossy());
149        }
150    }
151    path.to_string()
152}
153
154impl SetupState {
155    /// Create initial wizard state.
156    pub fn new(default_base_path: &str) -> Self {
157        Self::with_first_setup(default_base_path, false)
158    }
159
160    /// Create wizard state, optionally marking as first-time setup.
161    pub fn with_first_setup(default_base_path: &str, is_first_setup: bool) -> Self {
162        let provider_choices = vec![
163            ProviderChoice {
164                kind: ProviderKind::GitHub,
165                label: "GitHub".to_string(),
166                available: true,
167            },
168            ProviderChoice {
169                kind: ProviderKind::GitHubEnterprise,
170                label: "GitHub Enterprise (coming soon)".to_string(),
171                available: false,
172            },
173            ProviderChoice {
174                kind: ProviderKind::GitLab,
175                label: "GitLab.com (coming soon)".to_string(),
176                available: false,
177            },
178            ProviderChoice {
179                kind: ProviderKind::GitLabSelfManaged,
180                label: "GitLab Self-Managed (coming soon)".to_string(),
181                available: false,
182            },
183            ProviderChoice {
184                kind: ProviderKind::Codeberg,
185                label: "Codeberg.org (coming soon)".to_string(),
186                available: false,
187            },
188            ProviderChoice {
189                kind: ProviderKind::Bitbucket,
190                label: "Bitbucket.org (coming soon)".to_string(),
191                available: false,
192            },
193        ];
194
195        let base_path = default_base_path.to_string();
196        let path_cursor = base_path.len();
197
198        Self {
199            step: SetupStep::Requirements,
200            should_quit: false,
201            outcome: None,
202            check_results: Vec::new(),
203            checks_loading: false,
204            checks_triggered: false,
205            config_path_display: None,
206            config_was_created: false,
207            provider_choices,
208            provider_index: 0,
209            auth_status: AuthStatus::Pending,
210            username: None,
211            auth_token: None,
212            base_path,
213            path_cursor,
214            path_suggestions_mode: false,
215            path_suggestions: Vec::new(),
216            path_suggestion_index: 0,
217            path_completions: Vec::new(),
218            path_completion_index: 0,
219            path_browse_mode: false,
220            path_browse_current_dir: String::new(),
221            path_browse_entries: Vec::new(),
222            path_browse_index: 0,
223            path_browse_show_hidden: false,
224            path_browse_error: None,
225            path_browse_info: None,
226            orgs: Vec::new(),
227            org_index: 0,
228            org_loading: false,
229            org_discovery_in_progress: false,
230            org_error: None,
231            error_message: None,
232            tick_count: 0,
233            is_first_setup,
234        }
235    }
236
237    /// Get the selected provider kind.
238    pub fn selected_provider(&self) -> ProviderKind {
239        self.provider_choices[self.provider_index].kind
240    }
241
242    /// Build the WorkspaceProvider from current state.
243    pub fn build_workspace_provider(&self) -> WorkspaceProvider {
244        let kind = self.selected_provider();
245        WorkspaceProvider {
246            kind,
247            api_url: None,
248            ..WorkspaceProvider::default()
249        }
250    }
251
252    /// Get selected org names.
253    pub fn selected_orgs(&self) -> Vec<String> {
254        self.orgs
255            .iter()
256            .filter(|o| o.selected)
257            .map(|o| o.name.clone())
258            .collect()
259    }
260
261    /// Populate the path suggestions list for the SelectPath step.
262    pub fn populate_path_suggestions(&mut self) {
263        // Keep step 5 path fixed unless the user explicitly selects a folder
264        // from the folder navigator popup.
265        self.path_suggestions = vec![PathSuggestion {
266            path: self.base_path.clone(),
267            label: "terminal folder".to_string(),
268        }];
269        self.path_suggestion_index = 0;
270        self.path_suggestions_mode = false;
271        self.path_browse_mode = false;
272        self.path_browse_current_dir.clear();
273        self.path_browse_entries.clear();
274        self.path_browse_index = 0;
275        self.path_browse_show_hidden = false;
276        self.path_browse_error = None;
277        self.path_browse_info = None;
278        self.path_completions.clear();
279        self.path_completion_index = 0;
280        self.path_cursor = self.base_path.len();
281    }
282
283    /// Whether all critical requirement checks have passed.
284    pub fn requirements_passed(&self) -> bool {
285        !self.check_results.is_empty()
286            && self
287                .check_results
288                .iter()
289                .filter(|r| r.critical)
290                .all(|r| r.passed)
291    }
292
293    /// The 1-based step number for display.
294    pub fn step_number(&self) -> usize {
295        match self.step {
296            SetupStep::Requirements => 1,
297            SetupStep::SelectProvider => 2,
298            SetupStep::Authenticate => 3,
299            SetupStep::SelectOrgs => 4,
300            SetupStep::SelectPath => 5,
301            SetupStep::Confirm => 6,
302            SetupStep::Complete => 6,
303        }
304    }
305
306    /// Total number of numbered steps (excluding Complete).
307    pub const TOTAL_STEPS: usize = 6;
308
309    /// Move to the next step.
310    pub fn next_step(&mut self) {
311        self.error_message = None;
312        self.step = match self.step {
313            SetupStep::Requirements => SetupStep::SelectProvider,
314            SetupStep::SelectProvider => SetupStep::Authenticate,
315            SetupStep::Authenticate => {
316                self.org_loading = true;
317                self.org_discovery_in_progress = false;
318                self.orgs.clear();
319                self.org_index = 0;
320                self.org_error = None;
321                SetupStep::SelectOrgs
322            }
323            SetupStep::SelectOrgs => {
324                self.populate_path_suggestions();
325                SetupStep::SelectPath
326            }
327            SetupStep::SelectPath => SetupStep::Confirm,
328            SetupStep::Confirm => SetupStep::Complete,
329            SetupStep::Complete => {
330                self.outcome = Some(SetupOutcome::Completed);
331                self.should_quit = true;
332                SetupStep::Complete
333            }
334        };
335    }
336
337    /// Move to the previous step.
338    pub fn prev_step(&mut self) {
339        self.error_message = None;
340        self.step = match self.step {
341            SetupStep::Requirements => {
342                self.outcome = Some(SetupOutcome::Cancelled);
343                self.should_quit = true;
344                SetupStep::Requirements
345            }
346            SetupStep::SelectProvider => SetupStep::Requirements,
347            SetupStep::Authenticate => SetupStep::SelectProvider,
348            SetupStep::SelectOrgs => SetupStep::Authenticate,
349            SetupStep::SelectPath => SetupStep::SelectOrgs,
350            SetupStep::Confirm => SetupStep::SelectPath,
351            SetupStep::Complete => SetupStep::Confirm,
352        };
353    }
354}
355
356#[cfg(test)]
357#[path = "state_tests.rs"]
358mod tests;