detached 504ed69

refactor(config): add popup behavior configuration

Add PopupBehavior enum with four modes:
- split: open popups in split pane (default)
- stacked: open popups in stacked pane
- tabbed: open popups as new tab
- windowed: open popups in new window

This provides user control over popup placement while maintaining
native WebKit lifecycle management.
commit
504ed69539d84b4e1517a70efe4d69f23b6b9f16
author
bamen · · 2025-10-12 07:05 UTC
parent
c21700d
signature
unsigned
2 files · +30 -4
M internal/config/config.go +29 -4
@@ -282,18 +282,42 @@ type TabKeyConfig struct {
282282 PreviousTab string `mapstructure:"previous_tab" yaml:"previous_tab" json:"previous_tab"`
283283 }
284284
285+// PopupBehavior defines how popup windows should be opened
286+type PopupBehavior string
287+
288+const (
289+ // PopupBehaviorSplit opens popups in a split pane (default)
290+ PopupBehaviorSplit PopupBehavior = "split"
291+ // PopupBehaviorStacked opens popups in a stacked pane
292+ PopupBehaviorStacked PopupBehavior = "stacked"
293+ // PopupBehaviorTabbed opens popups as a new tab
294+ PopupBehaviorTabbed PopupBehavior = "tabbed"
295+ // PopupBehaviorWindowed opens popups in a new workspace window
296+ PopupBehaviorWindowed PopupBehavior = "windowed"
297+)
298+
285299 // PopupBehaviorConfig defines handling for popup windows.
286300 type PopupBehaviorConfig struct {
287- Placement string `mapstructure:"placement" yaml:"placement" json:"placement"`
288- OpenInNewPane bool `mapstructure:"open_in_new_pane" yaml:"open_in_new_pane" json:"open_in_new_pane"`
289- FollowPaneContext bool `mapstructure:"follow_pane_context" yaml:"follow_pane_context" json:"follow_pane_context"`
301+ // Behavior determines how popups are opened (split/stacked/tabbed/windowed)
302+ Behavior PopupBehavior `mapstructure:"behavior" yaml:"behavior" json:"behavior"`
303+
304+ // Placement specifies direction for split behavior ("right", "left", "top", "bottom")
305+ // Only used when Behavior is "split"
306+ Placement string `mapstructure:"placement" yaml:"placement" json:"placement"`
307+
308+ // OpenInNewPane controls whether popups are opened in workspace or blocked
309+ OpenInNewPane bool `mapstructure:"open_in_new_pane" yaml:"open_in_new_pane" json:"open_in_new_pane"`
310+
311+ // FollowPaneContext determines if popup placement follows parent pane context
312+ FollowPaneContext bool `mapstructure:"follow_pane_context" yaml:"follow_pane_context" json:"follow_pane_context"`
290313
291- // New fields for distinguishing window types and behaviors
292314 // BlankTargetBehavior determines how to handle window.open(url, "_blank") intents
293315 // Accepted values: "pane" (default) or "tab" (future support)
294316 BlankTargetBehavior string `mapstructure:"blank_target_behavior" yaml:"blank_target_behavior" json:"blank_target_behavior"`
317+
295318 // EnableSmartDetection uses WebKitWindowProperties to detect popup vs tab intents
296319 EnableSmartDetection bool `mapstructure:"enable_smart_detection" yaml:"enable_smart_detection" json:"enable_smart_detection"`
320+
297321 // OAuthAutoClose enables auto-closing OAuth popups after successful auth redirects
298322 OAuthAutoClose bool `mapstructure:"oauth_auto_close" yaml:"oauth_auto_close" json:"oauth_auto_close"`
299323 }
@@ -733,6 +757,7 @@ func (m *Manager) setDefaults() {
733757 m.viper.SetDefault("workspace.tabs.close_tab", defaults.Workspace.Tabs.CloseTab)
734758 m.viper.SetDefault("workspace.tabs.next_tab", defaults.Workspace.Tabs.NextTab)
735759 m.viper.SetDefault("workspace.tabs.previous_tab", defaults.Workspace.Tabs.PreviousTab)
760+ m.viper.SetDefault("workspace.popups.behavior", string(defaults.Workspace.Popups.Behavior))
736761 m.viper.SetDefault("workspace.popups.placement", defaults.Workspace.Popups.Placement)
737762 m.viper.SetDefault("workspace.popups.open_in_new_pane", defaults.Workspace.Popups.OpenInNewPane)
738763 m.viper.SetDefault("workspace.popups.follow_pane_context", defaults.Workspace.Popups.FollowPaneContext)
@@ -282,18 +282,42 @@ type TabKeyConfig struct {
282 PreviousTab string `mapstructure:"previous_tab" yaml:"previous_tab" json:"previous_tab"`282 PreviousTab string `mapstructure:"previous_tab" yaml:"previous_tab" json:"previous_tab"`
283 }283 }
284 284
285+// PopupBehavior defines how popup windows should be opened
286+type PopupBehavior string
287+
288+const (
289+ // PopupBehaviorSplit opens popups in a split pane (default)
290+ PopupBehaviorSplit PopupBehavior = "split"
291+ // PopupBehaviorStacked opens popups in a stacked pane
292+ PopupBehaviorStacked PopupBehavior = "stacked"
293+ // PopupBehaviorTabbed opens popups as a new tab
294+ PopupBehaviorTabbed PopupBehavior = "tabbed"
295+ // PopupBehaviorWindowed opens popups in a new workspace window
296+ PopupBehaviorWindowed PopupBehavior = "windowed"
297+)
298+
285 // PopupBehaviorConfig defines handling for popup windows.299 // PopupBehaviorConfig defines handling for popup windows.
286 type PopupBehaviorConfig struct {300 type PopupBehaviorConfig struct {
287- Placement string `mapstructure:"placement" yaml:"placement" json:"placement"`301+ // Behavior determines how popups are opened (split/stacked/tabbed/windowed)
288- OpenInNewPane bool `mapstructure:"open_in_new_pane" yaml:"open_in_new_pane" json:"open_in_new_pane"`302+ Behavior PopupBehavior `mapstructure:"behavior" yaml:"behavior" json:"behavior"`
289- FollowPaneContext bool `mapstructure:"follow_pane_context" yaml:"follow_pane_context" json:"follow_pane_context"`303+
304+ // Placement specifies direction for split behavior ("right", "left", "top", "bottom")
305+ // Only used when Behavior is "split"
306+ Placement string `mapstructure:"placement" yaml:"placement" json:"placement"`
307+
308+ // OpenInNewPane controls whether popups are opened in workspace or blocked
309+ OpenInNewPane bool `mapstructure:"open_in_new_pane" yaml:"open_in_new_pane" json:"open_in_new_pane"`
310+
311+ // FollowPaneContext determines if popup placement follows parent pane context
312+ FollowPaneContext bool `mapstructure:"follow_pane_context" yaml:"follow_pane_context" json:"follow_pane_context"`
290 313
291- // New fields for distinguishing window types and behaviors
292 // BlankTargetBehavior determines how to handle window.open(url, "_blank") intents314 // BlankTargetBehavior determines how to handle window.open(url, "_blank") intents
293 // Accepted values: "pane" (default) or "tab" (future support)315 // Accepted values: "pane" (default) or "tab" (future support)
294 BlankTargetBehavior string `mapstructure:"blank_target_behavior" yaml:"blank_target_behavior" json:"blank_target_behavior"`316 BlankTargetBehavior string `mapstructure:"blank_target_behavior" yaml:"blank_target_behavior" json:"blank_target_behavior"`
317+
295 // EnableSmartDetection uses WebKitWindowProperties to detect popup vs tab intents318 // EnableSmartDetection uses WebKitWindowProperties to detect popup vs tab intents
296 EnableSmartDetection bool `mapstructure:"enable_smart_detection" yaml:"enable_smart_detection" json:"enable_smart_detection"`319 EnableSmartDetection bool `mapstructure:"enable_smart_detection" yaml:"enable_smart_detection" json:"enable_smart_detection"`
320+
297 // OAuthAutoClose enables auto-closing OAuth popups after successful auth redirects321 // OAuthAutoClose enables auto-closing OAuth popups after successful auth redirects
298 OAuthAutoClose bool `mapstructure:"oauth_auto_close" yaml:"oauth_auto_close" json:"oauth_auto_close"`322 OAuthAutoClose bool `mapstructure:"oauth_auto_close" yaml:"oauth_auto_close" json:"oauth_auto_close"`
299 }323 }
@@ -733,6 +757,7 @@ func (m *Manager) setDefaults() {
733 m.viper.SetDefault("workspace.tabs.close_tab", defaults.Workspace.Tabs.CloseTab)757 m.viper.SetDefault("workspace.tabs.close_tab", defaults.Workspace.Tabs.CloseTab)
734 m.viper.SetDefault("workspace.tabs.next_tab", defaults.Workspace.Tabs.NextTab)758 m.viper.SetDefault("workspace.tabs.next_tab", defaults.Workspace.Tabs.NextTab)
735 m.viper.SetDefault("workspace.tabs.previous_tab", defaults.Workspace.Tabs.PreviousTab)759 m.viper.SetDefault("workspace.tabs.previous_tab", defaults.Workspace.Tabs.PreviousTab)
760+ m.viper.SetDefault("workspace.popups.behavior", string(defaults.Workspace.Popups.Behavior))
736 m.viper.SetDefault("workspace.popups.placement", defaults.Workspace.Popups.Placement)761 m.viper.SetDefault("workspace.popups.placement", defaults.Workspace.Popups.Placement)
737 m.viper.SetDefault("workspace.popups.open_in_new_pane", defaults.Workspace.Popups.OpenInNewPane)762 m.viper.SetDefault("workspace.popups.open_in_new_pane", defaults.Workspace.Popups.OpenInNewPane)
738 m.viper.SetDefault("workspace.popups.follow_pane_context", defaults.Workspace.Popups.FollowPaneContext)763 m.viper.SetDefault("workspace.popups.follow_pane_context", defaults.Workspace.Popups.FollowPaneContext)
M internal/config/defaults.go +1 -0
@@ -191,6 +191,7 @@ func DefaultConfig() *Config {
191191 PreviousTab: "cmdorctrl+shift+tab",
192192 },
193193 Popups: PopupBehaviorConfig{
194+ Behavior: PopupBehaviorSplit, // Default: open popups in split panes
194195 Placement: defaultPopupPlacement,
195196 OpenInNewPane: true,
196197 FollowPaneContext: true,
@@ -191,6 +191,7 @@ func DefaultConfig() *Config {
191 PreviousTab: "cmdorctrl+shift+tab",191 PreviousTab: "cmdorctrl+shift+tab",
192 },192 },
193 Popups: PopupBehaviorConfig{193 Popups: PopupBehaviorConfig{
194+ Behavior: PopupBehaviorSplit, // Default: open popups in split panes
194 Placement: defaultPopupPlacement,195 Placement: defaultPopupPlacement,
195 OpenInNewPane: true,196 OpenInNewPane: true,
196 FollowPaneContext: true,197 FollowPaneContext: true,

Keyboard shortcuts

Single-key shortcuts are on. ctrl k and esc work whatever this says.

ctrl k :
go to a section, branch, tag, file or repository
?
this list
j k
move the row cursor down / up
enter
open the row under the cursor; on Files, a directory opens or closes in place
↑ ↓ ← → home end
in the file tree (focus on a row): previous / next row, close / open a directory or go to its parent / first entry, first / last row. They never open a file
l h
on Files: open a directory or a file / close a directory or go up to its parent, then to the parent page. Elsewhere: into the side panel (on a merge request, its files) / back to the content; there j k move between its controls
g g G
first / last row
/
filter the list; on Files, find a file anywhere in the repository (esc clears, enter opens the best match)
b
switch branch or tag
g s g f g c g b g m g i g r g a
tabs: summary, files, commits, branches & tags, merge requests, issues, releases, actions (when available)
g h
home
w
wide mode: use the whole width of the screen, or not (remembered)
y
copy a permalink pinned to the commit
d u x
diff: split / unified / collapse or expand all
e E
diff: the whole file in view / every file, with the lines the hunks hide
n
merge request: next note in the diff
esc
close a dialog, leave a field

Branches 8

Tags 42

↑↓ moveenter openesc close