From beafe629f8890a7d8f5d84884db863068b14feb7 Mon Sep 17 00:00:00 2001 From: Robin Mitra Date: Thu, 6 Jun 2019 09:24:12 +0100 Subject: [PATCH 1/8] Add initial implementation of `browse` command. --- cmd/browse/browse.go | 237 +++++++++++++++++++++++++++++++++++++++++++ cmd/root.go | 2 + go.mod | 3 + go.sum | 14 +++ 4 files changed, 256 insertions(+) create mode 100644 cmd/browse/browse.go diff --git a/cmd/browse/browse.go b/cmd/browse/browse.go new file mode 100644 index 0000000..9d85190 --- /dev/null +++ b/cmd/browse/browse.go @@ -0,0 +1,237 @@ +package browse + +import ( + "errors" + "fmt" + "github.com/gdamore/tcell" + "github.com/rivo/tview" + "github.com/robinmitra/forest/formatter" + "github.com/spf13/cobra" + "log" + "os" + "path/filepath" + "regexp" + "strings" +) + +type options struct { + tree bool + root string +} + +func (o *options) initialise(cmd *cobra.Command, args []string) { + if len(args) != 0 { + r, _ := regexp.Compile("/$") + o.root = r.ReplaceAllString(args[0], "") + } else { + o.root = "." + } + if tree, _ := cmd.Flags().GetBool("tree"); tree { + o.tree = tree + } +} + +func (o *options) validate() { + if err := o.validatePath(os.Stat(o.root)); err != nil { + log.Fatal(err) + } +} + +func (o *options) validatePath(info os.FileInfo, err error) error { + if os.IsNotExist(err) { + return errors.New(fmt.Sprintf("Directory \"%s\" does not exist", o.root)) + } + return err +} + +func (o *options) run() { + if !o.tree { + log.Fatal("Unknown display mode") + return + } + node := buildFileTree(o.root) + renderTree(node) +} + +var cmd = &cobra.Command{ + Use: "browse", + Short: "Interactively browse directories and files", +} + +func NewInteractiveCmd() *cobra.Command { + o := options{} + + cmd.Run = func(cmd *cobra.Command, args []string) { + o.initialise(cmd, args) + o.validate() + o.run() + } + + cmd.Flags().BoolVarP( + &o.tree, + "tree", + "t", + true, + "browse the file tree", + ) + + return cmd +} + +func renderTree(n *node) { + getNodeText := func(n *node) string { + return fmt.Sprintf("%s (%s, %d)", n.name, formatter.HumaniseStorage(n.size), len(n.children)) + } + + root := tview.NewTreeNode(getNodeText(n)).SetReference(n).SetColor(tcell.ColorRed) + tree := tview.NewTreeView().SetRoot(root).SetCurrentNode(root) + + addChildren := func(n *tview.TreeNode) { + refNode := n.GetReference().(*node) + if len(refNode.children) > 0 { + for i, c := range refNode.children { + cNode := &refNode.children[i] + childNode := tview.NewTreeNode(getNodeText(cNode)).SetReference(cNode) + if c.isDir { + childNode.SetColor(tcell.ColorGreen) + } + n.AddChild(childNode) + } + } + } + + addChildren(root) + + tree.SetSelectedFunc(func(n *tview.TreeNode) { + refNode := n.GetReference() + if refNode == nil { + // Selecting the root node does nothing. + return + } + children := n.GetChildren() + if len(children) == 0 { + // Load and show files in this directory. + addChildren(n) + } else { + // Collapse if visible, expand if collapsed. + n.SetExpanded(!n.IsExpanded()) + } + }) + + if err := tview.NewApplication().SetRoot(tree, true).Run(); err != nil { + panic(err) + } +} + +type node struct { + name string + isDir bool + size int64 + children []node + parent *node +} + +func (n *node) has(name string) bool { + for _, c := range n.children { + if c.name == name { + return true + } + } + return false +} + +func (n *node) getChild(name string) (*node, bool) { + for i, c := range n.children { + if c.name == name { + // Get a reference to array element directly, since range returns copies. + return &n.children[i], true + } + } + return nil, false +} + +func AggregateChildrenSize(n *node) int64 { + var s int64 + for _, c := range n.children { + s += c.size + } + return s +} + +func buildNodesFromPath(n *node, path string, info os.FileInfo) { + nodeNames := strings.Split(path, "/") + currNodeName := nodeNames[0] + nestedNodeNames := nodeNames[1:] + // Last or trailing node + if len(nestedNodeNames) == 0 { + newNode := node{name: currNodeName, parent: n} + if info.IsDir() { + newNode.isDir = true + } else { + newNode.isDir = false + newNode.size = info.Size() + n.size += info.Size() + } + n.children = append(n.children, newNode) + } else { + if existingNode, ok := n.getChild(currNodeName); ok { + buildNodesFromPath(existingNode, strings.Join(nestedNodeNames, "/"), info) + } else { + newNode := node{name: currNodeName, isDir: true, parent: n} + buildNodesFromPath(&newNode, strings.Join(nestedNodeNames, "/"), info) + n.children = append(n.children, newNode) + } + n.size = AggregateChildrenSize(n) + } +} + +func processFile(node *node, rootPath string) filepath.WalkFunc { + return func(path string, info os.FileInfo, err error) error { + if err != nil { + return err + } + name := info.Name() + if path == "." || path == rootPath { + node.name = name + return nil + } + if rootPath != "." { + buildNodesFromPath(node, strings.Replace(path, rootPath+"/", "", 1), info) + } else { + buildNodesFromPath(node, path, info) + } + return nil + } +} + +func buildFileTree(root string) *node { + rootName := root + if root != "." { + path := strings.Split(root, "/") + rootName = path[len(path)-1] + } + rootNode := node{name: rootName, isDir: true} + if err := filepath.Walk(root, processFile(&rootNode, root)); err != nil { + log.Fatal(err) + } + return &rootNode +} + +func debugTree(n *node, spacer string) { + var b strings.Builder + fmt.Fprintf(&b, "%s %s", spacer, n.name) + fmt.Fprintf(&b, " (") + fmt.Fprintf(&b, "dir: %t", n.isDir) + if n.parent != nil { + fmt.Fprintf(&b, ", parent: %s", n.parent.name) + } + fmt.Fprintf(&b, ", size: %d", n.size) + fmt.Fprintf(&b, ")") + fmt.Println(b.String()) + + if n.isDir && len(n.children) > 0 { + for i, _ := range n.children { + debugTree(&n.children[i], fmt.Sprintf("%s-", spacer)) + } + } +} diff --git a/cmd/root.go b/cmd/root.go index 2035c4d..2874677 100644 --- a/cmd/root.go +++ b/cmd/root.go @@ -2,6 +2,7 @@ package cmd import ( "github.com/robinmitra/forest/cmd/analyse" + "github.com/robinmitra/forest/cmd/browse" "github.com/robinmitra/forest/cmd/version" log "github.com/sirupsen/logrus" "github.com/spf13/cobra" @@ -47,6 +48,7 @@ func NewRootCmd() *cobra.Command { cmd.AddCommand(analyse.NewAnalyseCmd()) cmd.AddCommand(version.NewVersionCmd(VERSION)) + cmd.AddCommand(browse.NewInteractiveCmd()) return cmd } diff --git a/go.mod b/go.mod index ed3eeb8..5ed0104 100644 --- a/go.mod +++ b/go.mod @@ -4,8 +4,11 @@ go 1.12 require ( github.com/cheynewallace/tabby v1.1.0 + github.com/gdamore/tcell v1.1.1 github.com/gosuri/uilive v0.0.2 github.com/inconshreveable/mousetrap v1.0.0 // indirect + github.com/rivo/tview v0.0.0-20190406182340-90b4da1bd64c + github.com/rivo/uniseg v0.0.0-20190313204849-f699dde9c340 // indirect github.com/sirupsen/logrus v1.4.1 github.com/spf13/cobra v0.0.3 github.com/spf13/pflag v1.0.3 // indirect diff --git a/go.sum b/go.sum index cb993fa..086b397 100644 --- a/go.sum +++ b/go.sum @@ -2,14 +2,26 @@ github.com/cheynewallace/tabby v1.1.0 h1:XtG/ZanoIvNZHfe0cClhWLzD/16GGF9UD7mMdWw github.com/cheynewallace/tabby v1.1.0/go.mod h1:Pba/6cUL8uYqvOc9RkyvFbHGrQ9wShyrn6/S/1OYVys= github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c= github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= +github.com/gdamore/encoding v1.0.0 h1:+7OoQ1Bc6eTm5niUzBa0Ctsh6JbMW6Ra+YNuAtDBdko= +github.com/gdamore/encoding v1.0.0/go.mod h1:alR0ol34c49FCSBLjhosxzcPHQbf2trDkoo5dl+VrEg= +github.com/gdamore/tcell v1.1.1 h1:U73YL+jMem2XfhvaIUfPO6MpJawaG92B2funXVb9qLs= +github.com/gdamore/tcell v1.1.1/go.mod h1:K1udHkiR3cOtlpKG5tZPD5XxrF7v2y7lDq7Whcj+xkQ= github.com/gosuri/uilive v0.0.2 h1:Q7HzS0bZt6eGom1okr1wDMYIf37MKYEzKDmZDYDJPdo= github.com/gosuri/uilive v0.0.2/go.mod h1:qkLSc0A5EXSP6B04TrN4oQoxqFI7A8XvoXSlJi8cwk8= github.com/inconshreveable/mousetrap v1.0.0 h1:Z8tu5sraLXCXIcARxBp/8cbvlwVa7Z1NHg9XEKhtSvM= github.com/inconshreveable/mousetrap v1.0.0/go.mod h1:PxqpIevigyE2G7u3NXJIT2ANytuPF1OarO4DADm73n8= 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/lucasb-eyer/go-colorful v0.0.0-20181028223441-12d3b2882a08 h1:5MnxBC15uMxFv5FY/J/8vzyaBiArCOkMdFT9Jsw78iY= +github.com/lucasb-eyer/go-colorful v0.0.0-20181028223441-12d3b2882a08/go.mod h1:NXg0ArsFk0Y01623LgUqoqcouGDB+PwCCQlrwrG6xJ4= +github.com/mattn/go-runewidth v0.0.4 h1:2BvfKmzob6Bmd4YsL0zygOqfdFnK7GR4QL06Do4/p7Y= +github.com/mattn/go-runewidth v0.0.4/go.mod h1:LwmH8dsx7+W8Uxz3IHJYH5QSwggIsqBzpuz5H//U1FU= github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= +github.com/rivo/tview v0.0.0-20190406182340-90b4da1bd64c h1:g/UvEDB8RutkfYbTULmcCUpN0uQCVeh6j4bHt+Te8yM= +github.com/rivo/tview v0.0.0-20190406182340-90b4da1bd64c/go.mod h1:J4W+hErFfITUbyFAEXizpmkuxX7ZN56dopxHB4XQhMw= +github.com/rivo/uniseg v0.0.0-20190313204849-f699dde9c340 h1:nOZbL5f2xmBAHWYrrHbHV1xatzZirN++oOQ3g83Ypgs= +github.com/rivo/uniseg v0.0.0-20190313204849-f699dde9c340/go.mod h1:SOLvOL4ybwgLJ6TYoX/rtaJ8EGOulH4XU7E9/TLrTCE= github.com/sirupsen/logrus v1.4.1 h1:GL2rEmy6nsikmW0r8opw9JIRScdMF5hA8cOYLH7In1k= github.com/sirupsen/logrus v1.4.1/go.mod h1:ni0Sbl8bgC9z8RoU9G6nDWqqs/fq4eDPysMBDgk/93Q= github.com/spf13/cobra v0.0.3 h1:ZlrZ4XsMRm04Fr5pSFxBgfND2EBVa1nLpiy1stUsX/8= @@ -23,3 +35,5 @@ golang.org/x/sys v0.0.0-20180905080454-ebe1bf3edb33 h1:I6FyU15t786LL7oL/hn43zqTu golang.org/x/sys v0.0.0-20180905080454-ebe1bf3edb33/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= golang.org/x/text v0.3.0 h1:g61tztE5qeGQ89tm6NTjjM9VPIm088od1l6aSorWRWg= golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= +gopkg.in/DATA-DOG/go-sqlmock.v1 v1.3.0 h1:FVCohIoYO7IJoDDVpV2pdq7SgrMH6wHnuTyrdrxJNoY= +gopkg.in/DATA-DOG/go-sqlmock.v1 v1.3.0/go.mod h1:OdE7CF6DbADk7lN8LIKRzRJTTZXIjtWgA5THM5lhBAw= From 601e3eef9587c98c38575cd70d3cda889ece38b0 Mon Sep 17 00:00:00 2001 From: Robin Mitra Date: Wed, 12 Jun 2019 19:23:03 +0100 Subject: [PATCH 2/8] Re-organise code for `browse`. --- cmd/browse/browse.go | 166 +------------------------------------------ cmd/browse/node.go | 28 ++++++++ cmd/browse/tree.go | 143 +++++++++++++++++++++++++++++++++++++ 3 files changed, 172 insertions(+), 165 deletions(-) create mode 100644 cmd/browse/node.go create mode 100644 cmd/browse/tree.go diff --git a/cmd/browse/browse.go b/cmd/browse/browse.go index 9d85190..0f098c5 100644 --- a/cmd/browse/browse.go +++ b/cmd/browse/browse.go @@ -3,15 +3,10 @@ package browse import ( "errors" "fmt" - "github.com/gdamore/tcell" - "github.com/rivo/tview" - "github.com/robinmitra/forest/formatter" "github.com/spf13/cobra" "log" "os" - "path/filepath" "regexp" - "strings" ) type options struct { @@ -49,8 +44,7 @@ func (o *options) run() { log.Fatal("Unknown display mode") return } - node := buildFileTree(o.root) - renderTree(node) + renderTree(buildFileTree(o.root)) } var cmd = &cobra.Command{ @@ -77,161 +71,3 @@ func NewInteractiveCmd() *cobra.Command { return cmd } - -func renderTree(n *node) { - getNodeText := func(n *node) string { - return fmt.Sprintf("%s (%s, %d)", n.name, formatter.HumaniseStorage(n.size), len(n.children)) - } - - root := tview.NewTreeNode(getNodeText(n)).SetReference(n).SetColor(tcell.ColorRed) - tree := tview.NewTreeView().SetRoot(root).SetCurrentNode(root) - - addChildren := func(n *tview.TreeNode) { - refNode := n.GetReference().(*node) - if len(refNode.children) > 0 { - for i, c := range refNode.children { - cNode := &refNode.children[i] - childNode := tview.NewTreeNode(getNodeText(cNode)).SetReference(cNode) - if c.isDir { - childNode.SetColor(tcell.ColorGreen) - } - n.AddChild(childNode) - } - } - } - - addChildren(root) - - tree.SetSelectedFunc(func(n *tview.TreeNode) { - refNode := n.GetReference() - if refNode == nil { - // Selecting the root node does nothing. - return - } - children := n.GetChildren() - if len(children) == 0 { - // Load and show files in this directory. - addChildren(n) - } else { - // Collapse if visible, expand if collapsed. - n.SetExpanded(!n.IsExpanded()) - } - }) - - if err := tview.NewApplication().SetRoot(tree, true).Run(); err != nil { - panic(err) - } -} - -type node struct { - name string - isDir bool - size int64 - children []node - parent *node -} - -func (n *node) has(name string) bool { - for _, c := range n.children { - if c.name == name { - return true - } - } - return false -} - -func (n *node) getChild(name string) (*node, bool) { - for i, c := range n.children { - if c.name == name { - // Get a reference to array element directly, since range returns copies. - return &n.children[i], true - } - } - return nil, false -} - -func AggregateChildrenSize(n *node) int64 { - var s int64 - for _, c := range n.children { - s += c.size - } - return s -} - -func buildNodesFromPath(n *node, path string, info os.FileInfo) { - nodeNames := strings.Split(path, "/") - currNodeName := nodeNames[0] - nestedNodeNames := nodeNames[1:] - // Last or trailing node - if len(nestedNodeNames) == 0 { - newNode := node{name: currNodeName, parent: n} - if info.IsDir() { - newNode.isDir = true - } else { - newNode.isDir = false - newNode.size = info.Size() - n.size += info.Size() - } - n.children = append(n.children, newNode) - } else { - if existingNode, ok := n.getChild(currNodeName); ok { - buildNodesFromPath(existingNode, strings.Join(nestedNodeNames, "/"), info) - } else { - newNode := node{name: currNodeName, isDir: true, parent: n} - buildNodesFromPath(&newNode, strings.Join(nestedNodeNames, "/"), info) - n.children = append(n.children, newNode) - } - n.size = AggregateChildrenSize(n) - } -} - -func processFile(node *node, rootPath string) filepath.WalkFunc { - return func(path string, info os.FileInfo, err error) error { - if err != nil { - return err - } - name := info.Name() - if path == "." || path == rootPath { - node.name = name - return nil - } - if rootPath != "." { - buildNodesFromPath(node, strings.Replace(path, rootPath+"/", "", 1), info) - } else { - buildNodesFromPath(node, path, info) - } - return nil - } -} - -func buildFileTree(root string) *node { - rootName := root - if root != "." { - path := strings.Split(root, "/") - rootName = path[len(path)-1] - } - rootNode := node{name: rootName, isDir: true} - if err := filepath.Walk(root, processFile(&rootNode, root)); err != nil { - log.Fatal(err) - } - return &rootNode -} - -func debugTree(n *node, spacer string) { - var b strings.Builder - fmt.Fprintf(&b, "%s %s", spacer, n.name) - fmt.Fprintf(&b, " (") - fmt.Fprintf(&b, "dir: %t", n.isDir) - if n.parent != nil { - fmt.Fprintf(&b, ", parent: %s", n.parent.name) - } - fmt.Fprintf(&b, ", size: %d", n.size) - fmt.Fprintf(&b, ")") - fmt.Println(b.String()) - - if n.isDir && len(n.children) > 0 { - for i, _ := range n.children { - debugTree(&n.children[i], fmt.Sprintf("%s-", spacer)) - } - } -} diff --git a/cmd/browse/node.go b/cmd/browse/node.go new file mode 100644 index 0000000..c274565 --- /dev/null +++ b/cmd/browse/node.go @@ -0,0 +1,28 @@ +package browse + +type node struct { + name string + isDir bool + size int64 + children []node + parent *node +} + +func (n *node) has(name string) bool { + for _, c := range n.children { + if c.name == name { + return true + } + } + return false +} + +func (n *node) getChild(name string) (*node, bool) { + for i, c := range n.children { + if c.name == name { + // Get a reference to array element directly, since range returns copies. + return &n.children[i], true + } + } + return nil, false +} diff --git a/cmd/browse/tree.go b/cmd/browse/tree.go new file mode 100644 index 0000000..a737148 --- /dev/null +++ b/cmd/browse/tree.go @@ -0,0 +1,143 @@ +package browse + +import ( + "fmt" + "github.com/gdamore/tcell" + "github.com/rivo/tview" + "github.com/robinmitra/forest/formatter" + "log" + "os" + "path/filepath" + "strings" +) + +func AggregateChildrenSize(n *node) int64 { + var s int64 + for _, c := range n.children { + s += c.size + } + return s +} + +func buildNodesFromPath(n *node, path string, info os.FileInfo) { + nodeNames := strings.Split(path, "/") + currNodeName := nodeNames[0] + nestedNodeNames := nodeNames[1:] + // Last or trailing node + if len(nestedNodeNames) == 0 { + newNode := node{name: currNodeName, parent: n} + if info.IsDir() { + newNode.isDir = true + } else { + newNode.isDir = false + newNode.size = info.Size() + n.size += info.Size() + } + n.children = append(n.children, newNode) + } else { + if existingNode, ok := n.getChild(currNodeName); ok { + buildNodesFromPath(existingNode, strings.Join(nestedNodeNames, "/"), info) + } else { + newNode := node{name: currNodeName, isDir: true, parent: n} + buildNodesFromPath(&newNode, strings.Join(nestedNodeNames, "/"), info) + n.children = append(n.children, newNode) + } + n.size = AggregateChildrenSize(n) + } +} + +func processFile(node *node, rootPath string) filepath.WalkFunc { + return func(path string, info os.FileInfo, err error) error { + if err != nil { + return err + } + name := info.Name() + if path == "." || path == rootPath { + node.name = name + return nil + } + if rootPath != "." { + buildNodesFromPath(node, strings.Replace(path, rootPath+"/", "", 1), info) + } else { + buildNodesFromPath(node, path, info) + } + return nil + } +} + +func buildFileTree(root string) *node { + rootName := root + if root != "." { + path := strings.Split(root, "/") + rootName = path[len(path)-1] + } + rootNode := node{name: rootName, isDir: true} + if err := filepath.Walk(root, processFile(&rootNode, root)); err != nil { + log.Fatal(err) + } + return &rootNode +} + +func renderTree(n *node) { + getNodeText := func(n *node) string { + return fmt.Sprintf("%s (%s, %d)", n.name, formatter.HumaniseStorage(n.size), len(n.children)) + } + + root := tview.NewTreeNode(getNodeText(n)).SetReference(n).SetColor(tcell.ColorRed) + tree := tview.NewTreeView().SetRoot(root).SetCurrentNode(root) + + addChildren := func(n *tview.TreeNode) { + refNode := n.GetReference().(*node) + if len(refNode.children) > 0 { + for i, c := range refNode.children { + cNode := &refNode.children[i] + childNode := tview.NewTreeNode(getNodeText(cNode)).SetReference(cNode) + if c.isDir { + childNode.SetColor(tcell.ColorGreen) + } + n.AddChild(childNode) + } + } + } + + addChildren(root) + + tree.SetSelectedFunc(func(n *tview.TreeNode) { + refNode := n.GetReference() + if refNode == nil { + // Selecting the root node does nothing. + return + } + children := n.GetChildren() + if len(children) == 0 { + // Load and show files in this directory. + addChildren(n) + } else { + // Collapse if visible, expand if collapsed. + n.SetExpanded(!n.IsExpanded()) + } + }) + + if err := tview.NewApplication().SetRoot(tree, true).Run(); err != nil { + panic(err) + } +} + +func debugTree(n *node, spacer string) { + var b strings.Builder + fmt.Fprintf(&b, "%s %s", spacer, n.name) + fmt.Fprintf(&b, " (") + fmt.Fprintf(&b, "dir: %t", n.isDir) + if n.parent != nil { + fmt.Fprintf(&b, ", parent: %s", n.parent.name) + } + fmt.Fprintf(&b, ", size: %d", n.size) + fmt.Fprintf(&b, ")") + fmt.Println(b.String()) + + if n.isDir && len(n.children) > 0 { + for i, _ := range n.children { + debugTree(&n.children[i], fmt.Sprintf("%s-", spacer)) + } + } +} From fdb6f296cf3ec5f780a3891d51fb482953366ea9 Mon Sep 17 00:00:00 2001 From: Robin Mitra Date: Wed, 19 Jun 2019 19:28:43 +0100 Subject: [PATCH 3/8] Upgrade `tview`. --- go.mod | 8 ++++---- go.sum | 25 +++++++++++++++---------- 2 files changed, 19 insertions(+), 14 deletions(-) diff --git a/go.mod b/go.mod index 5ed0104..67e7d12 100644 --- a/go.mod +++ b/go.mod @@ -4,13 +4,13 @@ go 1.12 require ( github.com/cheynewallace/tabby v1.1.0 - github.com/gdamore/tcell v1.1.1 + github.com/gdamore/tcell v1.1.2 github.com/gosuri/uilive v0.0.2 github.com/inconshreveable/mousetrap v1.0.0 // indirect - github.com/rivo/tview v0.0.0-20190406182340-90b4da1bd64c - github.com/rivo/uniseg v0.0.0-20190313204849-f699dde9c340 // indirect + github.com/rivo/tview v0.0.0-20190609162513-b62197ade412 github.com/sirupsen/logrus v1.4.1 github.com/spf13/cobra v0.0.3 github.com/spf13/pflag v1.0.3 // indirect - golang.org/x/text v0.3.0 + golang.org/x/sys v0.0.0-20190618155005-516e3c20635f // indirect + golang.org/x/text v0.3.2 ) diff --git a/go.sum b/go.sum index 086b397..950794e 100644 --- a/go.sum +++ b/go.sum @@ -1,27 +1,29 @@ +github.com/DATA-DOG/go-sqlmock v1.3.3 h1:CWUqKXe0s8A2z6qCgkP4Kru7wC11YoAnoupUKFDnH08= +github.com/DATA-DOG/go-sqlmock v1.3.3/go.mod h1:f/Ixk793poVmq4qj/V1dPUg2JEAKC73Q5eFN3EC/SaM= github.com/cheynewallace/tabby v1.1.0 h1:XtG/ZanoIvNZHfe0cClhWLzD/16GGF9UD7mMdWwYnCQ= github.com/cheynewallace/tabby v1.1.0/go.mod h1:Pba/6cUL8uYqvOc9RkyvFbHGrQ9wShyrn6/S/1OYVys= github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c= github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= github.com/gdamore/encoding v1.0.0 h1:+7OoQ1Bc6eTm5niUzBa0Ctsh6JbMW6Ra+YNuAtDBdko= github.com/gdamore/encoding v1.0.0/go.mod h1:alR0ol34c49FCSBLjhosxzcPHQbf2trDkoo5dl+VrEg= -github.com/gdamore/tcell v1.1.1 h1:U73YL+jMem2XfhvaIUfPO6MpJawaG92B2funXVb9qLs= -github.com/gdamore/tcell v1.1.1/go.mod h1:K1udHkiR3cOtlpKG5tZPD5XxrF7v2y7lDq7Whcj+xkQ= +github.com/gdamore/tcell v1.1.2 h1:Afe8cU6SECC06UmvaJ55Jr3Eh0tz/ywLjqWYqjGZp3s= +github.com/gdamore/tcell v1.1.2/go.mod h1:h3kq4HO9l2On+V9ed8w8ewqQEmGCSSHOgQ+2h8uzurE= github.com/gosuri/uilive v0.0.2 h1:Q7HzS0bZt6eGom1okr1wDMYIf37MKYEzKDmZDYDJPdo= github.com/gosuri/uilive v0.0.2/go.mod h1:qkLSc0A5EXSP6B04TrN4oQoxqFI7A8XvoXSlJi8cwk8= github.com/inconshreveable/mousetrap v1.0.0 h1:Z8tu5sraLXCXIcARxBp/8cbvlwVa7Z1NHg9XEKhtSvM= github.com/inconshreveable/mousetrap v1.0.0/go.mod h1:PxqpIevigyE2G7u3NXJIT2ANytuPF1OarO4DADm73n8= 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/lucasb-eyer/go-colorful v0.0.0-20181028223441-12d3b2882a08 h1:5MnxBC15uMxFv5FY/J/8vzyaBiArCOkMdFT9Jsw78iY= -github.com/lucasb-eyer/go-colorful v0.0.0-20181028223441-12d3b2882a08/go.mod h1:NXg0ArsFk0Y01623LgUqoqcouGDB+PwCCQlrwrG6xJ4= +github.com/lucasb-eyer/go-colorful v1.0.2 h1:mCMFu6PgSozg9tDNMMK3g18oJBX7oYGrC09mS6CXfO4= +github.com/lucasb-eyer/go-colorful v1.0.2/go.mod h1:0MS4r+7BZKSJ5mw4/S5MPN+qHFF1fYclkSPilDOKW0s= github.com/mattn/go-runewidth v0.0.4 h1:2BvfKmzob6Bmd4YsL0zygOqfdFnK7GR4QL06Do4/p7Y= github.com/mattn/go-runewidth v0.0.4/go.mod h1:LwmH8dsx7+W8Uxz3IHJYH5QSwggIsqBzpuz5H//U1FU= github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= -github.com/rivo/tview v0.0.0-20190406182340-90b4da1bd64c h1:g/UvEDB8RutkfYbTULmcCUpN0uQCVeh6j4bHt+Te8yM= -github.com/rivo/tview v0.0.0-20190406182340-90b4da1bd64c/go.mod h1:J4W+hErFfITUbyFAEXizpmkuxX7ZN56dopxHB4XQhMw= -github.com/rivo/uniseg v0.0.0-20190313204849-f699dde9c340 h1:nOZbL5f2xmBAHWYrrHbHV1xatzZirN++oOQ3g83Ypgs= -github.com/rivo/uniseg v0.0.0-20190313204849-f699dde9c340/go.mod h1:SOLvOL4ybwgLJ6TYoX/rtaJ8EGOulH4XU7E9/TLrTCE= +github.com/rivo/tview v0.0.0-20190609162513-b62197ade412 h1:muOFMct2jVhlSw9S3MRrxevpsAIPJeTh4e1Z7pEEQhE= +github.com/rivo/tview v0.0.0-20190609162513-b62197ade412/go.mod h1:+rKjP5+h9HMwWRpAfhIkkQ9KE3m3Nz5rwn7YtUpwgqk= +github.com/rivo/uniseg v0.0.0-20190513083848-b9f5b9457d44 h1:XKCbzPvK4/BbMXoMJOkYP2ANxiAEO0HM1xn6psSbXxY= +github.com/rivo/uniseg v0.0.0-20190513083848-b9f5b9457d44/go.mod h1:J6wj4VEh+S6ZtnVlnTBMWIodfgj8LQOQFoIToxlJtxc= github.com/sirupsen/logrus v1.4.1 h1:GL2rEmy6nsikmW0r8opw9JIRScdMF5hA8cOYLH7In1k= github.com/sirupsen/logrus v1.4.1/go.mod h1:ni0Sbl8bgC9z8RoU9G6nDWqqs/fq4eDPysMBDgk/93Q= github.com/spf13/cobra v0.0.3 h1:ZlrZ4XsMRm04Fr5pSFxBgfND2EBVa1nLpiy1stUsX/8= @@ -33,7 +35,10 @@ github.com/stretchr/testify v1.2.2 h1:bSDNvY7ZPG5RlJ8otE/7V6gMiyenm9RtJ7IUVIAoJ1 github.com/stretchr/testify v1.2.2/go.mod h1:a8OnRcib4nhh0OaRAV+Yts87kKdq0PP7pXfy6kDkUVs= golang.org/x/sys v0.0.0-20180905080454-ebe1bf3edb33 h1:I6FyU15t786LL7oL/hn43zqTuEGr4PN7F4XJ1p4E3Y8= golang.org/x/sys v0.0.0-20180905080454-ebe1bf3edb33/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= +golang.org/x/sys v0.0.0-20190618155005-516e3c20635f h1:dHNZYIYdq2QuU6w73vZ/DzesPbVlZVYZTtTZmrnsbQ8= +golang.org/x/sys v0.0.0-20190618155005-516e3c20635f/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/text v0.3.0 h1:g61tztE5qeGQ89tm6NTjjM9VPIm088od1l6aSorWRWg= golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= -gopkg.in/DATA-DOG/go-sqlmock.v1 v1.3.0 h1:FVCohIoYO7IJoDDVpV2pdq7SgrMH6wHnuTyrdrxJNoY= -gopkg.in/DATA-DOG/go-sqlmock.v1 v1.3.0/go.mod h1:OdE7CF6DbADk7lN8LIKRzRJTTZXIjtWgA5THM5lhBAw= +golang.org/x/text v0.3.2 h1:tW2bmiBqwgJj/UpqtC8EpXEZVYOwU0yG4iWbprSVAcs= +golang.org/x/text v0.3.2/go.mod h1:bEr9sfX3Q8Zfm5fL9x+3itogRgK3+ptLWKqgva+5dAk= +golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= From 3bb7a30887494d23f3bbfb1d9c694cffce9590a3 Mon Sep 17 00:00:00 2001 From: Robin Mitra Date: Wed, 19 Jun 2019 19:29:35 +0100 Subject: [PATCH 4/8] Make the `o` key toggle node expansion in tree view. --- cmd/browse/tree.go | 11 ++++++++++- 1 file changed, 10 insertions(+), 1 deletion(-) diff --git a/cmd/browse/tree.go b/cmd/browse/tree.go index a737148..a182a03 100644 --- a/cmd/browse/tree.go +++ b/cmd/browse/tree.go @@ -118,7 +118,16 @@ func renderTree(n *node) { } }) - if err := tview.NewApplication().SetRoot(tree, true).Run(); err != nil { + app := tview.NewApplication().SetRoot(tree, true) + + tree.SetInputCapture(func(event *tcell.EventKey) *tcell.EventKey { + if event.Key() == tcell.KeyRune && event.Rune() == 'o' { + return tcell.NewEventKey(tcell.KeyEnter, 0, tcell.ModNone) + } + return event + }) + + if err := app.Run(); err != nil { panic(err) } } From 3c54194681c7272c817062c1094bff42b9bad64f Mon Sep 17 00:00:00 2001 From: Robin Mitra Date: Wed, 19 Jun 2019 20:39:02 +0100 Subject: [PATCH 5/8] Add input path tests for `browse` command. --- cmd/browse/browse_test.go | 36 ++++++++++++++++++++++++++++++++++++ 1 file changed, 36 insertions(+) create mode 100644 cmd/browse/browse_test.go diff --git a/cmd/browse/browse_test.go b/cmd/browse/browse_test.go new file mode 100644 index 0000000..fa841f0 --- /dev/null +++ b/cmd/browse/browse_test.go @@ -0,0 +1,36 @@ +package browse + +import ( + "errors" + "github.com/spf13/cobra" + "os" + "testing" +) + +func TestInvalidPath(t *testing.T) { + cmd := cobra.Command{} + var args []string + o := options{} + o.initialise(&cmd, args) + var info os.FileInfo + + err := o.validatePath(info, errors.New("something went wrong")) + + if err == nil { + t.Errorf("Expected validation to fail when passing invalid path.") + } +} + +func TestValidPath(t *testing.T) { + cmd := cobra.Command{} + var args []string + o := options{} + o.initialise(&cmd, args) + var info os.FileInfo + + err := o.validatePath(info, nil) + + if err != nil { + t.Errorf("Expected validation to pass when passing valid path.") + } +} From db8e1d79400678442e0c7c4aeab1b25d30a3aca2 Mon Sep 17 00:00:00 2001 From: Robin Mitra Date: Wed, 26 Jun 2019 09:38:36 +0100 Subject: [PATCH 6/8] Refactor the logic around adding child node to tree to include calculating size. --- cmd/browse/node.go | 19 ++++++++++++++++--- cmd/browse/tree.go | 19 +++++-------------- 2 files changed, 21 insertions(+), 17 deletions(-) diff --git a/cmd/browse/node.go b/cmd/browse/node.go index c274565..d835b76 100644 --- a/cmd/browse/node.go +++ b/cmd/browse/node.go @@ -4,11 +4,16 @@ type node struct { name string isDir bool size int64 - children []node + children []*node parent *node } -func (n *node) has(name string) bool { +func (n *node) addChild(c *node) { + n.children = append(n.children, c) + n.size += c.size +} + +func (n *node) hasChild(name string) bool { for _, c := range n.children { if c.name == name { return true @@ -21,8 +26,16 @@ func (n *node) getChild(name string) (*node, bool) { for i, c := range n.children { if c.name == name { // Get a reference to array element directly, since range returns copies. - return &n.children[i], true + return n.children[i], true } } return nil, false } + +func (n *node) recalculateSize() { + var s int64 + for _, c := range n.children { + s += c.size + } + n.size = s +} diff --git a/cmd/browse/tree.go b/cmd/browse/tree.go index a182a03..56c4dc1 100644 --- a/cmd/browse/tree.go +++ b/cmd/browse/tree.go @@ -11,14 +11,6 @@ import ( "strings" ) -func AggregateChildrenSize(n *node) int64 { - var s int64 - for _, c := range n.children { - s += c.size - } - return s -} - func buildNodesFromPath(n *node, path string, info os.FileInfo) { nodeNames := strings.Split(path, "/") currNodeName := nodeNames[0] @@ -31,18 +23,17 @@ func buildNodesFromPath(n *node, path string, info os.FileInfo) { } else { newNode.isDir = false newNode.size = info.Size() - n.size += info.Size() } - n.children = append(n.children, newNode) + n.addChild(&newNode) } else { if existingNode, ok := n.getChild(currNodeName); ok { buildNodesFromPath(existingNode, strings.Join(nestedNodeNames, "/"), info) + existingNode.recalculateSize() } else { newNode := node{name: currNodeName, isDir: true, parent: n} buildNodesFromPath(&newNode, strings.Join(nestedNodeNames, "/"), info) - n.children = append(n.children, newNode) + n.addChild(&newNode) } - n.size = AggregateChildrenSize(n) } } @@ -90,7 +81,7 @@ func renderTree(n *node) { refNode := n.GetReference().(*node) if len(refNode.children) > 0 { for i, c := range refNode.children { - cNode := &refNode.children[i] + cNode := refNode.children[i] childNode := tview.NewTreeNode(getNodeText(cNode)).SetReference(cNode) if c.isDir { childNode.SetColor(tcell.ColorGreen) @@ -146,7 +137,7 @@ func debugTree(n *node, spacer string) { if n.isDir && len(n.children) > 0 { for i, _ := range n.children { - debugTree(&n.children[i], fmt.Sprintf("%s-", spacer)) + debugTree(n.children[i], fmt.Sprintf("%s-", spacer)) } } } From 2aa63d9157c8f99c87fcede5327a7452a3e4d1f1 Mon Sep 17 00:00:00 2001 From: Robin Mitra Date: Wed, 26 Jun 2019 22:06:22 +0100 Subject: [PATCH 7/8] Add tests for `node` used by `browse`. --- cmd/browse/node_test.go | 72 +++++++++++++++++++++++++++++++++++++++++ 1 file changed, 72 insertions(+) create mode 100644 cmd/browse/node_test.go diff --git a/cmd/browse/node_test.go b/cmd/browse/node_test.go new file mode 100644 index 0000000..34e5fde --- /dev/null +++ b/cmd/browse/node_test.go @@ -0,0 +1,72 @@ +package browse + +import ( + "testing" +) + +func TestCanAddAndRetrieveChild(t *testing.T) { + n := node{} + c1 := node{name: "C1", size: 100, isDir: false} + c2 := node{name: "C2", size: 200, isDir: false} + c3 := node{name: "C3", isDir: true} + + n.addChild(&c1) + n.addChild(&c2) + n.addChild(&c3) + + if len(n.children) != 3 { + t.Fatalf("Expected node to have 2 children, found %d", len(n.children)) + } + if n.size != 300 { + t.Fatalf("Expected node to have size of %d, found %d", 300, n.size) + } + if !n.hasChild("C1") || !n.hasChild("C2") { + t.Fatalf("Expected node to have children C1 and C2") + } + if n.hasChild("C4") { + t.Fatalf("Expected node to not have child C4") + } + if c, ok := n.getChild("C1"); &c1 != c || !ok { + t.Fatalf("Expected node to have child C1") + } + if c, ok := n.getChild("C4"); c != nil || ok { + t.Fatalf("Expected node to not have child C4") + } +} + +func TestCalculateSizeCorrectly(t *testing.T) { + n := node{name: "R"} + c1 := node{name: "C1", size: 100, isDir: false} + c2 := node{name: "C2", size: 200, isDir: false} + c3 := node{name: "C3", isDir: true} + c31 := node{name: "C31", size: 300, isDir: false} + c32 := node{name: "C32", size: 400, isDir: false} + c33 := node{name: "C33", isDir: true} + c331 := node{name: "C331", size: 500, isDir: false} + c332 := node{name: "C332", size: 600, isDir: false} + c333 := node{name: "C333", isDir: true} + + n.addChild(&c1) + n.addChild(&c2) + n.addChild(&c3) + c3.addChild(&c31) + c3.addChild(&c32) + c3.addChild(&c33) + c33.addChild(&c331) + c33.addChild(&c332) + c33.addChild(&c333) + + c33.recalculateSize() + c3.recalculateSize() + n.recalculateSize() + + if c33.size != 1100 { + t.Fatalf("Expected child node c33 to have size of %d, found %d", 1100, c33.size) + } + if c3.size != 1800 { + t.Fatalf("Expected child node c3 to have size of %d, found %d", 1800, c33.size) + } + if n.size != 2100 { + t.Fatalf("Expected root node to have size of %d, found %d", 2100, c33.size) + } +} From 96bea1c6c1c44532776dcf4838553cf110ef0b92 Mon Sep 17 00:00:00 2001 From: Robin Mitra Date: Wed, 26 Jun 2019 22:16:22 +0100 Subject: [PATCH 8/8] Only run Travis builds for pushes to `master`. This is so that pushes to PR branches does not trigger a duplicate build, as PR builds are automatically triggered anyway. --- .travis.yml | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/.travis.yml b/.travis.yml index 1143937..031e35a 100644 --- a/.travis.yml +++ b/.travis.yml @@ -8,4 +8,8 @@ go: script: go test -v ./... env: - - GO111MODULE=on \ No newline at end of file + - GO111MODULE=on + +branches: + only: + - master \ No newline at end of file