diff --git a/pkg/commands/image.go b/pkg/commands/image.go index 8bde987a..fcec2770 100644 --- a/pkg/commands/image.go +++ b/pkg/commands/image.go @@ -103,7 +103,7 @@ func (i *Image) RenderHistory() (string, error) { } // RefreshImages returns a slice of docker images -func (c *DockerCommand) RefreshImages() ([]*Image, error) { +func (c *DockerCommand) RefreshImages(filterString string) ([]*Image, error) { images, err := c.Client.ImageList(context.Background(), types.ImageListOptions{}) if err != nil { return nil, err @@ -151,6 +151,12 @@ func (c *DockerCommand) RefreshImages() ([]*Image, error) { }) }) + if filterString != "" { + ownImages = lo.Filter(ownImages, func(image *Image, _ int) bool { + return strings.Contains(image.Name, filterString) + }) + } + noneLabel := "" sort.Slice(ownImages, func(i, j int) bool { diff --git a/pkg/gui/arrangement.go b/pkg/gui/arrangement.go index f507685b..b1e6a975 100644 --- a/pkg/gui/arrangement.go +++ b/pkg/gui/arrangement.go @@ -28,7 +28,7 @@ func (gui *Gui) getWindowDimensions(informationStr string, appStatus string) map sidePanelsDirection = boxlayout.ROW } - showInfoSection := gui.Config.UserConfig.Gui.ShowBottomLine + showInfoSection := gui.Config.UserConfig.Gui.ShowBottomLine || gui.State.Searching.isSearching infoSectionSize := 0 if showInfoSection { infoSectionSize = 1 @@ -87,6 +87,19 @@ func (gui *Gui) getMidSectionWeights() (int, int) { } func (gui *Gui) infoSectionChildren(informationStr string, appStatus string) []*boxlayout.Box { + if gui.State.Searching.isSearching { + return []*boxlayout.Box{ + { + Window: "searchPrefix", + Size: runewidth.StringWidth(SEARCH_PREFIX), + }, + { + Window: "search", + Weight: 1, + }, + } + } + result := []*boxlayout.Box{} if len(appStatus) > 0 { diff --git a/pkg/gui/gui.go b/pkg/gui/gui.go index 69245453..613eb101 100644 --- a/pkg/gui/gui.go +++ b/pkg/gui/gui.go @@ -135,6 +135,14 @@ type guiState struct { Stats map[string]commands.ContainerStats ScreenMode WindowMaximisation + + Searching searchingState +} + +type searchingState struct { + view *gocui.View + isSearching bool + searchString string } // screen sizing determines how much space your selected window takes up (window diff --git a/pkg/gui/images_panel.go b/pkg/gui/images_panel.go index 9e2bd8cb..f9b24913 100644 --- a/pkg/gui/images_panel.go +++ b/pkg/gui/images_panel.go @@ -134,7 +134,7 @@ func (gui *Gui) refreshImages() error { // TODO: leave this to DockerCommand func (gui *Gui) refreshStateImages() error { - Images, err := gui.DockerCommand.RefreshImages() + Images, err := gui.DockerCommand.RefreshImages(gui.filterString(gui.Views.Images)) if err != nil { return err } @@ -144,6 +144,14 @@ func (gui *Gui) refreshStateImages() error { return nil } +func (gui *Gui) filterString(view *gocui.View) string { + if gui.State.Searching.view != view { + return "" + } + + return gui.State.Searching.searchString +} + func (gui *Gui) handleImagesNextLine(g *gocui.Gui, v *gocui.View) error { if gui.popupPanelFocused() || gui.g.CurrentView() != v { return nil diff --git a/pkg/gui/keybindings.go b/pkg/gui/keybindings.go index 3f8ac500..8477b654 100644 --- a/pkg/gui/keybindings.go +++ b/pkg/gui/keybindings.go @@ -443,6 +443,13 @@ func (gui *Gui) GetInitialKeybindings() []*Binding { Handler: gui.handleImagesBulkCommand, Description: gui.Tr.ViewBulkCommands, }, + { + ViewName: "images", + Key: '/', + Modifier: gocui.ModNone, + Handler: wrappedHandler(gui.handleOpenImageSearch), + Description: gui.Tr.FilterList, + }, { ViewName: "volumes", Key: '[', diff --git a/pkg/gui/searching.go b/pkg/gui/searching.go new file mode 100644 index 00000000..53395cf4 --- /dev/null +++ b/pkg/gui/searching.go @@ -0,0 +1,32 @@ +package gui + +import "github.com/jesseduffield/gocui" + +func (gui *Gui) handleOpenImageSearch() error { + return gui.handleOpenSearch(gui.Views.Images) +} + +func (gui *Gui) handleOpenSearch(view *gocui.View) error { + gui.State.Searching.isSearching = true + gui.State.Searching.view = view + + gui.Views.Search.ClearTextArea() + + return gui.switchFocus(gui.Views.Search) +} + +func (gui *Gui) onNewSearchString(value string) error { + // need to refresh the right list panel. + gui.State.Searching.searchString = value + return gui.refreshImages() +} + +func (gui *Gui) wrapEditor(f func(v *gocui.View, key gocui.Key, ch rune, mod gocui.Modifier) bool) func(v *gocui.View, key gocui.Key, ch rune, mod gocui.Modifier) bool { + return func(v *gocui.View, key gocui.Key, ch rune, mod gocui.Modifier) bool { + matched := f(v, key, ch, mod) + if matched { + gui.onNewSearchString(v.TextArea.GetContent()) + } + return matched + } +} diff --git a/pkg/gui/view_helpers.go b/pkg/gui/view_helpers.go index 000118d3..e597e21e 100644 --- a/pkg/gui/view_helpers.go +++ b/pkg/gui/view_helpers.go @@ -89,6 +89,8 @@ func (gui *Gui) newLineFocused(v *gocui.View) error { case "main": v.Highlight = false return nil + case "search": + return nil default: panic(gui.Tr.NoViewMachingNewLineFocusedSwitchStatement) } diff --git a/pkg/gui/views.go b/pkg/gui/views.go index bf417cbe..68cb9c9b 100644 --- a/pkg/gui/views.go +++ b/pkg/gui/views.go @@ -5,21 +5,34 @@ import ( "github.com/jesseduffield/gocui" ) +const SEARCH_PREFIX = "filter: " + type Views struct { + // side panels Project *gocui.View Services *gocui.View Containers *gocui.View Images *gocui.View Volumes *gocui.View + // main panel Main *gocui.View - Options *gocui.View + // bottom line + Options *gocui.View + Information *gocui.View + AppStatus *gocui.View + // text that prompts you to enter text in the Search view + SearchPrefix *gocui.View + // appears next to the SearchPrefix view, it's where you type in the search string + Search *gocui.View + + // popups Confirmation *gocui.View Menu *gocui.View - Information *gocui.View - AppStatus *gocui.View - Limit *gocui.View + + // will cover everything when it appears + Limit *gocui.View } type viewNameMapping struct { @@ -43,6 +56,8 @@ func (gui *Gui) orderedViewNameMappings() []viewNameMapping { {viewPtr: &gui.Views.Options, name: "options"}, {viewPtr: &gui.Views.AppStatus, name: "appStatus"}, {viewPtr: &gui.Views.Information, name: "information"}, + {viewPtr: &gui.Views.Search, name: "search"}, + {viewPtr: &gui.Views.SearchPrefix, name: "searchPrefix"}, // popups. {viewPtr: &gui.Views.Menu, name: "menu"}, @@ -113,6 +128,17 @@ func (gui *Gui) createAllViews() error { gui.Views.Limit.Title = gui.Tr.NotEnoughSpace gui.Views.Limit.Wrap = true + gui.Views.SearchPrefix.BgColor = gocui.ColorDefault + gui.Views.SearchPrefix.FgColor = gocui.ColorGreen + gui.Views.SearchPrefix.Frame = false + _ = gui.setViewContent(gui.Views.SearchPrefix, SEARCH_PREFIX) + + gui.Views.Search.BgColor = gocui.ColorDefault + gui.Views.Search.FgColor = gocui.ColorGreen + gui.Views.Search.Editable = true + gui.Views.Search.Frame = false + // gui.Views.Search.Editor = gocui.EditorFunc(gui.wrapEditor(gocui.SimpleEditor)) + gui.waitForIntro.Done() return nil @@ -134,5 +160,18 @@ func (gui *Gui) popupViewNames() []string { // these views have their position and size determined by arrangement.go func (gui *Gui) controlledBoundsViewNames() []string { - return []string{"project", "services", "containers", "images", "volumes", "options", "information", "appStatus", "main", "limit"} + return []string{ + "project", + "services", + "containers", + "images", + "volumes", + "options", + "information", + "appStatus", + "main", + "limit", + "searchPrefix", + "search", + } } diff --git a/pkg/i18n/english.go b/pkg/i18n/english.go index f747b5f5..dcbd43bd 100644 --- a/pkg/i18n/english.go +++ b/pkg/i18n/english.go @@ -97,6 +97,7 @@ type TranslationSet struct { ExecShell string RunCustomCommand string ViewBulkCommands string + FilterList string OpenInBrowser string SortContainersByState string @@ -190,6 +191,7 @@ func englishSet() TranslationSet { ExecShell: "exec shell", RunCustomCommand: "run predefined custom command", ViewBulkCommands: "view bulk commands", + FilterList: "filter list", OpenInBrowser: "open in browser (first port is http)", SortContainersByState: "sort containers by state",