detached b9e91d6

fix: terminate WebKit web processes on pane close to prevent resource leaks

commit
b9e91d64fedd2e73811741d77650a7ad02942b20
author
brice · · 2025-12-23 22:57 UTC
committer
brice · · 2025-12-24 16:11 UTC
parent
e736791
signature
unsigned
7 files · +105 -37
M CHANGELOG.md +5 -0
@@ -5,11 +5,16 @@ All notable changes to this project will be documented in this file.
55 ## [Unreleased]
66
77 ### Fixed
8+- **WebKit process leak on pane close**: Fixed zombie WebKit web processes not being terminated when panes were closed. `WebView.Destroy()` now properly calls `TerminateWebProcess()` and unparents GTK widgets, freeing GPU resources (VA-API, DMA-BUF, GL contexts). This should significantly improve video playback stability and reduce memory usage when opening/closing many panes.
89 - **White flash on navigation**: Eliminated white page flash when navigating in dark mode by setting WebView background color to match theme and hiding WebView until content commits.
910 - **Dark mode detection on websites**: Fixed `prefers-color-scheme: dark` detection by improving matchMedia query normalization and adding proper event listener stubs. Sites like Reddit now correctly detect dark mode preference.
1011 - **Alt+number tab switching**: Fixed Alt+1-9 shortcuts not working when WebView has focus by using GtkShortcutController with global scope instead of EventControllerKey.
1112 - **System locking during fullscreen video**: Added idle inhibition via XDG Desktop Portal when WebView enters fullscreen mode. Prevents system from locking/sleeping during fullscreen video playback on Wayland (works with all compositors: GNOME, KDE, sway, hyprland, etc.).
1213
14+### Changed
15+- **GSK renderer default**: Changed default from `vulkan` to `auto` to let GTK choose the best renderer and avoid potential DMA-BUF synchronization conflicts with WebKit.
16+- **Removed invalid GStreamer config**: Removed `video_buffer_size_mb` and `queue_buffer_time_sec` config options as these were not valid GStreamer environment variables (they are element properties that cannot be set via env vars).
17+
1318 ### Added
1419 - **Clean-architecture rewrite (pure Go)**: Major refactor to a ports/adapters style architecture with explicit `domain`, `application` (use cases), `infrastructure`, and `ui` layers.
1520 - **New CLI commands**: `doctor` (runtime dependency checks), `about` (build info), `setup` (desktop integration), plus expanded `purge` with an interactive TUI.
@@ -5,11 +5,16 @@ All notable changes to this project will be documented in this file.
5 ## [Unreleased]5 ## [Unreleased]
6 6
7 ### Fixed7 ### Fixed
8+- **WebKit process leak on pane close**: Fixed zombie WebKit web processes not being terminated when panes were closed. `WebView.Destroy()` now properly calls `TerminateWebProcess()` and unparents GTK widgets, freeing GPU resources (VA-API, DMA-BUF, GL contexts). This should significantly improve video playback stability and reduce memory usage when opening/closing many panes.
8 - **White flash on navigation**: Eliminated white page flash when navigating in dark mode by setting WebView background color to match theme and hiding WebView until content commits.9 - **White flash on navigation**: Eliminated white page flash when navigating in dark mode by setting WebView background color to match theme and hiding WebView until content commits.
9 - **Dark mode detection on websites**: Fixed `prefers-color-scheme: dark` detection by improving matchMedia query normalization and adding proper event listener stubs. Sites like Reddit now correctly detect dark mode preference.10 - **Dark mode detection on websites**: Fixed `prefers-color-scheme: dark` detection by improving matchMedia query normalization and adding proper event listener stubs. Sites like Reddit now correctly detect dark mode preference.
10 - **Alt+number tab switching**: Fixed Alt+1-9 shortcuts not working when WebView has focus by using GtkShortcutController with global scope instead of EventControllerKey.11 - **Alt+number tab switching**: Fixed Alt+1-9 shortcuts not working when WebView has focus by using GtkShortcutController with global scope instead of EventControllerKey.
11 - **System locking during fullscreen video**: Added idle inhibition via XDG Desktop Portal when WebView enters fullscreen mode. Prevents system from locking/sleeping during fullscreen video playback on Wayland (works with all compositors: GNOME, KDE, sway, hyprland, etc.).12 - **System locking during fullscreen video**: Added idle inhibition via XDG Desktop Portal when WebView enters fullscreen mode. Prevents system from locking/sleeping during fullscreen video playback on Wayland (works with all compositors: GNOME, KDE, sway, hyprland, etc.).
12 13
14+### Changed
15+- **GSK renderer default**: Changed default from `vulkan` to `auto` to let GTK choose the best renderer and avoid potential DMA-BUF synchronization conflicts with WebKit.
16+- **Removed invalid GStreamer config**: Removed `video_buffer_size_mb` and `queue_buffer_time_sec` config options as these were not valid GStreamer environment variables (they are element properties that cannot be set via env vars).
17+
13 ### Added18 ### Added
14 - **Clean-architecture rewrite (pure Go)**: Major refactor to a ports/adapters style architecture with explicit `domain`, `application` (use cases), `infrastructure`, and `ui` layers.19 - **Clean-architecture rewrite (pure Go)**: Major refactor to a ports/adapters style architecture with explicit `domain`, `application` (use cases), `infrastructure`, and `ui` layers.
15 - **New CLI commands**: `doctor` (runtime dependency checks), `about` (build info), `setup` (desktop integration), plus expanded `purge` with an interactive TUI.20 - **New CLI commands**: `doctor` (runtime dependency checks), `about` (build info), `setup` (desktop integration), plus expanded `purge` with an interactive TUI.
M docs/CONFIG.md +6 -14
@@ -182,7 +182,7 @@ border = "#363636"
182182 | `rendering.disable_dmabuf_renderer` | bool | `false` | - | Disable WebKit DMA-BUF renderer (may fix flicker on Wayland; slower) |
183183 | `rendering.force_compositing_mode` | bool | `false` | - | Force WebKit compositing mode (`WEBKIT_FORCE_COMPOSITING_MODE`) |
184184 | `rendering.disable_compositing_mode` | bool | `false` | - | Disable WebKit compositing mode (`WEBKIT_DISABLE_COMPOSITING_MODE`) |
185-| `rendering.gsk_renderer` | string | `"vulkan"` | `auto`, `opengl`, `vulkan`, `cairo` | GTK renderer selection (`GSK_RENDERER`) |
185+| `rendering.gsk_renderer` | string | `"auto"` | `auto`, `opengl`, `vulkan`, `cairo` | GTK renderer selection (`GSK_RENDERER`) |
186186 | `rendering.disable_mipmaps` | bool | `false` | - | Disable GTK mipmaps (`GSK_GPU_DISABLE=mipmap`) |
187187 | `rendering.prefer_gl` | bool | `false` | - | Prefer OpenGL over GLES (`GDK_DEBUG=gl-prefer-gl`) |
188188 | `rendering.draw_compositing_indicators` | bool | `false` | - | Draw WebKit compositing indicators (debug) |
@@ -282,13 +282,11 @@ cancel = ["escape"]
282282 | Key | Type | Default | Valid Values | Description |
283283 |-----|------|---------|--------------|-------------|
284284 | `media.hardware_decoding` | string | `"auto"` | `auto`, `force`, `disable` | Hardware video decoding mode |
285-| `media.prefer_av1` | bool | `true` | - | Prefer AV1 codec when available |
286-| `media.show_diagnostics` | bool | `true` | - | Show media diagnostics warnings at startup |
285+| `media.prefer_av1` | bool | `false` | - | Prefer AV1 codec when available |
286+| `media.show_diagnostics` | bool | `false` | - | Show media diagnostics warnings at startup |
287287 | `media.force_vsync` | bool | `false` | - | Force VSync for video playback (may help with tearing) |
288288 | `media.gl_rendering_mode` | string | `"auto"` | `auto`, `gles2`, `gl3`, `none` | OpenGL API selection for video rendering |
289289 | `media.gstreamer_debug_level` | int | `0` | `0-5` | GStreamer debug verbosity (0=off) |
290-| `media.video_buffer_size_mb` | int | `64` | > 0 | Video buffer size in MB for smoother streaming |
291-| `media.queue_buffer_time_sec` | int | `20` | > 0 | Queue prebuffer time in seconds |
292290
293291 **Hardware decoding modes:**
294292 - `auto` (recommended): Hardware preferred with software fallback - fixes Twitch Error #4000
@@ -307,21 +305,15 @@ Dumber automatically detects your GPU vendor (AMD/Intel/NVIDIA) and sets optimal
307305 - **Intel**: Uses `iHD` driver (modern, for Broadwell+)
308306 - **NVIDIA**: Uses `nvidia` driver with EGL platform
309307
310-**Buffering settings:**
311-- `video_buffer_size_mb`: Controls GStreamer buffer size. Larger buffers reduce rebuffering on bursty streams (Twitch, YouTube). Uses more memory.
312-- `queue_buffer_time_sec`: Controls prebuffer duration. Higher values allow more data to be buffered ahead of playback.
313-
314308 **Example:**
315309 ```toml
316310 [media]
317311 hardware_decoding = "auto" # HW preferred, SW fallback
318-prefer_av1 = true # AV1 is most efficient codec
319-show_diagnostics = true # Log warnings if HW accel unavailable
312+prefer_av1 = false # Let site choose codec
313+show_diagnostics = false # Enable for debugging
320314 force_vsync = false # Let compositor handle VSync
321315 gl_rendering_mode = "auto" # GStreamer picks best GL API
322316 gstreamer_debug_level = 0 # Increase to 3-5 for debugging
323-video_buffer_size_mb = 64 # 64 MB buffer for smooth streaming
324-queue_buffer_time_sec = 20 # 20 seconds prebuffer
325317 ```
326318
327319 **Diagnostics CLI:**
@@ -368,7 +360,7 @@ All config values can be overridden via environment variables with the prefix `D
368360 DUMBER_DATABASE_PATH=/custom/path/db.sqlite
369361
370362 # Rendering (examples)
371-# Defaults: rendering.disable_dmabuf_renderer=false, rendering.gsk_renderer="vulkan"
363+# Defaults: rendering.disable_dmabuf_renderer=false, rendering.gsk_renderer="auto"
372364 DUMBER_RENDERING_MODE=cpu
373365 # DUMBER_RENDERING_DISABLE_DMABUF_RENDERER=true
374366 # DUMBER_RENDERING_GSK_RENDERER=opengl
@@ -182,7 +182,7 @@ border = "#363636"
182 | `rendering.disable_dmabuf_renderer` | bool | `false` | - | Disable WebKit DMA-BUF renderer (may fix flicker on Wayland; slower) |182 | `rendering.disable_dmabuf_renderer` | bool | `false` | - | Disable WebKit DMA-BUF renderer (may fix flicker on Wayland; slower) |
183 | `rendering.force_compositing_mode` | bool | `false` | - | Force WebKit compositing mode (`WEBKIT_FORCE_COMPOSITING_MODE`) |183 | `rendering.force_compositing_mode` | bool | `false` | - | Force WebKit compositing mode (`WEBKIT_FORCE_COMPOSITING_MODE`) |
184 | `rendering.disable_compositing_mode` | bool | `false` | - | Disable WebKit compositing mode (`WEBKIT_DISABLE_COMPOSITING_MODE`) |184 | `rendering.disable_compositing_mode` | bool | `false` | - | Disable WebKit compositing mode (`WEBKIT_DISABLE_COMPOSITING_MODE`) |
185-| `rendering.gsk_renderer` | string | `"vulkan"` | `auto`, `opengl`, `vulkan`, `cairo` | GTK renderer selection (`GSK_RENDERER`) |185+| `rendering.gsk_renderer` | string | `"auto"` | `auto`, `opengl`, `vulkan`, `cairo` | GTK renderer selection (`GSK_RENDERER`) |
186 | `rendering.disable_mipmaps` | bool | `false` | - | Disable GTK mipmaps (`GSK_GPU_DISABLE=mipmap`) |186 | `rendering.disable_mipmaps` | bool | `false` | - | Disable GTK mipmaps (`GSK_GPU_DISABLE=mipmap`) |
187 | `rendering.prefer_gl` | bool | `false` | - | Prefer OpenGL over GLES (`GDK_DEBUG=gl-prefer-gl`) |187 | `rendering.prefer_gl` | bool | `false` | - | Prefer OpenGL over GLES (`GDK_DEBUG=gl-prefer-gl`) |
188 | `rendering.draw_compositing_indicators` | bool | `false` | - | Draw WebKit compositing indicators (debug) |188 | `rendering.draw_compositing_indicators` | bool | `false` | - | Draw WebKit compositing indicators (debug) |
@@ -282,13 +282,11 @@ cancel = ["escape"]
282 | Key | Type | Default | Valid Values | Description |282 | Key | Type | Default | Valid Values | Description |
283 |-----|------|---------|--------------|-------------|283 |-----|------|---------|--------------|-------------|
284 | `media.hardware_decoding` | string | `"auto"` | `auto`, `force`, `disable` | Hardware video decoding mode |284 | `media.hardware_decoding` | string | `"auto"` | `auto`, `force`, `disable` | Hardware video decoding mode |
285-| `media.prefer_av1` | bool | `true` | - | Prefer AV1 codec when available |285+| `media.prefer_av1` | bool | `false` | - | Prefer AV1 codec when available |
286-| `media.show_diagnostics` | bool | `true` | - | Show media diagnostics warnings at startup |286+| `media.show_diagnostics` | bool | `false` | - | Show media diagnostics warnings at startup |
287 | `media.force_vsync` | bool | `false` | - | Force VSync for video playback (may help with tearing) |287 | `media.force_vsync` | bool | `false` | - | Force VSync for video playback (may help with tearing) |
288 | `media.gl_rendering_mode` | string | `"auto"` | `auto`, `gles2`, `gl3`, `none` | OpenGL API selection for video rendering |288 | `media.gl_rendering_mode` | string | `"auto"` | `auto`, `gles2`, `gl3`, `none` | OpenGL API selection for video rendering |
289 | `media.gstreamer_debug_level` | int | `0` | `0-5` | GStreamer debug verbosity (0=off) |289 | `media.gstreamer_debug_level` | int | `0` | `0-5` | GStreamer debug verbosity (0=off) |
290-| `media.video_buffer_size_mb` | int | `64` | > 0 | Video buffer size in MB for smoother streaming |
291-| `media.queue_buffer_time_sec` | int | `20` | > 0 | Queue prebuffer time in seconds |
292 290
293 **Hardware decoding modes:**291 **Hardware decoding modes:**
294 - `auto` (recommended): Hardware preferred with software fallback - fixes Twitch Error #4000292 - `auto` (recommended): Hardware preferred with software fallback - fixes Twitch Error #4000
@@ -307,21 +305,15 @@ Dumber automatically detects your GPU vendor (AMD/Intel/NVIDIA) and sets optimal
307 - **Intel**: Uses `iHD` driver (modern, for Broadwell+)305 - **Intel**: Uses `iHD` driver (modern, for Broadwell+)
308 - **NVIDIA**: Uses `nvidia` driver with EGL platform306 - **NVIDIA**: Uses `nvidia` driver with EGL platform
309 307
310-**Buffering settings:**
311-- `video_buffer_size_mb`: Controls GStreamer buffer size. Larger buffers reduce rebuffering on bursty streams (Twitch, YouTube). Uses more memory.
312-- `queue_buffer_time_sec`: Controls prebuffer duration. Higher values allow more data to be buffered ahead of playback.
313-
314 **Example:**308 **Example:**
315 ```toml309 ```toml
316 [media]310 [media]
317 hardware_decoding = "auto" # HW preferred, SW fallback311 hardware_decoding = "auto" # HW preferred, SW fallback
318-prefer_av1 = true # AV1 is most efficient codec312+prefer_av1 = false # Let site choose codec
319-show_diagnostics = true # Log warnings if HW accel unavailable313+show_diagnostics = false # Enable for debugging
320 force_vsync = false # Let compositor handle VSync314 force_vsync = false # Let compositor handle VSync
321 gl_rendering_mode = "auto" # GStreamer picks best GL API315 gl_rendering_mode = "auto" # GStreamer picks best GL API
322 gstreamer_debug_level = 0 # Increase to 3-5 for debugging316 gstreamer_debug_level = 0 # Increase to 3-5 for debugging
323-video_buffer_size_mb = 64 # 64 MB buffer for smooth streaming
324-queue_buffer_time_sec = 20 # 20 seconds prebuffer
325 ```317 ```
326 318
327 **Diagnostics CLI:**319 **Diagnostics CLI:**
@@ -368,7 +360,7 @@ All config values can be overridden via environment variables with the prefix `D
368 DUMBER_DATABASE_PATH=/custom/path/db.sqlite360 DUMBER_DATABASE_PATH=/custom/path/db.sqlite
369 361
370 # Rendering (examples)362 # Rendering (examples)
371-# Defaults: rendering.disable_dmabuf_renderer=false, rendering.gsk_renderer="vulkan"363+# Defaults: rendering.disable_dmabuf_renderer=false, rendering.gsk_renderer="auto"
372 DUMBER_RENDERING_MODE=cpu364 DUMBER_RENDERING_MODE=cpu
373 # DUMBER_RENDERING_DISABLE_DMABUF_RENDERER=true365 # DUMBER_RENDERING_DISABLE_DMABUF_RENDERER=true
374 # DUMBER_RENDERING_GSK_RENDERER=opengl366 # DUMBER_RENDERING_GSK_RENDERER=opengl
M internal/infrastructure/config/defaults.go +5 -5
@@ -117,7 +117,7 @@ func DefaultConfig() *Config {
117117 DisableDMABufRenderer: false,
118118 ForceCompositingMode: false,
119119 DisableCompositingMode: false,
120- GSKRenderer: GSKRendererVulkan,
120+ GSKRenderer: GSKRendererAuto, // Let GTK choose - Vulkan can conflict with WebKit's DMA-BUF
121121 DisableMipmaps: false,
122122 PreferGL: false,
123123 DrawCompositingIndicators: false,
@@ -194,13 +194,13 @@ func DefaultConfig() *Config {
194194 },
195195 Media: MediaConfig{
196196 HardwareDecodingMode: HardwareDecodingAuto, // auto allows sw fallback
197- PreferAV1: true, // AV1 is most efficient codec
198- ShowDiagnosticsOnStartup: true, // Warn users if HW accel unavailable
197+ PreferAV1: false, // Don't force codec preference, let site choose
198+ ShowDiagnosticsOnStartup: false, // Disabled - diagnostics can be noisy
199199 ForceVSync: false, // Let compositor handle VSync
200200 GLRenderingMode: GLRenderingModeAuto, // GStreamer picks best GL API
201201 GStreamerDebugLevel: 0, // Disabled by default
202- VideoBufferSizeMB: 64, // Larger buffer for bursty streams (Twitch, YouTube)
203- QueueBufferTimeSec: 20, // More prebuffering for smooth playback
202+ VideoBufferSizeMB: 0, // Not a valid GStreamer env var, removed
203+ QueueBufferTimeSec: 0, // Not a valid GStreamer env var, removed
204204 },
205205 Runtime: RuntimeConfig{
206206 Prefix: "",
@@ -117,7 +117,7 @@ func DefaultConfig() *Config {
117 DisableDMABufRenderer: false,117 DisableDMABufRenderer: false,
118 ForceCompositingMode: false,118 ForceCompositingMode: false,
119 DisableCompositingMode: false,119 DisableCompositingMode: false,
120- GSKRenderer: GSKRendererVulkan,120+ GSKRenderer: GSKRendererAuto, // Let GTK choose - Vulkan can conflict with WebKit's DMA-BUF
121 DisableMipmaps: false,121 DisableMipmaps: false,
122 PreferGL: false,122 PreferGL: false,
123 DrawCompositingIndicators: false,123 DrawCompositingIndicators: false,
@@ -194,13 +194,13 @@ func DefaultConfig() *Config {
194 },194 },
195 Media: MediaConfig{195 Media: MediaConfig{
196 HardwareDecodingMode: HardwareDecodingAuto, // auto allows sw fallback196 HardwareDecodingMode: HardwareDecodingAuto, // auto allows sw fallback
197- PreferAV1: true, // AV1 is most efficient codec197+ PreferAV1: false, // Don't force codec preference, let site choose
198- ShowDiagnosticsOnStartup: true, // Warn users if HW accel unavailable198+ ShowDiagnosticsOnStartup: false, // Disabled - diagnostics can be noisy
199 ForceVSync: false, // Let compositor handle VSync199 ForceVSync: false, // Let compositor handle VSync
200 GLRenderingMode: GLRenderingModeAuto, // GStreamer picks best GL API200 GLRenderingMode: GLRenderingModeAuto, // GStreamer picks best GL API
201 GStreamerDebugLevel: 0, // Disabled by default201 GStreamerDebugLevel: 0, // Disabled by default
202- VideoBufferSizeMB: 64, // Larger buffer for bursty streams (Twitch, YouTube)202+ VideoBufferSizeMB: 0, // Not a valid GStreamer env var, removed
203- QueueBufferTimeSec: 20, // More prebuffering for smooth playback203+ QueueBufferTimeSec: 0, // Not a valid GStreamer env var, removed
204 },204 },
205 Runtime: RuntimeConfig{205 Runtime: RuntimeConfig{
206 Prefix: "",206 Prefix: "",
M internal/infrastructure/env/manager.go +4 -12
@@ -91,18 +91,10 @@ func (m *Manager) applyGStreamerEnv(settings port.RenderingEnvSettings) {
9191 m.setEnv("GST_DEBUG", fmt.Sprintf("%d", level))
9292 }
9393
94- // Video buffer size
95- if settings.VideoBufferSizeMB > 0 {
96- bufferBytes := settings.VideoBufferSizeMB * 1024 * 1024
97- m.setEnv("GST_BUFFER_SIZE", fmt.Sprintf("%d", bufferBytes))
98- m.setEnv("GST_QUEUE2_MAX_SIZE_BYTES", fmt.Sprintf("%d", bufferBytes))
99- }
100-
101- // Queue buffer time
102- if settings.QueueBufferTimeSec > 0 {
103- bufferTimeNs := settings.QueueBufferTimeSec * 1_000_000_000
104- m.setEnv("GST_QUEUE2_MAX_SIZE_TIME", fmt.Sprintf("%d", bufferTimeNs))
105- }
94+ // NOTE: GST_BUFFER_SIZE, GST_QUEUE2_MAX_SIZE_BYTES, GST_QUEUE2_MAX_SIZE_TIME
95+ // are NOT valid GStreamer environment variables. They are element properties
96+ // that must be set programmatically on individual GStreamer elements.
97+ // WebKitGTK manages its own GStreamer pipeline internally.
10698 }
10799
108100 func (m *Manager) applyGPUSpecificEnv() {
@@ -91,18 +91,10 @@ func (m *Manager) applyGStreamerEnv(settings port.RenderingEnvSettings) {
91 m.setEnv("GST_DEBUG", fmt.Sprintf("%d", level))91 m.setEnv("GST_DEBUG", fmt.Sprintf("%d", level))
92 }92 }
93 93
94- // Video buffer size94+ // NOTE: GST_BUFFER_SIZE, GST_QUEUE2_MAX_SIZE_BYTES, GST_QUEUE2_MAX_SIZE_TIME
95- if settings.VideoBufferSizeMB > 0 {95+ // are NOT valid GStreamer environment variables. They are element properties
96- bufferBytes := settings.VideoBufferSizeMB * 1024 * 102496+ // that must be set programmatically on individual GStreamer elements.
97- m.setEnv("GST_BUFFER_SIZE", fmt.Sprintf("%d", bufferBytes))97+ // WebKitGTK manages its own GStreamer pipeline internally.
98- m.setEnv("GST_QUEUE2_MAX_SIZE_BYTES", fmt.Sprintf("%d", bufferBytes))
99- }
100-
101- // Queue buffer time
102- if settings.QueueBufferTimeSec > 0 {
103- bufferTimeNs := settings.QueueBufferTimeSec * 1_000_000_000
104- m.setEnv("GST_QUEUE2_MAX_SIZE_TIME", fmt.Sprintf("%d", bufferTimeNs))
105- }
106 }98 }
107 99
108 func (m *Manager) applyGPUSpecificEnv() {100 func (m *Manager) applyGPUSpecificEnv() {
M internal/infrastructure/webkit/webview.go +41 -4
@@ -856,19 +856,56 @@ func (wv *WebView) DisconnectSignals() {
856856 wv.logger.Debug().Uint64("id", uint64(wv.id)).Msg("signals disconnected")
857857 }
858858
859-// Destroy cleans up the WebView resources.
859+// Destroy cleans up the WebView resources and terminates the web process.
860+// This must be called when a WebView is no longer needed to free GPU resources,
861+// VA-API decoder contexts, and DMA-BUF buffers held by the web process.
860862 func (wv *WebView) Destroy() {
861863 if wv.destroyed.Swap(true) {
862864 return // Already destroyed
863865 }
864866
865- // Disconnect all signal handlers first
867+ // 1. Disconnect all signal handlers first to prevent callbacks during cleanup
866868 wv.DisconnectSignals()
867869
868- // Unregister from global registry
870+ // 2. Clear all callbacks to prevent use-after-free
871+ wv.OnLoadChanged = nil
872+ wv.OnTitleChanged = nil
873+ wv.OnURIChanged = nil
874+ wv.OnProgressChanged = nil
875+ wv.OnFaviconChanged = nil
876+ wv.OnClose = nil
877+ wv.OnCreate = nil
878+ wv.OnReadyToShow = nil
879+ wv.OnLinkMiddleClick = nil
880+ wv.OnEnterFullscreen = nil
881+ wv.OnLeaveFullscreen = nil
882+
883+ // 3. Clear async callback references
884+ wv.mu.Lock()
885+ wv.asyncCallbacks = nil
886+ wv.mu.Unlock()
887+
888+ // 4. Unparent from GTK hierarchy (must happen before process termination)
889+ // This releases the widget from any parent container.
890+ if wv.inner != nil {
891+ wv.inner.Unparent()
892+ }
893+
894+ // 5. Terminate the web process to free GPU resources (VA-API, DMA-BUF, GL contexts)
895+ // This is critical to prevent zombie processes that hold video decoder resources.
896+ if wv.inner != nil {
897+ wv.inner.TerminateWebProcess()
898+ }
899+
900+ // 6. Unregister from global registry
869901 globalRegistry.unregister(wv.id)
870902
871- wv.logger.Debug().Uint64("id", uint64(wv.id)).Msg("webview destroyed")
903+ // 7. Clear internal references to allow GC
904+ wv.inner = nil
905+ wv.ucm = nil
906+ wv.findController = nil
907+
908+ wv.logger.Debug().Uint64("id", uint64(wv.id)).Msg("webview destroyed and web process terminated")
872909 }
873910
874911 // RunJavaScript executes script in the specified world (empty for main world).
@@ -856,19 +856,56 @@ func (wv *WebView) DisconnectSignals() {
856 wv.logger.Debug().Uint64("id", uint64(wv.id)).Msg("signals disconnected")856 wv.logger.Debug().Uint64("id", uint64(wv.id)).Msg("signals disconnected")
857 }857 }
858 858
859-// Destroy cleans up the WebView resources.859+// Destroy cleans up the WebView resources and terminates the web process.
860+// This must be called when a WebView is no longer needed to free GPU resources,
861+// VA-API decoder contexts, and DMA-BUF buffers held by the web process.
860 func (wv *WebView) Destroy() {862 func (wv *WebView) Destroy() {
861 if wv.destroyed.Swap(true) {863 if wv.destroyed.Swap(true) {
862 return // Already destroyed864 return // Already destroyed
863 }865 }
864 866
865- // Disconnect all signal handlers first867+ // 1. Disconnect all signal handlers first to prevent callbacks during cleanup
866 wv.DisconnectSignals()868 wv.DisconnectSignals()
867 869
868- // Unregister from global registry870+ // 2. Clear all callbacks to prevent use-after-free
871+ wv.OnLoadChanged = nil
872+ wv.OnTitleChanged = nil
873+ wv.OnURIChanged = nil
874+ wv.OnProgressChanged = nil
875+ wv.OnFaviconChanged = nil
876+ wv.OnClose = nil
877+ wv.OnCreate = nil
878+ wv.OnReadyToShow = nil
879+ wv.OnLinkMiddleClick = nil
880+ wv.OnEnterFullscreen = nil
881+ wv.OnLeaveFullscreen = nil
882+
883+ // 3. Clear async callback references
884+ wv.mu.Lock()
885+ wv.asyncCallbacks = nil
886+ wv.mu.Unlock()
887+
888+ // 4. Unparent from GTK hierarchy (must happen before process termination)
889+ // This releases the widget from any parent container.
890+ if wv.inner != nil {
891+ wv.inner.Unparent()
892+ }
893+
894+ // 5. Terminate the web process to free GPU resources (VA-API, DMA-BUF, GL contexts)
895+ // This is critical to prevent zombie processes that hold video decoder resources.
896+ if wv.inner != nil {
897+ wv.inner.TerminateWebProcess()
898+ }
899+
900+ // 6. Unregister from global registry
869 globalRegistry.unregister(wv.id)901 globalRegistry.unregister(wv.id)
870 902
871- wv.logger.Debug().Uint64("id", uint64(wv.id)).Msg("webview destroyed")903+ // 7. Clear internal references to allow GC
904+ wv.inner = nil
905+ wv.ucm = nil
906+ wv.findController = nil
907+
908+ wv.logger.Debug().Uint64("id", uint64(wv.id)).Msg("webview destroyed and web process terminated")
872 }909 }
873 910
874 // RunJavaScript executes script in the specified world (empty for main world).911 // RunJavaScript executes script in the specified world (empty for main world).
M internal/ui/component/pane_view.go +35 -0
@@ -349,3 +349,38 @@ func (pv *PaneView) ShowZoomToast(ctx context.Context, zoomPercent int) {
349349
350350 t.ShowZoom(ctx, zoomPercent)
351351 }
352+
353+// Cleanup removes the WebView widget from the overlay and clears references.
354+// This must be called before destroying the WebView to ensure proper GTK cleanup.
355+// After calling Cleanup, the PaneView should not be reused.
356+func (pv *PaneView) Cleanup() {
357+ pv.mu.Lock()
358+ defer pv.mu.Unlock()
359+
360+ // Clear callbacks to prevent use-after-free
361+ pv.onFocusIn = nil
362+ pv.onFocusOut = nil
363+ pv.onHover = nil
364+
365+ // Detach hover handler if present
366+ if pv.hoverHandler != nil {
367+ pv.hoverHandler.Detach()
368+ pv.hoverHandler = nil
369+ }
370+
371+ // Remove WebView from overlay (unparents it from GTK hierarchy)
372+ if pv.webViewWidget != nil {
373+ pv.overlay.SetChild(nil)
374+ pv.webViewWidget = nil
375+ }
376+
377+ // Clear other overlay children
378+ if pv.progressBar != nil {
379+ pv.overlay.RemoveOverlay(pv.progressBar.Widget())
380+ pv.progressBar = nil
381+ }
382+ if pv.toaster != nil {
383+ pv.overlay.RemoveOverlay(pv.toaster.Widget())
384+ pv.toaster = nil
385+ }
386+}
@@ -349,3 +349,38 @@ func (pv *PaneView) ShowZoomToast(ctx context.Context, zoomPercent int) {
349 349
350 t.ShowZoom(ctx, zoomPercent)350 t.ShowZoom(ctx, zoomPercent)
351 }351 }
352+
353+// Cleanup removes the WebView widget from the overlay and clears references.
354+// This must be called before destroying the WebView to ensure proper GTK cleanup.
355+// After calling Cleanup, the PaneView should not be reused.
356+func (pv *PaneView) Cleanup() {
357+ pv.mu.Lock()
358+ defer pv.mu.Unlock()
359+
360+ // Clear callbacks to prevent use-after-free
361+ pv.onFocusIn = nil
362+ pv.onFocusOut = nil
363+ pv.onHover = nil
364+
365+ // Detach hover handler if present
366+ if pv.hoverHandler != nil {
367+ pv.hoverHandler.Detach()
368+ pv.hoverHandler = nil
369+ }
370+
371+ // Remove WebView from overlay (unparents it from GTK hierarchy)
372+ if pv.webViewWidget != nil {
373+ pv.overlay.SetChild(nil)
374+ pv.webViewWidget = nil
375+ }
376+
377+ // Clear other overlay children
378+ if pv.progressBar != nil {
379+ pv.overlay.RemoveOverlay(pv.progressBar.Widget())
380+ pv.progressBar = nil
381+ }
382+ if pv.toaster != nil {
383+ pv.overlay.RemoveOverlay(pv.toaster.Widget())
384+ pv.toaster = nil
385+ }
386+}
M internal/ui/component/workspace_view.go +9 -2
@@ -379,12 +379,19 @@ func (wv *WorkspaceView) RegisterPaneView(paneID entity.PaneID, pv *PaneView) {
379379 wv.updateSinglePaneModeInternal()
380380 }
381381
382-// UnregisterPaneView removes a PaneView from the tracking map.
383-// Use this when closing a pane incrementally.
382+// UnregisterPaneView removes a PaneView from the tracking map and cleans it up.
383+// This must be called when closing a pane to properly release GTK resources
384+// before destroying the WebView.
384385 func (wv *WorkspaceView) UnregisterPaneView(paneID entity.PaneID) {
385386 wv.mu.Lock()
386387 defer wv.mu.Unlock()
387388
389+ // Call Cleanup on the PaneView to unparent widgets and clear callbacks
390+ // This must happen before WebView.Destroy() is called
391+ if pv, ok := wv.paneViews[paneID]; ok && pv != nil {
392+ pv.Cleanup()
393+ }
394+
388395 delete(wv.paneViews, paneID)
389396 wv.updateSinglePaneModeInternal()
390397 }
@@ -379,12 +379,19 @@ func (wv *WorkspaceView) RegisterPaneView(paneID entity.PaneID, pv *PaneView) {
379 wv.updateSinglePaneModeInternal()379 wv.updateSinglePaneModeInternal()
380 }380 }
381 381
382-// UnregisterPaneView removes a PaneView from the tracking map.382+// UnregisterPaneView removes a PaneView from the tracking map and cleans it up.
383-// Use this when closing a pane incrementally.383+// This must be called when closing a pane to properly release GTK resources
384+// before destroying the WebView.
384 func (wv *WorkspaceView) UnregisterPaneView(paneID entity.PaneID) {385 func (wv *WorkspaceView) UnregisterPaneView(paneID entity.PaneID) {
385 wv.mu.Lock()386 wv.mu.Lock()
386 defer wv.mu.Unlock()387 defer wv.mu.Unlock()
387 388
389+ // Call Cleanup on the PaneView to unparent widgets and clear callbacks
390+ // This must happen before WebView.Destroy() is called
391+ if pv, ok := wv.paneViews[paneID]; ok && pv != nil {
392+ pv.Cleanup()
393+ }
394+
388 delete(wv.paneViews, paneID)395 delete(wv.paneViews, paneID)
389 wv.updateSinglePaneModeInternal()396 wv.updateSinglePaneModeInternal()
390 }397 }

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