mirror of
https://github.com/wagoodman/dive
synced 2026-03-15 23:05:50 +01:00
Merge pull request #8 from wagoodman/filter-view
Add the capability to filter which nodes are shown by hitting Ctrl+/ and then typing a string
This commit is contained in:
commit
2000a9e8fd
5 changed files with 229 additions and 44 deletions
|
|
@ -1,14 +1,14 @@
|
|||
package filetree
|
||||
|
||||
import (
|
||||
"archive/tar"
|
||||
"fmt"
|
||||
"sort"
|
||||
"strings"
|
||||
|
||||
"github.com/fatih/color"
|
||||
"fmt"
|
||||
"github.com/phayes/permbits"
|
||||
"github.com/dustin/go-humanize"
|
||||
"github.com/wagoodman/docker-image-explorer/_vendor-20180604210951/github.com/Microsoft/go-winio/archive/tar"
|
||||
"github.com/fatih/color"
|
||||
"github.com/phayes/permbits"
|
||||
)
|
||||
|
||||
const (
|
||||
|
|
@ -16,11 +16,11 @@ const (
|
|||
)
|
||||
|
||||
type FileNode struct {
|
||||
Tree *FileTree
|
||||
Parent *FileNode
|
||||
Name string
|
||||
Data NodeData
|
||||
Children map[string]*FileNode
|
||||
Tree *FileTree
|
||||
Parent *FileNode
|
||||
Name string
|
||||
Data NodeData
|
||||
Children map[string]*FileNode
|
||||
}
|
||||
|
||||
func NewNode(parent *FileNode, name string, data FileInfo) (node *FileNode) {
|
||||
|
|
@ -125,7 +125,7 @@ func (node *FileNode) MetadataString() string {
|
|||
userGroup := fmt.Sprintf("%d:%d", user, group)
|
||||
size := humanize.Bytes(uint64(node.Data.FileInfo.TarHeader.FileInfo().Size()))
|
||||
|
||||
return style.Sprint(fmt.Sprintf(AttributeFormat,dir, fileMode, userGroup, size))
|
||||
return style.Sprint(fmt.Sprintf(AttributeFormat, dir, fileMode, userGroup, size))
|
||||
}
|
||||
|
||||
func (node *FileNode) VisitDepthChildFirst(visiter Visiter, evaluator VisitEvaluator) error {
|
||||
|
|
@ -161,7 +161,7 @@ func (node *FileNode) VisitDepthParentFirst(visiter Visiter, evaluator VisitEval
|
|||
}
|
||||
|
||||
// never visit the root node
|
||||
if node != node.Tree.Root{
|
||||
if node != node.Tree.Root {
|
||||
err = visiter(node)
|
||||
if err != nil {
|
||||
return err
|
||||
|
|
@ -225,7 +225,7 @@ func (node *FileNode) deriveDiffType(diffType DiffType) error {
|
|||
|
||||
}
|
||||
|
||||
return node.AssignDiffType(myDiffType)
|
||||
return node.AssignDiffType(myDiffType)
|
||||
}
|
||||
|
||||
func (node *FileNode) AssignDiffType(diffType DiffType) error {
|
||||
|
|
|
|||
94
ui/commandview.go
Normal file
94
ui/commandview.go
Normal file
|
|
@ -0,0 +1,94 @@
|
|||
package ui
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
|
||||
"github.com/jroimartin/gocui"
|
||||
)
|
||||
|
||||
// with special thanks to https://gist.github.com/jroimartin/3b2e943a3811d795e0718b4a95b89bec
|
||||
|
||||
type CommandView struct {
|
||||
Name string
|
||||
gui *gocui.Gui
|
||||
view *gocui.View
|
||||
maxLength int
|
||||
}
|
||||
|
||||
type Input struct {
|
||||
name string
|
||||
x, y int
|
||||
w int
|
||||
maxLength int
|
||||
}
|
||||
|
||||
func NewCommandView(name string, gui *gocui.Gui) (commandview *CommandView) {
|
||||
commandview = new(CommandView)
|
||||
|
||||
// populate main fields
|
||||
commandview.Name = name
|
||||
commandview.gui = gui
|
||||
|
||||
return commandview
|
||||
}
|
||||
|
||||
func (view *CommandView) Setup(v *gocui.View, header *gocui.View) error {
|
||||
|
||||
// set view options
|
||||
view.view = v
|
||||
view.maxLength = 200
|
||||
view.view.Frame = false
|
||||
view.view.BgColor = gocui.ColorDefault + gocui.AttrReverse
|
||||
view.view.Editable = true
|
||||
view.view.Editor = view
|
||||
// set keybindings
|
||||
// if err := view.gui.SetKeybinding(view.Name, gocui.KeyArrowDown, gocui.ModNone, func(*gocui.Gui, *gocui.View) error { return view.CursorDown() }); err != nil {
|
||||
// return err
|
||||
// }
|
||||
// if err := view.gui.SetKeybinding(view.Name, gocui.KeyArrowUp, gocui.ModNone, func(*gocui.Gui, *gocui.View) error { return view.CursorUp() }); err != nil {
|
||||
// return err
|
||||
// }
|
||||
|
||||
view.Render()
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (view *CommandView) CursorDown() error {
|
||||
return nil
|
||||
}
|
||||
|
||||
func (view *CommandView) CursorUp() error {
|
||||
return nil
|
||||
}
|
||||
|
||||
func (i *CommandView) Edit(v *gocui.View, key gocui.Key, ch rune, mod gocui.Modifier) {
|
||||
cx, _ := v.Cursor()
|
||||
ox, _ := v.Origin()
|
||||
limit := ox+cx+1 > i.maxLength
|
||||
switch {
|
||||
case ch != 0 && mod == 0 && !limit:
|
||||
v.EditWrite(ch)
|
||||
case key == gocui.KeySpace && !limit:
|
||||
v.EditWrite(' ')
|
||||
case key == gocui.KeyBackspace || key == gocui.KeyBackspace2:
|
||||
v.EditDelete(true)
|
||||
}
|
||||
if Views.Tree != nil {
|
||||
Views.Tree.ReRender()
|
||||
}
|
||||
}
|
||||
|
||||
func (view *CommandView) KeyHelp() string {
|
||||
return "Type string to filter"
|
||||
}
|
||||
|
||||
func (view *CommandView) Render() error {
|
||||
view.gui.Update(func(g *gocui.Gui) error {
|
||||
fmt.Fprintln(view.view, "")
|
||||
|
||||
return nil
|
||||
})
|
||||
// todo: blerg
|
||||
return nil
|
||||
}
|
||||
|
|
@ -3,12 +3,13 @@ package ui
|
|||
import (
|
||||
"errors"
|
||||
"fmt"
|
||||
|
||||
"github.com/jroimartin/gocui"
|
||||
"github.com/wagoodman/docker-image-explorer/filetree"
|
||||
"github.com/fatih/color"
|
||||
"regexp"
|
||||
"strings"
|
||||
|
||||
"github.com/fatih/color"
|
||||
"github.com/jroimartin/gocui"
|
||||
"github.com/lunixbochs/vtclean"
|
||||
"github.com/wagoodman/docker-image-explorer/filetree"
|
||||
)
|
||||
|
||||
type FileTreeView struct {
|
||||
|
|
@ -74,11 +75,14 @@ func (view *FileTreeView) Setup(v *gocui.View, header *gocui.View) error {
|
|||
if err := view.gui.SetKeybinding(view.Name, gocui.KeyCtrlU, gocui.ModNone, func(*gocui.Gui, *gocui.View) error { return view.toggleShowDiffType(filetree.Unchanged) }); err != nil {
|
||||
return err
|
||||
}
|
||||
if err := view.gui.SetKeybinding(view.Name, gocui.KeyCtrlSlash, gocui.ModNone, func(*gocui.Gui, *gocui.View) error { return nil }); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
view.updateViewTree()
|
||||
view.Render()
|
||||
|
||||
headerStr := fmt.Sprintf(filetree.AttributeFormat + " %s", "P","ermission", "UID:GID", "Size", "Filetree")
|
||||
headerStr := fmt.Sprintf(filetree.AttributeFormat+" %s", "P", "ermission", "UID:GID", "Size", "Filetree")
|
||||
fmt.Fprintln(view.header, Formatting.Header(vtclean.Clean(headerStr, false)))
|
||||
|
||||
return nil
|
||||
|
|
@ -101,12 +105,6 @@ func (view *FileTreeView) setLayer(layerIndex int) error {
|
|||
}
|
||||
view.ModelTree.VisitDepthChildFirst(visitor, nil)
|
||||
|
||||
if debug {
|
||||
v, _ := view.gui.View("debug")
|
||||
v.Clear()
|
||||
_, _ = fmt.Fprintln(v, view.RefTrees[layerIndex])
|
||||
}
|
||||
|
||||
view.view.SetCursor(0, 0)
|
||||
view.TreeIndex = 0
|
||||
view.ModelTree = newTree
|
||||
|
|
@ -146,12 +144,26 @@ func (view *FileTreeView) getAbsPositionNode() (node *filetree.FileNode) {
|
|||
dfsCounter++
|
||||
return nil
|
||||
}
|
||||
|
||||
evaluator = func(curNode *filetree.FileNode) bool {
|
||||
return !curNode.Parent.Data.ViewInfo.Collapsed && !curNode.Data.ViewInfo.Hidden
|
||||
var filterBytes []byte
|
||||
var filterRegex *regexp.Regexp
|
||||
read, err := Views.Command.view.Read(filterBytes)
|
||||
if read > 0 && err == nil {
|
||||
regex, err := regexp.Compile(string(filterBytes))
|
||||
if err == nil {
|
||||
filterRegex = regex
|
||||
}
|
||||
}
|
||||
|
||||
err := view.ModelTree.VisitDepthParentFirst(visiter, evaluator)
|
||||
evaluator = func(curNode *filetree.FileNode) bool {
|
||||
regexMatch := true
|
||||
if filterRegex != nil {
|
||||
match := filterRegex.Find([]byte(curNode.Path()))
|
||||
regexMatch = match != nil
|
||||
}
|
||||
return !curNode.Parent.Data.ViewInfo.Collapsed && !curNode.Data.ViewInfo.Hidden && regexMatch
|
||||
}
|
||||
|
||||
err = view.ModelTree.VisitDepthParentFirst(visiter, evaluator)
|
||||
if err != nil {
|
||||
panic(err)
|
||||
}
|
||||
|
|
@ -161,7 +173,9 @@ func (view *FileTreeView) getAbsPositionNode() (node *filetree.FileNode) {
|
|||
|
||||
func (view *FileTreeView) toggleCollapse() error {
|
||||
node := view.getAbsPositionNode()
|
||||
node.Data.ViewInfo.Collapsed = !node.Data.ViewInfo.Collapsed
|
||||
if node != nil {
|
||||
node.Data.ViewInfo.Collapsed = !node.Data.ViewInfo.Collapsed
|
||||
}
|
||||
view.updateViewTree()
|
||||
return view.Render()
|
||||
}
|
||||
|
|
@ -175,10 +189,39 @@ func (view *FileTreeView) toggleShowDiffType(diffType filetree.DiffType) error {
|
|||
return view.Render()
|
||||
}
|
||||
|
||||
func filterRegex() *regexp.Regexp {
|
||||
if Views.Command == nil || Views.Command.view == nil {
|
||||
return nil
|
||||
}
|
||||
filterString := strings.TrimSpace(Views.Command.view.Buffer())
|
||||
if len(filterString) < 1 {
|
||||
return nil
|
||||
}
|
||||
|
||||
regex, err := regexp.Compile(filterString)
|
||||
if err != nil {
|
||||
return nil
|
||||
}
|
||||
|
||||
return regex
|
||||
}
|
||||
|
||||
func (view *FileTreeView) updateViewTree() {
|
||||
regex := filterRegex()
|
||||
|
||||
// keep the view selection in parity with the current DiffType selection
|
||||
view.ModelTree.VisitDepthChildFirst(func(node *filetree.FileNode) error {
|
||||
node.Data.ViewInfo.Hidden = view.HiddenDiffTypes[node.Data.DiffType]
|
||||
visibleChild := false
|
||||
for _, child := range node.Children {
|
||||
if !child.Data.ViewInfo.Hidden {
|
||||
visibleChild = true
|
||||
}
|
||||
}
|
||||
if regex != nil && !visibleChild {
|
||||
match := regex.FindString(node.Path())
|
||||
node.Data.ViewInfo.Hidden = len(match) == 0
|
||||
}
|
||||
return nil
|
||||
}, nil)
|
||||
|
||||
|
|
@ -194,11 +237,11 @@ func (view *FileTreeView) updateViewTree() {
|
|||
|
||||
func (view *FileTreeView) KeyHelp() string {
|
||||
control := color.New(color.Bold).SprintFunc()
|
||||
return control("[Space]") + ": Collapse dir " +
|
||||
control("[^A]") + ": Added files " +
|
||||
control("[^R]") + ": Removed files " +
|
||||
control("[^M]") + ": Modified files " +
|
||||
control("[^U]") + ": Unmodified files"
|
||||
return control("[Space]") + ": Collapse dir " +
|
||||
control("[^A]") + ": Added files " +
|
||||
control("[^R]") + ": Removed files " +
|
||||
control("[^M]") + ": Modified files " +
|
||||
control("[^U]") + ": Unmodified files"
|
||||
}
|
||||
|
||||
func (view *FileTreeView) Render() error {
|
||||
|
|
@ -218,3 +261,8 @@ func (view *FileTreeView) Render() error {
|
|||
})
|
||||
return nil
|
||||
}
|
||||
|
||||
func (view *FileTreeView) ReRender() error {
|
||||
view.updateViewTree()
|
||||
return view.Render()
|
||||
}
|
||||
|
|
|
|||
|
|
@ -3,8 +3,8 @@ package ui
|
|||
import (
|
||||
"fmt"
|
||||
|
||||
"github.com/jroimartin/gocui"
|
||||
"github.com/fatih/color"
|
||||
"github.com/jroimartin/gocui"
|
||||
)
|
||||
|
||||
type StatusView struct {
|
||||
|
|
@ -43,7 +43,6 @@ func (view *StatusView) Setup(v *gocui.View, header *gocui.View) error {
|
|||
return nil
|
||||
}
|
||||
|
||||
|
||||
func (view *StatusView) CursorDown() error {
|
||||
return nil
|
||||
}
|
||||
|
|
@ -54,15 +53,16 @@ func (view *StatusView) CursorUp() error {
|
|||
|
||||
func (view *StatusView) KeyHelp() string {
|
||||
control := color.New(color.Bold).SprintFunc()
|
||||
return control("[^C]") + ": Quit " +
|
||||
control("[^Space]") + ": Switch View "
|
||||
return control("[^C]") + ": Quit " +
|
||||
control("[^Space]") + ": Switch View " +
|
||||
control("[^/]") + ": Filter files"
|
||||
|
||||
}
|
||||
|
||||
func (view *StatusView) Render() error {
|
||||
view.gui.Update(func(g *gocui.Gui) error {
|
||||
view.view.Clear()
|
||||
fmt.Fprintln(view.view, view.KeyHelp() + " | " + Views.lookup[view.gui.CurrentView().Name()].KeyHelp())
|
||||
fmt.Fprintln(view.view, view.KeyHelp()+" | "+Views.lookup[view.gui.CurrentView().Name()].KeyHelp())
|
||||
|
||||
return nil
|
||||
})
|
||||
|
|
|
|||
63
ui/ui.go
63
ui/ui.go
|
|
@ -1,27 +1,41 @@
|
|||
package ui
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"fmt"
|
||||
"log"
|
||||
|
||||
"github.com/fatih/color"
|
||||
"github.com/jroimartin/gocui"
|
||||
"github.com/wagoodman/docker-image-explorer/filetree"
|
||||
"github.com/wagoodman/docker-image-explorer/image"
|
||||
"github.com/fatih/color"
|
||||
"github.com/wagoodman/docker-image-explorer/_vendor-20180604210951/github.com/pkg/errors"
|
||||
)
|
||||
|
||||
const debug = false
|
||||
const debug = true
|
||||
|
||||
func debugPrint(s string) {
|
||||
if debug && Views.Tree != nil && Views.Tree.gui != nil {
|
||||
v, _ := Views.Tree.gui.View("debug")
|
||||
if v != nil {
|
||||
if len(v.BufferLines()) > 20 {
|
||||
v.Clear()
|
||||
}
|
||||
_, _ = fmt.Fprintln(v, s)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
var Formatting struct {
|
||||
Header func(...interface{})(string)
|
||||
StatusBar func(...interface{})(string)
|
||||
Header func(...interface{}) string
|
||||
StatusBar func(...interface{}) string
|
||||
}
|
||||
|
||||
var Views struct {
|
||||
Tree *FileTreeView
|
||||
Layer *LayerView
|
||||
Status *StatusView
|
||||
lookup map[string]View
|
||||
Tree *FileTreeView
|
||||
Layer *LayerView
|
||||
Status *StatusView
|
||||
Command *CommandView
|
||||
lookup map[string]View
|
||||
}
|
||||
|
||||
type View interface {
|
||||
|
|
@ -43,6 +57,20 @@ func toggleView(g *gocui.Gui, v *gocui.View) error {
|
|||
return err
|
||||
}
|
||||
|
||||
func focusFilterView(g *gocui.Gui, v *gocui.View) error {
|
||||
_, err := g.SetCurrentView(Views.Command.Name)
|
||||
Render()
|
||||
return err
|
||||
}
|
||||
|
||||
func returnToTreeView(g *gocui.Gui, v *gocui.View) error {
|
||||
_, err := g.SetCurrentView(Views.Tree.Name)
|
||||
if Views.Tree != nil {
|
||||
Views.Tree.ReRender()
|
||||
}
|
||||
return err
|
||||
}
|
||||
|
||||
func CursorDown(g *gocui.Gui, v *gocui.View) error {
|
||||
cx, cy := v.Cursor()
|
||||
|
||||
|
|
@ -91,6 +119,12 @@ func keybindings(g *gocui.Gui) error {
|
|||
if err := g.SetKeybinding("main", gocui.KeyCtrlSpace, gocui.ModNone, toggleView); err != nil {
|
||||
return err
|
||||
}
|
||||
if err := g.SetKeybinding("", gocui.KeyCtrlSlash, gocui.ModNone, focusFilterView); err != nil {
|
||||
return err
|
||||
}
|
||||
if err := g.SetKeybinding("command", gocui.KeyEnter, gocui.ModNone, returnToTreeView); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
|
@ -122,7 +156,6 @@ func layout(g *gocui.Gui) error {
|
|||
}
|
||||
}
|
||||
|
||||
|
||||
}
|
||||
// Filetree
|
||||
if view, err := g.SetView(Views.Tree.Name, splitCols, -1+headerRows, debugCols, maxY-bottomRows); err != nil {
|
||||
|
|
@ -154,6 +187,13 @@ func layout(g *gocui.Gui) error {
|
|||
}
|
||||
Views.Status.Setup(view, nil)
|
||||
|
||||
}
|
||||
if view, err := g.SetView(Views.Command.Name, -1, maxY-bottomRows-2, maxX, maxY-1); err != nil {
|
||||
if err != gocui.ErrUnknownView {
|
||||
return err
|
||||
}
|
||||
Views.Command.Setup(view, nil)
|
||||
|
||||
}
|
||||
|
||||
return nil
|
||||
|
|
@ -186,6 +226,9 @@ func Run(layers []*image.Layer, refTrees []*filetree.FileTree) {
|
|||
Views.Status = NewStatusView("status", g)
|
||||
Views.lookup[Views.Status.Name] = Views.Status
|
||||
|
||||
Views.Command = NewCommandView("command", g)
|
||||
Views.lookup[Views.Command.Name] = Views.Command
|
||||
|
||||
g.Cursor = false
|
||||
//g.Mouse = true
|
||||
g.SetManagerFunc(layout)
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue