1
0
mirror of https://github.com/jesseduffield/lazygit.git synced 2024-12-14 11:23:09 +02:00
lazygit/pkg/gui/context.go

405 lines
12 KiB
Go
Raw Normal View History

2019-02-16 12:01:17 +02:00
package gui
2020-08-16 02:05:45 +02:00
import (
2022-02-13 01:48:41 +02:00
"sort"
"strings"
2022-03-19 07:34:46 +02:00
"github.com/jesseduffield/generics/maps"
2022-03-19 10:12:58 +02:00
"github.com/jesseduffield/generics/slices"
2020-08-16 05:58:29 +02:00
"github.com/jesseduffield/gocui"
2022-01-29 10:15:46 +02:00
"github.com/jesseduffield/lazygit/pkg/gui/context"
"github.com/jesseduffield/lazygit/pkg/gui/types"
2020-08-16 02:05:45 +02:00
)
2022-05-07 07:42:36 +02:00
// 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.
func (gui *Gui) popupViewNames() []string {
2022-03-20 00:24:39 +02:00
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()
})
}
// use replaceContext when you don't want to return to the original context upon
// hitting escape: you want to go that context's parent instead.
func (gui *Gui) replaceContext(c types.Context) error {
2022-02-05 07:56:36 +02:00
if !c.IsFocusable() {
return nil
}
2022-02-13 03:47:15 +02:00
gui.State.ContextManager.Lock()
2022-01-15 03:04:00 +02:00
if len(gui.State.ContextManager.ContextStack) == 0 {
gui.State.ContextManager.ContextStack = []types.Context{c}
2022-01-15 03:04:00 +02:00
} else {
// replace the last item with the given item
gui.State.ContextManager.ContextStack = append(gui.State.ContextManager.ContextStack[0:len(gui.State.ContextManager.ContextStack)-1], c)
}
2022-02-13 03:47:15 +02:00
defer gui.State.ContextManager.Unlock()
return gui.activateContext(c, types.OnFocusOpts{})
}
func (gui *Gui) pushContext(c types.Context, opts types.OnFocusOpts) error {
2022-02-05 07:56:36 +02:00
if !c.IsFocusable() {
return nil
2022-02-05 05:42:56 +02:00
}
contextsToDeactivate, contextToActivate := gui.pushToContextStack(c)
for _, contextToDeactivate := range contextsToDeactivate {
if err := gui.deactivateContext(contextToDeactivate, types.OnFocusLostOpts{NewContextKey: c.GetKey()}); err != nil {
return err
}
}
if contextToActivate == nil {
return nil
}
return gui.activateContext(contextToActivate, opts)
}
// Adjusts the context stack based on the context that's being pushed and
// returns (contexts to deactivate, context to activate)
func (gui *Gui) pushToContextStack(c types.Context) ([]types.Context, types.Context) {
contextsToDeactivate := []types.Context{}
gui.State.ContextManager.Lock()
defer gui.State.ContextManager.Unlock()
if len(gui.State.ContextManager.ContextStack) > 0 &&
c == gui.State.ContextManager.ContextStack[len(gui.State.ContextManager.ContextStack)-1] {
// Context being pushed is already on top of the stack: nothing to
// deactivate or activate
return contextsToDeactivate, nil
}
if len(gui.State.ContextManager.ContextStack) == 0 {
gui.State.ContextManager.ContextStack = append(gui.State.ContextManager.ContextStack, c)
} else if c.GetKind() == types.SIDE_CONTEXT {
// if we are switching to a side context, remove all other contexts in the stack
contextsToDeactivate = gui.State.ContextManager.ContextStack
gui.State.ContextManager.ContextStack = []types.Context{c}
} else if c.GetKind() == types.MAIN_CONTEXT {
// if we're switching to a main context, remove all other main contexts in the stack
contextsToKeep := []types.Context{}
for _, stackContext := range gui.State.ContextManager.ContextStack {
if stackContext.GetKind() == types.MAIN_CONTEXT {
contextsToDeactivate = append(contextsToDeactivate, stackContext)
} else {
contextsToKeep = append(contextsToKeep, stackContext)
}
}
gui.State.ContextManager.ContextStack = append(contextsToKeep, c)
} else {
topContext := gui.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.
2022-08-02 01:16:01 +02:00
// 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)
_, gui.State.ContextManager.ContextStack = slices.Pop(gui.State.ContextManager.ContextStack)
}
gui.State.ContextManager.ContextStack = append(gui.State.ContextManager.ContextStack, c)
}
}
return contextsToDeactivate, c
2020-08-16 05:58:29 +02:00
}
func (gui *Gui) popContext() error {
2021-09-25 05:21:28 +02:00
gui.State.ContextManager.Lock()
2020-08-16 10:17:16 +02:00
2021-09-25 05:21:28 +02:00
if len(gui.State.ContextManager.ContextStack) == 1 {
// cannot escape from bottommost context
gui.State.ContextManager.Unlock()
return nil
}
2020-08-16 10:17:16 +02:00
var currentContext types.Context
currentContext, gui.State.ContextManager.ContextStack = slices.Pop(gui.State.ContextManager.ContextStack)
2020-08-16 10:17:16 +02:00
newContext := gui.State.ContextManager.ContextStack[len(gui.State.ContextManager.ContextStack)-1]
2021-09-25 05:21:28 +02:00
gui.State.ContextManager.Unlock()
2020-08-16 10:17:16 +02:00
if err := gui.deactivateContext(currentContext, types.OnFocusLostOpts{NewContextKey: newContext.GetKey()}); err != nil {
2021-09-25 05:21:28 +02:00
return err
}
return gui.activateContext(newContext, types.OnFocusOpts{})
2020-08-16 05:58:29 +02:00
}
func (gui *Gui) deactivateContext(c types.Context, opts types.OnFocusLostOpts) error {
2021-04-06 02:26:18 +02:00
view, _ := gui.g.View(c.GetViewName())
if view != nil && view.IsSearching() {
if err := gui.onSearchEscape(); err != nil {
return err
2021-04-04 15:51:59 +02:00
}
2020-08-17 13:58:30 +02:00
}
2021-04-06 02:26:18 +02:00
// 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) {
2021-04-06 02:26:18 +02:00
view.Visible = false
}
if err := c.HandleFocusLost(opts); err != nil {
2020-08-17 13:58:30 +02:00
return err
}
return nil
}
2020-08-19 11:07:14 +02:00
// 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 {
2020-08-19 11:07:14 +02:00
if err := c.HandleRender(); err != nil {
return err
}
if gui.currentViewName() == c.GetViewName() {
if err := c.HandleFocus(types.OnFocusOpts{}); err != nil {
2020-08-19 10:41:57 +02:00
return err
}
}
return nil
}
func (gui *Gui) activateContext(c types.Context, opts types.OnFocusOpts) error {
2020-08-16 10:17:16 +02:00
viewName := c.GetViewName()
2020-08-19 10:41:57 +02:00
v, err := gui.g.View(viewName)
2020-08-16 10:17:16 +02:00
if err != nil {
return err
2020-08-16 10:17:16 +02:00
}
2022-02-05 05:42:56 +02:00
gui.setWindowContext(c)
2020-08-19 11:07:14 +02:00
gui.moveToTopOfWindow(c)
2020-08-16 10:17:16 +02:00
if _, err := gui.g.SetCurrentView(viewName); err != nil {
return err
2020-08-16 05:58:29 +02:00
}
desiredTitle := c.Title()
if desiredTitle != "" {
v.Title = desiredTitle
}
2021-04-04 15:51:59 +02:00
v.Visible = true
2020-08-16 05:58:29 +02:00
2020-08-23 03:08:51 +02:00
gui.g.Cursor = v.Editable
2020-08-16 05:58:29 +02:00
// render the options available for the current context at the bottom of the screen
optionsMap := c.GetOptionsMap()
if optionsMap == nil {
optionsMap = gui.globalOptionsMap()
2020-08-16 05:58:29 +02:00
}
gui.renderOptionsMap(optionsMap)
2020-08-16 05:58:29 +02:00
if err := c.HandleFocus(opts); err != nil {
2020-08-16 05:58:29 +02:00
return err
}
return nil
}
2022-02-13 01:48:41 +02:00
func (gui *Gui) optionsMapToString(optionsMap map[string]string) string {
2022-03-19 12:01:10 +02:00
options := maps.MapToSlice(optionsMap, func(key string, description string) string {
2022-03-19 07:34:46 +02:00
return key + ": " + description
})
2022-03-19 12:01:10 +02:00
sort.Strings(options)
return strings.Join(options, ", ")
2022-02-13 01:48:41 +02:00
}
func (gui *Gui) renderOptionsMap(optionsMap map[string]string) {
_ = gui.renderString(gui.Views.Options, gui.optionsMapToString(optionsMap))
}
2021-04-05 04:45:27 +02:00
// // currently unused
// func (gui *Gui) renderContextStack() string {
// result := ""
// for _, context := range gui.State.ContextManager.ContextStack {
// result += string(context.GetKey()) + "\n"
// }
// return result
// }
2020-08-16 05:58:29 +02:00
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]
2020-08-16 02:05:45 +02:00
}
2021-04-04 15:51:59 +02:00
// the status panel is not yet a list context (and may never be), so this method is not
// quite the same as currentSideContext()
func (gui *Gui) currentSideListContext() types.IListContext {
2021-04-04 15:51:59 +02:00
context := gui.currentSideContext()
listContext, ok := context.(types.IListContext)
2021-04-04 15:51:59 +02:00
if !ok {
return nil
}
return listContext
}
func (gui *Gui) currentSideContext() types.Context {
gui.State.ContextManager.RLock()
defer gui.State.ContextManager.RUnlock()
stack := gui.State.ContextManager.ContextStack
2020-08-22 00:49:02 +02:00
// on startup the stack can be empty so we'll return an empty string in that case
if len(stack) == 0 {
2021-04-04 15:51:59 +02:00
return gui.defaultSideContext()
2020-08-22 00:49:02 +02:00
}
// find the first context in the stack with the type of types.SIDE_CONTEXT
2020-08-22 00:49:02 +02:00
for i := range stack {
context := stack[len(stack)-1-i]
if context.GetKind() == types.SIDE_CONTEXT {
2021-04-04 15:51:59 +02:00
return context
2020-08-22 00:49:02 +02:00
}
}
2021-04-04 15:51:59 +02:00
return gui.defaultSideContext()
2020-08-22 00:49:02 +02:00
}
2021-04-11 07:01:49 +02:00
// static as opposed to popup
func (gui *Gui) currentStaticContext() types.Context {
2021-04-11 07:01:49 +02:00
gui.State.ContextManager.RLock()
defer gui.State.ContextManager.RUnlock()
return gui.currentStaticContextWithoutLock()
}
func (gui *Gui) currentStaticContextWithoutLock() types.Context {
2021-04-11 07:01:49 +02:00
stack := gui.State.ContextManager.ContextStack
if len(stack) == 0 {
return 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 {
2021-04-11 07:01:49 +02:00
return context
}
}
return gui.defaultSideContext()
}
func (gui *Gui) defaultSideContext() types.Context {
2021-04-04 15:51:59 +02:00
if gui.State.Modes.Filtering.Active() {
2022-02-13 08:01:53 +02:00
return gui.State.Contexts.LocalCommits
2021-04-04 15:51:59 +02:00
} else {
return gui.State.Contexts.Files
}
2020-08-22 00:49:02 +02:00
}
2020-08-16 05:58:29 +02:00
// 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()) {
2022-07-30 08:10:29 +02:00
if err := gui.onViewFocusLost(previousView); err != nil {
2020-08-16 05:58:29 +02:00
return err
}
previousView = newView
}
return nil
}
}
2020-08-16 02:05:45 +02:00
2022-07-30 08:10:29 +02:00
func (gui *Gui) onViewFocusLost(oldView *gocui.View) error {
if oldView == nil {
2020-08-16 05:58:29 +02:00
return nil
}
oldView.Highlight = false
2021-11-02 11:35:53 +02:00
_ = oldView.SetOriginX(0)
2020-08-16 05:58:29 +02:00
return nil
}
2022-03-26 05:44:30 +02:00
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()
2020-08-19 10:41:57 +02:00
}
func (gui *Gui) getSideContextSelectedItemId() string {
2021-04-04 15:51:59 +02:00
currentSideContext := gui.currentSideListContext()
if currentSideContext == nil {
return ""
}
2022-01-30 11:03:08 +02:00
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
// }