Skip to content

Commit 22512d5

Browse files
committed
Add commandMenu property to custom commands
1 parent e799976 commit 22512d5

File tree

7 files changed

+191
-8
lines changed

7 files changed

+191
-8
lines changed

docs/Custom_Command_Keybindings.md

Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -324,6 +324,27 @@ We don't support accessing all elements of a range selection yet. We might add t
324324

325325
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)
326326

327+
## Menus of custom commands
328+
329+
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:
330+
331+
```yml
332+
customCommands:
333+
- key: X
334+
description: "Copy/paste commits across repos"
335+
commandMenu:
336+
- key: c
337+
command: 'git format-patch --stdout {{.SelectedCommitRange.From}}^..{{.SelectedCommitRange.To}} | pbcopy'
338+
context: commits, subCommits
339+
description: "Copy selected commits to clipboard"
340+
- key: v
341+
command: 'pbpaste | git am'
342+
context: "commits"
343+
description: "Paste selected commits from clipboard"
344+
```
345+
346+
If you use the commandMenu property, none of the other properties except key and description can be used.
347+
327348
## Debugging
328349

329350
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.

pkg/config/user_config.go

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -614,6 +614,9 @@ type CustomCommandAfterHook struct {
614614
type CustomCommand struct {
615615
// 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
616616
Key string `yaml:"key"`
617+
// 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.
618+
// When using this, all other fields except Key and Description are ignored and must be empty.
619+
CommandMenu []CustomCommand `yaml:"commandMenu"`
617620
// 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".
618621
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"`
619622
// The command to run (using Go template syntax for placeholder values)

pkg/gui/services/custom_commands/client.go

Lines changed: 80 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -1,15 +1,19 @@
11
package custom_commands
22

33
import (
4-
"github.com/jesseduffield/lazygit/pkg/common"
4+
"github.com/jesseduffield/gocui"
5+
"github.com/jesseduffield/lazygit/pkg/config"
56
"github.com/jesseduffield/lazygit/pkg/gui/controllers/helpers"
7+
"github.com/jesseduffield/lazygit/pkg/gui/keybindings"
68
"github.com/jesseduffield/lazygit/pkg/gui/types"
9+
"github.com/jesseduffield/lazygit/pkg/i18n"
10+
"github.com/samber/lo"
711
)
812

913
// Client is the entry point to this package. It returns a list of keybindings based on the config's user-defined custom commands.
1014
// See https://github.com/jesseduffield/lazygit/blob/master/docs/Custom_Command_Keybindings.md for more info.
1115
type Client struct {
12-
c *common.Common
16+
c *helpers.HelperCommon
1317
handlerCreator *HandlerCreator
1418
keybindingCreator *KeybindingCreator
1519
}
@@ -28,7 +32,7 @@ func NewClient(
2832
keybindingCreator := NewKeybindingCreator(c)
2933

3034
return &Client{
31-
c: c.Common,
35+
c: c,
3236
keybindingCreator: keybindingCreator,
3337
handlerCreator: handlerCreator,
3438
}
@@ -37,13 +41,81 @@ func NewClient(
3741
func (self *Client) GetCustomCommandKeybindings() ([]*types.Binding, error) {
3842
bindings := []*types.Binding{}
3943
for _, customCommand := range self.c.UserConfig().CustomCommands {
40-
handler := self.handlerCreator.call(customCommand)
41-
compoundBindings, err := self.keybindingCreator.call(customCommand, handler)
42-
if err != nil {
43-
return nil, err
44+
if len(customCommand.CommandMenu) > 0 {
45+
handler := func() error {
46+
return self.showCustomCommandsMenu(customCommand)
47+
}
48+
bindings = append(bindings, &types.Binding{
49+
ViewName: "", // custom commands menus are global; we filter the commands inside by context
50+
Key: keybindings.GetKey(customCommand.Key),
51+
Modifier: gocui.ModNone,
52+
Handler: handler,
53+
Description: getCustomCommandsMenuDescription(customCommand, self.c.Tr),
54+
OpensMenu: true,
55+
})
56+
} else {
57+
handler := self.handlerCreator.call(customCommand)
58+
compoundBindings, err := self.keybindingCreator.call(customCommand, handler)
59+
if err != nil {
60+
return nil, err
61+
}
62+
bindings = append(bindings, compoundBindings...)
4463
}
45-
bindings = append(bindings, compoundBindings...)
4664
}
4765

4866
return bindings, nil
4967
}
68+
69+
func (self *Client) showCustomCommandsMenu(customCommand config.CustomCommand) error {
70+
menuItems := make([]*types.MenuItem, 0, len(customCommand.CommandMenu))
71+
for _, subCommand := range customCommand.CommandMenu {
72+
if len(subCommand.CommandMenu) > 0 {
73+
handler := func() error {
74+
return self.showCustomCommandsMenu(subCommand)
75+
}
76+
menuItems = append(menuItems, &types.MenuItem{
77+
Label: subCommand.GetDescription(),
78+
Key: keybindings.GetKey(subCommand.Key),
79+
OnPress: handler,
80+
OpensMenu: true,
81+
})
82+
} else {
83+
if subCommand.Context != "" && subCommand.Context != "global" {
84+
viewNames, err := self.keybindingCreator.getViewNamesAndContexts(subCommand)
85+
if err != nil {
86+
return err
87+
}
88+
89+
currentView := self.c.GocuiGui().CurrentView()
90+
enabled := currentView != nil && lo.Contains(viewNames, currentView.Name())
91+
if !enabled {
92+
continue
93+
}
94+
}
95+
96+
menuItems = append(menuItems, &types.MenuItem{
97+
Label: subCommand.GetDescription(),
98+
Key: keybindings.GetKey(subCommand.Key),
99+
OnPress: self.handlerCreator.call(subCommand),
100+
})
101+
}
102+
}
103+
104+
if len(menuItems) == 0 {
105+
menuItems = append(menuItems, &types.MenuItem{
106+
Label: self.c.Tr.NoApplicableCommandsInThisContext,
107+
OnPress: func() error { return nil },
108+
})
109+
}
110+
111+
title := getCustomCommandsMenuDescription(customCommand, self.c.Tr)
112+
return self.c.Menu(types.CreateMenuOptions{Title: title, Items: menuItems, HideCancel: true})
113+
}
114+
115+
func getCustomCommandsMenuDescription(customCommand config.CustomCommand, tr *i18n.TranslationSet) string {
116+
if customCommand.Description != "" {
117+
return customCommand.Description
118+
}
119+
120+
return tr.CustomCommands
121+
}

pkg/i18n/english.go

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -843,6 +843,8 @@ type TranslationSet struct {
843843
RangeSelectNotSupportedForSubmodules string
844844
OldCherryPickKeyWarning string
845845
CommandDoesNotSupportOpeningInEditor string
846+
CustomCommands string
847+
NoApplicableCommandsInThisContext string
846848
Actions Actions
847849
Bisect Bisect
848850
Log Log
@@ -1879,6 +1881,8 @@ func EnglishTranslationSet() *TranslationSet {
18791881
RangeSelectNotSupportedForSubmodules: "Range select not supported for submodules",
18801882
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'",
18811883
CommandDoesNotSupportOpeningInEditor: "This command doesn't support switching to the editor",
1884+
CustomCommands: "Custom commands",
1885+
NoApplicableCommandsInThisContext: "(No applicable commands in this context)",
18821886

18831887
Actions: Actions{
18841888
// TODO: combine this with the original keybinding descriptions (those are all in lowercase atm)
Lines changed: 75 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,75 @@
1+
package custom_commands
2+
3+
import (
4+
"github.com/jesseduffield/lazygit/pkg/config"
5+
. "github.com/jesseduffield/lazygit/pkg/integration/components"
6+
)
7+
8+
var CustomCommandsSubmenu = NewIntegrationTest(NewIntegrationTestArgs{
9+
Description: "Using custom commands from a custom commands menu",
10+
ExtraCmdArgs: []string{},
11+
Skip: false,
12+
SetupRepo: func(shell *Shell) {},
13+
SetupConfig: func(cfg *config.AppConfig) {
14+
cfg.GetUserConfig().CustomCommands = []config.CustomCommand{
15+
{
16+
Key: "x",
17+
Description: "My Custom Commands",
18+
CommandMenu: []config.CustomCommand{
19+
{
20+
Key: "1",
21+
Context: "global",
22+
Command: "touch myfile-global",
23+
},
24+
{
25+
Key: "2",
26+
Context: "files",
27+
Command: "touch myfile-files",
28+
},
29+
{
30+
Key: "3",
31+
Context: "commits",
32+
Command: "touch myfile-commits",
33+
},
34+
},
35+
},
36+
}
37+
},
38+
Run: func(t *TestDriver, keys config.KeybindingConfig) {
39+
t.Views().Files().
40+
Focus().
41+
IsEmpty().
42+
Press("x").
43+
Tap(func() {
44+
t.ExpectPopup().Menu().
45+
Title(Equals("My Custom Commands")).
46+
Lines(
47+
Contains("1 touch myfile-global"),
48+
Contains("2 touch myfile-files"),
49+
).
50+
Select(Contains("touch myfile-files")).Confirm()
51+
}).
52+
Lines(
53+
Contains("myfile-files"),
54+
)
55+
56+
t.Views().Commits().
57+
Focus().
58+
Press("x").
59+
Tap(func() {
60+
t.ExpectPopup().Menu().
61+
Title(Equals("My Custom Commands")).
62+
Lines(
63+
Contains("1 touch myfile-global"),
64+
Contains("3 touch myfile-commits"),
65+
)
66+
t.GlobalPress("3")
67+
})
68+
69+
t.Views().Files().
70+
Lines(
71+
Contains("myfile-commits"),
72+
Contains("myfile-files"),
73+
)
74+
},
75+
})

pkg/integration/tests/test_list.go

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -140,6 +140,7 @@ var tests = []*components.IntegrationTest{
140140
custom_commands.AccessCommitProperties,
141141
custom_commands.BasicCommand,
142142
custom_commands.CheckForConflicts,
143+
custom_commands.CustomCommandsSubmenu,
143144
custom_commands.FormPrompts,
144145
custom_commands.GlobalContext,
145146
custom_commands.MenuFromCommand,

schema/config.json

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -63,6 +63,13 @@
6363
"type": "string",
6464
"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"
6565
},
66+
"commandMenu": {
67+
"items": {
68+
"$ref": "#/$defs/CustomCommand"
69+
},
70+
"type": "array",
71+
"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."
72+
},
6673
"context": {
6774
"type": "string",
6875
"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\".",

0 commit comments

Comments
 (0)