main 5041353

list.go RawHistoryFolder

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}

Keyboard shortcuts

Single-key shortcuts are on. ctrl k and esc work whatever this says.

ctrl k :
go to a section, branch, tag, file or repository
?
this list
j k
move the row cursor down / up
enter
open the row under the cursor; on Files, a directory opens or closes in place
↑ ↓ ← → home end
in the file tree (focus on a row): previous / next row, close / open a directory or go to its parent / first entry, first / last row. They never open a file
l h
on Files: open a directory or a file / close a directory or go up to its parent, then to the parent page. Elsewhere: into the side panel (on a merge request, its files) / back to the content; there j k move between its controls
g g G
first / last row
/
filter the list; on Files, find a file anywhere in the repository (esc clears, enter opens the best match)
b
switch branch or tag
g s g f g c g b g m g i g r g a
tabs: summary, files, commits, branches & tags, merge requests, issues, releases, actions (when available)
g h
home
w
wide mode: use the whole width of the screen, or not (remembered)
y
copy a permalink pinned to the commit
d u x
diff: split / unified / collapse or expand all
e E
diff: the whole file in view / every file, with the lines the hunks hide
n
merge request: next note in the diff
esc
close a dialog, leave a field

Branches 8

Tags 42

↑↓ moveenter openesc close