2020-09-26 07:23:28 +02:00
|
|
|
package gui
|
|
|
|
|
|
|
|
import (
|
|
|
|
"bytes"
|
2020-09-26 09:15:13 +02:00
|
|
|
"log"
|
2020-09-26 07:23:28 +02:00
|
|
|
"text/template"
|
|
|
|
|
2020-09-27 01:13:31 +02:00
|
|
|
"github.com/fatih/color"
|
2020-09-26 09:15:13 +02:00
|
|
|
"github.com/jesseduffield/gocui"
|
2020-09-26 07:23:28 +02:00
|
|
|
"github.com/jesseduffield/lazygit/pkg/commands"
|
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 {
|
|
|
|
SelectedLocalCommit *commands.Commit
|
|
|
|
SelectedReflogCommit *commands.Commit
|
|
|
|
SelectedSubCommit *commands.Commit
|
|
|
|
SelectedFile *commands.File
|
|
|
|
SelectedLocalBranch *commands.Branch
|
|
|
|
SelectedRemoteBranch *commands.RemoteBranch
|
|
|
|
SelectedRemote *commands.Remote
|
|
|
|
SelectedTag *commands.Tag
|
|
|
|
SelectedStashEntry *commands.StashEntry
|
|
|
|
SelectedCommitFile *commands.CommitFile
|
2020-09-26 09:50:22 +02:00
|
|
|
CheckedOutBranch *commands.Branch
|
2020-09-26 11:48:13 +02:00
|
|
|
PromptResponses []string
|
|
|
|
}
|
|
|
|
|
|
|
|
func (gui *Gui) resolveTemplate(templateStr string, promptResponses []string) (string, error) {
|
|
|
|
objects := CustomCommandObjects{
|
|
|
|
SelectedFile: gui.getSelectedFile(),
|
|
|
|
SelectedLocalCommit: gui.getSelectedLocalCommit(),
|
|
|
|
SelectedReflogCommit: gui.getSelectedReflogCommit(),
|
|
|
|
SelectedLocalBranch: gui.getSelectedBranch(),
|
|
|
|
SelectedRemoteBranch: gui.getSelectedRemoteBranch(),
|
|
|
|
SelectedRemote: gui.getSelectedRemote(),
|
|
|
|
SelectedTag: gui.getSelectedTag(),
|
|
|
|
SelectedStashEntry: gui.getSelectedStashEntry(),
|
|
|
|
SelectedCommitFile: gui.getSelectedCommitFile(),
|
|
|
|
SelectedSubCommit: gui.getSelectedSubCommit(),
|
|
|
|
CheckedOutBranch: gui.currentBranch(),
|
|
|
|
PromptResponses: promptResponses,
|
|
|
|
}
|
|
|
|
|
|
|
|
tmpl, err := template.New("template").Parse(templateStr)
|
|
|
|
if err != nil {
|
|
|
|
return "", err
|
|
|
|
}
|
|
|
|
|
|
|
|
var buf bytes.Buffer
|
|
|
|
if err := tmpl.Execute(&buf, objects); err != nil {
|
|
|
|
return "", err
|
|
|
|
}
|
|
|
|
|
|
|
|
cmdStr := buf.String()
|
|
|
|
|
|
|
|
return cmdStr, nil
|
2020-09-26 07:23:28 +02:00
|
|
|
}
|
|
|
|
|
2020-09-26 09:15:13 +02:00
|
|
|
func (gui *Gui) handleCustomCommandKeybinding(customCommand 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.surfaceError(err)
|
|
|
|
}
|
2020-09-26 07:23:28 +02:00
|
|
|
|
2020-09-26 11:48:13 +02:00
|
|
|
if customCommand.Subprocess {
|
|
|
|
gui.PrepareSubProcess(cmdStr)
|
|
|
|
return nil
|
|
|
|
}
|
2020-09-26 07:23:28 +02:00
|
|
|
|
2020-09-27 01:21:20 +02:00
|
|
|
loadingText := customCommand.LoadingText
|
|
|
|
if loadingText == "" {
|
|
|
|
loadingText = gui.Tr.SLocalize("runningCustomCommandStatus")
|
|
|
|
}
|
|
|
|
return gui.WithWaitingStatus(loadingText, func() error {
|
2020-09-26 11:48:13 +02:00
|
|
|
gui.OSCommand.PrepareSubProcess(cmdStr)
|
2020-09-26 07:23:28 +02:00
|
|
|
|
2020-09-26 11:48:13 +02:00
|
|
|
if err := gui.OSCommand.RunCommand(cmdStr); err != nil {
|
|
|
|
return gui.surfaceError(err)
|
|
|
|
}
|
|
|
|
return gui.refreshSidePanels(refreshOptions{})
|
|
|
|
})
|
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]
|
|
|
|
|
2020-09-26 13:47:01 +02:00
|
|
|
// 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 {
|
2020-09-26 13:47:01 +02:00
|
|
|
title, err := gui.resolveTemplate(prompt.Title, promptResponses)
|
|
|
|
if err != nil {
|
|
|
|
return gui.surfaceError(err)
|
|
|
|
}
|
|
|
|
|
|
|
|
initialValue, err := gui.resolveTemplate(prompt.InitialValue, promptResponses)
|
|
|
|
if err != nil {
|
|
|
|
return gui.surfaceError(err)
|
|
|
|
}
|
|
|
|
|
2020-09-26 12:32:19 +02:00
|
|
|
return gui.prompt(
|
2020-09-26 13:47:01 +02:00
|
|
|
title,
|
|
|
|
initialValue,
|
2020-09-26 12:32:19 +02:00
|
|
|
func(str string) error {
|
|
|
|
promptResponses[idx] = str
|
|
|
|
|
|
|
|
return wrappedF()
|
|
|
|
},
|
|
|
|
)
|
|
|
|
}
|
|
|
|
case "menu":
|
2020-09-26 13:47:01 +02:00
|
|
|
f = func() error {
|
|
|
|
// need to make a menu here some how
|
|
|
|
menuItems := make([]*menuItem, len(prompt.Options))
|
|
|
|
for i, option := range prompt.Options {
|
|
|
|
option := option
|
|
|
|
|
2020-09-27 01:11:19 +02:00
|
|
|
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)
|
2020-09-26 13:47:01 +02:00
|
|
|
if err != nil {
|
|
|
|
return gui.surfaceError(err)
|
|
|
|
}
|
|
|
|
|
|
|
|
description, err := gui.resolveTemplate(option.Description, promptResponses)
|
|
|
|
if err != nil {
|
|
|
|
return gui.surfaceError(err)
|
|
|
|
}
|
|
|
|
|
|
|
|
value, err := gui.resolveTemplate(option.Value, promptResponses)
|
|
|
|
if err != nil {
|
|
|
|
return gui.surfaceError(err)
|
|
|
|
}
|
|
|
|
|
|
|
|
menuItems[i] = &menuItem{
|
2020-09-27 01:13:31 +02:00
|
|
|
displayStrings: []string{name, utils.ColoredString(description, color.FgYellow)},
|
2020-09-26 13:47:01 +02:00
|
|
|
onPress: func() error {
|
|
|
|
promptResponses[idx] = value
|
|
|
|
|
|
|
|
return wrappedF()
|
|
|
|
},
|
|
|
|
}
|
|
|
|
}
|
2020-09-26 12:32:19 +02:00
|
|
|
|
2020-09-26 13:47:01 +02:00
|
|
|
title, err := gui.resolveTemplate(prompt.Title, promptResponses)
|
|
|
|
if err != nil {
|
|
|
|
return gui.surfaceError(err)
|
2020-09-26 12:32:19 +02:00
|
|
|
}
|
|
|
|
|
2020-09-26 13:47:01 +02:00
|
|
|
return gui.createMenu(title, menuItems, createMenuOptions{showCancel: true})
|
2020-09-26 12:32:19 +02:00
|
|
|
}
|
|
|
|
default:
|
2020-09-27 01:13:31 +02:00
|
|
|
return gui.createErrorPanel("custom command prompt must have a type of 'input' or 'menu'")
|
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
|
|
|
|
2020-09-26 12:32:19 +02:00
|
|
|
type CustomCommandMenuOption struct {
|
|
|
|
Name string `yaml:"name"`
|
|
|
|
Description string `yaml:"description"`
|
|
|
|
Value string `yaml:"value"`
|
|
|
|
}
|
|
|
|
|
2020-09-26 11:48:13 +02:00
|
|
|
type CustomCommandPrompt struct {
|
2020-09-27 01:13:31 +02:00
|
|
|
Type string `yaml:"type"` // one of 'input' and 'menu'
|
2020-09-26 12:32:19 +02:00
|
|
|
Title string `yaml:"title"`
|
|
|
|
|
|
|
|
// this only apply to prompts
|
2020-09-26 11:48:13 +02:00
|
|
|
InitialValue string `yaml:"initialValue"`
|
2020-09-26 12:32:19 +02:00
|
|
|
|
|
|
|
// this only applies to menus
|
|
|
|
Options []CustomCommandMenuOption
|
2020-09-26 11:48:13 +02:00
|
|
|
}
|
|
|
|
|
2020-09-26 09:15:13 +02:00
|
|
|
type CustomCommand struct {
|
2020-09-27 01:21:20 +02:00
|
|
|
Key string `yaml:"key"`
|
|
|
|
Context string `yaml:"context"`
|
|
|
|
Command string `yaml:"command"`
|
|
|
|
Subprocess bool `yaml:"subprocess"`
|
|
|
|
Prompts []CustomCommandPrompt `yaml:"prompts"`
|
|
|
|
LoadingText string `yaml:"loadingText"`
|
2020-09-26 09:15:13 +02:00
|
|
|
}
|
|
|
|
|
|
|
|
func (gui *Gui) GetCustomCommandKeybindings() []*Binding {
|
|
|
|
bindings := []*Binding{}
|
|
|
|
|
|
|
|
var customCommands []CustomCommand
|
|
|
|
|
|
|
|
if err := gui.Config.GetUserConfig().UnmarshalKey("customCommands", &customCommands); err != nil {
|
|
|
|
log.Fatalf("Error parsing custom command keybindings: %v", err)
|
|
|
|
}
|
|
|
|
|
|
|
|
for _, customCommand := range customCommands {
|
|
|
|
var viewName string
|
|
|
|
if customCommand.Context == "global" || customCommand.Context == "" {
|
|
|
|
viewName = ""
|
|
|
|
} else {
|
|
|
|
context := gui.contextForContextKey(customCommand.Context)
|
|
|
|
if context == nil {
|
|
|
|
log.Fatalf("Error when setting custom command keybindings: unknown context: %s", customCommand.Context)
|
|
|
|
}
|
|
|
|
// 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()
|
|
|
|
}
|
|
|
|
|
|
|
|
bindings = append(bindings, &Binding{
|
|
|
|
ViewName: viewName,
|
|
|
|
Contexts: []string{customCommand.Context},
|
|
|
|
Key: gui.getKey(customCommand.Key),
|
|
|
|
Modifier: gocui.ModNone,
|
|
|
|
Handler: gui.wrappedHandler(gui.handleCustomCommandKeybinding(customCommand)),
|
|
|
|
Description: customCommand.Command,
|
|
|
|
})
|
|
|
|
}
|
|
|
|
|
|
|
|
return bindings
|
|
|
|
}
|