mirror of
https://github.com/Valkyrie00/bold-brew.git
synced 2026-03-14 14:25:53 +01:00
* refactor(services): split brew.go into focused modules Reorganize the services package for better maintainability: - brew.go: Interface and struct definitions - data.go: Data loading, caching, and tap package management - cache.go: Low-level cache I/O helpers - packages.go: Package retrieval (GetFormulae, GetPackages) - operations.go: Package operations (install, update, remove) - parser.go: Brewfile parsing - brewfile.go: Brewfile-specific app logic (extracted from app.go) This reduces the largest file from 1124 to 322 lines and improves separation of concerns across the codebase. * refactor(services): introduce DataProvider pattern for data loading Extract data loading logic into a dedicated DataProvider service that handles all data retrieval operations (formulae, casks, analytics, cache). BrewService now delegates to DataProvider via interface for better testability and separation of concerns. Also centralizes cache file name constants in dataprovider.go for maintainability. * refactor(services): centralize data in DataProvider and improve naming consistency Move all data-related fields and retrieval methods from BrewService to DataProvider, making BrewService focused solely on brew operations. Rename formula fields (all, installed, remote, analytics) to be consistent with cask naming convention (allFormulae, installedFormulae, etc.). Remove redundant data.go and packages.go files. * refactor(services): consolidate code and fix shared dependencies - IOService now receives BrewService as parameter instead of creating new instance - Move API URL constants from brew.go to dataprovider.go (now private) - Move getCacheDir to cache.go where it belongs - Remove unused GetPrefixPath from BrewService interface - Consolidate Brewfile parsing into brewfile.go as standalone function - Delete parser.go (logic merged into brewfile.go) * fix(brewfile): prevent duplicate packages in list after refresh Add duplicate detection checks in loadBrewfilePackages to ensure each package appears only once in the Brewfile package list, even when refreshing after install/remove operations. * refactor(services): extract search methods to dedicated file Move search(), setResults(), and forceRefreshResults() from app.go to search.go for better code organization and separation of concerns. * refactor(services): remove dead code and unify helpers Remove unused methods and fields: - GetFormulae(), IsPackageInstalled() from DataProvider - InstallAllPackages(), RemoveAllPackages() from BrewService - allFormulae, allCasks fields from DataProvider Unify GetInstalledCaskNames and GetInstalledFormulaNames with a common getInstalledNames helper to reduce code duplication. * docs(brewfile): add package documentation with execution sequence Document Brewfile mode functionality including parsing, tap installation, and package loading. Add execution sequence diagram and note that methods are only active in Brewfile mode (bbrew -f <file>). * refactor(services): move fetchFromAPI to dataprovider Move HTTP fetch function from cache.go to dataprovider.go where it belongs semantically. cache.go now contains only cache I/O operations. * refactor(dataprovider): improve method naming consistency Rename Load* methods to Get* with forceRefresh parameter for clarity: - LoadInstalledFormulae → GetInstalledFormulae - LoadRemoteFormulae → GetRemoteFormulae - LoadTapPackages → GetTapPackages (and similar) Rename Get* methods that execute commands to Fetch* for accuracy: - GetInstalledCaskNames → FetchInstalledCaskNames - GetInstalledFormulaNames → FetchInstalledFormulaNames Replace forceDownload parameter with forceRefresh throughout. * refactor(input): rename io.go to input.go and simplify code Rename IOService/IOAction to InputService/InputAction for semantic correctness (io in Go refers to file I/O, not keyboard input). Simplify handleFilterEvent with helper methods: isFilterActive(), setFilterActive(), updateFilterUI() reducing ~30 lines of repetitive code. Extract handleBatchPackageOperation() helper for InstallAll/RemoveAll operations, reducing ~90 lines of duplicate code with a configurable batchOperation struct. * refactor(filters): replace 4 booleans with FilterType enum Replace showOnlyInstalled, showOnlyOutdated, showOnlyLeaves, showOnlyCasks with a single activeFilter field of type FilterType. Add FilterNone constant for no active filter state. Simplify handleFilterEvent to simple toggle logic. Extract applyFilter method with clean switch statement. Remove redundant isFilterActive and setFilterActive helpers. * refactor(brew): merge operations.go into brew.go Consolidate all BrewService methods into a single file. Add section comments to organize code into logical groups: - Core info (GetBrewVersion) - Package operations (Update/Install/Remove) - Tap support (InstallTap/IsTapInstalled) - Internal helpers (executeCommand) * fix(dataprovider): suppress gosec G107 false positive The URL passed to http.Get comes from internal constants (Homebrew API URLs), not from user input.
211 lines
6.8 KiB
Go
211 lines
6.8 KiB
Go
package services
|
|
|
|
import (
|
|
"bbrew/internal/models"
|
|
"bbrew/internal/ui"
|
|
"bbrew/internal/ui/theme"
|
|
"context"
|
|
"fmt"
|
|
"os"
|
|
"time"
|
|
|
|
"github.com/gdamore/tcell/v2"
|
|
"github.com/rivo/tview"
|
|
)
|
|
|
|
var (
|
|
AppName = "Bold Brew"
|
|
AppVersion = "0.0.1"
|
|
)
|
|
|
|
type AppServiceInterface interface {
|
|
GetApp() *tview.Application
|
|
GetLayout() ui.LayoutInterface
|
|
Boot() (err error)
|
|
BuildApp()
|
|
SetBrewfilePath(path string)
|
|
IsBrewfileMode() bool
|
|
GetBrewfilePackages() *[]models.Package
|
|
}
|
|
|
|
// AppService manages the application state, Homebrew integration, and UI components.
|
|
type AppService struct {
|
|
app *tview.Application
|
|
theme *theme.Theme
|
|
layout ui.LayoutInterface
|
|
|
|
packages *[]models.Package
|
|
filteredPackages *[]models.Package
|
|
activeFilter FilterType
|
|
brewVersion string
|
|
|
|
// Brewfile support
|
|
brewfilePath string
|
|
brewfilePackages *[]models.Package
|
|
brewfileTaps []string // Taps required by the Brewfile
|
|
|
|
brewService BrewServiceInterface
|
|
dataProvider DataProviderInterface // Direct access for Brewfile operations
|
|
selfUpdateService SelfUpdateServiceInterface
|
|
inputService InputServiceInterface
|
|
}
|
|
|
|
// NewAppService creates a new instance of AppService with initialized components.
|
|
var NewAppService = func() AppServiceInterface {
|
|
app := tview.NewApplication()
|
|
themeService := theme.NewTheme()
|
|
layout := ui.NewLayout(themeService)
|
|
|
|
s := &AppService{
|
|
app: app,
|
|
theme: themeService,
|
|
layout: layout,
|
|
|
|
packages: new([]models.Package),
|
|
filteredPackages: new([]models.Package),
|
|
activeFilter: FilterNone,
|
|
brewVersion: "-",
|
|
|
|
brewfilePath: "",
|
|
brewfilePackages: new([]models.Package),
|
|
}
|
|
|
|
// Initialize services
|
|
s.dataProvider = NewDataProvider()
|
|
s.brewService = NewBrewService()
|
|
s.inputService = NewInputService(s, s.brewService)
|
|
s.selfUpdateService = NewSelfUpdateService()
|
|
|
|
return s
|
|
}
|
|
|
|
func (s *AppService) GetApp() *tview.Application { return s.app }
|
|
func (s *AppService) GetLayout() ui.LayoutInterface { return s.layout }
|
|
func (s *AppService) SetBrewfilePath(path string) { s.brewfilePath = path }
|
|
func (s *AppService) IsBrewfileMode() bool { return s.brewfilePath != "" }
|
|
func (s *AppService) GetBrewfilePackages() *[]models.Package { return s.brewfilePackages }
|
|
|
|
// Boot initializes the application by setting up Homebrew and loading formulae data.
|
|
func (s *AppService) Boot() (err error) {
|
|
if s.brewVersion, err = s.brewService.GetBrewVersion(); err != nil {
|
|
// This error is critical, as we need Homebrew to function
|
|
return fmt.Errorf("failed to get Homebrew version: %v", err)
|
|
}
|
|
|
|
// Load Homebrew data from cache for fast startup
|
|
// Installation status might be stale but will be refreshed in background by updateHomeBrew()
|
|
if err = s.dataProvider.SetupData(false); err != nil {
|
|
// Log error but don't fail - app can work with empty/partial data
|
|
fmt.Fprintf(os.Stderr, "Warning: failed to load Homebrew data (will retry in background): %v\n", err)
|
|
}
|
|
|
|
// Initialize packages and filteredPackages
|
|
s.packages = s.dataProvider.GetPackages()
|
|
*s.filteredPackages = *s.packages
|
|
|
|
// If Brewfile is specified, parse it and filter packages
|
|
if s.IsBrewfileMode() {
|
|
if err = s.loadBrewfilePackages(); err != nil {
|
|
return fmt.Errorf("failed to load Brewfile: %v", err)
|
|
}
|
|
}
|
|
|
|
return nil
|
|
}
|
|
|
|
// updateHomeBrew updates the Homebrew formulae and refreshes the results in the UI.
|
|
func (s *AppService) updateHomeBrew() {
|
|
s.app.QueueUpdateDraw(func() {
|
|
s.layout.GetNotifier().ShowWarning("Updating Homebrew formulae...")
|
|
})
|
|
if err := s.brewService.UpdateHomebrew(); err != nil {
|
|
s.app.QueueUpdateDraw(func() {
|
|
s.layout.GetNotifier().ShowError("Could not update Homebrew formulae")
|
|
})
|
|
return
|
|
}
|
|
// Clear loading message and update results
|
|
s.app.QueueUpdateDraw(func() {
|
|
s.layout.GetNotifier().ShowSuccess("Homebrew formulae updated successfully")
|
|
})
|
|
s.forceRefreshResults()
|
|
}
|
|
|
|
// BuildApp builds the application layout, sets up event handlers, and initializes the UI components.
|
|
func (s *AppService) BuildApp() {
|
|
// Build the layout
|
|
s.layout.Setup()
|
|
|
|
// Update header and enable Brewfile mode features if needed
|
|
headerName := AppName
|
|
if s.IsBrewfileMode() {
|
|
headerName = fmt.Sprintf("%s [Brewfile Mode]", AppName)
|
|
s.layout.GetSearch().Field().SetLabel("Search (Brewfile): ")
|
|
s.inputService.EnableBrewfileMode() // Add Install All action
|
|
}
|
|
s.layout.GetHeader().Update(headerName, AppVersion, s.brewVersion)
|
|
|
|
// Evaluate if there is a new version available
|
|
// This is done in a goroutine to avoid blocking the UI during startup
|
|
// In the future, this could be replaced with a more sophisticated update check, and update
|
|
// the user if a new version is available instantly instead of waiting for the next app start
|
|
go func() {
|
|
ctx, cancel := context.WithTimeout(context.Background(), 60*time.Second)
|
|
defer cancel()
|
|
|
|
if latestVersion, err := s.selfUpdateService.CheckForUpdates(ctx); err == nil && latestVersion != AppVersion {
|
|
s.app.QueueUpdateDraw(func() {
|
|
AppVersion = fmt.Sprintf("%s ([orange]New Version Available: %s[-])", AppVersion, latestVersion)
|
|
headerName := AppName
|
|
if s.IsBrewfileMode() {
|
|
headerName = fmt.Sprintf("%s [Brewfile Mode]", AppName)
|
|
}
|
|
s.layout.GetHeader().Update(headerName, AppVersion, s.brewVersion)
|
|
})
|
|
}
|
|
}()
|
|
|
|
// Table handler to update the details view when a table row is selected
|
|
tableSelectionChangedFunc := func(row, _ int) {
|
|
if row > 0 && row-1 < len(*s.filteredPackages) {
|
|
s.layout.GetDetails().SetContent(&(*s.filteredPackages)[row-1])
|
|
}
|
|
}
|
|
s.layout.GetTable().View().SetSelectionChangedFunc(tableSelectionChangedFunc)
|
|
|
|
// Search input handlers
|
|
inputDoneFunc := func(key tcell.Key) {
|
|
if key == tcell.KeyEnter || key == tcell.KeyEscape {
|
|
s.app.SetFocus(s.layout.GetTable().View()) // Set focus back to the table on Enter or Escape
|
|
}
|
|
}
|
|
changedFunc := func(text string) { // Each time the search input changes
|
|
s.search(text, true) // Perform search and scroll to top
|
|
}
|
|
s.layout.GetSearch().SetHandlers(inputDoneFunc, changedFunc)
|
|
|
|
// Add key event handler
|
|
s.app.SetInputCapture(s.inputService.HandleKeyEventInput)
|
|
|
|
// Set the root of the application to the layout's root and focus on the table view
|
|
s.app.SetRoot(s.layout.Root(), true)
|
|
s.app.SetFocus(s.layout.GetTable().View())
|
|
|
|
// Start background tasks: install taps first (if Brewfile mode), then update Homebrew
|
|
go func() {
|
|
// In Brewfile mode, install missing taps first
|
|
if s.IsBrewfileMode() && len(s.brewfileTaps) > 0 {
|
|
s.installBrewfileTapsAtStartup()
|
|
}
|
|
// Then update Homebrew (which will reload all data including new taps)
|
|
s.updateHomeBrew()
|
|
}()
|
|
|
|
// Set initial results based on mode
|
|
if s.IsBrewfileMode() {
|
|
*s.filteredPackages = *s.brewfilePackages // Sync filteredPackages
|
|
s.setResults(s.brewfilePackages, true) // Show only Brewfile packages
|
|
} else {
|
|
s.setResults(s.packages, true) // Show all packages
|
|
}
|
|
}
|