detached 7c100fb

fix(browser): eliminate viewport flicker and GTK CSS errors in active pane borders

- Replace border-based approach with outline for zero layout impact
- Fix CSS class management to prevent GTK counting bloom filter errors
- Use outline with -2px offset for clean visual focus indication
- Add safety checks for CSS class existence before removal
- Eliminate viewport width flickering when switching between pane types
commit
7c100fb7dc2ae5780a3fc0e07bc96b6d21290110
author
Brice · · 2025-09-27 21:33 UTC
parent
07cfeef
signature
unsigned
2 files · +196 -8
M internal/app/browser/workspace_css.go +46 -8
@@ -3,7 +3,6 @@ package browser
33
44 import (
55 "fmt"
6- "log"
76
87 "github.com/bnema/dumber/internal/config"
98 "github.com/bnema/dumber/pkg/webkit"
@@ -35,12 +34,20 @@ func getStackTitleTextColor(isDark bool) string {
3534 return "#333333"
3635 }
3736
38-// generateStackedPaneCSS generates the CSS for stacked pane title bars
39-func (wm *WorkspaceManager) generateStackedPaneCSS() string {
37+// getActivePaneBorderColor returns the border color for active panes based on theme
38+func getActivePaneBorderColor(isDark bool) string {
39+ if isDark {
40+ return "#4A90E2" // Deeper blue pastel for dark theme
41+ }
42+ return "#87CEEB" // Sky blue pastel for light theme
43+}
44+
45+// generateWorkspaceCSS generates the complete CSS for workspace panes and stacked panes
46+func (wm *WorkspaceManager) generateWorkspaceCSS() string {
4047 cfg := config.Get()
4148 styling := cfg.Workspace.Styling
4249
43- // Get appropriate border colors based on GTK theme preference
50+ // Get appropriate colors based on GTK theme preference
4451 var inactiveBorderColor, windowBackgroundColor string
4552 isDark := webkit.PrefersDarkTheme()
4653 if isDark {
@@ -51,16 +58,32 @@ func (wm *WorkspaceManager) generateStackedPaneCSS() string {
5158 windowBackgroundColor = "#ffffff" // Light window background
5259 }
5360
61+ activeBorderColor := getActivePaneBorderColor(isDark)
62+
5463 css := fmt.Sprintf(`window {
5564 background-color: %s;
5665 }
5766
67+ /* Active pane border styling using outline (no layout impact) */
68+ .workspace-pane-active {
69+ outline: 2px solid %s;
70+ outline-offset: -2px;
71+ transition: outline-color %dms ease-in-out;
72+ }
73+
5874 /* Stacked panes styling */
5975 .stacked-pane-container {
6076 background-color: %s;
6177 border-radius: %dpx;
6278 }
6379
80+ /* Active stacked pane container gets the outline */
81+ .stacked-pane-container.workspace-pane-active {
82+ outline: 2px solid %s;
83+ outline-offset: -2px;
84+ transition: outline-color %dms ease-in-out;
85+ }
86+
6487 .stacked-pane-title {
6588 background-color: %s;
6689 border-bottom: 1px solid %s;
@@ -87,8 +110,12 @@ func (wm *WorkspaceManager) generateStackedPaneCSS() string {
87110 /* Collapsed panes are hidden - handled in code via widget visibility */
88111 }`,
89112 windowBackgroundColor, // window background
113+ activeBorderColor, // workspace-pane-active border
114+ styling.TransitionDuration, // workspace-pane-active transition
90115 windowBackgroundColor, // stacked-pane-container background
91116 styling.BorderRadius, // stacked-pane-container border-radius
117+ activeBorderColor, // stacked-pane-container.workspace-pane-active border
118+ styling.TransitionDuration, // stacked-pane-container.workspace-pane-active transition
92119 getStackTitleBg(isDark), // stacked-pane-title background
93120 inactiveBorderColor, // stacked-pane-title border-bottom
94121 styling.TransitionDuration, // stacked-pane-title transition
@@ -99,12 +126,23 @@ func (wm *WorkspaceManager) generateStackedPaneCSS() string {
99126 return css
100127 }
101128
102-// ensureStackedPaneStyles ensures that CSS styles are applied for stacked panes
103-func (wm *WorkspaceManager) ensureStackedPaneStyles() {
129+// generateStackedPaneCSS generates the CSS for stacked pane title bars (deprecated - use generateWorkspaceCSS)
130+func (wm *WorkspaceManager) generateStackedPaneCSS() string {
131+ // Delegate to the new comprehensive CSS generator
132+ return wm.generateWorkspaceCSS()
133+}
134+
135+// ensureWorkspaceStyles ensures that CSS styles are applied for workspace panes
136+func (wm *WorkspaceManager) ensureWorkspaceStyles() {
104137 if wm == nil || wm.cssInitialized {
105138 return
106139 }
107- stackedPaneCSS := wm.generateStackedPaneCSS()
108- webkit.AddCSSProvider(stackedPaneCSS)
140+ workspaceCSS := wm.generateWorkspaceCSS()
141+ webkit.AddCSSProvider(workspaceCSS)
109142 wm.cssInitialized = true
110143 }
144+
145+// ensureStackedPaneStyles ensures that CSS styles are applied for stacked panes (deprecated - use ensureWorkspaceStyles)
146+func (wm *WorkspaceManager) ensureStackedPaneStyles() {
147+ wm.ensureWorkspaceStyles()
148+}
@@ -3,7 +3,6 @@ package browser
3 3
4 import (4 import (
5 "fmt"5 "fmt"
6- "log"
7 6
8 "github.com/bnema/dumber/internal/config"7 "github.com/bnema/dumber/internal/config"
9 "github.com/bnema/dumber/pkg/webkit"8 "github.com/bnema/dumber/pkg/webkit"
@@ -35,12 +34,20 @@ func getStackTitleTextColor(isDark bool) string {
35 return "#333333"34 return "#333333"
36 }35 }
37 36
38-// generateStackedPaneCSS generates the CSS for stacked pane title bars37+// getActivePaneBorderColor returns the border color for active panes based on theme
39-func (wm *WorkspaceManager) generateStackedPaneCSS() string {38+func getActivePaneBorderColor(isDark bool) string {
39+ if isDark {
40+ return "#4A90E2" // Deeper blue pastel for dark theme
41+ }
42+ return "#87CEEB" // Sky blue pastel for light theme
43+}
44+
45+// generateWorkspaceCSS generates the complete CSS for workspace panes and stacked panes
46+func (wm *WorkspaceManager) generateWorkspaceCSS() string {
40 cfg := config.Get()47 cfg := config.Get()
41 styling := cfg.Workspace.Styling48 styling := cfg.Workspace.Styling
42 49
43- // Get appropriate border colors based on GTK theme preference50+ // Get appropriate colors based on GTK theme preference
44 var inactiveBorderColor, windowBackgroundColor string51 var inactiveBorderColor, windowBackgroundColor string
45 isDark := webkit.PrefersDarkTheme()52 isDark := webkit.PrefersDarkTheme()
46 if isDark {53 if isDark {
@@ -51,16 +58,32 @@ func (wm *WorkspaceManager) generateStackedPaneCSS() string {
51 windowBackgroundColor = "#ffffff" // Light window background58 windowBackgroundColor = "#ffffff" // Light window background
52 }59 }
53 60
61+ activeBorderColor := getActivePaneBorderColor(isDark)
62+
54 css := fmt.Sprintf(`window {63 css := fmt.Sprintf(`window {
55 background-color: %s;64 background-color: %s;
56 }65 }
57 66
67+ /* Active pane border styling using outline (no layout impact) */
68+ .workspace-pane-active {
69+ outline: 2px solid %s;
70+ outline-offset: -2px;
71+ transition: outline-color %dms ease-in-out;
72+ }
73+
58 /* Stacked panes styling */74 /* Stacked panes styling */
59 .stacked-pane-container {75 .stacked-pane-container {
60 background-color: %s;76 background-color: %s;
61 border-radius: %dpx;77 border-radius: %dpx;
62 }78 }
63 79
80+ /* Active stacked pane container gets the outline */
81+ .stacked-pane-container.workspace-pane-active {
82+ outline: 2px solid %s;
83+ outline-offset: -2px;
84+ transition: outline-color %dms ease-in-out;
85+ }
86+
64 .stacked-pane-title {87 .stacked-pane-title {
65 background-color: %s;88 background-color: %s;
66 border-bottom: 1px solid %s;89 border-bottom: 1px solid %s;
@@ -87,8 +110,12 @@ func (wm *WorkspaceManager) generateStackedPaneCSS() string {
87 /* Collapsed panes are hidden - handled in code via widget visibility */110 /* Collapsed panes are hidden - handled in code via widget visibility */
88 }`,111 }`,
89 windowBackgroundColor, // window background112 windowBackgroundColor, // window background
113+ activeBorderColor, // workspace-pane-active border
114+ styling.TransitionDuration, // workspace-pane-active transition
90 windowBackgroundColor, // stacked-pane-container background115 windowBackgroundColor, // stacked-pane-container background
91 styling.BorderRadius, // stacked-pane-container border-radius116 styling.BorderRadius, // stacked-pane-container border-radius
117+ activeBorderColor, // stacked-pane-container.workspace-pane-active border
118+ styling.TransitionDuration, // stacked-pane-container.workspace-pane-active transition
92 getStackTitleBg(isDark), // stacked-pane-title background119 getStackTitleBg(isDark), // stacked-pane-title background
93 inactiveBorderColor, // stacked-pane-title border-bottom120 inactiveBorderColor, // stacked-pane-title border-bottom
94 styling.TransitionDuration, // stacked-pane-title transition121 styling.TransitionDuration, // stacked-pane-title transition
@@ -99,12 +126,23 @@ func (wm *WorkspaceManager) generateStackedPaneCSS() string {
99 return css126 return css
100 }127 }
101 128
102-// ensureStackedPaneStyles ensures that CSS styles are applied for stacked panes129+// generateStackedPaneCSS generates the CSS for stacked pane title bars (deprecated - use generateWorkspaceCSS)
103-func (wm *WorkspaceManager) ensureStackedPaneStyles() {130+func (wm *WorkspaceManager) generateStackedPaneCSS() string {
131+ // Delegate to the new comprehensive CSS generator
132+ return wm.generateWorkspaceCSS()
133+}
134+
135+// ensureWorkspaceStyles ensures that CSS styles are applied for workspace panes
136+func (wm *WorkspaceManager) ensureWorkspaceStyles() {
104 if wm == nil || wm.cssInitialized {137 if wm == nil || wm.cssInitialized {
105 return138 return
106 }139 }
107- stackedPaneCSS := wm.generateStackedPaneCSS()140+ workspaceCSS := wm.generateWorkspaceCSS()
108- webkit.AddCSSProvider(stackedPaneCSS)141+ webkit.AddCSSProvider(workspaceCSS)
109 wm.cssInitialized = true142 wm.cssInitialized = true
110 }143 }
144+
145+// ensureStackedPaneStyles ensures that CSS styles are applied for stacked panes (deprecated - use ensureWorkspaceStyles)
146+func (wm *WorkspaceManager) ensureStackedPaneStyles() {
147+ wm.ensureWorkspaceStyles()
148+}
M internal/app/browser/workspace_utils.go +150 -0
@@ -4,6 +4,7 @@ package browser
44 import (
55 "errors"
66 "fmt"
7+ "log"
78
89 "github.com/bnema/dumber/pkg/webkit"
910 )
@@ -97,6 +98,14 @@ func (wm *WorkspaceManager) initializePaneWidgets(node *paneNode, containerPtr u
9798 // Register the main container
9899 node.container = wm.registerWidget(containerPtr, "pane-container")
99100
101+ // Add base CSS class to the container
102+ if node.container != nil {
103+ node.container.Execute(func(ptr uintptr) error {
104+ webkit.WidgetAddCSSClass(ptr, basePaneClass)
105+ return nil
106+ })
107+ }
108+
100109 // Initialize other widget fields as nil (will be set when needed)
101110 node.titleBar = nil
102111 node.stackWrapper = nil
@@ -142,3 +151,144 @@ func (wm *WorkspaceManager) setTitleBar(node *paneNode, ptr uintptr) {
142151 func (wm *WorkspaceManager) setStackWrapper(node *paneNode, ptr uintptr) {
143152 node.stackWrapper = wm.registerWidget(ptr, "stack-wrapper")
144153 }
154+
155+// Centralized Active Pane Border Management System
156+// Handles all pane types: regular, popup, stacked, split panes
157+
158+// PaneBorderContext holds the context for applying borders to any pane type
159+type PaneBorderContext struct {
160+ webViewWidget uintptr // The WebView's native widget (for margin)
161+ borderContainer *SafeWidget // The container that gets background color
162+ cssClasses []string // Additional CSS classes to apply
163+ paneType string // Description for debugging
164+}
165+
166+// determineBorderContext analyzes a pane node and determines the correct border context
167+func (wm *WorkspaceManager) determineBorderContext(node *paneNode) *PaneBorderContext {
168+ if node == nil {
169+ return nil
170+ }
171+
172+ ctx := &PaneBorderContext{}
173+
174+ // Step 1: Get WebView widget for margin (same for all pane types)
175+ if node.pane != nil && node.pane.webView != nil {
176+ ctx.webViewWidget = node.pane.webView.RootWidget()
177+ }
178+
179+ // Step 2: Determine border container and CSS classes based on pane hierarchy and type
180+ switch {
181+ case node.parent != nil && node.parent.isStacked:
182+ // STACKED PANE: Border goes on the stack container
183+ ctx.borderContainer = node.parent.container
184+ ctx.cssClasses = []string{stackContainerClass, activePaneClass}
185+ ctx.paneType = "stacked"
186+
187+ case node.windowType == webkit.WindowTypePopup:
188+ // POPUP PANE: Border goes on the popup's own container
189+ ctx.borderContainer = node.container
190+ ctx.cssClasses = []string{basePaneClass, activePaneClass}
191+ ctx.paneType = "popup"
192+
193+ case node.isLeaf && !node.isStacked:
194+ // REGULAR LEAF PANE: Border goes on the pane's container
195+ ctx.borderContainer = node.container
196+ ctx.cssClasses = []string{basePaneClass, activePaneClass}
197+ ctx.paneType = "regular-leaf"
198+
199+ case !node.isLeaf:
200+ // SPLIT PANE (branch node): Border goes on the split container
201+ ctx.borderContainer = node.container
202+ ctx.cssClasses = []string{basePaneClass, multiPaneClass, activePaneClass}
203+ ctx.paneType = "split-branch"
204+
205+ default:
206+ // FALLBACK: Use the node's own container
207+ ctx.borderContainer = node.container
208+ ctx.cssClasses = []string{basePaneClass, activePaneClass}
209+ ctx.paneType = "fallback"
210+ }
211+
212+ return ctx
213+}
214+
215+// applyActivePaneBorder adds the active pane visual border using the centralized system
216+func (wm *WorkspaceManager) applyActivePaneBorder(node *paneNode) {
217+ ctx := wm.determineBorderContext(node)
218+ if ctx == nil {
219+ return
220+ }
221+
222+ // Apply CSS classes to border container (provides border styling)
223+ if ctx.borderContainer != nil {
224+ ctx.borderContainer.Execute(func(containerPtr uintptr) error {
225+ // Only add the active class - base classes should already be set
226+ webkit.WidgetAddCSSClass(containerPtr, activePaneClass)
227+ return nil
228+ })
229+ }
230+
231+ log.Printf("[workspace] Applied border to %s pane: %p", ctx.paneType, node)
232+}
233+
234+// removeActivePaneBorder removes the active pane visual border using the centralized system
235+func (wm *WorkspaceManager) removeActivePaneBorder(node *paneNode) {
236+ ctx := wm.determineBorderContext(node)
237+ if ctx == nil {
238+ return
239+ }
240+
241+ // Remove CSS classes from border container
242+ if ctx.borderContainer != nil {
243+ ctx.borderContainer.Execute(func(containerPtr uintptr) error {
244+ // Only remove the active class if it exists
245+ if webkit.WidgetHasCSSClass(containerPtr, activePaneClass) {
246+ webkit.WidgetRemoveCSSClass(containerPtr, activePaneClass)
247+ }
248+ return nil
249+ })
250+ }
251+
252+ log.Printf("[workspace] Removed border from %s pane: %p", ctx.paneType, node)
253+}
254+
255+// removeActivePaneBorderFromAll removes active pane borders from all containers using centralized system
256+func (wm *WorkspaceManager) removeActivePaneBorderFromAll() {
257+ if wm == nil {
258+ return
259+ }
260+
261+ log.Printf("[workspace] Removing active borders from all panes")
262+
263+ // Use the centralized system for each pane
264+ for _, node := range wm.viewToNode {
265+ if node != nil {
266+ wm.removeActivePaneBorder(node)
267+ }
268+ }
269+}
270+
271+// isActivePaneBorderApplied checks if the active pane border is currently applied to a node using centralized system
272+func (wm *WorkspaceManager) isActivePaneBorderApplied(node *paneNode) bool {
273+ ctx := wm.determineBorderContext(node)
274+ if ctx == nil || ctx.borderContainer == nil {
275+ return false
276+ }
277+
278+ var hasClass bool
279+ ctx.borderContainer.Execute(func(containerPtr uintptr) error {
280+ hasClass = webkit.WidgetHasCSSClass(containerPtr, activePaneClass)
281+ return nil
282+ })
283+
284+ return hasClass
285+}
286+
287+// getBorderContextInfo returns debugging information about a pane's border context
288+func (wm *WorkspaceManager) getBorderContextInfo(node *paneNode) string {
289+ ctx := wm.determineBorderContext(node)
290+ if ctx == nil {
291+ return "no-context"
292+ }
293+ return ctx.paneType
294+}
@@ -4,6 +4,7 @@ package browser
4 import (4 import (
5 "errors"5 "errors"
6 "fmt"6 "fmt"
7+ "log"
7 8
8 "github.com/bnema/dumber/pkg/webkit"9 "github.com/bnema/dumber/pkg/webkit"
9 )10 )
@@ -97,6 +98,14 @@ func (wm *WorkspaceManager) initializePaneWidgets(node *paneNode, containerPtr u
97 // Register the main container98 // Register the main container
98 node.container = wm.registerWidget(containerPtr, "pane-container")99 node.container = wm.registerWidget(containerPtr, "pane-container")
99 100
101+ // Add base CSS class to the container
102+ if node.container != nil {
103+ node.container.Execute(func(ptr uintptr) error {
104+ webkit.WidgetAddCSSClass(ptr, basePaneClass)
105+ return nil
106+ })
107+ }
108+
100 // Initialize other widget fields as nil (will be set when needed)109 // Initialize other widget fields as nil (will be set when needed)
101 node.titleBar = nil110 node.titleBar = nil
102 node.stackWrapper = nil111 node.stackWrapper = nil
@@ -142,3 +151,144 @@ func (wm *WorkspaceManager) setTitleBar(node *paneNode, ptr uintptr) {
142 func (wm *WorkspaceManager) setStackWrapper(node *paneNode, ptr uintptr) {151 func (wm *WorkspaceManager) setStackWrapper(node *paneNode, ptr uintptr) {
143 node.stackWrapper = wm.registerWidget(ptr, "stack-wrapper")152 node.stackWrapper = wm.registerWidget(ptr, "stack-wrapper")
144 }153 }
154+
155+// Centralized Active Pane Border Management System
156+// Handles all pane types: regular, popup, stacked, split panes
157+
158+// PaneBorderContext holds the context for applying borders to any pane type
159+type PaneBorderContext struct {
160+ webViewWidget uintptr // The WebView's native widget (for margin)
161+ borderContainer *SafeWidget // The container that gets background color
162+ cssClasses []string // Additional CSS classes to apply
163+ paneType string // Description for debugging
164+}
165+
166+// determineBorderContext analyzes a pane node and determines the correct border context
167+func (wm *WorkspaceManager) determineBorderContext(node *paneNode) *PaneBorderContext {
168+ if node == nil {
169+ return nil
170+ }
171+
172+ ctx := &PaneBorderContext{}
173+
174+ // Step 1: Get WebView widget for margin (same for all pane types)
175+ if node.pane != nil && node.pane.webView != nil {
176+ ctx.webViewWidget = node.pane.webView.RootWidget()
177+ }
178+
179+ // Step 2: Determine border container and CSS classes based on pane hierarchy and type
180+ switch {
181+ case node.parent != nil && node.parent.isStacked:
182+ // STACKED PANE: Border goes on the stack container
183+ ctx.borderContainer = node.parent.container
184+ ctx.cssClasses = []string{stackContainerClass, activePaneClass}
185+ ctx.paneType = "stacked"
186+
187+ case node.windowType == webkit.WindowTypePopup:
188+ // POPUP PANE: Border goes on the popup's own container
189+ ctx.borderContainer = node.container
190+ ctx.cssClasses = []string{basePaneClass, activePaneClass}
191+ ctx.paneType = "popup"
192+
193+ case node.isLeaf && !node.isStacked:
194+ // REGULAR LEAF PANE: Border goes on the pane's container
195+ ctx.borderContainer = node.container
196+ ctx.cssClasses = []string{basePaneClass, activePaneClass}
197+ ctx.paneType = "regular-leaf"
198+
199+ case !node.isLeaf:
200+ // SPLIT PANE (branch node): Border goes on the split container
201+ ctx.borderContainer = node.container
202+ ctx.cssClasses = []string{basePaneClass, multiPaneClass, activePaneClass}
203+ ctx.paneType = "split-branch"
204+
205+ default:
206+ // FALLBACK: Use the node's own container
207+ ctx.borderContainer = node.container
208+ ctx.cssClasses = []string{basePaneClass, activePaneClass}
209+ ctx.paneType = "fallback"
210+ }
211+
212+ return ctx
213+}
214+
215+// applyActivePaneBorder adds the active pane visual border using the centralized system
216+func (wm *WorkspaceManager) applyActivePaneBorder(node *paneNode) {
217+ ctx := wm.determineBorderContext(node)
218+ if ctx == nil {
219+ return
220+ }
221+
222+ // Apply CSS classes to border container (provides border styling)
223+ if ctx.borderContainer != nil {
224+ ctx.borderContainer.Execute(func(containerPtr uintptr) error {
225+ // Only add the active class - base classes should already be set
226+ webkit.WidgetAddCSSClass(containerPtr, activePaneClass)
227+ return nil
228+ })
229+ }
230+
231+ log.Printf("[workspace] Applied border to %s pane: %p", ctx.paneType, node)
232+}
233+
234+// removeActivePaneBorder removes the active pane visual border using the centralized system
235+func (wm *WorkspaceManager) removeActivePaneBorder(node *paneNode) {
236+ ctx := wm.determineBorderContext(node)
237+ if ctx == nil {
238+ return
239+ }
240+
241+ // Remove CSS classes from border container
242+ if ctx.borderContainer != nil {
243+ ctx.borderContainer.Execute(func(containerPtr uintptr) error {
244+ // Only remove the active class if it exists
245+ if webkit.WidgetHasCSSClass(containerPtr, activePaneClass) {
246+ webkit.WidgetRemoveCSSClass(containerPtr, activePaneClass)
247+ }
248+ return nil
249+ })
250+ }
251+
252+ log.Printf("[workspace] Removed border from %s pane: %p", ctx.paneType, node)
253+}
254+
255+// removeActivePaneBorderFromAll removes active pane borders from all containers using centralized system
256+func (wm *WorkspaceManager) removeActivePaneBorderFromAll() {
257+ if wm == nil {
258+ return
259+ }
260+
261+ log.Printf("[workspace] Removing active borders from all panes")
262+
263+ // Use the centralized system for each pane
264+ for _, node := range wm.viewToNode {
265+ if node != nil {
266+ wm.removeActivePaneBorder(node)
267+ }
268+ }
269+}
270+
271+// isActivePaneBorderApplied checks if the active pane border is currently applied to a node using centralized system
272+func (wm *WorkspaceManager) isActivePaneBorderApplied(node *paneNode) bool {
273+ ctx := wm.determineBorderContext(node)
274+ if ctx == nil || ctx.borderContainer == nil {
275+ return false
276+ }
277+
278+ var hasClass bool
279+ ctx.borderContainer.Execute(func(containerPtr uintptr) error {
280+ hasClass = webkit.WidgetHasCSSClass(containerPtr, activePaneClass)
281+ return nil
282+ })
283+
284+ return hasClass
285+}
286+
287+// getBorderContextInfo returns debugging information about a pane's border context
288+func (wm *WorkspaceManager) getBorderContextInfo(node *paneNode) string {
289+ ctx := wm.determineBorderContext(node)
290+ if ctx == nil {
291+ return "no-context"
292+ }
293+ return ctx.paneType
294+}

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