diff options
author | dwillist <dthornton@vmware.com> | 2020-10-28 20:15:37 -0400 |
---|---|---|
committer | dwillist <dthornton@vmware.com> | 2020-10-28 20:16:18 -0400 |
commit | e2dbdcd3e7fcffc086c19c9c94b35b560a481587 (patch) | |
tree | 1baec6c70a80b11598e9d0ae51f766dad1494aca | |
parent | da7020ad87122c41d1aa3f4bbdf2a81685c14ada (diff) |
add filterview & update filetree drawing
Signed-off-by: dwillist <dthornton@vmware.com>
-rw-r--r-- | dive/filetree/file_tree.go | 51 | ||||
-rw-r--r-- | dive/filetree/file_tree_test.go | 23 | ||||
-rw-r--r-- | runtime/ui/app.go | 140 | ||||
-rw-r--r-- | runtime/ui/components/filetree_primative.go | 166 | ||||
-rw-r--r-- | runtime/ui/components/filter_primative.go | 28 | ||||
-rw-r--r-- | runtime/ui/components/image_details_view.go | 9 | ||||
-rw-r--r-- | runtime/ui/components/layers_primative.go | 22 |
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 +} |