mirror of
https://github.com/jesseduffield/lazydocker.git
synced 2026-07-22 15:11:02 +00:00
335 lines
9.1 KiB
Go
335 lines
9.1 KiB
Go
package gui
|
|
|
|
import (
|
|
"sync"
|
|
|
|
// "io"
|
|
// "io/ioutil"
|
|
|
|
"os/exec"
|
|
"time"
|
|
|
|
"github.com/go-errors/errors"
|
|
|
|
// "strings"
|
|
|
|
"github.com/jesseduffield/gocui"
|
|
"github.com/jesseduffield/lazydocker/pkg/commands"
|
|
"github.com/jesseduffield/lazydocker/pkg/config"
|
|
"github.com/jesseduffield/lazydocker/pkg/i18n"
|
|
"github.com/jesseduffield/lazydocker/pkg/tasks"
|
|
"github.com/sirupsen/logrus"
|
|
)
|
|
|
|
// OverlappingEdges determines if panel edges overlap
|
|
var OverlappingEdges = false
|
|
|
|
// SentinelErrors are the errors that have special meaning and need to be checked
|
|
// by calling functions. The less of these, the better
|
|
type SentinelErrors struct {
|
|
ErrSubProcess error
|
|
ErrNoContainers error
|
|
ErrNoImages error
|
|
}
|
|
|
|
// GenerateSentinelErrors makes the sentinel errors for the gui. We're defining it here
|
|
// because we can't do package-scoped errors with localization, and also because
|
|
// it seems like package-scoped variables are bad in general
|
|
// https://dave.cheney.net/2017/06/11/go-without-package-scoped-variables
|
|
// In the future it would be good to implement some of the recommendations of
|
|
// that article. For now, if we don't need an error to be a sentinel, we will just
|
|
// define it inline. This has implications for error messages that pop up everywhere
|
|
// in that we'll be duplicating the default values. We may need to look at
|
|
// having a default localisation bundle defined, and just using keys-only when
|
|
// localising things in the code.
|
|
func (gui *Gui) GenerateSentinelErrors() {
|
|
gui.Errors = SentinelErrors{
|
|
ErrSubProcess: errors.New(gui.Tr.SLocalize("RunningSubprocess")),
|
|
ErrNoContainers: errors.New(gui.Tr.SLocalize("NoContainers")),
|
|
ErrNoImages: errors.New(gui.Tr.SLocalize("NoImages")),
|
|
}
|
|
}
|
|
|
|
// Teml is short for template used to make the required map[string]interface{} shorter when using gui.Tr.SLocalize and gui.Tr.TemplateLocalize
|
|
type Teml i18n.Teml
|
|
|
|
// Gui wraps the gocui Gui object which handles rendering and events
|
|
type Gui struct {
|
|
g *gocui.Gui
|
|
Log *logrus.Entry
|
|
DockerCommand *commands.DockerCommand
|
|
OSCommand *commands.OSCommand
|
|
SubProcess *exec.Cmd
|
|
State guiState
|
|
Config *config.AppConfig
|
|
Tr *i18n.Localizer
|
|
Errors SentinelErrors
|
|
statusManager *statusManager
|
|
waitForIntro sync.WaitGroup
|
|
T *tasks.TaskManager
|
|
ErrorChan chan error
|
|
}
|
|
|
|
type servicePanelState struct {
|
|
SelectedLine int
|
|
ContextIndex int // for specifying if you are looking at logs/stats/config/etc
|
|
}
|
|
|
|
type containerPanelState struct {
|
|
SelectedLine int
|
|
ContextIndex int // for specifying if you are looking at logs/stats/config/etc
|
|
}
|
|
|
|
type menuPanelState struct {
|
|
SelectedLine int
|
|
}
|
|
|
|
type mainPanelState struct {
|
|
ObjectKey string
|
|
}
|
|
|
|
type imagePanelState struct {
|
|
SelectedLine int
|
|
ContextIndex int // for specifying if you are looking at logs/stats/config/etc
|
|
}
|
|
|
|
type panelStates struct {
|
|
Services *servicePanelState
|
|
Containers *containerPanelState
|
|
Menu *menuPanelState
|
|
Main *mainPanelState
|
|
Images *imagePanelState
|
|
}
|
|
|
|
type guiState struct {
|
|
MenuItemCount int // can't store the actual list because it's of interface{} type
|
|
PreviousView string
|
|
Platform commands.Platform
|
|
Panels *panelStates
|
|
SubProcessOutput string
|
|
Stats map[string]commands.ContainerStats
|
|
}
|
|
|
|
// NewGui builds a new gui handler
|
|
func NewGui(log *logrus.Entry, dockerCommand *commands.DockerCommand, oSCommand *commands.OSCommand, tr *i18n.Localizer, config *config.AppConfig, errorChan chan error) (*Gui, error) {
|
|
|
|
initialState := guiState{
|
|
PreviousView: "services",
|
|
Platform: *oSCommand.Platform,
|
|
Panels: &panelStates{
|
|
Services: &servicePanelState{SelectedLine: -1, ContextIndex: 0},
|
|
Containers: &containerPanelState{SelectedLine: -1, ContextIndex: 0},
|
|
Images: &imagePanelState{SelectedLine: -1, ContextIndex: 0},
|
|
Menu: &menuPanelState{SelectedLine: 0},
|
|
Main: &mainPanelState{
|
|
ObjectKey: "",
|
|
},
|
|
},
|
|
}
|
|
|
|
gui := &Gui{
|
|
Log: log,
|
|
DockerCommand: dockerCommand,
|
|
OSCommand: oSCommand,
|
|
// TODO: look into this warning
|
|
State: initialState,
|
|
Config: config,
|
|
Tr: tr,
|
|
statusManager: &statusManager{},
|
|
T: tasks.NewTaskManager(log),
|
|
ErrorChan: errorChan,
|
|
}
|
|
|
|
gui.GenerateSentinelErrors()
|
|
|
|
return gui, nil
|
|
}
|
|
|
|
func (gui *Gui) handleRefresh(g *gocui.Gui, v *gocui.View) error {
|
|
return gui.refreshSidePanels(g)
|
|
}
|
|
|
|
func max(a, b int) int {
|
|
if a > b {
|
|
return a
|
|
}
|
|
return b
|
|
}
|
|
|
|
func (gui *Gui) loadNewDirectory() error {
|
|
gui.waitForIntro.Done()
|
|
|
|
if err := gui.refreshSidePanels(gui.g); err != nil {
|
|
return err
|
|
}
|
|
|
|
if gui.Config.UserConfig.Reporting == "undetermined" {
|
|
if err := gui.promptAnonymousReporting(); err != nil {
|
|
return err
|
|
}
|
|
}
|
|
return nil
|
|
}
|
|
|
|
func (gui *Gui) promptAnonymousReporting() error {
|
|
return gui.createConfirmationPanel(gui.g, nil, gui.Tr.SLocalize("AnonymousReportingTitle"), gui.Tr.SLocalize("AnonymousReportingPrompt"), func(g *gocui.Gui, v *gocui.View) error {
|
|
gui.waitForIntro.Done()
|
|
return gui.Config.WriteToUserConfig(func(userConfig *config.UserConfig) error {
|
|
userConfig.Reporting = "on"
|
|
return nil
|
|
})
|
|
}, func(g *gocui.Gui, v *gocui.View) error {
|
|
gui.waitForIntro.Done()
|
|
return gui.Config.WriteToUserConfig(func(userConfig *config.UserConfig) error {
|
|
userConfig.Reporting = "off"
|
|
return nil
|
|
})
|
|
})
|
|
}
|
|
|
|
func (gui *Gui) renderAppStatus() error {
|
|
appStatus := gui.statusManager.getStatusString()
|
|
if appStatus != "" {
|
|
return gui.renderString(gui.g, "appStatus", appStatus)
|
|
}
|
|
return nil
|
|
}
|
|
|
|
func (gui *Gui) renderGlobalOptions() error {
|
|
return gui.renderOptionsMap(map[string]string{
|
|
"PgUp/PgDn": gui.Tr.SLocalize("scroll"),
|
|
"← → ↑ ↓": gui.Tr.SLocalize("navigate"),
|
|
"esc/q": gui.Tr.SLocalize("close"),
|
|
"x": gui.Tr.SLocalize("menu"),
|
|
})
|
|
}
|
|
|
|
func (gui *Gui) goEvery(interval time.Duration, function func() error) {
|
|
_ = function() // time.Tick doesn't run immediately so we'll do that here // TODO: maybe change
|
|
go func() {
|
|
for range time.Tick(interval) {
|
|
_ = function()
|
|
}
|
|
}()
|
|
}
|
|
|
|
// Run setup the gui with keybindings and start the mainloop
|
|
func (gui *Gui) Run() error {
|
|
g, err := gocui.NewGui(gocui.OutputNormal, OverlappingEdges)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
defer g.Close()
|
|
|
|
if gui.Config.UserConfig.Gui.MouseEvents {
|
|
g.Mouse = true
|
|
}
|
|
|
|
gui.g = g // TODO: always use gui.g rather than passing g around everywhere
|
|
|
|
if err := gui.SetColorScheme(); err != nil {
|
|
return err
|
|
}
|
|
|
|
if gui.Config.UserConfig.Reporting == "undetermined" {
|
|
gui.waitForIntro.Add(2)
|
|
} else {
|
|
gui.waitForIntro.Add(1)
|
|
}
|
|
|
|
go func() {
|
|
gui.waitForIntro.Wait()
|
|
gui.goEvery(time.Millisecond*50, gui.renderAppStatus)
|
|
gui.goEvery(time.Millisecond*30, gui.reRenderMain)
|
|
gui.goEvery(time.Millisecond*100, gui.refreshContainersAndServices)
|
|
gui.goEvery(time.Millisecond*1000, gui.DockerCommand.UpdateContainerDetails)
|
|
}()
|
|
|
|
go gui.DockerCommand.MonitorContainerStats()
|
|
|
|
go func() {
|
|
for err := range gui.ErrorChan {
|
|
gui.createErrorPanel(gui.g, err.Error())
|
|
}
|
|
}()
|
|
|
|
g.SetManager(gocui.ManagerFunc(gui.layout), gocui.ManagerFunc(gui.getFocusLayout()))
|
|
|
|
if err = gui.keybindings(g); err != nil {
|
|
return err
|
|
}
|
|
|
|
err = g.MainLoop()
|
|
return err
|
|
}
|
|
|
|
func (gui *Gui) reRenderMain() error {
|
|
mainView := gui.getMainView()
|
|
if mainView == nil {
|
|
return nil
|
|
}
|
|
if mainView.IsTainted() {
|
|
gui.g.Update(func(g *gocui.Gui) error {
|
|
return nil
|
|
})
|
|
}
|
|
return nil
|
|
}
|
|
|
|
func (gui *Gui) quit(g *gocui.Gui, v *gocui.View) error {
|
|
if gui.Config.UserConfig.ConfirmOnQuit {
|
|
return gui.createConfirmationPanel(g, v, "", gui.Tr.SLocalize("ConfirmQuit"), func(g *gocui.Gui, v *gocui.View) error {
|
|
return gocui.ErrQuit
|
|
}, nil)
|
|
}
|
|
return gocui.ErrQuit
|
|
}
|
|
|
|
func (gui *Gui) handleDonate(g *gocui.Gui, v *gocui.View) error {
|
|
if !gui.g.Mouse {
|
|
return nil
|
|
}
|
|
|
|
cx, _ := v.Cursor()
|
|
if cx > len(gui.Tr.SLocalize("Donate")) {
|
|
return nil
|
|
}
|
|
return gui.OSCommand.OpenLink("https://donorbox.org/lazydocker")
|
|
}
|
|
|
|
func (gui *Gui) editFile(filename string) error {
|
|
_, err := gui.runSyncOrAsyncCommand(gui.OSCommand.EditFile(filename))
|
|
return err
|
|
}
|
|
|
|
func (gui *Gui) openFile(filename string) error {
|
|
if err := gui.OSCommand.OpenFile(filename); err != nil {
|
|
return gui.createErrorPanel(gui.g, err.Error())
|
|
}
|
|
return nil
|
|
}
|
|
|
|
// runSyncOrAsyncCommand takes the output of a command that may have returned
|
|
// either no error, an error, or a subprocess to execute, and if a subprocess
|
|
// needs to be set on the gui object, it does so, and then returns the error
|
|
// the bool returned tells us whether the calling code should continue
|
|
func (gui *Gui) runSyncOrAsyncCommand(sub *exec.Cmd, err error) (bool, error) {
|
|
if err != nil {
|
|
if err != gui.Errors.ErrSubProcess {
|
|
return false, gui.createErrorPanel(gui.g, err.Error())
|
|
}
|
|
}
|
|
if sub != nil {
|
|
gui.SubProcess = sub
|
|
return false, gui.Errors.ErrSubProcess
|
|
}
|
|
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
|
|
})
|
|
}
|