mirror of
https://github.com/jesseduffield/lazygit.git
synced 2026-09-29 10:36:45 -04:00
Compare commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
8ad01fe32f | ||
|
|
fdb543fa7d | ||
|
|
52b5a6410c | ||
|
|
0034cfef5c | ||
|
|
78b62be96f | ||
|
|
1f5ccab1ce | ||
|
|
46be280c92 | ||
|
|
2a5763a771 | ||
|
|
370cec098b | ||
|
|
49a2f0191f | ||
|
|
fabdda0492 | ||
|
|
6fc3290a05 | ||
|
|
66e6369c28 | ||
|
|
0f0da9c32a | ||
|
|
0a69c1a02d | ||
|
|
feaf98bd01 | ||
|
|
0fe9c15ce8 | ||
|
|
f528e12c83 | ||
|
|
8ca9f93ccf | ||
|
|
73d8064837 | ||
|
|
5b1f60b7eb | ||
|
|
2e1344f611 | ||
|
|
5b9996b16f | ||
|
|
6fdc1791e4 | ||
|
|
fd4f37b5c3 | ||
|
|
d76e8887e5 | ||
|
|
eb9134685a | ||
|
|
d929b84786 | ||
|
|
8ef3297b11 | ||
|
|
27c7aeb117 | ||
|
|
c9714600e8 | ||
|
|
665fdded14 | ||
|
|
814a0ea36f |
+7
-1
@@ -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:
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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=
|
||||
|
||||
@@ -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)}
|
||||
|
||||
@@ -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 ""
|
||||
|
||||
@@ -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
@@ -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
|
||||
}
|
||||
|
||||
@@ -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")
|
||||
},
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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>'
|
||||
|
||||
@@ -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
|
||||
}
|
||||
|
||||
@@ -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
@@ -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)
|
||||
}
|
||||
|
||||
@@ -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
@@ -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
@@ -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
@@ -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
@@ -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
@@ -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
|
||||
|
||||
@@ -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{})
|
||||
}
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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)
|
||||
}
|
||||
|
||||
@@ -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)
|
||||
}
|
||||
@@ -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))
|
||||
}
|
||||
|
||||
@@ -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})
|
||||
}
|
||||
@@ -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
|
||||
}
|
||||
@@ -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
@@ -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
@@ -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())
|
||||
}
|
||||
|
||||
@@ -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})
|
||||
}
|
||||
|
||||
@@ -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
@@ -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
@@ -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
@@ -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]
|
||||
}
|
||||
|
||||
@@ -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
@@ -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
@@ -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
@@ -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
@@ -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) {
|
||||
|
||||
Vendored
+1
-1
@@ -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
|
||||
|
||||
Reference in New Issue
Block a user