summaryrefslogtreecommitdiffstats
path: root/pkg/gui/layout.go
diff options
context:
space:
mode:
authorJesse Duffield <jessedduffield@gmail.com>2020-03-29 10:31:34 +1100
committerJesse Duffield <jessedduffield@gmail.com>2020-03-29 11:37:29 +1100
commita8db672ffbeba92a32ad34f475760c78aa73303e (patch)
treee941074a316d4e7df057552a05f9d873ed6fb47a /pkg/gui/layout.go
parent76b66ae26f428c58b3b0993d8da4bf31d5933dc7 (diff)
refactor gui.go
Diffstat (limited to 'pkg/gui/layout.go')
-rw-r--r--pkg/gui/layout.go518
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
+}