detached 6b42329

Speed up CEF repaint after live resize

commit
6b42329cec503b516eabb61da383216986cea504
author
brice · · 2026-05-11 07:01 UTC
parent
013796f
signature
GPG VerifiedCD88F9E564E0102646FB6077A1A07C7C7C3E349D · arch-gpg Verified for the committer email
5 files · +145 -35
M internal/infrastructure/cef/factory.go +3 -17
@@ -177,27 +177,13 @@ func (f *WebViewFactory) configureInitialBrowserCreation(
177177 return
178178 }
179179
180- wv.mu.RLock()
181- host := wv.host
182- wv.mu.RUnlock()
183- if host == nil {
184- logging.FromContext(ctx).Debug().
185- Uint64("webview_id", uint64(wv.id)).
186- Int32("resize_width", w).
187- Int32("resize_height", h).
188- Bool("host_nil", true).
189- Bool("browser_ready", false).
190- Msg("cef: resize observed before browser host existed")
191- return
192- }
193- notifyBrowserResize(host)
180+ synced := wv.syncResizeViewportOnGTK(ctx, "gtk-size-observer")
194181 logging.FromContext(ctx).Debug().
195182 Uint64("webview_id", uint64(wv.id)).
196183 Int32("resize_width", w).
197184 Int32("resize_height", h).
198- Bool("host_nil", false).
199- Bool("browser_ready", true).
200- Msg("cef: browser host WasResized invoked")
185+ Bool("browser_ready", synced).
186+ Msg("cef: viewport sync handled after GTK size change")
201187 })
202188 })
203189 }
@@ -177,27 +177,13 @@ func (f *WebViewFactory) configureInitialBrowserCreation(
177 return177 return
178 }178 }
179 179
180- wv.mu.RLock()180+ synced := wv.syncResizeViewportOnGTK(ctx, "gtk-size-observer")
181- host := wv.host
182- wv.mu.RUnlock()
183- if host == nil {
184- logging.FromContext(ctx).Debug().
185- Uint64("webview_id", uint64(wv.id)).
186- Int32("resize_width", w).
187- Int32("resize_height", h).
188- Bool("host_nil", true).
189- Bool("browser_ready", false).
190- Msg("cef: resize observed before browser host existed")
191- return
192- }
193- notifyBrowserResize(host)
194 logging.FromContext(ctx).Debug().181 logging.FromContext(ctx).Debug().
195 Uint64("webview_id", uint64(wv.id)).182 Uint64("webview_id", uint64(wv.id)).
196 Int32("resize_width", w).183 Int32("resize_width", w).
197 Int32("resize_height", h).184 Int32("resize_height", h).
198- Bool("host_nil", false).185+ Bool("browser_ready", synced).
199- Bool("browser_ready", true).186+ Msg("cef: viewport sync handled after GTK size change")
200- Msg("cef: browser host WasResized invoked")
201 })187 })
202 })188 })
203 }189 }
M internal/infrastructure/cef/webview.go +12 -10
@@ -90,13 +90,17 @@ type WebView struct {
9090 previousProfileSnapshot cef2gtk.ProfileSnapshot
9191 latestProfileSnapshotAt time.Time
9292
93- removeSizeObserver func()
94- viewportSyncMu sync.Mutex
95- viewportSyncPending bool
96- viewportSyncReason string
97- viewportMapFunc func(gtk.Widget)
98- viewportShowFunc func(gtk.Widget)
99- viewportRealizeFunc func(gtk.Widget)
93+ removeSizeObserver func()
94+ viewportSyncMu sync.Mutex
95+ viewportSyncPending bool
96+ viewportSyncReason string
97+ viewportMapFunc func(gtk.Widget)
98+ viewportShowFunc func(gtk.Widget)
99+ viewportRealizeFunc func(gtk.Widget)
100+ viewportMapSignalID uint
101+ viewportShowSignalID uint
102+ viewportRealizeSignalID uint
103+ viewportResizePulseSeq atomic.Uint64
100104
101105 // beginFrameTick drives CEF external BeginFrame requests while the GTK
102106 // widget is visible. Access is guarded by mu.
@@ -1084,9 +1088,7 @@ func (wv *WebView) destroyViewBridgeOnGTKThread() {
10841088 wv.removeSizeObserver()
10851089 wv.removeSizeObserver = nil
10861090 }
1087- wv.viewportMapFunc = nil
1088- wv.viewportShowFunc = nil
1089- wv.viewportRealizeFunc = nil
1091+ wv.disconnectViewportSyncHooksOnGTKThread()
10901092 _ = wv.viewBridge.DetachInput()
10911093 _ = wv.viewBridge.Destroy()
10921094 wv.viewBridge = nil
@@ -90,13 +90,17 @@ type WebView struct {
90 previousProfileSnapshot cef2gtk.ProfileSnapshot90 previousProfileSnapshot cef2gtk.ProfileSnapshot
91 latestProfileSnapshotAt time.Time91 latestProfileSnapshotAt time.Time
92 92
93- removeSizeObserver func()93+ removeSizeObserver func()
94- viewportSyncMu sync.Mutex94+ viewportSyncMu sync.Mutex
95- viewportSyncPending bool95+ viewportSyncPending bool
96- viewportSyncReason string96+ viewportSyncReason string
97- viewportMapFunc func(gtk.Widget)97+ viewportMapFunc func(gtk.Widget)
98- viewportShowFunc func(gtk.Widget)98+ viewportShowFunc func(gtk.Widget)
99- viewportRealizeFunc func(gtk.Widget)99+ viewportRealizeFunc func(gtk.Widget)
100+ viewportMapSignalID uint
101+ viewportShowSignalID uint
102+ viewportRealizeSignalID uint
103+ viewportResizePulseSeq atomic.Uint64
100 104
101 // beginFrameTick drives CEF external BeginFrame requests while the GTK105 // beginFrameTick drives CEF external BeginFrame requests while the GTK
102 // widget is visible. Access is guarded by mu.106 // widget is visible. Access is guarded by mu.
@@ -1084,9 +1088,7 @@ func (wv *WebView) destroyViewBridgeOnGTKThread() {
1084 wv.removeSizeObserver()1088 wv.removeSizeObserver()
1085 wv.removeSizeObserver = nil1089 wv.removeSizeObserver = nil
1086 }1090 }
1087- wv.viewportMapFunc = nil1091+ wv.disconnectViewportSyncHooksOnGTKThread()
1088- wv.viewportShowFunc = nil
1089- wv.viewportRealizeFunc = nil
1090 _ = wv.viewBridge.DetachInput()1092 _ = wv.viewBridge.DetachInput()
1091 _ = wv.viewBridge.Destroy()1093 _ = wv.viewBridge.Destroy()
1092 wv.viewBridge = nil1094 wv.viewBridge = nil
M internal/infrastructure/cef/webview_viewport.go +89 -8
@@ -4,8 +4,11 @@ import (
44 "context"
55 "strings"
66
7- "github.com/bnema/dumber/internal/logging"
7+ purecef "github.com/bnema/purego-cef/cef"
8+ "github.com/bnema/puregotk/v4/gobject"
89 "github.com/bnema/puregotk/v4/gtk"
10+
11+ "github.com/bnema/dumber/internal/logging"
912 )
1013
1114 type viewportSyncBrowserHost interface {
@@ -66,13 +69,23 @@ func (wv *WebView) syncViewportOnGTK(ctx context.Context) {
6669 if wv == nil {
6770 return
6871 }
69- reason := wv.takeViewportSyncReason()
72+ wv.syncViewportNowOnGTK(ctx, wv.takeViewportSyncReason())
73+}
74+
75+func (wv *WebView) syncViewportNowOnGTK(ctx context.Context, reason string) bool {
76+ if wv == nil {
77+ return false
78+ }
79+ reason = strings.TrimSpace(reason)
80+ if reason == "" {
81+ reason = "manual"
82+ }
7083 if wv.destroyed.Load() || wv.viewBridge == nil {
71- return
84+ return false
7285 }
7386
7487 if wv.tryStartPendingBrowserCreateOnGTKThread(ctx, reason) {
75- return
88+ return false
7689 }
7790
7891 wv.mu.RLock()
@@ -83,7 +96,7 @@ func (wv *WebView) syncViewportOnGTK(ctx context.Context) {
8396 Uint64("webview_id", uint64(wv.id)).
8497 Str("reason", reason).
8598 Msg("cef: viewport sync skipped; browser host not ready")
86- return
99+ return false
87100 }
88101
89102 widget := wv.viewBridge.Widget()
@@ -94,6 +107,46 @@ func (wv *WebView) syncViewportOnGTK(ctx context.Context) {
94107 Bool("visible", visible).
95108 Str("reason", reason).
96109 Msg("cef: viewport sync requested")
110+ return true
111+}
112+
113+func (wv *WebView) syncResizeViewportOnGTK(ctx context.Context, reason string) bool {
114+ if !wv.syncViewportNowOnGTK(ctx, reason) {
115+ return false
116+ }
117+ wv.scheduleResizeRepaintPulse(ctx, reason)
118+ return true
119+}
120+
121+func (wv *WebView) scheduleResizeRepaintPulse(ctx context.Context, reason string) {
122+ if wv == nil || wv.destroyed.Load() {
123+ return
124+ }
125+ seq := wv.viewportResizePulseSeq.Add(1)
126+ for _, delayMs := range [...]int64{16, 48} {
127+ delayMs := delayMs
128+ task := cefNewTask(cefTaskFunc(func() {
129+ if wv == nil || wv.destroyed.Load() || wv.viewportResizePulseSeq.Load() != seq {
130+ return
131+ }
132+ wv.mu.RLock()
133+ host := wv.host
134+ wv.mu.RUnlock()
135+ if host == nil {
136+ return
137+ }
138+ host.Invalidate(purecef.PaintElementTypePetView)
139+ logging.FromContext(ctx).Debug().
140+ Uint64("webview_id", uint64(wv.id)).
141+ Int64("delay_ms", delayMs).
142+ Str("reason", reason).
143+ Msg("cef: delayed resize repaint pulse")
144+ }))
145+ if task == nil {
146+ continue
147+ }
148+ cefPostDelayedTask(purecef.ThreadIDTidUi, task, delayMs)
149+ }
97150 }
98151
99152 func (wv *WebView) tryStartPendingBrowserCreateOnGTKThread(ctx context.Context, reason string) bool {
@@ -179,9 +232,37 @@ func (wv *WebView) installViewportSyncHooksOnGTKThread() {
179232 wv.SyncViewport(wv.ctx, "gtk-realize")
180233 }
181234
182- widget.ConnectMap(&wv.viewportMapFunc)
183- widget.ConnectShow(&wv.viewportShowFunc)
184- widget.ConnectRealize(&wv.viewportRealizeFunc)
235+ wv.viewportMapSignalID = widget.ConnectMap(&wv.viewportMapFunc)
236+ wv.viewportShowSignalID = widget.ConnectShow(&wv.viewportShowFunc)
237+ wv.viewportRealizeSignalID = widget.ConnectRealize(&wv.viewportRealizeFunc)
238+}
239+
240+func (wv *WebView) disconnectViewportSyncHooksOnGTKThread() {
241+ if wv == nil || wv.viewBridge == nil {
242+ return
243+ }
244+ widget := wv.viewBridge.Widget()
245+ if widget != nil {
246+ widgetPtr := widget.GoPointer()
247+ if widgetPtr != 0 {
248+ obj := gobject.ObjectNewFromInternalPtr(widgetPtr)
249+ if wv.viewportMapSignalID != 0 {
250+ gobject.SignalHandlerDisconnect(obj, wv.viewportMapSignalID)
251+ wv.viewportMapSignalID = 0
252+ }
253+ if wv.viewportShowSignalID != 0 {
254+ gobject.SignalHandlerDisconnect(obj, wv.viewportShowSignalID)
255+ wv.viewportShowSignalID = 0
256+ }
257+ if wv.viewportRealizeSignalID != 0 {
258+ gobject.SignalHandlerDisconnect(obj, wv.viewportRealizeSignalID)
259+ wv.viewportRealizeSignalID = 0
260+ }
261+ }
262+ }
263+ wv.viewportMapFunc = nil
264+ wv.viewportShowFunc = nil
265+ wv.viewportRealizeFunc = nil
185266 }
186267
187268 func notifyBrowserViewportSync(host viewportSyncBrowserHost, visible bool) {
@@ -4,8 +4,11 @@ import (
4 "context"4 "context"
5 "strings"5 "strings"
6 6
7- "github.com/bnema/dumber/internal/logging"7+ purecef "github.com/bnema/purego-cef/cef"
8+ "github.com/bnema/puregotk/v4/gobject"
8 "github.com/bnema/puregotk/v4/gtk"9 "github.com/bnema/puregotk/v4/gtk"
10+
11+ "github.com/bnema/dumber/internal/logging"
9 )12 )
10 13
11 type viewportSyncBrowserHost interface {14 type viewportSyncBrowserHost interface {
@@ -66,13 +69,23 @@ func (wv *WebView) syncViewportOnGTK(ctx context.Context) {
66 if wv == nil {69 if wv == nil {
67 return70 return
68 }71 }
69- reason := wv.takeViewportSyncReason()72+ wv.syncViewportNowOnGTK(ctx, wv.takeViewportSyncReason())
73+}
74+
75+func (wv *WebView) syncViewportNowOnGTK(ctx context.Context, reason string) bool {
76+ if wv == nil {
77+ return false
78+ }
79+ reason = strings.TrimSpace(reason)
80+ if reason == "" {
81+ reason = "manual"
82+ }
70 if wv.destroyed.Load() || wv.viewBridge == nil {83 if wv.destroyed.Load() || wv.viewBridge == nil {
71- return84+ return false
72 }85 }
73 86
74 if wv.tryStartPendingBrowserCreateOnGTKThread(ctx, reason) {87 if wv.tryStartPendingBrowserCreateOnGTKThread(ctx, reason) {
75- return88+ return false
76 }89 }
77 90
78 wv.mu.RLock()91 wv.mu.RLock()
@@ -83,7 +96,7 @@ func (wv *WebView) syncViewportOnGTK(ctx context.Context) {
83 Uint64("webview_id", uint64(wv.id)).96 Uint64("webview_id", uint64(wv.id)).
84 Str("reason", reason).97 Str("reason", reason).
85 Msg("cef: viewport sync skipped; browser host not ready")98 Msg("cef: viewport sync skipped; browser host not ready")
86- return99+ return false
87 }100 }
88 101
89 widget := wv.viewBridge.Widget()102 widget := wv.viewBridge.Widget()
@@ -94,6 +107,46 @@ func (wv *WebView) syncViewportOnGTK(ctx context.Context) {
94 Bool("visible", visible).107 Bool("visible", visible).
95 Str("reason", reason).108 Str("reason", reason).
96 Msg("cef: viewport sync requested")109 Msg("cef: viewport sync requested")
110+ return true
111+}
112+
113+func (wv *WebView) syncResizeViewportOnGTK(ctx context.Context, reason string) bool {
114+ if !wv.syncViewportNowOnGTK(ctx, reason) {
115+ return false
116+ }
117+ wv.scheduleResizeRepaintPulse(ctx, reason)
118+ return true
119+}
120+
121+func (wv *WebView) scheduleResizeRepaintPulse(ctx context.Context, reason string) {
122+ if wv == nil || wv.destroyed.Load() {
123+ return
124+ }
125+ seq := wv.viewportResizePulseSeq.Add(1)
126+ for _, delayMs := range [...]int64{16, 48} {
127+ delayMs := delayMs
128+ task := cefNewTask(cefTaskFunc(func() {
129+ if wv == nil || wv.destroyed.Load() || wv.viewportResizePulseSeq.Load() != seq {
130+ return
131+ }
132+ wv.mu.RLock()
133+ host := wv.host
134+ wv.mu.RUnlock()
135+ if host == nil {
136+ return
137+ }
138+ host.Invalidate(purecef.PaintElementTypePetView)
139+ logging.FromContext(ctx).Debug().
140+ Uint64("webview_id", uint64(wv.id)).
141+ Int64("delay_ms", delayMs).
142+ Str("reason", reason).
143+ Msg("cef: delayed resize repaint pulse")
144+ }))
145+ if task == nil {
146+ continue
147+ }
148+ cefPostDelayedTask(purecef.ThreadIDTidUi, task, delayMs)
149+ }
97 }150 }
98 151
99 func (wv *WebView) tryStartPendingBrowserCreateOnGTKThread(ctx context.Context, reason string) bool {152 func (wv *WebView) tryStartPendingBrowserCreateOnGTKThread(ctx context.Context, reason string) bool {
@@ -179,9 +232,37 @@ func (wv *WebView) installViewportSyncHooksOnGTKThread() {
179 wv.SyncViewport(wv.ctx, "gtk-realize")232 wv.SyncViewport(wv.ctx, "gtk-realize")
180 }233 }
181 234
182- widget.ConnectMap(&wv.viewportMapFunc)235+ wv.viewportMapSignalID = widget.ConnectMap(&wv.viewportMapFunc)
183- widget.ConnectShow(&wv.viewportShowFunc)236+ wv.viewportShowSignalID = widget.ConnectShow(&wv.viewportShowFunc)
184- widget.ConnectRealize(&wv.viewportRealizeFunc)237+ wv.viewportRealizeSignalID = widget.ConnectRealize(&wv.viewportRealizeFunc)
238+}
239+
240+func (wv *WebView) disconnectViewportSyncHooksOnGTKThread() {
241+ if wv == nil || wv.viewBridge == nil {
242+ return
243+ }
244+ widget := wv.viewBridge.Widget()
245+ if widget != nil {
246+ widgetPtr := widget.GoPointer()
247+ if widgetPtr != 0 {
248+ obj := gobject.ObjectNewFromInternalPtr(widgetPtr)
249+ if wv.viewportMapSignalID != 0 {
250+ gobject.SignalHandlerDisconnect(obj, wv.viewportMapSignalID)
251+ wv.viewportMapSignalID = 0
252+ }
253+ if wv.viewportShowSignalID != 0 {
254+ gobject.SignalHandlerDisconnect(obj, wv.viewportShowSignalID)
255+ wv.viewportShowSignalID = 0
256+ }
257+ if wv.viewportRealizeSignalID != 0 {
258+ gobject.SignalHandlerDisconnect(obj, wv.viewportRealizeSignalID)
259+ wv.viewportRealizeSignalID = 0
260+ }
261+ }
262+ }
263+ wv.viewportMapFunc = nil
264+ wv.viewportShowFunc = nil
265+ wv.viewportRealizeFunc = nil
185 }266 }
186 267
187 func notifyBrowserViewportSync(host viewportSyncBrowserHost, visible bool) {268 func notifyBrowserViewportSync(host viewportSyncBrowserHost, visible bool) {
M internal/infrastructure/cef/webview_viewport_test.go +40 -0
@@ -1,6 +1,7 @@
11 package cef
22
33 import (
4+ "context"
45 "testing"
56
67 purecef "github.com/bnema/purego-cef/cef"
@@ -8,6 +9,7 @@ import (
89 )
910
1011 type viewportSyncOrderHost struct {
12+ purecef.BrowserHost
1113 calls []string
1214 }
1315
@@ -45,3 +47,41 @@ func TestNotifyBrowserViewportSync_HiddenSkipsWasHidden(t *testing.T) {
4547
4648 require.Equal(t, []string{"NotifyScreenInfoChanged", "WasResized", "Invalidate"}, host.calls)
4749 }
50+
51+func TestScheduleResizeRepaintPulse_CoalescesToLatestSequence(t *testing.T) {
52+ oldNewTask := cefNewTask
53+ oldPostDelayedTask := cefPostDelayedTask
54+ defer func() {
55+ cefNewTask = oldNewTask
56+ cefPostDelayedTask = oldPostDelayedTask
57+ }()
58+
59+ cefNewTask = func(task purecef.Task) purecef.Task { return task }
60+
61+ var scheduled []purecef.Task
62+ var delays []int64
63+ cefPostDelayedTask = func(threadID purecef.ThreadID, task purecef.Task, delayMs int64) int32 {
64+ require.Equal(t, purecef.ThreadIDTidUi, threadID)
65+ require.NotNil(t, task)
66+ scheduled = append(scheduled, task)
67+ delays = append(delays, delayMs)
68+ return 1
69+ }
70+
71+ host := &viewportSyncOrderHost{}
72+ wv := &WebView{ctx: context.Background(), host: host}
73+
74+ wv.scheduleResizeRepaintPulse(context.Background(), "first")
75+ wv.scheduleResizeRepaintPulse(context.Background(), "second")
76+
77+ require.Equal(t, []int64{16, 48, 16, 48}, delays)
78+ require.Len(t, scheduled, 4)
79+
80+ scheduled[0].Execute()
81+ scheduled[1].Execute()
82+ require.Empty(t, host.calls)
83+
84+ scheduled[2].Execute()
85+ scheduled[3].Execute()
86+ require.Equal(t, []string{"Invalidate", "Invalidate"}, host.calls)
87+}
@@ -1,6 +1,7 @@
1 package cef1 package cef
2 2
3 import (3 import (
4+ "context"
4 "testing"5 "testing"
5 6
6 purecef "github.com/bnema/purego-cef/cef"7 purecef "github.com/bnema/purego-cef/cef"
@@ -8,6 +9,7 @@ import (
8 )9 )
9 10
10 type viewportSyncOrderHost struct {11 type viewportSyncOrderHost struct {
12+ purecef.BrowserHost
11 calls []string13 calls []string
12 }14 }
13 15
@@ -45,3 +47,41 @@ func TestNotifyBrowserViewportSync_HiddenSkipsWasHidden(t *testing.T) {
45 47
46 require.Equal(t, []string{"NotifyScreenInfoChanged", "WasResized", "Invalidate"}, host.calls)48 require.Equal(t, []string{"NotifyScreenInfoChanged", "WasResized", "Invalidate"}, host.calls)
47 }49 }
50+
51+func TestScheduleResizeRepaintPulse_CoalescesToLatestSequence(t *testing.T) {
52+ oldNewTask := cefNewTask
53+ oldPostDelayedTask := cefPostDelayedTask
54+ defer func() {
55+ cefNewTask = oldNewTask
56+ cefPostDelayedTask = oldPostDelayedTask
57+ }()
58+
59+ cefNewTask = func(task purecef.Task) purecef.Task { return task }
60+
61+ var scheduled []purecef.Task
62+ var delays []int64
63+ cefPostDelayedTask = func(threadID purecef.ThreadID, task purecef.Task, delayMs int64) int32 {
64+ require.Equal(t, purecef.ThreadIDTidUi, threadID)
65+ require.NotNil(t, task)
66+ scheduled = append(scheduled, task)
67+ delays = append(delays, delayMs)
68+ return 1
69+ }
70+
71+ host := &viewportSyncOrderHost{}
72+ wv := &WebView{ctx: context.Background(), host: host}
73+
74+ wv.scheduleResizeRepaintPulse(context.Background(), "first")
75+ wv.scheduleResizeRepaintPulse(context.Background(), "second")
76+
77+ require.Equal(t, []int64{16, 48, 16, 48}, delays)
78+ require.Len(t, scheduled, 4)
79+
80+ scheduled[0].Execute()
81+ scheduled[1].Execute()
82+ require.Empty(t, host.calls)
83+
84+ scheduled[2].Execute()
85+ scheduled[3].Execute()
86+ require.Equal(t, []string{"Invalidate", "Invalidate"}, host.calls)
87+}
M internal/ui/app.go +1 -0
@@ -3821,6 +3821,7 @@ func (a *App) attachPopupToTab(ctx context.Context, tabID entity.TabID, pane *en
38213821 if widget != nil {
38223822 if err := wsView.SetWebViewWidget(pane.ID, widget); err != nil {
38233823 log.Warn().Err(err).Str("pane_id", string(pane.ID)).Msg("pane view not found for popup")
3824+ return
38243825 }
38253826 }
38263827 }
@@ -3821,6 +3821,7 @@ func (a *App) attachPopupToTab(ctx context.Context, tabID entity.TabID, pane *en
3821 if widget != nil {3821 if widget != nil {
3822 if err := wsView.SetWebViewWidget(pane.ID, widget); err != nil {3822 if err := wsView.SetWebViewWidget(pane.ID, widget); err != nil {
3823 log.Warn().Err(err).Str("pane_id", string(pane.ID)).Msg("pane view not found for popup")3823 log.Warn().Err(err).Str("pane_id", string(pane.ID)).Msg("pane view not found for popup")
3824+ return
3824 }3825 }
3825 }3826 }
3826 }3827 }

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