diff --git a/cmd/analyze.go b/cmd/analyze.go index 9018784..33b3498 100644 --- a/cmd/analyze.go +++ b/cmd/analyze.go @@ -8,6 +8,8 @@ import ( "github.com/wagoodman/dive/ui" ) +// analyze takes a docker image tag, digest, or id and displayes the +// image analysis to the screen func analyze(cmd *cobra.Command, args []string) { userImage := args[0] if userImage == "" { diff --git a/cmd/build.go b/cmd/build.go index be0d4be..6c94939 100644 --- a/cmd/build.go +++ b/cmd/build.go @@ -24,6 +24,7 @@ func init() { rootCmd.AddCommand(buildCmd) } +// doBuild implements the steps taken for the build command func doBuild(cmd *cobra.Command, args []string) { iidfile, err := ioutil.TempFile("/tmp", "dive.*.iid") if err != nil { @@ -46,6 +47,7 @@ func doBuild(cmd *cobra.Command, args []string) { ui.Run(manifest, refTrees) } +// runDockerCmd runs a given Docker command in the current tty func runDockerCmd(cmdStr string, args... string) error { allArgs := cleanArgs(append([]string{cmdStr}, args...)) @@ -59,6 +61,7 @@ func runDockerCmd(cmdStr string, args... string) error { return cmd.Run() } +// cleanArgs trims the whitespace from the given set of strings. func cleanArgs(s []string) []string { var r []string for _, str := range s { diff --git a/cmd/root.go b/cmd/root.go index 5fce13f..5ef24c2 100644 --- a/cmd/root.go +++ b/cmd/root.go @@ -21,7 +21,6 @@ var rootCmd = &cobra.Command{ } // Execute adds all child commands to the root command and sets flags appropriately. -// This is called by main.main(). It only needs to happen once to the rootCmd. func Execute() { if err := rootCmd.Execute(); err != nil { fmt.Println(err) @@ -69,6 +68,7 @@ func initConfig() { } } +// initLogging sets up the loggin object with a formatter and location func initLogging() { // TODO: clean this up and make more configurable var filename string = "dive.log" diff --git a/filetree/data.go b/filetree/data.go index 7617842..cc75c06 100644 --- a/filetree/data.go +++ b/filetree/data.go @@ -8,7 +8,6 @@ import ( "io" ) -// enum to show whether a file has changed const ( Unchanged DiffType = iota Changed @@ -16,26 +15,31 @@ const ( Removed ) +// NodeData is the payload for a FileNode type NodeData struct { ViewInfo ViewInfo FileInfo FileInfo DiffType DiffType } +// ViewInfo contains UI specific detail for a specific FileNode type ViewInfo struct { Collapsed bool Hidden bool } +// FileInfo contains tar metadata for a specific FileNode type FileInfo struct { Path string - Typeflag byte + TypeFlag byte MD5sum [16]byte TarHeader tar.Header } +// DiffType defines the comparison result between two FileNodes type DiffType int +// NewNodeData creates an empty NodeData struct for a FileNode func NewNodeData() (*NodeData) { return &NodeData{ ViewInfo: *NewViewInfo(), @@ -44,6 +48,7 @@ func NewNodeData() (*NodeData) { } } +// Copy duplicates a NodeData func (data *NodeData) Copy() (*NodeData) { return &NodeData{ ViewInfo: *data.ViewInfo.Copy(), @@ -53,6 +58,7 @@ func (data *NodeData) Copy() (*NodeData) { } +// NewViewInfo creates a default ViewInfo func NewViewInfo() (view *ViewInfo) { return &ViewInfo{ Collapsed: false, @@ -60,18 +66,20 @@ func NewViewInfo() (view *ViewInfo) { } } +// Copy duplicates a ViewInfo func (view *ViewInfo) Copy() (newView *ViewInfo) { newView = NewViewInfo() *newView = *view return newView } +// NewFileInfo extracts the metadata from a tar header and file contents and generates a new FileInfo object. func NewFileInfo(reader *tar.Reader, header *tar.Header, path string) FileInfo { if header.Typeflag == tar.TypeDir { return FileInfo{ - Path: path, - Typeflag: header.Typeflag, - MD5sum: [16]byte{}, + Path: path, + TypeFlag: header.Typeflag, + MD5sum: [16]byte{}, TarHeader: *header, } } @@ -83,14 +91,39 @@ func NewFileInfo(reader *tar.Reader, header *tar.Header, path string) FileInfo { return FileInfo{ Path: path, - Typeflag: header.Typeflag, + TypeFlag: header.Typeflag, MD5sum: md5.Sum(fileBytes), TarHeader: *header, } } -func (d DiffType) String() string { - switch d { +// Copy duplicates a FileInfo +func (data *FileInfo) Copy() *FileInfo { + if data == nil { + return nil + } + return &FileInfo{ + Path: data.Path, + TypeFlag: data.TypeFlag, + MD5sum: data.MD5sum, + TarHeader: data.TarHeader, + } +} + +// Compare determines the DiffType between two FileInfos based on the type and contents of each given FileInfo +func (data *FileInfo) Compare(other FileInfo) DiffType { + if data.TypeFlag == other.TypeFlag { + if bytes.Compare(data.MD5sum[:], other.MD5sum[:]) == 0 { + return Unchanged + } + } + return Changed +} + + +// String of a DiffType +func (diff DiffType) String() string { + switch diff { case Unchanged: return "Unchanged" case Changed: @@ -100,34 +133,17 @@ func (d DiffType) String() string { case Removed: return "Removed" default: - return fmt.Sprintf("%d", int(d)) + return fmt.Sprintf("%d", int(diff)) } } -func (a DiffType) merge(b DiffType) DiffType { - if a == b { - return a +// merge two DiffTypes into a single result. Essentially, return the given value unless they two values differ, +// in which case we can only determine that there is "a change". +func (diff DiffType) merge(other DiffType) DiffType { + if diff == other { + return diff } return Changed } -func (data *FileInfo) Copy() *FileInfo { - if data == nil { - return nil - } - return &FileInfo{ - Path: data.Path, - Typeflag: data.Typeflag, - MD5sum: data.MD5sum, - TarHeader: data.TarHeader, - } -} -func (data *FileInfo) getDiffType(other FileInfo) DiffType { - if data.Typeflag == other.Typeflag { - if bytes.Compare(data.MD5sum[:], other.MD5sum[:]) == 0 { - return Unchanged - } - } - return Changed -} diff --git a/filetree/data_test.go b/filetree/data_test.go index f107e30..7881ce7 100644 --- a/filetree/data_test.go +++ b/filetree/data_test.go @@ -34,7 +34,7 @@ func TestMergeDiffTypes(t *testing.T) { func BlankFileChangeInfo(path string) (f *FileInfo) { result := FileInfo{ Path: path, - Typeflag: 1, + TypeFlag: 1, MD5sum: [16]byte{1, 1, 1, 0, 1, 0, 0, 0, 0, 0, 0, 0}, } return &result diff --git a/filetree/efficiency.go b/filetree/efficiency.go index 657f523..403cb41 100644 --- a/filetree/efficiency.go +++ b/filetree/efficiency.go @@ -4,8 +4,7 @@ import ( "sort" ) -type EfficiencySlice []*EfficiencyData - +// EfficiencyData represents the storage and reference statistics for a given file tree path. type EfficiencyData struct { Path string Nodes []*FileNode @@ -13,19 +12,25 @@ type EfficiencyData struct { minDiscoveredSize int64 } -func (d EfficiencySlice) Len() int { - return len(d) +// EfficiencySlice represents an ordered set of EfficiencyData data structures. +type EfficiencySlice []*EfficiencyData + +// Len is required for sorting. +func (efs EfficiencySlice) Len() int { + return len(efs) } -func (d EfficiencySlice) Swap(i, j int) { - d[i], d[j] = d[j], d[i] +// Swap operation is required for sorting. +func (efs EfficiencySlice) Swap(i, j int) { + efs[i], efs[j] = efs[j], efs[i] } -func (d EfficiencySlice) Less(i, j int) bool { - return d[i].CumulativeSize < d[j].CumulativeSize +// Less comparison is required for sorting. +func (efs EfficiencySlice) Less(i, j int) bool { + return efs[i].CumulativeSize < efs[j].CumulativeSize } - +// Efficiency returns the score and file set of the given set of FileTrees (layers). This is loosely based on: // 1. Files that are duplicated across layers discounts your score, weighted by file size // 2. Files that are removed discounts your score, weighted by the original file size func Efficiency(trees []*FileTree) (float64, EfficiencySlice) { diff --git a/filetree/node.go b/filetree/node.go index b000f2e..f876e8e 100644 --- a/filetree/node.go +++ b/filetree/node.go @@ -22,6 +22,7 @@ var diffTypeColor = map[DiffType]*color.Color { Unchanged: color.New(color.Reset), } +// FileNode represents a single file, its relation to files beneath it, the tree it exists in, and the metadata of the given file. type FileNode struct { Tree *FileTree Parent *FileNode @@ -31,6 +32,7 @@ type FileNode struct { path string } +// NewNode creates a new FileNode relative to the given parent node with a payload. func NewNode(parent *FileNode, name string, data FileInfo) (node *FileNode) { node = new(FileNode) node.Name = name @@ -45,6 +47,7 @@ func NewNode(parent *FileNode, name string, data FileInfo) (node *FileNode) { return node } +// renderTreeLine returns a string representing this FileNode in the context of a greater ASCII tree. func (node *FileNode) renderTreeLine(spaces []bool, last bool, collapsed bool) string { var otherBranches string for _, space := range spaces { @@ -68,6 +71,7 @@ func (node *FileNode) renderTreeLine(spaces []bool, last bool, collapsed bool) s return otherBranches + thisBranch + collapsedIndicator + node.String() + newLine } +// Copy duplicates the existing node relative to a new parent node. func (node *FileNode) Copy(parent *FileNode) *FileNode { newNode := NewNode(parent, node.Name, node.Data.FileInfo) newNode.Data.ViewInfo = node.Data.ViewInfo @@ -79,6 +83,7 @@ func (node *FileNode) Copy(parent *FileNode) *FileNode { return newNode } +// AddChild creates a new node relative to the current FileNode. func (node *FileNode) AddChild(name string, data FileInfo) (child *FileNode) { child = NewNode(node, name, data) if node.Children[name] != nil { @@ -91,6 +96,7 @@ func (node *FileNode) AddChild(name string, data FileInfo) (child *FileNode) { return child } +// Remove deletes the current FileNode from it's parent FileNode's relations. func (node *FileNode) Remove() error { if node == node.Tree.Root { return fmt.Errorf("cannot remove the tree root") @@ -103,6 +109,7 @@ func (node *FileNode) Remove() error { return nil } +// String shows the filename formatted into the proper color (by DiffType), additionally indicating if it is a symlink. func (node *FileNode) String() string { var display string if node == nil { @@ -116,6 +123,7 @@ func (node *FileNode) String() string { return diffTypeColor[node.Data.DiffType].Sprint(display) } +// MetadatString returns the FileNode metadata in a columnar string. func (node *FileNode) MetadataString() string { if node == nil { return "" @@ -130,7 +138,6 @@ func (node *FileNode) MetadataString() string { group := node.Data.FileInfo.TarHeader.Gid userGroup := fmt.Sprintf("%d:%d", user, group) - //size := humanize.Bytes(uint64(node.Data.FileInfo.TarHeader.FileInfo().Size())) var sizeBytes int64 if node.Data.FileInfo.TarHeader.FileInfo().IsDir() { @@ -152,7 +159,8 @@ func (node *FileNode) MetadataString() string { return diffTypeColor[node.Data.DiffType].Sprint(fmt.Sprintf(AttributeFormat, dir, fileMode, userGroup, size)) } -func (node *FileNode) VisitDepthChildFirst(visiter Visiter, evaluator VisitEvaluator) error { +// VisitDepthChildFirst iterates a tree depth-first (starting at this FileNode), evaluating the deepest depths first (visit on bubble up) +func (node *FileNode) VisitDepthChildFirst(visitor Visitor, evaluator VisitEvaluator) error { var keys []string for key := range node.Children { keys = append(keys, key) @@ -160,7 +168,7 @@ func (node *FileNode) VisitDepthChildFirst(visiter Visiter, evaluator VisitEvalu sort.Strings(keys) for _, name := range keys { child := node.Children[name] - err := child.VisitDepthChildFirst(visiter, evaluator) + err := child.VisitDepthChildFirst(visitor, evaluator) if err != nil { return err } @@ -169,13 +177,14 @@ func (node *FileNode) VisitDepthChildFirst(visiter Visiter, evaluator VisitEvalu if node == node.Tree.Root { return nil } else if evaluator != nil && evaluator(node) || evaluator == nil { - return visiter(node) + return visitor(node) } return nil } -func (node *FileNode) VisitDepthParentFirst(visiter Visiter, evaluator VisitEvaluator) error { +// VisitDepthParentFirst iterates a tree depth-first (starting at this FileNode), evaluating the shallowest depths first (visit while sinking down) +func (node *FileNode) VisitDepthParentFirst(visitor Visitor, evaluator VisitEvaluator) error { var err error doVisit := evaluator != nil && evaluator(node) || evaluator == nil @@ -186,7 +195,7 @@ func (node *FileNode) VisitDepthParentFirst(visiter Visiter, evaluator VisitEval // never visit the root node if node != node.Tree.Root { - err = visiter(node) + err = visitor(node) if err != nil { return err } @@ -199,7 +208,7 @@ func (node *FileNode) VisitDepthParentFirst(visiter Visiter, evaluator VisitEval sort.Strings(keys) for _, name := range keys { child := node.Children[name] - err = child.VisitDepthParentFirst(visiter, evaluator) + err = child.VisitDepthParentFirst(visitor, evaluator) if err != nil { return err } @@ -207,10 +216,17 @@ func (node *FileNode) VisitDepthParentFirst(visiter Visiter, evaluator VisitEval return err } +// IsWhiteout returns an indication if this file may be a overlay-whiteout file. func (node *FileNode) IsWhiteout() bool { return strings.HasPrefix(node.Name, whiteoutPrefix) } +// IsLeaf returns true is the current node has no child nodes. +func (node *FileNode) IsLeaf() bool { + return len(node.Children) == 0 +} + +// Path returns a slash-delimited string from the root of the greater tree to the current node (e.g. /a/path/to/here) func (node *FileNode) Path() string { if node.path == "" { path := []string{} @@ -234,14 +250,9 @@ func (node *FileNode) Path() string { return node.path } -func (node *FileNode) IsLeaf() bool { - return len(node.Children) == 0 -} - +// deriveDiffType determines a DiffType to the current FileNode. Note: the DiffType of a node is always the DiffType of +// its attributes and its contents. The contents are the bytes of the file of the children of a directory. func (node *FileNode) deriveDiffType(diffType DiffType) error { - // THE DIFF_TYPE OF A NODE IS ALWAYS THE DIFF_TYPE OF ITS ATTRIBUTES AND ITS CONTENTS - // THE CONTENTS ARE THE BYTES OF A FILE OR THE CHILDREN OF A DIRECTORY - if node.IsLeaf() { return node.AssignDiffType(diffType) } @@ -255,6 +266,7 @@ func (node *FileNode) deriveDiffType(diffType DiffType) error { return node.AssignDiffType(myDiffType) } +// AssignDiffType will assign the given DiffType to this node, possible affecting child nodes. func (node *FileNode) AssignDiffType(diffType DiffType) error { var err error @@ -277,27 +289,27 @@ func (node *FileNode) AssignDiffType(diffType DiffType) error { return nil } -func (a *FileNode) compare(b *FileNode) DiffType { - if a == nil && b == nil { +// compare the current node against the given node, returning a definitive DiffType. +func (node *FileNode) compare(other *FileNode) DiffType { + if node == nil && other == nil { return Unchanged } - // a is nil but not b - if a == nil && b != nil { + + if node == nil && other != nil { return Added } - // b is nil but not a - if a != nil && b == nil { + if node != nil && other == nil { return Removed } - if b.IsWhiteout() { + if other.IsWhiteout() { return Removed } - if a.Name != b.Name { + if node.Name != other.Name { panic("comparing mismatched nodes") } // TODO: fails on nil - return a.Data.FileInfo.getDiffType(b.Data.FileInfo) + return node.Data.FileInfo.Compare(other.Data.FileInfo) } diff --git a/filetree/tree.go b/filetree/tree.go index 27703d5..4f806a1 100644 --- a/filetree/tree.go +++ b/filetree/tree.go @@ -18,6 +18,7 @@ const ( collapsedItem = "⊕ " ) +// FileTree represents a set of files, directories, and their relations. type FileTree struct { Root *FileNode Size int @@ -26,6 +27,7 @@ type FileTree struct { Id uuid.UUID } +// NewFileTree creates an empty FileTree func NewFileTree() (tree *FileTree) { tree = new(FileTree) tree.Size = 0 @@ -36,6 +38,8 @@ func NewFileTree() (tree *FileTree) { return tree } +// renderParams is a representation of a FileNode in the context of the greater tree. All +// data stored is necessary for rendering a single line in a tree format. type renderParams struct{ node *FileNode spaces []bool @@ -44,13 +48,15 @@ type renderParams struct{ isLast bool } +// renderStringTreeBetween returns a string representing the given tree between the given rows. Since each node +// is rendered on its own line, the returned string shows the visible nodes not affected by a collapsed parent. func (tree *FileTree) renderStringTreeBetween(startRow, stopRow int, showAttributes bool) string { // generate a list of nodes to render - var params []renderParams = make([]renderParams,0) + var params = make([]renderParams,0) var result string // visit from the front of the list - var paramsToVisit = []renderParams{ renderParams{node: tree.Root, spaces: []bool{}, showCollapsed: false, isLast: false} } + var paramsToVisit = []renderParams{ {node: tree.Root, spaces: []bool{}, showCollapsed: false, isLast: false} } for currentRow := 0; len(paramsToVisit) > 0 && currentRow <= stopRow; currentRow++ { // pop the first node var currentParams renderParams @@ -61,6 +67,7 @@ func (tree *FileTree) renderStringTreeBetween(startRow, stopRow int, showAttribu for key := range currentParams.node.Children { keys = append(keys, key) } + // we should always visit nodes in order sort.Strings(keys) var childParams = make([]renderParams,0) @@ -119,14 +126,17 @@ func (tree *FileTree) renderStringTreeBetween(startRow, stopRow int, showAttribu return result } +// 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) } +// Copy returns a copy of the given FileTree func (tree *FileTree) Copy() *FileTree { newTree := NewFileTree() newTree.Size = tree.Size @@ -142,19 +152,23 @@ func (tree *FileTree) Copy() *FileTree { return newTree } -type Visiter func(*FileNode) error +// Visitor is a function that processes, observes, or otherwise transforms the given node +type Visitor func(*FileNode) error + +// VisitEvaluator is a function that indicates whether the given node should be visited by a Visitor. type VisitEvaluator func(*FileNode) bool -// DFS bubble up -func (tree *FileTree) VisitDepthChildFirst(visiter Visiter, evaluator VisitEvaluator) error { - return tree.Root.VisitDepthChildFirst(visiter, evaluator) +// VisitDepthChildFirst iterates the given tree depth-first, evaluating the deepest depths first (visit on bubble up) +func (tree *FileTree) VisitDepthChildFirst(visitor Visitor, evaluator VisitEvaluator) error { + return tree.Root.VisitDepthChildFirst(visitor, evaluator) } -// DFS sink down -func (tree *FileTree) VisitDepthParentFirst(visiter Visiter, evaluator VisitEvaluator) error { - return tree.Root.VisitDepthParentFirst(visiter, evaluator) +// VisitDepthParentFirst iterates the given tree depth-first, evaluating the shallowest depths first (visit while sinking down) +func (tree *FileTree) VisitDepthParentFirst(visitor Visitor, evaluator VisitEvaluator) error { + return tree.Root.VisitDepthParentFirst(visitor, evaluator) } +// Stack takes two trees and combines them together. This is done by "stacking" the given tree on top of the owning tree. func (tree *FileTree) Stack(upper *FileTree) error { graft := func(node *FileNode) error { if node.IsWhiteout() { @@ -173,6 +187,7 @@ func (tree *FileTree) Stack(upper *FileTree) error { return upper.VisitDepthChildFirst(graft, nil) } +// GetNode fetches a single node when given a slash-delimited string from root ('/') to the desired node (e.g. '/a/node/path') func (tree *FileTree) GetNode(path string) (*FileNode, error) { nodeNames := strings.Split(strings.Trim(path, "/"), "/") node := tree.Root @@ -188,6 +203,7 @@ func (tree *FileTree) GetNode(path string) (*FileNode, error) { return node, nil } +// AddPath adds a new node to the tree with the given payload func (tree *FileTree) AddPath(path string, data FileInfo) (*FileNode, error) { nodeNames := strings.Split(strings.Trim(path, "/"), "/") node := tree.Root @@ -213,6 +229,7 @@ func (tree *FileTree) AddPath(path string, data FileInfo) (*FileNode, error) { return node, nil } +// RemovePath removes a node from the tree given its path. func (tree *FileTree) RemovePath(path string) error { node, err := tree.GetNode(path) if err != nil { @@ -221,10 +238,11 @@ func (tree *FileTree) RemovePath(path string) error { return node.Remove() } +// Compare marks the FileNodes in the owning tree with DiffType annotations when compared to the given tree. func (tree *FileTree) Compare(upper *FileTree) error { graft := func(upperNode *FileNode) error { if upperNode.IsWhiteout() { - err := tree.MarkRemoved(upperNode.Path()) + err := tree.markRemoved(upperNode.Path()) if err != nil { return fmt.Errorf("cannot remove upperNode %s: %v", upperNode.Path(), err.Error()) } @@ -246,7 +264,8 @@ func (tree *FileTree) Compare(upper *FileTree) error { return upper.VisitDepthChildFirst(graft, nil) } -func (tree *FileTree) MarkRemoved(path string) error { +// markRemoved annotates the FileNode at the given path as Removed. +func (tree *FileTree) markRemoved(path string) error { node, err := tree.GetNode(path) if err != nil { return err @@ -254,6 +273,7 @@ func (tree *FileTree) MarkRemoved(path string) error { return node.AssignDiffType(Removed) } +// StackRange combines an array of trees into a single tree func StackRange(trees []*FileTree, start, stop int) *FileTree { tree := trees[0].Copy() for idx := start; idx <= stop; idx++ { diff --git a/filetree/tree_test.go b/filetree/tree_test.go index 8d7eec7..6c95df0 100644 --- a/filetree/tree_test.go +++ b/filetree/tree_test.go @@ -262,7 +262,7 @@ func TestCompareWithNoChanges(t *testing.T) { for _, value := range paths { fakeData := FileInfo{ Path: value, - Typeflag: 1, + TypeFlag: 1, MD5sum: [16]byte{0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0}, } lowerTree.AddPath(value, fakeData) @@ -293,7 +293,7 @@ func TestCompareWithAdds(t *testing.T) { for _, value := range lowerPaths { lowerTree.AddPath(value, FileInfo{ Path: value, - Typeflag: 1, + TypeFlag: 1, MD5sum: [16]byte{0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0}, }) } @@ -301,7 +301,7 @@ func TestCompareWithAdds(t *testing.T) { for _, value := range upperPaths { upperTree.AddPath(value, FileInfo{ Path: value, - Typeflag: 1, + TypeFlag: 1, MD5sum: [16]byte{0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0}, }) } @@ -353,12 +353,12 @@ func TestCompareWithChanges(t *testing.T) { for _, value := range paths { lowerTree.AddPath(value, FileInfo{ Path: value, - Typeflag: 1, + TypeFlag: 1, MD5sum: [16]byte{0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0}, }) upperTree.AddPath(value, FileInfo{ Path: value, - Typeflag: 1, + TypeFlag: 1, MD5sum: [16]byte{1, 1, 1, 0, 1, 0, 0, 0, 0, 0, 0, 0}, }) } @@ -403,7 +403,7 @@ func TestCompareWithRemoves(t *testing.T) { for _, value := range lowerPaths { fakeData := FileInfo{ Path: value, - Typeflag: 1, + TypeFlag: 1, MD5sum: [16]byte{0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0}, } lowerTree.AddPath(value, fakeData) @@ -412,7 +412,7 @@ func TestCompareWithRemoves(t *testing.T) { for _, value := range upperPaths { fakeData := FileInfo{ Path: value, - Typeflag: 1, + TypeFlag: 1, MD5sum: [16]byte{0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0}, } upperTree.AddPath(value, fakeData) @@ -473,7 +473,7 @@ func TestStackRange(t *testing.T) { for _, value := range lowerPaths { fakeData := FileInfo{ Path: value, - Typeflag: 1, + TypeFlag: 1, MD5sum: [16]byte{0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0}, } lowerTree.AddPath(value, fakeData) @@ -482,7 +482,7 @@ func TestStackRange(t *testing.T) { for _, value := range upperPaths { fakeData := FileInfo{ Path: value, - Typeflag: 1, + TypeFlag: 1, MD5sum: [16]byte{1, 1, 1, 0, 1, 0, 0, 0, 0, 0, 0, 0}, } upperTree.AddPath(value, fakeData) @@ -499,7 +499,7 @@ func TestRemoveOnIterate(t *testing.T) { for _, value := range paths { fakeData := FileInfo{ Path: value, - Typeflag: 1, + TypeFlag: 1, MD5sum: [16]byte{0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0}, } node, err := tree.AddPath(value, fakeData) diff --git a/image/layer.go b/image/layer.go index 39a88c6..36cbf51 100644 --- a/image/layer.go +++ b/image/layer.go @@ -11,6 +11,7 @@ const ( LayerFormat = "%-25s %7s %s" ) +// Layer represents a Docker image layer and metadata type Layer struct { TarPath string History ImageHistoryEntry @@ -19,6 +20,7 @@ type Layer struct { RefTrees []*filetree.FileTree } +// Id returns the truncated id of the current layer. func (layer *Layer) Id() string { rangeBound := 25 if length := len(layer.History.ID); length < 25 { @@ -34,6 +36,7 @@ func (layer *Layer) Id() string { return id } +// String represents a layer in a columnar format. func (layer *Layer) String() string { return fmt.Sprintf(LayerFormat, diff --git a/ui/detailsview.go b/ui/detailsview.go index 9cac387..11a4cac 100644 --- a/ui/detailsview.go +++ b/ui/detailsview.go @@ -11,6 +11,8 @@ import ( "github.com/dustin/go-humanize" ) +// 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 @@ -20,16 +22,18 @@ type DetailsView struct { inefficiencies filetree.EfficiencySlice } -func NewStatisticsView(name string, gui *gocui.Gui) (detailsview *DetailsView) { - detailsview = new(DetailsView) +// NewDetailsView creates a new view object attached the the global [gocui] screen object. +func NewDetailsView(name string, gui *gocui.Gui) (detailsView *DetailsView) { + detailsView = new(DetailsView) // populate main fields - detailsview.Name = name - detailsview.gui = gui + detailsView.Name = name + detailsView.gui = gui - return detailsview + 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 @@ -55,18 +59,34 @@ func (view *DetailsView) Setup(v *gocui.View, header *gocui.View) error { 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 } -// we only need to update this view upon the initial tree load +// 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. Note: we only need to update this view upon the initial tree load func (view *DetailsView) Update() error { layerTrees := Views.Tree.RefTrees view.efficiency, view.inefficiencies = filetree.Efficiency(layerTrees[:len(layerTrees)-1]) 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() @@ -112,17 +132,7 @@ func (view *DetailsView) Render() error { return nil } -func (view *DetailsView) CursorDown() error { - return CursorDown(view.gui, view.view) -} - -func (view *DetailsView) CursorUp() error { - return CursorUp(view.gui, view.view) -} - - +// 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" - // return renderStatusOption("^L","Layer changes", view.CompareMode == CompareLayer) + - // renderStatusOption("^A","All changes", view.CompareMode == CompareAll) } diff --git a/ui/filetreeview.go b/ui/filetreeview.go index b1622dc..dee4255 100644 --- a/ui/filetreeview.go +++ b/ui/filetreeview.go @@ -17,7 +17,8 @@ const ( 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 @@ -33,19 +34,21 @@ type FileTreeView struct { bufferIndexLowerBound uint } -func NewFileTreeView(name string, gui *gocui.Gui, tree *filetree.FileTree, refTrees []*filetree.FileTree) (treeview *FileTreeView) { - treeview = new(FileTreeView) +// 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) (treeView *FileTreeView) { + treeView = new(FileTreeView) // populate main fields - treeview.Name = name - treeview.gui = gui - treeview.ModelTree = tree - treeview.RefTrees = refTrees - treeview.HiddenDiffTypes = make([]bool, 4) + treeView.Name = name + treeView.gui = gui + treeView.ModelTree = tree + treeView.RefTrees = refTrees + treeView.HiddenDiffTypes = make([]bool, 4) - return treeview + 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 @@ -91,16 +94,19 @@ func (view *FileTreeView) Setup(v *gocui.View, header *gocui.View) error { 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 @@ -109,9 +115,10 @@ func (view *FileTreeView) resetCursor() { 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) + return fmt.Errorf("invalid layer index given: %d of %d", topTreeStop, len(view.RefTrees)-1) } newTree := filetree.StackRange(view.RefTrees, bottomTreeStart, bottomTreeStop) @@ -136,6 +143,7 @@ func (view *FileTreeView) setTreeByLayer(bottomTreeStart, bottomTreeStop, topTre 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 { @@ -148,6 +156,7 @@ func (view *FileTreeView) doCursorUp() { } } +// 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 { @@ -160,22 +169,21 @@ func (view *FileTreeView) doCursorDown() { } } +// 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 { - // 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. - 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 { - // 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. - if view.TreeIndex > 0 { view.doCursorUp() return view.Render() @@ -183,12 +191,13 @@ func (view *FileTreeView) CursorUp() error { return nil } +// getAbsPositionNode determines the selected screen cursor's location in the file tree, returning the selected FileNode. func (view *FileTreeView) getAbsPositionNode() (node *filetree.FileNode) { - var visiter func(*filetree.FileNode) error + var visitor func(*filetree.FileNode) error var evaluator func(*filetree.FileNode) bool var dfsCounter uint - visiter = func(curNode *filetree.FileNode) error { + visitor = func(curNode *filetree.FileNode) error { if dfsCounter == view.TreeIndex { node = curNode } @@ -214,7 +223,7 @@ func (view *FileTreeView) getAbsPositionNode() (node *filetree.FileNode) { return !curNode.Parent.Data.ViewInfo.Collapsed && !curNode.Data.ViewInfo.Hidden && regexMatch } - err = view.ModelTree.VisitDepthParentFirst(visiter, evaluator) + err = view.ModelTree.VisitDepthParentFirst(visitor, evaluator) if err != nil { panic(err) } @@ -222,6 +231,7 @@ func (view *FileTreeView) getAbsPositionNode() (node *filetree.FileNode) { return node } +// toggleCollapse will collapse/expand the selected FileNode. func (view *FileTreeView) toggleCollapse() error { node := view.getAbsPositionNode() if node != nil { @@ -231,6 +241,7 @@ func (view *FileTreeView) toggleCollapse() error { 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] @@ -241,6 +252,7 @@ func (view *FileTreeView) toggleShowDiffType(diffType filetree.DiffType) error { 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 @@ -258,6 +270,7 @@ func filterRegex() *regexp.Regexp { return regex } +// Update refreshes the state objects for future rendering. func (view *FileTreeView) Update() error { regex := filterRegex() @@ -288,14 +301,7 @@ func (view *FileTreeView) Update() error { return nil } -func (view *FileTreeView) KeyHelp() string { - return renderStatusOption("Space","Collapse dir", false) + - renderStatusOption("^A","Added files", !view.HiddenDiffTypes[filetree.Added]) + - renderStatusOption("^R","Removed files", !view.HiddenDiffTypes[filetree.Removed]) + - renderStatusOption("^M","Modified files", !view.HiddenDiffTypes[filetree.Changed]) + - renderStatusOption("^U","Unmodified files", !view.HiddenDiffTypes[filetree.Unchanged]) -} - +// 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") @@ -337,3 +343,12 @@ func (view *FileTreeView) Render() error { }) 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("Space","Collapse dir", false) + + renderStatusOption("^A","Added files", !view.HiddenDiffTypes[filetree.Added]) + + renderStatusOption("^R","Removed files", !view.HiddenDiffTypes[filetree.Removed]) + + renderStatusOption("^M","Modified files", !view.HiddenDiffTypes[filetree.Changed]) + + renderStatusOption("^U","Unmodified files", !view.HiddenDiffTypes[filetree.Unchanged]) +} \ No newline at end of file diff --git a/ui/filterview.go b/ui/filterview.go index a7fad08..93df764 100644 --- a/ui/filterview.go +++ b/ui/filterview.go @@ -6,8 +6,8 @@ import ( "github.com/jroimartin/gocui" ) -// with special thanks to https://gist.github.com/jroimartin/3b2e943a3811d795e0718b4a95b89bec - +// 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 @@ -18,18 +18,20 @@ type FilterView struct { hidden bool } -func NewFilterView(name string, gui *gocui.Gui) (filterview *FilterView) { - filterview = new(FilterView) +// 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 + filterView.Name = name + filterView.gui = gui + filterView.headerStr = "Path Filter: " + filterView.hidden = true - return filterview + 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 @@ -46,32 +48,28 @@ func (view *FilterView) Setup(v *gocui.View, header *gocui.View) error { 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 - // } - 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 @@ -94,14 +92,12 @@ func (view *FilterView) Edit(v *gocui.View, key gocui.Key, ch rune, mod gocui.Mo } } -func (view *FilterView) KeyHelp() string { - return Formatting.StatusControlNormal("▏Type to filter the file tree ") -} - +// 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 @@ -109,6 +105,10 @@ func (view *FilterView) Render() error { return nil }) - // todo: blerg 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 ") +} \ No newline at end of file diff --git a/ui/layerview.go b/ui/layerview.go index 1815d86..00c8738 100644 --- a/ui/layerview.go +++ b/ui/layerview.go @@ -10,6 +10,8 @@ import ( "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 @@ -21,18 +23,20 @@ type LayerView struct { CompareStartIndex int } -func NewLayerView(name string, gui *gocui.Gui, layers []*image.Layer) (layerview *LayerView) { - layerview = new(LayerView) +// 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 - layerview.CompareMode = CompareLayer + layerView.Name = name + layerView.gui = gui + layerView.Layers = layers + layerView.CompareMode = CompareLayer - return layerview + 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 @@ -63,15 +67,50 @@ func (view *LayerView) Setup(v *gocui.View, header *gocui.View) error { 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() @@ -79,6 +118,7 @@ func (view *LayerView) setCompareMode(compareMode CompareType) error { 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 @@ -97,6 +137,7 @@ func (view *LayerView) getCompareIndexes() (bottomTreeStart, bottomTreeStop, top 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 := " " @@ -111,10 +152,14 @@ func (view *LayerView) renderCompareBar(layerIdx int) string { return result } +// Update refreshes the state objects for future rendering (currently does nothing). func (view *LayerView) Update() error { 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 @@ -160,39 +205,11 @@ func (view *LayerView) Render() error { } return nil }) - // todo: blerg return nil } -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 -} - -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 -} - -func (view *LayerView) SetCursor(layer int) error { - view.LayerIndex = layer - Views.Tree.setTreeByLayer(view.getCompareIndexes()) - Views.Details.Render() - view.Render() - - 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("^L","Show layer changes", view.CompareMode == CompareLayer) + renderStatusOption("^A","Show aggregated changes", view.CompareMode == CompareAll) diff --git a/ui/statusview.go b/ui/statusview.go index 766de6c..2180cdd 100644 --- a/ui/statusview.go +++ b/ui/statusview.go @@ -7,57 +7,59 @@ import ( "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 } -func NewStatusView(name string, gui *gocui.Gui) (statusview *StatusView) { - statusview = new(StatusView) +// 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 + statusView.Name = name + statusView.gui = gui - return statusview + 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.view.BgColor = gocui.ColorDefault + gocui.AttrReverse 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 } -func (view *StatusView) KeyHelp() string { - return renderStatusOption("^C","Quit", false) + - renderStatusOption("^Space","Switch view", false) + - renderStatusOption("^/","Filter files", Views.Filter.IsVisible()) -} - +// 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() @@ -68,3 +70,10 @@ func (view *StatusView) Render() error { // 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("^C","Quit", false) + + renderStatusOption("^Space","Switch view", false) + + renderStatusOption("^/","Filter files", Views.Filter.IsVisible()) +} \ No newline at end of file diff --git a/ui/ui.go b/ui/ui.go index bc4e6fa..54516b4 100644 --- a/ui/ui.go +++ b/ui/ui.go @@ -17,6 +17,10 @@ import ( const debug = false const profile = false +var cpuProfilePath *os.File +var memoryProfilePath *os.File + +// 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") @@ -29,6 +33,7 @@ func debugPrint(s string) { } } +// Formatting defines standard functions for formatting UI sections. var Formatting struct { Header func(...interface{})(string) Selected func(...interface{})(string) @@ -40,6 +45,7 @@ var Formatting struct { CompareBottom func(...interface{})(string) } +// Views contains all rendered UI panes. var Views struct { Tree *FileTreeView Layer *LayerView @@ -49,6 +55,7 @@ var Views struct { lookup map[string]View } +// View defines the a renderable terminal screen pane. type View interface { Setup(*gocui.View, *gocui.View) error CursorDown() error @@ -59,6 +66,7 @@ type View interface { IsVisible() bool } +// 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) @@ -72,6 +80,7 @@ func toggleView(g *gocui.Gui, v *gocui.View) error { return err } +// 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() @@ -94,6 +103,7 @@ func toggleFilterView(g *gocui.Gui, v *gocui.View) error { return nil } +// 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() @@ -114,6 +124,7 @@ func CursorDown(g *gocui.Gui, v *gocui.View) error { return nil } +// 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() cx, cy := v.Cursor() @@ -125,10 +136,7 @@ func CursorUp(g *gocui.Gui, v *gocui.View) error { return nil } - -var cpuProfilePath *os.File -var memoryProfilePath *os.File - +// quit is the gocui callback invoked when the user hits Ctrl+C func quit(g *gocui.Gui, v *gocui.View) error { if profile { pprof.StopCPUProfile() @@ -140,7 +148,8 @@ func quit(g *gocui.Gui, v *gocui.View) error { return gocui.ErrQuit } -func keybindings(g *gocui.Gui) error { +// keyBindings registers global key press actions, valid when in any pane. +func keyBindings(g *gocui.Gui) error { if err := g.SetKeybinding("", gocui.KeyCtrlC, gocui.ModNone, quit); err != nil { return err } @@ -157,6 +166,7 @@ func keybindings(g *gocui.Gui) error { return nil } +// isNewView determines if a view has already been created based on the set of errors given (a bit hokie) func isNewView(errs ...error) bool { for _, err := range errs { if err == nil { @@ -169,8 +179,12 @@ func isNewView(errs ...error) bool { return true } -// TODO: this logic should be refactored into an abstraction that takes care of the math for us + +// layout defines the definition of the window pane size and placement relations to one another. This +// is invoked at application start and whenever the screen dimensions change. 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() splitCols := maxX / 2 debugWidth := 0 @@ -250,12 +264,14 @@ func layout(g *gocui.Gui) error { return nil } +// Update refreshes the state objects for future rendering. func Update() { for _, view := range Views.lookup { view.Update() } } +// Render flushes the state objects to the screen. func Render() { for _, view := range Views.lookup { if view.IsVisible() { @@ -264,6 +280,7 @@ 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 + " ") @@ -272,6 +289,7 @@ func renderStatusOption(control, title string, selected bool) string { } } +// Run is the UI entrypoint. func Run(layers []*image.Layer, refTrees []*filetree.FileTree) { Formatting.Selected = color.New(color.ReverseVideo, color.Bold).SprintFunc() @@ -303,7 +321,7 @@ func Run(layers []*image.Layer, refTrees []*filetree.FileTree) { Views.Filter = NewFilterView("command", g) Views.lookup[Views.Filter.Name] = Views.Filter - Views.Details = NewStatisticsView("details", g) + Views.Details = NewDetailsView("details", g) Views.lookup[Views.Details.Name] = Views.Details @@ -318,7 +336,7 @@ func Run(layers []*image.Layer, refTrees []*filetree.FileTree) { // let the default position of the cursor be the last layer // Views.Layer.SetCursor(len(Views.Layer.Layers)-1) - if err := keybindings(g); err != nil { + if err := keyBindings(g); err != nil { log.Panicln(err) }