From b0ad01423638ffa5d611ae596977fb1f437f57f1 Mon Sep 17 00:00:00 2001 From: Vito Castellano Date: Sat, 11 Oct 2025 01:05:16 +0200 Subject: [PATCH] feat: add leaves filter to show explicitly installed packages Add new filter [L] to display only "leaf" packages - those installed explicitly by the user and not as dependencies of other packages. --- internal/services/app.go | 11 +++++++++++ internal/services/io.go | 28 ++++++++++++++++++++++++++-- 2 files changed, 37 insertions(+), 2 deletions(-) diff --git a/internal/services/app.go b/internal/services/app.go index ce91d26..f380976 100644 --- a/internal/services/app.go +++ b/internal/services/app.go @@ -36,6 +36,7 @@ type AppService struct { filteredPackages *[]models.Formula showOnlyInstalled bool showOnlyOutdated bool + showOnlyLeaves bool brewVersion string brewService BrewServiceInterface @@ -58,6 +59,7 @@ var NewAppService = func() AppServiceInterface { filteredPackages: new([]models.Formula), showOnlyInstalled: false, showOnlyOutdated: false, + showOnlyLeaves: false, brewVersion: "-", } @@ -127,6 +129,15 @@ func (s *AppService) search(searchText string, scrollToTop bool) { } } + if s.showOnlyLeaves { + sourceList = &[]models.Formula{} + for _, info := range *s.packages { + if info.LocallyInstalled && len(info.Installed) > 0 && info.Installed[0].InstalledOnRequest { + *sourceList = append(*sourceList, info) + } + } + } + if searchText == "" { // Reset to the appropriate list when the search string is empty filteredList = *sourceList diff --git a/internal/services/io.go b/internal/services/io.go index 6933555..836f0d4 100644 --- a/internal/services/io.go +++ b/internal/services/io.go @@ -3,6 +3,7 @@ package services import ( "bbrew/internal/ui" "fmt" + "github.com/gdamore/tcell/v2" ) @@ -11,6 +12,7 @@ type FilterType int const ( FilterInstalled FilterType = iota FilterOutdated + FilterLeaves ) // IOAction represents an input/output action that can be triggered by a key event. @@ -43,6 +45,7 @@ type IOService struct { ActionSearch *IOAction ActionFilterInstalled *IOAction ActionFilterOutdated *IOAction + ActionFilterLeaves *IOAction ActionInstall *IOAction ActionUpdate *IOAction ActionRemove *IOAction @@ -62,6 +65,7 @@ var NewIOService = func(appService *AppService) IOServiceInterface { 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.ActionFilterLeaves = &IOAction{Key: tcell.KeyRune, Rune: 'l', KeySlug: "l", Name: "Filter Leaves"} 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"} @@ -73,6 +77,7 @@ var NewIOService = func(appService *AppService) IOServiceInterface { s.ActionSearch.SetAction(s.handleSearchFieldEvent) s.ActionFilterInstalled.SetAction(s.handleFilterPackagesEvent) s.ActionFilterOutdated.SetAction(s.handleFilterOutdatedPackagesEvent) + s.ActionFilterLeaves.SetAction(s.handleFilterLeavesEvent) s.ActionInstall.SetAction(s.handleInstallPackageEvent) s.ActionUpdate.SetAction(s.handleUpdatePackageEvent) s.ActionRemove.SetAction(s.handleRemovePackageEvent) @@ -85,6 +90,7 @@ var NewIOService = func(appService *AppService) IOServiceInterface { s.ActionSearch, s.ActionFilterInstalled, s.ActionFilterOutdated, + s.ActionFilterLeaves, s.ActionInstall, s.ActionUpdate, s.ActionRemove, @@ -149,19 +155,29 @@ func (s *IOService) handleFilterEvent(filterType FilterType) { switch filterType { case FilterInstalled: - if s.appService.showOnlyOutdated { + if s.appService.showOnlyOutdated || s.appService.showOnlyLeaves { s.appService.showOnlyOutdated = false + s.appService.showOnlyLeaves = false s.appService.showOnlyInstalled = true } else { s.appService.showOnlyInstalled = !s.appService.showOnlyInstalled } case FilterOutdated: - if s.appService.showOnlyInstalled { + if s.appService.showOnlyInstalled || s.appService.showOnlyLeaves { s.appService.showOnlyInstalled = false + s.appService.showOnlyLeaves = false s.appService.showOnlyOutdated = true } else { s.appService.showOnlyOutdated = !s.appService.showOnlyOutdated } + case FilterLeaves: + if s.appService.showOnlyInstalled || s.appService.showOnlyOutdated { + s.appService.showOnlyInstalled = false + s.appService.showOnlyOutdated = false + s.appService.showOnlyLeaves = true + } else { + s.appService.showOnlyLeaves = !s.appService.showOnlyLeaves + } } // Update the search field label and legend based on the current filter state @@ -171,6 +187,9 @@ func (s *IOService) handleFilterEvent(filterType FilterType) { } else if s.appService.showOnlyInstalled { s.layout.GetSearch().Field().SetLabel("Search (Installed): ") s.layout.GetLegend().SetLegend(s.legendEntries, s.ActionFilterInstalled.KeySlug) + } else if s.appService.showOnlyLeaves { + s.layout.GetSearch().Field().SetLabel("Search (Leaves): ") + s.layout.GetLegend().SetLegend(s.legendEntries, s.ActionFilterLeaves.KeySlug) } else { s.layout.GetSearch().Field().SetLabel("Search (All): ") } @@ -188,6 +207,11 @@ func (s *IOService) handleFilterOutdatedPackagesEvent() { s.handleFilterEvent(FilterOutdated) } +// handleFilterLeavesEvent toggles the filter for leaf packages (installed on request) +func (s *IOService) handleFilterLeavesEvent() { + s.handleFilterEvent(FilterLeaves) +} + // 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()) {