bold-brew/internal/services/app.go
2025-02-16 00:59:17 +01:00

258 lines
7.6 KiB
Go

package services
import (
"bbrew/internal/models"
"fmt"
"github.com/gdamore/tcell/v2"
"github.com/rivo/tview"
"strings"
)
var (
AppName = "Bold Brew"
AppVersion = "0.0.1"
)
type AppServiceInterface interface {
GetApp() *tview.Application
Boot() (err error)
BuildApp()
}
type AppService struct {
app *tview.Application
// Data
packages *[]models.Formula
filteredPackages *[]models.Formula
showOnlyInstalled bool
brewVersion string
// Services
BrewService BrewServiceInterface
CommandService CommandServiceInterface
SelfUpdateService SelfUpdateServiceInterface
LayoutService LayoutServiceInterface
}
var NewAppService = func() AppServiceInterface {
return &AppService{
app: tview.NewApplication(), // Initialize the application
packages: new([]models.Formula),
filteredPackages: new([]models.Formula),
showOnlyInstalled: false, // Default to show all packages
brewVersion: "-",
// Services
BrewService: NewBrewService(),
CommandService: NewCommandService(),
SelfUpdateService: NewSelfUpdateService(),
LayoutService: NewLayoutService(),
}
}
func (s *AppService) GetApp() *tview.Application {
return s.app
}
func (s *AppService) Boot() (err error) {
if err = s.BrewService.LoadAllFormulae(); err != nil {
return fmt.Errorf("failed to load Homebrew formulae: %v", err)
}
s.packages = s.BrewService.GetAllFormulae()
*s.filteredPackages = *s.packages
if s.brewVersion, err = s.BrewService.GetCurrentBrewVersion(); err != nil {
return fmt.Errorf("failed to get Homebrew version: %v", err)
}
return nil
}
func (s *AppService) updateHomeBrew() {
s.LayoutService.SetNotificationMessageWarning("Updating Homebrew formulae...")
if err := s.CommandService.UpdateHomebrew(); err != nil {
s.LayoutService.SetNotificationMessageError("Could not update Homebrew formulae")
return
}
// Clear loading message and update results
s.LayoutService.SetNotificationMessageSuccess("Homebrew formulae updated successfully")
s.forceRefreshResults()
}
func (s *AppService) search(searchText string, scrollToTop bool) {
var filteredList []models.Formula
uniquePackages := make(map[string]bool)
// Determine the source list based on the current filter state
sourceList := s.packages
if s.showOnlyInstalled {
sourceList = &[]models.Formula{}
for _, info := range *s.packages {
if len(info.Installed) > 0 {
*sourceList = append(*sourceList, info)
}
}
}
if searchText == "" {
// Reset to the appropriate list when the search string is empty
filteredList = *sourceList
} else {
// Apply the search filter
for _, info := range *sourceList {
if strings.Contains(strings.ToLower(info.Name), strings.ToLower(searchText)) ||
strings.Contains(strings.ToLower(info.Description), strings.ToLower(searchText)) {
if !uniquePackages[info.Name] {
filteredList = append(filteredList, info)
uniquePackages[info.Name] = true
}
}
}
}
*s.filteredPackages = filteredList
s.setResults(s.filteredPackages, scrollToTop)
}
func (s *AppService) setDetails(info *models.Formula) {
if info != nil {
installedVersion := "Not installed"
packagePrefix := "-"
installedOnRequest := false
if len(info.Installed) > 0 {
if info.Installed[0].Version == info.Versions.Stable {
installedVersion = info.Installed[0].Version
} else {
installedVersion = fmt.Sprintf("[orange]%s[-]", info.Installed[0].Version)
}
packagePrefix, _ = s.BrewService.GetPrefixPath(info.Name)
installedOnRequest = info.Installed[0].InstalledOnRequest
}
dependencies := strings.Join(info.Dependencies, ", ")
if dependencies == "" {
dependencies = "None"
}
generalInfo := fmt.Sprintf(
"[blue]Name:[-] %s\n[blue]Description:[-] %s\n[blue]Homepage:[-] %s\n[blue]License:[-] %s\n[blue]Tap:[-] %s",
info.FullName, info.Description, info.Homepage, info.License, info.Tap,
)
installInfo := fmt.Sprintf(
"[blue]Installed:[-] %s\n[blue]Available Version:[-] %s\n[blue]Install Path:[-] %s\n[blue]Dependencies:[-] %s\n[blue]Installed On Request:[-] %t\n[blue]Outdated:[-] %t\n",
installedVersion, info.Versions.Stable, packagePrefix, dependencies, installedOnRequest, info.Outdated,
)
s.LayoutService.GetDetailsView().SetText(
fmt.Sprintf("%s\n\n%s", generalInfo, installInfo),
)
return
}
s.LayoutService.GetDetailsView().SetText("")
}
func (s *AppService) forceRefreshResults() {
s.app.QueueUpdateDraw(func() {
_ = s.BrewService.LoadAllFormulae()
s.search(s.LayoutService.GetSearchField().GetText(), false)
})
}
func (s *AppService) setResults(data *[]models.Formula, scrollToTop bool) {
headers := []string{"Name", "Version", "Description"}
s.LayoutService.GetResultTable().Clear()
for i, header := range headers {
s.LayoutService.GetResultTable().SetCell(0, i, tview.NewTableCell(header).
SetTextColor(tcell.ColorBlue).
SetAlign(tview.AlignLeft).
SetSelectable(false))
}
for i, info := range *data {
version := info.Versions.Stable
if len(info.Installed) > 0 && info.Installed[0].Version != info.Versions.Stable {
version = fmt.Sprintf("(%s) < %s", info.Installed[0].Version, info.Versions.Stable)
}
nameCell := tview.NewTableCell(info.Name).SetSelectable(true)
if len(info.Installed) > 0 {
nameCell.SetTextColor(tcell.ColorGreen)
}
versionCell := tview.NewTableCell(version).SetSelectable(true)
if version != "" && len(info.Installed) > 0 && info.Installed[0].Version < info.Versions.Stable {
versionCell.SetTextColor(tcell.ColorOrange)
}
s.LayoutService.GetResultTable().SetCell(i+1, 0, nameCell.SetExpansion(0))
s.LayoutService.GetResultTable().SetCell(i+1, 1, versionCell.SetExpansion(0))
s.LayoutService.GetResultTable().SetCell(i+1, 2, tview.NewTableCell(info.Description).SetSelectable(true).SetExpansion(1))
}
// Update the details view with the first item in the list
if len(*data) > 0 {
if scrollToTop {
s.LayoutService.GetResultTable().Select(1, 0)
s.LayoutService.GetResultTable().ScrollToBeginning()
s.setDetails(&(*data)[0])
}
// Update the filter counter
s.LayoutService.UpdateFilterCounterView(len(*s.packages), len(*s.filteredPackages))
return
}
s.setDetails(nil)
}
func (s *AppService) BuildApp() {
// Evaluate if there is a new version available
latestVersion, err := s.SelfUpdateService.CheckForUpdates()
if err == nil && latestVersion != AppVersion {
AppVersion = fmt.Sprintf("%s ([orange]New Version Available: %s[-])", AppVersion, latestVersion)
}
// Build the layout
s.LayoutService.SetNotificationView()
s.LayoutService.SetHeaderView(AppName, AppVersion, s.brewVersion)
s.LayoutService.SetLegendView()
s.LayoutService.SetDetailsView()
s.LayoutService.SetBuildOutputView()
s.LayoutService.SetFilterCounterView()
// Result table section
tableSelectionChangedFunc := func(row, _ int) {
if row > 0 && row-1 < len(*s.filteredPackages) {
s.setDetails(&(*s.filteredPackages)[row-1])
}
}
s.LayoutService.SetResultTable(tableSelectionChangedFunc)
// Search field section
inputDoneFunc := func(key tcell.Key) {
if key == tcell.KeyEnter || key == tcell.KeyEscape {
s.app.SetFocus(s.LayoutService.GetResultTable())
}
}
changedFunc := func(text string) {
s.search(text, true)
}
s.LayoutService.SetSearchField(inputDoneFunc, changedFunc)
// Set the grid layout (final step)
s.LayoutService.SetGrid()
// Add key event handler and set the root view
s.app.SetInputCapture(s.handleKeyEventInput)
s.app.SetRoot(s.LayoutService.GetGrid(), true)
s.app.SetFocus(s.LayoutService.GetResultTable())
go s.updateHomeBrew() // Update Async the Homebrew formulae
s.setResults(s.packages, true) // Set the results
}