mirror of
https://github.com/jesseduffield/lazygit.git
synced 2025-06-04 23:37:41 +02:00
Merge pull request #1416 from FoamScience/feature_menuOptions
This commit is contained in:
commit
5d1a9639b6
@ -47,7 +47,8 @@ customCommands:
|
|||||||
title: 'Remote branch:'
|
title: 'Remote branch:'
|
||||||
command: 'git branch -r --list {{index .PromptResponses 0}}/*'
|
command: 'git branch -r --list {{index .PromptResponses 0}}/*'
|
||||||
filter: '.*{{index .PromptResponses 0}}/(?P<branch>.*)'
|
filter: '.*{{index .PromptResponses 0}}/(?P<branch>.*)'
|
||||||
format: '{{ .branch }}'
|
valueFormat: '{{ .branch }}'
|
||||||
|
labelFormat: ''
|
||||||
```
|
```
|
||||||
|
|
||||||
Looking at the command assigned to the 'n' key, here's what the result looks like:
|
Looking at the command assigned to the 'n' key, here's what the result looks like:
|
||||||
@ -92,19 +93,26 @@ The permitted contexts are:
|
|||||||
|
|
||||||
The permitted prompt fields are:
|
The permitted prompt fields are:
|
||||||
|
|
||||||
| _field_ | _description_ | _required_ |
|
| _field_ | _description_ | _required_ |
|
||||||
| ------------ | -------------------------------------------------------------------------------- | ---------- |
|
| ------------ | -------------------------------------------------------------------------------- | ---------- |
|
||||||
| type | one of 'input' or 'menu' | yes |
|
| type | one of 'input' or 'menu' | yes |
|
||||||
| title | the title to display in the popup panel | no |
|
| title | the title to display in the popup panel | no |
|
||||||
| initialValue | (only applicable to 'input' prompts) the initial value to appear in the text box | no |
|
| initialValue | (only applicable to 'input' prompts) the initial value to appear in the text box | no |
|
||||||
| options | (only applicable to 'menu' prompts) the options to display in the menu | no |
|
| options | (only applicable to 'menu' prompts) the options to display in the menu | no |
|
||||||
| command | (only applicable to 'menuFromCommand' prompts) the command to run to generate | yes |
|
| command | (only applicable to 'menuFromCommand' prompts) the command to run to generate | yes |
|
||||||
| | menu options | |
|
| | menu options | |
|
||||||
| filter | (only applicable to 'menuFromCommand' prompts) the regexp to run specifying | yes |
|
| filter | (only applicable to 'menuFromCommand' prompts) the regexp to run specifying | yes |
|
||||||
| | groups which are going to be kept from the command's output | |
|
| | groups which are going to be kept from the command's output | |
|
||||||
| format | (only applicable to 'menuFromCommand' prompts) how to format matched groups from | yes |
|
| valueFormat | (only applicable to 'menuFromCommand' prompts) how to format matched groups from | yes |
|
||||||
| | the filter. You can use named groups, or `{{ .group_GROUPID }}`. | yes |
|
| | the filter to construct a menu item's value (What gets appended to prompt | |
|
||||||
| | PS: named groups keep first match only | yes |
|
| | responses when the item is selected). You can use named groups, | |
|
||||||
|
| | or `{{ .group_GROUPID }}`. | |
|
||||||
|
| | PS: named groups keep first match only | |
|
||||||
|
| labelFormat | (only applicable to 'menuFromCommand' prompts) how to format matched groups from | no |
|
||||||
|
| | the filter to construct the item's label (What's shown on screen). You can use | |
|
||||||
|
| | named groups, or `{{ .group_GROUPID }}`. If this is not specified, `valueFormat` | |
|
||||||
|
| | is shown instead. | |
|
||||||
|
| | PS: named groups keep first match only | |
|
||||||
|
|
||||||
The permitted option fields are:
|
The permitted option fields are:
|
||||||
| _field_ | _description_ | _required_ |
|
| _field_ | _description_ | _required_ |
|
||||||
|
@ -283,9 +283,10 @@ type CustomCommandPrompt struct {
|
|||||||
Options []CustomCommandMenuOption
|
Options []CustomCommandMenuOption
|
||||||
|
|
||||||
// this only applies to menuFromCommand
|
// this only applies to menuFromCommand
|
||||||
Command string `yaml:"command"`
|
Command string `yaml:"command"`
|
||||||
Filter string `yaml:"filter"`
|
Filter string `yaml:"filter"`
|
||||||
Format string `yaml:"format"`
|
ValueFormat string `yaml:"valueFormat"`
|
||||||
|
LabelFormat string `yaml:"labelFormat"`
|
||||||
}
|
}
|
||||||
|
|
||||||
type CustomCommandMenuOption struct {
|
type CustomCommandMenuOption struct {
|
||||||
|
@ -33,6 +33,11 @@ type CustomCommandObjects struct {
|
|||||||
PromptResponses []string
|
PromptResponses []string
|
||||||
}
|
}
|
||||||
|
|
||||||
|
type commandMenuEntry struct {
|
||||||
|
label string
|
||||||
|
value string
|
||||||
|
}
|
||||||
|
|
||||||
func (gui *Gui) resolveTemplate(templateStr string, promptResponses []string) (string, error) {
|
func (gui *Gui) resolveTemplate(templateStr string, promptResponses []string) (string, error) {
|
||||||
objects := CustomCommandObjects{
|
objects := CustomCommandObjects{
|
||||||
SelectedFile: gui.getSelectedFile(),
|
SelectedFile: gui.getSelectedFile(),
|
||||||
@ -118,21 +123,30 @@ func (gui *Gui) menuPrompt(prompt config.CustomCommandPrompt, promptResponses []
|
|||||||
return gui.createMenu(title, menuItems, createMenuOptions{showCancel: true})
|
return gui.createMenu(title, menuItems, createMenuOptions{showCancel: true})
|
||||||
}
|
}
|
||||||
|
|
||||||
func (gui *Gui) GenerateMenuCandidates(commandOutput string, filter string, format string) ([]string, error) {
|
func (gui *Gui) GenerateMenuCandidates(commandOutput, filter, valueFormat, labelFormat string) ([]commandMenuEntry, error) {
|
||||||
candidates := []string{}
|
|
||||||
reg, err := regexp.Compile(filter)
|
reg, err := regexp.Compile(filter)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return candidates, gui.surfaceError(errors.New("unable to parse filter regex, error: " + err.Error()))
|
return nil, gui.surfaceError(errors.New("unable to parse filter regex, error: " + err.Error()))
|
||||||
}
|
}
|
||||||
|
|
||||||
buff := bytes.NewBuffer(nil)
|
buff := bytes.NewBuffer(nil)
|
||||||
temp, err := template.New("format").Parse(format)
|
|
||||||
|
valueTemp, err := template.New("format").Parse(valueFormat)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return candidates, gui.surfaceError(errors.New("unable to parse format, error: " + err.Error()))
|
return nil, gui.surfaceError(errors.New("unable to parse value format, error: " + err.Error()))
|
||||||
}
|
}
|
||||||
|
|
||||||
|
descTemp, err := template.New("format").Parse(labelFormat)
|
||||||
|
if err != nil {
|
||||||
|
return nil, gui.surfaceError(errors.New("unable to parse label format, error: " + err.Error()))
|
||||||
|
}
|
||||||
|
|
||||||
|
candidates := []commandMenuEntry{}
|
||||||
for _, str := range strings.Split(string(commandOutput), "\n") {
|
for _, str := range strings.Split(string(commandOutput), "\n") {
|
||||||
if str == "" {
|
if str == "" {
|
||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
|
|
||||||
tmplData := map[string]string{}
|
tmplData := map[string]string{}
|
||||||
out := reg.FindAllStringSubmatch(str, -1)
|
out := reg.FindAllStringSubmatch(str, -1)
|
||||||
if len(out) > 0 {
|
if len(out) > 0 {
|
||||||
@ -146,12 +160,28 @@ func (gui *Gui) GenerateMenuCandidates(commandOutput string, filter string, form
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
err = temp.Execute(buff, tmplData)
|
|
||||||
|
err = valueTemp.Execute(buff, tmplData)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return candidates, gui.surfaceError(err)
|
return candidates, gui.surfaceError(err)
|
||||||
}
|
}
|
||||||
|
entry := commandMenuEntry{
|
||||||
|
value: strings.TrimSpace(buff.String()),
|
||||||
|
}
|
||||||
|
|
||||||
|
if labelFormat != "" {
|
||||||
|
buff.Reset()
|
||||||
|
err = descTemp.Execute(buff, tmplData)
|
||||||
|
if err != nil {
|
||||||
|
return candidates, gui.surfaceError(err)
|
||||||
|
}
|
||||||
|
entry.label = strings.TrimSpace(buff.String())
|
||||||
|
} else {
|
||||||
|
entry.label = entry.value
|
||||||
|
}
|
||||||
|
|
||||||
|
candidates = append(candidates, entry)
|
||||||
|
|
||||||
candidates = append(candidates, strings.TrimSpace(buff.String()))
|
|
||||||
buff.Reset()
|
buff.Reset()
|
||||||
}
|
}
|
||||||
return candidates, err
|
return candidates, err
|
||||||
@ -177,7 +207,7 @@ func (gui *Gui) menuPromptFromCommand(prompt config.CustomCommandPrompt, promptR
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Need to make a menu out of what the cmd has displayed
|
// Need to make a menu out of what the cmd has displayed
|
||||||
candidates, err := gui.GenerateMenuCandidates(message, filter, prompt.Format)
|
candidates, err := gui.GenerateMenuCandidates(message, filter, prompt.ValueFormat, prompt.LabelFormat)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return gui.surfaceError(err)
|
return gui.surfaceError(err)
|
||||||
}
|
}
|
||||||
@ -185,9 +215,9 @@ func (gui *Gui) menuPromptFromCommand(prompt config.CustomCommandPrompt, promptR
|
|||||||
menuItems := make([]*menuItem, len(candidates))
|
menuItems := make([]*menuItem, len(candidates))
|
||||||
for i := range candidates {
|
for i := range candidates {
|
||||||
menuItems[i] = &menuItem{
|
menuItems[i] = &menuItem{
|
||||||
displayStrings: []string{candidates[i]},
|
displayStrings: []string{candidates[i].label},
|
||||||
onPress: func() error {
|
onPress: func() error {
|
||||||
promptResponses[responseIdx] = candidates[i]
|
promptResponses[responseIdx] = candidates[i].value
|
||||||
return wrappedF()
|
return wrappedF()
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
|
@ -83,32 +83,37 @@ func runCmdHeadless(cmd *exec.Cmd) error {
|
|||||||
|
|
||||||
func TestGuiGenerateMenuCandidates(t *testing.T) {
|
func TestGuiGenerateMenuCandidates(t *testing.T) {
|
||||||
type scenario struct {
|
type scenario struct {
|
||||||
testName string
|
testName string
|
||||||
cmdOut string
|
cmdOut string
|
||||||
filter string
|
filter string
|
||||||
format string
|
valueFormat string
|
||||||
test func([]string, error)
|
labelFormat string
|
||||||
|
test func([]commandMenuEntry, error)
|
||||||
}
|
}
|
||||||
|
|
||||||
scenarios := []scenario{
|
scenarios := []scenario{
|
||||||
{
|
{
|
||||||
"Extract remote branch name",
|
"Extract remote branch name",
|
||||||
"upstream/pr-1",
|
"upstream/pr-1",
|
||||||
"upstream/(?P<branch>.*)",
|
"(?P<remote>[a-z_]+)/(?P<branch>.*)",
|
||||||
"{{ .branch }}",
|
"{{ .branch }}",
|
||||||
func(actual []string, err error) {
|
"Remote: {{ .remote }}",
|
||||||
|
func(actualEntry []commandMenuEntry, err error) {
|
||||||
assert.NoError(t, err)
|
assert.NoError(t, err)
|
||||||
assert.EqualValues(t, "pr-1", actual[0])
|
assert.EqualValues(t, "pr-1", actualEntry[0].value)
|
||||||
|
assert.EqualValues(t, "Remote: upstream", actualEntry[0].label)
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"Multiple named groups",
|
"Multiple named groups with empty labelFormat",
|
||||||
"upstream/pr-1",
|
"upstream/pr-1",
|
||||||
"(?P<remote>[a-z]*)/(?P<branch>.*)",
|
"(?P<remote>[a-z]*)/(?P<branch>.*)",
|
||||||
"{{ .branch }}|{{ .remote }}",
|
"{{ .branch }}|{{ .remote }}",
|
||||||
func(actual []string, err error) {
|
"",
|
||||||
|
func(actualEntry []commandMenuEntry, err error) {
|
||||||
assert.NoError(t, err)
|
assert.NoError(t, err)
|
||||||
assert.EqualValues(t, "pr-1|upstream", actual[0])
|
assert.EqualValues(t, "pr-1|upstream", actualEntry[0].value)
|
||||||
|
assert.EqualValues(t, "pr-1|upstream", actualEntry[0].label)
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
@ -116,16 +121,18 @@ func TestGuiGenerateMenuCandidates(t *testing.T) {
|
|||||||
"upstream/pr-1",
|
"upstream/pr-1",
|
||||||
"(?P<remote>[a-z]*)/(?P<branch>.*)",
|
"(?P<remote>[a-z]*)/(?P<branch>.*)",
|
||||||
"{{ .group_2 }}|{{ .group_1 }}",
|
"{{ .group_2 }}|{{ .group_1 }}",
|
||||||
func(actual []string, err error) {
|
"Remote: {{ .group_1 }}",
|
||||||
|
func(actualEntry []commandMenuEntry, err error) {
|
||||||
assert.NoError(t, err)
|
assert.NoError(t, err)
|
||||||
assert.EqualValues(t, "pr-1|upstream", actual[0])
|
assert.EqualValues(t, "pr-1|upstream", actualEntry[0].value)
|
||||||
|
assert.EqualValues(t, "Remote: upstream", actualEntry[0].label)
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
|
|
||||||
for _, s := range scenarios {
|
for _, s := range scenarios {
|
||||||
t.Run(s.testName, func(t *testing.T) {
|
t.Run(s.testName, func(t *testing.T) {
|
||||||
s.test(NewDummyGui().GenerateMenuCandidates(s.cmdOut, s.filter, s.format))
|
s.test(NewDummyGui().GenerateMenuCandidates(s.cmdOut, s.filter, s.valueFormat, s.labelFormat))
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
Loading…
x
Reference in New Issue
Block a user