v0.26.1 75cd777

link_status.go RawHistoryFolder

1// Package component provides UI components for the browser.
2package component
3
4import (
5	"sync"
6
7	"github.com/jwijenbergh/puregotk/v4/glib"
8	"github.com/jwijenbergh/puregotk/v4/gtk"
9
10	"github.com/bnema/dumber/internal/ui/layout"
11)
12
13const (
14	// linkStatusShowDelayMs is the delay before showing the overlay to avoid flicker.
15	linkStatusShowDelayMs = 100
16	// linkStatusAutoHideMs is the delay before auto-hiding the overlay (e.g., during fullscreen video).
17	linkStatusAutoHideMs = 10000
18	// linkStatusMaxChars is the maximum characters before truncation.
19	linkStatusMaxChars = 80
20)
21
22// LinkStatusOverlay displays the destination URL when hovering over links.
23// It appears in the bottom-left corner with a fade-in/fade-out transition.
24type LinkStatusOverlay struct {
25	factory   layout.WidgetFactory
26	container layout.BoxWidget
27	label     layout.LabelWidget
28
29	pendingURI    string // URI waiting to be shown after delay
30	showTimer     uint   // GLib timer for delayed show
31	autoHideTimer uint   // GLib timer for auto-hide after timeout
32	visible       bool   // Whether overlay is currently visible (has .visible class)
33	mu            sync.Mutex
34}
35
36// NewLinkStatusOverlay creates a new link status overlay component.
37// The overlay is positioned at bottom-left with fade transitions via CSS.
38func NewLinkStatusOverlay(factory layout.WidgetFactory) *LinkStatusOverlay {
39	// Create container box for styling and positioning
40	container := factory.NewBox(layout.OrientationHorizontal, 0)
41	container.AddCssClass("link-status")
42
43	// Position at bottom-left
44	container.SetHalign(gtk.AlignStartValue)
45	container.SetValign(gtk.AlignEndValue)
46
47	// Don't expand to fill space
48	container.SetHexpand(false)
49	container.SetVexpand(false)
50
51	// Don't intercept pointer events - let clicks pass through
52	container.SetCanTarget(false)
53	container.SetCanFocus(false)
54
55	// Create label for URL text
56	label := factory.NewLabel("")
57	label.SetCanTarget(false)
58	label.SetCanFocus(false)
59	label.SetEllipsize(layout.EllipsizeMiddle) // Truncate in middle for URLs
60	label.SetMaxWidthChars(linkStatusMaxChars)
61	container.Append(label)
62
63	return &LinkStatusOverlay{
64		factory:   factory,
65		container: container,
66		label:     label,
67		visible:   false,
68	}
69}
70
71// Show displays the link status overlay with the given URI.
72// If uri is empty, hides the overlay instead.
73// Uses a small delay to avoid flicker during rapid mouse movement.
74func (l *LinkStatusOverlay) Show(uri string) {
75	l.mu.Lock()
76	defer l.mu.Unlock()
77
78	// Cancel any pending show timer
79	if l.showTimer != 0 {
80		glib.SourceRemove(l.showTimer)
81		l.showTimer = 0
82	}
83
84	if uri == "" {
85		l.hide()
86		return
87	}
88
89	l.pendingURI = uri
90
91	// Start delay timer before showing
92	cb := glib.SourceFunc(func(_ uintptr) bool {
93		l.mu.Lock()
94		defer l.mu.Unlock()
95
96		if l.pendingURI != "" {
97			l.label.SetText(l.pendingURI)
98			if !l.visible {
99				l.visible = true
100				l.container.AddCssClass("visible")
101			}
102			// Start or reset auto-hide timer
103			l.resetAutoHideTimerLocked()
104		}
105		l.showTimer = 0
106		return false // Don't repeat
107	})
108
109	l.showTimer = glib.TimeoutAdd(linkStatusShowDelayMs, &cb, 0)
110}
111
112// Hide manually hides the link status overlay.
113func (l *LinkStatusOverlay) Hide() {
114	l.mu.Lock()
115	defer l.mu.Unlock()
116	l.hide()
117}
118
119// hide performs the actual hide operation (must be called with lock held).
120func (l *LinkStatusOverlay) hide() {
121	// Cancel pending show timer
122	if l.showTimer != 0 {
123		glib.SourceRemove(l.showTimer)
124		l.showTimer = 0
125	}
126	// Cancel auto-hide timer
127	if l.autoHideTimer != 0 {
128		glib.SourceRemove(l.autoHideTimer)
129		l.autoHideTimer = 0
130	}
131	l.pendingURI = ""
132
133	if l.visible {
134		l.visible = false
135		l.container.RemoveCssClass("visible")
136	}
137}
138
139// resetAutoHideTimerLocked starts or resets the auto-hide timer (must be called with lock held).
140func (l *LinkStatusOverlay) resetAutoHideTimerLocked() {
141	// Cancel existing auto-hide timer
142	if l.autoHideTimer != 0 {
143		glib.SourceRemove(l.autoHideTimer)
144		l.autoHideTimer = 0
145	}
146
147	// Start new auto-hide timer
148	cb := glib.SourceFunc(func(_ uintptr) bool {
149		l.mu.Lock()
150		defer l.mu.Unlock()
151
152		l.autoHideTimer = 0
153		l.hide()
154		return false // Don't repeat
155	})
156
157	l.autoHideTimer = glib.TimeoutAdd(linkStatusAutoHideMs, &cb, 0)
158}
159
160// Widget returns the underlying widget for embedding in overlays.
161func (l *LinkStatusOverlay) Widget() layout.Widget {
162	return l.container
163}
164
165// IsVisible returns whether the overlay is currently visible.
166func (l *LinkStatusOverlay) IsVisible() bool {
167	l.mu.Lock()
168	defer l.mu.Unlock()
169	return l.visible
170}
171
172// Cleanup cancels any pending timers and clears state.
173// Must be called before removing the overlay from the UI.
174func (l *LinkStatusOverlay) Cleanup() {
175	l.mu.Lock()
176	defer l.mu.Unlock()
177
178	// Cancel pending show timer to prevent use-after-free
179	if l.showTimer != 0 {
180		glib.SourceRemove(l.showTimer)
181		l.showTimer = 0
182	}
183	// Cancel auto-hide timer
184	if l.autoHideTimer != 0 {
185		glib.SourceRemove(l.autoHideTimer)
186		l.autoHideTimer = 0
187	}
188	l.pendingURI = ""
189	l.visible = false
190}

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