1
0
mirror of https://github.com/jesseduffield/lazygit.git synced 2025-06-02 23:27:32 +02:00

Merge pull request #280 from jesseduffield/hotfix/cursor-positioning

Fix cursor positioning bugs
This commit is contained in:
Jesse Duffield 2018-09-19 19:55:14 +10:00 committed by GitHub
commit 3072c93e13
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
19 changed files with 643 additions and 264 deletions

View File

@ -14,9 +14,9 @@ type Branch struct {
Recency string Recency string
} }
// GetDisplayString returns the dispaly string of branch // GetDisplayStrings returns the dispaly string of branch
func (b *Branch) GetDisplayString() string { func (b *Branch) GetDisplayStrings() []string {
return utils.WithPadding(b.Recency, 4) + utils.ColoredString(b.Name, b.GetColor()) return []string{b.Recency, utils.ColoredString(b.Name, b.GetColor())}
} }
// GetColor branch color // GetColor branch color

26
pkg/commands/commit.go Normal file
View File

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

36
pkg/commands/file.go Normal file
View File

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

View File

@ -105,17 +105,17 @@ func NewGitCommand(log *logrus.Entry, osCommand *OSCommand, tr *i18n.Localizer)
} }
// GetStashEntries stash entryies // GetStashEntries stash entryies
func (c *GitCommand) GetStashEntries() []StashEntry { func (c *GitCommand) GetStashEntries() []*StashEntry {
rawString, _ := c.OSCommand.RunCommandWithOutput("git stash list --pretty='%gs'") rawString, _ := c.OSCommand.RunCommandWithOutput("git stash list --pretty='%gs'")
stashEntries := []StashEntry{} stashEntries := []*StashEntry{}
for i, line := range utils.SplitLines(rawString) { for i, line := range utils.SplitLines(rawString) {
stashEntries = append(stashEntries, stashEntryFromLine(line, i)) stashEntries = append(stashEntries, stashEntryFromLine(line, i))
} }
return stashEntries return stashEntries
} }
func stashEntryFromLine(line string, index int) StashEntry { func stashEntryFromLine(line string, index int) *StashEntry {
return StashEntry{ return &StashEntry{
Name: line, Name: line,
Index: index, Index: index,
DisplayString: line, DisplayString: line,
@ -128,10 +128,10 @@ func (c *GitCommand) GetStashEntryDiff(index int) (string, error) {
} }
// GetStatusFiles git status files // GetStatusFiles git status files
func (c *GitCommand) GetStatusFiles() []File { func (c *GitCommand) GetStatusFiles() []*File {
statusOutput, _ := c.GitStatus() statusOutput, _ := c.GitStatus()
statusStrings := utils.SplitLines(statusOutput) statusStrings := utils.SplitLines(statusOutput)
files := []File{} files := []*File{}
for _, statusString := range statusStrings { for _, statusString := range statusStrings {
change := statusString[0:2] change := statusString[0:2]
@ -141,7 +141,7 @@ func (c *GitCommand) GetStatusFiles() []File {
_, untracked := map[string]bool{"??": true, "A ": true, "AM": true}[change] _, untracked := map[string]bool{"??": true, "A ": true, "AM": true}[change]
_, hasNoStagedChanges := map[string]bool{" ": true, "U": true, "?": true}[stagedChange] _, hasNoStagedChanges := map[string]bool{" ": true, "U": true, "?": true}[stagedChange]
file := File{ file := &File{
Name: filename, Name: filename,
DisplayString: statusString, DisplayString: statusString,
HasStagedChanges: !hasNoStagedChanges, HasStagedChanges: !hasNoStagedChanges,
@ -169,7 +169,7 @@ func (c *GitCommand) StashSave(message string) error {
} }
// MergeStatusFiles merge status files // MergeStatusFiles merge status files
func (c *GitCommand) MergeStatusFiles(oldFiles, newFiles []File) []File { func (c *GitCommand) MergeStatusFiles(oldFiles, newFiles []*File) []*File {
if len(oldFiles) == 0 { if len(oldFiles) == 0 {
return newFiles return newFiles
} }
@ -177,7 +177,7 @@ func (c *GitCommand) MergeStatusFiles(oldFiles, newFiles []File) []File {
appendedIndexes := []int{} appendedIndexes := []int{}
// retain position of files we already could see // retain position of files we already could see
result := []File{} result := []*File{}
for _, oldFile := range oldFiles { for _, oldFile := range oldFiles {
for newIndex, newFile := range newFiles { for newIndex, newFile := range newFiles {
if oldFile.Name == newFile.Name { if oldFile.Name == newFile.Name {
@ -231,9 +231,9 @@ func (c *GitCommand) UpstreamDifferenceCount() (string, string) {
return strings.TrimSpace(pushableCount), strings.TrimSpace(pullableCount) 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 // 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{} pushables := map[string]bool{}
o, err := c.OSCommand.RunCommandWithOutput("git rev-list @{u}..head --abbrev-commit") o, err := c.OSCommand.RunCommandWithOutput("git rev-list @{u}..head --abbrev-commit")
if err != nil { if err != nil {
@ -413,7 +413,7 @@ func (c *GitCommand) IsInMergeState() (bool, error) {
} }
// RemoveFile directly // 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 the file isn't tracked, we assume you want to delete it
if file.HasStagedChanges { if file.HasStagedChanges {
if err := c.OSCommand.RunCommand(fmt.Sprintf("git reset -- %s", file.Name)); err != nil { 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 // GetCommits obtains the commits of the current branch
func (c *GitCommand) GetCommits() []Commit { func (c *GitCommand) GetCommits() []*Commit {
pushables := c.getCommitsToPush() pushables := c.GetCommitsToPush()
log := c.GetLog() log := c.GetLog()
commits := []Commit{} commits := []*Commit{}
// now we can split it up and turn it into commits // now we can split it up and turn it into commits
for _, line := range utils.SplitLines(log) { for _, line := range utils.SplitLines(log) {
splitLine := strings.Split(line, " ") splitLine := strings.Split(line, " ")
sha := splitLine[0] sha := splitLine[0]
_, pushed := pushables[sha] _, pushed := pushables[sha]
commits = append(commits, Commit{ commits = append(commits, &Commit{
Sha: sha, Sha: sha,
Name: strings.Join(splitLine[1:], " "), Name: strings.Join(splitLine[1:], " "),
Pushed: pushed, Pushed: pushed,
@ -508,7 +508,7 @@ func (c *GitCommand) Show(sha string) string {
} }
// Diff returns the diff of a file // Diff returns the diff of a file
func (c *GitCommand) Diff(file File) string { func (c *GitCommand) Diff(file *File) string {
cachedArg := "" cachedArg := ""
fileName := c.OSCommand.Quote(file.Name) fileName := c.OSCommand.Quote(file.Name)
if file.HasStagedChanges && !file.HasUnstagedChanges { if file.HasStagedChanges && !file.HasUnstagedChanges {

View File

@ -1,32 +1,5 @@
package commands 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 // Conflict : A git conflict with a start middle and end corresponding to line
// numbers in the file where the conflict bars appear // numbers in the file where the conflict bars appear
type Conflict struct { type Conflict struct {

View File

@ -276,7 +276,7 @@ func TestGitCommandGetStashEntries(t *testing.T) {
type scenario struct { type scenario struct {
testName string testName string
command func(string, ...string) *exec.Cmd command func(string, ...string) *exec.Cmd
test func([]StashEntry) test func([]*StashEntry)
} }
scenarios := []scenario{ scenarios := []scenario{
@ -285,7 +285,7 @@ func TestGitCommandGetStashEntries(t *testing.T) {
func(string, ...string) *exec.Cmd { func(string, ...string) *exec.Cmd {
return exec.Command("echo") return exec.Command("echo")
}, },
func(entries []StashEntry) { func(entries []*StashEntry) {
assert.Len(t, entries, 0) assert.Len(t, entries, 0)
}, },
}, },
@ -294,8 +294,8 @@ func TestGitCommandGetStashEntries(t *testing.T) {
func(string, ...string) *exec.Cmd { 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") return exec.Command("echo", "WIP on add-pkg-commands-test: 55c6af2 increase parallel build\nWIP on master: bb86a3f update github template")
}, },
func(entries []StashEntry) { func(entries []*StashEntry) {
expected := []StashEntry{ expected := []*StashEntry{
{ {
0, 0,
"WIP on add-pkg-commands-test: 55c6af2 increase parallel build", "WIP on add-pkg-commands-test: 55c6af2 increase parallel build",
@ -342,7 +342,7 @@ func TestGitCommandGetStatusFiles(t *testing.T) {
type scenario struct { type scenario struct {
testName string testName string
command func(string, ...string) *exec.Cmd command func(string, ...string) *exec.Cmd
test func([]File) test func([]*File)
} }
scenarios := []scenario{ scenarios := []scenario{
@ -351,7 +351,7 @@ func TestGitCommandGetStatusFiles(t *testing.T) {
func(cmd string, args ...string) *exec.Cmd { func(cmd string, args ...string) *exec.Cmd {
return exec.Command("echo") return exec.Command("echo")
}, },
func(files []File) { func(files []*File) {
assert.Len(t, files, 0) 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", "MM file1.txt\nA file3.txt\nAM file2.txt\n?? file4.txt",
) )
}, },
func(files []File) { func(files []*File) {
assert.Len(t, files, 4) assert.Len(t, files, 4)
expected := []File{ expected := []*File{
{ {
Name: "file1.txt", Name: "file1.txt",
HasStagedChanges: true, HasStagedChanges: true,
@ -464,22 +464,22 @@ func TestGitCommandCommitAmend(t *testing.T) {
func TestGitCommandMergeStatusFiles(t *testing.T) { func TestGitCommandMergeStatusFiles(t *testing.T) {
type scenario struct { type scenario struct {
testName string testName string
oldFiles []File oldFiles []*File
newFiles []File newFiles []*File
test func([]File) test func([]*File)
} }
scenarios := []scenario{ scenarios := []scenario{
{ {
"Old file and new file are the same", "Old file and new file are the same",
[]File{}, []*File{},
[]File{ []*File{
{ {
Name: "new_file.txt", Name: "new_file.txt",
}, },
}, },
func(files []File) { func(files []*File) {
expected := []File{ expected := []*File{
{ {
Name: "new_file.txt", Name: "new_file.txt",
}, },
@ -491,7 +491,7 @@ func TestGitCommandMergeStatusFiles(t *testing.T) {
}, },
{ {
"Several files to merge, with some identical", "Several files to merge, with some identical",
[]File{ []*File{
{ {
Name: "new_file1.txt", Name: "new_file1.txt",
}, },
@ -502,7 +502,7 @@ func TestGitCommandMergeStatusFiles(t *testing.T) {
Name: "new_file3.txt", Name: "new_file3.txt",
}, },
}, },
[]File{ []*File{
{ {
Name: "new_file4.txt", Name: "new_file4.txt",
}, },
@ -513,8 +513,8 @@ func TestGitCommandMergeStatusFiles(t *testing.T) {
Name: "new_file1.txt", Name: "new_file1.txt",
}, },
}, },
func(files []File) { func(files []*File) {
expected := []File{ expected := []*File{
{ {
Name: "new_file1.txt", Name: "new_file1.txt",
}, },
@ -631,7 +631,7 @@ func TestGitCommandGetCommitsToPush(t *testing.T) {
t.Run(s.testName, func(t *testing.T) { t.Run(s.testName, func(t *testing.T) {
gitCmd := newDummyGitCommand() gitCmd := newDummyGitCommand()
gitCmd.OSCommand.command = s.command gitCmd.OSCommand.command = s.command
s.test(gitCmd.getCommitsToPush()) s.test(gitCmd.GetCommitsToPush())
}) })
} }
} }
@ -1225,7 +1225,7 @@ func TestGitCommandRemoveFile(t *testing.T) {
testName string testName string
command func() (func(string, ...string) *exec.Cmd, *[][]string) command func() (func(string, ...string) *exec.Cmd, *[][]string)
test func(*[][]string, error) test func(*[][]string, error)
file File file *File
removeFile func(string) error removeFile func(string) error
} }
@ -1247,7 +1247,7 @@ func TestGitCommandRemoveFile(t *testing.T) {
{"reset", "--", "test"}, {"reset", "--", "test"},
}) })
}, },
File{ &File{
Name: "test", Name: "test",
HasStagedChanges: true, HasStagedChanges: true,
}, },
@ -1270,7 +1270,7 @@ func TestGitCommandRemoveFile(t *testing.T) {
assert.EqualError(t, err, "an error occurred when removing file") assert.EqualError(t, err, "an error occurred when removing file")
assert.Len(t, *cmdsCalled, 0) assert.Len(t, *cmdsCalled, 0)
}, },
File{ &File{
Name: "test", Name: "test",
Tracked: false, Tracked: false,
}, },
@ -1295,7 +1295,7 @@ func TestGitCommandRemoveFile(t *testing.T) {
{"checkout", "--", "test"}, {"checkout", "--", "test"},
}) })
}, },
File{ &File{
Name: "test", Name: "test",
Tracked: true, Tracked: true,
HasStagedChanges: false, HasStagedChanges: false,
@ -1321,7 +1321,7 @@ func TestGitCommandRemoveFile(t *testing.T) {
{"checkout", "--", "test"}, {"checkout", "--", "test"},
}) })
}, },
File{ &File{
Name: "test", Name: "test",
Tracked: true, Tracked: true,
HasStagedChanges: false, HasStagedChanges: false,
@ -1348,7 +1348,7 @@ func TestGitCommandRemoveFile(t *testing.T) {
{"checkout", "--", "test"}, {"checkout", "--", "test"},
}) })
}, },
File{ &File{
Name: "test", Name: "test",
Tracked: true, Tracked: true,
HasStagedChanges: true, HasStagedChanges: true,
@ -1374,7 +1374,7 @@ func TestGitCommandRemoveFile(t *testing.T) {
{"reset", "--", "test"}, {"reset", "--", "test"},
}) })
}, },
File{ &File{
Name: "test", Name: "test",
Tracked: false, Tracked: false,
HasStagedChanges: true, HasStagedChanges: true,
@ -1398,7 +1398,7 @@ func TestGitCommandRemoveFile(t *testing.T) {
assert.NoError(t, err) assert.NoError(t, err)
assert.Len(t, *cmdsCalled, 0) assert.Len(t, *cmdsCalled, 0)
}, },
File{ &File{
Name: "test", Name: "test",
Tracked: false, Tracked: false,
HasStagedChanges: false, HasStagedChanges: false,
@ -1484,7 +1484,7 @@ func TestGitCommandGetCommits(t *testing.T) {
type scenario struct { type scenario struct {
testName string testName string
command func(string, ...string) *exec.Cmd command func(string, ...string) *exec.Cmd
test func([]Commit) test func([]*Commit)
} }
scenarios := []scenario{ scenarios := []scenario{
@ -1504,7 +1504,7 @@ func TestGitCommandGetCommits(t *testing.T) {
return nil return nil
}, },
func(commits []Commit) { func(commits []*Commit) {
assert.Len(t, commits, 0) assert.Len(t, commits, 0)
}, },
}, },
@ -1524,9 +1524,9 @@ func TestGitCommandGetCommits(t *testing.T) {
return nil return nil
}, },
func(commits []Commit) { func(commits []*Commit) {
assert.Len(t, commits, 2) assert.Len(t, commits, 2)
assert.EqualValues(t, []Commit{ assert.EqualValues(t, []*Commit{
{ {
Sha: "8a2bb0e", Sha: "8a2bb0e",
Name: "commit 1", Name: "commit 1",
@ -1557,7 +1557,7 @@ func TestGitCommandDiff(t *testing.T) {
gitCommand := newDummyGitCommand() gitCommand := newDummyGitCommand()
assert.NoError(t, test.GenerateRepo("lots_of_diffs.sh")) assert.NoError(t, test.GenerateRepo("lots_of_diffs.sh"))
files := []File{ files := []*File{
{ {
Name: "deleted_staged", Name: "deleted_staged",
HasStagedChanges: false, HasStagedChanges: false,

View File

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

View File

@ -34,7 +34,7 @@ func NewBranchListBuilder(log *logrus.Entry, gitCommand *commands.GitCommand) (*
}, nil }, 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, // I used go-git for this, but that breaks if you've just done a git init,
// even though you're on 'master' // even though you're on 'master'
branchName, err := b.GitCommand.OSCommand.RunCommandWithOutput("git symbolic-ref --short HEAD") branchName, err := b.GitCommand.OSCommand.RunCommandWithOutput("git symbolic-ref --short HEAD")
@ -44,11 +44,11 @@ func (b *BranchListBuilder) obtainCurrentBranch() commands.Branch {
panic(err.Error()) 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 { func (b *BranchListBuilder) obtainReflogBranches() []*commands.Branch {
branches := make([]commands.Branch, 0) branches := make([]*commands.Branch, 0)
rawString, err := b.GitCommand.OSCommand.RunCommandWithOutput("git reflog -n100 --pretty='%cr|%gs' --grep-reflog='checkout: moving' HEAD") rawString, err := b.GitCommand.OSCommand.RunCommandWithOutput("git reflog -n100 --pretty='%cr|%gs' --grep-reflog='checkout: moving' HEAD")
if err != nil { if err != nil {
return branches return branches
@ -58,14 +58,14 @@ func (b *BranchListBuilder) obtainReflogBranches() []commands.Branch {
for _, line := range branchLines { for _, line := range branchLines {
timeNumber, timeUnit, branchName := branchInfoFromLine(line) timeNumber, timeUnit, branchName := branchInfoFromLine(line)
timeUnit = abbreviatedTimeUnit(timeUnit) timeUnit = abbreviatedTimeUnit(timeUnit)
branch := commands.Branch{Name: branchName, Recency: timeNumber + timeUnit} branch := &commands.Branch{Name: branchName, Recency: timeNumber + timeUnit}
branches = append(branches, branch) branches = append(branches, branch)
} }
return branches return branches
} }
func (b *BranchListBuilder) obtainSafeBranches() []commands.Branch { func (b *BranchListBuilder) obtainSafeBranches() []*commands.Branch {
branches := make([]commands.Branch, 0) branches := make([]*commands.Branch, 0)
bIter, err := b.GitCommand.Repo.Branches() bIter, err := b.GitCommand.Repo.Branches()
if err != nil { if err != nil {
@ -73,14 +73,14 @@ func (b *BranchListBuilder) obtainSafeBranches() []commands.Branch {
} }
err = bIter.ForEach(func(b *plumbing.Reference) error { err = bIter.ForEach(func(b *plumbing.Reference) error {
name := b.Name().Short() name := b.Name().Short()
branches = append(branches, commands.Branch{Name: name}) branches = append(branches, &commands.Branch{Name: name})
return nil return nil
}) })
return branches 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 { for _, newBranch := range newBranches {
if included == branchIncluded(newBranch.Name, existingBranches) { if included == branchIncluded(newBranch.Name, existingBranches) {
finalBranches = append(finalBranches, newBranch) finalBranches = append(finalBranches, newBranch)
@ -89,7 +89,7 @@ func (b *BranchListBuilder) appendNewBranches(finalBranches, newBranches, existi
return finalBranches return finalBranches
} }
func sanitisedReflogName(reflogBranch commands.Branch, safeBranches []commands.Branch) string { func sanitisedReflogName(reflogBranch *commands.Branch, safeBranches []*commands.Branch) string {
for _, safeBranch := range safeBranches { for _, safeBranch := range safeBranches {
if strings.ToLower(safeBranch.Name) == strings.ToLower(reflogBranch.Name) { if strings.ToLower(safeBranch.Name) == strings.ToLower(reflogBranch.Name) {
return safeBranch.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 // Build the list of branches for the current repo
func (b *BranchListBuilder) Build() []commands.Branch { func (b *BranchListBuilder) Build() []*commands.Branch {
branches := make([]commands.Branch, 0) branches := make([]*commands.Branch, 0)
head := b.obtainCurrentBranch() head := b.obtainCurrentBranch()
safeBranches := b.obtainSafeBranches() safeBranches := b.obtainSafeBranches()
if len(safeBranches) == 0 { if len(safeBranches) == 0 {
return append(branches, head) return append(branches, head)
} }
reflogBranches := b.obtainReflogBranches() reflogBranches := b.obtainReflogBranches()
reflogBranches = uniqueByName(append([]commands.Branch{head}, reflogBranches...)) reflogBranches = uniqueByName(append([]*commands.Branch{head}, reflogBranches...))
for i, reflogBranch := range reflogBranches { for i, reflogBranch := range reflogBranches {
reflogBranches[i].Name = sanitisedReflogName(reflogBranch, safeBranches) reflogBranches[i].Name = sanitisedReflogName(reflogBranch, safeBranches)
} }
@ -118,7 +118,7 @@ func (b *BranchListBuilder) Build() []commands.Branch {
return branches return branches
} }
func branchIncluded(branchName string, branches []commands.Branch) bool { func branchIncluded(branchName string, branches []*commands.Branch) bool {
for _, existingBranch := range branches { for _, existingBranch := range branches {
if strings.ToLower(existingBranch.Name) == strings.ToLower(branchName) { if strings.ToLower(existingBranch.Name) == strings.ToLower(branchName) {
return true return true
@ -127,8 +127,8 @@ func branchIncluded(branchName string, branches []commands.Branch) bool {
return false return false
} }
func uniqueByName(branches []commands.Branch) []commands.Branch { func uniqueByName(branches []*commands.Branch) []*commands.Branch {
finalBranches := make([]commands.Branch, 0) finalBranches := make([]*commands.Branch, 0)
for _, branch := range branches { for _, branch := range branches {
if branchIncluded(branch.Name, finalBranches) { if branchIncluded(branch.Name, finalBranches) {
continue continue

View File

@ -7,6 +7,7 @@ import (
"github.com/jesseduffield/gocui" "github.com/jesseduffield/gocui"
"github.com/jesseduffield/lazygit/pkg/commands" "github.com/jesseduffield/lazygit/pkg/commands"
"github.com/jesseduffield/lazygit/pkg/git" "github.com/jesseduffield/lazygit/pkg/git"
"github.com/jesseduffield/lazygit/pkg/utils"
) )
func (gui *Gui) handleBranchPress(g *gocui.Gui, v *gocui.View) error { 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 return nil
} }
func (gui *Gui) getSelectedBranch(v *gocui.View) commands.Branch { func (gui *Gui) getSelectedBranch(v *gocui.View) *commands.Branch {
lineNumber := gui.getItemPosition(v) lineNumber := gui.getItemPosition(v)
return gui.State.Branches[lineNumber] return gui.State.Branches[lineNumber]
} }
@ -151,10 +152,15 @@ func (gui *Gui) refreshBranches(g *gocui.Gui) error {
return err return err
} }
gui.State.Branches = builder.Build() gui.State.Branches = builder.Build()
v.Clear() v.Clear()
for _, branch := range gui.State.Branches { list, err := utils.RenderList(gui.State.Branches)
fmt.Fprintln(v, branch.GetDisplayString()) if err != nil {
return err
} }
fmt.Fprint(v, list)
gui.resetOrigin(v) gui.resetOrigin(v)
return gui.refreshStatus(g) return gui.refreshStatus(g)
}) })

View File

@ -2,10 +2,11 @@ package gui
import ( import (
"errors" "errors"
"fmt"
"github.com/fatih/color"
"github.com/jesseduffield/gocui" "github.com/jesseduffield/gocui"
"github.com/jesseduffield/lazygit/pkg/commands" "github.com/jesseduffield/lazygit/pkg/commands"
"github.com/jesseduffield/lazygit/pkg/utils"
) )
func (gui *Gui) refreshCommits(g *gocui.Gui) error { func (gui *Gui) refreshCommits(g *gocui.Gui) error {
@ -15,20 +16,14 @@ func (gui *Gui) refreshCommits(g *gocui.Gui) error {
if err != nil { if err != nil {
panic(err) panic(err)
} }
v.Clear() v.Clear()
red := color.New(color.FgRed) list, err := utils.RenderList(gui.State.Commits)
yellow := color.New(color.FgYellow) if err != nil {
white := color.New(color.FgWhite) return err
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)
} }
fmt.Fprint(v, list)
gui.refreshStatus(g) gui.refreshStatus(g)
if g.CurrentView().Name() == "commits" { if g.CurrentView().Name() == "commits" {
gui.handleCommitSelect(g, v) gui.handleCommitSelect(g, v)
@ -99,7 +94,7 @@ func (gui *Gui) handleCommitSquashDown(g *gocui.Gui, v *gocui.View) error {
} }
// TODO: move to files panel // 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 { for _, file := range files {
if file.Tracked && file.HasUnstagedChanges { if file.Tracked && file.HasUnstagedChanges {
return true return true
@ -161,13 +156,13 @@ func (gui *Gui) handleRenameCommitEditor(g *gocui.Gui, v *gocui.View) error {
return nil 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") v, err := g.View("commits")
if err != nil { if err != nil {
panic(err) panic(err)
} }
if len(gui.State.Commits) == 0 { 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) lineNumber := gui.getItemPosition(v)
if lineNumber > len(gui.State.Commits)-1 { if lineNumber > len(gui.State.Commits)-1 {

View File

@ -7,16 +7,17 @@ import (
// "strings" // "strings"
"fmt"
"strings" "strings"
"github.com/fatih/color"
"github.com/jesseduffield/gocui" "github.com/jesseduffield/gocui"
"github.com/jesseduffield/lazygit/pkg/commands" "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 files := gui.State.Files
result := make([]commands.File, 0) result := make([]*commands.File, 0)
for _, file := range files { for _, file := range files {
if file.HasStagedChanges { if file.HasStagedChanges {
result = append(result, file) result = append(result, file)
@ -25,9 +26,9 @@ func (gui *Gui) stagedFiles() []commands.File {
return result return result
} }
func (gui *Gui) trackedFiles() []commands.File { func (gui *Gui) trackedFiles() []*commands.File {
files := gui.State.Files files := gui.State.Files
result := make([]commands.File, 0) result := make([]*commands.File, 0)
for _, file := range files { for _, file := range files {
if file.Tracked { if file.Tracked {
result = append(result, file) result = append(result, file)
@ -116,9 +117,9 @@ func (gui *Gui) handleAddPatch(g *gocui.Gui, v *gocui.View) error {
return gui.Errors.ErrSubProcess 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 { if len(gui.State.Files) == 0 {
return commands.File{}, gui.Errors.ErrNoFiles return &commands.File{}, gui.Errors.ErrNoFiles
} }
filesView, err := g.View("files") filesView, err := g.View("files")
if err != nil { 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")) gui.renderString(g, "main", gui.Tr.SLocalize("NoChangedFiles"))
return gui.renderfilesOptions(g, nil) return gui.renderfilesOptions(g, nil)
} }
gui.renderfilesOptions(g, &file) if err := gui.renderfilesOptions(g, file); err != nil {
return err
}
var content string var content string
if file.HasMergeConflicts { if file.HasMergeConflicts {
return gui.refreshMergePanel(g) return gui.refreshMergePanel(g)
@ -275,24 +278,6 @@ func (gui *Gui) updateHasMergeConflictStatus() error {
return nil 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) { func (gui *Gui) catSelectedFile(g *gocui.Gui) (string, error) {
item, err := gui.getSelectedFile(g) item, err := gui.getSelectedFile(g)
if err != nil { if err != nil {
@ -318,10 +303,14 @@ func (gui *Gui) refreshFiles(g *gocui.Gui) error {
return err return err
} }
gui.refreshStateFiles() gui.refreshStateFiles()
filesView.Clear() filesView.Clear()
for _, file := range gui.State.Files { list, err := utils.RenderList(gui.State.Files)
gui.renderFile(file, filesView) if err != nil {
return err
} }
fmt.Fprint(filesView, list)
gui.correctCursor(filesView) gui.correctCursor(filesView)
if filesView == g.CurrentView() { if filesView == g.CurrentView() {
gui.handleFileSelect(g, filesView) gui.handleFileSelect(g, filesView)

View File

@ -71,10 +71,10 @@ type Gui struct {
} }
type guiState struct { type guiState struct {
Files []commands.File Files []*commands.File
Branches []commands.Branch Branches []*commands.Branch
Commits []commands.Commit Commits []*commands.Commit
StashEntries []commands.StashEntry StashEntries []*commands.StashEntry
PreviousView string PreviousView string
HasMergeConflicts bool HasMergeConflicts bool
ConflictIndex int ConflictIndex int
@ -83,17 +83,16 @@ type guiState struct {
EditHistory *stack.Stack EditHistory *stack.Stack
Platform commands.Platform Platform commands.Platform
Updating bool Updating bool
Keys []Binding
} }
// NewGui builds a new gui handler // 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) { func NewGui(log *logrus.Entry, gitCommand *commands.GitCommand, oSCommand *commands.OSCommand, tr *i18n.Localizer, config config.AppConfigurer, updater *updates.Updater) (*Gui, error) {
initialState := guiState{ initialState := guiState{
Files: make([]commands.File, 0), Files: make([]*commands.File, 0),
PreviousView: "files", PreviousView: "files",
Commits: make([]commands.Commit, 0), Commits: make([]*commands.Commit, 0),
StashEntries: make([]commands.StashEntry, 0), StashEntries: make([]*commands.StashEntry, 0),
ConflictIndex: 0, ConflictIndex: 0,
ConflictTop: true, ConflictTop: true,
Conflicts: make([]commands.Conflict, 0), Conflicts: make([]commands.Conflict, 0),

View File

@ -1,6 +1,8 @@
package gui 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 // 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 // is only handled if the given view has focus, or handled globally if the view
@ -14,8 +16,26 @@ type Binding struct {
Description string Description string
} }
func (gui *Gui) GetKeybindings() []Binding { // GetDisplayStrings returns the display string of a file
bindings := []Binding{ 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: "", ViewName: "",
Key: 'q', Key: 'q',
@ -73,7 +93,7 @@ func (gui *Gui) GetKeybindings() []Binding {
ViewName: "", ViewName: "",
Key: 'x', Key: 'x',
Modifier: gocui.ModNone, Modifier: gocui.ModNone,
Handler: gui.handleMenu, Handler: gui.handleCreateOptionsMenu,
}, { }, {
ViewName: "status", ViewName: "status",
Key: 'e', Key: 'e',
@ -349,18 +369,13 @@ func (gui *Gui) GetKeybindings() []Binding {
Key: 'q', Key: 'q',
Modifier: gocui.ModNone, Modifier: gocui.ModNone,
Handler: gui.handleMenuClose, Handler: gui.handleMenuClose,
}, {
ViewName: "menu",
Key: gocui.KeySpace,
Modifier: gocui.ModNone,
Handler: gui.handleMenuPress,
}, },
} }
// Would make these keybindings global but that interferes with editing // Would make these keybindings global but that interferes with editing
// input in the confirmation panel // input in the confirmation panel
for _, viewName := range []string{"status", "files", "branches", "commits", "stash", "menu"} { 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.KeyTab, Modifier: gocui.ModNone, Handler: gui.nextView},
{ViewName: viewName, Key: gocui.KeyArrowLeft, Modifier: gocui.ModNone, Handler: gui.previousView}, {ViewName: viewName, Key: gocui.KeyArrowLeft, Modifier: gocui.ModNone, Handler: gui.previousView},
{ViewName: viewName, Key: gocui.KeyArrowRight, Modifier: gocui.ModNone, Handler: gui.nextView}, {ViewName: viewName, Key: gocui.KeyArrowRight, Modifier: gocui.ModNone, Handler: gui.nextView},

View File

@ -8,21 +8,6 @@ import (
"github.com/jesseduffield/lazygit/pkg/utils" "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 { func (gui *Gui) handleMenuSelect(g *gocui.Gui, v *gocui.View) error {
// doing nothing for now // doing nothing for now
// but it is needed for switch in newLineFocused // 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 { func (gui *Gui) handleMenuClose(g *gocui.Gui, v *gocui.View) error {
// better to delete because for example after closing update confirmation panel, if err := g.DeleteKeybinding("menu", gocui.KeySpace, gocui.ModNone); err != nil {
// the focus isn't set back to any of panels and one is unable to even quit return err
//_, err := g.SetViewOnBottom(v.Name()) }
err := g.DeleteView("menu") err := g.DeleteView("menu")
if err != nil { if err != nil {
return err return err
@ -49,83 +34,38 @@ func (gui *Gui) handleMenuClose(g *gocui.Gui, v *gocui.View) error {
return gui.returnFocus(g, v) return gui.returnFocus(g, v)
} }
func (gui *Gui) GetKey(binding Binding) string { func (gui *Gui) createMenu(items interface{}, handlePress func(int) error) error {
r, ok := binding.Key.(rune) list, err := utils.RenderList(items)
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 {
return err
}
fmt.Fprint(menuView, contentJoined)
g.Update(func(g *gocui.Gui) error {
_, err := g.SetViewOnTop("menu")
if err != nil { if err != nil {
return err return err
} }
return gui.switchFocus(g, v, menuView)
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)
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
}
currentView := gui.g.CurrentView()
return gui.switchFocus(gui.g, currentView, menuView)
}) })
return nil return nil
} }

View File

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

View File

@ -5,6 +5,7 @@ import (
"github.com/jesseduffield/gocui" "github.com/jesseduffield/gocui"
"github.com/jesseduffield/lazygit/pkg/commands" "github.com/jesseduffield/lazygit/pkg/commands"
"github.com/jesseduffield/lazygit/pkg/utils"
) )
func (gui *Gui) refreshStashEntries(g *gocui.Gui) error { func (gui *Gui) refreshStashEntries(g *gocui.Gui) error {
@ -14,10 +15,14 @@ func (gui *Gui) refreshStashEntries(g *gocui.Gui) error {
panic(err) panic(err)
} }
gui.State.StashEntries = gui.GitCommand.GetStashEntries() gui.State.StashEntries = gui.GitCommand.GetStashEntries()
v.Clear() v.Clear()
for _, stashEntry := range gui.State.StashEntries { list, err := utils.RenderList(gui.State.StashEntries)
fmt.Fprintln(v, stashEntry.DisplayString) if err != nil {
return err
} }
fmt.Fprint(v, list)
return gui.resetOrigin(v) return gui.resetOrigin(v)
}) })
return nil return nil
@ -29,7 +34,7 @@ func (gui *Gui) getSelectedStashEntry(v *gocui.View) *commands.StashEntry {
} }
stashView, _ := gui.g.View("stash") stashView, _ := gui.g.View("stash")
lineNumber := gui.getItemPosition(stashView) lineNumber := gui.getItemPosition(stashView)
return &gui.State.StashEntries[lineNumber] return gui.State.StashEntries[lineNumber]
} }
func (gui *Gui) renderStashOptions(g *gocui.Gui) error { func (gui *Gui) renderStashOptions(g *gocui.Gui) error {

View File

@ -160,7 +160,6 @@ func (gui *Gui) getItemPosition(v *gocui.View) int {
func (gui *Gui) cursorUp(g *gocui.Gui, v *gocui.View) error { func (gui *Gui) cursorUp(g *gocui.Gui, v *gocui.View) error {
// swallowing cursor movements in main // swallowing cursor movements in main
// TODO: pull this out
if v == nil || v.Name() == "main" { if v == nil || v.Name() == "main" {
return nil 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 { func (gui *Gui) cursorDown(g *gocui.Gui, v *gocui.View) error {
// swallowing cursor movements in main // swallowing cursor movements in main
// TODO: pull this out
if v == nil || v.Name() == "main" { if v == nil || v.Name() == "main" {
return nil return nil
} }
cx, cy := v.Cursor() cx, cy := v.Cursor()
ox, oy := v.Origin() 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 return nil
} }
if err := v.SetCursor(cx, cy+1); err != nil {
if err := v.SetOrigin(ox, oy+1); err != nil { var err error
return err 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) 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 // if the cursor down past the last item, move it to the last line
func (gui *Gui) correctCursor(v *gocui.View) error { func (gui *Gui) correctCursor(v *gocui.View) error {
cx, cy := v.Cursor() cx, cy := v.Cursor()
_, oy := v.Origin() ox, oy := v.Origin()
lineCount := len(v.BufferLines()) - 2 _, height := v.Size()
if cy >= lineCount-oy { maxY := height - 1
return v.SetCursor(cx, lineCount-oy) 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 return nil
} }

View File

@ -1,10 +1,12 @@
package utils package utils
import ( import (
"errors"
"fmt" "fmt"
"log" "log"
"os" "os"
"path/filepath" "path/filepath"
"reflect"
"strings" "strings"
"time" "time"
@ -99,3 +101,106 @@ func ResolvePlaceholderString(str string, arguments map[string]string) string {
} }
return str 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
}

View File

@ -1,6 +1,7 @@
package utils package utils
import ( import (
"errors"
"testing" "testing"
"github.com/stretchr/testify/assert" "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)) 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))
}
}