detached 4e82c5d

Merge pull request #48 from bnema/fix/fullscreen-tabbar-progress-timeout

fix(ui): fullscreen tab bar visibility and progress bar timeout
commit
4e82c5da8a3c39170a75ae13606efb6e022e31e8
author
bnema · · 2025-12-27 22:58 UTC
committer
GitHub · · 2025-12-27 22:58 UTC
merge of
d3cff9d + f5cc89d · diff against the first parent
signature
GPG UnverifiedB5690EEEBB952194 The key is unknown
4 files · +75 -1
M CHANGELOG.md +2 -0
@@ -35,6 +35,8 @@ All notable changes to this project will be documented in this file.
3535 - **Console logging**: Standardized console timestamps to `HH:MM:SS` across bootstrap and session logging.
3636
3737 ### Fixed
38+- **Fullscreen video tab bar**: Fixed tab bar remaining visible during fullscreen video playback. Now hides automatically when entering fullscreen and restores based on normal visibility logic when exiting.
39+- **Progress bar stuck state**: Added 30-second timeout to progress bar to auto-hide if page load stalls and never completes.
3840 - **OAuth popup login**: Fixed parent page going blank after OAuth popup closes (e.g., Google login on claude.ai, notion.com). Related WebViews share a web process with their parent; destroying the popup was terminating the shared process, killing the parent. Now skips `TerminateWebProcess()` for popup WebViews.
3941 - Startup: defer WebView pool prewarm until after initial tab creation to reduce cold-start navigation latency.
4042 - README CLI examples: updated to match current commands/flags (`purge --force` only, `logs` usage, `sessions list --limit`, removed deprecated `--dmenu` root flag).
@@ -35,6 +35,8 @@ All notable changes to this project will be documented in this file.
35 - **Console logging**: Standardized console timestamps to `HH:MM:SS` across bootstrap and session logging.35 - **Console logging**: Standardized console timestamps to `HH:MM:SS` across bootstrap and session logging.
36 36
37 ### Fixed37 ### Fixed
38+- **Fullscreen video tab bar**: Fixed tab bar remaining visible during fullscreen video playback. Now hides automatically when entering fullscreen and restores based on normal visibility logic when exiting.
39+- **Progress bar stuck state**: Added 30-second timeout to progress bar to auto-hide if page load stalls and never completes.
38 - **OAuth popup login**: Fixed parent page going blank after OAuth popup closes (e.g., Google login on claude.ai, notion.com). Related WebViews share a web process with their parent; destroying the popup was terminating the shared process, killing the parent. Now skips `TerminateWebProcess()` for popup WebViews.40 - **OAuth popup login**: Fixed parent page going blank after OAuth popup closes (e.g., Google login on claude.ai, notion.com). Related WebViews share a web process with their parent; destroying the popup was terminating the shared process, killing the parent. Now skips `TerminateWebProcess()` for popup WebViews.
39 - Startup: defer WebView pool prewarm until after initial tab creation to reduce cold-start navigation latency.41 - Startup: defer WebView pool prewarm until after initial tab creation to reduce cold-start navigation latency.
40 - README CLI examples: updated to match current commands/flags (`purge --force` only, `logs` usage, `sessions list --limit`, removed deprecated `--dmenu` root flag).42 - README CLI examples: updated to match current commands/flags (`purge --force` only, `logs` usage, `sessions list --limit`, removed deprecated `--dmenu` root flag).
M internal/ui/app.go +14 -0
@@ -1035,6 +1035,20 @@ func (a *App) initCoordinators(ctx context.Context) {
10351035 })
10361036 }
10371037
1038+ // Set fullscreen callback to hide/show tab bar (after tabCoord is initialized)
1039+ a.contentCoord.SetOnFullscreenChanged(func(entering bool) {
1040+ if a.mainWindow == nil || a.mainWindow.TabBar() == nil {
1041+ return
1042+ }
1043+ if entering {
1044+ // Hide tab bar when entering fullscreen video
1045+ a.mainWindow.TabBar().SetVisible(false)
1046+ } else {
1047+ // Restore tab bar visibility based on normal logic
1048+ a.tabCoord.UpdateBarVisibility(ctx)
1049+ }
1050+ })
1051+
10381052 // 3. Workspace Coordinator
10391053 a.wsCoord = coordinator.NewWorkspaceCoordinator(ctx, coordinator.WorkspaceCoordinatorConfig{
10401054 PanesUC: a.panesUC,
@@ -1035,6 +1035,20 @@ func (a *App) initCoordinators(ctx context.Context) {
1035 })1035 })
1036 }1036 }
1037 1037
1038+ // Set fullscreen callback to hide/show tab bar (after tabCoord is initialized)
1039+ a.contentCoord.SetOnFullscreenChanged(func(entering bool) {
1040+ if a.mainWindow == nil || a.mainWindow.TabBar() == nil {
1041+ return
1042+ }
1043+ if entering {
1044+ // Hide tab bar when entering fullscreen video
1045+ a.mainWindow.TabBar().SetVisible(false)
1046+ } else {
1047+ // Restore tab bar visibility based on normal logic
1048+ a.tabCoord.UpdateBarVisibility(ctx)
1049+ }
1050+ })
1051+
1038 // 3. Workspace Coordinator1052 // 3. Workspace Coordinator
1039 a.wsCoord = coordinator.NewWorkspaceCoordinator(ctx, coordinator.WorkspaceCoordinatorConfig{1053 a.wsCoord = coordinator.NewWorkspaceCoordinator(ctx, coordinator.WorkspaceCoordinatorConfig{
1040 PanesUC: a.panesUC,1054 PanesUC: a.panesUC,
M internal/ui/component/progress_bar.go +45 -1
@@ -15,11 +15,14 @@ const (
1515 progressStep = 0.02
1616 // Animation interval in milliseconds
1717 progressIntervalMs = 16 // ~60fps
18+ // Timeout to auto-hide progress bar if stuck (30 seconds)
19+ progressTimeoutMs = 30000
1820 )
1921
2022 // ProgressBar displays a slim loading progress indicator at the bottom of a pane.
2123 // Uses native GtkProgressBar with "osd" styling for overlay appearance.
2224 // Implements smooth animation by incrementing towards the target value.
25+// Includes a 30-second timeout to auto-hide if the page load stalls.
2326 type ProgressBar struct {
2427 progressBar layout.ProgressBarWidget
2528
@@ -27,6 +30,7 @@ type ProgressBar struct {
2730 currentValue float64 // Current displayed value
2831 targetValue float64 // Target value to animate towards
2932 animationTimer uint // Timer source ID for animation
33+ timeoutTimer uint // Timer source ID for auto-hide timeout
3034
3135 mu sync.Mutex
3236 }
@@ -127,7 +131,7 @@ func (pb *ProgressBar) startAnimation() {
127131 pb.animationTimer = glib.TimeoutAdd(progressIntervalMs, &cb, 0)
128132 }
129133
130-// Show makes the progress bar visible.
134+// Show makes the progress bar visible and starts the auto-hide timeout.
131135 func (pb *ProgressBar) Show() {
132136 pb.mu.Lock()
133137 defer pb.mu.Unlock()
@@ -136,6 +140,34 @@ func (pb *ProgressBar) Show() {
136140 pb.visible = true
137141 pb.progressBar.SetVisible(true)
138142 }
143+
144+ // Reset timeout timer on every Show call
145+ pb.resetTimeout()
146+}
147+
148+// resetTimeout cancels any existing timeout and starts a new one.
149+// Must be called with lock held.
150+func (pb *ProgressBar) resetTimeout() {
151+ // Cancel existing timeout
152+ if pb.timeoutTimer != 0 {
153+ glib.SourceRemove(pb.timeoutTimer)
154+ pb.timeoutTimer = 0
155+ }
156+
157+ // Start new timeout
158+ cb := glib.SourceFunc(func(_ uintptr) bool {
159+ pb.mu.Lock()
160+ defer pb.mu.Unlock()
161+
162+ // Clear timer ID (timer is being removed)
163+ pb.timeoutTimer = 0
164+
165+ // Hide the progress bar inline to avoid race condition
166+ pb.hideInternal()
167+ return false // Don't repeat
168+ })
169+
170+ pb.timeoutTimer = glib.TimeoutAdd(progressTimeoutMs, &cb, 0)
139171 }
140172
141173 // Hide makes the progress bar invisible and resets state.
@@ -143,6 +175,12 @@ func (pb *ProgressBar) Hide() {
143175 pb.mu.Lock()
144176 defer pb.mu.Unlock()
145177
178+ pb.hideInternal()
179+}
180+
181+// hideInternal performs the actual hide operation.
182+// Must be called with lock held.
183+func (pb *ProgressBar) hideInternal() {
146184 if pb.visible {
147185 pb.visible = false
148186 pb.progressBar.SetVisible(false)
@@ -153,6 +191,12 @@ func (pb *ProgressBar) Hide() {
153191 pb.animationTimer = 0
154192 }
155193
194+ // Stop timeout timer
195+ if pb.timeoutTimer != 0 {
196+ glib.SourceRemove(pb.timeoutTimer)
197+ pb.timeoutTimer = 0
198+ }
199+
156200 // Reset values
157201 pb.currentValue = 0
158202 pb.targetValue = 0
@@ -15,11 +15,14 @@ const (
15 progressStep = 0.0215 progressStep = 0.02
16 // Animation interval in milliseconds16 // Animation interval in milliseconds
17 progressIntervalMs = 16 // ~60fps17 progressIntervalMs = 16 // ~60fps
18+ // Timeout to auto-hide progress bar if stuck (30 seconds)
19+ progressTimeoutMs = 30000
18 )20 )
19 21
20 // ProgressBar displays a slim loading progress indicator at the bottom of a pane.22 // ProgressBar displays a slim loading progress indicator at the bottom of a pane.
21 // Uses native GtkProgressBar with "osd" styling for overlay appearance.23 // Uses native GtkProgressBar with "osd" styling for overlay appearance.
22 // Implements smooth animation by incrementing towards the target value.24 // Implements smooth animation by incrementing towards the target value.
25+// Includes a 30-second timeout to auto-hide if the page load stalls.
23 type ProgressBar struct {26 type ProgressBar struct {
24 progressBar layout.ProgressBarWidget27 progressBar layout.ProgressBarWidget
25 28
@@ -27,6 +30,7 @@ type ProgressBar struct {
27 currentValue float64 // Current displayed value30 currentValue float64 // Current displayed value
28 targetValue float64 // Target value to animate towards31 targetValue float64 // Target value to animate towards
29 animationTimer uint // Timer source ID for animation32 animationTimer uint // Timer source ID for animation
33+ timeoutTimer uint // Timer source ID for auto-hide timeout
30 34
31 mu sync.Mutex35 mu sync.Mutex
32 }36 }
@@ -127,7 +131,7 @@ func (pb *ProgressBar) startAnimation() {
127 pb.animationTimer = glib.TimeoutAdd(progressIntervalMs, &cb, 0)131 pb.animationTimer = glib.TimeoutAdd(progressIntervalMs, &cb, 0)
128 }132 }
129 133
130-// Show makes the progress bar visible.134+// Show makes the progress bar visible and starts the auto-hide timeout.
131 func (pb *ProgressBar) Show() {135 func (pb *ProgressBar) Show() {
132 pb.mu.Lock()136 pb.mu.Lock()
133 defer pb.mu.Unlock()137 defer pb.mu.Unlock()
@@ -136,6 +140,34 @@ func (pb *ProgressBar) Show() {
136 pb.visible = true140 pb.visible = true
137 pb.progressBar.SetVisible(true)141 pb.progressBar.SetVisible(true)
138 }142 }
143+
144+ // Reset timeout timer on every Show call
145+ pb.resetTimeout()
146+}
147+
148+// resetTimeout cancels any existing timeout and starts a new one.
149+// Must be called with lock held.
150+func (pb *ProgressBar) resetTimeout() {
151+ // Cancel existing timeout
152+ if pb.timeoutTimer != 0 {
153+ glib.SourceRemove(pb.timeoutTimer)
154+ pb.timeoutTimer = 0
155+ }
156+
157+ // Start new timeout
158+ cb := glib.SourceFunc(func(_ uintptr) bool {
159+ pb.mu.Lock()
160+ defer pb.mu.Unlock()
161+
162+ // Clear timer ID (timer is being removed)
163+ pb.timeoutTimer = 0
164+
165+ // Hide the progress bar inline to avoid race condition
166+ pb.hideInternal()
167+ return false // Don't repeat
168+ })
169+
170+ pb.timeoutTimer = glib.TimeoutAdd(progressTimeoutMs, &cb, 0)
139 }171 }
140 172
141 // Hide makes the progress bar invisible and resets state.173 // Hide makes the progress bar invisible and resets state.
@@ -143,6 +175,12 @@ func (pb *ProgressBar) Hide() {
143 pb.mu.Lock()175 pb.mu.Lock()
144 defer pb.mu.Unlock()176 defer pb.mu.Unlock()
145 177
178+ pb.hideInternal()
179+}
180+
181+// hideInternal performs the actual hide operation.
182+// Must be called with lock held.
183+func (pb *ProgressBar) hideInternal() {
146 if pb.visible {184 if pb.visible {
147 pb.visible = false185 pb.visible = false
148 pb.progressBar.SetVisible(false)186 pb.progressBar.SetVisible(false)
@@ -153,6 +191,12 @@ func (pb *ProgressBar) Hide() {
153 pb.animationTimer = 0191 pb.animationTimer = 0
154 }192 }
155 193
194+ // Stop timeout timer
195+ if pb.timeoutTimer != 0 {
196+ glib.SourceRemove(pb.timeoutTimer)
197+ pb.timeoutTimer = 0
198+ }
199+
156 // Reset values200 // Reset values
157 pb.currentValue = 0201 pb.currentValue = 0
158 pb.targetValue = 0202 pb.targetValue = 0
M internal/ui/coordinator/content.go +14 -0
@@ -75,6 +75,9 @@ type ContentCoordinator struct {
7575
7676 // Idle inhibitor for fullscreen video playback
7777 idleInhibitor port.IdleInhibitor
78+
79+ // Callback when fullscreen state changes (for hiding/showing tab bar)
80+ onFullscreenChanged func(entering bool)
7881 }
7982
8083 // NewContentCoordinator creates a new ContentCoordinator.
@@ -137,6 +140,11 @@ func (c *ContentCoordinator) SetIdleInhibitor(inhibitor port.IdleInhibitor) {
137140 c.idleInhibitor = inhibitor
138141 }
139142
143+// SetOnFullscreenChanged sets the callback for fullscreen state changes.
144+func (c *ContentCoordinator) SetOnFullscreenChanged(fn func(entering bool)) {
145+ c.onFullscreenChanged = fn
146+}
147+
140148 // EnsureWebView acquires or reuses a WebView for the given pane.
141149 func (c *ContentCoordinator) EnsureWebView(ctx context.Context, paneID entity.PaneID) (*webkit.WebView, error) {
142150 log := logging.FromContext(ctx)
@@ -1289,6 +1297,9 @@ func (c *ContentCoordinator) setupIdleInhibitionHandlers(ctx context.Context, pa
12891297 log.Warn().Err(err).Str("pane_id", string(paneID)).Msg("failed to inhibit idle")
12901298 }
12911299 }
1300+ if c.onFullscreenChanged != nil {
1301+ c.onFullscreenChanged(true)
1302+ }
12921303 return false // Allow fullscreen
12931304 }
12941305
@@ -1298,6 +1309,9 @@ func (c *ContentCoordinator) setupIdleInhibitionHandlers(ctx context.Context, pa
12981309 log.Warn().Err(err).Str("pane_id", string(paneID)).Msg("failed to uninhibit idle")
12991310 }
13001311 }
1312+ if c.onFullscreenChanged != nil {
1313+ c.onFullscreenChanged(false)
1314+ }
13011315 return false // Allow leaving fullscreen
13021316 }
13031317
@@ -75,6 +75,9 @@ type ContentCoordinator struct {
75 75
76 // Idle inhibitor for fullscreen video playback76 // Idle inhibitor for fullscreen video playback
77 idleInhibitor port.IdleInhibitor77 idleInhibitor port.IdleInhibitor
78+
79+ // Callback when fullscreen state changes (for hiding/showing tab bar)
80+ onFullscreenChanged func(entering bool)
78 }81 }
79 82
80 // NewContentCoordinator creates a new ContentCoordinator.83 // NewContentCoordinator creates a new ContentCoordinator.
@@ -137,6 +140,11 @@ func (c *ContentCoordinator) SetIdleInhibitor(inhibitor port.IdleInhibitor) {
137 c.idleInhibitor = inhibitor140 c.idleInhibitor = inhibitor
138 }141 }
139 142
143+// SetOnFullscreenChanged sets the callback for fullscreen state changes.
144+func (c *ContentCoordinator) SetOnFullscreenChanged(fn func(entering bool)) {
145+ c.onFullscreenChanged = fn
146+}
147+
140 // EnsureWebView acquires or reuses a WebView for the given pane.148 // EnsureWebView acquires or reuses a WebView for the given pane.
141 func (c *ContentCoordinator) EnsureWebView(ctx context.Context, paneID entity.PaneID) (*webkit.WebView, error) {149 func (c *ContentCoordinator) EnsureWebView(ctx context.Context, paneID entity.PaneID) (*webkit.WebView, error) {
142 log := logging.FromContext(ctx)150 log := logging.FromContext(ctx)
@@ -1289,6 +1297,9 @@ func (c *ContentCoordinator) setupIdleInhibitionHandlers(ctx context.Context, pa
1289 log.Warn().Err(err).Str("pane_id", string(paneID)).Msg("failed to inhibit idle")1297 log.Warn().Err(err).Str("pane_id", string(paneID)).Msg("failed to inhibit idle")
1290 }1298 }
1291 }1299 }
1300+ if c.onFullscreenChanged != nil {
1301+ c.onFullscreenChanged(true)
1302+ }
1292 return false // Allow fullscreen1303 return false // Allow fullscreen
1293 }1304 }
1294 1305
@@ -1298,6 +1309,9 @@ func (c *ContentCoordinator) setupIdleInhibitionHandlers(ctx context.Context, pa
1298 log.Warn().Err(err).Str("pane_id", string(paneID)).Msg("failed to uninhibit idle")1309 log.Warn().Err(err).Str("pane_id", string(paneID)).Msg("failed to uninhibit idle")
1299 }1310 }
1300 }1311 }
1312+ if c.onFullscreenChanged != nil {
1313+ c.onFullscreenChanged(false)
1314+ }
1301 return false // Allow leaving fullscreen1315 return false // Allow leaving fullscreen
1302 }1316 }
1303 1317

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