mirror of
https://github.com/jesseduffield/lazygit.git
synced 2025-02-13 13:59:06 +02:00
As part of making lazygit more discoverable, there are certain keys which you almost certainly need to press when you're in a given mode e.g. 'v' to paste commits when cherry-picking. This commit prominently shows these keybinding suggestions alongside the others in the option view. I'm using the same colours for these keybindings as is associated with the mode elsewhere e.g. yellow for rebasing and cyan for cherry-picking. The cherry-picking one is a bit weird because we also use cyan text to show loaders and app status at the bottom left so it may be confusing, but I haven't personally found it awkward from having tested it out myself. Previously we would render these options whenever a new context was activated, but now that we need to re-render options whenever a mode changes, I'm instead rendering them on each screen re-render (i.e. in the layout function). Given how cheap it is to render this text, I think it's fine performance-wise.
390 lines
10 KiB
Go
390 lines
10 KiB
Go
package gui
|
|
|
|
import (
|
|
"errors"
|
|
"sync"
|
|
|
|
"github.com/jesseduffield/lazygit/pkg/gui/context"
|
|
"github.com/jesseduffield/lazygit/pkg/gui/types"
|
|
"github.com/jesseduffield/lazygit/pkg/utils"
|
|
"github.com/samber/lo"
|
|
)
|
|
|
|
// This file is for the management of contexts. There is a context stack such that
|
|
// for example you might start off in the commits context and then open a menu, putting
|
|
// you in the menu context. When contexts are activated/deactivated certain things need
|
|
// to happen like showing/hiding views and rendering content.
|
|
|
|
type ContextMgr struct {
|
|
ContextStack []types.Context
|
|
sync.RWMutex
|
|
gui *Gui
|
|
|
|
allContexts *context.ContextTree
|
|
}
|
|
|
|
func NewContextMgr(
|
|
gui *Gui,
|
|
allContexts *context.ContextTree,
|
|
) *ContextMgr {
|
|
return &ContextMgr{
|
|
ContextStack: []types.Context{},
|
|
RWMutex: sync.RWMutex{},
|
|
gui: gui,
|
|
allContexts: allContexts,
|
|
}
|
|
}
|
|
|
|
// use when you don't want to return to the original context upon
|
|
// hitting escape: you want to go that context's parent instead.
|
|
func (self *ContextMgr) Replace(c types.Context) error {
|
|
if !c.IsFocusable() {
|
|
return nil
|
|
}
|
|
|
|
self.Lock()
|
|
|
|
if len(self.ContextStack) == 0 {
|
|
self.ContextStack = []types.Context{c}
|
|
} else {
|
|
// replace the last item with the given item
|
|
self.ContextStack = append(self.ContextStack[0:len(self.ContextStack)-1], c)
|
|
}
|
|
|
|
defer self.Unlock()
|
|
|
|
return self.ActivateContext(c, types.OnFocusOpts{})
|
|
}
|
|
|
|
func (self *ContextMgr) Push(c types.Context, opts ...types.OnFocusOpts) error {
|
|
if len(opts) > 1 {
|
|
return errors.New("cannot pass multiple opts to Push")
|
|
}
|
|
|
|
singleOpts := types.OnFocusOpts{}
|
|
if len(opts) > 0 {
|
|
// using triple dot but you should only ever pass one of these opt structs
|
|
singleOpts = opts[0]
|
|
}
|
|
|
|
if !c.IsFocusable() {
|
|
return nil
|
|
}
|
|
|
|
contextsToDeactivate, contextToActivate := self.pushToContextStack(c)
|
|
|
|
for _, contextToDeactivate := range contextsToDeactivate {
|
|
if err := self.deactivateContext(contextToDeactivate, types.OnFocusLostOpts{NewContextKey: c.GetKey()}); err != nil {
|
|
return err
|
|
}
|
|
}
|
|
|
|
if contextToActivate == nil {
|
|
return nil
|
|
}
|
|
|
|
return self.ActivateContext(contextToActivate, singleOpts)
|
|
}
|
|
|
|
// Adjusts the context stack based on the context that's being pushed and
|
|
// returns (contexts to deactivate, context to activate)
|
|
func (self *ContextMgr) pushToContextStack(c types.Context) ([]types.Context, types.Context) {
|
|
contextsToDeactivate := []types.Context{}
|
|
|
|
self.Lock()
|
|
defer self.Unlock()
|
|
|
|
if len(self.ContextStack) > 0 &&
|
|
c.GetKey() == self.ContextStack[len(self.ContextStack)-1].GetKey() {
|
|
// Context being pushed is already on top of the stack: nothing to
|
|
// deactivate or activate
|
|
return contextsToDeactivate, nil
|
|
}
|
|
|
|
if len(self.ContextStack) == 0 {
|
|
self.ContextStack = append(self.ContextStack, c)
|
|
} else if c.GetKind() == types.SIDE_CONTEXT {
|
|
// if we are switching to a side context, remove all other contexts in the stack
|
|
contextsToDeactivate = lo.Filter(self.ContextStack, func(context types.Context, _ int) bool {
|
|
return context.GetKey() != c.GetKey()
|
|
})
|
|
self.ContextStack = []types.Context{c}
|
|
} else if c.GetKind() == types.MAIN_CONTEXT {
|
|
// if we're switching to a main context, remove all other main contexts in the stack
|
|
contextsToKeep := []types.Context{}
|
|
for _, stackContext := range self.ContextStack {
|
|
if stackContext.GetKind() == types.MAIN_CONTEXT {
|
|
contextsToDeactivate = append(contextsToDeactivate, stackContext)
|
|
} else {
|
|
contextsToKeep = append(contextsToKeep, stackContext)
|
|
}
|
|
}
|
|
self.ContextStack = append(contextsToKeep, c)
|
|
} else {
|
|
topContext := self.currentContextWithoutLock()
|
|
|
|
// if we're pushing the same context on, we do nothing.
|
|
if topContext.GetKey() != c.GetKey() {
|
|
// if top one is a temporary popup, we remove it. Ideally you'd be able to
|
|
// escape back to previous temporary popups, but because we're currently reusing
|
|
// views for this, you might not be able to get back to where you previously were.
|
|
// The exception is when going to the search context e.g. for searching a menu.
|
|
if (topContext.GetKind() == types.TEMPORARY_POPUP && c.GetKey() != context.SEARCH_CONTEXT_KEY) ||
|
|
// we only ever want one main context on the stack at a time.
|
|
(topContext.GetKind() == types.MAIN_CONTEXT && c.GetKind() == types.MAIN_CONTEXT) {
|
|
|
|
contextsToDeactivate = append(contextsToDeactivate, topContext)
|
|
_, self.ContextStack = utils.Pop(self.ContextStack)
|
|
}
|
|
|
|
self.ContextStack = append(self.ContextStack, c)
|
|
}
|
|
}
|
|
|
|
return contextsToDeactivate, c
|
|
}
|
|
|
|
func (self *ContextMgr) Pop() error {
|
|
self.Lock()
|
|
|
|
if len(self.ContextStack) == 1 {
|
|
// cannot escape from bottommost context
|
|
self.Unlock()
|
|
return nil
|
|
}
|
|
|
|
var currentContext types.Context
|
|
currentContext, self.ContextStack = utils.Pop(self.ContextStack)
|
|
|
|
newContext := self.ContextStack[len(self.ContextStack)-1]
|
|
|
|
self.Unlock()
|
|
|
|
if err := self.deactivateContext(currentContext, types.OnFocusLostOpts{NewContextKey: newContext.GetKey()}); err != nil {
|
|
return err
|
|
}
|
|
|
|
return self.ActivateContext(newContext, types.OnFocusOpts{})
|
|
}
|
|
|
|
func (self *ContextMgr) RemoveContexts(contextsToRemove []types.Context) error {
|
|
self.Lock()
|
|
|
|
if len(self.ContextStack) == 1 {
|
|
self.Unlock()
|
|
return nil
|
|
}
|
|
|
|
rest := lo.Filter(self.ContextStack, func(context types.Context, _ int) bool {
|
|
for _, contextToRemove := range contextsToRemove {
|
|
if context.GetKey() == contextToRemove.GetKey() {
|
|
return false
|
|
}
|
|
}
|
|
return true
|
|
})
|
|
self.ContextStack = rest
|
|
contextToActivate := rest[len(rest)-1]
|
|
self.Unlock()
|
|
|
|
for _, context := range contextsToRemove {
|
|
if err := self.deactivateContext(context, types.OnFocusLostOpts{NewContextKey: contextToActivate.GetKey()}); err != nil {
|
|
return err
|
|
}
|
|
}
|
|
|
|
// activate the item at the top of the stack
|
|
return self.ActivateContext(contextToActivate, types.OnFocusOpts{})
|
|
}
|
|
|
|
func (self *ContextMgr) deactivateContext(c types.Context, opts types.OnFocusLostOpts) error {
|
|
view, _ := self.gui.c.GocuiGui().View(c.GetViewName())
|
|
|
|
if opts.NewContextKey != context.SEARCH_CONTEXT_KEY {
|
|
if c.GetKind() == types.MAIN_CONTEXT || c.GetKind() == types.TEMPORARY_POPUP {
|
|
self.gui.helpers.Search.CancelSearchIfSearching(c)
|
|
}
|
|
}
|
|
|
|
// if we are the kind of context that is sent to back upon deactivation, we should do that
|
|
if view != nil &&
|
|
(c.GetKind() == types.TEMPORARY_POPUP ||
|
|
c.GetKind() == types.PERSISTENT_POPUP) {
|
|
view.Visible = false
|
|
}
|
|
|
|
if err := c.HandleFocusLost(opts); err != nil {
|
|
return err
|
|
}
|
|
|
|
return nil
|
|
}
|
|
|
|
func (self *ContextMgr) ActivateContext(c types.Context, opts types.OnFocusOpts) error {
|
|
viewName := c.GetViewName()
|
|
v, err := self.gui.c.GocuiGui().View(viewName)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
self.gui.helpers.Window.SetWindowContext(c)
|
|
|
|
self.gui.helpers.Window.MoveToTopOfWindow(c)
|
|
if _, err := self.gui.c.GocuiGui().SetCurrentView(viewName); err != nil {
|
|
return err
|
|
}
|
|
|
|
self.gui.helpers.Search.RenderSearchStatus(c)
|
|
|
|
desiredTitle := c.Title()
|
|
if desiredTitle != "" {
|
|
v.Title = desiredTitle
|
|
}
|
|
|
|
v.Visible = true
|
|
|
|
self.gui.c.GocuiGui().Cursor = v.Editable
|
|
|
|
if err := c.HandleFocus(opts); err != nil {
|
|
return err
|
|
}
|
|
|
|
return nil
|
|
}
|
|
|
|
func (self *ContextMgr) Current() types.Context {
|
|
self.RLock()
|
|
defer self.RUnlock()
|
|
|
|
return self.currentContextWithoutLock()
|
|
}
|
|
|
|
func (self *ContextMgr) currentContextWithoutLock() types.Context {
|
|
if len(self.ContextStack) == 0 {
|
|
return self.gui.defaultSideContext()
|
|
}
|
|
|
|
return self.ContextStack[len(self.ContextStack)-1]
|
|
}
|
|
|
|
// Note that this could return the 'status' context which is not itself a list context.
|
|
func (self *ContextMgr) CurrentSide() types.Context {
|
|
self.RLock()
|
|
defer self.RUnlock()
|
|
|
|
stack := self.ContextStack
|
|
|
|
// find the first context in the stack with the type of types.SIDE_CONTEXT
|
|
for i := range stack {
|
|
context := stack[len(stack)-1-i]
|
|
|
|
if context.GetKind() == types.SIDE_CONTEXT {
|
|
return context
|
|
}
|
|
}
|
|
|
|
return self.gui.defaultSideContext()
|
|
}
|
|
|
|
// static as opposed to popup
|
|
func (self *ContextMgr) CurrentStatic() types.Context {
|
|
self.RLock()
|
|
defer self.RUnlock()
|
|
|
|
return self.currentStaticContextWithoutLock()
|
|
}
|
|
|
|
func (self *ContextMgr) currentStaticContextWithoutLock() types.Context {
|
|
stack := self.ContextStack
|
|
|
|
if len(stack) == 0 {
|
|
return self.gui.defaultSideContext()
|
|
}
|
|
|
|
// find the first context in the stack without a popup type
|
|
for i := range stack {
|
|
context := stack[len(stack)-1-i]
|
|
|
|
if context.GetKind() != types.TEMPORARY_POPUP && context.GetKind() != types.PERSISTENT_POPUP {
|
|
return context
|
|
}
|
|
}
|
|
|
|
return self.gui.defaultSideContext()
|
|
}
|
|
|
|
func (self *ContextMgr) ForEach(f func(types.Context)) {
|
|
self.RLock()
|
|
defer self.RUnlock()
|
|
|
|
for _, context := range self.gui.State.ContextMgr.ContextStack {
|
|
f(context)
|
|
}
|
|
}
|
|
|
|
func (self *ContextMgr) IsCurrent(c types.Context) bool {
|
|
return self.Current().GetKey() == c.GetKey()
|
|
}
|
|
|
|
func (self *ContextMgr) AllFilterable() []types.IFilterableContext {
|
|
var result []types.IFilterableContext
|
|
|
|
for _, context := range self.allContexts.Flatten() {
|
|
if ctx, ok := context.(types.IFilterableContext); ok {
|
|
result = append(result, ctx)
|
|
}
|
|
}
|
|
|
|
return result
|
|
}
|
|
|
|
func (self *ContextMgr) AllSearchable() []types.ISearchableContext {
|
|
var result []types.ISearchableContext
|
|
|
|
for _, context := range self.allContexts.Flatten() {
|
|
if ctx, ok := context.(types.ISearchableContext); ok {
|
|
result = append(result, ctx)
|
|
}
|
|
}
|
|
|
|
return result
|
|
}
|
|
|
|
// all list contexts
|
|
func (self *ContextMgr) AllList() []types.IListContext {
|
|
var listContexts []types.IListContext
|
|
|
|
for _, context := range self.allContexts.Flatten() {
|
|
if listContext, ok := context.(types.IListContext); ok {
|
|
listContexts = append(listContexts, listContext)
|
|
}
|
|
}
|
|
|
|
return listContexts
|
|
}
|
|
|
|
func (self *ContextMgr) AllPatchExplorer() []types.IPatchExplorerContext {
|
|
var listContexts []types.IPatchExplorerContext
|
|
|
|
for _, context := range self.allContexts.Flatten() {
|
|
if listContext, ok := context.(types.IPatchExplorerContext); ok {
|
|
listContexts = append(listContexts, listContext)
|
|
}
|
|
}
|
|
|
|
return listContexts
|
|
}
|
|
|
|
func (self *ContextMgr) ContextForKey(key types.ContextKey) types.Context {
|
|
self.RLock()
|
|
defer self.RUnlock()
|
|
|
|
for _, context := range self.allContexts.Flatten() {
|
|
if context.GetKey() == key {
|
|
return context
|
|
}
|
|
}
|
|
|
|
return nil
|
|
}
|