Skip to content

Commit a9f9dee

Browse files
committed
Combine customCommand's subprocess, stream, and showOutput fields into a single output enum
1 parent 5f4be3b commit a9f9dee

File tree

9 files changed

+187
-43
lines changed

9 files changed

+187
-43
lines changed

docs/Custom_Command_Keybindings.md

Lines changed: 4 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -14,7 +14,7 @@ customCommands:
1414
- key: 'C'
1515
context: 'global'
1616
command: "git commit"
17-
subprocess: true
17+
output: terminal
1818
- key: 'n'
1919
context: 'localBranches'
2020
prompts:
@@ -53,13 +53,11 @@ For a given custom command, here are the allowed fields:
5353
| key | The key to trigger the command. Use a single letter or one of the values from [here](https://github.com/jesseduffield/lazygit/blob/master/docs/keybindings/Custom_Keybindings.md). Custom commands without a key specified can be triggered by selecting them from the keybindings (`?`) menu | no |
5454
| command | The command to run (using Go template syntax for placeholder values) | yes |
5555
| context | The context in which to listen for the key (see [below](#contexts)) | yes |
56-
| subprocess | Whether you want the command to run in a subprocess (e.g. if the command requires user input) | no |
5756
| prompts | A list of prompts that will request user input before running the final command | no |
5857
| loadingText | Text to display while waiting for command to finish | no |
5958
| description | Label for the custom command when displayed in the keybindings menu | no |
60-
| stream | Whether you want to stream the command's output to the Command Log panel | no |
61-
| showOutput | Whether you want to show the command's output in a popup within Lazygit | no |
62-
| outputTitle | The title to display in the popup panel if showOutput is true. If left unset, the command will be used as the title. | no |
59+
| output | Where the output of the command should go. 'none' discards it, 'terminal' suspends lazygit and runs the command in the terminal (useful for commands that require user input), 'log' streams it to the command log, 'logWithPty' is like 'log' but runs the command in a pseudo terminal (can be useful for commands that produce colored output when the output is a terminal), and 'popup' shows it in a popup. | no |
60+
| outputTitle | The title to display in the popup panel if output is set to 'popup'. If left unset, the command will be used as the title. | no |
6361
| after | Actions to take after the command has completed | no |
6462

6563
Here are the options for the `after` key:
@@ -365,7 +363,7 @@ If you use the commandMenu property, none of the other properties except key and
365363

366364
## Debugging
367365

368-
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.
366+
If you want to verify that your command actually does what you expect, you can wrap it in an 'echo' call and set `output: popup` so that it doesn't actually execute the command but you can see how the placeholders were resolved.
369367

370368
## More Examples
371369

pkg/config/app_config.go

Lines changed: 45 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -281,6 +281,11 @@ func computeMigratedConfig(path string, content []byte) ([]byte, error) {
281281
return nil, fmt.Errorf("Couldn't migrate config file at `%s`: %s", path, err)
282282
}
283283

284+
err = changeCustomCommandStreamAndOutputToOutputEnum(&rootNode)
285+
if err != nil {
286+
return nil, fmt.Errorf("Couldn't migrate config file at `%s`: %s", path, err)
287+
}
288+
284289
// Add more migrations here...
285290

286291
if !reflect.DeepEqual(rootNode, originalCopy) {
@@ -341,6 +346,46 @@ func changeCommitPrefixesMap(rootNode *yaml.Node) error {
341346
})
342347
}
343348

349+
func changeCustomCommandStreamAndOutputToOutputEnum(rootNode *yaml.Node) error {
350+
return yaml_utils.Walk(rootNode, func(node *yaml.Node, path string) {
351+
// We are being lazy here and rely on the fact that the only mapping
352+
// nodes in the tree under customCommands are actual custom commands. If
353+
// this ever changes (e.g. because we add a struct field to
354+
// customCommand), then we need to change this to iterate properly.
355+
if strings.HasPrefix(path, "customCommands[") && node.Kind == yaml.MappingNode {
356+
output := ""
357+
if streamKey, streamValue := yaml_utils.RemoveKey(node, "subprocess"); streamKey != nil {
358+
if streamValue.Kind == yaml.ScalarNode && streamValue.Value == "true" {
359+
output = "terminal"
360+
}
361+
}
362+
if streamKey, streamValue := yaml_utils.RemoveKey(node, "stream"); streamKey != nil {
363+
if streamValue.Kind == yaml.ScalarNode && streamValue.Value == "true" && output == "" {
364+
output = "log"
365+
}
366+
}
367+
if streamKey, streamValue := yaml_utils.RemoveKey(node, "showOutput"); streamKey != nil {
368+
if streamValue.Kind == yaml.ScalarNode && streamValue.Value == "true" && output == "" {
369+
output = "popup"
370+
}
371+
}
372+
if output != "" {
373+
outputKeyNode := &yaml.Node{
374+
Kind: yaml.ScalarNode,
375+
Value: "output",
376+
Tag: "!!str",
377+
}
378+
outputValueNode := &yaml.Node{
379+
Kind: yaml.ScalarNode,
380+
Value: output,
381+
Tag: "!!str",
382+
}
383+
node.Content = append(node.Content, outputKeyNode, outputValueNode)
384+
}
385+
}
386+
})
387+
}
388+
344389
func (c *AppConfig) GetDebug() bool {
345390
return c.debug
346391
}

pkg/config/app_config_test.go

Lines changed: 86 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -88,6 +88,92 @@ git:
8888
}
8989
}
9090

91+
func TestCustomCommandsOutputMigration(t *testing.T) {
92+
scenarios := []struct {
93+
name string
94+
input string
95+
expected string
96+
}{
97+
{
98+
name: "Empty String",
99+
input: "",
100+
expected: "",
101+
}, {
102+
name: "Convert subprocess to output=terminal",
103+
input: `customCommands:
104+
- command: echo 'hello'
105+
subprocess: true
106+
`,
107+
expected: `customCommands:
108+
- command: echo 'hello'
109+
output: terminal
110+
`,
111+
}, {
112+
name: "Convert stream to output=log",
113+
input: `customCommands:
114+
- command: echo 'hello'
115+
stream: true
116+
`,
117+
expected: `customCommands:
118+
- command: echo 'hello'
119+
output: log
120+
`,
121+
}, {
122+
name: "Convert showOutput to output=popup",
123+
input: `customCommands:
124+
- command: echo 'hello'
125+
showOutput: true
126+
`,
127+
expected: `customCommands:
128+
- command: echo 'hello'
129+
output: popup
130+
`,
131+
}, {
132+
name: "Subprocess wins over the other two",
133+
input: `customCommands:
134+
- command: echo 'hello'
135+
subprocess: true
136+
stream: true
137+
showOutput: true
138+
`,
139+
expected: `customCommands:
140+
- command: echo 'hello'
141+
output: terminal
142+
`,
143+
}, {
144+
name: "Stream wins over showOutput",
145+
input: `customCommands:
146+
- command: echo 'hello'
147+
stream: true
148+
showOutput: true
149+
`,
150+
expected: `customCommands:
151+
- command: echo 'hello'
152+
output: log
153+
`,
154+
}, {
155+
name: "Explicitly setting to false doesn't create an output=none key",
156+
input: `customCommands:
157+
- command: echo 'hello'
158+
subprocess: false
159+
stream: false
160+
showOutput: false
161+
`,
162+
expected: `customCommands:
163+
- command: echo 'hello'
164+
`,
165+
},
166+
}
167+
168+
for _, s := range scenarios {
169+
t.Run(s.name, func(t *testing.T) {
170+
actual, err := computeMigratedConfig("path doesn't matter", []byte(s.input))
171+
assert.NoError(t, err)
172+
assert.Equal(t, s.expected, string(actual))
173+
})
174+
}
175+
}
176+
91177
var largeConfiguration = []byte(`
92178
# Config relating to the Lazygit UI
93179
gui:

pkg/config/user_config.go

Lines changed: 3 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -637,22 +637,15 @@ type CustomCommand struct {
637637
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"`
638638
// The command to run (using Go template syntax for placeholder values)
639639
Command string `yaml:"command" jsonschema:"example=git fetch {{.Form.Remote}} {{.Form.Branch}} && git checkout FETCH_HEAD"`
640-
// If true, run the command in a subprocess (e.g. if the command requires user input)
641-
// [dev] Pointer to bool so that we can distinguish unset (nil) from false.
642-
Subprocess *bool `yaml:"subprocess"`
643640
// A list of prompts that will request user input before running the final command
644641
Prompts []CustomCommandPrompt `yaml:"prompts"`
645642
// Text to display while waiting for command to finish
646643
LoadingText string `yaml:"loadingText" jsonschema:"example=Loading..."`
647644
// Label for the custom command when displayed in the keybindings menu
648645
Description string `yaml:"description"`
649-
// If true, stream the command's output to the Command Log panel
650-
// [dev] Pointer to bool so that we can distinguish unset (nil) from false.
651-
Stream *bool `yaml:"stream"`
652-
// If true, show the command's output in a popup within Lazygit
653-
// [dev] Pointer to bool so that we can distinguish unset (nil) from false.
654-
ShowOutput *bool `yaml:"showOutput"`
655-
// The title to display in the popup panel if showOutput is true. If left unset, the command will be used as the title.
646+
// Where the output of the command should go. 'none' discards it, 'terminal' suspends lazygit and runs the command in the terminal (useful for commands that require user input), 'log' streams it to the command log, 'logWithPty' is like 'log' but runs the command in a pseudo terminal (can be useful for commands that produce colored output when the output is a terminal), and 'popup' shows it in a popup.
647+
Output string `yaml:"output" jsonschema:"enum=none,enum=terminal,enum=log,enum=logWithPty,enum=popup"`
648+
// The title to display in the popup panel if output is set to 'popup'. If left unset, the command will be used as the title.
656649
OutputTitle string `yaml:"outputTitle"`
657650
// Actions to take after the command has completed
658651
// [dev] Pointer so that we can tell whether it appears in the config file

pkg/config/user_config_validation.go

Lines changed: 6 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -104,11 +104,9 @@ func validateCustomCommands(customCommands []CustomCommand) error {
104104
if len(customCommand.CommandMenu) > 0 {
105105
if len(customCommand.Context) > 0 ||
106106
len(customCommand.Command) > 0 ||
107-
customCommand.Subprocess != nil ||
108107
len(customCommand.Prompts) > 0 ||
109108
len(customCommand.LoadingText) > 0 ||
110-
customCommand.Stream != nil ||
111-
customCommand.ShowOutput != nil ||
109+
len(customCommand.Output) > 0 ||
112110
len(customCommand.OutputTitle) > 0 ||
113111
customCommand.After != nil {
114112
commandRef := ""
@@ -121,6 +119,11 @@ func validateCustomCommands(customCommands []CustomCommand) error {
121119
if err := validateCustomCommands(customCommand.CommandMenu); err != nil {
122120
return err
123121
}
122+
} else {
123+
if err := validateEnum("customCommand.output", customCommand.Output,
124+
[]string{"", "none", "terminal", "log", "logWithPty", "popup"}); err != nil {
125+
return err
126+
}
124127
}
125128
}
126129
return nil

pkg/config/user_config_validation_test.go

Lines changed: 21 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -95,6 +95,25 @@ func TestUserConfigValidate_enums(t *testing.T) {
9595
{value: "invalid_value", valid: false},
9696
},
9797
},
98+
{
99+
name: "Custom command output",
100+
setup: func(config *UserConfig, value string) {
101+
config.CustomCommands = []CustomCommand{
102+
{
103+
Output: value,
104+
},
105+
}
106+
},
107+
testCases: []testCase{
108+
{value: "", valid: true},
109+
{value: "none", valid: true},
110+
{value: "terminal", valid: true},
111+
{value: "log", valid: true},
112+
{value: "logWithPty", valid: true},
113+
{value: "popup", valid: true},
114+
{value: "invalid_value", valid: false},
115+
},
116+
},
98117
{
99118
name: "Custom command sub menu",
100119
setup: func(config *UserConfig, _ string) {
@@ -132,11 +151,10 @@ func TestUserConfigValidate_enums(t *testing.T) {
132151
{
133152
name: "Custom command sub menu",
134153
setup: func(config *UserConfig, _ string) {
135-
falseVal := false
136154
config.CustomCommands = []CustomCommand{
137155
{
138-
Key: "X",
139-
Subprocess: &falseVal, // other properties are not allowed for submenus (using subprocess as an example)
156+
Key: "X",
157+
LoadingText: "loading", // other properties are not allowed for submenus (using loadingText as an example)
140158
CommandMenu: []CustomCommand{
141159
{Key: "1", Command: "echo 'hello'", Context: "global"},
142160
},

pkg/gui/services/custom_commands/handler_creator.go

Lines changed: 6 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -262,7 +262,7 @@ func (self *HandlerCreator) finalHandler(customCommand config.CustomCommand, ses
262262

263263
cmdObj := self.c.OS().Cmd.NewShell(cmdStr, self.c.UserConfig().OS.ShellFunctionsFile)
264264

265-
if customCommand.Subprocess != nil && *customCommand.Subprocess {
265+
if customCommand.Output == "terminal" {
266266
return self.c.RunSubprocessAndRefresh(cmdObj)
267267
}
268268

@@ -274,9 +274,12 @@ func (self *HandlerCreator) finalHandler(customCommand config.CustomCommand, ses
274274
return self.c.WithWaitingStatus(loadingText, func(gocui.Task) error {
275275
self.c.LogAction(self.c.Tr.Actions.CustomCommand)
276276

277-
if customCommand.Stream != nil && *customCommand.Stream {
277+
if customCommand.Output == "log" || customCommand.Output == "logWithPty" {
278278
cmdObj.StreamOutput()
279279
}
280+
if customCommand.Output == "logWithPty" {
281+
cmdObj.UsePty()
282+
}
280283
output, err := cmdObj.RunWithOutput()
281284

282285
if refreshErr := self.c.Refresh(types.RefreshOptions{Mode: types.ASYNC}); err != nil {
@@ -291,7 +294,7 @@ func (self *HandlerCreator) finalHandler(customCommand config.CustomCommand, ses
291294
return err
292295
}
293296

294-
if customCommand.ShowOutput != nil && *customCommand.ShowOutput {
297+
if customCommand.Output == "popup" {
295298
if strings.TrimSpace(output) == "" {
296299
output = self.c.Tr.EmptyOutput
297300
}

pkg/integration/tests/custom_commands/show_output_in_panel.go

Lines changed: 5 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -15,19 +15,18 @@ var ShowOutputInPanel = NewIntegrationTest(NewIntegrationTestArgs{
1515
shell.EmptyCommit("my change")
1616
},
1717
SetupConfig: func(cfg *config.AppConfig) {
18-
trueVal := true
1918
cfg.GetUserConfig().CustomCommands = []config.CustomCommand{
2019
{
21-
Key: "X",
22-
Context: "commits",
23-
Command: "printf '%s' '{{ .SelectedLocalCommit.Name }}'",
24-
ShowOutput: &trueVal,
20+
Key: "X",
21+
Context: "commits",
22+
Command: "printf '%s' '{{ .SelectedLocalCommit.Name }}'",
23+
Output: "popup",
2524
},
2625
{
2726
Key: "Y",
2827
Context: "commits",
2928
Command: "printf '%s' '{{ .SelectedLocalCommit.Name }}'",
30-
ShowOutput: &trueVal,
29+
Output: "popup",
3130
OutputTitle: "Subject of commit {{ .SelectedLocalCommit.Hash }}",
3231
},
3332
}

schema/config.json

Lines changed: 11 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -96,10 +96,6 @@
9696
"git fetch {{.Form.Remote}} {{.Form.Branch}} \u0026\u0026 git checkout FETCH_HEAD"
9797
]
9898
},
99-
"subprocess": {
100-
"type": "boolean",
101-
"description": "If true, run the command in a subprocess (e.g. if the command requires user input)"
102-
},
10399
"prompts": {
104100
"items": {
105101
"$ref": "#/$defs/CustomCommandPrompt"
@@ -118,17 +114,20 @@
118114
"type": "string",
119115
"description": "Label for the custom command when displayed in the keybindings menu"
120116
},
121-
"stream": {
122-
"type": "boolean",
123-
"description": "If true, stream the command's output to the Command Log panel"
124-
},
125-
"showOutput": {
126-
"type": "boolean",
127-
"description": "If true, show the command's output in a popup within Lazygit"
117+
"output": {
118+
"type": "string",
119+
"enum": [
120+
"none",
121+
"terminal",
122+
"log",
123+
"logWithPty",
124+
"popup"
125+
],
126+
"description": "Where the output of the command should go. 'none' discards it, 'terminal' suspends lazygit and runs the command in the terminal (useful for commands that require user input), 'log' streams it to the command log, 'logWithPty' is like 'log' but runs the command in a pseudo terminal (can be useful for commands that produce colored output when the output is a terminal), and 'popup' shows it in a popup."
128127
},
129128
"outputTitle": {
130129
"type": "string",
131-
"description": "The title to display in the popup panel if showOutput is true. If left unset, the command will be used as the title."
130+
"description": "The title to display in the popup panel if output is set to 'popup'. If left unset, the command will be used as the title."
132131
},
133132
"after": {
134133
"$ref": "#/$defs/CustomCommandAfterHook",

0 commit comments

Comments
 (0)