summaryrefslogtreecommitdiffstats
diff options
context:
space:
mode:
authordwillist <dthornton@vmware.com>2020-10-28 20:15:37 -0400
committerdwillist <dthornton@vmware.com>2020-10-28 20:16:18 -0400
commite2dbdcd3e7fcffc086c19c9c94b35b560a481587 (patch)
tree1baec6c70a80b11598e9d0ae51f766dad1494aca
parentda7020ad87122c41d1aa3f4bbdf2a81685c14ada (diff)
add filterview & update filetree drawing
Signed-off-by: dwillist <dthornton@vmware.com>
-rw-r--r--dive/filetree/file_tree.go51
-rw-r--r--dive/filetree/file_tree_test.go23
-rw-r--r--runtime/ui/app.go140
-rw-r--r--runtime/ui/components/filetree_primative.go166
-rw-r--r--runtime/ui/components/filter_primative.go28
-rw-r--r--runtime/ui/components/image_details_view.go9
-rw-r--r--runtime/ui/components/layers_primative.go22
7 files changed, 368 insertions, 71 deletions
diff --git a/dive/filetree/file_tree.go b/dive/filetree/file_tree.go
index 4333e25..f667ffd 100644
--- a/dive/filetree/file_tree.go
+++ b/dive/filetree/file_tree.go
@@ -52,9 +52,54 @@ type renderParams struct {
isLast bool
}
-// renderStringTreeBetween returns a string representing the given tree between the given rows. Since each node
-// is rendered on its own line, the returned string shows the visible nodes not affected by a collapsed parent.
func (tree *FileTree) renderStringTreeBetween(startRow, stopRow int, showAttributes bool) string {
+ renderTree := NewFileTree()
+ nodeCount := 0
+ visitFunc := func(curNode *FileNode) error {
+ nodeCount++
+ node, _, err := renderTree.AddPath(curNode.Path(), FileInfo{})
+ if err != nil {
+ return err
+ }
+
+ node.Data = *curNode.Data.Copy()
+
+
+ if len(curNode.Children) == 0 && curNode.Data.ViewInfo.Collapsed {
+ node.Data.ViewInfo.Collapsed = false
+ }
+ return nil
+ }
+
+ evaluatorFunc := func(curNode *FileNode) bool {
+ switch {
+ case curNode == nil:
+ return false
+ case nodeCount > stopRow:
+ return false
+ case curNode.Data.ViewInfo.Hidden:
+ return false
+ case curNode.Parent == nil: // should only be true for the root node
+ return true
+ case curNode.Parent.Data.ViewInfo.Collapsed || curNode.Parent.Data.ViewInfo.Hidden:
+ return false
+ default:
+ return true
+ }
+ }
+ err := tree.VisitDepthChildFirst(visitFunc, evaluatorFunc)
+ if err != nil {
+ return ""
+ }
+
+ return renderTree.constructStringBetween(startRow, stopRow, showAttributes)
+}
+
+
+
+ // renderStringTreeBetween returns a string representing the given tree between the given rows. Since each node
+// is rendered on its own line, the returned string shows the visible nodes not affected by a collapsed parent.
+func (tree *FileTree) constructStringBetween(startRow, stopRow int, showAttributes bool) string {
// generate a list of nodes to render
var params = make([]renderParams, 0)
var result string
@@ -84,7 +129,7 @@ func (tree *FileTree) renderStringTreeBetween(startRow, stopRow int, showAttribu
// visit this node...
isLast := idx == (len(currentParams.node.Children) - 1)
- showCollapsed := child.Data.ViewInfo.Collapsed && len(child.Children) > 0
+ showCollapsed := child.Data.ViewInfo.Collapsed
// completely copy the reference slice
childSpaces := make([]bool, len(currentParams.childSpaces))
diff --git a/dive/filetree/file_tree_test.go b/dive/filetree/file_tree_test.go
index c1bc37c..4363384 100644
--- a/dive/filetree/file_tree_test.go
+++ b/dive/filetree/file_tree_test.go
@@ -57,7 +57,24 @@ func TestStringCollapsed(t *testing.T) {
if expected != actual {
t.Errorf("Expected tree string:\n--->%s<---\nGot:\n--->%s<---", expected, actual)
}
+}
+
+func TestStringHidden(t *testing.T) {
+ tree := NewFileTree()
+ tree.Root.AddChild("1 node!", FileInfo{})
+ tree.Root.AddChild("2 node!", FileInfo{})
+ three := tree.Root.AddChild("3 node!", FileInfo{})
+ three.Data.ViewInfo.Hidden = true
+
+ expected :=
+ `├── 1 node!
+└── 2 node!
+`
+ actual := tree.String(false)
+ if expected != actual {
+ t.Errorf("Expected tree string:\n--->%s<---\nGot:\n--->%s<---", expected, actual)
+ }
}
func TestString(t *testing.T) {
@@ -113,9 +130,9 @@ func TestStringBetween(t *testing.T) {
}
expected :=
- `│ └── public
-├── tmp
-│ └── nonsense
+`│ └── public
+└── tmp
+ └── nonsense
`
actual := tree.StringBetween(3, 5, false)
diff --git a/runtime/ui/app.go b/runtime/ui/app.go
index 631c85d..e8baed8 100644
--- a/runtime/ui/app.go
+++ b/runtime/ui/app.go
@@ -1,11 +1,16 @@
package ui
import (
+ "fmt"
"github.com/gdamore/tcell/v2"
"github.com/rivo/tview"
+ "github.com/sirupsen/logrus"
"github.com/wagoodman/dive/dive/filetree"
"github.com/wagoodman/dive/dive/image"
"github.com/wagoodman/dive/runtime/ui/components"
+ "os"
+ "path/filepath"
+ "regexp"
"sync"
)
@@ -21,15 +26,36 @@ type diveApp struct {
app *tview.Application
layers *components.LayerList
fileTree *components.TreeView
- finderFocus tview.Primitive
}
+//type Cache interface {
+// GetTree(key filetree.TreeIndexKey) (*filetree.FileTree, error)
+//}
+
+
+//func updateFileTree()
+//
+//func NewLayerListHandler(cache filetree.Comparer, analysis image.AnalysisResult,layerDetails tview.TextView) components.LayerListHandler {
+// return func(i int, stringer fmt.Stringer, r rune) {
+// bottomStart := intMax(0,i-1) // no values less than zero
+// bottomStop := intMax(0, i-1)
+// curTreeIndex := filetree.NewTreeIndexKey(bottomStart,bottomStop,i,i)
+// curTree, err := cache.GetTree(curTreeIndex)
+// layerDetails.SetText(components.LayerDetailsText(analysis.Layers[i]))
+// if err != nil {
+// panic(err)
+// }
+//
+// fileTreeView.SetTree(curTree)
+// }
+//}
+
func newApp(app *tview.Application, analysis *image.AnalysisResult, cache filetree.Comparer) (*diveApp, error) {
var err error
once.Do(func() {
- layersView := components.NewLayerList([]string{})
+ layersView := components.NewLayerList(nil)
layersView.SetSubtitle("Cmp Size Command").SetBorder(true).SetTitle("Layers")
curTreeIndex := filetree.NewTreeIndexKey(0,0,0,0)
@@ -47,81 +73,107 @@ func newApp(app *tview.Application, analysis *image.AnalysisResult, cache filetr
layerDetails.SetText(components.LayerDetailsText(analysis.Layers[0]))
for _, layer := range analysis.Layers {
- layersView.AddItem(layer.String()).SetChangedFunc(func(i int, s string, r rune) {
- bottomStart := intMax(0,i-1) // no values less than zero
- bottomStop := intMax(0, i-1)
- curTreeIndex := filetree.NewTreeIndexKey(bottomStart,bottomStop,i,i)
- curTree, err = cache.GetTree(curTreeIndex)
- layerDetailText := components.LayerDetailsText(analysis.Layers[i])
- layerDetails.SetText(layerDetailText)
- if err != nil {
- panic(err)
- }
-
- fileTreeView.SetTree(curTree)
- })
+ layersView.AddItem(layer)
}
+ layersView.SetChangedFunc(func(i int, stringer fmt.Stringer, r rune) {
+ bottomStart := intMax(0,i-1) // no values less than zero
+ bottomStop := intMax(0, i-1)
+ curTreeIndex := filetree.NewTreeIndexKey(bottomStart,bottomStop,i,i)
+ curTree, err = cache.GetTree(curTreeIndex)
+ layerDetailText := components.LayerDetailsText(analysis.Layers[i])
+ layerDetails.SetText(layerDetailText)
+ if err != nil {
+ panic(err)
+ }
- imageDetails := components.NewImageDetailsView(analysis)
+ fileTreeView.SetTree(curTree)
+ })
- grid := tview.NewGrid().SetRows(-4,-1,-1)
- grid.SetBorder(false)
- grid.AddItem(layersView, 0,0,1,1,5, 10, false).
- AddItem(layerDetails,1,0,1,1,10,10, false).
- AddItem(imageDetails,2,0,1, 1,10,10,false)
+ imageDetails := components.NewImageDetailsView(analysis)
+ grid := tview.NewGrid()
+ filterView := components.NewFilterView()
+ filterView.SetChangedFunc(
+ func(textToCheck string) {
+ var filterRegex *regexp.Regexp = nil
+ var err error
+
+ if len(textToCheck) > 0 {
+ filterRegex, err = regexp.Compile(textToCheck)
+ if err != nil {
+ return
+ }
+ }
+ fileTreeView.SetFilterRegex(filterRegex)
+ return
+ }).SetDoneFunc(func(key tcell.Key) {
+ switch {
+ case key == tcell.KeyEnter:
+ app.SetFocus(grid)
+ }
+ })
+ grid.SetRows(-4,-1,-1,1).SetColumns(-1,-1, 3)
+ grid.SetBorder(false)
+ grid.AddItem(layersView, 0,0,1,1,5, 10, true).
+ AddItem(layerDetails,1,0,1,1,10,40, false).
+ AddItem(imageDetails,2,0,1, 1,10,10,false).
+ AddItem(fileTreeView, 0, 1, 3, 1, 0,0, true).
+ AddItem(filterView, 3,0,1,2,0,0,false)
- flex := tview.NewFlex().
- AddItem(grid, 0, 1, true).
- AddItem(fileTreeView, 0, 1, false)
switchFocus := func(event *tcell.EventKey) *tcell.EventKey {
+ var result *tcell.EventKey = nil
switch event.Key() {
case tcell.KeyTAB:
+ //fmt.Println("Tab")
if appSingleton.layers.HasFocus() {
appSingleton.app.SetFocus(appSingleton.fileTree)
} else {
appSingleton.app.SetFocus(appSingleton.layers)
}
- return nil
+ case tcell.KeyCtrlF:
+ if filterView.HasFocus() {
+ filterView.Blur()
+ appSingleton.app.SetFocus(grid)
+ } else {
+ appSingleton.app.SetFocus(filterView)
+ }
+
default:
- return event
+ result = event
}
+ return result
}
- app.SetInputCapture(switchFocus)
+ grid.SetInputCapture(switchFocus)
- app.SetRoot(flex,true).SetFocus(layersView)
+ app.SetRoot(grid,true)
appSingleton = &diveApp{
app: app,
fileTree: fileTreeView,
layers: layersView,
}
+ app.SetFocus(layersView)
+ app.SetInputCapture(func(event *tcell.EventKey) *tcell.EventKey {
+ logrus.Debugf("application handling in put %s\n", event.Name())
+ return event
+ })
})
- once.Do(func() {
- curTreeIndex := filetree.NewTreeIndexKey(0,0,0,0)
- curTree, err := cache.GetTree(curTreeIndex)
- if err != nil {
- panic(err)
- }
- fileTreeView := components.NewTreeView(curTree)
- fileTreeView.SetTitle("Files").SetBorder(true)
- app.SetRoot(fileTreeView, true).SetFocus(fileTreeView)
- appSingleton = &diveApp{
- app: app,
- fileTree: fileTreeView,
- layers: nil,
- }
- })
return appSingleton, err
}
// Run is the UI entrypoint.
func Run(analysis *image.AnalysisResult, treeStack filetree.Comparer) error {
+ debugFile := filepath.Join("/tmp", "dive","debug.out")
+ LogOutputFile, _ := os.OpenFile(debugFile, os.O_RDWR | os.O_CREATE | os.O_TRUNC, 0666)
+ defer LogOutputFile.Close()
+ logrus.SetOutput(LogOutputFile)
+ logrus.SetFormatter(&logrus.TextFormatter{})
+ logrus.SetLevel(logrus.DebugLevel)
+ logrus.Debugln("debug start:")
app := tview.NewApplication()
-
_, err := newApp(app, analysis, treeStack)
if err != nil {
return err
diff --git a/runtime/ui/components/filetree_primative.go b/runtime/ui/components/filetree_primative.go
index 5a38efe..70f5e26 100644
--- a/runtime/ui/components/filetree_primative.go
+++ b/runtime/ui/components/filetree_primative.go
@@ -11,12 +11,18 @@ import (
"strings"
)
+// TODO simplify this interface.
+type TreeModel interface {
+ StringBetween(int, int, bool) string
+ VisitDepthParentFirst(filetree.Visitor, filetree.VisitEvaluator) error
+ VisitDepthChildFirst(filetree.Visitor, filetree.VisitEvaluator) error
+ RemovePath(path string) error
+ VisibleSize() int
+}
+
type TreeView struct {
*tview.Box
- // TODO: make me an interface
-
- tree *filetree.FileTree
-
+ tree TreeModel
// Note that the following two fields are distinct
// treeIndex is the index about where we are in the current fileTree
@@ -28,10 +34,12 @@ type TreeView struct {
bufferIndexLowerBound int
bufferIndex int
+ filterRegex *regexp.Regexp
//changed func(index int, mainText string, shortcut rune)
+
}
-func NewTreeView(tree *filetree.FileTree) *TreeView {
+func NewTreeView(tree TreeModel) *TreeView {
return &TreeView{
Box: tview.NewBox(),
tree: tree,
@@ -46,6 +54,10 @@ func (t *TreeView) InputHandler() func(event *tcell.EventKey, setFocus func(p tv
t.keyUp()
case tcell.KeyDown:
t.keyDown()
+ case tcell.KeyRight:
+ t.keyRight()
+ case tcell.KeyLeft:
+ t.keyLeft()
}
switch event.Rune() {
case ' ':
@@ -55,7 +67,7 @@ func (t *TreeView) InputHandler() func(event *tcell.EventKey, setFocus func(p tv
})
}
-func (t *TreeView) SetTree(newTree *filetree.FileTree) *TreeView {
+func (t *TreeView) SetTree(newTree TreeModel) *TreeView {
// preserve collapsed nodes based on path
collapsedList := map[string]interface{}{}
@@ -82,11 +94,14 @@ func (t *TreeView) SetTree(newTree *filetree.FileTree) *TreeView {
}, evaluateFunc)
t.tree = newTree
+ if err := t.FilterUpdate(); err != nil {
+ panic(err)
+ }
return t
}
-func (t *TreeView) GetTree(tree *filetree.FileTree) *filetree.FileTree {
+func (t *TreeView) GetTree() TreeModel {
return t.tree
}
@@ -98,15 +113,29 @@ func (t *TreeView) HasFocus() bool {
return t.Box.HasFocus()
}
+func (t *TreeView) SetFilterRegex(filterRegex *regexp.Regexp) {
+ t.filterRegex = filterRegex
+ if err := t.FilterUpdate(); err != nil {
+ panic(err)
+ }
+}
// Private helper methods
func (t *TreeView) spaceDown() bool {
node := t.getAbsPositionNode(nil)
if node != nil && node.Data.FileInfo.IsDir {
+ logrus.Debugf("collapsing node %s", node.Path())
node.Data.ViewInfo.Collapsed = !node.Data.ViewInfo.Collapsed
return true
}
+ if node != nil {
+ logrus.Debugf("unable to collapse node %s", node.Path())
+ logrus.Debugf(" IsDir: %t", node.Data.FileInfo.IsDir)
+
+ } else {
+ logrus.Debugf("unable to collapse nil node")
+ }
return false
}
@@ -142,7 +171,6 @@ func (t *TreeView) getAbsPositionNode(filterRegex *regexp.Regexp) (node *filetre
}
func (t *TreeView) keyDown() bool {
-
_, _, _, height := t.Box.GetInnerRect()
// treeIndex is the index about where we are in the current file
@@ -174,19 +202,137 @@ func (t *TreeView) keyUp() bool {
return true
}
+// TODO add regex filtering
+func (t *TreeView) keyRight() bool {
+ node := t.getAbsPositionNode(t.filterRegex)
+
+ _,_, _, height := t.Box.GetInnerRect()
+ if node == nil {
+ return false
+ }
+
+ if !node.Data.FileInfo.IsDir {
+ return false
+ }
+
+ if len(node.Children) == 0 {
+ return false
+ }
+
+ if node.Data.ViewInfo.Collapsed {
+ node.Data.ViewInfo.Collapsed = false
+ }
+
+ t.treeIndex++
+ if t.treeIndex > t.bufferIndexUpperBound() {
+ t.bufferIndexLowerBound++
+ }
+
+ t.bufferIndex++
+ if t.bufferIndex > height {
+ t.bufferIndex = height
+ }
+
+ return true
+}
+
+func (t *TreeView) keyLeft() bool {
+ var visitor func(*filetree.FileNode) error
+ var evaluator func(*filetree.FileNode) bool
+ var dfsCounter, newIndex int
+ oldIndex := t.treeIndex
+ currentNode := t.getAbsPositionNode(t.filterRegex)
+
+ if currentNode == nil {
+ return true
+ }
+ parentPath := currentNode.Parent.Path()
+
+ visitor = func(curNode *filetree.FileNode) error {
+ if strings.Compare(parentPath, curNode.Path()) == 0 {
+ newIndex = dfsCounter
+ }
+ dfsCounter++
+ return nil
+ }
+
+ evaluator = func(curNode *filetree.FileNode) bool {
+ regexMatch := true
+ if t.filterRegex != nil {
+ match := t.filterRegex.Find([]byte(curNode.Path()))
+ regexMatch = match != nil
+ }
+ return !curNode.Parent.Data.ViewInfo.Collapsed && !curNode.Data.ViewInfo.Hidden && regexMatch
+ }
+
+ err := t.tree.VisitDepthParentFirst(visitor, evaluator)
+ if err != nil {
+ // TODO: remove this panic
+ panic(err)
+ }
+
+ t.treeIndex = newIndex
+ moveIndex := oldIndex - newIndex
+ if newIndex < t.bufferIndexLowerBound {
+ t.bufferIndexLowerBound = t.treeIndex
+ }
+
+ if t.bufferIndex > moveIndex {
+ t.bufferIndex -= moveIndex
+ } else {
+ t.bufferIndex = 0
+ }
+
+ return true
+}
+
func (t *TreeView) bufferIndexUpperBound() int {
_,_, _, height := t.Box.GetInnerRect()
return t.bufferIndexLowerBound + height
+}
+
+func (t *TreeView) FilterUpdate() error {
+ // keep the t selection in parity with the current DiffType selection
+ err := t.tree.VisitDepthChildFirst(func(node *filetree.FileNode) error {
+ // TODO: add hidden datatypes.
+ //node.Data.ViewInfo.Hidden = t.HiddenDiffTypes[node.Data.DiffType]
+ visibleChild := false
+ if t.filterRegex == nil {
+ node.Data.ViewInfo.Hidden = false
+ return nil
+ }
+
+ for _, child := range node.Children {
+ if !child.Data.ViewInfo.Hidden {
+ visibleChild = true
+ node.Data.ViewInfo.Hidden = false
+ return nil
+ }
+ }
+
+ if !visibleChild { // hide nodes that do not match the current file filter regex (also don't unhide nodes that are already hidden)
+ match := t.filterRegex.FindString(node.Path())
+ node.Data.ViewInfo.Hidden = len(match) == 0
+ }
+ return nil
+ }, nil)
+ if err != nil {
+ logrus.Errorf("unable to propagate t model tree: %+v", err)
+ return err
+ }
+
+ return nil
}
+
func (t *TreeView) Draw(screen tcell.Screen) {
t.Box.Draw(screen)
x, y, width, height := t.Box.GetInnerRect()
-
+ showAttributes := width > 80
// TODO add switch for showing attributes.
- treeString := t.tree.StringBetween(t.bufferIndexLowerBound, t.bufferIndexUpperBound(), false)
+ treeString := t.tree.StringBetween(t.bufferIndexLowerBound, t.bufferIndexUpperBound(), showAttributes)
lines := strings.Split(treeString, "\n")
// update the contents
diff --git a/runtime/ui/components/filter_primative.go b/runtime/ui/components/filter_primative.go
new file mode 100644
index 0000000..cf6937f
--- /dev/null
+++ b/runtime/ui/components/filter_primative.go
@@ -0,0 +1,28 @@
+package components
+
+import (
+ "github.com/gdamore/tcell/v2"
+ "github.com/rivo/tview"
+)
+
+type FilterView struct {
+ *tview.InputField
+}
+
+func NewFilterView() *FilterView {
+ inputField := tview.NewInputField()
+ inputField.SetBackgroundColor(tcell.ColorGray)
+ inputField.SetFieldTextColor(tcell.ColorBlack)
+ inputField.SetFieldBackgroundColor(tcell.ColorGray)
+ //inputField.SetPlaceholderTextColor(tcell.ColorBlack)
+ inputField.SetLabelColor(tcell.ColorBlack)
+ inputField.SetLabel("Path Filter: ")
+ //inputField.SetPlaceholder("(regex)" )
+ return &FilterView{
+ InputField: inputField,
+ }
+}
+
+func (fv *FilterView) Empty() bool {
+ return fv.GetText() == ""
+} \ No newline at end of file
diff --git a/runtime/ui/components/image_details_view.go b/runtime/ui/components/image_details_view.go
index 1f8e1b1..cc39f27 100644
--- a/runtime/ui/components/image_details_view.go
+++ b/runtime/ui/components/image_details_view.go
@@ -8,9 +8,14 @@ import (
"strconv"
)
+
+type ImageDetails struct {
+ *tview.TextView
+}
+
func NewImageDetailsView(analysisResult *image.AnalysisResult) *tview.TextView {
- result := tview.NewTextView().
- SetDynamicColors(true).
+ result := tview.NewTextView()
+ result.SetDynamicColors(true).
SetScrollable(true)
result.SetBorder(true).
SetTitle("Image Details").
diff --git a/runtime/ui/components/layers_primative.go b/runtime/ui/components/layers_primative.go
index 4477f1c..a13085e 100644
--- a/runtime/ui/components/layers_primative.go
+++ b/runtime/ui/components/layers_primative.go
@@ -9,17 +9,21 @@ import (
type LayerList struct {
*tview.Box
subtitle string
- // TODO make me an interface
- layers []string
+ layers []fmt.Stringer
cmpIndex int
- changed func(index int, mainText string, shortcut rune)
+ changed LayerListHandler
selectedBackgroundColor tcell.Color
}
-func NewLayerList(options []string) *LayerList {
+type LayerListHandler func(index int, mainText fmt.Stringer, shortcut rune)
+
+func NewLayerList(layers []fmt.Stringer) *LayerList {
+ if layers == nil {
+ layers = []fmt.Stringer{}
+ }
return &LayerList{
Box: tview.NewBox(),
- layers: options,
+ layers: layers,
cmpIndex: 0,
}
}
@@ -106,7 +110,7 @@ func (ll *LayerList) InputHandler() func(event *tcell.EventKey, setFocus func(p
})
}
-func (ll *LayerList) InsertItem(index int, value string) *LayerList {
+func (ll *LayerList) InsertItem(index int, value fmt.Stringer) *LayerList {
if index < 0 {
ll.layers = append(ll.layers, value)
return ll
@@ -116,7 +120,7 @@ func (ll *LayerList) InsertItem(index int, value string) *LayerList {
return ll
}
-func (ll *LayerList) AddItem(mainText string) *LayerList {
+func (ll *LayerList) AddItem(mainText fmt.Stringer) *LayerList {
ll.InsertItem(-1, mainText)
return ll
}
@@ -129,7 +133,7 @@ func (ll *LayerList) HasFocus() bool {
return ll.Box.HasFocus()
}
-func (ll *LayerList) SetChangedFunc(handler func(index int, mainText string, shortcut rune)) *LayerList {
+func (ll *LayerList) SetChangedFunc(handler LayerListHandler) *LayerList {
ll.changed = handler
return ll
-} \ No newline at end of file
+}