diff --git a/pkg/app/app.go b/pkg/app/app.go index 23030b37d..5d0359d17 100644 --- a/pkg/app/app.go +++ b/pkg/app/app.go @@ -33,7 +33,6 @@ type App struct { Config config.AppConfigurer OSCommand *oscommands.OSCommand Gui *gui.Gui - Updater *updates.Updater // may only need this on the Gui } func Run( @@ -87,8 +86,7 @@ func NewApp(config config.AppConfigurer, common *common.Common) (*App, error) { app.OSCommand = oscommands.NewOSCommand(common, config, oscommands.GetPlatform(), oscommands.NewNullGuiIO(app.Log)) - var err error - app.Updater, err = updates.NewUpdater(common, config, app.OSCommand) + updater, err := updates.NewUpdater(common, config, app.OSCommand) if err != nil { return app, err } @@ -108,7 +106,7 @@ func NewApp(config config.AppConfigurer, common *common.Common) (*App, error) { return app, err } - app.Gui, err = gui.NewGui(common, config, gitVersion, app.Updater, showRecentRepos, dirName) + app.Gui, err = gui.NewGui(common, config, gitVersion, updater, showRecentRepos, dirName) if err != nil { return app, err } diff --git a/pkg/gui/app_status_manager.go b/pkg/gui/app_status_manager.go index 097a19438..02ba7779a 100644 --- a/pkg/gui/app_status_manager.go +++ b/pkg/gui/app_status_manager.go @@ -99,7 +99,8 @@ func (gui *Gui) renderAppStatus() { for range ticker.C { appStatus := gui.statusManager.getStatusString() gui.c.OnUIThread(func() error { - return gui.renderString(gui.Views.AppStatus, appStatus) + gui.c.SetViewContent(gui.Views.AppStatus, appStatus) + return nil }) if appStatus == "" { diff --git a/pkg/gui/arrangement.go b/pkg/gui/arrangement.go index ac6f2d9c1..d656f16d5 100644 --- a/pkg/gui/arrangement.go +++ b/pkg/gui/arrangement.go @@ -13,10 +13,14 @@ import ( const INFO_SECTION_PADDING = " " -func (gui *Gui) getWindowDimensions(informationStr string, appStatus string) map[string]boxlayout.Dimensions { - width, height := gui.g.Size() +type WindowArranger struct { + gui *Gui +} - sideSectionWeight, mainSectionWeight := gui.getMidSectionWeights() +func (self *WindowArranger) getWindowDimensions(informationStr string, appStatus string) map[string]boxlayout.Dimensions { + width, height := self.gui.g.Size() + + sideSectionWeight, mainSectionWeight := self.getMidSectionWeights() sidePanelsDirection := boxlayout.COLUMN portraitMode := width <= 84 && height > 45 @@ -25,13 +29,13 @@ func (gui *Gui) getWindowDimensions(informationStr string, appStatus string) map } mainPanelsDirection := boxlayout.ROW - if gui.splitMainPanelSideBySide() { + if self.splitMainPanelSideBySide() { mainPanelsDirection = boxlayout.COLUMN } - extrasWindowSize := gui.getExtrasWindowSize(height) + extrasWindowSize := self.getExtrasWindowSize(height) - showInfoSection := gui.c.UserConfig.Gui.ShowBottomLine || gui.State.Searching.isSearching || gui.isAnyModeActive() || gui.statusManager.showStatus() + showInfoSection := self.gui.c.UserConfig.Gui.ShowBottomLine || self.gui.State.Searching.isSearching || self.gui.isAnyModeActive() || self.gui.statusManager.showStatus() infoSectionSize := 0 if showInfoSection { infoSectionSize = 1 @@ -47,7 +51,7 @@ func (gui *Gui) getWindowDimensions(informationStr string, appStatus string) map { Direction: boxlayout.ROW, Weight: sideSectionWeight, - ConditionalChildren: gui.sidePanelChildren, + ConditionalChildren: self.sidePanelChildren, }, { Direction: boxlayout.ROW, @@ -55,7 +59,7 @@ func (gui *Gui) getWindowDimensions(informationStr string, appStatus string) map Children: []*boxlayout.Box{ { Direction: mainPanelsDirection, - Children: gui.mainSectionChildren(), + Children: self.mainSectionChildren(), Weight: 1, }, { @@ -69,7 +73,7 @@ func (gui *Gui) getWindowDimensions(informationStr string, appStatus string) map { Direction: boxlayout.COLUMN, Size: infoSectionSize, - Children: gui.infoSectionChildren(informationStr, appStatus), + Children: self.infoSectionChildren(informationStr, appStatus), }, }, } @@ -91,12 +95,12 @@ func MergeMaps[K comparable, V any](maps ...map[K]V) map[K]V { return result } -func (gui *Gui) mainSectionChildren() []*boxlayout.Box { - currentWindow := gui.currentWindow() +func (self *WindowArranger) mainSectionChildren() []*boxlayout.Box { + currentWindow := self.gui.helpers.Window.CurrentWindow() // if we're not in split mode we can just show the one main panel. Likewise if // the main panel is focused and we're in full-screen mode - if !gui.isMainPanelSplit() || (gui.State.ScreenMode == SCREEN_FULL && currentWindow == "main") { + if !self.gui.isMainPanelSplit() || (self.gui.State.ScreenMode == SCREEN_FULL && currentWindow == "main") { return []*boxlayout.Box{ { Window: "main", @@ -117,27 +121,27 @@ func (gui *Gui) mainSectionChildren() []*boxlayout.Box { } } -func (gui *Gui) getMidSectionWeights() (int, int) { - currentWindow := gui.currentWindow() +func (self *WindowArranger) getMidSectionWeights() (int, int) { + currentWindow := self.gui.helpers.Window.CurrentWindow() // we originally specified this as a ratio i.e. .20 would correspond to a weight of 1 against 4 - sidePanelWidthRatio := gui.c.UserConfig.Gui.SidePanelWidth + sidePanelWidthRatio := self.gui.c.UserConfig.Gui.SidePanelWidth // we could make this better by creating ratios like 2:3 rather than always 1:something mainSectionWeight := int(1/sidePanelWidthRatio) - 1 sideSectionWeight := 1 - if gui.splitMainPanelSideBySide() { + if self.splitMainPanelSideBySide() { mainSectionWeight = 5 // need to shrink side panel to make way for main panels if side-by-side } if currentWindow == "main" { - if gui.State.ScreenMode == SCREEN_HALF || gui.State.ScreenMode == SCREEN_FULL { + if self.gui.State.ScreenMode == SCREEN_HALF || self.gui.State.ScreenMode == SCREEN_FULL { sideSectionWeight = 0 } } else { - if gui.State.ScreenMode == SCREEN_HALF { + if self.gui.State.ScreenMode == SCREEN_HALF { mainSectionWeight = 1 - } else if gui.State.ScreenMode == SCREEN_FULL { + } else if self.gui.State.ScreenMode == SCREEN_FULL { mainSectionWeight = 0 } } @@ -145,8 +149,8 @@ func (gui *Gui) getMidSectionWeights() (int, int) { return sideSectionWeight, mainSectionWeight } -func (gui *Gui) infoSectionChildren(informationStr string, appStatus string) []*boxlayout.Box { - if gui.State.Searching.isSearching { +func (self *WindowArranger) infoSectionChildren(informationStr string, appStatus string) []*boxlayout.Box { + if self.gui.State.Searching.isSearching { return []*boxlayout.Box{ { Window: "searchPrefix", @@ -162,7 +166,7 @@ func (gui *Gui) infoSectionChildren(informationStr string, appStatus string) []* appStatusBox := &boxlayout.Box{Window: "appStatus"} optionsBox := &boxlayout.Box{Window: "options"} - if !gui.c.UserConfig.Gui.ShowBottomLine { + if !self.gui.c.UserConfig.Gui.ShowBottomLine { optionsBox.Weight = 0 appStatusBox.Weight = 1 } else { @@ -172,7 +176,7 @@ func (gui *Gui) infoSectionChildren(informationStr string, appStatus string) []* result := []*boxlayout.Box{appStatusBox, optionsBox} - if gui.c.UserConfig.Gui.ShowBottomLine || gui.isAnyModeActive() { + if self.gui.c.UserConfig.Gui.ShowBottomLine || self.gui.isAnyModeActive() { result = append(result, &boxlayout.Box{ Window: "information", // unlike appStatus, informationStr has various colors so we need to decolorise before taking the length @@ -183,13 +187,13 @@ func (gui *Gui) infoSectionChildren(informationStr string, appStatus string) []* return result } -func (gui *Gui) splitMainPanelSideBySide() bool { - if !gui.isMainPanelSplit() { +func (self *WindowArranger) splitMainPanelSideBySide() bool { + if !self.gui.isMainPanelSplit() { return false } - mainPanelSplitMode := gui.c.UserConfig.Gui.MainPanelSplitMode - width, height := gui.g.Size() + mainPanelSplitMode := self.gui.c.UserConfig.Gui.MainPanelSplitMode + width, height := self.gui.g.Size() switch mainPanelSplitMode { case "vertical": @@ -205,18 +209,18 @@ func (gui *Gui) splitMainPanelSideBySide() bool { } } -func (gui *Gui) getExtrasWindowSize(screenHeight int) int { - if !gui.ShowExtrasWindow { +func (self *WindowArranger) getExtrasWindowSize(screenHeight int) int { + if !self.gui.ShowExtrasWindow { return 0 } var baseSize int - if gui.currentStaticContext().GetKey() == context.COMMAND_LOG_CONTEXT_KEY { + if self.gui.c.CurrentStaticContext().GetKey() == context.COMMAND_LOG_CONTEXT_KEY { baseSize = 1000 // my way of saying 'fill the available space' } else if screenHeight < 40 { baseSize = 1 } else { - baseSize = gui.c.UserConfig.Gui.CommandLogSize + baseSize = self.gui.c.UserConfig.Gui.CommandLogSize } frameSize := 2 @@ -227,13 +231,13 @@ func (gui *Gui) getExtrasWindowSize(screenHeight int) int { // too much space, but if you access it it should take up some space. This is // the default behaviour when accordion 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.RLock() - defer gui.State.ContextManager.RUnlock() +func (self *WindowArranger) getDefaultStashWindowBox() *boxlayout.Box { + self.gui.State.ContextMgr.RLock() + defer self.gui.State.ContextMgr.RUnlock() box := &boxlayout.Box{Window: "stash"} stashWindowAccessed := false - for _, context := range gui.State.ContextManager.ContextStack { + for _, context := range self.gui.State.ContextMgr.ContextStack { if context.GetWindowName() == "stash" { stashWindowAccessed = true } @@ -248,10 +252,10 @@ func (gui *Gui) getDefaultStashWindowBox() *boxlayout.Box { return box } -func (gui *Gui) sidePanelChildren(width int, height int) []*boxlayout.Box { - currentWindow := gui.currentSideWindowName() +func (self *WindowArranger) sidePanelChildren(width int, height int) []*boxlayout.Box { + currentWindow := self.currentSideWindowName() - if gui.State.ScreenMode == SCREEN_FULL || gui.State.ScreenMode == SCREEN_HALF { + if self.gui.State.ScreenMode == SCREEN_FULL || self.gui.State.ScreenMode == SCREEN_HALF { fullHeightBox := func(window string) *boxlayout.Box { if window == currentWindow { return &boxlayout.Box{ @@ -274,7 +278,7 @@ func (gui *Gui) sidePanelChildren(width int, height int) []*boxlayout.Box { fullHeightBox("stash"), } } else if height >= 28 { - accordionMode := gui.c.UserConfig.Gui.ExpandFocusedSidePanel + accordionMode := self.gui.c.UserConfig.Gui.ExpandFocusedSidePanel accordionBox := func(defaultBox *boxlayout.Box) *boxlayout.Box { if accordionMode && defaultBox.Window == currentWindow { return &boxlayout.Box{ @@ -294,7 +298,7 @@ func (gui *Gui) sidePanelChildren(width int, height int) []*boxlayout.Box { accordionBox(&boxlayout.Box{Window: "files", Weight: 1}), accordionBox(&boxlayout.Box{Window: "branches", Weight: 1}), accordionBox(&boxlayout.Box{Window: "commits", Weight: 1}), - accordionBox(gui.getDefaultStashWindowBox()), + accordionBox(self.getDefaultStashWindowBox()), } } else { squashedHeight := 1 @@ -326,18 +330,14 @@ func (gui *Gui) sidePanelChildren(width int, height int) []*boxlayout.Box { } } -func (gui *Gui) getCyclableWindows() []string { - return []string{"status", "files", "branches", "commits", "stash"} -} - -func (gui *Gui) currentSideWindowName() string { +func (self *WindowArranger) currentSideWindowName() string { // there is always one and only one cyclable context in the context stack. We'll look from top to bottom - gui.State.ContextManager.RLock() - defer gui.State.ContextManager.RUnlock() + self.gui.State.ContextMgr.RLock() + defer self.gui.State.ContextMgr.RUnlock() - for idx := range gui.State.ContextManager.ContextStack { - reversedIdx := len(gui.State.ContextManager.ContextStack) - 1 - idx - context := gui.State.ContextManager.ContextStack[reversedIdx] + for idx := range self.gui.State.ContextMgr.ContextStack { + reversedIdx := len(self.gui.State.ContextMgr.ContextStack) - 1 - idx + context := self.gui.State.ContextMgr.ContextStack[reversedIdx] if context.GetKind() == types.SIDE_CONTEXT { return context.GetWindowName() diff --git a/pkg/gui/background.go b/pkg/gui/background.go index 6deac3c31..14ee70187 100644 --- a/pkg/gui/background.go +++ b/pkg/gui/background.go @@ -4,18 +4,33 @@ import ( "strings" "time" + "github.com/jesseduffield/lazygit/pkg/commands/git_commands" + "github.com/jesseduffield/lazygit/pkg/gui/types" "github.com/jesseduffield/lazygit/pkg/utils" ) -func (gui *Gui) startBackgroundRoutines() { - userConfig := gui.UserConfig +type BackgroundRoutineMgr struct { + gui *Gui + + // if we've suspended the gui (e.g. because we've switched to a subprocess) + // we typically want to pause some things that are running like background + // file refreshes + pauseBackgroundThreads bool +} + +func (self *BackgroundRoutineMgr) PauseBackgroundThreads(pause bool) { + self.pauseBackgroundThreads = pause +} + +func (self *BackgroundRoutineMgr) startBackgroundRoutines() { + userConfig := self.gui.UserConfig if userConfig.Git.AutoFetch { fetchInterval := userConfig.Refresher.FetchInterval if fetchInterval > 0 { - go utils.Safe(gui.startBackgroundFetch) + go utils.Safe(self.startBackgroundFetch) } else { - gui.c.Log.Errorf( + self.gui.c.Log.Errorf( "Value of config option 'refresher.fetchInterval' (%d) is invalid, disabling auto-fetch", fetchInterval) } @@ -24,42 +39,44 @@ func (gui *Gui) startBackgroundRoutines() { if userConfig.Git.AutoRefresh { refreshInterval := userConfig.Refresher.RefreshInterval if refreshInterval > 0 { - gui.goEvery(time.Second*time.Duration(refreshInterval), gui.stopChan, gui.refreshFilesAndSubmodules) + self.goEvery(time.Second*time.Duration(refreshInterval), self.gui.stopChan, func() error { + return self.gui.c.Refresh(types.RefreshOptions{Scope: []types.RefreshableView{types.FILES}}) + }) } else { - gui.c.Log.Errorf( + self.gui.c.Log.Errorf( "Value of config option 'refresher.refreshInterval' (%d) is invalid, disabling auto-refresh", refreshInterval) } } } -func (gui *Gui) startBackgroundFetch() { - gui.waitForIntro.Wait() - isNew := gui.IsNewRepo - userConfig := gui.UserConfig +func (self *BackgroundRoutineMgr) startBackgroundFetch() { + self.gui.waitForIntro.Wait() + isNew := self.gui.IsNewRepo + userConfig := self.gui.UserConfig if !isNew { time.After(time.Duration(userConfig.Refresher.FetchInterval) * time.Second) } - err := gui.backgroundFetch() + err := self.backgroundFetch() if err != nil && strings.Contains(err.Error(), "exit status 128") && isNew { - _ = gui.c.Alert(gui.c.Tr.NoAutomaticGitFetchTitle, gui.c.Tr.NoAutomaticGitFetchBody) + _ = self.gui.c.Alert(self.gui.c.Tr.NoAutomaticGitFetchTitle, self.gui.c.Tr.NoAutomaticGitFetchBody) } else { - gui.goEvery(time.Second*time.Duration(userConfig.Refresher.FetchInterval), gui.stopChan, func() error { - err := gui.backgroundFetch() - gui.render() + self.goEvery(time.Second*time.Duration(userConfig.Refresher.FetchInterval), self.gui.stopChan, func() error { + err := self.backgroundFetch() + self.gui.c.Render() return err }) } } -func (gui *Gui) goEvery(interval time.Duration, stop chan struct{}, function func() error) { +func (self *BackgroundRoutineMgr) goEvery(interval time.Duration, stop chan struct{}, function func() error) { go utils.Safe(func() { ticker := time.NewTicker(interval) defer ticker.Stop() for { select { case <-ticker.C: - if gui.PauseBackgroundThreads { + if self.pauseBackgroundThreads { continue } _ = function() @@ -69,3 +86,11 @@ func (gui *Gui) goEvery(interval time.Duration, stop chan struct{}, function fun } }) } + +func (self *BackgroundRoutineMgr) backgroundFetch() (err error) { + err = self.gui.git.Sync.Fetch(git_commands.FetchOptions{Background: true}) + + _ = self.gui.c.Refresh(types.RefreshOptions{Scope: []types.RefreshableView{types.BRANCHES, types.COMMITS, types.REMOTES, types.TAGS}, Mode: types.ASYNC}) + + return err +} diff --git a/pkg/gui/branches_panel.go b/pkg/gui/branches_panel.go deleted file mode 100644 index b9a25ea67..000000000 --- a/pkg/gui/branches_panel.go +++ /dev/null @@ -1,23 +0,0 @@ -package gui - -import "github.com/jesseduffield/lazygit/pkg/gui/types" - -func (gui *Gui) branchesRenderToMain() error { - var task types.UpdateTask - branch := gui.State.Contexts.Branches.GetSelected() - if branch == nil { - task = types.NewRenderStringTask(gui.c.Tr.NoBranchesThisRepo) - } else { - cmdObj := gui.git.Branch.GetGraphCmdObj(branch.FullRefName()) - - task = types.NewRunPtyTask(cmdObj.GetCmd()) - } - - return gui.c.RenderToMainViews(types.RefreshMainOpts{ - Pair: gui.c.MainViewPairs().Normal, - Main: &types.ViewUpdateOpts{ - Title: gui.c.Tr.LogTitle, - Task: task, - }, - }) -} diff --git a/pkg/gui/commit_files_panel.go b/pkg/gui/commit_files_panel.go deleted file mode 100644 index d88b95495..000000000 --- a/pkg/gui/commit_files_panel.go +++ /dev/null @@ -1,52 +0,0 @@ -package gui - -import ( - "github.com/jesseduffield/lazygit/pkg/gui/controllers" - "github.com/jesseduffield/lazygit/pkg/gui/types" -) - -func (gui *Gui) commitFilesRenderToMain() error { - node := gui.State.Contexts.CommitFiles.GetSelected() - if node == nil { - return nil - } - - ref := gui.State.Contexts.CommitFiles.GetRef() - to := ref.RefName() - from, reverse := gui.State.Modes.Diffing.GetFromAndReverseArgsForDiff(ref.ParentRefName()) - - cmdObj := gui.git.WorkingTree.ShowFileDiffCmdObj(from, to, reverse, node.GetPath(), false, - gui.IgnoreWhitespaceInDiffView) - task := types.NewRunPtyTask(cmdObj.GetCmd()) - - pair := gui.c.MainViewPairs().Normal - if node.File != nil { - pair = gui.c.MainViewPairs().PatchBuilding - } - - return gui.c.RenderToMainViews(types.RefreshMainOpts{ - Pair: pair, - Main: &types.ViewUpdateOpts{ - Title: gui.Tr.Patch, - Task: task, - }, - Secondary: gui.secondaryPatchPanelUpdateOpts(), - }) -} - -func (gui *Gui) SwitchToCommitFilesContext(opts controllers.SwitchToCommitFilesContextOpts) error { - gui.State.Contexts.CommitFiles.SetSelectedLineIdx(0) - gui.State.Contexts.CommitFiles.SetRef(opts.Ref) - gui.State.Contexts.CommitFiles.SetTitleRef(opts.Ref.Description()) - gui.State.Contexts.CommitFiles.SetCanRebase(opts.CanRebase) - gui.State.Contexts.CommitFiles.SetParentContext(opts.Context) - gui.State.Contexts.CommitFiles.SetWindowName(opts.Context.GetWindowName()) - - if err := gui.c.Refresh(types.RefreshOptions{ - Scope: []types.RefreshableView{types.COMMIT_FILES}, - }); err != nil { - return err - } - - return gui.c.PushContext(gui.State.Contexts.CommitFiles) -} diff --git a/pkg/gui/commit_message_panel.go b/pkg/gui/commit_message_panel.go index 4c8ddae2b..8455e7d02 100644 --- a/pkg/gui/commit_message_panel.go +++ b/pkg/gui/commit_message_panel.go @@ -21,7 +21,8 @@ func (gui *Gui) handleCommitMessageFocused() error { gui.RenderCommitLength() - return gui.renderString(gui.Views.Options, message) + gui.c.SetViewContent(gui.Views.Options, message) + return nil } func (gui *Gui) RenderCommitLength() { diff --git a/pkg/gui/commits_panel.go b/pkg/gui/commits_panel.go deleted file mode 100644 index 2b75be4d0..000000000 --- a/pkg/gui/commits_panel.go +++ /dev/null @@ -1,88 +0,0 @@ -package gui - -import ( - "github.com/fsmiamoto/git-todo-parser/todo" - "github.com/jesseduffield/lazygit/pkg/commands/models" - "github.com/jesseduffield/lazygit/pkg/gui/types" - "github.com/jesseduffield/lazygit/pkg/utils" -) - -// after selecting the 200th commit, we'll load in all the rest -const COMMIT_THRESHOLD = 200 - -// list panel functions - -func (gui *Gui) getSelectedLocalCommit() *models.Commit { - return gui.State.Contexts.LocalCommits.GetSelected() -} - -func (gui *Gui) onCommitFocus() error { - context := gui.State.Contexts.LocalCommits - if context.GetSelectedLineIdx() > COMMIT_THRESHOLD && context.GetLimitCommits() { - context.SetLimitCommits(false) - go utils.Safe(func() { - if err := gui.refreshCommitsWithLimit(); err != nil { - _ = gui.c.Error(err) - } - }) - } - - return nil -} - -func (gui *Gui) branchCommitsRenderToMain() error { - var task types.UpdateTask - commit := gui.State.Contexts.LocalCommits.GetSelected() - if commit == nil { - task = types.NewRenderStringTask(gui.c.Tr.NoCommitsThisBranch) - } else if commit.Action == todo.UpdateRef { - task = types.NewRenderStringTask( - utils.ResolvePlaceholderString( - gui.c.Tr.UpdateRefHere, - map[string]string{ - "ref": commit.Name, - })) - } else { - cmdObj := gui.git.Commit.ShowCmdObj(commit.Sha, gui.State.Modes.Filtering.GetPath(), - gui.IgnoreWhitespaceInDiffView) - task = types.NewRunPtyTask(cmdObj.GetCmd()) - } - - return gui.c.RenderToMainViews(types.RefreshMainOpts{ - Pair: gui.c.MainViewPairs().Normal, - Main: &types.ViewUpdateOpts{ - Title: "Patch", - Task: task, - }, - Secondary: gui.secondaryPatchPanelUpdateOpts(), - }) -} - -func (gui *Gui) secondaryPatchPanelUpdateOpts() *types.ViewUpdateOpts { - if gui.git.Patch.PatchBuilder.Active() { - patch := gui.git.Patch.PatchBuilder.RenderAggregatedPatch(false) - - return &types.ViewUpdateOpts{ - Task: types.NewRenderStringWithoutScrollTask(patch), - Title: gui.Tr.CustomPatch, - } - } - - return nil -} - -func (gui *Gui) refForLog() string { - bisectInfo := gui.git.Bisect.GetInfo() - gui.State.Model.BisectInfo = bisectInfo - - if !bisectInfo.Started() { - return "HEAD" - } - - // need to see if our bisect's current commit is reachable from our 'new' ref. - if bisectInfo.Bisecting() && !gui.git.Bisect.ReachableFromStart(bisectInfo) { - return bisectInfo.GetNewSha() - } - - return bisectInfo.GetStartSha() -} diff --git a/pkg/gui/confirmation_panel.go b/pkg/gui/confirmation_panel.go index c3e50c2a1..ff13c6fbe 100644 --- a/pkg/gui/confirmation_panel.go +++ b/pkg/gui/confirmation_panel.go @@ -195,10 +195,8 @@ func (gui *Gui) createPopupPanel(ctx context.Context, opts types.CreatePopupPane gui.resizeConfirmationPanel() confirmationView.RenderTextArea() } else { - if err := gui.renderString(confirmationView, style.AttrBold.Sprint(opts.Prompt)); err != nil { - cancel() - return err - } + gui.c.ResetViewOrigin(confirmationView) + gui.c.SetViewContent(confirmationView, style.AttrBold.Sprint(opts.Prompt)) } if err := gui.setKeyBindings(cancel, opts); err != nil { @@ -220,7 +218,7 @@ func (gui *Gui) setKeyBindings(cancel context.CancelFunc, opts types.CreatePopup }, ) - _ = gui.renderString(gui.Views.Options, actions) + gui.c.SetViewContent(gui.Views.Options, actions) var onConfirm func() error if opts.HandleConfirmPrompt != nil { onConfirm = gui.wrappedPromptConfirmationFunction(cancel, opts.HandleConfirmPrompt, func() string { return gui.Views.Confirmation.TextArea.GetContent() }) @@ -251,7 +249,7 @@ func (gui *Gui) setKeyBindings(cancel context.CancelFunc, opts types.CreatePopup Key: keybindings.GetKey(keybindingConfig.Universal.TogglePanel), Handler: func() error { if len(gui.State.Suggestions) > 0 { - return gui.replaceContext(gui.State.Contexts.Suggestions) + return gui.c.ReplaceContext(gui.State.Contexts.Suggestions) } return nil }, @@ -269,7 +267,7 @@ func (gui *Gui) setKeyBindings(cancel context.CancelFunc, opts types.CreatePopup { ViewName: "suggestions", Key: keybindings.GetKey(keybindingConfig.Universal.TogglePanel), - Handler: func() error { return gui.replaceContext(gui.State.Contexts.Confirmation) }, + Handler: func() error { return gui.c.ReplaceContext(gui.State.Contexts.Confirmation) }, }, } @@ -313,5 +311,6 @@ func (gui *Gui) handleAskFocused() error { }, ) - return gui.renderString(gui.Views.Options, message) + gui.c.SetViewContent(gui.Views.Options, message) + return nil } diff --git a/pkg/gui/context.go b/pkg/gui/context.go index f097df807..3adafd710 100644 --- a/pkg/gui/context.go +++ b/pkg/gui/context.go @@ -1,12 +1,10 @@ package gui import ( - "sort" - "strings" + "errors" + "sync" - "github.com/jesseduffield/generics/maps" "github.com/jesseduffield/generics/slices" - "github.com/jesseduffield/gocui" "github.com/jesseduffield/lazygit/pkg/gui/context" "github.com/jesseduffield/lazygit/pkg/gui/types" ) @@ -16,46 +14,60 @@ import ( // you in the menu context. When contexts are activated/deactivated certain things need // to happen like showing/hiding views and rendering content. -func (gui *Gui) popupViewNames() []string { - popups := slices.Filter(gui.State.Contexts.Flatten(), func(c types.Context) bool { - return c.GetKind() == types.PERSISTENT_POPUP || c.GetKind() == types.TEMPORARY_POPUP - }) +type ContextMgr struct { + ContextStack []types.Context + sync.RWMutex + gui *Gui +} - return slices.Map(popups, func(c types.Context) string { - return c.GetViewName() - }) +func NewContextMgr(initialContext types.Context, gui *Gui) ContextMgr { + return ContextMgr{ + ContextStack: []types.Context{}, + RWMutex: sync.RWMutex{}, + gui: gui, + } } // use replaceContext when you don't want to return to the original context upon // hitting escape: you want to go that context's parent instead. -func (gui *Gui) replaceContext(c types.Context) error { +func (self *ContextMgr) replaceContext(c types.Context) error { if !c.IsFocusable() { return nil } - gui.State.ContextManager.Lock() + self.Lock() - if len(gui.State.ContextManager.ContextStack) == 0 { - gui.State.ContextManager.ContextStack = []types.Context{c} + if len(self.ContextStack) == 0 { + self.ContextStack = []types.Context{c} } else { // replace the last item with the given item - gui.State.ContextManager.ContextStack = append(gui.State.ContextManager.ContextStack[0:len(gui.State.ContextManager.ContextStack)-1], c) + self.ContextStack = append(self.ContextStack[0:len(self.ContextStack)-1], c) } - defer gui.State.ContextManager.Unlock() + defer self.Unlock() - return gui.activateContext(c, types.OnFocusOpts{}) + return self.activateContext(c, types.OnFocusOpts{}) } -func (gui *Gui) pushContext(c types.Context, opts types.OnFocusOpts) error { +func (self *ContextMgr) pushContext(c types.Context, opts ...types.OnFocusOpts) error { + if len(opts) > 1 { + return errors.New("cannot pass multiple opts to pushContext") + } + + singleOpts := types.OnFocusOpts{} + if len(opts) > 0 { + // using triple dot but you should only ever pass one of these opt structs + singleOpts = opts[0] + } + if !c.IsFocusable() { return nil } - contextsToDeactivate, contextToActivate := gui.pushToContextStack(c) + contextsToDeactivate, contextToActivate := self.pushToContextStack(c) for _, contextToDeactivate := range contextsToDeactivate { - if err := gui.deactivateContext(contextToDeactivate, types.OnFocusLostOpts{NewContextKey: c.GetKey()}); err != nil { + if err := self.deactivateContext(contextToDeactivate, types.OnFocusLostOpts{NewContextKey: c.GetKey()}); err != nil { return err } } @@ -64,43 +76,43 @@ func (gui *Gui) pushContext(c types.Context, opts types.OnFocusOpts) error { return nil } - return gui.activateContext(contextToActivate, opts) + return self.activateContext(contextToActivate, singleOpts) } // Adjusts the context stack based on the context that's being pushed and // returns (contexts to deactivate, context to activate) -func (gui *Gui) pushToContextStack(c types.Context) ([]types.Context, types.Context) { +func (self *ContextMgr) pushToContextStack(c types.Context) ([]types.Context, types.Context) { contextsToDeactivate := []types.Context{} - gui.State.ContextManager.Lock() - defer gui.State.ContextManager.Unlock() + self.Lock() + defer self.Unlock() - if len(gui.State.ContextManager.ContextStack) > 0 && - c == gui.State.ContextManager.ContextStack[len(gui.State.ContextManager.ContextStack)-1] { + if len(self.ContextStack) > 0 && + c == self.ContextStack[len(self.ContextStack)-1] { // Context being pushed is already on top of the stack: nothing to // deactivate or activate return contextsToDeactivate, nil } - if len(gui.State.ContextManager.ContextStack) == 0 { - gui.State.ContextManager.ContextStack = append(gui.State.ContextManager.ContextStack, c) + if len(self.ContextStack) == 0 { + self.ContextStack = append(self.ContextStack, c) } else if c.GetKind() == types.SIDE_CONTEXT { // if we are switching to a side context, remove all other contexts in the stack - contextsToDeactivate = gui.State.ContextManager.ContextStack - gui.State.ContextManager.ContextStack = []types.Context{c} + contextsToDeactivate = self.ContextStack + self.ContextStack = []types.Context{c} } else if c.GetKind() == types.MAIN_CONTEXT { // if we're switching to a main context, remove all other main contexts in the stack contextsToKeep := []types.Context{} - for _, stackContext := range gui.State.ContextManager.ContextStack { + for _, stackContext := range self.ContextStack { if stackContext.GetKind() == types.MAIN_CONTEXT { contextsToDeactivate = append(contextsToDeactivate, stackContext) } else { contextsToKeep = append(contextsToKeep, stackContext) } } - gui.State.ContextManager.ContextStack = append(contextsToKeep, c) + self.ContextStack = append(contextsToKeep, c) } else { - topContext := gui.currentContextWithoutLock() + topContext := self.currentContextWithoutLock() // if we're pushing the same context on, we do nothing. if topContext.GetKey() != c.GetKey() { @@ -113,44 +125,44 @@ func (gui *Gui) pushToContextStack(c types.Context) ([]types.Context, types.Cont (topContext.GetKind() == types.MAIN_CONTEXT && c.GetKind() == types.MAIN_CONTEXT) { contextsToDeactivate = append(contextsToDeactivate, topContext) - _, gui.State.ContextManager.ContextStack = slices.Pop(gui.State.ContextManager.ContextStack) + _, self.ContextStack = slices.Pop(self.ContextStack) } - gui.State.ContextManager.ContextStack = append(gui.State.ContextManager.ContextStack, c) + self.ContextStack = append(self.ContextStack, c) } } return contextsToDeactivate, c } -func (gui *Gui) popContext() error { - gui.State.ContextManager.Lock() +func (self *ContextMgr) popContext() error { + self.Lock() - if len(gui.State.ContextManager.ContextStack) == 1 { + if len(self.ContextStack) == 1 { // cannot escape from bottommost context - gui.State.ContextManager.Unlock() + self.Unlock() return nil } var currentContext types.Context - currentContext, gui.State.ContextManager.ContextStack = slices.Pop(gui.State.ContextManager.ContextStack) + currentContext, self.ContextStack = slices.Pop(self.ContextStack) - newContext := gui.State.ContextManager.ContextStack[len(gui.State.ContextManager.ContextStack)-1] + newContext := self.ContextStack[len(self.ContextStack)-1] - gui.State.ContextManager.Unlock() + self.Unlock() - if err := gui.deactivateContext(currentContext, types.OnFocusLostOpts{NewContextKey: newContext.GetKey()}); err != nil { + if err := self.deactivateContext(currentContext, types.OnFocusLostOpts{NewContextKey: newContext.GetKey()}); err != nil { return err } - return gui.activateContext(newContext, types.OnFocusOpts{}) + return self.activateContext(newContext, types.OnFocusOpts{}) } -func (gui *Gui) deactivateContext(c types.Context, opts types.OnFocusLostOpts) error { - view, _ := gui.g.View(c.GetViewName()) +func (self *ContextMgr) deactivateContext(c types.Context, opts types.OnFocusLostOpts) error { + view, _ := self.gui.c.GocuiGui().View(c.GetViewName()) if view != nil && view.IsSearching() { - if err := gui.onSearchEscape(); err != nil { + if err := self.gui.onSearchEscape(); err != nil { return err } } @@ -169,34 +181,17 @@ func (gui *Gui) deactivateContext(c types.Context, opts types.OnFocusLostOpts) e return nil } -// postRefreshUpdate is to be called on a context after the state that it depends on has been refreshed -// if the context's view is set to another context we do nothing. -// if the context's view is the current view we trigger a focus; re-selecting the current item. -func (gui *Gui) postRefreshUpdate(c types.Context) error { - if err := c.HandleRender(); err != nil { - return err - } - - if gui.currentViewName() == c.GetViewName() { - if err := c.HandleFocus(types.OnFocusOpts{}); err != nil { - return err - } - } - - return nil -} - -func (gui *Gui) activateContext(c types.Context, opts types.OnFocusOpts) error { +func (self *ContextMgr) activateContext(c types.Context, opts types.OnFocusOpts) error { viewName := c.GetViewName() - v, err := gui.g.View(viewName) + v, err := self.gui.c.GocuiGui().View(viewName) if err != nil { return err } - gui.setWindowContext(c) + self.gui.helpers.Window.SetWindowContext(c) - gui.moveToTopOfWindow(c) - if _, err := gui.g.SetCurrentView(viewName); err != nil { + self.gui.helpers.Window.MoveToTopOfWindow(c) + if _, err := self.gui.c.GocuiGui().SetCurrentView(viewName); err != nil { return err } @@ -207,14 +202,9 @@ func (gui *Gui) activateContext(c types.Context, opts types.OnFocusOpts) error { v.Visible = true - gui.g.Cursor = v.Editable + self.gui.c.GocuiGui().Cursor = v.Editable - // render the options available for the current context at the bottom of the screen - optionsMap := c.GetOptionsMap() - if optionsMap == nil { - optionsMap = gui.globalOptionsMap() - } - gui.renderOptionsMap(optionsMap) + self.gui.renderContextOptionsMap(c) if err := c.HandleFocus(opts); err != nil { return err @@ -223,62 +213,31 @@ func (gui *Gui) activateContext(c types.Context, opts types.OnFocusOpts) error { return nil } -func (gui *Gui) optionsMapToString(optionsMap map[string]string) string { - options := maps.MapToSlice(optionsMap, func(key string, description string) string { - return key + ": " + description - }) - sort.Strings(options) - return strings.Join(options, ", ") +func (self *ContextMgr) currentContext() types.Context { + self.RLock() + defer self.RUnlock() + + return self.currentContextWithoutLock() } -func (gui *Gui) renderOptionsMap(optionsMap map[string]string) { - _ = gui.renderString(gui.Views.Options, gui.optionsMapToString(optionsMap)) -} - -// // currently unused -// func (gui *Gui) renderContextStack() string { -// result := "" -// for _, context := range gui.State.ContextManager.ContextStack { -// result += string(context.GetKey()) + "\n" -// } -// return result -// } - -func (gui *Gui) currentContext() types.Context { - gui.State.ContextManager.RLock() - defer gui.State.ContextManager.RUnlock() - - return gui.currentContextWithoutLock() -} - -func (gui *Gui) currentContextWithoutLock() types.Context { - if len(gui.State.ContextManager.ContextStack) == 0 { - return gui.defaultSideContext() +func (self *ContextMgr) currentContextWithoutLock() types.Context { + if len(self.ContextStack) == 0 { + return self.gui.defaultSideContext() } - return gui.State.ContextManager.ContextStack[len(gui.State.ContextManager.ContextStack)-1] + return self.ContextStack[len(self.ContextStack)-1] } -// the status panel is not yet a list context (and may never be), so this method is not -// quite the same as currentSideContext() -func (gui *Gui) currentSideListContext() types.IListContext { - context := gui.currentSideContext() - listContext, ok := context.(types.IListContext) - if !ok { - return nil - } - return listContext -} +// Note that this could return the 'status' context which is not itself a list context. +func (self *ContextMgr) currentSideContext() types.Context { + self.RLock() + defer self.RUnlock() -func (gui *Gui) currentSideContext() types.Context { - gui.State.ContextManager.RLock() - defer gui.State.ContextManager.RUnlock() - - stack := gui.State.ContextManager.ContextStack + stack := self.ContextStack // on startup the stack can be empty so we'll return an empty string in that case if len(stack) == 0 { - return gui.defaultSideContext() + return self.gui.defaultSideContext() } // find the first context in the stack with the type of types.SIDE_CONTEXT @@ -290,22 +249,22 @@ func (gui *Gui) currentSideContext() types.Context { } } - return gui.defaultSideContext() + return self.gui.defaultSideContext() } // static as opposed to popup -func (gui *Gui) currentStaticContext() types.Context { - gui.State.ContextManager.RLock() - defer gui.State.ContextManager.RUnlock() +func (self *ContextMgr) currentStaticContext() types.Context { + self.RLock() + defer self.RUnlock() - return gui.currentStaticContextWithoutLock() + return self.currentStaticContextWithoutLock() } -func (gui *Gui) currentStaticContextWithoutLock() types.Context { - stack := gui.State.ContextManager.ContextStack +func (self *ContextMgr) currentStaticContextWithoutLock() types.Context { + stack := self.ContextStack if len(stack) == 0 { - return gui.defaultSideContext() + return self.gui.defaultSideContext() } // find the first context in the stack without a popup type @@ -317,88 +276,5 @@ func (gui *Gui) currentStaticContextWithoutLock() types.Context { } } - return gui.defaultSideContext() + return self.gui.defaultSideContext() } - -func (gui *Gui) defaultSideContext() types.Context { - if gui.State.Modes.Filtering.Active() { - return gui.State.Contexts.LocalCommits - } else { - return gui.State.Contexts.Files - } -} - -// 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() - // 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); err != nil { - return err - } - - previousView = newView - } - return nil - } -} - -func (gui *Gui) onViewFocusLost(oldView *gocui.View) error { - if oldView == nil { - return nil - } - - oldView.Highlight = false - - _ = oldView.SetOriginX(0) - - return nil -} - -func (gui *Gui) TransientContexts() []types.Context { - return slices.Filter(gui.State.Contexts.Flatten(), func(context types.Context) bool { - return context.IsTransient() - }) -} - -func (gui *Gui) rerenderView(view *gocui.View) error { - context, ok := gui.contextForView(view.Name()) - if !ok { - gui.Log.Errorf("no context found for view %s", view.Name()) - return nil - } - - return context.HandleRender() -} - -func (gui *Gui) getSideContextSelectedItemId() string { - currentSideContext := gui.currentSideListContext() - if currentSideContext == nil { - return "" - } - - return currentSideContext.GetSelectedItemId() -} - -// currently unused -// func (gui *Gui) getCurrentSideView() *gocui.View { -// currentSideContext := gui.currentSideContext() -// if currentSideContext == nil { -// return nil -// } - -// view, _ := gui.g.View(currentSideContext.GetViewName()) - -// return view -// } - -// currently unused -// func (gui *Gui) renderContextStack() string { -// result := "" -// for _, context := range gui.State.ContextManager.ContextStack { -// result += context.GetViewName() + "\n" -// } -// return result -// } diff --git a/pkg/gui/context/base_context.go b/pkg/gui/context/base_context.go index 86d15c79a..421476368 100644 --- a/pkg/gui/context/base_context.go +++ b/pkg/gui/context/base_context.go @@ -16,6 +16,9 @@ type BaseContext struct { keybindingsFns []types.KeybindingsFn mouseKeybindingsFns []types.MouseKeybindingsFn onClickFn func() error + onRenderToMainFn func() error + onFocusFn onFocusFn + onFocusLostFn onFocusLostFn focusable bool transient bool @@ -25,6 +28,11 @@ type BaseContext struct { *ParentContextMgr } +type ( + onFocusFn = func(types.OnFocusOpts) error + onFocusLostFn = func(types.OnFocusLostOpts) error +) + var _ types.IBaseContext = &BaseContext{} type NewBaseContextOpts struct { @@ -129,6 +137,36 @@ func (self *BaseContext) GetOnClick() func() error { return self.onClickFn } +func (self *BaseContext) AddOnRenderToMainFn(fn func() error) { + if fn != nil { + self.onRenderToMainFn = fn + } +} + +func (self *BaseContext) GetOnRenderToMain() func() error { + return self.onRenderToMainFn +} + +func (self *BaseContext) AddOnFocusFn(fn onFocusFn) { + if fn != nil { + self.onFocusFn = fn + } +} + +func (self *BaseContext) GetOnFocus() onFocusFn { + return self.onFocusFn +} + +func (self *BaseContext) AddOnFocusLostFn(fn onFocusLostFn) { + if fn != nil { + self.onFocusLostFn = fn + } +} + +func (self *BaseContext) GetOnFocusLost() onFocusLostFn { + return self.onFocusLostFn +} + func (self *BaseContext) GetMouseKeybindings(opts types.KeybindingsOpts) []*gocui.ViewMouseBinding { bindings := []*gocui.ViewMouseBinding{} for i := range self.mouseKeybindingsFns { diff --git a/pkg/gui/context/branches_context.go b/pkg/gui/context/branches_context.go index a3e404fdb..f0d356a15 100644 --- a/pkg/gui/context/branches_context.go +++ b/pkg/gui/context/branches_context.go @@ -11,22 +11,21 @@ type BranchesContext struct { *ListContextTrait } -var _ types.IListContext = (*BranchesContext)(nil) +var ( + _ types.IListContext = (*BranchesContext)(nil) + _ types.DiffableContext = (*BranchesContext)(nil) +) func NewBranchesContext( getModel func() []*models.Branch, view *gocui.View, getDisplayStrings func(startIdx int, length int) [][]string, - onFocus func(types.OnFocusOpts) error, - onRenderToMain func() error, - onFocusLost func(opts types.OnFocusLostOpts) error, - c *types.HelperCommon, ) *BranchesContext { viewModel := NewBasicViewModel(getModel) - return &BranchesContext{ + self := &BranchesContext{ BasicViewModel: viewModel, ListContextTrait: &ListContextTrait{ Context: NewSimpleContext(NewBaseContext(NewBaseContextOpts{ @@ -35,16 +34,14 @@ func NewBranchesContext( Key: LOCAL_BRANCHES_CONTEXT_KEY, Kind: types.SIDE_CONTEXT, Focusable: true, - }), ContextCallbackOpts{ - OnFocus: onFocus, - OnFocusLost: onFocusLost, - OnRenderToMain: onRenderToMain, - }), + }), ContextCallbackOpts{}), list: viewModel, getDisplayStrings: getDisplayStrings, c: c, }, } + + return self } func (self *BranchesContext) GetSelectedItemId() string { @@ -63,3 +60,16 @@ func (self *BranchesContext) GetSelectedRef() types.Ref { } return branch } + +func (self *BranchesContext) GetDiffTerminals() []string { + // for our local branches we want to include both the branch and its upstream + branch := self.GetSelected() + if branch != nil { + names := []string{branch.ID()} + if branch.IsTrackingRemote() { + names = append(names, branch.ID()+"@{u}") + } + return names + } + return nil +} diff --git a/pkg/gui/context/commit_files_context.go b/pkg/gui/context/commit_files_context.go index 4a28ac4c5..c2de8b448 100644 --- a/pkg/gui/context/commit_files_context.go +++ b/pkg/gui/context/commit_files_context.go @@ -13,17 +13,16 @@ type CommitFilesContext struct { *DynamicTitleBuilder } -var _ types.IListContext = (*CommitFilesContext)(nil) +var ( + _ types.IListContext = (*CommitFilesContext)(nil) + _ types.DiffableContext = (*CommitFilesContext)(nil) +) func NewCommitFilesContext( getModel func() []*models.CommitFile, view *gocui.View, getDisplayStrings func(startIdx int, length int) [][]string, - onFocus func(types.OnFocusOpts) error, - onRenderToMain func() error, - onFocusLost func(opts types.OnFocusLostOpts) error, - c *types.HelperCommon, ) *CommitFilesContext { viewModel := filetree.NewCommitFileTreeViewModel(getModel, c.Log, c.UserConfig.Gui.ShowFileTree) @@ -41,11 +40,7 @@ func NewCommitFilesContext( Focusable: true, Transient: true, }), - ContextCallbackOpts{ - OnFocus: onFocus, - OnFocusLost: onFocusLost, - OnRenderToMain: onRenderToMain, - }), + ContextCallbackOpts{}), list: viewModel, getDisplayStrings: getDisplayStrings, c: c, @@ -61,3 +56,50 @@ func (self *CommitFilesContext) GetSelectedItemId() string { return item.ID() } + +func (self *CommitFilesContext) GetDiffTerminals() []string { + return []string{self.GetRef().RefName()} +} + +func (self *CommitFilesContext) renderToMain() error { + node := self.GetSelected() + if node == nil { + return nil + } + + ref := self.GetRef() + to := ref.RefName() + from, reverse := self.c.Modes().Diffing.GetFromAndReverseArgsForDiff(ref.ParentRefName()) + + cmdObj := self.c.Git().WorkingTree.ShowFileDiffCmdObj( + from, to, reverse, node.GetPath(), false, self.c.State().GetIgnoreWhitespaceInDiffView(), + ) + task := types.NewRunPtyTask(cmdObj.GetCmd()) + + pair := self.c.MainViewPairs().Normal + if node.File != nil { + pair = self.c.MainViewPairs().PatchBuilding + } + + return self.c.RenderToMainViews(types.RefreshMainOpts{ + Pair: pair, + Main: &types.ViewUpdateOpts{ + Title: self.c.Tr.Patch, + Task: task, + }, + Secondary: secondaryPatchPanelUpdateOpts(self.c), + }) +} + +func secondaryPatchPanelUpdateOpts(c *types.HelperCommon) *types.ViewUpdateOpts { + if c.Git().Patch.PatchBuilder.Active() { + patch := c.Git().Patch.PatchBuilder.RenderAggregatedPatch(false) + + return &types.ViewUpdateOpts{ + Task: types.NewRenderStringWithoutScrollTask(patch), + Title: c.Tr.CustomPatch, + } + } + + return nil +} diff --git a/pkg/gui/context/local_commits_context.go b/pkg/gui/context/local_commits_context.go index 462a85d59..de306f0d2 100644 --- a/pkg/gui/context/local_commits_context.go +++ b/pkg/gui/context/local_commits_context.go @@ -11,17 +11,16 @@ type LocalCommitsContext struct { *ViewportListContextTrait } -var _ types.IListContext = (*LocalCommitsContext)(nil) +var ( + _ types.IListContext = (*LocalCommitsContext)(nil) + _ types.DiffableContext = (*LocalCommitsContext)(nil) +) func NewLocalCommitsContext( getModel func() []*models.Commit, view *gocui.View, getDisplayStrings func(startIdx int, length int) [][]string, - onFocus func(types.OnFocusOpts) error, - onRenderToMain func() error, - onFocusLost func(opts types.OnFocusLostOpts) error, - c *types.HelperCommon, ) *LocalCommitsContext { viewModel := NewLocalCommitsViewModel(getModel, c) @@ -36,11 +35,7 @@ func NewLocalCommitsContext( Key: LOCAL_COMMITS_CONTEXT_KEY, Kind: types.SIDE_CONTEXT, Focusable: true, - }), ContextCallbackOpts{ - OnFocus: onFocus, - OnFocusLost: onFocusLost, - OnRenderToMain: onRenderToMain, - }), + }), ContextCallbackOpts{}), list: viewModel, getDisplayStrings: getDisplayStrings, c: c, @@ -91,6 +86,12 @@ func (self *LocalCommitsContext) GetSelectedRef() types.Ref { return commit } +func (self *LocalCommitsContext) GetDiffTerminals() []string { + itemId := self.GetSelectedItemId() + + return []string{itemId} +} + func (self *LocalCommitsViewModel) SetLimitCommits(value bool) { self.limitCommits = value } diff --git a/pkg/gui/context/menu_context.go b/pkg/gui/context/menu_context.go index 780c35660..8afd7df47 100644 --- a/pkg/gui/context/menu_context.go +++ b/pkg/gui/context/menu_context.go @@ -24,12 +24,6 @@ func NewMenuContext( ) *MenuContext { viewModel := NewMenuViewModel() - onFocus := func(types.OnFocusOpts) error { - selectedMenuItem := viewModel.GetSelected() - renderToDescriptionView(selectedMenuItem.Tooltip) - return nil - } - return &MenuContext{ MenuViewModel: viewModel, ListContextTrait: &ListContextTrait{ @@ -41,9 +35,7 @@ func NewMenuContext( OnGetOptionsMap: getOptionsMap, Focusable: true, HasUncontrolledBounds: true, - }), ContextCallbackOpts{ - OnFocus: onFocus, - }), + }), ContextCallbackOpts{}), getDisplayStrings: viewModel.GetDisplayStrings, list: viewModel, c: c, diff --git a/pkg/gui/context/reflog_commits_context.go b/pkg/gui/context/reflog_commits_context.go index e197a50bd..512aabfe0 100644 --- a/pkg/gui/context/reflog_commits_context.go +++ b/pkg/gui/context/reflog_commits_context.go @@ -11,17 +11,16 @@ type ReflogCommitsContext struct { *ListContextTrait } -var _ types.IListContext = (*ReflogCommitsContext)(nil) +var ( + _ types.IListContext = (*ReflogCommitsContext)(nil) + _ types.DiffableContext = (*ReflogCommitsContext)(nil) +) func NewReflogCommitsContext( getModel func() []*models.Commit, view *gocui.View, getDisplayStrings func(startIdx int, length int) [][]string, - onFocus func(types.OnFocusOpts) error, - onRenderToMain func() error, - onFocusLost func(opts types.OnFocusLostOpts) error, - c *types.HelperCommon, ) *ReflogCommitsContext { viewModel := NewBasicViewModel(getModel) @@ -35,11 +34,7 @@ func NewReflogCommitsContext( Key: REFLOG_COMMITS_CONTEXT_KEY, Kind: types.SIDE_CONTEXT, Focusable: true, - }), ContextCallbackOpts{ - OnFocus: onFocus, - OnFocusLost: onFocusLost, - OnRenderToMain: onRenderToMain, - }), + }), ContextCallbackOpts{}), list: viewModel, getDisplayStrings: getDisplayStrings, c: c, @@ -71,3 +66,9 @@ func (self *ReflogCommitsContext) GetSelectedRef() types.Ref { func (self *ReflogCommitsContext) GetCommits() []*models.Commit { return self.getModel() } + +func (self *ReflogCommitsContext) GetDiffTerminals() []string { + itemId := self.GetSelectedItemId() + + return []string{itemId} +} diff --git a/pkg/gui/context/remote_branches_context.go b/pkg/gui/context/remote_branches_context.go index 44dc06848..563255508 100644 --- a/pkg/gui/context/remote_branches_context.go +++ b/pkg/gui/context/remote_branches_context.go @@ -12,17 +12,16 @@ type RemoteBranchesContext struct { *DynamicTitleBuilder } -var _ types.IListContext = (*RemoteBranchesContext)(nil) +var ( + _ types.IListContext = (*RemoteBranchesContext)(nil) + _ types.DiffableContext = (*RemoteBranchesContext)(nil) +) func NewRemoteBranchesContext( getModel func() []*models.RemoteBranch, view *gocui.View, getDisplayStrings func(startIdx int, length int) [][]string, - onFocus func(types.OnFocusOpts) error, - onRenderToMain func() error, - onFocusLost func(opts types.OnFocusLostOpts) error, - c *types.HelperCommon, ) *RemoteBranchesContext { viewModel := NewBasicViewModel(getModel) @@ -38,11 +37,7 @@ func NewRemoteBranchesContext( Kind: types.SIDE_CONTEXT, Focusable: true, Transient: true, - }), ContextCallbackOpts{ - OnFocus: onFocus, - OnFocusLost: onFocusLost, - OnRenderToMain: onRenderToMain, - }), + }), ContextCallbackOpts{}), list: viewModel, getDisplayStrings: getDisplayStrings, c: c, @@ -66,3 +61,9 @@ func (self *RemoteBranchesContext) GetSelectedRef() types.Ref { } return remoteBranch } + +func (self *RemoteBranchesContext) GetDiffTerminals() []string { + itemId := self.GetSelectedItemId() + + return []string{itemId} +} diff --git a/pkg/gui/context/remotes_context.go b/pkg/gui/context/remotes_context.go index 0f11908dd..62c7241c6 100644 --- a/pkg/gui/context/remotes_context.go +++ b/pkg/gui/context/remotes_context.go @@ -11,17 +11,16 @@ type RemotesContext struct { *ListContextTrait } -var _ types.IListContext = (*RemotesContext)(nil) +var ( + _ types.IListContext = (*RemotesContext)(nil) + _ types.DiffableContext = (*RemotesContext)(nil) +) func NewRemotesContext( getModel func() []*models.Remote, view *gocui.View, getDisplayStrings func(startIdx int, length int) [][]string, - onFocus func(types.OnFocusOpts) error, - onRenderToMain func() error, - onFocusLost func(opts types.OnFocusLostOpts) error, - c *types.HelperCommon, ) *RemotesContext { viewModel := NewBasicViewModel(getModel) @@ -35,11 +34,7 @@ func NewRemotesContext( Key: REMOTES_CONTEXT_KEY, Kind: types.SIDE_CONTEXT, Focusable: true, - }), ContextCallbackOpts{ - OnFocus: onFocus, - OnFocusLost: onFocusLost, - OnRenderToMain: onRenderToMain, - }), + }), ContextCallbackOpts{}), list: viewModel, getDisplayStrings: getDisplayStrings, c: c, @@ -55,3 +50,9 @@ func (self *RemotesContext) GetSelectedItemId() string { return item.ID() } + +func (self *RemotesContext) GetDiffTerminals() []string { + itemId := self.GetSelectedItemId() + + return []string{itemId} +} diff --git a/pkg/gui/context/simple_context.go b/pkg/gui/context/simple_context.go index 38fe4ee78..7ded4bd68 100644 --- a/pkg/gui/context/simple_context.go +++ b/pkg/gui/context/simple_context.go @@ -6,29 +6,19 @@ import ( ) type SimpleContext struct { - OnFocus func(opts types.OnFocusOpts) error - OnFocusLost func(opts types.OnFocusLostOpts) error - OnRender func() error - // this is for pushing some content to the main view - OnRenderToMain func() error + OnRender func() error *BaseContext } type ContextCallbackOpts struct { - OnFocus func(opts types.OnFocusOpts) error - OnFocusLost func(opts types.OnFocusLostOpts) error - OnRender func() error - OnRenderToMain func() error + OnRender func() error } func NewSimpleContext(baseContext *BaseContext, opts ContextCallbackOpts) *SimpleContext { return &SimpleContext{ - OnFocus: opts.OnFocus, - OnFocusLost: opts.OnFocusLost, - OnRender: opts.OnRender, - OnRenderToMain: opts.OnRenderToMain, - BaseContext: baseContext, + OnRender: opts.OnRender, + BaseContext: baseContext, } } @@ -54,14 +44,14 @@ func (self *SimpleContext) HandleFocus(opts types.OnFocusOpts) error { self.GetViewTrait().SetHighlight(true) } - if self.OnFocus != nil { - if err := self.OnFocus(opts); err != nil { + if self.onFocusFn != nil { + if err := self.onFocusFn(opts); err != nil { return err } } - if self.OnRenderToMain != nil { - if err := self.OnRenderToMain(); err != nil { + if self.onRenderToMainFn != nil { + if err := self.onRenderToMainFn(); err != nil { return err } } @@ -70,8 +60,8 @@ func (self *SimpleContext) HandleFocus(opts types.OnFocusOpts) error { } func (self *SimpleContext) HandleFocusLost(opts types.OnFocusLostOpts) error { - if self.OnFocusLost != nil { - return self.OnFocusLost(opts) + if self.onFocusLostFn != nil { + return self.onFocusLostFn(opts) } return nil } @@ -84,8 +74,8 @@ func (self *SimpleContext) HandleRender() error { } func (self *SimpleContext) HandleRenderToMain() error { - if self.OnRenderToMain != nil { - return self.OnRenderToMain() + if self.onRenderToMainFn != nil { + return self.onRenderToMainFn() } return nil diff --git a/pkg/gui/context/stash_context.go b/pkg/gui/context/stash_context.go index 19eb5030a..2c8f2e271 100644 --- a/pkg/gui/context/stash_context.go +++ b/pkg/gui/context/stash_context.go @@ -11,17 +11,16 @@ type StashContext struct { *ListContextTrait } -var _ types.IListContext = (*StashContext)(nil) +var ( + _ types.IListContext = (*StashContext)(nil) + _ types.DiffableContext = (*StashContext)(nil) +) func NewStashContext( getModel func() []*models.StashEntry, view *gocui.View, getDisplayStrings func(startIdx int, length int) [][]string, - onFocus func(types.OnFocusOpts) error, - onRenderToMain func() error, - onFocusLost func(opts types.OnFocusLostOpts) error, - c *types.HelperCommon, ) *StashContext { viewModel := NewBasicViewModel(getModel) @@ -35,11 +34,7 @@ func NewStashContext( Key: STASH_CONTEXT_KEY, Kind: types.SIDE_CONTEXT, Focusable: true, - }), ContextCallbackOpts{ - OnFocus: onFocus, - OnFocusLost: onFocusLost, - OnRenderToMain: onRenderToMain, - }), + }), ContextCallbackOpts{}), list: viewModel, getDisplayStrings: getDisplayStrings, c: c, @@ -67,3 +62,9 @@ func (self *StashContext) GetSelectedRef() types.Ref { } return stash } + +func (self *StashContext) GetDiffTerminals() []string { + itemId := self.GetSelectedItemId() + + return []string{itemId} +} diff --git a/pkg/gui/context/sub_commits_context.go b/pkg/gui/context/sub_commits_context.go index 5fdea8e6d..ba3002d99 100644 --- a/pkg/gui/context/sub_commits_context.go +++ b/pkg/gui/context/sub_commits_context.go @@ -15,17 +15,16 @@ type SubCommitsContext struct { *DynamicTitleBuilder } -var _ types.IListContext = (*SubCommitsContext)(nil) +var ( + _ types.IListContext = (*SubCommitsContext)(nil) + _ types.DiffableContext = (*SubCommitsContext)(nil) +) func NewSubCommitsContext( getModel func() []*models.Commit, view *gocui.View, getDisplayStrings func(startIdx int, length int) [][]string, - onFocus func(types.OnFocusOpts) error, - onRenderToMain func() error, - onFocusLost func(opts types.OnFocusLostOpts) error, - c *types.HelperCommon, ) *SubCommitsContext { viewModel := &SubCommitsViewModel{ @@ -46,11 +45,7 @@ func NewSubCommitsContext( Kind: types.SIDE_CONTEXT, Focusable: true, Transient: true, - }), ContextCallbackOpts{ - OnFocus: onFocus, - OnFocusLost: onFocusLost, - OnRenderToMain: onRenderToMain, - }), + }), ContextCallbackOpts{}), list: viewModel, getDisplayStrings: getDisplayStrings, c: c, @@ -111,3 +106,9 @@ func (self *SubCommitsContext) SetLimitCommits(value bool) { func (self *SubCommitsContext) GetLimitCommits() bool { return self.limitCommits } + +func (self *SubCommitsContext) GetDiffTerminals() []string { + itemId := self.GetSelectedItemId() + + return []string{itemId} +} diff --git a/pkg/gui/context/submodules_context.go b/pkg/gui/context/submodules_context.go index 5491cd137..9b2f3331b 100644 --- a/pkg/gui/context/submodules_context.go +++ b/pkg/gui/context/submodules_context.go @@ -18,10 +18,6 @@ func NewSubmodulesContext( view *gocui.View, getDisplayStrings func(startIdx int, length int) [][]string, - onFocus func(types.OnFocusOpts) error, - onRenderToMain func() error, - onFocusLost func(opts types.OnFocusLostOpts) error, - c *types.HelperCommon, ) *SubmodulesContext { viewModel := NewBasicViewModel(getModel) @@ -35,11 +31,7 @@ func NewSubmodulesContext( Key: SUBMODULES_CONTEXT_KEY, Kind: types.SIDE_CONTEXT, Focusable: true, - }), ContextCallbackOpts{ - OnFocus: onFocus, - OnFocusLost: onFocusLost, - OnRenderToMain: onRenderToMain, - }), + }), ContextCallbackOpts{}), list: viewModel, getDisplayStrings: getDisplayStrings, c: c, diff --git a/pkg/gui/context/suggestions_context.go b/pkg/gui/context/suggestions_context.go index 4be86244a..346492e0c 100644 --- a/pkg/gui/context/suggestions_context.go +++ b/pkg/gui/context/suggestions_context.go @@ -17,10 +17,6 @@ func NewSuggestionsContext( view *gocui.View, getDisplayStrings func(startIdx int, length int) [][]string, - onFocus func(types.OnFocusOpts) error, - onRenderToMain func() error, - onFocusLost func(opts types.OnFocusLostOpts) error, - c *types.HelperCommon, ) *SuggestionsContext { viewModel := NewBasicViewModel(getModel) @@ -35,11 +31,7 @@ func NewSuggestionsContext( Kind: types.PERSISTENT_POPUP, Focusable: true, HasUncontrolledBounds: true, - }), ContextCallbackOpts{ - OnFocus: onFocus, - OnFocusLost: onFocusLost, - OnRenderToMain: onRenderToMain, - }), + }), ContextCallbackOpts{}), list: viewModel, getDisplayStrings: getDisplayStrings, c: c, diff --git a/pkg/gui/context/tags_context.go b/pkg/gui/context/tags_context.go index 6cb14e371..a9e05627d 100644 --- a/pkg/gui/context/tags_context.go +++ b/pkg/gui/context/tags_context.go @@ -11,17 +11,16 @@ type TagsContext struct { *ListContextTrait } -var _ types.IListContext = (*TagsContext)(nil) +var ( + _ types.IListContext = (*TagsContext)(nil) + _ types.DiffableContext = (*TagsContext)(nil) +) func NewTagsContext( getModel func() []*models.Tag, view *gocui.View, getDisplayStrings func(startIdx int, length int) [][]string, - onFocus func(types.OnFocusOpts) error, - onRenderToMain func() error, - onFocusLost func(opts types.OnFocusLostOpts) error, - c *types.HelperCommon, ) *TagsContext { viewModel := NewBasicViewModel(getModel) @@ -35,11 +34,7 @@ func NewTagsContext( Key: TAGS_CONTEXT_KEY, Kind: types.SIDE_CONTEXT, Focusable: true, - }), ContextCallbackOpts{ - OnFocus: onFocus, - OnFocusLost: onFocusLost, - OnRenderToMain: onRenderToMain, - }), + }), ContextCallbackOpts{}), list: viewModel, getDisplayStrings: getDisplayStrings, c: c, @@ -63,3 +58,9 @@ func (self *TagsContext) GetSelectedRef() types.Ref { } return tag } + +func (self *TagsContext) GetDiffTerminals() []string { + itemId := self.GetSelectedItemId() + + return []string{itemId} +} diff --git a/pkg/gui/context/working_tree_context.go b/pkg/gui/context/working_tree_context.go index f8da6a068..900158320 100644 --- a/pkg/gui/context/working_tree_context.go +++ b/pkg/gui/context/working_tree_context.go @@ -19,10 +19,6 @@ func NewWorkingTreeContext( view *gocui.View, getDisplayStrings func(startIdx int, length int) [][]string, - onFocus func(types.OnFocusOpts) error, - onRenderToMain func() error, - onFocusLost func(opts types.OnFocusLostOpts) error, - c *types.HelperCommon, ) *WorkingTreeContext { viewModel := filetree.NewFileTreeViewModel(getModel, c.Log, c.UserConfig.Gui.ShowFileTree) @@ -36,11 +32,7 @@ func NewWorkingTreeContext( Key: FILES_CONTEXT_KEY, Kind: types.SIDE_CONTEXT, Focusable: true, - }), ContextCallbackOpts{ - OnFocus: onFocus, - OnFocusLost: onFocusLost, - OnRenderToMain: onRenderToMain, - }), + }), ContextCallbackOpts{}), list: viewModel, getDisplayStrings: getDisplayStrings, c: c, diff --git a/pkg/gui/context_config.go b/pkg/gui/context_config.go index b19824237..87887a9e8 100644 --- a/pkg/gui/context_config.go +++ b/pkg/gui/context_config.go @@ -1,6 +1,7 @@ package gui import ( + "github.com/jesseduffield/generics/slices" "github.com/jesseduffield/lazygit/pkg/gui/context" "github.com/jesseduffield/lazygit/pkg/gui/types" ) @@ -16,9 +17,7 @@ func (gui *Gui) contextTree() *context.ContextTree { Focusable: false, HasUncontrolledBounds: true, // setting to true because the global context doesn't even have a view }), - context.ContextCallbackOpts{ - OnRenderToMain: gui.statusRenderToMain, - }, + context.ContextCallbackOpts{}, ), Status: context.NewSimpleContext( context.NewBaseContext(context.NewBaseContextOpts{ @@ -28,9 +27,7 @@ func (gui *Gui) contextTree() *context.ContextTree { Key: context.STATUS_CONTEXT_KEY, Focusable: true, }), - context.ContextCallbackOpts{ - OnRenderToMain: gui.statusRenderToMain, - }, + context.ContextCallbackOpts{}, ), Snake: context.NewSimpleContext( context.NewBaseContext(context.NewBaseContextOpts{ @@ -40,17 +37,7 @@ func (gui *Gui) contextTree() *context.ContextTree { Key: context.SNAKE_CONTEXT_KEY, Focusable: true, }), - context.ContextCallbackOpts{ - OnFocus: func(opts types.OnFocusOpts) error { - gui.startSnake() - return nil - }, - OnFocusLost: func(opts types.OnFocusLostOpts) error { - gui.snakeGame.Exit() - gui.moveToTopOfWindow(gui.State.Contexts.Submodules) - return nil - }, - }, + context.ContextCallbackOpts{}, ), Files: gui.filesListContext(), Submodules: gui.submodulesListContext(), @@ -73,11 +60,7 @@ func (gui *Gui) contextTree() *context.ContextTree { Key: context.NORMAL_MAIN_CONTEXT_KEY, Focusable: false, }), - context.ContextCallbackOpts{ - OnFocus: func(opts types.OnFocusOpts) error { - return nil // TODO: should we do something here? We should allow for scrolling the panel - }, - }, + context.ContextCallbackOpts{}, ), NormalSecondary: context.NewSimpleContext( context.NewBaseContext(context.NewBaseContextOpts{ @@ -97,7 +80,7 @@ func (gui *Gui) contextTree() *context.ContextTree { gui.Views.Staging.Wrap = false gui.Views.StagingSecondary.Wrap = false - return gui.refreshStagingPanel(opts) + return gui.helpers.Staging.RefreshStagingPanel(opts) }, func(opts types.OnFocusLostOpts) error { gui.State.Contexts.Staging.SetState(nil) @@ -121,7 +104,7 @@ func (gui *Gui) contextTree() *context.ContextTree { gui.Views.Staging.Wrap = false gui.Views.StagingSecondary.Wrap = false - return gui.refreshStagingPanel(opts) + return gui.helpers.Staging.RefreshStagingPanel(opts) }, func(opts types.OnFocusLostOpts) error { gui.State.Contexts.StagingSecondary.SetState(nil) @@ -145,7 +128,7 @@ func (gui *Gui) contextTree() *context.ContextTree { // no need to change wrap on the secondary view because it can't be interacted with gui.Views.PatchBuilding.Wrap = false - return gui.refreshPatchBuildingPanel(opts) + return gui.helpers.PatchBuilding.RefreshPatchBuildingPanel(opts) }, func(opts types.OnFocusLostOpts) error { gui.Views.PatchBuilding.Wrap = true @@ -180,20 +163,7 @@ func (gui *Gui) contextTree() *context.ContextTree { ), MergeConflicts: context.NewMergeConflictsContext( gui.Views.MergeConflicts, - context.ContextCallbackOpts{ - OnFocus: OnFocusWrapper(func() error { - gui.Views.MergeConflicts.Wrap = false - - return gui.refreshMergePanel(true) - }), - OnFocusLost: func(opts types.OnFocusLostOpts) error { - gui.State.Contexts.MergeConflicts.SetUserScrolling(false) - gui.State.Contexts.MergeConflicts.GetState().ResetConflictSelection() - gui.Views.MergeConflicts.Wrap = true - - return nil - }, - }, + context.ContextCallbackOpts{}, gui.c, func() map[string]string { // wrapping in a function because contexts are initialized before helpers @@ -211,10 +181,6 @@ func (gui *Gui) contextTree() *context.ContextTree { }), context.ContextCallbackOpts{ OnFocus: OnFocusWrapper(gui.handleAskFocused), - OnFocusLost: func(types.OnFocusLostOpts) error { - gui.deactivateConfirmationPrompt() - return nil - }, }, ), CommitMessage: context.NewSimpleContext( @@ -278,3 +244,27 @@ func (gui *Gui) getPatchExplorerContexts() []types.IPatchExplorerContext { gui.State.Contexts.CustomPatchBuilder, } } + +func (gui *Gui) popupViewNames() []string { + popups := slices.Filter(gui.State.Contexts.Flatten(), func(c types.Context) bool { + return c.GetKind() == types.PERSISTENT_POPUP || c.GetKind() == types.TEMPORARY_POPUP + }) + + return slices.Map(popups, func(c types.Context) string { + return c.GetViewName() + }) +} + +func (gui *Gui) defaultSideContext() types.Context { + if gui.State.Modes.Filtering.Active() { + return gui.State.Contexts.LocalCommits + } else { + return gui.State.Contexts.Files + } +} + +func (gui *Gui) TransientContexts() []types.Context { + return slices.Filter(gui.State.Contexts.Flatten(), func(context types.Context) bool { + return context.IsTransient() + }) +} diff --git a/pkg/gui/controllers.go b/pkg/gui/controllers.go index b8fba8c85..278e85602 100644 --- a/pkg/gui/controllers.go +++ b/pkg/gui/controllers.go @@ -10,7 +10,6 @@ import ( "github.com/jesseduffield/lazygit/pkg/gui/modes/cherrypicking" "github.com/jesseduffield/lazygit/pkg/gui/services/custom_commands" "github.com/jesseduffield/lazygit/pkg/gui/types" - "github.com/jesseduffield/lazygit/pkg/snake" ) func (gui *Gui) resetControllers() { @@ -31,10 +30,17 @@ func (gui *Gui) resetControllers() { return gui.State.savedCommitMessage } gpgHelper := helpers.NewGpgHelper(helperCommon, gui.os, gui.git) + viewHelper := helpers.NewViewHelper(helperCommon, gui.State.Contexts) + recordDirectoryHelper := helpers.NewRecordDirectoryHelper(helperCommon) + patchBuildingHelper := helpers.NewPatchBuildingHelper(helperCommon, gui.git, gui.State.Contexts) + stagingHelper := helpers.NewStagingHelper(helperCommon, gui.git, gui.State.Contexts) + mergeConflictsHelper := helpers.NewMergeConflictsHelper(helperCommon, gui.State.Contexts, gui.git) + refreshHelper := helpers.NewRefreshHelper(helperCommon, gui.State.Contexts, gui.git, refsHelper, rebaseHelper, patchBuildingHelper, stagingHelper, mergeConflictsHelper, gui.fileWatcher) gui.helpers = &helpers.Helpers{ Refs: refsHelper, Host: helpers.NewHostHelper(helperCommon, gui.git), - PatchBuilding: helpers.NewPatchBuildingHelper(helperCommon, gui.git, gui.State.Contexts), + PatchBuilding: patchBuildingHelper, + Staging: stagingHelper, Bisect: helpers.NewBisectHelper(helperCommon, gui.git), Suggestions: suggestionsHelper, Files: helpers.NewFilesHelper(helperCommon, gui.git, osCommand), @@ -42,7 +48,7 @@ func (gui *Gui) resetControllers() { Tags: helpers.NewTagsHelper(helperCommon, gui.git), GPG: gpgHelper, MergeAndRebase: rebaseHelper, - MergeConflicts: helpers.NewMergeConflictsHelper(helperCommon, gui.State.Contexts, gui.git), + MergeConflicts: mergeConflictsHelper, CherryPick: helpers.NewCherryPickHelper( helperCommon, gui.git, @@ -50,8 +56,16 @@ func (gui *Gui) resetControllers() { func() *cherrypicking.CherryPicking { return gui.State.Modes.CherryPicking }, rebaseHelper, ), - Upstream: helpers.NewUpstreamHelper(helperCommon, model, suggestionsHelper.GetRemoteBranchesSuggestionsFunc), - AmendHelper: helpers.NewAmendHelper(helperCommon, gui.git, gpgHelper), + Upstream: helpers.NewUpstreamHelper(helperCommon, model, suggestionsHelper.GetRemoteBranchesSuggestionsFunc), + AmendHelper: helpers.NewAmendHelper(helperCommon, gui.git, gpgHelper), + Snake: helpers.NewSnakeHelper(helperCommon), + Diff: helpers.NewDiffHelper(helperCommon), + Repos: helpers.NewRecentReposHelper(helperCommon, recordDirectoryHelper, gui.onNewRepo), + RecordDirectory: recordDirectoryHelper, + Update: helpers.NewUpdateHelper(helperCommon, gui.Updater), + Window: helpers.NewWindowHelper(helperCommon, viewHelper, gui.State.Contexts), + View: viewHelper, + Refresh: refreshHelper, } gui.CustomCommandsClient = custom_commands.NewClient( @@ -77,10 +91,7 @@ func (gui *Gui) resetControllers() { common, ) - submodulesController := controllers.NewSubmodulesController( - common, - gui.enterSubmodule, - ) + submodulesController := controllers.NewSubmodulesController(common) bisectController := controllers.NewBisectController(common) @@ -114,7 +125,6 @@ func (gui *Gui) resetControllers() { tagsController := controllers.NewTagsController(common) filesController := controllers.NewFilesController( common, - gui.enterSubmodule, setCommitMessage, getSavedCommitMessage, ) @@ -137,7 +147,10 @@ func (gui *Gui) resetControllers() { stagingController := controllers.NewStagingController(common, gui.State.Contexts.Staging, gui.State.Contexts.StagingSecondary, false) stagingSecondaryController := controllers.NewStagingController(common, gui.State.Contexts.StagingSecondary, gui.State.Contexts.Staging, true) patchBuildingController := controllers.NewPatchBuildingController(common) - snakeController := controllers.NewSnakeController(common, func() *snake.Game { return gui.snakeGame }) + snakeController := controllers.NewSnakeController(common) + reflogCommitsController := controllers.NewReflogCommitsController(common, gui.State.Contexts.ReflogCommits) + subCommitsController := controllers.NewSubCommitsController(common, gui.State.Contexts.SubCommits) + statusController := controllers.NewStatusController(common) setSubCommits := func(commits []*models.Commit) { gui.Mutexes.SubCommitsMutex.Lock() @@ -163,7 +176,7 @@ func (gui *Gui) resetControllers() { gui.State.Contexts.Stash, } { controllers.AttachControllers(context, controllers.NewSwitchToDiffFilesController( - common, gui.SwitchToCommitFilesContext, context, + common, context, gui.State.Contexts.CommitFiles, )) } @@ -175,6 +188,14 @@ func (gui *Gui) resetControllers() { controllers.AttachControllers(context, controllers.NewBasicCommitsController(common, context)) } + controllers.AttachControllers(gui.State.Contexts.ReflogCommits, + reflogCommitsController, + ) + + controllers.AttachControllers(gui.State.Contexts.SubCommits, + subCommitsController, + ) + // TODO: add scroll controllers for main panels (need to bring some more functionality across for that e.g. reading more from the currently displayed git command) controllers.AttachControllers(gui.State.Contexts.Staging, stagingController, @@ -254,6 +275,10 @@ func (gui *Gui) resetControllers() { remoteBranchesController, ) + controllers.AttachControllers(gui.State.Contexts.Status, + statusController, + ) + controllers.AttachControllers(gui.State.Contexts.Global, syncController, undoController, @@ -271,3 +296,14 @@ func (gui *Gui) resetControllers() { controllers.AttachControllers(context, listControllerFactory.Create(context)) } } + +func (gui *Gui) getSetTextareaTextFn(getView func() *gocui.View) func(string) { + return func(text string) { + // using a getView function so that we don't need to worry about when the view is created + view := getView() + view.ClearTextArea() + view.TextArea.TypeString(text) + _ = gui.resizePopupPanel(view, view.TextArea.GetContent()) + view.RenderTextArea() + } +} diff --git a/pkg/gui/controllers/attach.go b/pkg/gui/controllers/attach.go index 3e621c54c..6a24fd7d3 100644 --- a/pkg/gui/controllers/attach.go +++ b/pkg/gui/controllers/attach.go @@ -7,5 +7,8 @@ func AttachControllers(context types.Context, controllers ...types.IController) context.AddKeybindingsFn(controller.GetKeybindings) context.AddMouseKeybindingsFn(controller.GetMouseKeybindings) context.AddOnClickFn(controller.GetOnClick()) + context.AddOnRenderToMainFn(controller.GetOnRenderToMain()) + context.AddOnFocusFn(controller.GetOnFocus()) + context.AddOnFocusLostFn(controller.GetOnFocusLost()) } } diff --git a/pkg/gui/controllers/base_controller.go b/pkg/gui/controllers/base_controller.go index db7ad7a40..100acfd2a 100644 --- a/pkg/gui/controllers/base_controller.go +++ b/pkg/gui/controllers/base_controller.go @@ -18,3 +18,15 @@ func (self *baseController) GetMouseKeybindings(opts types.KeybindingsOpts) []*g func (self *baseController) GetOnClick() func() error { return nil } + +func (self *baseController) GetOnRenderToMain() func() error { + return nil +} + +func (self *baseController) GetOnFocus() func(types.OnFocusOpts) error { + return nil +} + +func (self *baseController) GetOnFocusLost() func(types.OnFocusLostOpts) error { + return nil +} diff --git a/pkg/gui/controllers/branches_controller.go b/pkg/gui/controllers/branches_controller.go index d5564ec95..27b3004c8 100644 --- a/pkg/gui/controllers/branches_controller.go +++ b/pkg/gui/controllers/branches_controller.go @@ -111,6 +111,30 @@ func (self *BranchesController) GetKeybindings(opts types.KeybindingsOpts) []*ty } } +func (self *BranchesController) GetOnRenderToMain() func() error { + return func() error { + return self.helpers.Diff.WithDiffModeCheck(func() error { + var task types.UpdateTask + branch := self.context().GetSelected() + if branch == nil { + task = types.NewRenderStringTask(self.c.Tr.NoBranchesThisRepo) + } else { + cmdObj := self.c.Git().Branch.GetGraphCmdObj(branch.FullRefName()) + + task = types.NewRunPtyTask(cmdObj.GetCmd()) + } + + return self.c.RenderToMainViews(types.RefreshMainOpts{ + Pair: self.c.MainViewPairs().Normal, + Main: &types.ViewUpdateOpts{ + Title: self.c.Tr.LogTitle, + Task: task, + }, + }) + }) + } +} + func (self *BranchesController) setUpstream(selectedBranch *models.Branch) error { return self.c.Menu(types.CreateMenuOptions{ Title: self.c.Tr.Actions.SetUnsetUpstream, diff --git a/pkg/gui/controllers/confirmation_controller.go b/pkg/gui/controllers/confirmation_controller.go new file mode 100644 index 000000000..c6026d83f --- /dev/null +++ b/pkg/gui/controllers/confirmation_controller.go @@ -0,0 +1,53 @@ +package controllers + +import ( + "github.com/jesseduffield/lazygit/pkg/gui/types" +) + +type ConfirmationController struct { + baseController + *controllerCommon +} + +var _ types.IController = &ConfirmationController{} + +func NewConfirmationController( + common *controllerCommon, +) *ConfirmationController { + return &ConfirmationController{ + baseController: baseController{}, + controllerCommon: common, + } +} + +func (self *ConfirmationController) GetKeybindings(opts types.KeybindingsOpts) []*types.Binding { + bindings := []*types.Binding{} + + return bindings +} + +func (self *ConfirmationController) GetOnFocusLost() func(types.OnFocusLostOpts) error { + return func(types.OnFocusLostOpts) error { + deactivateConfirmationPrompt(self.controllerCommon) + return nil + } +} + +func (self *ConfirmationController) Context() types.Context { + return self.context() +} + +func (self *ConfirmationController) context() types.Context { + return self.contexts.Confirmation +} + +func deactivateConfirmationPrompt(c *controllerCommon) { + c.mutexes.PopupMutex.Lock() + c.c.State().GetRepoState().SetCurrentPopupOpts(nil) + c.mutexes.PopupMutex.Unlock() + + c.c.Views().Confirmation.Visible = false + c.c.Views().Suggestions.Visible = false + + gui.clearConfirmationViewKeyBindings() +} diff --git a/pkg/gui/controllers/files_controller.go b/pkg/gui/controllers/files_controller.go index b028dae6e..4d3740b77 100644 --- a/pkg/gui/controllers/files_controller.go +++ b/pkg/gui/controllers/files_controller.go @@ -15,7 +15,6 @@ type FilesController struct { baseController // nolint: unused *controllerCommon - enterSubmodule func(submodule *models.SubmoduleConfig) error setCommitMessage func(message string) getSavedCommitMessage func() string } @@ -24,13 +23,11 @@ var _ types.IController = &FilesController{} func NewFilesController( common *controllerCommon, - enterSubmodule func(submodule *models.SubmoduleConfig) error, setCommitMessage func(message string), getSavedCommitMessage func() string, ) *FilesController { return &FilesController{ controllerCommon: common, - enterSubmodule: enterSubmodule, setCommitMessage: setCommitMessage, getSavedCommitMessage: getSavedCommitMessage, } @@ -175,6 +172,74 @@ func (self *FilesController) GetMouseKeybindings(opts types.KeybindingsOpts) []* } } +func (self *FilesController) GetOnRenderToMain() func() error { + return func() error { + return self.helpers.Diff.WithDiffModeCheck(func() error { + node := self.context().GetSelected() + + if node == nil { + return self.c.RenderToMainViews(types.RefreshMainOpts{ + Pair: self.c.MainViewPairs().Normal, + Main: &types.ViewUpdateOpts{ + Title: self.c.Tr.DiffTitle, + Task: types.NewRenderStringTask(self.c.Tr.NoChangedFiles), + }, + }) + } + + if node.File != nil && node.File.HasInlineMergeConflicts { + hasConflicts, err := self.helpers.MergeConflicts.SetMergeState(node.GetPath()) + if err != nil { + return err + } + + if hasConflicts { + return self.helpers.MergeConflicts.Render(false) + } + } + + self.helpers.MergeConflicts.ResetMergeState() + + pair := self.c.MainViewPairs().Normal + if node.File != nil { + pair = self.c.MainViewPairs().Staging + } + + split := self.c.UserConfig.Gui.SplitDiff == "always" || (node.GetHasUnstagedChanges() && node.GetHasStagedChanges()) + mainShowsStaged := !split && node.GetHasStagedChanges() + + cmdObj := self.git.WorkingTree.WorktreeFileDiffCmdObj(node, false, mainShowsStaged, self.c.State().GetIgnoreWhitespaceInDiffView()) + title := self.c.Tr.UnstagedChanges + if mainShowsStaged { + title = self.c.Tr.StagedChanges + } + refreshOpts := types.RefreshMainOpts{ + Pair: pair, + Main: &types.ViewUpdateOpts{ + Task: types.NewRunPtyTask(cmdObj.GetCmd()), + Title: title, + }, + } + + if split { + cmdObj := self.git.WorkingTree.WorktreeFileDiffCmdObj(node, false, true, self.c.State().GetIgnoreWhitespaceInDiffView()) + + title := self.c.Tr.StagedChanges + if mainShowsStaged { + title = self.c.Tr.UnstagedChanges + } + + refreshOpts.Secondary = &types.ViewUpdateOpts{ + Title: title, + Task: types.NewRunPtyTask(cmdObj.GetCmd()), + } + } + + return self.c.RenderToMainViews(refreshOpts) + }) + } +} + func (self *FilesController) GetOnClick() func() error { return self.checkSelectedFileNode(self.press) } @@ -379,7 +444,7 @@ func (self *FilesController) EnterFile(opts types.OnFocusOpts) error { submoduleConfigs := self.model.Submodules if file.IsSubmodule(submoduleConfigs) { submoduleConfig := file.SubmoduleConfig(submoduleConfigs) - return self.enterSubmodule(submoduleConfig) + return self.helpers.Repos.EnterSubmodule(submoduleConfig) } if file.HasInlineMergeConflicts { diff --git a/pkg/gui/controllers/helpers/diff_helper.go b/pkg/gui/controllers/helpers/diff_helper.go new file mode 100644 index 000000000..952104f81 --- /dev/null +++ b/pkg/gui/controllers/helpers/diff_helper.go @@ -0,0 +1,114 @@ +package helpers + +import ( + "fmt" + + "github.com/jesseduffield/lazygit/pkg/gui/context" + "github.com/jesseduffield/lazygit/pkg/gui/modes/diffing" + "github.com/jesseduffield/lazygit/pkg/gui/types" + "github.com/samber/lo" +) + +type DiffHelper struct { + c *types.HelperCommon +} + +func NewDiffHelper(c *types.HelperCommon) *DiffHelper { + return &DiffHelper{ + c: c, + } +} + +func (self *DiffHelper) DiffStr() string { + output := self.c.Modes().Diffing.Ref + + right := self.currentDiffTerminal() + if right != "" { + output += " " + right + } + + if self.c.Modes().Diffing.Reverse { + output += " -R" + } + + if self.c.State().GetIgnoreWhitespaceInDiffView() { + output += " --ignore-all-space" + } + + file := self.currentlySelectedFilename() + if file != "" { + output += " -- " + file + } else if self.c.Modes().Filtering.Active() { + output += " -- " + self.c.Modes().Filtering.GetPath() + } + + return output +} + +func (self *DiffHelper) ExitDiffMode() error { + self.c.Modes().Diffing = diffing.New() + return self.c.Refresh(types.RefreshOptions{Mode: types.ASYNC}) +} + +func (self *DiffHelper) RenderDiff() error { + cmdObj := self.c.OS().Cmd.New( + fmt.Sprintf("git diff --submodule --no-ext-diff --color %s", self.DiffStr()), + ) + task := types.NewRunPtyTask(cmdObj.GetCmd()) + + return self.c.RenderToMainViews(types.RefreshMainOpts{ + Pair: self.c.MainViewPairs().Normal, + Main: &types.ViewUpdateOpts{ + Title: "Diff", + Task: task, + }, + }) +} + +// CurrentDiffTerminals returns the current diff terminals of the currently selected item. +// in the case of a branch it returns both the branch and it's upstream name, +// which becomes an option when you bring up the diff menu, but when you're just +// flicking through branches it will be using the local branch name. +func (self *DiffHelper) CurrentDiffTerminals() []string { + c := self.c.CurrentSideContext() + + if c.GetKey() == "" { + return nil + } + + switch v := c.(type) { + case types.DiffableContext: + return v.GetDiffTerminals() + } + + return nil +} + +func (self *DiffHelper) currentDiffTerminal() string { + names := self.CurrentDiffTerminals() + if len(names) == 0 { + return "" + } + return names[0] +} + +func (self *DiffHelper) currentlySelectedFilename() string { + currentContext := self.c.CurrentContext() + + switch currentContext := currentContext.(type) { + case types.IListContext: + if lo.Contains([]types.ContextKey{context.FILES_CONTEXT_KEY, context.COMMIT_FILES_CONTEXT_KEY}, currentContext.GetKey()) { + return currentContext.GetSelectedItemId() + } + } + + return "" +} + +func (self *DiffHelper) WithDiffModeCheck(f func() error) error { + if self.c.Modes().Diffing.Active() { + return self.RenderDiff() + } + + return f() +} diff --git a/pkg/gui/controllers/helpers/helpers.go b/pkg/gui/controllers/helpers/helpers.go index a66d013bd..7d9ce6a30 100644 --- a/pkg/gui/controllers/helpers/helpers.go +++ b/pkg/gui/controllers/helpers/helpers.go @@ -12,26 +12,45 @@ type Helpers struct { CherryPick *CherryPickHelper Host *HostHelper PatchBuilding *PatchBuildingHelper + Staging *StagingHelper GPG *GpgHelper Upstream *UpstreamHelper AmendHelper *AmendHelper + Snake *SnakeHelper + // lives in context package because our contexts need it to render to main + Diff *DiffHelper + Repos *ReposHelper + RecordDirectory *RecordDirectoryHelper + Update *UpdateHelper + Window *WindowHelper + View *ViewHelper + Refresh *RefreshHelper } func NewStubHelpers() *Helpers { return &Helpers{ - Refs: &RefsHelper{}, - Bisect: &BisectHelper{}, - Suggestions: &SuggestionsHelper{}, - Files: &FilesHelper{}, - WorkingTree: &WorkingTreeHelper{}, - Tags: &TagsHelper{}, - MergeAndRebase: &MergeAndRebaseHelper{}, - MergeConflicts: &MergeConflictsHelper{}, - CherryPick: &CherryPickHelper{}, - Host: &HostHelper{}, - PatchBuilding: &PatchBuildingHelper{}, - GPG: &GpgHelper{}, - Upstream: &UpstreamHelper{}, - AmendHelper: &AmendHelper{}, + Refs: &RefsHelper{}, + Bisect: &BisectHelper{}, + Suggestions: &SuggestionsHelper{}, + Files: &FilesHelper{}, + WorkingTree: &WorkingTreeHelper{}, + Tags: &TagsHelper{}, + MergeAndRebase: &MergeAndRebaseHelper{}, + MergeConflicts: &MergeConflictsHelper{}, + CherryPick: &CherryPickHelper{}, + Host: &HostHelper{}, + PatchBuilding: &PatchBuildingHelper{}, + Staging: &StagingHelper{}, + GPG: &GpgHelper{}, + Upstream: &UpstreamHelper{}, + AmendHelper: &AmendHelper{}, + Snake: &SnakeHelper{}, + Diff: &DiffHelper{}, + Repos: &ReposHelper{}, + RecordDirectory: &RecordDirectoryHelper{}, + Update: &UpdateHelper{}, + Window: &WindowHelper{}, + View: &ViewHelper{}, + Refresh: &RefreshHelper{}, } } diff --git a/pkg/gui/controllers/helpers/merge_conflicts_helper.go b/pkg/gui/controllers/helpers/merge_conflicts_helper.go index d7f7aa747..d83626591 100644 --- a/pkg/gui/controllers/helpers/merge_conflicts_helper.go +++ b/pkg/gui/controllers/helpers/merge_conflicts_helper.go @@ -113,3 +113,42 @@ func (self *MergeConflictsHelper) SwitchToMerge(path string) error { func (self *MergeConflictsHelper) context() *context.MergeConflictsContext { return self.contexts.MergeConflicts } + +func (self *MergeConflictsHelper) Render(isFocused bool) error { + content := self.context().GetContentToRender(isFocused) + + var task types.UpdateTask + if self.context().IsUserScrolling() { + task = types.NewRenderStringWithoutScrollTask(content) + } else { + originY := self.context().GetOriginY() + task = types.NewRenderStringWithScrollTask(content, 0, originY) + } + + return self.c.RenderToMainViews(types.RefreshMainOpts{ + Pair: self.c.MainViewPairs().MergeConflicts, + Main: &types.ViewUpdateOpts{ + Task: task, + }, + }) +} + +func (self *MergeConflictsHelper) RefreshMergeState() error { + self.contexts.MergeConflicts.GetMutex().Lock() + defer self.contexts.MergeConflicts.GetMutex().Unlock() + + if self.c.CurrentContext().GetKey() != context.MERGE_CONFLICTS_CONTEXT_KEY { + return nil + } + + hasConflicts, err := self.SetConflictsAndRender(self.contexts.MergeConflicts.GetState().GetPath(), true) + if err != nil { + return self.c.Error(err) + } + + if !hasConflicts { + return self.EscapeMerge() + } + + return nil +} diff --git a/pkg/gui/controllers/helpers/patch_building_helper.go b/pkg/gui/controllers/helpers/patch_building_helper.go index 59de50b99..b58812733 100644 --- a/pkg/gui/controllers/helpers/patch_building_helper.go +++ b/pkg/gui/controllers/helpers/patch_building_helper.go @@ -4,6 +4,7 @@ import ( "github.com/jesseduffield/lazygit/pkg/commands" "github.com/jesseduffield/lazygit/pkg/commands/types/enums" "github.com/jesseduffield/lazygit/pkg/gui/context" + "github.com/jesseduffield/lazygit/pkg/gui/patch_exploring" "github.com/jesseduffield/lazygit/pkg/gui/types" ) @@ -60,3 +61,59 @@ func (self *PatchBuildingHelper) Reset() error { // refreshing the current context so that the secondary panel is hidden if necessary. return self.c.PostRefreshUpdate(self.c.CurrentContext()) } + +func (self *PatchBuildingHelper) RefreshPatchBuildingPanel(opts types.OnFocusOpts) error { + selectedLineIdx := -1 + if opts.ClickedWindowName == "main" { + selectedLineIdx = opts.ClickedViewLineIdx + } + + if !self.git.Patch.PatchBuilder.Active() { + return self.Escape() + } + + // get diff from commit file that's currently selected + path := self.contexts.CommitFiles.GetSelectedPath() + if path == "" { + return nil + } + + ref := self.contexts.CommitFiles.CommitFileTreeViewModel.GetRef() + to := ref.RefName() + from, reverse := self.c.Modes().Diffing.GetFromAndReverseArgsForDiff(ref.ParentRefName()) + diff, err := self.git.WorkingTree.ShowFileDiff(from, to, reverse, path, true, self.c.State().GetIgnoreWhitespaceInDiffView()) + if err != nil { + return err + } + + secondaryDiff := self.git.Patch.PatchBuilder.RenderPatchForFile(path, false, false) + if err != nil { + return err + } + + context := self.contexts.CustomPatchBuilder + + oldState := context.GetState() + + state := patch_exploring.NewState(diff, selectedLineIdx, oldState, self.c.Log) + context.SetState(state) + if state == nil { + return self.Escape() + } + + mainContent := context.GetContentToRender(true) + + self.contexts.CustomPatchBuilder.FocusSelection() + + return self.c.RenderToMainViews(types.RefreshMainOpts{ + Pair: self.c.MainViewPairs().PatchBuilding, + Main: &types.ViewUpdateOpts{ + Task: types.NewRenderStringWithoutScrollTask(mainContent), + Title: self.c.Tr.Patch, + }, + Secondary: &types.ViewUpdateOpts{ + Task: types.NewRenderStringWithoutScrollTask(secondaryDiff), + Title: self.c.Tr.CustomPatch, + }, + }) +} diff --git a/pkg/gui/controllers/helpers/record_directory_helper.go b/pkg/gui/controllers/helpers/record_directory_helper.go new file mode 100644 index 000000000..4877a6b46 --- /dev/null +++ b/pkg/gui/controllers/helpers/record_directory_helper.go @@ -0,0 +1,38 @@ +package helpers + +import ( + "os" + + "github.com/jesseduffield/lazygit/pkg/gui/types" +) + +type RecordDirectoryHelper struct { + c *types.HelperCommon +} + +func NewRecordDirectoryHelper(c *types.HelperCommon) *RecordDirectoryHelper { + return &RecordDirectoryHelper{ + c: c, + } +} + +// when a user runs lazygit with the LAZYGIT_NEW_DIR_FILE env variable defined +// we will write the current directory to that file on exit so that their +// shell can then change to that directory. That means you don't get kicked +// back to the directory that you started with. +func (self *RecordDirectoryHelper) RecordCurrentDirectory() error { + // determine current directory, set it in LAZYGIT_NEW_DIR_FILE + dirName, err := os.Getwd() + if err != nil { + return err + } + return self.RecordDirectory(dirName) +} + +func (self *RecordDirectoryHelper) RecordDirectory(dirName string) error { + newDirFilePath := os.Getenv("LAZYGIT_NEW_DIR_FILE") + if newDirFilePath == "" { + return nil + } + return self.c.OS().CreateFileWithContent(newDirFilePath, dirName) +} diff --git a/pkg/gui/controllers/helpers/refresh_helper.go b/pkg/gui/controllers/helpers/refresh_helper.go new file mode 100644 index 000000000..9a1089de3 --- /dev/null +++ b/pkg/gui/controllers/helpers/refresh_helper.go @@ -0,0 +1,617 @@ +package helpers + +import ( + "fmt" + "strings" + "sync" + + "github.com/jesseduffield/generics/set" + "github.com/jesseduffield/generics/slices" + "github.com/jesseduffield/lazygit/pkg/commands" + "github.com/jesseduffield/lazygit/pkg/commands/git_commands" + "github.com/jesseduffield/lazygit/pkg/commands/models" + "github.com/jesseduffield/lazygit/pkg/commands/types/enums" + "github.com/jesseduffield/lazygit/pkg/gui/context" + "github.com/jesseduffield/lazygit/pkg/gui/filetree" + "github.com/jesseduffield/lazygit/pkg/gui/mergeconflicts" + "github.com/jesseduffield/lazygit/pkg/gui/presentation" + "github.com/jesseduffield/lazygit/pkg/gui/style" + "github.com/jesseduffield/lazygit/pkg/gui/types" + "github.com/jesseduffield/lazygit/pkg/utils" +) + +type RefreshHelper struct { + c *types.HelperCommon + contexts *context.ContextTree + git *commands.GitCommand + refsHelper *RefsHelper + mergeAndRebaseHelper *MergeAndRebaseHelper + patchBuildingHelper *PatchBuildingHelper + stagingHelper *StagingHelper + mergeConflictsHelper *MergeConflictsHelper + fileWatcher types.IFileWatcher +} + +func NewRefreshHelper( + c *types.HelperCommon, + contexts *context.ContextTree, + git *commands.GitCommand, + refsHelper *RefsHelper, + mergeAndRebaseHelper *MergeAndRebaseHelper, + patchBuildingHelper *PatchBuildingHelper, + stagingHelper *StagingHelper, + mergeConflictsHelper *MergeConflictsHelper, + fileWatcher types.IFileWatcher, +) *RefreshHelper { + return &RefreshHelper{ + c: c, + contexts: contexts, + git: git, + refsHelper: refsHelper, + mergeAndRebaseHelper: mergeAndRebaseHelper, + patchBuildingHelper: patchBuildingHelper, + stagingHelper: stagingHelper, + mergeConflictsHelper: mergeConflictsHelper, + fileWatcher: fileWatcher, + } +} + +func (self *RefreshHelper) Refresh(options types.RefreshOptions) error { + if options.Scope == nil { + self.c.Log.Infof( + "refreshing all scopes in %s mode", + getModeName(options.Mode), + ) + } else { + self.c.Log.Infof( + "refreshing the following scopes in %s mode: %s", + getModeName(options.Mode), + strings.Join(getScopeNames(options.Scope), ","), + ) + } + + wg := sync.WaitGroup{} + + f := func() { + var scopeSet *set.Set[types.RefreshableView] + if len(options.Scope) == 0 { + // not refreshing staging/patch-building unless explicitly requested because we only need + // to refresh those while focused. + scopeSet = set.NewFromSlice([]types.RefreshableView{ + types.COMMITS, + types.BRANCHES, + types.FILES, + types.STASH, + types.REFLOG, + types.TAGS, + types.REMOTES, + types.STATUS, + types.BISECT_INFO, + }) + } else { + scopeSet = set.NewFromSlice(options.Scope) + } + + refresh := func(f func()) { + wg.Add(1) + func() { + if options.Mode == types.ASYNC { + go utils.Safe(f) + } else { + f() + } + wg.Done() + }() + } + + if scopeSet.Includes(types.COMMITS) || scopeSet.Includes(types.BRANCHES) || scopeSet.Includes(types.REFLOG) || scopeSet.Includes(types.BISECT_INFO) { + refresh(self.refreshCommits) + } else if scopeSet.Includes(types.REBASE_COMMITS) { + // the above block handles rebase commits so we only need to call this one + // if we've asked specifically for rebase commits and not those other things + refresh(func() { _ = self.refreshRebaseCommits() }) + } + + if scopeSet.Includes(types.SUB_COMMITS) { + refresh(func() { _ = self.refreshSubCommitsWithLimit() }) + } + + // reason we're not doing this if the COMMITS type is included is that if the COMMITS type _is_ included we will refresh the commit files context anyway + if scopeSet.Includes(types.COMMIT_FILES) && !scopeSet.Includes(types.COMMITS) { + refresh(func() { _ = self.refreshCommitFilesContext() }) + } + + if scopeSet.Includes(types.FILES) || scopeSet.Includes(types.SUBMODULES) { + refresh(func() { _ = self.refreshFilesAndSubmodules() }) + } + + if scopeSet.Includes(types.STASH) { + refresh(func() { _ = self.refreshStashEntries() }) + } + + if scopeSet.Includes(types.TAGS) { + refresh(func() { _ = self.refreshTags() }) + } + + if scopeSet.Includes(types.REMOTES) { + refresh(func() { _ = self.refreshRemotes() }) + } + + if scopeSet.Includes(types.STAGING) { + refresh(func() { _ = self.stagingHelper.RefreshStagingPanel(types.OnFocusOpts{}) }) + } + + if scopeSet.Includes(types.PATCH_BUILDING) { + refresh(func() { _ = self.patchBuildingHelper.RefreshPatchBuildingPanel(types.OnFocusOpts{}) }) + } + + if scopeSet.Includes(types.MERGE_CONFLICTS) || scopeSet.Includes(types.FILES) { + refresh(func() { _ = self.mergeConflictsHelper.RefreshMergeState() }) + } + + wg.Wait() + + self.refreshStatus() + + if options.Then != nil { + options.Then() + } + } + + if options.Mode == types.BLOCK_UI { + self.c.OnUIThread(func() error { + f() + return nil + }) + } else { + f() + } + + return nil +} + +func getScopeNames(scopes []types.RefreshableView) []string { + scopeNameMap := map[types.RefreshableView]string{ + types.COMMITS: "commits", + types.BRANCHES: "branches", + types.FILES: "files", + types.SUBMODULES: "submodules", + types.SUB_COMMITS: "subCommits", + types.STASH: "stash", + types.REFLOG: "reflog", + types.TAGS: "tags", + types.REMOTES: "remotes", + types.STATUS: "status", + types.BISECT_INFO: "bisect", + types.STAGING: "staging", + types.MERGE_CONFLICTS: "mergeConflicts", + } + + return slices.Map(scopes, func(scope types.RefreshableView) string { + return scopeNameMap[scope] + }) +} + +func getModeName(mode types.RefreshMode) string { + switch mode { + case types.SYNC: + return "sync" + case types.ASYNC: + return "async" + case types.BLOCK_UI: + return "block-ui" + default: + return "unknown mode" + } +} + +// during startup, the bottleneck is fetching the reflog entries. We need these +// on startup to sort the branches by recency. So we have two phases: INITIAL, and COMPLETE. +// In the initial phase we don't get any reflog commits, but we asynchronously get them +// and refresh the branches after that +func (self *RefreshHelper) refreshReflogCommitsConsideringStartup() { + switch self.c.State().GetRepoState().GetStartupStage() { + case types.INITIAL: + go utils.Safe(func() { + _ = self.refreshReflogCommits() + self.refreshBranches() + self.c.State().GetRepoState().SetStartupStage(types.COMPLETE) + }) + + case types.COMPLETE: + _ = self.refreshReflogCommits() + } +} + +// whenever we change commits, we should update branches because the upstream/downstream +// counts can change. Whenever we change branches we should probably also change commits +// e.g. in the case of switching branches. +func (self *RefreshHelper) refreshCommits() { + wg := sync.WaitGroup{} + wg.Add(2) + + go utils.Safe(func() { + self.refreshReflogCommitsConsideringStartup() + + self.refreshBranches() + wg.Done() + }) + + go utils.Safe(func() { + _ = self.refreshCommitsWithLimit() + ctx, ok := self.contexts.CommitFiles.GetParentContext() + if ok && ctx.GetKey() == context.LOCAL_COMMITS_CONTEXT_KEY { + // This makes sense when we've e.g. just amended a commit, meaning we get a new commit SHA at the same position. + // However if we've just added a brand new commit, it pushes the list down by one and so we would end up + // showing the contents of a different commit than the one we initially entered. + // Ideally we would know when to refresh the commit files context and when not to, + // or perhaps we could just pop that context off the stack whenever cycling windows. + // For now the awkwardness remains. + commit := self.contexts.LocalCommits.GetSelected() + if commit != nil { + self.contexts.CommitFiles.SetRef(commit) + self.contexts.CommitFiles.SetTitleRef(commit.RefName()) + _ = self.refreshCommitFilesContext() + } + } + wg.Done() + }) + + wg.Wait() +} + +func (self *RefreshHelper) refreshCommitsWithLimit() error { + self.c.Mutexes().LocalCommitsMutex.Lock() + defer self.c.Mutexes().LocalCommitsMutex.Unlock() + + commits, err := self.git.Loaders.CommitLoader.GetCommits( + git_commands.GetCommitsOptions{ + Limit: self.contexts.LocalCommits.GetLimitCommits(), + FilterPath: self.c.Modes().Filtering.GetPath(), + IncludeRebaseCommits: true, + RefName: self.refForLog(), + All: self.contexts.LocalCommits.GetShowWholeGitGraph(), + }, + ) + if err != nil { + return err + } + self.c.Model().Commits = commits + self.c.Model().WorkingTreeStateAtLastCommitRefresh = self.git.Status.WorkingTreeState() + + return self.c.PostRefreshUpdate(self.contexts.LocalCommits) +} + +func (self *RefreshHelper) refreshSubCommitsWithLimit() error { + self.c.Mutexes().SubCommitsMutex.Lock() + defer self.c.Mutexes().SubCommitsMutex.Unlock() + + commits, err := self.git.Loaders.CommitLoader.GetCommits( + git_commands.GetCommitsOptions{ + Limit: self.contexts.SubCommits.GetLimitCommits(), + FilterPath: self.c.Modes().Filtering.GetPath(), + IncludeRebaseCommits: false, + RefName: self.contexts.SubCommits.GetRef().FullRefName(), + }, + ) + if err != nil { + return err + } + self.c.Model().SubCommits = commits + + return self.c.PostRefreshUpdate(self.contexts.SubCommits) +} + +func (self *RefreshHelper) refreshCommitFilesContext() error { + ref := self.contexts.CommitFiles.GetRef() + to := ref.RefName() + from, reverse := self.c.Modes().Diffing.GetFromAndReverseArgsForDiff(ref.ParentRefName()) + + files, err := self.git.Loaders.CommitFileLoader.GetFilesInDiff(from, to, reverse) + if err != nil { + return self.c.Error(err) + } + self.c.Model().CommitFiles = files + self.contexts.CommitFiles.CommitFileTreeViewModel.SetTree() + + return self.c.PostRefreshUpdate(self.contexts.CommitFiles) +} + +func (self *RefreshHelper) refreshRebaseCommits() error { + self.c.Mutexes().LocalCommitsMutex.Lock() + defer self.c.Mutexes().LocalCommitsMutex.Unlock() + + updatedCommits, err := self.git.Loaders.CommitLoader.MergeRebasingCommits(self.c.Model().Commits) + if err != nil { + return err + } + self.c.Model().Commits = updatedCommits + self.c.Model().WorkingTreeStateAtLastCommitRefresh = self.git.Status.WorkingTreeState() + + return self.c.PostRefreshUpdate(self.contexts.LocalCommits) +} + +func (self *RefreshHelper) refreshTags() error { + tags, err := self.git.Loaders.TagLoader.GetTags() + if err != nil { + return self.c.Error(err) + } + + self.c.Model().Tags = tags + + return self.c.PostRefreshUpdate(self.contexts.Tags) +} + +func (self *RefreshHelper) refreshStateSubmoduleConfigs() error { + configs, err := self.git.Submodule.GetConfigs() + if err != nil { + return err + } + + self.c.Model().Submodules = configs + + return nil +} + +// self.refreshStatus is called at the end of this because that's when we can +// be sure there is a State.Model.Branches array to pick the current branch from +func (self *RefreshHelper) refreshBranches() { + reflogCommits := self.c.Model().FilteredReflogCommits + if self.c.Modes().Filtering.Active() { + // in filter mode we filter our reflog commits to just those containing the path + // however we need all the reflog entries to populate the recencies of our branches + // which allows us to order them correctly. So if we're filtering we'll just + // manually load all the reflog commits here + var err error + reflogCommits, _, err = self.git.Loaders.ReflogCommitLoader.GetReflogCommits(nil, "") + if err != nil { + self.c.Log.Error(err) + } + } + + branches, err := self.git.Loaders.BranchLoader.Load(reflogCommits) + if err != nil { + _ = self.c.Error(err) + } + + self.c.Model().Branches = branches + + if err := self.c.PostRefreshUpdate(self.contexts.Branches); err != nil { + self.c.Log.Error(err) + } + + self.refreshStatus() +} + +func (self *RefreshHelper) refreshFilesAndSubmodules() error { + self.c.Mutexes().RefreshingFilesMutex.Lock() + self.c.State().SetIsRefreshingFiles(true) + defer func() { + self.c.State().SetIsRefreshingFiles(false) + self.c.Mutexes().RefreshingFilesMutex.Unlock() + }() + + if err := self.refreshStateSubmoduleConfigs(); err != nil { + return err + } + + if err := self.refreshStateFiles(); err != nil { + return err + } + + self.c.OnUIThread(func() error { + if err := self.c.PostRefreshUpdate(self.contexts.Submodules); err != nil { + self.c.Log.Error(err) + } + + if err := self.c.PostRefreshUpdate(self.contexts.Files); err != nil { + self.c.Log.Error(err) + } + + return nil + }) + + return nil +} + +func (self *RefreshHelper) refreshStateFiles() error { + fileTreeViewModel := self.contexts.Files.FileTreeViewModel + + // If git thinks any of our files have inline merge conflicts, but they actually don't, + // we stage them. + // Note that if files with merge conflicts have both arisen and have been resolved + // between refreshes, we won't stage them here. This is super unlikely though, + // and this approach spares us from having to call `git status` twice in a row. + // Although this also means that at startup we won't be staging anything until + // we call git status again. + pathsToStage := []string{} + prevConflictFileCount := 0 + for _, file := range self.c.Model().Files { + if file.HasMergeConflicts { + prevConflictFileCount++ + } + if file.HasInlineMergeConflicts { + hasConflicts, err := mergeconflicts.FileHasConflictMarkers(file.Name) + if err != nil { + self.c.Log.Error(err) + } else if !hasConflicts { + pathsToStage = append(pathsToStage, file.Name) + } + } + } + + if len(pathsToStage) > 0 { + self.c.LogAction(self.c.Tr.Actions.StageResolvedFiles) + if err := self.git.WorkingTree.StageFiles(pathsToStage); err != nil { + return self.c.Error(err) + } + } + + files := self.git.Loaders.FileLoader. + GetStatusFiles(git_commands.GetStatusFileOptions{}) + + conflictFileCount := 0 + for _, file := range files { + if file.HasMergeConflicts { + conflictFileCount++ + } + } + + if self.git.Status.WorkingTreeState() != enums.REBASE_MODE_NONE && conflictFileCount == 0 && prevConflictFileCount > 0 { + self.c.OnUIThread(func() error { return self.mergeAndRebaseHelper.PromptToContinueRebase() }) + } + + fileTreeViewModel.RWMutex.Lock() + + // only taking over the filter if it hasn't already been set by the user. + // Though this does make it impossible for the user to actually say they want to display all if + // conflicts are currently being shown. Hmm. Worth it I reckon. If we need to add some + // extra state here to see if the user's set the filter themselves we can do that, but + // I'd prefer to maintain as little state as possible. + if conflictFileCount > 0 { + if fileTreeViewModel.GetFilter() == filetree.DisplayAll { + fileTreeViewModel.SetFilter(filetree.DisplayConflicted) + } + } else if fileTreeViewModel.GetFilter() == filetree.DisplayConflicted { + fileTreeViewModel.SetFilter(filetree.DisplayAll) + } + + self.c.Model().Files = files + fileTreeViewModel.SetTree() + fileTreeViewModel.RWMutex.Unlock() + + if err := self.fileWatcher.AddFilesToFileWatcher(files); err != nil { + return err + } + + return nil +} + +// the reflogs panel is the only panel where we cache data, in that we only +// load entries that have been created since we last ran the call. This means +// we need to be more careful with how we use this, and to ensure we're emptying +// the reflogs array when changing contexts. +// This method also manages two things: ReflogCommits and FilteredReflogCommits. +// FilteredReflogCommits are rendered in the reflogs panel, and ReflogCommits +// are used by the branches panel to obtain recency values for sorting. +func (self *RefreshHelper) refreshReflogCommits() error { + // pulling state into its own variable incase it gets swapped out for another state + // and we get an out of bounds exception + model := self.c.Model() + var lastReflogCommit *models.Commit + if len(model.ReflogCommits) > 0 { + lastReflogCommit = model.ReflogCommits[0] + } + + refresh := func(stateCommits *[]*models.Commit, filterPath string) error { + commits, onlyObtainedNewReflogCommits, err := self.git.Loaders.ReflogCommitLoader. + GetReflogCommits(lastReflogCommit, filterPath) + if err != nil { + return self.c.Error(err) + } + + if onlyObtainedNewReflogCommits { + *stateCommits = append(commits, *stateCommits...) + } else { + *stateCommits = commits + } + return nil + } + + if err := refresh(&model.ReflogCommits, ""); err != nil { + return err + } + + if self.c.Modes().Filtering.Active() { + if err := refresh(&model.FilteredReflogCommits, self.c.Modes().Filtering.GetPath()); err != nil { + return err + } + } else { + model.FilteredReflogCommits = model.ReflogCommits + } + + return self.c.PostRefreshUpdate(self.contexts.ReflogCommits) +} + +func (self *RefreshHelper) refreshRemotes() error { + prevSelectedRemote := self.contexts.Remotes.GetSelected() + + remotes, err := self.git.Loaders.RemoteLoader.GetRemotes() + if err != nil { + return self.c.Error(err) + } + + self.c.Model().Remotes = remotes + + // we need to ensure our selected remote branches aren't now outdated + if prevSelectedRemote != nil && self.c.Model().RemoteBranches != nil { + // find remote now + for _, remote := range remotes { + if remote.Name == prevSelectedRemote.Name { + self.c.Model().RemoteBranches = remote.Branches + break + } + } + } + + if err := self.c.PostRefreshUpdate(self.contexts.Remotes); err != nil { + return err + } + + if err := self.c.PostRefreshUpdate(self.contexts.RemoteBranches); err != nil { + return err + } + + return nil +} + +func (self *RefreshHelper) refreshStashEntries() error { + self.c.Model().StashEntries = self.git.Loaders.StashLoader. + GetStashEntries(self.c.Modes().Filtering.GetPath()) + + return self.c.PostRefreshUpdate(self.contexts.Stash) +} + +// never call this on its own, it should only be called from within refreshCommits() +func (self *RefreshHelper) refreshStatus() { + self.c.Mutexes().RefreshingStatusMutex.Lock() + defer self.c.Mutexes().RefreshingStatusMutex.Unlock() + + currentBranch := self.refsHelper.GetCheckedOutRef() + if currentBranch == nil { + // need to wait for branches to refresh + return + } + status := "" + + if currentBranch.IsRealBranch() { + status += presentation.ColoredBranchStatus(currentBranch, self.c.Tr) + " " + } + + workingTreeState := self.git.Status.WorkingTreeState() + if workingTreeState != enums.REBASE_MODE_NONE { + status += style.FgYellow.Sprintf("(%s) ", presentation.FormatWorkingTreeState(workingTreeState)) + } + + name := presentation.GetBranchTextStyle(currentBranch.Name).Sprint(currentBranch.Name) + repoName := utils.GetCurrentRepoName() + status += fmt.Sprintf("%s → %s ", repoName, name) + + self.c.SetViewContent(self.c.Views().Status, status) +} + +func (self *RefreshHelper) refForLog() string { + bisectInfo := self.git.Bisect.GetInfo() + self.c.Model().BisectInfo = bisectInfo + + if !bisectInfo.Started() { + return "HEAD" + } + + // need to see if our bisect's current commit is reachable from our 'new' ref. + if bisectInfo.Bisecting() && !self.git.Bisect.ReachableFromStart(bisectInfo) { + return bisectInfo.GetNewSha() + } + + return bisectInfo.GetStartSha() +} diff --git a/pkg/gui/controllers/helpers/repos_helper.go b/pkg/gui/controllers/helpers/repos_helper.go new file mode 100644 index 000000000..ef983e856 --- /dev/null +++ b/pkg/gui/controllers/helpers/repos_helper.go @@ -0,0 +1,175 @@ +package helpers + +import ( + "fmt" + "os" + "path/filepath" + "strings" + "sync" + + "github.com/jesseduffield/generics/slices" + appTypes "github.com/jesseduffield/lazygit/pkg/app/types" + "github.com/jesseduffield/lazygit/pkg/commands" + "github.com/jesseduffield/lazygit/pkg/commands/models" + "github.com/jesseduffield/lazygit/pkg/env" + "github.com/jesseduffield/lazygit/pkg/gui/presentation/icons" + "github.com/jesseduffield/lazygit/pkg/gui/style" + "github.com/jesseduffield/lazygit/pkg/gui/types" + "github.com/jesseduffield/lazygit/pkg/utils" +) + +type onNewRepoFn func(startArgs appTypes.StartArgs, reuseState bool) error + +// helps switch back and forth between repos +type ReposHelper struct { + c *types.HelperCommon + recordDirectoryHelper *RecordDirectoryHelper + onNewRepo onNewRepoFn +} + +func NewRecentReposHelper( + c *types.HelperCommon, + recordDirectoryHelper *RecordDirectoryHelper, + onNewRepo onNewRepoFn, +) *ReposHelper { + return &ReposHelper{ + c: c, + recordDirectoryHelper: recordDirectoryHelper, + onNewRepo: onNewRepo, + } +} + +func (self *ReposHelper) EnterSubmodule(submodule *models.SubmoduleConfig) error { + wd, err := os.Getwd() + if err != nil { + return err + } + self.c.State().GetRepoPathStack().Push(wd) + + return self.DispatchSwitchToRepo(submodule.Path, true) +} + +func (self *ReposHelper) getCurrentBranch(path string) string { + readHeadFile := func(path string) (string, error) { + headFile, err := os.ReadFile(filepath.Join(path, "HEAD")) + if err == nil { + content := strings.TrimSpace(string(headFile)) + refsPrefix := "ref: refs/heads/" + var branchDisplay string + if strings.HasPrefix(content, refsPrefix) { + // is a branch + branchDisplay = strings.TrimPrefix(content, refsPrefix) + } else { + // detached HEAD state, displaying short SHA + branchDisplay = utils.ShortSha(content) + } + return branchDisplay, nil + } + return "", err + } + + gitDirPath := filepath.Join(path, ".git") + + if gitDir, err := os.Stat(gitDirPath); err == nil { + if gitDir.IsDir() { + // ordinary repo + if branch, err := readHeadFile(gitDirPath); err == nil { + return branch + } + } else { + // worktree + if worktreeGitDir, err := os.ReadFile(gitDirPath); err == nil { + content := strings.TrimSpace(string(worktreeGitDir)) + worktreePath := strings.TrimPrefix(content, "gitdir: ") + if branch, err := readHeadFile(worktreePath); err == nil { + return branch + } + } + } + } + + return self.c.Tr.LcBranchUnknown +} + +func (self *ReposHelper) CreateRecentReposMenu() error { + // we'll show an empty panel if there are no recent repos + recentRepoPaths := []string{} + if len(self.c.GetAppState().RecentRepos) > 0 { + // we skip the first one because we're currently in it + recentRepoPaths = self.c.GetAppState().RecentRepos[1:] + } + + currentBranches := sync.Map{} + + wg := sync.WaitGroup{} + wg.Add(len(recentRepoPaths)) + + for _, path := range recentRepoPaths { + go func(path string) { + defer wg.Done() + currentBranches.Store(path, self.getCurrentBranch(path)) + }(path) + } + + wg.Wait() + + menuItems := slices.Map(recentRepoPaths, func(path string) *types.MenuItem { + branchName, _ := currentBranches.Load(path) + if icons.IsIconEnabled() { + branchName = icons.BRANCH_ICON + " " + fmt.Sprintf("%v", branchName) + } + + return &types.MenuItem{ + LabelColumns: []string{ + filepath.Base(path), + style.FgCyan.Sprint(branchName), + style.FgMagenta.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 + self.c.State().GetRepoPathStack().Clear() + return self.DispatchSwitchToRepo(path, false) + }, + } + }) + + return self.c.Menu(types.CreateMenuOptions{Title: self.c.Tr.RecentRepos, Items: menuItems}) +} + +func (self *ReposHelper) DispatchSwitchToRepo(path string, reuse bool) error { + env.UnsetGitDirEnvs() + originalPath, err := os.Getwd() + if err != nil { + return nil + } + + if err := os.Chdir(path); err != nil { + if os.IsNotExist(err) { + return self.c.ErrorMsg(self.c.Tr.ErrRepositoryMovedOrDeleted) + } + return err + } + + if err := commands.VerifyInGitRepo(self.c.OS()); err != nil { + if err := os.Chdir(originalPath); err != nil { + return err + } + + return err + } + + if err := self.recordDirectoryHelper.RecordCurrentDirectory(); err != nil { + return err + } + + // these two mutexes are used by our background goroutines (triggered via `self.goEvery`. We don't want to + // switch to a repo while one of these goroutines is in the process of updating something + self.c.Mutexes().SyncMutex.Lock() + defer self.c.Mutexes().SyncMutex.Unlock() + + self.c.Mutexes().RefreshingFilesMutex.Lock() + defer self.c.Mutexes().RefreshingFilesMutex.Unlock() + + return self.onNewRepo(appTypes.StartArgs{}, reuse) +} diff --git a/pkg/gui/controllers/helpers/snake_helper.go b/pkg/gui/controllers/helpers/snake_helper.go new file mode 100644 index 000000000..5f53273cf --- /dev/null +++ b/pkg/gui/controllers/helpers/snake_helper.go @@ -0,0 +1,76 @@ +package helpers + +import ( + "fmt" + "strings" + + "github.com/jesseduffield/lazygit/pkg/gui/style" + "github.com/jesseduffield/lazygit/pkg/gui/types" + "github.com/jesseduffield/lazygit/pkg/snake" +) + +type SnakeHelper struct { + c *types.HelperCommon + game *snake.Game +} + +func NewSnakeHelper(c *types.HelperCommon) *SnakeHelper { + return &SnakeHelper{ + c: c, + } +} + +func (self *SnakeHelper) StartGame() { + view := self.c.Views().Snake + + game := snake.NewGame(view.Width(), view.Height(), self.renderSnakeGame, self.c.LogAction) + self.game = game + game.Start() +} + +func (self *SnakeHelper) ExitGame() { + self.game.Exit() +} + +func (self *SnakeHelper) SetDirection(direction snake.Direction) { + self.game.SetDirection(direction) +} + +func (self *SnakeHelper) renderSnakeGame(cells [][]snake.CellType, alive bool) { + view := self.c.Views().Snake + + if !alive { + _ = self.c.ErrorMsg(self.c.Tr.YouDied) + return + } + + output := self.drawSnakeGame(cells) + + view.Clear() + fmt.Fprint(view, output) + self.c.Render() +} + +func (self *SnakeHelper) drawSnakeGame(cells [][]snake.CellType) string { + writer := &strings.Builder{} + + for i, row := range cells { + for _, cell := range row { + switch cell { + case snake.None: + writer.WriteString(" ") + case snake.Snake: + writer.WriteString("█") + case snake.Food: + writer.WriteString(style.FgMagenta.Sprint("█")) + } + } + + if i < len(cells) { + writer.WriteString("\n") + } + } + + output := writer.String() + return output +} diff --git a/pkg/gui/controllers/helpers/staging_helper.go b/pkg/gui/controllers/helpers/staging_helper.go new file mode 100644 index 000000000..4b4eb2626 --- /dev/null +++ b/pkg/gui/controllers/helpers/staging_helper.go @@ -0,0 +1,119 @@ +package helpers + +import ( + "github.com/jesseduffield/lazygit/pkg/commands" + "github.com/jesseduffield/lazygit/pkg/commands/models" + "github.com/jesseduffield/lazygit/pkg/gui/context" + "github.com/jesseduffield/lazygit/pkg/gui/patch_exploring" + "github.com/jesseduffield/lazygit/pkg/gui/types" +) + +type StagingHelper struct { + c *types.HelperCommon + git *commands.GitCommand + contexts *context.ContextTree +} + +func NewStagingHelper( + c *types.HelperCommon, + git *commands.GitCommand, + contexts *context.ContextTree, +) *StagingHelper { + return &StagingHelper{ + c: c, + git: git, + contexts: contexts, + } +} + +// NOTE: used from outside this file +func (self *StagingHelper) RefreshStagingPanel(focusOpts types.OnFocusOpts) error { + secondaryFocused := self.secondaryStagingFocused() + + mainSelectedLineIdx := -1 + secondarySelectedLineIdx := -1 + if focusOpts.ClickedViewLineIdx > 0 { + if secondaryFocused { + secondarySelectedLineIdx = focusOpts.ClickedViewLineIdx + } else { + mainSelectedLineIdx = focusOpts.ClickedViewLineIdx + } + } + + mainContext := self.contexts.Staging + secondaryContext := self.contexts.StagingSecondary + + var file *models.File + node := self.contexts.Files.GetSelected() + if node != nil { + file = node.File + } + + if file == nil || (!file.HasUnstagedChanges && !file.HasStagedChanges) { + return self.handleStagingEscape() + } + + mainDiff := self.git.WorkingTree.WorktreeFileDiff(file, true, false, false) + secondaryDiff := self.git.WorkingTree.WorktreeFileDiff(file, true, true, false) + + // grabbing locks here and releasing before we finish the function + // because pushing say the secondary context could mean entering this function + // again, and we don't want to have a deadlock + mainContext.GetMutex().Lock() + secondaryContext.GetMutex().Lock() + + mainContext.SetState( + patch_exploring.NewState(mainDiff, mainSelectedLineIdx, mainContext.GetState(), self.c.Log), + ) + + secondaryContext.SetState( + patch_exploring.NewState(secondaryDiff, secondarySelectedLineIdx, secondaryContext.GetState(), self.c.Log), + ) + + mainState := mainContext.GetState() + secondaryState := secondaryContext.GetState() + + mainContent := mainContext.GetContentToRender(!secondaryFocused) + secondaryContent := secondaryContext.GetContentToRender(secondaryFocused) + + mainContext.GetMutex().Unlock() + secondaryContext.GetMutex().Unlock() + + if mainState == nil && secondaryState == nil { + return self.handleStagingEscape() + } + + if mainState == nil && !secondaryFocused { + return self.c.PushContext(secondaryContext, focusOpts) + } + + if secondaryState == nil && secondaryFocused { + return self.c.PushContext(mainContext, focusOpts) + } + + if secondaryFocused { + self.contexts.StagingSecondary.FocusSelection() + } else { + self.contexts.Staging.FocusSelection() + } + + return self.c.RenderToMainViews(types.RefreshMainOpts{ + Pair: self.c.MainViewPairs().Staging, + Main: &types.ViewUpdateOpts{ + Task: types.NewRenderStringWithoutScrollTask(mainContent), + Title: self.c.Tr.UnstagedChanges, + }, + Secondary: &types.ViewUpdateOpts{ + Task: types.NewRenderStringWithoutScrollTask(secondaryContent), + Title: self.c.Tr.StagedChanges, + }, + }) +} + +func (self *StagingHelper) handleStagingEscape() error { + return self.c.PushContext(self.contexts.Files) +} + +func (self *StagingHelper) secondaryStagingFocused() bool { + return self.c.CurrentStaticContext().GetKey() == self.contexts.StagingSecondary.GetKey() +} diff --git a/pkg/gui/controllers/helpers/update_helper.go b/pkg/gui/controllers/helpers/update_helper.go new file mode 100644 index 000000000..21bf2ad83 --- /dev/null +++ b/pkg/gui/controllers/helpers/update_helper.go @@ -0,0 +1,98 @@ +package helpers + +import ( + "github.com/jesseduffield/lazygit/pkg/gui/types" + "github.com/jesseduffield/lazygit/pkg/updates" + "github.com/jesseduffield/lazygit/pkg/utils" +) + +type UpdateHelper struct { + c *types.HelperCommon + updater *updates.Updater +} + +func NewUpdateHelper(c *types.HelperCommon, updater *updates.Updater) *UpdateHelper { + return &UpdateHelper{ + c: c, + updater: updater, + } +} + +func (self *UpdateHelper) CheckForUpdateInBackground() error { + self.updater.CheckForNewUpdate(func(newVersion string, err error) error { + if err != nil { + // ignoring the error for now so that I'm not annoying users + self.c.Log.Error(err.Error()) + return nil + } + if newVersion == "" { + return nil + } + if self.c.UserConfig.Update.Method == "background" { + self.startUpdating(newVersion) + return nil + } + return self.showUpdatePrompt(newVersion) + }, false) + + return nil +} + +func (self *UpdateHelper) CheckForUpdateInForeground() error { + return self.c.WithWaitingStatus(self.c.Tr.CheckingForUpdates, func() error { + self.updater.CheckForNewUpdate(func(newVersion string, err error) error { + if err != nil { + return self.c.Error(err) + } + if newVersion == "" { + return self.c.ErrorMsg(self.c.Tr.FailedToRetrieveLatestVersionErr) + } + return self.showUpdatePrompt(newVersion) + }, true) + + return nil + }) +} + +func (self *UpdateHelper) startUpdating(newVersion string) { + _ = self.c.WithWaitingStatus(self.c.Tr.UpdateInProgressWaitingStatus, func() error { + self.c.State().SetUpdating(true) + err := self.updater.Update(newVersion) + return self.onUpdateFinish(err) + }) +} + +func (self *UpdateHelper) onUpdateFinish(err error) error { + self.c.State().SetUpdating(false) + self.c.OnUIThread(func() error { + self.c.SetViewContent(self.c.Views().AppStatus, "") + if err != nil { + errMessage := utils.ResolvePlaceholderString( + self.c.Tr.UpdateFailedErr, map[string]string{ + "errMessage": err.Error(), + }, + ) + return self.c.ErrorMsg(errMessage) + } + return self.c.Alert(self.c.Tr.UpdateCompletedTitle, self.c.Tr.UpdateCompleted) + }) + + return nil +} + +func (self *UpdateHelper) showUpdatePrompt(newVersion string) error { + message := utils.ResolvePlaceholderString( + self.c.Tr.UpdateAvailable, map[string]string{ + "newVersion": newVersion, + }, + ) + + return self.c.Confirm(types.ConfirmOpts{ + Title: self.c.Tr.UpdateAvailableTitle, + Prompt: message, + HandleConfirm: func() error { + self.startUpdating(newVersion) + return nil + }, + }) +} diff --git a/pkg/gui/controllers/helpers/view_helper.go b/pkg/gui/controllers/helpers/view_helper.go new file mode 100644 index 000000000..bbe7742cf --- /dev/null +++ b/pkg/gui/controllers/helpers/view_helper.go @@ -0,0 +1,33 @@ +package helpers + +import ( + "github.com/jesseduffield/lazygit/pkg/gui/context" + "github.com/jesseduffield/lazygit/pkg/gui/types" +) + +type ViewHelper struct { + c *types.HelperCommon + contexts *context.ContextTree +} + +func NewViewHelper(c *types.HelperCommon, contexts *context.ContextTree) *ViewHelper { + return &ViewHelper{ + c: c, + contexts: contexts, + } +} + +func (self *ViewHelper) ContextForView(viewName string) (types.Context, bool) { + view, err := self.c.GocuiGui().View(viewName) + if err != nil { + return nil, false + } + + for _, context := range self.contexts.Flatten() { + if context.GetViewName() == view.Name() { + return context, true + } + } + + return nil, false +} diff --git a/pkg/gui/controllers/helpers/window_helper.go b/pkg/gui/controllers/helpers/window_helper.go new file mode 100644 index 000000000..3b180ad80 --- /dev/null +++ b/pkg/gui/controllers/helpers/window_helper.go @@ -0,0 +1,138 @@ +package helpers + +import ( + "fmt" + + "github.com/jesseduffield/gocui" + "github.com/jesseduffield/lazygit/pkg/gui/context" + "github.com/jesseduffield/lazygit/pkg/gui/types" + "github.com/jesseduffield/lazygit/pkg/utils" + "github.com/samber/lo" +) + +type WindowHelper struct { + c *types.HelperCommon + viewHelper *ViewHelper + contexts *context.ContextTree +} + +func NewWindowHelper(c *types.HelperCommon, viewHelper *ViewHelper, contexts *context.ContextTree) *WindowHelper { + return &WindowHelper{ + c: c, + viewHelper: viewHelper, + contexts: contexts, + } +} + +// 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 that moves between windows + +func (self *WindowHelper) GetViewNameForWindow(window string) string { + viewName, ok := self.windowViewNameMap().Get(window) + if !ok { + panic(fmt.Sprintf("Viewname not found for window: %s", window)) + } + + return viewName +} + +func (self *WindowHelper) GetContextForWindow(window string) types.Context { + viewName := self.GetViewNameForWindow(window) + + context, ok := self.viewHelper.ContextForView(viewName) + if !ok { + panic("TODO: fix this") + } + + return context +} + +// for now all we actually care about is the context's view so we're storing that +func (self *WindowHelper) SetWindowContext(c types.Context) { + if c.IsTransient() { + self.resetWindowContext(c) + } + + self.windowViewNameMap().Set(c.GetWindowName(), c.GetViewName()) +} + +func (self *WindowHelper) windowViewNameMap() *utils.ThreadSafeMap[string, string] { + return self.c.State().GetRepoState().GetWindowViewNameMap() +} + +func (self *WindowHelper) CurrentWindow() string { + return self.c.CurrentContext().GetWindowName() +} + +// assumes the context's windowName has been set to the new window if necessary +func (self *WindowHelper) resetWindowContext(c types.Context) { + for _, windowName := range self.windowViewNameMap().Keys() { + viewName, ok := self.windowViewNameMap().Get(windowName) + if !ok { + continue + } + if viewName == c.GetViewName() && windowName != c.GetWindowName() { + for _, context := range self.contexts.Flatten() { + if context.GetKey() != c.GetKey() && context.GetWindowName() == windowName { + self.windowViewNameMap().Set(windowName, context.GetViewName()) + } + } + } + } +} + +// moves given context's view to the top of the window +func (self *WindowHelper) MoveToTopOfWindow(context types.Context) { + view := context.GetView() + if view == nil { + return + } + + window := context.GetWindowName() + + topView := self.TopViewInWindow(window) + + if view.Name() != topView.Name() { + if err := self.c.GocuiGui().SetViewOnTopOf(view.Name(), topView.Name()); err != nil { + self.c.Log.Error(err) + } + } +} + +func (self *WindowHelper) TopViewInWindow(windowName string) *gocui.View { + // now I need to find all views in that same window, via contexts. And I guess then I need to find the index of the highest view in that list. + viewNamesInWindow := self.viewNamesInWindow(windowName) + + // The views list is ordered highest-last, so we're grabbing the last view of the window + var topView *gocui.View + for _, currentView := range self.c.GocuiGui().Views() { + if lo.Contains(viewNamesInWindow, currentView.Name()) { + topView = currentView + } + } + + return topView +} + +func (self *WindowHelper) viewNamesInWindow(windowName string) []string { + result := []string{} + for _, context := range self.contexts.Flatten() { + if context.GetWindowName() == windowName { + result = append(result, context.GetViewName()) + } + } + + return result +} + +func (self *WindowHelper) WindowForView(viewName string) string { + context, ok := self.viewHelper.ContextForView(viewName) + if !ok { + panic("todo: deal with this") + } + + return context.GetWindowName() +} diff --git a/pkg/gui/controllers/local_commits_controller.go b/pkg/gui/controllers/local_commits_controller.go index 35e314357..d3fdc4e50 100644 --- a/pkg/gui/controllers/local_commits_controller.go +++ b/pkg/gui/controllers/local_commits_controller.go @@ -12,6 +12,9 @@ import ( "github.com/samber/lo" ) +// after selecting the 200th commit, we'll load in all the rest +const COMMIT_THRESHOLD = 200 + type ( PullFilesFn func() error ) @@ -150,6 +153,50 @@ func (self *LocalCommitsController) GetKeybindings(opts types.KeybindingsOpts) [ return bindings } +func (self *LocalCommitsController) GetOnRenderToMain() func() error { + return func() error { + return self.helpers.Diff.WithDiffModeCheck(func() error { + var task types.UpdateTask + commit := self.context().GetSelected() + if commit == nil { + task = types.NewRenderStringTask(self.c.Tr.NoCommitsThisBranch) + } else if commit.Action == todo.UpdateRef { + task = types.NewRenderStringTask( + utils.ResolvePlaceholderString( + self.c.Tr.UpdateRefHere, + map[string]string{ + "ref": commit.Name, + })) + } else { + cmdObj := self.c.Git().Commit.ShowCmdObj(commit.Sha, self.c.Modes().Filtering.GetPath(), self.c.State().GetIgnoreWhitespaceInDiffView()) + task = types.NewRunPtyTask(cmdObj.GetCmd()) + } + + return self.c.RenderToMainViews(types.RefreshMainOpts{ + Pair: self.c.MainViewPairs().Normal, + Main: &types.ViewUpdateOpts{ + Title: "Patch", + Task: task, + }, + Secondary: secondaryPatchPanelUpdateOpts(self.c), + }) + }) + } +} + +func secondaryPatchPanelUpdateOpts(c *types.HelperCommon) *types.ViewUpdateOpts { + if c.Git().Patch.PatchBuilder.Active() { + patch := c.Git().Patch.PatchBuilder.RenderAggregatedPatch(false) + + return &types.ViewUpdateOpts{ + Task: types.NewRenderStringWithoutScrollTask(patch), + Title: c.Tr.CustomPatch, + } + } + + return nil +} + func (self *LocalCommitsController) squashDown(commit *models.Commit) error { if self.context().GetSelectedLineIdx() >= len(self.model.Commits)-1 { return self.c.ErrorMsg(self.c.Tr.CannotSquashOrFixupFirstCommit) @@ -753,6 +800,22 @@ func (self *LocalCommitsController) checkSelected(callback func(*models.Commit) } } +func (self *LocalCommitsController) GetOnFocus() func(types.OnFocusOpts) error { + return func(types.OnFocusOpts) error { + context := self.context() + if context.GetSelectedLineIdx() > COMMIT_THRESHOLD && context.GetLimitCommits() { + context.SetLimitCommits(false) + go utils.Safe(func() { + if err := self.c.Refresh(types.RefreshOptions{Scope: []types.RefreshableView{types.COMMITS}}); err != nil { + _ = self.c.Error(err) + } + }) + } + + return nil + } +} + func (self *LocalCommitsController) Context() types.Context { return self.context() } diff --git a/pkg/gui/controllers/menu_controller.go b/pkg/gui/controllers/menu_controller.go index 6700fa99d..6b2b6b736 100644 --- a/pkg/gui/controllers/menu_controller.go +++ b/pkg/gui/controllers/menu_controller.go @@ -44,6 +44,14 @@ func (self *MenuController) GetOnClick() func() error { return self.press } +func (self *MenuController) GetOnFocus() func(types.OnFocusOpts) error { + return func(types.OnFocusOpts) error { + selectedMenuItem := self.context().GetSelected() + self.c.Views().Tooltip.SetContent(selectedMenuItem.Tooltip) + return nil + } +} + func (self *MenuController) press() error { return self.context().OnMenuPress(self.context().GetSelected()) } diff --git a/pkg/gui/controllers/merge_conflicts_controller.go b/pkg/gui/controllers/merge_conflicts_controller.go index de282d4c9..f4a96c2fb 100644 --- a/pkg/gui/controllers/merge_conflicts_controller.go +++ b/pkg/gui/controllers/merge_conflicts_controller.go @@ -134,6 +134,24 @@ func (self *MergeConflictsController) GetMouseKeybindings(opts types.Keybindings } } +func (self *MergeConflictsController) GetOnFocus() func(types.OnFocusOpts) error { + return func(types.OnFocusOpts) error { + self.c.Views().MergeConflicts.Wrap = false + + return self.helpers.MergeConflicts.Render(true) + } +} + +func (self *MergeConflictsController) GetOnFocusLost() func(types.OnFocusLostOpts) error { + return func(types.OnFocusLostOpts) error { + self.context().SetUserScrolling(false) + self.context().GetState().ResetConflictSelection() + self.c.Views().MergeConflicts.Wrap = true + + return nil + } +} + func (self *MergeConflictsController) HandleScrollUp() error { self.context().SetUserScrolling(true) self.context().GetViewTrait().ScrollUp(self.c.UserConfig.Gui.ScrollHeight) diff --git a/pkg/gui/controllers/reflog_commits_controller.go b/pkg/gui/controllers/reflog_commits_controller.go new file mode 100644 index 000000000..44cc50ab9 --- /dev/null +++ b/pkg/gui/controllers/reflog_commits_controller.go @@ -0,0 +1,53 @@ +package controllers + +import ( + "github.com/jesseduffield/lazygit/pkg/gui/context" + "github.com/jesseduffield/lazygit/pkg/gui/types" +) + +type ReflogCommitsController struct { + baseController + *controllerCommon + context *context.ReflogCommitsContext +} + +var _ types.IController = &ReflogCommitsController{} + +func NewReflogCommitsController( + common *controllerCommon, + context *context.ReflogCommitsContext, +) *ReflogCommitsController { + return &ReflogCommitsController{ + baseController: baseController{}, + controllerCommon: common, + context: context, + } +} + +func (self *ReflogCommitsController) Context() types.Context { + return self.context +} + +func (self *ReflogCommitsController) GetOnRenderToMain() func() error { + return func() error { + return self.helpers.Diff.WithDiffModeCheck(func() error { + commit := self.context.GetSelected() + var task types.UpdateTask + if commit == nil { + task = types.NewRenderStringTask("No reflog history") + } else { + cmdObj := self.c.Git().Commit.ShowCmdObj(commit.Sha, self.c.Modes().Filtering.GetPath(), self.c.State().GetIgnoreWhitespaceInDiffView()) + + task = types.NewRunPtyTask(cmdObj.GetCmd()) + } + + return self.c.RenderToMainViews(types.RefreshMainOpts{ + Pair: self.c.MainViewPairs().Normal, + Main: &types.ViewUpdateOpts{ + Title: "Reflog Entry", + Task: task, + }, + }) + }) + } +} diff --git a/pkg/gui/controllers/remote_branches_controller.go b/pkg/gui/controllers/remote_branches_controller.go index dcedde8c0..0f4efbbff 100644 --- a/pkg/gui/controllers/remote_branches_controller.go +++ b/pkg/gui/controllers/remote_branches_controller.go @@ -73,6 +73,29 @@ func (self *RemoteBranchesController) GetKeybindings(opts types.KeybindingsOpts) } } +func (self *RemoteBranchesController) GetOnRenderToMain() func() error { + return func() error { + return self.helpers.Diff.WithDiffModeCheck(func() error { + var task types.UpdateTask + remoteBranch := self.context().GetSelected() + if remoteBranch == nil { + task = types.NewRenderStringTask("No branches for this remote") + } else { + cmdObj := self.git.Branch.GetGraphCmdObj(remoteBranch.FullRefName()) + task = types.NewRunCommandTask(cmdObj.GetCmd()) + } + + return self.c.RenderToMainViews(types.RefreshMainOpts{ + Pair: self.c.MainViewPairs().Normal, + Main: &types.ViewUpdateOpts{ + Title: "Remote Branch", + Task: task, + }, + }) + }) + } +} + func (self *RemoteBranchesController) Context() types.Context { return self.context() } diff --git a/pkg/gui/controllers/remotes_controller.go b/pkg/gui/controllers/remotes_controller.go index 03427b9b7..2562474bf 100644 --- a/pkg/gui/controllers/remotes_controller.go +++ b/pkg/gui/controllers/remotes_controller.go @@ -1,8 +1,12 @@ package controllers import ( + "fmt" + "strings" + "github.com/jesseduffield/lazygit/pkg/commands/models" "github.com/jesseduffield/lazygit/pkg/gui/context" + "github.com/jesseduffield/lazygit/pkg/gui/style" "github.com/jesseduffield/lazygit/pkg/gui/types" "github.com/jesseduffield/lazygit/pkg/utils" ) @@ -60,6 +64,28 @@ func (self *RemotesController) GetKeybindings(opts types.KeybindingsOpts) []*typ return bindings } +func (self *RemotesController) GetOnRenderToMain() func() error { + return func() error { + return self.helpers.Diff.WithDiffModeCheck(func() error { + var task types.UpdateTask + remote := self.context.GetSelected() + if remote == nil { + task = types.NewRenderStringTask("No remotes") + } else { + task = types.NewRenderStringTask(fmt.Sprintf("%s\nUrls:\n%s", style.FgGreen.Sprint(remote.Name), strings.Join(remote.Urls, "\n"))) + } + + return self.c.RenderToMainViews(types.RefreshMainOpts{ + Pair: self.c.MainViewPairs().Normal, + Main: &types.ViewUpdateOpts{ + Title: "Remote", + Task: task, + }, + }) + }) + } +} + func (self *RemotesController) GetOnClick() func() error { return self.checkSelected(self.enter) } diff --git a/pkg/gui/controllers/snake_controller.go b/pkg/gui/controllers/snake_controller.go index 4217878e3..9524666dc 100644 --- a/pkg/gui/controllers/snake_controller.go +++ b/pkg/gui/controllers/snake_controller.go @@ -8,20 +8,16 @@ import ( type SnakeController struct { baseController *controllerCommon - - getGame func() *snake.Game } var _ types.IController = &SnakeController{} func NewSnakeController( common *controllerCommon, - getGame func() *snake.Game, ) *SnakeController { return &SnakeController{ baseController: baseController{}, controllerCommon: common, - getGame: getGame, } } @@ -56,9 +52,24 @@ func (self *SnakeController) Context() types.Context { return self.contexts.Snake } +func (self *SnakeController) GetOnFocus() func(types.OnFocusOpts) error { + return func(types.OnFocusOpts) error { + self.helpers.Snake.StartGame() + return nil + } +} + +func (self *SnakeController) GetOnFocusLost() func(types.OnFocusLostOpts) error { + return func(types.OnFocusLostOpts) error { + self.helpers.Snake.ExitGame() + self.helpers.Window.MoveToTopOfWindow(self.contexts.Submodules) + return nil + } +} + func (self *SnakeController) SetDirection(direction snake.Direction) func() error { return func() error { - self.getGame().SetDirection(direction) + self.helpers.Snake.SetDirection(direction) return nil } } diff --git a/pkg/gui/controllers/stash_controller.go b/pkg/gui/controllers/stash_controller.go index 68a121931..8eabfcc39 100644 --- a/pkg/gui/controllers/stash_controller.go +++ b/pkg/gui/controllers/stash_controller.go @@ -55,6 +55,28 @@ func (self *StashController) GetKeybindings(opts types.KeybindingsOpts) []*types return bindings } +func (self *StashController) GetOnRenderToMain() func() error { + return func() error { + return self.helpers.Diff.WithDiffModeCheck(func() error { + var task types.UpdateTask + stashEntry := self.context().GetSelected() + if stashEntry == nil { + task = types.NewRenderStringTask(self.c.Tr.NoStashEntries) + } else { + task = types.NewRunPtyTask(self.git.Stash.ShowStashEntryCmdObj(stashEntry.Index).GetCmd()) + } + + return self.c.RenderToMainViews(types.RefreshMainOpts{ + Pair: self.c.MainViewPairs().Normal, + Main: &types.ViewUpdateOpts{ + Title: "Stash", + Task: task, + }, + }) + }) + } +} + func (self *StashController) checkSelected(callback func(*models.StashEntry) error) func() error { return func() error { item := self.context().GetSelected() diff --git a/pkg/gui/controllers/status_controller.go b/pkg/gui/controllers/status_controller.go new file mode 100644 index 000000000..f89f7e16f --- /dev/null +++ b/pkg/gui/controllers/status_controller.go @@ -0,0 +1,198 @@ +package controllers + +import ( + "errors" + "fmt" + "strings" + + "github.com/jesseduffield/generics/slices" + "github.com/jesseduffield/lazygit/pkg/commands/types/enums" + "github.com/jesseduffield/lazygit/pkg/constants" + "github.com/jesseduffield/lazygit/pkg/gui/presentation" + "github.com/jesseduffield/lazygit/pkg/gui/style" + "github.com/jesseduffield/lazygit/pkg/gui/types" + "github.com/jesseduffield/lazygit/pkg/utils" +) + +type StatusController struct { + baseController + *controllerCommon +} + +var _ types.IController = &StatusController{} + +func NewStatusController( + common *controllerCommon, +) *StatusController { + return &StatusController{ + baseController: baseController{}, + controllerCommon: common, + } +} + +func (self *StatusController) GetKeybindings(opts types.KeybindingsOpts) []*types.Binding { + bindings := []*types.Binding{ + { + Key: opts.GetKey(opts.Config.Universal.OpenFile), + Handler: self.openConfig, + Description: self.c.Tr.OpenConfig, + }, + { + Key: opts.GetKey(opts.Config.Universal.Edit), + Handler: self.editConfig, + Description: self.c.Tr.EditConfig, + }, + { + Key: opts.GetKey(opts.Config.Status.CheckForUpdate), + Handler: self.handleCheckForUpdate, + Description: self.c.Tr.LcCheckForUpdate, + }, + { + Key: opts.GetKey(opts.Config.Status.RecentRepos), + Handler: self.helpers.Repos.CreateRecentReposMenu, + Description: self.c.Tr.SwitchRepo, + }, + { + Key: opts.GetKey(opts.Config.Status.AllBranchesLogGraph), + Handler: self.showAllBranchLogs, + Description: self.c.Tr.LcAllBranchesLogGraph, + }, + } + + return bindings +} + +func (self *StatusController) GetOnRenderToMain() func() error { + return func() error { + dashboardString := strings.Join( + []string{ + lazygitTitle(), + "Copyright 2022 Jesse Duffield", + fmt.Sprintf("Keybindings: %s", constants.Links.Docs.Keybindings), + fmt.Sprintf("Config Options: %s", constants.Links.Docs.Config), + fmt.Sprintf("Tutorial: %s", constants.Links.Docs.Tutorial), + fmt.Sprintf("Raise an Issue: %s", constants.Links.Issues), + fmt.Sprintf("Release Notes: %s", constants.Links.Releases), + style.FgMagenta.Sprintf("Become a sponsor: %s", constants.Links.Donate), // caffeine ain't free + }, "\n\n") + + return self.c.RenderToMainViews(types.RefreshMainOpts{ + Pair: self.c.MainViewPairs().Normal, + Main: &types.ViewUpdateOpts{ + Title: self.c.Tr.StatusTitle, + Task: types.NewRenderStringTask(dashboardString), + }, + }) + } +} + +func (self *StatusController) GetOnClick() func() error { + return self.onClick +} + +func (self *StatusController) Context() types.Context { + return self.contexts.Status +} + +func (self *StatusController) onClick() error { + // TODO: move into some abstraction (status is currently not a listViewContext where a lot of this code lives) + currentBranch := self.helpers.Refs.GetCheckedOutRef() + if currentBranch == nil { + // need to wait for branches to refresh + return nil + } + + if err := self.c.PushContext(self.Context()); err != nil { + return err + } + + cx, _ := self.c.Views().Status.Cursor() + upstreamStatus := presentation.BranchStatus(currentBranch, self.c.Tr) + repoName := utils.GetCurrentRepoName() + workingTreeState := self.git.Status.WorkingTreeState() + switch workingTreeState { + case enums.REBASE_MODE_REBASING, enums.REBASE_MODE_MERGING: + workingTreeStatus := fmt.Sprintf("(%s)", presentation.FormatWorkingTreeState(workingTreeState)) + if cursorInSubstring(cx, upstreamStatus+" ", workingTreeStatus) { + return self.helpers.MergeAndRebase.CreateRebaseOptionsMenu() + } + if cursorInSubstring(cx, upstreamStatus+" "+workingTreeStatus+" ", repoName) { + return self.helpers.Repos.CreateRecentReposMenu() + } + default: + if cursorInSubstring(cx, upstreamStatus+" ", repoName) { + return self.helpers.Repos.CreateRecentReposMenu() + } + } + + return nil +} + +func runeCount(str string) int { + return len([]rune(str)) +} + +func cursorInSubstring(cx int, prefix string, substring string) bool { + return cx >= runeCount(prefix) && cx < runeCount(prefix+substring) +} + +func lazygitTitle() string { + return ` + _ _ _ + | | (_) | + | | __ _ _____ _ __ _ _| |_ + | |/ _` + "`" + ` |_ / | | |/ _` + "`" + ` | | __| + | | (_| |/ /| |_| | (_| | | |_ + |_|\__,_/___|\__, |\__, |_|\__| + __/ | __/ | + |___/ |___/ ` +} + +func (self *StatusController) askForConfigFile(action func(file string) error) error { + confPaths := self.c.GetConfig().GetUserConfigPaths() + switch len(confPaths) { + case 0: + return errors.New(self.c.Tr.NoConfigFileFoundErr) + case 1: + return action(confPaths[0]) + default: + menuItems := slices.Map(confPaths, func(path string) *types.MenuItem { + return &types.MenuItem{ + Label: path, + OnPress: func() error { + return action(path) + }, + } + }) + + return self.c.Menu(types.CreateMenuOptions{ + Title: self.c.Tr.SelectConfigFile, + Items: menuItems, + }) + } +} + +func (self *StatusController) openConfig() error { + return self.askForConfigFile(self.helpers.Files.OpenFile) +} + +func (self *StatusController) editConfig() error { + return self.askForConfigFile(self.helpers.Files.EditFile) +} + +func (self *StatusController) showAllBranchLogs() error { + cmdObj := self.git.Branch.AllBranchesLogCmdObj() + task := types.NewRunPtyTask(cmdObj.GetCmd()) + + return self.c.RenderToMainViews(types.RefreshMainOpts{ + Pair: self.c.MainViewPairs().Normal, + Main: &types.ViewUpdateOpts{ + Title: self.c.Tr.LogTitle, + Task: task, + }, + }) +} + +func (self *StatusController) handleCheckForUpdate() error { + return self.helpers.Update.CheckForUpdateInForeground() +} diff --git a/pkg/gui/controllers/sub_commits_controller.go b/pkg/gui/controllers/sub_commits_controller.go new file mode 100644 index 000000000..d17042e47 --- /dev/null +++ b/pkg/gui/controllers/sub_commits_controller.go @@ -0,0 +1,70 @@ +package controllers + +import ( + "github.com/jesseduffield/lazygit/pkg/gui/context" + "github.com/jesseduffield/lazygit/pkg/gui/types" + "github.com/jesseduffield/lazygit/pkg/utils" +) + +type SubCommitsController struct { + baseController + *controllerCommon + context *context.SubCommitsContext +} + +var _ types.IController = &SubCommitsController{} + +func NewSubCommitsController( + common *controllerCommon, + context *context.SubCommitsContext, +) *SubCommitsController { + return &SubCommitsController{ + baseController: baseController{}, + controllerCommon: common, + context: context, + } +} + +func (self *SubCommitsController) Context() types.Context { + return self.context +} + +func (self *SubCommitsController) GetOnRenderToMain() func() error { + return func() error { + return self.helpers.Diff.WithDiffModeCheck(func() error { + commit := self.context.GetSelected() + var task types.UpdateTask + if commit == nil { + task = types.NewRenderStringTask("No commits") + } else { + cmdObj := self.git.Commit.ShowCmdObj(commit.Sha, self.modes.Filtering.GetPath(), self.c.State().GetIgnoreWhitespaceInDiffView()) + + task = types.NewRunPtyTask(cmdObj.GetCmd()) + } + + return self.c.RenderToMainViews(types.RefreshMainOpts{ + Pair: self.c.MainViewPairs().Normal, + Main: &types.ViewUpdateOpts{ + Title: "Commit", + Task: task, + }, + }) + }) + } +} + +func (self *SubCommitsController) GetOnFocus() func() error { + return func() error { + context := self.context + if context.GetSelectedLineIdx() > COMMIT_THRESHOLD && context.GetLimitCommits() { + context.SetLimitCommits(false) + go utils.Safe(func() { + if err := self.c.Refresh(types.RefreshOptions{Scope: []types.RefreshableView{types.SUB_COMMITS}}); err != nil { + _ = self.c.Error(err) + } + }) + } + + return nil + } +} diff --git a/pkg/gui/controllers/submodules_controller.go b/pkg/gui/controllers/submodules_controller.go index e6a6f8d17..745c17b36 100644 --- a/pkg/gui/controllers/submodules_controller.go +++ b/pkg/gui/controllers/submodules_controller.go @@ -14,20 +14,16 @@ import ( type SubmodulesController struct { baseController *controllerCommon - - enterSubmodule func(submodule *models.SubmoduleConfig) error } var _ types.IController = &SubmodulesController{} func NewSubmodulesController( controllerCommon *controllerCommon, - enterSubmodule func(submodule *models.SubmoduleConfig) error, ) *SubmodulesController { return &SubmodulesController{ baseController: baseController{}, controllerCommon: controllerCommon, - enterSubmodule: enterSubmodule, } } @@ -81,8 +77,43 @@ func (self *SubmodulesController) GetOnClick() func() error { return self.checkSelected(self.enter) } +func (self *SubmodulesController) GetOnRenderToMain() func() error { + return func() error { + return self.helpers.Diff.WithDiffModeCheck(func() error { + var task types.UpdateTask + submodule := self.context().GetSelected() + if submodule == nil { + task = types.NewRenderStringTask("No submodules") + } else { + prefix := fmt.Sprintf( + "Name: %s\nPath: %s\nUrl: %s\n\n", + style.FgGreen.Sprint(submodule.Name), + style.FgYellow.Sprint(submodule.Path), + style.FgCyan.Sprint(submodule.Url), + ) + + file := self.helpers.WorkingTree.FileForSubmodule(submodule) + if file == nil { + task = types.NewRenderStringTask(prefix) + } else { + cmdObj := self.git.WorkingTree.WorktreeFileDiffCmdObj(file, false, !file.HasUnstagedChanges && file.HasStagedChanges, self.c.State().GetIgnoreWhitespaceInDiffView()) + task = types.NewRunCommandTaskWithPrefix(cmdObj.GetCmd(), prefix) + } + } + + return self.c.RenderToMainViews(types.RefreshMainOpts{ + Pair: self.c.MainViewPairs().Normal, + Main: &types.ViewUpdateOpts{ + Title: "Submodule", + Task: task, + }, + }) + }) + } +} + func (self *SubmodulesController) enter(submodule *models.SubmoduleConfig) error { - return self.enterSubmodule(submodule) + return self.helpers.Repos.EnterSubmodule(submodule) } func (self *SubmodulesController) add() error { diff --git a/pkg/gui/controllers/suggestions_controller.go b/pkg/gui/controllers/suggestions_controller.go new file mode 100644 index 000000000..aaa4c8647 --- /dev/null +++ b/pkg/gui/controllers/suggestions_controller.go @@ -0,0 +1,43 @@ +package controllers + +import ( + "github.com/jesseduffield/lazygit/pkg/gui/context" + "github.com/jesseduffield/lazygit/pkg/gui/types" +) + +type SuggestionsController struct { + baseController + *controllerCommon +} + +var _ types.IController = &SuggestionsController{} + +func NewSuggestionsController( + common *controllerCommon, +) *SuggestionsController { + return &SuggestionsController{ + baseController: baseController{}, + controllerCommon: common, + } +} + +func (self *SuggestionsController) GetKeybindings(opts types.KeybindingsOpts) []*types.Binding { + bindings := []*types.Binding{} + + return bindings +} + +func (self *SuggestionsController) GetOnFocusLost() func(types.OnFocusLostOpts) error { + return func(types.OnFocusLostOpts) error { + deactivateConfirmationPrompt + return nil + } +} + +func (self *SuggestionsController) Context() types.Context { + return self.context() +} + +func (self *SuggestionsController) context() *context.SuggestionsContext { + return self.contexts.Suggestions +} diff --git a/pkg/gui/controllers/switch_to_diff_files_controller.go b/pkg/gui/controllers/switch_to_diff_files_controller.go index 275a5ebb2..fa6ccdc0d 100644 --- a/pkg/gui/controllers/switch_to_diff_files_controller.go +++ b/pkg/gui/controllers/switch_to_diff_files_controller.go @@ -1,6 +1,7 @@ package controllers import ( + "github.com/jesseduffield/lazygit/pkg/gui/context" "github.com/jesseduffield/lazygit/pkg/gui/types" ) @@ -17,20 +18,20 @@ type CanSwitchToDiffFiles interface { type SwitchToDiffFilesController struct { baseController *controllerCommon - context CanSwitchToDiffFiles - viewFiles func(SwitchToCommitFilesContextOpts) error + context CanSwitchToDiffFiles + diffFilesContext *context.CommitFilesContext } func NewSwitchToDiffFilesController( controllerCommon *controllerCommon, - viewFiles func(SwitchToCommitFilesContextOpts) error, context CanSwitchToDiffFiles, + diffFilesContext *context.CommitFilesContext, ) *SwitchToDiffFilesController { return &SwitchToDiffFilesController{ baseController: baseController{}, controllerCommon: controllerCommon, context: context, - viewFiles: viewFiles, + diffFilesContext: diffFilesContext, } } @@ -72,3 +73,22 @@ func (self *SwitchToDiffFilesController) enter(ref types.Ref) error { func (self *SwitchToDiffFilesController) Context() types.Context { return self.context } + +func (self *SwitchToDiffFilesController) viewFiles(opts SwitchToCommitFilesContextOpts) error { + diffFilesContext := self.diffFilesContext + + diffFilesContext.SetSelectedLineIdx(0) + diffFilesContext.SetRef(opts.Ref) + diffFilesContext.SetTitleRef(opts.Ref.Description()) + diffFilesContext.SetCanRebase(opts.CanRebase) + diffFilesContext.SetParentContext(opts.Context) + diffFilesContext.SetWindowName(opts.Context.GetWindowName()) + + if err := self.c.Refresh(types.RefreshOptions{ + Scope: []types.RefreshableView{types.COMMIT_FILES}, + }); err != nil { + return err + } + + return self.c.PushContext(diffFilesContext) +} diff --git a/pkg/gui/controllers/tags_controller.go b/pkg/gui/controllers/tags_controller.go index f4b23374c..a25c42f6a 100644 --- a/pkg/gui/controllers/tags_controller.go +++ b/pkg/gui/controllers/tags_controller.go @@ -56,6 +56,29 @@ func (self *TagsController) GetKeybindings(opts types.KeybindingsOpts) []*types. return bindings } +func (self *TagsController) GetOnRenderToMain() func() error { + return func() error { + return self.helpers.Diff.WithDiffModeCheck(func() error { + var task types.UpdateTask + tag := self.context().GetSelected() + if tag == nil { + task = types.NewRenderStringTask("No tags") + } else { + cmdObj := self.git.Branch.GetGraphCmdObj(tag.FullRefName()) + task = types.NewRunCommandTask(cmdObj.GetCmd()) + } + + return self.c.RenderToMainViews(types.RefreshMainOpts{ + Pair: self.c.MainViewPairs().Normal, + Main: &types.ViewUpdateOpts{ + Title: "Tag", + Task: task, + }, + }) + }) + } +} + func (self *TagsController) checkout(tag *models.Tag) error { self.c.LogAction(self.c.Tr.Actions.CheckoutTag) if err := self.helpers.Refs.CheckoutRef(tag.Name, types.CheckoutRefOptions{}); err != nil { diff --git a/pkg/gui/custom_patch_options_panel.go b/pkg/gui/custom_patch_options_panel.go index a25fc9580..837909deb 100644 --- a/pkg/gui/custom_patch_options_panel.go +++ b/pkg/gui/custom_patch_options_panel.go @@ -49,8 +49,8 @@ func (gui *Gui) handleCreatePatchOptionsMenu() error { }, }...) - if gui.currentContext().GetKey() == gui.State.Contexts.LocalCommits.GetKey() { - selectedCommit := gui.getSelectedLocalCommit() + if gui.c.CurrentContext().GetKey() == gui.State.Contexts.LocalCommits.GetKey() { + selectedCommit := gui.State.Contexts.LocalCommits.GetSelected() if selectedCommit != nil && gui.git.Patch.PatchBuilder.To != selectedCommit.Sha { // adding this option to index 1 menuItems = append( @@ -97,7 +97,7 @@ func (gui *Gui) validateNormalWorkingTreeState() (bool, error) { } func (gui *Gui) returnFocusFromPatchExplorerIfNecessary() error { - if gui.currentContext().GetKey() == gui.State.Contexts.CustomPatchBuilder.GetKey() { + if gui.c.CurrentContext().GetKey() == gui.State.Contexts.CustomPatchBuilder.GetKey() { return gui.helpers.PatchBuilding.Escape() } return nil diff --git a/pkg/gui/diff_context_size_test.go b/pkg/gui/diff_context_size_test.go deleted file mode 100644 index 62a784380..000000000 --- a/pkg/gui/diff_context_size_test.go +++ /dev/null @@ -1,182 +0,0 @@ -package gui - -// const diffForTest = `diff --git a/pkg/gui/diff_context_size.go b/pkg/gui/diff_context_size.go -// index 0da0a982..742b7dcf 100644 -// --- a/pkg/gui/diff_context_size.go -// +++ b/pkg/gui/diff_context_size.go -// @@ -9,12 +9,12 @@ func getRefreshFunction(gui *Gui) func()error { -// } -// } else if key == context.MAIN_STAGING_CONTEXT_KEY { -// return func() error { -// - selectedLine := gui.Views.Secondary.SelectedLineIdx() -// + selectedLine := gui.State.Panels.LineByLine.GetSelectedLineIdx() -// return gui.handleRefreshStagingPanel(false, selectedLine) -// } -// } else if key == context.MAIN_PATCH_BUILDING_CONTEXT_KEY { -// ` - -// func setupGuiForTest(gui *Gui) { -// gui.g = &gocui.Gui{} -// gui.Views.Main, _ = gui.prepareView("main") -// gui.Views.Secondary, _ = gui.prepareView("secondary") -// gui.Views.Options, _ = gui.prepareView("options") -// gui.git.Patch.PatchManager = &patch.PatchManager{} -// _, _ = gui.refreshLineByLinePanel(diffForTest, "", false, 11) -// } - -// func TestIncreasesContextInDiffViewByOneInContextWithDiff(t *testing.T) { -// contexts := []func(gui *Gui) types.Context{ -// func(gui *Gui) types.Context { return gui.State.Contexts.Files }, -// func(gui *Gui) types.Context { return gui.State.Contexts.BranchCommits }, -// func(gui *Gui) types.Context { return gui.State.Contexts.CommitFiles }, -// func(gui *Gui) types.Context { return gui.State.Contexts.Stash }, -// func(gui *Gui) types.Context { return gui.State.Contexts.Staging }, -// func(gui *Gui) types.Context { return gui.State.Contexts.PatchBuilding }, -// func(gui *Gui) types.Context { return gui.State.Contexts.SubCommits }, -// } - -// for _, c := range contexts { -// gui := NewDummyGui() -// context := c(gui) -// setupGuiForTest(gui) -// gui.c.UserConfig.Git.DiffContextSize = 1 -// _ = gui.c.PushContext(context) - -// _ = gui.IncreaseContextInDiffView() - -// assert.Equal(t, 2, gui.c.UserConfig.Git.DiffContextSize, string(context.GetKey())) -// } -// } - -// func TestDoesntIncreaseContextInDiffViewInContextWithoutDiff(t *testing.T) { -// contexts := []func(gui *Gui) types.Context{ -// func(gui *Gui) types.Context { return gui.State.Contexts.Status }, -// func(gui *Gui) types.Context { return gui.State.Contexts.Submodules }, -// func(gui *Gui) types.Context { return gui.State.Contexts.Remotes }, -// func(gui *Gui) types.Context { return gui.State.Contexts.Normal }, -// func(gui *Gui) types.Context { return gui.State.Contexts.ReflogCommits }, -// func(gui *Gui) types.Context { return gui.State.Contexts.RemoteBranches }, -// func(gui *Gui) types.Context { return gui.State.Contexts.Tags }, -// // not testing this because it will kick straight back to the files context -// // upon pushing the context -// // func(gui *Gui) types.Context { return gui.State.Contexts.Merging }, -// func(gui *Gui) types.Context { return gui.State.Contexts.CommandLog }, -// } - -// for _, c := range contexts { -// gui := NewDummyGui() -// context := c(gui) -// setupGuiForTest(gui) -// gui.c.UserConfig.Git.DiffContextSize = 1 -// _ = gui.c.PushContext(context) - -// _ = gui.IncreaseContextInDiffView() - -// assert.Equal(t, 1, gui.c.UserConfig.Git.DiffContextSize, string(context.GetKey())) -// } -// } - -// func TestDecreasesContextInDiffViewByOneInContextWithDiff(t *testing.T) { -// contexts := []func(gui *Gui) types.Context{ -// func(gui *Gui) types.Context { return gui.State.Contexts.Files }, -// func(gui *Gui) types.Context { return gui.State.Contexts.BranchCommits }, -// func(gui *Gui) types.Context { return gui.State.Contexts.CommitFiles }, -// func(gui *Gui) types.Context { return gui.State.Contexts.Stash }, -// func(gui *Gui) types.Context { return gui.State.Contexts.Staging }, -// func(gui *Gui) types.Context { return gui.State.Contexts.PatchBuilding }, -// func(gui *Gui) types.Context { return gui.State.Contexts.SubCommits }, -// } - -// for _, c := range contexts { -// gui := NewDummyGui() -// context := c(gui) -// setupGuiForTest(gui) -// gui.c.UserConfig.Git.DiffContextSize = 2 -// _ = gui.c.PushContext(context) - -// _ = gui.DecreaseContextInDiffView() - -// assert.Equal(t, 1, gui.c.UserConfig.Git.DiffContextSize, string(context.GetKey())) -// } -// } - -// func TestDoesntDecreaseContextInDiffViewInContextWithoutDiff(t *testing.T) { -// contexts := []func(gui *Gui) types.Context{ -// func(gui *Gui) types.Context { return gui.State.Contexts.Status }, -// func(gui *Gui) types.Context { return gui.State.Contexts.Submodules }, -// func(gui *Gui) types.Context { return gui.State.Contexts.Remotes }, -// func(gui *Gui) types.Context { return gui.State.Contexts.Normal }, -// func(gui *Gui) types.Context { return gui.State.Contexts.ReflogCommits }, -// func(gui *Gui) types.Context { return gui.State.Contexts.RemoteBranches }, -// func(gui *Gui) types.Context { return gui.State.Contexts.Tags }, -// // not testing this because it will kick straight back to the files context -// // upon pushing the context -// // func(gui *Gui) types.Context { return gui.State.Contexts.Merging }, -// func(gui *Gui) types.Context { return gui.State.Contexts.CommandLog }, -// } - -// for _, c := range contexts { -// gui := NewDummyGui() -// context := c(gui) -// setupGuiForTest(gui) -// gui.c.UserConfig.Git.DiffContextSize = 2 -// _ = gui.c.PushContext(context) - -// _ = gui.DecreaseContextInDiffView() - -// assert.Equal(t, 2, gui.c.UserConfig.Git.DiffContextSize, string(context.GetKey())) -// } -// } - -// func TestDoesntIncreaseContextInDiffViewInContextWhenInPatchBuildingMode(t *testing.T) { -// gui := NewDummyGui() -// setupGuiForTest(gui) -// gui.c.UserConfig.Git.DiffContextSize = 2 -// _ = gui.c.PushContext(gui.State.Contexts.CommitFiles) -// gui.git.Patch.PatchManager.Start("from", "to", false, false) - -// errorCount := 0 -// gui.PopupHandler = &popup.TestPopupHandler{ -// OnErrorMsg: func(message string) error { -// assert.Equal(t, gui.c.Tr.CantChangeContextSizeError, message) -// errorCount += 1 -// return nil -// }, -// } - -// _ = gui.IncreaseContextInDiffView() - -// assert.Equal(t, 1, errorCount) -// assert.Equal(t, 2, gui.c.UserConfig.Git.DiffContextSize) -// } - -// func TestDoesntDecreaseContextInDiffViewInContextWhenInPatchBuildingMode(t *testing.T) { -// gui := NewDummyGui() -// setupGuiForTest(gui) -// gui.c.UserConfig.Git.DiffContextSize = 2 -// _ = gui.c.PushContext(gui.State.Contexts.CommitFiles) -// gui.git.Patch.PatchManager.Start("from", "to", false, false) - -// errorCount := 0 -// gui.PopupHandler = &popup.TestPopupHandler{ -// OnErrorMsg: func(message string) error { -// assert.Equal(t, gui.c.Tr.CantChangeContextSizeError, message) -// errorCount += 1 -// return nil -// }, -// } - -// _ = gui.DecreaseContextInDiffView() - -// assert.Equal(t, 2, gui.c.UserConfig.Git.DiffContextSize) -// } - -// func TestDecreasesContextInDiffViewNoFurtherThanOne(t *testing.T) { -// gui := NewDummyGui() -// setupGuiForTest(gui) -// gui.c.UserConfig.Git.DiffContextSize = 1 - -// _ = gui.DecreaseContextInDiffView() - -// assert.Equal(t, 1, gui.c.UserConfig.Git.DiffContextSize) -// } diff --git a/pkg/gui/diffing.go b/pkg/gui/diffing.go index def73d2f1..9d3ff1943 100644 --- a/pkg/gui/diffing.go +++ b/pkg/gui/diffing.go @@ -4,113 +4,12 @@ import ( "fmt" "strings" - "github.com/jesseduffield/lazygit/pkg/gui/context" "github.com/jesseduffield/lazygit/pkg/gui/modes/diffing" "github.com/jesseduffield/lazygit/pkg/gui/types" ) -func (gui *Gui) exitDiffMode() error { - gui.State.Modes.Diffing = diffing.New() - return gui.c.Refresh(types.RefreshOptions{Mode: types.ASYNC}) -} - -func (gui *Gui) renderDiff() error { - cmdObj := gui.os.Cmd.New( - fmt.Sprintf("git diff --submodule --no-ext-diff --color %s", gui.diffStr()), - ) - task := types.NewRunPtyTask(cmdObj.GetCmd()) - - return gui.c.RenderToMainViews(types.RefreshMainOpts{ - Pair: gui.c.MainViewPairs().Normal, - Main: &types.ViewUpdateOpts{ - Title: "Diff", - Task: task, - }, - }) -} - -// currentDiffTerminals returns the current diff terminals of the currently selected item. -// in the case of a branch it returns both the branch and it's upstream name, -// which becomes an option when you bring up the diff menu, but when you're just -// flicking through branches it will be using the local branch name. -func (gui *Gui) currentDiffTerminals() []string { - c := gui.currentSideContext() - - if c.GetKey() == "" { - return nil - } - - switch v := c.(type) { - case *context.WorkingTreeContext, *context.SubmodulesContext: - // TODO: should we just return nil here? - return []string{""} - case *context.CommitFilesContext: - return []string{v.GetRef().RefName()} - case *context.BranchesContext: - // for our local branches we want to include both the branch and its upstream - branch := gui.State.Contexts.Branches.GetSelected() - if branch != nil { - names := []string{branch.ID()} - if branch.IsTrackingRemote() { - names = append(names, branch.ID()+"@{u}") - } - return names - } - return nil - case types.IListContext: - itemId := v.GetSelectedItemId() - - return []string{itemId} - } - - return nil -} - -func (gui *Gui) currentDiffTerminal() string { - names := gui.currentDiffTerminals() - if len(names) == 0 { - return "" - } - return names[0] -} - -func (gui *Gui) currentlySelectedFilename() string { - switch gui.currentContext().GetKey() { - case context.FILES_CONTEXT_KEY, context.COMMIT_FILES_CONTEXT_KEY: - return gui.getSideContextSelectedItemId() - default: - return "" - } -} - -func (gui *Gui) diffStr() string { - output := gui.State.Modes.Diffing.Ref - - right := gui.currentDiffTerminal() - if right != "" { - output += " " + right - } - - if gui.State.Modes.Diffing.Reverse { - output += " -R" - } - - if gui.IgnoreWhitespaceInDiffView { - output += " --ignore-all-space" - } - - file := gui.currentlySelectedFilename() - if file != "" { - output += " -- " + file - } else if gui.State.Modes.Filtering.Active() { - output += " -- " + gui.State.Modes.Filtering.GetPath() - } - - return output -} - func (gui *Gui) handleCreateDiffingMenuPanel() error { - names := gui.currentDiffTerminals() + names := gui.helpers.Diff.CurrentDiffTerminals() menuItems := []*types.MenuItem{} for _, name := range names { diff --git a/pkg/gui/extras_panel.go b/pkg/gui/extras_panel.go index c36f12a66..3ac5d6915 100644 --- a/pkg/gui/extras_panel.go +++ b/pkg/gui/extras_panel.go @@ -15,7 +15,7 @@ func (gui *Gui) handleCreateExtrasMenuPanel() error { { Label: gui.c.Tr.ToggleShowCommandLog, OnPress: func() error { - currentContext := gui.currentStaticContext() + currentContext := gui.c.CurrentStaticContext() if gui.ShowExtrasWindow && currentContext.GetKey() == context.COMMAND_LOG_CONTEXT_KEY { if err := gui.c.PopContext(); err != nil { return err @@ -39,7 +39,7 @@ func (gui *Gui) handleCreateExtrasMenuPanel() error { func (gui *Gui) handleFocusCommandLog() error { gui.ShowExtrasWindow = true // TODO: is this necessary? Can't I just call 'return from context'? - gui.State.Contexts.CommandLog.SetParentContext(gui.currentSideContext()) + gui.State.Contexts.CommandLog.SetParentContext(gui.c.CurrentSideContext()) return gui.c.PushContext(gui.State.Contexts.CommandLog) } diff --git a/pkg/gui/file_watching.go b/pkg/gui/file_watching.go index 01a2d0b88..ff04353e2 100644 --- a/pkg/gui/file_watching.go +++ b/pkg/gui/file_watching.go @@ -17,6 +17,8 @@ import ( // file watching is only really an added bonus for faster refreshing. const MAX_WATCHED_FILES = 50 +var _ types.IFileWatcher = new(fileWatcher) + type fileWatcher struct { Watcher *fsnotify.Watcher WatchedFilenames []string @@ -60,7 +62,7 @@ func (w *fileWatcher) watchFilename(filename string) { w.WatchedFilenames = append(w.WatchedFilenames, filename) } -func (w *fileWatcher) addFilesToFileWatcher(files []*models.File) error { +func (w *fileWatcher) AddFilesToFileWatcher(files []*models.File) error { if w.Disabled { return nil } @@ -102,7 +104,7 @@ func min(a int, b int) int { // NOTE: given that we often edit files ourselves, this may make us end up refreshing files too often // TODO: consider watching the whole directory recursively (could be more expensive) -func (gui *Gui) watchFilesForChanges() { +func (gui *Gui) WatchFilesForChanges() { gui.fileWatcher = NewFileWatcher(gui.Log) if gui.fileWatcher.Disabled { return @@ -117,7 +119,7 @@ func (gui *Gui) watchFilesForChanges() { continue } // only refresh if we're not already - if !gui.State.IsRefreshingFiles { + if !gui.IsRefreshingFiles { _ = gui.c.Refresh(types.RefreshOptions{Mode: types.ASYNC, Scope: []types.RefreshableView{types.FILES}}) } diff --git a/pkg/gui/files_panel.go b/pkg/gui/files_panel.go deleted file mode 100644 index 479b8aa50..000000000 --- a/pkg/gui/files_panel.go +++ /dev/null @@ -1,95 +0,0 @@ -package gui - -import ( - "github.com/jesseduffield/gocui" - "github.com/jesseduffield/lazygit/pkg/commands/models" - "github.com/jesseduffield/lazygit/pkg/gui/filetree" - "github.com/jesseduffield/lazygit/pkg/gui/types" -) - -func (gui *Gui) getSelectedFileNode() *filetree.FileNode { - return gui.State.Contexts.Files.GetSelected() -} - -func (gui *Gui) getSelectedFile() *models.File { - node := gui.getSelectedFileNode() - if node == nil { - return nil - } - return node.File -} - -func (gui *Gui) filesRenderToMain() error { - node := gui.getSelectedFileNode() - - if node == nil { - return gui.c.RenderToMainViews(types.RefreshMainOpts{ - Pair: gui.c.MainViewPairs().Normal, - Main: &types.ViewUpdateOpts{ - Title: gui.c.Tr.DiffTitle, - Task: types.NewRenderStringTask(gui.c.Tr.NoChangedFiles), - }, - }) - } - - if node.File != nil && node.File.HasInlineMergeConflicts { - hasConflicts, err := gui.helpers.MergeConflicts.SetMergeState(node.GetPath()) - if err != nil { - return err - } - - if hasConflicts { - return gui.refreshMergePanel(false) - } - } - - gui.helpers.MergeConflicts.ResetMergeState() - - pair := gui.c.MainViewPairs().Normal - if node.File != nil { - pair = gui.c.MainViewPairs().Staging - } - - split := gui.c.UserConfig.Gui.SplitDiff == "always" || (node.GetHasUnstagedChanges() && node.GetHasStagedChanges()) - mainShowsStaged := !split && node.GetHasStagedChanges() - - cmdObj := gui.git.WorkingTree.WorktreeFileDiffCmdObj(node, false, mainShowsStaged, gui.IgnoreWhitespaceInDiffView) - title := gui.c.Tr.UnstagedChanges - if mainShowsStaged { - title = gui.c.Tr.StagedChanges - } - refreshOpts := types.RefreshMainOpts{ - Pair: pair, - Main: &types.ViewUpdateOpts{ - Task: types.NewRunPtyTask(cmdObj.GetCmd()), - Title: title, - }, - } - - if split { - cmdObj := gui.git.WorkingTree.WorktreeFileDiffCmdObj(node, false, true, gui.IgnoreWhitespaceInDiffView) - - title := gui.c.Tr.StagedChanges - if mainShowsStaged { - title = gui.c.Tr.UnstagedChanges - } - - refreshOpts.Secondary = &types.ViewUpdateOpts{ - Title: title, - Task: types.NewRunPtyTask(cmdObj.GetCmd()), - } - } - - return gui.c.RenderToMainViews(refreshOpts) -} - -func (gui *Gui) getSetTextareaTextFn(getView func() *gocui.View) func(string) { - return func(text string) { - // using a getView function so that we don't need to worry about when the view is created - view := getView() - view.ClearTextArea() - view.TextArea.TypeString(text) - _ = gui.resizePopupPanel(view, view.TextArea.GetContent()) - view.RenderTextArea() - } -} diff --git a/pkg/gui/filtering_menu_panel.go b/pkg/gui/filtering_menu_panel.go index 97327324e..dba6e8e8c 100644 --- a/pkg/gui/filtering_menu_panel.go +++ b/pkg/gui/filtering_menu_panel.go @@ -9,9 +9,9 @@ import ( func (gui *Gui) handleCreateFilteringMenuPanel() error { fileName := "" - switch gui.currentSideListContext() { + switch gui.c.CurrentSideContext() { case gui.State.Contexts.Files: - node := gui.getSelectedFileNode() + node := gui.State.Contexts.Files.GetSelected() if node != nil { fileName = node.GetPath() } diff --git a/pkg/gui/global_handlers.go b/pkg/gui/global_handlers.go index 326b856bd..f2c762cc6 100644 --- a/pkg/gui/global_handlers.go +++ b/pkg/gui/global_handlers.go @@ -5,7 +5,6 @@ import ( "strings" "github.com/jesseduffield/gocui" - "github.com/jesseduffield/lazygit/pkg/commands/git_commands" "github.com/jesseduffield/lazygit/pkg/gui/types" "github.com/jesseduffield/lazygit/pkg/utils" ) @@ -113,13 +112,13 @@ func (gui *Gui) scrollDownMain() error { } func (gui *Gui) mainView() *gocui.View { - viewName := gui.getViewNameForWindow("main") + viewName := gui.helpers.Window.GetViewNameForWindow("main") view, _ := gui.g.View(viewName) return view } func (gui *Gui) secondaryView() *gocui.View { - viewName := gui.getViewNameForWindow("secondary") + viewName := gui.helpers.Window.GetViewNameForWindow("secondary") view, _ := gui.g.View(viewName) return view } @@ -162,17 +161,19 @@ func (gui *Gui) handleRefresh() error { return gui.c.Refresh(types.RefreshOptions{Mode: types.ASYNC}) } -func (gui *Gui) backgroundFetch() (err error) { - err = gui.git.Sync.Fetch(git_commands.FetchOptions{Background: true}) - - _ = gui.c.Refresh(types.RefreshOptions{Scope: []types.RefreshableView{types.BRANCHES, types.COMMITS, types.REMOTES, types.TAGS}, Mode: types.ASYNC}) - - return err -} - func (gui *Gui) handleCopySelectedSideContextItemToClipboard() error { // important to note that this assumes we've selected an item in a side context - itemId := gui.getSideContextSelectedItemId() + currentSideContext := gui.c.CurrentSideContext() + if currentSideContext == nil { + return nil + } + + listContext, ok := currentSideContext.(types.IListContext) + if !ok { + return nil + } + + itemId := listContext.GetSelectedItemId() if itemId == "" { return nil @@ -189,3 +190,13 @@ func (gui *Gui) handleCopySelectedSideContextItemToClipboard() error { return nil } + +func (gui *Gui) rerenderView(view *gocui.View) error { + context, ok := gui.helpers.View.ContextForView(view.Name()) + if !ok { + gui.Log.Errorf("no context found for view %s", view.Name()) + return nil + } + + return context.HandleRender() +} diff --git a/pkg/gui/gui.go b/pkg/gui/gui.go index aed1d5430..4235f2eb0 100644 --- a/pkg/gui/gui.go +++ b/pkg/gui/gui.go @@ -8,6 +8,7 @@ import ( "sync" "github.com/jesseduffield/gocui" + "github.com/jesseduffield/lazycore/pkg/boxlayout" appTypes "github.com/jesseduffield/lazygit/pkg/app/types" "github.com/jesseduffield/lazygit/pkg/commands" "github.com/jesseduffield/lazygit/pkg/commands/git_commands" @@ -32,7 +33,6 @@ import ( "github.com/jesseduffield/lazygit/pkg/gui/types" "github.com/jesseduffield/lazygit/pkg/integration/components" integrationTypes "github.com/jesseduffield/lazygit/pkg/integration/types" - "github.com/jesseduffield/lazygit/pkg/snake" "github.com/jesseduffield/lazygit/pkg/tasks" "github.com/jesseduffield/lazygit/pkg/theme" "github.com/jesseduffield/lazygit/pkg/updates" @@ -58,18 +58,6 @@ const StartupPopupVersion = 5 // OverlappingEdges determines if panel edges overlap var OverlappingEdges = false -type ContextManager struct { - ContextStack []types.Context - sync.RWMutex -} - -func NewContextManager() ContextManager { - return ContextManager{ - ContextStack: []types.Context{}, - RWMutex: sync.RWMutex{}, - } -} - type Repo string // Gui wraps the gocui Gui object which handles rendering and events @@ -117,12 +105,7 @@ type Gui struct { // this tells us whether our views have been initially set up ViewsSetup bool - Views Views - - // if we've suspended the gui (e.g. because we've switched to a subprocess) - // we typically want to pause some things that are running like background - // file refreshes - PauseBackgroundThreads bool + Views types.Views // Log of the commands that get run, to be displayed to the user. CmdLog []string @@ -139,6 +122,8 @@ type Gui struct { // flag as to whether or not the diff view should ignore whitespace IgnoreWhitespaceInDiffView bool + IsRefreshingFiles bool + // we use this to decide whether we'll return to the original directory that // lazygit was opened in, or if we'll retain the one we're currently in. RetainOriginalDir bool @@ -153,10 +138,52 @@ type Gui struct { // process InitialDir string + BackgroundRoutineMgr *BackgroundRoutineMgr + // for accessing the gui's state from outside this package + stateAccessor *StateAccessor + + Updating bool + c *types.HelperCommon helpers *helpers.Helpers +} - snakeGame *snake.Game +type StateAccessor struct { + gui *Gui +} + +var _ types.IStateAccessor = new(StateAccessor) + +func (self *StateAccessor) GetIgnoreWhitespaceInDiffView() bool { + return self.gui.IgnoreWhitespaceInDiffView +} + +func (self *StateAccessor) SetIgnoreWhitespaceInDiffView(value bool) { + self.gui.IgnoreWhitespaceInDiffView = value +} + +func (self *StateAccessor) GetRepoPathStack() *utils.StringStack { + return self.gui.RepoPathStack +} + +func (self *StateAccessor) GetUpdating() bool { + return self.gui.Updating +} + +func (self *StateAccessor) SetUpdating(value bool) { + self.gui.Updating = value +} + +func (self *StateAccessor) GetRepoState() types.IRepoStateAccessor { + return self.gui.State +} + +func (self *StateAccessor) GetIsRefreshingFiles() bool { + return self.gui.IsRefreshingFiles +} + +func (self *StateAccessor) SetIsRefreshingFiles(value bool) { + self.gui.IsRefreshingFiles = value } // we keep track of some stuff from one render to the next to see if certain @@ -174,16 +201,14 @@ type GuiRepoState struct { // Suggestions will sometimes appear when typing into a prompt Suggestions []*types.Suggestion - Updating bool SplitMainPanel bool LimitCommits bool - IsRefreshingFiles bool - Searching searchingState - StartupStage StartupStage // Allows us to not load everything at once + Searching searchingState + StartupStage types.StartupStage // Allows us to not load everything at once - ContextManager ContextManager - Contexts *context.ContextTree + ContextMgr ContextMgr + Contexts *context.ContextTree // WindowViewNameMap is a mapping of windows to the current view of that window. // Some views move between windows for example the commitFiles view and when cycling through @@ -204,20 +229,38 @@ type GuiRepoState struct { CurrentPopupOpts *types.CreatePopupPanelOpts } +var _ types.IRepoStateAccessor = new(GuiRepoState) + +func (self *GuiRepoState) GetViewsSetup() bool { + return self.ViewsSetup +} + +func (self *GuiRepoState) GetWindowViewNameMap() *utils.ThreadSafeMap[string, string] { + return self.WindowViewNameMap +} + +func (self *GuiRepoState) GetStartupStage() types.StartupStage { + return self.StartupStage +} + +func (self *GuiRepoState) SetStartupStage(value types.StartupStage) { + self.StartupStage = value +} + +func (self *GuiRepoState) GetCurrentPopupOpts() *types.CreatePopupPanelOpts { + return self.CurrentPopupOpts +} + +func (self *GuiRepoState) SetCurrentPopupOpts(value *types.CreatePopupPanelOpts) { + self.CurrentPopupOpts = value +} + type searchingState struct { view *gocui.View isSearching bool searchString string } -// startup stages so we don't need to load everything at once -type StartupStage int - -const ( - INITIAL StartupStage = iota - COMPLETE -) - func (gui *Gui) onNewRepo(startArgs appTypes.StartArgs, reuseState bool) error { var err error gui.git, err = commands.NewGitCommand( @@ -278,8 +321,6 @@ func (gui *Gui) resetState(startArgs appTypes.StartArgs, reuseState bool) { initialContext := initialContext(contextTree, startArgs) initialScreenMode := initialScreenMode(startArgs, gui.Config) - initialWindowViewNameMap := gui.initialWindowViewNameMap(contextTree) - gui.State = &GuiRepoState{ Model: &types.Model{ CommitFiles: nil, @@ -298,9 +339,9 @@ func (gui *Gui) resetState(startArgs appTypes.StartArgs, reuseState bool) { }, ScreenMode: initialScreenMode, // TODO: put contexts in the context manager - ContextManager: NewContextManager(), + ContextMgr: NewContextMgr(initialContext, gui), Contexts: contextTree, - WindowViewNameMap: initialWindowViewNameMap, + WindowViewNameMap: initialWindowViewNameMap(contextTree), } if err := gui.c.PushContext(initialContext); err != nil { @@ -310,6 +351,16 @@ func (gui *Gui) resetState(startArgs appTypes.StartArgs, reuseState bool) { gui.RepoStateMap[Repo(currentDir)] = gui.State } +func initialWindowViewNameMap(contextTree *context.ContextTree) *utils.ThreadSafeMap[string, string] { + result := utils.NewThreadSafeMap[string, string]() + + for _, context := range contextTree.Flatten() { + result.Set(context.GetWindowName(), context.GetViewName()) + } + + return result +} + func initialScreenMode(startArgs appTypes.StartArgs, config config.AppConfigurer) WindowMaximisation { if startArgs.FilterPath != "" || startArgs.GitArg != appTypes.GitArgNone { return SCREEN_HALF @@ -391,7 +442,7 @@ func NewGui( InitialDir: initialDir, } - gui.watchFilesForChanges() + gui.WatchFilesForChanges() gui.PopupHandler = popup.NewPopupHandler( cmn, @@ -429,6 +480,9 @@ func NewGui( icons.SetIconEnabled(gui.UserConfig.Gui.ShowIcons) presentation.SetCustomBranches(gui.UserConfig.Gui.BranchColors) + gui.BackgroundRoutineMgr = &BackgroundRoutineMgr{gui: gui} + gui.stateAccessor = &StateAccessor{gui: gui} + return gui, nil } @@ -539,7 +593,7 @@ func (gui *Gui) Run(startArgs appTypes.StartArgs) error { gui.waitForIntro.Add(1) - gui.startBackgroundRoutines() + gui.BackgroundRoutineMgr.startBackgroundRoutines() gui.c.Log.Info("starting main loop") @@ -565,11 +619,11 @@ func (gui *Gui) RunAndHandleError(startArgs appTypes.StartArgs) error { switch err { case gocui.ErrQuit: if gui.RetainOriginalDir { - if err := gui.recordDirectory(gui.InitialDir); err != nil { + if err := gui.helpers.RecordDirectory.RecordDirectory(gui.InitialDir); err != nil { return err } } else { - if err := gui.recordCurrentDirectory(); err != nil { + if err := gui.helpers.RecordDirectory.RecordCurrentDirectory(); err != nil { return err } } @@ -639,7 +693,8 @@ func (gui *Gui) runSubprocessWithSuspense(subprocess oscommands.ICmdObj) (bool, return false, gui.c.Error(err) } - gui.PauseBackgroundThreads = true + gui.BackgroundRoutineMgr.PauseBackgroundThreads(true) + defer gui.BackgroundRoutineMgr.PauseBackgroundThreads(false) cmdErr := gui.runSubprocess(subprocess) @@ -647,8 +702,6 @@ func (gui *Gui) runSubprocessWithSuspense(subprocess oscommands.ICmdObj) (bool, return false, err } - gui.PauseBackgroundThreads = false - if cmdErr != nil { return false, gui.c.Error(cmdErr) } @@ -751,3 +804,37 @@ func (gui *Gui) onUIThread(f func() error) { return f() }) } + +func (gui *Gui) startBackgroundRoutines() { + mgr := &BackgroundRoutineMgr{gui: gui} + mgr.startBackgroundRoutines() +} + +func (gui *Gui) getWindowDimensions(informationStr string, appStatus string) map[string]boxlayout.Dimensions { + windowArranger := &WindowArranger{gui: gui} + return windowArranger.getWindowDimensions(informationStr, appStatus) +} + +func (gui *Gui) replaceContext(c types.Context) error { + return gui.State.ContextMgr.replaceContext(c) +} + +func (gui *Gui) pushContext(c types.Context, opts ...types.OnFocusOpts) error { + return gui.State.ContextMgr.pushContext(c, opts...) +} + +func (gui *Gui) popContext() error { + return gui.State.ContextMgr.popContext() +} + +func (gui *Gui) currentContext() types.Context { + return gui.State.ContextMgr.currentContext() +} + +func (gui *Gui) currentSideContext() types.Context { + return gui.State.ContextMgr.currentSideContext() +} + +func (gui *Gui) currentStaticContext() types.Context { + return gui.State.ContextMgr.currentStaticContext() +} diff --git a/pkg/gui/gui_common.go b/pkg/gui/gui_common.go index 835aa4f54..a90824f9e 100644 --- a/pkg/gui/gui_common.go +++ b/pkg/gui/gui_common.go @@ -1,8 +1,8 @@ package gui import ( - "errors" - + "github.com/jesseduffield/gocui" + "github.com/jesseduffield/lazygit/pkg/commands" "github.com/jesseduffield/lazygit/pkg/commands/oscommands" "github.com/jesseduffield/lazygit/pkg/config" "github.com/jesseduffield/lazygit/pkg/gui/types" @@ -41,23 +41,17 @@ func (self *guiCommon) RunSubprocess(cmdObj oscommands.ICmdObj) (bool, error) { } func (self *guiCommon) PushContext(context types.Context, opts ...types.OnFocusOpts) error { - singleOpts := types.OnFocusOpts{} - if len(opts) > 0 { - // using triple dot but you should only ever pass one of these opt structs - if len(opts) > 1 { - return errors.New("cannot pass multiple opts to pushContext") - } - - singleOpts = opts[0] - } - - return self.gui.pushContext(context, singleOpts) + return self.gui.pushContext(context, opts...) } func (self *guiCommon) PopContext() error { return self.gui.popContext() } +func (self *guiCommon) ReplaceContext(context types.Context) error { + return self.gui.replaceContext(context) +} + func (self *guiCommon) CurrentContext() types.Context { return self.gui.currentContext() } @@ -66,6 +60,10 @@ func (self *guiCommon) CurrentStaticContext() types.Context { return self.gui.currentStaticContext() } +func (self *guiCommon) CurrentSideContext() types.Context { + return self.gui.currentSideContext() +} + func (self *guiCommon) IsCurrentContext(c types.Context) bool { return self.CurrentContext().GetKey() == c.GetKey() } @@ -78,14 +76,54 @@ func (self *guiCommon) SaveAppState() error { return self.gui.Config.SaveAppState() } +func (self *guiCommon) GetConfig() config.AppConfigurer { + return self.gui.Config +} + +func (self *guiCommon) ResetViewOrigin(view *gocui.View) { + self.gui.resetViewOrigin(view) +} + +func (self *guiCommon) SetViewContent(view *gocui.View, content string) { + self.gui.setViewContent(view, content) +} + func (self *guiCommon) Render() { self.gui.render() } +func (self *guiCommon) Views() types.Views { + return self.gui.Views +} + +func (self *guiCommon) Git() *commands.GitCommand { + return self.gui.git +} + +func (self *guiCommon) OS() *oscommands.OSCommand { + return self.gui.os +} + +func (self *guiCommon) Modes() *types.Modes { + return self.gui.State.Modes +} + +func (self *guiCommon) Model() *types.Model { + return self.gui.State.Model +} + +func (self *guiCommon) Mutexes() types.Mutexes { + return self.gui.Mutexes +} + func (self *guiCommon) OpenSearch() { _ = self.gui.handleOpenSearch(self.gui.currentViewName()) } +func (self *guiCommon) GocuiGui() *gocui.Gui { + return self.gui.g +} + func (self *guiCommon) OnUIThread(f func() error) { self.gui.onUIThread(f) } @@ -102,3 +140,7 @@ func (self *guiCommon) MainViewPairs() types.MainViewPairs { MergeConflicts: self.gui.mergingMainContextPair(), } } + +func (self *guiCommon) State() types.IStateAccessor { + return self.gui.stateAccessor +} diff --git a/pkg/gui/keybindings.go b/pkg/gui/keybindings.go index b63c91905..64ca5a190 100644 --- a/pkg/gui/keybindings.go +++ b/pkg/gui/keybindings.go @@ -77,7 +77,7 @@ func (self *Gui) GetInitialKeybindings() ([]*types.Binding, []*gocui.ViewMouseBi { ViewName: "", Key: opts.GetKey(opts.Config.Universal.OpenRecentRepos), - Handler: self.handleCreateRecentReposMenu, + Handler: self.helpers.Repos.CreateRecentReposMenu, Description: self.c.Tr.SwitchRepo, }, { @@ -153,12 +153,6 @@ func (self *Gui) GetInitialKeybindings() ([]*types.Binding, []*gocui.ViewMouseBi Description: self.c.Tr.LcOpenMenu, Handler: self.handleCreateOptionsMenu, }, - { - ViewName: "status", - Key: opts.GetKey(opts.Config.Universal.Edit), - Handler: self.handleEditConfig, - Description: self.c.Tr.EditConfig, - }, { ViewName: "", Key: opts.GetKey(opts.Config.Universal.NextScreenMode), @@ -171,30 +165,7 @@ func (self *Gui) GetInitialKeybindings() ([]*types.Binding, []*gocui.ViewMouseBi Handler: self.prevScreenMode, Description: self.c.Tr.LcPrevScreenMode, }, - { - ViewName: "status", - Key: opts.GetKey(opts.Config.Universal.OpenFile), - Handler: self.handleOpenConfig, - Description: self.c.Tr.OpenConfig, - }, - { - ViewName: "status", - Key: opts.GetKey(opts.Config.Status.CheckForUpdate), - Handler: self.handleCheckForUpdate, - Description: self.c.Tr.LcCheckForUpdate, - }, - { - ViewName: "status", - Key: opts.GetKey(opts.Config.Status.RecentRepos), - Handler: self.handleCreateRecentReposMenu, - Description: self.c.Tr.SwitchRepo, - }, - { - ViewName: "status", - Key: opts.GetKey(opts.Config.Status.AllBranchesLogGraph), - Handler: self.handleShowAllBranchLogs, - Description: self.c.Tr.LcAllBranchesLogGraph, - }, + { ViewName: "files", Key: opts.GetKey(opts.Config.Universal.CopyToClipboard), @@ -309,12 +280,6 @@ func (self *Gui) GetInitialKeybindings() ([]*types.Binding, []*gocui.ViewMouseBi Modifier: gocui.ModNone, Handler: self.scrollUpSecondary, }, - { - ViewName: "status", - Key: gocui.MouseLeft, - Modifier: gocui.ModNone, - Handler: self.handleStatusClick, - }, { ViewName: "search", Key: opts.GetKey(opts.Config.Universal.Confirm), @@ -496,7 +461,9 @@ func (gui *Gui) resetKeybindings() error { for _, values := range gui.viewTabMap() { for _, value := range values { viewName := value.ViewName - tabClickCallback := func(tabIndex int) error { return gui.onViewTabClick(gui.windowForView(viewName), tabIndex) } + tabClickCallback := func(tabIndex int) error { + return gui.onViewTabClick(gui.helpers.Window.WindowForView(viewName), tabIndex) + } if err := gui.g.SetTabClickBinding(viewName, tabClickCallback); err != nil { return err diff --git a/pkg/gui/layout.go b/pkg/gui/layout.go index 0deb37d2e..0b522ec88 100644 --- a/pkg/gui/layout.go +++ b/pkg/gui/layout.go @@ -101,11 +101,11 @@ func (gui *Gui) layout(g *gocui.Gui) error { if err != nil && !gocui.IsUnknownView(err) { return err } - view.Visible = gui.getViewNameForWindow(context.GetWindowName()) == context.GetViewName() + view.Visible = gui.helpers.Window.GetViewNameForWindow(context.GetWindowName()) == context.GetViewName() } if gui.PrevLayout.Information != informationStr { - gui.setViewContent(gui.Views.Information, informationStr) + gui.c.SetViewContent(gui.Views.Information, informationStr) gui.PrevLayout.Information = informationStr } @@ -181,7 +181,7 @@ func (gui *Gui) onInitialViewsCreationForRepo() error { } } - initialContext := gui.currentSideContext() + initialContext := gui.c.CurrentSideContext() if err := gui.c.PushContext(initialContext); err != nil { return err } @@ -226,15 +226,44 @@ func (gui *Gui) onInitialViewsCreation() error { } if gui.showRecentRepos { - if err := gui.handleCreateRecentReposMenu(); err != nil { + if err := gui.helpers.Repos.CreateRecentReposMenu(); err != nil { return err } gui.showRecentRepos = false } - gui.Updater.CheckForNewUpdate(gui.onBackgroundUpdateCheckFinish, false) + gui.helpers.Update.CheckForUpdateInBackground() gui.waitForIntro.Done() return nil } + +// 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() + // 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); err != nil { + return err + } + + previousView = newView + } + return nil + } +} + +func (gui *Gui) onViewFocusLost(oldView *gocui.View) error { + if oldView == nil { + return nil + } + + oldView.Highlight = false + + _ = oldView.SetOriginX(0) + + return nil +} diff --git a/pkg/gui/list_context_config.go b/pkg/gui/list_context_config.go index 7969c4d20..25b5a1666 100644 --- a/pkg/gui/list_context_config.go +++ b/pkg/gui/list_context_config.go @@ -34,9 +34,6 @@ func (gui *Gui) filesListContext() *context.WorkingTreeContext { return []string{line} }) }, - nil, - gui.withDiffModeCheck(gui.filesRenderToMain), - nil, gui.c, ) } @@ -48,9 +45,6 @@ func (gui *Gui) branchesListContext() *context.BranchesContext { func(startIdx int, length int) [][]string { return presentation.GetBranchListDisplayStrings(gui.State.Model.Branches, gui.State.ScreenMode != SCREEN_NORMAL, gui.State.Modes.Diffing.Ref, gui.Tr) }, - nil, - gui.withDiffModeCheck(gui.branchesRenderToMain), - nil, gui.c, ) } @@ -62,9 +56,6 @@ func (gui *Gui) remotesListContext() *context.RemotesContext { func(startIdx int, length int) [][]string { return presentation.GetRemoteListDisplayStrings(gui.State.Model.Remotes, gui.State.Modes.Diffing.Ref) }, - nil, - gui.withDiffModeCheck(gui.remotesRenderToMain), - nil, gui.c, ) } @@ -76,9 +67,6 @@ func (gui *Gui) remoteBranchesListContext() *context.RemoteBranchesContext { func(startIdx int, length int) [][]string { return presentation.GetRemoteBranchListDisplayStrings(gui.State.Model.RemoteBranches, gui.State.Modes.Diffing.Ref) }, - nil, - gui.withDiffModeCheck(gui.remoteBranchesRenderToMain), - nil, gui.c, ) } @@ -86,7 +74,7 @@ func (gui *Gui) remoteBranchesListContext() *context.RemoteBranchesContext { func (gui *Gui) withDiffModeCheck(f func() error) func() error { return func() error { if gui.State.Modes.Diffing.Active() { - return gui.renderDiff() + return gui.helpers.Diff.RenderDiff() } return f() @@ -100,9 +88,6 @@ func (gui *Gui) tagsListContext() *context.TagsContext { func(startIdx int, length int) [][]string { return presentation.GetTagListDisplayStrings(gui.State.Model.Tags, gui.State.Modes.Diffing.Ref) }, - nil, - gui.withDiffModeCheck(gui.tagsRenderToMain), - nil, gui.c, ) } @@ -113,7 +98,7 @@ func (gui *Gui) branchCommitsListContext() *context.LocalCommitsContext { gui.Views.Commits, func(startIdx int, length int) [][]string { selectedCommitSha := "" - if gui.currentContext().GetKey() == context.LOCAL_COMMITS_CONTEXT_KEY { + if gui.c.CurrentContext().GetKey() == context.LOCAL_COMMITS_CONTEXT_KEY { selectedCommit := gui.State.Contexts.LocalCommits.GetSelected() if selectedCommit != nil { selectedCommitSha = selectedCommit.Sha @@ -138,9 +123,6 @@ func (gui *Gui) branchCommitsListContext() *context.LocalCommitsContext { showYouAreHereLabel, ) }, - OnFocusWrapper(gui.onCommitFocus), - gui.withDiffModeCheck(gui.branchCommitsRenderToMain), - nil, gui.c, ) } @@ -151,7 +133,7 @@ func (gui *Gui) subCommitsListContext() *context.SubCommitsContext { gui.Views.SubCommits, func(startIdx int, length int) [][]string { selectedCommitSha := "" - if gui.currentContext().GetKey() == context.SUB_COMMITS_CONTEXT_KEY { + if gui.c.CurrentContext().GetKey() == context.SUB_COMMITS_CONTEXT_KEY { selectedCommit := gui.State.Contexts.SubCommits.GetSelected() if selectedCommit != nil { selectedCommitSha = selectedCommit.Sha @@ -173,9 +155,6 @@ func (gui *Gui) subCommitsListContext() *context.SubCommitsContext { false, ) }, - OnFocusWrapper(gui.onSubCommitFocus), - gui.withDiffModeCheck(gui.subCommitsRenderToMain), - nil, gui.c, ) } @@ -213,9 +192,6 @@ func (gui *Gui) reflogCommitsListContext() *context.ReflogCommitsContext { gui.c.UserConfig.Git.ParseEmoji, ) }, - nil, - gui.withDiffModeCheck(gui.reflogCommitsRenderToMain), - nil, gui.c, ) } @@ -227,9 +203,6 @@ func (gui *Gui) stashListContext() *context.StashContext { func(startIdx int, length int) [][]string { return presentation.GetStashEntryListDisplayStrings(gui.State.Model.StashEntries, gui.State.Modes.Diffing.Ref) }, - nil, - gui.withDiffModeCheck(gui.stashRenderToMain), - nil, gui.c, ) } @@ -248,9 +221,6 @@ func (gui *Gui) commitFilesListContext() *context.CommitFilesContext { return []string{line} }) }, - nil, - gui.withDiffModeCheck(gui.commitFilesRenderToMain), - nil, gui.c, ) } @@ -262,9 +232,6 @@ func (gui *Gui) submodulesListContext() *context.SubmodulesContext { func(startIdx int, length int) [][]string { return presentation.GetSubmoduleListDisplayStrings(gui.State.Model.Submodules) }, - nil, - gui.withDiffModeCheck(gui.submodulesRenderToMain), - nil, gui.c, ) } @@ -276,12 +243,6 @@ func (gui *Gui) suggestionsListContext() *context.SuggestionsContext { func(startIdx int, length int) [][]string { return presentation.GetSuggestionListDisplayStrings(gui.State.Suggestions) }, - nil, - nil, - func(types.OnFocusLostOpts) error { - gui.deactivateConfirmationPrompt() - return nil - }, gui.c, ) } diff --git a/pkg/gui/main_panels.go b/pkg/gui/main_panels.go index 9e36c18e9..3c6295725 100644 --- a/pkg/gui/main_panels.go +++ b/pkg/gui/main_panels.go @@ -34,11 +34,11 @@ func (gui *Gui) moveMainContextPairToTop(pair types.MainContextPair) { } func (gui *Gui) moveMainContextToTop(context types.Context) { - gui.setWindowContext(context) + gui.helpers.Window.SetWindowContext(context) view := context.GetView() - topView := gui.topViewInWindow(context.GetWindowName()) + topView := gui.helpers.Window.TopViewInWindow(context.GetWindowName()) if topView == nil { gui.Log.Error("unexpected: topView is nil") return diff --git a/pkg/gui/modes.go b/pkg/gui/modes.go index 750d1ad92..15e78c117 100644 --- a/pkg/gui/modes.go +++ b/pkg/gui/modes.go @@ -4,6 +4,7 @@ import ( "fmt" "github.com/jesseduffield/lazygit/pkg/commands/types/enums" + "github.com/jesseduffield/lazygit/pkg/gui/presentation" "github.com/jesseduffield/lazygit/pkg/gui/style" ) @@ -22,12 +23,12 @@ func (gui *Gui) modeStatuses() []modeStatus { fmt.Sprintf( "%s %s", gui.c.Tr.LcShowingGitDiff, - "git diff "+gui.diffStr(), + "git diff "+gui.helpers.Diff.DiffStr(), ), style.FgMagenta, ) }, - reset: gui.exitDiffMode, + reset: gui.helpers.Diff.ExitDiffMode, }, { isActive: gui.git.Patch.PatchBuilder.Active, @@ -77,7 +78,7 @@ func (gui *Gui) modeStatuses() []modeStatus { description: func() string { workingTreeState := gui.git.Status.WorkingTreeState() return gui.withResetButton( - formatWorkingTreeState(workingTreeState), style.FgYellow, + presentation.FormatWorkingTreeState(workingTreeState), style.FgYellow, ) }, reset: gui.helpers.MergeAndRebase.AbortMergeOrRebaseWithConfirm, diff --git a/pkg/gui/options_map.go b/pkg/gui/options_map.go new file mode 100644 index 000000000..f47025017 --- /dev/null +++ b/pkg/gui/options_map.go @@ -0,0 +1,56 @@ +package gui + +import ( + "fmt" + "sort" + "strings" + + "github.com/jesseduffield/generics/maps" + "github.com/jesseduffield/lazygit/pkg/gui/keybindings" + "github.com/jesseduffield/lazygit/pkg/gui/types" +) + +type OptionsMapMgr struct { + c *types.HelperCommon +} + +func (gui *Gui) renderContextOptionsMap(c types.Context) { + mgr := OptionsMapMgr{c: gui.c} + mgr.renderContextOptionsMap(c) +} + +// render the options available for the current context at the bottom of the screen +func (self *OptionsMapMgr) renderContextOptionsMap(c types.Context) { + optionsMap := c.GetOptionsMap() + if optionsMap == nil { + optionsMap = self.globalOptionsMap() + } + + self.renderOptions(self.optionsMapToString(optionsMap)) +} + +func (self *OptionsMapMgr) optionsMapToString(optionsMap map[string]string) string { + options := maps.MapToSlice(optionsMap, func(key string, description string) string { + return key + ": " + description + }) + sort.Strings(options) + return strings.Join(options, ", ") +} + +func (self *OptionsMapMgr) renderOptions(options string) { + self.c.SetViewContent(self.c.Views().Options, options) +} + +func (self *OptionsMapMgr) globalOptionsMap() map[string]string { + keybindingConfig := self.c.UserConfig.Keybinding + + return map[string]string{ + fmt.Sprintf("%s/%s", keybindings.Label(keybindingConfig.Universal.ScrollUpMain), keybindings.Label(keybindingConfig.Universal.ScrollDownMain)): self.c.Tr.LcScroll, + fmt.Sprintf("%s %s %s %s", keybindings.Label(keybindingConfig.Universal.PrevBlock), keybindings.Label(keybindingConfig.Universal.NextBlock), keybindings.Label(keybindingConfig.Universal.PrevItem), keybindings.Label(keybindingConfig.Universal.NextItem)): self.c.Tr.LcNavigate, + keybindings.Label(keybindingConfig.Universal.Return): self.c.Tr.LcCancel, + keybindings.Label(keybindingConfig.Universal.Quit): self.c.Tr.LcQuit, + keybindings.Label(keybindingConfig.Universal.OptionMenuAlt1): self.c.Tr.LcMenu, + fmt.Sprintf("%s-%s", keybindings.Label(keybindingConfig.Universal.JumpToBlock[0]), keybindings.Label(keybindingConfig.Universal.JumpToBlock[len(keybindingConfig.Universal.JumpToBlock)-1])): self.c.Tr.LcJump, + fmt.Sprintf("%s/%s", keybindings.Label(keybindingConfig.Universal.ScrollLeft), keybindings.Label(keybindingConfig.Universal.ScrollRight)): self.c.Tr.LcScrollLeftRight, + } +} diff --git a/pkg/gui/options_menu_panel.go b/pkg/gui/options_menu_panel.go index b7b13698f..1635e9fbd 100644 --- a/pkg/gui/options_menu_panel.go +++ b/pkg/gui/options_menu_panel.go @@ -50,7 +50,7 @@ func uniqueBindings(bindings []*types.Binding) []*types.Binding { } func (gui *Gui) handleCreateOptionsMenu() error { - ctx := gui.currentContext() + ctx := gui.c.CurrentContext() // Don't show menu while displaying popup. if ctx.GetKind() == types.PERSISTENT_POPUP || ctx.GetKind() == types.TEMPORARY_POPUP { return nil diff --git a/pkg/gui/presentation/working_tree.go b/pkg/gui/presentation/working_tree.go new file mode 100644 index 000000000..5ce46b734 --- /dev/null +++ b/pkg/gui/presentation/working_tree.go @@ -0,0 +1,14 @@ +package presentation + +import "github.com/jesseduffield/lazygit/pkg/commands/types/enums" + +func FormatWorkingTreeState(rebaseMode enums.RebaseMode) string { + switch rebaseMode { + case enums.REBASE_MODE_REBASING: + return "rebasing" + case enums.REBASE_MODE_MERGING: + return "merging" + default: + return "none" + } +} diff --git a/pkg/gui/quitting.go b/pkg/gui/quitting.go index cdb1ff09a..41c23f268 100644 --- a/pkg/gui/quitting.go +++ b/pkg/gui/quitting.go @@ -1,33 +1,10 @@ package gui import ( - "os" - "github.com/jesseduffield/gocui" "github.com/jesseduffield/lazygit/pkg/gui/types" ) -// when a user runs lazygit with the LAZYGIT_NEW_DIR_FILE env variable defined -// we will write the current directory to that file on exit so that their -// shell can then change to that directory. That means you don't get kicked -// back to the directory that you started with. -func (gui *Gui) recordCurrentDirectory() error { - // determine current directory, set it in LAZYGIT_NEW_DIR_FILE - dirName, err := os.Getwd() - if err != nil { - return err - } - return gui.recordDirectory(dirName) -} - -func (gui *Gui) recordDirectory(dirName string) error { - newDirFilePath := os.Getenv("LAZYGIT_NEW_DIR_FILE") - if newDirFilePath == "" { - return nil - } - return gui.os.CreateFileWithContent(newDirFilePath, dirName) -} - func (gui *Gui) handleQuitWithoutChangingDirectory() error { gui.RetainOriginalDir = true return gui.quit() @@ -39,7 +16,7 @@ func (gui *Gui) handleQuit() error { } func (gui *Gui) handleTopLevelReturn() error { - currentContext := gui.currentContext() + currentContext := gui.c.CurrentContext() parentContext, hasParent := currentContext.GetParentContext() if hasParent && currentContext != nil && parentContext != nil { @@ -53,11 +30,9 @@ func (gui *Gui) handleTopLevelReturn() error { } } - repoPathStack := gui.RepoPathStack + repoPathStack := gui.c.State().GetRepoPathStack() if !repoPathStack.IsEmpty() { - path := repoPathStack.Pop() - - return gui.dispatchSwitchToRepo(path, true) + return gui.helpers.Repos.DispatchSwitchToRepo(repoPathStack.Pop(), true) } if gui.c.UserConfig.QuitOnTopLevelReturn { @@ -68,7 +43,7 @@ func (gui *Gui) handleTopLevelReturn() error { } func (gui *Gui) quit() error { - if gui.State.Updating { + if gui.c.State().GetUpdating() { return gui.createUpdateQuitConfirmation() } @@ -84,3 +59,13 @@ func (gui *Gui) quit() error { return gocui.ErrQuit } + +func (gui *Gui) createUpdateQuitConfirmation() error { + return gui.c.Confirm(types.ConfirmOpts{ + Title: gui.Tr.ConfirmQuitDuringUpdateTitle, + Prompt: gui.Tr.ConfirmQuitDuringUpdate, + HandleConfirm: func() error { + return gocui.ErrQuit + }, + }) +} diff --git a/pkg/gui/recent_repos_panel.go b/pkg/gui/recent_repos_panel.go index b10a83078..a6953732a 100644 --- a/pkg/gui/recent_repos_panel.go +++ b/pkg/gui/recent_repos_panel.go @@ -1,160 +1,10 @@ package gui import ( - "fmt" "os" "path/filepath" - "strings" - "sync" - - "github.com/jesseduffield/generics/slices" - appTypes "github.com/jesseduffield/lazygit/pkg/app/types" - "github.com/jesseduffield/lazygit/pkg/commands" - "github.com/jesseduffield/lazygit/pkg/env" - "github.com/jesseduffield/lazygit/pkg/gui/presentation/icons" - "github.com/jesseduffield/lazygit/pkg/gui/style" - "github.com/jesseduffield/lazygit/pkg/gui/types" - "github.com/jesseduffield/lazygit/pkg/utils" ) -func (gui *Gui) getCurrentBranch(path string) string { - readHeadFile := func(path string) (string, error) { - headFile, err := os.ReadFile(filepath.Join(path, "HEAD")) - if err == nil { - content := strings.TrimSpace(string(headFile)) - refsPrefix := "ref: refs/heads/" - var branchDisplay string - if strings.HasPrefix(content, refsPrefix) { - // is a branch - branchDisplay = strings.TrimPrefix(content, refsPrefix) - } else { - // detached HEAD state, displaying short SHA - branchDisplay = utils.ShortSha(content) - } - return branchDisplay, nil - } - return "", err - } - - gitDirPath := filepath.Join(path, ".git") - - if gitDir, err := os.Stat(gitDirPath); err == nil { - if gitDir.IsDir() { - // ordinary repo - if branch, err := readHeadFile(gitDirPath); err == nil { - return branch - } - } else { - // worktree - if worktreeGitDir, err := os.ReadFile(gitDirPath); err == nil { - content := strings.TrimSpace(string(worktreeGitDir)) - worktreePath := strings.TrimPrefix(content, "gitdir: ") - if branch, err := readHeadFile(worktreePath); err == nil { - return branch - } - } - } - } - - return gui.c.Tr.LcBranchUnknown -} - -func (gui *Gui) handleCreateRecentReposMenu() error { - // we'll show an empty panel if there are no recent repos - recentRepoPaths := []string{} - if len(gui.c.GetAppState().RecentRepos) > 0 { - // we skip the first one because we're currently in it - recentRepoPaths = gui.c.GetAppState().RecentRepos[1:] - } - - currentBranches := sync.Map{} - - wg := sync.WaitGroup{} - wg.Add(len(recentRepoPaths)) - - for _, path := range recentRepoPaths { - go func(path string) { - defer wg.Done() - currentBranches.Store(path, gui.getCurrentBranch(path)) - }(path) - } - - wg.Wait() - - menuItems := slices.Map(recentRepoPaths, func(path string) *types.MenuItem { - branchName, _ := currentBranches.Load(path) - if icons.IsIconEnabled() { - branchName = icons.BRANCH_ICON + " " + fmt.Sprintf("%v", branchName) - } - - return &types.MenuItem{ - LabelColumns: []string{ - filepath.Base(path), - style.FgCyan.Sprint(branchName), - style.FgMagenta.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.Clear() - return gui.dispatchSwitchToRepo(path, false) - }, - } - }) - - return gui.c.Menu(types.CreateMenuOptions{Title: gui.c.Tr.RecentRepos, Items: menuItems}) -} - -func (gui *Gui) handleShowAllBranchLogs() error { - cmdObj := gui.git.Branch.AllBranchesLogCmdObj() - task := types.NewRunPtyTask(cmdObj.GetCmd()) - - return gui.c.RenderToMainViews(types.RefreshMainOpts{ - Pair: gui.c.MainViewPairs().Normal, - Main: &types.ViewUpdateOpts{ - Title: gui.c.Tr.LogTitle, - Task: task, - }, - }) -} - -func (gui *Gui) dispatchSwitchToRepo(path string, reuse bool) error { - env.UnsetGitDirEnvs() - originalPath, err := os.Getwd() - if err != nil { - return nil - } - - if err := os.Chdir(path); err != nil { - if os.IsNotExist(err) { - return gui.c.ErrorMsg(gui.c.Tr.ErrRepositoryMovedOrDeleted) - } - return err - } - - if err := commands.VerifyInGitRepo(gui.os); err != nil { - if err := os.Chdir(originalPath); err != nil { - return err - } - - return err - } - - if err := gui.recordCurrentDirectory(); err != nil { - return err - } - - // these two mutexes are used by our background goroutines (triggered via `gui.goEvery`. We don't want to - // switch to a repo while one of these goroutines is in the process of updating something - gui.Mutexes.SyncMutex.Lock() - defer gui.Mutexes.SyncMutex.Unlock() - - gui.Mutexes.RefreshingFilesMutex.Lock() - defer gui.Mutexes.RefreshingFilesMutex.Unlock() - - return gui.onNewRepo(appTypes.StartArgs{}, reuse) -} - // updateRecentRepoList registers the fact that we opened lazygit in this repo, // so that we can open the same repo via the 'recent repos' menu func (gui *Gui) updateRecentRepoList() error { diff --git a/pkg/gui/refresh.go b/pkg/gui/refresh.go index 2926152ed..8861945d3 100644 --- a/pkg/gui/refresh.go +++ b/pkg/gui/refresh.go @@ -1,746 +1,9 @@ package gui import ( - "fmt" - "strings" - "sync" - - "github.com/jesseduffield/generics/set" - "github.com/jesseduffield/generics/slices" - "github.com/jesseduffield/lazygit/pkg/commands/git_commands" - "github.com/jesseduffield/lazygit/pkg/commands/models" - "github.com/jesseduffield/lazygit/pkg/commands/types/enums" - "github.com/jesseduffield/lazygit/pkg/gui/context" - "github.com/jesseduffield/lazygit/pkg/gui/filetree" - "github.com/jesseduffield/lazygit/pkg/gui/mergeconflicts" - "github.com/jesseduffield/lazygit/pkg/gui/patch_exploring" - "github.com/jesseduffield/lazygit/pkg/gui/presentation" - "github.com/jesseduffield/lazygit/pkg/gui/style" "github.com/jesseduffield/lazygit/pkg/gui/types" - "github.com/jesseduffield/lazygit/pkg/utils" ) -func getScopeNames(scopes []types.RefreshableView) []string { - scopeNameMap := map[types.RefreshableView]string{ - types.COMMITS: "commits", - types.BRANCHES: "branches", - types.FILES: "files", - types.SUBMODULES: "submodules", - types.STASH: "stash", - types.REFLOG: "reflog", - types.TAGS: "tags", - types.REMOTES: "remotes", - types.STATUS: "status", - types.BISECT_INFO: "bisect", - types.STAGING: "staging", - types.MERGE_CONFLICTS: "mergeConflicts", - } - - return slices.Map(scopes, func(scope types.RefreshableView) string { - return scopeNameMap[scope] - }) -} - -func getModeName(mode types.RefreshMode) string { - switch mode { - case types.SYNC: - return "sync" - case types.ASYNC: - return "async" - case types.BLOCK_UI: - return "block-ui" - default: - return "unknown mode" - } -} - func (gui *Gui) Refresh(options types.RefreshOptions) error { - if options.Scope == nil { - gui.c.Log.Infof( - "refreshing all scopes in %s mode", - getModeName(options.Mode), - ) - } else { - gui.c.Log.Infof( - "refreshing the following scopes in %s mode: %s", - getModeName(options.Mode), - strings.Join(getScopeNames(options.Scope), ","), - ) - } - - wg := sync.WaitGroup{} - - f := func() { - var scopeSet *set.Set[types.RefreshableView] - if len(options.Scope) == 0 { - // not refreshing staging/patch-building unless explicitly requested because we only need - // to refresh those while focused. - scopeSet = set.NewFromSlice([]types.RefreshableView{ - types.COMMITS, - types.BRANCHES, - types.FILES, - types.STASH, - types.REFLOG, - types.TAGS, - types.REMOTES, - types.STATUS, - types.BISECT_INFO, - }) - } else { - scopeSet = set.NewFromSlice(options.Scope) - } - - refresh := func(f func()) { - wg.Add(1) - func() { - if options.Mode == types.ASYNC { - go utils.Safe(f) - } else { - f() - } - wg.Done() - }() - } - - if scopeSet.Includes(types.COMMITS) || scopeSet.Includes(types.BRANCHES) || scopeSet.Includes(types.REFLOG) || scopeSet.Includes(types.BISECT_INFO) { - refresh(gui.refreshCommits) - } else if scopeSet.Includes(types.REBASE_COMMITS) { - // the above block handles rebase commits so we only need to call this one - // if we've asked specifically for rebase commits and not those other things - refresh(func() { _ = gui.refreshRebaseCommits() }) - } - - // reason we're not doing this if the COMMITS type is included is that if the COMMITS type _is_ included we will refresh the commit files context anyway - if scopeSet.Includes(types.COMMIT_FILES) && !scopeSet.Includes(types.COMMITS) { - refresh(func() { _ = gui.refreshCommitFilesContext() }) - } - - if scopeSet.Includes(types.FILES) || scopeSet.Includes(types.SUBMODULES) { - refresh(func() { _ = gui.refreshFilesAndSubmodules() }) - } - - if scopeSet.Includes(types.STASH) { - refresh(func() { _ = gui.refreshStashEntries() }) - } - - if scopeSet.Includes(types.TAGS) { - refresh(func() { _ = gui.refreshTags() }) - } - - if scopeSet.Includes(types.REMOTES) { - refresh(func() { _ = gui.refreshRemotes() }) - } - - if scopeSet.Includes(types.STAGING) { - refresh(func() { _ = gui.refreshStagingPanel(types.OnFocusOpts{}) }) - } - - if scopeSet.Includes(types.PATCH_BUILDING) { - refresh(func() { _ = gui.refreshPatchBuildingPanel(types.OnFocusOpts{}) }) - } - - if scopeSet.Includes(types.MERGE_CONFLICTS) || scopeSet.Includes(types.FILES) { - refresh(func() { _ = gui.refreshMergeState() }) - } - - wg.Wait() - - gui.refreshStatus() - - if options.Then != nil { - options.Then() - } - } - - if options.Mode == types.BLOCK_UI { - gui.c.OnUIThread(func() error { - f() - return nil - }) - } else { - f() - } - - return nil -} - -// during startup, the bottleneck is fetching the reflog entries. We need these -// on startup to sort the branches by recency. So we have two phases: INITIAL, and COMPLETE. -// In the initial phase we don't get any reflog commits, but we asynchronously get them -// and refresh the branches after that -func (gui *Gui) refreshReflogCommitsConsideringStartup() { - switch gui.State.StartupStage { - case INITIAL: - go utils.Safe(func() { - _ = gui.refreshReflogCommits() - gui.refreshBranches() - gui.State.StartupStage = COMPLETE - }) - - case COMPLETE: - _ = gui.refreshReflogCommits() - } -} - -// whenever we change commits, we should update branches because the upstream/downstream -// counts can change. Whenever we change branches we should probably also change commits -// e.g. in the case of switching branches. -func (gui *Gui) refreshCommits() { - wg := sync.WaitGroup{} - wg.Add(2) - - go utils.Safe(func() { - gui.refreshReflogCommitsConsideringStartup() - - gui.refreshBranches() - wg.Done() - }) - - go utils.Safe(func() { - _ = gui.refreshCommitsWithLimit() - ctx, ok := gui.State.Contexts.CommitFiles.GetParentContext() - if ok && ctx.GetKey() == context.LOCAL_COMMITS_CONTEXT_KEY { - // This makes sense when we've e.g. just amended a commit, meaning we get a new commit SHA at the same position. - // However if we've just added a brand new commit, it pushes the list down by one and so we would end up - // showing the contents of a different commit than the one we initially entered. - // Ideally we would know when to refresh the commit files context and when not to, - // or perhaps we could just pop that context off the stack whenever cycling windows. - // For now the awkwardness remains. - commit := gui.getSelectedLocalCommit() - if commit != nil { - gui.State.Contexts.CommitFiles.SetRef(commit) - gui.State.Contexts.CommitFiles.SetTitleRef(commit.RefName()) - _ = gui.refreshCommitFilesContext() - } - } - wg.Done() - }) - - wg.Wait() -} - -func (gui *Gui) refreshCommitsWithLimit() error { - gui.Mutexes.LocalCommitsMutex.Lock() - defer gui.Mutexes.LocalCommitsMutex.Unlock() - - commits, err := gui.git.Loaders.CommitLoader.GetCommits( - git_commands.GetCommitsOptions{ - Limit: gui.State.Contexts.LocalCommits.GetLimitCommits(), - FilterPath: gui.State.Modes.Filtering.GetPath(), - IncludeRebaseCommits: true, - RefName: gui.refForLog(), - All: gui.State.Contexts.LocalCommits.GetShowWholeGitGraph(), - }, - ) - if err != nil { - return err - } - gui.State.Model.Commits = commits - gui.State.Model.WorkingTreeStateAtLastCommitRefresh = gui.git.Status.WorkingTreeState() - - return gui.c.PostRefreshUpdate(gui.State.Contexts.LocalCommits) -} - -func (gui *Gui) refreshCommitFilesContext() error { - ref := gui.State.Contexts.CommitFiles.GetRef() - to := ref.RefName() - from, reverse := gui.State.Modes.Diffing.GetFromAndReverseArgsForDiff(ref.ParentRefName()) - - files, err := gui.git.Loaders.CommitFileLoader.GetFilesInDiff(from, to, reverse) - if err != nil { - return gui.c.Error(err) - } - gui.State.Model.CommitFiles = files - gui.State.Contexts.CommitFiles.CommitFileTreeViewModel.SetTree() - - return gui.c.PostRefreshUpdate(gui.State.Contexts.CommitFiles) -} - -func (gui *Gui) refreshRebaseCommits() error { - gui.Mutexes.LocalCommitsMutex.Lock() - defer gui.Mutexes.LocalCommitsMutex.Unlock() - - updatedCommits, err := gui.git.Loaders.CommitLoader.MergeRebasingCommits(gui.State.Model.Commits) - if err != nil { - return err - } - gui.State.Model.Commits = updatedCommits - gui.State.Model.WorkingTreeStateAtLastCommitRefresh = gui.git.Status.WorkingTreeState() - - return gui.c.PostRefreshUpdate(gui.State.Contexts.LocalCommits) -} - -func (self *Gui) refreshTags() error { - tags, err := self.git.Loaders.TagLoader.GetTags() - if err != nil { - return self.c.Error(err) - } - - self.State.Model.Tags = tags - - return self.postRefreshUpdate(self.State.Contexts.Tags) -} - -func (gui *Gui) refreshStateSubmoduleConfigs() error { - configs, err := gui.git.Submodule.GetConfigs() - if err != nil { - return err - } - - gui.State.Model.Submodules = configs - - return nil -} - -// gui.refreshStatus is called at the end of this because that's when we can -// be sure there is a State.Model.Branches array to pick the current branch from -func (gui *Gui) refreshBranches() { - reflogCommits := gui.State.Model.FilteredReflogCommits - if gui.State.Modes.Filtering.Active() { - // in filter mode we filter our reflog commits to just those containing the path - // however we need all the reflog entries to populate the recencies of our branches - // which allows us to order them correctly. So if we're filtering we'll just - // manually load all the reflog commits here - var err error - reflogCommits, _, err = gui.git.Loaders.ReflogCommitLoader.GetReflogCommits(nil, "") - if err != nil { - gui.c.Log.Error(err) - } - } - - branches, err := gui.git.Loaders.BranchLoader.Load(reflogCommits) - if err != nil { - _ = gui.c.Error(err) - } - - gui.State.Model.Branches = branches - - if err := gui.c.PostRefreshUpdate(gui.State.Contexts.Branches); err != nil { - gui.c.Log.Error(err) - } - - gui.refreshStatus() -} - -func (gui *Gui) refreshFilesAndSubmodules() error { - gui.Mutexes.RefreshingFilesMutex.Lock() - gui.State.IsRefreshingFiles = true - defer func() { - gui.State.IsRefreshingFiles = false - gui.Mutexes.RefreshingFilesMutex.Unlock() - }() - - if err := gui.refreshStateSubmoduleConfigs(); err != nil { - return err - } - - if err := gui.refreshStateFiles(); err != nil { - return err - } - - gui.c.OnUIThread(func() error { - if err := gui.c.PostRefreshUpdate(gui.State.Contexts.Submodules); err != nil { - gui.c.Log.Error(err) - } - - if err := gui.c.PostRefreshUpdate(gui.State.Contexts.Files); err != nil { - gui.c.Log.Error(err) - } - - return nil - }) - - return nil -} - -func (gui *Gui) refreshMergeState() error { - gui.State.Contexts.MergeConflicts.GetMutex().Lock() - defer gui.State.Contexts.MergeConflicts.GetMutex().Unlock() - - if gui.currentContext().GetKey() != context.MERGE_CONFLICTS_CONTEXT_KEY { - return nil - } - - hasConflicts, err := gui.helpers.MergeConflicts.SetConflictsAndRender(gui.State.Contexts.MergeConflicts.GetState().GetPath(), true) - if err != nil { - return gui.c.Error(err) - } - - if !hasConflicts { - return gui.helpers.MergeConflicts.EscapeMerge() - } - - return nil -} - -func (gui *Gui) refreshStateFiles() error { - state := gui.State - - fileTreeViewModel := state.Contexts.Files.FileTreeViewModel - - // If git thinks any of our files have inline merge conflicts, but they actually don't, - // we stage them. - // Note that if files with merge conflicts have both arisen and have been resolved - // between refreshes, we won't stage them here. This is super unlikely though, - // and this approach spares us from having to call `git status` twice in a row. - // Although this also means that at startup we won't be staging anything until - // we call git status again. - pathsToStage := []string{} - prevConflictFileCount := 0 - for _, file := range gui.State.Model.Files { - if file.HasMergeConflicts { - prevConflictFileCount++ - } - if file.HasInlineMergeConflicts { - hasConflicts, err := mergeconflicts.FileHasConflictMarkers(file.Name) - if err != nil { - gui.Log.Error(err) - } else if !hasConflicts { - pathsToStage = append(pathsToStage, file.Name) - } - } - } - - if len(pathsToStage) > 0 { - gui.c.LogAction(gui.Tr.Actions.StageResolvedFiles) - if err := gui.git.WorkingTree.StageFiles(pathsToStage); err != nil { - return gui.c.Error(err) - } - } - - files := gui.git.Loaders.FileLoader. - GetStatusFiles(git_commands.GetStatusFileOptions{}) - - conflictFileCount := 0 - for _, file := range files { - if file.HasMergeConflicts { - conflictFileCount++ - } - } - - if gui.git.Status.WorkingTreeState() != enums.REBASE_MODE_NONE && conflictFileCount == 0 && prevConflictFileCount > 0 { - gui.c.OnUIThread(func() error { return gui.helpers.MergeAndRebase.PromptToContinueRebase() }) - } - - fileTreeViewModel.RWMutex.Lock() - - // only taking over the filter if it hasn't already been set by the user. - // Though this does make it impossible for the user to actually say they want to display all if - // conflicts are currently being shown. Hmm. Worth it I reckon. If we need to add some - // extra state here to see if the user's set the filter themselves we can do that, but - // I'd prefer to maintain as little state as possible. - if conflictFileCount > 0 { - if fileTreeViewModel.GetFilter() == filetree.DisplayAll { - fileTreeViewModel.SetFilter(filetree.DisplayConflicted) - } - } else if fileTreeViewModel.GetFilter() == filetree.DisplayConflicted { - fileTreeViewModel.SetFilter(filetree.DisplayAll) - } - - state.Model.Files = files - fileTreeViewModel.SetTree() - fileTreeViewModel.RWMutex.Unlock() - - if err := gui.fileWatcher.addFilesToFileWatcher(files); err != nil { - return err - } - - return nil -} - -// the reflogs panel is the only panel where we cache data, in that we only -// load entries that have been created since we last ran the call. This means -// we need to be more careful with how we use this, and to ensure we're emptying -// the reflogs array when changing contexts. -// This method also manages two things: ReflogCommits and FilteredReflogCommits. -// FilteredReflogCommits are rendered in the reflogs panel, and ReflogCommits -// are used by the branches panel to obtain recency values for sorting. -func (gui *Gui) refreshReflogCommits() error { - // pulling state into its own variable incase it gets swapped out for another state - // and we get an out of bounds exception - state := gui.State - var lastReflogCommit *models.Commit - if len(state.Model.ReflogCommits) > 0 { - lastReflogCommit = state.Model.ReflogCommits[0] - } - - refresh := func(stateCommits *[]*models.Commit, filterPath string) error { - commits, onlyObtainedNewReflogCommits, err := gui.git.Loaders.ReflogCommitLoader. - GetReflogCommits(lastReflogCommit, filterPath) - if err != nil { - return gui.c.Error(err) - } - - if onlyObtainedNewReflogCommits { - *stateCommits = append(commits, *stateCommits...) - } else { - *stateCommits = commits - } - return nil - } - - if err := refresh(&state.Model.ReflogCommits, ""); err != nil { - return err - } - - if gui.State.Modes.Filtering.Active() { - if err := refresh(&state.Model.FilteredReflogCommits, state.Modes.Filtering.GetPath()); err != nil { - return err - } - } else { - state.Model.FilteredReflogCommits = state.Model.ReflogCommits - } - - return gui.c.PostRefreshUpdate(gui.State.Contexts.ReflogCommits) -} - -func (gui *Gui) refreshRemotes() error { - prevSelectedRemote := gui.State.Contexts.Remotes.GetSelected() - - remotes, err := gui.git.Loaders.RemoteLoader.GetRemotes() - if err != nil { - return gui.c.Error(err) - } - - gui.State.Model.Remotes = remotes - - // we need to ensure our selected remote branches aren't now outdated - if prevSelectedRemote != nil && gui.State.Model.RemoteBranches != nil { - // find remote now - for _, remote := range remotes { - if remote.Name == prevSelectedRemote.Name { - gui.State.Model.RemoteBranches = remote.Branches - break - } - } - } - - if err := gui.c.PostRefreshUpdate(gui.State.Contexts.Remotes); err != nil { - return err - } - - if err := gui.c.PostRefreshUpdate(gui.State.Contexts.RemoteBranches); err != nil { - return err - } - - return nil -} - -func (gui *Gui) refreshStashEntries() error { - gui.State.Model.StashEntries = gui.git.Loaders.StashLoader. - GetStashEntries(gui.State.Modes.Filtering.GetPath()) - - return gui.postRefreshUpdate(gui.State.Contexts.Stash) -} - -// never call this on its own, it should only be called from within refreshCommits() -func (gui *Gui) refreshStatus() { - gui.Mutexes.RefreshingStatusMutex.Lock() - defer gui.Mutexes.RefreshingStatusMutex.Unlock() - - currentBranch := gui.helpers.Refs.GetCheckedOutRef() - if currentBranch == nil { - // need to wait for branches to refresh - return - } - status := "" - - if currentBranch.IsRealBranch() { - status += presentation.ColoredBranchStatus(currentBranch, gui.Tr) + " " - } - - workingTreeState := gui.git.Status.WorkingTreeState() - if workingTreeState != enums.REBASE_MODE_NONE { - status += style.FgYellow.Sprintf("(%s) ", formatWorkingTreeState(workingTreeState)) - } - - name := presentation.GetBranchTextStyle(currentBranch.Name).Sprint(currentBranch.Name) - repoName := utils.GetCurrentRepoName() - status += fmt.Sprintf("%s → %s ", repoName, name) - - gui.setViewContent(gui.Views.Status, status) -} - -func (gui *Gui) refreshStagingPanel(focusOpts types.OnFocusOpts) error { - secondaryFocused := gui.secondaryStagingFocused() - - mainSelectedLineIdx := -1 - secondarySelectedLineIdx := -1 - if focusOpts.ClickedViewLineIdx > 0 { - if secondaryFocused { - secondarySelectedLineIdx = focusOpts.ClickedViewLineIdx - } else { - mainSelectedLineIdx = focusOpts.ClickedViewLineIdx - } - } - - mainContext := gui.State.Contexts.Staging - secondaryContext := gui.State.Contexts.StagingSecondary - - file := gui.getSelectedFile() - if file == nil || (!file.HasUnstagedChanges && !file.HasStagedChanges) { - return gui.handleStagingEscape() - } - - mainDiff := gui.git.WorkingTree.WorktreeFileDiff(file, true, false, false) - secondaryDiff := gui.git.WorkingTree.WorktreeFileDiff(file, true, true, false) - - // grabbing locks here and releasing before we finish the function - // because pushing say the secondary context could mean entering this function - // again, and we don't want to have a deadlock - mainContext.GetMutex().Lock() - secondaryContext.GetMutex().Lock() - - mainContext.SetState( - patch_exploring.NewState(mainDiff, mainSelectedLineIdx, mainContext.GetState(), gui.Log), - ) - - secondaryContext.SetState( - patch_exploring.NewState(secondaryDiff, secondarySelectedLineIdx, secondaryContext.GetState(), gui.Log), - ) - - mainState := mainContext.GetState() - secondaryState := secondaryContext.GetState() - - mainContent := mainContext.GetContentToRender(!secondaryFocused) - secondaryContent := secondaryContext.GetContentToRender(secondaryFocused) - - mainContext.GetMutex().Unlock() - secondaryContext.GetMutex().Unlock() - - if mainState == nil && secondaryState == nil { - return gui.handleStagingEscape() - } - - if mainState == nil && !secondaryFocused { - return gui.c.PushContext(secondaryContext, focusOpts) - } - - if secondaryState == nil && secondaryFocused { - return gui.c.PushContext(mainContext, focusOpts) - } - - if secondaryFocused { - gui.State.Contexts.StagingSecondary.FocusSelection() - } else { - gui.State.Contexts.Staging.FocusSelection() - } - - return gui.c.RenderToMainViews(types.RefreshMainOpts{ - Pair: gui.c.MainViewPairs().Staging, - Main: &types.ViewUpdateOpts{ - Task: types.NewRenderStringWithoutScrollTask(mainContent), - Title: gui.Tr.UnstagedChanges, - }, - Secondary: &types.ViewUpdateOpts{ - Task: types.NewRenderStringWithoutScrollTask(secondaryContent), - Title: gui.Tr.StagedChanges, - }, - }) -} - -func (gui *Gui) handleStagingEscape() error { - return gui.c.PushContext(gui.State.Contexts.Files) -} - -func (gui *Gui) secondaryStagingFocused() bool { - return gui.currentStaticContext().GetKey() == gui.State.Contexts.StagingSecondary.GetKey() -} - -func (gui *Gui) refreshPatchBuildingPanel(opts types.OnFocusOpts) error { - selectedLineIdx := -1 - if opts.ClickedWindowName == "main" { - selectedLineIdx = opts.ClickedViewLineIdx - } - - if !gui.git.Patch.PatchBuilder.Active() { - return gui.helpers.PatchBuilding.Escape() - } - - // get diff from commit file that's currently selected - path := gui.State.Contexts.CommitFiles.GetSelectedPath() - if path == "" { - return nil - } - - ref := gui.State.Contexts.CommitFiles.CommitFileTreeViewModel.GetRef() - to := ref.RefName() - from, reverse := gui.State.Modes.Diffing.GetFromAndReverseArgsForDiff(ref.ParentRefName()) - diff, err := gui.git.WorkingTree.ShowFileDiff(from, to, reverse, path, true, - gui.IgnoreWhitespaceInDiffView) - if err != nil { - return err - } - - secondaryDiff := gui.git.Patch.PatchBuilder.RenderPatchForFile(path, false, false) - if err != nil { - return err - } - - context := gui.State.Contexts.CustomPatchBuilder - - oldState := context.GetState() - - state := patch_exploring.NewState(diff, selectedLineIdx, oldState, gui.Log) - context.SetState(state) - if state == nil { - return gui.helpers.PatchBuilding.Escape() - } - - gui.State.Contexts.CustomPatchBuilder.FocusSelection() - - mainContent := context.GetContentToRender(true) - - return gui.c.RenderToMainViews(types.RefreshMainOpts{ - Pair: gui.c.MainViewPairs().PatchBuilding, - Main: &types.ViewUpdateOpts{ - Task: types.NewRenderStringWithoutScrollTask(mainContent), - Title: gui.Tr.Patch, - }, - Secondary: &types.ViewUpdateOpts{ - Task: types.NewRenderStringWithoutScrollTask(secondaryDiff), - Title: gui.Tr.CustomPatch, - }, - }) -} - -func (gui *Gui) refreshMergePanel(isFocused bool) error { - content := gui.State.Contexts.MergeConflicts.GetContentToRender(isFocused) - - var task types.UpdateTask - if gui.State.Contexts.MergeConflicts.IsUserScrolling() { - task = types.NewRenderStringWithoutScrollTask(content) - } else { - originY := gui.State.Contexts.MergeConflicts.GetOriginY() - task = types.NewRenderStringWithScrollTask(content, 0, originY) - } - - return gui.c.RenderToMainViews(types.RefreshMainOpts{ - Pair: gui.c.MainViewPairs().MergeConflicts, - Main: &types.ViewUpdateOpts{ - Task: task, - }, - }) -} - -func (gui *Gui) refreshSubCommitsWithLimit() error { - gui.Mutexes.SubCommitsMutex.Lock() - defer gui.Mutexes.SubCommitsMutex.Unlock() - - context := gui.State.Contexts.SubCommits - - commits, err := gui.git.Loaders.CommitLoader.GetCommits( - git_commands.GetCommitsOptions{ - Limit: context.GetLimitCommits(), - FilterPath: gui.State.Modes.Filtering.GetPath(), - IncludeRebaseCommits: false, - RefName: context.GetRef().FullRefName(), - }, - ) - if err != nil { - return err - } - gui.State.Model.SubCommits = commits - - return gui.c.PostRefreshUpdate(gui.State.Contexts.SubCommits) + return gui.helpers.Refresh.Refresh(options) } diff --git a/pkg/gui/remote_branches_panel.go b/pkg/gui/remote_branches_panel.go deleted file mode 100644 index 6e9a8e779..000000000 --- a/pkg/gui/remote_branches_panel.go +++ /dev/null @@ -1,22 +0,0 @@ -package gui - -import "github.com/jesseduffield/lazygit/pkg/gui/types" - -func (gui *Gui) remoteBranchesRenderToMain() error { - var task types.UpdateTask - remoteBranch := gui.State.Contexts.RemoteBranches.GetSelected() - if remoteBranch == nil { - task = types.NewRenderStringTask("No branches for this remote") - } else { - cmdObj := gui.git.Branch.GetGraphCmdObj(remoteBranch.FullRefName()) - task = types.NewRunCommandTask(cmdObj.GetCmd()) - } - - return gui.c.RenderToMainViews(types.RefreshMainOpts{ - Pair: gui.c.MainViewPairs().Normal, - Main: &types.ViewUpdateOpts{ - Title: "Remote Branch", - Task: task, - }, - }) -} diff --git a/pkg/gui/remotes_panel.go b/pkg/gui/remotes_panel.go deleted file mode 100644 index edaade8a8..000000000 --- a/pkg/gui/remotes_panel.go +++ /dev/null @@ -1,29 +0,0 @@ -package gui - -import ( - "fmt" - "strings" - - "github.com/jesseduffield/lazygit/pkg/gui/style" - "github.com/jesseduffield/lazygit/pkg/gui/types" -) - -// list panel functions - -func (gui *Gui) remotesRenderToMain() error { - var task types.UpdateTask - remote := gui.State.Contexts.Remotes.GetSelected() - if remote == nil { - task = types.NewRenderStringTask("No remotes") - } else { - task = types.NewRenderStringTask(fmt.Sprintf("%s\nUrls:\n%s", style.FgGreen.Sprint(remote.Name), strings.Join(remote.Urls, "\n"))) - } - - return gui.c.RenderToMainViews(types.RefreshMainOpts{ - Pair: gui.c.MainViewPairs().Normal, - Main: &types.ViewUpdateOpts{ - Title: "Remote", - Task: task, - }, - }) -} diff --git a/pkg/gui/searching.go b/pkg/gui/searching.go index a8580655c..270fe1efd 100644 --- a/pkg/gui/searching.go +++ b/pkg/gui/searching.go @@ -48,7 +48,7 @@ func (gui *Gui) onSelectItemWrapper(innerFunc func(int) error) func(int, int, in return func(y int, index int, total int) error { if total == 0 { - return gui.renderString( + gui.c.SetViewContent( gui.Views.Search, fmt.Sprintf( "no matches for '%s' %s", @@ -56,8 +56,9 @@ func (gui *Gui) onSelectItemWrapper(innerFunc func(int) error) func(int, int, in theme.OptionsFgColor.Sprintf("%s: exit search mode", keybindings.Label(keybindingConfig.Universal.Return)), ), ) + return nil } - _ = gui.renderString( + gui.c.SetViewContent( gui.Views.Search, fmt.Sprintf( "matches for '%s' (%d of %d) %s", diff --git a/pkg/gui/side_window.go b/pkg/gui/side_window.go index b57998d00..a1eff7737 100644 --- a/pkg/gui/side_window.go +++ b/pkg/gui/side_window.go @@ -2,7 +2,7 @@ package gui func (gui *Gui) nextSideWindow() error { windows := gui.getCyclableWindows() - currentWindow := gui.currentWindow() + currentWindow := gui.helpers.Window.CurrentWindow() var newWindow string if currentWindow == "" || currentWindow == windows[len(windows)-1] { newWindow = windows[0] @@ -17,18 +17,16 @@ func (gui *Gui) nextSideWindow() error { } } } - if err := gui.resetOrigin(gui.Views.Main); err != nil { - return err - } + gui.c.ResetViewOrigin(gui.Views.Main) - context := gui.getContextForWindow(newWindow) + context := gui.helpers.Window.GetContextForWindow(newWindow) return gui.c.PushContext(context) } func (gui *Gui) previousSideWindow() error { windows := gui.getCyclableWindows() - currentWindow := gui.currentWindow() + currentWindow := gui.helpers.Window.CurrentWindow() var newWindow string if currentWindow == "" || currentWindow == windows[0] { newWindow = windows[len(windows)-1] @@ -43,19 +41,21 @@ func (gui *Gui) previousSideWindow() error { } } } - if err := gui.resetOrigin(gui.Views.Main); err != nil { - return err - } + gui.c.ResetViewOrigin(gui.Views.Main) - context := gui.getContextForWindow(newWindow) + context := gui.helpers.Window.GetContextForWindow(newWindow) return gui.c.PushContext(context) } func (gui *Gui) goToSideWindow(window string) func() error { return func() error { - context := gui.getContextForWindow(window) + context := gui.helpers.Window.GetContextForWindow(window) return gui.c.PushContext(context) } } + +func (gui *Gui) getCyclableWindows() []string { + return []string{"status", "files", "branches", "commits", "stash"} +} diff --git a/pkg/gui/snake.go b/pkg/gui/snake.go deleted file mode 100644 index 9d82275f8..000000000 --- a/pkg/gui/snake.go +++ /dev/null @@ -1,56 +0,0 @@ -package gui - -import ( - "fmt" - "strings" - - "github.com/jesseduffield/lazygit/pkg/gui/style" - "github.com/jesseduffield/lazygit/pkg/snake" -) - -func (gui *Gui) startSnake() { - view := gui.Views.Snake - - game := snake.NewGame(view.Width(), view.Height(), gui.renderSnakeGame, gui.c.LogAction) - gui.snakeGame = game - game.Start() -} - -func (gui *Gui) renderSnakeGame(cells [][]snake.CellType, alive bool) { - view := gui.Views.Snake - - if !alive { - _ = gui.c.ErrorMsg(gui.Tr.YouDied) - return - } - - output := drawSnakeGame(cells) - - view.Clear() - fmt.Fprint(view, output) - gui.c.Render() -} - -func drawSnakeGame(cells [][]snake.CellType) string { - writer := &strings.Builder{} - - for i, row := range cells { - for _, cell := range row { - switch cell { - case snake.None: - writer.WriteString(" ") - case snake.Snake: - writer.WriteString("█") - case snake.Food: - writer.WriteString(style.FgMagenta.Sprint("█")) - } - } - - if i < len(cells) { - writer.WriteString("\n") - } - } - - output := writer.String() - return output -} diff --git a/pkg/gui/stash_panel.go b/pkg/gui/stash_panel.go deleted file mode 100644 index 439d8205e..000000000 --- a/pkg/gui/stash_panel.go +++ /dev/null @@ -1,21 +0,0 @@ -package gui - -import "github.com/jesseduffield/lazygit/pkg/gui/types" - -func (gui *Gui) stashRenderToMain() error { - var task types.UpdateTask - stashEntry := gui.State.Contexts.Stash.GetSelected() - if stashEntry == nil { - task = types.NewRenderStringTask(gui.c.Tr.NoStashEntries) - } else { - task = types.NewRunPtyTask(gui.git.Stash.ShowStashEntryCmdObj(stashEntry.Index).GetCmd()) - } - - return gui.c.RenderToMainViews(types.RefreshMainOpts{ - Pair: gui.c.MainViewPairs().Normal, - Main: &types.ViewUpdateOpts{ - Title: "Stash", - Task: task, - }, - }) -} diff --git a/pkg/gui/status_panel.go b/pkg/gui/status_panel.go deleted file mode 100644 index 75f69b736..000000000 --- a/pkg/gui/status_panel.go +++ /dev/null @@ -1,141 +0,0 @@ -package gui - -import ( - "errors" - "fmt" - "strings" - - "github.com/jesseduffield/generics/slices" - "github.com/jesseduffield/lazygit/pkg/commands/types/enums" - "github.com/jesseduffield/lazygit/pkg/constants" - "github.com/jesseduffield/lazygit/pkg/gui/presentation" - "github.com/jesseduffield/lazygit/pkg/gui/style" - "github.com/jesseduffield/lazygit/pkg/gui/types" - "github.com/jesseduffield/lazygit/pkg/utils" -) - -func runeCount(str string) int { - return len([]rune(str)) -} - -func cursorInSubstring(cx int, prefix string, substring string) bool { - return cx >= runeCount(prefix) && cx < runeCount(prefix+substring) -} - -func (gui *Gui) handleCheckForUpdate() error { - return gui.c.WithWaitingStatus(gui.c.Tr.CheckingForUpdates, func() error { - gui.Updater.CheckForNewUpdate(gui.onUserUpdateCheckFinish, true) - return nil - }) -} - -func (gui *Gui) handleStatusClick() error { - // TODO: move into some abstraction (status is currently not a listViewContext where a lot of this code lives) - currentBranch := gui.helpers.Refs.GetCheckedOutRef() - if currentBranch == nil { - // need to wait for branches to refresh - return nil - } - - if err := gui.c.PushContext(gui.State.Contexts.Status); err != nil { - return err - } - - cx, _ := gui.Views.Status.Cursor() - upstreamStatus := presentation.BranchStatus(currentBranch, gui.Tr) - repoName := utils.GetCurrentRepoName() - workingTreeState := gui.git.Status.WorkingTreeState() - switch workingTreeState { - case enums.REBASE_MODE_REBASING, enums.REBASE_MODE_MERGING: - workingTreeStatus := fmt.Sprintf("(%s)", formatWorkingTreeState(workingTreeState)) - if cursorInSubstring(cx, upstreamStatus+" ", workingTreeStatus) { - return gui.helpers.MergeAndRebase.CreateRebaseOptionsMenu() - } - if cursorInSubstring(cx, upstreamStatus+" "+workingTreeStatus+" ", repoName) { - return gui.handleCreateRecentReposMenu() - } - default: - if cursorInSubstring(cx, upstreamStatus+" ", repoName) { - return gui.handleCreateRecentReposMenu() - } - } - - return nil -} - -func formatWorkingTreeState(rebaseMode enums.RebaseMode) string { - switch rebaseMode { - case enums.REBASE_MODE_REBASING: - return "rebasing" - case enums.REBASE_MODE_MERGING: - return "merging" - default: - return "none" - } -} - -func (gui *Gui) statusRenderToMain() error { - dashboardString := strings.Join( - []string{ - lazygitTitle(), - "Copyright 2022 Jesse Duffield", - fmt.Sprintf("Keybindings: %s", constants.Links.Docs.Keybindings), - fmt.Sprintf("Config Options: %s", constants.Links.Docs.Config), - fmt.Sprintf("Tutorial: %s", constants.Links.Docs.Tutorial), - fmt.Sprintf("Raise an Issue: %s", constants.Links.Issues), - fmt.Sprintf("Release Notes: %s", constants.Links.Releases), - style.FgMagenta.Sprintf("Become a sponsor: %s", constants.Links.Donate), // caffeine ain't free - }, "\n\n") - - return gui.c.RenderToMainViews(types.RefreshMainOpts{ - Pair: gui.c.MainViewPairs().Normal, - Main: &types.ViewUpdateOpts{ - Title: gui.c.Tr.StatusTitle, - Task: types.NewRenderStringTask(dashboardString), - }, - }) -} - -func (gui *Gui) askForConfigFile(action func(file string) error) error { - confPaths := gui.Config.GetUserConfigPaths() - switch len(confPaths) { - case 0: - return errors.New(gui.c.Tr.NoConfigFileFoundErr) - case 1: - return action(confPaths[0]) - default: - menuItems := slices.Map(confPaths, func(path string) *types.MenuItem { - return &types.MenuItem{ - Label: path, - OnPress: func() error { - return action(path) - }, - } - }) - - return gui.c.Menu(types.CreateMenuOptions{ - Title: gui.c.Tr.SelectConfigFile, - Items: menuItems, - }) - } -} - -func (gui *Gui) handleOpenConfig() error { - return gui.askForConfigFile(gui.helpers.Files.OpenFile) -} - -func (gui *Gui) handleEditConfig() error { - return gui.askForConfigFile(gui.helpers.Files.EditFile) -} - -func lazygitTitle() string { - return ` - _ _ _ - | | (_) | - | | __ _ _____ _ __ _ _| |_ - | |/ _` + "`" + ` |_ / | | |/ _` + "`" + ` | | __| - | | (_| |/ /| |_| | (_| | | |_ - |_|\__,_/___|\__, |\__, |_|\__| - __/ | __/ | - |___/ |___/ ` -} diff --git a/pkg/gui/sub_commits_panel.go b/pkg/gui/sub_commits_panel.go deleted file mode 100644 index bcc63bbb7..000000000 --- a/pkg/gui/sub_commits_panel.go +++ /dev/null @@ -1,43 +0,0 @@ -package gui - -import ( - "github.com/jesseduffield/lazygit/pkg/gui/types" - "github.com/jesseduffield/lazygit/pkg/utils" -) - -// list panel functions - -func (gui *Gui) onSubCommitFocus() error { - context := gui.State.Contexts.SubCommits - if context.GetSelectedLineIdx() > COMMIT_THRESHOLD && context.GetLimitCommits() { - context.SetLimitCommits(false) - go utils.Safe(func() { - if err := gui.refreshSubCommitsWithLimit(); err != nil { - _ = gui.c.Error(err) - } - }) - } - - return nil -} - -func (gui *Gui) subCommitsRenderToMain() error { - commit := gui.State.Contexts.SubCommits.GetSelected() - var task types.UpdateTask - if commit == nil { - task = types.NewRenderStringTask("No commits") - } else { - cmdObj := gui.git.Commit.ShowCmdObj(commit.Sha, gui.State.Modes.Filtering.GetPath(), - gui.IgnoreWhitespaceInDiffView) - - task = types.NewRunPtyTask(cmdObj.GetCmd()) - } - - return gui.c.RenderToMainViews(types.RefreshMainOpts{ - Pair: gui.c.MainViewPairs().Normal, - Main: &types.ViewUpdateOpts{ - Title: "Commit", - Task: task, - }, - }) -} diff --git a/pkg/gui/submodules_panel.go b/pkg/gui/submodules_panel.go deleted file mode 100644 index 163234153..000000000 --- a/pkg/gui/submodules_panel.go +++ /dev/null @@ -1,51 +0,0 @@ -package gui - -import ( - "fmt" - "os" - - "github.com/jesseduffield/lazygit/pkg/commands/models" - "github.com/jesseduffield/lazygit/pkg/gui/style" - "github.com/jesseduffield/lazygit/pkg/gui/types" -) - -func (gui *Gui) submodulesRenderToMain() error { - var task types.UpdateTask - submodule := gui.State.Contexts.Submodules.GetSelected() - if submodule == nil { - task = types.NewRenderStringTask("No submodules") - } else { - prefix := fmt.Sprintf( - "Name: %s\nPath: %s\nUrl: %s\n\n", - style.FgGreen.Sprint(submodule.Name), - style.FgYellow.Sprint(submodule.Path), - style.FgCyan.Sprint(submodule.Url), - ) - - file := gui.helpers.WorkingTree.FileForSubmodule(submodule) - if file == nil { - task = types.NewRenderStringTask(prefix) - } else { - cmdObj := gui.git.WorkingTree.WorktreeFileDiffCmdObj(file, false, !file.HasUnstagedChanges && file.HasStagedChanges, gui.IgnoreWhitespaceInDiffView) - task = types.NewRunCommandTaskWithPrefix(cmdObj.GetCmd(), prefix) - } - } - - return gui.c.RenderToMainViews(types.RefreshMainOpts{ - Pair: gui.c.MainViewPairs().Normal, - Main: &types.ViewUpdateOpts{ - Title: "Submodule", - Task: task, - }, - }) -} - -func (gui *Gui) enterSubmodule(submodule *models.SubmoduleConfig) error { - wd, err := os.Getwd() - if err != nil { - return err - } - gui.RepoPathStack.Push(wd) - - return gui.dispatchSwitchToRepo(submodule.Path, true) -} diff --git a/pkg/gui/suggestions_panel.go b/pkg/gui/suggestions_panel.go index d7b8b0d2b..50332079e 100644 --- a/pkg/gui/suggestions_panel.go +++ b/pkg/gui/suggestions_panel.go @@ -21,6 +21,6 @@ func (gui *Gui) getSelectedSuggestion() *types.Suggestion { func (gui *Gui) setSuggestions(suggestions []*types.Suggestion) { gui.State.Suggestions = suggestions gui.State.Contexts.Suggestions.SetSelectedLineIdx(0) - _ = gui.resetOrigin(gui.Views.Suggestions) + gui.c.ResetViewOrigin(gui.Views.Suggestions) _ = gui.State.Contexts.Suggestions.HandleRender() } diff --git a/pkg/gui/tags_panel.go b/pkg/gui/tags_panel.go deleted file mode 100644 index af09e4242..000000000 --- a/pkg/gui/tags_panel.go +++ /dev/null @@ -1,22 +0,0 @@ -package gui - -import "github.com/jesseduffield/lazygit/pkg/gui/types" - -func (gui *Gui) tagsRenderToMain() error { - var task types.UpdateTask - tag := gui.State.Contexts.Tags.GetSelected() - if tag == nil { - task = types.NewRenderStringTask("No tags") - } else { - cmdObj := gui.git.Branch.GetGraphCmdObj(tag.FullRefName()) - task = types.NewRunCommandTask(cmdObj.GetCmd()) - } - - return gui.c.RenderToMainViews(types.RefreshMainOpts{ - Pair: gui.c.MainViewPairs().Normal, - Main: &types.ViewUpdateOpts{ - Title: "Tag", - Task: task, - }, - }) -} diff --git a/pkg/gui/tasks_adapter.go b/pkg/gui/tasks_adapter.go index 69b64d7b1..2bd7be4f5 100644 --- a/pkg/gui/tasks_adapter.go +++ b/pkg/gui/tasks_adapter.go @@ -49,7 +49,7 @@ func (gui *Gui) newStringTaskWithoutScroll(view *gocui.View, str string) error { manager := gui.getManager(view) f := func(stop chan struct{}) error { - gui.setViewContent(view, str) + gui.c.SetViewContent(view, str) return nil } @@ -66,7 +66,7 @@ func (gui *Gui) newStringTaskWithScroll(view *gocui.View, str string, originX in manager := gui.getManager(view) f := func(stop chan struct{}) error { - gui.setViewContent(view, str) + gui.c.SetViewContent(view, str) _ = view.SetOrigin(originX, originY) return nil } @@ -82,7 +82,9 @@ func (gui *Gui) newStringTaskWithKey(view *gocui.View, str string, key string) e manager := gui.getManager(view) f := func(stop chan struct{}) error { - return gui.renderString(view, str) + gui.c.ResetViewOrigin(view) + gui.c.SetViewContent(view, str) + return nil } if err := manager.NewTask(f, key); err != nil { diff --git a/pkg/gui/types/common.go b/pkg/gui/types/common.go index aeb1da4c0..a92efb801 100644 --- a/pkg/gui/types/common.go +++ b/pkg/gui/types/common.go @@ -1,12 +1,15 @@ package types import ( + "github.com/jesseduffield/gocui" + "github.com/jesseduffield/lazygit/pkg/commands" "github.com/jesseduffield/lazygit/pkg/commands/git_commands" "github.com/jesseduffield/lazygit/pkg/commands/models" "github.com/jesseduffield/lazygit/pkg/commands/oscommands" "github.com/jesseduffield/lazygit/pkg/commands/types/enums" "github.com/jesseduffield/lazygit/pkg/common" "github.com/jesseduffield/lazygit/pkg/config" + "github.com/jesseduffield/lazygit/pkg/utils" "github.com/sasha-s/go-deadlock" "gopkg.in/ozeidan/fuzzy-patricia.v3/patricia" ) @@ -27,6 +30,12 @@ type IGuiCommon interface { // e.g. expanding or collapsing a folder in a file view. Calling 'Refresh' in this // case would be overkill, although refresh will internally call 'PostRefreshUpdate' PostRefreshUpdate(Context) error + + // renders string to a view without resetting its origin + SetViewContent(view *gocui.View, content string) + // resets cursor and origin of view. Often used before calling SetViewContent + ResetViewOrigin(view *gocui.View) + // this just re-renders the screen Render() // allows rendering to main views (i.e. the ones to the right of the side panel) @@ -42,12 +51,15 @@ type IGuiCommon interface { PushContext(context Context, opts ...OnFocusOpts) error PopContext() error + ReplaceContext(context Context) error CurrentContext() Context CurrentStaticContext() Context + CurrentSideContext() Context IsCurrentContext(Context) bool // enters search mode for the current view OpenSearch() + GetConfig() config.AppConfigurer GetAppState() *config.AppState SaveAppState() error @@ -55,6 +67,21 @@ type IGuiCommon interface { // Only necessary to call if you're not already on the UI thread i.e. you're inside a goroutine. // All controller handlers are executed on the UI thread. OnUIThread(f func() error) + + // returns the gocui Gui struct. There is a good chance you don't actually want to use + // this struct and instead want to use another method above + GocuiGui() *gocui.Gui + + Views() Views + + Git() *commands.GitCommand + OS() *oscommands.OSCommand + Model() *Model + Modes() *Modes + + Mutexes() Mutexes + + State() IStateAccessor } type IPopupHandler interface { @@ -176,3 +203,36 @@ type Mutexes struct { PopupMutex *deadlock.Mutex PtyMutex *deadlock.Mutex } + +type IStateAccessor interface { + GetIgnoreWhitespaceInDiffView() bool + SetIgnoreWhitespaceInDiffView(value bool) + GetRepoPathStack() *utils.StringStack + GetRepoState() IRepoStateAccessor + // tells us whether we're currently updating lazygit + GetUpdating() bool + SetUpdating(bool) + SetIsRefreshingFiles(bool) + GetIsRefreshingFiles() bool +} + +type IRepoStateAccessor interface { + GetViewsSetup() bool + GetWindowViewNameMap() *utils.ThreadSafeMap[string, string] + GetStartupStage() StartupStage + SetStartupStage(stage StartupStage) + GetCurrentPopupOpts() *CreatePopupPanelOpts + SetCurrentPopupOpts(*CreatePopupPanelOpts) +} + +// startup stages so we don't need to load everything at once +type StartupStage int + +const ( + INITIAL StartupStage = iota + COMPLETE +) + +type IFileWatcher interface { + AddFilesToFileWatcher(files []*models.File) error +} diff --git a/pkg/gui/types/context.go b/pkg/gui/types/context.go index e88d0d0f9..ef57e06bc 100644 --- a/pkg/gui/types/context.go +++ b/pkg/gui/types/context.go @@ -72,6 +72,10 @@ type IBaseContext interface { // our list controller can come along and wrap it in a list-specific click handler. // We'll need to think of a better way to do this. AddOnClickFn(func() error) + + AddOnRenderToMainFn(func() error) + AddOnFocusFn(func(OnFocusOpts) error) + AddOnFocusLostFn(func(OnFocusLostOpts) error) } type Context interface { @@ -83,6 +87,16 @@ type Context interface { HandleRenderToMain() error } +type DiffableContext interface { + Context + + // Returns the current diff terminals of the currently selected item. + // in the case of a branch it returns both the branch and it's upstream name, + // which becomes an option when you bring up the diff menu, but when you're just + // flicking through branches it will be using the local branch name. + GetDiffTerminals() []string +} + type IListContext interface { Context @@ -150,6 +164,9 @@ type HasKeybindings interface { GetKeybindings(opts KeybindingsOpts) []*Binding GetMouseKeybindings(opts KeybindingsOpts) []*gocui.ViewMouseBinding GetOnClick() func() error + GetOnRenderToMain() func() error + GetOnFocus() func(OnFocusOpts) error + GetOnFocusLost() func(OnFocusLostOpts) error } type IController interface { diff --git a/pkg/gui/types/refresh.go b/pkg/gui/types/refresh.go index 475b90942..6d6c6f8a4 100644 --- a/pkg/gui/types/refresh.go +++ b/pkg/gui/types/refresh.go @@ -6,6 +6,7 @@ type RefreshableView int const ( COMMITS RefreshableView = iota REBASE_COMMITS + SUB_COMMITS BRANCHES FILES STASH @@ -32,6 +33,6 @@ const ( type RefreshOptions struct { Then func() - Scope []RefreshableView // e.g. []int{COMMITS, BRANCHES}. Leave empty to refresh everything + Scope []RefreshableView // e.g. []RefreshableView{COMMITS, BRANCHES}. Leave empty to refresh everything Mode RefreshMode // one of SYNC (default), ASYNC, and BLOCK_UI } diff --git a/pkg/gui/types/views.go b/pkg/gui/types/views.go new file mode 100644 index 000000000..ee45cf7b6 --- /dev/null +++ b/pkg/gui/types/views.go @@ -0,0 +1,42 @@ +package types + +import "github.com/jesseduffield/gocui" + +type Views struct { + Status *gocui.View + Submodules *gocui.View + Files *gocui.View + Branches *gocui.View + Remotes *gocui.View + Tags *gocui.View + RemoteBranches *gocui.View + ReflogCommits *gocui.View + Commits *gocui.View + Stash *gocui.View + + Main *gocui.View + Secondary *gocui.View + Staging *gocui.View + StagingSecondary *gocui.View + PatchBuilding *gocui.View + PatchBuildingSecondary *gocui.View + MergeConflicts *gocui.View + + Options *gocui.View + Confirmation *gocui.View + Menu *gocui.View + CommitMessage *gocui.View + CommitFiles *gocui.View + SubCommits *gocui.View + Information *gocui.View + AppStatus *gocui.View + Search *gocui.View + SearchPrefix *gocui.View + Limit *gocui.View + Suggestions *gocui.View + Tooltip *gocui.View + Extras *gocui.View + + // for playing the easter egg snake game + Snake *gocui.View +} diff --git a/pkg/gui/updates.go b/pkg/gui/updates.go deleted file mode 100644 index 93231e4f0..000000000 --- a/pkg/gui/updates.go +++ /dev/null @@ -1,85 +0,0 @@ -package gui - -import ( - "github.com/jesseduffield/gocui" - "github.com/jesseduffield/lazygit/pkg/gui/types" - "github.com/jesseduffield/lazygit/pkg/utils" -) - -func (gui *Gui) showUpdatePrompt(newVersion string) error { - message := utils.ResolvePlaceholderString( - gui.Tr.UpdateAvailable, map[string]string{ - "newVersion": newVersion, - }, - ) - - return gui.c.Confirm(types.ConfirmOpts{ - Title: gui.Tr.UpdateAvailableTitle, - Prompt: message, - HandleConfirm: func() error { - gui.startUpdating(newVersion) - return nil - }, - }) -} - -func (gui *Gui) onUserUpdateCheckFinish(newVersion string, err error) error { - if err != nil { - return gui.c.Error(err) - } - if newVersion == "" { - return gui.c.ErrorMsg(gui.Tr.FailedToRetrieveLatestVersionErr) - } - return gui.showUpdatePrompt(newVersion) -} - -func (gui *Gui) onBackgroundUpdateCheckFinish(newVersion string, err error) error { - if err != nil { - // ignoring the error for now so that I'm not annoying users - gui.c.Log.Error(err.Error()) - return nil - } - if newVersion == "" { - return nil - } - if gui.c.UserConfig.Update.Method == "background" { - gui.startUpdating(newVersion) - return nil - } - return gui.showUpdatePrompt(newVersion) -} - -func (gui *Gui) startUpdating(newVersion string) { - gui.State.Updating = true - statusId := gui.statusManager.addWaitingStatus(gui.Tr.UpdateInProgressWaitingStatus) - gui.Updater.Update(newVersion, func(err error) error { return gui.onUpdateFinish(statusId, err) }) -} - -func (gui *Gui) onUpdateFinish(statusId int, err error) error { - gui.State.Updating = false - gui.statusManager.removeStatus(statusId) - gui.c.OnUIThread(func() error { - _ = gui.renderString(gui.Views.AppStatus, "") - if err != nil { - errMessage := utils.ResolvePlaceholderString( - gui.Tr.UpdateFailedErr, map[string]string{ - "errMessage": err.Error(), - }, - ) - return gui.c.ErrorMsg(errMessage) - } - return gui.c.Alert(gui.Tr.UpdateCompletedTitle, gui.Tr.UpdateCompleted) - }) - - return nil -} - -func (gui *Gui) createUpdateQuitConfirmation() error { - return gui.c.Confirm(types.ConfirmOpts{ - Title: gui.Tr.ConfirmQuitDuringUpdateTitle, - Prompt: gui.Tr.ConfirmQuitDuringUpdate, - HandleConfirm: func() error { - return gocui.ErrQuit - }, - }) -} diff --git a/pkg/gui/view_helpers.go b/pkg/gui/view_helpers.go index 8f2055245..7e6e6510e 100644 --- a/pkg/gui/view_helpers.go +++ b/pkg/gui/view_helpers.go @@ -1,19 +1,21 @@ package gui import ( - "fmt" - "github.com/jesseduffield/gocui" - "github.com/jesseduffield/lazygit/pkg/gui/keybindings" "github.com/jesseduffield/lazygit/pkg/gui/types" "github.com/jesseduffield/lazygit/pkg/tasks" "github.com/jesseduffield/lazygit/pkg/utils" "github.com/spkg/bom" ) -func (gui *Gui) resetOrigin(v *gocui.View) error { - _ = v.SetCursor(0, 0) - return v.SetOrigin(0, 0) +func (gui *Gui) resetViewOrigin(v *gocui.View) { + if err := v.SetCursor(0, 0); err != nil { + gui.Log.Error(err) + } + + if err := v.SetOrigin(0, 0); err != nil { + gui.Log.Error(err) + } } // Returns the number of lines that we should read initially from a cmd task so @@ -52,18 +54,6 @@ func (gui *Gui) setViewContent(v *gocui.View, s string) { v.SetContent(gui.cleanString(s)) } -// renderString resets the origin of a view and sets its content -func (gui *Gui) renderString(view *gocui.View, s string) error { - if err := view.SetOrigin(0, 0); err != nil { - return err - } - if err := view.SetCursor(0, 0); err != nil { - return err - } - gui.setViewContent(view, s) - return nil -} - func (gui *Gui) currentViewName() string { currentView := gui.g.CurrentView() if currentView == nil { @@ -129,20 +119,6 @@ func (gui *Gui) resizeConfirmationPanel() { _, _ = gui.g.SetView(gui.Views.Suggestions.Name(), x0, suggestionsViewTop, x1, suggestionsViewTop+suggestionsViewHeight, 0) } -func (gui *Gui) globalOptionsMap() map[string]string { - keybindingConfig := gui.c.UserConfig.Keybinding - - return map[string]string{ - fmt.Sprintf("%s/%s", keybindings.Label(keybindingConfig.Universal.ScrollUpMain), keybindings.Label(keybindingConfig.Universal.ScrollDownMain)): gui.c.Tr.LcScroll, - fmt.Sprintf("%s %s %s %s", keybindings.Label(keybindingConfig.Universal.PrevBlock), keybindings.Label(keybindingConfig.Universal.NextBlock), keybindings.Label(keybindingConfig.Universal.PrevItem), keybindings.Label(keybindingConfig.Universal.NextItem)): gui.c.Tr.LcNavigate, - keybindings.Label(keybindingConfig.Universal.Return): gui.c.Tr.LcCancel, - keybindings.Label(keybindingConfig.Universal.Quit): gui.c.Tr.LcQuit, - keybindings.Label(keybindingConfig.Universal.OptionMenuAlt1): gui.c.Tr.LcMenu, - fmt.Sprintf("%s-%s", keybindings.Label(keybindingConfig.Universal.JumpToBlock[0]), keybindings.Label(keybindingConfig.Universal.JumpToBlock[len(keybindingConfig.Universal.JumpToBlock)-1])): gui.c.Tr.LcJump, - fmt.Sprintf("%s/%s", keybindings.Label(keybindingConfig.Universal.ScrollLeft), keybindings.Label(keybindingConfig.Universal.ScrollRight)): gui.c.Tr.LcScrollLeftRight, - } -} - func (gui *Gui) isPopupPanel(viewName string) bool { return viewName == "commitMessage" || viewName == "confirmation" || viewName == "menu" } @@ -159,7 +135,7 @@ func (gui *Gui) onViewTabClick(windowName string, tabIndex int) error { viewName := tabs[tabIndex].ViewName - context, ok := gui.contextForView(viewName) + context, ok := gui.helpers.View.ContextForView(viewName) if !ok { return nil } @@ -167,21 +143,6 @@ func (gui *Gui) onViewTabClick(windowName string, tabIndex int) error { return gui.c.PushContext(context) } -func (gui *Gui) contextForView(viewName string) (types.Context, bool) { - view, err := gui.g.View(viewName) - if err != nil { - return nil, false - } - - for _, context := range gui.State.Contexts.Flatten() { - if context.GetViewName() == view.Name() { - return context, true - } - } - - return nil, false -} - func (gui *Gui) handleNextTab() error { view := getTabbedView(gui) if view == nil { @@ -220,7 +181,7 @@ func (gui *Gui) handlePrevTab() error { func getTabbedView(gui *Gui) *gocui.View { // It safe assumption that only static contexts have tabs - context := gui.currentStaticContext() + context := gui.c.CurrentStaticContext() view, _ := gui.g.View(context.GetViewName()) return view } @@ -228,3 +189,20 @@ func getTabbedView(gui *Gui) *gocui.View { func (gui *Gui) render() { gui.c.OnUIThread(func() error { return nil }) } + +// postRefreshUpdate is to be called on a context after the state that it depends on has been refreshed +// if the context's view is set to another context we do nothing. +// if the context's view is the current view we trigger a focus; re-selecting the current item. +func (gui *Gui) postRefreshUpdate(c types.Context) error { + if err := c.HandleRender(); err != nil { + return err + } + + if gui.currentViewName() == c.GetViewName() { + if err := c.HandleFocus(types.OnFocusOpts{}); err != nil { + return err + } + } + + return nil +} diff --git a/pkg/gui/views.go b/pkg/gui/views.go index 468cb9830..65d7d3f3a 100644 --- a/pkg/gui/views.go +++ b/pkg/gui/views.go @@ -6,45 +6,6 @@ import ( "github.com/jesseduffield/lazygit/pkg/theme" ) -type Views struct { - Status *gocui.View - Submodules *gocui.View - Files *gocui.View - Branches *gocui.View - Remotes *gocui.View - Tags *gocui.View - RemoteBranches *gocui.View - ReflogCommits *gocui.View - Commits *gocui.View - Stash *gocui.View - - Main *gocui.View - Secondary *gocui.View - Staging *gocui.View - StagingSecondary *gocui.View - PatchBuilding *gocui.View - PatchBuildingSecondary *gocui.View - MergeConflicts *gocui.View - - Options *gocui.View - Confirmation *gocui.View - Menu *gocui.View - CommitMessage *gocui.View - CommitFiles *gocui.View - SubCommits *gocui.View - Information *gocui.View - AppStatus *gocui.View - Search *gocui.View - SearchPrefix *gocui.View - Limit *gocui.View - Suggestions *gocui.View - Tooltip *gocui.View - Extras *gocui.View - - // for playing the easter egg snake game - Snake *gocui.View -} - type viewNameMapping struct { viewPtr **gocui.View name string @@ -104,15 +65,6 @@ func (gui *Gui) orderedViewNameMappings() []viewNameMapping { } } -func (gui *Gui) windowForView(viewName string) string { - context, ok := gui.contextForView(viewName) - if !ok { - panic("todo: deal with this") - } - - return context.GetWindowName() -} - func (gui *Gui) createAllViews() error { frameRunes := []rune{'─', '│', '┌', '┐', '└', '┘'} switch gui.c.UserConfig.Gui.Border { @@ -140,7 +92,7 @@ func (gui *Gui) createAllViews() error { gui.Views.SearchPrefix.BgColor = gocui.ColorDefault gui.Views.SearchPrefix.FgColor = gocui.ColorGreen gui.Views.SearchPrefix.Frame = false - gui.setViewContent(gui.Views.SearchPrefix, SEARCH_PREFIX) + gui.c.SetViewContent(gui.Views.SearchPrefix, SEARCH_PREFIX) gui.Views.Stash.Title = gui.c.Tr.StashTitle diff --git a/pkg/gui/whitespace-toggle.go b/pkg/gui/whitespace-toggle.go index b4ee798cc..dfbf541ff 100644 --- a/pkg/gui/whitespace-toggle.go +++ b/pkg/gui/whitespace-toggle.go @@ -13,5 +13,5 @@ func (gui *Gui) toggleWhitespaceInDiffView() error { } gui.c.Toast(toastMessage) - return gui.currentSideListContext().HandleFocus(types.OnFocusOpts{}) + return gui.c.CurrentSideContext().HandleFocus(types.OnFocusOpts{}) } diff --git a/pkg/gui/window.go b/pkg/gui/window.go deleted file mode 100644 index c5355bcea..000000000 --- a/pkg/gui/window.go +++ /dev/null @@ -1,121 +0,0 @@ -package gui - -import ( - "fmt" - - "github.com/jesseduffield/gocui" - "github.com/jesseduffield/lazygit/pkg/gui/context" - "github.com/jesseduffield/lazygit/pkg/gui/types" - "github.com/jesseduffield/lazygit/pkg/utils" - "github.com/samber/lo" -) - -// 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 that moves between windows - -func (gui *Gui) initialWindowViewNameMap(contextTree *context.ContextTree) *utils.ThreadSafeMap[string, string] { - result := utils.NewThreadSafeMap[string, string]() - - for _, context := range contextTree.Flatten() { - result.Set(context.GetWindowName(), context.GetViewName()) - } - - return result -} - -func (gui *Gui) getViewNameForWindow(window string) string { - viewName, ok := gui.State.WindowViewNameMap.Get(window) - if !ok { - panic(fmt.Sprintf("Viewname not found for window: %s", window)) - } - - return viewName -} - -func (gui *Gui) getContextForWindow(window string) types.Context { - viewName := gui.getViewNameForWindow(window) - - context, ok := gui.contextForView(viewName) - if !ok { - panic("TODO: fix this") - } - - return context -} - -// for now all we actually care about is the context's view so we're storing that -func (gui *Gui) setWindowContext(c types.Context) { - if c.IsTransient() { - gui.resetWindowContext(c) - } - - gui.State.WindowViewNameMap.Set(c.GetWindowName(), c.GetViewName()) -} - -func (gui *Gui) currentWindow() string { - return gui.currentContext().GetWindowName() -} - -// assumes the context's windowName has been set to the new window if necessary -func (gui *Gui) resetWindowContext(c types.Context) { - for _, windowName := range gui.State.WindowViewNameMap.Keys() { - viewName, ok := gui.State.WindowViewNameMap.Get(windowName) - if !ok { - continue - } - if viewName == c.GetViewName() && windowName != c.GetWindowName() { - for _, context := range gui.State.Contexts.Flatten() { - if context.GetKey() != c.GetKey() && context.GetWindowName() == windowName { - gui.State.WindowViewNameMap.Set(windowName, context.GetViewName()) - } - } - } - } -} - -// moves given context's view to the top of the window -func (gui *Gui) moveToTopOfWindow(context types.Context) { - view := context.GetView() - if view == nil { - return - } - - window := context.GetWindowName() - - topView := gui.topViewInWindow(window) - - if view.Name() != topView.Name() { - if err := gui.g.SetViewOnTopOf(view.Name(), topView.Name()); err != nil { - gui.Log.Error(err) - } - } -} - -func (gui *Gui) topViewInWindow(windowName string) *gocui.View { - // now I need to find all views in that same window, via contexts. And I guess then I need to find the index of the highest view in that list. - viewNamesInWindow := gui.viewNamesInWindow(windowName) - - // The views list is ordered highest-last, so we're grabbing the last view of the window - var topView *gocui.View - for _, currentView := range gui.g.Views() { - if lo.Contains(viewNamesInWindow, currentView.Name()) { - topView = currentView - } - } - - return topView -} - -func (gui *Gui) viewNamesInWindow(windowName string) []string { - result := []string{} - for _, context := range gui.State.Contexts.Flatten() { - if context.GetWindowName() == windowName { - result = append(result, context.GetViewName()) - } - } - - return result -} diff --git a/pkg/updates/updates.go b/pkg/updates/updates.go index 58c93fa7d..d375c0eda 100644 --- a/pkg/updates/updates.go +++ b/pkg/updates/updates.go @@ -235,13 +235,8 @@ func (u *Updater) getBinaryUrl(newVersion string) string { } // Update downloads the latest binary and replaces the current binary with it -func (u *Updater) Update(newVersion string, onFinish func(error) error) { - go utils.Safe(func() { - err := u.update(newVersion) - if err = onFinish(err); err != nil { - u.Log.Error(err) - } - }) +func (u *Updater) Update(newVersion string) error { + return u.update(newVersion) } func (u *Updater) update(newVersion string) error {