1
0
mirror of https://github.com/jesseduffield/lazygit.git synced 2025-06-19 00:28:03 +02:00

Begin refactoring gui

This begins a big refactor of moving more code out of the Gui struct into contexts, controllers, and helpers. We also move some code into structs in the
gui package purely for the sake of better encapsulation
This commit is contained in:
Jesse Duffield
2022-12-30 23:24:24 +11:00
parent 826128a8e0
commit 8edad826ca
101 changed files with 3331 additions and 2877 deletions

View File

@ -33,7 +33,6 @@ type App struct {
Config config.AppConfigurer Config config.AppConfigurer
OSCommand *oscommands.OSCommand OSCommand *oscommands.OSCommand
Gui *gui.Gui Gui *gui.Gui
Updater *updates.Updater // may only need this on the Gui
} }
func Run( 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)) app.OSCommand = oscommands.NewOSCommand(common, config, oscommands.GetPlatform(), oscommands.NewNullGuiIO(app.Log))
var err error updater, err := updates.NewUpdater(common, config, app.OSCommand)
app.Updater, err = updates.NewUpdater(common, config, app.OSCommand)
if err != nil { if err != nil {
return app, err return app, err
} }
@ -108,7 +106,7 @@ func NewApp(config config.AppConfigurer, common *common.Common) (*App, error) {
return app, err 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 { if err != nil {
return app, err return app, err
} }

View File

@ -99,7 +99,8 @@ func (gui *Gui) renderAppStatus() {
for range ticker.C { for range ticker.C {
appStatus := gui.statusManager.getStatusString() appStatus := gui.statusManager.getStatusString()
gui.c.OnUIThread(func() error { gui.c.OnUIThread(func() error {
return gui.renderString(gui.Views.AppStatus, appStatus) gui.c.SetViewContent(gui.Views.AppStatus, appStatus)
return nil
}) })
if appStatus == "" { if appStatus == "" {

View File

@ -13,10 +13,14 @@ import (
const INFO_SECTION_PADDING = " " const INFO_SECTION_PADDING = " "
func (gui *Gui) getWindowDimensions(informationStr string, appStatus string) map[string]boxlayout.Dimensions { type WindowArranger struct {
width, height := gui.g.Size() 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 sidePanelsDirection := boxlayout.COLUMN
portraitMode := width <= 84 && height > 45 portraitMode := width <= 84 && height > 45
@ -25,13 +29,13 @@ func (gui *Gui) getWindowDimensions(informationStr string, appStatus string) map
} }
mainPanelsDirection := boxlayout.ROW mainPanelsDirection := boxlayout.ROW
if gui.splitMainPanelSideBySide() { if self.splitMainPanelSideBySide() {
mainPanelsDirection = boxlayout.COLUMN 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 infoSectionSize := 0
if showInfoSection { if showInfoSection {
infoSectionSize = 1 infoSectionSize = 1
@ -47,7 +51,7 @@ func (gui *Gui) getWindowDimensions(informationStr string, appStatus string) map
{ {
Direction: boxlayout.ROW, Direction: boxlayout.ROW,
Weight: sideSectionWeight, Weight: sideSectionWeight,
ConditionalChildren: gui.sidePanelChildren, ConditionalChildren: self.sidePanelChildren,
}, },
{ {
Direction: boxlayout.ROW, Direction: boxlayout.ROW,
@ -55,7 +59,7 @@ func (gui *Gui) getWindowDimensions(informationStr string, appStatus string) map
Children: []*boxlayout.Box{ Children: []*boxlayout.Box{
{ {
Direction: mainPanelsDirection, Direction: mainPanelsDirection,
Children: gui.mainSectionChildren(), Children: self.mainSectionChildren(),
Weight: 1, Weight: 1,
}, },
{ {
@ -69,7 +73,7 @@ func (gui *Gui) getWindowDimensions(informationStr string, appStatus string) map
{ {
Direction: boxlayout.COLUMN, Direction: boxlayout.COLUMN,
Size: infoSectionSize, 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 return result
} }
func (gui *Gui) mainSectionChildren() []*boxlayout.Box { func (self *WindowArranger) mainSectionChildren() []*boxlayout.Box {
currentWindow := gui.currentWindow() currentWindow := self.gui.helpers.Window.CurrentWindow()
// if we're not in split mode we can just show the one main panel. Likewise if // 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 // 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{ return []*boxlayout.Box{
{ {
Window: "main", Window: "main",
@ -117,27 +121,27 @@ func (gui *Gui) mainSectionChildren() []*boxlayout.Box {
} }
} }
func (gui *Gui) getMidSectionWeights() (int, int) { func (self *WindowArranger) getMidSectionWeights() (int, int) {
currentWindow := gui.currentWindow() 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 // 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 // we could make this better by creating ratios like 2:3 rather than always 1:something
mainSectionWeight := int(1/sidePanelWidthRatio) - 1 mainSectionWeight := int(1/sidePanelWidthRatio) - 1
sideSectionWeight := 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 mainSectionWeight = 5 // need to shrink side panel to make way for main panels if side-by-side
} }
if currentWindow == "main" { 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 sideSectionWeight = 0
} }
} else { } else {
if gui.State.ScreenMode == SCREEN_HALF { if self.gui.State.ScreenMode == SCREEN_HALF {
mainSectionWeight = 1 mainSectionWeight = 1
} else if gui.State.ScreenMode == SCREEN_FULL { } else if self.gui.State.ScreenMode == SCREEN_FULL {
mainSectionWeight = 0 mainSectionWeight = 0
} }
} }
@ -145,8 +149,8 @@ func (gui *Gui) getMidSectionWeights() (int, int) {
return sideSectionWeight, mainSectionWeight return sideSectionWeight, mainSectionWeight
} }
func (gui *Gui) infoSectionChildren(informationStr string, appStatus string) []*boxlayout.Box { func (self *WindowArranger) infoSectionChildren(informationStr string, appStatus string) []*boxlayout.Box {
if gui.State.Searching.isSearching { if self.gui.State.Searching.isSearching {
return []*boxlayout.Box{ return []*boxlayout.Box{
{ {
Window: "searchPrefix", Window: "searchPrefix",
@ -162,7 +166,7 @@ func (gui *Gui) infoSectionChildren(informationStr string, appStatus string) []*
appStatusBox := &boxlayout.Box{Window: "appStatus"} appStatusBox := &boxlayout.Box{Window: "appStatus"}
optionsBox := &boxlayout.Box{Window: "options"} optionsBox := &boxlayout.Box{Window: "options"}
if !gui.c.UserConfig.Gui.ShowBottomLine { if !self.gui.c.UserConfig.Gui.ShowBottomLine {
optionsBox.Weight = 0 optionsBox.Weight = 0
appStatusBox.Weight = 1 appStatusBox.Weight = 1
} else { } else {
@ -172,7 +176,7 @@ func (gui *Gui) infoSectionChildren(informationStr string, appStatus string) []*
result := []*boxlayout.Box{appStatusBox, optionsBox} 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{ result = append(result, &boxlayout.Box{
Window: "information", Window: "information",
// unlike appStatus, informationStr has various colors so we need to decolorise before taking the length // 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 return result
} }
func (gui *Gui) splitMainPanelSideBySide() bool { func (self *WindowArranger) splitMainPanelSideBySide() bool {
if !gui.isMainPanelSplit() { if !self.gui.isMainPanelSplit() {
return false return false
} }
mainPanelSplitMode := gui.c.UserConfig.Gui.MainPanelSplitMode mainPanelSplitMode := self.gui.c.UserConfig.Gui.MainPanelSplitMode
width, height := gui.g.Size() width, height := self.gui.g.Size()
switch mainPanelSplitMode { switch mainPanelSplitMode {
case "vertical": case "vertical":
@ -205,18 +209,18 @@ func (gui *Gui) splitMainPanelSideBySide() bool {
} }
} }
func (gui *Gui) getExtrasWindowSize(screenHeight int) int { func (self *WindowArranger) getExtrasWindowSize(screenHeight int) int {
if !gui.ShowExtrasWindow { if !self.gui.ShowExtrasWindow {
return 0 return 0
} }
var baseSize int 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' baseSize = 1000 // my way of saying 'fill the available space'
} else if screenHeight < 40 { } else if screenHeight < 40 {
baseSize = 1 baseSize = 1
} else { } else {
baseSize = gui.c.UserConfig.Gui.CommandLogSize baseSize = self.gui.c.UserConfig.Gui.CommandLogSize
} }
frameSize := 2 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 // 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 // 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. // then when it's accessed it will have weight 2, not 1.
func (gui *Gui) getDefaultStashWindowBox() *boxlayout.Box { func (self *WindowArranger) getDefaultStashWindowBox() *boxlayout.Box {
gui.State.ContextManager.RLock() self.gui.State.ContextMgr.RLock()
defer gui.State.ContextManager.RUnlock() defer self.gui.State.ContextMgr.RUnlock()
box := &boxlayout.Box{Window: "stash"} box := &boxlayout.Box{Window: "stash"}
stashWindowAccessed := false stashWindowAccessed := false
for _, context := range gui.State.ContextManager.ContextStack { for _, context := range self.gui.State.ContextMgr.ContextStack {
if context.GetWindowName() == "stash" { if context.GetWindowName() == "stash" {
stashWindowAccessed = true stashWindowAccessed = true
} }
@ -248,10 +252,10 @@ func (gui *Gui) getDefaultStashWindowBox() *boxlayout.Box {
return box return box
} }
func (gui *Gui) sidePanelChildren(width int, height int) []*boxlayout.Box { func (self *WindowArranger) sidePanelChildren(width int, height int) []*boxlayout.Box {
currentWindow := gui.currentSideWindowName() 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 { fullHeightBox := func(window string) *boxlayout.Box {
if window == currentWindow { if window == currentWindow {
return &boxlayout.Box{ return &boxlayout.Box{
@ -274,7 +278,7 @@ func (gui *Gui) sidePanelChildren(width int, height int) []*boxlayout.Box {
fullHeightBox("stash"), fullHeightBox("stash"),
} }
} else if height >= 28 { } else if height >= 28 {
accordionMode := gui.c.UserConfig.Gui.ExpandFocusedSidePanel accordionMode := self.gui.c.UserConfig.Gui.ExpandFocusedSidePanel
accordionBox := func(defaultBox *boxlayout.Box) *boxlayout.Box { accordionBox := func(defaultBox *boxlayout.Box) *boxlayout.Box {
if accordionMode && defaultBox.Window == currentWindow { if accordionMode && defaultBox.Window == currentWindow {
return &boxlayout.Box{ 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: "files", Weight: 1}),
accordionBox(&boxlayout.Box{Window: "branches", Weight: 1}), accordionBox(&boxlayout.Box{Window: "branches", Weight: 1}),
accordionBox(&boxlayout.Box{Window: "commits", Weight: 1}), accordionBox(&boxlayout.Box{Window: "commits", Weight: 1}),
accordionBox(gui.getDefaultStashWindowBox()), accordionBox(self.getDefaultStashWindowBox()),
} }
} else { } else {
squashedHeight := 1 squashedHeight := 1
@ -326,18 +330,14 @@ func (gui *Gui) sidePanelChildren(width int, height int) []*boxlayout.Box {
} }
} }
func (gui *Gui) getCyclableWindows() []string { func (self *WindowArranger) currentSideWindowName() string {
return []string{"status", "files", "branches", "commits", "stash"}
}
func (gui *Gui) currentSideWindowName() string {
// there is always one and only one cyclable context in the context stack. We'll look from top to bottom // there is always one and only one cyclable context in the context stack. We'll look from top to bottom
gui.State.ContextManager.RLock() self.gui.State.ContextMgr.RLock()
defer gui.State.ContextManager.RUnlock() defer self.gui.State.ContextMgr.RUnlock()
for idx := range gui.State.ContextManager.ContextStack { for idx := range self.gui.State.ContextMgr.ContextStack {
reversedIdx := len(gui.State.ContextManager.ContextStack) - 1 - idx reversedIdx := len(self.gui.State.ContextMgr.ContextStack) - 1 - idx
context := gui.State.ContextManager.ContextStack[reversedIdx] context := self.gui.State.ContextMgr.ContextStack[reversedIdx]
if context.GetKind() == types.SIDE_CONTEXT { if context.GetKind() == types.SIDE_CONTEXT {
return context.GetWindowName() return context.GetWindowName()

View File

@ -4,18 +4,33 @@ import (
"strings" "strings"
"time" "time"
"github.com/jesseduffield/lazygit/pkg/commands/git_commands"
"github.com/jesseduffield/lazygit/pkg/gui/types"
"github.com/jesseduffield/lazygit/pkg/utils" "github.com/jesseduffield/lazygit/pkg/utils"
) )
func (gui *Gui) startBackgroundRoutines() { type BackgroundRoutineMgr struct {
userConfig := gui.UserConfig 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 { if userConfig.Git.AutoFetch {
fetchInterval := userConfig.Refresher.FetchInterval fetchInterval := userConfig.Refresher.FetchInterval
if fetchInterval > 0 { if fetchInterval > 0 {
go utils.Safe(gui.startBackgroundFetch) go utils.Safe(self.startBackgroundFetch)
} else { } else {
gui.c.Log.Errorf( self.gui.c.Log.Errorf(
"Value of config option 'refresher.fetchInterval' (%d) is invalid, disabling auto-fetch", "Value of config option 'refresher.fetchInterval' (%d) is invalid, disabling auto-fetch",
fetchInterval) fetchInterval)
} }
@ -24,42 +39,44 @@ func (gui *Gui) startBackgroundRoutines() {
if userConfig.Git.AutoRefresh { if userConfig.Git.AutoRefresh {
refreshInterval := userConfig.Refresher.RefreshInterval refreshInterval := userConfig.Refresher.RefreshInterval
if refreshInterval > 0 { 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 { } else {
gui.c.Log.Errorf( self.gui.c.Log.Errorf(
"Value of config option 'refresher.refreshInterval' (%d) is invalid, disabling auto-refresh", "Value of config option 'refresher.refreshInterval' (%d) is invalid, disabling auto-refresh",
refreshInterval) refreshInterval)
} }
} }
} }
func (gui *Gui) startBackgroundFetch() { func (self *BackgroundRoutineMgr) startBackgroundFetch() {
gui.waitForIntro.Wait() self.gui.waitForIntro.Wait()
isNew := gui.IsNewRepo isNew := self.gui.IsNewRepo
userConfig := gui.UserConfig userConfig := self.gui.UserConfig
if !isNew { if !isNew {
time.After(time.Duration(userConfig.Refresher.FetchInterval) * time.Second) 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 { 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 { } else {
gui.goEvery(time.Second*time.Duration(userConfig.Refresher.FetchInterval), gui.stopChan, func() error { self.goEvery(time.Second*time.Duration(userConfig.Refresher.FetchInterval), self.gui.stopChan, func() error {
err := gui.backgroundFetch() err := self.backgroundFetch()
gui.render() self.gui.c.Render()
return err 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() { go utils.Safe(func() {
ticker := time.NewTicker(interval) ticker := time.NewTicker(interval)
defer ticker.Stop() defer ticker.Stop()
for { for {
select { select {
case <-ticker.C: case <-ticker.C:
if gui.PauseBackgroundThreads { if self.pauseBackgroundThreads {
continue continue
} }
_ = function() _ = 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
}

View File

@ -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,
},
})
}

View File

@ -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)
}

View File

@ -21,7 +21,8 @@ func (gui *Gui) handleCommitMessageFocused() error {
gui.RenderCommitLength() gui.RenderCommitLength()
return gui.renderString(gui.Views.Options, message) gui.c.SetViewContent(gui.Views.Options, message)
return nil
} }
func (gui *Gui) RenderCommitLength() { func (gui *Gui) RenderCommitLength() {

View File

@ -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()
}

View File

@ -195,10 +195,8 @@ func (gui *Gui) createPopupPanel(ctx context.Context, opts types.CreatePopupPane
gui.resizeConfirmationPanel() gui.resizeConfirmationPanel()
confirmationView.RenderTextArea() confirmationView.RenderTextArea()
} else { } else {
if err := gui.renderString(confirmationView, style.AttrBold.Sprint(opts.Prompt)); err != nil { gui.c.ResetViewOrigin(confirmationView)
cancel() gui.c.SetViewContent(confirmationView, style.AttrBold.Sprint(opts.Prompt))
return err
}
} }
if err := gui.setKeyBindings(cancel, opts); err != nil { 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 var onConfirm func() error
if opts.HandleConfirmPrompt != nil { if opts.HandleConfirmPrompt != nil {
onConfirm = gui.wrappedPromptConfirmationFunction(cancel, opts.HandleConfirmPrompt, func() string { return gui.Views.Confirmation.TextArea.GetContent() }) 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), Key: keybindings.GetKey(keybindingConfig.Universal.TogglePanel),
Handler: func() error { Handler: func() error {
if len(gui.State.Suggestions) > 0 { if len(gui.State.Suggestions) > 0 {
return gui.replaceContext(gui.State.Contexts.Suggestions) return gui.c.ReplaceContext(gui.State.Contexts.Suggestions)
} }
return nil return nil
}, },
@ -269,7 +267,7 @@ func (gui *Gui) setKeyBindings(cancel context.CancelFunc, opts types.CreatePopup
{ {
ViewName: "suggestions", ViewName: "suggestions",
Key: keybindings.GetKey(keybindingConfig.Universal.TogglePanel), 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
} }

View File

@ -1,12 +1,10 @@
package gui package gui
import ( import (
"sort" "errors"
"strings" "sync"
"github.com/jesseduffield/generics/maps"
"github.com/jesseduffield/generics/slices" "github.com/jesseduffield/generics/slices"
"github.com/jesseduffield/gocui"
"github.com/jesseduffield/lazygit/pkg/gui/context" "github.com/jesseduffield/lazygit/pkg/gui/context"
"github.com/jesseduffield/lazygit/pkg/gui/types" "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 // you in the menu context. When contexts are activated/deactivated certain things need
// to happen like showing/hiding views and rendering content. // to happen like showing/hiding views and rendering content.
func (gui *Gui) popupViewNames() []string { type ContextMgr struct {
popups := slices.Filter(gui.State.Contexts.Flatten(), func(c types.Context) bool { ContextStack []types.Context
return c.GetKind() == types.PERSISTENT_POPUP || c.GetKind() == types.TEMPORARY_POPUP sync.RWMutex
}) gui *Gui
}
return slices.Map(popups, func(c types.Context) string { func NewContextMgr(initialContext types.Context, gui *Gui) ContextMgr {
return c.GetViewName() return ContextMgr{
}) ContextStack: []types.Context{},
RWMutex: sync.RWMutex{},
gui: gui,
}
} }
// use replaceContext when you don't want to return to the original context upon // 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. // 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() { if !c.IsFocusable() {
return nil return nil
} }
gui.State.ContextManager.Lock() self.Lock()
if len(gui.State.ContextManager.ContextStack) == 0 { if len(self.ContextStack) == 0 {
gui.State.ContextManager.ContextStack = []types.Context{c} self.ContextStack = []types.Context{c}
} else { } else {
// replace the last item with the given item // 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() { if !c.IsFocusable() {
return nil return nil
} }
contextsToDeactivate, contextToActivate := gui.pushToContextStack(c) contextsToDeactivate, contextToActivate := self.pushToContextStack(c)
for _, contextToDeactivate := range contextsToDeactivate { 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 return err
} }
} }
@ -64,43 +76,43 @@ func (gui *Gui) pushContext(c types.Context, opts types.OnFocusOpts) error {
return nil 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 // Adjusts the context stack based on the context that's being pushed and
// returns (contexts to deactivate, context to activate) // 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{} contextsToDeactivate := []types.Context{}
gui.State.ContextManager.Lock() self.Lock()
defer gui.State.ContextManager.Unlock() defer self.Unlock()
if len(gui.State.ContextManager.ContextStack) > 0 && if len(self.ContextStack) > 0 &&
c == gui.State.ContextManager.ContextStack[len(gui.State.ContextManager.ContextStack)-1] { c == self.ContextStack[len(self.ContextStack)-1] {
// Context being pushed is already on top of the stack: nothing to // Context being pushed is already on top of the stack: nothing to
// deactivate or activate // deactivate or activate
return contextsToDeactivate, nil return contextsToDeactivate, nil
} }
if len(gui.State.ContextManager.ContextStack) == 0 { if len(self.ContextStack) == 0 {
gui.State.ContextManager.ContextStack = append(gui.State.ContextManager.ContextStack, c) self.ContextStack = append(self.ContextStack, c)
} else if c.GetKind() == types.SIDE_CONTEXT { } else if c.GetKind() == types.SIDE_CONTEXT {
// if we are switching to a side context, remove all other contexts in the stack // if we are switching to a side context, remove all other contexts in the stack
contextsToDeactivate = gui.State.ContextManager.ContextStack contextsToDeactivate = self.ContextStack
gui.State.ContextManager.ContextStack = []types.Context{c} self.ContextStack = []types.Context{c}
} else if c.GetKind() == types.MAIN_CONTEXT { } else if c.GetKind() == types.MAIN_CONTEXT {
// if we're switching to a main context, remove all other main contexts in the stack // if we're switching to a main context, remove all other main contexts in the stack
contextsToKeep := []types.Context{} contextsToKeep := []types.Context{}
for _, stackContext := range gui.State.ContextManager.ContextStack { for _, stackContext := range self.ContextStack {
if stackContext.GetKind() == types.MAIN_CONTEXT { if stackContext.GetKind() == types.MAIN_CONTEXT {
contextsToDeactivate = append(contextsToDeactivate, stackContext) contextsToDeactivate = append(contextsToDeactivate, stackContext)
} else { } else {
contextsToKeep = append(contextsToKeep, stackContext) contextsToKeep = append(contextsToKeep, stackContext)
} }
} }
gui.State.ContextManager.ContextStack = append(contextsToKeep, c) self.ContextStack = append(contextsToKeep, c)
} else { } else {
topContext := gui.currentContextWithoutLock() topContext := self.currentContextWithoutLock()
// if we're pushing the same context on, we do nothing. // if we're pushing the same context on, we do nothing.
if topContext.GetKey() != c.GetKey() { 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) { (topContext.GetKind() == types.MAIN_CONTEXT && c.GetKind() == types.MAIN_CONTEXT) {
contextsToDeactivate = append(contextsToDeactivate, topContext) 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 return contextsToDeactivate, c
} }
func (gui *Gui) popContext() error { func (self *ContextMgr) popContext() error {
gui.State.ContextManager.Lock() self.Lock()
if len(gui.State.ContextManager.ContextStack) == 1 { if len(self.ContextStack) == 1 {
// cannot escape from bottommost context // cannot escape from bottommost context
gui.State.ContextManager.Unlock() self.Unlock()
return nil return nil
} }
var currentContext types.Context 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 err
} }
return gui.activateContext(newContext, types.OnFocusOpts{}) return self.activateContext(newContext, types.OnFocusOpts{})
} }
func (gui *Gui) deactivateContext(c types.Context, opts types.OnFocusLostOpts) error { func (self *ContextMgr) deactivateContext(c types.Context, opts types.OnFocusLostOpts) error {
view, _ := gui.g.View(c.GetViewName()) view, _ := self.gui.c.GocuiGui().View(c.GetViewName())
if view != nil && view.IsSearching() { if view != nil && view.IsSearching() {
if err := gui.onSearchEscape(); err != nil { if err := self.gui.onSearchEscape(); err != nil {
return err return err
} }
} }
@ -169,34 +181,17 @@ func (gui *Gui) deactivateContext(c types.Context, opts types.OnFocusLostOpts) e
return nil return nil
} }
// postRefreshUpdate is to be called on a context after the state that it depends on has been refreshed func (self *ContextMgr) activateContext(c types.Context, opts types.OnFocusOpts) error {
// 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 {
viewName := c.GetViewName() viewName := c.GetViewName()
v, err := gui.g.View(viewName) v, err := self.gui.c.GocuiGui().View(viewName)
if err != nil { if err != nil {
return err return err
} }
gui.setWindowContext(c) self.gui.helpers.Window.SetWindowContext(c)
gui.moveToTopOfWindow(c) self.gui.helpers.Window.MoveToTopOfWindow(c)
if _, err := gui.g.SetCurrentView(viewName); err != nil { if _, err := self.gui.c.GocuiGui().SetCurrentView(viewName); err != nil {
return err return err
} }
@ -207,14 +202,9 @@ func (gui *Gui) activateContext(c types.Context, opts types.OnFocusOpts) error {
v.Visible = true 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 self.gui.renderContextOptionsMap(c)
optionsMap := c.GetOptionsMap()
if optionsMap == nil {
optionsMap = gui.globalOptionsMap()
}
gui.renderOptionsMap(optionsMap)
if err := c.HandleFocus(opts); err != nil { if err := c.HandleFocus(opts); err != nil {
return err return err
@ -223,62 +213,31 @@ func (gui *Gui) activateContext(c types.Context, opts types.OnFocusOpts) error {
return nil return nil
} }
func (gui *Gui) optionsMapToString(optionsMap map[string]string) string { func (self *ContextMgr) currentContext() types.Context {
options := maps.MapToSlice(optionsMap, func(key string, description string) string { self.RLock()
return key + ": " + description defer self.RUnlock()
})
sort.Strings(options) return self.currentContextWithoutLock()
return strings.Join(options, ", ")
} }
func (gui *Gui) renderOptionsMap(optionsMap map[string]string) { func (self *ContextMgr) currentContextWithoutLock() types.Context {
_ = gui.renderString(gui.Views.Options, gui.optionsMapToString(optionsMap)) if len(self.ContextStack) == 0 {
} return self.gui.defaultSideContext()
// // 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()
} }
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 // Note that this could return the 'status' context which is not itself a list context.
// quite the same as currentSideContext() func (self *ContextMgr) currentSideContext() types.Context {
func (gui *Gui) currentSideListContext() types.IListContext { self.RLock()
context := gui.currentSideContext() defer self.RUnlock()
listContext, ok := context.(types.IListContext)
if !ok {
return nil
}
return listContext
}
func (gui *Gui) currentSideContext() types.Context { stack := self.ContextStack
gui.State.ContextManager.RLock()
defer gui.State.ContextManager.RUnlock()
stack := gui.State.ContextManager.ContextStack
// on startup the stack can be empty so we'll return an empty string in that case // on startup the stack can be empty so we'll return an empty string in that case
if len(stack) == 0 { 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 // 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 // static as opposed to popup
func (gui *Gui) currentStaticContext() types.Context { func (self *ContextMgr) currentStaticContext() types.Context {
gui.State.ContextManager.RLock() self.RLock()
defer gui.State.ContextManager.RUnlock() defer self.RUnlock()
return gui.currentStaticContextWithoutLock() return self.currentStaticContextWithoutLock()
} }
func (gui *Gui) currentStaticContextWithoutLock() types.Context { func (self *ContextMgr) currentStaticContextWithoutLock() types.Context {
stack := gui.State.ContextManager.ContextStack stack := self.ContextStack
if len(stack) == 0 { if len(stack) == 0 {
return gui.defaultSideContext() return self.gui.defaultSideContext()
} }
// find the first context in the stack without a popup type // 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
// }

View File

@ -16,6 +16,9 @@ type BaseContext struct {
keybindingsFns []types.KeybindingsFn keybindingsFns []types.KeybindingsFn
mouseKeybindingsFns []types.MouseKeybindingsFn mouseKeybindingsFns []types.MouseKeybindingsFn
onClickFn func() error onClickFn func() error
onRenderToMainFn func() error
onFocusFn onFocusFn
onFocusLostFn onFocusLostFn
focusable bool focusable bool
transient bool transient bool
@ -25,6 +28,11 @@ type BaseContext struct {
*ParentContextMgr *ParentContextMgr
} }
type (
onFocusFn = func(types.OnFocusOpts) error
onFocusLostFn = func(types.OnFocusLostOpts) error
)
var _ types.IBaseContext = &BaseContext{} var _ types.IBaseContext = &BaseContext{}
type NewBaseContextOpts struct { type NewBaseContextOpts struct {
@ -129,6 +137,36 @@ func (self *BaseContext) GetOnClick() func() error {
return self.onClickFn 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 { func (self *BaseContext) GetMouseKeybindings(opts types.KeybindingsOpts) []*gocui.ViewMouseBinding {
bindings := []*gocui.ViewMouseBinding{} bindings := []*gocui.ViewMouseBinding{}
for i := range self.mouseKeybindingsFns { for i := range self.mouseKeybindingsFns {

View File

@ -11,22 +11,21 @@ type BranchesContext struct {
*ListContextTrait *ListContextTrait
} }
var _ types.IListContext = (*BranchesContext)(nil) var (
_ types.IListContext = (*BranchesContext)(nil)
_ types.DiffableContext = (*BranchesContext)(nil)
)
func NewBranchesContext( func NewBranchesContext(
getModel func() []*models.Branch, getModel func() []*models.Branch,
view *gocui.View, view *gocui.View,
getDisplayStrings func(startIdx int, length int) [][]string, getDisplayStrings func(startIdx int, length int) [][]string,
onFocus func(types.OnFocusOpts) error,
onRenderToMain func() error,
onFocusLost func(opts types.OnFocusLostOpts) error,
c *types.HelperCommon, c *types.HelperCommon,
) *BranchesContext { ) *BranchesContext {
viewModel := NewBasicViewModel(getModel) viewModel := NewBasicViewModel(getModel)
return &BranchesContext{ self := &BranchesContext{
BasicViewModel: viewModel, BasicViewModel: viewModel,
ListContextTrait: &ListContextTrait{ ListContextTrait: &ListContextTrait{
Context: NewSimpleContext(NewBaseContext(NewBaseContextOpts{ Context: NewSimpleContext(NewBaseContext(NewBaseContextOpts{
@ -35,16 +34,14 @@ func NewBranchesContext(
Key: LOCAL_BRANCHES_CONTEXT_KEY, Key: LOCAL_BRANCHES_CONTEXT_KEY,
Kind: types.SIDE_CONTEXT, Kind: types.SIDE_CONTEXT,
Focusable: true, Focusable: true,
}), ContextCallbackOpts{ }), ContextCallbackOpts{}),
OnFocus: onFocus,
OnFocusLost: onFocusLost,
OnRenderToMain: onRenderToMain,
}),
list: viewModel, list: viewModel,
getDisplayStrings: getDisplayStrings, getDisplayStrings: getDisplayStrings,
c: c, c: c,
}, },
} }
return self
} }
func (self *BranchesContext) GetSelectedItemId() string { func (self *BranchesContext) GetSelectedItemId() string {
@ -63,3 +60,16 @@ func (self *BranchesContext) GetSelectedRef() types.Ref {
} }
return branch 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
}

View File

@ -13,17 +13,16 @@ type CommitFilesContext struct {
*DynamicTitleBuilder *DynamicTitleBuilder
} }
var _ types.IListContext = (*CommitFilesContext)(nil) var (
_ types.IListContext = (*CommitFilesContext)(nil)
_ types.DiffableContext = (*CommitFilesContext)(nil)
)
func NewCommitFilesContext( func NewCommitFilesContext(
getModel func() []*models.CommitFile, getModel func() []*models.CommitFile,
view *gocui.View, view *gocui.View,
getDisplayStrings func(startIdx int, length int) [][]string, getDisplayStrings func(startIdx int, length int) [][]string,
onFocus func(types.OnFocusOpts) error,
onRenderToMain func() error,
onFocusLost func(opts types.OnFocusLostOpts) error,
c *types.HelperCommon, c *types.HelperCommon,
) *CommitFilesContext { ) *CommitFilesContext {
viewModel := filetree.NewCommitFileTreeViewModel(getModel, c.Log, c.UserConfig.Gui.ShowFileTree) viewModel := filetree.NewCommitFileTreeViewModel(getModel, c.Log, c.UserConfig.Gui.ShowFileTree)
@ -41,11 +40,7 @@ func NewCommitFilesContext(
Focusable: true, Focusable: true,
Transient: true, Transient: true,
}), }),
ContextCallbackOpts{ ContextCallbackOpts{}),
OnFocus: onFocus,
OnFocusLost: onFocusLost,
OnRenderToMain: onRenderToMain,
}),
list: viewModel, list: viewModel,
getDisplayStrings: getDisplayStrings, getDisplayStrings: getDisplayStrings,
c: c, c: c,
@ -61,3 +56,50 @@ func (self *CommitFilesContext) GetSelectedItemId() string {
return item.ID() 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
}

View File

@ -11,17 +11,16 @@ type LocalCommitsContext struct {
*ViewportListContextTrait *ViewportListContextTrait
} }
var _ types.IListContext = (*LocalCommitsContext)(nil) var (
_ types.IListContext = (*LocalCommitsContext)(nil)
_ types.DiffableContext = (*LocalCommitsContext)(nil)
)
func NewLocalCommitsContext( func NewLocalCommitsContext(
getModel func() []*models.Commit, getModel func() []*models.Commit,
view *gocui.View, view *gocui.View,
getDisplayStrings func(startIdx int, length int) [][]string, getDisplayStrings func(startIdx int, length int) [][]string,
onFocus func(types.OnFocusOpts) error,
onRenderToMain func() error,
onFocusLost func(opts types.OnFocusLostOpts) error,
c *types.HelperCommon, c *types.HelperCommon,
) *LocalCommitsContext { ) *LocalCommitsContext {
viewModel := NewLocalCommitsViewModel(getModel, c) viewModel := NewLocalCommitsViewModel(getModel, c)
@ -36,11 +35,7 @@ func NewLocalCommitsContext(
Key: LOCAL_COMMITS_CONTEXT_KEY, Key: LOCAL_COMMITS_CONTEXT_KEY,
Kind: types.SIDE_CONTEXT, Kind: types.SIDE_CONTEXT,
Focusable: true, Focusable: true,
}), ContextCallbackOpts{ }), ContextCallbackOpts{}),
OnFocus: onFocus,
OnFocusLost: onFocusLost,
OnRenderToMain: onRenderToMain,
}),
list: viewModel, list: viewModel,
getDisplayStrings: getDisplayStrings, getDisplayStrings: getDisplayStrings,
c: c, c: c,
@ -91,6 +86,12 @@ func (self *LocalCommitsContext) GetSelectedRef() types.Ref {
return commit return commit
} }
func (self *LocalCommitsContext) GetDiffTerminals() []string {
itemId := self.GetSelectedItemId()
return []string{itemId}
}
func (self *LocalCommitsViewModel) SetLimitCommits(value bool) { func (self *LocalCommitsViewModel) SetLimitCommits(value bool) {
self.limitCommits = value self.limitCommits = value
} }

View File

@ -24,12 +24,6 @@ func NewMenuContext(
) *MenuContext { ) *MenuContext {
viewModel := NewMenuViewModel() viewModel := NewMenuViewModel()
onFocus := func(types.OnFocusOpts) error {
selectedMenuItem := viewModel.GetSelected()
renderToDescriptionView(selectedMenuItem.Tooltip)
return nil
}
return &MenuContext{ return &MenuContext{
MenuViewModel: viewModel, MenuViewModel: viewModel,
ListContextTrait: &ListContextTrait{ ListContextTrait: &ListContextTrait{
@ -41,9 +35,7 @@ func NewMenuContext(
OnGetOptionsMap: getOptionsMap, OnGetOptionsMap: getOptionsMap,
Focusable: true, Focusable: true,
HasUncontrolledBounds: true, HasUncontrolledBounds: true,
}), ContextCallbackOpts{ }), ContextCallbackOpts{}),
OnFocus: onFocus,
}),
getDisplayStrings: viewModel.GetDisplayStrings, getDisplayStrings: viewModel.GetDisplayStrings,
list: viewModel, list: viewModel,
c: c, c: c,

View File

@ -11,17 +11,16 @@ type ReflogCommitsContext struct {
*ListContextTrait *ListContextTrait
} }
var _ types.IListContext = (*ReflogCommitsContext)(nil) var (
_ types.IListContext = (*ReflogCommitsContext)(nil)
_ types.DiffableContext = (*ReflogCommitsContext)(nil)
)
func NewReflogCommitsContext( func NewReflogCommitsContext(
getModel func() []*models.Commit, getModel func() []*models.Commit,
view *gocui.View, view *gocui.View,
getDisplayStrings func(startIdx int, length int) [][]string, getDisplayStrings func(startIdx int, length int) [][]string,
onFocus func(types.OnFocusOpts) error,
onRenderToMain func() error,
onFocusLost func(opts types.OnFocusLostOpts) error,
c *types.HelperCommon, c *types.HelperCommon,
) *ReflogCommitsContext { ) *ReflogCommitsContext {
viewModel := NewBasicViewModel(getModel) viewModel := NewBasicViewModel(getModel)
@ -35,11 +34,7 @@ func NewReflogCommitsContext(
Key: REFLOG_COMMITS_CONTEXT_KEY, Key: REFLOG_COMMITS_CONTEXT_KEY,
Kind: types.SIDE_CONTEXT, Kind: types.SIDE_CONTEXT,
Focusable: true, Focusable: true,
}), ContextCallbackOpts{ }), ContextCallbackOpts{}),
OnFocus: onFocus,
OnFocusLost: onFocusLost,
OnRenderToMain: onRenderToMain,
}),
list: viewModel, list: viewModel,
getDisplayStrings: getDisplayStrings, getDisplayStrings: getDisplayStrings,
c: c, c: c,
@ -71,3 +66,9 @@ func (self *ReflogCommitsContext) GetSelectedRef() types.Ref {
func (self *ReflogCommitsContext) GetCommits() []*models.Commit { func (self *ReflogCommitsContext) GetCommits() []*models.Commit {
return self.getModel() return self.getModel()
} }
func (self *ReflogCommitsContext) GetDiffTerminals() []string {
itemId := self.GetSelectedItemId()
return []string{itemId}
}

View File

@ -12,17 +12,16 @@ type RemoteBranchesContext struct {
*DynamicTitleBuilder *DynamicTitleBuilder
} }
var _ types.IListContext = (*RemoteBranchesContext)(nil) var (
_ types.IListContext = (*RemoteBranchesContext)(nil)
_ types.DiffableContext = (*RemoteBranchesContext)(nil)
)
func NewRemoteBranchesContext( func NewRemoteBranchesContext(
getModel func() []*models.RemoteBranch, getModel func() []*models.RemoteBranch,
view *gocui.View, view *gocui.View,
getDisplayStrings func(startIdx int, length int) [][]string, getDisplayStrings func(startIdx int, length int) [][]string,
onFocus func(types.OnFocusOpts) error,
onRenderToMain func() error,
onFocusLost func(opts types.OnFocusLostOpts) error,
c *types.HelperCommon, c *types.HelperCommon,
) *RemoteBranchesContext { ) *RemoteBranchesContext {
viewModel := NewBasicViewModel(getModel) viewModel := NewBasicViewModel(getModel)
@ -38,11 +37,7 @@ func NewRemoteBranchesContext(
Kind: types.SIDE_CONTEXT, Kind: types.SIDE_CONTEXT,
Focusable: true, Focusable: true,
Transient: true, Transient: true,
}), ContextCallbackOpts{ }), ContextCallbackOpts{}),
OnFocus: onFocus,
OnFocusLost: onFocusLost,
OnRenderToMain: onRenderToMain,
}),
list: viewModel, list: viewModel,
getDisplayStrings: getDisplayStrings, getDisplayStrings: getDisplayStrings,
c: c, c: c,
@ -66,3 +61,9 @@ func (self *RemoteBranchesContext) GetSelectedRef() types.Ref {
} }
return remoteBranch return remoteBranch
} }
func (self *RemoteBranchesContext) GetDiffTerminals() []string {
itemId := self.GetSelectedItemId()
return []string{itemId}
}

View File

@ -11,17 +11,16 @@ type RemotesContext struct {
*ListContextTrait *ListContextTrait
} }
var _ types.IListContext = (*RemotesContext)(nil) var (
_ types.IListContext = (*RemotesContext)(nil)
_ types.DiffableContext = (*RemotesContext)(nil)
)
func NewRemotesContext( func NewRemotesContext(
getModel func() []*models.Remote, getModel func() []*models.Remote,
view *gocui.View, view *gocui.View,
getDisplayStrings func(startIdx int, length int) [][]string, getDisplayStrings func(startIdx int, length int) [][]string,
onFocus func(types.OnFocusOpts) error,
onRenderToMain func() error,
onFocusLost func(opts types.OnFocusLostOpts) error,
c *types.HelperCommon, c *types.HelperCommon,
) *RemotesContext { ) *RemotesContext {
viewModel := NewBasicViewModel(getModel) viewModel := NewBasicViewModel(getModel)
@ -35,11 +34,7 @@ func NewRemotesContext(
Key: REMOTES_CONTEXT_KEY, Key: REMOTES_CONTEXT_KEY,
Kind: types.SIDE_CONTEXT, Kind: types.SIDE_CONTEXT,
Focusable: true, Focusable: true,
}), ContextCallbackOpts{ }), ContextCallbackOpts{}),
OnFocus: onFocus,
OnFocusLost: onFocusLost,
OnRenderToMain: onRenderToMain,
}),
list: viewModel, list: viewModel,
getDisplayStrings: getDisplayStrings, getDisplayStrings: getDisplayStrings,
c: c, c: c,
@ -55,3 +50,9 @@ func (self *RemotesContext) GetSelectedItemId() string {
return item.ID() return item.ID()
} }
func (self *RemotesContext) GetDiffTerminals() []string {
itemId := self.GetSelectedItemId()
return []string{itemId}
}

View File

@ -6,29 +6,19 @@ import (
) )
type SimpleContext struct { type SimpleContext struct {
OnFocus func(opts types.OnFocusOpts) error OnRender func() error
OnFocusLost func(opts types.OnFocusLostOpts) error
OnRender func() error
// this is for pushing some content to the main view
OnRenderToMain func() error
*BaseContext *BaseContext
} }
type ContextCallbackOpts struct { type ContextCallbackOpts struct {
OnFocus func(opts types.OnFocusOpts) error OnRender func() error
OnFocusLost func(opts types.OnFocusLostOpts) error
OnRender func() error
OnRenderToMain func() error
} }
func NewSimpleContext(baseContext *BaseContext, opts ContextCallbackOpts) *SimpleContext { func NewSimpleContext(baseContext *BaseContext, opts ContextCallbackOpts) *SimpleContext {
return &SimpleContext{ return &SimpleContext{
OnFocus: opts.OnFocus, OnRender: opts.OnRender,
OnFocusLost: opts.OnFocusLost, BaseContext: baseContext,
OnRender: opts.OnRender,
OnRenderToMain: opts.OnRenderToMain,
BaseContext: baseContext,
} }
} }
@ -54,14 +44,14 @@ func (self *SimpleContext) HandleFocus(opts types.OnFocusOpts) error {
self.GetViewTrait().SetHighlight(true) self.GetViewTrait().SetHighlight(true)
} }
if self.OnFocus != nil { if self.onFocusFn != nil {
if err := self.OnFocus(opts); err != nil { if err := self.onFocusFn(opts); err != nil {
return err return err
} }
} }
if self.OnRenderToMain != nil { if self.onRenderToMainFn != nil {
if err := self.OnRenderToMain(); err != nil { if err := self.onRenderToMainFn(); err != nil {
return err return err
} }
} }
@ -70,8 +60,8 @@ func (self *SimpleContext) HandleFocus(opts types.OnFocusOpts) error {
} }
func (self *SimpleContext) HandleFocusLost(opts types.OnFocusLostOpts) error { func (self *SimpleContext) HandleFocusLost(opts types.OnFocusLostOpts) error {
if self.OnFocusLost != nil { if self.onFocusLostFn != nil {
return self.OnFocusLost(opts) return self.onFocusLostFn(opts)
} }
return nil return nil
} }
@ -84,8 +74,8 @@ func (self *SimpleContext) HandleRender() error {
} }
func (self *SimpleContext) HandleRenderToMain() error { func (self *SimpleContext) HandleRenderToMain() error {
if self.OnRenderToMain != nil { if self.onRenderToMainFn != nil {
return self.OnRenderToMain() return self.onRenderToMainFn()
} }
return nil return nil

View File

@ -11,17 +11,16 @@ type StashContext struct {
*ListContextTrait *ListContextTrait
} }
var _ types.IListContext = (*StashContext)(nil) var (
_ types.IListContext = (*StashContext)(nil)
_ types.DiffableContext = (*StashContext)(nil)
)
func NewStashContext( func NewStashContext(
getModel func() []*models.StashEntry, getModel func() []*models.StashEntry,
view *gocui.View, view *gocui.View,
getDisplayStrings func(startIdx int, length int) [][]string, getDisplayStrings func(startIdx int, length int) [][]string,
onFocus func(types.OnFocusOpts) error,
onRenderToMain func() error,
onFocusLost func(opts types.OnFocusLostOpts) error,
c *types.HelperCommon, c *types.HelperCommon,
) *StashContext { ) *StashContext {
viewModel := NewBasicViewModel(getModel) viewModel := NewBasicViewModel(getModel)
@ -35,11 +34,7 @@ func NewStashContext(
Key: STASH_CONTEXT_KEY, Key: STASH_CONTEXT_KEY,
Kind: types.SIDE_CONTEXT, Kind: types.SIDE_CONTEXT,
Focusable: true, Focusable: true,
}), ContextCallbackOpts{ }), ContextCallbackOpts{}),
OnFocus: onFocus,
OnFocusLost: onFocusLost,
OnRenderToMain: onRenderToMain,
}),
list: viewModel, list: viewModel,
getDisplayStrings: getDisplayStrings, getDisplayStrings: getDisplayStrings,
c: c, c: c,
@ -67,3 +62,9 @@ func (self *StashContext) GetSelectedRef() types.Ref {
} }
return stash return stash
} }
func (self *StashContext) GetDiffTerminals() []string {
itemId := self.GetSelectedItemId()
return []string{itemId}
}

View File

@ -15,17 +15,16 @@ type SubCommitsContext struct {
*DynamicTitleBuilder *DynamicTitleBuilder
} }
var _ types.IListContext = (*SubCommitsContext)(nil) var (
_ types.IListContext = (*SubCommitsContext)(nil)
_ types.DiffableContext = (*SubCommitsContext)(nil)
)
func NewSubCommitsContext( func NewSubCommitsContext(
getModel func() []*models.Commit, getModel func() []*models.Commit,
view *gocui.View, view *gocui.View,
getDisplayStrings func(startIdx int, length int) [][]string, getDisplayStrings func(startIdx int, length int) [][]string,
onFocus func(types.OnFocusOpts) error,
onRenderToMain func() error,
onFocusLost func(opts types.OnFocusLostOpts) error,
c *types.HelperCommon, c *types.HelperCommon,
) *SubCommitsContext { ) *SubCommitsContext {
viewModel := &SubCommitsViewModel{ viewModel := &SubCommitsViewModel{
@ -46,11 +45,7 @@ func NewSubCommitsContext(
Kind: types.SIDE_CONTEXT, Kind: types.SIDE_CONTEXT,
Focusable: true, Focusable: true,
Transient: true, Transient: true,
}), ContextCallbackOpts{ }), ContextCallbackOpts{}),
OnFocus: onFocus,
OnFocusLost: onFocusLost,
OnRenderToMain: onRenderToMain,
}),
list: viewModel, list: viewModel,
getDisplayStrings: getDisplayStrings, getDisplayStrings: getDisplayStrings,
c: c, c: c,
@ -111,3 +106,9 @@ func (self *SubCommitsContext) SetLimitCommits(value bool) {
func (self *SubCommitsContext) GetLimitCommits() bool { func (self *SubCommitsContext) GetLimitCommits() bool {
return self.limitCommits return self.limitCommits
} }
func (self *SubCommitsContext) GetDiffTerminals() []string {
itemId := self.GetSelectedItemId()
return []string{itemId}
}

View File

@ -18,10 +18,6 @@ func NewSubmodulesContext(
view *gocui.View, view *gocui.View,
getDisplayStrings func(startIdx int, length int) [][]string, getDisplayStrings func(startIdx int, length int) [][]string,
onFocus func(types.OnFocusOpts) error,
onRenderToMain func() error,
onFocusLost func(opts types.OnFocusLostOpts) error,
c *types.HelperCommon, c *types.HelperCommon,
) *SubmodulesContext { ) *SubmodulesContext {
viewModel := NewBasicViewModel(getModel) viewModel := NewBasicViewModel(getModel)
@ -35,11 +31,7 @@ func NewSubmodulesContext(
Key: SUBMODULES_CONTEXT_KEY, Key: SUBMODULES_CONTEXT_KEY,
Kind: types.SIDE_CONTEXT, Kind: types.SIDE_CONTEXT,
Focusable: true, Focusable: true,
}), ContextCallbackOpts{ }), ContextCallbackOpts{}),
OnFocus: onFocus,
OnFocusLost: onFocusLost,
OnRenderToMain: onRenderToMain,
}),
list: viewModel, list: viewModel,
getDisplayStrings: getDisplayStrings, getDisplayStrings: getDisplayStrings,
c: c, c: c,

View File

@ -17,10 +17,6 @@ func NewSuggestionsContext(
view *gocui.View, view *gocui.View,
getDisplayStrings func(startIdx int, length int) [][]string, getDisplayStrings func(startIdx int, length int) [][]string,
onFocus func(types.OnFocusOpts) error,
onRenderToMain func() error,
onFocusLost func(opts types.OnFocusLostOpts) error,
c *types.HelperCommon, c *types.HelperCommon,
) *SuggestionsContext { ) *SuggestionsContext {
viewModel := NewBasicViewModel(getModel) viewModel := NewBasicViewModel(getModel)
@ -35,11 +31,7 @@ func NewSuggestionsContext(
Kind: types.PERSISTENT_POPUP, Kind: types.PERSISTENT_POPUP,
Focusable: true, Focusable: true,
HasUncontrolledBounds: true, HasUncontrolledBounds: true,
}), ContextCallbackOpts{ }), ContextCallbackOpts{}),
OnFocus: onFocus,
OnFocusLost: onFocusLost,
OnRenderToMain: onRenderToMain,
}),
list: viewModel, list: viewModel,
getDisplayStrings: getDisplayStrings, getDisplayStrings: getDisplayStrings,
c: c, c: c,

View File

@ -11,17 +11,16 @@ type TagsContext struct {
*ListContextTrait *ListContextTrait
} }
var _ types.IListContext = (*TagsContext)(nil) var (
_ types.IListContext = (*TagsContext)(nil)
_ types.DiffableContext = (*TagsContext)(nil)
)
func NewTagsContext( func NewTagsContext(
getModel func() []*models.Tag, getModel func() []*models.Tag,
view *gocui.View, view *gocui.View,
getDisplayStrings func(startIdx int, length int) [][]string, getDisplayStrings func(startIdx int, length int) [][]string,
onFocus func(types.OnFocusOpts) error,
onRenderToMain func() error,
onFocusLost func(opts types.OnFocusLostOpts) error,
c *types.HelperCommon, c *types.HelperCommon,
) *TagsContext { ) *TagsContext {
viewModel := NewBasicViewModel(getModel) viewModel := NewBasicViewModel(getModel)
@ -35,11 +34,7 @@ func NewTagsContext(
Key: TAGS_CONTEXT_KEY, Key: TAGS_CONTEXT_KEY,
Kind: types.SIDE_CONTEXT, Kind: types.SIDE_CONTEXT,
Focusable: true, Focusable: true,
}), ContextCallbackOpts{ }), ContextCallbackOpts{}),
OnFocus: onFocus,
OnFocusLost: onFocusLost,
OnRenderToMain: onRenderToMain,
}),
list: viewModel, list: viewModel,
getDisplayStrings: getDisplayStrings, getDisplayStrings: getDisplayStrings,
c: c, c: c,
@ -63,3 +58,9 @@ func (self *TagsContext) GetSelectedRef() types.Ref {
} }
return tag return tag
} }
func (self *TagsContext) GetDiffTerminals() []string {
itemId := self.GetSelectedItemId()
return []string{itemId}
}

View File

@ -19,10 +19,6 @@ func NewWorkingTreeContext(
view *gocui.View, view *gocui.View,
getDisplayStrings func(startIdx int, length int) [][]string, getDisplayStrings func(startIdx int, length int) [][]string,
onFocus func(types.OnFocusOpts) error,
onRenderToMain func() error,
onFocusLost func(opts types.OnFocusLostOpts) error,
c *types.HelperCommon, c *types.HelperCommon,
) *WorkingTreeContext { ) *WorkingTreeContext {
viewModel := filetree.NewFileTreeViewModel(getModel, c.Log, c.UserConfig.Gui.ShowFileTree) viewModel := filetree.NewFileTreeViewModel(getModel, c.Log, c.UserConfig.Gui.ShowFileTree)
@ -36,11 +32,7 @@ func NewWorkingTreeContext(
Key: FILES_CONTEXT_KEY, Key: FILES_CONTEXT_KEY,
Kind: types.SIDE_CONTEXT, Kind: types.SIDE_CONTEXT,
Focusable: true, Focusable: true,
}), ContextCallbackOpts{ }), ContextCallbackOpts{}),
OnFocus: onFocus,
OnFocusLost: onFocusLost,
OnRenderToMain: onRenderToMain,
}),
list: viewModel, list: viewModel,
getDisplayStrings: getDisplayStrings, getDisplayStrings: getDisplayStrings,
c: c, c: c,

View File

@ -1,6 +1,7 @@
package gui package gui
import ( import (
"github.com/jesseduffield/generics/slices"
"github.com/jesseduffield/lazygit/pkg/gui/context" "github.com/jesseduffield/lazygit/pkg/gui/context"
"github.com/jesseduffield/lazygit/pkg/gui/types" "github.com/jesseduffield/lazygit/pkg/gui/types"
) )
@ -16,9 +17,7 @@ func (gui *Gui) contextTree() *context.ContextTree {
Focusable: false, Focusable: false,
HasUncontrolledBounds: true, // setting to true because the global context doesn't even have a view HasUncontrolledBounds: true, // setting to true because the global context doesn't even have a view
}), }),
context.ContextCallbackOpts{ context.ContextCallbackOpts{},
OnRenderToMain: gui.statusRenderToMain,
},
), ),
Status: context.NewSimpleContext( Status: context.NewSimpleContext(
context.NewBaseContext(context.NewBaseContextOpts{ context.NewBaseContext(context.NewBaseContextOpts{
@ -28,9 +27,7 @@ func (gui *Gui) contextTree() *context.ContextTree {
Key: context.STATUS_CONTEXT_KEY, Key: context.STATUS_CONTEXT_KEY,
Focusable: true, Focusable: true,
}), }),
context.ContextCallbackOpts{ context.ContextCallbackOpts{},
OnRenderToMain: gui.statusRenderToMain,
},
), ),
Snake: context.NewSimpleContext( Snake: context.NewSimpleContext(
context.NewBaseContext(context.NewBaseContextOpts{ context.NewBaseContext(context.NewBaseContextOpts{
@ -40,17 +37,7 @@ func (gui *Gui) contextTree() *context.ContextTree {
Key: context.SNAKE_CONTEXT_KEY, Key: context.SNAKE_CONTEXT_KEY,
Focusable: true, Focusable: true,
}), }),
context.ContextCallbackOpts{ 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
},
},
), ),
Files: gui.filesListContext(), Files: gui.filesListContext(),
Submodules: gui.submodulesListContext(), Submodules: gui.submodulesListContext(),
@ -73,11 +60,7 @@ func (gui *Gui) contextTree() *context.ContextTree {
Key: context.NORMAL_MAIN_CONTEXT_KEY, Key: context.NORMAL_MAIN_CONTEXT_KEY,
Focusable: false, Focusable: false,
}), }),
context.ContextCallbackOpts{ context.ContextCallbackOpts{},
OnFocus: func(opts types.OnFocusOpts) error {
return nil // TODO: should we do something here? We should allow for scrolling the panel
},
},
), ),
NormalSecondary: context.NewSimpleContext( NormalSecondary: context.NewSimpleContext(
context.NewBaseContext(context.NewBaseContextOpts{ context.NewBaseContext(context.NewBaseContextOpts{
@ -97,7 +80,7 @@ func (gui *Gui) contextTree() *context.ContextTree {
gui.Views.Staging.Wrap = false gui.Views.Staging.Wrap = false
gui.Views.StagingSecondary.Wrap = false gui.Views.StagingSecondary.Wrap = false
return gui.refreshStagingPanel(opts) return gui.helpers.Staging.RefreshStagingPanel(opts)
}, },
func(opts types.OnFocusLostOpts) error { func(opts types.OnFocusLostOpts) error {
gui.State.Contexts.Staging.SetState(nil) gui.State.Contexts.Staging.SetState(nil)
@ -121,7 +104,7 @@ func (gui *Gui) contextTree() *context.ContextTree {
gui.Views.Staging.Wrap = false gui.Views.Staging.Wrap = false
gui.Views.StagingSecondary.Wrap = false gui.Views.StagingSecondary.Wrap = false
return gui.refreshStagingPanel(opts) return gui.helpers.Staging.RefreshStagingPanel(opts)
}, },
func(opts types.OnFocusLostOpts) error { func(opts types.OnFocusLostOpts) error {
gui.State.Contexts.StagingSecondary.SetState(nil) 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 // no need to change wrap on the secondary view because it can't be interacted with
gui.Views.PatchBuilding.Wrap = false gui.Views.PatchBuilding.Wrap = false
return gui.refreshPatchBuildingPanel(opts) return gui.helpers.PatchBuilding.RefreshPatchBuildingPanel(opts)
}, },
func(opts types.OnFocusLostOpts) error { func(opts types.OnFocusLostOpts) error {
gui.Views.PatchBuilding.Wrap = true gui.Views.PatchBuilding.Wrap = true
@ -180,20 +163,7 @@ func (gui *Gui) contextTree() *context.ContextTree {
), ),
MergeConflicts: context.NewMergeConflictsContext( MergeConflicts: context.NewMergeConflictsContext(
gui.Views.MergeConflicts, gui.Views.MergeConflicts,
context.ContextCallbackOpts{ 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
},
},
gui.c, gui.c,
func() map[string]string { func() map[string]string {
// wrapping in a function because contexts are initialized before helpers // wrapping in a function because contexts are initialized before helpers
@ -211,10 +181,6 @@ func (gui *Gui) contextTree() *context.ContextTree {
}), }),
context.ContextCallbackOpts{ context.ContextCallbackOpts{
OnFocus: OnFocusWrapper(gui.handleAskFocused), OnFocus: OnFocusWrapper(gui.handleAskFocused),
OnFocusLost: func(types.OnFocusLostOpts) error {
gui.deactivateConfirmationPrompt()
return nil
},
}, },
), ),
CommitMessage: context.NewSimpleContext( CommitMessage: context.NewSimpleContext(
@ -278,3 +244,27 @@ func (gui *Gui) getPatchExplorerContexts() []types.IPatchExplorerContext {
gui.State.Contexts.CustomPatchBuilder, 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()
})
}

View File

@ -10,7 +10,6 @@ import (
"github.com/jesseduffield/lazygit/pkg/gui/modes/cherrypicking" "github.com/jesseduffield/lazygit/pkg/gui/modes/cherrypicking"
"github.com/jesseduffield/lazygit/pkg/gui/services/custom_commands" "github.com/jesseduffield/lazygit/pkg/gui/services/custom_commands"
"github.com/jesseduffield/lazygit/pkg/gui/types" "github.com/jesseduffield/lazygit/pkg/gui/types"
"github.com/jesseduffield/lazygit/pkg/snake"
) )
func (gui *Gui) resetControllers() { func (gui *Gui) resetControllers() {
@ -31,10 +30,17 @@ func (gui *Gui) resetControllers() {
return gui.State.savedCommitMessage return gui.State.savedCommitMessage
} }
gpgHelper := helpers.NewGpgHelper(helperCommon, gui.os, gui.git) 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{ gui.helpers = &helpers.Helpers{
Refs: refsHelper, Refs: refsHelper,
Host: helpers.NewHostHelper(helperCommon, gui.git), 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), Bisect: helpers.NewBisectHelper(helperCommon, gui.git),
Suggestions: suggestionsHelper, Suggestions: suggestionsHelper,
Files: helpers.NewFilesHelper(helperCommon, gui.git, osCommand), Files: helpers.NewFilesHelper(helperCommon, gui.git, osCommand),
@ -42,7 +48,7 @@ func (gui *Gui) resetControllers() {
Tags: helpers.NewTagsHelper(helperCommon, gui.git), Tags: helpers.NewTagsHelper(helperCommon, gui.git),
GPG: gpgHelper, GPG: gpgHelper,
MergeAndRebase: rebaseHelper, MergeAndRebase: rebaseHelper,
MergeConflicts: helpers.NewMergeConflictsHelper(helperCommon, gui.State.Contexts, gui.git), MergeConflicts: mergeConflictsHelper,
CherryPick: helpers.NewCherryPickHelper( CherryPick: helpers.NewCherryPickHelper(
helperCommon, helperCommon,
gui.git, gui.git,
@ -50,8 +56,16 @@ func (gui *Gui) resetControllers() {
func() *cherrypicking.CherryPicking { return gui.State.Modes.CherryPicking }, func() *cherrypicking.CherryPicking { return gui.State.Modes.CherryPicking },
rebaseHelper, rebaseHelper,
), ),
Upstream: helpers.NewUpstreamHelper(helperCommon, model, suggestionsHelper.GetRemoteBranchesSuggestionsFunc), Upstream: helpers.NewUpstreamHelper(helperCommon, model, suggestionsHelper.GetRemoteBranchesSuggestionsFunc),
AmendHelper: helpers.NewAmendHelper(helperCommon, gui.git, gpgHelper), 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( gui.CustomCommandsClient = custom_commands.NewClient(
@ -77,10 +91,7 @@ func (gui *Gui) resetControllers() {
common, common,
) )
submodulesController := controllers.NewSubmodulesController( submodulesController := controllers.NewSubmodulesController(common)
common,
gui.enterSubmodule,
)
bisectController := controllers.NewBisectController(common) bisectController := controllers.NewBisectController(common)
@ -114,7 +125,6 @@ func (gui *Gui) resetControllers() {
tagsController := controllers.NewTagsController(common) tagsController := controllers.NewTagsController(common)
filesController := controllers.NewFilesController( filesController := controllers.NewFilesController(
common, common,
gui.enterSubmodule,
setCommitMessage, setCommitMessage,
getSavedCommitMessage, getSavedCommitMessage,
) )
@ -137,7 +147,10 @@ func (gui *Gui) resetControllers() {
stagingController := controllers.NewStagingController(common, gui.State.Contexts.Staging, gui.State.Contexts.StagingSecondary, false) 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) stagingSecondaryController := controllers.NewStagingController(common, gui.State.Contexts.StagingSecondary, gui.State.Contexts.Staging, true)
patchBuildingController := controllers.NewPatchBuildingController(common) 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) { setSubCommits := func(commits []*models.Commit) {
gui.Mutexes.SubCommitsMutex.Lock() gui.Mutexes.SubCommitsMutex.Lock()
@ -163,7 +176,7 @@ func (gui *Gui) resetControllers() {
gui.State.Contexts.Stash, gui.State.Contexts.Stash,
} { } {
controllers.AttachControllers(context, controllers.NewSwitchToDiffFilesController( 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(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) // 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, controllers.AttachControllers(gui.State.Contexts.Staging,
stagingController, stagingController,
@ -254,6 +275,10 @@ func (gui *Gui) resetControllers() {
remoteBranchesController, remoteBranchesController,
) )
controllers.AttachControllers(gui.State.Contexts.Status,
statusController,
)
controllers.AttachControllers(gui.State.Contexts.Global, controllers.AttachControllers(gui.State.Contexts.Global,
syncController, syncController,
undoController, undoController,
@ -271,3 +296,14 @@ func (gui *Gui) resetControllers() {
controllers.AttachControllers(context, listControllerFactory.Create(context)) 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()
}
}

View File

@ -7,5 +7,8 @@ func AttachControllers(context types.Context, controllers ...types.IController)
context.AddKeybindingsFn(controller.GetKeybindings) context.AddKeybindingsFn(controller.GetKeybindings)
context.AddMouseKeybindingsFn(controller.GetMouseKeybindings) context.AddMouseKeybindingsFn(controller.GetMouseKeybindings)
context.AddOnClickFn(controller.GetOnClick()) context.AddOnClickFn(controller.GetOnClick())
context.AddOnRenderToMainFn(controller.GetOnRenderToMain())
context.AddOnFocusFn(controller.GetOnFocus())
context.AddOnFocusLostFn(controller.GetOnFocusLost())
} }
} }

View File

@ -18,3 +18,15 @@ func (self *baseController) GetMouseKeybindings(opts types.KeybindingsOpts) []*g
func (self *baseController) GetOnClick() func() error { func (self *baseController) GetOnClick() func() error {
return nil 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
}

View File

@ -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 { func (self *BranchesController) setUpstream(selectedBranch *models.Branch) error {
return self.c.Menu(types.CreateMenuOptions{ return self.c.Menu(types.CreateMenuOptions{
Title: self.c.Tr.Actions.SetUnsetUpstream, Title: self.c.Tr.Actions.SetUnsetUpstream,

View File

@ -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()
}

View File

@ -15,7 +15,6 @@ type FilesController struct {
baseController // nolint: unused baseController // nolint: unused
*controllerCommon *controllerCommon
enterSubmodule func(submodule *models.SubmoduleConfig) error
setCommitMessage func(message string) setCommitMessage func(message string)
getSavedCommitMessage func() string getSavedCommitMessage func() string
} }
@ -24,13 +23,11 @@ var _ types.IController = &FilesController{}
func NewFilesController( func NewFilesController(
common *controllerCommon, common *controllerCommon,
enterSubmodule func(submodule *models.SubmoduleConfig) error,
setCommitMessage func(message string), setCommitMessage func(message string),
getSavedCommitMessage func() string, getSavedCommitMessage func() string,
) *FilesController { ) *FilesController {
return &FilesController{ return &FilesController{
controllerCommon: common, controllerCommon: common,
enterSubmodule: enterSubmodule,
setCommitMessage: setCommitMessage, setCommitMessage: setCommitMessage,
getSavedCommitMessage: getSavedCommitMessage, 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 { func (self *FilesController) GetOnClick() func() error {
return self.checkSelectedFileNode(self.press) return self.checkSelectedFileNode(self.press)
} }
@ -379,7 +444,7 @@ func (self *FilesController) EnterFile(opts types.OnFocusOpts) error {
submoduleConfigs := self.model.Submodules submoduleConfigs := self.model.Submodules
if file.IsSubmodule(submoduleConfigs) { if file.IsSubmodule(submoduleConfigs) {
submoduleConfig := file.SubmoduleConfig(submoduleConfigs) submoduleConfig := file.SubmoduleConfig(submoduleConfigs)
return self.enterSubmodule(submoduleConfig) return self.helpers.Repos.EnterSubmodule(submoduleConfig)
} }
if file.HasInlineMergeConflicts { if file.HasInlineMergeConflicts {

View File

@ -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()
}

View File

@ -12,26 +12,45 @@ type Helpers struct {
CherryPick *CherryPickHelper CherryPick *CherryPickHelper
Host *HostHelper Host *HostHelper
PatchBuilding *PatchBuildingHelper PatchBuilding *PatchBuildingHelper
Staging *StagingHelper
GPG *GpgHelper GPG *GpgHelper
Upstream *UpstreamHelper Upstream *UpstreamHelper
AmendHelper *AmendHelper 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 { func NewStubHelpers() *Helpers {
return &Helpers{ return &Helpers{
Refs: &RefsHelper{}, Refs: &RefsHelper{},
Bisect: &BisectHelper{}, Bisect: &BisectHelper{},
Suggestions: &SuggestionsHelper{}, Suggestions: &SuggestionsHelper{},
Files: &FilesHelper{}, Files: &FilesHelper{},
WorkingTree: &WorkingTreeHelper{}, WorkingTree: &WorkingTreeHelper{},
Tags: &TagsHelper{}, Tags: &TagsHelper{},
MergeAndRebase: &MergeAndRebaseHelper{}, MergeAndRebase: &MergeAndRebaseHelper{},
MergeConflicts: &MergeConflictsHelper{}, MergeConflicts: &MergeConflictsHelper{},
CherryPick: &CherryPickHelper{}, CherryPick: &CherryPickHelper{},
Host: &HostHelper{}, Host: &HostHelper{},
PatchBuilding: &PatchBuildingHelper{}, PatchBuilding: &PatchBuildingHelper{},
GPG: &GpgHelper{}, Staging: &StagingHelper{},
Upstream: &UpstreamHelper{}, GPG: &GpgHelper{},
AmendHelper: &AmendHelper{}, Upstream: &UpstreamHelper{},
AmendHelper: &AmendHelper{},
Snake: &SnakeHelper{},
Diff: &DiffHelper{},
Repos: &ReposHelper{},
RecordDirectory: &RecordDirectoryHelper{},
Update: &UpdateHelper{},
Window: &WindowHelper{},
View: &ViewHelper{},
Refresh: &RefreshHelper{},
} }
} }

View File

@ -113,3 +113,42 @@ func (self *MergeConflictsHelper) SwitchToMerge(path string) error {
func (self *MergeConflictsHelper) context() *context.MergeConflictsContext { func (self *MergeConflictsHelper) context() *context.MergeConflictsContext {
return self.contexts.MergeConflicts 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
}

View File

@ -4,6 +4,7 @@ import (
"github.com/jesseduffield/lazygit/pkg/commands" "github.com/jesseduffield/lazygit/pkg/commands"
"github.com/jesseduffield/lazygit/pkg/commands/types/enums" "github.com/jesseduffield/lazygit/pkg/commands/types/enums"
"github.com/jesseduffield/lazygit/pkg/gui/context" "github.com/jesseduffield/lazygit/pkg/gui/context"
"github.com/jesseduffield/lazygit/pkg/gui/patch_exploring"
"github.com/jesseduffield/lazygit/pkg/gui/types" "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. // refreshing the current context so that the secondary panel is hidden if necessary.
return self.c.PostRefreshUpdate(self.c.CurrentContext()) 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,
},
})
}

View File

@ -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)
}

View File

@ -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()
}

View File

@ -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)
}

View File

@ -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
}

View File

@ -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()
}

View File

@ -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
},
})
}

View File

@ -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
}

View File

@ -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()
}

View File

@ -12,6 +12,9 @@ import (
"github.com/samber/lo" "github.com/samber/lo"
) )
// after selecting the 200th commit, we'll load in all the rest
const COMMIT_THRESHOLD = 200
type ( type (
PullFilesFn func() error PullFilesFn func() error
) )
@ -150,6 +153,50 @@ func (self *LocalCommitsController) GetKeybindings(opts types.KeybindingsOpts) [
return bindings 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 { func (self *LocalCommitsController) squashDown(commit *models.Commit) error {
if self.context().GetSelectedLineIdx() >= len(self.model.Commits)-1 { if self.context().GetSelectedLineIdx() >= len(self.model.Commits)-1 {
return self.c.ErrorMsg(self.c.Tr.CannotSquashOrFixupFirstCommit) 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 { func (self *LocalCommitsController) Context() types.Context {
return self.context() return self.context()
} }

View File

@ -44,6 +44,14 @@ func (self *MenuController) GetOnClick() func() error {
return self.press 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 { func (self *MenuController) press() error {
return self.context().OnMenuPress(self.context().GetSelected()) return self.context().OnMenuPress(self.context().GetSelected())
} }

View File

@ -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 { func (self *MergeConflictsController) HandleScrollUp() error {
self.context().SetUserScrolling(true) self.context().SetUserScrolling(true)
self.context().GetViewTrait().ScrollUp(self.c.UserConfig.Gui.ScrollHeight) self.context().GetViewTrait().ScrollUp(self.c.UserConfig.Gui.ScrollHeight)

View File

@ -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,
},
})
})
}
}

View File

@ -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 { func (self *RemoteBranchesController) Context() types.Context {
return self.context() return self.context()
} }

View File

@ -1,8 +1,12 @@
package controllers package controllers
import ( import (
"fmt"
"strings"
"github.com/jesseduffield/lazygit/pkg/commands/models" "github.com/jesseduffield/lazygit/pkg/commands/models"
"github.com/jesseduffield/lazygit/pkg/gui/context" "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/gui/types"
"github.com/jesseduffield/lazygit/pkg/utils" "github.com/jesseduffield/lazygit/pkg/utils"
) )
@ -60,6 +64,28 @@ func (self *RemotesController) GetKeybindings(opts types.KeybindingsOpts) []*typ
return bindings 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 { func (self *RemotesController) GetOnClick() func() error {
return self.checkSelected(self.enter) return self.checkSelected(self.enter)
} }

View File

@ -8,20 +8,16 @@ import (
type SnakeController struct { type SnakeController struct {
baseController baseController
*controllerCommon *controllerCommon
getGame func() *snake.Game
} }
var _ types.IController = &SnakeController{} var _ types.IController = &SnakeController{}
func NewSnakeController( func NewSnakeController(
common *controllerCommon, common *controllerCommon,
getGame func() *snake.Game,
) *SnakeController { ) *SnakeController {
return &SnakeController{ return &SnakeController{
baseController: baseController{}, baseController: baseController{},
controllerCommon: common, controllerCommon: common,
getGame: getGame,
} }
} }
@ -56,9 +52,24 @@ func (self *SnakeController) Context() types.Context {
return self.contexts.Snake 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 { func (self *SnakeController) SetDirection(direction snake.Direction) func() error {
return func() error { return func() error {
self.getGame().SetDirection(direction) self.helpers.Snake.SetDirection(direction)
return nil return nil
} }
} }

View File

@ -55,6 +55,28 @@ func (self *StashController) GetKeybindings(opts types.KeybindingsOpts) []*types
return bindings 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 { func (self *StashController) checkSelected(callback func(*models.StashEntry) error) func() error {
return func() error { return func() error {
item := self.context().GetSelected() item := self.context().GetSelected()

View File

@ -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()
}

View File

@ -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
}
}

View File

@ -14,20 +14,16 @@ import (
type SubmodulesController struct { type SubmodulesController struct {
baseController baseController
*controllerCommon *controllerCommon
enterSubmodule func(submodule *models.SubmoduleConfig) error
} }
var _ types.IController = &SubmodulesController{} var _ types.IController = &SubmodulesController{}
func NewSubmodulesController( func NewSubmodulesController(
controllerCommon *controllerCommon, controllerCommon *controllerCommon,
enterSubmodule func(submodule *models.SubmoduleConfig) error,
) *SubmodulesController { ) *SubmodulesController {
return &SubmodulesController{ return &SubmodulesController{
baseController: baseController{}, baseController: baseController{},
controllerCommon: controllerCommon, controllerCommon: controllerCommon,
enterSubmodule: enterSubmodule,
} }
} }
@ -81,8 +77,43 @@ func (self *SubmodulesController) GetOnClick() func() error {
return self.checkSelected(self.enter) 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 { func (self *SubmodulesController) enter(submodule *models.SubmoduleConfig) error {
return self.enterSubmodule(submodule) return self.helpers.Repos.EnterSubmodule(submodule)
} }
func (self *SubmodulesController) add() error { func (self *SubmodulesController) add() error {

View File

@ -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
}

View File

@ -1,6 +1,7 @@
package controllers package controllers
import ( import (
"github.com/jesseduffield/lazygit/pkg/gui/context"
"github.com/jesseduffield/lazygit/pkg/gui/types" "github.com/jesseduffield/lazygit/pkg/gui/types"
) )
@ -17,20 +18,20 @@ type CanSwitchToDiffFiles interface {
type SwitchToDiffFilesController struct { type SwitchToDiffFilesController struct {
baseController baseController
*controllerCommon *controllerCommon
context CanSwitchToDiffFiles context CanSwitchToDiffFiles
viewFiles func(SwitchToCommitFilesContextOpts) error diffFilesContext *context.CommitFilesContext
} }
func NewSwitchToDiffFilesController( func NewSwitchToDiffFilesController(
controllerCommon *controllerCommon, controllerCommon *controllerCommon,
viewFiles func(SwitchToCommitFilesContextOpts) error,
context CanSwitchToDiffFiles, context CanSwitchToDiffFiles,
diffFilesContext *context.CommitFilesContext,
) *SwitchToDiffFilesController { ) *SwitchToDiffFilesController {
return &SwitchToDiffFilesController{ return &SwitchToDiffFilesController{
baseController: baseController{}, baseController: baseController{},
controllerCommon: controllerCommon, controllerCommon: controllerCommon,
context: context, context: context,
viewFiles: viewFiles, diffFilesContext: diffFilesContext,
} }
} }
@ -72,3 +73,22 @@ func (self *SwitchToDiffFilesController) enter(ref types.Ref) error {
func (self *SwitchToDiffFilesController) Context() types.Context { func (self *SwitchToDiffFilesController) Context() types.Context {
return self.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)
}

View File

@ -56,6 +56,29 @@ func (self *TagsController) GetKeybindings(opts types.KeybindingsOpts) []*types.
return bindings 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 { func (self *TagsController) checkout(tag *models.Tag) error {
self.c.LogAction(self.c.Tr.Actions.CheckoutTag) self.c.LogAction(self.c.Tr.Actions.CheckoutTag)
if err := self.helpers.Refs.CheckoutRef(tag.Name, types.CheckoutRefOptions{}); err != nil { if err := self.helpers.Refs.CheckoutRef(tag.Name, types.CheckoutRefOptions{}); err != nil {

View File

@ -49,8 +49,8 @@ func (gui *Gui) handleCreatePatchOptionsMenu() error {
}, },
}...) }...)
if gui.currentContext().GetKey() == gui.State.Contexts.LocalCommits.GetKey() { if gui.c.CurrentContext().GetKey() == gui.State.Contexts.LocalCommits.GetKey() {
selectedCommit := gui.getSelectedLocalCommit() selectedCommit := gui.State.Contexts.LocalCommits.GetSelected()
if selectedCommit != nil && gui.git.Patch.PatchBuilder.To != selectedCommit.Sha { if selectedCommit != nil && gui.git.Patch.PatchBuilder.To != selectedCommit.Sha {
// adding this option to index 1 // adding this option to index 1
menuItems = append( menuItems = append(
@ -97,7 +97,7 @@ func (gui *Gui) validateNormalWorkingTreeState() (bool, error) {
} }
func (gui *Gui) returnFocusFromPatchExplorerIfNecessary() 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 gui.helpers.PatchBuilding.Escape()
} }
return nil return nil

View File

@ -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)
// }

View File

@ -4,113 +4,12 @@ import (
"fmt" "fmt"
"strings" "strings"
"github.com/jesseduffield/lazygit/pkg/gui/context"
"github.com/jesseduffield/lazygit/pkg/gui/modes/diffing" "github.com/jesseduffield/lazygit/pkg/gui/modes/diffing"
"github.com/jesseduffield/lazygit/pkg/gui/types" "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 { func (gui *Gui) handleCreateDiffingMenuPanel() error {
names := gui.currentDiffTerminals() names := gui.helpers.Diff.CurrentDiffTerminals()
menuItems := []*types.MenuItem{} menuItems := []*types.MenuItem{}
for _, name := range names { for _, name := range names {

View File

@ -15,7 +15,7 @@ func (gui *Gui) handleCreateExtrasMenuPanel() error {
{ {
Label: gui.c.Tr.ToggleShowCommandLog, Label: gui.c.Tr.ToggleShowCommandLog,
OnPress: func() error { OnPress: func() error {
currentContext := gui.currentStaticContext() currentContext := gui.c.CurrentStaticContext()
if gui.ShowExtrasWindow && currentContext.GetKey() == context.COMMAND_LOG_CONTEXT_KEY { if gui.ShowExtrasWindow && currentContext.GetKey() == context.COMMAND_LOG_CONTEXT_KEY {
if err := gui.c.PopContext(); err != nil { if err := gui.c.PopContext(); err != nil {
return err return err
@ -39,7 +39,7 @@ func (gui *Gui) handleCreateExtrasMenuPanel() error {
func (gui *Gui) handleFocusCommandLog() error { func (gui *Gui) handleFocusCommandLog() error {
gui.ShowExtrasWindow = true gui.ShowExtrasWindow = true
// TODO: is this necessary? Can't I just call 'return from context'? // 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) return gui.c.PushContext(gui.State.Contexts.CommandLog)
} }

View File

@ -17,6 +17,8 @@ import (
// file watching is only really an added bonus for faster refreshing. // file watching is only really an added bonus for faster refreshing.
const MAX_WATCHED_FILES = 50 const MAX_WATCHED_FILES = 50
var _ types.IFileWatcher = new(fileWatcher)
type fileWatcher struct { type fileWatcher struct {
Watcher *fsnotify.Watcher Watcher *fsnotify.Watcher
WatchedFilenames []string WatchedFilenames []string
@ -60,7 +62,7 @@ func (w *fileWatcher) watchFilename(filename string) {
w.WatchedFilenames = append(w.WatchedFilenames, filename) 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 { if w.Disabled {
return nil 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 // 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) // TODO: consider watching the whole directory recursively (could be more expensive)
func (gui *Gui) watchFilesForChanges() { func (gui *Gui) WatchFilesForChanges() {
gui.fileWatcher = NewFileWatcher(gui.Log) gui.fileWatcher = NewFileWatcher(gui.Log)
if gui.fileWatcher.Disabled { if gui.fileWatcher.Disabled {
return return
@ -117,7 +119,7 @@ func (gui *Gui) watchFilesForChanges() {
continue continue
} }
// only refresh if we're not already // 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}}) _ = gui.c.Refresh(types.RefreshOptions{Mode: types.ASYNC, Scope: []types.RefreshableView{types.FILES}})
} }

View File

@ -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()
}
}

View File

@ -9,9 +9,9 @@ import (
func (gui *Gui) handleCreateFilteringMenuPanel() error { func (gui *Gui) handleCreateFilteringMenuPanel() error {
fileName := "" fileName := ""
switch gui.currentSideListContext() { switch gui.c.CurrentSideContext() {
case gui.State.Contexts.Files: case gui.State.Contexts.Files:
node := gui.getSelectedFileNode() node := gui.State.Contexts.Files.GetSelected()
if node != nil { if node != nil {
fileName = node.GetPath() fileName = node.GetPath()
} }

View File

@ -5,7 +5,6 @@ import (
"strings" "strings"
"github.com/jesseduffield/gocui" "github.com/jesseduffield/gocui"
"github.com/jesseduffield/lazygit/pkg/commands/git_commands"
"github.com/jesseduffield/lazygit/pkg/gui/types" "github.com/jesseduffield/lazygit/pkg/gui/types"
"github.com/jesseduffield/lazygit/pkg/utils" "github.com/jesseduffield/lazygit/pkg/utils"
) )
@ -113,13 +112,13 @@ func (gui *Gui) scrollDownMain() error {
} }
func (gui *Gui) mainView() *gocui.View { func (gui *Gui) mainView() *gocui.View {
viewName := gui.getViewNameForWindow("main") viewName := gui.helpers.Window.GetViewNameForWindow("main")
view, _ := gui.g.View(viewName) view, _ := gui.g.View(viewName)
return view return view
} }
func (gui *Gui) secondaryView() *gocui.View { func (gui *Gui) secondaryView() *gocui.View {
viewName := gui.getViewNameForWindow("secondary") viewName := gui.helpers.Window.GetViewNameForWindow("secondary")
view, _ := gui.g.View(viewName) view, _ := gui.g.View(viewName)
return view return view
} }
@ -162,17 +161,19 @@ func (gui *Gui) handleRefresh() error {
return gui.c.Refresh(types.RefreshOptions{Mode: types.ASYNC}) 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 { func (gui *Gui) handleCopySelectedSideContextItemToClipboard() error {
// important to note that this assumes we've selected an item in a side context // 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 == "" { if itemId == "" {
return nil return nil
@ -189,3 +190,13 @@ func (gui *Gui) handleCopySelectedSideContextItemToClipboard() error {
return nil 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()
}

View File

@ -8,6 +8,7 @@ import (
"sync" "sync"
"github.com/jesseduffield/gocui" "github.com/jesseduffield/gocui"
"github.com/jesseduffield/lazycore/pkg/boxlayout"
appTypes "github.com/jesseduffield/lazygit/pkg/app/types" appTypes "github.com/jesseduffield/lazygit/pkg/app/types"
"github.com/jesseduffield/lazygit/pkg/commands" "github.com/jesseduffield/lazygit/pkg/commands"
"github.com/jesseduffield/lazygit/pkg/commands/git_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/gui/types"
"github.com/jesseduffield/lazygit/pkg/integration/components" "github.com/jesseduffield/lazygit/pkg/integration/components"
integrationTypes "github.com/jesseduffield/lazygit/pkg/integration/types" 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/tasks"
"github.com/jesseduffield/lazygit/pkg/theme" "github.com/jesseduffield/lazygit/pkg/theme"
"github.com/jesseduffield/lazygit/pkg/updates" "github.com/jesseduffield/lazygit/pkg/updates"
@ -58,18 +58,6 @@ const StartupPopupVersion = 5
// OverlappingEdges determines if panel edges overlap // OverlappingEdges determines if panel edges overlap
var OverlappingEdges = false 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 type Repo string
// Gui wraps the gocui Gui object which handles rendering and events // 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 // this tells us whether our views have been initially set up
ViewsSetup bool ViewsSetup bool
Views Views Views types.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
// Log of the commands that get run, to be displayed to the user. // Log of the commands that get run, to be displayed to the user.
CmdLog []string CmdLog []string
@ -139,6 +122,8 @@ type Gui struct {
// flag as to whether or not the diff view should ignore whitespace // flag as to whether or not the diff view should ignore whitespace
IgnoreWhitespaceInDiffView bool IgnoreWhitespaceInDiffView bool
IsRefreshingFiles bool
// we use this to decide whether we'll return to the original directory that // 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. // lazygit was opened in, or if we'll retain the one we're currently in.
RetainOriginalDir bool RetainOriginalDir bool
@ -153,10 +138,52 @@ type Gui struct {
// process // process
InitialDir string InitialDir string
BackgroundRoutineMgr *BackgroundRoutineMgr
// for accessing the gui's state from outside this package
stateAccessor *StateAccessor
Updating bool
c *types.HelperCommon c *types.HelperCommon
helpers *helpers.Helpers 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 // 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 will sometimes appear when typing into a prompt
Suggestions []*types.Suggestion Suggestions []*types.Suggestion
Updating bool
SplitMainPanel bool SplitMainPanel bool
LimitCommits bool LimitCommits bool
IsRefreshingFiles bool Searching searchingState
Searching searchingState StartupStage types.StartupStage // Allows us to not load everything at once
StartupStage StartupStage // Allows us to not load everything at once
ContextManager ContextManager ContextMgr ContextMgr
Contexts *context.ContextTree Contexts *context.ContextTree
// WindowViewNameMap is a mapping of windows to the current view of that window. // 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 // Some views move between windows for example the commitFiles view and when cycling through
@ -204,20 +229,38 @@ type GuiRepoState struct {
CurrentPopupOpts *types.CreatePopupPanelOpts 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 { type searchingState struct {
view *gocui.View view *gocui.View
isSearching bool isSearching bool
searchString string 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 { func (gui *Gui) onNewRepo(startArgs appTypes.StartArgs, reuseState bool) error {
var err error var err error
gui.git, err = commands.NewGitCommand( gui.git, err = commands.NewGitCommand(
@ -278,8 +321,6 @@ func (gui *Gui) resetState(startArgs appTypes.StartArgs, reuseState bool) {
initialContext := initialContext(contextTree, startArgs) initialContext := initialContext(contextTree, startArgs)
initialScreenMode := initialScreenMode(startArgs, gui.Config) initialScreenMode := initialScreenMode(startArgs, gui.Config)
initialWindowViewNameMap := gui.initialWindowViewNameMap(contextTree)
gui.State = &GuiRepoState{ gui.State = &GuiRepoState{
Model: &types.Model{ Model: &types.Model{
CommitFiles: nil, CommitFiles: nil,
@ -298,9 +339,9 @@ func (gui *Gui) resetState(startArgs appTypes.StartArgs, reuseState bool) {
}, },
ScreenMode: initialScreenMode, ScreenMode: initialScreenMode,
// TODO: put contexts in the context manager // TODO: put contexts in the context manager
ContextManager: NewContextManager(), ContextMgr: NewContextMgr(initialContext, gui),
Contexts: contextTree, Contexts: contextTree,
WindowViewNameMap: initialWindowViewNameMap, WindowViewNameMap: initialWindowViewNameMap(contextTree),
} }
if err := gui.c.PushContext(initialContext); err != nil { 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 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 { func initialScreenMode(startArgs appTypes.StartArgs, config config.AppConfigurer) WindowMaximisation {
if startArgs.FilterPath != "" || startArgs.GitArg != appTypes.GitArgNone { if startArgs.FilterPath != "" || startArgs.GitArg != appTypes.GitArgNone {
return SCREEN_HALF return SCREEN_HALF
@ -391,7 +442,7 @@ func NewGui(
InitialDir: initialDir, InitialDir: initialDir,
} }
gui.watchFilesForChanges() gui.WatchFilesForChanges()
gui.PopupHandler = popup.NewPopupHandler( gui.PopupHandler = popup.NewPopupHandler(
cmn, cmn,
@ -429,6 +480,9 @@ func NewGui(
icons.SetIconEnabled(gui.UserConfig.Gui.ShowIcons) icons.SetIconEnabled(gui.UserConfig.Gui.ShowIcons)
presentation.SetCustomBranches(gui.UserConfig.Gui.BranchColors) presentation.SetCustomBranches(gui.UserConfig.Gui.BranchColors)
gui.BackgroundRoutineMgr = &BackgroundRoutineMgr{gui: gui}
gui.stateAccessor = &StateAccessor{gui: gui}
return gui, nil return gui, nil
} }
@ -539,7 +593,7 @@ func (gui *Gui) Run(startArgs appTypes.StartArgs) error {
gui.waitForIntro.Add(1) gui.waitForIntro.Add(1)
gui.startBackgroundRoutines() gui.BackgroundRoutineMgr.startBackgroundRoutines()
gui.c.Log.Info("starting main loop") gui.c.Log.Info("starting main loop")
@ -565,11 +619,11 @@ func (gui *Gui) RunAndHandleError(startArgs appTypes.StartArgs) error {
switch err { switch err {
case gocui.ErrQuit: case gocui.ErrQuit:
if gui.RetainOriginalDir { if gui.RetainOriginalDir {
if err := gui.recordDirectory(gui.InitialDir); err != nil { if err := gui.helpers.RecordDirectory.RecordDirectory(gui.InitialDir); err != nil {
return err return err
} }
} else { } else {
if err := gui.recordCurrentDirectory(); err != nil { if err := gui.helpers.RecordDirectory.RecordCurrentDirectory(); err != nil {
return err return err
} }
} }
@ -639,7 +693,8 @@ func (gui *Gui) runSubprocessWithSuspense(subprocess oscommands.ICmdObj) (bool,
return false, gui.c.Error(err) return false, gui.c.Error(err)
} }
gui.PauseBackgroundThreads = true gui.BackgroundRoutineMgr.PauseBackgroundThreads(true)
defer gui.BackgroundRoutineMgr.PauseBackgroundThreads(false)
cmdErr := gui.runSubprocess(subprocess) cmdErr := gui.runSubprocess(subprocess)
@ -647,8 +702,6 @@ func (gui *Gui) runSubprocessWithSuspense(subprocess oscommands.ICmdObj) (bool,
return false, err return false, err
} }
gui.PauseBackgroundThreads = false
if cmdErr != nil { if cmdErr != nil {
return false, gui.c.Error(cmdErr) return false, gui.c.Error(cmdErr)
} }
@ -751,3 +804,37 @@ func (gui *Gui) onUIThread(f func() error) {
return f() 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()
}

View File

@ -1,8 +1,8 @@
package gui package gui
import ( import (
"errors" "github.com/jesseduffield/gocui"
"github.com/jesseduffield/lazygit/pkg/commands"
"github.com/jesseduffield/lazygit/pkg/commands/oscommands" "github.com/jesseduffield/lazygit/pkg/commands/oscommands"
"github.com/jesseduffield/lazygit/pkg/config" "github.com/jesseduffield/lazygit/pkg/config"
"github.com/jesseduffield/lazygit/pkg/gui/types" "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 { func (self *guiCommon) PushContext(context types.Context, opts ...types.OnFocusOpts) error {
singleOpts := types.OnFocusOpts{} return self.gui.pushContext(context, opts...)
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)
} }
func (self *guiCommon) PopContext() error { func (self *guiCommon) PopContext() error {
return self.gui.popContext() return self.gui.popContext()
} }
func (self *guiCommon) ReplaceContext(context types.Context) error {
return self.gui.replaceContext(context)
}
func (self *guiCommon) CurrentContext() types.Context { func (self *guiCommon) CurrentContext() types.Context {
return self.gui.currentContext() return self.gui.currentContext()
} }
@ -66,6 +60,10 @@ func (self *guiCommon) CurrentStaticContext() types.Context {
return self.gui.currentStaticContext() return self.gui.currentStaticContext()
} }
func (self *guiCommon) CurrentSideContext() types.Context {
return self.gui.currentSideContext()
}
func (self *guiCommon) IsCurrentContext(c types.Context) bool { func (self *guiCommon) IsCurrentContext(c types.Context) bool {
return self.CurrentContext().GetKey() == c.GetKey() return self.CurrentContext().GetKey() == c.GetKey()
} }
@ -78,14 +76,54 @@ func (self *guiCommon) SaveAppState() error {
return self.gui.Config.SaveAppState() 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() { func (self *guiCommon) Render() {
self.gui.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() { func (self *guiCommon) OpenSearch() {
_ = self.gui.handleOpenSearch(self.gui.currentViewName()) _ = self.gui.handleOpenSearch(self.gui.currentViewName())
} }
func (self *guiCommon) GocuiGui() *gocui.Gui {
return self.gui.g
}
func (self *guiCommon) OnUIThread(f func() error) { func (self *guiCommon) OnUIThread(f func() error) {
self.gui.onUIThread(f) self.gui.onUIThread(f)
} }
@ -102,3 +140,7 @@ func (self *guiCommon) MainViewPairs() types.MainViewPairs {
MergeConflicts: self.gui.mergingMainContextPair(), MergeConflicts: self.gui.mergingMainContextPair(),
} }
} }
func (self *guiCommon) State() types.IStateAccessor {
return self.gui.stateAccessor
}

View File

@ -77,7 +77,7 @@ func (self *Gui) GetInitialKeybindings() ([]*types.Binding, []*gocui.ViewMouseBi
{ {
ViewName: "", ViewName: "",
Key: opts.GetKey(opts.Config.Universal.OpenRecentRepos), Key: opts.GetKey(opts.Config.Universal.OpenRecentRepos),
Handler: self.handleCreateRecentReposMenu, Handler: self.helpers.Repos.CreateRecentReposMenu,
Description: self.c.Tr.SwitchRepo, Description: self.c.Tr.SwitchRepo,
}, },
{ {
@ -153,12 +153,6 @@ func (self *Gui) GetInitialKeybindings() ([]*types.Binding, []*gocui.ViewMouseBi
Description: self.c.Tr.LcOpenMenu, Description: self.c.Tr.LcOpenMenu,
Handler: self.handleCreateOptionsMenu, Handler: self.handleCreateOptionsMenu,
}, },
{
ViewName: "status",
Key: opts.GetKey(opts.Config.Universal.Edit),
Handler: self.handleEditConfig,
Description: self.c.Tr.EditConfig,
},
{ {
ViewName: "", ViewName: "",
Key: opts.GetKey(opts.Config.Universal.NextScreenMode), Key: opts.GetKey(opts.Config.Universal.NextScreenMode),
@ -171,30 +165,7 @@ func (self *Gui) GetInitialKeybindings() ([]*types.Binding, []*gocui.ViewMouseBi
Handler: self.prevScreenMode, Handler: self.prevScreenMode,
Description: self.c.Tr.LcPrevScreenMode, 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", ViewName: "files",
Key: opts.GetKey(opts.Config.Universal.CopyToClipboard), Key: opts.GetKey(opts.Config.Universal.CopyToClipboard),
@ -309,12 +280,6 @@ func (self *Gui) GetInitialKeybindings() ([]*types.Binding, []*gocui.ViewMouseBi
Modifier: gocui.ModNone, Modifier: gocui.ModNone,
Handler: self.scrollUpSecondary, Handler: self.scrollUpSecondary,
}, },
{
ViewName: "status",
Key: gocui.MouseLeft,
Modifier: gocui.ModNone,
Handler: self.handleStatusClick,
},
{ {
ViewName: "search", ViewName: "search",
Key: opts.GetKey(opts.Config.Universal.Confirm), Key: opts.GetKey(opts.Config.Universal.Confirm),
@ -496,7 +461,9 @@ func (gui *Gui) resetKeybindings() error {
for _, values := range gui.viewTabMap() { for _, values := range gui.viewTabMap() {
for _, value := range values { for _, value := range values {
viewName := value.ViewName 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 { if err := gui.g.SetTabClickBinding(viewName, tabClickCallback); err != nil {
return err return err

View File

@ -101,11 +101,11 @@ func (gui *Gui) layout(g *gocui.Gui) error {
if err != nil && !gocui.IsUnknownView(err) { if err != nil && !gocui.IsUnknownView(err) {
return 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 { if gui.PrevLayout.Information != informationStr {
gui.setViewContent(gui.Views.Information, informationStr) gui.c.SetViewContent(gui.Views.Information, informationStr)
gui.PrevLayout.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 { if err := gui.c.PushContext(initialContext); err != nil {
return err return err
} }
@ -226,15 +226,44 @@ func (gui *Gui) onInitialViewsCreation() error {
} }
if gui.showRecentRepos { if gui.showRecentRepos {
if err := gui.handleCreateRecentReposMenu(); err != nil { if err := gui.helpers.Repos.CreateRecentReposMenu(); err != nil {
return err return err
} }
gui.showRecentRepos = false gui.showRecentRepos = false
} }
gui.Updater.CheckForNewUpdate(gui.onBackgroundUpdateCheckFinish, false) gui.helpers.Update.CheckForUpdateInBackground()
gui.waitForIntro.Done() gui.waitForIntro.Done()
return nil 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
}

View File

@ -34,9 +34,6 @@ func (gui *Gui) filesListContext() *context.WorkingTreeContext {
return []string{line} return []string{line}
}) })
}, },
nil,
gui.withDiffModeCheck(gui.filesRenderToMain),
nil,
gui.c, gui.c,
) )
} }
@ -48,9 +45,6 @@ func (gui *Gui) branchesListContext() *context.BranchesContext {
func(startIdx int, length int) [][]string { 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) 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, gui.c,
) )
} }
@ -62,9 +56,6 @@ func (gui *Gui) remotesListContext() *context.RemotesContext {
func(startIdx int, length int) [][]string { func(startIdx int, length int) [][]string {
return presentation.GetRemoteListDisplayStrings(gui.State.Model.Remotes, gui.State.Modes.Diffing.Ref) return presentation.GetRemoteListDisplayStrings(gui.State.Model.Remotes, gui.State.Modes.Diffing.Ref)
}, },
nil,
gui.withDiffModeCheck(gui.remotesRenderToMain),
nil,
gui.c, gui.c,
) )
} }
@ -76,9 +67,6 @@ func (gui *Gui) remoteBranchesListContext() *context.RemoteBranchesContext {
func(startIdx int, length int) [][]string { func(startIdx int, length int) [][]string {
return presentation.GetRemoteBranchListDisplayStrings(gui.State.Model.RemoteBranches, gui.State.Modes.Diffing.Ref) return presentation.GetRemoteBranchListDisplayStrings(gui.State.Model.RemoteBranches, gui.State.Modes.Diffing.Ref)
}, },
nil,
gui.withDiffModeCheck(gui.remoteBranchesRenderToMain),
nil,
gui.c, gui.c,
) )
} }
@ -86,7 +74,7 @@ func (gui *Gui) remoteBranchesListContext() *context.RemoteBranchesContext {
func (gui *Gui) withDiffModeCheck(f func() error) func() error { func (gui *Gui) withDiffModeCheck(f func() error) func() error {
return func() error { return func() error {
if gui.State.Modes.Diffing.Active() { if gui.State.Modes.Diffing.Active() {
return gui.renderDiff() return gui.helpers.Diff.RenderDiff()
} }
return f() return f()
@ -100,9 +88,6 @@ func (gui *Gui) tagsListContext() *context.TagsContext {
func(startIdx int, length int) [][]string { func(startIdx int, length int) [][]string {
return presentation.GetTagListDisplayStrings(gui.State.Model.Tags, gui.State.Modes.Diffing.Ref) return presentation.GetTagListDisplayStrings(gui.State.Model.Tags, gui.State.Modes.Diffing.Ref)
}, },
nil,
gui.withDiffModeCheck(gui.tagsRenderToMain),
nil,
gui.c, gui.c,
) )
} }
@ -113,7 +98,7 @@ func (gui *Gui) branchCommitsListContext() *context.LocalCommitsContext {
gui.Views.Commits, gui.Views.Commits,
func(startIdx int, length int) [][]string { func(startIdx int, length int) [][]string {
selectedCommitSha := "" 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() selectedCommit := gui.State.Contexts.LocalCommits.GetSelected()
if selectedCommit != nil { if selectedCommit != nil {
selectedCommitSha = selectedCommit.Sha selectedCommitSha = selectedCommit.Sha
@ -138,9 +123,6 @@ func (gui *Gui) branchCommitsListContext() *context.LocalCommitsContext {
showYouAreHereLabel, showYouAreHereLabel,
) )
}, },
OnFocusWrapper(gui.onCommitFocus),
gui.withDiffModeCheck(gui.branchCommitsRenderToMain),
nil,
gui.c, gui.c,
) )
} }
@ -151,7 +133,7 @@ func (gui *Gui) subCommitsListContext() *context.SubCommitsContext {
gui.Views.SubCommits, gui.Views.SubCommits,
func(startIdx int, length int) [][]string { func(startIdx int, length int) [][]string {
selectedCommitSha := "" 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() selectedCommit := gui.State.Contexts.SubCommits.GetSelected()
if selectedCommit != nil { if selectedCommit != nil {
selectedCommitSha = selectedCommit.Sha selectedCommitSha = selectedCommit.Sha
@ -173,9 +155,6 @@ func (gui *Gui) subCommitsListContext() *context.SubCommitsContext {
false, false,
) )
}, },
OnFocusWrapper(gui.onSubCommitFocus),
gui.withDiffModeCheck(gui.subCommitsRenderToMain),
nil,
gui.c, gui.c,
) )
} }
@ -213,9 +192,6 @@ func (gui *Gui) reflogCommitsListContext() *context.ReflogCommitsContext {
gui.c.UserConfig.Git.ParseEmoji, gui.c.UserConfig.Git.ParseEmoji,
) )
}, },
nil,
gui.withDiffModeCheck(gui.reflogCommitsRenderToMain),
nil,
gui.c, gui.c,
) )
} }
@ -227,9 +203,6 @@ func (gui *Gui) stashListContext() *context.StashContext {
func(startIdx int, length int) [][]string { func(startIdx int, length int) [][]string {
return presentation.GetStashEntryListDisplayStrings(gui.State.Model.StashEntries, gui.State.Modes.Diffing.Ref) return presentation.GetStashEntryListDisplayStrings(gui.State.Model.StashEntries, gui.State.Modes.Diffing.Ref)
}, },
nil,
gui.withDiffModeCheck(gui.stashRenderToMain),
nil,
gui.c, gui.c,
) )
} }
@ -248,9 +221,6 @@ func (gui *Gui) commitFilesListContext() *context.CommitFilesContext {
return []string{line} return []string{line}
}) })
}, },
nil,
gui.withDiffModeCheck(gui.commitFilesRenderToMain),
nil,
gui.c, gui.c,
) )
} }
@ -262,9 +232,6 @@ func (gui *Gui) submodulesListContext() *context.SubmodulesContext {
func(startIdx int, length int) [][]string { func(startIdx int, length int) [][]string {
return presentation.GetSubmoduleListDisplayStrings(gui.State.Model.Submodules) return presentation.GetSubmoduleListDisplayStrings(gui.State.Model.Submodules)
}, },
nil,
gui.withDiffModeCheck(gui.submodulesRenderToMain),
nil,
gui.c, gui.c,
) )
} }
@ -276,12 +243,6 @@ func (gui *Gui) suggestionsListContext() *context.SuggestionsContext {
func(startIdx int, length int) [][]string { func(startIdx int, length int) [][]string {
return presentation.GetSuggestionListDisplayStrings(gui.State.Suggestions) return presentation.GetSuggestionListDisplayStrings(gui.State.Suggestions)
}, },
nil,
nil,
func(types.OnFocusLostOpts) error {
gui.deactivateConfirmationPrompt()
return nil
},
gui.c, gui.c,
) )
} }

View File

@ -34,11 +34,11 @@ func (gui *Gui) moveMainContextPairToTop(pair types.MainContextPair) {
} }
func (gui *Gui) moveMainContextToTop(context types.Context) { func (gui *Gui) moveMainContextToTop(context types.Context) {
gui.setWindowContext(context) gui.helpers.Window.SetWindowContext(context)
view := context.GetView() view := context.GetView()
topView := gui.topViewInWindow(context.GetWindowName()) topView := gui.helpers.Window.TopViewInWindow(context.GetWindowName())
if topView == nil { if topView == nil {
gui.Log.Error("unexpected: topView is nil") gui.Log.Error("unexpected: topView is nil")
return return

View File

@ -4,6 +4,7 @@ import (
"fmt" "fmt"
"github.com/jesseduffield/lazygit/pkg/commands/types/enums" "github.com/jesseduffield/lazygit/pkg/commands/types/enums"
"github.com/jesseduffield/lazygit/pkg/gui/presentation"
"github.com/jesseduffield/lazygit/pkg/gui/style" "github.com/jesseduffield/lazygit/pkg/gui/style"
) )
@ -22,12 +23,12 @@ func (gui *Gui) modeStatuses() []modeStatus {
fmt.Sprintf( fmt.Sprintf(
"%s %s", "%s %s",
gui.c.Tr.LcShowingGitDiff, gui.c.Tr.LcShowingGitDiff,
"git diff "+gui.diffStr(), "git diff "+gui.helpers.Diff.DiffStr(),
), ),
style.FgMagenta, style.FgMagenta,
) )
}, },
reset: gui.exitDiffMode, reset: gui.helpers.Diff.ExitDiffMode,
}, },
{ {
isActive: gui.git.Patch.PatchBuilder.Active, isActive: gui.git.Patch.PatchBuilder.Active,
@ -77,7 +78,7 @@ func (gui *Gui) modeStatuses() []modeStatus {
description: func() string { description: func() string {
workingTreeState := gui.git.Status.WorkingTreeState() workingTreeState := gui.git.Status.WorkingTreeState()
return gui.withResetButton( return gui.withResetButton(
formatWorkingTreeState(workingTreeState), style.FgYellow, presentation.FormatWorkingTreeState(workingTreeState), style.FgYellow,
) )
}, },
reset: gui.helpers.MergeAndRebase.AbortMergeOrRebaseWithConfirm, reset: gui.helpers.MergeAndRebase.AbortMergeOrRebaseWithConfirm,

56
pkg/gui/options_map.go Normal file
View File

@ -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,
}
}

View File

@ -50,7 +50,7 @@ func uniqueBindings(bindings []*types.Binding) []*types.Binding {
} }
func (gui *Gui) handleCreateOptionsMenu() error { func (gui *Gui) handleCreateOptionsMenu() error {
ctx := gui.currentContext() ctx := gui.c.CurrentContext()
// Don't show menu while displaying popup. // Don't show menu while displaying popup.
if ctx.GetKind() == types.PERSISTENT_POPUP || ctx.GetKind() == types.TEMPORARY_POPUP { if ctx.GetKind() == types.PERSISTENT_POPUP || ctx.GetKind() == types.TEMPORARY_POPUP {
return nil return nil

View File

@ -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"
}
}

View File

@ -1,33 +1,10 @@
package gui package gui
import ( import (
"os"
"github.com/jesseduffield/gocui" "github.com/jesseduffield/gocui"
"github.com/jesseduffield/lazygit/pkg/gui/types" "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 { func (gui *Gui) handleQuitWithoutChangingDirectory() error {
gui.RetainOriginalDir = true gui.RetainOriginalDir = true
return gui.quit() return gui.quit()
@ -39,7 +16,7 @@ func (gui *Gui) handleQuit() error {
} }
func (gui *Gui) handleTopLevelReturn() error { func (gui *Gui) handleTopLevelReturn() error {
currentContext := gui.currentContext() currentContext := gui.c.CurrentContext()
parentContext, hasParent := currentContext.GetParentContext() parentContext, hasParent := currentContext.GetParentContext()
if hasParent && currentContext != nil && parentContext != nil { 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() { if !repoPathStack.IsEmpty() {
path := repoPathStack.Pop() return gui.helpers.Repos.DispatchSwitchToRepo(repoPathStack.Pop(), true)
return gui.dispatchSwitchToRepo(path, true)
} }
if gui.c.UserConfig.QuitOnTopLevelReturn { if gui.c.UserConfig.QuitOnTopLevelReturn {
@ -68,7 +43,7 @@ func (gui *Gui) handleTopLevelReturn() error {
} }
func (gui *Gui) quit() error { func (gui *Gui) quit() error {
if gui.State.Updating { if gui.c.State().GetUpdating() {
return gui.createUpdateQuitConfirmation() return gui.createUpdateQuitConfirmation()
} }
@ -84,3 +59,13 @@ func (gui *Gui) quit() error {
return gocui.ErrQuit 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
},
})
}

View File

@ -1,160 +1,10 @@
package gui package gui
import ( import (
"fmt"
"os" "os"
"path/filepath" "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, // updateRecentRepoList registers the fact that we opened lazygit in this repo,
// so that we can open the same repo via the 'recent repos' menu // so that we can open the same repo via the 'recent repos' menu
func (gui *Gui) updateRecentRepoList() error { func (gui *Gui) updateRecentRepoList() error {

View File

@ -1,746 +1,9 @@
package gui package gui
import ( 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/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 { func (gui *Gui) Refresh(options types.RefreshOptions) error {
if options.Scope == nil { return gui.helpers.Refresh.Refresh(options)
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)
} }

View File

@ -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,
},
})
}

View File

@ -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,
},
})
}

View File

@ -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 { return func(y int, index int, total int) error {
if total == 0 { if total == 0 {
return gui.renderString( gui.c.SetViewContent(
gui.Views.Search, gui.Views.Search,
fmt.Sprintf( fmt.Sprintf(
"no matches for '%s' %s", "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)), theme.OptionsFgColor.Sprintf("%s: exit search mode", keybindings.Label(keybindingConfig.Universal.Return)),
), ),
) )
return nil
} }
_ = gui.renderString( gui.c.SetViewContent(
gui.Views.Search, gui.Views.Search,
fmt.Sprintf( fmt.Sprintf(
"matches for '%s' (%d of %d) %s", "matches for '%s' (%d of %d) %s",

View File

@ -2,7 +2,7 @@ package gui
func (gui *Gui) nextSideWindow() error { func (gui *Gui) nextSideWindow() error {
windows := gui.getCyclableWindows() windows := gui.getCyclableWindows()
currentWindow := gui.currentWindow() currentWindow := gui.helpers.Window.CurrentWindow()
var newWindow string var newWindow string
if currentWindow == "" || currentWindow == windows[len(windows)-1] { if currentWindow == "" || currentWindow == windows[len(windows)-1] {
newWindow = windows[0] newWindow = windows[0]
@ -17,18 +17,16 @@ func (gui *Gui) nextSideWindow() error {
} }
} }
} }
if err := gui.resetOrigin(gui.Views.Main); err != nil { gui.c.ResetViewOrigin(gui.Views.Main)
return err
}
context := gui.getContextForWindow(newWindow) context := gui.helpers.Window.GetContextForWindow(newWindow)
return gui.c.PushContext(context) return gui.c.PushContext(context)
} }
func (gui *Gui) previousSideWindow() error { func (gui *Gui) previousSideWindow() error {
windows := gui.getCyclableWindows() windows := gui.getCyclableWindows()
currentWindow := gui.currentWindow() currentWindow := gui.helpers.Window.CurrentWindow()
var newWindow string var newWindow string
if currentWindow == "" || currentWindow == windows[0] { if currentWindow == "" || currentWindow == windows[0] {
newWindow = windows[len(windows)-1] newWindow = windows[len(windows)-1]
@ -43,19 +41,21 @@ func (gui *Gui) previousSideWindow() error {
} }
} }
} }
if err := gui.resetOrigin(gui.Views.Main); err != nil { gui.c.ResetViewOrigin(gui.Views.Main)
return err
}
context := gui.getContextForWindow(newWindow) context := gui.helpers.Window.GetContextForWindow(newWindow)
return gui.c.PushContext(context) return gui.c.PushContext(context)
} }
func (gui *Gui) goToSideWindow(window string) func() error { func (gui *Gui) goToSideWindow(window string) func() error {
return func() error { return func() error {
context := gui.getContextForWindow(window) context := gui.helpers.Window.GetContextForWindow(window)
return gui.c.PushContext(context) return gui.c.PushContext(context)
} }
} }
func (gui *Gui) getCyclableWindows() []string {
return []string{"status", "files", "branches", "commits", "stash"}
}

View File

@ -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
}

View File

@ -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,
},
})
}

View File

@ -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 `
_ _ _
| | (_) |
| | __ _ _____ _ __ _ _| |_
| |/ _` + "`" + ` |_ / | | |/ _` + "`" + ` | | __|
| | (_| |/ /| |_| | (_| | | |_
|_|\__,_/___|\__, |\__, |_|\__|
__/ | __/ |
|___/ |___/ `
}

View File

@ -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,
},
})
}

View File

@ -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)
}

View File

@ -21,6 +21,6 @@ func (gui *Gui) getSelectedSuggestion() *types.Suggestion {
func (gui *Gui) setSuggestions(suggestions []*types.Suggestion) { func (gui *Gui) setSuggestions(suggestions []*types.Suggestion) {
gui.State.Suggestions = suggestions gui.State.Suggestions = suggestions
gui.State.Contexts.Suggestions.SetSelectedLineIdx(0) gui.State.Contexts.Suggestions.SetSelectedLineIdx(0)
_ = gui.resetOrigin(gui.Views.Suggestions) gui.c.ResetViewOrigin(gui.Views.Suggestions)
_ = gui.State.Contexts.Suggestions.HandleRender() _ = gui.State.Contexts.Suggestions.HandleRender()
} }

View File

@ -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,
},
})
}

View File

@ -49,7 +49,7 @@ func (gui *Gui) newStringTaskWithoutScroll(view *gocui.View, str string) error {
manager := gui.getManager(view) manager := gui.getManager(view)
f := func(stop chan struct{}) error { f := func(stop chan struct{}) error {
gui.setViewContent(view, str) gui.c.SetViewContent(view, str)
return nil return nil
} }
@ -66,7 +66,7 @@ func (gui *Gui) newStringTaskWithScroll(view *gocui.View, str string, originX in
manager := gui.getManager(view) manager := gui.getManager(view)
f := func(stop chan struct{}) error { f := func(stop chan struct{}) error {
gui.setViewContent(view, str) gui.c.SetViewContent(view, str)
_ = view.SetOrigin(originX, originY) _ = view.SetOrigin(originX, originY)
return nil return nil
} }
@ -82,7 +82,9 @@ func (gui *Gui) newStringTaskWithKey(view *gocui.View, str string, key string) e
manager := gui.getManager(view) manager := gui.getManager(view)
f := func(stop chan struct{}) error { 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 { if err := manager.NewTask(f, key); err != nil {

View File

@ -1,12 +1,15 @@
package types package types
import ( 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/git_commands"
"github.com/jesseduffield/lazygit/pkg/commands/models" "github.com/jesseduffield/lazygit/pkg/commands/models"
"github.com/jesseduffield/lazygit/pkg/commands/oscommands" "github.com/jesseduffield/lazygit/pkg/commands/oscommands"
"github.com/jesseduffield/lazygit/pkg/commands/types/enums" "github.com/jesseduffield/lazygit/pkg/commands/types/enums"
"github.com/jesseduffield/lazygit/pkg/common" "github.com/jesseduffield/lazygit/pkg/common"
"github.com/jesseduffield/lazygit/pkg/config" "github.com/jesseduffield/lazygit/pkg/config"
"github.com/jesseduffield/lazygit/pkg/utils"
"github.com/sasha-s/go-deadlock" "github.com/sasha-s/go-deadlock"
"gopkg.in/ozeidan/fuzzy-patricia.v3/patricia" "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 // 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' // case would be overkill, although refresh will internally call 'PostRefreshUpdate'
PostRefreshUpdate(Context) error 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 // this just re-renders the screen
Render() Render()
// allows rendering to main views (i.e. the ones to the right of the side panel) // 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 PushContext(context Context, opts ...OnFocusOpts) error
PopContext() error PopContext() error
ReplaceContext(context Context) error
CurrentContext() Context CurrentContext() Context
CurrentStaticContext() Context CurrentStaticContext() Context
CurrentSideContext() Context
IsCurrentContext(Context) bool IsCurrentContext(Context) bool
// enters search mode for the current view // enters search mode for the current view
OpenSearch() OpenSearch()
GetConfig() config.AppConfigurer
GetAppState() *config.AppState GetAppState() *config.AppState
SaveAppState() error 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. // 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. // All controller handlers are executed on the UI thread.
OnUIThread(f func() error) 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 { type IPopupHandler interface {
@ -176,3 +203,36 @@ type Mutexes struct {
PopupMutex *deadlock.Mutex PopupMutex *deadlock.Mutex
PtyMutex *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
}

View File

@ -72,6 +72,10 @@ type IBaseContext interface {
// our list controller can come along and wrap it in a list-specific click handler. // 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. // We'll need to think of a better way to do this.
AddOnClickFn(func() error) AddOnClickFn(func() error)
AddOnRenderToMainFn(func() error)
AddOnFocusFn(func(OnFocusOpts) error)
AddOnFocusLostFn(func(OnFocusLostOpts) error)
} }
type Context interface { type Context interface {
@ -83,6 +87,16 @@ type Context interface {
HandleRenderToMain() error 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 { type IListContext interface {
Context Context
@ -150,6 +164,9 @@ type HasKeybindings interface {
GetKeybindings(opts KeybindingsOpts) []*Binding GetKeybindings(opts KeybindingsOpts) []*Binding
GetMouseKeybindings(opts KeybindingsOpts) []*gocui.ViewMouseBinding GetMouseKeybindings(opts KeybindingsOpts) []*gocui.ViewMouseBinding
GetOnClick() func() error GetOnClick() func() error
GetOnRenderToMain() func() error
GetOnFocus() func(OnFocusOpts) error
GetOnFocusLost() func(OnFocusLostOpts) error
} }
type IController interface { type IController interface {

View File

@ -6,6 +6,7 @@ type RefreshableView int
const ( const (
COMMITS RefreshableView = iota COMMITS RefreshableView = iota
REBASE_COMMITS REBASE_COMMITS
SUB_COMMITS
BRANCHES BRANCHES
FILES FILES
STASH STASH
@ -32,6 +33,6 @@ const (
type RefreshOptions struct { type RefreshOptions struct {
Then func() 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 Mode RefreshMode // one of SYNC (default), ASYNC, and BLOCK_UI
} }

42
pkg/gui/types/views.go Normal file
View File

@ -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
}

View File

@ -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
},
})
}

View File

@ -1,19 +1,21 @@
package gui package gui
import ( import (
"fmt"
"github.com/jesseduffield/gocui" "github.com/jesseduffield/gocui"
"github.com/jesseduffield/lazygit/pkg/gui/keybindings"
"github.com/jesseduffield/lazygit/pkg/gui/types" "github.com/jesseduffield/lazygit/pkg/gui/types"
"github.com/jesseduffield/lazygit/pkg/tasks" "github.com/jesseduffield/lazygit/pkg/tasks"
"github.com/jesseduffield/lazygit/pkg/utils" "github.com/jesseduffield/lazygit/pkg/utils"
"github.com/spkg/bom" "github.com/spkg/bom"
) )
func (gui *Gui) resetOrigin(v *gocui.View) error { func (gui *Gui) resetViewOrigin(v *gocui.View) {
_ = v.SetCursor(0, 0) if err := v.SetCursor(0, 0); err != nil {
return v.SetOrigin(0, 0) 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 // 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)) 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 { func (gui *Gui) currentViewName() string {
currentView := gui.g.CurrentView() currentView := gui.g.CurrentView()
if currentView == nil { if currentView == nil {
@ -129,20 +119,6 @@ func (gui *Gui) resizeConfirmationPanel() {
_, _ = gui.g.SetView(gui.Views.Suggestions.Name(), x0, suggestionsViewTop, x1, suggestionsViewTop+suggestionsViewHeight, 0) _, _ = 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 { func (gui *Gui) isPopupPanel(viewName string) bool {
return viewName == "commitMessage" || viewName == "confirmation" || viewName == "menu" return viewName == "commitMessage" || viewName == "confirmation" || viewName == "menu"
} }
@ -159,7 +135,7 @@ func (gui *Gui) onViewTabClick(windowName string, tabIndex int) error {
viewName := tabs[tabIndex].ViewName viewName := tabs[tabIndex].ViewName
context, ok := gui.contextForView(viewName) context, ok := gui.helpers.View.ContextForView(viewName)
if !ok { if !ok {
return nil return nil
} }
@ -167,21 +143,6 @@ func (gui *Gui) onViewTabClick(windowName string, tabIndex int) error {
return gui.c.PushContext(context) 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 { func (gui *Gui) handleNextTab() error {
view := getTabbedView(gui) view := getTabbedView(gui)
if view == nil { if view == nil {
@ -220,7 +181,7 @@ func (gui *Gui) handlePrevTab() error {
func getTabbedView(gui *Gui) *gocui.View { func getTabbedView(gui *Gui) *gocui.View {
// It safe assumption that only static contexts have tabs // It safe assumption that only static contexts have tabs
context := gui.currentStaticContext() context := gui.c.CurrentStaticContext()
view, _ := gui.g.View(context.GetViewName()) view, _ := gui.g.View(context.GetViewName())
return view return view
} }
@ -228,3 +189,20 @@ func getTabbedView(gui *Gui) *gocui.View {
func (gui *Gui) render() { func (gui *Gui) render() {
gui.c.OnUIThread(func() error { return nil }) 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
}

View File

@ -6,45 +6,6 @@ import (
"github.com/jesseduffield/lazygit/pkg/theme" "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 { type viewNameMapping struct {
viewPtr **gocui.View viewPtr **gocui.View
name string 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 { func (gui *Gui) createAllViews() error {
frameRunes := []rune{'─', '│', '┌', '┐', '└', '┘'} frameRunes := []rune{'─', '│', '┌', '┐', '└', '┘'}
switch gui.c.UserConfig.Gui.Border { switch gui.c.UserConfig.Gui.Border {
@ -140,7 +92,7 @@ func (gui *Gui) createAllViews() error {
gui.Views.SearchPrefix.BgColor = gocui.ColorDefault gui.Views.SearchPrefix.BgColor = gocui.ColorDefault
gui.Views.SearchPrefix.FgColor = gocui.ColorGreen gui.Views.SearchPrefix.FgColor = gocui.ColorGreen
gui.Views.SearchPrefix.Frame = false 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 gui.Views.Stash.Title = gui.c.Tr.StashTitle

View File

@ -13,5 +13,5 @@ func (gui *Gui) toggleWhitespaceInDiffView() error {
} }
gui.c.Toast(toastMessage) gui.c.Toast(toastMessage)
return gui.currentSideListContext().HandleFocus(types.OnFocusOpts{}) return gui.c.CurrentSideContext().HandleFocus(types.OnFocusOpts{})
} }

View File

@ -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
}

Some files were not shown because too many files have changed in this diff Show More