1use crate::config::{Config, WorkspaceConfig};
4use crate::setup::state::{self, SetupState};
5use crate::types::{OpSummary, OwnedRepo};
6use ratatui::widgets::TableState;
7use serde::{Deserialize, Serialize};
8use std::collections::HashMap;
9use std::path::PathBuf;
10use std::time::Instant;
11
12#[derive(Debug, Clone, Copy, PartialEq, Eq)]
14pub enum Screen {
15 WorkspaceSetup,
16 Workspaces,
17 Dashboard,
18 Sync,
19 Settings,
20}
21
22#[derive(Debug, Clone, Copy, PartialEq, Eq)]
24pub enum WorkspacePane {
25 Left,
26 Right,
27}
28
29#[derive(Debug, Clone, Copy, PartialEq, Eq)]
31pub enum Operation {
32 Sync,
33 Status,
34}
35
36impl std::fmt::Display for Operation {
37 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
38 match self {
39 Operation::Sync => write!(f, "Sync"),
40 Operation::Status => write!(f, "Status"),
41 }
42 }
43}
44
45#[derive(Debug, Clone)]
47pub enum OperationState {
48 Idle,
49 Discovering {
50 operation: Operation,
51 message: String,
52 },
53 Running {
54 operation: Operation,
55 total: usize,
56 completed: usize,
57 failed: usize,
58 skipped: usize,
59 current_repo: String,
60 with_updates: usize,
62 cloned: usize,
64 synced: usize,
66 to_clone: usize,
68 to_sync: usize,
70 total_new_commits: u32,
72 started_at: Instant,
74 active_repos: Vec<String>,
76 throughput_samples: Vec<u64>,
78 last_sample_completed: usize,
80 },
81 Finished {
82 operation: Operation,
83 summary: OpSummary,
84 with_updates: usize,
86 cloned: usize,
88 synced: usize,
90 total_new_commits: u32,
92 duration_secs: f64,
94 },
95}
96
97#[derive(Debug, Clone)]
99pub struct SyncLogEntry {
100 pub repo_name: String,
101 pub status: SyncLogStatus,
102 pub message: String,
103 pub had_updates: bool,
104 pub is_clone: bool,
105 pub new_commits: Option<u32>,
106 pub path: Option<PathBuf>,
107}
108
109#[derive(Debug, Clone, Copy, PartialEq, Eq)]
111pub enum SyncLogStatus {
112 Success,
113 Updated,
114 Cloned,
115 Failed,
116 Skipped,
117}
118
119#[derive(Debug, Clone, Copy, PartialEq, Eq)]
121pub enum LogFilter {
122 All,
123 Updated,
124 Failed,
125 Skipped,
126 Changelog,
127}
128
129#[derive(Debug, Clone, Serialize, Deserialize)]
131pub struct SyncHistoryEntry {
132 pub timestamp: String,
133 pub duration_secs: f64,
134 pub success: usize,
135 pub failed: usize,
136 pub skipped: usize,
137 pub with_updates: usize,
138 pub cloned: usize,
139 pub total_new_commits: u32,
140}
141
142#[derive(Debug, Clone)]
144pub struct RepoEntry {
145 pub owner: String,
146 pub name: String,
147 pub full_name: String,
148 pub path: PathBuf,
149 pub branch: Option<String>,
150 pub is_uncommitted: bool,
151 pub ahead: usize,
152 pub behind: usize,
153 pub staged_count: usize,
154 pub unstaged_count: usize,
155 pub untracked_count: usize,
156}
157
158#[derive(Debug, Clone)]
160pub struct CheckEntry {
161 pub name: String,
162 pub passed: bool,
163 pub message: String,
164 pub suggestion: Option<String>,
165 pub critical: bool,
166}
167
168pub struct App {
170 pub should_quit: bool,
172
173 pub screen: Screen,
175
176 pub screen_stack: Vec<Screen>,
178
179 pub config: Config,
181
182 pub workspaces: Vec<WorkspaceConfig>,
184
185 pub active_workspace: Option<WorkspaceConfig>,
187
188 pub workspace_index: usize,
190
191 pub workspace_pane: WorkspacePane,
193
194 pub base_path: Option<PathBuf>,
196
197 pub repos_by_org: HashMap<String, Vec<OwnedRepo>>,
199
200 pub all_repos: Vec<OwnedRepo>,
202
203 pub orgs: Vec<String>,
205
206 pub local_repos: Vec<RepoEntry>,
208
209 pub operation_state: OperationState,
211
212 pub log_lines: Vec<String>,
214
215 pub repo_index: usize,
218
219 pub scroll_offset: usize,
221
222 pub filter_text: String,
224
225 pub filter_active: bool,
227
228 pub dry_run: bool,
230
231 pub error_message: Option<String>,
233
234 pub check_results: Vec<CheckEntry>,
236
237 pub checks_loading: bool,
239
240 pub sync_pull: bool,
242
243 pub setup_state: Option<SetupState>,
245
246 pub status_loading: bool,
248
249 pub last_status_scan: Option<std::time::Instant>,
251
252 pub stat_index: usize,
254
255 pub dashboard_table_state: TableState,
257
258 pub settings_index: usize,
260
261 pub settings_config_expanded: bool,
263
264 pub workspace_detail_scroll: u16,
266
267 pub tick_count: u64,
269
270 pub sync_log_entries: Vec<SyncLogEntry>,
272
273 pub log_filter: LogFilter,
275
276 pub sync_history: Vec<SyncHistoryEntry>,
278
279 pub show_sync_history: bool,
281
282 pub expanded_repo: Option<String>,
284
285 pub repo_commits: Vec<String>,
287
288 pub sync_log_index: usize,
290
291 pub changelog_commits: HashMap<String, Vec<String>>,
293
294 pub changelog_total: usize,
296
297 pub changelog_loaded: usize,
299
300 pub changelog_scroll: usize,
302}
303
304impl App {
305 pub fn new(config: Config, workspaces: Vec<WorkspaceConfig>, config_was_created: bool) -> Self {
307 let (screen, active_workspace, base_path) = match workspaces.len() {
308 0 => (Screen::WorkspaceSetup, None, None),
309 1 => {
310 let ws = workspaces[0].clone();
311 let bp = Some(ws.expanded_base_path());
312 (Screen::Dashboard, Some(ws), bp)
313 }
314 _ => {
315 if let Some(ref default_path) = config.default_workspace {
317 let expanded = shellexpand::tilde(default_path.as_str());
318 let default_root = std::path::PathBuf::from(expanded.as_ref());
319 if let Some(ws) = workspaces.iter().find(|w| w.root_path == default_root) {
320 let bp = Some(ws.expanded_base_path());
321 (Screen::Dashboard, Some(ws.clone()), bp)
322 } else {
323 (Screen::Workspaces, None, None)
324 }
325 } else {
326 (Screen::Workspaces, None, None)
327 }
328 }
329 };
330
331 let sync_history = active_workspace
332 .as_ref()
333 .and_then(|ws| {
334 crate::cache::SyncHistoryManager::for_workspace(&ws.root_path)
335 .and_then(|m| m.load())
336 .ok()
337 })
338 .unwrap_or_default();
339
340 Self {
341 should_quit: false,
342 screen,
343 screen_stack: Vec::new(),
344 config,
345 workspaces,
346 active_workspace,
347 workspace_index: 0,
348 workspace_pane: WorkspacePane::Left,
349 base_path,
350 repos_by_org: HashMap::new(),
351 all_repos: Vec::new(),
352 orgs: Vec::new(),
353 local_repos: Vec::new(),
354 operation_state: OperationState::Idle,
355 log_lines: Vec::new(),
356 repo_index: 0,
357 scroll_offset: 0,
358 filter_text: String::new(),
359 filter_active: false,
360 dry_run: false,
361 error_message: None,
362 check_results: Vec::new(),
363 checks_loading: false,
364 sync_pull: false,
365 setup_state: if screen == Screen::WorkspaceSetup {
366 let default_path = std::env::current_dir()
367 .map(|p| state::tilde_collapse(&p.to_string_lossy()))
368 .unwrap_or_else(|_| "~/Git-Same/GitHub".to_string());
369 let mut setup = SetupState::with_first_setup(&default_path, config_was_created);
370 setup.config_was_created = config_was_created;
371 Some(setup)
372 } else {
373 None
374 },
375 status_loading: false,
376 last_status_scan: None,
377 stat_index: 0,
378 dashboard_table_state: TableState::default().with_selected(0),
379 settings_index: 0,
380 settings_config_expanded: false,
381 workspace_detail_scroll: 0,
382 tick_count: 0,
383 sync_log_entries: Vec::new(),
384 log_filter: LogFilter::All,
385 sync_history,
386 show_sync_history: false,
387 expanded_repo: None,
388 repo_commits: Vec::new(),
389 sync_log_index: 0,
390 changelog_commits: HashMap::new(),
391 changelog_total: 0,
392 changelog_loaded: 0,
393 changelog_scroll: 0,
394 }
395 }
396
397 pub fn select_workspace(&mut self, index: usize) {
399 if let Some(ws) = self.workspaces.get(index).cloned() {
400 self.base_path = Some(ws.expanded_base_path());
401 self.sync_history = crate::cache::SyncHistoryManager::for_workspace(&ws.root_path)
403 .and_then(|m| m.load())
404 .unwrap_or_default();
405 self.active_workspace = Some(ws);
406 self.repos_by_org.clear();
408 self.all_repos.clear();
409 self.orgs.clear();
410 self.local_repos.clear();
411 self.last_status_scan = None;
412 }
413 }
414
415 pub fn navigate_to(&mut self, screen: Screen) {
417 self.screen_stack.push(self.screen);
418 self.screen = screen;
419 self.repo_index = 0;
420 self.scroll_offset = 0;
421 }
422
423 pub fn go_back(&mut self) {
425 if let Some(prev) = self.screen_stack.pop() {
426 self.screen = prev;
427 }
428 }
429}
430
431#[cfg(test)]
432#[path = "app_tests.rs"]
433mod tests;