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:
commit
3072c93e13
@ -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
26
pkg/commands/commit.go
Normal 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
36
pkg/commands/file.go
Normal 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}
|
||||||
|
}
|
@ -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 {
|
||||||
|
@ -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 {
|
||||||
|
@ -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,
|
||||||
|
13
pkg/commands/stash_entry.go
Normal file
13
pkg/commands/stash_entry.go
Normal 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}
|
||||||
|
}
|
@ -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
|
||||||
|
@ -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)
|
||||||
})
|
})
|
||||||
|
@ -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 {
|
||||||
|
@ -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)
|
||||||
|
@ -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),
|
||||||
|
@ -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},
|
||||||
|
@ -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
|
||||||
}
|
}
|
||||||
|
51
pkg/gui/options_menu_panel.go
Normal file
51
pkg/gui/options_menu_panel.go
Normal 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)
|
||||||
|
}
|
@ -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 {
|
||||||
|
@ -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
|
||||||
}
|
}
|
||||||
|
@ -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
|
||||||
|
}
|
||||||
|
@ -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))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
Loading…
x
Reference in New Issue
Block a user