feat(workspace): implement multi-pane window management and workspace system
- commit
- b0316ff1214c55048008745144c18918e8cba5ec
- author
- bamen · · 2025-09-22 05:55 UTC
- committer
- bamen · · 2025-09-22 18:37 UTC
- parent
- 66de55a
- signature
- unsigned
24 files · +1555 -259
M Makefile +2 -2
| @@ -43,8 +43,8 @@ build: build-frontend ## Build the application with GUI (frontend assets, then G | |||
| 43 | 43 | $(GOENV) CGO_ENABLED=1 go build -p $(NPROCS) $(LDFLAGS) -tags=webkit_cgo -o $(DIST_DIR)/$(BINARY_NAME) $(MAIN_PATH) | |
| 44 | 44 | @echo "✅ Build successful! Binary: $(DIST_DIR)/$(BINARY_NAME)" | |
| 45 | 45 | ||
| 46 | - | build-frontend: ## Build Svelte GUI with Tailwind CSS | |
| 47 | - | @echo "Building Svelte GUI with Tailwind CSS..." | |
| 46 | + | build-frontend: ## Build Svelte GUI with Tailwind CSS and main-world script | |
| 47 | + | @echo "Building Svelte GUI with Tailwind CSS and main-world script..." | |
| 48 | 48 | @cd gui && npm install --silent && npm run build | |
| 49 | 49 | @echo "GUI build complete" | |
| 50 | 50 | ||
| @@ -43,8 +43,8 @@ build: build-frontend ## Build the application with GUI (frontend assets, then G | |||||
| 43 | $(GOENV) CGO_ENABLED=1 go build -p $(NPROCS) $(LDFLAGS) -tags=webkit_cgo -o $(DIST_DIR)/$(BINARY_NAME) $(MAIN_PATH) | 43 | $(GOENV) CGO_ENABLED=1 go build -p $(NPROCS) $(LDFLAGS) -tags=webkit_cgo -o $(DIST_DIR)/$(BINARY_NAME) $(MAIN_PATH) | ||
| 44 | @echo "✅ Build successful! Binary: $(DIST_DIR)/$(BINARY_NAME)" | 44 | @echo "✅ Build successful! Binary: $(DIST_DIR)/$(BINARY_NAME)" | ||
| 45 | 45 | ||||
| 46 | - | build-frontend: ## Build Svelte GUI with Tailwind CSS | 46 | + | build-frontend: ## Build Svelte GUI with Tailwind CSS and main-world script |
| 47 | - | @echo "Building Svelte GUI with Tailwind CSS..." | 47 | + | @echo "Building Svelte GUI with Tailwind CSS and main-world script..." |
| 48 | @cd gui && npm install --silent && npm run build | 48 | @cd gui && npm install --silent && npm run build | ||
| 49 | @echo "GUI build complete" | 49 | @echo "GUI build complete" | ||
| 50 | 50 | ||||
M gui/package.json +3 -1
| @@ -4,13 +4,15 @@ | |||
| 4 | 4 | "private": true, | |
| 5 | 5 | "type": "module", | |
| 6 | 6 | "scripts": { | |
| 7 | - | "build": "npm run build:color-scheme && npm run build:gui && npm run build:homepage", | |
| 7 | + | "build": "npm run build:color-scheme && npm run build:gui && npm run build:homepage && npm run build:main-world", | |
| 8 | 8 | "build:gui": "vite build --config vite.config.gui.ts", | |
| 9 | 9 | "build:homepage": "vite build --config vite.config.homepage.ts", | |
| 10 | 10 | "build:color-scheme": "vite build --config vite.config.color-scheme.ts", | |
| 11 | + | "build:main-world": "vite build --config vite.config.main-world.ts", | |
| 11 | 12 | "dev": "npm run build:gui -- --watch", | |
| 12 | 13 | "dev:homepage": "npm run build:homepage -- --watch", | |
| 13 | 14 | "dev:color-scheme": "npm run build:color-scheme -- --watch", | |
| 15 | + | "dev:main-world": "npm run build:main-world -- --watch", | |
| 14 | 16 | "check": "svelte-check", | |
| 15 | 17 | "lint": "eslint src --fix", | |
| 16 | 18 | "fmt": "prettier --write .", | |
| @@ -4,13 +4,15 @@ | |||||
| 4 | "private": true, | 4 | "private": true, | ||
| 5 | "type": "module", | 5 | "type": "module", | ||
| 6 | "scripts": { | 6 | "scripts": { | ||
| 7 | - | "build": "npm run build:color-scheme && npm run build:gui && npm run build:homepage", | 7 | + | "build": "npm run build:color-scheme && npm run build:gui && npm run build:homepage && npm run build:main-world", |
| 8 | "build:gui": "vite build --config vite.config.gui.ts", | 8 | "build:gui": "vite build --config vite.config.gui.ts", | ||
| 9 | "build:homepage": "vite build --config vite.config.homepage.ts", | 9 | "build:homepage": "vite build --config vite.config.homepage.ts", | ||
| 10 | "build:color-scheme": "vite build --config vite.config.color-scheme.ts", | 10 | "build:color-scheme": "vite build --config vite.config.color-scheme.ts", | ||
| 11 | + | "build:main-world": "vite build --config vite.config.main-world.ts", | |||
| 11 | "dev": "npm run build:gui -- --watch", | 12 | "dev": "npm run build:gui -- --watch", | ||
| 12 | "dev:homepage": "npm run build:homepage -- --watch", | 13 | "dev:homepage": "npm run build:homepage -- --watch", | ||
| 13 | "dev:color-scheme": "npm run build:color-scheme -- --watch", | 14 | "dev:color-scheme": "npm run build:color-scheme -- --watch", | ||
| 15 | + | "dev:main-world": "npm run build:main-world -- --watch", | |||
| 14 | "check": "svelte-check", | 16 | "check": "svelte-check", | ||
| 15 | "lint": "eslint src --fix", | 17 | "lint": "eslint src --fix", | ||
| 16 | "fmt": "prettier --write .", | 18 | "fmt": "prettier --write .", | ||
M gui/src/components/omnibox/Omnibox.svelte +9 -0
| @@ -284,6 +284,15 @@ | |||
| 284 | 284 | } catch (error) { | |
| 285 | 285 | console.error('Error in findQuery:', error); | |
| 286 | 286 | } | |
| 287 | + | }, | |
| 288 | + | setActive: (active) => { | |
| 289 | + | try { | |
| 290 | + | console.log('🎯 Omnibox setActive called with:', active); | |
| 291 | + | // For now, this is a no-op but maintains API compatibility | |
| 292 | + | // In the future, this could control focus management or visibility | |
| 293 | + | } catch (error) { | |
| 294 | + | console.error('Error in setActive:', error); | |
| 295 | + | } | |
| 287 | 296 | } | |
| 288 | 297 | }; | |
| 289 | 298 | ||
| @@ -284,6 +284,15 @@ | |||||
| 284 | } catch (error) { | 284 | } catch (error) { | ||
| 285 | console.error('Error in findQuery:', error); | 285 | console.error('Error in findQuery:', error); | ||
| 286 | } | 286 | } | ||
| 287 | + | }, | |||
| 288 | + | setActive: (active) => { | |||
| 289 | + | try { | |||
| 290 | + | console.log('🎯 Omnibox setActive called with:', active); | |||
| 291 | + | // For now, this is a no-op but maintains API compatibility | |||
| 292 | + | // In the future, this could control focus management or visibility | |||
| 293 | + | } catch (error) { | |||
| 294 | + | console.error('Error in setActive:', error); | |||
| 295 | + | } | |||
| 287 | } | 296 | } | ||
| 288 | }; | 297 | }; | ||
| 289 | 298 | ||||
M gui/src/components/omnibox/messaging.ts +1 -0
| @@ -98,6 +98,7 @@ declare global { | |||
| 98 | 98 | open: (mode?: string, query?: string) => void; | |
| 99 | 99 | close: () => void; | |
| 100 | 100 | findQuery: (query: string) => void; | |
| 101 | + | setActive: (active: boolean) => void; | |
| 101 | 102 | }; | |
| 102 | 103 | } | |
| 103 | 104 | }⏎̸ | |
| @@ -98,6 +98,7 @@ declare global { | |||||
| 98 | open: (mode?: string, query?: string) => void; | 98 | open: (mode?: string, query?: string) => void; | ||
| 99 | close: () => void; | 99 | close: () => void; | ||
| 100 | findQuery: (query: string) => void; | 100 | findQuery: (query: string) => void; | ||
| 101 | + | setActive: (active: boolean) => void; | |||
| 101 | }; | 102 | }; | ||
| 102 | } | 103 | } | ||
| 103 | }⏎̸ | 104 | }⏎̸ | ||
M gui/src/injected/gui.ts +25 -11
| @@ -9,7 +9,9 @@ | |||
| 9 | 9 | import { initializeToast, type ToastConfig } from './modules/toast'; | |
| 10 | 10 | import { initializeOmnibox, type OmniboxInitConfig } from './modules/omnibox'; | |
| 11 | 11 | import { initializeWorkspace, type WorkspaceConfigPayload, type WorkspaceRuntime } from './modules/workspace'; | |
| 12 | + | import { initializeWindowOpenInterceptor } from './modules/window-open'; | |
| 12 | 13 | import { keyboardService, type KeyboardService } from '$lib/keyboard'; | |
| 14 | + | import type { Suggestion } from '../components/omnibox/types'; | |
| 13 | 15 | // Note: color-scheme module is loaded separately at document-start by WebKit | |
| 14 | 16 | ||
| 15 | 17 | // Global interface for the unified GUI system | |
| @@ -48,6 +50,16 @@ declare global { | |||
| 48 | 50 | __dumber_keyboard?: KeyboardService; | |
| 49 | 51 | __dumber_workspace_config?: WorkspaceConfigPayload; | |
| 50 | 52 | __dumber_workspace?: WorkspaceRuntime; | |
| 53 | + | __dumber_pane?: { id: string; active: boolean }; | |
| 54 | + | __dumber_omnibox?: { | |
| 55 | + | setSuggestions: (suggestions: Suggestion[]) => void; | |
| 56 | + | toggle: () => void; | |
| 57 | + | open: (mode?: string, query?: string) => void; | |
| 58 | + | close: () => void; | |
| 59 | + | findQuery: (query: string) => void; | |
| 60 | + | setActive: (active: boolean) => void; | |
| 61 | + | }; | |
| 62 | + | __dumber_gui_bootstrap?: () => void; | |
| 51 | 63 | // Legacy compatibility functions for Go bridge | |
| 52 | 64 | __dumber_toggle?: () => void; | |
| 53 | 65 | __dumber_find_open?: (query?: string) => void; | |
| @@ -96,10 +108,10 @@ if (!window.__dumber_gui_ready) { | |||
| 96 | 108 | // Handle GUI visibility based on focus | |
| 97 | 109 | if (window.__dumber_omnibox) { | |
| 98 | 110 | if (workspaceHasFocus) { | |
| 99 | - | window.__dumber_omnibox.setActive?.(true); | |
| 111 | + | window.__dumber_omnibox.setActive(true); | |
| 100 | 112 | } else { | |
| 101 | - | window.__dumber_omnibox.setActive?.(false); | |
| 102 | - | window.__dumber_omnibox.close?.(); | |
| 113 | + | window.__dumber_omnibox.setActive(false); | |
| 114 | + | window.__dumber_omnibox.close(); | |
| 103 | 115 | } | |
| 104 | 116 | } | |
| 105 | 117 | }); | |
| @@ -109,6 +121,9 @@ if (!window.__dumber_gui_ready) { | |||
| 109 | 121 | try { | |
| 110 | 122 | // Page-world bridge is injected by WebKit; avoid duplicate injection here | |
| 111 | 123 | ||
| 124 | + | // Initialize window.open interceptor first (must be early) | |
| 125 | + | initializeWindowOpenInterceptor(); | |
| 126 | + | ||
| 112 | 127 | // Initialize toast system first | |
| 113 | 128 | await initializeToast(); | |
| 114 | 129 | ||
| @@ -331,22 +346,21 @@ if (!window.__dumber_gui_ready) { | |||
| 331 | 346 | }; | |
| 332 | 347 | ||
| 333 | 348 | // Bootstrap function for lazy GUI initialization by workspace manager | |
| 334 | - | window.__dumber_gui_bootstrap = function(paneId: string) { | |
| 335 | - | console.log(`[gui-bootstrap] Initializing GUI for pane ${paneId}`); | |
| 349 | + | window.__dumber_gui_bootstrap = function() { | |
| 350 | + | console.log('[gui-bootstrap] Initializing GUI for workspace'); | |
| 336 | 351 | ||
| 337 | - | // Update current pane ID | |
| 338 | - | currentPaneId = paneId; | |
| 352 | + | // Pane ID will be updated via focus events | |
| 339 | 353 | ||
| 340 | 354 | // Ensure omnibox is available for this pane | |
| 341 | 355 | if (!window.__dumber_omnibox) { | |
| 342 | - | console.log(`[gui-bootstrap] Omnibox not yet available for pane ${paneId}, will be loaded when needed`); | |
| 356 | + | console.log(`[gui-bootstrap] Omnibox not yet available for pane ${currentPaneId}, will be loaded when needed`); | |
| 343 | 357 | } else { | |
| 344 | - | console.log(`[gui-bootstrap] Omnibox already available for pane ${paneId}`); | |
| 358 | + | console.log(`[gui-bootstrap] Omnibox already available for pane ${currentPaneId}`); | |
| 345 | 359 | // Ensure omnibox is aware of the current pane | |
| 346 | - | window.__dumber_omnibox.setActive?.(workspaceHasFocus); | |
| 360 | + | window.__dumber_omnibox.setActive(workspaceHasFocus); | |
| 347 | 361 | } | |
| 348 | 362 | ||
| 349 | - | console.log(`[gui-bootstrap] GUI bootstrap complete for pane ${paneId}`); | |
| 363 | + | console.log(`[gui-bootstrap] GUI bootstrap complete for pane ${currentPaneId}`); | |
| 350 | 364 | }; | |
| 351 | 365 | ||
| 352 | 366 | } | |
| @@ -9,7 +9,9 @@ | |||||
| 9 | import { initializeToast, type ToastConfig } from './modules/toast'; | 9 | import { initializeToast, type ToastConfig } from './modules/toast'; | ||
| 10 | import { initializeOmnibox, type OmniboxInitConfig } from './modules/omnibox'; | 10 | import { initializeOmnibox, type OmniboxInitConfig } from './modules/omnibox'; | ||
| 11 | import { initializeWorkspace, type WorkspaceConfigPayload, type WorkspaceRuntime } from './modules/workspace'; | 11 | import { initializeWorkspace, type WorkspaceConfigPayload, type WorkspaceRuntime } from './modules/workspace'; | ||
| 12 | + | import { initializeWindowOpenInterceptor } from './modules/window-open'; | |||
| 12 | import { keyboardService, type KeyboardService } from '$lib/keyboard'; | 13 | import { keyboardService, type KeyboardService } from '$lib/keyboard'; | ||
| 14 | + | import type { Suggestion } from '../components/omnibox/types'; | |||
| 13 | // Note: color-scheme module is loaded separately at document-start by WebKit | 15 | // Note: color-scheme module is loaded separately at document-start by WebKit | ||
| 14 | 16 | ||||
| 15 | // Global interface for the unified GUI system | 17 | // Global interface for the unified GUI system | ||
| @@ -48,6 +50,16 @@ declare global { | |||||
| 48 | __dumber_keyboard?: KeyboardService; | 50 | __dumber_keyboard?: KeyboardService; | ||
| 49 | __dumber_workspace_config?: WorkspaceConfigPayload; | 51 | __dumber_workspace_config?: WorkspaceConfigPayload; | ||
| 50 | __dumber_workspace?: WorkspaceRuntime; | 52 | __dumber_workspace?: WorkspaceRuntime; | ||
| 53 | + | __dumber_pane?: { id: string; active: boolean }; | |||
| 54 | + | __dumber_omnibox?: { | |||
| 55 | + | setSuggestions: (suggestions: Suggestion[]) => void; | |||
| 56 | + | toggle: () => void; | |||
| 57 | + | open: (mode?: string, query?: string) => void; | |||
| 58 | + | close: () => void; | |||
| 59 | + | findQuery: (query: string) => void; | |||
| 60 | + | setActive: (active: boolean) => void; | |||
| 61 | + | }; | |||
| 62 | + | __dumber_gui_bootstrap?: () => void; | |||
| 51 | // Legacy compatibility functions for Go bridge | 63 | // Legacy compatibility functions for Go bridge | ||
| 52 | __dumber_toggle?: () => void; | 64 | __dumber_toggle?: () => void; | ||
| 53 | __dumber_find_open?: (query?: string) => void; | 65 | __dumber_find_open?: (query?: string) => void; | ||
| @@ -96,10 +108,10 @@ if (!window.__dumber_gui_ready) { | |||||
| 96 | // Handle GUI visibility based on focus | 108 | // Handle GUI visibility based on focus | ||
| 97 | if (window.__dumber_omnibox) { | 109 | if (window.__dumber_omnibox) { | ||
| 98 | if (workspaceHasFocus) { | 110 | if (workspaceHasFocus) { | ||
| 99 | - | window.__dumber_omnibox.setActive?.(true); | 111 | + | window.__dumber_omnibox.setActive(true); |
| 100 | } else { | 112 | } else { | ||
| 101 | - | window.__dumber_omnibox.setActive?.(false); | 113 | + | window.__dumber_omnibox.setActive(false); |
| 102 | - | window.__dumber_omnibox.close?.(); | 114 | + | window.__dumber_omnibox.close(); |
| 103 | } | 115 | } | ||
| 104 | } | 116 | } | ||
| 105 | }); | 117 | }); | ||
| @@ -109,6 +121,9 @@ if (!window.__dumber_gui_ready) { | |||||
| 109 | try { | 121 | try { | ||
| 110 | // Page-world bridge is injected by WebKit; avoid duplicate injection here | 122 | // Page-world bridge is injected by WebKit; avoid duplicate injection here | ||
| 111 | 123 | ||||
| 124 | + | // Initialize window.open interceptor first (must be early) | |||
| 125 | + | initializeWindowOpenInterceptor(); | |||
| 126 | + | ||||
| 112 | // Initialize toast system first | 127 | // Initialize toast system first | ||
| 113 | await initializeToast(); | 128 | await initializeToast(); | ||
| 114 | 129 | ||||
| @@ -331,22 +346,21 @@ if (!window.__dumber_gui_ready) { | |||||
| 331 | }; | 346 | }; | ||
| 332 | 347 | ||||
| 333 | // Bootstrap function for lazy GUI initialization by workspace manager | 348 | // Bootstrap function for lazy GUI initialization by workspace manager | ||
| 334 | - | window.__dumber_gui_bootstrap = function(paneId: string) { | 349 | + | window.__dumber_gui_bootstrap = function() { |
| 335 | - | console.log(`[gui-bootstrap] Initializing GUI for pane ${paneId}`); | 350 | + | console.log('[gui-bootstrap] Initializing GUI for workspace'); |
| 336 | 351 | ||||
| 337 | - | // Update current pane ID | 352 | + | // Pane ID will be updated via focus events |
| 338 | - | currentPaneId = paneId; | |||
| 339 | 353 | ||||
| 340 | // Ensure omnibox is available for this pane | 354 | // Ensure omnibox is available for this pane | ||
| 341 | if (!window.__dumber_omnibox) { | 355 | if (!window.__dumber_omnibox) { | ||
| 342 | - | console.log(`[gui-bootstrap] Omnibox not yet available for pane ${paneId}, will be loaded when needed`); | 356 | + | console.log(`[gui-bootstrap] Omnibox not yet available for pane ${currentPaneId}, will be loaded when needed`); |
| 343 | } else { | 357 | } else { | ||
| 344 | - | console.log(`[gui-bootstrap] Omnibox already available for pane ${paneId}`); | 358 | + | console.log(`[gui-bootstrap] Omnibox already available for pane ${currentPaneId}`); |
| 345 | // Ensure omnibox is aware of the current pane | 359 | // Ensure omnibox is aware of the current pane | ||
| 346 | - | window.__dumber_omnibox.setActive?.(workspaceHasFocus); | 360 | + | window.__dumber_omnibox.setActive(workspaceHasFocus); |
| 347 | } | 361 | } | ||
| 348 | 362 | ||||
| 349 | - | console.log(`[gui-bootstrap] GUI bootstrap complete for pane ${paneId}`); | 363 | + | console.log(`[gui-bootstrap] GUI bootstrap complete for pane ${currentPaneId}`); |
| 350 | }; | 364 | }; | ||
| 351 | 365 | ||||
| 352 | } | 366 | } | ||
A gui/src/injected/main-world.ts +402 -0
| @@ -0,0 +1,402 @@ | |||
| 1 | + | /** | |
| 2 | + | * Main-world bridge script for Dumber Browser | |
| 3 | + | * | |
| 4 | + | * This script is injected into the page-world context to provide: | |
| 5 | + | * - Window.open interceptor with debouncing | |
| 6 | + | * - Toast notifications bridge | |
| 7 | + | * - Theme management | |
| 8 | + | * - DOM zoom functionality | |
| 9 | + | * - Omnibox suggestions bridge | |
| 10 | + | */ | |
| 11 | + | ||
| 12 | + | // Type definitions for window extensions | |
| 13 | + | declare global { | |
| 14 | + | interface Window { | |
| 15 | + | __dumber: DumberAPI; | |
| 16 | + | __dumber_page_bridge_installed?: boolean; | |
| 17 | + | __dumber_window_open_intercepted?: boolean; | |
| 18 | + | __dumber_dom_zoom_seed?: number; | |
| 19 | + | __dumber_dom_zoom_level?: number; | |
| 20 | + | __dumber_initial_theme?: string; | |
| 21 | + | __dumber_setTheme?: (theme: 'light' | 'dark') => void; | |
| 22 | + | __dumber_applyDomZoom?: (level: number) => void; | |
| 23 | + | __dumber_showToast?: (message: string, duration?: number, type?: 'info' | 'success' | 'error') => number | void; | |
| 24 | + | __dumber_showZoomToast?: (level: number) => void; | |
| 25 | + | __dumber_omnibox_suggestions?: (suggestions: Suggestion[]) => void; | |
| 26 | + | webkit?: { | |
| 27 | + | messageHandlers?: { | |
| 28 | + | dumber?: { | |
| 29 | + | postMessage: (message: string) => void; | |
| 30 | + | }; | |
| 31 | + | }; | |
| 32 | + | }; | |
| 33 | + | } | |
| 34 | + | } | |
| 35 | + | ||
| 36 | + | interface WindowIntent { | |
| 37 | + | url: string; | |
| 38 | + | target: string; | |
| 39 | + | features: string; | |
| 40 | + | timestamp: number; | |
| 41 | + | userTriggered: boolean; | |
| 42 | + | } | |
| 43 | + | ||
| 44 | + | interface Suggestion { | |
| 45 | + | // Define proper suggestion types based on your needs | |
| 46 | + | [key: string]: unknown; | |
| 47 | + | } | |
| 48 | + | ||
| 49 | + | interface DumberAPI { | |
| 50 | + | toast: { | |
| 51 | + | show: (message: string, duration?: number, type?: 'info' | 'success' | 'error') => void; | |
| 52 | + | zoom: (level: number) => void; | |
| 53 | + | }; | |
| 54 | + | omnibox: { | |
| 55 | + | suggestions: (suggestions: Suggestion[]) => void; | |
| 56 | + | }; | |
| 57 | + | } | |
| 58 | + | ||
| 59 | + | (() => { | |
| 60 | + | try { | |
| 61 | + | // Prevent multiple installations | |
| 62 | + | if (window.__dumber_page_bridge_installed) { | |
| 63 | + | return; | |
| 64 | + | } | |
| 65 | + | window.__dumber_page_bridge_installed = true; | |
| 66 | + | ||
| 67 | + | // Initialize zoom level placeholder (will be replaced by Go) | |
| 68 | + | const initialZoom = 1.0; // __DOM_ZOOM_DEFAULT__ will be replaced by Go | |
| 69 | + | window.__dumber_dom_zoom_seed = initialZoom; | |
| 70 | + | ||
| 71 | + | // Theme setter function for GTK theme integration | |
| 72 | + | window.__dumber_setTheme = (theme: 'light' | 'dark') => { | |
| 73 | + | window.__dumber_initial_theme = theme; | |
| 74 | + | console.log('[dumber] Setting theme to:', theme); | |
| 75 | + | if (theme === 'dark') { | |
| 76 | + | document.documentElement.classList.add('dark'); | |
| 77 | + | } else { | |
| 78 | + | document.documentElement.classList.remove('dark'); | |
| 79 | + | } | |
| 80 | + | }; | |
| 81 | + | ||
| 82 | + | // Initialize unified API object | |
| 83 | + | window.__dumber = window.__dumber || {} as DumberAPI; | |
| 84 | + | ||
| 85 | + | // Toast bridge | |
| 86 | + | window.__dumber.toast = window.__dumber.toast || { | |
| 87 | + | show: (message: string, duration?: number, type?: string) => { | |
| 88 | + | try { | |
| 89 | + | document.dispatchEvent(new CustomEvent('dumber:toast:show', { | |
| 90 | + | detail: { message, duration, type } | |
| 91 | + | })); | |
| 92 | + | // Legacy compatibility | |
| 93 | + | document.dispatchEvent(new CustomEvent('dumber:showToast', { | |
| 94 | + | detail: { message, duration, type } | |
| 95 | + | })); | |
| 96 | + | } catch { | |
| 97 | + | // ignore | |
| 98 | + | } | |
| 99 | + | }, | |
| 100 | + | zoom: (level: number) => { | |
| 101 | + | try { | |
| 102 | + | document.dispatchEvent(new CustomEvent('dumber:toast:zoom', { | |
| 103 | + | detail: { level } | |
| 104 | + | })); | |
| 105 | + | // Legacy compatibility | |
| 106 | + | document.dispatchEvent(new CustomEvent('dumber:showZoomToast', { | |
| 107 | + | detail: { level } | |
| 108 | + | })); | |
| 109 | + | } catch { | |
| 110 | + | // ignore | |
| 111 | + | } | |
| 112 | + | } | |
| 113 | + | }; | |
| 114 | + | ||
| 115 | + | // Legacy toast helpers | |
| 116 | + | window.__dumber_showToast = (message: string, duration?: number, type?: 'info' | 'success' | 'error') => { | |
| 117 | + | window.__dumber.toast.show(message, duration, type); | |
| 118 | + | }; | |
| 119 | + | window.__dumber_showZoomToast = (level: number) => { | |
| 120 | + | window.__dumber.toast.zoom(level); | |
| 121 | + | }; | |
| 122 | + | ||
| 123 | + | // DOM zoom functionality | |
| 124 | + | if (typeof window.__dumber_dom_zoom_level !== 'number') { | |
| 125 | + | window.__dumber_dom_zoom_level = initialZoom; | |
| 126 | + | } | |
| 127 | + | ||
| 128 | + | const applyZoomStyles = (node: HTMLElement, level: number): void => { | |
| 129 | + | if (!node) return; | |
| 130 | + | ||
| 131 | + | if (Math.abs(level - 1.0) < 1e-6) { | |
| 132 | + | // Reset zoom | |
| 133 | + | node.style.removeProperty('zoom'); | |
| 134 | + | node.style.removeProperty('transform'); | |
| 135 | + | node.style.removeProperty('transform-origin'); | |
| 136 | + | node.style.removeProperty('width'); | |
| 137 | + | node.style.removeProperty('min-width'); | |
| 138 | + | node.style.removeProperty('height'); | |
| 139 | + | node.style.removeProperty('min-height'); | |
| 140 | + | return; | |
| 141 | + | } | |
| 142 | + | ||
| 143 | + | const scale = level; | |
| 144 | + | const inversePercent = 100 / scale; | |
| 145 | + | const widthValue = `${inversePercent}%`; | |
| 146 | + | ||
| 147 | + | node.style.removeProperty('zoom'); | |
| 148 | + | node.style.transform = `scale(${scale})`; | |
| 149 | + | node.style.transformOrigin = '0 0'; | |
| 150 | + | node.style.width = widthValue; | |
| 151 | + | node.style.minWidth = widthValue; | |
| 152 | + | node.style.minHeight = '100%'; | |
| 153 | + | }; | |
| 154 | + | ||
| 155 | + | window.__dumber_applyDomZoom = (level: number) => { | |
| 156 | + | try { | |
| 157 | + | window.__dumber_dom_zoom_level = level; | |
| 158 | + | window.__dumber_dom_zoom_seed = level; | |
| 159 | + | applyZoomStyles(document.documentElement, level); | |
| 160 | + | if (document.body) { | |
| 161 | + | applyZoomStyles(document.body, level); | |
| 162 | + | } | |
| 163 | + | } catch (e) { | |
| 164 | + | console.error('[dumber] DOM zoom error', e); | |
| 165 | + | } | |
| 166 | + | }; | |
| 167 | + | ||
| 168 | + | // Apply initial zoom | |
| 169 | + | window.__dumber_applyDomZoom(window.__dumber_dom_zoom_level); | |
| 170 | + | ||
| 171 | + | if (!document.body) { | |
| 172 | + | document.addEventListener('DOMContentLoaded', () => { | |
| 173 | + | if (typeof window.__dumber_dom_zoom_level === 'number') { | |
| 174 | + | window.__dumber_applyDomZoom!(window.__dumber_dom_zoom_level); | |
| 175 | + | } | |
| 176 | + | }, { once: true }); | |
| 177 | + | } | |
| 178 | + | ||
| 179 | + | // Omnibox suggestions bridge | |
| 180 | + | const omniboxQueue: Suggestion[] = []; | |
| 181 | + | let omniboxReady = false; | |
| 182 | + | ||
| 183 | + | const omniboxDispatch = (suggestions: Suggestion[]) => { | |
| 184 | + | try { | |
| 185 | + | document.dispatchEvent(new CustomEvent('dumber:omnibox:suggestions', { | |
| 186 | + | detail: { suggestions } | |
| 187 | + | })); | |
| 188 | + | // Legacy compatibility | |
| 189 | + | document.dispatchEvent(new CustomEvent('dumber:omnibox-suggestions', { | |
| 190 | + | detail: { suggestions } | |
| 191 | + | })); | |
| 192 | + | } catch (e) { | |
| 193 | + | console.error('[dumber] Omnibox dispatch error', e); | |
| 194 | + | } | |
| 195 | + | }; | |
| 196 | + | ||
| 197 | + | window.__dumber_omnibox_suggestions = (suggestions: Suggestion[]) => { | |
| 198 | + | if (omniboxReady) { | |
| 199 | + | omniboxDispatch(suggestions); | |
| 200 | + | } else { | |
| 201 | + | try { | |
| 202 | + | omniboxQueue.push(...suggestions); | |
| 203 | + | } catch (e){ | |
| 204 | + | console.error('[dumber] Omnibox queue error', e); | |
| 205 | + | } | |
| 206 | + | } | |
| 207 | + | }; | |
| 208 | + | ||
| 209 | + | document.addEventListener('dumber:omnibox-ready', () => { | |
| 210 | + | omniboxReady = true; | |
| 211 | + | if (omniboxQueue && omniboxQueue.length) { | |
| 212 | + | const items = omniboxQueue.slice(); | |
| 213 | + | omniboxQueue.length = 0; | |
| 214 | + | items.forEach(s => omniboxDispatch([s]) ); | |
| 215 | + | } | |
| 216 | + | }); | |
| 217 | + | ||
| 218 | + | // Unified omnibox API | |
| 219 | + | window.__dumber.omnibox = window.__dumber.omnibox || { | |
| 220 | + | suggestions: (suggestions: Suggestion[]) => { | |
| 221 | + | window.__dumber_omnibox_suggestions!(suggestions); | |
| 222 | + | } | |
| 223 | + | }; | |
| 224 | + | ||
| 225 | + | // User interaction tracking and window.open interceptor | |
| 226 | + | let lastUserInteraction = 0; | |
| 227 | + | const INTERACTION_TIMEOUT = 5000; // 5 seconds | |
| 228 | + | const WINDOW_OPEN_DEBOUNCE = 1000; // 1 second between calls | |
| 229 | + | const recentWindowOpenCalls = new Map<string, number>(); | |
| 230 | + | ||
| 231 | + | const trackUserInteraction = () => { | |
| 232 | + | lastUserInteraction = Date.now(); | |
| 233 | + | }; | |
| 234 | + | ||
| 235 | + | const isUserInteraction = (): boolean => { | |
| 236 | + | const now = Date.now(); | |
| 237 | + | const timeSinceInteraction = now - lastUserInteraction; | |
| 238 | + | return timeSinceInteraction <= INTERACTION_TIMEOUT; | |
| 239 | + | }; | |
| 240 | + | ||
| 241 | + | const isDuplicateWindowOpen = (url: string): boolean => { | |
| 242 | + | const now = Date.now(); | |
| 243 | + | const lastCall = recentWindowOpenCalls.get(url); | |
| 244 | + | ||
| 245 | + | if (lastCall && (now - lastCall) < WINDOW_OPEN_DEBOUNCE) { | |
| 246 | + | return true; | |
| 247 | + | } | |
| 248 | + | ||
| 249 | + | recentWindowOpenCalls.set(url, now); | |
| 250 | + | ||
| 251 | + | // Clean up old entries | |
| 252 | + | for (const [key, timestamp] of recentWindowOpenCalls.entries()) { | |
| 253 | + | if (now - timestamp > WINDOW_OPEN_DEBOUNCE * 2) { | |
| 254 | + | recentWindowOpenCalls.delete(key); | |
| 255 | + | } | |
| 256 | + | } | |
| 257 | + | ||
| 258 | + | return false; | |
| 259 | + | }; | |
| 260 | + | ||
| 261 | + | // Track user interactions for popup validation | |
| 262 | + | ['click', 'mousedown', 'keydown', 'touchstart'].forEach(eventType => { | |
| 263 | + | document.addEventListener(eventType, trackUserInteraction, true); | |
| 264 | + | }); | |
| 265 | + | ||
| 266 | + | console.log('[window-open] User interaction tracking enabled'); | |
| 267 | + | ||
| 268 | + | // Window.open interceptor - only install once | |
| 269 | + | if (!window.__dumber_window_open_intercepted) { | |
| 270 | + | try { | |
| 271 | + | ||
| 272 | + | window.open = function( | |
| 273 | + | url?: string | URL | null, | |
| 274 | + | target?: string | null, | |
| 275 | + | features?: string | null | |
| 276 | + | ): WindowProxy | null { | |
| 277 | + | console.log('[window-open] Bridge called with:', url, target, features); | |
| 278 | + | console.log('[window-open] Call stack:', new Error().stack); | |
| 279 | + | ||
| 280 | + | const urlString = url ? (typeof url === 'string' ? url : url.toString()) : ''; | |
| 281 | + | ||
| 282 | + | // Check for duplicate calls to prevent multiple panes | |
| 283 | + | if (isDuplicateWindowOpen(urlString)) { | |
| 284 | + | console.log('[window-open] Blocked: duplicate call within debounce window'); | |
| 285 | + | return { | |
| 286 | + | closed: true, | |
| 287 | + | location: { href: urlString }, | |
| 288 | + | close: () => {}, | |
| 289 | + | focus: () => {}, | |
| 290 | + | blur: () => {}, | |
| 291 | + | postMessage: () => {} | |
| 292 | + | } as unknown as WindowProxy; | |
| 293 | + | } | |
| 294 | + | ||
| 295 | + | // Check for user interaction to prevent popup spam | |
| 296 | + | if (!isUserInteraction()) { | |
| 297 | + | console.log('[window-open] Blocked: no recent user interaction detected'); | |
| 298 | + | return { | |
| 299 | + | closed: true, | |
| 300 | + | location: { href: '' }, | |
| 301 | + | close: () => {}, | |
| 302 | + | focus: () => {}, | |
| 303 | + | blur: () => {}, | |
| 304 | + | postMessage: () => {} | |
| 305 | + | } as unknown as WindowProxy; | |
| 306 | + | } | |
| 307 | + | ||
| 308 | + | const originalFeatures = features || ''; | |
| 309 | + | console.log('[window-open] Using original features:', originalFeatures); | |
| 310 | + | ||
| 311 | + | const intent: WindowIntent = { | |
| 312 | + | url: urlString, | |
| 313 | + | target: target || '_blank', | |
| 314 | + | features: originalFeatures, | |
| 315 | + | timestamp: Date.now(), | |
| 316 | + | userTriggered: true | |
| 317 | + | }; | |
| 318 | + | ||
| 319 | + | // Send message to Go to handle popup creation | |
| 320 | + | try { | |
| 321 | + | if (window.webkit?.messageHandlers?.dumber) { | |
| 322 | + | window.webkit.messageHandlers.dumber.postMessage(JSON.stringify({ | |
| 323 | + | type: 'handle-window-open', | |
| 324 | + | payload: intent | |
| 325 | + | })); | |
| 326 | + | console.log('[window-open] Sent window.open request to Go - bypassing WebKit completely'); | |
| 327 | + | } | |
| 328 | + | } catch (e) { | |
| 329 | + | console.error('[window-open] Failed to send to Go:', e); | |
| 330 | + | } | |
| 331 | + | ||
| 332 | + | // Return fake window object - don't call original window.open | |
| 333 | + | console.log('[window-open] Returning fake window object - bypassing WebKit create signal'); | |
| 334 | + | return { | |
| 335 | + | closed: false, | |
| 336 | + | location: { href: urlString }, | |
| 337 | + | close() { (this as { closed: boolean }).closed = true; }, | |
| 338 | + | focus: () => {}, | |
| 339 | + | blur: () => {}, | |
| 340 | + | postMessage: () => {} | |
| 341 | + | } as unknown as WindowProxy; | |
| 342 | + | }; | |
| 343 | + | ||
| 344 | + | window.__dumber_window_open_intercepted = true; | |
| 345 | + | console.log('[window-open] ✅ Page-world bridge installed'); | |
| 346 | + | } catch (error) { | |
| 347 | + | console.error('[window-open] ❌ Failed to install page-world bridge:', error); | |
| 348 | + | } | |
| 349 | + | } | |
| 350 | + | ||
| 351 | + | // Console capture functionality | |
| 352 | + | const setupConsoleCapture = () => { | |
| 353 | + | const originalConsole = { | |
| 354 | + | log: console.log, | |
| 355 | + | warn: console.warn, | |
| 356 | + | error: console.error, | |
| 357 | + | info: console.info, | |
| 358 | + | debug: console.debug | |
| 359 | + | }; | |
| 360 | + | ||
| 361 | + | const sendConsoleMessage = (level: string, args: unknown[]) => { | |
| 362 | + | try { | |
| 363 | + | const message = args.map(arg => | |
| 364 | + | typeof arg === 'object' ? JSON.stringify(arg) : String(arg) | |
| 365 | + | ).join(' '); | |
| 366 | + | ||
| 367 | + | const formattedMessage = `${window.location.href} ${message}`; | |
| 368 | + | ||
| 369 | + | // Send to Go via existing message handler | |
| 370 | + | if (window.webkit?.messageHandlers?.dumber) { | |
| 371 | + | window.webkit.messageHandlers.dumber.postMessage(JSON.stringify({ | |
| 372 | + | type: 'console-message', | |
| 373 | + | payload: { level, message: formattedMessage, url: window.location.href } | |
| 374 | + | })); | |
| 375 | + | } | |
| 376 | + | } catch { | |
| 377 | + | // Silently ignore errors in console capture to avoid infinite loops | |
| 378 | + | } | |
| 379 | + | }; | |
| 380 | + | ||
| 381 | + | const createConsoleWrapper = (originalMethod: (...args: unknown[]) => void, level: string) => { | |
| 382 | + | return function(...args: unknown[]) { | |
| 383 | + | originalMethod.apply(console, args); | |
| 384 | + | sendConsoleMessage(level, args); | |
| 385 | + | }; | |
| 386 | + | }; | |
| 387 | + | ||
| 388 | + | // Override console methods to capture messages | |
| 389 | + | console.log = createConsoleWrapper(originalConsole.log, 'LOG'); | |
| 390 | + | console.warn = createConsoleWrapper(originalConsole.warn, 'WARN'); | |
| 391 | + | console.error = createConsoleWrapper(originalConsole.error, 'ERROR'); | |
| 392 | + | console.info = createConsoleWrapper(originalConsole.info, 'INFO'); | |
| 393 | + | console.debug = createConsoleWrapper(originalConsole.debug, 'DEBUG'); | |
| 394 | + | }; | |
| 395 | + | ||
| 396 | + | // Initialize console capture | |
| 397 | + | setupConsoleCapture(); | |
| 398 | + | ||
| 399 | + | } catch (e) { | |
| 400 | + | console.warn('[dumber] unified bridge init failed', e); | |
| 401 | + | } | |
| 402 | + | })();⏎̸ | |
| @@ -0,0 +1,402 @@ | |||||
| 1 | + | /** | |||
| 2 | + | * Main-world bridge script for Dumber Browser | |||
| 3 | + | * | |||
| 4 | + | * This script is injected into the page-world context to provide: | |||
| 5 | + | * - Window.open interceptor with debouncing | |||
| 6 | + | * - Toast notifications bridge | |||
| 7 | + | * - Theme management | |||
| 8 | + | * - DOM zoom functionality | |||
| 9 | + | * - Omnibox suggestions bridge | |||
| 10 | + | */ | |||
| 11 | + | ||||
| 12 | + | // Type definitions for window extensions | |||
| 13 | + | declare global { | |||
| 14 | + | interface Window { | |||
| 15 | + | __dumber: DumberAPI; | |||
| 16 | + | __dumber_page_bridge_installed?: boolean; | |||
| 17 | + | __dumber_window_open_intercepted?: boolean; | |||
| 18 | + | __dumber_dom_zoom_seed?: number; | |||
| 19 | + | __dumber_dom_zoom_level?: number; | |||
| 20 | + | __dumber_initial_theme?: string; | |||
| 21 | + | __dumber_setTheme?: (theme: 'light' | 'dark') => void; | |||
| 22 | + | __dumber_applyDomZoom?: (level: number) => void; | |||
| 23 | + | __dumber_showToast?: (message: string, duration?: number, type?: 'info' | 'success' | 'error') => number | void; | |||
| 24 | + | __dumber_showZoomToast?: (level: number) => void; | |||
| 25 | + | __dumber_omnibox_suggestions?: (suggestions: Suggestion[]) => void; | |||
| 26 | + | webkit?: { | |||
| 27 | + | messageHandlers?: { | |||
| 28 | + | dumber?: { | |||
| 29 | + | postMessage: (message: string) => void; | |||
| 30 | + | }; | |||
| 31 | + | }; | |||
| 32 | + | }; | |||
| 33 | + | } | |||
| 34 | + | } | |||
| 35 | + | ||||
| 36 | + | interface WindowIntent { | |||
| 37 | + | url: string; | |||
| 38 | + | target: string; | |||
| 39 | + | features: string; | |||
| 40 | + | timestamp: number; | |||
| 41 | + | userTriggered: boolean; | |||
| 42 | + | } | |||
| 43 | + | ||||
| 44 | + | interface Suggestion { | |||
| 45 | + | // Define proper suggestion types based on your needs | |||
| 46 | + | [key: string]: unknown; | |||
| 47 | + | } | |||
| 48 | + | ||||
| 49 | + | interface DumberAPI { | |||
| 50 | + | toast: { | |||
| 51 | + | show: (message: string, duration?: number, type?: 'info' | 'success' | 'error') => void; | |||
| 52 | + | zoom: (level: number) => void; | |||
| 53 | + | }; | |||
| 54 | + | omnibox: { | |||
| 55 | + | suggestions: (suggestions: Suggestion[]) => void; | |||
| 56 | + | }; | |||
| 57 | + | } | |||
| 58 | + | ||||
| 59 | + | (() => { | |||
| 60 | + | try { | |||
| 61 | + | // Prevent multiple installations | |||
| 62 | + | if (window.__dumber_page_bridge_installed) { | |||
| 63 | + | return; | |||
| 64 | + | } | |||
| 65 | + | window.__dumber_page_bridge_installed = true; | |||
| 66 | + | ||||
| 67 | + | // Initialize zoom level placeholder (will be replaced by Go) | |||
| 68 | + | const initialZoom = 1.0; // __DOM_ZOOM_DEFAULT__ will be replaced by Go | |||
| 69 | + | window.__dumber_dom_zoom_seed = initialZoom; | |||
| 70 | + | ||||
| 71 | + | // Theme setter function for GTK theme integration | |||
| 72 | + | window.__dumber_setTheme = (theme: 'light' | 'dark') => { | |||
| 73 | + | window.__dumber_initial_theme = theme; | |||
| 74 | + | console.log('[dumber] Setting theme to:', theme); | |||
| 75 | + | if (theme === 'dark') { | |||
| 76 | + | document.documentElement.classList.add('dark'); | |||
| 77 | + | } else { | |||
| 78 | + | document.documentElement.classList.remove('dark'); | |||
| 79 | + | } | |||
| 80 | + | }; | |||
| 81 | + | ||||
| 82 | + | // Initialize unified API object | |||
| 83 | + | window.__dumber = window.__dumber || {} as DumberAPI; | |||
| 84 | + | ||||
| 85 | + | // Toast bridge | |||
| 86 | + | window.__dumber.toast = window.__dumber.toast || { | |||
| 87 | + | show: (message: string, duration?: number, type?: string) => { | |||
| 88 | + | try { | |||
| 89 | + | document.dispatchEvent(new CustomEvent('dumber:toast:show', { | |||
| 90 | + | detail: { message, duration, type } | |||
| 91 | + | })); | |||
| 92 | + | // Legacy compatibility | |||
| 93 | + | document.dispatchEvent(new CustomEvent('dumber:showToast', { | |||
| 94 | + | detail: { message, duration, type } | |||
| 95 | + | })); | |||
| 96 | + | } catch { | |||
| 97 | + | // ignore | |||
| 98 | + | } | |||
| 99 | + | }, | |||
| 100 | + | zoom: (level: number) => { | |||
| 101 | + | try { | |||
| 102 | + | document.dispatchEvent(new CustomEvent('dumber:toast:zoom', { | |||
| 103 | + | detail: { level } | |||
| 104 | + | })); | |||
| 105 | + | // Legacy compatibility | |||
| 106 | + | document.dispatchEvent(new CustomEvent('dumber:showZoomToast', { | |||
| 107 | + | detail: { level } | |||
| 108 | + | })); | |||
| 109 | + | } catch { | |||
| 110 | + | // ignore | |||
| 111 | + | } | |||
| 112 | + | } | |||
| 113 | + | }; | |||
| 114 | + | ||||
| 115 | + | // Legacy toast helpers | |||
| 116 | + | window.__dumber_showToast = (message: string, duration?: number, type?: 'info' | 'success' | 'error') => { | |||
| 117 | + | window.__dumber.toast.show(message, duration, type); | |||
| 118 | + | }; | |||
| 119 | + | window.__dumber_showZoomToast = (level: number) => { | |||
| 120 | + | window.__dumber.toast.zoom(level); | |||
| 121 | + | }; | |||
| 122 | + | ||||
| 123 | + | // DOM zoom functionality | |||
| 124 | + | if (typeof window.__dumber_dom_zoom_level !== 'number') { | |||
| 125 | + | window.__dumber_dom_zoom_level = initialZoom; | |||
| 126 | + | } | |||
| 127 | + | ||||
| 128 | + | const applyZoomStyles = (node: HTMLElement, level: number): void => { | |||
| 129 | + | if (!node) return; | |||
| 130 | + | ||||
| 131 | + | if (Math.abs(level - 1.0) < 1e-6) { | |||
| 132 | + | // Reset zoom | |||
| 133 | + | node.style.removeProperty('zoom'); | |||
| 134 | + | node.style.removeProperty('transform'); | |||
| 135 | + | node.style.removeProperty('transform-origin'); | |||
| 136 | + | node.style.removeProperty('width'); | |||
| 137 | + | node.style.removeProperty('min-width'); | |||
| 138 | + | node.style.removeProperty('height'); | |||
| 139 | + | node.style.removeProperty('min-height'); | |||
| 140 | + | return; | |||
| 141 | + | } | |||
| 142 | + | ||||
| 143 | + | const scale = level; | |||
| 144 | + | const inversePercent = 100 / scale; | |||
| 145 | + | const widthValue = `${inversePercent}%`; | |||
| 146 | + | ||||
| 147 | + | node.style.removeProperty('zoom'); | |||
| 148 | + | node.style.transform = `scale(${scale})`; | |||
| 149 | + | node.style.transformOrigin = '0 0'; | |||
| 150 | + | node.style.width = widthValue; | |||
| 151 | + | node.style.minWidth = widthValue; | |||
| 152 | + | node.style.minHeight = '100%'; | |||
| 153 | + | }; | |||
| 154 | + | ||||
| 155 | + | window.__dumber_applyDomZoom = (level: number) => { | |||
| 156 | + | try { | |||
| 157 | + | window.__dumber_dom_zoom_level = level; | |||
| 158 | + | window.__dumber_dom_zoom_seed = level; | |||
| 159 | + | applyZoomStyles(document.documentElement, level); | |||
| 160 | + | if (document.body) { | |||
| 161 | + | applyZoomStyles(document.body, level); | |||
| 162 | + | } | |||
| 163 | + | } catch (e) { | |||
| 164 | + | console.error('[dumber] DOM zoom error', e); | |||
| 165 | + | } | |||
| 166 | + | }; | |||
| 167 | + | ||||
| 168 | + | // Apply initial zoom | |||
| 169 | + | window.__dumber_applyDomZoom(window.__dumber_dom_zoom_level); | |||
| 170 | + | ||||
| 171 | + | if (!document.body) { | |||
| 172 | + | document.addEventListener('DOMContentLoaded', () => { | |||
| 173 | + | if (typeof window.__dumber_dom_zoom_level === 'number') { | |||
| 174 | + | window.__dumber_applyDomZoom!(window.__dumber_dom_zoom_level); | |||
| 175 | + | } | |||
| 176 | + | }, { once: true }); | |||
| 177 | + | } | |||
| 178 | + | ||||
| 179 | + | // Omnibox suggestions bridge | |||
| 180 | + | const omniboxQueue: Suggestion[] = []; | |||
| 181 | + | let omniboxReady = false; | |||
| 182 | + | ||||
| 183 | + | const omniboxDispatch = (suggestions: Suggestion[]) => { | |||
| 184 | + | try { | |||
| 185 | + | document.dispatchEvent(new CustomEvent('dumber:omnibox:suggestions', { | |||
| 186 | + | detail: { suggestions } | |||
| 187 | + | })); | |||
| 188 | + | // Legacy compatibility | |||
| 189 | + | document.dispatchEvent(new CustomEvent('dumber:omnibox-suggestions', { | |||
| 190 | + | detail: { suggestions } | |||
| 191 | + | })); | |||
| 192 | + | } catch (e) { | |||
| 193 | + | console.error('[dumber] Omnibox dispatch error', e); | |||
| 194 | + | } | |||
| 195 | + | }; | |||
| 196 | + | ||||
| 197 | + | window.__dumber_omnibox_suggestions = (suggestions: Suggestion[]) => { | |||
| 198 | + | if (omniboxReady) { | |||
| 199 | + | omniboxDispatch(suggestions); | |||
| 200 | + | } else { | |||
| 201 | + | try { | |||
| 202 | + | omniboxQueue.push(...suggestions); | |||
| 203 | + | } catch (e){ | |||
| 204 | + | console.error('[dumber] Omnibox queue error', e); | |||
| 205 | + | } | |||
| 206 | + | } | |||
| 207 | + | }; | |||
| 208 | + | ||||
| 209 | + | document.addEventListener('dumber:omnibox-ready', () => { | |||
| 210 | + | omniboxReady = true; | |||
| 211 | + | if (omniboxQueue && omniboxQueue.length) { | |||
| 212 | + | const items = omniboxQueue.slice(); | |||
| 213 | + | omniboxQueue.length = 0; | |||
| 214 | + | items.forEach(s => omniboxDispatch([s]) ); | |||
| 215 | + | } | |||
| 216 | + | }); | |||
| 217 | + | ||||
| 218 | + | // Unified omnibox API | |||
| 219 | + | window.__dumber.omnibox = window.__dumber.omnibox || { | |||
| 220 | + | suggestions: (suggestions: Suggestion[]) => { | |||
| 221 | + | window.__dumber_omnibox_suggestions!(suggestions); | |||
| 222 | + | } | |||
| 223 | + | }; | |||
| 224 | + | ||||
| 225 | + | // User interaction tracking and window.open interceptor | |||
| 226 | + | let lastUserInteraction = 0; | |||
| 227 | + | const INTERACTION_TIMEOUT = 5000; // 5 seconds | |||
| 228 | + | const WINDOW_OPEN_DEBOUNCE = 1000; // 1 second between calls | |||
| 229 | + | const recentWindowOpenCalls = new Map<string, number>(); | |||
| 230 | + | ||||
| 231 | + | const trackUserInteraction = () => { | |||
| 232 | + | lastUserInteraction = Date.now(); | |||
| 233 | + | }; | |||
| 234 | + | ||||
| 235 | + | const isUserInteraction = (): boolean => { | |||
| 236 | + | const now = Date.now(); | |||
| 237 | + | const timeSinceInteraction = now - lastUserInteraction; | |||
| 238 | + | return timeSinceInteraction <= INTERACTION_TIMEOUT; | |||
| 239 | + | }; | |||
| 240 | + | ||||
| 241 | + | const isDuplicateWindowOpen = (url: string): boolean => { | |||
| 242 | + | const now = Date.now(); | |||
| 243 | + | const lastCall = recentWindowOpenCalls.get(url); | |||
| 244 | + | ||||
| 245 | + | if (lastCall && (now - lastCall) < WINDOW_OPEN_DEBOUNCE) { | |||
| 246 | + | return true; | |||
| 247 | + | } | |||
| 248 | + | ||||
| 249 | + | recentWindowOpenCalls.set(url, now); | |||
| 250 | + | ||||
| 251 | + | // Clean up old entries | |||
| 252 | + | for (const [key, timestamp] of recentWindowOpenCalls.entries()) { | |||
| 253 | + | if (now - timestamp > WINDOW_OPEN_DEBOUNCE * 2) { | |||
| 254 | + | recentWindowOpenCalls.delete(key); | |||
| 255 | + | } | |||
| 256 | + | } | |||
| 257 | + | ||||
| 258 | + | return false; | |||
| 259 | + | }; | |||
| 260 | + | ||||
| 261 | + | // Track user interactions for popup validation | |||
| 262 | + | ['click', 'mousedown', 'keydown', 'touchstart'].forEach(eventType => { | |||
| 263 | + | document.addEventListener(eventType, trackUserInteraction, true); | |||
| 264 | + | }); | |||
| 265 | + | ||||
| 266 | + | console.log('[window-open] User interaction tracking enabled'); | |||
| 267 | + | ||||
| 268 | + | // Window.open interceptor - only install once | |||
| 269 | + | if (!window.__dumber_window_open_intercepted) { | |||
| 270 | + | try { | |||
| 271 | + | ||||
| 272 | + | window.open = function( | |||
| 273 | + | url?: string | URL | null, | |||
| 274 | + | target?: string | null, | |||
| 275 | + | features?: string | null | |||
| 276 | + | ): WindowProxy | null { | |||
| 277 | + | console.log('[window-open] Bridge called with:', url, target, features); | |||
| 278 | + | console.log('[window-open] Call stack:', new Error().stack); | |||
| 279 | + | ||||
| 280 | + | const urlString = url ? (typeof url === 'string' ? url : url.toString()) : ''; | |||
| 281 | + | ||||
| 282 | + | // Check for duplicate calls to prevent multiple panes | |||
| 283 | + | if (isDuplicateWindowOpen(urlString)) { | |||
| 284 | + | console.log('[window-open] Blocked: duplicate call within debounce window'); | |||
| 285 | + | return { | |||
| 286 | + | closed: true, | |||
| 287 | + | location: { href: urlString }, | |||
| 288 | + | close: () => {}, | |||
| 289 | + | focus: () => {}, | |||
| 290 | + | blur: () => {}, | |||
| 291 | + | postMessage: () => {} | |||
| 292 | + | } as unknown as WindowProxy; | |||
| 293 | + | } | |||
| 294 | + | ||||
| 295 | + | // Check for user interaction to prevent popup spam | |||
| 296 | + | if (!isUserInteraction()) { | |||
| 297 | + | console.log('[window-open] Blocked: no recent user interaction detected'); | |||
| 298 | + | return { | |||
| 299 | + | closed: true, | |||
| 300 | + | location: { href: '' }, | |||
| 301 | + | close: () => {}, | |||
| 302 | + | focus: () => {}, | |||
| 303 | + | blur: () => {}, | |||
| 304 | + | postMessage: () => {} | |||
| 305 | + | } as unknown as WindowProxy; | |||
| 306 | + | } | |||
| 307 | + | ||||
| 308 | + | const originalFeatures = features || ''; | |||
| 309 | + | console.log('[window-open] Using original features:', originalFeatures); | |||
| 310 | + | ||||
| 311 | + | const intent: WindowIntent = { | |||
| 312 | + | url: urlString, | |||
| 313 | + | target: target || '_blank', | |||
| 314 | + | features: originalFeatures, | |||
| 315 | + | timestamp: Date.now(), | |||
| 316 | + | userTriggered: true | |||
| 317 | + | }; | |||
| 318 | + | ||||
| 319 | + | // Send message to Go to handle popup creation | |||
| 320 | + | try { | |||
| 321 | + | if (window.webkit?.messageHandlers?.dumber) { | |||
| 322 | + | window.webkit.messageHandlers.dumber.postMessage(JSON.stringify({ | |||
| 323 | + | type: 'handle-window-open', | |||
| 324 | + | payload: intent | |||
| 325 | + | })); | |||
| 326 | + | console.log('[window-open] Sent window.open request to Go - bypassing WebKit completely'); | |||
| 327 | + | } | |||
| 328 | + | } catch (e) { | |||
| 329 | + | console.error('[window-open] Failed to send to Go:', e); | |||
| 330 | + | } | |||
| 331 | + | ||||
| 332 | + | // Return fake window object - don't call original window.open | |||
| 333 | + | console.log('[window-open] Returning fake window object - bypassing WebKit create signal'); | |||
| 334 | + | return { | |||
| 335 | + | closed: false, | |||
| 336 | + | location: { href: urlString }, | |||
| 337 | + | close() { (this as { closed: boolean }).closed = true; }, | |||
| 338 | + | focus: () => {}, | |||
| 339 | + | blur: () => {}, | |||
| 340 | + | postMessage: () => {} | |||
| 341 | + | } as unknown as WindowProxy; | |||
| 342 | + | }; | |||
| 343 | + | ||||
| 344 | + | window.__dumber_window_open_intercepted = true; | |||
| 345 | + | console.log('[window-open] ✅ Page-world bridge installed'); | |||
| 346 | + | } catch (error) { | |||
| 347 | + | console.error('[window-open] ❌ Failed to install page-world bridge:', error); | |||
| 348 | + | } | |||
| 349 | + | } | |||
| 350 | + | ||||
| 351 | + | // Console capture functionality | |||
| 352 | + | const setupConsoleCapture = () => { | |||
| 353 | + | const originalConsole = { | |||
| 354 | + | log: console.log, | |||
| 355 | + | warn: console.warn, | |||
| 356 | + | error: console.error, | |||
| 357 | + | info: console.info, | |||
| 358 | + | debug: console.debug | |||
| 359 | + | }; | |||
| 360 | + | ||||
| 361 | + | const sendConsoleMessage = (level: string, args: unknown[]) => { | |||
| 362 | + | try { | |||
| 363 | + | const message = args.map(arg => | |||
| 364 | + | typeof arg === 'object' ? JSON.stringify(arg) : String(arg) | |||
| 365 | + | ).join(' '); | |||
| 366 | + | ||||
| 367 | + | const formattedMessage = `${window.location.href} ${message}`; | |||
| 368 | + | ||||
| 369 | + | // Send to Go via existing message handler | |||
| 370 | + | if (window.webkit?.messageHandlers?.dumber) { | |||
| 371 | + | window.webkit.messageHandlers.dumber.postMessage(JSON.stringify({ | |||
| 372 | + | type: 'console-message', | |||
| 373 | + | payload: { level, message: formattedMessage, url: window.location.href } | |||
| 374 | + | })); | |||
| 375 | + | } | |||
| 376 | + | } catch { | |||
| 377 | + | // Silently ignore errors in console capture to avoid infinite loops | |||
| 378 | + | } | |||
| 379 | + | }; | |||
| 380 | + | ||||
| 381 | + | const createConsoleWrapper = (originalMethod: (...args: unknown[]) => void, level: string) => { | |||
| 382 | + | return function(...args: unknown[]) { | |||
| 383 | + | originalMethod.apply(console, args); | |||
| 384 | + | sendConsoleMessage(level, args); | |||
| 385 | + | }; | |||
| 386 | + | }; | |||
| 387 | + | ||||
| 388 | + | // Override console methods to capture messages | |||
| 389 | + | console.log = createConsoleWrapper(originalConsole.log, 'LOG'); | |||
| 390 | + | console.warn = createConsoleWrapper(originalConsole.warn, 'WARN'); | |||
| 391 | + | console.error = createConsoleWrapper(originalConsole.error, 'ERROR'); | |||
| 392 | + | console.info = createConsoleWrapper(originalConsole.info, 'INFO'); | |||
| 393 | + | console.debug = createConsoleWrapper(originalConsole.debug, 'DEBUG'); | |||
| 394 | + | }; | |||
| 395 | + | ||||
| 396 | + | // Initialize console capture | |||
| 397 | + | setupConsoleCapture(); | |||
| 398 | + | ||||
| 399 | + | } catch (e) { | |||
| 400 | + | console.warn('[dumber] unified bridge init failed', e); | |||
| 401 | + | } | |||
| 402 | + | })();⏎̸ | |||
A gui/src/injected/modules/window-open.ts +29 -0
| @@ -0,0 +1,29 @@ | |||
| 1 | + | /** | |
| 2 | + | * Window Open Interceptor Module | |
| 3 | + | * | |
| 4 | + | * OBSOLETE: window.open() is now handled directly via page-world bridge bypass. | |
| 5 | + | * This module is kept for compatibility but no longer performs active interception. | |
| 6 | + | */ | |
| 7 | + | ||
| 8 | + | let isInitialized = false; | |
| 9 | + | ||
| 10 | + | /** | |
| 11 | + | * Initialize the window.open interceptor (no-op) | |
| 12 | + | * Window.open is now handled directly via JavaScript bypass in page-world | |
| 13 | + | */ | |
| 14 | + | export function initializeWindowOpenInterceptor(): void { | |
| 15 | + | if (isInitialized) { | |
| 16 | + | return; | |
| 17 | + | } | |
| 18 | + | ||
| 19 | + | // No-op - window.open handled directly via page-world bridge | |
| 20 | + | isInitialized = true; | |
| 21 | + | console.log('[window-open] ✅ Interceptor initialized (bypass mode)'); | |
| 22 | + | } | |
| 23 | + | ||
| 24 | + | /** | |
| 25 | + | * Get initialization status | |
| 26 | + | */ | |
| 27 | + | export function isWindowOpenInterceptorInitialized(): boolean { | |
| 28 | + | return isInitialized; | |
| 29 | + | }⏎̸ | |
| @@ -0,0 +1,29 @@ | |||||
| 1 | + | /** | |||
| 2 | + | * Window Open Interceptor Module | |||
| 3 | + | * | |||
| 4 | + | * OBSOLETE: window.open() is now handled directly via page-world bridge bypass. | |||
| 5 | + | * This module is kept for compatibility but no longer performs active interception. | |||
| 6 | + | */ | |||
| 7 | + | ||||
| 8 | + | let isInitialized = false; | |||
| 9 | + | ||||
| 10 | + | /** | |||
| 11 | + | * Initialize the window.open interceptor (no-op) | |||
| 12 | + | * Window.open is now handled directly via JavaScript bypass in page-world | |||
| 13 | + | */ | |||
| 14 | + | export function initializeWindowOpenInterceptor(): void { | |||
| 15 | + | if (isInitialized) { | |||
| 16 | + | return; | |||
| 17 | + | } | |||
| 18 | + | ||||
| 19 | + | // No-op - window.open handled directly via page-world bridge | |||
| 20 | + | isInitialized = true; | |||
| 21 | + | console.log('[window-open] ✅ Interceptor initialized (bypass mode)'); | |||
| 22 | + | } | |||
| 23 | + | ||||
| 24 | + | /** | |||
| 25 | + | * Get initialization status | |||
| 26 | + | */ | |||
| 27 | + | export function isWindowOpenInterceptorInitialized(): boolean { | |||
| 28 | + | return isInitialized; | |||
| 29 | + | }⏎̸ | |||
A gui/vite.config.main-world.ts +28 -0
| @@ -0,0 +1,28 @@ | |||
| 1 | + | import { defineConfig } from 'vite'; | |
| 2 | + | import { resolve } from 'path'; | |
| 3 | + | ||
| 4 | + | export default defineConfig({ | |
| 5 | + | build: { | |
| 6 | + | rollupOptions: { | |
| 7 | + | input: resolve(__dirname, 'src/injected/main-world.ts'), | |
| 8 | + | output: { | |
| 9 | + | dir: '../assets/gui', | |
| 10 | + | format: 'iife', | |
| 11 | + | entryFileNames: 'main-world.min.js', | |
| 12 | + | name: '__dumberMainWorld', | |
| 13 | + | inlineDynamicImports: true, | |
| 14 | + | manualChunks: undefined, | |
| 15 | + | assetFileNames: '[name].[ext]', | |
| 16 | + | }, | |
| 17 | + | }, | |
| 18 | + | emptyOutDir: false, | |
| 19 | + | target: ['es2020', 'chrome91', 'firefox90'], | |
| 20 | + | minify: true, | |
| 21 | + | sourcemap: false, | |
| 22 | + | cssCodeSplit: false, | |
| 23 | + | assetsInlineLimit: 100000000, | |
| 24 | + | }, | |
| 25 | + | define: { | |
| 26 | + | '__DOM_ZOOM_DEFAULT__': '1.0' // Default placeholder, will be replaced by Go | |
| 27 | + | } | |
| 28 | + | });⏎̸ | |
| @@ -0,0 +1,28 @@ | |||||
| 1 | + | import { defineConfig } from 'vite'; | |||
| 2 | + | import { resolve } from 'path'; | |||
| 3 | + | ||||
| 4 | + | export default defineConfig({ | |||
| 5 | + | build: { | |||
| 6 | + | rollupOptions: { | |||
| 7 | + | input: resolve(__dirname, 'src/injected/main-world.ts'), | |||
| 8 | + | output: { | |||
| 9 | + | dir: '../assets/gui', | |||
| 10 | + | format: 'iife', | |||
| 11 | + | entryFileNames: 'main-world.min.js', | |||
| 12 | + | name: '__dumberMainWorld', | |||
| 13 | + | inlineDynamicImports: true, | |||
| 14 | + | manualChunks: undefined, | |||
| 15 | + | assetFileNames: '[name].[ext]', | |||
| 16 | + | }, | |||
| 17 | + | }, | |||
| 18 | + | emptyOutDir: false, | |||
| 19 | + | target: ['es2020', 'chrome91', 'firefox90'], | |||
| 20 | + | minify: true, | |||
| 21 | + | sourcemap: false, | |||
| 22 | + | cssCodeSplit: false, | |||
| 23 | + | assetsInlineLimit: 100000000, | |||
| 24 | + | }, | |||
| 25 | + | define: { | |||
| 26 | + | '__DOM_ZOOM_DEFAULT__': '1.0' // Default placeholder, will be replaced by Go | |||
| 27 | + | } | |||
| 28 | + | });⏎̸ | |||
M internal/app/browser/shortcuts.go +24 -7
| @@ -30,16 +30,33 @@ func NewShortcutHandler(webView *webkit.WebView, clipboardController *control.Cl | |||
| 30 | 30 | ||
| 31 | 31 | func (s *ShortcutHandler) isActivePane() bool { | |
| 32 | 32 | if s == nil || s.webView == nil { | |
| 33 | - | log.Printf("[shortcut] isActivePane: handler or webView is nil") | |
| 34 | 33 | return false | |
| 35 | 34 | } | |
| 36 | - | if s.app == nil || s.app.activePane == nil { | |
| 37 | - | log.Printf("[shortcut] isActivePane: app or activePane is nil, returning true") | |
| 38 | - | return true | |
| 35 | + | if s.app == nil || s.app.workspace == nil { | |
| 36 | + | return true // No workspace context; allow | |
| 39 | 37 | } | |
| 40 | - | isActive := s.app.activePane.webView == s.webView | |
| 41 | - | log.Printf("[shortcut] isActivePane: handler webView=%p activePane webView=%p isActive=%v", s.webView, s.app.activePane.webView, isActive) | |
| 42 | - | return isActive | |
| 38 | + | ||
| 39 | + | // Find node for this WebView | |
| 40 | + | node := s.app.workspace.viewToNode[s.webView] | |
| 41 | + | if node == nil { | |
| 42 | + | return false | |
| 43 | + | } | |
| 44 | + | ||
| 45 | + | // For related popups, they're active if parent chain contains the active node | |
| 46 | + | if node.isRelated && node.parentPane != nil { | |
| 47 | + | activeNode := s.app.workspace.active | |
| 48 | + | cur := node | |
| 49 | + | for cur != nil { | |
| 50 | + | if cur == activeNode { | |
| 51 | + | return true | |
| 52 | + | } | |
| 53 | + | cur = cur.parentPane | |
| 54 | + | } | |
| 55 | + | return false | |
| 56 | + | } | |
| 57 | + | ||
| 58 | + | // Independent panes: equal to active node | |
| 59 | + | return node == s.app.workspace.active | |
| 43 | 60 | } | |
| 44 | 61 | ||
| 45 | 62 | // RegisterShortcuts registers pane-specific keyboard shortcuts with focus guards | |
| @@ -30,16 +30,33 @@ func NewShortcutHandler(webView *webkit.WebView, clipboardController *control.Cl | |||||
| 30 | 30 | ||||
| 31 | func (s *ShortcutHandler) isActivePane() bool { | 31 | func (s *ShortcutHandler) isActivePane() bool { | ||
| 32 | if s == nil || s.webView == nil { | 32 | if s == nil || s.webView == nil { | ||
| 33 | - | log.Printf("[shortcut] isActivePane: handler or webView is nil") | |||
| 34 | return false | 33 | return false | ||
| 35 | } | 34 | } | ||
| 36 | - | if s.app == nil || s.app.activePane == nil { | 35 | + | if s.app == nil || s.app.workspace == nil { |
| 37 | - | log.Printf("[shortcut] isActivePane: app or activePane is nil, returning true") | 36 | + | return true // No workspace context; allow |
| 38 | - | return true | |||
| 39 | } | 37 | } | ||
| 40 | - | isActive := s.app.activePane.webView == s.webView | 38 | + | |
| 41 | - | log.Printf("[shortcut] isActivePane: handler webView=%p activePane webView=%p isActive=%v", s.webView, s.app.activePane.webView, isActive) | 39 | + | // Find node for this WebView |
| 42 | - | return isActive | 40 | + | node := s.app.workspace.viewToNode[s.webView] |
| 41 | + | if node == nil { | |||
| 42 | + | return false | |||
| 43 | + | } | |||
| 44 | + | ||||
| 45 | + | // For related popups, they're active if parent chain contains the active node | |||
| 46 | + | if node.isRelated && node.parentPane != nil { | |||
| 47 | + | activeNode := s.app.workspace.active | |||
| 48 | + | cur := node | |||
| 49 | + | for cur != nil { | |||
| 50 | + | if cur == activeNode { | |||
| 51 | + | return true | |||
| 52 | + | } | |||
| 53 | + | cur = cur.parentPane | |||
| 54 | + | } | |||
| 55 | + | return false | |||
| 56 | + | } | |||
| 57 | + | ||||
| 58 | + | // Independent panes: equal to active node | |||
| 59 | + | return node == s.app.workspace.active | |||
| 43 | } | 60 | } | ||
| 44 | 61 | ||||
| 45 | // RegisterShortcuts registers pane-specific keyboard shortcuts with focus guards | 62 | // RegisterShortcuts registers pane-specific keyboard shortcuts with focus guards | ||
M internal/app/browser/workspace_manager.go +468 -6
| @@ -5,6 +5,7 @@ import ( | |||
| 5 | 5 | "fmt" | |
| 6 | 6 | "log" | |
| 7 | 7 | "math" | |
| 8 | + | neturl "net/url" | |
| 8 | 9 | "strings" | |
| 9 | 10 | "sync" | |
| 10 | 11 | "time" | |
| @@ -22,8 +23,14 @@ type paneNode struct { | |||
| 22 | 23 | container uintptr // GtkPaned for branch nodes, stable WebView container for leaves | |
| 23 | 24 | orientation webkit.Orientation | |
| 24 | 25 | isLeaf bool | |
| 25 | - | isPopup bool // Track if this is a popup pane for OAuth auto-close | |
| 26 | - | hoverToken uintptr | |
| 26 | + | isPopup bool // Deprecated: use windowType instead | |
| 27 | + | // Window type tracking | |
| 28 | + | windowType webkit.WindowType // Tab or Popup | |
| 29 | + | windowFeatures *webkit.WindowFeatures // Features if popup | |
| 30 | + | isRelated bool // Shares context | |
| 31 | + | parentPane *paneNode // Parent for related views | |
| 32 | + | autoClose bool // Auto-close on OAuth success | |
| 33 | + | hoverToken uintptr | |
| 27 | 34 | } | |
| 28 | 35 | ||
| 29 | 36 | // WorkspaceManager coordinates Zellij-style pane operations. | |
| @@ -241,6 +248,45 @@ func (wm *WorkspaceManager) OnWorkspaceMessage(source *webkit.WebView, msg messa | |||
| 241 | 248 | } | |
| 242 | 249 | wm.clonePaneState(node, newNode) | |
| 243 | 250 | wm.splitting = false | |
| 251 | + | case "create-pane": | |
| 252 | + | log.Printf("[workspace] create-pane requested: url=%s action=%s", msg.URL, msg.Action) | |
| 253 | + | ||
| 254 | + | if msg.URL == "" { | |
| 255 | + | log.Printf("[workspace] create-pane: empty URL, ignoring") | |
| 256 | + | break | |
| 257 | + | } | |
| 258 | + | ||
| 259 | + | // Create a basic window intent from the message | |
| 260 | + | intent := &messaging.WindowIntent{ | |
| 261 | + | URL: msg.URL, | |
| 262 | + | WindowType: msg.Action, // "tab" or "popup" | |
| 263 | + | } | |
| 264 | + | ||
| 265 | + | // Use the existing methods to handle tab vs popup creation | |
| 266 | + | switch strings.ToLower(msg.Action) { | |
| 267 | + | case "tab": | |
| 268 | + | newView := wm.handleIntentAsTab(node, msg.URL, intent) | |
| 269 | + | if newView != nil { | |
| 270 | + | log.Printf("[workspace] create-pane: tab created successfully") | |
| 271 | + | } else { | |
| 272 | + | log.Printf("[workspace] create-pane: failed to create tab") | |
| 273 | + | } | |
| 274 | + | case "popup": | |
| 275 | + | newView := wm.handleIntentAsPopup(node, msg.URL, intent) | |
| 276 | + | if newView != nil { | |
| 277 | + | log.Printf("[workspace] create-pane: popup created successfully") | |
| 278 | + | } else { | |
| 279 | + | log.Printf("[workspace] create-pane: failed to create popup") | |
| 280 | + | } | |
| 281 | + | default: | |
| 282 | + | log.Printf("[workspace] create-pane: unknown action '%s', defaulting to tab", msg.Action) | |
| 283 | + | newView := wm.handleIntentAsTab(node, msg.URL, intent) | |
| 284 | + | if newView != nil { | |
| 285 | + | log.Printf("[workspace] create-pane: default tab created successfully") | |
| 286 | + | } else { | |
| 287 | + | log.Printf("[workspace] create-pane: failed to create default tab") | |
| 288 | + | } | |
| 289 | + | } | |
| 244 | 290 | default: | |
| 245 | 291 | log.Printf("[workspace] unhandled workspace event: %s", msg.Event) | |
| 246 | 292 | } | |
| @@ -1267,13 +1313,14 @@ func (wm *WorkspaceManager) HandlePopup(source *webkit.WebView, url string) *web | |||
| 1267 | 1313 | return nil | |
| 1268 | 1314 | } | |
| 1269 | 1315 | ||
| 1316 | + | // Note: HandlePopup is now obsolete - window.open is handled directly via JavaScript bypass | |
| 1317 | + | ||
| 1270 | 1318 | cfg := wm.app.config | |
| 1271 | 1319 | if cfg == nil { | |
| 1272 | 1320 | log.Printf("[workspace] HandlePopup: nil config - allowing native popup") | |
| 1273 | 1321 | return nil | |
| 1274 | 1322 | } | |
| 1275 | 1323 | ||
| 1276 | - | ||
| 1277 | 1324 | popCfg := cfg.Workspace.Popups | |
| 1278 | 1325 | log.Printf("[workspace] Popup config - OpenInNewPane: %v, Placement: %s", popCfg.OpenInNewPane, popCfg.Placement) | |
| 1279 | 1326 | ||
| @@ -1282,15 +1329,50 @@ func (wm *WorkspaceManager) HandlePopup(source *webkit.WebView, url string) *web | |||
| 1282 | 1329 | return nil | |
| 1283 | 1330 | } | |
| 1284 | 1331 | ||
| 1285 | - | // Create a new WebView related to the source for process sharing and proper WindowFeatures | |
| 1332 | + | // Smart detection path: create temporary view and decide placement once type is known | |
| 1333 | + | if popCfg.EnableSmartDetection { | |
| 1334 | + | webkitCfg, err := wm.app.buildWebkitConfig() | |
| 1335 | + | if err != nil { | |
| 1336 | + | log.Printf("[workspace] failed to build webkit config: %v - allowing native popup", err) | |
| 1337 | + | return nil | |
| 1338 | + | } | |
| 1339 | + | webkitCfg.CreateWindow = false | |
| 1340 | + | // Create as related to avoid WindowFeatures crash; we'll decide final placement later | |
| 1341 | + | newView, err := webkit.NewWebViewWithRelated(webkitCfg, source) | |
| 1342 | + | if err != nil { | |
| 1343 | + | log.Printf("[workspace] failed to create temp WebView: %v - allowing native popup", err) | |
| 1344 | + | return nil | |
| 1345 | + | } | |
| 1346 | + | ||
| 1347 | + | // Register detection callback | |
| 1348 | + | newView.OnWindowTypeDetected(func(t webkit.WindowType, feat *webkit.WindowFeatures) { | |
| 1349 | + | wm.RunOnUI(func() { | |
| 1350 | + | wm.handleDetectedWindowType(node, newView, url, t, feat) | |
| 1351 | + | }) | |
| 1352 | + | }) | |
| 1353 | + | ||
| 1354 | + | // Fallback: if detection never fires, treat as popup as before | |
| 1355 | + | go func() { | |
| 1356 | + | time.Sleep(1500 * time.Millisecond) | |
| 1357 | + | if newView != nil { | |
| 1358 | + | wm.RunOnUI(func() { | |
| 1359 | + | if wm.viewToNode[newView] == nil { // not yet placed | |
| 1360 | + | wm.handleDetectedWindowType(node, newView, url, webkit.WindowTypePopup, nil) | |
| 1361 | + | } | |
| 1362 | + | }) | |
| 1363 | + | } | |
| 1364 | + | }() | |
| 1365 | + | ||
| 1366 | + | return newView | |
| 1367 | + | } | |
| 1368 | + | ||
| 1369 | + | // Legacy path preserved | |
| 1286 | 1370 | webkitCfg, err := wm.app.buildWebkitConfig() | |
| 1287 | 1371 | if err != nil { | |
| 1288 | 1372 | log.Printf("[workspace] failed to build webkit config: %v - allowing native popup", err) | |
| 1289 | 1373 | return nil | |
| 1290 | 1374 | } | |
| 1291 | - | // Don't create window initially - we'll embed directly in workspace | |
| 1292 | 1375 | webkitCfg.CreateWindow = false | |
| 1293 | - | ||
| 1294 | 1376 | newView, err := webkit.NewWebViewWithRelated(webkitCfg, source) | |
| 1295 | 1377 | if err != nil { | |
| 1296 | 1378 | log.Printf("[workspace] failed to create related popup WebView: %v - allowing native popup", err) | |
| @@ -1372,6 +1454,386 @@ func (wm *WorkspaceManager) HandlePopup(source *webkit.WebView, url string) *web | |||
| 1372 | 1454 | return newView | |
| 1373 | 1455 | } | |
| 1374 | 1456 | ||
| 1457 | + | // handleIntentAsTab creates an independent tab pane based on window.open intent | |
| 1458 | + | func (wm *WorkspaceManager) registerOAuthAutoClose(view *webkit.WebView, url string) { | |
| 1459 | + | initial := strings.TrimSpace(strings.ToLower(url)) | |
| 1460 | + | closed := false | |
| 1461 | + | view.RegisterURIChangedHandler(func(uri string) { | |
| 1462 | + | if closed { | |
| 1463 | + | return | |
| 1464 | + | } | |
| 1465 | + | if uri == "" { | |
| 1466 | + | return | |
| 1467 | + | } | |
| 1468 | + | u := strings.ToLower(uri) | |
| 1469 | + | if u == "about:blank" || u == initial { | |
| 1470 | + | return | |
| 1471 | + | } | |
| 1472 | + | ||
| 1473 | + | should := false | |
| 1474 | + | if strings.Contains(u, "auth/callback") || strings.Contains(u, "callback") || strings.Contains(u, "redirect") { | |
| 1475 | + | should = true | |
| 1476 | + | } | |
| 1477 | + | if !should { | |
| 1478 | + | if parsed, err := neturl.Parse(uri); err == nil { | |
| 1479 | + | q := parsed.Query() | |
| 1480 | + | if q.Has("code") || q.Has("access_token") || q.Has("id_token") { | |
| 1481 | + | should = true | |
| 1482 | + | } | |
| 1483 | + | if !should && parsed.Fragment != "" { | |
| 1484 | + | frag := strings.ToLower(parsed.Fragment) | |
| 1485 | + | if strings.Contains(frag, "access_token=") || strings.Contains(frag, "id_token=") || strings.Contains(frag, "code=") { | |
| 1486 | + | should = true | |
| 1487 | + | } | |
| 1488 | + | } | |
| 1489 | + | } | |
| 1490 | + | } | |
| 1491 | + | ||
| 1492 | + | if should { | |
| 1493 | + | log.Printf("[workspace] OAuth URL-based auto-close triggered for: %s", uri) | |
| 1494 | + | closed = true | |
| 1495 | + | time.AfterFunc(200*time.Millisecond, func() { | |
| 1496 | + | webkit.IdleAdd(func() bool { | |
| 1497 | + | if n := wm.viewToNode[view]; n != nil && n.isPopup { | |
| 1498 | + | if err := wm.closePane(n); err != nil { | |
| 1499 | + | log.Printf("[workspace] Failed to close OAuth popup pane: %v", err) | |
| 1500 | + | } | |
| 1501 | + | } | |
| 1502 | + | return false | |
| 1503 | + | }) | |
| 1504 | + | }) | |
| 1505 | + | } | |
| 1506 | + | }) | |
| 1507 | + | } | |
| 1508 | + | ||
| 1509 | + | func (wm *WorkspaceManager) applyWindowFeatures(view *webkit.WebView, intent *messaging.WindowIntent, isPopup bool) { | |
| 1510 | + | if intent == nil { | |
| 1511 | + | return | |
| 1512 | + | } | |
| 1513 | + | ||
| 1514 | + | features := &webkit.WindowFeatures{} | |
| 1515 | + | ||
| 1516 | + | // Apply dimensions if specified | |
| 1517 | + | if intent.Width != nil { | |
| 1518 | + | features.Width = *intent.Width | |
| 1519 | + | } | |
| 1520 | + | if intent.Height != nil { | |
| 1521 | + | features.Height = *intent.Height | |
| 1522 | + | } | |
| 1523 | + | ||
| 1524 | + | // Apply visibility features based on window type | |
| 1525 | + | defaultToolbar := !isPopup | |
| 1526 | + | defaultLocation := !isPopup | |
| 1527 | + | defaultMenubar := !isPopup | |
| 1528 | + | ||
| 1529 | + | if intent.Toolbar != nil { | |
| 1530 | + | features.ToolbarVisible = *intent.Toolbar | |
| 1531 | + | } else { | |
| 1532 | + | features.ToolbarVisible = defaultToolbar | |
| 1533 | + | } | |
| 1534 | + | ||
| 1535 | + | if intent.Location != nil { | |
| 1536 | + | features.LocationbarVisible = *intent.Location | |
| 1537 | + | } else { | |
| 1538 | + | features.LocationbarVisible = defaultLocation | |
| 1539 | + | } | |
| 1540 | + | ||
| 1541 | + | if intent.Menubar != nil { | |
| 1542 | + | features.MenubarVisible = *intent.Menubar | |
| 1543 | + | } else { | |
| 1544 | + | features.MenubarVisible = defaultMenubar | |
| 1545 | + | } | |
| 1546 | + | ||
| 1547 | + | if intent.Resizable != nil { | |
| 1548 | + | features.Resizable = *intent.Resizable | |
| 1549 | + | } else { | |
| 1550 | + | features.Resizable = true // Usually resizable unless explicitly disabled | |
| 1551 | + | } | |
| 1552 | + | ||
| 1553 | + | view.SetWindowFeatures(features) | |
| 1554 | + | windowTypeStr := "tab" | |
| 1555 | + | if isPopup { | |
| 1556 | + | windowTypeStr = "popup" | |
| 1557 | + | } | |
| 1558 | + | log.Printf("[workspace] Applied %s window features from intent: size=%dx%d, toolbar=%t, location=%t, menubar=%t, resizable=%t", | |
| 1559 | + | windowTypeStr, features.Width, features.Height, features.ToolbarVisible, features.LocationbarVisible, features.MenubarVisible, features.Resizable) | |
| 1560 | + | } | |
| 1561 | + | ||
| 1562 | + | func (wm *WorkspaceManager) handleIntentAsTab(sourceNode *paneNode, url string, intent *messaging.WindowIntent) *webkit.WebView { | |
| 1563 | + | log.Printf("[workspace] Handling intent as tab: %s", url) | |
| 1564 | + | ||
| 1565 | + | webkitCfg, err := wm.app.buildWebkitConfig() | |
| 1566 | + | if err != nil { | |
| 1567 | + | log.Printf("[workspace] failed to build webkit config: %v - allowing native popup", err) | |
| 1568 | + | return nil | |
| 1569 | + | } | |
| 1570 | + | webkitCfg.CreateWindow = false | |
| 1571 | + | ||
| 1572 | + | newView, err := webkit.NewWebView(webkitCfg) | |
| 1573 | + | if err != nil { | |
| 1574 | + | log.Printf("[workspace] failed to create tab WebView: %v - allowing native popup", err) | |
| 1575 | + | return nil | |
| 1576 | + | } | |
| 1577 | + | ||
| 1578 | + | newPane, err := wm.createPane(newView) | |
| 1579 | + | if err != nil { | |
| 1580 | + | log.Printf("[workspace] failed to create tab pane: %v - allowing native popup", err) | |
| 1581 | + | return nil | |
| 1582 | + | } | |
| 1583 | + | ||
| 1584 | + | direction := strings.ToLower(wm.app.config.Workspace.Popups.Placement) | |
| 1585 | + | if direction == "" { | |
| 1586 | + | direction = "right" | |
| 1587 | + | } | |
| 1588 | + | ||
| 1589 | + | if err := wm.insertPopupPane(sourceNode, newPane, direction); err != nil { | |
| 1590 | + | log.Printf("[workspace] tab pane insertion failed: %v - allowing native popup", err) | |
| 1591 | + | return nil | |
| 1592 | + | } | |
| 1593 | + | ||
| 1594 | + | node := wm.viewToNode[newView] | |
| 1595 | + | if node != nil { | |
| 1596 | + | node.windowType = webkit.WindowTypeTab | |
| 1597 | + | node.isRelated = false | |
| 1598 | + | ||
| 1599 | + | // Apply window features from JavaScript intent | |
| 1600 | + | wm.applyWindowFeatures(newView, intent, false) | |
| 1601 | + | } | |
| 1602 | + | ||
| 1603 | + | wm.ensureGUIInPane(newPane) | |
| 1604 | + | ||
| 1605 | + | if url != "" { | |
| 1606 | + | if err := newView.LoadURL(url); err != nil { | |
| 1607 | + | log.Printf("[workspace] failed to load tab URL: %v", err) | |
| 1608 | + | } | |
| 1609 | + | if err := newView.Show(); err != nil { | |
| 1610 | + | log.Printf("[workspace] failed to show tab WebView: %v", err) | |
| 1611 | + | } | |
| 1612 | + | } | |
| 1613 | + | ||
| 1614 | + | log.Printf("[workspace] Created tab pane for URL: %s", url) | |
| 1615 | + | return newView | |
| 1616 | + | } | |
| 1617 | + | ||
| 1618 | + | // handleIntentAsPopup creates a related popup pane based on window.open intent | |
| 1619 | + | func (wm *WorkspaceManager) handleIntentAsPopup(sourceNode *paneNode, url string, intent *messaging.WindowIntent) *webkit.WebView { | |
| 1620 | + | log.Printf("[workspace] Handling intent as popup: %s", url) | |
| 1621 | + | ||
| 1622 | + | webkitCfg, err := wm.app.buildWebkitConfig() | |
| 1623 | + | if err != nil { | |
| 1624 | + | log.Printf("[workspace] failed to build webkit config: %v - allowing native popup", err) | |
| 1625 | + | return nil | |
| 1626 | + | } | |
| 1627 | + | webkitCfg.CreateWindow = false | |
| 1628 | + | ||
| 1629 | + | newView, err := webkit.NewWebViewWithRelated(webkitCfg, sourceNode.pane.webView) | |
| 1630 | + | if err != nil { | |
| 1631 | + | log.Printf("[workspace] failed to create popup WebView: %v - allowing native popup", err) | |
| 1632 | + | return nil | |
| 1633 | + | } | |
| 1634 | + | ||
| 1635 | + | newPane, err := wm.createPane(newView) | |
| 1636 | + | if err != nil { | |
| 1637 | + | log.Printf("[workspace] failed to create popup pane: %v - allowing native popup", err) | |
| 1638 | + | return nil | |
| 1639 | + | } | |
| 1640 | + | ||
| 1641 | + | direction := strings.ToLower(wm.app.config.Workspace.Popups.Placement) | |
| 1642 | + | if direction == "" { | |
| 1643 | + | direction = "right" | |
| 1644 | + | } | |
| 1645 | + | ||
| 1646 | + | if err := wm.insertPopupPane(sourceNode, newPane, direction); err != nil { | |
| 1647 | + | log.Printf("[workspace] popup pane insertion failed: %v - allowing native popup", err) | |
| 1648 | + | return nil | |
| 1649 | + | } | |
| 1650 | + | ||
| 1651 | + | node := wm.viewToNode[newView] | |
| 1652 | + | if node != nil { | |
| 1653 | + | node.windowType = webkit.WindowTypePopup | |
| 1654 | + | node.isRelated = true | |
| 1655 | + | node.parentPane = sourceNode | |
| 1656 | + | node.isPopup = true | |
| 1657 | + | node.autoClose = wm.shouldAutoClose(url) | |
| 1658 | + | ||
| 1659 | + | // Apply window features from JavaScript intent | |
| 1660 | + | wm.applyWindowFeatures(newView, intent, true) | |
| 1661 | + | } | |
| 1662 | + | ||
| 1663 | + | // Register close handler for popup auto-close | |
| 1664 | + | newView.RegisterCloseHandler(func() { | |
| 1665 | + | log.Printf("[workspace] Popup requesting close via window.close()") | |
| 1666 | + | if n := wm.viewToNode[newView]; n != nil && n.isPopup { | |
| 1667 | + | time.AfterFunc(200*time.Millisecond, func() { | |
| 1668 | + | webkit.IdleAdd(func() bool { | |
| 1669 | + | if err := wm.closePane(n); err != nil { | |
| 1670 | + | log.Printf("[workspace] Failed to close popup pane: %v", err) | |
| 1671 | + | } | |
| 1672 | + | return false | |
| 1673 | + | }) | |
| 1674 | + | }) | |
| 1675 | + | } | |
| 1676 | + | }) | |
| 1677 | + | ||
| 1678 | + | // URL-based auto-close for OAuth popups | |
| 1679 | + | if node != nil && node.isPopup && node.autoClose { | |
| 1680 | + | wm.registerOAuthAutoClose(newView, url) | |
| 1681 | + | } | |
| 1682 | + | ||
| 1683 | + | wm.ensureGUIInPane(newPane) | |
| 1684 | + | ||
| 1685 | + | if url != "" { | |
| 1686 | + | if err := newView.LoadURL(url); err != nil { | |
| 1687 | + | log.Printf("[workspace] failed to load popup URL: %v", err) | |
| 1688 | + | } | |
| 1689 | + | if err := newView.Show(); err != nil { | |
| 1690 | + | log.Printf("[workspace] failed to show popup WebView: %v", err) | |
| 1691 | + | } | |
| 1692 | + | } | |
| 1693 | + | ||
| 1694 | + | log.Printf("[workspace] Created popup pane for URL: %s", url) | |
| 1695 | + | return newView | |
| 1696 | + | } | |
| 1697 | + | ||
| 1698 | + | // insertIndependentPane inserts a new independent pane next to the source | |
| 1699 | + | func (wm *WorkspaceManager) insertIndependentPane(sourceNode *paneNode, webView *webkit.WebView, url string) error { | |
| 1700 | + | newPane, err := wm.createPane(webView) | |
| 1701 | + | if err != nil { | |
| 1702 | + | return err | |
| 1703 | + | } | |
| 1704 | + | direction := strings.ToLower(wm.app.config.Workspace.Popups.Placement) | |
| 1705 | + | if direction == "" { | |
| 1706 | + | direction = "right" | |
| 1707 | + | } | |
| 1708 | + | if err := wm.insertPopupPane(sourceNode, newPane, direction); err != nil { // reuse insertion primitive | |
| 1709 | + | return err | |
| 1710 | + | } | |
| 1711 | + | node := wm.viewToNode[webView] | |
| 1712 | + | if node != nil { | |
| 1713 | + | node.windowType = webkit.WindowTypeTab | |
| 1714 | + | node.isRelated = false | |
| 1715 | + | } | |
| 1716 | + | if url != "" { | |
| 1717 | + | _ = webView.LoadURL(url) | |
| 1718 | + | } | |
| 1719 | + | return nil | |
| 1720 | + | } | |
| 1721 | + | ||
| 1722 | + | // configureRelatedPopup creates a related view and inserts it | |
| 1723 | + | func (wm *WorkspaceManager) configureRelatedPopup(sourceNode *paneNode, webView *webkit.WebView, url string, feat *webkit.WindowFeatures) { | |
| 1724 | + | // Use the WebView that was already created and returned to WebKit | |
| 1725 | + | related := webView | |
| 1726 | + | newPane, err := wm.createPane(related) | |
| 1727 | + | if err != nil { | |
| 1728 | + | log.Printf("[workspace] failed to create related popup pane: %v", err) | |
| 1729 | + | return | |
| 1730 | + | } | |
| 1731 | + | direction := strings.ToLower(wm.app.config.Workspace.Popups.Placement) | |
| 1732 | + | if direction == "" { | |
| 1733 | + | direction = "right" | |
| 1734 | + | } | |
| 1735 | + | if err := wm.insertPopupPane(sourceNode, newPane, direction); err != nil { | |
| 1736 | + | log.Printf("[workspace] failed to insert related popup pane: %v", err) | |
| 1737 | + | return | |
| 1738 | + | } | |
| 1739 | + | node := wm.viewToNode[related] | |
| 1740 | + | if node != nil { | |
| 1741 | + | node.windowType = webkit.WindowTypePopup | |
| 1742 | + | node.windowFeatures = feat | |
| 1743 | + | node.isRelated = true | |
| 1744 | + | node.parentPane = sourceNode | |
| 1745 | + | node.isPopup = true | |
| 1746 | + | // Heuristic + config for auto-close intent | |
| 1747 | + | node.autoClose = wm.shouldAutoClose(url) | |
| 1748 | + | } | |
| 1749 | + | // Pipe into existing auto-close flow only for popups (confirmed by detection) | |
| 1750 | + | related.RegisterCloseHandler(func() { | |
| 1751 | + | log.Printf("[workspace] Popup requesting close via window.close()") | |
| 1752 | + | if n := wm.viewToNode[related]; n != nil && n.isPopup { | |
| 1753 | + | time.AfterFunc(200*time.Millisecond, func() { | |
| 1754 | + | webkit.IdleAdd(func() bool { | |
| 1755 | + | if err := wm.closePane(n); err != nil { | |
| 1756 | + | log.Printf("[workspace] Failed to close popup pane: %v", err) | |
| 1757 | + | } | |
| 1758 | + | return false | |
| 1759 | + | }) | |
| 1760 | + | }) | |
| 1761 | + | } | |
| 1762 | + | }) | |
| 1763 | + | ||
| 1764 | + | // URL-based fallback: if providers don't call window.close(), auto-close on OAuth callback URLs | |
| 1765 | + | if node != nil && node.isPopup && node.autoClose { | |
| 1766 | + | wm.registerOAuthAutoClose(related, url) | |
| 1767 | + | } | |
| 1768 | + | if url != "" { | |
| 1769 | + | _ = related.LoadURL(url) | |
| 1770 | + | } | |
| 1771 | + | } | |
| 1772 | + | ||
| 1773 | + | // shouldAutoClose checks simple OAuth-like URL patterns and config flag | |
| 1774 | + | func (wm *WorkspaceManager) shouldAutoClose(url string) bool { | |
| 1775 | + | if wm == nil || wm.app == nil || wm.app.config == nil { | |
| 1776 | + | return true | |
| 1777 | + | } | |
| 1778 | + | if !wm.app.config.Workspace.Popups.OAuthAutoClose { | |
| 1779 | + | return false | |
| 1780 | + | } | |
| 1781 | + | u := strings.ToLower(url) | |
| 1782 | + | patterns := []string{"oauth", "authorize", "login/oauth", "auth/callback", "connect/authorize"} | |
| 1783 | + | for _, p := range patterns { | |
| 1784 | + | if strings.Contains(u, p) { | |
| 1785 | + | return true | |
| 1786 | + | } | |
| 1787 | + | } | |
| 1788 | + | return false | |
| 1789 | + | } | |
| 1790 | + | ||
| 1791 | + | // RunOnUI schedules a function; here simply executes inline as GTK main loop is single-threaded | |
| 1792 | + | func (wm *WorkspaceManager) RunOnUI(fn func()) { | |
| 1793 | + | if fn != nil { | |
| 1794 | + | fn() | |
| 1795 | + | } | |
| 1796 | + | } | |
| 1797 | + | ||
| 1798 | + | // handleDetectedWindowType handles window type detection from smart detection path | |
| 1799 | + | func (wm *WorkspaceManager) handleDetectedWindowType(sourceNode *paneNode, webView *webkit.WebView, url string, windowType webkit.WindowType, features *webkit.WindowFeatures) { | |
| 1800 | + | if wm.viewToNode[webView] != nil { | |
| 1801 | + | return // Already placed | |
| 1802 | + | } | |
| 1803 | + | ||
| 1804 | + | log.Printf("[workspace] Smart detection result: type=%d url=%s", windowType, url) | |
| 1805 | + | ||
| 1806 | + | switch windowType { | |
| 1807 | + | case webkit.WindowTypeTab: | |
| 1808 | + | // For tabs, create a NEW independent WebView (can't use the related one) | |
| 1809 | + | webkitCfg, err := wm.app.buildWebkitConfig() | |
| 1810 | + | if err != nil { | |
| 1811 | + | log.Printf("[workspace] failed to build webkit config for tab: %v", err) | |
| 1812 | + | return | |
| 1813 | + | } | |
| 1814 | + | webkitCfg.CreateWindow = false | |
| 1815 | + | ||
| 1816 | + | // Create independent WebView like handleIntentAsTab does | |
| 1817 | + | independentView, err := webkit.NewWebView(webkitCfg) | |
| 1818 | + | if err != nil { | |
| 1819 | + | log.Printf("[workspace] failed to create independent tab WebView: %v", err) | |
| 1820 | + | return | |
| 1821 | + | } | |
| 1822 | + | ||
| 1823 | + | // The related webView was just for detection - we don't use it for tabs | |
| 1824 | + | // Insert the new independent view as a tab | |
| 1825 | + | if err := wm.insertIndependentPane(sourceNode, independentView, url); err != nil { | |
| 1826 | + | log.Printf("[workspace] Failed to insert independent pane: %v", err) | |
| 1827 | + | } | |
| 1828 | + | ||
| 1829 | + | case webkit.WindowTypePopup: | |
| 1830 | + | // For popups, use the related WebView we already created | |
| 1831 | + | wm.configureRelatedPopup(sourceNode, webView, url, features) | |
| 1832 | + | default: | |
| 1833 | + | // Fallback to popup behavior | |
| 1834 | + | wm.configureRelatedPopup(sourceNode, webView, url, features) | |
| 1835 | + | } | |
| 1836 | + | } | |
| 1375 | 1837 | ||
| 1376 | 1838 | // insertPopupPane inserts a pre-created popup pane into the workspace | |
| 1377 | 1839 | func (wm *WorkspaceManager) insertPopupPane(target *paneNode, newPane *BrowserPane, direction string) error { | |
| @@ -5,6 +5,7 @@ import ( | |||||
| 5 | "fmt" | 5 | "fmt" | ||
| 6 | "log" | 6 | "log" | ||
| 7 | "math" | 7 | "math" | ||
| 8 | + | neturl "net/url" | |||
| 8 | "strings" | 9 | "strings" | ||
| 9 | "sync" | 10 | "sync" | ||
| 10 | "time" | 11 | "time" | ||
| @@ -22,8 +23,14 @@ type paneNode struct { | |||||
| 22 | container uintptr // GtkPaned for branch nodes, stable WebView container for leaves | 23 | container uintptr // GtkPaned for branch nodes, stable WebView container for leaves | ||
| 23 | orientation webkit.Orientation | 24 | orientation webkit.Orientation | ||
| 24 | isLeaf bool | 25 | isLeaf bool | ||
| 25 | - | isPopup bool // Track if this is a popup pane for OAuth auto-close | 26 | + | isPopup bool // Deprecated: use windowType instead |
| 26 | - | hoverToken uintptr | 27 | + | // Window type tracking |
| 28 | + | windowType webkit.WindowType // Tab or Popup | |||
| 29 | + | windowFeatures *webkit.WindowFeatures // Features if popup | |||
| 30 | + | isRelated bool // Shares context | |||
| 31 | + | parentPane *paneNode // Parent for related views | |||
| 32 | + | autoClose bool // Auto-close on OAuth success | |||
| 33 | + | hoverToken uintptr | |||
| 27 | } | 34 | } | ||
| 28 | 35 | ||||
| 29 | // WorkspaceManager coordinates Zellij-style pane operations. | 36 | // WorkspaceManager coordinates Zellij-style pane operations. | ||
| @@ -241,6 +248,45 @@ func (wm *WorkspaceManager) OnWorkspaceMessage(source *webkit.WebView, msg messa | |||||
| 241 | } | 248 | } | ||
| 242 | wm.clonePaneState(node, newNode) | 249 | wm.clonePaneState(node, newNode) | ||
| 243 | wm.splitting = false | 250 | wm.splitting = false | ||
| 251 | + | case "create-pane": | |||
| 252 | + | log.Printf("[workspace] create-pane requested: url=%s action=%s", msg.URL, msg.Action) | |||
| 253 | + | ||||
| 254 | + | if msg.URL == "" { | |||
| 255 | + | log.Printf("[workspace] create-pane: empty URL, ignoring") | |||
| 256 | + | break | |||
| 257 | + | } | |||
| 258 | + | ||||
| 259 | + | // Create a basic window intent from the message | |||
| 260 | + | intent := &messaging.WindowIntent{ | |||
| 261 | + | URL: msg.URL, | |||
| 262 | + | WindowType: msg.Action, // "tab" or "popup" | |||
| 263 | + | } | |||
| 264 | + | ||||
| 265 | + | // Use the existing methods to handle tab vs popup creation | |||
| 266 | + | switch strings.ToLower(msg.Action) { | |||
| 267 | + | case "tab": | |||
| 268 | + | newView := wm.handleIntentAsTab(node, msg.URL, intent) | |||
| 269 | + | if newView != nil { | |||
| 270 | + | log.Printf("[workspace] create-pane: tab created successfully") | |||
| 271 | + | } else { | |||
| 272 | + | log.Printf("[workspace] create-pane: failed to create tab") | |||
| 273 | + | } | |||
| 274 | + | case "popup": | |||
| 275 | + | newView := wm.handleIntentAsPopup(node, msg.URL, intent) | |||
| 276 | + | if newView != nil { | |||
| 277 | + | log.Printf("[workspace] create-pane: popup created successfully") | |||
| 278 | + | } else { | |||
| 279 | + | log.Printf("[workspace] create-pane: failed to create popup") | |||
| 280 | + | } | |||
| 281 | + | default: | |||
| 282 | + | log.Printf("[workspace] create-pane: unknown action '%s', defaulting to tab", msg.Action) | |||
| 283 | + | newView := wm.handleIntentAsTab(node, msg.URL, intent) | |||
| 284 | + | if newView != nil { | |||
| 285 | + | log.Printf("[workspace] create-pane: default tab created successfully") | |||
| 286 | + | } else { | |||
| 287 | + | log.Printf("[workspace] create-pane: failed to create default tab") | |||
| 288 | + | } | |||
| 289 | + | } | |||
| 244 | default: | 290 | default: | ||
| 245 | log.Printf("[workspace] unhandled workspace event: %s", msg.Event) | 291 | log.Printf("[workspace] unhandled workspace event: %s", msg.Event) | ||
| 246 | } | 292 | } | ||
| @@ -1267,13 +1313,14 @@ func (wm *WorkspaceManager) HandlePopup(source *webkit.WebView, url string) *web | |||||
| 1267 | return nil | 1313 | return nil | ||
| 1268 | } | 1314 | } | ||
| 1269 | 1315 | ||||
| 1316 | + | // Note: HandlePopup is now obsolete - window.open is handled directly via JavaScript bypass | |||
| 1317 | + | ||||
| 1270 | cfg := wm.app.config | 1318 | cfg := wm.app.config | ||
| 1271 | if cfg == nil { | 1319 | if cfg == nil { | ||
| 1272 | log.Printf("[workspace] HandlePopup: nil config - allowing native popup") | 1320 | log.Printf("[workspace] HandlePopup: nil config - allowing native popup") | ||
| 1273 | return nil | 1321 | return nil | ||
| 1274 | } | 1322 | } | ||
| 1275 | 1323 | ||||
| 1276 | - | ||||
| 1277 | popCfg := cfg.Workspace.Popups | 1324 | popCfg := cfg.Workspace.Popups | ||
| 1278 | log.Printf("[workspace] Popup config - OpenInNewPane: %v, Placement: %s", popCfg.OpenInNewPane, popCfg.Placement) | 1325 | log.Printf("[workspace] Popup config - OpenInNewPane: %v, Placement: %s", popCfg.OpenInNewPane, popCfg.Placement) | ||
| 1279 | 1326 | ||||
| @@ -1282,15 +1329,50 @@ func (wm *WorkspaceManager) HandlePopup(source *webkit.WebView, url string) *web | |||||
| 1282 | return nil | 1329 | return nil | ||
| 1283 | } | 1330 | } | ||
| 1284 | 1331 | ||||
| 1285 | - | // Create a new WebView related to the source for process sharing and proper WindowFeatures | 1332 | + | // Smart detection path: create temporary view and decide placement once type is known |
| 1333 | + | if popCfg.EnableSmartDetection { | |||
| 1334 | + | webkitCfg, err := wm.app.buildWebkitConfig() | |||
| 1335 | + | if err != nil { | |||
| 1336 | + | log.Printf("[workspace] failed to build webkit config: %v - allowing native popup", err) | |||
| 1337 | + | return nil | |||
| 1338 | + | } | |||
| 1339 | + | webkitCfg.CreateWindow = false | |||
| 1340 | + | // Create as related to avoid WindowFeatures crash; we'll decide final placement later | |||
| 1341 | + | newView, err := webkit.NewWebViewWithRelated(webkitCfg, source) | |||
| 1342 | + | if err != nil { | |||
| 1343 | + | log.Printf("[workspace] failed to create temp WebView: %v - allowing native popup", err) | |||
| 1344 | + | return nil | |||
| 1345 | + | } | |||
| 1346 | + | ||||
| 1347 | + | // Register detection callback | |||
| 1348 | + | newView.OnWindowTypeDetected(func(t webkit.WindowType, feat *webkit.WindowFeatures) { | |||
| 1349 | + | wm.RunOnUI(func() { | |||
| 1350 | + | wm.handleDetectedWindowType(node, newView, url, t, feat) | |||
| 1351 | + | }) | |||
| 1352 | + | }) | |||
| 1353 | + | ||||
| 1354 | + | // Fallback: if detection never fires, treat as popup as before | |||
| 1355 | + | go func() { | |||
| 1356 | + | time.Sleep(1500 * time.Millisecond) | |||
| 1357 | + | if newView != nil { | |||
| 1358 | + | wm.RunOnUI(func() { | |||
| 1359 | + | if wm.viewToNode[newView] == nil { // not yet placed | |||
| 1360 | + | wm.handleDetectedWindowType(node, newView, url, webkit.WindowTypePopup, nil) | |||
| 1361 | + | } | |||
| 1362 | + | }) | |||
| 1363 | + | } | |||
| 1364 | + | }() | |||
| 1365 | + | ||||
| 1366 | + | return newView | |||
| 1367 | + | } | |||
| 1368 | + | ||||
| 1369 | + | // Legacy path preserved | |||
| 1286 | webkitCfg, err := wm.app.buildWebkitConfig() | 1370 | webkitCfg, err := wm.app.buildWebkitConfig() | ||
| 1287 | if err != nil { | 1371 | if err != nil { | ||
| 1288 | log.Printf("[workspace] failed to build webkit config: %v - allowing native popup", err) | 1372 | log.Printf("[workspace] failed to build webkit config: %v - allowing native popup", err) | ||
| 1289 | return nil | 1373 | return nil | ||
| 1290 | } | 1374 | } | ||
| 1291 | - | // Don't create window initially - we'll embed directly in workspace | |||
| 1292 | webkitCfg.CreateWindow = false | 1375 | webkitCfg.CreateWindow = false | ||
| 1293 | - | ||||
| 1294 | newView, err := webkit.NewWebViewWithRelated(webkitCfg, source) | 1376 | newView, err := webkit.NewWebViewWithRelated(webkitCfg, source) | ||
| 1295 | if err != nil { | 1377 | if err != nil { | ||
| 1296 | log.Printf("[workspace] failed to create related popup WebView: %v - allowing native popup", err) | 1378 | log.Printf("[workspace] failed to create related popup WebView: %v - allowing native popup", err) | ||
| @@ -1372,6 +1454,386 @@ func (wm *WorkspaceManager) HandlePopup(source *webkit.WebView, url string) *web | |||||
| 1372 | return newView | 1454 | return newView | ||
| 1373 | } | 1455 | } | ||
| 1374 | 1456 | ||||
| 1457 | + | // handleIntentAsTab creates an independent tab pane based on window.open intent | |||
| 1458 | + | func (wm *WorkspaceManager) registerOAuthAutoClose(view *webkit.WebView, url string) { | |||
| 1459 | + | initial := strings.TrimSpace(strings.ToLower(url)) | |||
| 1460 | + | closed := false | |||
| 1461 | + | view.RegisterURIChangedHandler(func(uri string) { | |||
| 1462 | + | if closed { | |||
| 1463 | + | return | |||
| 1464 | + | } | |||
| 1465 | + | if uri == "" { | |||
| 1466 | + | return | |||
| 1467 | + | } | |||
| 1468 | + | u := strings.ToLower(uri) | |||
| 1469 | + | if u == "about:blank" || u == initial { | |||
| 1470 | + | return | |||
| 1471 | + | } | |||
| 1472 | + | ||||
| 1473 | + | should := false | |||
| 1474 | + | if strings.Contains(u, "auth/callback") || strings.Contains(u, "callback") || strings.Contains(u, "redirect") { | |||
| 1475 | + | should = true | |||
| 1476 | + | } | |||
| 1477 | + | if !should { | |||
| 1478 | + | if parsed, err := neturl.Parse(uri); err == nil { | |||
| 1479 | + | q := parsed.Query() | |||
| 1480 | + | if q.Has("code") || q.Has("access_token") || q.Has("id_token") { | |||
| 1481 | + | should = true | |||
| 1482 | + | } | |||
| 1483 | + | if !should && parsed.Fragment != "" { | |||
| 1484 | + | frag := strings.ToLower(parsed.Fragment) | |||
| 1485 | + | if strings.Contains(frag, "access_token=") || strings.Contains(frag, "id_token=") || strings.Contains(frag, "code=") { | |||
| 1486 | + | should = true | |||
| 1487 | + | } | |||
| 1488 | + | } | |||
| 1489 | + | } | |||
| 1490 | + | } | |||
| 1491 | + | ||||
| 1492 | + | if should { | |||
| 1493 | + | log.Printf("[workspace] OAuth URL-based auto-close triggered for: %s", uri) | |||
| 1494 | + | closed = true | |||
| 1495 | + | time.AfterFunc(200*time.Millisecond, func() { | |||
| 1496 | + | webkit.IdleAdd(func() bool { | |||
| 1497 | + | if n := wm.viewToNode[view]; n != nil && n.isPopup { | |||
| 1498 | + | if err := wm.closePane(n); err != nil { | |||
| 1499 | + | log.Printf("[workspace] Failed to close OAuth popup pane: %v", err) | |||
| 1500 | + | } | |||
| 1501 | + | } | |||
| 1502 | + | return false | |||
| 1503 | + | }) | |||
| 1504 | + | }) | |||
| 1505 | + | } | |||
| 1506 | + | }) | |||
| 1507 | + | } | |||
| 1508 | + | ||||
| 1509 | + | func (wm *WorkspaceManager) applyWindowFeatures(view *webkit.WebView, intent *messaging.WindowIntent, isPopup bool) { | |||
| 1510 | + | if intent == nil { | |||
| 1511 | + | return | |||
| 1512 | + | } | |||
| 1513 | + | ||||
| 1514 | + | features := &webkit.WindowFeatures{} | |||
| 1515 | + | ||||
| 1516 | + | // Apply dimensions if specified | |||
| 1517 | + | if intent.Width != nil { | |||
| 1518 | + | features.Width = *intent.Width | |||
| 1519 | + | } | |||
| 1520 | + | if intent.Height != nil { | |||
| 1521 | + | features.Height = *intent.Height | |||
| 1522 | + | } | |||
| 1523 | + | ||||
| 1524 | + | // Apply visibility features based on window type | |||
| 1525 | + | defaultToolbar := !isPopup | |||
| 1526 | + | defaultLocation := !isPopup | |||
| 1527 | + | defaultMenubar := !isPopup | |||
| 1528 | + | ||||
| 1529 | + | if intent.Toolbar != nil { | |||
| 1530 | + | features.ToolbarVisible = *intent.Toolbar | |||
| 1531 | + | } else { | |||
| 1532 | + | features.ToolbarVisible = defaultToolbar | |||
| 1533 | + | } | |||
| 1534 | + | ||||
| 1535 | + | if intent.Location != nil { | |||
| 1536 | + | features.LocationbarVisible = *intent.Location | |||
| 1537 | + | } else { | |||
| 1538 | + | features.LocationbarVisible = defaultLocation | |||
| 1539 | + | } | |||
| 1540 | + | ||||
| 1541 | + | if intent.Menubar != nil { | |||
| 1542 | + | features.MenubarVisible = *intent.Menubar | |||
| 1543 | + | } else { | |||
| 1544 | + | features.MenubarVisible = defaultMenubar | |||
| 1545 | + | } | |||
| 1546 | + | ||||
| 1547 | + | if intent.Resizable != nil { | |||
| 1548 | + | features.Resizable = *intent.Resizable | |||
| 1549 | + | } else { | |||
| 1550 | + | features.Resizable = true // Usually resizable unless explicitly disabled | |||
| 1551 | + | } | |||
| 1552 | + | ||||
| 1553 | + | view.SetWindowFeatures(features) | |||
| 1554 | + | windowTypeStr := "tab" | |||
| 1555 | + | if isPopup { | |||
| 1556 | + | windowTypeStr = "popup" | |||
| 1557 | + | } | |||
| 1558 | + | log.Printf("[workspace] Applied %s window features from intent: size=%dx%d, toolbar=%t, location=%t, menubar=%t, resizable=%t", | |||
| 1559 | + | windowTypeStr, features.Width, features.Height, features.ToolbarVisible, features.LocationbarVisible, features.MenubarVisible, features.Resizable) | |||
| 1560 | + | } | |||
| 1561 | + | ||||
| 1562 | + | func (wm *WorkspaceManager) handleIntentAsTab(sourceNode *paneNode, url string, intent *messaging.WindowIntent) *webkit.WebView { | |||
| 1563 | + | log.Printf("[workspace] Handling intent as tab: %s", url) | |||
| 1564 | + | ||||
| 1565 | + | webkitCfg, err := wm.app.buildWebkitConfig() | |||
| 1566 | + | if err != nil { | |||
| 1567 | + | log.Printf("[workspace] failed to build webkit config: %v - allowing native popup", err) | |||
| 1568 | + | return nil | |||
| 1569 | + | } | |||
| 1570 | + | webkitCfg.CreateWindow = false | |||
| 1571 | + | ||||
| 1572 | + | newView, err := webkit.NewWebView(webkitCfg) | |||
| 1573 | + | if err != nil { | |||
| 1574 | + | log.Printf("[workspace] failed to create tab WebView: %v - allowing native popup", err) | |||
| 1575 | + | return nil | |||
| 1576 | + | } | |||
| 1577 | + | ||||
| 1578 | + | newPane, err := wm.createPane(newView) | |||
| 1579 | + | if err != nil { | |||
| 1580 | + | log.Printf("[workspace] failed to create tab pane: %v - allowing native popup", err) | |||
| 1581 | + | return nil | |||
| 1582 | + | } | |||
| 1583 | + | ||||
| 1584 | + | direction := strings.ToLower(wm.app.config.Workspace.Popups.Placement) | |||
| 1585 | + | if direction == "" { | |||
| 1586 | + | direction = "right" | |||
| 1587 | + | } | |||
| 1588 | + | ||||
| 1589 | + | if err := wm.insertPopupPane(sourceNode, newPane, direction); err != nil { | |||
| 1590 | + | log.Printf("[workspace] tab pane insertion failed: %v - allowing native popup", err) | |||
| 1591 | + | return nil | |||
| 1592 | + | } | |||
| 1593 | + | ||||
| 1594 | + | node := wm.viewToNode[newView] | |||
| 1595 | + | if node != nil { | |||
| 1596 | + | node.windowType = webkit.WindowTypeTab | |||
| 1597 | + | node.isRelated = false | |||
| 1598 | + | ||||
| 1599 | + | // Apply window features from JavaScript intent | |||
| 1600 | + | wm.applyWindowFeatures(newView, intent, false) | |||
| 1601 | + | } | |||
| 1602 | + | ||||
| 1603 | + | wm.ensureGUIInPane(newPane) | |||
| 1604 | + | ||||
| 1605 | + | if url != "" { | |||
| 1606 | + | if err := newView.LoadURL(url); err != nil { | |||
| 1607 | + | log.Printf("[workspace] failed to load tab URL: %v", err) | |||
| 1608 | + | } | |||
| 1609 | + | if err := newView.Show(); err != nil { | |||
| 1610 | + | log.Printf("[workspace] failed to show tab WebView: %v", err) | |||
| 1611 | + | } | |||
| 1612 | + | } | |||
| 1613 | + | ||||
| 1614 | + | log.Printf("[workspace] Created tab pane for URL: %s", url) | |||
| 1615 | + | return newView | |||
| 1616 | + | } | |||
| 1617 | + | ||||
| 1618 | + | // handleIntentAsPopup creates a related popup pane based on window.open intent | |||
| 1619 | + | func (wm *WorkspaceManager) handleIntentAsPopup(sourceNode *paneNode, url string, intent *messaging.WindowIntent) *webkit.WebView { | |||
| 1620 | + | log.Printf("[workspace] Handling intent as popup: %s", url) | |||
| 1621 | + | ||||
| 1622 | + | webkitCfg, err := wm.app.buildWebkitConfig() | |||
| 1623 | + | if err != nil { | |||
| 1624 | + | log.Printf("[workspace] failed to build webkit config: %v - allowing native popup", err) | |||
| 1625 | + | return nil | |||
| 1626 | + | } | |||
| 1627 | + | webkitCfg.CreateWindow = false | |||
| 1628 | + | ||||
| 1629 | + | newView, err := webkit.NewWebViewWithRelated(webkitCfg, sourceNode.pane.webView) | |||
| 1630 | + | if err != nil { | |||
| 1631 | + | log.Printf("[workspace] failed to create popup WebView: %v - allowing native popup", err) | |||
| 1632 | + | return nil | |||
| 1633 | + | } | |||
| 1634 | + | ||||
| 1635 | + | newPane, err := wm.createPane(newView) | |||
| 1636 | + | if err != nil { | |||
| 1637 | + | log.Printf("[workspace] failed to create popup pane: %v - allowing native popup", err) | |||
| 1638 | + | return nil | |||
| 1639 | + | } | |||
| 1640 | + | ||||
| 1641 | + | direction := strings.ToLower(wm.app.config.Workspace.Popups.Placement) | |||
| 1642 | + | if direction == "" { | |||
| 1643 | + | direction = "right" | |||
| 1644 | + | } | |||
| 1645 | + | ||||
| 1646 | + | if err := wm.insertPopupPane(sourceNode, newPane, direction); err != nil { | |||
| 1647 | + | log.Printf("[workspace] popup pane insertion failed: %v - allowing native popup", err) | |||
| 1648 | + | return nil | |||
| 1649 | + | } | |||
| 1650 | + | ||||
| 1651 | + | node := wm.viewToNode[newView] | |||
| 1652 | + | if node != nil { | |||
| 1653 | + | node.windowType = webkit.WindowTypePopup | |||
| 1654 | + | node.isRelated = true | |||
| 1655 | + | node.parentPane = sourceNode | |||
| 1656 | + | node.isPopup = true | |||
| 1657 | + | node.autoClose = wm.shouldAutoClose(url) | |||
| 1658 | + | ||||
| 1659 | + | // Apply window features from JavaScript intent | |||
| 1660 | + | wm.applyWindowFeatures(newView, intent, true) | |||
| 1661 | + | } | |||
| 1662 | + | ||||
| 1663 | + | // Register close handler for popup auto-close | |||
| 1664 | + | newView.RegisterCloseHandler(func() { | |||
| 1665 | + | log.Printf("[workspace] Popup requesting close via window.close()") | |||
| 1666 | + | if n := wm.viewToNode[newView]; n != nil && n.isPopup { | |||
| 1667 | + | time.AfterFunc(200*time.Millisecond, func() { | |||
| 1668 | + | webkit.IdleAdd(func() bool { | |||
| 1669 | + | if err := wm.closePane(n); err != nil { | |||
| 1670 | + | log.Printf("[workspace] Failed to close popup pane: %v", err) | |||
| 1671 | + | } | |||
| 1672 | + | return false | |||
| 1673 | + | }) | |||
| 1674 | + | }) | |||
| 1675 | + | } | |||
| 1676 | + | }) | |||
| 1677 | + | ||||
| 1678 | + | // URL-based auto-close for OAuth popups | |||
| 1679 | + | if node != nil && node.isPopup && node.autoClose { | |||
| 1680 | + | wm.registerOAuthAutoClose(newView, url) | |||
| 1681 | + | } | |||
| 1682 | + | ||||
| 1683 | + | wm.ensureGUIInPane(newPane) | |||
| 1684 | + | ||||
| 1685 | + | if url != "" { | |||
| 1686 | + | if err := newView.LoadURL(url); err != nil { | |||
| 1687 | + | log.Printf("[workspace] failed to load popup URL: %v", err) | |||
| 1688 | + | } | |||
| 1689 | + | if err := newView.Show(); err != nil { | |||
| 1690 | + | log.Printf("[workspace] failed to show popup WebView: %v", err) | |||
| 1691 | + | } | |||
| 1692 | + | } | |||
| 1693 | + | ||||
| 1694 | + | log.Printf("[workspace] Created popup pane for URL: %s", url) | |||
| 1695 | + | return newView | |||
| 1696 | + | } | |||
| 1697 | + | ||||
| 1698 | + | // insertIndependentPane inserts a new independent pane next to the source | |||
| 1699 | + | func (wm *WorkspaceManager) insertIndependentPane(sourceNode *paneNode, webView *webkit.WebView, url string) error { | |||
| 1700 | + | newPane, err := wm.createPane(webView) | |||
| 1701 | + | if err != nil { | |||
| 1702 | + | return err | |||
| 1703 | + | } | |||
| 1704 | + | direction := strings.ToLower(wm.app.config.Workspace.Popups.Placement) | |||
| 1705 | + | if direction == "" { | |||
| 1706 | + | direction = "right" | |||
| 1707 | + | } | |||
| 1708 | + | if err := wm.insertPopupPane(sourceNode, newPane, direction); err != nil { // reuse insertion primitive | |||
| 1709 | + | return err | |||
| 1710 | + | } | |||
| 1711 | + | node := wm.viewToNode[webView] | |||
| 1712 | + | if node != nil { | |||
| 1713 | + | node.windowType = webkit.WindowTypeTab | |||
| 1714 | + | node.isRelated = false | |||
| 1715 | + | } | |||
| 1716 | + | if url != "" { | |||
| 1717 | + | _ = webView.LoadURL(url) | |||
| 1718 | + | } | |||
| 1719 | + | return nil | |||
| 1720 | + | } | |||
| 1721 | + | ||||
| 1722 | + | // configureRelatedPopup creates a related view and inserts it | |||
| 1723 | + | func (wm *WorkspaceManager) configureRelatedPopup(sourceNode *paneNode, webView *webkit.WebView, url string, feat *webkit.WindowFeatures) { | |||
| 1724 | + | // Use the WebView that was already created and returned to WebKit | |||
| 1725 | + | related := webView | |||
| 1726 | + | newPane, err := wm.createPane(related) | |||
| 1727 | + | if err != nil { | |||
| 1728 | + | log.Printf("[workspace] failed to create related popup pane: %v", err) | |||
| 1729 | + | return | |||
| 1730 | + | } | |||
| 1731 | + | direction := strings.ToLower(wm.app.config.Workspace.Popups.Placement) | |||
| 1732 | + | if direction == "" { | |||
| 1733 | + | direction = "right" | |||
| 1734 | + | } | |||
| 1735 | + | if err := wm.insertPopupPane(sourceNode, newPane, direction); err != nil { | |||
| 1736 | + | log.Printf("[workspace] failed to insert related popup pane: %v", err) | |||
| 1737 | + | return | |||
| 1738 | + | } | |||
| 1739 | + | node := wm.viewToNode[related] | |||
| 1740 | + | if node != nil { | |||
| 1741 | + | node.windowType = webkit.WindowTypePopup | |||
| 1742 | + | node.windowFeatures = feat | |||
| 1743 | + | node.isRelated = true | |||
| 1744 | + | node.parentPane = sourceNode | |||
| 1745 | + | node.isPopup = true | |||
| 1746 | + | // Heuristic + config for auto-close intent | |||
| 1747 | + | node.autoClose = wm.shouldAutoClose(url) | |||
| 1748 | + | } | |||
| 1749 | + | // Pipe into existing auto-close flow only for popups (confirmed by detection) | |||
| 1750 | + | related.RegisterCloseHandler(func() { | |||
| 1751 | + | log.Printf("[workspace] Popup requesting close via window.close()") | |||
| 1752 | + | if n := wm.viewToNode[related]; n != nil && n.isPopup { | |||
| 1753 | + | time.AfterFunc(200*time.Millisecond, func() { | |||
| 1754 | + | webkit.IdleAdd(func() bool { | |||
| 1755 | + | if err := wm.closePane(n); err != nil { | |||
| 1756 | + | log.Printf("[workspace] Failed to close popup pane: %v", err) | |||
| 1757 | + | } | |||
| 1758 | + | return false | |||
| 1759 | + | }) | |||
| 1760 | + | }) | |||
| 1761 | + | } | |||
| 1762 | + | }) | |||
| 1763 | + | ||||
| 1764 | + | // URL-based fallback: if providers don't call window.close(), auto-close on OAuth callback URLs | |||
| 1765 | + | if node != nil && node.isPopup && node.autoClose { | |||
| 1766 | + | wm.registerOAuthAutoClose(related, url) | |||
| 1767 | + | } | |||
| 1768 | + | if url != "" { | |||
| 1769 | + | _ = related.LoadURL(url) | |||
| 1770 | + | } | |||
| 1771 | + | } | |||
| 1772 | + | ||||
| 1773 | + | // shouldAutoClose checks simple OAuth-like URL patterns and config flag | |||
| 1774 | + | func (wm *WorkspaceManager) shouldAutoClose(url string) bool { | |||
| 1775 | + | if wm == nil || wm.app == nil || wm.app.config == nil { | |||
| 1776 | + | return true | |||
| 1777 | + | } | |||
| 1778 | + | if !wm.app.config.Workspace.Popups.OAuthAutoClose { | |||
| 1779 | + | return false | |||
| 1780 | + | } | |||
| 1781 | + | u := strings.ToLower(url) | |||
| 1782 | + | patterns := []string{"oauth", "authorize", "login/oauth", "auth/callback", "connect/authorize"} | |||
| 1783 | + | for _, p := range patterns { | |||
| 1784 | + | if strings.Contains(u, p) { | |||
| 1785 | + | return true | |||
| 1786 | + | } | |||
| 1787 | + | } | |||
| 1788 | + | return false | |||
| 1789 | + | } | |||
| 1790 | + | ||||
| 1791 | + | // RunOnUI schedules a function; here simply executes inline as GTK main loop is single-threaded | |||
| 1792 | + | func (wm *WorkspaceManager) RunOnUI(fn func()) { | |||
| 1793 | + | if fn != nil { | |||
| 1794 | + | fn() | |||
| 1795 | + | } | |||
| 1796 | + | } | |||
| 1797 | + | ||||
| 1798 | + | // handleDetectedWindowType handles window type detection from smart detection path | |||
| 1799 | + | func (wm *WorkspaceManager) handleDetectedWindowType(sourceNode *paneNode, webView *webkit.WebView, url string, windowType webkit.WindowType, features *webkit.WindowFeatures) { | |||
| 1800 | + | if wm.viewToNode[webView] != nil { | |||
| 1801 | + | return // Already placed | |||
| 1802 | + | } | |||
| 1803 | + | ||||
| 1804 | + | log.Printf("[workspace] Smart detection result: type=%d url=%s", windowType, url) | |||
| 1805 | + | ||||
| 1806 | + | switch windowType { | |||
| 1807 | + | case webkit.WindowTypeTab: | |||
| 1808 | + | // For tabs, create a NEW independent WebView (can't use the related one) | |||
| 1809 | + | webkitCfg, err := wm.app.buildWebkitConfig() | |||
| 1810 | + | if err != nil { | |||
| 1811 | + | log.Printf("[workspace] failed to build webkit config for tab: %v", err) | |||
| 1812 | + | return | |||
| 1813 | + | } | |||
| 1814 | + | webkitCfg.CreateWindow = false | |||
| 1815 | + | ||||
| 1816 | + | // Create independent WebView like handleIntentAsTab does | |||
| 1817 | + | independentView, err := webkit.NewWebView(webkitCfg) | |||
| 1818 | + | if err != nil { | |||
| 1819 | + | log.Printf("[workspace] failed to create independent tab WebView: %v", err) | |||
| 1820 | + | return | |||
| 1821 | + | } | |||
| 1822 | + | ||||
| 1823 | + | // The related webView was just for detection - we don't use it for tabs | |||
| 1824 | + | // Insert the new independent view as a tab | |||
| 1825 | + | if err := wm.insertIndependentPane(sourceNode, independentView, url); err != nil { | |||
| 1826 | + | log.Printf("[workspace] Failed to insert independent pane: %v", err) | |||
| 1827 | + | } | |||
| 1828 | + | ||||
| 1829 | + | case webkit.WindowTypePopup: | |||
| 1830 | + | // For popups, use the related WebView we already created | |||
| 1831 | + | wm.configureRelatedPopup(sourceNode, webView, url, features) | |||
| 1832 | + | default: | |||
| 1833 | + | // Fallback to popup behavior | |||
| 1834 | + | wm.configureRelatedPopup(sourceNode, webView, url, features) | |||
| 1835 | + | } | |||
| 1836 | + | } | |||
| 1375 | 1837 | ||||
| 1376 | // insertPopupPane inserts a pre-created popup pane into the workspace | 1838 | // insertPopupPane inserts a pre-created popup pane into the workspace | ||
| 1377 | func (wm *WorkspaceManager) insertPopupPane(target *paneNode, newPane *BrowserPane, direction string) error { | 1839 | func (wm *WorkspaceManager) insertPopupPane(target *paneNode, newPane *BrowserPane, direction string) error { | ||
M internal/app/messaging/handler.go +75 -1
| @@ -11,6 +11,8 @@ import ( | |||
| 11 | 11 | ||
| 12 | 12 | "github.com/bnema/dumber/internal/app/constants" | |
| 13 | 13 | "github.com/bnema/dumber/internal/app/control" | |
| 14 | + | "github.com/bnema/dumber/internal/config" | |
| 15 | + | "github.com/bnema/dumber/internal/logging" | |
| 14 | 16 | "github.com/bnema/dumber/internal/services" | |
| 15 | 17 | "github.com/bnema/dumber/pkg/webkit" | |
| 16 | 18 | ) | |
| @@ -50,6 +52,24 @@ type Message struct { | |||
| 50 | 52 | Payload json.RawMessage `json:"payload"` | |
| 51 | 53 | } | |
| 52 | 54 | ||
| 55 | + | // WindowIntent represents an intercepted window.open call from JavaScript | |
| 56 | + | type WindowIntent struct { | |
| 57 | + | URL string `json:"url"` | |
| 58 | + | Target string `json:"target"` | |
| 59 | + | Features string `json:"features"` | |
| 60 | + | Timestamp int64 `json:"timestamp"` | |
| 61 | + | WindowType string `json:"windowType"` // "tab", "popup", "unknown" | |
| 62 | + | IsPopup bool `json:"isPopup"` | |
| 63 | + | IsTab bool `json:"isTab"` | |
| 64 | + | // Parsed features | |
| 65 | + | Width *int `json:"width,omitempty"` | |
| 66 | + | Height *int `json:"height,omitempty"` | |
| 67 | + | Toolbar *bool `json:"toolbar,omitempty"` | |
| 68 | + | Location *bool `json:"location,omitempty"` | |
| 69 | + | Menubar *bool `json:"menubar,omitempty"` | |
| 70 | + | Resizable *bool `json:"resizable,omitempty"` | |
| 71 | + | } | |
| 72 | + | ||
| 53 | 73 | // NewHandler creates a new message handler | |
| 54 | 74 | func NewHandler(parserService *services.ParserService, browserService *services.BrowserService) *Handler { | |
| 55 | 75 | return &Handler{ | |
| @@ -98,6 +118,10 @@ func (h *Handler) Handle(payload string) { | |||
| 98 | 118 | h.handleHistoryDelete(msg) | |
| 99 | 119 | case "workspace": | |
| 100 | 120 | h.handleWorkspace(msg) | |
| 121 | + | case "handle-window-open": | |
| 122 | + | h.handleWindowOpen(msg) | |
| 123 | + | case "console-message": | |
| 124 | + | h.handleConsoleMessage(msg) | |
| 101 | 125 | } | |
| 102 | 126 | } | |
| 103 | 127 | ||
| @@ -422,7 +446,7 @@ func (h *Handler) handleHistoryRecent(msg Message) { | |||
| 422 | 446 | } | |
| 423 | 447 | ||
| 424 | 448 | // handleHistoryStats processes history stats requests | |
| 425 | - | func (h *Handler) handleHistoryStats(msg Message) { | |
| 449 | + | func (h *Handler) handleHistoryStats(_ Message) { | |
| 426 | 450 | if h.browserService == nil || h.webView == nil { | |
| 427 | 451 | return | |
| 428 | 452 | } | |
| @@ -496,3 +520,53 @@ func (h *Handler) handleHistoryDelete(msg Message) { | |||
| 496 | 520 | b, _ := json.Marshal(successData) | |
| 497 | 521 | _ = h.webView.InjectScript("window.__dumber_history_deleted && window.__dumber_history_deleted(" + string(b) + ")") | |
| 498 | 522 | } | |
| 523 | + | ||
| 524 | + | // handleWindowOpen processes handle-window-open messages to create panes directly | |
| 525 | + | func (h *Handler) handleWindowOpen(msg Message) { | |
| 526 | + | var intent WindowIntent | |
| 527 | + | if err := json.Unmarshal(msg.Payload, &intent); err != nil { | |
| 528 | + | log.Printf("[messaging] Failed to unmarshal handle-window-open: %v", err) | |
| 529 | + | return | |
| 530 | + | } | |
| 531 | + | ||
| 532 | + | log.Printf("[messaging] Handling direct window.open request: url=%s type=%s", intent.URL, intent.WindowType) | |
| 533 | + | ||
| 534 | + | // Forward to workspace observer if available | |
| 535 | + | if h.workspaceObserver != nil && h.webView != nil { | |
| 536 | + | // Create a workspace message to trigger pane creation | |
| 537 | + | workspaceMsg := Message{ | |
| 538 | + | Type: "workspace", | |
| 539 | + | Event: "create-pane", | |
| 540 | + | URL: intent.URL, | |
| 541 | + | Action: intent.WindowType, // "tab" or "popup" | |
| 542 | + | } | |
| 543 | + | log.Printf("[messaging] Forwarding to workspace: %+v", workspaceMsg) | |
| 544 | + | h.workspaceObserver.OnWorkspaceMessage(h.webView, workspaceMsg) | |
| 545 | + | } else { | |
| 546 | + | log.Printf("[messaging] No workspace observer available for direct window.open") | |
| 547 | + | } | |
| 548 | + | } | |
| 549 | + | ||
| 550 | + | // handleConsoleMessage processes console-message from JavaScript | |
| 551 | + | func (h *Handler) handleConsoleMessage(msg Message) { | |
| 552 | + | // Check if console capture is enabled | |
| 553 | + | cfg := config.Get() | |
| 554 | + | if !cfg.Logging.CaptureConsole { | |
| 555 | + | return | |
| 556 | + | } | |
| 557 | + | ||
| 558 | + | // Parse the console message payload | |
| 559 | + | var consolePayload struct { | |
| 560 | + | Level string `json:"level"` | |
| 561 | + | Message string `json:"message"` | |
| 562 | + | URL string `json:"url"` | |
| 563 | + | } | |
| 564 | + | ||
| 565 | + | if err := json.Unmarshal(msg.Payload, &consolePayload); err != nil { | |
| 566 | + | log.Printf("[messaging] Failed to unmarshal console-message: %v", err) | |
| 567 | + | return | |
| 568 | + | } | |
| 569 | + | ||
| 570 | + | // Send to logging system with [CONSOLE] tag | |
| 571 | + | logging.CaptureWebKitLog(consolePayload.Message) | |
| 572 | + | } | |
| @@ -11,6 +11,8 @@ import ( | |||||
| 11 | 11 | ||||
| 12 | "github.com/bnema/dumber/internal/app/constants" | 12 | "github.com/bnema/dumber/internal/app/constants" | ||
| 13 | "github.com/bnema/dumber/internal/app/control" | 13 | "github.com/bnema/dumber/internal/app/control" | ||
| 14 | + | "github.com/bnema/dumber/internal/config" | |||
| 15 | + | "github.com/bnema/dumber/internal/logging" | |||
| 14 | "github.com/bnema/dumber/internal/services" | 16 | "github.com/bnema/dumber/internal/services" | ||
| 15 | "github.com/bnema/dumber/pkg/webkit" | 17 | "github.com/bnema/dumber/pkg/webkit" | ||
| 16 | ) | 18 | ) | ||
| @@ -50,6 +52,24 @@ type Message struct { | |||||
| 50 | Payload json.RawMessage `json:"payload"` | 52 | Payload json.RawMessage `json:"payload"` | ||
| 51 | } | 53 | } | ||
| 52 | 54 | ||||
| 55 | + | // WindowIntent represents an intercepted window.open call from JavaScript | |||
| 56 | + | type WindowIntent struct { | |||
| 57 | + | URL string `json:"url"` | |||
| 58 | + | Target string `json:"target"` | |||
| 59 | + | Features string `json:"features"` | |||
| 60 | + | Timestamp int64 `json:"timestamp"` | |||
| 61 | + | WindowType string `json:"windowType"` // "tab", "popup", "unknown" | |||
| 62 | + | IsPopup bool `json:"isPopup"` | |||
| 63 | + | IsTab bool `json:"isTab"` | |||
| 64 | + | // Parsed features | |||
| 65 | + | Width *int `json:"width,omitempty"` | |||
| 66 | + | Height *int `json:"height,omitempty"` | |||
| 67 | + | Toolbar *bool `json:"toolbar,omitempty"` | |||
| 68 | + | Location *bool `json:"location,omitempty"` | |||
| 69 | + | Menubar *bool `json:"menubar,omitempty"` | |||
| 70 | + | Resizable *bool `json:"resizable,omitempty"` | |||
| 71 | + | } | |||
| 72 | + | ||||
| 53 | // NewHandler creates a new message handler | 73 | // NewHandler creates a new message handler | ||
| 54 | func NewHandler(parserService *services.ParserService, browserService *services.BrowserService) *Handler { | 74 | func NewHandler(parserService *services.ParserService, browserService *services.BrowserService) *Handler { | ||
| 55 | return &Handler{ | 75 | return &Handler{ | ||
| @@ -98,6 +118,10 @@ func (h *Handler) Handle(payload string) { | |||||
| 98 | h.handleHistoryDelete(msg) | 118 | h.handleHistoryDelete(msg) | ||
| 99 | case "workspace": | 119 | case "workspace": | ||
| 100 | h.handleWorkspace(msg) | 120 | h.handleWorkspace(msg) | ||
| 121 | + | case "handle-window-open": | |||
| 122 | + | h.handleWindowOpen(msg) | |||
| 123 | + | case "console-message": | |||
| 124 | + | h.handleConsoleMessage(msg) | |||
| 101 | } | 125 | } | ||
| 102 | } | 126 | } | ||
| 103 | 127 | ||||
| @@ -422,7 +446,7 @@ func (h *Handler) handleHistoryRecent(msg Message) { | |||||
| 422 | } | 446 | } | ||
| 423 | 447 | ||||
| 424 | // handleHistoryStats processes history stats requests | 448 | // handleHistoryStats processes history stats requests | ||
| 425 | - | func (h *Handler) handleHistoryStats(msg Message) { | 449 | + | func (h *Handler) handleHistoryStats(_ Message) { |
| 426 | if h.browserService == nil || h.webView == nil { | 450 | if h.browserService == nil || h.webView == nil { | ||
| 427 | return | 451 | return | ||
| 428 | } | 452 | } | ||
| @@ -496,3 +520,53 @@ func (h *Handler) handleHistoryDelete(msg Message) { | |||||
| 496 | b, _ := json.Marshal(successData) | 520 | b, _ := json.Marshal(successData) | ||
| 497 | _ = h.webView.InjectScript("window.__dumber_history_deleted && window.__dumber_history_deleted(" + string(b) + ")") | 521 | _ = h.webView.InjectScript("window.__dumber_history_deleted && window.__dumber_history_deleted(" + string(b) + ")") | ||
| 498 | } | 522 | } | ||
| 523 | + | ||||
| 524 | + | // handleWindowOpen processes handle-window-open messages to create panes directly | |||
| 525 | + | func (h *Handler) handleWindowOpen(msg Message) { | |||
| 526 | + | var intent WindowIntent | |||
| 527 | + | if err := json.Unmarshal(msg.Payload, &intent); err != nil { | |||
| 528 | + | log.Printf("[messaging] Failed to unmarshal handle-window-open: %v", err) | |||
| 529 | + | return | |||
| 530 | + | } | |||
| 531 | + | ||||
| 532 | + | log.Printf("[messaging] Handling direct window.open request: url=%s type=%s", intent.URL, intent.WindowType) | |||
| 533 | + | ||||
| 534 | + | // Forward to workspace observer if available | |||
| 535 | + | if h.workspaceObserver != nil && h.webView != nil { | |||
| 536 | + | // Create a workspace message to trigger pane creation | |||
| 537 | + | workspaceMsg := Message{ | |||
| 538 | + | Type: "workspace", | |||
| 539 | + | Event: "create-pane", | |||
| 540 | + | URL: intent.URL, | |||
| 541 | + | Action: intent.WindowType, // "tab" or "popup" | |||
| 542 | + | } | |||
| 543 | + | log.Printf("[messaging] Forwarding to workspace: %+v", workspaceMsg) | |||
| 544 | + | h.workspaceObserver.OnWorkspaceMessage(h.webView, workspaceMsg) | |||
| 545 | + | } else { | |||
| 546 | + | log.Printf("[messaging] No workspace observer available for direct window.open") | |||
| 547 | + | } | |||
| 548 | + | } | |||
| 549 | + | ||||
| 550 | + | // handleConsoleMessage processes console-message from JavaScript | |||
| 551 | + | func (h *Handler) handleConsoleMessage(msg Message) { | |||
| 552 | + | // Check if console capture is enabled | |||
| 553 | + | cfg := config.Get() | |||
| 554 | + | if !cfg.Logging.CaptureConsole { | |||
| 555 | + | return | |||
| 556 | + | } | |||
| 557 | + | ||||
| 558 | + | // Parse the console message payload | |||
| 559 | + | var consolePayload struct { | |||
| 560 | + | Level string `json:"level"` | |||
| 561 | + | Message string `json:"message"` | |||
| 562 | + | URL string `json:"url"` | |||
| 563 | + | } | |||
| 564 | + | ||||
| 565 | + | if err := json.Unmarshal(msg.Payload, &consolePayload); err != nil { | |||
| 566 | + | log.Printf("[messaging] Failed to unmarshal console-message: %v", err) | |||
| 567 | + | return | |||
| 568 | + | } | |||
| 569 | + | ||||
| 570 | + | // Send to logging system with [CONSOLE] tag | |||
| 571 | + | logging.CaptureWebKitLog(consolePayload.Message) | |||
| 572 | + | } | |||
M internal/cache/favicon.go +18 -4
| @@ -135,7 +135,11 @@ func (fc *FaviconCache) downloadFavicon(faviconURL string) error { | |||
| 135 | 135 | log.Printf("[favicon] failed to close download file: %v", err) | |
| 136 | 136 | } | |
| 137 | 137 | }() | |
| 138 | - | defer os.Remove(tempDownloadFile) | |
| 138 | + | defer func() { | |
| 139 | + | if err := os.Remove(tempDownloadFile); err != nil { | |
| 140 | + | log.Printf("[favicon] failed to remove temp download file: %v", err) | |
| 141 | + | } | |
| 142 | + | }() | |
| 139 | 143 | ||
| 140 | 144 | // Save raw favicon data | |
| 141 | 145 | if _, err := io.Copy(downloadFile, limitedBody); err != nil { | |
| @@ -177,7 +181,11 @@ func (fc *FaviconCache) tryStandardImageDecode(filePath string) (image.Image, er | |||
| 177 | 181 | if err != nil { | |
| 178 | 182 | return nil, err | |
| 179 | 183 | } | |
| 180 | - | defer file.Close() | |
| 184 | + | defer func() { | |
| 185 | + | if err := file.Close(); err != nil { | |
| 186 | + | log.Printf("[favicon] failed to close file: %v", err) | |
| 187 | + | } | |
| 188 | + | }() | |
| 181 | 189 | ||
| 182 | 190 | img, _, err := image.Decode(file) | |
| 183 | 191 | return img, err | |
| @@ -190,10 +198,16 @@ func (fc *FaviconCache) savePNG(img image.Image, outputPath string) error { | |||
| 190 | 198 | if err != nil { | |
| 191 | 199 | return fmt.Errorf("failed to create temp file: %w", err) | |
| 192 | 200 | } | |
| 193 | - | defer file.Close() | |
| 201 | + | defer func() { | |
| 202 | + | if err := file.Close(); err != nil { | |
| 203 | + | log.Printf("[favicon] failed to close file: %v", err) | |
| 204 | + | } | |
| 205 | + | }() | |
| 194 | 206 | ||
| 195 | 207 | if err := png.Encode(file, img); err != nil { | |
| 196 | - | os.Remove(tempFile) | |
| 208 | + | if removeErr := os.Remove(tempFile); removeErr != nil { | |
| 209 | + | log.Printf("[favicon] failed to remove temp file: %v", removeErr) | |
| 210 | + | } | |
| 197 | 211 | return fmt.Errorf("failed to encode PNG: %w", err) | |
| 198 | 212 | } | |
| 199 | 213 | ||
| @@ -135,7 +135,11 @@ func (fc *FaviconCache) downloadFavicon(faviconURL string) error { | |||||
| 135 | log.Printf("[favicon] failed to close download file: %v", err) | 135 | log.Printf("[favicon] failed to close download file: %v", err) | ||
| 136 | } | 136 | } | ||
| 137 | }() | 137 | }() | ||
| 138 | - | defer os.Remove(tempDownloadFile) | 138 | + | defer func() { |
| 139 | + | if err := os.Remove(tempDownloadFile); err != nil { | |||
| 140 | + | log.Printf("[favicon] failed to remove temp download file: %v", err) | |||
| 141 | + | } | |||
| 142 | + | }() | |||
| 139 | 143 | ||||
| 140 | // Save raw favicon data | 144 | // Save raw favicon data | ||
| 141 | if _, err := io.Copy(downloadFile, limitedBody); err != nil { | 145 | if _, err := io.Copy(downloadFile, limitedBody); err != nil { | ||
| @@ -177,7 +181,11 @@ func (fc *FaviconCache) tryStandardImageDecode(filePath string) (image.Image, er | |||||
| 177 | if err != nil { | 181 | if err != nil { | ||
| 178 | return nil, err | 182 | return nil, err | ||
| 179 | } | 183 | } | ||
| 180 | - | defer file.Close() | 184 | + | defer func() { |
| 185 | + | if err := file.Close(); err != nil { | |||
| 186 | + | log.Printf("[favicon] failed to close file: %v", err) | |||
| 187 | + | } | |||
| 188 | + | }() | |||
| 181 | 189 | ||||
| 182 | img, _, err := image.Decode(file) | 190 | img, _, err := image.Decode(file) | ||
| 183 | return img, err | 191 | return img, err | ||
| @@ -190,10 +198,16 @@ func (fc *FaviconCache) savePNG(img image.Image, outputPath string) error { | |||||
| 190 | if err != nil { | 198 | if err != nil { | ||
| 191 | return fmt.Errorf("failed to create temp file: %w", err) | 199 | return fmt.Errorf("failed to create temp file: %w", err) | ||
| 192 | } | 200 | } | ||
| 193 | - | defer file.Close() | 201 | + | defer func() { |
| 202 | + | if err := file.Close(); err != nil { | |||
| 203 | + | log.Printf("[favicon] failed to close file: %v", err) | |||
| 204 | + | } | |||
| 205 | + | }() | |||
| 194 | 206 | ||||
| 195 | if err := png.Encode(file, img); err != nil { | 207 | if err := png.Encode(file, img); err != nil { | ||
| 196 | - | os.Remove(tempFile) | 208 | + | if removeErr := os.Remove(tempFile); removeErr != nil { |
| 209 | + | log.Printf("[favicon] failed to remove temp file: %v", removeErr) | |||
| 210 | + | } | |||
| 197 | return fmt.Errorf("failed to encode PNG: %w", err) | 211 | return fmt.Errorf("failed to encode PNG: %w", err) | ||
| 198 | } | 212 | } | ||
| 199 | 213 | ||||
M internal/cache/filter_cache.go +10 -3
| @@ -4,6 +4,7 @@ import ( | |||
| 4 | 4 | "encoding/binary" | |
| 5 | 5 | "fmt" | |
| 6 | 6 | "io" | |
| 7 | + | "log" | |
| 7 | 8 | "os" | |
| 8 | 9 | "syscall" | |
| 9 | 10 | "time" | |
| @@ -110,18 +111,24 @@ func (fc *FilterCache) LoadMapped() (*FilterData, error) { | |||
| 110 | 111 | // Parse the header | |
| 111 | 112 | header, err := fc.parseHeader() | |
| 112 | 113 | if err != nil { | |
| 113 | - | fc.Unmap() | |
| 114 | + | if unmapErr := fc.Unmap(); unmapErr != nil { | |
| 115 | + | log.Printf("[filter-cache] failed to unmap after header parse error: %v", unmapErr) | |
| 116 | + | } | |
| 114 | 117 | return nil, fmt.Errorf("invalid cache header: %w", err) | |
| 115 | 118 | } | |
| 116 | 119 | ||
| 117 | 120 | // Validate magic and version | |
| 118 | 121 | if header.Magic != cacheMagic { | |
| 119 | - | fc.Unmap() | |
| 122 | + | if unmapErr := fc.Unmap(); unmapErr != nil { | |
| 123 | + | log.Printf("[filter-cache] failed to unmap after magic validation: %v", unmapErr) | |
| 124 | + | } | |
| 120 | 125 | return nil, fmt.Errorf("invalid cache magic bytes") | |
| 121 | 126 | } | |
| 122 | 127 | ||
| 123 | 128 | if header.Version != cacheFormatVersion { | |
| 124 | - | fc.Unmap() | |
| 129 | + | if unmapErr := fc.Unmap(); unmapErr != nil { | |
| 130 | + | log.Printf("[filter-cache] failed to unmap after version validation: %v", unmapErr) | |
| 131 | + | } | |
| 125 | 132 | return nil, fmt.Errorf("unsupported cache version: %d", header.Version) | |
| 126 | 133 | } | |
| 127 | 134 | ||
| @@ -4,6 +4,7 @@ import ( | |||||
| 4 | "encoding/binary" | 4 | "encoding/binary" | ||
| 5 | "fmt" | 5 | "fmt" | ||
| 6 | "io" | 6 | "io" | ||
| 7 | + | "log" | |||
| 7 | "os" | 8 | "os" | ||
| 8 | "syscall" | 9 | "syscall" | ||
| 9 | "time" | 10 | "time" | ||
| @@ -110,18 +111,24 @@ func (fc *FilterCache) LoadMapped() (*FilterData, error) { | |||||
| 110 | // Parse the header | 111 | // Parse the header | ||
| 111 | header, err := fc.parseHeader() | 112 | header, err := fc.parseHeader() | ||
| 112 | if err != nil { | 113 | if err != nil { | ||
| 113 | - | fc.Unmap() | 114 | + | if unmapErr := fc.Unmap(); unmapErr != nil { |
| 115 | + | log.Printf("[filter-cache] failed to unmap after header parse error: %v", unmapErr) | |||
| 116 | + | } | |||
| 114 | return nil, fmt.Errorf("invalid cache header: %w", err) | 117 | return nil, fmt.Errorf("invalid cache header: %w", err) | ||
| 115 | } | 118 | } | ||
| 116 | 119 | ||||
| 117 | // Validate magic and version | 120 | // Validate magic and version | ||
| 118 | if header.Magic != cacheMagic { | 121 | if header.Magic != cacheMagic { | ||
| 119 | - | fc.Unmap() | 122 | + | if unmapErr := fc.Unmap(); unmapErr != nil { |
| 123 | + | log.Printf("[filter-cache] failed to unmap after magic validation: %v", unmapErr) | |||
| 124 | + | } | |||
| 120 | return nil, fmt.Errorf("invalid cache magic bytes") | 125 | return nil, fmt.Errorf("invalid cache magic bytes") | ||
| 121 | } | 126 | } | ||
| 122 | 127 | ||||
| 123 | if header.Version != cacheFormatVersion { | 128 | if header.Version != cacheFormatVersion { | ||
| 124 | - | fc.Unmap() | 129 | + | if unmapErr := fc.Unmap(); unmapErr != nil { |
| 130 | + | log.Printf("[filter-cache] failed to unmap after version validation: %v", unmapErr) | |||
| 131 | + | } | |||
| 125 | return nil, fmt.Errorf("unsupported cache version: %d", header.Version) | 132 | return nil, fmt.Errorf("unsupported cache version: %d", header.Version) | ||
| 126 | } | 133 | } | ||
| 127 | 134 | ||||
M internal/cli/logs.go +87 -0
| @@ -26,6 +26,7 @@ func NewLogsCmd() *cobra.Command { | |||
| 26 | 26 | cmd.AddCommand(newLogsListCmd()) | |
| 27 | 27 | cmd.AddCommand(newLogsCleanCmd()) | |
| 28 | 28 | cmd.AddCommand(newLogsPathCmd()) | |
| 29 | + | cmd.AddCommand(newLogsConsoleCmd()) | |
| 29 | 30 | ||
| 30 | 31 | return cmd | |
| 31 | 32 | } | |
| @@ -126,6 +127,32 @@ func newLogsPathCmd() *cobra.Command { | |||
| 126 | 127 | return cmd | |
| 127 | 128 | } | |
| 128 | 129 | ||
| 130 | + | // newLogsConsoleCmd creates the console subcommand for filtering console logs | |
| 131 | + | func newLogsConsoleCmd() *cobra.Command { | |
| 132 | + | var lines int | |
| 133 | + | var level string | |
| 134 | + | ||
| 135 | + | cmd := &cobra.Command{ | |
| 136 | + | Use: "console", | |
| 137 | + | Short: "View console logs from WebKit", | |
| 138 | + | Long: "Display console.log, console.error, and other console messages from web pages.", | |
| 139 | + | RunE: func(cmd *cobra.Command, args []string) error { | |
| 140 | + | logDir, err := config.GetLogDir() | |
| 141 | + | if err != nil { | |
| 142 | + | return fmt.Errorf("failed to get log directory: %w", err) | |
| 143 | + | } | |
| 144 | + | ||
| 145 | + | currentLog := filepath.Join(logDir, "dumber.log") | |
| 146 | + | return tailConsoleMessages(currentLog, lines, level) | |
| 147 | + | }, | |
| 148 | + | } | |
| 149 | + | ||
| 150 | + | cmd.Flags().IntVarP(&lines, "lines", "n", 50, "Number of lines to show") | |
| 151 | + | cmd.Flags().StringVarP(&level, "level", "l", "", "Filter by console level (error, warn, info, log, debug)") | |
| 152 | + | ||
| 153 | + | return cmd | |
| 154 | + | } | |
| 155 | + | ||
| 129 | 156 | // tailFile displays the last N lines of a file | |
| 130 | 157 | func tailFile(filePath string, lines int) error { | |
| 131 | 158 | file, err := os.Open(filePath) | |
| @@ -297,3 +324,63 @@ func formatSize(size int64) string { | |||
| 297 | 324 | ||
| 298 | 325 | return fmt.Sprintf("%.1f %cB", float64(size)/float64(div), "KMGTPE"[exp]) | |
| 299 | 326 | } | |
| 327 | + | ||
| 328 | + | // tailConsoleMessages displays console messages from the log file | |
| 329 | + | func tailConsoleMessages(filePath string, lines int, level string) error { | |
| 330 | + | file, err := os.Open(filePath) | |
| 331 | + | if err != nil { | |
| 332 | + | if os.IsNotExist(err) { | |
| 333 | + | fmt.Printf("Log file does not exist: %s\n", filePath) | |
| 334 | + | return nil | |
| 335 | + | } | |
| 336 | + | return fmt.Errorf("failed to open log file: %w", err) | |
| 337 | + | } | |
| 338 | + | defer file.Close() | |
| 339 | + | ||
| 340 | + | // Read all lines and filter for console messages | |
| 341 | + | var consoleLines []string | |
| 342 | + | scanner := bufio.NewScanner(file) | |
| 343 | + | for scanner.Scan() { | |
| 344 | + | line := scanner.Text() | |
| 345 | + | ||
| 346 | + | // Filter for [CONSOLE] tagged messages | |
| 347 | + | if !strings.Contains(line, "[CONSOLE]") { | |
| 348 | + | continue | |
| 349 | + | } | |
| 350 | + | ||
| 351 | + | // Filter by level if specified | |
| 352 | + | if level != "" { | |
| 353 | + | levelFilter := fmt.Sprintf("[%s]", strings.ToUpper(level)) | |
| 354 | + | if !strings.Contains(line, levelFilter) { | |
| 355 | + | continue | |
| 356 | + | } | |
| 357 | + | } | |
| 358 | + | ||
| 359 | + | consoleLines = append(consoleLines, line) | |
| 360 | + | } | |
| 361 | + | ||
| 362 | + | if err := scanner.Err(); err != nil { | |
| 363 | + | return fmt.Errorf("error reading log file: %w", err) | |
| 364 | + | } | |
| 365 | + | ||
| 366 | + | if len(consoleLines) == 0 { | |
| 367 | + | if level != "" { | |
| 368 | + | fmt.Printf("No console messages found with level '%s'\n", level) | |
| 369 | + | } else { | |
| 370 | + | fmt.Println("No console messages found") | |
| 371 | + | } | |
| 372 | + | return nil | |
| 373 | + | } | |
| 374 | + | ||
| 375 | + | // Display last N lines | |
| 376 | + | start := len(consoleLines) - lines | |
| 377 | + | if start < 0 { | |
| 378 | + | start = 0 | |
| 379 | + | } | |
| 380 | + | ||
| 381 | + | for i := start; i < len(consoleLines); i++ { | |
| 382 | + | fmt.Println(consoleLines[i]) | |
| 383 | + | } | |
| 384 | + | ||
| 385 | + | return nil | |
| 386 | + | } | |
| @@ -26,6 +26,7 @@ func NewLogsCmd() *cobra.Command { | |||||
| 26 | cmd.AddCommand(newLogsListCmd()) | 26 | cmd.AddCommand(newLogsListCmd()) | ||
| 27 | cmd.AddCommand(newLogsCleanCmd()) | 27 | cmd.AddCommand(newLogsCleanCmd()) | ||
| 28 | cmd.AddCommand(newLogsPathCmd()) | 28 | cmd.AddCommand(newLogsPathCmd()) | ||
| 29 | + | cmd.AddCommand(newLogsConsoleCmd()) | |||
| 29 | 30 | ||||
| 30 | return cmd | 31 | return cmd | ||
| 31 | } | 32 | } | ||
| @@ -126,6 +127,32 @@ func newLogsPathCmd() *cobra.Command { | |||||
| 126 | return cmd | 127 | return cmd | ||
| 127 | } | 128 | } | ||
| 128 | 129 | ||||
| 130 | + | // newLogsConsoleCmd creates the console subcommand for filtering console logs | |||
| 131 | + | func newLogsConsoleCmd() *cobra.Command { | |||
| 132 | + | var lines int | |||
| 133 | + | var level string | |||
| 134 | + | ||||
| 135 | + | cmd := &cobra.Command{ | |||
| 136 | + | Use: "console", | |||
| 137 | + | Short: "View console logs from WebKit", | |||
| 138 | + | Long: "Display console.log, console.error, and other console messages from web pages.", | |||
| 139 | + | RunE: func(cmd *cobra.Command, args []string) error { | |||
| 140 | + | logDir, err := config.GetLogDir() | |||
| 141 | + | if err != nil { | |||
| 142 | + | return fmt.Errorf("failed to get log directory: %w", err) | |||
| 143 | + | } | |||
| 144 | + | ||||
| 145 | + | currentLog := filepath.Join(logDir, "dumber.log") | |||
| 146 | + | return tailConsoleMessages(currentLog, lines, level) | |||
| 147 | + | }, | |||
| 148 | + | } | |||
| 149 | + | ||||
| 150 | + | cmd.Flags().IntVarP(&lines, "lines", "n", 50, "Number of lines to show") | |||
| 151 | + | cmd.Flags().StringVarP(&level, "level", "l", "", "Filter by console level (error, warn, info, log, debug)") | |||
| 152 | + | ||||
| 153 | + | return cmd | |||
| 154 | + | } | |||
| 155 | + | ||||
| 129 | // tailFile displays the last N lines of a file | 156 | // tailFile displays the last N lines of a file | ||
| 130 | func tailFile(filePath string, lines int) error { | 157 | func tailFile(filePath string, lines int) error { | ||
| 131 | file, err := os.Open(filePath) | 158 | file, err := os.Open(filePath) | ||
| @@ -297,3 +324,63 @@ func formatSize(size int64) string { | |||||
| 297 | 324 | ||||
| 298 | return fmt.Sprintf("%.1f %cB", float64(size)/float64(div), "KMGTPE"[exp]) | 325 | return fmt.Sprintf("%.1f %cB", float64(size)/float64(div), "KMGTPE"[exp]) | ||
| 299 | } | 326 | } | ||
| 327 | + | ||||
| 328 | + | // tailConsoleMessages displays console messages from the log file | |||
| 329 | + | func tailConsoleMessages(filePath string, lines int, level string) error { | |||
| 330 | + | file, err := os.Open(filePath) | |||
| 331 | + | if err != nil { | |||
| 332 | + | if os.IsNotExist(err) { | |||
| 333 | + | fmt.Printf("Log file does not exist: %s\n", filePath) | |||
| 334 | + | return nil | |||
| 335 | + | } | |||
| 336 | + | return fmt.Errorf("failed to open log file: %w", err) | |||
| 337 | + | } | |||
| 338 | + | defer file.Close() | |||
| 339 | + | ||||
| 340 | + | // Read all lines and filter for console messages | |||
| 341 | + | var consoleLines []string | |||
| 342 | + | scanner := bufio.NewScanner(file) | |||
| 343 | + | for scanner.Scan() { | |||
| 344 | + | line := scanner.Text() | |||
| 345 | + | ||||
| 346 | + | // Filter for [CONSOLE] tagged messages | |||
| 347 | + | if !strings.Contains(line, "[CONSOLE]") { | |||
| 348 | + | continue | |||
| 349 | + | } | |||
| 350 | + | ||||
| 351 | + | // Filter by level if specified | |||
| 352 | + | if level != "" { | |||
| 353 | + | levelFilter := fmt.Sprintf("[%s]", strings.ToUpper(level)) | |||
| 354 | + | if !strings.Contains(line, levelFilter) { | |||
| 355 | + | continue | |||
| 356 | + | } | |||
| 357 | + | } | |||
| 358 | + | ||||
| 359 | + | consoleLines = append(consoleLines, line) | |||
| 360 | + | } | |||
| 361 | + | ||||
| 362 | + | if err := scanner.Err(); err != nil { | |||
| 363 | + | return fmt.Errorf("error reading log file: %w", err) | |||
| 364 | + | } | |||
| 365 | + | ||||
| 366 | + | if len(consoleLines) == 0 { | |||
| 367 | + | if level != "" { | |||
| 368 | + | fmt.Printf("No console messages found with level '%s'\n", level) | |||
| 369 | + | } else { | |||
| 370 | + | fmt.Println("No console messages found") | |||
| 371 | + | } | |||
| 372 | + | return nil | |||
| 373 | + | } | |||
| 374 | + | ||||
| 375 | + | // Display last N lines | |||
| 376 | + | start := len(consoleLines) - lines | |||
| 377 | + | if start < 0 { | |||
| 378 | + | start = 0 | |||
| 379 | + | } | |||
| 380 | + | ||||
| 381 | + | for i := start; i < len(consoleLines); i++ { | |||
| 382 | + | fmt.Println(consoleLines[i]) | |||
| 383 | + | } | |||
| 384 | + | ||||
| 385 | + | return nil | |||
| 386 | + | } | |||
M internal/config/config.go +17 -3
| @@ -101,9 +101,10 @@ type LoggingConfig struct { | |||
| 101 | 101 | EnableFileLog bool `mapstructure:"enable_file_log" yaml:"enable_file_log"` | |
| 102 | 102 | ||
| 103 | 103 | // Capture settings | |
| 104 | - | CaptureStdout bool `mapstructure:"capture_stdout" yaml:"capture_stdout"` | |
| 105 | - | CaptureStderr bool `mapstructure:"capture_stderr" yaml:"capture_stderr"` | |
| 106 | - | CaptureCOutput bool `mapstructure:"capture_c_output" yaml:"capture_c_output"` | |
| 104 | + | CaptureStdout bool `mapstructure:"capture_stdout" yaml:"capture_stdout" json:"capture_stdout"` | |
| 105 | + | CaptureStderr bool `mapstructure:"capture_stderr" yaml:"capture_stderr" json:"capture_stderr"` | |
| 106 | + | CaptureCOutput bool `mapstructure:"capture_c_output" yaml:"capture_c_output" json:"capture_c_output"` | |
| 107 | + | CaptureConsole bool `mapstructure:"capture_console" yaml:"capture_console" json:"capture_console"` | |
| 107 | 108 | ||
| 108 | 109 | // Debug output | |
| 109 | 110 | DebugFile string `mapstructure:"debug_file" yaml:"debug_file"` | |
| @@ -258,6 +259,15 @@ type PopupBehaviorConfig struct { | |||
| 258 | 259 | Placement string `mapstructure:"placement" yaml:"placement" json:"placement"` | |
| 259 | 260 | OpenInNewPane bool `mapstructure:"open_in_new_pane" yaml:"open_in_new_pane" json:"open_in_new_pane"` | |
| 260 | 261 | FollowPaneContext bool `mapstructure:"follow_pane_context" yaml:"follow_pane_context" json:"follow_pane_context"` | |
| 262 | + | ||
| 263 | + | // New fields for distinguishing window types and behaviors | |
| 264 | + | // BlankTargetBehavior determines how to handle window.open(url, "_blank") intents | |
| 265 | + | // Accepted values: "pane" (default) or "tab" (future support) | |
| 266 | + | BlankTargetBehavior string `mapstructure:"blank_target_behavior" yaml:"blank_target_behavior" json:"blank_target_behavior"` | |
| 267 | + | // EnableSmartDetection uses WebKitWindowProperties to detect popup vs tab intents | |
| 268 | + | EnableSmartDetection bool `mapstructure:"enable_smart_detection" yaml:"enable_smart_detection" json:"enable_smart_detection"` | |
| 269 | + | // OAuthAutoClose enables auto-closing OAuth popups after successful auth redirects | |
| 270 | + | OAuthAutoClose bool `mapstructure:"oauth_auto_close" yaml:"oauth_auto_close" json:"oauth_auto_close"` | |
| 261 | 271 | } | |
| 262 | 272 | ||
| 263 | 273 | // WorkspaceStylingConfig defines visual styling for workspace panes. | |
| @@ -678,6 +688,10 @@ func (m *Manager) setDefaults() { | |||
| 678 | 688 | m.viper.SetDefault("workspace.popups.placement", defaults.Workspace.Popups.Placement) | |
| 679 | 689 | m.viper.SetDefault("workspace.popups.open_in_new_pane", defaults.Workspace.Popups.OpenInNewPane) | |
| 680 | 690 | m.viper.SetDefault("workspace.popups.follow_pane_context", defaults.Workspace.Popups.FollowPaneContext) | |
| 691 | + | // New popup behaviour defaults | |
| 692 | + | m.viper.SetDefault("workspace.popups.blank_target_behavior", defaults.Workspace.Popups.BlankTargetBehavior) | |
| 693 | + | m.viper.SetDefault("workspace.popups.enable_smart_detection", defaults.Workspace.Popups.EnableSmartDetection) | |
| 694 | + | m.viper.SetDefault("workspace.popups.oauth_auto_close", defaults.Workspace.Popups.OAuthAutoClose) | |
| 681 | 695 | m.viper.SetDefault("workspace.styling.border_width", defaults.Workspace.Styling.BorderWidth) | |
| 682 | 696 | m.viper.SetDefault("workspace.styling.border_color", defaults.Workspace.Styling.BorderColor) | |
| 683 | 697 | m.viper.SetDefault("workspace.styling.transition_duration", defaults.Workspace.Styling.TransitionDuration) | |
| @@ -101,9 +101,10 @@ type LoggingConfig struct { | |||||
| 101 | EnableFileLog bool `mapstructure:"enable_file_log" yaml:"enable_file_log"` | 101 | EnableFileLog bool `mapstructure:"enable_file_log" yaml:"enable_file_log"` | ||
| 102 | 102 | ||||
| 103 | // Capture settings | 103 | // Capture settings | ||
| 104 | - | CaptureStdout bool `mapstructure:"capture_stdout" yaml:"capture_stdout"` | 104 | + | CaptureStdout bool `mapstructure:"capture_stdout" yaml:"capture_stdout" json:"capture_stdout"` |
| 105 | - | CaptureStderr bool `mapstructure:"capture_stderr" yaml:"capture_stderr"` | 105 | + | CaptureStderr bool `mapstructure:"capture_stderr" yaml:"capture_stderr" json:"capture_stderr"` |
| 106 | - | CaptureCOutput bool `mapstructure:"capture_c_output" yaml:"capture_c_output"` | 106 | + | CaptureCOutput bool `mapstructure:"capture_c_output" yaml:"capture_c_output" json:"capture_c_output"` |
| 107 | + | CaptureConsole bool `mapstructure:"capture_console" yaml:"capture_console" json:"capture_console"` | |||
| 107 | 108 | ||||
| 108 | // Debug output | 109 | // Debug output | ||
| 109 | DebugFile string `mapstructure:"debug_file" yaml:"debug_file"` | 110 | DebugFile string `mapstructure:"debug_file" yaml:"debug_file"` | ||
| @@ -258,6 +259,15 @@ type PopupBehaviorConfig struct { | |||||
| 258 | Placement string `mapstructure:"placement" yaml:"placement" json:"placement"` | 259 | Placement string `mapstructure:"placement" yaml:"placement" json:"placement"` | ||
| 259 | OpenInNewPane bool `mapstructure:"open_in_new_pane" yaml:"open_in_new_pane" json:"open_in_new_pane"` | 260 | OpenInNewPane bool `mapstructure:"open_in_new_pane" yaml:"open_in_new_pane" json:"open_in_new_pane"` | ||
| 260 | FollowPaneContext bool `mapstructure:"follow_pane_context" yaml:"follow_pane_context" json:"follow_pane_context"` | 261 | FollowPaneContext bool `mapstructure:"follow_pane_context" yaml:"follow_pane_context" json:"follow_pane_context"` | ||
| 262 | + | ||||
| 263 | + | // New fields for distinguishing window types and behaviors | |||
| 264 | + | // BlankTargetBehavior determines how to handle window.open(url, "_blank") intents | |||
| 265 | + | // Accepted values: "pane" (default) or "tab" (future support) | |||
| 266 | + | BlankTargetBehavior string `mapstructure:"blank_target_behavior" yaml:"blank_target_behavior" json:"blank_target_behavior"` | |||
| 267 | + | // EnableSmartDetection uses WebKitWindowProperties to detect popup vs tab intents | |||
| 268 | + | EnableSmartDetection bool `mapstructure:"enable_smart_detection" yaml:"enable_smart_detection" json:"enable_smart_detection"` | |||
| 269 | + | // OAuthAutoClose enables auto-closing OAuth popups after successful auth redirects | |||
| 270 | + | OAuthAutoClose bool `mapstructure:"oauth_auto_close" yaml:"oauth_auto_close" json:"oauth_auto_close"` | |||
| 261 | } | 271 | } | ||
| 262 | 272 | ||||
| 263 | // WorkspaceStylingConfig defines visual styling for workspace panes. | 273 | // WorkspaceStylingConfig defines visual styling for workspace panes. | ||
| @@ -678,6 +688,10 @@ func (m *Manager) setDefaults() { | |||||
| 678 | m.viper.SetDefault("workspace.popups.placement", defaults.Workspace.Popups.Placement) | 688 | m.viper.SetDefault("workspace.popups.placement", defaults.Workspace.Popups.Placement) | ||
| 679 | m.viper.SetDefault("workspace.popups.open_in_new_pane", defaults.Workspace.Popups.OpenInNewPane) | 689 | m.viper.SetDefault("workspace.popups.open_in_new_pane", defaults.Workspace.Popups.OpenInNewPane) | ||
| 680 | m.viper.SetDefault("workspace.popups.follow_pane_context", defaults.Workspace.Popups.FollowPaneContext) | 690 | m.viper.SetDefault("workspace.popups.follow_pane_context", defaults.Workspace.Popups.FollowPaneContext) | ||
| 691 | + | // New popup behaviour defaults | |||
| 692 | + | m.viper.SetDefault("workspace.popups.blank_target_behavior", defaults.Workspace.Popups.BlankTargetBehavior) | |||
| 693 | + | m.viper.SetDefault("workspace.popups.enable_smart_detection", defaults.Workspace.Popups.EnableSmartDetection) | |||
| 694 | + | m.viper.SetDefault("workspace.popups.oauth_auto_close", defaults.Workspace.Popups.OAuthAutoClose) | |||
| 681 | m.viper.SetDefault("workspace.styling.border_width", defaults.Workspace.Styling.BorderWidth) | 695 | m.viper.SetDefault("workspace.styling.border_width", defaults.Workspace.Styling.BorderWidth) | ||
| 682 | m.viper.SetDefault("workspace.styling.border_color", defaults.Workspace.Styling.BorderColor) | 696 | m.viper.SetDefault("workspace.styling.border_color", defaults.Workspace.Styling.BorderColor) | ||
| 683 | m.viper.SetDefault("workspace.styling.transition_duration", defaults.Workspace.Styling.TransitionDuration) | 697 | m.viper.SetDefault("workspace.styling.transition_duration", defaults.Workspace.Styling.TransitionDuration) | ||
M internal/config/defaults.go +7 -3
| @@ -101,6 +101,7 @@ func DefaultConfig() *Config { | |||
| 101 | 101 | CaptureStdout: false, | |
| 102 | 102 | CaptureStderr: false, | |
| 103 | 103 | CaptureCOutput: false, // Disabled by default to avoid performance impact | |
| 104 | + | CaptureConsole: false, // Disabled by default | |
| 104 | 105 | DebugFile: "debug.log", | |
| 105 | 106 | VerboseWebKit: false, | |
| 106 | 107 | }, | |
| @@ -183,9 +184,12 @@ func DefaultConfig() *Config { | |||
| 183 | 184 | PreviousTab: "cmdorctrl+shift+tab", | |
| 184 | 185 | }, | |
| 185 | 186 | Popups: PopupBehaviorConfig{ | |
| 186 | - | Placement: defaultPopupPlacement, | |
| 187 | - | OpenInNewPane: true, | |
| 188 | - | FollowPaneContext: true, | |
| 187 | + | Placement: defaultPopupPlacement, | |
| 188 | + | OpenInNewPane: true, | |
| 189 | + | FollowPaneContext: true, | |
| 190 | + | BlankTargetBehavior: "pane", // Default to pane, future: "tab" | |
| 191 | + | EnableSmartDetection: true, // Use WindowProperties to detect popup vs tab | |
| 192 | + | OAuthAutoClose: true, // Auto-close OAuth popups on success | |
| 189 | 193 | }, | |
| 190 | 194 | Styling: WorkspaceStylingConfig{ | |
| 191 | 195 | BorderWidth: defaultBorderWidth, | |
| @@ -101,6 +101,7 @@ func DefaultConfig() *Config { | |||||
| 101 | CaptureStdout: false, | 101 | CaptureStdout: false, | ||
| 102 | CaptureStderr: false, | 102 | CaptureStderr: false, | ||
| 103 | CaptureCOutput: false, // Disabled by default to avoid performance impact | 103 | CaptureCOutput: false, // Disabled by default to avoid performance impact | ||
| 104 | + | CaptureConsole: false, // Disabled by default | |||
| 104 | DebugFile: "debug.log", | 105 | DebugFile: "debug.log", | ||
| 105 | VerboseWebKit: false, | 106 | VerboseWebKit: false, | ||
| 106 | }, | 107 | }, | ||
| @@ -183,9 +184,12 @@ func DefaultConfig() *Config { | |||||
| 183 | PreviousTab: "cmdorctrl+shift+tab", | 184 | PreviousTab: "cmdorctrl+shift+tab", | ||
| 184 | }, | 185 | }, | ||
| 185 | Popups: PopupBehaviorConfig{ | 186 | Popups: PopupBehaviorConfig{ | ||
| 186 | - | Placement: defaultPopupPlacement, | 187 | + | Placement: defaultPopupPlacement, |
| 187 | - | OpenInNewPane: true, | 188 | + | OpenInNewPane: true, |
| 188 | - | FollowPaneContext: true, | 189 | + | FollowPaneContext: true, |
| 190 | + | BlankTargetBehavior: "pane", // Default to pane, future: "tab" | |||
| 191 | + | EnableSmartDetection: true, // Use WindowProperties to detect popup vs tab | |||
| 192 | + | OAuthAutoClose: true, // Auto-close OAuth popups on success | |||
| 189 | }, | 193 | }, | ||
| 190 | Styling: WorkspaceStylingConfig{ | 194 | Styling: WorkspaceStylingConfig{ | ||
| 191 | BorderWidth: defaultBorderWidth, | 195 | BorderWidth: defaultBorderWidth, | ||
M internal/logging/logger.go +8 -1
| @@ -218,10 +218,17 @@ func Fatal(message string) { | |||
| 218 | 218 | ||
| 219 | 219 | func CaptureWebKitLog(message string) { | |
| 220 | 220 | if l := GetLogger(); l != nil { | |
| 221 | - | l.WriteTagged("WEBKIT", message) | |
| 221 | + | l.WriteTagged("CONSOLE", message) | |
| 222 | 222 | } | |
| 223 | 223 | } | |
| 224 | 224 | ||
| 225 | + | // StartConsoleCapture starts capturing stdout for console messages | |
| 226 | + | // StartConsoleCapture starts capturing stdout for console messages and writing to console.log | |
| 227 | + | // StartConsoleCapture - placeholder for console message capture | |
| 228 | + | func StartConsoleCapture() { | |
| 229 | + | // Placeholder - console message capture will be implemented differently | |
| 230 | + | } | |
| 231 | + | ||
| 225 | 232 | // LogLevelInfo returns the INFO log level constant for external packages | |
| 226 | 233 | func LogLevelInfo() LogLevel { | |
| 227 | 234 | return INFO | |
| @@ -218,10 +218,17 @@ func Fatal(message string) { | |||||
| 218 | 218 | ||||
| 219 | func CaptureWebKitLog(message string) { | 219 | func CaptureWebKitLog(message string) { | ||
| 220 | if l := GetLogger(); l != nil { | 220 | if l := GetLogger(); l != nil { | ||
| 221 | - | l.WriteTagged("WEBKIT", message) | 221 | + | l.WriteTagged("CONSOLE", message) |
| 222 | } | 222 | } | ||
| 223 | } | 223 | } | ||
| 224 | 224 | ||||
| 225 | + | // StartConsoleCapture starts capturing stdout for console messages | |||
| 226 | + | // StartConsoleCapture starts capturing stdout for console messages and writing to console.log | |||
| 227 | + | // StartConsoleCapture - placeholder for console message capture | |||
| 228 | + | func StartConsoleCapture() { | |||
| 229 | + | // Placeholder - console message capture will be implemented differently | |||
| 230 | + | } | |||
| 231 | + | ||||
| 225 | // LogLevelInfo returns the INFO log level constant for external packages | 232 | // LogLevelInfo returns the INFO log level constant for external packages | ||
| 226 | func LogLevelInfo() LogLevel { | 233 | func LogLevelInfo() LogLevel { | ||
| 227 | return INFO | 234 | return INFO | ||
M pkg/webkit/keyboard_cgo.go +2 -41
| @@ -3,9 +3,10 @@ | |||
| 3 | 3 | package webkit | |
| 4 | 4 | ||
| 5 | 5 | /* | |
| 6 | - | #cgo pkg-config: gtk4 | |
| 6 | + | #cgo pkg-config: gtk4 webkitgtk-6.0 | |
| 7 | 7 | #include <gtk/gtk.h> | |
| 8 | 8 | #include <gdk/gdk.h> | |
| 9 | + | #include <webkit/webkit.h> | |
| 9 | 10 | ||
| 10 | 11 | // Declarations for Go callbacks | |
| 11 | 12 | extern gboolean goOnKeyPress(unsigned long id, unsigned int keyval, GdkModifierType state); | |
| @@ -84,7 +85,6 @@ import ( | |||
| 84 | 85 | "sync" | |
| 85 | 86 | "sync/atomic" | |
| 86 | 87 | "time" | |
| 87 | - | "unsafe" | |
| 88 | 88 | ) | |
| 89 | 89 | ||
| 90 | 90 | // Registry of accelerators per view id. | |
| @@ -489,45 +489,6 @@ func goHandleNewWindowPolicy(id C.ulong, curi *C.char, navType C.int) C.gboolean | |||
| 489 | 489 | } | |
| 490 | 490 | } | |
| 491 | 491 | ||
| 492 | - | //export goHandleCreateWebView | |
| 493 | - | func goHandleCreateWebView(id C.ulong, curi *C.char) *C.GtkWidget { | |
| 494 | - | uid := uintptr(id) | |
| 495 | - | regMu.RLock() | |
| 496 | - | vw := viewByID[uid] | |
| 497 | - | regMu.RUnlock() | |
| 498 | - | ||
| 499 | - | if vw == nil { | |
| 500 | - | log.Printf("[webkit] Create signal: no WebView found for ID %d", uid) | |
| 501 | - | return nil // Fallback to native popup | |
| 502 | - | } | |
| 503 | - | ||
| 504 | - | uri := "" | |
| 505 | - | if curi != nil { | |
| 506 | - | uri = C.GoString(curi) | |
| 507 | - | } | |
| 508 | - | ||
| 509 | - | log.Printf("[webkit] Create signal: creating new pane for URI=%s", uri) | |
| 510 | - | ||
| 511 | - | // Check if we have a popup handler | |
| 512 | - | if vw.popupHandler == nil { | |
| 513 | - | log.Printf("[webkit] Create signal: no popup handler registered") | |
| 514 | - | return nil // Fallback to native popup | |
| 515 | - | } | |
| 516 | - | ||
| 517 | - | // Always use the popup handler to create workspace pane (tiling WM design) | |
| 518 | - | newView := vw.popupHandler(uri) | |
| 519 | - | if newView != nil { | |
| 520 | - | widget := newView.Widget() | |
| 521 | - | if widget != 0 { | |
| 522 | - | log.Printf("[webkit] Create signal: created new workspace pane for popup") | |
| 523 | - | return (*C.GtkWidget)(unsafe.Pointer(widget)) | |
| 524 | - | } | |
| 525 | - | } | |
| 526 | - | ||
| 527 | - | log.Printf("[webkit] Create signal: popup handler failed to create pane, falling back to native") | |
| 528 | - | return nil // Fallback to native popup if workspace creation fails | |
| 529 | - | } | |
| 530 | - | ||
| 531 | 492 | //export goHandleLoadChanged | |
| 532 | 493 | func goHandleLoadChanged(id C.ulong, curi *C.char, loadEvent C.int) { | |
| 533 | 494 | uid := uintptr(id) | |
| @@ -3,9 +3,10 @@ | |||||
| 3 | package webkit | 3 | package webkit | ||
| 4 | 4 | ||||
| 5 | /* | 5 | /* | ||
| 6 | - | #cgo pkg-config: gtk4 | 6 | + | #cgo pkg-config: gtk4 webkitgtk-6.0 |
| 7 | #include <gtk/gtk.h> | 7 | #include <gtk/gtk.h> | ||
| 8 | #include <gdk/gdk.h> | 8 | #include <gdk/gdk.h> | ||
| 9 | + | #include <webkit/webkit.h> | |||
| 9 | 10 | ||||
| 10 | // Declarations for Go callbacks | 11 | // Declarations for Go callbacks | ||
| 11 | extern gboolean goOnKeyPress(unsigned long id, unsigned int keyval, GdkModifierType state); | 12 | extern gboolean goOnKeyPress(unsigned long id, unsigned int keyval, GdkModifierType state); | ||
| @@ -84,7 +85,6 @@ import ( | |||||
| 84 | "sync" | 85 | "sync" | ||
| 85 | "sync/atomic" | 86 | "sync/atomic" | ||
| 86 | "time" | 87 | "time" | ||
| 87 | - | "unsafe" | |||
| 88 | ) | 88 | ) | ||
| 89 | 89 | ||||
| 90 | // Registry of accelerators per view id. | 90 | // Registry of accelerators per view id. | ||
| @@ -489,45 +489,6 @@ func goHandleNewWindowPolicy(id C.ulong, curi *C.char, navType C.int) C.gboolean | |||||
| 489 | } | 489 | } | ||
| 490 | } | 490 | } | ||
| 491 | 491 | ||||
| 492 | - | //export goHandleCreateWebView | |||
| 493 | - | func goHandleCreateWebView(id C.ulong, curi *C.char) *C.GtkWidget { | |||
| 494 | - | uid := uintptr(id) | |||
| 495 | - | regMu.RLock() | |||
| 496 | - | vw := viewByID[uid] | |||
| 497 | - | regMu.RUnlock() | |||
| 498 | - | ||||
| 499 | - | if vw == nil { | |||
| 500 | - | log.Printf("[webkit] Create signal: no WebView found for ID %d", uid) | |||
| 501 | - | return nil // Fallback to native popup | |||
| 502 | - | } | |||
| 503 | - | ||||
| 504 | - | uri := "" | |||
| 505 | - | if curi != nil { | |||
| 506 | - | uri = C.GoString(curi) | |||
| 507 | - | } | |||
| 508 | - | ||||
| 509 | - | log.Printf("[webkit] Create signal: creating new pane for URI=%s", uri) | |||
| 510 | - | ||||
| 511 | - | // Check if we have a popup handler | |||
| 512 | - | if vw.popupHandler == nil { | |||
| 513 | - | log.Printf("[webkit] Create signal: no popup handler registered") | |||
| 514 | - | return nil // Fallback to native popup | |||
| 515 | - | } | |||
| 516 | - | ||||
| 517 | - | // Always use the popup handler to create workspace pane (tiling WM design) | |||
| 518 | - | newView := vw.popupHandler(uri) | |||
| 519 | - | if newView != nil { | |||
| 520 | - | widget := newView.Widget() | |||
| 521 | - | if widget != 0 { | |||
| 522 | - | log.Printf("[webkit] Create signal: created new workspace pane for popup") | |||
| 523 | - | return (*C.GtkWidget)(unsafe.Pointer(widget)) | |||
| 524 | - | } | |||
| 525 | - | } | |||
| 526 | - | ||||
| 527 | - | log.Printf("[webkit] Create signal: popup handler failed to create pane, falling back to native") | |||
| 528 | - | return nil // Fallback to native popup if workspace creation fails | |||
| 529 | - | } | |||
| 530 | - | ||||
| 531 | //export goHandleLoadChanged | 492 | //export goHandleLoadChanged | ||
| 532 | func goHandleLoadChanged(id C.ulong, curi *C.char, loadEvent C.int) { | 493 | func goHandleLoadChanged(id C.ulong, curi *C.char, loadEvent C.int) { | ||
| 533 | uid := uintptr(id) | 494 | uid := uintptr(id) | ||
M pkg/webkit/webview.go +23 -0
| @@ -26,6 +26,11 @@ type WebView struct { | |||
| 26 | 26 | useDomZoom bool | |
| 27 | 27 | domZoomSeed float64 | |
| 28 | 28 | container uintptr | |
| 29 | + | ||
| 30 | + | // Window type fields (no-op in stub) | |
| 31 | + | windowType WindowType | |
| 32 | + | windowFeatures *WindowFeatures | |
| 33 | + | windowTypeCallback func(WindowType, *WindowFeatures) | |
| 29 | 34 | } | |
| 30 | 35 | ||
| 31 | 36 | // NewWebView constructs a new WebView instance. | |
| @@ -211,6 +216,19 @@ func (w *WebView) SeedDomZoom(level float64) { | |||
| 211 | 216 | w.domZoomSeed = level | |
| 212 | 217 | } | |
| 213 | 218 | ||
| 219 | + | // CreateRelatedView returns a new WebView (stub creates independent) | |
| 220 | + | func (w *WebView) CreateRelatedView() *WebView { | |
| 221 | + | nw, _ := NewWebView(w.config) | |
| 222 | + | return nw | |
| 223 | + | } | |
| 224 | + | ||
| 225 | + | // OnWindowTypeDetected registers a callback (stored but never invoked in stub) | |
| 226 | + | func (w *WebView) OnWindowTypeDetected(callback func(WindowType, *WindowFeatures)) { | |
| 227 | + | if w != nil { | |
| 228 | + | w.windowTypeCallback = callback | |
| 229 | + | } | |
| 230 | + | } | |
| 231 | + | ||
| 214 | 232 | // InitializeContentBlocking initializes WebKit content blocking with filter manager (stub) | |
| 215 | 233 | func (w *WebView) InitializeContentBlocking(filterManager interface{}) error { | |
| 216 | 234 | return ErrNotImplemented | |
| @@ -225,3 +243,8 @@ func (w *WebView) OnNavigate(url string, filterManager interface{}) { | |||
| 225 | 243 | func (w *WebView) UpdateContentFilters(filterManager interface{}) error { | |
| 226 | 244 | return ErrNotImplemented | |
| 227 | 245 | } | |
| 246 | + | ||
| 247 | + | // SetWindowFeatures sets the window features for this WebView (stub) | |
| 248 | + | func (w *WebView) SetWindowFeatures(features *WindowFeatures) { | |
| 249 | + | w.windowFeatures = features | |
| 250 | + | } | |
| @@ -26,6 +26,11 @@ type WebView struct { | |||||
| 26 | useDomZoom bool | 26 | useDomZoom bool | ||
| 27 | domZoomSeed float64 | 27 | domZoomSeed float64 | ||
| 28 | container uintptr | 28 | container uintptr | ||
| 29 | + | ||||
| 30 | + | // Window type fields (no-op in stub) | |||
| 31 | + | windowType WindowType | |||
| 32 | + | windowFeatures *WindowFeatures | |||
| 33 | + | windowTypeCallback func(WindowType, *WindowFeatures) | |||
| 29 | } | 34 | } | ||
| 30 | 35 | ||||
| 31 | // NewWebView constructs a new WebView instance. | 36 | // NewWebView constructs a new WebView instance. | ||
| @@ -211,6 +216,19 @@ func (w *WebView) SeedDomZoom(level float64) { | |||||
| 211 | w.domZoomSeed = level | 216 | w.domZoomSeed = level | ||
| 212 | } | 217 | } | ||
| 213 | 218 | ||||
| 219 | + | // CreateRelatedView returns a new WebView (stub creates independent) | |||
| 220 | + | func (w *WebView) CreateRelatedView() *WebView { | |||
| 221 | + | nw, _ := NewWebView(w.config) | |||
| 222 | + | return nw | |||
| 223 | + | } | |||
| 224 | + | ||||
| 225 | + | // OnWindowTypeDetected registers a callback (stored but never invoked in stub) | |||
| 226 | + | func (w *WebView) OnWindowTypeDetected(callback func(WindowType, *WindowFeatures)) { | |||
| 227 | + | if w != nil { | |||
| 228 | + | w.windowTypeCallback = callback | |||
| 229 | + | } | |||
| 230 | + | } | |||
| 231 | + | ||||
| 214 | // InitializeContentBlocking initializes WebKit content blocking with filter manager (stub) | 232 | // InitializeContentBlocking initializes WebKit content blocking with filter manager (stub) | ||
| 215 | func (w *WebView) InitializeContentBlocking(filterManager interface{}) error { | 233 | func (w *WebView) InitializeContentBlocking(filterManager interface{}) error { | ||
| 216 | return ErrNotImplemented | 234 | return ErrNotImplemented | ||
| @@ -225,3 +243,8 @@ func (w *WebView) OnNavigate(url string, filterManager interface{}) { | |||||
| 225 | func (w *WebView) UpdateContentFilters(filterManager interface{}) error { | 243 | func (w *WebView) UpdateContentFilters(filterManager interface{}) error { | ||
| 226 | return ErrNotImplemented | 244 | return ErrNotImplemented | ||
| 227 | } | 245 | } | ||
| 246 | + | ||||
| 247 | + | // SetWindowFeatures sets the window features for this WebView (stub) | |||
| 248 | + | func (w *WebView) SetWindowFeatures(features *WindowFeatures) { | |||
| 249 | + | w.windowFeatures = features | |||
| 250 | + | } | |||
M pkg/webkit/webview_cgo.go +211 -176
| @@ -24,7 +24,6 @@ extern void goQuitMainLoop(); | |||
| 24 | 24 | extern void goInvokeHandle(uintptr_t handle); | |
| 25 | 25 | extern gboolean goHandleNewWindowPolicy(unsigned long id, char* uri, int nav_type); | |
| 26 | 26 | extern void goHandlePopupGeometry(unsigned long id, int x, int y, int width, int height); | |
| 27 | - | extern GtkWidget* goHandleCreateWebView(unsigned long id, char* uri); | |
| 28 | 27 | extern void goHandleLoadChanged(unsigned long id, char* uri, int load_event); | |
| 29 | 28 | extern void goHandleWebViewClose(unsigned long id); | |
| 30 | 29 | static gboolean invoke_handle_cb(gpointer data) { | |
| @@ -49,27 +48,17 @@ static void maybe_set_cookie_policy(WebKitCookieManager* cm, int policy); | |||
| 49 | 48 | ||
| 50 | 49 | // TLS error handling forward declarations | |
| 51 | 50 | extern gboolean goHandleTLSError(char* failing_uri, char* host, int error_flags, char* cert_info); | |
| 51 | + | ||
| 52 | + | // Console message handling forward declarations | |
| 53 | + | extern void goHandleConsoleMessage(char* message); | |
| 54 | + | ||
| 52 | 55 | static gboolean on_load_failed_with_tls_errors(WebKitWebView *web_view, | |
| 53 | 56 | const char *failing_uri, | |
| 54 | 57 | GTlsCertificate *certificate, | |
| 55 | 58 | GTlsCertificateFlags errors, | |
| 56 | 59 | gpointer user_data); | |
| 57 | 60 | ||
| 58 | - | // Handle ready-to-show signal for popup windows to properly initialize window properties | |
| 59 | - | static void on_popup_ready_to_show(WebKitWebView* web_view, gpointer user_data) { | |
| 60 | - | // Get window properties to ensure they're properly initialized | |
| 61 | - | WebKitWindowProperties* props = webkit_web_view_get_window_properties(web_view); | |
| 62 | - | if (props) { | |
| 63 | - | // Access properties to ensure WindowFeatures are initialized | |
| 64 | - | GdkRectangle geometry; | |
| 65 | - | webkit_window_properties_get_geometry(props, &geometry); | |
| 66 | 61 | ||
| 67 | - | // Pass geometry to Go for workspace positioning | |
| 68 | - | unsigned long view_id = (unsigned long)user_data; | |
| 69 | - | goHandlePopupGeometry(view_id, geometry.x, geometry.y, | |
| 70 | - | geometry.width, geometry.height); | |
| 71 | - | } | |
| 72 | - | } | |
| 73 | 62 | ||
| 74 | 63 | // Handle load_changed signal for OAuth completion detection | |
| 75 | 64 | static void on_load_changed(WebKitWebView *web_view, WebKitLoadEvent load_event, gpointer user_data) { | |
| @@ -90,38 +79,6 @@ static void on_webview_close(WebKitWebView* web_view, gpointer user_data) { | |||
| 90 | 79 | goHandleWebViewClose(view_id); | |
| 91 | 80 | } | |
| 92 | 81 | ||
| 93 | - | // Handle create signal for popup windows - essential for window.open() support | |
| 94 | - | static GtkWidget* on_create_web_view(WebKitWebView *web_view, | |
| 95 | - | WebKitNavigationAction *navigation_action, | |
| 96 | - | gpointer user_data) { | |
| 97 | - | WebKitURIRequest *request = webkit_navigation_action_get_request(navigation_action); | |
| 98 | - | const char* uri = webkit_uri_request_get_uri(request); | |
| 99 | - | ||
| 100 | - | printf("[webkit-create] Create signal for URI: %s\n", uri ? uri : "NULL"); | |
| 101 | - | ||
| 102 | - | unsigned long view_id = (unsigned long)user_data; | |
| 103 | - | GtkWidget* new_webview = goHandleCreateWebView(view_id, (char*)uri); | |
| 104 | - | ||
| 105 | - | if (new_webview) { | |
| 106 | - | printf("[webkit-create] Go handler provided WebView - using workspace pane\n"); | |
| 107 | - | ||
| 108 | - | // Connect load_changed signal to the new popup WebView for OAuth detection | |
| 109 | - | WebKitWebView* popup_webview = WEBKIT_WEB_VIEW(new_webview); | |
| 110 | - | g_signal_connect_data(G_OBJECT(popup_webview), "load-changed", | |
| 111 | - | G_CALLBACK(on_load_changed), user_data, NULL, 0); | |
| 112 | - | printf("[webkit-create] Connected load-changed signal to popup for OAuth detection\n"); | |
| 113 | - | ||
| 114 | - | // Connect close signal to handle window.close() from OAuth providers | |
| 115 | - | g_signal_connect_data(G_OBJECT(popup_webview), "close", | |
| 116 | - | G_CALLBACK(on_webview_close), user_data, NULL, 0); | |
| 117 | - | printf("[webkit-create] Connected close signal to popup for OAuth auto-close\n"); | |
| 118 | - | ||
| 119 | - | return new_webview; | |
| 120 | - | } else { | |
| 121 | - | printf("[webkit-create] Go handler declined - allowing native popup window\n"); | |
| 122 | - | return NULL; // Let WebKit create native popup window | |
| 123 | - | } | |
| 124 | - | } | |
| 125 | 82 | ||
| 126 | 83 | // Handle decide-policy signal for new window requests - more robust than create signal | |
| 127 | 84 | static gboolean on_decide_policy(WebKitWebView *web_view, | |
| @@ -282,9 +239,9 @@ static GtkWidget* new_webview_related(WebKitWebView* related_view, WebKitUserCon | |||
| 282 | 239 | ||
| 283 | 240 | static void connect_policy_handler(WebKitWebView* web_view, unsigned long id) { | |
| 284 | 241 | if (!web_view) return; | |
| 285 | - | g_signal_connect_data(G_OBJECT(web_view), "decide-policy", G_CALLBACK(on_decide_policy), (gpointer)id, NULL, 0); | |
| 286 | - | // Connect create signal for popup window support | |
| 287 | - | g_signal_connect_data(G_OBJECT(web_view), "create", G_CALLBACK(on_create_web_view), (gpointer)id, NULL, 0); | |
| 242 | + | // Create signal is now handled in window_handler_cgo.go | |
| 243 | + | // Use ConnectCreateSignal from Go to connect the signal | |
| 244 | + | printf("[webkit-debug] Policy handler connected (create signal handled separately)\n"); | |
| 288 | 245 | } | |
| 289 | 246 | ||
| 290 | 247 | // Connect close signal to a WebView for popup auto-close functionality | |
| @@ -780,6 +737,7 @@ import ( | |||
| 780 | 737 | "context" | |
| 781 | 738 | "crypto/sha256" | |
| 782 | 739 | "database/sql" | |
| 740 | + | "encoding/json" | |
| 783 | 741 | "errors" | |
| 784 | 742 | "fmt" | |
| 785 | 743 | "log" | |
| @@ -807,125 +765,7 @@ type FilterManager interface { | |||
| 807 | 765 | GetCosmeticScriptForDomain(domain string) string | |
| 808 | 766 | } | |
| 809 | 767 | ||
| 810 | - | const domBridgeTemplate = `(() => { try { | |
| 811 | - | if (window.__dumber_page_bridge_installed) return; | |
| 812 | - | window.__dumber_page_bridge_installed = true; | |
| 813 | - | ||
| 814 | - | const __dumberInitialZoom = __DOM_ZOOM_DEFAULT__; | |
| 815 | - | window.__dumber_dom_zoom_seed = __dumberInitialZoom; | |
| 816 | - | ||
| 817 | - | // Theme setter function for GTK theme integration | |
| 818 | - | window.__dumber_setTheme = (theme) => { | |
| 819 | - | window.__dumber_initial_theme = theme; | |
| 820 | - | console.log('[dumber] Setting theme to:', theme); | |
| 821 | - | if (theme === 'dark') { | |
| 822 | - | document.documentElement.classList.add('dark'); | |
| 823 | - | } else { | |
| 824 | - | document.documentElement.classList.remove('dark'); | |
| 825 | - | } | |
| 826 | - | }; | |
| 827 | - | ||
| 828 | - | // Ensure unified API object | |
| 829 | - | window.__dumber = window.__dumber || {}; | |
| 830 | - | // Toast namespace | |
| 831 | - | window.__dumber.toast = window.__dumber.toast || { | |
| 832 | - | show: function(message, duration, type) { | |
| 833 | - | try { | |
| 834 | - | document.dispatchEvent(new CustomEvent('dumber:toast:show', { detail: { message, duration, type } })); | |
| 835 | - | // Legacy compatibility | |
| 836 | - | document.dispatchEvent(new CustomEvent('dumber:showToast', { detail: { message, duration, type } })); | |
| 837 | - | } catch(e) { /* ignore */ } | |
| 838 | - | }, | |
| 839 | - | zoom: function(level) { | |
| 840 | - | try { | |
| 841 | - | document.dispatchEvent(new CustomEvent('dumber:toast:zoom', { detail: { level } })); | |
| 842 | - | // Legacy compatibility | |
| 843 | - | document.dispatchEvent(new CustomEvent('dumber:showZoomToast', { detail: { level } })); | |
| 844 | - | } catch(e) { /* ignore */ } | |
| 845 | - | } | |
| 846 | - | }; | |
| 847 | - | // Back-compat global helpers | |
| 848 | - | window.__dumber_showToast = function(message, duration, type) { window.__dumber.toast.show(message, duration, type); }; | |
| 849 | - | window.__dumber_showZoomToast = function(level) { window.__dumber.toast.zoom(level); }; | |
| 850 | - | ||
| 851 | - | // DOM zoom helpers (optional native override) | |
| 852 | - | if (typeof window.__dumber_dom_zoom_level !== 'number') { | |
| 853 | - | window.__dumber_dom_zoom_level = __dumberInitialZoom; | |
| 854 | - | } | |
| 855 | - | const __dumberApplyZoomStyles = function(node, level) { | |
| 856 | - | if (!node) return; | |
| 857 | - | if (Math.abs(level - 1.0) < 1e-6) { | |
| 858 | - | node.style.removeProperty('zoom'); | |
| 859 | - | node.style.removeProperty('transform'); | |
| 860 | - | node.style.removeProperty('transform-origin'); | |
| 861 | - | node.style.removeProperty('width'); | |
| 862 | - | node.style.removeProperty('min-width'); | |
| 863 | - | node.style.removeProperty('height'); | |
| 864 | - | node.style.removeProperty('min-height'); | |
| 865 | - | return; | |
| 866 | - | } | |
| 867 | - | const scale = level; | |
| 868 | - | const inversePercent = 100 / scale; | |
| 869 | - | const widthValue = inversePercent + '%'; | |
| 870 | - | node.style.removeProperty('zoom'); | |
| 871 | - | node.style.transform = 'scale(' + scale + ')'; | |
| 872 | - | node.style.transformOrigin = '0 0'; | |
| 873 | - | node.style.width = widthValue; | |
| 874 | - | node.style.minWidth = widthValue; | |
| 875 | - | node.style.minHeight = '100%'; | |
| 876 | - | }; | |
| 877 | - | window.__dumber_applyDomZoom = function(level) { | |
| 878 | - | try { | |
| 879 | - | window.__dumber_dom_zoom_level = level; | |
| 880 | - | window.__dumber_dom_zoom_seed = level; | |
| 881 | - | __dumberApplyZoomStyles(document.documentElement, level); | |
| 882 | - | if (document.body) { | |
| 883 | - | __dumberApplyZoomStyles(document.body, level); | |
| 884 | - | } | |
| 885 | - | } catch (e) { | |
| 886 | - | console.error('[dumber] DOM zoom error', e); | |
| 887 | - | } | |
| 888 | - | }; | |
| 889 | - | // Apply immediately so first paint uses the desired zoom, then reapply when body exists. | |
| 890 | - | window.__dumber_applyDomZoom(window.__dumber_dom_zoom_level); | |
| 891 | - | if (!document.body) { | |
| 892 | - | document.addEventListener('DOMContentLoaded', function() { | |
| 893 | - | if (typeof window.__dumber_dom_zoom_level === 'number') { | |
| 894 | - | window.__dumber_applyDomZoom(window.__dumber_dom_zoom_level); | |
| 895 | - | } | |
| 896 | - | }, { once: true }); | |
| 897 | - | } | |
| 898 | - | ||
| 899 | - | // Omnibox suggestions bridge (with queue before ready) | |
| 900 | - | let __omniboxQueue = []; | |
| 901 | - | let __omniboxReady = false; | |
| 902 | - | function __omniboxDispatch(suggestions) { | |
| 903 | - | try { | |
| 904 | - | document.dispatchEvent(new CustomEvent('dumber:omnibox:suggestions', { detail: { suggestions } })); | |
| 905 | - | // Legacy compatibility event name | |
| 906 | - | document.dispatchEvent(new CustomEvent('dumber:omnibox-suggestions', { detail: { suggestions } })); | |
| 907 | - | } catch (e) { /* ignore */ } | |
| 908 | - | } | |
| 909 | - | window.__dumber_omnibox_suggestions = function(suggestions) { | |
| 910 | - | if (__omniboxReady) { | |
| 911 | - | __omniboxDispatch(suggestions); | |
| 912 | - | } else { | |
| 913 | - | try { __omniboxQueue.push(suggestions); } catch (_) { /* ignore */ } | |
| 914 | - | } | |
| 915 | - | }; | |
| 916 | - | document.addEventListener('dumber:omnibox-ready', function() { | |
| 917 | - | __omniboxReady = true; | |
| 918 | - | if (__omniboxQueue && __omniboxQueue.length) { | |
| 919 | - | const items = __omniboxQueue.slice(); | |
| 920 | - | __omniboxQueue.length = 0; | |
| 921 | - | for (const s of items) __omniboxDispatch(s); | |
| 922 | - | } | |
| 923 | - | }); | |
| 924 | - | // Unified omnibox API for page-world callers | |
| 925 | - | window.__dumber.omnibox = window.__dumber.omnibox || { | |
| 926 | - | suggestions: function(suggestions) { window.__dumber_omnibox_suggestions(suggestions); } | |
| 927 | - | }; | |
| 928 | - | } catch (e) { console.warn('[dumber] unified bridge init failed', e); } })();` | |
| 768 | + | // domBridgeTemplate removed - now loading compiled main-world.min.js | |
| 929 | 769 | ||
| 930 | 770 | var registerSchemeOnce sync.Once | |
| 931 | 771 | ||
| @@ -1071,9 +911,9 @@ type WebView struct { | |||
| 1071 | 911 | titleHandler func(title string) | |
| 1072 | 912 | uriHandler func(uri string) | |
| 1073 | 913 | zoomHandler func(level float64) | |
| 1074 | - | popupHandler func(string) *WebView | |
| 1075 | - | closeHandler func() | |
| 1076 | - | memStats *memoryStats | |
| 914 | + | popupHandler func(string) *WebView | |
| 915 | + | closeHandler func() | |
| 916 | + | memStats *memoryStats | |
| 1077 | 917 | gcTicker *time.Ticker | |
| 1078 | 918 | gcDone chan struct{} | |
| 1079 | 919 | tlsExceptions map[string]bool // host -> whether user allowed certificate exception | |
| @@ -1081,6 +921,13 @@ type WebView struct { | |||
| 1081 | 921 | domZoomSeed float64 | |
| 1082 | 922 | domBridgeScript *C.WebKitUserScript | |
| 1083 | 923 | wasReparented bool // Track if this WebView has been reparented | |
| 924 | + | ||
| 925 | + | // Window type detection and relationships | |
| 926 | + | windowType WindowType | |
| 927 | + | windowFeatures *WindowFeatures | |
| 928 | + | windowTypeCallback func(WindowType, *WindowFeatures) | |
| 929 | + | parentWebView *WebView | |
| 930 | + | isRelated bool | |
| 1084 | 931 | } | |
| 1085 | 932 | ||
| 1086 | 933 | // NewWebView constructs a new WebView instance with native WebKit2GTK widgets. | |
| @@ -1237,6 +1084,7 @@ func NewWebView(cfg *Config) (*WebView, error) { | |||
| 1237 | 1084 | v.id = nextViewID() | |
| 1238 | 1085 | registerView(v.id, v) | |
| 1239 | 1086 | C.connect_policy_handler(v.native.wv, C.ulong(v.id)) | |
| 1087 | + | ConnectCreateSignal(v) // Connect create signal for window.open support | |
| 1240 | 1088 | ||
| 1241 | 1089 | // Register with global memory manager if monitoring is enabled | |
| 1242 | 1090 | if cfg.Memory.EnableMemoryMonitoring { | |
| @@ -1446,9 +1294,124 @@ func NewWebView(cfg *Config) (*WebView, error) { | |||
| 1446 | 1294 | } | |
| 1447 | 1295 | } | |
| 1448 | 1296 | ||
| 1297 | + | // Setup console message logging | |
| 1298 | + | v.setupConsoleLogging() | |
| 1299 | + | ||
| 1449 | 1300 | return v, nil | |
| 1450 | 1301 | } | |
| 1451 | 1302 | ||
| 1303 | + | // setupConsoleLogging enables console message capture for the WebView | |
| 1304 | + | func (v *WebView) setupConsoleLogging() { | |
| 1305 | + | if v.native == nil || v.native.wv == nil { | |
| 1306 | + | return | |
| 1307 | + | } | |
| 1308 | + | ||
| 1309 | + | // Check if console capture is enabled in config | |
| 1310 | + | cfg := config.Get() | |
| 1311 | + | if !cfg.Logging.CaptureConsole { | |
| 1312 | + | // Disable console messages to stdout (always, to keep backend clean) | |
| 1313 | + | if settings := C.webkit_web_view_get_settings(v.native.wv); settings != nil { | |
| 1314 | + | C.webkit_settings_set_enable_write_console_messages_to_stdout(settings, C.gboolean(0)) | |
| 1315 | + | } | |
| 1316 | + | return | |
| 1317 | + | } | |
| 1318 | + | ||
| 1319 | + | // If console capture is enabled, disable stdout and enable our capture | |
| 1320 | + | if settings := C.webkit_web_view_get_settings(v.native.wv); settings != nil { | |
| 1321 | + | C.webkit_settings_set_enable_write_console_messages_to_stdout(settings, C.gboolean(0)) | |
| 1322 | + | log.Printf("[webkit] Console message capture enabled - redirecting to console.log file") | |
| 1323 | + | } | |
| 1324 | + | ||
| 1325 | + | // TODO: Implement JavaScript injection for console capture | |
| 1326 | + | // This will inject code to intercept console.log/error/warn and send to Go | |
| 1327 | + | } | |
| 1328 | + | ||
| 1329 | + | // setupGlobalConsoleCapture sets up a global stdout filter to capture console messages | |
| 1330 | + | func setupGlobalConsoleCapture() { | |
| 1331 | + | // This should be called once globally, not per WebView | |
| 1332 | + | logging.StartConsoleCapture() | |
| 1333 | + | } | |
| 1334 | + | ||
| 1335 | + | // injectConsoleCapture injects JavaScript to capture console messages | |
| 1336 | + | func (v *WebView) injectConsoleCapture() { | |
| 1337 | + | consoleScript := ` | |
| 1338 | + | (function() { | |
| 1339 | + | const originalLog = console.log; | |
| 1340 | + | const originalWarn = console.warn; | |
| 1341 | + | const originalError = console.error; | |
| 1342 | + | const originalInfo = console.info; | |
| 1343 | + | const originalDebug = console.debug; | |
| 1344 | + | ||
| 1345 | + | function sendToGo(level, args) { | |
| 1346 | + | const message = args.map(arg => | |
| 1347 | + | typeof arg === 'object' ? JSON.stringify(arg) : String(arg) | |
| 1348 | + | ).join(' '); | |
| 1349 | + | ||
| 1350 | + | const formattedMessage = '[' + level + '] ' + window.location.href + ' ' + message; | |
| 1351 | + | ||
| 1352 | + | // Send to Go via user script message | |
| 1353 | + | if (window.webkit && window.webkit.messageHandlers && window.webkit.messageHandlers.dumberBridge) { | |
| 1354 | + | window.webkit.messageHandlers.dumberBridge.postMessage({ | |
| 1355 | + | type: 'console', | |
| 1356 | + | data: formattedMessage | |
| 1357 | + | }); | |
| 1358 | + | } | |
| 1359 | + | } | |
| 1360 | + | ||
| 1361 | + | console.log = function(...args) { | |
| 1362 | + | originalLog.apply(console, args); | |
| 1363 | + | sendToGo('LOG', args); | |
| 1364 | + | }; | |
| 1365 | + | ||
| 1366 | + | console.warn = function(...args) { | |
| 1367 | + | originalWarn.apply(console, args); | |
| 1368 | + | sendToGo('WARN', args); | |
| 1369 | + | }; | |
| 1370 | + | ||
| 1371 | + | console.error = function(...args) { | |
| 1372 | + | originalError.apply(console, args); | |
| 1373 | + | sendToGo('ERROR', args); | |
| 1374 | + | }; | |
| 1375 | + | ||
| 1376 | + | console.info = function(...args) { | |
| 1377 | + | originalInfo.apply(console, args); | |
| 1378 | + | sendToGo('INFO', args); | |
| 1379 | + | }; | |
| 1380 | + | ||
| 1381 | + | console.debug = function(...args) { | |
| 1382 | + | originalDebug.apply(console, args); | |
| 1383 | + | sendToGo('DEBUG', args); | |
| 1384 | + | }; | |
| 1385 | + | })(); | |
| 1386 | + | ` | |
| 1387 | + | _ = v.InjectScript(consoleScript) | |
| 1388 | + | } | |
| 1389 | + | ||
| 1390 | + | // handleConsoleMessage processes console messages from JavaScript | |
| 1391 | + | func (v *WebView) handleConsoleMessage(payload string) { | |
| 1392 | + | // Parse the message payload to check if it's a console message | |
| 1393 | + | var msg struct { | |
| 1394 | + | Type string `json:"type"` | |
| 1395 | + | Data string `json:"data"` | |
| 1396 | + | } | |
| 1397 | + | ||
| 1398 | + | if err := json.Unmarshal([]byte(payload), &msg); err != nil { | |
| 1399 | + | return | |
| 1400 | + | } | |
| 1401 | + | ||
| 1402 | + | if msg.Type == "console" { | |
| 1403 | + | // Send to logging system with [CONSOLE] tag | |
| 1404 | + | logging.CaptureWebKitLog(msg.Data) | |
| 1405 | + | } | |
| 1406 | + | } | |
| 1407 | + | ||
| 1408 | + | // startConsoleCapture starts capturing stdout to filter console messages | |
| 1409 | + | func (v *WebView) startConsoleCapture() { | |
| 1410 | + | // This is a simplified approach - we'll rely on the existing OutputCapture | |
| 1411 | + | // mechanism in internal/logging/capture.go to handle stdout capture | |
| 1412 | + | // and filter for console messages in the log processing | |
| 1413 | + | } | |
| 1414 | + | ||
| 1452 | 1415 | // NewWebViewWithRelated creates a new WebView related to an existing one for process sharing | |
| 1453 | 1416 | func NewWebViewWithRelated(cfg *Config, relatedView *WebView) (*WebView, error) { | |
| 1454 | 1417 | if relatedView == nil || relatedView.native == nil || relatedView.native.wv == nil { | |
| @@ -1532,6 +1495,7 @@ func NewWebViewWithRelated(cfg *Config, relatedView *WebView) (*WebView, error) | |||
| 1532 | 1495 | v.id = nextViewID() | |
| 1533 | 1496 | registerView(v.id, v) | |
| 1534 | 1497 | C.connect_policy_handler(v.native.wv, C.ulong(v.id)) | |
| 1498 | + | ConnectCreateSignal(v) // Connect create signal for window.open support | |
| 1535 | 1499 | ||
| 1536 | 1500 | // Register with memory manager if enabled | |
| 1537 | 1501 | if cfg.Memory.EnableMemoryMonitoring { | |
| @@ -1744,6 +1708,46 @@ func (w *WebView) dispatchPopupRequest(uri string) *WebView { | |||
| 1744 | 1708 | return nil | |
| 1745 | 1709 | } | |
| 1746 | 1710 | ||
| 1711 | + | // CreateRelatedView creates a WebView that shares process/context with this view | |
| 1712 | + | func (w *WebView) CreateRelatedView() *WebView { | |
| 1713 | + | if w == nil || w.native == nil || w.native.wv == nil { | |
| 1714 | + | return nil | |
| 1715 | + | } | |
| 1716 | + | // Build a minimal config inheriting from current view | |
| 1717 | + | cfg := &Config{ | |
| 1718 | + | Assets: w.config.Assets, | |
| 1719 | + | ZoomDefault: w.config.ZoomDefault, | |
| 1720 | + | EnableDeveloperExtras: w.config.EnableDeveloperExtras, | |
| 1721 | + | DataDir: w.config.DataDir, | |
| 1722 | + | CacheDir: w.config.CacheDir, | |
| 1723 | + | APIToken: w.config.APIToken, | |
| 1724 | + | DefaultSansFont: w.config.DefaultSansFont, | |
| 1725 | + | DefaultSerifFont: w.config.DefaultSerifFont, | |
| 1726 | + | DefaultMonospaceFont: w.config.DefaultMonospaceFont, | |
| 1727 | + | DefaultFontSize: w.config.DefaultFontSize, | |
| 1728 | + | Rendering: w.config.Rendering, | |
| 1729 | + | UseDomZoom: w.config.UseDomZoom, | |
| 1730 | + | VideoAcceleration: w.config.VideoAcceleration, | |
| 1731 | + | Memory: w.config.Memory, | |
| 1732 | + | CodecPreferences: w.config.CodecPreferences, | |
| 1733 | + | CreateWindow: false, | |
| 1734 | + | } | |
| 1735 | + | related, err := NewWebViewWithRelated(cfg, w) | |
| 1736 | + | if err != nil { | |
| 1737 | + | log.Printf("[webkit] CreateRelatedView failed: %v", err) | |
| 1738 | + | return nil | |
| 1739 | + | } | |
| 1740 | + | return related | |
| 1741 | + | } | |
| 1742 | + | ||
| 1743 | + | // OnWindowTypeDetected registers a callback invoked when window type is detected | |
| 1744 | + | func (w *WebView) OnWindowTypeDetected(callback func(WindowType, *WindowFeatures)) { | |
| 1745 | + | if w == nil { | |
| 1746 | + | return | |
| 1747 | + | } | |
| 1748 | + | w.windowTypeCallback = callback | |
| 1749 | + | } | |
| 1750 | + | ||
| 1747 | 1751 | // GetNativePointer returns the native WebView pointer for CGO callbacks | |
| 1748 | 1752 | func (w *WebView) GetNativePointer() unsafe.Pointer { | |
| 1749 | 1753 | if w != nil && w.native != nil && w.native.wv != nil { | |
| @@ -2027,10 +2031,26 @@ func clampDomZoom(level float64) float64 { | |||
| 2027 | 2031 | return level | |
| 2028 | 2032 | } | |
| 2029 | 2033 | ||
| 2030 | - | func buildDomBridgeScript(initialZoom float64) string { | |
| 2034 | + | func buildDomBridgeScript(cfg *Config, initialZoom float64) string { | |
| 2035 | + | if cfg == nil || cfg.Assets == nil { | |
| 2036 | + | log.Printf("[webkit] Config or assets not available for main-world script") | |
| 2037 | + | return "" | |
| 2038 | + | } | |
| 2039 | + | ||
| 2040 | + | // Load the compiled main-world script | |
| 2041 | + | scriptBytes, err := cfg.Assets.ReadFile("assets/gui/main-world.min.js") | |
| 2042 | + | if err != nil { | |
| 2043 | + | log.Printf("[webkit] Failed to load main-world script: %v", err) | |
| 2044 | + | return "" | |
| 2045 | + | } | |
| 2046 | + | ||
| 2047 | + | // Replace the zoom placeholder with the actual value | |
| 2031 | 2048 | zoom := clampDomZoom(initialZoom) | |
| 2032 | 2049 | zoomStr := strconv.FormatFloat(zoom, 'f', -1, 64) | |
| 2033 | - | return strings.ReplaceAll(domBridgeTemplate, "__DOM_ZOOM_DEFAULT__", zoomStr) | |
| 2050 | + | script := strings.ReplaceAll(string(scriptBytes), "__DOM_ZOOM_DEFAULT__", zoomStr) | |
| 2051 | + | ||
| 2052 | + | log.Printf("[webkit] Loaded main-world script successfully, size: %d bytes", len(scriptBytes)) | |
| 2053 | + | return script | |
| 2034 | 2054 | } | |
| 2035 | 2055 | ||
| 2036 | 2056 | func (w *WebView) UsesDomZoom() bool { | |
| @@ -2050,7 +2070,7 @@ func (w *WebView) installDomBridgeScript() { | |||
| 2050 | 2070 | w.domBridgeScript = nil | |
| 2051 | 2071 | } | |
| 2052 | 2072 | ||
| 2053 | - | source := buildDomBridgeScript(w.domZoomSeed) | |
| 2073 | + | source := buildDomBridgeScript(w.config, w.domZoomSeed) | |
| 2054 | 2074 | cBridge := C.CString(source) | |
| 2055 | 2075 | defer C.free(unsafe.Pointer(cBridge)) | |
| 2056 | 2076 | ||
| @@ -2076,8 +2096,11 @@ func (w *WebView) SeedDomZoom(level float64) { | |||
| 2076 | 2096 | return | |
| 2077 | 2097 | } | |
| 2078 | 2098 | w.domZoomSeed = seed | |
| 2099 | + | // Instead of re-injecting the entire script, just update the zoom level | |
| 2079 | 2100 | w.RunOnMainThread(func() { | |
| 2080 | - | w.installDomBridgeScript() | |
| 2101 | + | zoomStr := strconv.FormatFloat(seed, 'f', -1, 64) | |
| 2102 | + | script := fmt.Sprintf("if (window.__dumber_applyDomZoom) { window.__dumber_applyDomZoom(%s); }", zoomStr) | |
| 2103 | + | w.InjectScript(script) | |
| 2081 | 2104 | }) | |
| 2082 | 2105 | } | |
| 2083 | 2106 | ||
| @@ -2212,6 +2235,13 @@ func goHandleTLSError(failingURI *C.char, host *C.char, errorFlags C.int, certIn | |||
| 2212 | 2235 | return C.FALSE // Don't proceed | |
| 2213 | 2236 | } | |
| 2214 | 2237 | ||
| 2238 | + | //export goHandleConsoleMessage | |
| 2239 | + | //export goHandleConsoleMessage | |
| 2240 | + | func goHandleConsoleMessage(message *C.char) { | |
| 2241 | + | messageText := C.GoString(message) | |
| 2242 | + | logging.CaptureWebKitLog(messageText) | |
| 2243 | + | } | |
| 2244 | + | ||
| 2215 | 2245 | // showTLSWarningDialog displays a warning dialog for TLS certificate errors with storage capability | |
| 2216 | 2246 | func (w *WebView) showTLSWarningDialog(hostname, uri string, errorFlags int, certificateInfo string) bool { | |
| 2217 | 2247 | if w == nil || w.native == nil || w.native.win == nil { | |
| @@ -2530,3 +2560,8 @@ func (w *WebView) detectAndLogRenderingBackend() { | |||
| 2530 | 2560 | w.InjectScript(detectionScript) | |
| 2531 | 2561 | }() | |
| 2532 | 2562 | } | |
| 2563 | + | ||
| 2564 | + | // SetWindowFeatures sets the window features for this WebView | |
| 2565 | + | func (w *WebView) SetWindowFeatures(features *WindowFeatures) { | |
| 2566 | + | w.windowFeatures = features | |
| 2567 | + | } | |
| @@ -24,7 +24,6 @@ extern void goQuitMainLoop(); | |||||
| 24 | extern void goInvokeHandle(uintptr_t handle); | 24 | extern void goInvokeHandle(uintptr_t handle); | ||
| 25 | extern gboolean goHandleNewWindowPolicy(unsigned long id, char* uri, int nav_type); | 25 | extern gboolean goHandleNewWindowPolicy(unsigned long id, char* uri, int nav_type); | ||
| 26 | extern void goHandlePopupGeometry(unsigned long id, int x, int y, int width, int height); | 26 | extern void goHandlePopupGeometry(unsigned long id, int x, int y, int width, int height); | ||
| 27 | - | extern GtkWidget* goHandleCreateWebView(unsigned long id, char* uri); | |||
| 28 | extern void goHandleLoadChanged(unsigned long id, char* uri, int load_event); | 27 | extern void goHandleLoadChanged(unsigned long id, char* uri, int load_event); | ||
| 29 | extern void goHandleWebViewClose(unsigned long id); | 28 | extern void goHandleWebViewClose(unsigned long id); | ||
| 30 | static gboolean invoke_handle_cb(gpointer data) { | 29 | static gboolean invoke_handle_cb(gpointer data) { | ||
| @@ -49,27 +48,17 @@ static void maybe_set_cookie_policy(WebKitCookieManager* cm, int policy); | |||||
| 49 | 48 | ||||
| 50 | // TLS error handling forward declarations | 49 | // TLS error handling forward declarations | ||
| 51 | extern gboolean goHandleTLSError(char* failing_uri, char* host, int error_flags, char* cert_info); | 50 | extern gboolean goHandleTLSError(char* failing_uri, char* host, int error_flags, char* cert_info); | ||
| 51 | + | ||||
| 52 | + | // Console message handling forward declarations | |||
| 53 | + | extern void goHandleConsoleMessage(char* message); | |||
| 54 | + | ||||
| 52 | static gboolean on_load_failed_with_tls_errors(WebKitWebView *web_view, | 55 | static gboolean on_load_failed_with_tls_errors(WebKitWebView *web_view, | ||
| 53 | const char *failing_uri, | 56 | const char *failing_uri, | ||
| 54 | GTlsCertificate *certificate, | 57 | GTlsCertificate *certificate, | ||
| 55 | GTlsCertificateFlags errors, | 58 | GTlsCertificateFlags errors, | ||
| 56 | gpointer user_data); | 59 | gpointer user_data); | ||
| 57 | 60 | ||||
| 58 | - | // Handle ready-to-show signal for popup windows to properly initialize window properties | |||
| 59 | - | static void on_popup_ready_to_show(WebKitWebView* web_view, gpointer user_data) { | |||
| 60 | - | // Get window properties to ensure they're properly initialized | |||
| 61 | - | WebKitWindowProperties* props = webkit_web_view_get_window_properties(web_view); | |||
| 62 | - | if (props) { | |||
| 63 | - | // Access properties to ensure WindowFeatures are initialized | |||
| 64 | - | GdkRectangle geometry; | |||
| 65 | - | webkit_window_properties_get_geometry(props, &geometry); | |||
| 66 | 61 | ||||
| 67 | - | // Pass geometry to Go for workspace positioning | |||
| 68 | - | unsigned long view_id = (unsigned long)user_data; | |||
| 69 | - | goHandlePopupGeometry(view_id, geometry.x, geometry.y, | |||
| 70 | - | geometry.width, geometry.height); | |||
| 71 | - | } | |||
| 72 | - | } | |||
| 73 | 62 | ||||
| 74 | // Handle load_changed signal for OAuth completion detection | 63 | // Handle load_changed signal for OAuth completion detection | ||
| 75 | static void on_load_changed(WebKitWebView *web_view, WebKitLoadEvent load_event, gpointer user_data) { | 64 | static void on_load_changed(WebKitWebView *web_view, WebKitLoadEvent load_event, gpointer user_data) { | ||
| @@ -90,38 +79,6 @@ static void on_webview_close(WebKitWebView* web_view, gpointer user_data) { | |||||
| 90 | goHandleWebViewClose(view_id); | 79 | goHandleWebViewClose(view_id); | ||
| 91 | } | 80 | } | ||
| 92 | 81 | ||||
| 93 | - | // Handle create signal for popup windows - essential for window.open() support | |||
| 94 | - | static GtkWidget* on_create_web_view(WebKitWebView *web_view, | |||
| 95 | - | WebKitNavigationAction *navigation_action, | |||
| 96 | - | gpointer user_data) { | |||
| 97 | - | WebKitURIRequest *request = webkit_navigation_action_get_request(navigation_action); | |||
| 98 | - | const char* uri = webkit_uri_request_get_uri(request); | |||
| 99 | - | ||||
| 100 | - | printf("[webkit-create] Create signal for URI: %s\n", uri ? uri : "NULL"); | |||
| 101 | - | ||||
| 102 | - | unsigned long view_id = (unsigned long)user_data; | |||
| 103 | - | GtkWidget* new_webview = goHandleCreateWebView(view_id, (char*)uri); | |||
| 104 | - | ||||
| 105 | - | if (new_webview) { | |||
| 106 | - | printf("[webkit-create] Go handler provided WebView - using workspace pane\n"); | |||
| 107 | - | ||||
| 108 | - | // Connect load_changed signal to the new popup WebView for OAuth detection | |||
| 109 | - | WebKitWebView* popup_webview = WEBKIT_WEB_VIEW(new_webview); | |||
| 110 | - | g_signal_connect_data(G_OBJECT(popup_webview), "load-changed", | |||
| 111 | - | G_CALLBACK(on_load_changed), user_data, NULL, 0); | |||
| 112 | - | printf("[webkit-create] Connected load-changed signal to popup for OAuth detection\n"); | |||
| 113 | - | ||||
| 114 | - | // Connect close signal to handle window.close() from OAuth providers | |||
| 115 | - | g_signal_connect_data(G_OBJECT(popup_webview), "close", | |||
| 116 | - | G_CALLBACK(on_webview_close), user_data, NULL, 0); | |||
| 117 | - | printf("[webkit-create] Connected close signal to popup for OAuth auto-close\n"); | |||
| 118 | - | ||||
| 119 | - | return new_webview; | |||
| 120 | - | } else { | |||
| 121 | - | printf("[webkit-create] Go handler declined - allowing native popup window\n"); | |||
| 122 | - | return NULL; // Let WebKit create native popup window | |||
| 123 | - | } | |||
| 124 | - | } | |||
| 125 | 82 | ||||
| 126 | // Handle decide-policy signal for new window requests - more robust than create signal | 83 | // Handle decide-policy signal for new window requests - more robust than create signal | ||
| 127 | static gboolean on_decide_policy(WebKitWebView *web_view, | 84 | static gboolean on_decide_policy(WebKitWebView *web_view, | ||
| @@ -282,9 +239,9 @@ static GtkWidget* new_webview_related(WebKitWebView* related_view, WebKitUserCon | |||||
| 282 | 239 | ||||
| 283 | static void connect_policy_handler(WebKitWebView* web_view, unsigned long id) { | 240 | static void connect_policy_handler(WebKitWebView* web_view, unsigned long id) { | ||
| 284 | if (!web_view) return; | 241 | if (!web_view) return; | ||
| 285 | - | g_signal_connect_data(G_OBJECT(web_view), "decide-policy", G_CALLBACK(on_decide_policy), (gpointer)id, NULL, 0); | 242 | + | // Create signal is now handled in window_handler_cgo.go |
| 286 | - | // Connect create signal for popup window support | 243 | + | // Use ConnectCreateSignal from Go to connect the signal |
| 287 | - | g_signal_connect_data(G_OBJECT(web_view), "create", G_CALLBACK(on_create_web_view), (gpointer)id, NULL, 0); | 244 | + | printf("[webkit-debug] Policy handler connected (create signal handled separately)\n"); |
| 288 | } | 245 | } | ||
| 289 | 246 | ||||
| 290 | // Connect close signal to a WebView for popup auto-close functionality | 247 | // Connect close signal to a WebView for popup auto-close functionality | ||
| @@ -780,6 +737,7 @@ import ( | |||||
| 780 | "context" | 737 | "context" | ||
| 781 | "crypto/sha256" | 738 | "crypto/sha256" | ||
| 782 | "database/sql" | 739 | "database/sql" | ||
| 740 | + | "encoding/json" | |||
| 783 | "errors" | 741 | "errors" | ||
| 784 | "fmt" | 742 | "fmt" | ||
| 785 | "log" | 743 | "log" | ||
| @@ -807,125 +765,7 @@ type FilterManager interface { | |||||
| 807 | GetCosmeticScriptForDomain(domain string) string | 765 | GetCosmeticScriptForDomain(domain string) string | ||
| 808 | } | 766 | } | ||
| 809 | 767 | ||||
| 810 | - | const domBridgeTemplate = `(() => { try { | 768 | + | // domBridgeTemplate removed - now loading compiled main-world.min.js |
| 811 | - | if (window.__dumber_page_bridge_installed) return; | |||
| 812 | - | window.__dumber_page_bridge_installed = true; | |||
| 813 | - | ||||
| 814 | - | const __dumberInitialZoom = __DOM_ZOOM_DEFAULT__; | |||
| 815 | - | window.__dumber_dom_zoom_seed = __dumberInitialZoom; | |||
| 816 | - | ||||
| 817 | - | // Theme setter function for GTK theme integration | |||
| 818 | - | window.__dumber_setTheme = (theme) => { | |||
| 819 | - | window.__dumber_initial_theme = theme; | |||
| 820 | - | console.log('[dumber] Setting theme to:', theme); | |||
| 821 | - | if (theme === 'dark') { | |||
| 822 | - | document.documentElement.classList.add('dark'); | |||
| 823 | - | } else { | |||
| 824 | - | document.documentElement.classList.remove('dark'); | |||
| 825 | - | } | |||
| 826 | - | }; | |||
| 827 | - | ||||
| 828 | - | // Ensure unified API object | |||
| 829 | - | window.__dumber = window.__dumber || {}; | |||
| 830 | - | // Toast namespace | |||
| 831 | - | window.__dumber.toast = window.__dumber.toast || { | |||
| 832 | - | show: function(message, duration, type) { | |||
| 833 | - | try { | |||
| 834 | - | document.dispatchEvent(new CustomEvent('dumber:toast:show', { detail: { message, duration, type } })); | |||
| 835 | - | // Legacy compatibility | |||
| 836 | - | document.dispatchEvent(new CustomEvent('dumber:showToast', { detail: { message, duration, type } })); | |||
| 837 | - | } catch(e) { /* ignore */ } | |||
| 838 | - | }, | |||
| 839 | - | zoom: function(level) { | |||
| 840 | - | try { | |||
| 841 | - | document.dispatchEvent(new CustomEvent('dumber:toast:zoom', { detail: { level } })); | |||
| 842 | - | // Legacy compatibility | |||
| 843 | - | document.dispatchEvent(new CustomEvent('dumber:showZoomToast', { detail: { level } })); | |||
| 844 | - | } catch(e) { /* ignore */ } | |||
| 845 | - | } | |||
| 846 | - | }; | |||
| 847 | - | // Back-compat global helpers | |||
| 848 | - | window.__dumber_showToast = function(message, duration, type) { window.__dumber.toast.show(message, duration, type); }; | |||
| 849 | - | window.__dumber_showZoomToast = function(level) { window.__dumber.toast.zoom(level); }; | |||
| 850 | - | ||||
| 851 | - | // DOM zoom helpers (optional native override) | |||
| 852 | - | if (typeof window.__dumber_dom_zoom_level !== 'number') { | |||
| 853 | - | window.__dumber_dom_zoom_level = __dumberInitialZoom; | |||
| 854 | - | } | |||
| 855 | - | const __dumberApplyZoomStyles = function(node, level) { | |||
| 856 | - | if (!node) return; | |||
| 857 | - | if (Math.abs(level - 1.0) < 1e-6) { | |||
| 858 | - | node.style.removeProperty('zoom'); | |||
| 859 | - | node.style.removeProperty('transform'); | |||
| 860 | - | node.style.removeProperty('transform-origin'); | |||
| 861 | - | node.style.removeProperty('width'); | |||
| 862 | - | node.style.removeProperty('min-width'); | |||
| 863 | - | node.style.removeProperty('height'); | |||
| 864 | - | node.style.removeProperty('min-height'); | |||
| 865 | - | return; | |||
| 866 | - | } | |||
| 867 | - | const scale = level; | |||
| 868 | - | const inversePercent = 100 / scale; | |||
| 869 | - | const widthValue = inversePercent + '%'; | |||
| 870 | - | node.style.removeProperty('zoom'); | |||
| 871 | - | node.style.transform = 'scale(' + scale + ')'; | |||
| 872 | - | node.style.transformOrigin = '0 0'; | |||
| 873 | - | node.style.width = widthValue; | |||
| 874 | - | node.style.minWidth = widthValue; | |||
| 875 | - | node.style.minHeight = '100%'; | |||
| 876 | - | }; | |||
| 877 | - | window.__dumber_applyDomZoom = function(level) { | |||
| 878 | - | try { | |||
| 879 | - | window.__dumber_dom_zoom_level = level; | |||
| 880 | - | window.__dumber_dom_zoom_seed = level; | |||
| 881 | - | __dumberApplyZoomStyles(document.documentElement, level); | |||
| 882 | - | if (document.body) { | |||
| 883 | - | __dumberApplyZoomStyles(document.body, level); | |||
| 884 | - | } | |||
| 885 | - | } catch (e) { | |||
| 886 | - | console.error('[dumber] DOM zoom error', e); | |||
| 887 | - | } | |||
| 888 | - | }; | |||
| 889 | - | // Apply immediately so first paint uses the desired zoom, then reapply when body exists. | |||
| 890 | - | window.__dumber_applyDomZoom(window.__dumber_dom_zoom_level); | |||
| 891 | - | if (!document.body) { | |||
| 892 | - | document.addEventListener('DOMContentLoaded', function() { | |||
| 893 | - | if (typeof window.__dumber_dom_zoom_level === 'number') { | |||
| 894 | - | window.__dumber_applyDomZoom(window.__dumber_dom_zoom_level); | |||
| 895 | - | } | |||
| 896 | - | }, { once: true }); | |||
| 897 | - | } | |||
| 898 | - | ||||
| 899 | - | // Omnibox suggestions bridge (with queue before ready) | |||
| 900 | - | let __omniboxQueue = []; | |||
| 901 | - | let __omniboxReady = false; | |||
| 902 | - | function __omniboxDispatch(suggestions) { | |||
| 903 | - | try { | |||
| 904 | - | document.dispatchEvent(new CustomEvent('dumber:omnibox:suggestions', { detail: { suggestions } })); | |||
| 905 | - | // Legacy compatibility event name | |||
| 906 | - | document.dispatchEvent(new CustomEvent('dumber:omnibox-suggestions', { detail: { suggestions } })); | |||
| 907 | - | } catch (e) { /* ignore */ } | |||
| 908 | - | } | |||
| 909 | - | window.__dumber_omnibox_suggestions = function(suggestions) { | |||
| 910 | - | if (__omniboxReady) { | |||
| 911 | - | __omniboxDispatch(suggestions); | |||
| 912 | - | } else { | |||
| 913 | - | try { __omniboxQueue.push(suggestions); } catch (_) { /* ignore */ } | |||
| 914 | - | } | |||
| 915 | - | }; | |||
| 916 | - | document.addEventListener('dumber:omnibox-ready', function() { | |||
| 917 | - | __omniboxReady = true; | |||
| 918 | - | if (__omniboxQueue && __omniboxQueue.length) { | |||
| 919 | - | const items = __omniboxQueue.slice(); | |||
| 920 | - | __omniboxQueue.length = 0; | |||
| 921 | - | for (const s of items) __omniboxDispatch(s); | |||
| 922 | - | } | |||
| 923 | - | }); | |||
| 924 | - | // Unified omnibox API for page-world callers | |||
| 925 | - | window.__dumber.omnibox = window.__dumber.omnibox || { | |||
| 926 | - | suggestions: function(suggestions) { window.__dumber_omnibox_suggestions(suggestions); } | |||
| 927 | - | }; | |||
| 928 | - | } catch (e) { console.warn('[dumber] unified bridge init failed', e); } })();` | |||
| 929 | 769 | ||||
| 930 | var registerSchemeOnce sync.Once | 770 | var registerSchemeOnce sync.Once | ||
| 931 | 771 | ||||
| @@ -1071,9 +911,9 @@ type WebView struct { | |||||
| 1071 | titleHandler func(title string) | 911 | titleHandler func(title string) | ||
| 1072 | uriHandler func(uri string) | 912 | uriHandler func(uri string) | ||
| 1073 | zoomHandler func(level float64) | 913 | zoomHandler func(level float64) | ||
| 1074 | - | popupHandler func(string) *WebView | 914 | + | popupHandler func(string) *WebView |
| 1075 | - | closeHandler func() | 915 | + | closeHandler func() |
| 1076 | - | memStats *memoryStats | 916 | + | memStats *memoryStats |
| 1077 | gcTicker *time.Ticker | 917 | gcTicker *time.Ticker | ||
| 1078 | gcDone chan struct{} | 918 | gcDone chan struct{} | ||
| 1079 | tlsExceptions map[string]bool // host -> whether user allowed certificate exception | 919 | tlsExceptions map[string]bool // host -> whether user allowed certificate exception | ||
| @@ -1081,6 +921,13 @@ type WebView struct { | |||||
| 1081 | domZoomSeed float64 | 921 | domZoomSeed float64 | ||
| 1082 | domBridgeScript *C.WebKitUserScript | 922 | domBridgeScript *C.WebKitUserScript | ||
| 1083 | wasReparented bool // Track if this WebView has been reparented | 923 | wasReparented bool // Track if this WebView has been reparented | ||
| 924 | + | ||||
| 925 | + | // Window type detection and relationships | |||
| 926 | + | windowType WindowType | |||
| 927 | + | windowFeatures *WindowFeatures | |||
| 928 | + | windowTypeCallback func(WindowType, *WindowFeatures) | |||
| 929 | + | parentWebView *WebView | |||
| 930 | + | isRelated bool | |||
| 1084 | } | 931 | } | ||
| 1085 | 932 | ||||
| 1086 | // NewWebView constructs a new WebView instance with native WebKit2GTK widgets. | 933 | // NewWebView constructs a new WebView instance with native WebKit2GTK widgets. | ||
| @@ -1237,6 +1084,7 @@ func NewWebView(cfg *Config) (*WebView, error) { | |||||
| 1237 | v.id = nextViewID() | 1084 | v.id = nextViewID() | ||
| 1238 | registerView(v.id, v) | 1085 | registerView(v.id, v) | ||
| 1239 | C.connect_policy_handler(v.native.wv, C.ulong(v.id)) | 1086 | C.connect_policy_handler(v.native.wv, C.ulong(v.id)) | ||
| 1087 | + | ConnectCreateSignal(v) // Connect create signal for window.open support | |||
| 1240 | 1088 | ||||
| 1241 | // Register with global memory manager if monitoring is enabled | 1089 | // Register with global memory manager if monitoring is enabled | ||
| 1242 | if cfg.Memory.EnableMemoryMonitoring { | 1090 | if cfg.Memory.EnableMemoryMonitoring { | ||
| @@ -1446,9 +1294,124 @@ func NewWebView(cfg *Config) (*WebView, error) { | |||||
| 1446 | } | 1294 | } | ||
| 1447 | } | 1295 | } | ||
| 1448 | 1296 | ||||
| 1297 | + | // Setup console message logging | |||
| 1298 | + | v.setupConsoleLogging() | |||
| 1299 | + | ||||
| 1449 | return v, nil | 1300 | return v, nil | ||
| 1450 | } | 1301 | } | ||
| 1451 | 1302 | ||||
| 1303 | + | // setupConsoleLogging enables console message capture for the WebView | |||
| 1304 | + | func (v *WebView) setupConsoleLogging() { | |||
| 1305 | + | if v.native == nil || v.native.wv == nil { | |||
| 1306 | + | return | |||
| 1307 | + | } | |||
| 1308 | + | ||||
| 1309 | + | // Check if console capture is enabled in config | |||
| 1310 | + | cfg := config.Get() | |||
| 1311 | + | if !cfg.Logging.CaptureConsole { | |||
| 1312 | + | // Disable console messages to stdout (always, to keep backend clean) | |||
| 1313 | + | if settings := C.webkit_web_view_get_settings(v.native.wv); settings != nil { | |||
| 1314 | + | C.webkit_settings_set_enable_write_console_messages_to_stdout(settings, C.gboolean(0)) | |||
| 1315 | + | } | |||
| 1316 | + | return | |||
| 1317 | + | } | |||
| 1318 | + | ||||
| 1319 | + | // If console capture is enabled, disable stdout and enable our capture | |||
| 1320 | + | if settings := C.webkit_web_view_get_settings(v.native.wv); settings != nil { | |||
| 1321 | + | C.webkit_settings_set_enable_write_console_messages_to_stdout(settings, C.gboolean(0)) | |||
| 1322 | + | log.Printf("[webkit] Console message capture enabled - redirecting to console.log file") | |||
| 1323 | + | } | |||
| 1324 | + | ||||
| 1325 | + | // TODO: Implement JavaScript injection for console capture | |||
| 1326 | + | // This will inject code to intercept console.log/error/warn and send to Go | |||
| 1327 | + | } | |||
| 1328 | + | ||||
| 1329 | + | // setupGlobalConsoleCapture sets up a global stdout filter to capture console messages | |||
| 1330 | + | func setupGlobalConsoleCapture() { | |||
| 1331 | + | // This should be called once globally, not per WebView | |||
| 1332 | + | logging.StartConsoleCapture() | |||
| 1333 | + | } | |||
| 1334 | + | ||||
| 1335 | + | // injectConsoleCapture injects JavaScript to capture console messages | |||
| 1336 | + | func (v *WebView) injectConsoleCapture() { | |||
| 1337 | + | consoleScript := ` | |||
| 1338 | + | (function() { | |||
| 1339 | + | const originalLog = console.log; | |||
| 1340 | + | const originalWarn = console.warn; | |||
| 1341 | + | const originalError = console.error; | |||
| 1342 | + | const originalInfo = console.info; | |||
| 1343 | + | const originalDebug = console.debug; | |||
| 1344 | + | ||||
| 1345 | + | function sendToGo(level, args) { | |||
| 1346 | + | const message = args.map(arg => | |||
| 1347 | + | typeof arg === 'object' ? JSON.stringify(arg) : String(arg) | |||
| 1348 | + | ).join(' '); | |||
| 1349 | + | ||||
| 1350 | + | const formattedMessage = '[' + level + '] ' + window.location.href + ' ' + message; | |||
| 1351 | + | ||||
| 1352 | + | // Send to Go via user script message | |||
| 1353 | + | if (window.webkit && window.webkit.messageHandlers && window.webkit.messageHandlers.dumberBridge) { | |||
| 1354 | + | window.webkit.messageHandlers.dumberBridge.postMessage({ | |||
| 1355 | + | type: 'console', | |||
| 1356 | + | data: formattedMessage | |||
| 1357 | + | }); | |||
| 1358 | + | } | |||
| 1359 | + | } | |||
| 1360 | + | ||||
| 1361 | + | console.log = function(...args) { | |||
| 1362 | + | originalLog.apply(console, args); | |||
| 1363 | + | sendToGo('LOG', args); | |||
| 1364 | + | }; | |||
| 1365 | + | ||||
| 1366 | + | console.warn = function(...args) { | |||
| 1367 | + | originalWarn.apply(console, args); | |||
| 1368 | + | sendToGo('WARN', args); | |||
| 1369 | + | }; | |||
| 1370 | + | ||||
| 1371 | + | console.error = function(...args) { | |||
| 1372 | + | originalError.apply(console, args); | |||
| 1373 | + | sendToGo('ERROR', args); | |||
| 1374 | + | }; | |||
| 1375 | + | ||||
| 1376 | + | console.info = function(...args) { | |||
| 1377 | + | originalInfo.apply(console, args); | |||
| 1378 | + | sendToGo('INFO', args); | |||
| 1379 | + | }; | |||
| 1380 | + | ||||
| 1381 | + | console.debug = function(...args) { | |||
| 1382 | + | originalDebug.apply(console, args); | |||
| 1383 | + | sendToGo('DEBUG', args); | |||
| 1384 | + | }; | |||
| 1385 | + | })(); | |||
| 1386 | + | ` | |||
| 1387 | + | _ = v.InjectScript(consoleScript) | |||
| 1388 | + | } | |||
| 1389 | + | ||||
| 1390 | + | // handleConsoleMessage processes console messages from JavaScript | |||
| 1391 | + | func (v *WebView) handleConsoleMessage(payload string) { | |||
| 1392 | + | // Parse the message payload to check if it's a console message | |||
| 1393 | + | var msg struct { | |||
| 1394 | + | Type string `json:"type"` | |||
| 1395 | + | Data string `json:"data"` | |||
| 1396 | + | } | |||
| 1397 | + | ||||
| 1398 | + | if err := json.Unmarshal([]byte(payload), &msg); err != nil { | |||
| 1399 | + | return | |||
| 1400 | + | } | |||
| 1401 | + | ||||
| 1402 | + | if msg.Type == "console" { | |||
| 1403 | + | // Send to logging system with [CONSOLE] tag | |||
| 1404 | + | logging.CaptureWebKitLog(msg.Data) | |||
| 1405 | + | } | |||
| 1406 | + | } | |||
| 1407 | + | ||||
| 1408 | + | // startConsoleCapture starts capturing stdout to filter console messages | |||
| 1409 | + | func (v *WebView) startConsoleCapture() { | |||
| 1410 | + | // This is a simplified approach - we'll rely on the existing OutputCapture | |||
| 1411 | + | // mechanism in internal/logging/capture.go to handle stdout capture | |||
| 1412 | + | // and filter for console messages in the log processing | |||
| 1413 | + | } | |||
| 1414 | + | ||||
| 1452 | // NewWebViewWithRelated creates a new WebView related to an existing one for process sharing | 1415 | // NewWebViewWithRelated creates a new WebView related to an existing one for process sharing | ||
| 1453 | func NewWebViewWithRelated(cfg *Config, relatedView *WebView) (*WebView, error) { | 1416 | func NewWebViewWithRelated(cfg *Config, relatedView *WebView) (*WebView, error) { | ||
| 1454 | if relatedView == nil || relatedView.native == nil || relatedView.native.wv == nil { | 1417 | if relatedView == nil || relatedView.native == nil || relatedView.native.wv == nil { | ||
| @@ -1532,6 +1495,7 @@ func NewWebViewWithRelated(cfg *Config, relatedView *WebView) (*WebView, error) | |||||
| 1532 | v.id = nextViewID() | 1495 | v.id = nextViewID() | ||
| 1533 | registerView(v.id, v) | 1496 | registerView(v.id, v) | ||
| 1534 | C.connect_policy_handler(v.native.wv, C.ulong(v.id)) | 1497 | C.connect_policy_handler(v.native.wv, C.ulong(v.id)) | ||
| 1498 | + | ConnectCreateSignal(v) // Connect create signal for window.open support | |||
| 1535 | 1499 | ||||
| 1536 | // Register with memory manager if enabled | 1500 | // Register with memory manager if enabled | ||
| 1537 | if cfg.Memory.EnableMemoryMonitoring { | 1501 | if cfg.Memory.EnableMemoryMonitoring { | ||
| @@ -1744,6 +1708,46 @@ func (w *WebView) dispatchPopupRequest(uri string) *WebView { | |||||
| 1744 | return nil | 1708 | return nil | ||
| 1745 | } | 1709 | } | ||
| 1746 | 1710 | ||||
| 1711 | + | // CreateRelatedView creates a WebView that shares process/context with this view | |||
| 1712 | + | func (w *WebView) CreateRelatedView() *WebView { | |||
| 1713 | + | if w == nil || w.native == nil || w.native.wv == nil { | |||
| 1714 | + | return nil | |||
| 1715 | + | } | |||
| 1716 | + | // Build a minimal config inheriting from current view | |||
| 1717 | + | cfg := &Config{ | |||
| 1718 | + | Assets: w.config.Assets, | |||
| 1719 | + | ZoomDefault: w.config.ZoomDefault, | |||
| 1720 | + | EnableDeveloperExtras: w.config.EnableDeveloperExtras, | |||
| 1721 | + | DataDir: w.config.DataDir, | |||
| 1722 | + | CacheDir: w.config.CacheDir, | |||
| 1723 | + | APIToken: w.config.APIToken, | |||
| 1724 | + | DefaultSansFont: w.config.DefaultSansFont, | |||
| 1725 | + | DefaultSerifFont: w.config.DefaultSerifFont, | |||
| 1726 | + | DefaultMonospaceFont: w.config.DefaultMonospaceFont, | |||
| 1727 | + | DefaultFontSize: w.config.DefaultFontSize, | |||
| 1728 | + | Rendering: w.config.Rendering, | |||
| 1729 | + | UseDomZoom: w.config.UseDomZoom, | |||
| 1730 | + | VideoAcceleration: w.config.VideoAcceleration, | |||
| 1731 | + | Memory: w.config.Memory, | |||
| 1732 | + | CodecPreferences: w.config.CodecPreferences, | |||
| 1733 | + | CreateWindow: false, | |||
| 1734 | + | } | |||
| 1735 | + | related, err := NewWebViewWithRelated(cfg, w) | |||
| 1736 | + | if err != nil { | |||
| 1737 | + | log.Printf("[webkit] CreateRelatedView failed: %v", err) | |||
| 1738 | + | return nil | |||
| 1739 | + | } | |||
| 1740 | + | return related | |||
| 1741 | + | } | |||
| 1742 | + | ||||
| 1743 | + | // OnWindowTypeDetected registers a callback invoked when window type is detected | |||
| 1744 | + | func (w *WebView) OnWindowTypeDetected(callback func(WindowType, *WindowFeatures)) { | |||
| 1745 | + | if w == nil { | |||
| 1746 | + | return | |||
| 1747 | + | } | |||
| 1748 | + | w.windowTypeCallback = callback | |||
| 1749 | + | } | |||
| 1750 | + | ||||
| 1747 | // GetNativePointer returns the native WebView pointer for CGO callbacks | 1751 | // GetNativePointer returns the native WebView pointer for CGO callbacks | ||
| 1748 | func (w *WebView) GetNativePointer() unsafe.Pointer { | 1752 | func (w *WebView) GetNativePointer() unsafe.Pointer { | ||
| 1749 | if w != nil && w.native != nil && w.native.wv != nil { | 1753 | if w != nil && w.native != nil && w.native.wv != nil { | ||
| @@ -2027,10 +2031,26 @@ func clampDomZoom(level float64) float64 { | |||||
| 2027 | return level | 2031 | return level | ||
| 2028 | } | 2032 | } | ||
| 2029 | 2033 | ||||
| 2030 | - | func buildDomBridgeScript(initialZoom float64) string { | 2034 | + | func buildDomBridgeScript(cfg *Config, initialZoom float64) string { |
| 2035 | + | if cfg == nil || cfg.Assets == nil { | |||
| 2036 | + | log.Printf("[webkit] Config or assets not available for main-world script") | |||
| 2037 | + | return "" | |||
| 2038 | + | } | |||
| 2039 | + | ||||
| 2040 | + | // Load the compiled main-world script | |||
| 2041 | + | scriptBytes, err := cfg.Assets.ReadFile("assets/gui/main-world.min.js") | |||
| 2042 | + | if err != nil { | |||
| 2043 | + | log.Printf("[webkit] Failed to load main-world script: %v", err) | |||
| 2044 | + | return "" | |||
| 2045 | + | } | |||
| 2046 | + | ||||
| 2047 | + | // Replace the zoom placeholder with the actual value | |||
| 2031 | zoom := clampDomZoom(initialZoom) | 2048 | zoom := clampDomZoom(initialZoom) | ||
| 2032 | zoomStr := strconv.FormatFloat(zoom, 'f', -1, 64) | 2049 | zoomStr := strconv.FormatFloat(zoom, 'f', -1, 64) | ||
| 2033 | - | return strings.ReplaceAll(domBridgeTemplate, "__DOM_ZOOM_DEFAULT__", zoomStr) | 2050 | + | script := strings.ReplaceAll(string(scriptBytes), "__DOM_ZOOM_DEFAULT__", zoomStr) |
| 2051 | + | ||||
| 2052 | + | log.Printf("[webkit] Loaded main-world script successfully, size: %d bytes", len(scriptBytes)) | |||
| 2053 | + | return script | |||
| 2034 | } | 2054 | } | ||
| 2035 | 2055 | ||||
| 2036 | func (w *WebView) UsesDomZoom() bool { | 2056 | func (w *WebView) UsesDomZoom() bool { | ||
| @@ -2050,7 +2070,7 @@ func (w *WebView) installDomBridgeScript() { | |||||
| 2050 | w.domBridgeScript = nil | 2070 | w.domBridgeScript = nil | ||
| 2051 | } | 2071 | } | ||
| 2052 | 2072 | ||||
| 2053 | - | source := buildDomBridgeScript(w.domZoomSeed) | 2073 | + | source := buildDomBridgeScript(w.config, w.domZoomSeed) |
| 2054 | cBridge := C.CString(source) | 2074 | cBridge := C.CString(source) | ||
| 2055 | defer C.free(unsafe.Pointer(cBridge)) | 2075 | defer C.free(unsafe.Pointer(cBridge)) | ||
| 2056 | 2076 | ||||
| @@ -2076,8 +2096,11 @@ func (w *WebView) SeedDomZoom(level float64) { | |||||
| 2076 | return | 2096 | return | ||
| 2077 | } | 2097 | } | ||
| 2078 | w.domZoomSeed = seed | 2098 | w.domZoomSeed = seed | ||
| 2099 | + | // Instead of re-injecting the entire script, just update the zoom level | |||
| 2079 | w.RunOnMainThread(func() { | 2100 | w.RunOnMainThread(func() { | ||
| 2080 | - | w.installDomBridgeScript() | 2101 | + | zoomStr := strconv.FormatFloat(seed, 'f', -1, 64) |
| 2102 | + | script := fmt.Sprintf("if (window.__dumber_applyDomZoom) { window.__dumber_applyDomZoom(%s); }", zoomStr) | |||
| 2103 | + | w.InjectScript(script) | |||
| 2081 | }) | 2104 | }) | ||
| 2082 | } | 2105 | } | ||
| 2083 | 2106 | ||||
| @@ -2212,6 +2235,13 @@ func goHandleTLSError(failingURI *C.char, host *C.char, errorFlags C.int, certIn | |||||
| 2212 | return C.FALSE // Don't proceed | 2235 | return C.FALSE // Don't proceed | ||
| 2213 | } | 2236 | } | ||
| 2214 | 2237 | ||||
| 2238 | + | //export goHandleConsoleMessage | |||
| 2239 | + | //export goHandleConsoleMessage | |||
| 2240 | + | func goHandleConsoleMessage(message *C.char) { | |||
| 2241 | + | messageText := C.GoString(message) | |||
| 2242 | + | logging.CaptureWebKitLog(messageText) | |||
| 2243 | + | } | |||
| 2244 | + | ||||
| 2215 | // showTLSWarningDialog displays a warning dialog for TLS certificate errors with storage capability | 2245 | // showTLSWarningDialog displays a warning dialog for TLS certificate errors with storage capability | ||
| 2216 | func (w *WebView) showTLSWarningDialog(hostname, uri string, errorFlags int, certificateInfo string) bool { | 2246 | func (w *WebView) showTLSWarningDialog(hostname, uri string, errorFlags int, certificateInfo string) bool { | ||
| 2217 | if w == nil || w.native == nil || w.native.win == nil { | 2247 | if w == nil || w.native == nil || w.native.win == nil { | ||
| @@ -2530,3 +2560,8 @@ func (w *WebView) detectAndLogRenderingBackend() { | |||||
| 2530 | w.InjectScript(detectionScript) | 2560 | w.InjectScript(detectionScript) | ||
| 2531 | }() | 2561 | }() | ||
| 2532 | } | 2562 | } | ||
| 2563 | + | ||||
| 2564 | + | // SetWindowFeatures sets the window features for this WebView | |||
| 2565 | + | func (w *WebView) SetWindowFeatures(features *WindowFeatures) { | |||
| 2566 | + | w.windowFeatures = features | |||
| 2567 | + | } | |||
A pkg/webkit/window_handler_cgo.go +72 -0
| @@ -0,0 +1,72 @@ | |||
| 1 | + | //go:build webkit_cgo | |
| 2 | + | ||
| 3 | + | package webkit | |
| 4 | + | ||
| 5 | + | /* | |
| 6 | + | #cgo pkg-config: gtk4 webkitgtk-6.0 | |
| 7 | + | #include <gtk/gtk.h> | |
| 8 | + | #include <gdk/gdk.h> | |
| 9 | + | #include <webkit/webkit.h> | |
| 10 | + | ||
| 11 | + | // Declarations for Go callbacks | |
| 12 | + | extern GtkWidget* goHandleCreateWebView(unsigned long id, char* uri); | |
| 13 | + | ||
| 14 | + | // Handle create signal for popup windows - essential for window.open() support | |
| 15 | + | static GtkWidget* on_create_web_view(WebKitWebView *web_view, | |
| 16 | + | WebKitNavigationAction *navigation_action, | |
| 17 | + | gpointer user_data) { | |
| 18 | + | WebKitURIRequest *request = webkit_navigation_action_get_request(navigation_action); | |
| 19 | + | const char* uri = webkit_uri_request_get_uri(request); | |
| 20 | + | ||
| 21 | + | printf("[webkit-create] Create signal for URI: %s\n", uri ? uri : "NULL"); | |
| 22 | + | ||
| 23 | + | unsigned long parent_view_id = (unsigned long)user_data; | |
| 24 | + | ||
| 25 | + | // Let Go handler create WebView and return it like Epiphany does | |
| 26 | + | GtkWidget* new_webview = goHandleCreateWebView(parent_view_id, (char*)uri); | |
| 27 | + | ||
| 28 | + | if (new_webview) { | |
| 29 | + | printf("[webkit-create] Go handler provided WebView - using workspace pane\n"); | |
| 30 | + | return new_webview; | |
| 31 | + | } else { | |
| 32 | + | printf("[webkit-create] Go handler declined - allowing native popup window\n"); | |
| 33 | + | return NULL; // Let WebKit create native popup window | |
| 34 | + | } | |
| 35 | + | } | |
| 36 | + | ||
| 37 | + | static void connect_create_signal(WebKitWebView* web_view, unsigned long id) { | |
| 38 | + | if (!web_view) return; | |
| 39 | + | g_signal_connect_data(G_OBJECT(web_view), "create", G_CALLBACK(on_create_web_view), (gpointer)id, NULL, 0); | |
| 40 | + | printf("[webkit-debug] create-web-view callback ENABLED with WebKit-created WebViews\n"); | |
| 41 | + | } | |
| 42 | + | */ | |
| 43 | + | import "C" | |
| 44 | + | import ( | |
| 45 | + | "log" | |
| 46 | + | "unsafe" | |
| 47 | + | ) | |
| 48 | + | ||
| 49 | + | //export goHandleCreateWebView | |
| 50 | + | func goHandleCreateWebView(parentID C.ulong, curi *C.char) *C.GtkWidget { | |
| 51 | + | uri := "" | |
| 52 | + | if curi != nil { | |
| 53 | + | uri = C.GoString(curi) | |
| 54 | + | } | |
| 55 | + | ||
| 56 | + | log.Printf("[webkit] Create signal bypassed: URI=%s (handled by JavaScript bypass)", uri) | |
| 57 | + | ||
| 58 | + | // Always return NULL - window.open is now handled entirely by JavaScript bypass | |
| 59 | + | // This prevents duplicate pane creation from WebKit's create signal | |
| 60 | + | return nil | |
| 61 | + | } | |
| 62 | + | ||
| 63 | + | // ConnectCreateSignal connects the create signal to a WebView | |
| 64 | + | func ConnectCreateSignal(view *WebView) { | |
| 65 | + | if view == nil || view.native == nil || view.native.view == nil { | |
| 66 | + | log.Printf("[webkit] ConnectCreateSignal: invalid WebView") | |
| 67 | + | return | |
| 68 | + | } | |
| 69 | + | ||
| 70 | + | webkitView := (*C.WebKitWebView)(unsafe.Pointer(view.native.view)) | |
| 71 | + | C.connect_create_signal(webkitView, C.ulong(view.id)) | |
| 72 | + | } | |
| @@ -0,0 +1,72 @@ | |||||
| 1 | + | //go:build webkit_cgo | |||
| 2 | + | ||||
| 3 | + | package webkit | |||
| 4 | + | ||||
| 5 | + | /* | |||
| 6 | + | #cgo pkg-config: gtk4 webkitgtk-6.0 | |||
| 7 | + | #include <gtk/gtk.h> | |||
| 8 | + | #include <gdk/gdk.h> | |||
| 9 | + | #include <webkit/webkit.h> | |||
| 10 | + | ||||
| 11 | + | // Declarations for Go callbacks | |||
| 12 | + | extern GtkWidget* goHandleCreateWebView(unsigned long id, char* uri); | |||
| 13 | + | ||||
| 14 | + | // Handle create signal for popup windows - essential for window.open() support | |||
| 15 | + | static GtkWidget* on_create_web_view(WebKitWebView *web_view, | |||
| 16 | + | WebKitNavigationAction *navigation_action, | |||
| 17 | + | gpointer user_data) { | |||
| 18 | + | WebKitURIRequest *request = webkit_navigation_action_get_request(navigation_action); | |||
| 19 | + | const char* uri = webkit_uri_request_get_uri(request); | |||
| 20 | + | ||||
| 21 | + | printf("[webkit-create] Create signal for URI: %s\n", uri ? uri : "NULL"); | |||
| 22 | + | ||||
| 23 | + | unsigned long parent_view_id = (unsigned long)user_data; | |||
| 24 | + | ||||
| 25 | + | // Let Go handler create WebView and return it like Epiphany does | |||
| 26 | + | GtkWidget* new_webview = goHandleCreateWebView(parent_view_id, (char*)uri); | |||
| 27 | + | ||||
| 28 | + | if (new_webview) { | |||
| 29 | + | printf("[webkit-create] Go handler provided WebView - using workspace pane\n"); | |||
| 30 | + | return new_webview; | |||
| 31 | + | } else { | |||
| 32 | + | printf("[webkit-create] Go handler declined - allowing native popup window\n"); | |||
| 33 | + | return NULL; // Let WebKit create native popup window | |||
| 34 | + | } | |||
| 35 | + | } | |||
| 36 | + | ||||
| 37 | + | static void connect_create_signal(WebKitWebView* web_view, unsigned long id) { | |||
| 38 | + | if (!web_view) return; | |||
| 39 | + | g_signal_connect_data(G_OBJECT(web_view), "create", G_CALLBACK(on_create_web_view), (gpointer)id, NULL, 0); | |||
| 40 | + | printf("[webkit-debug] create-web-view callback ENABLED with WebKit-created WebViews\n"); | |||
| 41 | + | } | |||
| 42 | + | */ | |||
| 43 | + | import "C" | |||
| 44 | + | import ( | |||
| 45 | + | "log" | |||
| 46 | + | "unsafe" | |||
| 47 | + | ) | |||
| 48 | + | ||||
| 49 | + | //export goHandleCreateWebView | |||
| 50 | + | func goHandleCreateWebView(parentID C.ulong, curi *C.char) *C.GtkWidget { | |||
| 51 | + | uri := "" | |||
| 52 | + | if curi != nil { | |||
| 53 | + | uri = C.GoString(curi) | |||
| 54 | + | } | |||
| 55 | + | ||||
| 56 | + | log.Printf("[webkit] Create signal bypassed: URI=%s (handled by JavaScript bypass)", uri) | |||
| 57 | + | ||||
| 58 | + | // Always return NULL - window.open is now handled entirely by JavaScript bypass | |||
| 59 | + | // This prevents duplicate pane creation from WebKit's create signal | |||
| 60 | + | return nil | |||
| 61 | + | } | |||
| 62 | + | ||||
| 63 | + | // ConnectCreateSignal connects the create signal to a WebView | |||
| 64 | + | func ConnectCreateSignal(view *WebView) { | |||
| 65 | + | if view == nil || view.native == nil || view.native.view == nil { | |||
| 66 | + | log.Printf("[webkit] ConnectCreateSignal: invalid WebView") | |||
| 67 | + | return | |||
| 68 | + | } | |||
| 69 | + | ||||
| 70 | + | webkitView := (*C.WebKitWebView)(unsafe.Pointer(view.native.view)) | |||
| 71 | + | C.connect_create_signal(webkitView, C.ulong(view.id)) | |||
| 72 | + | } | |||
A pkg/webkit/window_types.go +23 -0
| @@ -0,0 +1,23 @@ | |||
| 1 | + | package webkit | |
| 2 | + | ||
| 3 | + | // WindowType indicates how a new WebView should be treated | |
| 4 | + | type WindowType int | |
| 5 | + | ||
| 6 | + | const ( | |
| 7 | + | // WindowTypeTab represents an independent WebView (future: tab) | |
| 8 | + | WindowTypeTab WindowType = iota | |
| 9 | + | // WindowTypePopup represents a related WebView (shares process/context) | |
| 10 | + | WindowTypePopup | |
| 11 | + | // WindowTypeUnknown indicates type not detected yet | |
| 12 | + | WindowTypeUnknown | |
| 13 | + | ) | |
| 14 | + | ||
| 15 | + | // WindowFeatures describes the window features detected from WebKitWindowProperties | |
| 16 | + | type WindowFeatures struct { | |
| 17 | + | Width int | |
| 18 | + | Height int | |
| 19 | + | ToolbarVisible bool | |
| 20 | + | LocationbarVisible bool | |
| 21 | + | MenubarVisible bool | |
| 22 | + | Resizable bool | |
| 23 | + | } | |
| @@ -0,0 +1,23 @@ | |||||
| 1 | + | package webkit | |||
| 2 | + | ||||
| 3 | + | // WindowType indicates how a new WebView should be treated | |||
| 4 | + | type WindowType int | |||
| 5 | + | ||||
| 6 | + | const ( | |||
| 7 | + | // WindowTypeTab represents an independent WebView (future: tab) | |||
| 8 | + | WindowTypeTab WindowType = iota | |||
| 9 | + | // WindowTypePopup represents a related WebView (shares process/context) | |||
| 10 | + | WindowTypePopup | |||
| 11 | + | // WindowTypeUnknown indicates type not detected yet | |||
| 12 | + | WindowTypeUnknown | |||
| 13 | + | ) | |||
| 14 | + | ||||
| 15 | + | // WindowFeatures describes the window features detected from WebKitWindowProperties | |||
| 16 | + | type WindowFeatures struct { | |||
| 17 | + | Width int | |||
| 18 | + | Height int | |||
| 19 | + | ToolbarVisible bool | |||
| 20 | + | LocationbarVisible bool | |||
| 21 | + | MenubarVisible bool | |||
| 22 | + | Resizable bool | |||
| 23 | + | } | |||
M pkg/webkit/workspace_widgets_cgo.go +8 -0
| @@ -306,6 +306,14 @@ func PanedSetResizeEnd(paned uintptr, resize bool) { | |||
| 306 | 306 | C.gtk_paned_set_resize_end_child((*C.GtkPaned)(unsafe.Pointer(paned)), goBool(resize)) | |
| 307 | 307 | } | |
| 308 | 308 | ||
| 309 | + | // PanedSetPosition sets the pixel position of the divider. | |
| 310 | + | func PanedSetPosition(paned uintptr, pos int) { | |
| 311 | + | if paned == 0 { | |
| 312 | + | return | |
| 313 | + | } | |
| 314 | + | C.gtk_paned_set_position((*C.GtkPaned)(unsafe.Pointer(paned)), C.int(pos)) | |
| 315 | + | } | |
| 316 | + | ||
| 309 | 317 | // WidgetUnparent detaches a widget from its current parent. | |
| 310 | 318 | func WidgetUnparent(widget uintptr) { | |
| 311 | 319 | if widget == 0 { | |
| @@ -306,6 +306,14 @@ func PanedSetResizeEnd(paned uintptr, resize bool) { | |||||
| 306 | C.gtk_paned_set_resize_end_child((*C.GtkPaned)(unsafe.Pointer(paned)), goBool(resize)) | 306 | C.gtk_paned_set_resize_end_child((*C.GtkPaned)(unsafe.Pointer(paned)), goBool(resize)) | ||
| 307 | } | 307 | } | ||
| 308 | 308 | ||||
| 309 | + | // PanedSetPosition sets the pixel position of the divider. | |||
| 310 | + | func PanedSetPosition(paned uintptr, pos int) { | |||
| 311 | + | if paned == 0 { | |||
| 312 | + | return | |||
| 313 | + | } | |||
| 314 | + | C.gtk_paned_set_position((*C.GtkPaned)(unsafe.Pointer(paned)), C.int(pos)) | |||
| 315 | + | } | |||
| 316 | + | ||||
| 309 | // WidgetUnparent detaches a widget from its current parent. | 317 | // WidgetUnparent detaches a widget from its current parent. | ||
| 310 | func WidgetUnparent(widget uintptr) { | 318 | func WidgetUnparent(widget uintptr) { | ||
| 311 | if widget == 0 { | 319 | if widget == 0 { | ||
M pkg/webkit/workspace_widgets_stub.go +3 -0
| @@ -93,6 +93,9 @@ func PanedSetResizeStart(paned uintptr, resize bool) {} | |||
| 93 | 93 | ||
| 94 | 94 | func PanedSetResizeEnd(paned uintptr, resize bool) {} | |
| 95 | 95 | ||
| 96 | + | // PanedSetPosition is a no-op in stub builds. | |
| 97 | + | func PanedSetPosition(paned uintptr, pos int) {} | |
| 98 | + | ||
| 96 | 99 | func WidgetUnparent(widget uintptr) {} | |
| 97 | 100 | ||
| 98 | 101 | func WidgetSetHExpand(widget uintptr, expand bool) {} | |
| @@ -93,6 +93,9 @@ func PanedSetResizeStart(paned uintptr, resize bool) {} | |||||
| 93 | 93 | ||||
| 94 | func PanedSetResizeEnd(paned uintptr, resize bool) {} | 94 | func PanedSetResizeEnd(paned uintptr, resize bool) {} | ||
| 95 | 95 | ||||
| 96 | + | // PanedSetPosition is a no-op in stub builds. | |||
| 97 | + | func PanedSetPosition(paned uintptr, pos int) {} | |||
| 98 | + | ||||
| 96 | func WidgetUnparent(widget uintptr) {} | 99 | func WidgetUnparent(widget uintptr) {} | ||
| 97 | 100 | ||||
| 98 | func WidgetSetHExpand(widget uintptr, expand bool) {} | 101 | func WidgetSetHExpand(widget uintptr, expand bool) {} | ||