2018-08-14 11:05:26 +02:00
package gui
2018-05-26 05:23:39 +02:00
import (
2018-07-21 07:51:18 +02:00
"fmt"
"sort"
"strings"
2020-03-26 14:48:11 +02:00
"sync"
2018-05-26 05:23:39 +02:00
2018-07-21 07:51:18 +02:00
"github.com/jesseduffield/gocui"
2018-08-19 13:20:50 +02:00
"github.com/jesseduffield/lazygit/pkg/utils"
2018-08-16 07:53:53 +02:00
"github.com/spkg/bom"
2018-05-26 05:23:39 +02:00
)
2020-08-16 05:58:29 +02:00
func ( gui * Gui ) getCyclableWindows ( ) [ ] string {
2020-05-17 13:54:51 +02:00
return [ ] string { "status" , "files" , "branches" , "commits" , "stash" }
}
2018-06-06 04:17:49 +02:00
2020-03-28 02:32:31 +02:00
// models/views that we can refresh
2020-03-27 10:12:15 +02:00
const (
COMMITS = iota
BRANCHES
FILES
STASH
REFLOG
TAGS
REMOTES
2020-03-28 00:57:36 +02:00
STATUS
2020-03-27 10:12:15 +02:00
)
const (
SYNC = iota // wait until everything is done before returning
ASYNC // return immediately, allowing each independent thing to update itself
BLOCK_UI // wrap code in an update call to ensure UI updates all at once and keybindings aren't executed till complete
)
type refreshOptions struct {
then func ( )
2020-03-28 02:32:31 +02:00
scope [ ] int // e.g. []int{COMMITS, BRANCHES}. Leave empty to refresh everything
mode int // one of SYNC (default), ASYNC, and BLOCK_UI
2020-03-27 10:12:15 +02:00
}
func intArrToMap ( arr [ ] int ) map [ int ] bool {
output := map [ int ] bool { }
for _ , el := range arr {
output [ el ] = true
}
return output
}
func ( gui * Gui ) refreshSidePanels ( options refreshOptions ) error {
2020-03-26 14:48:11 +02:00
wg := sync . WaitGroup { }
2020-03-27 10:12:15 +02:00
f := func ( ) {
var scopeMap map [ int ] bool
if len ( options . scope ) == 0 {
2020-03-28 00:57:36 +02:00
scopeMap = intArrToMap ( [ ] int { COMMITS , BRANCHES , FILES , STASH , REFLOG , TAGS , REMOTES , STATUS } )
2020-03-27 10:12:15 +02:00
} else {
scopeMap = intArrToMap ( options . scope )
}
2020-03-28 01:27:34 +02:00
if scopeMap [ COMMITS ] || scopeMap [ BRANCHES ] || scopeMap [ REFLOG ] {
2020-03-27 10:12:15 +02:00
wg . Add ( 1 )
func ( ) {
if options . mode == ASYNC {
2020-03-27 12:23:42 +02:00
go gui . refreshCommits ( )
2020-03-27 10:12:15 +02:00
} else {
2020-03-27 12:23:42 +02:00
gui . refreshCommits ( )
2020-03-27 10:12:15 +02:00
}
wg . Done ( )
} ( )
}
2020-03-26 14:48:11 +02:00
2020-03-27 10:12:15 +02:00
if scopeMap [ FILES ] {
wg . Add ( 1 )
func ( ) {
if options . mode == ASYNC {
go gui . refreshFiles ( )
} else {
gui . refreshFiles ( )
}
wg . Done ( )
} ( )
}
if scopeMap [ STASH ] {
wg . Add ( 1 )
func ( ) {
if options . mode == ASYNC {
2020-08-19 10:41:57 +02:00
go gui . refreshStashEntries ( )
2020-03-27 10:12:15 +02:00
} else {
2020-08-19 10:41:57 +02:00
gui . refreshStashEntries ( )
2020-03-27 10:12:15 +02:00
}
wg . Done ( )
} ( )
}
2020-03-26 14:48:11 +02:00
2020-03-28 01:27:34 +02:00
if scopeMap [ TAGS ] {
wg . Add ( 1 )
func ( ) {
if options . mode == ASYNC {
go gui . refreshTags ( )
} else {
gui . refreshTags ( )
}
wg . Done ( )
} ( )
}
if scopeMap [ REMOTES ] {
wg . Add ( 1 )
func ( ) {
if options . mode == ASYNC {
go gui . refreshRemotes ( )
} else {
gui . refreshRemotes ( )
}
wg . Done ( )
} ( )
}
2020-03-27 10:12:15 +02:00
wg . Wait ( )
2020-03-26 14:48:11 +02:00
2020-03-28 01:27:34 +02:00
gui . refreshStatus ( )
2020-03-27 10:12:15 +02:00
if options . then != nil {
options . then ( )
}
}
if options . mode == BLOCK_UI {
gui . g . Update ( func ( g * gocui . Gui ) error {
f ( )
return nil
} )
} else {
f ( )
}
2019-03-11 04:04:08 +02:00
2020-03-26 14:48:11 +02:00
return nil
2018-06-06 04:17:49 +02:00
}
2018-08-14 11:05:26 +02:00
func ( gui * Gui ) resetOrigin ( v * gocui . View ) error {
2019-04-25 21:37:19 +02:00
_ = v . SetCursor ( 0 , 0 )
2018-07-21 07:51:18 +02:00
return v . SetOrigin ( 0 , 0 )
2018-06-09 11:06:33 +02:00
}
2019-01-15 11:12:31 +02:00
func ( gui * Gui ) cleanString ( s string ) string {
2018-12-12 13:34:20 +02:00
output := string ( bom . Clean ( [ ] byte ( s ) ) )
2019-01-15 11:12:31 +02:00
return utils . NormalizeLinefeeds ( output )
}
2020-05-19 10:01:29 +02:00
func ( gui * Gui ) setViewContent ( v * gocui . View , s string ) {
2019-01-15 11:12:31 +02:00
v . Clear ( )
fmt . Fprint ( v , gui . cleanString ( s ) )
2018-12-12 13:34:20 +02:00
}
2019-01-15 11:12:31 +02:00
// renderString resets the origin of a view and sets its content
2020-08-15 08:36:39 +02:00
func ( gui * Gui ) renderString ( viewName , s string ) {
gui . g . Update ( func ( * gocui . Gui ) error {
2020-08-16 10:25:08 +02:00
return gui . renderStringSync ( viewName , s )
2018-07-21 07:51:18 +02:00
} )
2018-05-26 05:23:39 +02:00
}
2020-08-16 10:25:08 +02:00
func ( gui * Gui ) renderStringSync ( viewName , s string ) error {
v , err := gui . g . View ( viewName )
if err != nil {
return nil // return gracefully if view has been deleted
}
if err := v . SetOrigin ( 0 , 0 ) ; err != nil {
return err
}
if err := v . SetCursor ( 0 , 0 ) ; err != nil {
return err
}
gui . setViewContent ( v , s )
return nil
}
2018-08-14 11:05:26 +02:00
func ( gui * Gui ) optionsMapToString ( optionsMap map [ string ] string ) string {
2018-07-21 07:51:18 +02:00
optionsArray := make ( [ ] string , 0 )
for key , description := range optionsMap {
optionsArray = append ( optionsArray , key + ": " + description )
}
sort . Strings ( optionsArray )
return strings . Join ( optionsArray , ", " )
2018-06-09 11:06:33 +02:00
}
2018-12-07 09:52:31 +02:00
func ( gui * Gui ) renderOptionsMap ( optionsMap map [ string ] string ) error {
2020-08-15 08:36:39 +02:00
gui . renderString ( "options" , gui . optionsMapToString ( optionsMap ) )
2020-03-09 02:34:10 +02:00
return nil
2018-06-09 11:06:33 +02:00
}
2018-07-22 04:07:36 +02:00
2018-08-11 07:09:37 +02:00
// TODO: refactor properly
2018-09-03 18:45:52 +02:00
// i'm so sorry but had to add this getBranchesView
2018-12-08 07:54:54 +02:00
func ( gui * Gui ) getFilesView ( ) * gocui . View {
v , _ := gui . g . View ( "files" )
return v
}
func ( gui * Gui ) getCommitsView ( ) * gocui . View {
v , _ := gui . g . View ( "commits" )
2018-08-11 07:09:37 +02:00
return v
}
2018-12-08 07:54:54 +02:00
func ( gui * Gui ) getCommitMessageView ( ) * gocui . View {
v , _ := gui . g . View ( "commitMessage" )
2018-08-11 07:09:37 +02:00
return v
}
2018-12-08 07:54:54 +02:00
func ( gui * Gui ) getBranchesView ( ) * gocui . View {
v , _ := gui . g . View ( "branches" )
2018-08-11 07:09:37 +02:00
return v
}
2018-08-14 11:05:26 +02:00
2018-12-08 07:54:54 +02:00
func ( gui * Gui ) getMainView ( ) * gocui . View {
v , _ := gui . g . View ( "main" )
2018-12-02 10:57:01 +02:00
return v
}
2019-10-30 11:23:25 +02:00
func ( gui * Gui ) getSecondaryView ( ) * gocui . View {
v , _ := gui . g . View ( "secondary" )
return v
}
2018-12-08 07:54:54 +02:00
func ( gui * Gui ) getStashView ( ) * gocui . View {
v , _ := gui . g . View ( "stash" )
2018-12-06 13:18:17 +02:00
return v
}
2019-03-11 00:28:47 +02:00
func ( gui * Gui ) getCommitFilesView ( ) * gocui . View {
v , _ := gui . g . View ( "commitFiles" )
return v
}
2019-11-16 05:00:27 +02:00
func ( gui * Gui ) getMenuView ( ) * gocui . View {
v , _ := gui . g . View ( "menu" )
return v
}
2020-02-23 12:53:30 +02:00
func ( gui * Gui ) getSearchView ( ) * gocui . View {
v , _ := gui . g . View ( "search" )
return v
}
2020-03-26 14:20:12 +02:00
func ( gui * Gui ) getStatusView ( ) * gocui . View {
v , _ := gui . g . View ( "status" )
return v
}
2020-08-16 05:58:29 +02:00
func ( gui * Gui ) getConfirmationView ( ) * gocui . View {
v , _ := gui . g . View ( "confirmation" )
return v
}
2018-08-14 11:05:26 +02:00
func ( gui * Gui ) trimmedContent ( v * gocui . View ) string {
return strings . TrimSpace ( v . Buffer ( ) )
}
2019-02-25 13:11:35 +02:00
func ( gui * Gui ) currentViewName ( ) string {
currentView := gui . g . CurrentView ( )
2020-05-16 04:35:19 +02:00
if currentView == nil {
return ""
}
2018-08-14 11:05:26 +02:00
return currentView . Name ( )
}
2018-09-05 11:07:46 +02:00
2020-08-15 09:23:16 +02:00
func ( gui * Gui ) resizeCurrentPopupPanel ( ) error {
v := gui . g . CurrentView ( )
2020-08-18 14:41:14 +02:00
if v == nil {
return nil
}
2019-03-11 04:04:08 +02:00
if gui . isPopupPanel ( v . Name ( ) ) {
2020-08-15 09:23:16 +02:00
return gui . resizePopupPanel ( v )
2018-09-05 11:07:46 +02:00
}
return nil
}
2020-08-15 09:23:16 +02:00
func ( gui * Gui ) resizePopupPanel ( v * gocui . View ) error {
2018-09-05 11:07:46 +02:00
// If the confirmation panel is already displayed, just resize the width,
// otherwise continue
content := v . Buffer ( )
2020-08-15 09:23:16 +02:00
x0 , y0 , x1 , y1 := gui . getConfirmationPanelDimensions ( v . Wrap , content )
2018-09-05 11:07:46 +02:00
vx0 , vy0 , vx1 , vy1 := v . Dimensions ( )
if vx0 == x0 && vy0 == y0 && vx1 == x1 && vy1 == y1 {
return nil
}
gui . Log . Info ( gui . Tr . SLocalize ( "resizingPopupPanel" ) )
2020-08-15 09:23:16 +02:00
_ , err := gui . g . SetView ( v . Name ( ) , x0 , y0 , x1 , y1 , 0 )
2018-09-05 11:07:46 +02:00
return err
}
2018-12-04 10:50:11 +02:00
2020-08-20 00:52:51 +02:00
func ( gui * Gui ) changeSelectedLine ( panelState IListPanelState , total int , change int ) {
2019-11-16 05:00:27 +02:00
// TODO: find out why we're doing this
2020-08-20 00:52:51 +02:00
line := panelState . GetSelectedLineIdx ( )
if line == - 1 {
2019-11-16 05:00:27 +02:00
return
}
2020-08-20 00:52:51 +02:00
var newLine int
if line + change < 0 {
newLine = 0
} else if line + change >= total {
newLine = total - 1
2018-12-04 10:50:11 +02:00
} else {
2020-08-20 00:52:51 +02:00
newLine = line + change
2018-12-04 10:50:11 +02:00
}
2020-08-20 00:52:51 +02:00
panelState . SetSelectedLineIdx ( newLine )
2018-12-04 10:50:11 +02:00
}
2020-08-20 00:52:51 +02:00
func ( gui * Gui ) refreshSelectedLine ( panelState IListPanelState , total int ) {
line := panelState . GetSelectedLineIdx ( )
if line == - 1 && total > 0 {
panelState . SetSelectedLineIdx ( 0 )
} else if total - 1 < line {
panelState . SetSelectedLineIdx ( total - 1 )
2018-12-04 10:50:11 +02:00
}
}
2018-12-07 09:52:31 +02:00
2020-02-25 11:11:07 +02:00
func ( gui * Gui ) renderDisplayStrings ( v * gocui . View , displayStrings [ ] [ ] string ) {
gui . g . Update ( func ( g * gocui . Gui ) error {
list := utils . RenderDisplayStrings ( displayStrings )
v . Clear ( )
fmt . Fprint ( v , list )
return nil
} )
}
2018-12-07 09:52:31 +02:00
func ( gui * Gui ) renderPanelOptions ( ) error {
currentView := gui . g . CurrentView ( )
switch currentView . Name ( ) {
case "menu" :
return gui . renderMenuOptions ( )
case "main" :
2019-11-16 03:41:04 +02:00
if gui . State . MainContext == "merging" {
2019-02-16 03:07:27 +02:00
return gui . renderMergeOptions ( )
}
2018-12-07 09:52:31 +02:00
}
2019-02-16 03:07:27 +02:00
return gui . renderGlobalOptions ( )
2018-12-07 09:52:31 +02:00
}
2019-02-25 13:11:35 +02:00
2020-03-29 01:31:34 +02:00
func ( gui * Gui ) renderGlobalOptions ( ) error {
return gui . renderOptionsMap ( map [ string ] string {
fmt . Sprintf ( "%s/%s" , gui . getKeyDisplay ( "universal.scrollUpMain" ) , gui . getKeyDisplay ( "universal.scrollDownMain" ) ) : gui . Tr . SLocalize ( "scroll" ) ,
fmt . Sprintf ( "%s %s %s %s" , gui . getKeyDisplay ( "universal.prevBlock" ) , gui . getKeyDisplay ( "universal.nextBlock" ) , gui . getKeyDisplay ( "universal.prevItem" ) , gui . getKeyDisplay ( "universal.nextItem" ) ) : gui . Tr . SLocalize ( "navigate" ) ,
2020-07-18 11:41:13 +02:00
gui . getKeyDisplay ( "universal.return" ) : gui . Tr . SLocalize ( "cancel" ) ,
gui . getKeyDisplay ( "universal.quit" ) : gui . Tr . SLocalize ( "quit" ) ,
2020-03-29 01:31:34 +02:00
gui . getKeyDisplay ( "universal.optionMenu" ) : gui . Tr . SLocalize ( "menu" ) ,
"1-5" : gui . Tr . SLocalize ( "jump" ) ,
} )
}
2019-03-11 04:04:08 +02:00
func ( gui * Gui ) isPopupPanel ( viewName string ) bool {
return viewName == "commitMessage" || viewName == "credentials" || viewName == "confirmation" || viewName == "menu"
}
2019-02-25 13:11:35 +02:00
func ( gui * Gui ) popupPanelFocused ( ) bool {
2019-03-11 04:04:08 +02:00
return gui . isPopupPanel ( gui . currentViewName ( ) )
2019-02-25 13:11:35 +02:00
}
2019-11-10 07:20:35 +02:00
2019-11-17 08:23:06 +02:00
// often gocui wants functions in the form `func(g *gocui.Gui, v *gocui.View) error`
// but sometimes we just have a function that returns an error, so this is a
// convenience wrapper to give gocui what it wants.
func ( gui * Gui ) wrappedHandler ( f func ( ) error ) func ( g * gocui . Gui , v * gocui . View ) error {
return func ( g * gocui . Gui , v * gocui . View ) error {
return f ( )
}
}
2020-05-18 14:00:07 +02:00
// secondaryViewFocused tells us whether it appears that the secondary view is focused. The view is actually never focused for real: we just swap the main and secondary views and then you're still focused on the main view so that we can give you access to all its keybindings for free. I will probably regret this design decision soon enough.
func ( gui * Gui ) secondaryViewFocused ( ) bool {
return gui . State . Panels . LineByLine != nil && gui . State . Panels . LineByLine . SecondaryFocused
}
2020-08-17 13:58:30 +02:00
func ( gui * Gui ) clearEditorView ( v * gocui . View ) {
v . Clear ( )
_ = v . SetCursor ( 0 , 0 )
_ = v . SetOrigin ( 0 , 0 )
}
2020-08-19 11:31:58 +02:00
func ( gui * Gui ) onViewTabClick ( viewName string , tabIndex int ) error {
context := gui . ViewTabContextMap [ viewName ] [ tabIndex ] . contexts [ 0 ]
return gui . switchContext ( context )
}
func ( gui * Gui ) handleNextTab ( g * gocui . Gui , v * gocui . View ) error {
return gui . onViewTabClick (
v . Name ( ) ,
utils . ModuloWithWrap ( v . TabIndex + 1 , len ( v . Tabs ) ) ,
)
}
func ( gui * Gui ) handlePrevTab ( g * gocui . Gui , v * gocui . View ) error {
return gui . onViewTabClick (
v . Name ( ) ,
utils . ModuloWithWrap ( v . TabIndex - 1 , len ( v . Tabs ) ) ,
)
}