detached 6dfc884

feat(webkit): add content blocking stubs and keyboard dispatcher improvements

commit
6dfc8843bab0e256d276dc536edbff1d4762d1d7
author
bamen · · 2025-09-15 05:21 UTC
parent
66a30d0
signature
unsigned
6 files · +462 -55
M pkg/webkit/codec_control.go +47 -31
@@ -59,8 +59,13 @@ func GenerateCodecControlScript(prefs CodecPreferencesConfig) string {
5959
6060 js.WriteString(`(() => {
6161 'use strict';
62-
63- console.log('[dumber-codec] Initializing codec control');
62+
63+ const isMainFrame = (window.self === window.top);
64+ function log(msg) {
65+ if (isMainFrame) console.log('[dumber-codec] ' + msg);
66+ }
67+
68+ log('Initializing codec control');
6469
6570 // Smart fullscreen handling that works with WebKit's native behavior
6671 const videoFullscreenStates = new WeakMap();
@@ -80,7 +85,7 @@ func GenerateCodecControlScript(prefs CodecPreferencesConfig) string {
8085 };
8186
8287 videoFullscreenStates.set(video, state);
83- console.log('[dumber-codec] Tracking video for fullscreen - playing:', state.wasPlaying, 'time:', state.currentTime);
88+ log('Tracking video for fullscreen - playing: ' + state.wasPlaying + ', time: ' + state.currentTime);
8489 }
8590
8691 // Gentle recovery without forcing reloads
@@ -88,20 +93,20 @@ func GenerateCodecControlScript(prefs CodecPreferencesConfig) string {
8893 const state = videoFullscreenStates.get(video);
8994 if (!state) return;
9095
91- console.log('[dumber-codec] Recovering video after fullscreen transition');
96+ log('Recovering video after fullscreen transition');
9297
9398 // Only intervene if video is stuck in a loading state
9499 const isStuck = video.readyState < 2 && video.networkState === 2; // NETWORK_LOADING but not ready
95100 const hasLostTime = Math.abs(video.currentTime - state.currentTime) > 2;
96101
97102 if (isStuck || hasLostTime) {
98- console.log('[dumber-codec] Video appears stuck, applying gentle recovery');
103+ log('Video appears stuck, applying gentle recovery');
99104
100105 // Gentle nudge - just try to resume if it was playing
101106 if (state.wasPlaying && video.paused) {
102107 setTimeout(() => {
103108 if (video.readyState >= 2) {
104- video.play().catch(e => console.log('[dumber-codec] Play failed:', e));
109+ video.play().catch(e => log('Play failed: ' + e));
105110 }
106111 }, 200);
107112 }
@@ -115,11 +120,11 @@ func GenerateCodecControlScript(prefs CodecPreferencesConfig) string {
115120 if (fullscreenTransitionInProgress) return;
116121 fullscreenTransitionInProgress = true;
117122
118- console.log('[dumber-codec] WebKit fullscreen transition detected');
123+ log('WebKit fullscreen transition detected');
119124
120125 // Skip fullscreen recovery on Twitch to prevent theater/fullscreen freezing
121126 if (location.hostname.includes('twitch.tv')) {
122- console.log('[dumber-codec] Skipping fullscreen recovery on Twitch for stability');
127+ log('Skipping fullscreen recovery on Twitch for stability');
123128 fullscreenTransitionInProgress = false;
124129 return;
125130 }
@@ -148,7 +153,7 @@ func GenerateCodecControlScript(prefs CodecPreferencesConfig) string {
148153 // Override canPlayType for HTMLMediaElement (primary codec detection)
149154 const originalCanPlayType = HTMLMediaElement.prototype.canPlayType;
150155 HTMLMediaElement.prototype.canPlayType = function(type) {
151- console.log('[dumber-codec] canPlayType called with:', type);
156+ log('canPlayType called with: ' + type);
152157
153158 // Block unwanted codecs`)
154159
@@ -156,7 +161,7 @@ func GenerateCodecControlScript(prefs CodecPreferencesConfig) string {
156161 js.WriteString(`
157162 const blockedRegex = /` + blockedPattern + `/i;
158163 if (blockedRegex.test(type)) {
159- console.log('[dumber-codec] Blocking codec via canPlayType:', type);
164+ log('Blocking codec via canPlayType: ' + type);
160165 return '';
161166 }`)
162167 }
@@ -168,7 +173,7 @@ func GenerateCodecControlScript(prefs CodecPreferencesConfig) string {
168173 const preferredCodecs = ` + preferredCodecsJS + `;
169174 for (const preferred of preferredCodecs) {
170175 if (type.toLowerCase().includes(preferred)) {
171- console.log('[dumber-codec] Boosting preferred codec via canPlayType:', type);
176+ log('Boosting preferred codec via canPlayType: ' + type);
172177 return 'probably';
173178 }
174179 }`)
@@ -184,7 +189,7 @@ func GenerateCodecControlScript(prefs CodecPreferencesConfig) string {
184189 if (window.MediaSource && MediaSource.isTypeSupported) {
185190 const originalIsTypeSupported = MediaSource.isTypeSupported.bind(MediaSource);
186191 MediaSource.isTypeSupported = function(type) {
187- console.log('[dumber-codec] MediaSource.isTypeSupported called with:', type);`)
192+ log('MediaSource.isTypeSupported called with: ' + type);`)
188193
189194 if hasBlockedCodecs {
190195 js.WriteString(`
@@ -192,7 +197,7 @@ func GenerateCodecControlScript(prefs CodecPreferencesConfig) string {
192197 // Block unwanted codecs
193198 const blockedRegex = /` + blockedPattern + `/i;
194199 if (blockedRegex.test(type)) {
195- console.log('[dumber-codec] Blocking codec via MediaSource.isTypeSupported:', type);
200+ log('Blocking codec via MediaSource.isTypeSupported: ' + type);
196201 return false;
197202 }`)
198203 }
@@ -204,7 +209,7 @@ func GenerateCodecControlScript(prefs CodecPreferencesConfig) string {
204209 const preferredCodecs = ` + preferredCodecsJS + `;
205210 for (const preferred of preferredCodecs) {
206211 if (type.toLowerCase().includes(preferred)) {
207- console.log('[dumber-codec] Boosting preferred codec via MediaSource.isTypeSupported:', type);
212+ log('Boosting preferred codec via MediaSource.isTypeSupported: ' + type);
208213 return true;
209214 }
210215 }`)
@@ -237,8 +242,8 @@ func GenerateCodecControlScript(prefs CodecPreferencesConfig) string {
237242 const is1080p60 = (height >= 1080 || width >= 1920) &&
238243 (config.video?.framerate >= 50 || config.video?.frameRate >= 50);
239244
240- console.log('[dumber-codec] MediaCapabilities query - contentType:', contentType,
241- 'resolution:', width + 'x' + height, 'highRes:', isHighRes, '4K:', is4K, '1080p60:', is1080p60);
245+ log('MediaCapabilities query - contentType: ' + contentType +
246+ ', resolution: ' + width + 'x' + height + ', highRes: ' + isHighRes + ', 4K: ' + is4K + ', 1080p60: ' + is1080p60);
242247
243248 // Block unwanted codecs`)
244249
@@ -246,7 +251,7 @@ func GenerateCodecControlScript(prefs CodecPreferencesConfig) string {
246251 js.WriteString(`
247252 const blockedRegex = /` + blockedPattern + `/i;
248253 if (blockedRegex.test(contentType)) {
249- console.log('[dumber-codec] Blocking codec:', contentType);
254+ log('Blocking codec: ' + contentType);
250255 return {
251256 supported: false,
252257 smooth: false,
@@ -264,7 +269,7 @@ func GenerateCodecControlScript(prefs CodecPreferencesConfig) string {
264269 const preferredCodecs = ` + preferredCodecsJS + `;
265270 for (const preferred of preferredCodecs) {
266271 if (contentType.toLowerCase().includes(preferred)) {
267- console.log('[dumber-codec] Boosting preferred codec:', contentType);
272+ log('Boosting preferred codec: ' + contentType);
268273
269274 // Resolution-aware capability reporting for AV1
270275 if (preferred === 'av01' || preferred === 'av1') {
@@ -328,7 +333,7 @@ func GenerateCodecControlScript(prefs CodecPreferencesConfig) string {
328333
329334 // YouTube-specific codec forcing
330335 if (location.hostname.includes('youtube.com')) {
331- console.log('[dumber-codec] Applying YouTube codec preferences');
336+ log('Applying YouTube codec preferences');
332337
333338 // Set YouTube's localStorage preferences for smart AV1 usage
334339 // 2048 = prefer AV1 for lower resolutions, fallback to VP9 for higher res
@@ -340,7 +345,7 @@ func GenerateCodecControlScript(prefs CodecPreferencesConfig) string {
340345 writable: false,
341346 configurable: false
342347 });
343- console.log('[dumber-codec] YouTube: Set smart AV1 localStorage preference (2048)');
348+ log('YouTube: Set smart AV1 localStorage preference (2048)');
344349 } catch (e) {
345350 console.warn('[dumber-codec] YouTube: Failed to set localStorage:', e);
346351 }
@@ -358,7 +363,7 @@ func GenerateCodecControlScript(prefs CodecPreferencesConfig) string {
358363 js.WriteString(`
359364 // Force AV1 if enabled
360365 player.config.args.preferred_codecs = 'av01';
361- console.log('[dumber-codec] YouTube: Forced AV1 codec');`)
366+ log('YouTube: Forced AV1 codec');`)
362367 }
363368
364369 if hasPreferredCodecs {
@@ -375,7 +380,7 @@ func GenerateCodecControlScript(prefs CodecPreferencesConfig) string {
375380 }
376381 }).join(',');
377382 player.config.args.preferred_codecs = codecOrder;
378- console.log('[dumber-codec] YouTube: Set codec order:', codecOrder);`)
383+ log('YouTube: Set codec order: ' + codecOrder);`)
379384 }
380385
381386 js.WriteString(`
@@ -388,7 +393,7 @@ func GenerateCodecControlScript(prefs CodecPreferencesConfig) string {
388393 const originalFetch = window.fetch;
389394 window.fetch = function(input, init) {
390395 if (typeof input === 'string' && input.includes('/youtubei/v1/player')) {
391- console.log('[dumber-codec] YouTube: Intercepting player request');`)
396+ log('YouTube: Intercepting player request');`)
392397
393398 if hasPreferredCodecs {
394399 js.WriteString(`
@@ -422,7 +427,7 @@ func GenerateCodecControlScript(prefs CodecPreferencesConfig) string {
422427
423428 // Manipulate streaming data if present
424429 if (data.streamingData && data.streamingData.formats) {
425- console.log('[dumber-codec] YouTube: Processing format manifest');
430+ log('YouTube: Processing format manifest');
426431
427432 // Sort formats to prioritize AV1 for lower resolutions, VP9 for higher
428433 data.streamingData.formats.sort((a, b) => {
@@ -447,7 +452,7 @@ func GenerateCodecControlScript(prefs CodecPreferencesConfig) string {
447452 return 0;
448453 });
449454
450- console.log('[dumber-codec] YouTube: Reordered formats for optimal codec selection');
455+ log('YouTube: Reordered formats for optimal codec selection');
451456 }
452457
453458 return new Response(JSON.stringify(data), {
@@ -473,7 +478,7 @@ func GenerateCodecControlScript(prefs CodecPreferencesConfig) string {
473478
474479 // Twitch: No codec interference - let Twitch handle codec selection natively
475480 if (location.hostname.includes('twitch.tv')) {
476- console.log('[dumber-codec] Detected Twitch domain - no codec interference for stability');
481+ log('Detected Twitch domain - no codec interference for stability');
477482 // Twitch codec control completely removed to prevent theater/fullscreen freezing
478483 }
479484
@@ -483,11 +488,11 @@ func GenerateCodecControlScript(prefs CodecPreferencesConfig) string {
483488 videos.forEach(video => {
484489 if (!video.__codecMonitored) {
485490 video.__codecMonitored = true;
486- console.log('[dumber-codec] Found video element, adding passive monitoring');
491+ log('Found video element, adding passive monitoring');
487492
488493 // Passive codec detection only
489494 video.addEventListener('canplay', function() {
490- console.log('[dumber-codec] Video can play - resolution:', this.videoWidth + 'x' + this.videoHeight);
495+ log('Video can play - resolution: ' + this.videoWidth + 'x' + this.videoHeight);
491496 }, { passive: true, once: true });
492497 }
493498 });
@@ -495,10 +500,21 @@ func GenerateCodecControlScript(prefs CodecPreferencesConfig) string {
495500
496501 // Monitor for video elements periodically without overriding createElement
497502 const videoObserver = new MutationObserver(monitorExistingVideos);
498- videoObserver.observe(document.body, { childList: true, subtree: true });
499- monitorExistingVideos(); // Check existing videos
503+
504+ // Wait for document.body to be available before observing
505+ function startVideoObserver() {
506+ if (document.body) {
507+ videoObserver.observe(document.body, { childList: true, subtree: true });
508+ monitorExistingVideos(); // Check existing videos
509+ log('Video observer started');
510+ } else {
511+ // Retry after a short delay if body not ready
512+ setTimeout(startVideoObserver, 50);
513+ }
514+ }
515+ startVideoObserver();
500516
501- console.log('[dumber-codec] Codec control initialization complete');
517+ log('Codec control initialization complete');
502518 })();`)
503519
504520 return js.String()
@@ -59,8 +59,13 @@ func GenerateCodecControlScript(prefs CodecPreferencesConfig) string {
59 59
60 js.WriteString(`(() => {60 js.WriteString(`(() => {
61 'use strict';61 'use strict';
62- 62+
63- console.log('[dumber-codec] Initializing codec control');63+ const isMainFrame = (window.self === window.top);
64+ function log(msg) {
65+ if (isMainFrame) console.log('[dumber-codec] ' + msg);
66+ }
67+
68+ log('Initializing codec control');
64 69
65 // Smart fullscreen handling that works with WebKit's native behavior70 // Smart fullscreen handling that works with WebKit's native behavior
66 const videoFullscreenStates = new WeakMap();71 const videoFullscreenStates = new WeakMap();
@@ -80,7 +85,7 @@ func GenerateCodecControlScript(prefs CodecPreferencesConfig) string {
80 };85 };
81 86
82 videoFullscreenStates.set(video, state);87 videoFullscreenStates.set(video, state);
83- console.log('[dumber-codec] Tracking video for fullscreen - playing:', state.wasPlaying, 'time:', state.currentTime);88+ log('Tracking video for fullscreen - playing: ' + state.wasPlaying + ', time: ' + state.currentTime);
84 }89 }
85 90
86 // Gentle recovery without forcing reloads91 // Gentle recovery without forcing reloads
@@ -88,20 +93,20 @@ func GenerateCodecControlScript(prefs CodecPreferencesConfig) string {
88 const state = videoFullscreenStates.get(video);93 const state = videoFullscreenStates.get(video);
89 if (!state) return;94 if (!state) return;
90 95
91- console.log('[dumber-codec] Recovering video after fullscreen transition');96+ log('Recovering video after fullscreen transition');
92 97
93 // Only intervene if video is stuck in a loading state98 // Only intervene if video is stuck in a loading state
94 const isStuck = video.readyState < 2 && video.networkState === 2; // NETWORK_LOADING but not ready99 const isStuck = video.readyState < 2 && video.networkState === 2; // NETWORK_LOADING but not ready
95 const hasLostTime = Math.abs(video.currentTime - state.currentTime) > 2;100 const hasLostTime = Math.abs(video.currentTime - state.currentTime) > 2;
96 101
97 if (isStuck || hasLostTime) {102 if (isStuck || hasLostTime) {
98- console.log('[dumber-codec] Video appears stuck, applying gentle recovery');103+ log('Video appears stuck, applying gentle recovery');
99 104
100 // Gentle nudge - just try to resume if it was playing105 // Gentle nudge - just try to resume if it was playing
101 if (state.wasPlaying && video.paused) {106 if (state.wasPlaying && video.paused) {
102 setTimeout(() => {107 setTimeout(() => {
103 if (video.readyState >= 2) {108 if (video.readyState >= 2) {
104- video.play().catch(e => console.log('[dumber-codec] Play failed:', e));109+ video.play().catch(e => log('Play failed: ' + e));
105 }110 }
106 }, 200);111 }, 200);
107 }112 }
@@ -115,11 +120,11 @@ func GenerateCodecControlScript(prefs CodecPreferencesConfig) string {
115 if (fullscreenTransitionInProgress) return;120 if (fullscreenTransitionInProgress) return;
116 fullscreenTransitionInProgress = true;121 fullscreenTransitionInProgress = true;
117 122
118- console.log('[dumber-codec] WebKit fullscreen transition detected');123+ log('WebKit fullscreen transition detected');
119 124
120 // Skip fullscreen recovery on Twitch to prevent theater/fullscreen freezing125 // Skip fullscreen recovery on Twitch to prevent theater/fullscreen freezing
121 if (location.hostname.includes('twitch.tv')) {126 if (location.hostname.includes('twitch.tv')) {
122- console.log('[dumber-codec] Skipping fullscreen recovery on Twitch for stability');127+ log('Skipping fullscreen recovery on Twitch for stability');
123 fullscreenTransitionInProgress = false;128 fullscreenTransitionInProgress = false;
124 return;129 return;
125 }130 }
@@ -148,7 +153,7 @@ func GenerateCodecControlScript(prefs CodecPreferencesConfig) string {
148 // Override canPlayType for HTMLMediaElement (primary codec detection)153 // Override canPlayType for HTMLMediaElement (primary codec detection)
149 const originalCanPlayType = HTMLMediaElement.prototype.canPlayType;154 const originalCanPlayType = HTMLMediaElement.prototype.canPlayType;
150 HTMLMediaElement.prototype.canPlayType = function(type) {155 HTMLMediaElement.prototype.canPlayType = function(type) {
151- console.log('[dumber-codec] canPlayType called with:', type);156+ log('canPlayType called with: ' + type);
152 157
153 // Block unwanted codecs`)158 // Block unwanted codecs`)
154 159
@@ -156,7 +161,7 @@ func GenerateCodecControlScript(prefs CodecPreferencesConfig) string {
156 js.WriteString(`161 js.WriteString(`
157 const blockedRegex = /` + blockedPattern + `/i;162 const blockedRegex = /` + blockedPattern + `/i;
158 if (blockedRegex.test(type)) {163 if (blockedRegex.test(type)) {
159- console.log('[dumber-codec] Blocking codec via canPlayType:', type);164+ log('Blocking codec via canPlayType: ' + type);
160 return '';165 return '';
161 }`)166 }`)
162 }167 }
@@ -168,7 +173,7 @@ func GenerateCodecControlScript(prefs CodecPreferencesConfig) string {
168 const preferredCodecs = ` + preferredCodecsJS + `;173 const preferredCodecs = ` + preferredCodecsJS + `;
169 for (const preferred of preferredCodecs) {174 for (const preferred of preferredCodecs) {
170 if (type.toLowerCase().includes(preferred)) {175 if (type.toLowerCase().includes(preferred)) {
171- console.log('[dumber-codec] Boosting preferred codec via canPlayType:', type);176+ log('Boosting preferred codec via canPlayType: ' + type);
172 return 'probably';177 return 'probably';
173 }178 }
174 }`)179 }`)
@@ -184,7 +189,7 @@ func GenerateCodecControlScript(prefs CodecPreferencesConfig) string {
184 if (window.MediaSource && MediaSource.isTypeSupported) {189 if (window.MediaSource && MediaSource.isTypeSupported) {
185 const originalIsTypeSupported = MediaSource.isTypeSupported.bind(MediaSource);190 const originalIsTypeSupported = MediaSource.isTypeSupported.bind(MediaSource);
186 MediaSource.isTypeSupported = function(type) {191 MediaSource.isTypeSupported = function(type) {
187- console.log('[dumber-codec] MediaSource.isTypeSupported called with:', type);`)192+ log('MediaSource.isTypeSupported called with: ' + type);`)
188 193
189 if hasBlockedCodecs {194 if hasBlockedCodecs {
190 js.WriteString(`195 js.WriteString(`
@@ -192,7 +197,7 @@ func GenerateCodecControlScript(prefs CodecPreferencesConfig) string {
192 // Block unwanted codecs197 // Block unwanted codecs
193 const blockedRegex = /` + blockedPattern + `/i;198 const blockedRegex = /` + blockedPattern + `/i;
194 if (blockedRegex.test(type)) {199 if (blockedRegex.test(type)) {
195- console.log('[dumber-codec] Blocking codec via MediaSource.isTypeSupported:', type);200+ log('Blocking codec via MediaSource.isTypeSupported: ' + type);
196 return false;201 return false;
197 }`)202 }`)
198 }203 }
@@ -204,7 +209,7 @@ func GenerateCodecControlScript(prefs CodecPreferencesConfig) string {
204 const preferredCodecs = ` + preferredCodecsJS + `;209 const preferredCodecs = ` + preferredCodecsJS + `;
205 for (const preferred of preferredCodecs) {210 for (const preferred of preferredCodecs) {
206 if (type.toLowerCase().includes(preferred)) {211 if (type.toLowerCase().includes(preferred)) {
207- console.log('[dumber-codec] Boosting preferred codec via MediaSource.isTypeSupported:', type);212+ log('Boosting preferred codec via MediaSource.isTypeSupported: ' + type);
208 return true;213 return true;
209 }214 }
210 }`)215 }`)
@@ -237,8 +242,8 @@ func GenerateCodecControlScript(prefs CodecPreferencesConfig) string {
237 const is1080p60 = (height >= 1080 || width >= 1920) && 242 const is1080p60 = (height >= 1080 || width >= 1920) &&
238 (config.video?.framerate >= 50 || config.video?.frameRate >= 50);243 (config.video?.framerate >= 50 || config.video?.frameRate >= 50);
239 244
240- console.log('[dumber-codec] MediaCapabilities query - contentType:', contentType, 245+ log('MediaCapabilities query - contentType: ' + contentType +
241- 'resolution:', width + 'x' + height, 'highRes:', isHighRes, '4K:', is4K, '1080p60:', is1080p60);246+ ', resolution: ' + width + 'x' + height + ', highRes: ' + isHighRes + ', 4K: ' + is4K + ', 1080p60: ' + is1080p60);
242 247
243 // Block unwanted codecs`)248 // Block unwanted codecs`)
244 249
@@ -246,7 +251,7 @@ func GenerateCodecControlScript(prefs CodecPreferencesConfig) string {
246 js.WriteString(`251 js.WriteString(`
247 const blockedRegex = /` + blockedPattern + `/i;252 const blockedRegex = /` + blockedPattern + `/i;
248 if (blockedRegex.test(contentType)) {253 if (blockedRegex.test(contentType)) {
249- console.log('[dumber-codec] Blocking codec:', contentType);254+ log('Blocking codec: ' + contentType);
250 return {255 return {
251 supported: false,256 supported: false,
252 smooth: false,257 smooth: false,
@@ -264,7 +269,7 @@ func GenerateCodecControlScript(prefs CodecPreferencesConfig) string {
264 const preferredCodecs = ` + preferredCodecsJS + `;269 const preferredCodecs = ` + preferredCodecsJS + `;
265 for (const preferred of preferredCodecs) {270 for (const preferred of preferredCodecs) {
266 if (contentType.toLowerCase().includes(preferred)) {271 if (contentType.toLowerCase().includes(preferred)) {
267- console.log('[dumber-codec] Boosting preferred codec:', contentType);272+ log('Boosting preferred codec: ' + contentType);
268 273
269 // Resolution-aware capability reporting for AV1274 // Resolution-aware capability reporting for AV1
270 if (preferred === 'av01' || preferred === 'av1') {275 if (preferred === 'av01' || preferred === 'av1') {
@@ -328,7 +333,7 @@ func GenerateCodecControlScript(prefs CodecPreferencesConfig) string {
328 333
329 // YouTube-specific codec forcing334 // YouTube-specific codec forcing
330 if (location.hostname.includes('youtube.com')) {335 if (location.hostname.includes('youtube.com')) {
331- console.log('[dumber-codec] Applying YouTube codec preferences');336+ log('Applying YouTube codec preferences');
332 337
333 // Set YouTube's localStorage preferences for smart AV1 usage338 // Set YouTube's localStorage preferences for smart AV1 usage
334 // 2048 = prefer AV1 for lower resolutions, fallback to VP9 for higher res339 // 2048 = prefer AV1 for lower resolutions, fallback to VP9 for higher res
@@ -340,7 +345,7 @@ func GenerateCodecControlScript(prefs CodecPreferencesConfig) string {
340 writable: false,345 writable: false,
341 configurable: false346 configurable: false
342 });347 });
343- console.log('[dumber-codec] YouTube: Set smart AV1 localStorage preference (2048)');348+ log('YouTube: Set smart AV1 localStorage preference (2048)');
344 } catch (e) {349 } catch (e) {
345 console.warn('[dumber-codec] YouTube: Failed to set localStorage:', e);350 console.warn('[dumber-codec] YouTube: Failed to set localStorage:', e);
346 }351 }
@@ -358,7 +363,7 @@ func GenerateCodecControlScript(prefs CodecPreferencesConfig) string {
358 js.WriteString(`363 js.WriteString(`
359 // Force AV1 if enabled364 // Force AV1 if enabled
360 player.config.args.preferred_codecs = 'av01';365 player.config.args.preferred_codecs = 'av01';
361- console.log('[dumber-codec] YouTube: Forced AV1 codec');`)366+ log('YouTube: Forced AV1 codec');`)
362 }367 }
363 368
364 if hasPreferredCodecs {369 if hasPreferredCodecs {
@@ -375,7 +380,7 @@ func GenerateCodecControlScript(prefs CodecPreferencesConfig) string {
375 }380 }
376 }).join(',');381 }).join(',');
377 player.config.args.preferred_codecs = codecOrder;382 player.config.args.preferred_codecs = codecOrder;
378- console.log('[dumber-codec] YouTube: Set codec order:', codecOrder);`)383+ log('YouTube: Set codec order: ' + codecOrder);`)
379 }384 }
380 385
381 js.WriteString(`386 js.WriteString(`
@@ -388,7 +393,7 @@ func GenerateCodecControlScript(prefs CodecPreferencesConfig) string {
388 const originalFetch = window.fetch;393 const originalFetch = window.fetch;
389 window.fetch = function(input, init) {394 window.fetch = function(input, init) {
390 if (typeof input === 'string' && input.includes('/youtubei/v1/player')) {395 if (typeof input === 'string' && input.includes('/youtubei/v1/player')) {
391- console.log('[dumber-codec] YouTube: Intercepting player request');`)396+ log('YouTube: Intercepting player request');`)
392 397
393 if hasPreferredCodecs {398 if hasPreferredCodecs {
394 js.WriteString(`399 js.WriteString(`
@@ -422,7 +427,7 @@ func GenerateCodecControlScript(prefs CodecPreferencesConfig) string {
422 427
423 // Manipulate streaming data if present428 // Manipulate streaming data if present
424 if (data.streamingData && data.streamingData.formats) {429 if (data.streamingData && data.streamingData.formats) {
425- console.log('[dumber-codec] YouTube: Processing format manifest');430+ log('YouTube: Processing format manifest');
426 431
427 // Sort formats to prioritize AV1 for lower resolutions, VP9 for higher432 // Sort formats to prioritize AV1 for lower resolutions, VP9 for higher
428 data.streamingData.formats.sort((a, b) => {433 data.streamingData.formats.sort((a, b) => {
@@ -447,7 +452,7 @@ func GenerateCodecControlScript(prefs CodecPreferencesConfig) string {
447 return 0;452 return 0;
448 });453 });
449 454
450- console.log('[dumber-codec] YouTube: Reordered formats for optimal codec selection');455+ log('YouTube: Reordered formats for optimal codec selection');
451 }456 }
452 457
453 return new Response(JSON.stringify(data), {458 return new Response(JSON.stringify(data), {
@@ -473,7 +478,7 @@ func GenerateCodecControlScript(prefs CodecPreferencesConfig) string {
473 478
474 // Twitch: No codec interference - let Twitch handle codec selection natively479 // Twitch: No codec interference - let Twitch handle codec selection natively
475 if (location.hostname.includes('twitch.tv')) {480 if (location.hostname.includes('twitch.tv')) {
476- console.log('[dumber-codec] Detected Twitch domain - no codec interference for stability');481+ log('Detected Twitch domain - no codec interference for stability');
477 // Twitch codec control completely removed to prevent theater/fullscreen freezing482 // Twitch codec control completely removed to prevent theater/fullscreen freezing
478 }483 }
479 484
@@ -483,11 +488,11 @@ func GenerateCodecControlScript(prefs CodecPreferencesConfig) string {
483 videos.forEach(video => {488 videos.forEach(video => {
484 if (!video.__codecMonitored) {489 if (!video.__codecMonitored) {
485 video.__codecMonitored = true;490 video.__codecMonitored = true;
486- console.log('[dumber-codec] Found video element, adding passive monitoring');491+ log('Found video element, adding passive monitoring');
487 492
488 // Passive codec detection only493 // Passive codec detection only
489 video.addEventListener('canplay', function() {494 video.addEventListener('canplay', function() {
490- console.log('[dumber-codec] Video can play - resolution:', this.videoWidth + 'x' + this.videoHeight);495+ log('Video can play - resolution: ' + this.videoWidth + 'x' + this.videoHeight);
491 }, { passive: true, once: true });496 }, { passive: true, once: true });
492 }497 }
493 });498 });
@@ -495,10 +500,21 @@ func GenerateCodecControlScript(prefs CodecPreferencesConfig) string {
495 500
496 // Monitor for video elements periodically without overriding createElement501 // Monitor for video elements periodically without overriding createElement
497 const videoObserver = new MutationObserver(monitorExistingVideos);502 const videoObserver = new MutationObserver(monitorExistingVideos);
498- videoObserver.observe(document.body, { childList: true, subtree: true });503+
499- monitorExistingVideos(); // Check existing videos504+ // Wait for document.body to be available before observing
505+ function startVideoObserver() {
506+ if (document.body) {
507+ videoObserver.observe(document.body, { childList: true, subtree: true });
508+ monitorExistingVideos(); // Check existing videos
509+ log('Video observer started');
510+ } else {
511+ // Retry after a short delay if body not ready
512+ setTimeout(startVideoObserver, 50);
513+ }
514+ }
515+ startVideoObserver();
500 516
501- console.log('[dumber-codec] Codec control initialization complete');517+ log('Codec control initialization complete');
502 })();`)518 })();`)
503 519
504 return js.String()520 return js.String()
M pkg/webkit/keyboard_cgo.go +3 -0
@@ -153,6 +153,8 @@ func dispatchAccelerator(uid uintptr, keyval uint, state uint) {
153153 keyName = "r"
154154 case uint(C.GDK_KEY_l), uint(C.GDK_KEY_L):
155155 keyName = "l"
156+ case uint(C.GDK_KEY_f), uint(C.GDK_KEY_F):
157+ keyName = "f"
156158 default:
157159 // If Control held, log unknown keyval for diagnostics
158160 if ctrl {
@@ -305,6 +307,7 @@ s.textContent=':root{color-scheme:' + (d?'dark':'light') + ';}';
305307 _ = vw.InjectScript(js)
306308 }
307309
310+
308311 //export goOnUcmMessage
309312 func goOnUcmMessage(id C.ulong, json *C.char) {
310313 uid := uintptr(id)
@@ -153,6 +153,8 @@ func dispatchAccelerator(uid uintptr, keyval uint, state uint) {
153 keyName = "r"153 keyName = "r"
154 case uint(C.GDK_KEY_l), uint(C.GDK_KEY_L):154 case uint(C.GDK_KEY_l), uint(C.GDK_KEY_L):
155 keyName = "l"155 keyName = "l"
156+ case uint(C.GDK_KEY_f), uint(C.GDK_KEY_F):
157+ keyName = "f"
156 default:158 default:
157 // If Control held, log unknown keyval for diagnostics159 // If Control held, log unknown keyval for diagnostics
158 if ctrl {160 if ctrl {
@@ -305,6 +307,7 @@ s.textContent=':root{color-scheme:' + (d?'dark':'light') + ';}';
305 _ = vw.InjectScript(js)307 _ = vw.InjectScript(js)
306 }308 }
307 309
310+
308 //export goOnUcmMessage311 //export goOnUcmMessage
309 func goOnUcmMessage(id C.ulong, json *C.char) {312 func goOnUcmMessage(id C.ulong, json *C.char) {
310 uid := uintptr(id)313 uid := uintptr(id)
M pkg/webkit/settings.go +3 -0
@@ -3,6 +3,9 @@ package webkit
33 // Config holds initialization settings for the WebKit-based browser components.
44 // This will map to WebKit2GTK and GTK settings in the real implementation.
55 type Config struct {
6+ // Assets provides access to embedded GUI files
7+ Assets interface{ ReadFile(string) ([]byte, error) }
8+
69 // InitialURL is the first URL to load when creating a WebView.
710 InitialURL string
811
@@ -3,6 +3,9 @@ package webkit
3 // Config holds initialization settings for the WebKit-based browser components.3 // Config holds initialization settings for the WebKit-based browser components.
4 // This will map to WebKit2GTK and GTK settings in the real implementation.4 // This will map to WebKit2GTK and GTK settings in the real implementation.
5 type Config struct {5 type Config struct {
6+ // Assets provides access to embedded GUI files
7+ Assets interface{ ReadFile(string) ([]byte, error) }
8+
6 // InitialURL is the first URL to load when creating a WebView.9 // InitialURL is the first URL to load when creating a WebView.
7 InitialURL string10 InitialURL string
8 11
A pkg/webkit/ucm_js_cgo.go.deprecated +335 -0
@@ -0,0 +1,335 @@
1+//go:build webkit_cgo
2+
3+package webkit
4+
5+// getOmniboxScript returns the injected omnibox/find component script.
6+// Kept in a cgo-tagged file to ensure availability when referenced
7+// from other cgo files in this package.
8+func getOmniboxScript() string {
9+ return `(() => {
10+ try {
11+ if (window.__dumber_omnibox_loaded) return; // idempotent
12+ window.__dumber_omnibox_loaded = true;
13+
14+ const MAX_MATCHES = 2000;
15+ const H = {
16+ el: null,
17+ box: null,
18+ input: null,
19+ list: null,
20+ visible: false,
21+ mode: 'omnibox', // 'omnibox' | 'find'
22+ suggestions: [],
23+ matches: [], // {el, context}
24+ selectedIndex: -1,
25+ activeIndex: -1,
26+ debounceTimer: 0,
27+ highlightNodes: [],
28+ faded: false,
29+ prevOverflow: '',
30+ post(msg){ try { window.webkit?.messageHandlers?.dumber?.postMessage(JSON.stringify(msg)); } catch(_){} },
31+ render(){
32+ // Performance optimization: elements are pre-mounted, just show/hide
33+ H.el.style.display = H.visible ? 'block' : 'none';
34+ if (!H.visible) return;
35+ H.input.placeholder = H.mode === 'find' ? 'Find in page…' : 'Type URL or search…';
36+ H.input.focus();
37+ },
38+ setMode(m){ H.mode = m === 'find' ? 'find' : 'omnibox'; H.selectedIndex = -1; H.setFaded(false); H.paintList(); },
39+ open(mode, initial){ H.setMode(mode||'omnibox'); H.toggle(true); if (typeof initial==='string') { H.input.value = initial; H.onInput(); } },
40+ close(){ if (H.mode==='find') H.clearHighlights(); H.setFaded(false); H.toggle(false); },
41+ toggle(v){
42+ const newState = (typeof v==='boolean')? v : !H.visible;
43+ if (newState && !H.visible) {
44+ // Disable page scrolling while overlay is visible
45+ H.prevOverflow = document.documentElement.style.overflow;
46+ document.documentElement.style.overflow = 'hidden';
47+ } else if (!newState && H.visible) {
48+ // Restore page scrolling on close
49+ document.documentElement.style.overflow = H.prevOverflow || '';
50+ H.prevOverflow = '';
51+ }
52+ H.visible = newState;
53+ H.render();
54+ },
55+ setFaded(v){
56+ H.faded = !!v;
57+ if (!H.box || !H.input || !H.list) return;
58+ if (H.faded) {
59+ H.box.style.background = 'rgba(27,27,27,0.25)';
60+ H.box.style.backdropFilter = 'blur(2px) saturate(110%)';
61+ H.box.style.webkitBackdropFilter = 'blur(2px) saturate(110%)';
62+ H.input.style.background = 'rgba(18,18,18,0.35)';
63+ H.input.style.color = '#eee';
64+ } else {
65+ H.box.style.background = '#1b1b1b';
66+ H.box.style.backdropFilter = '';
67+ H.box.style.webkitBackdropFilter = '';
68+ H.input.style.background = '#121212';
69+ H.input.style.color = '#eee';
70+ }
71+ },
72+ scrollListToSelection(){
73+ if (!H.list) return;
74+ let idx = H.selectedIndex|0;
75+ if (idx < 0) return;
76+ // Account for header in find mode
77+ const childIdx = (H.mode==='find' ? 1 : 0) + idx;
78+ const el = H.list.children[childIdx];
79+ if (el && el.scrollIntoView) {
80+ try { el.scrollIntoView({block:'nearest'}); } catch(_) { el.scrollIntoView(); }
81+ }
82+ },
83+ mount(){
84+ const root = document.createElement('div');
85+ root.id = 'dumber-omnibox-root';
86+ root.style.cssText = 'position:fixed;inset:0;z-index:2147483647;display:none;';
87+ const box = document.createElement('div');
88+ box.style.cssText = 'width:min(90vw, 720px);margin:8vh auto;padding:8px 12px;background:#1b1b1b;color:#eee;border:1px solid #444;border-radius:8px;box-shadow:0 10px 30px rgba(0,0,0,.6);font-family:system-ui,-apple-system,Segoe UI,Roboto,Ubuntu,"Helvetica Neue",Arial,sans-serif;';
89+ const input = document.createElement('input');
90+ input.type = 'text';
91+ input.placeholder = 'Type URL or search…';
92+ input.style.cssText = 'width:100%;box-sizing:border-box;padding:10px 12px;border-radius:6px;border:1px solid #555;background:#121212;color:#eee;font-size:16px;outline:none;';
93+ const list = document.createElement('div');
94+ list.style.cssText = 'margin-top:8px;max-height:50vh;overflow:auto;';
95+ const style = document.createElement('style');
96+ style.textContent = '.dumber-find-highlight{background:#ffeb3b;color:#000;padding:0 1px;border-radius:2px;box-shadow:0 0 0 1px #c8b900 inset}.dumber-find-active{background:#ff9800 !important;color:#000;box-shadow:0 0 0 1px #b36b00 inset}';
97+ document.documentElement.appendChild(style);
98+ box.appendChild(input); box.appendChild(list); root.appendChild(box); document.documentElement.appendChild(root);
99+ H.box = box;
100+ // Dynamic style updater for responsive behavior
101+ const updateStyles = () => {
102+ const vw = window.innerWidth;
103+ const isSmall = vw < 640;
104+ const isLarge = vw > 1920;
105+
106+ if (box) {
107+ box.style.width = isSmall ? '95vw' : isLarge ? 'min(90vw, 840px)' : 'min(90vw, 720px)';
108+ box.style.padding = isSmall ? '6px 8px' : '8px 12px';
109+ }
110+
111+ if (input) {
112+ input.style.fontSize = isSmall ? '14px' : isLarge ? '17px' : '16px';
113+ input.style.padding = isSmall ? '8px 10px' : '10px 12px';
114+ input.style.boxSizing = 'border-box';
115+ }
116+ };
117+ updateStyles();
118+ window.addEventListener('resize', updateStyles);
119+ // Click outside the box closes the overlay
120+ root.addEventListener('mousedown', (e)=>{
121+ const tgt = e.target;
122+ if (tgt && box && !box.contains(tgt)) {
123+ e.preventDefault();
124+ e.stopPropagation();
125+ H.close();
126+ }
127+ }, true);
128+ box.addEventListener('mousedown', (e)=>{ e.stopPropagation(); }, true);
129+ // Focus and select input on hover for quick typing
130+ input.addEventListener('mouseenter', ()=>{ if (H.visible) { try { input.focus({preventScroll:true}); input.select(); } catch(_) { input.focus(); } } });
131+ root.addEventListener('mouseenter', ()=>{ if (H.visible) { try { input.focus({preventScroll:true}); } catch(_) { input.focus(); } H.setFaded(false); } });
132+ // Clear fading when user goes back to input typing
133+ input.addEventListener('focus', ()=> H.setFaded(false));
134+ input.addEventListener('keydown', (e)=>{
135+ if (e.key === 'Escape'){ H.close(); }
136+ else if (H.mode==='omnibox' && e.key === 'Enter'){
137+ const pick = H.suggestions && H.suggestions[H.selectedIndex|0];
138+ const v = (pick && pick.url) || input.value || '';
139+ if (v) H.post({type:'navigate', url:v});
140+ H.toggle(false);
141+ } else if (H.mode==='find' && e.key === 'Enter'){
142+ e.preventDefault();
143+ if (e.shiftKey) {
144+ H.jump(-1);
145+ } else if (e.altKey) {
146+ // Center on current match but keep overlay open for further navigation
147+ H.revealSelection();
148+ } else {
149+ // Default: center and close
150+ H.revealSelection();
151+ H.close();
152+ }
153+ } else if (e.key === 'ArrowDown' || e.key === 'ArrowUp'){
154+ e.preventDefault(); e.stopPropagation();
155+ const n = (H.mode==='find'? H.matches.length : H.suggestions.length);
156+ if (n){
157+ H.selectedIndex = (H.selectedIndex||0) + (e.key==='ArrowDown'?1:-1);
158+ if (H.selectedIndex<0) H.selectedIndex = n-1; if (H.selectedIndex>=n) H.selectedIndex = 0;
159+ H.paintList();
160+ H.scrollListToSelection();
161+ // Fade overlay while navigating results; also reveal active match in find mode
162+ H.setFaded(true);
163+ if (H.mode==='find') { H.revealSelection(); }
164+ }
165+ } else {
166+ // Any other key input should restore full opacity while editing
167+ H.setFaded(false);
168+ }
169+ });
170+ input.addEventListener('input', ()=> { H.onInput(); H.setFaded(false); });
171+ input.addEventListener('mousedown', ()=> H.setFaded(false));
172+ input.addEventListener('click', ()=> H.setFaded(false));
173+ H.el = root; H.input = input; H.list = list; H.selectedIndex = -1; H.paintList();
174+ },
175+ onInput(){
176+ const q = H.input.value || '';
177+ if (H.mode === 'omnibox'){
178+ clearTimeout(H.debounceTimer);
179+ H.debounceTimer = setTimeout(()=> H.post({type:'query', q, limit:10}), 120);
180+ } else {
181+ H.find(q);
182+ }
183+ },
184+ paintList(){
185+ const list = H.list; if (!list) return; list.textContent = '';
186+
187+ // Show/hide separator based on content
188+ const hasContent = (H.mode==='omnibox' && H.suggestions.length > 0) || (H.mode==='find' && H.matches.length > 0);
189+ list.style.borderTop = hasContent ? '1px solid #333' : 'none';
190+
191+ if (H.mode==='omnibox'){
192+ H.suggestions.forEach((s, i)=>{
193+ const item = document.createElement('div');
194+ item.style.cssText = 'padding:8px 10px;display:flex;gap:10px;align-items:center;cursor:pointer;border-bottom:1px solid #2a2a2a;'+(i===H.selectedIndex?'background:#0a0a0a;':'');
195+ // Favicon chip (wrapper ensures perfect circle sizing)
196+ const chip = document.createElement('div');
197+ chip.style.cssText = 'flex:0 0 20px;width:20px;height:20px;border-radius:50%;background:#ccc;border:1px solid rgba(0,0,0,.12);box-shadow:0 1px 2px rgba(0,0,0,.12);display:flex;align-items:center;justify-content:center;';
198+ const icon = document.createElement('img');
199+ icon.src = s.favicon || '';
200+ icon.loading = 'lazy';
201+ icon.style.cssText = 'width:16px;height:16px;filter:brightness(1.06) contrast(1.03);image-rendering:-webkit-optimize-contrast;';
202+ icon.onerror = ()=>{ chip.style.display='none'; };
203+ chip.appendChild(icon);
204+ // Text line: Domain | full path (one line, fade at end)
205+ let domain = '', path = '';
206+ try { const u = new URL(s.url, window.location.href); domain = u.hostname; path = (u.pathname||'') + (u.search||'') + (u.hash||''); } catch(_) { domain = s.url || ''; }
207+ const text = document.createElement('div');
208+ text.style.cssText = 'flex:1;min-width:0;display:flex;gap:8px;align-items:center;white-space:nowrap;overflow:hidden;';
209+ // Apply gradient fade using mask for WebKit
210+ text.style.webkitMaskImage = 'linear-gradient(90deg, black 85%, transparent 100%)';
211+ text.style.maskImage = 'linear-gradient(90deg, black 85%, transparent 100%)';
212+ const domainEl = document.createElement('span'); domainEl.textContent = domain; domainEl.style.cssText = 'color:#e6e6e6;opacity:.95;';
213+ const sep = document.createElement('span'); sep.textContent = ' | '; sep.style.cssText = 'color:#777;';
214+ const pathEl = document.createElement('span'); pathEl.textContent = path || '/'; pathEl.style.cssText = 'color:#9ad;';
215+ text.appendChild(domainEl); text.appendChild(sep); text.appendChild(pathEl);
216+ item.appendChild(chip); item.appendChild(text);
217+ item.addEventListener('mouseenter', ()=>{ H.selectedIndex = i; H.paintList(); H.scrollListToSelection(); H.setFaded(true); });
218+ item.addEventListener('click',()=>{ H.post({type:'navigate', url:s.url}); H.toggle(false); });
219+ list.appendChild(item);
220+ });
221+ } else {
222+ const total = H.matches.length;
223+ const header = document.createElement('div');
224+ header.textContent = total ? total + ' matches' : 'No matches';
225+ header.style.cssText = 'padding:6px 10px;color:#bbb;font-size:12px;border-bottom:1px solid #2a2a2a;';
226+ list.appendChild(header);
227+ H.matches.forEach((m, i)=>{
228+ const item = document.createElement('div');
229+ item.style.cssText = 'padding:8px 10px;cursor:pointer;border-bottom:1px solid #2a2a2a;'+(i===H.selectedIndex?'background:#0a0a0a;':'');
230+ const ctx = document.createElement('div'); ctx.textContent = m.context || ''; ctx.style.cssText = 'color:#ddd;white-space:nowrap;overflow:hidden;text-overflow:ellipsis;';
231+ item.appendChild(ctx);
232+ item.addEventListener('mouseenter', ()=>{ H.selectedIndex = i; H.paintList(); H.scrollListToSelection(); H.setFaded(true); });
233+ item.addEventListener('click',()=>{ H.selectedIndex=i; H.revealSelection(); H.paintList(); H.close(); });
234+ list.appendChild(item);
235+ });
236+ }
237+ },
238+ setSuggestions(arr){ H.suggestions = Array.isArray(arr)? arr: []; H.selectedIndex = -1; H.paintList(); },
239+ // FIND MODE IMPLEMENTATION
240+ clearHighlights(){
241+ try {
242+ H.highlightNodes.forEach(({span, text})=>{
243+ const p = span.parentNode; if (!p) return; p.replaceChild(text, span); p.normalize();
244+ });
245+ } catch(_){}
246+ H.highlightNodes = [];
247+ H.matches = [];
248+ H.selectedIndex = -1;
249+ H.activeIndex = -1;
250+ H.paintList();
251+ },
252+ find(q){
253+ H.clearHighlights();
254+ q = (q||'').trim(); if (!q) return;
255+ const body = document.body; if (!body) return;
256+ const root = document.getElementById('dumber-omnibox-root');
257+ const walker = document.createTreeWalker(body, NodeFilter.SHOW_TEXT, {
258+ acceptNode(node){
259+ if (!node.nodeValue || !node.nodeValue.trim()) return NodeFilter.FILTER_REJECT;
260+ let el = node.parentElement; if (!el) return NodeFilter.FILTER_REJECT;
261+ if (root && root.contains(el)) return NodeFilter.FILTER_REJECT;
262+ const name = el.tagName; if (name==='SCRIPT'||name==='STYLE'||name==='NOSCRIPT') return NodeFilter.FILTER_REJECT;
263+ if (getComputedStyle(el).visibility==='hidden' || getComputedStyle(el).display==='none') return NodeFilter.FILTER_REJECT;
264+ return NodeFilter.FILTER_ACCEPT;
265+ }
266+ });
267+ const lc = q.toLowerCase();
268+ while (walker.nextNode()){
269+ const text = walker.currentNode;
270+ let s = text.nodeValue; let i = 0;
271+ while (s && (i = s.toLowerCase().indexOf(lc)) !== -1) {
272+ const before = document.createTextNode(s.slice(0, i));
273+ const match = document.createTextNode(s.slice(i, i+q.length));
274+ const afterVal = s.slice(i+q.length);
275+ const span = document.createElement('span'); span.className = 'dumber-find-highlight'; span.appendChild(match);
276+ const parent = text.parentNode; if (!parent) break;
277+ parent.insertBefore(before, text);
278+ parent.insertBefore(span, text);
279+ text.nodeValue = afterVal;
280+ H.highlightNodes.push({span, text: match});
281+ const leftCtx = before.nodeValue.slice(-30);
282+ let rightRaw = afterVal.slice(0, 30);
283+ const boundaryIdx = rightRaw.search(/[\.,;:\-]/);
284+ if (boundaryIdx !== -1) {
285+ rightRaw = rightRaw.slice(0, boundaryIdx+1);
286+ }
287+ const context = (leftCtx + match.nodeValue + rightRaw).replace(/\s+/g,' ').trim();
288+ H.matches.push({el: span, context});
289+ if (H.matches.length >= MAX_MATCHES) break;
290+ s = afterVal;
291+ }
292+ if (H.matches.length >= MAX_MATCHES) break;
293+ }
294+ H.selectedIndex = H.matches.length ? 0 : -1;
295+ H.paintList();
296+ H.revealSelection();
297+ },
298+ revealSelection(){
299+ const prev = H.matches[H.activeIndex|0];
300+ if (prev && prev.el && prev.el.classList) { prev.el.classList.remove('dumber-find-active'); }
301+ const m = H.matches[H.selectedIndex|0]; if (!m) return;
302+ if (m.el && m.el.classList) { m.el.classList.add('dumber-find-active'); }
303+ H.activeIndex = H.selectedIndex|0;
304+ try { m.el.scrollIntoView({block:'center', inline:'nearest'}); } catch(_){ m.el.scrollIntoView(); }
305+ },
306+ jump(delta){
307+ const n = H.matches.length; if (!n) return;
308+ H.selectedIndex = ((H.selectedIndex||0) + delta) % n; if (H.selectedIndex<0) H.selectedIndex = n-1;
309+ H.paintList(); H.revealSelection();
310+ }
311+ };
312+
313+ // Keyboard hooks
314+ window.addEventListener('keydown', (e)=>{
315+ const mod = (e.ctrlKey||e.metaKey);
316+ if (mod && (e.key==='l' || e.key==='L')) { e.preventDefault(); H.open('omnibox'); }
317+ if (mod && (e.key==='f' || e.key==='F')) { e.preventDefault(); H.open('find'); }
318+ }, true);
319+
320+ // Public API for native
321+ window.__dumber_setSuggestions = (arr)=> H.setSuggestions(arr);
322+ window.__dumber_toggle = ()=> H.toggle();
323+ window.__dumber_find_open = (q)=> H.open('find', q||'');
324+ window.__dumber_find_close = ()=> H.close();
325+ window.__dumber_find_query = (q)=> { if (H.mode!=='find') H.setMode('find'); if (!H.visible) H.toggle(true); H.input.value = q||''; H.find(q||''); };
326+
327+ // Performance optimization: Pre-mount DOM elements immediately for instant show/hide
328+ if (document.readyState === 'loading') {
329+ document.addEventListener('DOMContentLoaded', ()=> H.mount());
330+ } else {
331+ H.mount(); // DOM already ready
332+ }
333+ } catch (e) { /* no-op */ }
334+})();`
335+}
@@ -0,0 +1,335 @@
1+//go:build webkit_cgo
2+
3+package webkit
4+
5+// getOmniboxScript returns the injected omnibox/find component script.
6+// Kept in a cgo-tagged file to ensure availability when referenced
7+// from other cgo files in this package.
8+func getOmniboxScript() string {
9+ return `(() => {
10+ try {
11+ if (window.__dumber_omnibox_loaded) return; // idempotent
12+ window.__dumber_omnibox_loaded = true;
13+
14+ const MAX_MATCHES = 2000;
15+ const H = {
16+ el: null,
17+ box: null,
18+ input: null,
19+ list: null,
20+ visible: false,
21+ mode: 'omnibox', // 'omnibox' | 'find'
22+ suggestions: [],
23+ matches: [], // {el, context}
24+ selectedIndex: -1,
25+ activeIndex: -1,
26+ debounceTimer: 0,
27+ highlightNodes: [],
28+ faded: false,
29+ prevOverflow: '',
30+ post(msg){ try { window.webkit?.messageHandlers?.dumber?.postMessage(JSON.stringify(msg)); } catch(_){} },
31+ render(){
32+ // Performance optimization: elements are pre-mounted, just show/hide
33+ H.el.style.display = H.visible ? 'block' : 'none';
34+ if (!H.visible) return;
35+ H.input.placeholder = H.mode === 'find' ? 'Find in page…' : 'Type URL or search…';
36+ H.input.focus();
37+ },
38+ setMode(m){ H.mode = m === 'find' ? 'find' : 'omnibox'; H.selectedIndex = -1; H.setFaded(false); H.paintList(); },
39+ open(mode, initial){ H.setMode(mode||'omnibox'); H.toggle(true); if (typeof initial==='string') { H.input.value = initial; H.onInput(); } },
40+ close(){ if (H.mode==='find') H.clearHighlights(); H.setFaded(false); H.toggle(false); },
41+ toggle(v){
42+ const newState = (typeof v==='boolean')? v : !H.visible;
43+ if (newState && !H.visible) {
44+ // Disable page scrolling while overlay is visible
45+ H.prevOverflow = document.documentElement.style.overflow;
46+ document.documentElement.style.overflow = 'hidden';
47+ } else if (!newState && H.visible) {
48+ // Restore page scrolling on close
49+ document.documentElement.style.overflow = H.prevOverflow || '';
50+ H.prevOverflow = '';
51+ }
52+ H.visible = newState;
53+ H.render();
54+ },
55+ setFaded(v){
56+ H.faded = !!v;
57+ if (!H.box || !H.input || !H.list) return;
58+ if (H.faded) {
59+ H.box.style.background = 'rgba(27,27,27,0.25)';
60+ H.box.style.backdropFilter = 'blur(2px) saturate(110%)';
61+ H.box.style.webkitBackdropFilter = 'blur(2px) saturate(110%)';
62+ H.input.style.background = 'rgba(18,18,18,0.35)';
63+ H.input.style.color = '#eee';
64+ } else {
65+ H.box.style.background = '#1b1b1b';
66+ H.box.style.backdropFilter = '';
67+ H.box.style.webkitBackdropFilter = '';
68+ H.input.style.background = '#121212';
69+ H.input.style.color = '#eee';
70+ }
71+ },
72+ scrollListToSelection(){
73+ if (!H.list) return;
74+ let idx = H.selectedIndex|0;
75+ if (idx < 0) return;
76+ // Account for header in find mode
77+ const childIdx = (H.mode==='find' ? 1 : 0) + idx;
78+ const el = H.list.children[childIdx];
79+ if (el && el.scrollIntoView) {
80+ try { el.scrollIntoView({block:'nearest'}); } catch(_) { el.scrollIntoView(); }
81+ }
82+ },
83+ mount(){
84+ const root = document.createElement('div');
85+ root.id = 'dumber-omnibox-root';
86+ root.style.cssText = 'position:fixed;inset:0;z-index:2147483647;display:none;';
87+ const box = document.createElement('div');
88+ box.style.cssText = 'width:min(90vw, 720px);margin:8vh auto;padding:8px 12px;background:#1b1b1b;color:#eee;border:1px solid #444;border-radius:8px;box-shadow:0 10px 30px rgba(0,0,0,.6);font-family:system-ui,-apple-system,Segoe UI,Roboto,Ubuntu,"Helvetica Neue",Arial,sans-serif;';
89+ const input = document.createElement('input');
90+ input.type = 'text';
91+ input.placeholder = 'Type URL or search…';
92+ input.style.cssText = 'width:100%;box-sizing:border-box;padding:10px 12px;border-radius:6px;border:1px solid #555;background:#121212;color:#eee;font-size:16px;outline:none;';
93+ const list = document.createElement('div');
94+ list.style.cssText = 'margin-top:8px;max-height:50vh;overflow:auto;';
95+ const style = document.createElement('style');
96+ style.textContent = '.dumber-find-highlight{background:#ffeb3b;color:#000;padding:0 1px;border-radius:2px;box-shadow:0 0 0 1px #c8b900 inset}.dumber-find-active{background:#ff9800 !important;color:#000;box-shadow:0 0 0 1px #b36b00 inset}';
97+ document.documentElement.appendChild(style);
98+ box.appendChild(input); box.appendChild(list); root.appendChild(box); document.documentElement.appendChild(root);
99+ H.box = box;
100+ // Dynamic style updater for responsive behavior
101+ const updateStyles = () => {
102+ const vw = window.innerWidth;
103+ const isSmall = vw < 640;
104+ const isLarge = vw > 1920;
105+
106+ if (box) {
107+ box.style.width = isSmall ? '95vw' : isLarge ? 'min(90vw, 840px)' : 'min(90vw, 720px)';
108+ box.style.padding = isSmall ? '6px 8px' : '8px 12px';
109+ }
110+
111+ if (input) {
112+ input.style.fontSize = isSmall ? '14px' : isLarge ? '17px' : '16px';
113+ input.style.padding = isSmall ? '8px 10px' : '10px 12px';
114+ input.style.boxSizing = 'border-box';
115+ }
116+ };
117+ updateStyles();
118+ window.addEventListener('resize', updateStyles);
119+ // Click outside the box closes the overlay
120+ root.addEventListener('mousedown', (e)=>{
121+ const tgt = e.target;
122+ if (tgt && box && !box.contains(tgt)) {
123+ e.preventDefault();
124+ e.stopPropagation();
125+ H.close();
126+ }
127+ }, true);
128+ box.addEventListener('mousedown', (e)=>{ e.stopPropagation(); }, true);
129+ // Focus and select input on hover for quick typing
130+ input.addEventListener('mouseenter', ()=>{ if (H.visible) { try { input.focus({preventScroll:true}); input.select(); } catch(_) { input.focus(); } } });
131+ root.addEventListener('mouseenter', ()=>{ if (H.visible) { try { input.focus({preventScroll:true}); } catch(_) { input.focus(); } H.setFaded(false); } });
132+ // Clear fading when user goes back to input typing
133+ input.addEventListener('focus', ()=> H.setFaded(false));
134+ input.addEventListener('keydown', (e)=>{
135+ if (e.key === 'Escape'){ H.close(); }
136+ else if (H.mode==='omnibox' && e.key === 'Enter'){
137+ const pick = H.suggestions && H.suggestions[H.selectedIndex|0];
138+ const v = (pick && pick.url) || input.value || '';
139+ if (v) H.post({type:'navigate', url:v});
140+ H.toggle(false);
141+ } else if (H.mode==='find' && e.key === 'Enter'){
142+ e.preventDefault();
143+ if (e.shiftKey) {
144+ H.jump(-1);
145+ } else if (e.altKey) {
146+ // Center on current match but keep overlay open for further navigation
147+ H.revealSelection();
148+ } else {
149+ // Default: center and close
150+ H.revealSelection();
151+ H.close();
152+ }
153+ } else if (e.key === 'ArrowDown' || e.key === 'ArrowUp'){
154+ e.preventDefault(); e.stopPropagation();
155+ const n = (H.mode==='find'? H.matches.length : H.suggestions.length);
156+ if (n){
157+ H.selectedIndex = (H.selectedIndex||0) + (e.key==='ArrowDown'?1:-1);
158+ if (H.selectedIndex<0) H.selectedIndex = n-1; if (H.selectedIndex>=n) H.selectedIndex = 0;
159+ H.paintList();
160+ H.scrollListToSelection();
161+ // Fade overlay while navigating results; also reveal active match in find mode
162+ H.setFaded(true);
163+ if (H.mode==='find') { H.revealSelection(); }
164+ }
165+ } else {
166+ // Any other key input should restore full opacity while editing
167+ H.setFaded(false);
168+ }
169+ });
170+ input.addEventListener('input', ()=> { H.onInput(); H.setFaded(false); });
171+ input.addEventListener('mousedown', ()=> H.setFaded(false));
172+ input.addEventListener('click', ()=> H.setFaded(false));
173+ H.el = root; H.input = input; H.list = list; H.selectedIndex = -1; H.paintList();
174+ },
175+ onInput(){
176+ const q = H.input.value || '';
177+ if (H.mode === 'omnibox'){
178+ clearTimeout(H.debounceTimer);
179+ H.debounceTimer = setTimeout(()=> H.post({type:'query', q, limit:10}), 120);
180+ } else {
181+ H.find(q);
182+ }
183+ },
184+ paintList(){
185+ const list = H.list; if (!list) return; list.textContent = '';
186+
187+ // Show/hide separator based on content
188+ const hasContent = (H.mode==='omnibox' && H.suggestions.length > 0) || (H.mode==='find' && H.matches.length > 0);
189+ list.style.borderTop = hasContent ? '1px solid #333' : 'none';
190+
191+ if (H.mode==='omnibox'){
192+ H.suggestions.forEach((s, i)=>{
193+ const item = document.createElement('div');
194+ item.style.cssText = 'padding:8px 10px;display:flex;gap:10px;align-items:center;cursor:pointer;border-bottom:1px solid #2a2a2a;'+(i===H.selectedIndex?'background:#0a0a0a;':'');
195+ // Favicon chip (wrapper ensures perfect circle sizing)
196+ const chip = document.createElement('div');
197+ chip.style.cssText = 'flex:0 0 20px;width:20px;height:20px;border-radius:50%;background:#ccc;border:1px solid rgba(0,0,0,.12);box-shadow:0 1px 2px rgba(0,0,0,.12);display:flex;align-items:center;justify-content:center;';
198+ const icon = document.createElement('img');
199+ icon.src = s.favicon || '';
200+ icon.loading = 'lazy';
201+ icon.style.cssText = 'width:16px;height:16px;filter:brightness(1.06) contrast(1.03);image-rendering:-webkit-optimize-contrast;';
202+ icon.onerror = ()=>{ chip.style.display='none'; };
203+ chip.appendChild(icon);
204+ // Text line: Domain | full path (one line, fade at end)
205+ let domain = '', path = '';
206+ try { const u = new URL(s.url, window.location.href); domain = u.hostname; path = (u.pathname||'') + (u.search||'') + (u.hash||''); } catch(_) { domain = s.url || ''; }
207+ const text = document.createElement('div');
208+ text.style.cssText = 'flex:1;min-width:0;display:flex;gap:8px;align-items:center;white-space:nowrap;overflow:hidden;';
209+ // Apply gradient fade using mask for WebKit
210+ text.style.webkitMaskImage = 'linear-gradient(90deg, black 85%, transparent 100%)';
211+ text.style.maskImage = 'linear-gradient(90deg, black 85%, transparent 100%)';
212+ const domainEl = document.createElement('span'); domainEl.textContent = domain; domainEl.style.cssText = 'color:#e6e6e6;opacity:.95;';
213+ const sep = document.createElement('span'); sep.textContent = ' | '; sep.style.cssText = 'color:#777;';
214+ const pathEl = document.createElement('span'); pathEl.textContent = path || '/'; pathEl.style.cssText = 'color:#9ad;';
215+ text.appendChild(domainEl); text.appendChild(sep); text.appendChild(pathEl);
216+ item.appendChild(chip); item.appendChild(text);
217+ item.addEventListener('mouseenter', ()=>{ H.selectedIndex = i; H.paintList(); H.scrollListToSelection(); H.setFaded(true); });
218+ item.addEventListener('click',()=>{ H.post({type:'navigate', url:s.url}); H.toggle(false); });
219+ list.appendChild(item);
220+ });
221+ } else {
222+ const total = H.matches.length;
223+ const header = document.createElement('div');
224+ header.textContent = total ? total + ' matches' : 'No matches';
225+ header.style.cssText = 'padding:6px 10px;color:#bbb;font-size:12px;border-bottom:1px solid #2a2a2a;';
226+ list.appendChild(header);
227+ H.matches.forEach((m, i)=>{
228+ const item = document.createElement('div');
229+ item.style.cssText = 'padding:8px 10px;cursor:pointer;border-bottom:1px solid #2a2a2a;'+(i===H.selectedIndex?'background:#0a0a0a;':'');
230+ const ctx = document.createElement('div'); ctx.textContent = m.context || ''; ctx.style.cssText = 'color:#ddd;white-space:nowrap;overflow:hidden;text-overflow:ellipsis;';
231+ item.appendChild(ctx);
232+ item.addEventListener('mouseenter', ()=>{ H.selectedIndex = i; H.paintList(); H.scrollListToSelection(); H.setFaded(true); });
233+ item.addEventListener('click',()=>{ H.selectedIndex=i; H.revealSelection(); H.paintList(); H.close(); });
234+ list.appendChild(item);
235+ });
236+ }
237+ },
238+ setSuggestions(arr){ H.suggestions = Array.isArray(arr)? arr: []; H.selectedIndex = -1; H.paintList(); },
239+ // FIND MODE IMPLEMENTATION
240+ clearHighlights(){
241+ try {
242+ H.highlightNodes.forEach(({span, text})=>{
243+ const p = span.parentNode; if (!p) return; p.replaceChild(text, span); p.normalize();
244+ });
245+ } catch(_){}
246+ H.highlightNodes = [];
247+ H.matches = [];
248+ H.selectedIndex = -1;
249+ H.activeIndex = -1;
250+ H.paintList();
251+ },
252+ find(q){
253+ H.clearHighlights();
254+ q = (q||'').trim(); if (!q) return;
255+ const body = document.body; if (!body) return;
256+ const root = document.getElementById('dumber-omnibox-root');
257+ const walker = document.createTreeWalker(body, NodeFilter.SHOW_TEXT, {
258+ acceptNode(node){
259+ if (!node.nodeValue || !node.nodeValue.trim()) return NodeFilter.FILTER_REJECT;
260+ let el = node.parentElement; if (!el) return NodeFilter.FILTER_REJECT;
261+ if (root && root.contains(el)) return NodeFilter.FILTER_REJECT;
262+ const name = el.tagName; if (name==='SCRIPT'||name==='STYLE'||name==='NOSCRIPT') return NodeFilter.FILTER_REJECT;
263+ if (getComputedStyle(el).visibility==='hidden' || getComputedStyle(el).display==='none') return NodeFilter.FILTER_REJECT;
264+ return NodeFilter.FILTER_ACCEPT;
265+ }
266+ });
267+ const lc = q.toLowerCase();
268+ while (walker.nextNode()){
269+ const text = walker.currentNode;
270+ let s = text.nodeValue; let i = 0;
271+ while (s && (i = s.toLowerCase().indexOf(lc)) !== -1) {
272+ const before = document.createTextNode(s.slice(0, i));
273+ const match = document.createTextNode(s.slice(i, i+q.length));
274+ const afterVal = s.slice(i+q.length);
275+ const span = document.createElement('span'); span.className = 'dumber-find-highlight'; span.appendChild(match);
276+ const parent = text.parentNode; if (!parent) break;
277+ parent.insertBefore(before, text);
278+ parent.insertBefore(span, text);
279+ text.nodeValue = afterVal;
280+ H.highlightNodes.push({span, text: match});
281+ const leftCtx = before.nodeValue.slice(-30);
282+ let rightRaw = afterVal.slice(0, 30);
283+ const boundaryIdx = rightRaw.search(/[\.,;:\-]/);
284+ if (boundaryIdx !== -1) {
285+ rightRaw = rightRaw.slice(0, boundaryIdx+1);
286+ }
287+ const context = (leftCtx + match.nodeValue + rightRaw).replace(/\s+/g,' ').trim();
288+ H.matches.push({el: span, context});
289+ if (H.matches.length >= MAX_MATCHES) break;
290+ s = afterVal;
291+ }
292+ if (H.matches.length >= MAX_MATCHES) break;
293+ }
294+ H.selectedIndex = H.matches.length ? 0 : -1;
295+ H.paintList();
296+ H.revealSelection();
297+ },
298+ revealSelection(){
299+ const prev = H.matches[H.activeIndex|0];
300+ if (prev && prev.el && prev.el.classList) { prev.el.classList.remove('dumber-find-active'); }
301+ const m = H.matches[H.selectedIndex|0]; if (!m) return;
302+ if (m.el && m.el.classList) { m.el.classList.add('dumber-find-active'); }
303+ H.activeIndex = H.selectedIndex|0;
304+ try { m.el.scrollIntoView({block:'center', inline:'nearest'}); } catch(_){ m.el.scrollIntoView(); }
305+ },
306+ jump(delta){
307+ const n = H.matches.length; if (!n) return;
308+ H.selectedIndex = ((H.selectedIndex||0) + delta) % n; if (H.selectedIndex<0) H.selectedIndex = n-1;
309+ H.paintList(); H.revealSelection();
310+ }
311+ };
312+
313+ // Keyboard hooks
314+ window.addEventListener('keydown', (e)=>{
315+ const mod = (e.ctrlKey||e.metaKey);
316+ if (mod && (e.key==='l' || e.key==='L')) { e.preventDefault(); H.open('omnibox'); }
317+ if (mod && (e.key==='f' || e.key==='F')) { e.preventDefault(); H.open('find'); }
318+ }, true);
319+
320+ // Public API for native
321+ window.__dumber_setSuggestions = (arr)=> H.setSuggestions(arr);
322+ window.__dumber_toggle = ()=> H.toggle();
323+ window.__dumber_find_open = (q)=> H.open('find', q||'');
324+ window.__dumber_find_close = ()=> H.close();
325+ window.__dumber_find_query = (q)=> { if (H.mode!=='find') H.setMode('find'); if (!H.visible) H.toggle(true); H.input.value = q||''; H.find(q||''); };
326+
327+ // Performance optimization: Pre-mount DOM elements immediately for instant show/hide
328+ if (document.readyState === 'loading') {
329+ document.addEventListener('DOMContentLoaded', ()=> H.mount());
330+ } else {
331+ H.mount(); // DOM already ready
332+ }
333+ } catch (e) { /* no-op */ }
334+})();`
335+}
M pkg/webkit/webview.go +15 -0
@@ -140,3 +140,18 @@ func (w *WebView) dispatchZoomChanged(level float64) {
140140 w.zoomHandler(level)
141141 }
142142 }
143+
144+// InitializeContentBlocking initializes WebKit content blocking with filter manager (stub)
145+func (w *WebView) InitializeContentBlocking(filterManager interface{}) error {
146+ return ErrNotImplemented
147+}
148+
149+// OnNavigate sets up domain-specific cosmetic filtering on navigation (stub)
150+func (w *WebView) OnNavigate(url string, filterManager interface{}) {
151+ // No-op in stub
152+}
153+
154+// UpdateContentFilters updates the content filters dynamically (stub)
155+func (w *WebView) UpdateContentFilters(filterManager interface{}) error {
156+ return ErrNotImplemented
157+}
@@ -140,3 +140,18 @@ func (w *WebView) dispatchZoomChanged(level float64) {
140 w.zoomHandler(level)140 w.zoomHandler(level)
141 }141 }
142 }142 }
143+
144+// InitializeContentBlocking initializes WebKit content blocking with filter manager (stub)
145+func (w *WebView) InitializeContentBlocking(filterManager interface{}) error {
146+ return ErrNotImplemented
147+}
148+
149+// OnNavigate sets up domain-specific cosmetic filtering on navigation (stub)
150+func (w *WebView) OnNavigate(url string, filterManager interface{}) {
151+ // No-op in stub
152+}
153+
154+// UpdateContentFilters updates the content filters dynamically (stub)
155+func (w *WebView) UpdateContentFilters(filterManager interface{}) error {
156+ return ErrNotImplemented
157+}
M pkg/webkit/webview_cgo.go +59 -24
@@ -948,11 +948,17 @@ func NewWebView(cfg *Config) (*WebView, error) {
948948 _ = v.SetZoom(nz)
949949 })
950950 _ = v.RegisterKeyboardShortcut("cmdorctrl+0", func() { _ = v.SetZoom(1.0) })
951- // Find in page (Ctrl/Cmd+F) reuses omnibox component in find mode
952- _ = v.RegisterKeyboardShortcut("cmdorctrl+f", func() { _ = v.OpenFind("") })
953- // Navigation with Alt+Arrow keys
954- _ = v.RegisterKeyboardShortcut("alt+ArrowLeft", func() { _ = v.GoBack() })
955- _ = v.RegisterKeyboardShortcut("alt+ArrowRight", func() { _ = v.GoForward() })
951+ // Find in page (Ctrl/Cmd+F): use new keyboard service bridge
952+ _ = v.RegisterKeyboardShortcut("cmdorctrl+f", func() {
953+ _ = v.InjectScript("window.__dumber_keyboard && window.__dumber_keyboard.handleNativeShortcut('cmdorctrl+f')")
954+ })
955+ // Navigation with Alt+Arrow keys: use new keyboard service bridge
956+ _ = v.RegisterKeyboardShortcut("alt+ArrowLeft", func() {
957+ _ = v.InjectScript("window.__dumber_keyboard && window.__dumber_keyboard.handleNativeShortcut('alt+arrowleft')")
958+ })
959+ _ = v.RegisterKeyboardShortcut("alt+ArrowRight", func() {
960+ _ = v.InjectScript("window.__dumber_keyboard && window.__dumber_keyboard.handleNativeShortcut('alt+arrowright')")
961+ })
956962 // Update window title when page title changes
957963 C.connect_title_notify(viewWidget, (*C.GtkWindow)(unsafe.Pointer(win)))
958964 // Also dispatch title change to Go with view id
@@ -1235,6 +1241,7 @@ func (w *WebView) dispatchZoomChanged(level float64) {
12351241 }
12361242 }
12371243
1244+
12381245 // enableUserContentManager registers the 'dumber' message handler and injects the omnibox script.
12391246 func (w *WebView) enableUserContentManager(cfg *Config) {
12401247 if w == nil || w.native == nil || w.native.ucm == nil {
@@ -1248,6 +1255,7 @@ func (w *WebView) enableUserContentManager(cfg *Config) {
12481255 }
12491256 C.free(unsafe.Pointer(cname))
12501257
1258+
12511259 // Inject color-scheme preference script at document-start to inform sites of system theme
12521260 preferDark := C.gtk_prefers_dark() != 0
12531261 if preferDark {
@@ -1263,37 +1271,64 @@ func (w *WebView) enableUserContentManager(cfg *Config) {
12631271 }
12641272 cScheme := C.CString(schemeJS)
12651273 defer C.free(unsafe.Pointer(cScheme))
1266- schemeScript := C.webkit_user_script_new((*C.gchar)(cScheme), C.WEBKIT_USER_CONTENT_INJECT_ALL_FRAMES, C.WEBKIT_USER_SCRIPT_INJECT_AT_DOCUMENT_START, nil, nil)
1274+ schemeScript := C.webkit_user_script_new((*C.gchar)(cScheme), C.WEBKIT_USER_CONTENT_INJECT_TOP_FRAME, C.WEBKIT_USER_SCRIPT_INJECT_AT_DOCUMENT_START, nil, nil)
12671275 if schemeScript != nil {
12681276 C.webkit_user_content_manager_add_script(w.native.ucm, schemeScript)
12691277 C.webkit_user_script_unref(schemeScript)
12701278 }
12711279
1272- // Add user script at document-start (omnibox/find reusable component)
1273- src := C.CString(getOmniboxScript())
1274- defer C.free(unsafe.Pointer(src))
1275- script := C.webkit_user_script_new((*C.gchar)(src), C.WEBKIT_USER_CONTENT_INJECT_ALL_FRAMES, C.WEBKIT_USER_SCRIPT_INJECT_AT_DOCUMENT_START, nil, nil)
1276- if script != nil {
1277- C.webkit_user_content_manager_add_script(w.native.ucm, script)
1278- C.webkit_user_script_unref(script)
1279- log.Printf("[webkit] UCM omnibox script injected at document-start")
1280- }
1280+ // Add GUI bundle as user script at document-start (contains toast, omnibox, and controls)
1281+ if cfg != nil && cfg.Assets != nil {
1282+ log.Printf("[webkit] Attempting to load GUI bundle from assets/gui/gui.min.js")
1283+ if guiBytes, err := cfg.Assets.ReadFile("assets/gui/gui.min.js"); err == nil {
1284+ guiScript := string(guiBytes)
1285+ log.Printf("[webkit] GUI bundle loaded successfully, size: %d bytes", len(guiBytes))
1286+
1287+ cGui := C.CString(guiScript)
1288+ defer C.free(unsafe.Pointer(cGui))
1289+
1290+ log.Printf("[webkit] Creating GUI user script for document-start injection in main world")
1291+ guiUserScript := C.webkit_user_script_new((*C.gchar)(cGui), C.WEBKIT_USER_CONTENT_INJECT_TOP_FRAME, C.WEBKIT_USER_SCRIPT_INJECT_AT_DOCUMENT_START, nil, nil)
1292+ if guiUserScript != nil {
1293+ C.webkit_user_content_manager_add_script(w.native.ucm, guiUserScript)
1294+ C.webkit_user_script_unref(guiUserScript)
1295+ log.Printf("[webkit] GUI bundle successfully injected as user script at document-start in main world")
1296+ } else {
1297+ log.Printf("[webkit] ERROR: Failed to create GUI user script")
1298+ }
1299+ } else {
1300+ log.Printf("[webkit] ERROR: Failed to load GUI bundle from assets: %v", err)
1301+ }
12811302
1282- // Add user script at document-start (toast notification system)
1283- toastSrc := C.CString(getToastScript())
1284- defer C.free(unsafe.Pointer(toastSrc))
1285- toastScript := C.webkit_user_script_new((*C.gchar)(toastSrc), C.WEBKIT_USER_CONTENT_INJECT_ALL_FRAMES, C.WEBKIT_USER_SCRIPT_INJECT_AT_DOCUMENT_START, nil, nil)
1286- if toastScript != nil {
1287- C.webkit_user_content_manager_add_script(w.native.ucm, toastScript)
1288- C.webkit_user_script_unref(toastScript)
1289- log.Printf("[webkit] UCM toast script injected at document-start")
1303+ // Add GUI CSS styles as user stylesheet using WebKit native API
1304+ if cssBytes, err := cfg.Assets.ReadFile("assets/gui/style.css"); err == nil {
1305+ cssContent := string(cssBytes)
1306+ cCss := C.CString(cssContent)
1307+ defer C.free(unsafe.Pointer(cCss))
1308+
1309+ styleSheet := C.webkit_user_style_sheet_new(
1310+ (*C.gchar)(cCss),
1311+ C.WEBKIT_USER_CONTENT_INJECT_TOP_FRAME,
1312+ C.WEBKIT_USER_STYLE_LEVEL_AUTHOR,
1313+ nil, // allow_list
1314+ nil, // block_list
1315+ )
1316+ if styleSheet != nil {
1317+ C.webkit_user_content_manager_add_style_sheet(w.native.ucm, styleSheet)
1318+ C.webkit_user_style_sheet_unref(styleSheet)
1319+ } else {
1320+ log.Printf("[webkit] ERROR: Failed to create GUI stylesheet")
1321+ }
1322+ } else {
1323+ log.Printf("[webkit] ERROR: Failed to load GUI CSS from assets: %v", err)
1324+ }
12901325 }
12911326
12921327 // Inject Wails runtime fetch interceptor for homepage bridging
12931328 wailsBridge := `(() => { try { const origFetch = window.fetch.bind(window); const waiters = Object.create(null); window.__dumber_wails_resolve = (id, json) => { const w = waiters[id]; if(!w) return; delete waiters[id]; try { const headers = new Headers({"Content-Type":"application/json"}); w.resolve(new Response(json, { headers })); } catch(e){ w.reject(e); } }; window.fetch = (input, init) => { try { const url = new URL(input instanceof Request ? input.url : input, window.location.origin); if (url.pathname === '/wails/runtime') { const args = url.searchParams.get('args'); let payload = {}; try { payload = args ? JSON.parse(args) : {}; } catch(_){} const id = String(Date.now()) + '-' + Math.random().toString(36).slice(2); return new Promise((resolve, reject) => { waiters[id] = { resolve, reject }; try { window.webkit?.messageHandlers?.dumber?.postMessage(JSON.stringify({ type: 'wails', id, payload })); } catch (e) { reject(e); } }); } } catch(_){} return origFetch(input, init); }; } catch(_){} })();`
12941329 cWails := C.CString(wailsBridge)
12951330 defer C.free(unsafe.Pointer(cWails))
1296- wailsScript := C.webkit_user_script_new((*C.gchar)(cWails), C.WEBKIT_USER_CONTENT_INJECT_ALL_FRAMES, C.WEBKIT_USER_SCRIPT_INJECT_AT_DOCUMENT_START, nil, nil)
1331+ wailsScript := C.webkit_user_script_new((*C.gchar)(cWails), C.WEBKIT_USER_CONTENT_INJECT_TOP_FRAME, C.WEBKIT_USER_SCRIPT_INJECT_AT_DOCUMENT_START, nil, nil)
12971332 if wailsScript != nil {
12981333 C.webkit_user_content_manager_add_script(w.native.ucm, wailsScript)
12991334 C.webkit_user_script_unref(wailsScript)
@@ -948,11 +948,17 @@ func NewWebView(cfg *Config) (*WebView, error) {
948 _ = v.SetZoom(nz)948 _ = v.SetZoom(nz)
949 })949 })
950 _ = v.RegisterKeyboardShortcut("cmdorctrl+0", func() { _ = v.SetZoom(1.0) })950 _ = v.RegisterKeyboardShortcut("cmdorctrl+0", func() { _ = v.SetZoom(1.0) })
951- // Find in page (Ctrl/Cmd+F) reuses omnibox component in find mode951+ // Find in page (Ctrl/Cmd+F): use new keyboard service bridge
952- _ = v.RegisterKeyboardShortcut("cmdorctrl+f", func() { _ = v.OpenFind("") })952+ _ = v.RegisterKeyboardShortcut("cmdorctrl+f", func() {
953- // Navigation with Alt+Arrow keys953+ _ = v.InjectScript("window.__dumber_keyboard && window.__dumber_keyboard.handleNativeShortcut('cmdorctrl+f')")
954- _ = v.RegisterKeyboardShortcut("alt+ArrowLeft", func() { _ = v.GoBack() })954+ })
955- _ = v.RegisterKeyboardShortcut("alt+ArrowRight", func() { _ = v.GoForward() })955+ // Navigation with Alt+Arrow keys: use new keyboard service bridge
956+ _ = v.RegisterKeyboardShortcut("alt+ArrowLeft", func() {
957+ _ = v.InjectScript("window.__dumber_keyboard && window.__dumber_keyboard.handleNativeShortcut('alt+arrowleft')")
958+ })
959+ _ = v.RegisterKeyboardShortcut("alt+ArrowRight", func() {
960+ _ = v.InjectScript("window.__dumber_keyboard && window.__dumber_keyboard.handleNativeShortcut('alt+arrowright')")
961+ })
956 // Update window title when page title changes962 // Update window title when page title changes
957 C.connect_title_notify(viewWidget, (*C.GtkWindow)(unsafe.Pointer(win)))963 C.connect_title_notify(viewWidget, (*C.GtkWindow)(unsafe.Pointer(win)))
958 // Also dispatch title change to Go with view id964 // Also dispatch title change to Go with view id
@@ -1235,6 +1241,7 @@ func (w *WebView) dispatchZoomChanged(level float64) {
1235 }1241 }
1236 }1242 }
1237 1243
1244+
1238 // enableUserContentManager registers the 'dumber' message handler and injects the omnibox script.1245 // enableUserContentManager registers the 'dumber' message handler and injects the omnibox script.
1239 func (w *WebView) enableUserContentManager(cfg *Config) {1246 func (w *WebView) enableUserContentManager(cfg *Config) {
1240 if w == nil || w.native == nil || w.native.ucm == nil {1247 if w == nil || w.native == nil || w.native.ucm == nil {
@@ -1248,6 +1255,7 @@ func (w *WebView) enableUserContentManager(cfg *Config) {
1248 }1255 }
1249 C.free(unsafe.Pointer(cname))1256 C.free(unsafe.Pointer(cname))
1250 1257
1258+
1251 // Inject color-scheme preference script at document-start to inform sites of system theme1259 // Inject color-scheme preference script at document-start to inform sites of system theme
1252 preferDark := C.gtk_prefers_dark() != 01260 preferDark := C.gtk_prefers_dark() != 0
1253 if preferDark {1261 if preferDark {
@@ -1263,37 +1271,64 @@ func (w *WebView) enableUserContentManager(cfg *Config) {
1263 }1271 }
1264 cScheme := C.CString(schemeJS)1272 cScheme := C.CString(schemeJS)
1265 defer C.free(unsafe.Pointer(cScheme))1273 defer C.free(unsafe.Pointer(cScheme))
1266- schemeScript := C.webkit_user_script_new((*C.gchar)(cScheme), C.WEBKIT_USER_CONTENT_INJECT_ALL_FRAMES, C.WEBKIT_USER_SCRIPT_INJECT_AT_DOCUMENT_START, nil, nil)1274+ schemeScript := C.webkit_user_script_new((*C.gchar)(cScheme), C.WEBKIT_USER_CONTENT_INJECT_TOP_FRAME, C.WEBKIT_USER_SCRIPT_INJECT_AT_DOCUMENT_START, nil, nil)
1267 if schemeScript != nil {1275 if schemeScript != nil {
1268 C.webkit_user_content_manager_add_script(w.native.ucm, schemeScript)1276 C.webkit_user_content_manager_add_script(w.native.ucm, schemeScript)
1269 C.webkit_user_script_unref(schemeScript)1277 C.webkit_user_script_unref(schemeScript)
1270 }1278 }
1271 1279
1272- // Add user script at document-start (omnibox/find reusable component)1280+ // Add GUI bundle as user script at document-start (contains toast, omnibox, and controls)
1273- src := C.CString(getOmniboxScript())1281+ if cfg != nil && cfg.Assets != nil {
1274- defer C.free(unsafe.Pointer(src))1282+ log.Printf("[webkit] Attempting to load GUI bundle from assets/gui/gui.min.js")
1275- script := C.webkit_user_script_new((*C.gchar)(src), C.WEBKIT_USER_CONTENT_INJECT_ALL_FRAMES, C.WEBKIT_USER_SCRIPT_INJECT_AT_DOCUMENT_START, nil, nil)1283+ if guiBytes, err := cfg.Assets.ReadFile("assets/gui/gui.min.js"); err == nil {
1276- if script != nil {1284+ guiScript := string(guiBytes)
1277- C.webkit_user_content_manager_add_script(w.native.ucm, script)1285+ log.Printf("[webkit] GUI bundle loaded successfully, size: %d bytes", len(guiBytes))
1278- C.webkit_user_script_unref(script)1286+
1279- log.Printf("[webkit] UCM omnibox script injected at document-start")1287+ cGui := C.CString(guiScript)
1280- }1288+ defer C.free(unsafe.Pointer(cGui))
1289+
1290+ log.Printf("[webkit] Creating GUI user script for document-start injection in main world")
1291+ guiUserScript := C.webkit_user_script_new((*C.gchar)(cGui), C.WEBKIT_USER_CONTENT_INJECT_TOP_FRAME, C.WEBKIT_USER_SCRIPT_INJECT_AT_DOCUMENT_START, nil, nil)
1292+ if guiUserScript != nil {
1293+ C.webkit_user_content_manager_add_script(w.native.ucm, guiUserScript)
1294+ C.webkit_user_script_unref(guiUserScript)
1295+ log.Printf("[webkit] GUI bundle successfully injected as user script at document-start in main world")
1296+ } else {
1297+ log.Printf("[webkit] ERROR: Failed to create GUI user script")
1298+ }
1299+ } else {
1300+ log.Printf("[webkit] ERROR: Failed to load GUI bundle from assets: %v", err)
1301+ }
1281 1302
1282- // Add user script at document-start (toast notification system)1303+ // Add GUI CSS styles as user stylesheet using WebKit native API
1283- toastSrc := C.CString(getToastScript())1304+ if cssBytes, err := cfg.Assets.ReadFile("assets/gui/style.css"); err == nil {
1284- defer C.free(unsafe.Pointer(toastSrc))1305+ cssContent := string(cssBytes)
1285- toastScript := C.webkit_user_script_new((*C.gchar)(toastSrc), C.WEBKIT_USER_CONTENT_INJECT_ALL_FRAMES, C.WEBKIT_USER_SCRIPT_INJECT_AT_DOCUMENT_START, nil, nil)1306+ cCss := C.CString(cssContent)
1286- if toastScript != nil {1307+ defer C.free(unsafe.Pointer(cCss))
1287- C.webkit_user_content_manager_add_script(w.native.ucm, toastScript)1308+
1288- C.webkit_user_script_unref(toastScript)1309+ styleSheet := C.webkit_user_style_sheet_new(
1289- log.Printf("[webkit] UCM toast script injected at document-start")1310+ (*C.gchar)(cCss),
1311+ C.WEBKIT_USER_CONTENT_INJECT_TOP_FRAME,
1312+ C.WEBKIT_USER_STYLE_LEVEL_AUTHOR,
1313+ nil, // allow_list
1314+ nil, // block_list
1315+ )
1316+ if styleSheet != nil {
1317+ C.webkit_user_content_manager_add_style_sheet(w.native.ucm, styleSheet)
1318+ C.webkit_user_style_sheet_unref(styleSheet)
1319+ } else {
1320+ log.Printf("[webkit] ERROR: Failed to create GUI stylesheet")
1321+ }
1322+ } else {
1323+ log.Printf("[webkit] ERROR: Failed to load GUI CSS from assets: %v", err)
1324+ }
1290 }1325 }
1291 1326
1292 // Inject Wails runtime fetch interceptor for homepage bridging1327 // Inject Wails runtime fetch interceptor for homepage bridging
1293 wailsBridge := `(() => { try { const origFetch = window.fetch.bind(window); const waiters = Object.create(null); window.__dumber_wails_resolve = (id, json) => { const w = waiters[id]; if(!w) return; delete waiters[id]; try { const headers = new Headers({"Content-Type":"application/json"}); w.resolve(new Response(json, { headers })); } catch(e){ w.reject(e); } }; window.fetch = (input, init) => { try { const url = new URL(input instanceof Request ? input.url : input, window.location.origin); if (url.pathname === '/wails/runtime') { const args = url.searchParams.get('args'); let payload = {}; try { payload = args ? JSON.parse(args) : {}; } catch(_){} const id = String(Date.now()) + '-' + Math.random().toString(36).slice(2); return new Promise((resolve, reject) => { waiters[id] = { resolve, reject }; try { window.webkit?.messageHandlers?.dumber?.postMessage(JSON.stringify({ type: 'wails', id, payload })); } catch (e) { reject(e); } }); } } catch(_){} return origFetch(input, init); }; } catch(_){} })();`1328 wailsBridge := `(() => { try { const origFetch = window.fetch.bind(window); const waiters = Object.create(null); window.__dumber_wails_resolve = (id, json) => { const w = waiters[id]; if(!w) return; delete waiters[id]; try { const headers = new Headers({"Content-Type":"application/json"}); w.resolve(new Response(json, { headers })); } catch(e){ w.reject(e); } }; window.fetch = (input, init) => { try { const url = new URL(input instanceof Request ? input.url : input, window.location.origin); if (url.pathname === '/wails/runtime') { const args = url.searchParams.get('args'); let payload = {}; try { payload = args ? JSON.parse(args) : {}; } catch(_){} const id = String(Date.now()) + '-' + Math.random().toString(36).slice(2); return new Promise((resolve, reject) => { waiters[id] = { resolve, reject }; try { window.webkit?.messageHandlers?.dumber?.postMessage(JSON.stringify({ type: 'wails', id, payload })); } catch (e) { reject(e); } }); } } catch(_){} return origFetch(input, init); }; } catch(_){} })();`
1294 cWails := C.CString(wailsBridge)1329 cWails := C.CString(wailsBridge)
1295 defer C.free(unsafe.Pointer(cWails))1330 defer C.free(unsafe.Pointer(cWails))
1296- wailsScript := C.webkit_user_script_new((*C.gchar)(cWails), C.WEBKIT_USER_CONTENT_INJECT_ALL_FRAMES, C.WEBKIT_USER_SCRIPT_INJECT_AT_DOCUMENT_START, nil, nil)1331+ wailsScript := C.webkit_user_script_new((*C.gchar)(cWails), C.WEBKIT_USER_CONTENT_INJECT_TOP_FRAME, C.WEBKIT_USER_SCRIPT_INJECT_AT_DOCUMENT_START, nil, nil)
1297 if wailsScript != nil {1332 if wailsScript != nil {
1298 C.webkit_user_content_manager_add_script(w.native.ucm, wailsScript)1333 C.webkit_user_content_manager_add_script(w.native.ucm, wailsScript)
1299 C.webkit_user_script_unref(wailsScript)1334 C.webkit_user_script_unref(wailsScript)

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