diff --git a/.dev/IMPROVEMENT_PLAN.md b/.dev/IMPROVEMENT_PLAN.md deleted file mode 100644 index 6991f1c..0000000 --- a/.dev/IMPROVEMENT_PLAN.md +++ /dev/null @@ -1,449 +0,0 @@ -# Brewsty - Architecture Improvement Plan - -## Overview -This document outlines comprehensive improvements to enhance DI, components, DRY, SOLID principles, and code hierarchy. - -## Current Issues - -### 1. Dependency Injection (DI) Issues -**Problems:** -- ❌ 10 use case parameters in `BrewstyApp::new()` - excessive constructor injection -- ❌ Runtime created in struct instead of injected -- ❌ Each use case creates threads/runtimes independently - -**Impact:** Hard to test, maintain, and extend - -### 2. DRY Violations -**Major Code Duplication:** -- `load_installed_packages()` vs `load_outdated_packages()` - **90% identical** -- `handle_install()`, `handle_uninstall()`, `handle_update()` - **similar pattern** -- Status message + logging pattern repeated ~20 times - -**Impact:** Bug fixes need to be applied in multiple places, increased maintenance burden - -### 3. SOLID Violations - -#### Single Responsibility Principle (SRP): -- ❌ `BrewstyApp` has **8+ responsibilities**: - - UI rendering - - Async task management - - State management - - Package operations - - Logging - - Filtering - - Cleanup operations - - Search functionality -- ✅ **Should be:** `BrewstyApp` only coordinates, delegates to specialized components - -#### Open/Closed Principle: -- ❌ Adding new tabs requires modifying `BrewstyApp` -- ✅ **Should use:** Tab trait/registry pattern - -#### Dependency Inversion: -- ❌ `AsyncTask` enum tightly coupled to specific data structures -- ✅ **Should use:** Generic task abstraction - -### 4. Component Architecture Issues - -**Missing Components:** -``` -src/presentation/ - components/ - ✅ package_list.rs - ❌ async_task_manager.rs (NEW) - ❌ package_operation_handler.rs (NEW) - ❌ tab_manager.rs (NEW) - ❌ filter_state.rs (NEW) - ❌ cleanup_modal.rs (NEW) - ❌ status_bar.rs (NEW) - ❌ output_log.rs (NEW) - services/ - ❌ async_executor.rs (NEW) - ❌ event_bus.rs (NEW) -``` - -### 5. Hierarchy Issues - -**Flattened Use Case Structure:** -``` -use_cases/ - ❌ All 8+ use cases in one file (package_operations.rs) - -✅ Should be organized by domain: - use_cases/ - queries/ - list_installed.rs - list_outdated.rs - search_packages.rs - get_package_info.rs - commands/ - install_package.rs - uninstall_package.rs - update_package.rs - update_all_packages.rs - maintenance/ - clean_cache.rs - cleanup_old_versions.rs -``` - -## Implementation Plan - -### Phase 1: HIGH PRIORITY (Biggest Impact) - -#### 1. Create UseCaseContainer -**File:** `src/application/use_case_container.rs` - -Consolidates all use cases into a single injectable container: -```rust -pub struct UseCaseContainer { - pub list_installed: Arc, - pub list_outdated: Arc, - pub install: Arc, - pub uninstall: Arc, - pub update: Arc, - pub update_all: Arc, - pub clean_cache: Arc, - pub cleanup_old_versions: Arc, - pub search: Arc, - pub get_package_info: Arc, -} -``` - -**Benefits:** -- Reduces `BrewstyApp::new()` from 10 parameters to 1 -- Easier to add new use cases -- Better encapsulation - -#### 2. Create AsyncExecutor Service -**File:** `src/presentation/services/async_executor.rs` - -Manages runtime lifecycle and async task execution: -```rust -pub struct AsyncExecutor { - runtime: tokio::runtime::Runtime, -} - -impl AsyncExecutor { - pub fn execute(&self, future: F) -> T - where F: Future; - - pub fn spawn(&self, future: F) -> JoinHandle; -} -``` - -**Benefits:** -- Single runtime instance -- Centralized async execution -- Better resource management -- Testable async code - -#### 3. Create AsyncTaskManager Component -**File:** `src/presentation/components/async_task_manager.rs` - -Separates async task polling and management from UI: -```rust -pub struct AsyncTaskManager { - active_task: Option, - package_info_tasks: Vec<(String, AsyncTask)>, - packages_loading_info: HashSet, - pending_loads: Vec<(String, PackageType)>, -} - -impl AsyncTaskManager { - pub fn poll_tasks(&mut self, /* callbacks */); - pub fn start_package_load(&mut self, name: String, type: PackageType); - pub fn is_loading(&self, package_name: &str) -> bool; -} -``` - -**Benefits:** -- Separation of concerns -- Reusable async task logic -- Easier testing -- Reduces `app.rs` complexity - -#### 4. Create PackageOperationHandler -**File:** `src/presentation/services/package_operation_handler.rs` - -Eliminates DRY violations in package operations: -```rust -pub struct PackageOperationHandler { - use_cases: Arc, - executor: Arc, -} - -impl PackageOperationHandler { - pub async fn handle_operation( - &self, - operation: PackageOperation, - package: Package, - ) -> Result; -} - -pub enum PackageOperation { - Install, - Uninstall, - Update, -} - -pub struct OperationResult { - pub success: bool, - pub message: String, - pub should_reload_installed: bool, - pub should_reload_outdated: bool, -} -``` - -**Benefits:** -- DRY - Single place for operation logic -- Consistent error handling -- Easier to add new operations -- Unified logging pattern - -#### 5. Extract StatusNotifier/EventBus -**File:** `src/presentation/services/event_bus.rs` - -Decouples status updates and logging: -```rust -pub struct EventBus { - status_message: Arc>, - output_log: Arc>>, -} - -impl EventBus { - pub fn notify_status(&self, message: String); - pub fn log(&self, message: String); - pub fn get_status(&self) -> String; - pub fn get_recent_logs(&self, count: usize) -> Vec; -} -``` - -**Benefits:** -- Decoupled logging -- Thread-safe status updates -- Observable pattern -- Better for testing - -### Phase 2: MEDIUM PRIORITY (Better Architecture) - -#### 6. Create TabManager Component -**File:** `src/presentation/components/tab_manager.rs` - -Manages tab state and navigation: -```rust -pub struct TabManager { - current_tab: Tab, - tab_states: HashMap, -} - -impl TabManager { - pub fn switch_to(&mut self, tab: Tab); - pub fn current(&self) -> - pub fn is_loaded(&self, tab: Tab) -> bool; - pub fn mark_loaded(&mut self, tab: Tab); -} -``` - -**Benefits:** -- Centralized tab logic -- Easier to add new tabs -- Cleaner state management - -#### 7. Create FilterState Component -**File:** `src/presentation/components/filter_state.rs` - -Manages filtering state: -```rust -pub struct FilterState { - show_formulae: bool, - show_casks: bool, - search_query: String, - installed_search_query: String, -} - -impl FilterState { - pub fn should_show_package(&self, package: &Package, context: FilterContext) -> bool; - pub fn reset(&mut self); -} -``` - -**Benefits:** -- Reusable filter logic -- Centralized filter state -- Easier to add new filters - -#### 8. Reorganize Use Case Hierarchy -**Structure:** -``` -src/application/use_cases/ - queries/ - list_installed.rs - list_outdated.rs - search_packages.rs - get_package_info.rs - commands/ - install_package.rs - uninstall_package.rs - update_package.rs - update_all_packages.rs - maintenance/ - clean_cache.rs - cleanup_old_versions.rs - mod.rs (re-exports) -``` - -**Benefits:** -- Clear CQRS pattern -- Better organization -- Easier to navigate -- Scalable structure - -#### 9. Add Proper Error Types -**File:** `src/domain/errors.rs` - -Custom error types for better error handling: -```rust -#[derive(Debug, thiserror::Error)] -pub enum BrewstyError { - #[error("Package not found: {0}")] - PackageNotFound(String), - - #[error("Installation failed: {0}")] - InstallationFailed(String), - - #[error("Brew command failed: {0}")] - BrewCommandFailed(String), - - #[error("Timeout loading package info: {0}")] - PackageInfoTimeout(String), -} -``` - -**Benefits:** -- Type-safe error handling -- Better error messages -- Easier debugging -- Pattern matching on errors - -### Phase 3: LOW PRIORITY (Nice to Have) - -#### 10. Extract CleanupModalComponent -**File:** `src/presentation/components/cleanup_modal.rs` - -Separate cleanup modal UI logic: -```rust -pub struct CleanupModal { - show: bool, - cleanup_type: Option, - preview: Option, -} - -impl CleanupModal { - pub fn render(&mut self, ctx: &egui::Context) -> Option; -} -``` - -#### 11. Create LogManager Component -**File:** `src/presentation/components/log_manager.rs` - -Manages output log with features: -```rust -pub struct LogManager { - logs: VecDeque, - max_size: usize, - filters: Vec, -} - -impl LogManager { - pub fn push(&mut self, message: String, level: LogLevel); - pub fn get_recent(&self, count: usize) -> Vec<&LogEntry>; - pub fn filter(&self, filter: LogFilter) -> Vec<&LogEntry>; -} -``` - -#### 12. Create StatusBar Component -**File:** `src/presentation/components/status_bar.rs` - -Dedicated status bar component: -```rust -pub struct StatusBar { - message: String, - loading: bool, -} - -impl StatusBar { - pub fn render(&self, ui: &mut egui::Ui); -} -``` - -## Expected Outcomes - -### Code Metrics (Estimated) -- **app.rs**: 1115 lines → ~400 lines (64% reduction) -- **Cyclomatic Complexity**: Reduced by ~60% -- **Test Coverage**: Easier to achieve >80% -- **Parameters in constructors**: 10 → 3-4 - -### Architecture Improvements -- ✅ Clear separation of concerns -- ✅ Testable components -- ✅ Reusable services -- ✅ SOLID compliance -- ✅ DRY code -- ✅ Better DI - -### Maintainability -- ✅ New features easier to add -- ✅ Bugs easier to locate and fix -- ✅ Code easier to understand -- ✅ Better onboarding for new developers - -## Implementation Order - -1. **Day 1: Foundation (HIGH)** - - UseCaseContainer (#1) - - AsyncExecutor (#2) - - AsyncTaskManager (#3) - -2. **Day 2: Core Logic (HIGH)** - - PackageOperationHandler (#5) - - EventBus (#4) - -3. **Day 3: Component Extraction (MEDIUM)** - - TabManager (#6) - - FilterState (#7) - - Use Case Reorganization (#8) - -4. **Day 4: Polish (MEDIUM/LOW)** - - Error Types (#9) - - CleanupModal (#10) - - LogManager (#11) - - StatusBar (#12) - -## Testing Strategy - -Each new component should have: -- ✅ Unit tests -- ✅ Integration tests (where applicable) -- ✅ Mock implementations for repositories -- ✅ Property-based tests for complex logic - -## Migration Path - -To minimize risk: -1. Create new components alongside existing code -2. Gradually migrate functionality -3. Run tests after each migration -4. Remove old code only when fully migrated -5. Update documentation - -## Success Criteria - -- [ ] All tests passing -- [ ] No clippy warnings -- [ ] app.rs < 500 lines -- [ ] All components have unit tests -- [ ] No code duplication for common operations -- [ ] Constructor injection ≤ 4 parameters -- [ ] Clear component boundaries -- [ ] Documentation updated diff --git a/.dev/PROJECT_OVERVIEW.md b/.dev/PROJECT_OVERVIEW.md deleted file mode 100644 index 077ef86..0000000 --- a/.dev/PROJECT_OVERVIEW.md +++ /dev/null @@ -1,113 +0,0 @@ -# Brewsty - Project Overview - -## ✅ What's Been Created - -A complete Rust-based GUI application for managing Homebrew packages on macOS with clean architecture and SOLID principles. - -### Architecture Layers - -1. **Domain Layer** (`src/domain/`) - - `entities/`: Core business models (Package, PackageType, CacheInfo) - - `repositories/`: Repository trait defining package operations interface - - `services/`: Domain services for validation - -2. **Infrastructure Layer** (`src/infrastructure/`) - - `brew/command.rs`: Low-level Homebrew CLI commands wrapper - - `brew/repository.rs`: Implementation of PackageRepository using Homebrew - -3. **Application Layer** (`src/application/`) - - `use_cases/`: All business logic operations (Install, Uninstall, Update, Search, Clean, etc.) - - `dto/`: Data transfer objects - -4. **Presentation Layer** (`src/presentation/`) - - `ui/app.rs`: Main application with egui GUI - - `components/package_list.rs`: Reusable package list component - -### Key Features Implemented - -✅ View installed formulae and casks -✅ Check for outdated packages -✅ Install/uninstall packages -✅ Update individual packages or all at once -✅ Search for packages -✅ Clean cache -✅ Remove old versions -✅ Modern tabbed GUI interface with egui -✅ Async operations with Tokio -✅ Dependency injection pattern -✅ Repository pattern for testability -✅ Use case pattern for business logic - -### SOLID Principles Applied - -- **Single Responsibility**: Each module has one clear purpose -- **Open/Closed**: Repository interface allows extending without modifying -- **Liskov Substitution**: PackageRepository can be swapped with different implementations -- **Interface Segregation**: Clean, focused trait definitions -- **Dependency Inversion**: Use cases depend on repository abstraction, not concrete implementation - -## 🔧 Build Issue - -The project structure is complete but build failed due to macOS system linker issues. This is NOT a code problem - it's a system configuration issue. - -### To Fix: - -```bash -# Install/reinstall Xcode Command Line Tools -xcode-select --install - -# If that doesn't work, try: -sudo rm -rf /Library/Developer/CommandLineTools -xcode-select --install - -# Then rebuild: -cargo build --release -``` - -## 🚀 Running the Application - -Once the build issue is resolved: - -```bash -cargo run --release -``` - -The GUI will launch with tabs for: -- **Installed**: View all installed formulae/casks -- **Outdated**: See packages that need updates -- **Search**: Find new packages to install -- **Maintenance**: Clean cache and old versions - -## 📁 Project Structure - -``` -brewsty/ -├── Cargo.toml # Dependencies and project config -├── README.md # Project documentation -├── src/ -│ ├── main.rs # Entry point with DI setup -│ ├── domain/ # Core business logic (framework-agnostic) -│ │ ├── entities/ -│ │ ├── repositories/ -│ │ └── services/ -│ ├── infrastructure/ # External systems (Homebrew) -│ │ └── brew/ -│ ├── application/ # Use cases -│ │ ├── use_cases/ -│ │ └── dto/ -│ └── presentation/ # GUI layer -│ ├── ui/ -│ └── components/ -``` - -## 🎯 Next Steps - -1. Fix the macOS linker issue (see above) -2. Build and run the application -3. Optional enhancements: - - Add progress bars for long operations - - Implement background refresh - - Add package details view - - Export package lists - - Add filtering/sorting options - - Persist user preferences diff --git a/.gitignore b/.gitignore index 2673a7f..9be2c49 100644 --- a/.gitignore +++ b/.gitignore @@ -22,3 +22,4 @@ Cargo.lock # Local development CLAUDE.md +.dev/ diff --git a/Cargo.toml b/Cargo.toml index d2bff84..47ebdca 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "brewsty" -version = "0.3.0-beta" +version = "0.4.0-beta" edition = "2024" [dependencies] @@ -20,3 +20,6 @@ tracing-subscriber = "0.3" opt-level = 3 lto = true codegen-units = 1 + +[profile.release.package."*"] +opt-level = 3 diff --git a/src/application/mod.rs b/src/application/mod.rs index 0259500..f9ab86d 100644 --- a/src/application/mod.rs +++ b/src/application/mod.rs @@ -1,5 +1,5 @@ pub mod dto; -pub mod use_cases; pub mod use_case_container; +pub mod use_cases; pub use use_case_container::UseCaseContainer; diff --git a/src/application/use_cases/package_operations.rs b/src/application/use_cases/package_operations.rs index 3b3bc81..75bd0fc 100644 --- a/src/application/use_cases/package_operations.rs +++ b/src/application/use_cases/package_operations.rs @@ -5,178 +5,231 @@ use crate::domain::{ use anyhow::Result; use std::sync::Arc; -pub struct ListInstalledPackages { +pub struct RepositoryUseCase { repository: Arc, } -impl ListInstalledPackages { +impl RepositoryUseCase { pub fn new(repository: Arc) -> Self { Self { repository } } + pub fn repository(&self) -> Arc { + Arc::clone(&self.repository) + } +} + +pub struct ListInstalledPackages { + use_case: RepositoryUseCase, +} + +impl ListInstalledPackages { + pub fn new(repository: Arc) -> Self { + Self { + use_case: RepositoryUseCase::new(repository), + } + } + pub async fn execute(&self, package_type: PackageType) -> Result> { - self.repository.get_installed_packages(package_type).await + self.use_case + .repository() + .get_installed_packages(package_type) + .await } } pub struct ListOutdatedPackages { - repository: Arc, + use_case: RepositoryUseCase, } impl ListOutdatedPackages { pub fn new(repository: Arc) -> Self { - Self { repository } + Self { + use_case: RepositoryUseCase::new(repository), + } } pub async fn execute(&self, package_type: PackageType) -> Result> { - self.repository.get_outdated_packages(package_type).await + self.use_case + .repository() + .get_outdated_packages(package_type) + .await } } pub struct InstallPackage { - repository: Arc, + use_case: RepositoryUseCase, } impl InstallPackage { pub fn new(repository: Arc) -> Self { - Self { repository } + Self { + use_case: RepositoryUseCase::new(repository), + } } pub async fn execute(&self, package: Package) -> Result<()> { - self.repository.install_package(&package).await + self.use_case.repository().install_package(&package).await } } pub struct UninstallPackage { - repository: Arc, + use_case: RepositoryUseCase, } impl UninstallPackage { pub fn new(repository: Arc) -> Self { - Self { repository } + Self { + use_case: RepositoryUseCase::new(repository), + } } pub async fn execute(&self, package: Package) -> Result<()> { - self.repository.uninstall_package(&package).await + self.use_case.repository().uninstall_package(&package).await } } pub struct UpdatePackage { - repository: Arc, + use_case: RepositoryUseCase, } impl UpdatePackage { pub fn new(repository: Arc) -> Self { - Self { repository } + Self { + use_case: RepositoryUseCase::new(repository), + } } pub async fn execute(&self, package: Package) -> Result<()> { - self.repository.update_package(&package).await + self.use_case.repository().update_package(&package).await } } pub struct UpdateAllPackages { - repository: Arc, + use_case: RepositoryUseCase, } impl UpdateAllPackages { pub fn new(repository: Arc) -> Self { - Self { repository } + Self { + use_case: RepositoryUseCase::new(repository), + } } pub async fn execute(&self) -> Result<()> { - self.repository.update_all().await + self.use_case.repository().update_all().await } } pub struct CleanCache { - repository: Arc, + use_case: RepositoryUseCase, } impl CleanCache { pub fn new(repository: Arc) -> Self { - Self { repository } + Self { + use_case: RepositoryUseCase::new(repository), + } } pub async fn preview(&self) -> Result { - self.repository.get_cleanup_preview().await + self.use_case.repository().get_cleanup_preview().await } pub async fn execute(&self) -> Result<()> { - self.repository.clean_cache().await + self.use_case.repository().clean_cache().await } } pub struct CleanupOldVersions { - repository: Arc, + use_case: RepositoryUseCase, } impl CleanupOldVersions { pub fn new(repository: Arc) -> Self { - Self { repository } + Self { + use_case: RepositoryUseCase::new(repository), + } } pub async fn preview(&self) -> Result { - self.repository.get_cleanup_old_versions_preview().await + self.use_case + .repository() + .get_cleanup_old_versions_preview() + .await } pub async fn execute(&self) -> Result<()> { - self.repository.cleanup_old_versions().await + self.use_case.repository().cleanup_old_versions().await } } pub struct SearchPackages { - repository: Arc, + use_case: RepositoryUseCase, } impl SearchPackages { pub fn new(repository: Arc) -> Self { - Self { repository } + Self { + use_case: RepositoryUseCase::new(repository), + } } pub async fn execute(&self, query: &str, package_type: PackageType) -> Result> { - self.repository.search_packages(query, package_type).await + self.use_case + .repository() + .search_packages(query, package_type) + .await } } pub struct GetPackageInfo { - repository: Arc, + use_case: RepositoryUseCase, } impl GetPackageInfo { pub fn new(repository: Arc) -> Self { - Self { repository } + Self { + use_case: RepositoryUseCase::new(repository), + } } pub async fn execute(&self, name: &str, package_type: PackageType) -> Result { - self.repository.get_package_info(name, package_type).await + self.use_case + .repository() + .get_package_info(name, package_type) + .await } } pub struct PinPackage { - repository: Arc, + use_case: RepositoryUseCase, } impl PinPackage { pub fn new(repository: Arc) -> Self { - Self { repository } + Self { + use_case: RepositoryUseCase::new(repository), + } } pub async fn execute(&self, package: Package) -> Result<()> { - self.repository.pin_package(&package).await + self.use_case.repository().pin_package(&package).await } } pub struct UnpinPackage { - repository: Arc, + use_case: RepositoryUseCase, } impl UnpinPackage { pub fn new(repository: Arc) -> Self { - Self { repository } + Self { + use_case: RepositoryUseCase::new(repository), + } } pub async fn execute(&self, package: Package) -> Result<()> { - self.repository.unpin_package(&package).await + self.use_case.repository().unpin_package(&package).await } } diff --git a/src/domain/entities/mod.rs b/src/domain/entities/mod.rs index f976a94..9946758 100644 --- a/src/domain/entities/mod.rs +++ b/src/domain/entities/mod.rs @@ -1,3 +1,3 @@ pub mod package; -pub use package::{Package, PackageType, CleanupItem, CleanupPreview}; +pub use package::{CleanupItem, CleanupPreview, Package, PackageType}; diff --git a/src/domain/entities/package.rs b/src/domain/entities/package.rs index 88f0822..9903413 100644 --- a/src/domain/entities/package.rs +++ b/src/domain/entities/package.rs @@ -29,10 +29,7 @@ pub struct Package { } impl Package { - pub fn new( - name: String, - package_type: PackageType, - ) -> Self { + pub fn new(name: String, package_type: PackageType) -> Self { Self { name, version: None, diff --git a/src/domain/repositories/package_repository.rs b/src/domain/repositories/package_repository.rs index d75ec46..1032200 100644 --- a/src/domain/repositories/package_repository.rs +++ b/src/domain/repositories/package_repository.rs @@ -14,7 +14,8 @@ pub trait PackageRepository: Send + Sync { async fn get_cleanup_old_versions_preview(&self) -> Result; async fn clean_cache(&self) -> Result<()>; async fn cleanup_old_versions(&self) -> Result<()>; - async fn search_packages(&self, query: &str, package_type: PackageType) -> Result>; + async fn search_packages(&self, query: &str, package_type: PackageType) + -> Result>; async fn get_package_info(&self, name: &str, package_type: PackageType) -> Result; async fn pin_package(&self, package: &Package) -> Result<()>; async fn unpin_package(&self, package: &Package) -> Result<()>; diff --git a/src/infrastructure/brew/command.rs b/src/infrastructure/brew/command.rs index 549accc..780a661 100644 --- a/src/infrastructure/brew/command.rs +++ b/src/infrastructure/brew/command.rs @@ -1,4 +1,5 @@ -use anyhow::{anyhow, Result}; +use crate::domain::entities::PackageType; +use anyhow::{Result, anyhow}; use std::process::Command; pub struct BrewOutput { @@ -9,154 +10,91 @@ pub struct BrewOutput { pub struct BrewCommand; impl BrewCommand { - pub fn list_formulae() -> Result { - let output = Command::new("brew") - .args(["info", "--json=v2", "--installed", "--formula"]) - .output()?; - - if !output.status.success() { - return Err(anyhow!("Failed to list formulae: {}", String::from_utf8_lossy(&output.stderr))); + fn get_package_type_arg(package_type: PackageType) -> &'static str { + match package_type { + PackageType::Formula => "--formula", + PackageType::Cask => "--cask", } - - Ok(String::from_utf8(output.stdout)?) } - pub fn list_casks() -> Result { - let output = Command::new("brew") - .args(["info", "--json=v2", "--installed", "--cask"]) - .output()?; + fn execute_brew(args: &[&str]) -> Result { + let output = Command::new("brew").args(args).output()?; if !output.status.success() { - return Err(anyhow!("Failed to list casks: {}", String::from_utf8_lossy(&output.stderr))); + return Err(anyhow!( + "Brew command failed: {}", + String::from_utf8_lossy(&output.stderr) + )); } Ok(String::from_utf8(output.stdout)?) } - pub fn get_formula_info(name: &str) -> Result { - tracing::debug!("Running: brew info --json=v2 --formula {}", name); - - let output = Command::new("brew") - .args(["info", "--json=v2", "--formula", name]) - .output()?; - - if !output.status.success() { - let error_msg = String::from_utf8_lossy(&output.stderr); - tracing::error!("brew info --formula {} failed: {}", name, error_msg); - return Err(anyhow!("Failed to get formula info: {}", error_msg)); - } - - let result = String::from_utf8(output.stdout)?; - tracing::debug!("brew info --formula {} returned {} bytes", name, result.len()); - Ok(result) - } - - pub fn get_cask_info(name: &str) -> Result { - tracing::debug!("Running: brew info --json=v2 --cask {}", name); - - let output = Command::new("brew") - .args(["info", "--json=v2", "--cask", name]) - .output()?; - - if !output.status.success() { - let error_msg = String::from_utf8_lossy(&output.stderr); - tracing::error!("brew info --cask {} failed: {}", name, error_msg); - return Err(anyhow!("Failed to get cask info: {}", error_msg)); - } - - let result = String::from_utf8(output.stdout)?; - tracing::debug!("brew info --cask {} returned {} bytes", name, result.len()); - Ok(result) - } - - pub fn outdated_formulae() -> Result { - let output = Command::new("brew") - .args(["outdated", "--formula", "--json=v2"]) - .output()?; - - if !output.status.success() { - return Err(anyhow!("Failed to get outdated formulae: {}", String::from_utf8_lossy(&output.stderr))); - } - - Ok(String::from_utf8(output.stdout)?) - } - - pub fn outdated_casks() -> Result { - let output = Command::new("brew") - .args(["outdated", "--cask", "--json=v2"]) - .output()?; - - if !output.status.success() { - return Err(anyhow!("Failed to get outdated casks: {}", String::from_utf8_lossy(&output.stderr))); - } - - Ok(String::from_utf8(output.stdout)?) - } - - pub fn install_formula(name: &str) -> Result { - let output = Command::new("brew") - .args(["install", "--formula", name]) - .output()?; + fn execute_brew_with_output(args: &[&str]) -> Result { + let output = Command::new("brew").args(args).output()?; let stdout = String::from_utf8(output.stdout)?; let stderr = String::from_utf8(output.stderr)?; if !output.status.success() { - return Err(anyhow!("Failed to install formula: {}", stderr)); + return Err(anyhow!("Brew command failed: {}", stderr)); } Ok(BrewOutput { stdout, stderr }) } - pub fn install_cask(name: &str) -> Result { - let output = Command::new("brew") - .args(["install", "--cask", name]) - .output()?; - - let stdout = String::from_utf8(output.stdout)?; - let stderr = String::from_utf8(output.stderr)?; - - if !output.status.success() { - return Err(anyhow!("Failed to install cask: {}", stderr)); - } - - Ok(BrewOutput { stdout, stderr }) + pub fn list_packages(package_type: PackageType) -> Result { + let type_arg = match package_type { + PackageType::Formula => "--formula", + PackageType::Cask => "--cask", + }; + tracing::debug!("Running: brew list {} --versions", type_arg); + let result = Self::execute_brew(&["list", type_arg, "--versions"])?; + tracing::debug!("brew list {} returned {} bytes", type_arg, result.len()); + Ok(result) } - pub fn uninstall_formula(name: &str) -> Result { + pub fn get_package_info(name: &str, package_type: PackageType) -> Result { + let type_arg = Self::get_package_type_arg(package_type); + tracing::debug!("Running: brew info --json=v2 {} {}", type_arg, name); + let output = Command::new("brew") - .args(["uninstall", "--formula", name]) + .args(&["info", "--json=v2", type_arg, name]) .output()?; - let stdout = String::from_utf8(output.stdout)?; - let stderr = String::from_utf8(output.stderr)?; - if !output.status.success() { - return Err(anyhow!("Failed to uninstall formula: {}", stderr)); + let error_msg = String::from_utf8_lossy(&output.stderr); + tracing::error!("brew info {} {} failed: {}", type_arg, name, error_msg); + return Err(anyhow!("Failed to get package info: {}", error_msg)); } - Ok(BrewOutput { stdout, stderr }) + let result = String::from_utf8(output.stdout)?; + tracing::debug!( + "brew info {} {} returned {} bytes", + type_arg, + name, + result.len() + ); + Ok(result) } - pub fn uninstall_cask(name: &str) -> Result { - let output = Command::new("brew") - .args(["uninstall", "--cask", name]) - .output()?; - - let stdout = String::from_utf8(output.stdout)?; - let stderr = String::from_utf8(output.stderr)?; + pub fn outdated_packages(package_type: PackageType) -> Result { + let type_arg = Self::get_package_type_arg(package_type); + Self::execute_brew(&["outdated", type_arg, "--json=v2"]) + } - if !output.status.success() { - return Err(anyhow!("Failed to uninstall cask: {}", stderr)); - } + pub fn install_package(name: &str, package_type: PackageType) -> Result { + let type_arg = Self::get_package_type_arg(package_type); + Self::execute_brew_with_output(&["install", type_arg, name]) + } - Ok(BrewOutput { stdout, stderr }) + pub fn uninstall_package(name: &str, package_type: PackageType) -> Result { + let type_arg = Self::get_package_type_arg(package_type); + Self::execute_brew_with_output(&["uninstall", type_arg, name]) } pub fn upgrade_package(name: &str) -> Result { - let output = Command::new("brew") - .args(["upgrade", name]) - .output()?; + let output = Command::new("brew").args(["upgrade", name]).output()?; let stdout = String::from_utf8(output.stdout)?; let stderr = String::from_utf8(output.stderr)?; @@ -169,9 +107,7 @@ impl BrewCommand { } pub fn upgrade_all() -> Result { - let output = Command::new("brew") - .args(["upgrade"]) - .output()?; + let output = Command::new("brew").args(["upgrade"]).output()?; let stdout = String::from_utf8(output.stdout)?; let stderr = String::from_utf8(output.stderr)?; @@ -184,21 +120,11 @@ impl BrewCommand { } pub fn cleanup_dry_run() -> Result { - let output = Command::new("brew") - .args(["cleanup", "-s", "--dry-run"]) - .output()?; - - if !output.status.success() { - return Err(anyhow!("Failed to get cleanup info: {}", String::from_utf8_lossy(&output.stderr))); - } - - Ok(String::from_utf8(output.stdout)?) + Self::execute_brew(&["cleanup", "-s", "--dry-run"]) } pub fn cleanup() -> Result { - let output = Command::new("brew") - .args(["cleanup", "-s"]) - .output()?; + let output = Command::new("brew").args(["cleanup", "-s"]).output()?; let stdout = String::from_utf8(output.stdout)?; let stderr = String::from_utf8(output.stderr)?; @@ -211,15 +137,7 @@ impl BrewCommand { } pub fn cleanup_old_versions_dry_run() -> Result { - let output = Command::new("brew") - .args(["cleanup", "--prune=all", "--dry-run"]) - .output()?; - - if !output.status.success() { - return Err(anyhow!("Failed to get cleanup info: {}", String::from_utf8_lossy(&output.stderr))); - } - - Ok(String::from_utf8(output.stdout)?) + Self::execute_brew(&["cleanup", "--prune=all", "--dry-run"]) } pub fn cleanup_old_versions() -> Result { @@ -237,69 +155,38 @@ impl BrewCommand { Ok(BrewOutput { stdout, stderr }) } - pub fn search_formula(query: &str) -> Result { - let output = Command::new("brew") - .args(["search", "--formula", query]) - .output()?; - - if !output.status.success() { - return Err(anyhow!("Failed to search formulae: {}", String::from_utf8_lossy(&output.stderr))); - } - - Ok(String::from_utf8(output.stdout)?) + pub fn search_packages(query: &str, package_type: PackageType) -> Result { + let type_arg = Self::get_package_type_arg(package_type); + Self::execute_brew(&["search", type_arg, query]) } - pub fn search_cask(query: &str) -> Result { - let output = Command::new("brew") - .args(["search", "--cask", query]) - .output()?; - - if !output.status.success() { - return Err(anyhow!("Failed to search casks: {}", String::from_utf8_lossy(&output.stderr))); - } - - Ok(String::from_utf8(output.stdout)?) + pub fn list_pinned() -> Result { + Self::execute_brew(&["list", "--pinned"]) } - pub fn list_pinned() -> Result { - let output = Command::new("brew") - .args(["list", "--pinned"]) - .output()?; + pub fn pin_package(name: &str) -> Result { + let output = Command::new("brew").args(["pin", name]).output()?; + + let stdout = String::from_utf8(output.stdout)?; + let stderr = String::from_utf8(output.stderr)?; if !output.status.success() { - return Err(anyhow!("Failed to list pinned packages: {}", String::from_utf8_lossy(&output.stderr))); + return Err(anyhow!("Failed to pin package: {}", stderr)); } - Ok(String::from_utf8(output.stdout)?) + Ok(BrewOutput { stdout, stderr }) } - pub fn pin_package(name: &str) -> Result { - let output = Command::new("brew") - .args(["pin", name]) - .output()?; + pub fn unpin_package(name: &str) -> Result { + let output = Command::new("brew").args(["unpin", name]).output()?; let stdout = String::from_utf8(output.stdout)?; let stderr = String::from_utf8(output.stderr)?; if !output.status.success() { - return Err(anyhow!("Failed to pin package: {}", stderr)); + return Err(anyhow!("Failed to unpin package: {}", stderr)); } Ok(BrewOutput { stdout, stderr }) } - - pub fn unpin_package(name: &str) -> Result { - let output = Command::new("brew") - .args(["unpin", name]) - .output()?; - - let stdout = String::from_utf8(output.stdout)?; - let stderr = String::from_utf8(output.stderr)?; - - if !output.status.success() { - return Err(anyhow!("Failed to unpin package: {}", stderr)); - } - - Ok(BrewOutput { stdout, stderr }) - } } diff --git a/src/infrastructure/brew/repository.rs b/src/infrastructure/brew/repository.rs index 6d8856d..f1b1bda 100644 --- a/src/infrastructure/brew/repository.rs +++ b/src/infrastructure/brew/repository.rs @@ -24,7 +24,49 @@ impl BrewPackageRepository { .collect()) } - fn parse_installed_packages(&self, json: &str, package_type: PackageType) -> Result> { + fn extract_package_item( + item: &Value, + package_type: PackageType, + version_key: &str, + is_pinned: bool, + ) -> Option { + let name = item.get("name").and_then(|v| v.as_str())?; + + let version_str = match version_key { + "installed" => item + .get("installed") + .and_then(|v| v.as_array()) + .and_then(|arr| arr.first()) + .and_then(|v| v.get("version")) + .and_then(|v| v.as_str()), + "installed_versions" => item + .get("installed_versions") + .and_then(|v| v.as_array()) + .and_then(|arr| arr.first()) + .and_then(|v| v.as_str()), + _ => None, + }; + + let mut package = Package::new(name.to_string(), package_type) + .set_installed(true) + .with_version(version_str.unwrap_or_default().to_string()) + .set_pinned(is_pinned); + + if let Some(current_version) = item.get("current_version").and_then(|v| v.as_str()) { + package = package + .set_outdated(true) + .with_available_version(current_version.to_string()); + } + + Some(package) + } + + fn parse_packages_from_json( + &self, + json: &str, + package_type: PackageType, + version_key: &str, + ) -> Result> { let data: Value = serde_json::from_str(json)?; let mut packages = Vec::new(); @@ -38,22 +80,16 @@ impl BrewPackageRepository { if let Some(items) = data.get(items_key).and_then(|v| v.as_array()) { for item in items { if let Some(name) = item.get("name").and_then(|v| v.as_str()) { - let version = item - .get("installed") - .and_then(|v| v.as_array()) - .and_then(|arr| arr.first()) - .and_then(|v| v.get("version")) - .and_then(|v| v.as_str()) - .map(String::from); - let is_pinned = pinned_packages.contains(&name.to_string()); - packages.push( - Package::new(name.to_string(), package_type.clone()) - .set_installed(true) - .with_version(version.unwrap_or_default()) - .set_pinned(is_pinned), - ); + if let Some(package) = Self::extract_package_item( + item, + package_type.clone(), + version_key, + is_pinned, + ) { + packages.push(package); + } } } } @@ -61,52 +97,60 @@ impl BrewPackageRepository { Ok(packages) } - fn parse_outdated_json(&self, json: &str, package_type: PackageType) -> Result> { - let data: Value = serde_json::from_str(json)?; - let mut packages = Vec::new(); + fn parse_installed_packages_plain_text( + &self, + output: &str, + package_type: PackageType, + pinned_packages: &[String], + ) -> Result> { + tracing::debug!( + "parse_installed_packages_plain_text called for {:?}", + package_type + ); + tracing::debug!("Output length: {} bytes", output.len()); - let items_key = match package_type { - PackageType::Formula => "formulae", - PackageType::Cask => "casks", - }; - - let pinned_packages = self.get_pinned_packages().unwrap_or_default(); - - if let Some(items) = data.get(items_key).and_then(|v| v.as_array()) { - for item in items { - if let Some(name) = item.get("name").and_then(|v| v.as_str()) { - let version = item - .get("installed_versions") - .and_then(|v| v.as_array()) - .and_then(|arr| arr.first()) - .and_then(|v| v.as_str()) - .map(String::from); - - let available_version = item - .get("current_version") - .and_then(|v| v.as_str()) - .map(String::from); + let mut packages = Vec::new(); + let mut line_count = 0; - let is_pinned = pinned_packages.contains(&name.to_string()); + for line in output.lines() { + line_count += 1; + let trimmed = line.trim(); + if trimmed.is_empty() { + continue; + } - let mut package = Package::new(name.to_string(), package_type.clone()) - .set_installed(true) - .set_outdated(true) - .with_version(version.unwrap_or_default()) - .set_pinned(is_pinned); + let parts: Vec<&str> = trimmed.split_whitespace().collect(); + if parts.len() >= 2 { + let name = parts[0].to_string(); + let version = parts[1].to_string(); + let is_pinned = pinned_packages.contains(&name); - if let Some(av) = available_version { - package = package.with_available_version(av); - } + let package = Package::new(name, package_type.clone()) + .set_installed(true) + .with_version(version) + .set_pinned(is_pinned); - packages.push(package); - } + packages.push(package); } } + tracing::debug!("Processing {} lines, parsed {} packages for {:?}", line_count, packages.len(), package_type); Ok(packages) } + fn parse_installed_packages( + &self, + output: &str, + package_type: PackageType, + ) -> Result> { + let pinned_packages = self.get_pinned_packages().unwrap_or_default(); + self.parse_installed_packages_plain_text(output, package_type, &pinned_packages) + } + + fn parse_outdated_json(&self, json: &str, package_type: PackageType) -> Result> { + self.parse_packages_from_json(json, package_type, "installed_versions") + } + fn parse_cleanup_output(&self, output: &str) -> Result { let mut items = Vec::new(); let mut total_size = 0u64; @@ -121,7 +165,19 @@ impl BrewPackageRepository { continue; } - if let Some(path_str) = trimmed.strip_prefix("Would remove: ").or_else(|| Some(trimmed)) { + let path_str_opt = if let Some(path_str) = trimmed.strip_prefix("Would remove: ") { + Some(path_str) + } else if !trimmed.is_empty() + && !trimmed.starts_with("Would remove:") + && !trimmed.starts_with("Removing:") + && !trimmed.starts_with("==>") + { + Some(trimmed) + } else { + None + }; + + if let Some(path_str) = path_str_opt { let path = Path::new(path_str); let size = if path.exists() { if path.is_file() { @@ -175,24 +231,25 @@ impl BrewPackageRepository { #[async_trait] impl PackageRepository for BrewPackageRepository { async fn get_installed_packages(&self, package_type: PackageType) -> Result> { + tracing::info!("get_installed_packages called for {:?}", package_type); let package_type_clone = package_type.clone(); - let output = tokio::task::spawn_blocking(move || match package_type_clone { - PackageType::Formula => BrewCommand::list_formulae(), - PackageType::Cask => BrewCommand::list_casks(), - }) - .await??; - - self.parse_installed_packages(&output, package_type) + let output = + tokio::task::spawn_blocking(move || BrewCommand::list_packages(package_type_clone)) + .await??; + tracing::info!("Got output for {:?}: {} bytes", package_type, output.len()); + let result = self.parse_installed_packages(&output, package_type); + tracing::info!( + "parse_installed_packages returned: {:?}", + result.as_ref().map(|p| p.len()).map_err(|e| e.to_string()) + ); + result } async fn get_outdated_packages(&self, package_type: PackageType) -> Result> { let package_type_clone = package_type.clone(); - let output = tokio::task::spawn_blocking(move || match package_type_clone { - PackageType::Formula => BrewCommand::outdated_formulae(), - PackageType::Cask => BrewCommand::outdated_casks(), - }) - .await??; - + let output = + tokio::task::spawn_blocking(move || BrewCommand::outdated_packages(package_type_clone)) + .await??; self.parse_outdated_json(&output, package_type) } @@ -200,11 +257,9 @@ impl PackageRepository for BrewPackageRepository { let name = package.name.clone(); let package_type = package.package_type.clone(); - let output = tokio::task::spawn_blocking(move || match package_type { - PackageType::Formula => BrewCommand::install_formula(&name), - PackageType::Cask => BrewCommand::install_cask(&name), - }) - .await??; + let output = + tokio::task::spawn_blocking(move || BrewCommand::install_package(&name, package_type)) + .await??; Self::log_brew_output(&output).await; @@ -215,9 +270,8 @@ impl PackageRepository for BrewPackageRepository { let name = package.name.clone(); let package_type = package.package_type.clone(); - let output = tokio::task::spawn_blocking(move || match package_type { - PackageType::Formula => BrewCommand::uninstall_formula(&name), - PackageType::Cask => BrewCommand::uninstall_cask(&name), + let output = tokio::task::spawn_blocking(move || { + BrewCommand::uninstall_package(&name, package_type) }) .await??; @@ -229,7 +283,8 @@ impl PackageRepository for BrewPackageRepository { async fn update_package(&self, package: &Package) -> Result<()> { let name = package.name.clone(); - let output = tokio::task::spawn_blocking(move || BrewCommand::upgrade_package(&name)).await??; + let output = + tokio::task::spawn_blocking(move || BrewCommand::upgrade_package(&name)).await??; Self::log_brew_output(&output).await; @@ -250,7 +305,8 @@ impl PackageRepository for BrewPackageRepository { } async fn get_cleanup_old_versions_preview(&self) -> Result { - let output = tokio::task::spawn_blocking(|| BrewCommand::cleanup_old_versions_dry_run()).await??; + let output = + tokio::task::spawn_blocking(|| BrewCommand::cleanup_old_versions_dry_run()).await??; self.parse_cleanup_output(&output) } @@ -270,12 +326,15 @@ impl PackageRepository for BrewPackageRepository { Ok(()) } - async fn search_packages(&self, query: &str, package_type: PackageType) -> Result> { + async fn search_packages( + &self, + query: &str, + package_type: PackageType, + ) -> Result> { let query = query.to_string(); let package_type_clone = package_type.clone(); - let output = tokio::task::spawn_blocking(move || match package_type_clone { - PackageType::Formula => BrewCommand::search_formula(&query), - PackageType::Cask => BrewCommand::search_cask(&query), + let output = tokio::task::spawn_blocking(move || { + BrewCommand::search_packages(&query, package_type_clone) }) .await??; @@ -290,39 +349,42 @@ impl PackageRepository for BrewPackageRepository { async fn get_package_info(&self, name: &str, package_type: PackageType) -> Result { tracing::debug!("get_package_info called for {} ({:?})", name, package_type); - + let name = name.to_string(); let name_clone = name.clone(); let package_type_clone = package_type.clone(); - + let output = tokio::time::timeout( std::time::Duration::from_secs(10), - tokio::task::spawn_blocking(move || match package_type_clone { - PackageType::Formula => BrewCommand::get_formula_info(&name_clone), - PackageType::Cask => BrewCommand::get_cask_info(&name_clone), - }) + tokio::task::spawn_blocking(move || { + BrewCommand::get_package_info(&name_clone, package_type_clone) + }), ) .await .map_err(|_| anyhow::anyhow!("Timeout loading package info for {}", name))???; - + tracing::debug!("Raw brew output for {}: {} bytes", name, output.len()); - let data: Value = serde_json::from_str(&output) - .map_err(|e| { - tracing::error!("Failed to parse JSON for {}: {}", name, e); - e - })?; - + let data: Value = serde_json::from_str(&output).map_err(|e| { + tracing::error!("Failed to parse JSON for {}: {}", name, e); + e + })?; + tracing::debug!("Parsed JSON for {}: {:?}", name, data); - + let items_key = match package_type { PackageType::Formula => "formulae", PackageType::Cask => "casks", }; if let Some(items) = data.get(items_key).and_then(|v| v.as_array()) { - tracing::debug!("Found {} items for {} in '{}'", items.len(), name, items_key); - + tracing::debug!( + "Found {} items for {} in '{}'", + items.len(), + name, + items_key + ); + if let Some(item) = items.first() { let version = item .get("version") @@ -330,12 +392,14 @@ impl PackageRepository for BrewPackageRepository { .and_then(|v| v.as_str()) .map(String::from); - let description = item - .get("desc") - .and_then(|v| v.as_str()) - .map(String::from); + let description = item.get("desc").and_then(|v| v.as_str()).map(String::from); - tracing::debug!("Extracted for {}: version={:?}, desc={:?}", name, version, description); + tracing::debug!( + "Extracted for {}: version={:?}, desc={:?}", + name, + version, + description + ); let mut package = Package::new(name.clone(), package_type); if let Some(v) = version { @@ -344,7 +408,7 @@ impl PackageRepository for BrewPackageRepository { if let Some(d) = description { package = package.with_description(d); } - + tracing::debug!("Successfully created package info for {}", name); return Ok(package); } else { @@ -366,12 +430,13 @@ impl PackageRepository for BrewPackageRepository { Ok(()) } - async fn unpin_package(&self, package: &Package) -> Result<()> { - let name = package.name.clone(); - let output = tokio::task::spawn_blocking(move || BrewCommand::unpin_package(&name)).await??; - - Self::log_brew_output(&output).await; - - Ok(()) - } + async fn unpin_package(&self, package: &Package) -> Result<()> { + let name = package.name.clone(); + let output = + tokio::task::spawn_blocking(move || BrewCommand::unpin_package(&name)).await??; + + Self::log_brew_output(&output).await; + + Ok(()) + } } diff --git a/src/main.rs b/src/main.rs index 85f783c..a7ab9c4 100644 --- a/src/main.rs +++ b/src/main.rs @@ -6,8 +6,8 @@ mod presentation; use application::UseCaseContainer; use domain::repositories::PackageRepository; use infrastructure::brew::BrewPackageRepository; -use presentation::ui::BrewstyApp; use presentation::services::log_capture; +use presentation::ui::BrewstyApp; use std::sync::Arc; fn main() -> eframe::Result<()> { @@ -26,8 +26,6 @@ fn main() -> eframe::Result<()> { eframe::run_native( "Brewsty - Homebrew Package Manager", options, - Box::new(|_cc| { - Ok(Box::new(BrewstyApp::new(use_cases, log_rx))) - }), + Box::new(|_cc| Ok(Box::new(BrewstyApp::new(use_cases, log_rx)))), ) } diff --git a/src/presentation/components/cleanup_modal.rs b/src/presentation/components/cleanup_modal.rs index 12eb69d..f4bb893 100644 --- a/src/presentation/components/cleanup_modal.rs +++ b/src/presentation/components/cleanup_modal.rs @@ -50,11 +50,17 @@ impl CleanupModal { .resizable(true) .show(ctx, |ui| { if let Some(preview) = &self.preview { - ui.heading(format!("Total size to free: {}", format_size(preview.total_size))); + ui.heading(format!( + "Total size to free: {}", + format_size(preview.total_size) + )); ui.separator(); - ui.label(format!("Files and folders to be removed ({} items):", preview.items.len())); - + ui.label(format!( + "Files and folders to be removed ({} items):", + preview.items.len() + )); + egui::ScrollArea::vertical() .max_height(300.0) .show(ui, |ui| { diff --git a/src/presentation/components/info_modal.rs b/src/presentation/components/info_modal.rs index 9349bda..8798387 100644 --- a/src/presentation/components/info_modal.rs +++ b/src/presentation/components/info_modal.rs @@ -63,7 +63,7 @@ impl InfoModal { } }); }); - + if !open { self.close(); } diff --git a/src/presentation/components/log_manager.rs b/src/presentation/components/log_manager.rs index 3cef087..8db3986 100644 --- a/src/presentation/components/log_manager.rs +++ b/src/presentation/components/log_manager.rs @@ -2,14 +2,38 @@ use std::collections::VecDeque; const MAX_LOG_SIZE: usize = 200; +#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash)] +pub enum LogLevel { + Trace, + Debug, + Info, + Warn, + Error, +} + +impl LogLevel { + pub fn from_str(s: &str) -> Option { + match s { + "TRACE" => Some(LogLevel::Trace), + "DEBUG" => Some(LogLevel::Debug), + "INFO" => Some(LogLevel::Info), + "WARN" => Some(LogLevel::Warn), + "ERROR" => Some(LogLevel::Error), + _ => None, + } + } +} + pub struct LogEntry { pub message: String, pub timestamp: std::time::SystemTime, + pub level: LogLevel, } impl LogEntry { pub fn format_timestamp(&self) -> String { - let timestamp = self.timestamp + let timestamp = self + .timestamp .duration_since(std::time::UNIX_EPOCH) .unwrap_or_default(); let hours = (timestamp.as_secs() / 3600) % 24; @@ -21,22 +45,35 @@ impl LogEntry { pub struct LogManager { logs: VecDeque, + visible_levels: std::collections::HashSet, } impl LogManager { pub fn new() -> Self { + let mut visible_levels = std::collections::HashSet::new(); + visible_levels.insert(LogLevel::Info); + visible_levels.insert(LogLevel::Warn); + visible_levels.insert(LogLevel::Error); Self { logs: VecDeque::with_capacity(MAX_LOG_SIZE), + visible_levels, } } pub fn push(&mut self, message: String) { + let level = message + .split(']') + .next() + .and_then(|s| s.strip_prefix('[')) + .and_then(|level_str| LogLevel::from_str(level_str)) + .unwrap_or(LogLevel::Info); if self.logs.len() >= MAX_LOG_SIZE { self.logs.pop_front(); } self.logs.push_back(LogEntry { message, timestamp: std::time::SystemTime::now(), + level, }); } @@ -49,6 +86,31 @@ impl LogManager { pub fn all_logs(&self) -> impl Iterator { self.logs.iter() } + + pub fn filtered_logs(&self) -> impl Iterator { + self.logs + .iter() + .filter(move |entry| self.visible_levels.contains(&entry.level)) + } + + pub fn filtered_logs_reversed(&self) -> impl Iterator { + self.logs + .iter() + .rev() + .filter(move |entry| self.visible_levels.contains(&entry.level)) + } + + pub fn set_level_visible(&mut self, level: LogLevel, visible: bool) { + if visible { + self.visible_levels.insert(level); + } else { + self.visible_levels.remove(&level); + } + } + + pub fn is_level_visible(&self, level: LogLevel) -> bool { + self.visible_levels.contains(&level) + } } impl Default for LogManager { diff --git a/src/presentation/components/mod.rs b/src/presentation/components/mod.rs index 78243d5..69e16cf 100644 --- a/src/presentation/components/mod.rs +++ b/src/presentation/components/mod.rs @@ -1,13 +1,13 @@ -pub mod package_list; -pub mod tab_manager; -pub mod filter_state; pub mod cleanup_modal; -pub mod log_manager; +pub mod filter_state; pub mod info_modal; +pub mod log_manager; +pub mod package_list; +pub mod tab_manager; -pub use package_list::PackageList; -pub use tab_manager::{Tab, TabManager}; +pub use cleanup_modal::{CleanupAction, CleanupModal, CleanupType}; pub use filter_state::FilterState; -pub use cleanup_modal::{CleanupModal, CleanupType, CleanupAction}; -pub use log_manager::LogManager; pub use info_modal::InfoModal; +pub use log_manager::{LogLevel, LogManager}; +pub use package_list::PackageList; +pub use tab_manager::{Tab, TabManager}; diff --git a/src/presentation/components/package_list.rs b/src/presentation/components/package_list.rs index bf3086b..848f1e4 100644 --- a/src/presentation/components/package_list.rs +++ b/src/presentation/components/package_list.rs @@ -49,127 +49,143 @@ impl PackageList { on_unpin: &mut Option, ) { let search_lower = search_query.to_lowercase(); - - ScrollArea::vertical().auto_shrink([false, false]).show(ui, |ui| { - egui::Grid::new("package_grid") - .striped(true) - .spacing([10.0, 8.0]) - .min_col_width(ui.available_width() / 5.0) - .show(ui, |ui| { - ui.heading("Name"); - ui.heading("Version"); - ui.heading("Type"); - ui.heading("Status"); - ui.heading("Actions"); - ui.end_row(); - - for package in &self.packages { - let should_show = match package.package_type { - PackageType::Formula => show_formulae, - PackageType::Cask => show_casks, - }; - - if !should_show { - continue; - } - if !search_query.is_empty() && !package.name.to_lowercase().contains(&search_lower) { - continue; - } + ScrollArea::vertical() + .auto_shrink([false, false]) + .show(ui, |ui| { + egui::Grid::new("package_grid") + .striped(true) + .spacing([10.0, 8.0]) + .min_col_width(ui.available_width() / 5.0) + .show(ui, |ui| { + ui.heading("Name"); + ui.heading("Version"); + ui.heading("Type"); + ui.heading("Status"); + ui.heading("Actions"); + ui.end_row(); - let is_selected = self - .selected_package - .as_ref() - .map_or(false, |s| s == &package.name); + for package in &self.packages { + let should_show = match package.package_type { + PackageType::Formula => show_formulae, + PackageType::Cask => show_casks, + }; - if ui - .selectable_label(is_selected, &package.name) - .clicked() - { - self.selected_package = Some(package.name.clone()); - } + if !should_show { + continue; + } + + if !search_query.is_empty() + && !package.name.to_lowercase().contains(&search_lower) + { + continue; + } + + let is_selected = self + .selected_package + .as_ref() + .map_or(false, |s| s == &package.name); + + if ui.selectable_label(is_selected, &package.name).clicked() { + self.selected_package = Some(package.name.clone()); + } - let version_text = if package.version_load_failed { - "Failed".to_string() - } else if package.outdated { - if let Some(av) = &package.available_version { - format!("{} -> {}", package.version.as_deref().unwrap_or("N/A"), av) + let version_text = if package.version_load_failed { + "Failed".to_string() + } else if package.outdated { + if let Some(av) = &package.available_version { + format!( + "{} -> {}", + package.version.as_deref().unwrap_or("N/A"), + av + ) + } else { + package.version.as_deref().unwrap_or("N/A").to_string() + } } else { package.version.as_deref().unwrap_or("N/A").to_string() + }; + + if packages_loading_info.contains(&package.name) { + ui.spinner(); + } else if package.version_load_failed { + ui.label( + RichText::new(version_text).color(Color32::from_rgb(255, 0, 0)), + ); + } else if package.pinned { + ui.label( + RichText::new(version_text) + .color(Color32::from_rgb(255, 200, 0)), + ); + } else { + ui.label(version_text); } - } else { - package.version.as_deref().unwrap_or("N/A").to_string() - }; - - if packages_loading_info.contains(&package.name) { - ui.spinner(); - } else if package.version_load_failed { - ui.label(RichText::new(version_text).color(Color32::from_rgb(255, 0, 0))); - } else if package.pinned { - ui.label(RichText::new(version_text).color(Color32::from_rgb(255, 200, 0))); - } else { - ui.label(version_text); - } - - ui.label(package.package_type.to_string()); - - let is_operating = packages_loading_info.contains(&package.name); - let status_text = if package.pinned { - RichText::new("Pinned").color(Color32::from_rgb(255, 200, 0)) - } else if package.outdated { - RichText::new("Outdated").color(Color32::from_rgb(255, 165, 0)) - } else if package.installed { - RichText::new("Installed").color(Color32::from_rgb(0, 255, 0)) - } else { - RichText::new("Available").color(Color32::GRAY) - }; - - if is_operating { - ui.spinner(); - } else { - ui.label(status_text); - } - ui.horizontal(|ui| { - if package.installed { - if ui.button("Uninstall").clicked() { - *on_uninstall = Some(package.clone()); - } - if package.outdated && !package.pinned && ui.button("Update").clicked() { - *on_update = Some(package.clone()); - } - // Only show pin/unpin for formulae (casks don't support pinning in Homebrew) - if matches!(package.package_type, PackageType::Formula) { - if package.pinned { - if ui.button("Unpin").clicked() { - *on_unpin = Some(package.clone()); - } - } else { - if ui.button("Pin").clicked() { - *on_pin = Some(package.clone()); - } - } - } + ui.label(package.package_type.to_string()); + + let is_operating = packages_loading_info.contains(&package.name); + let status_text = if package.pinned { + RichText::new("Pinned").color(Color32::from_rgb(255, 200, 0)) + } else if package.outdated { + RichText::new("Outdated").color(Color32::from_rgb(255, 165, 0)) + } else if package.installed { + RichText::new("Installed").color(Color32::from_rgb(0, 255, 0)) } else { - if ui.button("Install").clicked() { - *on_install = Some(package.clone()); - } + RichText::new("Available").color(Color32::GRAY) + }; + + if is_operating { + ui.spinner(); + } else { + ui.label(status_text); } - - if package.version.is_none() && !package.version_load_failed && !packages_loading_info.contains(&package.name) { - if ui.button("Load Info").clicked() { - *on_load_info = Some(package.clone()); + + ui.horizontal(|ui| { + if package.installed { + if ui.button("Uninstall").clicked() { + *on_uninstall = Some(package.clone()); + } + if package.outdated + && !package.pinned + && ui.button("Update").clicked() + { + *on_update = Some(package.clone()); + } + // Only show pin/unpin for formulae (casks don't support pinning in Homebrew) + if matches!(package.package_type, PackageType::Formula) { + if package.pinned { + if ui.button("Unpin").clicked() { + *on_unpin = Some(package.clone()); + } + } else { + if ui.button("Pin").clicked() { + *on_pin = Some(package.clone()); + } + } + } + } else { + if ui.button("Install").clicked() { + *on_install = Some(package.clone()); + } } - } else if package.description.is_some() { - if ui.button("Info").clicked() { - self.show_info_action = Some(package.clone()); + + if package.version.is_none() + && !package.version_load_failed + && !packages_loading_info.contains(&package.name) + { + if ui.button("Load Info").clicked() { + *on_load_info = Some(package.clone()); + } + } else if package.description.is_some() { + if ui.button("Info").clicked() { + self.show_info_action = Some(package.clone()); + } } - } - }); + }); - ui.end_row(); - } - }); - }); + ui.end_row(); + } + }); + }); } } diff --git a/src/presentation/services/async_task_manager.rs b/src/presentation/services/async_task_manager.rs index 000132a..6413e05 100644 --- a/src/presentation/services/async_task_manager.rs +++ b/src/presentation/services/async_task_manager.rs @@ -1,6 +1,13 @@ use crate::domain::entities::{Package, PackageType}; -use std::sync::{Arc, Mutex}; use std::collections::HashSet; +use std::sync::{Arc, Mutex}; + +#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)] +pub enum TaskKind { + LoadInstalled, + LoadOutdated, + Search, +} pub enum AsyncTask { LoadInstalled { @@ -83,7 +90,7 @@ pub struct TaskResult { } pub struct AsyncTaskManager { - active_task: Option, + active_tasks: Vec, package_info_tasks: Vec<(String, AsyncTask)>, packages_loading_info: HashSet, pending_package_info_loads: Vec<(String, PackageType)>, @@ -92,7 +99,7 @@ pub struct AsyncTaskManager { impl AsyncTaskManager { pub fn new() -> Self { Self { - active_task: None, + active_tasks: Vec::new(), package_info_tasks: Vec::new(), packages_loading_info: HashSet::new(), pending_package_info_loads: Vec::new(), @@ -100,7 +107,18 @@ impl AsyncTaskManager { } pub fn set_active_task(&mut self, task: AsyncTask) { - self.active_task = Some(task); + if let Some(kind) = task.kind() { + if self.has_task_kind(kind) { + tracing::warn!("{:?} task is already running, ignoring duplicate", kind); + return; + } + } + + self.active_tasks.push(task); + } + + pub fn has_task_kind(&self, kind: TaskKind) -> bool { + self.active_tasks.iter().any(|task| task.kind() == Some(kind)) } pub fn add_package_info_task(&mut self, package_name: String, task: AsyncTask) { @@ -117,13 +135,18 @@ impl AsyncTaskManager { tracing::debug!("Already loading info for {}, skipping", package_name); return; } - - if self.pending_package_info_loads.iter().any(|(name, _)| name == &package_name) { + + if self + .pending_package_info_loads + .iter() + .any(|(name, _)| name == &package_name) + { tracing::debug!("Already queued for loading: {}", package_name); return; } - - self.pending_package_info_loads.push((package_name, package_type)); + + self.pending_package_info_loads + .push((package_name, package_type)); } pub fn can_load_more_package_info(&self) -> bool { @@ -131,7 +154,9 @@ impl AsyncTaskManager { } pub fn drain_pending_loads(&mut self, count: usize) -> Vec<(String, PackageType)> { - self.pending_package_info_loads.drain(..count.min(self.pending_package_info_loads.len())).collect() + self.pending_package_info_loads + .drain(..count.min(self.pending_package_info_loads.len())) + .collect() } pub fn pending_loads_count(&self) -> usize { @@ -157,14 +182,23 @@ impl AsyncTaskManager { }; let mut tasks_to_keep = Vec::new(); - + for (pkg_name, task) in self.package_info_tasks.drain(..) { match task { - AsyncTask::LoadPackageInfo { package_name, package_type, result: pkg_result, started_at } => { + AsyncTask::LoadPackageInfo { + package_name, + package_type, + result: pkg_result, + started_at, + } => { let elapsed = started_at.elapsed(); - + if elapsed > std::time::Duration::from_secs(10) { - tracing::warn!("Package info loading timed out for {} after {:?}", package_name, elapsed); + tracing::warn!( + "Package info loading timed out for {} after {:?}", + package_name, + elapsed + ); let failed_package = Package::new(package_name.clone(), package_type) .set_version_load_failed(true); result.package_info = Some((package_name.clone(), failed_package)); @@ -172,12 +206,15 @@ impl AsyncTaskManager { result.completed_package_info_loads.push(package_name); continue; } - + let package_name_clone = package_name.clone(); let should_keep = match pkg_result.try_lock() { Ok(pkg_opt) => { if let Some(package) = pkg_opt.clone() { - tracing::info!("Updating search results with package info for {}", package_name_clone); + tracing::info!( + "Updating search results with package info for {}", + package_name_clone + ); result.package_info = Some((package_name_clone.clone(), package)); self.packages_loading_info.remove(&package_name_clone); result.completed_package_info_loads.push(package_name_clone); @@ -186,20 +223,30 @@ impl AsyncTaskManager { true } } - Err(_) => true + Err(_) => true, }; - + if should_keep { - tasks_to_keep.push((pkg_name, AsyncTask::LoadPackageInfo { package_name, package_type, result: pkg_result, started_at })); + tasks_to_keep.push(( + pkg_name, + AsyncTask::LoadPackageInfo { + package_name, + package_type, + result: pkg_result, + started_at, + }, + )); } } _ => {} } } - + self.package_info_tasks = tasks_to_keep; - if let Some(task) = self.active_task.take() { + let mut active_tasks_to_keep = Vec::new(); + + for task in self.active_tasks.drain(..) { match task { AsyncTask::LoadInstalled { packages, logs } => { let should_put_back = match logs.try_lock() { @@ -216,11 +263,11 @@ impl AsyncTaskManager { true } } - Err(_) => true + Err(_) => true, }; - + if should_put_back { - self.active_task = Some(AsyncTask::LoadInstalled { packages, logs }); + active_tasks_to_keep.push(AsyncTask::LoadInstalled { packages, logs }); } } AsyncTask::LoadOutdated { packages, logs } => { @@ -238,11 +285,11 @@ impl AsyncTaskManager { true } } - Err(_) => true + Err(_) => true, }; - + if should_put_back { - self.active_task = Some(AsyncTask::LoadOutdated { packages, logs }); + active_tasks_to_keep.push(AsyncTask::LoadOutdated { packages, logs }); } } AsyncTask::Search { results, logs } => { @@ -250,7 +297,10 @@ impl AsyncTaskManager { Ok(res) => { if let Ok(log) = logs.try_lock() { if !log.is_empty() { - tracing::info!("Search completed, found {} packages", res.len()); + tracing::info!( + "Search completed, found {} packages", + res.len() + ); result.search_results = Some(res.clone()); result.logs.extend(log.clone()); false @@ -261,14 +311,18 @@ impl AsyncTaskManager { true } } - Err(_) => true + Err(_) => true, }; - + if should_put_back { - self.active_task = Some(AsyncTask::Search { results, logs }); + active_tasks_to_keep.push(AsyncTask::Search { results, logs }); } } - AsyncTask::Install { success, logs, message } => { + AsyncTask::Install { + success, + logs, + message, + } => { let should_put_back = match success.try_lock() { Ok(success_opt) => { if let Some(succeeded) = *success_opt { @@ -285,12 +339,20 @@ impl AsyncTaskManager { } Err(_) => true, }; - + if should_put_back { - self.active_task = Some(AsyncTask::Install { success, logs, message }); + active_tasks_to_keep.push(AsyncTask::Install { + success, + logs, + message, + }); } } - AsyncTask::Uninstall { success, logs, message } => { + AsyncTask::Uninstall { + success, + logs, + message, + } => { let should_put_back = match success.try_lock() { Ok(success_opt) => { if let Some(succeeded) = *success_opt { @@ -307,12 +369,20 @@ impl AsyncTaskManager { } Err(_) => true, }; - + if should_put_back { - self.active_task = Some(AsyncTask::Uninstall { success, logs, message }); + active_tasks_to_keep.push(AsyncTask::Uninstall { + success, + logs, + message, + }); } } - AsyncTask::Update { success, logs, message } => { + AsyncTask::Update { + success, + logs, + message, + } => { let should_put_back = match success.try_lock() { Ok(success_opt) => { if let Some(succeeded) = *success_opt { @@ -329,12 +399,20 @@ impl AsyncTaskManager { } Err(_) => true, }; - + if should_put_back { - self.active_task = Some(AsyncTask::Update { success, logs, message }); + active_tasks_to_keep.push(AsyncTask::Update { + success, + logs, + message, + }); } } - AsyncTask::UpdateAll { success, logs, message } => { + AsyncTask::UpdateAll { + success, + logs, + message, + } => { let should_put_back = match success.try_lock() { Ok(success_opt) => { if let Some(succeeded) = *success_opt { @@ -351,12 +429,20 @@ impl AsyncTaskManager { } Err(_) => true, }; - + if should_put_back { - self.active_task = Some(AsyncTask::UpdateAll { success, logs, message }); + active_tasks_to_keep.push(AsyncTask::UpdateAll { + success, + logs, + message, + }); } } - AsyncTask::CleanCache { success, logs, message } => { + AsyncTask::CleanCache { + success, + logs, + message, + } => { let should_put_back = match success.try_lock() { Ok(success_opt) => { if let Some(succeeded) = *success_opt { @@ -373,17 +459,26 @@ impl AsyncTaskManager { } Err(_) => true, }; - + if should_put_back { - self.active_task = Some(AsyncTask::CleanCache { success, logs, message }); + active_tasks_to_keep.push(AsyncTask::CleanCache { + success, + logs, + message, + }); } } - AsyncTask::CleanupOldVersions { success, logs, message } => { + AsyncTask::CleanupOldVersions { + success, + logs, + message, + } => { let should_put_back = match success.try_lock() { Ok(success_opt) => { if let Some(succeeded) = *success_opt { if let (Ok(log), Ok(msg)) = (logs.try_lock(), message.try_lock()) { - result.cleanup_old_versions_completed = Some((succeeded, msg.clone())); + result.cleanup_old_versions_completed = + Some((succeeded, msg.clone())); result.logs.extend(log.clone()); false } else { @@ -395,17 +490,27 @@ impl AsyncTaskManager { } Err(_) => true, }; - + if should_put_back { - self.active_task = Some(AsyncTask::CleanupOldVersions { success, logs, message }); + active_tasks_to_keep.push(AsyncTask::CleanupOldVersions { + success, + logs, + message, + }); } } - AsyncTask::Pin { package_name, success, logs, message } => { + AsyncTask::Pin { + package_name, + success, + logs, + message, + } => { let should_put_back = match success.try_lock() { Ok(success_opt) => { if let Some(succeeded) = *success_opt { if let (Ok(log), Ok(msg)) = (logs.try_lock(), message.try_lock()) { - result.pin_completed = Some((package_name.clone(), succeeded, msg.clone())); + result.pin_completed = + Some((package_name.clone(), succeeded, msg.clone())); result.logs.extend(log.clone()); false } else { @@ -417,17 +522,28 @@ impl AsyncTaskManager { } Err(_) => true, }; - + if should_put_back { - self.active_task = Some(AsyncTask::Pin { package_name, success, logs, message }); + active_tasks_to_keep.push(AsyncTask::Pin { + package_name, + success, + logs, + message, + }); } } - AsyncTask::Unpin { package_name, success, logs, message } => { + AsyncTask::Unpin { + package_name, + success, + logs, + message, + } => { let should_put_back = match success.try_lock() { Ok(success_opt) => { if let Some(succeeded) = *success_opt { if let (Ok(log), Ok(msg)) = (logs.try_lock(), message.try_lock()) { - result.unpin_completed = Some((package_name.clone(), succeeded, msg.clone())); + result.unpin_completed = + Some((package_name.clone(), succeeded, msg.clone())); result.logs.extend(log.clone()); false } else { @@ -439,16 +555,33 @@ impl AsyncTaskManager { } Err(_) => true, }; - + if should_put_back { - self.active_task = Some(AsyncTask::Unpin { package_name, success, logs, message }); + active_tasks_to_keep.push(AsyncTask::Unpin { + package_name, + success, + logs, + message, + }); } } - AsyncTask::LoadPackageInfo { .. } => { - } + AsyncTask::LoadPackageInfo { .. } => {} } } + self.active_tasks = active_tasks_to_keep; + result } } + +impl AsyncTask { + pub fn kind(&self) -> Option { + match self { + AsyncTask::LoadInstalled { .. } => Some(TaskKind::LoadInstalled), + AsyncTask::LoadOutdated { .. } => Some(TaskKind::LoadOutdated), + AsyncTask::Search { .. } => Some(TaskKind::Search), + _ => None, + } + } +} diff --git a/src/presentation/services/log_capture.rs b/src/presentation/services/log_capture.rs index 13af17b..9568ac3 100644 --- a/src/presentation/services/log_capture.rs +++ b/src/presentation/services/log_capture.rs @@ -1,22 +1,32 @@ -use std::sync::mpsc::{channel, Receiver, Sender}; +use std::sync::mpsc::{Receiver, Sender, channel}; +use tracing_subscriber::Layer; +use tracing_subscriber::filter::LevelFilter; use tracing_subscriber::layer::SubscriberExt; use tracing_subscriber::util::SubscriberInitExt; -use tracing_subscriber::Layer; static LOG_SENDER: std::sync::OnceLock> = std::sync::OnceLock::new(); pub fn init_log_capture() -> Receiver { let (tx, rx) = channel(); - LOG_SENDER.set(tx).expect("log capture already initialized - init_log_capture() must be called exactly once"); - + LOG_SENDER + .set(tx) + .expect("log capture already initialized - init_log_capture() must be called exactly once"); + let capture_layer = CaptureLayer { sender: LOG_SENDER.get().unwrap().clone(), }; - + + #[cfg(debug_assertions)] + let filter = LevelFilter::TRACE; + + #[cfg(not(debug_assertions))] + let filter = LevelFilter::DEBUG; + tracing_subscriber::registry() + .with(filter) .with(capture_layer) .init(); - + rx } @@ -35,19 +45,22 @@ where ) { let metadata = event.metadata(); let target = metadata.target(); - - if !target.starts_with("brewsty::infrastructure::brew") { + + if !target.starts_with("brewsty::infrastructure::brew") + && !target.starts_with("brewsty::application") + && !target.starts_with("brewsty::presentation") + { return; } - + let level = *metadata.level(); - + let mut visitor = LogVisitor { message: String::new(), }; - + event.record(&mut visitor); - + if !visitor.message.is_empty() { let log_entry = format!("[{}] {}", level, visitor.message); let _ = self.sender.send(log_entry); @@ -65,7 +78,7 @@ impl tracing::field::Visit for LogVisitor { self.message = format!("{:?}", value); } } - + fn record_str(&mut self, field: &tracing::field::Field, value: &str) { if field.name() == "message" { self.message = value.to_string(); diff --git a/src/presentation/ui/app.rs b/src/presentation/ui/app.rs index 9077764..c0573a4 100644 --- a/src/presentation/ui/app.rs +++ b/src/presentation/ui/app.rs @@ -1,13 +1,13 @@ use crate::application::UseCaseContainer; use crate::domain::entities::{Package, PackageType}; use crate::presentation::components::{ - CleanupAction, CleanupModal, CleanupType, FilterState, InfoModal, LogManager, PackageList, Tab, TabManager + CleanupAction, CleanupModal, CleanupType, FilterState, InfoModal, LogLevel, LogManager, + PackageList, Tab, TabManager, }; -use crate::presentation::services::{ - AsyncExecutor, AsyncTask, AsyncTaskManager -}; -use std::sync::{Arc, Mutex}; +use crate::presentation::services::{AsyncExecutor, AsyncTask, AsyncTaskManager}; +use anyhow::Result; use std::sync::mpsc::Receiver; +use std::sync::{Arc, Mutex}; use std::thread; pub struct BrewstyApp { @@ -17,36 +17,36 @@ pub struct BrewstyApp { info_modal: InfoModal, log_manager: LogManager, log_rx: Receiver, - + installed_packages: PackageList, outdated_packages: PackageList, search_results: PackageList, - + auto_load_version_info: bool, - + initialized: bool, - + loading_installed: bool, loading_outdated: bool, loading_search: bool, - + loading_install: bool, loading_uninstall: bool, loading_update: bool, loading_update_all: bool, loading_clean_cache: bool, loading_cleanup_old_versions: bool, - + current_install_package: Option, current_uninstall_package: Option, current_update_package: Option, packages_in_operation: std::collections::HashSet, - + task_manager: AsyncTaskManager, - + use_cases: Arc, executor: AsyncExecutor, - + loading: bool, status_message: String, output_panel_height: f32, @@ -74,14 +74,14 @@ impl BrewstyApp { loading_install: false, loading_uninstall: false, loading_update: false, - loading_update_all: false, - loading_clean_cache: false, - loading_cleanup_old_versions: false, - current_install_package: None, - current_uninstall_package: None, - current_update_package: None, - packages_in_operation: std::collections::HashSet::new(), - task_manager: AsyncTaskManager::new(), + loading_update_all: false, + loading_clean_cache: false, + loading_cleanup_old_versions: false, + current_install_package: None, + current_uninstall_package: None, + current_update_package: None, + packages_in_operation: std::collections::HashSet::new(), + task_manager: AsyncTaskManager::new(), use_cases, executor, loading: false, @@ -94,70 +94,112 @@ impl BrewstyApp { if self.loading_installed { return; } - + self.loading_installed = true; self.status_message = "Loading installed packages...".to_string(); - self.log_manager.push("Loading installed packages (formulae and casks)".to_string()); + self.log_manager + .push("Loading installed packages (formulae and casks)".to_string()); tracing::info!("Loading installed packages (formulae and casks)"); let use_case_formulae = Arc::clone(&self.use_cases.list_installed); let use_case_casks = Arc::clone(&self.use_cases.list_installed); - + let installed_packages = Arc::new(Mutex::new(Vec::new())); let output_log = Arc::new(Mutex::new(Vec::new())); - + self.task_manager.set_active_task(AsyncTask::LoadInstalled { packages: Arc::clone(&installed_packages), logs: Arc::clone(&output_log), }); - - thread::spawn(move || { - let rt = tokio::runtime::Runtime::new().unwrap(); - - let formulae_result = rt.block_on(async { - use_case_formulae.execute(PackageType::Formula).await - }); - - let casks_result = rt.block_on(async { - use_case_casks.execute(PackageType::Cask).await - }); - - let mut packages = Vec::new(); - let mut logs = Vec::new(); - match formulae_result { - Ok(pkgs) => { - let msg = format!("Loaded {} formulae", pkgs.len()); - logs.push(msg.clone()); - tracing::info!("{}", msg); - packages.extend(pkgs); - } - Err(e) => { - let msg = format!("Error loading formulae: {}", e); - logs.push(msg.clone()); - tracing::error!("{}", msg); + thread::spawn(move || { + tracing::trace!("THREAD STARTED: load_installed_packages"); + if let Err(e) = (|| -> anyhow::Result<()> { + tracing::trace!("THREAD: about to create runtime"); + let rt = tokio::runtime::Runtime::new()?; + tracing::trace!("THREAD: runtime created"); + + tracing::debug!("Starting to load installed packages"); + + tracing::trace!("THREAD: about to execute formulae"); + let formulae_result = + rt.block_on(async { use_case_formulae.execute(PackageType::Formula).await }); + + tracing::debug!( + "Formulae result: {:?}", + formulae_result + .as_ref() + .map(|p| p.len()) + .map_err(|e| e.to_string()) + ); + + tracing::trace!("THREAD: about to execute casks"); + let casks_result = + rt.block_on(async { use_case_casks.execute(PackageType::Cask).await }); + + tracing::debug!( + "Casks result: {:?}", + casks_result + .as_ref() + .map(|p| p.len()) + .map_err(|e| e.to_string()) + ); + + let mut packages = Vec::new(); + let mut logs = Vec::new(); + + match formulae_result { + Ok(pkgs) => { + let msg = format!("Loaded {} formulae", pkgs.len()); + logs.push(msg.clone()); + tracing::info!("{}", msg); + packages.extend(pkgs); + } + Err(e) => { + let msg = format!("Error loading formulae: {}", e); + logs.push(msg.clone()); + tracing::error!("{}", msg); + } } - } - match casks_result { - Ok(pkgs) => { - let msg = format!("Loaded {} casks", pkgs.len()); - logs.push(msg.clone()); - tracing::info!("{}", msg); - packages.extend(pkgs); + match casks_result { + Ok(pkgs) => { + let msg = format!("Loaded {} casks", pkgs.len()); + logs.push(msg.clone()); + tracing::info!("{}", msg); + packages.extend(pkgs); + } + Err(e) => { + let msg = format!("Error loading casks: {}", e); + logs.push(msg.clone()); + tracing::error!("{}", msg); + } } - Err(e) => { - let msg = format!("Error loading casks: {}", e); - logs.push(msg.clone()); - tracing::error!("{}", msg); + + tracing::debug!("About to write {} packages to mutex", packages.len()); + tracing::debug!("About to lock packages mutex"); + *installed_packages + .lock() + .map_err(|e| anyhow::anyhow!("Failed to lock packages: {}", e))? = packages; + tracing::debug!("Successfully locked packages, now adding finish log"); + + logs.push("Finished loading installed packages".to_string()); + tracing::info!("Finished loading installed packages"); + + tracing::debug!("About to lock logs mutex with {} log entries", logs.len()); + *output_log + .lock() + .map_err(|e| anyhow::anyhow!("Failed to lock logs: {}", e))? = logs; + tracing::debug!("Successfully updated mutexes"); + + Ok(()) + })() { + tracing::error!("Error in load_installed_packages thread: {}", e); + if let Ok(mut logs) = output_log.lock() { + logs.push(format!("Thread error: {}", e)); } } - - logs.push("Finished loading installed packages".to_string()); - tracing::info!("Finished loading installed packages"); - - *installed_packages.lock().unwrap() = packages; - *output_log.lock().unwrap() = logs; + tracing::trace!("THREAD ENDED: load_installed_packages"); }); } @@ -165,15 +207,16 @@ impl BrewstyApp { if self.loading_outdated { return; } - + self.loading_outdated = true; self.status_message = "Loading outdated packages...".to_string(); - self.log_manager.push("Loading outdated packages (formulae and casks)".to_string()); + self.log_manager + .push("Loading outdated packages (formulae and casks)".to_string()); tracing::info!("Loading outdated packages (formulae and casks)"); let use_case_formulae = Arc::clone(&self.use_cases.list_outdated); let use_case_casks = Arc::clone(&self.use_cases.list_outdated); - + let outdated_packages = Arc::new(Mutex::new(Vec::new())); let output_log = Arc::new(Mutex::new(Vec::new())); @@ -183,52 +226,93 @@ impl BrewstyApp { }); thread::spawn(move || { - let rt = tokio::runtime::Runtime::new().unwrap(); - - let formulae_result = rt.block_on(async { - use_case_formulae.execute(PackageType::Formula).await - }); - - let casks_result = rt.block_on(async { - use_case_casks.execute(PackageType::Cask).await - }); - - let mut packages = Vec::new(); - let mut logs = Vec::new(); - - match formulae_result { - Ok(pkgs) => { - let msg = format!("Loaded {} outdated formulae", pkgs.len()); - logs.push(msg.clone()); - tracing::info!("{}", msg); - packages.extend(pkgs); - } - Err(e) => { - let msg = format!("Error loading outdated formulae: {}", e); - logs.push(msg.clone()); - tracing::error!("{}", msg); + if let Err(e) = (|| -> Result<()> { + let rt = tokio::runtime::Runtime::new()?; + + tracing::debug!("Starting to load outdated packages"); + + let formulae_result = + rt.block_on(async { use_case_formulae.execute(PackageType::Formula).await }); + + tracing::debug!( + "Outdated formulae result: {:?}", + formulae_result + .as_ref() + .map(|p| p.len()) + .map_err(|e| e.to_string()) + ); + + let casks_result = + rt.block_on(async { use_case_casks.execute(PackageType::Cask).await }); + + tracing::debug!( + "Outdated casks result: {:?}", + casks_result + .as_ref() + .map(|p| p.len()) + .map_err(|e| e.to_string()) + ); + + let mut packages = Vec::new(); + let mut logs = Vec::new(); + + match formulae_result { + Ok(pkgs) => { + let msg = format!("Loaded {} outdated formulae", pkgs.len()); + logs.push(msg.clone()); + tracing::info!("{}", msg); + packages.extend(pkgs); + } + Err(e) => { + let msg = format!("Error loading outdated formulae: {}", e); + logs.push(msg.clone()); + tracing::error!("{}", msg); + } } - } - match casks_result { - Ok(pkgs) => { - let msg = format!("Loaded {} outdated casks", pkgs.len()); - logs.push(msg.clone()); - tracing::info!("{}", msg); - packages.extend(pkgs); + match casks_result { + Ok(pkgs) => { + let msg = format!("Loaded {} outdated casks", pkgs.len()); + logs.push(msg.clone()); + tracing::info!("{}", msg); + packages.extend(pkgs); + } + Err(e) => { + let msg = format!("Error loading outdated casks: {}", e); + logs.push(msg.clone()); + tracing::error!("{}", msg); + } } - Err(e) => { - let msg = format!("Error loading outdated casks: {}", e); - logs.push(msg.clone()); - tracing::error!("{}", msg); + + tracing::debug!( + "About to write {} outdated packages to mutex", + packages.len() + ); + tracing::debug!("About to lock outdated packages mutex"); + *outdated_packages + .lock() + .map_err(|e| anyhow::anyhow!("Failed to lock packages: {}", e))? = packages; + tracing::debug!("Successfully locked packages, now adding finish log"); + + logs.push("Finished loading outdated packages".to_string()); + tracing::info!("Finished loading outdated packages"); + + tracing::debug!( + "About to lock outdated logs mutex with {} log entries", + logs.len() + ); + *output_log + .lock() + .map_err(|e| anyhow::anyhow!("Failed to lock logs: {}", e))? = logs; + tracing::debug!("Successfully updated outdated mutexes"); + + Ok(()) + })() { + tracing::error!("Error in load_outdated_packages thread: {}", e); + if let Ok(mut logs) = output_log.lock() { + logs.push(format!("Thread error: {}", e)); } } - - logs.push("Finished loading outdated packages".to_string()); - tracing::info!("Finished loading outdated packages"); - - *outdated_packages.lock().unwrap() = packages; - *output_log.lock().unwrap() = logs; }); } @@ -236,14 +320,14 @@ impl BrewstyApp { if self.loading_install { return; } - + let package_name = package.name.clone(); self.loading_install = true; self.loading = true; self.current_install_package = Some(package_name.clone()); self.packages_in_operation.insert(package_name.clone()); self.status_message = format!("Installing {}...", package.name); - + let package_type = package.package_type.clone(); let initial_msg = format!("Installing package: {} ({:?})", package_name, package_type); self.log_manager.push(initial_msg.clone()); @@ -252,7 +336,7 @@ impl BrewstyApp { let success = Arc::new(Mutex::new(None)); let logs = Arc::new(Mutex::new(Vec::new())); let message = Arc::new(Mutex::new(String::new())); - + self.task_manager.set_active_task(AsyncTask::Install { success: Arc::clone(&success), logs: Arc::clone(&logs), @@ -261,11 +345,9 @@ impl BrewstyApp { let use_case = Arc::clone(&self.use_cases.install); let executor = self.executor.clone(); - + thread::spawn(move || { - let result = executor.execute(async move { - use_case.execute(package).await - }); + let result = executor.execute(async move { use_case.execute(package).await }); let mut log_vec = Vec::new(); match result { @@ -284,7 +366,7 @@ impl BrewstyApp { *message.lock().unwrap() = msg; } } - + *logs.lock().unwrap() = log_vec; }); } @@ -293,24 +375,26 @@ impl BrewstyApp { if self.loading_uninstall { return; } - + let package_name = package.name.clone(); self.loading_uninstall = true; self.loading = true; self.current_uninstall_package = Some(package_name.clone()); self.packages_in_operation.insert(package_name.clone()); self.status_message = format!("Uninstalling {}...", package.name); - - + let package_type = package.package_type.clone(); - let initial_msg = format!("Uninstalling package: {} ({:?})", package_name, package_type); + let initial_msg = format!( + "Uninstalling package: {} ({:?})", + package_name, package_type + ); self.log_manager.push(initial_msg.clone()); tracing::info!("{}", initial_msg); let success = Arc::new(Mutex::new(None)); let logs = Arc::new(Mutex::new(Vec::new())); let message = Arc::new(Mutex::new(String::new())); - + self.task_manager.set_active_task(AsyncTask::Uninstall { success: Arc::clone(&success), logs: Arc::clone(&logs), @@ -319,11 +403,9 @@ impl BrewstyApp { let use_case = Arc::clone(&self.use_cases.uninstall); let executor = self.executor.clone(); - + thread::spawn(move || { - let result = executor.execute(async move { - use_case.execute(package).await - }); + let result = executor.execute(async move { use_case.execute(package).await }); let mut log_vec = Vec::new(); match result { @@ -342,7 +424,7 @@ impl BrewstyApp { *message.lock().unwrap() = msg; } } - + *logs.lock().unwrap() = log_vec; }); } @@ -351,14 +433,14 @@ impl BrewstyApp { if self.loading_update { return; } - + let package_name = package.name.clone(); self.loading_update = true; self.loading = true; self.current_update_package = Some(package_name.clone()); self.packages_in_operation.insert(package_name.clone()); self.status_message = format!("Updating {}...", package.name); - + let package_type = package.package_type.clone(); let initial_msg = format!("Updating package: {} ({:?})", package_name, package_type); self.log_manager.push(initial_msg.clone()); @@ -367,7 +449,7 @@ impl BrewstyApp { let success = Arc::new(Mutex::new(None)); let logs = Arc::new(Mutex::new(Vec::new())); let message = Arc::new(Mutex::new(String::new())); - + self.task_manager.set_active_task(AsyncTask::Update { success: Arc::clone(&success), logs: Arc::clone(&logs), @@ -376,11 +458,9 @@ impl BrewstyApp { let use_case = Arc::clone(&self.use_cases.update); let executor = self.executor.clone(); - + thread::spawn(move || { - let result = executor.execute(async move { - use_case.execute(package).await - }); + let result = executor.execute(async move { use_case.execute(package).await }); let mut log_vec = Vec::new(); match result { @@ -399,7 +479,7 @@ impl BrewstyApp { *message.lock().unwrap() = msg; } } - + *logs.lock().unwrap() = log_vec; }); } @@ -408,7 +488,7 @@ impl BrewstyApp { self.loading = true; self.packages_in_operation.insert(package.name.clone()); self.status_message = format!("Pinning {}...", package.name); - + let package_name = package.name.clone(); let package_type = package.package_type.clone(); let initial_msg = format!("Pinning package: {} ({:?})", package_name, package_type); @@ -418,7 +498,7 @@ impl BrewstyApp { let success = Arc::new(Mutex::new(None)); let logs = Arc::new(Mutex::new(Vec::new())); let message = Arc::new(Mutex::new(String::new())); - + self.task_manager.set_active_task(AsyncTask::Pin { package_name: package.name.clone(), success: Arc::clone(&success), @@ -451,7 +531,7 @@ impl BrewstyApp { self.loading = true; self.packages_in_operation.insert(package.name.clone()); self.status_message = format!("Unpinning {}...", package.name); - + let package_name = package.name.clone(); let package_type = package.package_type.clone(); let initial_msg = format!("Unpinning package: {} ({:?})", package_name, package_type); @@ -461,7 +541,7 @@ impl BrewstyApp { let success = Arc::new(Mutex::new(None)); let logs = Arc::new(Mutex::new(Vec::new())); let message = Arc::new(Mutex::new(String::new())); - + self.task_manager.set_active_task(AsyncTask::Unpin { package_name: package.name.clone(), success: Arc::clone(&success), @@ -469,32 +549,32 @@ impl BrewstyApp { message: Arc::clone(&message), }); - let use_case = Arc::clone(&self.use_cases.unpin); - let package_clone = package.clone(); - - self.executor.execute(async move { - match use_case.execute(package_clone).await { - Ok(_) => { - let msg = format!("Successfully unpinned {}", package_name); - *logs.lock().unwrap() = vec![msg.clone()]; - *success.lock().unwrap() = Some(true); - *message.lock().unwrap() = format!("{} unpinned successfully", package_name); - } - Err(e) => { - let msg = format!("Error unpinning {}: {}", package_name, e); - *logs.lock().unwrap() = vec![msg.clone()]; - *success.lock().unwrap() = Some(false); - *message.lock().unwrap() = msg; - } - } - }); + let use_case = Arc::clone(&self.use_cases.unpin); + let package_clone = package.clone(); + + self.executor.execute(async move { + match use_case.execute(package_clone).await { + Ok(_) => { + let msg = format!("Successfully unpinned {}", package_name); + *logs.lock().unwrap() = vec![msg.clone()]; + *success.lock().unwrap() = Some(true); + *message.lock().unwrap() = format!("{} unpinned successfully", package_name); + } + Err(e) => { + let msg = format!("Error unpinning {}: {}", package_name, e); + *logs.lock().unwrap() = vec![msg.clone()]; + *success.lock().unwrap() = Some(false); + *message.lock().unwrap() = msg; + } + } + }); } fn handle_update_all(&mut self) { if self.loading_update_all { return; } - + self.loading_update_all = true; self.loading = true; self.status_message = "Updating all packages...".to_string(); @@ -504,7 +584,7 @@ impl BrewstyApp { let success = Arc::new(Mutex::new(None)); let logs = Arc::new(Mutex::new(Vec::new())); let message = Arc::new(Mutex::new(String::new())); - + self.task_manager.set_active_task(AsyncTask::UpdateAll { success: Arc::clone(&success), logs: Arc::clone(&logs), @@ -513,11 +593,9 @@ impl BrewstyApp { let use_case = Arc::clone(&self.use_cases.update_all); let executor = self.executor.clone(); - + thread::spawn(move || { - let result = executor.execute(async move { - use_case.execute().await - }); + let result = executor.execute(async move { use_case.execute().await }); let mut log_vec = Vec::new(); match result { @@ -536,7 +614,7 @@ impl BrewstyApp { *message.lock().unwrap() = msg; } } - + *logs.lock().unwrap() = log_vec; }); } @@ -559,9 +637,11 @@ impl BrewstyApp { match preview_result { Ok(preview) => { - let msg = format!("Found {} items to clean ({})", - preview.items.len(), - format_size(preview.total_size)); + let msg = format!( + "Found {} items to clean ({})", + preview.items.len(), + format_size(preview.total_size) + ); self.log_manager.push(msg); self.cleanup_modal.show_preview(cleanup_type, preview); } @@ -579,7 +659,7 @@ impl BrewstyApp { if self.loading_clean_cache { return; } - + self.loading_clean_cache = true; self.loading = true; self.status_message = "Cleaning cache...".to_string(); @@ -589,7 +669,7 @@ impl BrewstyApp { let success = Arc::new(Mutex::new(None)); let logs = Arc::new(Mutex::new(Vec::new())); let message = Arc::new(Mutex::new(String::new())); - + self.task_manager.set_active_task(AsyncTask::CleanCache { success: Arc::clone(&success), logs: Arc::clone(&logs), @@ -598,11 +678,9 @@ impl BrewstyApp { let use_case = Arc::clone(&self.use_cases.clean_cache); let executor = self.executor.clone(); - + thread::spawn(move || { - let result = executor.execute(async move { - use_case.execute().await - }); + let result = executor.execute(async move { use_case.execute().await }); let mut log_vec = Vec::new(); match result { @@ -621,7 +699,7 @@ impl BrewstyApp { *message.lock().unwrap() = msg; } } - + *logs.lock().unwrap() = log_vec; }); } @@ -630,30 +708,30 @@ impl BrewstyApp { if self.loading_cleanup_old_versions { return; } - + self.loading_cleanup_old_versions = true; self.loading = true; self.status_message = "Cleaning up old versions...".to_string(); - self.log_manager.push("Cleaning up old versions".to_string()); + self.log_manager + .push("Cleaning up old versions".to_string()); tracing::info!("Cleaning up old versions"); let success = Arc::new(Mutex::new(None)); let logs = Arc::new(Mutex::new(Vec::new())); let message = Arc::new(Mutex::new(String::new())); - - self.task_manager.set_active_task(AsyncTask::CleanupOldVersions { - success: Arc::clone(&success), - logs: Arc::clone(&logs), - message: Arc::clone(&message), - }); + + self.task_manager + .set_active_task(AsyncTask::CleanupOldVersions { + success: Arc::clone(&success), + logs: Arc::clone(&logs), + message: Arc::clone(&message), + }); let use_case = Arc::clone(&self.use_cases.cleanup_old_versions); let executor = self.executor.clone(); - + thread::spawn(move || { - let result = executor.execute(async move { - use_case.execute().await - }); + let result = executor.execute(async move { use_case.execute().await }); let mut log_vec = Vec::new(); match result { @@ -672,7 +750,7 @@ impl BrewstyApp { *message.lock().unwrap() = msg; } } - + *logs.lock().unwrap() = log_vec; }); } @@ -681,7 +759,7 @@ impl BrewstyApp { if self.filter_state.search_query().is_empty() { return; } - + if self.loading_search { return; } @@ -695,7 +773,7 @@ impl BrewstyApp { let use_case_formulae = Arc::clone(&self.use_cases.search); let use_case_casks = Arc::clone(&self.use_cases.search); let query = self.filter_state.search_query().to_string(); - + let search_results = Arc::new(Mutex::new(Vec::new())); let output_log = Arc::new(Mutex::new(Vec::new())); let query_clone = query.clone(); @@ -707,13 +785,17 @@ impl BrewstyApp { thread::spawn(move || { let rt = tokio::runtime::Runtime::new().unwrap(); - + let formulae_result = rt.block_on(async { - use_case_formulae.execute(&query, PackageType::Formula).await + use_case_formulae + .execute(&query, PackageType::Formula) + .await }); let casks_result = rt.block_on(async { - use_case_casks.execute(&query_clone, PackageType::Cask).await + use_case_casks + .execute(&query_clone, PackageType::Cask) + .await }); let mut results = Vec::new(); @@ -751,50 +833,59 @@ impl BrewstyApp { *output_log.lock().unwrap() = logs; }); } - + fn load_package_info(&mut self, package_name: String, package_type: PackageType) { if self.task_manager.can_load_more_package_info() { self.load_package_info_immediate(package_name, package_type); } else { - self.task_manager.queue_package_info_load(package_name, package_type); + self.task_manager + .queue_package_info_load(package_name, package_type); } } - + fn load_package_info_immediate(&mut self, package_name: String, package_type: PackageType) { if self.task_manager.is_loading_package_info(&package_name) { tracing::debug!("Already loading info for {}, skipping", package_name); return; } - - tracing::info!("Starting to load package info for {} ({:?})", package_name, package_type); - + + tracing::info!( + "Starting to load package info for {} ({:?})", + package_name, + package_type + ); + let use_case = Arc::clone(&self.use_cases.get_package_info); let result = Arc::new(Mutex::new(None)); let name_clone = package_name.clone(); let package_type_clone = package_type.clone(); let package_type_clone2 = package_type.clone(); - + let task = AsyncTask::LoadPackageInfo { package_name: package_name.clone(), package_type: package_type.clone(), result: Arc::clone(&result), started_at: std::time::Instant::now(), }; - - self.task_manager.add_package_info_task(package_name.clone(), task); - + + self.task_manager + .add_package_info_task(package_name.clone(), task); + thread::spawn(move || { let rt = tokio::runtime::Runtime::new().unwrap(); - + tracing::debug!("Spawned thread for loading {}", name_clone); - - let info_result = rt.block_on(async { - use_case.execute(&name_clone, package_type_clone).await - }); - + + let info_result = + rt.block_on(async { use_case.execute(&name_clone, package_type_clone).await }); + match info_result { Ok(package) => { - tracing::info!("Successfully loaded package info for {}: version={:?}", name_clone, package.version); + tracing::info!( + "Successfully loaded package info for {}: version={:?}", + name_clone, + package.version + ); *result.lock().unwrap() = Some(package); } Err(e) => { @@ -806,11 +897,13 @@ impl BrewstyApp { } }); } - + fn poll_async_tasks(&mut self) { + tracing::trace!("poll_async_tasks called, checking for active task"); let result = self.task_manager.poll(); if let Some(packages) = result.installed_packages { + tracing::info!("Got {} installed packages from poll", packages.len()); self.installed_packages.update_packages(packages); self.tab_manager.mark_loaded(Tab::Installed); self.loading_installed = false; @@ -818,6 +911,7 @@ impl BrewstyApp { } if let Some(packages) = result.outdated_packages { + tracing::info!("Got {} outdated packages from poll", packages.len()); self.outdated_packages.update_packages(packages); self.tab_manager.mark_loaded(Tab::Outdated); self.loading_outdated = false; @@ -852,11 +946,11 @@ impl BrewstyApp { self.packages_in_operation.remove(pkg); } self.status_message = message; - + if success { self.tab_manager.mark_unloaded(Tab::Installed); self.load_installed_packages(); - + if let Some(pkg_name) = installed_pkg_name { if let Some(mut pkg) = self.search_results.get_package(&pkg_name) { pkg.installed = true; @@ -873,7 +967,7 @@ impl BrewstyApp { self.packages_in_operation.remove(&pkg); } self.status_message = message; - + if success { self.tab_manager.mark_unloaded(Tab::Installed); self.load_installed_packages(); @@ -887,7 +981,7 @@ impl BrewstyApp { self.packages_in_operation.remove(&pkg); } self.status_message = message; - + if success { self.tab_manager.mark_unloaded(Tab::Installed); self.tab_manager.mark_unloaded(Tab::Outdated); @@ -900,7 +994,7 @@ impl BrewstyApp { self.loading_update_all = false; self.loading = false; self.status_message = message; - + if success { self.tab_manager.mark_unloaded(Tab::Installed); self.tab_manager.mark_unloaded(Tab::Outdated); @@ -937,21 +1031,26 @@ impl BrewstyApp { self.log_manager.extend(result.logs); - if self.task_manager.can_load_more_package_info() && self.task_manager.pending_loads_count() > 0 { + if self.task_manager.can_load_more_package_info() + && self.task_manager.pending_loads_count() > 0 + { let to_load = 15 - self.task_manager.pending_loads_count(); let batch = self.task_manager.drain_pending_loads(to_load); - + if !batch.is_empty() { - tracing::info!("Starting batch load of {} packages ({} remaining in queue)", - batch.len(), self.task_manager.pending_loads_count()); - + tracing::info!( + "Starting batch load of {} packages ({} remaining in queue)", + batch.len(), + self.task_manager.pending_loads_count() + ); + for (name, pkg_type) in batch { self.load_package_info_immediate(name, pkg_type); } } } } - + fn show_loader(&self, ui: &mut egui::Ui, message: &str) { ui.vertical_centered(|ui| { ui.add_space(100.0); @@ -959,7 +1058,7 @@ impl BrewstyApp { ui.label(message); }); } - + fn poll_logs(&mut self) { while let Ok(log_entry) = self.log_rx.try_recv() { self.log_manager.push(log_entry); @@ -988,7 +1087,7 @@ impl eframe::App for BrewstyApp { self.poll_logs(); self.poll_async_tasks(); ctx.request_repaint(); - + if !self.initialized { self.initialized = true; self.load_installed_packages(); @@ -999,26 +1098,44 @@ impl eframe::App for BrewstyApp { ui.heading("🍺 Brewsty"); ui.label(format!("v{}", env!("CARGO_PKG_VERSION"))); ui.separator(); - - if ui.selectable_label(self.tab_manager.is_current(Tab::Installed), "Installed").clicked() { + + if ui + .selectable_label(self.tab_manager.is_current(Tab::Installed), "Installed") + .clicked() + { self.tab_manager.switch_to(Tab::Installed); if !self.tab_manager.is_loaded(Tab::Installed) { self.load_installed_packages(); } } - if ui.selectable_label(self.tab_manager.is_current(Tab::Outdated), "Outdated").clicked() { + if ui + .selectable_label(self.tab_manager.is_current(Tab::Outdated), "Outdated") + .clicked() + { self.tab_manager.switch_to(Tab::Outdated); if !self.tab_manager.is_loaded(Tab::Outdated) { self.load_outdated_packages(); } } - if ui.selectable_label(self.tab_manager.is_current(Tab::SearchInstall), "Search & Install").clicked() { + if ui + .selectable_label( + self.tab_manager.is_current(Tab::SearchInstall), + "Search & Install", + ) + .clicked() + { self.tab_manager.switch_to(Tab::SearchInstall); } - if ui.selectable_label(self.tab_manager.is_current(Tab::Settings), "Settings").clicked() { + if ui + .selectable_label(self.tab_manager.is_current(Tab::Settings), "Settings") + .clicked() + { self.tab_manager.switch_to(Tab::Settings); } - if ui.selectable_label(self.tab_manager.is_current(Tab::Log), "Log").clicked() { + if ui + .selectable_label(self.tab_manager.is_current(Tab::Log), "Log") + .clicked() + { self.tab_manager.switch_to(Tab::Log); } }); @@ -1030,29 +1147,43 @@ impl eframe::App for BrewstyApp { .show(ctx, |ui| { ui.add_space(8.0); ui.horizontal(|ui| { - ui.add_space(ui.available_width() / 2.0 - 40.0); if ui.button("Clear Output").clicked() { self.log_manager = LogManager::new(); } + ui.separator(); + if ui.button("📋 Copy Output").clicked() { + let output = self + .log_manager + .all_logs() + .map(|entry| { + format!("[{}] {}", entry.format_timestamp(), entry.message) + }) + .collect::>() + .join("\n"); + ctx.copy_text(output); + } }); - + ui.separator(); - + egui::ScrollArea::vertical() .auto_shrink([false; 2]) .stick_to_bottom(true) .show(ui, |ui| { ui.set_width(ui.available_width()); - - for entry in self.log_manager.all_logs() { + + for entry in self.log_manager.filtered_logs() { ui.horizontal(|ui| { - ui.label(egui::RichText::new(format!("[{}]", entry.format_timestamp())) - .color(egui::Color32::GRAY).monospace()); + ui.label( + egui::RichText::new(format!("[{}]", entry.format_timestamp())) + .color(egui::Color32::GRAY) + .monospace(), + ); ui.monospace(&entry.message); }); } }); - + self.output_panel_height = ui.min_rect().height(); }); @@ -1189,7 +1320,8 @@ impl eframe::App for BrewstyApp { Tab::SearchInstall => { ui.horizontal(|ui| { ui.label("Search:"); - let response = ui.text_edit_singleline(self.filter_state.search_query_mut()); + let response = + ui.text_edit_singleline(self.filter_state.search_query_mut()); if response.lost_focus() && ui.input(|i| i.key_pressed(egui::Key::Enter)) { self.handle_search(); } @@ -1263,6 +1395,32 @@ impl eframe::App for BrewstyApp { ui.heading("Settings & Maintenance"); ui.separator(); + ui.group(|ui| { + ui.heading("Log Levels"); + ui.horizontal(|ui| { + let mut debug = self.log_manager.is_level_visible(LogLevel::Debug); + let mut info = self.log_manager.is_level_visible(LogLevel::Info); + let mut warn = self.log_manager.is_level_visible(LogLevel::Warn); + let mut error = self.log_manager.is_level_visible(LogLevel::Error); + + if ui.checkbox(&mut debug, "Debug").changed() { + self.log_manager.set_level_visible(LogLevel::Debug, debug); + } + if ui.checkbox(&mut info, "Info").changed() { + self.log_manager.set_level_visible(LogLevel::Info, info); + } + if ui.checkbox(&mut warn, "Warn").changed() { + self.log_manager.set_level_visible(LogLevel::Warn, warn); + } + if ui.checkbox(&mut error, "Error").changed() { + self.log_manager.set_level_visible(LogLevel::Error, error); + } + }); + }); + + ui.separator(); + ui.heading("Maintenance"); + ui.vertical_centered(|ui| { if ui.button("Clean Cache").clicked() { self.show_cleanup_preview(CleanupType::Cache); @@ -1288,32 +1446,66 @@ impl eframe::App for BrewstyApp { Tab::Log => { ui.heading("Command Log"); ui.separator(); - + + ui.horizontal(|ui| { + if ui.button("📋 Copy All").clicked() { + let output = self + .log_manager + .all_logs() + .map(|entry| { + format!("[{}] {}", entry.format_timestamp(), entry.message) + }) + .collect::>() + .join("\n"); + ctx.copy_text(output); + } + if ui.button("🗑 Clear").clicked() { + self.log_manager = LogManager::new(); + } + }); + + ui.separator(); + egui::ScrollArea::vertical() .auto_shrink([false; 2]) - .stick_to_bottom(true) .show(ui, |ui| { - ui.set_width(ui.available_width()); - - for entry in self.log_manager.all_logs() { - ui.horizontal(|ui| { - ui.label(egui::RichText::new(format!("[{}]", entry.format_timestamp())) - .color(egui::Color32::GRAY).monospace()); - ui.monospace(&entry.message); + ui.visuals_mut().override_text_color = + Some(egui::Color32::from_rgb(0, 255, 0)); + let bg_frame = egui::Frame::default() + .fill(egui::Color32::BLACK) + .inner_margin(8.0); + bg_frame.show(ui, |ui| { + ui.set_width(ui.available_width()); + ui.set_style({ + let mut style = (*ui.ctx().style()).clone(); + style.override_font_id = Some(egui::FontId::monospace(12.0)); + style }); - } + + for entry in self.log_manager.filtered_logs_reversed() { + ui.horizontal(|ui| { + ui.label( + egui::RichText::new(format!( + "[{}]", + entry.format_timestamp() + )) + .color(egui::Color32::GRAY) + .monospace(), + ); + ui.monospace(&entry.message); + }); + } + }); }); } } if let Some(action) = self.cleanup_modal.render(ctx) { match action { - CleanupAction::Confirm(cleanup_type) => { - match cleanup_type { - CleanupType::Cache => self.handle_clean_cache(), - CleanupType::OldVersions => self.handle_cleanup_old_versions(), - } - } + CleanupAction::Confirm(cleanup_type) => match cleanup_type { + CleanupType::Cache => self.handle_clean_cache(), + CleanupType::OldVersions => self.handle_cleanup_old_versions(), + }, CleanupAction::Cancel => { self.cleanup_modal.close(); }