mirror of
https://github.com/jesseduffield/lazydocker.git
synced 2026-07-25 08:31:03 +00:00
support custom commands and do minor refactor
This commit is contained in:
parent
645f588cb1
commit
c55e83aeaa
8 changed files with 340 additions and 318 deletions
|
|
@ -158,15 +158,6 @@ func (gui *Gui) renderLogsForRegularContainer(mainView *gocui.View, container *c
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func (gui *Gui) obtainLock() {
|
|
||||||
gui.State.MainProcessChan <- struct{}{}
|
|
||||||
gui.State.MainProcessMutex.Lock()
|
|
||||||
}
|
|
||||||
|
|
||||||
func (gui *Gui) releaseLock() {
|
|
||||||
gui.State.MainProcessMutex.Unlock()
|
|
||||||
}
|
|
||||||
|
|
||||||
func (gui *Gui) runProcessWithLock(cmd *exec.Cmd) {
|
func (gui *Gui) runProcessWithLock(cmd *exec.Cmd) {
|
||||||
gui.T.NewTask(func(stop chan struct{}) {
|
gui.T.NewTask(func(stop chan struct{}) {
|
||||||
cmd.Start()
|
cmd.Start()
|
||||||
|
|
|
||||||
301
pkg/gui/gui.go
301
pkg/gui/gui.go
|
|
@ -1,7 +1,6 @@
|
||||||
package gui
|
package gui
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"math"
|
|
||||||
"sync"
|
"sync"
|
||||||
|
|
||||||
// "io"
|
// "io"
|
||||||
|
|
@ -19,7 +18,6 @@ import (
|
||||||
"github.com/jesseduffield/lazydocker/pkg/config"
|
"github.com/jesseduffield/lazydocker/pkg/config"
|
||||||
"github.com/jesseduffield/lazydocker/pkg/i18n"
|
"github.com/jesseduffield/lazydocker/pkg/i18n"
|
||||||
"github.com/jesseduffield/lazydocker/pkg/tasks"
|
"github.com/jesseduffield/lazydocker/pkg/tasks"
|
||||||
"github.com/jesseduffield/lazydocker/pkg/utils"
|
|
||||||
"github.com/sirupsen/logrus"
|
"github.com/sirupsen/logrus"
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
@ -109,8 +107,6 @@ type guiState struct {
|
||||||
Platform commands.Platform
|
Platform commands.Platform
|
||||||
Panels *panelStates
|
Panels *panelStates
|
||||||
SubProcessOutput string
|
SubProcessOutput string
|
||||||
MainProcessMutex sync.Mutex
|
|
||||||
MainProcessChan chan struct{}
|
|
||||||
Stats map[string]commands.ContainerStats
|
Stats map[string]commands.ContainerStats
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -129,17 +125,8 @@ func NewGui(log *logrus.Entry, dockerCommand *commands.DockerCommand, oSCommand
|
||||||
ObjectKey: "",
|
ObjectKey: "",
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
MainProcessChan: make(chan struct{}),
|
|
||||||
}
|
}
|
||||||
|
|
||||||
go func() {
|
|
||||||
// setting up a goroutine for listening to the first stop signal on this channel
|
|
||||||
// because whenever something wants to lock the mutex, it tells the existing process to stop
|
|
||||||
// but on startup we don't have a process so we just mock it
|
|
||||||
// this is because we're using an unbuffered channel
|
|
||||||
<-initialState.MainProcessChan
|
|
||||||
}()
|
|
||||||
|
|
||||||
gui := &Gui{
|
gui := &Gui{
|
||||||
Log: log,
|
Log: log,
|
||||||
DockerCommand: dockerCommand,
|
DockerCommand: dockerCommand,
|
||||||
|
|
@ -158,32 +145,6 @@ func NewGui(log *logrus.Entry, dockerCommand *commands.DockerCommand, oSCommand
|
||||||
return gui, nil
|
return gui, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func (gui *Gui) scrollUpMain(g *gocui.Gui, v *gocui.View) error {
|
|
||||||
mainView, _ := g.View("main")
|
|
||||||
mainView.Autoscroll = false
|
|
||||||
ox, oy := mainView.Origin()
|
|
||||||
newOy := int(math.Max(0, float64(oy-gui.Config.UserConfig.Gui.ScrollHeight)))
|
|
||||||
return mainView.SetOrigin(ox, newOy)
|
|
||||||
}
|
|
||||||
|
|
||||||
func (gui *Gui) scrollDownMain(g *gocui.Gui, v *gocui.View) error {
|
|
||||||
mainView, _ := g.View("main")
|
|
||||||
ox, oy := mainView.Origin()
|
|
||||||
y := oy
|
|
||||||
if !gui.Config.UserConfig.Gui.ScrollPastBottom {
|
|
||||||
_, sy := mainView.Size()
|
|
||||||
y += sy
|
|
||||||
}
|
|
||||||
// for some reason we can't work out whether we've hit the bottomq
|
|
||||||
// there is a large discrepancy in the origin's y value and the length of BufferLines
|
|
||||||
return mainView.SetOrigin(ox, oy+gui.Config.UserConfig.Gui.ScrollHeight)
|
|
||||||
}
|
|
||||||
|
|
||||||
func (gui *Gui) autoScrollMain(g *gocui.Gui, v *gocui.View) error {
|
|
||||||
gui.getMainView().Autoscroll = true
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func (gui *Gui) handleRefresh(g *gocui.Gui, v *gocui.View) error {
|
func (gui *Gui) handleRefresh(g *gocui.Gui, v *gocui.View) error {
|
||||||
return gui.refreshSidePanels(g)
|
return gui.refreshSidePanels(g)
|
||||||
}
|
}
|
||||||
|
|
@ -195,260 +156,6 @@ func max(a, b int) int {
|
||||||
return b
|
return b
|
||||||
}
|
}
|
||||||
|
|
||||||
// getFocusLayout returns a manager function for when view gain and lose focus
|
|
||||||
func (gui *Gui) getFocusLayout() func(g *gocui.Gui) error {
|
|
||||||
var previousView *gocui.View
|
|
||||||
return func(g *gocui.Gui) error {
|
|
||||||
newView := gui.g.CurrentView()
|
|
||||||
if err := gui.onFocusChange(); err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
// for now we don't consider losing focus to a popup panel as actually losing focus
|
|
||||||
if newView != previousView && !gui.isPopupPanel(newView.Name()) {
|
|
||||||
if err := gui.onFocusLost(previousView, newView); err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
if err := gui.onFocus(newView); err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
previousView = newView
|
|
||||||
}
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func (gui *Gui) onFocusChange() error {
|
|
||||||
currentView := gui.g.CurrentView()
|
|
||||||
for _, view := range gui.g.Views() {
|
|
||||||
view.Highlight = view == currentView
|
|
||||||
}
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func (gui *Gui) onFocusLost(v *gocui.View, newView *gocui.View) error {
|
|
||||||
if v == nil {
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
gui.Log.Info(v.Name() + " focus lost")
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func (gui *Gui) onFocus(v *gocui.View) error {
|
|
||||||
if v == nil {
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
gui.Log.Info(v.Name() + " focus gained")
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
// layout is called for every screen re-render e.g. when the screen is resized
|
|
||||||
func (gui *Gui) layout(g *gocui.Gui) error {
|
|
||||||
g.Highlight = true
|
|
||||||
width, height := g.Size()
|
|
||||||
|
|
||||||
information := gui.Config.Version
|
|
||||||
|
|
||||||
minimumHeight := 9
|
|
||||||
minimumWidth := 10
|
|
||||||
if height < minimumHeight || width < minimumWidth {
|
|
||||||
v, err := g.SetView("limit", 0, 0, width-1, height-1, 0)
|
|
||||||
if err != nil {
|
|
||||||
if err.Error() != "unknown view" {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
v.Title = gui.Tr.SLocalize("NotEnoughSpace")
|
|
||||||
v.Wrap = true
|
|
||||||
_, _ = g.SetViewOnTop("limit")
|
|
||||||
}
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
currView := gui.g.CurrentView()
|
|
||||||
currentCyclebleView := gui.State.PreviousView
|
|
||||||
if currView != nil {
|
|
||||||
viewName := currView.Name()
|
|
||||||
usePreviouseView := true
|
|
||||||
for _, view := range cyclableViews {
|
|
||||||
if view == viewName {
|
|
||||||
currentCyclebleView = viewName
|
|
||||||
usePreviouseView = false
|
|
||||||
break
|
|
||||||
}
|
|
||||||
}
|
|
||||||
if usePreviouseView {
|
|
||||||
currentCyclebleView = gui.State.PreviousView
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
usableSpace := height - 4
|
|
||||||
|
|
||||||
tallPanels := 3
|
|
||||||
|
|
||||||
vHeights := map[string]int{
|
|
||||||
"status": tallPanels,
|
|
||||||
"services": usableSpace/tallPanels + usableSpace%tallPanels,
|
|
||||||
"containers": usableSpace / tallPanels,
|
|
||||||
"images": usableSpace / tallPanels,
|
|
||||||
"options": 1,
|
|
||||||
}
|
|
||||||
|
|
||||||
if height < 28 {
|
|
||||||
defaultHeight := 3
|
|
||||||
if height < 21 {
|
|
||||||
defaultHeight = 1
|
|
||||||
}
|
|
||||||
vHeights = map[string]int{
|
|
||||||
"status": defaultHeight,
|
|
||||||
"services": defaultHeight,
|
|
||||||
"containers": defaultHeight,
|
|
||||||
"images": defaultHeight,
|
|
||||||
"options": defaultHeight,
|
|
||||||
}
|
|
||||||
vHeights[currentCyclebleView] = height - defaultHeight*tallPanels - 1
|
|
||||||
}
|
|
||||||
|
|
||||||
optionsVersionBoundary := width - max(len(utils.Decolorise(information)), 1)
|
|
||||||
leftSideWidth := width / 3
|
|
||||||
|
|
||||||
appStatus := gui.statusManager.getStatusString()
|
|
||||||
appStatusOptionsBoundary := 0
|
|
||||||
if appStatus != "" {
|
|
||||||
appStatusOptionsBoundary = len(appStatus) + 2
|
|
||||||
}
|
|
||||||
|
|
||||||
_, _ = g.SetViewOnBottom("limit")
|
|
||||||
g.DeleteView("limit")
|
|
||||||
|
|
||||||
v, err := g.SetView("main", leftSideWidth+1, 0, width-1, height-2, gocui.LEFT)
|
|
||||||
if err != nil {
|
|
||||||
if err.Error() != "unknown view" {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
v.Wrap = true
|
|
||||||
v.FgColor = gocui.ColorWhite
|
|
||||||
}
|
|
||||||
|
|
||||||
if v, err := g.SetView("status", 0, 0, leftSideWidth, vHeights["status"]-1, gocui.BOTTOM|gocui.RIGHT); err != nil {
|
|
||||||
if err.Error() != "unknown view" {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
v.Title = gui.Tr.SLocalize("StatusTitle")
|
|
||||||
v.FgColor = gocui.ColorWhite
|
|
||||||
}
|
|
||||||
|
|
||||||
servicesView, err := g.SetViewBeneath("services", "status", vHeights["services"])
|
|
||||||
if err != nil {
|
|
||||||
if err.Error() != "unknown view" {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
servicesView.Highlight = true
|
|
||||||
servicesView.Title = gui.Tr.SLocalize("ServicesTitle")
|
|
||||||
servicesView.FgColor = gocui.ColorWhite
|
|
||||||
|
|
||||||
gui.focusPoint(0, gui.State.Panels.Services.SelectedLine, len(gui.DockerCommand.Services), servicesView)
|
|
||||||
}
|
|
||||||
|
|
||||||
containersView, err := g.SetViewBeneath("containers", "services", vHeights["containers"])
|
|
||||||
if err != nil {
|
|
||||||
if err.Error() != "unknown view" {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
containersView.Highlight = true
|
|
||||||
containersView.Title = gui.Tr.SLocalize("ContainersTitle")
|
|
||||||
containersView.FgColor = gocui.ColorWhite
|
|
||||||
|
|
||||||
gui.focusPoint(0, gui.State.Panels.Containers.SelectedLine, len(gui.DockerCommand.Containers), containersView)
|
|
||||||
}
|
|
||||||
|
|
||||||
imagesView, err := g.SetViewBeneath("images", "containers", vHeights["images"])
|
|
||||||
if err != nil {
|
|
||||||
if err.Error() != "unknown view" {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
imagesView.Highlight = true
|
|
||||||
imagesView.Title = gui.Tr.SLocalize("ImagesTitle")
|
|
||||||
imagesView.FgColor = gocui.ColorWhite
|
|
||||||
|
|
||||||
gui.focusPoint(0, gui.State.Panels.Images.SelectedLine, len(gui.DockerCommand.Images), imagesView)
|
|
||||||
}
|
|
||||||
|
|
||||||
if v, err := g.SetView("options", appStatusOptionsBoundary-1, height-2, optionsVersionBoundary-1, height, 0); err != nil {
|
|
||||||
if err.Error() != "unknown view" {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
v.Frame = false
|
|
||||||
if v.FgColor, err = gui.GetOptionsPanelTextColor(); err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if appStatusView, err := g.SetView("appStatus", -1, height-2, width, height, 0); err != nil {
|
|
||||||
if err.Error() != "unknown view" {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
appStatusView.BgColor = gocui.ColorDefault
|
|
||||||
appStatusView.FgColor = gocui.ColorCyan
|
|
||||||
appStatusView.Frame = false
|
|
||||||
if _, err := g.SetViewOnBottom("appStatus"); err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if v, err := g.SetView("information", optionsVersionBoundary-1, height-2, width, height, 0); err != nil {
|
|
||||||
if err.Error() != "unknown view" {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
v.BgColor = gocui.ColorDefault
|
|
||||||
v.FgColor = gocui.ColorGreen
|
|
||||||
v.Frame = false
|
|
||||||
if err := gui.renderString(g, "information", information); err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
|
|
||||||
// doing this here because it'll only happen once
|
|
||||||
if err := gui.loadNewDirectory(); err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if gui.g.CurrentView() == nil {
|
|
||||||
if _, err := gui.g.SetCurrentView(gui.getContainersView().Name()); err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
|
|
||||||
if err := gui.switchFocus(gui.g, nil, gui.getServicesView()); err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
type listViewState struct {
|
|
||||||
selectedLine int
|
|
||||||
lineCount int
|
|
||||||
}
|
|
||||||
|
|
||||||
listViews := map[*gocui.View]listViewState{
|
|
||||||
containersView: {selectedLine: gui.State.Panels.Containers.SelectedLine, lineCount: len(gui.DockerCommand.Containers)},
|
|
||||||
imagesView: {selectedLine: gui.State.Panels.Images.SelectedLine, lineCount: len(gui.DockerCommand.Images)},
|
|
||||||
}
|
|
||||||
|
|
||||||
// menu view might not exist so we check to be safe
|
|
||||||
if menuView, err := gui.g.View("menu"); err == nil {
|
|
||||||
listViews[menuView] = listViewState{selectedLine: gui.State.Panels.Menu.SelectedLine, lineCount: gui.State.MenuItemCount}
|
|
||||||
}
|
|
||||||
for view, state := range listViews {
|
|
||||||
// check if the selected line is now out of view and if so refocus it
|
|
||||||
if err := gui.focusPoint(0, state.selectedLine, state.lineCount, view); err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// here is a good place log some stuff
|
|
||||||
// if you download humanlog and do tail -f development.log | humanlog
|
|
||||||
// this will let you see these branches as prettified json
|
|
||||||
// gui.Log.Info(utils.AsJson(gui.State.Branches[0:4]))
|
|
||||||
return gui.resizeCurrentPopupPanel(g)
|
|
||||||
}
|
|
||||||
|
|
||||||
func (gui *Gui) loadNewDirectory() error {
|
func (gui *Gui) loadNewDirectory() error {
|
||||||
gui.waitForIntro.Done()
|
gui.waitForIntro.Done()
|
||||||
|
|
||||||
|
|
@ -618,3 +325,11 @@ func (gui *Gui) runSyncOrAsyncCommand(sub *exec.Cmd, err error) (bool, error) {
|
||||||
}
|
}
|
||||||
return true, nil
|
return true, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (gui *Gui) handleCustomCommand(g *gocui.Gui, v *gocui.View) error {
|
||||||
|
return gui.createPromptPanel(g, v, gui.Tr.SLocalize("CustomCommand"), func(g *gocui.Gui, v *gocui.View) error {
|
||||||
|
command := gui.trimmedContent(v)
|
||||||
|
gui.SubProcess = gui.OSCommand.RunCustomCommand(command)
|
||||||
|
return gui.Errors.ErrSubProcess
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
|
||||||
|
|
@ -76,43 +76,57 @@ func (gui *Gui) GetInitialKeybindings() []*Binding {
|
||||||
Key: gocui.KeyEsc,
|
Key: gocui.KeyEsc,
|
||||||
Modifier: gocui.ModNone,
|
Modifier: gocui.ModNone,
|
||||||
Handler: gui.quit,
|
Handler: gui.quit,
|
||||||
}, {
|
},
|
||||||
|
{
|
||||||
ViewName: "",
|
ViewName: "",
|
||||||
Key: gocui.KeyPgup,
|
Key: gocui.KeyPgup,
|
||||||
Modifier: gocui.ModNone,
|
Modifier: gocui.ModNone,
|
||||||
Handler: gui.scrollUpMain,
|
Handler: gui.scrollUpMain,
|
||||||
}, {
|
},
|
||||||
|
{
|
||||||
ViewName: "",
|
ViewName: "",
|
||||||
Key: gocui.KeyPgdn,
|
Key: gocui.KeyPgdn,
|
||||||
Modifier: gocui.ModNone,
|
Modifier: gocui.ModNone,
|
||||||
Handler: gui.scrollDownMain,
|
Handler: gui.scrollDownMain,
|
||||||
}, {
|
},
|
||||||
|
{
|
||||||
ViewName: "",
|
ViewName: "",
|
||||||
Key: gocui.KeyCtrlU,
|
Key: gocui.KeyCtrlU,
|
||||||
Modifier: gocui.ModNone,
|
Modifier: gocui.ModNone,
|
||||||
Handler: gui.scrollUpMain,
|
Handler: gui.scrollUpMain,
|
||||||
}, {
|
},
|
||||||
|
{
|
||||||
ViewName: "",
|
ViewName: "",
|
||||||
Key: gocui.KeyCtrlD,
|
Key: gocui.KeyCtrlD,
|
||||||
Modifier: gocui.ModNone,
|
Modifier: gocui.ModNone,
|
||||||
Handler: gui.scrollDownMain,
|
Handler: gui.scrollDownMain,
|
||||||
}, {
|
},
|
||||||
|
{
|
||||||
ViewName: "",
|
ViewName: "",
|
||||||
Key: gocui.KeyEnd,
|
Key: gocui.KeyEnd,
|
||||||
Modifier: gocui.ModNone,
|
Modifier: gocui.ModNone,
|
||||||
Handler: gui.autoScrollMain,
|
Handler: gui.autoScrollMain,
|
||||||
}, {
|
},
|
||||||
|
{
|
||||||
ViewName: "",
|
ViewName: "",
|
||||||
Key: 'x',
|
Key: 'x',
|
||||||
Modifier: gocui.ModNone,
|
Modifier: gocui.ModNone,
|
||||||
Handler: gui.handleCreateOptionsMenu,
|
Handler: gui.handleCreateOptionsMenu,
|
||||||
}, {
|
},
|
||||||
|
{
|
||||||
|
ViewName: "",
|
||||||
|
Key: 'X',
|
||||||
|
Modifier: gocui.ModNone,
|
||||||
|
Handler: gui.handleCustomCommand,
|
||||||
|
},
|
||||||
|
{
|
||||||
ViewName: "status",
|
ViewName: "status",
|
||||||
Key: 'e',
|
Key: 'e',
|
||||||
Modifier: gocui.ModNone,
|
Modifier: gocui.ModNone,
|
||||||
Handler: gui.handleEditConfig,
|
Handler: gui.handleEditConfig,
|
||||||
Description: gui.Tr.SLocalize("EditConfig"),
|
Description: gui.Tr.SLocalize("EditConfig"),
|
||||||
}, {
|
},
|
||||||
|
{
|
||||||
ViewName: "status",
|
ViewName: "status",
|
||||||
Key: 'o',
|
Key: 'o',
|
||||||
Modifier: gocui.ModNone,
|
Modifier: gocui.ModNone,
|
||||||
|
|
@ -124,23 +138,27 @@ func (gui *Gui) GetInitialKeybindings() []*Binding {
|
||||||
Key: gocui.KeyEsc,
|
Key: gocui.KeyEsc,
|
||||||
Modifier: gocui.ModNone,
|
Modifier: gocui.ModNone,
|
||||||
Handler: gui.handleMenuClose,
|
Handler: gui.handleMenuClose,
|
||||||
}, {
|
},
|
||||||
|
{
|
||||||
ViewName: "menu",
|
ViewName: "menu",
|
||||||
Key: 'q',
|
Key: 'q',
|
||||||
Modifier: gocui.ModNone,
|
Modifier: gocui.ModNone,
|
||||||
Handler: gui.handleMenuClose,
|
Handler: gui.handleMenuClose,
|
||||||
}, {
|
},
|
||||||
|
{
|
||||||
ViewName: "information",
|
ViewName: "information",
|
||||||
Key: gocui.MouseLeft,
|
Key: gocui.MouseLeft,
|
||||||
Modifier: gocui.ModNone,
|
Modifier: gocui.ModNone,
|
||||||
Handler: gui.handleDonate,
|
Handler: gui.handleDonate,
|
||||||
}, {
|
},
|
||||||
|
{
|
||||||
ViewName: "containers",
|
ViewName: "containers",
|
||||||
Key: '[',
|
Key: '[',
|
||||||
Modifier: gocui.ModNone,
|
Modifier: gocui.ModNone,
|
||||||
Handler: gui.handleContainersPrevContext,
|
Handler: gui.handleContainersPrevContext,
|
||||||
Description: gui.Tr.SLocalize("previousContext"),
|
Description: gui.Tr.SLocalize("previousContext"),
|
||||||
}, {
|
},
|
||||||
|
{
|
||||||
ViewName: "containers",
|
ViewName: "containers",
|
||||||
Key: ']',
|
Key: ']',
|
||||||
Modifier: gocui.ModNone,
|
Modifier: gocui.ModNone,
|
||||||
|
|
@ -230,7 +248,8 @@ func (gui *Gui) GetInitialKeybindings() []*Binding {
|
||||||
Modifier: gocui.ModNone,
|
Modifier: gocui.ModNone,
|
||||||
Handler: gui.handleImagesPrevContext,
|
Handler: gui.handleImagesPrevContext,
|
||||||
Description: gui.Tr.SLocalize("previousContext"),
|
Description: gui.Tr.SLocalize("previousContext"),
|
||||||
}, {
|
},
|
||||||
|
{
|
||||||
ViewName: "images",
|
ViewName: "images",
|
||||||
Key: ']',
|
Key: ']',
|
||||||
Modifier: gocui.ModNone,
|
Modifier: gocui.ModNone,
|
||||||
|
|
|
||||||
260
pkg/gui/layout.go
Normal file
260
pkg/gui/layout.go
Normal file
|
|
@ -0,0 +1,260 @@
|
||||||
|
package gui
|
||||||
|
|
||||||
|
import (
|
||||||
|
"github.com/jesseduffield/gocui"
|
||||||
|
"github.com/jesseduffield/lazydocker/pkg/utils"
|
||||||
|
)
|
||||||
|
|
||||||
|
// getFocusLayout returns a manager function for when view gain and lose focus
|
||||||
|
func (gui *Gui) getFocusLayout() func(g *gocui.Gui) error {
|
||||||
|
var previousView *gocui.View
|
||||||
|
return func(g *gocui.Gui) error {
|
||||||
|
newView := gui.g.CurrentView()
|
||||||
|
if err := gui.onFocusChange(); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
// for now we don't consider losing focus to a popup panel as actually losing focus
|
||||||
|
if newView != previousView && !gui.isPopupPanel(newView.Name()) {
|
||||||
|
if err := gui.onFocusLost(previousView, newView); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
if err := gui.onFocus(newView); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
previousView = newView
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (gui *Gui) onFocusChange() error {
|
||||||
|
currentView := gui.g.CurrentView()
|
||||||
|
for _, view := range gui.g.Views() {
|
||||||
|
view.Highlight = view == currentView
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (gui *Gui) onFocusLost(v *gocui.View, newView *gocui.View) error {
|
||||||
|
if v == nil {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
gui.Log.Info(v.Name() + " focus lost")
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (gui *Gui) onFocus(v *gocui.View) error {
|
||||||
|
if v == nil {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
gui.Log.Info(v.Name() + " focus gained")
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// layout is called for every screen re-render e.g. when the screen is resized
|
||||||
|
func (gui *Gui) layout(g *gocui.Gui) error {
|
||||||
|
g.Highlight = true
|
||||||
|
width, height := g.Size()
|
||||||
|
|
||||||
|
information := gui.Config.Version
|
||||||
|
|
||||||
|
minimumHeight := 9
|
||||||
|
minimumWidth := 10
|
||||||
|
if height < minimumHeight || width < minimumWidth {
|
||||||
|
v, err := g.SetView("limit", 0, 0, width-1, height-1, 0)
|
||||||
|
if err != nil {
|
||||||
|
if err.Error() != "unknown view" {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
v.Title = gui.Tr.SLocalize("NotEnoughSpace")
|
||||||
|
v.Wrap = true
|
||||||
|
_, _ = g.SetViewOnTop("limit")
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
currView := gui.g.CurrentView()
|
||||||
|
currentCyclebleView := gui.State.PreviousView
|
||||||
|
if currView != nil {
|
||||||
|
viewName := currView.Name()
|
||||||
|
usePreviouseView := true
|
||||||
|
for _, view := range cyclableViews {
|
||||||
|
if view == viewName {
|
||||||
|
currentCyclebleView = viewName
|
||||||
|
usePreviouseView = false
|
||||||
|
break
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if usePreviouseView {
|
||||||
|
currentCyclebleView = gui.State.PreviousView
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
usableSpace := height - 4
|
||||||
|
|
||||||
|
tallPanels := 3
|
||||||
|
|
||||||
|
vHeights := map[string]int{
|
||||||
|
"status": tallPanels,
|
||||||
|
"services": usableSpace/tallPanels + usableSpace%tallPanels,
|
||||||
|
"containers": usableSpace / tallPanels,
|
||||||
|
"images": usableSpace / tallPanels,
|
||||||
|
"options": 1,
|
||||||
|
}
|
||||||
|
|
||||||
|
if height < 28 {
|
||||||
|
defaultHeight := 3
|
||||||
|
if height < 21 {
|
||||||
|
defaultHeight = 1
|
||||||
|
}
|
||||||
|
vHeights = map[string]int{
|
||||||
|
"status": defaultHeight,
|
||||||
|
"services": defaultHeight,
|
||||||
|
"containers": defaultHeight,
|
||||||
|
"images": defaultHeight,
|
||||||
|
"options": defaultHeight,
|
||||||
|
}
|
||||||
|
vHeights[currentCyclebleView] = height - defaultHeight*tallPanels - 1
|
||||||
|
}
|
||||||
|
|
||||||
|
optionsVersionBoundary := width - max(len(utils.Decolorise(information)), 1)
|
||||||
|
leftSideWidth := width / 3
|
||||||
|
|
||||||
|
appStatus := gui.statusManager.getStatusString()
|
||||||
|
appStatusOptionsBoundary := 0
|
||||||
|
if appStatus != "" {
|
||||||
|
appStatusOptionsBoundary = len(appStatus) + 2
|
||||||
|
}
|
||||||
|
|
||||||
|
_, _ = g.SetViewOnBottom("limit")
|
||||||
|
g.DeleteView("limit")
|
||||||
|
|
||||||
|
v, err := g.SetView("main", leftSideWidth+1, 0, width-1, height-2, gocui.LEFT)
|
||||||
|
if err != nil {
|
||||||
|
if err.Error() != "unknown view" {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
v.Wrap = true
|
||||||
|
v.FgColor = gocui.ColorWhite
|
||||||
|
}
|
||||||
|
|
||||||
|
if v, err := g.SetView("status", 0, 0, leftSideWidth, vHeights["status"]-1, gocui.BOTTOM|gocui.RIGHT); err != nil {
|
||||||
|
if err.Error() != "unknown view" {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
v.Title = gui.Tr.SLocalize("StatusTitle")
|
||||||
|
v.FgColor = gocui.ColorWhite
|
||||||
|
}
|
||||||
|
|
||||||
|
servicesView, err := g.SetViewBeneath("services", "status", vHeights["services"])
|
||||||
|
if err != nil {
|
||||||
|
if err.Error() != "unknown view" {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
servicesView.Highlight = true
|
||||||
|
servicesView.Title = gui.Tr.SLocalize("ServicesTitle")
|
||||||
|
servicesView.FgColor = gocui.ColorWhite
|
||||||
|
|
||||||
|
gui.focusPoint(0, gui.State.Panels.Services.SelectedLine, len(gui.DockerCommand.Services), servicesView)
|
||||||
|
}
|
||||||
|
|
||||||
|
containersView, err := g.SetViewBeneath("containers", "services", vHeights["containers"])
|
||||||
|
if err != nil {
|
||||||
|
if err.Error() != "unknown view" {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
containersView.Highlight = true
|
||||||
|
containersView.Title = gui.Tr.SLocalize("ContainersTitle")
|
||||||
|
containersView.FgColor = gocui.ColorWhite
|
||||||
|
|
||||||
|
gui.focusPoint(0, gui.State.Panels.Containers.SelectedLine, len(gui.DockerCommand.Containers), containersView)
|
||||||
|
}
|
||||||
|
|
||||||
|
imagesView, err := g.SetViewBeneath("images", "containers", vHeights["images"])
|
||||||
|
if err != nil {
|
||||||
|
if err.Error() != "unknown view" {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
imagesView.Highlight = true
|
||||||
|
imagesView.Title = gui.Tr.SLocalize("ImagesTitle")
|
||||||
|
imagesView.FgColor = gocui.ColorWhite
|
||||||
|
|
||||||
|
gui.focusPoint(0, gui.State.Panels.Images.SelectedLine, len(gui.DockerCommand.Images), imagesView)
|
||||||
|
}
|
||||||
|
|
||||||
|
if v, err := g.SetView("options", appStatusOptionsBoundary-1, height-2, optionsVersionBoundary-1, height, 0); err != nil {
|
||||||
|
if err.Error() != "unknown view" {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
v.Frame = false
|
||||||
|
if v.FgColor, err = gui.GetOptionsPanelTextColor(); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if appStatusView, err := g.SetView("appStatus", -1, height-2, width, height, 0); err != nil {
|
||||||
|
if err.Error() != "unknown view" {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
appStatusView.BgColor = gocui.ColorDefault
|
||||||
|
appStatusView.FgColor = gocui.ColorCyan
|
||||||
|
appStatusView.Frame = false
|
||||||
|
if _, err := g.SetViewOnBottom("appStatus"); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if v, err := g.SetView("information", optionsVersionBoundary-1, height-2, width, height, 0); err != nil {
|
||||||
|
if err.Error() != "unknown view" {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
v.BgColor = gocui.ColorDefault
|
||||||
|
v.FgColor = gocui.ColorGreen
|
||||||
|
v.Frame = false
|
||||||
|
if err := gui.renderString(g, "information", information); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
// doing this here because it'll only happen once
|
||||||
|
if err := gui.loadNewDirectory(); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if gui.g.CurrentView() == nil {
|
||||||
|
if _, err := gui.g.SetCurrentView(gui.getContainersView().Name()); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
if err := gui.switchFocus(gui.g, nil, gui.getServicesView()); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
type listViewState struct {
|
||||||
|
selectedLine int
|
||||||
|
lineCount int
|
||||||
|
}
|
||||||
|
|
||||||
|
listViews := map[*gocui.View]listViewState{
|
||||||
|
containersView: {selectedLine: gui.State.Panels.Containers.SelectedLine, lineCount: len(gui.DockerCommand.Containers)},
|
||||||
|
imagesView: {selectedLine: gui.State.Panels.Images.SelectedLine, lineCount: len(gui.DockerCommand.Images)},
|
||||||
|
}
|
||||||
|
|
||||||
|
// menu view might not exist so we check to be safe
|
||||||
|
if menuView, err := gui.g.View("menu"); err == nil {
|
||||||
|
listViews[menuView] = listViewState{selectedLine: gui.State.Panels.Menu.SelectedLine, lineCount: gui.State.MenuItemCount}
|
||||||
|
}
|
||||||
|
for view, state := range listViews {
|
||||||
|
// check if the selected line is now out of view and if so refocus it
|
||||||
|
if err := gui.focusPoint(0, state.selectedLine, state.lineCount, view); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// here is a good place log some stuff
|
||||||
|
// if you download humanlog and do tail -f development.log | humanlog
|
||||||
|
// this will let you see these branches as prettified json
|
||||||
|
// gui.Log.Info(utils.AsJson(gui.State.Branches[0:4]))
|
||||||
|
return gui.resizeCurrentPopupPanel(g)
|
||||||
|
}
|
||||||
33
pkg/gui/main_panel.go
Normal file
33
pkg/gui/main_panel.go
Normal file
|
|
@ -0,0 +1,33 @@
|
||||||
|
package gui
|
||||||
|
|
||||||
|
import (
|
||||||
|
"math"
|
||||||
|
|
||||||
|
"github.com/jesseduffield/gocui"
|
||||||
|
)
|
||||||
|
|
||||||
|
func (gui *Gui) scrollUpMain(g *gocui.Gui, v *gocui.View) error {
|
||||||
|
mainView, _ := g.View("main")
|
||||||
|
mainView.Autoscroll = false
|
||||||
|
ox, oy := mainView.Origin()
|
||||||
|
newOy := int(math.Max(0, float64(oy-gui.Config.UserConfig.Gui.ScrollHeight)))
|
||||||
|
return mainView.SetOrigin(ox, newOy)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (gui *Gui) scrollDownMain(g *gocui.Gui, v *gocui.View) error {
|
||||||
|
mainView, _ := g.View("main")
|
||||||
|
ox, oy := mainView.Origin()
|
||||||
|
y := oy
|
||||||
|
if !gui.Config.UserConfig.Gui.ScrollPastBottom {
|
||||||
|
_, sy := mainView.Size()
|
||||||
|
y += sy
|
||||||
|
}
|
||||||
|
// for some reason we can't work out whether we've hit the bottomq
|
||||||
|
// there is a large discrepancy in the origin's y value and the length of BufferLines
|
||||||
|
return mainView.SetOrigin(ox, oy+gui.Config.UserConfig.Gui.ScrollHeight)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (gui *Gui) autoScrollMain(g *gocui.Gui, v *gocui.View) error {
|
||||||
|
gui.getMainView().Autoscroll = true
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
@ -62,9 +62,9 @@ func (gui *Gui) runCommand() error {
|
||||||
gui.SubProcess.Stdin = nil
|
gui.SubProcess.Stdin = nil
|
||||||
gui.SubProcess = nil
|
gui.SubProcess = nil
|
||||||
|
|
||||||
// fmt.Fprintf(os.Stdout, "\n%s", utils.ColoredString(gui.Tr.SLocalize("pressEnterToReturn"), color.FgGreen))
|
fmt.Fprintf(os.Stdout, "\n%s", utils.ColoredString(gui.Tr.SLocalize("pressEnterToReturn"), color.FgGreen))
|
||||||
|
|
||||||
// fmt.Scanln() // wait for enter press
|
fmt.Scanln() // wait for enter press
|
||||||
|
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -868,5 +868,9 @@ func addEnglish(i18nObject *i18n.Bundle) error {
|
||||||
ID: "StopService",
|
ID: "StopService",
|
||||||
Other: "Are you sure you want to stop this service's containers? (enter/esc)",
|
Other: "Are you sure you want to stop this service's containers? (enter/esc)",
|
||||||
},
|
},
|
||||||
|
&i18n.Message{
|
||||||
|
ID: "pressEnterToReturn",
|
||||||
|
Other: "Press enter to return to lazydocker",
|
||||||
|
},
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -58,7 +58,7 @@ func (t *Task) Stop() {
|
||||||
// NewTickerTask is a convenience function for making a new task that repeats some action once per e.g. second
|
// NewTickerTask is a convenience function for making a new task that repeats some action once per e.g. second
|
||||||
func (t *TaskManager) NewTickerTask(duration time.Duration, f func()) error {
|
func (t *TaskManager) NewTickerTask(duration time.Duration, f func()) error {
|
||||||
return t.NewTask(func(stop chan struct{}) {
|
return t.NewTask(func(stop chan struct{}) {
|
||||||
tickChan := time.NewTicker(time.Second)
|
tickChan := time.NewTicker(duration)
|
||||||
f() // calling f first so that we're not waiting for the first tick
|
f() // calling f first so that we're not waiting for the first tick
|
||||||
for {
|
for {
|
||||||
select {
|
select {
|
||||||
|
|
|
||||||
Loading…
Add table
Reference in a new issue