detached 96a4258

feat(workspace): add configurable styling for pane borders

commit
96a4258455890ee0bd5af97554d98f3d4fc418cd
author
bamen · · 2025-09-19 05:13 UTC
parent
cb15db6
signature
unsigned
3 files · +64 -15
M internal/app/browser/workspace_manager.go +34 -15
@@ -10,6 +10,7 @@ import (
1010 "time"
1111
1212 "github.com/bnema/dumber/internal/app/messaging"
13+ "github.com/bnema/dumber/internal/config"
1314 "github.com/bnema/dumber/pkg/webkit"
1415 )
1516
@@ -47,15 +48,6 @@ type WorkspaceManager struct {
4748 }
4849
4950 const (
50- // TODO : should be defined via Config + Defaults
51- activePaneCSS = `.workspace-pane {
52- box-shadow: inset 0 0 0 0px transparent;
53- transition: box-shadow 150ms ease-in-out;
54- }
55-
56- .workspace-pane-active {
57- box-shadow: inset 0 0 0 2px @theme_selected_bg_color;
58- }`
5951 activePaneClass = "workspace-pane-active"
6052 basePaneClass = "workspace-pane"
6153 multiPaneClass = "workspace-multi-pane"
@@ -281,8 +273,8 @@ func (wm *WorkspaceManager) focusNode(node *paneNode) {
281273 }
282274 }
283275
284- // Remove active class from previous pane only if we have multiple panes
285- if wm.active != nil && wm.active.container != 0 && wm.hasMultiplePanes() {
276+ // Remove active class from previous pane (border stays, just color changes)
277+ if wm.active != nil && wm.active.container != 0 {
286278 webkit.WidgetRemoveCSSClass(wm.active.container, activePaneClass)
287279 }
288280
@@ -312,10 +304,8 @@ func (wm *WorkspaceManager) focusNode(node *paneNode) {
312304 container := node.container
313305 viewWidget := node.pane.webView.Widget()
314306 if container != 0 && container != previousContainer {
315- // Only add active class if we have multiple panes (no border for single pane)
316- if wm.hasMultiplePanes() {
317- webkit.WidgetAddCSSClass(container, activePaneClass)
318- }
307+ // Always add active class (border visibility controlled by CSS multi-pane rules)
308+ webkit.WidgetAddCSSClass(container, activePaneClass)
319309 if !webkit.WidgetIsValid(container) {
320310 log.Printf("[workspace] focus aborted: container invalid widget=%#x", container)
321311 return
@@ -369,10 +359,39 @@ func (wm *WorkspaceManager) focusNode(node *paneNode) {
369359 }
370360 }
371361
362+// generateActivePaneCSS generates the CSS for workspace panes based on config
363+func (wm *WorkspaceManager) generateActivePaneCSS() string {
364+ cfg := config.Get()
365+ styling := cfg.Workspace.Styling
366+
367+ return fmt.Sprintf(`.workspace-pane {
368+ border: %dpx solid transparent;
369+ transition: border-color %dms ease-in-out;
370+ border-radius: %dpx;
371+ }
372+
373+ .workspace-pane.workspace-multi-pane {
374+ border: %dpx solid transparent;
375+ border-radius: %dpx;
376+ }
377+
378+ .workspace-pane.workspace-multi-pane.workspace-pane-active {
379+ border-color: %s;
380+ }`,
381+ styling.BorderWidth,
382+ styling.TransitionDuration,
383+ styling.BorderRadius,
384+ styling.BorderWidth,
385+ styling.BorderRadius,
386+ styling.BorderColor,
387+ )
388+}
389+
372390 func (wm *WorkspaceManager) ensureStyles() {
373391 if wm == nil || wm.cssInitialized {
374392 return
375393 }
394+ activePaneCSS := wm.generateActivePaneCSS()
376395 webkit.AddCSSProvider(activePaneCSS)
377396 wm.cssInitialized = true
378397 }
@@ -10,6 +10,7 @@ import (
10 "time"10 "time"
11 11
12 "github.com/bnema/dumber/internal/app/messaging"12 "github.com/bnema/dumber/internal/app/messaging"
13+ "github.com/bnema/dumber/internal/config"
13 "github.com/bnema/dumber/pkg/webkit"14 "github.com/bnema/dumber/pkg/webkit"
14 )15 )
15 16
@@ -47,15 +48,6 @@ type WorkspaceManager struct {
47 }48 }
48 49
49 const (50 const (
50- // TODO : should be defined via Config + Defaults
51- activePaneCSS = `.workspace-pane {
52- box-shadow: inset 0 0 0 0px transparent;
53- transition: box-shadow 150ms ease-in-out;
54- }
55-
56- .workspace-pane-active {
57- box-shadow: inset 0 0 0 2px @theme_selected_bg_color;
58- }`
59 activePaneClass = "workspace-pane-active"51 activePaneClass = "workspace-pane-active"
60 basePaneClass = "workspace-pane"52 basePaneClass = "workspace-pane"
61 multiPaneClass = "workspace-multi-pane"53 multiPaneClass = "workspace-multi-pane"
@@ -281,8 +273,8 @@ func (wm *WorkspaceManager) focusNode(node *paneNode) {
281 }273 }
282 }274 }
283 275
284- // Remove active class from previous pane only if we have multiple panes276+ // Remove active class from previous pane (border stays, just color changes)
285- if wm.active != nil && wm.active.container != 0 && wm.hasMultiplePanes() {277+ if wm.active != nil && wm.active.container != 0 {
286 webkit.WidgetRemoveCSSClass(wm.active.container, activePaneClass)278 webkit.WidgetRemoveCSSClass(wm.active.container, activePaneClass)
287 }279 }
288 280
@@ -312,10 +304,8 @@ func (wm *WorkspaceManager) focusNode(node *paneNode) {
312 container := node.container304 container := node.container
313 viewWidget := node.pane.webView.Widget()305 viewWidget := node.pane.webView.Widget()
314 if container != 0 && container != previousContainer {306 if container != 0 && container != previousContainer {
315- // Only add active class if we have multiple panes (no border for single pane)307+ // Always add active class (border visibility controlled by CSS multi-pane rules)
316- if wm.hasMultiplePanes() {308+ webkit.WidgetAddCSSClass(container, activePaneClass)
317- webkit.WidgetAddCSSClass(container, activePaneClass)
318- }
319 if !webkit.WidgetIsValid(container) {309 if !webkit.WidgetIsValid(container) {
320 log.Printf("[workspace] focus aborted: container invalid widget=%#x", container)310 log.Printf("[workspace] focus aborted: container invalid widget=%#x", container)
321 return311 return
@@ -369,10 +359,39 @@ func (wm *WorkspaceManager) focusNode(node *paneNode) {
369 }359 }
370 }360 }
371 361
362+// generateActivePaneCSS generates the CSS for workspace panes based on config
363+func (wm *WorkspaceManager) generateActivePaneCSS() string {
364+ cfg := config.Get()
365+ styling := cfg.Workspace.Styling
366+
367+ return fmt.Sprintf(`.workspace-pane {
368+ border: %dpx solid transparent;
369+ transition: border-color %dms ease-in-out;
370+ border-radius: %dpx;
371+ }
372+
373+ .workspace-pane.workspace-multi-pane {
374+ border: %dpx solid transparent;
375+ border-radius: %dpx;
376+ }
377+
378+ .workspace-pane.workspace-multi-pane.workspace-pane-active {
379+ border-color: %s;
380+ }`,
381+ styling.BorderWidth,
382+ styling.TransitionDuration,
383+ styling.BorderRadius,
384+ styling.BorderWidth,
385+ styling.BorderRadius,
386+ styling.BorderColor,
387+ )
388+}
389+
372 func (wm *WorkspaceManager) ensureStyles() {390 func (wm *WorkspaceManager) ensureStyles() {
373 if wm == nil || wm.cssInitialized {391 if wm == nil || wm.cssInitialized {
374 return392 return
375 }393 }
394+ activePaneCSS := wm.generateActivePaneCSS()
376 webkit.AddCSSProvider(activePaneCSS)395 webkit.AddCSSProvider(activePaneCSS)
377 wm.cssInitialized = true396 wm.cssInitialized = true
378 }397 }
M internal/config/config.go +18 -0
@@ -234,6 +234,8 @@ type WorkspaceConfig struct {
234234 Tabs TabKeyConfig `mapstructure:"tabs" yaml:"tabs" json:"tabs"`
235235 // Popups configures default popup placement rules.
236236 Popups PopupBehaviorConfig `mapstructure:"popups" yaml:"popups" json:"popups"`
237+ // Styling configures workspace visual appearance.
238+ Styling WorkspaceStylingConfig `mapstructure:"styling" yaml:"styling" json:"styling"`
237239 }
238240
239241 // PaneModeConfig defines modal behaviour for pane management.
@@ -258,6 +260,18 @@ type PopupBehaviorConfig struct {
258260 FollowPaneContext bool `mapstructure:"follow_pane_context" yaml:"follow_pane_context" json:"follow_pane_context"`
259261 }
260262
263+// WorkspaceStylingConfig defines visual styling for workspace panes.
264+type WorkspaceStylingConfig struct {
265+ // BorderWidth in pixels for pane borders
266+ BorderWidth int `mapstructure:"border_width" yaml:"border_width" json:"border_width"`
267+ // BorderColor for focused panes (CSS color value or theme variable)
268+ BorderColor string `mapstructure:"border_color" yaml:"border_color" json:"border_color"`
269+ // TransitionDuration in milliseconds for border animations
270+ TransitionDuration int `mapstructure:"transition_duration" yaml:"transition_duration" json:"transition_duration"`
271+ // BorderRadius in pixels for pane border corners
272+ BorderRadius int `mapstructure:"border_radius" yaml:"border_radius" json:"border_radius"`
273+}
274+
261275 // Manager handles configuration loading, watching, and reloading.
262276 type Manager struct {
263277 config *Config
@@ -664,6 +678,10 @@ func (m *Manager) setDefaults() {
664678 m.viper.SetDefault("workspace.popups.placement", defaults.Workspace.Popups.Placement)
665679 m.viper.SetDefault("workspace.popups.open_in_new_pane", defaults.Workspace.Popups.OpenInNewPane)
666680 m.viper.SetDefault("workspace.popups.follow_pane_context", defaults.Workspace.Popups.FollowPaneContext)
681+ m.viper.SetDefault("workspace.styling.border_width", defaults.Workspace.Styling.BorderWidth)
682+ m.viper.SetDefault("workspace.styling.border_color", defaults.Workspace.Styling.BorderColor)
683+ m.viper.SetDefault("workspace.styling.transition_duration", defaults.Workspace.Styling.TransitionDuration)
684+ m.viper.SetDefault("workspace.styling.border_radius", defaults.Workspace.Styling.BorderRadius)
667685 }
668686
669687 func (m *Manager) ensurePersistedDefaults(cfg *Config) {
@@ -234,6 +234,8 @@ type WorkspaceConfig struct {
234 Tabs TabKeyConfig `mapstructure:"tabs" yaml:"tabs" json:"tabs"`234 Tabs TabKeyConfig `mapstructure:"tabs" yaml:"tabs" json:"tabs"`
235 // Popups configures default popup placement rules.235 // Popups configures default popup placement rules.
236 Popups PopupBehaviorConfig `mapstructure:"popups" yaml:"popups" json:"popups"`236 Popups PopupBehaviorConfig `mapstructure:"popups" yaml:"popups" json:"popups"`
237+ // Styling configures workspace visual appearance.
238+ Styling WorkspaceStylingConfig `mapstructure:"styling" yaml:"styling" json:"styling"`
237 }239 }
238 240
239 // PaneModeConfig defines modal behaviour for pane management.241 // PaneModeConfig defines modal behaviour for pane management.
@@ -258,6 +260,18 @@ type PopupBehaviorConfig struct {
258 FollowPaneContext bool `mapstructure:"follow_pane_context" yaml:"follow_pane_context" json:"follow_pane_context"`260 FollowPaneContext bool `mapstructure:"follow_pane_context" yaml:"follow_pane_context" json:"follow_pane_context"`
259 }261 }
260 262
263+// WorkspaceStylingConfig defines visual styling for workspace panes.
264+type WorkspaceStylingConfig struct {
265+ // BorderWidth in pixels for pane borders
266+ BorderWidth int `mapstructure:"border_width" yaml:"border_width" json:"border_width"`
267+ // BorderColor for focused panes (CSS color value or theme variable)
268+ BorderColor string `mapstructure:"border_color" yaml:"border_color" json:"border_color"`
269+ // TransitionDuration in milliseconds for border animations
270+ TransitionDuration int `mapstructure:"transition_duration" yaml:"transition_duration" json:"transition_duration"`
271+ // BorderRadius in pixels for pane border corners
272+ BorderRadius int `mapstructure:"border_radius" yaml:"border_radius" json:"border_radius"`
273+}
274+
261 // Manager handles configuration loading, watching, and reloading.275 // Manager handles configuration loading, watching, and reloading.
262 type Manager struct {276 type Manager struct {
263 config *Config277 config *Config
@@ -664,6 +678,10 @@ func (m *Manager) setDefaults() {
664 m.viper.SetDefault("workspace.popups.placement", defaults.Workspace.Popups.Placement)678 m.viper.SetDefault("workspace.popups.placement", defaults.Workspace.Popups.Placement)
665 m.viper.SetDefault("workspace.popups.open_in_new_pane", defaults.Workspace.Popups.OpenInNewPane)679 m.viper.SetDefault("workspace.popups.open_in_new_pane", defaults.Workspace.Popups.OpenInNewPane)
666 m.viper.SetDefault("workspace.popups.follow_pane_context", defaults.Workspace.Popups.FollowPaneContext)680 m.viper.SetDefault("workspace.popups.follow_pane_context", defaults.Workspace.Popups.FollowPaneContext)
681+ m.viper.SetDefault("workspace.styling.border_width", defaults.Workspace.Styling.BorderWidth)
682+ m.viper.SetDefault("workspace.styling.border_color", defaults.Workspace.Styling.BorderColor)
683+ m.viper.SetDefault("workspace.styling.transition_duration", defaults.Workspace.Styling.TransitionDuration)
684+ m.viper.SetDefault("workspace.styling.border_radius", defaults.Workspace.Styling.BorderRadius)
667 }685 }
668 686
669 func (m *Manager) ensurePersistedDefaults(cfg *Config) {687 func (m *Manager) ensurePersistedDefaults(cfg *Config) {
M internal/config/defaults.go +12 -0
@@ -30,6 +30,12 @@ const (
3030 defaultPaneActivationShortcut = "cmdorctrl+p"
3131 defaultPaneTimeoutMilliseconds = 3000
3232 defaultPopupPlacement = "right"
33+
34+ // Workspace styling defaults
35+ defaultBorderWidth = 2
36+ defaultBorderColor = "@theme_selected_bg_color"
37+ defaultTransitionDuration = 120
38+ defaultBorderRadius = 0
3339 )
3440
3541 // getDefaultLogDir returns the default log directory, falls back to empty string on error
@@ -181,6 +187,12 @@ func DefaultConfig() *Config {
181187 OpenInNewPane: true,
182188 FollowPaneContext: true,
183189 },
190+ Styling: WorkspaceStylingConfig{
191+ BorderWidth: defaultBorderWidth,
192+ BorderColor: defaultBorderColor,
193+ TransitionDuration: defaultTransitionDuration,
194+ BorderRadius: defaultBorderRadius,
195+ },
184196 },
185197 }
186198 }
@@ -30,6 +30,12 @@ const (
30 defaultPaneActivationShortcut = "cmdorctrl+p"30 defaultPaneActivationShortcut = "cmdorctrl+p"
31 defaultPaneTimeoutMilliseconds = 300031 defaultPaneTimeoutMilliseconds = 3000
32 defaultPopupPlacement = "right"32 defaultPopupPlacement = "right"
33+
34+ // Workspace styling defaults
35+ defaultBorderWidth = 2
36+ defaultBorderColor = "@theme_selected_bg_color"
37+ defaultTransitionDuration = 120
38+ defaultBorderRadius = 0
33 )39 )
34 40
35 // getDefaultLogDir returns the default log directory, falls back to empty string on error41 // getDefaultLogDir returns the default log directory, falls back to empty string on error
@@ -181,6 +187,12 @@ func DefaultConfig() *Config {
181 OpenInNewPane: true,187 OpenInNewPane: true,
182 FollowPaneContext: true,188 FollowPaneContext: true,
183 },189 },
190+ Styling: WorkspaceStylingConfig{
191+ BorderWidth: defaultBorderWidth,
192+ BorderColor: defaultBorderColor,
193+ TransitionDuration: defaultTransitionDuration,
194+ BorderRadius: defaultBorderRadius,
195+ },
184 },196 },
185 }197 }
186 }198 }

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