mirror of
https://github.com/jesseduffield/lazygit.git
synced 2026-01-26 01:41:35 +03:00
feat: support custom keybinds in custom command prompt menus
This commit is contained in:
@@ -192,6 +192,7 @@ The permitted option fields are:
|
||||
| name | The first part of the label | no |
|
||||
| description | The second part of the label | no |
|
||||
| value | the value that will be used in the command | yes |
|
||||
| key | Keybinding to invoke this menu option without needing to navigate to it. Can be a single letter or one of the values from [here](https://github.com/jesseduffield/lazygit/blob/master/docs/keybindings/Custom_Keybindings.md) | no |
|
||||
|
||||
If an option has no name the value will be displayed to the user in place of the name, so you're allowed to only include the value like so:
|
||||
|
||||
@@ -233,6 +234,34 @@ customCommands:
|
||||
description: 'branch for a release'
|
||||
```
|
||||
|
||||
Here's an example of supplying keybindings for menu options:
|
||||
|
||||
```yml
|
||||
customCommands:
|
||||
- key: 'a'
|
||||
command: 'echo {{.Form.BranchType | quote}}'
|
||||
context: 'commits'
|
||||
prompts:
|
||||
- type: 'menu'
|
||||
title: 'What kind of branch is it?'
|
||||
key: 'BranchType'
|
||||
options:
|
||||
- value: 'feature'
|
||||
name: 'feature branch'
|
||||
description: 'branch based off develop'
|
||||
key: 'f'
|
||||
- value: 'hotfix'
|
||||
name: 'hotfix branch'
|
||||
description: 'branch based off main for fast bug fixes'
|
||||
key: 'h'
|
||||
- value: 'release'
|
||||
name: 'release branch'
|
||||
description: 'branch for a release'
|
||||
key: 'r'
|
||||
```
|
||||
|
||||
In this example, pressing 'f', 'h', or 'r' will directly select the corresponding option without needing to navigate to it first.
|
||||
|
||||
### Menu-from-command
|
||||
|
||||
| _field_ | _description_ | _required_ |
|
||||
|
||||
@@ -733,6 +733,8 @@ type CustomCommandMenuOption struct {
|
||||
Description string `yaml:"description"`
|
||||
// The value that will be used in the command
|
||||
Value string `yaml:"value" jsonschema:"example=feature,minLength=1"`
|
||||
// Keybinding to invoke this menu option without needing to navigate to it
|
||||
Key string `yaml:"key"`
|
||||
}
|
||||
|
||||
type CustomIconsConfig struct {
|
||||
|
||||
@@ -136,6 +136,12 @@ func validateCustomCommands(customCommands []CustomCommand) error {
|
||||
return err
|
||||
}
|
||||
} else {
|
||||
for _, prompt := range customCommand.Prompts {
|
||||
if err := validateCustomCommandPrompt(prompt); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
|
||||
if err := validateEnum("customCommand.output", customCommand.Output,
|
||||
[]string{"", "none", "terminal", "log", "logWithPty", "popup"}); err != nil {
|
||||
return err
|
||||
@@ -144,3 +150,14 @@ func validateCustomCommands(customCommands []CustomCommand) error {
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func validateCustomCommandPrompt(prompt CustomCommandPrompt) error {
|
||||
for _, option := range prompt.Options {
|
||||
if !isValidKeybindingKey(option.Key) {
|
||||
return fmt.Errorf("Unrecognized key '%s' for custom command prompt option. For permitted values see %s",
|
||||
option.Key, constants.Links.Docs.CustomKeybindings)
|
||||
}
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
@@ -176,6 +176,31 @@ func TestUserConfigValidate_enums(t *testing.T) {
|
||||
{value: "invalid_value", valid: false},
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "Custom command keybinding in prompt menu",
|
||||
setup: func(config *UserConfig, value string) {
|
||||
config.CustomCommands = []CustomCommand{
|
||||
{
|
||||
Key: "X",
|
||||
Description: "My Custom Commands",
|
||||
Prompts: []CustomCommandPrompt{
|
||||
{
|
||||
Options: []CustomCommandMenuOption{
|
||||
{Key: value},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
}
|
||||
},
|
||||
testCases: []testCase{
|
||||
{value: "", valid: true},
|
||||
{value: "<disabled>", valid: true},
|
||||
{value: "q", valid: true},
|
||||
{value: "<c-c>", valid: true},
|
||||
{value: "invalid_value", valid: false},
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "Custom command output",
|
||||
setup: func(config *UserConfig, value string) {
|
||||
|
||||
@@ -9,6 +9,7 @@ import (
|
||||
"github.com/jesseduffield/gocui"
|
||||
"github.com/jesseduffield/lazygit/pkg/config"
|
||||
"github.com/jesseduffield/lazygit/pkg/gui/controllers/helpers"
|
||||
"github.com/jesseduffield/lazygit/pkg/gui/keybindings"
|
||||
"github.com/jesseduffield/lazygit/pkg/gui/style"
|
||||
"github.com/jesseduffield/lazygit/pkg/gui/types"
|
||||
"github.com/jesseduffield/lazygit/pkg/utils"
|
||||
@@ -203,6 +204,7 @@ func (self *HandlerCreator) menuPrompt(prompt *config.CustomCommandPrompt, wrapp
|
||||
OnPress: func() error {
|
||||
return wrappedF(option.Value)
|
||||
},
|
||||
Key: keybindings.GetKey(option.Key),
|
||||
}
|
||||
})
|
||||
|
||||
|
||||
@@ -108,6 +108,7 @@ func (self *Resolver) resolveMenuOption(option *config.CustomCommandMenuOption,
|
||||
Name: name,
|
||||
Description: description,
|
||||
Value: value,
|
||||
Key: option.Key,
|
||||
}, nil
|
||||
}
|
||||
|
||||
|
||||
@@ -0,0 +1,65 @@
|
||||
package custom_commands
|
||||
|
||||
import (
|
||||
"github.com/jesseduffield/lazygit/pkg/config"
|
||||
. "github.com/jesseduffield/lazygit/pkg/integration/components"
|
||||
)
|
||||
|
||||
var MenuPromptWithKeys = NewIntegrationTest(NewIntegrationTestArgs{
|
||||
Description: "Using a custom command with menu options that have keybindings",
|
||||
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 | quote}} > 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",
|
||||
},
|
||||
{
|
||||
Name: "third",
|
||||
Description: "Third option",
|
||||
Value: "THIRD",
|
||||
Key: "3",
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
}
|
||||
},
|
||||
Run: func(t *TestDriver, keys config.KeybindingConfig) {
|
||||
t.Views().Files().
|
||||
IsFocused().
|
||||
Press("a")
|
||||
|
||||
t.ExpectPopup().Menu().
|
||||
Title(Equals("Choose an option"))
|
||||
|
||||
// 'H' is normally a navigation key (ScrollLeft), so this tests that menu item
|
||||
// keybindings have proper precedence over non-essential navigation keys
|
||||
t.Views().Menu().Press("H")
|
||||
|
||||
t.FileSystem().FileContent("result.txt", Equals("SECOND\n"))
|
||||
},
|
||||
})
|
||||
@@ -175,6 +175,7 @@ var tests = []*components.IntegrationTest{
|
||||
custom_commands.GlobalContext,
|
||||
custom_commands.MenuFromCommand,
|
||||
custom_commands.MenuFromCommandsOutput,
|
||||
custom_commands.MenuPromptWithKeys,
|
||||
custom_commands.MultipleContexts,
|
||||
custom_commands.MultiplePrompts,
|
||||
custom_commands.RunCommand,
|
||||
|
||||
@@ -163,6 +163,10 @@
|
||||
"examples": [
|
||||
"feature"
|
||||
]
|
||||
},
|
||||
"key": {
|
||||
"type": "string",
|
||||
"description": "Keybinding to invoke this menu option without needing to navigate to it"
|
||||
}
|
||||
},
|
||||
"additionalProperties": false,
|
||||
|
||||
Reference in New Issue
Block a user