feat: add full help toggle

Ayman Bagabas created

Change summary

ui/components/code/code.go         | 32 ++++++------
ui/components/footer/footer.go     | 43 +++++++++++++----
ui/components/selector/selector.go |  9 +++
ui/components/viewport/viewport.go | 76 ++++++++++++++++++++++---------
ui/keymap/keymap.go                | 36 ++++++++++++++
ui/pages/repo/files.go             | 70 ++++++++++++++++++++++++++++
ui/pages/repo/log.go               | 78 +++++++++++++++++++++----------
ui/pages/repo/refs.go              | 45 ++++++++++++++++++
ui/pages/repo/repo.go              | 36 ++++++++++++--
ui/pages/selection/selection.go    | 67 ++++++++++++++++++---------
ui/ui.go                           | 77 ++++++++++++++++++-------------
11 files changed, 433 insertions(+), 136 deletions(-)

Detailed changes

ui/components/code/code.go 🔗

@@ -16,10 +16,10 @@ import (
 
 // Code is a code snippet.
 type Code struct {
+	*vp.Viewport
 	common         common.Common
 	content        string
 	extension      string
-	viewport       *vp.Viewport
 	NoContentStyle lipgloss.Style
 }
 
@@ -29,7 +29,7 @@ func New(c common.Common, content, extension string) *Code {
 		common:         c,
 		content:        content,
 		extension:      extension,
-		viewport:       vp.New(),
+		Viewport:       vp.New(c),
 		NoContentStyle: c.Styles.CodeNoContent.Copy(),
 	}
 	r.SetSize(c.Width, c.Height)
@@ -39,7 +39,7 @@ func New(c common.Common, content, extension string) *Code {
 // SetSize implements common.Component.
 func (r *Code) SetSize(width, height int) {
 	r.common.SetSize(width, height)
-	r.viewport.SetSize(width, height)
+	r.Viewport.SetSize(width, height)
 }
 
 // SetContent sets the content of the Code.
@@ -66,7 +66,7 @@ func (r *Code) Init() tea.Cmd {
 	for i, l := range s {
 		s[i] = l + "\x1b[0m"
 	}
-	r.viewport.Viewport.SetContent(strings.Join(s, "\n"))
+	r.Viewport.Model.SetContent(strings.Join(s, "\n"))
 	return nil
 }
 
@@ -78,8 +78,8 @@ func (r *Code) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
 		// Recalculate content width and line wrap.
 		cmds = append(cmds, r.Init())
 	}
-	v, cmd := r.viewport.Update(msg)
-	r.viewport = v.(*vp.Viewport)
+	v, cmd := r.Viewport.Update(msg)
+	r.Viewport = v.(*vp.Viewport)
 	if cmd != nil {
 		cmds = append(cmds, cmd)
 	}
@@ -88,52 +88,52 @@ func (r *Code) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
 
 // View implements tea.View.
 func (r *Code) View() string {
-	return r.viewport.View()
+	return r.Viewport.View()
 }
 
 // GotoTop moves the viewport to the top of the log.
 func (r *Code) GotoTop() {
-	r.viewport.GotoTop()
+	r.Viewport.GotoTop()
 }
 
 // GotoBottom moves the viewport to the bottom of the log.
 func (r *Code) GotoBottom() {
-	r.viewport.GotoBottom()
+	r.Viewport.GotoBottom()
 }
 
 // HalfViewDown moves the viewport down by half the viewport height.
 func (r *Code) HalfViewDown() {
-	r.viewport.HalfViewDown()
+	r.Viewport.HalfViewDown()
 }
 
 // HalfViewUp moves the viewport up by half the viewport height.
 func (r *Code) HalfViewUp() {
-	r.viewport.HalfViewUp()
+	r.Viewport.HalfViewUp()
 }
 
 // ViewUp moves the viewport up by a page.
 func (r *Code) ViewUp() []string {
-	return r.viewport.ViewUp()
+	return r.Viewport.ViewUp()
 }
 
 // ViewDown moves the viewport down by a page.
 func (r *Code) ViewDown() []string {
-	return r.viewport.ViewDown()
+	return r.Viewport.ViewDown()
 }
 
 // LineUp moves the viewport up by the given number of lines.
 func (r *Code) LineUp(n int) []string {
-	return r.viewport.LineUp(n)
+	return r.Viewport.LineUp(n)
 }
 
 // LineDown moves the viewport down by the given number of lines.
 func (r *Code) LineDown(n int) []string {
-	return r.viewport.LineDown(n)
+	return r.Viewport.LineDown(n)
 }
 
 // ScrollPercent returns the viewport's scroll percentage.
 func (r *Code) ScrollPercent() float64 {
-	return r.viewport.ScrollPercent()
+	return r.Viewport.ScrollPercent()
 }
 
 func styleConfig() gansi.StyleConfig {

ui/components/footer/footer.go 🔗

@@ -1,7 +1,10 @@
 package footer
 
 import (
+	"strings"
+
 	"github.com/charmbracelet/bubbles/help"
+	"github.com/charmbracelet/bubbles/key"
 	tea "github.com/charmbracelet/bubbletea"
 	"github.com/charmbracelet/soft-serve/ui/common"
 )
@@ -25,13 +28,14 @@ func New(c common.Common, keymap help.KeyMap) *Footer {
 		help:   h,
 		keymap: keymap,
 	}
+	f.SetSize(c.Width, c.Height)
 	return f
 }
 
 // SetSize implements common.Component.
 func (f *Footer) SetSize(width, height int) {
-	f.common.Width = width
-	f.common.Height = height
+	f.common.SetSize(width, height)
+	f.help.Width = width
 }
 
 // Init implements tea.Model.
@@ -41,13 +45,6 @@ func (f *Footer) Init() tea.Cmd {
 
 // Update implements tea.Model.
 func (f *Footer) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
-	switch msg := msg.(type) {
-	case tea.KeyMsg:
-		switch msg.String() {
-		case "?":
-			f.help.ShowAll = !f.help.ShowAll
-		}
-	}
 	return f, nil
 }
 
@@ -57,5 +54,31 @@ func (f *Footer) View() string {
 		return ""
 	}
 	s := f.common.Styles.Footer.Copy().Width(f.common.Width)
-	return s.Render(f.help.View(f.keymap))
+	helpView := f.help.View(f.keymap)
+	return s.Render(helpView)
+}
+
+// ShortHelp returns the short help key bindings.
+func (f *Footer) ShortHelp() []key.Binding {
+	return f.keymap.ShortHelp()
+}
+
+// FullHelp returns the full help key bindings.
+func (f *Footer) FullHelp() [][]key.Binding {
+	return f.keymap.FullHelp()
+}
+
+// ShowAll returns whether the full help is shown.
+func (f *Footer) ShowAll() bool {
+	return f.help.ShowAll
+}
+
+// SetShowAll sets whether the full help is shown.
+func (f *Footer) SetShowAll(show bool) {
+	f.help.ShowAll = show
+}
+
+// Height returns the height of the footer.
+func (f *Footer) Height() int {
+	return len(strings.Split(f.View(), "\n"))
 }

ui/components/selector/selector.go 🔗

@@ -144,9 +144,16 @@ func (s *Selector) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
 			s.Model.CursorDown()
 		}
 	case tea.KeyMsg:
+		filterState := s.Model.FilterState()
 		switch {
+		case key.Matches(msg, s.common.KeyMap.Help):
+			if filterState == list.Filtering {
+				return s, tea.Batch(cmds...)
+			}
 		case key.Matches(msg, s.common.KeyMap.Select):
-			cmds = append(cmds, s.selectCmd)
+			if filterState != list.Filtering {
+				cmds = append(cmds, s.selectCmd)
+			}
 		}
 	case list.FilterMatchesMsg:
 		cmds = append(cmds, s.activeFilterCmd)

ui/components/viewport/viewport.go 🔗

@@ -1,27 +1,59 @@
 package viewport
 
 import (
+	"github.com/charmbracelet/bubbles/key"
 	"github.com/charmbracelet/bubbles/viewport"
 	tea "github.com/charmbracelet/bubbletea"
+	"github.com/charmbracelet/soft-serve/ui/common"
 )
 
 // Viewport represents a viewport component.
 type Viewport struct {
-	Viewport *viewport.Model
-}
-
-func New() *Viewport {
+	common common.Common
+	*viewport.Model
+}
+
+// New returns a new Viewport.
+func New(c common.Common) *Viewport {
+	vp := viewport.New(c.Width, c.Height)
+	vp.MouseWheelEnabled = true
+	vp.KeyMap = viewport.KeyMap{
+		PageDown: key.NewBinding(
+			key.WithKeys("pgdown", " ", "f"),
+			key.WithHelp("f/pgdn", "page down"),
+		),
+		PageUp: key.NewBinding(
+			key.WithKeys("pgup", "b"),
+			key.WithHelp("b/pgup", "page up"),
+		),
+		HalfPageUp: key.NewBinding(
+			key.WithKeys("u", "ctrl+u"),
+			key.WithHelp("ctrl+u/u", "half page up"),
+		),
+		HalfPageDown: key.NewBinding(
+			key.WithKeys("d", "ctrl+d"),
+			key.WithHelp("ctrl+d/d", "half page down"),
+		),
+		Up: key.NewBinding(
+			key.WithKeys("up", "k"),
+			key.WithHelp("↑/k", "up"),
+		),
+		Down: key.NewBinding(
+			key.WithKeys("down", "j"),
+			key.WithHelp("↓/j", "down"),
+		),
+	}
 	return &Viewport{
-		Viewport: &viewport.Model{
-			MouseWheelEnabled: true,
-		},
+		common: c,
+		Model:  &vp,
 	}
 }
 
 // SetSize implements common.Component.
 func (v *Viewport) SetSize(width, height int) {
-	v.Viewport.Width = width
-	v.Viewport.Height = height
+	v.common.SetSize(width, height)
+	v.Model.Width = width
+	v.Model.Height = height
 }
 
 // Init implements tea.Model.
@@ -31,62 +63,62 @@ func (v *Viewport) Init() tea.Cmd {
 
 // Update implements tea.Model.
 func (v *Viewport) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
-	vp, cmd := v.Viewport.Update(msg)
-	v.Viewport = &vp
+	vp, cmd := v.Model.Update(msg)
+	v.Model = &vp
 	return v, cmd
 }
 
 // View implements tea.Model.
 func (v *Viewport) View() string {
-	return v.Viewport.View()
+	return v.Model.View()
 }
 
 // SetContent sets the viewport's content.
 func (v *Viewport) SetContent(content string) {
-	v.Viewport.SetContent(content)
+	v.Model.SetContent(content)
 }
 
 // GotoTop moves the viewport to the top of the log.
 func (v *Viewport) GotoTop() {
-	v.Viewport.GotoTop()
+	v.Model.GotoTop()
 }
 
 // GotoBottom moves the viewport to the bottom of the log.
 func (v *Viewport) GotoBottom() {
-	v.Viewport.GotoBottom()
+	v.Model.GotoBottom()
 }
 
 // HalfViewDown moves the viewport down by half the viewport height.
 func (v *Viewport) HalfViewDown() {
-	v.Viewport.HalfViewDown()
+	v.Model.HalfViewDown()
 }
 
 // HalfViewUp moves the viewport up by half the viewport height.
 func (v *Viewport) HalfViewUp() {
-	v.Viewport.HalfViewUp()
+	v.Model.HalfViewUp()
 }
 
 // ViewUp moves the viewport up by a page.
 func (v *Viewport) ViewUp() []string {
-	return v.Viewport.ViewUp()
+	return v.Model.ViewUp()
 }
 
 // ViewDown moves the viewport down by a page.
 func (v *Viewport) ViewDown() []string {
-	return v.Viewport.ViewDown()
+	return v.Model.ViewDown()
 }
 
 // LineUp moves the viewport up by the given number of lines.
 func (v *Viewport) LineUp(n int) []string {
-	return v.Viewport.LineUp(n)
+	return v.Model.LineUp(n)
 }
 
 // LineDown moves the viewport down by the given number of lines.
 func (v *Viewport) LineDown(n int) []string {
-	return v.Viewport.LineDown(n)
+	return v.Model.LineDown(n)
 }
 
 // ScrollPercent returns the viewport's scroll percentage.
 func (v *Viewport) ScrollPercent() float64 {
-	return v.Viewport.ScrollPercent()
+	return v.Model.ScrollPercent()
 }

ui/keymap/keymap.go 🔗

@@ -15,6 +15,10 @@ type KeyMap struct {
 	Back      key.Binding
 	PrevPage  key.Binding
 	NextPage  key.Binding
+	Help      key.Binding
+
+	SelectItem key.Binding
+	BackItem   key.Binding
 }
 
 // DefaultKeyMap returns the default key map.
@@ -152,5 +156,37 @@ func DefaultKeyMap() *KeyMap {
 		),
 	)
 
+	km.Help = key.NewBinding(
+		key.WithKeys(
+			"?",
+		),
+		key.WithHelp(
+			"?",
+			"toggle help",
+		),
+	)
+
+	km.SelectItem = key.NewBinding(
+		key.WithKeys(
+			"l",
+			"right",
+		),
+		key.WithHelp(
+			"→",
+			"select",
+		),
+	)
+
+	km.BackItem = key.NewBinding(
+		key.WithKeys(
+			"h",
+			"left",
+		),
+		key.WithHelp(
+			"←",
+			"back",
+		),
+	)
+
 	return km
 }

ui/pages/repo/files.go 🔗

@@ -6,6 +6,7 @@ import (
 	"log"
 	"path/filepath"
 
+	"github.com/charmbracelet/bubbles/key"
 	tea "github.com/charmbracelet/bubbletea"
 	ggit "github.com/charmbracelet/soft-serve/git"
 	"github.com/charmbracelet/soft-serve/ui/common"
@@ -80,6 +81,75 @@ func (f *Files) SetSize(width, height int) {
 	f.code.SetSize(width, height)
 }
 
+// ShortHelp implements help.KeyMap.
+func (f *Files) ShortHelp() []key.Binding {
+	k := f.selector.KeyMap
+	switch f.activeView {
+	case filesViewFiles:
+		return []key.Binding{
+			f.common.KeyMap.SelectItem,
+			f.common.KeyMap.BackItem,
+			k.CursorUp,
+			k.CursorDown,
+		}
+	case filesViewContent:
+		return []key.Binding{
+			f.common.KeyMap.UpDown,
+			f.common.KeyMap.BackItem,
+		}
+	default:
+		return []key.Binding{}
+	}
+}
+
+// FullHelp implements help.KeyMap.
+func (f *Files) FullHelp() [][]key.Binding {
+	b := make([][]key.Binding, 0)
+	switch f.activeView {
+	case filesViewFiles:
+		k := f.selector.KeyMap
+		b = append(b, []key.Binding{
+			f.common.KeyMap.SelectItem,
+			f.common.KeyMap.BackItem,
+		})
+		b = append(b, [][]key.Binding{
+			{},
+			{
+				k.CursorUp,
+				k.CursorDown,
+			},
+			{
+				k.NextPage,
+				k.PrevPage,
+			},
+			{
+				k.GoToStart,
+				k.GoToEnd,
+			},
+		}...)
+	case filesViewContent:
+		k := f.code.KeyMap
+		b = append(b, []key.Binding{
+			f.common.KeyMap.BackItem,
+		})
+		b = append(b, [][]key.Binding{
+			{
+				k.PageDown,
+				k.PageUp,
+			},
+			{
+				k.HalfPageDown,
+				k.HalfPageUp,
+			},
+			{
+				k.Down,
+				k.Up,
+			},
+		}...)
+	}
+	return b
+}
+
 // Init implements tea.Model.
 func (f *Files) Init() tea.Cmd {
 	f.path = ""

ui/pages/repo/log.go 🔗

@@ -57,7 +57,7 @@ type Log struct {
 func NewLog(common common.Common) *Log {
 	l := &Log{
 		common:     common,
-		vp:         viewport.New(),
+		vp:         viewport.New(common),
 		activeView: logViewCommits,
 	}
 	selector := selector.New(common, []selector.IdentifiableItem{}, LogItemDelegate{common.Styles})
@@ -81,43 +81,68 @@ func (l *Log) SetSize(width, height int) {
 	l.vp.SetSize(width, height)
 }
 
-// ShortHelp implements key.KeyMap.
+// ShortHelp implements help.KeyMap.
 func (l *Log) ShortHelp() []key.Binding {
 	switch l.activeView {
 	case logViewCommits:
 		return []key.Binding{
-			key.NewBinding(
-				key.WithKeys(
-					"l",
-					"right",
-				),
-				key.WithHelp(
-					"→",
-					"select",
-				),
-			),
+			l.common.KeyMap.SelectItem,
 		}
 	case logViewDiff:
 		return []key.Binding{
 			l.common.KeyMap.UpDown,
-			key.NewBinding(
-				key.WithKeys(
-					"h",
-					"left",
-				),
-				key.WithHelp(
-					"←",
-					"back",
-				),
-			),
+			l.common.KeyMap.BackItem,
 		}
 	default:
 		return []key.Binding{}
 	}
 }
 
-func (l Log) FullHelp() [][]key.Binding {
-	return [][]key.Binding{}
+// FullHelp implements help.KeyMap.
+func (l *Log) FullHelp() [][]key.Binding {
+	k := l.selector.KeyMap
+	b := make([][]key.Binding, 0)
+	switch l.activeView {
+	case logViewCommits:
+		b = append(b, []key.Binding{
+			l.common.KeyMap.SelectItem,
+			l.common.KeyMap.BackItem,
+		})
+		b = append(b, [][]key.Binding{
+			{
+				k.CursorUp,
+				k.CursorDown,
+			},
+			{
+				k.NextPage,
+				k.PrevPage,
+			},
+			{
+				k.GoToStart,
+				k.GoToEnd,
+			},
+		}...)
+	case logViewDiff:
+		k := l.vp.KeyMap
+		b = append(b, []key.Binding{
+			l.common.KeyMap.BackItem,
+		})
+		b = append(b, [][]key.Binding{
+			{
+				k.PageDown,
+				k.PageUp,
+			},
+			{
+				k.HalfPageDown,
+				k.HalfPageUp,
+			},
+			{
+				k.Down,
+				k.Up,
+			},
+		}...)
+	}
+	return b
 }
 
 // Init implements tea.Model.
@@ -147,7 +172,10 @@ func (l *Log) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
 		cmds = append(cmds, l.selector.SetItems(msg))
 		l.selector.SetPage(l.nextPage)
 		l.SetSize(l.common.Width, l.common.Height)
-		l.activeCommit = l.selector.SelectedItem().(LogItem).Commit
+		i := l.selector.SelectedItem()
+		if i != nil {
+			l.activeCommit = i.(LogItem).Commit
+		}
 	case tea.KeyMsg, tea.MouseMsg:
 		switch l.activeView {
 		case logViewCommits:

ui/pages/repo/refs.go 🔗

@@ -4,6 +4,7 @@ import (
 	"sort"
 	"strings"
 
+	"github.com/charmbracelet/bubbles/key"
 	tea "github.com/charmbracelet/bubbletea"
 	ggit "github.com/charmbracelet/soft-serve/git"
 	"github.com/charmbracelet/soft-serve/ui/common"
@@ -12,11 +13,13 @@ import (
 	"github.com/charmbracelet/soft-serve/ui/git"
 )
 
+// RefItemsMsg is a message that contains a list of RefItem.
 type RefItemsMsg struct {
 	prefix string
 	items  []selector.IdentifiableItem
 }
 
+// Refs is a component that displays a list of references.
 type Refs struct {
 	common    common.Common
 	selector  *selector.Selector
@@ -26,6 +29,7 @@ type Refs struct {
 	refPrefix string
 }
 
+// NewRefs creates a new Refs component.
 func NewRefs(common common.Common, refPrefix string) *Refs {
 	r := &Refs{
 		common:    common,
@@ -43,15 +47,48 @@ func NewRefs(common common.Common, refPrefix string) *Refs {
 	return r
 }
 
+// SetSize implements common.Component.
 func (r *Refs) SetSize(width, height int) {
 	r.common.SetSize(width, height)
 	r.selector.SetSize(width, height)
 }
 
+// ShortHelp implements help.KeyMap.
+func (r *Refs) ShortHelp() []key.Binding {
+	k := r.selector.KeyMap
+	return []key.Binding{
+		r.common.KeyMap.SelectItem,
+		k.CursorUp,
+		k.CursorDown,
+	}
+}
+
+// FullHelp implements help.KeyMap.
+func (r *Refs) FullHelp() [][]key.Binding {
+	k := r.selector.KeyMap
+	return [][]key.Binding{
+		{r.common.KeyMap.SelectItem},
+		{
+			k.CursorUp,
+			k.CursorDown,
+		},
+		{
+			k.NextPage,
+			k.PrevPage,
+		},
+		{
+			k.GoToStart,
+			k.GoToEnd,
+		},
+	}
+}
+
+// Init implements tea.Model.
 func (r *Refs) Init() tea.Cmd {
 	return r.updateItemsCmd
 }
 
+// Update implements tea.Model.
 func (r *Refs) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
 	cmds := make([]tea.Cmd, 0)
 	switch msg := msg.(type) {
@@ -64,7 +101,10 @@ func (r *Refs) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
 		cmds = append(cmds, r.Init())
 	case RefItemsMsg:
 		cmds = append(cmds, r.selector.SetItems(msg.items))
-		r.activeRef = r.selector.SelectedItem().(RefItem).Reference
+		i := r.selector.SelectedItem()
+		if i != nil {
+			r.activeRef = i.(RefItem).Reference
+		}
 	case selector.ActiveMsg:
 		switch sel := msg.IdentifiableItem.(type) {
 		case RefItem:
@@ -93,10 +133,12 @@ func (r *Refs) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
 	return r, tea.Batch(cmds...)
 }
 
+// View implements tea.Model.
 func (r *Refs) View() string {
 	return r.selector.View()
 }
 
+// StausBarValue implements statusbar.StatusBar.
 func (r *Refs) StatusBarValue() string {
 	if r.activeRef == nil {
 		return ""
@@ -104,6 +146,7 @@ func (r *Refs) StatusBarValue() string {
 	return r.activeRef.Name().String()
 }
 
+// StatusBarInfo implements statusbar.StatusBar.
 func (r *Refs) StatusBarInfo() string {
 	return ""
 }

ui/pages/repo/repo.go 🔗

@@ -92,19 +92,24 @@ func (r *Repo) SetSize(width, height int) {
 	}
 }
 
-// ShortHelp implements help.KeyMap.
-func (r *Repo) ShortHelp() []key.Binding {
+func (r *Repo) commonHelp() []key.Binding {
 	b := make([]key.Binding, 0)
+	back := r.common.KeyMap.Back
+	back.SetHelp("esc", "back to menu")
 	tab := r.common.KeyMap.Section
 	tab.SetHelp("tab", "switch tab")
-	back := r.common.KeyMap.Back
-	back.SetHelp("esc", "repos")
 	b = append(b, back)
 	b = append(b, tab)
+	return b
+}
+
+// ShortHelp implements help.KeyMap.
+func (r *Repo) ShortHelp() []key.Binding {
+	b := r.commonHelp()
 	switch r.activeTab {
 	case readmeTab:
 		b = append(b, r.common.KeyMap.UpDown)
-	case commitsTab:
+	default:
 		b = append(b, r.boxes[commitsTab].(help.KeyMap).ShortHelp()...)
 	}
 	return b
@@ -113,6 +118,27 @@ func (r *Repo) ShortHelp() []key.Binding {
 // FullHelp implements help.KeyMap.
 func (r *Repo) FullHelp() [][]key.Binding {
 	b := make([][]key.Binding, 0)
+	b = append(b, r.commonHelp())
+	switch r.activeTab {
+	case readmeTab:
+		k := r.boxes[readmeTab].(*code.Code).KeyMap
+		b = append(b, [][]key.Binding{
+			{
+				k.PageDown,
+				k.PageUp,
+			},
+			{
+				k.HalfPageDown,
+				k.HalfPageUp,
+			},
+			{
+				k.Down,
+				k.Up,
+			},
+		}...)
+	default:
+		b = append(b, r.boxes[r.activeTab].(help.KeyMap).FullHelp()...)
+	}
 	return b
 }
 

ui/pages/selection/selection.go 🔗

@@ -62,7 +62,7 @@ func (s *Selection) SetSize(width, height int) {
 		// +1 to get wrapping to work.
 		// This is needed because the readme box width has to be -1 from the
 		// readme style in order for wrapping to not break.
-		2
+		1
 	hm := s.common.Styles.ReadmeBox.GetVerticalFrameSize()
 	s.readme.SetSize(width-wm, height-hm)
 	s.selector.SetSize(sw, height)
@@ -87,30 +87,51 @@ func (s *Selection) ShortHelp() []key.Binding {
 }
 
 // FullHelp implements help.KeyMap.
-// TODO implement full help on ?
 func (s *Selection) FullHelp() [][]key.Binding {
-	k := s.selector.KeyMap
-	return [][]key.Binding{
-		{
-			k.CursorUp,
-			k.CursorDown,
-			k.NextPage,
-			k.PrevPage,
-			k.GoToStart,
-			k.GoToEnd,
-		},
-		{
-			k.Filter,
-			k.ClearFilter,
-			k.CancelWhileFiltering,
-			k.AcceptWhileFiltering,
-			k.ShowFullHelp,
-			k.CloseFullHelp,
-		},
-		// Ignore the following keys:
-		// k.Quit,
-		// k.ForceQuit,
+	switch s.activeBox {
+	case readmeBox:
+		k := s.readme.KeyMap
+		return [][]key.Binding{
+			{
+				k.PageDown,
+				k.PageUp,
+			},
+			{
+				k.HalfPageDown,
+				k.HalfPageUp,
+			},
+			{
+				k.Down,
+				k.Up,
+			},
+		}
+	case selectorBox:
+		k := s.selector.KeyMap
+		return [][]key.Binding{
+			{
+				s.common.KeyMap.Select,
+			},
+			{
+				k.CursorUp,
+				k.CursorDown,
+			},
+			{
+				k.NextPage,
+				k.PrevPage,
+			},
+			{
+				k.GoToStart,
+				k.GoToEnd,
+			},
+			{
+				k.Filter,
+				k.ClearFilter,
+				k.CancelWhileFiltering,
+				k.AcceptWhileFiltering,
+			},
+		}
 	}
+	return [][]key.Binding{}
 }
 
 // Init implements tea.Model.

ui/ui.go 🔗

@@ -2,7 +2,6 @@ package ui
 
 import (
 	"log"
-	"strings"
 
 	"github.com/charmbracelet/bubbles/key"
 	tea "github.com/charmbracelet/bubbletea"
@@ -57,7 +56,7 @@ func (ui *UI) getMargins() (wm, hm int) {
 	wm = ui.common.Styles.App.GetHorizontalFrameSize()
 	hm = ui.common.Styles.App.GetVerticalFrameSize() +
 		ui.common.Styles.Header.GetHeight() +
-		ui.common.Styles.Footer.GetHeight()
+		ui.footer.Height()
 	return
 }
 
@@ -70,7 +69,10 @@ func (ui *UI) ShortHelp() []key.Binding {
 	case loadedState:
 		b = append(b, ui.pages[ui.activePage].ShortHelp()...)
 	}
-	b = append(b, ui.common.KeyMap.Quit)
+	b = append(b,
+		ui.common.KeyMap.Quit,
+		ui.common.KeyMap.Help,
+	)
 	return b
 }
 
@@ -83,7 +85,10 @@ func (ui *UI) FullHelp() [][]key.Binding {
 	case loadedState:
 		b = append(b, ui.pages[ui.activePage].FullHelp()...)
 	}
-	b = append(b, []key.Binding{ui.common.KeyMap.Quit})
+	b = append(b, []key.Binding{
+		ui.common.KeyMap.Quit,
+		ui.common.KeyMap.Help,
+	})
 	return b
 }
 
@@ -114,7 +119,6 @@ func (ui *UI) Init() tea.Cmd {
 }
 
 // Update implements tea.Model.
-// TODO show full help.
 func (ui *UI) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
 	log.Printf("msg: %T", msg)
 	cmds := make([]tea.Cmd, 0)
@@ -128,15 +132,20 @@ func (ui *UI) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
 				cmds = append(cmds, cmd)
 			}
 		}
-	case tea.KeyMsg:
-		switch {
-		case key.Matches(msg, ui.common.KeyMap.Back) && ui.error != nil:
-			ui.error = nil
-			ui.state = loadedState
-		case key.Matches(msg, ui.common.KeyMap.Quit):
-			return ui, tea.Quit
-		case ui.activePage == 1 && key.Matches(msg, ui.common.KeyMap.Back):
-			ui.activePage = 0
+	case tea.KeyMsg, tea.MouseMsg:
+		switch msg := msg.(type) {
+		case tea.KeyMsg:
+			switch {
+			case key.Matches(msg, ui.common.KeyMap.Back) && ui.error != nil:
+				ui.error = nil
+				ui.state = loadedState
+			case key.Matches(msg, ui.common.KeyMap.Help):
+				ui.footer.SetShowAll(!ui.footer.ShowAll())
+			case key.Matches(msg, ui.common.KeyMap.Quit):
+				return ui, tea.Quit
+			case ui.activePage == 1 && key.Matches(msg, ui.common.KeyMap.Back):
+				ui.activePage = 0
+			}
 		}
 	case common.ErrorMsg:
 		ui.error = msg
@@ -168,43 +177,45 @@ func (ui *UI) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
 			cmds = append(cmds, cmd)
 		}
 	}
+	// This fixes determining the height margin of the footer.
+	ui.SetSize(ui.common.Width, ui.common.Height)
 	return ui, tea.Batch(cmds...)
 }
 
 // View implements tea.Model.
 func (ui *UI) View() string {
-	s := strings.Builder{}
+	var view string
+	footer := ui.footer.View()
+	style := ui.common.Styles.App.Copy()
 	switch ui.state {
 	case startState:
-		s.WriteString("Loading...")
+		view = "Loading..."
 	case errorState:
 		err := ui.common.Styles.ErrorTitle.Render("Bummer")
 		err += ui.common.Styles.ErrorBody.Render(ui.error.Error())
-		view := ui.common.Styles.ErrorBody.Copy().
+		view = ui.common.Styles.ErrorBody.Copy().
 			Width(ui.common.Width -
-				ui.common.Styles.App.GetHorizontalFrameSize() -
+				style.GetWidth() -
+				style.GetHorizontalFrameSize() -
 				ui.common.Styles.ErrorBody.GetHorizontalFrameSize()).
 			Height(ui.common.Height -
-				ui.common.Styles.App.GetVerticalFrameSize() -
+				style.GetHeight() -
+				style.GetVerticalFrameSize() -
 				ui.common.Styles.Header.GetVerticalFrameSize() - 2).
 			Render(err)
-		s.WriteString(lipgloss.JoinVertical(
-			lipgloss.Bottom,
-			ui.header.View(),
-			view,
-			ui.footer.View(),
-		))
 	case loadedState:
-		s.WriteString(lipgloss.JoinVertical(
-			lipgloss.Bottom,
-			ui.header.View(),
-			ui.pages[ui.activePage].View(),
-			ui.footer.View(),
-		))
+		view = ui.pages[ui.activePage].View()
 	default:
-		s.WriteString("Unknown state :/ this is a bug!")
+		view = "Unknown state :/ this is a bug!"
 	}
-	return ui.common.Styles.App.Render(s.String())
+	return style.Render(
+		lipgloss.JoinVertical(
+			lipgloss.Bottom,
+			ui.header.View(),
+			view,
+			footer,
+		),
+	)
 }
 
 func (ui *UI) setRepoCmd(rn string) tea.Cmd {