mirror of
https://github.com/Valkyrie00/bold-brew.git
synced 2026-03-14 14:25:53 +01:00
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:
parent
78eb621275
commit
5546ad1b33
6 changed files with 271 additions and 229 deletions
|
|
@ -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 {
|
||||
|
|
|
|||
|
|
@ -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())
|
||||
|
||||
|
|
|
|||
|
|
@ -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
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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
|
||||
}
|
||||
|
|
@ -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
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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()
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue