detached 3e836ca

fix(context-menu): preserve fallback error paths

commit
3e836caf563f8a35249b9d12af007198f617ee51
author
brice · · 2026-04-14 06:14 UTC
committer
brice · · 2026-04-14 12:06 UTC
parent
366c2e5
signature
GPG VerifiedCD88F9E564E0102646FB6077A1A07C7C7C3E349D · arch-gpg Verified for the committer email
4 files · +158 -4
M internal/infrastructure/cef/context_menu.go +1 -2
@@ -68,8 +68,7 @@ func (h *handlerSet) RunContextMenu(
6868 }
6969 }
7070 if h.wv == nil || h.wv.pipeline == nil || h.wv.pipeline.glArea == nil {
71- callback.Cancel()
72- return 1
71+ return 0
7372 }
7473
7574 x, y := contextMenuAnchorPosition(params, h.wv.pipeline.scale)
@@ -68,8 +68,7 @@ func (h *handlerSet) RunContextMenu(
68 }68 }
69 }69 }
70 if h.wv == nil || h.wv.pipeline == nil || h.wv.pipeline.glArea == nil {70 if h.wv == nil || h.wv.pipeline == nil || h.wv.pipeline.glArea == nil {
71- callback.Cancel()71+ return 0
72- return 1
73 }72 }
74 73
75 x, y := contextMenuAnchorPosition(params, h.wv.pipeline.scale)74 x, y := contextMenuAnchorPosition(params, h.wv.pipeline.scale)
A internal/infrastructure/cef/context_menu_run_context_menu_test.go +117 -0
@@ -0,0 +1,117 @@
1+package cef
2+
3+import (
4+ "context"
5+ "testing"
6+ "unsafe"
7+
8+ purecef "github.com/bnema/purego-cef/cef"
9+
10+ "github.com/bnema/dumber/internal/application/port"
11+ "github.com/stretchr/testify/require"
12+)
13+
14+type stubMenuModel struct{}
15+
16+func (stubMenuModel) IsSubMenu() bool { return false }
17+func (stubMenuModel) Clear() int32 { panic("unexpected call") }
18+func (stubMenuModel) GetCount() int { return 1 }
19+func (stubMenuModel) AddSeparator() int32 { panic("unexpected call") }
20+func (stubMenuModel) AddItem(commandID int32, label string) int32 { panic("unexpected call") }
21+func (stubMenuModel) AddCheckItem(commandID int32, label string) int32 { panic("unexpected call") }
22+func (stubMenuModel) AddRadioItem(commandID int32, label string, groupID int32) int32 {
23+ panic("unexpected call")
24+}
25+func (stubMenuModel) AddSubMenu(commandID int32, label string) purecef.MenuModel {
26+ panic("unexpected call")
27+}
28+func (stubMenuModel) InsertSeparatorAt(index int) int32 { panic("unexpected call") }
29+func (stubMenuModel) InsertItemAt(index int, commandID int32, label string) int32 {
30+ panic("unexpected call")
31+}
32+func (stubMenuModel) InsertCheckItemAt(index int, commandID int32, label string) int32 {
33+ panic("unexpected call")
34+}
35+func (stubMenuModel) InsertRadioItemAt(index int, commandID int32, label string, groupID int32) int32 {
36+ panic("unexpected call")
37+}
38+func (stubMenuModel) InsertSubMenuAt(index int, commandID int32, label string) purecef.MenuModel {
39+ panic("unexpected call")
40+}
41+func (stubMenuModel) Remove(commandID int32) int32 { panic("unexpected call") }
42+func (stubMenuModel) RemoveAt(index int) int32 { panic("unexpected call") }
43+func (stubMenuModel) GetIndexOf(commandID int32) int32 { panic("unexpected call") }
44+func (stubMenuModel) GetCommandIDAt(index int) int32 { return 101 }
45+func (stubMenuModel) SetCommandIDAt(index int, commandID int32) int32 { panic("unexpected call") }
46+func (stubMenuModel) GetLabel(commandID int32) string { panic("unexpected call") }
47+func (stubMenuModel) GetLabelAt(index int) string { return "Copy" }
48+func (stubMenuModel) SetLabel(commandID int32, label string) int32 { panic("unexpected call") }
49+func (stubMenuModel) SetLabelAt(index int, label string) int32 { panic("unexpected call") }
50+func (stubMenuModel) GetType(commandID int32) purecef.MenuItemType { panic("unexpected call") }
51+func (stubMenuModel) GetTypeAt(index int) purecef.MenuItemType {
52+ return purecef.MenuItemTypeMenuitemtypeCommand
53+}
54+func (stubMenuModel) GetGroupID(commandID int32) int32 { panic("unexpected call") }
55+func (stubMenuModel) GetGroupIDAt(index int) int32 { panic("unexpected call") }
56+func (stubMenuModel) SetGroupID(commandID int32, groupID int32) int32 { panic("unexpected call") }
57+func (stubMenuModel) SetGroupIDAt(index int, groupID int32) int32 { panic("unexpected call") }
58+func (stubMenuModel) GetSubMenu(commandID int32) purecef.MenuModel { panic("unexpected call") }
59+func (stubMenuModel) GetSubMenuAt(index int) purecef.MenuModel { panic("unexpected call") }
60+func (stubMenuModel) IsVisible(commandID int32) bool { panic("unexpected call") }
61+func (stubMenuModel) IsVisibleAt(index int) bool { panic("unexpected call") }
62+func (stubMenuModel) SetVisible(commandID int32, visible int32) int32 { panic("unexpected call") }
63+func (stubMenuModel) SetVisibleAt(index int, visible int32) int32 { panic("unexpected call") }
64+func (stubMenuModel) IsEnabled(commandID int32) bool { panic("unexpected call") }
65+func (stubMenuModel) IsEnabledAt(index int) bool { panic("unexpected call") }
66+func (stubMenuModel) SetEnabled(commandID int32, enabled int32) int32 { panic("unexpected call") }
67+func (stubMenuModel) SetEnabledAt(index int, enabled int32) int32 { panic("unexpected call") }
68+func (stubMenuModel) IsChecked(commandID int32) bool { panic("unexpected call") }
69+func (stubMenuModel) IsCheckedAt(index int) bool { panic("unexpected call") }
70+func (stubMenuModel) SetChecked(commandID int32, checked int32) int32 { panic("unexpected call") }
71+func (stubMenuModel) SetCheckedAt(index int, checked int32) int32 { panic("unexpected call") }
72+func (stubMenuModel) HasAccelerator(commandID int32) bool { panic("unexpected call") }
73+func (stubMenuModel) HasAcceleratorAt(index int) bool { panic("unexpected call") }
74+func (stubMenuModel) SetAccelerator(commandID int32, keyCode int32, shiftPressed int32, ctrlPressed int32, altPressed int32) int32 {
75+ panic("unexpected call")
76+}
77+func (stubMenuModel) SetAcceleratorAt(index int, keyCode int32, shiftPressed int32, ctrlPressed int32, altPressed int32) int32 {
78+ panic("unexpected call")
79+}
80+func (stubMenuModel) RemoveAccelerator(commandID int32) int32 { panic("unexpected call") }
81+func (stubMenuModel) RemoveAcceleratorAt(index int) int32 { panic("unexpected call") }
82+func (stubMenuModel) GetAccelerator(commandID int32, keyCode unsafe.Pointer, shiftPressed unsafe.Pointer, ctrlPressed unsafe.Pointer, altPressed unsafe.Pointer) int32 {
83+ panic("unexpected call")
84+}
85+func (stubMenuModel) GetAcceleratorAt(index int, keyCode unsafe.Pointer, shiftPressed unsafe.Pointer, ctrlPressed unsafe.Pointer, altPressed unsafe.Pointer) int32 {
86+ panic("unexpected call")
87+}
88+func (stubMenuModel) SetColor(commandID int32, colorType purecef.MenuColorType, color uintptr) int32 {
89+ panic("unexpected call")
90+}
91+func (stubMenuModel) SetColorAt(index int32, colorType purecef.MenuColorType, color uintptr) int32 {
92+ panic("unexpected call")
93+}
94+func (stubMenuModel) GetColor(commandID int32, colorType purecef.MenuColorType, color uintptr) int32 {
95+ panic("unexpected call")
96+}
97+func (stubMenuModel) GetColorAt(index int32, colorType purecef.MenuColorType, color uintptr) int32 {
98+ panic("unexpected call")
99+}
100+func (stubMenuModel) SetFontList(commandID int32, fontList string) int32 { panic("unexpected call") }
101+func (stubMenuModel) SetFontListAt(index int32, fontList string) int32 { panic("unexpected call") }
102+
103+type stubContextMenuBuilder struct{ items []port.MenuItem }
104+
105+func (b stubContextMenuBuilder) Build(context.Context, port.MenuContext) []port.MenuItem {
106+ return b.items
107+}
108+
109+func TestRunContextMenuReturnsZeroWhenAnchorMissing(t *testing.T) {
110+ h := &handlerSet{wv: &WebView{engine: &Engine{ctxMenuBuilder: stubContextMenuBuilder{items: []port.MenuItem{{Action: port.MenuActionCopySelection, Label: "Copy"}}}}}}
111+ callback := &stubRunContextMenuCallback{}
112+
113+ result := h.RunContextMenu(nil, nil, stubContextMenuParams{}, stubMenuModel{}, callback)
114+
115+ require.Equal(t, int32(0), result)
116+ require.Zero(t, callback.cancelCalls)
117+}
@@ -0,0 +1,117 @@
1+package cef
2+
3+import (
4+ "context"
5+ "testing"
6+ "unsafe"
7+
8+ purecef "github.com/bnema/purego-cef/cef"
9+
10+ "github.com/bnema/dumber/internal/application/port"
11+ "github.com/stretchr/testify/require"
12+)
13+
14+type stubMenuModel struct{}
15+
16+func (stubMenuModel) IsSubMenu() bool { return false }
17+func (stubMenuModel) Clear() int32 { panic("unexpected call") }
18+func (stubMenuModel) GetCount() int { return 1 }
19+func (stubMenuModel) AddSeparator() int32 { panic("unexpected call") }
20+func (stubMenuModel) AddItem(commandID int32, label string) int32 { panic("unexpected call") }
21+func (stubMenuModel) AddCheckItem(commandID int32, label string) int32 { panic("unexpected call") }
22+func (stubMenuModel) AddRadioItem(commandID int32, label string, groupID int32) int32 {
23+ panic("unexpected call")
24+}
25+func (stubMenuModel) AddSubMenu(commandID int32, label string) purecef.MenuModel {
26+ panic("unexpected call")
27+}
28+func (stubMenuModel) InsertSeparatorAt(index int) int32 { panic("unexpected call") }
29+func (stubMenuModel) InsertItemAt(index int, commandID int32, label string) int32 {
30+ panic("unexpected call")
31+}
32+func (stubMenuModel) InsertCheckItemAt(index int, commandID int32, label string) int32 {
33+ panic("unexpected call")
34+}
35+func (stubMenuModel) InsertRadioItemAt(index int, commandID int32, label string, groupID int32) int32 {
36+ panic("unexpected call")
37+}
38+func (stubMenuModel) InsertSubMenuAt(index int, commandID int32, label string) purecef.MenuModel {
39+ panic("unexpected call")
40+}
41+func (stubMenuModel) Remove(commandID int32) int32 { panic("unexpected call") }
42+func (stubMenuModel) RemoveAt(index int) int32 { panic("unexpected call") }
43+func (stubMenuModel) GetIndexOf(commandID int32) int32 { panic("unexpected call") }
44+func (stubMenuModel) GetCommandIDAt(index int) int32 { return 101 }
45+func (stubMenuModel) SetCommandIDAt(index int, commandID int32) int32 { panic("unexpected call") }
46+func (stubMenuModel) GetLabel(commandID int32) string { panic("unexpected call") }
47+func (stubMenuModel) GetLabelAt(index int) string { return "Copy" }
48+func (stubMenuModel) SetLabel(commandID int32, label string) int32 { panic("unexpected call") }
49+func (stubMenuModel) SetLabelAt(index int, label string) int32 { panic("unexpected call") }
50+func (stubMenuModel) GetType(commandID int32) purecef.MenuItemType { panic("unexpected call") }
51+func (stubMenuModel) GetTypeAt(index int) purecef.MenuItemType {
52+ return purecef.MenuItemTypeMenuitemtypeCommand
53+}
54+func (stubMenuModel) GetGroupID(commandID int32) int32 { panic("unexpected call") }
55+func (stubMenuModel) GetGroupIDAt(index int) int32 { panic("unexpected call") }
56+func (stubMenuModel) SetGroupID(commandID int32, groupID int32) int32 { panic("unexpected call") }
57+func (stubMenuModel) SetGroupIDAt(index int, groupID int32) int32 { panic("unexpected call") }
58+func (stubMenuModel) GetSubMenu(commandID int32) purecef.MenuModel { panic("unexpected call") }
59+func (stubMenuModel) GetSubMenuAt(index int) purecef.MenuModel { panic("unexpected call") }
60+func (stubMenuModel) IsVisible(commandID int32) bool { panic("unexpected call") }
61+func (stubMenuModel) IsVisibleAt(index int) bool { panic("unexpected call") }
62+func (stubMenuModel) SetVisible(commandID int32, visible int32) int32 { panic("unexpected call") }
63+func (stubMenuModel) SetVisibleAt(index int, visible int32) int32 { panic("unexpected call") }
64+func (stubMenuModel) IsEnabled(commandID int32) bool { panic("unexpected call") }
65+func (stubMenuModel) IsEnabledAt(index int) bool { panic("unexpected call") }
66+func (stubMenuModel) SetEnabled(commandID int32, enabled int32) int32 { panic("unexpected call") }
67+func (stubMenuModel) SetEnabledAt(index int, enabled int32) int32 { panic("unexpected call") }
68+func (stubMenuModel) IsChecked(commandID int32) bool { panic("unexpected call") }
69+func (stubMenuModel) IsCheckedAt(index int) bool { panic("unexpected call") }
70+func (stubMenuModel) SetChecked(commandID int32, checked int32) int32 { panic("unexpected call") }
71+func (stubMenuModel) SetCheckedAt(index int, checked int32) int32 { panic("unexpected call") }
72+func (stubMenuModel) HasAccelerator(commandID int32) bool { panic("unexpected call") }
73+func (stubMenuModel) HasAcceleratorAt(index int) bool { panic("unexpected call") }
74+func (stubMenuModel) SetAccelerator(commandID int32, keyCode int32, shiftPressed int32, ctrlPressed int32, altPressed int32) int32 {
75+ panic("unexpected call")
76+}
77+func (stubMenuModel) SetAcceleratorAt(index int, keyCode int32, shiftPressed int32, ctrlPressed int32, altPressed int32) int32 {
78+ panic("unexpected call")
79+}
80+func (stubMenuModel) RemoveAccelerator(commandID int32) int32 { panic("unexpected call") }
81+func (stubMenuModel) RemoveAcceleratorAt(index int) int32 { panic("unexpected call") }
82+func (stubMenuModel) GetAccelerator(commandID int32, keyCode unsafe.Pointer, shiftPressed unsafe.Pointer, ctrlPressed unsafe.Pointer, altPressed unsafe.Pointer) int32 {
83+ panic("unexpected call")
84+}
85+func (stubMenuModel) GetAcceleratorAt(index int, keyCode unsafe.Pointer, shiftPressed unsafe.Pointer, ctrlPressed unsafe.Pointer, altPressed unsafe.Pointer) int32 {
86+ panic("unexpected call")
87+}
88+func (stubMenuModel) SetColor(commandID int32, colorType purecef.MenuColorType, color uintptr) int32 {
89+ panic("unexpected call")
90+}
91+func (stubMenuModel) SetColorAt(index int32, colorType purecef.MenuColorType, color uintptr) int32 {
92+ panic("unexpected call")
93+}
94+func (stubMenuModel) GetColor(commandID int32, colorType purecef.MenuColorType, color uintptr) int32 {
95+ panic("unexpected call")
96+}
97+func (stubMenuModel) GetColorAt(index int32, colorType purecef.MenuColorType, color uintptr) int32 {
98+ panic("unexpected call")
99+}
100+func (stubMenuModel) SetFontList(commandID int32, fontList string) int32 { panic("unexpected call") }
101+func (stubMenuModel) SetFontListAt(index int32, fontList string) int32 { panic("unexpected call") }
102+
103+type stubContextMenuBuilder struct{ items []port.MenuItem }
104+
105+func (b stubContextMenuBuilder) Build(context.Context, port.MenuContext) []port.MenuItem {
106+ return b.items
107+}
108+
109+func TestRunContextMenuReturnsZeroWhenAnchorMissing(t *testing.T) {
110+ h := &handlerSet{wv: &WebView{engine: &Engine{ctxMenuBuilder: stubContextMenuBuilder{items: []port.MenuItem{{Action: port.MenuActionCopySelection, Label: "Copy"}}}}}}
111+ callback := &stubRunContextMenuCallback{}
112+
113+ result := h.RunContextMenu(nil, nil, stubContextMenuParams{}, stubMenuModel{}, callback)
114+
115+ require.Equal(t, int32(0), result)
116+ require.Zero(t, callback.cancelCalls)
117+}
M internal/infrastructure/clipboard/clipboard.go +12 -0
@@ -150,6 +150,7 @@ func selectSystemClipboard() (copyCmd, pasteCmd string) {
150150 func (a *Adapter) WriteText(ctx context.Context, text string) error {
151151 log := logging.FromContext(ctx)
152152 var commandErr error
153+ var toolkitErr error
153154 if a.copyCmd != "" {
154155 err := a.writeTextWithCommand(ctx, text, log)
155156 if err == nil {
@@ -163,11 +164,16 @@ func (a *Adapter) WriteText(ctx context.Context, text string) error {
163164 log.Debug().Str("backend", "toolkit").Int("len", len(text)).Msg("clipboard write success")
164165 return nil
165166 }
167+ toolkitErr = err
166168 log.Debug().Err(err).Msg("toolkit clipboard write failed; falling back")
167169 }
168170 if commandErr != nil {
169171 return commandErr
170172 }
173+ if toolkitErr != nil {
174+ log.Error().Err(toolkitErr).Msg("clipboard write failed")
175+ return toolkitErr
176+ }
171177 err := fmt.Errorf("no clipboard tool available (install wl-clipboard or xclip)")
172178 log.Error().Err(err).Msg("clipboard write failed")
173179 return err
@@ -179,6 +185,7 @@ func (a *Adapter) WriteText(ctx context.Context, text string) error {
179185 func (a *Adapter) WriteImage(ctx context.Context, image entity.ImageData) error {
180186 log := logging.FromContext(ctx)
181187 var commandErr error
188+ var toolkitErr error
182189
183190 if len(image.Bytes) == 0 {
184191 err := fmt.Errorf("empty image data")
@@ -195,6 +202,7 @@ func (a *Adapter) WriteImage(ctx context.Context, image entity.ImageData) error
195202 Msg("clipboard image write success")
196203 return nil
197204 }
205+ toolkitErr = err
198206 log.Debug().Err(err).Msg("toolkit clipboard image write failed; falling back")
199207 }
200208 if a.copyCmd != "" {
@@ -207,6 +215,10 @@ func (a *Adapter) WriteImage(ctx context.Context, image entity.ImageData) error
207215 if commandErr != nil {
208216 return commandErr
209217 }
218+ if toolkitErr != nil {
219+ log.Error().Err(toolkitErr).Msg("clipboard image write failed")
220+ return toolkitErr
221+ }
210222 err := fmt.Errorf("no clipboard tool available (install wl-clipboard or xclip)")
211223 log.Error().Err(err).Msg("clipboard image write failed")
212224 return err
@@ -150,6 +150,7 @@ func selectSystemClipboard() (copyCmd, pasteCmd string) {
150 func (a *Adapter) WriteText(ctx context.Context, text string) error {150 func (a *Adapter) WriteText(ctx context.Context, text string) error {
151 log := logging.FromContext(ctx)151 log := logging.FromContext(ctx)
152 var commandErr error152 var commandErr error
153+ var toolkitErr error
153 if a.copyCmd != "" {154 if a.copyCmd != "" {
154 err := a.writeTextWithCommand(ctx, text, log)155 err := a.writeTextWithCommand(ctx, text, log)
155 if err == nil {156 if err == nil {
@@ -163,11 +164,16 @@ func (a *Adapter) WriteText(ctx context.Context, text string) error {
163 log.Debug().Str("backend", "toolkit").Int("len", len(text)).Msg("clipboard write success")164 log.Debug().Str("backend", "toolkit").Int("len", len(text)).Msg("clipboard write success")
164 return nil165 return nil
165 }166 }
167+ toolkitErr = err
166 log.Debug().Err(err).Msg("toolkit clipboard write failed; falling back")168 log.Debug().Err(err).Msg("toolkit clipboard write failed; falling back")
167 }169 }
168 if commandErr != nil {170 if commandErr != nil {
169 return commandErr171 return commandErr
170 }172 }
173+ if toolkitErr != nil {
174+ log.Error().Err(toolkitErr).Msg("clipboard write failed")
175+ return toolkitErr
176+ }
171 err := fmt.Errorf("no clipboard tool available (install wl-clipboard or xclip)")177 err := fmt.Errorf("no clipboard tool available (install wl-clipboard or xclip)")
172 log.Error().Err(err).Msg("clipboard write failed")178 log.Error().Err(err).Msg("clipboard write failed")
173 return err179 return err
@@ -179,6 +185,7 @@ func (a *Adapter) WriteText(ctx context.Context, text string) error {
179 func (a *Adapter) WriteImage(ctx context.Context, image entity.ImageData) error {185 func (a *Adapter) WriteImage(ctx context.Context, image entity.ImageData) error {
180 log := logging.FromContext(ctx)186 log := logging.FromContext(ctx)
181 var commandErr error187 var commandErr error
188+ var toolkitErr error
182 189
183 if len(image.Bytes) == 0 {190 if len(image.Bytes) == 0 {
184 err := fmt.Errorf("empty image data")191 err := fmt.Errorf("empty image data")
@@ -195,6 +202,7 @@ func (a *Adapter) WriteImage(ctx context.Context, image entity.ImageData) error
195 Msg("clipboard image write success")202 Msg("clipboard image write success")
196 return nil203 return nil
197 }204 }
205+ toolkitErr = err
198 log.Debug().Err(err).Msg("toolkit clipboard image write failed; falling back")206 log.Debug().Err(err).Msg("toolkit clipboard image write failed; falling back")
199 }207 }
200 if a.copyCmd != "" {208 if a.copyCmd != "" {
@@ -207,6 +215,10 @@ func (a *Adapter) WriteImage(ctx context.Context, image entity.ImageData) error
207 if commandErr != nil {215 if commandErr != nil {
208 return commandErr216 return commandErr
209 }217 }
218+ if toolkitErr != nil {
219+ log.Error().Err(toolkitErr).Msg("clipboard image write failed")
220+ return toolkitErr
221+ }
210 err := fmt.Errorf("no clipboard tool available (install wl-clipboard or xclip)")222 err := fmt.Errorf("no clipboard tool available (install wl-clipboard or xclip)")
211 log.Error().Err(err).Msg("clipboard image write failed")223 log.Error().Err(err).Msg("clipboard image write failed")
212 return err224 return err
M internal/infrastructure/clipboard/clipboard_test.go +28 -2
@@ -17,18 +17,44 @@ type fakeToolkitClipboard struct {
1717 writeImageCalls int
1818 text string
1919 image entity.ImageData
20+ writeTextErr error
21+ writeImageErr error
2022 }
2123
2224 func (f *fakeToolkitClipboard) WriteText(_ context.Context, text string) error {
2325 f.writeTextCalls++
2426 f.text = text
25- return nil
27+ return f.writeTextErr
2628 }
2729
2830 func (f *fakeToolkitClipboard) WriteImage(_ context.Context, image entity.ImageData) error {
2931 f.writeImageCalls++
3032 f.image = image
31- return nil
33+ return f.writeImageErr
34+}
35+
36+func TestAdapter_WriteTextReturnsToolkitErrorWhenNoSystemTool(t *testing.T) {
37+ sentinel := errors.New("toolkit text failed")
38+ fake := &fakeToolkitClipboard{writeTextErr: sentinel}
39+ adapter := &Adapter{toolkit: fake}
40+
41+ err := adapter.WriteText(context.Background(), "hello")
42+
43+ require.Equal(t, sentinel, err)
44+ require.Equal(t, 1, fake.writeTextCalls)
45+ require.Equal(t, "hello", fake.text)
46+}
47+
48+func TestAdapter_WriteImageReturnsToolkitErrorWhenNoSystemTool(t *testing.T) {
49+ sentinel := errors.New("toolkit image failed")
50+ fake := &fakeToolkitClipboard{writeImageErr: sentinel}
51+ adapter := &Adapter{toolkit: fake}
52+
53+ err := adapter.WriteImage(context.Background(), entity.ImageData{Bytes: []byte{1}, MimeType: "image/png"})
54+
55+ require.Equal(t, sentinel, err)
56+ require.Equal(t, 1, fake.writeImageCalls)
57+ require.Equal(t, entity.ImageData{Bytes: []byte{1}, MimeType: "image/png"}, fake.image)
3258 }
3359
3460 func TestNew_FallsBackToToolkitClipboardWhenSystemToolsUnavailable(t *testing.T) {
@@ -17,18 +17,44 @@ type fakeToolkitClipboard struct {
17 writeImageCalls int17 writeImageCalls int
18 text string18 text string
19 image entity.ImageData19 image entity.ImageData
20+ writeTextErr error
21+ writeImageErr error
20 }22 }
21 23
22 func (f *fakeToolkitClipboard) WriteText(_ context.Context, text string) error {24 func (f *fakeToolkitClipboard) WriteText(_ context.Context, text string) error {
23 f.writeTextCalls++25 f.writeTextCalls++
24 f.text = text26 f.text = text
25- return nil27+ return f.writeTextErr
26 }28 }
27 29
28 func (f *fakeToolkitClipboard) WriteImage(_ context.Context, image entity.ImageData) error {30 func (f *fakeToolkitClipboard) WriteImage(_ context.Context, image entity.ImageData) error {
29 f.writeImageCalls++31 f.writeImageCalls++
30 f.image = image32 f.image = image
31- return nil33+ return f.writeImageErr
34+}
35+
36+func TestAdapter_WriteTextReturnsToolkitErrorWhenNoSystemTool(t *testing.T) {
37+ sentinel := errors.New("toolkit text failed")
38+ fake := &fakeToolkitClipboard{writeTextErr: sentinel}
39+ adapter := &Adapter{toolkit: fake}
40+
41+ err := adapter.WriteText(context.Background(), "hello")
42+
43+ require.Equal(t, sentinel, err)
44+ require.Equal(t, 1, fake.writeTextCalls)
45+ require.Equal(t, "hello", fake.text)
46+}
47+
48+func TestAdapter_WriteImageReturnsToolkitErrorWhenNoSystemTool(t *testing.T) {
49+ sentinel := errors.New("toolkit image failed")
50+ fake := &fakeToolkitClipboard{writeImageErr: sentinel}
51+ adapter := &Adapter{toolkit: fake}
52+
53+ err := adapter.WriteImage(context.Background(), entity.ImageData{Bytes: []byte{1}, MimeType: "image/png"})
54+
55+ require.Equal(t, sentinel, err)
56+ require.Equal(t, 1, fake.writeImageCalls)
57+ require.Equal(t, entity.ImageData{Bytes: []byte{1}, MimeType: "image/png"}, fake.image)
32 }58 }
33 59
34 func TestNew_FallsBackToToolkitClipboardWhenSystemToolsUnavailable(t *testing.T) {60 func TestNew_FallsBackToToolkitClipboardWhenSystemToolsUnavailable(t *testing.T) {

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