diff --git a/pkg/gui/arrangement.go b/pkg/gui/arrangement.go index bcd4aa1..610e6b1 100644 --- a/pkg/gui/arrangement.go +++ b/pkg/gui/arrangement.go @@ -35,6 +35,25 @@ func (gui *Gui) getWindowDimensions(informationStr string, appStatus string) map infoSectionSize = 1 } + mainBox := &boxlayout.Box{ + Direction: boxlayout.ROW, + Weight: mainSectionWeight, + Children: []*boxlayout.Box{ + { + Window: "main", + Weight: 1, + }, + }, + } + + showMainInfoView := true + if showMainInfoView { + mainBox.Children = utils.Append(mainBox.Children, &boxlayout.Box{ + Window: "mainInfo", + Size: 3, + }) + } + root := &boxlayout.Box{ Direction: boxlayout.ROW, Children: []*boxlayout.Box{ @@ -47,10 +66,7 @@ func (gui *Gui) getWindowDimensions(informationStr string, appStatus string) map Weight: sideSectionWeight, ConditionalChildren: gui.sidePanelChildren, }, - { - Window: "main", - Weight: mainSectionWeight, - }, + mainBox, }, }, { diff --git a/pkg/gui/confirmation_panel.go b/pkg/gui/confirmation_panel.go index 0db7e78..e961e48 100644 --- a/pkg/gui/confirmation_panel.go +++ b/pkg/gui/confirmation_panel.go @@ -62,14 +62,21 @@ func (gui *Gui) getConfirmationPanelDimensions(wrap bool, prompt string) (int, i height/2 + panelHeight/2 } -func (gui *Gui) createPromptPanel(title string, handleConfirm func(*gocui.Gui, *gocui.View) error) error { +func (gui *Gui) Prompt(title string, handleConfirm func(string) error) error { + gui.Views.Confirmation.ClearTextArea() + + wrappedHandleConfirm := func(g *gocui.Gui, v *gocui.View) error { + input := gui.trimmedContent(v) + return handleConfirm(input) + } + gui.onNewPopupPanel() err := gui.prepareConfirmationPanel(title, "", false) if err != nil { return err } gui.Views.Confirmation.Editable = true - return gui.setKeyBindings(gui.g, handleConfirm, nil) + return gui.setKeyBindings(gui.g, wrappedHandleConfirm, nil) } func (gui *Gui) prepareConfirmationPanel(title, prompt string, hasLoader bool) error { diff --git a/pkg/gui/container_logs.go b/pkg/gui/container_logs.go index bb2feb8..1a6b441 100644 --- a/pkg/gui/container_logs.go +++ b/pkg/gui/container_logs.go @@ -24,7 +24,7 @@ func (gui *Gui) renderContainerLogsToMain(container *commands.Container) tasks.T Duration: time.Millisecond * 200, // TODO: see why this isn't working (when switching from Top tab to Logs tab in the services panel, the tops tab's content isn't removed) Before: func(ctx context.Context) { gui.clearMainView() }, - Wrap: gui.Config.UserConfig.Gui.WrapMainPanel, + Wrap: gui.State.LogConfig.Wrap, Autoscroll: true, }) } @@ -37,7 +37,7 @@ func (gui *Gui) renderContainerLogsToMainAux(container *commands.Container, ctx mainView := gui.Views.Main - if err := gui.writeContainerLogs(container, ctx, mainView); err != nil { + if err := gui.writeContainerLogs(container, ctx, mainView, gui.State.LogConfig); err != nil { gui.Log.Error(err) } @@ -85,7 +85,7 @@ func (gui *Gui) renderLogsToStdout(container *commands.Container) { } }() - if err := gui.writeContainerLogs(container, ctx, os.Stdout); err != nil { + if err := gui.writeContainerLogs(container, ctx, os.Stdout, gui.State.LogConfig); err != nil { gui.Log.Error(err) return } @@ -104,13 +104,13 @@ func (gui *Gui) promptToReturn() { } } -func (gui *Gui) writeContainerLogs(container *commands.Container, ctx context.Context, writer io.Writer) error { +func (gui *Gui) writeContainerLogs(container *commands.Container, ctx context.Context, writer io.Writer, logConfig LogConfig) error { readCloser, err := gui.DockerCommand.Client.ContainerLogs(ctx, container.ID, dockerTypes.ContainerLogsOptions{ ShowStdout: true, ShowStderr: true, - Timestamps: gui.Config.UserConfig.Logs.Timestamps, - Since: gui.Config.UserConfig.Logs.Since, - Tail: gui.Config.UserConfig.Logs.Tail, + Timestamps: logConfig.Timestamps, + Since: logConfig.Since, + Tail: logConfig.Tail, Follow: true, }) if err != nil { diff --git a/pkg/gui/containers_panel.go b/pkg/gui/containers_panel.go index 862fc4b..f3d2900 100644 --- a/pkg/gui/containers_panel.go +++ b/pkg/gui/containers_panel.go @@ -69,7 +69,7 @@ func (gui *Gui) getContainersPanel() *panels.SideListPanel[*commands.Container] // where a container restarts but the new logs don't get read. // Note that this might be jarring if we have a lot of logs and the container // restarts a lot, so let's keep an eye on it. - return "containers-" + container.ID + "-" + container.Container.State + return "containers-" + container.ID + "-" + container.Container.State + gui.logArgsKey() }, }, ListPanel: panels.ListPanel[*commands.Container]{ @@ -239,7 +239,7 @@ func (gui *Gui) renderContainerTop(container *commands.Container) tasks.TaskFunc }, Duration: time.Second, Before: func(ctx context.Context) { gui.clearMainView() }, - Wrap: gui.Config.UserConfig.Gui.WrapMainPanel, + Wrap: gui.State.LogConfig.Wrap, Autoscroll: false, }) } diff --git a/pkg/gui/gui.go b/pkg/gui/gui.go index 1ffc889..dd7a1ac 100644 --- a/pkg/gui/gui.go +++ b/pkg/gui/gui.go @@ -89,6 +89,16 @@ type guiState struct { // Maintains the state of manual filtering i.e. typing in a substring // to filter on in the current panel. Filter filterState + + LogConfig LogConfig +} + +type LogConfig struct { + Tail string + Since string + Autoscroll bool + Timestamps bool + Wrap bool } type filterState struct { @@ -123,7 +133,13 @@ func NewGui(log *logrus.Entry, dockerCommand *commands.DockerCommand, oSCommand }, }, ViewStack: []string{}, - + LogConfig: LogConfig{ + Tail: config.UserConfig.Logs.Tail, + Since: config.UserConfig.Logs.Since, + Timestamps: config.UserConfig.Logs.Timestamps, + Wrap: config.UserConfig.Gui.WrapMainPanel, + Autoscroll: true, + }, ShowExitedContainers: true, } @@ -424,9 +440,8 @@ func (gui *Gui) openFile(filename string) error { } func (gui *Gui) handleCustomCommand(g *gocui.Gui, v *gocui.View) error { - return gui.createPromptPanel(gui.Tr.CustomCommandTitle, func(g *gocui.Gui, v *gocui.View) error { - command := gui.trimmedContent(v) - return gui.runSubprocess(gui.OSCommand.RunCustomCommand(command)) + return gui.Prompt(gui.Tr.CustomCommandTitle, func(input string) error { + return gui.runSubprocess(gui.OSCommand.RunCustomCommand(input)) }) } diff --git a/pkg/gui/keybindings.go b/pkg/gui/keybindings.go index 4c08646..225a366 100644 --- a/pkg/gui/keybindings.go +++ b/pkg/gui/keybindings.go @@ -452,6 +452,13 @@ func (gui *Gui) GetInitialKeybindings() []*Binding { Modifier: gocui.ModNone, Handler: wrappedHandler(gui.escapeFilterPrompt), }, + { + ViewName: "", + Key: 'i', + Modifier: gocui.ModNone, + Handler: wrappedHandler(gui.handleOpenLogMenu), + Description: "Set log args (tail, since, etc)", + }, { ViewName: "", Key: 'J', diff --git a/pkg/gui/layout.go b/pkg/gui/layout.go index ea28f50..8e43975 100644 --- a/pkg/gui/layout.go +++ b/pkg/gui/layout.go @@ -108,6 +108,10 @@ func (gui *Gui) layout(g *gocui.Gui) error { } } + if err := gui.renderMainInfoView(); 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 diff --git a/pkg/gui/main_info_view.go b/pkg/gui/main_info_view.go new file mode 100644 index 0000000..4ce4037 --- /dev/null +++ b/pkg/gui/main_info_view.go @@ -0,0 +1,102 @@ +package gui + +import ( + "fmt" + + "github.com/fatih/color" + "github.com/jesseduffield/lazydocker/pkg/gui/types" +) + +func (gui *Gui) renderMainInfoView() error { + str := "" + keyColorSprintFn := color.New(color.FgMagenta).SprintFunc() + valueColorSprintFn := color.New(color.FgGreen).SprintFunc() + + keyVal := func(key, value string) string { + return fmt.Sprintf("%s:%s ", keyColorSprintFn(key), valueColorSprintFn(value)) + } + str += keyVal("Tail", blankToNil(gui.State.LogConfig.Tail)) + str += keyVal("Since", blankToNil(gui.State.LogConfig.Since)) + str += keyVal("Timestamps", boolToStr(gui.State.LogConfig.Timestamps)) + str += keyVal("Wrap", boolToStr(gui.State.LogConfig.Wrap)) + str += keyVal("Autoscroll", boolToStr(gui.Views.Main.Autoscroll)) + + return gui.renderString(gui.g, "mainInfo", str) +} + +func boolToStr(b bool) string { + if b { + return "On" + } + return "Off" +} + +func blankToNil(s string) string { + if s == "" { + return "-" + } + return s +} + +func (gui *Gui) reselectSideListItem() error { + currentSidePanel, ok := gui.currentSidePanel() + if ok { + if err := currentSidePanel.HandleSelect(); err != nil { + return err + } + } + return nil +} + +func (gui *Gui) handleOpenLogMenu() error { + return gui.Menu(CreateMenuOptions{ + Title: "Log Options", + Items: []*types.MenuItem{ + { + Label: "Set tail", + OnPress: func() error { + return gui.Prompt("Set tail value (e.g. 200). Unset with empty value", func(input string) error { + gui.State.LogConfig.Tail = input + return gui.reselectSideListItem() + }) + }, + }, + { + Label: "Set since", + OnPress: func() error { + return gui.Prompt("Set since value (e.g. 60m). Unset with empty value", func(input string) error { + gui.State.LogConfig.Since = input + return gui.reselectSideListItem() + }) + }, + }, + { + Label: "Toggle timestamps", + OnPress: func() error { + gui.State.LogConfig.Timestamps = !gui.State.LogConfig.Timestamps + return gui.reselectSideListItem() + }, + }, + { + Label: "Toggle wrap", + OnPress: func() error { + gui.State.LogConfig.Wrap = !gui.State.LogConfig.Wrap + return gui.reselectSideListItem() + }, + }, + { + Label: "Toggle autoscroll", + OnPress: func() error { + gui.Views.Main.Autoscroll = !gui.Views.Main.Autoscroll + // the view will refresh automatically + return nil + }, + }, + }, + }) +} + +func (gui *Gui) logArgsKey() string { + // not including autoscroll because that doesn't require refetching the logs + return gui.State.LogConfig.Since + gui.State.LogConfig.Tail + boolToStr(gui.State.LogConfig.Timestamps) + boolToStr(gui.State.LogConfig.Wrap) +} diff --git a/pkg/gui/project_panel.go b/pkg/gui/project_panel.go index bfc829f..a1ea209 100644 --- a/pkg/gui/project_panel.go +++ b/pkg/gui/project_panel.go @@ -52,7 +52,7 @@ func (gui *Gui) getProjectPanel() *panels.SideListPanel[*commands.Project] { } }, GetItemContextCacheKey: func(project *commands.Project) string { - return "projects-" + project.Name + return "projects-" + project.Name + gui.logArgsKey() }, }, @@ -115,7 +115,7 @@ func (gui *Gui) creditsStr() string { func (gui *Gui) renderAllLogs(_project *commands.Project) tasks.TaskFunc { return gui.NewTask(TaskOpts{ Autoscroll: true, - Wrap: gui.Config.UserConfig.Gui.WrapMainPanel, + Wrap: gui.State.LogConfig.Wrap, Func: func(ctx context.Context) { gui.clearMainView() diff --git a/pkg/gui/services_panel.go b/pkg/gui/services_panel.go index 2c1caae..1f3b4a0 100644 --- a/pkg/gui/services_panel.go +++ b/pkg/gui/services_panel.go @@ -50,10 +50,13 @@ func (gui *Gui) getServicesPanel() *panels.SideListPanel[*commands.Service] { } }, GetItemContextCacheKey: func(service *commands.Service) string { + key := "services-" + service.ID + "-" + gui.logArgsKey() + if service.Container == nil { - return "services-" + service.ID + return key } - return "services-" + service.ID + "-" + service.Container.ID + "-" + service.Container.Container.State + + return key + service.Container.ID + "-" + service.Container.Container.State }, }, ListPanel: panels.ListPanel[*commands.Service]{ @@ -117,7 +120,7 @@ func (gui *Gui) renderServiceTop(service *commands.Service) tasks.TaskFunc { }, Duration: time.Second, Before: func(ctx context.Context) { gui.clearMainView() }, - Wrap: gui.Config.UserConfig.Gui.WrapMainPanel, + Wrap: gui.State.LogConfig.Wrap, Autoscroll: false, }) } diff --git a/pkg/gui/tasks_adapter.go b/pkg/gui/tasks_adapter.go index f85e472..11aead4 100644 --- a/pkg/gui/tasks_adapter.go +++ b/pkg/gui/tasks_adapter.go @@ -48,7 +48,7 @@ func (gui *Gui) NewSimpleRenderStringTask(getContent func() string) tasks.TaskFu return gui.NewRenderStringTask(RenderStringTaskOpts{ GetStrContent: getContent, Autoscroll: false, - Wrap: gui.Config.UserConfig.Gui.WrapMainPanel, + Wrap: gui.State.LogConfig.Wrap, }) } diff --git a/pkg/gui/view_helpers.go b/pkg/gui/view_helpers.go index 8ddae58..3fe7e03 100644 --- a/pkg/gui/view_helpers.go +++ b/pkg/gui/view_helpers.go @@ -66,7 +66,7 @@ func (gui *Gui) previousView(g *gocui.Gui, v *gocui.View) error { func (gui *Gui) resetMainView() { gui.State.Panels.Main.ObjectKey = "" - gui.Views.Main.Wrap = gui.Config.UserConfig.Gui.WrapMainPanel + gui.Views.Main.Wrap = gui.State.LogConfig.Wrap } // if the cursor down past the last item, move it to the last line diff --git a/pkg/gui/views.go b/pkg/gui/views.go index 05b9db5..6bac61f 100644 --- a/pkg/gui/views.go +++ b/pkg/gui/views.go @@ -36,6 +36,9 @@ type Views struct { // main panel Main *gocui.View + // shows information about what's being rendered in the main panel (e.g. `docker logs` tail arg) + MainInfo *gocui.View + // bottom line Options *gocui.View Information *gocui.View @@ -72,6 +75,7 @@ func (gui *Gui) orderedViewNameMappings() []viewNameMapping { {viewPtr: &gui.Views.Volumes, name: "volumes", autoPosition: true}, {viewPtr: &gui.Views.Main, name: "main", autoPosition: true}, + {viewPtr: &gui.Views.MainInfo, name: "mainInfo", autoPosition: true}, // bottom line {viewPtr: &gui.Views.Options, name: "options", autoPosition: true}, diff --git a/pkg/utils/utils.go b/pkg/utils/utils.go index be86bd2..e32c4ab 100644 --- a/pkg/utils/utils.go +++ b/pkg/utils/utils.go @@ -339,3 +339,11 @@ func IsValidHexValue(v string) bool { func OpensMenuStyle(str string) string { return ColoredString(fmt.Sprintf("%s...", str), color.FgMagenta) } + +func Prepend[T any](slice []T, item T) []T { + return append([]T{item}, slice...) +} + +func Append[T any](slice []T, item T) []T { + return append(slice, item) +}