create filtered list struct

pull/392/head
Jesse Duffield 2 years ago
parent 57379252a5
commit cb38e48add

@ -40,11 +40,11 @@ type DockerCommand struct {
ErrorChan chan error
ContainerMutex sync.Mutex
ServiceMutex sync.Mutex
Services []*Service
Containers []*Container
Services []*Service
Containers []*Container
// DisplayContainers is the array of containers we will display in the containers panel. If Gui.ShowAllContainers is false, this will only be those containers which aren't based on a service. This reduces clutter and duplication in the UI
DisplayContainers []*Container
Images []*Image
Volumes []*Volume
Closers []io.Closer
}

@ -2,11 +2,9 @@ package commands
import (
"context"
"sort"
"strings"
"github.com/docker/docker/api/types/image"
"github.com/samber/lo"
"github.com/docker/docker/api/types"
"github.com/docker/docker/api/types/filters"
@ -103,7 +101,7 @@ func (i *Image) RenderHistory() (string, error) {
}
// RefreshImages returns a slice of docker images
func (c *DockerCommand) RefreshImages(filterString string) ([]*Image, error) {
func (c *DockerCommand) RefreshImages() ([]*Image, error) {
images, err := c.Client.ImageList(context.Background(), types.ImageListOptions{})
if err != nil {
return nil, err
@ -145,32 +143,6 @@ func (c *DockerCommand) RefreshImages(filterString string) ([]*Image, error) {
}
}
ownImages = lo.Filter(ownImages, func(image *Image, _ int) bool {
return !lo.SomeBy(c.Config.UserConfig.Ignore, func(ignore string) bool {
return strings.Contains(image.Name, ignore)
})
})
if filterString != "" {
ownImages = lo.Filter(ownImages, func(image *Image, _ int) bool {
return strings.Contains(image.Name, filterString)
})
}
noneLabel := "<none>"
sort.Slice(ownImages, func(i, j int) bool {
if ownImages[i].Name == noneLabel && ownImages[j].Name != noneLabel {
return false
}
if ownImages[i].Name != noneLabel && ownImages[j].Name == noneLabel {
return true
}
return ownImages[i].Name < ownImages[j].Name
})
return ownImages, nil
}

@ -0,0 +1,88 @@
package gui
import (
"sort"
"sync"
)
type FilteredList[T comparable] struct {
allItems []T
// indices of items in the allItems slice that are included in the filtered list
indices []int
mutex sync.RWMutex
}
func NewFilteredList[T comparable]() *FilteredList[T] {
return &FilteredList[T]{}
}
func (self *FilteredList[T]) SetItems(items []T) {
self.mutex.Lock()
defer self.mutex.Unlock()
self.allItems = items
self.indices = make([]int, len(items))
for i := range self.indices {
self.indices[i] = i
}
}
func (self *FilteredList[T]) Filter(filter func(T, int) bool) {
self.mutex.Lock()
defer self.mutex.Unlock()
self.indices = self.indices[:0]
for i, item := range self.allItems {
if filter(item, i) {
self.indices = append(self.indices, i)
}
}
}
func (self *FilteredList[T]) Sort(less func(T, T) bool) {
self.mutex.Lock()
defer self.mutex.Unlock()
sort.Slice(self.indices, func(i, j int) bool {
return less(self.allItems[self.indices[i]], self.allItems[self.indices[j]])
})
}
func (self *FilteredList[T]) Get(index int) T {
self.mutex.RLock()
defer self.mutex.RUnlock()
return self.allItems[self.indices[index]]
}
// returns the length of the filtered list
func (self *FilteredList[T]) Len() int {
self.mutex.RLock()
defer self.mutex.RUnlock()
return len(self.indices)
}
func (self *FilteredList[T]) GetIndex(item T) int {
self.mutex.RLock()
defer self.mutex.RUnlock()
for i, index := range self.indices {
if self.allItems[index] == item {
return i
}
}
return -1
}
func (self *FilteredList[T]) GetItems() []T {
self.mutex.RLock()
defer self.mutex.RUnlock()
result := make([]T, len(self.indices))
for i, index := range self.indices {
result[i] = self.allItems[index]
}
return result
}

@ -0,0 +1,183 @@
package gui
import (
"testing"
"github.com/stretchr/testify/assert"
)
func TestFilteredListGet(t *testing.T) {
tests := []struct {
f FilteredList[int]
args int
want int
}{
{
f: FilteredList[int]{allItems: []int{1, 2, 3}, indices: []int{0, 1, 2}},
args: 1,
want: 2,
},
{
f: FilteredList[int]{allItems: []int{1, 2, 3}, indices: []int{0, 1, 2}},
args: 2,
want: 3,
},
{
f: FilteredList[int]{allItems: []int{1, 2, 3}, indices: []int{1}},
args: 0,
want: 2,
},
}
for _, tt := range tests {
if got := tt.f.Get(tt.args); got != tt.want {
t.Errorf("FilteredList.Get() = %v, want %v", got, tt.want)
}
}
}
func TestFilteredListLen(t *testing.T) {
tests := []struct {
f FilteredList[int]
want int
}{
{
f: FilteredList[int]{allItems: []int{1, 2, 3}, indices: []int{0, 1, 2}},
want: 3,
},
{
f: FilteredList[int]{allItems: []int{1, 2, 3}, indices: []int{1}},
want: 1,
},
}
for _, tt := range tests {
if got := tt.f.Len(); got != tt.want {
t.Errorf("FilteredList.Len() = %v, want %v", got, tt.want)
}
}
}
func TestFilteredListFilter(t *testing.T) {
tests := []struct {
f FilteredList[int]
args func(int, int) bool
want FilteredList[int]
}{
{
f: FilteredList[int]{allItems: []int{1, 2, 3}, indices: []int{0, 1, 2}},
args: func(i int, _ int) bool { return i%2 == 0 },
want: FilteredList[int]{allItems: []int{1, 2, 3}, indices: []int{1}},
},
{
f: FilteredList[int]{allItems: []int{1, 2, 3}, indices: []int{0, 1, 2}},
args: func(i int, _ int) bool { return i%2 == 1 },
want: FilteredList[int]{allItems: []int{1, 2, 3}, indices: []int{0, 2}},
},
}
for _, tt := range tests {
tt.f.Filter(tt.args)
assert.EqualValues(t, tt.f.indices, tt.want.indices)
}
}
func TestFilteredListSort(t *testing.T) {
tests := []struct {
f FilteredList[int]
args func(int, int) bool
want FilteredList[int]
}{
{
f: FilteredList[int]{allItems: []int{1, 2, 3}, indices: []int{0, 1, 2}},
args: func(i int, j int) bool { return i < j },
want: FilteredList[int]{allItems: []int{1, 2, 3}, indices: []int{0, 1, 2}},
},
{
f: FilteredList[int]{allItems: []int{1, 2, 3}, indices: []int{0, 1, 2}},
args: func(i int, j int) bool { return i > j },
want: FilteredList[int]{allItems: []int{1, 2, 3}, indices: []int{2, 1, 0}},
},
}
for _, tt := range tests {
tt.f.Sort(tt.args)
assert.EqualValues(t, tt.f.indices, tt.want.indices)
}
}
func TestFilteredListGetIndex(t *testing.T) {
tests := []struct {
f FilteredList[int]
args int
want int
}{
{
f: FilteredList[int]{allItems: []int{1, 2, 3}, indices: []int{0, 1, 2}},
args: 1,
want: 0,
},
{
f: FilteredList[int]{allItems: []int{1, 2, 3}, indices: []int{0, 1, 2}},
args: 2,
want: 1,
},
{
f: FilteredList[int]{allItems: []int{1, 2, 3}, indices: []int{1}},
args: 0,
want: -1,
},
}
for _, tt := range tests {
if got := tt.f.GetIndex(tt.args); got != tt.want {
t.Errorf("FilteredList.GetIndex() = %v, want %v", got, tt.want)
}
}
}
func TestFilteredListGetItems(t *testing.T) {
tests := []struct {
f FilteredList[int]
want []int
}{
{
f: FilteredList[int]{allItems: []int{1, 2, 3}, indices: []int{0, 1, 2}},
want: []int{1, 2, 3},
},
{
f: FilteredList[int]{allItems: []int{1, 2, 3}, indices: []int{1}},
want: []int{2},
},
}
for _, tt := range tests {
got := tt.f.GetItems()
assert.EqualValues(t, got, tt.want)
}
}
func TestFilteredListSetItems(t *testing.T) {
tests := []struct {
f FilteredList[int]
args []int
want FilteredList[int]
}{
{
f: FilteredList[int]{allItems: []int{1, 2, 3}, indices: []int{0, 1, 2}},
args: []int{4, 5, 6},
want: FilteredList[int]{allItems: []int{4, 5, 6}, indices: []int{0, 1, 2}},
},
{
f: FilteredList[int]{allItems: []int{1, 2, 3}, indices: []int{1}},
args: []int{4},
want: FilteredList[int]{allItems: []int{4}, indices: []int{0}},
},
}
for _, tt := range tests {
tt.f.SetItems(tt.args)
assert.EqualValues(t, tt.f.indices, tt.want.indices)
assert.EqualValues(t, tt.f.allItems, tt.want.allItems)
}
}

@ -137,6 +137,16 @@ type guiState struct {
ScreenMode WindowMaximisation
Searching searchingState
Lists Lists
}
// these are the items we display, after filtering is applied.
type Lists struct {
Containers *FilteredList[*commands.Container]
Services *FilteredList[*commands.Service]
Images *FilteredList[*commands.Image]
Volumes *FilteredList[*commands.Volume]
}
type searchingState struct {
@ -173,6 +183,12 @@ func NewGui(log *logrus.Entry, dockerCommand *commands.DockerCommand, oSCommand
Project: &projectState{ContextIndex: 0},
},
ViewStack: []string{},
Lists: Lists{
Containers: NewFilteredList[*commands.Container](),
Services: NewFilteredList[*commands.Service](),
Images: NewFilteredList[*commands.Image](),
Volumes: NewFilteredList[*commands.Volume](),
},
}
cyclableViews := []string{"project", "containers", "images", "volumes"}
@ -313,7 +329,7 @@ func (gui *Gui) refresh() {
}
}()
go func() {
if err := gui.refreshImages(); err != nil {
if err := gui.reloadImages(); err != nil {
gui.Log.Error(err)
}
}()

@ -12,6 +12,7 @@ import (
"github.com/jesseduffield/lazydocker/pkg/commands"
"github.com/jesseduffield/lazydocker/pkg/config"
"github.com/jesseduffield/lazydocker/pkg/utils"
"github.com/samber/lo"
)
// list panel functions
@ -30,11 +31,11 @@ func (gui *Gui) getSelectedImage() (*commands.Image, error) {
return &commands.Image{}, gui.Errors.ErrNoImages
}
return gui.DockerCommand.Images[selectedLine], nil
return gui.State.Lists.Images.Get(selectedLine), nil
}
func (gui *Gui) handleImagesClick(g *gocui.Gui, v *gocui.View) error {
itemCount := len(gui.DockerCommand.Images)
itemCount := gui.State.Lists.Images.Len()
handleSelect := gui.handleImageSelect
selectedLine := &gui.State.Panels.Images.SelectedLine
@ -50,7 +51,7 @@ func (gui *Gui) handleImageSelect(g *gocui.Gui, v *gocui.View) error {
return gui.renderString(g, "main", gui.Tr.NoImages)
}
gui.focusY(gui.State.Panels.Images.SelectedLine, len(gui.DockerCommand.Images), v)
gui.focusY(gui.State.Panels.Images.SelectedLine, gui.State.Lists.Images.Len(), v)
key := "images-" + Image.ID + "-" + gui.getImageContexts()[gui.State.Panels.Images.ContextIndex]
if !gui.shouldRefresh(key) {
@ -97,34 +98,63 @@ func (gui *Gui) renderImageConfig(mainView *gocui.View, image *commands.Image) e
})
}
func (gui *Gui) refreshImages() error {
ImagesView := gui.getImagesView()
if ImagesView == nil {
// if the ImagesView hasn't been instantiated yet we just return
return nil
}
func (gui *Gui) reloadImages() error {
if err := gui.refreshStateImages(); err != nil {
return err
}
if len(gui.DockerCommand.Images) > 0 && gui.State.Panels.Images.SelectedLine == -1 {
return gui.rerenderImages()
}
func (gui *Gui) rerenderImages() error {
filterString := gui.filterString(gui.Views.Images)
gui.State.Lists.Images.Filter(func(image *commands.Image, index int) bool {
if lo.SomeBy(gui.Config.UserConfig.Ignore, func(ignore string) bool {
return strings.Contains(image.Name, ignore)
}) {
return false
}
if filterString != "" {
return strings.Contains(image.Name, filterString)
}
return true
})
noneLabel := "<none>"
gui.State.Lists.Images.Sort(func(a *commands.Image, b *commands.Image) bool {
if a.Name == noneLabel && b.Name != noneLabel {
return false
}
if a.Name != noneLabel && b.Name == noneLabel {
return true
}
return a.Name < b.Name
})
if gui.State.Lists.Images.Len() > 0 && gui.State.Panels.Images.SelectedLine == -1 {
gui.State.Panels.Images.SelectedLine = 0
}
if len(gui.DockerCommand.Images)-1 < gui.State.Panels.Images.SelectedLine {
gui.State.Panels.Images.SelectedLine = len(gui.DockerCommand.Images) - 1
if gui.State.Lists.Images.Len()-1 < gui.State.Panels.Images.SelectedLine {
gui.State.Panels.Images.SelectedLine = gui.State.Lists.Images.Len() - 1
}
gui.g.Update(func(g *gocui.Gui) error {
ImagesView.Clear()
gui.Views.Images.Clear()
isFocused := gui.g.CurrentView() == gui.Views.Images
list, err := utils.RenderList(gui.DockerCommand.Images, utils.IsFocused(isFocused))
list, err := utils.RenderList(gui.State.Lists.Images.GetItems(), utils.IsFocused(isFocused))
if err != nil {
return err
}
fmt.Fprint(ImagesView, list)
fmt.Fprint(gui.Views.Images, list)
if ImagesView == g.CurrentView() {
return gui.handleImageSelect(g, ImagesView)
if gui.Views.Images == g.CurrentView() {
return gui.handleImageSelect(g, gui.Views.Images)
}
return nil
})
@ -132,14 +162,14 @@ func (gui *Gui) refreshImages() error {
return nil
}
// TODO: leave this to DockerCommand
func (gui *Gui) refreshStateImages() error {
Images, err := gui.DockerCommand.RefreshImages(gui.filterString(gui.Views.Images))
images, err := gui.DockerCommand.RefreshImages()
if err != nil {
return err
}
gui.DockerCommand.Images = Images
// TODO: think about also re-filtering/sorting
gui.State.Lists.Images.SetItems(images)
return nil
}
@ -158,7 +188,7 @@ func (gui *Gui) handleImagesNextLine(g *gocui.Gui, v *gocui.View) error {
}
panelState := gui.State.Panels.Images
gui.changeSelectedLine(&panelState.SelectedLine, len(gui.DockerCommand.Images), false)
gui.changeSelectedLine(&panelState.SelectedLine, gui.State.Lists.Images.Len(), false)
return gui.handleImageSelect(gui.g, v)
}
@ -169,7 +199,7 @@ func (gui *Gui) handleImagesPrevLine(g *gocui.Gui, v *gocui.View) error {
}
panelState := gui.State.Panels.Images
gui.changeSelectedLine(&panelState.SelectedLine, len(gui.DockerCommand.Images), true)
gui.changeSelectedLine(&panelState.SelectedLine, gui.State.Lists.Images.Len(), true)
return gui.handleImageSelect(gui.g, v)
}
@ -274,7 +304,7 @@ func (gui *Gui) handlePruneImages() error {
if err != nil {
return gui.createErrorPanel(err.Error())
}
return gui.refreshImages()
return gui.reloadImages()
})
}, nil)
}

@ -516,6 +516,18 @@ func (gui *Gui) GetInitialKeybindings() []*Binding {
Modifier: gocui.ModNone,
Handler: gui.scrollRightMain,
},
{
ViewName: "search",
Key: gocui.KeyEnter,
Modifier: gocui.ModNone,
Handler: wrappedHandler(gui.commitSearch),
},
{
ViewName: "search",
Key: gocui.KeyEsc,
Modifier: gocui.ModNone,
Handler: wrappedHandler(gui.escapeSearchPrompt),
},
{
ViewName: "",
Key: 'J',

@ -147,7 +147,7 @@ func (gui *Gui) focusPointInView(view *gocui.View) {
listViews := map[string]listViewState{
"containers": {selectedLine: gui.State.Panels.Containers.SelectedLine, lineCount: len(gui.DockerCommand.DisplayContainers)},
"images": {selectedLine: gui.State.Panels.Images.SelectedLine, lineCount: len(gui.DockerCommand.Images)},
"images": {selectedLine: gui.State.Panels.Images.SelectedLine, lineCount: gui.State.Lists.Images.Len()},
"volumes": {selectedLine: gui.State.Panels.Volumes.SelectedLine, lineCount: len(gui.DockerCommand.Volumes)},
"services": {selectedLine: gui.State.Panels.Services.SelectedLine, lineCount: len(gui.DockerCommand.Services)},
"menu": {selectedLine: gui.State.Panels.Menu.SelectedLine, lineCount: gui.State.MenuItemCount},

@ -1,6 +1,8 @@
package gui
import "github.com/jesseduffield/gocui"
import (
"github.com/jesseduffield/gocui"
)
func (gui *Gui) handleOpenImageSearch() error {
return gui.handleOpenSearch(gui.Views.Images)
@ -10,23 +12,44 @@ 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()
return gui.rerenderImages()
}
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())
// TODO: handle error
_ = gui.onNewSearchString(v.TextArea.GetContent())
}
return matched
}
}
func (gui *Gui) escapeSearchPrompt() error {
if err := gui.clearSearch(); err != nil {
return err
}
return gui.returnFocus()
}
func (gui *Gui) clearSearch() error {
gui.State.Searching.searchString = ""
gui.State.Searching.isSearching = false
gui.State.Searching.view = nil
gui.Views.Search.ClearTextArea()
return gui.rerenderImages()
}
// returns to the list view with the filter still applied
func (gui *Gui) commitSearch() error {
return gui.returnFocus()
}

@ -137,7 +137,7 @@ func (gui *Gui) createAllViews() error {
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.Views.Search.Editor = gocui.EditorFunc(gui.wrapEditor(gocui.SimpleEditor))
gui.waitForIntro.Done()

Loading…
Cancel
Save