detached 1896eba

fix(workspace): properly remove pane mode border after splits

Refactored pane mode border implementation from CSS-based to GTK
margin-based approach. The previous CSS method didn't properly remove
the border after pane splits because the root container reference changed.

Changes:
- Use GTK widget margins instead of CSS borders for pane mode indicator
- Save container reference when applying margins to ensure proper cleanup
- Add configurable pane_mode_border_color (defaults to #FFA500 orange)
- Window background color shows through margins to create border effect

The new approach ensures the border is always removed from the correct
container, even after the workspace tree structure changes.
commit
1896eba40b8317aed08913f27f7797ba530774da
author
bamen · · 2025-10-13 17:59 UTC
committer
bamen · · 2025-10-13 17:50 UTC
parent
5675ccb
signature
unsigned
5 files · +80 -33
M internal/app/browser/workspace_css.go +13 -12
@@ -73,10 +73,10 @@ func (wm *WorkspaceManager) generateWorkspaceCSS() string {
7373
7474 activeBorderColor := getActivePaneBorderColor(styling, isDark)
7575
76- // Pane mode border color (brighter/more prominent than active pane border)
77- paneModeColor := "#FFA500" // Orange for pane mode
78- if styling.BorderColor != "" && strings.HasPrefix(styling.BorderColor, "@") {
79- paneModeColor = styling.BorderColor // Use theme variable if configured
76+ // Pane mode border color - use configured value or fallback to orange
77+ paneModeColor := styling.PaneModeBorderColor
78+ if paneModeColor == "" {
79+ paneModeColor = "#FFA500" // Fallback to orange if not configured
8080 }
8181
8282 css := fmt.Sprintf(`window {
@@ -85,15 +85,15 @@ func (wm *WorkspaceManager) generateWorkspaceCSS() string {
8585 margin: 0;
8686 }
8787
88- /* Workspace root container - normal state */
89- paned, box {
90- border: 0px solid transparent;
91- transition: border-width 150ms ease-in-out, border-color 150ms ease-in-out;
88+ /* Pane mode active - change window background to border color */
89+ window.pane-mode-active {
90+ background-color: %s;
9291 }
9392
94- /* Pane mode active - thick orange border around workspace root */
95- .workspace-pane-mode-active {
96- border: 4px solid %s !important;
93+ /* Workspace root container */
94+ paned, box {
95+ background-color: %s;
96+ transition: margin 150ms ease-in-out;
9797 }
9898
9999 /* Base pane styling - subtle border for inactive panes */
@@ -149,7 +149,8 @@ func (wm *WorkspaceManager) generateWorkspaceCSS() string {
149149 /* Collapsed panes are hidden - handled in code via widget visibility */
150150 }`,
151151 windowBackgroundColor, // window background
152- paneModeColor, // workspace-pane-mode-active border color
152+ paneModeColor, // window.pane-mode-active background (border color)
153+ windowBackgroundColor, // paned, box background
153154 inactiveBorderColor, // base pane border color (inactive)
154155 styling.BorderRadius, // base pane border radius
155156 styling.TransitionDuration, // base pane border transition
@@ -73,10 +73,10 @@ func (wm *WorkspaceManager) generateWorkspaceCSS() string {
73 73
74 activeBorderColor := getActivePaneBorderColor(styling, isDark)74 activeBorderColor := getActivePaneBorderColor(styling, isDark)
75 75
76- // Pane mode border color (brighter/more prominent than active pane border)76+ // Pane mode border color - use configured value or fallback to orange
77- paneModeColor := "#FFA500" // Orange for pane mode77+ paneModeColor := styling.PaneModeBorderColor
78- if styling.BorderColor != "" && strings.HasPrefix(styling.BorderColor, "@") {78+ if paneModeColor == "" {
79- paneModeColor = styling.BorderColor // Use theme variable if configured79+ paneModeColor = "#FFA500" // Fallback to orange if not configured
80 }80 }
81 81
82 css := fmt.Sprintf(`window {82 css := fmt.Sprintf(`window {
@@ -85,15 +85,15 @@ func (wm *WorkspaceManager) generateWorkspaceCSS() string {
85 margin: 0;85 margin: 0;
86 }86 }
87 87
88- /* Workspace root container - normal state */88+ /* Pane mode active - change window background to border color */
89- paned, box {89+ window.pane-mode-active {
90- border: 0px solid transparent;90+ background-color: %s;
91- transition: border-width 150ms ease-in-out, border-color 150ms ease-in-out;
92 }91 }
93 92
94- /* Pane mode active - thick orange border around workspace root */93+ /* Workspace root container */
95- .workspace-pane-mode-active {94+ paned, box {
96- border: 4px solid %s !important;95+ background-color: %s;
96+ transition: margin 150ms ease-in-out;
97 }97 }
98 98
99 /* Base pane styling - subtle border for inactive panes */99 /* Base pane styling - subtle border for inactive panes */
@@ -149,7 +149,8 @@ func (wm *WorkspaceManager) generateWorkspaceCSS() string {
149 /* Collapsed panes are hidden - handled in code via widget visibility */149 /* Collapsed panes are hidden - handled in code via widget visibility */
150 }`,150 }`,
151 windowBackgroundColor, // window background151 windowBackgroundColor, // window background
152- paneModeColor, // workspace-pane-mode-active border color152+ paneModeColor, // window.pane-mode-active background (border color)
153+ windowBackgroundColor, // paned, box background
153 inactiveBorderColor, // base pane border color (inactive)154 inactiveBorderColor, // base pane border color (inactive)
154 styling.BorderRadius, // base pane border radius155 styling.BorderRadius, // base pane border radius
155 styling.TransitionDuration, // base pane border transition156 styling.TransitionDuration, // base pane border transition
M internal/app/browser/workspace_manager.go +2 -0
@@ -13,6 +13,7 @@ import (
1313 "github.com/bnema/dumber/internal/app/messaging"
1414 "github.com/bnema/dumber/pkg/webkit"
1515 "github.com/diamondburned/gotk4/pkg/glib/v2"
16+ "github.com/diamondburned/gotk4/pkg/gtk/v4"
1617 )
1718
1819 // WorkspaceManager coordinates Zellij-style pane operations.
@@ -33,6 +34,7 @@ type WorkspaceManager struct {
3334 // Coordination fields for preventing duplicate events
3435 paneModeSource *webkit.WebView // Which webview initiated pane mode
3536 paneModeActivePane *paneNode // Which pane node has pane mode active
37+ paneModeContainer gtk.Widgetter // The container widget that has margin applied
3638 paneMutex sync.Mutex // Protects pane mode state
3739
3840 // Focus throttling fields to prevent infinite loops
@@ -13,6 +13,7 @@ import (
13 "github.com/bnema/dumber/internal/app/messaging"13 "github.com/bnema/dumber/internal/app/messaging"
14 "github.com/bnema/dumber/pkg/webkit"14 "github.com/bnema/dumber/pkg/webkit"
15 "github.com/diamondburned/gotk4/pkg/glib/v2"15 "github.com/diamondburned/gotk4/pkg/glib/v2"
16+ "github.com/diamondburned/gotk4/pkg/gtk/v4"
16 )17 )
17 18
18 // WorkspaceManager coordinates Zellij-style pane operations.19 // WorkspaceManager coordinates Zellij-style pane operations.
@@ -33,6 +34,7 @@ type WorkspaceManager struct {
33 // Coordination fields for preventing duplicate events34 // Coordination fields for preventing duplicate events
34 paneModeSource *webkit.WebView // Which webview initiated pane mode35 paneModeSource *webkit.WebView // Which webview initiated pane mode
35 paneModeActivePane *paneNode // Which pane node has pane mode active36 paneModeActivePane *paneNode // Which pane node has pane mode active
37+ paneModeContainer gtk.Widgetter // The container widget that has margin applied
36 paneMutex sync.Mutex // Protects pane mode state38 paneMutex sync.Mutex // Protects pane mode state
37 39
38 // Focus throttling fields to prevent infinite loops40 // Focus throttling fields to prevent infinite loops
M internal/app/browser/workspace_pane_mode.go +51 -13
@@ -173,28 +173,66 @@ func (wm *WorkspaceManager) dispatchPaneModeEvent(event string, detail string) {
173173 }
174174 }
175175
176-// applyPaneModeBorder adds the pane mode CSS class to the workspace root container
176+// applyPaneModeBorder applies pane mode visual indicator using GTK margins
177177 func (wm *WorkspaceManager) applyPaneModeBorder() {
178- if wm.root == nil || wm.root.container == nil {
178+ if wm.root == nil || wm.root.container == nil || wm.window == nil {
179179 return
180180 }
181181
182- // Add CSS class to root container for Zellij-style border
183- if !webkit.WidgetHasCSSClass(wm.root.container, "workspace-pane-mode-active") {
184- webkit.WidgetAddCSSClass(wm.root.container, "workspace-pane-mode-active")
185- log.Printf("[pane-mode] Applied border to workspace root")
186- }
182+ // Save the container reference so we can remove margins from it later
183+ // (even if wm.root.container changes due to splits)
184+ wm.paneModeContainer = wm.root.container
185+
186+ // Apply 4px margins to root container to create space for the border
187+ webkit.WidgetSetMargin(wm.root.container, 4)
188+
189+ // Add CSS class to window for background color (the "border" color shows in the margin space)
190+ webkit.WidgetAddCSSClass(wm.window.AsWindow(), "pane-mode-active")
191+
192+ // Force resize/allocation to apply margin changes immediately
193+ webkit.WidgetQueueResize(wm.root.container)
194+ webkit.WidgetQueueAllocate(wm.root.container)
195+
196+ // Queue redraw to show changes
197+ webkit.WidgetQueueDraw(wm.window.AsWindow())
198+ webkit.WidgetQueueDraw(wm.root.container)
199+
200+ log.Printf("[pane-mode] Applied border to workspace root using margins (container=%p)", wm.root.container)
187201 }
188202
189-// removePaneModeBorder removes the pane mode CSS class from the workspace root container
203+// removePaneModeBorder removes the pane mode visual indicator
190204 func (wm *WorkspaceManager) removePaneModeBorder() {
191- if wm.root == nil || wm.root.container == nil {
205+ if wm.window == nil {
192206 return
193207 }
194208
195- // Remove CSS class from root container
196- if webkit.WidgetHasCSSClass(wm.root.container, "workspace-pane-mode-active") {
197- webkit.WidgetRemoveCSSClass(wm.root.container, "workspace-pane-mode-active")
198- log.Printf("[pane-mode] Removed border from workspace root")
209+ // Use the saved container reference (the one that had margins applied)
210+ // This is important because wm.root.container may have changed due to splits
211+ container := wm.paneModeContainer
212+ if container == nil {
213+ log.Printf("[pane-mode] Warning: no saved container, trying current root")
214+ if wm.root == nil || wm.root.container == nil {
215+ return
216+ }
217+ container = wm.root.container
199218 }
219+
220+ // Remove margins from the container that had them applied
221+ webkit.WidgetSetMargin(container, 0)
222+
223+ // Remove CSS class from window
224+ webkit.WidgetRemoveCSSClass(wm.window.AsWindow(), "pane-mode-active")
225+
226+ // Force resize/allocation to apply margin changes immediately
227+ webkit.WidgetQueueResize(container)
228+ webkit.WidgetQueueAllocate(container)
229+
230+ // Queue redraw to show changes
231+ webkit.WidgetQueueDraw(wm.window.AsWindow())
232+ webkit.WidgetQueueDraw(container)
233+
234+ // Clear the saved container reference
235+ wm.paneModeContainer = nil
236+
237+ log.Printf("[pane-mode] Removed border from workspace root (container=%p)", container)
200238 }
@@ -173,28 +173,66 @@ func (wm *WorkspaceManager) dispatchPaneModeEvent(event string, detail string) {
173 }173 }
174 }174 }
175 175
176-// applyPaneModeBorder adds the pane mode CSS class to the workspace root container176+// applyPaneModeBorder applies pane mode visual indicator using GTK margins
177 func (wm *WorkspaceManager) applyPaneModeBorder() {177 func (wm *WorkspaceManager) applyPaneModeBorder() {
178- if wm.root == nil || wm.root.container == nil {178+ if wm.root == nil || wm.root.container == nil || wm.window == nil {
179 return179 return
180 }180 }
181 181
182- // Add CSS class to root container for Zellij-style border182+ // Save the container reference so we can remove margins from it later
183- if !webkit.WidgetHasCSSClass(wm.root.container, "workspace-pane-mode-active") {183+ // (even if wm.root.container changes due to splits)
184- webkit.WidgetAddCSSClass(wm.root.container, "workspace-pane-mode-active")184+ wm.paneModeContainer = wm.root.container
185- log.Printf("[pane-mode] Applied border to workspace root")185+
186- }186+ // Apply 4px margins to root container to create space for the border
187+ webkit.WidgetSetMargin(wm.root.container, 4)
188+
189+ // Add CSS class to window for background color (the "border" color shows in the margin space)
190+ webkit.WidgetAddCSSClass(wm.window.AsWindow(), "pane-mode-active")
191+
192+ // Force resize/allocation to apply margin changes immediately
193+ webkit.WidgetQueueResize(wm.root.container)
194+ webkit.WidgetQueueAllocate(wm.root.container)
195+
196+ // Queue redraw to show changes
197+ webkit.WidgetQueueDraw(wm.window.AsWindow())
198+ webkit.WidgetQueueDraw(wm.root.container)
199+
200+ log.Printf("[pane-mode] Applied border to workspace root using margins (container=%p)", wm.root.container)
187 }201 }
188 202
189-// removePaneModeBorder removes the pane mode CSS class from the workspace root container203+// removePaneModeBorder removes the pane mode visual indicator
190 func (wm *WorkspaceManager) removePaneModeBorder() {204 func (wm *WorkspaceManager) removePaneModeBorder() {
191- if wm.root == nil || wm.root.container == nil {205+ if wm.window == nil {
192 return206 return
193 }207 }
194 208
195- // Remove CSS class from root container209+ // Use the saved container reference (the one that had margins applied)
196- if webkit.WidgetHasCSSClass(wm.root.container, "workspace-pane-mode-active") {210+ // This is important because wm.root.container may have changed due to splits
197- webkit.WidgetRemoveCSSClass(wm.root.container, "workspace-pane-mode-active")211+ container := wm.paneModeContainer
198- log.Printf("[pane-mode] Removed border from workspace root")212+ if container == nil {
213+ log.Printf("[pane-mode] Warning: no saved container, trying current root")
214+ if wm.root == nil || wm.root.container == nil {
215+ return
216+ }
217+ container = wm.root.container
199 }218 }
219+
220+ // Remove margins from the container that had them applied
221+ webkit.WidgetSetMargin(container, 0)
222+
223+ // Remove CSS class from window
224+ webkit.WidgetRemoveCSSClass(wm.window.AsWindow(), "pane-mode-active")
225+
226+ // Force resize/allocation to apply margin changes immediately
227+ webkit.WidgetQueueResize(container)
228+ webkit.WidgetQueueAllocate(container)
229+
230+ // Queue redraw to show changes
231+ webkit.WidgetQueueDraw(wm.window.AsWindow())
232+ webkit.WidgetQueueDraw(container)
233+
234+ // Clear the saved container reference
235+ wm.paneModeContainer = nil
236+
237+ log.Printf("[pane-mode] Removed border from workspace root (container=%p)", container)
200 }238 }
M internal/config/config.go +4 -0
@@ -328,6 +328,9 @@ type WorkspaceStylingConfig struct {
328328 BorderWidth int `mapstructure:"border_width" yaml:"border_width" json:"border_width"`
329329 // BorderColor for focused panes (CSS color value or theme variable)
330330 BorderColor string `mapstructure:"border_color" yaml:"border_color" json:"border_color"`
331+ // PaneModeBorderColor for the pane mode indicator border (CSS color value or theme variable)
332+ // Defaults to "#FFA500" (orange) if not set
333+ PaneModeBorderColor string `mapstructure:"pane_mode_border_color" yaml:"pane_mode_border_color" json:"pane_mode_border_color"`
331334 // TransitionDuration in milliseconds for border animations
332335 TransitionDuration int `mapstructure:"transition_duration" yaml:"transition_duration" json:"transition_duration"`
333336 // BorderRadius in pixels for pane border corners
@@ -767,6 +770,7 @@ func (m *Manager) setDefaults() {
767770 m.viper.SetDefault("workspace.popups.oauth_auto_close", defaults.Workspace.Popups.OAuthAutoClose)
768771 m.viper.SetDefault("workspace.styling.border_width", defaults.Workspace.Styling.BorderWidth)
769772 m.viper.SetDefault("workspace.styling.border_color", defaults.Workspace.Styling.BorderColor)
773+ m.viper.SetDefault("workspace.styling.pane_mode_border_color", defaults.Workspace.Styling.PaneModeBorderColor)
770774 m.viper.SetDefault("workspace.styling.transition_duration", defaults.Workspace.Styling.TransitionDuration)
771775 m.viper.SetDefault("workspace.styling.border_radius", defaults.Workspace.Styling.BorderRadius)
772776 }
@@ -328,6 +328,9 @@ type WorkspaceStylingConfig struct {
328 BorderWidth int `mapstructure:"border_width" yaml:"border_width" json:"border_width"`328 BorderWidth int `mapstructure:"border_width" yaml:"border_width" json:"border_width"`
329 // BorderColor for focused panes (CSS color value or theme variable)329 // BorderColor for focused panes (CSS color value or theme variable)
330 BorderColor string `mapstructure:"border_color" yaml:"border_color" json:"border_color"`330 BorderColor string `mapstructure:"border_color" yaml:"border_color" json:"border_color"`
331+ // PaneModeBorderColor for the pane mode indicator border (CSS color value or theme variable)
332+ // Defaults to "#FFA500" (orange) if not set
333+ PaneModeBorderColor string `mapstructure:"pane_mode_border_color" yaml:"pane_mode_border_color" json:"pane_mode_border_color"`
331 // TransitionDuration in milliseconds for border animations334 // TransitionDuration in milliseconds for border animations
332 TransitionDuration int `mapstructure:"transition_duration" yaml:"transition_duration" json:"transition_duration"`335 TransitionDuration int `mapstructure:"transition_duration" yaml:"transition_duration" json:"transition_duration"`
333 // BorderRadius in pixels for pane border corners336 // BorderRadius in pixels for pane border corners
@@ -767,6 +770,7 @@ func (m *Manager) setDefaults() {
767 m.viper.SetDefault("workspace.popups.oauth_auto_close", defaults.Workspace.Popups.OAuthAutoClose)770 m.viper.SetDefault("workspace.popups.oauth_auto_close", defaults.Workspace.Popups.OAuthAutoClose)
768 m.viper.SetDefault("workspace.styling.border_width", defaults.Workspace.Styling.BorderWidth)771 m.viper.SetDefault("workspace.styling.border_width", defaults.Workspace.Styling.BorderWidth)
769 m.viper.SetDefault("workspace.styling.border_color", defaults.Workspace.Styling.BorderColor)772 m.viper.SetDefault("workspace.styling.border_color", defaults.Workspace.Styling.BorderColor)
773+ m.viper.SetDefault("workspace.styling.pane_mode_border_color", defaults.Workspace.Styling.PaneModeBorderColor)
770 m.viper.SetDefault("workspace.styling.transition_duration", defaults.Workspace.Styling.TransitionDuration)774 m.viper.SetDefault("workspace.styling.transition_duration", defaults.Workspace.Styling.TransitionDuration)
771 m.viper.SetDefault("workspace.styling.border_radius", defaults.Workspace.Styling.BorderRadius)775 m.viper.SetDefault("workspace.styling.border_radius", defaults.Workspace.Styling.BorderRadius)
772 }776 }
M internal/config/defaults.go +10 -8
@@ -32,10 +32,11 @@ const (
3232 defaultPopupPlacement = "right"
3333
3434 // Workspace styling defaults
35- defaultBorderWidth = 2
36- defaultBorderColor = "@theme_selected_bg_color"
37- defaultTransitionDuration = 120
38- defaultBorderRadius = 0
35+ defaultBorderWidth = 2
36+ defaultBorderColor = "@theme_selected_bg_color"
37+ defaultPaneModeBorderColor = "#FFA500" // Orange for pane mode indicator
38+ defaultTransitionDuration = 120
39+ defaultBorderRadius = 0
3940 )
4041
4142 // getDefaultLogDir returns the default log directory, falls back to empty string on error
@@ -200,10 +201,11 @@ func DefaultConfig() *Config {
200201 OAuthAutoClose: true, // Auto-close OAuth popups on success
201202 },
202203 Styling: WorkspaceStylingConfig{
203- BorderWidth: defaultBorderWidth,
204- BorderColor: defaultBorderColor,
205- TransitionDuration: defaultTransitionDuration,
206- BorderRadius: defaultBorderRadius,
204+ BorderWidth: defaultBorderWidth,
205+ BorderColor: defaultBorderColor,
206+ PaneModeBorderColor: defaultPaneModeBorderColor,
207+ TransitionDuration: defaultTransitionDuration,
208+ BorderRadius: defaultBorderRadius,
207209 },
208210 },
209211 ContentFilteringWhitelist: []string{
@@ -32,10 +32,11 @@ const (
32 defaultPopupPlacement = "right"32 defaultPopupPlacement = "right"
33 33
34 // Workspace styling defaults34 // Workspace styling defaults
35- defaultBorderWidth = 235+ defaultBorderWidth = 2
36- defaultBorderColor = "@theme_selected_bg_color"36+ defaultBorderColor = "@theme_selected_bg_color"
37- defaultTransitionDuration = 12037+ defaultPaneModeBorderColor = "#FFA500" // Orange for pane mode indicator
38- defaultBorderRadius = 038+ defaultTransitionDuration = 120
39+ defaultBorderRadius = 0
39 )40 )
40 41
41 // getDefaultLogDir returns the default log directory, falls back to empty string on error42 // getDefaultLogDir returns the default log directory, falls back to empty string on error
@@ -200,10 +201,11 @@ func DefaultConfig() *Config {
200 OAuthAutoClose: true, // Auto-close OAuth popups on success201 OAuthAutoClose: true, // Auto-close OAuth popups on success
201 },202 },
202 Styling: WorkspaceStylingConfig{203 Styling: WorkspaceStylingConfig{
203- BorderWidth: defaultBorderWidth,204+ BorderWidth: defaultBorderWidth,
204- BorderColor: defaultBorderColor,205+ BorderColor: defaultBorderColor,
205- TransitionDuration: defaultTransitionDuration,206+ PaneModeBorderColor: defaultPaneModeBorderColor,
206- BorderRadius: defaultBorderRadius,207+ TransitionDuration: defaultTransitionDuration,
208+ BorderRadius: defaultBorderRadius,
207 },209 },
208 },210 },
209 ContentFilteringWhitelist: []string{211 ContentFilteringWhitelist: []string{

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