From 0ec279788e9ce867bcbe5bad952259bb7952a0ba Mon Sep 17 00:00:00 2001
From: Alex Goodman <wagoodman@users.noreply.github.com>
Date: Sun, 14 Oct 2018 10:55:54 -0400
Subject: [PATCH 1/5] implements build command (#22)

---
 cmd/build.go   | 69 +++++++++++++++++++++++++++++++++++++++-----------
 image/image.go |  2 +-
 2 files changed, 55 insertions(+), 16 deletions(-)

diff --git a/cmd/build.go b/cmd/build.go
index 7ccdbfc..be0d4be 100644
--- a/cmd/build.go
+++ b/cmd/build.go
@@ -1,9 +1,14 @@
 package cmd
 
 import (
-	"fmt"
-
 	"github.com/spf13/cobra"
+	"os/exec"
+	"os"
+	"strings"
+	"io/ioutil"
+	log "github.com/sirupsen/logrus"
+	"github.com/wagoodman/dive/image"
+	"github.com/wagoodman/dive/ui"
 )
 
 // buildCmd represents the build command
@@ -11,21 +16,55 @@ var buildCmd = &cobra.Command{
 	Use:   "build",
 	Short: "Build and analyze a docker image",
 	Long: `Build and analyze a docker image`,
-	Run: func(cmd *cobra.Command, args []string) {
-		fmt.Println("build called")
-	},
+	DisableFlagParsing: true,
+	Run: doBuild,
 }
 
 func init() {
 	rootCmd.AddCommand(buildCmd)
-
-	// Here you will define your flags and configuration settings.
-
-	// Cobra supports Persistent Flags which will work for this command
-	// and all subcommands, e.g.:
-	// buildCmd.PersistentFlags().String("foo", "", "A help for foo")
-
-	// Cobra supports local flags which will only run when this command
-	// is called directly, e.g.:
-	// buildCmd.Flags().BoolP("toggle", "t", false, "Help message for toggle")
 }
+
+func doBuild(cmd *cobra.Command, args []string) {
+	iidfile, err := ioutil.TempFile("/tmp", "dive.*.iid")
+	if err != nil {
+		log.Fatal(err)
+	}
+	defer os.Remove(iidfile.Name())
+
+	allArgs := append([]string{"--iidfile", iidfile.Name()}, args...)
+	err = runDockerCmd("build", allArgs...)
+	if err != nil {
+		log.Fatal(err)
+	}
+
+	imageId, err := ioutil.ReadFile(iidfile.Name())
+	if err != nil {
+		log.Fatal(err)
+	}
+
+	manifest, refTrees := image.InitializeData(string(imageId))
+	ui.Run(manifest, refTrees)
+}
+
+func runDockerCmd(cmdStr string, args... string) error {
+
+	allArgs := cleanArgs(append([]string{cmdStr}, args...))
+
+	cmd := exec.Command("docker", allArgs...)
+
+	cmd.Stdout = os.Stdout
+	cmd.Stderr = os.Stderr
+	cmd.Stdin = os.Stdin
+
+	return cmd.Run()
+}
+
+func cleanArgs(s []string) []string {
+	var r []string
+	for _, str := range s {
+		if str != "" {
+			r = append(r, strings.Trim(str, " "))
+		}
+	}
+	return r
+}
\ No newline at end of file
diff --git a/image/image.go b/image/image.go
index c2957b9..638efcd 100644
--- a/image/image.go
+++ b/image/image.go
@@ -141,7 +141,7 @@ func InitializeData(imageID string) ([]*Layer, []*filetree.FileTree) {
 	defer os.RemoveAll(tmpDir)
 
 	// read through the image contents and build a tree
-	fmt.Println("Reading image...")
+	fmt.Printf("Reading image '%s'...\n", imageID)
 	tarFile, err := os.Open(imageTarPath)
 	if err != nil {
 		fmt.Println(err)

From 85fa13501d2299b2df44df1f8ef519b8c8a20983 Mon Sep 17 00:00:00 2001
From: Alex Goodman <wagoodman@users.noreply.github.com>
Date: Sun, 14 Oct 2018 10:56:09 -0400
Subject: [PATCH 2/5] Add details pane (#23)

---
 image/image.go     |  4 +-
 ui/detailsview.go  | 95 ++++++++++++++++++++++++++++++++++++++++++++++
 ui/filetreeview.go | 28 +++++++++++---
 ui/filterview.go   |  7 ----
 ui/layerview.go    | 54 ++++++++++----------------
 ui/ui.go           | 30 +++++++++++----
 6 files changed, 164 insertions(+), 54 deletions(-)
 create mode 100644 ui/detailsview.go

diff --git a/image/image.go b/image/image.go
index 638efcd..b923433 100644
--- a/image/image.go
+++ b/image/image.go
@@ -135,8 +135,8 @@ func InitializeData(imageID string) ([]*Layer, []*filetree.FileTree) {
 	// save this image to disk temporarily to get the content info
 	fmt.Println("Fetching image...")
 	imageTarPath, tmpDir := saveImage(imageID)
-	// imageTarPath := "/tmp/dive932744808/image.tar"
-	// tmpDir := "/tmp/dive031537738"
+	// imageTarPath := "/tmp/dive229500681/image.tar"
+	// tmpDir := "/tmp/dive229500681"
 	// fmt.Println(tmpDir)
 	defer os.RemoveAll(tmpDir)
 
diff --git a/ui/detailsview.go b/ui/detailsview.go
new file mode 100644
index 0000000..b215521
--- /dev/null
+++ b/ui/detailsview.go
@@ -0,0 +1,95 @@
+package ui
+
+import (
+	"fmt"
+
+	"github.com/jroimartin/gocui"
+	"github.com/lunixbochs/vtclean"
+	"strings"
+)
+
+type DetailsView struct {
+	Name       string
+	gui        *gocui.Gui
+	view       *gocui.View
+	header     *gocui.View
+}
+
+func NewStatisticsView(name string, gui *gocui.Gui) (detailsview *DetailsView) {
+	detailsview = new(DetailsView)
+
+	// populate main fields
+	detailsview.Name = name
+	detailsview.gui = gui
+
+	return detailsview
+}
+
+func (view *DetailsView) Setup(v *gocui.View, header *gocui.View) error {
+
+	// set view options
+	view.view = v
+	view.view.Editable = false
+	view.view.Wrap = true
+	view.view.Highlight = false
+	view.view.Frame = false
+
+	view.header = header
+	view.header.Editable = false
+	view.header.Wrap = false
+	view.header.Frame = false
+
+	// set keybindings
+	if err := view.gui.SetKeybinding(view.Name, gocui.KeyArrowDown, gocui.ModNone, func(*gocui.Gui, *gocui.View) error { return view.CursorDown() }); err != nil {
+		return err
+	}
+	if err := view.gui.SetKeybinding(view.Name, gocui.KeyArrowUp, gocui.ModNone, func(*gocui.Gui, *gocui.View) error { return view.CursorUp() }); err != nil {
+		return err
+	}
+
+	return view.Render()
+}
+
+func (view *DetailsView) IsVisible() bool {
+	if view == nil {return false}
+	return true
+}
+
+func (view *DetailsView) Update() error {
+	return nil
+}
+
+func (view *DetailsView) Render() error {
+	currentLayer := Views.Layer.currentLayer()
+
+	view.gui.Update(func(g *gocui.Gui) error {
+		// update header
+		view.header.Clear()
+		width, _ := g.Size()
+		headerStr := fmt.Sprintf("[Image & Layer Details]%s", strings.Repeat("─",width*2))
+		fmt.Fprintln(view.header, Formatting.Header(vtclean.Clean(headerStr, false)))
+
+		// update contents
+		view.view.Clear()
+		fmt.Fprintln(view.view, Formatting.Header("Command"))
+		fmt.Fprintln(view.view, currentLayer.History.CreatedBy)
+
+		return nil
+	})
+	return nil
+}
+
+func (view *DetailsView) CursorDown() error {
+	return CursorDown(view.gui, view.view)
+}
+
+func (view *DetailsView) CursorUp() error {
+	return CursorUp(view.gui, view.view)
+}
+
+
+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 cec927c..b1622dc 100644
--- a/ui/filetreeview.go
+++ b/ui/filetreeview.go
@@ -101,6 +101,13 @@ func (view *FileTreeView) IsVisible() bool {
 	return true
 }
 
+func (view *FileTreeView) resetCursor() {
+	view.view.SetCursor(0, 0)
+	view.TreeIndex = 0
+	view.bufferIndex = 0
+	view.bufferIndexLowerBound = 0
+	view.bufferIndexUpperBound = view.height()
+}
 
 func (view *FileTreeView) setTreeByLayer(bottomTreeStart, bottomTreeStop, topTreeStart, topTreeStop int) error {
 	if topTreeStop > len(view.RefTrees)-1 {
@@ -122,8 +129,8 @@ func (view *FileTreeView) setTreeByLayer(bottomTreeStart, bottomTreeStop, topTre
 	}
 	view.ModelTree.VisitDepthChildFirst(visitor, nil)
 
-	view.view.SetCursor(0, 0)
-	view.TreeIndex = 0
+	view.resetCursor()
+
 	view.ModelTree = newTree
 	view.Update()
 	return view.Render()
@@ -227,8 +234,7 @@ func (view *FileTreeView) toggleCollapse() error {
 func (view *FileTreeView) toggleShowDiffType(diffType filetree.DiffType) error {
 	view.HiddenDiffTypes[diffType] = !view.HiddenDiffTypes[diffType]
 
-	view.view.SetCursor(0, 0)
-	view.TreeIndex = 0
+	view.resetCursor()
 
 	Update()
 	Render()
@@ -299,10 +305,22 @@ func (view *FileTreeView) Render() error {
 		view.doCursorUp()
 	}
 
+	title := "Current Layer Contents"
+	if Views.Layer.CompareMode == CompareAll {
+		title = "Aggregated Layer Contents"
+	}
+
+	// indicate when selected
+	if view.gui.CurrentView() == view.view {
+		title = "● "+title
+	}
+
 	view.gui.Update(func(g *gocui.Gui) error {
 		// update the header
 		view.header.Clear()
-		headerStr := fmt.Sprintf(filetree.AttributeFormat+" %s", "P", "ermission", "UID:GID", "Size", "Filetree")
+		width, _ := g.Size()
+		headerStr := fmt.Sprintf("[%s]%s\n", title, strings.Repeat("─", width*2))
+		headerStr += fmt.Sprintf(filetree.AttributeFormat+" %s", "P", "ermission", "UID:GID", "Size", "Filetree")
 		fmt.Fprintln(view.header, Formatting.Header(vtclean.Clean(headerStr, false)))
 
 		// update the contents
diff --git a/ui/filterview.go b/ui/filterview.go
index 5849891..a7fad08 100644
--- a/ui/filterview.go
+++ b/ui/filterview.go
@@ -18,13 +18,6 @@ type FilterView struct {
 	hidden    bool
 }
 
-type Input struct {
-	name      string
-	x, y      int
-	w         int
-	maxLength int
-}
-
 func NewFilterView(name string, gui *gocui.Gui) (filterview *FilterView) {
 	filterview = new(FilterView)
 
diff --git a/ui/layerview.go b/ui/layerview.go
index 1881ee6..b36f6ea 100644
--- a/ui/layerview.go
+++ b/ui/layerview.go
@@ -7,6 +7,7 @@ import (
 	"github.com/wagoodman/dive/image"
 	"github.com/lunixbochs/vtclean"
 	"github.com/dustin/go-humanize"
+	"strings"
 )
 
 type LayerView struct {
@@ -38,9 +39,6 @@ func (view *LayerView) Setup(v *gocui.View, header *gocui.View) error {
 	view.view = v
 	view.view.Editable = false
 	view.view.Wrap = false
-	//view.view.Highlight = true
-	//view.view.SelBgColor = gocui.ColorGreen
-	//view.view.SelFgColor = gocui.ColorBlack
 	view.view.Frame = false
 
 	view.header = header
@@ -70,6 +68,10 @@ func (view *LayerView) IsVisible() bool {
 	return true
 }
 
+func (view *LayerView) currentLayer() *image.Layer {
+	return view.Layers[(len(view.Layers)-1)-view.LayerIndex]
+}
+
 func (view *LayerView) setCompareMode(compareMode CompareType) error {
 	view.CompareMode = compareMode
 	Update()
@@ -99,12 +101,6 @@ func (view *LayerView) renderCompareBar(layerIdx int) string {
 	bottomTreeStart, bottomTreeStop, topTreeStart, topTreeStop := view.getCompareIndexes()
 	result := "  "
 
-	//if debug {
-	//	v, _ := view.gui.View("debug")
-	//	v.Clear()
-	//	_, _ = fmt.Fprintf(v, "bStart: %d bStop: %d tStart: %d tStop: %d", bottomTreeStart, bottomTreeStop, topTreeStart, topTreeStop)
-	//}
-
 	if layerIdx >= bottomTreeStart && layerIdx <= bottomTreeStop {
 		result = Formatting.CompareBottom("  ")
 	}
@@ -112,18 +108,6 @@ func (view *LayerView) renderCompareBar(layerIdx int) string {
 		result = Formatting.CompareTop("  ")
 	}
 
-	//if bottomTreeStop == topTreeStart {
-	//	result += "  "
-	//} else {
-	//	if layerIdx == bottomTreeStop {
-	//		result += "─┐"
-	//	} else if layerIdx == topTreeStart {
-	//		result += "─┘"
-	//	} else {
-	//		result += "  "
-	//	}
-	//}
-
 	return result
 }
 
@@ -132,9 +116,19 @@ func (view *LayerView) Update() error {
 }
 
 func (view *LayerView) Render() error {
+
+	// indicate when selected
+	title := "Layers"
+	if view.gui.CurrentView() == view.view {
+		title = "● "+title
+	}
+
 	view.gui.Update(func(g *gocui.Gui) error {
 		// update header
-		headerStr := fmt.Sprintf("Cmp "+image.LayerFormat, "Image ID", "%Eff.", "Size", "Filter")
+		view.header.Clear()
+		width, _ := g.Size()
+		headerStr := fmt.Sprintf("[%s]%s\n", title, strings.Repeat("─", width*2))
+		headerStr += fmt.Sprintf("Cmp "+image.LayerFormat, "Image ID", "%Eff.", "Size", "Command")
 		fmt.Fprintln(view.header, Formatting.Header(vtclean.Clean(headerStr, false)))
 
 		// update contents
@@ -174,10 +168,7 @@ func (view *LayerView) CursorDown() error {
 	if view.LayerIndex < len(view.Layers) {
 		err := CursorDown(view.gui, view.view)
 		if err == nil {
-			view.LayerIndex++
-			Views.Tree.setTreeByLayer(view.getCompareIndexes())
-			view.Render()
-			// debugPrint(fmt.Sprintf("%d",len(filetree.Cache)))
+			view.SetCursor(view.LayerIndex+1)
 		}
 	}
 	return nil
@@ -187,25 +178,22 @@ func (view *LayerView) CursorUp() error {
 	if view.LayerIndex > 0 {
 		err := CursorUp(view.gui, view.view)
 		if err == nil {
-			view.LayerIndex--
-			Views.Tree.setTreeByLayer(view.getCompareIndexes())
-			view.Render()
-			// debugPrint(fmt.Sprintf("%d",len(filetree.Cache)))
+			view.SetCursor(view.LayerIndex-1)
 		}
 	}
 	return nil
 }
 
 func (view *LayerView) SetCursor(layer int) error {
-	// view.view.SetCursor(0, layer)
 	view.LayerIndex = layer
 	Views.Tree.setTreeByLayer(view.getCompareIndexes())
+	Views.Details.Render()
 	view.Render()
 
 	return nil
 }
 
 func (view *LayerView) KeyHelp() string {
-	return  renderStatusOption("^L","Layer changes", view.CompareMode == CompareLayer) +
-			renderStatusOption("^A","All changes", view.CompareMode == CompareAll)
+	return  renderStatusOption("^L","Show layer changes", view.CompareMode == CompareLayer) +
+			renderStatusOption("^A","Show aggregated changes", view.CompareMode == CompareAll)
 }
diff --git a/ui/ui.go b/ui/ui.go
index 0f11759..ca7cd04 100644
--- a/ui/ui.go
+++ b/ui/ui.go
@@ -41,11 +41,12 @@ var Formatting struct {
 }
 
 var Views struct {
-	Tree   *FileTreeView
-	Layer  *LayerView
-	Status *StatusView
-	Filter *FilterView
-	lookup map[string]View
+	Tree    *FileTreeView
+	Layer   *LayerView
+	Status  *StatusView
+	Filter  *FilterView
+	Details *DetailsView
+	lookup  map[string]View
 }
 
 type View interface {
@@ -178,7 +179,7 @@ func layout(g *gocui.Gui) error {
 	}
 	debugCols := maxX - debugWidth
 	bottomRows := 1
-	headerRows := 1
+	headerRows := 2
 
 	filterBarHeight := 1
 	statusBarHeight := 1
@@ -186,6 +187,8 @@ func layout(g *gocui.Gui) error {
 	statusBarIndex := 1
 	filterBarIndex := 2
 
+	layersHeight := len(Views.Layer.Layers) + headerRows + 1 // layers + header + base image layer row
+
 	var view, header *gocui.View
 	var viewErr, headerErr, err error
 
@@ -204,7 +207,7 @@ func layout(g *gocui.Gui) error {
 	}
 
 	// Layers
-	view, viewErr = g.SetView(Views.Layer.Name, -1, -1+headerRows, splitCols, maxY-bottomRows)
+	view, viewErr = g.SetView(Views.Layer.Name, -1, -1+headerRows, splitCols, layersHeight)
 	header, headerErr = g.SetView(Views.Layer.Name+"header", -1, -1, splitCols, headerRows)
 	if isNewView(viewErr, headerErr) {
 		Views.Layer.Setup(view, header)
@@ -212,6 +215,15 @@ func layout(g *gocui.Gui) error {
 		if _, err = g.SetCurrentView(Views.Layer.Name); err != nil {
 			return err
 		}
+		// since we are selecting the view, we should rerender to indicate it is selected
+		Views.Layer.Render()
+	}
+
+	// Details
+	view, viewErr = g.SetView(Views.Details.Name, -1, -1+layersHeight+headerRows, splitCols, maxY-bottomRows)
+	header, headerErr = g.SetView(Views.Details.Name+"header", -1, -1+layersHeight, splitCols, layersHeight+headerRows)
+	if isNewView(viewErr, headerErr) {
+		Views.Details.Setup(view, header)
 	}
 
 	// Filetree
@@ -291,6 +303,10 @@ 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.lookup[Views.Details.Name] = Views.Details
+
+
 	g.Cursor = false
 	//g.Mouse = true
 	g.SetManagerFunc(layout)

From 18b405db72fe34b079dd3523d465563465fa1623 Mon Sep 17 00:00:00 2001
From: Alex Goodman <wagoodman@users.noreply.github.com>
Date: Sun, 14 Oct 2018 10:57:59 -0400
Subject: [PATCH 3/5] Size based efficiency (#24)

---
 filetree/efficiency.go | 80 ++++++++++++++++++++++++++++++++++++++++++
 filetree/tree.go       | 27 --------------
 filetree/tree_test.go  |  4 +--
 image/image.go         |  4 ---
 image/layer.go         |  7 ++--
 ui/detailsview.go      | 30 +++++++++++++++-
 ui/layerview.go        |  4 +--
 ui/ui.go               |  4 +++
 8 files changed, 121 insertions(+), 39 deletions(-)
 create mode 100644 filetree/efficiency.go

diff --git a/filetree/efficiency.go b/filetree/efficiency.go
new file mode 100644
index 0000000..657f523
--- /dev/null
+++ b/filetree/efficiency.go
@@ -0,0 +1,80 @@
+package filetree
+
+import (
+	"sort"
+)
+
+type EfficiencySlice []*EfficiencyData
+
+type EfficiencyData struct {
+	Path              string
+	Nodes             []*FileNode
+	CumulativeSize    int64
+	minDiscoveredSize int64
+}
+
+func (d EfficiencySlice) Len() int {
+	return len(d)
+}
+
+func (d EfficiencySlice) Swap(i, j int) {
+	d[i], d[j] = d[j], d[i]
+}
+
+func (d EfficiencySlice) Less(i, j int) bool {
+	return d[i].CumulativeSize < d[j].CumulativeSize
+}
+
+
+// 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) {
+	efficiencyMap := make(map[string]*EfficiencyData)
+	inefficientMatches := make(EfficiencySlice, 0)
+
+	visitor := func(node *FileNode) error {
+		path := node.Path()
+		if _, ok := efficiencyMap[path]; !ok {
+			efficiencyMap[path] = &EfficiencyData{
+				Path:              path,
+				Nodes:             make([]*FileNode,0),
+				minDiscoveredSize: -1,
+			}
+		}
+		data := efficiencyMap[path]
+		data.CumulativeSize += node.Data.FileInfo.TarHeader.Size
+		if data.minDiscoveredSize < 0 || node.Data.FileInfo.TarHeader.Size < data.minDiscoveredSize {
+			data.minDiscoveredSize = node.Data.FileInfo.TarHeader.Size
+		}
+		data.Nodes = append(data.Nodes, node)
+
+		if len(data.Nodes) == 2 {
+			inefficientMatches = append(inefficientMatches, data)
+		}
+
+		return nil
+	}
+	visitEvaluator := func(node *FileNode) bool {
+		return node.IsLeaf()
+	}
+	for _, tree := range trees {
+		tree.VisitDepthChildFirst(visitor, visitEvaluator)
+	}
+
+
+	// calculate the score
+	var minimumPathSizes int64
+	var discoveredPathSizes int64
+
+	for _, value := range efficiencyMap {
+		minimumPathSizes += value.minDiscoveredSize
+		discoveredPathSizes += value.CumulativeSize
+	}
+	score := float64(minimumPathSizes) / float64(discoveredPathSizes)
+
+	sort.Sort(inefficientMatches)
+
+	return score, inefficientMatches
+}
+
+
diff --git a/filetree/tree.go b/filetree/tree.go
index a7bfddb..27703d5 100644
--- a/filetree/tree.go
+++ b/filetree/tree.go
@@ -262,30 +262,3 @@ func StackRange(trees []*FileTree, start, stop int) *FileTree {
 
 	return tree
 }
-
-// EfficiencyMap creates a map[string]int showing how often each int
-// appears in the
-func EfficiencyMap(trees []*FileTree) map[string]int {
-	result := make(map[string]int)
-	visitor := func(node *FileNode) error {
-		result[node.Path()]++
-		return nil
-	}
-	visitEvaluator := func(node *FileNode) bool {
-		return node.IsLeaf()
-	}
-	for _, tree := range trees {
-		tree.VisitDepthChildFirst(visitor, visitEvaluator)
-	}
-	return result
-}
-
-func EfficiencyScore(trees []*FileTree) float64 {
-	efficiencyMap := EfficiencyMap(trees)
-	uniquePaths := len(efficiencyMap)
-	pathAppearances := 0
-	for _, value := range efficiencyMap {
-		pathAppearances += value
-	}
-	return float64(uniquePaths) / float64(pathAppearances)
-}
diff --git a/filetree/tree_test.go b/filetree/tree_test.go
index 2711ede..8d7eec7 100644
--- a/filetree/tree_test.go
+++ b/filetree/tree_test.go
@@ -554,7 +554,7 @@ func TestEfficiencyScore(t *testing.T) {
 		trees[ix] = tree
 	}
 	expected := 2.0 / 6.0
-	actual := EfficiencyScore(trees)
+	actual := CalculateEfficiency(trees)
 	if math.Abs(expected-actual) > 0.0001 {
 		t.Fatalf("Expected %f but got %f", expected, actual)
 	}
@@ -567,7 +567,7 @@ func TestEfficiencyScore(t *testing.T) {
 		trees[ix] = tree
 	}
 	expected = 1.0
-	actual = EfficiencyScore(trees)
+	actual = CalculateEfficiency(trees)
 	if math.Abs(expected-actual) > 0.0001 {
 		t.Fatalf("Expected %f but got %f", expected, actual)
 	}
diff --git a/image/image.go b/image/image.go
index b923433..9b2d80f 100644
--- a/image/image.go
+++ b/image/image.go
@@ -17,10 +17,6 @@ import (
 	"golang.org/x/net/context"
 )
 
-const (
-	LayerFormat = "%-25s %5s %7s %s"
-)
-
 func check(e error) {
 	if e != nil {
 		panic(e)
diff --git a/image/layer.go b/image/layer.go
index 85d7ae2..39a88c6 100644
--- a/image/layer.go
+++ b/image/layer.go
@@ -4,10 +4,13 @@ import (
 	"github.com/wagoodman/dive/filetree"
 	"strings"
 	"fmt"
-	"strconv"
 	"github.com/dustin/go-humanize"
 )
 
+const (
+	LayerFormat = "%-25s %7s  %s"
+)
+
 type Layer struct {
 	TarPath  string
 	History ImageHistoryEntry
@@ -35,8 +38,6 @@ func (layer *Layer) String() string {
 
 	return fmt.Sprintf(LayerFormat,
 		layer.Id(),
-		strconv.Itoa(int(100.0*filetree.EfficiencyScore(layer.RefTrees[:layer.Index+1]))) + "%",
-		//"100%",
 		humanize.Bytes(uint64(layer.History.Size)),
 		strings.TrimPrefix(layer.History.CreatedBy, "/bin/sh -c "))
 }
diff --git a/ui/detailsview.go b/ui/detailsview.go
index b215521..61a542d 100644
--- a/ui/detailsview.go
+++ b/ui/detailsview.go
@@ -6,6 +6,9 @@ import (
 	"github.com/jroimartin/gocui"
 	"github.com/lunixbochs/vtclean"
 	"strings"
+	"github.com/wagoodman/dive/filetree"
+	"strconv"
+	"github.com/dustin/go-humanize"
 )
 
 type DetailsView struct {
@@ -13,6 +16,8 @@ type DetailsView struct {
 	gui        *gocui.Gui
 	view       *gocui.View
 	header     *gocui.View
+	efficiency float64
+	inefficiencies filetree.EfficiencySlice
 }
 
 func NewStatisticsView(name string, gui *gocui.Gui) (detailsview *DetailsView) {
@@ -55,13 +60,32 @@ func (view *DetailsView) IsVisible() bool {
 	return true
 }
 
+// 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
 }
 
 func (view *DetailsView) Render() error {
 	currentLayer := Views.Layer.currentLayer()
 
+	var wastedSpace int64
+
+	template := "%5s  %12s  %-s\n"
+	inefficiencyReport := fmt.Sprintf(Formatting.Header(template), "Count", "Total Space", "Path")
+	for idx := len(view.inefficiencies)-1; idx > 0; idx-- {
+		data := view.inefficiencies[idx]
+		if data.CumulativeSize == 0 {
+			continue
+		}
+		wastedSpace += data.CumulativeSize
+		inefficiencyReport += fmt.Sprintf(template, strconv.Itoa(len(data.Nodes)), humanize.Bytes(uint64(data.CumulativeSize)), data.Path)
+	}
+
+	effStr := fmt.Sprintf("\n%s %d %%", Formatting.Header("Image efficiency score:"), int(100.0*view.efficiency))
+	spaceStr := fmt.Sprintf("%s %s\n", Formatting.Header("Potential wasted space:"),  humanize.Bytes(uint64(wastedSpace)))
+
 	view.gui.Update(func(g *gocui.Gui) error {
 		// update header
 		view.header.Clear()
@@ -71,9 +95,13 @@ func (view *DetailsView) Render() error {
 
 		// update contents
 		view.view.Clear()
-		fmt.Fprintln(view.view, Formatting.Header("Command"))
+		fmt.Fprintln(view.view, Formatting.Header("Layer Command"))
 		fmt.Fprintln(view.view, currentLayer.History.CreatedBy)
 
+		fmt.Fprintln(view.view, effStr)
+		fmt.Fprintln(view.view, spaceStr)
+
+		fmt.Fprintln(view.view, inefficiencyReport)
 		return nil
 	})
 	return nil
diff --git a/ui/layerview.go b/ui/layerview.go
index b36f6ea..1815d86 100644
--- a/ui/layerview.go
+++ b/ui/layerview.go
@@ -128,7 +128,7 @@ func (view *LayerView) Render() error {
 		view.header.Clear()
 		width, _ := g.Size()
 		headerStr := fmt.Sprintf("[%s]%s\n", title, strings.Repeat("─", width*2))
-		headerStr += fmt.Sprintf("Cmp "+image.LayerFormat, "Image ID", "%Eff.", "Size", "Command")
+		headerStr += fmt.Sprintf("Cmp "+image.LayerFormat, "Image ID", "Size", "Command")
 		fmt.Fprintln(view.header, Formatting.Header(vtclean.Clean(headerStr, false)))
 
 		// update contents
@@ -146,7 +146,7 @@ func (view *LayerView) Render() error {
 					layerId = fmt.Sprintf("%-25s", layer.History.ID)
 				}
 
-				layerStr = fmt.Sprintf(image.LayerFormat, layerId, "", humanize.Bytes(uint64(layer.History.Size)), "FROM "+layer.Id())
+				layerStr = fmt.Sprintf(image.LayerFormat, layerId, humanize.Bytes(uint64(layer.History.Size)), "FROM "+layer.Id())
 			}
 
 			compareBar := view.renderCompareBar(idx)
diff --git a/ui/ui.go b/ui/ui.go
index ca7cd04..bc4e6fa 100644
--- a/ui/ui.go
+++ b/ui/ui.go
@@ -311,6 +311,10 @@ func Run(layers []*image.Layer, refTrees []*filetree.FileTree) {
 	//g.Mouse = true
 	g.SetManagerFunc(layout)
 
+	// perform the first update and render now that all resources have been loaded
+	Update()
+	Render()
+
 	// let the default position of the cursor be the last layer
 	// Views.Layer.SetCursor(len(Views.Layer.Layers)-1)
 

From 4e7e9b64e3cf469b080d1afb1306964f2debd8f0 Mon Sep 17 00:00:00 2001
From: Alex Goodman <wagoodman@gmail.com>
Date: Sun, 14 Oct 2018 11:04:11 -0400
Subject: [PATCH 4/5] dont show inefficiencies breakdown when there are none

---
 ui/detailsview.go | 5 +++++
 1 file changed, 5 insertions(+)

diff --git a/ui/detailsview.go b/ui/detailsview.go
index 61a542d..9cac387 100644
--- a/ui/detailsview.go
+++ b/ui/detailsview.go
@@ -73,15 +73,20 @@ func (view *DetailsView) Render() error {
 	var wastedSpace int64
 
 	template := "%5s  %12s  %-s\n"
+	var trueInefficiencies int
 	inefficiencyReport := fmt.Sprintf(Formatting.Header(template), "Count", "Total Space", "Path")
 	for idx := len(view.inefficiencies)-1; idx > 0; idx-- {
 		data := view.inefficiencies[idx]
 		if data.CumulativeSize == 0 {
 			continue
 		}
+		trueInefficiencies++
 		wastedSpace += data.CumulativeSize
 		inefficiencyReport += fmt.Sprintf(template, strconv.Itoa(len(data.Nodes)), humanize.Bytes(uint64(data.CumulativeSize)), data.Path)
 	}
+	if trueInefficiencies == 0 {
+		inefficiencyReport = ""
+	}
 
 	effStr := fmt.Sprintf("\n%s %d %%", Formatting.Header("Image efficiency score:"), int(100.0*view.efficiency))
 	spaceStr := fmt.Sprintf("%s %s\n", Formatting.Header("Potential wasted space:"),  humanize.Bytes(uint64(wastedSpace)))

From 4c99bd093df6a6963d0736f8eb3c1b69ab12ffd8 Mon Sep 17 00:00:00 2001
From: Alex Goodman <wagoodman@gmail.com>
Date: Mon, 15 Oct 2018 22:51:48 -0400
Subject: [PATCH 5/5] added basic documentation

---
 cmd/analyze.go         |  2 +
 cmd/build.go           |  3 ++
 cmd/root.go            |  2 +-
 filetree/data.go       | 78 +++++++++++++++++++++---------------
 filetree/data_test.go  |  2 +-
 filetree/efficiency.go | 23 ++++++-----
 filetree/node.go       | 58 ++++++++++++++++-----------
 filetree/tree.go       | 42 ++++++++++++++------
 filetree/tree_test.go  | 20 +++++-----
 image/layer.go         |  3 ++
 ui/detailsview.go      | 44 +++++++++++++--------
 ui/filetreeview.go     | 75 +++++++++++++++++++++--------------
 ui/filterview.go       | 44 ++++++++++-----------
 ui/layerview.go        | 89 +++++++++++++++++++++++++-----------------
 ui/statusview.go       | 33 ++++++++++------
 ui/ui.go               | 34 ++++++++++++----
 16 files changed, 341 insertions(+), 211 deletions(-)

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