summaryrefslogtreecommitdiffstats
path: root/pkg/gui/context
diff options
context:
space:
mode:
authorJesse Duffield <jessedduffield@gmail.com>2022-01-29 19:09:20 +1100
committerJesse Duffield <jessedduffield@gmail.com>2022-03-17 19:13:40 +1100
commit138be04e6575f2bd087630e49d122af578c78bf6 (patch)
treeed038641d6871e49ff426096f300e6b3b3ab4f1e /pkg/gui/context
parent1a74ed32143f826104e1d60f4392d2d8ba53cd80 (diff)
refactor contexts code
Diffstat (limited to 'pkg/gui/context')
-rw-r--r--pkg/gui/context/base_context.go67
-rw-r--r--pkg/gui/context/context.go2
-rw-r--r--pkg/gui/context/list_context_trait.go231
-rw-r--r--pkg/gui/context/list_trait.go32
-rw-r--r--pkg/gui/context/parent_context_mgr.go20
-rw-r--r--pkg/gui/context/tags_context.go107
-rw-r--r--pkg/gui/context/view_trait.go80
7 files changed, 538 insertions, 1 deletions
diff --git a/pkg/gui/context/base_context.go b/pkg/gui/context/base_context.go
new file mode 100644
index 000000000..e95b298a4
--- /dev/null
+++ b/pkg/gui/context/base_context.go
@@ -0,0 +1,67 @@
+package context
+
+import "github.com/jesseduffield/lazygit/pkg/gui/types"
+
+type BaseContext struct {
+ Kind types.ContextKind
+ Key types.ContextKey
+ ViewName string
+ WindowName string
+ OnGetOptionsMap func() map[string]string
+
+ *ParentContextMgr
+}
+
+func (self *BaseContext) GetOptionsMap() map[string]string {
+ if self.OnGetOptionsMap != nil {
+ return self.OnGetOptionsMap()
+ }
+ return nil
+}
+
+func (self *BaseContext) SetWindowName(windowName string) {
+ self.WindowName = windowName
+}
+
+func (self *BaseContext) GetWindowName() string {
+ windowName := self.WindowName
+
+ if windowName != "" {
+ return windowName
+ }
+
+ // TODO: actually set this for everything so we don't default to the view name
+ return self.ViewName
+}
+
+func (self *BaseContext) GetViewName() string {
+ return self.ViewName
+}
+
+func (self *BaseContext) GetKind() types.ContextKind {
+ return self.Kind
+}
+
+func (self *BaseContext) GetKey() types.ContextKey {
+ return self.Key
+}
+
+type NewBaseContextOpts struct {
+ Kind types.ContextKind
+ Key types.ContextKey
+ ViewName string
+ WindowName string
+
+ OnGetOptionsMap func() map[string]string
+}
+
+func NewBaseContext(opts NewBaseContextOpts) *BaseContext {
+ return &BaseContext{
+ Kind: opts.Kind,
+ Key: opts.Key,
+ ViewName: opts.ViewName,
+ WindowName: opts.WindowName,
+ OnGetOptionsMap: opts.OnGetOptionsMap,
+ ParentContextMgr: &ParentContextMgr{},
+ }
+}
diff --git a/pkg/gui/context/context.go b/pkg/gui/context/context.go
index 67de237ed..30030990f 100644
--- a/pkg/gui/context/context.go
+++ b/pkg/gui/context/context.go
@@ -10,7 +10,7 @@ type ContextTree struct {
Branches types.IListContext
Remotes types.IListContext
RemoteBranches types.IListContext
- Tags types.IListContext
+ Tags *TagsContext
BranchCommits types.IListContext
CommitFiles types.IListContext
ReflogCommits types.IListContext
diff --git a/pkg/gui/context/list_context_trait.go b/pkg/gui/context/list_context_trait.go
new file mode 100644
index 000000000..776056fad
--- /dev/null
+++ b/pkg/gui/context/list_context_trait.go
@@ -0,0 +1,231 @@
+package context
+
+import (
+ "fmt"
+
+ "github.com/jesseduffield/gocui"
+ "github.com/jesseduffield/lazygit/pkg/config"
+ "github.com/jesseduffield/lazygit/pkg/gui/types"
+ "github.com/jesseduffield/lazygit/pkg/utils"
+)
+
+type Thing interface {
+ // the boolean here tells us whether the item is nil. This is needed because you can't work it out on the calling end once the pointer is wrapped in an interface (unless you want to use reflection)
+ GetSelectedItem() (types.ListItem, bool)
+}
+
+type ListContextTrait struct {
+ base types.IBaseContext
+ thing Thing
+ listTrait *ListTrait
+ viewTrait *ViewTrait
+
+ takeFocus func() error
+
+ GetDisplayStrings func(startIdx int, length int) [][]string
+ OnFocus func(...types.OnFocusOpts) error
+ OnRenderToMain func(...types.OnFocusOpts) error
+ OnFocusLost func() error
+
+ // if this is true, we'll call GetDisplayStrings for just the visible part of the
+ // view and re-render that. This is useful when you need to render different
+ // content based on the selection (e.g. for showing the selected commit)
+ RenderSelection bool
+
+ c *types.ControllerCommon
+}
+
+func (self *ListContextTrait) GetPanelState() types.IListPanelState {
+ return self.listTrait
+}
+
+func (self *ListContextTrait) FocusLine() {
+ // we need a way of knowing whether we've rendered to the view yet.
+ self.viewTrait.FocusPoint(self.listTrait.GetSelectedLineIdx())
+ if self.RenderSelection {
+ min, max := self.viewTrait.ViewPortYBounds()
+ displayStrings := self.GetDisplayStrings(min, max)
+ content := utils.RenderDisplayStrings(displayStrings)
+ self.viewTrait.SetViewPortContent(content)
+ }
+ self.viewTrait.SetFooter(formatListFooter(self.listTrait.GetSelectedLineIdx(), self.listTrait.GetItemsLength()))
+}
+
+func formatListFooter(selectedLineIdx int, length int) string {
+ return fmt.Sprintf("%d of %d", selectedLineIdx+1, length)
+}
+
+func (self *ListContextTrait) GetSelectedItemId() string {
+ item, ok := self.thing.GetSelectedItem()
+
+ if !ok {
+ return ""
+ }
+
+ return item.ID()
+}
+
+// OnFocus assumes that the content of the context has already been rendered to the view. OnRender is the function which actually renders the content to the view
+func (self *ListContextTrait) HandleRender() error {
+ if self.GetDisplayStrings != nil {
+ self.listTrait.RefreshSelectedIdx()
+ content := utils.RenderDisplayStrings(self.GetDisplayStrings(0, self.listTrait.GetItemsLength()))
+ self.viewTrait.SetContent(content)
+ self.c.Render()
+ }
+
+ return nil
+}
+
+func (self *ListContextTrait) HandleFocusLost() error {
+ if self.OnFocusLost != nil {
+ return self.OnFocusLost()
+ }
+
+ self.viewTrait.SetOriginX(0)
+
+ return nil
+}
+
+func (self *ListContextTrait) HandleFocus(opts ...types.OnFocusOpts) error {
+ self.FocusLine()
+
+ if self.OnFocus != nil {
+ if err := self.OnFocus(opts...); err != nil {
+ return err
+ }
+ }
+
+ if self.OnRenderToMain != nil {
+ if err := self.OnRenderToMain(opts...); err != nil {
+ return err
+ }
+ }
+
+ return nil
+}
+
+func (self *ListContextTrait) HandlePrevLine() error {
+ return self.handleLineChange(-1)
+}
+
+func (self *ListContextTrait) HandleNextLine() error {
+ return self.handleLineChange(1)
+}
+
+func (self *ListContextTrait) HandleScrollLeft() error {
+ return self.scroll(self.viewTrait.ScrollLeft)
+}
+
+func (self *ListContextTrait) HandleScrollRight() error {
+ return self.scroll(self.viewTrait.ScrollRight)
+}
+
+func (self *ListContextTrait) scroll(scrollFunc func()) error {
+ scrollFunc()
+
+ return self.HandleFocus()
+}
+
+func (self *ListContextTrait) handleLineChange(change int) error {
+ before := self.listTrait.GetSelectedLineIdx()
+ self.listTrait.MoveSelectedLine(change)
+ after := self.listTrait.GetSelectedLineIdx()
+
+ if before != after {
+ return self.HandleFocus()
+ }
+
+ return nil
+}
+
+func (self *ListContextTrait) HandlePrevPage() error {
+ return self.handleLineChange(-self.viewTrait.PageDelta())
+}
+
+func (self *ListContextTrait) HandleNextPage() error {
+ return self.handleLineChange(self.viewTrait.PageDelta())
+}
+
+func (self *ListContextTrait) HandleGotoTop() error {
+ return self.handleLineChange(-self.listTrait.GetItemsLength())
+}
+
+func (self *ListContextTrait) HandleGotoBottom() error {
+ return self.handleLineChange(self.listTrait.GetItemsLength())
+}
+
+func (self *ListContextTrait) HandleClick(onClick func() error) error {
+ prevSelectedLineIdx := self.listTrait.GetSelectedLineIdx()
+ // because we're handling a click, we need to determine the new line idx based
+ // on the view itself.
+ newSelectedLineIdx := self.viewTrait.SelectedLineIdx()
+
+ currentContextKey := self.c.CurrentContext().GetKey()
+ alreadyFocused := currentContextKey == self.base.GetKey()
+
+ // we need to focus the view
+ if !alreadyFocused {
+
+ if err := self.takeFocus(); err != nil {
+ return err
+ }
+ }
+
+ if newSelectedLineIdx > self.listTrait.GetItemsLength()-1 {
+ return nil
+ }
+
+ self.listTrait.SetSelectedLineIdx(newSelectedLineIdx)
+
+ if prevSelectedLineIdx == newSelectedLineIdx && alreadyFocused && onClick != nil {
+ return onClick()
+ }
+ return self.HandleFocus()
+}
+
+func (self *ListContextTrait) OnSearchSelect(selectedLineIdx int) error {
+ self.listTrait.SetSelectedLineIdx(selectedLineIdx)
+ return self.HandleFocus()
+}
+
+func (self *ListContextTrait) HandleRenderToMain() error {
+ if self.OnRenderToMain != nil {
+ return self.OnRenderToMain()
+ }
+
+ return nil
+}
+
+func (self *ListContextTrait) Keybindings(
+ getKey func(key string) interface{},
+ config config.KeybindingConfig,
+ guards types.KeybindingGuards,
+) []*types.Binding {
+ return []*types.Binding{
+ {Tag: "navigation", Key: getKey(config.Universal.PrevItemAlt), Modifier: gocui.ModNone, Handler: self.HandlePrevLine},
+ {Tag: "navigation", Key: getKey(config.Universal.PrevItem), Modifier: gocui.ModNone, Handler: self.HandlePrevLine},
+ {Tag: "navigation", Key: gocui.MouseWheelUp, Modifier: gocui.ModNone, Handler: self.HandlePrevLine},
+ {Tag: "navigation", Key: getKey(config.Universal.NextItemAlt), Modifier: gocui.ModNone, Handler: self.HandleNextLine},
+ {Tag: "navigation", Key: getKey(config.Universal.NextItem), Modifier: gocui.ModNone, Handler: self.HandleNextLine},
+ {Tag: "navigation", Key: getKey(config.Universal.PrevPage), Modifier: gocui.ModNone, Handler: self.HandlePrevPage, Description: self.c.Tr.LcPrevPage},
+ {Tag: "navigation", Key: getKey(config.Universal.NextPage), Modifier: gocui.ModNone, Handler: self.HandleNextPage, Description: self.c.Tr.LcNextPage},
+ {Tag: "navigation", Key: getKey(config.Universal.GotoTop), Modifier: gocui.ModNone, Handler: self.HandleGotoTop, Description: self.c.Tr.LcGotoTop},
+ {Key: gocui.MouseLeft, Modifier: gocui.ModNone, Handler: func() error { return self.HandleClick(nil) }},
+ {Tag: "navigation", Key: gocui.MouseWheelDown, Modifier: gocui.ModNone, Handler: self.HandleNextLine},
+ {Tag: "navigation", Key: getKey(config.Universal.ScrollLeft), Modifier: gocui.ModNone, Handler: self.HandleScrollLeft},
+ {Tag: "navigation", Key: getKey(config.Universal.ScrollRight), Modifier: gocui.ModNone, Handler: self.HandleScrollRight},
+ {
+ Key: getKey(config.Universal.StartSearch),
+ Handler: func() error { self.c.OpenSearch(); return nil },
+ Description: self.c.Tr.LcStartSearch,
+ Tag: "navigation",
+ },
+ {
+ Key: getKey(config.Universal.GotoBottom),
+ Description: self.c.Tr.LcGotoBottom,
+ Handler: self.HandleGotoBottom,
+ Tag: "navigation",
+ },
+ }
+}
diff --git a/pkg/gui/context/list_trait.go b/pkg/gui/context/list_trait.go
new file mode 100644
index 000000000..27b0b5345
--- /dev/null
+++ b/pkg/gui/context/list_trait.go
@@ -0,0 +1,32 @@
+package context
+
+import "github.com/jesseduffield/lazygit/pkg/gui/types"
+
+type HasLength interface {
+ GetItemsLength() int
+}
+
+type ListTrait struct {
+ selectedIdx int
+ HasLength
+}
+
+var _ types.IListPanelState = (*ListTrait)(nil)
+
+func (self *ListTrait) GetSelectedLineIdx() int {
+ return self.selectedIdx
+}
+
+func (self *ListTrait) SetSelectedLineIdx(value int) {
+ self.selectedIdx = clamp(value, 0, self.GetItemsLength()-1)
+}
+
+// moves the cursor up or down by the given amount
+func (self *ListTrait) MoveSelectedLine(value int) {
+ self.SetSelectedLineIdx(self.selectedIdx + value)
+}
+
+// to be called when the model might have shrunk so that our selection is not not out of bounds
+func (self *ListTrait) RefreshSelectedIdx() {
+ self.SetSelectedLineIdx(self.selectedIdx)
+}
diff --git a/pkg/gui/context/parent_context_mgr.go b/pkg/gui/context/parent_context_mgr.go
new file mode 100644
index 000000000..50747a3a0
--- /dev/null
+++ b/pkg/gui/context/parent_context_mgr.go
@@ -0,0 +1,20 @@
+package context
+
+import "github.com/jesseduffield/lazygit/pkg/gui/types"
+
+type ParentContextMgr struct {
+ ParentContext types.Context
+ // we can't know on the calling end whether a Context is actually a nil value without reflection, so we're storing this flag here to tell us. There has got to be a better way around this
+ hasParent bool
+}
+
+var _ types.ParentContexter = (*ParentContextMgr)(nil)
+
+func (self *ParentContextMgr) SetParentContext(context types.Context) {
+ self.ParentContext = context
+ self.hasParent = true
+}
+
+func (self *ParentContextMgr) GetParentContext() (types.Context, bool) {
+ return self.ParentContext, self.hasParent
+}
diff --git a/pkg/gui/context/tags_context.go b/pkg/gui/context/tags_context.go
new file mode 100644
index 000000000..f5e7f6cb8
--- /dev/null
+++ b/pkg/gui/context/tags_context.go
@@ -0,0 +1,107 @@
+package context
+
+import (
+ "github.com/jesseduffield/gocui"
+ "github.com/jesseduffield/lazygit/pkg/commands/models"
+ "github.com/jesseduffield/lazygit/pkg/gui/types"
+)
+
+type TagsContext struct {
+ *TagsContextAux
+ *BaseContext
+ *ListContextTrait
+}
+
+var _ types.IListContext = (*TagsContext)(nil)
+
+func NewTagsContext(
+ getModel func() []*models.Tag,
+ getView func() *gocui.View,
+ getDisplayStrings func(startIdx int, length int) [][]string,
+
+ onFocus func(...types.OnFocusOpts) error,
+ onRenderToMain func(...types.OnFocusOpts) error,
+ onFocusLost func() error,
+
+ c *types.ControllerCommon,
+) *TagsContext {
+ baseContext := NewBaseContext(NewBaseContextOpts{
+ ViewName: "branches",
+ WindowName: "branches",
+ Key: TAGS_CONTEXT_KEY,
+ Kind: types.SIDE_CONTEXT,
+ })
+
+ self := &TagsContext{}
+ takeFocus := func() error { return c.PushContext(self) }
+
+ aux := NewTagsContextAux(getModel)
+ viewTrait := NewViewTrait(getView)
+ listContextTrait := &ListContextTrait{
+ base: baseContext,
+ thing: aux,
+ listTrait: aux.list,
+ viewTrait: viewTrait,
+
+ GetDisplayStrings: getDisplayStrings,
+ OnFocus: onFocus,
+ OnRenderToMain: onRenderToMain,
+ OnFocusLost: onFocusLost,
+ takeFocus: takeFocus,
+
+ // TODO: handle this in a trait
+ RenderSelection: false,
+
+ c: c,
+ }
+
+ self.BaseContext = baseContext
+ self.ListContextTrait = listContextTrait
+ self.TagsContextAux = aux
+
+ return self
+}
+
+type TagsContextAux struct {
+ list *ListTrait
+ getModel func() []*models.Tag
+}
+
+func (self *TagsContextAux) GetItemsLength() int {
+ return len(self.getModel())
+}
+
+func (self *TagsContextAux) GetSelectedTag() *models.Tag {
+ if self.GetItemsLength() == 0 {
+ return nil
+ }
+
+ return self.getModel()[self.list.GetSelectedLineIdx()]
+}
+
+func (self *TagsContextAux) GetSelectedItem() (types.ListItem, bool) {
+ tag := self.GetSelectedTag()
+ return tag, tag != nil
+}
+
+func NewTagsContextAux(getModel func() []*models.Tag) *TagsContextAux {
+ self := &TagsContextAux{
+ getModel: getModel,
+ }
+
+ self.list = &ListTrait{
+ selectedIdx: 0,
+ HasLength: self,
+ }
+
+ return self
+}
+
+func clamp(x int, min int, max int) int {
+ if x < min {
+ return min
+ } else if x > max {
+ return max
+ }
+ return x
+}
diff --git a/pkg/gui/context/view_trait.go b/pkg/gui/context/view_trait.go
new file mode 100644
index 000000000..4c02a4990
--- /dev/null
+++ b/pkg/gui/context/view_trait.go
@@ -0,0 +1,80 @@
+package context
+
+import (
+ "github.com/jesseduffield/gocui"
+ "github.com/jesseduffield/lazygit/pkg/utils"
+)
+
+const HORIZONTAL_SCROLL_FACTOR = 3
+
+type ViewTrait struct {
+ getView func() *gocui.View
+}
+
+func NewViewTrait(getView func() *gocui.View) *ViewTrait {
+ return &ViewTrait{getView: getView}
+}
+
+func (self *ViewTrait) FocusPoint(yIdx int) {
+ view := self.getView()
+ view.FocusPoint(view.OriginX(), yIdx)
+}
+
+func (self *ViewTrait) SetViewPortContent(content string) {
+ view := self.getView()
+
+ _, y := view.Origin()
+ view.OverwriteLines(y, content)
+}
+
+func (self *ViewTrait) SetContent(content string) {
+ self.getView().SetContent(content)
+}
+
+func (self *ViewTrait) SetFooter(value string) {
+ self.getView().Footer = value
+}
+
+func (self *ViewTrait) SetOriginX(value int) {
+ self.getView().SetOriginX(value)
+}
+
+// tells us the bounds of line indexes shown in the view currently
+func (self *ViewTrait) ViewPortYBounds() (int, int) {
+ view := self.getView()
+
+ _, min := view.Origin()
+ max := view.InnerHeight() + 1
+ return min, max
+}
+
+func (self *ViewTrait) ScrollLeft() {
+ view := self.getView()
+
+ newOriginX := utils.Max(view.OriginX()-view.InnerWidth()/HORIZONTAL_SCROLL_FACTOR, 0)
+ _ = view.SetOriginX(newOriginX)
+}
+
+func (self *ViewTrait) ScrollRight() {
+ view := self.getView()
+
+ _ = view.SetOriginX(view.OriginX() + view.InnerWidth()/HORIZONTAL_SCROLL_FACTOR)
+}
+
+// this returns the amount we'll scroll if we want to scroll by a page.
+func (self *ViewTrait) PageDelta() int {
+ view := self.getView()
+
+ _, height := view.Size()
+
+ delta := height - 1
+ if delta == 0 {
+ return 1
+ }
+
+ return delta
+}
+
+func (self *ViewTrait) SelectedLineIdx() int {
+ return self.getView().SelectedLineIdx()
+}