mirror of
https://github.com/jesseduffield/lazygit.git
synced 2026-09-10 07:36:27 -04:00
Add condition field to custom command prompts (#5364)
When building multi-step custom command forms, some prompts are only relevant depending on earlier answers. Without conditional logic, users must dismiss irrelevant prompts manually. Prompts now accept a `condition` field with a template expression evaluated against prior form values. Skipped prompts default to an empty string.
This commit is contained in:
commit
7f5250a64a
|
|
@ -102,6 +102,7 @@ These fields are applicable to all prompts.
|
|||
| type | One of 'input', 'confirm', 'menu', 'menuFromCommand' | yes |
|
||||
| title | The title to display in the popup panel | no |
|
||||
| key | Used to reference the entered value from within the custom command. E.g. a prompt with `key: 'Branch'` can be referred to as `{{.Form.Branch}}` in the command | yes |
|
||||
| condition | A Go template expression; if it resolves to empty string or `false`, the prompt is skipped. See [Conditional prompts](#conditional-prompts) | no |
|
||||
|
||||
### Input
|
||||
|
||||
|
|
@ -319,6 +320,41 @@ Here's an example using a command but not specifying anything else: so each line
|
|||
command: 'ls'
|
||||
```
|
||||
|
||||
### Conditional prompts
|
||||
|
||||
Here's an example of a conditional prompt:
|
||||
|
||||
```yml
|
||||
customCommands:
|
||||
- key: 'a'
|
||||
context: 'localBranches'
|
||||
prompts:
|
||||
- type: 'menu'
|
||||
title: 'How do you want to create the branch?'
|
||||
key: 'Method'
|
||||
options:
|
||||
- value: 'simple'
|
||||
name: 'Simple'
|
||||
description: 'just a branch name'
|
||||
- value: 'prefix'
|
||||
name: 'With prefix'
|
||||
description: 'with a category prefix'
|
||||
- type: 'menu'
|
||||
title: 'Branch prefix'
|
||||
key: 'Prefix'
|
||||
condition: '{{ eq .Form.Method "prefix" }}'
|
||||
options:
|
||||
- value: 'feature/'
|
||||
- value: 'hotfix/'
|
||||
- value: 'release/'
|
||||
- type: 'input'
|
||||
title: 'Branch name'
|
||||
key: 'Name'
|
||||
command: "git checkout -b '{{.Form.Prefix}}{{.Form.Name}}'"
|
||||
```
|
||||
|
||||
In this example the 'Branch prefix' menu only appears if the user chose 'With prefix'. Otherwise it is skipped and `.Form.Prefix` defaults to empty string.
|
||||
|
||||
## Placeholder values
|
||||
|
||||
Your commands can contain placeholder strings using Go's [template syntax](https://jan.newmarch.name/golang/template/chapter-template.html). The template syntax is pretty powerful, letting you do things like conditionals if you want, but for the most part you'll simply want to be accessing the fields on the following objects:
|
||||
|
|
|
|||
|
|
@ -730,6 +730,9 @@ type CustomCommandPrompt struct {
|
|||
// Like valueFormat but for the labels. If `labelFormat` is not specified, `valueFormat` is shown instead.
|
||||
// Only for menuFromCommand prompts.
|
||||
LabelFormat string `yaml:"labelFormat" jsonschema:"example={{ .branch | green }}"`
|
||||
|
||||
// A Go template expression evaluated against the current form state. If it resolves to empty string or 'false', the prompt is skipped.
|
||||
Condition string `yaml:"condition" jsonschema:"example={{ eq .Form.Choice \"yes\" }}"`
|
||||
}
|
||||
|
||||
type CustomCommandSuggestions struct {
|
||||
|
|
|
|||
|
|
@ -107,12 +107,41 @@ func (self *HandlerCreator) call(customCommand config.CustomCommand) func() erro
|
|||
default:
|
||||
return errors.New("custom command prompt must have a type of 'input', 'menu', 'menuFromCommand', or 'confirm'")
|
||||
}
|
||||
|
||||
if prompt.Condition != "" {
|
||||
showPrompt := f
|
||||
conditionTemplate := prompt.Condition
|
||||
f = func() error {
|
||||
resolved, err := resolveCondition(conditionTemplate, resolveTemplate)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if resolved {
|
||||
return showPrompt()
|
||||
}
|
||||
if _, exists := form[prompt.Key]; !exists {
|
||||
form[prompt.Key] = ""
|
||||
}
|
||||
return g()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return f()
|
||||
}
|
||||
}
|
||||
|
||||
func resolveCondition(condition string, resolveTemplate func(string) (string, error)) (bool, error) {
|
||||
if strings.TrimSpace(condition) == "" {
|
||||
return false, nil
|
||||
}
|
||||
resolved, err := resolveTemplate(condition)
|
||||
if err != nil {
|
||||
return false, err
|
||||
}
|
||||
return strings.TrimSpace(resolved) != "" && strings.TrimSpace(resolved) != "false", nil
|
||||
}
|
||||
|
||||
func (self *HandlerCreator) inputPrompt(prompt *config.CustomCommandPrompt, wrappedF func(string) error) error {
|
||||
findSuggestionsFn, err := self.generateFindSuggestionsFunc(prompt)
|
||||
if err != nil {
|
||||
|
|
|
|||
|
|
@ -0,0 +1,71 @@
|
|||
package custom_commands
|
||||
|
||||
import (
|
||||
"github.com/jesseduffield/lazygit/pkg/config"
|
||||
. "github.com/jesseduffield/lazygit/pkg/integration/components"
|
||||
)
|
||||
|
||||
var ConditionalPromptFalseString = NewIntegrationTest(NewIntegrationTestArgs{
|
||||
Description: "Conditional prompt is skipped when condition is bare false or template false",
|
||||
ExtraCmdArgs: []string{},
|
||||
Skip: false,
|
||||
SetupRepo: func(shell *Shell) {
|
||||
shell.EmptyCommit("blah")
|
||||
},
|
||||
SetupConfig: func(cfg *config.AppConfig) {
|
||||
cfg.GetUserConfig().CustomCommands = []config.CustomCommand{
|
||||
{
|
||||
Key: "a",
|
||||
Context: "files",
|
||||
Command: `echo "{{.Form.Choice}}" > result.txt`,
|
||||
Prompts: []config.CustomCommandPrompt{
|
||||
{
|
||||
Key: "Choice",
|
||||
Type: "menu",
|
||||
Title: "Pick one",
|
||||
Options: []config.CustomCommandMenuOption{
|
||||
{
|
||||
Name: "foo",
|
||||
Description: "Foo",
|
||||
Value: "FOO",
|
||||
},
|
||||
{
|
||||
Name: "bar",
|
||||
Description: "Bar",
|
||||
Value: "BAR",
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
Key: "Skipped1",
|
||||
Type: "input",
|
||||
Title: "This is always skipped (false)",
|
||||
Condition: `false`,
|
||||
},
|
||||
{
|
||||
Key: "Skipped2",
|
||||
Type: "input",
|
||||
Title: "This is always skipped (template false)",
|
||||
Condition: `{{ eq "a" "b" }}`,
|
||||
},
|
||||
},
|
||||
},
|
||||
}
|
||||
},
|
||||
Run: func(t *TestDriver, keys config.KeybindingConfig) {
|
||||
t.Views().Files().
|
||||
IsFocused().
|
||||
Press("a")
|
||||
|
||||
t.ExpectPopup().Menu().Title(Equals("Pick one")).Select(Contains("foo")).Confirm()
|
||||
|
||||
// Both conditional prompts skipped, file created directly
|
||||
t.Views().Files().
|
||||
Focus().
|
||||
Lines(
|
||||
Contains("result.txt").IsSelected(),
|
||||
)
|
||||
|
||||
t.FileSystem().FileContent("result.txt", Equals("FOO\n"))
|
||||
},
|
||||
})
|
||||
|
|
@ -0,0 +1,55 @@
|
|||
package custom_commands
|
||||
|
||||
import (
|
||||
"github.com/jesseduffield/lazygit/pkg/config"
|
||||
. "github.com/jesseduffield/lazygit/pkg/integration/components"
|
||||
)
|
||||
|
||||
var ConditionalPromptFalseValue = NewIntegrationTest(NewIntegrationTestArgs{
|
||||
Description: "Entering literal false as form input does not incorrectly skip a conditional prompt",
|
||||
ExtraCmdArgs: []string{},
|
||||
Skip: false,
|
||||
SetupRepo: func(shell *Shell) {
|
||||
shell.EmptyCommit("blah")
|
||||
},
|
||||
SetupConfig: func(cfg *config.AppConfig) {
|
||||
cfg.GetUserConfig().CustomCommands = []config.CustomCommand{
|
||||
{
|
||||
Key: "a",
|
||||
Context: "files",
|
||||
Command: `echo "{{.Form.Word}} {{.Form.Extra}}" > result.txt`,
|
||||
Prompts: []config.CustomCommandPrompt{
|
||||
{
|
||||
Key: "Word",
|
||||
Type: "input",
|
||||
Title: "Enter a word",
|
||||
},
|
||||
{
|
||||
Key: "Extra",
|
||||
Type: "input",
|
||||
Title: "Enter extra",
|
||||
Condition: `{{ eq .Form.Word "false" }}`,
|
||||
},
|
||||
},
|
||||
},
|
||||
}
|
||||
},
|
||||
Run: func(t *TestDriver, keys config.KeybindingConfig) {
|
||||
t.Views().Files().
|
||||
IsFocused().
|
||||
Press("a")
|
||||
|
||||
t.ExpectPopup().Prompt().Title(Equals("Enter a word")).Type("false").Confirm()
|
||||
|
||||
// Condition {{ eq .Form.Word "false" }} evaluates to true, so prompt should appear
|
||||
t.ExpectPopup().Prompt().Title(Equals("Enter extra")).Type("baz").Confirm()
|
||||
|
||||
t.Views().Files().
|
||||
Focus().
|
||||
Lines(
|
||||
Contains("result.txt").IsSelected(),
|
||||
)
|
||||
|
||||
t.FileSystem().FileContent("result.txt", Equals("false baz\n"))
|
||||
},
|
||||
})
|
||||
96
pkg/integration/tests/custom_commands/conditional_prompts.go
Normal file
96
pkg/integration/tests/custom_commands/conditional_prompts.go
Normal file
|
|
@ -0,0 +1,96 @@
|
|||
package custom_commands
|
||||
|
||||
import (
|
||||
"github.com/jesseduffield/lazygit/pkg/config"
|
||||
. "github.com/jesseduffield/lazygit/pkg/integration/components"
|
||||
)
|
||||
|
||||
var ConditionalPrompts = NewIntegrationTest(NewIntegrationTestArgs{
|
||||
Description: "Using a custom command with conditional prompts that are skipped based on form values",
|
||||
ExtraCmdArgs: []string{},
|
||||
Skip: false,
|
||||
SetupRepo: func(shell *Shell) {
|
||||
shell.EmptyCommit("initial commit")
|
||||
},
|
||||
SetupConfig: func(cfg *config.AppConfig) {
|
||||
cfg.GetUserConfig().CustomCommands = []config.CustomCommand{
|
||||
{
|
||||
Key: "a",
|
||||
Context: "files",
|
||||
Command: `echo "{{.Form.Choice}}{{if .Form.Detail}} {{.Form.Detail}}{{end}}" > result.txt`,
|
||||
Prompts: []config.CustomCommandPrompt{
|
||||
{
|
||||
Key: "Choice",
|
||||
Type: "menu",
|
||||
Title: "Choose an option",
|
||||
Options: []config.CustomCommandMenuOption{
|
||||
{
|
||||
Name: "first",
|
||||
Description: "First option",
|
||||
Value: "FIRST",
|
||||
Key: "1",
|
||||
},
|
||||
{
|
||||
Name: "second",
|
||||
Description: "Second option",
|
||||
Value: "SECOND",
|
||||
Key: "H",
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
Key: "Detail",
|
||||
Type: "input",
|
||||
Title: "Enter detail for second option",
|
||||
Condition: `{{ eq .Form.Choice "SECOND" }}`,
|
||||
},
|
||||
},
|
||||
},
|
||||
}
|
||||
},
|
||||
Run: func(t *TestDriver, keys config.KeybindingConfig) {
|
||||
// Test 1: Select "first" via key — conditional prompt should be skipped
|
||||
t.Views().Files().
|
||||
IsFocused().
|
||||
Press("a")
|
||||
|
||||
t.ExpectPopup().Menu().
|
||||
Title(Equals("Choose an option"))
|
||||
|
||||
t.Views().Menu().Press("1")
|
||||
|
||||
// Detail prompt should be skipped, file should be created directly
|
||||
t.Views().Files().
|
||||
Focus().
|
||||
Lines(
|
||||
Contains("result.txt").IsSelected(),
|
||||
)
|
||||
|
||||
t.FileSystem().FileContent("result.txt", Equals("FIRST\n"))
|
||||
|
||||
// Test 2: Select "second" via key — conditional prompt should appear
|
||||
t.Shell().DeleteFile("result.txt")
|
||||
t.GlobalPress(keys.Files.RefreshFiles)
|
||||
|
||||
t.Views().Files().
|
||||
IsEmpty().
|
||||
IsFocused().
|
||||
Press("a")
|
||||
|
||||
t.ExpectPopup().Menu().
|
||||
Title(Equals("Choose an option"))
|
||||
|
||||
t.Views().Menu().Press("H")
|
||||
|
||||
// Detail prompt should appear because Choice == "SECOND"
|
||||
t.ExpectPopup().Prompt().Title(Equals("Enter detail for second option")).Type("extra").Confirm()
|
||||
|
||||
t.Views().Files().
|
||||
Focus().
|
||||
Lines(
|
||||
Contains("result.txt").IsSelected(),
|
||||
)
|
||||
|
||||
t.FileSystem().FileContent("result.txt", Equals("SECOND extra\n"))
|
||||
},
|
||||
})
|
||||
|
|
@ -172,6 +172,9 @@ var tests = []*components.IntegrationTest{
|
|||
custom_commands.AccessCommitProperties,
|
||||
custom_commands.BasicCommand,
|
||||
custom_commands.CheckForConflicts,
|
||||
custom_commands.ConditionalPromptFalseString,
|
||||
custom_commands.ConditionalPromptFalseValue,
|
||||
custom_commands.ConditionalPrompts,
|
||||
custom_commands.CustomCommandsSubmenu,
|
||||
custom_commands.CustomCommandsSubmenuWithSpecialKeybindings,
|
||||
custom_commands.FormPrompts,
|
||||
|
|
|
|||
|
|
@ -235,6 +235,13 @@
|
|||
"examples": [
|
||||
"{{ .branch | green }}"
|
||||
]
|
||||
},
|
||||
"condition": {
|
||||
"type": "string",
|
||||
"description": "A Go template expression evaluated against the current form state. If it resolves to empty string or 'false', the prompt is skipped.",
|
||||
"examples": [
|
||||
"{{ eq .Form.Choice \"yes\" }}"
|
||||
]
|
||||
}
|
||||
},
|
||||
"additionalProperties": false,
|
||||
|
|
|
|||
Loading…
Reference in a new issue