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
11pub struct QuickLauncher {
13 repo_cache: RepositoryCache,
14 git_cache: GitStatusCache,
15}
16
17#[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 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 repo_cache.initialize().await?;
47 git_cache.initialize().await?;
48
49 Ok(Self {
50 repo_cache,
51 git_cache,
52 })
53 }
54
55 pub async fn launch(&self, workspace_manager: &mut WorkspaceManager) -> Result<()> {
57 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 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 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 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 let available_apps = workspace_manager.get_available_apps().await;
98
99 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 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 let configured_apps: Vec<String> = repo.apps.keys().cloned().collect();
117 let has_configured_apps = !configured_apps.is_empty();
118
119 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 let mut sorted_items = launch_items;
143 sorted_items.sort_by(|a, b| a.name.cmp(&b.name));
144
145 let mut display_options = Vec::new();
147 let mut item_map = std::collections::HashMap::new();
148
149 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 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 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 self.launch_universal_repository(workspace_manager, selected_item)
187 .await?;
188
189 Ok(())
190 }
191
192 async fn launch_universal_repository(
194 &self,
195 workspace_manager: &mut WorkspaceManager,
196 item: &UniversalLaunchItem,
197 ) -> Result<()> {
198 workspace_manager.smart_open_repository(&item.name).await?;
200
201 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(), );
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 #[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 workspace_manager
234 .open_repo_with_app_options(repo_name, last_app, false)
235 .await?;
236
237 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 pub async fn refresh_cache(&self, workspace_manager: &WorkspaceManager) -> Result<()> {
264 println!("{} Updating repository cache...", style("š").blue());
265
266 self.repo_cache
268 .refresh_from_config(
269 &workspace_manager.config().repositories,
270 workspace_manager.get_workspace_root(),
271 )
272 .await?;
273
274 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(¤t_names)
284 .await?;
285
286 println!("{} Repository cache updated", style("ā").green());
287
288 Ok(())
289 }
290
291 #[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 #[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#[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 let stats = launcher.get_cache_stats().await.unwrap();
382 println!("{}", stats);
383 }
384}