diff --git a/pkg/cheatsheet/generate.go b/pkg/cheatsheet/generate.go index 15e390356..f499a2f58 100644 --- a/pkg/cheatsheet/generate.go +++ b/pkg/cheatsheet/generate.go @@ -124,7 +124,7 @@ func formatBinding(binding *types.Binding) string { func getBindingSections(mApp *app.App) []*bindingSection { bindingSections := []*bindingSection{} - bindings := mApp.Gui.GetInitialKeybindings() + bindings, _ := mApp.Gui.GetInitialKeybindings() type contextAndViewType struct { subtitle string diff --git a/pkg/gui/commit_files_panel.go b/pkg/gui/commit_files_panel.go index f55b3d6c3..05f23374c 100644 --- a/pkg/gui/commit_files_panel.go +++ b/pkg/gui/commit_files_panel.go @@ -257,7 +257,7 @@ func (gui *Gui) handleToggleCommitFileDirCollapsed() error { func (gui *Gui) SwitchToCommitFilesContext(opts controllers.SwitchToCommitFilesContextOpts) error { // sometimes the commitFiles view is already shown in another window, so we need to ensure that window // no longer considers the commitFiles view as its main view. - gui.resetWindowForView(gui.Views.CommitFiles) + gui.resetWindowContext(gui.State.Contexts.CommitFiles) gui.State.Contexts.CommitFiles.SetSelectedLineIdx(0) gui.State.Contexts.CommitFiles.SetRefName(opts.RefName) diff --git a/pkg/gui/context.go b/pkg/gui/context.go index 09b669b04..657274c1f 100644 --- a/pkg/gui/context.go +++ b/pkg/gui/context.go @@ -60,6 +60,10 @@ func (gui *Gui) pushContext(c types.Context, opts ...types.OnFocusOpts) error { return errors.New("cannot pass multiple opts to pushContext") } + if c.GetKey() == context.GLOBAL_CONTEXT_KEY { + return errors.New("Cannot push global context") + } + gui.State.ContextManager.Lock() // push onto stack @@ -112,6 +116,8 @@ func (gui *Gui) returnFromContext() error { gui.State.ContextManager.ContextStack = gui.State.ContextManager.ContextStack[:n] + gui.g.SetCurrentContext(string(newContext.GetKey())) + gui.State.ContextManager.Unlock() if err := gui.deactivateContext(currentContext); err != nil { @@ -146,12 +152,7 @@ func (gui *Gui) deactivateContext(c types.Context) 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 { - v, err := gui.g.View(c.GetViewName()) - if err != nil { - return nil - } - - if types.ContextKey(v.Context) != c.GetKey() { + if gui.State.ViewContextMap[c.GetViewName()].GetKey() != c.GetKey() { return nil } @@ -174,19 +175,18 @@ func (gui *Gui) activateContext(c types.Context, opts ...types.OnFocusOpts) erro if err != nil { return err } - originalViewContextKey := types.ContextKey(v.Context) - - // ensure that any other window for which this view was active is now set to the default for that window. - gui.setViewAsActiveForWindow(v) - - if viewName == "main" { - gui.changeMainViewsContext(c.GetKey()) - } else { - gui.changeMainViewsContext(context.MAIN_NORMAL_CONTEXT_KEY) - } + originalViewContextKey := gui.State.ViewContextMap[viewName].GetKey() + gui.setWindowContext(c) gui.setViewTabForContext(c) + if viewName == "main" { + gui.changeMainViewsContext(c) + } else { + gui.changeMainViewsContext(gui.State.Contexts.Normal) + } + + gui.g.SetCurrentContext(string(c.GetKey())) if _, err := gui.g.SetCurrentView(viewName); err != nil { return err } @@ -200,7 +200,7 @@ func (gui *Gui) activateContext(c types.Context, opts ...types.OnFocusOpts) erro } } - v.Context = string(c.GetKey()) + gui.State.ViewContextMap[viewName] = c gui.g.Cursor = v.Editable @@ -310,21 +310,6 @@ func (gui *Gui) defaultSideContext() types.Context { } } -// remove the need to do this: always use a mapping -func (gui *Gui) setInitialViewContexts() { - // arguably we should only have our ViewContextMap and we should do away with - // contexts on views, or vice versa - for viewName, context := range gui.State.ViewContextMap { - // see if the view exists. If it does, set the context on it - view, err := gui.g.View(viewName) - if err != nil { - continue - } - - view.Context = string(context.GetKey()) - } -} - // 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 @@ -364,7 +349,7 @@ func (gui *Gui) onViewFocusLost(oldView *gocui.View, newView *gocui.View) error _ = oldView.SetOriginX(0) if oldView == gui.Views.CommitFiles && newView != gui.Views.Main && newView != gui.Views.Secondary && newView != gui.Views.Search { - gui.resetWindowForView(gui.Views.CommitFiles) + gui.resetWindowContext(gui.State.Contexts.CommitFiles) if err := gui.deactivateContext(gui.State.Contexts.CommitFiles); err != nil { return err } @@ -377,20 +362,20 @@ func (gui *Gui) onViewFocusLost(oldView *gocui.View, newView *gocui.View) error // which currently just means a context that affects both the main and secondary views // other views can have their context changed directly but this function helps // keep the main and secondary views in sync -func (gui *Gui) changeMainViewsContext(contextKey types.ContextKey) { - if gui.State.MainContext == contextKey { +func (gui *Gui) changeMainViewsContext(c types.Context) { + if gui.State.MainContext == c.GetKey() { return } - switch contextKey { + switch c.GetKey() { case context.MAIN_NORMAL_CONTEXT_KEY, context.MAIN_PATCH_BUILDING_CONTEXT_KEY, context.MAIN_STAGING_CONTEXT_KEY, context.MAIN_MERGING_CONTEXT_KEY: - gui.Views.Main.Context = string(contextKey) - gui.Views.Secondary.Context = string(contextKey) + gui.State.ViewContextMap[gui.Views.Main.Name()] = c + gui.State.ViewContextMap[gui.Views.Secondary.Name()] = c default: - panic(fmt.Sprintf("unknown context for main: %s", contextKey)) + panic(fmt.Sprintf("unknown context for main: %s", c.GetKey())) } - gui.State.MainContext = contextKey + gui.State.MainContext = c.GetKey() } func (gui *Gui) viewTabNames(viewName string) []string { @@ -452,8 +437,11 @@ func (gui *Gui) contextForContextKey(contextKey types.ContextKey) (types.Context } func (gui *Gui) rerenderView(view *gocui.View) error { - contextKey := types.ContextKey(view.Context) - context := gui.mustContextForContextKey(contextKey) + context, ok := gui.State.ViewContextMap[view.Name()] + + if !ok { + panic("no context set against view " + view.Name()) + } return context.HandleRender() } @@ -467,6 +455,10 @@ func (gui *Gui) getSideContextSelectedItemId() string { return currentSideContext.GetSelectedItemId() } +func (gui *Gui) isContextVisible(c types.Context) bool { + return gui.State.WindowViewNameMap[c.GetWindowName()] == c.GetViewName() && gui.State.ViewContextMap[c.GetViewName()].GetKey() == c.GetKey() +} + // currently unused // func (gui *Gui) getCurrentSideView() *gocui.View { // currentSideContext := gui.currentSideContext() diff --git a/pkg/gui/context/base_context.go b/pkg/gui/context/base_context.go index c61f57cf4..5dbc3cbf4 100644 --- a/pkg/gui/context/base_context.go +++ b/pkg/gui/context/base_context.go @@ -1,6 +1,7 @@ package context import ( + "github.com/jesseduffield/gocui" "github.com/jesseduffield/lazygit/pkg/gui/types" ) @@ -11,8 +12,8 @@ type BaseContext struct { windowName string onGetOptionsMap func() map[string]string - keybindingsFns []types.KeybindingsFn - keybindings []*types.Binding + keybindingsFns []types.KeybindingsFn + mouseKeybindingsFns []types.MouseKeybindingsFn *ParentContextMgr } @@ -80,3 +81,18 @@ func (self *BaseContext) GetKeybindings(opts types.KeybindingsOpts) []*types.Bin func (self *BaseContext) AddKeybindingsFn(fn types.KeybindingsFn) { self.keybindingsFns = append(self.keybindingsFns, fn) } + +func (self *BaseContext) AddMouseKeybindingsFn(fn types.MouseKeybindingsFn) { + self.mouseKeybindingsFns = append(self.mouseKeybindingsFns, fn) +} + +func (self *BaseContext) GetMouseKeybindings(opts types.KeybindingsOpts) []*gocui.ViewMouseBinding { + bindings := []*gocui.ViewMouseBinding{} + for i := range self.mouseKeybindingsFns { + // the first binding in the bindings array takes precedence but we want the + // last keybindingsFn to take precedence to we add them in reverse + bindings = append(bindings, self.mouseKeybindingsFns[len(self.mouseKeybindingsFns)-1-i](opts)...) + } + + return bindings +} diff --git a/pkg/gui/context/context.go b/pkg/gui/context/context.go index 20d67a5a7..b45a089be 100644 --- a/pkg/gui/context/context.go +++ b/pkg/gui/context/context.go @@ -3,6 +3,7 @@ package context import "github.com/jesseduffield/lazygit/pkg/gui/types" const ( + GLOBAL_CONTEXT_KEY types.ContextKey = "global" STATUS_CONTEXT_KEY types.ContextKey = "status" FILES_CONTEXT_KEY types.ContextKey = "files" LOCAL_BRANCHES_CONTEXT_KEY types.ContextKey = "localBranches" @@ -29,6 +30,7 @@ const ( ) var AllContextKeys = []types.ContextKey{ + GLOBAL_CONTEXT_KEY, STATUS_CONTEXT_KEY, FILES_CONTEXT_KEY, LOCAL_BRANCHES_CONTEXT_KEY, @@ -55,6 +57,7 @@ var AllContextKeys = []types.ContextKey{ } type ContextTree struct { + Global types.Context Status types.Context Files *WorkingTreeContext Submodules types.IListContext diff --git a/pkg/gui/context_config.go b/pkg/gui/context_config.go index eec6cdd69..4e5c241d0 100644 --- a/pkg/gui/context_config.go +++ b/pkg/gui/context_config.go @@ -7,6 +7,7 @@ import ( func (gui *Gui) allContexts() []types.Context { return []types.Context{ + gui.State.Contexts.Global, gui.State.Contexts.Status, gui.State.Contexts.Files, gui.State.Contexts.Submodules, @@ -34,12 +35,23 @@ func (gui *Gui) allContexts() []types.Context { func (gui *Gui) contextTree() *context.ContextTree { return &context.ContextTree{ + Global: NewSimpleContext( + context.NewBaseContext(context.NewBaseContextOpts{ + Kind: types.GLOBAL_CONTEXT, + ViewName: "", + WindowName: "", + Key: context.GLOBAL_CONTEXT_KEY, + }), + NewSimpleContextOpts{ + OnRenderToMain: OnFocusWrapper(gui.statusRenderToMain), + }, + ), Status: NewSimpleContext( context.NewBaseContext(context.NewBaseContextOpts{ Kind: types.SIDE_CONTEXT, ViewName: "status", - Key: context.STATUS_CONTEXT_KEY, WindowName: "status", + Key: context.STATUS_CONTEXT_KEY, }), NewSimpleContextOpts{ OnRenderToMain: OnFocusWrapper(gui.statusRenderToMain), diff --git a/pkg/gui/controllers/attach.go b/pkg/gui/controllers/attach.go new file mode 100644 index 000000000..008c15505 --- /dev/null +++ b/pkg/gui/controllers/attach.go @@ -0,0 +1,10 @@ +package controllers + +import "github.com/jesseduffield/lazygit/pkg/gui/types" + +func AttachControllers(context types.Context, controllers ...types.IController) { + for _, controller := range controllers { + context.AddKeybindingsFn(controller.GetKeybindings) + context.AddMouseKeybindingsFn(controller.GetMouseKeybindings) + } +} diff --git a/pkg/gui/controllers/base_controller.go b/pkg/gui/controllers/base_controller.go new file mode 100644 index 000000000..e510c1a9f --- /dev/null +++ b/pkg/gui/controllers/base_controller.go @@ -0,0 +1,16 @@ +package controllers + +import ( + "github.com/jesseduffield/gocui" + "github.com/jesseduffield/lazygit/pkg/gui/types" +) + +type baseController struct{} + +func (self *baseController) GetKeybindings(opts types.KeybindingsOpts) []*types.Binding { + return nil +} + +func (self *baseController) GetMouseKeybindings(opts types.KeybindingsOpts) []*gocui.ViewMouseBinding { + return nil +} diff --git a/pkg/gui/controllers/bisect_controller.go b/pkg/gui/controllers/bisect_controller.go index 9841b5e59..58c8a6db7 100644 --- a/pkg/gui/controllers/bisect_controller.go +++ b/pkg/gui/controllers/bisect_controller.go @@ -11,6 +11,8 @@ import ( ) type BisectController struct { + baseController + c *types.ControllerCommon context types.IListContext git *commands.GitCommand @@ -32,10 +34,11 @@ func NewBisectController( getCommits func() []*models.Commit, ) *BisectController { return &BisectController{ - c: c, - context: context, - git: git, - bisectHelper: bisectHelper, + baseController: baseController{}, + c: c, + context: context, + git: git, + bisectHelper: bisectHelper, getSelectedLocalCommit: getSelectedLocalCommit, getCommits: getCommits, diff --git a/pkg/gui/controllers/files_controller.go b/pkg/gui/controllers/files_controller.go index c8af30e62..d853ba731 100644 --- a/pkg/gui/controllers/files_controller.go +++ b/pkg/gui/controllers/files_controller.go @@ -190,6 +190,21 @@ func (self *FilesController) GetKeybindings(opts types.KeybindingsOpts) []*types } } +func (self *FilesController) GetMouseKeybindings(opts types.KeybindingsOpts) []*gocui.ViewMouseBinding { + return []*gocui.ViewMouseBinding{ + { + ViewName: "main", + Key: gocui.MouseLeft, + Handler: self.onClickMain, + }, + { + ViewName: "secondary", + Key: gocui.MouseLeft, + Handler: self.onClickSecondary, + }, + } +} + func (self *FilesController) press(node *filetree.FileNode) error { if node.IsLeaf() { file := node.File @@ -672,3 +687,13 @@ func (self *FilesController) handleStashSave(stashFunc func(message string) erro }, }) } + +func (self *FilesController) onClickMain(opts gocui.ViewMouseBindingOpts) error { + clickedViewLineIdx := opts.Cy + opts.Oy + return self.EnterFile(types.OnFocusOpts{ClickedViewName: "main", ClickedViewLineIdx: clickedViewLineIdx}) +} + +func (self *FilesController) onClickSecondary(opts gocui.ViewMouseBindingOpts) error { + clickedViewLineIdx := opts.Cy + opts.Oy + return self.EnterFile(types.OnFocusOpts{ClickedViewName: "secondary", ClickedViewLineIdx: clickedViewLineIdx}) +} diff --git a/pkg/gui/controllers/global_controller.go b/pkg/gui/controllers/global_controller.go index 3560a0412..dd0c8ea3b 100644 --- a/pkg/gui/controllers/global_controller.go +++ b/pkg/gui/controllers/global_controller.go @@ -7,6 +7,8 @@ import ( ) type GlobalController struct { + baseController + c *types.ControllerCommon os *oscommands.OSCommand } @@ -16,8 +18,9 @@ func NewGlobalController( os *oscommands.OSCommand, ) *GlobalController { return &GlobalController{ - c: c, - os: os, + baseController: baseController{}, + c: c, + os: os, } } diff --git a/pkg/gui/controllers/local_commits_controller.go b/pkg/gui/controllers/local_commits_controller.go index 5458697e3..e962ea48e 100644 --- a/pkg/gui/controllers/local_commits_controller.go +++ b/pkg/gui/controllers/local_commits_controller.go @@ -22,6 +22,7 @@ type ( ) type LocalCommitsController struct { + baseController c *types.ControllerCommon context types.IListContext os *oscommands.OSCommand @@ -68,6 +69,7 @@ func NewLocalCommitsController( setShowWholeGitGraph func(bool), ) *LocalCommitsController { return &LocalCommitsController{ + baseController: baseController{}, c: c, context: context, os: os, diff --git a/pkg/gui/controllers/menu_controller.go b/pkg/gui/controllers/menu_controller.go index 7c93ef6f7..cbd24e188 100644 --- a/pkg/gui/controllers/menu_controller.go +++ b/pkg/gui/controllers/menu_controller.go @@ -6,6 +6,8 @@ import ( ) type MenuController struct { + baseController + c *types.ControllerCommon context types.IListContext @@ -20,6 +22,8 @@ func NewMenuController( getSelectedMenuItem func() *types.MenuItem, ) *MenuController { return &MenuController{ + baseController: baseController{}, + c: c, context: context, getSelectedMenuItem: getSelectedMenuItem, diff --git a/pkg/gui/controllers/remotes_controller.go b/pkg/gui/controllers/remotes_controller.go index 9f7acb9d0..73b1c57ab 100644 --- a/pkg/gui/controllers/remotes_controller.go +++ b/pkg/gui/controllers/remotes_controller.go @@ -10,6 +10,8 @@ import ( ) type RemotesController struct { + baseController + c *types.ControllerCommon context types.IListContext git *commands.GitCommand @@ -30,6 +32,7 @@ func NewRemotesController( setRemoteBranches func([]*models.RemoteBranch), ) *RemotesController { return &RemotesController{ + baseController: baseController{}, c: c, git: git, contexts: contexts, diff --git a/pkg/gui/controllers/submodules_controller.go b/pkg/gui/controllers/submodules_controller.go index f1ac7acf3..1db27f6e6 100644 --- a/pkg/gui/controllers/submodules_controller.go +++ b/pkg/gui/controllers/submodules_controller.go @@ -13,6 +13,8 @@ import ( ) type SubmodulesController struct { + baseController + c *types.ControllerCommon context types.IListContext git *commands.GitCommand @@ -31,6 +33,7 @@ func NewSubmodulesController( getSelectedSubmodule func() *models.SubmoduleConfig, ) *SubmodulesController { return &SubmodulesController{ + baseController: baseController{}, c: c, context: context, git: git, diff --git a/pkg/gui/controllers/sync_controller.go b/pkg/gui/controllers/sync_controller.go index 106b4c516..f3f2894b0 100644 --- a/pkg/gui/controllers/sync_controller.go +++ b/pkg/gui/controllers/sync_controller.go @@ -11,10 +11,8 @@ import ( ) type SyncController struct { - // I've said publicly that I'm against single-letter variable names but in this - // case I would actually prefer a _zero_ letter variable name in the form of - // struct embedding, but Go does not allow hiding public fields in an embedded struct - // to the client + baseController + c *types.ControllerCommon git *commands.GitCommand @@ -35,8 +33,9 @@ func NewSyncController( CheckMergeOrRebase func(error) error, ) *SyncController { return &SyncController{ - c: c, - git: git, + baseController: baseController{}, + c: c, + git: git, getCheckedOutBranch: getCheckedOutBranch, suggestionsHelper: suggestionsHelper, diff --git a/pkg/gui/controllers/tags_controller.go b/pkg/gui/controllers/tags_controller.go index 231f12736..508820061 100644 --- a/pkg/gui/controllers/tags_controller.go +++ b/pkg/gui/controllers/tags_controller.go @@ -9,6 +9,8 @@ import ( ) type TagsController struct { + baseController + c *types.ControllerCommon context *context.TagsContext git *commands.GitCommand @@ -35,6 +37,7 @@ func NewTagsController( switchToSubCommitsContext func(string) error, ) *TagsController { return &TagsController{ + baseController: baseController{}, c: c, context: context, git: git, diff --git a/pkg/gui/controllers/undo_controller.go b/pkg/gui/controllers/undo_controller.go index 0f288957c..683fb2b84 100644 --- a/pkg/gui/controllers/undo_controller.go +++ b/pkg/gui/controllers/undo_controller.go @@ -19,6 +19,8 @@ import ( // two user actions, meaning we end up undoing reflog entry C. Redoing works in a similar way. type UndoController struct { + baseController + c *types.ControllerCommon git *commands.GitCommand @@ -39,6 +41,7 @@ func NewUndoController( getFilteredReflogCommits func() []*models.Commit, ) *UndoController { return &UndoController{ + baseController: baseController{}, c: c, git: git, refsHelper: refsHelper, diff --git a/pkg/gui/global_handlers.go b/pkg/gui/global_handlers.go index 04f7efb61..ba8a0a237 100644 --- a/pkg/gui/global_handlers.go +++ b/pkg/gui/global_handlers.go @@ -182,11 +182,6 @@ func (gui *Gui) handleRefresh() error { func (gui *Gui) handleMouseDownMain() error { switch gui.currentSideContext() { - case gui.State.Contexts.Files: - // set filename, set primary/secondary selected, set line number, then switch context - // I'll need to know it was changed though. - // Could I pass something along to the context change? - return gui.Controllers.Files.EnterFile(types.OnFocusOpts{ClickedViewName: "main", ClickedViewLineIdx: gui.Views.Main.SelectedLineIdx()}) case gui.State.Contexts.CommitFiles: return gui.enterCommitFile(types.OnFocusOpts{ClickedViewName: "main", ClickedViewLineIdx: gui.Views.Main.SelectedLineIdx()}) } @@ -194,15 +189,6 @@ func (gui *Gui) handleMouseDownMain() error { return nil } -func (gui *Gui) handleMouseDownSecondary() error { - switch gui.g.CurrentView() { - case gui.Views.Files: - return gui.Controllers.Files.EnterFile(types.OnFocusOpts{ClickedViewName: "secondary", ClickedViewLineIdx: gui.Views.Secondary.SelectedLineIdx()}) - } - - return nil -} - func (gui *Gui) fetch() (err error) { gui.c.LogAction("Fetch") err = gui.git.Sync.Fetch(git_commands.FetchOptions{}) diff --git a/pkg/gui/gui.go b/pkg/gui/gui.go index 2b4fe66ca..ccb032175 100644 --- a/pkg/gui/gui.go +++ b/pkg/gui/gui.go @@ -590,6 +590,15 @@ func (gui *Gui) resetControllers() { gui.getSelectedSubmodule, ) + bisectController := controllers.NewBisectController( + controllerCommon, + gui.State.Contexts.BranchCommits, + gui.git, + gui.helpers.Bisect, + gui.getSelectedLocalCommit, + func() []*models.Commit { return gui.State.Model.Commits }, + ) + gui.Controllers = Controllers{ Submodules: submodulesController, Global: controllers.NewGlobalController( @@ -660,14 +669,6 @@ func (gui *Gui) resetControllers() { gui.State.Contexts.Menu, gui.getSelectedMenuItem, ), - Bisect: controllers.NewBisectController( - controllerCommon, - gui.State.Contexts.BranchCommits, - gui.git, - gui.helpers.Bisect, - gui.getSelectedLocalCommit, - func() []*models.Commit { return gui.State.Model.Commits }, - ), Undo: controllers.NewUndoController( controllerCommon, gui.git, @@ -678,17 +679,13 @@ func (gui *Gui) resetControllers() { Sync: syncController, } - gui.State.Contexts.Submodules.AddKeybindingsFn(gui.Controllers.Submodules.GetKeybindings) - gui.Controllers.Files.Attach(gui.State.Contexts.Files) - gui.State.Contexts.Files.AddKeybindingsFn(gui.Controllers.Files.GetKeybindings) - gui.State.Contexts.Tags.AddKeybindingsFn(gui.Controllers.Tags.GetKeybindings) - // TODO: commit to one name here: local commits or branch commits - gui.State.Contexts.BranchCommits.AddKeybindingsFn(gui.Controllers.LocalCommits.GetKeybindings) - gui.State.Contexts.BranchCommits.AddKeybindingsFn(gui.Controllers.Bisect.GetKeybindings) - gui.State.Contexts.Remotes.AddKeybindingsFn(gui.Controllers.Remotes.GetKeybindings) - gui.State.Contexts.Menu.AddKeybindingsFn(gui.Controllers.Menu.GetKeybindings) - gui.State.Contexts.Menu.AddKeybindingsFn(gui.Controllers.Menu.GetKeybindings) - // TODO: handle global contexts + controllers.AttachControllers(gui.State.Contexts.Files, gui.Controllers.Files) + controllers.AttachControllers(gui.State.Contexts.Tags, gui.Controllers.Tags) + controllers.AttachControllers(gui.State.Contexts.Submodules, gui.Controllers.Submodules) + controllers.AttachControllers(gui.State.Contexts.BranchCommits, gui.Controllers.LocalCommits, bisectController) + controllers.AttachControllers(gui.State.Contexts.Remotes, gui.Controllers.Remotes) + controllers.AttachControllers(gui.State.Contexts.Menu, gui.Controllers.Menu) + controllers.AttachControllers(gui.State.Contexts.Global, gui.Controllers.Sync, gui.Controllers.Undo, gui.Controllers.Global) } var RuneReplacements = map[rune]string{ diff --git a/pkg/gui/gui_test.go b/pkg/gui/gui_test.go index 1290f3185..e35ab1896 100644 --- a/pkg/gui/gui_test.go +++ b/pkg/gui/gui_test.go @@ -39,7 +39,6 @@ import ( // original playback speed. Speed may be a decimal. func Test(t *testing.T) { - return mode := integration.GetModeFromEnv() speedEnv := os.Getenv("SPEED") includeSkipped := os.Getenv("INCLUDE_SKIPPED") != "" diff --git a/pkg/gui/keybindings.go b/pkg/gui/keybindings.go index e72e604a1..50a8587ec 100644 --- a/pkg/gui/keybindings.go +++ b/pkg/gui/keybindings.go @@ -194,8 +194,7 @@ func (gui *Gui) noPopupPanel(f func() error) func() error { } } -// GetInitialKeybindings is a function. -func (gui *Gui) GetInitialKeybindings() []*types.Binding { +func (gui *Gui) GetInitialKeybindings() ([]*types.Binding, []*gocui.ViewMouseBinding) { config := gui.c.UserConfig.Keybinding guards := types.KeybindingGuards{ @@ -306,12 +305,6 @@ func (gui *Gui) GetInitialKeybindings() []*types.Binding { Modifier: gocui.ModNone, Handler: gui.handleCreateOptionsMenu, }, - { - ViewName: "", - Key: gocui.MouseMiddle, - Modifier: gocui.ModNone, - Handler: gui.handleCreateOptionsMenu, - }, { ViewName: "status", Key: gui.getKey(config.Universal.Edit), @@ -788,13 +781,6 @@ func (gui *Gui) GetInitialKeybindings() []*types.Binding { Modifier: gocui.ModNone, Handler: gui.scrollDownSecondary, }, - { - ViewName: "secondary", - Contexts: []string{string(context.MAIN_NORMAL_CONTEXT_KEY)}, - Key: gocui.MouseLeft, - Modifier: gocui.ModNone, - Handler: gui.handleMouseDownSecondary, - }, { ViewName: "main", Contexts: []string{string(context.MAIN_NORMAL_CONTEXT_KEY)}, @@ -1361,35 +1347,24 @@ func (gui *Gui) GetInitialKeybindings() []*types.Binding { Guards: guards, } - // global bindings - for _, controller := range []types.IController{ - gui.Controllers.Sync, - gui.Controllers.Undo, - gui.Controllers.Global, - } { - context := controller.Context() - viewName := "" - var contextKeys []string - // nil context means global keybinding - if context != nil { - viewName = context.GetViewName() - contextKeys = []string{string(context.GetKey())} - } - - for _, binding := range controller.GetKeybindings(keybindingsOpts) { - binding.Contexts = contextKeys + mouseKeybindings := []*gocui.ViewMouseBinding{} + for _, c := range gui.allContexts() { + viewName := c.GetViewName() + contextKey := c.GetKey() + for _, binding := range c.GetKeybindings(keybindingsOpts) { + // TODO: move all mouse keybindings into the mouse keybindings approach below + if !gocui.IsMouseKey(binding.Key) && contextKey != context.GLOBAL_CONTEXT_KEY { + binding.Contexts = []string{string(contextKey)} + } binding.ViewName = viewName bindings = append(bindings, binding) } - } - for _, context := range gui.allContexts() { - viewName := context.GetViewName() - contextKey := context.GetKey() - for _, binding := range context.GetKeybindings(keybindingsOpts) { - binding.Contexts = []string{string(contextKey)} - binding.ViewName = viewName - bindings = append(bindings, binding) + for _, binding := range c.GetMouseKeybindings(keybindingsOpts) { + if contextKey != context.GLOBAL_CONTEXT_KEY { + binding.FromContext = string(contextKey) + } + mouseKeybindings = append(mouseKeybindings, binding) } } @@ -1438,7 +1413,7 @@ func (gui *Gui) GetInitialKeybindings() []*types.Binding { }...) } - return bindings + return bindings, mouseKeybindings } func (gui *Gui) resetKeybindings() error { @@ -1446,7 +1421,10 @@ func (gui *Gui) resetKeybindings() error { bindings := gui.GetCustomCommandKeybindings() - bindings = append(bindings, gui.GetInitialKeybindings()...) + bindings, mouseBindings := gui.GetInitialKeybindings() + + // prepending because we want to give our custom keybindings precedence over default keybindings + bindings = append(gui.GetCustomCommandKeybindings(), bindings...) for _, binding := range bindings { if err := gui.SetKeybinding(binding); err != nil { @@ -1454,6 +1432,12 @@ func (gui *Gui) resetKeybindings() error { } } + for _, binding := range mouseBindings { + if err := gui.SetMouseKeybinding(binding); err != nil { + return err + } + } + for viewName := range gui.State.Contexts.InitialViewTabContextMap() { viewName := viewName tabClickCallback := func(tabIndex int) error { return gui.onViewTabClick(viewName, tabIndex) } @@ -1474,7 +1458,8 @@ func (gui *Gui) wrappedHandler(f func() error) func(g *gocui.Gui, v *gocui.View) func (gui *Gui) SetKeybinding(binding *types.Binding) error { handler := binding.Handler - if isMouseKey(binding.Key) { + // TODO: move all mouse-ey stuff into new mouse approach + if gocui.IsMouseKey(binding.Key) { handler = func() error { // we ignore click events on views that aren't popup panels, when a popup panel is focused if gui.popupPanelFocused() && gui.currentViewName() != binding.ViewName { @@ -1488,19 +1473,18 @@ func (gui *Gui) SetKeybinding(binding *types.Binding) error { return gui.g.SetKeybinding(binding.ViewName, binding.Contexts, binding.Key, binding.Modifier, gui.wrappedHandler(handler)) } -func isMouseKey(key interface{}) bool { - switch key { - case - gocui.MouseLeft, - gocui.MouseRight, - gocui.MouseMiddle, - gocui.MouseRelease, - gocui.MouseWheelUp, - gocui.MouseWheelDown, - gocui.MouseWheelLeft, - gocui.MouseWheelRight: - return true - default: - return false +// warning: mutates the binding +func (gui *Gui) SetMouseKeybinding(binding *gocui.ViewMouseBinding) error { + baseHandler := binding.Handler + newHandler := func(opts gocui.ViewMouseBindingOpts) error { + // we ignore click events on views that aren't popup panels, when a popup panel is focused + if gui.popupPanelFocused() && gui.currentViewName() != binding.ViewName { + return nil + } + + return baseHandler(opts) } + binding.Handler = newHandler + + return gui.g.SetViewClickBinding(binding) } diff --git a/pkg/gui/layout.go b/pkg/gui/layout.go index 7a0d98384..7f8e9edef 100644 --- a/pkg/gui/layout.go +++ b/pkg/gui/layout.go @@ -2,7 +2,6 @@ package gui import ( "github.com/jesseduffield/gocui" - "github.com/jesseduffield/lazygit/pkg/gui/types" "github.com/jesseduffield/lazygit/pkg/theme" ) @@ -262,8 +261,7 @@ func (gui *Gui) layout(g *gocui.Gui) error { continue } - // ignore contexts whose view is owned by another context right now - if types.ContextKey(view.Context) != listContext.GetKey() { + if !gui.isContextVisible(listContext) { continue } @@ -300,8 +298,6 @@ func (gui *Gui) prepareView(viewName string) (*gocui.View, error) { } func (gui *Gui) onInitialViewsCreationForRepo() error { - gui.setInitialViewContexts() - // hide any popup views. This only applies when we've just switched repos for _, viewName := range gui.popupViewNames() { view, err := gui.g.View(viewName) diff --git a/pkg/gui/options_menu_panel.go b/pkg/gui/options_menu_panel.go index 8afcaeccb..b9df16722 100644 --- a/pkg/gui/options_menu_panel.go +++ b/pkg/gui/options_menu_panel.go @@ -3,28 +3,25 @@ package gui import ( "strings" - "github.com/jesseduffield/gocui" "github.com/jesseduffield/lazygit/pkg/gui/style" "github.com/jesseduffield/lazygit/pkg/gui/types" "github.com/jesseduffield/lazygit/pkg/utils" ) -func (gui *Gui) getBindings(v *gocui.View) []*types.Binding { +func (gui *Gui) getBindings(context types.Context) []*types.Binding { var ( bindingsGlobal, bindingsPanel []*types.Binding ) - bindings := append(gui.GetCustomCommandKeybindings(), gui.GetInitialKeybindings()...) + bindings, _ := gui.GetInitialKeybindings() + bindings = append(gui.GetCustomCommandKeybindings(), bindings...) for _, binding := range bindings { if GetKeyDisplay(binding.Key) != "" && binding.Description != "" { - switch binding.ViewName { - case "": + if len(binding.Contexts) == 0 { bindingsGlobal = append(bindingsGlobal, binding) - case v.Name(): - if len(binding.Contexts) == 0 || utils.IncludesString(binding.Contexts, v.Context) { - bindingsPanel = append(bindingsPanel, binding) - } + } else if utils.IncludesString(binding.Contexts, string(context.GetKey())) { + bindingsPanel = append(bindingsPanel, binding) } } } @@ -48,12 +45,8 @@ func opensMenuStyle(str string) string { } func (gui *Gui) handleCreateOptionsMenu() error { - view := gui.g.CurrentView() - if view == nil { - return nil - } - - bindings := gui.getBindings(view) + context := gui.currentContext() + bindings := gui.getBindings(context) menuItems := make([]*types.MenuItem, len(bindings)) diff --git a/pkg/gui/refresh.go b/pkg/gui/refresh.go index 3b1932c81..a19a685dc 100644 --- a/pkg/gui/refresh.go +++ b/pkg/gui/refresh.go @@ -318,7 +318,7 @@ func (gui *Gui) refreshFilesAndSubmodules() error { gui.c.Log.Error(err) } - if types.ContextKey(gui.Views.Files.Context) == context.FILES_CONTEXT_KEY { + if gui.isContextVisible(gui.State.Contexts.Files) { // doing this a little custom (as opposed to using gui.c.PostRefreshUpdate) because we handle selecting the file explicitly below if err := gui.State.Contexts.Files.HandleRender(); err != nil { return err @@ -503,7 +503,15 @@ func (gui *Gui) refreshRemotes() error { } } - return gui.c.PostRefreshUpdate(gui.mustContextForContextKey(types.ContextKey(gui.Views.Branches.Context))) + 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 { diff --git a/pkg/gui/types/context.go b/pkg/gui/types/context.go index e4a11779e..381374adf 100644 --- a/pkg/gui/types/context.go +++ b/pkg/gui/types/context.go @@ -1,6 +1,9 @@ package types -import "github.com/jesseduffield/lazygit/pkg/config" +import ( + "github.com/jesseduffield/gocui" + "github.com/jesseduffield/lazygit/pkg/config" +) type ContextKind int @@ -10,6 +13,8 @@ const ( TEMPORARY_POPUP PERSISTENT_POPUP EXTRAS_CONTEXT + // only used by the one global context + GLOBAL_CONTEXT ) type ParentContexter interface { @@ -31,6 +36,8 @@ type IBaseContext interface { GetKeybindings(opts KeybindingsOpts) []*Binding AddKeybindingsFn(KeybindingsFn) + GetMouseKeybindings(opts KeybindingsOpts) []*gocui.ViewMouseBinding + AddMouseKeybindingsFn(MouseKeybindingsFn) } type Context interface { @@ -56,9 +63,11 @@ type KeybindingsOpts struct { } type KeybindingsFn func(opts KeybindingsOpts) []*Binding +type MouseKeybindingsFn func(opts KeybindingsOpts) []*gocui.ViewMouseBinding type HasKeybindings interface { GetKeybindings(opts KeybindingsOpts) []*Binding + GetMouseKeybindings(opts KeybindingsOpts) []*gocui.ViewMouseBinding } type IController interface { diff --git a/pkg/gui/window.go b/pkg/gui/window.go index 3dccde7e7..4ea33292a 100644 --- a/pkg/gui/window.go +++ b/pkg/gui/window.go @@ -1,6 +1,8 @@ package gui -import "github.com/jesseduffield/gocui" +import ( + "github.com/jesseduffield/lazygit/pkg/gui/types" +) // 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 @@ -17,28 +19,21 @@ func (gui *Gui) getViewNameForWindow(window string) string { return viewName } -func (gui *Gui) getWindowForView(view *gocui.View) string { - if view == gui.Views.CommitFiles { - return gui.State.Contexts.CommitFiles.GetWindowName() - } - - return view.Name() -} - -func (gui *Gui) setViewAsActiveForWindow(view *gocui.View) { +// 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 gui.State.WindowViewNameMap == nil { gui.State.WindowViewNameMap = map[string]string{} } - gui.State.WindowViewNameMap[gui.getWindowForView(view)] = view.Name() + gui.State.WindowViewNameMap[c.GetWindowName()] = c.GetViewName() } func (gui *Gui) currentWindow() string { - return gui.getWindowForView(gui.g.CurrentView()) + return gui.currentContext().GetWindowName() } -func (gui *Gui) resetWindowForView(view *gocui.View) { - window := gui.getWindowForView(view) +func (gui *Gui) resetWindowContext(c types.Context) { // we assume here that the window contains as its default view a view with the same name as the window - gui.State.WindowViewNameMap[window] = window + windowName := c.GetWindowName() + gui.State.WindowViewNameMap[windowName] = windowName } diff --git a/vendor/github.com/jesseduffield/gocui/gui.go b/vendor/github.com/jesseduffield/gocui/gui.go index 1c3b4a9cb..b374c82c0 100644 --- a/vendor/github.com/jesseduffield/gocui/gui.go +++ b/vendor/github.com/jesseduffield/gocui/gui.go @@ -69,6 +69,30 @@ type tabClickBinding struct { handler tabClickHandler } +type ViewMouseBinding struct { + // the view that is clicked + ViewName string + + // the context we are in when the click occurs. Not necessarily the context + // of the view we're clicking. If this is blank then it is a global binding. + FromContext string + + Handler func(ViewMouseBindingOpts) error + + // must be a mouse key + Key Key +} + +type ViewMouseBindingOpts struct { + // cursor x/y + Cx int + Cy int + + // origin x/y + Ox int + Oy int +} + type GuiMutexes struct { // tickingMutex ensures we don't have two loops ticking. The point of 'ticking' // is to refresh the gui rapidly so that loader characters can be animated. @@ -110,17 +134,18 @@ type Gui struct { PlayMode PlayMode StartTime time.Time - tabClickBindings []*tabClickBinding - gEvents chan GocuiEvent - userEvents chan userEvent - views []*View - currentView *View - managers []Manager - keybindings []*keybinding - maxX, maxY int - outputMode OutputMode - stop chan struct{} - blacklist []Key + tabClickBindings []*tabClickBinding + viewMouseBindings []*ViewMouseBinding + gEvents chan GocuiEvent + userEvents chan userEvent + views []*View + currentView *View + managers []Manager + keybindings []*keybinding + maxX, maxY int + outputMode OutputMode + stop chan struct{} + blacklist []Key // BgColor and FgColor allow to configure the background and foreground // colors of the GUI. @@ -162,6 +187,8 @@ type Gui struct { screen tcell.Screen suspendedMutex sync.Mutex suspended bool + + currentContext string } // NewGui returns a new Gui object with a given output mode. @@ -237,6 +264,10 @@ func (g *Gui) Size() (x, y int) { return g.maxX, g.maxY } +func (g *Gui) SetCurrentContext(context string) { + g.currentContext = context +} + // SetRune writes a rune at the given point, relative to the top-left // corner of the terminal. It checks if the position is valid and applies // the given colors. @@ -472,6 +503,7 @@ func (g *Gui) DeleteKeybinding(viewname string, key interface{}, mod Modifier) e func (g *Gui) DeleteAllKeybindings() { g.keybindings = []*keybinding{} g.tabClickBindings = []*tabClickBinding{} + g.viewMouseBindings = []*ViewMouseBinding{} } // DeleteKeybindings deletes all keybindings of view. @@ -495,6 +527,12 @@ func (g *Gui) SetTabClickBinding(viewName string, handler tabClickHandler) error return nil } +func (g *Gui) SetViewClickBinding(binding *ViewMouseBinding) error { + g.viewMouseBindings = append(g.viewMouseBindings, binding) + + return nil +} + // BlackListKeybinding adds a keybinding to the blacklist func (g *Gui) BlacklistKeybinding(k Key) error { for _, j := range g.blacklist { @@ -1098,6 +1136,17 @@ func (g *Gui) onKey(ev *GocuiEvent) error { return err } + if ev.Mod == ModNone && IsMouseKey(ev.Key) { + opts := ViewMouseBindingOpts{Cx: newCx, Cy: newCy, Ox: v.ox, Oy: v.oy} + matched, err := g.execMouseKeybindings(v.Name(), ev, opts) + if err != nil { + return err + } + if matched { + return nil + } + } + if _, err := g.execKeybindings(v, ev); err != nil { return err } @@ -1106,6 +1155,40 @@ func (g *Gui) onKey(ev *GocuiEvent) error { return nil } +func (g *Gui) execMouseKeybindings(viewName string, ev *GocuiEvent, opts ViewMouseBindingOpts) (bool, error) { + // first pass looks for ones that match both the view and the current context + for _, binding := range g.viewMouseBindings { + if binding.ViewName == viewName && binding.FromContext == g.currentContext && ev.Key == binding.Key { + return true, binding.Handler(opts) + } + } + + for _, binding := range g.viewMouseBindings { + if binding.ViewName == viewName && ev.Key == binding.Key { + return true, binding.Handler(opts) + } + } + + return false, nil +} + +func IsMouseKey(key interface{}) bool { + switch key { + case + MouseLeft, + MouseRight, + MouseMiddle, + MouseRelease, + MouseWheelUp, + MouseWheelDown, + MouseWheelLeft, + MouseWheelRight: + return true + default: + return false + } +} + // execKeybindings executes the keybinding handlers that match the passed view // and event. The value of matched is true if there is a match and no errors. func (g *Gui) execKeybindings(v *View, ev *GocuiEvent) (matched bool, err error) { @@ -1136,10 +1219,10 @@ func (g *Gui) execKeybindings(v *View, ev *GocuiEvent) (matched bool, err error) if !kb.matchKeypress(Key(ev.Key), ev.Ch, Modifier(ev.Mod)) { continue } - if kb.matchView(v) { + if g.matchView(v, kb) { return g.execKeybinding(v, kb) } - if v != nil && kb.matchView(v.ParentView) { + if v != nil && g.matchView(v.ParentView, kb) { matchingParentViewKb = kb } if globalKb == nil && kb.viewName == "" && ((v != nil && !v.Editable) || (kb.ch == 0 && kb.key != KeyCtrlU && kb.key != KeyCtrlA && kb.key != KeyCtrlE)) { @@ -1340,3 +1423,27 @@ func (g *Gui) Resume() error { return g.screen.Resume() } + +// matchView returns if the keybinding matches the current view (and the view's context) +func (g *Gui) matchView(v *View, kb *keybinding) bool { + // if the user is typing in a field, ignore char keys + if v == nil { + return false + } + if v.Editable == true && kb.ch != 0 { + return false + } + if kb.viewName != v.name { + return false + } + // if the keybinding doesn't specify contexts, it applies for all contexts + if len(kb.contexts) == 0 { + return true + } + for _, context := range kb.contexts { + if context == g.currentContext { + return true + } + } + return false +} diff --git a/vendor/github.com/jesseduffield/gocui/keybinding.go b/vendor/github.com/jesseduffield/gocui/keybinding.go index 95857656e..7a675d55b 100644 --- a/vendor/github.com/jesseduffield/gocui/keybinding.go +++ b/vendor/github.com/jesseduffield/gocui/keybinding.go @@ -124,30 +124,6 @@ func (kb *keybinding) matchKeypress(key Key, ch rune, mod Modifier) bool { return kb.key == key && kb.ch == ch && kb.mod == mod } -// matchView returns if the keybinding matches the current view (and the view's context) -func (kb *keybinding) matchView(v *View) bool { - // if the user is typing in a field, ignore char keys - if v == nil { - return false - } - if v.Editable == true && kb.ch != 0 { - return false - } - if kb.viewName != v.name { - return false - } - // if the keybinding doesn't specify contexts, it applies for all contexts - if len(kb.contexts) == 0 { - return true - } - for _, context := range kb.contexts { - if context == v.Context { - return true - } - } - return false -} - // translations for strings to keys var translate = map[string]Key{ "F1": KeyF1, diff --git a/vendor/github.com/jesseduffield/gocui/view.go b/vendor/github.com/jesseduffield/gocui/view.go index 1316ced2e..9783d7637 100644 --- a/vendor/github.com/jesseduffield/gocui/view.go +++ b/vendor/github.com/jesseduffield/gocui/view.go @@ -149,8 +149,6 @@ type View struct { // ParentView is the view which catches events bubbled up from the given view if there's no matching handler ParentView *View - Context string // this is for assigning keybindings to a view only in certain contexts - searcher *searcher // KeybindOnEdit should be set to true when you want to execute keybindings even when the view is editable