detached 8320306

Merge: 004-migrate-to-webkitgtk (WebKitGTK 6 migration, UCM, cookies/cache, theme sync, rendering mode)

commit
8320306012084a596934cf3a455484672765b12e
author
Brice · · 2025-09-09 23:12 UTC
merge of
bf7c821 + 2045a0e · diff against the first parent
signature
unsigned
26 files · +1845 -198
M CLAUDE.md +27 -9
@@ -3,8 +3,9 @@
33 Auto-generated from all feature plans. Last updated: 2025-01-21
44
55 ## Active Technologies
6-- Go 1.25.1 + WebKit2GTK (native), Cobra, Viper, SQLite3 (003-webkit2gtk-migration)
7-- CGO bindings for WebKit2GTK C API integration
6+- Go 1.25.1 + WebKitGTK 6.0 (GTK4), Cobra, Viper, SQLite3 (004-migrate-to-webkitgtk)
7+- CGO bindings for WebKitGTK 6.0 C API integration
8+- GPU rendering via Vulkan/OpenGL with configurable modes
89
910 ## Project Structure
1011 ```
@@ -51,14 +52,31 @@ Go 1.25.1: Follow standard Go conventions with gofmt
5152 - Constitutional simplicity principles
5253
5354 ## Recent Changes
54-- 003-webkit2gtk-migration: Migrating from Wails to native WebKit2GTK with custom CGO bindings
55+- 004-migrate-to-webkitgtk: Migrating to WebKitGTK 6.0 (GTK4) for Vulkan GPU rendering support (UCM restored, GTK4 input, persistent cookies/cache)
56+- 003-webkit2gtk-migration: Migrated from Wails to native WebKit2GTK with custom CGO bindings
5557 - 002-browser-controls-ui: Added keyboard/mouse controls, zoom persistence, clipboard integration
5658
57-## WebKit2GTK Integration
58-- Reference repository: /home/brice/dev/clone/webkit2gtk-reference
59-- pkg/webkit: Reusable WebKit2GTK Go bindings
60-- Direct GTK event handling for keyboard shortcuts
61-- WebKitUserContentManager for script injection
59+## WebKitGTK 6.0 Integration
60+- pkg/webkit: Reusable WebKitGTK 6.0 Go bindings (GTK4)
61+- Hardware acceleration via Vulkan/OpenGL with configurable rendering modes
62+- Direct GTK4 event handling with GtkEventController
63+- WebKitNetworkSession replaces WebKitWebContext
64+- Rendering modes: auto (default), gpu, cpu via --rendering-mode flag
6265
6366 <!-- MANUAL ADDITIONS START -->
64-<!-- MANUAL ADDITIONS END -->⏎̸
67+### Migration Status (004-migrate-to-webkitgtk)
68+- WebView construction via `g_object_new(WEBKIT_TYPE_WEB_VIEW, …)` (GTK4).
69+- Native UCM messaging (script-message-received::dumber) replaces fallback bridge.
70+- Persistent storage: cookies (SQLite) and cache via `WebKitNetworkSession`.
71+- Runtime theme sync: listens to GTK `gtk-application-prefer-dark-theme` and updates page color-scheme live.
72+- Native zoom shortcuts + GTK4 key/mouse controllers (including AZERTY fix for zoom-out).
73+- Media: GStreamer required; default “safe” startup demotes VAAPI and uses a safe sink to avoid crashes (can be disabled).
74+
75+### GPU Rendering
76+- GTK4 renderer can use Vulkan; WebKit settings map `auto|gpu|cpu` to the appropriate hardware acceleration policy where available.
77+- Status endpoint: `dumb://homepage/api/rendering/status` reports current configured mode.
78+
79+### Next
80+- Expose cookie policy/media mode toggles in config/CLI.
81+- “Clear browsing data” surfaced via CookieManager/WebsiteDataManager.
82+<!-- MANUAL ADDITIONS END -->
@@ -3,8 +3,9 @@
3 Auto-generated from all feature plans. Last updated: 2025-01-213 Auto-generated from all feature plans. Last updated: 2025-01-21
4 4
5 ## Active Technologies 5 ## Active Technologies
6-- Go 1.25.1 + WebKit2GTK (native), Cobra, Viper, SQLite3 (003-webkit2gtk-migration)6+- Go 1.25.1 + WebKitGTK 6.0 (GTK4), Cobra, Viper, SQLite3 (004-migrate-to-webkitgtk)
7-- CGO bindings for WebKit2GTK C API integration7+- CGO bindings for WebKitGTK 6.0 C API integration
8+- GPU rendering via Vulkan/OpenGL with configurable modes
8 9
9 ## Project Structure10 ## Project Structure
10 ```11 ```
@@ -51,14 +52,31 @@ Go 1.25.1: Follow standard Go conventions with gofmt
51 - Constitutional simplicity principles52 - Constitutional simplicity principles
52 53
53 ## Recent Changes54 ## Recent Changes
54-- 003-webkit2gtk-migration: Migrating from Wails to native WebKit2GTK with custom CGO bindings55+- 004-migrate-to-webkitgtk: Migrating to WebKitGTK 6.0 (GTK4) for Vulkan GPU rendering support (UCM restored, GTK4 input, persistent cookies/cache)
56+- 003-webkit2gtk-migration: Migrated from Wails to native WebKit2GTK with custom CGO bindings
55 - 002-browser-controls-ui: Added keyboard/mouse controls, zoom persistence, clipboard integration57 - 002-browser-controls-ui: Added keyboard/mouse controls, zoom persistence, clipboard integration
56 58
57-## WebKit2GTK Integration59+## WebKitGTK 6.0 Integration
58-- Reference repository: /home/brice/dev/clone/webkit2gtk-reference60+- pkg/webkit: Reusable WebKitGTK 6.0 Go bindings (GTK4)
59-- pkg/webkit: Reusable WebKit2GTK Go bindings61+- Hardware acceleration via Vulkan/OpenGL with configurable rendering modes
60-- Direct GTK event handling for keyboard shortcuts62+- Direct GTK4 event handling with GtkEventController
61-- WebKitUserContentManager for script injection63+- WebKitNetworkSession replaces WebKitWebContext
64+- Rendering modes: auto (default), gpu, cpu via --rendering-mode flag
62 65
63 <!-- MANUAL ADDITIONS START -->66 <!-- MANUAL ADDITIONS START -->
64-<!-- MANUAL ADDITIONS END -->⏎̸67+### Migration Status (004-migrate-to-webkitgtk)
68+- WebView construction via `g_object_new(WEBKIT_TYPE_WEB_VIEW, …)` (GTK4).
69+- Native UCM messaging (script-message-received::dumber) replaces fallback bridge.
70+- Persistent storage: cookies (SQLite) and cache via `WebKitNetworkSession`.
71+- Runtime theme sync: listens to GTK `gtk-application-prefer-dark-theme` and updates page color-scheme live.
72+- Native zoom shortcuts + GTK4 key/mouse controllers (including AZERTY fix for zoom-out).
73+- Media: GStreamer required; default “safe” startup demotes VAAPI and uses a safe sink to avoid crashes (can be disabled).
74+
75+### GPU Rendering
76+- GTK4 renderer can use Vulkan; WebKit settings map `auto|gpu|cpu` to the appropriate hardware acceleration policy where available.
77+- Status endpoint: `dumb://homepage/api/rendering/status` reports current configured mode.
78+
79+### Next
80+- Expose cookie policy/media mode toggles in config/CLI.
81+- “Clear browsing data” surfaced via CookieManager/WebsiteDataManager.
82+<!-- MANUAL ADDITIONS END -->
M Makefile +31 -17
@@ -7,6 +7,9 @@ BINARY_NAME=dumber
77 MAIN_PATH=.
88 DIST_DIR=dist
99
10+# Local caches to avoid $HOME permission issues in sandboxed environments
11+GOENV=GOMODCACHE=$(PWD)/tmp/go-mod GOCACHE=$(PWD)/tmp/go-cache GOTMPDIR=$(PWD)/tmp
12+
1013 # Version information from git
1114 VERSION=$(shell git describe --tags --always --dirty 2>/dev/null || echo "v0.0.0-dev")
1215 COMMIT=$(shell git rev-parse --short HEAD 2>/dev/null || echo "unknown")
@@ -23,8 +26,8 @@ help: ## Show this help message
2326 # Build targets
2427 build: build-frontend ## Build the application (frontend assets, then Go binary)
2528 @echo "Building $(BINARY_NAME) $(VERSION)..."
26- @mkdir -p $(DIST_DIR)
27- CGO_ENABLED=1 go build $(LDFLAGS) -o $(DIST_DIR)/$(BINARY_NAME) $(MAIN_PATH)
29+ @mkdir -p $(DIST_DIR) tmp tmp/go-cache tmp/go-mod
30+ $(GOENV) CGO_ENABLED=1 go build $(LDFLAGS) -o $(DIST_DIR)/$(BINARY_NAME) $(MAIN_PATH)
2831
2932 build-frontend: ## Build TypeScript frontend
3033 @echo "Building TypeScript frontend..."
@@ -33,19 +36,28 @@ build-frontend: ## Build TypeScript frontend
3336
3437 build-static: ## Build static binary (CGO disabled, CLI-only functionality)
3538 @echo "Building static $(BINARY_NAME) $(VERSION) (CLI-only)..."
36- @mkdir -p $(DIST_DIR)
37- CGO_ENABLED=0 go build $(LDFLAGS) -o $(DIST_DIR)/$(BINARY_NAME)-static $(MAIN_PATH)
39+ @mkdir -p $(DIST_DIR) tmp tmp/go-cache tmp/go-mod
40+ $(GOENV) CGO_ENABLED=0 go build $(LDFLAGS) -o $(DIST_DIR)/$(BINARY_NAME)-static $(MAIN_PATH)
3841
39-# GUI build with WebKit2GTK
42+# GUI build with WebKitGTK 6.0 (GTK4)
4043 .PHONY: build-gui run-gui
41-build-gui: build-frontend ## Build GUI binary with native WebKit2GTK (requires dev packages)
44+build-gui: build-frontend ## Build GUI binary with native WebKitGTK 6.0 (requires GTK4/WebKitGTK 6 dev packages)
4245 @echo "Building $(BINARY_NAME) (GUI, webkit_cgo)…"
43- @mkdir -p $(DIST_DIR)
44- CGO_ENABLED=1 go build $(LDFLAGS) -tags=webkit_cgo -o $(DIST_DIR)/$(BINARY_NAME) $(MAIN_PATH)
46+ @mkdir -p $(DIST_DIR) tmp tmp/go-cache tmp/go-mod
47+ $(GOENV) CGO_ENABLED=1 go build $(LDFLAGS) -tags=webkit_cgo -o $(DIST_DIR)/$(BINARY_NAME) $(MAIN_PATH)
4548
46-run-gui: ## Run the GUI with native WebKit2GTK (requires dev packages)
49+run-gui: ## Run the GUI with native WebKitGTK 6.0 (requires GTK4/WebKitGTK 6 dev packages)
4750 @echo "Running GUI (webkit_cgo)…"
48- CGO_ENABLED=1 go run -tags=webkit_cgo $(MAIN_PATH)
51+ @mkdir -p tmp tmp/go-cache tmp/go-mod
52+ $(GOENV) CGO_ENABLED=1 go run -tags=webkit_cgo $(MAIN_PATH)
53+
54+.PHONY: check-webkit
55+check-webkit: ## Check system has GTK4/WebKitGTK 6.0/JavaScriptCore 6.0
56+ @echo "Checking pkg-config versions..."
57+ @which pkg-config >/dev/null 2>&1 || (echo "pkg-config not found" && exit 1)
58+ @echo "gtk4: $$(pkg-config --modversion gtk4 2>/dev/null || echo not found)"
59+ @echo "webkitgtk-6.0: $$(pkg-config --modversion webkitgtk-6.0 2>/dev/null || echo not found)"
60+ @echo "javascriptcoregtk-6.0: $$(pkg-config --modversion javascriptcoregtk-6.0 2>/dev/null || echo not found)"
4961
5062 # Development targets
5163 dev: ## Run in development mode
@@ -60,11 +72,13 @@ generate: ## Generate code (SQLC)
6072 # Testing
6173 test: ## Run tests
6274 @echo "Running tests..."
63- CGO_ENABLED=0 go test -v ./...
75+ @mkdir -p tmp tmp/go-cache tmp/go-mod
76+ $(GOENV) CGO_ENABLED=0 go test -v ./...
6477
6578 test-race: ## Run tests with race detection
6679 @echo "Running tests with race detection..."
67- CGO_ENABLED=1 go test -race -v ./...
80+ @mkdir -p tmp tmp/go-cache tmp/go-mod
81+ $(GOENV) CGO_ENABLED=1 go test -race -v ./...
6882
6983 # Linting
7084 lint: ## Run golangci-lint
@@ -100,15 +114,15 @@ init: install-tools ## Initialize project dependencies and tools
100114 check: ## Check that all tools and dependencies are working
101115 @echo "Checking project setup..."
102116 @echo "Go version:"
103- @go version
117+ @$(GOENV) go version
104118 @echo "\nSQLC version:"
105- @sqlc version
119+ @$(GOENV) sqlc version
106120 @echo "\nGolangci-lint version:"
107- @golangci-lint version
121+ @$(GOENV) golangci-lint version
108122 @echo "\nBuilding project..."
109- @make build
123+ @$(MAKE) build
110124 @echo "\nTesting built binary..."
111125 @$(DIST_DIR)/$(BINARY_NAME) version
112126 @echo "\nRunning tests..."
113- @make test
127+ @$(MAKE) test
114128 @echo "\n✅ All checks passed! Project is ready for development."
@@ -7,6 +7,9 @@ BINARY_NAME=dumber
7 MAIN_PATH=.7 MAIN_PATH=.
8 DIST_DIR=dist8 DIST_DIR=dist
9 9
10+# Local caches to avoid $HOME permission issues in sandboxed environments
11+GOENV=GOMODCACHE=$(PWD)/tmp/go-mod GOCACHE=$(PWD)/tmp/go-cache GOTMPDIR=$(PWD)/tmp
12+
10 # Version information from git13 # Version information from git
11 VERSION=$(shell git describe --tags --always --dirty 2>/dev/null || echo "v0.0.0-dev")14 VERSION=$(shell git describe --tags --always --dirty 2>/dev/null || echo "v0.0.0-dev")
12 COMMIT=$(shell git rev-parse --short HEAD 2>/dev/null || echo "unknown")15 COMMIT=$(shell git rev-parse --short HEAD 2>/dev/null || echo "unknown")
@@ -23,8 +26,8 @@ help: ## Show this help message
23 # Build targets26 # Build targets
24 build: build-frontend ## Build the application (frontend assets, then Go binary)27 build: build-frontend ## Build the application (frontend assets, then Go binary)
25 @echo "Building $(BINARY_NAME) $(VERSION)..."28 @echo "Building $(BINARY_NAME) $(VERSION)..."
26- @mkdir -p $(DIST_DIR)29+ @mkdir -p $(DIST_DIR) tmp tmp/go-cache tmp/go-mod
27- CGO_ENABLED=1 go build $(LDFLAGS) -o $(DIST_DIR)/$(BINARY_NAME) $(MAIN_PATH)30+ $(GOENV) CGO_ENABLED=1 go build $(LDFLAGS) -o $(DIST_DIR)/$(BINARY_NAME) $(MAIN_PATH)
28 31
29 build-frontend: ## Build TypeScript frontend32 build-frontend: ## Build TypeScript frontend
30 @echo "Building TypeScript frontend..."33 @echo "Building TypeScript frontend..."
@@ -33,19 +36,28 @@ build-frontend: ## Build TypeScript frontend
33 36
34 build-static: ## Build static binary (CGO disabled, CLI-only functionality)37 build-static: ## Build static binary (CGO disabled, CLI-only functionality)
35 @echo "Building static $(BINARY_NAME) $(VERSION) (CLI-only)..."38 @echo "Building static $(BINARY_NAME) $(VERSION) (CLI-only)..."
36- @mkdir -p $(DIST_DIR)39+ @mkdir -p $(DIST_DIR) tmp tmp/go-cache tmp/go-mod
37- CGO_ENABLED=0 go build $(LDFLAGS) -o $(DIST_DIR)/$(BINARY_NAME)-static $(MAIN_PATH)40+ $(GOENV) CGO_ENABLED=0 go build $(LDFLAGS) -o $(DIST_DIR)/$(BINARY_NAME)-static $(MAIN_PATH)
38 41
39-# GUI build with WebKit2GTK42+# GUI build with WebKitGTK 6.0 (GTK4)
40 .PHONY: build-gui run-gui43 .PHONY: build-gui run-gui
41-build-gui: build-frontend ## Build GUI binary with native WebKit2GTK (requires dev packages)44+build-gui: build-frontend ## Build GUI binary with native WebKitGTK 6.0 (requires GTK4/WebKitGTK 6 dev packages)
42 @echo "Building $(BINARY_NAME) (GUI, webkit_cgo)…"45 @echo "Building $(BINARY_NAME) (GUI, webkit_cgo)…"
43- @mkdir -p $(DIST_DIR)46+ @mkdir -p $(DIST_DIR) tmp tmp/go-cache tmp/go-mod
44- CGO_ENABLED=1 go build $(LDFLAGS) -tags=webkit_cgo -o $(DIST_DIR)/$(BINARY_NAME) $(MAIN_PATH)47+ $(GOENV) CGO_ENABLED=1 go build $(LDFLAGS) -tags=webkit_cgo -o $(DIST_DIR)/$(BINARY_NAME) $(MAIN_PATH)
45 48
46-run-gui: ## Run the GUI with native WebKit2GTK (requires dev packages)49+run-gui: ## Run the GUI with native WebKitGTK 6.0 (requires GTK4/WebKitGTK 6 dev packages)
47 @echo "Running GUI (webkit_cgo)…"50 @echo "Running GUI (webkit_cgo)…"
48- CGO_ENABLED=1 go run -tags=webkit_cgo $(MAIN_PATH)51+ @mkdir -p tmp tmp/go-cache tmp/go-mod
52+ $(GOENV) CGO_ENABLED=1 go run -tags=webkit_cgo $(MAIN_PATH)
53+
54+.PHONY: check-webkit
55+check-webkit: ## Check system has GTK4/WebKitGTK 6.0/JavaScriptCore 6.0
56+ @echo "Checking pkg-config versions..."
57+ @which pkg-config >/dev/null 2>&1 || (echo "pkg-config not found" && exit 1)
58+ @echo "gtk4: $$(pkg-config --modversion gtk4 2>/dev/null || echo not found)"
59+ @echo "webkitgtk-6.0: $$(pkg-config --modversion webkitgtk-6.0 2>/dev/null || echo not found)"
60+ @echo "javascriptcoregtk-6.0: $$(pkg-config --modversion javascriptcoregtk-6.0 2>/dev/null || echo not found)"
49 61
50 # Development targets62 # Development targets
51 dev: ## Run in development mode63 dev: ## Run in development mode
@@ -60,11 +72,13 @@ generate: ## Generate code (SQLC)
60 # Testing72 # Testing
61 test: ## Run tests73 test: ## Run tests
62 @echo "Running tests..."74 @echo "Running tests..."
63- CGO_ENABLED=0 go test -v ./...75+ @mkdir -p tmp tmp/go-cache tmp/go-mod
76+ $(GOENV) CGO_ENABLED=0 go test -v ./...
64 77
65 test-race: ## Run tests with race detection78 test-race: ## Run tests with race detection
66 @echo "Running tests with race detection..."79 @echo "Running tests with race detection..."
67- CGO_ENABLED=1 go test -race -v ./...80+ @mkdir -p tmp tmp/go-cache tmp/go-mod
81+ $(GOENV) CGO_ENABLED=1 go test -race -v ./...
68 82
69 # Linting83 # Linting
70 lint: ## Run golangci-lint84 lint: ## Run golangci-lint
@@ -100,15 +114,15 @@ init: install-tools ## Initialize project dependencies and tools
100 check: ## Check that all tools and dependencies are working114 check: ## Check that all tools and dependencies are working
101 @echo "Checking project setup..."115 @echo "Checking project setup..."
102 @echo "Go version:"116 @echo "Go version:"
103- @go version117+ @$(GOENV) go version
104 @echo "\nSQLC version:"118 @echo "\nSQLC version:"
105- @sqlc version119+ @$(GOENV) sqlc version
106 @echo "\nGolangci-lint version:"120 @echo "\nGolangci-lint version:"
107- @golangci-lint version121+ @$(GOENV) golangci-lint version
108 @echo "\nBuilding project..."122 @echo "\nBuilding project..."
109- @make build123+ @$(MAKE) build
110 @echo "\nTesting built binary..."124 @echo "\nTesting built binary..."
111 @$(DIST_DIR)/$(BINARY_NAME) version125 @$(DIST_DIR)/$(BINARY_NAME) version
112 @echo "\nRunning tests..."126 @echo "\nRunning tests..."
113- @make test127+ @$(MAKE) test
114 @echo "\n✅ All checks passed! Project is ready for development."128 @echo "\n✅ All checks passed! Project is ready for development."
M README.md +28 -11
@@ -1,19 +1,19 @@
1-# Dumber — a tiny, dumb browser for Wayland
1+# Dumber, fully unfeatured unbloated browser for tiling WMs
22
3-Dumber is a minimalist browser and launcher companion focused on speed and simplicity. It ships a built‑in WebKit2GTK window for navigation and a CLI with dmenu‑style launcher integration (rofi, fuzzel).
3+Dumber is a minimalist browser and launcher companion focused on speed and simplicity. It ships a built‑in WebKitGTK (GTK4) window for navigation and a CLI with dmenu‑style launcher integration (rofi, fuzzel).
44
5-- Native GUI via WebKit2GTK (CGO) with an embedded homepage UI.
5+- Native GUI via WebKitGTK 6 (GTK4, CGO) with an embedded homepage UI.
66 - CLI that parses URLs, supports search shortcuts (e.g., `g:query`), and integrates with rofi/dmenu.
77 - Persistent history and per‑domain zoom levels (SQLite, XDG locations).
88 - Configurable via `config.json` and environment variables, with live reload.
99
1010 ## Wayland‑First
11-- Dumb on purpose, designed for Wayland window managers (Sway, Hyprland, River, Niri, etc..) — a tiny companion that plays nicely with your launcher.
12-- Uses a native WebKit2GTK window; no external browser required and no X11/XWayland dependency.
11+- Dumb on purpose, designed for Wayland window managers (Sway, Hyprland, River, Niri, etc..) it's a tiny companion that interact nicely with your launcher.
12+- Uses a native WebKitGTKdo i window; no external browser required and no X11/XWayland dependency.
1313 - Optimized for dmenu‑style launchers (rofi, fuzzel, wofi) and keyboard‑driven workflows.
1414
1515 ## Status
16-- Very early stage — expect sharp edges and breaking changes, but it works well enough to browse and play with the launcher flows.
16+- Very early stage, expect sharp edges and breaking changes, but it works well enough to browse and play with the launcher flows.
1717
1818 ## Features
1919 - Built‑in browser window (no external browser needed)
@@ -27,9 +27,9 @@ Dumber is a minimalist browser and launcher companion focused on speed and simpl
2727 Prerequisites:
2828 - Go 1.25+
2929 - Node.js 20+ (for building the embedded frontend)
30-- For GUI build: WebKit2GTK and GTK dev packages (examples)
31- - Debian/Ubuntu: `libwebkit2gtk-4.1-dev libgtk-3-dev build-essential`
32- - Arch: `webkit2gtk gtk3 base-devel`
30+- For GUI build: WebKitGTK 6 and GTK4 dev packages (examples)
31+ - Debian/Ubuntu: `libwebkitgtk-6.0-dev libgtk-4-dev build-essential`
32+ - Arch: `webkitgtk-6.0 gtk4 base-devel`
3333
3434 Build options:
3535 - GUI (recommended):
@@ -104,15 +104,30 @@ SQLite tables (created automatically):
104104 - `shortcuts(shortcut, url_template, description, created_at)`
105105 - `zoom_levels(domain PRIMARY KEY, zoom_factor, updated_at)`
106106
107-## Building With WebKit2GTK
107+## Building With WebKitGTK 6 (GTK4)
108108 - The GUI requires building with `-tags=webkit_cgo` and CGO enabled.
109-- Ensure WebKit2GTK and GTK dev headers are installed (see prerequisites).
109+- Ensure WebKitGTK 6 and GTK4 dev headers are installed (see prerequisites).
110110 - Make targets handle both frontend and Go build steps:
111111 - `make build-gui` (CGO enabled)
112112 - `make build` (CGO enabled by default in the Makefile’s main build)
113113
114114 Without the native tag, a stub backend is used: you can still run CLI flows and see logs, but no native window is displayed.
115115
116+## Media (GStreamer) Requirements
117+
118+WebKitGTK uses GStreamer for media playback. Install the following packages to ensure audio/video work correctly.
119+
120+- Arch Linux:
121+ - `sudo pacman -S gst-plugins-base gst-plugins-good gst-plugins-bad gst-plugins-ugly gst-libav gst-plugin-pipewire pipewire pipewire-pulse`
122+ - Optional hardware accel: `gstreamer-vaapi`
123+- Debian/Ubuntu:
124+ - `sudo apt install gstreamer1.0-tools gstreamer1.0-plugins-base gstreamer1.0-plugins-good gstreamer1.0-plugins-bad gstreamer1.0-plugins-ugly gstreamer1.0-libav gstreamer1.0-pipewire`
125+ - Optional hardware accel: `gstreamer1.0-vaapi`
126+
127+## Theme and Runtime Updates
128+- The homepage and pages respect your system’s light/dark preference.
129+- The app updates color‑scheme live when your GTK theme changes (no relaunch needed).
130+
116131 ## Project Structure
117132 - `main.go`: CLI/GUI entrypoint, `dumb://` scheme, service wiring
118133 - `internal/cli`: Cobra commands (`browse`, `dmenu`, `version`)
@@ -127,6 +142,8 @@ Without the native tag, a stub backend is used: you can still run CLI flows and
127142 - The `tmp/` directory is ignored by Git and kept out of history.
128143
129144 ## Roadmap
145+- WebKitGTK 6 (GTK4) migration — almost complete (GPU/Vulkan path landed).
146+- GPU rendering (Vulkan via GTK4 renderer) with graceful CPU fallback.
130147 - Full Vim-style motion and keyboard navigation across pages and UI.
131148 - Performance work: faster startup, lower memory, snappier UI.
132149 - Basic ad-block extension — because fuck ads.
@@ -1,19 +1,19 @@
1-# Dumber — a tiny, dumb browser for Wayland1+# Dumber, fully unfeatured unbloated browser for tiling WMs
2 2
3-Dumber is a minimalist browser and launcher companion focused on speed and simplicity. It ships a built‑in WebKit2GTK window for navigation and a CLI with dmenu‑style launcher integration (rofi, fuzzel).3+Dumber is a minimalist browser and launcher companion focused on speed and simplicity. It ships a built‑in WebKitGTK (GTK4) window for navigation and a CLI with dmenu‑style launcher integration (rofi, fuzzel).
4 4
5-- Native GUI via WebKit2GTK (CGO) with an embedded homepage UI.5+- Native GUI via WebKitGTK 6 (GTK4, CGO) with an embedded homepage UI.
6 - CLI that parses URLs, supports search shortcuts (e.g., `g:query`), and integrates with rofi/dmenu.6 - CLI that parses URLs, supports search shortcuts (e.g., `g:query`), and integrates with rofi/dmenu.
7 - Persistent history and per‑domain zoom levels (SQLite, XDG locations).7 - Persistent history and per‑domain zoom levels (SQLite, XDG locations).
8 - Configurable via `config.json` and environment variables, with live reload.8 - Configurable via `config.json` and environment variables, with live reload.
9 9
10 ## Wayland‑First10 ## Wayland‑First
11-- Dumb on purpose, designed for Wayland window managers (Sway, Hyprland, River, Niri, etc..) — a tiny companion that plays nicely with your launcher.11+- Dumb on purpose, designed for Wayland window managers (Sway, Hyprland, River, Niri, etc..) it's a tiny companion that interact nicely with your launcher.
12-- Uses a native WebKit2GTK window; no external browser required and no X11/XWayland dependency.12+- Uses a native WebKitGTKdo i window; no external browser required and no X11/XWayland dependency.
13 - Optimized for dmenu‑style launchers (rofi, fuzzel, wofi) and keyboard‑driven workflows.13 - Optimized for dmenu‑style launchers (rofi, fuzzel, wofi) and keyboard‑driven workflows.
14 14
15 ## Status15 ## Status
16-- Very early stage — expect sharp edges and breaking changes, but it works well enough to browse and play with the launcher flows.16+- Very early stage, expect sharp edges and breaking changes, but it works well enough to browse and play with the launcher flows.
17 17
18 ## Features18 ## Features
19 - Built‑in browser window (no external browser needed)19 - Built‑in browser window (no external browser needed)
@@ -27,9 +27,9 @@ Dumber is a minimalist browser and launcher companion focused on speed and simpl
27 Prerequisites:27 Prerequisites:
28 - Go 1.25+28 - Go 1.25+
29 - Node.js 20+ (for building the embedded frontend)29 - Node.js 20+ (for building the embedded frontend)
30-- For GUI build: WebKit2GTK and GTK dev packages (examples)30+- For GUI build: WebKitGTK 6 and GTK4 dev packages (examples)
31- - Debian/Ubuntu: `libwebkit2gtk-4.1-dev libgtk-3-dev build-essential`31+ - Debian/Ubuntu: `libwebkitgtk-6.0-dev libgtk-4-dev build-essential`
32- - Arch: `webkit2gtk gtk3 base-devel`32+ - Arch: `webkitgtk-6.0 gtk4 base-devel`
33 33
34 Build options:34 Build options:
35 - GUI (recommended):35 - GUI (recommended):
@@ -104,15 +104,30 @@ SQLite tables (created automatically):
104 - `shortcuts(shortcut, url_template, description, created_at)`104 - `shortcuts(shortcut, url_template, description, created_at)`
105 - `zoom_levels(domain PRIMARY KEY, zoom_factor, updated_at)`105 - `zoom_levels(domain PRIMARY KEY, zoom_factor, updated_at)`
106 106
107-## Building With WebKit2GTK107+## Building With WebKitGTK 6 (GTK4)
108 - The GUI requires building with `-tags=webkit_cgo` and CGO enabled.108 - The GUI requires building with `-tags=webkit_cgo` and CGO enabled.
109-- Ensure WebKit2GTK and GTK dev headers are installed (see prerequisites).109+- Ensure WebKitGTK 6 and GTK4 dev headers are installed (see prerequisites).
110 - Make targets handle both frontend and Go build steps:110 - Make targets handle both frontend and Go build steps:
111 - `make build-gui` (CGO enabled)111 - `make build-gui` (CGO enabled)
112 - `make build` (CGO enabled by default in the Makefile’s main build)112 - `make build` (CGO enabled by default in the Makefile’s main build)
113 113
114 Without the native tag, a stub backend is used: you can still run CLI flows and see logs, but no native window is displayed.114 Without the native tag, a stub backend is used: you can still run CLI flows and see logs, but no native window is displayed.
115 115
116+## Media (GStreamer) Requirements
117+
118+WebKitGTK uses GStreamer for media playback. Install the following packages to ensure audio/video work correctly.
119+
120+- Arch Linux:
121+ - `sudo pacman -S gst-plugins-base gst-plugins-good gst-plugins-bad gst-plugins-ugly gst-libav gst-plugin-pipewire pipewire pipewire-pulse`
122+ - Optional hardware accel: `gstreamer-vaapi`
123+- Debian/Ubuntu:
124+ - `sudo apt install gstreamer1.0-tools gstreamer1.0-plugins-base gstreamer1.0-plugins-good gstreamer1.0-plugins-bad gstreamer1.0-plugins-ugly gstreamer1.0-libav gstreamer1.0-pipewire`
125+ - Optional hardware accel: `gstreamer1.0-vaapi`
126+
127+## Theme and Runtime Updates
128+- The homepage and pages respect your system’s light/dark preference.
129+- The app updates color‑scheme live when your GTK theme changes (no relaunch needed).
130+
116 ## Project Structure131 ## Project Structure
117 - `main.go`: CLI/GUI entrypoint, `dumb://` scheme, service wiring132 - `main.go`: CLI/GUI entrypoint, `dumb://` scheme, service wiring
118 - `internal/cli`: Cobra commands (`browse`, `dmenu`, `version`)133 - `internal/cli`: Cobra commands (`browse`, `dmenu`, `version`)
@@ -127,6 +142,8 @@ Without the native tag, a stub backend is used: you can still run CLI flows and
127 - The `tmp/` directory is ignored by Git and kept out of history.142 - The `tmp/` directory is ignored by Git and kept out of history.
128 143
129 ## Roadmap144 ## Roadmap
145+- WebKitGTK 6 (GTK4) migration — almost complete (GPU/Vulkan path landed).
146+- GPU rendering (Vulkan via GTK4 renderer) with graceful CPU fallback.
130 - Full Vim-style motion and keyboard navigation across pages and UI.147 - Full Vim-style motion and keyboard navigation across pages and UI.
131 - Performance work: faster startup, lower memory, snappier UI.148 - Performance work: faster startup, lower memory, snappier UI.
132 - Basic ad-block extension — because fuck ads.149 - Basic ad-block extension — because fuck ads.
M internal/cli/browse.go +24 -9
@@ -17,9 +17,11 @@ import (
1717
1818 // NewBrowseCmd creates the browse command
1919 func NewBrowseCmd() *cobra.Command {
20- cmd := &cobra.Command{
21- Use: "browse <url|query>",
22- Short: "Browse a URL or search query",
20+ var renderingMode string
21+
22+ cmd := &cobra.Command{
23+ Use: "browse <url|query>",
24+ Short: "Browse a URL or search query",
2325 Long: `Browse a URL directly or use search shortcuts like:
2426 g:golang -> Google search for "golang"
2527 gh:cobra -> GitHub search for "cobra"
@@ -30,11 +32,21 @@ Direct URLs are also supported:
3032 https://example.com
3133 example.com (automatically adds https://)`,
3234 Args: cobra.ExactArgs(1),
33- RunE: func(_ *cobra.Command, args []string) error {
34- cli, err := NewCLI()
35- if err != nil {
36- return fmt.Errorf("failed to initialize CLI: %w", err)
37- }
35+ RunE: func(_ *cobra.Command, args []string) error {
36+ // Propagate rendering mode to GUI process via env var
37+ if renderingMode != "" {
38+ rm := strings.ToLower(renderingMode)
39+ switch rm {
40+ case "auto", "gpu", "cpu":
41+ _ = os.Setenv("DUMBER_RENDERING_MODE", rm)
42+ default:
43+ return fmt.Errorf("invalid --rendering-mode: %s (expected auto|gpu|cpu)", renderingMode)
44+ }
45+ }
46+ cli, err := NewCLI()
47+ if err != nil {
48+ return fmt.Errorf("failed to initialize CLI: %w", err)
49+ }
3850 defer func() {
3951 if closeErr := cli.Close(); closeErr != nil {
4052 fmt.Fprintf(os.Stderr, "Warning: failed to close database: %v\n", closeErr)
@@ -45,7 +57,10 @@ Direct URLs are also supported:
4557 },
4658 }
4759
48- return cmd
60+ // Flags
61+ cmd.Flags().StringVar(&renderingMode, "rendering-mode", "", "Rendering mode: auto|gpu|cpu")
62+
63+ return cmd
4964 }
5065
5166 // browse handles the core browsing logic
@@ -17,9 +17,11 @@ import (
17 17
18 // NewBrowseCmd creates the browse command18 // NewBrowseCmd creates the browse command
19 func NewBrowseCmd() *cobra.Command {19 func NewBrowseCmd() *cobra.Command {
20- cmd := &cobra.Command{20+ var renderingMode string
21- Use: "browse <url|query>",21+
22- Short: "Browse a URL or search query",22+ cmd := &cobra.Command{
23+ Use: "browse <url|query>",
24+ Short: "Browse a URL or search query",
23 Long: `Browse a URL directly or use search shortcuts like:25 Long: `Browse a URL directly or use search shortcuts like:
24 g:golang -> Google search for "golang"26 g:golang -> Google search for "golang"
25 gh:cobra -> GitHub search for "cobra"27 gh:cobra -> GitHub search for "cobra"
@@ -30,11 +32,21 @@ Direct URLs are also supported:
30 https://example.com32 https://example.com
31 example.com (automatically adds https://)`,33 example.com (automatically adds https://)`,
32 Args: cobra.ExactArgs(1),34 Args: cobra.ExactArgs(1),
33- RunE: func(_ *cobra.Command, args []string) error {35+ RunE: func(_ *cobra.Command, args []string) error {
34- cli, err := NewCLI()36+ // Propagate rendering mode to GUI process via env var
35- if err != nil {37+ if renderingMode != "" {
36- return fmt.Errorf("failed to initialize CLI: %w", err)38+ rm := strings.ToLower(renderingMode)
37- }39+ switch rm {
40+ case "auto", "gpu", "cpu":
41+ _ = os.Setenv("DUMBER_RENDERING_MODE", rm)
42+ default:
43+ return fmt.Errorf("invalid --rendering-mode: %s (expected auto|gpu|cpu)", renderingMode)
44+ }
45+ }
46+ cli, err := NewCLI()
47+ if err != nil {
48+ return fmt.Errorf("failed to initialize CLI: %w", err)
49+ }
38 defer func() {50 defer func() {
39 if closeErr := cli.Close(); closeErr != nil {51 if closeErr := cli.Close(); closeErr != nil {
40 fmt.Fprintf(os.Stderr, "Warning: failed to close database: %v\n", closeErr)52 fmt.Fprintf(os.Stderr, "Warning: failed to close database: %v\n", closeErr)
@@ -45,7 +57,10 @@ Direct URLs are also supported:
45 },57 },
46 }58 }
47 59
48- return cmd60+ // Flags
61+ cmd.Flags().StringVar(&renderingMode, "rendering-mode", "", "Rendering mode: auto|gpu|cpu")
62+
63+ return cmd
49 }64 }
50 65
51 // browse handles the core browsing logic66 // browse handles the core browsing logic
M internal/config/config.go +62 -19
@@ -22,8 +22,19 @@ type Config struct {
2222 Dmenu DmenuConfig `mapstructure:"dmenu" yaml:"dmenu"`
2323 Logging LoggingConfig `mapstructure:"logging" yaml:"logging"`
2424 Appearance AppearanceConfig `mapstructure:"appearance" yaml:"appearance"`
25+ // RenderingMode controls GPU/CPU rendering selection for WebKit
26+ RenderingMode RenderingMode `mapstructure:"rendering_mode" yaml:"rendering_mode"`
2527 }
2628
29+// RenderingMode selects GPU vs CPU rendering.
30+type RenderingMode string
31+
32+const (
33+ RenderingModeAuto RenderingMode = "auto"
34+ RenderingModeGPU RenderingMode = "gpu"
35+ RenderingModeCPU RenderingMode = "cpu"
36+)
37+
2738 // DatabaseConfig holds database-related configuration.
2839 type DatabaseConfig struct {
2940 Path string `mapstructure:"path" yaml:"path"`
@@ -108,7 +119,7 @@ func NewManager() (*Manager, error) {
108119 v.AutomaticEnv()
109120
110121 // Bind specific environment variables
111- bindings := map[string]string{
122+ bindings := map[string]string{
112123 "database.path": "DATABASE_PATH",
113124 "database.max_connections": "DATABASE_MAX_CONNECTIONS",
114125 "database.max_idle_time": "DATABASE_MAX_IDLE_TIME",
@@ -136,11 +147,16 @@ func NewManager() (*Manager, error) {
136147 "logging.compress": "LOGGING_COMPRESS",
137148 }
138149
139- for key, env := range bindings {
140- if err := v.BindEnv(key, "DUMB_BROWSER_"+env); err != nil {
141- return nil, fmt.Errorf("failed to bind environment variable %s: %w", env, err)
142- }
143- }
150+ for key, env := range bindings {
151+ if err := v.BindEnv(key, "DUMB_BROWSER_"+env); err != nil {
152+ return nil, fmt.Errorf("failed to bind environment variable %s: %w", env, err)
153+ }
154+ }
155+
156+ // Explicit binding for rendering mode via dedicated env var
157+ if err := v.BindEnv("rendering_mode", "DUMBER_RENDERING_MODE"); err != nil {
158+ return nil, fmt.Errorf("failed to bind DUMBER_RENDERING_MODE: %w", err)
159+ }
144160
145161 return &Manager{
146162 viper: v,
@@ -173,11 +189,11 @@ func (m *Manager) Load() error {
173189 }
174190 }
175191
176- // Unmarshal into config struct
177- config := &Config{}
178- if err := m.viper.Unmarshal(config); err != nil {
179- return fmt.Errorf("failed to unmarshal config: %w", err)
180- }
192+ // Unmarshal into config struct
193+ config := &Config{}
194+ if err := m.viper.Unmarshal(config); err != nil {
195+ return fmt.Errorf("failed to unmarshal config: %w", err)
196+ }
181197
182198 // Set database path if not specified
183199 if config.Database.Path == "" {
@@ -188,8 +204,20 @@ func (m *Manager) Load() error {
188204 config.Database.Path = dbPath
189205 }
190206
191- m.config = config
192- return nil
207+ // Normalize/validate rendering mode
208+ switch strings.ToLower(string(config.RenderingMode)) {
209+ case "", string(RenderingModeAuto):
210+ config.RenderingMode = RenderingModeAuto
211+ case string(RenderingModeGPU):
212+ config.RenderingMode = RenderingModeGPU
213+ case string(RenderingModeCPU):
214+ config.RenderingMode = RenderingModeCPU
215+ default:
216+ config.RenderingMode = RenderingModeAuto
217+ }
218+
219+ m.config = config
220+ return nil
193221 }
194222
195223 // Get returns the current configuration (thread-safe).
@@ -249,10 +277,10 @@ func (m *Manager) reload() error {
249277 return err
250278 }
251279
252- config := &Config{}
253- if err := m.viper.Unmarshal(config); err != nil {
254- return err
255- }
280+ config := &Config{}
281+ if err := m.viper.Unmarshal(config); err != nil {
282+ return err
283+ }
256284
257285 // Set database path if not specified
258286 if config.Database.Path == "" {
@@ -263,8 +291,20 @@ func (m *Manager) reload() error {
263291 config.Database.Path = dbPath
264292 }
265293
266- m.config = config
267- return nil
294+ // Normalize/validate rendering mode
295+ switch strings.ToLower(string(config.RenderingMode)) {
296+ case "", string(RenderingModeAuto):
297+ config.RenderingMode = RenderingModeAuto
298+ case string(RenderingModeGPU):
299+ config.RenderingMode = RenderingModeGPU
300+ case string(RenderingModeCPU):
301+ config.RenderingMode = RenderingModeCPU
302+ default:
303+ config.RenderingMode = RenderingModeAuto
304+ }
305+
306+ m.config = config
307+ return nil
268308 }
269309
270310 // setDefaults sets default configuration values in Viper.
@@ -310,6 +350,9 @@ func (m *Manager) setDefaults() {
310350 m.viper.SetDefault("appearance.serif_font", defaults.Appearance.SerifFont)
311351 m.viper.SetDefault("appearance.monospace_font", defaults.Appearance.MonospaceFont)
312352 m.viper.SetDefault("appearance.default_font_size", defaults.Appearance.DefaultFontSize)
353+
354+ // Rendering defaults
355+ m.viper.SetDefault("rendering_mode", string(RenderingModeAuto))
313356 }
314357
315358 // createDefaultConfig creates a default configuration file.
@@ -22,8 +22,19 @@ type Config struct {
22 Dmenu DmenuConfig `mapstructure:"dmenu" yaml:"dmenu"`22 Dmenu DmenuConfig `mapstructure:"dmenu" yaml:"dmenu"`
23 Logging LoggingConfig `mapstructure:"logging" yaml:"logging"`23 Logging LoggingConfig `mapstructure:"logging" yaml:"logging"`
24 Appearance AppearanceConfig `mapstructure:"appearance" yaml:"appearance"`24 Appearance AppearanceConfig `mapstructure:"appearance" yaml:"appearance"`
25+ // RenderingMode controls GPU/CPU rendering selection for WebKit
26+ RenderingMode RenderingMode `mapstructure:"rendering_mode" yaml:"rendering_mode"`
25 }27 }
26 28
29+// RenderingMode selects GPU vs CPU rendering.
30+type RenderingMode string
31+
32+const (
33+ RenderingModeAuto RenderingMode = "auto"
34+ RenderingModeGPU RenderingMode = "gpu"
35+ RenderingModeCPU RenderingMode = "cpu"
36+)
37+
27 // DatabaseConfig holds database-related configuration.38 // DatabaseConfig holds database-related configuration.
28 type DatabaseConfig struct {39 type DatabaseConfig struct {
29 Path string `mapstructure:"path" yaml:"path"`40 Path string `mapstructure:"path" yaml:"path"`
@@ -108,7 +119,7 @@ func NewManager() (*Manager, error) {
108 v.AutomaticEnv()119 v.AutomaticEnv()
109 120
110 // Bind specific environment variables121 // Bind specific environment variables
111- bindings := map[string]string{122+ bindings := map[string]string{
112 "database.path": "DATABASE_PATH",123 "database.path": "DATABASE_PATH",
113 "database.max_connections": "DATABASE_MAX_CONNECTIONS",124 "database.max_connections": "DATABASE_MAX_CONNECTIONS",
114 "database.max_idle_time": "DATABASE_MAX_IDLE_TIME",125 "database.max_idle_time": "DATABASE_MAX_IDLE_TIME",
@@ -136,11 +147,16 @@ func NewManager() (*Manager, error) {
136 "logging.compress": "LOGGING_COMPRESS",147 "logging.compress": "LOGGING_COMPRESS",
137 }148 }
138 149
139- for key, env := range bindings {150+ for key, env := range bindings {
140- if err := v.BindEnv(key, "DUMB_BROWSER_"+env); err != nil {151+ if err := v.BindEnv(key, "DUMB_BROWSER_"+env); err != nil {
141- return nil, fmt.Errorf("failed to bind environment variable %s: %w", env, err)152+ return nil, fmt.Errorf("failed to bind environment variable %s: %w", env, err)
142- }153+ }
143- }154+ }
155+
156+ // Explicit binding for rendering mode via dedicated env var
157+ if err := v.BindEnv("rendering_mode", "DUMBER_RENDERING_MODE"); err != nil {
158+ return nil, fmt.Errorf("failed to bind DUMBER_RENDERING_MODE: %w", err)
159+ }
144 160
145 return &Manager{161 return &Manager{
146 viper: v,162 viper: v,
@@ -173,11 +189,11 @@ func (m *Manager) Load() error {
173 }189 }
174 }190 }
175 191
176- // Unmarshal into config struct192+ // Unmarshal into config struct
177- config := &Config{}193+ config := &Config{}
178- if err := m.viper.Unmarshal(config); err != nil {194+ if err := m.viper.Unmarshal(config); err != nil {
179- return fmt.Errorf("failed to unmarshal config: %w", err)195+ return fmt.Errorf("failed to unmarshal config: %w", err)
180- }196+ }
181 197
182 // Set database path if not specified198 // Set database path if not specified
183 if config.Database.Path == "" {199 if config.Database.Path == "" {
@@ -188,8 +204,20 @@ func (m *Manager) Load() error {
188 config.Database.Path = dbPath204 config.Database.Path = dbPath
189 }205 }
190 206
191- m.config = config207+ // Normalize/validate rendering mode
192- return nil208+ switch strings.ToLower(string(config.RenderingMode)) {
209+ case "", string(RenderingModeAuto):
210+ config.RenderingMode = RenderingModeAuto
211+ case string(RenderingModeGPU):
212+ config.RenderingMode = RenderingModeGPU
213+ case string(RenderingModeCPU):
214+ config.RenderingMode = RenderingModeCPU
215+ default:
216+ config.RenderingMode = RenderingModeAuto
217+ }
218+
219+ m.config = config
220+ return nil
193 }221 }
194 222
195 // Get returns the current configuration (thread-safe).223 // Get returns the current configuration (thread-safe).
@@ -249,10 +277,10 @@ func (m *Manager) reload() error {
249 return err277 return err
250 }278 }
251 279
252- config := &Config{}280+ config := &Config{}
253- if err := m.viper.Unmarshal(config); err != nil {281+ if err := m.viper.Unmarshal(config); err != nil {
254- return err282+ return err
255- }283+ }
256 284
257 // Set database path if not specified285 // Set database path if not specified
258 if config.Database.Path == "" {286 if config.Database.Path == "" {
@@ -263,8 +291,20 @@ func (m *Manager) reload() error {
263 config.Database.Path = dbPath291 config.Database.Path = dbPath
264 }292 }
265 293
266- m.config = config294+ // Normalize/validate rendering mode
267- return nil295+ switch strings.ToLower(string(config.RenderingMode)) {
296+ case "", string(RenderingModeAuto):
297+ config.RenderingMode = RenderingModeAuto
298+ case string(RenderingModeGPU):
299+ config.RenderingMode = RenderingModeGPU
300+ case string(RenderingModeCPU):
301+ config.RenderingMode = RenderingModeCPU
302+ default:
303+ config.RenderingMode = RenderingModeAuto
304+ }
305+
306+ m.config = config
307+ return nil
268 }308 }
269 309
270 // setDefaults sets default configuration values in Viper.310 // setDefaults sets default configuration values in Viper.
@@ -310,6 +350,9 @@ func (m *Manager) setDefaults() {
310 m.viper.SetDefault("appearance.serif_font", defaults.Appearance.SerifFont)350 m.viper.SetDefault("appearance.serif_font", defaults.Appearance.SerifFont)
311 m.viper.SetDefault("appearance.monospace_font", defaults.Appearance.MonospaceFont)351 m.viper.SetDefault("appearance.monospace_font", defaults.Appearance.MonospaceFont)
312 m.viper.SetDefault("appearance.default_font_size", defaults.Appearance.DefaultFontSize)352 m.viper.SetDefault("appearance.default_font_size", defaults.Appearance.DefaultFontSize)
353+
354+ // Rendering defaults
355+ m.viper.SetDefault("rendering_mode", string(RenderingModeAuto))
313 }356 }
314 357
315 // createDefaultConfig creates a default configuration file.358 // createDefaultConfig creates a default configuration file.
M internal/config/defaults.go +1 -0
@@ -61,6 +61,7 @@ func DefaultConfig() *Config {
6161 MonospaceFont: "Fira Code",
6262 DefaultFontSize: 16,
6363 },
64+ RenderingMode: RenderingModeAuto,
6465 }
6566 }
6667
@@ -61,6 +61,7 @@ func DefaultConfig() *Config {
61 MonospaceFont: "Fira Code",61 MonospaceFont: "Fira Code",
62 DefaultFontSize: 16,62 DefaultFontSize: 16,
63 },63 },
64+ RenderingMode: RenderingModeAuto,
64 }65 }
65 }66 }
66 67
M main.go +32 -26
@@ -121,6 +121,24 @@ func runBrowser() {
121121 defer runtime.UnlockOSThread()
122122 }
123123
124+ // Proactively harden media pipeline to avoid WebProcess crashes from buggy VAAPI/plugins.
125+ // Can be disabled with DUMBER_MEDIA_SAFE=0
126+ if os.Getenv("DUMBER_MEDIA_SAFE") != "0" {
127+ // Demote VAAPI-related elements so they won't be auto-picked
128+ if os.Getenv("GST_PLUGIN_FEATURE_RANK") == "" {
129+ os.Setenv("GST_PLUGIN_FEATURE_RANK", "vaapisink:0,vaapidecodebin:0,vaapih264dec:0,vaapivideoconvert:0")
130+ }
131+ // Provide a safe audio sink to avoid autoaudiosink failures
132+ if os.Getenv("GST_AUDIO_SINK") == "" {
133+ os.Setenv("GST_AUDIO_SINK", "fakesink")
134+ }
135+ // Optionally disable media stream features that can engage complex pipelines
136+ if os.Getenv("WEBKIT_DISABLE_MEDIA_STREAM") == "" {
137+ os.Setenv("WEBKIT_DISABLE_MEDIA_STREAM", "1")
138+ }
139+ log.Printf("[media] Safe mode enabled (override with DUMBER_MEDIA_SAFE=0)")
140+ }
141+
124142 if err := config.Init(); err != nil {
125143 log.Fatalf("Failed to initialize config: %v", err)
126144 }
@@ -142,7 +160,8 @@ func runBrowser() {
142160 // JS controls injection is disabled; using native shortcuts via GTK.
143161
144162 // Track current URL for zoom persistence
145- var currentURL string
163+ // Initialize to the initial homepage so shortcuts work immediately
164+ var currentURL string = "dumb://homepage"
146165
147166 // Register custom scheme resolver for dumb://
148167 webkit.SetURISchemeResolver(func(uri string) (string, []byte, bool) {
@@ -169,6 +188,14 @@ func runBrowser() {
169188 if strings.HasPrefix(path, "/api/") { path = strings.TrimPrefix(path, "/api") }
170189 if path == "/api" { path = "/" }
171190 switch {
191+ case strings.HasPrefix(path, "/rendering/status"):
192+ log.Printf("[api] GET /rendering/status")
193+ // Report configured rendering mode; runtime GPU state depends on WebKit internals
194+ resp := struct{
195+ Mode string `json:"mode"`
196+ }{Mode: string(cfg.RenderingMode)}
197+ b, _ := json.Marshal(resp)
198+ return "application/json; charset=utf-8", b, true
172199 case strings.HasPrefix(path, "/config"):
173200 log.Printf("[api] GET /config")
174201 // Build config info
@@ -290,6 +317,7 @@ func runBrowser() {
290317 DefaultSerifFont: cfg.Appearance.SerifFont,
291318 DefaultMonospaceFont: cfg.Appearance.MonospaceFont,
292319 DefaultFontSize: cfg.Appearance.DefaultFontSize,
320+ Rendering: webkit.RenderingConfig{Mode: string(cfg.RenderingMode)},
293321 })
294322 if err != nil {
295323 log.Printf("Warning: failed to create WebView: %v", err)
@@ -325,6 +353,7 @@ func runBrowser() {
325353 URL string `json:"url"`
326354 Q string `json:"q"`
327355 Limit int `json:"limit"`
356+ Value string `json:"value"`
328357 // Wails fetch bridge
329358 ID string `json:"id"`
330359 Payload json.RawMessage `json:"payload"`
@@ -397,7 +426,7 @@ func runBrowser() {
397426 _ = view.InjectScript("window.__dumber_wails_resolve('" + m.ID + "', '{}')")
398427 }
399428 case "theme":
400- if m.URL != "" { log.Printf("[theme] page reported color-scheme: %s", m.URL) }
429+ if m.Value != "" { log.Printf("[theme] page reported color-scheme: %s", m.Value) }
401430 }
402431 })
403432 log.Printf("Showing WebView window…")
@@ -437,7 +466,6 @@ func runBrowser() {
437466
438467 // Register basic keyboard shortcuts on the native view to preserve behavior
439468 if view != nil {
440- ctx := context.Background()
441469 // DevTools
442470 _ = view.RegisterKeyboardShortcut("F12", func() { log.Printf("Shortcut: F12 (devtools)"); _ = view.ShowDevTools() })
443471 // Omnibox (Ctrl+L): rely on injected script listener
@@ -445,29 +473,7 @@ func runBrowser() {
445473 log.Printf("Shortcut: Omnibox toggle")
446474 _ = view.InjectScript("window.__dumber_toggle && window.__dumber_toggle()")
447475 })
448- // Zoom In
449- _ = view.RegisterKeyboardShortcut("cmdorctrl+=", func() {
450- log.Printf("Shortcut: Zoom In")
451- if currentURL == "" { return }
452- if z, err := browserService.ZoomIn(ctx, currentURL); err == nil { _ = view.SetZoom(z) }
453- })
454- _ = view.RegisterKeyboardShortcut("cmdorctrl+plus", func() {
455- log.Printf("Shortcut: Zoom In")
456- if currentURL == "" { return }
457- if z, err := browserService.ZoomIn(ctx, currentURL); err == nil { _ = view.SetZoom(z) }
458- })
459- // Zoom Out
460- _ = view.RegisterKeyboardShortcut("cmdorctrl+-", func() {
461- log.Printf("Shortcut: Zoom Out")
462- if currentURL == "" { return }
463- if z, err := browserService.ZoomOut(ctx, currentURL); err == nil { _ = view.SetZoom(z) }
464- })
465- // Zoom Reset
466- _ = view.RegisterKeyboardShortcut("cmdorctrl+0", func() {
467- log.Printf("Shortcut: Zoom Reset")
468- if currentURL == "" { return }
469- if z, err := browserService.ResetZoom(ctx, currentURL); err == nil { _ = view.SetZoom(z) }
470- })
476+ // Zoom handled natively in webkit package (built-in shortcuts)
471477 }
472478
473479 // Enter GTK main loop only when native backend is available.
@@ -121,6 +121,24 @@ func runBrowser() {
121 defer runtime.UnlockOSThread()121 defer runtime.UnlockOSThread()
122 }122 }
123 123
124+ // Proactively harden media pipeline to avoid WebProcess crashes from buggy VAAPI/plugins.
125+ // Can be disabled with DUMBER_MEDIA_SAFE=0
126+ if os.Getenv("DUMBER_MEDIA_SAFE") != "0" {
127+ // Demote VAAPI-related elements so they won't be auto-picked
128+ if os.Getenv("GST_PLUGIN_FEATURE_RANK") == "" {
129+ os.Setenv("GST_PLUGIN_FEATURE_RANK", "vaapisink:0,vaapidecodebin:0,vaapih264dec:0,vaapivideoconvert:0")
130+ }
131+ // Provide a safe audio sink to avoid autoaudiosink failures
132+ if os.Getenv("GST_AUDIO_SINK") == "" {
133+ os.Setenv("GST_AUDIO_SINK", "fakesink")
134+ }
135+ // Optionally disable media stream features that can engage complex pipelines
136+ if os.Getenv("WEBKIT_DISABLE_MEDIA_STREAM") == "" {
137+ os.Setenv("WEBKIT_DISABLE_MEDIA_STREAM", "1")
138+ }
139+ log.Printf("[media] Safe mode enabled (override with DUMBER_MEDIA_SAFE=0)")
140+ }
141+
124 if err := config.Init(); err != nil {142 if err := config.Init(); err != nil {
125 log.Fatalf("Failed to initialize config: %v", err)143 log.Fatalf("Failed to initialize config: %v", err)
126 }144 }
@@ -142,7 +160,8 @@ func runBrowser() {
142 // JS controls injection is disabled; using native shortcuts via GTK.160 // JS controls injection is disabled; using native shortcuts via GTK.
143 161
144 // Track current URL for zoom persistence162 // Track current URL for zoom persistence
145- var currentURL string163+ // Initialize to the initial homepage so shortcuts work immediately
164+ var currentURL string = "dumb://homepage"
146 165
147 // Register custom scheme resolver for dumb://166 // Register custom scheme resolver for dumb://
148 webkit.SetURISchemeResolver(func(uri string) (string, []byte, bool) {167 webkit.SetURISchemeResolver(func(uri string) (string, []byte, bool) {
@@ -169,6 +188,14 @@ func runBrowser() {
169 if strings.HasPrefix(path, "/api/") { path = strings.TrimPrefix(path, "/api") }188 if strings.HasPrefix(path, "/api/") { path = strings.TrimPrefix(path, "/api") }
170 if path == "/api" { path = "/" }189 if path == "/api" { path = "/" }
171 switch {190 switch {
191+ case strings.HasPrefix(path, "/rendering/status"):
192+ log.Printf("[api] GET /rendering/status")
193+ // Report configured rendering mode; runtime GPU state depends on WebKit internals
194+ resp := struct{
195+ Mode string `json:"mode"`
196+ }{Mode: string(cfg.RenderingMode)}
197+ b, _ := json.Marshal(resp)
198+ return "application/json; charset=utf-8", b, true
172 case strings.HasPrefix(path, "/config"):199 case strings.HasPrefix(path, "/config"):
173 log.Printf("[api] GET /config")200 log.Printf("[api] GET /config")
174 // Build config info201 // Build config info
@@ -290,6 +317,7 @@ func runBrowser() {
290 DefaultSerifFont: cfg.Appearance.SerifFont,317 DefaultSerifFont: cfg.Appearance.SerifFont,
291 DefaultMonospaceFont: cfg.Appearance.MonospaceFont,318 DefaultMonospaceFont: cfg.Appearance.MonospaceFont,
292 DefaultFontSize: cfg.Appearance.DefaultFontSize,319 DefaultFontSize: cfg.Appearance.DefaultFontSize,
320+ Rendering: webkit.RenderingConfig{Mode: string(cfg.RenderingMode)},
293 })321 })
294 if err != nil {322 if err != nil {
295 log.Printf("Warning: failed to create WebView: %v", err)323 log.Printf("Warning: failed to create WebView: %v", err)
@@ -325,6 +353,7 @@ func runBrowser() {
325 URL string `json:"url"`353 URL string `json:"url"`
326 Q string `json:"q"`354 Q string `json:"q"`
327 Limit int `json:"limit"`355 Limit int `json:"limit"`
356+ Value string `json:"value"`
328 // Wails fetch bridge357 // Wails fetch bridge
329 ID string `json:"id"`358 ID string `json:"id"`
330 Payload json.RawMessage `json:"payload"`359 Payload json.RawMessage `json:"payload"`
@@ -397,7 +426,7 @@ func runBrowser() {
397 _ = view.InjectScript("window.__dumber_wails_resolve('" + m.ID + "', '{}')")426 _ = view.InjectScript("window.__dumber_wails_resolve('" + m.ID + "', '{}')")
398 }427 }
399 case "theme":428 case "theme":
400- if m.URL != "" { log.Printf("[theme] page reported color-scheme: %s", m.URL) }429+ if m.Value != "" { log.Printf("[theme] page reported color-scheme: %s", m.Value) }
401 }430 }
402 })431 })
403 log.Printf("Showing WebView window…")432 log.Printf("Showing WebView window…")
@@ -437,7 +466,6 @@ func runBrowser() {
437 466
438 // Register basic keyboard shortcuts on the native view to preserve behavior467 // Register basic keyboard shortcuts on the native view to preserve behavior
439 if view != nil {468 if view != nil {
440- ctx := context.Background()
441 // DevTools469 // DevTools
442 _ = view.RegisterKeyboardShortcut("F12", func() { log.Printf("Shortcut: F12 (devtools)"); _ = view.ShowDevTools() })470 _ = view.RegisterKeyboardShortcut("F12", func() { log.Printf("Shortcut: F12 (devtools)"); _ = view.ShowDevTools() })
443 // Omnibox (Ctrl+L): rely on injected script listener471 // Omnibox (Ctrl+L): rely on injected script listener
@@ -445,29 +473,7 @@ func runBrowser() {
445 log.Printf("Shortcut: Omnibox toggle")473 log.Printf("Shortcut: Omnibox toggle")
446 _ = view.InjectScript("window.__dumber_toggle && window.__dumber_toggle()")474 _ = view.InjectScript("window.__dumber_toggle && window.__dumber_toggle()")
447 })475 })
448- // Zoom In476+ // Zoom handled natively in webkit package (built-in shortcuts)
449- _ = view.RegisterKeyboardShortcut("cmdorctrl+=", func() {
450- log.Printf("Shortcut: Zoom In")
451- if currentURL == "" { return }
452- if z, err := browserService.ZoomIn(ctx, currentURL); err == nil { _ = view.SetZoom(z) }
453- })
454- _ = view.RegisterKeyboardShortcut("cmdorctrl+plus", func() {
455- log.Printf("Shortcut: Zoom In")
456- if currentURL == "" { return }
457- if z, err := browserService.ZoomIn(ctx, currentURL); err == nil { _ = view.SetZoom(z) }
458- })
459- // Zoom Out
460- _ = view.RegisterKeyboardShortcut("cmdorctrl+-", func() {
461- log.Printf("Shortcut: Zoom Out")
462- if currentURL == "" { return }
463- if z, err := browserService.ZoomOut(ctx, currentURL); err == nil { _ = view.SetZoom(z) }
464- })
465- // Zoom Reset
466- _ = view.RegisterKeyboardShortcut("cmdorctrl+0", func() {
467- log.Printf("Shortcut: Zoom Reset")
468- if currentURL == "" { return }
469- if z, err := browserService.ResetZoom(ctx, currentURL); err == nil { _ = view.SetZoom(z) }
470- })
471 }477 }
472 478
473 // Enter GTK main loop only when native backend is available.479 // Enter GTK main loop only when native backend is available.
M pkg/webkit/build_cgo_stub.go +2 -3
@@ -3,8 +3,8 @@
33 package webkit
44
55 /*
6-#cgo pkg-config: webkit2gtk-4.0 gtk+-3.0
7-#include <webkit2/webkit2.h>
6+#cgo pkg-config: webkitgtk-6.0 gtk4
7+#include <webkit/webkit.h>
88 #include <gtk/gtk.h>
99 */
1010 import "C"
@@ -12,4 +12,3 @@ import "C"
1212 // This file documents intended CGO linkage. It is excluded from builds unless
1313 // the 'webkit_cgo' build tag is specified. Real implementations will move APIs
1414 // from the stubs into CGO-backed functions and types.
15-
@@ -3,8 +3,8 @@
3 package webkit3 package webkit
4 4
5 /*5 /*
6-#cgo pkg-config: webkit2gtk-4.0 gtk+-3.06+#cgo pkg-config: webkitgtk-6.0 gtk4
7-#include <webkit2/webkit2.h>7+#include <webkit/webkit.h>
8 #include <gtk/gtk.h>8 #include <gtk/gtk.h>
9 */9 */
10 import "C"10 import "C"
@@ -12,4 +12,3 @@ import "C"
12 // This file documents intended CGO linkage. It is excluded from builds unless12 // This file documents intended CGO linkage. It is excluded from builds unless
13 // the 'webkit_cgo' build tag is specified. Real implementations will move APIs13 // the 'webkit_cgo' build tag is specified. Real implementations will move APIs
14 // from the stubs into CGO-backed functions and types.14 // from the stubs into CGO-backed functions and types.
15-
M pkg/webkit/devtools_cgo.go +2 -3
@@ -3,8 +3,8 @@
33 package webkit
44
55 /*
6-#cgo pkg-config: webkit2gtk-4.0 gtk+-3.0
7-#include <webkit2/webkit2.h>
6+#cgo pkg-config: webkitgtk-6.0 gtk4
7+#include <webkit/webkit.h>
88 */
99 import "C"
1010
@@ -23,4 +23,3 @@ func (w *WebView) CloseDevTools() error {
2323 C.webkit_web_inspector_close(insp)
2424 return nil
2525 }
26-
@@ -3,8 +3,8 @@
3 package webkit3 package webkit
4 4
5 /*5 /*
6-#cgo pkg-config: webkit2gtk-4.0 gtk+-3.06+#cgo pkg-config: webkitgtk-6.0 gtk4
7-#include <webkit2/webkit2.h>7+#include <webkit/webkit.h>
8 */8 */
9 import "C"9 import "C"
10 10
@@ -23,4 +23,3 @@ func (w *WebView) CloseDevTools() error {
23 C.webkit_web_inspector_close(insp)23 C.webkit_web_inspector_close(insp)
24 return nil24 return nil
25 }25 }
26-
M pkg/webkit/keyboard_cgo.go +73 -2
@@ -3,9 +3,43 @@
33 package webkit
44
55 /*
6-#cgo pkg-config: gtk+-3.0
6+#cgo pkg-config: gtk4
77 #include <gtk/gtk.h>
88 #include <gdk/gdk.h>
9+
10+// Declarations for Go callbacks
11+extern void goOnKeyPress(unsigned long id, unsigned int keyval, GdkModifierType state);
12+extern void goOnButtonPress(unsigned long id, unsigned int button, GdkModifierType state);
13+
14+// Key controller callback -> forward to Go
15+static gboolean on_key_pressed(GtkEventControllerKey* controller, guint keyval, guint keycode, GdkModifierType state, gpointer user_data) {
16+ (void)controller; (void)keycode;
17+ goOnKeyPress((unsigned long)user_data, keyval, state);
18+ return FALSE; // do not stop other handlers
19+}
20+
21+// Mouse click gesture -> forward side buttons to Go
22+static void on_click_pressed(GtkGestureClick* gesture, gint n_press, gdouble x, gdouble y, gpointer user_data) {
23+ (void)n_press; (void)x; (void)y;
24+ guint button = gtk_gesture_single_get_current_button(GTK_GESTURE_SINGLE(gesture));
25+ // Pass 0 for modifier state (not provided via gesture API)
26+ goOnButtonPress((unsigned long)user_data, button, 0);
27+}
28+
29+// Attach controllers to a widget (GTK4)
30+static void attach_key_controller(GtkWidget* widget, unsigned long id) {
31+ if (!widget) return;
32+ GtkEventController* keyc = gtk_event_controller_key_new();
33+ g_signal_connect_data(keyc, "key-pressed", G_CALLBACK(on_key_pressed), (gpointer)id, NULL, 0);
34+ gtk_widget_add_controller(widget, keyc);
35+}
36+
37+static void attach_mouse_gesture(GtkWidget* widget, unsigned long id) {
38+ if (!widget) return;
39+ GtkGesture* click = gtk_gesture_click_new();
40+ g_signal_connect_data(click, "pressed", G_CALLBACK(on_click_pressed), (gpointer)id, NULL, 0);
41+ gtk_widget_add_controller(widget, GTK_EVENT_CONTROLLER(click));
42+}
943 */
1044 import "C"
1145
@@ -63,7 +97,8 @@ func dispatchAccelerator(uid uintptr, keyval uint, state uint) {
6397 keyName = "plus"
6498 case uint(C.GDK_KEY_equal):
6599 keyName = "="
66- case uint(C.GDK_KEY_minus), uint(C.GDK_KEY_KP_Subtract):
100+ case uint(C.GDK_KEY_minus), uint(C.GDK_KEY_KP_Subtract), uint(C.GDK_KEY_underscore):
101+ // Treat underscore as minus for layouts where '-' requires Shift (e.g., AZERTY)
67102 keyName = "-"
68103 case uint(C.GDK_KEY_0), uint(C.GDK_KEY_KP_0):
69104 keyName = "0"
@@ -123,6 +158,42 @@ func unregisterView(id uintptr) {
123158 regMu.Unlock()
124159 }
125160
161+// AttachKeyboardControllers attaches GTK4 key and mouse controllers to the view widget.
162+func AttachKeyboardControllers(w *WebView) {
163+ if w == nil || w.native == nil || w.native.view == nil {
164+ return
165+ }
166+ C.attach_key_controller(w.native.view, C.ulong(w.id))
167+ if w.native.win != nil {
168+ C.attach_key_controller(w.native.win, C.ulong(w.id))
169+ }
170+ C.attach_mouse_gesture(w.native.view, C.ulong(w.id))
171+ log.Printf("[input] GTK4 key/mouse controllers attached")
172+}
173+
174+//export goOnThemeChanged
175+func goOnThemeChanged(id C.ulong, preferDark C.int) {
176+ uid := uintptr(id)
177+ regMu.RLock()
178+ vw := viewByID[uid]
179+ regMu.RUnlock()
180+ if vw == nil {
181+ return
182+ }
183+ // Inject runtime theme update into the page
184+ dval := "false"
185+ if int(preferDark) != 0 { dval = "true" }
186+ js := fmt.Sprintf(`(() => { try { const d=%s; const cs=d?'dark':'light'; console.log('[dumber] theme change:'+cs);
187+let m=document.querySelector('meta[name="color-scheme"]');
188+if(!m){ m=document.createElement('meta'); m.name='color-scheme'; document.head.appendChild(m); }
189+m.setAttribute('content', d ? 'dark light' : 'light dark');
190+let s=document.getElementById('__dumber_theme_style');
191+if(!s){ s=document.createElement('style'); s.id='__dumber_theme_style'; document.documentElement.appendChild(s); }
192+s.textContent=':root{color-scheme:' + (d?'dark':'light') + ';}';
193+} catch(e) { console.warn('[dumber] theme runtime update failed', e); } })();`, dval)
194+ _ = vw.InjectScript(js)
195+}
196+
126197 //export goOnUcmMessage
127198 func goOnUcmMessage(id C.ulong, json *C.char) {
128199 uid := uintptr(id)
@@ -3,9 +3,43 @@
3 package webkit3 package webkit
4 4
5 /*5 /*
6-#cgo pkg-config: gtk+-3.06+#cgo pkg-config: gtk4
7 #include <gtk/gtk.h>7 #include <gtk/gtk.h>
8 #include <gdk/gdk.h>8 #include <gdk/gdk.h>
9+
10+// Declarations for Go callbacks
11+extern void goOnKeyPress(unsigned long id, unsigned int keyval, GdkModifierType state);
12+extern void goOnButtonPress(unsigned long id, unsigned int button, GdkModifierType state);
13+
14+// Key controller callback -> forward to Go
15+static gboolean on_key_pressed(GtkEventControllerKey* controller, guint keyval, guint keycode, GdkModifierType state, gpointer user_data) {
16+ (void)controller; (void)keycode;
17+ goOnKeyPress((unsigned long)user_data, keyval, state);
18+ return FALSE; // do not stop other handlers
19+}
20+
21+// Mouse click gesture -> forward side buttons to Go
22+static void on_click_pressed(GtkGestureClick* gesture, gint n_press, gdouble x, gdouble y, gpointer user_data) {
23+ (void)n_press; (void)x; (void)y;
24+ guint button = gtk_gesture_single_get_current_button(GTK_GESTURE_SINGLE(gesture));
25+ // Pass 0 for modifier state (not provided via gesture API)
26+ goOnButtonPress((unsigned long)user_data, button, 0);
27+}
28+
29+// Attach controllers to a widget (GTK4)
30+static void attach_key_controller(GtkWidget* widget, unsigned long id) {
31+ if (!widget) return;
32+ GtkEventController* keyc = gtk_event_controller_key_new();
33+ g_signal_connect_data(keyc, "key-pressed", G_CALLBACK(on_key_pressed), (gpointer)id, NULL, 0);
34+ gtk_widget_add_controller(widget, keyc);
35+}
36+
37+static void attach_mouse_gesture(GtkWidget* widget, unsigned long id) {
38+ if (!widget) return;
39+ GtkGesture* click = gtk_gesture_click_new();
40+ g_signal_connect_data(click, "pressed", G_CALLBACK(on_click_pressed), (gpointer)id, NULL, 0);
41+ gtk_widget_add_controller(widget, GTK_EVENT_CONTROLLER(click));
42+}
9 */43 */
10 import "C"44 import "C"
11 45
@@ -63,7 +97,8 @@ func dispatchAccelerator(uid uintptr, keyval uint, state uint) {
63 keyName = "plus"97 keyName = "plus"
64 case uint(C.GDK_KEY_equal):98 case uint(C.GDK_KEY_equal):
65 keyName = "="99 keyName = "="
66- case uint(C.GDK_KEY_minus), uint(C.GDK_KEY_KP_Subtract):100+ case uint(C.GDK_KEY_minus), uint(C.GDK_KEY_KP_Subtract), uint(C.GDK_KEY_underscore):
101+ // Treat underscore as minus for layouts where '-' requires Shift (e.g., AZERTY)
67 keyName = "-"102 keyName = "-"
68 case uint(C.GDK_KEY_0), uint(C.GDK_KEY_KP_0):103 case uint(C.GDK_KEY_0), uint(C.GDK_KEY_KP_0):
69 keyName = "0"104 keyName = "0"
@@ -123,6 +158,42 @@ func unregisterView(id uintptr) {
123 regMu.Unlock()158 regMu.Unlock()
124 }159 }
125 160
161+// AttachKeyboardControllers attaches GTK4 key and mouse controllers to the view widget.
162+func AttachKeyboardControllers(w *WebView) {
163+ if w == nil || w.native == nil || w.native.view == nil {
164+ return
165+ }
166+ C.attach_key_controller(w.native.view, C.ulong(w.id))
167+ if w.native.win != nil {
168+ C.attach_key_controller(w.native.win, C.ulong(w.id))
169+ }
170+ C.attach_mouse_gesture(w.native.view, C.ulong(w.id))
171+ log.Printf("[input] GTK4 key/mouse controllers attached")
172+}
173+
174+//export goOnThemeChanged
175+func goOnThemeChanged(id C.ulong, preferDark C.int) {
176+ uid := uintptr(id)
177+ regMu.RLock()
178+ vw := viewByID[uid]
179+ regMu.RUnlock()
180+ if vw == nil {
181+ return
182+ }
183+ // Inject runtime theme update into the page
184+ dval := "false"
185+ if int(preferDark) != 0 { dval = "true" }
186+ js := fmt.Sprintf(`(() => { try { const d=%s; const cs=d?'dark':'light'; console.log('[dumber] theme change:'+cs);
187+let m=document.querySelector('meta[name="color-scheme"]');
188+if(!m){ m=document.createElement('meta'); m.name='color-scheme'; document.head.appendChild(m); }
189+m.setAttribute('content', d ? 'dark light' : 'light dark');
190+let s=document.getElementById('__dumber_theme_style');
191+if(!s){ s=document.createElement('style'); s.id='__dumber_theme_style'; document.documentElement.appendChild(s); }
192+s.textContent=':root{color-scheme:' + (d?'dark':'light') + ';}';
193+} catch(e) { console.warn('[dumber] theme runtime update failed', e); } })();`, dval)
194+ _ = vw.InjectScript(js)
195+}
196+
126 //export goOnUcmMessage197 //export goOnUcmMessage
127 func goOnUcmMessage(id C.ulong, json *C.char) {198 func goOnUcmMessage(id C.ulong, json *C.char) {
128 uid := uintptr(id)199 uid := uintptr(id)
M pkg/webkit/loop_cgo.go +13 -6
@@ -3,18 +3,25 @@
33 package webkit
44
55 /*
6-#cgo pkg-config: gtk+-3.0
6+#cgo pkg-config: gtk4
77 #include <gtk/gtk.h>
8+#include <glib.h>
89 */
910 import "C"
1011
11-// RunMainLoop enters the GTK main loop and blocks until quit.
12+var mainLoop *C.GMainLoop
13+
14+// RunMainLoop enters the GLib main loop (GTK4) and blocks until quit.
1215 func RunMainLoop() {
13- C.gtk_main()
16+ if mainLoop == nil {
17+ mainLoop = C.g_main_loop_new(nil, C.gboolean(0))
18+ }
19+ C.g_main_loop_run(mainLoop)
1420 }
1521
16-// QuitMainLoop requests the GTK main loop to exit.
22+// QuitMainLoop requests the main loop to exit.
1723 func QuitMainLoop() {
18- C.gtk_main_quit()
24+ if mainLoop != nil {
25+ C.g_main_loop_quit(mainLoop)
26+ }
1927 }
20-
@@ -3,18 +3,25 @@
3 package webkit3 package webkit
4 4
5 /*5 /*
6-#cgo pkg-config: gtk+-3.06+#cgo pkg-config: gtk4
7 #include <gtk/gtk.h>7 #include <gtk/gtk.h>
8+#include <glib.h>
8 */9 */
9 import "C"10 import "C"
10 11
11-// RunMainLoop enters the GTK main loop and blocks until quit.12+var mainLoop *C.GMainLoop
13+
14+// RunMainLoop enters the GLib main loop (GTK4) and blocks until quit.
12 func RunMainLoop() {15 func RunMainLoop() {
13- C.gtk_main()16+ if mainLoop == nil {
17+ mainLoop = C.g_main_loop_new(nil, C.gboolean(0))
18+ }
19+ C.g_main_loop_run(mainLoop)
14 }20 }
15 21
16-// QuitMainLoop requests the GTK main loop to exit.22+// QuitMainLoop requests the main loop to exit.
17 func QuitMainLoop() {23 func QuitMainLoop() {
18- C.gtk_main_quit()24+ if mainLoop != nil {
25+ C.g_main_loop_quit(mainLoop)
26+ }
19 }27 }
20-
A pkg/webkit/scheme.go +9 -0
@@ -0,0 +1,9 @@
1+//go:build !webkit_cgo
2+
3+package webkit
4+
5+// SchemeResolver provides bytes and mime for a given custom URI.
6+type SchemeResolver func(uri string) (mime string, data []byte, ok bool)
7+
8+// SetURISchemeResolver is a no-op in non-CGO builds.
9+func SetURISchemeResolver(r SchemeResolver) { /* no-op */ }
@@ -0,0 +1,9 @@
1+//go:build !webkit_cgo
2+
3+package webkit
4+
5+// SchemeResolver provides bytes and mime for a given custom URI.
6+type SchemeResolver func(uri string) (mime string, data []byte, ok bool)
7+
8+// SetURISchemeResolver is a no-op in non-CGO builds.
9+func SetURISchemeResolver(r SchemeResolver) { /* no-op */ }
M pkg/webkit/script_cgo.go +2 -2
@@ -3,9 +3,9 @@
33 package webkit
44
55 /*
6-#cgo pkg-config: webkit2gtk-4.0 gtk+-3.0
6+#cgo pkg-config: webkitgtk-6.0 gtk4
77 #include <stdlib.h>
8-#include <webkit2/webkit2.h>
8+#include <webkit/webkit.h>
99 */
1010 import "C"
1111
@@ -3,9 +3,9 @@
3 package webkit3 package webkit
4 4
5 /*5 /*
6-#cgo pkg-config: webkit2gtk-4.0 gtk+-3.06+#cgo pkg-config: webkitgtk-6.0 gtk4
7 #include <stdlib.h>7 #include <stdlib.h>
8-#include <webkit2/webkit2.h>8+#include <webkit/webkit.h>
9 */9 */
10 import "C"10 import "C"
11 11
M pkg/webkit/settings.go +11 -0
@@ -25,4 +25,15 @@ type Config struct {
2525 DefaultSerifFont string
2626 DefaultMonospaceFont string
2727 DefaultFontSize int // CSS px (~points)
28+
29+ // Rendering controls GPU/CPU selection and debug options
30+ Rendering RenderingConfig
31+}
32+
33+// RenderingConfig controls hardware acceleration preferences.
34+// Mode accepts: "auto" (default), "gpu", or "cpu".
35+// DebugGPU enables compositing indicators if supported.
36+type RenderingConfig struct {
37+ Mode string
38+ DebugGPU bool
2839 }
@@ -25,4 +25,15 @@ type Config struct {
25 DefaultSerifFont string25 DefaultSerifFont string
26 DefaultMonospaceFont string26 DefaultMonospaceFont string
27 DefaultFontSize int // CSS px (~points)27 DefaultFontSize int // CSS px (~points)
28+
29+ // Rendering controls GPU/CPU selection and debug options
30+ Rendering RenderingConfig
31+}
32+
33+// RenderingConfig controls hardware acceleration preferences.
34+// Mode accepts: "auto" (default), "gpu", or "cpu".
35+// DebugGPU enables compositing indicators if supported.
36+type RenderingConfig struct {
37+ Mode string
38+ DebugGPU bool
28 }39 }
M pkg/webkit/webview_cgo.go +252 -80
@@ -3,21 +3,53 @@
33 package webkit
44
55 /*
6-#cgo pkg-config: webkit2gtk-4.0 gtk+-3.0 javascriptcoregtk-4.0
6+#cgo pkg-config: webkitgtk-6.0 gtk4 javascriptcoregtk-6.0
77 #include <stdlib.h>
88 #include <gtk/gtk.h>
9-#include <webkit2/webkit2.h>
9+#include <webkit/webkit.h>
1010 #include <glib-object.h>
1111 #include <gdk/gdk.h>
1212 #include <jsc/jsc.h>
1313 #include <glib.h>
1414 #include <gio/gio.h>
1515
16-static GtkWidget* new_window() { return gtk_window_new(GTK_WINDOW_TOPLEVEL); }
17-static void connect_destroy_quit(GtkWidget* w) { g_signal_connect(w, "destroy", G_CALLBACK(gtk_main_quit), NULL); }
16+static GtkWidget* new_window() { return GTK_WIDGET(gtk_window_new()); }
17+// GTK4: gtk_main_quit removed; leave as no-op for now (Quit handled in Go)
18+static void connect_destroy_quit(GtkWidget* w) { (void)w; }
1819 static WebKitWebView* as_webview(GtkWidget* w) { return WEBKIT_WEB_VIEW(w); }
19-static WebKitWebsiteDataManager* make_wdm(const gchar* data, const gchar* cache){
20- return webkit_website_data_manager_new("base-data-directory", data, "base-cache-directory", cache, NULL);
20+// WebsiteDataManager creation will be handled via GTK4/WebKit6 APIs in Go code.
21+
22+// Forward declare helpers used below
23+static void maybe_set_cookie_policy(WebKitCookieManager* cm, int policy);
24+
25+// Construct a WebKitWebView via g_object_new with a fresh UserContentManager.
26+static GtkWidget* new_webview_with_ucm_and_session(const char* data_dir, const char* cache_dir, const char* cookie_path, WebKitUserContentManager** out_ucm) {
27+ WebKitUserContentManager* u = webkit_user_content_manager_new();
28+ if (!u) return NULL;
29+ // WebKitGTK 6: create NetworkSession with data/cache directories
30+ WebKitNetworkSession* sess = webkit_network_session_new(
31+ data_dir ? data_dir : "",
32+ cache_dir ? cache_dir : "");
33+ if (!sess) { g_object_unref(u); return NULL; }
34+ // Configure cookie persistence and policy (no third-party by default)
35+ WebKitCookieManager* cm = webkit_network_session_get_cookie_manager(sess);
36+ if (cm) {
37+ if (cookie_path && cookie_path[0]) {
38+ webkit_cookie_manager_set_persistent_storage(cm, cookie_path, WEBKIT_COOKIE_PERSISTENT_STORAGE_SQLITE);
39+ }
40+ // 2 => no-third-party (default), 0 => always, 1 => never
41+ maybe_set_cookie_policy(cm, 2);
42+ }
43+ // Persist credentials (HTTP auth, etc.)
44+ webkit_network_session_set_persistent_credential_storage_enabled(sess, TRUE);
45+ GtkWidget* w = GTK_WIDGET(g_object_new(WEBKIT_TYPE_WEB_VIEW,
46+ "user-content-manager", u,
47+ "network-session", sess,
48+ NULL));
49+ // WebView holds refs to provided objects; drop our temporary refs
50+ g_object_unref(sess);
51+ if (out_ucm) { *out_ucm = u; }
52+ return w;
2153 }
2254
2355 static gboolean gtk_prefers_dark() {
@@ -31,28 +63,12 @@ static gboolean gtk_prefers_dark() {
3163 // Note: preferred color scheme is handled via a user script injection to support
3264 // older WebKitGTK versions without the color-scheme API. See enableUserContentManager.
3365
34-// Forward declaration of Go callback
35-extern void goOnKeyPress(unsigned long id, guint keyval, GdkModifierType state);
36-extern void goOnButtonPress(unsigned long id, guint button, GdkModifierType state);
3766 extern void goOnUcmMessage(unsigned long id, const char* json);
3867 extern void goOnTitleChanged(unsigned long id, const char* title);
3968 extern void goOnURIChanged(unsigned long id, const char* uri);
69+extern void goOnThemeChanged(unsigned long id, int prefer_dark);
4070 extern void* goResolveURIScheme(char* uri, size_t* out_len, char** out_mime);
4171
42-gboolean on_key_press(GtkWidget* widget, GdkEventKey* event, gpointer user_data) {
43- (void)widget;
44- unsigned long id = (unsigned long)user_data;
45- goOnKeyPress(id, event->keyval, event->state);
46- return FALSE; // Propagate
47-}
48-
49-gboolean on_button_press(GtkWidget* widget, GdkEventButton* event, gpointer user_data) {
50- (void)widget;
51- unsigned long id = (unsigned long)user_data;
52- goOnButtonPress(id, event->button, event->state);
53- return FALSE;
54-}
55-
5672 void on_title_notify(GObject* obj, GParamSpec* pspec, gpointer user_data) {
5773 (void)pspec;
5874 WebKitWebView* view = WEBKIT_WEB_VIEW(obj);
@@ -75,19 +91,17 @@ void on_uri_notify(GObject* obj, GParamSpec* pspec, gpointer user_data) {
7591 if (uri) { goOnURIChanged((unsigned long)user_data, uri); }
7692 }
7793
78-static gchar* js_result_to_utf8(WebKitJavascriptResult* r) {
79- JSCValue* v = webkit_javascript_result_get_js_value(r);
80- // Returns newly-allocated string; free with g_free()
81- return jsc_value_to_string(v);
94+// React to GTK theme preference changes at runtime
95+void on_theme_changed(GObject* obj, GParamSpec* pspec, gpointer user_data) {
96+ (void)pspec;
97+ GtkSettings* settings = GTK_SETTINGS(obj);
98+ if (!settings) return;
99+ gboolean prefer = FALSE;
100+ g_object_get(settings, "gtk-application-prefer-dark-theme", &prefer, NULL);
101+ goOnThemeChanged((unsigned long)user_data, prefer ? 1 : 0);
82102 }
83103
84-void on_ucm_message(WebKitUserContentManager* m, WebKitJavascriptResult* r, gpointer user_data) {
85- (void)m;
86- unsigned long id = (unsigned long)user_data;
87- gchar* s = js_result_to_utf8(r);
88- goOnUcmMessage(id, s);
89- g_free(s);
90-}
104+// NOTE: UCM message callback signature changed in WebKitGTK 6; will be reimplemented later.
91105
92106 void on_uri_scheme(WebKitURISchemeRequest* request, gpointer user_data) {
93107 (void)user_data;
@@ -106,6 +120,107 @@ void on_uri_scheme(WebKitURISchemeRequest* request, gpointer user_data) {
106120 g_error_free(err);
107121 }
108122 }
123+
124+// Script message handler wiring for WebKitGTK 6
125+// Forward declaration for callback used in g_signal_connect_data
126+// WebKitGTK 6 delivers JSCValue* for script-message-received::NAME
127+static void ucm_on_script_message_cb(WebKitUserContentManager* ucm, JSCValue* val, gpointer user_data);
128+// Uses WebKitScriptMessage and script-message-received::NAME signal.
129+static void ucm_on_script_message_cb(WebKitUserContentManager* ucm, JSCValue* val, gpointer user_data) {
130+ (void)ucm;
131+ if (!val) return;
132+ gchar* s = jsc_value_to_string(val);
133+ if (!s) return;
134+ goOnUcmMessage((unsigned long)user_data, s);
135+ g_free(s);
136+}
137+
138+static gboolean register_ucm_handler(WebKitUserContentManager* ucm, const char* name, unsigned long id) {
139+ if (!ucm || !name) return FALSE;
140+ // WebKitGTK 6 signature: (ucm, name, world_name)
141+ webkit_user_content_manager_register_script_message_handler(ucm, name, NULL);
142+ gchar* signal = g_strdup_printf("script-message-received::%s", name);
143+ if (!signal) return FALSE;
144+ g_signal_connect_data(G_OBJECT(ucm), signal, G_CALLBACK(ucm_on_script_message_cb), (gpointer)id, NULL, 0);
145+ g_free(signal);
146+ return TRUE;
147+}
148+
149+// ----- Conditional helpers for settings across WebKit versions -----
150+#ifndef WEBKIT_CHECK_VERSION
151+#define WEBKIT_CHECK_VERSION(major,minor,micro) (0)
152+#endif
153+
154+static void maybe_set_hw_policy(WebKitSettings* settings, int mode) {
155+#if WEBKIT_CHECK_VERSION(2,40,0)
156+ // mode: 0=ON_DEMAND, 1=ALWAYS, 2=NEVER
157+ if (!settings) return;
158+ if (mode == 1) {
159+ webkit_settings_set_hardware_acceleration_policy(settings, WEBKIT_HARDWARE_ACCELERATION_POLICY_ALWAYS);
160+ } else if (mode == 2) {
161+ webkit_settings_set_hardware_acceleration_policy(settings, WEBKIT_HARDWARE_ACCELERATION_POLICY_NEVER);
162+ } else {
163+ // Leave default policy (auto) without forcing a value to avoid referencing
164+ // WEBKIT_HARDWARE_ACCELERATION_POLICY_ON_DEMAND on older headers.
165+ }
166+#else
167+ (void)settings; (void)mode;
168+#endif
169+}
170+
171+static void maybe_set_draw_indicators(WebKitSettings* settings, int enable) {
172+#if WEBKIT_CHECK_VERSION(2,36,0)
173+ if (!settings) return;
174+ webkit_settings_set_draw_compositing_indicators(settings, enable ? 1 : 0);
175+#else
176+ (void)settings; (void)enable;
177+#endif
178+}
179+
180+static void maybe_set_webgl(WebKitSettings* settings, int enable) {
181+#if WEBKIT_CHECK_VERSION(2,20,0)
182+ if (!settings) return;
183+ webkit_settings_set_enable_webgl(settings, enable ? 1 : 0);
184+#else
185+ (void)settings; (void)enable;
186+#endif
187+}
188+
189+static void maybe_set_canvas_accel(WebKitSettings* settings, int enable) {
190+#if WEBKIT_CHECK_VERSION(2,20,0)
191+ if (!settings) return;
192+ webkit_settings_set_enable_2d_canvas_acceleration(settings, enable ? 1 : 0);
193+#else
194+ (void)settings; (void)enable;
195+#endif
196+}
197+
198+static void maybe_set_media_user_gesture(WebKitSettings* settings, int require) {
199+#if WEBKIT_CHECK_VERSION(2,20,0)
200+ if (!settings) return;
201+ webkit_settings_set_media_playback_requires_user_gesture(settings, require ? 1 : 0);
202+#else
203+ (void)settings; (void)require;
204+#endif
205+}
206+
207+// Set cookie accept policy with compile-time compatibility across WebKit versions
208+static void maybe_set_cookie_policy(WebKitCookieManager* cm, int policy) {
209+ if (!cm) return;
210+#if defined(WEBKIT_COOKIE_ACCEPT_POLICY_ALWAYS)
211+ WebKitCookieAcceptPolicy p = WEBKIT_COOKIE_ACCEPT_POLICY_ALWAYS;
212+ if (policy == 1) p = WEBKIT_COOKIE_ACCEPT_POLICY_NEVER; // never
213+ else if (policy == 2) p = WEBKIT_COOKIE_ACCEPT_POLICY_NO_THIRD_PARTY; // no third-party
214+ webkit_cookie_manager_set_accept_policy(cm, p);
215+#elif defined(WEBKIT_COOKIE_POLICY_ACCEPT_ALWAYS)
216+ WebKitCookieAcceptPolicy p = WEBKIT_COOKIE_POLICY_ACCEPT_ALWAYS;
217+ if (policy == 1) p = WEBKIT_COOKIE_POLICY_ACCEPT_NEVER;
218+ else if (policy == 2) p = WEBKIT_COOKIE_POLICY_ACCEPT_NO_THIRD_PARTY;
219+ webkit_cookie_manager_set_accept_policy(cm, p);
220+#else
221+ (void)policy; // Not supported in this header version
222+#endif
223+}
109224 */
110225 import "C"
111226
@@ -172,7 +287,7 @@ type WebView struct {
172287 // NewWebView constructs a new WebView instance with native WebKit2GTK widgets.
173288 func NewWebView(cfg *Config) (*WebView, error) {
174289 log.Printf("[webkit] Initializing GTK and creating WebView (CGO)")
175- if C.gtk_init_check(nil, nil) == 0 {
290+ if C.gtk_init_check() == 0 {
176291 return nil, errors.New("failed to initialize GTK")
177292 }
178293
@@ -190,12 +305,14 @@ func NewWebView(cfg *Config) (*WebView, error) {
190305
191306 cData := C.CString(dataDir)
192307 cCache := C.CString(cacheDir)
308+ cookieFile := filepath.Join(dataDir, "cookies.db")
309+ cCookie := C.CString(cookieFile)
193310 defer C.free(unsafe.Pointer(cData))
194311 defer C.free(unsafe.Pointer(cCache))
195- wdm := C.make_wdm((*C.gchar)(cData), (*C.gchar)(cCache))
196- if wdm == nil { return nil, errors.New("failed to create WebsiteDataManager") }
197- ctx := C.webkit_web_context_new_with_website_data_manager(wdm)
198- if ctx == nil { return nil, errors.New("failed to create WebContext") }
312+ defer C.free(unsafe.Pointer(cCookie))
313+ // Use default WebContext to register the custom URI scheme handler; the
314+ // WebView itself is created with a WebKitNetworkSession for persistence.
315+ ctx := C.webkit_web_context_get_default()
199316
200317 // Register custom URI scheme handler for "dumb://"
201318 {
@@ -203,19 +320,11 @@ func NewWebView(cfg *Config) (*WebView, error) {
203320 C.webkit_web_context_register_uri_scheme(ctx, sch, (C.WebKitURISchemeRequestCallback)(C.on_uri_scheme), nil, nil)
204321 C.free(unsafe.Pointer(sch))
205322 }
323+ // Cookie manager persistent storage handled via NetworkSession in new_webview_with_ucm_and_session
206324
207- // Cookie manager persistent storage
208- cm := C.webkit_web_context_get_cookie_manager(ctx)
209- if cm != nil {
210- cookiePath := filepath.Join(dataDir, "cookies.sqlite")
211- cCookie := C.CString(cookiePath)
212- defer C.free(unsafe.Pointer(cCookie))
213- C.webkit_cookie_manager_set_persistent_storage(cm, (*C.gchar)(cCookie), C.WEBKIT_COOKIE_PERSISTENT_STORAGE_SQLITE)
214- C.webkit_cookie_manager_set_accept_policy(cm, C.WEBKIT_COOKIE_POLICY_ACCEPT_ALWAYS)
215- }
216-
217- // Create WebView with context
218- viewWidget := C.webkit_web_view_new_with_context(ctx) // GtkWidget*
325+ // Create WebView through g_object_new with a fresh UCM (GTK4/WebKit 6 style)
326+ var createdUcm *C.WebKitUserContentManager
327+ viewWidget := C.new_webview_with_ucm_and_session(cData, cCache, cCookie, &createdUcm)
219328 if viewWidget == nil {
220329 return nil, errors.New("failed to create WebKitWebView")
221330 }
@@ -227,32 +336,92 @@ func NewWebView(cfg *Config) (*WebView, error) {
227336 }
228337
229338 // Pack view widget into the window
230- C.gtk_container_add((*C.GtkContainer)(unsafe.Pointer(win)), viewWidget)
339+ // GTK4: containers removed; use gtk_window_set_child
340+ C.gtk_window_set_child((*C.GtkWindow)(unsafe.Pointer(win)), viewWidget)
231341 C.gtk_window_set_default_size((*C.GtkWindow)(unsafe.Pointer(win)), 1024, 768)
232342 C.connect_destroy_quit(win)
233343
234344 v := &WebView{
235345 config: cfg,
236346 zoom: 1.0,
237- native: &nativeView{win: win, view: viewWidget, wv: C.as_webview(viewWidget), ucm: nil},
347+ native: &nativeView{win: win, view: viewWidget, wv: C.as_webview(viewWidget), ucm: createdUcm},
238348 window: &Window{win: win},
239349 }
240- // Allow receiving button press events
241- C.gtk_widget_add_events(win, C.GDK_BUTTON_PRESS_MASK|C.GDK_BUTTON_RELEASE_MASK)
242- C.gtk_widget_add_events(viewWidget, C.GDK_BUTTON_PRESS_MASK|C.GDK_BUTTON_RELEASE_MASK)
243-
244- // Assign an ID for accelerator dispatch and connect input events
350+ // Assign an ID for accelerator dispatch
245351 v.id = nextViewID()
246352 registerView(v.id, v)
247- C.g_signal_connect_data(C.gpointer(unsafe.Pointer(win)), C.CString("key-press-event"), C.GCallback(C.on_key_press), C.gpointer(v.id), nil, 0)
248- C.g_signal_connect_data(C.gpointer(unsafe.Pointer(viewWidget)), C.CString("button-press-event"), C.GCallback(C.on_button_press), C.gpointer(v.id), nil, 0)
249- C.g_signal_connect_data(C.gpointer(unsafe.Pointer(win)), C.CString("button-press-event"), C.GCallback(C.on_button_press), C.gpointer(v.id), nil, 0)
353+ // Attach GTK4 input controllers (keyboard, mouse)
354+ AttachKeyboardControllers(v)
355+
356+ // Watch for GTK theme changes and propagate to page at runtime
357+ if settings := C.gtk_settings_get_default(); settings != nil {
358+ cprop := C.CString("notify::gtk-application-prefer-dark-theme")
359+ C.g_signal_connect_data(C.gpointer(unsafe.Pointer(settings)), cprop, C.GCallback(C.on_theme_changed), C.gpointer(v.id), nil, 0)
360+ C.free(unsafe.Pointer(cprop))
361+ }
362+
363+ // Native zoom shortcuts (independent of app services)
364+ // Ctrl+='=' and Ctrl+'+' → Zoom In; Ctrl+'-' → Zoom Out; Ctrl+'0' → Reset
365+ _ = v.RegisterKeyboardShortcut("cmdorctrl+=", func() {
366+ nz := v.zoom
367+ if nz <= 0 { nz = 1.0 }
368+ nz *= 1.1
369+ if nz < 0.25 { nz = 0.25 }
370+ if nz > 5.0 { nz = 5.0 }
371+ _ = v.SetZoom(nz)
372+ })
373+ _ = v.RegisterKeyboardShortcut("cmdorctrl+plus", func() {
374+ nz := v.zoom
375+ if nz <= 0 { nz = 1.0 }
376+ nz *= 1.1
377+ if nz < 0.25 { nz = 0.25 }
378+ if nz > 5.0 { nz = 5.0 }
379+ _ = v.SetZoom(nz)
380+ })
381+ _ = v.RegisterKeyboardShortcut("cmdorctrl-", func() {
382+ nz := v.zoom
383+ if nz <= 0 { nz = 1.0 }
384+ nz /= 1.1
385+ if nz < 0.25 { nz = 0.25 }
386+ if nz > 5.0 { nz = 5.0 }
387+ _ = v.SetZoom(nz)
388+ })
389+ _ = v.RegisterKeyboardShortcut("cmdorctrl+0", func() { _ = v.SetZoom(1.0) })
250390 // Update window title when page title changes
251- C.g_signal_connect_data(C.gpointer(unsafe.Pointer(viewWidget)), C.CString("notify::title"), C.GCallback(C.on_title_notify), C.gpointer(unsafe.Pointer(win)), nil, 0)
391+ cNotifyTitle1 := C.CString("notify::title")
392+ C.g_signal_connect_data(C.gpointer(unsafe.Pointer(viewWidget)), cNotifyTitle1, C.GCallback(C.on_title_notify), C.gpointer(unsafe.Pointer(win)), nil, 0)
393+ C.free(unsafe.Pointer(cNotifyTitle1))
252394 // Also dispatch title change to Go with view id
253- C.g_signal_connect_data(C.gpointer(unsafe.Pointer(viewWidget)), C.CString("notify::title"), C.GCallback(C.on_title_notify_id), C.gpointer(v.id), nil, 0)
395+ cNotifyTitle2 := C.CString("notify::title")
396+ C.g_signal_connect_data(C.gpointer(unsafe.Pointer(viewWidget)), cNotifyTitle2, C.GCallback(C.on_title_notify_id), C.gpointer(v.id), nil, 0)
397+ C.free(unsafe.Pointer(cNotifyTitle2))
254398 // Notify URI changes to Go to keep current URL in sync
255- C.g_signal_connect_data(C.gpointer(unsafe.Pointer(viewWidget)), C.CString("notify::uri"), C.GCallback(C.on_uri_notify), C.gpointer(v.id), nil, 0)
399+ cNotifyURI := C.CString("notify::uri")
400+ C.g_signal_connect_data(C.gpointer(unsafe.Pointer(viewWidget)), cNotifyURI, C.GCallback(C.on_uri_notify), C.gpointer(v.id), nil, 0)
401+ C.free(unsafe.Pointer(cNotifyURI))
402+ // Apply hardware acceleration and related settings based on cfg.Rendering
403+ if settings := C.webkit_web_view_get_settings(v.native.wv); settings != nil {
404+ // Hardware acceleration policy (guarded by version)
405+ switch cfg.Rendering.Mode {
406+ case "gpu":
407+ C.maybe_set_hw_policy(settings, 1)
408+ C.maybe_set_webgl(settings, 1)
409+ C.maybe_set_canvas_accel(settings, 1)
410+ case "cpu":
411+ C.maybe_set_hw_policy(settings, 2)
412+ C.maybe_set_webgl(settings, 0)
413+ C.maybe_set_canvas_accel(settings, 0)
414+ default: // auto
415+ C.maybe_set_hw_policy(settings, 0)
416+ C.maybe_set_webgl(settings, 1)
417+ C.maybe_set_canvas_accel(settings, 1)
418+ }
419+ // Optional compositing indicators for debugging (guarded by version)
420+ if cfg.Rendering.DebugGPU { C.maybe_set_draw_indicators(settings, 1) }
421+ // Reduce media pipeline churn by requiring a user gesture for playback
422+ C.maybe_set_media_user_gesture(settings, 1)
423+ }
424+
256425 if cfg.ZoomDefault > 0 {
257426 v.zoom = cfg.ZoomDefault
258427 C.webkit_web_view_set_zoom_level(v.native.wv, C.gdouble(v.zoom))
@@ -295,13 +464,9 @@ func NewWebView(cfg *Config) (*WebView, error) {
295464 runtime.SetFinalizer(v, func(v *WebView) { _ = v.Destroy() })
296465 log.Printf("[webkit] WebView created (CGO)")
297466
298- // Obtain the view's UserContentManager and inject script
299- if v.native != nil && v.native.wv != nil {
300- u := C.webkit_web_view_get_user_content_manager(v.native.wv)
301- if u != nil {
302- v.native.ucm = u
303- v.enableUserContentManager()
304- }
467+ // Initialize UCM scripts and handlers
468+ if v.native != nil && v.native.ucm != nil {
469+ v.enableUserContentManager()
305470 }
306471 return v, nil
307472 }
@@ -322,8 +487,9 @@ func (w *WebView) Show() error {
322487 if w == nil || w.destroyed || w.native == nil || w.native.win == nil {
323488 return ErrNotImplemented
324489 }
325- C.gtk_widget_show(w.native.win)
326- C.gtk_widget_show(w.native.view)
490+ C.gtk_widget_set_visible(w.native.view, C.gboolean(1))
491+ C.gtk_widget_set_visible(w.native.win, C.gboolean(1))
492+ C.gtk_window_present((*C.GtkWindow)(unsafe.Pointer(w.native.win)))
327493 log.Printf("[webkit] Show window")
328494 return nil
329495 }
@@ -332,7 +498,8 @@ func (w *WebView) Hide() error {
332498 if w == nil || w.destroyed || w.native == nil || w.native.win == nil {
333499 return ErrNotImplemented
334500 }
335- C.gtk_widget_hide(w.native.win)
501+ C.gtk_widget_set_visible(w.native.view, C.gboolean(0))
502+ C.gtk_widget_set_visible(w.native.win, C.gboolean(0))
336503 return nil
337504 }
338505
@@ -340,7 +507,8 @@ func (w *WebView) Destroy() error {
340507 if w == nil || w.native == nil || w.native.win == nil {
341508 return ErrNotImplemented
342509 }
343- C.gtk_widget_destroy(w.native.win)
510+ // GTK4: destroy windows via gtk_window_destroy
511+ C.gtk_window_destroy((*C.GtkWindow)(unsafe.Pointer(w.native.win)))
344512 w.destroyed = true
345513 unregisterView(w.id)
346514 log.Printf("[webkit] Destroy window")
@@ -396,9 +564,11 @@ func (w *WebView) enableUserContentManager() {
396564 if w == nil || w.native == nil || w.native.ucm == nil { return }
397565 // Register handler "dumber"
398566 cname := C.CString("dumber")
399- defer C.free(unsafe.Pointer(cname))
400- C.webkit_user_content_manager_register_script_message_handler(w.native.ucm, (*C.gchar)(cname))
401- C.g_signal_connect_data(C.gpointer(unsafe.Pointer(w.native.ucm)), C.CString("script-message-received::dumber"), C.GCallback(C.on_ucm_message), C.gpointer(w.id), nil, 0)
567+ ok := C.register_ucm_handler(w.native.ucm, cname, C.ulong(w.id))
568+ if ok == 0 {
569+ log.Printf("[webkit] Failed to register UCM handler; fallback bridge will handle messages")
570+ }
571+ C.free(unsafe.Pointer(cname))
402572
403573 // Inject color-scheme preference script at document-start to inform sites of system theme
404574 preferDark := C.gtk_prefers_dark() != 0
@@ -409,9 +579,9 @@ func (w *WebView) enableUserContentManager() {
409579 }
410580 var schemeJS string
411581 if preferDark {
412- schemeJS = "(() => { try { const d=true; const cs=d?'dark':'light'; console.log('[dumber] color-scheme set:', cs); try{ window.webkit?.messageHandlers?.dumber?.postMessage(JSON.stringify({type:'theme', value: cs})) }catch(_){} const meta=document.createElement('meta'); meta.name='color-scheme'; meta.content='dark light'; document.documentElement.appendChild(meta); const s=document.createElement('style'); s.textContent=':root{color-scheme:dark;}'; document.documentElement.appendChild(s); const qD='(prefers-color-scheme: dark)'; const qL='(prefers-color-scheme: light)'; const orig=window.matchMedia; window.matchMedia=function(q){ if(typeof q==='string'&&(q.includes(qD)||q.includes(qL))){ const m={matches:q.includes('dark')?d:!d,media:q,onchange:null,addListener(){},removeListener(){},addEventListener(){},removeEventListener(){},dispatchEvent(){return false;}}; return m;} return orig.call(window,q); }; } catch(e){ console.warn('[dumber] color-scheme injection failed', e) } })();"
582+ schemeJS = "(() => { try { const d=true; const cs=d?'dark':'light'; console.log('[dumber] color-scheme set:' + cs); try{ window.webkit?.messageHandlers?.dumber?.postMessage(JSON.stringify({type:'theme', value: cs})) }catch(_){} const meta=document.createElement('meta'); meta.name='color-scheme'; meta.content='dark light'; document.documentElement.appendChild(meta); const s=document.createElement('style'); s.textContent=':root{color-scheme:dark;}'; document.documentElement.appendChild(s); const qD='(prefers-color-scheme: dark)'; const qL='(prefers-color-scheme: light)'; const orig=window.matchMedia; window.matchMedia=function(q){ if(typeof q==='string'&&(q.includes(qD)||q.includes(qL))){ const m={matches:q.includes('dark')?d:!d,media:q,onchange:null,addListener(){},removeListener(){},addEventListener(){},removeEventListener(){},dispatchEvent(){return false;}}; return m;} return orig.call(window,q); }; } catch(e){ console.warn('[dumber] color-scheme injection failed', e) } })();"
413583 } else {
414- schemeJS = "(() => { try { const d=false; const cs=d?'dark':'light'; console.log('[dumber] color-scheme set:', cs); try{ window.webkit?.messageHandlers?.dumber?.postMessage(JSON.stringify({type:'theme', value: cs})) }catch(_){} const meta=document.createElement('meta'); meta.name='color-scheme'; meta.content='light dark'; document.documentElement.appendChild(meta); const s=document.createElement('style'); s.textContent=':root{color-scheme:light;}'; document.documentElement.appendChild(s); const qD='(prefers-color-scheme: dark)'; const qL='(prefers-color-scheme: light)'; const orig=window.matchMedia; window.matchMedia=function(q){ if(typeof q==='string'&&(q.includes(qD)||q.includes(qL))){ const m={matches:q.includes('dark')?d:!d,media:q,onchange:null,addListener(){},removeListener(){},addEventListener(){},removeEventListener(){},dispatchEvent(){return false;}}; return m;} return orig.call(window,q); }; } catch(e){ console.warn('[dumber] color-scheme injection failed', e) } })();"
584+ schemeJS = "(() => { try { const d=false; const cs=d?'dark':'light'; console.log('[dumber] color-scheme set:' + cs); try{ window.webkit?.messageHandlers?.dumber?.postMessage(JSON.stringify({type:'theme', value: cs})) }catch(_){} const meta=document.createElement('meta'); meta.name='color-scheme'; meta.content='light dark'; document.documentElement.appendChild(meta); const s=document.createElement('style'); s.textContent=':root{color-scheme:light;}'; document.documentElement.appendChild(s); const qD='(prefers-color-scheme: dark)'; const qL='(prefers-color-scheme: light)'; const orig=window.matchMedia; window.matchMedia=function(q){ if(typeof q==='string'&&(q.includes(qD)||q.includes(qL))){ const m={matches:q.includes('dark')?d:!d,media:q,onchange:null,addListener(){},removeListener(){},addEventListener(){},removeEventListener(){},dispatchEvent(){return false;}}; return m;} return orig.call(window,q); }; } catch(e){ console.warn('[dumber] color-scheme injection failed', e) } })();"
415585 }
416586 cScheme := C.CString(schemeJS)
417587 defer C.free(unsafe.Pointer(cScheme))
@@ -440,4 +610,6 @@ func (w *WebView) enableUserContentManager() {
440610 C.webkit_user_content_manager_add_script(w.native.ucm, wailsScript)
441611 C.webkit_user_script_unref(wailsScript)
442612 }
613+
614+ // No JS fallback bridge — native UCM is active
443615 }
@@ -3,21 +3,53 @@
3 package webkit3 package webkit
4 4
5 /*5 /*
6-#cgo pkg-config: webkit2gtk-4.0 gtk+-3.0 javascriptcoregtk-4.06+#cgo pkg-config: webkitgtk-6.0 gtk4 javascriptcoregtk-6.0
7 #include <stdlib.h>7 #include <stdlib.h>
8 #include <gtk/gtk.h>8 #include <gtk/gtk.h>
9-#include <webkit2/webkit2.h>9+#include <webkit/webkit.h>
10 #include <glib-object.h>10 #include <glib-object.h>
11 #include <gdk/gdk.h>11 #include <gdk/gdk.h>
12 #include <jsc/jsc.h>12 #include <jsc/jsc.h>
13 #include <glib.h>13 #include <glib.h>
14 #include <gio/gio.h>14 #include <gio/gio.h>
15 15
16-static GtkWidget* new_window() { return gtk_window_new(GTK_WINDOW_TOPLEVEL); }16+static GtkWidget* new_window() { return GTK_WIDGET(gtk_window_new()); }
17-static void connect_destroy_quit(GtkWidget* w) { g_signal_connect(w, "destroy", G_CALLBACK(gtk_main_quit), NULL); }17+// GTK4: gtk_main_quit removed; leave as no-op for now (Quit handled in Go)
18+static void connect_destroy_quit(GtkWidget* w) { (void)w; }
18 static WebKitWebView* as_webview(GtkWidget* w) { return WEBKIT_WEB_VIEW(w); }19 static WebKitWebView* as_webview(GtkWidget* w) { return WEBKIT_WEB_VIEW(w); }
19-static WebKitWebsiteDataManager* make_wdm(const gchar* data, const gchar* cache){20+// WebsiteDataManager creation will be handled via GTK4/WebKit6 APIs in Go code.
20- return webkit_website_data_manager_new("base-data-directory", data, "base-cache-directory", cache, NULL);21+
22+// Forward declare helpers used below
23+static void maybe_set_cookie_policy(WebKitCookieManager* cm, int policy);
24+
25+// Construct a WebKitWebView via g_object_new with a fresh UserContentManager.
26+static GtkWidget* new_webview_with_ucm_and_session(const char* data_dir, const char* cache_dir, const char* cookie_path, WebKitUserContentManager** out_ucm) {
27+ WebKitUserContentManager* u = webkit_user_content_manager_new();
28+ if (!u) return NULL;
29+ // WebKitGTK 6: create NetworkSession with data/cache directories
30+ WebKitNetworkSession* sess = webkit_network_session_new(
31+ data_dir ? data_dir : "",
32+ cache_dir ? cache_dir : "");
33+ if (!sess) { g_object_unref(u); return NULL; }
34+ // Configure cookie persistence and policy (no third-party by default)
35+ WebKitCookieManager* cm = webkit_network_session_get_cookie_manager(sess);
36+ if (cm) {
37+ if (cookie_path && cookie_path[0]) {
38+ webkit_cookie_manager_set_persistent_storage(cm, cookie_path, WEBKIT_COOKIE_PERSISTENT_STORAGE_SQLITE);
39+ }
40+ // 2 => no-third-party (default), 0 => always, 1 => never
41+ maybe_set_cookie_policy(cm, 2);
42+ }
43+ // Persist credentials (HTTP auth, etc.)
44+ webkit_network_session_set_persistent_credential_storage_enabled(sess, TRUE);
45+ GtkWidget* w = GTK_WIDGET(g_object_new(WEBKIT_TYPE_WEB_VIEW,
46+ "user-content-manager", u,
47+ "network-session", sess,
48+ NULL));
49+ // WebView holds refs to provided objects; drop our temporary refs
50+ g_object_unref(sess);
51+ if (out_ucm) { *out_ucm = u; }
52+ return w;
21 }53 }
22 54
23 static gboolean gtk_prefers_dark() {55 static gboolean gtk_prefers_dark() {
@@ -31,28 +63,12 @@ static gboolean gtk_prefers_dark() {
31 // Note: preferred color scheme is handled via a user script injection to support63 // Note: preferred color scheme is handled via a user script injection to support
32 // older WebKitGTK versions without the color-scheme API. See enableUserContentManager.64 // older WebKitGTK versions without the color-scheme API. See enableUserContentManager.
33 65
34-// Forward declaration of Go callback
35-extern void goOnKeyPress(unsigned long id, guint keyval, GdkModifierType state);
36-extern void goOnButtonPress(unsigned long id, guint button, GdkModifierType state);
37 extern void goOnUcmMessage(unsigned long id, const char* json);66 extern void goOnUcmMessage(unsigned long id, const char* json);
38 extern void goOnTitleChanged(unsigned long id, const char* title);67 extern void goOnTitleChanged(unsigned long id, const char* title);
39 extern void goOnURIChanged(unsigned long id, const char* uri);68 extern void goOnURIChanged(unsigned long id, const char* uri);
69+extern void goOnThemeChanged(unsigned long id, int prefer_dark);
40 extern void* goResolveURIScheme(char* uri, size_t* out_len, char** out_mime);70 extern void* goResolveURIScheme(char* uri, size_t* out_len, char** out_mime);
41 71
42-gboolean on_key_press(GtkWidget* widget, GdkEventKey* event, gpointer user_data) {
43- (void)widget;
44- unsigned long id = (unsigned long)user_data;
45- goOnKeyPress(id, event->keyval, event->state);
46- return FALSE; // Propagate
47-}
48-
49-gboolean on_button_press(GtkWidget* widget, GdkEventButton* event, gpointer user_data) {
50- (void)widget;
51- unsigned long id = (unsigned long)user_data;
52- goOnButtonPress(id, event->button, event->state);
53- return FALSE;
54-}
55-
56 void on_title_notify(GObject* obj, GParamSpec* pspec, gpointer user_data) {72 void on_title_notify(GObject* obj, GParamSpec* pspec, gpointer user_data) {
57 (void)pspec;73 (void)pspec;
58 WebKitWebView* view = WEBKIT_WEB_VIEW(obj);74 WebKitWebView* view = WEBKIT_WEB_VIEW(obj);
@@ -75,19 +91,17 @@ void on_uri_notify(GObject* obj, GParamSpec* pspec, gpointer user_data) {
75 if (uri) { goOnURIChanged((unsigned long)user_data, uri); }91 if (uri) { goOnURIChanged((unsigned long)user_data, uri); }
76 }92 }
77 93
78-static gchar* js_result_to_utf8(WebKitJavascriptResult* r) {94+// React to GTK theme preference changes at runtime
79- JSCValue* v = webkit_javascript_result_get_js_value(r);95+void on_theme_changed(GObject* obj, GParamSpec* pspec, gpointer user_data) {
80- // Returns newly-allocated string; free with g_free()96+ (void)pspec;
81- return jsc_value_to_string(v);97+ GtkSettings* settings = GTK_SETTINGS(obj);
98+ if (!settings) return;
99+ gboolean prefer = FALSE;
100+ g_object_get(settings, "gtk-application-prefer-dark-theme", &prefer, NULL);
101+ goOnThemeChanged((unsigned long)user_data, prefer ? 1 : 0);
82 }102 }
83 103
84-void on_ucm_message(WebKitUserContentManager* m, WebKitJavascriptResult* r, gpointer user_data) {104+// NOTE: UCM message callback signature changed in WebKitGTK 6; will be reimplemented later.
85- (void)m;
86- unsigned long id = (unsigned long)user_data;
87- gchar* s = js_result_to_utf8(r);
88- goOnUcmMessage(id, s);
89- g_free(s);
90-}
91 105
92 void on_uri_scheme(WebKitURISchemeRequest* request, gpointer user_data) {106 void on_uri_scheme(WebKitURISchemeRequest* request, gpointer user_data) {
93 (void)user_data;107 (void)user_data;
@@ -106,6 +120,107 @@ void on_uri_scheme(WebKitURISchemeRequest* request, gpointer user_data) {
106 g_error_free(err);120 g_error_free(err);
107 }121 }
108 }122 }
123+
124+// Script message handler wiring for WebKitGTK 6
125+// Forward declaration for callback used in g_signal_connect_data
126+// WebKitGTK 6 delivers JSCValue* for script-message-received::NAME
127+static void ucm_on_script_message_cb(WebKitUserContentManager* ucm, JSCValue* val, gpointer user_data);
128+// Uses WebKitScriptMessage and script-message-received::NAME signal.
129+static void ucm_on_script_message_cb(WebKitUserContentManager* ucm, JSCValue* val, gpointer user_data) {
130+ (void)ucm;
131+ if (!val) return;
132+ gchar* s = jsc_value_to_string(val);
133+ if (!s) return;
134+ goOnUcmMessage((unsigned long)user_data, s);
135+ g_free(s);
136+}
137+
138+static gboolean register_ucm_handler(WebKitUserContentManager* ucm, const char* name, unsigned long id) {
139+ if (!ucm || !name) return FALSE;
140+ // WebKitGTK 6 signature: (ucm, name, world_name)
141+ webkit_user_content_manager_register_script_message_handler(ucm, name, NULL);
142+ gchar* signal = g_strdup_printf("script-message-received::%s", name);
143+ if (!signal) return FALSE;
144+ g_signal_connect_data(G_OBJECT(ucm), signal, G_CALLBACK(ucm_on_script_message_cb), (gpointer)id, NULL, 0);
145+ g_free(signal);
146+ return TRUE;
147+}
148+
149+// ----- Conditional helpers for settings across WebKit versions -----
150+#ifndef WEBKIT_CHECK_VERSION
151+#define WEBKIT_CHECK_VERSION(major,minor,micro) (0)
152+#endif
153+
154+static void maybe_set_hw_policy(WebKitSettings* settings, int mode) {
155+#if WEBKIT_CHECK_VERSION(2,40,0)
156+ // mode: 0=ON_DEMAND, 1=ALWAYS, 2=NEVER
157+ if (!settings) return;
158+ if (mode == 1) {
159+ webkit_settings_set_hardware_acceleration_policy(settings, WEBKIT_HARDWARE_ACCELERATION_POLICY_ALWAYS);
160+ } else if (mode == 2) {
161+ webkit_settings_set_hardware_acceleration_policy(settings, WEBKIT_HARDWARE_ACCELERATION_POLICY_NEVER);
162+ } else {
163+ // Leave default policy (auto) without forcing a value to avoid referencing
164+ // WEBKIT_HARDWARE_ACCELERATION_POLICY_ON_DEMAND on older headers.
165+ }
166+#else
167+ (void)settings; (void)mode;
168+#endif
169+}
170+
171+static void maybe_set_draw_indicators(WebKitSettings* settings, int enable) {
172+#if WEBKIT_CHECK_VERSION(2,36,0)
173+ if (!settings) return;
174+ webkit_settings_set_draw_compositing_indicators(settings, enable ? 1 : 0);
175+#else
176+ (void)settings; (void)enable;
177+#endif
178+}
179+
180+static void maybe_set_webgl(WebKitSettings* settings, int enable) {
181+#if WEBKIT_CHECK_VERSION(2,20,0)
182+ if (!settings) return;
183+ webkit_settings_set_enable_webgl(settings, enable ? 1 : 0);
184+#else
185+ (void)settings; (void)enable;
186+#endif
187+}
188+
189+static void maybe_set_canvas_accel(WebKitSettings* settings, int enable) {
190+#if WEBKIT_CHECK_VERSION(2,20,0)
191+ if (!settings) return;
192+ webkit_settings_set_enable_2d_canvas_acceleration(settings, enable ? 1 : 0);
193+#else
194+ (void)settings; (void)enable;
195+#endif
196+}
197+
198+static void maybe_set_media_user_gesture(WebKitSettings* settings, int require) {
199+#if WEBKIT_CHECK_VERSION(2,20,0)
200+ if (!settings) return;
201+ webkit_settings_set_media_playback_requires_user_gesture(settings, require ? 1 : 0);
202+#else
203+ (void)settings; (void)require;
204+#endif
205+}
206+
207+// Set cookie accept policy with compile-time compatibility across WebKit versions
208+static void maybe_set_cookie_policy(WebKitCookieManager* cm, int policy) {
209+ if (!cm) return;
210+#if defined(WEBKIT_COOKIE_ACCEPT_POLICY_ALWAYS)
211+ WebKitCookieAcceptPolicy p = WEBKIT_COOKIE_ACCEPT_POLICY_ALWAYS;
212+ if (policy == 1) p = WEBKIT_COOKIE_ACCEPT_POLICY_NEVER; // never
213+ else if (policy == 2) p = WEBKIT_COOKIE_ACCEPT_POLICY_NO_THIRD_PARTY; // no third-party
214+ webkit_cookie_manager_set_accept_policy(cm, p);
215+#elif defined(WEBKIT_COOKIE_POLICY_ACCEPT_ALWAYS)
216+ WebKitCookieAcceptPolicy p = WEBKIT_COOKIE_POLICY_ACCEPT_ALWAYS;
217+ if (policy == 1) p = WEBKIT_COOKIE_POLICY_ACCEPT_NEVER;
218+ else if (policy == 2) p = WEBKIT_COOKIE_POLICY_ACCEPT_NO_THIRD_PARTY;
219+ webkit_cookie_manager_set_accept_policy(cm, p);
220+#else
221+ (void)policy; // Not supported in this header version
222+#endif
223+}
109 */224 */
110 import "C"225 import "C"
111 226
@@ -172,7 +287,7 @@ type WebView struct {
172 // NewWebView constructs a new WebView instance with native WebKit2GTK widgets.287 // NewWebView constructs a new WebView instance with native WebKit2GTK widgets.
173 func NewWebView(cfg *Config) (*WebView, error) {288 func NewWebView(cfg *Config) (*WebView, error) {
174 log.Printf("[webkit] Initializing GTK and creating WebView (CGO)")289 log.Printf("[webkit] Initializing GTK and creating WebView (CGO)")
175- if C.gtk_init_check(nil, nil) == 0 {290+ if C.gtk_init_check() == 0 {
176 return nil, errors.New("failed to initialize GTK")291 return nil, errors.New("failed to initialize GTK")
177 }292 }
178 293
@@ -190,12 +305,14 @@ func NewWebView(cfg *Config) (*WebView, error) {
190 305
191 cData := C.CString(dataDir)306 cData := C.CString(dataDir)
192 cCache := C.CString(cacheDir)307 cCache := C.CString(cacheDir)
308+ cookieFile := filepath.Join(dataDir, "cookies.db")
309+ cCookie := C.CString(cookieFile)
193 defer C.free(unsafe.Pointer(cData))310 defer C.free(unsafe.Pointer(cData))
194 defer C.free(unsafe.Pointer(cCache))311 defer C.free(unsafe.Pointer(cCache))
195- wdm := C.make_wdm((*C.gchar)(cData), (*C.gchar)(cCache))312+ defer C.free(unsafe.Pointer(cCookie))
196- if wdm == nil { return nil, errors.New("failed to create WebsiteDataManager") }313+ // Use default WebContext to register the custom URI scheme handler; the
197- ctx := C.webkit_web_context_new_with_website_data_manager(wdm)314+ // WebView itself is created with a WebKitNetworkSession for persistence.
198- if ctx == nil { return nil, errors.New("failed to create WebContext") }315+ ctx := C.webkit_web_context_get_default()
199 316
200 // Register custom URI scheme handler for "dumb://"317 // Register custom URI scheme handler for "dumb://"
201 {318 {
@@ -203,19 +320,11 @@ func NewWebView(cfg *Config) (*WebView, error) {
203 C.webkit_web_context_register_uri_scheme(ctx, sch, (C.WebKitURISchemeRequestCallback)(C.on_uri_scheme), nil, nil)320 C.webkit_web_context_register_uri_scheme(ctx, sch, (C.WebKitURISchemeRequestCallback)(C.on_uri_scheme), nil, nil)
204 C.free(unsafe.Pointer(sch))321 C.free(unsafe.Pointer(sch))
205 }322 }
323+ // Cookie manager persistent storage handled via NetworkSession in new_webview_with_ucm_and_session
206 324
207- // Cookie manager persistent storage325+ // Create WebView through g_object_new with a fresh UCM (GTK4/WebKit 6 style)
208- cm := C.webkit_web_context_get_cookie_manager(ctx)326+ var createdUcm *C.WebKitUserContentManager
209- if cm != nil {327+ viewWidget := C.new_webview_with_ucm_and_session(cData, cCache, cCookie, &createdUcm)
210- cookiePath := filepath.Join(dataDir, "cookies.sqlite")
211- cCookie := C.CString(cookiePath)
212- defer C.free(unsafe.Pointer(cCookie))
213- C.webkit_cookie_manager_set_persistent_storage(cm, (*C.gchar)(cCookie), C.WEBKIT_COOKIE_PERSISTENT_STORAGE_SQLITE)
214- C.webkit_cookie_manager_set_accept_policy(cm, C.WEBKIT_COOKIE_POLICY_ACCEPT_ALWAYS)
215- }
216-
217- // Create WebView with context
218- viewWidget := C.webkit_web_view_new_with_context(ctx) // GtkWidget*
219 if viewWidget == nil {328 if viewWidget == nil {
220 return nil, errors.New("failed to create WebKitWebView")329 return nil, errors.New("failed to create WebKitWebView")
221 }330 }
@@ -227,32 +336,92 @@ func NewWebView(cfg *Config) (*WebView, error) {
227 }336 }
228 337
229 // Pack view widget into the window338 // Pack view widget into the window
230- C.gtk_container_add((*C.GtkContainer)(unsafe.Pointer(win)), viewWidget)339+ // GTK4: containers removed; use gtk_window_set_child
340+ C.gtk_window_set_child((*C.GtkWindow)(unsafe.Pointer(win)), viewWidget)
231 C.gtk_window_set_default_size((*C.GtkWindow)(unsafe.Pointer(win)), 1024, 768)341 C.gtk_window_set_default_size((*C.GtkWindow)(unsafe.Pointer(win)), 1024, 768)
232 C.connect_destroy_quit(win)342 C.connect_destroy_quit(win)
233 343
234 v := &WebView{344 v := &WebView{
235 config: cfg,345 config: cfg,
236 zoom: 1.0,346 zoom: 1.0,
237- native: &nativeView{win: win, view: viewWidget, wv: C.as_webview(viewWidget), ucm: nil},347+ native: &nativeView{win: win, view: viewWidget, wv: C.as_webview(viewWidget), ucm: createdUcm},
238 window: &Window{win: win},348 window: &Window{win: win},
239 }349 }
240- // Allow receiving button press events350+ // Assign an ID for accelerator dispatch
241- C.gtk_widget_add_events(win, C.GDK_BUTTON_PRESS_MASK|C.GDK_BUTTON_RELEASE_MASK)
242- C.gtk_widget_add_events(viewWidget, C.GDK_BUTTON_PRESS_MASK|C.GDK_BUTTON_RELEASE_MASK)
243-
244- // Assign an ID for accelerator dispatch and connect input events
245 v.id = nextViewID()351 v.id = nextViewID()
246 registerView(v.id, v)352 registerView(v.id, v)
247- C.g_signal_connect_data(C.gpointer(unsafe.Pointer(win)), C.CString("key-press-event"), C.GCallback(C.on_key_press), C.gpointer(v.id), nil, 0)353+ // Attach GTK4 input controllers (keyboard, mouse)
248- C.g_signal_connect_data(C.gpointer(unsafe.Pointer(viewWidget)), C.CString("button-press-event"), C.GCallback(C.on_button_press), C.gpointer(v.id), nil, 0)354+ AttachKeyboardControllers(v)
249- C.g_signal_connect_data(C.gpointer(unsafe.Pointer(win)), C.CString("button-press-event"), C.GCallback(C.on_button_press), C.gpointer(v.id), nil, 0)355+
356+ // Watch for GTK theme changes and propagate to page at runtime
357+ if settings := C.gtk_settings_get_default(); settings != nil {
358+ cprop := C.CString("notify::gtk-application-prefer-dark-theme")
359+ C.g_signal_connect_data(C.gpointer(unsafe.Pointer(settings)), cprop, C.GCallback(C.on_theme_changed), C.gpointer(v.id), nil, 0)
360+ C.free(unsafe.Pointer(cprop))
361+ }
362+
363+ // Native zoom shortcuts (independent of app services)
364+ // Ctrl+='=' and Ctrl+'+' → Zoom In; Ctrl+'-' → Zoom Out; Ctrl+'0' → Reset
365+ _ = v.RegisterKeyboardShortcut("cmdorctrl+=", func() {
366+ nz := v.zoom
367+ if nz <= 0 { nz = 1.0 }
368+ nz *= 1.1
369+ if nz < 0.25 { nz = 0.25 }
370+ if nz > 5.0 { nz = 5.0 }
371+ _ = v.SetZoom(nz)
372+ })
373+ _ = v.RegisterKeyboardShortcut("cmdorctrl+plus", func() {
374+ nz := v.zoom
375+ if nz <= 0 { nz = 1.0 }
376+ nz *= 1.1
377+ if nz < 0.25 { nz = 0.25 }
378+ if nz > 5.0 { nz = 5.0 }
379+ _ = v.SetZoom(nz)
380+ })
381+ _ = v.RegisterKeyboardShortcut("cmdorctrl-", func() {
382+ nz := v.zoom
383+ if nz <= 0 { nz = 1.0 }
384+ nz /= 1.1
385+ if nz < 0.25 { nz = 0.25 }
386+ if nz > 5.0 { nz = 5.0 }
387+ _ = v.SetZoom(nz)
388+ })
389+ _ = v.RegisterKeyboardShortcut("cmdorctrl+0", func() { _ = v.SetZoom(1.0) })
250 // Update window title when page title changes390 // Update window title when page title changes
251- C.g_signal_connect_data(C.gpointer(unsafe.Pointer(viewWidget)), C.CString("notify::title"), C.GCallback(C.on_title_notify), C.gpointer(unsafe.Pointer(win)), nil, 0)391+ cNotifyTitle1 := C.CString("notify::title")
392+ C.g_signal_connect_data(C.gpointer(unsafe.Pointer(viewWidget)), cNotifyTitle1, C.GCallback(C.on_title_notify), C.gpointer(unsafe.Pointer(win)), nil, 0)
393+ C.free(unsafe.Pointer(cNotifyTitle1))
252 // Also dispatch title change to Go with view id394 // Also dispatch title change to Go with view id
253- C.g_signal_connect_data(C.gpointer(unsafe.Pointer(viewWidget)), C.CString("notify::title"), C.GCallback(C.on_title_notify_id), C.gpointer(v.id), nil, 0)395+ cNotifyTitle2 := C.CString("notify::title")
396+ C.g_signal_connect_data(C.gpointer(unsafe.Pointer(viewWidget)), cNotifyTitle2, C.GCallback(C.on_title_notify_id), C.gpointer(v.id), nil, 0)
397+ C.free(unsafe.Pointer(cNotifyTitle2))
254 // Notify URI changes to Go to keep current URL in sync398 // Notify URI changes to Go to keep current URL in sync
255- C.g_signal_connect_data(C.gpointer(unsafe.Pointer(viewWidget)), C.CString("notify::uri"), C.GCallback(C.on_uri_notify), C.gpointer(v.id), nil, 0)399+ cNotifyURI := C.CString("notify::uri")
400+ C.g_signal_connect_data(C.gpointer(unsafe.Pointer(viewWidget)), cNotifyURI, C.GCallback(C.on_uri_notify), C.gpointer(v.id), nil, 0)
401+ C.free(unsafe.Pointer(cNotifyURI))
402+ // Apply hardware acceleration and related settings based on cfg.Rendering
403+ if settings := C.webkit_web_view_get_settings(v.native.wv); settings != nil {
404+ // Hardware acceleration policy (guarded by version)
405+ switch cfg.Rendering.Mode {
406+ case "gpu":
407+ C.maybe_set_hw_policy(settings, 1)
408+ C.maybe_set_webgl(settings, 1)
409+ C.maybe_set_canvas_accel(settings, 1)
410+ case "cpu":
411+ C.maybe_set_hw_policy(settings, 2)
412+ C.maybe_set_webgl(settings, 0)
413+ C.maybe_set_canvas_accel(settings, 0)
414+ default: // auto
415+ C.maybe_set_hw_policy(settings, 0)
416+ C.maybe_set_webgl(settings, 1)
417+ C.maybe_set_canvas_accel(settings, 1)
418+ }
419+ // Optional compositing indicators for debugging (guarded by version)
420+ if cfg.Rendering.DebugGPU { C.maybe_set_draw_indicators(settings, 1) }
421+ // Reduce media pipeline churn by requiring a user gesture for playback
422+ C.maybe_set_media_user_gesture(settings, 1)
423+ }
424+
256 if cfg.ZoomDefault > 0 {425 if cfg.ZoomDefault > 0 {
257 v.zoom = cfg.ZoomDefault426 v.zoom = cfg.ZoomDefault
258 C.webkit_web_view_set_zoom_level(v.native.wv, C.gdouble(v.zoom))427 C.webkit_web_view_set_zoom_level(v.native.wv, C.gdouble(v.zoom))
@@ -295,13 +464,9 @@ func NewWebView(cfg *Config) (*WebView, error) {
295 runtime.SetFinalizer(v, func(v *WebView) { _ = v.Destroy() })464 runtime.SetFinalizer(v, func(v *WebView) { _ = v.Destroy() })
296 log.Printf("[webkit] WebView created (CGO)")465 log.Printf("[webkit] WebView created (CGO)")
297 466
298- // Obtain the view's UserContentManager and inject script467+ // Initialize UCM scripts and handlers
299- if v.native != nil && v.native.wv != nil {468+ if v.native != nil && v.native.ucm != nil {
300- u := C.webkit_web_view_get_user_content_manager(v.native.wv)469+ v.enableUserContentManager()
301- if u != nil {
302- v.native.ucm = u
303- v.enableUserContentManager()
304- }
305 }470 }
306 return v, nil471 return v, nil
307 }472 }
@@ -322,8 +487,9 @@ func (w *WebView) Show() error {
322 if w == nil || w.destroyed || w.native == nil || w.native.win == nil {487 if w == nil || w.destroyed || w.native == nil || w.native.win == nil {
323 return ErrNotImplemented488 return ErrNotImplemented
324 }489 }
325- C.gtk_widget_show(w.native.win)490+ C.gtk_widget_set_visible(w.native.view, C.gboolean(1))
326- C.gtk_widget_show(w.native.view)491+ C.gtk_widget_set_visible(w.native.win, C.gboolean(1))
492+ C.gtk_window_present((*C.GtkWindow)(unsafe.Pointer(w.native.win)))
327 log.Printf("[webkit] Show window")493 log.Printf("[webkit] Show window")
328 return nil494 return nil
329 }495 }
@@ -332,7 +498,8 @@ func (w *WebView) Hide() error {
332 if w == nil || w.destroyed || w.native == nil || w.native.win == nil {498 if w == nil || w.destroyed || w.native == nil || w.native.win == nil {
333 return ErrNotImplemented499 return ErrNotImplemented
334 }500 }
335- C.gtk_widget_hide(w.native.win)501+ C.gtk_widget_set_visible(w.native.view, C.gboolean(0))
502+ C.gtk_widget_set_visible(w.native.win, C.gboolean(0))
336 return nil503 return nil
337 }504 }
338 505
@@ -340,7 +507,8 @@ func (w *WebView) Destroy() error {
340 if w == nil || w.native == nil || w.native.win == nil {507 if w == nil || w.native == nil || w.native.win == nil {
341 return ErrNotImplemented508 return ErrNotImplemented
342 }509 }
343- C.gtk_widget_destroy(w.native.win)510+ // GTK4: destroy windows via gtk_window_destroy
511+ C.gtk_window_destroy((*C.GtkWindow)(unsafe.Pointer(w.native.win)))
344 w.destroyed = true512 w.destroyed = true
345 unregisterView(w.id)513 unregisterView(w.id)
346 log.Printf("[webkit] Destroy window")514 log.Printf("[webkit] Destroy window")
@@ -396,9 +564,11 @@ func (w *WebView) enableUserContentManager() {
396 if w == nil || w.native == nil || w.native.ucm == nil { return }564 if w == nil || w.native == nil || w.native.ucm == nil { return }
397 // Register handler "dumber"565 // Register handler "dumber"
398 cname := C.CString("dumber")566 cname := C.CString("dumber")
399- defer C.free(unsafe.Pointer(cname))567+ ok := C.register_ucm_handler(w.native.ucm, cname, C.ulong(w.id))
400- C.webkit_user_content_manager_register_script_message_handler(w.native.ucm, (*C.gchar)(cname))568+ if ok == 0 {
401- C.g_signal_connect_data(C.gpointer(unsafe.Pointer(w.native.ucm)), C.CString("script-message-received::dumber"), C.GCallback(C.on_ucm_message), C.gpointer(w.id), nil, 0)569+ log.Printf("[webkit] Failed to register UCM handler; fallback bridge will handle messages")
570+ }
571+ C.free(unsafe.Pointer(cname))
402 572
403 // Inject color-scheme preference script at document-start to inform sites of system theme573 // Inject color-scheme preference script at document-start to inform sites of system theme
404 preferDark := C.gtk_prefers_dark() != 0574 preferDark := C.gtk_prefers_dark() != 0
@@ -409,9 +579,9 @@ func (w *WebView) enableUserContentManager() {
409 }579 }
410 var schemeJS string580 var schemeJS string
411 if preferDark {581 if preferDark {
412- schemeJS = "(() => { try { const d=true; const cs=d?'dark':'light'; console.log('[dumber] color-scheme set:', cs); try{ window.webkit?.messageHandlers?.dumber?.postMessage(JSON.stringify({type:'theme', value: cs})) }catch(_){} const meta=document.createElement('meta'); meta.name='color-scheme'; meta.content='dark light'; document.documentElement.appendChild(meta); const s=document.createElement('style'); s.textContent=':root{color-scheme:dark;}'; document.documentElement.appendChild(s); const qD='(prefers-color-scheme: dark)'; const qL='(prefers-color-scheme: light)'; const orig=window.matchMedia; window.matchMedia=function(q){ if(typeof q==='string'&&(q.includes(qD)||q.includes(qL))){ const m={matches:q.includes('dark')?d:!d,media:q,onchange:null,addListener(){},removeListener(){},addEventListener(){},removeEventListener(){},dispatchEvent(){return false;}}; return m;} return orig.call(window,q); }; } catch(e){ console.warn('[dumber] color-scheme injection failed', e) } })();"582+ schemeJS = "(() => { try { const d=true; const cs=d?'dark':'light'; console.log('[dumber] color-scheme set:' + cs); try{ window.webkit?.messageHandlers?.dumber?.postMessage(JSON.stringify({type:'theme', value: cs})) }catch(_){} const meta=document.createElement('meta'); meta.name='color-scheme'; meta.content='dark light'; document.documentElement.appendChild(meta); const s=document.createElement('style'); s.textContent=':root{color-scheme:dark;}'; document.documentElement.appendChild(s); const qD='(prefers-color-scheme: dark)'; const qL='(prefers-color-scheme: light)'; const orig=window.matchMedia; window.matchMedia=function(q){ if(typeof q==='string'&&(q.includes(qD)||q.includes(qL))){ const m={matches:q.includes('dark')?d:!d,media:q,onchange:null,addListener(){},removeListener(){},addEventListener(){},removeEventListener(){},dispatchEvent(){return false;}}; return m;} return orig.call(window,q); }; } catch(e){ console.warn('[dumber] color-scheme injection failed', e) } })();"
413 } else {583 } else {
414- schemeJS = "(() => { try { const d=false; const cs=d?'dark':'light'; console.log('[dumber] color-scheme set:', cs); try{ window.webkit?.messageHandlers?.dumber?.postMessage(JSON.stringify({type:'theme', value: cs})) }catch(_){} const meta=document.createElement('meta'); meta.name='color-scheme'; meta.content='light dark'; document.documentElement.appendChild(meta); const s=document.createElement('style'); s.textContent=':root{color-scheme:light;}'; document.documentElement.appendChild(s); const qD='(prefers-color-scheme: dark)'; const qL='(prefers-color-scheme: light)'; const orig=window.matchMedia; window.matchMedia=function(q){ if(typeof q==='string'&&(q.includes(qD)||q.includes(qL))){ const m={matches:q.includes('dark')?d:!d,media:q,onchange:null,addListener(){},removeListener(){},addEventListener(){},removeEventListener(){},dispatchEvent(){return false;}}; return m;} return orig.call(window,q); }; } catch(e){ console.warn('[dumber] color-scheme injection failed', e) } })();"584+ schemeJS = "(() => { try { const d=false; const cs=d?'dark':'light'; console.log('[dumber] color-scheme set:' + cs); try{ window.webkit?.messageHandlers?.dumber?.postMessage(JSON.stringify({type:'theme', value: cs})) }catch(_){} const meta=document.createElement('meta'); meta.name='color-scheme'; meta.content='light dark'; document.documentElement.appendChild(meta); const s=document.createElement('style'); s.textContent=':root{color-scheme:light;}'; document.documentElement.appendChild(s); const qD='(prefers-color-scheme: dark)'; const qL='(prefers-color-scheme: light)'; const orig=window.matchMedia; window.matchMedia=function(q){ if(typeof q==='string'&&(q.includes(qD)||q.includes(qL))){ const m={matches:q.includes('dark')?d:!d,media:q,onchange:null,addListener(){},removeListener(){},addEventListener(){},removeEventListener(){},dispatchEvent(){return false;}}; return m;} return orig.call(window,q); }; } catch(e){ console.warn('[dumber] color-scheme injection failed', e) } })();"
415 }585 }
416 cScheme := C.CString(schemeJS)586 cScheme := C.CString(schemeJS)
417 defer C.free(unsafe.Pointer(cScheme))587 defer C.free(unsafe.Pointer(cScheme))
@@ -440,4 +610,6 @@ func (w *WebView) enableUserContentManager() {
440 C.webkit_user_content_manager_add_script(w.native.ucm, wailsScript)610 C.webkit_user_content_manager_add_script(w.native.ucm, wailsScript)
441 C.webkit_user_script_unref(wailsScript)611 C.webkit_user_script_unref(wailsScript)
442 }612 }
613+
614+ // No JS fallback bridge — native UCM is active
443 }615 }
M pkg/webkit/window_cgo.go +4 -3
@@ -3,7 +3,7 @@
33 package webkit
44
55 /*
6-#cgo pkg-config: webkit2gtk-4.0 gtk+-3.0
6+#cgo pkg-config: webkitgtk-6.0 gtk4
77 #include <stdlib.h>
88 #include <gtk/gtk.h>
99 */
@@ -16,10 +16,11 @@ type Window struct {
1616 }
1717
1818 func NewWindow(title string) (*Window, error) {
19- if C.gtk_init_check(nil, nil) == 0 {
19+ if C.gtk_init_check() == 0 {
2020 return nil, ErrNotImplemented
2121 }
22- w := C.gtk_window_new(C.GTK_WINDOW_TOPLEVEL)
22+ // GTK4: gtk_window_new() returns GtkWindow*, no toplevel enum
23+ w := (*C.GtkWidget)(unsafe.Pointer(C.gtk_window_new()))
2324 if w == nil {
2425 return nil, ErrNotImplemented
2526 }
@@ -3,7 +3,7 @@
3 package webkit3 package webkit
4 4
5 /*5 /*
6-#cgo pkg-config: webkit2gtk-4.0 gtk+-3.06+#cgo pkg-config: webkitgtk-6.0 gtk4
7 #include <stdlib.h>7 #include <stdlib.h>
8 #include <gtk/gtk.h>8 #include <gtk/gtk.h>
9 */9 */
@@ -16,10 +16,11 @@ type Window struct {
16 }16 }
17 17
18 func NewWindow(title string) (*Window, error) {18 func NewWindow(title string) (*Window, error) {
19- if C.gtk_init_check(nil, nil) == 0 {19+ if C.gtk_init_check() == 0 {
20 return nil, ErrNotImplemented20 return nil, ErrNotImplemented
21 }21 }
22- w := C.gtk_window_new(C.GTK_WINDOW_TOPLEVEL)22+ // GTK4: gtk_window_new() returns GtkWindow*, no toplevel enum
23+ w := (*C.GtkWidget)(unsafe.Pointer(C.gtk_window_new()))
23 if w == nil {24 if w == nil {
24 return nil, ErrNotImplemented25 return nil, ErrNotImplemented
25 }26 }
M pkg/webkit/zoom_cgo.go +2 -3
@@ -3,8 +3,8 @@
33 package webkit
44
55 /*
6-#cgo pkg-config: webkit2gtk-4.0 gtk+-3.0
7-#include <webkit2/webkit2.h>
6+#cgo pkg-config: webkitgtk-6.0 gtk4
7+#include <webkit/webkit.h>
88 */
99 import "C"
1010
@@ -31,4 +31,3 @@ func (w *WebView) GetZoom() (float64, error) {
3131 w.zoom = z
3232 return z, nil
3333 }
34-
@@ -3,8 +3,8 @@
3 package webkit3 package webkit
4 4
5 /*5 /*
6-#cgo pkg-config: webkit2gtk-4.0 gtk+-3.06+#cgo pkg-config: webkitgtk-6.0 gtk4
7-#include <webkit2/webkit2.h>7+#include <webkit/webkit.h>
8 */8 */
9 import "C"9 import "C"
10 10
@@ -31,4 +31,3 @@ func (w *WebView) GetZoom() (float64, error) {
31 w.zoom = z31 w.zoom = z
32 return z, nil32 return z, nil
33 }33 }
34-
M services/config_service.go +27 -5
@@ -6,7 +6,8 @@ import (
66 "os"
77 "path/filepath"
88
9- "github.com/bnema/dumber/internal/config"
9+ "github.com/bnema/dumber/internal/config"
10+ "strings"
1011 )
1112
1213 // ConfigService handles configuration operations for the application.
@@ -19,10 +20,11 @@ const defaultDirPerm = 0o750
1920
2021 // ConfigInfo represents configuration information for the frontend.
2122 type ConfigInfo struct {
22- ConfigPath string `json:"config_path"`
23- DatabasePath string `json:"database_path"`
24- SearchShortcuts map[string]config.SearchShortcut `json:"search_shortcuts"`
25- DmenuSettings *config.DmenuConfig `json:"dmenu_settings"`
23+ ConfigPath string `json:"config_path"`
24+ DatabasePath string `json:"database_path"`
25+ SearchShortcuts map[string]config.SearchShortcut `json:"search_shortcuts"`
26+ DmenuSettings *config.DmenuConfig `json:"dmenu_settings"`
27+ RenderingMode string `json:"rendering_mode"`
2628 }
2729
2830 // NewConfigService creates a new ConfigService instance.
@@ -41,6 +43,7 @@ func (s *ConfigService) GetConfigInfo(ctx context.Context) (*ConfigInfo, error)
4143 DatabasePath: s.config.Database.Path,
4244 SearchShortcuts: s.config.SearchShortcuts,
4345 DmenuSettings: &s.config.Dmenu,
46+ RenderingMode: string(s.config.RenderingMode),
4447 }, nil
4548 }
4649
@@ -250,6 +253,25 @@ func (s *ConfigService) ImportConfig(ctx context.Context, newConfig *config.Conf
250253 return s.saveConfig()
251254 }
252255
256+// GetRenderingMode returns the current rendering mode (auto|gpu|cpu).
257+func (s *ConfigService) GetRenderingMode(ctx context.Context) (string, error) {
258+ _ = ctx
259+ return string(s.config.RenderingMode), nil
260+}
261+
262+// SetRenderingMode updates the rendering mode (auto|gpu|cpu).
263+func (s *ConfigService) SetRenderingMode(ctx context.Context, mode string) error {
264+ _ = ctx
265+ m := strings.ToLower(strings.TrimSpace(mode))
266+ switch m {
267+ case "auto", "gpu", "cpu":
268+ s.config.RenderingMode = config.RenderingMode(m)
269+ return s.saveConfig()
270+ default:
271+ return fmt.Errorf("invalid rendering mode: %s", mode)
272+ }
273+}
274+
253275 // saveConfig saves the current configuration to file.
254276 func (s *ConfigService) saveConfig() error {
255277 // For now, return success - implementing config saving would require
@@ -6,7 +6,8 @@ import (
6 "os"6 "os"
7 "path/filepath"7 "path/filepath"
8 8
9- "github.com/bnema/dumber/internal/config"9+ "github.com/bnema/dumber/internal/config"
10+ "strings"
10 )11 )
11 12
12 // ConfigService handles configuration operations for the application.13 // ConfigService handles configuration operations for the application.
@@ -19,10 +20,11 @@ const defaultDirPerm = 0o750
19 20
20 // ConfigInfo represents configuration information for the frontend.21 // ConfigInfo represents configuration information for the frontend.
21 type ConfigInfo struct {22 type ConfigInfo struct {
22- ConfigPath string `json:"config_path"`23+ ConfigPath string `json:"config_path"`
23- DatabasePath string `json:"database_path"`24+ DatabasePath string `json:"database_path"`
24- SearchShortcuts map[string]config.SearchShortcut `json:"search_shortcuts"`25+ SearchShortcuts map[string]config.SearchShortcut `json:"search_shortcuts"`
25- DmenuSettings *config.DmenuConfig `json:"dmenu_settings"`26+ DmenuSettings *config.DmenuConfig `json:"dmenu_settings"`
27+ RenderingMode string `json:"rendering_mode"`
26 }28 }
27 29
28 // NewConfigService creates a new ConfigService instance.30 // NewConfigService creates a new ConfigService instance.
@@ -41,6 +43,7 @@ func (s *ConfigService) GetConfigInfo(ctx context.Context) (*ConfigInfo, error)
41 DatabasePath: s.config.Database.Path,43 DatabasePath: s.config.Database.Path,
42 SearchShortcuts: s.config.SearchShortcuts,44 SearchShortcuts: s.config.SearchShortcuts,
43 DmenuSettings: &s.config.Dmenu,45 DmenuSettings: &s.config.Dmenu,
46+ RenderingMode: string(s.config.RenderingMode),
44 }, nil47 }, nil
45 }48 }
46 49
@@ -250,6 +253,25 @@ func (s *ConfigService) ImportConfig(ctx context.Context, newConfig *config.Conf
250 return s.saveConfig()253 return s.saveConfig()
251 }254 }
252 255
256+// GetRenderingMode returns the current rendering mode (auto|gpu|cpu).
257+func (s *ConfigService) GetRenderingMode(ctx context.Context) (string, error) {
258+ _ = ctx
259+ return string(s.config.RenderingMode), nil
260+}
261+
262+// SetRenderingMode updates the rendering mode (auto|gpu|cpu).
263+func (s *ConfigService) SetRenderingMode(ctx context.Context, mode string) error {
264+ _ = ctx
265+ m := strings.ToLower(strings.TrimSpace(mode))
266+ switch m {
267+ case "auto", "gpu", "cpu":
268+ s.config.RenderingMode = config.RenderingMode(m)
269+ return s.saveConfig()
270+ default:
271+ return fmt.Errorf("invalid rendering mode: %s", mode)
272+ }
273+}
274+
253 // saveConfig saves the current configuration to file.275 // saveConfig saves the current configuration to file.
254 func (s *ConfigService) saveConfig() error {276 func (s *ConfigService) saveConfig() error {
255 // For now, return success - implementing config saving would require277 // For now, return success - implementing config saving would require
A specs/004-migrate-to-webkitgtk/contracts/rendering-config.yaml +153 -0
@@ -0,0 +1,153 @@
1+# Rendering Configuration Contract
2+# Defines the interface for GPU/CPU rendering configuration
3+
4+version: "1.0.0"
5+component: "RenderingConfig"
6+
7+# Configuration Interface
8+config:
9+ rendering_mode:
10+ type: "enum"
11+ values:
12+ - name: "auto"
13+ description: "Automatically detect GPU availability and use if present"
14+ default: true
15+ - name: "gpu"
16+ description: "Force GPU acceleration (fail if unavailable)"
17+ default: false
18+ - name: "cpu"
19+ description: "Force software rendering (disable GPU)"
20+ default: false
21+
22+ settings:
23+ enable_2d_canvas:
24+ type: "boolean"
25+ default: true
26+ description: "Enable hardware acceleration for 2D canvas"
27+
28+ enable_webgl:
29+ type: "boolean"
30+ default: true
31+ description: "Enable WebGL for 3D content"
32+
33+ draw_compositing_indicators:
34+ type: "boolean"
35+ default: false
36+ description: "Show GPU compositing borders (debug)"
37+
38+# CLI Interface
39+cli:
40+ flags:
41+ - name: "--rendering-mode"
42+ type: "string"
43+ values: ["auto", "gpu", "cpu"]
44+ default: "auto"
45+ description: "Set rendering mode"
46+
47+ - name: "--disable-gpu"
48+ type: "boolean"
49+ default: false
50+ description: "Shorthand for --rendering-mode=cpu"
51+
52+ - name: "--force-gpu"
53+ type: "boolean"
54+ default: false
55+ description: "Shorthand for --rendering-mode=gpu"
56+
57+ - name: "--debug-gpu"
58+ type: "boolean"
59+ default: false
60+ description: "Enable GPU compositing indicators"
61+
62+# Environment Variables
63+environment:
64+ - name: "DUMBER_RENDERING_MODE"
65+ values: ["auto", "gpu", "cpu"]
66+ description: "Override rendering mode"
67+
68+ - name: "WEBKIT_DISABLE_COMPOSITING_MODE"
69+ values: ["1", "0"]
70+ description: "WebKit native disable flag"
71+
72+# Status API
73+status:
74+ endpoint: "/api/rendering/status"
75+ response:
76+ mode:
77+ type: "string"
78+ enum: ["auto", "gpu", "cpu"]
79+
80+ gpu_active:
81+ type: "boolean"
82+ description: "Whether GPU acceleration is currently active"
83+
84+ gpu_available:
85+ type: "boolean"
86+ description: "Whether GPU hardware is available"
87+
88+ vulkan_enabled:
89+ type: "boolean"
90+ description: "Whether Vulkan renderer is active"
91+
92+ errors:
93+ type: "array"
94+ items:
95+ type: "object"
96+ properties:
97+ code: "string"
98+ message: "string"
99+ fallback_used: "boolean"
100+
101+ performance:
102+ type: "object"
103+ properties:
104+ fps: "number"
105+ cpu_usage_percent: "number"
106+ gpu_usage_percent: "number"
107+ memory_mb: "integer"
108+
109+# Validation Rules
110+validation:
111+ startup:
112+ - check: "GPU availability if mode=gpu"
113+ error: "GPU acceleration requested but not available"
114+ action: "fail_startup"
115+
116+ - check: "Vulkan support if GPU active"
117+ warning: "Vulkan not available, using OpenGL"
118+ action: "log_warning"
119+
120+ - check: "Driver compatibility"
121+ error: "GPU driver incompatible"
122+ action: "fallback_to_cpu"
123+
124+ runtime:
125+ - check: "GPU memory < 500MB"
126+ warning: "High GPU memory usage"
127+ action: "log_metric"
128+
129+ - check: "FPS > 30"
130+ warning: "Low frame rate detected"
131+ action: "log_metric"
132+
133+# Error Codes
134+errors:
135+ GPU_NOT_AVAILABLE:
136+ code: "E001"
137+ message: "GPU hardware not detected"
138+ severity: "error"
139+
140+ VULKAN_INIT_FAILED:
141+ code: "E002"
142+ message: "Failed to initialize Vulkan renderer"
143+ severity: "warning"
144+
145+ DRIVER_INCOMPATIBLE:
146+ code: "E003"
147+ message: "GPU driver version incompatible"
148+ severity: "error"
149+
150+ MEMORY_EXCEEDED:
151+ code: "W001"
152+ message: "GPU memory usage exceeded threshold"
153+ severity: "warning"⏎̸
@@ -0,0 +1,153 @@
1+# Rendering Configuration Contract
2+# Defines the interface for GPU/CPU rendering configuration
3+
4+version: "1.0.0"
5+component: "RenderingConfig"
6+
7+# Configuration Interface
8+config:
9+ rendering_mode:
10+ type: "enum"
11+ values:
12+ - name: "auto"
13+ description: "Automatically detect GPU availability and use if present"
14+ default: true
15+ - name: "gpu"
16+ description: "Force GPU acceleration (fail if unavailable)"
17+ default: false
18+ - name: "cpu"
19+ description: "Force software rendering (disable GPU)"
20+ default: false
21+
22+ settings:
23+ enable_2d_canvas:
24+ type: "boolean"
25+ default: true
26+ description: "Enable hardware acceleration for 2D canvas"
27+
28+ enable_webgl:
29+ type: "boolean"
30+ default: true
31+ description: "Enable WebGL for 3D content"
32+
33+ draw_compositing_indicators:
34+ type: "boolean"
35+ default: false
36+ description: "Show GPU compositing borders (debug)"
37+
38+# CLI Interface
39+cli:
40+ flags:
41+ - name: "--rendering-mode"
42+ type: "string"
43+ values: ["auto", "gpu", "cpu"]
44+ default: "auto"
45+ description: "Set rendering mode"
46+
47+ - name: "--disable-gpu"
48+ type: "boolean"
49+ default: false
50+ description: "Shorthand for --rendering-mode=cpu"
51+
52+ - name: "--force-gpu"
53+ type: "boolean"
54+ default: false
55+ description: "Shorthand for --rendering-mode=gpu"
56+
57+ - name: "--debug-gpu"
58+ type: "boolean"
59+ default: false
60+ description: "Enable GPU compositing indicators"
61+
62+# Environment Variables
63+environment:
64+ - name: "DUMBER_RENDERING_MODE"
65+ values: ["auto", "gpu", "cpu"]
66+ description: "Override rendering mode"
67+
68+ - name: "WEBKIT_DISABLE_COMPOSITING_MODE"
69+ values: ["1", "0"]
70+ description: "WebKit native disable flag"
71+
72+# Status API
73+status:
74+ endpoint: "/api/rendering/status"
75+ response:
76+ mode:
77+ type: "string"
78+ enum: ["auto", "gpu", "cpu"]
79+
80+ gpu_active:
81+ type: "boolean"
82+ description: "Whether GPU acceleration is currently active"
83+
84+ gpu_available:
85+ type: "boolean"
86+ description: "Whether GPU hardware is available"
87+
88+ vulkan_enabled:
89+ type: "boolean"
90+ description: "Whether Vulkan renderer is active"
91+
92+ errors:
93+ type: "array"
94+ items:
95+ type: "object"
96+ properties:
97+ code: "string"
98+ message: "string"
99+ fallback_used: "boolean"
100+
101+ performance:
102+ type: "object"
103+ properties:
104+ fps: "number"
105+ cpu_usage_percent: "number"
106+ gpu_usage_percent: "number"
107+ memory_mb: "integer"
108+
109+# Validation Rules
110+validation:
111+ startup:
112+ - check: "GPU availability if mode=gpu"
113+ error: "GPU acceleration requested but not available"
114+ action: "fail_startup"
115+
116+ - check: "Vulkan support if GPU active"
117+ warning: "Vulkan not available, using OpenGL"
118+ action: "log_warning"
119+
120+ - check: "Driver compatibility"
121+ error: "GPU driver incompatible"
122+ action: "fallback_to_cpu"
123+
124+ runtime:
125+ - check: "GPU memory < 500MB"
126+ warning: "High GPU memory usage"
127+ action: "log_metric"
128+
129+ - check: "FPS > 30"
130+ warning: "Low frame rate detected"
131+ action: "log_metric"
132+
133+# Error Codes
134+errors:
135+ GPU_NOT_AVAILABLE:
136+ code: "E001"
137+ message: "GPU hardware not detected"
138+ severity: "error"
139+
140+ VULKAN_INIT_FAILED:
141+ code: "E002"
142+ message: "Failed to initialize Vulkan renderer"
143+ severity: "warning"
144+
145+ DRIVER_INCOMPATIBLE:
146+ code: "E003"
147+ message: "GPU driver version incompatible"
148+ severity: "error"
149+
150+ MEMORY_EXCEEDED:
151+ code: "W001"
152+ message: "GPU memory usage exceeded threshold"
153+ severity: "warning"⏎̸
A specs/004-migrate-to-webkitgtk/contracts/webkit-api.yaml +111 -0
@@ -0,0 +1,111 @@
1+# WebKit API Migration Contract
2+# Defines the interface mapping between WebKit2GTK 4.0 and WebKitGTK 6.0
3+
4+version: "1.0.0"
5+migration:
6+ from: "webkit2gtk-4.0"
7+ to: "webkitgtk-6.0"
8+
9+# Core WebView Management
10+webview:
11+ creation:
12+ old:
13+ function: "webkit_web_view_new_with_context"
14+ params: ["WebKitWebContext*"]
15+ returns: "GtkWidget*"
16+ new:
17+ function: "g_object_new"
18+ params: ["WEBKIT_TYPE_WEB_VIEW", "properties..."]
19+ returns: "GtkWidget*"
20+
21+ settings:
22+ hardware_acceleration:
23+ function: "webkit_settings_set_hardware_acceleration_policy"
24+ params: ["WebKitSettings*", "WebKitHardwareAccelerationPolicy"]
25+ values:
26+ - WEBKIT_HARDWARE_ACCELERATION_POLICY_ON_DEMAND
27+ - WEBKIT_HARDWARE_ACCELERATION_POLICY_ALWAYS
28+ - WEBKIT_HARDWARE_ACCELERATION_POLICY_NEVER
29+
30+ canvas_acceleration:
31+ function: "webkit_settings_set_enable_2d_canvas_acceleration"
32+ params: ["WebKitSettings*", "gboolean"]
33+
34+ webgl:
35+ function: "webkit_settings_set_enable_webgl"
36+ params: ["WebKitSettings*", "gboolean"]
37+
38+# GTK4 Widget Changes
39+gtk:
40+ window:
41+ create:
42+ old: "gtk_window_new(GTK_WINDOW_TOPLEVEL)"
43+ new: "gtk_window_new()"
44+
45+ add_child:
46+ old: "gtk_container_add(container, widget)"
47+ new: "gtk_window_set_child(window, widget)"
48+
49+ destroy:
50+ old: "gtk_widget_destroy(widget)"
51+ new: "gtk_window_destroy(GTK_WINDOW(widget))"
52+
53+ events:
54+ key_press:
55+ old:
56+ signal: "key-press-event"
57+ callback: "gboolean (*)(GtkWidget*, GdkEventKey*, gpointer)"
58+ new:
59+ controller: "gtk_event_controller_key_new()"
60+ signal: "key-pressed"
61+ callback: "gboolean (*)(GtkEventControllerKey*, guint, guint, GdkModifierType, gpointer)"
62+
63+ button_press:
64+ old:
65+ signal: "button-press-event"
66+ callback: "gboolean (*)(GtkWidget*, GdkEventButton*, gpointer)"
67+ new:
68+ controller: "gtk_gesture_click_new()"
69+ signal: "pressed"
70+ callback: "void (*)(GtkGestureClick*, gint, gdouble, gdouble, gpointer)"
71+
72+# Network Session (replaces WebContext)
73+network:
74+ session:
75+ old:
76+ type: "WebKitWebContext"
77+ data_manager: "webkit_website_data_manager_new"
78+ new:
79+ type: "WebKitNetworkSession"
80+ create: "webkit_network_session_new"
81+ ephemeral: "webkit_network_session_new_ephemeral"
82+ default: "webkit_network_session_get_default"
83+
84+# Build Configuration
85+build:
86+ pkg_config:
87+ old:
88+ - "webkit2gtk-4.0"
89+ - "gtk+-3.0"
90+ - "javascriptcoregtk-4.0"
91+ new:
92+ - "webkitgtk-6.0"
93+ - "gtk4"
94+ - "javascriptcoregtk-6.0"
95+
96+ headers:
97+ old:
98+ - "#include <webkit2/webkit2.h>"
99+ - "#include <gtk/gtk.h>"
100+ new:
101+ - "#include <webkit/webkit.h>"
102+ - "#include <gtk/gtk.h>"
103+
104+# Compatibility Requirements
105+compatibility:
106+ minimum_gtk: "4.0"
107+ minimum_webkit: "2.40" # First version with webkitgtk-6.0
108+ required_features:
109+ - hardware_acceleration_policy
110+ - network_session
111+ - sandbox_always_enabled⏎̸
@@ -0,0 +1,111 @@
1+# WebKit API Migration Contract
2+# Defines the interface mapping between WebKit2GTK 4.0 and WebKitGTK 6.0
3+
4+version: "1.0.0"
5+migration:
6+ from: "webkit2gtk-4.0"
7+ to: "webkitgtk-6.0"
8+
9+# Core WebView Management
10+webview:
11+ creation:
12+ old:
13+ function: "webkit_web_view_new_with_context"
14+ params: ["WebKitWebContext*"]
15+ returns: "GtkWidget*"
16+ new:
17+ function: "g_object_new"
18+ params: ["WEBKIT_TYPE_WEB_VIEW", "properties..."]
19+ returns: "GtkWidget*"
20+
21+ settings:
22+ hardware_acceleration:
23+ function: "webkit_settings_set_hardware_acceleration_policy"
24+ params: ["WebKitSettings*", "WebKitHardwareAccelerationPolicy"]
25+ values:
26+ - WEBKIT_HARDWARE_ACCELERATION_POLICY_ON_DEMAND
27+ - WEBKIT_HARDWARE_ACCELERATION_POLICY_ALWAYS
28+ - WEBKIT_HARDWARE_ACCELERATION_POLICY_NEVER
29+
30+ canvas_acceleration:
31+ function: "webkit_settings_set_enable_2d_canvas_acceleration"
32+ params: ["WebKitSettings*", "gboolean"]
33+
34+ webgl:
35+ function: "webkit_settings_set_enable_webgl"
36+ params: ["WebKitSettings*", "gboolean"]
37+
38+# GTK4 Widget Changes
39+gtk:
40+ window:
41+ create:
42+ old: "gtk_window_new(GTK_WINDOW_TOPLEVEL)"
43+ new: "gtk_window_new()"
44+
45+ add_child:
46+ old: "gtk_container_add(container, widget)"
47+ new: "gtk_window_set_child(window, widget)"
48+
49+ destroy:
50+ old: "gtk_widget_destroy(widget)"
51+ new: "gtk_window_destroy(GTK_WINDOW(widget))"
52+
53+ events:
54+ key_press:
55+ old:
56+ signal: "key-press-event"
57+ callback: "gboolean (*)(GtkWidget*, GdkEventKey*, gpointer)"
58+ new:
59+ controller: "gtk_event_controller_key_new()"
60+ signal: "key-pressed"
61+ callback: "gboolean (*)(GtkEventControllerKey*, guint, guint, GdkModifierType, gpointer)"
62+
63+ button_press:
64+ old:
65+ signal: "button-press-event"
66+ callback: "gboolean (*)(GtkWidget*, GdkEventButton*, gpointer)"
67+ new:
68+ controller: "gtk_gesture_click_new()"
69+ signal: "pressed"
70+ callback: "void (*)(GtkGestureClick*, gint, gdouble, gdouble, gpointer)"
71+
72+# Network Session (replaces WebContext)
73+network:
74+ session:
75+ old:
76+ type: "WebKitWebContext"
77+ data_manager: "webkit_website_data_manager_new"
78+ new:
79+ type: "WebKitNetworkSession"
80+ create: "webkit_network_session_new"
81+ ephemeral: "webkit_network_session_new_ephemeral"
82+ default: "webkit_network_session_get_default"
83+
84+# Build Configuration
85+build:
86+ pkg_config:
87+ old:
88+ - "webkit2gtk-4.0"
89+ - "gtk+-3.0"
90+ - "javascriptcoregtk-4.0"
91+ new:
92+ - "webkitgtk-6.0"
93+ - "gtk4"
94+ - "javascriptcoregtk-6.0"
95+
96+ headers:
97+ old:
98+ - "#include <webkit2/webkit2.h>"
99+ - "#include <gtk/gtk.h>"
100+ new:
101+ - "#include <webkit/webkit.h>"
102+ - "#include <gtk/gtk.h>"
103+
104+# Compatibility Requirements
105+compatibility:
106+ minimum_gtk: "4.0"
107+ minimum_webkit: "2.40" # First version with webkitgtk-6.0
108+ required_features:
109+ - hardware_acceleration_policy
110+ - network_session
111+ - sandbox_always_enabled⏎̸
A specs/004-migrate-to-webkitgtk/data-model.md +170 -0
@@ -0,0 +1,170 @@
1+# Data Model: WebKitGTK 6.0 Migration
2+
3+## Configuration Model
4+
5+### RenderingConfig
6+```go
7+type RenderingMode string
8+
9+const (
10+ RenderingModeAuto RenderingMode = "auto" // Detect GPU availability
11+ RenderingModeGPU RenderingMode = "gpu" // Force GPU acceleration
12+ RenderingModeCPU RenderingMode = "cpu" // Force software rendering
13+)
14+
15+type RenderingConfig struct {
16+ Mode RenderingMode // Default: auto
17+ Enable2DCanvas bool // Default: true
18+ EnableWebGL bool // Default: true
19+ DrawCompositingIndicators bool // Default: false (debug only)
20+}
21+```
22+
23+### Updated WebKit Config
24+```go
25+type Config struct {
26+ // Existing fields
27+ InitialURL string
28+ UserAgent string
29+ EnableDeveloperExtras bool
30+ ZoomDefault float64
31+ DataDir string
32+ CacheDir string
33+ DefaultSansFont string
34+ DefaultSerifFont string
35+ DefaultMonospaceFont string
36+ DefaultFontSize int
37+
38+ // New GPU rendering fields
39+ Rendering RenderingConfig
40+}
41+```
42+
43+## API Mapping Model
44+
45+### Core WebView APIs
46+| Component | WebKit2GTK 4.0 | WebKitGTK 6.0 | Status |
47+|-----------|---------------|---------------|---------|
48+| WebView Creation | `webkit_web_view_new_with_context()` | `g_object_new(WEBKIT_TYPE_WEB_VIEW, ...)` | Breaking |
49+| Context | `WebKitWebContext` | `WebKitNetworkSession` | Breaking |
50+| Settings | `WebKitSettings` | `WebKitSettings` (expanded) | Compatible |
51+| UserContentManager | Same | Same | Compatible |
52+| JavaScript | `webkit_web_view_run_javascript()` | Same (async only) | Compatible |
53+
54+### GTK Widget APIs
55+| Component | GTK3 | GTK4 | Status |
56+|-----------|------|------|---------|
57+| Window | `gtk_window_new(GTK_WINDOW_TOPLEVEL)` | `gtk_window_new()` | Breaking |
58+| Container | `gtk_container_add()` | `gtk_window_set_child()` | Breaking |
59+| Events | `GdkEventKey*` | `GtkEventController` | Breaking |
60+| Show/Hide | `gtk_widget_show()` | Same | Compatible |
61+| Destroy | `gtk_widget_destroy()` | `gtk_window_destroy()` | Breaking |
62+
63+### Hardware Acceleration APIs
64+```go
65+// New acceleration policy enum
66+type HardwareAccelerationPolicy int
67+
68+const (
69+ PolicyOnDemand HardwareAccelerationPolicy = iota // Auto-detect
70+ PolicyAlways // Force GPU
71+ PolicyNever // Force CPU
72+)
73+
74+// Mapping to WebKit constants
75+var policyMap = map[RenderingMode]HardwareAccelerationPolicy{
76+ RenderingModeAuto: PolicyOnDemand,
77+ RenderingModeGPU: PolicyAlways,
78+ RenderingModeCPU: PolicyNever,
79+}
80+```
81+
82+## Migration States
83+
84+### File Migration Status
85+```go
86+type MigrationStatus string
87+
88+const (
89+ StatusPending MigrationStatus = "pending"
90+ StatusInProgress MigrationStatus = "in_progress"
91+ StatusComplete MigrationStatus = "complete"
92+ StatusTested MigrationStatus = "tested"
93+)
94+
95+type FileMigration struct {
96+ Path string
97+ Status MigrationStatus
98+ Changes []string // List of API changes made
99+ Tests []string // Associated test files
100+}
101+```
102+
103+## Error Handling Model
104+
105+### GPU Fallback Errors
106+```go
107+type GPUError struct {
108+ Type string // "initialization", "driver", "memory"
109+ Message string
110+ Fallback bool // Whether CPU fallback was successful
111+}
112+
113+type RenderingStatus struct {
114+ Mode RenderingMode
115+ GPUActive bool
116+ Errors []GPUError
117+ Performance struct {
118+ FPS float64
119+ CPUUsage float64
120+ GPUUsage float64
121+ MemoryMB int
122+ }
123+}
124+```
125+
126+## Build Configuration
127+
128+### Package Config Changes
129+```makefile
130+# Old build flags
131+WEBKIT_PKG = webkit2gtk-4.0
132+GTK_PKG = gtk+-3.0
133+JS_PKG = javascriptcoregtk-4.0
134+
135+# New build flags
136+WEBKIT_PKG = webkitgtk-6.0
137+GTK_PKG = gtk4
138+JS_PKG = javascriptcoregtk-6.0
139+```
140+
141+## Validation Rules
142+
143+### Rendering Mode Validation
144+- If GPU forced but unavailable → Error with suggestion to use auto
145+- If CPU forced → Warn about performance impact
146+- If auto → Log detected mode at startup
147+
148+### Performance Thresholds
149+- Startup time must remain < 500ms
150+- GPU memory usage < 500MB
151+- CPU usage reduction > 20% when GPU active
152+- FPS improvement > 30% for animations
153+
154+## State Transitions
155+
156+### Rendering Mode State Machine
157+```
158+┌─────┐ Check GPU ┌──────┐ Success ┌─────┐
159+│Auto │────────────→│Detect│────────→│GPU │
160+└─────┘ └──────┘ └─────┘
161+ │ Fail │ Error
162+ ↓ ↓
163+ ┌─────┐ ┌─────┐
164+ │CPU │←─────────│Retry│
165+ └─────┘ Fallback └─────┘
166+```
167+
168+## Data Persistence
169+
170+No changes to existing SQLite schema required. Rendering mode stored in config file only.⏎̸
@@ -0,0 +1,170 @@
1+# Data Model: WebKitGTK 6.0 Migration
2+
3+## Configuration Model
4+
5+### RenderingConfig
6+```go
7+type RenderingMode string
8+
9+const (
10+ RenderingModeAuto RenderingMode = "auto" // Detect GPU availability
11+ RenderingModeGPU RenderingMode = "gpu" // Force GPU acceleration
12+ RenderingModeCPU RenderingMode = "cpu" // Force software rendering
13+)
14+
15+type RenderingConfig struct {
16+ Mode RenderingMode // Default: auto
17+ Enable2DCanvas bool // Default: true
18+ EnableWebGL bool // Default: true
19+ DrawCompositingIndicators bool // Default: false (debug only)
20+}
21+```
22+
23+### Updated WebKit Config
24+```go
25+type Config struct {
26+ // Existing fields
27+ InitialURL string
28+ UserAgent string
29+ EnableDeveloperExtras bool
30+ ZoomDefault float64
31+ DataDir string
32+ CacheDir string
33+ DefaultSansFont string
34+ DefaultSerifFont string
35+ DefaultMonospaceFont string
36+ DefaultFontSize int
37+
38+ // New GPU rendering fields
39+ Rendering RenderingConfig
40+}
41+```
42+
43+## API Mapping Model
44+
45+### Core WebView APIs
46+| Component | WebKit2GTK 4.0 | WebKitGTK 6.0 | Status |
47+|-----------|---------------|---------------|---------|
48+| WebView Creation | `webkit_web_view_new_with_context()` | `g_object_new(WEBKIT_TYPE_WEB_VIEW, ...)` | Breaking |
49+| Context | `WebKitWebContext` | `WebKitNetworkSession` | Breaking |
50+| Settings | `WebKitSettings` | `WebKitSettings` (expanded) | Compatible |
51+| UserContentManager | Same | Same | Compatible |
52+| JavaScript | `webkit_web_view_run_javascript()` | Same (async only) | Compatible |
53+
54+### GTK Widget APIs
55+| Component | GTK3 | GTK4 | Status |
56+|-----------|------|------|---------|
57+| Window | `gtk_window_new(GTK_WINDOW_TOPLEVEL)` | `gtk_window_new()` | Breaking |
58+| Container | `gtk_container_add()` | `gtk_window_set_child()` | Breaking |
59+| Events | `GdkEventKey*` | `GtkEventController` | Breaking |
60+| Show/Hide | `gtk_widget_show()` | Same | Compatible |
61+| Destroy | `gtk_widget_destroy()` | `gtk_window_destroy()` | Breaking |
62+
63+### Hardware Acceleration APIs
64+```go
65+// New acceleration policy enum
66+type HardwareAccelerationPolicy int
67+
68+const (
69+ PolicyOnDemand HardwareAccelerationPolicy = iota // Auto-detect
70+ PolicyAlways // Force GPU
71+ PolicyNever // Force CPU
72+)
73+
74+// Mapping to WebKit constants
75+var policyMap = map[RenderingMode]HardwareAccelerationPolicy{
76+ RenderingModeAuto: PolicyOnDemand,
77+ RenderingModeGPU: PolicyAlways,
78+ RenderingModeCPU: PolicyNever,
79+}
80+```
81+
82+## Migration States
83+
84+### File Migration Status
85+```go
86+type MigrationStatus string
87+
88+const (
89+ StatusPending MigrationStatus = "pending"
90+ StatusInProgress MigrationStatus = "in_progress"
91+ StatusComplete MigrationStatus = "complete"
92+ StatusTested MigrationStatus = "tested"
93+)
94+
95+type FileMigration struct {
96+ Path string
97+ Status MigrationStatus
98+ Changes []string // List of API changes made
99+ Tests []string // Associated test files
100+}
101+```
102+
103+## Error Handling Model
104+
105+### GPU Fallback Errors
106+```go
107+type GPUError struct {
108+ Type string // "initialization", "driver", "memory"
109+ Message string
110+ Fallback bool // Whether CPU fallback was successful
111+}
112+
113+type RenderingStatus struct {
114+ Mode RenderingMode
115+ GPUActive bool
116+ Errors []GPUError
117+ Performance struct {
118+ FPS float64
119+ CPUUsage float64
120+ GPUUsage float64
121+ MemoryMB int
122+ }
123+}
124+```
125+
126+## Build Configuration
127+
128+### Package Config Changes
129+```makefile
130+# Old build flags
131+WEBKIT_PKG = webkit2gtk-4.0
132+GTK_PKG = gtk+-3.0
133+JS_PKG = javascriptcoregtk-4.0
134+
135+# New build flags
136+WEBKIT_PKG = webkitgtk-6.0
137+GTK_PKG = gtk4
138+JS_PKG = javascriptcoregtk-6.0
139+```
140+
141+## Validation Rules
142+
143+### Rendering Mode Validation
144+- If GPU forced but unavailable → Error with suggestion to use auto
145+- If CPU forced → Warn about performance impact
146+- If auto → Log detected mode at startup
147+
148+### Performance Thresholds
149+- Startup time must remain < 500ms
150+- GPU memory usage < 500MB
151+- CPU usage reduction > 20% when GPU active
152+- FPS improvement > 30% for animations
153+
154+## State Transitions
155+
156+### Rendering Mode State Machine
157+```
158+┌─────┐ Check GPU ┌──────┐ Success ┌─────┐
159+│Auto │────────────→│Detect│────────→│GPU │
160+└─────┘ └──────┘ └─────┘
161+ │ Fail │ Error
162+ ↓ ↓
163+ ┌─────┐ ┌─────┐
164+ │CPU │←─────────│Retry│
165+ └─────┘ Fallback └─────┘
166+```
167+
168+## Data Persistence
169+
170+No changes to existing SQLite schema required. Rendering mode stored in config file only.⏎̸
A specs/004-migrate-to-webkitgtk/plan.md +251 -0
@@ -0,0 +1,251 @@
1+# Implementation Plan: Migrate to WebKitGTK 6.0
2+
3+**Branch**: `004-migrate-to-webkitgtk` | **Date**: 2025-01-21 | **Spec**: [spec.md](./spec.md)
4+**Input**: Feature specification from `/specs/004-migrate-to-webkitgtk/spec.md`
5+
6+## Execution Flow (/plan command scope)
7+```
8+1. Load feature spec from Input path
9+ → Successfully loaded spec for WebKitGTK 6.0 migration
10+2. Fill Technical Context (scan for NEEDS CLARIFICATION)
11+ → Project Type: Single (Go browser application)
12+ → Structure Decision: Option 1 (single project)
13+ → Open clarifications remain in spec (FR-008, FR-011, FR-012)
14+3. Evaluate Constitution Check section below
15+ → Adjust for remaining clarifications; document deviations
16+ → Update Progress Tracking: Initial Constitution Check
17+4. Execute Phase 0 → research.md (complete)
18+ → Research WebKit API differences and migration path
19+5. Execute Phase 1 → contracts, data-model.md, quickstart.md (complete)
20+6. Re-evaluate Constitution Check section
21+ → Design aligned with constitution principles; clarifications still pending
22+ → Update Progress Tracking: Post-Design Constitution Check
23+7. Plan Phase 2 → Describe task generation approach (DO NOT create tasks.md) (complete)
24+8. Generate tasks via /tasks (exists: tasks.md)
25+9. Track Phase 4 implementation progress (in progress)
26+```
27+
28+## Summary
29+Migrate the dumber browser from WebKit2GTK 4.0 (GTK3) to WebKitGTK 6.0 (GTK4) to enable GPU rendering through Vulkan layer. The migration will involve incrementally updating the pkg/webkit package files while maintaining browser functionality. A global rendering configuration will be added to allow users to choose between CPU and GPU rendering, with GPU enabled by default when supported.
30+
31+## Technical Context
32+**Language/Version**: Go 1.25.1
33+**Primary Dependencies**: WebKitGTK 6.0, GTK4, libsoup3, SQLite3
34+**Storage**: SQLite3 for history/settings, WebKitWebsiteDataManager for browser data
35+**Testing**: Go tests, contract tests for WebKit bindings
36+**Target Platform**: Linux (Wayland-first, X11 compatible)
37+**Project Type**: single - Go browser application
38+**Performance Goals**: < 500ms startup, improved GPU rendering performance
39+**Constraints**: Maintain all existing functionality, graceful fallback for non-Vulkan systems
40+**Scale/Scope**: ~15 files in pkg/webkit to migrate, ~3000 LOC
41+
42+**User-Provided Context**: No TDD for this, we will start by checking the WebKit API differences and migrate pkg/webkit files one by one
43+
44+## Current Snapshot (repo state)
45+- Build flags already target GTK4/WebKitGTK 6.0 in `pkg/webkit/*_cgo.go` ✓
46+- Core files present: `webview_cgo.go`, `window_cgo.go`, `script_cgo.go`, `scheme_cgo.go`, `zoom_cgo.go`, `keyboard_cgo.go`, `devtools_cgo.go`, `loop_cgo.go`, plus Go wrappers (`*.go`) ✓
47+- Migration underway in `webview_cgo.go` (GTK4 APIs, evaluate_javascript) ✓
48+- Spec still has [NEEDS CLARIFICATION] for performance targets and timeline ✗
49+
50+## Constitution Check
51+*GATE: Must pass before Phase 0 research. Re-check after Phase 1 design.*
52+
53+**Simplicity**:
54+- Projects: 1 (single Go browser project) ✓
55+- Using framework directly? YES (direct CGO bindings to WebKitGTK) ✓
56+- Single data model? YES (existing models unchanged) ✓
57+- Avoiding patterns? YES (no unnecessary abstractions) ✓
58+
59+**Architecture**:
60+- EVERY feature as library? pkg/webkit is already a library ✓
61+- Libraries listed: pkg/webkit (WebKit2GTK Go bindings)
62+- CLI per library: Browser CLI with browse, history, dmenu commands ✓
63+- Library docs: Will update with new API documentation
64+
65+**Testing (Modified per user request)**:
66+- User specified: "No TDD for this" - migration-focused approach
67+- Will add tests after migration for regression prevention
68+- Contract tests for new WebKit bindings
69+- Integration tests for browser functionality
70+
71+**Observability**:
72+- Structured logging included? YES (slog) ✓
73+- Frontend logs → backend? N/A (native application)
74+- Error context sufficient? YES ✓
75+
76+**Versioning**:
77+- Version number assigned? Will increment after migration
78+- BUILD increments on every change? YES ✓
79+- Breaking changes handled? API compatibility layer during migration
80+
81+## Project Structure
82+
83+### Documentation (this feature)
84+```
85+specs/004-migrate-to-webkitgtk/
86+├── plan.md # This file (/plan command output)
87+├── research.md # Phase 0 output (/plan command)
88+├── data-model.md # Phase 1 output (/plan command)
89+├── quickstart.md # Phase 1 output (/plan command)
90+├── contracts/ # Phase 1 output (/plan command)
91+└── tasks.md # Phase 2 output (/tasks command - NOT created by /plan)
92+```
93+
94+### Source Code (repository root)
95+```
96+# Option 1: Single project (SELECTED)
97+pkg/
98+└── webkit/ # WebKitGTK Go bindings (migration target)
99+ ├── webview_cgo.go # WebView implementation (GTK4/WebKit 6)
100+ ├── window_cgo.go # Window management (GTK4)
101+ ├── script_cgo.go # JavaScript evaluation/injection
102+ ├── scheme_cgo.go # Custom URI scheme handler
103+ ├── zoom_cgo.go # Zoom control
104+ ├── keyboard_cgo.go # Keyboard handling (controllers)
105+ ├── devtools_cgo.go # Inspector
106+ ├── loop_cgo.go # Main loop helpers
107+ ├── settings.go # High-level settings glue
108+ └── *.go # Non-CGO wrappers (window.go, zoom.go, script.go, ...)
109+
110+internal/
111+├── cli/ # CLI entry points (browse, history, dmenu)
112+├── config/ # App config (extend for rendering)
113+└── db/ # SQLite persistence
114+
115+tests/
116+├── contract/ # Contract tests for WebKit bindings
117+├── integration/ # End-to-end validation
118+└── unit/ # Unit tests
119+```
120+
121+**Structure Decision**: Option 1 (Single project) - maintaining existing structure
122+
123+## Phase 0: Outline & Research
124+1. **Extract unknowns from Technical Context**:
125+ - WebKitGTK 6.0 API changes from 4.0
126+ - GTK4 migration requirements
127+ - Vulkan renderer configuration
128+ - CGO binding updates needed
129+ - Build system changes (pkg-config)
130+
131+2. **Generate and dispatch research agents**:
132+ ```
133+ Task: "Research WebKitGTK 6.0 API breaking changes"
134+ Task: "Find GTK3 to GTK4 migration patterns for CGO"
135+ Task: "Research Vulkan enablement in WebKitGTK 6.0"
136+ Task: "Identify deprecated WebKit2GTK 4.0 APIs"
137+ ```
138+
139+3. **Consolidate findings** in `research.md`
140+
141+**Output**: research.md with migration path defined
142+
143+## Phase 1: Design & Contracts
144+*Prerequisites: research.md complete*
145+
146+1. **API Mapping** → `data-model.md`:
147+ - Map WebKit2GTK 4.0 APIs to WebKitGTK 6.0 equivalents
148+ - Document removed/replaced functions
149+ - Identify new required APIs
150+
151+2. **Generate migration contracts**:
152+ - Define compatibility layer interfaces
153+ - Create migration checkpoints
154+ - Output to `/contracts/`
155+
156+3. **Migration test scenarios**:
157+ - Test each webkit file migration
158+ - Verify GPU acceleration
159+ - Fallback testing
160+
161+4. **Create quickstart guide**:
162+ - Build requirements for GTK4/WebKitGTK 6.0
163+ - Verification steps
164+ - Performance testing
165+
166+5. **Update CLAUDE.md**:
167+ - Add WebKitGTK 6.0 context
168+ - GTK4 migration notes
169+ - Recent changes
170+
171+**Output**: data-model.md, /contracts/*, quickstart.md, CLAUDE.md updates
172+
173+## File‑By‑File Migration Plan (grounded in repo)
174+- `pkg/webkit/build_flags_cgo.go` / `build_cgo_stub.go` / `build_flags_stub.go`: ensure pkg-config switched to `webkitgtk-6.0 gtk4 javascriptcoregtk-6.0` (present; verify includes and tags)
175+- `pkg/webkit/webview_cgo.go`: use `g_object_new` patterns as needed; ensure `webkit_web_view_evaluate_javascript` (async) usage; connect notify signals via GTK4; migrate any deprecated UCM message handlers.
176+- `pkg/webkit/window_cgo.go`: use `gtk_window_new()`, `gtk_window_set_child()`, and `gtk_window_destroy()`; remove `gtk_container_add()` remnants.
177+- `pkg/webkit/keyboard_cgo.go`: replace GdkEvent handlers with `GtkEventControllerKey` (key-pressed) and gestures for mouse input.
178+- `pkg/webkit/zoom_cgo.go` / `zoom.go`: confirm zoom ranges and GTK4 APIs for zoom if used; keep high-level validation in Go.
179+- `pkg/webkit/script_cgo.go` / `script.go`: standardize JS evaluation via `webkit_web_view_evaluate_javascript`.
180+- `pkg/webkit/scheme_cgo.go`: verify `WebKitURISchemeRequest` handling and memory ownership semantics on GTK4/WebKit 6.
181+- `pkg/webkit/devtools_cgo.go`: validate inspector APIs on WebKit 6.
182+- `pkg/webkit/loop_cgo.go`: update any deprecated main-loop helpers.
183+- `internal/config/*`: add `RenderingConfig` and `RenderingMode` with env/flag wiring.
184+- `internal/cli/browse.go`: add `--rendering-mode`, `--disable-gpu`, `--debug-gpu`.
185+- `services/*`: plumb rendering config into WebView creation.
186+
187+## Phase 2: Task Planning Approach
188+*This section describes what the /tasks command will do - DO NOT execute during /plan*
189+
190+**Task Generation Strategy**:
191+- Analyze each file in pkg/webkit for required changes
192+- Create migration task for each file
193+- Order by dependency (build files first, then core, then features)
194+- Add verification tasks after each major component
195+
196+**Ordering Strategy**:
197+1. Build configuration (pkg-config, CGO flags)
198+2. Core types and structures
199+3. Window and WebView creation
200+4. Feature-specific files (zoom, script, keyboard)
201+5. Integration testing
202+6. Performance verification
203+
204+**Estimated Output**: 20-25 migration tasks in tasks.md
205+
206+**IMPORTANT**: This phase is executed by the /tasks command, NOT by /plan
207+
208+## Phase 3+: Future Implementation
209+*These phases are beyond the scope of the /plan command*
210+
211+**Phase 3**: Task execution (/tasks command creates tasks.md)
212+**Phase 4**: Implementation (execute migration tasks)
213+**Phase 5**: Validation (verify GPU acceleration, run performance tests)
214+
215+## Risks & Mitigations
216+- Driver variability: prefer ON_DEMAND policy by default; expose override flags; add CPU fallback on init failures.
217+- API drift: adhere to contracts in `contracts/webkit-api.yaml`; gate changes behind compile tags where helpful.
218+- Performance regressions: provide quickstart validation and capture metrics; keep rollback point per task.
219+
220+## Complexity Tracking
221+*Fill ONLY if Constitution Check has violations that must be justified*
222+
223+| Violation | Why Needed | Simpler Alternative Rejected Because |
224+|-----------|------------|-------------------------------------|
225+| No TDD for migration | User-specified approach | TDD would slow down API exploration during migration |
226+
227+## Progress Tracking
228+*This checklist is updated during execution flow*
229+
230+**Phase Status**:
231+- [x] Phase 0: Research complete (/plan command)
232+- [x] Phase 1: Design complete (/plan command)
233+- [x] Phase 2: Task planning complete (/plan command - describe approach only)
234+- [x] Phase 3: Tasks generated (/tasks command)
235+- [ ] Phase 4: Implementation in progress
236+- [ ] Phase 5: Validation passed
237+
238+**Gate Status**:
239+- [x] Initial Constitution Check: PASS
240+- [x] Post-Design Constitution Check: PASS (performance targets pending)
241+- [ ] All NEEDS CLARIFICATION resolved (see spec FR-008, FR-011, FR-012)
242+- [x] Complexity deviations documented
243+
244+## Next Actions
245+- Verify and finalize build flags and includes across `pkg/webkit/*_cgo.go`.
246+- Align `internal/config` and `internal/cli` with `RenderingConfig` and flags.
247+- Complete event/gesture migration in `keyboard_cgo.go` and related handlers.
248+- Add hardware acceleration policy mapping during WebView initialization.
249+
250+---
251+*Based on Constitution v1.0.0 - See `/memory/constitution.md`*
@@ -0,0 +1,251 @@
1+# Implementation Plan: Migrate to WebKitGTK 6.0
2+
3+**Branch**: `004-migrate-to-webkitgtk` | **Date**: 2025-01-21 | **Spec**: [spec.md](./spec.md)
4+**Input**: Feature specification from `/specs/004-migrate-to-webkitgtk/spec.md`
5+
6+## Execution Flow (/plan command scope)
7+```
8+1. Load feature spec from Input path
9+ → Successfully loaded spec for WebKitGTK 6.0 migration
10+2. Fill Technical Context (scan for NEEDS CLARIFICATION)
11+ → Project Type: Single (Go browser application)
12+ → Structure Decision: Option 1 (single project)
13+ → Open clarifications remain in spec (FR-008, FR-011, FR-012)
14+3. Evaluate Constitution Check section below
15+ → Adjust for remaining clarifications; document deviations
16+ → Update Progress Tracking: Initial Constitution Check
17+4. Execute Phase 0 → research.md (complete)
18+ → Research WebKit API differences and migration path
19+5. Execute Phase 1 → contracts, data-model.md, quickstart.md (complete)
20+6. Re-evaluate Constitution Check section
21+ → Design aligned with constitution principles; clarifications still pending
22+ → Update Progress Tracking: Post-Design Constitution Check
23+7. Plan Phase 2 → Describe task generation approach (DO NOT create tasks.md) (complete)
24+8. Generate tasks via /tasks (exists: tasks.md)
25+9. Track Phase 4 implementation progress (in progress)
26+```
27+
28+## Summary
29+Migrate the dumber browser from WebKit2GTK 4.0 (GTK3) to WebKitGTK 6.0 (GTK4) to enable GPU rendering through Vulkan layer. The migration will involve incrementally updating the pkg/webkit package files while maintaining browser functionality. A global rendering configuration will be added to allow users to choose between CPU and GPU rendering, with GPU enabled by default when supported.
30+
31+## Technical Context
32+**Language/Version**: Go 1.25.1
33+**Primary Dependencies**: WebKitGTK 6.0, GTK4, libsoup3, SQLite3
34+**Storage**: SQLite3 for history/settings, WebKitWebsiteDataManager for browser data
35+**Testing**: Go tests, contract tests for WebKit bindings
36+**Target Platform**: Linux (Wayland-first, X11 compatible)
37+**Project Type**: single - Go browser application
38+**Performance Goals**: < 500ms startup, improved GPU rendering performance
39+**Constraints**: Maintain all existing functionality, graceful fallback for non-Vulkan systems
40+**Scale/Scope**: ~15 files in pkg/webkit to migrate, ~3000 LOC
41+
42+**User-Provided Context**: No TDD for this, we will start by checking the WebKit API differences and migrate pkg/webkit files one by one
43+
44+## Current Snapshot (repo state)
45+- Build flags already target GTK4/WebKitGTK 6.0 in `pkg/webkit/*_cgo.go` ✓
46+- Core files present: `webview_cgo.go`, `window_cgo.go`, `script_cgo.go`, `scheme_cgo.go`, `zoom_cgo.go`, `keyboard_cgo.go`, `devtools_cgo.go`, `loop_cgo.go`, plus Go wrappers (`*.go`) ✓
47+- Migration underway in `webview_cgo.go` (GTK4 APIs, evaluate_javascript) ✓
48+- Spec still has [NEEDS CLARIFICATION] for performance targets and timeline ✗
49+
50+## Constitution Check
51+*GATE: Must pass before Phase 0 research. Re-check after Phase 1 design.*
52+
53+**Simplicity**:
54+- Projects: 1 (single Go browser project) ✓
55+- Using framework directly? YES (direct CGO bindings to WebKitGTK) ✓
56+- Single data model? YES (existing models unchanged) ✓
57+- Avoiding patterns? YES (no unnecessary abstractions) ✓
58+
59+**Architecture**:
60+- EVERY feature as library? pkg/webkit is already a library ✓
61+- Libraries listed: pkg/webkit (WebKit2GTK Go bindings)
62+- CLI per library: Browser CLI with browse, history, dmenu commands ✓
63+- Library docs: Will update with new API documentation
64+
65+**Testing (Modified per user request)**:
66+- User specified: "No TDD for this" - migration-focused approach
67+- Will add tests after migration for regression prevention
68+- Contract tests for new WebKit bindings
69+- Integration tests for browser functionality
70+
71+**Observability**:
72+- Structured logging included? YES (slog) ✓
73+- Frontend logs → backend? N/A (native application)
74+- Error context sufficient? YES ✓
75+
76+**Versioning**:
77+- Version number assigned? Will increment after migration
78+- BUILD increments on every change? YES ✓
79+- Breaking changes handled? API compatibility layer during migration
80+
81+## Project Structure
82+
83+### Documentation (this feature)
84+```
85+specs/004-migrate-to-webkitgtk/
86+├── plan.md # This file (/plan command output)
87+├── research.md # Phase 0 output (/plan command)
88+├── data-model.md # Phase 1 output (/plan command)
89+├── quickstart.md # Phase 1 output (/plan command)
90+├── contracts/ # Phase 1 output (/plan command)
91+└── tasks.md # Phase 2 output (/tasks command - NOT created by /plan)
92+```
93+
94+### Source Code (repository root)
95+```
96+# Option 1: Single project (SELECTED)
97+pkg/
98+└── webkit/ # WebKitGTK Go bindings (migration target)
99+ ├── webview_cgo.go # WebView implementation (GTK4/WebKit 6)
100+ ├── window_cgo.go # Window management (GTK4)
101+ ├── script_cgo.go # JavaScript evaluation/injection
102+ ├── scheme_cgo.go # Custom URI scheme handler
103+ ├── zoom_cgo.go # Zoom control
104+ ├── keyboard_cgo.go # Keyboard handling (controllers)
105+ ├── devtools_cgo.go # Inspector
106+ ├── loop_cgo.go # Main loop helpers
107+ ├── settings.go # High-level settings glue
108+ └── *.go # Non-CGO wrappers (window.go, zoom.go, script.go, ...)
109+
110+internal/
111+├── cli/ # CLI entry points (browse, history, dmenu)
112+├── config/ # App config (extend for rendering)
113+└── db/ # SQLite persistence
114+
115+tests/
116+├── contract/ # Contract tests for WebKit bindings
117+├── integration/ # End-to-end validation
118+└── unit/ # Unit tests
119+```
120+
121+**Structure Decision**: Option 1 (Single project) - maintaining existing structure
122+
123+## Phase 0: Outline & Research
124+1. **Extract unknowns from Technical Context**:
125+ - WebKitGTK 6.0 API changes from 4.0
126+ - GTK4 migration requirements
127+ - Vulkan renderer configuration
128+ - CGO binding updates needed
129+ - Build system changes (pkg-config)
130+
131+2. **Generate and dispatch research agents**:
132+ ```
133+ Task: "Research WebKitGTK 6.0 API breaking changes"
134+ Task: "Find GTK3 to GTK4 migration patterns for CGO"
135+ Task: "Research Vulkan enablement in WebKitGTK 6.0"
136+ Task: "Identify deprecated WebKit2GTK 4.0 APIs"
137+ ```
138+
139+3. **Consolidate findings** in `research.md`
140+
141+**Output**: research.md with migration path defined
142+
143+## Phase 1: Design & Contracts
144+*Prerequisites: research.md complete*
145+
146+1. **API Mapping** → `data-model.md`:
147+ - Map WebKit2GTK 4.0 APIs to WebKitGTK 6.0 equivalents
148+ - Document removed/replaced functions
149+ - Identify new required APIs
150+
151+2. **Generate migration contracts**:
152+ - Define compatibility layer interfaces
153+ - Create migration checkpoints
154+ - Output to `/contracts/`
155+
156+3. **Migration test scenarios**:
157+ - Test each webkit file migration
158+ - Verify GPU acceleration
159+ - Fallback testing
160+
161+4. **Create quickstart guide**:
162+ - Build requirements for GTK4/WebKitGTK 6.0
163+ - Verification steps
164+ - Performance testing
165+
166+5. **Update CLAUDE.md**:
167+ - Add WebKitGTK 6.0 context
168+ - GTK4 migration notes
169+ - Recent changes
170+
171+**Output**: data-model.md, /contracts/*, quickstart.md, CLAUDE.md updates
172+
173+## File‑By‑File Migration Plan (grounded in repo)
174+- `pkg/webkit/build_flags_cgo.go` / `build_cgo_stub.go` / `build_flags_stub.go`: ensure pkg-config switched to `webkitgtk-6.0 gtk4 javascriptcoregtk-6.0` (present; verify includes and tags)
175+- `pkg/webkit/webview_cgo.go`: use `g_object_new` patterns as needed; ensure `webkit_web_view_evaluate_javascript` (async) usage; connect notify signals via GTK4; migrate any deprecated UCM message handlers.
176+- `pkg/webkit/window_cgo.go`: use `gtk_window_new()`, `gtk_window_set_child()`, and `gtk_window_destroy()`; remove `gtk_container_add()` remnants.
177+- `pkg/webkit/keyboard_cgo.go`: replace GdkEvent handlers with `GtkEventControllerKey` (key-pressed) and gestures for mouse input.
178+- `pkg/webkit/zoom_cgo.go` / `zoom.go`: confirm zoom ranges and GTK4 APIs for zoom if used; keep high-level validation in Go.
179+- `pkg/webkit/script_cgo.go` / `script.go`: standardize JS evaluation via `webkit_web_view_evaluate_javascript`.
180+- `pkg/webkit/scheme_cgo.go`: verify `WebKitURISchemeRequest` handling and memory ownership semantics on GTK4/WebKit 6.
181+- `pkg/webkit/devtools_cgo.go`: validate inspector APIs on WebKit 6.
182+- `pkg/webkit/loop_cgo.go`: update any deprecated main-loop helpers.
183+- `internal/config/*`: add `RenderingConfig` and `RenderingMode` with env/flag wiring.
184+- `internal/cli/browse.go`: add `--rendering-mode`, `--disable-gpu`, `--debug-gpu`.
185+- `services/*`: plumb rendering config into WebView creation.
186+
187+## Phase 2: Task Planning Approach
188+*This section describes what the /tasks command will do - DO NOT execute during /plan*
189+
190+**Task Generation Strategy**:
191+- Analyze each file in pkg/webkit for required changes
192+- Create migration task for each file
193+- Order by dependency (build files first, then core, then features)
194+- Add verification tasks after each major component
195+
196+**Ordering Strategy**:
197+1. Build configuration (pkg-config, CGO flags)
198+2. Core types and structures
199+3. Window and WebView creation
200+4. Feature-specific files (zoom, script, keyboard)
201+5. Integration testing
202+6. Performance verification
203+
204+**Estimated Output**: 20-25 migration tasks in tasks.md
205+
206+**IMPORTANT**: This phase is executed by the /tasks command, NOT by /plan
207+
208+## Phase 3+: Future Implementation
209+*These phases are beyond the scope of the /plan command*
210+
211+**Phase 3**: Task execution (/tasks command creates tasks.md)
212+**Phase 4**: Implementation (execute migration tasks)
213+**Phase 5**: Validation (verify GPU acceleration, run performance tests)
214+
215+## Risks & Mitigations
216+- Driver variability: prefer ON_DEMAND policy by default; expose override flags; add CPU fallback on init failures.
217+- API drift: adhere to contracts in `contracts/webkit-api.yaml`; gate changes behind compile tags where helpful.
218+- Performance regressions: provide quickstart validation and capture metrics; keep rollback point per task.
219+
220+## Complexity Tracking
221+*Fill ONLY if Constitution Check has violations that must be justified*
222+
223+| Violation | Why Needed | Simpler Alternative Rejected Because |
224+|-----------|------------|-------------------------------------|
225+| No TDD for migration | User-specified approach | TDD would slow down API exploration during migration |
226+
227+## Progress Tracking
228+*This checklist is updated during execution flow*
229+
230+**Phase Status**:
231+- [x] Phase 0: Research complete (/plan command)
232+- [x] Phase 1: Design complete (/plan command)
233+- [x] Phase 2: Task planning complete (/plan command - describe approach only)
234+- [x] Phase 3: Tasks generated (/tasks command)
235+- [ ] Phase 4: Implementation in progress
236+- [ ] Phase 5: Validation passed
237+
238+**Gate Status**:
239+- [x] Initial Constitution Check: PASS
240+- [x] Post-Design Constitution Check: PASS (performance targets pending)
241+- [ ] All NEEDS CLARIFICATION resolved (see spec FR-008, FR-011, FR-012)
242+- [x] Complexity deviations documented
243+
244+## Next Actions
245+- Verify and finalize build flags and includes across `pkg/webkit/*_cgo.go`.
246+- Align `internal/config` and `internal/cli` with `RenderingConfig` and flags.
247+- Complete event/gesture migration in `keyboard_cgo.go` and related handlers.
248+- Add hardware acceleration policy mapping during WebView initialization.
249+
250+---
251+*Based on Constitution v1.0.0 - See `/memory/constitution.md`*
A specs/004-migrate-to-webkitgtk/quickstart.md +194 -0
@@ -0,0 +1,194 @@
1+# Quickstart: WebKitGTK 6.0 Migration
2+
3+## Prerequisites
4+
5+### System Requirements
6+```bash
7+# Check GTK4 installation
8+pkg-config --modversion gtk4 # Should be >= 4.0
9+
10+# Check WebKitGTK 6.0
11+pkg-config --modversion webkitgtk-6.0 # Should be >= 2.40
12+
13+# Check Vulkan support (optional but recommended)
14+vulkaninfo | grep "Vulkan Instance Version"
15+```
16+
17+### Install Dependencies (Arch Linux)
18+```bash
19+sudo pacman -S webkit2gtk-6.0 gtk4 vulkan-tools
20+```
21+
22+### Install Dependencies (Ubuntu/Debian)
23+```bash
24+sudo apt install libwebkitgtk-6.0-dev libgtk-4-dev vulkan-tools
25+```
26+
27+## Build and Test
28+
29+### 1. Switch to Feature Branch
30+```bash
31+git checkout 004-migrate-to-webkitgtk
32+```
33+
34+### 2. Build the Browser
35+```bash
36+make clean
37+make build
38+```
39+
40+### 3. Verify GPU Rendering
41+
42+#### Test Auto-Detection (Default)
43+```bash
44+# Should auto-detect GPU availability
45+./dumber browse https://webglsamples.org/aquarium/aquarium.html
46+```
47+
48+#### Force GPU Mode
49+```bash
50+# Force GPU acceleration
51+./dumber browse --rendering-mode=gpu https://webglsamples.org/aquarium/aquarium.html
52+
53+# Or using environment variable
54+DUMBER_RENDERING_MODE=gpu ./dumber browse https://example.com
55+```
56+
57+#### Force CPU Mode
58+```bash
59+# Force software rendering
60+./dumber browse --rendering-mode=cpu https://example.com
61+
62+# Or shorthand
63+./dumber browse --disable-gpu https://example.com
64+```
65+
66+#### Debug GPU Compositing
67+```bash
68+# Show GPU layer borders
69+./dumber browse --debug-gpu https://example.com
70+```
71+
72+### 4. Check Rendering Status
73+```bash
74+# While browser is running, check status
75+curl http://localhost:8080/api/rendering/status
76+```
77+
78+Expected response:
79+```json
80+{
81+ "mode": "auto",
82+ "gpu_active": true,
83+ "gpu_available": true,
84+ "vulkan_enabled": true,
85+ "errors": [],
86+ "performance": {
87+ "fps": 60,
88+ "cpu_usage_percent": 15.2,
89+ "gpu_usage_percent": 45.8,
90+ "memory_mb": 256
91+ }
92+}
93+```
94+
95+## Performance Testing
96+
97+### 1. Benchmark Page Load
98+```bash
99+# Measure with GPU
100+time ./dumber browse --rendering-mode=gpu https://browserbench.org/MotionMark/
101+
102+# Measure with CPU
103+time ./dumber browse --rendering-mode=cpu https://browserbench.org/MotionMark/
104+```
105+
106+### 2. Monitor Resource Usage
107+```bash
108+# In another terminal, monitor GPU
109+nvidia-smi -l 1 # For NVIDIA
110+radeontop # For AMD
111+
112+# Monitor CPU
113+htop
114+```
115+
116+### 3. Test WebGL Performance
117+```bash
118+# High-demand WebGL test
119+./dumber browse https://webglsamples.org/dynamic-cubemap/dynamic-cubemap.html
120+
121+# Check FPS counter in browser
122+```
123+
124+## Troubleshooting
125+
126+### GPU Not Detected
127+```bash
128+# Check Vulkan drivers
129+vulkaninfo
130+
131+# Check OpenGL
132+glxinfo | grep "OpenGL renderer"
133+
134+# Force CPU mode if GPU issues
135+./dumber browse --disable-gpu
136+```
137+
138+### Performance Issues
139+```bash
140+# Enable debug indicators
141+./dumber browse --debug-gpu https://example.com
142+
143+# Check for compositing issues (red borders = repaints)
144+```
145+
146+### Build Errors
147+```bash
148+# Ensure correct pkg-config
149+pkg-config --libs webkitgtk-6.0 gtk4
150+
151+# Clean and rebuild
152+make clean
153+make build
154+```
155+
156+## Validation Checklist
157+
158+- [ ] Browser builds successfully with WebKitGTK 6.0
159+- [ ] Browser starts in < 500ms
160+- [ ] GPU acceleration activates when available
161+- [ ] CPU fallback works when GPU unavailable
162+- [ ] WebGL content renders correctly
163+- [ ] No visual artifacts or rendering issues
164+- [ ] History and bookmarks preserved
165+- [ ] All existing features work
166+- [ ] Performance improved with GPU enabled
167+- [ ] Resource usage within acceptable limits
168+
169+## Migration Verification
170+
171+### Check API Usage
172+```bash
173+# Verify new GTK4/WebKit6 symbols
174+ldd ./dumber | grep -E "(gtk-4|webkit.*6)"
175+
176+# Should see:
177+# libgtk-4.so.1
178+# libwebkitgtk-6.0.so.4
179+```
180+
181+### Test Key Features
182+1. **Navigation**: URLs load correctly
183+2. **History**: Previous data accessible
184+3. **Zoom**: Ctrl+Plus/Minus works
185+4. **DevTools**: F12 opens inspector
186+5. **Dmenu**: Integration still works
187+
188+## Report Issues
189+
190+If you encounter issues:
191+1. Note the rendering mode used
192+2. Check `/tmp/dumber.log` for errors
193+3. Run with `--debug-gpu` for visual debugging
194+4. Report with output of `vulkaninfo` and `glxinfo`⏎̸
@@ -0,0 +1,194 @@
1+# Quickstart: WebKitGTK 6.0 Migration
2+
3+## Prerequisites
4+
5+### System Requirements
6+```bash
7+# Check GTK4 installation
8+pkg-config --modversion gtk4 # Should be >= 4.0
9+
10+# Check WebKitGTK 6.0
11+pkg-config --modversion webkitgtk-6.0 # Should be >= 2.40
12+
13+# Check Vulkan support (optional but recommended)
14+vulkaninfo | grep "Vulkan Instance Version"
15+```
16+
17+### Install Dependencies (Arch Linux)
18+```bash
19+sudo pacman -S webkit2gtk-6.0 gtk4 vulkan-tools
20+```
21+
22+### Install Dependencies (Ubuntu/Debian)
23+```bash
24+sudo apt install libwebkitgtk-6.0-dev libgtk-4-dev vulkan-tools
25+```
26+
27+## Build and Test
28+
29+### 1. Switch to Feature Branch
30+```bash
31+git checkout 004-migrate-to-webkitgtk
32+```
33+
34+### 2. Build the Browser
35+```bash
36+make clean
37+make build
38+```
39+
40+### 3. Verify GPU Rendering
41+
42+#### Test Auto-Detection (Default)
43+```bash
44+# Should auto-detect GPU availability
45+./dumber browse https://webglsamples.org/aquarium/aquarium.html
46+```
47+
48+#### Force GPU Mode
49+```bash
50+# Force GPU acceleration
51+./dumber browse --rendering-mode=gpu https://webglsamples.org/aquarium/aquarium.html
52+
53+# Or using environment variable
54+DUMBER_RENDERING_MODE=gpu ./dumber browse https://example.com
55+```
56+
57+#### Force CPU Mode
58+```bash
59+# Force software rendering
60+./dumber browse --rendering-mode=cpu https://example.com
61+
62+# Or shorthand
63+./dumber browse --disable-gpu https://example.com
64+```
65+
66+#### Debug GPU Compositing
67+```bash
68+# Show GPU layer borders
69+./dumber browse --debug-gpu https://example.com
70+```
71+
72+### 4. Check Rendering Status
73+```bash
74+# While browser is running, check status
75+curl http://localhost:8080/api/rendering/status
76+```
77+
78+Expected response:
79+```json
80+{
81+ "mode": "auto",
82+ "gpu_active": true,
83+ "gpu_available": true,
84+ "vulkan_enabled": true,
85+ "errors": [],
86+ "performance": {
87+ "fps": 60,
88+ "cpu_usage_percent": 15.2,
89+ "gpu_usage_percent": 45.8,
90+ "memory_mb": 256
91+ }
92+}
93+```
94+
95+## Performance Testing
96+
97+### 1. Benchmark Page Load
98+```bash
99+# Measure with GPU
100+time ./dumber browse --rendering-mode=gpu https://browserbench.org/MotionMark/
101+
102+# Measure with CPU
103+time ./dumber browse --rendering-mode=cpu https://browserbench.org/MotionMark/
104+```
105+
106+### 2. Monitor Resource Usage
107+```bash
108+# In another terminal, monitor GPU
109+nvidia-smi -l 1 # For NVIDIA
110+radeontop # For AMD
111+
112+# Monitor CPU
113+htop
114+```
115+
116+### 3. Test WebGL Performance
117+```bash
118+# High-demand WebGL test
119+./dumber browse https://webglsamples.org/dynamic-cubemap/dynamic-cubemap.html
120+
121+# Check FPS counter in browser
122+```
123+
124+## Troubleshooting
125+
126+### GPU Not Detected
127+```bash
128+# Check Vulkan drivers
129+vulkaninfo
130+
131+# Check OpenGL
132+glxinfo | grep "OpenGL renderer"
133+
134+# Force CPU mode if GPU issues
135+./dumber browse --disable-gpu
136+```
137+
138+### Performance Issues
139+```bash
140+# Enable debug indicators
141+./dumber browse --debug-gpu https://example.com
142+
143+# Check for compositing issues (red borders = repaints)
144+```
145+
146+### Build Errors
147+```bash
148+# Ensure correct pkg-config
149+pkg-config --libs webkitgtk-6.0 gtk4
150+
151+# Clean and rebuild
152+make clean
153+make build
154+```
155+
156+## Validation Checklist
157+
158+- [ ] Browser builds successfully with WebKitGTK 6.0
159+- [ ] Browser starts in < 500ms
160+- [ ] GPU acceleration activates when available
161+- [ ] CPU fallback works when GPU unavailable
162+- [ ] WebGL content renders correctly
163+- [ ] No visual artifacts or rendering issues
164+- [ ] History and bookmarks preserved
165+- [ ] All existing features work
166+- [ ] Performance improved with GPU enabled
167+- [ ] Resource usage within acceptable limits
168+
169+## Migration Verification
170+
171+### Check API Usage
172+```bash
173+# Verify new GTK4/WebKit6 symbols
174+ldd ./dumber | grep -E "(gtk-4|webkit.*6)"
175+
176+# Should see:
177+# libgtk-4.so.1
178+# libwebkitgtk-6.0.so.4
179+```
180+
181+### Test Key Features
182+1. **Navigation**: URLs load correctly
183+2. **History**: Previous data accessible
184+3. **Zoom**: Ctrl+Plus/Minus works
185+4. **DevTools**: F12 opens inspector
186+5. **Dmenu**: Integration still works
187+
188+## Report Issues
189+
190+If you encounter issues:
191+1. Note the rendering mode used
192+2. Check `/tmp/dumber.log` for errors
193+3. Run with `--debug-gpu` for visual debugging
194+4. Report with output of `vulkaninfo` and `glxinfo`⏎̸
A specs/004-migrate-to-webkitgtk/research.md +115 -0
@@ -0,0 +1,115 @@
1+# Research: WebKitGTK 6.0 Migration
2+
3+## API Migration Research
4+
5+### Decision: WebKitGTK 6.0 with GTK4
6+**Rationale**:
7+- GTK4 provides native Vulkan renderer support
8+- WebKitGTK 6.0 is the only actively maintained version for GTK4
9+- Enables hardware-accelerated rendering via modern GPU APIs
10+- Better Wayland support and performance
11+
12+**Alternatives considered**:
13+- Staying on WebKit2GTK 4.0: No Vulkan support, limited to OpenGL
14+- Using WebKit2GTK 4.1: Still GTK3, no Vulkan renderer
15+- Custom Vulkan implementation: Too complex, maintenance burden
16+
17+### GTK3 to GTK4 Migration Patterns
18+
19+**Key Changes**:
20+1. **Event handling**: GdkEvent parameter removed from signals
21+2. **Widget construction**: Direct use of g_object_new() instead of specific constructors
22+3. **Drawing**: No more GtkDrawingArea draw signal, use GtkSnapshot
23+4. **CSS**: GTK4 uses standard CSS, removed custom properties
24+5. **Layout**: New layout managers replace containers
25+
26+**CGO Binding Updates**:
27+- pkg-config: `webkit2gtk-4.0` → `webkitgtk-6.0`
28+- pkg-config: `gtk+-3.0` → `gtk4`
29+- Header: `gtk/gtk.h` remains same
30+- Header: `webkit2/webkit2.h` → `webkit/webkit.h`
31+
32+### Vulkan Renderer Configuration
33+
34+**Decision: Hardware Acceleration Policy**
35+**Rationale**: Provides user control over GPU vs CPU rendering
36+
37+**Configuration Options**:
38+1. `WEBKIT_HARDWARE_ACCELERATION_POLICY_ALWAYS`: Force GPU
39+2. `WEBKIT_HARDWARE_ACCELERATION_POLICY_NEVER`: Force CPU
40+3. `WEBKIT_HARDWARE_ACCELERATION_POLICY_ON_DEMAND`: Auto-detect (default)
41+
42+**New Settings in WebKitGTK 6.0**:
43+- `webkit_settings_set_hardware_acceleration_policy()`
44+- `webkit_settings_set_enable_2d_canvas_acceleration()`
45+- `webkit_settings_set_enable_webgl()`
46+- `webkit_settings_set_draw_compositing_indicators()` (debugging)
47+
48+### Deprecated APIs to Replace
49+
50+| WebKit2GTK 4.0 API | WebKitGTK 6.0 Replacement |
51+|-------------------|---------------------------|
52+| `webkit_web_view_new_with_context()` | `g_object_new()` with properties |
53+| `webkit_web_context_set_sandbox_enabled()` | Always enabled, use `add_path_to_sandbox()` |
54+| `WebKitWebContext` | `WebKitNetworkSession` for networking |
55+| `webkit_web_view_run_javascript()` | Same, but async only |
56+| Process swap property | Always enabled |
57+
58+### Build System Changes
59+
60+**pkg-config flags**:
61+```bash
62+# Old (WebKit2GTK 4.0)
63+pkg-config --cflags --libs webkit2gtk-4.0 gtk+-3.0 javascriptcoregtk-4.0
64+
65+# New (WebKitGTK 6.0)
66+pkg-config --cflags --libs webkitgtk-6.0 gtk4 javascriptcoregtk-6.0
67+```
68+
69+### Performance Considerations
70+
71+**GPU Rendering Benefits**:
72+- Vulkan backend via GTK4's unified GPU renderer
73+- Hardware-accelerated 2D canvas
74+- Improved WebGL performance
75+- Reduced CPU usage for compositing
76+- Better multi-threaded rendering
77+
78+**Fallback Strategy**:
79+- Detect GPU capabilities at runtime
80+- Use `ON_DEMAND` policy for automatic selection
81+- Provide config flag for manual override
82+- Monitor performance metrics
83+
84+### Global Rendering Configuration
85+
86+**Decision: Add RenderingMode to config**
87+**Rationale**: User control over CPU/GPU rendering with smart defaults
88+
89+**Implementation**:
90+```go
91+type RenderingMode string
92+
93+const (
94+ RenderingModeAuto RenderingMode = "auto" // Default, detect at runtime
95+ RenderingModeGPU RenderingMode = "gpu" // Force GPU acceleration
96+ RenderingModeCPU RenderingMode = "cpu" // Force software rendering
97+)
98+```
99+
100+**Config Integration**:
101+- Add to `internal/config/config.go`
102+- Default to "auto" (GPU if available)
103+- CLI flag: `--rendering-mode=[auto|gpu|cpu]`
104+- Environment variable: `DUMBER_RENDERING_MODE`
105+
106+## Summary
107+
108+The migration to WebKitGTK 6.0 requires:
109+1. Complete GTK3 → GTK4 widget/event migration
110+2. Update all WebKit API calls to new versions
111+3. Implement hardware acceleration configuration
112+4. Add global rendering mode configuration
113+5. Test Vulkan renderer activation and fallback
114+
115+All technical questions have been resolved through research.⏎̸
@@ -0,0 +1,115 @@
1+# Research: WebKitGTK 6.0 Migration
2+
3+## API Migration Research
4+
5+### Decision: WebKitGTK 6.0 with GTK4
6+**Rationale**:
7+- GTK4 provides native Vulkan renderer support
8+- WebKitGTK 6.0 is the only actively maintained version for GTK4
9+- Enables hardware-accelerated rendering via modern GPU APIs
10+- Better Wayland support and performance
11+
12+**Alternatives considered**:
13+- Staying on WebKit2GTK 4.0: No Vulkan support, limited to OpenGL
14+- Using WebKit2GTK 4.1: Still GTK3, no Vulkan renderer
15+- Custom Vulkan implementation: Too complex, maintenance burden
16+
17+### GTK3 to GTK4 Migration Patterns
18+
19+**Key Changes**:
20+1. **Event handling**: GdkEvent parameter removed from signals
21+2. **Widget construction**: Direct use of g_object_new() instead of specific constructors
22+3. **Drawing**: No more GtkDrawingArea draw signal, use GtkSnapshot
23+4. **CSS**: GTK4 uses standard CSS, removed custom properties
24+5. **Layout**: New layout managers replace containers
25+
26+**CGO Binding Updates**:
27+- pkg-config: `webkit2gtk-4.0` → `webkitgtk-6.0`
28+- pkg-config: `gtk+-3.0` → `gtk4`
29+- Header: `gtk/gtk.h` remains same
30+- Header: `webkit2/webkit2.h` → `webkit/webkit.h`
31+
32+### Vulkan Renderer Configuration
33+
34+**Decision: Hardware Acceleration Policy**
35+**Rationale**: Provides user control over GPU vs CPU rendering
36+
37+**Configuration Options**:
38+1. `WEBKIT_HARDWARE_ACCELERATION_POLICY_ALWAYS`: Force GPU
39+2. `WEBKIT_HARDWARE_ACCELERATION_POLICY_NEVER`: Force CPU
40+3. `WEBKIT_HARDWARE_ACCELERATION_POLICY_ON_DEMAND`: Auto-detect (default)
41+
42+**New Settings in WebKitGTK 6.0**:
43+- `webkit_settings_set_hardware_acceleration_policy()`
44+- `webkit_settings_set_enable_2d_canvas_acceleration()`
45+- `webkit_settings_set_enable_webgl()`
46+- `webkit_settings_set_draw_compositing_indicators()` (debugging)
47+
48+### Deprecated APIs to Replace
49+
50+| WebKit2GTK 4.0 API | WebKitGTK 6.0 Replacement |
51+|-------------------|---------------------------|
52+| `webkit_web_view_new_with_context()` | `g_object_new()` with properties |
53+| `webkit_web_context_set_sandbox_enabled()` | Always enabled, use `add_path_to_sandbox()` |
54+| `WebKitWebContext` | `WebKitNetworkSession` for networking |
55+| `webkit_web_view_run_javascript()` | Same, but async only |
56+| Process swap property | Always enabled |
57+
58+### Build System Changes
59+
60+**pkg-config flags**:
61+```bash
62+# Old (WebKit2GTK 4.0)
63+pkg-config --cflags --libs webkit2gtk-4.0 gtk+-3.0 javascriptcoregtk-4.0
64+
65+# New (WebKitGTK 6.0)
66+pkg-config --cflags --libs webkitgtk-6.0 gtk4 javascriptcoregtk-6.0
67+```
68+
69+### Performance Considerations
70+
71+**GPU Rendering Benefits**:
72+- Vulkan backend via GTK4's unified GPU renderer
73+- Hardware-accelerated 2D canvas
74+- Improved WebGL performance
75+- Reduced CPU usage for compositing
76+- Better multi-threaded rendering
77+
78+**Fallback Strategy**:
79+- Detect GPU capabilities at runtime
80+- Use `ON_DEMAND` policy for automatic selection
81+- Provide config flag for manual override
82+- Monitor performance metrics
83+
84+### Global Rendering Configuration
85+
86+**Decision: Add RenderingMode to config**
87+**Rationale**: User control over CPU/GPU rendering with smart defaults
88+
89+**Implementation**:
90+```go
91+type RenderingMode string
92+
93+const (
94+ RenderingModeAuto RenderingMode = "auto" // Default, detect at runtime
95+ RenderingModeGPU RenderingMode = "gpu" // Force GPU acceleration
96+ RenderingModeCPU RenderingMode = "cpu" // Force software rendering
97+)
98+```
99+
100+**Config Integration**:
101+- Add to `internal/config/config.go`
102+- Default to "auto" (GPU if available)
103+- CLI flag: `--rendering-mode=[auto|gpu|cpu]`
104+- Environment variable: `DUMBER_RENDERING_MODE`
105+
106+## Summary
107+
108+The migration to WebKitGTK 6.0 requires:
109+1. Complete GTK3 → GTK4 widget/event migration
110+2. Update all WebKit API calls to new versions
111+3. Implement hardware acceleration configuration
112+4. Add global rendering mode configuration
113+5. Test Vulkan renderer activation and fallback
114+
115+All technical questions have been resolved through research.⏎̸
A specs/004-migrate-to-webkitgtk/spec.md +111 -0
@@ -0,0 +1,111 @@
1+# Feature Specification: Migrate to WebKitGTK 6.0
2+
3+**Feature Branch**: `004-migrate-to-webkitgtk`
4+**Created**: 2025-01-21
5+**Status**: Draft
6+**Input**: User description: "Migrate To webkitgtk-6.0, I Want to migrate the entire project to webkitgtk6 in order to be able to enable GPU rendering through vulkan layer"
7+
8+## Execution Flow (main)
9+```
10+1. Parse user description from Input
11+ → Migrate browser to WebKitGTK 6.0 for Vulkan GPU rendering
12+2. Extract key concepts from description
13+ → Identified: migration, WebKitGTK 6.0, GPU rendering, Vulkan support
14+3. For each unclear aspect:
15+ → Performance targets marked for clarification
16+ → Migration timeline marked for clarification
17+4. Fill User Scenarios & Testing section
18+ → User flow: Browser continues to work with improved GPU performance
19+5. Generate Functional Requirements
20+ → Each requirement is testable
21+ → Migration scope defined
22+6. Identify Key Entities
23+ → Browser engine, rendering pipeline, GPU acceleration
24+7. Run Review Checklist
25+ → WARN "Spec has uncertainties around performance targets"
26+8. Return: SUCCESS (spec ready for planning)
27+```
28+
29+---
30+
31+## ⚡ Quick Guidelines
32+- ✅ Focus on WHAT users need and WHY
33+- ❌ Avoid HOW to implement (no tech stack, APIs, code structure)
34+- 👥 Written for business stakeholders, not developers
35+
36+---
37+
38+## User Scenarios & Testing
39+
40+### Primary User Story
41+As a user of the dumber browser, I want the browser to utilize modern GPU acceleration capabilities through Vulkan rendering, resulting in smoother scrolling, faster page rendering, and improved performance for graphics-intensive web content, while maintaining all existing browser functionality.
42+
43+### Acceptance Scenarios
44+1. **Given** a user opens the browser, **When** they navigate to any website, **Then** the browser renders pages using GPU acceleration when available
45+2. **Given** a user visits a graphics-intensive website (e.g., with 3D content, animations), **When** the page loads, **Then** GPU rendering provides smooth performance without visual artifacts
46+3. **Given** a system with Vulkan-capable GPU, **When** the browser starts, **Then** it automatically detects and enables GPU acceleration
47+4. **Given** a system without Vulkan support, **When** the browser starts, **Then** it gracefully falls back to software rendering
48+5. **Given** the browser is using GPU acceleration, **When** monitoring system resources, **Then** CPU usage is reduced compared to software rendering
49+
50+### Edge Cases
51+- What happens when GPU drivers are outdated or incompatible?
52+- How does system handle GPU driver crashes during browsing?
53+- What happens on systems with multiple GPUs (integrated + discrete)?
54+- How does browser perform on low-end GPUs vs high-end GPUs?
55+- What happens when switching between GPU and software rendering mid-session?
56+
57+## Requirements
58+
59+### Functional Requirements
60+- **FR-001**: System MUST maintain all existing browser functionality after migration
61+- **FR-002**: System MUST support GPU-accelerated rendering when Vulkan-capable hardware is available
62+- **FR-003**: System MUST provide fallback to software rendering when GPU acceleration is unavailable
63+- **FR-004**: System MUST maintain backward compatibility with existing user data (bookmarks, history, settings)
64+- **FR-005**: System MUST preserve current browser interface and user experience
65+- **FR-006**: System MUST detect GPU capabilities at startup and choose appropriate rendering path
66+- **FR-007**: System MUST handle GPU driver failures gracefully without crashing
67+- **FR-008**: Browser performance MUST improve by [NEEDS CLARIFICATION: specific performance target not specified - 20%? 50%? What metrics?]
68+- **FR-009**: System MUST support common web standards (HTML5, CSS3, JavaScript) without regression
69+- **FR-010**: System MUST maintain support for all previously working websites
70+- **FR-011**: GPU memory usage MUST remain within [NEEDS CLARIFICATION: acceptable limits not specified]
71+- **FR-012**: Migration MUST be completed within [NEEDS CLARIFICATION: timeline not specified]
72+
73+### Key Entities
74+- **Browser Engine**: Core rendering engine that processes and displays web content
75+- **GPU Rendering Pipeline**: System component responsible for hardware-accelerated graphics
76+- **Fallback Renderer**: Software-based rendering system for non-GPU scenarios
77+- **Performance Monitor**: Component tracking rendering performance and resource usage
78+- **Compatibility Layer**: System ensuring existing functionality works with new rendering engine
79+
80+---
81+
82+## Review & Acceptance Checklist
83+*GATE: Automated checks run during main() execution*
84+
85+### Content Quality
86+- [x] No implementation details (languages, frameworks, APIs)
87+- [x] Focused on user value and business needs
88+- [x] Written for non-technical stakeholders
89+- [x] All mandatory sections completed
90+
91+### Requirement Completeness
92+- [ ] No [NEEDS CLARIFICATION] markers remain
93+- [x] Requirements are testable and unambiguous (except marked items)
94+- [ ] Success criteria are measurable (performance targets needed)
95+- [x] Scope is clearly bounded
96+- [x] Dependencies and assumptions identified
97+
98+---
99+
100+## Execution Status
101+*Updated by main() during processing*
102+
103+- [x] User description parsed
104+- [x] Key concepts extracted
105+- [x] Ambiguities marked
106+- [x] User scenarios defined
107+- [x] Requirements generated
108+- [x] Entities identified
109+- [ ] Review checklist passed (has clarifications needed)
110+
111+---⏎̸
@@ -0,0 +1,111 @@
1+# Feature Specification: Migrate to WebKitGTK 6.0
2+
3+**Feature Branch**: `004-migrate-to-webkitgtk`
4+**Created**: 2025-01-21
5+**Status**: Draft
6+**Input**: User description: "Migrate To webkitgtk-6.0, I Want to migrate the entire project to webkitgtk6 in order to be able to enable GPU rendering through vulkan layer"
7+
8+## Execution Flow (main)
9+```
10+1. Parse user description from Input
11+ → Migrate browser to WebKitGTK 6.0 for Vulkan GPU rendering
12+2. Extract key concepts from description
13+ → Identified: migration, WebKitGTK 6.0, GPU rendering, Vulkan support
14+3. For each unclear aspect:
15+ → Performance targets marked for clarification
16+ → Migration timeline marked for clarification
17+4. Fill User Scenarios & Testing section
18+ → User flow: Browser continues to work with improved GPU performance
19+5. Generate Functional Requirements
20+ → Each requirement is testable
21+ → Migration scope defined
22+6. Identify Key Entities
23+ → Browser engine, rendering pipeline, GPU acceleration
24+7. Run Review Checklist
25+ → WARN "Spec has uncertainties around performance targets"
26+8. Return: SUCCESS (spec ready for planning)
27+```
28+
29+---
30+
31+## ⚡ Quick Guidelines
32+- ✅ Focus on WHAT users need and WHY
33+- ❌ Avoid HOW to implement (no tech stack, APIs, code structure)
34+- 👥 Written for business stakeholders, not developers
35+
36+---
37+
38+## User Scenarios & Testing
39+
40+### Primary User Story
41+As a user of the dumber browser, I want the browser to utilize modern GPU acceleration capabilities through Vulkan rendering, resulting in smoother scrolling, faster page rendering, and improved performance for graphics-intensive web content, while maintaining all existing browser functionality.
42+
43+### Acceptance Scenarios
44+1. **Given** a user opens the browser, **When** they navigate to any website, **Then** the browser renders pages using GPU acceleration when available
45+2. **Given** a user visits a graphics-intensive website (e.g., with 3D content, animations), **When** the page loads, **Then** GPU rendering provides smooth performance without visual artifacts
46+3. **Given** a system with Vulkan-capable GPU, **When** the browser starts, **Then** it automatically detects and enables GPU acceleration
47+4. **Given** a system without Vulkan support, **When** the browser starts, **Then** it gracefully falls back to software rendering
48+5. **Given** the browser is using GPU acceleration, **When** monitoring system resources, **Then** CPU usage is reduced compared to software rendering
49+
50+### Edge Cases
51+- What happens when GPU drivers are outdated or incompatible?
52+- How does system handle GPU driver crashes during browsing?
53+- What happens on systems with multiple GPUs (integrated + discrete)?
54+- How does browser perform on low-end GPUs vs high-end GPUs?
55+- What happens when switching between GPU and software rendering mid-session?
56+
57+## Requirements
58+
59+### Functional Requirements
60+- **FR-001**: System MUST maintain all existing browser functionality after migration
61+- **FR-002**: System MUST support GPU-accelerated rendering when Vulkan-capable hardware is available
62+- **FR-003**: System MUST provide fallback to software rendering when GPU acceleration is unavailable
63+- **FR-004**: System MUST maintain backward compatibility with existing user data (bookmarks, history, settings)
64+- **FR-005**: System MUST preserve current browser interface and user experience
65+- **FR-006**: System MUST detect GPU capabilities at startup and choose appropriate rendering path
66+- **FR-007**: System MUST handle GPU driver failures gracefully without crashing
67+- **FR-008**: Browser performance MUST improve by [NEEDS CLARIFICATION: specific performance target not specified - 20%? 50%? What metrics?]
68+- **FR-009**: System MUST support common web standards (HTML5, CSS3, JavaScript) without regression
69+- **FR-010**: System MUST maintain support for all previously working websites
70+- **FR-011**: GPU memory usage MUST remain within [NEEDS CLARIFICATION: acceptable limits not specified]
71+- **FR-012**: Migration MUST be completed within [NEEDS CLARIFICATION: timeline not specified]
72+
73+### Key Entities
74+- **Browser Engine**: Core rendering engine that processes and displays web content
75+- **GPU Rendering Pipeline**: System component responsible for hardware-accelerated graphics
76+- **Fallback Renderer**: Software-based rendering system for non-GPU scenarios
77+- **Performance Monitor**: Component tracking rendering performance and resource usage
78+- **Compatibility Layer**: System ensuring existing functionality works with new rendering engine
79+
80+---
81+
82+## Review & Acceptance Checklist
83+*GATE: Automated checks run during main() execution*
84+
85+### Content Quality
86+- [x] No implementation details (languages, frameworks, APIs)
87+- [x] Focused on user value and business needs
88+- [x] Written for non-technical stakeholders
89+- [x] All mandatory sections completed
90+
91+### Requirement Completeness
92+- [ ] No [NEEDS CLARIFICATION] markers remain
93+- [x] Requirements are testable and unambiguous (except marked items)
94+- [ ] Success criteria are measurable (performance targets needed)
95+- [x] Scope is clearly bounded
96+- [x] Dependencies and assumptions identified
97+
98+---
99+
100+## Execution Status
101+*Updated by main() during processing*
102+
103+- [x] User description parsed
104+- [x] Key concepts extracted
105+- [x] Ambiguities marked
106+- [x] User scenarios defined
107+- [x] Requirements generated
108+- [x] Entities identified
109+- [ ] Review checklist passed (has clarifications needed)
110+
111+---⏎̸
A specs/004-migrate-to-webkitgtk/tasks.md +138 -0
@@ -0,0 +1,138 @@
1+# Tasks: Migrate to WebKitGTK 6.0
2+
3+**Input**: Design documents from `/specs/004-migrate-to-webkitgtk/`
4+**Prerequisites**: plan.md (required), research.md, data-model.md, contracts/
5+
6+## Execution Flow (main)
7+```
8+1. Load plan.md from feature directory
9+ → Loaded: WebKitGTK 6.0 migration plan
10+ → Extract: Go 1.25.1, WebKitGTK 6.0, GTK4, no TDD
11+2. Load optional design documents:
12+ → data-model.md: RenderingConfig model
13+ → contracts/: webkit-api.yaml, rendering-config.yaml
14+ → research.md: API migration mappings
15+3. Generate tasks by category:
16+ → Setup: Build config, dependencies
17+ → Core: Migrate webkit files incrementally
18+ → Integration: Rendering config, CLI flags
19+ → Verification: Test GPU acceleration
20+4. Apply task rules:
21+ → Different files = mark [P] for parallel
22+ → Same file = sequential (no [P])
23+ → No TDD per user request
24+5. Number tasks sequentially (T001, T002...)
25+6. Generate dependency graph
26+7. Create parallel execution examples
27+8. Validate task completeness:
28+ → All webkit files migrated?
29+ → Rendering config implemented?
30+ → GPU acceleration verified?
31+9. Return: SUCCESS (tasks ready for execution)
32+```
33+
34+## Format: `[ID] [P?] Description`
35+- **[P]**: Can run in parallel (different files, no dependencies)
36+- Include exact file paths in descriptions
37+
38+## Path Conventions
39+- **Single project**: `pkg/webkit/` for WebKit bindings
40+- **Config**: `internal/config/` for rendering configuration
41+- **CLI**: `internal/cli/` for command-line flags
42+
43+## Phase 3.1: Setup & Build Configuration
44+- [ ] T001 Update pkg-config in all CGO files from webkit2gtk-4.0 to webkitgtk-6.0
45+- [ ] T002 Update GTK pkg-config from gtk+-3.0 to gtk4 in all CGO files
46+- [ ] T003 Update JavaScript pkg-config from javascriptcoregtk-4.0 to javascriptcoregtk-6.0
47+- [ ] T004 Update Makefile build flags for GTK4/WebKitGTK 6.0
48+- [ ] T005 Verify build environment has GTK4 and WebKitGTK 6.0 installed
49+
50+## Phase 3.2: Rendering Configuration
51+- [ ] T006 [P] Add RenderingConfig struct to pkg/webkit/settings.go
52+- [ ] T007 [P] Add RenderingMode constants and validation to internal/config/config.go
53+- [ ] T008 [P] Add --rendering-mode CLI flag to internal/cli/browse.go
54+- [ ] T009 [P] Add DUMBER_RENDERING_MODE environment variable support to internal/config/config.go
55+
56+## Phase 3.3: Core WebKit Migration - Build Files
57+- [ ] T010 [P] Migrate pkg/webkit/build_flags_cgo.go to WebKitGTK 6.0 headers
58+- [ ] T011 [P] Migrate pkg/webkit/build_cgo_stub.go build tags for GTK4
59+- [ ] T012 [P] Update pkg/webkit/build_flags_stub.go for new build configuration
60+
61+## Phase 3.4: Core WebKit Migration - Main Components
62+- [ ] T013 Migrate pkg/webkit/webview_cgo.go - Update WebView creation to use g_object_new instead of webkit_web_view_new_with_context
63+- [ ] T014 Migrate pkg/webkit/webview_cgo.go - Replace WebKitWebContext with WebKitNetworkSession
64+- [ ] T015 Migrate pkg/webkit/webview_cgo.go - Add hardware acceleration policy settings
65+- [ ] T016 Migrate pkg/webkit/window_cgo.go - Update gtk_window_new() without GTK_WINDOW_TOPLEVEL
66+- [ ] T017 Migrate pkg/webkit/window_cgo.go - Replace gtk_container_add with gtk_window_set_child
67+- [ ] T018 Migrate pkg/webkit/window_cgo.go - Update gtk_widget_destroy to gtk_window_destroy
68+
69+## Phase 3.5: Core WebKit Migration - Event Handling
70+- [ ] T019 Migrate pkg/webkit/keyboard_cgo.go - Replace GdkEventKey with GtkEventController
71+- [ ] T020 Update pkg/webkit/keyboard_cgo.go - Migrate key-press-event to key-pressed signal
72+- [ ] T021 Migrate button-press-event to GtkGestureClick in pkg/webkit/webview_cgo.go
73+
74+## Phase 3.6: Feature Migration
75+- [ ] T022 [P] Update pkg/webkit/script_cgo.go for GTK4 compatibility
76+- [ ] T023 [P] Update pkg/webkit/zoom_cgo.go for GTK4 compatibility
77+- [ ] T024 [P] Update pkg/webkit/devtools_cgo.go for GTK4 compatibility
78+- [ ] T025 [P] Update pkg/webkit/loop_cgo.go for GTK4 main loop
79+- [ ] T026 [P] Update pkg/webkit/scheme_cgo.go for new URI scheme handling
80+
81+## Phase 3.7: Integration & Services
82+- [ ] T027 Update services/browser_service.go to pass RenderingConfig to WebView
83+- [ ] T028 Update services/config_service.go to handle rendering mode configuration
84+- [ ] T029 Add GPU detection logic to determine auto mode in pkg/webkit/webview.go
85+
86+## Phase 3.8: Testing & Verification
87+- [ ] T030 Create test for GPU acceleration detection in tests/integration/test_gpu_rendering.go
88+- [ ] T031 Create test for CPU fallback in tests/integration/test_cpu_fallback.go
89+- [ ] T032 Update existing integration tests for GTK4 compatibility
90+- [ ] T033 Manual test: Verify browser starts with GTK4/WebKitGTK 6.0
91+- [ ] T034 Manual test: Verify GPU acceleration with WebGL content
92+- [ ] T035 Manual test: Verify CPU fallback with --rendering-mode=cpu
93+
94+## Phase 3.9: Documentation & Cleanup
95+- [ ] T036 [P] Update README.md with new build requirements
96+- [ ] T037 [P] Document rendering mode configuration in docs/
97+- [ ] T038 Remove deprecated WebKit2GTK 4.0 compatibility code
98+- [ ] T039 Update CLAUDE.md to reflect completed migration
99+
100+## Dependencies
101+- Build config (T001-T005) must complete first
102+- Rendering config (T006-T009) can run in parallel
103+- Core migration (T010-T026) depends on build config
104+- T013-T015 must be sequential (same file)
105+- T016-T018 must be sequential (same file)
106+- Integration (T027-T029) depends on core migration
107+- Testing (T030-T035) depends on all implementation
108+- Documentation (T036-T039) can start anytime
109+
110+## Parallel Example
111+```bash
112+# Launch T006-T009 together (different files):
113+Task: "Add RenderingConfig struct to pkg/webkit/settings.go"
114+Task: "Add RenderingMode constants to internal/config/config.go"
115+Task: "Add --rendering-mode CLI flag to internal/cli/browse.go"
116+Task: "Add DUMBER_RENDERING_MODE env var to internal/config/config.go"
117+
118+# Launch T022-T026 together (independent files):
119+Task: "Update pkg/webkit/script_cgo.go for GTK4"
120+Task: "Update pkg/webkit/zoom_cgo.go for GTK4"
121+Task: "Update pkg/webkit/devtools_cgo.go for GTK4"
122+Task: "Update pkg/webkit/loop_cgo.go for GTK4"
123+Task: "Update pkg/webkit/scheme_cgo.go for URI handling"
124+```
125+
126+## Notes
127+- No TDD per user request - migrate first, test after
128+- Focus on incremental file-by-file migration
129+- Test GPU acceleration after each major component
130+- Commit after each task to track progress
131+- Use quickstart.md for verification steps
132+
133+## Critical Migration Points
134+1. **WebView Creation**: Must update to g_object_new pattern
135+2. **Network Session**: Replace all WebContext usage
136+3. **Event Handling**: GTK4 uses controllers not direct events
137+4. **Container API**: gtk_container_add no longer exists
138+5. **Hardware Acceleration**: Must configure policy correctly⏎̸
@@ -0,0 +1,138 @@
1+# Tasks: Migrate to WebKitGTK 6.0
2+
3+**Input**: Design documents from `/specs/004-migrate-to-webkitgtk/`
4+**Prerequisites**: plan.md (required), research.md, data-model.md, contracts/
5+
6+## Execution Flow (main)
7+```
8+1. Load plan.md from feature directory
9+ → Loaded: WebKitGTK 6.0 migration plan
10+ → Extract: Go 1.25.1, WebKitGTK 6.0, GTK4, no TDD
11+2. Load optional design documents:
12+ → data-model.md: RenderingConfig model
13+ → contracts/: webkit-api.yaml, rendering-config.yaml
14+ → research.md: API migration mappings
15+3. Generate tasks by category:
16+ → Setup: Build config, dependencies
17+ → Core: Migrate webkit files incrementally
18+ → Integration: Rendering config, CLI flags
19+ → Verification: Test GPU acceleration
20+4. Apply task rules:
21+ → Different files = mark [P] for parallel
22+ → Same file = sequential (no [P])
23+ → No TDD per user request
24+5. Number tasks sequentially (T001, T002...)
25+6. Generate dependency graph
26+7. Create parallel execution examples
27+8. Validate task completeness:
28+ → All webkit files migrated?
29+ → Rendering config implemented?
30+ → GPU acceleration verified?
31+9. Return: SUCCESS (tasks ready for execution)
32+```
33+
34+## Format: `[ID] [P?] Description`
35+- **[P]**: Can run in parallel (different files, no dependencies)
36+- Include exact file paths in descriptions
37+
38+## Path Conventions
39+- **Single project**: `pkg/webkit/` for WebKit bindings
40+- **Config**: `internal/config/` for rendering configuration
41+- **CLI**: `internal/cli/` for command-line flags
42+
43+## Phase 3.1: Setup & Build Configuration
44+- [ ] T001 Update pkg-config in all CGO files from webkit2gtk-4.0 to webkitgtk-6.0
45+- [ ] T002 Update GTK pkg-config from gtk+-3.0 to gtk4 in all CGO files
46+- [ ] T003 Update JavaScript pkg-config from javascriptcoregtk-4.0 to javascriptcoregtk-6.0
47+- [ ] T004 Update Makefile build flags for GTK4/WebKitGTK 6.0
48+- [ ] T005 Verify build environment has GTK4 and WebKitGTK 6.0 installed
49+
50+## Phase 3.2: Rendering Configuration
51+- [ ] T006 [P] Add RenderingConfig struct to pkg/webkit/settings.go
52+- [ ] T007 [P] Add RenderingMode constants and validation to internal/config/config.go
53+- [ ] T008 [P] Add --rendering-mode CLI flag to internal/cli/browse.go
54+- [ ] T009 [P] Add DUMBER_RENDERING_MODE environment variable support to internal/config/config.go
55+
56+## Phase 3.3: Core WebKit Migration - Build Files
57+- [ ] T010 [P] Migrate pkg/webkit/build_flags_cgo.go to WebKitGTK 6.0 headers
58+- [ ] T011 [P] Migrate pkg/webkit/build_cgo_stub.go build tags for GTK4
59+- [ ] T012 [P] Update pkg/webkit/build_flags_stub.go for new build configuration
60+
61+## Phase 3.4: Core WebKit Migration - Main Components
62+- [ ] T013 Migrate pkg/webkit/webview_cgo.go - Update WebView creation to use g_object_new instead of webkit_web_view_new_with_context
63+- [ ] T014 Migrate pkg/webkit/webview_cgo.go - Replace WebKitWebContext with WebKitNetworkSession
64+- [ ] T015 Migrate pkg/webkit/webview_cgo.go - Add hardware acceleration policy settings
65+- [ ] T016 Migrate pkg/webkit/window_cgo.go - Update gtk_window_new() without GTK_WINDOW_TOPLEVEL
66+- [ ] T017 Migrate pkg/webkit/window_cgo.go - Replace gtk_container_add with gtk_window_set_child
67+- [ ] T018 Migrate pkg/webkit/window_cgo.go - Update gtk_widget_destroy to gtk_window_destroy
68+
69+## Phase 3.5: Core WebKit Migration - Event Handling
70+- [ ] T019 Migrate pkg/webkit/keyboard_cgo.go - Replace GdkEventKey with GtkEventController
71+- [ ] T020 Update pkg/webkit/keyboard_cgo.go - Migrate key-press-event to key-pressed signal
72+- [ ] T021 Migrate button-press-event to GtkGestureClick in pkg/webkit/webview_cgo.go
73+
74+## Phase 3.6: Feature Migration
75+- [ ] T022 [P] Update pkg/webkit/script_cgo.go for GTK4 compatibility
76+- [ ] T023 [P] Update pkg/webkit/zoom_cgo.go for GTK4 compatibility
77+- [ ] T024 [P] Update pkg/webkit/devtools_cgo.go for GTK4 compatibility
78+- [ ] T025 [P] Update pkg/webkit/loop_cgo.go for GTK4 main loop
79+- [ ] T026 [P] Update pkg/webkit/scheme_cgo.go for new URI scheme handling
80+
81+## Phase 3.7: Integration & Services
82+- [ ] T027 Update services/browser_service.go to pass RenderingConfig to WebView
83+- [ ] T028 Update services/config_service.go to handle rendering mode configuration
84+- [ ] T029 Add GPU detection logic to determine auto mode in pkg/webkit/webview.go
85+
86+## Phase 3.8: Testing & Verification
87+- [ ] T030 Create test for GPU acceleration detection in tests/integration/test_gpu_rendering.go
88+- [ ] T031 Create test for CPU fallback in tests/integration/test_cpu_fallback.go
89+- [ ] T032 Update existing integration tests for GTK4 compatibility
90+- [ ] T033 Manual test: Verify browser starts with GTK4/WebKitGTK 6.0
91+- [ ] T034 Manual test: Verify GPU acceleration with WebGL content
92+- [ ] T035 Manual test: Verify CPU fallback with --rendering-mode=cpu
93+
94+## Phase 3.9: Documentation & Cleanup
95+- [ ] T036 [P] Update README.md with new build requirements
96+- [ ] T037 [P] Document rendering mode configuration in docs/
97+- [ ] T038 Remove deprecated WebKit2GTK 4.0 compatibility code
98+- [ ] T039 Update CLAUDE.md to reflect completed migration
99+
100+## Dependencies
101+- Build config (T001-T005) must complete first
102+- Rendering config (T006-T009) can run in parallel
103+- Core migration (T010-T026) depends on build config
104+- T013-T015 must be sequential (same file)
105+- T016-T018 must be sequential (same file)
106+- Integration (T027-T029) depends on core migration
107+- Testing (T030-T035) depends on all implementation
108+- Documentation (T036-T039) can start anytime
109+
110+## Parallel Example
111+```bash
112+# Launch T006-T009 together (different files):
113+Task: "Add RenderingConfig struct to pkg/webkit/settings.go"
114+Task: "Add RenderingMode constants to internal/config/config.go"
115+Task: "Add --rendering-mode CLI flag to internal/cli/browse.go"
116+Task: "Add DUMBER_RENDERING_MODE env var to internal/config/config.go"
117+
118+# Launch T022-T026 together (independent files):
119+Task: "Update pkg/webkit/script_cgo.go for GTK4"
120+Task: "Update pkg/webkit/zoom_cgo.go for GTK4"
121+Task: "Update pkg/webkit/devtools_cgo.go for GTK4"
122+Task: "Update pkg/webkit/loop_cgo.go for GTK4"
123+Task: "Update pkg/webkit/scheme_cgo.go for URI handling"
124+```
125+
126+## Notes
127+- No TDD per user request - migrate first, test after
128+- Focus on incremental file-by-file migration
129+- Test GPU acceleration after each major component
130+- Commit after each task to track progress
131+- Use quickstart.md for verification steps
132+
133+## Critical Migration Points
134+1. **WebView Creation**: Must update to g_object_new pattern
135+2. **Network Session**: Replace all WebContext usage
136+3. **Event Handling**: GTK4 uses controllers not direct events
137+4. **Container API**: gtk_container_add no longer exists
138+5. **Hardware Acceleration**: Must configure policy correctly⏎̸

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