From d53d8926bca6c831de5d6599547913b8a580d4f2 Mon Sep 17 00:00:00 2001
From: Alex Goodman <wagoodman@gmail.com>
Date: Mon, 7 Oct 2019 11:44:41 -0400
Subject: [PATCH] stop swallowing errors & cleanup exit logic

---
 cmd/analyze.go                       | 11 ++--
 cmd/build.go                         |  3 -
 cmd/root.go                          |  6 +-
 dive/image/docker/image_archive.go   |  7 +--
 dive/image/podman/image_directory.go |  2 +-
 runtime/run.go                       | 24 ++++----
 runtime/ui/details_controller.go     | 30 ++++++----
 runtime/ui/filetree_controller.go    | 31 ++++++-----
 runtime/ui/filetree_viewmodel.go     | 22 +++++---
 runtime/ui/filter_controller.go      |  6 +-
 runtime/ui/layer_controller.go       | 29 ++++++----
 runtime/ui/status_controller.go      |  9 ++-
 runtime/ui/ui.go                     | 82 ++++++++++++++++++----------
 utils/exit.go                        | 34 ------------
 14 files changed, 153 insertions(+), 143 deletions(-)
 delete mode 100644 utils/exit.go

diff --git a/cmd/analyze.go b/cmd/analyze.go
index 1b181cf..4e6c27e 100644
--- a/cmd/analyze.go
+++ b/cmd/analyze.go
@@ -3,16 +3,15 @@ package cmd
 import (
 	"fmt"
 	"github.com/wagoodman/dive/dive"
+	"os"
 
 	"github.com/spf13/cobra"
 	"github.com/wagoodman/dive/runtime"
-	"github.com/wagoodman/dive/utils"
 )
 
 // doAnalyzeCmd takes a docker image tag, digest, or id and displays the
 // image analysis to the screen
 func doAnalyzeCmd(cmd *cobra.Command, args []string) {
-	defer utils.Cleanup()
 
 	if len(args) == 0 {
 		printVersionFlag, err := cmd.PersistentFlags().GetBool("version")
@@ -22,13 +21,13 @@ func doAnalyzeCmd(cmd *cobra.Command, args []string) {
 		}
 
 		fmt.Println("No image argument given")
-		utils.Exit(1)
+		os.Exit(1)
 	}
 
 	userImage := args[0]
 	if userImage == "" {
 		fmt.Println("No image argument given")
-		utils.Exit(1)
+		os.Exit(1)
 	}
 
 	initLogging()
@@ -37,13 +36,13 @@ func doAnalyzeCmd(cmd *cobra.Command, args []string) {
 
 	if err != nil {
 		fmt.Printf("ci configuration error: %v\n", err)
-		utils.Exit(1)
+		os.Exit(1)
 	}
 
 	engine, err := cmd.PersistentFlags().GetString("engine")
 	if err != nil {
 		fmt.Printf("unable to determine engine: %v\n", err)
-		utils.Exit(1)
+		os.Exit(1)
 	}
 
 	runtime.Run(runtime.Options{
diff --git a/cmd/build.go b/cmd/build.go
index cc6e09a..0cff973 100644
--- a/cmd/build.go
+++ b/cmd/build.go
@@ -5,7 +5,6 @@ import (
 	"github.com/spf13/viper"
 	"github.com/wagoodman/dive/dive"
 	"github.com/wagoodman/dive/runtime"
-	"github.com/wagoodman/dive/utils"
 )
 
 // buildCmd represents the build command
@@ -22,8 +21,6 @@ func init() {
 
 // doBuildCmd implements the steps taken for the build command
 func doBuildCmd(cmd *cobra.Command, args []string) {
-	defer utils.Cleanup()
-
 	initLogging()
 
 	// there is no cli options allowed, only config can be supplied
diff --git a/cmd/root.go b/cmd/root.go
index b09b5aa..41abe52 100644
--- a/cmd/root.go
+++ b/cmd/root.go
@@ -13,7 +13,6 @@ import (
 	log "github.com/sirupsen/logrus"
 	"github.com/spf13/cobra"
 	"github.com/spf13/viper"
-	"github.com/wagoodman/dive/utils"
 )
 
 var cfgFile string
@@ -36,9 +35,8 @@ the amount of wasted space and identifies the offending files from the image.`,
 func Execute() {
 	if err := rootCmd.Execute(); err != nil {
 		fmt.Println(err)
-		utils.Exit(1)
+		os.Exit(1)
 	}
-	utils.Cleanup()
 }
 
 func init() {
@@ -160,7 +158,7 @@ func getCfgFile(fromFlag string) string {
 	home, err := homedir.Dir()
 	if err != nil {
 		fmt.Println(err)
-		utils.Exit(0)
+		os.Exit(0)
 	}
 
 	xdgHome := os.Getenv("XDG_CONFIG_HOME")
diff --git a/dive/image/docker/image_archive.go b/dive/image/docker/image_archive.go
index 69b1772..5c4b001 100644
--- a/dive/image/docker/image_archive.go
+++ b/dive/image/docker/image_archive.go
@@ -5,9 +5,9 @@ import (
 	"fmt"
 	"github.com/wagoodman/dive/dive/filetree"
 	"github.com/wagoodman/dive/dive/image"
-	"github.com/wagoodman/dive/utils"
 	"io"
 	"io/ioutil"
+	"os"
 	"strings"
 )
 
@@ -37,7 +37,7 @@ func NewImageArchive(tarFile io.ReadCloser) (*ImageArchive, error) {
 
 		if err != nil {
 			fmt.Println(err)
-			utils.Exit(1)
+			os.Exit(1)
 		}
 
 		name := header.Name
@@ -116,8 +116,7 @@ func getFileList(tarReader *tar.Reader) ([]filetree.FileInfo, error) {
 		if err == io.EOF {
 			break
 		} else if err != nil {
-			fmt.Println(err)
-			utils.Exit(1)
+			return nil, err
 		}
 
 		name := header.Name
diff --git a/dive/image/podman/image_directory.go b/dive/image/podman/image_directory.go
index 4b03dae..8e551a8 100644
--- a/dive/image/podman/image_directory.go
+++ b/dive/image/podman/image_directory.go
@@ -58,7 +58,7 @@ func NewImageDirectoryRef(img *podmanImage.Image) (*ImageDirectoryRef, error) {
 		// record the tree and layer info
 		imgDirRef.treeMap[curImg.ID()] = tree
 		imgDirRef.layerMap[curImg.ID()] = curImg
-		imgDirRef.layerOrder = append(imgDirRef.layerOrder, curImg.ID())
+		imgDirRef.layerOrder = append([]string{curImg.ID()}, imgDirRef.layerOrder...)
 
 		// continue to the next image
 		curImg, err = curImg.GetParent(ctx)
diff --git a/runtime/run.go b/runtime/run.go
index b8185e8..65b3670 100644
--- a/runtime/run.go
+++ b/runtime/run.go
@@ -6,6 +6,7 @@ import (
 	"github.com/wagoodman/dive/dive"
 	"github.com/wagoodman/dive/runtime/ci"
 	"github.com/wagoodman/dive/runtime/export"
+	"os"
 	"time"
 
 	"github.com/dustin/go-humanize"
@@ -26,9 +27,9 @@ func runCi(analysis *image.AnalysisResult, options Options) {
 	evaluator.Report()
 
 	if pass {
-		utils.Exit(0)
+		os.Exit(0)
 	}
-	utils.Exit(1)
+	os.Exit(1)
 }
 
 func Run(options Options) {
@@ -44,7 +45,7 @@ func Run(options Options) {
 	imageResolver, err := dive.GetImageHandler(options.Engine)
 	if err != nil {
 		fmt.Printf("cannot determine image provider: %v\n", err)
-		utils.Exit(1)
+		os.Exit(1)
 	}
 
 	var img *image.Image
@@ -54,13 +55,13 @@ func Run(options Options) {
 		img, err = imageResolver.Build(options.BuildArgs)
 		if err != nil {
 			fmt.Printf("cannot build image: %v\n", err)
-			utils.Exit(1)
+			os.Exit(1)
 		}
 	} else {
 		img, err = imageResolver.Fetch(options.ImageId)
 		if err != nil {
 			fmt.Printf("cannot fetch image: %v\n", err)
-			utils.Exit(1)
+			os.Exit(1)
 		}
 	}
 
@@ -76,14 +77,14 @@ func Run(options Options) {
 	result, err := img.Analyze()
 	if err != nil {
 		fmt.Printf("cannot analyze image: %v\n", err)
-		utils.Exit(1)
+		os.Exit(1)
 	}
 
 	if doExport {
 		err = export.NewExport(result).ToFile(options.ExportFile)
 		if err != nil {
 			fmt.Printf("cannot write export file: %v\n", err)
-			utils.Exit(1)
+			os.Exit(1)
 		}
 	}
 
@@ -91,7 +92,7 @@ func Run(options Options) {
 		runCi(result, options)
 	} else {
 		if doExport {
-			utils.Exit(0)
+			os.Exit(0)
 		}
 
 		fmt.Println(utils.TitleFormat("Building cache..."))
@@ -99,7 +100,7 @@ func Run(options Options) {
 		err := cache.Build()
 		if err != nil {
 			logrus.Error(err)
-			utils.Exit(1)
+			os.Exit(1)
 		}
 
 		// it appears there is a race condition where termbox.Init() will
@@ -110,11 +111,10 @@ func Run(options Options) {
 		time.Sleep(100 * time.Millisecond)
 
 		err = ui.Run(result, cache)
-
 		if err != nil {
 			logrus.Error(err)
-			utils.Exit(1)
+			os.Exit(1)
 		}
-		utils.Exit(0)
+		os.Exit(0)
 	}
 }
diff --git a/runtime/ui/details_controller.go b/runtime/ui/details_controller.go
index 92d7a6a..a9ec51c 100644
--- a/runtime/ui/details_controller.go
+++ b/runtime/ui/details_controller.go
@@ -2,6 +2,7 @@ package ui
 
 import (
 	"fmt"
+	"github.com/sirupsen/logrus"
 	"github.com/wagoodman/dive/dive/filetree"
 	"strconv"
 	"strings"
@@ -121,22 +122,29 @@ func (controller *DetailsController) Render() error {
 		layerHeaderStr := fmt.Sprintf("[Layer Details]%s", strings.Repeat("─", width-15))
 		imageHeaderStr := fmt.Sprintf("[Image Details]%s", strings.Repeat("─", width-15))
 
-		_, _ = fmt.Fprintln(controller.header, Formatting.Header(vtclean.Clean(layerHeaderStr, false)))
+		_, err := fmt.Fprintln(controller.header, Formatting.Header(vtclean.Clean(layerHeaderStr, false)))
+		if err != nil {
+			return err
+		}
 
 		// update contents
 		controller.view.Clear()
-		_, _ = fmt.Fprintln(controller.view, Formatting.Header("Digest: ")+currentLayer.Id())
-		_, _ = fmt.Fprintln(controller.view, Formatting.Header("Command:"))
-		_, _ = fmt.Fprintln(controller.view, currentLayer.Command())
 
-		_, _ = fmt.Fprintln(controller.view, "\n"+Formatting.Header(vtclean.Clean(imageHeaderStr, false)))
+		var lines = make([]string, 0)
+		lines = append(lines, Formatting.Header("Digest: ")+currentLayer.Id())
+		lines = append(lines, Formatting.Header("Command:"))
+		lines = append(lines, currentLayer.Command())
+		lines = append(lines, "\n"+Formatting.Header(vtclean.Clean(imageHeaderStr, false)))
+		lines = append(lines, imageSizeStr)
+		lines = append(lines, wastedSpaceStr)
+		lines = append(lines, effStr+"\n")
+		lines = append(lines, inefficiencyReport)
 
-		_, _ = fmt.Fprintln(controller.view, imageSizeStr)
-		_, _ = fmt.Fprintln(controller.view, wastedSpaceStr)
-		_, _ = fmt.Fprintln(controller.view, effStr+"\n")
-
-		_, _ = fmt.Fprintln(controller.view, inefficiencyReport)
-		return nil
+		_, err = fmt.Fprintln(controller.view, strings.Join(lines, "\n"))
+		if err != nil {
+			logrus.Debug("unable to write to buffer: ", err)
+		}
+		return err
 	})
 	return nil
 }
diff --git a/runtime/ui/filetree_controller.go b/runtime/ui/filetree_controller.go
index 90bcba6..16a72b2 100644
--- a/runtime/ui/filetree_controller.go
+++ b/runtime/ui/filetree_controller.go
@@ -42,15 +42,17 @@ type FileTreeController struct {
 }
 
 // NewFileTreeController creates a new view object attached the the global [gocui] screen object.
-func NewFileTreeController(name string, gui *gocui.Gui, tree *filetree.FileTree, refTrees []*filetree.FileTree, cache filetree.TreeCache) (controller *FileTreeController) {
+func NewFileTreeController(name string, gui *gocui.Gui, tree *filetree.FileTree, refTrees []*filetree.FileTree, cache filetree.TreeCache) (controller *FileTreeController, err error) {
 	controller = new(FileTreeController)
 
 	// populate main fields
 	controller.Name = name
 	controller.gui = gui
-	controller.vm = NewFileTreeViewModel(tree, refTrees, cache)
+	controller.vm, err = NewFileTreeViewModel(tree, refTrees, cache)
+	if err != nil {
+		return nil, err
+	}
 
-	var err error
 	controller.keybindingToggleCollapse, err = keybinding.ParseAll(viper.GetString("keybinding.toggle-collapse-dir"))
 	if err != nil {
 		logrus.Error(err)
@@ -100,7 +102,7 @@ func NewFileTreeController(name string, gui *gocui.Gui, tree *filetree.FileTree,
 		logrus.Error(err)
 	}
 
-	return controller
+	return controller, err
 }
 
 // Setup initializes the UI concerns within the context of a global [gocui] view object.
@@ -302,19 +304,15 @@ func (controller *FileTreeController) toggleAttributes() error {
 	if err != nil {
 		return err
 	}
-	// we need to render the changes to the status pane as well
-	Update()
-	Render()
-	return nil
+	// we need to render the changes to the status pane as well (not just this contoller/view)
+	return UpdateAndRender()
 }
 
 // toggleShowDiffType will show/hide the selected DiffType in the filetree pane.
 func (controller *FileTreeController) toggleShowDiffType(diffType filetree.DiffType) error {
 	controller.vm.toggleShowDiffType(diffType)
-	// we need to render the changes to the status pane as well
-	Update()
-	Render()
-	return nil
+	// we need to render the changes to the status pane as well (not just this contoller/view)
+	return UpdateAndRender()
 }
 
 // filterRegex will return a regular expression object to match the user's filter input.
@@ -383,10 +381,13 @@ func (controller *FileTreeController) Render() error {
 
 		// update the contents
 		controller.view.Clear()
-		_ = controller.vm.Render()
-		_, _ = fmt.Fprint(controller.view, controller.vm.mainBuf.String())
+		err := controller.vm.Render()
+		if err != nil {
+			return err
+		}
+		_, err = fmt.Fprint(controller.view, controller.vm.mainBuf.String())
 
-		return nil
+		return err
 	})
 	return nil
 }
diff --git a/runtime/ui/filetree_viewmodel.go b/runtime/ui/filetree_viewmodel.go
index 30009fd..18ebfdd 100644
--- a/runtime/ui/filetree_viewmodel.go
+++ b/runtime/ui/filetree_viewmodel.go
@@ -6,11 +6,9 @@ import (
 	"regexp"
 	"strings"
 
+	"github.com/lunixbochs/vtclean"
 	"github.com/sirupsen/logrus"
 	"github.com/spf13/viper"
-	"github.com/wagoodman/dive/utils"
-
-	"github.com/lunixbochs/vtclean"
 	"github.com/wagoodman/dive/dive/filetree"
 )
 
@@ -36,7 +34,7 @@ type FileTreeViewModel struct {
 }
 
 // NewFileTreeController creates a new view object attached the the global [gocui] screen object.
-func NewFileTreeViewModel(tree *filetree.FileTree, refTrees []*filetree.FileTree, cache filetree.TreeCache) (treeViewModel *FileTreeViewModel) {
+func NewFileTreeViewModel(tree *filetree.FileTree, refTrees []*filetree.FileTree, cache filetree.TreeCache) (treeViewModel *FileTreeViewModel, err error) {
 	treeViewModel = new(FileTreeViewModel)
 
 	// populate main fields
@@ -59,11 +57,11 @@ func NewFileTreeViewModel(tree *filetree.FileTree, refTrees []*filetree.FileTree
 		case "unmodified":
 			treeViewModel.HiddenDiffTypes[filetree.Unmodified] = true
 		default:
-			utils.PrintAndExit(fmt.Sprintf("unknown diff.hide value: %s", t))
+			return nil, fmt.Errorf("unknown diff.hide value: %s", t)
 		}
 	}
 
-	return treeViewModel
+	return treeViewModel, nil
 }
 
 // Setup initializes the UI concerns within the context of a global [gocui] view object.
@@ -420,9 +418,17 @@ func (vm *FileTreeViewModel) Render() error {
 	vm.mainBuf.Reset()
 	for idx, line := range lines {
 		if idx == vm.bufferIndex {
-			fmt.Fprintln(&vm.mainBuf, Formatting.Selected(vtclean.Clean(line, false)))
+			_, err := fmt.Fprintln(&vm.mainBuf, Formatting.Selected(vtclean.Clean(line, false)))
+			if err != nil {
+				logrus.Debug("unable to write to buffer: ", err)
+				return err
+			}
 		} else {
-			fmt.Fprintln(&vm.mainBuf, line)
+			_, err := fmt.Fprintln(&vm.mainBuf, line)
+			if err != nil {
+				logrus.Debug("unable to write to buffer: ", err)
+				return err
+			}
 		}
 	}
 	return nil
diff --git a/runtime/ui/filter_controller.go b/runtime/ui/filter_controller.go
index 7902052..bd740b3 100644
--- a/runtime/ui/filter_controller.go
+++ b/runtime/ui/filter_controller.go
@@ -3,6 +3,7 @@ package ui
 import (
 	"fmt"
 	"github.com/jroimartin/gocui"
+	"github.com/sirupsen/logrus"
 )
 
 // FilterController holds the UI objects and data models for populating the bottom row. Specifically the pane that
@@ -99,9 +100,10 @@ func (controller *FilterController) Update() error {
 // Render flushes the state objects to the screen. Currently this is the users path filter input.
 func (controller *FilterController) Render() error {
 	controller.gui.Update(func(g *gocui.Gui) error {
-		// render the header
 		_, err := fmt.Fprintln(controller.header, Formatting.Header(controller.headerStr))
-
+		if err != nil {
+			logrus.Error("unable to write to buffer: ", err)
+		}
 		return err
 	})
 	return nil
diff --git a/runtime/ui/layer_controller.go b/runtime/ui/layer_controller.go
index ed7f259..b7c3740 100644
--- a/runtime/ui/layer_controller.go
+++ b/runtime/ui/layer_controller.go
@@ -9,7 +9,6 @@ import (
 	"github.com/lunixbochs/vtclean"
 	"github.com/sirupsen/logrus"
 	"github.com/spf13/viper"
-	"github.com/wagoodman/dive/utils"
 	"github.com/wagoodman/keybinding"
 )
 
@@ -33,7 +32,7 @@ type LayerController struct {
 }
 
 // NewLayerController creates a new view object attached the the global [gocui] screen object.
-func NewLayerController(name string, gui *gocui.Gui, layers []image.Layer) (controller *LayerController) {
+func NewLayerController(name string, gui *gocui.Gui, layers []image.Layer) (controller *LayerController, err error) {
 	controller = new(LayerController)
 
 	// populate main fields
@@ -47,10 +46,9 @@ func NewLayerController(name string, gui *gocui.Gui, layers []image.Layer) (cont
 	case false:
 		controller.CompareMode = CompareLayer
 	default:
-		utils.PrintAndExit(fmt.Sprintf("unknown layer.show-aggregated-changes value: %v", mode))
+		return nil, fmt.Errorf("unknown layer.show-aggregated-changes value: %v", mode)
 	}
 
-	var err error
 	controller.keybindingCompareAll, err = keybinding.ParseAll(viper.GetString("keybinding.compare-all"))
 	if err != nil {
 		logrus.Error(err)
@@ -71,7 +69,7 @@ func NewLayerController(name string, gui *gocui.Gui, layers []image.Layer) (cont
 		logrus.Error(err)
 	}
 
-	return controller
+	return controller, err
 }
 
 // Setup initializes the UI concerns within the context of a global [gocui] view object.
@@ -218,8 +216,11 @@ func (controller *LayerController) currentLayer() image.Layer {
 // setCompareMode switches the layer comparison between a single-layer comparison to an aggregated comparison.
 func (controller *LayerController) setCompareMode(compareMode CompareType) error {
 	controller.CompareMode = compareMode
-	Update()
-	Render()
+	err := UpdateAndRender()
+	if err != nil {
+		logrus.Errorf("unable to set compare mode: %+v", err)
+		return err
+	}
 	return Controllers.Tree.setTreeByLayer(controller.getCompareIndexes())
 }
 
@@ -283,7 +284,10 @@ func (controller *LayerController) Render() error {
 		width, _ := g.Size()
 		headerStr := fmt.Sprintf("[%s]%s\n", title, strings.Repeat("─", width*2))
 		headerStr += fmt.Sprintf("Cmp"+image.LayerFormat, "Size", "Command")
-		_, _ = fmt.Fprintln(controller.header, Formatting.Header(vtclean.Clean(headerStr, false)))
+		_, err := fmt.Fprintln(controller.header, Formatting.Header(vtclean.Clean(headerStr, false)))
+		if err != nil {
+			return err
+		}
 
 		// update contents
 		controller.view.Clear()
@@ -295,9 +299,14 @@ func (controller *LayerController) Render() error {
 			compareBar := controller.renderCompareBar(idx)
 
 			if idx == controller.LayerIndex {
-				_, _ = fmt.Fprintln(controller.view, compareBar+" "+Formatting.Selected(layerStr))
+				_, err = fmt.Fprintln(controller.view, compareBar+" "+Formatting.Selected(layerStr))
 			} else {
-				_, _ = fmt.Fprintln(controller.view, compareBar+" "+layerStr)
+				_, err = fmt.Fprintln(controller.view, compareBar+" "+layerStr)
+			}
+
+			if err != nil {
+				logrus.Debug("unable to write to buffer: ", err)
+				return err
 			}
 
 		}
diff --git a/runtime/ui/status_controller.go b/runtime/ui/status_controller.go
index 2b5e933..8e590df 100644
--- a/runtime/ui/status_controller.go
+++ b/runtime/ui/status_controller.go
@@ -2,6 +2,7 @@ package ui
 
 import (
 	"fmt"
+	"github.com/sirupsen/logrus"
 
 	"strings"
 
@@ -61,11 +62,13 @@ func (controller *StatusController) Update() error {
 func (controller *StatusController) Render() error {
 	controller.gui.Update(func(g *gocui.Gui) error {
 		controller.view.Clear()
-		_, _ = fmt.Fprintln(controller.view, controller.KeyHelp()+Controllers.lookup[controller.gui.CurrentView().Name()].KeyHelp()+Formatting.StatusNormal("▏"+strings.Repeat(" ", 1000)))
+		_, err := fmt.Fprintln(controller.view, controller.KeyHelp()+Controllers.lookup[controller.gui.CurrentView().Name()].KeyHelp()+Formatting.StatusNormal("▏"+strings.Repeat(" ", 1000)))
+		if err != nil {
+			logrus.Debug("unable to write to buffer: ", err)
+		}
 
-		return nil
+		return err
 	})
-	// todo: blerg
 	return nil
 }
 
diff --git a/runtime/ui/ui.go b/runtime/ui/ui.go
index 6f4e5ed..0e6c3d2 100644
--- a/runtime/ui/ui.go
+++ b/runtime/ui/ui.go
@@ -9,7 +9,6 @@ import (
 	"github.com/sirupsen/logrus"
 	"github.com/spf13/viper"
 	"github.com/wagoodman/dive/dive/filetree"
-	"github.com/wagoodman/dive/utils"
 	"github.com/wagoodman/keybinding"
 )
 
@@ -72,6 +71,22 @@ type View interface {
 	IsVisible() bool
 }
 
+func UpdateAndRender() error {
+	err := Update()
+	if err != nil {
+		logrus.Debug("failed update: ", err)
+		return err
+	}
+
+	err = Render()
+	if err != nil {
+		logrus.Debug("failed render: ", err)
+		return err
+	}
+
+	return nil
+}
+
 // toggleView switches between the file view and the layer view and re-renders the screen.
 func toggleView(g *gocui.Gui, v *gocui.View) (err error) {
 	if v == nil || v.Name() == Controllers.Layer.Name {
@@ -79,9 +94,13 @@ func toggleView(g *gocui.Gui, v *gocui.View) (err error) {
 	} else {
 		_, err = g.SetCurrentView(Controllers.Layer.Name)
 	}
-	Update()
-	Render()
-	return err
+
+	if err != nil {
+		logrus.Error("unable to toggle view: ", err)
+		return err
+	}
+
+	return UpdateAndRender()
 }
 
 // toggleFilterView shows/hides the file tree filter pane.
@@ -95,22 +114,24 @@ func toggleFilterView(g *gocui.Gui, v *gocui.View) error {
 	if !Controllers.Filter.hidden {
 		_, err := g.SetCurrentView(Controllers.Filter.Name)
 		if err != nil {
+			logrus.Error("unable to toggle filter view: ", err)
 			return err
 		}
-		Update()
-		Render()
-	} else {
-		err := toggleView(g, v)
-		if err != nil {
-			return err
-		}
-
-		err = Controllers.Filter.view.SetCursor(0, 0)
-		if err != nil {
-			return err
-		}
+		return UpdateAndRender()
 	}
 
+	err := toggleView(g, v)
+	if err != nil {
+		logrus.Error("unable to toggle filter view (back): ", err)
+		return err
+	}
+
+	err = Controllers.Filter.view.SetCursor(0, 0)
+	if err != nil {
+		return err
+	}
+
+
 	return nil
 }
 
@@ -325,9 +346,10 @@ func layout(g *gocui.Gui) error {
 
 // Update refreshes the state objects for future rendering.
 func Update() error {
-	for _, view := range Controllers.lookup {
-		err := view.Update()
+	for _, controller := range Controllers.lookup {
+		err := controller.Update()
 		if err != nil {
+			logrus.Debug("unable to update controller: ")
 			return err
 		}
 	}
@@ -336,9 +358,9 @@ func Update() error {
 
 // Render flushes the state objects to the screen.
 func Render() error {
-	for _, view := range Controllers.lookup {
-		if view.IsVisible() {
-			err := view.Render()
+	for _, controller := range Controllers.lookup {
+		if controller.IsVisible() {
+			err := controller.Render()
 			if err != nil {
 				return err
 			}
@@ -386,19 +408,24 @@ func Run(analysis *image.AnalysisResult, cache filetree.TreeCache) error {
 	if err != nil {
 		return err
 	}
-	utils.SetUi(g)
 	defer g.Close()
 
 	Controllers.lookup = make(map[string]View)
 
-	Controllers.Layer = NewLayerController("side", g, analysis.Layers)
+	Controllers.Layer, err = NewLayerController("side", g, analysis.Layers)
+	if err != nil {
+		return err
+	}
 	Controllers.lookup[Controllers.Layer.Name] = Controllers.Layer
 
 	treeStack, err := filetree.StackTreeRange(analysis.RefTrees, 0, 0)
 	if err != nil {
 		return err
 	}
-	Controllers.Tree = NewFileTreeController("main", g, treeStack, analysis.RefTrees, cache)
+	Controllers.Tree, err = NewFileTreeController("main", g, treeStack, analysis.RefTrees, cache)
+	if err != nil {
+		return err
+	}
 	Controllers.lookup[Controllers.Tree.Name] = Controllers.Tree
 
 	Controllers.Status = NewStatusController("status", g)
@@ -421,12 +448,7 @@ func Run(analysis *image.AnalysisResult, cache filetree.TreeCache) error {
 	// }
 
 	// perform the first update and render now that all resources have been loaded
-	err = Update()
-	if err != nil {
-		return err
-	}
-
-	err = Render()
+	err = UpdateAndRender()
 	if err != nil {
 		return err
 	}
diff --git a/utils/exit.go b/utils/exit.go
deleted file mode 100644
index 512d37c..0000000
--- a/utils/exit.go
+++ /dev/null
@@ -1,34 +0,0 @@
-package utils
-
-import (
-	"fmt"
-	"os"
-
-	"github.com/jroimartin/gocui"
-	"github.com/sirupsen/logrus"
-)
-
-var ui *gocui.Gui
-
-func SetUi(g *gocui.Gui) {
-	ui = g
-}
-
-func PrintAndExit(args ...interface{}) {
-	logrus.Println(args...)
-	Cleanup()
-	fmt.Println(args...)
-	os.Exit(1)
-}
-
-// Note: this should only be used when exiting from non-gocui code
-func Exit(rc int) {
-	Cleanup()
-	os.Exit(rc)
-}
-
-func Cleanup() {
-	if ui != nil {
-		ui.Close()
-	}
-}