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

401 lines
9.4 KiB
Go
Raw Normal View History

2018-08-14 11:05:26 +02:00
package gui
2018-05-26 05:23:39 +02:00
import (
"fmt"
"sort"
"strings"
2020-03-26 14:48:11 +02:00
"sync"
2018-05-26 05:23:39 +02:00
"github.com/jesseduffield/gocui"
"github.com/jesseduffield/lazygit/pkg/utils"
"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
const (
COMMITS = iota
BRANCHES
FILES
STASH
REFLOG
TAGS
REMOTES
2020-03-28 00:57:36 +02:00
STATUS
)
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
}
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{}
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})
} else {
scopeMap = intArrToMap(options.scope)
}
if scopeMap[COMMITS] || scopeMap[BRANCHES] || scopeMap[REFLOG] {
wg.Add(1)
func() {
if options.mode == ASYNC {
2020-03-27 12:23:42 +02:00
go gui.refreshCommits()
} else {
2020-03-27 12:23:42 +02:00
gui.refreshCommits()
}
wg.Done()
}()
}
2020-03-26 14:48:11 +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()
} else {
2020-08-19 10:41:57 +02:00
gui.refreshStashEntries()
}
wg.Done()
}()
}
2020-03-26 14:48:11 +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()
}()
}
wg.Wait()
2020-03-26 14:48:11 +02:00
gui.refreshStatus()
if options.then != nil {
options.then()
}
}
if options.mode == BLOCK_UI {
gui.g.Update(func(g *gocui.Gui) error {
f()
return nil
})
} else {
f()
}
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)
return v.SetOrigin(0, 0)
2018-06-09 11:06:33 +02:00
}
func (gui *Gui) cleanString(s string) string {
output := string(bom.Clean([]byte(s)))
return utils.NormalizeLinefeeds(output)
}
func (gui *Gui) setViewContent(v *gocui.View, s string) {
v.Clear()
fmt.Fprint(v, gui.cleanString(s))
}
// 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 {
return gui.renderStringSync(viewName, s)
})
2018-05-26 05:23:39 +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 {
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-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")
return v
}
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")
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
}
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
}
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
}
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
line := panelState.GetSelectedLineIdx()
if line == -1 {
2019-11-16 05:00:27 +02:00
return
}
var newLine int
if line+change < 0 {
newLine = 0
} else if line+change >= total {
newLine = total - 1
} else {
newLine = line + change
}
panelState.SetSelectedLineIdx(newLine)
}
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-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" {
return gui.renderMergeOptions()
}
2018-12-07 09:52:31 +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"),
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"),
})
}
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 {
return gui.isPopupPanel(gui.currentViewName())
2019-02-25 13:11:35 +02:00
}
2019-11-10 07:20:35 +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)),
)
}