Merge branch 'master' into config/toggle-unchanged-files

# Conflicts:
#	README.md
This commit is contained in:
Alex Goodman 2019-07-21 14:50:45 -04:00
commit 48df08f117
No known key found for this signature in database
GPG key ID: 98AF011C5C78EB7E
55 changed files with 3742 additions and 1570 deletions

View file

@ -12,3 +12,4 @@ ADD .scripts/ /root/.data/
RUN cp /root/saved.txt /tmp/saved.again1.txt
RUN cp /root/saved.txt /root/.data/saved.again2.txt
RUN chmod +x /root/saved.txt
RUN chmod 421 /root

View file

@ -1,8 +1,10 @@
/.git
/.data
/.cover
/dist
/ui
/utils
/image
/cmd
/build
coverage.txt

1
.gitignore vendored
View file

@ -20,3 +20,4 @@
*.log
/dist
.cover
coverage.txt

26
.gitlab-ci.yml Normal file
View file

@ -0,0 +1,26 @@
image: golang:1.12.5
cache:
paths:
- .cache
variables:
GOPATH: $CI_PROJECT_DIR/.cache
stages:
- setup
- validation
setup:
stage: setup
script:
- mkdir -p .cache
- go get ./...
- curl -sfL https://install.goreleaser.com/github.com/golangci/golangci-lint.sh | sh -s -- -b $(go env GOPATH)/bin v1.17.1
validation:
stage: validation
before_script:
- export PATH="$GOPATH/bin:$PATH"
script:
- make ci

View file

@ -1,30 +0,0 @@
language: go
go:
- '1.9.x'
- '1.10.x'
- '1.11.x'
- 'master'
# Skip the install step. Don't `go get` dependencies. Only build with the
# code in vendor/
install: true
matrix:
# It's ok if our code fails on unstable development versions of Go.
allow_failures:
- go: master
# Don't wait for tip tests to finish. Mark the test run green if the
# tests pass on the stable versions of Go.
fast_finish: true
notifications:
email: false
before_script:
- go get -t ./...
# Note: scripts always run to completion
script:
- make validate
- make test

View file

@ -14,22 +14,26 @@ run-large: build
build:
go build -o build/$(BIN)
release: test validate
release: test-coverage validate
./.scripts/tag.sh
goreleaser --rm-dist
install:
go install ./...
test: build
go test -cover -v ./...
ci: clean validate test-coverage
coverage: build
./.scripts/test.sh
test: build
go test -cover -v -race ./...
test-coverage: build
./.scripts/test-coverage.sh
validate:
@! gofmt -s -d -l . 2>&1 | grep -vE '^\.git/'
grep -R 'const allowTestDataCapture = false' ui/
go vet ./...
@! gofmt -s -l . 2>&1 | grep -vE '^\.git/' | grep -vE '^\.cache/'
golangci-lint run
lint: build
golint -set_exit_status $$(go list ./...)
@ -39,7 +43,6 @@ generate-test-data:
clean:
rm -rf build
rm -rf vendor
go clean
.PHONY: build install test lint clean release validate generate-test-data
.PHONY: build install test lint clean release validate generate-test-data test-coverage ci

View file

@ -1,6 +1,6 @@
# dive
[![Go Report Card](https://goreportcard.com/badge/github.com/wagoodman/dive)](https://goreportcard.com/report/github.com/wagoodman/dive)
[![Pipeline Status](https://api.travis-ci.org/wagoodman/dive.svg?branch=master)](https://travis-ci.org/wagoodman/dive)
[![Pipeline Status](https://gitlab.com/wagoodman/dive/badges/master/pipeline.svg)](https://gitlab.com/wagoodman/dive/pipelines?scope=branches&page=1)
**A tool for exploring a docker image, layer contents, and discovering ways to shrink your Docker image size.**
@ -74,14 +74,14 @@ Analyze and image and get a pass/fail result based on the image efficiency and w
**Ubuntu/Debian**
```bash
wget https://github.com/wagoodman/dive/releases/download/v0.6.0/dive_0.6.0_linux_amd64.deb
sudo apt install ./dive_0.6.0_linux_amd64.deb
wget https://github.com/wagoodman/dive/releases/download/v0.7.2/dive_0.7.2_linux_amd64.deb
sudo apt install ./dive_0.7.2_linux_amd64.deb
```
**RHEL/Centos**
```bash
curl -OL https://github.com/wagoodman/dive/releases/download/v0.6.0/dive_0.6.0_linux_amd64.rpm
rpm -i dive_0.6.0_linux_amd64.rpm
curl -OL https://github.com/wagoodman/dive/releases/download/v0.7.2/dive_0.7.2_linux_amd64.rpm
rpm -i dive_0.7.2_linux_amd64.rpm
```
**Arch Linux**
@ -100,11 +100,11 @@ The above example assumes [`yay`](https://aur.archlinux.org/packages/yay/) as th
brew tap wagoodman/dive
brew install dive
```
or download the latest Darwin build from the [releases page](https://github.com/wagoodman/dive/releases/download/v0.6.0/dive_0.6.0_darwin_amd64.tar.gz).
or download the latest Darwin build from the [releases page](https://github.com/wagoodman/dive/releases/download/v0.7.2/dive_0.7.2_darwin_amd64.tar.gz).
**Windows**
Download the [latest release](https://github.com/wagoodman/dive/releases/download/v0.6.0/dive_0.6.0_windows_amd64.zip).
Download the [latest release](https://github.com/wagoodman/dive/releases/download/v0.7.2/dive_0.7.2_windows_amd64.zip).
**Go tools**
Requires Go version 1.9 or higher.
@ -176,15 +176,19 @@ You can override the CI config path with the `--ci-config` option.
Key Binding | Description
-------------------------------------------|---------------------------------------------------------
<kbd>Ctrl + C</kbd> | Exit
<kbd>Tab</kbd> or <kbd>Ctrl + Space</kbd> | Switch between the layer and filetree views
<kbd>Tab</kbd> | Switch between the layer and filetree views
<kbd>Ctrl + F</kbd> | Filter files
<kbd>PageUp</kbd> | Scroll up a page
<kbd>PageDown</kbd> | Scroll down a page
<kbd>Ctrl + A</kbd> | Layer view: see aggregated image modifications
<kbd>Ctrl + L</kbd> | Layer view: see current layer modifications
<kbd>Space</kbd> | Filetree view: collapse/uncollapse a directory
<kbd>Ctrl + Space</kbd> | Filetree view: collapse/uncollapse all directories
<kbd>Ctrl + A</kbd> | Filetree view: show/hide added files
<kbd>Ctrl + R</kbd> | Filetree view: show/hide removed files
<kbd>Ctrl + M</kbd> | Filetree view: show/hide modified files
<kbd>Ctrl + U</kbd> | Filetree view: show/hide unchanged files
<kbd>Ctrl + U</kbd> | Filetree view: show/hide unmodified files
<kbd>Ctrl + B</kbd> | Filetree view: show/hide file attributes
<kbd>PageUp</kbd> | Filetree view: scroll up a page
<kbd>PageDown</kbd> | Filetree view: scroll down a page
@ -202,7 +206,7 @@ log:
keybinding:
# Global bindings
quit: ctrl+c
toggle-view: tab, ctrl+space
toggle-view: tab
filter-files: ctrl+f, ctrl+slash
# Layer view specific bindings
@ -211,10 +215,12 @@ keybinding:
# File view specific bindings
toggle-collapse-dir: space
toggle-collapse-all-dir: ctrl+space
toggle-added-files: ctrl+a
toggle-removed-files: ctrl+r
toggle-modified-files: ctrl+m
toggle-unchanged-files: ctrl+u
toggle-unmodified-files: ctrl+u
toggle-filetree-attributes: ctrl+b
page-up: pgup
page-down: pgdn
@ -232,6 +238,9 @@ filetree:
# The percentage of screen width the filetree should take on the screen (must be >0 and <1)
pane-width: 0.5
# Show the file attributes next to the filetree
show-attributes: true
layer:
# Enable showing all changes from this layer and ever previous layer

View file

@ -19,17 +19,19 @@ func doAnalyzeCmd(cmd *cobra.Command, args []string) {
}
fmt.Println("No image argument given")
cmd.Help()
_ = cmd.Help()
utils.Exit(1)
}
userImage := args[0]
if userImage == "" {
fmt.Println("No image argument given")
cmd.Help()
_ = cmd.Help()
utils.Exit(1)
}
initLogging()
runtime.Run(runtime.Options{
ImageId: userImage,
ExportFile: exportFile,

View file

@ -22,6 +22,8 @@ func init() {
func doBuildCmd(cmd *cobra.Command, args []string) {
defer utils.Cleanup()
initLogging()
runtime.Run(runtime.Options{
BuildArgs: args,
ExportFile: exportFile,

View file

@ -7,7 +7,6 @@ import (
"path"
"strings"
"github.com/k0kubun/go-ansi"
"github.com/mitchellh/go-homedir"
log "github.com/sirupsen/logrus"
"github.com/spf13/cobra"
@ -16,8 +15,6 @@ import (
"github.com/wagoodman/dive/utils"
)
const pathSep = string(os.PathSeparator)
var cfgFile string
var exportFile string
var ciConfigFile string
@ -42,12 +39,9 @@ func Execute() {
}
func init() {
ansi.CursorHide()
cobra.OnInitialize(initConfig)
cobra.OnInitialize(initLogging)
rootCmd.PersistentFlags().StringVar(&cfgFile, "config", "", "config file (default is $HOME/.dive.yaml, ~/.config/dive.yaml, or $XDG_CONFIG_HOME/dive.yaml)")
rootCmd.PersistentFlags().StringVar(&cfgFile, "config", "", "config file (default is $HOME/.dive.yaml, ~/.config/dive/*.yaml, or $XDG_CONFIG_HOME/dive.yaml)")
rootCmd.PersistentFlags().BoolP("version", "v", false, "display version number")
rootCmd.Flags().StringVarP(&exportFile, "json", "j", "", "Skip the interactive TUI and write the layer analysis statistics to a given file.")
@ -64,13 +58,15 @@ func initConfig() {
viper.SetDefault("log.enabled", true)
// keybindings: status view / global
viper.SetDefault("keybinding.quit", "ctrl+c")
viper.SetDefault("keybinding.toggle-view", "tab, ctrl+space")
viper.SetDefault("keybinding.toggle-view", "tab")
viper.SetDefault("keybinding.filter-files", "ctrl+f, ctrl+slash")
// keybindings: layer view
viper.SetDefault("keybinding.compare-all", "ctrl+a")
viper.SetDefault("keybinding.compare-layer", "ctrl+l")
// keybindings: filetree view
viper.SetDefault("keybinding.toggle-collapse-dir", "space")
viper.SetDefault("keybinding.toggle-collapse-all-dir", "ctrl+space")
viper.SetDefault("keybinding.toggle-filetree-attributes", "ctrl+b")
viper.SetDefault("keybinding.toggle-added-files", "ctrl+a")
viper.SetDefault("keybinding.toggle-removed-files", "ctrl+r")
viper.SetDefault("keybinding.toggle-modified-files", "ctrl+m")
@ -84,6 +80,7 @@ func initConfig() {
viper.SetDefault("filetree.collapse-dir", false)
viper.SetDefault("filetree.pane-width", 0.5)
viper.SetDefault("filetree.show-attributes", true)
viper.AutomaticEnv() // read in environment variables that match
@ -142,7 +139,7 @@ func getCfgFile(fromFlag string) string {
xdgHome := os.Getenv("XDG_CONFIG_HOME")
xdgDirs := os.Getenv("XDG_CONFIG_DIRS")
xdgPaths := append([]string{xdgHome}, strings.Split(xdgDirs, ":")...)
allDirs := append(xdgPaths, home+pathSep+".config")
allDirs := append(xdgPaths, path.Join(home, ".config"))
for _, val := range allDirs {
file := findInPath(val)
@ -150,13 +147,13 @@ func getCfgFile(fromFlag string) string {
return file
}
}
return home + pathSep + "dive.yaml"
return path.Join(home, ".dive.yaml")
}
// findInPath returns first "*.yaml" file in path's subdirectory "dive"
// if not found returns empty string
func findInPath(pathTo string) string {
directory := pathTo + pathSep + "dive"
directory := path.Join(pathTo, "dive")
files, err := ioutil.ReadDir(directory)
if err != nil {
return ""
@ -165,7 +162,7 @@ func findInPath(pathTo string) string {
for _, file := range files {
filename := file.Name()
if path.Ext(filename) == ".yaml" {
return directory + pathSep + filename
return path.Join(directory, filename)
}
}
return ""

View file

@ -1,5 +1,9 @@
package filetree
import (
"github.com/sirupsen/logrus"
)
type TreeCacheKey struct {
bottomTreeStart, bottomTreeStop, topTreeStart, topTreeStop int
}
@ -13,8 +17,6 @@ func (cache *TreeCache) Get(bottomTreeStart, bottomTreeStop, topTreeStart, topTr
key := TreeCacheKey{bottomTreeStart, bottomTreeStop, topTreeStart, topTreeStop}
if value, exists := cache.cache[key]; exists {
return value
} else {
}
value := cache.buildTree(key)
cache.cache[key] = value
@ -23,9 +25,11 @@ func (cache *TreeCache) Get(bottomTreeStart, bottomTreeStop, topTreeStart, topTr
func (cache *TreeCache) buildTree(key TreeCacheKey) *FileTree {
newTree := StackTreeRange(cache.refTrees, key.bottomTreeStart, key.bottomTreeStop)
for idx := key.topTreeStart; idx <= key.topTreeStop; idx++ {
newTree.Compare(cache.refTrees[idx])
err := newTree.CompareAndMark(cache.refTrees[idx])
if err != nil {
logrus.Errorf("unable to build tree: %+v", err)
}
}
return newTree
}

View file

@ -64,7 +64,10 @@ func getHashFromReader(reader io.Reader) uint64 {
break
}
h.Write(buf[:n])
_, err = h.Write(buf[:n])
if err != nil {
logrus.Panic(err)
}
}
return h.Sum64()

View file

@ -56,7 +56,10 @@ func Efficiency(trees []*FileTree) (float64, EfficiencySlice) {
if err != nil {
logrus.Debug(fmt.Sprintf("CurrentTree: %d : %s", currentTree, err))
} else if previousTreeNode.Data.FileInfo.IsDir {
previousTreeNode.VisitDepthChildFirst(sizer, nil)
err = previousTreeNode.VisitDepthChildFirst(sizer, nil)
if err != nil {
logrus.Errorf("unable to propagate whiteout dir: %+v", err)
}
}
} else {
@ -80,7 +83,10 @@ func Efficiency(trees []*FileTree) (float64, EfficiencySlice) {
}
for idx, tree := range trees {
currentTree = idx
tree.VisitDepthChildFirst(visitor, visitEvaluator)
err := tree.VisitDepthChildFirst(visitor, visitEvaluator)
if err != nil {
logrus.Errorf("unable to propagate ref tree: %+v", err)
}
}
// calculate the score

View file

@ -4,19 +4,31 @@ import (
"testing"
)
func checkError(t *testing.T, err error, message string) {
if err != nil {
t.Errorf(message+": %+v", err)
}
}
func TestEfficency(t *testing.T) {
trees := make([]*FileTree, 3)
for idx := range trees {
trees[idx] = NewFileTree()
}
trees[0].AddPath("/etc/nginx/nginx.conf", FileInfo{Size: 2000})
trees[0].AddPath("/etc/nginx/public", FileInfo{Size: 3000})
_, _, err := trees[0].AddPath("/etc/nginx/nginx.conf", FileInfo{Size: 2000})
checkError(t, err, "could not setup test")
trees[1].AddPath("/etc/nginx/nginx.conf", FileInfo{Size: 5000})
trees[1].AddPath("/etc/athing", FileInfo{Size: 10000})
_, _, err = trees[0].AddPath("/etc/nginx/public", FileInfo{Size: 3000})
checkError(t, err, "could not setup test")
trees[2].AddPath("/etc/.wh.nginx", *BlankFileChangeInfo("/etc/.wh.nginx"))
_, _, err = trees[1].AddPath("/etc/nginx/nginx.conf", FileInfo{Size: 5000})
checkError(t, err, "could not setup test")
_, _, err = trees[1].AddPath("/etc/athing", FileInfo{Size: 10000})
checkError(t, err, "could not setup test")
_, _, err = trees[2].AddPath("/etc/.wh.nginx", *BlankFileChangeInfo("/etc/.wh.nginx"))
checkError(t, err, "could not setup test")
var expectedScore = 0.75
var expectedMatches = EfficiencySlice{
@ -50,7 +62,8 @@ func TestEfficency_ScratchImage(t *testing.T) {
trees[idx] = NewFileTree()
}
trees[0].AddPath("/nothing", FileInfo{Size: 0})
_, _, err := trees[0].AddPath("/nothing", FileInfo{Size: 0})
checkError(t, err, "could not setup test")
var expectedScore = 1.0
var expectedMatches = EfficiencySlice{}

View file

@ -3,6 +3,7 @@ package filetree
import (
"archive/tar"
"fmt"
"github.com/sirupsen/logrus"
"sort"
"strings"
@ -99,7 +100,10 @@ func (node *FileNode) Remove() error {
return fmt.Errorf("cannot remove the tree root")
}
for _, child := range node.Children {
child.Remove()
err := child.Remove()
if err != nil {
return err
}
}
delete(node.Parent.Children, node.Name)
node.Tree.Size--
@ -149,7 +153,10 @@ func (node *FileNode) MetadataString() string {
return nil
}
node.VisitDepthChildFirst(sizer, nil)
err := node.VisitDepthChildFirst(sizer, nil)
if err != nil {
logrus.Errorf("unable to propagate node for metadata: %+v", err)
}
}
size := humanize.Bytes(uint64(sizeBytes))
@ -258,7 +265,6 @@ func (node *FileNode) deriveDiffType(diffType DiffType) error {
myDiffType := diffType
for _, v := range node.Children {
myDiffType = myDiffType.merge(v.Data.DiffType)
}
return node.AssignDiffType(myDiffType)
@ -303,7 +309,6 @@ func (node *FileNode) compare(other *FileNode) DiffType {
if node.Name != other.Name {
panic("comparing mismatched nodes")
}
// TODO: fails on nil
return node.Data.FileInfo.Compare(other.Data.FileInfo)
}

View file

@ -56,7 +56,8 @@ func TestRemoveChild(t *testing.T) {
forth := two.AddChild("forth", FileInfo{})
two.AddChild("fifth", FileInfo{})
forth.Remove()
err := forth.Remove()
checkError(t, err, "unable to setup test")
expected, actual = 4, tree.Size
if expected != actual {
@ -67,7 +68,8 @@ func TestRemoveChild(t *testing.T) {
t.Errorf("Expected 'forth' node to be deleted.")
}
two.Remove()
err = two.Remove()
checkError(t, err, "unable to setup test")
expected, actual = 2, tree.Size
if expected != actual {
@ -121,7 +123,8 @@ func TestDiffTypeFromAddedChildren(t *testing.T) {
node, _, _ = tree.AddPath("/usr/bin2", *BlankFileChangeInfo("/usr/bin2"))
node.Data.DiffType = Removed
tree.Root.Children["usr"].deriveDiffType(Unchanged)
err := tree.Root.Children["usr"].deriveDiffType(Unchanged)
checkError(t, err, "unable to setup test")
if tree.Root.Children["usr"].Data.DiffType != Changed {
t.Errorf("Expected Changed but got %v", tree.Root.Children["usr"].Data.DiffType)
@ -129,17 +132,18 @@ func TestDiffTypeFromAddedChildren(t *testing.T) {
}
func TestDiffTypeFromRemovedChildren(t *testing.T) {
tree := NewFileTree()
node, _, _ := tree.AddPath("/usr", *BlankFileChangeInfo("/usr"))
_, _, _ = tree.AddPath("/usr", *BlankFileChangeInfo("/usr"))
info1 := BlankFileChangeInfo("/usr/.wh.bin")
node, _, _ = tree.AddPath("/usr/.wh.bin", *info1)
node, _, _ := tree.AddPath("/usr/.wh.bin", *info1)
node.Data.DiffType = Removed
info2 := BlankFileChangeInfo("/usr/.wh.bin2")
node, _, _ = tree.AddPath("/usr/.wh.bin2", *info2)
node.Data.DiffType = Removed
tree.Root.Children["usr"].deriveDiffType(Unchanged)
err := tree.Root.Children["usr"].deriveDiffType(Unchanged)
checkError(t, err, "unable to setup test")
if tree.Root.Children["usr"].Data.DiffType != Changed {
t.Errorf("Expected Changed but got %v", tree.Root.Children["usr"].Data.DiffType)
@ -149,9 +153,12 @@ func TestDiffTypeFromRemovedChildren(t *testing.T) {
func TestDirSize(t *testing.T) {
tree1 := NewFileTree()
tree1.AddPath("/etc/nginx/public1", FileInfo{Size: 100})
tree1.AddPath("/etc/nginx/thing1", FileInfo{Size: 200})
tree1.AddPath("/etc/nginx/public3/thing2", FileInfo{Size: 300})
_, _, err := tree1.AddPath("/etc/nginx/public1", FileInfo{Size: 100})
checkError(t, err, "unable to setup test")
_, _, err = tree1.AddPath("/etc/nginx/thing1", FileInfo{Size: 200})
checkError(t, err, "unable to setup test")
_, _, err = tree1.AddPath("/etc/nginx/public3/thing2", FileInfo{Size: 300})
checkError(t, err, "unable to setup test")
node, _ := tree1.GetNode("/etc/nginx")
expected, actual := "---------- 0:0 600 B ", node.MetadataString()

View file

@ -119,14 +119,42 @@ func (tree *FileTree) renderStringTreeBetween(startRow, stopRow int, showAttribu
return result
}
func (tree *FileTree) VisibleSize() int {
var size int
visitor := func(node *FileNode) error {
size++
return nil
}
visitEvaluator := func(node *FileNode) bool {
if node.Data.FileInfo.IsDir {
// we won't visit a collapsed dir, but we need to count it
if node.Data.ViewInfo.Collapsed {
size++
}
return !node.Data.ViewInfo.Collapsed && !node.Data.ViewInfo.Hidden
}
return !node.Data.ViewInfo.Hidden
}
err := tree.VisitDepthParentFirst(visitor, visitEvaluator)
if err != nil {
logrus.Errorf("unable to determine visible tree size: %+v", err)
}
// don't include root
size--
return size
}
// String returns the entire tree in an ASCII representation.
func (tree *FileTree) String(showAttributes bool) string {
return tree.renderStringTreeBetween(0, tree.Size, showAttributes)
}
// StringBetween returns a partial tree in an ASCII representation.
func (tree *FileTree) StringBetween(start, stop uint, showAttributes bool) string {
return tree.renderStringTreeBetween(int(start), int(stop), showAttributes)
func (tree *FileTree) StringBetween(start, stop int, showAttributes bool) string {
return tree.renderStringTreeBetween(start, stop, showAttributes)
}
// Copy returns a copy of the given FileTree
@ -137,11 +165,15 @@ func (tree *FileTree) Copy() *FileTree {
newTree.Root = tree.Root.Copy(newTree.Root)
// update the tree pointers
newTree.VisitDepthChildFirst(func(node *FileNode) error {
err := newTree.VisitDepthChildFirst(func(node *FileNode) error {
node.Tree = newTree
return nil
}, nil)
if err != nil {
logrus.Errorf("unable to propagate tree on copy(): %+v", err)
}
return newTree
}
@ -209,6 +241,11 @@ func (tree *FileTree) AddPath(path string, data FileInfo) (*FileNode, []*FileNod
if node.Children[name] != nil {
node = node.Children[name]
} else {
// don't add paths that should be deleted
if strings.HasPrefix(name, doubleWhiteoutPrefix) {
return nil, addedNodes, nil
}
// don't attach the payload. The payload is destined for the
// Path's end node, not any intermediary node.
node = node.AddChild(name, FileInfo{})
@ -216,7 +253,7 @@ func (tree *FileTree) AddPath(path string, data FileInfo) (*FileNode, []*FileNod
if node == nil {
// the child could not be added
return node, addedNodes, fmt.Errorf(fmt.Sprintf("could not add child node '%s'", name))
return node, addedNodes, fmt.Errorf(fmt.Sprintf("could not add child node: '%s' (path:'%s')", name, path))
}
}
@ -239,13 +276,14 @@ func (tree *FileTree) RemovePath(path string) error {
}
type compareMark struct {
node *FileNode
lowerNode *FileNode
upperNode *FileNode
tentative DiffType
final DiffType
}
// Compare marks the FileNodes in the owning (lower) tree with DiffType annotations when compared to the given (upper) tree.
func (tree *FileTree) Compare(upper *FileTree) error {
// CompareAndMark marks the FileNodes in the owning (lower) tree with DiffType annotations when compared to the given (upper) tree.
func (tree *FileTree) CompareAndMark(upper *FileTree) error {
// always compare relative to the original, unaltered tree.
originalTree := tree
@ -257,28 +295,30 @@ func (tree *FileTree) Compare(upper *FileTree) error {
if err != nil {
return fmt.Errorf("cannot remove upperNode %s: %v", upperNode.Path(), err.Error())
}
} else {
// note: since we are not comparing against the original tree (copying the tree is expensive) we may mark the parent
// of an added node incorrectly as modified. This will be corrected later.
originalLowerNode, _ := originalTree.GetNode(upperNode.Path())
if originalLowerNode == nil {
_, newNodes, err := tree.AddPath(upperNode.Path(), upperNode.Data.FileInfo)
if err != nil {
return fmt.Errorf("cannot add new upperNode %s: %v", upperNode.Path(), err.Error())
}
for idx := len(newNodes) - 1; idx >= 0; idx-- {
newNode := newNodes[idx]
modifications = append(modifications, compareMark{node: newNode, tentative: -1, final: Added})
}
} else {
// check the tree for comparison markings
lowerNode, _ := tree.GetNode(upperNode.Path())
diffType := lowerNode.compare(upperNode)
modifications = append(modifications, compareMark{node: lowerNode, tentative: diffType, final: -1})
}
return nil
}
// note: since we are not comparing against the original tree (copying the tree is expensive) we may mark the parent
// of an added node incorrectly as modified. This will be corrected later.
originalLowerNode, _ := originalTree.GetNode(upperNode.Path())
if originalLowerNode == nil {
_, newNodes, err := tree.AddPath(upperNode.Path(), upperNode.Data.FileInfo)
if err != nil {
return fmt.Errorf("cannot add new upperNode %s: %v", upperNode.Path(), err.Error())
}
for idx := len(newNodes) - 1; idx >= 0; idx-- {
newNode := newNodes[idx]
modifications = append(modifications, compareMark{lowerNode: newNode, upperNode: upperNode, tentative: -1, final: Added})
}
return nil
}
// the file exists in the lower layer
lowerNode, _ := tree.GetNode(upperNode.Path())
diffType := lowerNode.compare(upperNode)
modifications = append(modifications, compareMark{lowerNode: lowerNode, upperNode: upperNode, tentative: diffType, final: -1})
return nil
}
// we must visit from the leaves upwards to ensure that diff types can be derived from and assigned to children
@ -287,15 +327,22 @@ func (tree *FileTree) Compare(upper *FileTree) error {
return err
}
// take note of the comparison results on each note in the owning tree
// take note of the comparison results on each note in the owning tree.
for _, pair := range modifications {
if pair.final > 0 {
pair.node.AssignDiffType(pair.final)
} else {
if pair.node.Data.DiffType == Unchanged {
pair.node.deriveDiffType(pair.tentative)
err = pair.lowerNode.AssignDiffType(pair.final)
if err != nil {
return err
}
} else if pair.lowerNode.Data.DiffType == Unchanged {
err = pair.lowerNode.deriveDiffType(pair.tentative)
if err != nil {
return err
}
}
// persist the upper's payload on the owning tree
pair.lowerNode.Data.FileInfo = *pair.upperNode.Data.FileInfo.Copy()
}
return nil
}
@ -316,7 +363,7 @@ func StackTreeRange(trees []*FileTree, start, stop int) *FileTree {
for idx := start; idx <= stop; idx++ {
err := tree.Stack(trees[idx])
if err != nil {
logrus.Debug("could not stack tree range:", err)
logrus.Errorf("could not stack tree range: %v", err)
}
}
return tree

View file

@ -87,12 +87,30 @@ func TestString(t *testing.T) {
func TestStringBetween(t *testing.T) {
tree := NewFileTree()
tree.AddPath("/etc/nginx/nginx.conf", FileInfo{})
tree.AddPath("/etc/nginx/public", FileInfo{})
tree.AddPath("/var/run/systemd", FileInfo{})
tree.AddPath("/var/run/bashful", FileInfo{})
tree.AddPath("/tmp", FileInfo{})
tree.AddPath("/tmp/nonsense", FileInfo{})
_, _, err := tree.AddPath("/etc/nginx/nginx.conf", FileInfo{})
if err != nil {
t.Errorf("could not setup test: %v", err)
}
_, _, err = tree.AddPath("/etc/nginx/public", FileInfo{})
if err != nil {
t.Errorf("could not setup test: %v", err)
}
_, _, err = tree.AddPath("/var/run/systemd", FileInfo{})
if err != nil {
t.Errorf("could not setup test: %v", err)
}
_, _, err = tree.AddPath("/var/run/bashful", FileInfo{})
if err != nil {
t.Errorf("could not setup test: %v", err)
}
_, _, err = tree.AddPath("/tmp", FileInfo{})
if err != nil {
t.Errorf("could not setup test: %v", err)
}
_, _, err = tree.AddPath("/tmp/nonsense", FileInfo{})
if err != nil {
t.Errorf("could not setup test: %v", err)
}
expected :=
` public
@ -109,12 +127,30 @@ func TestStringBetween(t *testing.T) {
func TestAddPath(t *testing.T) {
tree := NewFileTree()
tree.AddPath("/etc/nginx/nginx.conf", FileInfo{})
tree.AddPath("/etc/nginx/public", FileInfo{})
tree.AddPath("/var/run/systemd", FileInfo{})
tree.AddPath("/var/run/bashful", FileInfo{})
tree.AddPath("/tmp", FileInfo{})
tree.AddPath("/tmp/nonsense", FileInfo{})
_, _, err := tree.AddPath("/etc/nginx/nginx.conf", FileInfo{})
if err != nil {
t.Errorf("could not setup test: %v", err)
}
_, _, err = tree.AddPath("/etc/nginx/public", FileInfo{})
if err != nil {
t.Errorf("could not setup test: %v", err)
}
_, _, err = tree.AddPath("/var/run/systemd", FileInfo{})
if err != nil {
t.Errorf("could not setup test: %v", err)
}
_, _, err = tree.AddPath("/var/run/bashful", FileInfo{})
if err != nil {
t.Errorf("could not setup test: %v", err)
}
_, _, err = tree.AddPath("/tmp", FileInfo{})
if err != nil {
t.Errorf("could not setup test: %v", err)
}
_, _, err = tree.AddPath("/tmp/nonsense", FileInfo{})
if err != nil {
t.Errorf("could not setup test: %v", err)
}
expected :=
` etc
@ -136,17 +172,65 @@ func TestAddPath(t *testing.T) {
}
func TestAddWhiteoutPath(t *testing.T) {
tree := NewFileTree()
node, _, err := tree.AddPath("usr/local/lib/python3.7/site-packages/pip/.wh..wh..opq", FileInfo{})
if err != nil {
t.Errorf("expected no error but got: %v", err)
}
if node != nil {
t.Errorf("expected node to be nil, but got: %v", node)
}
expected :=
` usr
local
lib
python3.7
site-packages
pip
`
actual := tree.String(false)
if expected != actual {
t.Errorf("Expected tree string:\n--->%s<---\nGot:\n--->%s<---", expected, actual)
}
}
func TestRemovePath(t *testing.T) {
tree := NewFileTree()
tree.AddPath("/etc/nginx/nginx.conf", FileInfo{})
tree.AddPath("/etc/nginx/public", FileInfo{})
tree.AddPath("/var/run/systemd", FileInfo{})
tree.AddPath("/var/run/bashful", FileInfo{})
tree.AddPath("/tmp", FileInfo{})
tree.AddPath("/tmp/nonsense", FileInfo{})
_, _, err := tree.AddPath("/etc/nginx/nginx.conf", FileInfo{})
if err != nil {
t.Errorf("could not setup test: %v", err)
}
_, _, err = tree.AddPath("/etc/nginx/public", FileInfo{})
if err != nil {
t.Errorf("could not setup test: %v", err)
}
_, _, err = tree.AddPath("/var/run/systemd", FileInfo{})
if err != nil {
t.Errorf("could not setup test: %v", err)
}
_, _, err = tree.AddPath("/var/run/bashful", FileInfo{})
if err != nil {
t.Errorf("could not setup test: %v", err)
}
_, _, err = tree.AddPath("/tmp", FileInfo{})
if err != nil {
t.Errorf("could not setup test: %v", err)
}
_, _, err = tree.AddPath("/tmp/nonsense", FileInfo{})
if err != nil {
t.Errorf("could not setup test: %v", err)
}
tree.RemovePath("/var/run/bashful")
tree.RemovePath("/tmp")
err = tree.RemovePath("/var/run/bashful")
if err != nil {
t.Errorf("could not setup test: %v", err)
}
err = tree.RemovePath("/tmp")
if err != nil {
t.Errorf("could not setup test: %v", err)
}
expected :=
` etc
@ -173,24 +257,57 @@ func TestStack(t *testing.T) {
tree1 := NewFileTree()
tree1.AddPath("/etc/nginx/public", FileInfo{})
tree1.AddPath(payloadKey, FileInfo{})
tree1.AddPath("/var/run/bashful", FileInfo{})
tree1.AddPath("/tmp", FileInfo{})
tree1.AddPath("/tmp/nonsense", FileInfo{})
_, _, err := tree1.AddPath("/etc/nginx/public", FileInfo{})
if err != nil {
t.Errorf("could not setup test: %v", err)
}
_, _, err = tree1.AddPath(payloadKey, FileInfo{})
if err != nil {
t.Errorf("could not setup test: %v", err)
}
_, _, err = tree1.AddPath("/var/run/bashful", FileInfo{})
if err != nil {
t.Errorf("could not setup test: %v", err)
}
_, _, err = tree1.AddPath("/tmp", FileInfo{})
if err != nil {
t.Errorf("could not setup test: %v", err)
}
_, _, err = tree1.AddPath("/tmp/nonsense", FileInfo{})
if err != nil {
t.Errorf("could not setup test: %v", err)
}
tree2 := NewFileTree()
// add new files
tree2.AddPath("/etc/nginx/nginx.conf", FileInfo{})
_, _, err = tree2.AddPath("/etc/nginx/nginx.conf", FileInfo{})
if err != nil {
t.Errorf("could not setup test: %v", err)
}
// modify current files
tree2.AddPath(payloadKey, payloadValue)
_, _, err = tree2.AddPath(payloadKey, payloadValue)
if err != nil {
t.Errorf("could not setup test: %v", err)
}
// whiteout the following files
tree2.AddPath("/var/run/.wh.bashful", FileInfo{})
tree2.AddPath("/.wh.tmp", FileInfo{})
_, _, err = tree2.AddPath("/var/run/.wh.bashful", FileInfo{})
if err != nil {
t.Errorf("could not setup test: %v", err)
}
_, _, err = tree2.AddPath("/.wh.tmp", FileInfo{})
if err != nil {
t.Errorf("could not setup test: %v", err)
}
// ignore opaque whiteout files entirely
tree2.AddPath("/.wh..wh..opq", FileInfo{})
node, _, err := tree2.AddPath("/.wh..wh..opq", FileInfo{})
if err != nil {
t.Errorf("expected no error on whiteout file add, but got %v", err)
}
if node != nil {
t.Errorf("expected no node on whiteout file add, but got %v", node)
}
err := tree1.Stack(tree2)
err = tree1.Stack(tree2)
if err != nil {
t.Errorf("Could not stack refTrees: %v", err)
@ -206,12 +323,12 @@ func TestStack(t *testing.T) {
systemd
`
node, err := tree1.GetNode(payloadKey)
node, err = tree1.GetNode(payloadKey)
if err != nil {
t.Errorf("Expected '%s' to still exist, but it doesn't", payloadKey)
}
if node.Data.FileInfo.Path != payloadValue.Path {
if node == nil || node.Data.FileInfo.Path != payloadValue.Path {
t.Errorf("Expected '%s' value to be %+v but got %+v", payloadKey, payloadValue, node.Data.FileInfo)
}
@ -225,15 +342,39 @@ func TestStack(t *testing.T) {
func TestCopy(t *testing.T) {
tree := NewFileTree()
tree.AddPath("/etc/nginx/nginx.conf", FileInfo{})
tree.AddPath("/etc/nginx/public", FileInfo{})
tree.AddPath("/var/run/systemd", FileInfo{})
tree.AddPath("/var/run/bashful", FileInfo{})
tree.AddPath("/tmp", FileInfo{})
tree.AddPath("/tmp/nonsense", FileInfo{})
_, _, err := tree.AddPath("/etc/nginx/nginx.conf", FileInfo{})
if err != nil {
t.Errorf("could not setup test: %v", err)
}
_, _, err = tree.AddPath("/etc/nginx/public", FileInfo{})
if err != nil {
t.Errorf("could not setup test: %v", err)
}
_, _, err = tree.AddPath("/var/run/systemd", FileInfo{})
if err != nil {
t.Errorf("could not setup test: %v", err)
}
_, _, err = tree.AddPath("/var/run/bashful", FileInfo{})
if err != nil {
t.Errorf("could not setup test: %v", err)
}
_, _, err = tree.AddPath("/tmp", FileInfo{})
if err != nil {
t.Errorf("could not setup test: %v", err)
}
_, _, err = tree.AddPath("/tmp/nonsense", FileInfo{})
if err != nil {
t.Errorf("could not setup test: %v", err)
}
tree.RemovePath("/var/run/bashful")
tree.RemovePath("/tmp")
err = tree.RemovePath("/var/run/bashful")
if err != nil {
t.Errorf("could not setup test: %v", err)
}
err = tree.RemovePath("/tmp")
if err != nil {
t.Errorf("could not setup test: %v", err)
}
expected :=
` etc
@ -265,10 +406,19 @@ func TestCompareWithNoChanges(t *testing.T) {
TypeFlag: 1,
hash: 123,
}
lowerTree.AddPath(value, fakeData)
upperTree.AddPath(value, fakeData)
_, _, err := lowerTree.AddPath(value, fakeData)
if err != nil {
t.Errorf("could not setup test: %v", err)
}
_, _, err = upperTree.AddPath(value, fakeData)
if err != nil {
t.Errorf("could not setup test: %v", err)
}
}
err := lowerTree.CompareAndMark(upperTree)
if err != nil {
t.Errorf("could not setup test: %v", err)
}
lowerTree.Compare(upperTree)
asserter := func(n *FileNode) error {
if n.Path() == "/" {
return nil
@ -278,7 +428,7 @@ func TestCompareWithNoChanges(t *testing.T) {
}
return nil
}
err := lowerTree.VisitDepthChildFirst(asserter, nil)
err = lowerTree.VisitDepthChildFirst(asserter, nil)
if err != nil {
t.Error(err)
}
@ -291,23 +441,29 @@ func TestCompareWithAdds(t *testing.T) {
upperPaths := [...]string{"/etc", "/etc/sudoers", "/usr", "/etc/hosts", "/usr/bin", "/usr/bin/bash", "/a/new/path"}
for _, value := range lowerPaths {
lowerTree.AddPath(value, FileInfo{
_, _, err := lowerTree.AddPath(value, FileInfo{
Path: value,
TypeFlag: 1,
hash: 123,
})
if err != nil {
t.Errorf("could not setup test: %v", err)
}
}
for _, value := range upperPaths {
upperTree.AddPath(value, FileInfo{
_, _, err := upperTree.AddPath(value, FileInfo{
Path: value,
TypeFlag: 1,
hash: 123,
})
if err != nil {
t.Errorf("could not setup test: %v", err)
}
}
failedAssertions := []error{}
err := lowerTree.Compare(upperTree)
err := lowerTree.CompareAndMark(upperTree)
if err != nil {
t.Errorf("Expected tree compare to have no errors, got: %v", err)
}
@ -351,39 +507,51 @@ func TestCompareWithChanges(t *testing.T) {
changedPaths := []string{"/etc", "/usr", "/etc/hosts", "/etc/sudoers", "/usr/bin"}
for _, value := range changedPaths {
lowerTree.AddPath(value, FileInfo{
_, _, err := lowerTree.AddPath(value, FileInfo{
Path: value,
TypeFlag: 1,
hash: 123,
})
upperTree.AddPath(value, FileInfo{
if err != nil {
t.Errorf("could not setup test: %v", err)
}
_, _, err = upperTree.AddPath(value, FileInfo{
Path: value,
TypeFlag: 1,
hash: 456,
})
if err != nil {
t.Errorf("could not setup test: %v", err)
}
}
chmodPath := "/etc/non-data-change"
lowerTree.AddPath(chmodPath, FileInfo{
_, _, err := lowerTree.AddPath(chmodPath, FileInfo{
Path: chmodPath,
TypeFlag: 1,
hash: 123,
Mode: 0,
})
if err != nil {
t.Errorf("could not setup test: %v", err)
}
upperTree.AddPath(chmodPath, FileInfo{
_, _, err = upperTree.AddPath(chmodPath, FileInfo{
Path: chmodPath,
TypeFlag: 1,
hash: 123,
Mode: 1,
})
if err != nil {
t.Errorf("could not setup test: %v", err)
}
changedPaths = append(changedPaths, chmodPath)
chownPath := "/etc/non-data-change-2"
lowerTree.AddPath(chmodPath, FileInfo{
_, _, err = lowerTree.AddPath(chmodPath, FileInfo{
Path: chownPath,
TypeFlag: 1,
hash: 123,
@ -391,8 +559,11 @@ func TestCompareWithChanges(t *testing.T) {
Gid: 0,
Uid: 0,
})
if err != nil {
t.Errorf("could not setup test: %v", err)
}
upperTree.AddPath(chmodPath, FileInfo{
_, _, err = upperTree.AddPath(chmodPath, FileInfo{
Path: chownPath,
TypeFlag: 1,
hash: 123,
@ -400,10 +571,17 @@ func TestCompareWithChanges(t *testing.T) {
Gid: 12,
Uid: 12,
})
if err != nil {
t.Errorf("could not setup test: %v", err)
}
changedPaths = append(changedPaths, chownPath)
lowerTree.Compare(upperTree)
err = lowerTree.CompareAndMark(upperTree)
if err != nil {
t.Errorf("unable to compare and mark: %+v", err)
}
failedAssertions := []error{}
asserter := func(n *FileNode) error {
p := n.Path()
@ -420,7 +598,7 @@ func TestCompareWithChanges(t *testing.T) {
}
return nil
}
err := lowerTree.VisitDepthChildFirst(asserter, nil)
err = lowerTree.VisitDepthChildFirst(asserter, nil)
if err != nil {
t.Errorf("Expected no errors when visiting nodes, got: %+v", err)
}
@ -446,7 +624,10 @@ func TestCompareWithRemoves(t *testing.T) {
TypeFlag: 1,
hash: 123,
}
lowerTree.AddPath(value, fakeData)
_, _, err := lowerTree.AddPath(value, fakeData)
if err != nil {
t.Errorf("could not setup test: %v", err)
}
}
for _, value := range upperPaths {
@ -455,10 +636,16 @@ func TestCompareWithRemoves(t *testing.T) {
TypeFlag: 1,
hash: 123,
}
upperTree.AddPath(value, fakeData)
_, _, err := upperTree.AddPath(value, fakeData)
if err != nil {
t.Errorf("could not setup test: %v", err)
}
}
lowerTree.Compare(upperTree)
err := lowerTree.CompareAndMark(upperTree)
if err != nil {
t.Errorf("could not setup test: %v", err)
}
failedAssertions := []error{}
asserter := func(n *FileNode) error {
p := n.Path()
@ -479,7 +666,7 @@ func TestCompareWithRemoves(t *testing.T) {
}
return nil
}
err := lowerTree.VisitDepthChildFirst(asserter, nil)
err = lowerTree.VisitDepthChildFirst(asserter, nil)
if err != nil {
t.Errorf("Expected no errors when visiting nodes, got: %+v", err)
}
@ -495,15 +682,39 @@ func TestCompareWithRemoves(t *testing.T) {
func TestStackRange(t *testing.T) {
tree := NewFileTree()
tree.AddPath("/etc/nginx/nginx.conf", FileInfo{})
tree.AddPath("/etc/nginx/public", FileInfo{})
tree.AddPath("/var/run/systemd", FileInfo{})
tree.AddPath("/var/run/bashful", FileInfo{})
tree.AddPath("/tmp", FileInfo{})
tree.AddPath("/tmp/nonsense", FileInfo{})
_, _, err := tree.AddPath("/etc/nginx/nginx.conf", FileInfo{})
if err != nil {
t.Errorf("could not setup test: %v", err)
}
_, _, err = tree.AddPath("/etc/nginx/public", FileInfo{})
if err != nil {
t.Errorf("could not setup test: %v", err)
}
_, _, err = tree.AddPath("/var/run/systemd", FileInfo{})
if err != nil {
t.Errorf("could not setup test: %v", err)
}
_, _, err = tree.AddPath("/var/run/bashful", FileInfo{})
if err != nil {
t.Errorf("could not setup test: %v", err)
}
_, _, err = tree.AddPath("/tmp", FileInfo{})
if err != nil {
t.Errorf("could not setup test: %v", err)
}
_, _, err = tree.AddPath("/tmp/nonsense", FileInfo{})
if err != nil {
t.Errorf("could not setup test: %v", err)
}
tree.RemovePath("/var/run/bashful")
tree.RemovePath("/tmp")
err = tree.RemovePath("/var/run/bashful")
if err != nil {
t.Errorf("could not setup test: %v", err)
}
err = tree.RemovePath("/tmp")
if err != nil {
t.Errorf("could not setup test: %v", err)
}
lowerTree := NewFileTree()
upperTree := NewFileTree()
@ -516,7 +727,10 @@ func TestStackRange(t *testing.T) {
TypeFlag: 1,
hash: 123,
}
lowerTree.AddPath(value, fakeData)
_, _, err = lowerTree.AddPath(value, fakeData)
if err != nil {
t.Errorf("could not setup test: %v", err)
}
}
for _, value := range upperPaths {
@ -525,7 +739,10 @@ func TestStackRange(t *testing.T) {
TypeFlag: 1,
hash: 456,
}
upperTree.AddPath(value, fakeData)
_, _, err = upperTree.AddPath(value, fakeData)
if err != nil {
t.Errorf("could not setup test: %v", err)
}
}
trees := []*FileTree{lowerTree, upperTree, tree}
StackTreeRange(trees, 0, 2)
@ -548,12 +765,18 @@ func TestRemoveOnIterate(t *testing.T) {
}
}
tree.VisitDepthChildFirst(func(node *FileNode) error {
err := tree.VisitDepthChildFirst(func(node *FileNode) error {
if node.Data.ViewInfo.Hidden {
tree.RemovePath(node.Path())
err := tree.RemovePath(node.Path())
if err != nil {
t.Errorf("could not setup test: %v", err)
}
}
return nil
}, nil)
if err != nil {
t.Errorf("could not setup test: %v", err)
}
expected :=
` usr

16
go.mod
View file

@ -2,41 +2,35 @@ module github.com/wagoodman/dive
require (
github.com/Azure/go-ansiterm v0.0.0-20170929234023-d6e3b3328b78 // indirect
github.com/BurntSushi/toml v0.3.1 // indirect
github.com/Microsoft/go-winio v0.4.11 // indirect
github.com/Nvveen/Gotty v0.0.0-20120604004816-cd527374f1e5 // indirect
github.com/cespare/xxhash v1.1.0
github.com/docker/cli v0.0.0-20190303104010-8ddde26af67f
github.com/docker/distribution v0.0.0-20181126153310-93e082742a009850ac46962150b2f652a822c5ff // indirect
github.com/docker/docker v0.0.0-20181126153310-0b7cb16dde4a20d024c7be59801d63bcfd18611b
github.com/docker/go-connections v0.4.0 // indirect
github.com/docker/go-units v0.3.3 // indirect
github.com/dustin/go-humanize v1.0.0
github.com/fatih/color v1.7.0
github.com/gogo/protobuf v1.1.1 // indirect
github.com/google/go-cmp v0.2.0 // indirect
github.com/golangci/golangci-lint v1.17.1 // indirect
github.com/google/uuid v1.1.0
github.com/gorilla/context v1.1.1 // indirect
github.com/gorilla/mux v1.6.2 // indirect
github.com/inconshreveable/mousetrap v1.0.0 // indirect
github.com/jroimartin/gocui v0.4.0
github.com/k0kubun/go-ansi v0.0.0-20180517002512-3bf9e2903213
github.com/k0kubun/go-ansi v0.0.0-20180517002512-3bf9e2903213 // indirect
github.com/logrusorgru/aurora v0.0.0-20181002194514-a7b3b318ed4e
github.com/lunixbochs/vtclean v0.0.0-20180621232353-2d01aacdc34a
github.com/mattn/go-colorable v0.0.9 // indirect
github.com/mattn/go-isatty v0.0.4 // indirect
github.com/mitchellh/go-homedir v1.0.0
github.com/opencontainers/go-digest v1.0.0-rc1 // indirect
github.com/opencontainers/image-spec v1.0.1 // indirect
github.com/phayes/permbits v0.0.0-20180830030258-59f2482cd460
github.com/pkg/errors v0.8.0 // indirect
github.com/sergi/go-diff v1.0.0
github.com/sirupsen/logrus v1.2.0
github.com/spf13/cobra v0.0.3
github.com/spf13/viper v1.2.1
github.com/wagoodman/keybinding v0.0.0-20181213133715-6a824da6df05
golang.org/x/crypto v0.0.0-20181112202954-3d3f9f413869 // indirect
golang.org/x/net v0.0.0-20181114220301-adae6a3d119a
golang.org/x/sync v0.0.0-20181108010431-42b317875d0f // indirect
golang.org/x/sys v0.0.0-20181116161606-93218def8b18 // indirect
golang.org/x/net v0.0.0-20190313220215-9f648a60d977
golang.org/x/time v0.0.0-20181108054448-85acf8d2951c // indirect
google.golang.org/grpc v1.16.0 // indirect
gotest.tools v2.2.0+incompatible // indirect

164
go.sum
View file

@ -1,13 +1,20 @@
cloud.google.com/go v0.26.0/go.mod h1:aQUYkXzVsufM+DwF1aE+0xfcU+56JwCaLick0ClmMTw=
github.com/Azure/go-ansiterm v0.0.0-20170929234023-d6e3b3328b78/go.mod h1:LmzpDX56iTiv29bbRTIsUNlaFfuhWRQBWjQdVyAevI8=
github.com/BurntSushi/toml v0.3.1 h1:WXkYYl6Yr3qBf1K79EBnL4mak0OimBfB0XUf9Vl28OQ=
github.com/BurntSushi/toml v0.3.1/go.mod h1:xHWCNGjB5oqiDr8zfno3MHue2Ht5sIBksp03qcyfWMU=
github.com/Microsoft/go-winio v0.4.11 h1:zoIOcVf0xPN1tnMVbTtEdI+P8OofVk3NObnwOQ6nK2Q=
github.com/Microsoft/go-winio v0.4.11/go.mod h1:VhR8bwka0BXejwEJY73c50VrPtXAaKcyvVC4A4RozmA=
github.com/Nvveen/Gotty v0.0.0-20120604004816-cd527374f1e5/go.mod h1:lmUJ/7eu/Q8D7ML55dXQrVaamCz2vxCfdQBasLZfHKk=
github.com/OneOfOne/xxhash v1.2.2/go.mod h1:HSdplMjZKSmBqAxg5vPj2TmRDmfkzw+cTzAElWljhcU=
github.com/OpenPeeDeeP/depguard v0.0.0-20180806142446-a69c782687b2 h1:HTOmFEEYrWi4MW5ZKUx6xfeyM10Sx3kQF65xiQJMPYA=
github.com/OpenPeeDeeP/depguard v0.0.0-20180806142446-a69c782687b2/go.mod h1:7/4sitnI9YlQgTLLk734QlzXT8DuHVnAyztLplQjk+o=
github.com/StackExchange/wmi v0.0.0-20180116203802-5d049714c4a6/go.mod h1:3eOhrUMpNV+6aFIbp5/iudMxNCF27Vw2OZgy4xEx0Fg=
github.com/cespare/xxhash v1.1.0 h1:a6HrQnmkObjyL+Gs60czilIUGqrzKutQD6XZog3p+ko=
github.com/cespare/xxhash v1.1.0/go.mod h1:XrSqR1VqqWfGrhpAt58auRo0WTKS1nRRg3ghfAqPWnc=
github.com/client9/misspell v0.3.4/go.mod h1:qj6jICC3Q7zFZvVWo7KLAzC3yx5G7kyvSDkc90ppPyw=
github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
github.com/docker/cli v0.0.0-20190303104010-8ddde26af67f h1:avQULAD2yl7pQnDbjIxI/H5cwpRn8H/NeMNIbhnDtfI=
github.com/docker/cli v0.0.0-20190303104010-8ddde26af67f/go.mod h1:JLrzqnKDaYBop7H2jaqPtU4hHvMKP+vjCwu2uszcLI8=
github.com/docker/distribution v0.0.0-20181126153310-93e082742a009850ac46962150b2f652a822c5ff h1:dtgxyWsA/HV5EbB2FO4YKoCKEjLwr8SXZQCGjA9mRx4=
github.com/docker/distribution v0.0.0-20181126153310-93e082742a009850ac46962150b2f652a822c5ff/go.mod h1:J2gT2udsDAN96Uj4KfcMRqY0/ypR+oyYUYmja8H+y+w=
github.com/docker/docker v0.0.0-20181126153310-0b7cb16dde4a20d024c7be59801d63bcfd18611b h1:T6w2S9UvmzJQBKIUsOs3+lNWfZea5yUPLh2eMihRrKY=
@ -18,52 +25,145 @@ github.com/docker/go-units v0.3.3 h1:Xk8S3Xj5sLGlG5g67hJmYMmUgXv5N4PhkjJHHqrwnTk
github.com/docker/go-units v0.3.3/go.mod h1:fgPhTUdO+D/Jk86RDLlptpiXQzgHJF7gydDDbaIK4Dk=
github.com/dustin/go-humanize v1.0.0 h1:VSnTsYCnlFHaM2/igO1h6X3HA71jcobQuxemgkq4zYo=
github.com/dustin/go-humanize v1.0.0/go.mod h1:HtrtbFcZ19U5GC7JDqmcUSB87Iq5E25KnS6fMYU6eOk=
github.com/fatih/color v1.6.0/go.mod h1:Zm6kSWBoL9eyXnKyktHP6abPY2pDugNf5KwzbycvMj4=
github.com/fatih/color v1.7.0 h1:DkWD4oS2D8LGGgTQ6IvwJJXSL5Vp2ffcQg58nFV38Ys=
github.com/fatih/color v1.7.0/go.mod h1:Zm6kSWBoL9eyXnKyktHP6abPY2pDugNf5KwzbycvMj4=
github.com/fsnotify/fsnotify v1.4.7 h1:IXs+QLmnXW2CcXuY+8Mzv/fWEsPGWxqefPtCP5CnV9I=
github.com/fsnotify/fsnotify v1.4.7/go.mod h1:jwhsz4b93w/PPRr/qN1Yymfu8t87LnFCMoQvtojpjFo=
github.com/go-critic/go-critic v0.0.0-20181204210945-1df300866540 h1:7CU1IXBpPvxpQ/NqJrpuMXMHAw+FB2vfqtRF8tgW9fw=
github.com/go-critic/go-critic v0.0.0-20181204210945-1df300866540/go.mod h1:+sE8vrLDS2M0pZkBk0wy6+nLdKexVDrl/jBqQOTDThA=
github.com/go-lintpack/lintpack v0.5.2 h1:DI5mA3+eKdWeJ40nU4d6Wc26qmdG8RCi/btYq0TuRN0=
github.com/go-lintpack/lintpack v0.5.2/go.mod h1:NwZuYi2nUHho8XEIZ6SIxihrnPoqBTDqfpXvXAN0sXM=
github.com/go-ole/go-ole v1.2.1/go.mod h1:7FAglXiTm7HKlQRDeOQ6ZNUHidzCWXuZWq/1dTyBNF8=
github.com/go-toolsmith/astcast v1.0.0 h1:JojxlmI6STnFVG9yOImLeGREv8W2ocNUM+iOhR6jE7g=
github.com/go-toolsmith/astcast v1.0.0/go.mod h1:mt2OdQTeAQcY4DQgPSArJjHCcOwlX+Wl/kwN+LbLGQ4=
github.com/go-toolsmith/astcopy v1.0.0 h1:OMgl1b1MEpjFQ1m5ztEO06rz5CUd3oBv9RF7+DyvdG8=
github.com/go-toolsmith/astcopy v1.0.0/go.mod h1:vrgyG+5Bxrnz4MZWPF+pI4R8h3qKRjjyvV/DSez4WVQ=
github.com/go-toolsmith/astequal v0.0.0-20180903214952-dcb477bfacd6/go.mod h1:H+xSiq0+LtiDC11+h1G32h7Of5O3CYFJ99GVbS5lDKY=
github.com/go-toolsmith/astequal v1.0.0 h1:4zxD8j3JRFNyLN46lodQuqz3xdKSrur7U/sr0SDS/gQ=
github.com/go-toolsmith/astequal v1.0.0/go.mod h1:H+xSiq0+LtiDC11+h1G32h7Of5O3CYFJ99GVbS5lDKY=
github.com/go-toolsmith/astfmt v0.0.0-20180903215011-8f8ee99c3086/go.mod h1:mP93XdblcopXwlyN4X4uodxXQhldPGZbcEJIimQHrkg=
github.com/go-toolsmith/astfmt v1.0.0 h1:A0vDDXt+vsvLEdbMFJAUBI/uTbRw1ffOPnxsILnFL6k=
github.com/go-toolsmith/astfmt v1.0.0/go.mod h1:cnWmsOAuq4jJY6Ct5YWlVLmcmLMn1JUPuQIHCY7CJDw=
github.com/go-toolsmith/astinfo v0.0.0-20180906194353-9809ff7efb21/go.mod h1:dDStQCHtmZpYOmjRP/8gHHnCCch3Zz3oEgCdZVdtweU=
github.com/go-toolsmith/astp v0.0.0-20180903215135-0af7e3c24f30/go.mod h1:SV2ur98SGypH1UjcPpCatrV5hPazG6+IfNHbkDXBRrk=
github.com/go-toolsmith/astp v1.0.0 h1:alXE75TXgcmupDsMK1fRAy0YUzLzqPVvBKoyWV+KPXg=
github.com/go-toolsmith/astp v1.0.0/go.mod h1:RSyrtpVlfTFGDYRbrjyWP1pYu//tSFcvdYrA8meBmLI=
github.com/go-toolsmith/pkgload v0.0.0-20181119091011-e9e65178eee8/go.mod h1:WoMrjiy4zvdS+Bg6z9jZH82QXwkcgCBX6nOfnmdaHks=
github.com/go-toolsmith/pkgload v1.0.0/go.mod h1:5eFArkbO80v7Z0kdngIxsRXRMTaX4Ilcwuh3clNrQJc=
github.com/go-toolsmith/strparse v1.0.0 h1:Vcw78DnpCAKlM20kSbAyO4mPfJn/lyYA4BJUDxe2Jb4=
github.com/go-toolsmith/strparse v1.0.0/go.mod h1:YI2nUKP9YGZnL/L1/DLFBfixrcjslWct4wyljWhSRy8=
github.com/go-toolsmith/typep v1.0.0 h1:zKymWyA1TRYvqYrYDrfEMZULyrhcnGY3x7LDKU2XQaA=
github.com/go-toolsmith/typep v1.0.0/go.mod h1:JSQCQMUPdRlMZFswiq3TGpNp1GMktqkR2Ns5AIQkATU=
github.com/gobwas/glob v0.2.3 h1:A4xDbljILXROh+kObIiy5kIaPYD8e96x1tgBhUI5J+Y=
github.com/gobwas/glob v0.2.3/go.mod h1:d3Ez4x06l9bZtSvzIay5+Yzi0fmZzPgnTbPcKjJAkT8=
github.com/gogo/protobuf v1.1.1 h1:72R+M5VuhED/KujmZVcIquuo8mBgX4oVda//DQb3PXo=
github.com/gogo/protobuf v1.1.1/go.mod h1:r8qH/GZQm5c6nD/R0oafs1akxWv10x8SbQlK7atdtwQ=
github.com/golang/glog v0.0.0-20160126235308-23def4e6c14b/go.mod h1:SBH7ygxi8pfUlaOkMMuAQtPIUF8ecWP5IEl/CR7VP2Q=
github.com/golang/lint v0.0.0-20180702182130-06c8688daad7/go.mod h1:tluoj9z5200jBnyusfRPU2LqT6J+DAorxEvtC7LHB+E=
github.com/golang/mock v1.0.0/go.mod h1:oTYuIxOrZwtPieC+H1uAHpcLFnEyAGVDL/k47Jfbm0A=
github.com/golang/mock v1.1.1 h1:G5FRp8JnTd7RQH5kemVNlMeyXQAztQ3mOWV95KxsXH8=
github.com/golang/mock v1.1.1/go.mod h1:oTYuIxOrZwtPieC+H1uAHpcLFnEyAGVDL/k47Jfbm0A=
github.com/golang/protobuf v1.2.0/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U=
github.com/golangci/check v0.0.0-20180506172741-cfe4005ccda2 h1:23T5iq8rbUYlhpt5DB4XJkc6BU31uODLD1o1gKvZmD0=
github.com/golangci/check v0.0.0-20180506172741-cfe4005ccda2/go.mod h1:k9Qvh+8juN+UKMCS/3jFtGICgW8O96FVaZsaxdzDkR4=
github.com/golangci/dupl v0.0.0-20180902072040-3e9179ac440a h1:w8hkcTqaFpzKqonE9uMCefW1WDie15eSP/4MssdenaM=
github.com/golangci/dupl v0.0.0-20180902072040-3e9179ac440a/go.mod h1:ryS0uhF+x9jgbj/N71xsEqODy9BN81/GonCZiOzirOk=
github.com/golangci/errcheck v0.0.0-20181003203344-ef45e06d44b6 h1:i2jIkQFb8RG45DuQs+ElyROY848cSJIoIkBM+7XXypA=
github.com/golangci/errcheck v0.0.0-20181003203344-ef45e06d44b6/go.mod h1:DbHgvLiFKX1Sh2T1w8Q/h4NAI8MHIpzCdnBUDTXU3I0=
github.com/golangci/go-misc v0.0.0-20180628070357-927a3d87b613 h1:9kfjN3AdxcbsZBf8NjltjWihK2QfBBBZuv91cMFfDHw=
github.com/golangci/go-misc v0.0.0-20180628070357-927a3d87b613/go.mod h1:SyvUF2NxV+sN8upjjeVYr5W7tyxaT1JVtvhKhOn2ii8=
github.com/golangci/go-tools v0.0.0-20180109140146-af6baa5dc196 h1:9rtVlONXLF1rJZzvLt4tfOXtnAFUEhxCJ64Ibzj6ECo=
github.com/golangci/go-tools v0.0.0-20180109140146-af6baa5dc196/go.mod h1:unzUULGw35sjyOYjUt0jMTXqHlZPpPc6e+xfO4cd6mM=
github.com/golangci/goconst v0.0.0-20180610141641-041c5f2b40f3 h1:pe9JHs3cHHDQgOFXJJdYkK6fLz2PWyYtP4hthoCMvs8=
github.com/golangci/goconst v0.0.0-20180610141641-041c5f2b40f3/go.mod h1:JXrF4TWy4tXYn62/9x8Wm/K/dm06p8tCKwFRDPZG/1o=
github.com/golangci/gocyclo v0.0.0-20180528134321-2becd97e67ee h1:J2XAy40+7yz70uaOiMbNnluTg7gyQhtGqLQncQh+4J8=
github.com/golangci/gocyclo v0.0.0-20180528134321-2becd97e67ee/go.mod h1:ozx7R9SIwqmqf5pRP90DhR2Oay2UIjGuKheCBCNwAYU=
github.com/golangci/gofmt v0.0.0-20181105071733-0b8337e80d98 h1:ir6/L2ZOJfFrJlOTsuf/hlzdPuUwXV/VzkSlgS6f1vs=
github.com/golangci/gofmt v0.0.0-20181105071733-0b8337e80d98/go.mod h1:9qCChq59u/eW8im404Q2WWTrnBUQKjpNYKMbU4M7EFU=
github.com/golangci/golangci-lint v1.17.1 h1:lc8Hf9GPCjIr0hg3S/xhvFT1+Hydass8F1xchr8jkME=
github.com/golangci/golangci-lint v1.17.1/go.mod h1:+5sJSl2h3aly+fpmL2meSP8CaSKua2E4Twi9LPy7b1g=
github.com/golangci/gosec v0.0.0-20180901114220-66fb7fc33547 h1:qMomh8bv+kDazm1dSLZ9S3zZ2PJZMHL4ilfBjxFOlmI=
github.com/golangci/gosec v0.0.0-20180901114220-66fb7fc33547/go.mod h1:0qUabqiIQgfmlAmulqxyiGkkyF6/tOGSnY2cnPVwrzU=
github.com/golangci/ineffassign v0.0.0-20180808204949-42439a7714cc h1:XRFao922N8F3EcIXBSNX8Iywk+GI0dxD/8FicMX2D/c=
github.com/golangci/ineffassign v0.0.0-20180808204949-42439a7714cc/go.mod h1:e5tpTHCfVze+7EpLEozzMB3eafxo2KT5veNg1k6byQU=
github.com/golangci/lint-1 v0.0.0-20180610141402-ee948d087217 h1:r7vyX+SN24x6+5AnpnrRn/bdwBb7U+McZqCHOVtXDuk=
github.com/golangci/lint-1 v0.0.0-20180610141402-ee948d087217/go.mod h1:66R6K6P6VWk9I95jvqGxkqJxVWGFy9XlDwLwVz1RCFg=
github.com/golangci/maligned v0.0.0-20180506175553-b1d89398deca h1:kNY3/svz5T29MYHubXix4aDDuE3RWHkPvopM/EDv/MA=
github.com/golangci/maligned v0.0.0-20180506175553-b1d89398deca/go.mod h1:tvlJhZqDe4LMs4ZHD0oMUlt9G2LWuDGoisJTBzLMV9o=
github.com/golangci/misspell v0.0.0-20180809174111-950f5d19e770 h1:EL/O5HGrF7Jaq0yNhBLucz9hTuRzj2LdwGBOaENgxIk=
github.com/golangci/misspell v0.0.0-20180809174111-950f5d19e770/go.mod h1:dEbvlSfYbMQDtrpRMQU675gSDLDNa8sCPPChZ7PhiVA=
github.com/golangci/prealloc v0.0.0-20180630174525-215b22d4de21 h1:leSNB7iYzLYSSx3J/s5sVf4Drkc68W2wm4Ixh/mr0us=
github.com/golangci/prealloc v0.0.0-20180630174525-215b22d4de21/go.mod h1:tf5+bzsHdTM0bsB7+8mt0GUMvjCgwLpTapNZHU8AajI=
github.com/golangci/revgrep v0.0.0-20180526074752-d9c87f5ffaf0 h1:HVfrLniijszjS1aiNg8JbBMO2+E1WIQ+j/gL4SQqGPg=
github.com/golangci/revgrep v0.0.0-20180526074752-d9c87f5ffaf0/go.mod h1:qOQCunEYvmd/TLamH+7LlVccLvUH5kZNhbCgTHoBbp4=
github.com/golangci/unconvert v0.0.0-20180507085042-28b1c447d1f4 h1:zwtduBRr5SSWhqsYNgcuWO2kFlpdOZbP0+yRjmvPGys=
github.com/golangci/unconvert v0.0.0-20180507085042-28b1c447d1f4/go.mod h1:Izgrg8RkN3rCIMLGE9CyYmU9pY2Jer6DgANEnZ/L/cQ=
github.com/google/go-cmp v0.2.0/go.mod h1:oXzfMopK8JAjlY9xF4vHSVASa0yLyX7SntLO5aqRK0M=
github.com/google/uuid v1.1.0 h1:Jf4mxPC/ziBnoPIdpQdPJ9OeiomAUHLvxmPRSPH9m4s=
github.com/google/uuid v1.1.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=
github.com/gorilla/context v1.1.1/go.mod h1:kBGZzfjB9CEq2AlWe17Uuf7NDRt0dE0s8S51q0aT7Yg=
github.com/gorilla/mux v1.6.2/go.mod h1:1lud6UwP+6orDFRuTfBEV8e9/aOM/c4fVVCaMa2zaAs=
github.com/gostaticanalysis/analysisutil v0.0.0-20190318220348-4088753ea4d3 h1:JVnpOZS+qxli+rgVl98ILOXVNbW+kb5wcxeGx8ShUIw=
github.com/gostaticanalysis/analysisutil v0.0.0-20190318220348-4088753ea4d3/go.mod h1:eEOZF4jCKGi+aprrirO9e7WKB3beBRtWgqGunKl6pKE=
github.com/hashicorp/hcl v0.0.0-20180404174102-ef8a98b0bbce/go.mod h1:oZtUIOe8dh44I2q6ScRibXws4Ajl+d+nod3AaR9vL5w=
github.com/hashicorp/hcl v1.0.0 h1:0Anlzjpi4vEasTeNFn2mLJgTSwt0+6sfsiTG8qcWGx4=
github.com/hashicorp/hcl v1.0.0/go.mod h1:E5yfLk+7swimpb2L/Alb/PJmXilQ/rhwaUYs4T20WEQ=
github.com/hpcloud/tail v1.0.0/go.mod h1:ab1qPbhIpdTxEkNHXyeSf5vhxWSCs/tWer42PpOxQnU=
github.com/inconshreveable/mousetrap v1.0.0 h1:Z8tu5sraLXCXIcARxBp/8cbvlwVa7Z1NHg9XEKhtSvM=
github.com/inconshreveable/mousetrap v1.0.0/go.mod h1:PxqpIevigyE2G7u3NXJIT2ANytuPF1OarO4DADm73n8=
github.com/jroimartin/gocui v0.4.0 h1:52jnalstgmc25FmtGcWqa0tcbMEWS6RpFLsOIO+I+E8=
github.com/jroimartin/gocui v0.4.0/go.mod h1:7i7bbj99OgFHzo7kB2zPb8pXLqMBSQegY7azfqXMkyY=
github.com/k0kubun/go-ansi v0.0.0-20180517002512-3bf9e2903213 h1:qGQQKEcAR99REcMpsXCp3lJ03zYT1PkRd3kQGPn9GVg=
github.com/k0kubun/go-ansi v0.0.0-20180517002512-3bf9e2903213/go.mod h1:vNUNkEQ1e29fT/6vq2aBdFsgNPmy8qMdSay1npru+Sw=
github.com/kisielk/gotool v0.0.0-20161130080628-0de1eaf82fa3/go.mod h1:jxZFDH7ILpTPQTk+E2s+z4CUas9lVNjIuKR4c5/zKgM=
github.com/kisielk/gotool v1.0.0 h1:AV2c/EiW3KqPNT9ZKl07ehoAGi4C5/01Cfbblndcapg=
github.com/kisielk/gotool v1.0.0/go.mod h1:XhKaO+MFFWcvkIS/tQcRk01m1F5IRFswLeQ+oQHNcck=
github.com/klauspost/compress v1.4.0/go.mod h1:RyIbtBH6LamlWaDj8nUwkbUhJ87Yi3uG0guNDohfE1A=
github.com/klauspost/compress v1.4.1/go.mod h1:RyIbtBH6LamlWaDj8nUwkbUhJ87Yi3uG0guNDohfE1A=
github.com/klauspost/cpuid v0.0.0-20180405133222-e7e905edc00e/go.mod h1:Pj4uuM528wm8OyEC2QMXAi2YiTZ96dNQPGgoMS4s3ek=
github.com/klauspost/cpuid v1.2.0/go.mod h1:Pj4uuM528wm8OyEC2QMXAi2YiTZ96dNQPGgoMS4s3ek=
github.com/konsorten/go-windows-terminal-sequences v1.0.1 h1:mweAR1A6xJ3oS2pRaGiHgQ4OO8tzTaLawm8vnODuwDk=
github.com/konsorten/go-windows-terminal-sequences v1.0.1/go.mod h1:T0+1ngSBFLxvqU3pZ+m/2kptfBszLMUkC4ZK/EgS/cQ=
github.com/kr/pretty v0.1.0/go.mod h1:dAy3ld7l9f0ibDNOQOHHMYYIIbhfbHSm3C4ZsoJORNo=
github.com/kr/pty v1.1.1/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ=
github.com/kr/text v0.1.0/go.mod h1:4Jbv+DJW3UT/LiOwJeYQe1efqtUx/iVham/4vfdArNI=
github.com/logrusorgru/aurora v0.0.0-20181002194514-a7b3b318ed4e h1:9MlwzLdW7QSDrhDjFlsEYmxpFyIoXmYRon3dt0io31k=
github.com/logrusorgru/aurora v0.0.0-20181002194514-a7b3b318ed4e/go.mod h1:7rIyQOR62GCctdiQpZ/zOJlFyk6y+94wXzv6RNZgaR4=
github.com/lunixbochs/vtclean v0.0.0-20180621232353-2d01aacdc34a h1:weJVJJRzAJBFRlAiJQROKQs8oC9vOxvm4rZmBBk0ONw=
github.com/lunixbochs/vtclean v0.0.0-20180621232353-2d01aacdc34a/go.mod h1:pHhQNgMf3btfWnGBVipUOjRYhoOsdGqdm/+2c2E2WMI=
github.com/magiconair/properties v1.7.6/go.mod h1:PppfXfuXeibc/6YijjN8zIbojt8czPbwD3XqdrwzmxQ=
github.com/magiconair/properties v1.8.0 h1:LLgXmsheXeRoUOBOjtwPQCWIYqM/LU1ayDtDePerRcY=
github.com/magiconair/properties v1.8.0/go.mod h1:PppfXfuXeibc/6YijjN8zIbojt8czPbwD3XqdrwzmxQ=
github.com/mattn/go-colorable v0.0.9 h1:UVL0vNpWh04HeJXV0KLcaT7r06gOH2l4OW6ddYRUIY4=
github.com/mattn/go-colorable v0.0.9/go.mod h1:9vuHe8Xs5qXnSaW/c/ABM9alt+Vo+STaOChaDxuIBZU=
github.com/mattn/go-isatty v0.0.3/go.mod h1:M+lRXTBqGeGNdLjl/ufCoiOlB5xdOkqRJdNxMWT7Zi4=
github.com/mattn/go-isatty v0.0.4 h1:bnP0vzxcAdeI1zdubAl5PjU6zsERjGZb7raWodagDYs=
github.com/mattn/go-isatty v0.0.4/go.mod h1:M+lRXTBqGeGNdLjl/ufCoiOlB5xdOkqRJdNxMWT7Zi4=
github.com/mattn/go-runewidth v0.0.3 h1:a+kO+98RDGEfo6asOGMmpodZq4FNtnGP54yps8BzLR4=
github.com/mattn/go-runewidth v0.0.3/go.mod h1:LwmH8dsx7+W8Uxz3IHJYH5QSwggIsqBzpuz5H//U1FU=
github.com/mattn/goveralls v0.0.2/go.mod h1:8d1ZMHsd7fW6IRPKQh46F2WRpyib5/X4FOpevwGNQEw=
github.com/mitchellh/go-homedir v1.0.0 h1:vKb8ShqSby24Yrqr/yDYkuFz8d0WUjys40rvnGC8aR0=
github.com/mitchellh/go-homedir v1.0.0/go.mod h1:SfyaCUpYCn1Vlf4IUYiD9fPX4A5wJrkLzIz1N1q0pr0=
github.com/mitchellh/go-ps v0.0.0-20170309133038-4fdf99ab2936/go.mod h1:r1VsdOzOPt1ZSrGZWFoNhsAedKnEd6r9Np1+5blZCWk=
github.com/mitchellh/mapstructure v0.0.0-20180220230111-00c29f56e238/go.mod h1:FVVH3fgwuzCH5S8UJGiWEs2h04kUh9fWfEaFds41c1Y=
github.com/mitchellh/mapstructure v1.0.0 h1:vVpGvMXJPqSDh2VYHF7gsfQj8Ncx+Xw5Y1KHeTRY+7I=
github.com/mitchellh/mapstructure v1.0.0/go.mod h1:FVVH3fgwuzCH5S8UJGiWEs2h04kUh9fWfEaFds41c1Y=
github.com/mozilla/tls-observatory v0.0.0-20180409132520-8791a200eb40/go.mod h1:SrKMQvPiws7F7iqYp8/TX+IhxCYhzr6N/1yb8cwHsGk=
github.com/nbutton23/zxcvbn-go v0.0.0-20160627004424-a22cb81b2ecd/go.mod h1:o96djdrsSGy3AWPyBgZMAGfxZNfgntdJG+11KU4QvbU=
github.com/nbutton23/zxcvbn-go v0.0.0-20171102151520-eafdab6b0663 h1:Ri1EhipkbhWsffPJ3IPlrb4SkTOPa2PfRXp3jchBczw=
github.com/nbutton23/zxcvbn-go v0.0.0-20171102151520-eafdab6b0663/go.mod h1:o96djdrsSGy3AWPyBgZMAGfxZNfgntdJG+11KU4QvbU=
github.com/nsf/termbox-go v0.0.0-20181027232701-60ab7e3d12ed h1:bAVGG6B+R5qpSylrrA+BAMrzYkdAoiTaKPVxRB+4cyM=
github.com/nsf/termbox-go v0.0.0-20181027232701-60ab7e3d12ed/go.mod h1:IuKpRQcYE1Tfu+oAQqaLisqDeXgjyyltCfsaoYN18NQ=
github.com/onsi/ginkgo v0.0.0-20170829012221-11459a886d9c/go.mod h1:lLunBs/Ym6LB5Z9jYTR76FiuTmxDTDusOGeTQH+WWjE=
github.com/onsi/ginkgo v1.6.0/go.mod h1:lLunBs/Ym6LB5Z9jYTR76FiuTmxDTDusOGeTQH+WWjE=
github.com/onsi/gomega v0.0.0-20170829124025-dcabb60a477c/go.mod h1:C1qb7wdrVGGVU+Z6iS04AVkA3Q65CEZX59MT0QO5uiA=
github.com/onsi/gomega v1.4.2/go.mod h1:ex+gbHU/CVuBBDIJjb2X0qEXbFg53c61hWP/1CpauHY=
github.com/opencontainers/go-digest v1.0.0-rc1 h1:WzifXhOVOEOuFYOJAW6aQqW0TooG2iki3E3Ii+WN7gQ=
github.com/opencontainers/go-digest v1.0.0-rc1/go.mod h1:cMLVZDEM3+U2I4VmLI6N8jQYUd2OVphdqWwCJHrFt2s=
github.com/opencontainers/image-spec v1.0.1 h1:JMemWkRwHx4Zj+fVxWoMCFm/8sYGGrUVojFA6h/TRcI=
github.com/opencontainers/image-spec v1.0.1/go.mod h1:BtxoFyWECRxE4U/7sNtV5W15zMzWCbyJoFRP3s7yZA0=
github.com/pelletier/go-toml v1.1.0/go.mod h1:5z9KED0ma1S8pY6P1sdut58dfprrGBbd/94hg7ilaic=
github.com/pelletier/go-toml v1.2.0 h1:T5zMGML61Wp+FlcbWjRDT7yAxhJNAiPPLOFECq181zc=
github.com/pelletier/go-toml v1.2.0/go.mod h1:5z9KED0ma1S8pY6P1sdut58dfprrGBbd/94hg7ilaic=
github.com/phayes/permbits v0.0.0-20180830030258-59f2482cd460 h1:B9xJsGjeteSbA5LYAmW9KF9/jQcmrJkmpgVWsqRxc0k=
@ -71,49 +171,113 @@ github.com/phayes/permbits v0.0.0-20180830030258-59f2482cd460/go.mod h1:3uODdxMg
github.com/pkg/errors v0.8.0 h1:WdK/asTD0HN+q6hsWO3/vpuAkAr+tw6aNJNDFFf0+qw=
github.com/pkg/errors v0.8.0/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0=
github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
github.com/quasilyte/go-consistent v0.0.0-20190521200055-c6f3937de18c/go.mod h1:5STLWrekHfjyYwxBRVRXNOSewLJ3PWfDJd1VyTS21fI=
github.com/rogpeppe/go-internal v1.1.0/go.mod h1:M8bDsm7K2OlrFYOpmOWEs/qY81heoFRclV5y23lUDJ4=
github.com/ryanuber/go-glob v0.0.0-20170128012129-256dc444b735/go.mod h1:807d1WSdnB0XRJzKNil9Om6lcp/3a0v4qIHxIXzX/Yc=
github.com/sergi/go-diff v1.0.0 h1:Kpca3qRNrduNnOQeazBd0ysaKrUJiIuISHxogkT9RPQ=
github.com/sergi/go-diff v1.0.0/go.mod h1:0CfEIISq7TuYL3j771MWULgwwjU+GofnZX9QAmXWZgo=
github.com/shirou/gopsutil v0.0.0-20180427012116-c95755e4bcd7/go.mod h1:5b4v6he4MtMOwMlS0TUMTu2PcXUg8+E1lC7eC3UO/RA=
github.com/shirou/w32 v0.0.0-20160930032740-bb4de0191aa4/go.mod h1:qsXQc7+bwAM3Q1u/4XEfrquwF8Lw7D7y5cD8CuHnfIc=
github.com/shurcooL/go v0.0.0-20180423040247-9e1955d9fb6e/go.mod h1:TDJrrUr11Vxrven61rcy3hJMUqaf/CLWYhHNPmT14Lk=
github.com/shurcooL/go-goon v0.0.0-20170922171312-37c2f522c041/go.mod h1:N5mDOmsrJOB+vfqUK+7DmDyjhSLIIBnXo9lvZJj3MWQ=
github.com/sirupsen/logrus v1.0.5/go.mod h1:pMByvHTf9Beacp5x1UXfOR9xyW/9antXMhjMPG0dEzc=
github.com/sirupsen/logrus v1.2.0 h1:juTguoYk5qI21pwyTXY3B3Y5cOTH3ZUyZCg1v/mihuo=
github.com/sirupsen/logrus v1.2.0/go.mod h1:LxeOpSwHxABJmUn/MG1IvRgCAasNZTLOkJPxbbu5VWo=
github.com/sourcegraph/go-diff v0.5.1 h1:gO6i5zugwzo1RVTvgvfwCOSVegNuvnNi6bAD1QCmkHs=
github.com/sourcegraph/go-diff v0.5.1/go.mod h1:j2dHj3m8aZgQO8lMTcTnBcXkRRRqi34cd2MNlA9u1mE=
github.com/spaolacci/murmur3 v0.0.0-20180118202830-f09979ecbc72/go.mod h1:JwIasOWyU6f++ZhiEuf87xNszmSA2myDM2Kzu9HwQUA=
github.com/spf13/afero v1.1.0/go.mod h1:j4pytiNVoe2o6bmDsKpLACNPDBIoEAkihy7loJ1B0CQ=
github.com/spf13/afero v1.1.2 h1:m8/z1t7/fwjysjQRYbP0RD+bUIF/8tJwPdEZsI83ACI=
github.com/spf13/afero v1.1.2/go.mod h1:j4pytiNVoe2o6bmDsKpLACNPDBIoEAkihy7loJ1B0CQ=
github.com/spf13/cast v1.2.0 h1:HHl1DSRbEQN2i8tJmtS6ViPyHx35+p51amrdsiTCrkg=
github.com/spf13/cast v1.2.0/go.mod h1:r2rcYCSwa1IExKTDiTfzaxqT2FNHs8hODu4LnUfgKEg=
github.com/spf13/cobra v0.0.2/go.mod h1:1l0Ry5zgKvJasoi3XT1TypsSe7PqH0Sj9dhYf7v3XqQ=
github.com/spf13/cobra v0.0.3 h1:ZlrZ4XsMRm04Fr5pSFxBgfND2EBVa1nLpiy1stUsX/8=
github.com/spf13/cobra v0.0.3/go.mod h1:1l0Ry5zgKvJasoi3XT1TypsSe7PqH0Sj9dhYf7v3XqQ=
github.com/spf13/jwalterweatherman v0.0.0-20180109140146-7c0cea34c8ec/go.mod h1:cQK4TGJAtQXfYWX+Ddv3mKDzgVb68N+wFjFa4jdeBTo=
github.com/spf13/jwalterweatherman v1.0.0 h1:XHEdyB+EcvlqZamSM4ZOMGlc93t6AcsBEu9Gc1vn7yk=
github.com/spf13/jwalterweatherman v1.0.0/go.mod h1:cQK4TGJAtQXfYWX+Ddv3mKDzgVb68N+wFjFa4jdeBTo=
github.com/spf13/pflag v1.0.1/go.mod h1:DYY7MBk1bdzusC3SYhjObp+wFpr4gzcvqqNjLnInEg4=
github.com/spf13/pflag v1.0.2 h1:Fy0orTDgHdbnzHcsOgfCN4LtHf0ec3wwtiwJqwvf3Gc=
github.com/spf13/pflag v1.0.2/go.mod h1:DYY7MBk1bdzusC3SYhjObp+wFpr4gzcvqqNjLnInEg4=
github.com/spf13/viper v1.0.2/go.mod h1:A8kyI5cUJhb8N+3pkfONlcEcZbueH6nhAm0Fq7SrnBM=
github.com/spf13/viper v1.2.1 h1:bIcUwXqLseLF3BDAZduuNfekWG87ibtFxi59Bq+oI9M=
github.com/spf13/viper v1.2.1/go.mod h1:P4AexN0a+C9tGAnUFNwDMYYZv3pjFuvmeiMyKRaNVlI=
github.com/stretchr/objx v0.1.1/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
github.com/stretchr/testify v1.2.2/go.mod h1:a8OnRcib4nhh0OaRAV+Yts87kKdq0PP7pXfy6kDkUVs=
github.com/timakin/bodyclose v0.0.0-20190407043127-4a873e97b2bb h1:lI9ufgFfvuqRctP9Ny8lDDLbSWCMxBPletcSqrnyFYM=
github.com/timakin/bodyclose v0.0.0-20190407043127-4a873e97b2bb/go.mod h1:Qimiffbc6q9tBWlVV6x0P9sat/ao1xEkREYPPj9hphk=
github.com/valyala/bytebufferpool v1.0.0/go.mod h1:6bBcMArwyJ5K/AmCkWv1jt77kVWyCJ6HpOuEn7z0Csc=
github.com/valyala/fasthttp v1.2.0/go.mod h1:4vX61m6KN+xDduDNwXrhIAVZaZaZiQ1luJk8LWSxF3s=
github.com/valyala/quicktemplate v1.1.1/go.mod h1:EH+4AkTd43SvgIbQHYu59/cJyxDoOVRUAfrukLPuGJ4=
github.com/valyala/tcplisten v0.0.0-20161114210144-ceec8f93295a/go.mod h1:v3UYOV9WzVtRmSR+PDvWpU/qWl4Wa5LApYYX4ZtKbio=
github.com/wagoodman/keybinding v0.0.0-20181213133715-6a824da6df05 h1:YMcRwVDe8DLDZ/vrhuImCfqjjG/+gZs6SF61DDQkL/8=
github.com/wagoodman/keybinding v0.0.0-20181213133715-6a824da6df05/go.mod h1:gXFkc2sM2o06uzn5Lgo6Ql76uweGdxNfeAlFyKiHAdk=
golang.org/x/crypto v0.0.0-20180904163835-0709b304e793/go.mod h1:6SG95UA2DQfeDnfUPMdvaQW0Q7yPrPDi9nlGo2tz2b4=
golang.org/x/crypto v0.0.0-20181112202954-3d3f9f413869 h1:kkXA53yGe04D0adEYJwEVQjeBppL01Exg+fnMjfUraU=
golang.org/x/crypto v0.0.0-20181112202954-3d3f9f413869/go.mod h1:6SG95UA2DQfeDnfUPMdvaQW0Q7yPrPDi9nlGo2tz2b4=
golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w=
golang.org/x/crypto v0.0.0-20190313024323-a1f597ede03a h1:YX8ljsm6wXlHZO+aRz9Exqr0evNhKRNe5K/gi+zKh4U=
golang.org/x/crypto v0.0.0-20190313024323-a1f597ede03a/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w=
golang.org/x/lint v0.0.0-20180702182130-06c8688daad7/go.mod h1:UVdnD1Gm6xHRNCYTkRU2/jEulfH38KcIWyp/GAMgvoE=
golang.org/x/net v0.0.0-20170915142106-8351a756f30f/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
golang.org/x/net v0.0.0-20180826012351-8a410e7b638d/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
golang.org/x/net v0.0.0-20180906233101-161cd47e91fd/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
golang.org/x/net v0.0.0-20180911220305-26e67e76b6c3/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
golang.org/x/net v0.0.0-20181114220301-adae6a3d119a h1:gOpx8G595UYyvj8UK4+OFyY4rx037g3fmfhe5SasG3U=
golang.org/x/net v0.0.0-20181114220301-adae6a3d119a/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
golang.org/x/net v0.0.0-20190311183353-d8887717615a/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg=
golang.org/x/net v0.0.0-20190313220215-9f648a60d977 h1:actzWV6iWn3GLqN8dZjzsB+CLt+gaV2+wsxroxiQI8I=
golang.org/x/net v0.0.0-20190313220215-9f648a60d977/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg=
golang.org/x/oauth2 v0.0.0-20180821212333-d2e6202438be/go.mod h1:N/0e6XlmueqKjAGxoOufVs8QHGRruUQn6yWY3a++T0U=
golang.org/x/sync v0.0.0-20180314180146-1d60e4601c6f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sync v0.0.0-20181108010431-42b317875d0f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sys v0.0.0-20171026204733-164713f0dfce/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
golang.org/x/sys v0.0.0-20180830151530-49385e6e1522/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
golang.org/x/sys v0.0.0-20180905080454-ebe1bf3edb33/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
golang.org/x/sys v0.0.0-20180906133057-8cf3aee42992/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
golang.org/x/sys v0.0.0-20180909124046-d0be0721c37e/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
golang.org/x/sys v0.0.0-20181116161606-93218def8b18 h1:Wh+XCfg3kNpjhdq2LXrsiOProjtQZKme5XUx7VcxwAw=
golang.org/x/sys v0.0.0-20181116161606-93218def8b18/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
golang.org/x/sys v0.0.0-20190312061237-fead79001313 h1:pczuHS43Cp2ktBEEmLwScxgjWsBSzdaQiKzUyf3DTTc=
golang.org/x/sys v0.0.0-20190312061237-fead79001313/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/text v0.0.0-20170915090833-1cbadb444a80/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
golang.org/x/text v0.3.0 h1:g61tztE5qeGQ89tm6NTjjM9VPIm088od1l6aSorWRWg=
golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
golang.org/x/time v0.0.0-20181108054448-85acf8d2951c/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ=
golang.org/x/tools v0.0.0-20170915040203-e531a2a1c15f/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
golang.org/x/tools v0.0.0-20180221164845-07fd8470d635/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
golang.org/x/tools v0.0.0-20180828015842-6cd1fcedba52/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
golang.org/x/tools v0.0.0-20181117154741-2ddaf7f79a09/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
golang.org/x/tools v0.0.0-20190110163146-51295c7ec13a/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
golang.org/x/tools v0.0.0-20190121143147-24cd39ecf745/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
golang.org/x/tools v0.0.0-20190311212946-11955173bddd/go.mod h1:LCzVGOaR6xXOjkQ3onu1FJEFr0SW1gC7cKk1uF8kGRs=
golang.org/x/tools v0.0.0-20190311215038-5c2858a9cfe5/go.mod h1:LCzVGOaR6xXOjkQ3onu1FJEFr0SW1gC7cKk1uF8kGRs=
golang.org/x/tools v0.0.0-20190322203728-c1a832b0ad89/go.mod h1:LCzVGOaR6xXOjkQ3onu1FJEFr0SW1gC7cKk1uF8kGRs=
golang.org/x/tools v0.0.0-20190521203540-521d6ed310dd h1:7E3PabyysDSEjnaANKBgums/hyvMI/HoHQ50qZEzTrg=
golang.org/x/tools v0.0.0-20190521203540-521d6ed310dd/go.mod h1:RgjU9mgBXZiqYHBnxXauZ1Gv1EHHAz9KjViQ78xBX0Q=
google.golang.org/appengine v1.1.0/go.mod h1:EbEs0AVv82hx2wNQdGPgUI5lhzA/G0D9YwlJXL52JkM=
google.golang.org/genproto v0.0.0-20180817151627-c66870c02cf8/go.mod h1:JiN7NxoALGmiZfu7CAH4rXhgtRTLTxftemlI0sWmxmc=
google.golang.org/grpc v1.16.0/go.mod h1:0JHn/cJsOMiMfNA9+DeHDlAU7KAAB5GDlYFpa9MZMio=
gopkg.in/airbrake/gobrake.v2 v2.0.9/go.mod h1:/h5ZAUhDkGaJfjzjKLSjv6zCL6O0LLBxU4K+aSYdM/U=
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
gopkg.in/check.v1 v1.0.0-20180628173108-788fd7840127/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
gopkg.in/errgo.v2 v2.1.0/go.mod h1:hNsd1EY+bozCKY1Ytp96fpM3vjJbqLJn88ws8XvfDNI=
gopkg.in/fsnotify.v1 v1.4.7/go.mod h1:Tz8NjZHkW78fSQdbUxIjBTcgA1z1m8ZHf0WmKUhAMys=
gopkg.in/gemnasium/logrus-airbrake-hook.v2 v2.1.2/go.mod h1:Xk6kEKp8OKb+X14hQBKWaSkCsqBpgog8nAV2xsGOxlo=
gopkg.in/tomb.v1 v1.0.0-20141024135613-dd632973f1e7/go.mod h1:dt/ZhP58zS4L8KSrWDmTeBkI65Dw0HsyUHuEVlX15mw=
gopkg.in/yaml.v2 v2.0.0-20170812160011-eb3733d160e7/go.mod h1:JAlM8MvJe8wmxCU4Bli9HhUf9+ttbYbLASfIpnQbh74=
gopkg.in/yaml.v2 v2.2.1 h1:mUhvW9EsL+naU5Q3cakzfE91YhliOondGd6ZrsDBHQE=
gopkg.in/yaml.v2 v2.2.1/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
gotest.tools v2.2.0+incompatible/go.mod h1:DsYFclhRJ6vuDpmuTbkuFWG+y2sxOXAzmJt81HFBacw=
honnef.co/go/tools v0.0.0-20180728063816-88497007e858/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4=
mvdan.cc/interfacer v0.0.0-20180901003855-c20040233aed h1:WX1yoOaKQfddO/mLzdV4wptyWgoH/6hwLs7QHTixo0I=
mvdan.cc/interfacer v0.0.0-20180901003855-c20040233aed/go.mod h1:Xkxe497xwlCKkIaQYRfC7CSLworTXY9RMqwhhCm+8Nc=
mvdan.cc/lint v0.0.0-20170908181259-adc824a0674b h1:DxJ5nJdkhDlLok9K6qO+5290kphDJbHOQO1DFFFTeBo=
mvdan.cc/lint v0.0.0-20170908181259-adc824a0674b/go.mod h1:2odslEg/xrtNQqCYg2/jCoyKnw3vv5biOc3JnIcYfL4=
mvdan.cc/unparam v0.0.0-20190124213536-fbb59629db34 h1:B1LAOfRqg2QUyCdzfjf46quTSYUTAK5OCwbh6pljHbM=
mvdan.cc/unparam v0.0.0-20190124213536-fbb59629db34/go.mod h1:H6SUd1XjIs+qQCyskXg5OFSrilMRUkD8ePJpHKDPaeY=
sourcegraph.com/sqs/pbtypes v0.0.0-20180604144634-d3ebe8f20ae4 h1:JPJh2pk3+X4lXAkZIk2RuE/7/FoK9maXw+TNPJhVS/c=
sourcegraph.com/sqs/pbtypes v0.0.0-20180604144634-d3ebe8f20ae4/go.mod h1:ketZ/q3QxT9HOBeFhu6RdvsftgpsbFHBF5Cas6cDKZ0=

View file

@ -4,14 +4,18 @@ import (
"archive/tar"
"encoding/json"
"fmt"
"io"
"io/ioutil"
"net/http"
"os"
"strings"
"github.com/docker/cli/cli/connhelper"
"github.com/docker/docker/client"
"github.com/sirupsen/logrus"
"github.com/wagoodman/dive/filetree"
"github.com/wagoodman/dive/utils"
"golang.org/x/net/context"
"io"
"io/ioutil"
"strings"
)
var dockerVersion string
@ -59,7 +63,33 @@ func (image *dockerImageAnalyzer) Fetch() (io.ReadCloser, error) {
// pull the image if it does not exist
ctx := context.Background()
image.client, err = client.NewClientWithOpts(client.WithVersion(dockerVersion), client.FromEnv)
host := os.Getenv("DOCKER_HOST")
var clientOpts []func(*client.Client) error
switch strings.Split(host, ":")[0] {
case "ssh":
helper, err := connhelper.GetConnectionHelper(host)
if err != nil {
fmt.Println("docker host", err)
}
clientOpts = append(clientOpts, func(c *client.Client) error {
httpClient := &http.Client{
Transport: &http.Transport{
DialContext: helper.Dialer,
},
}
return client.WithHTTPClient(httpClient)(c)
})
clientOpts = append(clientOpts, client.WithHost(helper.Host))
clientOpts = append(clientOpts, client.WithDialContext(helper.Dialer))
default:
clientOpts = append(clientOpts, client.FromEnv)
}
clientOpts = append(clientOpts, client.WithVersion(dockerVersion))
image.client, err = client.NewClientWithOpts(clientOpts...)
if err != nil {
return nil, err
}
@ -67,7 +97,10 @@ func (image *dockerImageAnalyzer) Fetch() (io.ReadCloser, error) {
if err != nil {
// don't use the API, the CLI has more informative output
fmt.Println("Image not available locally. Trying to pull '" + image.id + "'...")
utils.RunDockerCmd("pull", image.id)
err = utils.RunDockerCmd("pull", image.id)
if err != nil {
return nil, err
}
}
readCloser, err := image.client.ImageSave(ctx, []string{image.id})
@ -86,7 +119,6 @@ func (image *dockerImageAnalyzer) Parse(tarFile io.ReadCloser) error {
header, err := tarReader.Next()
if err == io.EOF {
fmt.Println(" ╧")
break
}
@ -139,25 +171,36 @@ func (image *dockerImageAnalyzer) Analyze() (*AnalysisResult, error) {
// note that the image config stores images in reverse chronological order, so iterate backwards through layers
// as you iterate chronologically through history (ignoring history items that have no layer contents)
layerIdx := len(image.trees) - 1
// Note: history is not required metadata in a docker image!
tarPathIdx := 0
for idx := 0; idx < len(config.History); idx++ {
// ignore empty layers, we are only observing layers with content
if config.History[idx].EmptyLayer {
continue
}
histIdx := 0
for layerIdx := len(image.trees) - 1; layerIdx >= 0; layerIdx-- {
tree := image.trees[(len(image.trees)-1)-layerIdx]
config.History[idx].Size = uint64(tree.FileSize)
// ignore empty layers, we are only observing layers with content
historyObj := dockerImageHistoryEntry{
CreatedBy: "(missing)",
}
for nextHistIdx := histIdx; nextHistIdx < len(config.History); nextHistIdx++ {
if !config.History[nextHistIdx].EmptyLayer {
histIdx = nextHistIdx
break
}
}
if histIdx < len(config.History) && !config.History[histIdx].EmptyLayer {
historyObj = config.History[histIdx]
histIdx++
}
image.layers[layerIdx] = &dockerLayer{
history: config.History[idx],
index: layerIdx,
history: historyObj,
index: tarPathIdx,
tree: image.trees[layerIdx],
tarPath: manifest.LayerTarPaths[tarPathIdx],
}
image.layers[layerIdx].history.Size = uint64(tree.FileSize)
layerIdx--
tarPathIdx++
}
@ -191,36 +234,23 @@ func (image *dockerImageAnalyzer) Analyze() (*AnalysisResult, error) {
}, nil
}
// todo: it is bad that this is printing out to the screen. As the interface gets more flushed out, an event update mechanism should be built in (so the caller can format and print updates)
func (image *dockerImageAnalyzer) processLayerTar(name string, layerIdx uint, reader *tar.Reader) error {
tree := filetree.NewFileTree()
tree.Name = name
title := fmt.Sprintf("[layer: %2d]", layerIdx)
message := fmt.Sprintf(" ├─ %s %s ", title, "working...")
fmt.Printf("\r%s", message)
fileInfos, err := image.getFileList(reader)
if err != nil {
return err
}
shortName := name[:15]
pb := utils.NewProgressBar(int64(len(fileInfos)), 30)
for idx, element := range fileInfos {
for _, element := range fileInfos {
tree.FileSize += uint64(element.Size)
// todo: we should check for errors but also allow whiteout files to be not be added (thus not error out)
tree.AddPath(element.Path, element)
if pb.Update(int64(idx)) {
message = fmt.Sprintf(" ├─ %s %s : %s", title, shortName, pb.String())
fmt.Printf("\r%s", message)
_, _, err := tree.AddPath(element.Path, element)
if err != nil {
return err
}
}
pb.Done()
message = fmt.Sprintf(" ├─ %s %s : %s", title, shortName, pb.String())
fmt.Printf("\r%s\n", message)
image.layerMap[tree.Name] = tree
return nil

View file

@ -8,7 +8,8 @@ import (
)
const (
LayerFormat = "%-25s %7s %s"
// LayerFormat = "%-15s %7s %s"
LayerFormat = "%7s %s"
)
// ShortId returns the truncated id of the current layer.
@ -43,9 +44,9 @@ func (layer *dockerLayer) Command() string {
// ShortId returns the truncated id of the current layer.
func (layer *dockerLayer) ShortId() string {
rangeBound := 25
rangeBound := 15
id := layer.Id()
if length := len(id); length < 25 {
if length := len(id); length < 15 {
rangeBound = length
}
id = id[0:rangeBound]
@ -63,12 +64,14 @@ func (layer *dockerLayer) String() string {
if layer.index == 0 {
return fmt.Sprintf(LayerFormat,
layer.ShortId(),
// layer.ShortId(),
// fmt.Sprintf("%d",layer.Index()),
humanize.Bytes(layer.Size()),
"FROM "+layer.ShortId())
}
return fmt.Sprintf(LayerFormat,
layer.ShortId(),
// layer.ShortId(),
// fmt.Sprintf("%d",layer.Index()),
humanize.Bytes(layer.Size()),
layer.Command())
}

View file

@ -42,10 +42,7 @@ func (ci *Evaluator) LoadConfig(configFile string) error {
func (ci *Evaluator) isRuleEnabled(rule Rule) bool {
value := ci.Config.GetString(rule.Key())
if value == "disabled" {
return false
}
return true
return value != "disabled"
}
func (ci *Evaluator) Evaluate(analysis *image.AnalysisResult) bool {

View file

@ -17,7 +17,7 @@ func newExport(analysis *image.AnalysisResult) *export {
idx := (len(analysis.Layers) - 1) - revIdx
data.Layer[idx] = exportLayer{
Index: idx,
Index: layer.Index(),
DigestID: layer.Id(),
SizeBytes: layer.Size(),
Command: layer.Command(),

View file

@ -79,7 +79,7 @@ func Run(options Options) {
analyzer := image.GetAnalyzer(options.ImageId)
fmt.Println(title("Fetching image..."))
fmt.Println(title("Fetching image...") + " (this can take a while with large images)")
reader, err := analyzer.Fetch()
if err != nil {
fmt.Printf("cannot fetch image: %v\n", err)

148
ui/details_controller.go Normal file
View file

@ -0,0 +1,148 @@
package ui
import (
"fmt"
"github.com/dustin/go-humanize"
"github.com/jroimartin/gocui"
"github.com/lunixbochs/vtclean"
"github.com/wagoodman/dive/filetree"
"strconv"
"strings"
)
// DetailsController holds the UI objects and data models for populating the lower-left pane. Specifically the pane that
// shows the layer details and image statistics.
type DetailsController struct {
Name string
gui *gocui.Gui
view *gocui.View
header *gocui.View
efficiency float64
inefficiencies filetree.EfficiencySlice
}
// NewDetailsController creates a new view object attached the the global [gocui] screen object.
func NewDetailsController(name string, gui *gocui.Gui, efficiency float64, inefficiencies filetree.EfficiencySlice) (controller *DetailsController) {
controller = new(DetailsController)
// populate main fields
controller.Name = name
controller.gui = gui
controller.efficiency = efficiency
controller.inefficiencies = inefficiencies
return controller
}
// Setup initializes the UI concerns within the context of a global [gocui] view object.
func (controller *DetailsController) Setup(v *gocui.View, header *gocui.View) error {
// set controller options
controller.view = v
controller.view.Editable = false
controller.view.Wrap = true
controller.view.Highlight = false
controller.view.Frame = false
controller.header = header
controller.header.Editable = false
controller.header.Wrap = false
controller.header.Frame = false
// set keybindings
if err := controller.gui.SetKeybinding(controller.Name, gocui.KeyArrowDown, gocui.ModNone, func(*gocui.Gui, *gocui.View) error { return controller.CursorDown() }); err != nil {
return err
}
if err := controller.gui.SetKeybinding(controller.Name, gocui.KeyArrowUp, gocui.ModNone, func(*gocui.Gui, *gocui.View) error { return controller.CursorUp() }); err != nil {
return err
}
return controller.Render()
}
// IsVisible indicates if the details view pane is currently initialized.
func (controller *DetailsController) IsVisible() bool {
return controller != nil
}
// CursorDown moves the cursor down in the details pane (currently indicates nothing).
func (controller *DetailsController) CursorDown() error {
return CursorDown(controller.gui, controller.view)
}
// CursorUp moves the cursor up in the details pane (currently indicates nothing).
func (controller *DetailsController) CursorUp() error {
return CursorUp(controller.gui, controller.view)
}
// Update refreshes the state objects for future rendering.
func (controller *DetailsController) Update() error {
return nil
}
// Render flushes the state objects to the screen. The details pane reports:
// 1. the current selected layer's command string
// 2. the image efficiency score
// 3. the estimated wasted image space
// 4. a list of inefficient file allocations
func (controller *DetailsController) Render() error {
currentLayer := Controllers.Layer.currentLayer()
var wastedSpace int64
template := "%5s %12s %-s\n"
inefficiencyReport := fmt.Sprintf(Formatting.Header(template), "Count", "Total Space", "Path")
height := 100
if controller.view != nil {
_, height = controller.view.Size()
}
for idx := 0; idx < len(controller.inefficiencies); idx++ {
data := controller.inefficiencies[len(controller.inefficiencies)-1-idx]
wastedSpace += data.CumulativeSize
// todo: make this report scrollable
if idx < height {
inefficiencyReport += fmt.Sprintf(template, strconv.Itoa(len(data.Nodes)), humanize.Bytes(uint64(data.CumulativeSize)), data.Path)
}
}
imageSizeStr := fmt.Sprintf("%s %s", Formatting.Header("Total Image size:"), humanize.Bytes(Controllers.Layer.ImageSize))
effStr := fmt.Sprintf("%s %d %%", Formatting.Header("Image efficiency score:"), int(100.0*controller.efficiency))
wastedSpaceStr := fmt.Sprintf("%s %s", Formatting.Header("Potential wasted space:"), humanize.Bytes(uint64(wastedSpace)))
controller.gui.Update(func(g *gocui.Gui) error {
// update header
controller.header.Clear()
width, _ := controller.view.Size()
layerHeaderStr := fmt.Sprintf("[Layer Details]%s", strings.Repeat("─", width-15))
imageHeaderStr := fmt.Sprintf("[Image Details]%s", strings.Repeat("─", width-15))
_, _ = fmt.Fprintln(controller.header, Formatting.Header(vtclean.Clean(layerHeaderStr, false)))
// update contents
controller.view.Clear()
_, _ = fmt.Fprintln(controller.view, Formatting.Header("Digest: ")+currentLayer.Id())
// TODO: add back in with controller model
// fmt.Fprintln(view.view, Formatting.Header("Tar ID: ")+currentLayer.TarId())
_, _ = fmt.Fprintln(controller.view, Formatting.Header("Command:"))
_, _ = fmt.Fprintln(controller.view, currentLayer.Command())
_, _ = fmt.Fprintln(controller.view, "\n"+Formatting.Header(vtclean.Clean(imageHeaderStr, false)))
_, _ = fmt.Fprintln(controller.view, imageSizeStr)
_, _ = fmt.Fprintln(controller.view, wastedSpaceStr)
_, _ = fmt.Fprintln(controller.view, effStr+"\n")
_, _ = fmt.Fprintln(controller.view, inefficiencyReport)
return nil
})
return nil
}
// KeyHelp indicates all the possible actions a user can take while the current pane is selected (currently does nothing).
func (controller *DetailsController) KeyHelp() string {
return "TBD"
}

View file

@ -1,151 +0,0 @@
package ui
import (
"fmt"
"github.com/dustin/go-humanize"
"github.com/jroimartin/gocui"
"github.com/lunixbochs/vtclean"
"github.com/wagoodman/dive/filetree"
"strconv"
"strings"
)
// DetailsView holds the UI objects and data models for populating the lower-left pane. Specifically the pane that
// shows the layer details and image statistics.
type DetailsView struct {
Name string
gui *gocui.Gui
view *gocui.View
header *gocui.View
efficiency float64
inefficiencies filetree.EfficiencySlice
}
// NewDetailsView creates a new view object attached the the global [gocui] screen object.
func NewDetailsView(name string, gui *gocui.Gui, efficiency float64, inefficiencies filetree.EfficiencySlice) (detailsView *DetailsView) {
detailsView = new(DetailsView)
// populate main fields
detailsView.Name = name
detailsView.gui = gui
detailsView.efficiency = efficiency
detailsView.inefficiencies = inefficiencies
return detailsView
}
// Setup initializes the UI concerns within the context of a global [gocui] view object.
func (view *DetailsView) Setup(v *gocui.View, header *gocui.View) error {
// set view options
view.view = v
view.view.Editable = false
view.view.Wrap = true
view.view.Highlight = false
view.view.Frame = false
view.header = header
view.header.Editable = false
view.header.Wrap = false
view.header.Frame = false
// 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
}
return view.Render()
}
// IsVisible indicates if the details view pane is currently initialized.
func (view *DetailsView) IsVisible() bool {
if view == nil {
return false
}
return true
}
// CursorDown moves the cursor down in the details pane (currently indicates nothing).
func (view *DetailsView) CursorDown() error {
return CursorDown(view.gui, view.view)
}
// CursorUp moves the cursor up in the details pane (currently indicates nothing).
func (view *DetailsView) CursorUp() error {
return CursorUp(view.gui, view.view)
}
// Update refreshes the state objects for future rendering.
func (view *DetailsView) Update() error {
return nil
}
// Render flushes the state objects to the screen. The details pane reports:
// 1. the current selected layer's command string
// 2. the image efficiency score
// 3. the estimated wasted image space
// 4. a list of inefficient file allocations
func (view *DetailsView) Render() error {
currentLayer := Views.Layer.currentLayer()
var wastedSpace int64
template := "%5s %12s %-s\n"
inefficiencyReport := fmt.Sprintf(Formatting.Header(template), "Count", "Total Space", "Path")
height := 100
if view.view != nil {
_, height = view.view.Size()
}
for idx := 0; idx < len(view.inefficiencies); idx++ {
data := view.inefficiencies[len(view.inefficiencies)-1-idx]
wastedSpace += data.CumulativeSize
// todo: make this report scrollable
if idx < height {
inefficiencyReport += fmt.Sprintf(template, strconv.Itoa(len(data.Nodes)), humanize.Bytes(uint64(data.CumulativeSize)), data.Path)
}
}
imageSizeStr := fmt.Sprintf("%s %s", Formatting.Header("Total Image size:"), humanize.Bytes(Views.Layer.ImageSize))
effStr := fmt.Sprintf("%s %d %%", Formatting.Header("Image efficiency score:"), int(100.0*view.efficiency))
wastedSpaceStr := fmt.Sprintf("%s %s", Formatting.Header("Potential wasted space:"), humanize.Bytes(uint64(wastedSpace)))
view.gui.Update(func(g *gocui.Gui) error {
// update header
view.header.Clear()
width, _ := view.view.Size()
layerHeaderStr := fmt.Sprintf("[Layer Details]%s", strings.Repeat("─", width-15))
imageHeaderStr := fmt.Sprintf("[Image Details]%s", strings.Repeat("─", width-15))
fmt.Fprintln(view.header, Formatting.Header(vtclean.Clean(layerHeaderStr, false)))
// update contents
view.view.Clear()
fmt.Fprintln(view.view, Formatting.Header("Digest: ")+currentLayer.Id())
// TODO: add back in with view model
// fmt.Fprintln(view.view, Formatting.Header("Tar ID: ")+currentLayer.TarId())
fmt.Fprintln(view.view, Formatting.Header("Command:"))
fmt.Fprintln(view.view, currentLayer.Command())
fmt.Fprintln(view.view, "\n"+Formatting.Header(vtclean.Clean(imageHeaderStr, false)))
fmt.Fprintln(view.view, imageSizeStr)
fmt.Fprintln(view.view, wastedSpaceStr)
fmt.Fprintln(view.view, effStr+"\n")
fmt.Fprintln(view.view, inefficiencyReport)
return nil
})
return nil
}
// KeyHelp indicates all the possible actions a user can take while the current pane is selected (currently does nothing).
func (view *DetailsView) KeyHelp() string {
return "TBD"
}

398
ui/filetree_controller.go Normal file
View file

@ -0,0 +1,398 @@
package ui
import (
"fmt"
"github.com/lunixbochs/vtclean"
"github.com/sirupsen/logrus"
"github.com/spf13/viper"
"github.com/wagoodman/keybinding"
"regexp"
"strings"
"github.com/jroimartin/gocui"
"github.com/wagoodman/dive/filetree"
)
const (
CompareLayer CompareType = iota
CompareAll
)
type CompareType int
// FileTreeController holds the UI objects and data models for populating the right pane. Specifically the pane that
// shows selected layer or aggregate file ASCII tree.
type FileTreeController struct {
Name string
gui *gocui.Gui
view *gocui.View
header *gocui.View
vm *FileTreeViewModel
keybindingToggleCollapse []keybinding.Key
keybindingToggleCollapseAll []keybinding.Key
keybindingToggleAttributes []keybinding.Key
keybindingToggleAdded []keybinding.Key
keybindingToggleRemoved []keybinding.Key
keybindingToggleModified []keybinding.Key
keybindingToggleUnchanged []keybinding.Key
keybindingPageDown []keybinding.Key
keybindingPageUp []keybinding.Key
}
// NewFileTreeController creates a new view object attached the the global [gocui] screen object.
func NewFileTreeController(name string, gui *gocui.Gui, tree *filetree.FileTree, refTrees []*filetree.FileTree, cache filetree.TreeCache) (controller *FileTreeController) {
controller = new(FileTreeController)
// populate main fields
controller.Name = name
controller.gui = gui
controller.vm = NewFileTreeViewModel(tree, refTrees, cache)
var err error
controller.keybindingToggleCollapse, err = keybinding.ParseAll(viper.GetString("keybinding.toggle-collapse-dir"))
if err != nil {
logrus.Error(err)
}
controller.keybindingToggleCollapseAll, err = keybinding.ParseAll(viper.GetString("keybinding.toggle-collapse-all-dir"))
if err != nil {
logrus.Error(err)
}
controller.keybindingToggleAttributes, err = keybinding.ParseAll(viper.GetString("keybinding.toggle-filetree-attributes"))
if err != nil {
logrus.Error(err)
}
controller.keybindingToggleAdded, err = keybinding.ParseAll(viper.GetString("keybinding.toggle-added-files"))
if err != nil {
logrus.Error(err)
}
controller.keybindingToggleRemoved, err = keybinding.ParseAll(viper.GetString("keybinding.toggle-removed-files"))
if err != nil {
logrus.Error(err)
}
controller.keybindingToggleModified, err = keybinding.ParseAll(viper.GetString("keybinding.toggle-modified-files"))
if err != nil {
logrus.Error(err)
}
controller.keybindingToggleUnchanged, err = keybinding.ParseAll(viper.GetString("keybinding.toggle-unchanged-files"))
if err != nil {
logrus.Error(err)
}
controller.keybindingPageUp, err = keybinding.ParseAll(viper.GetString("keybinding.page-up"))
if err != nil {
logrus.Error(err)
}
controller.keybindingPageDown, err = keybinding.ParseAll(viper.GetString("keybinding.page-down"))
if err != nil {
logrus.Error(err)
}
return controller
}
// Setup initializes the UI concerns within the context of a global [gocui] view object.
func (controller *FileTreeController) Setup(v *gocui.View, header *gocui.View) error {
// set controller options
controller.view = v
controller.view.Editable = false
controller.view.Wrap = false
controller.view.Frame = false
controller.header = header
controller.header.Editable = false
controller.header.Wrap = false
controller.header.Frame = false
// set keybindings
if err := controller.gui.SetKeybinding(controller.Name, gocui.KeyArrowDown, gocui.ModNone, func(*gocui.Gui, *gocui.View) error { return controller.CursorDown() }); err != nil {
return err
}
if err := controller.gui.SetKeybinding(controller.Name, gocui.KeyArrowUp, gocui.ModNone, func(*gocui.Gui, *gocui.View) error { return controller.CursorUp() }); err != nil {
return err
}
if err := controller.gui.SetKeybinding(controller.Name, gocui.KeyArrowLeft, gocui.ModNone, func(*gocui.Gui, *gocui.View) error { return controller.CursorLeft() }); err != nil {
return err
}
if err := controller.gui.SetKeybinding(controller.Name, gocui.KeyArrowRight, gocui.ModNone, func(*gocui.Gui, *gocui.View) error { return controller.CursorRight() }); err != nil {
return err
}
for _, key := range controller.keybindingPageUp {
if err := controller.gui.SetKeybinding(controller.Name, key.Value, key.Modifier, func(*gocui.Gui, *gocui.View) error { return controller.PageUp() }); err != nil {
return err
}
}
for _, key := range controller.keybindingPageDown {
if err := controller.gui.SetKeybinding(controller.Name, key.Value, key.Modifier, func(*gocui.Gui, *gocui.View) error { return controller.PageDown() }); err != nil {
return err
}
}
for _, key := range controller.keybindingToggleCollapse {
if err := controller.gui.SetKeybinding(controller.Name, key.Value, key.Modifier, func(*gocui.Gui, *gocui.View) error { return controller.toggleCollapse() }); err != nil {
return err
}
}
for _, key := range controller.keybindingToggleCollapseAll {
if err := controller.gui.SetKeybinding(controller.Name, key.Value, key.Modifier, func(*gocui.Gui, *gocui.View) error { return controller.toggleCollapseAll() }); err != nil {
return err
}
}
for _, key := range controller.keybindingToggleAttributes {
if err := controller.gui.SetKeybinding(controller.Name, key.Value, key.Modifier, func(*gocui.Gui, *gocui.View) error { return controller.toggleAttributes() }); err != nil {
return err
}
}
for _, key := range controller.keybindingToggleAdded {
if err := controller.gui.SetKeybinding(controller.Name, key.Value, key.Modifier, func(*gocui.Gui, *gocui.View) error { return controller.toggleShowDiffType(filetree.Added) }); err != nil {
return err
}
}
for _, key := range controller.keybindingToggleRemoved {
if err := controller.gui.SetKeybinding(controller.Name, key.Value, key.Modifier, func(*gocui.Gui, *gocui.View) error { return controller.toggleShowDiffType(filetree.Removed) }); err != nil {
return err
}
}
for _, key := range controller.keybindingToggleModified {
if err := controller.gui.SetKeybinding(controller.Name, key.Value, key.Modifier, func(*gocui.Gui, *gocui.View) error { return controller.toggleShowDiffType(filetree.Changed) }); err != nil {
return err
}
}
for _, key := range controller.keybindingToggleUnchanged {
if err := controller.gui.SetKeybinding(controller.Name, key.Value, key.Modifier, func(*gocui.Gui, *gocui.View) error { return controller.toggleShowDiffType(filetree.Unchanged) }); err != nil {
return err
}
}
_, height := controller.view.Size()
controller.vm.Setup(0, height)
_ = controller.Update()
_ = controller.Render()
return nil
}
// IsVisible indicates if the file tree view pane is currently initialized
func (controller *FileTreeController) IsVisible() bool {
return controller != nil
}
// resetCursor moves the cursor back to the top of the buffer and translates to the top of the buffer.
func (controller *FileTreeController) resetCursor() {
_ = controller.view.SetCursor(0, 0)
controller.vm.resetCursor()
}
// setTreeByLayer populates the view model by stacking the indicated image layer file trees.
func (controller *FileTreeController) setTreeByLayer(bottomTreeStart, bottomTreeStop, topTreeStart, topTreeStop int) error {
err := controller.vm.setTreeByLayer(bottomTreeStart, bottomTreeStop, topTreeStart, topTreeStop)
if err != nil {
return err
}
// controller.resetCursor()
_ = controller.Update()
return controller.Render()
}
// CursorDown moves the cursor down and renders the view.
// Note: we cannot use the gocui buffer since any state change requires writing the entire tree to the buffer.
// Instead we are keeping an upper and lower bounds of the tree string to render and only flushing
// this range into the view buffer. This is much faster when tree sizes are large.
func (controller *FileTreeController) CursorDown() error {
if controller.vm.CursorDown() {
return controller.Render()
}
return nil
}
// CursorUp moves the cursor up and renders the view.
// Note: we cannot use the gocui buffer since any state change requires writing the entire tree to the buffer.
// Instead we are keeping an upper and lower bounds of the tree string to render and only flushing
// this range into the view buffer. This is much faster when tree sizes are large.
func (controller *FileTreeController) CursorUp() error {
if controller.vm.CursorUp() {
return controller.Render()
}
return nil
}
// CursorLeft moves the cursor up until we reach the Parent Node or top of the tree
func (controller *FileTreeController) CursorLeft() error {
err := controller.vm.CursorLeft(filterRegex())
if err != nil {
return err
}
_ = controller.Update()
return controller.Render()
}
// CursorRight descends into directory expanding it if needed
func (controller *FileTreeController) CursorRight() error {
err := controller.vm.CursorRight(filterRegex())
if err != nil {
return err
}
_ = controller.Update()
return controller.Render()
}
// PageDown moves to next page putting the cursor on top
func (controller *FileTreeController) PageDown() error {
err := controller.vm.PageDown()
if err != nil {
return err
}
return controller.Render()
}
// PageUp moves to previous page putting the cursor on top
func (controller *FileTreeController) PageUp() error {
err := controller.vm.PageUp()
if err != nil {
return err
}
return controller.Render()
}
// getAbsPositionNode determines the selected screen cursor's location in the file tree, returning the selected FileNode.
// func (controller *FileTreeController) getAbsPositionNode() (node *filetree.FileNode) {
// return controller.vm.getAbsPositionNode(filterRegex())
// }
// toggleCollapse will collapse/expand the selected FileNode.
func (controller *FileTreeController) toggleCollapse() error {
err := controller.vm.toggleCollapse(filterRegex())
if err != nil {
return err
}
_ = controller.Update()
return controller.Render()
}
// toggleCollapseAll will collapse/expand the all directories.
func (controller *FileTreeController) toggleCollapseAll() error {
err := controller.vm.toggleCollapseAll()
if err != nil {
return err
}
if controller.vm.CollapseAll {
controller.resetCursor()
}
_ = controller.Update()
return controller.Render()
}
// toggleAttributes will show/hide file attributes
func (controller *FileTreeController) toggleAttributes() error {
err := controller.vm.toggleAttributes()
if err != nil {
return err
}
// we need to render the changes to the status pane as well
Update()
Render()
return nil
}
// toggleShowDiffType will show/hide the selected DiffType in the filetree pane.
func (controller *FileTreeController) toggleShowDiffType(diffType filetree.DiffType) error {
controller.vm.toggleShowDiffType(diffType)
// we need to render the changes to the status pane as well
Update()
Render()
return nil
}
// filterRegex will return a regular expression object to match the user's filter input.
func filterRegex() *regexp.Regexp {
if Controllers.Filter == nil || Controllers.Filter.view == nil {
return nil
}
filterString := strings.TrimSpace(Controllers.Filter.view.Buffer())
if len(filterString) == 0 {
return nil
}
regex, err := regexp.Compile(filterString)
if err != nil {
return nil
}
return regex
}
// onLayoutChange is called by the UI framework to inform the view-model of the new screen dimensions
func (controller *FileTreeController) onLayoutChange(resized bool) error {
_ = controller.Update()
if resized {
return controller.Render()
}
return nil
}
// Update refreshes the state objects for future rendering.
func (controller *FileTreeController) Update() error {
var width, height int
if controller.view != nil {
width, height = controller.view.Size()
} else {
// before the TUI is setup there may not be a controller to reference. Use the entire screen as reference.
width, height = controller.gui.Size()
}
// height should account for the header
return controller.vm.Update(filterRegex(), width, height-1)
}
// Render flushes the state objects (file tree) to the pane.
func (controller *FileTreeController) Render() error {
title := "Current Layer Contents"
if Controllers.Layer.CompareMode == CompareAll {
title = "Aggregated Layer Contents"
}
// indicate when selected
if controller.gui.CurrentView() == controller.view {
title = "● " + title
}
controller.gui.Update(func(g *gocui.Gui) error {
// update the header
controller.header.Clear()
width, _ := g.Size()
headerStr := fmt.Sprintf("[%s]%s\n", title, strings.Repeat("─", width*2))
if controller.vm.ShowAttributes {
headerStr += fmt.Sprintf(filetree.AttributeFormat+" %s", "P", "ermission", "UID:GID", "Size", "Filetree")
}
_, _ = fmt.Fprintln(controller.header, Formatting.Header(vtclean.Clean(headerStr, false)))
// update the contents
controller.view.Clear()
_ = controller.vm.Render()
_, _ = fmt.Fprint(controller.view, controller.vm.mainBuf.String())
return nil
})
return nil
}
// KeyHelp indicates all the possible actions a user can take while the current pane is selected.
func (controller *FileTreeController) KeyHelp() string {
return renderStatusOption(controller.keybindingToggleCollapse[0].String(), "Collapse dir", false) +
renderStatusOption(controller.keybindingToggleCollapseAll[0].String(), "Collapse all dir", false) +
renderStatusOption(controller.keybindingToggleAdded[0].String(), "Added", !controller.vm.HiddenDiffTypes[filetree.Added]) +
renderStatusOption(controller.keybindingToggleRemoved[0].String(), "Removed", !controller.vm.HiddenDiffTypes[filetree.Removed]) +
renderStatusOption(controller.keybindingToggleModified[0].String(), "Modified", !controller.vm.HiddenDiffTypes[filetree.Changed]) +
renderStatusOption(controller.keybindingToggleUnchanged[0].String(), "Unmodified", !controller.vm.HiddenDiffTypes[filetree.Unchanged]) +
renderStatusOption(controller.keybindingToggleAttributes[0].String(), "Attributes", controller.vm.ShowAttributes)
}

424
ui/filetree_viewmodel.go Normal file
View file

@ -0,0 +1,424 @@
package ui
import (
"bytes"
"fmt"
"github.com/sirupsen/logrus"
"github.com/spf13/viper"
"github.com/wagoodman/dive/utils"
"regexp"
"strings"
"github.com/lunixbochs/vtclean"
"github.com/wagoodman/dive/filetree"
)
// FileTreeViewModel holds the UI objects and data models for populating the right pane. Specifically the pane that
// shows selected layer or aggregate file ASCII tree.
type FileTreeViewModel struct {
ModelTree *filetree.FileTree
ViewTree *filetree.FileTree
RefTrees []*filetree.FileTree
cache filetree.TreeCache
CollapseAll bool
ShowAttributes bool
HiddenDiffTypes []bool
TreeIndex int
bufferIndex int
bufferIndexLowerBound int
refHeight int
refWidth int
mainBuf bytes.Buffer
}
// NewFileTreeController creates a new view object attached the the global [gocui] screen object.
func NewFileTreeViewModel(tree *filetree.FileTree, refTrees []*filetree.FileTree, cache filetree.TreeCache) (treeViewModel *FileTreeViewModel) {
treeViewModel = new(FileTreeViewModel)
// populate main fields
treeViewModel.ShowAttributes = viper.GetBool("filetree.show-attributes")
treeViewModel.CollapseAll = viper.GetBool("filetree.collapse-dir")
treeViewModel.ModelTree = tree
treeViewModel.RefTrees = refTrees
treeViewModel.cache = cache
treeViewModel.HiddenDiffTypes = make([]bool, 4)
hiddenTypes := viper.GetStringSlice("diff.hide")
for _, hType := range hiddenTypes {
switch t := strings.ToLower(hType); t {
case "added":
treeViewModel.HiddenDiffTypes[filetree.Added] = true
case "removed":
treeViewModel.HiddenDiffTypes[filetree.Removed] = true
case "changed":
treeViewModel.HiddenDiffTypes[filetree.Changed] = true
case "unchanged":
treeViewModel.HiddenDiffTypes[filetree.Unchanged] = true
default:
utils.PrintAndExit(fmt.Sprintf("unknown diff.hide value: %s", t))
}
}
return treeViewModel
}
// Setup initializes the UI concerns within the context of a global [gocui] view object.
func (vm *FileTreeViewModel) Setup(lowerBound, height int) {
vm.bufferIndexLowerBound = lowerBound
vm.refHeight = height
}
// height returns the current height and considers the header
func (vm *FileTreeViewModel) height() int {
if vm.ShowAttributes {
return vm.refHeight - 1
}
return vm.refHeight
}
// bufferIndexUpperBound returns the current upper bounds for the view
func (vm *FileTreeViewModel) bufferIndexUpperBound() int {
return vm.bufferIndexLowerBound + vm.height()
}
// IsVisible indicates if the file tree view pane is currently initialized
func (vm *FileTreeViewModel) IsVisible() bool {
return vm != nil
}
// resetCursor moves the cursor back to the top of the buffer and translates to the top of the buffer.
func (vm *FileTreeViewModel) resetCursor() {
vm.TreeIndex = 0
vm.bufferIndex = 0
vm.bufferIndexLowerBound = 0
}
// setTreeByLayer populates the view model by stacking the indicated image layer file trees.
func (vm *FileTreeViewModel) setTreeByLayer(bottomTreeStart, bottomTreeStop, topTreeStart, topTreeStop int) error {
if topTreeStop > len(vm.RefTrees)-1 {
return fmt.Errorf("invalid layer index given: %d of %d", topTreeStop, len(vm.RefTrees)-1)
}
newTree := vm.cache.Get(bottomTreeStart, bottomTreeStop, topTreeStart, topTreeStop)
// preserve vm state on copy
visitor := func(node *filetree.FileNode) error {
newNode, err := newTree.GetNode(node.Path())
if err == nil {
newNode.Data.ViewInfo = node.Data.ViewInfo
}
return nil
}
err := vm.ModelTree.VisitDepthChildFirst(visitor, nil)
if err != nil {
logrus.Errorf("unable to propagate layer tree: %+v", err)
return err
}
vm.ModelTree = newTree
return nil
}
// doCursorUp performs the internal view's buffer adjustments on cursor up. Note: this is independent of the gocui buffer.
func (vm *FileTreeViewModel) CursorUp() bool {
if vm.TreeIndex <= 0 {
return false
}
vm.TreeIndex--
if vm.TreeIndex < vm.bufferIndexLowerBound {
vm.bufferIndexLowerBound--
}
if vm.bufferIndex > 0 {
vm.bufferIndex--
}
return true
}
// doCursorDown performs the internal view's buffer adjustments on cursor down. Note: this is independent of the gocui buffer.
func (vm *FileTreeViewModel) CursorDown() bool {
if vm.TreeIndex >= vm.ModelTree.VisibleSize() {
return false
}
vm.TreeIndex++
if vm.TreeIndex > vm.bufferIndexUpperBound() {
vm.bufferIndexLowerBound++
}
vm.bufferIndex++
if vm.bufferIndex > vm.height() {
vm.bufferIndex = vm.height()
}
return true
}
// CursorLeft moves the cursor up until we reach the Parent Node or top of the tree
func (vm *FileTreeViewModel) CursorLeft(filterRegex *regexp.Regexp) error {
var visitor func(*filetree.FileNode) error
var evaluator func(*filetree.FileNode) bool
var dfsCounter, newIndex int
oldIndex := vm.TreeIndex
currentNode := vm.getAbsPositionNode(filterRegex)
if currentNode == nil {
return nil
}
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 filterRegex != nil {
match := filterRegex.Find([]byte(curNode.Path()))
regexMatch = match != nil
}
return !curNode.Parent.Data.ViewInfo.Collapsed && !curNode.Data.ViewInfo.Hidden && regexMatch
}
err := vm.ModelTree.VisitDepthParentFirst(visitor, evaluator)
if err != nil {
logrus.Errorf("could not propagate tree on cursorLeft: %+v", err)
return err
}
vm.TreeIndex = newIndex
moveIndex := oldIndex - newIndex
if newIndex < vm.bufferIndexLowerBound {
vm.bufferIndexLowerBound = vm.TreeIndex
}
if vm.bufferIndex > moveIndex {
vm.bufferIndex = vm.bufferIndex - moveIndex
} else {
vm.bufferIndex = 0
}
return nil
}
// CursorRight descends into directory expanding it if needed
func (vm *FileTreeViewModel) CursorRight(filterRegex *regexp.Regexp) error {
node := vm.getAbsPositionNode(filterRegex)
if node == nil {
return nil
}
if !node.Data.FileInfo.IsDir {
return nil
}
if len(node.Children) == 0 {
return nil
}
if node.Data.ViewInfo.Collapsed {
node.Data.ViewInfo.Collapsed = false
}
vm.TreeIndex++
if vm.TreeIndex > vm.bufferIndexUpperBound() {
vm.bufferIndexLowerBound++
}
vm.bufferIndex++
if vm.bufferIndex > vm.height() {
vm.bufferIndex = vm.height()
}
return nil
}
// PageDown moves to next page putting the cursor on top
func (vm *FileTreeViewModel) PageDown() error {
nextBufferIndexLowerBound := vm.bufferIndexLowerBound + vm.height()
nextBufferIndexUpperBound := nextBufferIndexLowerBound + vm.height()
// todo: this work should be saved or passed to render...
treeString := vm.ViewTree.StringBetween(nextBufferIndexLowerBound, nextBufferIndexUpperBound, vm.ShowAttributes)
lines := strings.Split(treeString, "\n")
newLines := len(lines) - 1
if vm.height() >= newLines {
nextBufferIndexLowerBound = vm.bufferIndexLowerBound + newLines
}
vm.bufferIndexLowerBound = nextBufferIndexLowerBound
if vm.TreeIndex < nextBufferIndexLowerBound {
vm.bufferIndex = 0
vm.TreeIndex = nextBufferIndexLowerBound
} else {
vm.bufferIndex = vm.bufferIndex - newLines
}
return nil
}
// PageUp moves to previous page putting the cursor on top
func (vm *FileTreeViewModel) PageUp() error {
nextBufferIndexLowerBound := vm.bufferIndexLowerBound - vm.height()
nextBufferIndexUpperBound := nextBufferIndexLowerBound + vm.height()
// todo: this work should be saved or passed to render...
treeString := vm.ViewTree.StringBetween(nextBufferIndexLowerBound, nextBufferIndexUpperBound, vm.ShowAttributes)
lines := strings.Split(treeString, "\n")
newLines := len(lines) - 2
if vm.height() >= newLines {
nextBufferIndexLowerBound = vm.bufferIndexLowerBound - newLines
}
vm.bufferIndexLowerBound = nextBufferIndexLowerBound
if vm.TreeIndex > (nextBufferIndexUpperBound - 1) {
vm.bufferIndex = 0
vm.TreeIndex = nextBufferIndexLowerBound
} else {
vm.bufferIndex = vm.bufferIndex + newLines
}
return nil
}
// getAbsPositionNode determines the selected screen cursor's location in the file tree, returning the selected FileNode.
func (vm *FileTreeViewModel) getAbsPositionNode(filterRegex *regexp.Regexp) (node *filetree.FileNode) {
var visitor func(*filetree.FileNode) error
var evaluator func(*filetree.FileNode) bool
var dfsCounter int
visitor = func(curNode *filetree.FileNode) error {
if dfsCounter == vm.TreeIndex {
node = curNode
}
dfsCounter++
return nil
}
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 := vm.ModelTree.VisitDepthParentFirst(visitor, evaluator)
if err != nil {
logrus.Errorf("unable to get node position: %+v", err)
}
return node
}
// toggleCollapse will collapse/expand the selected FileNode.
func (vm *FileTreeViewModel) toggleCollapse(filterRegex *regexp.Regexp) error {
node := vm.getAbsPositionNode(filterRegex)
if node != nil && node.Data.FileInfo.IsDir {
node.Data.ViewInfo.Collapsed = !node.Data.ViewInfo.Collapsed
}
return nil
}
// toggleCollapseAll will collapse/expand the all directories.
func (vm *FileTreeViewModel) toggleCollapseAll() error {
vm.CollapseAll = !vm.CollapseAll
visitor := func(curNode *filetree.FileNode) error {
curNode.Data.ViewInfo.Collapsed = vm.CollapseAll
return nil
}
evaluator := func(curNode *filetree.FileNode) bool {
return curNode.Data.FileInfo.IsDir
}
err := vm.ModelTree.VisitDepthChildFirst(visitor, evaluator)
if err != nil {
logrus.Errorf("unable to propagate tree on toggleCollapseAll: %+v", err)
}
return nil
}
// toggleCollapse will collapse/expand the selected FileNode.
func (vm *FileTreeViewModel) toggleAttributes() error {
vm.ShowAttributes = !vm.ShowAttributes
return nil
}
// toggleShowDiffType will show/hide the selected DiffType in the filetree pane.
func (vm *FileTreeViewModel) toggleShowDiffType(diffType filetree.DiffType) {
vm.HiddenDiffTypes[diffType] = !vm.HiddenDiffTypes[diffType]
}
// Update refreshes the state objects for future rendering.
func (vm *FileTreeViewModel) Update(filterRegex *regexp.Regexp, width, height int) error {
vm.refWidth = width
vm.refHeight = height
// keep the vm selection in parity with the current DiffType selection
err := vm.ModelTree.VisitDepthChildFirst(func(node *filetree.FileNode) error {
node.Data.ViewInfo.Hidden = vm.HiddenDiffTypes[node.Data.DiffType]
visibleChild := false
for _, child := range node.Children {
if !child.Data.ViewInfo.Hidden {
visibleChild = true
node.Data.ViewInfo.Hidden = false
}
}
// hide nodes that do not match the current file filter regex (also don't unhide nodes that are already hidden)
if filterRegex != nil && !visibleChild && !node.Data.ViewInfo.Hidden {
match := filterRegex.FindString(node.Path())
node.Data.ViewInfo.Hidden = len(match) == 0
}
return nil
}, nil)
if err != nil {
logrus.Errorf("unable to propagate vm model tree: %+v", err)
return err
}
// make a new tree with only visible nodes
vm.ViewTree = vm.ModelTree.Copy()
err = vm.ViewTree.VisitDepthParentFirst(func(node *filetree.FileNode) error {
if node.Data.ViewInfo.Hidden {
err1 := vm.ViewTree.RemovePath(node.Path())
if err1 != nil {
return err1
}
}
return nil
}, nil)
if err != nil {
logrus.Errorf("unable to propagate vm view tree: %+v", err)
return err
}
return nil
}
// Render flushes the state objects (file tree) to the pane.
func (vm *FileTreeViewModel) Render() error {
treeString := vm.ViewTree.StringBetween(vm.bufferIndexLowerBound, vm.bufferIndexUpperBound(), vm.ShowAttributes)
lines := strings.Split(treeString, "\n")
// update the contents
vm.mainBuf.Reset()
for idx, line := range lines {
if idx == vm.bufferIndex {
fmt.Fprintln(&vm.mainBuf, Formatting.Selected(vtclean.Clean(line, false)))
} else {
fmt.Fprintln(&vm.mainBuf, line)
}
}
return nil
}

View file

@ -0,0 +1,384 @@
package ui
import (
"bytes"
"github.com/fatih/color"
"github.com/sergi/go-diff/diffmatchpatch"
"github.com/wagoodman/dive/filetree"
"github.com/wagoodman/dive/image"
"io/ioutil"
"os"
"path/filepath"
"regexp"
"testing"
)
const allowTestDataCapture = false
func fileExists(filename string) bool {
info, err := os.Stat(filename)
if os.IsNotExist(err) {
return false
}
return !info.IsDir()
}
func testCaseDataFilePath(name string) string {
return filepath.Join("testdata", name+".txt")
}
func helperLoadBytes(t *testing.T) []byte {
path := testCaseDataFilePath(t.Name())
theBytes, err := ioutil.ReadFile(path)
if err != nil {
t.Fatalf("unable to load test data ('%s'): %+v", t.Name(), err)
}
return theBytes
}
func helperCaptureBytes(t *testing.T, data []byte) {
if !allowTestDataCapture {
t.Fatalf("cannot capture data in test mode: %s", t.Name())
}
path := testCaseDataFilePath(t.Name())
err := ioutil.WriteFile(path, data, 0644)
if err != nil {
t.Fatalf("unable to save test data ('%s'): %+v", t.Name(), err)
}
}
func helperCheckDiff(t *testing.T, expected, actual []byte) {
if !bytes.Equal(expected, actual) {
dmp := diffmatchpatch.New()
diffs := dmp.DiffMain(string(expected), string(actual), true)
t.Errorf(dmp.DiffPrettyText(diffs))
t.Errorf("%s: bytes mismatch", t.Name())
}
}
func assertTestData(t *testing.T, actualBytes []byte) {
path := testCaseDataFilePath(t.Name())
if !fileExists(path) {
if allowTestDataCapture {
helperCaptureBytes(t, actualBytes)
} else {
t.Fatalf("missing test data: %s", path)
}
}
expectedBytes := helperLoadBytes(t)
helperCheckDiff(t, expectedBytes, actualBytes)
}
func initializeTestViewModel(t *testing.T) *FileTreeViewModel {
result, err := image.TestLoadDockerImageTar("../.data/test-docker-image.tar")
if err != nil {
t.Fatalf("%s: unable to fetch analysis: %v", t.Name(), err)
}
cache := filetree.NewFileTreeCache(result.RefTrees)
cache.Build()
Formatting.Selected = color.New(color.ReverseVideo, color.Bold).SprintFunc()
return NewFileTreeViewModel(filetree.StackTreeRange(result.RefTrees, 0, 0), result.RefTrees, cache)
}
func runTestCase(t *testing.T, vm *FileTreeViewModel, width, height int, filterRegex *regexp.Regexp) {
err := vm.Update(filterRegex, width, height)
if err != nil {
t.Errorf("failed to update viewmodel: %v", err)
}
err = vm.Render()
if err != nil {
t.Errorf("failed to render viewmodel: %v", err)
}
assertTestData(t, vm.mainBuf.Bytes())
}
func checkError(t *testing.T, err error, message string) {
if err != nil {
t.Errorf(message+": %+v", err)
}
}
func TestFileTreeGoCase(t *testing.T) {
vm := initializeTestViewModel(t)
width, height := 100, 1000
vm.Setup(0, height)
vm.ShowAttributes = true
runTestCase(t, vm, width, height, nil)
}
func TestFileTreeNoAttributes(t *testing.T) {
vm := initializeTestViewModel(t)
width, height := 100, 1000
vm.Setup(0, height)
vm.ShowAttributes = false
runTestCase(t, vm, width, height, nil)
}
func TestFileTreeRestrictedHeight(t *testing.T) {
vm := initializeTestViewModel(t)
width, height := 100, 20
vm.Setup(0, height)
vm.ShowAttributes = false
runTestCase(t, vm, width, height, nil)
}
func TestFileTreeDirCollapse(t *testing.T) {
vm := initializeTestViewModel(t)
width, height := 100, 100
vm.Setup(0, height)
vm.ShowAttributes = true
// collapse /bin
err := vm.toggleCollapse(nil)
checkError(t, err, "unable to collapse /bin")
moved := vm.CursorDown()
if !moved {
t.Error("unable to cursor down")
}
moved = vm.CursorDown()
if !moved {
t.Error("unable to cursor down")
}
// collapse /etc
err = vm.toggleCollapse(nil)
checkError(t, err, "unable to collapse /etc")
runTestCase(t, vm, width, height, nil)
}
func TestFileTreeDirCollapseAll(t *testing.T) {
vm := initializeTestViewModel(t)
width, height := 100, 100
vm.Setup(0, height)
vm.ShowAttributes = true
err := vm.toggleCollapseAll()
checkError(t, err, "unable to collapse all dir")
runTestCase(t, vm, width, height, nil)
}
func TestFileTreeSelectLayer(t *testing.T) {
vm := initializeTestViewModel(t)
width, height := 100, 100
vm.Setup(0, height)
vm.ShowAttributes = true
// collapse /bin
err := vm.toggleCollapse(nil)
checkError(t, err, "unable to collapse /bin")
// select the next layer, compareMode = layer
err = vm.setTreeByLayer(0, 0, 1, 1)
if err != nil {
t.Errorf("unable to setTreeByLayer: %v", err)
}
runTestCase(t, vm, width, height, nil)
}
func TestFileShowAggregateChanges(t *testing.T) {
vm := initializeTestViewModel(t)
width, height := 100, 100
vm.Setup(0, height)
vm.ShowAttributes = true
// collapse /bin
err := vm.toggleCollapse(nil)
checkError(t, err, "unable to collapse /bin")
// select the next layer, compareMode = layer
err = vm.setTreeByLayer(0, 0, 1, 13)
checkError(t, err, "unable to setTreeByLayer")
runTestCase(t, vm, width, height, nil)
}
func TestFileTreePageDown(t *testing.T) {
vm := initializeTestViewModel(t)
width, height := 100, 10
vm.Setup(0, height)
vm.ShowAttributes = true
err := vm.Update(nil, width, height)
checkError(t, err, "unable to update")
err = vm.PageDown()
checkError(t, err, "unable to page down")
err = vm.PageDown()
checkError(t, err, "unable to page down")
err = vm.PageDown()
checkError(t, err, "unable to page down")
runTestCase(t, vm, width, height, nil)
}
func TestFileTreePageUp(t *testing.T) {
vm := initializeTestViewModel(t)
width, height := 100, 10
vm.Setup(0, height)
vm.ShowAttributes = true
// these operations have a render step for intermediate results, which require at least one update to be done first
err := vm.Update(nil, width, height)
checkError(t, err, "unable to update")
err = vm.PageDown()
checkError(t, err, "unable to page down")
err = vm.PageDown()
checkError(t, err, "unable to page down")
err = vm.PageUp()
checkError(t, err, "unable to page up")
runTestCase(t, vm, width, height, nil)
}
func TestFileTreeDirCursorRight(t *testing.T) {
vm := initializeTestViewModel(t)
width, height := 100, 100
vm.Setup(0, height)
vm.ShowAttributes = true
// collapse /bin
err := vm.toggleCollapse(nil)
checkError(t, err, "unable to collapse /bin")
moved := vm.CursorDown()
if !moved {
t.Error("unable to cursor down")
}
moved = vm.CursorDown()
if !moved {
t.Error("unable to cursor down")
}
// collapse /etc
err = vm.toggleCollapse(nil)
checkError(t, err, "unable to collapse /etc")
// expand /etc
err = vm.CursorRight(nil)
checkError(t, err, "unable to cursor right")
runTestCase(t, vm, width, height, nil)
}
func TestFileTreeFilterTree(t *testing.T) {
vm := initializeTestViewModel(t)
width, height := 100, 1000
vm.Setup(0, height)
vm.ShowAttributes = true
regex, err := regexp.Compile("network")
if err != nil {
t.Errorf("could not create filter regex: %+v", err)
}
runTestCase(t, vm, width, height, regex)
}
func TestFileTreeHideAddedRemovedModified(t *testing.T) {
vm := initializeTestViewModel(t)
width, height := 100, 100
vm.Setup(0, height)
vm.ShowAttributes = true
// collapse /bin
err := vm.toggleCollapse(nil)
checkError(t, err, "unable to collapse /bin")
// select the 7th layer, compareMode = layer
err = vm.setTreeByLayer(0, 0, 1, 7)
if err != nil {
t.Errorf("unable to setTreeByLayer: %v", err)
}
// hide added files
vm.toggleShowDiffType(filetree.Added)
// hide modified files
vm.toggleShowDiffType(filetree.Changed)
// hide removed files
vm.toggleShowDiffType(filetree.Removed)
runTestCase(t, vm, width, height, nil)
}
func TestFileTreeHideUnmodified(t *testing.T) {
vm := initializeTestViewModel(t)
width, height := 100, 100
vm.Setup(0, height)
vm.ShowAttributes = true
// collapse /bin
err := vm.toggleCollapse(nil)
checkError(t, err, "unable to collapse /bin")
// select the 7th layer, compareMode = layer
err = vm.setTreeByLayer(0, 0, 1, 7)
if err != nil {
t.Errorf("unable to setTreeByLayer: %v", err)
}
// hide unmodified files
vm.toggleShowDiffType(filetree.Unchanged)
runTestCase(t, vm, width, height, nil)
}
func TestFileTreeHideTypeWithFilter(t *testing.T) {
vm := initializeTestViewModel(t)
width, height := 100, 100
vm.Setup(0, height)
vm.ShowAttributes = true
// collapse /bin
err := vm.toggleCollapse(nil)
checkError(t, err, "unable to collapse /bin")
// select the 7th layer, compareMode = layer
err = vm.setTreeByLayer(0, 0, 1, 7)
if err != nil {
t.Errorf("unable to setTreeByLayer: %v", err)
}
// hide added files
vm.toggleShowDiffType(filetree.Added)
regex, err := regexp.Compile("saved")
if err != nil {
t.Errorf("could not create filter regex: %+v", err)
}
runTestCase(t, vm, width, height, regex)
}

View file

@ -1,583 +0,0 @@
package ui
import (
"fmt"
"github.com/sirupsen/logrus"
"github.com/spf13/viper"
"github.com/wagoodman/dive/utils"
"github.com/wagoodman/keybinding"
"log"
"regexp"
"strings"
"github.com/jroimartin/gocui"
"github.com/lunixbochs/vtclean"
"github.com/wagoodman/dive/filetree"
)
const (
CompareLayer CompareType = iota
CompareAll
)
type CompareType int
// FileTreeView holds the UI objects and data models for populating the right pane. Specifically the pane that
// shows selected layer or aggregate file ASCII tree.
type FileTreeView struct {
Name string
gui *gocui.Gui
view *gocui.View
header *gocui.View
ModelTree *filetree.FileTree
ViewTree *filetree.FileTree
RefTrees []*filetree.FileTree
cache filetree.TreeCache
HiddenDiffTypes []bool
TreeIndex uint
bufferIndex uint
bufferIndexUpperBound uint
bufferIndexLowerBound uint
keybindingToggleCollapse []keybinding.Key
keybindingToggleAdded []keybinding.Key
keybindingToggleRemoved []keybinding.Key
keybindingToggleModified []keybinding.Key
keybindingToggleUnchanged []keybinding.Key
keybindingPageDown []keybinding.Key
keybindingPageUp []keybinding.Key
}
// NewFileTreeView creates a new view object attached the the global [gocui] screen object.
func NewFileTreeView(name string, gui *gocui.Gui, tree *filetree.FileTree, refTrees []*filetree.FileTree, cache filetree.TreeCache) (treeView *FileTreeView) {
treeView = new(FileTreeView)
// populate main fields
treeView.Name = name
treeView.gui = gui
treeView.ModelTree = tree
treeView.RefTrees = refTrees
treeView.cache = cache
treeView.HiddenDiffTypes = make([]bool, 4)
hiddenTypes := viper.GetStringSlice("diff.hide")
for _, hType := range hiddenTypes {
switch t := strings.ToLower(hType); t {
case "added":
treeView.HiddenDiffTypes[filetree.Added] = true
case "removed":
treeView.HiddenDiffTypes[filetree.Removed] = true
case "changed":
treeView.HiddenDiffTypes[filetree.Changed] = true
case "unchanged":
treeView.HiddenDiffTypes[filetree.Unchanged] = true
default:
utils.PrintAndExit(fmt.Sprintf("unknown diff.hide value: %s", t))
}
}
var err error
treeView.keybindingToggleCollapse, err = keybinding.ParseAll(viper.GetString("keybinding.toggle-collapse-dir"))
if err != nil {
log.Panicln(err)
}
treeView.keybindingToggleAdded, err = keybinding.ParseAll(viper.GetString("keybinding.toggle-added-files"))
if err != nil {
log.Panicln(err)
}
treeView.keybindingToggleRemoved, err = keybinding.ParseAll(viper.GetString("keybinding.toggle-removed-files"))
if err != nil {
log.Panicln(err)
}
treeView.keybindingToggleModified, err = keybinding.ParseAll(viper.GetString("keybinding.toggle-modified-files"))
if err != nil {
log.Panicln(err)
}
treeView.keybindingToggleUnchanged, err = keybinding.ParseAll(viper.GetString("keybinding.toggle-unchanged-files"))
if err != nil {
log.Panicln(err)
}
treeView.keybindingPageUp, err = keybinding.ParseAll(viper.GetString("keybinding.page-up"))
if err != nil {
log.Panicln(err)
}
treeView.keybindingPageDown, err = keybinding.ParseAll(viper.GetString("keybinding.page-down"))
if err != nil {
log.Panicln(err)
}
return treeView
}
// Setup initializes the UI concerns within the context of a global [gocui] view object.
func (view *FileTreeView) Setup(v *gocui.View, header *gocui.View) error {
// set view options
view.view = v
view.view.Editable = false
view.view.Wrap = false
view.view.Frame = false
view.header = header
view.header.Editable = false
view.header.Wrap = false
view.header.Frame = false
// 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
}
if err := view.gui.SetKeybinding(view.Name, gocui.KeyArrowLeft, gocui.ModNone, func(*gocui.Gui, *gocui.View) error { return view.CursorLeft() }); err != nil {
return err
}
if err := view.gui.SetKeybinding(view.Name, gocui.KeyArrowRight, gocui.ModNone, func(*gocui.Gui, *gocui.View) error { return view.CursorRight() }); err != nil {
return err
}
for _, key := range view.keybindingPageUp {
if err := view.gui.SetKeybinding(view.Name, key.Value, key.Modifier, func(*gocui.Gui, *gocui.View) error { return view.PageUp() }); err != nil {
return err
}
}
for _, key := range view.keybindingPageDown {
if err := view.gui.SetKeybinding(view.Name, key.Value, key.Modifier, func(*gocui.Gui, *gocui.View) error { return view.PageDown() }); err != nil {
return err
}
}
for _, key := range view.keybindingToggleCollapse {
if err := view.gui.SetKeybinding(view.Name, key.Value, key.Modifier, func(*gocui.Gui, *gocui.View) error { return view.toggleCollapse() }); err != nil {
return err
}
}
for _, key := range view.keybindingToggleAdded {
if err := view.gui.SetKeybinding(view.Name, key.Value, key.Modifier, func(*gocui.Gui, *gocui.View) error { return view.toggleShowDiffType(filetree.Added) }); err != nil {
return err
}
}
for _, key := range view.keybindingToggleRemoved {
if err := view.gui.SetKeybinding(view.Name, key.Value, key.Modifier, func(*gocui.Gui, *gocui.View) error { return view.toggleShowDiffType(filetree.Removed) }); err != nil {
return err
}
}
for _, key := range view.keybindingToggleModified {
if err := view.gui.SetKeybinding(view.Name, key.Value, key.Modifier, func(*gocui.Gui, *gocui.View) error { return view.toggleShowDiffType(filetree.Changed) }); err != nil {
return err
}
}
for _, key := range view.keybindingToggleUnchanged {
if err := view.gui.SetKeybinding(view.Name, key.Value, key.Modifier, func(*gocui.Gui, *gocui.View) error { return view.toggleShowDiffType(filetree.Unchanged) }); err != nil {
return err
}
}
view.bufferIndexLowerBound = 0
view.bufferIndexUpperBound = view.height() // don't include the header or footer in the view size
view.Update()
view.Render()
return nil
}
// height obtains the height of the current pane (taking into account the lost space due to headers and footers).
func (view *FileTreeView) height() uint {
_, height := view.view.Size()
return uint(height - 2)
}
// IsVisible indicates if the file tree view pane is currently initialized
func (view *FileTreeView) IsVisible() bool {
if view == nil {
return false
}
return true
}
// resetCursor moves the cursor back to the top of the buffer and translates to the top of the buffer.
func (view *FileTreeView) resetCursor() {
view.view.SetCursor(0, 0)
view.TreeIndex = 0
view.bufferIndex = 0
view.bufferIndexLowerBound = 0
view.bufferIndexUpperBound = view.height()
}
// setTreeByLayer populates the view model by stacking the indicated image layer file trees.
func (view *FileTreeView) setTreeByLayer(bottomTreeStart, bottomTreeStop, topTreeStart, topTreeStop int) error {
if topTreeStop > len(view.RefTrees)-1 {
return fmt.Errorf("invalid layer index given: %d of %d", topTreeStop, len(view.RefTrees)-1)
}
newTree := view.cache.Get(bottomTreeStart, bottomTreeStop, topTreeStart, topTreeStop)
// preserve view state on copy
visitor := func(node *filetree.FileNode) error {
newNode, err := newTree.GetNode(node.Path())
if err == nil {
newNode.Data.ViewInfo = node.Data.ViewInfo
}
return nil
}
view.ModelTree.VisitDepthChildFirst(visitor, nil)
view.resetCursor()
view.ModelTree = newTree
view.Update()
return view.Render()
}
// doCursorUp performs the internal view's buffer adjustments on cursor up. Note: this is independent of the gocui buffer.
func (view *FileTreeView) doCursorUp() {
view.TreeIndex--
if view.TreeIndex < view.bufferIndexLowerBound {
view.bufferIndexUpperBound--
view.bufferIndexLowerBound--
}
if view.bufferIndex > 0 {
view.bufferIndex--
}
}
// doCursorDown performs the internal view's buffer adjustments on cursor down. Note: this is independent of the gocui buffer.
func (view *FileTreeView) doCursorDown() {
view.TreeIndex++
if view.TreeIndex > view.bufferIndexUpperBound {
view.bufferIndexUpperBound++
view.bufferIndexLowerBound++
}
view.bufferIndex++
if view.bufferIndex > view.height() {
view.bufferIndex = view.height()
}
}
// CursorDown moves the cursor down and renders the view.
// Note: we cannot use the gocui buffer since any state change requires writing the entire tree to the buffer.
// Instead we are keeping an upper and lower bounds of the tree string to render and only flushing
// this range into the view buffer. This is much faster when tree sizes are large.
func (view *FileTreeView) CursorDown() error {
view.doCursorDown()
return view.Render()
}
// CursorUp moves the cursor up and renders the view.
// Note: we cannot use the gocui buffer since any state change requires writing the entire tree to the buffer.
// Instead we are keeping an upper and lower bounds of the tree string to render and only flushing
// this range into the view buffer. This is much faster when tree sizes are large.
func (view *FileTreeView) CursorUp() error {
if view.TreeIndex > 0 {
view.doCursorUp()
return view.Render()
}
return nil
}
// CursorLeft moves the cursor up until we reach the Parent Node or top of the tree
func (view *FileTreeView) CursorLeft() error {
var visitor func(*filetree.FileNode) error
var evaluator func(*filetree.FileNode) bool
var dfsCounter, newIndex uint
oldIndex := view.TreeIndex
currentNode := view.getAbsPositionNode()
if currentNode == nil {
return nil
}
parentPath := currentNode.Parent.Path()
visitor = func(curNode *filetree.FileNode) error {
if strings.Compare(parentPath, curNode.Path()) == 0 {
newIndex = dfsCounter
}
dfsCounter++
return nil
}
var filterBytes []byte
var filterRegex *regexp.Regexp
read, err := Views.Filter.view.Read(filterBytes)
if read > 0 && err == nil {
regex, err := regexp.Compile(string(filterBytes))
if err == nil {
filterRegex = regex
}
}
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(visitor, evaluator)
if err != nil {
logrus.Panic(err)
}
view.TreeIndex = newIndex
moveIndex := oldIndex - newIndex
if newIndex < view.bufferIndexLowerBound {
view.bufferIndexUpperBound = view.TreeIndex + view.height()
view.bufferIndexLowerBound = view.TreeIndex
}
if view.bufferIndex > moveIndex {
view.bufferIndex = view.bufferIndex - moveIndex
} else {
view.bufferIndex = 0
}
view.Update()
return view.Render()
}
// CursorRight descends into directory expanding it if needed
func (view *FileTreeView) CursorRight() error {
node := view.getAbsPositionNode()
if node == nil {
return nil
}
if !node.Data.FileInfo.IsDir {
return nil
}
if len(node.Children) == 0 {
return nil
}
if node.Data.ViewInfo.Collapsed {
node.Data.ViewInfo.Collapsed = false
}
view.TreeIndex++
if view.TreeIndex > view.bufferIndexUpperBound {
view.bufferIndexUpperBound++
view.bufferIndexLowerBound++
}
view.bufferIndex++
if view.bufferIndex > view.height() {
view.bufferIndex = view.height()
}
view.Update()
return view.Render()
}
// PageDown moves to next page putting the cursor on top
func (view *FileTreeView) PageDown() error {
nextBufferIndexLowerBound := view.bufferIndexLowerBound + view.height()
nextBufferIndexUpperBound := view.bufferIndexUpperBound + view.height()
treeString := view.ViewTree.StringBetween(nextBufferIndexLowerBound, nextBufferIndexUpperBound, true)
lines := strings.Split(treeString, "\n")
newLines := uint(len(lines)) - 1
if view.height() >= newLines {
nextBufferIndexLowerBound = view.bufferIndexLowerBound + newLines
nextBufferIndexUpperBound = view.bufferIndexUpperBound + newLines
}
view.bufferIndexLowerBound = nextBufferIndexLowerBound
view.bufferIndexUpperBound = nextBufferIndexUpperBound
if view.TreeIndex < nextBufferIndexLowerBound {
view.bufferIndex = 0
view.TreeIndex = nextBufferIndexLowerBound
} else {
view.bufferIndex = view.bufferIndex - newLines
}
return view.Render()
}
// PageUp moves to previous page putting the cursor on top
func (view *FileTreeView) PageUp() error {
nextBufferIndexLowerBound := view.bufferIndexLowerBound - view.height()
nextBufferIndexUpperBound := view.bufferIndexUpperBound - view.height()
treeString := view.ViewTree.StringBetween(nextBufferIndexLowerBound, nextBufferIndexUpperBound, true)
lines := strings.Split(treeString, "\n")
newLines := uint(len(lines)) - 2
if view.height() >= newLines {
nextBufferIndexLowerBound = view.bufferIndexLowerBound - newLines
nextBufferIndexUpperBound = view.bufferIndexUpperBound - newLines
}
view.bufferIndexLowerBound = nextBufferIndexLowerBound
view.bufferIndexUpperBound = nextBufferIndexUpperBound
if view.TreeIndex > (nextBufferIndexUpperBound - 1) {
view.bufferIndex = 0
view.TreeIndex = nextBufferIndexLowerBound
} else {
view.bufferIndex = view.bufferIndex + newLines
}
return view.Render()
}
// getAbsPositionNode determines the selected screen cursor's location in the file tree, returning the selected FileNode.
func (view *FileTreeView) getAbsPositionNode() (node *filetree.FileNode) {
var visitor func(*filetree.FileNode) error
var evaluator func(*filetree.FileNode) bool
var dfsCounter uint
visitor = func(curNode *filetree.FileNode) error {
if dfsCounter == view.TreeIndex {
node = curNode
}
dfsCounter++
return nil
}
var filterBytes []byte
var filterRegex *regexp.Regexp
read, err := Views.Filter.view.Read(filterBytes)
if read > 0 && err == nil {
regex, err := regexp.Compile(string(filterBytes))
if err == nil {
filterRegex = regex
}
}
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(visitor, evaluator)
if err != nil {
logrus.Panic(err)
}
return node
}
// toggleCollapse will collapse/expand the selected FileNode.
func (view *FileTreeView) toggleCollapse() error {
node := view.getAbsPositionNode()
if node != nil {
node.Data.ViewInfo.Collapsed = !node.Data.ViewInfo.Collapsed
}
view.Update()
return view.Render()
}
// toggleShowDiffType will show/hide the selected DiffType in the filetree pane.
func (view *FileTreeView) toggleShowDiffType(diffType filetree.DiffType) error {
view.HiddenDiffTypes[diffType] = !view.HiddenDiffTypes[diffType]
view.resetCursor()
Update()
Render()
return nil
}
// filterRegex will return a regular expression object to match the user's filter input.
func filterRegex() *regexp.Regexp {
if Views.Filter == nil || Views.Filter.view == nil {
return nil
}
filterString := strings.TrimSpace(Views.Filter.view.Buffer())
if len(filterString) == 0 {
return nil
}
regex, err := regexp.Compile(filterString)
if err != nil {
return nil
}
return regex
}
// Update refreshes the state objects for future rendering.
func (view *FileTreeView) Update() error {
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)
// make a new tree with only visible nodes
view.ViewTree = view.ModelTree.Copy()
view.ViewTree.VisitDepthParentFirst(func(node *filetree.FileNode) error {
if node.Data.ViewInfo.Hidden {
view.ViewTree.RemovePath(node.Path())
}
return nil
}, nil)
return nil
}
// Render flushes the state objects (file tree) to the pane.
func (view *FileTreeView) Render() error {
treeString := view.ViewTree.StringBetween(view.bufferIndexLowerBound, view.bufferIndexUpperBound, true)
lines := strings.Split(treeString, "\n")
// undo a cursor down that has gone past bottom of the visible tree
if view.bufferIndex >= uint(len(lines))-1 {
view.doCursorUp()
}
title := "Current Layer Contents"
if Views.Layer.CompareMode == CompareAll {
title = "Aggregated Layer Contents"
}
// indicate when selected
if view.gui.CurrentView() == view.view {
title = "● " + title
}
view.gui.Update(func(g *gocui.Gui) error {
// update the header
view.header.Clear()
width, _ := g.Size()
headerStr := fmt.Sprintf("[%s]%s\n", title, strings.Repeat("─", width*2))
headerStr += fmt.Sprintf(filetree.AttributeFormat+" %s", "P", "ermission", "UID:GID", "Size", "Filetree")
fmt.Fprintln(view.header, Formatting.Header(vtclean.Clean(headerStr, false)))
// update the contents
view.view.Clear()
for idx, line := range lines {
if uint(idx) == view.bufferIndex {
fmt.Fprintln(view.view, Formatting.Selected(vtclean.Clean(line, false)))
} else {
fmt.Fprintln(view.view, line)
}
}
// todo: should we check error on the view println?
return nil
})
return nil
}
// KeyHelp indicates all the possible actions a user can take while the current pane is selected.
func (view *FileTreeView) KeyHelp() string {
return renderStatusOption(view.keybindingToggleCollapse[0].String(), "Collapse dir", false) +
renderStatusOption(view.keybindingToggleAdded[0].String(), "Added files", !view.HiddenDiffTypes[filetree.Added]) +
renderStatusOption(view.keybindingToggleRemoved[0].String(), "Removed files", !view.HiddenDiffTypes[filetree.Removed]) +
renderStatusOption(view.keybindingToggleModified[0].String(), "Modified files", !view.HiddenDiffTypes[filetree.Changed]) +
renderStatusOption(view.keybindingToggleUnchanged[0].String(), "Unmodified files", !view.HiddenDiffTypes[filetree.Unchanged])
}

114
ui/filter_controller.go Normal file
View file

@ -0,0 +1,114 @@
package ui
import (
"fmt"
"github.com/jroimartin/gocui"
)
// FilterController holds the UI objects and data models for populating the bottom row. Specifically the pane that
// allows the user to filter the file tree by path.
type FilterController struct {
Name string
gui *gocui.Gui
view *gocui.View
header *gocui.View
headerStr string
maxLength int
hidden bool
}
// NewFilterController creates a new view object attached the the global [gocui] screen object.
func NewFilterController(name string, gui *gocui.Gui) (controller *FilterController) {
controller = new(FilterController)
// populate main fields
controller.Name = name
controller.gui = gui
controller.headerStr = "Path Filter: "
controller.hidden = true
return controller
}
// Setup initializes the UI concerns within the context of a global [gocui] view object.
func (controller *FilterController) Setup(v *gocui.View, header *gocui.View) error {
// set controller options
controller.view = v
controller.maxLength = 200
controller.view.Frame = false
controller.view.BgColor = gocui.AttrReverse
controller.view.Editable = true
controller.view.Editor = controller
controller.header = header
controller.header.BgColor = gocui.AttrReverse
controller.header.Editable = false
controller.header.Wrap = false
controller.header.Frame = false
return controller.Render()
}
// IsVisible indicates if the filter view pane is currently initialized
func (controller *FilterController) IsVisible() bool {
if controller == nil {
return false
}
return !controller.hidden
}
// CursorDown moves the cursor down in the filter pane (currently indicates nothing).
func (controller *FilterController) CursorDown() error {
return nil
}
// CursorUp moves the cursor up in the filter pane (currently indicates nothing).
func (controller *FilterController) CursorUp() error {
return nil
}
// Edit intercepts the key press events in the filer view to update the file view in real time.
func (controller *FilterController) Edit(v *gocui.View, key gocui.Key, ch rune, mod gocui.Modifier) {
if !controller.IsVisible() {
return
}
cx, _ := v.Cursor()
ox, _ := v.Origin()
limit := ox+cx+1 > controller.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 Controllers.Tree != nil {
_ = Controllers.Tree.Update()
_ = Controllers.Tree.Render()
}
}
// Update refreshes the state objects for future rendering (currently does nothing).
func (controller *FilterController) Update() error {
return nil
}
// Render flushes the state objects to the screen. Currently this is the users path filter input.
func (controller *FilterController) Render() error {
controller.gui.Update(func(g *gocui.Gui) error {
// render the header
_, err := fmt.Fprintln(controller.header, Formatting.Header(controller.headerStr))
return err
})
return nil
}
// KeyHelp indicates all the possible actions a user can take while the current pane is selected.
func (controller *FilterController) KeyHelp() string {
return Formatting.StatusControlNormal("▏Type to filter the file tree ")
}

View file

@ -1,116 +0,0 @@
package ui
import (
"fmt"
"github.com/jroimartin/gocui"
)
// DetailsView holds the UI objects and data models for populating the bottom row. Specifically the pane that
// allows the user to filter the file tree by path.
type FilterView struct {
Name string
gui *gocui.Gui
view *gocui.View
header *gocui.View
headerStr string
maxLength int
hidden bool
}
// NewFilterView creates a new view object attached the the global [gocui] screen object.
func NewFilterView(name string, gui *gocui.Gui) (filterView *FilterView) {
filterView = new(FilterView)
// populate main fields
filterView.Name = name
filterView.gui = gui
filterView.headerStr = "Path Filter: "
filterView.hidden = true
return filterView
}
// Setup initializes the UI concerns within the context of a global [gocui] view object.
func (view *FilterView) 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.AttrReverse
view.view.Editable = true
view.view.Editor = view
view.header = header
view.header.BgColor = gocui.AttrReverse
view.header.Editable = false
view.header.Wrap = false
view.header.Frame = false
view.Render()
return nil
}
// IsVisible indicates if the filter view pane is currently initialized
func (view *FilterView) IsVisible() bool {
if view == nil {
return false
}
return !view.hidden
}
// CursorDown moves the cursor down in the filter pane (currently indicates nothing).
func (view *FilterView) CursorDown() error {
return nil
}
// CursorUp moves the cursor up in the filter pane (currently indicates nothing).
func (view *FilterView) CursorUp() error {
return nil
}
// Edit intercepts the key press events in the filer view to update the file view in real time.
func (view *FilterView) Edit(v *gocui.View, key gocui.Key, ch rune, mod gocui.Modifier) {
if !view.IsVisible() {
return
}
cx, _ := v.Cursor()
ox, _ := v.Origin()
limit := ox+cx+1 > view.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.Update()
Views.Tree.Render()
}
}
// Update refreshes the state objects for future rendering (currently does nothing).
func (view *FilterView) Update() error {
return nil
}
// Render flushes the state objects to the screen. Currently this is the users path filter input.
func (view *FilterView) Render() error {
view.gui.Update(func(g *gocui.Gui) error {
// render the header
fmt.Fprintln(view.header, Formatting.Header(view.headerStr))
return nil
})
return nil
}
// KeyHelp indicates all the possible actions a user can take while the current pane is selected.
func (view *FilterView) KeyHelp() string {
return Formatting.StatusControlNormal("▏Type to filter the file tree ")
}

313
ui/layer_controller.go Normal file
View file

@ -0,0 +1,313 @@
package ui
import (
"fmt"
"github.com/jroimartin/gocui"
"github.com/lunixbochs/vtclean"
"github.com/sirupsen/logrus"
"github.com/spf13/viper"
"github.com/wagoodman/dive/image"
"github.com/wagoodman/dive/utils"
"github.com/wagoodman/keybinding"
"strings"
)
// LayerController holds the UI objects and data models for populating the lower-left pane. Specifically the pane that
// shows the image layers and layer selector.
type LayerController struct {
Name string
gui *gocui.Gui
view *gocui.View
header *gocui.View
LayerIndex int
Layers []image.Layer
CompareMode CompareType
CompareStartIndex int
ImageSize uint64
keybindingCompareAll []keybinding.Key
keybindingCompareLayer []keybinding.Key
keybindingPageDown []keybinding.Key
keybindingPageUp []keybinding.Key
}
// NewLayerController creates a new view object attached the the global [gocui] screen object.
func NewLayerController(name string, gui *gocui.Gui, layers []image.Layer) (controller *LayerController) {
controller = new(LayerController)
// populate main fields
controller.Name = name
controller.gui = gui
controller.Layers = layers
switch mode := viper.GetBool("layer.show-aggregated-changes"); mode {
case true:
controller.CompareMode = CompareAll
case false:
controller.CompareMode = CompareLayer
default:
utils.PrintAndExit(fmt.Sprintf("unknown layer.show-aggregated-changes value: %v", mode))
}
var err error
controller.keybindingCompareAll, err = keybinding.ParseAll(viper.GetString("keybinding.compare-all"))
if err != nil {
logrus.Error(err)
}
controller.keybindingCompareLayer, err = keybinding.ParseAll(viper.GetString("keybinding.compare-layer"))
if err != nil {
logrus.Error(err)
}
controller.keybindingPageUp, err = keybinding.ParseAll(viper.GetString("keybinding.page-up"))
if err != nil {
logrus.Error(err)
}
controller.keybindingPageDown, err = keybinding.ParseAll(viper.GetString("keybinding.page-down"))
if err != nil {
logrus.Error(err)
}
return controller
}
// Setup initializes the UI concerns within the context of a global [gocui] view object.
func (controller *LayerController) Setup(v *gocui.View, header *gocui.View) error {
// set controller options
controller.view = v
controller.view.Editable = false
controller.view.Wrap = false
controller.view.Frame = false
controller.header = header
controller.header.Editable = false
controller.header.Wrap = false
controller.header.Frame = false
// set keybindings
if err := controller.gui.SetKeybinding(controller.Name, gocui.KeyArrowDown, gocui.ModNone, func(*gocui.Gui, *gocui.View) error { return controller.CursorDown() }); err != nil {
return err
}
if err := controller.gui.SetKeybinding(controller.Name, gocui.KeyArrowUp, gocui.ModNone, func(*gocui.Gui, *gocui.View) error { return controller.CursorUp() }); err != nil {
return err
}
if err := controller.gui.SetKeybinding(controller.Name, gocui.KeyArrowRight, gocui.ModNone, func(*gocui.Gui, *gocui.View) error { return controller.CursorDown() }); err != nil {
return err
}
if err := controller.gui.SetKeybinding(controller.Name, gocui.KeyArrowLeft, gocui.ModNone, func(*gocui.Gui, *gocui.View) error { return controller.CursorUp() }); err != nil {
return err
}
for _, key := range controller.keybindingPageUp {
if err := controller.gui.SetKeybinding(controller.Name, key.Value, key.Modifier, func(*gocui.Gui, *gocui.View) error { return controller.PageUp() }); err != nil {
return err
}
}
for _, key := range controller.keybindingPageDown {
if err := controller.gui.SetKeybinding(controller.Name, key.Value, key.Modifier, func(*gocui.Gui, *gocui.View) error { return controller.PageDown() }); err != nil {
return err
}
}
for _, key := range controller.keybindingCompareLayer {
if err := controller.gui.SetKeybinding(controller.Name, key.Value, key.Modifier, func(*gocui.Gui, *gocui.View) error { return controller.setCompareMode(CompareLayer) }); err != nil {
return err
}
}
for _, key := range controller.keybindingCompareAll {
if err := controller.gui.SetKeybinding(controller.Name, key.Value, key.Modifier, func(*gocui.Gui, *gocui.View) error { return controller.setCompareMode(CompareAll) }); err != nil {
return err
}
}
return controller.Render()
}
// height obtains the height of the current pane (taking into account the lost space due to the header).
func (controller *LayerController) height() uint {
_, height := controller.view.Size()
return uint(height - 1)
}
// IsVisible indicates if the layer view pane is currently initialized.
func (controller *LayerController) IsVisible() bool {
return controller != nil
}
// PageDown moves to next page putting the cursor on top
func (controller *LayerController) PageDown() error {
step := int(controller.height()) + 1
targetLayerIndex := controller.LayerIndex + step
if targetLayerIndex > len(controller.Layers) {
step -= targetLayerIndex - (len(controller.Layers) - 1)
}
if step > 0 {
err := CursorStep(controller.gui, controller.view, step)
if err == nil {
return controller.SetCursor(controller.LayerIndex + step)
}
}
return nil
}
// PageUp moves to previous page putting the cursor on top
func (controller *LayerController) PageUp() error {
step := int(controller.height()) + 1
targetLayerIndex := controller.LayerIndex - step
if targetLayerIndex < 0 {
step += targetLayerIndex
}
if step > 0 {
err := CursorStep(controller.gui, controller.view, -step)
if err == nil {
return controller.SetCursor(controller.LayerIndex - step)
}
}
return nil
}
// CursorDown moves the cursor down in the layer pane (selecting a higher layer).
func (controller *LayerController) CursorDown() error {
if controller.LayerIndex < len(controller.Layers) {
err := CursorDown(controller.gui, controller.view)
if err == nil {
return controller.SetCursor(controller.LayerIndex + 1)
}
}
return nil
}
// CursorUp moves the cursor up in the layer pane (selecting a lower layer).
func (controller *LayerController) CursorUp() error {
if controller.LayerIndex > 0 {
err := CursorUp(controller.gui, controller.view)
if err == nil {
return controller.SetCursor(controller.LayerIndex - 1)
}
}
return nil
}
// SetCursor resets the cursor and orients the file tree view based on the given layer index.
func (controller *LayerController) SetCursor(layer int) error {
controller.LayerIndex = layer
err := Controllers.Tree.setTreeByLayer(controller.getCompareIndexes())
if err != nil {
return err
}
_ = Controllers.Details.Render()
return controller.Render()
}
// currentLayer returns the Layer object currently selected.
func (controller *LayerController) currentLayer() image.Layer {
return controller.Layers[(len(controller.Layers)-1)-controller.LayerIndex]
}
// setCompareMode switches the layer comparison between a single-layer comparison to an aggregated comparison.
func (controller *LayerController) setCompareMode(compareMode CompareType) error {
controller.CompareMode = compareMode
Update()
Render()
return Controllers.Tree.setTreeByLayer(controller.getCompareIndexes())
}
// getCompareIndexes determines the layer boundaries to use for comparison (based on the current compare mode)
func (controller *LayerController) getCompareIndexes() (bottomTreeStart, bottomTreeStop, topTreeStart, topTreeStop int) {
bottomTreeStart = controller.CompareStartIndex
topTreeStop = controller.LayerIndex
if controller.LayerIndex == controller.CompareStartIndex {
bottomTreeStop = controller.LayerIndex
topTreeStart = controller.LayerIndex
} else if controller.CompareMode == CompareLayer {
bottomTreeStop = controller.LayerIndex - 1
topTreeStart = controller.LayerIndex
} else {
bottomTreeStop = controller.CompareStartIndex
topTreeStart = controller.CompareStartIndex + 1
}
return bottomTreeStart, bottomTreeStop, topTreeStart, topTreeStop
}
// renderCompareBar returns the formatted string for the given layer.
func (controller *LayerController) renderCompareBar(layerIdx int) string {
bottomTreeStart, bottomTreeStop, topTreeStart, topTreeStop := controller.getCompareIndexes()
result := " "
if layerIdx >= bottomTreeStart && layerIdx <= bottomTreeStop {
result = Formatting.CompareBottom(" ")
}
if layerIdx >= topTreeStart && layerIdx <= topTreeStop {
result = Formatting.CompareTop(" ")
}
return result
}
// Update refreshes the state objects for future rendering (currently does nothing).
func (controller *LayerController) Update() error {
controller.ImageSize = 0
for idx := 0; idx < len(controller.Layers); idx++ {
controller.ImageSize += controller.Layers[idx].Size()
}
return nil
}
// Render flushes the state objects to the screen. The layers pane reports:
// 1. the layers of the image + metadata
// 2. the current selected image
func (controller *LayerController) Render() error {
// indicate when selected
title := "Layers"
if controller.gui.CurrentView() == controller.view {
title = "● " + title
}
controller.gui.Update(func(g *gocui.Gui) error {
// update header
controller.header.Clear()
width, _ := g.Size()
headerStr := fmt.Sprintf("[%s]%s\n", title, strings.Repeat("─", width*2))
// headerStr += fmt.Sprintf("Cmp "+image.LayerFormat, "Layer Digest", "Size", "Command")
headerStr += fmt.Sprintf("Cmp"+image.LayerFormat, "Size", "Command")
_, _ = fmt.Fprintln(controller.header, Formatting.Header(vtclean.Clean(headerStr, false)))
// update contents
controller.view.Clear()
for revIdx := len(controller.Layers) - 1; revIdx >= 0; revIdx-- {
layer := controller.Layers[revIdx]
idx := (len(controller.Layers) - 1) - revIdx
layerStr := layer.String()
compareBar := controller.renderCompareBar(idx)
if idx == controller.LayerIndex {
_, _ = fmt.Fprintln(controller.view, compareBar+" "+Formatting.Selected(layerStr))
} else {
_, _ = fmt.Fprintln(controller.view, compareBar+" "+layerStr)
}
}
return nil
})
return nil
}
// KeyHelp indicates all the possible actions a user can take while the current pane is selected.
func (controller *LayerController) KeyHelp() string {
return renderStatusOption(controller.keybindingCompareLayer[0].String(), "Show layer changes", controller.CompareMode == CompareLayer) +
renderStatusOption(controller.keybindingCompareAll[0].String(), "Show aggregated changes", controller.CompareMode == CompareAll)
}

View file

@ -1,242 +0,0 @@
package ui
import (
"fmt"
"github.com/spf13/viper"
"github.com/wagoodman/dive/utils"
"github.com/wagoodman/keybinding"
"log"
"github.com/jroimartin/gocui"
"github.com/lunixbochs/vtclean"
"github.com/wagoodman/dive/image"
"strings"
)
// LayerView holds the UI objects and data models for populating the lower-left pane. Specifically the pane that
// shows the image layers and layer selector.
type LayerView struct {
Name string
gui *gocui.Gui
view *gocui.View
header *gocui.View
LayerIndex int
Layers []image.Layer
CompareMode CompareType
CompareStartIndex int
ImageSize uint64
keybindingCompareAll []keybinding.Key
keybindingCompareLayer []keybinding.Key
}
// NewDetailsView creates a new view object attached the the global [gocui] screen object.
func NewLayerView(name string, gui *gocui.Gui, layers []image.Layer) (layerView *LayerView) {
layerView = new(LayerView)
// populate main fields
layerView.Name = name
layerView.gui = gui
layerView.Layers = layers
switch mode := viper.GetBool("layer.show-aggregated-changes"); mode {
case true:
layerView.CompareMode = CompareAll
case false:
layerView.CompareMode = CompareLayer
default:
utils.PrintAndExit(fmt.Sprintf("unknown layer.show-aggregated-changes value: %v", mode))
}
var err error
layerView.keybindingCompareAll, err = keybinding.ParseAll(viper.GetString("keybinding.compare-all"))
if err != nil {
log.Panicln(err)
}
layerView.keybindingCompareLayer, err = keybinding.ParseAll(viper.GetString("keybinding.compare-layer"))
if err != nil {
log.Panicln(err)
}
return layerView
}
// Setup initializes the UI concerns within the context of a global [gocui] view object.
func (view *LayerView) Setup(v *gocui.View, header *gocui.View) error {
// set view options
view.view = v
view.view.Editable = false
view.view.Wrap = false
view.view.Frame = false
view.header = header
view.header.Editable = false
view.header.Wrap = false
view.header.Frame = false
// 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
}
for _, key := range view.keybindingCompareLayer {
if err := view.gui.SetKeybinding(view.Name, key.Value, key.Modifier, func(*gocui.Gui, *gocui.View) error { return view.setCompareMode(CompareLayer) }); err != nil {
return err
}
}
for _, key := range view.keybindingCompareAll {
if err := view.gui.SetKeybinding(view.Name, key.Value, key.Modifier, func(*gocui.Gui, *gocui.View) error { return view.setCompareMode(CompareAll) }); err != nil {
return err
}
}
return view.Render()
}
// IsVisible indicates if the layer view pane is currently initialized.
func (view *LayerView) IsVisible() bool {
if view == nil {
return false
}
return true
}
// CursorDown moves the cursor down in the layer pane (selecting a higher layer).
func (view *LayerView) CursorDown() error {
if view.LayerIndex < len(view.Layers) {
err := CursorDown(view.gui, view.view)
if err == nil {
view.SetCursor(view.LayerIndex + 1)
}
}
return nil
}
// CursorUp moves the cursor up in the layer pane (selecting a lower layer).
func (view *LayerView) CursorUp() error {
if view.LayerIndex > 0 {
err := CursorUp(view.gui, view.view)
if err == nil {
view.SetCursor(view.LayerIndex - 1)
}
}
return nil
}
// SetCursor resets the cursor and orients the file tree view based on the given layer index.
func (view *LayerView) SetCursor(layer int) error {
view.LayerIndex = layer
Views.Tree.setTreeByLayer(view.getCompareIndexes())
Views.Details.Render()
view.Render()
return nil
}
// currentLayer returns the Layer object currently selected.
func (view *LayerView) currentLayer() image.Layer {
return view.Layers[(len(view.Layers)-1)-view.LayerIndex]
}
// setCompareMode switches the layer comparison between a single-layer comparison to an aggregated comparison.
func (view *LayerView) setCompareMode(compareMode CompareType) error {
view.CompareMode = compareMode
Update()
Render()
return Views.Tree.setTreeByLayer(view.getCompareIndexes())
}
// getCompareIndexes determines the layer boundaries to use for comparison (based on the current compare mode)
func (view *LayerView) getCompareIndexes() (bottomTreeStart, bottomTreeStop, topTreeStart, topTreeStop int) {
bottomTreeStart = view.CompareStartIndex
topTreeStop = view.LayerIndex
if view.LayerIndex == view.CompareStartIndex {
bottomTreeStop = view.LayerIndex
topTreeStart = view.LayerIndex
} else if view.CompareMode == CompareLayer {
bottomTreeStop = view.LayerIndex - 1
topTreeStart = view.LayerIndex
} else {
bottomTreeStop = view.CompareStartIndex
topTreeStart = view.CompareStartIndex + 1
}
return bottomTreeStart, bottomTreeStop, topTreeStart, topTreeStop
}
// renderCompareBar returns the formatted string for the given layer.
func (view *LayerView) renderCompareBar(layerIdx int) string {
bottomTreeStart, bottomTreeStop, topTreeStart, topTreeStop := view.getCompareIndexes()
result := " "
if layerIdx >= bottomTreeStart && layerIdx <= bottomTreeStop {
result = Formatting.CompareBottom(" ")
}
if layerIdx >= topTreeStart && layerIdx <= topTreeStop {
result = Formatting.CompareTop(" ")
}
return result
}
// Update refreshes the state objects for future rendering (currently does nothing).
func (view *LayerView) Update() error {
view.ImageSize = 0
for idx := 0; idx < len(view.Layers); idx++ {
view.ImageSize += view.Layers[idx].Size()
}
return nil
}
// Render flushes the state objects to the screen. The layers pane reports:
// 1. the layers of the image + metadata
// 2. the current selected image
func (view *LayerView) Render() error {
// indicate when selected
title := "Layers"
if view.gui.CurrentView() == view.view {
title = "● " + title
}
view.gui.Update(func(g *gocui.Gui) error {
// update header
view.header.Clear()
width, _ := g.Size()
headerStr := fmt.Sprintf("[%s]%s\n", title, strings.Repeat("─", width*2))
headerStr += fmt.Sprintf("Cmp "+image.LayerFormat, "Image ID", "Size", "Command")
fmt.Fprintln(view.header, Formatting.Header(vtclean.Clean(headerStr, false)))
// update contents
view.view.Clear()
for revIdx := len(view.Layers) - 1; revIdx >= 0; revIdx-- {
layer := view.Layers[revIdx]
idx := (len(view.Layers) - 1) - revIdx
layerStr := layer.String()
compareBar := view.renderCompareBar(idx)
if idx == view.LayerIndex {
fmt.Fprintln(view.view, compareBar+" "+Formatting.Selected(layerStr))
} else {
fmt.Fprintln(view.view, compareBar+" "+layerStr)
}
}
return nil
})
return nil
}
// KeyHelp indicates all the possible actions a user can take while the current pane is selected.
func (view *LayerView) KeyHelp() string {
return renderStatusOption(view.keybindingCompareLayer[0].String(), "Show layer changes", view.CompareMode == CompareLayer) +
renderStatusOption(view.keybindingCompareAll[0].String(), "Show aggregated changes", view.CompareMode == CompareAll)
}

76
ui/status_controller.go Normal file
View file

@ -0,0 +1,76 @@
package ui
import (
"fmt"
"github.com/jroimartin/gocui"
"strings"
)
// StatusController holds the UI objects and data models for populating the bottom-most pane. Specifically the panel
// shows the user a set of possible actions to take in the window and currently selected pane.
type StatusController struct {
Name string
gui *gocui.Gui
view *gocui.View
}
// NewStatusController creates a new view object attached the the global [gocui] screen object.
func NewStatusController(name string, gui *gocui.Gui) (controller *StatusController) {
controller = new(StatusController)
// populate main fields
controller.Name = name
controller.gui = gui
return controller
}
// Setup initializes the UI concerns within the context of a global [gocui] view object.
func (controller *StatusController) Setup(v *gocui.View, header *gocui.View) error {
// set controller options
controller.view = v
controller.view.Frame = false
return controller.Render()
}
// IsVisible indicates if the status view pane is currently initialized.
func (controller *StatusController) IsVisible() bool {
return controller != nil
}
// CursorDown moves the cursor down in the details pane (currently indicates nothing).
func (controller *StatusController) CursorDown() error {
return nil
}
// CursorUp moves the cursor up in the details pane (currently indicates nothing).
func (controller *StatusController) CursorUp() error {
return nil
}
// Update refreshes the state objects for future rendering (currently does nothing).
func (controller *StatusController) Update() error {
return nil
}
// Render flushes the state objects to the screen.
func (controller *StatusController) Render() error {
controller.gui.Update(func(g *gocui.Gui) error {
controller.view.Clear()
_, _ = fmt.Fprintln(controller.view, controller.KeyHelp()+Controllers.lookup[controller.gui.CurrentView().Name()].KeyHelp()+Formatting.StatusNormal("▏"+strings.Repeat(" ", 1000)))
return nil
})
// todo: blerg
return nil
}
// KeyHelp indicates all the possible global actions a user can take when any pane is selected.
func (controller *StatusController) KeyHelp() string {
return renderStatusOption(GlobalKeybindings.quit[0].String(), "Quit", false) +
renderStatusOption(GlobalKeybindings.toggleView[0].String(), "Switch view", false) +
renderStatusOption(GlobalKeybindings.filterView[0].String(), "Filter", Controllers.Filter.IsVisible())
}

View file

@ -1,81 +0,0 @@
package ui
import (
"fmt"
"github.com/jroimartin/gocui"
"strings"
)
// DetailsView holds the UI objects and data models for populating the bottom-most pane. Specifcially the panel
// shows the user a set of possible actions to take in the window and currently selected pane.
type StatusView struct {
Name string
gui *gocui.Gui
view *gocui.View
}
// NewStatusView creates a new view object attached the the global [gocui] screen object.
func NewStatusView(name string, gui *gocui.Gui) (statusView *StatusView) {
statusView = new(StatusView)
// populate main fields
statusView.Name = name
statusView.gui = gui
return statusView
}
// Setup initializes the UI concerns within the context of a global [gocui] view object.
func (view *StatusView) Setup(v *gocui.View, header *gocui.View) error {
// set view options
view.view = v
view.view.Frame = false
view.Render()
return nil
}
// IsVisible indicates if the status view pane is currently initialized.
func (view *StatusView) IsVisible() bool {
if view == nil {
return false
}
return true
}
// CursorDown moves the cursor down in the details pane (currently indicates nothing).
func (view *StatusView) CursorDown() error {
return nil
}
// CursorUp moves the cursor up in the details pane (currently indicates nothing).
func (view *StatusView) CursorUp() error {
return nil
}
// Update refreshes the state objects for future rendering (currently does nothing).
func (view *StatusView) Update() error {
return nil
}
// Render flushes the state objects to the screen.
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()+Formatting.StatusNormal("▏"+strings.Repeat(" ", 1000)))
return nil
})
// todo: blerg
return nil
}
// KeyHelp indicates all the possible global actions a user can take when any pane is selected.
func (view *StatusView) KeyHelp() string {
return renderStatusOption(GlobalKeybindings.quit[0].String(), "Quit", false) +
renderStatusOption(GlobalKeybindings.toggleView[0].String(), "Switch view", false) +
renderStatusOption(GlobalKeybindings.filterView[0].String(), "Filter files", Views.Filter.IsVisible())
}

View file

@ -0,0 +1,36 @@
drwxr-xr-x 0:0 1.2 MB ├─⊕ bin
drwxr-xr-x 0:0 0 B ├── dev
drwxr-xr-x 0:0 1.0 kB ├── etc
-rw-rw-r-- 0:0 307 B │ ├── group
-rw-r--r-- 0:0 127 B │ ├── localtime
drwxr-xr-x 0:0 0 B │ ├── network
drwxr-xr-x 0:0 0 B │ │ ├── if-down.d
drwxr-xr-x 0:0 0 B │ │ ├── if-post-down.d
drwxr-xr-x 0:0 0 B │ │ ├── if-pre-up.d
drwxr-xr-x 0:0 0 B │ │ └── if-up.d
-rw-r--r-- 0:0 340 B │ ├── passwd
-rw------- 0:0 243 B │ └── shadow
drwxr-xr-x 65534:65534 0 B ├── home
drwx------ 0:0 21 kB ├── root
drwxr-xr-x 0:0 8.6 kB │ ├── .data
-rw-r--r-- 0:0 6.4 kB │ │ ├── saved.again2.txt
-rwxrwxr-x 0:0 917 B │ │ ├── tag.sh
-rwxr-xr-x 0:0 1.3 kB │ │ └── test.sh
-rw-r--r-- 0:0 6.4 kB │ ├── .saved.txt
drwxr-xr-x 0:0 19 kB │ ├── example
drwxr-xr-x 0:0 0 B │ │ ├── really
drwxr-xr-x 0:0 0 B │ │ │ └── nested
-r--r--r-- 0:0 6.4 kB │ │ ├── somefile1.txt
-rw-r--r-- 0:0 6.4 kB │ │ ├── somefile2.txt
-rw-r--r-- 0:0 6.4 kB │ │ └── somefile3.txt
-rwxr-xr-x 0:0 6.4 kB │ └── saved.txt
-rw-rw-r-- 0:0 6.4 kB ├── somefile.txt
drwxrwxrwx 0:0 6.4 kB ├── tmp
-rw-r--r-- 0:0 6.4 kB │ └── saved.again1.txt
drwxr-xr-x 0:0 0 B ├── usr
drwxr-xr-x 1:1 0 B │ └── sbin
drwxr-xr-x 0:0 0 B └── var
drwxr-xr-x 0:0 0 B ├── spool
drwxr-xr-x 8:8 0 B │ └── mail
drwxr-xr-x 0:0 0 B └── www

13
ui/testdata/TestFileTreeDirCollapse.txt vendored Normal file
View file

@ -0,0 +1,13 @@
drwxr-xr-x 0:0 1.2 MB ├─⊕ bin
drwxr-xr-x 0:0 0 B ├── dev
drwxr-xr-x 0:0 1.0 kB ├─⊕ etc
drwxr-xr-x 65534:65534 0 B ├── home
drwx------ 0:0 0 B ├── root
drwxrwxrwx 0:0 0 B ├── tmp
drwxr-xr-x 0:0 0 B ├── usr
drwxr-xr-x 1:1 0 B │ └── sbin
drwxr-xr-x 0:0 0 B └── var
drwxr-xr-x 0:0 0 B ├── spool
drwxr-xr-x 8:8 0 B │ └── mail
drwxr-xr-x 0:0 0 B └── www

View file

@ -0,0 +1,9 @@
drwxr-xr-x 0:0 1.2 MB ├─⊕ bin
drwxr-xr-x 0:0 0 B ├── dev
drwxr-xr-x 0:0 1.0 kB ├─⊕ etc
drwxr-xr-x 65534:65534 0 B ├── home
drwx------ 0:0 0 B ├── root
drwxrwxrwx 0:0 0 B ├── tmp
drwxr-xr-x 0:0 0 B ├─⊕ usr
drwxr-xr-x 0:0 0 B └─⊕ var

View file

@ -0,0 +1,22 @@
drwxr-xr-x 0:0 1.2 MB ├─⊕ bin
drwxr-xr-x 0:0 0 B ├── dev
drwxr-xr-x 0:0 1.0 kB ├── etc
-rw-rw-r-- 0:0 307 B │ ├── group
-rw-r--r-- 0:0 127 B │ ├── localtime
drwxr-xr-x 0:0 0 B │ ├── network
drwxr-xr-x 0:0 0 B │ │ ├── if-down.d
drwxr-xr-x 0:0 0 B │ │ ├── if-post-down.d
drwxr-xr-x 0:0 0 B │ │ ├── if-pre-up.d
drwxr-xr-x 0:0 0 B │ │ └── if-up.d
-rw-r--r-- 0:0 340 B │ ├── passwd
-rw------- 0:0 243 B │ └── shadow
drwxr-xr-x 65534:65534 0 B ├── home
drwx------ 0:0 0 B ├── root
drwxrwxrwx 0:0 0 B ├── tmp
drwxr-xr-x 0:0 0 B ├── usr
drwxr-xr-x 1:1 0 B │ └── sbin
drwxr-xr-x 0:0 0 B └── var
drwxr-xr-x 0:0 0 B ├── spool
drwxr-xr-x 8:8 0 B │ └── mail
drwxr-xr-x 0:0 0 B └── www

View file

@ -0,0 +1,7 @@
drwxr-xr-x 0:0 0 B └── etc
drwxr-xr-x 0:0 0 B └── network
drwxr-xr-x 0:0 0 B ├── if-down.d
drwxr-xr-x 0:0 0 B ├── if-post-down.d
drwxr-xr-x 0:0 0 B ├── if-pre-up.d
drwxr-xr-x 0:0 0 B └── if-up.d

416
ui/testdata/TestFileTreeGoCase.txt vendored Normal file
View file

@ -0,0 +1,416 @@
drwxr-xr-x 0:0 1.2 MB ├── bin
-rwxr-xr-x 0:0 1.1 MB │ ├── [
-rwxr-xr-x 0:0 0 B │ ├── [[ → bin/[
-rwxr-xr-x 0:0 0 B │ ├── acpid → bin/[
-rwxr-xr-x 0:0 0 B │ ├── add-shell → bin/[
-rwxr-xr-x 0:0 0 B │ ├── addgroup → bin/[
-rwxr-xr-x 0:0 0 B │ ├── adduser → bin/[
-rwxr-xr-x 0:0 0 B │ ├── adjtimex → bin/[
-rwxr-xr-x 0:0 0 B │ ├── ar → bin/[
-rwxr-xr-x 0:0 0 B │ ├── arch → bin/[
-rwxr-xr-x 0:0 0 B │ ├── arp → bin/[
-rwxr-xr-x 0:0 0 B │ ├── arping → bin/[
-rwxr-xr-x 0:0 0 B │ ├── ash → bin/[
-rwxr-xr-x 0:0 0 B │ ├── awk → bin/[
-rwxr-xr-x 0:0 0 B │ ├── base64 → bin/[
-rwxr-xr-x 0:0 0 B │ ├── basename → bin/[
-rwxr-xr-x 0:0 0 B │ ├── beep → bin/[
-rwxr-xr-x 0:0 0 B │ ├── blkdiscard → bin/[
-rwxr-xr-x 0:0 0 B │ ├── blkid → bin/[
-rwxr-xr-x 0:0 0 B │ ├── blockdev → bin/[
-rwxr-xr-x 0:0 0 B │ ├── bootchartd → bin/[
-rwxr-xr-x 0:0 0 B │ ├── brctl → bin/[
-rwxr-xr-x 0:0 0 B │ ├── bunzip2 → bin/[
-rwxr-xr-x 0:0 0 B │ ├── busybox → bin/[
-rwxr-xr-x 0:0 0 B │ ├── bzcat → bin/[
-rwxr-xr-x 0:0 0 B │ ├── bzip2 → bin/[
-rwxr-xr-x 0:0 0 B │ ├── cal → bin/[
-rwxr-xr-x 0:0 0 B │ ├── cat → bin/[
-rwxr-xr-x 0:0 0 B │ ├── chat → bin/[
-rwxr-xr-x 0:0 0 B │ ├── chattr → bin/[
-rwxr-xr-x 0:0 0 B │ ├── chgrp → bin/[
-rwxr-xr-x 0:0 0 B │ ├── chmod → bin/[
-rwxr-xr-x 0:0 0 B │ ├── chown → bin/[
-rwxr-xr-x 0:0 0 B │ ├── chpasswd → bin/[
-rwxr-xr-x 0:0 0 B │ ├── chpst → bin/[
-rwxr-xr-x 0:0 0 B │ ├── chroot → bin/[
-rwxr-xr-x 0:0 0 B │ ├── chrt → bin/[
-rwxr-xr-x 0:0 0 B │ ├── chvt → bin/[
-rwxr-xr-x 0:0 0 B │ ├── cksum → bin/[
-rwxr-xr-x 0:0 0 B │ ├── clear → bin/[
-rwxr-xr-x 0:0 0 B │ ├── cmp → bin/[
-rwxr-xr-x 0:0 0 B │ ├── comm → bin/[
-rwxr-xr-x 0:0 0 B │ ├── conspy → bin/[
-rwxr-xr-x 0:0 0 B │ ├── cp → bin/[
-rwxr-xr-x 0:0 0 B │ ├── cpio → bin/[
-rwxr-xr-x 0:0 0 B │ ├── crond → bin/[
-rwxr-xr-x 0:0 0 B │ ├── crontab → bin/[
-rwxr-xr-x 0:0 0 B │ ├── cryptpw → bin/[
-rwxr-xr-x 0:0 0 B │ ├── cttyhack → bin/[
-rwxr-xr-x 0:0 0 B │ ├── cut → bin/[
-rwxr-xr-x 0:0 0 B │ ├── date → bin/[
-rwxr-xr-x 0:0 0 B │ ├── dc → bin/[
-rwxr-xr-x 0:0 0 B │ ├── dd → bin/[
-rwxr-xr-x 0:0 0 B │ ├── deallocvt → bin/[
-rwxr-xr-x 0:0 0 B │ ├── delgroup → bin/[
-rwxr-xr-x 0:0 0 B │ ├── deluser → bin/[
-rwxr-xr-x 0:0 0 B │ ├── depmod → bin/[
-rwxr-xr-x 0:0 0 B │ ├── devmem → bin/[
-rwxr-xr-x 0:0 0 B │ ├── df → bin/[
-rwxr-xr-x 0:0 0 B │ ├── dhcprelay → bin/[
-rwxr-xr-x 0:0 0 B │ ├── diff → bin/[
-rwxr-xr-x 0:0 0 B │ ├── dirname → bin/[
-rwxr-xr-x 0:0 0 B │ ├── dmesg → bin/[
-rwxr-xr-x 0:0 0 B │ ├── dnsd → bin/[
-rwxr-xr-x 0:0 0 B │ ├── dnsdomainname → bin/[
-rwxr-xr-x 0:0 0 B │ ├── dos2unix → bin/[
-rwxr-xr-x 0:0 0 B │ ├── dpkg → bin/[
-rwxr-xr-x 0:0 0 B │ ├── dpkg-deb → bin/[
-rwxr-xr-x 0:0 0 B │ ├── du → bin/[
-rwxr-xr-x 0:0 0 B │ ├── dumpkmap → bin/[
-rwxr-xr-x 0:0 0 B │ ├── dumpleases → bin/[
-rwxr-xr-x 0:0 0 B │ ├── echo → bin/[
-rwxr-xr-x 0:0 0 B │ ├── ed → bin/[
-rwxr-xr-x 0:0 0 B │ ├── egrep → bin/[
-rwxr-xr-x 0:0 0 B │ ├── eject → bin/[
-rwxr-xr-x 0:0 0 B │ ├── env → bin/[
-rwxr-xr-x 0:0 0 B │ ├── envdir → bin/[
-rwxr-xr-x 0:0 0 B │ ├── envuidgid → bin/[
-rwxr-xr-x 0:0 0 B │ ├── ether-wake → bin/[
-rwxr-xr-x 0:0 0 B │ ├── expand → bin/[
-rwxr-xr-x 0:0 0 B │ ├── expr → bin/[
-rwxr-xr-x 0:0 0 B │ ├── factor → bin/[
-rwxr-xr-x 0:0 0 B │ ├── fakeidentd → bin/[
-rwxr-xr-x 0:0 0 B │ ├── fallocate → bin/[
-rwxr-xr-x 0:0 0 B │ ├── false → bin/[
-rwxr-xr-x 0:0 0 B │ ├── fatattr → bin/[
-rwxr-xr-x 0:0 0 B │ ├── fbset → bin/[
-rwxr-xr-x 0:0 0 B │ ├── fbsplash → bin/[
-rwxr-xr-x 0:0 0 B │ ├── fdflush → bin/[
-rwxr-xr-x 0:0 0 B │ ├── fdformat → bin/[
-rwxr-xr-x 0:0 0 B │ ├── fdisk → bin/[
-rwxr-xr-x 0:0 0 B │ ├── fgconsole → bin/[
-rwxr-xr-x 0:0 0 B │ ├── fgrep → bin/[
-rwxr-xr-x 0:0 0 B │ ├── find → bin/[
-rwxr-xr-x 0:0 0 B │ ├── findfs → bin/[
-rwxr-xr-x 0:0 0 B │ ├── flock → bin/[
-rwxr-xr-x 0:0 0 B │ ├── fold → bin/[
-rwxr-xr-x 0:0 0 B │ ├── free → bin/[
-rwxr-xr-x 0:0 0 B │ ├── freeramdisk → bin/[
-rwxr-xr-x 0:0 0 B │ ├── fsck → bin/[
-rwxr-xr-x 0:0 0 B │ ├── fsck.minix → bin/[
-rwxr-xr-x 0:0 0 B │ ├── fsfreeze → bin/[
-rwxr-xr-x 0:0 0 B │ ├── fstrim → bin/[
-rwxr-xr-x 0:0 0 B │ ├── fsync → bin/[
-rwxr-xr-x 0:0 0 B │ ├── ftpd → bin/[
-rwxr-xr-x 0:0 0 B │ ├── ftpget → bin/[
-rwxr-xr-x 0:0 0 B │ ├── ftpput → bin/[
-rwxr-xr-x 0:0 0 B │ ├── fuser → bin/[
-rwxr-xr-x 0:0 78 kB │ ├── getconf
-rwxr-xr-x 0:0 0 B │ ├── getopt → bin/[
-rwxr-xr-x 0:0 0 B │ ├── getty → bin/[
-rwxr-xr-x 0:0 0 B │ ├── grep → bin/[
-rwxr-xr-x 0:0 0 B │ ├── groups → bin/[
-rwxr-xr-x 0:0 0 B │ ├── gunzip → bin/[
-rwxr-xr-x 0:0 0 B │ ├── gzip → bin/[
-rwxr-xr-x 0:0 0 B │ ├── halt → bin/[
-rwxr-xr-x 0:0 0 B │ ├── hd → bin/[
-rwxr-xr-x 0:0 0 B │ ├── hdparm → bin/[
-rwxr-xr-x 0:0 0 B │ ├── head → bin/[
-rwxr-xr-x 0:0 0 B │ ├── hexdump → bin/[
-rwxr-xr-x 0:0 0 B │ ├── hexedit → bin/[
-rwxr-xr-x 0:0 0 B │ ├── hostid → bin/[
-rwxr-xr-x 0:0 0 B │ ├── hostname → bin/[
-rwxr-xr-x 0:0 0 B │ ├── httpd → bin/[
-rwxr-xr-x 0:0 0 B │ ├── hush → bin/[
-rwxr-xr-x 0:0 0 B │ ├── hwclock → bin/[
-rwxr-xr-x 0:0 0 B │ ├── i2cdetect → bin/[
-rwxr-xr-x 0:0 0 B │ ├── i2cdump → bin/[
-rwxr-xr-x 0:0 0 B │ ├── i2cget → bin/[
-rwxr-xr-x 0:0 0 B │ ├── i2cset → bin/[
-rwxr-xr-x 0:0 0 B │ ├── id → bin/[
-rwxr-xr-x 0:0 0 B │ ├── ifconfig → bin/[
-rwxr-xr-x 0:0 0 B │ ├── ifdown → bin/[
-rwxr-xr-x 0:0 0 B │ ├── ifenslave → bin/[
-rwxr-xr-x 0:0 0 B │ ├── ifplugd → bin/[
-rwxr-xr-x 0:0 0 B │ ├── ifup → bin/[
-rwxr-xr-x 0:0 0 B │ ├── inetd → bin/[
-rwxr-xr-x 0:0 0 B │ ├── init → bin/[
-rwxr-xr-x 0:0 0 B │ ├── insmod → bin/[
-rwxr-xr-x 0:0 0 B │ ├── install → bin/[
-rwxr-xr-x 0:0 0 B │ ├── ionice → bin/[
-rwxr-xr-x 0:0 0 B │ ├── iostat → bin/[
-rwxr-xr-x 0:0 0 B │ ├── ip → bin/[
-rwxr-xr-x 0:0 0 B │ ├── ipaddr → bin/[
-rwxr-xr-x 0:0 0 B │ ├── ipcalc → bin/[
-rwxr-xr-x 0:0 0 B │ ├── ipcrm → bin/[
-rwxr-xr-x 0:0 0 B │ ├── ipcs → bin/[
-rwxr-xr-x 0:0 0 B │ ├── iplink → bin/[
-rwxr-xr-x 0:0 0 B │ ├── ipneigh → bin/[
-rwxr-xr-x 0:0 0 B │ ├── iproute → bin/[
-rwxr-xr-x 0:0 0 B │ ├── iprule → bin/[
-rwxr-xr-x 0:0 0 B │ ├── iptunnel → bin/[
-rwxr-xr-x 0:0 0 B │ ├── kbd_mode → bin/[
-rwxr-xr-x 0:0 0 B │ ├── kill → bin/[
-rwxr-xr-x 0:0 0 B │ ├── killall → bin/[
-rwxr-xr-x 0:0 0 B │ ├── killall5 → bin/[
-rwxr-xr-x 0:0 0 B │ ├── klogd → bin/[
-rwxr-xr-x 0:0 0 B │ ├── last → bin/[
-rwxr-xr-x 0:0 0 B │ ├── less → bin/[
-rwxr-xr-x 0:0 0 B │ ├── link → bin/[
-rwxr-xr-x 0:0 0 B │ ├── linux32 → bin/[
-rwxr-xr-x 0:0 0 B │ ├── linux64 → bin/[
-rwxr-xr-x 0:0 0 B │ ├── linuxrc → bin/[
-rwxr-xr-x 0:0 0 B │ ├── ln → bin/[
-rwxr-xr-x 0:0 0 B │ ├── loadfont → bin/[
-rwxr-xr-x 0:0 0 B │ ├── loadkmap → bin/[
-rwxr-xr-x 0:0 0 B │ ├── logger → bin/[
-rwxr-xr-x 0:0 0 B │ ├── login → bin/[
-rwxr-xr-x 0:0 0 B │ ├── logname → bin/[
-rwxr-xr-x 0:0 0 B │ ├── logread → bin/[
-rwxr-xr-x 0:0 0 B │ ├── losetup → bin/[
-rwxr-xr-x 0:0 0 B │ ├── lpd → bin/[
-rwxr-xr-x 0:0 0 B │ ├── lpq → bin/[
-rwxr-xr-x 0:0 0 B │ ├── lpr → bin/[
-rwxr-xr-x 0:0 0 B │ ├── ls → bin/[
-rwxr-xr-x 0:0 0 B │ ├── lsattr → bin/[
-rwxr-xr-x 0:0 0 B │ ├── lsmod → bin/[
-rwxr-xr-x 0:0 0 B │ ├── lsof → bin/[
-rwxr-xr-x 0:0 0 B │ ├── lspci → bin/[
-rwxr-xr-x 0:0 0 B │ ├── lsscsi → bin/[
-rwxr-xr-x 0:0 0 B │ ├── lsusb → bin/[
-rwxr-xr-x 0:0 0 B │ ├── lzcat → bin/[
-rwxr-xr-x 0:0 0 B │ ├── lzma → bin/[
-rwxr-xr-x 0:0 0 B │ ├── lzop → bin/[
-rwxr-xr-x 0:0 0 B │ ├── makedevs → bin/[
-rwxr-xr-x 0:0 0 B │ ├── makemime → bin/[
-rwxr-xr-x 0:0 0 B │ ├── man → bin/[
-rwxr-xr-x 0:0 0 B │ ├── md5sum → bin/[
-rwxr-xr-x 0:0 0 B │ ├── mdev → bin/[
-rwxr-xr-x 0:0 0 B │ ├── mesg → bin/[
-rwxr-xr-x 0:0 0 B │ ├── microcom → bin/[
-rwxr-xr-x 0:0 0 B │ ├── mkdir → bin/[
-rwxr-xr-x 0:0 0 B │ ├── mkdosfs → bin/[
-rwxr-xr-x 0:0 0 B │ ├── mke2fs → bin/[
-rwxr-xr-x 0:0 0 B │ ├── mkfifo → bin/[
-rwxr-xr-x 0:0 0 B │ ├── mkfs.ext2 → bin/[
-rwxr-xr-x 0:0 0 B │ ├── mkfs.minix → bin/[
-rwxr-xr-x 0:0 0 B │ ├── mkfs.vfat → bin/[
-rwxr-xr-x 0:0 0 B │ ├── mknod → bin/[
-rwxr-xr-x 0:0 0 B │ ├── mkpasswd → bin/[
-rwxr-xr-x 0:0 0 B │ ├── mkswap → bin/[
-rwxr-xr-x 0:0 0 B │ ├── mktemp → bin/[
-rwxr-xr-x 0:0 0 B │ ├── modinfo → bin/[
-rwxr-xr-x 0:0 0 B │ ├── modprobe → bin/[
-rwxr-xr-x 0:0 0 B │ ├── more → bin/[
-rwxr-xr-x 0:0 0 B │ ├── mount → bin/[
-rwxr-xr-x 0:0 0 B │ ├── mountpoint → bin/[
-rwxr-xr-x 0:0 0 B │ ├── mpstat → bin/[
-rwxr-xr-x 0:0 0 B │ ├── mt → bin/[
-rwxr-xr-x 0:0 0 B │ ├── mv → bin/[
-rwxr-xr-x 0:0 0 B │ ├── nameif → bin/[
-rwxr-xr-x 0:0 0 B │ ├── nanddump → bin/[
-rwxr-xr-x 0:0 0 B │ ├── nandwrite → bin/[
-rwxr-xr-x 0:0 0 B │ ├── nbd-client → bin/[
-rwxr-xr-x 0:0 0 B │ ├── nc → bin/[
-rwxr-xr-x 0:0 0 B │ ├── netstat → bin/[
-rwxr-xr-x 0:0 0 B │ ├── nice → bin/[
-rwxr-xr-x 0:0 0 B │ ├── nl → bin/[
-rwxr-xr-x 0:0 0 B │ ├── nmeter → bin/[
-rwxr-xr-x 0:0 0 B │ ├── nohup → bin/[
-rwxr-xr-x 0:0 0 B │ ├── nproc → bin/[
-rwxr-xr-x 0:0 0 B │ ├── nsenter → bin/[
-rwxr-xr-x 0:0 0 B │ ├── nslookup → bin/[
-rwxr-xr-x 0:0 0 B │ ├── ntpd → bin/[
-rwxr-xr-x 0:0 0 B │ ├── nuke → bin/[
-rwxr-xr-x 0:0 0 B │ ├── od → bin/[
-rwxr-xr-x 0:0 0 B │ ├── openvt → bin/[
-rwxr-xr-x 0:0 0 B │ ├── partprobe → bin/[
-rwxr-xr-x 0:0 0 B │ ├── passwd → bin/[
-rwxr-xr-x 0:0 0 B │ ├── paste → bin/[
-rwxr-xr-x 0:0 0 B │ ├── patch → bin/[
-rwxr-xr-x 0:0 0 B │ ├── pgrep → bin/[
-rwxr-xr-x 0:0 0 B │ ├── pidof → bin/[
-rwxr-xr-x 0:0 0 B │ ├── ping → bin/[
-rwxr-xr-x 0:0 0 B │ ├── ping6 → bin/[
-rwxr-xr-x 0:0 0 B │ ├── pipe_progress → bin/[
-rwxr-xr-x 0:0 0 B │ ├── pivot_root → bin/[
-rwxr-xr-x 0:0 0 B │ ├── pkill → bin/[
-rwxr-xr-x 0:0 0 B │ ├── pmap → bin/[
-rwxr-xr-x 0:0 0 B │ ├── popmaildir → bin/[
-rwxr-xr-x 0:0 0 B │ ├── poweroff → bin/[
-rwxr-xr-x 0:0 0 B │ ├── powertop → bin/[
-rwxr-xr-x 0:0 0 B │ ├── printenv → bin/[
-rwxr-xr-x 0:0 0 B │ ├── printf → bin/[
-rwxr-xr-x 0:0 0 B │ ├── ps → bin/[
-rwxr-xr-x 0:0 0 B │ ├── pscan → bin/[
-rwxr-xr-x 0:0 0 B │ ├── pstree → bin/[
-rwxr-xr-x 0:0 0 B │ ├── pwd → bin/[
-rwxr-xr-x 0:0 0 B │ ├── pwdx → bin/[
-rwxr-xr-x 0:0 0 B │ ├── raidautorun → bin/[
-rwxr-xr-x 0:0 0 B │ ├── rdate → bin/[
-rwxr-xr-x 0:0 0 B │ ├── rdev → bin/[
-rwxr-xr-x 0:0 0 B │ ├── readahead → bin/[
-rwxr-xr-x 0:0 0 B │ ├── readlink → bin/[
-rwxr-xr-x 0:0 0 B │ ├── readprofile → bin/[
-rwxr-xr-x 0:0 0 B │ ├── realpath → bin/[
-rwxr-xr-x 0:0 0 B │ ├── reboot → bin/[
-rwxr-xr-x 0:0 0 B │ ├── reformime → bin/[
-rwxr-xr-x 0:0 0 B │ ├── remove-shell → bin/[
-rwxr-xr-x 0:0 0 B │ ├── renice → bin/[
-rwxr-xr-x 0:0 0 B │ ├── reset → bin/[
-rwxr-xr-x 0:0 0 B │ ├── resize → bin/[
-rwxr-xr-x 0:0 0 B │ ├── resume → bin/[
-rwxr-xr-x 0:0 0 B │ ├── rev → bin/[
-rwxr-xr-x 0:0 0 B │ ├── rm → bin/[
-rwxr-xr-x 0:0 0 B │ ├── rmdir → bin/[
-rwxr-xr-x 0:0 0 B │ ├── rmmod → bin/[
-rwxr-xr-x 0:0 0 B │ ├── route → bin/[
-rwxr-xr-x 0:0 0 B │ ├── rpm → bin/[
-rwxr-xr-x 0:0 0 B │ ├── rpm2cpio → bin/[
-rwxr-xr-x 0:0 0 B │ ├── rtcwake → bin/[
-rwxr-xr-x 0:0 0 B │ ├── run-init → bin/[
-rwxr-xr-x 0:0 0 B │ ├── run-parts → bin/[
-rwxr-xr-x 0:0 0 B │ ├── runlevel → bin/[
-rwxr-xr-x 0:0 0 B │ ├── runsv → bin/[
-rwxr-xr-x 0:0 0 B │ ├── runsvdir → bin/[
-rwxr-xr-x 0:0 0 B │ ├── rx → bin/[
-rwxr-xr-x 0:0 0 B │ ├── script → bin/[
-rwxr-xr-x 0:0 0 B │ ├── scriptreplay → bin/[
-rwxr-xr-x 0:0 0 B │ ├── sed → bin/[
-rwxr-xr-x 0:0 0 B │ ├── sendmail → bin/[
-rwxr-xr-x 0:0 0 B │ ├── seq → bin/[
-rwxr-xr-x 0:0 0 B │ ├── setarch → bin/[
-rwxr-xr-x 0:0 0 B │ ├── setconsole → bin/[
-rwxr-xr-x 0:0 0 B │ ├── setfattr → bin/[
-rwxr-xr-x 0:0 0 B │ ├── setfont → bin/[
-rwxr-xr-x 0:0 0 B │ ├── setkeycodes → bin/[
-rwxr-xr-x 0:0 0 B │ ├── setlogcons → bin/[
-rwxr-xr-x 0:0 0 B │ ├── setpriv → bin/[
-rwxr-xr-x 0:0 0 B │ ├── setserial → bin/[
-rwxr-xr-x 0:0 0 B │ ├── setsid → bin/[
-rwxr-xr-x 0:0 0 B │ ├── setuidgid → bin/[
-rwxr-xr-x 0:0 0 B │ ├── sh → bin/[
-rwxr-xr-x 0:0 0 B │ ├── sha1sum → bin/[
-rwxr-xr-x 0:0 0 B │ ├── sha256sum → bin/[
-rwxr-xr-x 0:0 0 B │ ├── sha3sum → bin/[
-rwxr-xr-x 0:0 0 B │ ├── sha512sum → bin/[
-rwxr-xr-x 0:0 0 B │ ├── showkey → bin/[
-rwxr-xr-x 0:0 0 B │ ├── shred → bin/[
-rwxr-xr-x 0:0 0 B │ ├── shuf → bin/[
-rwxr-xr-x 0:0 0 B │ ├── slattach → bin/[
-rwxr-xr-x 0:0 0 B │ ├── sleep → bin/[
-rwxr-xr-x 0:0 0 B │ ├── smemcap → bin/[
-rwxr-xr-x 0:0 0 B │ ├── softlimit → bin/[
-rwxr-xr-x 0:0 0 B │ ├── sort → bin/[
-rwxr-xr-x 0:0 0 B │ ├── split → bin/[
-rwxr-xr-x 0:0 0 B │ ├── ssl_client → bin/[
-rwxr-xr-x 0:0 0 B │ ├── start-stop-daemon → bin/[
-rwxr-xr-x 0:0 0 B │ ├── stat → bin/[
-rwxr-xr-x 0:0 0 B │ ├── strings → bin/[
-rwxr-xr-x 0:0 0 B │ ├── stty → bin/[
-rwxr-xr-x 0:0 0 B │ ├── su → bin/[
-rwxr-xr-x 0:0 0 B │ ├── sulogin → bin/[
-rwxr-xr-x 0:0 0 B │ ├── sum → bin/[
-rwxr-xr-x 0:0 0 B │ ├── sv → bin/[
-rwxr-xr-x 0:0 0 B │ ├── svc → bin/[
-rwxr-xr-x 0:0 0 B │ ├── svlogd → bin/[
-rwxr-xr-x 0:0 0 B │ ├── svok → bin/[
-rwxr-xr-x 0:0 0 B │ ├── swapoff → bin/[
-rwxr-xr-x 0:0 0 B │ ├── swapon → bin/[
-rwxr-xr-x 0:0 0 B │ ├── switch_root → bin/[
-rwxr-xr-x 0:0 0 B │ ├── sync → bin/[
-rwxr-xr-x 0:0 0 B │ ├── sysctl → bin/[
-rwxr-xr-x 0:0 0 B │ ├── syslogd → bin/[
-rwxr-xr-x 0:0 0 B │ ├── tac → bin/[
-rwxr-xr-x 0:0 0 B │ ├── tail → bin/[
-rwxr-xr-x 0:0 0 B │ ├── tar → bin/[
-rwxr-xr-x 0:0 0 B │ ├── taskset → bin/[
-rwxr-xr-x 0:0 0 B │ ├── tc → bin/[
-rwxr-xr-x 0:0 0 B │ ├── tcpsvd → bin/[
-rwxr-xr-x 0:0 0 B │ ├── tee → bin/[
-rwxr-xr-x 0:0 0 B │ ├── telnet → bin/[
-rwxr-xr-x 0:0 0 B │ ├── telnetd → bin/[
-rwxr-xr-x 0:0 0 B │ ├── test → bin/[
-rwxr-xr-x 0:0 0 B │ ├── tftp → bin/[
-rwxr-xr-x 0:0 0 B │ ├── tftpd → bin/[
-rwxr-xr-x 0:0 0 B │ ├── time → bin/[
-rwxr-xr-x 0:0 0 B │ ├── timeout → bin/[
-rwxr-xr-x 0:0 0 B │ ├── top → bin/[
-rwxr-xr-x 0:0 0 B │ ├── touch → bin/[
-rwxr-xr-x 0:0 0 B │ ├── tr → bin/[
-rwxr-xr-x 0:0 0 B │ ├── traceroute → bin/[
-rwxr-xr-x 0:0 0 B │ ├── traceroute6 → bin/[
-rwxr-xr-x 0:0 0 B │ ├── true → bin/[
-rwxr-xr-x 0:0 0 B │ ├── truncate → bin/[
-rwxr-xr-x 0:0 0 B │ ├── tty → bin/[
-rwxr-xr-x 0:0 0 B │ ├── ttysize → bin/[
-rwxr-xr-x 0:0 0 B │ ├── tunctl → bin/[
-rwxr-xr-x 0:0 0 B │ ├── ubiattach → bin/[
-rwxr-xr-x 0:0 0 B │ ├── ubidetach → bin/[
-rwxr-xr-x 0:0 0 B │ ├── ubimkvol → bin/[
-rwxr-xr-x 0:0 0 B │ ├── ubirename → bin/[
-rwxr-xr-x 0:0 0 B │ ├── ubirmvol → bin/[
-rwxr-xr-x 0:0 0 B │ ├── ubirsvol → bin/[
-rwxr-xr-x 0:0 0 B │ ├── ubiupdatevol → bin/[
-rwxr-xr-x 0:0 0 B │ ├── udhcpc → bin/[
-rwxr-xr-x 0:0 0 B │ ├── udhcpd → bin/[
-rwxr-xr-x 0:0 0 B │ ├── udpsvd → bin/[
-rwxr-xr-x 0:0 0 B │ ├── uevent → bin/[
-rwxr-xr-x 0:0 0 B │ ├── umount → bin/[
-rwxr-xr-x 0:0 0 B │ ├── uname → bin/[
-rwxr-xr-x 0:0 0 B │ ├── unexpand → bin/[
-rwxr-xr-x 0:0 0 B │ ├── uniq → bin/[
-rwxr-xr-x 0:0 0 B │ ├── unix2dos → bin/[
-rwxr-xr-x 0:0 0 B │ ├── unlink → bin/[
-rwxr-xr-x 0:0 0 B │ ├── unlzma → bin/[
-rwxr-xr-x 0:0 0 B │ ├── unshare → bin/[
-rwxr-xr-x 0:0 0 B │ ├── unxz → bin/[
-rwxr-xr-x 0:0 0 B │ ├── unzip → bin/[
-rwxr-xr-x 0:0 0 B │ ├── uptime → bin/[
-rwxr-xr-x 0:0 0 B │ ├── users → bin/[
-rwxr-xr-x 0:0 0 B │ ├── usleep → bin/[
-rwxr-xr-x 0:0 0 B │ ├── uudecode → bin/[
-rwxr-xr-x 0:0 0 B │ ├── uuencode → bin/[
-rwxr-xr-x 0:0 0 B │ ├── vconfig → bin/[
-rwxr-xr-x 0:0 0 B │ ├── vi → bin/[
-rwxr-xr-x 0:0 0 B │ ├── vlock → bin/[
-rwxr-xr-x 0:0 0 B │ ├── volname → bin/[
-rwxr-xr-x 0:0 0 B │ ├── w → bin/[
-rwxr-xr-x 0:0 0 B │ ├── wall → bin/[
-rwxr-xr-x 0:0 0 B │ ├── watch → bin/[
-rwxr-xr-x 0:0 0 B │ ├── watchdog → bin/[
-rwxr-xr-x 0:0 0 B │ ├── wc → bin/[
-rwxr-xr-x 0:0 0 B │ ├── wget → bin/[
-rwxr-xr-x 0:0 0 B │ ├── which → bin/[
-rwxr-xr-x 0:0 0 B │ ├── who → bin/[
-rwxr-xr-x 0:0 0 B │ ├── whoami → bin/[
-rwxr-xr-x 0:0 0 B │ ├── whois → bin/[
-rwxr-xr-x 0:0 0 B │ ├── xargs → bin/[
-rwxr-xr-x 0:0 0 B │ ├── xxd → bin/[
-rwxr-xr-x 0:0 0 B │ ├── xz → bin/[
-rwxr-xr-x 0:0 0 B │ ├── xzcat → bin/[
-rwxr-xr-x 0:0 0 B │ ├── yes → bin/[
-rwxr-xr-x 0:0 0 B │ ├── zcat → bin/[
-rwxr-xr-x 0:0 0 B │ └── zcip → bin/[
drwxr-xr-x 0:0 0 B ├── dev
drwxr-xr-x 0:0 1.0 kB ├── etc
-rw-rw-r-- 0:0 307 B │ ├── group
-rw-r--r-- 0:0 127 B │ ├── localtime
drwxr-xr-x 0:0 0 B │ ├── network
drwxr-xr-x 0:0 0 B │ │ ├── if-down.d
drwxr-xr-x 0:0 0 B │ │ ├── if-post-down.d
drwxr-xr-x 0:0 0 B │ │ ├── if-pre-up.d
drwxr-xr-x 0:0 0 B │ │ └── if-up.d
-rw-r--r-- 0:0 340 B │ ├── passwd
-rw------- 0:0 243 B │ └── shadow
drwxr-xr-x 65534:65534 0 B ├── home
drwx------ 0:0 0 B ├── root
drwxrwxrwx 0:0 0 B ├── tmp
drwxr-xr-x 0:0 0 B ├── usr
drwxr-xr-x 1:1 0 B │ └── sbin
drwxr-xr-x 0:0 0 B └── var
drwxr-xr-x 0:0 0 B ├── spool
drwxr-xr-x 8:8 0 B │ └── mail
drwxr-xr-x 0:0 0 B └── www

View file

@ -0,0 +1,21 @@
drwxr-xr-x 0:0 1.2 MB ├─⊕ bin
drwxr-xr-x 0:0 0 B ├── dev
drwxr-xr-x 0:0 1.0 kB ├── etc
-rw-rw-r-- 0:0 307 B │ ├── group
-rw-r--r-- 0:0 127 B │ ├── localtime
drwxr-xr-x 0:0 0 B │ ├── network
drwxr-xr-x 0:0 0 B │ │ ├── if-down.d
drwxr-xr-x 0:0 0 B │ │ ├── if-post-down.d
drwxr-xr-x 0:0 0 B │ │ ├── if-pre-up.d
drwxr-xr-x 0:0 0 B │ │ └── if-up.d
-rw-r--r-- 0:0 340 B │ ├── passwd
-rw------- 0:0 243 B │ └── shadow
drwxr-xr-x 65534:65534 0 B ├── home
drwxrwxrwx 0:0 0 B ├── tmp
drwxr-xr-x 0:0 0 B ├── usr
drwxr-xr-x 1:1 0 B │ └── sbin
drwxr-xr-x 0:0 0 B └── var
drwxr-xr-x 0:0 0 B ├── spool
drwxr-xr-x 8:8 0 B │ └── mail
drwxr-xr-x 0:0 0 B └── www

View file

@ -0,0 +1 @@

View file

@ -0,0 +1,10 @@
drwx------ 0:0 19 kB ├── root
drwxr-xr-x 0:0 13 kB │ ├── example
drwxr-xr-x 0:0 0 B │ │ ├── really
drwxr-xr-x 0:0 0 B │ │ │ └── nested
-r--r--r-- 0:0 6.4 kB │ │ ├── somefile1.txt
-rw-r--r-- 0:0 6.4 kB │ │ ├── somefile2.txt
-rw-r--r-- 0:0 6.4 kB │ │ └── somefile3.txt
-rw-r--r-- 0:0 6.4 kB │ └── saved.txt
-rw-rw-r-- 0:0 6.4 kB └── somefile.txt

416
ui/testdata/TestFileTreeNoAttributes.txt vendored Normal file
View file

@ -0,0 +1,416 @@
├── bin
│ ├── [
│ ├── [[ → bin/[
│ ├── acpid → bin/[
│ ├── add-shell → bin/[
│ ├── addgroup → bin/[
│ ├── adduser → bin/[
│ ├── adjtimex → bin/[
│ ├── ar → bin/[
│ ├── arch → bin/[
│ ├── arp → bin/[
│ ├── arping → bin/[
│ ├── ash → bin/[
│ ├── awk → bin/[
│ ├── base64 → bin/[
│ ├── basename → bin/[
│ ├── beep → bin/[
│ ├── blkdiscard → bin/[
│ ├── blkid → bin/[
│ ├── blockdev → bin/[
│ ├── bootchartd → bin/[
│ ├── brctl → bin/[
│ ├── bunzip2 → bin/[
│ ├── busybox → bin/[
│ ├── bzcat → bin/[
│ ├── bzip2 → bin/[
│ ├── cal → bin/[
│ ├── cat → bin/[
│ ├── chat → bin/[
│ ├── chattr → bin/[
│ ├── chgrp → bin/[
│ ├── chmod → bin/[
│ ├── chown → bin/[
│ ├── chpasswd → bin/[
│ ├── chpst → bin/[
│ ├── chroot → bin/[
│ ├── chrt → bin/[
│ ├── chvt → bin/[
│ ├── cksum → bin/[
│ ├── clear → bin/[
│ ├── cmp → bin/[
│ ├── comm → bin/[
│ ├── conspy → bin/[
│ ├── cp → bin/[
│ ├── cpio → bin/[
│ ├── crond → bin/[
│ ├── crontab → bin/[
│ ├── cryptpw → bin/[
│ ├── cttyhack → bin/[
│ ├── cut → bin/[
│ ├── date → bin/[
│ ├── dc → bin/[
│ ├── dd → bin/[
│ ├── deallocvt → bin/[
│ ├── delgroup → bin/[
│ ├── deluser → bin/[
│ ├── depmod → bin/[
│ ├── devmem → bin/[
│ ├── df → bin/[
│ ├── dhcprelay → bin/[
│ ├── diff → bin/[
│ ├── dirname → bin/[
│ ├── dmesg → bin/[
│ ├── dnsd → bin/[
│ ├── dnsdomainname → bin/[
│ ├── dos2unix → bin/[
│ ├── dpkg → bin/[
│ ├── dpkg-deb → bin/[
│ ├── du → bin/[
│ ├── dumpkmap → bin/[
│ ├── dumpleases → bin/[
│ ├── echo → bin/[
│ ├── ed → bin/[
│ ├── egrep → bin/[
│ ├── eject → bin/[
│ ├── env → bin/[
│ ├── envdir → bin/[
│ ├── envuidgid → bin/[
│ ├── ether-wake → bin/[
│ ├── expand → bin/[
│ ├── expr → bin/[
│ ├── factor → bin/[
│ ├── fakeidentd → bin/[
│ ├── fallocate → bin/[
│ ├── false → bin/[
│ ├── fatattr → bin/[
│ ├── fbset → bin/[
│ ├── fbsplash → bin/[
│ ├── fdflush → bin/[
│ ├── fdformat → bin/[
│ ├── fdisk → bin/[
│ ├── fgconsole → bin/[
│ ├── fgrep → bin/[
│ ├── find → bin/[
│ ├── findfs → bin/[
│ ├── flock → bin/[
│ ├── fold → bin/[
│ ├── free → bin/[
│ ├── freeramdisk → bin/[
│ ├── fsck → bin/[
│ ├── fsck.minix → bin/[
│ ├── fsfreeze → bin/[
│ ├── fstrim → bin/[
│ ├── fsync → bin/[
│ ├── ftpd → bin/[
│ ├── ftpget → bin/[
│ ├── ftpput → bin/[
│ ├── fuser → bin/[
│ ├── getconf
│ ├── getopt → bin/[
│ ├── getty → bin/[
│ ├── grep → bin/[
│ ├── groups → bin/[
│ ├── gunzip → bin/[
│ ├── gzip → bin/[
│ ├── halt → bin/[
│ ├── hd → bin/[
│ ├── hdparm → bin/[
│ ├── head → bin/[
│ ├── hexdump → bin/[
│ ├── hexedit → bin/[
│ ├── hostid → bin/[
│ ├── hostname → bin/[
│ ├── httpd → bin/[
│ ├── hush → bin/[
│ ├── hwclock → bin/[
│ ├── i2cdetect → bin/[
│ ├── i2cdump → bin/[
│ ├── i2cget → bin/[
│ ├── i2cset → bin/[
│ ├── id → bin/[
│ ├── ifconfig → bin/[
│ ├── ifdown → bin/[
│ ├── ifenslave → bin/[
│ ├── ifplugd → bin/[
│ ├── ifup → bin/[
│ ├── inetd → bin/[
│ ├── init → bin/[
│ ├── insmod → bin/[
│ ├── install → bin/[
│ ├── ionice → bin/[
│ ├── iostat → bin/[
│ ├── ip → bin/[
│ ├── ipaddr → bin/[
│ ├── ipcalc → bin/[
│ ├── ipcrm → bin/[
│ ├── ipcs → bin/[
│ ├── iplink → bin/[
│ ├── ipneigh → bin/[
│ ├── iproute → bin/[
│ ├── iprule → bin/[
│ ├── iptunnel → bin/[
│ ├── kbd_mode → bin/[
│ ├── kill → bin/[
│ ├── killall → bin/[
│ ├── killall5 → bin/[
│ ├── klogd → bin/[
│ ├── last → bin/[
│ ├── less → bin/[
│ ├── link → bin/[
│ ├── linux32 → bin/[
│ ├── linux64 → bin/[
│ ├── linuxrc → bin/[
│ ├── ln → bin/[
│ ├── loadfont → bin/[
│ ├── loadkmap → bin/[
│ ├── logger → bin/[
│ ├── login → bin/[
│ ├── logname → bin/[
│ ├── logread → bin/[
│ ├── losetup → bin/[
│ ├── lpd → bin/[
│ ├── lpq → bin/[
│ ├── lpr → bin/[
│ ├── ls → bin/[
│ ├── lsattr → bin/[
│ ├── lsmod → bin/[
│ ├── lsof → bin/[
│ ├── lspci → bin/[
│ ├── lsscsi → bin/[
│ ├── lsusb → bin/[
│ ├── lzcat → bin/[
│ ├── lzma → bin/[
│ ├── lzop → bin/[
│ ├── makedevs → bin/[
│ ├── makemime → bin/[
│ ├── man → bin/[
│ ├── md5sum → bin/[
│ ├── mdev → bin/[
│ ├── mesg → bin/[
│ ├── microcom → bin/[
│ ├── mkdir → bin/[
│ ├── mkdosfs → bin/[
│ ├── mke2fs → bin/[
│ ├── mkfifo → bin/[
│ ├── mkfs.ext2 → bin/[
│ ├── mkfs.minix → bin/[
│ ├── mkfs.vfat → bin/[
│ ├── mknod → bin/[
│ ├── mkpasswd → bin/[
│ ├── mkswap → bin/[
│ ├── mktemp → bin/[
│ ├── modinfo → bin/[
│ ├── modprobe → bin/[
│ ├── more → bin/[
│ ├── mount → bin/[
│ ├── mountpoint → bin/[
│ ├── mpstat → bin/[
│ ├── mt → bin/[
│ ├── mv → bin/[
│ ├── nameif → bin/[
│ ├── nanddump → bin/[
│ ├── nandwrite → bin/[
│ ├── nbd-client → bin/[
│ ├── nc → bin/[
│ ├── netstat → bin/[
│ ├── nice → bin/[
│ ├── nl → bin/[
│ ├── nmeter → bin/[
│ ├── nohup → bin/[
│ ├── nproc → bin/[
│ ├── nsenter → bin/[
│ ├── nslookup → bin/[
│ ├── ntpd → bin/[
│ ├── nuke → bin/[
│ ├── od → bin/[
│ ├── openvt → bin/[
│ ├── partprobe → bin/[
│ ├── passwd → bin/[
│ ├── paste → bin/[
│ ├── patch → bin/[
│ ├── pgrep → bin/[
│ ├── pidof → bin/[
│ ├── ping → bin/[
│ ├── ping6 → bin/[
│ ├── pipe_progress → bin/[
│ ├── pivot_root → bin/[
│ ├── pkill → bin/[
│ ├── pmap → bin/[
│ ├── popmaildir → bin/[
│ ├── poweroff → bin/[
│ ├── powertop → bin/[
│ ├── printenv → bin/[
│ ├── printf → bin/[
│ ├── ps → bin/[
│ ├── pscan → bin/[
│ ├── pstree → bin/[
│ ├── pwd → bin/[
│ ├── pwdx → bin/[
│ ├── raidautorun → bin/[
│ ├── rdate → bin/[
│ ├── rdev → bin/[
│ ├── readahead → bin/[
│ ├── readlink → bin/[
│ ├── readprofile → bin/[
│ ├── realpath → bin/[
│ ├── reboot → bin/[
│ ├── reformime → bin/[
│ ├── remove-shell → bin/[
│ ├── renice → bin/[
│ ├── reset → bin/[
│ ├── resize → bin/[
│ ├── resume → bin/[
│ ├── rev → bin/[
│ ├── rm → bin/[
│ ├── rmdir → bin/[
│ ├── rmmod → bin/[
│ ├── route → bin/[
│ ├── rpm → bin/[
│ ├── rpm2cpio → bin/[
│ ├── rtcwake → bin/[
│ ├── run-init → bin/[
│ ├── run-parts → bin/[
│ ├── runlevel → bin/[
│ ├── runsv → bin/[
│ ├── runsvdir → bin/[
│ ├── rx → bin/[
│ ├── script → bin/[
│ ├── scriptreplay → bin/[
│ ├── sed → bin/[
│ ├── sendmail → bin/[
│ ├── seq → bin/[
│ ├── setarch → bin/[
│ ├── setconsole → bin/[
│ ├── setfattr → bin/[
│ ├── setfont → bin/[
│ ├── setkeycodes → bin/[
│ ├── setlogcons → bin/[
│ ├── setpriv → bin/[
│ ├── setserial → bin/[
│ ├── setsid → bin/[
│ ├── setuidgid → bin/[
│ ├── sh → bin/[
│ ├── sha1sum → bin/[
│ ├── sha256sum → bin/[
│ ├── sha3sum → bin/[
│ ├── sha512sum → bin/[
│ ├── showkey → bin/[
│ ├── shred → bin/[
│ ├── shuf → bin/[
│ ├── slattach → bin/[
│ ├── sleep → bin/[
│ ├── smemcap → bin/[
│ ├── softlimit → bin/[
│ ├── sort → bin/[
│ ├── split → bin/[
│ ├── ssl_client → bin/[
│ ├── start-stop-daemon → bin/[
│ ├── stat → bin/[
│ ├── strings → bin/[
│ ├── stty → bin/[
│ ├── su → bin/[
│ ├── sulogin → bin/[
│ ├── sum → bin/[
│ ├── sv → bin/[
│ ├── svc → bin/[
│ ├── svlogd → bin/[
│ ├── svok → bin/[
│ ├── swapoff → bin/[
│ ├── swapon → bin/[
│ ├── switch_root → bin/[
│ ├── sync → bin/[
│ ├── sysctl → bin/[
│ ├── syslogd → bin/[
│ ├── tac → bin/[
│ ├── tail → bin/[
│ ├── tar → bin/[
│ ├── taskset → bin/[
│ ├── tc → bin/[
│ ├── tcpsvd → bin/[
│ ├── tee → bin/[
│ ├── telnet → bin/[
│ ├── telnetd → bin/[
│ ├── test → bin/[
│ ├── tftp → bin/[
│ ├── tftpd → bin/[
│ ├── time → bin/[
│ ├── timeout → bin/[
│ ├── top → bin/[
│ ├── touch → bin/[
│ ├── tr → bin/[
│ ├── traceroute → bin/[
│ ├── traceroute6 → bin/[
│ ├── true → bin/[
│ ├── truncate → bin/[
│ ├── tty → bin/[
│ ├── ttysize → bin/[
│ ├── tunctl → bin/[
│ ├── ubiattach → bin/[
│ ├── ubidetach → bin/[
│ ├── ubimkvol → bin/[
│ ├── ubirename → bin/[
│ ├── ubirmvol → bin/[
│ ├── ubirsvol → bin/[
│ ├── ubiupdatevol → bin/[
│ ├── udhcpc → bin/[
│ ├── udhcpd → bin/[
│ ├── udpsvd → bin/[
│ ├── uevent → bin/[
│ ├── umount → bin/[
│ ├── uname → bin/[
│ ├── unexpand → bin/[
│ ├── uniq → bin/[
│ ├── unix2dos → bin/[
│ ├── unlink → bin/[
│ ├── unlzma → bin/[
│ ├── unshare → bin/[
│ ├── unxz → bin/[
│ ├── unzip → bin/[
│ ├── uptime → bin/[
│ ├── users → bin/[
│ ├── usleep → bin/[
│ ├── uudecode → bin/[
│ ├── uuencode → bin/[
│ ├── vconfig → bin/[
│ ├── vi → bin/[
│ ├── vlock → bin/[
│ ├── volname → bin/[
│ ├── w → bin/[
│ ├── wall → bin/[
│ ├── watch → bin/[
│ ├── watchdog → bin/[
│ ├── wc → bin/[
│ ├── wget → bin/[
│ ├── which → bin/[
│ ├── who → bin/[
│ ├── whoami → bin/[
│ ├── whois → bin/[
│ ├── xargs → bin/[
│ ├── xxd → bin/[
│ ├── xz → bin/[
│ ├── xzcat → bin/[
│ ├── yes → bin/[
│ ├── zcat → bin/[
│ └── zcip → bin/[
├── dev
├── etc
│ ├── group
│ ├── localtime
│ ├── network
│ │ ├── if-down.d
│ │ ├── if-post-down.d
│ │ ├── if-pre-up.d
│ │ └── if-up.d
│ ├── passwd
│ └── shadow
├── home
├── root
├── tmp
├── usr
│ └── sbin
└── var
├── spool
│ └── mail
└── www

11
ui/testdata/TestFileTreePageDown.txt vendored Normal file
View file

@ -0,0 +1,11 @@
-rwxr-xr-x 0:0 0 B │ ├── cat → bin/[
-rwxr-xr-x 0:0 0 B │ ├── chat → bin/[
-rwxr-xr-x 0:0 0 B │ ├── chattr → bin/[
-rwxr-xr-x 0:0 0 B │ ├── chgrp → bin/[
-rwxr-xr-x 0:0 0 B │ ├── chmod → bin/[
-rwxr-xr-x 0:0 0 B │ ├── chown → bin/[
-rwxr-xr-x 0:0 0 B │ ├── chpasswd → bin/[
-rwxr-xr-x 0:0 0 B │ ├── chpst → bin/[
-rwxr-xr-x 0:0 0 B │ ├── chroot → bin/[
-rwxr-xr-x 0:0 0 B │ ├── chrt → bin/[

11
ui/testdata/TestFileTreePageUp.txt vendored Normal file
View file

@ -0,0 +1,11 @@
-rwxr-xr-x 0:0 0 B │ ├── arch → bin/[
-rwxr-xr-x 0:0 0 B │ ├── arp → bin/[
-rwxr-xr-x 0:0 0 B │ ├── arping → bin/[
-rwxr-xr-x 0:0 0 B │ ├── ash → bin/[
-rwxr-xr-x 0:0 0 B │ ├── awk → bin/[
-rwxr-xr-x 0:0 0 B │ ├── base64 → bin/[
-rwxr-xr-x 0:0 0 B │ ├── basename → bin/[
-rwxr-xr-x 0:0 0 B │ ├── beep → bin/[
-rwxr-xr-x 0:0 0 B │ ├── blkdiscard → bin/[
-rwxr-xr-x 0:0 0 B │ ├── blkid → bin/[

View file

@ -0,0 +1,22 @@
├── bin
│ ├── [
│ ├── [[ → bin/[
│ ├── acpid → bin/[
│ ├── add-shell → bin/[
│ ├── addgroup → bin/[
│ ├── adduser → bin/[
│ ├── adjtimex → bin/[
│ ├── ar → bin/[
│ ├── arch → bin/[
│ ├── arp → bin/[
│ ├── arping → bin/[
│ ├── ash → bin/[
│ ├── awk → bin/[
│ ├── base64 → bin/[
│ ├── basename → bin/[
│ ├── beep → bin/[
│ ├── blkdiscard → bin/[
│ ├── blkid → bin/[
│ ├── blockdev → bin/[
│ ├── bootchartd → bin/[

23
ui/testdata/TestFileTreeSelectLayer.txt vendored Normal file
View file

@ -0,0 +1,23 @@
drwxr-xr-x 0:0 1.2 MB ├─⊕ bin
drwxr-xr-x 0:0 0 B ├── dev
drwxr-xr-x 0:0 1.0 kB ├── etc
-rw-rw-r-- 0:0 307 B │ ├── group
-rw-r--r-- 0:0 127 B │ ├── localtime
drwxr-xr-x 0:0 0 B │ ├── network
drwxr-xr-x 0:0 0 B │ │ ├── if-down.d
drwxr-xr-x 0:0 0 B │ │ ├── if-post-down.d
drwxr-xr-x 0:0 0 B │ │ ├── if-pre-up.d
drwxr-xr-x 0:0 0 B │ │ └── if-up.d
-rw-r--r-- 0:0 340 B │ ├── passwd
-rw------- 0:0 243 B │ └── shadow
drwxr-xr-x 65534:65534 0 B ├── home
drwx------ 0:0 0 B ├── root
-rw-rw-r-- 0:0 6.4 kB ├── somefile.txt
drwxrwxrwx 0:0 0 B ├── tmp
drwxr-xr-x 0:0 0 B ├── usr
drwxr-xr-x 1:1 0 B │ └── sbin
drwxr-xr-x 0:0 0 B └── var
drwxr-xr-x 0:0 0 B ├── spool
drwxr-xr-x 8:8 0 B │ └── mail
drwxr-xr-x 0:0 0 B └── www

204
ui/ui.go
View file

@ -2,7 +2,6 @@ package ui
import (
"errors"
"fmt"
"github.com/fatih/color"
"github.com/jroimartin/gocui"
"github.com/sirupsen/logrus"
@ -11,7 +10,6 @@ import (
"github.com/wagoodman/dive/image"
"github.com/wagoodman/dive/utils"
"github.com/wagoodman/keybinding"
"log"
)
const debug = false
@ -20,17 +18,17 @@ const debug = false
// var onExit func()
// debugPrint writes the given string to the debug pane (if the debug pane is enabled)
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)
}
}
}
// func debugPrint(s string) {
// if Controllers.Tree != nil && Controllers.Tree.gui != nil {
// v, _ := Controllers.Tree.gui.View("debug")
// if v != nil {
// if len(v.BufferLines()) > 20 {
// v.Clear()
// }
// _, _ = fmt.Fprintln(v, s)
// }
// }
// }
// Formatting defines standard functions for formatting UI sections.
var Formatting struct {
@ -44,13 +42,13 @@ var Formatting struct {
CompareBottom func(...interface{}) string
}
// Views contains all rendered UI panes.
var Views struct {
Tree *FileTreeView
Layer *LayerView
Status *StatusView
Filter *FilterView
Details *DetailsView
// Controllers contains all rendered UI panes.
var Controllers struct {
Tree *FileTreeController
Layer *LayerController
Status *StatusController
Filter *FilterController
Details *DetailsController
lookup map[string]View
}
@ -60,6 +58,8 @@ var GlobalKeybindings struct {
filterView []keybinding.Key
}
var lastX, lastY int
// View defines the a renderable terminal screen pane.
type View interface {
Setup(*gocui.View, *gocui.View) error
@ -72,14 +72,12 @@ type View interface {
}
// toggleView switches between the file view and the layer view and re-renders the screen.
func toggleView(g *gocui.Gui, v *gocui.View) error {
if v == nil || v.Name() == Views.Layer.Name {
_, err := g.SetCurrentView(Views.Tree.Name)
Update()
Render()
return err
func toggleView(g *gocui.Gui, v *gocui.View) (err error) {
if v == nil || v.Name() == Controllers.Layer.Name {
_, err = g.SetCurrentView(Controllers.Tree.Name)
} else {
_, err = g.SetCurrentView(Controllers.Layer.Name)
}
_, err := g.SetCurrentView(Views.Layer.Name)
Update()
Render()
return err
@ -88,21 +86,24 @@ func toggleView(g *gocui.Gui, v *gocui.View) error {
// toggleFilterView shows/hides the file tree filter pane.
func toggleFilterView(g *gocui.Gui, v *gocui.View) error {
// delete all user input from the tree view
Views.Filter.view.Clear()
Views.Filter.view.SetCursor(0, 0)
Controllers.Filter.view.Clear()
err := Controllers.Filter.view.SetCursor(0, 0)
if err != nil {
return err
}
// toggle hiding
Views.Filter.hidden = !Views.Filter.hidden
Controllers.Filter.hidden = !Controllers.Filter.hidden
if !Views.Filter.hidden {
_, err := g.SetCurrentView(Views.Filter.Name)
if !Controllers.Filter.hidden {
_, err := g.SetCurrentView(Controllers.Filter.Name)
if err != nil {
return err
}
Update()
Render()
} else {
toggleView(g, v)
return toggleView(g, v)
}
return nil
@ -110,31 +111,29 @@ func toggleFilterView(g *gocui.Gui, v *gocui.View) error {
// CursorDown moves the cursor down in the currently selected gocui pane, scrolling the screen as needed.
func CursorDown(g *gocui.Gui, v *gocui.View) error {
cx, cy := v.Cursor()
// if there isn't a next line
line, err := v.Line(cy + 1)
if err != nil {
// todo: handle error
}
if len(line) == 0 {
return errors.New("unable to move cursor down, empty line")
}
if err := v.SetCursor(cx, cy+1); err != nil {
ox, oy := v.Origin()
if err := v.SetOrigin(ox, oy+1); err != nil {
return err
}
}
return nil
return CursorStep(g, v, 1)
}
// CursorUp moves the cursor up in the currently selected gocui pane, scrolling the screen as needed.
func CursorUp(g *gocui.Gui, v *gocui.View) error {
ox, oy := v.Origin()
return CursorStep(g, v, -1)
}
// Moves the cursor the given step distance, setting the origin to the new cursor line
func CursorStep(g *gocui.Gui, v *gocui.View, step int) error {
cx, cy := v.Cursor()
if err := v.SetCursor(cx, cy-1); err != nil && oy > 0 {
if err := v.SetOrigin(ox, oy-1); err != nil {
// if there isn't a next line
line, err := v.Line(cy + step)
if err != nil {
return err
}
if len(line) == 0 {
return errors.New("unable to move the cursor, empty line")
}
if err := v.SetCursor(cx, cy+step); err != nil {
ox, oy := v.Origin()
if err := v.SetOrigin(ox, oy+step); err != nil {
return err
}
}
@ -179,7 +178,7 @@ func isNewView(errs ...error) bool {
if err == nil {
return false
}
if err != nil && err != gocui.ErrUnknownView {
if err != gocui.ErrUnknownView {
return false
}
}
@ -192,6 +191,14 @@ func layout(g *gocui.Gui) error {
// TODO: this logic should be refactored into an abstraction that takes care of the math for us
maxX, maxY := g.Size()
var resized bool
if maxX != lastX {
resized = true
}
if maxY != lastY {
resized = true
}
lastX, lastY = maxX, maxY
fileTreeSplitRatio := viper.GetFloat64("filetree.pane-width")
if fileTreeSplitRatio >= 1 || fileTreeSplitRatio <= 0 {
logrus.Errorf("invalid config value: 'filetree.pane-width' should be 0 < value < 1, given '%v'", fileTreeSplitRatio)
@ -212,7 +219,7 @@ func layout(g *gocui.Gui) error {
statusBarIndex := 1
filterBarIndex := 2
layersHeight := len(Views.Layer.Layers) + headerRows + 1 // layers + header + base image layer row
layersHeight := len(Controllers.Layer.Layers) + headerRows + 1 // layers + header + base image layer row
maxLayerHeight := int(0.75 * float64(maxY))
if layersHeight > maxLayerHeight {
layersHeight = maxLayerHeight
@ -221,7 +228,7 @@ func layout(g *gocui.Gui) error {
var view, header *gocui.View
var viewErr, headerErr, err error
if Views.Filter.hidden {
if Controllers.Filter.hidden {
bottomRows--
filterBarHeight = 0
}
@ -236,43 +243,48 @@ func layout(g *gocui.Gui) error {
}
// Layers
view, viewErr = g.SetView(Views.Layer.Name, -1, -1+headerRows, splitCols, layersHeight)
header, headerErr = g.SetView(Views.Layer.Name+"header", -1, -1, splitCols, headerRows)
view, viewErr = g.SetView(Controllers.Layer.Name, -1, -1+headerRows, splitCols, layersHeight)
header, headerErr = g.SetView(Controllers.Layer.Name+"header", -1, -1, splitCols, headerRows)
if isNewView(viewErr, headerErr) {
Views.Layer.Setup(view, header)
_ = Controllers.Layer.Setup(view, header)
if _, err = g.SetCurrentView(Views.Layer.Name); err != nil {
if _, err = g.SetCurrentView(Controllers.Layer.Name); err != nil {
return err
}
// since we are selecting the view, we should rerender to indicate it is selected
Views.Layer.Render()
_ = Controllers.Layer.Render()
}
// Details
view, viewErr = g.SetView(Views.Details.Name, -1, -1+layersHeight+headerRows, splitCols, maxY-bottomRows)
header, headerErr = g.SetView(Views.Details.Name+"header", -1, -1+layersHeight, splitCols, layersHeight+headerRows)
view, viewErr = g.SetView(Controllers.Details.Name, -1, -1+layersHeight+headerRows, splitCols, maxY-bottomRows)
header, headerErr = g.SetView(Controllers.Details.Name+"header", -1, -1+layersHeight, splitCols, layersHeight+headerRows)
if isNewView(viewErr, headerErr) {
Views.Details.Setup(view, header)
_ = Controllers.Details.Setup(view, header)
}
// Filetree
view, viewErr = g.SetView(Views.Tree.Name, splitCols, -1+headerRows, debugCols, maxY-bottomRows)
header, headerErr = g.SetView(Views.Tree.Name+"header", splitCols, -1, debugCols, headerRows)
if isNewView(viewErr, headerErr) {
Views.Tree.Setup(view, header)
offset := 0
if !Controllers.Tree.vm.ShowAttributes {
offset = 1
}
view, viewErr = g.SetView(Controllers.Tree.Name, splitCols, -1+headerRows-offset, debugCols, maxY-bottomRows)
header, headerErr = g.SetView(Controllers.Tree.Name+"header", splitCols, -1, debugCols, headerRows-offset)
if isNewView(viewErr, headerErr) {
_ = Controllers.Tree.Setup(view, header)
}
_ = Controllers.Tree.onLayoutChange(resized)
// Status Bar
view, viewErr = g.SetView(Views.Status.Name, -1, maxY-statusBarHeight-statusBarIndex, maxX, maxY-(statusBarIndex-1))
view, viewErr = g.SetView(Controllers.Status.Name, -1, maxY-statusBarHeight-statusBarIndex, maxX, maxY-(statusBarIndex-1))
if isNewView(viewErr, headerErr) {
Views.Status.Setup(view, nil)
_ = Controllers.Status.Setup(view, nil)
}
// Filter Bar
view, viewErr = g.SetView(Views.Filter.Name, len(Views.Filter.headerStr)-1, maxY-filterBarHeight-filterBarIndex, maxX, maxY-(filterBarIndex-1))
header, headerErr = g.SetView(Views.Filter.Name+"header", -1, maxY-filterBarHeight-filterBarIndex, len(Views.Filter.headerStr), maxY-(filterBarIndex-1))
view, viewErr = g.SetView(Controllers.Filter.Name, len(Controllers.Filter.headerStr)-1, maxY-filterBarHeight-filterBarIndex, maxX, maxY-(filterBarIndex-1))
header, headerErr = g.SetView(Controllers.Filter.Name+"header", -1, maxY-filterBarHeight-filterBarIndex, len(Controllers.Filter.headerStr), maxY-(filterBarIndex-1))
if isNewView(viewErr, headerErr) {
Views.Filter.Setup(view, header)
_ = Controllers.Filter.Setup(view, header)
}
return nil
@ -280,16 +292,16 @@ func layout(g *gocui.Gui) error {
// Update refreshes the state objects for future rendering.
func Update() {
for _, view := range Views.lookup {
view.Update()
for _, view := range Controllers.lookup {
_ = view.Update()
}
}
// Render flushes the state objects to the screen.
func Render() {
for _, view := range Views.lookup {
for _, view := range Controllers.lookup {
if view.IsVisible() {
view.Render()
_ = view.Render()
}
}
}
@ -297,9 +309,9 @@ func Render() {
// renderStatusOption formats key help bindings-to-title pairs.
func renderStatusOption(control, title string, selected bool) string {
if selected {
return Formatting.StatusSelected("▏") + Formatting.StatusControlSelected(control) + Formatting.StatusSelected(" "+title+" ")
return Formatting.StatusSelected("▏") + Formatting.StatusControlSelected(control) + Formatting.StatusSelected(" "+title+" ")
} else {
return Formatting.StatusNormal("▏") + Formatting.StatusControlNormal(control) + Formatting.StatusNormal(" "+title+" ")
return Formatting.StatusNormal("▏") + Formatting.StatusControlNormal(control) + Formatting.StatusNormal(" "+title+" ")
}
}
@ -318,40 +330,40 @@ func Run(analysis *image.AnalysisResult, cache filetree.TreeCache) {
var err error
GlobalKeybindings.quit, err = keybinding.ParseAll(viper.GetString("keybinding.quit"))
if err != nil {
log.Panicln(err)
logrus.Error(err)
}
GlobalKeybindings.toggleView, err = keybinding.ParseAll(viper.GetString("keybinding.toggle-view"))
if err != nil {
log.Panicln(err)
logrus.Error(err)
}
GlobalKeybindings.filterView, err = keybinding.ParseAll(viper.GetString("keybinding.filter-files"))
if err != nil {
log.Panicln(err)
logrus.Error(err)
}
g, err := gocui.NewGui(gocui.OutputNormal)
if err != nil {
log.Panicln(err)
logrus.Error(err)
}
utils.SetUi(g)
defer g.Close()
Views.lookup = make(map[string]View)
Controllers.lookup = make(map[string]View)
Views.Layer = NewLayerView("side", g, analysis.Layers)
Views.lookup[Views.Layer.Name] = Views.Layer
Controllers.Layer = NewLayerController("side", g, analysis.Layers)
Controllers.lookup[Controllers.Layer.Name] = Controllers.Layer
Views.Tree = NewFileTreeView("main", g, filetree.StackTreeRange(analysis.RefTrees, 0, 0), analysis.RefTrees, cache)
Views.lookup[Views.Tree.Name] = Views.Tree
Controllers.Tree = NewFileTreeController("main", g, filetree.StackTreeRange(analysis.RefTrees, 0, 0), analysis.RefTrees, cache)
Controllers.lookup[Controllers.Tree.Name] = Controllers.Tree
Views.Status = NewStatusView("status", g)
Views.lookup[Views.Status.Name] = Views.Status
Controllers.Status = NewStatusController("status", g)
Controllers.lookup[Controllers.Status.Name] = Controllers.Status
Views.Filter = NewFilterView("command", g)
Views.lookup[Views.Filter.Name] = Views.Filter
Controllers.Filter = NewFilterController("command", g)
Controllers.lookup[Controllers.Filter.Name] = Controllers.Filter
Views.Details = NewDetailsView("details", g, analysis.Efficiency, analysis.Inefficiencies)
Views.lookup[Views.Details.Name] = Views.Details
Controllers.Details = NewDetailsController("details", g, analysis.Efficiency, analysis.Inefficiencies)
Controllers.lookup[Controllers.Details.Name] = Controllers.Details
g.Cursor = false
//g.Mouse = true
@ -368,11 +380,11 @@ func Run(analysis *image.AnalysisResult, cache filetree.TreeCache) {
Render()
if err := keyBindings(g); err != nil {
log.Panicln(err)
logrus.Error(err)
}
if err := g.MainLoop(); err != nil && err != gocui.ErrQuit {
log.Panicln(err)
logrus.Error(err)
}
utils.Exit(0)
}

View file

@ -3,7 +3,6 @@ package utils
import (
"fmt"
"github.com/jroimartin/gocui"
"github.com/k0kubun/go-ansi"
"github.com/sirupsen/logrus"
"os"
)
@ -31,5 +30,4 @@ func Cleanup() {
if ui != nil {
ui.Close()
}
ansi.CursorShow()
}

View file

@ -1,49 +0,0 @@
package utils
import (
"fmt"
"strings"
)
type progressBar struct {
width int
percent int
rawTotal int64
rawCurrent int64
}
func NewProgressBar(total int64, width int) *progressBar {
return &progressBar{
rawTotal: total,
width: width,
}
}
func (pb *progressBar) Done() {
pb.rawCurrent = pb.rawTotal
pb.percent = 100
}
func (pb *progressBar) Update(currentValue int64) (hasChanged bool) {
pb.rawCurrent = currentValue
percent := int(100.0 * (float64(pb.rawCurrent) / float64(pb.rawTotal)))
if percent != pb.percent {
hasChanged = true
}
pb.percent = percent
return hasChanged
}
func (pb *progressBar) String() string {
done := int((pb.percent * pb.width) / 100.0)
if done > pb.width {
done = pb.width
}
todo := pb.width - done
if todo < 0 {
todo = 0
}
head := 1
return "[" + strings.Repeat("=", done) + strings.Repeat(">", head) + strings.Repeat(" ", todo) + "]" + fmt.Sprintf(" %d %% (%d/%d)", pb.percent, pb.rawCurrent, pb.rawTotal)
}