Skip to main content

vibe_workspace/ui/
quick_launcher.rs

1use anyhow::Result;
2use console::style;
3use inquire::{InquireError, Select};
4use std::collections::HashMap;
5
6use crate::cache::{GitStatusCache, RepositoryCache};
7use crate::ui::formatting;
8use crate::ui::state::VibeState;
9use crate::workspace::{operations::GitStatus, WorkspaceManager};
10
11/// Enhanced repository launcher with caching
12pub struct QuickLauncher {
13    repo_cache: RepositoryCache,
14    git_cache: GitStatusCache,
15}
16
17/// Enhanced launch item that supports both configured and unconfigured repositories
18#[derive(Debug, Clone)]
19pub struct UniversalLaunchItem {
20    pub name: String,
21    pub display_string: String,
22    #[allow(dead_code)]
23    pub has_configured_apps: bool,
24    #[allow(dead_code)]
25    pub configured_apps: Vec<String>,
26    #[allow(dead_code)]
27    pub available_apps: Vec<String>,
28    #[allow(dead_code)]
29    pub git_status: Option<GitStatus>,
30    #[allow(dead_code)]
31    pub is_recent: bool,
32    #[allow(dead_code)]
33    pub recent_rank: Option<usize>,
34    #[allow(dead_code)]
35    pub last_accessed: Option<String>,
36    pub last_app: Option<String>,
37}
38
39impl QuickLauncher {
40    /// Create a new quick launcher with cache system
41    pub async fn new(cache_dir: &std::path::Path) -> Result<Self> {
42        let repo_cache = RepositoryCache::new(cache_dir.join("repositories.db"));
43        let git_cache = GitStatusCache::new(cache_dir.join("git_status.db"));
44
45        // Initialize caches
46        repo_cache.initialize().await?;
47        git_cache.initialize().await?;
48
49        Ok(Self {
50            repo_cache,
51            git_cache,
52        })
53    }
54
55    /// Universal launch - shows ALL repositories (configured and unconfigured)
56    pub async fn launch(&self, workspace_manager: &mut WorkspaceManager) -> Result<()> {
57        // Get ALL repositories from workspace manager
58        let all_repos = workspace_manager.list_repositories();
59
60        if all_repos.is_empty() {
61            println!("āŒ No repositories found in workspace");
62            println!("šŸ’” Scan for repositories: 'vibe git scan'");
63            println!("šŸ’” Clone a repository: 'vibe clone <url>'");
64            return Ok(());
65        }
66
67        // Get recent repositories for prioritization
68        let user_state = VibeState::load().unwrap_or_default();
69        let recent_repos = user_state.get_recent_repos(15);
70        let recent_names: HashMap<String, usize> = recent_repos
71            .iter()
72            .enumerate()
73            .map(|(i, repo)| (repo.repo_id.clone(), i + 1))
74            .collect();
75
76        // Create a map for recent repo details (time, last app)
77        let recent_details: HashMap<String, (&crate::ui::state::RecentRepo, String)> = recent_repos
78            .iter()
79            .map(|repo| {
80                let time_ago = formatting::format_time_ago(&repo.last_accessed);
81                (repo.repo_id.clone(), (repo, time_ago))
82            })
83            .collect();
84
85        // Load git status from cache (optional - don't block if missing)
86        let git_statuses = self
87            .git_cache
88            .get_all_git_statuses()
89            .await
90            .unwrap_or_default();
91        let git_status_map: HashMap<String, GitStatus> = git_statuses
92            .into_iter()
93            .map(|cached| (cached.repository_name.clone(), cached.into()))
94            .collect();
95
96        // Get available apps on system for unconfigured repos
97        let available_apps = workspace_manager.get_available_apps().await;
98
99        // Create universal launch items with ALL repositories
100        let launch_items: Vec<UniversalLaunchItem> = all_repos
101            .iter()
102            .map(|repo| {
103                let git_status = git_status_map.get(&repo.name).cloned();
104                let recent_rank = recent_names.get(&repo.name).cloned();
105                let is_recent = recent_rank.is_some();
106
107                // Get recent repo details if available
108                let (last_accessed, last_app) =
109                    if let Some((recent_repo, time_ago)) = recent_details.get(&repo.name) {
110                        (Some(time_ago.clone()), recent_repo.last_app.clone())
111                    } else {
112                        (None, None)
113                    };
114
115                // Check if repository has configured apps
116                let configured_apps: Vec<String> = repo.apps.keys().cloned().collect();
117                let has_configured_apps = !configured_apps.is_empty();
118
119                // Create clean display with consistent folder icons
120                let display_string = if has_configured_apps {
121                    format!("šŸ“ {} šŸ“‹[{}]", repo.name, configured_apps.len())
122                } else {
123                    format!("šŸ“ {}", repo.name)
124                };
125
126                UniversalLaunchItem {
127                    name: repo.name.clone(),
128                    display_string,
129                    has_configured_apps,
130                    configured_apps,
131                    available_apps: available_apps.clone(),
132                    git_status,
133                    is_recent,
134                    recent_rank,
135                    last_accessed,
136                    last_app,
137                }
138            })
139            .collect();
140
141        // Sort all repositories alphabetically for clean browsing
142        let mut sorted_items = launch_items;
143        sorted_items.sort_by(|a, b| a.name.cmp(&b.name));
144
145        // Create display options for all repositories
146        let mut display_options = Vec::new();
147        let mut item_map = std::collections::HashMap::new();
148
149        // Add all repositories in alphabetical order
150        for item in &sorted_items {
151            display_options.push(item.display_string.clone());
152            item_map.insert(item.display_string.clone(), item);
153        }
154
155        // Show selection UI with updated messaging
156        println!("\nšŸ“‚ Select a repository to open:");
157        println!(
158            "   {} repositories available • {} apps auto-detected for unconfigured repos",
159            sorted_items.len(),
160            available_apps.len()
161        );
162
163        // Repository selection
164        let selected_display_result = Select::new("Repository:", display_options.clone())
165            .with_help_message("Use arrow keys to navigate, type to filter • ESC to exit")
166            .with_page_size(workspace_manager.get_quick_launch_page_size())
167            .prompt();
168
169        let selected_display = match selected_display_result {
170            Ok(value) => value,
171            Err(InquireError::OperationCanceled) => {
172                println!("{} Repository selection cancelled", style("ā„¹ļø").blue());
173                return Ok(());
174            }
175            Err(error) => return Err(anyhow::Error::from(error)),
176        };
177
178        let selected_item = item_map.get(&selected_display).copied().ok_or_else(|| {
179            anyhow::anyhow!(
180                "Selected repository '{}' not found in item map",
181                selected_display
182            )
183        })?;
184
185        // Handle app selection and launch
186        self.launch_universal_repository(workspace_manager, selected_item)
187            .await?;
188
189        Ok(())
190    }
191
192    /// Launch a universal repository (configured or unconfigured) with smart app selection
193    async fn launch_universal_repository(
194        &self,
195        workspace_manager: &mut WorkspaceManager,
196        item: &UniversalLaunchItem,
197    ) -> Result<()> {
198        // Use smart_open_repository for manual selection - this shows choice menu
199        workspace_manager.smart_open_repository(&item.name).await?;
200
201        // Update recent repositories state with the last app chosen
202        if let Some(repo_info) = workspace_manager.get_repository(&item.name) {
203            let mut user_state = VibeState::load().unwrap_or_default();
204            user_state.add_recent_repo(
205                item.name.clone(),
206                repo_info.path.clone(),
207                item.last_app.clone(), // Use the app from selection or previous choice
208            );
209            if let Err(e) = user_state.save() {
210                eprintln!("Warning: Failed to save recent repositories: {e}");
211            }
212        }
213
214        Ok(())
215    }
216
217    /// Quick launch from recent repos (position 1-9) - uses immediate selection
218    #[allow(dead_code)]
219    pub async fn quick_launch_recent(
220        &self,
221        workspace_manager: &mut WorkspaceManager,
222        position: usize,
223    ) -> Result<()> {
224        let user_state = VibeState::load().unwrap_or_default();
225        let recent_repos = user_state.get_recent_repos(9);
226
227        if let Some(recent_repo) = recent_repos.get(position - 1) {
228            let repo_name = &recent_repo.repo_id;
229            let default_app = "vscode".to_string();
230            let last_app = recent_repo.last_app.as_ref().unwrap_or(&default_app);
231
232            // Immediate opening with saved app - NO choice menu
233            workspace_manager
234                .open_repo_with_app_options(repo_name, last_app, false)
235                .await?;
236
237            // Update access tracking
238            let mut updated_state = VibeState::load().unwrap_or_default();
239            updated_state.add_recent_repo(
240                repo_name.clone(),
241                recent_repo.path.clone(),
242                Some(last_app.clone()),
243            );
244            if let Err(e) = updated_state.save() {
245                eprintln!("Warning: Failed to save recent repositories: {e}");
246            }
247
248            println!(
249                "{} Opened {} with {} (quick launch #{})",
250                style("šŸš€").green(),
251                style(repo_name).cyan().bold(),
252                style(last_app).blue(),
253                position
254            );
255        } else {
256            anyhow::bail!("No repository found at position {}", position);
257        }
258
259        Ok(())
260    }
261
262    /// Refresh cache from workspace configuration
263    pub async fn refresh_cache(&self, workspace_manager: &WorkspaceManager) -> Result<()> {
264        println!("{} Updating repository cache...", style("šŸ”„").blue());
265
266        // Update repository cache
267        self.repo_cache
268            .refresh_from_config(
269                &workspace_manager.config().repositories,
270                workspace_manager.get_workspace_root(),
271            )
272            .await?;
273
274        // Clean up stale entries
275        let current_names: Vec<String> = workspace_manager
276            .config()
277            .repositories
278            .iter()
279            .map(|r| r.name.clone())
280            .collect();
281
282        self.repo_cache
283            .cleanup_stale_entries(&current_names)
284            .await?;
285
286        println!("{} Repository cache updated", style("āœ“").green());
287
288        Ok(())
289    }
290
291    /// Update git status cache in background for specific repositories
292    #[allow(dead_code)]
293    pub async fn update_git_status_cache(
294        &self,
295        workspace_manager: &WorkspaceManager,
296        repo_names: &[String],
297    ) -> Result<()> {
298        for repo_name in repo_names {
299            if let Some(repo_config) = workspace_manager
300                .config()
301                .repositories
302                .iter()
303                .find(|r| r.name == *repo_name)
304            {
305                let repo_path = workspace_manager
306                    .config()
307                    .workspace
308                    .root
309                    .join(&repo_config.path);
310
311                match crate::workspace::operations::get_git_status(&repo_path).await {
312                    Ok(git_status) => {
313                        let cached_status = git_status.into();
314                        if let Err(e) = self.git_cache.cache_git_status(&cached_status).await {
315                            eprintln!("Warning: Failed to cache git status for {repo_name}: {e}");
316                        }
317                    }
318                    Err(e) => {
319                        eprintln!("Warning: Failed to get git status for {repo_name}: {e}");
320                    }
321                }
322            }
323        }
324
325        Ok(())
326    }
327
328    /// Get cache statistics for monitoring
329    #[allow(dead_code)]
330    pub async fn get_cache_stats(&self) -> Result<CacheStatistics> {
331        let repo_stats = self.repo_cache.get_stats().await?;
332        let git_stats = self.git_cache.get_stats().await?;
333
334        Ok(CacheStatistics {
335            repositories: repo_stats,
336            git_status: git_stats,
337        })
338    }
339}
340
341/// Combined cache statistics
342#[derive(Debug)]
343pub struct CacheStatistics {
344    pub repositories: crate::cache::repository_cache::CacheStats,
345    pub git_status: crate::cache::git_status_cache::GitCacheStats,
346}
347
348impl std::fmt::Display for CacheStatistics {
349    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
350        writeln!(f, "šŸ“Š Cache Statistics:")?;
351        writeln!(
352            f,
353            "  Repositories: {} total, {} with apps, {} existing",
354            self.repositories.total_repositories,
355            self.repositories.repositories_with_apps,
356            self.repositories.existing_repositories
357        )?;
358        writeln!(
359            f,
360            "  Git Status: {} total, {} valid, {} expired (TTL: {}min)",
361            self.git_status.total_entries,
362            self.git_status.valid_entries,
363            self.git_status.expired_entries,
364            self.git_status.ttl_minutes
365        )?;
366        Ok(())
367    }
368}
369
370#[cfg(test)]
371mod tests {
372    use super::*;
373    use tempfile::tempdir;
374
375    #[tokio::test]
376    async fn test_quick_launcher_creation() {
377        let temp_dir = tempdir().unwrap();
378        let launcher = QuickLauncher::new(temp_dir.path()).await.unwrap();
379
380        // Test that caches are initialized
381        let stats = launcher.get_cache_stats().await.unwrap();
382        println!("{}", stats);
383    }
384}