diff --git a/pkg/commands/branch.go b/pkg/commands/branch.go
index 13c26e766..19553a26b 100644
--- a/pkg/commands/branch.go
+++ b/pkg/commands/branch.go
@@ -14,9 +14,9 @@ type Branch struct {
 	Recency string
 }
 
-// GetDisplayString returns the dispaly string of branch
-func (b *Branch) GetDisplayString() string {
-	return utils.WithPadding(b.Recency, 4) + utils.ColoredString(b.Name, b.GetColor())
+// GetDisplayStrings returns the dispaly string of branch
+func (b *Branch) GetDisplayStrings() []string {
+	return []string{b.Recency, utils.ColoredString(b.Name, b.GetColor())}
 }
 
 // GetColor branch color
diff --git a/pkg/commands/commit.go b/pkg/commands/commit.go
new file mode 100644
index 000000000..37c3e9525
--- /dev/null
+++ b/pkg/commands/commit.go
@@ -0,0 +1,26 @@
+package commands
+
+import (
+	"github.com/fatih/color"
+)
+
+// Commit : A git commit
+type Commit struct {
+	Sha           string
+	Name          string
+	Pushed        bool
+	DisplayString string
+}
+
+func (c *Commit) GetDisplayStrings() []string {
+	red := color.New(color.FgRed)
+	yellow := color.New(color.FgYellow)
+	white := color.New(color.FgWhite)
+
+	shaColor := yellow
+	if c.Pushed {
+		shaColor = red
+	}
+
+	return []string{shaColor.Sprint(c.Sha), white.Sprint(c.Name)}
+}
diff --git a/pkg/commands/file.go b/pkg/commands/file.go
new file mode 100644
index 000000000..8fcd9aff9
--- /dev/null
+++ b/pkg/commands/file.go
@@ -0,0 +1,36 @@
+package commands
+
+import "github.com/fatih/color"
+
+// File : A file from git status
+// duplicating this for now
+type File struct {
+	Name               string
+	HasStagedChanges   bool
+	HasUnstagedChanges bool
+	Tracked            bool
+	Deleted            bool
+	HasMergeConflicts  bool
+	DisplayString      string
+	Type               string // one of 'file', 'directory', and 'other'
+}
+
+// GetDisplayStrings returns the display string of a file
+func (f *File) GetDisplayStrings() []string {
+	// potentially inefficient to be instantiating these color
+	// objects with each render
+	red := color.New(color.FgRed)
+	green := color.New(color.FgGreen)
+	if !f.Tracked && !f.HasStagedChanges {
+		return []string{red.Sprint(f.DisplayString)}
+	}
+
+	output := green.Sprint(f.DisplayString[0:1])
+	output += red.Sprint(f.DisplayString[1:3])
+	if f.HasUnstagedChanges {
+		output += red.Sprint(f.Name)
+	} else {
+		output += green.Sprint(f.Name)
+	}
+	return []string{output}
+}
diff --git a/pkg/commands/git.go b/pkg/commands/git.go
index a106d8dce..c37ca9b47 100644
--- a/pkg/commands/git.go
+++ b/pkg/commands/git.go
@@ -105,17 +105,17 @@ func NewGitCommand(log *logrus.Entry, osCommand *OSCommand, tr *i18n.Localizer)
 }
 
 // GetStashEntries stash entryies
-func (c *GitCommand) GetStashEntries() []StashEntry {
+func (c *GitCommand) GetStashEntries() []*StashEntry {
 	rawString, _ := c.OSCommand.RunCommandWithOutput("git stash list --pretty='%gs'")
-	stashEntries := []StashEntry{}
+	stashEntries := []*StashEntry{}
 	for i, line := range utils.SplitLines(rawString) {
 		stashEntries = append(stashEntries, stashEntryFromLine(line, i))
 	}
 	return stashEntries
 }
 
-func stashEntryFromLine(line string, index int) StashEntry {
-	return StashEntry{
+func stashEntryFromLine(line string, index int) *StashEntry {
+	return &StashEntry{
 		Name:          line,
 		Index:         index,
 		DisplayString: line,
@@ -128,10 +128,10 @@ func (c *GitCommand) GetStashEntryDiff(index int) (string, error) {
 }
 
 // GetStatusFiles git status files
-func (c *GitCommand) GetStatusFiles() []File {
+func (c *GitCommand) GetStatusFiles() []*File {
 	statusOutput, _ := c.GitStatus()
 	statusStrings := utils.SplitLines(statusOutput)
-	files := []File{}
+	files := []*File{}
 
 	for _, statusString := range statusStrings {
 		change := statusString[0:2]
@@ -141,7 +141,7 @@ func (c *GitCommand) GetStatusFiles() []File {
 		_, untracked := map[string]bool{"??": true, "A ": true, "AM": true}[change]
 		_, hasNoStagedChanges := map[string]bool{" ": true, "U": true, "?": true}[stagedChange]
 
-		file := File{
+		file := &File{
 			Name:               filename,
 			DisplayString:      statusString,
 			HasStagedChanges:   !hasNoStagedChanges,
@@ -169,7 +169,7 @@ func (c *GitCommand) StashSave(message string) error {
 }
 
 // MergeStatusFiles merge status files
-func (c *GitCommand) MergeStatusFiles(oldFiles, newFiles []File) []File {
+func (c *GitCommand) MergeStatusFiles(oldFiles, newFiles []*File) []*File {
 	if len(oldFiles) == 0 {
 		return newFiles
 	}
@@ -177,7 +177,7 @@ func (c *GitCommand) MergeStatusFiles(oldFiles, newFiles []File) []File {
 	appendedIndexes := []int{}
 
 	// retain position of files we already could see
-	result := []File{}
+	result := []*File{}
 	for _, oldFile := range oldFiles {
 		for newIndex, newFile := range newFiles {
 			if oldFile.Name == newFile.Name {
@@ -231,9 +231,9 @@ func (c *GitCommand) UpstreamDifferenceCount() (string, string) {
 	return strings.TrimSpace(pushableCount), strings.TrimSpace(pullableCount)
 }
 
-// getCommitsToPush Returns the sha's of the commits that have not yet been pushed
+// GetCommitsToPush Returns the sha's of the commits that have not yet been pushed
 // to the remote branch of the current branch, a map is returned to ease look up
-func (c *GitCommand) getCommitsToPush() map[string]bool {
+func (c *GitCommand) GetCommitsToPush() map[string]bool {
 	pushables := map[string]bool{}
 	o, err := c.OSCommand.RunCommandWithOutput("git rev-list @{u}..head --abbrev-commit")
 	if err != nil {
@@ -413,7 +413,7 @@ func (c *GitCommand) IsInMergeState() (bool, error) {
 }
 
 // RemoveFile directly
-func (c *GitCommand) RemoveFile(file File) error {
+func (c *GitCommand) RemoveFile(file *File) error {
 	// if the file isn't tracked, we assume you want to delete it
 	if file.HasStagedChanges {
 		if err := c.OSCommand.RunCommand(fmt.Sprintf("git reset -- %s", file.Name)); err != nil {
@@ -460,16 +460,16 @@ func (c *GitCommand) GetBranchGraph(branchName string) (string, error) {
 }
 
 // GetCommits obtains the commits of the current branch
-func (c *GitCommand) GetCommits() []Commit {
-	pushables := c.getCommitsToPush()
+func (c *GitCommand) GetCommits() []*Commit {
+	pushables := c.GetCommitsToPush()
 	log := c.GetLog()
-	commits := []Commit{}
+	commits := []*Commit{}
 	// now we can split it up and turn it into commits
 	for _, line := range utils.SplitLines(log) {
 		splitLine := strings.Split(line, " ")
 		sha := splitLine[0]
 		_, pushed := pushables[sha]
-		commits = append(commits, Commit{
+		commits = append(commits, &Commit{
 			Sha:           sha,
 			Name:          strings.Join(splitLine[1:], " "),
 			Pushed:        pushed,
@@ -508,7 +508,7 @@ func (c *GitCommand) Show(sha string) string {
 }
 
 // Diff returns the diff of a file
-func (c *GitCommand) Diff(file File) string {
+func (c *GitCommand) Diff(file *File) string {
 	cachedArg := ""
 	fileName := c.OSCommand.Quote(file.Name)
 	if file.HasStagedChanges && !file.HasUnstagedChanges {
diff --git a/pkg/commands/git_structs.go b/pkg/commands/git_structs.go
index 2b3b3f2db..1590ce32e 100644
--- a/pkg/commands/git_structs.go
+++ b/pkg/commands/git_structs.go
@@ -1,32 +1,5 @@
 package commands
 
-// File : A staged/unstaged file
-type File struct {
-	Name               string
-	HasStagedChanges   bool
-	HasUnstagedChanges bool
-	Tracked            bool
-	Deleted            bool
-	HasMergeConflicts  bool
-	DisplayString      string
-	Type               string // one of 'file', 'directory', and 'other'
-}
-
-// Commit : A git commit
-type Commit struct {
-	Sha           string
-	Name          string
-	Pushed        bool
-	DisplayString string
-}
-
-// StashEntry : A git stash entry
-type StashEntry struct {
-	Index         int
-	Name          string
-	DisplayString string
-}
-
 // Conflict : A git conflict with a start middle and end corresponding to line
 // numbers in the file where the conflict bars appear
 type Conflict struct {
diff --git a/pkg/commands/git_test.go b/pkg/commands/git_test.go
index aedf5253b..790a34690 100644
--- a/pkg/commands/git_test.go
+++ b/pkg/commands/git_test.go
@@ -276,7 +276,7 @@ func TestGitCommandGetStashEntries(t *testing.T) {
 	type scenario struct {
 		testName string
 		command  func(string, ...string) *exec.Cmd
-		test     func([]StashEntry)
+		test     func([]*StashEntry)
 	}
 
 	scenarios := []scenario{
@@ -285,7 +285,7 @@ func TestGitCommandGetStashEntries(t *testing.T) {
 			func(string, ...string) *exec.Cmd {
 				return exec.Command("echo")
 			},
-			func(entries []StashEntry) {
+			func(entries []*StashEntry) {
 				assert.Len(t, entries, 0)
 			},
 		},
@@ -294,8 +294,8 @@ func TestGitCommandGetStashEntries(t *testing.T) {
 			func(string, ...string) *exec.Cmd {
 				return exec.Command("echo", "WIP on add-pkg-commands-test: 55c6af2 increase parallel build\nWIP on master: bb86a3f update github template")
 			},
-			func(entries []StashEntry) {
-				expected := []StashEntry{
+			func(entries []*StashEntry) {
+				expected := []*StashEntry{
 					{
 						0,
 						"WIP on add-pkg-commands-test: 55c6af2 increase parallel build",
@@ -342,7 +342,7 @@ func TestGitCommandGetStatusFiles(t *testing.T) {
 	type scenario struct {
 		testName string
 		command  func(string, ...string) *exec.Cmd
-		test     func([]File)
+		test     func([]*File)
 	}
 
 	scenarios := []scenario{
@@ -351,7 +351,7 @@ func TestGitCommandGetStatusFiles(t *testing.T) {
 			func(cmd string, args ...string) *exec.Cmd {
 				return exec.Command("echo")
 			},
-			func(files []File) {
+			func(files []*File) {
 				assert.Len(t, files, 0)
 			},
 		},
@@ -363,10 +363,10 @@ func TestGitCommandGetStatusFiles(t *testing.T) {
 					"MM file1.txt\nA  file3.txt\nAM file2.txt\n?? file4.txt",
 				)
 			},
-			func(files []File) {
+			func(files []*File) {
 				assert.Len(t, files, 4)
 
-				expected := []File{
+				expected := []*File{
 					{
 						Name:               "file1.txt",
 						HasStagedChanges:   true,
@@ -464,22 +464,22 @@ func TestGitCommandCommitAmend(t *testing.T) {
 func TestGitCommandMergeStatusFiles(t *testing.T) {
 	type scenario struct {
 		testName string
-		oldFiles []File
-		newFiles []File
-		test     func([]File)
+		oldFiles []*File
+		newFiles []*File
+		test     func([]*File)
 	}
 
 	scenarios := []scenario{
 		{
 			"Old file and new file are the same",
-			[]File{},
-			[]File{
+			[]*File{},
+			[]*File{
 				{
 					Name: "new_file.txt",
 				},
 			},
-			func(files []File) {
-				expected := []File{
+			func(files []*File) {
+				expected := []*File{
 					{
 						Name: "new_file.txt",
 					},
@@ -491,7 +491,7 @@ func TestGitCommandMergeStatusFiles(t *testing.T) {
 		},
 		{
 			"Several files to merge, with some identical",
-			[]File{
+			[]*File{
 				{
 					Name: "new_file1.txt",
 				},
@@ -502,7 +502,7 @@ func TestGitCommandMergeStatusFiles(t *testing.T) {
 					Name: "new_file3.txt",
 				},
 			},
-			[]File{
+			[]*File{
 				{
 					Name: "new_file4.txt",
 				},
@@ -513,8 +513,8 @@ func TestGitCommandMergeStatusFiles(t *testing.T) {
 					Name: "new_file1.txt",
 				},
 			},
-			func(files []File) {
-				expected := []File{
+			func(files []*File) {
+				expected := []*File{
 					{
 						Name: "new_file1.txt",
 					},
@@ -631,7 +631,7 @@ func TestGitCommandGetCommitsToPush(t *testing.T) {
 		t.Run(s.testName, func(t *testing.T) {
 			gitCmd := newDummyGitCommand()
 			gitCmd.OSCommand.command = s.command
-			s.test(gitCmd.getCommitsToPush())
+			s.test(gitCmd.GetCommitsToPush())
 		})
 	}
 }
@@ -1225,7 +1225,7 @@ func TestGitCommandRemoveFile(t *testing.T) {
 		testName   string
 		command    func() (func(string, ...string) *exec.Cmd, *[][]string)
 		test       func(*[][]string, error)
-		file       File
+		file       *File
 		removeFile func(string) error
 	}
 
@@ -1247,7 +1247,7 @@ func TestGitCommandRemoveFile(t *testing.T) {
 					{"reset", "--", "test"},
 				})
 			},
-			File{
+			&File{
 				Name:             "test",
 				HasStagedChanges: true,
 			},
@@ -1270,7 +1270,7 @@ func TestGitCommandRemoveFile(t *testing.T) {
 				assert.EqualError(t, err, "an error occurred when removing file")
 				assert.Len(t, *cmdsCalled, 0)
 			},
-			File{
+			&File{
 				Name:    "test",
 				Tracked: false,
 			},
@@ -1295,7 +1295,7 @@ func TestGitCommandRemoveFile(t *testing.T) {
 					{"checkout", "--", "test"},
 				})
 			},
-			File{
+			&File{
 				Name:             "test",
 				Tracked:          true,
 				HasStagedChanges: false,
@@ -1321,7 +1321,7 @@ func TestGitCommandRemoveFile(t *testing.T) {
 					{"checkout", "--", "test"},
 				})
 			},
-			File{
+			&File{
 				Name:             "test",
 				Tracked:          true,
 				HasStagedChanges: false,
@@ -1348,7 +1348,7 @@ func TestGitCommandRemoveFile(t *testing.T) {
 					{"checkout", "--", "test"},
 				})
 			},
-			File{
+			&File{
 				Name:             "test",
 				Tracked:          true,
 				HasStagedChanges: true,
@@ -1374,7 +1374,7 @@ func TestGitCommandRemoveFile(t *testing.T) {
 					{"reset", "--", "test"},
 				})
 			},
-			File{
+			&File{
 				Name:             "test",
 				Tracked:          false,
 				HasStagedChanges: true,
@@ -1398,7 +1398,7 @@ func TestGitCommandRemoveFile(t *testing.T) {
 				assert.NoError(t, err)
 				assert.Len(t, *cmdsCalled, 0)
 			},
-			File{
+			&File{
 				Name:             "test",
 				Tracked:          false,
 				HasStagedChanges: false,
@@ -1484,7 +1484,7 @@ func TestGitCommandGetCommits(t *testing.T) {
 	type scenario struct {
 		testName string
 		command  func(string, ...string) *exec.Cmd
-		test     func([]Commit)
+		test     func([]*Commit)
 	}
 
 	scenarios := []scenario{
@@ -1504,7 +1504,7 @@ func TestGitCommandGetCommits(t *testing.T) {
 
 				return nil
 			},
-			func(commits []Commit) {
+			func(commits []*Commit) {
 				assert.Len(t, commits, 0)
 			},
 		},
@@ -1524,9 +1524,9 @@ func TestGitCommandGetCommits(t *testing.T) {
 
 				return nil
 			},
-			func(commits []Commit) {
+			func(commits []*Commit) {
 				assert.Len(t, commits, 2)
-				assert.EqualValues(t, []Commit{
+				assert.EqualValues(t, []*Commit{
 					{
 						Sha:           "8a2bb0e",
 						Name:          "commit 1",
@@ -1557,7 +1557,7 @@ func TestGitCommandDiff(t *testing.T) {
 	gitCommand := newDummyGitCommand()
 	assert.NoError(t, test.GenerateRepo("lots_of_diffs.sh"))
 
-	files := []File{
+	files := []*File{
 		{
 			Name:               "deleted_staged",
 			HasStagedChanges:   false,
diff --git a/pkg/commands/stash_entry.go b/pkg/commands/stash_entry.go
new file mode 100644
index 000000000..3886fd4c9
--- /dev/null
+++ b/pkg/commands/stash_entry.go
@@ -0,0 +1,13 @@
+package commands
+
+// StashEntry : A git stash entry
+type StashEntry struct {
+	Index         int
+	Name          string
+	DisplayString string
+}
+
+// GetDisplayStrings returns the dispaly string of branch
+func (s *StashEntry) GetDisplayStrings() []string {
+	return []string{s.DisplayString}
+}
diff --git a/pkg/git/branch_list_builder.go b/pkg/git/branch_list_builder.go
index 651b684f5..151e5b0b4 100644
--- a/pkg/git/branch_list_builder.go
+++ b/pkg/git/branch_list_builder.go
@@ -34,7 +34,7 @@ func NewBranchListBuilder(log *logrus.Entry, gitCommand *commands.GitCommand) (*
 	}, nil
 }
 
-func (b *BranchListBuilder) obtainCurrentBranch() commands.Branch {
+func (b *BranchListBuilder) obtainCurrentBranch() *commands.Branch {
 	// I used go-git for this, but that breaks if you've just done a git init,
 	// even though you're on 'master'
 	branchName, err := b.GitCommand.OSCommand.RunCommandWithOutput("git symbolic-ref --short HEAD")
@@ -44,11 +44,11 @@ func (b *BranchListBuilder) obtainCurrentBranch() commands.Branch {
 			panic(err.Error())
 		}
 	}
-	return commands.Branch{Name: strings.TrimSpace(branchName), Recency: "  *"}
+	return &commands.Branch{Name: strings.TrimSpace(branchName), Recency: "  *"}
 }
 
-func (b *BranchListBuilder) obtainReflogBranches() []commands.Branch {
-	branches := make([]commands.Branch, 0)
+func (b *BranchListBuilder) obtainReflogBranches() []*commands.Branch {
+	branches := make([]*commands.Branch, 0)
 	rawString, err := b.GitCommand.OSCommand.RunCommandWithOutput("git reflog -n100 --pretty='%cr|%gs' --grep-reflog='checkout: moving' HEAD")
 	if err != nil {
 		return branches
@@ -58,14 +58,14 @@ func (b *BranchListBuilder) obtainReflogBranches() []commands.Branch {
 	for _, line := range branchLines {
 		timeNumber, timeUnit, branchName := branchInfoFromLine(line)
 		timeUnit = abbreviatedTimeUnit(timeUnit)
-		branch := commands.Branch{Name: branchName, Recency: timeNumber + timeUnit}
+		branch := &commands.Branch{Name: branchName, Recency: timeNumber + timeUnit}
 		branches = append(branches, branch)
 	}
 	return branches
 }
 
-func (b *BranchListBuilder) obtainSafeBranches() []commands.Branch {
-	branches := make([]commands.Branch, 0)
+func (b *BranchListBuilder) obtainSafeBranches() []*commands.Branch {
+	branches := make([]*commands.Branch, 0)
 
 	bIter, err := b.GitCommand.Repo.Branches()
 	if err != nil {
@@ -73,14 +73,14 @@ func (b *BranchListBuilder) obtainSafeBranches() []commands.Branch {
 	}
 	err = bIter.ForEach(func(b *plumbing.Reference) error {
 		name := b.Name().Short()
-		branches = append(branches, commands.Branch{Name: name})
+		branches = append(branches, &commands.Branch{Name: name})
 		return nil
 	})
 
 	return branches
 }
 
-func (b *BranchListBuilder) appendNewBranches(finalBranches, newBranches, existingBranches []commands.Branch, included bool) []commands.Branch {
+func (b *BranchListBuilder) appendNewBranches(finalBranches, newBranches, existingBranches []*commands.Branch, included bool) []*commands.Branch {
 	for _, newBranch := range newBranches {
 		if included == branchIncluded(newBranch.Name, existingBranches) {
 			finalBranches = append(finalBranches, newBranch)
@@ -89,7 +89,7 @@ func (b *BranchListBuilder) appendNewBranches(finalBranches, newBranches, existi
 	return finalBranches
 }
 
-func sanitisedReflogName(reflogBranch commands.Branch, safeBranches []commands.Branch) string {
+func sanitisedReflogName(reflogBranch *commands.Branch, safeBranches []*commands.Branch) string {
 	for _, safeBranch := range safeBranches {
 		if strings.ToLower(safeBranch.Name) == strings.ToLower(reflogBranch.Name) {
 			return safeBranch.Name
@@ -99,15 +99,15 @@ func sanitisedReflogName(reflogBranch commands.Branch, safeBranches []commands.B
 }
 
 // Build the list of branches for the current repo
-func (b *BranchListBuilder) Build() []commands.Branch {
-	branches := make([]commands.Branch, 0)
+func (b *BranchListBuilder) Build() []*commands.Branch {
+	branches := make([]*commands.Branch, 0)
 	head := b.obtainCurrentBranch()
 	safeBranches := b.obtainSafeBranches()
 	if len(safeBranches) == 0 {
 		return append(branches, head)
 	}
 	reflogBranches := b.obtainReflogBranches()
-	reflogBranches = uniqueByName(append([]commands.Branch{head}, reflogBranches...))
+	reflogBranches = uniqueByName(append([]*commands.Branch{head}, reflogBranches...))
 	for i, reflogBranch := range reflogBranches {
 		reflogBranches[i].Name = sanitisedReflogName(reflogBranch, safeBranches)
 	}
@@ -118,7 +118,7 @@ func (b *BranchListBuilder) Build() []commands.Branch {
 	return branches
 }
 
-func branchIncluded(branchName string, branches []commands.Branch) bool {
+func branchIncluded(branchName string, branches []*commands.Branch) bool {
 	for _, existingBranch := range branches {
 		if strings.ToLower(existingBranch.Name) == strings.ToLower(branchName) {
 			return true
@@ -127,8 +127,8 @@ func branchIncluded(branchName string, branches []commands.Branch) bool {
 	return false
 }
 
-func uniqueByName(branches []commands.Branch) []commands.Branch {
-	finalBranches := make([]commands.Branch, 0)
+func uniqueByName(branches []*commands.Branch) []*commands.Branch {
+	finalBranches := make([]*commands.Branch, 0)
 	for _, branch := range branches {
 		if branchIncluded(branch.Name, finalBranches) {
 			continue
diff --git a/pkg/gui/branches_panel.go b/pkg/gui/branches_panel.go
index 26c3c5618..0f66533b1 100644
--- a/pkg/gui/branches_panel.go
+++ b/pkg/gui/branches_panel.go
@@ -7,6 +7,7 @@ import (
 	"github.com/jesseduffield/gocui"
 	"github.com/jesseduffield/lazygit/pkg/commands"
 	"github.com/jesseduffield/lazygit/pkg/git"
+	"github.com/jesseduffield/lazygit/pkg/utils"
 )
 
 func (gui *Gui) handleBranchPress(g *gocui.Gui, v *gocui.View) error {
@@ -109,7 +110,7 @@ func (gui *Gui) handleMerge(g *gocui.Gui, v *gocui.View) error {
 	return nil
 }
 
-func (gui *Gui) getSelectedBranch(v *gocui.View) commands.Branch {
+func (gui *Gui) getSelectedBranch(v *gocui.View) *commands.Branch {
 	lineNumber := gui.getItemPosition(v)
 	return gui.State.Branches[lineNumber]
 }
@@ -151,10 +152,15 @@ func (gui *Gui) refreshBranches(g *gocui.Gui) error {
 			return err
 		}
 		gui.State.Branches = builder.Build()
+
 		v.Clear()
-		for _, branch := range gui.State.Branches {
-			fmt.Fprintln(v, branch.GetDisplayString())
+		list, err := utils.RenderList(gui.State.Branches)
+		if err != nil {
+			return err
 		}
+
+		fmt.Fprint(v, list)
+
 		gui.resetOrigin(v)
 		return gui.refreshStatus(g)
 	})
diff --git a/pkg/gui/commits_panel.go b/pkg/gui/commits_panel.go
index 075d66c83..7c09559ff 100644
--- a/pkg/gui/commits_panel.go
+++ b/pkg/gui/commits_panel.go
@@ -2,10 +2,11 @@ package gui
 
 import (
 	"errors"
+	"fmt"
 
-	"github.com/fatih/color"
 	"github.com/jesseduffield/gocui"
 	"github.com/jesseduffield/lazygit/pkg/commands"
+	"github.com/jesseduffield/lazygit/pkg/utils"
 )
 
 func (gui *Gui) refreshCommits(g *gocui.Gui) error {
@@ -15,20 +16,14 @@ func (gui *Gui) refreshCommits(g *gocui.Gui) error {
 		if err != nil {
 			panic(err)
 		}
+
 		v.Clear()
-		red := color.New(color.FgRed)
-		yellow := color.New(color.FgYellow)
-		white := color.New(color.FgWhite)
-		shaColor := white
-		for _, commit := range gui.State.Commits {
-			if commit.Pushed {
-				shaColor = red
-			} else {
-				shaColor = yellow
-			}
-			shaColor.Fprint(v, commit.Sha+" ")
-			white.Fprintln(v, commit.Name)
+		list, err := utils.RenderList(gui.State.Commits)
+		if err != nil {
+			return err
 		}
+		fmt.Fprint(v, list)
+
 		gui.refreshStatus(g)
 		if g.CurrentView().Name() == "commits" {
 			gui.handleCommitSelect(g, v)
@@ -99,7 +94,7 @@ func (gui *Gui) handleCommitSquashDown(g *gocui.Gui, v *gocui.View) error {
 }
 
 // TODO: move to files panel
-func (gui *Gui) anyUnStagedChanges(files []commands.File) bool {
+func (gui *Gui) anyUnStagedChanges(files []*commands.File) bool {
 	for _, file := range files {
 		if file.Tracked && file.HasUnstagedChanges {
 			return true
@@ -161,13 +156,13 @@ func (gui *Gui) handleRenameCommitEditor(g *gocui.Gui, v *gocui.View) error {
 	return nil
 }
 
-func (gui *Gui) getSelectedCommit(g *gocui.Gui) (commands.Commit, error) {
+func (gui *Gui) getSelectedCommit(g *gocui.Gui) (*commands.Commit, error) {
 	v, err := g.View("commits")
 	if err != nil {
 		panic(err)
 	}
 	if len(gui.State.Commits) == 0 {
-		return commands.Commit{}, errors.New(gui.Tr.SLocalize("NoCommitsThisBranch"))
+		return &commands.Commit{}, errors.New(gui.Tr.SLocalize("NoCommitsThisBranch"))
 	}
 	lineNumber := gui.getItemPosition(v)
 	if lineNumber > len(gui.State.Commits)-1 {
diff --git a/pkg/gui/files_panel.go b/pkg/gui/files_panel.go
index bfcc938bb..574e6bc1c 100644
--- a/pkg/gui/files_panel.go
+++ b/pkg/gui/files_panel.go
@@ -7,16 +7,17 @@ import (
 
 	// "strings"
 
+	"fmt"
 	"strings"
 
-	"github.com/fatih/color"
 	"github.com/jesseduffield/gocui"
 	"github.com/jesseduffield/lazygit/pkg/commands"
+	"github.com/jesseduffield/lazygit/pkg/utils"
 )
 
-func (gui *Gui) stagedFiles() []commands.File {
+func (gui *Gui) stagedFiles() []*commands.File {
 	files := gui.State.Files
-	result := make([]commands.File, 0)
+	result := make([]*commands.File, 0)
 	for _, file := range files {
 		if file.HasStagedChanges {
 			result = append(result, file)
@@ -25,9 +26,9 @@ func (gui *Gui) stagedFiles() []commands.File {
 	return result
 }
 
-func (gui *Gui) trackedFiles() []commands.File {
+func (gui *Gui) trackedFiles() []*commands.File {
 	files := gui.State.Files
-	result := make([]commands.File, 0)
+	result := make([]*commands.File, 0)
 	for _, file := range files {
 		if file.Tracked {
 			result = append(result, file)
@@ -116,9 +117,9 @@ func (gui *Gui) handleAddPatch(g *gocui.Gui, v *gocui.View) error {
 	return gui.Errors.ErrSubProcess
 }
 
-func (gui *Gui) getSelectedFile(g *gocui.Gui) (commands.File, error) {
+func (gui *Gui) getSelectedFile(g *gocui.Gui) (*commands.File, error) {
 	if len(gui.State.Files) == 0 {
-		return commands.File{}, gui.Errors.ErrNoFiles
+		return &commands.File{}, gui.Errors.ErrNoFiles
 	}
 	filesView, err := g.View("files")
 	if err != nil {
@@ -184,7 +185,9 @@ func (gui *Gui) handleFileSelect(g *gocui.Gui, v *gocui.View) error {
 		gui.renderString(g, "main", gui.Tr.SLocalize("NoChangedFiles"))
 		return gui.renderfilesOptions(g, nil)
 	}
-	gui.renderfilesOptions(g, &file)
+	if err := gui.renderfilesOptions(g, file); err != nil {
+		return err
+	}
 	var content string
 	if file.HasMergeConflicts {
 		return gui.refreshMergePanel(g)
@@ -275,24 +278,6 @@ func (gui *Gui) updateHasMergeConflictStatus() error {
 	return nil
 }
 
-func (gui *Gui) renderFile(file commands.File, filesView *gocui.View) {
-	// potentially inefficient to be instantiating these color
-	// objects with each render
-	red := color.New(color.FgRed)
-	green := color.New(color.FgGreen)
-	if !file.Tracked && !file.HasStagedChanges {
-		red.Fprintln(filesView, file.DisplayString)
-		return
-	}
-	green.Fprint(filesView, file.DisplayString[0:1])
-	red.Fprint(filesView, file.DisplayString[1:3])
-	if file.HasUnstagedChanges {
-		red.Fprintln(filesView, file.Name)
-	} else {
-		green.Fprintln(filesView, file.Name)
-	}
-}
-
 func (gui *Gui) catSelectedFile(g *gocui.Gui) (string, error) {
 	item, err := gui.getSelectedFile(g)
 	if err != nil {
@@ -318,10 +303,14 @@ func (gui *Gui) refreshFiles(g *gocui.Gui) error {
 		return err
 	}
 	gui.refreshStateFiles()
+
 	filesView.Clear()
-	for _, file := range gui.State.Files {
-		gui.renderFile(file, filesView)
+	list, err := utils.RenderList(gui.State.Files)
+	if err != nil {
+		return err
 	}
+	fmt.Fprint(filesView, list)
+
 	gui.correctCursor(filesView)
 	if filesView == g.CurrentView() {
 		gui.handleFileSelect(g, filesView)
diff --git a/pkg/gui/gui.go b/pkg/gui/gui.go
index e3cc61529..8a2aaea81 100644
--- a/pkg/gui/gui.go
+++ b/pkg/gui/gui.go
@@ -71,10 +71,10 @@ type Gui struct {
 }
 
 type guiState struct {
-	Files             []commands.File
-	Branches          []commands.Branch
-	Commits           []commands.Commit
-	StashEntries      []commands.StashEntry
+	Files             []*commands.File
+	Branches          []*commands.Branch
+	Commits           []*commands.Commit
+	StashEntries      []*commands.StashEntry
 	PreviousView      string
 	HasMergeConflicts bool
 	ConflictIndex     int
@@ -83,17 +83,16 @@ type guiState struct {
 	EditHistory       *stack.Stack
 	Platform          commands.Platform
 	Updating          bool
-	Keys              []Binding
 }
 
 // NewGui builds a new gui handler
 func NewGui(log *logrus.Entry, gitCommand *commands.GitCommand, oSCommand *commands.OSCommand, tr *i18n.Localizer, config config.AppConfigurer, updater *updates.Updater) (*Gui, error) {
 
 	initialState := guiState{
-		Files:         make([]commands.File, 0),
+		Files:         make([]*commands.File, 0),
 		PreviousView:  "files",
-		Commits:       make([]commands.Commit, 0),
-		StashEntries:  make([]commands.StashEntry, 0),
+		Commits:       make([]*commands.Commit, 0),
+		StashEntries:  make([]*commands.StashEntry, 0),
 		ConflictIndex: 0,
 		ConflictTop:   true,
 		Conflicts:     make([]commands.Conflict, 0),
diff --git a/pkg/gui/keybindings.go b/pkg/gui/keybindings.go
index c8ed7d3c8..72a187472 100644
--- a/pkg/gui/keybindings.go
+++ b/pkg/gui/keybindings.go
@@ -1,6 +1,8 @@
 package gui
 
-import "github.com/jesseduffield/gocui"
+import (
+	"github.com/jesseduffield/gocui"
+)
 
 // Binding - a keybinding mapping a key and modifier to a handler. The keypress
 // is only handled if the given view has focus, or handled globally if the view
@@ -14,8 +16,26 @@ type Binding struct {
 	Description string
 }
 
-func (gui *Gui) GetKeybindings() []Binding {
-	bindings := []Binding{
+// GetDisplayStrings returns the display string of a file
+func (b *Binding) GetDisplayStrings() []string {
+	return []string{b.GetKey(), b.Description}
+}
+
+func (b *Binding) GetKey() string {
+	r, ok := b.Key.(rune)
+	key := ""
+
+	if ok {
+		key = string(r)
+	} else if b.KeyReadable != "" {
+		key = b.KeyReadable
+	}
+
+	return key
+}
+
+func (gui *Gui) GetKeybindings() []*Binding {
+	bindings := []*Binding{
 		{
 			ViewName: "",
 			Key:      'q',
@@ -73,7 +93,7 @@ func (gui *Gui) GetKeybindings() []Binding {
 			ViewName: "",
 			Key:      'x',
 			Modifier: gocui.ModNone,
-			Handler:  gui.handleMenu,
+			Handler:  gui.handleCreateOptionsMenu,
 		}, {
 			ViewName:    "status",
 			Key:         'e',
@@ -349,18 +369,13 @@ func (gui *Gui) GetKeybindings() []Binding {
 			Key:      'q',
 			Modifier: gocui.ModNone,
 			Handler:  gui.handleMenuClose,
-		}, {
-			ViewName: "menu",
-			Key:      gocui.KeySpace,
-			Modifier: gocui.ModNone,
-			Handler:  gui.handleMenuPress,
 		},
 	}
 
 	// Would make these keybindings global but that interferes with editing
 	// input in the confirmation panel
 	for _, viewName := range []string{"status", "files", "branches", "commits", "stash", "menu"} {
-		bindings = append(bindings, []Binding{
+		bindings = append(bindings, []*Binding{
 			{ViewName: viewName, Key: gocui.KeyTab, Modifier: gocui.ModNone, Handler: gui.nextView},
 			{ViewName: viewName, Key: gocui.KeyArrowLeft, Modifier: gocui.ModNone, Handler: gui.previousView},
 			{ViewName: viewName, Key: gocui.KeyArrowRight, Modifier: gocui.ModNone, Handler: gui.nextView},
diff --git a/pkg/gui/menu_panel.go b/pkg/gui/menu_panel.go
index 68041390d..753e8f84d 100644
--- a/pkg/gui/menu_panel.go
+++ b/pkg/gui/menu_panel.go
@@ -8,21 +8,6 @@ import (
 	"github.com/jesseduffield/lazygit/pkg/utils"
 )
 
-func (gui *Gui) handleMenuPress(g *gocui.Gui, v *gocui.View) error {
-	lineNumber := gui.getItemPosition(v)
-	if gui.State.Keys[lineNumber].Key == nil {
-		return nil
-	}
-	if len(gui.State.Keys) > lineNumber {
-		err := gui.handleMenuClose(g, v)
-		if err != nil {
-			return err
-		}
-		return gui.State.Keys[lineNumber].Handler(g, v)
-	}
-	return nil
-}
-
 func (gui *Gui) handleMenuSelect(g *gocui.Gui, v *gocui.View) error {
 	// doing nothing for now
 	// but it is needed for switch in newLineFocused
@@ -39,9 +24,9 @@ func (gui *Gui) renderMenuOptions(g *gocui.Gui) error {
 }
 
 func (gui *Gui) handleMenuClose(g *gocui.Gui, v *gocui.View) error {
-	// better to delete because for example after closing update confirmation panel,
-	// the focus isn't set back to any of panels and one is unable to even quit
-	//_, err := g.SetViewOnBottom(v.Name())
+	if err := g.DeleteKeybinding("menu", gocui.KeySpace, gocui.ModNone); err != nil {
+		return err
+	}
 	err := g.DeleteView("menu")
 	if err != nil {
 		return err
@@ -49,83 +34,38 @@ func (gui *Gui) handleMenuClose(g *gocui.Gui, v *gocui.View) error {
 	return gui.returnFocus(g, v)
 }
 
-func (gui *Gui) GetKey(binding Binding) string {
-	r, ok := binding.Key.(rune)
-	key := ""
-
-	if ok {
-		key = string(r)
-	} else if binding.KeyReadable != "" {
-		key = binding.KeyReadable
-	}
-
-	return key
-}
-
-func (gui *Gui) GetMaxKeyLength(bindings []Binding) int {
-	max := 0
-	for _, binding := range bindings {
-		keyLength := len(gui.GetKey(binding))
-		if keyLength > max {
-			max = keyLength
-		}
-	}
-	return max
-}
-
-func (gui *Gui) handleMenu(g *gocui.Gui, v *gocui.View) error {
-	var (
-		contentGlobal, contentPanel   []string
-		bindingsGlobal, bindingsPanel []Binding
-	)
-	// clear keys slice, so we don't have ghost elements
-	gui.State.Keys = gui.State.Keys[:0]
-	bindings := gui.GetKeybindings()
-	padWidth := gui.GetMaxKeyLength(bindings)
-
-	for _, binding := range bindings {
-		key := gui.GetKey(binding)
-		if key != "" && binding.Description != "" {
-			content := fmt.Sprintf("%s  %s", utils.WithPadding(key, padWidth), binding.Description)
-			switch binding.ViewName {
-			case "":
-				contentGlobal = append(contentGlobal, content)
-				bindingsGlobal = append(bindingsGlobal, binding)
-			case v.Name():
-				contentPanel = append(contentPanel, content)
-				bindingsPanel = append(bindingsPanel, binding)
-			}
-		}
-	}
-
-	// append dummy element to have a separator between
-	// panel and global keybindings
-	contentPanel = append(contentPanel, "")
-	bindingsPanel = append(bindingsPanel, Binding{})
-
-	content := append(contentPanel, contentGlobal...)
-	gui.State.Keys = append(bindingsPanel, bindingsGlobal...)
-	// append newline at the end so the last line would be selectable
-	contentJoined := strings.Join(content, "\n") + "\n"
-
-	// y1-1 so there will not be an extra space at the end of panel
-	x0, y0, x1, y1 := gui.getConfirmationPanelDimensions(g, contentJoined)
-	menuView, _ := g.SetView("menu", x0, y0, x1, y1-1, 0)
-	menuView.Title = strings.Title(gui.Tr.SLocalize("menu"))
-	menuView.FgColor = gocui.ColorWhite
-
-	if err := gui.renderMenuOptions(g); err != nil {
+func (gui *Gui) createMenu(items interface{}, handlePress func(int) error) error {
+	list, err := utils.RenderList(items)
+	if err != nil {
 		return err
 	}
 
-	fmt.Fprint(menuView, contentJoined)
+	x0, y0, x1, y1 := gui.getConfirmationPanelDimensions(gui.g, list)
+	menuView, _ := gui.g.SetView("menu", x0, y0, x1, y1, 0)
+	menuView.Title = strings.Title(gui.Tr.SLocalize("menu"))
+	menuView.FgColor = gocui.ColorWhite
+	menuView.Clear()
+	fmt.Fprint(menuView, list)
 
-	g.Update(func(g *gocui.Gui) error {
-		_, err := g.SetViewOnTop("menu")
-		if err != nil {
+	if err := gui.renderMenuOptions(gui.g); err != nil {
+		return err
+	}
+
+	wrappedHandlePress := func(g *gocui.Gui, v *gocui.View) error {
+		lineNumber := gui.getItemPosition(v)
+		return handlePress(lineNumber)
+	}
+
+	if err := gui.g.SetKeybinding("menu", gocui.KeySpace, gocui.ModNone, wrappedHandlePress); err != nil {
+		return err
+	}
+
+	gui.g.Update(func(g *gocui.Gui) error {
+		if _, err := g.SetViewOnTop("menu"); err != nil {
 			return err
 		}
-		return gui.switchFocus(g, v, menuView)
+		currentView := gui.g.CurrentView()
+		return gui.switchFocus(gui.g, currentView, menuView)
 	})
 	return nil
 }
diff --git a/pkg/gui/options_menu_panel.go b/pkg/gui/options_menu_panel.go
new file mode 100644
index 000000000..2da002b43
--- /dev/null
+++ b/pkg/gui/options_menu_panel.go
@@ -0,0 +1,51 @@
+package gui
+
+import (
+	"errors"
+
+	"github.com/jesseduffield/gocui"
+)
+
+func (gui *Gui) getBindings(v *gocui.View) []*Binding {
+	var (
+		bindingsGlobal, bindingsPanel []*Binding
+	)
+
+	bindings := gui.GetKeybindings()
+
+	for _, binding := range bindings {
+		if binding.GetKey() != "" && binding.Description != "" {
+			switch binding.ViewName {
+			case "":
+				bindingsGlobal = append(bindingsGlobal, binding)
+			case v.Name():
+				bindingsPanel = append(bindingsPanel, binding)
+			}
+		}
+	}
+
+	// append dummy element to have a separator between
+	// panel and global keybindings
+	bindingsPanel = append(bindingsPanel, &Binding{})
+	return append(bindingsPanel, bindingsGlobal...)
+}
+
+func (gui *Gui) handleCreateOptionsMenu(g *gocui.Gui, v *gocui.View) error {
+	bindings := gui.getBindings(v)
+
+	handleOptionsMenuPress := func(index int) error {
+		if bindings[index].Key == nil {
+			return nil
+		}
+		if index <= len(bindings) {
+			return errors.New("Index is greater than size of bindings")
+		}
+		err := gui.handleMenuClose(g, v)
+		if err != nil {
+			return err
+		}
+		return bindings[index].Handler(g, v)
+	}
+
+	return gui.createMenu(bindings, handleOptionsMenuPress)
+}
diff --git a/pkg/gui/stash_panel.go b/pkg/gui/stash_panel.go
index 9ca07717b..62b4efda7 100644
--- a/pkg/gui/stash_panel.go
+++ b/pkg/gui/stash_panel.go
@@ -5,6 +5,7 @@ import (
 
 	"github.com/jesseduffield/gocui"
 	"github.com/jesseduffield/lazygit/pkg/commands"
+	"github.com/jesseduffield/lazygit/pkg/utils"
 )
 
 func (gui *Gui) refreshStashEntries(g *gocui.Gui) error {
@@ -14,10 +15,14 @@ func (gui *Gui) refreshStashEntries(g *gocui.Gui) error {
 			panic(err)
 		}
 		gui.State.StashEntries = gui.GitCommand.GetStashEntries()
+
 		v.Clear()
-		for _, stashEntry := range gui.State.StashEntries {
-			fmt.Fprintln(v, stashEntry.DisplayString)
+		list, err := utils.RenderList(gui.State.StashEntries)
+		if err != nil {
+			return err
 		}
+		fmt.Fprint(v, list)
+
 		return gui.resetOrigin(v)
 	})
 	return nil
@@ -29,7 +34,7 @@ func (gui *Gui) getSelectedStashEntry(v *gocui.View) *commands.StashEntry {
 	}
 	stashView, _ := gui.g.View("stash")
 	lineNumber := gui.getItemPosition(stashView)
-	return &gui.State.StashEntries[lineNumber]
+	return gui.State.StashEntries[lineNumber]
 }
 
 func (gui *Gui) renderStashOptions(g *gocui.Gui) error {
diff --git a/pkg/gui/view_helpers.go b/pkg/gui/view_helpers.go
index 7a5bd8874..5178bd4d9 100644
--- a/pkg/gui/view_helpers.go
+++ b/pkg/gui/view_helpers.go
@@ -160,7 +160,6 @@ func (gui *Gui) getItemPosition(v *gocui.View) int {
 
 func (gui *Gui) cursorUp(g *gocui.Gui, v *gocui.View) error {
 	// swallowing cursor movements in main
-	// TODO: pull this out
 	if v == nil || v.Name() == "main" {
 		return nil
 	}
@@ -179,19 +178,28 @@ func (gui *Gui) cursorUp(g *gocui.Gui, v *gocui.View) error {
 
 func (gui *Gui) cursorDown(g *gocui.Gui, v *gocui.View) error {
 	// swallowing cursor movements in main
-	// TODO: pull this out
 	if v == nil || v.Name() == "main" {
 		return nil
 	}
 	cx, cy := v.Cursor()
 	ox, oy := v.Origin()
-	if cy+oy >= len(v.BufferLines())-2 {
+	ly := len(v.BufferLines()) - 1
+	_, height := v.Size()
+	maxY := height - 1
+
+	// if we are at the end we just return
+	if cy+oy == ly {
 		return nil
 	}
-	if err := v.SetCursor(cx, cy+1); err != nil {
-		if err := v.SetOrigin(ox, oy+1); err != nil {
-			return err
-		}
+
+	var err error
+	if cy < maxY {
+		err = v.SetCursor(cx, cy+1)
+	} else {
+		err = v.SetOrigin(ox, oy+1)
+	}
+	if err != nil {
+		return err
 	}
 
 	gui.newLineFocused(g, v)
@@ -208,10 +216,19 @@ func (gui *Gui) resetOrigin(v *gocui.View) error {
 // if the cursor down past the last item, move it to the last line
 func (gui *Gui) correctCursor(v *gocui.View) error {
 	cx, cy := v.Cursor()
-	_, oy := v.Origin()
-	lineCount := len(v.BufferLines()) - 2
-	if cy >= lineCount-oy {
-		return v.SetCursor(cx, lineCount-oy)
+	ox, oy := v.Origin()
+	_, height := v.Size()
+	maxY := height - 1
+	ly := len(v.BufferLines()) - 1
+	if oy+cy <= ly {
+		return nil
+	}
+	newCy := utils.Min(ly, maxY)
+	if err := v.SetCursor(cx, newCy); err != nil {
+		return err
+	}
+	if err := v.SetOrigin(ox, ly-newCy); err != nil {
+		return err
 	}
 	return nil
 }
diff --git a/pkg/utils/utils.go b/pkg/utils/utils.go
index e28ab1824..aef653c50 100644
--- a/pkg/utils/utils.go
+++ b/pkg/utils/utils.go
@@ -1,10 +1,12 @@
 package utils
 
 import (
+	"errors"
 	"fmt"
 	"log"
 	"os"
 	"path/filepath"
+	"reflect"
 	"strings"
 	"time"
 
@@ -99,3 +101,106 @@ func ResolvePlaceholderString(str string, arguments map[string]string) string {
 	}
 	return str
 }
+
+// Min returns the minimum of two integers
+func Min(x, y int) int {
+	if x < y {
+		return x
+	}
+	return y
+}
+
+type Displayable interface {
+	GetDisplayStrings() []string
+}
+
+// RenderList takes a slice of items, confirms they implement the Displayable
+// interface, then generates a list of their displaystrings to write to a panel's
+// buffer
+func RenderList(slice interface{}) (string, error) {
+	s := reflect.ValueOf(slice)
+	if s.Kind() != reflect.Slice {
+		return "", errors.New("RenderList given a non-slice type")
+	}
+
+	displayables := make([]Displayable, s.Len())
+
+	for i := 0; i < s.Len(); i++ {
+		value, ok := s.Index(i).Interface().(Displayable)
+		if !ok {
+			return "", errors.New("item does not implement the Displayable interface")
+		}
+		displayables[i] = value
+	}
+
+	return renderDisplayableList(displayables)
+}
+
+// renderDisplayableList takes a list of displayable items, obtains their display
+// strings via GetDisplayStrings() and then returns a single string containing
+// each item's string representation on its own line, with appropriate horizontal
+// padding between the item's own strings
+func renderDisplayableList(items []Displayable) (string, error) {
+	if len(items) == 0 {
+		return "", nil
+	}
+
+	stringArrays := getDisplayStringArrays(items)
+
+	if !displayArraysAligned(stringArrays) {
+		return "", errors.New("Each item must return the same number of strings to display")
+	}
+
+	padWidths := getPadWidths(stringArrays)
+	paddedDisplayStrings := getPaddedDisplayStrings(stringArrays, padWidths)
+
+	return strings.Join(paddedDisplayStrings, "\n"), nil
+}
+
+func getPadWidths(stringArrays [][]string) []int {
+	if len(stringArrays[0]) <= 1 {
+		return []int{}
+	}
+	padWidths := make([]int, len(stringArrays[0])-1)
+	for i := range padWidths {
+		for _, strings := range stringArrays {
+			if len(strings[i]) > padWidths[i] {
+				padWidths[i] = len(strings[i])
+			}
+		}
+	}
+	return padWidths
+}
+
+func getPaddedDisplayStrings(stringArrays [][]string, padWidths []int) []string {
+	paddedDisplayStrings := make([]string, len(stringArrays))
+	for i, stringArray := range stringArrays {
+		if len(stringArray) == 0 {
+			continue
+		}
+		for j, padWidth := range padWidths {
+			paddedDisplayStrings[i] += WithPadding(stringArray[j], padWidth) + " "
+		}
+		paddedDisplayStrings[i] += stringArray[len(padWidths)]
+	}
+	return paddedDisplayStrings
+}
+
+// displayArraysAligned returns true if every string array returned from our
+// list of displayables has the same length
+func displayArraysAligned(stringArrays [][]string) bool {
+	for _, strings := range stringArrays {
+		if len(strings) != len(stringArrays[0]) {
+			return false
+		}
+	}
+	return true
+}
+
+func getDisplayStringArrays(displayables []Displayable) [][]string {
+	stringArrays := make([][]string, len(displayables))
+	for i, item := range displayables {
+		stringArrays[i] = item.GetDisplayStrings()
+	}
+	return stringArrays
+}
diff --git a/pkg/utils/utils_test.go b/pkg/utils/utils_test.go
index 0b2d35959..918031512 100644
--- a/pkg/utils/utils_test.go
+++ b/pkg/utils/utils_test.go
@@ -1,6 +1,7 @@
 package utils
 
 import (
+	"errors"
 	"testing"
 
 	"github.com/stretchr/testify/assert"
@@ -167,3 +168,211 @@ func TestResolvePlaceholderString(t *testing.T) {
 		assert.EqualValues(t, string(s.expected), ResolvePlaceholderString(s.templateString, s.arguments))
 	}
 }
+
+func TestDisplayArraysAligned(t *testing.T) {
+	type scenario struct {
+		input    [][]string
+		expected bool
+	}
+
+	scenarios := []scenario{
+		{
+			[][]string{{"", ""}, {"", ""}},
+			true,
+		},
+		{
+			[][]string{{""}, {"", ""}},
+			false,
+		},
+	}
+
+	for _, s := range scenarios {
+		assert.EqualValues(t, s.expected, displayArraysAligned(s.input))
+	}
+}
+
+type myDisplayable struct {
+	strings []string
+}
+
+type myStruct struct{}
+
+func (d *myDisplayable) GetDisplayStrings() []string {
+	return d.strings
+}
+
+func TestGetDisplayStringArrays(t *testing.T) {
+	type scenario struct {
+		input    []Displayable
+		expected [][]string
+	}
+
+	scenarios := []scenario{
+		{
+			[]Displayable{
+				Displayable(&myDisplayable{[]string{"a", "b"}}),
+				Displayable(&myDisplayable{[]string{"c", "d"}}),
+			},
+			[][]string{{"a", "b"}, {"c", "d"}},
+		},
+	}
+
+	for _, s := range scenarios {
+		assert.EqualValues(t, s.expected, getDisplayStringArrays(s.input))
+	}
+}
+
+func TestRenderDisplayableList(t *testing.T) {
+	type scenario struct {
+		input          []Displayable
+		expectedString string
+		expectedError  error
+	}
+
+	scenarios := []scenario{
+		{
+			[]Displayable{
+				Displayable(&myDisplayable{[]string{}}),
+				Displayable(&myDisplayable{[]string{}}),
+			},
+			"\n",
+			nil,
+		},
+		{
+			[]Displayable{
+				Displayable(&myDisplayable{[]string{"aa", "b"}}),
+				Displayable(&myDisplayable{[]string{"c", "d"}}),
+			},
+			"aa b\nc  d",
+			nil,
+		},
+		{
+			[]Displayable{
+				Displayable(&myDisplayable{[]string{"a"}}),
+				Displayable(&myDisplayable{[]string{"b", "c"}}),
+			},
+			"",
+			errors.New("Each item must return the same number of strings to display"),
+		},
+	}
+
+	for _, s := range scenarios {
+		str, err := renderDisplayableList(s.input)
+		assert.EqualValues(t, s.expectedString, str)
+		assert.EqualValues(t, s.expectedError, err)
+	}
+}
+
+func TestRenderList(t *testing.T) {
+	type scenario struct {
+		input          interface{}
+		expectedString string
+		expectedError  error
+	}
+
+	scenarios := []scenario{
+		{
+			[]*myDisplayable{
+				{[]string{"aa", "b"}},
+				{[]string{"c", "d"}},
+			},
+			"aa b\nc  d",
+			nil,
+		},
+		{
+			[]*myStruct{
+				{},
+				{},
+			},
+			"",
+			errors.New("item does not implement the Displayable interface"),
+		},
+		{
+			&myStruct{},
+			"",
+			errors.New("RenderList given a non-slice type"),
+		},
+	}
+
+	for _, s := range scenarios {
+		str, err := RenderList(s.input)
+		assert.EqualValues(t, s.expectedString, str)
+		assert.EqualValues(t, s.expectedError, err)
+	}
+}
+
+func TestGetPaddedDisplayStrings(t *testing.T) {
+	type scenario struct {
+		stringArrays [][]string
+		padWidths    []int
+		expected     []string
+	}
+
+	scenarios := []scenario{
+		{
+			[][]string{{"a", "b"}, {"c", "d"}},
+			[]int{1},
+			[]string{"a b", "c d"},
+		},
+	}
+
+	for _, s := range scenarios {
+		assert.EqualValues(t, s.expected, getPaddedDisplayStrings(s.stringArrays, s.padWidths))
+	}
+}
+
+func TestGetPadWidths(t *testing.T) {
+	type scenario struct {
+		stringArrays [][]string
+		expected     []int
+	}
+
+	scenarios := []scenario{
+		{
+			[][]string{{""}, {""}},
+			[]int{},
+		},
+		{
+			[][]string{{"a"}, {""}},
+			[]int{},
+		},
+		{
+			[][]string{{"aa", "b", "ccc"}, {"c", "d", "e"}},
+			[]int{2, 1},
+		},
+	}
+
+	for _, s := range scenarios {
+		assert.EqualValues(t, s.expected, getPadWidths(s.stringArrays))
+	}
+}
+
+func TestMin(t *testing.T) {
+	type scenario struct {
+		a        int
+		b        int
+		expected int
+	}
+
+	scenarios := []scenario{
+		{
+			1,
+			1,
+			1,
+		},
+		{
+			1,
+			2,
+			1,
+		},
+		{
+			2,
+			1,
+			1,
+		},
+	}
+
+	for _, s := range scenarios {
+		assert.EqualValues(t, s.expected, Min(s.a, s.b))
+	}
+}