From 65f40c2ee68f8d29597e6eb9ae8204a564e45d2c Mon Sep 17 00:00:00 2001 From: Alex Goodman Date: Fri, 27 Dec 2019 09:51:11 -0500 Subject: [PATCH 1/3] partially complete with slim layer --- runtime/ui/key/binding.go | 2 +- runtime/ui/view/layer.go | 2 - runtime/ui/view/layer_change_listener.go | 5 + runtime/ui/view/layer_slim.go | 372 +++++++++++++++++++++++ 4 files changed, 378 insertions(+), 3 deletions(-) create mode 100644 runtime/ui/view/layer_change_listener.go create mode 100644 runtime/ui/view/layer_slim.go diff --git a/runtime/ui/key/binding.go b/runtime/ui/key/binding.go index 69fe007b..76ffa318 100644 --- a/runtime/ui/key/binding.go +++ b/runtime/ui/key/binding.go @@ -60,7 +60,7 @@ func NewBindingFromConfig(gui *gocui.Gui, influence string, configKeys []string, for _, configKey := range configKeys { bindStr := viper.GetString(configKey) if bindStr == "" { - logrus.Debug("skipping keybinding '%s' (no value given)", configKey) + logrus.Debugf("skipping keybinding '%s' (no value given)", configKey) continue } logrus.Debugf("parsing keybinding '%s' --> '%s'", configKey, bindStr) diff --git a/runtime/ui/view/layer.go b/runtime/ui/view/layer.go index 578689df..50f86fd4 100644 --- a/runtime/ui/view/layer.go +++ b/runtime/ui/view/layer.go @@ -11,8 +11,6 @@ import ( "github.com/wagoodman/dive/runtime/ui/viewmodel" ) -type LayerChangeListener func(viewmodel.LayerSelection) error - // Layer holds the UI objects and data models for populating the lower-left pane. Specifically the pane that // shows the image layers and layer selector. type Layer struct { diff --git a/runtime/ui/view/layer_change_listener.go b/runtime/ui/view/layer_change_listener.go new file mode 100644 index 00000000..3a7096f7 --- /dev/null +++ b/runtime/ui/view/layer_change_listener.go @@ -0,0 +1,5 @@ +package view + +import "github.com/wagoodman/dive/runtime/ui/viewmodel" + +type LayerChangeListener func(viewmodel.LayerSelection) error diff --git a/runtime/ui/view/layer_slim.go b/runtime/ui/view/layer_slim.go new file mode 100644 index 00000000..d567bd5e --- /dev/null +++ b/runtime/ui/view/layer_slim.go @@ -0,0 +1,372 @@ +package view + +import ( + "fmt" + "github.com/jroimartin/gocui" + "github.com/sirupsen/logrus" + "github.com/spf13/viper" + "github.com/wagoodman/dive/dive/image" + "github.com/wagoodman/dive/runtime/ui/format" + "github.com/wagoodman/dive/runtime/ui/key" + "github.com/wagoodman/dive/runtime/ui/viewmodel" + "github.com/wagoodman/dive/utils" +) + +// Layer holds the UI objects and data models for populating the lower-left pane. Specifically the pane that +// shows the image layers and layer selector. +type LayerSlim struct { + name string + gui *gocui.Gui + view *gocui.View + header *gocui.View + LayerIndex int + Layers []*image.Layer + CompareMode CompareType + CompareStartIndex int + + listeners []LayerChangeListener + + helpKeys []*key.Binding +} + +// newLayerView creates a new view object attached the the global [gocui] screen object. +func newLayerSlimView(gui *gocui.Gui, layers []*image.Layer) (controller *Layer, err error) { + controller = new(Layer) + + controller.listeners = make([]LayerChangeListener, 0) + + // populate main fields + controller.name = "layer" + controller.gui = gui + controller.Layers = layers + + switch mode := viper.GetBool("layer.show-aggregated-changes"); mode { + case true: + controller.CompareMode = CompareAll + case false: + controller.CompareMode = CompareLayer + default: + return nil, fmt.Errorf("unknown layer.show-aggregated-changes value: %v", mode) + } + + return controller, err +} + +func (v *LayerSlim) AddLayerChangeListener(listener ...LayerChangeListener) { + v.listeners = append(v.listeners, listener...) +} + +func (v *LayerSlim) notifyLayerChangeListeners() error { + bottomTreeStart, bottomTreeStop, topTreeStart, topTreeStop := v.getCompareIndexes() + selection := viewmodel.LayerSelection{ + Layer: v.CurrentLayer(), + BottomTreeStart: bottomTreeStart, + BottomTreeStop: bottomTreeStop, + TopTreeStart: topTreeStart, + TopTreeStop: topTreeStop, + } + for _, listener := range v.listeners { + err := listener(selection) + if err != nil { + logrus.Errorf("notifyLayerChangeListeners error: %+v", err) + return err + } + } + return nil +} + +func (v *LayerSlim) Name() string { + return v.name +} + +// Setup initializes the UI concerns within the context of a global [gocui] view object. +func (v *LayerSlim) Setup(view *gocui.View, header *gocui.View) error { + logrus.Tracef("view.Setup() %s", v.Name()) + + // set controller options + v.view = view + v.view.Editable = false + v.view.Wrap = false + v.view.Frame = false + + v.header = header + v.header.Editable = false + v.header.Wrap = false + v.header.Frame = false + + var infos = []key.BindingInfo{ + { + ConfigKeys: []string{"keybinding.compare-layer"}, + OnAction: func() error { return v.setCompareMode(CompareLayer) }, + IsSelected: func() bool { return v.CompareMode == CompareLayer }, + Display: "Show layer changes", + }, + { + ConfigKeys: []string{"keybinding.compare-all"}, + OnAction: func() error { return v.setCompareMode(CompareAll) }, + IsSelected: func() bool { return v.CompareMode == CompareAll }, + Display: "Show aggregated changes", + }, + { + Key: gocui.KeyArrowDown, + Modifier: gocui.ModNone, + OnAction: v.CursorDown, + }, + { + Key: gocui.KeyArrowUp, + Modifier: gocui.ModNone, + OnAction: v.CursorUp, + }, + { + Key: gocui.KeyArrowLeft, + Modifier: gocui.ModNone, + OnAction: v.CursorUp, + }, + { + Key: gocui.KeyArrowRight, + Modifier: gocui.ModNone, + OnAction: v.CursorDown, + }, + { + ConfigKeys: []string{"keybinding.page-up"}, + OnAction: v.PageUp, + }, + { + ConfigKeys: []string{"keybinding.page-down"}, + OnAction: v.PageDown, + }, + } + + helpKeys, err := key.GenerateBindings(v.gui, v.name, infos) + if err != nil { + return err + } + v.helpKeys = helpKeys + + return v.Render() +} + +// height obtains the height of the current pane (taking into account the lost space due to the header). +func (v *LayerSlim) height() uint { + _, height := v.view.Size() + return uint(height - 1) +} + +// IsVisible indicates if the layer view pane is currently initialized. +func (v *LayerSlim) IsVisible() bool { + return v != nil +} + +// PageDown moves to next page putting the cursor on top +func (v *LayerSlim) PageDown() error { + step := int(v.height()) + 1 + targetLayerIndex := v.LayerIndex + step + + if targetLayerIndex > len(v.Layers) { + step -= targetLayerIndex - (len(v.Layers) - 1) + } + + if step > 0 { + err := CursorStep(v.gui, v.view, step) + if err == nil { + return v.SetCursor(v.LayerIndex + step) + } + } + return nil +} + +// PageUp moves to previous page putting the cursor on top +func (v *LayerSlim) PageUp() error { + step := int(v.height()) + 1 + targetLayerIndex := v.LayerIndex - step + + if targetLayerIndex < 0 { + step += targetLayerIndex + } + + if step > 0 { + err := CursorStep(v.gui, v.view, -step) + if err == nil { + return v.SetCursor(v.LayerIndex - step) + } + } + return nil +} + +// CursorDown moves the cursor down in the layer pane (selecting a higher layer). +func (v *LayerSlim) CursorDown() error { + if v.LayerIndex < len(v.Layers) { + err := CursorDown(v.gui, v.view) + if err == nil { + return v.SetCursor(v.LayerIndex + 1) + } + } + return nil +} + +// CursorUp moves the cursor up in the layer pane (selecting a lower layer). +func (v *LayerSlim) CursorUp() error { + if v.LayerIndex > 0 { + err := CursorUp(v.gui, v.view) + if err == nil { + return v.SetCursor(v.LayerIndex - 1) + } + } + return nil +} + +// SetCursor resets the cursor and orients the file tree view based on the given layer index. +func (v *LayerSlim) SetCursor(layer int) error { + v.LayerIndex = layer + err := v.notifyLayerChangeListeners() + if err != nil { + return err + } + + return v.Render() +} + +// CurrentLayer returns the Layer object currently selected. +func (v *LayerSlim) CurrentLayer() *image.Layer { + return v.Layers[v.LayerIndex] +} + +// setCompareMode switches the layer comparison between a single-layer comparison to an aggregated comparison. +func (v *LayerSlim) setCompareMode(compareMode CompareType) error { + v.CompareMode = compareMode + return v.notifyLayerChangeListeners() +} + +// getCompareIndexes determines the layer boundaries to use for comparison (based on the current compare mode) +func (v *LayerSlim) getCompareIndexes() (bottomTreeStart, bottomTreeStop, topTreeStart, topTreeStop int) { + bottomTreeStart = v.CompareStartIndex + topTreeStop = v.LayerIndex + + if v.LayerIndex == v.CompareStartIndex { + bottomTreeStop = v.LayerIndex + topTreeStart = v.LayerIndex + } else if v.CompareMode == CompareLayer { + bottomTreeStop = v.LayerIndex - 1 + topTreeStart = v.LayerIndex + } else { + bottomTreeStop = v.CompareStartIndex + topTreeStart = v.CompareStartIndex + 1 + } + + return bottomTreeStart, bottomTreeStop, topTreeStart, topTreeStop +} + +// renderCompareBar returns the formatted string for the given layer. +func (v *LayerSlim) renderCompareBar(layerIdx int) string { + bottomTreeStart, bottomTreeStop, topTreeStart, topTreeStop := v.getCompareIndexes() + result := " " + + if layerIdx >= bottomTreeStart && layerIdx <= bottomTreeStop { + result = format.CompareBottom(" ") + } + if layerIdx >= topTreeStart && layerIdx <= topTreeStop { + result = format.CompareTop(" ") + } + + return result +} + +// OnLayoutChange is called whenever the screen dimensions are changed +func (v *LayerSlim) OnLayoutChange() error { + err := v.Update() + if err != nil { + return err + } + return v.Render() +} + +// Update refreshes the state objects for future rendering (currently does nothing). +func (v *LayerSlim) Update() error { + return nil +} + +// Render flushes the state objects to the screen. The layers pane reports: +// 1. the layers of the image + metadata +// 2. the current selected image +func (v *LayerSlim) Render() error { + logrus.Tracef("view.Render() %s", v.Name()) + + // indicate when selected + title := "Layers" + isSelected := v.gui.CurrentView() == v.view + + v.gui.Update(func(g *gocui.Gui) error { + // update header + v.header.Clear() + width, _ := g.Size() + headerStr := format.RenderHeader(title, width, isSelected) + _, err := fmt.Fprintln(v.header, headerStr) + if err != nil { + return err + } + + // update contents + v.view.Clear() + for idx, layer := range v.Layers { + + layerStr := layer.String() + compareBar := v.renderCompareBar(idx) + + if idx == v.LayerIndex { + _, err = fmt.Fprintln(v.view, compareBar+" "+format.Selected(layerStr)) + } else { + _, err = fmt.Fprintln(v.view, compareBar+" "+layerStr) + } + + if err != nil { + logrus.Debug("unable to write to buffer: ", err) + return err + } + + } + return nil + }) + return nil +} + +// KeyHelp indicates all the possible actions a user can take while the current pane is selected. +func (v *LayerSlim) KeyHelp() string { + var help string + for _, binding := range v.helpKeys { + help += binding.RenderKeyHelp() + } + return help +} + +func (v *LayerSlim) Layout(g *gocui.Gui, minX, minY, maxX, maxY int) error { + logrus.Tracef("view.LayoutSlim(minX: %d, minY: %d, maxX: %d, maxY: %d) %s", minX, minY, maxX, maxY, v.Name()) + + // header + border + layerHeaderHeight := 1 + + // note: maxY needs to account for the (invisible) border, thus a +1 + header, headerErr := g.SetView(v.Name()+"header", minX, minY, maxX, maxY+layerHeaderHeight+1) + + main, viewErr := g.SetView(v.Name(), minX, minY+layerHeaderHeight, maxX, maxY) + + if utils.IsNewView(viewErr, headerErr) { + err := v.Setup(main, header) + if err != nil { + logrus.Error("unable to setup slim layer layout", err) + return err + } + + if _, err = g.SetCurrentView(v.Name()); err != nil { + logrus.Error("unable to set view to slim layer", err) + return err + } + } + + return nil +} + +func (v *LayerSlim) RequestedSize(available int) *int { + size := 5 + return &size +} + From 8818b9934f74032d7c1baaeedb4b80a2a1fc103a Mon Sep 17 00:00:00 2001 From: Alex Goodman Date: Sat, 28 Dec 2019 12:28:19 -0500 Subject: [PATCH 2/3] reactive layout; addresses #179 --- runtime/ui/controller.go | 2 +- runtime/ui/format/format.go | 19 +- .../layout/compound/layer_details_column.go | 57 ++- runtime/ui/layout/manager.go | 9 + runtime/ui/view/filetree.go | 30 +- runtime/ui/view/layer.go | 136 ++++--- runtime/ui/view/layer_slim.go | 372 ------------------ runtime/ui/viewmodel/filetree.go | 37 +- runtime/ui/viewmodel/layer_compare.go | 8 + runtime/ui/viewmodel/layer_set_state.go | 36 ++ 10 files changed, 240 insertions(+), 466 deletions(-) delete mode 100644 runtime/ui/view/layer_slim.go create mode 100644 runtime/ui/viewmodel/layer_compare.go create mode 100644 runtime/ui/viewmodel/layer_set_state.go diff --git a/runtime/ui/controller.go b/runtime/ui/controller.go index 1a801dfc..d9ca3303 100644 --- a/runtime/ui/controller.go +++ b/runtime/ui/controller.go @@ -90,7 +90,7 @@ func (c *Controller) onLayerChange(selection viewmodel.LayerSelection) error { return err } - if c.views.Layer.CompareMode == view.CompareAll { + if c.views.Layer.CompareMode() == viewmodel.CompareAllLayers { c.views.Tree.SetTitle("Aggregated Layer Contents") } else { c.views.Tree.SetTitle("Current Layer Contents") diff --git a/runtime/ui/format/format.go b/runtime/ui/format/format.go index 0f549102..6a7c671c 100644 --- a/runtime/ui/format/format.go +++ b/runtime/ui/format/format.go @@ -58,17 +58,32 @@ func init() { CompareBottom = color.New(color.BgGreen).SprintFunc() } +func RenderNoHeader(width int, selected bool) string { + if selected { + return strings.Repeat(selectedFillStr, width) + } + return strings.Repeat(fillStr, width) +} + func RenderHeader(title string, width int, selected bool) string { if selected { body := Header(fmt.Sprintf("%s%s ", selectStr, title)) bodyLen := len(vtclean.Clean(body, false)) - return fmt.Sprintf("%s%s%s%s\n", selectedLeftBracketStr, body, selectedRightBracketStr, strings.Repeat(selectedFillStr, width-bodyLen-2)) + repeatCount := width - bodyLen - 2 + if repeatCount < 0 { + repeatCount = 0 + } + return fmt.Sprintf("%s%s%s%s\n", selectedLeftBracketStr, body, selectedRightBracketStr, strings.Repeat(selectedFillStr, repeatCount)) //return fmt.Sprintf("%s%s%s%s\n", Selected(selectedLeftBracketStr), body, Selected(selectedRightBracketStr), Selected(strings.Repeat(selectedFillStr, width-bodyLen-2))) //return fmt.Sprintf("%s%s%s%s\n", Selected(selectedLeftBracketStr), body, Selected(selectedRightBracketStr), strings.Repeat(selectedFillStr, width-bodyLen-2)) } body := Header(fmt.Sprintf(" %s ", title)) bodyLen := len(vtclean.Clean(body, false)) - return fmt.Sprintf("%s%s%s%s\n", leftBracketStr, body, rightBracketStr, strings.Repeat(fillStr, width-bodyLen-2)) + repeatCount := width - bodyLen - 2 + if repeatCount < 0 { + repeatCount = 0 + } + return fmt.Sprintf("%s%s%s%s\n", leftBracketStr, body, rightBracketStr, strings.Repeat(fillStr, repeatCount)) } func RenderHelpKey(control, title string, selected bool) string { diff --git a/runtime/ui/layout/compound/layer_details_column.go b/runtime/ui/layout/compound/layer_details_column.go index 754b6f03..de45f982 100644 --- a/runtime/ui/layout/compound/layer_details_column.go +++ b/runtime/ui/layout/compound/layer_details_column.go @@ -8,8 +8,9 @@ import ( ) type LayerDetailsCompoundLayout struct { - layer *view.Layer - details *view.Details + layer *view.Layer + details *view.Details + constrainRealEstate bool } func NewLayerDetailsCompoundLayout(layer *view.Layer, details *view.Details) *LayerDetailsCompoundLayout { @@ -48,7 +49,7 @@ func (cl *LayerDetailsCompoundLayout) Layout(g *gocui.Gui, minX, minY, maxX, max // header + border layerHeaderHeight := 2 - layersHeight := len(cl.layer.Layers) + layerHeaderHeight + 1 // layers + header + base image layer row + layersHeight := cl.layer.LayerCount() + layerHeaderHeight + 1 // layers + header + base image layer row maxLayerHeight := int(0.75 * float64(maxY)) if layersHeight > maxLayerHeight { layersHeight = maxLayerHeight @@ -80,24 +81,56 @@ func (cl *LayerDetailsCompoundLayout) Layout(g *gocui.Gui, minX, minY, maxX, max // header + border detailsHeaderHeight := 2 - // note: maxY needs to account for the (invisible) border, thus a +1 - header, headerErr = g.SetView(cl.details.Name()+"header", minX, detailsMinY, maxX, detailsMinY+detailsHeaderHeight+1) + v, _ := g.View(cl.details.Name()) + if v != nil { + // the view exists already! + + // don't show the details pane when there isn't enough room on the screen + if cl.constrainRealEstate { + // take note: deleting a view will invoke layout again, so ensure this call is protected from an infinite loop + err := g.DeleteView(cl.details.Name()) + if err != nil { + return err + } + // take note: deleting a view will invoke layout again, so ensure this call is protected from an infinite loop + err = g.DeleteView(cl.details.Name() + "header") + if err != nil { + return err + } + } - // we are going to overlap the view over the (invisible) border (so minY will be one less than expected) - // additionally, maxY will be bumped by one to include the border - main, viewErr = g.SetView(cl.details.Name(), minX, detailsMinY+detailsHeaderHeight, maxX, maxY+1) + } else { - if utils.IsNewView(viewErr, headerErr) { - err := cl.details.Setup(main, header) - if err != nil { - return err + // note: maxY needs to account for the (invisible) border, thus a +1 + header, headerErr = g.SetView(cl.details.Name()+"header", minX, detailsMinY, maxX, detailsMinY+detailsHeaderHeight+1) + + // we are going to overlap the view over the (invisible) border (so minY will be one less than expected) + // additionally, maxY will be bumped by one to include the border + main, viewErr = g.SetView(cl.details.Name(), minX, detailsMinY+detailsHeaderHeight, maxX, maxY+1) + + if utils.IsNewView(viewErr, headerErr) { + err := cl.details.Setup(main, header) + if err != nil { + return err + } } + } return nil } func (cl *LayerDetailsCompoundLayout) RequestedSize(available int) *int { + // "available" is the entire screen real estate, so we can guess when its a bit too small and take action. + // This isn't perfect, but it gets the job done for now without complicated layout constraint solvers + if available < 90 { + cl.layer.ConstrainLayout() + cl.constrainRealEstate = true + size := 8 + return &size + } + cl.layer.ExpandLayout() + cl.constrainRealEstate = false return nil } diff --git a/runtime/ui/layout/manager.go b/runtime/ui/layout/manager.go index 6bfe0aa1..460cbeaa 100644 --- a/runtime/ui/layout/manager.go +++ b/runtime/ui/layout/manager.go @@ -5,6 +5,8 @@ import ( "github.com/sirupsen/logrus" ) +type Constraint func(int) int + type Manager struct { lastX, lastY int lastHeaderArea, lastFooterArea, lastColumnArea Area @@ -113,6 +115,13 @@ func (lm *Manager) planAndLayoutColumns(g *gocui.Gui, area Area) (Area, error) { } } + // at least one column must have a variable width, force the last column to be variable if there are no + // variable columns + if variableColumns == 0 { + variableColumns = 1 + widths[len(widths)-1] = -1 + } + defaultWidth := availableWidth / variableColumns // second pass: layout columns left to right (based off predetermined widths) diff --git a/runtime/ui/view/filetree.go b/runtime/ui/view/filetree.go index a499aed5..97e6f7b0 100644 --- a/runtime/ui/view/filetree.go +++ b/runtime/ui/view/filetree.go @@ -13,13 +13,6 @@ import ( "regexp" ) -const ( - CompareLayer CompareType = iota - CompareAll -) - -type CompareType int - type ViewOptionChangeListener func() error // FileTree holds the UI objects and data models for populating the right pane. Specifically the pane that @@ -77,10 +70,6 @@ func (v *FileTree) Name() string { return v.name } -func (v *FileTree) areAttributesVisible() bool { - return v.vm.ShowAttributes -} - // Setup initializes the UI concerns within the context of a global [gocui] view object. func (v *FileTree) Setup(view *gocui.View, header *gocui.View) error { logrus.Tracef("view.Setup() %s", v.Name()) @@ -377,7 +366,7 @@ func (v *FileTree) Render() error { if v.vm.ShowAttributes { headerStr += fmt.Sprintf(filetree.AttributeFormat+" %s", "P", "ermission", "UID:GID", "Size", "Filetree") } - _, _ = fmt.Fprintln(v.header, headerStr, false) + _, _ = fmt.Fprintln(v.header, headerStr) // update the contents v.view.Clear() @@ -404,9 +393,19 @@ func (v *FileTree) KeyHelp() string { func (v *FileTree) Layout(g *gocui.Gui, minX, minY, maxX, maxY int) error { logrus.Tracef("view.Layout(minX: %d, minY: %d, maxX: %d, maxY: %d) %s", minX, minY, maxX, maxY, v.Name()) attributeRowSize := 0 - if v.areAttributesVisible() { + + // make the layout responsive to the available realestate. Make more room for the main content by hiding auxillary + // content when there is not enough room + if maxX-minX < 60 { + v.vm.ConstrainLayout() + } else { + v.vm.ExpandLayout() + } + + if v.vm.ShowAttributes { attributeRowSize = 1 } + // header + attribute header headerSize := 1 + attributeRowSize // note: maxY needs to account for the (invisible) border, thus a +1 @@ -425,6 +424,7 @@ func (v *FileTree) Layout(g *gocui.Gui, minX, minY, maxX, maxY int) error { } func (v *FileTree) RequestedSize(available int) *int { - var requestedWidth = int(float64(available) * (1.0 - v.requestedWidthRatio)) - return &requestedWidth + //var requestedWidth = int(float64(available) * (1.0 - v.requestedWidthRatio)) + //return &requestedWidth + return nil } diff --git a/runtime/ui/view/layer.go b/runtime/ui/view/layer.go index 50f86fd4..08472fa4 100644 --- a/runtime/ui/view/layer.go +++ b/runtime/ui/view/layer.go @@ -14,14 +14,12 @@ import ( // Layer holds the UI objects and data models for populating the lower-left pane. Specifically the pane that // shows the image layers and layer selector. type Layer struct { - name string - gui *gocui.Gui - view *gocui.View - header *gocui.View - LayerIndex int - Layers []*image.Layer - CompareMode CompareType - CompareStartIndex int + name string + gui *gocui.Gui + view *gocui.View + header *gocui.View + vm *viewmodel.LayerSetState + constrainedRealEstate bool listeners []LayerChangeListener @@ -37,17 +35,20 @@ func newLayerView(gui *gocui.Gui, layers []*image.Layer) (controller *Layer, err // populate main fields controller.name = "layer" controller.gui = gui - controller.Layers = layers + + var compareMode viewmodel.LayerCompareMode switch mode := viper.GetBool("layer.show-aggregated-changes"); mode { case true: - controller.CompareMode = CompareAll + compareMode = viewmodel.CompareAllLayers case false: - controller.CompareMode = CompareLayer + compareMode = viewmodel.CompareSingleLayer default: return nil, fmt.Errorf("unknown layer.show-aggregated-changes value: %v", mode) } + controller.vm = viewmodel.NewLayerSetState(layers, compareMode) + return controller, err } @@ -56,7 +57,7 @@ func (v *Layer) AddLayerChangeListener(listener ...LayerChangeListener) { } func (v *Layer) notifyLayerChangeListeners() error { - bottomTreeStart, bottomTreeStop, topTreeStart, topTreeStop := v.getCompareIndexes() + bottomTreeStart, bottomTreeStop, topTreeStart, topTreeStop := v.vm.GetCompareIndexes() selection := viewmodel.LayerSelection{ Layer: v.CurrentLayer(), BottomTreeStart: bottomTreeStart, @@ -96,14 +97,14 @@ func (v *Layer) Setup(view *gocui.View, header *gocui.View) error { var infos = []key.BindingInfo{ { ConfigKeys: []string{"keybinding.compare-layer"}, - OnAction: func() error { return v.setCompareMode(CompareLayer) }, - IsSelected: func() bool { return v.CompareMode == CompareLayer }, + OnAction: func() error { return v.setCompareMode(viewmodel.CompareSingleLayer) }, + IsSelected: func() bool { return v.vm.CompareMode == viewmodel.CompareSingleLayer }, Display: "Show layer changes", }, { ConfigKeys: []string{"keybinding.compare-all"}, - OnAction: func() error { return v.setCompareMode(CompareAll) }, - IsSelected: func() bool { return v.CompareMode == CompareAll }, + OnAction: func() error { return v.setCompareMode(viewmodel.CompareAllLayers) }, + IsSelected: func() bool { return v.vm.CompareMode == viewmodel.CompareAllLayers }, Display: "Show aggregated changes", }, { @@ -151,6 +152,10 @@ func (v *Layer) height() uint { return uint(height - 1) } +func (v *Layer) CompareMode() viewmodel.LayerCompareMode { + return v.vm.CompareMode +} + // IsVisible indicates if the layer view pane is currently initialized. func (v *Layer) IsVisible() bool { return v != nil @@ -159,16 +164,16 @@ func (v *Layer) IsVisible() bool { // PageDown moves to next page putting the cursor on top func (v *Layer) PageDown() error { step := int(v.height()) + 1 - targetLayerIndex := v.LayerIndex + step + targetLayerIndex := v.vm.LayerIndex + step - if targetLayerIndex > len(v.Layers) { - step -= targetLayerIndex - (len(v.Layers) - 1) + if targetLayerIndex > len(v.vm.Layers) { + step -= targetLayerIndex - (len(v.vm.Layers) - 1) } if step > 0 { err := CursorStep(v.gui, v.view, step) if err == nil { - return v.SetCursor(v.LayerIndex + step) + return v.SetCursor(v.vm.LayerIndex + step) } } return nil @@ -177,7 +182,7 @@ func (v *Layer) PageDown() error { // PageUp moves to previous page putting the cursor on top func (v *Layer) PageUp() error { step := int(v.height()) + 1 - targetLayerIndex := v.LayerIndex - step + targetLayerIndex := v.vm.LayerIndex - step if targetLayerIndex < 0 { step += targetLayerIndex @@ -186,7 +191,7 @@ func (v *Layer) PageUp() error { if step > 0 { err := CursorStep(v.gui, v.view, -step) if err == nil { - return v.SetCursor(v.LayerIndex - step) + return v.SetCursor(v.vm.LayerIndex - step) } } return nil @@ -194,10 +199,10 @@ func (v *Layer) PageUp() error { // CursorDown moves the cursor down in the layer pane (selecting a higher layer). func (v *Layer) CursorDown() error { - if v.LayerIndex < len(v.Layers) { + if v.vm.LayerIndex < len(v.vm.Layers) { err := CursorDown(v.gui, v.view) if err == nil { - return v.SetCursor(v.LayerIndex + 1) + return v.SetCursor(v.vm.LayerIndex + 1) } } return nil @@ -205,10 +210,10 @@ func (v *Layer) CursorDown() error { // CursorUp moves the cursor up in the layer pane (selecting a lower layer). func (v *Layer) CursorUp() error { - if v.LayerIndex > 0 { + if v.vm.LayerIndex > 0 { err := CursorUp(v.gui, v.view) if err == nil { - return v.SetCursor(v.LayerIndex - 1) + return v.SetCursor(v.vm.LayerIndex - 1) } } return nil @@ -216,7 +221,7 @@ func (v *Layer) CursorUp() error { // SetCursor resets the cursor and orients the file tree view based on the given layer index. func (v *Layer) SetCursor(layer int) error { - v.LayerIndex = layer + v.vm.LayerIndex = layer err := v.notifyLayerChangeListeners() if err != nil { return err @@ -227,37 +232,18 @@ func (v *Layer) SetCursor(layer int) error { // CurrentLayer returns the Layer object currently selected. func (v *Layer) CurrentLayer() *image.Layer { - return v.Layers[v.LayerIndex] + return v.vm.Layers[v.vm.LayerIndex] } // setCompareMode switches the layer comparison between a single-layer comparison to an aggregated comparison. -func (v *Layer) setCompareMode(compareMode CompareType) error { - v.CompareMode = compareMode +func (v *Layer) setCompareMode(compareMode viewmodel.LayerCompareMode) error { + v.vm.CompareMode = compareMode return v.notifyLayerChangeListeners() } -// getCompareIndexes determines the layer boundaries to use for comparison (based on the current compare mode) -func (v *Layer) getCompareIndexes() (bottomTreeStart, bottomTreeStop, topTreeStart, topTreeStop int) { - bottomTreeStart = v.CompareStartIndex - topTreeStop = v.LayerIndex - - if v.LayerIndex == v.CompareStartIndex { - bottomTreeStop = v.LayerIndex - topTreeStart = v.LayerIndex - } else if v.CompareMode == CompareLayer { - bottomTreeStop = v.LayerIndex - 1 - topTreeStart = v.LayerIndex - } else { - bottomTreeStop = v.CompareStartIndex - topTreeStart = v.CompareStartIndex + 1 - } - - return bottomTreeStart, bottomTreeStop, topTreeStart, topTreeStop -} - // renderCompareBar returns the formatted string for the given layer. func (v *Layer) renderCompareBar(layerIdx int) string { - bottomTreeStart, bottomTreeStop, topTreeStart, topTreeStop := v.getCompareIndexes() + bottomTreeStart, bottomTreeStop, topTreeStart, topTreeStop := v.vm.GetCompareIndexes() result := " " if layerIdx >= bottomTreeStart && layerIdx <= bottomTreeStop { @@ -270,6 +256,20 @@ func (v *Layer) renderCompareBar(layerIdx int) string { return result } +func (v *Layer) ConstrainLayout() { + if !v.constrainedRealEstate { + logrus.Debugf("constraining layer layout") + v.constrainedRealEstate = true + } +} + +func (v *Layer) ExpandLayout() { + if v.constrainedRealEstate { + logrus.Debugf("expanding layer layout") + v.constrainedRealEstate = false + } +} + // OnLayoutChange is called whenever the screen dimensions are changed func (v *Layer) OnLayoutChange() error { err := v.Update() @@ -295,24 +295,40 @@ func (v *Layer) Render() error { isSelected := v.gui.CurrentView() == v.view v.gui.Update(func(g *gocui.Gui) error { + var err error // update header v.header.Clear() width, _ := g.Size() - headerStr := format.RenderHeader(title, width, isSelected) - headerStr += fmt.Sprintf("Cmp"+image.LayerFormat, "Size", "Command") - _, err := fmt.Fprintln(v.header, headerStr) - if err != nil { - return err + if v.constrainedRealEstate { + headerStr := format.RenderNoHeader(width, isSelected) + headerStr += fmt.Sprintf("\nLayer") + _, err := fmt.Fprintln(v.header, headerStr) + if err != nil { + return err + } + } else { + headerStr := format.RenderHeader(title, width, isSelected) + headerStr += fmt.Sprintf("Cmp"+image.LayerFormat, "Size", "Command") + _, err := fmt.Fprintln(v.header, headerStr) + if err != nil { + return err + } } // update contents v.view.Clear() - for idx, layer := range v.Layers { + for idx, layer := range v.vm.Layers { + + var layerStr string + if v.constrainedRealEstate { + layerStr = fmt.Sprintf("%-4d", layer.Index) + } else { + layerStr = layer.String() + } - layerStr := layer.String() compareBar := v.renderCompareBar(idx) - if idx == v.LayerIndex { + if idx == v.vm.LayerIndex { _, err = fmt.Fprintln(v.view, compareBar+" "+format.Selected(layerStr)) } else { _, err = fmt.Fprintln(v.view, compareBar+" "+layerStr) @@ -329,6 +345,10 @@ func (v *Layer) Render() error { return nil } +func (v *Layer) LayerCount() int { + return len(v.vm.Layers) +} + // KeyHelp indicates all the possible actions a user can take while the current pane is selected. func (v *Layer) KeyHelp() string { var help string diff --git a/runtime/ui/view/layer_slim.go b/runtime/ui/view/layer_slim.go deleted file mode 100644 index d567bd5e..00000000 --- a/runtime/ui/view/layer_slim.go +++ /dev/null @@ -1,372 +0,0 @@ -package view - -import ( - "fmt" - "github.com/jroimartin/gocui" - "github.com/sirupsen/logrus" - "github.com/spf13/viper" - "github.com/wagoodman/dive/dive/image" - "github.com/wagoodman/dive/runtime/ui/format" - "github.com/wagoodman/dive/runtime/ui/key" - "github.com/wagoodman/dive/runtime/ui/viewmodel" - "github.com/wagoodman/dive/utils" -) - -// Layer holds the UI objects and data models for populating the lower-left pane. Specifically the pane that -// shows the image layers and layer selector. -type LayerSlim struct { - name string - gui *gocui.Gui - view *gocui.View - header *gocui.View - LayerIndex int - Layers []*image.Layer - CompareMode CompareType - CompareStartIndex int - - listeners []LayerChangeListener - - helpKeys []*key.Binding -} - -// newLayerView creates a new view object attached the the global [gocui] screen object. -func newLayerSlimView(gui *gocui.Gui, layers []*image.Layer) (controller *Layer, err error) { - controller = new(Layer) - - controller.listeners = make([]LayerChangeListener, 0) - - // populate main fields - controller.name = "layer" - controller.gui = gui - controller.Layers = layers - - switch mode := viper.GetBool("layer.show-aggregated-changes"); mode { - case true: - controller.CompareMode = CompareAll - case false: - controller.CompareMode = CompareLayer - default: - return nil, fmt.Errorf("unknown layer.show-aggregated-changes value: %v", mode) - } - - return controller, err -} - -func (v *LayerSlim) AddLayerChangeListener(listener ...LayerChangeListener) { - v.listeners = append(v.listeners, listener...) -} - -func (v *LayerSlim) notifyLayerChangeListeners() error { - bottomTreeStart, bottomTreeStop, topTreeStart, topTreeStop := v.getCompareIndexes() - selection := viewmodel.LayerSelection{ - Layer: v.CurrentLayer(), - BottomTreeStart: bottomTreeStart, - BottomTreeStop: bottomTreeStop, - TopTreeStart: topTreeStart, - TopTreeStop: topTreeStop, - } - for _, listener := range v.listeners { - err := listener(selection) - if err != nil { - logrus.Errorf("notifyLayerChangeListeners error: %+v", err) - return err - } - } - return nil -} - -func (v *LayerSlim) Name() string { - return v.name -} - -// Setup initializes the UI concerns within the context of a global [gocui] view object. -func (v *LayerSlim) Setup(view *gocui.View, header *gocui.View) error { - logrus.Tracef("view.Setup() %s", v.Name()) - - // set controller options - v.view = view - v.view.Editable = false - v.view.Wrap = false - v.view.Frame = false - - v.header = header - v.header.Editable = false - v.header.Wrap = false - v.header.Frame = false - - var infos = []key.BindingInfo{ - { - ConfigKeys: []string{"keybinding.compare-layer"}, - OnAction: func() error { return v.setCompareMode(CompareLayer) }, - IsSelected: func() bool { return v.CompareMode == CompareLayer }, - Display: "Show layer changes", - }, - { - ConfigKeys: []string{"keybinding.compare-all"}, - OnAction: func() error { return v.setCompareMode(CompareAll) }, - IsSelected: func() bool { return v.CompareMode == CompareAll }, - Display: "Show aggregated changes", - }, - { - Key: gocui.KeyArrowDown, - Modifier: gocui.ModNone, - OnAction: v.CursorDown, - }, - { - Key: gocui.KeyArrowUp, - Modifier: gocui.ModNone, - OnAction: v.CursorUp, - }, - { - Key: gocui.KeyArrowLeft, - Modifier: gocui.ModNone, - OnAction: v.CursorUp, - }, - { - Key: gocui.KeyArrowRight, - Modifier: gocui.ModNone, - OnAction: v.CursorDown, - }, - { - ConfigKeys: []string{"keybinding.page-up"}, - OnAction: v.PageUp, - }, - { - ConfigKeys: []string{"keybinding.page-down"}, - OnAction: v.PageDown, - }, - } - - helpKeys, err := key.GenerateBindings(v.gui, v.name, infos) - if err != nil { - return err - } - v.helpKeys = helpKeys - - return v.Render() -} - -// height obtains the height of the current pane (taking into account the lost space due to the header). -func (v *LayerSlim) height() uint { - _, height := v.view.Size() - return uint(height - 1) -} - -// IsVisible indicates if the layer view pane is currently initialized. -func (v *LayerSlim) IsVisible() bool { - return v != nil -} - -// PageDown moves to next page putting the cursor on top -func (v *LayerSlim) PageDown() error { - step := int(v.height()) + 1 - targetLayerIndex := v.LayerIndex + step - - if targetLayerIndex > len(v.Layers) { - step -= targetLayerIndex - (len(v.Layers) - 1) - } - - if step > 0 { - err := CursorStep(v.gui, v.view, step) - if err == nil { - return v.SetCursor(v.LayerIndex + step) - } - } - return nil -} - -// PageUp moves to previous page putting the cursor on top -func (v *LayerSlim) PageUp() error { - step := int(v.height()) + 1 - targetLayerIndex := v.LayerIndex - step - - if targetLayerIndex < 0 { - step += targetLayerIndex - } - - if step > 0 { - err := CursorStep(v.gui, v.view, -step) - if err == nil { - return v.SetCursor(v.LayerIndex - step) - } - } - return nil -} - -// CursorDown moves the cursor down in the layer pane (selecting a higher layer). -func (v *LayerSlim) CursorDown() error { - if v.LayerIndex < len(v.Layers) { - err := CursorDown(v.gui, v.view) - if err == nil { - return v.SetCursor(v.LayerIndex + 1) - } - } - return nil -} - -// CursorUp moves the cursor up in the layer pane (selecting a lower layer). -func (v *LayerSlim) CursorUp() error { - if v.LayerIndex > 0 { - err := CursorUp(v.gui, v.view) - if err == nil { - return v.SetCursor(v.LayerIndex - 1) - } - } - return nil -} - -// SetCursor resets the cursor and orients the file tree view based on the given layer index. -func (v *LayerSlim) SetCursor(layer int) error { - v.LayerIndex = layer - err := v.notifyLayerChangeListeners() - if err != nil { - return err - } - - return v.Render() -} - -// CurrentLayer returns the Layer object currently selected. -func (v *LayerSlim) CurrentLayer() *image.Layer { - return v.Layers[v.LayerIndex] -} - -// setCompareMode switches the layer comparison between a single-layer comparison to an aggregated comparison. -func (v *LayerSlim) setCompareMode(compareMode CompareType) error { - v.CompareMode = compareMode - return v.notifyLayerChangeListeners() -} - -// getCompareIndexes determines the layer boundaries to use for comparison (based on the current compare mode) -func (v *LayerSlim) getCompareIndexes() (bottomTreeStart, bottomTreeStop, topTreeStart, topTreeStop int) { - bottomTreeStart = v.CompareStartIndex - topTreeStop = v.LayerIndex - - if v.LayerIndex == v.CompareStartIndex { - bottomTreeStop = v.LayerIndex - topTreeStart = v.LayerIndex - } else if v.CompareMode == CompareLayer { - bottomTreeStop = v.LayerIndex - 1 - topTreeStart = v.LayerIndex - } else { - bottomTreeStop = v.CompareStartIndex - topTreeStart = v.CompareStartIndex + 1 - } - - return bottomTreeStart, bottomTreeStop, topTreeStart, topTreeStop -} - -// renderCompareBar returns the formatted string for the given layer. -func (v *LayerSlim) renderCompareBar(layerIdx int) string { - bottomTreeStart, bottomTreeStop, topTreeStart, topTreeStop := v.getCompareIndexes() - result := " " - - if layerIdx >= bottomTreeStart && layerIdx <= bottomTreeStop { - result = format.CompareBottom(" ") - } - if layerIdx >= topTreeStart && layerIdx <= topTreeStop { - result = format.CompareTop(" ") - } - - return result -} - -// OnLayoutChange is called whenever the screen dimensions are changed -func (v *LayerSlim) OnLayoutChange() error { - err := v.Update() - if err != nil { - return err - } - return v.Render() -} - -// Update refreshes the state objects for future rendering (currently does nothing). -func (v *LayerSlim) Update() error { - return nil -} - -// Render flushes the state objects to the screen. The layers pane reports: -// 1. the layers of the image + metadata -// 2. the current selected image -func (v *LayerSlim) Render() error { - logrus.Tracef("view.Render() %s", v.Name()) - - // indicate when selected - title := "Layers" - isSelected := v.gui.CurrentView() == v.view - - v.gui.Update(func(g *gocui.Gui) error { - // update header - v.header.Clear() - width, _ := g.Size() - headerStr := format.RenderHeader(title, width, isSelected) - _, err := fmt.Fprintln(v.header, headerStr) - if err != nil { - return err - } - - // update contents - v.view.Clear() - for idx, layer := range v.Layers { - - layerStr := layer.String() - compareBar := v.renderCompareBar(idx) - - if idx == v.LayerIndex { - _, err = fmt.Fprintln(v.view, compareBar+" "+format.Selected(layerStr)) - } else { - _, err = fmt.Fprintln(v.view, compareBar+" "+layerStr) - } - - if err != nil { - logrus.Debug("unable to write to buffer: ", err) - return err - } - - } - return nil - }) - return nil -} - -// KeyHelp indicates all the possible actions a user can take while the current pane is selected. -func (v *LayerSlim) KeyHelp() string { - var help string - for _, binding := range v.helpKeys { - help += binding.RenderKeyHelp() - } - return help -} - -func (v *LayerSlim) Layout(g *gocui.Gui, minX, minY, maxX, maxY int) error { - logrus.Tracef("view.LayoutSlim(minX: %d, minY: %d, maxX: %d, maxY: %d) %s", minX, minY, maxX, maxY, v.Name()) - - // header + border - layerHeaderHeight := 1 - - // note: maxY needs to account for the (invisible) border, thus a +1 - header, headerErr := g.SetView(v.Name()+"header", minX, minY, maxX, maxY+layerHeaderHeight+1) - - main, viewErr := g.SetView(v.Name(), minX, minY+layerHeaderHeight, maxX, maxY) - - if utils.IsNewView(viewErr, headerErr) { - err := v.Setup(main, header) - if err != nil { - logrus.Error("unable to setup slim layer layout", err) - return err - } - - if _, err = g.SetCurrentView(v.Name()); err != nil { - logrus.Error("unable to set view to slim layer", err) - return err - } - } - - return nil -} - -func (v *LayerSlim) RequestedSize(available int) *int { - size := 5 - return &size -} - diff --git a/runtime/ui/viewmodel/filetree.go b/runtime/ui/viewmodel/filetree.go index fe7f00d1..2240ab3c 100644 --- a/runtime/ui/viewmodel/filetree.go +++ b/runtime/ui/viewmodel/filetree.go @@ -21,12 +21,15 @@ type FileTree struct { RefTrees []*filetree.FileTree cache filetree.Comparer - CollapseAll bool - ShowAttributes bool - HiddenDiffTypes []bool - TreeIndex int - bufferIndex int - bufferIndexLowerBound int + constrainedRealEstate bool + + CollapseAll bool + ShowAttributes bool + unconstrainedShowAttributes bool + HiddenDiffTypes []bool + TreeIndex int + bufferIndex int + bufferIndexLowerBound int refHeight int refWidth int @@ -40,6 +43,7 @@ func NewFileTreeViewModel(tree *filetree.FileTree, refTrees []*filetree.FileTree // populate main fields treeViewModel.ShowAttributes = viper.GetBool("filetree.show-attributes") + treeViewModel.unconstrainedShowAttributes = treeViewModel.ShowAttributes treeViewModel.CollapseAll = viper.GetBool("filetree.collapse-dir") treeViewModel.ModelTree = tree treeViewModel.RefTrees = refTrees @@ -351,8 +355,29 @@ func (vm *FileTree) ToggleCollapseAll() error { return nil } +func (vm *FileTree) ConstrainLayout() { + if !vm.constrainedRealEstate { + logrus.Debugf("constraining filetree layout") + vm.constrainedRealEstate = true + vm.unconstrainedShowAttributes = vm.ShowAttributes + vm.ShowAttributes = false + } +} + +func (vm *FileTree) ExpandLayout() { + if vm.constrainedRealEstate { + logrus.Debugf("expanding filetree layout") + vm.ShowAttributes = vm.unconstrainedShowAttributes + vm.constrainedRealEstate = false + } +} + // ToggleCollapse will collapse/expand the selected FileNode. func (vm *FileTree) ToggleAttributes() error { + // ignore any attempt to show the attributes when the layout is constrained + if vm.constrainedRealEstate { + return nil + } vm.ShowAttributes = !vm.ShowAttributes return nil } diff --git a/runtime/ui/viewmodel/layer_compare.go b/runtime/ui/viewmodel/layer_compare.go new file mode 100644 index 00000000..2313c84d --- /dev/null +++ b/runtime/ui/viewmodel/layer_compare.go @@ -0,0 +1,8 @@ +package viewmodel + +const ( + CompareSingleLayer LayerCompareMode = iota + CompareAllLayers +) + +type LayerCompareMode int diff --git a/runtime/ui/viewmodel/layer_set_state.go b/runtime/ui/viewmodel/layer_set_state.go new file mode 100644 index 00000000..3f028176 --- /dev/null +++ b/runtime/ui/viewmodel/layer_set_state.go @@ -0,0 +1,36 @@ +package viewmodel + +import "github.com/wagoodman/dive/dive/image" + +type LayerSetState struct { + LayerIndex int + Layers []*image.Layer + CompareMode LayerCompareMode + CompareStartIndex int +} + +func NewLayerSetState(layers []*image.Layer, compareMode LayerCompareMode) *LayerSetState { + return &LayerSetState{ + Layers: layers, + CompareMode: compareMode, + } +} + +// getCompareIndexes determines the layer boundaries to use for comparison (based on the current compare mode) +func (state *LayerSetState) GetCompareIndexes() (bottomTreeStart, bottomTreeStop, topTreeStart, topTreeStop int) { + bottomTreeStart = state.CompareStartIndex + topTreeStop = state.LayerIndex + + if state.LayerIndex == state.CompareStartIndex { + bottomTreeStop = state.LayerIndex + topTreeStart = state.LayerIndex + } else if state.CompareMode == CompareSingleLayer { + bottomTreeStop = state.LayerIndex - 1 + topTreeStart = state.LayerIndex + } else { + bottomTreeStop = state.CompareStartIndex + topTreeStart = state.CompareStartIndex + 1 + } + + return bottomTreeStart, bottomTreeStop, topTreeStart, topTreeStop +} From 16fde15dbf6161f99b94f56862588fee4c48fed4 Mon Sep 17 00:00:00 2001 From: Alex Goodman Date: Sun, 9 Feb 2020 10:43:11 -0500 Subject: [PATCH 3/3] fix expand bug for details pane and bottom status bar --- .../layout/compound/layer_details_column.go | 22 ++++++++----------- 1 file changed, 9 insertions(+), 13 deletions(-) diff --git a/runtime/ui/layout/compound/layer_details_column.go b/runtime/ui/layout/compound/layer_details_column.go index de45f982..1941edf0 100644 --- a/runtime/ui/layout/compound/layer_details_column.go +++ b/runtime/ui/layout/compound/layer_details_column.go @@ -97,24 +97,20 @@ func (cl *LayerDetailsCompoundLayout) Layout(g *gocui.Gui, minX, minY, maxX, max if err != nil { return err } - } - } else { + return nil + } - // note: maxY needs to account for the (invisible) border, thus a +1 - header, headerErr = g.SetView(cl.details.Name()+"header", minX, detailsMinY, maxX, detailsMinY+detailsHeaderHeight+1) + } - // we are going to overlap the view over the (invisible) border (so minY will be one less than expected) - // additionally, maxY will be bumped by one to include the border - main, viewErr = g.SetView(cl.details.Name(), minX, detailsMinY+detailsHeaderHeight, maxX, maxY+1) + header, headerErr = g.SetView(cl.details.Name()+"header", minX, detailsMinY, maxX, detailsMinY+detailsHeaderHeight) + main, viewErr = g.SetView(cl.details.Name(), minX, detailsMinY+detailsHeaderHeight, maxX, maxY) - if utils.IsNewView(viewErr, headerErr) { - err := cl.details.Setup(main, header) - if err != nil { - return err - } + if utils.IsNewView(viewErr, headerErr) { + err := cl.details.Setup(main, header) + if err != nil { + return err } - } return nil