main 5041353

link_status.go RawHistoryFolder

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

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