feat: general improvement and removed command service (#20)

* improved quality code and add general logic fix

* feat: removed command service
This commit is contained in:
Vito 2025-06-26 15:43:26 +02:00 committed by GitHub
commit 5546ad1b33
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
6 changed files with 271 additions and 229 deletions

View file

@ -52,8 +52,8 @@ type Formula struct {
RubySourceChecksum RubySourceChecksum `json:"ruby_source_checksum"`
Analytics90dRank int
Analytics90dDownloads int
LocallyInstalled bool `json:"-"`
LocalPath string `json:"-"` // Local installation path
LocallyInstalled bool `json:"-"` // Internal flag to indicate if the formula is installed locally [internal use]
LocalPath string `json:"-"` // Internal path to the formula in the local Homebrew Cellar [internal use]
}
type Analytics struct {

View file

@ -26,6 +26,7 @@ type AppServiceInterface interface {
BuildApp()
}
// AppService manages the application state, Homebrew integration, and UI components.
type AppService struct {
app *tview.Application
theme *theme.Theme
@ -37,12 +38,13 @@ type AppService struct {
showOnlyOutdated bool
brewVersion string
BrewService BrewServiceInterface
SelfUpdateService SelfUpdateServiceInterface
IOService IOServiceInterface
brewService BrewServiceInterface
selfUpdateService SelfUpdateServiceInterface
ioService IOServiceInterface
}
func NewAppService() AppServiceInterface {
// NewAppService creates a new instance of AppService with initialized components.
var NewAppService = func() AppServiceInterface {
app := tview.NewApplication()
themeService := theme.NewTheme()
layout := ui.NewLayout(themeService)
@ -60,9 +62,9 @@ func NewAppService() AppServiceInterface {
}
// Initialize services
s.IOService = NewIOService(s)
s.BrewService = NewBrewService()
s.SelfUpdateService = NewSelfUpdateService()
s.ioService = NewIOService(s)
s.brewService = NewBrewService()
s.selfUpdateService = NewSelfUpdateService()
return s
}
@ -70,26 +72,28 @@ func NewAppService() AppServiceInterface {
func (s *AppService) GetApp() *tview.Application { return s.app }
func (s *AppService) GetLayout() ui.LayoutInterface { return s.layout }
// 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 {
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)
}
// Download and parse Homebrew formulae data
if err = s.BrewService.SetupData(false); err != nil {
if err = s.brewService.SetupData(false); err != nil {
return fmt.Errorf("failed to load Homebrew formulae: %v", err)
}
s.packages = s.BrewService.GetFormulae()
// Initialize packages and filteredPackages
s.packages = s.brewService.GetFormulae()
*s.filteredPackages = *s.packages
return nil
}
// updateHomeBrew updates the Homebrew formulae and refreshes the results in the UI.
func (s *AppService) updateHomeBrew() {
s.layout.GetNotifier().ShowWarning("Updating Homebrew formulae...")
if err := s.BrewService.UpdateHomebrew(); err != nil {
if err := s.brewService.UpdateHomebrew(); err != nil {
s.layout.GetNotifier().ShowError("Could not update Homebrew formulae")
return
}
@ -98,6 +102,7 @@ func (s *AppService) updateHomeBrew() {
s.forceRefreshResults()
}
// search filters the packages based on the search text and the current filter state.
func (s *AppService) search(searchText string, scrollToTop bool) {
var filteredList []models.Formula
uniquePackages := make(map[string]bool)
@ -154,18 +159,10 @@ func (s *AppService) search(searchText string, scrollToTop bool) {
s.setResults(s.filteredPackages, scrollToTop)
}
func (s *AppService) setDetails(info *models.Formula) {
if info == nil {
s.layout.GetDetails().SetContent(nil)
return
}
s.layout.GetDetails().SetContent(info)
}
// forceRefreshResults forces a refresh of the Homebrew formulae data and updates the results in the UI.
func (s *AppService) forceRefreshResults() {
_ = s.BrewService.SetupData(true)
s.packages = s.BrewService.GetFormulae()
_ = s.brewService.SetupData(true)
s.packages = s.brewService.GetFormulae()
*s.filteredPackages = *s.packages
s.app.QueueUpdateDraw(func() {
@ -173,6 +170,7 @@ func (s *AppService) forceRefreshResults() {
})
}
// setResults updates the results table with the provided data and optionally scrolls to the top.
func (s *AppService) setResults(data *[]models.Formula, scrollToTop bool) {
s.layout.GetTable().Clear()
s.layout.GetTable().SetTableHeaders("Name", "Version", "Description", "↓ (90d)")
@ -211,7 +209,7 @@ func (s *AppService) setResults(data *[]models.Formula, scrollToTop bool) {
if scrollToTop {
s.layout.GetTable().View().Select(1, 0)
s.layout.GetTable().View().ScrollToBeginning()
s.setDetails(&(*data)[0])
s.layout.GetDetails().SetContent(&(*data)[0])
}
// Update the filter counter
@ -219,20 +217,24 @@ func (s *AppService) setResults(data *[]models.Formula, scrollToTop bool) {
return
}
s.setDetails(nil)
s.layout.GetDetails().SetContent(nil) // Clear details if no results
}
// BuildApp builds the application layout, sets up event handlers, and initializes the UI components.
func (s *AppService) BuildApp() {
// Build the layout
s.layout.Setup()
s.layout.GetHeader().Update(AppName, 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 {
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)
s.layout.GetHeader().Update(AppName, AppVersion, s.brewVersion)
@ -240,27 +242,29 @@ func (s *AppService) BuildApp() {
}
}()
// Result table section
// 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.setDetails(&(*s.filteredPackages)[row-1])
s.layout.GetDetails().SetContent(&(*s.filteredPackages)[row-1])
}
}
s.layout.GetTable().View().SetSelectionChangedFunc(tableSelectionChangedFunc)
// Search field section
// Search input handlers
inputDoneFunc := func(key tcell.Key) {
if key == tcell.KeyEnter || key == tcell.KeyEscape {
s.app.SetFocus(s.layout.GetTable().View())
s.app.SetFocus(s.layout.GetTable().View()) // Set focus back to the table on Enter or Escape
}
}
changedFunc := func(text string) {
s.search(text, true)
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 and set the root view
s.app.SetInputCapture(s.IOService.HandleKeyEventInput)
// Add key event handler
s.app.SetInputCapture(s.ioService.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())

View file

@ -3,6 +3,8 @@ package services
import (
"bbrew/internal/models"
"encoding/json"
"fmt"
"github.com/rivo/tview"
"io"
"net/http"
"os"
@ -11,6 +13,7 @@ import (
"sort"
"strconv"
"strings"
"sync"
)
const FormulaeAPIURL = "https://formulae.brew.sh/api/formula.json"
@ -21,9 +24,16 @@ type BrewServiceInterface interface {
GetFormulae() (formulae *[]models.Formula)
SetupData(forceDownload bool) (err error)
GetBrewVersion() (version string, err error)
UpdateHomebrew() error
UpdateAllPackages(app *tview.Application, outputView *tview.TextView) error
UpdatePackage(info models.Formula, app *tview.Application, outputView *tview.TextView) error
RemovePackage(info models.Formula, app *tview.Application, outputView *tview.TextView) error
InstallPackage(info models.Formula, app *tview.Application, outputView *tview.TextView) error
}
// BrewService provides methods to interact with Homebrew, including
// retrieving formulae, managing packages, and handling analytics.
type BrewService struct {
// Package lists
all *[]models.Formula
@ -35,6 +45,7 @@ type BrewService struct {
prefixPath string
}
// NewBrewService creates a new instance of BrewService with initialized package lists.
var NewBrewService = func() BrewServiceInterface {
return &BrewService{
all: new([]models.Formula),
@ -43,6 +54,7 @@ var NewBrewService = func() BrewServiceInterface {
}
}
// GetPrefixPath retrieves the Homebrew prefix path, caching it for future calls.
func (s *BrewService) GetPrefixPath() (path string) {
if s.prefixPath != "" {
return s.prefixPath
@ -59,17 +71,18 @@ func (s *BrewService) GetPrefixPath() (path string) {
return s.prefixPath
}
// GetFormulae retrieves all formulae, merging remote and installed packages,
func (s *BrewService) GetFormulae() (formulae *[]models.Formula) {
packageMap := make(map[string]models.Formula)
// Add remote packages to the map if they don't already exist
// Add REMOTE packages to the map if they don't already exist
for _, formula := range *s.remote {
if _, exists := packageMap[formula.Name]; !exists {
packageMap[formula.Name] = formula
}
}
// Add installed packages to the map
// Add INSTALLED packages to the map
for _, formula := range *s.installed {
packageMap[formula.Name] = formula
}
@ -94,6 +107,7 @@ func (s *BrewService) GetFormulae() (formulae *[]models.Formula) {
return s.all
}
// SetupData initializes the BrewService by loading installed packages, remote formulae, and analytics data.
func (s *BrewService) SetupData(forceDownload bool) (err error) {
if err = s.loadInstalled(); err != nil {
return err
@ -110,6 +124,7 @@ func (s *BrewService) SetupData(forceDownload bool) (err error) {
return nil
}
// loadInstalled retrieves the list of installed Homebrew formulae and updates their local paths.
func (s *BrewService) loadInstalled() (err error) {
cmd := exec.Command("brew", "info", "--json=v1", "--installed")
output, err := cmd.Output()
@ -133,6 +148,7 @@ func (s *BrewService) loadInstalled() (err error) {
return nil
}
// loadRemote retrieves the list of remote Homebrew formulae from the API and caches them locally.
func (s *BrewService) loadRemote(forceDownload bool) (err error) {
homeDir, err := os.UserHomeDir()
if err != nil {
@ -182,6 +198,7 @@ func (s *BrewService) loadRemote(forceDownload bool) (err error) {
return nil
}
// loadAnalytics retrieves the analytics data for Homebrew formulae from the API.
func (s *BrewService) loadAnalytics() (err error) {
resp, err := http.Get(AnalyticsAPIURL)
if err != nil {
@ -204,6 +221,7 @@ func (s *BrewService) loadAnalytics() (err error) {
return nil
}
// GetBrewVersion retrieves the version of Homebrew installed on the system, caching it for future calls.
func (s *BrewService) GetBrewVersion() (version string, err error) {
if s.brewVersion != "" {
return s.brewVersion, nil
@ -219,6 +237,7 @@ func (s *BrewService) GetBrewVersion() (version string, err error) {
return s.brewVersion, nil
}
// UpdateHomebrew updates the Homebrew package manager by running the `brew update` command.
func (s *BrewService) UpdateHomebrew() error {
cmd := exec.Command("brew", "update")
if err := cmd.Run(); err != nil {
@ -226,3 +245,106 @@ func (s *BrewService) UpdateHomebrew() error {
}
return nil
}
func (s *BrewService) UpdateAllPackages(app *tview.Application, outputView *tview.TextView) error {
cmd := exec.Command("brew", "upgrade") // #nosec G204
return s.executeCommand(app, cmd, outputView)
}
func (s *BrewService) UpdatePackage(info models.Formula, app *tview.Application, outputView *tview.TextView) error {
cmd := exec.Command("brew", "upgrade", info.Name) // #nosec G204
return s.executeCommand(app, cmd, outputView)
}
func (s *BrewService) RemovePackage(info models.Formula, app *tview.Application, outputView *tview.TextView) error {
cmd := exec.Command("brew", "remove", info.Name) // #nosec G204
return s.executeCommand(app, cmd, outputView)
}
func (s *BrewService) InstallPackage(info models.Formula, app *tview.Application, outputView *tview.TextView) error {
cmd := exec.Command("brew", "install", info.Name) // #nosec G204
return s.executeCommand(app, cmd, outputView)
}
// executeCommand runs a command and captures its output, updating the provided TextView in the application.
func (s *BrewService) executeCommand(
app *tview.Application,
cmd *exec.Cmd,
outputView *tview.TextView,
) error {
stdoutPipe, stdoutWriter := io.Pipe()
stderrPipe, stderrWriter := io.Pipe()
cmd.Stdout = stdoutWriter
cmd.Stderr = stderrWriter
if err := cmd.Start(); err != nil {
return err
}
// Add a WaitGroup to wait for all goroutines to finish
var wg sync.WaitGroup
wg.Add(3)
// Goroutine to wait for the command to finish
go func() {
defer wg.Done()
defer stdoutWriter.Close()
defer stderrWriter.Close()
cmd.Wait()
}()
// Stdout handler
go func() {
defer wg.Done()
defer stdoutPipe.Close()
buf := make([]byte, 1024)
for {
n, err := stdoutPipe.Read(buf)
if n > 0 {
output := make([]byte, n)
copy(output, buf[:n])
app.QueueUpdateDraw(func() {
outputView.Write(output)
outputView.ScrollToEnd()
})
}
if err != nil {
if err != io.EOF {
app.QueueUpdateDraw(func() {
fmt.Fprintf(outputView, "\nError: %v\n", err)
})
}
break
}
}
}()
// Stderr handler
go func() {
defer wg.Done()
defer stderrPipe.Close()
buf := make([]byte, 1024)
for {
n, err := stderrPipe.Read(buf)
if n > 0 {
output := make([]byte, n)
copy(output, buf[:n])
app.QueueUpdateDraw(func() {
outputView.Write(output)
outputView.ScrollToEnd()
})
}
if err != nil {
if err != io.EOF {
app.QueueUpdateDraw(func() {
fmt.Fprintf(outputView, "\nError: %v\n", err)
})
}
break
}
}
}()
wg.Wait()
return nil
}

View file

@ -1,125 +0,0 @@
package services
import (
"bbrew/internal/models"
"fmt"
"github.com/rivo/tview"
"io"
"os/exec"
"sync"
)
type CommandServiceInterface interface {
UpdateAllPackages(app *tview.Application, outputView *tview.TextView) error
UpdatePackage(info models.Formula, app *tview.Application, outputView *tview.TextView) error
RemovePackage(info models.Formula, app *tview.Application, outputView *tview.TextView) error
InstallPackage(info models.Formula, app *tview.Application, outputView *tview.TextView) error
}
type CommandService struct{}
var NewCommandService = func() CommandServiceInterface {
return &CommandService{}
}
func (s *CommandService) UpdateAllPackages(app *tview.Application, outputView *tview.TextView) error {
cmd := exec.Command("brew", "upgrade") // #nosec G204
return s.executeCommand(app, cmd, outputView)
}
func (s *CommandService) UpdatePackage(info models.Formula, app *tview.Application, outputView *tview.TextView) error {
cmd := exec.Command("brew", "upgrade", info.Name) // #nosec G204
return s.executeCommand(app, cmd, outputView)
}
func (s *CommandService) RemovePackage(info models.Formula, app *tview.Application, outputView *tview.TextView) error {
cmd := exec.Command("brew", "remove", info.Name) // #nosec G204
return s.executeCommand(app, cmd, outputView)
}
func (s *CommandService) InstallPackage(info models.Formula, app *tview.Application, outputView *tview.TextView) error {
cmd := exec.Command("brew", "install", info.Name) // #nosec G204
return s.executeCommand(app, cmd, outputView)
}
func (s *CommandService) executeCommand(
app *tview.Application,
cmd *exec.Cmd,
outputView *tview.TextView,
) error {
stdoutPipe, stdoutWriter := io.Pipe()
stderrPipe, stderrWriter := io.Pipe()
cmd.Stdout = stdoutWriter
cmd.Stderr = stderrWriter
if err := cmd.Start(); err != nil {
return err
}
// Add a WaitGroup to wait for all goroutines to finish
var wg sync.WaitGroup
wg.Add(3)
// Wait for the command to finish
go func() {
defer wg.Done()
defer stdoutWriter.Close()
defer stderrWriter.Close()
cmd.Wait()
}()
// Stdout handler
go func() {
defer wg.Done()
defer stdoutPipe.Close()
buf := make([]byte, 1024)
for {
n, err := stdoutPipe.Read(buf)
if n > 0 {
output := make([]byte, n)
copy(output, buf[:n])
app.QueueUpdateDraw(func() {
outputView.Write(output)
outputView.ScrollToEnd()
})
}
if err != nil {
if err != io.EOF {
app.QueueUpdateDraw(func() {
fmt.Fprintf(outputView, "\nError: %v\n", err)
})
}
break
}
}
}()
// Stderr handler
go func() {
defer wg.Done()
defer stderrPipe.Close()
buf := make([]byte, 1024)
for {
n, err := stderrPipe.Read(buf)
if n > 0 {
output := make([]byte, n)
copy(output, buf[:n])
app.QueueUpdateDraw(func() {
outputView.Write(output)
outputView.ScrollToEnd()
})
}
if err != nil {
if err != io.EOF {
app.QueueUpdateDraw(func() {
fmt.Fprintf(outputView, "\nError: %v\n", err)
})
}
break
}
}
}()
wg.Wait()
return nil
}

View file

@ -6,18 +6,14 @@ import (
"github.com/gdamore/tcell/v2"
)
var (
IoSearch = IOAction{Key: tcell.KeyRune, Rune: '/', KeySlug: "/", Name: "Search"}
IoFilterInstalled = IOAction{Key: tcell.KeyRune, Rune: 'f', KeySlug: "f", Name: "Filter Installed"}
IoFilterOutdated = IOAction{Key: tcell.KeyRune, Rune: 'o', KeySlug: "o", Name: "Filter Outdated"}
IoInstall = IOAction{Key: tcell.KeyRune, Rune: 'i', KeySlug: "i", Name: "Install"}
IoUpdate = IOAction{Key: tcell.KeyRune, Rune: 'u', KeySlug: "u", Name: "Update"}
IoRemove = IOAction{Key: tcell.KeyRune, Rune: 'r', KeySlug: "r", Name: "Remove"}
IoUpdateAll = IOAction{Key: tcell.KeyCtrlU, Rune: 0, KeySlug: "ctrl+u", Name: "Update All"}
IoBack = IOAction{Key: tcell.KeyEsc, Rune: 0, KeySlug: "esc", Name: "Back to Table"}
IoQuit = IOAction{Key: tcell.KeyRune, Rune: 'q', KeySlug: "q", Name: "Quit"}
type FilterType int
const (
FilterInstalled FilterType = iota
FilterOutdated
)
// IOAction represents an input/output action that can be triggered by a key event.
type IOAction struct {
Key tcell.Key
Rune rune
@ -30,48 +26,85 @@ func (k *IOAction) SetAction(action func()) {
k.Action = action
}
// IOServiceInterface defines the interface for handling input/output actions in the application.
type IOServiceInterface interface {
HandleKeyEventInput(event *tcell.EventKey) *tcell.EventKey
}
// IOService implements the IOServiceInterface and handles key events for the application.
type IOService struct {
appService *AppService
layout ui.LayoutInterface
commandService CommandServiceInterface
keyActions []*IOAction
legendEntries []struct{ KeySlug, Name string }
appService *AppService
layout ui.LayoutInterface
brewService BrewServiceInterface
keyActions []*IOAction
legendEntries []struct{ KeySlug, Name string }
// Actions for each key input
ActionSearch *IOAction
ActionFilterInstalled *IOAction
ActionFilterOutdated *IOAction
ActionInstall *IOAction
ActionUpdate *IOAction
ActionRemove *IOAction
ActionUpdateAll *IOAction
ActionBack *IOAction
ActionQuit *IOAction
}
var NewIOService = func(appService *AppService) IOServiceInterface {
s := &IOService{
appService: appService,
layout: appService.GetLayout(),
commandService: NewCommandService(),
appService: appService,
layout: appService.GetLayout(),
brewService: NewBrewService(),
}
// Define actions for each key input
s.keyActions = []*IOAction{&IoSearch, &IoFilterInstalled, &IoFilterOutdated, &IoInstall, &IoUpdate, &IoUpdateAll, &IoRemove, &IoBack, &IoQuit}
IoQuit.SetAction(s.handleQuitEvent)
IoUpdate.SetAction(s.handleUpdatePackageEvent)
IoUpdateAll.SetAction(s.handleUpdateAllPackagesEvent)
IoRemove.SetAction(s.handleRemovePackageEvent)
IoInstall.SetAction(s.handleInstallPackageEvent)
IoSearch.SetAction(s.handleSearchFieldEvent)
IoFilterInstalled.SetAction(s.handleFilterPackagesEvent)
IoFilterOutdated.SetAction(s.handleFilterOutdatedPackagesEvent)
IoBack.SetAction(s.handleBack)
// Initialize key actions with their respective keys, runes, and names.
s.ActionSearch = &IOAction{Key: tcell.KeyRune, Rune: '/', KeySlug: "/", Name: "Search"}
s.ActionFilterInstalled = &IOAction{Key: tcell.KeyRune, Rune: 'f', KeySlug: "f", Name: "Filter Installed"}
s.ActionFilterOutdated = &IOAction{Key: tcell.KeyRune, Rune: 'o', KeySlug: "o", Name: "Filter Outdated"}
s.ActionInstall = &IOAction{Key: tcell.KeyRune, Rune: 'i', KeySlug: "i", Name: "Install"}
s.ActionUpdate = &IOAction{Key: tcell.KeyRune, Rune: 'u', KeySlug: "u", Name: "Update"}
s.ActionRemove = &IOAction{Key: tcell.KeyRune, Rune: 'r', KeySlug: "r", Name: "Remove"}
s.ActionUpdateAll = &IOAction{Key: tcell.KeyCtrlU, Rune: 0, KeySlug: "ctrl+u", Name: "Update All"}
s.ActionBack = &IOAction{Key: tcell.KeyEsc, Rune: 0, KeySlug: "esc", Name: "Back to Table"}
s.ActionQuit = &IOAction{Key: tcell.KeyRune, Rune: 'q', KeySlug: "q", Name: "Quit"}
// Convert IOMap to a map for easier access
// Define actions for each key input,
s.ActionSearch.SetAction(s.handleSearchFieldEvent)
s.ActionFilterInstalled.SetAction(s.handleFilterPackagesEvent)
s.ActionFilterOutdated.SetAction(s.handleFilterOutdatedPackagesEvent)
s.ActionInstall.SetAction(s.handleInstallPackageEvent)
s.ActionUpdate.SetAction(s.handleUpdatePackageEvent)
s.ActionRemove.SetAction(s.handleRemovePackageEvent)
s.ActionUpdateAll.SetAction(s.handleUpdateAllPackagesEvent)
s.ActionBack.SetAction(s.handleBack)
s.ActionQuit.SetAction(s.handleQuitEvent)
// Add all actions to the keyActions slice
s.keyActions = []*IOAction{
s.ActionSearch,
s.ActionFilterInstalled,
s.ActionFilterOutdated,
s.ActionInstall,
s.ActionUpdate,
s.ActionRemove,
s.ActionUpdateAll,
s.ActionBack,
s.ActionQuit,
}
// Convert keyActions to legend entries
s.legendEntries = make([]struct{ KeySlug, Name string }, len(s.keyActions))
for i, input := range s.keyActions {
s.legendEntries[i] = struct{ KeySlug, Name string }{KeySlug: input.KeySlug, Name: input.Name}
}
// Initialize the legend text
// Initialize the legend text, literally the UI component that displays the key bindings
s.layout.GetLegend().SetLegend(s.legendEntries, "")
return s
}
// HandleKeyEventInput processes key events and triggers the corresponding actions.
func (s *IOService) HandleKeyEventInput(event *tcell.EventKey) *tcell.EventKey {
if s.layout.GetSearch().Field().HasFocus() {
return event
@ -94,77 +127,81 @@ func (s *IOService) HandleKeyEventInput(event *tcell.EventKey) *tcell.EventKey {
return event
}
// handleBack is called when the user presses the back key (Esc).
func (s *IOService) handleBack() {
s.appService.GetApp().SetRoot(s.layout.Root(), true)
s.appService.GetApp().SetFocus(s.layout.GetTable().View())
}
// handleSearchFieldEvent is called when the user presses the search key (/).
func (s *IOService) handleSearchFieldEvent() {
s.appService.GetApp().SetFocus(s.layout.GetSearch().Field())
}
// handleQuitEvent is called when the user presses the quit key (q).
func (s *IOService) handleQuitEvent() {
s.appService.GetApp().Stop()
}
// handleFilterEvent toggles the filter for installed or outdated packages based on the provided filter type.
func (s *IOService) handleFilterEvent(filterType FilterType) {
s.layout.GetLegend().SetLegend(s.legendEntries, "")
switch filterType {
case FilterInstalled:
if s.appService.showOnlyOutdated {
s.appService.showOnlyOutdated = false
s.appService.showOnlyInstalled = true
} else {
s.appService.showOnlyInstalled = !s.appService.showOnlyInstalled
}
case FilterOutdated:
if s.appService.showOnlyInstalled {
s.appService.showOnlyInstalled = false
s.appService.showOnlyOutdated = true
} else {
s.appService.showOnlyOutdated = !s.appService.showOnlyOutdated
}
}
// Update the search field label and legend based on the current filter state
if s.appService.showOnlyOutdated {
s.layout.GetSearch().Field().SetLabel("Search (Outdated): ")
s.layout.GetLegend().SetLegend(s.legendEntries, s.ActionFilterOutdated.KeySlug)
} else if s.appService.showOnlyInstalled {
s.layout.GetSearch().Field().SetLabel("Search (Installed): ")
s.layout.GetLegend().SetLegend(s.legendEntries, s.ActionFilterInstalled.KeySlug)
} else {
s.layout.GetSearch().Field().SetLabel("Search (All): ")
}
s.appService.search(s.layout.GetSearch().Field().GetText(), true)
}
// handleFilterPackagesEvent toggles the filter for installed packages
func (s *IOService) handleFilterPackagesEvent() {
s.layout.GetLegend().SetLegend(s.legendEntries, "")
if s.appService.showOnlyOutdated {
s.appService.showOnlyOutdated = false
s.appService.showOnlyInstalled = true
} else {
s.appService.showOnlyInstalled = !s.appService.showOnlyInstalled
}
// Update the search field label
if s.appService.showOnlyOutdated {
s.layout.GetSearch().Field().SetLabel("Search (Outdated): ")
s.layout.GetLegend().SetLegend(s.legendEntries, IoFilterOutdated.KeySlug)
} else if s.appService.showOnlyInstalled {
s.layout.GetSearch().Field().SetLabel("Search (Installed): ")
s.layout.GetLegend().SetLegend(s.legendEntries, IoFilterInstalled.KeySlug)
} else {
s.layout.GetSearch().Field().SetLabel("Search (All): ")
}
s.appService.search(s.layout.GetSearch().Field().GetText(), true)
s.handleFilterEvent(FilterInstalled)
}
// handleFilterOutdatedPackagesEvent toggles the filter for outdated packages
func (s *IOService) handleFilterOutdatedPackagesEvent() {
s.layout.GetLegend().SetLegend(s.legendEntries, "")
if s.appService.showOnlyInstalled {
s.appService.showOnlyInstalled = false
s.appService.showOnlyOutdated = true
} else {
s.appService.showOnlyOutdated = !s.appService.showOnlyOutdated
}
// Update the search field label
if s.appService.showOnlyOutdated {
s.layout.GetSearch().Field().SetLabel("Search (Outdated): ")
s.layout.GetLegend().SetLegend(s.legendEntries, IoFilterOutdated.KeySlug)
} else if s.appService.showOnlyInstalled {
s.layout.GetSearch().Field().SetLabel("Search (Installed): ")
s.layout.GetLegend().SetLegend(s.legendEntries, IoFilterInstalled.KeySlug)
} else {
s.layout.GetSearch().Field().SetLabel("Search (All): ")
}
s.appService.search(s.layout.GetSearch().Field().GetText(), true)
s.handleFilterEvent(FilterOutdated)
}
// showModal displays a modal dialog with the specified text and confirmation/cancellation actions.
// This is used for actions like installing, removing, or updating packages, invoking user confirmation.
func (s *IOService) showModal(text string, confirmFunc func(), cancelFunc func()) {
modal := s.layout.GetModal().Build(text, confirmFunc, cancelFunc)
s.appService.app.SetRoot(modal, true)
}
// closeModal closes the currently displayed modal dialog and returns focus to the main table view.
func (s *IOService) closeModal() {
s.appService.app.SetRoot(s.layout.Root(), true)
s.appService.app.SetFocus(s.layout.GetTable().View())
}
// handleInstallPackageEvent is called when the user presses the installation key (i).
func (s *IOService) handleInstallPackageEvent() {
row, _ := s.layout.GetTable().View().GetSelection()
if row > 0 {
@ -176,7 +213,7 @@ func (s *IOService) handleInstallPackageEvent() {
s.layout.GetOutput().Clear()
go func() {
s.layout.GetNotifier().ShowWarning(fmt.Sprintf("Installing %s...", info.Name))
if err := s.commandService.InstallPackage(info, s.appService.app, s.layout.GetOutput().View()); err != nil {
if err := s.brewService.InstallPackage(info, s.appService.app, s.layout.GetOutput().View()); err != nil {
s.layout.GetNotifier().ShowError(fmt.Sprintf("Failed to install %s", info.Name))
return
}
@ -187,6 +224,7 @@ func (s *IOService) handleInstallPackageEvent() {
}
}
// handleRemovePackageEvent is called when the user presses the removal key (r).
func (s *IOService) handleRemovePackageEvent() {
row, _ := s.layout.GetTable().View().GetSelection()
if row > 0 {
@ -198,7 +236,7 @@ func (s *IOService) handleRemovePackageEvent() {
s.layout.GetOutput().Clear()
go func() {
s.layout.GetNotifier().ShowWarning(fmt.Sprintf("Removing %s...", info.Name))
if err := s.commandService.RemovePackage(info, s.appService.app, s.layout.GetOutput().View()); err != nil {
if err := s.brewService.RemovePackage(info, s.appService.app, s.layout.GetOutput().View()); err != nil {
s.layout.GetNotifier().ShowError(fmt.Sprintf("Failed to remove %s", info.Name))
return
}
@ -209,6 +247,7 @@ func (s *IOService) handleRemovePackageEvent() {
}
}
// handleUpdatePackageEvent is called when the user presses the update key (u).
func (s *IOService) handleUpdatePackageEvent() {
row, _ := s.layout.GetTable().View().GetSelection()
if row > 0 {
@ -220,7 +259,7 @@ func (s *IOService) handleUpdatePackageEvent() {
s.layout.GetOutput().Clear()
go func() {
s.layout.GetNotifier().ShowWarning(fmt.Sprintf("Updating %s...", info.Name))
if err := s.commandService.UpdatePackage(info, s.appService.app, s.layout.GetOutput().View()); err != nil {
if err := s.brewService.UpdatePackage(info, s.appService.app, s.layout.GetOutput().View()); err != nil {
s.layout.GetNotifier().ShowError(fmt.Sprintf("Failed to update %s", info.Name))
return
}
@ -231,13 +270,14 @@ func (s *IOService) handleUpdatePackageEvent() {
}
}
// handleUpdateAllPackagesEvent is called when the user presses the update all key (Ctrl+U).
func (s *IOService) handleUpdateAllPackagesEvent() {
s.showModal("Are you sure you want to update all Packages?", func() {
s.closeModal()
s.layout.GetOutput().Clear()
go func() {
s.layout.GetNotifier().ShowWarning("Updating all Packages...")
if err := s.commandService.UpdateAllPackages(s.appService.app, s.layout.GetOutput().View()); err != nil {
if err := s.brewService.UpdateAllPackages(s.appService.app, s.layout.GetOutput().View()); err != nil {
s.layout.GetNotifier().ShowError("Failed to update all Packages")
return
}

View file

@ -23,6 +23,7 @@ var NewSelfUpdateService = func() SelfUpdateServiceInterface {
return &SelfUpdateService{}
}
// CheckForUpdates checks for the latest version of the Bold Brew package using Homebrew.
func (s *SelfUpdateService) CheckForUpdates(ctx context.Context) (string, error) {
cmd := exec.CommandContext(ctx, "brew", "info", "--json=v1", "valkyrie00/bbrew/bbrew")
output, err := cmd.CombinedOutput()