Skip to content

Commit 95710b8

Browse files
committed
feat(tui): add priority picker and help overlay
- Add 'P' shortcut to change bean priority from list and detail views - Add '?' shortcut to show help overlay with all keyboard shortcuts - Help overlay organized by context: List View, Detail View, Pickers - Priority picker follows same UX pattern as status/type pickers
1 parent 9b090c4 commit 95710b8

File tree

7 files changed

+442
-5
lines changed

7 files changed

+442
-5
lines changed

.beans/beans-1s6z--tui-p-shortcut-to-change-bean-priority.md

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,10 +1,10 @@
11
---
22
title: 'TUI: ''P'' shortcut to change bean priority'
3-
status: todo
3+
status: completed
44
type: feature
55
priority: normal
66
created_at: 2025-12-13T00:47:21Z
7-
updated_at: 2025-12-13T00:48:10Z
7+
updated_at: 2025-12-13T01:22:59Z
88
parent: beans-xnp8
99
---
1010

.beans/beans-jw2f--tui-help-overlay-showing-keybindings.md

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,10 +1,10 @@
11
---
22
title: 'TUI: ''?'' help overlay showing keybindings'
3-
status: todo
3+
status: completed
44
type: feature
55
priority: normal
66
created_at: 2025-12-13T00:47:21Z
7-
updated_at: 2025-12-13T00:48:10Z
7+
updated_at: 2025-12-13T01:27:03Z
88
parent: beans-xnp8
99
---
1010

internal/tui/detail.go

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -346,6 +346,16 @@ func (m detailModel) Update(msg tea.Msg) (detailModel, tea.Cmd) {
346346
}
347347
}
348348

349+
case "P":
350+
// Open priority picker
351+
return m, func() tea.Msg {
352+
return openPriorityPickerMsg{
353+
beanID: m.bean.ID,
354+
beanTitle: m.bean.Title,
355+
currentPriority: m.bean.Priority,
356+
}
357+
}
358+
349359
case "b":
350360
// Open blocking picker
351361
return m, func() tea.Msg {
@@ -435,9 +445,11 @@ func (m detailModel) View() string {
435445
footer += helpKeyStyle.Render("e") + " " + helpStyle.Render("edit") + " " +
436446
helpKeyStyle.Render("s") + " " + helpStyle.Render("status") + " " +
437447
helpKeyStyle.Render("t") + " " + helpStyle.Render("type") + " " +
448+
helpKeyStyle.Render("P") + " " + helpStyle.Render("priority") + " " +
438449
helpKeyStyle.Render("p") + " " + helpStyle.Render("parent") + " " +
439450
helpKeyStyle.Render("b") + " " + helpStyle.Render("blocking") + " " +
440451
helpKeyStyle.Render("j/k") + " " + helpStyle.Render("scroll") + " " +
452+
helpKeyStyle.Render("?") + " " + helpStyle.Render("help") + " " +
441453
helpKeyStyle.Render("esc") + " " + helpStyle.Render("back") + " " +
442454
helpKeyStyle.Render("q") + " " + helpStyle.Render("quit")
443455

internal/tui/help.go

Lines changed: 147 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,147 @@
1+
package tui
2+
3+
import (
4+
"strings"
5+
6+
tea "github.com/charmbracelet/bubbletea"
7+
"github.com/charmbracelet/lipgloss"
8+
"github.com/hmans/beans/internal/ui"
9+
)
10+
11+
// openHelpMsg requests opening the help overlay
12+
type openHelpMsg struct{}
13+
14+
// closeHelpMsg is sent when the help overlay is closed
15+
type closeHelpMsg struct{}
16+
17+
// helpOverlayModel displays keyboard shortcuts organized by context
18+
type helpOverlayModel struct {
19+
width int
20+
height int
21+
}
22+
23+
func newHelpOverlayModel(width, height int) helpOverlayModel {
24+
return helpOverlayModel{
25+
width: width,
26+
height: height,
27+
}
28+
}
29+
30+
func (m helpOverlayModel) Init() tea.Cmd {
31+
return nil
32+
}
33+
34+
func (m helpOverlayModel) Update(msg tea.Msg) (helpOverlayModel, tea.Cmd) {
35+
switch msg := msg.(type) {
36+
case tea.WindowSizeMsg:
37+
m.width = msg.Width
38+
m.height = msg.Height
39+
40+
case tea.KeyMsg:
41+
switch msg.String() {
42+
case "?", "esc":
43+
return m, func() tea.Msg {
44+
return closeHelpMsg{}
45+
}
46+
}
47+
}
48+
49+
return m, nil
50+
}
51+
52+
func (m helpOverlayModel) View() string {
53+
if m.width == 0 {
54+
return "Loading..."
55+
}
56+
57+
// Calculate modal dimensions - make it wider than pickers
58+
modalWidth := max(70, min(90, m.width*70/100))
59+
60+
// Title
61+
title := lipgloss.NewStyle().
62+
Bold(true).
63+
Foreground(ui.ColorPrimary).
64+
Render("Keyboard Shortcuts")
65+
66+
// Helper to create a shortcut line
67+
shortcut := func(key, desc string) string {
68+
keyStyle := lipgloss.NewStyle().
69+
Foreground(ui.ColorPrimary).
70+
Bold(true).
71+
Width(20).
72+
Render(key)
73+
descStyle := lipgloss.NewStyle().
74+
Foreground(lipgloss.Color("#fff")).
75+
Render(desc)
76+
return keyStyle + descStyle
77+
}
78+
79+
// Helper to create a section header
80+
sectionHeader := func(name string) string {
81+
return lipgloss.NewStyle().
82+
Bold(true).
83+
Foreground(ui.ColorBlue).
84+
Render(name)
85+
}
86+
87+
var content strings.Builder
88+
content.WriteString(title + "\n\n")
89+
90+
// List view section
91+
content.WriteString(sectionHeader("List View") + "\n")
92+
content.WriteString(shortcut("j/k, ↓/↑", "Navigate up/down") + "\n")
93+
content.WriteString(shortcut("enter", "View bean details") + "\n")
94+
content.WriteString(shortcut("c", "Create new bean") + "\n")
95+
content.WriteString(shortcut("e", "Edit bean in $EDITOR") + "\n")
96+
content.WriteString(shortcut("s", "Change status") + "\n")
97+
content.WriteString(shortcut("t", "Change type") + "\n")
98+
content.WriteString(shortcut("P", "Change priority") + "\n")
99+
content.WriteString(shortcut("p", "Set parent") + "\n")
100+
content.WriteString(shortcut("b", "Manage blocking relationships") + "\n")
101+
content.WriteString(shortcut("/", "Filter list") + "\n")
102+
content.WriteString(shortcut("g t", "Go to tags (filter by tag)") + "\n")
103+
content.WriteString(shortcut("esc", "Clear filter") + "\n")
104+
content.WriteString(shortcut("q", "Quit") + "\n")
105+
content.WriteString("\n")
106+
107+
// Detail view section
108+
content.WriteString(sectionHeader("Detail View") + "\n")
109+
content.WriteString(shortcut("j/k, ↓/↑", "Scroll up/down") + "\n")
110+
content.WriteString(shortcut("tab", "Switch focus (links/body)") + "\n")
111+
content.WriteString(shortcut("enter", "Navigate to linked bean") + "\n")
112+
content.WriteString(shortcut("e", "Edit bean in $EDITOR") + "\n")
113+
content.WriteString(shortcut("s", "Change status") + "\n")
114+
content.WriteString(shortcut("t", "Change type") + "\n")
115+
content.WriteString(shortcut("P", "Change priority") + "\n")
116+
content.WriteString(shortcut("p", "Set parent") + "\n")
117+
content.WriteString(shortcut("b", "Manage blocking relationships") + "\n")
118+
content.WriteString(shortcut("esc/backspace", "Back to list/previous bean") + "\n")
119+
content.WriteString(shortcut("q", "Quit") + "\n")
120+
content.WriteString("\n")
121+
122+
// Picker/Dialog section
123+
content.WriteString(sectionHeader("Pickers & Dialogs") + "\n")
124+
content.WriteString(shortcut("j/k, ↓/↑", "Navigate up/down") + "\n")
125+
content.WriteString(shortcut("enter", "Select/confirm") + "\n")
126+
content.WriteString(shortcut("/", "Filter items") + "\n")
127+
content.WriteString(shortcut("esc", "Cancel") + "\n")
128+
content.WriteString("\n")
129+
130+
// Footer
131+
footer := helpKeyStyle.Render("?/esc") + " " + helpStyle.Render("close")
132+
133+
// Border style
134+
border := lipgloss.NewStyle().
135+
Border(lipgloss.RoundedBorder()).
136+
BorderForeground(ui.ColorPrimary).
137+
Padding(1, 2).
138+
Width(modalWidth)
139+
140+
return border.Render(content.String() + footer)
141+
}
142+
143+
// ModalView returns the help overlay as a centered modal on top of the background
144+
func (m helpOverlayModel) ModalView(bgView string, fullWidth, fullHeight int) string {
145+
modal := m.View()
146+
return overlayModal(bgView, modal, fullWidth, fullHeight)
147+
}

internal/tui/list.go

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -291,6 +291,17 @@ func (m listModel) Update(msg tea.Msg) (listModel, tea.Cmd) {
291291
}
292292
}
293293
}
294+
case "P":
295+
// Open priority picker for selected bean
296+
if item, ok := m.list.SelectedItem().(beanItem); ok {
297+
return m, func() tea.Msg {
298+
return openPriorityPickerMsg{
299+
beanID: item.bean.ID,
300+
beanTitle: item.bean.Title,
301+
currentPriority: item.bean.Priority,
302+
}
303+
}
304+
}
294305
case "b":
295306
// Open blocking picker for selected bean
296307
if item, ok := m.list.SelectedItem().(beanItem); ok {
@@ -378,19 +389,23 @@ func (m listModel) View() string {
378389
helpKeyStyle.Render("e") + " " + helpStyle.Render("edit") + " " +
379390
helpKeyStyle.Render("s") + " " + helpStyle.Render("status") + " " +
380391
helpKeyStyle.Render("t") + " " + helpStyle.Render("type") + " " +
392+
helpKeyStyle.Render("P") + " " + helpStyle.Render("priority") + " " +
381393
helpKeyStyle.Render("p") + " " + helpStyle.Render("parent") + " " +
382394
helpKeyStyle.Render("b") + " " + helpStyle.Render("blocking") + " " +
383395
helpKeyStyle.Render("esc") + " " + helpStyle.Render("clear filter") + " " +
396+
helpKeyStyle.Render("?") + " " + helpStyle.Render("help") + " " +
384397
helpKeyStyle.Render("q") + " " + helpStyle.Render("quit")
385398
} else {
386399
help = helpKeyStyle.Render("enter") + " " + helpStyle.Render("view") + " " +
387400
helpKeyStyle.Render("c") + " " + helpStyle.Render("create") + " " +
388401
helpKeyStyle.Render("e") + " " + helpStyle.Render("edit") + " " +
389402
helpKeyStyle.Render("s") + " " + helpStyle.Render("status") + " " +
390403
helpKeyStyle.Render("t") + " " + helpStyle.Render("type") + " " +
404+
helpKeyStyle.Render("P") + " " + helpStyle.Render("priority") + " " +
391405
helpKeyStyle.Render("p") + " " + helpStyle.Render("parent") + " " +
392406
helpKeyStyle.Render("b") + " " + helpStyle.Render("blocking") + " " +
393407
helpKeyStyle.Render("/") + " " + helpStyle.Render("filter") + " " +
408+
helpKeyStyle.Render("?") + " " + helpStyle.Render("help") + " " +
394409
helpKeyStyle.Render("q") + " " + helpStyle.Render("quit")
395410
}
396411

0 commit comments

Comments
 (0)