1
0
mirror of https://github.com/jesseduffield/lazygit.git synced 2025-06-15 00:15:32 +02:00
This commit is contained in:
Jesse Duffield
2020-08-16 13:58:29 +10:00
parent 0ea0c48631
commit 7f89113245
30 changed files with 615 additions and 461 deletions

View File

@ -244,27 +244,22 @@ func (gui *Gui) sidePanelChildren(width int, height int) []*boxlayout.Box {
} }
func (gui *Gui) currentCyclableViewName() string { func (gui *Gui) currentCyclableViewName() string {
currView := gui.g.CurrentView() // there is always a cyclable context in the context stack. We'll look from top to bottom
currentCyclebleView := gui.State.PreviousView for idx := range gui.State.ContextStack {
if currView != nil { reversedIdx := len(gui.State.ContextStack) - 1 - idx
viewName := currView.Name() context := gui.State.ContextStack[reversedIdx]
usePreviousView := true
for _, view := range gui.getCyclableViews() { if context.GetKind() == SIDE_CONTEXT {
if view == viewName { viewName := context.GetViewName()
currentCyclebleView = viewName
usePreviousView = false // unfortunate result of the fact that these are separate views, have to map explicitly
break if viewName == "commitFiles" {
return "commits"
} }
}
if usePreviousView { return viewName
currentCyclebleView = gui.State.PreviousView
} }
} }
// unfortunate result of the fact that these are separate views, have to map explicitly return "files" // default
if currentCyclebleView == "commitFiles" {
return "commits"
}
return currentCyclebleView
} }

View File

@ -44,8 +44,6 @@ func (gui *Gui) handleCommitFileSelect() error {
return err return err
} }
gui.getCommitFilesView().FocusPoint(0, gui.State.Panels.CommitFiles.SelectedLine)
cmd := gui.OSCommand.ExecutableFromString( cmd := gui.OSCommand.ExecutableFromString(
gui.GitCommand.ShowCommitFileCmdStr(commitFile.Sha, commitFile.Name, false), gui.GitCommand.ShowCommitFileCmdStr(commitFile.Sha, commitFile.Name, false),
) )
@ -57,7 +55,7 @@ func (gui *Gui) handleCommitFileSelect() error {
} }
func (gui *Gui) handleSwitchToCommitsPanel(g *gocui.Gui, v *gocui.View) error { func (gui *Gui) handleSwitchToCommitsPanel(g *gocui.Gui, v *gocui.View) error {
return gui.switchFocus(v, gui.getCommitsView()) return gui.switchContext(gui.Contexts.BranchCommits.Context)
} }
func (gui *Gui) handleCheckoutCommitFile(g *gocui.Gui, v *gocui.View) error { func (gui *Gui) handleCheckoutCommitFile(g *gocui.Gui, v *gocui.View) error {
@ -223,8 +221,8 @@ func (gui *Gui) enterCommitFile(selectedLineIdx int) error {
} }
} }
gui.changeMainViewsContext("patch-building") gui.changeMainViewsContext("patch-building") // TODO: bring into context code
if err := gui.switchFocus(gui.getCommitFilesView(), gui.getMainView()); err != nil { if err := gui.switchContext(gui.Contexts.PatchBuilding.Context); err != nil {
return err return err
} }
return gui.refreshPatchBuildingPanel(selectedLineIdx) return gui.refreshPatchBuildingPanel(selectedLineIdx)
@ -241,7 +239,7 @@ func (gui *Gui) enterCommitFile(selectedLineIdx int) error {
return enterTheFile(selectedLineIdx) return enterTheFile(selectedLineIdx)
}, },
handleClose: func() error { handleClose: func() error {
return gui.switchFocus(nil, gui.getCommitFilesView()) return gui.switchContext(gui.Contexts.BranchCommits.Files.Context)
}, },
}) })
} }

View File

@ -44,19 +44,19 @@ func (gui *Gui) handleCommitConfirm(g *gocui.Gui, v *gocui.View) error {
} }
v.Clear() v.Clear()
_, _ = g.SetViewOnBottom("commitMessage") // TODO: bring into context code
_ = v.SetCursor(0, 0) _ = v.SetCursor(0, 0)
_ = v.SetOrigin(0, 0) _ = v.SetOrigin(0, 0)
_, _ = g.SetViewOnBottom("commitMessage") _ = gui.returnFromContext()
_ = gui.switchFocus(v, gui.getFilesView())
return gui.refreshSidePanels(refreshOptions{mode: ASYNC}) return gui.refreshSidePanels(refreshOptions{mode: ASYNC})
} }
func (gui *Gui) handleCommitClose(g *gocui.Gui, v *gocui.View) error { func (gui *Gui) handleCommitClose(g *gocui.Gui, v *gocui.View) error {
_, _ = g.SetViewOnBottom("commitMessage") _, _ = g.SetViewOnBottom("commitMessage") // TODO: bring into context code
return gui.switchFocus(v, gui.getFilesView()) return gui.returnFromContext()
} }
func (gui *Gui) handleCommitFocused() error { func (gui *Gui) handleCommitMessageFocused() error {
if _, err := gui.g.SetViewOnTop("commitMessage"); err != nil { if _, err := gui.g.SetViewOnTop("commitMessage"); err != nil {
return err return err
} }

View File

@ -54,8 +54,6 @@ func (gui *Gui) handleCommitSelect() error {
return gui.newStringTask("main", gui.Tr.SLocalize("NoCommitsThisBranch")) return gui.newStringTask("main", gui.Tr.SLocalize("NoCommitsThisBranch"))
} }
gui.getCommitsView().FocusPoint(0, gui.State.Panels.Commits.SelectedLine)
if gui.inDiffMode() { if gui.inDiffMode() {
return gui.renderDiff() return gui.renderDiff()
} }
@ -104,7 +102,7 @@ func (gui *Gui) refreshCommits() error {
go func() { go func() {
_ = gui.refreshCommitsWithLimit() _ = gui.refreshCommitsWithLimit()
if gui.g.CurrentView() == gui.getCommitFilesView() || (gui.g.CurrentView() == gui.getMainView() && gui.State.MainContext == "patch-building") { if gui.g.CurrentView() == gui.getCommitFilesView() || (gui.currentContext().GetKey() == gui.Contexts.PatchBuilding.Context.GetKey()) {
_ = gui.refreshCommitFilesView() _ = gui.refreshCommitFilesView()
} }
wg.Done() wg.Done()
@ -528,7 +526,7 @@ func (gui *Gui) handleSwitchToCommitFilesPanel() error {
return err return err
} }
return gui.switchFocus(gui.getCommitsView(), gui.getCommitFilesView()) return gui.switchContext(gui.Contexts.BranchCommits.Files.Context)
} }
func (gui *Gui) hasCommit(commits []*commands.Commit, target string) (int, bool) { func (gui *Gui) hasCommit(commits []*commands.Commit, target string) (int, bool) {

View File

@ -94,15 +94,13 @@ func (gui *Gui) wrappedPromptConfirmationFunction(function func(string) error, r
} }
func (gui *Gui) closeConfirmationPrompt(returnFocusOnClose bool) error { func (gui *Gui) closeConfirmationPrompt(returnFocusOnClose bool) error {
view, err := gui.g.View("confirmation") view := gui.getConfirmationView()
if err != nil { if view == nil {
return nil // if it's already been closed we can just return return nil // if it's already been closed we can just return
} }
view.Editable = false view.Editable = false
if returnFocusOnClose { if err := gui.returnFromContext(); err != nil {
if err := gui.returnFocus(view); err != nil { return err
panic(err)
}
} }
gui.g.DeleteKeybinding("confirmation", gui.getKey("universal.confirm"), gocui.ModNone) gui.g.DeleteKeybinding("confirmation", gui.getKey("universal.confirm"), gocui.ModNone)
@ -164,7 +162,7 @@ func (gui *Gui) prepareConfirmationPanel(currentView *gocui.View, title, prompt
confirmationView.FgColor = theme.GocuiDefaultTextColor confirmationView.FgColor = theme.GocuiDefaultTextColor
} }
gui.g.Update(func(g *gocui.Gui) error { gui.g.Update(func(g *gocui.Gui) error {
return gui.switchFocus(currentView, confirmationView) return gui.switchContext(gui.Contexts.Confirmation.Context)
}) })
return confirmationView, nil return confirmationView, nil
} }

View File

@ -1,7 +1,8 @@
package gui package gui
import ( import (
"github.com/jesseduffield/lazygit/pkg/gui/stack" "github.com/golang-collections/collections/stack"
"github.com/jesseduffield/gocui"
) )
// changeContext is a helper function for when we want to change a 'main' context // changeContext is a helper function for when we want to change a 'main' context
@ -22,6 +23,27 @@ func (gui *Gui) changeMainViewsContext(context string) {
gui.State.MainContext = context gui.State.MainContext = context
} }
type Stack struct {
stack []Context
}
func (s *Stack) Push(contextKey Context) {
s.stack = append(s.stack, contextKey)
}
func (s *Stack) Pop() (Context, bool) {
if len(s.stack) == 0 {
return nil, false
}
n := len(s.stack) - 1
value := s.stack[n]
s.stack = s.stack[:n]
return value, true
}
// the context manager maintains a stack of contexts so that we can easily switch focus back and forth
type contextManager struct { type contextManager struct {
gui *Gui gui *Gui
stack stack.Stack stack stack.Stack
@ -33,54 +55,345 @@ func (c *contextManager) push(contextKey string) {
// push focus, pop focus. // push focus, pop focus.
const (
SIDE_CONTEXT int = iota
MAIN_CONTEXT
TEMPORARY_POPUP
PERSISTENT_POPUP
)
func GetKindWrapper(k int) func() int { return func() int { return k } }
type Context interface { type Context interface {
OnFocus() error HandleFocus() error
HandleFocusLost() error
GetKind() int
GetViewName() string
GetKey() string
} }
type SimpleContext struct { type BasicContext struct {
Self Context OnFocus func() error
OnFocusLost func() error
Kind int
Key string
ViewName string
} }
type RemotesContext struct { func (c BasicContext) GetViewName() string {
Self Context return c.ViewName
Branches Context
} }
type CommitsContext struct { func (c BasicContext) HandleFocus() error {
Self Context return c.OnFocus()
Files Context }
func (c BasicContext) HandleFocusLost() error {
if c.OnFocusLost != nil {
return c.OnFocusLost()
}
return nil
}
func (c BasicContext) GetKind() int {
return c.Kind
}
func (c BasicContext) GetKey() string {
return c.Key
}
type SimpleContextNode struct {
Context Context
}
type RemotesContextNode struct {
Context Context
Branches SimpleContextNode
}
type CommitsContextNode struct {
Context Context
Files SimpleContextNode
} }
type ContextTree struct { type ContextTree struct {
Status SimpleContext Status SimpleContextNode
Files SimpleContext Files SimpleContextNode
Branches SimpleContext Menu SimpleContextNode
Remotes RemotesContext Branches SimpleContextNode
Tags SimpleContext Remotes RemotesContextNode
Commits CommitsContext Tags SimpleContextNode
Stash SimpleContext BranchCommits CommitsContextNode
Staging SimpleContext ReflogCommits SimpleContextNode
PatchBuilding SimpleContext Stash SimpleContextNode
Merging SimpleContext Staging SimpleContextNode
Menu SimpleContext PatchBuilding SimpleContextNode
Credentials SimpleContext Merging SimpleContextNode
Confirmation SimpleContext Credentials SimpleContextNode
CommitMessage SimpleContext Confirmation SimpleContextNode
CommitMessage SimpleContextNode
Search SimpleContextNode
}
func (gui *Gui) switchContext(c Context) error {
// push onto stack
// if we are switching to a side context, remove all other contexts in the stack
if c.GetKind() == SIDE_CONTEXT {
gui.State.ContextStack = []Context{c}
} else {
// TODO: think about other exceptional cases
gui.State.ContextStack = append(gui.State.ContextStack, c)
}
return gui.activateContext(c)
}
// switchContextToView is to be used when you don't know which context you
// want to switch to: you only know the view that you want to switch to. It will
// look up the context currently active for that view and switch to that context
func (gui *Gui) switchContextToView(viewName string) error {
return gui.switchContext(gui.State.ViewContextMap[viewName])
}
func (gui *Gui) renderContextStack() string {
result := ""
for _, context := range gui.State.ContextStack {
result += context.GetViewName() + "\n"
}
return result
}
func (gui *Gui) activateContext(c Context) error {
gui.Log.Warn(gui.renderContextStack())
if _, err := gui.g.SetCurrentView(c.GetViewName()); err != nil {
return err
}
if _, err := gui.g.SetViewOnTop(c.GetViewName()); err != nil {
return err
}
newView := gui.g.CurrentView()
gui.g.Cursor = newView.Editable
// TODO: move this logic to the context
if err := gui.renderPanelOptions(); err != nil {
return err
}
// return gui.newLineFocused(newView)
if err := c.HandleFocus(); err != nil {
return err
}
gui.State.ViewContextMap[c.GetViewName()] = c
return nil
}
func (gui *Gui) returnFromContext() error {
// TODO: add mutexes
if len(gui.State.ContextStack) == 1 {
// cannot escape from bottommost context
return nil
}
n := len(gui.State.ContextStack) - 1
currentContext := gui.State.ContextStack[n]
newContext := gui.State.ContextStack[n-1]
gui.State.ContextStack = gui.State.ContextStack[:n]
if err := currentContext.HandleFocusLost(); err != nil {
return err
}
return gui.activateContext(newContext)
}
func (gui *Gui) currentContext() Context {
return gui.State.ContextStack[len(gui.State.ContextStack)-1]
} }
func (gui *Gui) createContextTree() { func (gui *Gui) createContextTree() {
gui.State.Contexts = ContextTree{ gui.Contexts = ContextTree{
Files: SimpleContext{ Status: SimpleContextNode{
Self: gui.filesListView(), Context: BasicContext{
OnFocus: gui.handleStatusSelect,
Kind: SIDE_CONTEXT,
ViewName: "status",
},
}, },
Files: SimpleContextNode{
Context: gui.filesListView(),
},
Menu: SimpleContextNode{
Context: gui.menuListView(),
},
Remotes: RemotesContextNode{
Context: gui.remotesListView(),
Branches: SimpleContextNode{
Context: gui.remoteBranchesListView(),
},
},
BranchCommits: CommitsContextNode{
Context: gui.branchCommitsListView(),
Files: SimpleContextNode{
Context: gui.commitFilesListView(),
},
},
ReflogCommits: SimpleContextNode{
Context: gui.reflogCommitsListView(),
},
Branches: SimpleContextNode{
Context: gui.branchesListView(),
},
Tags: SimpleContextNode{
Context: gui.tagsListView(),
},
Stash: SimpleContextNode{
Context: gui.stashListView(),
},
Staging: SimpleContextNode{
Context: BasicContext{
// TODO: think about different situations where this arises
OnFocus: func() error {
return gui.refreshStagingPanel(false, -1)
},
Kind: MAIN_CONTEXT,
ViewName: "main",
Key: "staging",
},
},
PatchBuilding: SimpleContextNode{
Context: BasicContext{
// TODO: think about different situations where this arises
OnFocus: func() error {
return gui.refreshPatchBuildingPanel(-1)
},
Kind: MAIN_CONTEXT,
ViewName: "main",
Key: "patch-building",
},
},
Merging: SimpleContextNode{
Context: BasicContext{
// TODO: think about different situations where this arises
OnFocus: func() error {
return gui.refreshMergePanel()
},
Kind: MAIN_CONTEXT,
ViewName: "main",
Key: "merging",
},
},
Credentials: SimpleContextNode{
Context: BasicContext{
OnFocus: func() error { return gui.handleCredentialsViewFocused() },
Kind: PERSISTENT_POPUP,
ViewName: "credentials",
Key: "credentials",
},
},
Confirmation: SimpleContextNode{
Context: BasicContext{
OnFocus: func() error { return nil },
Kind: TEMPORARY_POPUP,
ViewName: "confirmation",
Key: "confirmation",
},
},
CommitMessage: SimpleContextNode{
Context: BasicContext{
OnFocus: func() error { return gui.handleCommitMessageFocused() },
Kind: PERSISTENT_POPUP,
ViewName: "commitMessage",
Key: "commit-message",
},
},
Search: SimpleContextNode{
Context: BasicContext{
OnFocus: func() error { return nil },
Kind: PERSISTENT_POPUP,
ViewName: "search",
Key: "search",
},
},
}
gui.State.ViewContextMap = map[string]Context{
"status": gui.Contexts.Status.Context,
"files": gui.Contexts.Files.Context,
"branches": gui.Contexts.Branches.Context,
"commits": gui.Contexts.BranchCommits.Context,
"stash": gui.Contexts.Stash.Context,
"menu": gui.Contexts.Menu.Context,
"confirmation": gui.Contexts.Confirmation.Context,
"credentials": gui.Contexts.Credentials.Context,
"commitMessage": gui.Contexts.CommitMessage.Context,
"main": gui.Contexts.Staging.Context,
} }
} }
// func (c *contextManager) pop() (string, bool) { // getFocusLayout returns a manager function for when view gain and lose focus
// value, ok := c.stack.Pop() 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.onViewFocusLost(previousView, newView); err != nil {
return err
}
// if !ok { if err := gui.onViewFocus(newView); err != nil {
// // bottom of the stack, let's go to the default context: the files context return err
// c.gui.switchFocus(nil, newView) }
// } previousView = newView
// } }
return nil
}
}
func (gui *Gui) onFocusChange() error {
currentView := gui.g.CurrentView()
for _, view := range gui.g.Views() {
view.Highlight = view.Name() != "main" && view == currentView
}
return nil
}
func (gui *Gui) onViewFocusLost(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
}
}
if v.Name() == "main" {
// if we have lost focus to a first-class panel, we need to do some cleanup
gui.changeMainViewsContext("normal")
}
gui.Log.Info(v.Name() + " focus lost")
return nil
}
func (gui *Gui) onViewFocus(newView *gocui.View) error {
gui.setViewAsActiveForWindow(newView.Name())
return nil
}

View File

@ -20,10 +20,11 @@ func (gui *Gui) promptUserForCredential(passOrUname string) string {
credentialsView.Title = gui.Tr.SLocalize("CredentialsPassword") credentialsView.Title = gui.Tr.SLocalize("CredentialsPassword")
credentialsView.Mask = '*' credentialsView.Mask = '*'
} }
err := gui.switchFocus(gui.g.CurrentView(), credentialsView)
if err != nil { if err := gui.switchContext(gui.Contexts.Credentials.Context); err != nil {
return err return err
} }
gui.RenderCommitLength() gui.RenderCommitLength()
return nil return nil
}) })
@ -38,15 +39,11 @@ func (gui *Gui) handleSubmitCredential(g *gocui.Gui, v *gocui.View) error {
gui.credentials <- message gui.credentials <- message
v.Clear() v.Clear()
_ = v.SetCursor(0, 0) _ = v.SetCursor(0, 0)
_, _ = g.SetViewOnBottom("credentials") _, _ = g.SetViewOnBottom("credentials") // TODO: move to context code
nextView, err := gui.g.View("confirmation") if err := gui.returnFromContext(); err != nil {
if err != nil {
nextView = gui.getFilesView()
}
err = gui.switchFocus(nil, nextView)
if err != nil {
return err return err
} }
return gui.refreshSidePanels(refreshOptions{}) return gui.refreshSidePanels(refreshOptions{})
} }
@ -57,7 +54,7 @@ func (gui *Gui) handleCloseCredentialsView(g *gocui.Gui, v *gocui.View) error {
} }
gui.credentials <- "" gui.credentials <- ""
return gui.switchFocus(nil, gui.getFilesView()) return gui.returnFromContext()
} }
func (gui *Gui) handleCredentialsViewFocused() error { func (gui *Gui) handleCredentialsViewFocused() error {

View File

@ -171,11 +171,10 @@ func (gui *Gui) enterFile(forceSecondaryFocused bool, selectedLineIdx int) error
if file.HasMergeConflicts { if file.HasMergeConflicts {
return gui.createErrorPanel(gui.Tr.SLocalize("FileStagingRequirements")) return gui.createErrorPanel(gui.Tr.SLocalize("FileStagingRequirements"))
} }
gui.changeMainViewsContext("staging") gui.changeMainViewsContext("staging") // TODO: move into context code
if err := gui.switchFocus(gui.getFilesView(), gui.getMainView()); err != nil { gui.switchContext(gui.Contexts.Staging.Context)
return err
} return gui.refreshStagingPanel(forceSecondaryFocused, selectedLineIdx) // TODO: check if this is broken, try moving into context code
return gui.refreshStagingPanel(forceSecondaryFocused, selectedLineIdx)
} }
func (gui *Gui) handleFilePress() error { func (gui *Gui) handleFilePress() error {
@ -310,11 +309,7 @@ func (gui *Gui) handleCommitPress() error {
} }
gui.g.Update(func(g *gocui.Gui) error { gui.g.Update(func(g *gocui.Gui) error {
if _, err := g.SetViewOnTop("commitMessage"); err != nil { if err := gui.switchContext(gui.Contexts.CommitMessage.Context); err != nil {
return err
}
if err := gui.switchFocus(gui.getFilesView(), commitMessageView); err != nil {
return err return err
} }
@ -596,11 +591,8 @@ func (gui *Gui) handleSwitchToMerge() error {
if !file.HasInlineMergeConflicts { if !file.HasInlineMergeConflicts {
return gui.createErrorPanel(gui.Tr.SLocalize("FileNoMergeCons")) return gui.createErrorPanel(gui.Tr.SLocalize("FileNoMergeCons"))
} }
gui.changeMainViewsContext("merging") gui.changeMainViewsContext("merging") // TODO: move into context code
if err := gui.switchFocus(gui.g.CurrentView(), gui.getMainView()); err != nil { return gui.switchContext(gui.Contexts.Merging.Context)
return err
}
return gui.refreshMergePanel()
} }
func (gui *Gui) openFile(filename string) error { func (gui *Gui) openFile(filename string) error {

View File

@ -97,6 +97,7 @@ type Gui struct {
// when lazygit is opened outside a git directory we want to open to the most // when lazygit is opened outside a git directory we want to open to the most
// recent repo with the recent repos popup showing // recent repo with the recent repos popup showing
showRecentRepos bool showRecentRepos bool
Contexts ContextTree
} }
// for now the staging panel state, unlike the other panel states, is going to be // for now the staging panel state, unlike the other panel states, is going to be
@ -237,7 +238,15 @@ type guiState struct {
FilterPath string // the filename that gets passed to git log FilterPath string // the filename that gets passed to git log
Diff DiffState Diff DiffState
Contexts ContextTree ContextStack []Context
ViewContextMap map[string]Context
// WindowViewNameMap is a mapping of windows to the current view of that window.
// Currently the only case where the distinction between a window and a view
// matters is with the commits view and the commitFiles view which both appear
// in the same place (and thus constitute the 'commits' window).
// If a window contains only one view, it shares the same name as the view.
WindowViewNameMap map[string]string
} }
func (gui *Gui) resetState() { func (gui *Gui) resetState() {

View File

@ -1393,17 +1393,17 @@ func (gui *Gui) GetInitialKeybindings() []*Binding {
for _, viewName := range []string{"status", "branches", "files", "commits", "commitFiles", "stash", "menu"} { for _, viewName := range []string{"status", "branches", "files", "commits", "commitFiles", "stash", "menu"} {
bindings = append(bindings, []*Binding{ bindings = append(bindings, []*Binding{
{ViewName: viewName, Key: gui.getKey("universal.togglePanel"), Modifier: gocui.ModNone, Handler: gui.nextView}, {ViewName: viewName, Key: gui.getKey("universal.togglePanel"), Modifier: gocui.ModNone, Handler: gui.wrappedHandler(gui.nextSideWindow)},
{ViewName: viewName, Key: gui.getKey("universal.prevBlock"), Modifier: gocui.ModNone, Handler: gui.previousView}, {ViewName: viewName, Key: gui.getKey("universal.prevBlock"), Modifier: gocui.ModNone, Handler: gui.wrappedHandler(gui.previousSideWindow)},
{ViewName: viewName, Key: gui.getKey("universal.nextBlock"), Modifier: gocui.ModNone, Handler: gui.nextView}, {ViewName: viewName, Key: gui.getKey("universal.nextBlock"), Modifier: gocui.ModNone, Handler: gui.wrappedHandler(gui.nextSideWindow)},
{ViewName: viewName, Key: gui.getKey("universal.prevBlock-alt"), Modifier: gocui.ModNone, Handler: gui.previousView}, {ViewName: viewName, Key: gui.getKey("universal.prevBlock-alt"), Modifier: gocui.ModNone, Handler: gui.wrappedHandler(gui.previousSideWindow)},
{ViewName: viewName, Key: gui.getKey("universal.nextBlock-alt"), Modifier: gocui.ModNone, Handler: gui.nextView}, {ViewName: viewName, Key: gui.getKey("universal.nextBlock-alt"), Modifier: gocui.ModNone, Handler: gui.wrappedHandler(gui.nextSideWindow)},
}...) }...)
} }
// Appends keybindings to jump to a particular sideView using numbers // Appends keybindings to jump to a particular sideView using numbers
for i, viewName := range []string{"status", "files", "branches", "commits", "stash"} { for i, window := range []string{"status", "files", "branches", "commits", "stash"} {
bindings = append(bindings, &Binding{ViewName: "", Key: rune(i+1) + '0', Modifier: gocui.ModNone, Handler: gui.goToSideView(viewName)}) bindings = append(bindings, &Binding{ViewName: "", Key: rune(i+1) + '0', Modifier: gocui.ModNone, Handler: gui.goToSideWindow(window)})
} }
for _, listView := range gui.getListViews() { for _, listView := range gui.getListViews() {

View File

@ -12,68 +12,6 @@ import (
const SEARCH_PREFIX = "search: " const SEARCH_PREFIX = "search: "
const INFO_SECTION_PADDING = " " const INFO_SECTION_PADDING = " "
// 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) informationStr() string { func (gui *Gui) informationStr() string {
if gui.inDiffMode() { if gui.inDiffMode() {
return utils.ColoredString(fmt.Sprintf("%s %s %s", gui.Tr.SLocalize("showingGitDiff"), "git diff "+gui.diffStr(), utils.ColoredString(gui.Tr.SLocalize("(reset)"), color.Underline)), color.FgMagenta) return utils.ColoredString(fmt.Sprintf("%s %s %s", gui.Tr.SLocalize("showingGitDiff"), "git diff "+gui.diffStr(), utils.ColoredString(gui.Tr.SLocalize("(reset)"), color.Underline)), color.FgMagenta)
@ -329,15 +267,12 @@ func (gui *Gui) layout(g *gocui.Gui) error {
} }
if gui.g.CurrentView() == nil { if gui.g.CurrentView() == nil {
initialView := gui.getFilesView() initialContext := gui.Contexts.Files.Context
if gui.inFilterMode() { if gui.inFilterMode() {
initialView = gui.getCommitsView() initialContext = gui.Contexts.BranchCommits.Context
}
if _, err := gui.g.SetCurrentView(initialView.Name()); err != nil {
return err
} }
if err := gui.switchFocus(nil, initialView); err != nil { if err := gui.switchContext(initialContext); err != nil {
return err return err
} }
} }
@ -347,7 +282,7 @@ func (gui *Gui) layout(g *gocui.Gui) error {
lineCount int lineCount int
view *gocui.View view *gocui.View
context string context string
listView *listView listView *ListView
} }
listViewStates := []listViewState{ listViewStates := []listViewState{
@ -397,6 +332,10 @@ func (gui *Gui) layout(g *gocui.Gui) error {
} }
func (gui *Gui) onInitialViewsCreation() error { func (gui *Gui) onInitialViewsCreation() error {
gui.createContextTree()
gui.switchContext(gui.Contexts.Files.Context)
gui.changeMainViewsContext("normal") gui.changeMainViewsContext("normal")
gui.getBranchesView().Context = "local-branches" gui.getBranchesView().Context = "local-branches"

View File

@ -234,7 +234,7 @@ func (gui *Gui) refreshMainView() error {
var includedLineIndices []int var includedLineIndices []int
// I'd prefer not to have knowledge of contexts using this file but I'm not sure // I'd prefer not to have knowledge of contexts using this file but I'm not sure
// how to get around this // how to get around this
if gui.State.MainContext == "patch-building" { if gui.currentContext().GetKey() == gui.Contexts.PatchBuilding.Context.GetKey() {
filename := gui.getSelectedCommitFileName() filename := gui.getSelectedCommitFileName()
includedLineIndices = gui.GitCommand.PatchManager.GetFileIncLineIndices(filename) includedLineIndices = gui.GitCommand.PatchManager.GetFileIncLineIndices(filename)
} }

View File

@ -2,27 +2,54 @@ package gui
import "github.com/jesseduffield/gocui" import "github.com/jesseduffield/gocui"
type listView struct { type ListView struct {
ViewName string ViewName string
Context string Context string
GetItemsLength func() int GetItemsLength func() int
GetSelectedLineIdxPtr func() *int GetSelectedLineIdxPtr func() *int
OnFocus func() error OnFocus func() error
OnFocusLost func() error
OnItemSelect func() error OnItemSelect func() error
OnClickSelectedItem func() error OnClickSelectedItem func() error
Gui *Gui Gui *Gui
RendersToMainView bool RendersToMainView bool
Kind int
Key string
} }
func (lv *listView) handlePrevLine(g *gocui.Gui, v *gocui.View) error { func (lv *ListView) GetKey() string {
return lv.Key
}
func (lv *ListView) GetKind() int {
return lv.Kind
}
func (lv *ListView) GetViewName() string {
return lv.ViewName
}
func (lv *ListView) HandleFocusLost() error {
if lv.OnFocusLost != nil {
return lv.OnFocusLost()
}
return nil
}
func (lv *ListView) HandleFocus() error {
return lv.OnFocus()
}
func (lv *ListView) handlePrevLine(g *gocui.Gui, v *gocui.View) error {
return lv.handleLineChange(-1) return lv.handleLineChange(-1)
} }
func (lv *listView) handleNextLine(g *gocui.Gui, v *gocui.View) error { func (lv *ListView) handleNextLine(g *gocui.Gui, v *gocui.View) error {
return lv.handleLineChange(1) return lv.handleLineChange(1)
} }
func (lv *listView) handleLineChange(change int) error { func (lv *ListView) handleLineChange(change int) error {
if !lv.Gui.isPopupPanel(lv.ViewName) && lv.Gui.popupPanelFocused() { if !lv.Gui.isPopupPanel(lv.ViewName) && lv.Gui.popupPanelFocused() {
return nil return nil
} }
@ -50,7 +77,7 @@ func (lv *listView) handleLineChange(change int) error {
return nil return nil
} }
func (lv *listView) handleNextPage(g *gocui.Gui, v *gocui.View) error { func (lv *ListView) handleNextPage(g *gocui.Gui, v *gocui.View) error {
view, err := lv.Gui.g.View(lv.ViewName) view, err := lv.Gui.g.View(lv.ViewName)
if err != nil { if err != nil {
return nil return nil
@ -63,15 +90,15 @@ func (lv *listView) handleNextPage(g *gocui.Gui, v *gocui.View) error {
return lv.handleLineChange(delta) return lv.handleLineChange(delta)
} }
func (lv *listView) handleGotoTop(g *gocui.Gui, v *gocui.View) error { func (lv *ListView) handleGotoTop(g *gocui.Gui, v *gocui.View) error {
return lv.handleLineChange(-lv.GetItemsLength()) return lv.handleLineChange(-lv.GetItemsLength())
} }
func (lv *listView) handleGotoBottom(g *gocui.Gui, v *gocui.View) error { func (lv *ListView) handleGotoBottom(g *gocui.Gui, v *gocui.View) error {
return lv.handleLineChange(lv.GetItemsLength()) return lv.handleLineChange(lv.GetItemsLength())
} }
func (lv *listView) handlePrevPage(g *gocui.Gui, v *gocui.View) error { func (lv *ListView) handlePrevPage(g *gocui.Gui, v *gocui.View) error {
view, err := lv.Gui.g.View(lv.ViewName) view, err := lv.Gui.g.View(lv.ViewName)
if err != nil { if err != nil {
return nil return nil
@ -84,7 +111,7 @@ func (lv *listView) handlePrevPage(g *gocui.Gui, v *gocui.View) error {
return lv.handleLineChange(-delta) return lv.handleLineChange(-delta)
} }
func (lv *listView) handleClick(g *gocui.Gui, v *gocui.View) error { func (lv *ListView) handleClick(g *gocui.Gui, v *gocui.View) error {
if !lv.Gui.isPopupPanel(lv.ViewName) && lv.Gui.popupPanelFocused() { if !lv.Gui.isPopupPanel(lv.ViewName) && lv.Gui.popupPanelFocused() {
return nil return nil
} }
@ -94,12 +121,12 @@ func (lv *listView) handleClick(g *gocui.Gui, v *gocui.View) error {
newSelectedLineIdx := v.SelectedLineIdx() newSelectedLineIdx := v.SelectedLineIdx()
// we need to focus the view // we need to focus the view
if err := lv.Gui.switchFocus(nil, v); err != nil { if err := lv.Gui.switchContext(lv); err != nil {
return err return err
} }
if newSelectedLineIdx > lv.GetItemsLength()-1 { if newSelectedLineIdx > lv.GetItemsLength()-1 {
return lv.OnFocus() return nil
} }
*selectedLineIdxPtr = newSelectedLineIdx *selectedLineIdxPtr = newSelectedLineIdx
@ -114,7 +141,7 @@ func (lv *listView) handleClick(g *gocui.Gui, v *gocui.View) error {
return nil return nil
} }
func (lv *listView) onSearchSelect(selectedLineIdx int) error { func (lv *ListView) onSearchSelect(selectedLineIdx int) error {
*lv.GetSelectedLineIdxPtr() = selectedLineIdx *lv.GetSelectedLineIdxPtr() = selectedLineIdx
if lv.OnItemSelect != nil { if lv.OnItemSelect != nil {
return lv.OnItemSelect() return lv.OnItemSelect()
@ -122,8 +149,8 @@ func (lv *listView) onSearchSelect(selectedLineIdx int) error {
return nil return nil
} }
func (gui *Gui) menuListView() *listView { func (gui *Gui) menuListView() *ListView {
return &listView{ return &ListView{
ViewName: "menu", ViewName: "menu",
GetItemsLength: func() int { return gui.getMenuView().LinesHeight() }, GetItemsLength: func() int { return gui.getMenuView().LinesHeight() },
GetSelectedLineIdxPtr: func() *int { return &gui.State.Panels.Menu.SelectedLine }, GetSelectedLineIdxPtr: func() *int { return &gui.State.Panels.Menu.SelectedLine },
@ -133,11 +160,13 @@ func (gui *Gui) menuListView() *listView {
OnClickSelectedItem: func() error { return gui.State.Panels.Menu.OnPress(gui.g, nil) }, OnClickSelectedItem: func() error { return gui.State.Panels.Menu.OnPress(gui.g, nil) },
Gui: gui, Gui: gui,
RendersToMainView: false, RendersToMainView: false,
Kind: PERSISTENT_POPUP,
Key: "menu",
} }
} }
func (gui *Gui) filesListView() *listView { func (gui *Gui) filesListView() *ListView {
return &listView{ return &ListView{
ViewName: "files", ViewName: "files",
GetItemsLength: func() int { return len(gui.State.Files) }, GetItemsLength: func() int { return len(gui.State.Files) },
GetSelectedLineIdxPtr: func() *int { return &gui.State.Panels.Files.SelectedLine }, GetSelectedLineIdxPtr: func() *int { return &gui.State.Panels.Files.SelectedLine },
@ -146,11 +175,13 @@ func (gui *Gui) filesListView() *listView {
OnClickSelectedItem: gui.handleFilePress, OnClickSelectedItem: gui.handleFilePress,
Gui: gui, Gui: gui,
RendersToMainView: false, RendersToMainView: false,
Kind: SIDE_CONTEXT,
Key: "files",
} }
} }
func (gui *Gui) branchesListView() *listView { func (gui *Gui) branchesListView() *ListView {
return &listView{ return &ListView{
ViewName: "branches", ViewName: "branches",
Context: "local-branches", Context: "local-branches",
GetItemsLength: func() int { return len(gui.State.Branches) }, GetItemsLength: func() int { return len(gui.State.Branches) },
@ -159,11 +190,13 @@ func (gui *Gui) branchesListView() *listView {
OnItemSelect: gui.handleBranchSelect, OnItemSelect: gui.handleBranchSelect,
Gui: gui, Gui: gui,
RendersToMainView: true, RendersToMainView: true,
Kind: SIDE_CONTEXT,
Key: "menu",
} }
} }
func (gui *Gui) remotesListView() *listView { func (gui *Gui) remotesListView() *ListView {
return &listView{ return &ListView{
ViewName: "branches", ViewName: "branches",
Context: "remotes", Context: "remotes",
GetItemsLength: func() int { return len(gui.State.Remotes) }, GetItemsLength: func() int { return len(gui.State.Remotes) },
@ -173,11 +206,12 @@ func (gui *Gui) remotesListView() *listView {
OnClickSelectedItem: gui.handleRemoteEnter, OnClickSelectedItem: gui.handleRemoteEnter,
Gui: gui, Gui: gui,
RendersToMainView: true, RendersToMainView: true,
Kind: SIDE_CONTEXT,
} }
} }
func (gui *Gui) remoteBranchesListView() *listView { func (gui *Gui) remoteBranchesListView() *ListView {
return &listView{ return &ListView{
ViewName: "branches", ViewName: "branches",
Context: "remote-branches", Context: "remote-branches",
GetItemsLength: func() int { return len(gui.State.RemoteBranches) }, GetItemsLength: func() int { return len(gui.State.RemoteBranches) },
@ -186,11 +220,12 @@ func (gui *Gui) remoteBranchesListView() *listView {
OnItemSelect: gui.handleRemoteBranchSelect, OnItemSelect: gui.handleRemoteBranchSelect,
Gui: gui, Gui: gui,
RendersToMainView: true, RendersToMainView: true,
Kind: SIDE_CONTEXT,
} }
} }
func (gui *Gui) tagsListView() *listView { func (gui *Gui) tagsListView() *ListView {
return &listView{ return &ListView{
ViewName: "branches", ViewName: "branches",
Context: "tags", Context: "tags",
GetItemsLength: func() int { return len(gui.State.Tags) }, GetItemsLength: func() int { return len(gui.State.Tags) },
@ -199,11 +234,12 @@ func (gui *Gui) tagsListView() *listView {
OnItemSelect: gui.handleTagSelect, OnItemSelect: gui.handleTagSelect,
Gui: gui, Gui: gui,
RendersToMainView: true, RendersToMainView: true,
Kind: SIDE_CONTEXT,
} }
} }
func (gui *Gui) branchCommitsListView() *listView { func (gui *Gui) branchCommitsListView() *ListView {
return &listView{ return &ListView{
ViewName: "commits", ViewName: "commits",
Context: "branch-commits", Context: "branch-commits",
GetItemsLength: func() int { return len(gui.State.Commits) }, GetItemsLength: func() int { return len(gui.State.Commits) },
@ -213,11 +249,12 @@ func (gui *Gui) branchCommitsListView() *listView {
OnClickSelectedItem: gui.handleSwitchToCommitFilesPanel, OnClickSelectedItem: gui.handleSwitchToCommitFilesPanel,
Gui: gui, Gui: gui,
RendersToMainView: true, RendersToMainView: true,
Kind: SIDE_CONTEXT,
} }
} }
func (gui *Gui) reflogCommitsListView() *listView { func (gui *Gui) reflogCommitsListView() *ListView {
return &listView{ return &ListView{
ViewName: "commits", ViewName: "commits",
Context: "reflog-commits", Context: "reflog-commits",
GetItemsLength: func() int { return len(gui.State.FilteredReflogCommits) }, GetItemsLength: func() int { return len(gui.State.FilteredReflogCommits) },
@ -226,11 +263,12 @@ func (gui *Gui) reflogCommitsListView() *listView {
OnItemSelect: gui.handleReflogCommitSelect, OnItemSelect: gui.handleReflogCommitSelect,
Gui: gui, Gui: gui,
RendersToMainView: true, RendersToMainView: true,
Kind: SIDE_CONTEXT,
} }
} }
func (gui *Gui) stashListView() *listView { func (gui *Gui) stashListView() *ListView {
return &listView{ return &ListView{
ViewName: "stash", ViewName: "stash",
GetItemsLength: func() int { return len(gui.State.StashEntries) }, GetItemsLength: func() int { return len(gui.State.StashEntries) },
GetSelectedLineIdxPtr: func() *int { return &gui.State.Panels.Stash.SelectedLine }, GetSelectedLineIdxPtr: func() *int { return &gui.State.Panels.Stash.SelectedLine },
@ -238,11 +276,12 @@ func (gui *Gui) stashListView() *listView {
OnItemSelect: gui.handleStashEntrySelect, OnItemSelect: gui.handleStashEntrySelect,
Gui: gui, Gui: gui,
RendersToMainView: true, RendersToMainView: true,
Kind: SIDE_CONTEXT,
} }
} }
func (gui *Gui) commitFilesListView() *listView { func (gui *Gui) commitFilesListView() *ListView {
return &listView{ return &ListView{
ViewName: "commitFiles", ViewName: "commitFiles",
GetItemsLength: func() int { return len(gui.State.CommitFiles) }, GetItemsLength: func() int { return len(gui.State.CommitFiles) },
GetSelectedLineIdxPtr: func() *int { return &gui.State.Panels.CommitFiles.SelectedLine }, GetSelectedLineIdxPtr: func() *int { return &gui.State.Panels.CommitFiles.SelectedLine },
@ -250,11 +289,12 @@ func (gui *Gui) commitFilesListView() *listView {
OnItemSelect: gui.handleCommitFileSelect, OnItemSelect: gui.handleCommitFileSelect,
Gui: gui, Gui: gui,
RendersToMainView: true, RendersToMainView: true,
Kind: SIDE_CONTEXT,
} }
} }
func (gui *Gui) getListViews() []*listView { func (gui *Gui) getListViews() []*ListView {
return []*listView{ return []*ListView{
gui.menuListView(), gui.menuListView(),
gui.filesListView(), gui.filesListView(),
gui.branchesListView(), gui.branchesListView(),

View File

@ -17,7 +17,6 @@ type menuItem struct {
// list panel functions // list panel functions
func (gui *Gui) handleMenuSelect() error { func (gui *Gui) handleMenuSelect() error {
gui.getMenuView().FocusPoint(0, gui.State.Panels.Menu.SelectedLine)
return nil return nil
} }
@ -46,7 +45,7 @@ func (gui *Gui) handleMenuClose(g *gocui.Gui, v *gocui.View) error {
if err != nil { if err != nil {
return err return err
} }
return gui.returnFocus(v) return gui.returnFromContext()
} }
type createMenuOptions struct { type createMenuOptions struct {
@ -84,8 +83,6 @@ func (gui *Gui) createMenu(title string, items []*menuItem, createMenuOptions cr
menuView.ContainsList = true menuView.ContainsList = true
menuView.Clear() menuView.Clear()
menuView.SetOnSelectItem(gui.onSelectItemWrapper(func(selectedLine int) error { menuView.SetOnSelectItem(gui.onSelectItemWrapper(func(selectedLine int) error {
gui.State.Panels.Menu.SelectedLine = selectedLine
menuView.FocusPoint(0, selectedLine)
return nil return nil
})) }))
fmt.Fprint(menuView, list) fmt.Fprint(menuView, list)
@ -103,7 +100,7 @@ func (gui *Gui) createMenu(title string, items []*menuItem, createMenuOptions cr
} }
} }
return gui.returnFocus(menuView) return gui.returnFromContext()
} }
gui.State.Panels.Menu.OnPress = wrappedHandlePress gui.State.Panels.Menu.OnPress = wrappedHandlePress
@ -117,13 +114,7 @@ func (gui *Gui) createMenu(title string, items []*menuItem, createMenuOptions cr
} }
gui.g.Update(func(g *gocui.Gui) error { gui.g.Update(func(g *gocui.Gui) error {
if _, err := gui.g.View("menu"); err == nil { return gui.switchContext(gui.Contexts.Menu.Context)
if _, err := g.SetViewOnTop("menu"); err != nil {
return err
}
}
currentView := gui.g.CurrentView()
return gui.switchFocus(currentView, menuView)
}) })
return nil return nil
} }

View File

@ -308,7 +308,7 @@ func (gui *Gui) handleEscapeMerge() error {
// it's possible this method won't be called from the merging view so we need to // it's possible this method won't be called from the merging view so we need to
// ensure we only 'return' focus if we already have it // ensure we only 'return' focus if we already have it
if gui.g.CurrentView() == gui.getMainView() { if gui.g.CurrentView() == gui.getMainView() {
return gui.switchFocus(gui.getMainView(), gui.getFilesView()) return gui.switchContext(gui.Contexts.Files.Context)
} }
return nil return nil
} }

View File

@ -83,7 +83,7 @@ func (gui *Gui) handleEscapePatchBuildingPanel() error {
gui.State.SplitMainPanel = false gui.State.SplitMainPanel = false
} }
return gui.switchFocus(nil, gui.getCommitFilesView()) return gui.switchContext(gui.Contexts.BranchCommits.Files.Context)
} }
func (gui *Gui) refreshSecondaryPatchPanel() error { func (gui *Gui) refreshSecondaryPatchPanel() error {

View File

@ -35,8 +35,6 @@ func (gui *Gui) handleReflogCommitSelect() error {
if commit == nil { if commit == nil {
return gui.newStringTask("main", "No reflog history") return gui.newStringTask("main", "No reflog history")
} }
gui.getCommitsView().FocusPoint(0, gui.State.Panels.ReflogCommits.SelectedLine)
if gui.inDiffMode() { if gui.inDiffMode() {
return gui.renderDiff() return gui.renderDiff()
} }

View File

@ -37,8 +37,6 @@ func (gui *Gui) handleRemoteBranchSelect() error {
return gui.newStringTask("main", "No branches for this remote") return gui.newStringTask("main", "No branches for this remote")
} }
gui.getBranchesView().FocusPoint(0, gui.State.Panels.RemoteBranches.SelectedLine)
if gui.inDiffMode() { if gui.inDiffMode() {
return gui.renderDiff() return gui.renderDiff()
} }

View File

@ -39,8 +39,6 @@ func (gui *Gui) handleRemoteSelect() error {
if remote == nil { if remote == nil {
return gui.newStringTask("main", "No remotes") return gui.newStringTask("main", "No remotes")
} }
gui.getBranchesView().FocusPoint(0, gui.State.Panels.Remotes.SelectedLine)
if gui.inDiffMode() { if gui.inDiffMode() {
return gui.renderDiff() return gui.renderDiff()
} }

View File

@ -12,7 +12,7 @@ func (gui *Gui) handleOpenSearch(g *gocui.Gui, v *gocui.View) error {
gui.State.Searching.isSearching = true gui.State.Searching.isSearching = true
gui.State.Searching.view = v gui.State.Searching.view = v
gui.renderString("search", "") gui.renderString("search", "")
if err := gui.switchFocus(v, gui.getSearchView()); err != nil { if err := gui.switchContext(gui.Contexts.Search.Context); err != nil {
return err return err
} }
@ -21,7 +21,7 @@ func (gui *Gui) handleOpenSearch(g *gocui.Gui, v *gocui.View) error {
func (gui *Gui) handleSearch(g *gocui.Gui, v *gocui.View) error { func (gui *Gui) handleSearch(g *gocui.Gui, v *gocui.View) error {
gui.State.Searching.searchString = gui.getSearchView().Buffer() gui.State.Searching.searchString = gui.getSearchView().Buffer()
if err := gui.switchFocus(nil, gui.State.Searching.view); err != nil { if err := gui.switchContextToView(gui.State.Searching.view.Name()); err != nil {
return err return err
} }
@ -84,7 +84,7 @@ func (gui *Gui) onSearchEscape() error {
} }
func (gui *Gui) handleSearchEscape(g *gocui.Gui, v *gocui.View) error { func (gui *Gui) handleSearchEscape(g *gocui.Gui, v *gocui.View) error {
if err := gui.switchFocus(nil, gui.State.Searching.view); err != nil { if err := gui.switchContextToView(gui.State.Searching.view.Name()); err != nil {
return err return err
} }

66
pkg/gui/side_window.go Normal file
View File

@ -0,0 +1,66 @@
package gui
import "github.com/jesseduffield/gocui"
func (gui *Gui) nextSideWindow() error {
windows := gui.getCyclableWindows()
currentWindow := gui.currentWindow()
var newWindow string
if currentWindow == "" || currentWindow == windows[len(windows)-1] {
newWindow = windows[0]
} else {
for i := range windows {
if currentWindow == windows[i] {
newWindow = windows[i+1]
break
}
if i == len(windows)-1 {
return nil
}
}
}
if err := gui.resetOrigin(gui.getMainView()); err != nil {
return err
}
viewName := gui.getViewNameForWindow(newWindow)
return gui.switchContext(gui.State.ViewContextMap[viewName])
}
func (gui *Gui) previousSideWindow() error {
windows := gui.getCyclableWindows()
currentWindow := gui.currentWindow()
var newWindow string
if currentWindow == "" || currentWindow == windows[0] {
newWindow = windows[len(windows)-1]
} else {
for i := range windows {
if currentWindow == windows[i] {
newWindow = windows[i-1]
break
}
if i == len(windows)-1 {
return nil
}
}
}
if err := gui.resetOrigin(gui.getMainView()); err != nil {
return err
}
viewName := gui.getViewNameForWindow(newWindow)
return gui.switchContext(gui.State.ViewContextMap[viewName])
}
func (gui *Gui) goToSideWindow(sideViewName string) func(g *gocui.Gui, v *gocui.View) error {
return func(g *gocui.Gui, v *gocui.View) error {
err := gui.closePopupPanels()
if err != nil {
gui.Log.Error(err)
return nil
}
return gui.switchContext(gui.State.ViewContextMap[sideViewName])
}
}

View File

@ -1,21 +0,0 @@
package stack
type Stack struct {
stack []string
}
func (s *Stack) Push(contextKey string) {
s.stack = append(s.stack, contextKey)
}
func (s *Stack) Pop() (string, bool) {
if len(s.stack) == 0 {
return "", false
}
n := len(s.stack) - 1
value := s.stack[n]
s.stack = s.stack[:n]
return value, true
}

View File

@ -12,12 +12,12 @@ func (gui *Gui) refreshStagingPanel(forceSecondaryFocused bool, selectedLineIdx
state := gui.State.Panels.LineByLine state := gui.State.Panels.LineByLine
// We need to force focus here because the confirmation panel for safely staging lines does not return focus automatically. // // We need to force focus here because the confirmation panel for safely staging lines does not return focus automatically.
// This is because if we tell it to return focus it will unconditionally return it to the main panel which may not be what we want // // This is because if we tell it to return focus it will unconditionally return it to the main panel which may not be what we want
// e.g. in the event that there's nothing left to stage. // // e.g. in the event that there's nothing left to stage.
if err := gui.switchFocus(nil, gui.getMainView()); err != nil { // if err := gui.switchContext(nil, gui.getMainView()); err != nil {
return err // return err
} // }
file, err := gui.getSelectedFile() file, err := gui.getSelectedFile()
if err != nil { if err != nil {
@ -96,7 +96,7 @@ func (gui *Gui) handleTogglePanel(g *gocui.Gui, v *gocui.View) error {
func (gui *Gui) handleStagingEscape() error { func (gui *Gui) handleStagingEscape() error {
gui.handleEscapeLineByLinePanel() gui.handleEscapeLineByLinePanel()
return gui.switchFocus(nil, gui.getFilesView()) return gui.switchContext(gui.Contexts.Files.Context)
} }
func (gui *Gui) handleToggleStagedSelection(g *gocui.Gui, v *gocui.View) error { func (gui *Gui) handleToggleStagedSelection(g *gocui.Gui, v *gocui.View) error {

View File

@ -34,8 +34,6 @@ func (gui *Gui) handleStashEntrySelect() error {
if stashEntry == nil { if stashEntry == nil {
return gui.newStringTask("main", gui.Tr.SLocalize("NoStashEntries")) return gui.newStringTask("main", gui.Tr.SLocalize("NoStashEntries"))
} }
gui.getStashView().FocusPoint(0, gui.State.Panels.Stash.SelectedLine)
if gui.inDiffMode() { if gui.inDiffMode() {
return gui.renderDiff() return gui.renderDiff()
} }

View File

@ -30,7 +30,6 @@ func (gui *Gui) handleTagSelect() error {
if tag == nil { if tag == nil {
return gui.newStringTask("main", "No tags") return gui.newStringTask("main", "No tags")
} }
gui.getBranchesView().FocusPoint(0, gui.State.Panels.Tags.SelectedLine)
if gui.inDiffMode() { if gui.inDiffMode() {
return gui.renderDiff() return gui.renderDiff()

View File

@ -6,13 +6,12 @@ import (
"strings" "strings"
"sync" "sync"
"github.com/go-errors/errors"
"github.com/jesseduffield/gocui" "github.com/jesseduffield/gocui"
"github.com/jesseduffield/lazygit/pkg/utils" "github.com/jesseduffield/lazygit/pkg/utils"
"github.com/spkg/bom" "github.com/spkg/bom"
) )
func (gui *Gui) getCyclableViews() []string { func (gui *Gui) getCyclableWindows() []string {
return []string{"status", "files", "branches", "commits", "stash"} return []string{"status", "files", "branches", "commits", "stash"}
} }
@ -140,157 +139,6 @@ func (gui *Gui) refreshSidePanels(options refreshOptions) error {
return nil return nil
} }
func (gui *Gui) nextView(g *gocui.Gui, v *gocui.View) error {
var focusedViewName string
cyclableViews := gui.getCyclableViews()
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)
}
if err := gui.resetOrigin(gui.getMainView()); err != nil {
return err
}
return gui.switchFocus(v, focusedView)
}
func (gui *Gui) previousView(g *gocui.Gui, v *gocui.View) error {
cyclableViews := gui.getCyclableViews()
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)
}
if err := gui.resetOrigin(gui.getMainView()); err != nil {
return err
}
return gui.switchFocus(v, focusedView)
}
func (gui *Gui) newLineFocused(v *gocui.View) error {
switch v.Name() {
case "menu":
return gui.handleMenuSelect()
case "status":
return gui.handleStatusSelect()
case "files":
return gui.focusAndSelectFile()
case "branches":
branchesView := gui.getBranchesView()
switch branchesView.Context {
case "local-branches":
return gui.handleBranchSelect()
case "remotes":
return gui.handleRemoteSelect()
case "remote-branches":
return gui.handleRemoteBranchSelect()
case "tags":
return gui.handleTagSelect()
default:
return errors.New("unknown branches panel context: " + branchesView.Context)
}
case "commits":
return gui.handleCommitSelect()
case "commitFiles":
return gui.handleCommitFileSelect()
case "stash":
return gui.handleStashEntrySelect()
case "confirmation":
return nil
case "commitMessage":
return gui.handleCommitFocused()
case "credentials":
return gui.handleCredentialsViewFocused()
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(v *gocui.View) error {
previousView, err := gui.g.View(gui.State.PreviousView)
if err != nil {
// always fall back to files view if there's no 'previous' view stored
previousView, err = gui.g.View("files")
if err != nil {
gui.Log.Error(err)
}
}
return gui.switchFocus(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(nil, view)
}
}
func (gui *Gui) closePopupPanels() error { func (gui *Gui) closePopupPanels() error {
gui.onNewPopupPanel() gui.onNewPopupPanel()
err := gui.closeConfirmationPrompt(true) err := gui.closeConfirmationPrompt(true)
@ -301,39 +149,6 @@ func (gui *Gui) closePopupPanels() error {
return nil 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(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 := gui.g.SetCurrentView(newView.Name()); err != nil {
return err
}
if _, err := gui.g.SetViewOnTop(newView.Name()); err != nil {
return err
}
gui.g.Cursor = newView.Editable
if err := gui.renderPanelOptions(); err != nil {
return err
}
return gui.newLineFocused(newView)
}
func (gui *Gui) resetOrigin(v *gocui.View) error { func (gui *Gui) resetOrigin(v *gocui.View) error {
_ = v.SetCursor(0, 0) _ = v.SetCursor(0, 0)
return v.SetOrigin(0, 0) return v.SetOrigin(0, 0)
@ -442,6 +257,11 @@ func (gui *Gui) getStatusView() *gocui.View {
return v return v
} }
func (gui *Gui) getConfirmationView() *gocui.View {
v, _ := gui.g.View("confirmation")
return v
}
func (gui *Gui) trimmedContent(v *gocui.View) string { func (gui *Gui) trimmedContent(v *gocui.View) string {
return strings.TrimSpace(v.Buffer()) return strings.TrimSpace(v.Buffer())
} }

37
pkg/gui/window.go Normal file
View File

@ -0,0 +1,37 @@
package gui
// A window refers to a place on the screen which can hold one or more views.
// A view is a box that renders content, and within a window only one view will
// appear at a time. When a view appears within a window, it occupies the whole
// space. Right now most windows are 1:1 with views, except for commitFiles which
// is a view belonging to the 'commits' window, alongside the 'commits' view.
func (gui *Gui) getViewNameForWindow(window string) string {
viewName, ok := gui.State.WindowViewNameMap[window]
if !ok {
return window
}
return viewName
}
func (gui *Gui) getWindowForViewName(viewName string) string {
// should soft-code this
if viewName == "commitFiles" {
return "commits"
}
return viewName
}
func (gui *Gui) setViewAsActiveForWindow(viewName string) {
if gui.State.WindowViewNameMap == nil {
gui.State.WindowViewNameMap = map[string]string{}
}
gui.State.WindowViewNameMap[gui.getWindowForViewName(viewName)] = viewName
}
func (gui *Gui) currentWindow() string {
return gui.getWindowForViewName(gui.currentViewName())
}

View File

@ -397,9 +397,6 @@ func addDutch(i18nObject *i18n.Bundle) error {
}, &i18n.Message{ }, &i18n.Message{
ID: "IssntListOfViews", ID: "IssntListOfViews",
Other: "{{.name}} is niet in de lijst van weergaves", Other: "{{.name}} is niet in de lijst van weergaves",
}, &i18n.Message{
ID: "NoViewMachingNewLineFocusedSwitchStatement",
Other: "Er machen geen weergave met de newLineFocused switch declaratie",
}, &i18n.Message{ }, &i18n.Message{
ID: "newFocusedViewIs", ID: "newFocusedViewIs",
Other: "nieuw gefocussed weergave is {{.newFocusedView}}", Other: "nieuw gefocussed weergave is {{.newFocusedView}}",

View File

@ -405,9 +405,6 @@ func addEnglish(i18nObject *i18n.Bundle) error {
}, &i18n.Message{ }, &i18n.Message{
ID: "IssntListOfViews", ID: "IssntListOfViews",
Other: "{{.name}} is not in the list of views", Other: "{{.name}} is not in the list of views",
}, &i18n.Message{
ID: "NoViewMachingNewLineFocusedSwitchStatement",
Other: "No view matching newLineFocused switch statement",
}, &i18n.Message{ }, &i18n.Message{
ID: "newFocusedViewIs", ID: "newFocusedViewIs",
Other: "new focused view is {{.newFocusedView}}", Other: "new focused view is {{.newFocusedView}}",

View File

@ -320,9 +320,6 @@ func addPolish(i18nObject *i18n.Bundle) error {
}, &i18n.Message{ }, &i18n.Message{
ID: "IssntListOfViews", ID: "IssntListOfViews",
Other: "{{.name}} nie jest na liście widoków", Other: "{{.name}} nie jest na liście widoków",
}, &i18n.Message{
ID: "NoViewMachingNewLineFocusedSwitchStatement",
Other: "Brak widoku pasującego do instrukcji przełączania newLineFocused",
}, &i18n.Message{ }, &i18n.Message{
ID: "newFocusedViewIs", ID: "newFocusedViewIs",
Other: "nowy skupiony widok to {{.newFocusedView}}", Other: "nowy skupiony widok to {{.newFocusedView}}",