bold-brew/internal/services/brew.go
Vito Castellano b1e2c581e5
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)
2025-12-29 14:23:54 +01:00

208 lines
5.5 KiB
Go

package services
import (
"bbrew/internal/models"
"fmt"
"io"
"os/exec"
"strings"
"sync"
"github.com/rivo/tview"
)
// BrewServiceInterface defines the contract for Homebrew operations.
// BrewService is a pure executor of brew commands - it does NOT hold data.
// For data retrieval, use DataProviderInterface.
type BrewServiceInterface interface {
// Core info
GetBrewVersion() (string, error)
// Package operations
UpdateHomebrew() error
UpdateAllPackages(app *tview.Application, outputView *tview.TextView) error
UpdatePackage(info models.Package, app *tview.Application, outputView *tview.TextView) error
RemovePackage(info models.Package, app *tview.Application, outputView *tview.TextView) error
InstallPackage(info models.Package, app *tview.Application, outputView *tview.TextView) error
// Tap support
InstallTap(tapName string, app *tview.Application, outputView *tview.TextView) error
IsTapInstalled(tapName string) bool
}
// BrewService provides methods to execute Homebrew commands.
// It is a pure executor - no data storage. Use DataProvider for data.
type BrewService struct {
brewVersion string
}
// NewBrewService creates a new instance of BrewService.
var NewBrewService = func() BrewServiceInterface {
return &BrewService{}
}
// GetBrewVersion retrieves the version of Homebrew installed on the system, caching it for future calls.
func (s *BrewService) GetBrewVersion() (string, error) {
if s.brewVersion != "" {
return s.brewVersion, nil
}
cmd := exec.Command("brew", "--version")
output, err := cmd.Output()
if err != nil {
return "", err
}
s.brewVersion = strings.TrimSpace(string(output))
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")
return cmd.Run()
}
// UpdateAllPackages upgrades all outdated packages.
func (s *BrewService) UpdateAllPackages(app *tview.Application, outputView *tview.TextView) error {
cmd := exec.Command("brew", "upgrade") // #nosec G204
return s.executeCommand(app, cmd, outputView)
}
// UpdatePackage upgrades a specific package.
func (s *BrewService) UpdatePackage(info models.Package, app *tview.Application, outputView *tview.TextView) error {
var cmd *exec.Cmd
if info.Type == models.PackageTypeCask {
cmd = exec.Command("brew", "upgrade", "--cask", info.Name) // #nosec G204
} else {
cmd = exec.Command("brew", "upgrade", info.Name) // #nosec G204
}
return s.executeCommand(app, cmd, outputView)
}
// RemovePackage uninstalls a package.
func (s *BrewService) RemovePackage(info models.Package, app *tview.Application, outputView *tview.TextView) error {
var cmd *exec.Cmd
if info.Type == models.PackageTypeCask {
cmd = exec.Command("brew", "uninstall", "--cask", info.Name) // #nosec G204
} else {
cmd = exec.Command("brew", "uninstall", info.Name) // #nosec G204
}
return s.executeCommand(app, cmd, outputView)
}
// InstallPackage installs a package.
func (s *BrewService) InstallPackage(info models.Package, app *tview.Application, outputView *tview.TextView) error {
var cmd *exec.Cmd
if info.Type == models.PackageTypeCask {
cmd = exec.Command("brew", "install", "--cask", info.Name) // #nosec G204
} else {
cmd = exec.Command("brew", "install", info.Name) // #nosec G204
}
return s.executeCommand(app, cmd, outputView)
}
// InstallTap installs a Homebrew tap.
func (s *BrewService) InstallTap(tapName string, app *tview.Application, outputView *tview.TextView) error {
cmd := exec.Command("brew", "tap", tapName) // #nosec G204
return s.executeCommand(app, cmd, outputView)
}
// IsTapInstalled checks if a tap is already installed.
func (s *BrewService) IsTapInstalled(tapName string) bool {
cmd := exec.Command("brew", "tap")
output, err := cmd.Output()
if err != nil {
return false
}
taps := strings.Split(strings.TrimSpace(string(output)), "\n")
for _, tap := range taps {
if strings.TrimSpace(tap) == tapName {
return true
}
}
return false
}
// executeCommand runs a command and captures its output, updating the provided TextView.
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
}
var wg sync.WaitGroup
wg.Add(3)
cmdErrCh := make(chan error, 1)
go func() {
defer wg.Done()
defer stdoutWriter.Close()
defer stderrWriter.Close()
cmdErrCh <- cmd.Wait()
}()
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) // #nosec G104
outputView.ScrollToEnd()
})
}
if err != nil {
if err != io.EOF {
app.QueueUpdateDraw(func() {
fmt.Fprintf(outputView, "\nError: %v\n", err)
})
}
break
}
}
}()
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) // #nosec G104
outputView.ScrollToEnd()
})
}
if err != nil {
if err != io.EOF {
app.QueueUpdateDraw(func() {
fmt.Fprintf(outputView, "\nError: %v\n", err)
})
}
break
}
}
}()
wg.Wait()
return <-cmdErrCh
}