From 14706152a1172d28507c244f137d7df2998c056e Mon Sep 17 00:00:00 2001 From: Alex Goodman Date: Fri, 22 Nov 2019 17:33:13 -0500 Subject: [PATCH] added simple layout manager --- cmd/root.go | 4 + runtime/ui/app.go | 31 +- runtime/ui/controller.go | 94 ++---- .../layout/compound/layer_details_column.go | 96 ++++++ runtime/ui/layout/layout.go | 10 + runtime/ui/layout/location.go | 9 + runtime/ui/layout/manager.go | 176 +++++++++++ runtime/ui/layout_manager.go | 169 ---------- runtime/ui/view/details.go | 97 +++--- runtime/ui/view/filetree.go | 288 ++++++++++-------- runtime/ui/view/filter.go | 142 +++++---- runtime/ui/view/layer.go | 186 +++++------ runtime/ui/view/renderer.go | 3 + runtime/ui/view/status.go | 76 +++-- runtime/ui/view/views.go | 56 ++++ utils/view.go | 20 ++ 16 files changed, 867 insertions(+), 590 deletions(-) create mode 100644 runtime/ui/layout/compound/layer_details_column.go create mode 100644 runtime/ui/layout/layout.go create mode 100644 runtime/ui/layout/location.go create mode 100644 runtime/ui/layout/manager.go delete mode 100644 runtime/ui/layout_manager.go create mode 100644 runtime/ui/view/views.go create mode 100644 utils/view.go diff --git a/cmd/root.go b/cmd/root.go index 680632c..149ec05 100644 --- a/cmd/root.go +++ b/cmd/root.go @@ -141,6 +141,10 @@ func initLogging() { log.SetLevel(level) log.Debug("Starting Dive...") + log.Debugf("config filepath: %s", viper.ConfigFileUsed()) + for k, v := range viper.AllSettings() { + log.Debug("config value: ", k, " : ", v) + } } // getCfgFile checks for config file in paths from xdg specs diff --git a/runtime/ui/app.go b/runtime/ui/app.go index 0d77116..f4bc03d 100644 --- a/runtime/ui/app.go +++ b/runtime/ui/app.go @@ -3,6 +3,8 @@ package ui import ( "github.com/wagoodman/dive/dive/image" "github.com/wagoodman/dive/runtime/ui/key" + "github.com/wagoodman/dive/runtime/ui/layout" + "github.com/wagoodman/dive/runtime/ui/layout/compound" "sync" "github.com/jroimartin/gocui" @@ -16,7 +18,7 @@ const debug = false type app struct { gui *gocui.Gui controllers *Controller - layout *layoutManager + layout *layout.Manager } var ( @@ -27,19 +29,24 @@ var ( func newApp(gui *gocui.Gui, analysis *image.AnalysisResult, cache filetree.Comparer) (*app, error) { var err error once.Do(func() { - var theControls *Controller + var controller *Controller var globalHelpKeys []*key.Binding - theControls, err = NewCollection(gui, analysis, cache) + controller, err = NewCollection(gui, analysis, cache) if err != nil { return } - lm := newLayoutManager(theControls) + // note: order matters when adding elements to the layout + lm := layout.NewManager() + lm.Add(controller.views.Status, layout.LocationFooter) + lm.Add(controller.views.Filter, layout.LocationFooter) + lm.Add(compound.NewLayerDetailsCompoundLayout(controller.views.Layer, controller.views.Details), layout.LocationColumn) + lm.Add(controller.views.Tree, layout.LocationColumn) gui.Cursor = false //g.Mouse = true - gui.SetManagerFunc(lm.layout) + gui.SetManagerFunc(lm.Layout) // var profileObj = profile.Start(profile.CPUProfile, profile.ProfilePath("."), profile.NoShutdownHook) // @@ -49,7 +56,7 @@ func newApp(gui *gocui.Gui, analysis *image.AnalysisResult, cache filetree.Compa appSingleton = &app{ gui: gui, - controllers: theControls, + controllers: controller, layout: lm, } @@ -61,13 +68,13 @@ func newApp(gui *gocui.Gui, analysis *image.AnalysisResult, cache filetree.Compa }, { ConfigKeys: []string{"keybinding.toggle-view"}, - OnAction: theControls.ToggleView, + OnAction: controller.ToggleView, Display: "Switch view", }, { ConfigKeys: []string{"keybinding.filter-files"}, - OnAction: theControls.ToggleFilterView, - IsSelected: theControls.Filter.IsVisible, + OnAction: controller.ToggleFilterView, + IsSelected: controller.views.Filter.IsVisible, Display: "Filter", }, } @@ -77,10 +84,10 @@ func newApp(gui *gocui.Gui, analysis *image.AnalysisResult, cache filetree.Compa return } - theControls.Status.AddHelpKeys(globalHelpKeys...) + controller.views.Status.AddHelpKeys(globalHelpKeys...) // perform the first update and render now that all resources have been loaded - err = theControls.UpdateAndRender() + err = controller.UpdateAndRender() if err != nil { return } @@ -106,8 +113,6 @@ func newApp(gui *gocui.Gui, analysis *image.AnalysisResult, cache filetree.Compa // } // } -var lastX, lastY int - // quit is the gocui callback invoked when the user hits Ctrl+C func (a *app) quit() error { diff --git a/runtime/ui/controller.go b/runtime/ui/controller.go index 4a2fb70..1a801df 100644 --- a/runtime/ui/controller.go +++ b/runtime/ui/controller.go @@ -11,61 +11,33 @@ import ( ) type Controller struct { - gui *gocui.Gui - Tree *view.FileTree - Layer *view.Layer - Status *view.Status - Filter *view.Filter - Details *view.Details - lookup map[string]view.Renderer + gui *gocui.Gui + views *view.Views } func NewCollection(g *gocui.Gui, analysis *image.AnalysisResult, cache filetree.Comparer) (*Controller, error) { - var err error + views, err := view.NewViews(g, analysis, cache) + if err != nil { + return nil, err + } controller := &Controller{ - gui: g, + gui: g, + views: views, } - controller.lookup = make(map[string]view.Renderer) - - controller.Layer, err = view.NewLayerView("layers", g, analysis.Layers) - if err != nil { - return nil, err - } - controller.lookup[controller.Layer.Name()] = controller.Layer - - //treeStack, err := filetree.StackTreeRange(analysis.RefTrees, 0, 0) - //if err != nil { - // return nil, err - //} - treeStack := analysis.RefTrees[0] - controller.Tree, err = view.NewFileTreeView("filetree", g, treeStack, analysis.RefTrees, cache) - if err != nil { - return nil, err - } - controller.lookup[controller.Tree.Name()] = controller.Tree // layer view cursor down event should trigger an update in the file tree - controller.Layer.AddLayerChangeListener(controller.onLayerChange) - - controller.Status = view.NewStatusView("status", g) - controller.lookup[controller.Status.Name()] = controller.Status - // set the layer view as the first selected view - controller.Status.SetCurrentView(controller.Layer) + controller.views.Layer.AddLayerChangeListener(controller.onLayerChange) // update the status pane when a filetree option is changed by the user - controller.Tree.AddViewOptionChangeListener(controller.onFileTreeViewOptionChange) + controller.views.Tree.AddViewOptionChangeListener(controller.onFileTreeViewOptionChange) - controller.Filter = view.NewFilterView("filter", g) - controller.lookup[controller.Filter.Name()] = controller.Filter - controller.Filter.AddFilterEditListener(controller.onFilterEdit) - - controller.Details = view.NewDetailsView("details", g, analysis.Efficiency, analysis.Inefficiencies, analysis.SizeBytes) - controller.lookup[controller.Details.Name()] = controller.Details + // update the tree view while the user types into the filter view + controller.views.Filter.AddFilterEditListener(controller.onFilterEdit) // propagate initial conditions to necessary views err = controller.onLayerChange(viewmodel.LayerSelection{ - Layer: controller.Layer.CurrentLayer(), + Layer: controller.views.Layer.CurrentLayer(), BottomTreeStart: 0, BottomTreeStop: 0, TopTreeStart: 0, @@ -80,11 +52,11 @@ func NewCollection(g *gocui.Gui, analysis *image.AnalysisResult, cache filetree. } func (c *Controller) onFileTreeViewOptionChange() error { - err := c.Status.Update() + err := c.views.Status.Update() if err != nil { return err } - return c.Status.Render() + return c.views.Status.Render() } func (c *Controller) onFilterEdit(filter string) error { @@ -98,30 +70,30 @@ func (c *Controller) onFilterEdit(filter string) error { } } - c.Tree.SetFilterRegex(filterRegex) + c.views.Tree.SetFilterRegex(filterRegex) - err = c.Tree.Update() + err = c.views.Tree.Update() if err != nil { return err } - return c.Tree.Render() + return c.views.Tree.Render() } func (c *Controller) onLayerChange(selection viewmodel.LayerSelection) error { // update the details - c.Details.SetCurrentLayer(selection.Layer) + c.views.Details.SetCurrentLayer(selection.Layer) // update the filetree - err := c.Tree.SetTree(selection.BottomTreeStart, selection.BottomTreeStop, selection.TopTreeStart, selection.TopTreeStop) + err := c.views.Tree.SetTree(selection.BottomTreeStart, selection.BottomTreeStop, selection.TopTreeStart, selection.TopTreeStop) if err != nil { return err } - if c.Layer.CompareMode == view.CompareAll { - c.Tree.SetTitle("Aggregated Layer Contents") + if c.views.Layer.CompareMode == view.CompareAll { + c.views.Tree.SetTitle("Aggregated Layer Contents") } else { - c.Tree.SetTitle("Current Layer Contents") + c.views.Tree.SetTitle("Current Layer Contents") } // update details and filetree panes @@ -146,7 +118,7 @@ func (c *Controller) UpdateAndRender() error { // Update refreshes the state objects for future rendering. func (c *Controller) Update() error { - for _, controller := range c.lookup { + for _, controller := range c.views.All() { err := controller.Update() if err != nil { logrus.Debug("unable to update controller: ") @@ -158,7 +130,7 @@ func (c *Controller) Update() error { // Render flushes the state objects to the screen. func (c *Controller) Render() error { - for _, controller := range c.lookup { + for _, controller := range c.views.All() { if controller.IsVisible() { err := controller.Render() if err != nil { @@ -172,12 +144,12 @@ func (c *Controller) Render() error { // ToggleView switches between the file view and the layer view and re-renders the screen. func (c *Controller) ToggleView() (err error) { v := c.gui.CurrentView() - if v == nil || v.Name() == c.Layer.Name() { - _, err = c.gui.SetCurrentView(c.Tree.Name()) - c.Status.SetCurrentView(c.Tree) + if v == nil || v.Name() == c.views.Layer.Name() { + _, err = c.gui.SetCurrentView(c.views.Tree.Name()) + c.views.Status.SetCurrentView(c.views.Tree) } else { - _, err = c.gui.SetCurrentView(c.Layer.Name()) - c.Status.SetCurrentView(c.Layer) + _, err = c.gui.SetCurrentView(c.views.Layer.Name()) + c.views.Status.SetCurrentView(c.views.Layer) } if err != nil { @@ -190,16 +162,16 @@ func (c *Controller) ToggleView() (err error) { func (c *Controller) ToggleFilterView() error { // delete all user input from the tree view - err := c.Filter.ToggleVisible() + err := c.views.Filter.ToggleVisible() if err != nil { logrus.Error("unable to toggle filter visibility: ", err) return err } // we have just hidden the filter view... - if !c.Filter.IsVisible() { + if !c.views.Filter.IsVisible() { // ...remove any filter from the tree - c.Tree.SetFilterRegex(nil) + c.views.Tree.SetFilterRegex(nil) // ...adjust focus to a valid (visible) view err = c.ToggleView() diff --git a/runtime/ui/layout/compound/layer_details_column.go b/runtime/ui/layout/compound/layer_details_column.go new file mode 100644 index 0000000..61cd127 --- /dev/null +++ b/runtime/ui/layout/compound/layer_details_column.go @@ -0,0 +1,96 @@ +package compound + +import ( + "github.com/jroimartin/gocui" + "github.com/sirupsen/logrus" + "github.com/wagoodman/dive/runtime/ui/view" + "github.com/wagoodman/dive/utils" +) + +type LayerDetailsCompoundLayout struct { + layer *view.Layer + details *view.Details +} + +func NewLayerDetailsCompoundLayout(layer *view.Layer, details *view.Details) *LayerDetailsCompoundLayout { + return &LayerDetailsCompoundLayout{ + layer: layer, + details: details, + } +} + +func (cl *LayerDetailsCompoundLayout) Name() string { + return "layer-details-compound-column" +} + +func (cl *LayerDetailsCompoundLayout) Layout(g *gocui.Gui, minX, minY, maxX, maxY int, hasResized bool) error { + logrus.Debugf("view.Layout(minX: %d, minY: %d, maxX: %d, maxY: %d) %s", minX, minY, maxX, maxY, cl.Name()) + + //////////////////////////////////////////////////////////////////////////////////// + // Layers View + + // header + border + layerHeaderHeight := 2 + + layersHeight := len(cl.layer.Layers) + layerHeaderHeight + 1 // layers + header + base image layer row + maxLayerHeight := int(0.75 * float64(maxY)) + if layersHeight > maxLayerHeight { + layersHeight = maxLayerHeight + } + + // note: maxY needs to account for the (invisible) border, thus a +1 + header, headerErr := g.SetView(cl.layer.Name()+"header", minX, minY, maxX, minY+layerHeaderHeight+1) + + // we are going to overlap the view over the (invisible) border (so minY will be one less than expected) + main, viewErr := g.SetView(cl.layer.Name(), minX, minY+layerHeaderHeight, maxX, minY+layerHeaderHeight+layersHeight) + + if utils.IsNewView(viewErr, headerErr) { + err := cl.layer.Setup(main, header) + if err != nil { + logrus.Error("unable to setup layer layout", err) + return err + } + + if _, err = g.SetCurrentView(cl.layer.Name()); err != nil { + logrus.Error("unable to set view to layer", err) + return err + } + // since we are selecting the view, we should rerender to indicate it is selected + err = cl.layer.Render() + if err != nil { + logrus.Error("unable to render layer view", err) + return err + } + } + + //////////////////////////////////////////////////////////////////////////////////// + // Details + detailsMinY := minY + layersHeight + + // 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) + + // 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 { + return nil +} + +// todo: make this variable based on the nested views +func (cl *LayerDetailsCompoundLayout) IsVisible() bool { + return true +} diff --git a/runtime/ui/layout/layout.go b/runtime/ui/layout/layout.go new file mode 100644 index 0000000..52be197 --- /dev/null +++ b/runtime/ui/layout/layout.go @@ -0,0 +1,10 @@ +package layout + +import "github.com/jroimartin/gocui" + +type Layout interface { + Name() string + Layout(g *gocui.Gui, minX, minY, maxX, maxY int, hasResized bool) error + RequestedSize(available int) *int + IsVisible() bool +} diff --git a/runtime/ui/layout/location.go b/runtime/ui/layout/location.go new file mode 100644 index 0000000..ee1d616 --- /dev/null +++ b/runtime/ui/layout/location.go @@ -0,0 +1,9 @@ +package layout + +const ( + LocationFooter Location = iota + LocationHeader + LocationColumn +) + +type Location int diff --git a/runtime/ui/layout/manager.go b/runtime/ui/layout/manager.go new file mode 100644 index 0000000..cbb25a2 --- /dev/null +++ b/runtime/ui/layout/manager.go @@ -0,0 +1,176 @@ +package layout + +import ( + "github.com/jroimartin/gocui" + "github.com/sirupsen/logrus" +) + +type Manager struct { + lastX, lastY int + elements map[Location][]Layout +} + +func NewManager() *Manager { + return &Manager{ + elements: make(map[Location][]Layout), + } +} + +func (lm *Manager) Add(element Layout, location Location) { + if _, exists := lm.elements[location]; !exists { + lm.elements[location] = make([]Layout, 0) + } + lm.elements[location] = append(lm.elements[location], element) +} + +// layout defines the definition of the window pane size and placement relations to one another. This +// is invoked at application start and whenever the screen dimensions change. +// A few things to note: +// 1. gocui has borders around all views (even if Frame=false). This means there are a lot of +1/-1 magic numbers +// needed (but there are comments!). +// 2. since there are borders, in order for it to appear as if there aren't any spaces for borders, the views must +// overlap. To prevent screen artifacts, all elements must be layedout from the top of the screen to the bottom. +func (lm *Manager) Layout(g *gocui.Gui) error { + + minX, minY := -1, -1 + maxX, maxY := g.Size() + + var hasResized bool + if maxX != lm.lastX || maxY != lm.lastY { + hasResized = true + } + lm.lastX, lm.lastY = maxX, maxY + + // layout headers top down + if elements, exists := lm.elements[LocationHeader]; exists { + for _, element := range elements { + // a visible header cannot take up the whole screen, default to 1. + // this eliminates the need to discover a default size based on all element requests + height := 0 + if element.IsVisible() { + requestedHeight := element.RequestedSize(maxY) + if requestedHeight != nil { + height = *requestedHeight + } else { + height = 1 + } + } + + // layout the header within the allocated space + err := element.Layout(g, minX, minY, maxX, minY+height, hasResized) + if err != nil { + logrus.Errorf("failed to layout '%s' header: %+v", element.Name(), err) + } + + // restrict the available screen real estate + minY += height + + } + } + + var footerHeights = make([]int, 0) + // we need to keep the current maxY before carving out the space for the body columns + var footerMaxY = maxY + var footerMinX = minX + var footerMaxX = maxX + + // we need to layout the footers last, but account for them when drawing the columns. This block is for planning + // out the real estate needed for the footers now (but not laying out yet) + if elements, exists := lm.elements[LocationFooter]; exists { + footerHeights = make([]int, len(elements)) + for idx := range footerHeights { + footerHeights[idx] = 1 + } + + for idx, element := range elements { + // a visible footer cannot take up the whole screen, default to 1. + // this eliminates the need to discover a default size based on all element requests + height := 0 + if element.IsVisible() { + requestedHeight := element.RequestedSize(maxY) + if requestedHeight != nil { + height = *requestedHeight + } else { + height = 1 + } + } + footerHeights[idx] = height + } + // restrict the available screen real estate + for _, height := range footerHeights { + maxY -= height + } + } + + // layout columns left to right + if elements, exists := lm.elements[LocationColumn]; exists { + widths := make([]int, len(elements)) + for idx := range widths { + widths[idx] = -1 + } + variableColumns := len(elements) + availableWidth := maxX + + // first pass: planout the column sizes based on the given requests + for idx, element := range elements { + if !element.IsVisible() { + widths[idx] = 0 + variableColumns-- + continue + } + + requestedWidth := element.RequestedSize(availableWidth) + if requestedWidth != nil { + widths[idx] = *requestedWidth + variableColumns-- + availableWidth -= widths[idx] + } + } + + defaultWidth := int(availableWidth / variableColumns) + + // second pass: layout columns left to right (based off predetermined widths) + for idx, element := range elements { + // use the requested or default width + width := widths[idx] + if width == -1 { + width = defaultWidth + } + + // layout the column within the allocated space + err := element.Layout(g, minX, minY, minX+width, maxY, hasResized) + if err != nil { + logrus.Errorf("failed to layout '%s' column: %+v", element.Name(), err) + } + + // move left to right, scratching off real estate as it is taken + minX += width + + } + } + + // layout footers top down (which is why the list is reversed). Top down is needed due to border overlap. + if elements, exists := lm.elements[LocationFooter]; exists { + for idx := len(elements) - 1; idx >= 0; idx-- { + element := elements[idx] + height := footerHeights[idx] + var topY, bottomY, bottomPadding int + for oIdx := 0; oIdx <= idx; oIdx++ { + bottomPadding += footerHeights[oIdx] + } + topY = footerMaxY - bottomPadding - height + // +1 for border + bottomY = topY + height + 1 + + // layout the footer within the allocated space + // note: since the headers and rows are inclusive counting from -1 (to account for a border) we must + // do the same vertically, thus a -1 is needed for a starting Y + err := element.Layout(g, footerMinX, topY, footerMaxX, bottomY, hasResized) + if err != nil { + logrus.Errorf("failed to layout '%s' footer: %+v", element.Name(), err) + } + } + } + + return nil +} diff --git a/runtime/ui/layout_manager.go b/runtime/ui/layout_manager.go deleted file mode 100644 index 72f8c26..0000000 --- a/runtime/ui/layout_manager.go +++ /dev/null @@ -1,169 +0,0 @@ -package ui - -import ( - "github.com/jroimartin/gocui" - "github.com/sirupsen/logrus" - "github.com/spf13/viper" -) - -type layoutManager struct { - fileTreeSplitRatio float64 - controllers *Controller -} - -// todo: this needs a major refactor (derive layout from view obj info, which should not live here) -func newLayoutManager(c *Controller) *layoutManager { - - fileTreeSplitRatio := viper.GetFloat64("filetree.pane-width") - if fileTreeSplitRatio >= 1 || fileTreeSplitRatio <= 0 { - logrus.Errorf("invalid config value: 'filetree.pane-width' should be 0 < value < 1, given '%v'", fileTreeSplitRatio) - fileTreeSplitRatio = 0.5 - } - - return &layoutManager{ - fileTreeSplitRatio: fileTreeSplitRatio, - controllers: c, - } -} - -// IsNewView determines if a view has already been created based on the set of errors given (a bit hokie) -func IsNewView(errs ...error) bool { - for _, err := range errs { - if err == nil { - return false - } - if err != gocui.ErrUnknownView { - return false - } - } - return true -} - -// layout defines the definition of the window pane size and placement relations to one another. This -// is invoked at application start and whenever the screen dimensions change. -func (lm *layoutManager) layout(g *gocui.Gui) error { - // TODO: this logic should be refactored into an abstraction that takes care of the math for us - - maxX, maxY := g.Size() - var resized bool - if maxX != lastX { - resized = true - } - if maxY != lastY { - resized = true - } - lastX, lastY = maxX, maxY - - splitCols := int(float64(maxX) * (1.0 - lm.fileTreeSplitRatio)) - debugWidth := 0 - if debug { - debugWidth = maxX / 4 - } - debugCols := maxX - debugWidth - bottomRows := 1 - headerRows := 2 - - filterBarHeight := 1 - statusBarHeight := 1 - - statusBarIndex := 1 - filterBarIndex := 2 - - layersHeight := len(lm.controllers.Layer.Layers) + headerRows + 1 // layers + header + base image layer row - maxLayerHeight := int(0.75 * float64(maxY)) - if layersHeight > maxLayerHeight { - layersHeight = maxLayerHeight - } - - var view, header *gocui.View - var viewErr, headerErr, err error - - if !lm.controllers.Filter.IsVisible() { - bottomRows-- - filterBarHeight = 0 - } - - // Debug pane - if debug { - if _, err := g.SetView("debug", debugCols, -1, maxX, maxY-bottomRows); err != nil { - if err != gocui.ErrUnknownView { - return err - } - } - } - - // Layers - view, viewErr = g.SetView(lm.controllers.Layer.Name(), -1, -1+headerRows, splitCols, layersHeight) - header, headerErr = g.SetView(lm.controllers.Layer.Name()+"header", -1, -1, splitCols, headerRows) - if IsNewView(viewErr, headerErr) { - err = lm.controllers.Layer.Setup(view, header) - if err != nil { - logrus.Error("unable to setup layer controller", err) - return err - } - - if _, err = g.SetCurrentView(lm.controllers.Layer.Name()); err != nil { - logrus.Error("unable to set view to layer", err) - return err - } - // since we are selecting the view, we should rerender to indicate it is selected - err = lm.controllers.Layer.Render() - if err != nil { - logrus.Error("unable to render layer view", err) - return err - } - } - - // Details - view, viewErr = g.SetView(lm.controllers.Details.Name(), -1, -1+layersHeight+headerRows, splitCols, maxY-bottomRows) - header, headerErr = g.SetView(lm.controllers.Details.Name()+"header", -1, -1+layersHeight, splitCols, layersHeight+headerRows) - if IsNewView(viewErr, headerErr) { - err = lm.controllers.Details.Setup(view, header) - if err != nil { - return err - } - } - - // Filetree - offset := 0 - if !lm.controllers.Tree.AreAttributesVisible() { - offset = 1 - } - view, viewErr = g.SetView(lm.controllers.Tree.Name(), splitCols, -1+headerRows-offset, debugCols, maxY-bottomRows) - header, headerErr = g.SetView(lm.controllers.Tree.Name()+"header", splitCols, -1, debugCols, headerRows-offset) - if IsNewView(viewErr, headerErr) { - err = lm.controllers.Tree.Setup(view, header) - if err != nil { - logrus.Error("unable to setup tree controller", err) - return err - } - } - err = lm.controllers.Tree.OnLayoutChange(resized) - if err != nil { - logrus.Error("unable to setup layer controller onLayoutChange", err) - return err - } - - // Status Bar - view, viewErr = g.SetView(lm.controllers.Status.Name(), -1, maxY-statusBarHeight-statusBarIndex, maxX, maxY-(statusBarIndex-1)) - if IsNewView(viewErr, headerErr) { - err = lm.controllers.Status.Setup(view, nil) - if err != nil { - logrus.Error("unable to setup status controller", err) - return err - } - } - - // Filter Bar - view, viewErr = g.SetView(lm.controllers.Filter.Name(), len(lm.controllers.Filter.HeaderStr())-1, maxY-filterBarHeight-filterBarIndex, maxX, maxY-(filterBarIndex-1)) - header, headerErr = g.SetView(lm.controllers.Filter.Name()+"header", -1, maxY-filterBarHeight-filterBarIndex, len(lm.controllers.Filter.HeaderStr()), maxY-(filterBarIndex-1)) - if IsNewView(viewErr, headerErr) { - err = lm.controllers.Filter.Setup(view, header) - if err != nil { - logrus.Error("unable to setup filter controller", err) - return err - } - } - - return nil -} diff --git a/runtime/ui/view/details.go b/runtime/ui/view/details.go index 9cb5ce0..a82a6a4 100644 --- a/runtime/ui/view/details.go +++ b/runtime/ui/view/details.go @@ -29,8 +29,8 @@ type Details struct { currentLayer *image.Layer } -// NewDetailsView creates a new view object attached the the global [gocui] screen object. -func NewDetailsView(name string, gui *gocui.Gui, efficiency float64, inefficiencies filetree.EfficiencySlice, imageSize uint64) (controller *Details) { +// newDetailsView creates a new view object attached the the global [gocui] screen object. +func newDetailsView(name string, gui *gocui.Gui, efficiency float64, inefficiencies filetree.EfficiencySlice, imageSize uint64) (controller *Details) { controller = new(Details) // populate main fields @@ -43,68 +43,69 @@ func NewDetailsView(name string, gui *gocui.Gui, efficiency float64, inefficienc return controller } -func (c *Details) Name() string { - return c.name +func (v *Details) Name() string { + return v.name } // Setup initializes the UI concerns within the context of a global [gocui] view object. -func (c *Details) Setup(v *gocui.View, header *gocui.View) error { +func (v *Details) Setup(view *gocui.View, header *gocui.View) error { + logrus.Debugf("view.Setup() %s", v.Name()) // set controller options - c.view = v - c.view.Editable = false - c.view.Wrap = true - c.view.Highlight = false - c.view.Frame = false + v.view = view + v.view.Editable = false + v.view.Wrap = true + v.view.Highlight = false + v.view.Frame = false - c.header = header - c.header.Editable = false - c.header.Wrap = false - c.header.Frame = false + v.header = header + v.header.Editable = false + v.header.Wrap = false + v.header.Frame = false var infos = []key.BindingInfo{ { Key: gocui.KeyArrowDown, Modifier: gocui.ModNone, - OnAction: c.CursorDown, + OnAction: v.CursorDown, }, { Key: gocui.KeyArrowUp, Modifier: gocui.ModNone, - OnAction: c.CursorUp, + OnAction: v.CursorUp, }, } - _, err := key.GenerateBindings(c.gui, c.name, infos) + _, err := key.GenerateBindings(v.gui, v.name, infos) if err != nil { return err } - return c.Render() + return v.Render() } // IsVisible indicates if the details view pane is currently initialized. -func (c *Details) IsVisible() bool { - return c != nil +func (v *Details) IsVisible() bool { + return v != nil } // CursorDown moves the cursor down in the details pane (currently indicates nothing). -func (c *Details) CursorDown() error { - return CursorDown(c.gui, c.view) +func (v *Details) CursorDown() error { + return CursorDown(v.gui, v.view) } // CursorUp moves the cursor up in the details pane (currently indicates nothing). -func (c *Details) CursorUp() error { - return CursorUp(c.gui, c.view) +func (v *Details) CursorUp() error { + return CursorUp(v.gui, v.view) } // Update refreshes the state objects for future rendering. -func (c *Details) Update() error { +func (v *Details) Update() error { return nil } -func (c *Details) SetCurrentLayer(layer *image.Layer) { - c.currentLayer = layer +func (v *Details) SetCurrentLayer(layer *image.Layer) { + v.currentLayer = layer } // Render flushes the state objects to the screen. The details pane reports: @@ -112,8 +113,10 @@ func (c *Details) SetCurrentLayer(layer *image.Layer) { // 2. the image efficiency score // 3. the estimated wasted image space // 4. a list of inefficient file allocations -func (c *Details) Render() error { - if c.currentLayer == nil { +func (v *Details) Render() error { + logrus.Debugf("view.Render() %s", v.Name()) + + if v.currentLayer == nil { return fmt.Errorf("no layer selected") } @@ -123,12 +126,12 @@ func (c *Details) Render() error { inefficiencyReport := fmt.Sprintf(format.Header(template), "Count", "Total Space", "Path") height := 100 - if c.view != nil { - _, height = c.view.Size() + if v.view != nil { + _, height = v.view.Size() } - for idx := 0; idx < len(c.inefficiencies); idx++ { - data := c.inefficiencies[len(c.inefficiencies)-1-idx] + for idx := 0; idx < len(v.inefficiencies); idx++ { + data := v.inefficiencies[len(v.inefficiencies)-1-idx] wastedSpace += data.CumulativeSize // todo: make this report scrollable @@ -137,43 +140,43 @@ func (c *Details) Render() error { } } - imageSizeStr := fmt.Sprintf("%s %s", format.Header("Total Image size:"), humanize.Bytes(c.imageSize)) - effStr := fmt.Sprintf("%s %d %%", format.Header("Image efficiency score:"), int(100.0*c.efficiency)) + imageSizeStr := fmt.Sprintf("%s %s", format.Header("Total Image size:"), humanize.Bytes(v.imageSize)) + effStr := fmt.Sprintf("%s %d %%", format.Header("Image efficiency score:"), int(100.0*v.efficiency)) wastedSpaceStr := fmt.Sprintf("%s %s", format.Header("Potential wasted space:"), humanize.Bytes(uint64(wastedSpace))) - c.gui.Update(func(g *gocui.Gui) error { + v.gui.Update(func(g *gocui.Gui) error { // update header - c.header.Clear() - width, _ := c.view.Size() + v.header.Clear() + width, _ := v.view.Size() layerHeaderStr := fmt.Sprintf("[Layer Details]%s", strings.Repeat("─", width-15)) imageHeaderStr := fmt.Sprintf("[Image Details]%s", strings.Repeat("─", width-15)) - _, err := fmt.Fprintln(c.header, format.Header(vtclean.Clean(layerHeaderStr, false))) + _, err := fmt.Fprintln(v.header, format.Header(vtclean.Clean(layerHeaderStr, false))) if err != nil { return err } // update contents - c.view.Clear() + v.view.Clear() var lines = make([]string, 0) - if c.currentLayer.Names != nil && len(c.currentLayer.Names) > 0 { - lines = append(lines, format.Header("Tags: ")+strings.Join(c.currentLayer.Names, ", ")) + if v.currentLayer.Names != nil && len(v.currentLayer.Names) > 0 { + lines = append(lines, format.Header("Tags: ")+strings.Join(v.currentLayer.Names, ", ")) } else { lines = append(lines, format.Header("Tags: ")+"(none)") } - lines = append(lines, format.Header("Id: ")+c.currentLayer.Id) - lines = append(lines, format.Header("Digest: ")+c.currentLayer.Digest) + lines = append(lines, format.Header("Id: ")+v.currentLayer.Id) + lines = append(lines, format.Header("Digest: ")+v.currentLayer.Digest) lines = append(lines, format.Header("Command:")) - lines = append(lines, c.currentLayer.Command) + lines = append(lines, v.currentLayer.Command) lines = append(lines, "\n"+format.Header(vtclean.Clean(imageHeaderStr, false))) lines = append(lines, imageSizeStr) lines = append(lines, wastedSpaceStr) lines = append(lines, effStr+"\n") lines = append(lines, inefficiencyReport) - _, err = fmt.Fprintln(c.view, strings.Join(lines, "\n")) + _, err = fmt.Fprintln(v.view, strings.Join(lines, "\n")) if err != nil { logrus.Debug("unable to write to buffer: ", err) } @@ -183,6 +186,6 @@ func (c *Details) Render() error { } // KeyHelp indicates all the possible actions a user can take while the current pane is selected (currently does nothing). -func (c *Details) KeyHelp() string { +func (v *Details) KeyHelp() string { return "TBD" } diff --git a/runtime/ui/view/filetree.go b/runtime/ui/view/filetree.go index d460892..0cd324d 100644 --- a/runtime/ui/view/filetree.go +++ b/runtime/ui/view/filetree.go @@ -3,9 +3,11 @@ package view import ( "fmt" "github.com/sirupsen/logrus" + "github.com/spf13/viper" "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" "regexp" "strings" @@ -33,15 +35,14 @@ type FileTree struct { vm *viewmodel.FileTree title string - filterRegex *regexp.Regexp - - listeners []ViewOptionChangeListener - - helpKeys []*key.Binding + filterRegex *regexp.Regexp + listeners []ViewOptionChangeListener + helpKeys []*key.Binding + requestedWidthRatio float64 } -// NewFileTreeView creates a new view object attached the the global [gocui] screen object. -func NewFileTreeView(name string, gui *gocui.Gui, tree *filetree.FileTree, refTrees []*filetree.FileTree, cache filetree.Comparer) (controller *FileTree, err error) { +// newFileTreeView creates a new view object attached the the global [gocui] screen object. +func newFileTreeView(name string, gui *gocui.Gui, tree *filetree.FileTree, refTrees []*filetree.FileTree, cache filetree.Comparer) (controller *FileTree, err error) { controller = new(FileTree) controller.listeners = make([]ViewOptionChangeListener, 0) @@ -53,157 +54,165 @@ func NewFileTreeView(name string, gui *gocui.Gui, tree *filetree.FileTree, refTr return nil, err } + requestedWidthRatio := viper.GetFloat64("filetree.pane-width") + if requestedWidthRatio >= 1 || requestedWidthRatio <= 0 { + logrus.Errorf("invalid config value: 'filetree.pane-width' should be 0 < value < 1, given '%v'", requestedWidthRatio) + requestedWidthRatio = 0.5 + } + controller.requestedWidthRatio = requestedWidthRatio + return controller, err } -func (c *FileTree) AddViewOptionChangeListener(listener ...ViewOptionChangeListener) { - c.listeners = append(c.listeners, listener...) +func (v *FileTree) AddViewOptionChangeListener(listener ...ViewOptionChangeListener) { + v.listeners = append(v.listeners, listener...) } -func (c *FileTree) SetTitle(title string) { - c.title = title +func (v *FileTree) SetTitle(title string) { + v.title = title } -func (c *FileTree) SetFilterRegex(filterRegex *regexp.Regexp) { - c.filterRegex = filterRegex +func (v *FileTree) SetFilterRegex(filterRegex *regexp.Regexp) { + v.filterRegex = filterRegex } -func (c *FileTree) Name() string { - return c.name +func (v *FileTree) Name() string { + return v.name } -func (c *FileTree) AreAttributesVisible() bool { - return c.vm.ShowAttributes +func (v *FileTree) areAttributesVisible() bool { + return v.vm.ShowAttributes } // Setup initializes the UI concerns within the context of a global [gocui] view object. -func (c *FileTree) Setup(v *gocui.View, header *gocui.View) error { +func (v *FileTree) Setup(view *gocui.View, header *gocui.View) error { + logrus.Debugf("view.Setup() %s", v.Name()) // set controller options - c.view = v - c.view.Editable = false - c.view.Wrap = false - c.view.Frame = false + v.view = view + v.view.Editable = false + v.view.Wrap = false + v.view.Frame = false - c.header = header - c.header.Editable = false - c.header.Wrap = false - c.header.Frame = false + v.header = header + v.header.Editable = false + v.header.Wrap = false + v.header.Frame = false var infos = []key.BindingInfo{ { ConfigKeys: []string{"keybinding.toggle-collapse-dir"}, - OnAction: c.toggleCollapse, + OnAction: v.toggleCollapse, Display: "Collapse dir", }, { ConfigKeys: []string{"keybinding.toggle-collapse-all-dir"}, - OnAction: c.toggleCollapseAll, + OnAction: v.toggleCollapseAll, Display: "Collapse all dir", }, { ConfigKeys: []string{"keybinding.toggle-added-files"}, - OnAction: func() error { return c.toggleShowDiffType(filetree.Added) }, - IsSelected: func() bool { return !c.vm.HiddenDiffTypes[filetree.Added] }, + OnAction: func() error { return v.toggleShowDiffType(filetree.Added) }, + IsSelected: func() bool { return !v.vm.HiddenDiffTypes[filetree.Added] }, Display: "Added", }, { ConfigKeys: []string{"keybinding.toggle-removed-files"}, - OnAction: func() error { return c.toggleShowDiffType(filetree.Removed) }, - IsSelected: func() bool { return !c.vm.HiddenDiffTypes[filetree.Removed] }, + OnAction: func() error { return v.toggleShowDiffType(filetree.Removed) }, + IsSelected: func() bool { return !v.vm.HiddenDiffTypes[filetree.Removed] }, Display: "Removed", }, { ConfigKeys: []string{"keybinding.toggle-modified-files"}, - OnAction: func() error { return c.toggleShowDiffType(filetree.Modified) }, - IsSelected: func() bool { return !c.vm.HiddenDiffTypes[filetree.Modified] }, + OnAction: func() error { return v.toggleShowDiffType(filetree.Modified) }, + IsSelected: func() bool { return !v.vm.HiddenDiffTypes[filetree.Modified] }, Display: "Modified", }, { ConfigKeys: []string{"keybinding.toggle-unchanged-files", "keybinding.toggle-unmodified-files"}, - OnAction: func() error { return c.toggleShowDiffType(filetree.Unmodified) }, - IsSelected: func() bool { return !c.vm.HiddenDiffTypes[filetree.Unmodified] }, + OnAction: func() error { return v.toggleShowDiffType(filetree.Unmodified) }, + IsSelected: func() bool { return !v.vm.HiddenDiffTypes[filetree.Unmodified] }, Display: "Unmodified", }, { ConfigKeys: []string{"keybinding.toggle-filetree-attributes"}, - OnAction: c.toggleAttributes, - IsSelected: func() bool { return c.vm.ShowAttributes }, + OnAction: v.toggleAttributes, + IsSelected: func() bool { return v.vm.ShowAttributes }, Display: "Attributes", }, { ConfigKeys: []string{"keybinding.page-up"}, - OnAction: c.PageUp, + OnAction: v.PageUp, }, { ConfigKeys: []string{"keybinding.page-down"}, - OnAction: c.PageDown, + OnAction: v.PageDown, }, { Key: gocui.KeyArrowDown, Modifier: gocui.ModNone, - OnAction: c.CursorDown, + OnAction: v.CursorDown, }, { Key: gocui.KeyArrowUp, Modifier: gocui.ModNone, - OnAction: c.CursorUp, + OnAction: v.CursorUp, }, { Key: gocui.KeyArrowLeft, Modifier: gocui.ModNone, - OnAction: c.CursorLeft, + OnAction: v.CursorLeft, }, { Key: gocui.KeyArrowRight, Modifier: gocui.ModNone, - OnAction: c.CursorRight, + OnAction: v.CursorRight, }, } - helpKeys, err := key.GenerateBindings(c.gui, c.name, infos) + helpKeys, err := key.GenerateBindings(v.gui, v.name, infos) if err != nil { return err } - c.helpKeys = helpKeys + v.helpKeys = helpKeys - _, height := c.view.Size() - c.vm.Setup(0, height) - _ = c.Update() - _ = c.Render() + _, height := v.view.Size() + v.vm.Setup(0, height) + _ = v.Update() + _ = v.Render() return nil } // IsVisible indicates if the file tree view pane is currently initialized -func (c *FileTree) IsVisible() bool { - return c != nil +func (v *FileTree) IsVisible() bool { + return v != nil } // ResetCursor moves the cursor back to the top of the buffer and translates to the top of the buffer. -func (c *FileTree) resetCursor() { - _ = c.view.SetCursor(0, 0) - c.vm.ResetCursor() +func (v *FileTree) resetCursor() { + _ = v.view.SetCursor(0, 0) + v.vm.ResetCursor() } // SetTreeByLayer populates the view model by stacking the indicated image layer file trees. -func (c *FileTree) SetTree(bottomTreeStart, bottomTreeStop, topTreeStart, topTreeStop int) error { - err := c.vm.SetTreeByLayer(bottomTreeStart, bottomTreeStop, topTreeStart, topTreeStop) +func (v *FileTree) SetTree(bottomTreeStart, bottomTreeStop, topTreeStart, topTreeStop int) error { + err := v.vm.SetTreeByLayer(bottomTreeStart, bottomTreeStop, topTreeStart, topTreeStop) if err != nil { return err } - _ = c.Update() - return c.Render() + _ = v.Update() + return v.Render() } // CursorDown moves the cursor down and renders the view. // Note: we cannot use the gocui buffer since any state change requires writing the entire tree to the buffer. // Instead we are keeping an upper and lower bounds of the tree string to render and only flushing // this range into the view buffer. This is much faster when tree sizes are large. -func (c *FileTree) CursorDown() error { - if c.vm.CursorDown() { - return c.Render() +func (v *FileTree) CursorDown() error { + if v.vm.CursorDown() { + return v.Render() } return nil } @@ -212,49 +221,49 @@ func (c *FileTree) CursorDown() error { // Note: we cannot use the gocui buffer since any state change requires writing the entire tree to the buffer. // Instead we are keeping an upper and lower bounds of the tree string to render and only flushing // this range into the view buffer. This is much faster when tree sizes are large. -func (c *FileTree) CursorUp() error { - if c.vm.CursorUp() { - return c.Render() +func (v *FileTree) CursorUp() error { + if v.vm.CursorUp() { + return v.Render() } return nil } // CursorLeft moves the cursor up until we reach the Parent Node or top of the tree -func (c *FileTree) CursorLeft() error { - err := c.vm.CursorLeft(c.filterRegex) +func (v *FileTree) CursorLeft() error { + err := v.vm.CursorLeft(v.filterRegex) if err != nil { return err } - _ = c.Update() - return c.Render() + _ = v.Update() + return v.Render() } // CursorRight descends into directory expanding it if needed -func (c *FileTree) CursorRight() error { - err := c.vm.CursorRight(c.filterRegex) +func (v *FileTree) CursorRight() error { + err := v.vm.CursorRight(v.filterRegex) if err != nil { return err } - _ = c.Update() - return c.Render() + _ = v.Update() + return v.Render() } // PageDown moves to next page putting the cursor on top -func (c *FileTree) PageDown() error { - err := c.vm.PageDown() +func (v *FileTree) PageDown() error { + err := v.vm.PageDown() if err != nil { return err } - return c.Render() + return v.Render() } // PageUp moves to previous page putting the cursor on top -func (c *FileTree) PageUp() error { - err := c.vm.PageUp() +func (v *FileTree) PageUp() error { + err := v.vm.PageUp() if err != nil { return err } - return c.Render() + return v.Render() } // getAbsPositionNode determines the selected screen cursor's location in the file tree, returning the selected FileNode. @@ -263,30 +272,30 @@ func (c *FileTree) PageUp() error { // } // ToggleCollapse will collapse/expand the selected FileNode. -func (c *FileTree) toggleCollapse() error { - err := c.vm.ToggleCollapse(c.filterRegex) +func (v *FileTree) toggleCollapse() error { + err := v.vm.ToggleCollapse(v.filterRegex) if err != nil { return err } - _ = c.Update() - return c.Render() + _ = v.Update() + return v.Render() } // ToggleCollapseAll will collapse/expand the all directories. -func (c *FileTree) toggleCollapseAll() error { - err := c.vm.ToggleCollapseAll() +func (v *FileTree) toggleCollapseAll() error { + err := v.vm.ToggleCollapseAll() if err != nil { return err } - if c.vm.CollapseAll { - c.resetCursor() + if v.vm.CollapseAll { + v.resetCursor() } - _ = c.Update() - return c.Render() + _ = v.Update() + return v.Render() } -func (c *FileTree) notifyOnViewOptionChangeListeners() error { - for _, listener := range c.listeners { +func (v *FileTree) notifyOnViewOptionChangeListeners() error { + for _, listener := range v.listeners { err := listener() if err != nil { logrus.Errorf("notifyOnViewOptionChangeListeners error: %+v", err) @@ -297,95 +306,97 @@ func (c *FileTree) notifyOnViewOptionChangeListeners() error { } // ToggleAttributes will show/hide file attributes -func (c *FileTree) toggleAttributes() error { - err := c.vm.ToggleAttributes() +func (v *FileTree) toggleAttributes() error { + err := v.vm.ToggleAttributes() if err != nil { return err } - err = c.Update() + err = v.Update() if err != nil { return err } - err = c.Render() + err = v.Render() if err != nil { return err } // we need to render the changes to the status pane as well (not just this contoller/view) - return c.notifyOnViewOptionChangeListeners() + return v.notifyOnViewOptionChangeListeners() } // ToggleShowDiffType will show/hide the selected DiffType in the filetree pane. -func (c *FileTree) toggleShowDiffType(diffType filetree.DiffType) error { - c.vm.ToggleShowDiffType(diffType) +func (v *FileTree) toggleShowDiffType(diffType filetree.DiffType) error { + v.vm.ToggleShowDiffType(diffType) - err := c.Update() + err := v.Update() if err != nil { return err } - err = c.Render() + err = v.Render() if err != nil { return err } // we need to render the changes to the status pane as well (not just this contoller/view) - return c.notifyOnViewOptionChangeListeners() + return v.notifyOnViewOptionChangeListeners() } // OnLayoutChange is called by the UI framework to inform the view-model of the new screen dimensions -func (c *FileTree) OnLayoutChange(resized bool) error { - err := c.Update() +func (v *FileTree) OnLayoutChange(resized bool) error { + err := v.Update() if err != nil { return err } if resized { - return c.Render() + return v.Render() } return nil } // Update refreshes the state objects for future rendering. -func (c *FileTree) Update() error { +func (v *FileTree) Update() error { var width, height int - if c.view != nil { - width, height = c.view.Size() + if v.view != nil { + width, height = v.view.Size() } else { // before the TUI is setup there may not be a controller to reference. Use the entire screen as reference. - width, height = c.gui.Size() + width, height = v.gui.Size() } // height should account for the header - return c.vm.Update(c.filterRegex, width, height-1) + return v.vm.Update(v.filterRegex, width, height-1) } // Render flushes the state objects (file tree) to the pane. -func (c *FileTree) Render() error { - title := c.title +func (v *FileTree) Render() error { + logrus.Debugf("view.Render() %s", v.Name()) + + title := v.title // indicate when selected - if c.gui.CurrentView() == c.view { - title = "● " + c.title + if v.gui.CurrentView() == v.view { + title = "● " + v.title } - c.gui.Update(func(g *gocui.Gui) error { + v.gui.Update(func(g *gocui.Gui) error { // update the header - c.header.Clear() + v.header.Clear() width, _ := g.Size() headerStr := fmt.Sprintf("[%s]%s\n", title, strings.Repeat("─", width*2)) - if c.vm.ShowAttributes { + if v.vm.ShowAttributes { headerStr += fmt.Sprintf(filetree.AttributeFormat+" %s", "P", "ermission", "UID:GID", "Size", "Filetree") } - _, _ = fmt.Fprintln(c.header, format.Header(vtclean.Clean(headerStr, false))) + _, _ = fmt.Fprintln(v.header, format.Header(vtclean.Clean(headerStr, false))) // update the contents - c.view.Clear() - err := c.vm.Render() + v.view.Clear() + err := v.vm.Render() if err != nil { return err } - _, err = fmt.Fprint(c.view, c.vm.Buffer.String()) + _, err = fmt.Fprint(v.view, v.vm.Buffer.String()) return err }) @@ -393,10 +404,43 @@ func (c *FileTree) Render() error { } // KeyHelp indicates all the possible actions a user can take while the current pane is selected. -func (c *FileTree) KeyHelp() string { +func (v *FileTree) KeyHelp() string { var help string - for _, binding := range c.helpKeys { + for _, binding := range v.helpKeys { help += binding.RenderKeyHelp() } return help } + +func (v *FileTree) Layout(g *gocui.Gui, minX, minY, maxX, maxY int, hasResized bool) error { + logrus.Debugf("view.Layout(minX: %d, minY: %d, maxX: %d, maxY: %d) %s", minX, minY, maxX, maxY, v.Name()) + attributeRowSize := 0 + if !v.areAttributesVisible() { + attributeRowSize = 1 + } + // header + attribute + border + headerSize := 1 + attributeRowSize + 1 + // note: maxY needs to account for the (invisible) border, thus a +1 + header, headerErr := g.SetView(v.Name()+"header", minX, minY, maxX, minY+headerSize+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 + view, viewErr := g.SetView(v.Name(), minX, minY+headerSize, maxX, maxY+1) + if utils.IsNewView(viewErr, headerErr) { + err := v.Setup(view, header) + if err != nil { + logrus.Error("unable to setup tree controller", err) + return err + } + } + err := v.OnLayoutChange(hasResized) + if err != nil { + logrus.Error("unable to setup layer controller onLayoutChange", err) + return err + } + return nil +} + +func (v *FileTree) RequestedSize(available int) *int { + var requestedWidth = int(float64(available) * (1.0 - v.requestedWidthRatio)) + return &requestedWidth +} diff --git a/runtime/ui/view/filter.go b/runtime/ui/view/filter.go index e32957a..0eeafcd 100644 --- a/runtime/ui/view/filter.go +++ b/runtime/ui/view/filter.go @@ -5,6 +5,7 @@ import ( "github.com/jroimartin/gocui" "github.com/sirupsen/logrus" "github.com/wagoodman/dive/runtime/ui/format" + "github.com/wagoodman/dive/utils" "strings" ) @@ -13,19 +14,20 @@ type FilterEditListener func(string) error // Filter holds the UI objects and data models for populating the bottom row. Specifically the pane that // allows the user to filter the file tree by path. type Filter struct { - name string - gui *gocui.Gui - view *gocui.View - header *gocui.View - headerStr string - maxLength int - hidden bool + name string + gui *gocui.Gui + view *gocui.View + header *gocui.View + labelStr string + maxLength int + hidden bool + requestedHeight int filterEditListeners []FilterEditListener } -// NewFilterView creates a new view object attached the the global [gocui] screen object. -func NewFilterView(name string, gui *gocui.Gui) (controller *Filter) { +// newFilterView creates a new view object attached the the global [gocui] screen object. +func newFilterView(name string, gui *gocui.Gui) (controller *Filter) { controller = new(Filter) controller.filterEditListeners = make([]FilterEditListener, 0) @@ -33,50 +35,53 @@ func NewFilterView(name string, gui *gocui.Gui) (controller *Filter) { // populate main fields controller.name = name controller.gui = gui - controller.headerStr = "Path Filter: " + controller.labelStr = "Path Filter: " controller.hidden = true + controller.requestedHeight = 1 + return controller } -func (c *Filter) AddFilterEditListener(listener ...FilterEditListener) { - c.filterEditListeners = append(c.filterEditListeners, listener...) +func (v *Filter) AddFilterEditListener(listener ...FilterEditListener) { + v.filterEditListeners = append(v.filterEditListeners, listener...) } -func (c *Filter) Name() string { - return c.name +func (v *Filter) Name() string { + return v.name } // Setup initializes the UI concerns within the context of a global [gocui] view object. -func (c *Filter) Setup(v *gocui.View, header *gocui.View) error { +func (v *Filter) Setup(view *gocui.View, header *gocui.View) error { + logrus.Debugf("view.Setup() %s", v.Name()) // set controller options - c.view = v - c.maxLength = 200 - c.view.Frame = false - c.view.BgColor = gocui.AttrReverse - c.view.Editable = true - c.view.Editor = c + v.view = view + v.maxLength = 200 + v.view.Frame = false + v.view.BgColor = gocui.AttrReverse + v.view.Editable = true + v.view.Editor = v - c.header = header - c.header.BgColor = gocui.AttrReverse - c.header.Editable = false - c.header.Wrap = false - c.header.Frame = false + v.header = header + v.header.BgColor = gocui.AttrReverse + v.header.Editable = false + v.header.Wrap = false + v.header.Frame = false - return c.Render() + return v.Render() } // ToggleFilterView shows/hides the file tree filter pane. -func (c *Filter) ToggleVisible() error { +func (v *Filter) ToggleVisible() error { // delete all user input from the tree view - c.view.Clear() + v.view.Clear() // toggle hiding - c.hidden = !c.hidden + v.hidden = !v.hidden - if !c.hidden { - _, err := c.gui.SetCurrentView(c.name) + if !v.hidden { + _, err := v.gui.SetCurrentView(v.name) if err != nil { logrus.Error("unable to toggle filter view: ", err) return err @@ -87,57 +92,52 @@ func (c *Filter) ToggleVisible() error { // reset the cursor for the next time it is visible // Note: there is a subtle gocui behavior here where this cannot be called when the view // is newly visible. Is this a problem with dive or gocui? - return c.view.SetCursor(0, 0) -} - -// todo: remove the need for this -func (c *Filter) HeaderStr() string { - return c.headerStr + return v.view.SetCursor(0, 0) } // IsVisible indicates if the filter view pane is currently initialized -func (c *Filter) IsVisible() bool { - if c == nil { +func (v *Filter) IsVisible() bool { + if v == nil { return false } - return !c.hidden + return !v.hidden } // CursorDown moves the cursor down in the filter pane (currently indicates nothing). -func (c *Filter) CursorDown() error { +func (v *Filter) CursorDown() error { return nil } // CursorUp moves the cursor up in the filter pane (currently indicates nothing). -func (c *Filter) CursorUp() error { +func (v *Filter) CursorUp() error { return nil } // Edit intercepts the key press events in the filer view to update the file view in real time. -func (c *Filter) Edit(v *gocui.View, key gocui.Key, ch rune, mod gocui.Modifier) { - if !c.IsVisible() { +func (v *Filter) Edit(view *gocui.View, key gocui.Key, ch rune, mod gocui.Modifier) { + if !v.IsVisible() { return } - cx, _ := v.Cursor() - ox, _ := v.Origin() - limit := ox+cx+1 > c.maxLength + cx, _ := view.Cursor() + ox, _ := view.Origin() + limit := ox+cx+1 > v.maxLength switch { case ch != 0 && mod == 0 && !limit: - v.EditWrite(ch) + view.EditWrite(ch) case key == gocui.KeySpace && !limit: - v.EditWrite(' ') + view.EditWrite(' ') case key == gocui.KeyBackspace || key == gocui.KeyBackspace2: - v.EditDelete(true) + view.EditDelete(true) } // notify listeners - c.notifyFilterEditListeners() + v.notifyFilterEditListeners() } -func (c *Filter) notifyFilterEditListeners() { - currentValue := strings.TrimSpace(c.view.Buffer()) - for _, listener := range c.filterEditListeners { +func (v *Filter) notifyFilterEditListeners() { + currentValue := strings.TrimSpace(v.view.Buffer()) + for _, listener := range v.filterEditListeners { err := listener(currentValue) if err != nil { // note: cannot propagate error from here since this is from the main gogui thread @@ -147,14 +147,16 @@ func (c *Filter) notifyFilterEditListeners() { } // Update refreshes the state objects for future rendering (currently does nothing). -func (c *Filter) Update() error { +func (v *Filter) Update() error { return nil } // Render flushes the state objects to the screen. Currently this is the users path filter input. -func (c *Filter) Render() error { - c.gui.Update(func(g *gocui.Gui) error { - _, err := fmt.Fprintln(c.header, format.Header(c.headerStr)) +func (v *Filter) Render() error { + logrus.Debugf("view.Render() %s", v.Name()) + + v.gui.Update(func(g *gocui.Gui) error { + _, err := fmt.Fprintln(v.header, format.Header(v.labelStr)) if err != nil { logrus.Error("unable to write to buffer: ", err) } @@ -164,6 +166,26 @@ func (c *Filter) Render() error { } // KeyHelp indicates all the possible actions a user can take while the current pane is selected. -func (c *Filter) KeyHelp() string { +func (v *Filter) KeyHelp() string { return format.StatusControlNormal("▏Type to filter the file tree ") } + +func (v *Filter) Layout(g *gocui.Gui, minX, minY, maxX, maxY int, hasResized bool) error { + logrus.Debugf("view.Layout(minX: %d, minY: %d, maxX: %d, maxY: %d) %s", minX, minY, maxX, maxY, v.Name()) + + label, labelErr := g.SetView(v.Name()+"label", minX, minY, len(v.labelStr), maxY) + view, viewErr := g.SetView(v.Name(), minX+(len(v.labelStr)-1), minY, maxX, maxY) + + if utils.IsNewView(viewErr, labelErr) { + err := v.Setup(view, label) + if err != nil { + logrus.Error("unable to setup status controller", err) + return err + } + } + return nil +} + +func (v *Filter) RequestedSize(available int) *int { + return &v.requestedHeight +} diff --git a/runtime/ui/view/layer.go b/runtime/ui/view/layer.go index 5d3c678..687eb7b 100644 --- a/runtime/ui/view/layer.go +++ b/runtime/ui/view/layer.go @@ -33,8 +33,8 @@ type Layer struct { helpKeys []*key.Binding } -// NewLayerView creates a new view object attached the the global [gocui] screen object. -func NewLayerView(name string, gui *gocui.Gui, layers []*image.Layer) (controller *Layer, err error) { +// newLayerView creates a new view object attached the the global [gocui] screen object. +func newLayerView(name string, gui *gocui.Gui, layers []*image.Layer) (controller *Layer, err error) { controller = new(Layer) controller.listeners = make([]LayerChangeListener, 0) @@ -56,20 +56,20 @@ func NewLayerView(name string, gui *gocui.Gui, layers []*image.Layer) (controlle return controller, err } -func (c *Layer) AddLayerChangeListener(listener ...LayerChangeListener) { - c.listeners = append(c.listeners, listener...) +func (v *Layer) AddLayerChangeListener(listener ...LayerChangeListener) { + v.listeners = append(v.listeners, listener...) } -func (c *Layer) notifyLayerChangeListeners() error { - bottomTreeStart, bottomTreeStop, topTreeStart, topTreeStop := c.getCompareIndexes() +func (v *Layer) notifyLayerChangeListeners() error { + bottomTreeStart, bottomTreeStop, topTreeStart, topTreeStop := v.getCompareIndexes() selection := viewmodel.LayerSelection{ - Layer: c.CurrentLayer(), + Layer: v.CurrentLayer(), BottomTreeStart: bottomTreeStart, BottomTreeStop: bottomTreeStop, TopTreeStart: topTreeStart, TopTreeStop: topTreeStop, } - for _, listener := range c.listeners { + for _, listener := range v.listeners { err := listener(selection) if err != nil { logrus.Errorf("notifyLayerChangeListeners error: %+v", err) @@ -79,189 +79,190 @@ func (c *Layer) notifyLayerChangeListeners() error { return nil } -func (c *Layer) Name() string { - return c.name +func (v *Layer) Name() string { + return v.name } // Setup initializes the UI concerns within the context of a global [gocui] view object. -func (c *Layer) Setup(v *gocui.View, header *gocui.View) error { +func (v *Layer) Setup(view *gocui.View, header *gocui.View) error { + logrus.Debugf("view.Setup() %s", v.Name()) // set controller options - c.view = v - c.view.Editable = false - c.view.Wrap = false - c.view.Frame = false + v.view = view + v.view.Editable = false + v.view.Wrap = false + v.view.Frame = false - c.header = header - c.header.Editable = false - c.header.Wrap = false - c.header.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 c.setCompareMode(CompareLayer) }, - IsSelected: func() bool { return c.CompareMode == CompareLayer }, + 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 c.setCompareMode(CompareAll) }, - IsSelected: func() bool { return c.CompareMode == CompareAll }, + 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: c.CursorDown, + OnAction: v.CursorDown, }, { Key: gocui.KeyArrowUp, Modifier: gocui.ModNone, - OnAction: c.CursorUp, + OnAction: v.CursorUp, }, { Key: gocui.KeyArrowLeft, Modifier: gocui.ModNone, - OnAction: c.CursorUp, + OnAction: v.CursorUp, }, { Key: gocui.KeyArrowRight, Modifier: gocui.ModNone, - OnAction: c.CursorDown, + OnAction: v.CursorDown, }, { ConfigKeys: []string{"keybinding.page-up"}, - OnAction: c.PageUp, + OnAction: v.PageUp, }, { ConfigKeys: []string{"keybinding.page-down"}, - OnAction: c.PageDown, + OnAction: v.PageDown, }, } - helpKeys, err := key.GenerateBindings(c.gui, c.name, infos) + helpKeys, err := key.GenerateBindings(v.gui, v.name, infos) if err != nil { return err } - c.helpKeys = helpKeys + v.helpKeys = helpKeys - return c.Render() + return v.Render() } // height obtains the height of the current pane (taking into account the lost space due to the header). -func (c *Layer) height() uint { - _, height := c.view.Size() +func (v *Layer) height() uint { + _, height := v.view.Size() return uint(height - 1) } // IsVisible indicates if the layer view pane is currently initialized. -func (c *Layer) IsVisible() bool { - return c != nil +func (v *Layer) IsVisible() bool { + return v != nil } // PageDown moves to next page putting the cursor on top -func (c *Layer) PageDown() error { - step := int(c.height()) + 1 - targetLayerIndex := c.LayerIndex + step +func (v *Layer) PageDown() error { + step := int(v.height()) + 1 + targetLayerIndex := v.LayerIndex + step - if targetLayerIndex > len(c.Layers) { - step -= targetLayerIndex - (len(c.Layers) - 1) + if targetLayerIndex > len(v.Layers) { + step -= targetLayerIndex - (len(v.Layers) - 1) } if step > 0 { - err := CursorStep(c.gui, c.view, step) + err := CursorStep(v.gui, v.view, step) if err == nil { - return c.SetCursor(c.LayerIndex + step) + return v.SetCursor(v.LayerIndex + step) } } return nil } // PageUp moves to previous page putting the cursor on top -func (c *Layer) PageUp() error { - step := int(c.height()) + 1 - targetLayerIndex := c.LayerIndex - step +func (v *Layer) PageUp() error { + step := int(v.height()) + 1 + targetLayerIndex := v.LayerIndex - step if targetLayerIndex < 0 { step += targetLayerIndex } if step > 0 { - err := CursorStep(c.gui, c.view, -step) + err := CursorStep(v.gui, v.view, -step) if err == nil { - return c.SetCursor(c.LayerIndex - step) + return v.SetCursor(v.LayerIndex - step) } } return nil } // CursorDown moves the cursor down in the layer pane (selecting a higher layer). -func (c *Layer) CursorDown() error { - if c.LayerIndex < len(c.Layers) { - err := CursorDown(c.gui, c.view) +func (v *Layer) CursorDown() error { + if v.LayerIndex < len(v.Layers) { + err := CursorDown(v.gui, v.view) if err == nil { - return c.SetCursor(c.LayerIndex + 1) + return v.SetCursor(v.LayerIndex + 1) } } return nil } // CursorUp moves the cursor up in the layer pane (selecting a lower layer). -func (c *Layer) CursorUp() error { - if c.LayerIndex > 0 { - err := CursorUp(c.gui, c.view) +func (v *Layer) CursorUp() error { + if v.LayerIndex > 0 { + err := CursorUp(v.gui, v.view) if err == nil { - return c.SetCursor(c.LayerIndex - 1) + 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 (c *Layer) SetCursor(layer int) error { - c.LayerIndex = layer - err := c.notifyLayerChangeListeners() +func (v *Layer) SetCursor(layer int) error { + v.LayerIndex = layer + err := v.notifyLayerChangeListeners() if err != nil { return err } - return c.Render() + return v.Render() } // CurrentLayer returns the Layer object currently selected. -func (c *Layer) CurrentLayer() *image.Layer { - return c.Layers[c.LayerIndex] +func (v *Layer) CurrentLayer() *image.Layer { + return v.Layers[v.LayerIndex] } // setCompareMode switches the layer comparison between a single-layer comparison to an aggregated comparison. -func (c *Layer) setCompareMode(compareMode CompareType) error { - c.CompareMode = compareMode - return c.notifyLayerChangeListeners() +func (v *Layer) 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 (c *Layer) getCompareIndexes() (bottomTreeStart, bottomTreeStop, topTreeStart, topTreeStop int) { - bottomTreeStart = c.CompareStartIndex - topTreeStop = c.LayerIndex +func (v *Layer) getCompareIndexes() (bottomTreeStart, bottomTreeStop, topTreeStart, topTreeStop int) { + bottomTreeStart = v.CompareStartIndex + topTreeStop = v.LayerIndex - if c.LayerIndex == c.CompareStartIndex { - bottomTreeStop = c.LayerIndex - topTreeStart = c.LayerIndex - } else if c.CompareMode == CompareLayer { - bottomTreeStop = c.LayerIndex - 1 - topTreeStart = c.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 = c.CompareStartIndex - topTreeStart = c.CompareStartIndex + 1 + bottomTreeStop = v.CompareStartIndex + topTreeStart = v.CompareStartIndex + 1 } return bottomTreeStart, bottomTreeStop, topTreeStart, topTreeStop } // renderCompareBar returns the formatted string for the given layer. -func (c *Layer) renderCompareBar(layerIdx int) string { - bottomTreeStart, bottomTreeStop, topTreeStart, topTreeStop := c.getCompareIndexes() +func (v *Layer) renderCompareBar(layerIdx int) string { + bottomTreeStart, bottomTreeStop, topTreeStart, topTreeStop := v.getCompareIndexes() result := " " if layerIdx >= bottomTreeStart && layerIdx <= bottomTreeStop { @@ -275,43 +276,44 @@ func (c *Layer) renderCompareBar(layerIdx int) string { } // Update refreshes the state objects for future rendering (currently does nothing). -func (c *Layer) Update() error { +func (v *Layer) 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 (c *Layer) Render() error { +func (v *Layer) Render() error { + logrus.Debugf("view.Render() %s", v.Name()) // indicate when selected title := "Layers" - if c.gui.CurrentView() == c.view { + if v.gui.CurrentView() == v.view { title = "● " + title } - c.gui.Update(func(g *gocui.Gui) error { + v.gui.Update(func(g *gocui.Gui) error { // update header - c.header.Clear() + v.header.Clear() width, _ := g.Size() headerStr := fmt.Sprintf("[%s]%s\n", title, strings.Repeat("─", width*2)) headerStr += fmt.Sprintf("Cmp"+image.LayerFormat, "Size", "Command") - _, err := fmt.Fprintln(c.header, format.Header(vtclean.Clean(headerStr, false))) + _, err := fmt.Fprintln(v.header, format.Header(vtclean.Clean(headerStr, false))) if err != nil { return err } // update contents - c.view.Clear() - for idx, layer := range c.Layers { + v.view.Clear() + for idx, layer := range v.Layers { layerStr := layer.String() - compareBar := c.renderCompareBar(idx) + compareBar := v.renderCompareBar(idx) - if idx == c.LayerIndex { - _, err = fmt.Fprintln(c.view, compareBar+" "+format.Selected(layerStr)) + if idx == v.LayerIndex { + _, err = fmt.Fprintln(v.view, compareBar+" "+format.Selected(layerStr)) } else { - _, err = fmt.Fprintln(c.view, compareBar+" "+layerStr) + _, err = fmt.Fprintln(v.view, compareBar+" "+layerStr) } if err != nil { @@ -326,9 +328,9 @@ func (c *Layer) Render() error { } // KeyHelp indicates all the possible actions a user can take while the current pane is selected. -func (c *Layer) KeyHelp() string { +func (v *Layer) KeyHelp() string { var help string - for _, binding := range c.helpKeys { + for _, binding := range v.helpKeys { help += binding.RenderKeyHelp() } return help diff --git a/runtime/ui/view/renderer.go b/runtime/ui/view/renderer.go index c3fadf5..d6c908f 100644 --- a/runtime/ui/view/renderer.go +++ b/runtime/ui/view/renderer.go @@ -5,5 +5,8 @@ type Renderer interface { Update() error Render() error IsVisible() bool +} + +type Helper interface { KeyHelp() string } diff --git a/runtime/ui/view/status.go b/runtime/ui/view/status.go index bdd0363..0e05da7 100644 --- a/runtime/ui/view/status.go +++ b/runtime/ui/view/status.go @@ -5,6 +5,7 @@ import ( "github.com/sirupsen/logrus" "github.com/wagoodman/dive/runtime/ui/format" "github.com/wagoodman/dive/runtime/ui/key" + "github.com/wagoodman/dive/utils" "strings" "github.com/jroimartin/gocui" @@ -17,76 +18,81 @@ type Status struct { gui *gocui.Gui view *gocui.View - selectedView Renderer + selectedView Helper + requestedHeight int helpKeys []*key.Binding } -// NewStatusView creates a new view object attached the the global [gocui] screen object. -func NewStatusView(name string, gui *gocui.Gui) (controller *Status) { +// newStatusView creates a new view object attached the the global [gocui] screen object. +func newStatusView(name string, gui *gocui.Gui) (controller *Status) { controller = new(Status) // populate main fields controller.name = name controller.gui = gui controller.helpKeys = make([]*key.Binding, 0) + controller.requestedHeight = 1 return controller } -func (c *Status) SetCurrentView(r Renderer) { - c.selectedView = r +func (v *Status) SetCurrentView(r Helper) { + v.selectedView = r } -func (c *Status) Name() string { - return c.name +func (v *Status) Name() string { + return v.name } -func (c *Status) AddHelpKeys(keys ...*key.Binding) { - c.helpKeys = append(c.helpKeys, keys...) +func (v *Status) AddHelpKeys(keys ...*key.Binding) { + v.helpKeys = append(v.helpKeys, keys...) } // Setup initializes the UI concerns within the context of a global [gocui] view object. -func (c *Status) Setup(v *gocui.View, header *gocui.View) error { +func (v *Status) Setup(view *gocui.View) error { + logrus.Debugf("view.Setup() %s", v.Name()) // set controller options - c.view = v - c.view.Frame = false + v.view = view + v.view.Frame = false - return c.Render() + return v.Render() } // IsVisible indicates if the status view pane is currently initialized. -func (c *Status) IsVisible() bool { - return c != nil +func (v *Status) IsVisible() bool { + return v != nil } // CursorDown moves the cursor down in the details pane (currently indicates nothing). -func (c *Status) CursorDown() error { +func (v *Status) CursorDown() error { return nil } // CursorUp moves the cursor up in the details pane (currently indicates nothing). -func (c *Status) CursorUp() error { +func (v *Status) CursorUp() error { return nil } // Update refreshes the state objects for future rendering (currently does nothing). -func (c *Status) Update() error { +func (v *Status) Update() error { return nil } // Render flushes the state objects to the screen. -func (c *Status) Render() error { - c.gui.Update(func(g *gocui.Gui) error { - c.view.Clear() +func (v *Status) Render() error { + logrus.Debugf("view.Render() %s", v.Name()) + + v.gui.Update(func(g *gocui.Gui) error { + v.view.Clear() var selectedHelp string - if c.selectedView != nil { - selectedHelp = c.selectedView.KeyHelp() + if v.selectedView != nil { + selectedHelp = v.selectedView.KeyHelp() } - _, err := fmt.Fprintln(c.view, c.KeyHelp()+selectedHelp+format.StatusNormal("▏"+strings.Repeat(" ", 1000))) + _, err := fmt.Fprintln(v.view, v.KeyHelp()+selectedHelp+format.StatusNormal("▏"+strings.Repeat(" ", 1000))) if err != nil { logrus.Debug("unable to write to buffer: ", err) } @@ -97,10 +103,28 @@ func (c *Status) Render() error { } // KeyHelp indicates all the possible global actions a user can take when any pane is selected. -func (c *Status) KeyHelp() string { +func (v *Status) KeyHelp() string { var help string - for _, binding := range c.helpKeys { + for _, binding := range v.helpKeys { help += binding.RenderKeyHelp() } return help } + +func (v *Status) Layout(g *gocui.Gui, minX, minY, maxX, maxY int, hasResized bool) error { + logrus.Debugf("view.Layout(minX: %d, minY: %d, maxX: %d, maxY: %d) %s", minX, minY, maxX, maxY, v.Name()) + + view, viewErr := g.SetView(v.Name(), minX, minY, maxX, maxY) + if utils.IsNewView(viewErr) { + err := v.Setup(view) + if err != nil { + logrus.Error("unable to setup status controller", err) + return err + } + } + return nil +} + +func (v *Status) RequestedSize(available int) *int { + return &v.requestedHeight +} diff --git a/runtime/ui/view/views.go b/runtime/ui/view/views.go new file mode 100644 index 0000000..55ec6f6 --- /dev/null +++ b/runtime/ui/view/views.go @@ -0,0 +1,56 @@ +package view + +import ( + "github.com/jroimartin/gocui" + "github.com/wagoodman/dive/dive/filetree" + "github.com/wagoodman/dive/dive/image" +) + +type Views struct { + Tree *FileTree + Layer *Layer + Status *Status + Filter *Filter + Details *Details + all []*Renderer +} + +func NewViews(g *gocui.Gui, analysis *image.AnalysisResult, cache filetree.Comparer) (*Views, error) { + Layer, err := newLayerView("layers", g, analysis.Layers) + if err != nil { + return nil, err + } + + treeStack := analysis.RefTrees[0] + Tree, err := newFileTreeView("filetree", g, treeStack, analysis.RefTrees, cache) + if err != nil { + return nil, err + } + + Status := newStatusView("status", g) + + // set the layer view as the first selected view + Status.SetCurrentView(Layer) + + Filter := newFilterView("filter", g) + + Details := newDetailsView("details", g, analysis.Efficiency, analysis.Inefficiencies, analysis.SizeBytes) + + return &Views{ + Tree: Tree, + Layer: Layer, + Status: Status, + Filter: Filter, + Details: Details, + }, nil +} + +func (views *Views) All() []Renderer { + return []Renderer{ + views.Tree, + views.Layer, + views.Status, + views.Filter, + views.Details, + } +} diff --git a/utils/view.go b/utils/view.go new file mode 100644 index 0000000..270da4b --- /dev/null +++ b/utils/view.go @@ -0,0 +1,20 @@ +package utils + +import ( + "github.com/jroimartin/gocui" + "github.com/sirupsen/logrus" +) + +// isNewView determines if a view has already been created based on the set of errors given (a bit hokie) +func IsNewView(errs ...error) bool { + for _, err := range errs { + if err == nil { + return false + } + if err != gocui.ErrUnknownView { + logrus.Errorf("IsNewView() unexpected error: %+v", err) + return true + } + } + return true +}