Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
18 changes: 17 additions & 1 deletion docs-master/Custom_Command_Keybindings.md
Original file line number Diff line number Diff line change
Expand Up @@ -56,7 +56,7 @@ For a given custom command, here are the allowed fields:
| prompts | A list of prompts that will request user input before running the final command | no |
| loadingText | Text to display while waiting for command to finish | no |
| description | Label for the custom command when displayed in the keybindings menu | no |
| 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 |
| 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), 'popup' shows it in a popup, and 'commitMessagePanel' opens the commit message panel with the command output as the initial message. | no |
| 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 |
| after | Actions to take after the command has completed | no |

Expand Down Expand Up @@ -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 |
| loadingText | Text to display while resolving the prompt, loading suggestions, or generating `menuFromCommand` options | no |
| condition | A Go template expression; if it resolves to empty string or `false`, the prompt is skipped. See [Conditional prompts](#conditional-prompts) | no |

### Input
Expand Down Expand Up @@ -162,6 +163,21 @@ customCommands:
initialValue: "{{.SelectedRemote.Name}}"
```

If the initial value depends on a slow command, you can show a waiting status while the value is generated:

```yml
customCommands:
- key: 'a'
command: 'git commit -m {{.Form.Message | quote}}'
context: 'files'
prompts:
- type: 'input'
title: 'Commit message'
key: 'Message'
loadingText: 'Generating commit message'
initialValue: '{{ runCommand "ai-commit-message" }}'
```

### Confirm

| _field_ | _description_ | _required_ |
Expand Down
6 changes: 4 additions & 2 deletions pkg/config/user_config.go
Original file line number Diff line number Diff line change
Expand Up @@ -724,8 +724,8 @@ type CustomCommand struct {
LoadingText string `yaml:"loadingText" jsonschema:"example=Loading..."`
// Label for the custom command when displayed in the keybindings menu
Description string `yaml:"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.
Output string `yaml:"output" jsonschema:"enum=none,enum=terminal,enum=log,enum=logWithPty,enum=popup"`
// 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), 'popup' shows it in a popup, and 'commitMessagePanel' opens the commit message panel with the command output as the initial message.
Output string `yaml:"output" jsonschema:"enum=none,enum=terminal,enum=log,enum=logWithPty,enum=popup,enum=commitMessagePanel"`
// 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.
OutputTitle string `yaml:"outputTitle"`
// Actions to take after the command has completed
Expand All @@ -748,6 +748,8 @@ type CustomCommandPrompt struct {
Key string `yaml:"key"`
// The title to display in the popup panel
Title string `yaml:"title"`
// Text to display while resolving the prompt, loading suggestions, or generating menuFromCommand options.
LoadingText string `yaml:"loadingText" jsonschema:"example=Loading..."`

// The initial value to appear in the text box.
// Only for input prompts.
Expand Down
2 changes: 1 addition & 1 deletion pkg/config/user_config_validation.go
Original file line number Diff line number Diff line change
Expand Up @@ -222,7 +222,7 @@ func validateCustomCommands(customCommands []CustomCommand) error {
}

if err := validateEnum("customCommand.output", customCommand.Output,
[]string{"", "none", "terminal", "log", "logWithPty", "popup"}); err != nil {
[]string{"", "none", "terminal", "log", "logWithPty", "popup", "commitMessagePanel"}); err != nil {
return err
}
}
Expand Down
1 change: 1 addition & 0 deletions pkg/gui/services/custom_commands/client.go
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,7 @@ func NewClient(
sessionStateLoader,
helpers.Suggestions,
helpers.MergeAndRebase,
helpers.WorkingTree,
)
keybindingCreator := NewKeybindingCreator(c)

Expand Down
90 changes: 75 additions & 15 deletions pkg/gui/services/custom_commands/handler_creator.go
Original file line number Diff line number Diff line change
Expand Up @@ -23,13 +23,15 @@ type HandlerCreator struct {
menuGenerator *MenuGenerator
suggestionsHelper *helpers.SuggestionsHelper
mergeAndRebaseHelper *helpers.MergeAndRebaseHelper
workingTreeHelper *helpers.WorkingTreeHelper
}

func NewHandlerCreator(
c *helpers.HelperCommon,
sessionStateLoader *SessionStateLoader,
suggestionsHelper *helpers.SuggestionsHelper,
mergeAndRebaseHelper *helpers.MergeAndRebaseHelper,
workingTreeHelper *helpers.WorkingTreeHelper,
) *HandlerCreator {
resolver := NewResolver(c.Common)
menuGenerator := NewMenuGenerator(c.Common)
Expand All @@ -41,6 +43,7 @@ func NewHandlerCreator(
menuGenerator: menuGenerator,
suggestionsHelper: suggestionsHelper,
mergeAndRebaseHelper: mergeAndRebaseHelper,
workingTreeHelper: workingTreeHelper,
}
}

Expand Down Expand Up @@ -73,31 +76,31 @@ func (self *HandlerCreator) call(customCommand config.CustomCommand) func() erro
switch prompt.Type {
case "input":
f = func() error {
resolvedPrompt, err := self.resolver.resolvePrompt(&prompt, resolveTemplate)
resolvedPrompt, err := self.resolvePrompt(&prompt, resolveTemplate)
if err != nil {
return err
}
return self.inputPrompt(resolvedPrompt, wrappedF)
}
case "menu":
f = func() error {
resolvedPrompt, err := self.resolver.resolvePrompt(&prompt, resolveTemplate)
resolvedPrompt, err := self.resolvePrompt(&prompt, resolveTemplate)
if err != nil {
return err
}
return self.menuPrompt(resolvedPrompt, wrappedF)
}
case "menuFromCommand":
f = func() error {
resolvedPrompt, err := self.resolver.resolvePrompt(&prompt, resolveTemplate)
resolvedPrompt, err := self.resolvePrompt(&prompt, resolveTemplate)
if err != nil {
return err
}
return self.menuPromptFromCommand(resolvedPrompt, wrappedF)
}
case "confirm":
f = func() error {
resolvedPrompt, err := self.resolver.resolvePrompt(&prompt, resolveTemplate)
resolvedPrompt, err := self.resolvePrompt(&prompt, resolveTemplate)
if err != nil {
return err
}
Expand Down Expand Up @@ -141,6 +144,27 @@ func resolveCondition(condition string, resolveTemplate func(string) (string, er
return strings.TrimSpace(resolved) != "" && strings.TrimSpace(resolved) != "false", nil
}

func (self *HandlerCreator) resolvePrompt(
prompt *config.CustomCommandPrompt,
resolveTemplate func(string) (string, error),
) (*config.CustomCommandPrompt, error) {
var resolvedPrompt *config.CustomCommandPrompt
err := self.withPromptLoading(prompt, func() error {
var err error
resolvedPrompt, err = self.resolver.resolvePrompt(prompt, resolveTemplate)
return err
})
return resolvedPrompt, err
}

func (self *HandlerCreator) withPromptLoading(prompt *config.CustomCommandPrompt, f func() error) error {
if prompt.LoadingText == "" {
return f()
}

return self.c.WithWaitingStatusSync(prompt.LoadingText, f)
}

func (self *HandlerCreator) inputPrompt(prompt *config.CustomCommandPrompt, wrappedF func(string) error) error {
findSuggestionsFn, err := self.generateFindSuggestionsFunc(prompt)
if err != nil {
Expand Down Expand Up @@ -171,17 +195,19 @@ func (self *HandlerCreator) generateFindSuggestionsFunc(prompt *config.CustomCom
} else if prompt.Suggestions.Preset != "" {
return self.getPresetSuggestionsFn(prompt.Suggestions.Preset)
} else if prompt.Suggestions.Command != "" {
return self.getCommandSuggestionsFn(prompt.Suggestions.Command)
return self.getCommandSuggestionsFn(prompt)
}

return nil, nil
}

func (self *HandlerCreator) getCommandSuggestionsFn(command string) (func(string) []*types.Suggestion, error) {
func (self *HandlerCreator) getCommandSuggestionsFn(prompt *config.CustomCommandPrompt) (func(string) []*types.Suggestion, error) {
lines := []*types.Suggestion{}
err := self.c.OS().Cmd.NewShell(command, self.c.UserConfig().OS.ShellFunctionsFile).RunAndProcessLines(func(line string) (bool, error) {
lines = append(lines, &types.Suggestion{Value: line, Label: line})
return false, nil
err := self.withPromptLoading(prompt, func() error {
return self.c.OS().Cmd.NewShell(prompt.Suggestions.Command, self.c.UserConfig().OS.ShellFunctionsFile).RunAndProcessLines(func(line string) (bool, error) {
lines = append(lines, &types.Suggestion{Value: line, Label: line})
return false, nil
})
})
if err != nil {
return nil, err
Expand Down Expand Up @@ -241,7 +267,7 @@ func (self *HandlerCreator) menuPrompt(prompt *config.CustomCommandPrompt, wrapp

func (self *HandlerCreator) menuPromptFromCommand(prompt *config.CustomCommandPrompt, wrappedF func(string) error) error {
// Run and save output
message, err := self.c.Git().Custom.RunWithOutput(prompt.Command)
message, err := self.runMenuPromptCommand(prompt)
if err != nil {
return err
}
Expand All @@ -264,6 +290,16 @@ func (self *HandlerCreator) menuPromptFromCommand(prompt *config.CustomCommandPr
return self.c.Menu(types.CreateMenuOptions{Title: prompt.Title, Items: menuItems})
}

func (self *HandlerCreator) runMenuPromptCommand(prompt *config.CustomCommandPrompt) (string, error) {
var message string
err := self.withPromptLoading(prompt, func() error {
var err error
message, err = self.c.Git().Custom.RunWithOutput(prompt.Command)
return err
})
return message, err
}

type CustomCommandObjects struct {
*SessionState
PromptResponses []string
Expand Down Expand Up @@ -303,6 +339,26 @@ func (self *HandlerCreator) finalHandler(customCommand config.CustomCommand, ses
loadingText = self.c.Tr.RunningCustomCommandStatus
}

if customCommand.Output == "commitMessagePanel" {
if self.workingTreeHelper == nil {
return errors.New("custom command output 'commitMessagePanel' requires the working tree helper")
}

var output string
err := self.c.WithWaitingStatusSync(loadingText, func() error {
self.c.LogAction(self.c.Tr.Actions.CustomCommand)

var err error
output, err = cmdObj.RunWithOutput()
return err
})
if err != nil {
return self.handleCommandError(customCommand, err)
}

return self.workingTreeHelper.HandleCommitPressWithMessage(strings.TrimRight(output, "\r\n"), false)
}

return self.c.WithWaitingStatus(loadingText, func(gocui.Task) error {
self.c.LogAction(self.c.Tr.Actions.CustomCommand)

Expand All @@ -317,11 +373,7 @@ func (self *HandlerCreator) finalHandler(customCommand config.CustomCommand, ses
self.c.Refresh(types.RefreshOptions{Mode: types.ASYNC})

if err != nil {
if customCommand.After != nil && customCommand.After.CheckForConflicts {
return self.mergeAndRebaseHelper.CheckForConflicts(err)
}

return err
return self.handleCommandError(customCommand, err)
}

if customCommand.Output == "popup" {
Expand All @@ -342,3 +394,11 @@ func (self *HandlerCreator) finalHandler(customCommand config.CustomCommand, ses
return nil
})
}

func (self *HandlerCreator) handleCommandError(customCommand config.CustomCommand, err error) error {
if customCommand.After != nil && customCommand.After.CheckForConflicts {
return self.mergeAndRebaseHelper.CheckForConflicts(err)
}

return err
}
Loading