This commit is contained in:
James Reilly 2026-01-13 09:50:05 -06:00 committed by GitHub
commit ee7a1c286d
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
4 changed files with 154 additions and 8 deletions

4
.env
View file

@ -2,5 +2,5 @@ APP_NAME=bbrew
APP_VERSION=0.0.1-local
CONTAINER_IMAGE_NAME=bbrew
BUILD_GOVERSION=1.25
BUILD_GOOS=darwin
BUILD_GOARCH=arm64
BUILD_GOOS=linux
BUILD_GOARCH=amd64

View file

@ -13,7 +13,7 @@ BUILD_GOOS ?= $(shell go env GOOS)
BUILD_GOARCH ?= $(shell go env GOARCH)
# Container runtime command
CONTAINER_RUN = podman run --rm -v $(PWD):/app $(CONTAINER_IMAGE_NAME)
CONTAINER_RUN = podman run --rm -v $(PWD):/app:Z $(CONTAINER_IMAGE_NAME)
##############################
# HELP

View file

@ -58,6 +58,7 @@ type InputService struct {
ActionHelp *InputAction
ActionBack *InputAction
ActionQuit *InputAction
ActionToggleSelection *InputAction
}
var NewInputService = func(appService *AppService, brewService BrewServiceInterface) InputServiceInterface {
@ -124,13 +125,17 @@ var NewInputService = func(appService *AppService, brewService BrewServiceInterf
Key: tcell.KeyRune, Rune: 'q', KeySlug: "q", Name: "Quit",
Action: s.handleQuitEvent, HideFromLegend: true,
}
s.ActionToggleSelection = &InputAction{
Key: tcell.KeyRune, Rune: ' ', KeySlug: "space", Name: "Select",
Action: s.handleToggleSelectionEvent, HideFromLegend: true,
}
// Build keyActions slice (InstallAll/RemoveAll added dynamically in Brewfile mode)
s.keyActions = []*InputAction{
s.ActionSearch, s.ActionFilterInstalled, s.ActionFilterOutdated,
s.ActionFilterLeaves, s.ActionFilterCasks, s.ActionInstall,
s.ActionUpdate, s.ActionRemove, s.ActionUpdateAll,
s.ActionHelp, s.ActionBack, s.ActionQuit,
s.ActionHelp, s.ActionBack, s.ActionQuit, s.ActionToggleSelection,
}
// Convert keyActions to legend entries
@ -169,6 +174,12 @@ func (s *InputService) HandleKeyEventInput(event *tcell.EventKey) *tcell.EventKe
return event
}
// Handle Space explicitly since it might conflict or need special handling
if event.Key() == tcell.KeyRune && event.Rune() == ' ' {
s.handleToggleSelectionEvent()
return nil
}
for _, input := range s.keyActions {
if event.Modifiers() == tcell.ModNone && input.Key == event.Key() && input.Rune == event.Rune() { // Check Rune
if input.Action != nil {
@ -188,8 +199,33 @@ func (s *InputService) HandleKeyEventInput(event *tcell.EventKey) *tcell.EventKe
// handleBack is called when the user presses the back key (Esc).
func (s *InputService) handleBack() {
s.layout.GetTable().ClearSelection()
s.appService.GetApp().SetRoot(s.layout.Root(), true)
s.appService.GetApp().SetFocus(s.layout.GetTable().View())
// Force redraw of table to remove selection visuals
// s.appService.forceRefreshResults() // Might be too heavy?
// Actually Table.ToggleSelection updates visual.
// ClearSelection needs to update visual too.
// But Table.ClearSelection just clears the map. I need to implement visual clear in Table or just force refresh.
// For now, let's just assume we need to refresh.
s.appService.search(s.layout.GetSearch().Field().GetText(), false)
}
// handleToggleSelectionEvent toggles the selection of the current row.
func (s *InputService) handleToggleSelectionEvent() {
row, _ := s.layout.GetTable().View().GetSelection()
if row > 0 { // Skip header
// Determine highlight color based on package status
color := tcell.ColorDarkCyan
if row-1 < len(*s.appService.filteredPackages) {
pkg := (*s.appService.filteredPackages)[row-1]
if pkg.LocallyInstalled {
color = tcell.ColorDarkRed // Use DarkRed for installed packages to indicate different state
}
}
s.layout.GetTable().ToggleSelection(row, color)
}
}
// handleSearchFieldEvent is called when the user presses the search key (/).
@ -306,6 +342,13 @@ func (s *InputService) closeModal() {
// handleInstallPackageEvent is called when the user presses the installation key (i).
func (s *InputService) handleInstallPackageEvent() {
if len(s.layout.GetTable().GetSelectedRows()) > 0 {
s.processSelectedPackages("install", "INSTALL", func(pkg models.Package) error {
return s.brewService.InstallPackage(pkg, s.appService.app, s.layout.GetOutput().View())
})
return
}
row, _ := s.layout.GetTable().View().GetSelection()
if row > 0 {
info := (*s.appService.filteredPackages)[row-1]
@ -329,6 +372,13 @@ func (s *InputService) handleInstallPackageEvent() {
// handleRemovePackageEvent is called when the user presses the removal key (r).
func (s *InputService) handleRemovePackageEvent() {
if len(s.layout.GetTable().GetSelectedRows()) > 0 {
s.processSelectedPackages("remove", "REMOVE", func(pkg models.Package) error {
return s.brewService.RemovePackage(pkg, s.appService.app, s.layout.GetOutput().View())
})
return
}
row, _ := s.layout.GetTable().View().GetSelection()
if row > 0 {
info := (*s.appService.filteredPackages)[row-1]
@ -352,6 +402,13 @@ func (s *InputService) handleRemovePackageEvent() {
// handleUpdatePackageEvent is called when the user presses the update key (u).
func (s *InputService) handleUpdatePackageEvent() {
if len(s.layout.GetTable().GetSelectedRows()) > 0 {
s.processSelectedPackages("update", "UPDATE", func(pkg models.Package) error {
return s.brewService.UpdatePackage(pkg, s.appService.app, s.layout.GetOutput().View())
})
return
}
row, _ := s.layout.GetTable().View().GetSelection()
if row > 0 {
info := (*s.appService.filteredPackages)[row-1]
@ -399,6 +456,53 @@ type batchOperation struct {
execute func(pkg models.Package) error
}
// processSelectedPackages processes the selected packages from the table.
func (s *InputService) processSelectedPackages(verb, tag string, action func(models.Package) error) {
selectedRows := s.layout.GetTable().GetSelectedRows()
if len(selectedRows) == 0 {
return
}
packages := make([]models.Package, 0, len(selectedRows))
for _, row := range selectedRows {
if row > 0 && row-1 < len(*s.appService.filteredPackages) {
packages = append(packages, (*s.appService.filteredPackages)[row-1])
}
}
if len(packages) == 0 {
return
}
s.showModal(fmt.Sprintf("Are you sure you want to %s %d selected packages?", verb, len(packages)), func() {
s.closeModal()
s.layout.GetOutput().Clear()
go func() {
total := len(packages)
for i, pkg := range packages {
s.layout.GetNotifier().ShowWarning(fmt.Sprintf("[%d/%d] %s %s...", i+1, total, verb, pkg.Name))
s.appService.app.QueueUpdateDraw(func() {
fmt.Fprintf(s.layout.GetOutput().View(), "\n[%s] %s %s...\n", tag, verb, pkg.Name)
})
if err := action(pkg); err != nil {
s.layout.GetNotifier().ShowError(fmt.Sprintf("Failed to %s %s", verb, pkg.Name))
s.appService.app.QueueUpdateDraw(func() {
fmt.Fprintf(s.layout.GetOutput().View(), "[ERROR] Failed to %s %s: %v\n", verb, pkg.Name, err)
})
continue
}
s.appService.app.QueueUpdateDraw(func() {
fmt.Fprintf(s.layout.GetOutput().View(), "[SUCCESS] %s processed successfully\n", pkg.Name)
})
}
s.layout.GetNotifier().ShowSuccess(fmt.Sprintf("Completed! Processed %d packages", total))
s.layout.GetTable().ClearSelection() // Clear selection after batch operation
s.appService.forceRefreshResults()
}()
}, s.closeModal)
}
// handleBatchPackageOperation processes multiple packages with progress notifications.
func (s *InputService) handleBatchPackageOperation(op batchOperation) {
if !s.appService.IsBrewfileMode() {

View file

@ -8,14 +8,16 @@ import (
)
type Table struct {
view *tview.Table
theme *theme.Theme
view *tview.Table
theme *theme.Theme
selectedRows map[int]bool
}
func NewTable(theme *theme.Theme) *Table {
table := &Table{
view: tview.NewTable(),
theme: theme,
view: tview.NewTable(),
theme: theme,
selectedRows: make(map[int]bool),
}
table.view.SetBorders(false)
table.view.SetSelectable(true, false)
@ -37,6 +39,46 @@ func (t *Table) View() *tview.Table {
func (t *Table) Clear() {
t.view.Clear()
t.selectedRows = make(map[int]bool)
}
func (t *Table) ClearSelection() {
t.selectedRows = make(map[int]bool)
}
func (t *Table) ToggleSelection(row int, highlightColor tcell.Color) {
isSelected := false
if t.selectedRows[row] {
delete(t.selectedRows, row)
} else {
t.selectedRows[row] = true
isSelected = true
}
// Update visual style for the row
colCount := t.view.GetColumnCount()
for i := 0; i < colCount; i++ {
cell := t.view.GetCell(row, i)
if cell != nil {
if isSelected {
cell.SetBackgroundColor(highlightColor)
} else {
cell.SetBackgroundColor(t.theme.DefaultBgColor) // Or tcell.ColorDefault
}
}
}
}
func (t *Table) IsSelected(row int) bool {
return t.selectedRows[row]
}
func (t *Table) GetSelectedRows() []int {
rows := make([]int, 0, len(t.selectedRows))
for row := range t.selectedRows {
rows = append(rows, row)
}
return rows
}
func (t *Table) SetTableHeaders(headers ...string) {