1
0
mirror of https://github.com/jesseduffield/lazygit.git synced 2024-12-12 11:15:00 +02:00
lazygit/pkg/gui/custom_commands.go

351 lines
10 KiB
Go
Raw Normal View History

2020-09-26 07:23:28 +02:00
package gui
import (
"bytes"
"errors"
2020-09-26 09:15:13 +02:00
"log"
"regexp"
"strconv"
2020-09-27 01:37:22 +02:00
"strings"
"text/template"
2020-09-26 07:23:28 +02:00
2020-09-26 09:15:13 +02:00
"github.com/jesseduffield/gocui"
"github.com/jesseduffield/lazygit/pkg/commands/models"
2020-10-03 06:54:55 +02:00
"github.com/jesseduffield/lazygit/pkg/config"
2022-01-29 10:15:46 +02:00
"github.com/jesseduffield/lazygit/pkg/gui/context"
"github.com/jesseduffield/lazygit/pkg/gui/style"
2022-01-28 11:44:36 +02:00
"github.com/jesseduffield/lazygit/pkg/gui/types"
2020-09-27 01:13:31 +02:00
"github.com/jesseduffield/lazygit/pkg/utils"
2020-09-26 07:23:28 +02:00
)
type CustomCommandObjects struct {
2021-03-31 13:08:55 +02:00
SelectedLocalCommit *models.Commit
SelectedReflogCommit *models.Commit
SelectedSubCommit *models.Commit
SelectedFile *models.File
SelectedPath string
SelectedLocalBranch *models.Branch
SelectedRemoteBranch *models.RemoteBranch
SelectedRemote *models.Remote
SelectedTag *models.Tag
SelectedStashEntry *models.StashEntry
SelectedCommitFile *models.CommitFile
SelectedCommitFilePath string
CheckedOutBranch *models.Branch
PromptResponses []string
2020-09-26 11:48:13 +02:00
}
type commandMenuEntry struct {
label string
value string
}
2020-09-26 11:48:13 +02:00
func (gui *Gui) resolveTemplate(templateStr string, promptResponses []string) (string, error) {
objects := CustomCommandObjects{
2021-03-31 13:08:55 +02:00
SelectedFile: gui.getSelectedFile(),
SelectedPath: gui.getSelectedPath(),
SelectedLocalCommit: gui.getSelectedLocalCommit(),
SelectedReflogCommit: gui.getSelectedReflogCommit(),
SelectedLocalBranch: gui.getSelectedBranch(),
SelectedRemoteBranch: gui.getSelectedRemoteBranch(),
SelectedRemote: gui.getSelectedRemote(),
2022-01-29 10:09:20 +02:00
SelectedTag: gui.State.Contexts.Tags.GetSelectedTag(),
2021-03-31 13:08:55 +02:00
SelectedStashEntry: gui.getSelectedStashEntry(),
SelectedCommitFile: gui.getSelectedCommitFile(),
SelectedCommitFilePath: gui.getSelectedCommitFilePath(),
SelectedSubCommit: gui.getSelectedSubCommit(),
CheckedOutBranch: gui.getCheckedOutBranch(),
2021-03-31 13:08:55 +02:00
PromptResponses: promptResponses,
2020-09-26 11:48:13 +02:00
}
2020-09-30 13:12:03 +02:00
return utils.ResolveTemplate(templateStr, objects)
2020-09-26 07:23:28 +02:00
}
func (gui *Gui) inputPrompt(prompt config.CustomCommandPrompt, promptResponses []string, responseIdx int, wrappedF func() error) error {
title, err := gui.resolveTemplate(prompt.Title, promptResponses)
if err != nil {
return gui.c.Error(err)
}
initialValue, err := gui.resolveTemplate(prompt.InitialValue, promptResponses)
if err != nil {
return gui.c.Error(err)
}
2022-01-29 10:09:20 +02:00
return gui.c.Prompt(types.PromptOpts{
2022-01-28 11:44:36 +02:00
Title: title,
InitialContent: initialValue,
HandleConfirm: func(str string) error {
promptResponses[responseIdx] = str
return wrappedF()
},
})
}
func (gui *Gui) menuPrompt(prompt config.CustomCommandPrompt, promptResponses []string, responseIdx int, wrappedF func() error) error {
// need to make a menu here some how
2022-01-29 10:09:20 +02:00
menuItems := make([]*types.MenuItem, len(prompt.Options))
for i, option := range prompt.Options {
option := option
nameTemplate := option.Name
if nameTemplate == "" {
// this allows you to only pass values rather than bother with names/descriptions
nameTemplate = option.Value
}
name, err := gui.resolveTemplate(nameTemplate, promptResponses)
if err != nil {
return gui.c.Error(err)
}
description, err := gui.resolveTemplate(option.Description, promptResponses)
if err != nil {
return gui.c.Error(err)
}
value, err := gui.resolveTemplate(option.Value, promptResponses)
if err != nil {
return gui.c.Error(err)
}
2022-01-29 10:09:20 +02:00
menuItems[i] = &types.MenuItem{
2022-01-28 11:44:36 +02:00
DisplayStrings: []string{name, style.FgYellow.Sprint(description)},
OnPress: func() error {
promptResponses[responseIdx] = value
return wrappedF()
},
}
}
title, err := gui.resolveTemplate(prompt.Title, promptResponses)
if err != nil {
return gui.c.Error(err)
}
2022-01-29 10:09:20 +02:00
return gui.c.Menu(types.CreateMenuOptions{Title: title, Items: menuItems})
}
func (gui *Gui) GenerateMenuCandidates(commandOutput, filter, valueFormat, labelFormat string) ([]commandMenuEntry, error) {
reg, err := regexp.Compile(filter)
if err != nil {
return nil, gui.c.Error(errors.New("unable to parse filter regex, error: " + err.Error()))
}
buff := bytes.NewBuffer(nil)
valueTemp, err := template.New("format").Parse(valueFormat)
if err != nil {
return nil, gui.c.Error(errors.New("unable to parse value format, error: " + err.Error()))
}
colorFuncMap := style.TemplateFuncMapAddColors(template.FuncMap{})
descTemp, err := template.New("format").Funcs(colorFuncMap).Parse(labelFormat)
if err != nil {
return nil, gui.c.Error(errors.New("unable to parse label format, error: " + err.Error()))
}
candidates := []commandMenuEntry{}
2022-01-08 06:46:35 +02:00
for _, str := range strings.Split(commandOutput, "\n") {
if str == "" {
continue
}
tmplData := map[string]string{}
out := reg.FindAllStringSubmatch(str, -1)
if len(out) > 0 {
for groupIdx, group := range reg.SubexpNames() {
// Record matched group with group ids
matchName := "group_" + strconv.Itoa(groupIdx)
2021-07-22 19:45:43 +02:00
tmplData[matchName] = out[0][groupIdx]
// Record last named group non-empty matches as group matches
if group != "" {
tmplData[group] = out[0][groupIdx]
}
}
}
err = valueTemp.Execute(buff, tmplData)
if err != nil {
return candidates, gui.c.Error(err)
}
entry := commandMenuEntry{
value: strings.TrimSpace(buff.String()),
}
if labelFormat != "" {
buff.Reset()
err = descTemp.Execute(buff, tmplData)
if err != nil {
return candidates, gui.c.Error(err)
}
entry.label = strings.TrimSpace(buff.String())
} else {
entry.label = entry.value
}
candidates = append(candidates, entry)
buff.Reset()
}
return candidates, err
}
func (gui *Gui) menuPromptFromCommand(prompt config.CustomCommandPrompt, promptResponses []string, responseIdx int, wrappedF func() error) error {
// Collect cmd to run from config
cmdStr, err := gui.resolveTemplate(prompt.Command, promptResponses)
if err != nil {
return gui.c.Error(err)
}
// Collect Filter regexp
filter, err := gui.resolveTemplate(prompt.Filter, promptResponses)
if err != nil {
return gui.c.Error(err)
}
// Run and save output
message, err := gui.git.Custom.RunWithOutput(cmdStr)
if err != nil {
return gui.c.Error(err)
}
// Need to make a menu out of what the cmd has displayed
candidates, err := gui.GenerateMenuCandidates(message, filter, prompt.ValueFormat, prompt.LabelFormat)
if err != nil {
return gui.c.Error(err)
}
2022-01-29 10:09:20 +02:00
menuItems := make([]*types.MenuItem, len(candidates))
for i := range candidates {
2022-01-08 06:46:35 +02:00
i := i
2022-01-29 10:09:20 +02:00
menuItems[i] = &types.MenuItem{
2022-01-28 11:44:36 +02:00
DisplayStrings: []string{candidates[i].label},
OnPress: func() error {
promptResponses[responseIdx] = candidates[i].value
return wrappedF()
},
}
}
title, err := gui.resolveTemplate(prompt.Title, promptResponses)
if err != nil {
return gui.c.Error(err)
}
2022-01-29 10:09:20 +02:00
return gui.c.Menu(types.CreateMenuOptions{Title: title, Items: menuItems})
}
2020-10-03 06:54:55 +02:00
func (gui *Gui) handleCustomCommandKeybinding(customCommand config.CustomCommand) func() error {
2020-09-26 07:23:28 +02:00
return func() error {
2020-09-26 11:48:13 +02:00
promptResponses := make([]string, len(customCommand.Prompts))
2020-09-26 07:23:28 +02:00
2020-09-26 11:48:13 +02:00
f := func() error {
cmdStr, err := gui.resolveTemplate(customCommand.Command, promptResponses)
if err != nil {
return gui.c.Error(err)
2020-09-26 11:48:13 +02:00
}
2020-09-26 07:23:28 +02:00
2020-09-26 11:48:13 +02:00
if customCommand.Subprocess {
2021-12-29 05:33:38 +02:00
return gui.runSubprocessWithSuspenseAndRefresh(gui.OSCommand.Cmd.NewShell(cmdStr))
2020-09-26 11:48:13 +02:00
}
2020-09-26 07:23:28 +02:00
2020-09-27 01:21:20 +02:00
loadingText := customCommand.LoadingText
if loadingText == "" {
loadingText = gui.c.Tr.LcRunningCustomCommandStatus
2020-09-27 01:21:20 +02:00
}
return gui.c.WithWaitingStatus(loadingText, func() error {
gui.c.LogAction(gui.c.Tr.Actions.CustomCommand)
2022-01-19 09:32:27 +02:00
cmdObj := gui.OSCommand.Cmd.NewShell(cmdStr)
if customCommand.Stream {
cmdObj.StreamOutput()
}
err := cmdObj.Run()
2021-12-29 05:33:38 +02:00
if err != nil {
return gui.c.Error(err)
2020-09-26 11:48:13 +02:00
}
return gui.c.Refresh(types.RefreshOptions{})
2020-09-26 11:48:13 +02:00
})
2020-09-26 09:15:13 +02:00
}
2020-09-26 11:48:13 +02:00
// if we have prompts we'll recursively wrap our confirm handlers with more prompts
// until we reach the actual command
for reverseIdx := range customCommand.Prompts {
idx := len(customCommand.Prompts) - 1 - reverseIdx
2020-09-26 09:15:13 +02:00
2020-09-26 11:48:13 +02:00
// going backwards so the outermost prompt is the first one
prompt := customCommand.Prompts[idx]
// need to do this because f's value will change with each iteration
wrappedF := f
2020-09-26 12:32:19 +02:00
switch prompt.Type {
2020-09-27 01:13:31 +02:00
case "input":
2020-09-26 12:32:19 +02:00
f = func() error {
return gui.inputPrompt(prompt, promptResponses, idx, wrappedF)
2020-09-26 12:32:19 +02:00
}
case "menu":
f = func() error {
return gui.menuPrompt(prompt, promptResponses, idx, wrappedF)
2021-07-17 19:02:11 +02:00
}
case "menuFromCommand":
f = func() error {
return gui.menuPromptFromCommand(prompt, promptResponses, idx, wrappedF)
2020-09-26 12:32:19 +02:00
}
default:
return gui.c.ErrorMsg("custom command prompt must have a type of 'input', 'menu' or 'menuFromCommand'")
2020-09-26 07:23:28 +02:00
}
2020-09-26 12:32:19 +02:00
2020-09-26 11:48:13 +02:00
}
return f()
2020-09-26 07:23:28 +02:00
}
}
2020-09-26 09:15:13 +02:00
2022-01-28 11:44:36 +02:00
func (gui *Gui) GetCustomCommandKeybindings() []*types.Binding {
bindings := []*types.Binding{}
customCommands := gui.c.UserConfig.CustomCommands
2020-09-26 09:15:13 +02:00
for _, customCommand := range customCommands {
var viewName string
var contexts []string
2020-09-27 01:37:22 +02:00
switch customCommand.Context {
case "global":
2020-09-26 09:15:13 +02:00
viewName = ""
2020-09-27 01:37:22 +02:00
case "":
log.Fatalf("Error parsing custom command keybindings: context not provided (use context: 'global' for the global context). Key: %s, Command: %s", customCommand.Key, customCommand.Command)
default:
2022-01-29 10:15:46 +02:00
ctx, ok := gui.contextForContextKey(types.ContextKey(customCommand.Context))
2021-04-04 15:51:59 +02:00
// stupid golang making me build an array of strings for this.
2022-01-29 10:15:46 +02:00
allContextKeyStrings := make([]string, len(context.AllContextKeys))
for i := range context.AllContextKeys {
allContextKeyStrings[i] = string(context.AllContextKeys[i])
2021-04-04 15:51:59 +02:00
}
2020-09-27 01:37:22 +02:00
if !ok {
2021-04-04 15:51:59 +02:00
log.Fatalf("Error when setting custom command keybindings: unknown context: %s. Key: %s, Command: %s.\nPermitted contexts: %s", customCommand.Context, customCommand.Key, customCommand.Command, strings.Join(allContextKeyStrings, ", "))
2020-09-26 09:15:13 +02:00
}
// here we assume that a given context will always belong to the same view.
// Currently this is a safe bet but it's by no means guaranteed in the long term
// and we might need to make some changes in the future to support it.
2022-01-29 10:15:46 +02:00
viewName = ctx.GetViewName()
contexts = []string{customCommand.Context}
2020-09-26 09:15:13 +02:00
}
description := customCommand.Description
if description == "" {
description = customCommand.Command
}
2022-01-28 11:44:36 +02:00
bindings = append(bindings, &types.Binding{
2020-09-26 09:15:13 +02:00
ViewName: viewName,
Contexts: contexts,
2020-09-26 09:15:13 +02:00
Key: gui.getKey(customCommand.Key),
Modifier: gocui.ModNone,
Handler: gui.handleCustomCommandKeybinding(customCommand),
Description: description,
2020-09-26 09:15:13 +02:00
})
}
return bindings
}