feat(workspace): open _blank/gesture links in stacked mode by default
- New config option: workspace.popups.blank_target_behavior (split/stacked/tabbed) - Default behavior: stacked (was split-right) - Applies to: target="_blank" links, Ctrl+click, middle-click - JavaScript popups still use workspace.popups.behavior config - Prevents browser exit when closing parent pane
- commit
- 6d403db5812bd1e23ae44e4eb9c0e023fdb9e052
- author
- bnema · · 2025-11-16 17:50 UTC
- committer
- bnema · · 2025-11-16 19:52 UTC
- parent
- 81e87e8
- signature
- unsigned
6 files · +94 -20
M CHANGELOG.md +1 -0
| @@ -12,6 +12,7 @@ All notable changes to this project will be documented in this file. | |||
| 12 | 12 | - **Configurable default search engine**: Added `default_search_engine` config option with explicit URL template format. DuckDuckGo is now the default (was Google). Users can set any search engine with `%s` placeholder (e.g., `default_search_engine = "https://duckduckgo.com/?q=%s"`). | |
| 13 | 13 | ||
| 14 | 14 | ### Changed | |
| 15 | + | - **Target="_blank" and gesture link behavior**: Links with `target="_blank"`, Ctrl+click, and middle-click now open in stacked panes mode by default instead of split-right. New config option `workspace.popups.blank_target_behavior` (values: "split", "stacked", "tabbed") controls this behavior separately from JavaScript popups. Prevents browser exit when closing parent pane by marking _blank panes as popup-type. | |
| 15 | 16 | - **Config format**: TOML is now the default format (JSON/YAML still supported). Action bindings inverted to `action→keys` structure for maintainability with O(1) runtime lookup. Added comprehensive validation. | |
| 16 | 17 | - **Code cleanup**: Removed dead code files and emojis from GUI modules | |
| 17 | 18 | - **Bridge architecture**: Unified main-world to isolated-world bridge system with comprehensive documentation. Single dispatcher for all window function bridges (omnibox, favorites, toasts). Better separation between CustomEvents (shared between worlds) and window functions (require bridging). | |
| @@ -12,6 +12,7 @@ All notable changes to this project will be documented in this file. | |||||
| 12 | - **Configurable default search engine**: Added `default_search_engine` config option with explicit URL template format. DuckDuckGo is now the default (was Google). Users can set any search engine with `%s` placeholder (e.g., `default_search_engine = "https://duckduckgo.com/?q=%s"`). | 12 | - **Configurable default search engine**: Added `default_search_engine` config option with explicit URL template format. DuckDuckGo is now the default (was Google). Users can set any search engine with `%s` placeholder (e.g., `default_search_engine = "https://duckduckgo.com/?q=%s"`). | ||
| 13 | 13 | ||||
| 14 | ### Changed | 14 | ### Changed | ||
| 15 | + | - **Target="_blank" and gesture link behavior**: Links with `target="_blank"`, Ctrl+click, and middle-click now open in stacked panes mode by default instead of split-right. New config option `workspace.popups.blank_target_behavior` (values: "split", "stacked", "tabbed") controls this behavior separately from JavaScript popups. Prevents browser exit when closing parent pane by marking _blank panes as popup-type. | |||
| 15 | - **Config format**: TOML is now the default format (JSON/YAML still supported). Action bindings inverted to `action→keys` structure for maintainability with O(1) runtime lookup. Added comprehensive validation. | 16 | - **Config format**: TOML is now the default format (JSON/YAML still supported). Action bindings inverted to `action→keys` structure for maintainability with O(1) runtime lookup. Added comprehensive validation. | ||
| 16 | - **Code cleanup**: Removed dead code files and emojis from GUI modules | 17 | - **Code cleanup**: Removed dead code files and emojis from GUI modules | ||
| 17 | - **Bridge architecture**: Unified main-world to isolated-world bridge system with comprehensive documentation. Single dispatcher for all window function bridges (omnibox, favorites, toasts). Better separation between CustomEvents (shared between worlds) and window functions (require bridging). | 18 | - **Bridge architecture**: Unified main-world to isolated-world bridge system with comprehensive documentation. Single dispatcher for all window function bridges (omnibox, favorites, toasts). Better separation between CustomEvents (shared between worlds) and window functions (require bridging). | ||
M internal/app/browser/workspace_manager.go +49 -8
| @@ -463,7 +463,7 @@ func (wm *WorkspaceManager) RegisterNavigationHandler(webView *webkit.WebView) { | |||
| 463 | 463 | return false | |
| 464 | 464 | } | |
| 465 | 465 | ||
| 466 | - | log.Printf("[workspace] Middle-click on link, opening in new pane: %s", linkURL) | |
| 466 | + | log.Printf("[workspace] Middle-click/Ctrl+click on link, opening in new pane: %s", linkURL) | |
| 467 | 467 | ||
| 468 | 468 | // Get the node for this webview | |
| 469 | 469 | node := wm.GetNodeForWebView(webView) | |
| @@ -472,16 +472,57 @@ func (wm *WorkspaceManager) RegisterNavigationHandler(webView *webkit.WebView) { | |||
| 472 | 472 | return false | |
| 473 | 473 | } | |
| 474 | 474 | ||
| 475 | - | // Get split direction from popup placement config or use right as default | |
| 476 | - | direction := "right" | |
| 477 | - | if wm.app != nil && wm.app.config != nil && wm.app.config.Workspace.Popups.Placement != "" { | |
| 478 | - | direction = strings.ToLower(wm.app.config.Workspace.Popups.Placement) | |
| 475 | + | // Get behavior from BlankTargetBehavior config | |
| 476 | + | behavior := "stacked" // Default | |
| 477 | + | if wm.app != nil && wm.app.config != nil && wm.app.config.Workspace.Popups.BlankTargetBehavior != "" { | |
| 478 | + | behavior = strings.ToLower(wm.app.config.Workspace.Popups.BlankTargetBehavior) | |
| 479 | + | } | |
| 480 | + | ||
| 481 | + | var newNode *paneNode | |
| 482 | + | var err error | |
| 483 | + | ||
| 484 | + | switch behavior { | |
| 485 | + | case "stacked": | |
| 486 | + | // Stack the current pane - this also creates a new pane in the stack | |
| 487 | + | log.Printf("[workspace] Using stacked behavior for gesture link") | |
| 488 | + | newNode, err = wm.stackedPaneManager.StackPane(node) | |
| 489 | + | if err != nil { | |
| 490 | + | log.Printf("[workspace] Failed to stack pane for gesture link: %v", err) | |
| 491 | + | return false | |
| 492 | + | } | |
| 493 | + | // newNode is the new pane that was added to the stack | |
| 494 | + | log.Printf("[workspace] StackPane created new pane in stack: %p", newNode) | |
| 495 | + | ||
| 496 | + | case "split": | |
| 497 | + | // Regular split behavior | |
| 498 | + | log.Printf("[workspace] Using split behavior for gesture link") | |
| 499 | + | direction := "right" | |
| 500 | + | if wm.app.config.Workspace.Popups.Placement != "" { | |
| 501 | + | direction = strings.ToLower(wm.app.config.Workspace.Popups.Placement) | |
| 502 | + | } | |
| 503 | + | newNode, err = wm.SplitPane(node, direction) | |
| 504 | + | ||
| 505 | + | case "tabbed": | |
| 506 | + | // Tabbed not yet implemented, fall back to split | |
| 507 | + | log.Printf("[workspace] WARNING: Tabbed behavior not yet implemented for gesture links, falling back to split") | |
| 508 | + | direction := "right" | |
| 509 | + | if wm.app.config.Workspace.Popups.Placement != "" { | |
| 510 | + | direction = strings.ToLower(wm.app.config.Workspace.Popups.Placement) | |
| 511 | + | } | |
| 512 | + | newNode, err = wm.SplitPane(node, direction) | |
| 513 | + | ||
| 514 | + | default: | |
| 515 | + | // Unknown behavior, fall back to split | |
| 516 | + | log.Printf("[workspace] WARNING: Unknown behavior '%s' for gesture links, falling back to split", behavior) | |
| 517 | + | direction := "right" | |
| 518 | + | if wm.app.config.Workspace.Popups.Placement != "" { | |
| 519 | + | direction = strings.ToLower(wm.app.config.Workspace.Popups.Placement) | |
| 520 | + | } | |
| 521 | + | newNode, err = wm.SplitPane(node, direction) | |
| 479 | 522 | } | |
| 480 | 523 | ||
| 481 | - | // Split the pane | |
| 482 | - | newNode, err := wm.SplitPane(node, direction) | |
| 483 | 524 | if err != nil { | |
| 484 | - | log.Printf("[workspace] Failed to split pane for middle-click: %v", err) | |
| 525 | + | log.Printf("[workspace] Failed to handle gesture link: %v", err) | |
| 485 | 526 | return false | |
| 486 | 527 | } | |
| 487 | 528 | ||
| @@ -463,7 +463,7 @@ func (wm *WorkspaceManager) RegisterNavigationHandler(webView *webkit.WebView) { | |||||
| 463 | return false | 463 | return false | ||
| 464 | } | 464 | } | ||
| 465 | 465 | ||||
| 466 | - | log.Printf("[workspace] Middle-click on link, opening in new pane: %s", linkURL) | 466 | + | log.Printf("[workspace] Middle-click/Ctrl+click on link, opening in new pane: %s", linkURL) |
| 467 | 467 | ||||
| 468 | // Get the node for this webview | 468 | // Get the node for this webview | ||
| 469 | node := wm.GetNodeForWebView(webView) | 469 | node := wm.GetNodeForWebView(webView) | ||
| @@ -472,16 +472,57 @@ func (wm *WorkspaceManager) RegisterNavigationHandler(webView *webkit.WebView) { | |||||
| 472 | return false | 472 | return false | ||
| 473 | } | 473 | } | ||
| 474 | 474 | ||||
| 475 | - | // Get split direction from popup placement config or use right as default | 475 | + | // Get behavior from BlankTargetBehavior config |
| 476 | - | direction := "right" | 476 | + | behavior := "stacked" // Default |
| 477 | - | if wm.app != nil && wm.app.config != nil && wm.app.config.Workspace.Popups.Placement != "" { | 477 | + | if wm.app != nil && wm.app.config != nil && wm.app.config.Workspace.Popups.BlankTargetBehavior != "" { |
| 478 | - | direction = strings.ToLower(wm.app.config.Workspace.Popups.Placement) | 478 | + | behavior = strings.ToLower(wm.app.config.Workspace.Popups.BlankTargetBehavior) |
| 479 | + | } | |||
| 480 | + | ||||
| 481 | + | var newNode *paneNode | |||
| 482 | + | var err error | |||
| 483 | + | ||||
| 484 | + | switch behavior { | |||
| 485 | + | case "stacked": | |||
| 486 | + | // Stack the current pane - this also creates a new pane in the stack | |||
| 487 | + | log.Printf("[workspace] Using stacked behavior for gesture link") | |||
| 488 | + | newNode, err = wm.stackedPaneManager.StackPane(node) | |||
| 489 | + | if err != nil { | |||
| 490 | + | log.Printf("[workspace] Failed to stack pane for gesture link: %v", err) | |||
| 491 | + | return false | |||
| 492 | + | } | |||
| 493 | + | // newNode is the new pane that was added to the stack | |||
| 494 | + | log.Printf("[workspace] StackPane created new pane in stack: %p", newNode) | |||
| 495 | + | ||||
| 496 | + | case "split": | |||
| 497 | + | // Regular split behavior | |||
| 498 | + | log.Printf("[workspace] Using split behavior for gesture link") | |||
| 499 | + | direction := "right" | |||
| 500 | + | if wm.app.config.Workspace.Popups.Placement != "" { | |||
| 501 | + | direction = strings.ToLower(wm.app.config.Workspace.Popups.Placement) | |||
| 502 | + | } | |||
| 503 | + | newNode, err = wm.SplitPane(node, direction) | |||
| 504 | + | ||||
| 505 | + | case "tabbed": | |||
| 506 | + | // Tabbed not yet implemented, fall back to split | |||
| 507 | + | log.Printf("[workspace] WARNING: Tabbed behavior not yet implemented for gesture links, falling back to split") | |||
| 508 | + | direction := "right" | |||
| 509 | + | if wm.app.config.Workspace.Popups.Placement != "" { | |||
| 510 | + | direction = strings.ToLower(wm.app.config.Workspace.Popups.Placement) | |||
| 511 | + | } | |||
| 512 | + | newNode, err = wm.SplitPane(node, direction) | |||
| 513 | + | ||||
| 514 | + | default: | |||
| 515 | + | // Unknown behavior, fall back to split | |||
| 516 | + | log.Printf("[workspace] WARNING: Unknown behavior '%s' for gesture links, falling back to split", behavior) | |||
| 517 | + | direction := "right" | |||
| 518 | + | if wm.app.config.Workspace.Popups.Placement != "" { | |||
| 519 | + | direction = strings.ToLower(wm.app.config.Workspace.Popups.Placement) | |||
| 520 | + | } | |||
| 521 | + | newNode, err = wm.SplitPane(node, direction) | |||
| 479 | } | 522 | } | ||
| 480 | 523 | ||||
| 481 | - | // Split the pane | |||
| 482 | - | newNode, err := wm.SplitPane(node, direction) | |||
| 483 | if err != nil { | 524 | if err != nil { | ||
| 484 | - | log.Printf("[workspace] Failed to split pane for middle-click: %v", err) | 525 | + | log.Printf("[workspace] Failed to handle gesture link: %v", err) |
| 485 | return false | 526 | return false | ||
| 486 | } | 527 | } | ||
| 487 | 528 | ||||
M internal/app/browser/workspace_popup.go +37 -6
| @@ -409,9 +409,39 @@ func (wm *WorkspaceManager) handlePopupReadyToShow(popupID uint64) { | |||
| 409 | 409 | ||
| 410 | 410 | log.Printf("[workspace] Created popup pane, now inserting into workspace") | |
| 411 | 411 | ||
| 412 | - | // NOW it's safe to insert into workspace based on configured behavior | |
| 413 | - | behavior := wm.app.config.Workspace.Popups.Behavior | |
| 414 | - | log.Printf("[workspace] Popup behavior: %s", behavior) | |
| 412 | + | // Determine window type from node (set during setupPopupHandling) | |
| 413 | + | node := wm.viewToNode[wrappedView] | |
| 414 | + | windowType := webkit.WindowTypePopup | |
| 415 | + | if node != nil { | |
| 416 | + | windowType = node.windowType | |
| 417 | + | } | |
| 418 | + | ||
| 419 | + | // Select behavior based on window type: | |
| 420 | + | // - WindowTypeTab (_blank links): Use BlankTargetBehavior config | |
| 421 | + | // - WindowTypePopup (JavaScript popups): Use global Behavior config | |
| 422 | + | var behaviorStr string | |
| 423 | + | if windowType == webkit.WindowTypeTab { | |
| 424 | + | behaviorStr = wm.app.config.Workspace.Popups.BlankTargetBehavior | |
| 425 | + | log.Printf("[workspace] WindowTypeTab (_blank link) detected, using BlankTargetBehavior: %s", behaviorStr) | |
| 426 | + | } else { | |
| 427 | + | behaviorStr = string(wm.app.config.Workspace.Popups.Behavior) | |
| 428 | + | log.Printf("[workspace] WindowTypePopup detected, using Behavior: %s", behaviorStr) | |
| 429 | + | } | |
| 430 | + | ||
| 431 | + | // Map string to PopupBehavior enum | |
| 432 | + | var behavior config.PopupBehavior | |
| 433 | + | switch behaviorStr { | |
| 434 | + | case "split": | |
| 435 | + | behavior = config.PopupBehaviorSplit | |
| 436 | + | case "stacked": | |
| 437 | + | behavior = config.PopupBehaviorStacked | |
| 438 | + | case "tabbed": | |
| 439 | + | behavior = config.PopupBehaviorTabbed | |
| 440 | + | default: | |
| 441 | + | behavior = config.PopupBehaviorSplit // Fallback | |
| 442 | + | } | |
| 443 | + | ||
| 444 | + | log.Printf("[workspace] Final behavior: %s", behavior) | |
| 415 | 445 | ||
| 416 | 446 | switch behavior { | |
| 417 | 447 | case config.PopupBehaviorSplit: | |
| @@ -481,12 +511,13 @@ func (wm *WorkspaceManager) handlePopupReadyToShow(popupID uint64) { | |||
| 481 | 511 | } | |
| 482 | 512 | ||
| 483 | 513 | // Configure the popup node | |
| 484 | - | node := wm.viewToNode[wrappedView] | |
| 514 | + | // Note: windowType should already be set correctly from setupPopupHandling | |
| 515 | + | // Don't overwrite it here - preserve WindowTypeTab for _blank links | |
| 516 | + | node = wm.viewToNode[wrappedView] | |
| 485 | 517 | if node != nil { | |
| 486 | - | node.windowType = webkit.WindowTypePopup | |
| 487 | 518 | node.isRelated = true | |
| 488 | 519 | node.parentPane = info.parentNode | |
| 489 | - | node.isPopup = true | |
| 520 | + | node.isPopup = true // Mark as popup to prevent browser exit when parent closes | |
| 490 | 521 | node.autoClose = wm.shouldAutoClose(info.url) | |
| 491 | 522 | node.popupID = popupID // Store popupID for close signal lookup | |
| 492 | 523 | ||
| @@ -409,9 +409,39 @@ func (wm *WorkspaceManager) handlePopupReadyToShow(popupID uint64) { | |||||
| 409 | 409 | ||||
| 410 | log.Printf("[workspace] Created popup pane, now inserting into workspace") | 410 | log.Printf("[workspace] Created popup pane, now inserting into workspace") | ||
| 411 | 411 | ||||
| 412 | - | // NOW it's safe to insert into workspace based on configured behavior | 412 | + | // Determine window type from node (set during setupPopupHandling) |
| 413 | - | behavior := wm.app.config.Workspace.Popups.Behavior | 413 | + | node := wm.viewToNode[wrappedView] |
| 414 | - | log.Printf("[workspace] Popup behavior: %s", behavior) | 414 | + | windowType := webkit.WindowTypePopup |
| 415 | + | if node != nil { | |||
| 416 | + | windowType = node.windowType | |||
| 417 | + | } | |||
| 418 | + | ||||
| 419 | + | // Select behavior based on window type: | |||
| 420 | + | // - WindowTypeTab (_blank links): Use BlankTargetBehavior config | |||
| 421 | + | // - WindowTypePopup (JavaScript popups): Use global Behavior config | |||
| 422 | + | var behaviorStr string | |||
| 423 | + | if windowType == webkit.WindowTypeTab { | |||
| 424 | + | behaviorStr = wm.app.config.Workspace.Popups.BlankTargetBehavior | |||
| 425 | + | log.Printf("[workspace] WindowTypeTab (_blank link) detected, using BlankTargetBehavior: %s", behaviorStr) | |||
| 426 | + | } else { | |||
| 427 | + | behaviorStr = string(wm.app.config.Workspace.Popups.Behavior) | |||
| 428 | + | log.Printf("[workspace] WindowTypePopup detected, using Behavior: %s", behaviorStr) | |||
| 429 | + | } | |||
| 430 | + | ||||
| 431 | + | // Map string to PopupBehavior enum | |||
| 432 | + | var behavior config.PopupBehavior | |||
| 433 | + | switch behaviorStr { | |||
| 434 | + | case "split": | |||
| 435 | + | behavior = config.PopupBehaviorSplit | |||
| 436 | + | case "stacked": | |||
| 437 | + | behavior = config.PopupBehaviorStacked | |||
| 438 | + | case "tabbed": | |||
| 439 | + | behavior = config.PopupBehaviorTabbed | |||
| 440 | + | default: | |||
| 441 | + | behavior = config.PopupBehaviorSplit // Fallback | |||
| 442 | + | } | |||
| 443 | + | ||||
| 444 | + | log.Printf("[workspace] Final behavior: %s", behavior) | |||
| 415 | 445 | ||||
| 416 | switch behavior { | 446 | switch behavior { | ||
| 417 | case config.PopupBehaviorSplit: | 447 | case config.PopupBehaviorSplit: | ||
| @@ -481,12 +511,13 @@ func (wm *WorkspaceManager) handlePopupReadyToShow(popupID uint64) { | |||||
| 481 | } | 511 | } | ||
| 482 | 512 | ||||
| 483 | // Configure the popup node | 513 | // Configure the popup node | ||
| 484 | - | node := wm.viewToNode[wrappedView] | 514 | + | // Note: windowType should already be set correctly from setupPopupHandling |
| 515 | + | // Don't overwrite it here - preserve WindowTypeTab for _blank links | |||
| 516 | + | node = wm.viewToNode[wrappedView] | |||
| 485 | if node != nil { | 517 | if node != nil { | ||
| 486 | - | node.windowType = webkit.WindowTypePopup | |||
| 487 | node.isRelated = true | 518 | node.isRelated = true | ||
| 488 | node.parentPane = info.parentNode | 519 | node.parentPane = info.parentNode | ||
| 489 | - | node.isPopup = true | 520 | + | node.isPopup = true // Mark as popup to prevent browser exit when parent closes |
| 490 | node.autoClose = wm.shouldAutoClose(info.url) | 521 | node.autoClose = wm.shouldAutoClose(info.url) | ||
| 491 | node.popupID = popupID // Store popupID for close signal lookup | 522 | node.popupID = popupID // Store popupID for close signal lookup | ||
| 492 | 523 | ||||
M internal/config/config.go +3 -2
| @@ -282,8 +282,9 @@ type PopupBehaviorConfig struct { | |||
| 282 | 282 | // FollowPaneContext determines if popup placement follows parent pane context | |
| 283 | 283 | FollowPaneContext bool `mapstructure:"follow_pane_context" yaml:"follow_pane_context" json:"follow_pane_context"` | |
| 284 | 284 | ||
| 285 | - | // BlankTargetBehavior determines how to handle window.open(url, "_blank") intents | |
| 286 | - | // Accepted values: "pane" (default) or "tab" (future support) | |
| 285 | + | // BlankTargetBehavior determines how target="_blank" links are opened | |
| 286 | + | // Accepted values: "split", "stacked" (default), "tabbed" | |
| 287 | + | // This is separate from Behavior which controls JavaScript popups | |
| 287 | 288 | BlankTargetBehavior string `mapstructure:"blank_target_behavior" yaml:"blank_target_behavior" json:"blank_target_behavior"` | |
| 288 | 289 | ||
| 289 | 290 | // EnableSmartDetection uses WebKitWindowProperties to detect popup vs tab intents | |
| @@ -282,8 +282,9 @@ type PopupBehaviorConfig struct { | |||||
| 282 | // FollowPaneContext determines if popup placement follows parent pane context | 282 | // FollowPaneContext determines if popup placement follows parent pane context | ||
| 283 | FollowPaneContext bool `mapstructure:"follow_pane_context" yaml:"follow_pane_context" json:"follow_pane_context"` | 283 | FollowPaneContext bool `mapstructure:"follow_pane_context" yaml:"follow_pane_context" json:"follow_pane_context"` | ||
| 284 | 284 | ||||
| 285 | - | // BlankTargetBehavior determines how to handle window.open(url, "_blank") intents | 285 | + | // BlankTargetBehavior determines how target="_blank" links are opened |
| 286 | - | // Accepted values: "pane" (default) or "tab" (future support) | 286 | + | // Accepted values: "split", "stacked" (default), "tabbed" |
| 287 | + | // This is separate from Behavior which controls JavaScript popups | |||
| 287 | BlankTargetBehavior string `mapstructure:"blank_target_behavior" yaml:"blank_target_behavior" json:"blank_target_behavior"` | 288 | BlankTargetBehavior string `mapstructure:"blank_target_behavior" yaml:"blank_target_behavior" json:"blank_target_behavior"` | ||
| 288 | 289 | ||||
| 289 | // EnableSmartDetection uses WebKitWindowProperties to detect popup vs tab intents | 290 | // EnableSmartDetection uses WebKitWindowProperties to detect popup vs tab intents | ||
M internal/config/defaults.go +2 -2
| @@ -166,11 +166,11 @@ func DefaultConfig() *Config { | |||
| 166 | 166 | PreviousTab: "ctrl+shift+tab", | |
| 167 | 167 | }, | |
| 168 | 168 | Popups: PopupBehaviorConfig{ | |
| 169 | - | Behavior: PopupBehaviorSplit, // Default: open popups in split panes | |
| 169 | + | Behavior: PopupBehaviorSplit, // Default: open JavaScript popups in split panes | |
| 170 | 170 | Placement: defaultPopupPlacement, | |
| 171 | 171 | OpenInNewPane: true, | |
| 172 | 172 | FollowPaneContext: true, | |
| 173 | - | BlankTargetBehavior: "pane", // Default to pane, future: "tab" | |
| 173 | + | BlankTargetBehavior: "stacked", // Default: open _blank links in stacked mode | |
| 174 | 174 | EnableSmartDetection: true, // Use WindowProperties to detect popup vs tab | |
| 175 | 175 | OAuthAutoClose: true, // Auto-close OAuth popups on success | |
| 176 | 176 | }, | |
| @@ -166,11 +166,11 @@ func DefaultConfig() *Config { | |||||
| 166 | PreviousTab: "ctrl+shift+tab", | 166 | PreviousTab: "ctrl+shift+tab", | ||
| 167 | }, | 167 | }, | ||
| 168 | Popups: PopupBehaviorConfig{ | 168 | Popups: PopupBehaviorConfig{ | ||
| 169 | - | Behavior: PopupBehaviorSplit, // Default: open popups in split panes | 169 | + | Behavior: PopupBehaviorSplit, // Default: open JavaScript popups in split panes |
| 170 | Placement: defaultPopupPlacement, | 170 | Placement: defaultPopupPlacement, | ||
| 171 | OpenInNewPane: true, | 171 | OpenInNewPane: true, | ||
| 172 | FollowPaneContext: true, | 172 | FollowPaneContext: true, | ||
| 173 | - | BlankTargetBehavior: "pane", // Default to pane, future: "tab" | 173 | + | BlankTargetBehavior: "stacked", // Default: open _blank links in stacked mode |
| 174 | EnableSmartDetection: true, // Use WindowProperties to detect popup vs tab | 174 | EnableSmartDetection: true, // Use WindowProperties to detect popup vs tab | ||
| 175 | OAuthAutoClose: true, // Auto-close OAuth popups on success | 175 | OAuthAutoClose: true, // Auto-close OAuth popups on success | ||
| 176 | }, | 176 | }, | ||
M internal/config/validation.go +2 -2
| @@ -60,10 +60,10 @@ func validateConfig(config *Config) error { | |||
| 60 | 60 | ||
| 61 | 61 | // Validate blank target behavior | |
| 62 | 62 | switch config.Workspace.Popups.BlankTargetBehavior { | |
| 63 | - | case "pane", "tab": | |
| 63 | + | case "split", "stacked", "tabbed": | |
| 64 | 64 | // Valid | |
| 65 | 65 | default: | |
| 66 | - | validationErrors = append(validationErrors, fmt.Sprintf("workspace.popups.blank_target_behavior must be one of: pane, tab (got: %s)", config.Workspace.Popups.BlankTargetBehavior)) | |
| 66 | + | validationErrors = append(validationErrors, fmt.Sprintf("workspace.popups.blank_target_behavior must be one of: split, stacked, tabbed (got: %s)", config.Workspace.Popups.BlankTargetBehavior)) | |
| 67 | 67 | } | |
| 68 | 68 | ||
| 69 | 69 | // Validate workspace styling values | |
| @@ -60,10 +60,10 @@ func validateConfig(config *Config) error { | |||||
| 60 | 60 | ||||
| 61 | // Validate blank target behavior | 61 | // Validate blank target behavior | ||
| 62 | switch config.Workspace.Popups.BlankTargetBehavior { | 62 | switch config.Workspace.Popups.BlankTargetBehavior { | ||
| 63 | - | case "pane", "tab": | 63 | + | case "split", "stacked", "tabbed": |
| 64 | // Valid | 64 | // Valid | ||
| 65 | default: | 65 | default: | ||
| 66 | - | validationErrors = append(validationErrors, fmt.Sprintf("workspace.popups.blank_target_behavior must be one of: pane, tab (got: %s)", config.Workspace.Popups.BlankTargetBehavior)) | 66 | + | validationErrors = append(validationErrors, fmt.Sprintf("workspace.popups.blank_target_behavior must be one of: split, stacked, tabbed (got: %s)", config.Workspace.Popups.BlankTargetBehavior)) |
| 67 | } | 67 | } | ||
| 68 | 68 | ||||
| 69 | // Validate workspace styling values | 69 | // Validate workspace styling values | ||