detached 525c783

Merge branch 'vk/718f-add-unit-t' into feat/zellij-pane-mode

commit
525c783354b77ae3a88f7af6fc499e58358dcaeb
author
bamen · · 2025-09-19 04:30 UTC
merge of
3075954 + b1ee2a5 · diff against the first parent
signature
unsigned
9 files · +135 -16
M internal/app/browser/browser.go +10 -6
@@ -54,10 +54,10 @@ type BrowserApp struct {
5454 filterManager *filtering.FilterManager
5555
5656 // Handlers
57- schemeHandler *api.SchemeHandler
58- messageHandler *messaging.Handler
59- shortcutHandler *ShortcutHandler
60- // windowShortcutHandler disabled for testing
57+ schemeHandler *api.SchemeHandler
58+ messageHandler *messaging.Handler
59+ shortcutHandler *ShortcutHandler
60+ windowShortcutHandler WindowShortcutHandlerInterface
6161 }
6262
6363 // Run starts the browser application
@@ -180,8 +180,12 @@ func (app *BrowserApp) Run() {
180180 func (app *BrowserApp) cleanup() {
181181 log.Printf("Starting browser cleanup...")
182182
183- // Cleanup window shortcuts first - disabled for testing
184- log.Printf("Window-level shortcuts cleanup disabled for testing")
183+ // Cleanup window shortcuts first
184+ if app.windowShortcutHandler != nil {
185+ log.Printf("Cleaning up window shortcuts")
186+ app.windowShortcutHandler.Cleanup()
187+ app.windowShortcutHandler = nil
188+ }
185189
186190 // Cleanup all panes
187191 if app.panes != nil {
@@ -54,10 +54,10 @@ type BrowserApp struct {
54 filterManager *filtering.FilterManager54 filterManager *filtering.FilterManager
55 55
56 // Handlers56 // Handlers
57- schemeHandler *api.SchemeHandler57+ schemeHandler *api.SchemeHandler
58- messageHandler *messaging.Handler58+ messageHandler *messaging.Handler
59- shortcutHandler *ShortcutHandler59+ shortcutHandler *ShortcutHandler
60- // windowShortcutHandler disabled for testing60+ windowShortcutHandler WindowShortcutHandlerInterface
61 }61 }
62 62
63 // Run starts the browser application63 // Run starts the browser application
@@ -180,8 +180,12 @@ func (app *BrowserApp) Run() {
180 func (app *BrowserApp) cleanup() {180 func (app *BrowserApp) cleanup() {
181 log.Printf("Starting browser cleanup...")181 log.Printf("Starting browser cleanup...")
182 182
183- // Cleanup window shortcuts first - disabled for testing183+ // Cleanup window shortcuts first
184- log.Printf("Window-level shortcuts cleanup disabled for testing")184+ if app.windowShortcutHandler != nil {
185+ log.Printf("Cleaning up window shortcuts")
186+ app.windowShortcutHandler.Cleanup()
187+ app.windowShortcutHandler = nil
188+ }
185 189
186 // Cleanup all panes190 // Cleanup all panes
187 if app.panes != nil {191 if app.panes != nil {
M internal/app/browser/interfaces.go +5 -0
@@ -81,3 +81,8 @@ type BrowserAppInterface interface {
8181 type WindowInterface interface {
8282 SetChild(widget uintptr)
8383 }
84+
85+// WindowShortcutHandlerInterface abstracts window shortcut handling for testing
86+type WindowShortcutHandlerInterface interface {
87+ Cleanup()
88+}
@@ -81,3 +81,8 @@ type BrowserAppInterface interface {
81 type WindowInterface interface {81 type WindowInterface interface {
82 SetChild(widget uintptr)82 SetChild(widget uintptr)
83 }83 }
84+
85+// WindowShortcutHandlerInterface abstracts window shortcut handling for testing
86+type WindowShortcutHandlerInterface interface {
87+ Cleanup()
88+}
M internal/app/browser/mocks/mock_browser.go +35 -0
@@ -715,3 +715,38 @@ func (mr *MockWindowInterfaceMockRecorder) SetChild(widget interface{}) *gomock.
715715 mr.mock.ctrl.T.Helper()
716716 return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "SetChild", reflect.TypeOf((*MockWindowInterface)(nil).SetChild), widget)
717717 }
718+
719+// MockWindowShortcutHandlerInterface is a mock of WindowShortcutHandlerInterface interface.
720+type MockWindowShortcutHandlerInterface struct {
721+ ctrl *gomock.Controller
722+ recorder *MockWindowShortcutHandlerInterfaceMockRecorder
723+}
724+
725+// MockWindowShortcutHandlerInterfaceMockRecorder is the mock recorder for MockWindowShortcutHandlerInterface.
726+type MockWindowShortcutHandlerInterfaceMockRecorder struct {
727+ mock *MockWindowShortcutHandlerInterface
728+}
729+
730+// NewMockWindowShortcutHandlerInterface creates a new mock instance.
731+func NewMockWindowShortcutHandlerInterface(ctrl *gomock.Controller) *MockWindowShortcutHandlerInterface {
732+ mock := &MockWindowShortcutHandlerInterface{ctrl: ctrl}
733+ mock.recorder = &MockWindowShortcutHandlerInterfaceMockRecorder{mock}
734+ return mock
735+}
736+
737+// EXPECT returns an object that allows the caller to indicate expected use.
738+func (m *MockWindowShortcutHandlerInterface) EXPECT() *MockWindowShortcutHandlerInterfaceMockRecorder {
739+ return m.recorder
740+}
741+
742+// Cleanup mocks base method.
743+func (m *MockWindowShortcutHandlerInterface) Cleanup() {
744+ m.ctrl.T.Helper()
745+ m.ctrl.Call(m, "Cleanup")
746+}
747+
748+// Cleanup indicates an expected call of Cleanup.
749+func (mr *MockWindowShortcutHandlerInterfaceMockRecorder) Cleanup() *gomock.Call {
750+ mr.mock.ctrl.T.Helper()
751+ return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "Cleanup", reflect.TypeOf((*MockWindowShortcutHandlerInterface)(nil).Cleanup))
752+}
@@ -715,3 +715,38 @@ func (mr *MockWindowInterfaceMockRecorder) SetChild(widget interface{}) *gomock.
715 mr.mock.ctrl.T.Helper()715 mr.mock.ctrl.T.Helper()
716 return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "SetChild", reflect.TypeOf((*MockWindowInterface)(nil).SetChild), widget)716 return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "SetChild", reflect.TypeOf((*MockWindowInterface)(nil).SetChild), widget)
717 }717 }
718+
719+// MockWindowShortcutHandlerInterface is a mock of WindowShortcutHandlerInterface interface.
720+type MockWindowShortcutHandlerInterface struct {
721+ ctrl *gomock.Controller
722+ recorder *MockWindowShortcutHandlerInterfaceMockRecorder
723+}
724+
725+// MockWindowShortcutHandlerInterfaceMockRecorder is the mock recorder for MockWindowShortcutHandlerInterface.
726+type MockWindowShortcutHandlerInterfaceMockRecorder struct {
727+ mock *MockWindowShortcutHandlerInterface
728+}
729+
730+// NewMockWindowShortcutHandlerInterface creates a new mock instance.
731+func NewMockWindowShortcutHandlerInterface(ctrl *gomock.Controller) *MockWindowShortcutHandlerInterface {
732+ mock := &MockWindowShortcutHandlerInterface{ctrl: ctrl}
733+ mock.recorder = &MockWindowShortcutHandlerInterfaceMockRecorder{mock}
734+ return mock
735+}
736+
737+// EXPECT returns an object that allows the caller to indicate expected use.
738+func (m *MockWindowShortcutHandlerInterface) EXPECT() *MockWindowShortcutHandlerInterfaceMockRecorder {
739+ return m.recorder
740+}
741+
742+// Cleanup mocks base method.
743+func (m *MockWindowShortcutHandlerInterface) Cleanup() {
744+ m.ctrl.T.Helper()
745+ m.ctrl.Call(m, "Cleanup")
746+}
747+
748+// Cleanup indicates an expected call of Cleanup.
749+func (mr *MockWindowShortcutHandlerInterfaceMockRecorder) Cleanup() *gomock.Call {
750+ mr.mock.ctrl.T.Helper()
751+ return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "Cleanup", reflect.TypeOf((*MockWindowShortcutHandlerInterface)(nil).Cleanup))
752+}
A internal/app/browser/test_helpers.go +8 -0
@@ -0,0 +1,8 @@
1+package browser
2+
3+// Mock WindowShortcutHandler for testing
4+type mockWindowShortcutHandler struct{}
5+
6+func (m *mockWindowShortcutHandler) Cleanup() {
7+ // No-op for testing
8+}⏎̸
@@ -0,0 +1,8 @@
1+package browser
2+
3+// Mock WindowShortcutHandler for testing
4+type mockWindowShortcutHandler struct{}
5+
6+func (m *mockWindowShortcutHandler) Cleanup() {
7+ // No-op for testing
8+}⏎̸
M internal/app/browser/webview.go +6 -2
@@ -256,8 +256,12 @@ func (app *BrowserApp) createWebView() error {
256256
257257 // Initialize window-level global shortcuts AFTER workspace is set up
258258 if window := view.Window(); window != nil {
259- // app.windowShortcutHandler = NewWindowShortcutHandler(window, app) // disabled for testing
260- log.Printf("Window-level global shortcuts disabled for testing")
259+ app.windowShortcutHandler = NewWindowShortcutHandler(window, app)
260+ if app.windowShortcutHandler != nil {
261+ log.Printf("Window-level global shortcuts initialized")
262+ } else {
263+ log.Printf("Warning: failed to initialize window-level shortcuts")
264+ }
261265 }
262266
263267 app.setupWebViewIntegration()
@@ -256,8 +256,12 @@ func (app *BrowserApp) createWebView() error {
256 256
257 // Initialize window-level global shortcuts AFTER workspace is set up257 // Initialize window-level global shortcuts AFTER workspace is set up
258 if window := view.Window(); window != nil {258 if window := view.Window(); window != nil {
259- // app.windowShortcutHandler = NewWindowShortcutHandler(window, app) // disabled for testing259+ app.windowShortcutHandler = NewWindowShortcutHandler(window, app)
260- log.Printf("Window-level global shortcuts disabled for testing")260+ if app.windowShortcutHandler != nil {
261+ log.Printf("Window-level global shortcuts initialized")
262+ } else {
263+ log.Printf("Warning: failed to initialize window-level shortcuts")
264+ }
261 }265 }
262 266
263 app.setupWebViewIntegration()267 app.setupWebViewIntegration()
M internal/app/browser/workspace_manager_recursion_test.go +15 -5
@@ -77,13 +77,23 @@ func TestInfiniteRecursionProtection(t *testing.T) {
7777 wm := newTestWorkspaceManagerWithMocksForTree(t)
7878
7979 // Create a tree with geometry
80- right, _ := wm.splitNode(wm.root, "right")
81- bottom, _ := wm.splitNode(wm.root, "down")
80+ right, err := wm.splitNode(wm.root, "right")
81+ if err != nil {
82+ t.Fatalf("Failed to split right: %v", err)
83+ }
84+ bottom, err := wm.splitNode(right, "down")
85+ if err != nil {
86+ t.Fatalf("Failed to split down: %v", err)
87+ }
8288
8389 // Set widget bounds
8490 webkit.SetWidgetBoundsForTesting(wm.root.container, webkit.WidgetBounds{X: 0, Y: 0, Width: 100, Height: 100})
85- webkit.SetWidgetBoundsForTesting(right.container, webkit.WidgetBounds{X: 120, Y: 0, Width: 100, Height: 100})
86- webkit.SetWidgetBoundsForTesting(bottom.container, webkit.WidgetBounds{X: 0, Y: 120, Width: 100, Height: 100})
91+ if right != nil {
92+ webkit.SetWidgetBoundsForTesting(right.container, webkit.WidgetBounds{X: 120, Y: 0, Width: 100, Height: 100})
93+ }
94+ if bottom != nil {
95+ webkit.SetWidgetBoundsForTesting(bottom.container, webkit.WidgetBounds{X: 0, Y: 120, Width: 100, Height: 100})
96+ }
8797
8898 // Test that focus neighbor calculations complete quickly
8999 start := time.Now()
@@ -159,4 +169,4 @@ func TestRecursionLimits(t *testing.T) {
159169 t.Errorf("Corrupted node handling took too long: %v", elapsed)
160170 }
161171 })
162-}⏎̸
172+}
@@ -77,13 +77,23 @@ func TestInfiniteRecursionProtection(t *testing.T) {
77 wm := newTestWorkspaceManagerWithMocksForTree(t)77 wm := newTestWorkspaceManagerWithMocksForTree(t)
78 78
79 // Create a tree with geometry79 // Create a tree with geometry
80- right, _ := wm.splitNode(wm.root, "right")80+ right, err := wm.splitNode(wm.root, "right")
81- bottom, _ := wm.splitNode(wm.root, "down")81+ if err != nil {
82+ t.Fatalf("Failed to split right: %v", err)
83+ }
84+ bottom, err := wm.splitNode(right, "down")
85+ if err != nil {
86+ t.Fatalf("Failed to split down: %v", err)
87+ }
82 88
83 // Set widget bounds89 // Set widget bounds
84 webkit.SetWidgetBoundsForTesting(wm.root.container, webkit.WidgetBounds{X: 0, Y: 0, Width: 100, Height: 100})90 webkit.SetWidgetBoundsForTesting(wm.root.container, webkit.WidgetBounds{X: 0, Y: 0, Width: 100, Height: 100})
85- webkit.SetWidgetBoundsForTesting(right.container, webkit.WidgetBounds{X: 120, Y: 0, Width: 100, Height: 100})91+ if right != nil {
86- webkit.SetWidgetBoundsForTesting(bottom.container, webkit.WidgetBounds{X: 0, Y: 120, Width: 100, Height: 100})92+ webkit.SetWidgetBoundsForTesting(right.container, webkit.WidgetBounds{X: 120, Y: 0, Width: 100, Height: 100})
93+ }
94+ if bottom != nil {
95+ webkit.SetWidgetBoundsForTesting(bottom.container, webkit.WidgetBounds{X: 0, Y: 120, Width: 100, Height: 100})
96+ }
87 97
88 // Test that focus neighbor calculations complete quickly98 // Test that focus neighbor calculations complete quickly
89 start := time.Now()99 start := time.Now()
@@ -159,4 +169,4 @@ func TestRecursionLimits(t *testing.T) {
159 t.Errorf("Corrupted node handling took too long: %v", elapsed)169 t.Errorf("Corrupted node handling took too long: %v", elapsed)
160 }170 }
161 })171 })
162-}⏎̸172+}
M internal/app/browser/workspace_manager_test.go +4 -0
@@ -17,10 +17,14 @@ func newTestWorkspaceManager(t *testing.T) *WorkspaceManager {
1717
1818 rootPane := &BrowserPane{webView: rootView}
1919
20+ // Create mock window shortcut handler
21+ mockWindowShortcutHandler := &mockWindowShortcutHandler{}
22+
2023 app := &BrowserApp{}
2124 app.panes = []*BrowserPane{rootPane}
2225 app.activePane = rootPane
2326 app.webView = rootView
27+ app.windowShortcutHandler = mockWindowShortcutHandler
2428
2529 wm := NewWorkspaceManager(app, rootPane)
2630 wm.createWebViewFn = func() (*webkit.WebView, error) {
@@ -17,10 +17,14 @@ func newTestWorkspaceManager(t *testing.T) *WorkspaceManager {
17 17
18 rootPane := &BrowserPane{webView: rootView}18 rootPane := &BrowserPane{webView: rootView}
19 19
20+ // Create mock window shortcut handler
21+ mockWindowShortcutHandler := &mockWindowShortcutHandler{}
22+
20 app := &BrowserApp{}23 app := &BrowserApp{}
21 app.panes = []*BrowserPane{rootPane}24 app.panes = []*BrowserPane{rootPane}
22 app.activePane = rootPane25 app.activePane = rootPane
23 app.webView = rootView26 app.webView = rootView
27+ app.windowShortcutHandler = mockWindowShortcutHandler
24 28
25 wm := NewWorkspaceManager(app, rootPane)29 wm := NewWorkspaceManager(app, rootPane)
26 wm.createWebViewFn = func() (*webkit.WebView, error) {30 wm.createWebViewFn = func() (*webkit.WebView, error) {
M internal/app/browser/workspace_manager_tree_test.go +7 -3
@@ -553,10 +553,14 @@ func newTestWorkspaceManagerWithMocksForTree(t *testing.T) *WorkspaceManager {
553553 id: "root",
554554 }
555555
556+ // Create mock window shortcut handler
557+ mockWindowShortcutHandler := &mockWindowShortcutHandler{}
558+
556559 app := &BrowserApp{
557- panes: []*BrowserPane{rootPane},
558- activePane: rootPane,
559- webView: rootView,
560+ panes: []*BrowserPane{rootPane},
561+ activePane: rootPane,
562+ webView: rootView,
563+ windowShortcutHandler: mockWindowShortcutHandler,
560564 }
561565
562566 wm := &WorkspaceManager{
@@ -553,10 +553,14 @@ func newTestWorkspaceManagerWithMocksForTree(t *testing.T) *WorkspaceManager {
553 id: "root",553 id: "root",
554 }554 }
555 555
556+ // Create mock window shortcut handler
557+ mockWindowShortcutHandler := &mockWindowShortcutHandler{}
558+
556 app := &BrowserApp{559 app := &BrowserApp{
557- panes: []*BrowserPane{rootPane},560+ panes: []*BrowserPane{rootPane},
558- activePane: rootPane,561+ activePane: rootPane,
559- webView: rootView,562+ webView: rootView,
563+ windowShortcutHandler: mockWindowShortcutHandler,
560 }564 }
561 565
562 wm := &WorkspaceManager{566 wm := &WorkspaceManager{
A pkg/webkit/window_shortcuts_stub.go +45 -0
@@ -0,0 +1,45 @@
1+//go:build !webkit_cgo
2+
3+package webkit
4+
5+import "log"
6+
7+// WindowShortcuts stub implementation for non-CGO builds
8+type WindowShortcuts struct {
9+ window *Window
10+}
11+
12+// InitializeGlobalShortcuts returns a stub implementation for non-CGO builds
13+func (w *Window) InitializeGlobalShortcuts() *WindowShortcuts {
14+ log.Printf("[window-shortcuts] Stub implementation - global shortcuts not available without webkit_cgo")
15+ return &WindowShortcuts{
16+ window: w,
17+ }
18+}
19+
20+// RegisterGlobalShortcut stub - always returns nil for non-CGO builds
21+func (ws *WindowShortcuts) RegisterGlobalShortcut(key string, callback func()) error {
22+ log.Printf("[window-shortcuts] Stub: would register %s (webkit_cgo required for actual functionality)", key)
23+ return nil
24+}
25+
26+// Cleanup stub implementation
27+func (ws *WindowShortcuts) Cleanup() {
28+ log.Printf("[window-shortcuts] Stub cleanup")
29+}
30+
31+// Stub implementations for Go wrapper functions
32+func CreateGlobalShortcutController(window uintptr) uintptr {
33+ return 0
34+}
35+
36+func AddShortcutToController(controller uintptr, key string, handle uintptr) error {
37+ return nil
38+}
39+
40+func registerWindowShortcutCallback(cb func()) uintptr {
41+ return 0
42+}
43+
44+func unregisterWindowShortcutCallback(id uintptr) {
45+}⏎̸
@@ -0,0 +1,45 @@
1+//go:build !webkit_cgo
2+
3+package webkit
4+
5+import "log"
6+
7+// WindowShortcuts stub implementation for non-CGO builds
8+type WindowShortcuts struct {
9+ window *Window
10+}
11+
12+// InitializeGlobalShortcuts returns a stub implementation for non-CGO builds
13+func (w *Window) InitializeGlobalShortcuts() *WindowShortcuts {
14+ log.Printf("[window-shortcuts] Stub implementation - global shortcuts not available without webkit_cgo")
15+ return &WindowShortcuts{
16+ window: w,
17+ }
18+}
19+
20+// RegisterGlobalShortcut stub - always returns nil for non-CGO builds
21+func (ws *WindowShortcuts) RegisterGlobalShortcut(key string, callback func()) error {
22+ log.Printf("[window-shortcuts] Stub: would register %s (webkit_cgo required for actual functionality)", key)
23+ return nil
24+}
25+
26+// Cleanup stub implementation
27+func (ws *WindowShortcuts) Cleanup() {
28+ log.Printf("[window-shortcuts] Stub cleanup")
29+}
30+
31+// Stub implementations for Go wrapper functions
32+func CreateGlobalShortcutController(window uintptr) uintptr {
33+ return 0
34+}
35+
36+func AddShortcutToController(controller uintptr, key string, handle uintptr) error {
37+ return nil
38+}
39+
40+func registerWindowShortcutCallback(cb func()) uintptr {
41+ return 0
42+}
43+
44+func unregisterWindowShortcutCallback(id uintptr) {
45+}⏎̸

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