mirror of
https://github.com/jesseduffield/lazygit.git
synced 2024-12-12 11:15:00 +02:00
46be280c92
For now we're just doing side panels, because it will take more work to support this in the various main panel contexts
462 lines
11 KiB
Go
462 lines
11 KiB
Go
package gui
|
|
|
|
import (
|
|
"fmt"
|
|
"sort"
|
|
"strings"
|
|
|
|
"github.com/go-errors/errors"
|
|
"github.com/jesseduffield/gocui"
|
|
"github.com/jesseduffield/lazygit/pkg/utils"
|
|
"github.com/spkg/bom"
|
|
)
|
|
|
|
var cyclableViews = []string{"status", "files", "branches", "commits", "stash"}
|
|
|
|
func (gui *Gui) refreshSidePanels(g *gocui.Gui) error {
|
|
if err := gui.refreshBranches(g); err != nil {
|
|
return err
|
|
}
|
|
if err := gui.refreshFiles(); err != nil {
|
|
return err
|
|
}
|
|
if err := gui.refreshCommits(g); err != nil {
|
|
return err
|
|
}
|
|
|
|
return gui.refreshStashEntries(g)
|
|
}
|
|
|
|
func (gui *Gui) nextView(g *gocui.Gui, v *gocui.View) error {
|
|
var focusedViewName string
|
|
if v == nil || v.Name() == cyclableViews[len(cyclableViews)-1] {
|
|
focusedViewName = cyclableViews[0]
|
|
} else {
|
|
// if we're in the commitFiles view we'll act like we're in the commits view
|
|
viewName := v.Name()
|
|
if viewName == "commitFiles" {
|
|
viewName = "commits"
|
|
}
|
|
for i := range cyclableViews {
|
|
if viewName == cyclableViews[i] {
|
|
focusedViewName = cyclableViews[i+1]
|
|
break
|
|
}
|
|
if i == len(cyclableViews)-1 {
|
|
message := gui.Tr.TemplateLocalize(
|
|
"IssntListOfViews",
|
|
Teml{
|
|
"name": viewName,
|
|
},
|
|
)
|
|
gui.Log.Info(message)
|
|
return nil
|
|
}
|
|
}
|
|
}
|
|
focusedView, err := g.View(focusedViewName)
|
|
if err != nil {
|
|
panic(err)
|
|
}
|
|
return gui.switchFocus(g, v, focusedView)
|
|
}
|
|
|
|
func (gui *Gui) previousView(g *gocui.Gui, v *gocui.View) error {
|
|
var focusedViewName string
|
|
if v == nil || v.Name() == cyclableViews[0] {
|
|
focusedViewName = cyclableViews[len(cyclableViews)-1]
|
|
} else {
|
|
// if we're in the commitFiles view we'll act like we're in the commits view
|
|
viewName := v.Name()
|
|
if viewName == "commitFiles" {
|
|
viewName = "commits"
|
|
}
|
|
for i := range cyclableViews {
|
|
if viewName == cyclableViews[i] {
|
|
focusedViewName = cyclableViews[i-1] // TODO: make this work properly
|
|
break
|
|
}
|
|
if i == len(cyclableViews)-1 {
|
|
message := gui.Tr.TemplateLocalize(
|
|
"IssntListOfViews",
|
|
Teml{
|
|
"name": viewName,
|
|
},
|
|
)
|
|
gui.Log.Info(message)
|
|
return nil
|
|
}
|
|
}
|
|
}
|
|
focusedView, err := g.View(focusedViewName)
|
|
if err != nil {
|
|
panic(err)
|
|
}
|
|
return gui.switchFocus(g, v, focusedView)
|
|
}
|
|
|
|
func (gui *Gui) newLineFocused(g *gocui.Gui, v *gocui.View) error {
|
|
switch v.Name() {
|
|
case "menu":
|
|
return gui.handleMenuSelect(g, v)
|
|
case "status":
|
|
return gui.handleStatusSelect(g, v)
|
|
case "files":
|
|
return gui.focusAndSelectFile(g, v)
|
|
case "branches":
|
|
branchesView := gui.getBranchesView()
|
|
switch branchesView.Context {
|
|
case "local-branches":
|
|
return gui.handleBranchSelect(g, v)
|
|
case "remotes":
|
|
return gui.handleRemoteSelect(g, v)
|
|
case "remote-branches":
|
|
return gui.handleRemoteBranchSelect(g, v)
|
|
case "tags":
|
|
return gui.handleTagSelect(g, v)
|
|
default:
|
|
return errors.New("unknown branches panel context: " + branchesView.Context)
|
|
}
|
|
case "commits":
|
|
return gui.handleCommitSelect(g, v)
|
|
case "commitFiles":
|
|
return gui.handleCommitFileSelect(g, v)
|
|
case "stash":
|
|
return gui.handleStashEntrySelect(g, v)
|
|
case "confirmation":
|
|
return nil
|
|
case "commitMessage":
|
|
return gui.handleCommitFocused(g, v)
|
|
case "credentials":
|
|
return gui.handleCredentialsViewFocused(g, v)
|
|
case "main":
|
|
if gui.State.MainContext == "merging" {
|
|
return gui.refreshMergePanel()
|
|
}
|
|
v.Highlight = false
|
|
return nil
|
|
case "search":
|
|
return nil
|
|
default:
|
|
panic(gui.Tr.SLocalize("NoViewMachingNewLineFocusedSwitchStatement"))
|
|
}
|
|
}
|
|
|
|
func (gui *Gui) returnFocus(g *gocui.Gui, v *gocui.View) error {
|
|
previousView, err := g.View(gui.State.PreviousView)
|
|
if err != nil {
|
|
// always fall back to files view if there's no 'previous' view stored
|
|
previousView, err = g.View("files")
|
|
if err != nil {
|
|
gui.Log.Error(err)
|
|
}
|
|
}
|
|
return gui.switchFocus(g, v, previousView)
|
|
}
|
|
|
|
func (gui *Gui) goToSideView(sideViewName string) func(g *gocui.Gui, v *gocui.View) error {
|
|
return func(g *gocui.Gui, v *gocui.View) error {
|
|
view, err := g.View(sideViewName)
|
|
if err != nil {
|
|
gui.Log.Error(err)
|
|
return nil
|
|
}
|
|
err = gui.closePopupPanels()
|
|
if err != nil {
|
|
gui.Log.Error(err)
|
|
return nil
|
|
}
|
|
return gui.switchFocus(g, nil, view)
|
|
}
|
|
}
|
|
|
|
func (gui *Gui) closePopupPanels() error {
|
|
gui.onNewPopupPanel()
|
|
err := gui.closeConfirmationPrompt(gui.g, true)
|
|
if err != nil {
|
|
gui.Log.Error(err)
|
|
return err
|
|
}
|
|
return nil
|
|
}
|
|
|
|
// pass in oldView = nil if you don't want to be able to return to your old view
|
|
// TODO: move some of this logic into our onFocusLost and onFocus hooks
|
|
func (gui *Gui) switchFocus(g *gocui.Gui, oldView, newView *gocui.View) error {
|
|
// we assume we'll never want to return focus to a popup panel i.e.
|
|
// we should never stack popup panels
|
|
if oldView != nil && !gui.isPopupPanel(oldView.Name()) {
|
|
gui.State.PreviousView = oldView.Name()
|
|
}
|
|
|
|
gui.Log.Info("setting highlight to true for view" + newView.Name())
|
|
message := gui.Tr.TemplateLocalize(
|
|
"newFocusedViewIs",
|
|
Teml{
|
|
"newFocusedView": newView.Name(),
|
|
},
|
|
)
|
|
gui.Log.Info(message)
|
|
if _, err := g.SetCurrentView(newView.Name()); err != nil {
|
|
return err
|
|
}
|
|
if _, err := g.SetViewOnTop(newView.Name()); err != nil {
|
|
return err
|
|
}
|
|
|
|
g.Cursor = newView.Editable
|
|
|
|
if err := gui.renderPanelOptions(); err != nil {
|
|
return err
|
|
}
|
|
|
|
return gui.newLineFocused(g, newView)
|
|
}
|
|
|
|
func (gui *Gui) resetOrigin(v *gocui.View) error {
|
|
_ = v.SetCursor(0, 0)
|
|
return v.SetOrigin(0, 0)
|
|
}
|
|
|
|
// if the cursor down past the last item, move it to the last line
|
|
func (gui *Gui) focusPoint(cx int, cy int, lineCount int, v *gocui.View) error {
|
|
v.FocusPoint(cx, cy)
|
|
return nil
|
|
}
|
|
|
|
func (gui *Gui) cleanString(s string) string {
|
|
output := string(bom.Clean([]byte(s)))
|
|
return utils.NormalizeLinefeeds(output)
|
|
}
|
|
|
|
func (gui *Gui) setViewContent(g *gocui.Gui, v *gocui.View, s string) error {
|
|
v.Clear()
|
|
fmt.Fprint(v, gui.cleanString(s))
|
|
return nil
|
|
}
|
|
|
|
// renderString resets the origin of a view and sets its content
|
|
func (gui *Gui) renderString(g *gocui.Gui, viewName, s string) error {
|
|
g.Update(func(*gocui.Gui) error {
|
|
v, err := 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
|
|
}
|
|
return gui.setViewContent(gui.g, v, s)
|
|
})
|
|
return nil
|
|
}
|
|
|
|
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, ", ")
|
|
}
|
|
|
|
func (gui *Gui) renderOptionsMap(optionsMap map[string]string) error {
|
|
return gui.renderString(gui.g, "options", gui.optionsMapToString(optionsMap))
|
|
}
|
|
|
|
// TODO: refactor properly
|
|
// i'm so sorry but had to add this getBranchesView
|
|
func (gui *Gui) getFilesView() *gocui.View {
|
|
v, _ := gui.g.View("files")
|
|
return v
|
|
}
|
|
|
|
func (gui *Gui) getCommitsView() *gocui.View {
|
|
v, _ := gui.g.View("commits")
|
|
return v
|
|
}
|
|
|
|
func (gui *Gui) getCommitMessageView() *gocui.View {
|
|
v, _ := gui.g.View("commitMessage")
|
|
return v
|
|
}
|
|
|
|
func (gui *Gui) getBranchesView() *gocui.View {
|
|
v, _ := gui.g.View("branches")
|
|
return v
|
|
}
|
|
|
|
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
|
|
}
|
|
|
|
func (gui *Gui) getStashView() *gocui.View {
|
|
v, _ := gui.g.View("stash")
|
|
return v
|
|
}
|
|
|
|
func (gui *Gui) getCommitFilesView() *gocui.View {
|
|
v, _ := gui.g.View("commitFiles")
|
|
return v
|
|
}
|
|
|
|
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
|
|
}
|
|
|
|
func (gui *Gui) trimmedContent(v *gocui.View) string {
|
|
return strings.TrimSpace(v.Buffer())
|
|
}
|
|
|
|
func (gui *Gui) currentViewName() string {
|
|
currentView := gui.g.CurrentView()
|
|
return currentView.Name()
|
|
}
|
|
|
|
func (gui *Gui) resizeCurrentPopupPanel(g *gocui.Gui) error {
|
|
v := g.CurrentView()
|
|
if gui.isPopupPanel(v.Name()) {
|
|
return gui.resizePopupPanel(g, v)
|
|
}
|
|
return nil
|
|
}
|
|
|
|
func (gui *Gui) resizePopupPanel(g *gocui.Gui, v *gocui.View) error {
|
|
// If the confirmation panel is already displayed, just resize the width,
|
|
// otherwise continue
|
|
content := v.Buffer()
|
|
x0, y0, x1, y1 := gui.getConfirmationPanelDimensions(g, v.Wrap, content)
|
|
vx0, vy0, vx1, vy1 := v.Dimensions()
|
|
if vx0 == x0 && vy0 == y0 && vx1 == x1 && vy1 == y1 {
|
|
return nil
|
|
}
|
|
gui.Log.Info(gui.Tr.SLocalize("resizingPopupPanel"))
|
|
_, err := g.SetView(v.Name(), x0, y0, x1, y1, 0)
|
|
return err
|
|
}
|
|
|
|
// generalFocusLine takes a lineNumber to focus, and a bottomLine to ensure we can see
|
|
func (gui *Gui) generalFocusLine(lineNumber int, bottomLine int, v *gocui.View) error {
|
|
_, height := v.Size()
|
|
overScroll := bottomLine - height + 1
|
|
if overScroll < 0 {
|
|
overScroll = 0
|
|
}
|
|
if err := v.SetOrigin(0, overScroll); err != nil {
|
|
return err
|
|
}
|
|
if err := v.SetCursor(0, lineNumber-overScroll); err != nil {
|
|
return err
|
|
}
|
|
return nil
|
|
}
|
|
|
|
func (gui *Gui) changeSelectedLine(line *int, total int, change int) {
|
|
// TODO: find out why we're doing this
|
|
if *line == -1 {
|
|
return
|
|
}
|
|
if *line+change < 0 {
|
|
*line = 0
|
|
} else if *line+change >= total {
|
|
*line = total - 1
|
|
} else {
|
|
*line += change
|
|
}
|
|
}
|
|
|
|
func (gui *Gui) refreshSelectedLine(line *int, total int) {
|
|
if *line == -1 && total > 0 {
|
|
*line = 0
|
|
} else if total-1 < *line {
|
|
*line = total - 1
|
|
}
|
|
}
|
|
|
|
func (gui *Gui) renderListPanel(v *gocui.View, items interface{}) error {
|
|
gui.g.Update(func(g *gocui.Gui) error {
|
|
isFocused := gui.g.CurrentView().Name() == v.Name()
|
|
list, err := utils.RenderList(items, isFocused)
|
|
if err != nil {
|
|
return gui.createErrorPanel(gui.g, err.Error())
|
|
}
|
|
v.Clear()
|
|
fmt.Fprint(v, list)
|
|
return nil
|
|
})
|
|
return nil
|
|
}
|
|
|
|
func (gui *Gui) renderPanelOptions() error {
|
|
currentView := gui.g.CurrentView()
|
|
switch currentView.Name() {
|
|
case "menu":
|
|
return gui.renderMenuOptions()
|
|
case "main":
|
|
if gui.State.MainContext == "merging" {
|
|
return gui.renderMergeOptions()
|
|
}
|
|
}
|
|
return gui.renderGlobalOptions()
|
|
}
|
|
|
|
func (gui *Gui) handleFocusView(g *gocui.Gui, v *gocui.View) error {
|
|
_, err := gui.g.SetCurrentView(v.Name())
|
|
return err
|
|
}
|
|
|
|
func (gui *Gui) isPopupPanel(viewName string) bool {
|
|
return viewName == "commitMessage" || viewName == "credentials" || viewName == "confirmation" || viewName == "menu"
|
|
}
|
|
|
|
func (gui *Gui) popupPanelFocused() bool {
|
|
return gui.isPopupPanel(gui.currentViewName())
|
|
}
|
|
|
|
func (gui *Gui) handleClick(v *gocui.View, itemCount int, selectedLine *int, handleSelect func(*gocui.Gui, *gocui.View) error) error {
|
|
if gui.popupPanelFocused() && v != nil && !gui.isPopupPanel(v.Name()) {
|
|
return nil
|
|
}
|
|
|
|
if _, err := gui.g.SetCurrentView(v.Name()); err != nil {
|
|
return err
|
|
}
|
|
|
|
newSelectedLine := v.SelectedLineIdx()
|
|
|
|
if newSelectedLine < 0 {
|
|
newSelectedLine = 0
|
|
}
|
|
|
|
if newSelectedLine > itemCount-1 {
|
|
newSelectedLine = itemCount - 1
|
|
}
|
|
|
|
*selectedLine = newSelectedLine
|
|
|
|
return handleSelect(gui.g, v)
|
|
}
|
|
|
|
// 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()
|
|
}
|
|
}
|