2020-09-26 07:23:28 +02:00
package gui
import (
2021-07-19 12:46:29 +02:00
"bytes"
"errors"
2020-09-26 09:15:13 +02:00
"log"
2021-07-18 19:38:06 +02:00
"regexp"
"strconv"
2020-09-27 01:37:22 +02:00
"strings"
2021-07-19 12:46:29 +02:00
"text/template"
2020-09-26 07:23:28 +02:00
2020-09-26 09:15:13 +02:00
"github.com/jesseduffield/gocui"
2020-09-29 12:28:39 +02:00
"github.com/jesseduffield/lazygit/pkg/commands/models"
2020-10-03 06:54:55 +02:00
"github.com/jesseduffield/lazygit/pkg/config"
2021-07-27 15:00:37 +02:00
"github.com/jesseduffield/lazygit/pkg/gui/style"
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
}
2021-08-06 21:50:53 +02:00
type commandMenuEntry struct {
2021-08-06 11:53:32 +02:00
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 ( ) ,
SelectedTag : gui . getSelectedTag ( ) ,
SelectedStashEntry : gui . getSelectedStashEntry ( ) ,
SelectedCommitFile : gui . getSelectedCommitFile ( ) ,
SelectedCommitFilePath : gui . getSelectedCommitFilePath ( ) ,
SelectedSubCommit : gui . getSelectedSubCommit ( ) ,
CheckedOutBranch : gui . currentBranch ( ) ,
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
}
2021-07-20 21:59:03 +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 . surfaceError ( err )
}
initialValue , err := gui . resolveTemplate ( prompt . InitialValue , promptResponses )
if err != nil {
return gui . surfaceError ( err )
}
return gui . prompt ( 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 ( [ ] * 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 . 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 {
2021-07-27 15:00:37 +02:00
displayStrings : [ ] string { name , style . FgYellow . Sprint ( description ) } ,
2021-07-20 21:59:03 +02:00
onPress : func ( ) error {
promptResponses [ responseIdx ] = value
return wrappedF ( )
} ,
}
}
title , err := gui . resolveTemplate ( prompt . Title , promptResponses )
if err != nil {
return gui . surfaceError ( err )
}
return gui . createMenu ( title , menuItems , createMenuOptions { showCancel : true } )
}
2021-07-22 16:44:16 +02:00
2021-08-06 21:50:53 +02:00
func ( gui * Gui ) GenerateMenuCandidates ( commandOutput , filter , valueFormat , labelFormat string ) ( [ ] commandMenuEntry , error ) {
2021-07-20 21:59:03 +02:00
reg , err := regexp . Compile ( filter )
if err != nil {
2021-08-06 21:50:53 +02:00
return nil , gui . surfaceError ( errors . New ( "unable to parse filter regex, error: " + err . Error ( ) ) )
2021-07-20 21:59:03 +02:00
}
2021-08-06 19:38:26 +02:00
2021-08-06 21:50:53 +02:00
buff := bytes . NewBuffer ( nil )
2021-08-06 19:38:26 +02:00
valueTemp , err := template . New ( "format" ) . Parse ( valueFormat )
2021-07-20 21:59:03 +02:00
if err != nil {
2021-08-06 21:50:53 +02:00
return nil , gui . surfaceError ( errors . New ( "unable to parse value format, error: " + err . Error ( ) ) )
2021-08-05 16:24:17 +02:00
}
2021-08-06 19:38:26 +02:00
2021-08-09 12:52:00 +02:00
colorFuncMap := style . TemplateFuncMapAddColors ( template . FuncMap { } )
2021-08-07 17:06:36 +02:00
descTemp , err := template . New ( "format" ) . Funcs ( colorFuncMap ) . Parse ( labelFormat )
2021-08-05 16:24:17 +02:00
if err != nil {
2021-08-06 21:50:53 +02:00
return nil , gui . surfaceError ( errors . New ( "unable to parse label format, error: " + err . Error ( ) ) )
2021-07-20 21:59:03 +02:00
}
2021-08-06 19:38:26 +02:00
2021-08-06 21:50:53 +02:00
candidates := [ ] commandMenuEntry { }
2021-07-20 21:59:03 +02:00
for _ , str := range strings . Split ( string ( commandOutput ) , "\n" ) {
if str == "" {
continue
}
2021-08-06 21:50:53 +02:00
2021-07-20 21:59:03 +02:00
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 ]
2021-07-20 21:59:03 +02:00
// Record last named group non-empty matches as group matches
if group != "" {
tmplData [ group ] = out [ 0 ] [ groupIdx ]
}
}
}
2021-08-06 19:38:26 +02:00
2021-08-06 21:50:53 +02:00
err = valueTemp . Execute ( buff , tmplData )
2021-08-05 16:24:17 +02:00
if err != nil {
2021-08-06 11:53:32 +02:00
return candidates , gui . surfaceError ( err )
2021-08-05 16:24:17 +02:00
}
2021-08-06 21:50:53 +02:00
entry := commandMenuEntry {
value : strings . TrimSpace ( buff . String ( ) ) ,
}
2021-08-06 19:38:26 +02:00
if labelFormat != "" {
2021-08-06 21:50:53 +02:00
buff . Reset ( )
err = descTemp . Execute ( buff , tmplData )
2021-08-06 19:38:26 +02:00
if err != nil {
return candidates , gui . surfaceError ( err )
}
2021-08-06 21:50:53 +02:00
entry . label = strings . TrimSpace ( buff . String ( ) )
2021-08-06 19:38:26 +02:00
} else {
2021-08-06 21:50:53 +02:00
entry . label = entry . value
2021-07-20 21:59:03 +02:00
}
2021-08-06 11:53:32 +02:00
candidates = append ( candidates , entry )
2021-08-06 19:38:26 +02:00
2021-08-06 21:50:53 +02:00
buff . Reset ( )
2021-07-20 21:59:03 +02:00
}
2021-08-06 11:53:32 +02:00
return candidates , err
2021-07-20 21:59:03 +02:00
}
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 . surfaceError ( err )
}
// Collect Filter regexp
filter , err := gui . resolveTemplate ( prompt . Filter , promptResponses )
if err != nil {
return gui . surfaceError ( err )
}
// Run and save output
message , err := gui . GitCommand . RunCommandWithOutput ( cmdStr )
if err != nil {
return gui . surfaceError ( err )
}
// Need to make a menu out of what the cmd has displayed
2021-08-06 11:53:32 +02:00
candidates , err := gui . GenerateMenuCandidates ( message , filter , prompt . ValueFormat , prompt . LabelFormat )
2021-07-22 16:44:16 +02:00
if err != nil {
return gui . surfaceError ( err )
}
2021-07-20 21:59:03 +02:00
menuItems := make ( [ ] * menuItem , len ( candidates ) )
for i := range candidates {
menuItems [ i ] = & menuItem {
2021-08-06 19:38:26 +02:00
displayStrings : [ ] string { candidates [ i ] . label } ,
2021-07-20 21:59:03 +02:00
onPress : func ( ) error {
2021-08-06 11:53:32 +02:00
promptResponses [ responseIdx ] = candidates [ i ] . value
2021-07-20 21:59:03 +02:00
return wrappedF ( )
} ,
}
}
title , err := gui . resolveTemplate ( prompt . Title , promptResponses )
if err != nil {
return gui . surfaceError ( err )
}
return gui . createMenu ( title , menuItems , createMenuOptions { showCancel : true } )
}
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 . surfaceError ( err )
}
2020-09-26 07:23:28 +02:00
2020-09-26 11:48:13 +02:00
if customCommand . Subprocess {
2021-04-10 03:40:42 +02:00
return gui . runSubprocessWithSuspenseAndRefresh ( gui . OSCommand . PrepareShellSubProcess ( 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 == "" {
2020-10-04 02:00:48 +02:00
loadingText = gui . Tr . LcRunningCustomCommandStatus
2020-09-27 01:21:20 +02:00
}
return gui . WithWaitingStatus ( loadingText , func ( ) error {
2021-04-11 11:35:42 +02:00
if err := gui . OSCommand . WithSpan ( gui . Tr . Spans . CustomCommand ) . RunShellCommand ( cmdStr ) ; err != nil {
2020-09-26 11:48:13 +02:00
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 {
2021-07-20 21:59:03 +02:00
return gui . inputPrompt ( prompt , promptResponses , idx , wrappedF )
2020-09-26 12:32:19 +02:00
}
case "menu" :
2020-09-26 13:47:01 +02:00
f = func ( ) error {
2021-07-20 21:59:03 +02:00
return gui . menuPrompt ( prompt , promptResponses , idx , wrappedF )
2021-07-17 19:02:11 +02:00
}
case "menuFromCommand" :
f = func ( ) error {
2021-07-20 21:59:03 +02:00
return gui . menuPromptFromCommand ( prompt , promptResponses , idx , wrappedF )
2020-09-26 12:32:19 +02:00
}
default :
2021-07-17 19:02:11 +02:00
return gui . createErrorPanel ( "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
func ( gui * Gui ) GetCustomCommandKeybindings ( ) [ ] * Binding {
bindings := [ ] * Binding { }
2020-10-03 06:54:55 +02:00
customCommands := gui . Config . GetUserConfig ( ) . CustomCommands
2020-09-26 09:15:13 +02:00
for _ , customCommand := range customCommands {
var viewName string
2020-09-27 03:04:57 +02:00
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 :
2021-04-04 15:51:59 +02:00
context , ok := gui . contextForContextKey ( 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 ] )
}
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.
viewName = context . GetViewName ( )
2020-09-27 03:04:57 +02:00
contexts = [ ] string { customCommand . Context }
2020-09-26 09:15:13 +02:00
}
2020-09-27 03:29:10 +02:00
description := customCommand . Description
if description == "" {
description = customCommand . Command
}
2020-09-26 09:15:13 +02:00
bindings = append ( bindings , & Binding {
ViewName : viewName ,
2020-09-27 03:04:57 +02:00
Contexts : contexts ,
2020-09-26 09:15:13 +02:00
Key : gui . getKey ( customCommand . Key ) ,
Modifier : gocui . ModNone ,
2021-04-02 10:20:40 +02:00
Handler : gui . handleCustomCommandKeybinding ( customCommand ) ,
2020-09-27 03:29:10 +02:00
Description : description ,
2020-09-26 09:15:13 +02:00
} )
}
return bindings
}