1
0
mirror of https://github.com/jesseduffield/lazygit.git synced 2025-08-08 22:36:49 +02:00

Custom commands submenus (#4324)

- **PR Description**

I want to be able to configure custom commands that I don't need very
often; I don't want these to pollute the global keybindings menu, and I
don't want to assign globally unique keybindings to them (because there
are only so many of these available, and also because I wouldn't be able
to remember them, because the commands are not used often). However, I
still want to invoke them through keybindings somehow.

I find that the perfect solution for this is to configure a menu that
contains custom commands. I can pop open the menu using only one key
that I need to remember, but I can access the individual custom commands
inside using keys that don't need to be unique with the rest of the
global keybindings.

In this PR we achieve this by adding an optional `subCommands` property
to customCommand that can be used instead of the other properties like
`command`, etc. This is an alternative approach to #4276, which added a
new top-level property for custom command menus.

Potentially closes #3799.

- **Please check if the PR fulfills these requirements**

* [x] Cheatsheets are up-to-date (run `go generate ./...`)
* [x] Code has been formatted (see
[here](https://github.com/jesseduffield/lazygit/blob/master/CONTRIBUTING.md#code-formatting))
* [x] Tests have been added/updated (see
[here](https://github.com/jesseduffield/lazygit/blob/master/pkg/integration/README.md)
for the integration test guide)
* [x] Text is internationalised (see
[here](https://github.com/jesseduffield/lazygit/blob/master/CONTRIBUTING.md#internationalisation))
* [x] If a new UserConfig entry was added, make sure it can be
hot-reloaded (see
[here](https://github.com/jesseduffield/lazygit/blob/master/docs/dev/Codebase_Guide.md#using-userconfig))
* [x] Docs have been updated if necessary
* [x] You've read through your own file changes for silly mistakes etc
This commit is contained in:
Stefan Haller
2025-02-28 10:47:59 +01:00
committed by GitHub
16 changed files with 294 additions and 36 deletions

View File

@ -1,6 +1,6 @@
# Custom Command Keybindings # Custom Command Keybindings
You can add custom command keybindings in your config.yml (accessible by pressing 'o' on the status panel from within lazygit) like so: You can add custom command keybindings in your config.yml (accessible by pressing 'e' on the status panel from within lazygit) like so:
```yml ```yml
customCommands: customCommands:
@ -324,6 +324,27 @@ We don't support accessing all elements of a range selection yet. We might add t
If your custom keybinding collides with an inbuilt keybinding that is defined for the same context, only the custom keybinding will be executed. This also applies to the global context. However, one caveat is that if you have a custom keybinding defined on the global context for some key, and there is an in-built keybinding defined for the same key and for a specific context (say the 'files' context), then the in-built keybinding will take precedence. See how to change in-built keybindings [here](https://github.com/jesseduffield/lazygit/blob/master/docs/Config.md#keybindings) If your custom keybinding collides with an inbuilt keybinding that is defined for the same context, only the custom keybinding will be executed. This also applies to the global context. However, one caveat is that if you have a custom keybinding defined on the global context for some key, and there is an in-built keybinding defined for the same key and for a specific context (say the 'files' context), then the in-built keybinding will take precedence. See how to change in-built keybindings [here](https://github.com/jesseduffield/lazygit/blob/master/docs/Config.md#keybindings)
## Menus of custom commands
For custom commands that are not used very frequently it may be preferable to hide them in a menu; you can assign a key to open the menu, and the commands will appear inside. This has the advantage that you don't have to come up with individual unique keybindings for all those commands that you don't use often; the keybindings for the commands in the menu only need to be unique within the menu. Here is an example:
```yml
customCommands:
- key: X
description: "Copy/paste commits across repos"
commandMenu:
- key: c
command: 'git format-patch --stdout {{.SelectedCommitRange.From}}^..{{.SelectedCommitRange.To}} | pbcopy'
context: commits, subCommits
description: "Copy selected commits to clipboard"
- key: v
command: 'pbpaste | git am'
context: "commits"
description: "Paste selected commits from clipboard"
```
If you use the commandMenu property, none of the other properties except key and description can be used.
## Debugging ## Debugging
If you want to verify that your command actually does what you expect, you can wrap it in an 'echo' call and set `showOutput: true` so that it doesn't actually execute the command but you can see how the placeholders were resolved. If you want to verify that your command actually does what you expect, you can wrap it in an 'echo' call and set `showOutput: true` so that it doesn't actually execute the command but you can see how the placeholders were resolved.

View File

@ -614,12 +614,16 @@ type CustomCommandAfterHook struct {
type CustomCommand struct { type CustomCommand struct {
// The key to trigger the command. Use a single letter or one of the values from https://github.com/jesseduffield/lazygit/blob/master/docs/keybindings/Custom_Keybindings.md // The key to trigger the command. Use a single letter or one of the values from https://github.com/jesseduffield/lazygit/blob/master/docs/keybindings/Custom_Keybindings.md
Key string `yaml:"key"` Key string `yaml:"key"`
// Instead of defining a single custom command, create a menu of custom commands. Useful for grouping related commands together under a single keybinding, and for keeping them out of the global keybindings menu.
// When using this, all other fields except Key and Description are ignored and must be empty.
CommandMenu []CustomCommand `yaml:"commandMenu"`
// The context in which to listen for the key. Valid values are: status, files, worktrees, localBranches, remotes, remoteBranches, tags, commits, reflogCommits, subCommits, commitFiles, stash, and global. Multiple contexts separated by comma are allowed; most useful for "commits, subCommits" or "files, commitFiles". // The context in which to listen for the key. Valid values are: status, files, worktrees, localBranches, remotes, remoteBranches, tags, commits, reflogCommits, subCommits, commitFiles, stash, and global. Multiple contexts separated by comma are allowed; most useful for "commits, subCommits" or "files, commitFiles".
Context string `yaml:"context" jsonschema:"example=status,example=files,example=worktrees,example=localBranches,example=remotes,example=remoteBranches,example=tags,example=commits,example=reflogCommits,example=subCommits,example=commitFiles,example=stash,example=global"` Context string `yaml:"context" jsonschema:"example=status,example=files,example=worktrees,example=localBranches,example=remotes,example=remoteBranches,example=tags,example=commits,example=reflogCommits,example=subCommits,example=commitFiles,example=stash,example=global"`
// The command to run (using Go template syntax for placeholder values) // The command to run (using Go template syntax for placeholder values)
Command string `yaml:"command" jsonschema:"example=git fetch {{.Form.Remote}} {{.Form.Branch}} && git checkout FETCH_HEAD"` Command string `yaml:"command" jsonschema:"example=git fetch {{.Form.Remote}} {{.Form.Branch}} && git checkout FETCH_HEAD"`
// If true, run the command in a subprocess (e.g. if the command requires user input) // If true, run the command in a subprocess (e.g. if the command requires user input)
Subprocess bool `yaml:"subprocess"` // [dev] Pointer to bool so that we can distinguish unset (nil) from false.
Subprocess *bool `yaml:"subprocess"`
// A list of prompts that will request user input before running the final command // A list of prompts that will request user input before running the final command
Prompts []CustomCommandPrompt `yaml:"prompts"` Prompts []CustomCommandPrompt `yaml:"prompts"`
// Text to display while waiting for command to finish // Text to display while waiting for command to finish
@ -627,13 +631,24 @@ type CustomCommand struct {
// Label for the custom command when displayed in the keybindings menu // Label for the custom command when displayed in the keybindings menu
Description string `yaml:"description"` Description string `yaml:"description"`
// If true, stream the command's output to the Command Log panel // If true, stream the command's output to the Command Log panel
Stream bool `yaml:"stream"` // [dev] Pointer to bool so that we can distinguish unset (nil) from false.
Stream *bool `yaml:"stream"`
// If true, show the command's output in a popup within Lazygit // If true, show the command's output in a popup within Lazygit
ShowOutput bool `yaml:"showOutput"` // [dev] Pointer to bool so that we can distinguish unset (nil) from false.
ShowOutput *bool `yaml:"showOutput"`
// The title to display in the popup panel if showOutput is true. If left unset, the command will be used as the title. // The title to display in the popup panel if showOutput is true. If left unset, the command will be used as the title.
OutputTitle string `yaml:"outputTitle"` OutputTitle string `yaml:"outputTitle"`
// Actions to take after the command has completed // Actions to take after the command has completed
After CustomCommandAfterHook `yaml:"after"` // [dev] Pointer so that we can tell whether it appears in the config file
After *CustomCommandAfterHook `yaml:"after"`
}
func (c *CustomCommand) GetDescription() string {
if c.Description != "" {
return c.Description
}
return c.Command
} }
type CustomCommandPrompt struct { type CustomCommandPrompt struct {

View File

@ -96,6 +96,23 @@ func validateCustomCommands(customCommands []CustomCommand) error {
if err := validateCustomCommandKey(customCommand.Key); err != nil { if err := validateCustomCommandKey(customCommand.Key); err != nil {
return err return err
} }
if len(customCommand.CommandMenu) > 0 &&
(len(customCommand.Context) > 0 ||
len(customCommand.Command) > 0 ||
customCommand.Subprocess != nil ||
len(customCommand.Prompts) > 0 ||
len(customCommand.LoadingText) > 0 ||
customCommand.Stream != nil ||
customCommand.ShowOutput != nil ||
len(customCommand.OutputTitle) > 0 ||
customCommand.After != nil) {
commandRef := ""
if len(customCommand.Key) > 0 {
commandRef = fmt.Sprintf(" with key '%s'", customCommand.Key)
}
return fmt.Errorf("Error with custom command%s: it is not allowed to use both commandMenu and any of the other fields except key and description.", commandRef)
}
} }
return nil return nil
} }

View File

@ -74,6 +74,58 @@ func TestUserConfigValidate_enums(t *testing.T) {
{value: "invalid_value", valid: false}, {value: "invalid_value", valid: false},
}, },
}, },
{
name: "Custom command sub menu",
setup: func(config *UserConfig, _ string) {
config.CustomCommands = []CustomCommand{
{
Key: "X",
Description: "My Custom Commands",
CommandMenu: []CustomCommand{
{Key: "1", Command: "echo 'hello'", Context: "global"},
},
},
}
},
testCases: []testCase{
{value: "", valid: true},
},
},
{
name: "Custom command sub menu",
setup: func(config *UserConfig, _ string) {
config.CustomCommands = []CustomCommand{
{
Key: "X",
Context: "global",
CommandMenu: []CustomCommand{
{Key: "1", Command: "echo 'hello'", Context: "global"},
},
},
}
},
testCases: []testCase{
{value: "", valid: false},
},
},
{
name: "Custom command sub menu",
setup: func(config *UserConfig, _ string) {
falseVal := false
config.CustomCommands = []CustomCommand{
{
Key: "X",
Subprocess: &falseVal,
CommandMenu: []CustomCommand{
{Key: "1", Command: "echo 'hello'", Context: "global"},
},
},
}
},
testCases: []testCase{
{value: "", valid: false},
},
},
} }
for _, s := range scenarios { for _, s := range scenarios {

View File

@ -1,15 +1,19 @@
package custom_commands package custom_commands
import ( import (
"github.com/jesseduffield/lazygit/pkg/common" "github.com/jesseduffield/gocui"
"github.com/jesseduffield/lazygit/pkg/config"
"github.com/jesseduffield/lazygit/pkg/gui/controllers/helpers" "github.com/jesseduffield/lazygit/pkg/gui/controllers/helpers"
"github.com/jesseduffield/lazygit/pkg/gui/keybindings"
"github.com/jesseduffield/lazygit/pkg/gui/types" "github.com/jesseduffield/lazygit/pkg/gui/types"
"github.com/jesseduffield/lazygit/pkg/i18n"
"github.com/samber/lo"
) )
// Client is the entry point to this package. It returns a list of keybindings based on the config's user-defined custom commands. // Client is the entry point to this package. It returns a list of keybindings based on the config's user-defined custom commands.
// See https://github.com/jesseduffield/lazygit/blob/master/docs/Custom_Command_Keybindings.md for more info. // See https://github.com/jesseduffield/lazygit/blob/master/docs/Custom_Command_Keybindings.md for more info.
type Client struct { type Client struct {
c *common.Common c *helpers.HelperCommon
handlerCreator *HandlerCreator handlerCreator *HandlerCreator
keybindingCreator *KeybindingCreator keybindingCreator *KeybindingCreator
} }
@ -28,7 +32,7 @@ func NewClient(
keybindingCreator := NewKeybindingCreator(c) keybindingCreator := NewKeybindingCreator(c)
return &Client{ return &Client{
c: c.Common, c: c,
keybindingCreator: keybindingCreator, keybindingCreator: keybindingCreator,
handlerCreator: handlerCreator, handlerCreator: handlerCreator,
} }
@ -37,13 +41,81 @@ func NewClient(
func (self *Client) GetCustomCommandKeybindings() ([]*types.Binding, error) { func (self *Client) GetCustomCommandKeybindings() ([]*types.Binding, error) {
bindings := []*types.Binding{} bindings := []*types.Binding{}
for _, customCommand := range self.c.UserConfig().CustomCommands { for _, customCommand := range self.c.UserConfig().CustomCommands {
handler := self.handlerCreator.call(customCommand) if len(customCommand.CommandMenu) > 0 {
compoundBindings, err := self.keybindingCreator.call(customCommand, handler) handler := func() error {
if err != nil { return self.showCustomCommandsMenu(customCommand)
return nil, err }
bindings = append(bindings, &types.Binding{
ViewName: "", // custom commands menus are global; we filter the commands inside by context
Key: keybindings.GetKey(customCommand.Key),
Modifier: gocui.ModNone,
Handler: handler,
Description: getCustomCommandsMenuDescription(customCommand, self.c.Tr),
OpensMenu: true,
})
} else {
handler := self.handlerCreator.call(customCommand)
compoundBindings, err := self.keybindingCreator.call(customCommand, handler)
if err != nil {
return nil, err
}
bindings = append(bindings, compoundBindings...)
} }
bindings = append(bindings, compoundBindings...)
} }
return bindings, nil return bindings, nil
} }
func (self *Client) showCustomCommandsMenu(customCommand config.CustomCommand) error {
menuItems := make([]*types.MenuItem, 0, len(customCommand.CommandMenu))
for _, subCommand := range customCommand.CommandMenu {
if len(subCommand.CommandMenu) > 0 {
handler := func() error {
return self.showCustomCommandsMenu(subCommand)
}
menuItems = append(menuItems, &types.MenuItem{
Label: subCommand.GetDescription(),
Key: keybindings.GetKey(subCommand.Key),
OnPress: handler,
OpensMenu: true,
})
} else {
if subCommand.Context != "" && subCommand.Context != "global" {
viewNames, err := self.keybindingCreator.getViewNamesAndContexts(subCommand)
if err != nil {
return err
}
currentView := self.c.GocuiGui().CurrentView()
enabled := currentView != nil && lo.Contains(viewNames, currentView.Name())
if !enabled {
continue
}
}
menuItems = append(menuItems, &types.MenuItem{
Label: subCommand.GetDescription(),
Key: keybindings.GetKey(subCommand.Key),
OnPress: self.handlerCreator.call(subCommand),
})
}
}
if len(menuItems) == 0 {
menuItems = append(menuItems, &types.MenuItem{
Label: self.c.Tr.NoApplicableCommandsInThisContext,
OnPress: func() error { return nil },
})
}
title := getCustomCommandsMenuDescription(customCommand, self.c.Tr)
return self.c.Menu(types.CreateMenuOptions{Title: title, Items: menuItems, HideCancel: true})
}
func getCustomCommandsMenuDescription(customCommand config.CustomCommand, tr *i18n.TranslationSet) string {
if customCommand.Description != "" {
return customCommand.Description
}
return tr.CustomCommands
}

View File

@ -261,7 +261,7 @@ func (self *HandlerCreator) finalHandler(customCommand config.CustomCommand, ses
cmdObj := self.c.OS().Cmd.NewShell(cmdStr) cmdObj := self.c.OS().Cmd.NewShell(cmdStr)
if customCommand.Subprocess { if customCommand.Subprocess != nil && *customCommand.Subprocess {
return self.c.RunSubprocessAndRefresh(cmdObj) return self.c.RunSubprocessAndRefresh(cmdObj)
} }
@ -273,7 +273,7 @@ func (self *HandlerCreator) finalHandler(customCommand config.CustomCommand, ses
return self.c.WithWaitingStatus(loadingText, func(gocui.Task) error { return self.c.WithWaitingStatus(loadingText, func(gocui.Task) error {
self.c.LogAction(self.c.Tr.Actions.CustomCommand) self.c.LogAction(self.c.Tr.Actions.CustomCommand)
if customCommand.Stream { if customCommand.Stream != nil && *customCommand.Stream {
cmdObj.StreamOutput() cmdObj.StreamOutput()
} }
output, err := cmdObj.RunWithOutput() output, err := cmdObj.RunWithOutput()
@ -283,14 +283,14 @@ func (self *HandlerCreator) finalHandler(customCommand config.CustomCommand, ses
} }
if err != nil { if err != nil {
if customCommand.After.CheckForConflicts { if customCommand.After != nil && customCommand.After.CheckForConflicts {
return self.mergeAndRebaseHelper.CheckForConflicts(err) return self.mergeAndRebaseHelper.CheckForConflicts(err)
} }
return err return err
} }
if customCommand.ShowOutput { if customCommand.ShowOutput != nil && *customCommand.ShowOutput {
if strings.TrimSpace(output) == "" { if strings.TrimSpace(output) == "" {
output = self.c.Tr.EmptyOutput output = self.c.Tr.EmptyOutput
} }

View File

@ -34,18 +34,13 @@ func (self *KeybindingCreator) call(customCommand config.CustomCommand, handler
return nil, err return nil, err
} }
description := customCommand.Description
if description == "" {
description = customCommand.Command
}
return lo.Map(viewNames, func(viewName string, _ int) *types.Binding { return lo.Map(viewNames, func(viewName string, _ int) *types.Binding {
return &types.Binding{ return &types.Binding{
ViewName: viewName, ViewName: viewName,
Key: keybindings.GetKey(customCommand.Key), Key: keybindings.GetKey(customCommand.Key),
Modifier: gocui.ModNone, Modifier: gocui.ModNone,
Handler: handler, Handler: handler,
Description: description, Description: customCommand.GetDescription(),
} }
}), nil }), nil
} }

View File

@ -14,8 +14,8 @@ import (
// compatibility. We already did this for Commit.Sha, which was renamed to Hash. // compatibility. We already did this for Commit.Sha, which was renamed to Hash.
type Commit struct { type Commit struct {
Hash string // deprecated: use Sha Hash string
Sha string Sha string // deprecated: use Hash
Name string Name string
Status models.CommitStatus Status models.CommitStatus
Action todo.TodoCommand Action todo.TodoCommand

View File

@ -843,6 +843,8 @@ type TranslationSet struct {
RangeSelectNotSupportedForSubmodules string RangeSelectNotSupportedForSubmodules string
OldCherryPickKeyWarning string OldCherryPickKeyWarning string
CommandDoesNotSupportOpeningInEditor string CommandDoesNotSupportOpeningInEditor string
CustomCommands string
NoApplicableCommandsInThisContext string
Actions Actions Actions Actions
Bisect Bisect Bisect Bisect
Log Log Log Log
@ -1879,6 +1881,8 @@ func EnglishTranslationSet() *TranslationSet {
RangeSelectNotSupportedForSubmodules: "Range select not supported for submodules", RangeSelectNotSupportedForSubmodules: "Range select not supported for submodules",
OldCherryPickKeyWarning: "The 'c' key is no longer the default key for copying commits to cherry pick. Please use `{{.copy}}` instead (and `{{.paste}}` to paste). The reason for this change is that the 'v' key for selecting a range of lines when staging is now also used for selecting a range of lines in any list view, meaning that we needed to find a new key for pasting commits, and if we're going to now use `{{.paste}}` for pasting commits, we may as well use `{{.copy}}` for copying them. If you want to configure the keybindings to get the old behaviour, set the following in your config:\n\nkeybinding:\n universal:\n toggleRangeSelect: <something other than v>\n commits:\n cherryPickCopy: 'c'\n pasteCommits: 'v'", OldCherryPickKeyWarning: "The 'c' key is no longer the default key for copying commits to cherry pick. Please use `{{.copy}}` instead (and `{{.paste}}` to paste). The reason for this change is that the 'v' key for selecting a range of lines when staging is now also used for selecting a range of lines in any list view, meaning that we needed to find a new key for pasting commits, and if we're going to now use `{{.paste}}` for pasting commits, we may as well use `{{.copy}}` for copying them. If you want to configure the keybindings to get the old behaviour, set the following in your config:\n\nkeybinding:\n universal:\n toggleRangeSelect: <something other than v>\n commits:\n cherryPickCopy: 'c'\n pasteCommits: 'v'",
CommandDoesNotSupportOpeningInEditor: "This command doesn't support switching to the editor", CommandDoesNotSupportOpeningInEditor: "This command doesn't support switching to the editor",
CustomCommands: "Custom commands",
NoApplicableCommandsInThisContext: "(No applicable commands in this context)",
Actions: Actions{ Actions: Actions{
// TODO: combine this with the original keybinding descriptions (those are all in lowercase atm) // TODO: combine this with the original keybinding descriptions (those are all in lowercase atm)

View File

@ -19,7 +19,7 @@ var CheckForConflicts = NewIntegrationTest(NewIntegrationTestArgs{
Key: "m", Key: "m",
Context: "localBranches", Context: "localBranches",
Command: "git merge {{ .SelectedLocalBranch.Name | quote }}", Command: "git merge {{ .SelectedLocalBranch.Name | quote }}",
After: config.CustomCommandAfterHook{ After: &config.CustomCommandAfterHook{
CheckForConflicts: true, CheckForConflicts: true,
}, },
}, },

View File

@ -0,0 +1,75 @@
package custom_commands
import (
"github.com/jesseduffield/lazygit/pkg/config"
. "github.com/jesseduffield/lazygit/pkg/integration/components"
)
var CustomCommandsSubmenu = NewIntegrationTest(NewIntegrationTestArgs{
Description: "Using custom commands from a custom commands menu",
ExtraCmdArgs: []string{},
Skip: false,
SetupRepo: func(shell *Shell) {},
SetupConfig: func(cfg *config.AppConfig) {
cfg.GetUserConfig().CustomCommands = []config.CustomCommand{
{
Key: "x",
Description: "My Custom Commands",
CommandMenu: []config.CustomCommand{
{
Key: "1",
Context: "global",
Command: "touch myfile-global",
},
{
Key: "2",
Context: "files",
Command: "touch myfile-files",
},
{
Key: "3",
Context: "commits",
Command: "touch myfile-commits",
},
},
},
}
},
Run: func(t *TestDriver, keys config.KeybindingConfig) {
t.Views().Files().
Focus().
IsEmpty().
Press("x").
Tap(func() {
t.ExpectPopup().Menu().
Title(Equals("My Custom Commands")).
Lines(
Contains("1 touch myfile-global"),
Contains("2 touch myfile-files"),
).
Select(Contains("touch myfile-files")).Confirm()
}).
Lines(
Contains("myfile-files"),
)
t.Views().Commits().
Focus().
Press("x").
Tap(func() {
t.ExpectPopup().Menu().
Title(Equals("My Custom Commands")).
Lines(
Contains("1 touch myfile-global"),
Contains("3 touch myfile-commits"),
)
t.GlobalPress("3")
})
t.Views().Files().
Lines(
Contains("myfile-commits"),
Contains("myfile-files"),
)
},
})

View File

@ -15,10 +15,9 @@ var GlobalContext = NewIntegrationTest(NewIntegrationTestArgs{
SetupConfig: func(cfg *config.AppConfig) { SetupConfig: func(cfg *config.AppConfig) {
cfg.GetUserConfig().CustomCommands = []config.CustomCommand{ cfg.GetUserConfig().CustomCommands = []config.CustomCommand{
{ {
Key: "X", Key: "X",
Context: "global", Context: "global",
Command: "touch myfile", Command: "touch myfile",
ShowOutput: false,
}, },
} }
}, },

View File

@ -15,10 +15,9 @@ var MultipleContexts = NewIntegrationTest(NewIntegrationTestArgs{
SetupConfig: func(cfg *config.AppConfig) { SetupConfig: func(cfg *config.AppConfig) {
cfg.GetUserConfig().CustomCommands = []config.CustomCommand{ cfg.GetUserConfig().CustomCommands = []config.CustomCommand{
{ {
Key: "X", Key: "X",
Context: "commits, reflogCommits", Context: "commits, reflogCommits",
Command: "touch myfile", Command: "touch myfile",
ShowOutput: false,
}, },
} }
}, },

View File

@ -15,18 +15,19 @@ var ShowOutputInPanel = NewIntegrationTest(NewIntegrationTestArgs{
shell.EmptyCommit("my change") shell.EmptyCommit("my change")
}, },
SetupConfig: func(cfg *config.AppConfig) { SetupConfig: func(cfg *config.AppConfig) {
trueVal := true
cfg.GetUserConfig().CustomCommands = []config.CustomCommand{ cfg.GetUserConfig().CustomCommands = []config.CustomCommand{
{ {
Key: "X", Key: "X",
Context: "commits", Context: "commits",
Command: "printf '%s' '{{ .SelectedLocalCommit.Name }}'", Command: "printf '%s' '{{ .SelectedLocalCommit.Name }}'",
ShowOutput: true, ShowOutput: &trueVal,
}, },
{ {
Key: "Y", Key: "Y",
Context: "commits", Context: "commits",
Command: "printf '%s' '{{ .SelectedLocalCommit.Name }}'", Command: "printf '%s' '{{ .SelectedLocalCommit.Name }}'",
ShowOutput: true, ShowOutput: &trueVal,
OutputTitle: "Subject of commit {{ .SelectedLocalCommit.Hash }}", OutputTitle: "Subject of commit {{ .SelectedLocalCommit.Hash }}",
}, },
} }

View File

@ -140,6 +140,7 @@ var tests = []*components.IntegrationTest{
custom_commands.AccessCommitProperties, custom_commands.AccessCommitProperties,
custom_commands.BasicCommand, custom_commands.BasicCommand,
custom_commands.CheckForConflicts, custom_commands.CheckForConflicts,
custom_commands.CustomCommandsSubmenu,
custom_commands.FormPrompts, custom_commands.FormPrompts,
custom_commands.GlobalContext, custom_commands.GlobalContext,
custom_commands.MenuFromCommand, custom_commands.MenuFromCommand,

View File

@ -63,6 +63,13 @@
"type": "string", "type": "string",
"description": "The key to trigger the command. Use a single letter or one of the values from https://github.com/jesseduffield/lazygit/blob/master/docs/keybindings/Custom_Keybindings.md" "description": "The key to trigger the command. Use a single letter or one of the values from https://github.com/jesseduffield/lazygit/blob/master/docs/keybindings/Custom_Keybindings.md"
}, },
"commandMenu": {
"items": {
"$ref": "#/$defs/CustomCommand"
},
"type": "array",
"description": "Instead of defining a single custom command, create a menu of custom commands. Useful for grouping related commands together under a single keybinding, and for keeping them out of the global keybindings menu.\nWhen using this, all other fields except Key and Description are ignored and must be empty."
},
"context": { "context": {
"type": "string", "type": "string",
"description": "The context in which to listen for the key. Valid values are: status, files, worktrees, localBranches, remotes, remoteBranches, tags, commits, reflogCommits, subCommits, commitFiles, stash, and global. Multiple contexts separated by comma are allowed; most useful for \"commits, subCommits\" or \"files, commitFiles\".", "description": "The context in which to listen for the key. Valid values are: status, files, worktrees, localBranches, remotes, remoteBranches, tags, commits, reflogCommits, subCommits, commitFiles, stash, and global. Multiple contexts separated by comma are allowed; most useful for \"commits, subCommits\" or \"files, commitFiles\".",