diff --git a/pkg/app/app.go b/pkg/app/app.go index 7a53b2be5..a624a0d30 100644 --- a/pkg/app/app.go +++ b/pkg/app/app.go @@ -239,7 +239,7 @@ func (app *App) Run() error { os.Exit(0) } - err := app.Gui.RunWithRestarts() + err := app.Gui.RunAndHandleError() return err } diff --git a/pkg/gui/arrangement.go b/pkg/gui/arrangement.go index 3c6cbc5b9..422941243 100644 --- a/pkg/gui/arrangement.go +++ b/pkg/gui/arrangement.go @@ -181,9 +181,12 @@ func (gui *Gui) getWindowDimensions(informationStr string, appStatus string) map // the default behaviour when accordian mode is NOT in effect. If it is in effect // then when it's accessed it will have weight 2, not 1. func (gui *Gui) getDefaultStashWindowBox() *boxlayout.Box { + gui.State.ContextManager.Lock() + defer gui.State.ContextManager.Unlock() + box := &boxlayout.Box{Window: "stash"} stashWindowAccessed := false - for _, context := range gui.State.ContextStack { + for _, context := range gui.State.ContextManager.ContextStack { if context.GetWindowName() == "stash" { stashWindowAccessed = true } @@ -278,9 +281,12 @@ func (gui *Gui) sidePanelChildren(width int, height int) []*boxlayout.Box { func (gui *Gui) currentSideWindowName() string { // there is always one and only one cyclable context in the context stack. We'll look from top to bottom - for idx := range gui.State.ContextStack { - reversedIdx := len(gui.State.ContextStack) - 1 - idx - context := gui.State.ContextStack[reversedIdx] + gui.State.ContextManager.Lock() + defer gui.State.ContextManager.Unlock() + + for idx := range gui.State.ContextManager.ContextStack { + reversedIdx := len(gui.State.ContextManager.ContextStack) - 1 - idx + context := gui.State.ContextManager.ContextStack[reversedIdx] if context.GetKind() == SIDE_CONTEXT { return context.GetWindowName() diff --git a/pkg/gui/context.go b/pkg/gui/context.go index 8b2015711..6d8492411 100644 --- a/pkg/gui/context.go +++ b/pkg/gui/context.go @@ -296,7 +296,18 @@ func (gui *Gui) initialViewContextMap() map[string]Context { } } -func (gui *Gui) viewTabContextMap() map[string][]tabContext { +func (gui *Gui) popupViewNames() []string { + result := []string{} + for _, context := range gui.allContexts() { + if context.GetKind() == PERSISTENT_POPUP || context.GetKind() == TEMPORARY_POPUP { + result = append(result, context.GetViewName()) + } + } + + return result +} + +func (gui *Gui) initialViewTabContextMap() map[string][]tabContext { return map[string][]tabContext{ "branches": { { @@ -343,7 +354,10 @@ func (gui *Gui) viewTabContextMap() map[string][]tabContext { } func (gui *Gui) currentContextKeyIgnoringPopups() string { - stack := gui.State.ContextStack + gui.State.ContextManager.Lock() + defer gui.State.ContextManager.Unlock() + + stack := gui.State.ContextManager.ContextStack for i := range stack { reversedIndex := len(stack) - 1 - i @@ -361,11 +375,14 @@ func (gui *Gui) currentContextKeyIgnoringPopups() string { // hitting escape: you want to go that context's parent instead. func (gui *Gui) replaceContext(c Context) error { gui.g.Update(func(*gocui.Gui) error { - if len(gui.State.ContextStack) == 0 { - gui.State.ContextStack = []Context{c} + gui.State.ContextManager.Lock() + defer gui.State.ContextManager.Unlock() + + if len(gui.State.ContextManager.ContextStack) == 0 { + gui.State.ContextManager.ContextStack = []Context{c} } else { // replace the last item with the given item - gui.State.ContextStack = append(gui.State.ContextStack[0:len(gui.State.ContextStack)-1], c) + gui.State.ContextManager.ContextStack = append(gui.State.ContextManager.ContextStack[0:len(gui.State.ContextManager.ContextStack)-1], c) } return gui.activateContext(c) @@ -376,28 +393,37 @@ func (gui *Gui) replaceContext(c Context) error { func (gui *Gui) pushContext(c Context) error { gui.g.Update(func(*gocui.Gui) error { - // push onto stack - // if we are switching to a side context, remove all other contexts in the stack - if c.GetKind() == SIDE_CONTEXT { - for _, stackContext := range gui.State.ContextStack { - if stackContext.GetKey() != c.GetKey() { - if err := gui.deactivateContext(stackContext); err != nil { - return err - } - } - } - gui.State.ContextStack = []Context{c} - } else { - // TODO: think about other exceptional cases - gui.State.ContextStack = append(gui.State.ContextStack, c) - } + gui.State.ContextManager.Lock() + defer gui.State.ContextManager.Unlock() - return gui.activateContext(c) + return gui.pushContextDirect(c) }) return nil } +func (gui *Gui) pushContextDirect(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 { + for _, stackContext := range gui.State.ContextManager.ContextStack { + if stackContext.GetKey() != c.GetKey() { + if err := gui.deactivateContext(stackContext); err != nil { + return err + } + } + } + gui.State.ContextManager.ContextStack = []Context{c} + } else { + // TODO: think about other exceptional cases + gui.State.ContextManager.ContextStack = append(gui.State.ContextManager.ContextStack, c) + } + + return gui.activateContext(c) +} + +// asynchronous code idea: functions return an error via a channel, when done + // pushContextWithView 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 @@ -407,19 +433,20 @@ func (gui *Gui) pushContextWithView(viewName string) error { func (gui *Gui) returnFromContext() error { gui.g.Update(func(*gocui.Gui) error { - // TODO: add mutexes + gui.State.ContextManager.Lock() + defer gui.State.ContextManager.Unlock() - if len(gui.State.ContextStack) == 1 { + if len(gui.State.ContextManager.ContextStack) == 1 { // cannot escape from bottommost context return nil } - n := len(gui.State.ContextStack) - 1 + n := len(gui.State.ContextManager.ContextStack) - 1 - currentContext := gui.State.ContextStack[n] - newContext := gui.State.ContextStack[n-1] + currentContext := gui.State.ContextManager.ContextStack[n] + newContext := gui.State.ContextManager.ContextStack[n-1] - gui.State.ContextStack = gui.State.ContextStack[:n] + gui.State.ContextManager.ContextStack = gui.State.ContextManager.ContextStack[:n] if err := gui.deactivateContext(currentContext); err != nil { return err @@ -529,24 +556,30 @@ func (gui *Gui) activateContext(c Context) error { } // currently unused -// func (gui *Gui) renderContextStack() string { -// result := "" -// for _, context := range gui.State.ContextStack { -// result += context.GetKey() + "\n" -// } -// return result -// } +func (gui *Gui) renderContextStack() string { + result := "" + for _, context := range gui.State.ContextManager.ContextStack { + result += context.GetKey() + "\n" + } + return result +} func (gui *Gui) currentContext() Context { - if len(gui.State.ContextStack) == 0 { + gui.State.ContextManager.Lock() + defer gui.State.ContextManager.Unlock() + + if len(gui.State.ContextManager.ContextStack) == 0 { return gui.defaultSideContext() } - return gui.State.ContextStack[len(gui.State.ContextStack)-1] + return gui.State.ContextManager.ContextStack[len(gui.State.ContextManager.ContextStack)-1] } func (gui *Gui) currentSideContext() *ListContext { - stack := gui.State.ContextStack + gui.State.ContextManager.Lock() + defer gui.State.ContextManager.Unlock() + + stack := gui.State.ContextManager.ContextStack // on startup the stack can be empty so we'll return an empty string in that case if len(stack) == 0 { diff --git a/pkg/gui/errors.go b/pkg/gui/errors.go index 651b964c2..c275fcdcc 100644 --- a/pkg/gui/errors.go +++ b/pkg/gui/errors.go @@ -5,9 +5,7 @@ import "github.com/go-errors/errors" // SentinelErrors are the errors that have special meaning and need to be checked // by calling functions. The less of these, the better type SentinelErrors struct { - ErrNoFiles error - ErrSwitchRepo error - ErrRestart error + ErrNoFiles error } const UNKNOWN_VIEW_ERROR_MSG = "unknown view" @@ -24,14 +22,12 @@ const UNKNOWN_VIEW_ERROR_MSG = "unknown view" // localising things in the code. func (gui *Gui) GenerateSentinelErrors() { gui.Errors = SentinelErrors{ - ErrNoFiles: errors.New(gui.Tr.NoChangedFiles), - ErrSwitchRepo: errors.New("switching repo"), + ErrNoFiles: errors.New(gui.Tr.NoChangedFiles), } } func (gui *Gui) sentinelErrorsArr() []error { return []error{ gui.Errors.ErrNoFiles, - gui.Errors.ErrSwitchRepo, } } diff --git a/pkg/gui/filetree/commit_file_node.go b/pkg/gui/filetree/commit_file_node.go index b6fd5ea55..91aba14c0 100644 --- a/pkg/gui/filetree/commit_file_node.go +++ b/pkg/gui/filetree/commit_file_node.go @@ -107,6 +107,10 @@ func (n *CommitFileNode) Flatten(collapsedPaths map[string]bool) []*CommitFileNo } func (node *CommitFileNode) GetNodeAtIndex(index int, collapsedPaths map[string]bool) *CommitFileNode { + if node == nil { + return nil + } + return getNodeAtIndex(node, index, collapsedPaths).(*CommitFileNode) } diff --git a/pkg/gui/filetree/file_node.go b/pkg/gui/filetree/file_node.go index dfda68f29..649578fb0 100644 --- a/pkg/gui/filetree/file_node.go +++ b/pkg/gui/filetree/file_node.go @@ -93,6 +93,10 @@ func (n *FileNode) Flatten(collapsedPaths map[string]bool) []*FileNode { } func (node *FileNode) GetNodeAtIndex(index int, collapsedPaths map[string]bool) *FileNode { + if node == nil { + return nil + } + return getNodeAtIndex(node, index, collapsedPaths).(*FileNode) } diff --git a/pkg/gui/gui.go b/pkg/gui/gui.go index a9343d115..0a7770a01 100644 --- a/pkg/gui/gui.go +++ b/pkg/gui/gui.go @@ -48,6 +48,18 @@ const StartupPopupVersion = 3 // OverlappingEdges determines if panel edges overlap var OverlappingEdges = false +type ContextManager struct { + ContextStack []Context + sync.Mutex +} + +func NewContextManager(contexts ContextTree) ContextManager { + return ContextManager{ + ContextStack: []Context{contexts.Files}, + Mutex: sync.Mutex{}, + } +} + // Gui wraps the gocui Gui object which handles rendering and events type Gui struct { g *gocui.Gui @@ -83,6 +95,10 @@ type Gui struct { // findSuggestions will take a string that the user has typed into a prompt // and return a slice of suggestions which match that string. findSuggestions func(string) []*types.Suggestion + + // when you enter into a submodule we'll append the superproject's path to this array + // so that you can return to the superproject + RepoPathStack []string } type RecordedEvent struct { @@ -298,7 +314,7 @@ type guiState struct { Modes Modes - ContextStack []Context + ContextManager ContextManager ViewContextMap map[string]Context // WindowViewNameMap is a mapping of windows to the current view of that window. @@ -306,35 +322,18 @@ type guiState struct { // side windows we need to know which view to give focus to for a given window WindowViewNameMap map[string]string - // when you enter into a submodule we'll append the superproject's path to this array - // so that you can return to the superproject - RepoPathStack []string + // tells us whether we've set up our views. We only do this once per repo + ViewsSetup bool } -func (gui *Gui) resetState() { - // we carry over the filter path and diff state - prevFiltering := filtering.NewFiltering() - prevDiff := Diffing{} - prevCherryPicking := CherryPicking{ - CherryPickedCommits: make([]*models.Commit, 0), - ContextKey: "", - } - prevRepoPathStack := []string{} - if gui.State != nil { - prevFiltering = gui.State.Modes.Filtering - prevDiff = gui.State.Modes.Diffing - prevCherryPicking = gui.State.Modes.CherryPicking - prevRepoPathStack = gui.State.RepoPathStack - } - - modes := Modes{ - Filtering: prevFiltering, - CherryPicking: prevCherryPicking, - Diffing: prevDiff, - } - +func (gui *Gui) resetState(filterPath string) { showTree := gui.Config.GetUserConfig().Gui.ShowFileTree + screenMode := SCREEN_NORMAL + if filterPath != "" { + screenMode = SCREEN_HALF + } + gui.State = &guiState{ FileManager: filetree.NewFileManager(make([]*models.File, 0), gui.Log, showTree), CommitFileManager: filetree.NewCommitFileManager(make([]*models.CommitFile, 0), gui.Log, showTree), @@ -365,18 +364,22 @@ func (gui *Gui) resetState() { ConflictsMutex: sync.Mutex{}, }, }, - SideView: nil, - Ptmx: nil, - Modes: modes, + SideView: nil, + Ptmx: nil, + Modes: Modes{ + Filtering: filtering.NewFiltering(), + CherryPicking: CherryPicking{ + CherryPickedCommits: make([]*models.Commit, 0), + ContextKey: "", + }, + Diffing: Diffing{}, + }, ViewContextMap: gui.initialViewContextMap(), - RepoPathStack: prevRepoPathStack, + ScreenMode: screenMode, + ContextManager: NewContextManager(gui.Contexts), } - if gui.State.Modes.Filtering.Active() { - gui.State.ScreenMode = SCREEN_HALF - } else { - gui.State.ScreenMode = SCREEN_NORMAL - } + gui.ViewTabContextMap = gui.initialViewTabContextMap() } // for now the split view will always be on @@ -393,12 +396,11 @@ func NewGui(log *logrus.Entry, gitCommand *commands.GitCommand, oSCommand *oscom viewBufferManagerMap: map[string]*tasks.ViewBufferManager{}, showRecentRepos: showRecentRepos, RecordedEvents: []RecordedEvent{}, + RepoPathStack: []string{}, } - gui.resetState() - gui.State.Modes.Filtering.SetPath(filterPath) gui.Contexts = gui.contextTree() - gui.ViewTabContextMap = gui.viewTabContextMap() + gui.resetState(filterPath) gui.watchFilesForChanges() @@ -409,8 +411,6 @@ func NewGui(log *logrus.Entry, gitCommand *commands.GitCommand, oSCommand *oscom // Run setup the gui with keybindings and start the mainloop func (gui *Gui) Run() error { - gui.resetState() - recordEvents := recordingEvents() g, err := gocui.NewGui(gocui.OutputTrue, OverlappingEdges, recordEvents) @@ -457,6 +457,11 @@ func (gui *Gui) Run() error { go utils.Safe(gui.startBackgroundFetch) } + go func() { + gui.Updater.CheckForNewUpdate(gui.onBackgroundUpdateCheckFinish, false) + gui.waitForIntro.Done() + }() + gui.goEvery(time.Second*time.Duration(userConfig.Refresher.RefreshInterval), gui.stopChan, gui.refreshFilesAndSubmodules) g.SetManager(gocui.ManagerFunc(gui.layout), gocui.ManagerFunc(gui.getFocusLayout())) @@ -467,19 +472,17 @@ func (gui *Gui) Run() error { return err } -// RunWithRestarts loops, instantiating a new gocui.Gui with each iteration -// (i.e. when switching repos or restarting). If it's a random error, we quit -func (gui *Gui) RunWithRestarts() error { +// RunAndHandleError +func (gui *Gui) RunAndHandleError() error { gui.StartTime = time.Now() go utils.Safe(gui.replayRecordedEvents) - for { - gui.stopChan = make(chan struct{}) + gui.stopChan = make(chan struct{}) + return utils.SafeWithError(func() error { if err := gui.Run(); err != nil { for _, manager := range gui.viewBufferManagerMap { manager.Close() } - gui.viewBufferManagerMap = map[string]*tasks.ViewBufferManager{} if !gui.fileWatcher.Disabled { gui.fileWatcher.Watcher.Close() @@ -500,13 +503,14 @@ func (gui *Gui) RunWithRestarts() error { } return nil - case gui.Errors.ErrSwitchRepo: - continue + default: return err } } - } + + return nil + }) } func (gui *Gui) runSubprocessWithSuspense(subprocess *exec.Cmd) error { @@ -551,11 +555,9 @@ func (gui *Gui) runSubprocess(subprocess *exec.Cmd) error { } func (gui *Gui) loadNewRepo() error { - gui.Updater.CheckForNewUpdate(gui.onBackgroundUpdateCheckFinish, false) if err := gui.updateRecentRepoList(); err != nil { return err } - gui.waitForIntro.Done() if err := gui.refreshSidePanels(refreshOptions{mode: ASYNC}); err != nil { return err diff --git a/pkg/gui/keybindings.go b/pkg/gui/keybindings.go index a2f762da6..3ebceaa9e 100644 --- a/pkg/gui/keybindings.go +++ b/pkg/gui/keybindings.go @@ -1706,7 +1706,7 @@ func (gui *Gui) GetInitialKeybindings() []*Binding { bindings = append(bindings, &Binding{ViewName: "", Key: rune(i+1) + '0', Modifier: gocui.ModNone, Handler: gui.goToSideWindow(window)}) } - for viewName := range gui.viewTabContextMap() { + for viewName := range gui.initialViewTabContextMap() { bindings = append(bindings, []*Binding{ { ViewName: viewName, @@ -1741,7 +1741,7 @@ func (gui *Gui) keybindings() error { } } - for viewName := range gui.viewTabContextMap() { + for viewName := range gui.initialViewTabContextMap() { viewName := viewName tabClickCallback := func(tabIndex int) error { return gui.onViewTabClick(viewName, tabIndex) } diff --git a/pkg/gui/layout.go b/pkg/gui/layout.go index 32ad1de85..a60a137af 100644 --- a/pkg/gui/layout.go +++ b/pkg/gui/layout.go @@ -216,11 +216,6 @@ func (gui *Gui) layout(g *gocui.Gui) error { } 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 @@ -271,6 +266,14 @@ func (gui *Gui) layout(g *gocui.Gui) error { gui.State.OldInformation = informationStr } + if !gui.State.ViewsSetup { + if err := gui.onInitialViewsCreation(); err != nil { + return err + } + + gui.State.ViewsSetup = true + } + if gui.g.CurrentView() == nil { initialContext := gui.Contexts.Files if gui.State.Modes.Filtering.Active() { @@ -323,8 +326,13 @@ func (gui *Gui) layout(g *gocui.Gui) error { func (gui *Gui) onInitialViewsCreation() error { gui.setInitialViewContexts() - // add tabs to views + // hide any popup views. This only applies when we've just switched repos + for _, viewName := range gui.popupViewNames() { + _, _ = gui.g.SetViewOnBottom(viewName) + } + gui.g.Mutexes.ViewsMutex.Lock() + // add tabs to views for _, view := range gui.g.Views() { tabs := gui.viewTabNames(view.Name()) if len(tabs) == 0 { diff --git a/pkg/gui/quitting.go b/pkg/gui/quitting.go index bd2f09f66..778db1a61 100644 --- a/pkg/gui/quitting.go +++ b/pkg/gui/quitting.go @@ -49,13 +49,13 @@ func (gui *Gui) handleTopLevelReturn() error { } } - repoPathStack := gui.State.RepoPathStack + repoPathStack := gui.RepoPathStack if len(repoPathStack) > 0 { n := len(repoPathStack) - 1 path := repoPathStack[n] - gui.State.RepoPathStack = repoPathStack[:n] + gui.RepoPathStack = repoPathStack[:n] return gui.dispatchSwitchToRepo(path) } diff --git a/pkg/gui/recent_repos_panel.go b/pkg/gui/recent_repos_panel.go index 691e63d68..c219e1001 100644 --- a/pkg/gui/recent_repos_panel.go +++ b/pkg/gui/recent_repos_panel.go @@ -5,6 +5,7 @@ import ( "path/filepath" "github.com/fatih/color" + "github.com/jesseduffield/gocui" "github.com/jesseduffield/lazygit/pkg/commands" "github.com/jesseduffield/lazygit/pkg/env" "github.com/jesseduffield/lazygit/pkg/utils" @@ -24,6 +25,9 @@ func (gui *Gui) handleCreateRecentReposMenu() error { yellow.Sprint(path), }, onPress: func() error { + // if we were in a submodule, we want to forget about that stack of repos + // so that hitting escape in the new repo does nothing + gui.RepoPathStack = []string{} return gui.dispatchSwitchToRepo(path) }, } @@ -73,8 +77,14 @@ func (gui *Gui) dispatchSwitchToRepo(path string) error { return err } gui.GitCommand = newGitCommand - gui.State.Modes.Filtering.Reset() - return gui.Errors.ErrSwitchRepo + + gui.g.Update(func(*gocui.Gui) error { + gui.resetState("") + + return nil + }) + + return nil } // updateRecentRepoList registers the fact that we opened lazygit in this repo, diff --git a/pkg/gui/submodules_panel.go b/pkg/gui/submodules_panel.go index 8c1bcf76a..65ce2c2d5 100644 --- a/pkg/gui/submodules_panel.go +++ b/pkg/gui/submodules_panel.go @@ -71,7 +71,7 @@ func (gui *Gui) enterSubmodule(submodule *models.SubmoduleConfig) error { if err != nil { return err } - gui.State.RepoPathStack = append(gui.State.RepoPathStack, wd) + gui.RepoPathStack = append(gui.RepoPathStack, wd) return gui.dispatchSwitchToRepo(submodule.Path) } diff --git a/pkg/utils/utils.go b/pkg/utils/utils.go index 339bcb0a9..945586001 100644 --- a/pkg/utils/utils.go +++ b/pkg/utils/utils.go @@ -365,6 +365,10 @@ func ResolveTemplate(templateStr string, object interface{}) (string, error) { // Safe will close tcell if a panic occurs so that we don't end up in a malformed // terminal state func Safe(f func()) { + _ = SafeWithError(func() error { f(); return nil }) +} + +func SafeWithError(f func() error) error { panicking := true defer func() { if panicking && gocui.Screen != nil { @@ -372,7 +376,9 @@ func Safe(f func()) { } }() - f() + err := f() panicking = false + + return err }