mirror of
https://github.com/jesseduffield/lazygit.git
synced 2024-12-12 11:15:00 +02:00
2e05ac0c90
support searching in line by line panel move mutexes into their own struct add line by line panel mutex apply LBL panel mutex bump gocui to prevent crashing when search item count decreases
376 lines
11 KiB
Go
376 lines
11 KiB
Go
package gui
|
|
|
|
import (
|
|
"github.com/fatih/color"
|
|
"github.com/jesseduffield/gocui"
|
|
"github.com/jesseduffield/lazygit/pkg/theme"
|
|
)
|
|
|
|
const SEARCH_PREFIX = "search: "
|
|
const INFO_SECTION_PADDING = " "
|
|
|
|
func (gui *Gui) informationStr() string {
|
|
for _, mode := range gui.modeStatuses() {
|
|
if mode.isActive() {
|
|
return mode.description()
|
|
}
|
|
}
|
|
|
|
if gui.g.Mouse {
|
|
donate := color.New(color.FgMagenta, color.Underline).Sprint(gui.Tr.Donate)
|
|
return donate + " " + gui.Config.GetVersion()
|
|
} else {
|
|
return gui.Config.GetVersion()
|
|
}
|
|
}
|
|
|
|
// layout is called for every screen re-render e.g. when the screen is resized
|
|
func (gui *Gui) layout(g *gocui.Gui) error {
|
|
g.Highlight = true
|
|
width, height := g.Size()
|
|
|
|
minimumHeight := 9
|
|
minimumWidth := 10
|
|
if height < minimumHeight || width < minimumWidth {
|
|
v, err := g.SetView("limit", 0, 0, width-1, height-1, 0)
|
|
if err != nil {
|
|
if err.Error() != "unknown view" {
|
|
return err
|
|
}
|
|
v.Title = gui.Tr.NotEnoughSpace
|
|
v.Wrap = true
|
|
_, _ = g.SetViewOnTop("limit")
|
|
}
|
|
return nil
|
|
}
|
|
|
|
informationStr := gui.informationStr()
|
|
appStatus := gui.statusManager.getStatusString()
|
|
|
|
viewDimensions := gui.getWindowDimensions(informationStr, appStatus)
|
|
|
|
_, _ = g.SetViewOnBottom("limit")
|
|
_ = g.DeleteView("limit")
|
|
|
|
textColor := theme.GocuiDefaultTextColor
|
|
|
|
// reading more lines into main view buffers upon resize
|
|
prevMainView, err := gui.g.View("main")
|
|
if err == nil {
|
|
_, prevMainHeight := prevMainView.Size()
|
|
newMainHeight := viewDimensions["main"].Y1 - viewDimensions["main"].Y0 - 1
|
|
heightDiff := newMainHeight - prevMainHeight
|
|
if heightDiff > 0 {
|
|
if manager, ok := gui.viewBufferManagerMap["main"]; ok {
|
|
manager.ReadLines(heightDiff)
|
|
}
|
|
if manager, ok := gui.viewBufferManagerMap["secondary"]; ok {
|
|
manager.ReadLines(heightDiff)
|
|
}
|
|
}
|
|
}
|
|
|
|
setViewFromDimensions := func(viewName string, windowName string, frame bool) (*gocui.View, error) {
|
|
dimensionsObj, ok := viewDimensions[windowName]
|
|
|
|
if !ok {
|
|
// view not specified in dimensions object: so create the view and hide it
|
|
// making the view take up the whole space in the background in case it needs
|
|
// to render content as soon as it appears, because lazyloaded content (via a pty task)
|
|
// cares about the size of the view.
|
|
view, err := g.SetView(viewName, 0, 0, width, height, 0)
|
|
if err != nil {
|
|
return view, err
|
|
}
|
|
return g.SetViewOnBottom(viewName)
|
|
}
|
|
|
|
frameOffset := 1
|
|
if frame {
|
|
frameOffset = 0
|
|
}
|
|
return g.SetView(
|
|
viewName,
|
|
dimensionsObj.X0-frameOffset,
|
|
dimensionsObj.Y0-frameOffset,
|
|
dimensionsObj.X1+frameOffset,
|
|
dimensionsObj.Y1+frameOffset,
|
|
0,
|
|
)
|
|
}
|
|
|
|
v, err := setViewFromDimensions("main", "main", true)
|
|
if err != nil {
|
|
if err.Error() != "unknown view" {
|
|
return err
|
|
}
|
|
v.Title = gui.Tr.DiffTitle
|
|
v.Wrap = true
|
|
v.FgColor = textColor
|
|
v.IgnoreCarriageReturns = true
|
|
}
|
|
|
|
secondaryView, err := setViewFromDimensions("secondary", "secondary", true)
|
|
if err != nil {
|
|
if err.Error() != "unknown view" {
|
|
return err
|
|
}
|
|
secondaryView.Title = gui.Tr.DiffTitle
|
|
secondaryView.Wrap = true
|
|
secondaryView.FgColor = textColor
|
|
secondaryView.IgnoreCarriageReturns = true
|
|
}
|
|
|
|
hiddenViewOffset := 9999
|
|
|
|
if v, err := setViewFromDimensions("status", "status", true); err != nil {
|
|
if err.Error() != "unknown view" {
|
|
return err
|
|
}
|
|
v.Title = gui.Tr.StatusTitle
|
|
v.FgColor = textColor
|
|
}
|
|
|
|
filesView, err := setViewFromDimensions("files", "files", true)
|
|
if err != nil {
|
|
if err.Error() != "unknown view" {
|
|
return err
|
|
}
|
|
filesView.Highlight = true
|
|
filesView.Title = gui.Tr.FilesTitle
|
|
filesView.ContainsList = true
|
|
}
|
|
|
|
branchesView, err := setViewFromDimensions("branches", "branches", true)
|
|
if err != nil {
|
|
if err.Error() != "unknown view" {
|
|
return err
|
|
}
|
|
branchesView.Title = gui.Tr.BranchesTitle
|
|
branchesView.FgColor = textColor
|
|
branchesView.ContainsList = true
|
|
}
|
|
|
|
commitFilesView, err := setViewFromDimensions("commitFiles", gui.Contexts.CommitFiles.Context.GetWindowName(), true)
|
|
if err != nil {
|
|
if err.Error() != "unknown view" {
|
|
return err
|
|
}
|
|
commitFilesView.Title = gui.Tr.CommitFiles
|
|
commitFilesView.FgColor = textColor
|
|
commitFilesView.ContainsList = true
|
|
}
|
|
|
|
commitsView, err := setViewFromDimensions("commits", "commits", true)
|
|
if err != nil {
|
|
if err.Error() != "unknown view" {
|
|
return err
|
|
}
|
|
commitsView.Title = gui.Tr.CommitsTitle
|
|
commitsView.FgColor = textColor
|
|
commitsView.ContainsList = true
|
|
}
|
|
|
|
stashView, err := setViewFromDimensions("stash", "stash", true)
|
|
if err != nil {
|
|
if err.Error() != "unknown view" {
|
|
return err
|
|
}
|
|
stashView.Title = gui.Tr.StashTitle
|
|
stashView.FgColor = textColor
|
|
stashView.ContainsList = true
|
|
}
|
|
|
|
if gui.getCommitMessageView() == nil {
|
|
// doesn't matter where this view starts because it will be hidden
|
|
if commitMessageView, err := g.SetView("commitMessage", hiddenViewOffset, hiddenViewOffset, hiddenViewOffset+10, hiddenViewOffset+10, 0); err != nil {
|
|
if err.Error() != "unknown view" {
|
|
return err
|
|
}
|
|
_, _ = g.SetViewOnBottom("commitMessage")
|
|
commitMessageView.Title = gui.Tr.CommitMessage
|
|
commitMessageView.FgColor = textColor
|
|
commitMessageView.Editable = true
|
|
commitMessageView.Editor = gocui.EditorFunc(gui.commitMessageEditor)
|
|
}
|
|
}
|
|
|
|
if check, _ := g.View("credentials"); check == nil {
|
|
// doesn't matter where this view starts because it will be hidden
|
|
if credentialsView, err := g.SetView("credentials", hiddenViewOffset, hiddenViewOffset, hiddenViewOffset+10, hiddenViewOffset+10, 0); err != nil {
|
|
if err.Error() != "unknown view" {
|
|
return err
|
|
}
|
|
_, _ = g.SetViewOnBottom("credentials")
|
|
credentialsView.Title = gui.Tr.CredentialsUsername
|
|
credentialsView.FgColor = textColor
|
|
credentialsView.Editable = true
|
|
}
|
|
}
|
|
|
|
if v, err := setViewFromDimensions("options", "options", false); err != nil {
|
|
if err.Error() != "unknown view" {
|
|
return err
|
|
}
|
|
v.Frame = false
|
|
v.FgColor = theme.OptionsColor
|
|
|
|
// doing this here because it'll only happen once
|
|
if err := gui.onInitialViewsCreation(); err != nil {
|
|
return err
|
|
}
|
|
}
|
|
|
|
// this view takes up one character. Its only purpose is to show the slash when searching
|
|
if searchPrefixView, err := setViewFromDimensions("searchPrefix", "searchPrefix", false); err != nil {
|
|
if err.Error() != "unknown view" {
|
|
return err
|
|
}
|
|
|
|
searchPrefixView.BgColor = gocui.ColorDefault
|
|
searchPrefixView.FgColor = gocui.ColorGreen
|
|
searchPrefixView.Frame = false
|
|
gui.setViewContent(searchPrefixView, SEARCH_PREFIX)
|
|
}
|
|
|
|
if searchView, err := setViewFromDimensions("search", "search", false); err != nil {
|
|
if err.Error() != "unknown view" {
|
|
return err
|
|
}
|
|
|
|
searchView.BgColor = gocui.ColorDefault
|
|
searchView.FgColor = gocui.ColorGreen
|
|
searchView.Frame = false
|
|
searchView.Editable = true
|
|
}
|
|
|
|
if appStatusView, err := setViewFromDimensions("appStatus", "appStatus", false); err != nil {
|
|
if err.Error() != "unknown view" {
|
|
return err
|
|
}
|
|
appStatusView.BgColor = gocui.ColorDefault
|
|
appStatusView.FgColor = gocui.ColorCyan
|
|
appStatusView.Frame = false
|
|
_, _ = g.SetViewOnBottom("appStatus")
|
|
}
|
|
|
|
informationView, err := setViewFromDimensions("information", "information", false)
|
|
if err != nil {
|
|
if err.Error() != "unknown view" {
|
|
return err
|
|
}
|
|
informationView.BgColor = gocui.ColorDefault
|
|
informationView.FgColor = gocui.ColorGreen
|
|
informationView.Frame = false
|
|
gui.renderString("information", INFO_SECTION_PADDING+informationStr)
|
|
}
|
|
if gui.State.OldInformation != informationStr {
|
|
gui.setViewContent(informationView, informationStr)
|
|
gui.State.OldInformation = informationStr
|
|
}
|
|
|
|
if gui.g.CurrentView() == nil {
|
|
initialContext := gui.Contexts.Files.Context
|
|
if gui.State.Modes.Filtering.Active() {
|
|
initialContext = gui.Contexts.BranchCommits.Context
|
|
}
|
|
|
|
if err := gui.switchContext(initialContext); err != nil {
|
|
return err
|
|
}
|
|
}
|
|
|
|
type listContextState struct {
|
|
view *gocui.View
|
|
listContext *ListContext
|
|
}
|
|
|
|
// TODO: don't we already have the view included in the context object itself? Or might that change in a way we don't want reflected here?
|
|
listContextStates := []listContextState{
|
|
{view: filesView, listContext: gui.filesListContext()},
|
|
{view: filesView, listContext: gui.submodulesListContext()},
|
|
{view: branchesView, listContext: gui.branchesListContext()},
|
|
{view: branchesView, listContext: gui.remotesListContext()},
|
|
{view: branchesView, listContext: gui.remoteBranchesListContext()},
|
|
{view: branchesView, listContext: gui.tagsListContext()},
|
|
{view: commitsView, listContext: gui.branchCommitsListContext()},
|
|
{view: commitsView, listContext: gui.reflogCommitsListContext()},
|
|
{view: stashView, listContext: gui.stashListContext()},
|
|
{view: commitFilesView, listContext: gui.commitFilesListContext()},
|
|
}
|
|
|
|
// menu view might not exist so we check to be safe
|
|
if menuView, err := gui.g.View("menu"); err == nil {
|
|
listContextStates = append(listContextStates, listContextState{view: menuView, listContext: gui.menuListContext()})
|
|
}
|
|
for _, listContextState := range listContextStates {
|
|
// ignore contexts whose view is owned by another context right now
|
|
if listContextState.view.Context != listContextState.listContext.GetKey() {
|
|
continue
|
|
}
|
|
// check if the selected line is now out of view and if so refocus it
|
|
listContextState.view.FocusPoint(0, listContextState.listContext.GetPanelState().GetSelectedLineIdx())
|
|
|
|
listContextState.view.SelBgColor = theme.GocuiSelectedLineBgColor
|
|
|
|
// I doubt this is expensive though it's admittedly redundant after the first render
|
|
listContextState.view.SetOnSelectItem(gui.onSelectItemWrapper(listContextState.listContext.onSearchSelect))
|
|
}
|
|
|
|
gui.getMainView().SetOnSelectItem(gui.onSelectItemWrapper(gui.handlelineByLineNavigateTo))
|
|
|
|
mainViewWidth, mainViewHeight := gui.getMainView().Size()
|
|
if mainViewWidth != gui.State.PrevMainWidth || mainViewHeight != gui.State.PrevMainHeight {
|
|
gui.State.PrevMainWidth = mainViewWidth
|
|
gui.State.PrevMainHeight = mainViewHeight
|
|
if err := gui.onResize(); err != nil {
|
|
return err
|
|
}
|
|
}
|
|
|
|
// here is a good place log some stuff
|
|
// if you run `lazygit --logs`
|
|
// this will let you see these branches as prettified json
|
|
// gui.Log.Info(utils.AsJson(gui.State.Branches[0:4]))
|
|
return gui.resizeCurrentPopupPanel()
|
|
}
|
|
|
|
func (gui *Gui) onInitialViewsCreation() error {
|
|
gui.setInitialViewContexts()
|
|
|
|
// add tabs to views
|
|
gui.g.Mutexes.ViewsMutex.Lock()
|
|
for _, view := range gui.g.Views() {
|
|
tabs := gui.viewTabNames(view.Name())
|
|
if len(tabs) == 0 {
|
|
continue
|
|
}
|
|
view.Tabs = tabs
|
|
}
|
|
gui.g.Mutexes.ViewsMutex.Unlock()
|
|
|
|
if err := gui.switchContext(gui.defaultSideContext()); err != nil {
|
|
return err
|
|
}
|
|
|
|
if err := gui.keybindings(); err != nil {
|
|
return err
|
|
}
|
|
|
|
if gui.showRecentRepos {
|
|
if err := gui.handleCreateRecentReposMenu(); err != nil {
|
|
return err
|
|
}
|
|
gui.showRecentRepos = false
|
|
}
|
|
|
|
return gui.loadNewRepo()
|
|
}
|
|
|
|
func max(a, b int) int {
|
|
if a > b {
|
|
return a
|
|
}
|
|
return b
|
|
}
|