|
|
|
@ -1,15 +1,21 @@
|
|
|
|
|
package gui
|
|
|
|
|
|
|
|
|
|
import (
|
|
|
|
|
"fmt"
|
|
|
|
|
"strings"
|
|
|
|
|
|
|
|
|
|
"github.com/go-errors/errors"
|
|
|
|
|
"github.com/jesseduffield/gocui"
|
|
|
|
|
"github.com/jesseduffield/lazydocker/pkg/commands"
|
|
|
|
|
"github.com/jesseduffield/lazydocker/pkg/utils"
|
|
|
|
|
"github.com/samber/lo"
|
|
|
|
|
)
|
|
|
|
|
|
|
|
|
|
type ListPanel[T comparable] struct {
|
|
|
|
|
toColumns func(T) []string
|
|
|
|
|
|
|
|
|
|
selectedIdx int
|
|
|
|
|
list FilteredList[T]
|
|
|
|
|
list *FilteredList[T]
|
|
|
|
|
view *gocui.View
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
@ -42,4 +48,224 @@ type SideListPanel[T comparable] struct {
|
|
|
|
|
ListPanel[T]
|
|
|
|
|
|
|
|
|
|
contextIdx int
|
|
|
|
|
|
|
|
|
|
noItemsMessge string
|
|
|
|
|
|
|
|
|
|
gui IGui
|
|
|
|
|
|
|
|
|
|
contexts []ContextConfig[T]
|
|
|
|
|
|
|
|
|
|
// returns strings that can be filtered on
|
|
|
|
|
getSearchStrings func(item T) []string
|
|
|
|
|
|
|
|
|
|
sort func(a, b T) bool
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
type ContextConfig[T any] struct {
|
|
|
|
|
key string
|
|
|
|
|
title string
|
|
|
|
|
render func(item T) error
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
type IGui interface {
|
|
|
|
|
HandleClick(v *gocui.View, itemCount int, selectedLine *int, handleSelect func() error) error
|
|
|
|
|
RenderStringMain(message string) error
|
|
|
|
|
FocusY(selectedLine int, itemCount int, view *gocui.View)
|
|
|
|
|
ShouldRefresh(key string) bool
|
|
|
|
|
GetMainView() *gocui.View
|
|
|
|
|
PopupPanelFocused() bool
|
|
|
|
|
// TODO: replace with IsCurrentView() bool
|
|
|
|
|
CurrentView() *gocui.View
|
|
|
|
|
FilterString(view *gocui.View) string
|
|
|
|
|
IgnoreStrings() []string
|
|
|
|
|
Update(func() error)
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
func (gui *Gui) intoInterface() IGui {
|
|
|
|
|
return gui
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
func (gui *Gui) getImagePanel() *SideListPanel[*commands.Image] {
|
|
|
|
|
noneLabel := "<none>"
|
|
|
|
|
|
|
|
|
|
return &SideListPanel[*commands.Image]{
|
|
|
|
|
contextKeyPrefix: "images",
|
|
|
|
|
ListPanel: ListPanel[*commands.Image]{
|
|
|
|
|
toColumns: func(image *commands.Image) []string {
|
|
|
|
|
return []string{
|
|
|
|
|
image.Name,
|
|
|
|
|
image.Tag,
|
|
|
|
|
utils.FormatDecimalBytes(int(image.Image.Size)),
|
|
|
|
|
}
|
|
|
|
|
},
|
|
|
|
|
list: NewFilteredList[*commands.Image](),
|
|
|
|
|
view: gui.Views.Images,
|
|
|
|
|
},
|
|
|
|
|
contextIdx: -1, // TODO: see if this should be 0
|
|
|
|
|
noItemsMessge: gui.Tr.NoImages,
|
|
|
|
|
gui: gui.intoInterface(),
|
|
|
|
|
contexts: []ContextConfig[*commands.Image]{
|
|
|
|
|
{
|
|
|
|
|
key: "config",
|
|
|
|
|
title: gui.Tr.ConfigTitle,
|
|
|
|
|
render: func(image *commands.Image) error {
|
|
|
|
|
return gui.renderImageConfig(image)
|
|
|
|
|
},
|
|
|
|
|
},
|
|
|
|
|
},
|
|
|
|
|
getSearchStrings: func(image *commands.Image) []string {
|
|
|
|
|
return []string{image.Name, image.Tag}
|
|
|
|
|
},
|
|
|
|
|
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
|
|
|
|
|
},
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
func (self *SideListPanel[T]) OnClick() error {
|
|
|
|
|
itemCount := self.list.Len()
|
|
|
|
|
handleSelect := self.HandleSelect
|
|
|
|
|
selectedLine := &self.selectedIdx
|
|
|
|
|
|
|
|
|
|
return self.gui.HandleClick(self.view, itemCount, selectedLine, handleSelect)
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
func (self *SideListPanel[T]) HandleSelect() error {
|
|
|
|
|
item, err := self.GetSelectedItem()
|
|
|
|
|
if err != nil {
|
|
|
|
|
if err.Error() != self.noItemsMessge {
|
|
|
|
|
return err
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
return self.gui.RenderStringMain(self.noItemsMessge)
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
self.gui.FocusY(self.selectedIdx, self.list.Len(), self.view)
|
|
|
|
|
|
|
|
|
|
key := self.contextKeyPrefix + "-" + self.contexts[self.contextIdx].key
|
|
|
|
|
if !self.gui.ShouldRefresh(key) {
|
|
|
|
|
return nil
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
mainView := self.gui.GetMainView()
|
|
|
|
|
mainView.Tabs = self.GetContextTitles()
|
|
|
|
|
mainView.TabIndex = self.contextIdx
|
|
|
|
|
|
|
|
|
|
// now I have an item. What do I do with it?
|
|
|
|
|
return self.contexts[self.contextIdx].render(item)
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
func (self *SideListPanel[T]) GetContextTitles() []string {
|
|
|
|
|
return lo.Map(self.contexts, func(context ContextConfig[T], _ int) string {
|
|
|
|
|
return context.title
|
|
|
|
|
})
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
func (self *SideListPanel[T]) GetSelectedItem() (T, error) {
|
|
|
|
|
var zero T
|
|
|
|
|
|
|
|
|
|
if self.selectedIdx == -1 {
|
|
|
|
|
return zero, errors.New(self.noItemsMessge)
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
item, ok := self.list.TryGet(self.selectedIdx)
|
|
|
|
|
if !ok {
|
|
|
|
|
// could probably have a better error here
|
|
|
|
|
return zero, errors.New(self.noItemsMessge)
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
return item, nil
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
func (self *SideListPanel[T]) OnNextLine() error {
|
|
|
|
|
if self.ignoreKeypress() {
|
|
|
|
|
return nil
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
self.SelectNextLine()
|
|
|
|
|
|
|
|
|
|
return self.HandleSelect()
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
func (self *SideListPanel[T]) OnPrevLine() error {
|
|
|
|
|
if self.ignoreKeypress() {
|
|
|
|
|
return nil
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
self.SelectPrevLine()
|
|
|
|
|
|
|
|
|
|
return self.HandleSelect()
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
func (self *SideListPanel[T]) ignoreKeypress() bool {
|
|
|
|
|
return self.gui.PopupPanelFocused() || self.gui.CurrentView() != self.view
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
func (self *SideListPanel[T]) OnNextContext() error {
|
|
|
|
|
self.contextIdx = (self.contextIdx + 1) % len(self.contexts)
|
|
|
|
|
|
|
|
|
|
return self.HandleSelect()
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
func (self *SideListPanel[T]) OnPrevContext() error {
|
|
|
|
|
self.contextIdx = (self.contextIdx - 1 + len(self.contexts)) % len(self.contexts)
|
|
|
|
|
|
|
|
|
|
return self.HandleSelect()
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
func (self *SideListPanel[T]) RerenderList() error {
|
|
|
|
|
filterString := self.gui.FilterString(self.view)
|
|
|
|
|
|
|
|
|
|
self.list.Filter(func(item T, index int) bool {
|
|
|
|
|
if lo.SomeBy(self.gui.IgnoreStrings(), func(ignore string) bool {
|
|
|
|
|
return lo.SomeBy(self.getSearchStrings(item), func(searchString string) bool {
|
|
|
|
|
return strings.Contains(searchString, ignore)
|
|
|
|
|
})
|
|
|
|
|
}) {
|
|
|
|
|
return false
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
if filterString != "" {
|
|
|
|
|
return lo.SomeBy(self.getSearchStrings(item), func(searchString string) bool {
|
|
|
|
|
return strings.Contains(searchString, filterString)
|
|
|
|
|
})
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
return true
|
|
|
|
|
})
|
|
|
|
|
|
|
|
|
|
self.list.Sort(self.sort)
|
|
|
|
|
|
|
|
|
|
// TODO: use clamp?
|
|
|
|
|
if self.list.Len() > 0 && self.selectedIdx == -1 {
|
|
|
|
|
self.selectedIdx = 0
|
|
|
|
|
}
|
|
|
|
|
if self.list.Len()-1 < self.selectedIdx {
|
|
|
|
|
self.selectedIdx = self.list.Len() - 1
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
self.gui.Update(func() error {
|
|
|
|
|
self.view.Clear()
|
|
|
|
|
isFocused := self.gui.CurrentView() == self.view
|
|
|
|
|
list, err := utils.RenderList(self.list.GetItems(), utils.IsFocused(isFocused))
|
|
|
|
|
if err != nil {
|
|
|
|
|
return err
|
|
|
|
|
}
|
|
|
|
|
fmt.Fprint(self.view, list)
|
|
|
|
|
|
|
|
|
|
if self.view == self.gui.CurrentView() {
|
|
|
|
|
return self.HandleSelect()
|
|
|
|
|
}
|
|
|
|
|
return nil
|
|
|
|
|
})
|
|
|
|
|
|
|
|
|
|
return nil
|
|
|
|
|
}
|
|
|
|
|