1package styles
2
3import (
4 "fmt"
5 "io"
6 "strings"
7 "time"
8
9 "github.com/charmbracelet/bubbles/list"
10 tea "github.com/charmbracelet/bubbletea"
11 "github.com/charmbracelet/lipgloss"
12)
13
14// HistoryItem represents a history entry for the list.
15type HistoryItem struct {
16 ID int64
17 URL string
18 Title string
19 Domain string
20 VisitCount int
21 LastVisited time.Time
22}
23
24// FilterValue implements list.Item.
25func (i HistoryItem) FilterValue() string {
26 return i.Title + " " + i.URL
27}
28
29// Title implements list.DefaultItem.
30func (i HistoryItem) TitleValue() string {
31 if i.Title != "" {
32 return i.Title
33 }
34 return i.URL
35}
36
37// Description implements list.DefaultItem.
38func (i HistoryItem) DescriptionValue() string {
39 return i.URL
40}
41
42// HistoryDelegate renders history items with theme styling.
43type HistoryDelegate struct {
44 Theme *Theme
45 ShowDomain bool
46}
47
48// NewHistoryDelegate creates a themed history list delegate.
49func NewHistoryDelegate(theme *Theme) HistoryDelegate {
50 return HistoryDelegate{
51 Theme: theme,
52 ShowDomain: true,
53 }
54}
55
56// Height returns the height of each item.
57func (d HistoryDelegate) Height() int {
58 if d.Theme == nil {
59 return 2
60 }
61 return 2
62}
63
64// Spacing returns the spacing between items.
65func (d HistoryDelegate) Spacing() int {
66 if d.Theme == nil {
67 return 0
68 }
69 return 0
70}
71
72// Update handles item-level events.
73func (d HistoryDelegate) Update(_ tea.Msg, _ *list.Model) tea.Cmd {
74 if d.Theme == nil {
75 return nil
76 }
77 return nil
78}
79
80// Render renders a single list item.
81func (d HistoryDelegate) Render(w io.Writer, m list.Model, index int, item list.Item) {
82 hi, ok := item.(HistoryItem)
83 if !ok {
84 return
85 }
86
87 t := d.Theme
88 isSelected := index == m.Index()
89 const (
90 maxTitleLength = 60
91 maxURLLength = 50
92 ellipsisLength = 3
93 )
94
95 // Title line
96 title := hi.TitleValue()
97 if len(title) > maxTitleLength {
98 title = title[:maxTitleLength-ellipsisLength] + "..."
99 }
100
101 // URL/description line
102 url := hi.URL
103 if len(url) > maxURLLength {
104 url = url[:maxURLLength-ellipsisLength] + "..."
105 }
106
107 // Metadata badges
108 visitBadge := t.MutedBadge(fmt.Sprintf("%d", hi.VisitCount))
109 timeBadge := t.MutedBadge(RelativeTime(hi.LastVisited))
110
111 // Build cursor
112 cursor := cursorEmpty
113 if isSelected {
114 cursor = cursorSelected
115 }
116
117 // Apply styles
118 cursorStyle := t.Highlight
119 titleStyle := t.ListItemTitle
120 urlStyle := t.ListItemDesc
121
122 if isSelected {
123 titleStyle = titleStyle.Foreground(t.Accent).Bold(true)
124 urlStyle = urlStyle.Foreground(t.Text)
125 }
126
127 // First line: cursor + title
128 line1 := lipgloss.JoinHorizontal(
129 lipgloss.Left,
130 cursorStyle.Render(cursor),
131 titleStyle.Render(title),
132 )
133
134 // Second line: url + badges
135 meta := lipgloss.JoinHorizontal(
136 lipgloss.Left,
137 visitBadge,
138 " ",
139 timeBadge,
140 )
141
142 line2 := lipgloss.JoinHorizontal(
143 lipgloss.Left,
144 strings.Repeat(" ", 3), // Indent under cursor
145 urlStyle.Render(url),
146 " ",
147 meta,
148 )
149
150 _, _ = fmt.Fprintf(w, "%s\n%s", line1, line2)
151}
152
153// NewHistoryList creates a themed list for history items.
154func NewHistoryList(theme *Theme, items []HistoryItem, width, height int) list.Model {
155 // Convert to list.Item slice
156 listItems := make([]list.Item, len(items))
157 for i, item := range items {
158 listItems[i] = item
159 }
160
161 delegate := NewHistoryDelegate(theme)
162
163 l := list.New(listItems, delegate, width, height)
164 l.SetShowTitle(false)
165 l.SetShowStatusBar(false)
166 l.SetShowFilter(false)
167 l.SetShowHelp(false)
168 l.SetShowPagination(true)
169
170 // Apply theme colors to pagination
171 l.Styles.PaginationStyle = lipgloss.NewStyle().Foreground(theme.Muted)
172 l.Styles.ActivePaginationDot = lipgloss.NewStyle().Foreground(theme.Accent)
173 l.Styles.InactivePaginationDot = lipgloss.NewStyle().Foreground(theme.Muted)
174
175 return l
176}
177
178// SimpleItem is a basic list item with title and description.
179type SimpleItem struct {
180 TitleText string
181 DescText string
182}
183
184// FilterValue implements list.Item.
185func (i SimpleItem) FilterValue() string {
186 return i.TitleText + " " + i.DescText
187}
188
189// SimpleDelegate renders simple items.
190type SimpleDelegate struct {
191 Theme *Theme
192}
193
194// Height returns the height of each item.
195func (d SimpleDelegate) Height() int {
196 if d.Theme == nil {
197 return 1
198 }
199 return 1
200}
201
202// Spacing returns the spacing between items.
203func (d SimpleDelegate) Spacing() int {
204 if d.Theme == nil {
205 return 0
206 }
207 return 0
208}
209
210// Update handles item-level events.
211func (d SimpleDelegate) Update(_ tea.Msg, _ *list.Model) tea.Cmd {
212 if d.Theme == nil {
213 return nil
214 }
215 return nil
216}
217
218// Render renders a single list item.
219func (d SimpleDelegate) Render(w io.Writer, m list.Model, index int, item list.Item) {
220 si, ok := item.(SimpleItem)
221 if !ok {
222 return
223 }
224
225 t := d.Theme
226 isSelected := index == m.Index()
227
228 cursor := cursorEmpty
229 style := t.ListItem
230 if isSelected {
231 cursor = cursorSelected
232 style = t.ListItemSelected
233 }
234
235 line := lipgloss.JoinHorizontal(
236 lipgloss.Left,
237 t.Highlight.Render(cursor),
238 style.Render(si.TitleText),
239 )
240
241 if si.DescText != "" {
242 line = lipgloss.JoinHorizontal(
243 lipgloss.Left,
244 line,
245 " ",
246 t.Subtle.Render(si.DescText),
247 )
248 }
249
250 _, _ = fmt.Fprint(w, line)
251}