v0.22.0 a85da87

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	// linkStatusMaxChars is the maximum characters before truncation.
17	linkStatusMaxChars = 80
18)
19
20// LinkStatusOverlay displays the destination URL when hovering over links.
21// It appears in the bottom-left corner with a fade-in/fade-out transition.
22type LinkStatusOverlay struct {
23	factory   layout.WidgetFactory
24	container layout.BoxWidget
25	label     layout.LabelWidget
26
27	pendingURI string // URI waiting to be shown after delay
28	showTimer  uint   // GLib timer for delayed show
29	visible    bool   // Whether overlay is currently visible (has .visible class)
30	mu         sync.Mutex
31}
32
33// NewLinkStatusOverlay creates a new link status overlay component.
34// The overlay is positioned at bottom-left with fade transitions via CSS.
35func NewLinkStatusOverlay(factory layout.WidgetFactory) *LinkStatusOverlay {
36	// Create container box for styling and positioning
37	container := factory.NewBox(layout.OrientationHorizontal, 0)
38	container.AddCssClass("link-status")
39
40	// Position at bottom-left
41	container.SetHalign(gtk.AlignStartValue)
42	container.SetValign(gtk.AlignEndValue)
43
44	// Don't expand to fill space
45	container.SetHexpand(false)
46	container.SetVexpand(false)
47
48	// Don't intercept pointer events - let clicks pass through
49	container.SetCanTarget(false)
50	container.SetCanFocus(false)
51
52	// Create label for URL text
53	label := factory.NewLabel("")
54	label.SetCanTarget(false)
55	label.SetCanFocus(false)
56	label.SetEllipsize(layout.EllipsizeMiddle) // Truncate in middle for URLs
57	label.SetMaxWidthChars(linkStatusMaxChars)
58	container.Append(label)
59
60	return &LinkStatusOverlay{
61		factory:   factory,
62		container: container,
63		label:     label,
64		visible:   false,
65	}
66}
67
68// Show displays the link status overlay with the given URI.
69// If uri is empty, hides the overlay instead.
70// Uses a small delay to avoid flicker during rapid mouse movement.
71func (l *LinkStatusOverlay) Show(uri string) {
72	l.mu.Lock()
73	defer l.mu.Unlock()
74
75	// Cancel any pending show timer
76	if l.showTimer != 0 {
77		glib.SourceRemove(l.showTimer)
78		l.showTimer = 0
79	}
80
81	if uri == "" {
82		l.hide()
83		return
84	}
85
86	l.pendingURI = uri
87
88	// Start delay timer before showing
89	cb := glib.SourceFunc(func(_ uintptr) bool {
90		l.mu.Lock()
91		defer l.mu.Unlock()
92
93		if l.pendingURI != "" {
94			l.label.SetText(l.pendingURI)
95			if !l.visible {
96				l.visible = true
97				l.container.AddCssClass("visible")
98			}
99		}
100		l.showTimer = 0
101		return false // Don't repeat
102	})
103
104	l.showTimer = glib.TimeoutAdd(linkStatusShowDelayMs, &cb, 0)
105}
106
107// Hide manually hides the link status overlay.
108func (l *LinkStatusOverlay) Hide() {
109	l.mu.Lock()
110	defer l.mu.Unlock()
111	l.hide()
112}
113
114// hide performs the actual hide operation (must be called with lock held).
115func (l *LinkStatusOverlay) hide() {
116	// Cancel pending show timer
117	if l.showTimer != 0 {
118		glib.SourceRemove(l.showTimer)
119		l.showTimer = 0
120	}
121	l.pendingURI = ""
122
123	if l.visible {
124		l.visible = false
125		l.container.RemoveCssClass("visible")
126	}
127}
128
129// Widget returns the underlying widget for embedding in overlays.
130func (l *LinkStatusOverlay) Widget() layout.Widget {
131	return l.container
132}
133
134// IsVisible returns whether the overlay is currently visible.
135func (l *LinkStatusOverlay) IsVisible() bool {
136	l.mu.Lock()
137	defer l.mu.Unlock()
138	return l.visible
139}
140
141// Cleanup cancels any pending timers and clears state.
142// Must be called before removing the overlay from the UI.
143func (l *LinkStatusOverlay) Cleanup() {
144	l.mu.Lock()
145	defer l.mu.Unlock()
146
147	// Cancel pending show timer to prevent use-after-free
148	if l.showTimer != 0 {
149		glib.SourceRemove(l.showTimer)
150		l.showTimer = 0
151	}
152	l.pendingURI = ""
153	l.visible = false
154}

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