Skip to content

Commit d940f42

Browse files
committed
feat: add help menu toggle
Signed-off-by: Ayush <mail@ayuch.dev>
1 parent 001fae8 commit d940f42

6 files changed

Lines changed: 203 additions & 36 deletions

File tree

internal/tui/keys.go

Lines changed: 17 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -5,8 +5,9 @@ import "github.com/charmbracelet/bubbles/key"
55
// KeyMap defines the keybindings for the application.
66
type KeyMap struct {
77
// miscellaneous keybindings
8-
Quit key.Binding
9-
Help key.Binding
8+
Quit key.Binding
9+
Escape key.Binding
10+
ToggleHelp key.Binding
1011

1112
// keybindings for changing theme
1213
SwitchTheme key.Binding
@@ -30,22 +31,20 @@ type KeyMap struct {
3031
// help for all keybindings
3132
func (k KeyMap) FullHelp() [][]key.Binding {
3233
return [][]key.Binding{
33-
// Navigation Help
34-
{k.FocusNext, k.FocusPrev, k.FocusZero, k.FocusOne},
35-
{k.FocusTwo, k.FocusThree, k.FocusFour, k.FocusFive},
36-
37-
// FilesPanel help
38-
{k.StageItem},
39-
{k.StageAll},
40-
41-
// Misc commands help
42-
{k.SwitchTheme, k.Help, k.Quit},
34+
// Navigation
35+
{k.FocusNext, k.FocusPrev},
36+
// Panel Focus
37+
{k.FocusZero, k.FocusOne, k.FocusTwo, k.FocusThree, k.FocusFour, k.FocusFive},
38+
// File Actions
39+
{k.StageItem, k.StageAll},
40+
// Misc Actions
41+
{k.SwitchTheme, k.ToggleHelp, k.Quit},
4342
}
4443
}
4544

4645
// ShortHelp returns a slice of key.Binding containing help for default keybindings
4746
func (k KeyMap) ShortHelp() []key.Binding {
48-
return []key.Binding{k.FocusNext, k.Help, k.Quit}
47+
return []key.Binding{k.FocusNext, k.ToggleHelp, k.Escape, k.Quit}
4948
}
5049

5150
// FilesPanelHelp returns a slice of key.Binding containing help for keybindings related to Files Panel
@@ -62,7 +61,11 @@ func DefaultKeyMap() KeyMap {
6261
key.WithKeys("q", "ctrl+c"),
6362
key.WithHelp("q", "quit"),
6463
),
65-
Help: key.NewBinding(
64+
Escape: key.NewBinding(
65+
key.WithKeys("escape"),
66+
key.WithHelp("esc", "cancel"),
67+
),
68+
ToggleHelp: key.NewBinding(
6669
key.WithKeys("?"),
6770
key.WithHelp("?", "toggle help"),
6871
),

internal/tui/model.go

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@ package tui
33
import (
44
"github.com/charmbracelet/bubbles/help"
55
"github.com/charmbracelet/bubbles/key"
6+
"github.com/charmbracelet/bubbles/viewport"
67
tea "github.com/charmbracelet/bubbletea"
78
)
89

@@ -15,6 +16,8 @@ type Model struct {
1516
themeIndex int
1617
focusedPanel Panel
1718
help help.Model
19+
helpViewport viewport.Model
20+
helpContent string
1821
showHelp bool
1922
}
2023

@@ -26,6 +29,7 @@ func initialModel() Model {
2629
themeIndex: 0,
2730
focusedPanel: MainPanel,
2831
help: help.New(),
32+
helpViewport: viewport.New(0, 0),
2933
showHelp: false,
3034
}
3135
}

internal/tui/model_test.go

Lines changed: 42 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -140,7 +140,7 @@ func TestModel_contextualHelp(t *testing.T) {
140140
{
141141
name: "Files Panel Help",
142142
focusedPanel: FilesPanel,
143-
expectedKeys: []key.Binding{keys.StageItem, keys.StageAll, keys.FocusNext, keys.Help, keys.Quit},
143+
expectedKeys: []key.Binding{keys.StageItem, keys.StageAll, keys.FocusNext, keys.ToggleHelp, keys.Escape, keys.Quit},
144144
},
145145
}
146146

@@ -156,6 +156,47 @@ func TestModel_contextualHelp(t *testing.T) {
156156
}
157157
}
158158

159+
func TestModel_HelpToggle(t *testing.T) {
160+
t.Run("toggles help when '?' is pressed", func(t *testing.T) {
161+
m := initialModel()
162+
helpKey := key.NewBinding(key.WithKeys("?"))
163+
164+
msg := tea.KeyMsg{Type: tea.KeyRunes, Runes: []rune("?")}
165+
updatedModel, _ := m.Update(msg)
166+
m = updatedModel.(Model)
167+
168+
if !m.showHelp {
169+
t.Errorf("showHelp should be true after pressing '%s', but got %t", helpKey.Keys()[0], m.showHelp)
170+
}
171+
})
172+
173+
t.Run("closes help window if open and '?' is pressed", func(t *testing.T) {
174+
m := initialModel()
175+
helpKey := key.NewBinding(key.WithKeys("?"))
176+
m.showHelp = true
177+
178+
msg := tea.KeyMsg{Type: tea.KeyRunes, Runes: []rune("?")}
179+
updatedModel, _ := m.Update(msg)
180+
m = updatedModel.(Model)
181+
182+
if m.showHelp {
183+
t.Errorf("showHelp should be false after pressing '%s', but got %t", helpKey.Keys()[0], m.showHelp)
184+
}
185+
})
186+
187+
t.Run("does not quit the app when 'q' is pressed while help window is open", func(t *testing.T) {
188+
m := initialModel()
189+
m.showHelp = true
190+
191+
msg := tea.KeyMsg{Type: tea.KeyRunes, Runes: []rune("q")}
192+
_, cmd := m.Update(msg)
193+
194+
if cmd != nil {
195+
t.Errorf("Update should not return a quit command when closing the help view, but it did")
196+
}
197+
})
198+
}
199+
159200
// assertPanel is a helper to compare focused panels.
160201
func assertPanel(t testing.TB, got, want Panel) {
161202
if got != want {

internal/tui/theme.go

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,7 @@ type Theme struct {
99
ActiveTitle lipgloss.Style
1010
InactiveTitle lipgloss.Style
1111
NormalText lipgloss.Style
12+
HelpTitle lipgloss.Style
1213
}
1314

1415
// Themes holds all the available themes.
@@ -28,6 +29,9 @@ var Themes = map[string]Theme{
2829
Foreground(lipgloss.Color("#cad3f5")), // Text
2930
NormalText: lipgloss.NewStyle().
3031
Foreground(lipgloss.Color("#cad3f5")), // Text
32+
HelpTitle: lipgloss.NewStyle().
33+
Foreground(lipgloss.Color("#f5c2e7")).
34+
Bold(true),
3135
},
3236
"Dracula": {
3337
ActivePanel: lipgloss.NewStyle().

internal/tui/update.go

Lines changed: 51 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -3,34 +3,79 @@ package tui
33
import (
44
"github.com/charmbracelet/bubbles/key"
55
tea "github.com/charmbracelet/bubbletea"
6+
"github.com/charmbracelet/lipgloss"
67
)
78

9+
// keys is a package-level variable that holds the application's keybindings.
810
var keys = DefaultKeyMap()
911

10-
// Update handles all incoming messages and updates the model accordingly.
12+
// Update is the central message handler for the application. It's called by the
13+
// Bubble Tea runtime when a message is received. It's responsible for updating
14+
// the model's state based on the message and returning any commands to execute.
1115
func (m Model) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
16+
var cmd tea.Cmd
17+
var cmds []tea.Cmd
18+
1219
switch msg := msg.(type) {
13-
// tea.WindowSizeMsg is sent when the terminal window is resized.
20+
// Handle terminal window resize events.
1421
case tea.WindowSizeMsg:
1522
m.width = msg.Width
1623
m.height = msg.Height
1724
m.help.Width = msg.Width
25+
// Recalculate the dimensions of the help viewport.
26+
m.helpViewport.Width = int(float64(m.width) * 0.5)
27+
m.helpViewport.Height = int(float64(m.height) * 0.75)
1828

19-
// tea.KeyMsg is sent when a key is pressed.
29+
// Handle keyboard input.
2030
case tea.KeyMsg:
31+
// If the help view is currently visible, handle its specific keybindings.
32+
if m.showHelp {
33+
// Allow the viewport to handle scrolling with arrow keys.
34+
m.helpViewport, cmd = m.helpViewport.Update(msg)
35+
cmds = append(cmds, cmd)
36+
37+
// Check for keys that close the help view.
38+
switch {
39+
case key.Matches(msg, keys.Quit), key.Matches(msg, keys.ToggleHelp), key.Matches(msg, keys.Escape):
40+
m.showHelp = false
41+
}
42+
return m, tea.Batch(cmds...)
43+
}
44+
45+
// Handle keybindings for the main application view.
2146
switch {
2247
case key.Matches(msg, keys.Quit):
2348
return m, tea.Quit
2449

50+
case key.Matches(msg, keys.ToggleHelp):
51+
m.showHelp = true
52+
// Generate and style the help content when the view is opened.
53+
m.helpContent = m.generateHelpContent()
54+
m.helpViewport.SetContent(m.helpContent)
55+
m.helpViewport.Style = lipgloss.NewStyle().
56+
Border(lipgloss.RoundedBorder()).
57+
BorderForeground(m.theme.ActivePanel.GetBorderTopForeground()).
58+
Padding(1, 2)
59+
m.helpViewport.GotoTop()
60+
2561
case key.Matches(msg, keys.SwitchTheme):
2662
m.nextTheme()
63+
// Regenerate help content to apply new theme colors.
64+
m.helpContent = m.generateHelpContent()
65+
m.helpViewport.SetContent(m.helpContent)
66+
m.helpViewport.Style = lipgloss.NewStyle().
67+
Border(lipgloss.RoundedBorder()).
68+
BorderForeground(m.theme.ActivePanel.GetBorderTopForeground()).
69+
Padding(1, 2)
2770

71+
// Handle panel focus navigation.
2872
case key.Matches(msg, keys.FocusNext):
2973
m.nextPanel()
3074

3175
case key.Matches(msg, keys.FocusPrev):
3276
m.prevPanel()
3377

78+
// Handle direct panel focus via number keys.
3479
case key.Matches(msg, keys.FocusZero):
3580
m.focusedPanel = MainPanel
3681

@@ -51,7 +96,8 @@ func (m Model) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
5196
}
5297
// Return the updated model to the Bubble Tea runtime.
5398
return m, nil
54-
5599
}
56-
return m, nil
100+
101+
// Batch and return any commands that were generated.
102+
return m, tea.Batch(cmds...)
57103
}

0 commit comments

Comments
 (0)