added basic documentation

This commit is contained in:
Alex Goodman 2018-10-15 22:51:48 -04:00
parent 4e7e9b64e3
commit 4c99bd093d
No known key found for this signature in database
GPG Key ID: 05328C611D8A520E
16 changed files with 341 additions and 211 deletions

View File

@ -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 == "" {

View File

@ -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 {

View File

@ -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"

View File

@ -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
}

View File

@ -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

View File

@ -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) {

View File

@ -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)
}

View File

@ -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++ {

View File

@ -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)

View File

@ -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,

View File

@ -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)
}

View File

@ -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])
}

View File

@ -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 ")
}

View File

@ -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)

View File

@ -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())
}

View File

@ -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)
}