detached 11eef0c

refactor(keybindings): wire use cases with gateway adapter following clean architecture

commit
11eef0c162e72dce3128e0c304b302c1835a1dab
author
brice · · 2026-01-24 14:18 UTC
parent
61db896
signature
GPG VerifiedCD88F9E564E0102646FB6077A1A07C7C7C3E349D · arch-gpg Verified for the committer email
8 files · +501 -302
M internal/application/port/keybindings.go +6 -0
@@ -46,10 +46,16 @@ type KeybindingConflict struct {
4646 Key string `json:"key"`
4747 }
4848
49+// SetKeybindingResponse is the response from setting a keybinding.
50+type SetKeybindingResponse struct {
51+ Conflicts []KeybindingConflict `json:"conflicts"`
52+}
53+
4954 // KeybindingsProvider provides keybinding configuration data.
5055 type KeybindingsProvider interface {
5156 GetKeybindings(ctx context.Context) (KeybindingsConfig, error)
5257 GetDefaultKeybindings(ctx context.Context) (KeybindingsConfig, error)
58+ CheckConflicts(ctx context.Context, mode, action string, keys []string) ([]KeybindingConflict, error)
5359 }
5460
5561 // KeybindingsSaver persists keybinding changes.
@@ -46,10 +46,16 @@ type KeybindingConflict struct {
46 Key string `json:"key"`46 Key string `json:"key"`
47 }47 }
48 48
49+// SetKeybindingResponse is the response from setting a keybinding.
50+type SetKeybindingResponse struct {
51+ Conflicts []KeybindingConflict `json:"conflicts"`
52+}
53+
49 // KeybindingsProvider provides keybinding configuration data.54 // KeybindingsProvider provides keybinding configuration data.
50 type KeybindingsProvider interface {55 type KeybindingsProvider interface {
51 GetKeybindings(ctx context.Context) (KeybindingsConfig, error)56 GetKeybindings(ctx context.Context) (KeybindingsConfig, error)
52 GetDefaultKeybindings(ctx context.Context) (KeybindingsConfig, error)57 GetDefaultKeybindings(ctx context.Context) (KeybindingsConfig, error)
58+ CheckConflicts(ctx context.Context, mode, action string, keys []string) ([]KeybindingConflict, error)
53 }59 }
54 60
55 // KeybindingsSaver persists keybinding changes.61 // KeybindingsSaver persists keybinding changes.
M internal/application/port/mocks/mocks.go +80 -0
@@ -1589,6 +1589,86 @@ func (_m *MockKeybindingsProvider) EXPECT() *MockKeybindingsProvider_Expecter {
15891589 return &MockKeybindingsProvider_Expecter{mock: &_m.Mock}
15901590 }
15911591
1592+// CheckConflicts provides a mock function for the type MockKeybindingsProvider
1593+func (_mock *MockKeybindingsProvider) CheckConflicts(ctx context.Context, mode string, action string, keys []string) ([]port.KeybindingConflict, error) {
1594+ ret := _mock.Called(ctx, mode, action, keys)
1595+
1596+ if len(ret) == 0 {
1597+ panic("no return value specified for CheckConflicts")
1598+ }
1599+
1600+ var r0 []port.KeybindingConflict
1601+ var r1 error
1602+ if returnFunc, ok := ret.Get(0).(func(context.Context, string, string, []string) ([]port.KeybindingConflict, error)); ok {
1603+ return returnFunc(ctx, mode, action, keys)
1604+ }
1605+ if returnFunc, ok := ret.Get(0).(func(context.Context, string, string, []string) []port.KeybindingConflict); ok {
1606+ r0 = returnFunc(ctx, mode, action, keys)
1607+ } else {
1608+ if ret.Get(0) != nil {
1609+ r0 = ret.Get(0).([]port.KeybindingConflict)
1610+ }
1611+ }
1612+ if returnFunc, ok := ret.Get(1).(func(context.Context, string, string, []string) error); ok {
1613+ r1 = returnFunc(ctx, mode, action, keys)
1614+ } else {
1615+ r1 = ret.Error(1)
1616+ }
1617+ return r0, r1
1618+}
1619+
1620+// MockKeybindingsProvider_CheckConflicts_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'CheckConflicts'
1621+type MockKeybindingsProvider_CheckConflicts_Call struct {
1622+ *mock.Call
1623+}
1624+
1625+// CheckConflicts is a helper method to define mock.On call
1626+// - ctx context.Context
1627+// - mode string
1628+// - action string
1629+// - keys []string
1630+func (_e *MockKeybindingsProvider_Expecter) CheckConflicts(ctx interface{}, mode interface{}, action interface{}, keys interface{}) *MockKeybindingsProvider_CheckConflicts_Call {
1631+ return &MockKeybindingsProvider_CheckConflicts_Call{Call: _e.mock.On("CheckConflicts", ctx, mode, action, keys)}
1632+}
1633+
1634+func (_c *MockKeybindingsProvider_CheckConflicts_Call) Run(run func(ctx context.Context, mode string, action string, keys []string)) *MockKeybindingsProvider_CheckConflicts_Call {
1635+ _c.Call.Run(func(args mock.Arguments) {
1636+ var arg0 context.Context
1637+ if args[0] != nil {
1638+ arg0 = args[0].(context.Context)
1639+ }
1640+ var arg1 string
1641+ if args[1] != nil {
1642+ arg1 = args[1].(string)
1643+ }
1644+ var arg2 string
1645+ if args[2] != nil {
1646+ arg2 = args[2].(string)
1647+ }
1648+ var arg3 []string
1649+ if args[3] != nil {
1650+ arg3 = args[3].([]string)
1651+ }
1652+ run(
1653+ arg0,
1654+ arg1,
1655+ arg2,
1656+ arg3,
1657+ )
1658+ })
1659+ return _c
1660+}
1661+
1662+func (_c *MockKeybindingsProvider_CheckConflicts_Call) Return(keybindingConflicts []port.KeybindingConflict, err error) *MockKeybindingsProvider_CheckConflicts_Call {
1663+ _c.Call.Return(keybindingConflicts, err)
1664+ return _c
1665+}
1666+
1667+func (_c *MockKeybindingsProvider_CheckConflicts_Call) RunAndReturn(run func(ctx context.Context, mode string, action string, keys []string) ([]port.KeybindingConflict, error)) *MockKeybindingsProvider_CheckConflicts_Call {
1668+ _c.Call.Return(run)
1669+ return _c
1670+}
1671+
15921672 // GetDefaultKeybindings provides a mock function for the type MockKeybindingsProvider
15931673 func (_mock *MockKeybindingsProvider) GetDefaultKeybindings(ctx context.Context) (port.KeybindingsConfig, error) {
15941674 ret := _mock.Called(ctx)
@@ -1589,6 +1589,86 @@ func (_m *MockKeybindingsProvider) EXPECT() *MockKeybindingsProvider_Expecter {
1589 return &MockKeybindingsProvider_Expecter{mock: &_m.Mock}1589 return &MockKeybindingsProvider_Expecter{mock: &_m.Mock}
1590 }1590 }
1591 1591
1592+// CheckConflicts provides a mock function for the type MockKeybindingsProvider
1593+func (_mock *MockKeybindingsProvider) CheckConflicts(ctx context.Context, mode string, action string, keys []string) ([]port.KeybindingConflict, error) {
1594+ ret := _mock.Called(ctx, mode, action, keys)
1595+
1596+ if len(ret) == 0 {
1597+ panic("no return value specified for CheckConflicts")
1598+ }
1599+
1600+ var r0 []port.KeybindingConflict
1601+ var r1 error
1602+ if returnFunc, ok := ret.Get(0).(func(context.Context, string, string, []string) ([]port.KeybindingConflict, error)); ok {
1603+ return returnFunc(ctx, mode, action, keys)
1604+ }
1605+ if returnFunc, ok := ret.Get(0).(func(context.Context, string, string, []string) []port.KeybindingConflict); ok {
1606+ r0 = returnFunc(ctx, mode, action, keys)
1607+ } else {
1608+ if ret.Get(0) != nil {
1609+ r0 = ret.Get(0).([]port.KeybindingConflict)
1610+ }
1611+ }
1612+ if returnFunc, ok := ret.Get(1).(func(context.Context, string, string, []string) error); ok {
1613+ r1 = returnFunc(ctx, mode, action, keys)
1614+ } else {
1615+ r1 = ret.Error(1)
1616+ }
1617+ return r0, r1
1618+}
1619+
1620+// MockKeybindingsProvider_CheckConflicts_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'CheckConflicts'
1621+type MockKeybindingsProvider_CheckConflicts_Call struct {
1622+ *mock.Call
1623+}
1624+
1625+// CheckConflicts is a helper method to define mock.On call
1626+// - ctx context.Context
1627+// - mode string
1628+// - action string
1629+// - keys []string
1630+func (_e *MockKeybindingsProvider_Expecter) CheckConflicts(ctx interface{}, mode interface{}, action interface{}, keys interface{}) *MockKeybindingsProvider_CheckConflicts_Call {
1631+ return &MockKeybindingsProvider_CheckConflicts_Call{Call: _e.mock.On("CheckConflicts", ctx, mode, action, keys)}
1632+}
1633+
1634+func (_c *MockKeybindingsProvider_CheckConflicts_Call) Run(run func(ctx context.Context, mode string, action string, keys []string)) *MockKeybindingsProvider_CheckConflicts_Call {
1635+ _c.Call.Run(func(args mock.Arguments) {
1636+ var arg0 context.Context
1637+ if args[0] != nil {
1638+ arg0 = args[0].(context.Context)
1639+ }
1640+ var arg1 string
1641+ if args[1] != nil {
1642+ arg1 = args[1].(string)
1643+ }
1644+ var arg2 string
1645+ if args[2] != nil {
1646+ arg2 = args[2].(string)
1647+ }
1648+ var arg3 []string
1649+ if args[3] != nil {
1650+ arg3 = args[3].([]string)
1651+ }
1652+ run(
1653+ arg0,
1654+ arg1,
1655+ arg2,
1656+ arg3,
1657+ )
1658+ })
1659+ return _c
1660+}
1661+
1662+func (_c *MockKeybindingsProvider_CheckConflicts_Call) Return(keybindingConflicts []port.KeybindingConflict, err error) *MockKeybindingsProvider_CheckConflicts_Call {
1663+ _c.Call.Return(keybindingConflicts, err)
1664+ return _c
1665+}
1666+
1667+func (_c *MockKeybindingsProvider_CheckConflicts_Call) RunAndReturn(run func(ctx context.Context, mode string, action string, keys []string) ([]port.KeybindingConflict, error)) *MockKeybindingsProvider_CheckConflicts_Call {
1668+ _c.Call.Return(run)
1669+ return _c
1670+}
1671+
1592 // GetDefaultKeybindings provides a mock function for the type MockKeybindingsProvider1672 // GetDefaultKeybindings provides a mock function for the type MockKeybindingsProvider
1593 func (_mock *MockKeybindingsProvider) GetDefaultKeybindings(ctx context.Context) (port.KeybindingsConfig, error) {1673 func (_mock *MockKeybindingsProvider) GetDefaultKeybindings(ctx context.Context) (port.KeybindingsConfig, error) {
1594 ret := _mock.Called(ctx)1674 ret := _mock.Called(ctx)
M internal/application/usecase/keybindings.go +23 -8
@@ -36,25 +36,40 @@ func (uc *GetKeybindingsUseCase) Execute(ctx context.Context) (port.KeybindingsC
3636
3737 // SetKeybindingUseCase updates a single keybinding.
3838 type SetKeybindingUseCase struct {
39- saver port.KeybindingsSaver
39+ provider port.KeybindingsProvider
40+ saver port.KeybindingsSaver
4041 }
4142
4243 // NewSetKeybindingUseCase creates a new SetKeybindingUseCase.
43-func NewSetKeybindingUseCase(saver port.KeybindingsSaver) *SetKeybindingUseCase {
44- return &SetKeybindingUseCase{saver: saver}
44+func NewSetKeybindingUseCase(provider port.KeybindingsProvider, saver port.KeybindingsSaver) *SetKeybindingUseCase {
45+ return &SetKeybindingUseCase{provider: provider, saver: saver}
4546 }
4647
47-// Execute updates a keybinding.
48-func (uc *SetKeybindingUseCase) Execute(ctx context.Context, req port.SetKeybindingRequest) error {
48+// Execute updates a keybinding and returns any conflicts.
49+func (uc *SetKeybindingUseCase) Execute(ctx context.Context, req port.SetKeybindingRequest) (port.SetKeybindingResponse, error) {
4950 if uc == nil || uc.saver == nil {
50- return fmt.Errorf("keybindings saver is nil")
51+ return port.SetKeybindingResponse{}, fmt.Errorf("keybindings saver is nil")
5152 }
5253
5354 if err := validateSetKeybindingRequest(req); err != nil {
54- return err
55+ return port.SetKeybindingResponse{}, err
56+ }
57+
58+ // Check for conflicts before saving
59+ var conflicts []port.KeybindingConflict
60+ if uc.provider != nil {
61+ var err error
62+ conflicts, err = uc.provider.CheckConflicts(ctx, req.Mode, req.Action, req.Keys)
63+ if err != nil {
64+ return port.SetKeybindingResponse{}, fmt.Errorf("failed to check conflicts: %w", err)
65+ }
66+ }
67+
68+ if err := uc.saver.SetKeybinding(ctx, req); err != nil {
69+ return port.SetKeybindingResponse{}, err
5570 }
5671
57- return uc.saver.SetKeybinding(ctx, req)
72+ return port.SetKeybindingResponse{Conflicts: conflicts}, nil
5873 }
5974
6075 // ResetKeybindingUseCase resets a keybinding to default.
@@ -36,25 +36,40 @@ func (uc *GetKeybindingsUseCase) Execute(ctx context.Context) (port.KeybindingsC
36 36
37 // SetKeybindingUseCase updates a single keybinding.37 // SetKeybindingUseCase updates a single keybinding.
38 type SetKeybindingUseCase struct {38 type SetKeybindingUseCase struct {
39- saver port.KeybindingsSaver39+ provider port.KeybindingsProvider
40+ saver port.KeybindingsSaver
40 }41 }
41 42
42 // NewSetKeybindingUseCase creates a new SetKeybindingUseCase.43 // NewSetKeybindingUseCase creates a new SetKeybindingUseCase.
43-func NewSetKeybindingUseCase(saver port.KeybindingsSaver) *SetKeybindingUseCase {44+func NewSetKeybindingUseCase(provider port.KeybindingsProvider, saver port.KeybindingsSaver) *SetKeybindingUseCase {
44- return &SetKeybindingUseCase{saver: saver}45+ return &SetKeybindingUseCase{provider: provider, saver: saver}
45 }46 }
46 47
47-// Execute updates a keybinding.48+// Execute updates a keybinding and returns any conflicts.
48-func (uc *SetKeybindingUseCase) Execute(ctx context.Context, req port.SetKeybindingRequest) error {49+func (uc *SetKeybindingUseCase) Execute(ctx context.Context, req port.SetKeybindingRequest) (port.SetKeybindingResponse, error) {
49 if uc == nil || uc.saver == nil {50 if uc == nil || uc.saver == nil {
50- return fmt.Errorf("keybindings saver is nil")51+ return port.SetKeybindingResponse{}, fmt.Errorf("keybindings saver is nil")
51 }52 }
52 53
53 if err := validateSetKeybindingRequest(req); err != nil {54 if err := validateSetKeybindingRequest(req); err != nil {
54- return err55+ return port.SetKeybindingResponse{}, err
56+ }
57+
58+ // Check for conflicts before saving
59+ var conflicts []port.KeybindingConflict
60+ if uc.provider != nil {
61+ var err error
62+ conflicts, err = uc.provider.CheckConflicts(ctx, req.Mode, req.Action, req.Keys)
63+ if err != nil {
64+ return port.SetKeybindingResponse{}, fmt.Errorf("failed to check conflicts: %w", err)
65+ }
66+ }
67+
68+ if err := uc.saver.SetKeybinding(ctx, req); err != nil {
69+ return port.SetKeybindingResponse{}, err
55 }70 }
56 71
57- return uc.saver.SetKeybinding(ctx, req)72+ return port.SetKeybindingResponse{Conflicts: conflicts}, nil
58 }73 }
59 74
60 // ResetKeybindingUseCase resets a keybinding to default.75 // ResetKeybindingUseCase resets a keybinding to default.
M internal/application/usecase/keybindings_test.go +48 -14
@@ -74,48 +74,79 @@ func TestGetKeybindingsUseCase_Execute(t *testing.T) {
7474 func TestSetKeybindingUseCase_Execute(t *testing.T) {
7575 t.Run("successfully sets keybinding", func(t *testing.T) {
7676 // Arrange
77+ mockProvider := mocks.NewMockKeybindingsProvider(t)
7778 mockSaver := mocks.NewMockKeybindingsSaver(t)
7879 req := port.SetKeybindingRequest{
7980 Mode: "pane",
8081 Action: "split-right",
8182 Keys: []string{"r"},
8283 }
84+ mockProvider.EXPECT().CheckConflicts(mock.Anything, req.Mode, req.Action, req.Keys).Return(nil, nil)
8385 mockSaver.EXPECT().SetKeybinding(mock.Anything, req).Return(nil)
8486
85- uc := usecase.NewSetKeybindingUseCase(mockSaver)
87+ uc := usecase.NewSetKeybindingUseCase(mockProvider, mockSaver)
8688
8789 // Act
88- err := uc.Execute(context.Background(), req)
90+ resp, err := uc.Execute(context.Background(), req)
8991
9092 // Assert
9193 require.NoError(t, err)
92- mock.AssertExpectationsForObjects(t, mockSaver)
94+ assert.Empty(t, resp.Conflicts)
95+ mock.AssertExpectationsForObjects(t, mockProvider, mockSaver)
96+ })
97+
98+ t.Run("returns conflicts when detected", func(t *testing.T) {
99+ // Arrange
100+ mockProvider := mocks.NewMockKeybindingsProvider(t)
101+ mockSaver := mocks.NewMockKeybindingsSaver(t)
102+ req := port.SetKeybindingRequest{
103+ Mode: "pane",
104+ Action: "split-right",
105+ Keys: []string{"r"},
106+ }
107+ conflicts := []port.KeybindingConflict{
108+ {ConflictingAction: "other-action", ConflictingMode: "pane", Key: "r"},
109+ }
110+ mockProvider.EXPECT().CheckConflicts(mock.Anything, req.Mode, req.Action, req.Keys).Return(conflicts, nil)
111+ mockSaver.EXPECT().SetKeybinding(mock.Anything, req).Return(nil)
112+
113+ uc := usecase.NewSetKeybindingUseCase(mockProvider, mockSaver)
114+
115+ // Act
116+ resp, err := uc.Execute(context.Background(), req)
117+
118+ // Assert
119+ require.NoError(t, err)
120+ assert.Equal(t, conflicts, resp.Conflicts)
121+ mock.AssertExpectationsForObjects(t, mockProvider, mockSaver)
93122 })
94123
95124 t.Run("returns error when saver fails", func(t *testing.T) {
96125 // Arrange
126+ mockProvider := mocks.NewMockKeybindingsProvider(t)
97127 mockSaver := mocks.NewMockKeybindingsSaver(t)
98128 req := port.SetKeybindingRequest{
99129 Mode: "pane",
100130 Action: "split-right",
101131 Keys: []string{"r"},
102132 }
133+ mockProvider.EXPECT().CheckConflicts(mock.Anything, req.Mode, req.Action, req.Keys).Return(nil, nil)
103134 mockSaver.EXPECT().SetKeybinding(mock.Anything, req).Return(errors.New("save error"))
104135
105- uc := usecase.NewSetKeybindingUseCase(mockSaver)
136+ uc := usecase.NewSetKeybindingUseCase(mockProvider, mockSaver)
106137
107138 // Act
108- err := uc.Execute(context.Background(), req)
139+ _, err := uc.Execute(context.Background(), req)
109140
110141 // Assert
111142 require.Error(t, err)
112143 assert.Contains(t, err.Error(), "save error")
113- mock.AssertExpectationsForObjects(t, mockSaver)
144+ mock.AssertExpectationsForObjects(t, mockProvider, mockSaver)
114145 })
115146
116147 t.Run("returns error when saver is nil", func(t *testing.T) {
117148 // Arrange
118- uc := usecase.NewSetKeybindingUseCase(nil)
149+ uc := usecase.NewSetKeybindingUseCase(nil, nil)
119150 req := port.SetKeybindingRequest{
120151 Mode: "pane",
121152 Action: "split-right",
@@ -123,7 +154,7 @@ func TestSetKeybindingUseCase_Execute(t *testing.T) {
123154 }
124155
125156 // Act
126- err := uc.Execute(context.Background(), req)
157+ _, err := uc.Execute(context.Background(), req)
127158
128159 // Assert
129160 require.Error(t, err)
@@ -132,6 +163,7 @@ func TestSetKeybindingUseCase_Execute(t *testing.T) {
132163
133164 t.Run("returns error when mode is empty", func(t *testing.T) {
134165 // Arrange
166+ mockProvider := mocks.NewMockKeybindingsProvider(t)
135167 mockSaver := mocks.NewMockKeybindingsSaver(t)
136168 req := port.SetKeybindingRequest{
137169 Mode: "",
@@ -139,10 +171,10 @@ func TestSetKeybindingUseCase_Execute(t *testing.T) {
139171 Keys: []string{"r"},
140172 }
141173
142- uc := usecase.NewSetKeybindingUseCase(mockSaver)
174+ uc := usecase.NewSetKeybindingUseCase(mockProvider, mockSaver)
143175
144176 // Act
145- err := uc.Execute(context.Background(), req)
177+ _, err := uc.Execute(context.Background(), req)
146178
147179 // Assert
148180 require.Error(t, err)
@@ -151,6 +183,7 @@ func TestSetKeybindingUseCase_Execute(t *testing.T) {
151183
152184 t.Run("returns error when action is empty", func(t *testing.T) {
153185 // Arrange
186+ mockProvider := mocks.NewMockKeybindingsProvider(t)
154187 mockSaver := mocks.NewMockKeybindingsSaver(t)
155188 req := port.SetKeybindingRequest{
156189 Mode: "pane",
@@ -158,10 +191,10 @@ func TestSetKeybindingUseCase_Execute(t *testing.T) {
158191 Keys: []string{"r"},
159192 }
160193
161- uc := usecase.NewSetKeybindingUseCase(mockSaver)
194+ uc := usecase.NewSetKeybindingUseCase(mockProvider, mockSaver)
162195
163196 // Act
164- err := uc.Execute(context.Background(), req)
197+ _, err := uc.Execute(context.Background(), req)
165198
166199 // Assert
167200 require.Error(t, err)
@@ -170,6 +203,7 @@ func TestSetKeybindingUseCase_Execute(t *testing.T) {
170203
171204 t.Run("returns error for invalid mode", func(t *testing.T) {
172205 // Arrange
206+ mockProvider := mocks.NewMockKeybindingsProvider(t)
173207 mockSaver := mocks.NewMockKeybindingsSaver(t)
174208 req := port.SetKeybindingRequest{
175209 Mode: "invalid",
@@ -177,10 +211,10 @@ func TestSetKeybindingUseCase_Execute(t *testing.T) {
177211 Keys: []string{"r"},
178212 }
179213
180- uc := usecase.NewSetKeybindingUseCase(mockSaver)
214+ uc := usecase.NewSetKeybindingUseCase(mockProvider, mockSaver)
181215
182216 // Act
183- err := uc.Execute(context.Background(), req)
217+ _, err := uc.Execute(context.Background(), req)
184218
185219 // Assert
186220 require.Error(t, err)
@@ -74,48 +74,79 @@ func TestGetKeybindingsUseCase_Execute(t *testing.T) {
74 func TestSetKeybindingUseCase_Execute(t *testing.T) {74 func TestSetKeybindingUseCase_Execute(t *testing.T) {
75 t.Run("successfully sets keybinding", func(t *testing.T) {75 t.Run("successfully sets keybinding", func(t *testing.T) {
76 // Arrange76 // Arrange
77+ mockProvider := mocks.NewMockKeybindingsProvider(t)
77 mockSaver := mocks.NewMockKeybindingsSaver(t)78 mockSaver := mocks.NewMockKeybindingsSaver(t)
78 req := port.SetKeybindingRequest{79 req := port.SetKeybindingRequest{
79 Mode: "pane",80 Mode: "pane",
80 Action: "split-right",81 Action: "split-right",
81 Keys: []string{"r"},82 Keys: []string{"r"},
82 }83 }
84+ mockProvider.EXPECT().CheckConflicts(mock.Anything, req.Mode, req.Action, req.Keys).Return(nil, nil)
83 mockSaver.EXPECT().SetKeybinding(mock.Anything, req).Return(nil)85 mockSaver.EXPECT().SetKeybinding(mock.Anything, req).Return(nil)
84 86
85- uc := usecase.NewSetKeybindingUseCase(mockSaver)87+ uc := usecase.NewSetKeybindingUseCase(mockProvider, mockSaver)
86 88
87 // Act89 // Act
88- err := uc.Execute(context.Background(), req)90+ resp, err := uc.Execute(context.Background(), req)
89 91
90 // Assert92 // Assert
91 require.NoError(t, err)93 require.NoError(t, err)
92- mock.AssertExpectationsForObjects(t, mockSaver)94+ assert.Empty(t, resp.Conflicts)
95+ mock.AssertExpectationsForObjects(t, mockProvider, mockSaver)
96+ })
97+
98+ t.Run("returns conflicts when detected", func(t *testing.T) {
99+ // Arrange
100+ mockProvider := mocks.NewMockKeybindingsProvider(t)
101+ mockSaver := mocks.NewMockKeybindingsSaver(t)
102+ req := port.SetKeybindingRequest{
103+ Mode: "pane",
104+ Action: "split-right",
105+ Keys: []string{"r"},
106+ }
107+ conflicts := []port.KeybindingConflict{
108+ {ConflictingAction: "other-action", ConflictingMode: "pane", Key: "r"},
109+ }
110+ mockProvider.EXPECT().CheckConflicts(mock.Anything, req.Mode, req.Action, req.Keys).Return(conflicts, nil)
111+ mockSaver.EXPECT().SetKeybinding(mock.Anything, req).Return(nil)
112+
113+ uc := usecase.NewSetKeybindingUseCase(mockProvider, mockSaver)
114+
115+ // Act
116+ resp, err := uc.Execute(context.Background(), req)
117+
118+ // Assert
119+ require.NoError(t, err)
120+ assert.Equal(t, conflicts, resp.Conflicts)
121+ mock.AssertExpectationsForObjects(t, mockProvider, mockSaver)
93 })122 })
94 123
95 t.Run("returns error when saver fails", func(t *testing.T) {124 t.Run("returns error when saver fails", func(t *testing.T) {
96 // Arrange125 // Arrange
126+ mockProvider := mocks.NewMockKeybindingsProvider(t)
97 mockSaver := mocks.NewMockKeybindingsSaver(t)127 mockSaver := mocks.NewMockKeybindingsSaver(t)
98 req := port.SetKeybindingRequest{128 req := port.SetKeybindingRequest{
99 Mode: "pane",129 Mode: "pane",
100 Action: "split-right",130 Action: "split-right",
101 Keys: []string{"r"},131 Keys: []string{"r"},
102 }132 }
133+ mockProvider.EXPECT().CheckConflicts(mock.Anything, req.Mode, req.Action, req.Keys).Return(nil, nil)
103 mockSaver.EXPECT().SetKeybinding(mock.Anything, req).Return(errors.New("save error"))134 mockSaver.EXPECT().SetKeybinding(mock.Anything, req).Return(errors.New("save error"))
104 135
105- uc := usecase.NewSetKeybindingUseCase(mockSaver)136+ uc := usecase.NewSetKeybindingUseCase(mockProvider, mockSaver)
106 137
107 // Act138 // Act
108- err := uc.Execute(context.Background(), req)139+ _, err := uc.Execute(context.Background(), req)
109 140
110 // Assert141 // Assert
111 require.Error(t, err)142 require.Error(t, err)
112 assert.Contains(t, err.Error(), "save error")143 assert.Contains(t, err.Error(), "save error")
113- mock.AssertExpectationsForObjects(t, mockSaver)144+ mock.AssertExpectationsForObjects(t, mockProvider, mockSaver)
114 })145 })
115 146
116 t.Run("returns error when saver is nil", func(t *testing.T) {147 t.Run("returns error when saver is nil", func(t *testing.T) {
117 // Arrange148 // Arrange
118- uc := usecase.NewSetKeybindingUseCase(nil)149+ uc := usecase.NewSetKeybindingUseCase(nil, nil)
119 req := port.SetKeybindingRequest{150 req := port.SetKeybindingRequest{
120 Mode: "pane",151 Mode: "pane",
121 Action: "split-right",152 Action: "split-right",
@@ -123,7 +154,7 @@ func TestSetKeybindingUseCase_Execute(t *testing.T) {
123 }154 }
124 155
125 // Act156 // Act
126- err := uc.Execute(context.Background(), req)157+ _, err := uc.Execute(context.Background(), req)
127 158
128 // Assert159 // Assert
129 require.Error(t, err)160 require.Error(t, err)
@@ -132,6 +163,7 @@ func TestSetKeybindingUseCase_Execute(t *testing.T) {
132 163
133 t.Run("returns error when mode is empty", func(t *testing.T) {164 t.Run("returns error when mode is empty", func(t *testing.T) {
134 // Arrange165 // Arrange
166+ mockProvider := mocks.NewMockKeybindingsProvider(t)
135 mockSaver := mocks.NewMockKeybindingsSaver(t)167 mockSaver := mocks.NewMockKeybindingsSaver(t)
136 req := port.SetKeybindingRequest{168 req := port.SetKeybindingRequest{
137 Mode: "",169 Mode: "",
@@ -139,10 +171,10 @@ func TestSetKeybindingUseCase_Execute(t *testing.T) {
139 Keys: []string{"r"},171 Keys: []string{"r"},
140 }172 }
141 173
142- uc := usecase.NewSetKeybindingUseCase(mockSaver)174+ uc := usecase.NewSetKeybindingUseCase(mockProvider, mockSaver)
143 175
144 // Act176 // Act
145- err := uc.Execute(context.Background(), req)177+ _, err := uc.Execute(context.Background(), req)
146 178
147 // Assert179 // Assert
148 require.Error(t, err)180 require.Error(t, err)
@@ -151,6 +183,7 @@ func TestSetKeybindingUseCase_Execute(t *testing.T) {
151 183
152 t.Run("returns error when action is empty", func(t *testing.T) {184 t.Run("returns error when action is empty", func(t *testing.T) {
153 // Arrange185 // Arrange
186+ mockProvider := mocks.NewMockKeybindingsProvider(t)
154 mockSaver := mocks.NewMockKeybindingsSaver(t)187 mockSaver := mocks.NewMockKeybindingsSaver(t)
155 req := port.SetKeybindingRequest{188 req := port.SetKeybindingRequest{
156 Mode: "pane",189 Mode: "pane",
@@ -158,10 +191,10 @@ func TestSetKeybindingUseCase_Execute(t *testing.T) {
158 Keys: []string{"r"},191 Keys: []string{"r"},
159 }192 }
160 193
161- uc := usecase.NewSetKeybindingUseCase(mockSaver)194+ uc := usecase.NewSetKeybindingUseCase(mockProvider, mockSaver)
162 195
163 // Act196 // Act
164- err := uc.Execute(context.Background(), req)197+ _, err := uc.Execute(context.Background(), req)
165 198
166 // Assert199 // Assert
167 require.Error(t, err)200 require.Error(t, err)
@@ -170,6 +203,7 @@ func TestSetKeybindingUseCase_Execute(t *testing.T) {
170 203
171 t.Run("returns error for invalid mode", func(t *testing.T) {204 t.Run("returns error for invalid mode", func(t *testing.T) {
172 // Arrange205 // Arrange
206+ mockProvider := mocks.NewMockKeybindingsProvider(t)
173 mockSaver := mocks.NewMockKeybindingsSaver(t)207 mockSaver := mocks.NewMockKeybindingsSaver(t)
174 req := port.SetKeybindingRequest{208 req := port.SetKeybindingRequest{
175 Mode: "invalid",209 Mode: "invalid",
@@ -177,10 +211,10 @@ func TestSetKeybindingUseCase_Execute(t *testing.T) {
177 Keys: []string{"r"},211 Keys: []string{"r"},
178 }212 }
179 213
180- uc := usecase.NewSetKeybindingUseCase(mockSaver)214+ uc := usecase.NewSetKeybindingUseCase(mockProvider, mockSaver)
181 215
182 // Act216 // Act
183- err := uc.Execute(context.Background(), req)217+ _, err := uc.Execute(context.Background(), req)
184 218
185 // Assert219 // Assert
186 require.Error(t, err)220 require.Error(t, err)
A internal/infrastructure/config/keybindings_gateway.go +285 -0
@@ -0,0 +1,285 @@
1+package config
2+
3+import (
4+ "context"
5+ "reflect"
6+ "sort"
7+
8+ "github.com/bnema/dumber/internal/application/port"
9+ "github.com/bnema/dumber/internal/logging"
10+)
11+
12+const modeGlobal = "global"
13+
14+// KeybindingsGateway implements port.KeybindingsProvider and port.KeybindingsSaver.
15+type KeybindingsGateway struct {
16+ mgr *Manager
17+}
18+
19+// NewKeybindingsGateway creates a new KeybindingsGateway.
20+func NewKeybindingsGateway(mgr *Manager) *KeybindingsGateway {
21+ return &KeybindingsGateway{mgr: mgr}
22+}
23+
24+// GetKeybindings returns all keybindings grouped by mode.
25+func (g *KeybindingsGateway) GetKeybindings(ctx context.Context) (port.KeybindingsConfig, error) {
26+ log := logging.FromContext(ctx)
27+ cfg := g.mgr.Get()
28+ defaults := DefaultConfig()
29+ log.Debug().Msg("keybindings gateway: fetching keybindings")
30+
31+ groups := []port.KeybindingGroup{
32+ g.buildGlobalGroup(cfg, defaults),
33+ g.buildPaneModeGroup(cfg, defaults),
34+ g.buildTabModeGroup(cfg, defaults),
35+ g.buildResizeModeGroup(cfg, defaults),
36+ g.buildSessionModeGroup(cfg, defaults),
37+ }
38+
39+ return port.KeybindingsConfig{Groups: groups}, nil
40+}
41+
42+// GetDefaultKeybindings returns the default keybindings configuration.
43+func (g *KeybindingsGateway) GetDefaultKeybindings(ctx context.Context) (port.KeybindingsConfig, error) {
44+ log := logging.FromContext(ctx)
45+ defaults := DefaultConfig()
46+
47+ groups := []port.KeybindingGroup{
48+ g.buildGlobalGroup(defaults, defaults),
49+ g.buildPaneModeGroup(defaults, defaults),
50+ g.buildTabModeGroup(defaults, defaults),
51+ g.buildResizeModeGroup(defaults, defaults),
52+ g.buildSessionModeGroup(defaults, defaults),
53+ }
54+
55+ log.Debug().Int("groups", len(groups)).Msg("keybindings gateway: returning default keybindings")
56+ return port.KeybindingsConfig{Groups: groups}, nil
57+}
58+
59+// CheckConflicts scans all modes for keybinding conflicts.
60+func (g *KeybindingsGateway) CheckConflicts(ctx context.Context, mode, action string, keys []string) ([]port.KeybindingConflict, error) {
61+ log := logging.FromContext(ctx)
62+ cfg := g.mgr.Get()
63+
64+ conflicts := g.checkConflicts(cfg, mode, action, keys)
65+ if len(conflicts) > 0 {
66+ log.Warn().Int("conflicts", len(conflicts)).Msg("keybindings gateway: conflicts detected")
67+ }
68+
69+ return conflicts, nil
70+}
71+
72+// SetKeybinding updates a single keybinding.
73+func (g *KeybindingsGateway) SetKeybinding(ctx context.Context, req port.SetKeybindingRequest) error {
74+ log := logging.FromContext(ctx)
75+ log.Debug().Str("mode", req.Mode).Str("action", req.Action).Msg("keybindings gateway: setting keybinding")
76+ cfg := g.mgr.Get()
77+ g.updateKeybinding(cfg, req)
78+ return g.mgr.Save(cfg)
79+}
80+
81+// ResetKeybinding resets a keybinding to its default value.
82+func (g *KeybindingsGateway) ResetKeybinding(ctx context.Context, req port.ResetKeybindingRequest) error {
83+ log := logging.FromContext(ctx)
84+ log.Info().Str("mode", req.Mode).Str("action", req.Action).Msg("keybindings gateway: resetting keybinding to default")
85+
86+ defaults := DefaultConfig()
87+ defaultKeys := g.getDefaultKeys(defaults, req.Mode, req.Action)
88+
89+ setReq := port.SetKeybindingRequest{
90+ Mode: req.Mode,
91+ Action: req.Action,
92+ Keys: defaultKeys,
93+ }
94+
95+ cfg := g.mgr.Get()
96+ g.updateKeybinding(cfg, setReq)
97+
98+ return g.mgr.Save(cfg)
99+}
100+
101+// ResetAllKeybindings resets all keybindings to their default values.
102+func (g *KeybindingsGateway) ResetAllKeybindings(ctx context.Context) error {
103+ log := logging.FromContext(ctx)
104+ log.Info().Msg("keybindings gateway: resetting all keybindings to defaults")
105+
106+ cfg := g.mgr.Get()
107+ defaults := DefaultConfig()
108+
109+ cfg.Workspace.PaneMode.Actions = defaults.Workspace.PaneMode.Actions
110+ cfg.Workspace.TabMode.Actions = defaults.Workspace.TabMode.Actions
111+ cfg.Workspace.ResizeMode.Actions = defaults.Workspace.ResizeMode.Actions
112+ cfg.Workspace.Shortcuts = defaults.Workspace.Shortcuts
113+ cfg.Session.SessionMode.Actions = defaults.Session.SessionMode.Actions
114+
115+ return g.mgr.Save(cfg)
116+}
117+
118+// buildGlobalGroup builds the global shortcuts group.
119+func (g *KeybindingsGateway) buildGlobalGroup(cfg, defaults *Config) port.KeybindingGroup {
120+ return port.KeybindingGroup{
121+ Mode: modeGlobal,
122+ DisplayName: "Global Shortcuts",
123+ Bindings: g.buildModeBindings(cfg.Workspace.Shortcuts.Actions, defaults.Workspace.Shortcuts.Actions),
124+ }
125+}
126+
127+// buildPaneModeGroup builds the pane mode group.
128+func (g *KeybindingsGateway) buildPaneModeGroup(cfg, defaults *Config) port.KeybindingGroup {
129+ return port.KeybindingGroup{
130+ Mode: "pane",
131+ DisplayName: "Pane Mode",
132+ Bindings: g.buildModeBindings(cfg.Workspace.PaneMode.Actions, defaults.Workspace.PaneMode.Actions),
133+ Activation: cfg.Workspace.PaneMode.ActivationShortcut,
134+ }
135+}
136+
137+// buildTabModeGroup builds the tab mode group.
138+func (g *KeybindingsGateway) buildTabModeGroup(cfg, defaults *Config) port.KeybindingGroup {
139+ return port.KeybindingGroup{
140+ Mode: "tab",
141+ DisplayName: "Tab Mode",
142+ Bindings: g.buildModeBindings(cfg.Workspace.TabMode.Actions, defaults.Workspace.TabMode.Actions),
143+ Activation: cfg.Workspace.TabMode.ActivationShortcut,
144+ }
145+}
146+
147+// buildResizeModeGroup builds the resize mode group.
148+func (g *KeybindingsGateway) buildResizeModeGroup(cfg, defaults *Config) port.KeybindingGroup {
149+ return port.KeybindingGroup{
150+ Mode: "resize",
151+ DisplayName: "Resize Mode",
152+ Bindings: g.buildModeBindings(cfg.Workspace.ResizeMode.Actions, defaults.Workspace.ResizeMode.Actions),
153+ Activation: cfg.Workspace.ResizeMode.ActivationShortcut,
154+ }
155+}
156+
157+// buildSessionModeGroup builds the session mode group.
158+func (g *KeybindingsGateway) buildSessionModeGroup(cfg, defaults *Config) port.KeybindingGroup {
159+ return port.KeybindingGroup{
160+ Mode: "session",
161+ DisplayName: "Session Mode",
162+ Bindings: g.buildModeBindings(cfg.Session.SessionMode.Actions, defaults.Session.SessionMode.Actions),
163+ Activation: cfg.Session.SessionMode.ActivationShortcut,
164+ }
165+}
166+
167+// buildModeBindings builds keybinding entries for a modal mode.
168+func (*KeybindingsGateway) buildModeBindings(actions, defaultActions map[string]ActionBinding) []port.KeybindingEntry {
169+ var entries []port.KeybindingEntry
170+
171+ actionNames := make([]string, 0, len(actions))
172+ for action := range actions {
173+ actionNames = append(actionNames, action)
174+ }
175+ sort.Strings(actionNames)
176+
177+ for _, action := range actionNames {
178+ binding := actions[action]
179+ defaultBinding := defaultActions[action]
180+
181+ entries = append(entries, port.KeybindingEntry{
182+ Action: action,
183+ Description: binding.Desc,
184+ Keys: binding.Keys,
185+ DefaultKeys: defaultBinding.Keys,
186+ IsCustom: !reflect.DeepEqual(binding.Keys, defaultBinding.Keys),
187+ })
188+ }
189+
190+ return entries
191+}
192+
193+// updateKeybinding updates a keybinding in the config.
194+func (*KeybindingsGateway) updateKeybinding(cfg *Config, req port.SetKeybindingRequest) {
195+ switch req.Mode {
196+ case modeGlobal:
197+ if existing, ok := cfg.Workspace.Shortcuts.Actions[req.Action]; ok {
198+ existing.Keys = req.Keys
199+ cfg.Workspace.Shortcuts.Actions[req.Action] = existing
200+ }
201+ case "pane":
202+ if existing, ok := cfg.Workspace.PaneMode.Actions[req.Action]; ok {
203+ existing.Keys = req.Keys
204+ cfg.Workspace.PaneMode.Actions[req.Action] = existing
205+ }
206+ case "tab":
207+ if existing, ok := cfg.Workspace.TabMode.Actions[req.Action]; ok {
208+ existing.Keys = req.Keys
209+ cfg.Workspace.TabMode.Actions[req.Action] = existing
210+ }
211+ case "resize":
212+ if existing, ok := cfg.Workspace.ResizeMode.Actions[req.Action]; ok {
213+ existing.Keys = req.Keys
214+ cfg.Workspace.ResizeMode.Actions[req.Action] = existing
215+ }
216+ case "session":
217+ if existing, ok := cfg.Session.SessionMode.Actions[req.Action]; ok {
218+ existing.Keys = req.Keys
219+ cfg.Session.SessionMode.Actions[req.Action] = existing
220+ }
221+ }
222+}
223+
224+// getDefaultKeys returns the default keys for an action.
225+func (*KeybindingsGateway) getDefaultKeys(defaults *Config, mode, action string) []string {
226+ switch mode {
227+ case modeGlobal:
228+ if binding, ok := defaults.Workspace.Shortcuts.Actions[action]; ok {
229+ return binding.Keys
230+ }
231+ case "pane":
232+ return defaults.Workspace.PaneMode.Actions[action].Keys
233+ case "tab":
234+ return defaults.Workspace.TabMode.Actions[action].Keys
235+ case "resize":
236+ return defaults.Workspace.ResizeMode.Actions[action].Keys
237+ case "session":
238+ return defaults.Session.SessionMode.Actions[action].Keys
239+ }
240+ return nil
241+}
242+
243+// checkConflicts scans all modes for keybinding conflicts.
244+func (*KeybindingsGateway) checkConflicts(cfg *Config, targetMode, targetAction string, newKeys []string) []port.KeybindingConflict {
245+ var conflicts []port.KeybindingConflict
246+
247+ type binding struct {
248+ mode string
249+ action string
250+ }
251+ keyMap := make(map[string][]binding)
252+
253+ addBindings := func(mode string, actions map[string]ActionBinding) {
254+ for action, ab := range actions {
255+ for _, key := range ab.Keys {
256+ keyMap[key] = append(keyMap[key], binding{mode: mode, action: action})
257+ }
258+ }
259+ }
260+
261+ addBindings(modeGlobal, cfg.Workspace.Shortcuts.Actions)
262+ addBindings("pane", cfg.Workspace.PaneMode.Actions)
263+ addBindings("tab", cfg.Workspace.TabMode.Actions)
264+ addBindings("resize", cfg.Workspace.ResizeMode.Actions)
265+ addBindings("session", cfg.Session.SessionMode.Actions)
266+
267+ for _, newKey := range newKeys {
268+ if existing, ok := keyMap[newKey]; ok {
269+ for _, b := range existing {
270+ if b.mode == targetMode && b.action == targetAction {
271+ continue
272+ }
273+ if targetMode == modeGlobal || b.mode == modeGlobal || b.mode == targetMode {
274+ conflicts = append(conflicts, port.KeybindingConflict{
275+ ConflictingAction: b.action,
276+ ConflictingMode: b.mode,
277+ Key: newKey,
278+ })
279+ }
280+ }
281+ }
282+ }
283+
284+ return conflicts
285+}
@@ -0,0 +1,285 @@
1+package config
2+
3+import (
4+ "context"
5+ "reflect"
6+ "sort"
7+
8+ "github.com/bnema/dumber/internal/application/port"
9+ "github.com/bnema/dumber/internal/logging"
10+)
11+
12+const modeGlobal = "global"
13+
14+// KeybindingsGateway implements port.KeybindingsProvider and port.KeybindingsSaver.
15+type KeybindingsGateway struct {
16+ mgr *Manager
17+}
18+
19+// NewKeybindingsGateway creates a new KeybindingsGateway.
20+func NewKeybindingsGateway(mgr *Manager) *KeybindingsGateway {
21+ return &KeybindingsGateway{mgr: mgr}
22+}
23+
24+// GetKeybindings returns all keybindings grouped by mode.
25+func (g *KeybindingsGateway) GetKeybindings(ctx context.Context) (port.KeybindingsConfig, error) {
26+ log := logging.FromContext(ctx)
27+ cfg := g.mgr.Get()
28+ defaults := DefaultConfig()
29+ log.Debug().Msg("keybindings gateway: fetching keybindings")
30+
31+ groups := []port.KeybindingGroup{
32+ g.buildGlobalGroup(cfg, defaults),
33+ g.buildPaneModeGroup(cfg, defaults),
34+ g.buildTabModeGroup(cfg, defaults),
35+ g.buildResizeModeGroup(cfg, defaults),
36+ g.buildSessionModeGroup(cfg, defaults),
37+ }
38+
39+ return port.KeybindingsConfig{Groups: groups}, nil
40+}
41+
42+// GetDefaultKeybindings returns the default keybindings configuration.
43+func (g *KeybindingsGateway) GetDefaultKeybindings(ctx context.Context) (port.KeybindingsConfig, error) {
44+ log := logging.FromContext(ctx)
45+ defaults := DefaultConfig()
46+
47+ groups := []port.KeybindingGroup{
48+ g.buildGlobalGroup(defaults, defaults),
49+ g.buildPaneModeGroup(defaults, defaults),
50+ g.buildTabModeGroup(defaults, defaults),
51+ g.buildResizeModeGroup(defaults, defaults),
52+ g.buildSessionModeGroup(defaults, defaults),
53+ }
54+
55+ log.Debug().Int("groups", len(groups)).Msg("keybindings gateway: returning default keybindings")
56+ return port.KeybindingsConfig{Groups: groups}, nil
57+}
58+
59+// CheckConflicts scans all modes for keybinding conflicts.
60+func (g *KeybindingsGateway) CheckConflicts(ctx context.Context, mode, action string, keys []string) ([]port.KeybindingConflict, error) {
61+ log := logging.FromContext(ctx)
62+ cfg := g.mgr.Get()
63+
64+ conflicts := g.checkConflicts(cfg, mode, action, keys)
65+ if len(conflicts) > 0 {
66+ log.Warn().Int("conflicts", len(conflicts)).Msg("keybindings gateway: conflicts detected")
67+ }
68+
69+ return conflicts, nil
70+}
71+
72+// SetKeybinding updates a single keybinding.
73+func (g *KeybindingsGateway) SetKeybinding(ctx context.Context, req port.SetKeybindingRequest) error {
74+ log := logging.FromContext(ctx)
75+ log.Debug().Str("mode", req.Mode).Str("action", req.Action).Msg("keybindings gateway: setting keybinding")
76+ cfg := g.mgr.Get()
77+ g.updateKeybinding(cfg, req)
78+ return g.mgr.Save(cfg)
79+}
80+
81+// ResetKeybinding resets a keybinding to its default value.
82+func (g *KeybindingsGateway) ResetKeybinding(ctx context.Context, req port.ResetKeybindingRequest) error {
83+ log := logging.FromContext(ctx)
84+ log.Info().Str("mode", req.Mode).Str("action", req.Action).Msg("keybindings gateway: resetting keybinding to default")
85+
86+ defaults := DefaultConfig()
87+ defaultKeys := g.getDefaultKeys(defaults, req.Mode, req.Action)
88+
89+ setReq := port.SetKeybindingRequest{
90+ Mode: req.Mode,
91+ Action: req.Action,
92+ Keys: defaultKeys,
93+ }
94+
95+ cfg := g.mgr.Get()
96+ g.updateKeybinding(cfg, setReq)
97+
98+ return g.mgr.Save(cfg)
99+}
100+
101+// ResetAllKeybindings resets all keybindings to their default values.
102+func (g *KeybindingsGateway) ResetAllKeybindings(ctx context.Context) error {
103+ log := logging.FromContext(ctx)
104+ log.Info().Msg("keybindings gateway: resetting all keybindings to defaults")
105+
106+ cfg := g.mgr.Get()
107+ defaults := DefaultConfig()
108+
109+ cfg.Workspace.PaneMode.Actions = defaults.Workspace.PaneMode.Actions
110+ cfg.Workspace.TabMode.Actions = defaults.Workspace.TabMode.Actions
111+ cfg.Workspace.ResizeMode.Actions = defaults.Workspace.ResizeMode.Actions
112+ cfg.Workspace.Shortcuts = defaults.Workspace.Shortcuts
113+ cfg.Session.SessionMode.Actions = defaults.Session.SessionMode.Actions
114+
115+ return g.mgr.Save(cfg)
116+}
117+
118+// buildGlobalGroup builds the global shortcuts group.
119+func (g *KeybindingsGateway) buildGlobalGroup(cfg, defaults *Config) port.KeybindingGroup {
120+ return port.KeybindingGroup{
121+ Mode: modeGlobal,
122+ DisplayName: "Global Shortcuts",
123+ Bindings: g.buildModeBindings(cfg.Workspace.Shortcuts.Actions, defaults.Workspace.Shortcuts.Actions),
124+ }
125+}
126+
127+// buildPaneModeGroup builds the pane mode group.
128+func (g *KeybindingsGateway) buildPaneModeGroup(cfg, defaults *Config) port.KeybindingGroup {
129+ return port.KeybindingGroup{
130+ Mode: "pane",
131+ DisplayName: "Pane Mode",
132+ Bindings: g.buildModeBindings(cfg.Workspace.PaneMode.Actions, defaults.Workspace.PaneMode.Actions),
133+ Activation: cfg.Workspace.PaneMode.ActivationShortcut,
134+ }
135+}
136+
137+// buildTabModeGroup builds the tab mode group.
138+func (g *KeybindingsGateway) buildTabModeGroup(cfg, defaults *Config) port.KeybindingGroup {
139+ return port.KeybindingGroup{
140+ Mode: "tab",
141+ DisplayName: "Tab Mode",
142+ Bindings: g.buildModeBindings(cfg.Workspace.TabMode.Actions, defaults.Workspace.TabMode.Actions),
143+ Activation: cfg.Workspace.TabMode.ActivationShortcut,
144+ }
145+}
146+
147+// buildResizeModeGroup builds the resize mode group.
148+func (g *KeybindingsGateway) buildResizeModeGroup(cfg, defaults *Config) port.KeybindingGroup {
149+ return port.KeybindingGroup{
150+ Mode: "resize",
151+ DisplayName: "Resize Mode",
152+ Bindings: g.buildModeBindings(cfg.Workspace.ResizeMode.Actions, defaults.Workspace.ResizeMode.Actions),
153+ Activation: cfg.Workspace.ResizeMode.ActivationShortcut,
154+ }
155+}
156+
157+// buildSessionModeGroup builds the session mode group.
158+func (g *KeybindingsGateway) buildSessionModeGroup(cfg, defaults *Config) port.KeybindingGroup {
159+ return port.KeybindingGroup{
160+ Mode: "session",
161+ DisplayName: "Session Mode",
162+ Bindings: g.buildModeBindings(cfg.Session.SessionMode.Actions, defaults.Session.SessionMode.Actions),
163+ Activation: cfg.Session.SessionMode.ActivationShortcut,
164+ }
165+}
166+
167+// buildModeBindings builds keybinding entries for a modal mode.
168+func (*KeybindingsGateway) buildModeBindings(actions, defaultActions map[string]ActionBinding) []port.KeybindingEntry {
169+ var entries []port.KeybindingEntry
170+
171+ actionNames := make([]string, 0, len(actions))
172+ for action := range actions {
173+ actionNames = append(actionNames, action)
174+ }
175+ sort.Strings(actionNames)
176+
177+ for _, action := range actionNames {
178+ binding := actions[action]
179+ defaultBinding := defaultActions[action]
180+
181+ entries = append(entries, port.KeybindingEntry{
182+ Action: action,
183+ Description: binding.Desc,
184+ Keys: binding.Keys,
185+ DefaultKeys: defaultBinding.Keys,
186+ IsCustom: !reflect.DeepEqual(binding.Keys, defaultBinding.Keys),
187+ })
188+ }
189+
190+ return entries
191+}
192+
193+// updateKeybinding updates a keybinding in the config.
194+func (*KeybindingsGateway) updateKeybinding(cfg *Config, req port.SetKeybindingRequest) {
195+ switch req.Mode {
196+ case modeGlobal:
197+ if existing, ok := cfg.Workspace.Shortcuts.Actions[req.Action]; ok {
198+ existing.Keys = req.Keys
199+ cfg.Workspace.Shortcuts.Actions[req.Action] = existing
200+ }
201+ case "pane":
202+ if existing, ok := cfg.Workspace.PaneMode.Actions[req.Action]; ok {
203+ existing.Keys = req.Keys
204+ cfg.Workspace.PaneMode.Actions[req.Action] = existing
205+ }
206+ case "tab":
207+ if existing, ok := cfg.Workspace.TabMode.Actions[req.Action]; ok {
208+ existing.Keys = req.Keys
209+ cfg.Workspace.TabMode.Actions[req.Action] = existing
210+ }
211+ case "resize":
212+ if existing, ok := cfg.Workspace.ResizeMode.Actions[req.Action]; ok {
213+ existing.Keys = req.Keys
214+ cfg.Workspace.ResizeMode.Actions[req.Action] = existing
215+ }
216+ case "session":
217+ if existing, ok := cfg.Session.SessionMode.Actions[req.Action]; ok {
218+ existing.Keys = req.Keys
219+ cfg.Session.SessionMode.Actions[req.Action] = existing
220+ }
221+ }
222+}
223+
224+// getDefaultKeys returns the default keys for an action.
225+func (*KeybindingsGateway) getDefaultKeys(defaults *Config, mode, action string) []string {
226+ switch mode {
227+ case modeGlobal:
228+ if binding, ok := defaults.Workspace.Shortcuts.Actions[action]; ok {
229+ return binding.Keys
230+ }
231+ case "pane":
232+ return defaults.Workspace.PaneMode.Actions[action].Keys
233+ case "tab":
234+ return defaults.Workspace.TabMode.Actions[action].Keys
235+ case "resize":
236+ return defaults.Workspace.ResizeMode.Actions[action].Keys
237+ case "session":
238+ return defaults.Session.SessionMode.Actions[action].Keys
239+ }
240+ return nil
241+}
242+
243+// checkConflicts scans all modes for keybinding conflicts.
244+func (*KeybindingsGateway) checkConflicts(cfg *Config, targetMode, targetAction string, newKeys []string) []port.KeybindingConflict {
245+ var conflicts []port.KeybindingConflict
246+
247+ type binding struct {
248+ mode string
249+ action string
250+ }
251+ keyMap := make(map[string][]binding)
252+
253+ addBindings := func(mode string, actions map[string]ActionBinding) {
254+ for action, ab := range actions {
255+ for _, key := range ab.Keys {
256+ keyMap[key] = append(keyMap[key], binding{mode: mode, action: action})
257+ }
258+ }
259+ }
260+
261+ addBindings(modeGlobal, cfg.Workspace.Shortcuts.Actions)
262+ addBindings("pane", cfg.Workspace.PaneMode.Actions)
263+ addBindings("tab", cfg.Workspace.TabMode.Actions)
264+ addBindings("resize", cfg.Workspace.ResizeMode.Actions)
265+ addBindings("session", cfg.Session.SessionMode.Actions)
266+
267+ for _, newKey := range newKeys {
268+ if existing, ok := keyMap[newKey]; ok {
269+ for _, b := range existing {
270+ if b.mode == targetMode && b.action == targetAction {
271+ continue
272+ }
273+ if targetMode == modeGlobal || b.mode == modeGlobal || b.mode == targetMode {
274+ conflicts = append(conflicts, port.KeybindingConflict{
275+ ConflictingAction: b.action,
276+ ConflictingMode: b.mode,
277+ Key: newKey,
278+ })
279+ }
280+ }
281+ }
282+ }
283+
284+ return conflicts
285+}
M internal/infrastructure/webkit/handlers/keybindings.go +33 -279
@@ -4,51 +4,43 @@ import (
44 "context"
55 "encoding/json"
66 "fmt"
7- "reflect"
8- "sort"
97
108 "github.com/bnema/dumber/internal/application/port"
11- "github.com/bnema/dumber/internal/infrastructure/config"
9+ "github.com/bnema/dumber/internal/application/usecase"
1210 "github.com/bnema/dumber/internal/infrastructure/webkit"
1311 "github.com/bnema/dumber/internal/logging"
1412 )
1513
16-const modeGlobal = "global"
17-
1814 // KeybindingsHandler handles keybinding-related messages.
19-type KeybindingsHandler struct{}
15+type KeybindingsHandler struct {
16+ getUC *usecase.GetKeybindingsUseCase
17+ setUC *usecase.SetKeybindingUseCase
18+ resetUC *usecase.ResetKeybindingUseCase
19+ resetAllUC *usecase.ResetAllKeybindingsUseCase
20+}
2021
2122 // NewKeybindingsHandler creates a new KeybindingsHandler.
22-func NewKeybindingsHandler() *KeybindingsHandler {
23- return &KeybindingsHandler{}
23+func NewKeybindingsHandler(
24+ getUC *usecase.GetKeybindingsUseCase,
25+ setUC *usecase.SetKeybindingUseCase,
26+ resetUC *usecase.ResetKeybindingUseCase,
27+ resetAllUC *usecase.ResetAllKeybindingsUseCase,
28+) *KeybindingsHandler {
29+ return &KeybindingsHandler{
30+ getUC: getUC,
31+ setUC: setUC,
32+ resetUC: resetUC,
33+ resetAllUC: resetAllUC,
34+ }
2435 }
2536
2637 // HandleGetKeybindings returns all keybindings grouped by mode.
27-func (*KeybindingsHandler) HandleGetKeybindings(ctx context.Context, _ webkit.WebViewID, _ json.RawMessage) (any, error) {
28- log := logging.FromContext(ctx).With().Str("handler", "keybindings").Logger()
29-
30- mgr := config.GetManager()
31- if mgr == nil {
32- return nil, fmt.Errorf("config manager not initialized")
33- }
34-
35- cfg := mgr.Get()
36- defaults := config.DefaultConfig()
37-
38- groups := []port.KeybindingGroup{
39- buildGlobalGroup(cfg, defaults),
40- buildPaneModeGroup(cfg, defaults),
41- buildTabModeGroup(cfg, defaults),
42- buildResizeModeGroup(cfg, defaults),
43- buildSessionModeGroup(cfg, defaults),
44- }
45-
46- log.Debug().Int("groups", len(groups)).Msg("returning keybindings")
47- return port.KeybindingsConfig{Groups: groups}, nil
38+func (h *KeybindingsHandler) HandleGetKeybindings(ctx context.Context, _ webkit.WebViewID, _ json.RawMessage) (any, error) {
39+ return h.getUC.Execute(ctx)
4840 }
4941
5042 // HandleSetKeybinding updates a single keybinding.
51-func (*KeybindingsHandler) HandleSetKeybinding(ctx context.Context, _ webkit.WebViewID, payload json.RawMessage) (any, error) {
43+func (h *KeybindingsHandler) HandleSetKeybinding(ctx context.Context, _ webkit.WebViewID, payload json.RawMessage) (any, error) {
5244 log := logging.FromContext(ctx).With().Str("handler", "keybindings").Logger()
5345 log.Debug().RawJSON("payload", payload).Msg("HandleSetKeybinding called")
5446
@@ -58,35 +50,19 @@ func (*KeybindingsHandler) HandleSetKeybinding(ctx context.Context, _ webkit.Web
5850 return nil, fmt.Errorf("invalid request: %w", err)
5951 }
6052
61- log.Info().Str("mode", req.Mode).Str("action", req.Action).Strs("keys", req.Keys).Msg("setting keybinding")
62-
63- mgr := config.GetManager()
64- if mgr == nil {
65- return nil, fmt.Errorf("config manager not initialized")
66- }
67-
68- cfg := mgr.Get()
69-
70- // Check for conflicts before saving
71- conflicts := checkConflicts(cfg, req.Mode, req.Action, req.Keys)
72- if len(conflicts) > 0 {
73- log.Warn().Int("conflicts", len(conflicts)).Msg("keybinding conflicts detected")
74- }
75-
76- updateKeybinding(cfg, req)
77-
78- if err := mgr.Save(cfg); err != nil {
79- return nil, fmt.Errorf("failed to save: %w", err)
53+ resp, err := h.setUC.Execute(ctx, req)
54+ if err != nil {
55+ return nil, err
8056 }
8157
8258 return map[string]any{
8359 "status": "success",
84- "conflicts": conflicts,
60+ "conflicts": resp.Conflicts,
8561 }, nil
8662 }
8763
8864 // HandleResetKeybinding resets a keybinding to default.
89-func (*KeybindingsHandler) HandleResetKeybinding(ctx context.Context, _ webkit.WebViewID, payload json.RawMessage) (any, error) {
65+func (h *KeybindingsHandler) HandleResetKeybinding(ctx context.Context, _ webkit.WebViewID, payload json.RawMessage) (any, error) {
9066 log := logging.FromContext(ctx).With().Str("handler", "keybindings").Logger()
9167
9268 var req port.ResetKeybindingRequest
@@ -96,249 +72,27 @@ func (*KeybindingsHandler) HandleResetKeybinding(ctx context.Context, _ webkit.W
9672
9773 log.Info().Str("mode", req.Mode).Str("action", req.Action).Msg("resetting keybinding to default")
9874
99- defaults := config.DefaultConfig()
100- defaultKeys := getDefaultKeys(defaults, req.Mode, req.Action)
101-
102- setReq := port.SetKeybindingRequest{
103- Mode: req.Mode,
104- Action: req.Action,
105- Keys: defaultKeys,
106- }
107-
108- mgr := config.GetManager()
109- if mgr == nil {
110- return nil, fmt.Errorf("config manager not initialized")
111- }
112-
113- cfg := mgr.Get()
114- updateKeybinding(cfg, setReq)
115-
116- if err := mgr.Save(cfg); err != nil {
117- return nil, fmt.Errorf("failed to save: %w", err)
75+ if err := h.resetUC.Execute(ctx, req); err != nil {
76+ return nil, err
11877 }
11978
12079 return map[string]any{"status": "success"}, nil
12180 }
12281
12382 // HandleResetAllKeybindings resets all keybindings to defaults.
124-func (*KeybindingsHandler) HandleResetAllKeybindings(ctx context.Context, _ webkit.WebViewID, _ json.RawMessage) (any, error) {
83+func (h *KeybindingsHandler) HandleResetAllKeybindings(ctx context.Context, _ webkit.WebViewID, _ json.RawMessage) (any, error) {
12584 log := logging.FromContext(ctx).With().Str("handler", "keybindings").Logger()
12685 log.Info().Msg("resetting all keybindings to defaults")
12786
128- mgr := config.GetManager()
129- if mgr == nil {
130- return nil, fmt.Errorf("config manager not initialized")
131- }
132-
133- cfg := mgr.Get()
134- defaults := config.DefaultConfig()
135-
136- // Reset all keybinding sections
137- cfg.Workspace.PaneMode.Actions = defaults.Workspace.PaneMode.Actions
138- cfg.Workspace.TabMode.Actions = defaults.Workspace.TabMode.Actions
139- cfg.Workspace.ResizeMode.Actions = defaults.Workspace.ResizeMode.Actions
140- cfg.Workspace.Shortcuts = defaults.Workspace.Shortcuts
141- cfg.Session.SessionMode.Actions = defaults.Session.SessionMode.Actions
142-
143- if err := mgr.Save(cfg); err != nil {
144- return nil, fmt.Errorf("failed to save: %w", err)
87+ if err := h.resetAllUC.Execute(ctx); err != nil {
88+ return nil, err
14589 }
14690
14791 return map[string]any{"status": "success"}, nil
14892 }
14993
150-// buildGlobalGroup builds the global shortcuts group.
151-func buildGlobalGroup(cfg, defaults *config.Config) port.KeybindingGroup {
152- return port.KeybindingGroup{
153- Mode: modeGlobal,
154- DisplayName: "Global Shortcuts",
155- Bindings: buildModeBindings(cfg.Workspace.Shortcuts.Actions, defaults.Workspace.Shortcuts.Actions),
156- }
157-}
158-
159-// buildPaneModeGroup builds the pane mode group.
160-func buildPaneModeGroup(cfg, defaults *config.Config) port.KeybindingGroup {
161- return port.KeybindingGroup{
162- Mode: "pane",
163- DisplayName: "Pane Mode",
164- Bindings: buildModeBindings(cfg.Workspace.PaneMode.Actions, defaults.Workspace.PaneMode.Actions),
165- Activation: cfg.Workspace.PaneMode.ActivationShortcut,
166- }
167-}
168-
169-// buildTabModeGroup builds the tab mode group.
170-func buildTabModeGroup(cfg, defaults *config.Config) port.KeybindingGroup {
171- return port.KeybindingGroup{
172- Mode: "tab",
173- DisplayName: "Tab Mode",
174- Bindings: buildModeBindings(cfg.Workspace.TabMode.Actions, defaults.Workspace.TabMode.Actions),
175- Activation: cfg.Workspace.TabMode.ActivationShortcut,
176- }
177-}
178-
179-// buildResizeModeGroup builds the resize mode group.
180-func buildResizeModeGroup(cfg, defaults *config.Config) port.KeybindingGroup {
181- return port.KeybindingGroup{
182- Mode: "resize",
183- DisplayName: "Resize Mode",
184- Bindings: buildModeBindings(cfg.Workspace.ResizeMode.Actions, defaults.Workspace.ResizeMode.Actions),
185- Activation: cfg.Workspace.ResizeMode.ActivationShortcut,
186- }
187-}
188-
189-// buildSessionModeGroup builds the session mode group.
190-func buildSessionModeGroup(cfg, defaults *config.Config) port.KeybindingGroup {
191- return port.KeybindingGroup{
192- Mode: "session",
193- DisplayName: "Session Mode",
194- Bindings: buildModeBindings(cfg.Session.SessionMode.Actions, defaults.Session.SessionMode.Actions),
195- Activation: cfg.Session.SessionMode.ActivationShortcut,
196- }
197-}
198-
199-// buildModeBindings builds keybinding entries for a modal mode.
200-func buildModeBindings(actions, defaultActions map[string]config.ActionBinding) []port.KeybindingEntry {
201- var entries []port.KeybindingEntry
202-
203- // Get all action names and sort them
204- actionNames := make([]string, 0, len(actions))
205- for action := range actions {
206- actionNames = append(actionNames, action)
207- }
208- sort.Strings(actionNames)
209-
210- for _, action := range actionNames {
211- binding := actions[action]
212- defaultBinding := defaultActions[action]
213-
214- entries = append(entries, port.KeybindingEntry{
215- Action: action,
216- Description: binding.Desc,
217- Keys: binding.Keys,
218- DefaultKeys: defaultBinding.Keys,
219- IsCustom: !reflect.DeepEqual(binding.Keys, defaultBinding.Keys),
220- })
221- }
222-
223- return entries
224-}
225-
226-// updateKeybinding updates a keybinding in the config.
227-func updateKeybinding(cfg *config.Config, req port.SetKeybindingRequest) {
228- switch req.Mode {
229- case modeGlobal:
230- updateGlobalShortcut(cfg, req.Action, req.Keys)
231- case "pane":
232- if existing, ok := cfg.Workspace.PaneMode.Actions[req.Action]; ok {
233- existing.Keys = req.Keys
234- cfg.Workspace.PaneMode.Actions[req.Action] = existing
235- }
236- case "tab":
237- if existing, ok := cfg.Workspace.TabMode.Actions[req.Action]; ok {
238- existing.Keys = req.Keys
239- cfg.Workspace.TabMode.Actions[req.Action] = existing
240- }
241- case "resize":
242- if existing, ok := cfg.Workspace.ResizeMode.Actions[req.Action]; ok {
243- existing.Keys = req.Keys
244- cfg.Workspace.ResizeMode.Actions[req.Action] = existing
245- }
246- case "session":
247- if existing, ok := cfg.Session.SessionMode.Actions[req.Action]; ok {
248- existing.Keys = req.Keys
249- cfg.Session.SessionMode.Actions[req.Action] = existing
250- }
251- }
252-}
253-
254-// updateGlobalShortcut updates a global shortcut in the config.
255-func updateGlobalShortcut(cfg *config.Config, action string, keys []string) {
256- if existing, ok := cfg.Workspace.Shortcuts.Actions[action]; ok {
257- existing.Keys = keys
258- cfg.Workspace.Shortcuts.Actions[action] = existing
259- }
260-}
261-
262-// getDefaultKeys returns the default keys for an action.
263-func getDefaultKeys(defaults *config.Config, mode, action string) []string {
264- switch mode {
265- case modeGlobal:
266- return getDefaultGlobalKeys(defaults, action)
267- case "pane":
268- return defaults.Workspace.PaneMode.Actions[action].Keys
269- case "tab":
270- return defaults.Workspace.TabMode.Actions[action].Keys
271- case "resize":
272- return defaults.Workspace.ResizeMode.Actions[action].Keys
273- case "session":
274- return defaults.Session.SessionMode.Actions[action].Keys
275- }
276- return nil
277-}
278-
279-// getDefaultGlobalKeys returns the default key for a global shortcut.
280-func getDefaultGlobalKeys(defaults *config.Config, action string) []string {
281- if binding, ok := defaults.Workspace.Shortcuts.Actions[action]; ok {
282- return binding.Keys
283- }
284- return nil
285-}
286-
287-// checkConflicts scans all modes for keybinding conflicts.
288-// Returns a list of conflicts where the same key is bound to different actions.
289-func checkConflicts(cfg *config.Config, targetMode, targetAction string, newKeys []string) []port.KeybindingConflict {
290- var conflicts []port.KeybindingConflict
291-
292- // Build a map of key -> (mode, action) for all bindings
293- type binding struct {
294- mode string
295- action string
296- }
297- keyMap := make(map[string][]binding)
298-
299- // Helper to add bindings from a mode
300- addBindings := func(mode string, actions map[string]config.ActionBinding) {
301- for action, ab := range actions {
302- for _, key := range ab.Keys {
303- keyMap[key] = append(keyMap[key], binding{mode: mode, action: action})
304- }
305- }
306- }
307-
308- // Collect all current bindings
309- addBindings(modeGlobal, cfg.Workspace.Shortcuts.Actions)
310- addBindings("pane", cfg.Workspace.PaneMode.Actions)
311- addBindings("tab", cfg.Workspace.TabMode.Actions)
312- addBindings("resize", cfg.Workspace.ResizeMode.Actions)
313- addBindings("session", cfg.Session.SessionMode.Actions)
314-
315- // Check each new key for conflicts
316- for _, newKey := range newKeys {
317- if existing, ok := keyMap[newKey]; ok {
318- for _, b := range existing {
319- // Skip if it's the same action we're updating
320- if b.mode == targetMode && b.action == targetAction {
321- continue
322- }
323- // Global shortcuts conflict with everything
324- // Modal bindings only conflict within same mode or with global
325- if targetMode == modeGlobal || b.mode == modeGlobal || b.mode == targetMode {
326- conflicts = append(conflicts, port.KeybindingConflict{
327- ConflictingAction: b.action,
328- ConflictingMode: b.mode,
329- Key: newKey,
330- })
331- }
332- }
333- }
334- }
335-
336- return conflicts
337-}
338-
33994 // RegisterKeybindingsHandlers registers keybindings handlers with the router.
340-func RegisterKeybindingsHandlers(ctx context.Context, router *webkit.MessageRouter) error {
341- handler := NewKeybindingsHandler()
95+func RegisterKeybindingsHandlers(ctx context.Context, router *webkit.MessageRouter, handler *KeybindingsHandler) error {
34296 log := logging.FromContext(ctx).With().Str("component", "handlers").Logger()
34397
34498 // Get all keybindings
@@ -4,51 +4,43 @@ import (
4 "context"4 "context"
5 "encoding/json"5 "encoding/json"
6 "fmt"6 "fmt"
7- "reflect"
8- "sort"
9 7
10 "github.com/bnema/dumber/internal/application/port"8 "github.com/bnema/dumber/internal/application/port"
11- "github.com/bnema/dumber/internal/infrastructure/config"9+ "github.com/bnema/dumber/internal/application/usecase"
12 "github.com/bnema/dumber/internal/infrastructure/webkit"10 "github.com/bnema/dumber/internal/infrastructure/webkit"
13 "github.com/bnema/dumber/internal/logging"11 "github.com/bnema/dumber/internal/logging"
14 )12 )
15 13
16-const modeGlobal = "global"
17-
18 // KeybindingsHandler handles keybinding-related messages.14 // KeybindingsHandler handles keybinding-related messages.
19-type KeybindingsHandler struct{}15+type KeybindingsHandler struct {
16+ getUC *usecase.GetKeybindingsUseCase
17+ setUC *usecase.SetKeybindingUseCase
18+ resetUC *usecase.ResetKeybindingUseCase
19+ resetAllUC *usecase.ResetAllKeybindingsUseCase
20+}
20 21
21 // NewKeybindingsHandler creates a new KeybindingsHandler.22 // NewKeybindingsHandler creates a new KeybindingsHandler.
22-func NewKeybindingsHandler() *KeybindingsHandler {23+func NewKeybindingsHandler(
23- return &KeybindingsHandler{}24+ getUC *usecase.GetKeybindingsUseCase,
25+ setUC *usecase.SetKeybindingUseCase,
26+ resetUC *usecase.ResetKeybindingUseCase,
27+ resetAllUC *usecase.ResetAllKeybindingsUseCase,
28+) *KeybindingsHandler {
29+ return &KeybindingsHandler{
30+ getUC: getUC,
31+ setUC: setUC,
32+ resetUC: resetUC,
33+ resetAllUC: resetAllUC,
34+ }
24 }35 }
25 36
26 // HandleGetKeybindings returns all keybindings grouped by mode.37 // HandleGetKeybindings returns all keybindings grouped by mode.
27-func (*KeybindingsHandler) HandleGetKeybindings(ctx context.Context, _ webkit.WebViewID, _ json.RawMessage) (any, error) {38+func (h *KeybindingsHandler) HandleGetKeybindings(ctx context.Context, _ webkit.WebViewID, _ json.RawMessage) (any, error) {
28- log := logging.FromContext(ctx).With().Str("handler", "keybindings").Logger()39+ return h.getUC.Execute(ctx)
29-
30- mgr := config.GetManager()
31- if mgr == nil {
32- return nil, fmt.Errorf("config manager not initialized")
33- }
34-
35- cfg := mgr.Get()
36- defaults := config.DefaultConfig()
37-
38- groups := []port.KeybindingGroup{
39- buildGlobalGroup(cfg, defaults),
40- buildPaneModeGroup(cfg, defaults),
41- buildTabModeGroup(cfg, defaults),
42- buildResizeModeGroup(cfg, defaults),
43- buildSessionModeGroup(cfg, defaults),
44- }
45-
46- log.Debug().Int("groups", len(groups)).Msg("returning keybindings")
47- return port.KeybindingsConfig{Groups: groups}, nil
48 }40 }
49 41
50 // HandleSetKeybinding updates a single keybinding.42 // HandleSetKeybinding updates a single keybinding.
51-func (*KeybindingsHandler) HandleSetKeybinding(ctx context.Context, _ webkit.WebViewID, payload json.RawMessage) (any, error) {43+func (h *KeybindingsHandler) HandleSetKeybinding(ctx context.Context, _ webkit.WebViewID, payload json.RawMessage) (any, error) {
52 log := logging.FromContext(ctx).With().Str("handler", "keybindings").Logger()44 log := logging.FromContext(ctx).With().Str("handler", "keybindings").Logger()
53 log.Debug().RawJSON("payload", payload).Msg("HandleSetKeybinding called")45 log.Debug().RawJSON("payload", payload).Msg("HandleSetKeybinding called")
54 46
@@ -58,35 +50,19 @@ func (*KeybindingsHandler) HandleSetKeybinding(ctx context.Context, _ webkit.Web
58 return nil, fmt.Errorf("invalid request: %w", err)50 return nil, fmt.Errorf("invalid request: %w", err)
59 }51 }
60 52
61- log.Info().Str("mode", req.Mode).Str("action", req.Action).Strs("keys", req.Keys).Msg("setting keybinding")53+ resp, err := h.setUC.Execute(ctx, req)
62-54+ if err != nil {
63- mgr := config.GetManager()55+ return nil, err
64- if mgr == nil {
65- return nil, fmt.Errorf("config manager not initialized")
66- }
67-
68- cfg := mgr.Get()
69-
70- // Check for conflicts before saving
71- conflicts := checkConflicts(cfg, req.Mode, req.Action, req.Keys)
72- if len(conflicts) > 0 {
73- log.Warn().Int("conflicts", len(conflicts)).Msg("keybinding conflicts detected")
74- }
75-
76- updateKeybinding(cfg, req)
77-
78- if err := mgr.Save(cfg); err != nil {
79- return nil, fmt.Errorf("failed to save: %w", err)
80 }56 }
81 57
82 return map[string]any{58 return map[string]any{
83 "status": "success",59 "status": "success",
84- "conflicts": conflicts,60+ "conflicts": resp.Conflicts,
85 }, nil61 }, nil
86 }62 }
87 63
88 // HandleResetKeybinding resets a keybinding to default.64 // HandleResetKeybinding resets a keybinding to default.
89-func (*KeybindingsHandler) HandleResetKeybinding(ctx context.Context, _ webkit.WebViewID, payload json.RawMessage) (any, error) {65+func (h *KeybindingsHandler) HandleResetKeybinding(ctx context.Context, _ webkit.WebViewID, payload json.RawMessage) (any, error) {
90 log := logging.FromContext(ctx).With().Str("handler", "keybindings").Logger()66 log := logging.FromContext(ctx).With().Str("handler", "keybindings").Logger()
91 67
92 var req port.ResetKeybindingRequest68 var req port.ResetKeybindingRequest
@@ -96,249 +72,27 @@ func (*KeybindingsHandler) HandleResetKeybinding(ctx context.Context, _ webkit.W
96 72
97 log.Info().Str("mode", req.Mode).Str("action", req.Action).Msg("resetting keybinding to default")73 log.Info().Str("mode", req.Mode).Str("action", req.Action).Msg("resetting keybinding to default")
98 74
99- defaults := config.DefaultConfig()75+ if err := h.resetUC.Execute(ctx, req); err != nil {
100- defaultKeys := getDefaultKeys(defaults, req.Mode, req.Action)76+ return nil, err
101-
102- setReq := port.SetKeybindingRequest{
103- Mode: req.Mode,
104- Action: req.Action,
105- Keys: defaultKeys,
106- }
107-
108- mgr := config.GetManager()
109- if mgr == nil {
110- return nil, fmt.Errorf("config manager not initialized")
111- }
112-
113- cfg := mgr.Get()
114- updateKeybinding(cfg, setReq)
115-
116- if err := mgr.Save(cfg); err != nil {
117- return nil, fmt.Errorf("failed to save: %w", err)
118 }77 }
119 78
120 return map[string]any{"status": "success"}, nil79 return map[string]any{"status": "success"}, nil
121 }80 }
122 81
123 // HandleResetAllKeybindings resets all keybindings to defaults.82 // HandleResetAllKeybindings resets all keybindings to defaults.
124-func (*KeybindingsHandler) HandleResetAllKeybindings(ctx context.Context, _ webkit.WebViewID, _ json.RawMessage) (any, error) {83+func (h *KeybindingsHandler) HandleResetAllKeybindings(ctx context.Context, _ webkit.WebViewID, _ json.RawMessage) (any, error) {
125 log := logging.FromContext(ctx).With().Str("handler", "keybindings").Logger()84 log := logging.FromContext(ctx).With().Str("handler", "keybindings").Logger()
126 log.Info().Msg("resetting all keybindings to defaults")85 log.Info().Msg("resetting all keybindings to defaults")
127 86
128- mgr := config.GetManager()87+ if err := h.resetAllUC.Execute(ctx); err != nil {
129- if mgr == nil {88+ return nil, err
130- return nil, fmt.Errorf("config manager not initialized")
131- }
132-
133- cfg := mgr.Get()
134- defaults := config.DefaultConfig()
135-
136- // Reset all keybinding sections
137- cfg.Workspace.PaneMode.Actions = defaults.Workspace.PaneMode.Actions
138- cfg.Workspace.TabMode.Actions = defaults.Workspace.TabMode.Actions
139- cfg.Workspace.ResizeMode.Actions = defaults.Workspace.ResizeMode.Actions
140- cfg.Workspace.Shortcuts = defaults.Workspace.Shortcuts
141- cfg.Session.SessionMode.Actions = defaults.Session.SessionMode.Actions
142-
143- if err := mgr.Save(cfg); err != nil {
144- return nil, fmt.Errorf("failed to save: %w", err)
145 }89 }
146 90
147 return map[string]any{"status": "success"}, nil91 return map[string]any{"status": "success"}, nil
148 }92 }
149 93
150-// buildGlobalGroup builds the global shortcuts group.
151-func buildGlobalGroup(cfg, defaults *config.Config) port.KeybindingGroup {
152- return port.KeybindingGroup{
153- Mode: modeGlobal,
154- DisplayName: "Global Shortcuts",
155- Bindings: buildModeBindings(cfg.Workspace.Shortcuts.Actions, defaults.Workspace.Shortcuts.Actions),
156- }
157-}
158-
159-// buildPaneModeGroup builds the pane mode group.
160-func buildPaneModeGroup(cfg, defaults *config.Config) port.KeybindingGroup {
161- return port.KeybindingGroup{
162- Mode: "pane",
163- DisplayName: "Pane Mode",
164- Bindings: buildModeBindings(cfg.Workspace.PaneMode.Actions, defaults.Workspace.PaneMode.Actions),
165- Activation: cfg.Workspace.PaneMode.ActivationShortcut,
166- }
167-}
168-
169-// buildTabModeGroup builds the tab mode group.
170-func buildTabModeGroup(cfg, defaults *config.Config) port.KeybindingGroup {
171- return port.KeybindingGroup{
172- Mode: "tab",
173- DisplayName: "Tab Mode",
174- Bindings: buildModeBindings(cfg.Workspace.TabMode.Actions, defaults.Workspace.TabMode.Actions),
175- Activation: cfg.Workspace.TabMode.ActivationShortcut,
176- }
177-}
178-
179-// buildResizeModeGroup builds the resize mode group.
180-func buildResizeModeGroup(cfg, defaults *config.Config) port.KeybindingGroup {
181- return port.KeybindingGroup{
182- Mode: "resize",
183- DisplayName: "Resize Mode",
184- Bindings: buildModeBindings(cfg.Workspace.ResizeMode.Actions, defaults.Workspace.ResizeMode.Actions),
185- Activation: cfg.Workspace.ResizeMode.ActivationShortcut,
186- }
187-}
188-
189-// buildSessionModeGroup builds the session mode group.
190-func buildSessionModeGroup(cfg, defaults *config.Config) port.KeybindingGroup {
191- return port.KeybindingGroup{
192- Mode: "session",
193- DisplayName: "Session Mode",
194- Bindings: buildModeBindings(cfg.Session.SessionMode.Actions, defaults.Session.SessionMode.Actions),
195- Activation: cfg.Session.SessionMode.ActivationShortcut,
196- }
197-}
198-
199-// buildModeBindings builds keybinding entries for a modal mode.
200-func buildModeBindings(actions, defaultActions map[string]config.ActionBinding) []port.KeybindingEntry {
201- var entries []port.KeybindingEntry
202-
203- // Get all action names and sort them
204- actionNames := make([]string, 0, len(actions))
205- for action := range actions {
206- actionNames = append(actionNames, action)
207- }
208- sort.Strings(actionNames)
209-
210- for _, action := range actionNames {
211- binding := actions[action]
212- defaultBinding := defaultActions[action]
213-
214- entries = append(entries, port.KeybindingEntry{
215- Action: action,
216- Description: binding.Desc,
217- Keys: binding.Keys,
218- DefaultKeys: defaultBinding.Keys,
219- IsCustom: !reflect.DeepEqual(binding.Keys, defaultBinding.Keys),
220- })
221- }
222-
223- return entries
224-}
225-
226-// updateKeybinding updates a keybinding in the config.
227-func updateKeybinding(cfg *config.Config, req port.SetKeybindingRequest) {
228- switch req.Mode {
229- case modeGlobal:
230- updateGlobalShortcut(cfg, req.Action, req.Keys)
231- case "pane":
232- if existing, ok := cfg.Workspace.PaneMode.Actions[req.Action]; ok {
233- existing.Keys = req.Keys
234- cfg.Workspace.PaneMode.Actions[req.Action] = existing
235- }
236- case "tab":
237- if existing, ok := cfg.Workspace.TabMode.Actions[req.Action]; ok {
238- existing.Keys = req.Keys
239- cfg.Workspace.TabMode.Actions[req.Action] = existing
240- }
241- case "resize":
242- if existing, ok := cfg.Workspace.ResizeMode.Actions[req.Action]; ok {
243- existing.Keys = req.Keys
244- cfg.Workspace.ResizeMode.Actions[req.Action] = existing
245- }
246- case "session":
247- if existing, ok := cfg.Session.SessionMode.Actions[req.Action]; ok {
248- existing.Keys = req.Keys
249- cfg.Session.SessionMode.Actions[req.Action] = existing
250- }
251- }
252-}
253-
254-// updateGlobalShortcut updates a global shortcut in the config.
255-func updateGlobalShortcut(cfg *config.Config, action string, keys []string) {
256- if existing, ok := cfg.Workspace.Shortcuts.Actions[action]; ok {
257- existing.Keys = keys
258- cfg.Workspace.Shortcuts.Actions[action] = existing
259- }
260-}
261-
262-// getDefaultKeys returns the default keys for an action.
263-func getDefaultKeys(defaults *config.Config, mode, action string) []string {
264- switch mode {
265- case modeGlobal:
266- return getDefaultGlobalKeys(defaults, action)
267- case "pane":
268- return defaults.Workspace.PaneMode.Actions[action].Keys
269- case "tab":
270- return defaults.Workspace.TabMode.Actions[action].Keys
271- case "resize":
272- return defaults.Workspace.ResizeMode.Actions[action].Keys
273- case "session":
274- return defaults.Session.SessionMode.Actions[action].Keys
275- }
276- return nil
277-}
278-
279-// getDefaultGlobalKeys returns the default key for a global shortcut.
280-func getDefaultGlobalKeys(defaults *config.Config, action string) []string {
281- if binding, ok := defaults.Workspace.Shortcuts.Actions[action]; ok {
282- return binding.Keys
283- }
284- return nil
285-}
286-
287-// checkConflicts scans all modes for keybinding conflicts.
288-// Returns a list of conflicts where the same key is bound to different actions.
289-func checkConflicts(cfg *config.Config, targetMode, targetAction string, newKeys []string) []port.KeybindingConflict {
290- var conflicts []port.KeybindingConflict
291-
292- // Build a map of key -> (mode, action) for all bindings
293- type binding struct {
294- mode string
295- action string
296- }
297- keyMap := make(map[string][]binding)
298-
299- // Helper to add bindings from a mode
300- addBindings := func(mode string, actions map[string]config.ActionBinding) {
301- for action, ab := range actions {
302- for _, key := range ab.Keys {
303- keyMap[key] = append(keyMap[key], binding{mode: mode, action: action})
304- }
305- }
306- }
307-
308- // Collect all current bindings
309- addBindings(modeGlobal, cfg.Workspace.Shortcuts.Actions)
310- addBindings("pane", cfg.Workspace.PaneMode.Actions)
311- addBindings("tab", cfg.Workspace.TabMode.Actions)
312- addBindings("resize", cfg.Workspace.ResizeMode.Actions)
313- addBindings("session", cfg.Session.SessionMode.Actions)
314-
315- // Check each new key for conflicts
316- for _, newKey := range newKeys {
317- if existing, ok := keyMap[newKey]; ok {
318- for _, b := range existing {
319- // Skip if it's the same action we're updating
320- if b.mode == targetMode && b.action == targetAction {
321- continue
322- }
323- // Global shortcuts conflict with everything
324- // Modal bindings only conflict within same mode or with global
325- if targetMode == modeGlobal || b.mode == modeGlobal || b.mode == targetMode {
326- conflicts = append(conflicts, port.KeybindingConflict{
327- ConflictingAction: b.action,
328- ConflictingMode: b.mode,
329- Key: newKey,
330- })
331- }
332- }
333- }
334- }
335-
336- return conflicts
337-}
338-
339 // RegisterKeybindingsHandlers registers keybindings handlers with the router.94 // RegisterKeybindingsHandlers registers keybindings handlers with the router.
340-func RegisterKeybindingsHandlers(ctx context.Context, router *webkit.MessageRouter) error {95+func RegisterKeybindingsHandlers(ctx context.Context, router *webkit.MessageRouter, handler *KeybindingsHandler) error {
341- handler := NewKeybindingsHandler()
342 log := logging.FromContext(ctx).With().Str("component", "handlers").Logger()96 log := logging.FromContext(ctx).With().Str("component", "handlers").Logger()
343 97
344 // Get all keybindings98 // Get all keybindings
M internal/infrastructure/webkit/handlers/register.go +23 -1
@@ -2,6 +2,7 @@ package handlers
22
33 import (
44 "context"
5+ "fmt"
56
67 "github.com/bnema/dumber/internal/application/port"
78 "github.com/bnema/dumber/internal/application/usecase"
@@ -37,7 +38,11 @@ func RegisterAll(ctx context.Context, router *webkit.MessageRouter, cfg Config)
3738 }
3839
3940 // Keybindings handlers (always available)
40- if err := RegisterKeybindingsHandlers(ctx, router); err != nil {
41+ keybindingsHandler, err := createKeybindingsHandler()
42+ if err != nil {
43+ return fmt.Errorf("failed to create keybindings handler: %w", err)
44+ }
45+ if err := RegisterKeybindingsHandlers(ctx, router, keybindingsHandler); err != nil {
4146 return err
4247 }
4348
@@ -55,3 +60,20 @@ func RegisterAll(ctx context.Context, router *webkit.MessageRouter, cfg Config)
5560
5661 return nil
5762 }
63+
64+// createKeybindingsHandler wires the gateway and use cases for keybindings.
65+func createKeybindingsHandler() (*KeybindingsHandler, error) {
66+ mgr := config.GetManager()
67+ if mgr == nil {
68+ return nil, fmt.Errorf("config manager not initialized")
69+ }
70+
71+ gateway := config.NewKeybindingsGateway(mgr)
72+
73+ return NewKeybindingsHandler(
74+ usecase.NewGetKeybindingsUseCase(gateway),
75+ usecase.NewSetKeybindingUseCase(gateway, gateway),
76+ usecase.NewResetKeybindingUseCase(gateway),
77+ usecase.NewResetAllKeybindingsUseCase(gateway),
78+ ), nil
79+}
@@ -2,6 +2,7 @@ package handlers
2 2
3 import (3 import (
4 "context"4 "context"
5+ "fmt"
5 6
6 "github.com/bnema/dumber/internal/application/port"7 "github.com/bnema/dumber/internal/application/port"
7 "github.com/bnema/dumber/internal/application/usecase"8 "github.com/bnema/dumber/internal/application/usecase"
@@ -37,7 +38,11 @@ func RegisterAll(ctx context.Context, router *webkit.MessageRouter, cfg Config)
37 }38 }
38 39
39 // Keybindings handlers (always available)40 // Keybindings handlers (always available)
40- if err := RegisterKeybindingsHandlers(ctx, router); err != nil {41+ keybindingsHandler, err := createKeybindingsHandler()
42+ if err != nil {
43+ return fmt.Errorf("failed to create keybindings handler: %w", err)
44+ }
45+ if err := RegisterKeybindingsHandlers(ctx, router, keybindingsHandler); err != nil {
41 return err46 return err
42 }47 }
43 48
@@ -55,3 +60,20 @@ func RegisterAll(ctx context.Context, router *webkit.MessageRouter, cfg Config)
55 60
56 return nil61 return nil
57 }62 }
63+
64+// createKeybindingsHandler wires the gateway and use cases for keybindings.
65+func createKeybindingsHandler() (*KeybindingsHandler, error) {
66+ mgr := config.GetManager()
67+ if mgr == nil {
68+ return nil, fmt.Errorf("config manager not initialized")
69+ }
70+
71+ gateway := config.NewKeybindingsGateway(mgr)
72+
73+ return NewKeybindingsHandler(
74+ usecase.NewGetKeybindingsUseCase(gateway),
75+ usecase.NewSetKeybindingUseCase(gateway, gateway),
76+ usecase.NewResetKeybindingUseCase(gateway),
77+ usecase.NewResetAllKeybindingsUseCase(gateway),
78+ ), nil
79+}
M internal/infrastructure/webkit/messaging.go +3 -0
@@ -174,6 +174,8 @@ func (r *MessageRouter) SetupMessageHandler(ucm *webkit.UserContentManager, _ st
174174 func (r *MessageRouter) handleScriptMessage(senderUCM webkit.UserContentManager, valuePtr uintptr) {
175175 log := logging.FromContext(r.baseCtx).With().Str("component", "message-router").Logger()
176176
177+ log.Debug().Uint64("value_ptr", uint64(valuePtr)).Msg("handleScriptMessage entry")
178+
177179 if valuePtr == 0 {
178180 log.Warn().Msg("received script message with nil value pointer")
179181 return
@@ -186,6 +188,7 @@ func (r *MessageRouter) handleScriptMessage(senderUCM webkit.UserContentManager,
186188 }
187189
188190 rawJSON := jscValue.ToJson(0)
191+ log.Debug().Int("json_len", len(rawJSON)).Msg("ToJson result")
189192 if rawJSON == "" {
190193 // This typically happens when the JS side sends non-serializable values
191194 // like Svelte 5 Proxy objects or functions. The frontend should use
@@ -174,6 +174,8 @@ func (r *MessageRouter) SetupMessageHandler(ucm *webkit.UserContentManager, _ st
174 func (r *MessageRouter) handleScriptMessage(senderUCM webkit.UserContentManager, valuePtr uintptr) {174 func (r *MessageRouter) handleScriptMessage(senderUCM webkit.UserContentManager, valuePtr uintptr) {
175 log := logging.FromContext(r.baseCtx).With().Str("component", "message-router").Logger()175 log := logging.FromContext(r.baseCtx).With().Str("component", "message-router").Logger()
176 176
177+ log.Debug().Uint64("value_ptr", uint64(valuePtr)).Msg("handleScriptMessage entry")
178+
177 if valuePtr == 0 {179 if valuePtr == 0 {
178 log.Warn().Msg("received script message with nil value pointer")180 log.Warn().Msg("received script message with nil value pointer")
179 return181 return
@@ -186,6 +188,7 @@ func (r *MessageRouter) handleScriptMessage(senderUCM webkit.UserContentManager,
186 }188 }
187 189
188 rawJSON := jscValue.ToJson(0)190 rawJSON := jscValue.ToJson(0)
191+ log.Debug().Int("json_len", len(rawJSON)).Msg("ToJson result")
189 if rawJSON == "" {192 if rawJSON == "" {
190 // This typically happens when the JS side sends non-serializable values193 // This typically happens when the JS side sends non-serializable values
191 // like Svelte 5 Proxy objects or functions. The frontend should use194 // like Svelte 5 Proxy objects or functions. The frontend should use

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