support filtered list panel without dependency on gui package

pull/392/head
Jesse Duffield 2 years ago
parent dcce4c67ec
commit e46b908006

@ -105,7 +105,7 @@ func (gui *Gui) renderContainerEnv(container *commands.Container) error {
mainView.Wrap = gui.Config.UserConfig.Gui.WrapMainPanel
return gui.T.NewTask(func(stop chan struct{}) {
_ = gui.renderString(gui.g, "main", gui.containerEnv(container))
_ = gui.renderStringMain(gui.containerEnv(container))
})
}
@ -139,7 +139,7 @@ func (gui *Gui) containerEnv(container *commands.Container) string {
func (gui *Gui) renderContainerConfig(container *commands.Container) error {
if !container.DetailsLoaded() {
return gui.T.NewTask(func(stop chan struct{}) {
_ = gui.renderString(gui.g, "main", gui.Tr.WaitingForContainerInfo)
_ = gui.renderStringMain(gui.Tr.WaitingForContainerInfo)
})
}
@ -189,7 +189,7 @@ func (gui *Gui) renderContainerConfig(container *commands.Container) error {
output += fmt.Sprintf("\nFull details:\n\n%s", string(data))
return gui.T.NewTask(func(stop chan struct{}) {
_ = gui.renderString(gui.g, "main", output)
_ = gui.renderStringMain(output)
})
}

@ -56,6 +56,18 @@ func (self *FilteredList[T]) Get(index int) T {
return self.allItems[self.indices[index]]
}
func (self *FilteredList[T]) TryGet(index int) (T, bool) {
self.mutex.RLock()
defer self.mutex.RUnlock()
if index < 0 || index >= len(self.indices) {
var zero T
return zero, false
}
return self.allItems[self.indices[index]], true
}
// returns the length of the filtered list
func (self *FilteredList[T]) Len() int {
self.mutex.RLock()

@ -74,6 +74,12 @@ type Gui struct {
PauseBackgroundThreads bool
Mutexes
Panels Panels
}
type Panels struct {
Images *SideListPanel[*commands.Image]
}
type Mutexes struct {
@ -210,6 +216,11 @@ func NewGui(log *logrus.Entry, dockerCommand *commands.DockerCommand, oSCommand
CyclableViews: cyclableViews,
}
// TODO: see if we can avoid the circular dependency
gui.Panels = Panels{
Images: gui.getImagePanel(),
}
gui.GenerateSentinelErrors()
return gui, nil
@ -460,9 +471,21 @@ func (gui *Gui) shouldRefresh(key string) bool {
return true
}
func (gui *Gui) ShouldRefresh(key string) bool {
return gui.shouldRefresh(key)
}
func (gui *Gui) initiallyFocusedViewName() string {
if gui.DockerCommand.InDockerComposeProject {
return "services"
}
return "containers"
}
func (gui *Gui) IgnoreStrings() []string {
return gui.Config.UserConfig.Ignore
}
func (gui *Gui) Update(f func() error) {
gui.g.Update(func(*gocui.Gui) error { return f() })
}

@ -43,17 +43,17 @@ func (gui *Gui) handleImagesClick(g *gocui.Gui, v *gocui.View) error {
}
func (gui *Gui) handleImageSelect(g *gocui.Gui, v *gocui.View) error {
Image, err := gui.getSelectedImage()
image, err := gui.getSelectedImage()
if err != nil {
if err != gui.Errors.ErrNoImages {
return err
}
return gui.renderString(g, "main", gui.Tr.NoImages)
return gui.renderStringMain(gui.Tr.NoImages)
}
gui.focusY(gui.State.Panels.Images.SelectedLine, gui.State.Lists.Images.Len(), v)
key := "images-" + Image.ID + "-" + gui.getImageContexts()[gui.State.Panels.Images.ContextIndex]
key := "images-" + image.ID + "-" + gui.getImageContexts()[gui.State.Panels.Images.ContextIndex]
if !gui.shouldRefresh(key) {
return nil
}
@ -64,7 +64,7 @@ func (gui *Gui) handleImageSelect(g *gocui.Gui, v *gocui.View) error {
switch gui.getImageContexts()[gui.State.Panels.Images.ContextIndex] {
case "config":
if err := gui.renderImageConfig(mainView, Image); err != nil {
if err := gui.renderImageConfig(image); err != nil {
return err
}
default:
@ -74,7 +74,7 @@ func (gui *Gui) handleImageSelect(g *gocui.Gui, v *gocui.View) error {
return nil
}
func (gui *Gui) renderImageConfig(mainView *gocui.View, image *commands.Image) error {
func (gui *Gui) renderImageConfig(image *commands.Image) error {
return gui.T.NewTask(func(stop chan struct{}) {
padding := 10
output := ""
@ -91,10 +91,11 @@ func (gui *Gui) renderImageConfig(mainView *gocui.View, image *commands.Image) e
output += "\n\n" + history
mainView := gui.Views.Main
mainView.Autoscroll = false
mainView.Wrap = false // don't care what your config is this page is ugly without wrapping
_ = gui.renderString(gui.g, "main", output)
_ = gui.renderStringMain(output)
})
}
@ -182,6 +183,11 @@ func (gui *Gui) filterString(view *gocui.View) string {
return gui.State.Searching.searchString
}
// TODO: merge into the above
func (gui *Gui) FilterString(view *gocui.View) string {
return gui.filterString(view)
}
func (gui *Gui) handleImagesNextLine(g *gocui.Gui, v *gocui.View) error {
if gui.popupPanelFocused() || gui.g.CurrentView() != v {
return nil

@ -586,7 +586,7 @@ func (gui *Gui) GetInitialKeybindings() []*Binding {
"menu": {onKeyUpPress: gui.handleMenuPrevLine, onKeyDownPress: gui.handleMenuNextLine, onClick: gui.handleMenuClick},
"services": {onKeyUpPress: gui.handleServicesPrevLine, onKeyDownPress: gui.handleServicesNextLine, onClick: gui.handleServicesClick},
"containers": {onKeyUpPress: gui.handleContainersPrevLine, onKeyDownPress: gui.handleContainersNextLine, onClick: gui.handleContainersClick},
"images": {onKeyUpPress: gui.handleImagesPrevLine, onKeyDownPress: gui.handleImagesNextLine, onClick: gui.handleImagesClick},
"images": {onKeyUpPress: wrappedHandler(gui.Panels.Images.OnPrevLine), onKeyDownPress: wrappedHandler(gui.Panels.Images.OnNextLine), onClick: wrappedHandler(gui.Panels.Images.OnClick)},
"volumes": {onKeyUpPress: gui.handleVolumesPrevLine, onKeyDownPress: gui.handleVolumesNextLine, onClick: gui.handleVolumesClick},
"main": {onKeyUpPress: gui.scrollUpMain, onKeyDownPress: gui.scrollDownMain, onClick: gui.handleMainClick},
}

@ -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
}

@ -123,7 +123,7 @@ func (gui *Gui) renderCredits() error {
configBuf.String(),
}, "\n\n")
_ = gui.renderString(gui.g, "main", dashboardString)
_ = gui.renderStringMain(dashboardString)
})
}
@ -166,7 +166,7 @@ func (gui *Gui) renderDockerComposeConfig() error {
mainView.Wrap = gui.Config.UserConfig.Gui.WrapMainPanel
config := gui.DockerCommand.DockerComposeConfig()
_ = gui.renderString(gui.g, "main", config)
_ = gui.renderStringMain(config)
})
}

@ -229,6 +229,11 @@ func (gui *Gui) focusY(selectedY int, lineCount int, v *gocui.View) {
gui.focusPoint(0, selectedY, lineCount, v)
}
// TODO: combine with above
func (gui *Gui) FocusY(selectedY int, lineCount int, v *gocui.View) {
gui.focusY(selectedY, lineCount, v)
}
func (gui *Gui) cleanString(s string) string {
output := string(bom.Clean([]byte(s)))
return utils.NormalizeLinefeeds(output)
@ -262,6 +267,11 @@ func (gui *Gui) renderStringMain(s string) error {
return gui.renderString(gui.g, "main", s)
}
// TODO: merge with above
func (gui *Gui) RenderStringMain(s string) error {
return gui.renderStringMain(s)
}
// reRenderString sets the main view's content, without changing its origin
func (gui *Gui) reRenderStringMain(s string) {
gui.reRenderString("main", s)
@ -317,8 +327,11 @@ func (gui *Gui) getVolumesView() *gocui.View {
}
func (gui *Gui) getMainView() *gocui.View {
v, _ := gui.g.View("main")
return v
return gui.Views.Main
}
func (gui *Gui) GetMainView() *gocui.View {
return gui.getMainView()
}
func (gui *Gui) trimmedContent(v *gocui.View) string {
@ -390,6 +403,11 @@ func (gui *Gui) popupPanelFocused() bool {
return gui.isPopupPanel(gui.currentViewName())
}
// TODO: merge into above
func (gui *Gui) PopupPanelFocused() bool {
return gui.popupPanelFocused()
}
func (gui *Gui) clearMainView() {
mainView := gui.getMainView()
mainView.Clear()
@ -424,6 +442,14 @@ func (gui *Gui) handleClick(v *gocui.View, itemCount int, selectedLine *int, han
return handleSelect(gui.g, v)
}
// TODO: combine with above
func (gui *Gui) HandleClick(v *gocui.View, itemCount int, selectedLine *int, handleSelect func() error) error {
wrappedHandleSelect := func(g *gocui.Gui, v *gocui.View) error {
return handleSelect()
}
return gui.handleClick(v, itemCount, selectedLine, wrappedHandleSelect)
}
func (gui *Gui) nextScreenMode() error {
if gui.currentViewName() == "main" {
gui.State.ScreenMode = prevIntInCycle([]WindowMaximisation{SCREEN_NORMAL, SCREEN_HALF, SCREEN_FULL}, gui.State.ScreenMode)
@ -471,3 +497,7 @@ func prevIntInCycle(sl []WindowMaximisation, current WindowMaximisation) WindowM
}
return sl[len(sl)-1]
}
func (gui *Gui) CurrentView() *gocui.View {
return gui.g.CurrentView()
}

@ -44,7 +44,7 @@ func (gui *Gui) handleVolumeSelect(g *gocui.Gui, v *gocui.View) error {
if err != gui.Errors.ErrNoVolumes {
return err
}
return gui.renderString(g, "main", gui.Tr.NoVolumes)
return gui.renderStringMain(gui.Tr.NoVolumes)
}
gui.focusY(gui.State.Panels.Volumes.SelectedLine, len(gui.DockerCommand.Volumes), v)
@ -99,7 +99,7 @@ func (gui *Gui) renderVolumeConfig(mainView *gocui.View, volume *commands.Volume
output += utils.WithPadding("Size: ", padding) + utils.FormatBinaryBytes(int(volume.Volume.UsageData.Size)) + "\n"
}
_ = gui.renderString(gui.g, "main", output)
_ = gui.renderStringMain(output)
})
}

Loading…
Cancel
Save