convert menu panel to use new struct
parent
158ef68372
commit
60df208924
@ -1,173 +1,165 @@
|
||||
package commands
|
||||
|
||||
import (
|
||||
"testing"
|
||||
|
||||
"github.com/docker/docker/api/types"
|
||||
"github.com/jesseduffield/lazydocker/pkg/config"
|
||||
"github.com/stretchr/testify/assert"
|
||||
)
|
||||
|
||||
func sampleContainers(userConfig *config.AppConfig) []*Container {
|
||||
return []*Container{
|
||||
{
|
||||
ID: "1",
|
||||
Name: "1",
|
||||
Container: types.Container{
|
||||
State: "exited",
|
||||
},
|
||||
Config: userConfig,
|
||||
},
|
||||
{
|
||||
ID: "2",
|
||||
Name: "2",
|
||||
Container: types.Container{
|
||||
State: "running",
|
||||
},
|
||||
Config: userConfig,
|
||||
},
|
||||
{
|
||||
ID: "3",
|
||||
Name: "3",
|
||||
Container: types.Container{
|
||||
State: "running",
|
||||
},
|
||||
Config: userConfig,
|
||||
},
|
||||
{
|
||||
ID: "4",
|
||||
Name: "4",
|
||||
Container: types.Container{
|
||||
State: "created",
|
||||
},
|
||||
Config: userConfig,
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
func expectedPerStatusContainers(appConfig *config.AppConfig) []*Container {
|
||||
return []*Container{
|
||||
{
|
||||
ID: "2",
|
||||
Name: "2",
|
||||
Container: types.Container{
|
||||
State: "running",
|
||||
},
|
||||
Config: appConfig,
|
||||
},
|
||||
{
|
||||
ID: "3",
|
||||
Name: "3",
|
||||
Container: types.Container{
|
||||
State: "running",
|
||||
},
|
||||
Config: appConfig,
|
||||
},
|
||||
{
|
||||
ID: "1",
|
||||
Name: "1",
|
||||
Container: types.Container{
|
||||
State: "exited",
|
||||
},
|
||||
Config: appConfig,
|
||||
},
|
||||
{
|
||||
ID: "4",
|
||||
Name: "4",
|
||||
Container: types.Container{
|
||||
State: "created",
|
||||
},
|
||||
Config: appConfig,
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
func expectedLegacySortedContainers(appConfig *config.AppConfig) []*Container {
|
||||
return []*Container{
|
||||
{
|
||||
ID: "1",
|
||||
Name: "1",
|
||||
Container: types.Container{
|
||||
State: "exited",
|
||||
},
|
||||
Config: appConfig,
|
||||
},
|
||||
{
|
||||
ID: "2",
|
||||
Name: "2",
|
||||
Container: types.Container{
|
||||
State: "running",
|
||||
},
|
||||
Config: appConfig,
|
||||
},
|
||||
{
|
||||
ID: "3",
|
||||
Name: "3",
|
||||
Container: types.Container{
|
||||
State: "running",
|
||||
},
|
||||
Config: appConfig,
|
||||
},
|
||||
{
|
||||
ID: "4",
|
||||
Name: "4",
|
||||
Container: types.Container{
|
||||
State: "created",
|
||||
},
|
||||
Config: appConfig,
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
func assertEqualContainers(t *testing.T, left *Container, right *Container) {
|
||||
t.Helper()
|
||||
assert.Equal(t, left.Container.State, right.Container.State)
|
||||
assert.Equal(t, left.Container.ID, right.Container.ID)
|
||||
assert.Equal(t, left.Name, right.Name)
|
||||
}
|
||||
|
||||
func TestSortContainers(t *testing.T) {
|
||||
appConfig := NewDummyAppConfig()
|
||||
appConfig.UserConfig = &config.UserConfig{
|
||||
Gui: config.GuiConfig{
|
||||
LegacySortContainers: false,
|
||||
},
|
||||
}
|
||||
command := &DockerCommand{
|
||||
Config: appConfig,
|
||||
}
|
||||
|
||||
containers := sampleContainers(appConfig)
|
||||
|
||||
sorted := expectedPerStatusContainers(appConfig)
|
||||
|
||||
ct := command.sortedContainers(containers)
|
||||
|
||||
assert.Equal(t, len(ct), len(sorted))
|
||||
|
||||
for i := 0; i < len(ct); i++ {
|
||||
assertEqualContainers(t, sorted[i], ct[i])
|
||||
}
|
||||
}
|
||||
|
||||
func TestLegacySortedContainers(t *testing.T) {
|
||||
appConfig := NewDummyAppConfig()
|
||||
appConfig.UserConfig = &config.UserConfig{
|
||||
Gui: config.GuiConfig{
|
||||
LegacySortContainers: true,
|
||||
},
|
||||
}
|
||||
command := &DockerCommand{
|
||||
Config: appConfig,
|
||||
}
|
||||
|
||||
containers := sampleContainers(appConfig)
|
||||
|
||||
sorted := expectedLegacySortedContainers(appConfig)
|
||||
|
||||
ct := command.sortedContainers(containers)
|
||||
|
||||
for i := 0; i < len(ct); i++ {
|
||||
assertEqualContainers(t, sorted[i], ct[i])
|
||||
}
|
||||
}
|
||||
// func sampleContainers(userConfig *config.AppConfig) []*Container {
|
||||
// return []*Container{
|
||||
// {
|
||||
// ID: "1",
|
||||
// Name: "1",
|
||||
// Container: types.Container{
|
||||
// State: "exited",
|
||||
// },
|
||||
// Config: userConfig,
|
||||
// },
|
||||
// {
|
||||
// ID: "2",
|
||||
// Name: "2",
|
||||
// Container: types.Container{
|
||||
// State: "running",
|
||||
// },
|
||||
// Config: userConfig,
|
||||
// },
|
||||
// {
|
||||
// ID: "3",
|
||||
// Name: "3",
|
||||
// Container: types.Container{
|
||||
// State: "running",
|
||||
// },
|
||||
// Config: userConfig,
|
||||
// },
|
||||
// {
|
||||
// ID: "4",
|
||||
// Name: "4",
|
||||
// Container: types.Container{
|
||||
// State: "created",
|
||||
// },
|
||||
// Config: userConfig,
|
||||
// },
|
||||
// }
|
||||
// }
|
||||
|
||||
// func expectedPerStatusContainers(appConfig *config.AppConfig) []*Container {
|
||||
// return []*Container{
|
||||
// {
|
||||
// ID: "2",
|
||||
// Name: "2",
|
||||
// Container: types.Container{
|
||||
// State: "running",
|
||||
// },
|
||||
// Config: appConfig,
|
||||
// },
|
||||
// {
|
||||
// ID: "3",
|
||||
// Name: "3",
|
||||
// Container: types.Container{
|
||||
// State: "running",
|
||||
// },
|
||||
// Config: appConfig,
|
||||
// },
|
||||
// {
|
||||
// ID: "1",
|
||||
// Name: "1",
|
||||
// Container: types.Container{
|
||||
// State: "exited",
|
||||
// },
|
||||
// Config: appConfig,
|
||||
// },
|
||||
// {
|
||||
// ID: "4",
|
||||
// Name: "4",
|
||||
// Container: types.Container{
|
||||
// State: "created",
|
||||
// },
|
||||
// Config: appConfig,
|
||||
// },
|
||||
// }
|
||||
// }
|
||||
|
||||
// func expectedLegacySortedContainers(appConfig *config.AppConfig) []*Container {
|
||||
// return []*Container{
|
||||
// {
|
||||
// ID: "1",
|
||||
// Name: "1",
|
||||
// Container: types.Container{
|
||||
// State: "exited",
|
||||
// },
|
||||
// Config: appConfig,
|
||||
// },
|
||||
// {
|
||||
// ID: "2",
|
||||
// Name: "2",
|
||||
// Container: types.Container{
|
||||
// State: "running",
|
||||
// },
|
||||
// Config: appConfig,
|
||||
// },
|
||||
// {
|
||||
// ID: "3",
|
||||
// Name: "3",
|
||||
// Container: types.Container{
|
||||
// State: "running",
|
||||
// },
|
||||
// Config: appConfig,
|
||||
// },
|
||||
// {
|
||||
// ID: "4",
|
||||
// Name: "4",
|
||||
// Container: types.Container{
|
||||
// State: "created",
|
||||
// },
|
||||
// Config: appConfig,
|
||||
// },
|
||||
// }
|
||||
// }
|
||||
|
||||
// func assertEqualContainers(t *testing.T, left *Container, right *Container) {
|
||||
// t.Helper()
|
||||
// assert.Equal(t, left.Container.State, right.Container.State)
|
||||
// assert.Equal(t, left.Container.ID, right.Container.ID)
|
||||
// assert.Equal(t, left.Name, right.Name)
|
||||
// }
|
||||
|
||||
// func TestSortContainers(t *testing.T) {
|
||||
// appConfig := NewDummyAppConfig()
|
||||
// appConfig.UserConfig = &config.UserConfig{
|
||||
// Gui: config.GuiConfig{
|
||||
// LegacySortContainers: false,
|
||||
// },
|
||||
// }
|
||||
// command := &DockerCommand{
|
||||
// Config: appConfig,
|
||||
// }
|
||||
|
||||
// containers := sampleContainers(appConfig)
|
||||
|
||||
// sorted := expectedPerStatusContainers(appConfig)
|
||||
|
||||
// ct := command.sortedContainers(containers)
|
||||
|
||||
// assert.Equal(t, len(ct), len(sorted))
|
||||
|
||||
// for i := 0; i < len(ct); i++ {
|
||||
// assertEqualContainers(t, sorted[i], ct[i])
|
||||
// }
|
||||
// }
|
||||
|
||||
// func TestLegacySortedContainers(t *testing.T) {
|
||||
// appConfig := NewDummyAppConfig()
|
||||
// appConfig.UserConfig = &config.UserConfig{
|
||||
// Gui: config.GuiConfig{
|
||||
// LegacySortContainers: true,
|
||||
// },
|
||||
// }
|
||||
// command := &DockerCommand{
|
||||
// Config: appConfig,
|
||||
// }
|
||||
|
||||
// containers := sampleContainers(appConfig)
|
||||
|
||||
// sorted := expectedLegacySortedContainers(appConfig)
|
||||
|
||||
// ct := command.sortedContainers(containers)
|
||||
|
||||
// for i := 0; i < len(ct); i++ {
|
||||
// assertEqualContainers(t, sorted[i], ct[i])
|
||||
// }
|
||||
// }
|
||||
|
@ -1,112 +1,139 @@
|
||||
package gui
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
|
||||
"github.com/jesseduffield/gocui"
|
||||
"github.com/jesseduffield/lazydocker/pkg/utils"
|
||||
)
|
||||
|
||||
// list panel functions
|
||||
type MenuItem struct {
|
||||
Label string
|
||||
|
||||
func (gui *Gui) handleMenuSelect(g *gocui.Gui, v *gocui.View) error {
|
||||
gui.focusY(gui.State.Panels.Menu.SelectedLine, gui.State.MenuItemCount, v)
|
||||
return nil
|
||||
}
|
||||
// alternative to Label. Allows specifying columns which will be auto-aligned
|
||||
LabelColumns []string
|
||||
|
||||
func (gui *Gui) handleMenuNextLine(g *gocui.Gui, v *gocui.View) error {
|
||||
panelState := gui.State.Panels.Menu
|
||||
gui.changeSelectedLine(&panelState.SelectedLine, v.LinesHeight(), false)
|
||||
OnPress func() error
|
||||
|
||||
return gui.handleMenuSelect(g, v)
|
||||
// Only applies when Label is used
|
||||
OpensMenu bool
|
||||
}
|
||||
|
||||
func (gui *Gui) handleMenuClick(g *gocui.Gui, v *gocui.View) error {
|
||||
itemCount := gui.State.MenuItemCount
|
||||
handleSelect := gui.handleMenuSelect
|
||||
selectedLine := &gui.State.Panels.Menu.SelectedLine
|
||||
type CreateMenuOptions struct {
|
||||
Title string
|
||||
Items []*MenuItem
|
||||
HideCancel bool
|
||||
}
|
||||
|
||||
if err := gui.handleClick(v, itemCount, selectedLine, handleSelect); err != nil {
|
||||
func (gui *Gui) getMenuPanel() *SideListPanel[*MenuItem] {
|
||||
return &SideListPanel[*MenuItem]{
|
||||
ListPanel: ListPanel[*MenuItem]{
|
||||
list: NewFilteredList[*MenuItem](),
|
||||
view: gui.Views.Menu,
|
||||
},
|
||||
noItemsMessage: "",
|
||||
gui: gui.intoInterface(),
|
||||
getSearchStrings: func(menuItem *MenuItem) []string {
|
||||
return menuItem.LabelColumns
|
||||
},
|
||||
onClick: gui.onMenuPress,
|
||||
sort: nil,
|
||||
getDisplayStrings: func(menuItem *MenuItem) []string {
|
||||
return menuItem.LabelColumns
|
||||
},
|
||||
|
||||
// the menu panel doesn't actually have any contexts to display on the main view
|
||||
// so what follows are all dummy values
|
||||
contextKeyPrefix: "menu",
|
||||
contextIdx: 0,
|
||||
getContextCacheKey: func(menuItem *MenuItem) string {
|
||||
return ""
|
||||
},
|
||||
getContexts: func() []ContextConfig[*MenuItem] {
|
||||
return []ContextConfig[*MenuItem]{}
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
func (gui *Gui) onMenuPress(menuItem *MenuItem) error {
|
||||
gui.Views.Menu.Visible = false
|
||||
err := gui.returnFocus()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
return gui.State.Panels.Menu.OnPress(g, v)
|
||||
if menuItem.OnPress == nil {
|
||||
return nil
|
||||
}
|
||||
|
||||
return menuItem.OnPress()
|
||||
}
|
||||
|
||||
func (gui *Gui) handleMenuPrevLine(g *gocui.Gui, v *gocui.View) error {
|
||||
panelState := gui.State.Panels.Menu
|
||||
gui.changeSelectedLine(&panelState.SelectedLine, v.LinesHeight(), true)
|
||||
func (gui *Gui) handleMenuPress() error {
|
||||
selectedMenuItem, err := gui.Panels.Menu.GetSelectedItem()
|
||||
if err != nil {
|
||||
return nil
|
||||
}
|
||||
|
||||
return gui.handleMenuSelect(g, v)
|
||||
return gui.onMenuPress(selectedMenuItem)
|
||||
}
|
||||
|
||||
// specific functions
|
||||
|
||||
func (gui *Gui) renderMenuOptions() error {
|
||||
optionsMap := map[string]string{
|
||||
"esc/q": gui.Tr.Close,
|
||||
"↑ ↓": gui.Tr.Navigate,
|
||||
"enter": gui.Tr.Execute,
|
||||
func (gui *Gui) Menu(opts CreateMenuOptions) error {
|
||||
if !opts.HideCancel {
|
||||
// this is mutative but I'm okay with that for now
|
||||
opts.Items = append(opts.Items, &MenuItem{
|
||||
LabelColumns: []string{gui.Tr.Cancel},
|
||||
OnPress: func() error {
|
||||
return nil
|
||||
},
|
||||
})
|
||||
}
|
||||
return gui.renderOptionsMap(optionsMap)
|
||||
}
|
||||
|
||||
func (gui *Gui) handleMenuClose(g *gocui.Gui, v *gocui.View) error {
|
||||
for _, key := range []gocui.Key{gocui.KeySpace, gocui.KeyEnter, 'y'} {
|
||||
if err := g.DeleteKeybinding("menu", key, gocui.ModNone); err != nil {
|
||||
return err
|
||||
maxColumnSize := 1
|
||||
|
||||
for _, item := range opts.Items {
|
||||
if item.LabelColumns == nil {
|
||||
item.LabelColumns = []string{item.Label}
|
||||
}
|
||||
}
|
||||
gui.Views.Menu.Visible = false
|
||||
return gui.returnFocus()
|
||||
}
|
||||
|
||||
func (gui *Gui) createMenu(title string, items interface{}, itemCount int, handlePress func(int) error) error {
|
||||
isFocused := gui.g.CurrentView().Name() == "menu"
|
||||
gui.State.MenuItemCount = itemCount
|
||||
list, err := utils.RenderList(items, utils.IsFocused(isFocused))
|
||||
if err != nil {
|
||||
return err
|
||||
if item.OpensMenu {
|
||||
item.LabelColumns[0] = utils.OpensMenuStyle(item.LabelColumns[0])
|
||||
}
|
||||
|
||||
maxColumnSize = utils.Max(maxColumnSize, len(item.LabelColumns))
|
||||
}
|
||||
|
||||
x0, y0, x1, y1 := gui.getConfirmationPanelDimensions(gui.g, false, list)
|
||||
_, _ = gui.g.SetView("menu", x0, y0, x1, y1, 0)
|
||||
menuView := gui.Views.Menu
|
||||
menuView.Title = title
|
||||
menuView.FgColor = gocui.ColorDefault
|
||||
menuView.Clear()
|
||||
fmt.Fprint(menuView, list)
|
||||
gui.State.Panels.Menu.SelectedLine = 0
|
||||
|
||||
wrappedHandlePress := func(g *gocui.Gui, v *gocui.View) error {
|
||||
selectedLine := gui.State.Panels.Menu.SelectedLine
|
||||
|
||||
menuView.Visible = false
|
||||
err := gui.returnFocus()
|
||||
if err != nil {
|
||||
return err
|
||||
for _, item := range opts.Items {
|
||||
if len(item.LabelColumns) < maxColumnSize {
|
||||
// we require that each item has the same number of columns so we're padding out with blank strings
|
||||
// if this item has too few
|
||||
item.LabelColumns = append(item.LabelColumns, make([]string, maxColumnSize-len(item.LabelColumns))...)
|
||||
}
|
||||
}
|
||||
|
||||
if err := handlePress(selectedLine); err != nil {
|
||||
return err
|
||||
}
|
||||
gui.Panels.Menu.SetItems(opts.Items)
|
||||
gui.Panels.Menu.setSelectedLineIdx(0)
|
||||
|
||||
return nil
|
||||
if err := gui.Panels.Menu.RerenderList(); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
gui.State.Panels.Menu.OnPress = wrappedHandlePress
|
||||
gui.Views.Menu.Title = opts.Title
|
||||
gui.Views.Menu.Visible = true
|
||||
|
||||
for _, key := range []gocui.Key{gocui.KeySpace, gocui.KeyEnter, 'y'} {
|
||||
_ = gui.g.DeleteKeybinding("menu", key, gocui.ModNone)
|
||||
return gui.switchFocus(gui.Views.Menu)
|
||||
}
|
||||
|
||||
if err := gui.g.SetKeybinding("menu", key, gocui.ModNone, wrappedHandlePress); err != nil {
|
||||
return err
|
||||
}
|
||||
// specific functions
|
||||
|
||||
func (gui *Gui) renderMenuOptions() error {
|
||||
optionsMap := map[string]string{
|
||||
"esc/q": gui.Tr.Close,
|
||||
"↑ ↓": gui.Tr.Navigate,
|
||||
"enter": gui.Tr.Execute,
|
||||
}
|
||||
return gui.renderOptionsMap(optionsMap)
|
||||
}
|
||||
|
||||
gui.g.Update(func(g *gocui.Gui) error {
|
||||
menuView.Visible = true
|
||||
return gui.switchFocus(menuView)
|
||||
})
|
||||
return nil
|
||||
func (gui *Gui) handleMenuClose(g *gocui.Gui, v *gocui.View) error {
|
||||
gui.Views.Menu.Visible = false
|
||||
return gui.returnFocus()
|
||||
}
|
||||
|
Loading…
Reference in New Issue