1use crate::config::WorkspaceProvider;
4use crate::types::ProviderKind;
5
6#[derive(Debug, Clone, Copy, PartialEq, Eq)]
8pub enum SetupStep {
9 Requirements,
11 SelectProvider,
13 Authenticate,
15 SelectOrgs,
17 SelectPath,
19 Confirm,
21 Complete,
23}
24
25#[derive(Debug, Clone)]
27pub struct OrgEntry {
28 pub name: String,
29 pub repo_count: usize,
30 pub selected: bool,
31}
32
33#[derive(Debug, Clone)]
35pub enum SetupOutcome {
36 Completed,
38 Cancelled,
40}
41
42#[derive(Debug, Clone)]
44pub struct ProviderChoice {
45 pub kind: ProviderKind,
46 pub label: String,
47 pub available: bool,
48}
49
50#[derive(Debug, Clone)]
52pub struct PathSuggestion {
53 pub path: String,
54 pub label: String,
55}
56
57#[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
67pub struct SetupState {
69 pub step: SetupStep,
71 pub should_quit: bool,
73 pub outcome: Option<SetupOutcome>,
75
76 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 pub provider_choices: Vec<ProviderChoice>,
85 pub provider_index: usize,
86
87 pub auth_status: AuthStatus,
89 pub username: Option<String>,
90 pub auth_token: Option<String>,
91
92 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 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 pub error_message: Option<String>,
117
118 pub tick_count: u64,
121 pub is_first_setup: bool,
123}
124
125#[derive(Debug, Clone, PartialEq, Eq)]
127pub enum AuthStatus {
128 Pending,
130 Checking,
132 Success,
134 Failed(String),
136}
137
138pub 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 pub fn new(default_base_path: &str) -> Self {
157 Self::with_first_setup(default_base_path, false)
158 }
159
160 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 pub fn selected_provider(&self) -> ProviderKind {
239 self.provider_choices[self.provider_index].kind
240 }
241
242 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 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 pub fn populate_path_suggestions(&mut self) {
263 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 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 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 pub const TOTAL_STEPS: usize = 6;
308
309 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 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;