Compare commits

...
33 Commits
Author SHA1 Message Date
Jesse Duffield 8ad01fe32f refresh commits when adding a tag 2020-02-25 09:10:23 +11:00
Jesse Duffield fdb543fa7d add half and fullscreen modes 2020-02-25 08:45:30 +11:00
Jesse Duffield 52b5a6410c show item counts in frames 2020-02-25 07:19:46 +11:00
Jesse Duffield 0034cfef5c show tags in commits panel 2020-02-24 23:13:54 +11:00
Jesse Duffield 78b62be96f better handling of clearing the search 2020-02-24 22:18:04 +11:00
Jesse Duffield 1f5ccab1ce eagerload commits when searching 2020-02-24 22:18:04 +11:00
Jesse Duffield 46be280c92 support searching in side panels
For now we're just doing side panels, because it will take more work
to support this in the various main panel contexts
2020-02-24 22:18:04 +11:00
Jesse Duffield 2a5763a771 switch custom command keybinding to ':' 2020-02-24 22:04:39 +11:00
Jesse Duffield 370cec098b show diff stat 2020-02-24 09:20:50 +11:00
Dawid Dziurla 49a2f0191f tasks: don't use a function that requires Go 1.12 2020-02-24 09:09:27 +11:00
Jesse Duffield fabdda0492 allow customizing background color in staging mode 2020-02-23 18:37:19 +11:00
Glenn Vriesman 6fc3290a05 Reflog: Use 20 sha digits instead of 7
Signed-off-by: Glenn Vriesman <glenn.vriesman@gmail.com>
2020-02-20 08:34:01 +11:00
Jesse Duffield 66e6369c28 allow fastforwarding the current branch 2020-02-18 23:07:38 +11:00
Jesse Duffield 0f0da9c32a fix wording 2020-02-16 09:57:49 +11:00
Jesse Duffield 0a69c1a02d add reset to reflog commit menu 2020-02-16 09:57:49 +11:00
Jesse Duffield feaf98bd01 add reset to upstream option on files panel 2020-02-16 09:57:49 +11:00
Jesse Duffield 0fe9c15ce8 add mixed option to HEAD resetting, remove @{upstream} 2020-02-16 09:57:49 +11:00
Jesse Duffield f528e12c83 allow resetting to tag 2020-02-16 09:57:49 +11:00
Jesse Duffield 8ca9f93ccf allow resetting to remote branch 2020-02-16 09:57:49 +11:00
Jesse Duffield 73d8064837 allow resetting to branch 2020-02-16 09:57:49 +11:00
Jesse Duffield 5b1f60b7eb refactor create reset menu logic 2020-02-16 09:57:49 +11:00
Jesse Duffield 2e1344f611 fix specs 2020-02-15 08:47:36 +11:00
Jesse Duffield 5b9996b16f remove old createMenu function 2020-02-15 08:47:36 +11:00
Jesse Duffield 6fdc1791e4 refactor stash options menu 2020-02-15 08:47:36 +11:00
Jesse Duffield fd4f37b5c3 refactor git flow menu 2020-02-15 08:47:36 +11:00
Jesse Duffield d76e8887e5 refactor patch options menu panel 2020-02-15 08:47:36 +11:00
Jesse Duffield eb9134685a refactor rebase menu panel 2020-02-15 08:47:36 +11:00
Jesse Duffield d929b84786 refactor recent repos menu panel 2020-02-15 08:47:36 +11:00
Jesse Duffield 8ef3297b11 refactor reflog reset options panel 2020-02-15 08:47:36 +11:00
Jesse Duffield 27c7aeb117 refactor workspace reset options panel 2020-02-15 08:47:36 +11:00
Jesse Duffield c9714600e8 refactor commit reset menu 2020-02-15 08:47:36 +11:00
Jesse Duffield 665fdded14 continue refactor of menu panel 2020-02-15 08:47:36 +11:00
Jesse Duffield 814a0ea36f begin refactor of menu panel 2020-02-15 08:47:36 +11:00
42 changed files with 1201 additions and 546 deletions
+7 -1
View File
@@ -21,6 +21,8 @@ Default path for the config file:
- white
optionsTextColor:
- blue
selectedLineBgColor:
- blue
commitLength:
show: true
mouseEvents: true
@@ -65,7 +67,7 @@ Default path for the config file:
scrollDownMain-alt1: 'J' # main panel scrool down
scrollUpMain-alt2: '<c-u>' # main panel scrool up
scrollDownMain-alt2: '<c-d>' # main panel scrool down
executeCustomCommand: 'X'
executeCustomCommand: ':'
createRebaseOptionsMenu: 'm'
pushFiles: 'P'
pullFiles: 'p'
@@ -73,6 +75,8 @@ Default path for the config file:
createPatchOptionsMenu: '<c-p>'
nextTab: ']'
prevTab: '['
nextScreenMode: '+'
prevScreenMode: '_'
status:
checkForUpdate: 'u'
recentRepos: '<enter>'
@@ -193,6 +197,8 @@ If you have issues with a light terminal theme where you can't read / see the te
- bold
inactiveBorderColor:
- black
selectedLineBgColor:
- blue
```
## Example Coloring:
+1 -1
View File
@@ -11,7 +11,7 @@ require (
github.com/golang/protobuf v1.3.2 // indirect
github.com/google/go-cmp v0.3.1 // indirect
github.com/integrii/flaggy v1.4.0
github.com/jesseduffield/gocui v0.3.1-0.20200201013258-57fdcf23edc5
github.com/jesseduffield/gocui v0.3.1-0.20200224201655-5024a02682ed
github.com/jesseduffield/pty v1.2.1
github.com/jesseduffield/rollrus v0.0.0-20190701125922-dd028cb0bfd7 // indirect
github.com/jesseduffield/termbox-go v0.0.0-20200130214842-1d31d1faa3c9 // indirect
+4
View File
@@ -87,6 +87,10 @@ github.com/jesseduffield/gocui v0.3.1-0.20200131131454-a319843434ac h1:vp7I0RpFq
github.com/jesseduffield/gocui v0.3.1-0.20200131131454-a319843434ac/go.mod h1:2RtZznzYKt8RLRwvFiSkXjU0Ei8WwHdubgnlaYH47dw=
github.com/jesseduffield/gocui v0.3.1-0.20200201013258-57fdcf23edc5 h1:tE0w3tuL/bj1o5VMhjjE0ep6i7Fva+RYjKcMFcniJEY=
github.com/jesseduffield/gocui v0.3.1-0.20200201013258-57fdcf23edc5/go.mod h1:2RtZznzYKt8RLRwvFiSkXjU0Ei8WwHdubgnlaYH47dw=
github.com/jesseduffield/gocui v0.3.1-0.20200223105115-3e1f0f7c3efe h1:UQyebauOcBzbGq32kTXwEyuJaqp3BkI8JoCrGs2jijU=
github.com/jesseduffield/gocui v0.3.1-0.20200223105115-3e1f0f7c3efe/go.mod h1:2RtZznzYKt8RLRwvFiSkXjU0Ei8WwHdubgnlaYH47dw=
github.com/jesseduffield/gocui v0.3.1-0.20200224201655-5024a02682ed h1:glGs+mzPZOl1iHiUsBW3918WeFwqsbQQ/jtLkkQXDi4=
github.com/jesseduffield/gocui v0.3.1-0.20200224201655-5024a02682ed/go.mod h1:2RtZznzYKt8RLRwvFiSkXjU0Ei8WwHdubgnlaYH47dw=
github.com/jesseduffield/pty v1.2.1 h1:7xYBiwNH0PpWqC8JmvrPq1a/ksNqyCavzWu9pbBGYWI=
github.com/jesseduffield/pty v1.2.1/go.mod h1:7jlS40+UhOqkZJDIG1B/H21xnuET/+fvbbnHCa8wSIo=
github.com/jesseduffield/roll v0.0.0-20190629104057-695be2e62b00 h1:+JaOkfBNYQYlGD7dgru8mCwYNEc5tRRI8mThlVANhSM=
+2 -1
View File
@@ -61,7 +61,8 @@ func (c *Commit) GetDisplayStrings(isFocused bool) []string {
if c.Action != "" {
actionString = cyan.Sprint(utils.WithPadding(c.Action, 7)) + " "
} else if len(c.Tags) > 0 {
tagString = utils.ColoredString(strings.Join(c.Tags, " "), color.FgMagenta) + " "
tagColor := color.New(color.FgMagenta, color.Bold)
tagString = utils.ColoredStringDirect(strings.Join(c.Tags, " "), tagColor) + " "
}
return []string{shaColor.Sprint(c.Sha[:8]), actionString + tagString + defaultColor.Sprint(c.Name)}
+43 -6
View File
@@ -45,6 +45,44 @@ func NewCommitListBuilder(log *logrus.Entry, gitCommand *GitCommand, osCommand *
}, nil
}
// nameAndTag takes a line from a git log and extracts the sha, message and tag (if present)
// example inputs:
// 66e6369c284e96ed5af5 (tag: v0.14.4) allow fastforwarding the current branch
// 32e650e0bb3f4327749f (HEAD -> show-tags) this is my commit
// 32e650e0bb3f4327749e this is my other commit
func (c *CommitListBuilder) commitLineParts(line string) (string, string, []string) {
re := regexp.MustCompile(`(\w+) (.*)`)
shaMatch := re.FindStringSubmatch(line)
if len(shaMatch) <= 1 {
return line, "", nil
}
sha := shaMatch[1]
rest := shaMatch[2]
if !strings.HasPrefix(rest, "(") {
return sha, rest, nil
}
re = regexp.MustCompile(`\((.*)\) (.*)`)
parensMatch := re.FindStringSubmatch(rest)
if len(parensMatch) <= 1 {
return sha, rest, nil
}
notes := parensMatch[1]
message := parensMatch[2]
re = regexp.MustCompile(`tag: ([^,]+)`)
tagMatch := re.FindStringSubmatch(notes)
if len(tagMatch) <= 1 {
return sha, message, nil
}
tag := tagMatch[1]
return sha, message, []string{tag}
}
// GetCommits obtains the commits of the current branch
func (c *CommitListBuilder) GetCommits(limit bool) ([]*Commit, error) {
commits := []*Commit{}
@@ -69,16 +107,15 @@ func (c *CommitListBuilder) GetCommits(limit bool) ([]*Commit, error) {
// now we can split it up and turn it into commits
for _, line := range utils.SplitLines(log) {
splitLine := strings.Split(line, " ")
sha := splitLine[0]
sha, name, tags := c.commitLineParts(line)
_, unpushed := unpushedCommits[sha]
status := map[bool]string{true: "unpushed", false: "pushed"}[unpushed]
commits = append(commits, &Commit{
Sha: sha,
Name: strings.Join(splitLine[1:], " "),
Name: name,
Status: status,
DisplayString: strings.Join(splitLine, " "),
// TODO: add tags here
DisplayString: line,
Tags: tags,
})
}
if rebaseMode != "" {
@@ -288,7 +325,7 @@ func (c *CommitListBuilder) getLog(limit bool) string {
limitFlag = "-30"
}
result, err := c.OSCommand.RunCommandWithOutput(fmt.Sprintf("git log --oneline %s --abbrev=%d", limitFlag, 20))
result, err := c.OSCommand.RunCommandWithOutput(fmt.Sprintf("git log --decorate --oneline %s --abbrev=%d", limitFlag, 20))
if err != nil {
// assume if there is an error there are no commits yet for this branch
return ""
+5 -5
View File
@@ -163,7 +163,7 @@ func TestCommitListBuilderGetLog(t *testing.T) {
"Retrieves logs",
func(cmd string, args ...string) *exec.Cmd {
assert.EqualValues(t, "git", cmd)
assert.EqualValues(t, []string{"log", "--oneline", "-30", "--abbrev=20"}, args)
assert.EqualValues(t, []string{"log", "--decorate", "--oneline", "-30", "--abbrev=20"}, args)
return exec.Command("echo", "6f0b32f commands/git : add GetCommits tests refactor\n9d9d775 circle : remove new line")
},
@@ -175,7 +175,7 @@ func TestCommitListBuilderGetLog(t *testing.T) {
"An error occurred when retrieving logs",
func(cmd string, args ...string) *exec.Cmd {
assert.EqualValues(t, "git", cmd)
assert.EqualValues(t, []string{"log", "--oneline", "-30", "--abbrev=20"}, args)
assert.EqualValues(t, []string{"log", "--decorate", "--oneline", "-30", "--abbrev=20"}, args)
return exec.Command("test")
},
func(output string) {
@@ -212,7 +212,7 @@ func TestCommitListBuilderGetCommits(t *testing.T) {
assert.EqualValues(t, []string{"rev-list", "@{u}..HEAD", "--abbrev-commit"}, args)
return exec.Command("echo")
case "log":
assert.EqualValues(t, []string{"log", "--oneline", "-30", "--abbrev=20"}, args)
assert.EqualValues(t, []string{"log", "--decorate", "--oneline", "-30", "--abbrev=20"}, args)
return exec.Command("echo")
case "merge-base":
assert.EqualValues(t, []string{"merge-base", "HEAD", "master"}, args)
@@ -239,7 +239,7 @@ func TestCommitListBuilderGetCommits(t *testing.T) {
assert.EqualValues(t, []string{"rev-list", "@{u}..HEAD", "--abbrev-commit"}, args)
return exec.Command("echo", "8a2bb0e")
case "log":
assert.EqualValues(t, []string{"log", "--oneline", "-30", "--abbrev=20"}, args)
assert.EqualValues(t, []string{"log", "--decorate", "--oneline", "-30", "--abbrev=20"}, args)
return exec.Command("echo", "8a2bb0e commit 1\n78976bc commit 2")
case "merge-base":
assert.EqualValues(t, []string{"merge-base", "HEAD", "master"}, args)
@@ -280,7 +280,7 @@ func TestCommitListBuilderGetCommits(t *testing.T) {
assert.EqualValues(t, []string{"rev-list", "@{u}..HEAD", "--abbrev-commit"}, args)
return exec.Command("echo", "8a2bb0e")
case "log":
assert.EqualValues(t, []string{"log", "--oneline", "-30", "--abbrev=20"}, args)
assert.EqualValues(t, []string{"log", "--decorate", "--oneline", "-30", "--abbrev=20"}, args)
return exec.Command("echo", "8a2bb0e commit 1\n78976bc commit 2")
case "merge-base":
assert.EqualValues(t, []string{"merge-base", "HEAD", "master"}, args)
+8 -3
View File
@@ -408,6 +408,11 @@ func (c *GitCommand) Pull(args string, ask func(string) string) error {
return c.OSCommand.DetectUnamePass("git pull --no-edit "+args, ask)
}
// PullWithoutPasswordCheck assumes that the pull will not prompt the user for a password
func (c *GitCommand) PullWithoutPasswordCheck(args string) error {
return c.OSCommand.RunCommand("git pull --no-edit " + args)
}
// Push pushes to a branch
func (c *GitCommand) Push(branchName string, force bool, upstream string, args string, ask func(string) string) error {
forceFlag := ""
@@ -553,7 +558,7 @@ func (c *GitCommand) Ignore(filename string) error {
}
func (c *GitCommand) ShowCmdStr(sha string) string {
return fmt.Sprintf("git show --color --no-renames %s", sha)
return fmt.Sprintf("git show --color --no-renames --stat -p %s", sha)
}
func (c *GitCommand) GetBranchGraphCmdStr(branchName string) string {
@@ -599,7 +604,7 @@ func (c *GitCommand) DiffCmdStr(file *File, plain bool, cached bool) string {
colorArg = ""
}
return fmt.Sprintf("git diff %s %s %s %s", colorArg, cachedArg, trackedArg, fileName)
return fmt.Sprintf("git diff --stat -p %s %s %s %s", colorArg, cachedArg, trackedArg, fileName)
}
func (c *GitCommand) ApplyPatch(patch string, flags ...string) error {
@@ -1100,7 +1105,7 @@ func (c *GitCommand) FetchRemote(remoteName string) error {
}
func (c *GitCommand) GetReflogCommits() ([]*Commit, error) {
output, err := c.OSCommand.RunCommandWithOutput("git reflog")
output, err := c.OSCommand.RunCommandWithOutput("git reflog --abbrev=20")
if err != nil {
return nil, err
}
+4 -4
View File
@@ -1473,7 +1473,7 @@ func TestGitCommandDiff(t *testing.T) {
"Default case",
func(cmd string, args ...string) *exec.Cmd {
assert.EqualValues(t, "git", cmd)
assert.EqualValues(t, []string{"diff", "--color", "--", "test.txt"}, args)
assert.EqualValues(t, []string{"diff", "--stat", "-p", "--color", "--", "test.txt"}, args)
return exec.Command("echo")
},
@@ -1489,7 +1489,7 @@ func TestGitCommandDiff(t *testing.T) {
"cached",
func(cmd string, args ...string) *exec.Cmd {
assert.EqualValues(t, "git", cmd)
assert.EqualValues(t, []string{"diff", "--color", "--cached", "--", "test.txt"}, args)
assert.EqualValues(t, []string{"diff", "--stat", "-p", "--color", "--cached", "--", "test.txt"}, args)
return exec.Command("echo")
},
@@ -1505,7 +1505,7 @@ func TestGitCommandDiff(t *testing.T) {
"plain",
func(cmd string, args ...string) *exec.Cmd {
assert.EqualValues(t, "git", cmd)
assert.EqualValues(t, []string{"diff", "--", "test.txt"}, args)
assert.EqualValues(t, []string{"diff", "--stat", "-p", "--", "test.txt"}, args)
return exec.Command("echo")
},
@@ -1521,7 +1521,7 @@ func TestGitCommandDiff(t *testing.T) {
"File not tracked and file has no staged changes",
func(cmd string, args ...string) *exec.Cmd {
assert.EqualValues(t, "git", cmd)
assert.EqualValues(t, []string{"diff", "--color", "--no-index", "/dev/null", "test.txt"}, args)
assert.EqualValues(t, []string{"diff", "--stat", "-p", "--color", "--no-index", "/dev/null", "test.txt"}, args)
return exec.Command("echo")
},
+1 -1
View File
@@ -120,7 +120,7 @@ func coloredString(colorAttr color.Attribute, str string, selected bool, include
var cl *color.Color
attributes := []color.Attribute{colorAttr}
if selected {
attributes = append(attributes, color.BgBlue)
attributes = append(attributes, theme.SelectedLineBgColor)
}
cl = color.New(attributes...)
var clIncluded *color.Color
+8 -1
View File
@@ -253,6 +253,8 @@ func GetDefaultConfig() []byte {
- white
optionsTextColor:
- blue
selectedLineBgColor:
- blue
commitLength:
show: true
git:
@@ -281,6 +283,9 @@ keybinding:
nextBlock: '<right>'
prevBlock-alt: 'h'
nextBlock-alt: 'l'
nextMatch: 'n'
prevMatch: 'N'
startSearch: '/'
optionMenu: 'x'
optionMenu-alt1: '?'
select: '<space>'
@@ -295,7 +300,7 @@ keybinding:
scrollDownMain-alt1: 'J'
scrollUpMain-alt2: '<c-u>'
scrollDownMain-alt2: '<c-d>'
executeCustomCommand: 'X'
executeCustomCommand: ':'
createRebaseOptionsMenu: 'm'
pushFiles: 'P'
pullFiles: 'p'
@@ -303,6 +308,8 @@ keybinding:
createPatchOptionsMenu: '<c-p>'
nextTab: ']'
prevTab: '['
nextScreenMode: '+'
prevScreenMode: '_'
status:
checkForUpdate: 'u'
recentRepos: '<enter>'
+36 -3
View File
@@ -372,12 +372,19 @@ func (gui *Gui) handleFastForward(g *gocui.Gui, v *gocui.View) error {
go func() {
_ = gui.createLoaderPanel(gui.g, v, message)
if err := gui.GitCommand.FastForward(branch.Name, remoteName, remoteBranchName); err != nil {
_ = gui.createErrorPanel(gui.g, err.Error())
if gui.State.Panels.Branches.SelectedLine == 0 {
if err := gui.GitCommand.PullWithoutPasswordCheck("--ff-only"); err != nil {
_ = gui.createErrorPanel(gui.g, err.Error())
}
_ = gui.refreshSidePanels(gui.g)
} else {
_ = gui.closeConfirmationPrompt(gui.g, true)
if err := gui.GitCommand.FastForward(branch.Name, remoteName, remoteBranchName); err != nil {
_ = gui.createErrorPanel(gui.g, err.Error())
}
_ = gui.RenderSelectedBranchUpstreamDifferences()
}
_ = gui.closeConfirmationPrompt(gui.g, true)
}()
return nil
}
@@ -393,6 +400,7 @@ func (gui *Gui) onBranchesTabClick(tabIndex int) error {
func (gui *Gui) switchBranchesPanelContext(context string) error {
branchesView := gui.getBranchesView()
branchesView.Context = context
gui.onSearchEscape()
contextTabIndexMap := map[string]int{
"local-branches": 0,
@@ -428,3 +436,28 @@ func (gui *Gui) handlePrevBranchesTab(g *gocui.Gui, v *gocui.View) error {
utils.ModuloWithWrap(v.TabIndex-1, len(v.Tabs)),
)
}
func (gui *Gui) handleCreateResetToBranchMenu(g *gocui.Gui, v *gocui.View) error {
branch := gui.getSelectedBranch()
if branch == nil {
return nil
}
return gui.createResetMenu(branch.Name)
}
func (gui *Gui) onBranchesPanelSearchSelect(selectedLine int) error {
branchesView := gui.getBranchesView()
switch branchesView.Context {
case "local-branches":
gui.State.Panels.Branches.SelectedLine = selectedLine
return gui.handleBranchSelect(gui.g, branchesView)
case "remotes":
gui.State.Panels.Remotes.SelectedLine = selectedLine
return gui.handleRemoteSelect(gui.g, branchesView)
case "remote-branches":
gui.State.Panels.RemoteBranches.SelectedLine = selectedLine
return gui.handleRemoteBranchSelect(gui.g, branchesView)
}
return nil
}
+5
View File
@@ -212,3 +212,8 @@ func (gui *Gui) enterCommitFile(selectedLineIdx int) error {
return enterTheFile(selectedLineIdx)
}
func (gui *Gui) onCommitFilesPanelSearchSelect(selectedLine int) error {
gui.State.Panels.CommitFiles.SelectedLine = selectedLine
return gui.handleCommitFileSelect(gui.g, gui.getCommitFilesView())
}
+35 -49
View File
@@ -1,10 +1,8 @@
package gui
import (
"fmt"
"strconv"
"github.com/fatih/color"
"github.com/go-errors/errors"
"github.com/jesseduffield/gocui"
@@ -560,53 +558,6 @@ func (gui *Gui) handleSquashAllAboveFixupCommits(g *gocui.Gui, v *gocui.View) er
}, nil)
}
type resetOption struct {
description string
command string
}
// GetDisplayStrings is a function.
func (r *resetOption) GetDisplayStrings(isFocused bool) []string {
return []string{r.description, color.New(color.FgRed).Sprint(r.command)}
}
func (gui *Gui) handleCreateCommitResetMenu(g *gocui.Gui, v *gocui.View) error {
commit := gui.getSelectedCommit(g)
if commit == nil {
return gui.createErrorPanel(gui.g, gui.Tr.SLocalize("NoCommitsThisBranch"))
}
strengths := []string{"soft", "mixed", "hard"}
options := make([]*resetOption, len(strengths))
for i, strength := range strengths {
options[i] = &resetOption{
description: fmt.Sprintf("%s reset", strength),
command: fmt.Sprintf("reset --%s %s", strength, commit.Sha),
}
}
handleMenuPress := func(index int) error {
if err := gui.GitCommand.ResetToCommit(commit.Sha, strengths[index]); err != nil {
return err
}
if err := gui.refreshCommits(g); err != nil {
return err
}
if err := gui.refreshFiles(); err != nil {
return err
}
if err := gui.resetOrigin(gui.getCommitsView()); err != nil {
return err
}
gui.State.Panels.Commits.SelectedLine = 0
return gui.handleCommitSelect(g, gui.getCommitsView())
}
return gui.createMenu(fmt.Sprintf("%s %s", gui.Tr.SLocalize("resetTo"), commit.Sha), options, len(options), handleMenuPress)
}
func (gui *Gui) handleTagCommit(g *gocui.Gui, v *gocui.View) error {
// TODO: bring up menu asking if you want to make a lightweight or annotated tag
// if annotated, switch to a subprocess to create the message
@@ -672,6 +623,7 @@ func (gui *Gui) onCommitsTabClick(tabIndex int) error {
func (gui *Gui) switchCommitsPanelContext(context string) error {
commitsView := gui.getCommitsView()
commitsView.Context = context
gui.onSearchEscape()
contextTabIndexMap := map[string]int{
"branch-commits": 0,
@@ -701,3 +653,37 @@ func (gui *Gui) handlePrevCommitsTab(g *gocui.Gui, v *gocui.View) error {
utils.ModuloWithWrap(v.TabIndex-1, len(v.Tabs)),
)
}
func (gui *Gui) handleCreateCommitResetMenu(g *gocui.Gui, v *gocui.View) error {
commit := gui.getSelectedCommit(g)
if commit == nil {
return gui.createErrorPanel(gui.g, gui.Tr.SLocalize("NoCommitsThisBranch"))
}
return gui.createResetMenu(commit.Sha)
}
func (gui *Gui) onCommitsPanelSearchSelect(selectedLine int) error {
commitsView := gui.getCommitsView()
switch commitsView.Context {
case "branch-commits":
gui.State.Panels.Commits.SelectedLine = selectedLine
return gui.handleCommitSelect(gui.g, commitsView)
case "reflog-commits":
gui.State.Panels.ReflogCommits.SelectedLine = selectedLine
return gui.handleReflogCommitSelect(gui.g, commitsView)
}
return nil
}
func (gui *Gui) handleOpenSearchForCommitsPanel(g *gocui.Gui, v *gocui.View) error {
// we usually lazyload these commits but now that we're searching we need to load them now
if gui.State.Panels.Commits.LimitCommits {
gui.State.Panels.Commits.LimitCommits = false
if err := gui.refreshCommits(gui.g); err != nil {
return err
}
}
return gui.handleOpenSearch(gui.g, v)
}
+42
View File
@@ -0,0 +1,42 @@
package gui
import (
"github.com/jesseduffield/gocui"
)
func (gui *Gui) handleCreateDiscardMenu(g *gocui.Gui, v *gocui.View) error {
file, err := gui.getSelectedFile(g)
if err != nil {
if err != gui.Errors.ErrNoFiles {
return err
}
return nil
}
menuItems := []*menuItem{
{
displayString: gui.Tr.SLocalize("discardAllChanges"),
onPress: func() error {
if err := gui.GitCommand.DiscardAllFileChanges(file); err != nil {
return err
}
return gui.refreshFiles()
},
},
}
if file.HasStagedChanges && file.HasUnstagedChanges {
menuItems = append(menuItems, &menuItem{
displayString: gui.Tr.SLocalize("discardUnstagedChanges"),
onPress: func() error {
if err := gui.GitCommand.DiscardUnstagedFileChanges(file); err != nil {
return err
}
return gui.refreshFiles()
},
})
}
return gui.createMenu(file.Name, menuItems, createMenuOptions{showCancel: true})
}
+15 -87
View File
@@ -540,67 +540,6 @@ func (gui *Gui) anyFilesWithMergeConflicts() bool {
return false
}
type discardOption struct {
handler func(fileName *commands.File) error
description string
}
// GetDisplayStrings is a function.
func (r *discardOption) GetDisplayStrings(isFocused bool) []string {
return []string{r.description}
}
func (gui *Gui) handleCreateDiscardMenu(g *gocui.Gui, v *gocui.View) error {
file, err := gui.getSelectedFile(g)
if err != nil {
if err != gui.Errors.ErrNoFiles {
return err
}
return nil
}
options := []*discardOption{
{
description: gui.Tr.SLocalize("discardAllChanges"),
handler: func(file *commands.File) error {
return gui.GitCommand.DiscardAllFileChanges(file)
},
},
{
description: gui.Tr.SLocalize("cancel"),
handler: func(file *commands.File) error {
return nil
},
},
}
if file.HasStagedChanges && file.HasUnstagedChanges {
discardUnstagedChanges := &discardOption{
description: gui.Tr.SLocalize("discardUnstagedChanges"),
handler: func(file *commands.File) error {
return gui.GitCommand.DiscardUnstagedFileChanges(file)
},
}
options = append(options[:1], append([]*discardOption{discardUnstagedChanges}, options[1:]...)...)
}
handleMenuPress := func(index int) error {
file, err := gui.getSelectedFile(g)
if err != nil {
return err
}
if err := options[index].handler(file); err != nil {
return err
}
return gui.refreshFiles()
}
return gui.createMenu(file.Name, options, len(options), handleMenuPress)
}
func (gui *Gui) handleCustomCommand(g *gocui.Gui, v *gocui.View) error {
return gui.createPromptPanel(g, v, gui.Tr.SLocalize("CustomCommand"), "", func(g *gocui.Gui, v *gocui.View) error {
command := gui.trimmedContent(v)
@@ -609,45 +548,34 @@ func (gui *Gui) handleCustomCommand(g *gocui.Gui, v *gocui.View) error {
})
}
type stashOption struct {
description string
handler func() error
}
// GetDisplayStrings is a function.
func (o *stashOption) GetDisplayStrings(isFocused bool) []string {
return []string{o.description}
}
func (gui *Gui) handleCreateStashMenu(g *gocui.Gui, v *gocui.View) error {
options := []*stashOption{
menuItems := []*menuItem{
{
description: gui.Tr.SLocalize("stashAllChanges"),
handler: func() error {
displayString: gui.Tr.SLocalize("stashAllChanges"),
onPress: func() error {
return gui.handleStashSave(gui.GitCommand.StashSave)
},
},
{
description: gui.Tr.SLocalize("stashStagedChanges"),
handler: func() error {
displayString: gui.Tr.SLocalize("stashStagedChanges"),
onPress: func() error {
return gui.handleStashSave(gui.GitCommand.StashSaveStagedChanges)
},
},
{
description: gui.Tr.SLocalize("cancel"),
handler: func() error {
return nil
},
},
}
handleMenuPress := func(index int) error {
return options[index].handler()
}
return gui.createMenu(gui.Tr.SLocalize("stashOptions"), options, len(options), handleMenuPress)
return gui.createMenu(gui.Tr.SLocalize("stashOptions"), menuItems, createMenuOptions{showCancel: true})
}
func (gui *Gui) handleStashChanges(g *gocui.Gui, v *gocui.View) error {
return gui.handleStashSave(gui.GitCommand.StashSave)
}
func (gui *Gui) handleCreateResetToUpstreamMenu(g *gocui.Gui, v *gocui.View) error {
return gui.createResetMenu("@{upstream}")
}
func (gui *Gui) onFilesPanelSearchSelect(selectedLine int) error {
gui.State.Panels.Files.SelectedLine = selectedLine
return gui.focusAndSelectFile(gui.g, gui.getFilesView())
}
+10 -30
View File
@@ -8,11 +8,6 @@ import (
"github.com/jesseduffield/gocui"
)
type gitFlowOption struct {
handler func() error
description string
}
func (gui *Gui) gitFlowFinishBranch(gitFlowConfig string, branchName string) error {
// need to find out what kind of branch this is
prefix := strings.SplitAfterN(branchName, "/", 2)[0]
@@ -41,11 +36,6 @@ func (gui *Gui) gitFlowFinishBranch(gitFlowConfig string, branchName string) err
return gui.Errors.ErrSubProcess
}
// GetDisplayStrings is a function.
func (r *gitFlowOption) GetDisplayStrings(isFocused bool) []string {
return []string{r.description}
}
func (gui *Gui) handleCreateGitFlowMenu(g *gocui.Gui, v *gocui.View) error {
branch := gui.getSelectedBranch()
if branch == nil {
@@ -70,37 +60,27 @@ func (gui *Gui) handleCreateGitFlowMenu(g *gocui.Gui, v *gocui.View) error {
}
}
options := []*gitFlowOption{
menuItems := []*menuItem{
{
// not localising here because it's one to one with the actual git flow commands
description: fmt.Sprintf("finish branch '%s'", branch.Name),
handler: func() error {
displayString: fmt.Sprintf("finish branch '%s'", branch.Name),
onPress: func() error {
return gui.gitFlowFinishBranch(gitFlowConfig, branch.Name)
},
},
{
description: "start feature",
handler: startHandler("feature"),
displayString: "start feature",
onPress: startHandler("feature"),
},
{
description: "start hotfix",
handler: startHandler("hotfix"),
displayString: "start hotfix",
onPress: startHandler("hotfix"),
},
{
description: "start release",
handler: startHandler("release"),
},
{
description: gui.Tr.SLocalize("cancel"),
handler: func() error {
return nil
},
displayString: "start release",
onPress: startHandler("release"),
},
}
handleMenuPress := func(index int) error {
return options[index].handler()
}
return gui.createMenu("git flow", options, len(options), handleMenuPress)
return gui.createMenu("git flow", menuItems, createMenuOptions{})
}
+177 -58
View File
@@ -33,6 +33,12 @@ import (
"github.com/sirupsen/logrus"
)
const (
SCREEN_NORMAL int = iota
SCREEN_HALF
SCREEN_FULL
)
const StartupPopupVersion = 1
// OverlappingEdges determines if panel edges overlap
@@ -172,6 +178,12 @@ type panelStates struct {
Status *statusPanelState
}
type searchingState struct {
view *gocui.View
isSearching bool
searchString string
}
type guiState struct {
Files []*commands.File
Branches []*commands.Branch
@@ -195,6 +207,9 @@ type guiState struct {
RetainOriginalDir bool
IsRefreshingFiles bool
RefreshingFilesMutex sync.Mutex
Searching searchingState
ScreenMode int
SideView *gocui.View
}
// for now the split view will always be on
@@ -229,6 +244,8 @@ func NewGui(log *logrus.Entry, gitCommand *commands.GitCommand, oSCommand *comma
},
Status: &statusPanelState{},
},
ScreenMode: SCREEN_NORMAL,
SideView: nil,
}
gui := &Gui{
@@ -250,6 +267,18 @@ func NewGui(log *logrus.Entry, gitCommand *commands.GitCommand, oSCommand *comma
return gui, nil
}
func (gui *Gui) nextScreenMode(g *gocui.Gui, v *gocui.View) error {
gui.State.ScreenMode = utils.NextIntInCycle([]int{SCREEN_NORMAL, SCREEN_HALF, SCREEN_FULL}, gui.State.ScreenMode)
return nil
}
func (gui *Gui) prevScreenMode(g *gocui.Gui, v *gocui.View) error {
gui.State.ScreenMode = utils.PrevIntInCycle([]int{SCREEN_NORMAL, SCREEN_HALF, SCREEN_FULL}, gui.State.ScreenMode)
return nil
}
func (gui *Gui) scrollUpView(viewName string) error {
mainView, _ := gui.g.View(viewName)
ox, oy := mainView.Origin()
@@ -338,6 +367,9 @@ func (gui *Gui) onFocusLost(v *gocui.View, newView *gocui.View) error {
if v == nil {
return nil
}
if v.IsSearching() && newView.Name() != "search" {
gui.onSearchEscape()
}
switch v.Name() {
case "branches":
if v.Context == "local-branches" {
@@ -369,6 +401,75 @@ func (gui *Gui) onFocus(v *gocui.View) error {
return nil
}
func (gui *Gui) getViewHeights() map[string]int {
currView := gui.g.CurrentView()
currentCyclebleView := gui.State.PreviousView
if currView != nil {
viewName := currView.Name()
usePreviousView := true
for _, view := range cyclableViews {
if view == viewName {
currentCyclebleView = viewName
usePreviousView = false
break
}
}
if usePreviousView {
currentCyclebleView = gui.State.PreviousView
}
}
// unfortunate result of the fact that these are separate views, have to map explicitly
if currentCyclebleView == "commitFiles" {
currentCyclebleView = "commits"
}
_, height := gui.g.Size()
if gui.State.ScreenMode == SCREEN_FULL || gui.State.ScreenMode == SCREEN_HALF {
vHeights := map[string]int{
"status": 0,
"files": 0,
"branches": 0,
"commits": 0,
"stash": 0,
"options": 0,
}
vHeights[currentCyclebleView] = height - 1
return vHeights
}
usableSpace := height - 7
extraSpace := usableSpace - (usableSpace/3)*3
if height >= 28 {
return map[string]int{
"status": 3,
"files": (usableSpace / 3) + extraSpace,
"branches": usableSpace / 3,
"commits": usableSpace / 3,
"stash": 3,
"options": 1,
}
}
defaultHeight := 3
if height < 21 {
defaultHeight = 1
}
vHeights := map[string]int{
"status": defaultHeight,
"files": defaultHeight,
"branches": defaultHeight,
"commits": defaultHeight,
"stash": defaultHeight,
"options": defaultHeight,
}
vHeights[currentCyclebleView] = height - defaultHeight*4 - 1
return vHeights
}
// layout is called for every screen re-render e.g. when the screen is resized
func (gui *Gui) layout(g *gocui.Gui) error {
g.Highlight = true
@@ -395,50 +496,7 @@ func (gui *Gui) layout(g *gocui.Gui) error {
return nil
}
currView := gui.g.CurrentView()
currentCyclebleView := gui.State.PreviousView
if currView != nil {
viewName := currView.Name()
usePreviouseView := true
for _, view := range cyclableViews {
if view == viewName {
currentCyclebleView = viewName
usePreviouseView = false
break
}
}
if usePreviouseView {
currentCyclebleView = gui.State.PreviousView
}
}
usableSpace := height - 7
extraSpace := usableSpace - (usableSpace/3)*3
vHeights := map[string]int{
"status": 3,
"files": (usableSpace / 3) + extraSpace,
"branches": usableSpace / 3,
"commits": usableSpace / 3,
"stash": 3,
"options": 1,
}
if height < 28 {
defaultHeight := 3
if height < 21 {
defaultHeight = 1
}
vHeights = map[string]int{
"status": defaultHeight,
"files": defaultHeight,
"branches": defaultHeight,
"commits": defaultHeight,
"stash": defaultHeight,
"options": defaultHeight,
}
vHeights[currentCyclebleView] = height - defaultHeight*4 - 1
}
vHeights := gui.getViewHeights()
optionsVersionBoundary := width - max(len(utils.Decolorise(information)), 1)
@@ -448,29 +506,45 @@ func (gui *Gui) layout(g *gocui.Gui) error {
appStatusOptionsBoundary = len(appStatus) + 2
}
panelSpacing := 1
if OverlappingEdges {
panelSpacing = 0
}
_, _ = g.SetViewOnBottom("limit")
g.DeleteView("limit")
textColor := theme.GocuiDefaultTextColor
leftSideWidth := width / 3
var leftSideWidth int
switch gui.State.ScreenMode {
case SCREEN_NORMAL:
leftSideWidth = width / 3
case SCREEN_HALF:
leftSideWidth = width / 2
case SCREEN_FULL:
currentView := gui.g.CurrentView()
if currentView != nil && currentView.Name() == "main" {
leftSideWidth = 0
} else {
leftSideWidth = width - 1
}
}
panelSplitX := width - 1
mainPanelLeft := leftSideWidth + 1
mainPanelRight := width - 1
secondaryPanelLeft := width - 1
secondaryPanelTop := 0
mainPanelBottom := height - 2
if gui.State.SplitMainPanel {
if width < 220 {
if gui.State.ScreenMode == SCREEN_FULL {
mainPanelLeft = 0
panelSplitX = width/2 - 4
mainPanelRight = panelSplitX
secondaryPanelLeft = panelSplitX + 1
} else if width < 220 {
mainPanelBottom = height/2 - 1
secondaryPanelTop = mainPanelBottom + 1
secondaryPanelLeft = leftSideWidth + 1
} else {
units := 5
leftSideWidth = width / units
mainPanelLeft = leftSideWidth + 1
panelSplitX = (1 + ((units - 1) / 2)) * width / units
mainPanelRight = panelSplitX
secondaryPanelLeft = panelSplitX + 1
@@ -500,7 +574,7 @@ func (gui *Gui) layout(g *gocui.Gui) error {
}
}
v, err := g.SetView(main, leftSideWidth+panelSpacing, 0, mainPanelRight, mainPanelBottom, gocui.LEFT)
v, err := g.SetView(main, mainPanelLeft, 0, mainPanelRight, mainPanelBottom, gocui.LEFT)
if err != nil {
if err.Error() != "unknown view" {
return err
@@ -511,11 +585,13 @@ func (gui *Gui) layout(g *gocui.Gui) error {
v.IgnoreCarriageReturns = true
}
hiddenViewOffset := 0
hiddenViewOffset := 9999
hiddenSecondaryPanelOffset := 0
if !gui.State.SplitMainPanel {
hiddenViewOffset = 9999
hiddenSecondaryPanelOffset = hiddenViewOffset
}
secondaryView, err := g.SetView(secondary, secondaryPanelLeft+hiddenViewOffset, hiddenViewOffset+secondaryPanelTop, width-1+hiddenViewOffset, height-2+hiddenViewOffset, gocui.LEFT)
secondaryView, err := g.SetView(secondary, secondaryPanelLeft+hiddenSecondaryPanelOffset, hiddenSecondaryPanelOffset+secondaryPanelTop, width-1+hiddenSecondaryPanelOffset, height-2+hiddenSecondaryPanelOffset, gocui.LEFT)
if err != nil {
if err.Error() != "unknown view" {
return err
@@ -541,7 +617,8 @@ func (gui *Gui) layout(g *gocui.Gui) error {
}
filesView.Highlight = true
filesView.Title = gui.Tr.SLocalize("FilesTitle")
v.FgColor = textColor
filesView.SetOnSelectItem(gui.onSelectItemWrapper(gui.onFilesPanelSearchSelect))
filesView.ContainsList = true
}
branchesView, err := g.SetViewBeneath("branches", "files", vHeights["branches"])
@@ -552,6 +629,8 @@ func (gui *Gui) layout(g *gocui.Gui) error {
branchesView.Title = gui.Tr.SLocalize("BranchesTitle")
branchesView.Tabs = []string{"Local Branches", "Remotes", "Tags"}
branchesView.FgColor = textColor
branchesView.SetOnSelectItem(gui.onSelectItemWrapper(gui.onBranchesPanelSearchSelect))
branchesView.ContainsList = true
}
if v, err := g.SetViewBeneath("commitFiles", "branches", vHeights["commits"]); err != nil {
@@ -560,6 +639,8 @@ func (gui *Gui) layout(g *gocui.Gui) error {
}
v.Title = gui.Tr.SLocalize("CommitFiles")
v.FgColor = textColor
v.SetOnSelectItem(gui.onSelectItemWrapper(gui.onCommitFilesPanelSearchSelect))
v.ContainsList = true
}
commitsView, err := g.SetViewBeneath("commits", "branches", vHeights["commits"])
@@ -570,6 +651,8 @@ func (gui *Gui) layout(g *gocui.Gui) error {
commitsView.Title = gui.Tr.SLocalize("CommitsTitle")
commitsView.Tabs = []string{"Commits", "Reflog"}
commitsView.FgColor = textColor
commitsView.SetOnSelectItem(gui.onSelectItemWrapper(gui.onCommitsPanelSearchSelect))
commitsView.ContainsList = true
}
stashView, err := g.SetViewBeneath("stash", "commits", vHeights["stash"])
@@ -579,6 +662,8 @@ func (gui *Gui) layout(g *gocui.Gui) error {
}
stashView.Title = gui.Tr.SLocalize("StashTitle")
stashView.FgColor = textColor
stashView.SetOnSelectItem(gui.onSelectItemWrapper(gui.onStashPanelSearchSelect))
stashView.ContainsList = true
}
if v, err := g.SetView("options", appStatusOptionsBoundary-1, height-2, optionsVersionBoundary-1, height, 0); err != nil {
@@ -586,8 +671,7 @@ func (gui *Gui) layout(g *gocui.Gui) error {
return err
}
v.Frame = false
userConfig := gui.Config.GetUserConfig()
v.FgColor = theme.GetColor(userConfig.GetStringSlice("gui.theme.optionsTextColor"))
v.FgColor = theme.OptionsColor
}
if gui.getCommitMessageView() == nil {
@@ -620,6 +704,35 @@ func (gui *Gui) layout(g *gocui.Gui) error {
}
}
searchViewOffset := hiddenViewOffset
if gui.State.Searching.isSearching {
searchViewOffset = 0
}
// this view takes up one character. Its only purpose is to show the slash when searching
searchPrefix := "search: "
if searchPrefixView, err := g.SetView("searchPrefix", appStatusOptionsBoundary-1+searchViewOffset, height-2+searchViewOffset, len(searchPrefix)+searchViewOffset, height+searchViewOffset, 0); err != nil {
if err.Error() != "unknown view" {
return err
}
searchPrefixView.BgColor = gocui.ColorDefault
searchPrefixView.FgColor = gocui.ColorGreen
searchPrefixView.Frame = false
gui.setViewContent(gui.g, searchPrefixView, searchPrefix)
}
if searchView, err := g.SetView("search", appStatusOptionsBoundary-1+searchViewOffset+len(searchPrefix), height-2+searchViewOffset, optionsVersionBoundary+searchViewOffset, height+searchViewOffset, 0); err != nil {
if err.Error() != "unknown view" {
return err
}
searchView.BgColor = gocui.ColorDefault
searchView.FgColor = gocui.ColorGreen
searchView.Frame = false
searchView.Editable = true
}
if appStatusView, err := g.SetView("appStatus", -1, height-2, width, height, 0); err != nil {
if err.Error() != "unknown view" {
return err
@@ -827,6 +940,12 @@ func (gui *Gui) Run() error {
return err
}
defer g.Close()
g.OnSearchEscape = gui.onSearchEscape
g.SearchEscapeKey = gui.getKey("universal.return")
g.NextSearchMatchKey = gui.getKey("universal.nextMatch")
g.PrevSearchMatchKey = gui.getKey("universal.prevMatch")
gui.stopChan = make(chan struct{})
g.ASCII = runtime.GOOS == "windows" && runewidth.IsEastAsian()
+69 -1
View File
@@ -317,6 +317,20 @@ func (gui *Gui) GetInitialKeybindings() []*Binding {
Handler: gui.handleEditConfig,
Description: gui.Tr.SLocalize("EditConfig"),
},
{
ViewName: "",
Key: gui.getKey("universal.nextScreenMode"),
Modifier: gocui.ModNone,
Handler: gui.nextScreenMode,
Description: gui.Tr.SLocalize("nextScreenMode"),
},
{
ViewName: "",
Key: gui.getKey("universal.prevScreenMode"),
Modifier: gocui.ModNone,
Handler: gui.prevScreenMode,
Description: gui.Tr.SLocalize("prevScreenMode"),
},
{
ViewName: "status",
Key: gui.getKey("universal.openFile"),
@@ -451,12 +465,19 @@ func (gui *Gui) GetInitialKeybindings() []*Binding {
Description: gui.Tr.SLocalize("fetch"),
},
{
ViewName: "files",
ViewName: "",
Key: gui.getKey("universal.executeCustomCommand"),
Modifier: gocui.ModNone,
Handler: gui.handleCustomCommand,
Description: gui.Tr.SLocalize("executeCustomCommand"),
},
{
ViewName: "files",
Key: gui.getKey("commits.viewResetOptions"),
Modifier: gocui.ModNone,
Handler: gui.handleCreateResetToUpstreamMenu,
Description: gui.Tr.SLocalize("viewResetToUpstreamOptions"),
},
{
ViewName: "branches",
Contexts: []string{"local-branches"},
@@ -537,6 +558,14 @@ func (gui *Gui) GetInitialKeybindings() []*Binding {
Handler: gui.handleFastForward,
Description: gui.Tr.SLocalize("FastForward"),
},
{
ViewName: "branches",
Contexts: []string{"local-branches"},
Key: gui.getKey("commits.viewResetOptions"),
Modifier: gocui.ModNone,
Handler: gui.handleCreateResetToBranchMenu,
Description: gui.Tr.SLocalize("viewResetOptions"),
},
{
ViewName: "branches",
Contexts: []string{"tags"},
@@ -569,6 +598,14 @@ func (gui *Gui) GetInitialKeybindings() []*Binding {
Handler: gui.handleCreateTag,
Description: gui.Tr.SLocalize("createTag"),
},
{
ViewName: "branches",
Contexts: []string{"tags"},
Key: gui.getKey("commits.viewResetOptions"),
Modifier: gocui.ModNone,
Handler: gui.handleCreateResetToTagMenu,
Description: gui.Tr.SLocalize("viewResetOptions"),
},
{
ViewName: "branches",
Key: gui.getKey("universal.nextTab"),
@@ -589,6 +626,14 @@ func (gui *Gui) GetInitialKeybindings() []*Binding {
Handler: gui.handleRemoteBranchesEscape,
Description: gui.Tr.SLocalize("ReturnToRemotesList"),
},
{
ViewName: "branches",
Contexts: []string{"remote-branches"},
Key: gui.getKey("commits.viewResetOptions"),
Modifier: gocui.ModNone,
Handler: gui.handleCreateResetToRemoteBranchMenu,
Description: gui.Tr.SLocalize("viewResetOptions"),
},
{
ViewName: "branches",
Contexts: []string{"remotes"},
@@ -609,6 +654,12 @@ func (gui *Gui) GetInitialKeybindings() []*Binding {
Modifier: gocui.ModNone,
Handler: gui.handlePrevCommitsTab,
},
{
ViewName: "commits",
Key: gui.getKey("universal.startSearch"),
Modifier: gocui.ModNone,
Handler: gui.handleOpenSearchForCommitsPanel,
},
{
ViewName: "commits",
Contexts: []string{"branch-commits"},
@@ -1367,6 +1418,18 @@ func (gui *Gui) GetInitialKeybindings() []*Binding {
Modifier: gocui.ModNone,
Handler: gui.handleCommitFilesClick,
},
{
ViewName: "search",
Key: gocui.KeyEnter,
Modifier: gocui.ModNone,
Handler: gui.handleSearch,
},
{
ViewName: "search",
Key: gui.getKey("universal.return"),
Modifier: gocui.ModNone,
Handler: gui.handleSearchEscape,
},
}
for _, viewName := range []string{"status", "branches", "files", "commits", "commitFiles", "stash", "menu"} {
@@ -1394,6 +1457,11 @@ func (gui *Gui) GetInitialKeybindings() []*Binding {
{ViewName: listView.viewName, Contexts: []string{listView.context}, Key: gocui.MouseWheelDown, Modifier: gocui.ModNone, Handler: listView.handleNextLine},
{ViewName: listView.viewName, Contexts: []string{listView.context}, Key: gocui.MouseLeft, Modifier: gocui.ModNone, Handler: listView.handleClick},
}...)
// we need a specific keybinding for the commits panel beacuse it usually lazyloads commits
if listView.viewName != "commits" {
bindings = append(bindings, &Binding{ViewName: listView.viewName, Contexts: []string{listView.context}, Key: gui.getKey("universal.startSearch"), Modifier: gocui.ModNone, Handler: gui.handleOpenSearch})
}
}
return bindings
+35 -7
View File
@@ -8,6 +8,12 @@ import (
"github.com/jesseduffield/lazygit/pkg/utils"
)
type menuItem struct {
displayString string
displayStrings []string
onPress func() error
}
// list panel functions
func (gui *Gui) handleMenuSelect(g *gocui.Gui, v *gocui.View) error {
@@ -38,27 +44,49 @@ func (gui *Gui) handleMenuClose(g *gocui.Gui, v *gocui.View) error {
return gui.returnFocus(g, v)
}
func (gui *Gui) createMenu(title string, items interface{}, itemCount int, handlePress func(int) error) error {
isFocused := gui.g.CurrentView().Name() == "menu"
gui.State.MenuItemCount = itemCount
list, err := utils.RenderList(items, isFocused)
if err != nil {
return err
type createMenuOptions struct {
showCancel bool
}
func (gui *Gui) createMenu(title string, items []*menuItem, createMenuOptions createMenuOptions) error {
if createMenuOptions.showCancel {
// this is mutative but I'm okay with that for now
items = append(items, &menuItem{
displayStrings: []string{gui.Tr.SLocalize("cancel")},
onPress: func() error {
return nil
},
})
}
gui.State.MenuItemCount = len(items)
stringArrays := make([][]string, len(items))
for i, item := range items {
if item.displayStrings == nil {
stringArrays[i] = []string{item.displayString}
} else {
stringArrays[i] = item.displayStrings
}
}
list := utils.RenderDisplayStrings(stringArrays)
x0, y0, x1, y1 := gui.getConfirmationPanelDimensions(gui.g, false, list)
menuView, _ := gui.g.SetView("menu", x0, y0, x1, y1, 0)
menuView.Title = title
menuView.FgColor = theme.GocuiDefaultTextColor
menuView.ContainsList = true
menuView.Clear()
fmt.Fprint(menuView, list)
gui.State.Panels.Menu.SelectedLine = 0
wrappedHandlePress := func(g *gocui.Gui, v *gocui.View) error {
selectedLine := gui.State.Panels.Menu.SelectedLine
if err := handlePress(selectedLine); err != nil {
if err := items[selectedLine].onPress(); err != nil {
return err
}
if _, err := gui.g.View("menu"); err == nil {
if _, err := gui.g.SetViewOnBottom("menu"); err != nil {
return err
+16 -14
View File
@@ -3,8 +3,6 @@ package gui
import (
"strings"
"github.com/go-errors/errors"
"github.com/jesseduffield/gocui"
"github.com/jesseduffield/lazygit/pkg/utils"
)
@@ -38,19 +36,23 @@ func (gui *Gui) getBindings(v *gocui.View) []*Binding {
func (gui *Gui) handleCreateOptionsMenu(g *gocui.Gui, v *gocui.View) error {
bindings := gui.getBindings(v)
handleMenuPress := func(index int) error {
if bindings[index].Key == nil {
return nil
menuItems := make([]*menuItem, len(bindings))
for i, binding := range bindings {
innerBinding := binding // note to self, never close over loop variables
menuItems[i] = &menuItem{
displayStrings: []string{GetKeyDisplay(innerBinding.Key), innerBinding.Description},
onPress: func() error {
if innerBinding.Key == nil {
return nil
}
if err := gui.handleMenuClose(g, v); err != nil {
return err
}
return innerBinding.Handler(g, v)
},
}
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(strings.Title(gui.Tr.SLocalize("menu")), bindings, len(bindings), handleMenuPress)
return gui.createMenu(strings.Title(gui.Tr.SLocalize("menu")), menuItems, createMenuOptions{})
}
+20 -25
View File
@@ -6,48 +6,43 @@ import (
"github.com/jesseduffield/gocui"
)
type patchMenuOption struct {
displayName string
function func() error
}
// GetDisplayStrings is a function.
func (o *patchMenuOption) GetDisplayStrings(isFocused bool) []string {
return []string{o.displayName}
}
func (gui *Gui) handleCreatePatchOptionsMenu(g *gocui.Gui, v *gocui.View) error {
if !gui.GitCommand.PatchManager.CommitSelected() {
return gui.createErrorPanel(gui.g, gui.Tr.SLocalize("NoPatchError"))
}
options := []*patchMenuOption{
{displayName: fmt.Sprintf("remove patch from original commit (%s)", gui.GitCommand.PatchManager.CommitSha), function: gui.handleDeletePatchFromCommit},
{displayName: "pull patch out into index", function: gui.handlePullPatchIntoWorkingTree},
{displayName: "reset patch", function: gui.handleResetPatch},
menuItems := []*menuItem{
{
displayString: fmt.Sprintf("remove patch from original commit (%s)", gui.GitCommand.PatchManager.CommitSha),
onPress: gui.handleDeletePatchFromCommit,
},
{
displayString: "pull patch out into index",
onPress: gui.handlePullPatchIntoWorkingTree,
},
{
displayString: "reset patch",
onPress: gui.handleResetPatch,
},
}
selectedCommit := gui.getSelectedCommit(gui.g)
if selectedCommit != nil && gui.GitCommand.PatchManager.CommitSha != selectedCommit.Sha {
// adding this option to index 1
options = append(
options[:1],
menuItems = append(
menuItems[:1],
append(
[]*patchMenuOption{
[]*menuItem{
{
displayName: fmt.Sprintf("move patch to selected commit (%s)", selectedCommit.Sha),
function: gui.handleMovePatchToSelectedCommit,
displayString: fmt.Sprintf("move patch to selected commit (%s)", selectedCommit.Sha),
onPress: gui.handleMovePatchToSelectedCommit,
},
}, options[1:]...,
}, menuItems[1:]...,
)...,
)
}
handleMenuPress := func(index int) error {
return options[index].function()
}
return gui.createMenu(gui.Tr.SLocalize("PatchOptionsTitle"), options, len(options), handleMenuPress)
return gui.createMenu(gui.Tr.SLocalize("PatchOptionsTitle"), menuItems, createMenuOptions{showCancel: true})
}
func (gui *Gui) getPatchCommitIndex() int {
+10 -22
View File
@@ -7,33 +7,21 @@ import (
"github.com/jesseduffield/gocui"
)
type option struct {
value string
}
// GetDisplayStrings is a function.
func (r *option) GetDisplayStrings(isFocused bool) []string {
return []string{r.value}
}
func (gui *Gui) handleCreateRebaseOptionsMenu(g *gocui.Gui, v *gocui.View) error {
options := []*option{
{value: "continue"},
{value: "abort"},
}
options := []string{"continue", "abort"}
if gui.State.WorkingTreeState == "rebasing" {
options = append(options, &option{value: "skip"})
options = append(options, "skip")
}
options = append(options, &option{value: "cancel"})
handleMenuPress := func(index int) error {
command := options[index].value
if command == "cancel" {
return nil
menuItems := make([]*menuItem, len(options))
for i, option := range options {
menuItems[i] = &menuItem{
displayString: option,
onPress: func() error {
return gui.genericMergeCommand(option)
},
}
return gui.genericMergeCommand(command)
}
var title string
@@ -43,7 +31,7 @@ func (gui *Gui) handleCreateRebaseOptionsMenu(g *gocui.Gui, v *gocui.View) error
title = gui.Tr.SLocalize("RebaseOptionsTitle")
}
return gui.createMenu(title, options, len(options), handleMenuPress)
return gui.createMenu(title, menuItems, createMenuOptions{showCancel: true})
}
func (gui *Gui) genericMergeCommand(command string) error {
+21 -28
View File
@@ -10,41 +10,34 @@ import (
"github.com/jesseduffield/lazygit/pkg/utils"
)
type recentRepo struct {
path string
}
// GetDisplayStrings returns the path from a recent repo.
func (r *recentRepo) GetDisplayStrings(isFocused bool) []string {
yellow := color.New(color.FgMagenta)
base := filepath.Base(r.path)
path := yellow.Sprint(r.path)
return []string{base, path}
}
func (gui *Gui) handleCreateRecentReposMenu(g *gocui.Gui, v *gocui.View) error {
recentRepoPaths := gui.Config.GetAppState().RecentRepos
reposCount := utils.Min(len(recentRepoPaths), 20)
yellow := color.New(color.FgMagenta)
// we won't show the current repo hence the -1
recentRepos := make([]*recentRepo, reposCount-1)
menuItems := make([]*menuItem, reposCount-1)
for i, path := range recentRepoPaths[1:reposCount] {
recentRepos[i] = &recentRepo{path: path}
innerPath := path
menuItems[i] = &menuItem{
displayStrings: []string{
filepath.Base(innerPath),
yellow.Sprint(innerPath),
},
onPress: func() error {
if err := os.Chdir(innerPath); err != nil {
return err
}
newGitCommand, err := commands.NewGitCommand(gui.Log, gui.OSCommand, gui.Tr, gui.Config)
if err != nil {
return err
}
gui.GitCommand = newGitCommand
return gui.Errors.ErrSwitchRepo
},
}
}
handleMenuPress := func(index int) error {
repo := recentRepos[index]
if err := os.Chdir(repo.path); err != nil {
return err
}
newGitCommand, err := commands.NewGitCommand(gui.Log, gui.OSCommand, gui.Tr, gui.Config)
if err != nil {
return err
}
gui.GitCommand = newGitCommand
return gui.Errors.ErrSwitchRepo
}
return gui.createMenu(gui.Tr.SLocalize("RecentRepos"), recentRepos, len(recentRepos), handleMenuPress)
return gui.createMenu(gui.Tr.SLocalize("RecentRepos"), menuItems, createMenuOptions{showCancel: true})
}
// updateRecentRepoList registers the fact that we opened lazygit in this repo,
+6
View File
@@ -95,3 +95,9 @@ func (gui *Gui) handleCheckoutReflogCommit(g *gocui.Gui, v *gocui.View) error {
return nil
}
func (gui *Gui) handleCreateReflogResetMenu(g *gocui.Gui, v *gocui.View) error {
commit := gui.getSelectedReflogCommit()
return gui.createResetMenu(commit.Sha)
}
-60
View File
@@ -1,60 +0,0 @@
package gui
import (
"fmt"
"github.com/fatih/color"
"github.com/jesseduffield/gocui"
)
type reflogResetOption struct {
handler func() error
description string
command string
}
// GetDisplayStrings is a function.
func (r *reflogResetOption) GetDisplayStrings(isFocused bool) []string {
return []string{r.description, color.New(color.FgRed).Sprint(r.command)}
}
func (gui *Gui) handleCreateReflogResetMenu(g *gocui.Gui, v *gocui.View) error {
commit := gui.getSelectedReflogCommit()
resetFunction := func(reset func(string) error) func() error {
return func() error {
if err := reset(commit.Sha); err != nil {
return gui.createErrorPanel(gui.g, err.Error())
}
gui.State.Panels.ReflogCommits.SelectedLine = 0
return gui.refreshSidePanels(gui.g)
}
}
options := []*reflogResetOption{
{
description: gui.Tr.SLocalize("hardReset"),
command: fmt.Sprintf("reset --hard %s", commit.Sha),
handler: resetFunction(gui.GitCommand.ResetHard),
},
{
description: gui.Tr.SLocalize("softReset"),
command: fmt.Sprintf("reset --soft %s", commit.Sha),
handler: resetFunction(gui.GitCommand.ResetSoft),
},
{
description: gui.Tr.SLocalize("cancel"),
handler: func() error {
return nil
},
},
}
handleMenuPress := func(index int) error {
return options[index].handler()
}
return gui.createMenu("", options, len(options), handleMenuPress)
}
+9
View File
@@ -132,3 +132,12 @@ func (gui *Gui) handleSetBranchUpstream(g *gocui.Gui, v *gocui.View) error {
return gui.refreshSidePanels(gui.g)
}, nil)
}
func (gui *Gui) handleCreateResetToRemoteBranchMenu(g *gocui.Gui, v *gocui.View) error {
selectedBranch := gui.getSelectedRemoteBranch()
if selectedBranch == nil {
return nil
}
return gui.createResetMenu(fmt.Sprintf("%s/%s", selectedBranch.RemoteName, selectedBranch.Name))
}
+46
View File
@@ -0,0 +1,46 @@
package gui
import (
"fmt"
"github.com/fatih/color"
)
func (gui *Gui) createResetMenu(ref string) error {
strengths := []string{"soft", "mixed", "hard"}
menuItems := make([]*menuItem, len(strengths))
for i, strength := range strengths {
innerStrength := strength
menuItems[i] = &menuItem{
displayStrings: []string{
fmt.Sprintf("%s reset", strength),
color.New(color.FgRed).Sprint(
fmt.Sprintf("reset --%s %s", strength, ref),
),
},
onPress: func() error {
if err := gui.GitCommand.ResetToCommit(ref, innerStrength); err != nil {
return gui.createErrorPanel(gui.g, err.Error())
}
gui.switchCommitsPanelContext("branch-commits")
gui.State.Panels.Commits.SelectedLine = 0
gui.State.Panels.ReflogCommits.SelectedLine = 0
if err := gui.refreshCommits(gui.g); err != nil {
return err
}
if err := gui.refreshFiles(); err != nil {
return err
}
if err := gui.resetOrigin(gui.getCommitsView()); err != nil {
return err
}
return gui.handleCommitSelect(gui.g, gui.getCommitsView())
},
}
}
return gui.createMenu(fmt.Sprintf("%s %s", gui.Tr.SLocalize("resetTo"), ref), menuItems, createMenuOptions{showCancel: true})
}
+91
View File
@@ -0,0 +1,91 @@
package gui
import (
"fmt"
"github.com/jesseduffield/gocui"
"github.com/jesseduffield/lazygit/pkg/theme"
"github.com/jesseduffield/lazygit/pkg/utils"
)
func (gui *Gui) handleOpenSearch(g *gocui.Gui, v *gocui.View) error {
gui.State.Searching.isSearching = true
gui.State.Searching.view = v
gui.renderString(gui.g, "search", "")
gui.switchFocus(gui.g, v, gui.getSearchView())
return nil
}
func (gui *Gui) handleSearch(g *gocui.Gui, v *gocui.View) error {
gui.State.Searching.searchString = gui.getSearchView().Buffer()
gui.switchFocus(gui.g, nil, gui.State.Searching.view)
if err := gui.State.Searching.view.Search(gui.State.Searching.searchString); err != nil {
return err
}
return nil
}
func (gui *Gui) onSelectItemWrapper(innerFunc func(int) error) func(int, int, int) error {
return func(y int, index int, total int) error {
if total == 0 {
gui.renderString(
gui.g,
"search",
fmt.Sprintf(
"no matches for '%s' %s",
gui.State.Searching.searchString,
utils.ColoredString(
fmt.Sprintf("%s: exit search mode", gui.getKeyDisplay("universal.return")),
theme.OptionsFgColor,
),
),
)
return nil
}
gui.renderString(
gui.g,
"search",
fmt.Sprintf(
"matches for '%s' (%d of %d) %s",
gui.State.Searching.searchString,
index+1,
total,
utils.ColoredString(
fmt.Sprintf(
"%s: next match, %s: previous match, %s: exit search mode",
gui.getKeyDisplay("universal.nextMatch"),
gui.getKeyDisplay("universal.prevMatch"),
gui.getKeyDisplay("universal.return"),
),
theme.OptionsFgColor,
),
),
)
if err := innerFunc(y); err != nil {
return err
}
return nil
}
}
func (gui *Gui) onSearchEscape() error {
gui.State.Searching.isSearching = false
if gui.State.Searching.view != nil {
gui.State.Searching.view.ClearSearch()
gui.State.Searching.view = nil
}
return nil
}
func (gui *Gui) handleSearchEscape(g *gocui.Gui, v *gocui.View) error {
if err := gui.switchFocus(gui.g, nil, gui.State.Searching.view); err != nil {
return err
}
gui.onSearchEscape()
return nil
}
+5
View File
@@ -122,3 +122,8 @@ func (gui *Gui) handleStashSave(stashFunc func(message string) error) error {
return gui.refreshFiles()
})
}
func (gui *Gui) onStashPanelSearchSelect(selectedLine int) error {
gui.State.Panels.Stash.SelectedLine = selectedLine
return gui.handleStashEntrySelect(gui.g, gui.getStashView())
}
+23 -2
View File
@@ -106,7 +106,13 @@ func (gui *Gui) handleDeleteTag(g *gocui.Gui, v *gocui.View) error {
if err := gui.GitCommand.DeleteTag(tag.Name); err != nil {
return gui.createErrorPanel(gui.g, err.Error())
}
return gui.refreshTags()
if err := gui.refreshCommits(g); err != nil {
return gui.createErrorPanel(g, err.Error())
}
if err := gui.refreshTags(); err != nil {
return gui.createErrorPanel(g, err.Error())
}
return nil
}, nil)
}
@@ -137,6 +143,21 @@ func (gui *Gui) handleCreateTag(g *gocui.Gui, v *gocui.View) error {
if err := gui.GitCommand.CreateLightweightTag(v.Buffer(), ""); err != nil {
return gui.createErrorPanel(gui.g, err.Error())
}
return gui.refreshTags()
if err := gui.refreshCommits(g); err != nil {
return gui.createErrorPanel(g, err.Error())
}
if err := gui.refreshTags(); err != nil {
return gui.createErrorPanel(g, err.Error())
}
return nil
})
}
func (gui *Gui) handleCreateResetToTagMenu(g *gocui.Gui, v *gocui.View) error {
tag := gui.getSelectedTag()
if tag == nil {
return nil
}
return gui.createResetMenu(tag.Name)
}
+11 -26
View File
@@ -135,6 +135,8 @@ func (gui *Gui) newLineFocused(g *gocui.Gui, v *gocui.View) error {
}
v.Highlight = false
return nil
case "search":
return nil
default:
panic(gui.Tr.SLocalize("NoViewMachingNewLineFocusedSwitchStatement"))
}
@@ -218,32 +220,7 @@ func (gui *Gui) resetOrigin(v *gocui.View) error {
// if the cursor down past the last item, move it to the last line
func (gui *Gui) focusPoint(cx int, cy int, lineCount int, v *gocui.View) error {
if cy < 0 || cy > lineCount {
return nil
}
ox, oy := v.Origin()
_, height := v.Size()
ly := height - 1
if ly == -1 {
ly = 0
}
// if line is above origin, move origin and set cursor to zero
// if line is below origin + height, move origin and set cursor to max
// otherwise set cursor to value - origin
if ly > lineCount {
_ = v.SetCursor(cx, cy)
_ = v.SetOrigin(ox, 0)
} else if cy < oy {
_ = v.SetCursor(cx, 0)
_ = v.SetOrigin(ox, cy)
} else if cy > oy+ly {
_ = v.SetCursor(cx, ly)
_ = v.SetOrigin(ox, cy-ly)
} else {
_ = v.SetCursor(cx, cy-oy)
}
v.FocusPoint(cx, cy)
return nil
}
@@ -268,6 +245,9 @@ func (gui *Gui) renderString(g *gocui.Gui, viewName, s string) error {
if err := v.SetOrigin(0, 0); err != nil {
return err
}
if err := v.SetCursor(0, 0); err != nil {
return err
}
return gui.setViewContent(gui.g, v, s)
})
return nil
@@ -333,6 +313,11 @@ func (gui *Gui) getMenuView() *gocui.View {
return v
}
func (gui *Gui) getSearchView() *gocui.View {
v, _ := gui.g.View("search")
return v
}
func (gui *Gui) trimmedContent(v *gocui.View) string {
return strings.TrimSpace(v.Buffer())
}
+37 -44
View File
@@ -5,23 +5,16 @@ import (
"github.com/jesseduffield/gocui"
)
type workspaceResetOption struct {
handler func() error
description string
command string
}
// GetDisplayStrings is a function.
func (r *workspaceResetOption) GetDisplayStrings(isFocused bool) []string {
return []string{r.description, color.New(color.FgRed).Sprint(r.command)}
}
func (gui *Gui) handleCreateResetMenu(g *gocui.Gui, v *gocui.View) error {
options := []*workspaceResetOption{
red := color.New(color.FgRed)
menuItems := []*menuItem{
{
description: gui.Tr.SLocalize("discardAllChangesToAllFiles"),
command: "reset --hard HEAD && git clean -fd",
handler: func() error {
displayStrings: []string{
gui.Tr.SLocalize("discardAllChangesToAllFiles"),
red.Sprint("reset --hard HEAD && git clean -fd"),
},
onPress: func() error {
if err := gui.GitCommand.ResetAndClean(); err != nil {
return gui.createErrorPanel(gui.g, err.Error())
}
@@ -30,9 +23,11 @@ func (gui *Gui) handleCreateResetMenu(g *gocui.Gui, v *gocui.View) error {
},
},
{
description: gui.Tr.SLocalize("discardAnyUnstagedChanges"),
command: "git checkout -- .",
handler: func() error {
displayStrings: []string{
gui.Tr.SLocalize("discardAnyUnstagedChanges"),
red.Sprint("git checkout -- ."),
},
onPress: func() error {
if err := gui.GitCommand.DiscardAnyUnstagedFileChanges(); err != nil {
return gui.createErrorPanel(gui.g, err.Error())
}
@@ -41,9 +36,11 @@ func (gui *Gui) handleCreateResetMenu(g *gocui.Gui, v *gocui.View) error {
},
},
{
description: gui.Tr.SLocalize("discardUntrackedFiles"),
command: "git clean -fd",
handler: func() error {
displayStrings: []string{
gui.Tr.SLocalize("discardUntrackedFiles"),
red.Sprint("git clean -fd"),
},
onPress: func() error {
if err := gui.GitCommand.RemoveUntrackedFiles(); err != nil {
return gui.createErrorPanel(gui.g, err.Error())
}
@@ -52,9 +49,11 @@ func (gui *Gui) handleCreateResetMenu(g *gocui.Gui, v *gocui.View) error {
},
},
{
description: gui.Tr.SLocalize("softReset"),
command: "git reset --soft HEAD",
handler: func() error {
displayStrings: []string{
gui.Tr.SLocalize("softReset"),
red.Sprint("git reset --soft HEAD"),
},
onPress: func() error {
if err := gui.GitCommand.ResetSoft("HEAD"); err != nil {
return gui.createErrorPanel(gui.g, err.Error())
}
@@ -63,10 +62,12 @@ func (gui *Gui) handleCreateResetMenu(g *gocui.Gui, v *gocui.View) error {
},
},
{
description: gui.Tr.SLocalize("hardReset"),
command: "git reset --hard HEAD",
handler: func() error {
if err := gui.GitCommand.ResetHard("HEAD"); err != nil {
displayStrings: []string{
"mixed reset",
red.Sprint("git reset --mixed HEAD"),
},
onPress: func() error {
if err := gui.GitCommand.ResetSoft("HEAD"); err != nil {
return gui.createErrorPanel(gui.g, err.Error())
}
@@ -74,27 +75,19 @@ func (gui *Gui) handleCreateResetMenu(g *gocui.Gui, v *gocui.View) error {
},
},
{
description: gui.Tr.SLocalize("hardResetUpstream"),
command: "git reset --hard @{upstream}",
handler: func() error {
if err := gui.GitCommand.ResetHard("@{upstream}"); err != nil {
displayStrings: []string{
gui.Tr.SLocalize("hardReset"),
red.Sprint("git reset --hard HEAD"),
},
onPress: func() error {
if err := gui.GitCommand.ResetHard("HEAD"); err != nil {
return gui.createErrorPanel(gui.g, err.Error())
}
return gui.refreshSidePanels(gui.g)
},
},
{
description: gui.Tr.SLocalize("cancel"),
handler: func() error {
return nil
return gui.refreshFiles()
},
},
}
handleMenuPress := func(index int) error {
return options[index].handler()
}
return gui.createMenu("", options, len(options), handleMenuPress)
return gui.createMenu("", menuItems, createMenuOptions{showCancel: true})
}
+9
View File
@@ -942,6 +942,15 @@ func addEnglish(i18nObject *i18n.Bundle) error {
}, &i18n.Message{
ID: "IgnoreTrackedPrompt",
Other: "Are you sure you want to ignore a tracked file?",
}, &i18n.Message{
ID: "viewResetToUpstreamOptions",
Other: "view upstream reset options",
}, &i18n.Message{
ID: "nextScreenMode",
Other: "next screen mode (normal/half/fullscreen)",
}, &i18n.Message{
ID: "prevScreenMode",
Other: "prev screen mode",
},
)
}
+1 -1
View File
@@ -59,7 +59,7 @@ func (m *ViewBufferManager) NewCmdTask(cmd *exec.Cmd, linesToRead int) func(chan
go func() {
<-stop
if cmd.ProcessState.ExitCode() == -1 {
if cmd.ProcessState == nil {
if err := kill(cmd); err != nil {
m.Log.Warn(err)
}
+75 -33
View File
@@ -20,12 +20,22 @@ var (
// InactiveBorderColor is the border color of the inactive active frames
InactiveBorderColor gocui.Attribute
// SelectedLineBgColor is the background color for the selected line
SelectedLineBgColor color.Attribute
OptionsFgColor color.Attribute
OptionsColor gocui.Attribute
)
// UpdateTheme updates all theme variables
func UpdateTheme(userConfig *viper.Viper) {
ActiveBorderColor = getColor(userConfig.GetStringSlice("gui.theme.activeBorderColor"))
InactiveBorderColor = getColor(userConfig.GetStringSlice("gui.theme.inactiveBorderColor"))
ActiveBorderColor = GetGocuiColor(userConfig.GetStringSlice("gui.theme.activeBorderColor"))
InactiveBorderColor = GetGocuiColor(userConfig.GetStringSlice("gui.theme.inactiveBorderColor"))
SelectedLineBgColor = GetBgColor(userConfig.GetStringSlice("gui.theme.selectedLineBgColor"))
OptionsColor = GetGocuiColor(userConfig.GetStringSlice("gui.theme.optionsTextColor"))
OptionsFgColor = GetFgColor(userConfig.GetStringSlice("gui.theme.optionsTextColor"))
isLightTheme := userConfig.GetBool("gui.theme.lightTheme")
if isLightTheme {
@@ -39,8 +49,8 @@ func UpdateTheme(userConfig *viper.Viper) {
}
}
// getAttribute gets the gocui color attribute from the string
func getAttribute(key string) gocui.Attribute {
// GetAttribute gets the gocui color attribute from the string
func GetGocuiAttribute(key string) gocui.Attribute {
colorMap := map[string]gocui.Attribute{
"default": gocui.ColorDefault,
"black": gocui.ColorBlack,
@@ -62,43 +72,75 @@ func getAttribute(key string) gocui.Attribute {
return gocui.ColorWhite
}
// getColor bitwise OR's a list of attributes obtained via the given keys
func getColor(keys []string) gocui.Attribute {
// GetFgAttribute gets the color foreground attribute from the string
func GetFgAttribute(key string) color.Attribute {
colorMap := map[string]color.Attribute{
"default": color.FgWhite,
"black": color.FgBlack,
"red": color.FgRed,
"green": color.FgGreen,
"yellow": color.FgYellow,
"blue": color.FgBlue,
"magenta": color.FgMagenta,
"cyan": color.FgCyan,
"white": color.FgWhite,
"bold": color.Bold,
"reverse": color.ReverseVideo,
"underline": color.Underline,
}
value, present := colorMap[key]
if present {
return value
}
return color.FgWhite
}
// GetBgAttribute gets the color background attribute from the string
func GetBgAttribute(key string) color.Attribute {
colorMap := map[string]color.Attribute{
"default": color.BgWhite,
"black": color.BgBlack,
"red": color.BgRed,
"green": color.BgGreen,
"yellow": color.BgYellow,
"blue": color.BgBlue,
"magenta": color.BgMagenta,
"cyan": color.BgCyan,
"white": color.BgWhite,
"bold": color.Bold,
"reverse": color.ReverseVideo,
"underline": color.Underline,
}
value, present := colorMap[key]
if present {
return value
}
return color.FgWhite
}
// GetGocuiColor bitwise OR's a list of attributes obtained via the given keys
func GetGocuiColor(keys []string) gocui.Attribute {
var attribute gocui.Attribute
for _, key := range keys {
attribute |= getAttribute(key)
attribute |= GetGocuiAttribute(key)
}
return attribute
}
// GetAttribute gets the gocui color attribute from the string
func GetAttribute(key string) gocui.Attribute {
colorMap := map[string]gocui.Attribute{
"default": gocui.ColorDefault,
"black": gocui.ColorBlack,
"red": gocui.ColorRed,
"green": gocui.ColorGreen,
"yellow": gocui.ColorYellow,
"blue": gocui.ColorBlue,
"magenta": gocui.ColorMagenta,
"cyan": gocui.ColorCyan,
"white": gocui.ColorWhite,
"bold": gocui.AttrBold,
"reverse": gocui.AttrReverse,
"underline": gocui.AttrUnderline,
}
value, present := colorMap[key]
if present {
return value
}
return gocui.ColorWhite
}
// GetColor bitwise OR's a list of attributes obtained via the given keys
func GetColor(keys []string) gocui.Attribute {
var attribute gocui.Attribute
func GetBgColor(keys []string) color.Attribute {
var attribute color.Attribute
for _, key := range keys {
attribute |= GetAttribute(key)
attribute |= GetBgAttribute(key)
}
return attribute
}
// GetColor bitwise OR's a list of attributes obtained via the given keys
func GetFgColor(keys []string) color.Attribute {
var attribute color.Attribute
for _, key := range keys {
attribute |= GetFgAttribute(key)
}
return attribute
}
+46 -8
View File
@@ -151,14 +151,14 @@ func renderDisplayableList(items []Displayable, isFocused bool) (string, error)
stringArrays := getDisplayStringArrays(items, isFocused)
if !displayArraysAligned(stringArrays) {
return "", errors.New("Each item must return the same number of strings to display")
}
return RenderDisplayStrings(stringArrays), nil
}
padWidths := getPadWidths(stringArrays)
paddedDisplayStrings := getPaddedDisplayStrings(stringArrays, padWidths)
func RenderDisplayStrings(displayStringsArr [][]string) string {
padWidths := getPadWidths(displayStringsArr)
paddedDisplayStrings := getPaddedDisplayStrings(displayStringsArr, padWidths)
return strings.Join(paddedDisplayStrings, "\n"), nil
return strings.Join(paddedDisplayStrings, "\n")
}
// Decolorise strips a string of color
@@ -168,10 +168,16 @@ func Decolorise(str string) string {
}
func getPadWidths(stringArrays [][]string) []int {
if len(stringArrays[0]) <= 1 {
maxWidth := 0
for _, stringArray := range stringArrays {
if len(stringArray) > maxWidth {
maxWidth = len(stringArray)
}
}
if maxWidth-1 < 0 {
return []int{}
}
padWidths := make([]int, len(stringArrays[0])-1)
padWidths := make([]int, maxWidth-1)
for i := range padWidths {
for _, strings := range stringArrays {
uncoloredString := Decolorise(strings[i])
@@ -190,8 +196,14 @@ func getPaddedDisplayStrings(stringArrays [][]string, padWidths []int) []string
continue
}
for j, padWidth := range padWidths {
if len(stringArray)-1 < j {
continue
}
paddedDisplayStrings[i] += WithPadding(stringArray[j], padWidth) + " "
}
if len(stringArray)-1 < len(padWidths) {
continue
}
paddedDisplayStrings[i] += stringArray[len(padWidths)]
}
return paddedDisplayStrings
@@ -310,3 +322,29 @@ func ModuloWithWrap(n, max int) int {
return n
}
}
// NextIntInCycle returns the next int in a slice, returning to the first index if we've reached the end
func NextIntInCycle(sl []int, current int) int {
for i, val := range sl {
if val == current {
if i == len(sl)-1 {
return sl[0]
}
return sl[i+1]
}
}
return sl[0]
}
// PrevIntInCycle returns the prev int in a slice, returning to the first index if we've reached the end
func PrevIntInCycle(sl []int, current int) int {
for i, val := range sl {
if val == current {
if i > 0 {
return sl[i-1]
}
return sl[len(sl)-1]
}
}
return sl[len(sl)-1]
}
+1 -1
View File
@@ -276,8 +276,8 @@ func TestRenderDisplayableList(t *testing.T) {
Displayable(&myDisplayable{[]string{"b", "c"}}),
},
false,
"a \nb c",
"",
"Each item must return the same number of strings to display",
},
{
[]Displayable{
-14
View File
@@ -6,7 +6,6 @@ package gocui
import (
"strconv"
"sync"
"github.com/go-errors/errors"
)
@@ -17,7 +16,6 @@ type escapeInterpreter struct {
csiParam []string
curFgColor, curBgColor Attribute
mode OutputMode
mutex sync.Mutex
}
type escapeState int
@@ -37,9 +35,6 @@ var (
// runes in case of error will output the non-parsed runes as a string.
func (ei *escapeInterpreter) runes() []rune {
ei.mutex.Lock()
defer ei.mutex.Unlock()
switch ei.state {
case stateNone:
return []rune{0x1b}
@@ -72,9 +67,6 @@ func newEscapeInterpreter(mode OutputMode) *escapeInterpreter {
// reset sets the escapeInterpreter in initial state.
func (ei *escapeInterpreter) reset() {
ei.mutex.Lock()
defer ei.mutex.Unlock()
ei.state = stateNone
ei.curFgColor = ColorDefault
ei.curBgColor = ColorDefault
@@ -85,9 +77,6 @@ func (ei *escapeInterpreter) reset() {
// of an escape sequence, and as such should not be printed verbatim. Otherwise,
// it's not an escape sequence.
func (ei *escapeInterpreter) parseOne(ch rune) (isEscape bool, err error) {
ei.mutex.Lock()
defer ei.mutex.Unlock()
// Sanity checks
if len(ei.csiParam) > 20 {
return false, errCSITooLong
@@ -191,9 +180,6 @@ func (ei *escapeInterpreter) outputNormal() error {
// 0x11 - 0xe8: 216 different colors
// 0xe9 - 0x1ff: 24 different shades of grey
func (ei *escapeInterpreter) output256() error {
ei.mutex.Lock()
defer ei.mutex.Unlock()
if len(ei.csiParam) < 3 {
return ei.outputNormal()
}
+62
View File
@@ -6,6 +6,7 @@ package gocui
import (
standardErrors "errors"
"fmt"
"strings"
"sync"
"time"
@@ -94,6 +95,12 @@ type Gui struct {
// tickingMutex ensures we don't have two loops ticking. The point of 'ticking'
// is to refresh the gui rapidly so that loader characters can be animated.
tickingMutex sync.Mutex
OnSearchEscape func() error
// these keys must either be of type Key of rune
SearchEscapeKey interface{}
NextSearchMatchKey interface{}
PrevSearchMatchKey interface{}
}
// NewGui returns a new Gui object with a given output mode.
@@ -124,6 +131,11 @@ func NewGui(mode OutputMode, supportOverlaps bool) (*Gui, error) {
// view edges
g.SupportOverlaps = supportOverlaps
// default keys for when searching strings in a view
g.SearchEscapeKey = KeyEsc
g.NextSearchMatchKey = 'n'
g.PrevSearchMatchKey = 'N'
return g, nil
}
@@ -531,6 +543,11 @@ func (g *Gui) flush() error {
return err
}
}
if v.ContainsList {
if err := g.drawListFooter(v, fgColor, bgColor); err != nil {
return err
}
}
}
if err := g.draw(v); err != nil {
return err
@@ -718,6 +735,34 @@ func (g *Gui) drawSubtitle(v *View, fgColor, bgColor Attribute) error {
return nil
}
// drawListFooter draws the footer of a list view, showing something like '1 of 10'
func (g *Gui) drawListFooter(v *View, fgColor, bgColor Attribute) error {
if len(v.lines) == 0 {
return nil
}
message := fmt.Sprintf("%d of %d", v.cy+v.oy+1, len(v.lines))
if v.y1 < 0 || v.y1 >= g.maxY {
return nil
}
start := v.x1 - 1 - len(message)
if start < v.x0 {
return nil
}
for i, ch := range message {
x := start + i
if x >= v.x1 {
break
}
if err := g.SetRune(x, v.y1, ch, fgColor, bgColor); err != nil {
return err
}
}
return nil
}
// draw manages the cursor and calls the draw function of a view.
func (g *Gui) draw(v *View) error {
if g.Cursor {
@@ -803,6 +848,23 @@ func (g *Gui) execKeybindings(v *View, ev *termbox.Event) (matched bool, err err
var globalKb *keybinding
var matchingParentViewKb *keybinding
// if we're searching, and we've hit n/N/Esc, we ignore the default keybinding
if v.IsSearching() && Modifier(ev.Mod) == ModNone {
if eventMatchesKey(ev, g.NextSearchMatchKey) {
return true, v.gotoNextMatch()
} else if eventMatchesKey(ev, g.PrevSearchMatchKey) {
return true, v.gotoPreviousMatch()
} else if eventMatchesKey(ev, g.SearchEscapeKey) {
v.searcher.clearSearch()
if g.OnSearchEscape != nil {
if err := g.OnSearchEscape(); err != nil {
return true, err
}
}
return true, nil
}
}
for _, kb := range g.keybindings {
if kb.handler == nil {
continue
+14
View File
@@ -29,6 +29,20 @@ func newKeybinding(viewname string, contexts []string, key Key, ch rune, mod Mod
return kb
}
func eventMatchesKey(ev *termbox.Event, key interface{}) bool {
// assuming ModNone for now
if Modifier(ev.Mod) != ModNone {
return false
}
k, ch, err := getKey(key)
if err != nil {
return false
}
return k == Key(ev.Key) && ch == ev.Ch
}
// matchKeypress returns if the keybinding matches the keypress.
func (kb *keybinding) matchKeypress(key Key, ch rune, mod Modifier) bool {
return kb.key == key && kb.ch == ch && kb.mod == mod
+190 -9
View File
@@ -105,6 +105,140 @@ type View struct {
ParentView *View
Context string // this is for assigning keybindings to a view only in certain contexts
searcher *searcher
// when ContainsList is true, we show the current index and total count in the view
ContainsList bool
}
type searcher struct {
searchString string
searchPositions []cellPos
currentSearchIndex int
onSelectItem func(int, int, int) error
}
func (v *View) SetOnSelectItem(onSelectItem func(int, int, int) error) {
v.searcher.onSelectItem = onSelectItem
}
func (v *View) gotoNextMatch() error {
if len(v.searcher.searchPositions) == 0 {
return nil
}
if v.searcher.currentSearchIndex == len(v.searcher.searchPositions)-1 {
v.searcher.currentSearchIndex = 0
} else {
v.searcher.currentSearchIndex++
}
return v.SelectSearchResult(v.searcher.currentSearchIndex)
}
func (v *View) gotoPreviousMatch() error {
if len(v.searcher.searchPositions) == 0 {
return nil
}
if v.searcher.currentSearchIndex == 0 {
if len(v.searcher.searchPositions) > 0 {
v.searcher.currentSearchIndex = len(v.searcher.searchPositions) - 1
}
} else {
v.searcher.currentSearchIndex--
}
return v.SelectSearchResult(v.searcher.currentSearchIndex)
}
func (v *View) SelectSearchResult(index int) error {
y := v.searcher.searchPositions[index].y
v.FocusPoint(0, y)
if v.searcher.onSelectItem != nil {
return v.searcher.onSelectItem(y, index, len(v.searcher.searchPositions))
}
return nil
}
func (v *View) Search(str string) error {
v.writeMutex.Lock()
defer v.writeMutex.Unlock()
v.searcher.search(str)
v.updateSearchPositions()
if len(v.searcher.searchPositions) > 0 {
// get the first result past the current cursor
currentIndex := 0
adjustedY := v.oy + v.cy
adjustedX := v.ox + v.cx
for i, pos := range v.searcher.searchPositions {
if pos.y > adjustedY || (pos.y == adjustedY && pos.x > adjustedX) {
currentIndex = i
break
}
}
v.searcher.currentSearchIndex = currentIndex
return v.SelectSearchResult(currentIndex)
} else {
return v.searcher.onSelectItem(-1, -1, 0)
}
return nil
}
func (v *View) ClearSearch() {
v.searcher.clearSearch()
}
func (v *View) IsSearching() bool {
return v.searcher.searchString != ""
}
func (v *View) FocusPoint(cx int, cy int) {
lineCount := len(v.lines)
if cy < 0 || cy > lineCount {
return
}
_, height := v.Size()
ly := height - 1
if ly == -1 {
ly = 0
}
// if line is above origin, move origin and set cursor to zero
// if line is below origin + height, move origin and set cursor to max
// otherwise set cursor to value - origin
if ly > lineCount {
v.cx = cx
v.cy = cy
v.oy = 0
} else if cy < v.oy {
v.cx = cx
v.cy = 0
v.oy = cy
} else if cy > v.oy+ly {
v.cx = cx
v.cy = ly
v.oy = cy - ly
} else {
v.cx = cx
v.cy = cy - v.oy
}
}
func (s *searcher) search(str string) {
s.searchString = str
s.searchPositions = []cellPos{}
s.currentSearchIndex = 0
}
func (s *searcher) clearSearch() {
s.searchString = ""
s.searchPositions = []cellPos{}
s.currentSearchIndex = 0
}
type cellPos struct {
x int
y int
}
type viewLine struct {
@@ -131,15 +265,16 @@ func (l lineType) String() string {
// newView returns a new View object.
func newView(name string, x0, y0, x1, y1 int, mode OutputMode) *View {
v := &View{
name: name,
x0: x0,
y0: y0,
x1: x1,
y1: y1,
Frame: true,
Editor: DefaultEditor,
tainted: true,
ei: newEscapeInterpreter(mode),
name: name,
x0: x0,
y0: y0,
x1: x1,
y1: y1,
Frame: true,
Editor: DefaultEditor,
tainted: true,
ei: newEscapeInterpreter(mode),
searcher: &searcher{},
}
return v
}
@@ -331,8 +466,35 @@ func (v *View) Rewind() {
v.readOffset = 0
}
func (v *View) updateSearchPositions() {
if v.searcher.searchString != "" {
v.searcher.searchPositions = []cellPos{}
for y, line := range v.lines {
lineLoop:
for x, _ := range line {
if line[x].chr == rune(v.searcher.searchString[0]) {
for offset := 1; offset < len(v.searcher.searchString); offset++ {
if len(line)-1 < x+offset {
continue lineLoop
}
if line[x+offset].chr != rune(v.searcher.searchString[offset]) {
continue lineLoop
}
}
v.searcher.searchPositions = append(v.searcher.searchPositions, cellPos{x: x, y: y})
}
}
}
}
}
// draw re-draws the view's contents.
func (v *View) draw() error {
v.writeMutex.Lock()
defer v.writeMutex.Unlock()
v.updateSearchPositions()
maxX, maxY := v.Size()
if v.Wrap {
@@ -392,6 +554,13 @@ func (v *View) draw() error {
if bgColor == ColorDefault {
bgColor = v.BgColor
}
if matched, selected := v.isPatternMatchedRune(x, y); matched {
if selected {
bgColor = ColorCyan
} else {
bgColor = ColorYellow
}
}
if err := v.setRune(x, y, c.chr, fgColor, bgColor); err != nil {
return err
@@ -403,6 +572,18 @@ func (v *View) draw() error {
return nil
}
func (v *View) isPatternMatchedRune(x, y int) (bool, bool) {
searchStringLength := len(v.searcher.searchString)
for i, pos := range v.searcher.searchPositions {
adjustedY := y + v.oy
adjustedX := x + v.ox
if adjustedY == pos.y && adjustedX >= pos.x && adjustedX < pos.x+searchStringLength {
return true, i == v.searcher.currentSearchIndex
}
}
return false, false
}
// realPosition returns the position in the internal buffer corresponding to the
// point (x, y) of the view.
func (v *View) realPosition(vx, vy int) (x, y int, err error) {
+1 -1
View File
@@ -32,7 +32,7 @@ github.com/hashicorp/hcl/json/token
github.com/integrii/flaggy
# github.com/jbenet/go-context v0.0.0-20150711004518-d14ea06fba99
github.com/jbenet/go-context/io
# github.com/jesseduffield/gocui v0.3.1-0.20200201013258-57fdcf23edc5
# github.com/jesseduffield/gocui v0.3.1-0.20200224201655-5024a02682ed
github.com/jesseduffield/gocui
# github.com/jesseduffield/pty v1.2.1
github.com/jesseduffield/pty