history_sidebar_rendering.go RawHistoryFolder

1package component
2
3import (
4	"github.com/bnema/puregotk/v4/gtk"
5	"github.com/bnema/puregotk/v4/pango"
6
7	"github.com/bnema/dumber/internal/domain/entity"
8)
9
10// =============================================================================
11// List rendering
12// =============================================================================
13
14// rebuildList clears and repopulates the list box from current groups.
15// Must be called on the GTK main thread. Preserves scroll and selection.
16func (hs *HistorySidebar) rebuildList() {
17	hs.mu.Lock()
18	if hs.destroyed || hs.listBox == nil {
19		hs.mu.Unlock()
20		return
21	}
22	listBox := hs.listBox
23	rows := append([]historyDisplayRow(nil), hs.displayRows...)
24	query := hs.currentQuery
25	hasSearchResults := hs.searchResults != nil
26	searchDone := hs.searchDone
27	totalLoaded := hs.totalLoaded
28	isLoading := hs.isLoading
29	hs.clearRelativeTimeBindingsLocked()
30	hs.mu.Unlock()
31
32	// Remove all rows
33	listBox.RemoveAll()
34
35	if len(rows) == 0 {
36		if !hasSearchResults && totalLoaded == 0 {
37			// Browse has not loaded yet AND no search has completed.
38			hs.showLoadingOrEmpty(listBox, query, searchDone, isLoading)
39			return
40		}
41		// Search completed with 0 results, or browse loaded but empty (no history).
42		hs.showEmptyState(listBox, query)
43		hs.restoreScrollAndSelectionInListBox(listBox)
44		return
45	}
46
47	for _, row := range rows {
48		switch row.Kind {
49		case historyDisplayRowHeader:
50			hs.appendGroupHeader(listBox, row.Label)
51		case historyDisplayRowEntry:
52			hs.appendEntryRow(listBox, row.Entry)
53		}
54	}
55
56	listBox.Show()
57
58	// Restore previous scroll position and selection
59	hs.restoreScrollAndSelectionInListBox(listBox)
60
61	// If no selection was restored and this is the first load, select first entry
62	hs.ensureAtLeastOneSelectionInListBox(listBox)
63}
64
65func (hs *HistorySidebar) showLoadingOrEmpty(listBox *gtk.ListBox, query string, searchDone, isLoading bool) {
66	label := gtk.NewLabel(nil)
67	if label == nil {
68		return
69	}
70	label.AddCssClass("history-sidebar-loading")
71
72	switch {
73	case isLoading && query == "":
74		label.SetText("Loading history...")
75	case query != "" && !searchDone:
76		label.SetText("Searching...")
77	case query != "":
78		label.SetText(noResultsText(query))
79	default:
80		label.SetText("No browsing history")
81	}
82
83	label.SetWrap(false)
84	label.SetXalign(0.0)
85
86	row := gtk.NewListBoxRow()
87	if row == nil {
88		return
89	}
90	row.SetSelectable(false)
91	row.SetCanFocus(false)
92	row.SetActivatable(false)
93	row.SetChild(&label.Widget)
94	listBox.Append(&row.Widget)
95}
96
97func noResultsText(query string) string {
98	return "No results for \"" + query + "\""
99}
100
101func (hs *HistorySidebar) showEmptyState(listBox *gtk.ListBox, query string) {
102	label := gtk.NewLabel(nil)
103	if label == nil {
104		return
105	}
106	label.AddCssClass("sidebar-empty")
107	label.AddCssClass("history-sidebar-empty")
108
109	if query != "" {
110		label.SetText(noResultsText(query))
111	} else {
112		label.SetText("No browsing history")
113	}
114
115	label.SetWrap(false)
116	label.SetXalign(0.0)
117
118	row := gtk.NewListBoxRow()
119	if row == nil {
120		return
121	}
122	row.SetSelectable(false)
123	row.SetCanFocus(false)
124	row.SetActivatable(false)
125	row.SetChild(&label.Widget)
126	listBox.Append(&row.Widget)
127}
128
129// appendGroupHeader adds a non-selectable group header label to the list.
130func (hs *HistorySidebar) appendGroupHeader(listBox *gtk.ListBox, labelText string) {
131	label := gtk.NewLabel(&labelText)
132	if label == nil {
133		return
134	}
135	label.AddCssClass("history-sidebar-group-header")
136	label.SetXalign(0.0)
137	label.SetHexpand(true)
138
139	row := gtk.NewListBoxRow()
140	if row == nil {
141		return
142	}
143	row.SetSelectable(false)
144	row.SetCanFocus(false)
145	row.SetActivatable(false)
146	row.SetChild(&label.Widget)
147	listBox.Append(&row.Widget)
148}
149
150// appendEntryRow adds a selectable two-line entry row to the list.
151func (hs *HistorySidebar) appendEntryRow(listBox *gtk.ListBox, entry *entity.HistoryEntry) {
152	if entry == nil {
153		return
154	}
155	// Outer vertical box for two-line layout
156	rowBox := gtk.NewBox(gtk.OrientationVerticalValue, 1)
157	if rowBox == nil {
158		return
159	}
160	rowBox.SetHexpand(true)
161
162	// Title line (first line)
163	titleLabel := gtk.NewLabel(nil)
164	if titleLabel == nil {
165		return
166	}
167	titleLabel.AddCssClass("sidebar-row-title")
168	titleLabel.AddCssClass("history-sidebar-row-title")
169	titleLabel.SetText(safeSidebarString(entry.Title, entry.URL))
170	titleLabel.SetXalign(0.0)
171	titleLabel.SetHexpand(true)
172	titleLabel.SetEllipsize(pango.EllipsizeEndValue)
173
174	// Subtitle line with URL and time
175	subBox := gtk.NewBox(gtk.OrientationHorizontalValue, 0)
176	if subBox == nil {
177		return
178	}
179	subBox.SetHexpand(true)
180
181	urlLabel := gtk.NewLabel(nil)
182	if urlLabel == nil {
183		return
184	}
185	urlLabel.AddCssClass("sidebar-row-subtitle")
186	urlLabel.AddCssClass("history-sidebar-row-subtitle")
187	urlLabel.SetText(readableURL(entry.URL))
188	urlLabel.SetXalign(0.0)
189	urlLabel.SetHexpand(true)
190	urlLabel.SetEllipsize(pango.EllipsizeEndValue)
191
192	timeLabel := gtk.NewLabel(nil)
193	if timeLabel == nil {
194		return
195	}
196	timeLabel.AddCssClass("history-sidebar-row-time")
197	timeLabel.SetText(relativeTimeAt(entry.LastVisited, hs.currentTime()))
198	timeLabel.SetXalign(1.0)
199
200	subBox.Append(&urlLabel.Widget)
201	subBox.Append(&timeLabel.Widget)
202
203	rowBox.Append(&titleLabel.Widget)
204	rowBox.Append(&subBox.Widget)
205
206	// Create the list box row
207	row := gtk.NewListBoxRow()
208	if row == nil {
209		return
210	}
211	row.AddCssClass("sidebar-row")
212	row.AddCssClass("history-sidebar-row")
213	row.SetSelectable(true)
214	row.SetActivatable(true)
215	row.SetCanFocus(true)
216	row.SetFocusOnClick(true)
217	row.SetChild(&rowBox.Widget)
218
219	listBox.Append(&row.Widget)
220	hs.bindRelativeTimeLabel(timeLabel, entry.LastVisited)
221}
222
223// ensureAtLeastOneSelection selects the first selectable row if nothing is selected.
224func (hs *HistorySidebar) ensureAtLeastOneSelectionInListBox(listBox *gtk.ListBox) {
225	if listBox == nil || listBox.GetSelectedRow() != nil {
226		return
227	}
228	for i := 0; ; i++ {
229		row := listBox.GetRowAtIndex(i)
230		if row == nil {
231			break
232		}
233		if row.GetSelectable() {
234			listBox.SelectRow(row)
235			return
236		}
237	}
238}

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