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 +} +