Skip to content

Commit 221915e

Browse files
committed
feat(tui): add slide navigation command interface
- add `Command` struct with list-based slide navigation - implement keyboard controls for slide selection and filtering - add styling and layout for the navigation modal - support slide title display with fallback numbering - include quit and selection handling with proper state management
1 parent 8d9260e commit 221915e

File tree

1 file changed

+185
-0
lines changed

1 file changed

+185
-0
lines changed

internal/tui/command.go

Lines changed: 185 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,185 @@
1+
package tui
2+
3+
import (
4+
"fmt"
5+
"io"
6+
"strings"
7+
8+
"github.com/charmbracelet/bubbles/key"
9+
"github.com/charmbracelet/bubbles/list"
10+
tea "github.com/charmbracelet/bubbletea"
11+
"github.com/charmbracelet/lipgloss"
12+
)
13+
14+
var (
15+
titleStyle = lipgloss.NewStyle()
16+
itemStyle = lipgloss.NewStyle().
17+
PaddingLeft(2)
18+
selectedItemStyle = lipgloss.NewStyle().
19+
Foreground(lipgloss.Color("#9999CC"))
20+
paginationStyle = list.DefaultStyles().PaginationStyle.
21+
PaddingLeft(2)
22+
helpStyle = list.DefaultStyles().HelpStyle.
23+
PaddingLeft(2).
24+
PaddingBottom(2)
25+
quitTextStyle = lipgloss.NewStyle().
26+
Margin(0, 0, 0, 2)
27+
)
28+
29+
type SlideItem struct {
30+
slide *Slide
31+
title string
32+
number int
33+
}
34+
35+
func (s SlideItem) FilterValue() string { return s.title }
36+
37+
type itemDelegate struct{}
38+
39+
func (d itemDelegate) Height() int { return 1 }
40+
func (d itemDelegate) Spacing() int { return 0 }
41+
func (d itemDelegate) Update(_ tea.Msg, _ *list.Model) tea.Cmd { return nil }
42+
func (d itemDelegate) Render(w io.Writer, m list.Model, index int, listItem list.Item) {
43+
i, ok := listItem.(SlideItem)
44+
if !ok {
45+
return
46+
}
47+
48+
str := fmt.Sprintf("%d. %s", i.number, i.title)
49+
50+
fn := itemStyle.Render
51+
if index == m.Index() {
52+
fn = func(s ...string) string {
53+
return selectedItemStyle.Render("> " + strings.Join(s, " "))
54+
}
55+
}
56+
57+
fmt.Fprint(w, fn(str))
58+
}
59+
60+
type Command struct {
61+
list list.Model
62+
choice *Slide
63+
quitting bool
64+
}
65+
66+
func NewCommand(rootSlide *Slide) Command {
67+
items := []list.Item{}
68+
69+
current := rootSlide
70+
slideNumber := 1
71+
72+
for current != nil {
73+
title := current.Properties.Title
74+
if title == "" {
75+
title = fmt.Sprintf("#%d", slideNumber)
76+
}
77+
78+
items = append(items, SlideItem{
79+
slide: current,
80+
title: title,
81+
number: slideNumber,
82+
})
83+
84+
current = current.Next
85+
slideNumber++
86+
}
87+
88+
const modalWidth = 90
89+
const listHeight = 10
90+
91+
l := list.New(items, itemDelegate{}, modalWidth, listHeight)
92+
l.Title = "Go to Slide"
93+
l.SetShowStatusBar(false)
94+
l.SetFilteringEnabled(true)
95+
l.SetShowHelp(true)
96+
97+
l.Styles.PaginationStyle = paginationStyle
98+
l.Styles.Title = titleStyle
99+
l.Styles.HelpStyle = helpStyle
100+
101+
l.Styles.NoItems = lipgloss.NewStyle().
102+
MarginLeft(0).
103+
Foreground(lipgloss.Color("240"))
104+
105+
l.KeyMap.Filter = key.NewBinding(
106+
key.WithKeys("f"),
107+
key.WithHelp("f", "filter"),
108+
)
109+
110+
return Command{list: l}
111+
}
112+
113+
func (m Command) Init() tea.Cmd {
114+
return nil
115+
}
116+
117+
func (m Command) Update(msg tea.Msg) (Command, tea.Cmd) {
118+
switch msg := msg.(type) {
119+
case tea.WindowSizeMsg:
120+
m.list.SetWidth(msg.Width)
121+
return m, nil
122+
123+
case tea.KeyMsg:
124+
switch keypress := msg.String(); keypress {
125+
case "ctrl+c", "q":
126+
m.quitting = true
127+
return m, tea.Quit
128+
case "esc":
129+
// If currently filtering, let the list handle it (clear filter)
130+
// Otherwise, quit the modal
131+
if m.list.FilterState() == list.Filtering || m.list.FilterState() == list.FilterApplied {
132+
var cmd tea.Cmd
133+
m.list, cmd = m.list.Update(msg)
134+
return m, cmd
135+
} else {
136+
m.quitting = true
137+
return m, tea.Quit
138+
}
139+
case "enter":
140+
if item := m.list.SelectedItem(); item != nil {
141+
if i, ok := item.(SlideItem); ok {
142+
m.choice = i.slide
143+
}
144+
}
145+
m.quitting = true
146+
return m, tea.Quit
147+
}
148+
}
149+
150+
var cmd tea.Cmd
151+
m.list, cmd = m.list.Update(msg)
152+
return m, cmd
153+
}
154+
155+
func (m Command) View() string {
156+
if m.choice != nil {
157+
title := m.choice.Properties.Title
158+
if title == "" {
159+
title = "selected slide"
160+
}
161+
return quitTextStyle.Render(fmt.Sprintf("Navigating to: %s", title))
162+
}
163+
if m.quitting {
164+
return quitTextStyle.Render("Cancelled.")
165+
}
166+
167+
content := lipgloss.Place(
168+
90,
169+
15,
170+
lipgloss.Left,
171+
lipgloss.Center,
172+
m.list.View(),
173+
)
174+
175+
return "\n" + content
176+
}
177+
178+
func (m Command) Choice() *Slide {
179+
return m.choice
180+
}
181+
182+
type OpenCommandMsg struct{}
183+
type CloseCommandMsg struct {
184+
SelectedSlide *Slide
185+
}

0 commit comments

Comments
 (0)