v0.29.0 21b3150

floating_pane.go RawHistoryFolder

1package component
2
3import (
4	"context"
5	"strings"
6	"sync"
7
8	"github.com/bnema/dumber/internal/ui/layout"
9)
10
11const defaultFloatingPaneURL = "about:blank"
12
13// FloatingPaneOptions controls floating pane behavior.
14type FloatingPaneOptions struct {
15	WidthPct       float64
16	HeightPct      float64
17	FallbackWidth  int
18	FallbackHeight int
19	OnNavigate     func(ctx context.Context, url string) error
20}
21
22// FloatingPane tracks persistent floating workspace pane state.
23type FloatingPane struct {
24	parent         layout.OverlayWidget
25	widthPct       float64
26	heightPct      float64
27	fallbackWidth  int
28	fallbackHeight int
29	onNavigate     func(ctx context.Context, url string) error
30
31	mu               sync.RWMutex
32	visible          bool
33	omniboxVisible   bool
34	sessionStarted   bool
35	currentURL       string
36	stateVersion     uint64
37	calculatedWidth  int
38	calculatedHeight int
39}
40
41// NewFloatingPane creates a floating pane state container.
42func NewFloatingPane(parent layout.OverlayWidget, opts FloatingPaneOptions) *FloatingPane {
43	if opts.WidthPct <= 0 || opts.WidthPct > 1 {
44		opts.WidthPct = 0.82
45	}
46	if opts.HeightPct <= 0 || opts.HeightPct > 1 {
47		opts.HeightPct = 0.72
48	}
49	if opts.FallbackWidth <= 0 {
50		opts.FallbackWidth = 1200
51	}
52	if opts.FallbackHeight <= 0 {
53		opts.FallbackHeight = 800
54	}
55
56	fp := &FloatingPane{
57		parent:         parent,
58		widthPct:       opts.WidthPct,
59		heightPct:      opts.HeightPct,
60		fallbackWidth:  opts.FallbackWidth,
61		fallbackHeight: opts.FallbackHeight,
62		onNavigate:     opts.OnNavigate,
63	}
64	fp.Resize()
65	return fp
66}
67
68// ShowToggle toggles pane visibility; first open initializes a blank session.
69func (fp *FloatingPane) ShowToggle(ctx context.Context) error {
70	fp.mu.Lock()
71	previousVisible := fp.visible
72	previousOmniboxVisible := fp.omniboxVisible
73	if fp.visible {
74		fp.visible = false
75		fp.omniboxVisible = false
76		fp.stateVersion++
77		fp.mu.Unlock()
78		return nil
79	}
80
81	navigateToBlank := !fp.sessionStarted
82	showOmnibox := navigateToBlank || fp.currentURL == defaultFloatingPaneURL
83	fp.visible = true
84	fp.omniboxVisible = showOmnibox
85	fp.stateVersion++
86	rollbackVersion := fp.stateVersion
87	// Release lock before Navigate: callbacks may block and must not run while
88	// holding fp.mu. On error, rollback below restores visibility state.
89	fp.mu.Unlock()
90
91	if navigateToBlank {
92		if err := fp.Navigate(ctx, defaultFloatingPaneURL); err != nil {
93			fp.mu.Lock()
94			if fp.stateVersion == rollbackVersion {
95				fp.visible = previousVisible
96				fp.omniboxVisible = previousOmniboxVisible
97				fp.stateVersion++
98			}
99			fp.mu.Unlock()
100			return err
101		}
102	}
103
104	return nil
105}
106
107// Show makes the pane visible without navigation.
108// Use for profile sessions that already have a loaded URL.
109func (fp *FloatingPane) Show() {
110	fp.mu.Lock()
111	defer fp.mu.Unlock()
112	fp.visible = true
113	fp.omniboxVisible = false
114	fp.stateVersion++
115}
116
117// SessionStarted reports whether the floating pane has navigated at least once.
118func (fp *FloatingPane) SessionStarted() bool {
119	fp.mu.RLock()
120	defer fp.mu.RUnlock()
121	return fp.sessionStarted
122}
123
124// ShowURL shows the pane and navigates to a target URL.
125func (fp *FloatingPane) ShowURL(ctx context.Context, url string) error {
126	url = strings.TrimSpace(url)
127	if url == "" {
128		fp.mu.Lock()
129		fp.visible = true
130		fp.omniboxVisible = true
131		fp.stateVersion++
132		fp.mu.Unlock()
133		return nil
134	}
135
136	fp.mu.Lock()
137	previousVisible := fp.visible
138	previousOmniboxVisible := fp.omniboxVisible
139	fp.visible = true
140	fp.omniboxVisible = false
141	shouldNavigate := !fp.sessionStarted || fp.currentURL != url
142	fp.stateVersion++
143	rollbackVersion := fp.stateVersion
144	fp.mu.Unlock()
145
146	if shouldNavigate {
147		if err := fp.Navigate(ctx, url); err != nil {
148			fp.mu.Lock()
149			if fp.stateVersion == rollbackVersion {
150				fp.visible = previousVisible
151				fp.omniboxVisible = previousOmniboxVisible
152				fp.stateVersion++
153			}
154			fp.mu.Unlock()
155			return err
156		}
157	}
158
159	return nil
160}
161
162// Hide hides the pane while preserving web session state.
163func (fp *FloatingPane) Hide(_ context.Context) {
164	fp.mu.Lock()
165	defer fp.mu.Unlock()
166	fp.visible = false
167	fp.omniboxVisible = false
168	fp.stateVersion++
169}
170
171// Navigate loads a URL into the floating pane's persistent session.
172func (fp *FloatingPane) Navigate(ctx context.Context, url string) error {
173	url = strings.TrimSpace(url)
174	if url == "" {
175		return nil
176	}
177
178	fp.mu.RLock()
179	onNavigate := fp.onNavigate
180	fp.mu.RUnlock()
181
182	if onNavigate != nil {
183		if err := onNavigate(ctx, url); err != nil {
184			return err
185		}
186	}
187
188	fp.mu.Lock()
189	fp.currentURL = url
190	fp.sessionStarted = true
191	fp.stateVersion++
192	fp.mu.Unlock()
193
194	return nil
195}
196
197// Resize recalculates pane dimensions from parent overlay allocation.
198func (fp *FloatingPane) Resize() {
199	fp.mu.Lock()
200	defer fp.mu.Unlock()
201	fp.calculatedWidth, fp.calculatedHeight = CalculateOverlayDimensions(
202		fp.parent,
203		fp.widthPct,
204		fp.heightPct,
205		fp.fallbackWidth,
206		fp.fallbackHeight,
207	)
208}
209
210// SetParentOverlay updates the workspace overlay used for sizing calculations.
211func (fp *FloatingPane) SetParentOverlay(parent layout.OverlayWidget) {
212	fp.mu.Lock()
213	fp.parent = parent
214	fp.mu.Unlock()
215	fp.Resize()
216}
217
218// Dimensions returns the last calculated pane dimensions.
219func (fp *FloatingPane) Dimensions() (width, height int) {
220	fp.mu.RLock()
221	defer fp.mu.RUnlock()
222	return fp.calculatedWidth, fp.calculatedHeight
223}
224
225// CurrentURL returns the current URL loaded in the floating session.
226func (fp *FloatingPane) CurrentURL() string {
227	fp.mu.RLock()
228	defer fp.mu.RUnlock()
229	return fp.currentURL
230}
231
232// IsVisible reports whether the floating pane is currently visible.
233func (fp *FloatingPane) IsVisible() bool {
234	fp.mu.RLock()
235	defer fp.mu.RUnlock()
236	return fp.visible
237}
238
239// IsOmniboxVisible reports whether the floating pane omnibox is visible.
240func (fp *FloatingPane) IsOmniboxVisible() bool {
241	fp.mu.RLock()
242	defer fp.mu.RUnlock()
243	return fp.omniboxVisible
244}
245
246// SetOmniboxVisible updates omnibox visibility state for the floating pane.
247func (fp *FloatingPane) SetOmniboxVisible(visible bool) {
248	fp.mu.Lock()
249	fp.omniboxVisible = visible
250	fp.stateVersion++
251	fp.mu.Unlock()
252}
253
254// RecordLoadedURL updates floating pane URL state from external navigation events.
255func (fp *FloatingPane) RecordLoadedURL(url string) {
256	url = strings.TrimSpace(url)
257	if url == "" {
258		return
259	}
260
261	fp.mu.Lock()
262	fp.currentURL = url
263	fp.sessionStarted = true
264	fp.stateVersion++
265	fp.mu.Unlock()
266}

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