mirror of
https://github.com/jesseduffield/lazydocker.git
synced 2026-07-21 23:01:03 +00:00
support filtered list panel without dependency on gui package
This commit is contained in:
parent
dcce4c67ec
commit
e46b908006
9 changed files with 314 additions and 17 deletions
|
|
@ -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…
Add table
Reference in a new issue