1
0
mirror of https://github.com/jesseduffield/lazygit.git synced 2025-07-03 12:22:34 +03:00
Files
lazygit/pkg/gui/custom_commands.go
Jesse Duffield 1dd7307fde start moving commit panel handlers into controller
more

and more

move rebase commit refreshing into existing abstraction

and more

and more

WIP

and more

handling clicks

properly fix merge conflicts

update cheatsheet

lots more preparation to start moving things into controllers

WIP

better typing

expand on remotes controller

moving more code into controllers
2022-03-17 19:13:40 +11:00

351 lines
10 KiB
Go

package gui
import (
"bytes"
"errors"
"log"
"regexp"
"strconv"
"strings"
"text/template"
"github.com/jesseduffield/gocui"
"github.com/jesseduffield/lazygit/pkg/commands/models"
"github.com/jesseduffield/lazygit/pkg/config"
"github.com/jesseduffield/lazygit/pkg/gui/popup"
"github.com/jesseduffield/lazygit/pkg/gui/style"
"github.com/jesseduffield/lazygit/pkg/gui/types"
"github.com/jesseduffield/lazygit/pkg/utils"
)
type CustomCommandObjects struct {
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
}
type commandMenuEntry struct {
label string
value string
}
func (gui *Gui) resolveTemplate(templateStr string, promptResponses []string) (string, error) {
objects := CustomCommandObjects{
SelectedFile: gui.getSelectedFile(),
SelectedPath: gui.getSelectedPath(),
SelectedLocalCommit: gui.getSelectedLocalCommit(),
SelectedReflogCommit: gui.getSelectedReflogCommit(),
SelectedLocalBranch: gui.getSelectedBranch(),
SelectedRemoteBranch: gui.getSelectedRemoteBranch(),
SelectedRemote: gui.getSelectedRemote(),
SelectedTag: gui.getSelectedTag(),
SelectedStashEntry: gui.getSelectedStashEntry(),
SelectedCommitFile: gui.getSelectedCommitFile(),
SelectedCommitFilePath: gui.getSelectedCommitFilePath(),
SelectedSubCommit: gui.getSelectedSubCommit(),
CheckedOutBranch: gui.getCheckedOutBranch(),
PromptResponses: promptResponses,
}
return utils.ResolveTemplate(templateStr, objects)
}
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)
}
return gui.c.Prompt(popup.PromptOpts{
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
menuItems := make([]*popup.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)
}
menuItems[i] = &popup.MenuItem{
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)
}
return gui.c.Menu(popup.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{}
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)
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)
}
menuItems := make([]*popup.MenuItem, len(candidates))
for i := range candidates {
i := i
menuItems[i] = &popup.MenuItem{
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)
}
return gui.c.Menu(popup.CreateMenuOptions{Title: title, Items: menuItems})
}
func (gui *Gui) handleCustomCommandKeybinding(customCommand config.CustomCommand) func() error {
return func() error {
promptResponses := make([]string, len(customCommand.Prompts))
f := func() error {
cmdStr, err := gui.resolveTemplate(customCommand.Command, promptResponses)
if err != nil {
return gui.c.Error(err)
}
if customCommand.Subprocess {
return gui.runSubprocessWithSuspenseAndRefresh(gui.OSCommand.Cmd.NewShell(cmdStr))
}
loadingText := customCommand.LoadingText
if loadingText == "" {
loadingText = gui.c.Tr.LcRunningCustomCommandStatus
}
return gui.c.WithWaitingStatus(loadingText, func() error {
gui.c.LogAction(gui.c.Tr.Actions.CustomCommand)
cmdObj := gui.OSCommand.Cmd.NewShell(cmdStr)
if customCommand.Stream {
cmdObj.StreamOutput()
}
err := cmdObj.Run()
if err != nil {
return gui.c.Error(err)
}
return gui.c.Refresh(types.RefreshOptions{})
})
}
// 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
// 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
switch prompt.Type {
case "input":
f = func() error {
return gui.inputPrompt(prompt, promptResponses, idx, wrappedF)
}
case "menu":
f = func() error {
return gui.menuPrompt(prompt, promptResponses, idx, wrappedF)
}
case "menuFromCommand":
f = func() error {
return gui.menuPromptFromCommand(prompt, promptResponses, idx, wrappedF)
}
default:
return gui.c.ErrorMsg("custom command prompt must have a type of 'input', 'menu' or 'menuFromCommand'")
}
}
return f()
}
}
func (gui *Gui) GetCustomCommandKeybindings() []*types.Binding {
bindings := []*types.Binding{}
customCommands := gui.c.UserConfig.CustomCommands
for _, customCommand := range customCommands {
var viewName string
var contexts []string
switch customCommand.Context {
case "global":
viewName = ""
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:
context, ok := gui.contextForContextKey(types.ContextKey(customCommand.Context))
// stupid golang making me build an array of strings for this.
allContextKeyStrings := make([]string, len(AllContextKeys))
for i := range AllContextKeys {
allContextKeyStrings[i] = string(AllContextKeys[i])
}
if !ok {
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, ", "))
}
// 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.
viewName = context.GetViewName()
contexts = []string{customCommand.Context}
}
description := customCommand.Description
if description == "" {
description = customCommand.Command
}
bindings = append(bindings, &types.Binding{
ViewName: viewName,
Contexts: contexts,
Key: gui.getKey(customCommand.Key),
Modifier: gocui.ModNone,
Handler: gui.handleCustomCommandKeybinding(customCommand),
Description: description,
})
}
return bindings
}