diff options
author | Jesse Duffield <jessedduffield@gmail.com> | 2020-03-29 10:31:34 +1100 |
---|---|---|
committer | Jesse Duffield <jessedduffield@gmail.com> | 2020-03-29 11:37:29 +1100 |
commit | a8db672ffbeba92a32ad34f475760c78aa73303e (patch) | |
tree | e941074a316d4e7df057552a05f9d873ed6fb47a /pkg/gui/layout.go | |
parent | 76b66ae26f428c58b3b0993d8da4bf31d5933dc7 (diff) |
refactor gui.go
Diffstat (limited to 'pkg/gui/layout.go')
-rw-r--r-- | pkg/gui/layout.go | 518 |
1 files changed, 518 insertions, 0 deletions
diff --git a/pkg/gui/layout.go b/pkg/gui/layout.go new file mode 100644 index 000000000..0384fcd72 --- /dev/null +++ b/pkg/gui/layout.go @@ -0,0 +1,518 @@ +package gui + +import ( + "fmt" + + "github.com/fatih/color" + "github.com/jesseduffield/gocui" + "github.com/jesseduffield/lazygit/pkg/theme" + "github.com/jesseduffield/lazygit/pkg/utils" +) + +// getFocusLayout returns a manager function for when view gain and lose focus +func (gui *Gui) getFocusLayout() func(g *gocui.Gui) error { + var previousView *gocui.View + return func(g *gocui.Gui) error { + newView := gui.g.CurrentView() + if err := gui.onFocusChange(); err != nil { + return err + } + // for now we don't consider losing focus to a popup panel as actually losing focus + if newView != previousView && !gui.isPopupPanel(newView.Name()) { + if err := gui.onFocusLost(previousView, newView); err != nil { + return err + } + if err := gui.onFocus(newView); err != nil { + return err + } + previousView = newView + } + return nil + } +} + +func (gui *Gui) onFocusChange() error { + currentView := gui.g.CurrentView() + for _, view := range gui.g.Views() { + view.Highlight = view == currentView + } + return nil +} + +func (gui *Gui) onFocusLost(v *gocui.View, newView *gocui.View) error { + if v == nil { + return nil + } + if v.IsSearching() && newView.Name() != "search" { + if err := gui.onSearchEscape(); err != nil { + return err + } + } + switch v.Name() { + case "main": + // if we have lost focus to a first-class panel, we need to do some cleanup + gui.changeMainViewsContext("normal") + case "commitFiles": + if gui.State.MainContext != "patch-building" { + if _, err := gui.g.SetViewOnBottom(v.Name()); err != nil { + return err + } + } + } + gui.Log.Info(v.Name() + " focus lost") + return nil +} + +func (gui *Gui) onFocus(v *gocui.View) error { + if v == nil { + return nil + } + gui.Log.Info(v.Name() + " focus gained") + return nil +} + +func (gui *Gui) getViewHeights() map[string]int { + currView := gui.g.CurrentView() + currentCyclebleView := gui.State.PreviousView + if currView != nil { + viewName := currView.Name() + usePreviousView := true + for _, view := range cyclableViews { + if view == viewName { + currentCyclebleView = viewName + usePreviousView = false + break + } + } + if usePreviousView { + currentCyclebleView = gui.State.PreviousView + } + } + + // unfortunate result of the fact that these are separate views, have to map explicitly + if currentCyclebleView == "commitFiles" { + currentCyclebleView = "commits" + } + + _, height := gui.g.Size() + + if gui.State.ScreenMode == SCREEN_FULL || gui.State.ScreenMode == SCREEN_HALF { + vHeights := map[string]int{ + "status": 0, + "files": 0, + "branches": 0, + "commits": 0, + "stash": 0, + "options": 0, + } + vHeights[currentCyclebleView] = height - 1 + return vHeights + } + + usableSpace := height - 7 + extraSpace := usableSpace - (usableSpace/3)*3 + + if height >= 28 { + return map[string]int{ + "status": 3, + "files": (usableSpace / 3) + extraSpace, + "branches": usableSpace / 3, + "commits": usableSpace / 3, + "stash": 3, + "options": 1, + } + } + + defaultHeight := 3 + if height < 21 { + defaultHeight = 1 + } + vHeights := map[string]int{ + "status": defaultHeight, + "files": defaultHeight, + "branches": defaultHeight, + "commits": defaultHeight, + "stash": defaultHeight, + "options": defaultHeight, + } + vHeights[currentCyclebleView] = height - defaultHeight*4 - 1 + + return vHeights +} + +// 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() + + information := gui.Config.GetVersion() + if gui.g.Mouse { + donate := color.New(color.FgMagenta, color.Underline).Sprint(gui.Tr.SLocalize("Donate")) + information = donate + " " + information + } + if gui.inFilterMode() { + information = utils.ColoredString(fmt.Sprintf("%s '%s' %s", gui.Tr.SLocalize("filteringBy"), gui.State.FilterPath, utils.ColoredString(gui.Tr.SLocalize("(reset)"), color.Underline)), color.FgRed, color.Bold) + } else if len(gui.State.CherryPickedCommits) > 0 { + information = utils.ColoredString(fmt.Sprintf("%d commits copied", len(gui.State.CherryPickedCommits)), color.FgCyan) + } + + 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.SLocalize("NotEnoughSpace") + v.Wrap = true + _, _ = g.SetViewOnTop("limit") + } + return nil + } + + vHeights := gui.getViewHeights() + + optionsVersionBoundary := width - max(len(utils.Decolorise(information)), 1) + + appStatus := gui.statusManager.getStatusString() + appStatusOptionsBoundary := 0 + if appStatus != "" { + appStatusOptionsBoundary = len(appStatus) + 2 + } + + _, _ = g.SetViewOnBottom("limit") + _ = g.DeleteView("limit") + + sidePanelWidthRatio := gui.Config.GetUserConfig().GetFloat64("gui.sidePanelWidth") + + textColor := theme.GocuiDefaultTextColor + var leftSideWidth int + switch gui.State.ScreenMode { + case SCREEN_NORMAL: + leftSideWidth = int(float64(width) * sidePanelWidthRatio) + case SCREEN_HALF: + leftSideWidth = width/2 - 2 + case SCREEN_FULL: + currentView := gui.g.CurrentView() + if currentView != nil && currentView.Name() == "main" { + leftSideWidth = 0 + } else { + leftSideWidth = width - 1 + } + } + + mainPanelLeft := leftSideWidth + 1 + mainPanelRight := width - 1 + secondaryPanelLeft := width - 1 + secondaryPanelTop := 0 + mainPanelBottom := height - 2 + if gui.State.SplitMainPanel { + if gui.State.ScreenMode == SCREEN_FULL { + mainPanelLeft = 0 + panelSplitX := width/2 - 4 + mainPanelRight = panelSplitX + secondaryPanelLeft = panelSplitX + 1 + } else if width < 220 { + mainPanelBottom = height/2 - 1 + secondaryPanelTop = mainPanelBottom + 1 + secondaryPanelLeft = leftSideWidth + 1 + } else { + units := 5 + leftSideWidth = width / units + mainPanelLeft = leftSideWidth + 1 + panelSplitX := (1 + ((units - 1) / 2)) * width / units + mainPanelRight = panelSplitX + secondaryPanelLeft = panelSplitX + 1 + } + } + + main := "main" + secondary := "secondary" + swappingMainPanels := gui.State.Panels.LineByLine != nil && gui.State.Panels.LineByLine.SecondaryFocused + if swappingMainPanels { + main = "secondary" + secondary = "main" + } + + // reading more lines into main view buffers upon resize + prevMainView, err := gui.g.View("main") + if err == nil { + _, prevMainHeight := prevMainView.Size() + heightDiff := mainPanelBottom - prevMainHeight - 1 + if heightDiff > 0 { + if manager, ok := gui.viewBufferManagerMap["main"]; ok { + manager.ReadLines(heightDiff) + } + if manager, ok := gui.viewBufferManagerMap["secondary"]; ok { + manager.ReadLines(heightDiff) + } + } + } + + v, err := g.SetView(main, mainPanelLeft, 0, mainPanelRight, mainPanelBottom, gocui.LEFT) + if err != nil { + if err.Error() != "unknown view" { + return err + } + v.Title = gui.Tr.SLocalize("DiffTitle") + v.Wrap = true + v.FgColor = textColor + v.IgnoreCarriageReturns = true + } + + hiddenViewOffset := 9999 + + hiddenSecondaryPanelOffset := 0 + if !gui.State.SplitMainPanel { + hiddenSecondaryPanelOffset = hiddenViewOffset + } + secondaryView, err := g.SetView(secondary, secondaryPanelLeft+hiddenSecondaryPanelOffset, hiddenSecondaryPanelOffset+secondaryPanelTop, width-1+hiddenSecondaryPanelOffset, height-2+hiddenSecondaryPanelOffset, gocui.LEFT) + if err != nil { + if err.Error() != "unknown view" { + return err + } + secondaryView.Title = gui.Tr.SLocalize("DiffTitle") + secondaryView.Wrap = true + secondaryView.FgColor = gocui.ColorWhite + secondaryView.IgnoreCarriageReturns = true + } + + if v, err := g.SetView("status", 0, 0, leftSideWidth, vHeights["status"]-1, gocui.BOTTOM|gocui.RIGHT); err != nil { + if err.Error() != "unknown view" { + return err + } + v.Title = gui.Tr.SLocalize("StatusTitle") + v.FgColor = textColor + } + + filesView, err := g.SetViewBeneath("files", "status", vHeights["files"]) + if err != nil { + if err.Error() != "unknown view" { + return err + } + filesView.Highlight = true + filesView.Title = gui.Tr.SLocalize("FilesTitle") + filesView.SetOnSelectItem(gui.onSelectItemWrapper(gui.onFilesPanelSearchSelect)) + filesView.ContainsList = true + } + + branchesView, err := g.SetViewBeneath("branches", "files", vHeights["branches"]) + if err != nil { + if err.Error() != "unknown view" { + return err + } + branchesView.Title = gui.Tr.SLocalize("BranchesTitle") + branchesView.Tabs = []string{"Local Branches", "Remotes", "Tags"} + branchesView.FgColor = textColor + branchesView.SetOnSelectItem(gui.onSelectItemWrapper(gui.onBranchesPanelSearchSelect)) + branchesView.ContainsList = true + } + + if v, err := g.SetViewBeneath("commitFiles", "branches", vHeights["commits"]); err != nil { + if err.Error() != "unknown view" { + return err + } + v.Title = gui.Tr.SLocalize("CommitFiles") + v.FgColor = textColor + v.SetOnSelectItem(gui.onSelectItemWrapper(gui.onCommitFilesPanelSearchSelect)) + v.ContainsList = true + } + + commitsView, err := g.SetViewBeneath("commits", "branches", vHeights["commits"]) + if err != nil { + if err.Error() != "unknown view" { + return err + } + commitsView.Title = gui.Tr.SLocalize("CommitsTitle") + commitsView.Tabs = []string{"Commits", "Reflog"} + commitsView.FgColor = textColor + commitsView.SetOnSelectItem(gui.onSelectItemWrapper(gui.onCommitsPanelSearchSelect)) + commitsView.ContainsList = true + } + + stashView, err := g.SetViewBeneath("stash", "commits", vHeights["stash"]) + if err != nil { + if err.Error() != "unknown view" { + return err + } + stashView.Title = gui.Tr.SLocalize("StashTitle") + stashView.FgColor = textColor + stashView.SetOnSelectItem(gui.onSelectItemWrapper(gui.onStashPanelSearchSelect)) + stashView.ContainsList = true + } + + if v, err := g.SetView("options", appStatusOptionsBoundary-1, height-2, optionsVersionBoundary-1, height, 0); err != nil { + if err.Error() != "unknown view" { + return err + } + v.Frame = false + v.FgColor = theme.OptionsColor + } + + 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.SLocalize("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 + } + _, err := g.SetViewOnBottom("credentials") + if err != nil { + return err + } + credentialsView.Title = gui.Tr.SLocalize("CredentialsUsername") + credentialsView.FgColor = textColor + credentialsView.Editable = true + } + } + + searchViewOffset := hiddenViewOffset + if gui.State.Searching.isSearching { + searchViewOffset = 0 + } + + // this view takes up one character. Its only purpose is to show the slash when searching + searchPrefix := "search: " + if searchPrefixView, err := g.SetView("searchPrefix", appStatusOptionsBoundary-1+searchViewOffset, height-2+searchViewOffset, len(searchPrefix)+searchViewOffset, height+searchViewOffset, 0); err != nil { + if err.Error() != "unknown view" { + return err + } + + searchPrefixView.BgColor = gocui.ColorDefault + searchPrefixView.FgColor = gocui.ColorGreen + searchPrefixView.Frame = false + gui.setViewContent(gui.g, searchPrefixView, searchPrefix) + } + + if searchView, err := g.SetView("search", appStatusOptionsBoundary-1+searchViewOffset+len(searchPrefix), height-2+searchViewOffset, optionsVersionBoundary+searchViewOffset, height+searchViewOffset, 0); 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 := g.SetView("appStatus", -1, height-2, width, height, 0); err != nil { + if err.Error() != "unknown view" { + return err + } + appStatusView.BgColor = gocui.ColorDefault + appStatusView.FgColor = gocui.ColorCyan + appStatusView.Frame = false + if _, err := g.SetViewOnBottom("appStatus"); err != nil { + return err + } + } + + informationView, err := g.SetView("information", optionsVersionBoundary-1, height-2, width, height, 0) + if err != nil { + if err.Error() != "unknown view" { + return err + } + informationView.BgColor = gocui.ColorDefault + informationView.FgColor = gocui.ColorGreen + informationView.Frame = false + gui.renderString(g, "information", information) + + // doing this here because it'll only happen once + if err := gui.onInitialViewsCreation(); err != nil { + return err + } + } + if gui.State.OldInformation != information { + gui.setViewContent(g, informationView, information) + gui.State.OldInformation = information + } + + if gui.g.CurrentView() == nil { + initialView := gui.getFilesView() + if gui.inFilterMode() { + initialView = gui.getCommitsView() + } + if _, err := gui.g.SetCurrentView(initialView.Name()); err != nil { + return err + } + + if err := gui.switchFocus(gui.g, nil, initialView); err != nil { + return err + } + } + + type listViewState struct { + selectedLine int + lineCount int + view *gocui.View + context string + } + + listViews := []listViewState{ + {view: filesView, context: "", selectedLine: gui.State.Panels.Files.SelectedLine, lineCount: len(gui.State.Files)}, + {view: branchesView, context: "local-branches", selectedLine: gui.State.Panels.Branches.SelectedLine, lineCount: len(gui.State.Branches)}, + {view: branchesView, context: "remotes", selectedLine: gui.State.Panels.Remotes.SelectedLine, lineCount: len(gui.State.Remotes)}, + {view: branchesView, context: "remote-branches", selectedLine: gui.State.Panels.RemoteBranches.SelectedLine, lineCount: len(gui.State.Remotes)}, + {view: commitsView, context: "branch-commits", selectedLine: gui.State.Panels.Commits.SelectedLine, lineCount: len(gui.State.Commits)}, + {view: commitsView, context: "reflog-commits", selectedLine: gui.State.Panels.ReflogCommits.SelectedLine, lineCount: len(gui.State.ReflogCommits)}, + {view: stashView, context: "", selectedLine: gui.State.Panels.Stash.SelectedLine, lineCount: len(gui.State.StashEntries)}, + } + + // menu view might not exist so we check to be safe + if menuView, err := gui.g.View("menu"); err == nil { + listViews = append(listViews, listViewState{view: menuView, context: "", selectedLine: gui.State.Panels.Menu.SelectedLine, lineCount: gui.State.MenuItemCount}) + } + for _, listView := range listViews { + // ignore views where the context doesn't match up with the selected line we're trying to focus + if listView.context != "" && (listView.view.Context != listView.context) { + continue + } + // check if the selected line is now out of view and if so refocus it + listView.view.FocusPoint(0, listView.selectedLine) + } + + 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 download humanlog and do tail -f development.log | humanlog + // this will let you see these branches as prettified json + // gui.Log.Info(utils.AsJson(gui.State.Branches[0:4])) + return gui.resizeCurrentPopupPanel(g) +} + +func (gui *Gui) onInitialViewsCreation() error { + gui.changeMainViewsContext("normal") + + gui.getBranchesView().Context = "local-branches" + gui.getCommitsView().Context = "branch-commits" + + return gui.loadNewRepo() +} + +func max(a, b int) int { + if a > b { + return a + } + return b +} |