diff --git a/filetree/node.go b/filetree/node.go index 979ecf1..05596b4 100644 --- a/filetree/node.go +++ b/filetree/node.go @@ -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 { diff --git a/ui/commandview.go b/ui/commandview.go new file mode 100644 index 0000000..14fe2be --- /dev/null +++ b/ui/commandview.go @@ -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 +} diff --git a/ui/filetreeview.go b/ui/filetreeview.go index 8b49f8e..5f38dc7 100644 --- a/ui/filetreeview.go +++ b/ui/filetreeview.go @@ -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() +} diff --git a/ui/statusview.go b/ui/statusview.go index 99cc579..5d440c8 100644 --- a/ui/statusview.go +++ b/ui/statusview.go @@ -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 }) diff --git a/ui/ui.go b/ui/ui.go index 434adfd..59b69bb 100644 --- a/ui/ui.go +++ b/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)