add filterview & update filetree drawing

Signed-off-by: dwillist <dthornton@vmware.com>
This commit is contained in:
dwillist 2020-10-28 20:15:37 -04:00
commit e2dbdcd3e7
7 changed files with 368 additions and 71 deletions

View file

@ -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))

View file

@ -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)

View file

@ -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)
}
fileTreeView.SetTree(curTree)
})
imageDetails := components.NewImageDetailsView(analysis)
grid := tview.NewGrid()
filterView := components.NewFilterView()
filterView.SetChangedFunc(
func(textToCheck string) {
var filterRegex *regexp.Regexp = nil
var err error
grid := tview.NewGrid().SetRows(-4,-1,-1)
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, false).
AddItem(layerDetails,1,0,1,1,10,10, false).
AddItem(imageDetails,2,0,1, 1,10,10,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

View file

@ -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

View file

@ -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() == ""
}

View file

@ -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").

View file

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