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 Verified
CD88F9E564E0102646FB6077A1A07C7C7C3E349D· 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 { | |||
| 46 | 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 | 54 | // KeybindingsProvider provides keybinding configuration data. | |
| 50 | 55 | type KeybindingsProvider interface { | |
| 51 | 56 | GetKeybindings(ctx context.Context) (KeybindingsConfig, error) | |
| 52 | 57 | GetDefaultKeybindings(ctx context.Context) (KeybindingsConfig, error) | |
| 58 | + | CheckConflicts(ctx context.Context, mode, action string, keys []string) ([]KeybindingConflict, error) | |
| 53 | 59 | } | |
| 54 | 60 | ||
| 55 | 61 | // 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 { | |||
| 1589 | 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 | 1672 | // GetDefaultKeybindings provides a mock function for the type MockKeybindingsProvider | |
| 1593 | 1673 | func (_mock *MockKeybindingsProvider) GetDefaultKeybindings(ctx context.Context) (port.KeybindingsConfig, error) { | |
| 1594 | 1674 | 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 MockKeybindingsProvider | 1672 | // 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 | |||
| 36 | 36 | ||
| 37 | 37 | // SetKeybindingUseCase updates a single keybinding. | |
| 38 | 38 | type SetKeybindingUseCase struct { | |
| 39 | - | saver port.KeybindingsSaver | |
| 39 | + | provider port.KeybindingsProvider | |
| 40 | + | saver port.KeybindingsSaver | |
| 40 | 41 | } | |
| 41 | 42 | ||
| 42 | 43 | // 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} | |
| 45 | 46 | } | |
| 46 | 47 | ||
| 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) { | |
| 49 | 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 | 54 | 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 | |
| 55 | 70 | } | |
| 56 | 71 | ||
| 57 | - | return uc.saver.SetKeybinding(ctx, req) | |
| 72 | + | return port.SetKeybindingResponse{Conflicts: conflicts}, nil | |
| 58 | 73 | } | |
| 59 | 74 | ||
| 60 | 75 | // 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.KeybindingsSaver | 39 | + | 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 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 | |||
| 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) { | |||
| 74 | 74 | func TestSetKeybindingUseCase_Execute(t *testing.T) { | |
| 75 | 75 | t.Run("successfully sets keybinding", func(t *testing.T) { | |
| 76 | 76 | // Arrange | |
| 77 | + | mockProvider := mocks.NewMockKeybindingsProvider(t) | |
| 77 | 78 | mockSaver := mocks.NewMockKeybindingsSaver(t) | |
| 78 | 79 | req := port.SetKeybindingRequest{ | |
| 79 | 80 | Mode: "pane", | |
| 80 | 81 | Action: "split-right", | |
| 81 | 82 | Keys: []string{"r"}, | |
| 82 | 83 | } | |
| 84 | + | mockProvider.EXPECT().CheckConflicts(mock.Anything, req.Mode, req.Action, req.Keys).Return(nil, nil) | |
| 83 | 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 | 89 | // Act | |
| 88 | - | err := uc.Execute(context.Background(), req) | |
| 90 | + | resp, err := uc.Execute(context.Background(), req) | |
| 89 | 91 | ||
| 90 | 92 | // Assert | |
| 91 | 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 | 124 | t.Run("returns error when saver fails", func(t *testing.T) { | |
| 96 | 125 | // Arrange | |
| 126 | + | mockProvider := mocks.NewMockKeybindingsProvider(t) | |
| 97 | 127 | mockSaver := mocks.NewMockKeybindingsSaver(t) | |
| 98 | 128 | req := port.SetKeybindingRequest{ | |
| 99 | 129 | Mode: "pane", | |
| 100 | 130 | Action: "split-right", | |
| 101 | 131 | Keys: []string{"r"}, | |
| 102 | 132 | } | |
| 133 | + | mockProvider.EXPECT().CheckConflicts(mock.Anything, req.Mode, req.Action, req.Keys).Return(nil, nil) | |
| 103 | 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 | 138 | // Act | |
| 108 | - | err := uc.Execute(context.Background(), req) | |
| 139 | + | _, err := uc.Execute(context.Background(), req) | |
| 109 | 140 | ||
| 110 | 141 | // Assert | |
| 111 | 142 | require.Error(t, err) | |
| 112 | 143 | assert.Contains(t, err.Error(), "save error") | |
| 113 | - | mock.AssertExpectationsForObjects(t, mockSaver) | |
| 144 | + | mock.AssertExpectationsForObjects(t, mockProvider, mockSaver) | |
| 114 | 145 | }) | |
| 115 | 146 | ||
| 116 | 147 | t.Run("returns error when saver is nil", func(t *testing.T) { | |
| 117 | 148 | // Arrange | |
| 118 | - | uc := usecase.NewSetKeybindingUseCase(nil) | |
| 149 | + | uc := usecase.NewSetKeybindingUseCase(nil, nil) | |
| 119 | 150 | req := port.SetKeybindingRequest{ | |
| 120 | 151 | Mode: "pane", | |
| 121 | 152 | Action: "split-right", | |
| @@ -123,7 +154,7 @@ func TestSetKeybindingUseCase_Execute(t *testing.T) { | |||
| 123 | 154 | } | |
| 124 | 155 | ||
| 125 | 156 | // Act | |
| 126 | - | err := uc.Execute(context.Background(), req) | |
| 157 | + | _, err := uc.Execute(context.Background(), req) | |
| 127 | 158 | ||
| 128 | 159 | // Assert | |
| 129 | 160 | require.Error(t, err) | |
| @@ -132,6 +163,7 @@ func TestSetKeybindingUseCase_Execute(t *testing.T) { | |||
| 132 | 163 | ||
| 133 | 164 | t.Run("returns error when mode is empty", func(t *testing.T) { | |
| 134 | 165 | // Arrange | |
| 166 | + | mockProvider := mocks.NewMockKeybindingsProvider(t) | |
| 135 | 167 | mockSaver := mocks.NewMockKeybindingsSaver(t) | |
| 136 | 168 | req := port.SetKeybindingRequest{ | |
| 137 | 169 | Mode: "", | |
| @@ -139,10 +171,10 @@ func TestSetKeybindingUseCase_Execute(t *testing.T) { | |||
| 139 | 171 | Keys: []string{"r"}, | |
| 140 | 172 | } | |
| 141 | 173 | ||
| 142 | - | uc := usecase.NewSetKeybindingUseCase(mockSaver) | |
| 174 | + | uc := usecase.NewSetKeybindingUseCase(mockProvider, mockSaver) | |
| 143 | 175 | ||
| 144 | 176 | // Act | |
| 145 | - | err := uc.Execute(context.Background(), req) | |
| 177 | + | _, err := uc.Execute(context.Background(), req) | |
| 146 | 178 | ||
| 147 | 179 | // Assert | |
| 148 | 180 | require.Error(t, err) | |
| @@ -151,6 +183,7 @@ func TestSetKeybindingUseCase_Execute(t *testing.T) { | |||
| 151 | 183 | ||
| 152 | 184 | t.Run("returns error when action is empty", func(t *testing.T) { | |
| 153 | 185 | // Arrange | |
| 186 | + | mockProvider := mocks.NewMockKeybindingsProvider(t) | |
| 154 | 187 | mockSaver := mocks.NewMockKeybindingsSaver(t) | |
| 155 | 188 | req := port.SetKeybindingRequest{ | |
| 156 | 189 | Mode: "pane", | |
| @@ -158,10 +191,10 @@ func TestSetKeybindingUseCase_Execute(t *testing.T) { | |||
| 158 | 191 | Keys: []string{"r"}, | |
| 159 | 192 | } | |
| 160 | 193 | ||
| 161 | - | uc := usecase.NewSetKeybindingUseCase(mockSaver) | |
| 194 | + | uc := usecase.NewSetKeybindingUseCase(mockProvider, mockSaver) | |
| 162 | 195 | ||
| 163 | 196 | // Act | |
| 164 | - | err := uc.Execute(context.Background(), req) | |
| 197 | + | _, err := uc.Execute(context.Background(), req) | |
| 165 | 198 | ||
| 166 | 199 | // Assert | |
| 167 | 200 | require.Error(t, err) | |
| @@ -170,6 +203,7 @@ func TestSetKeybindingUseCase_Execute(t *testing.T) { | |||
| 170 | 203 | ||
| 171 | 204 | t.Run("returns error for invalid mode", func(t *testing.T) { | |
| 172 | 205 | // Arrange | |
| 206 | + | mockProvider := mocks.NewMockKeybindingsProvider(t) | |
| 173 | 207 | mockSaver := mocks.NewMockKeybindingsSaver(t) | |
| 174 | 208 | req := port.SetKeybindingRequest{ | |
| 175 | 209 | Mode: "invalid", | |
| @@ -177,10 +211,10 @@ func TestSetKeybindingUseCase_Execute(t *testing.T) { | |||
| 177 | 211 | Keys: []string{"r"}, | |
| 178 | 212 | } | |
| 179 | 213 | ||
| 180 | - | uc := usecase.NewSetKeybindingUseCase(mockSaver) | |
| 214 | + | uc := usecase.NewSetKeybindingUseCase(mockProvider, mockSaver) | |
| 181 | 215 | ||
| 182 | 216 | // Act | |
| 183 | - | err := uc.Execute(context.Background(), req) | |
| 217 | + | _, err := uc.Execute(context.Background(), req) | |
| 184 | 218 | ||
| 185 | 219 | // Assert | |
| 186 | 220 | 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 | // Arrange | 76 | // 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 | // Act | 89 | // Act | ||
| 88 | - | err := uc.Execute(context.Background(), req) | 90 | + | resp, err := uc.Execute(context.Background(), req) |
| 89 | 91 | ||||
| 90 | // Assert | 92 | // 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 | // Arrange | 125 | // 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 | // Act | 138 | // Act | ||
| 108 | - | err := uc.Execute(context.Background(), req) | 139 | + | _, err := uc.Execute(context.Background(), req) |
| 109 | 140 | ||||
| 110 | // Assert | 141 | // 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 | // Arrange | 148 | // 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 | // Act | 156 | // Act | ||
| 126 | - | err := uc.Execute(context.Background(), req) | 157 | + | _, err := uc.Execute(context.Background(), req) |
| 127 | 158 | ||||
| 128 | // Assert | 159 | // 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 | // Arrange | 165 | // 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 | // Act | 176 | // Act | ||
| 145 | - | err := uc.Execute(context.Background(), req) | 177 | + | _, err := uc.Execute(context.Background(), req) |
| 146 | 178 | ||||
| 147 | // Assert | 179 | // 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 | // Arrange | 185 | // 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 | // Act | 196 | // Act | ||
| 164 | - | err := uc.Execute(context.Background(), req) | 197 | + | _, err := uc.Execute(context.Background(), req) |
| 165 | 198 | ||||
| 166 | // Assert | 199 | // 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 | // Arrange | 205 | // 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 | // Act | 216 | // Act | ||
| 183 | - | err := uc.Execute(context.Background(), req) | 217 | + | _, err := uc.Execute(context.Background(), req) |
| 184 | 218 | ||||
| 185 | // Assert | 219 | // 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 ( | |||
| 4 | 4 | "context" | |
| 5 | 5 | "encoding/json" | |
| 6 | 6 | "fmt" | |
| 7 | - | "reflect" | |
| 8 | - | "sort" | |
| 9 | 7 | ||
| 10 | 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 | 10 | "github.com/bnema/dumber/internal/infrastructure/webkit" | |
| 13 | 11 | "github.com/bnema/dumber/internal/logging" | |
| 14 | 12 | ) | |
| 15 | 13 | ||
| 16 | - | const modeGlobal = "global" | |
| 17 | - | ||
| 18 | 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 | 22 | // 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 | + | } | |
| 24 | 35 | } | |
| 25 | 36 | ||
| 26 | 37 | // 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) | |
| 48 | 40 | } | |
| 49 | 41 | ||
| 50 | 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 | 44 | log := logging.FromContext(ctx).With().Str("handler", "keybindings").Logger() | |
| 53 | 45 | log.Debug().RawJSON("payload", payload).Msg("HandleSetKeybinding called") | |
| 54 | 46 | ||
| @@ -58,35 +50,19 @@ func (*KeybindingsHandler) HandleSetKeybinding(ctx context.Context, _ webkit.Web | |||
| 58 | 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") | |
| 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 | |
| 80 | 56 | } | |
| 81 | 57 | ||
| 82 | 58 | return map[string]any{ | |
| 83 | 59 | "status": "success", | |
| 84 | - | "conflicts": conflicts, | |
| 60 | + | "conflicts": resp.Conflicts, | |
| 85 | 61 | }, nil | |
| 86 | 62 | } | |
| 87 | 63 | ||
| 88 | 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 | 66 | log := logging.FromContext(ctx).With().Str("handler", "keybindings").Logger() | |
| 91 | 67 | ||
| 92 | 68 | var req port.ResetKeybindingRequest | |
| @@ -96,249 +72,27 @@ func (*KeybindingsHandler) HandleResetKeybinding(ctx context.Context, _ webkit.W | |||
| 96 | 72 | ||
| 97 | 73 | log.Info().Str("mode", req.Mode).Str("action", req.Action).Msg("resetting keybinding to default") | |
| 98 | 74 | ||
| 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 | |
| 118 | 77 | } | |
| 119 | 78 | ||
| 120 | 79 | return map[string]any{"status": "success"}, nil | |
| 121 | 80 | } | |
| 122 | 81 | ||
| 123 | 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 | 84 | log := logging.FromContext(ctx).With().Str("handler", "keybindings").Logger() | |
| 126 | 85 | log.Info().Msg("resetting all keybindings to defaults") | |
| 127 | 86 | ||
| 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 | |
| 145 | 89 | } | |
| 146 | 90 | ||
| 147 | 91 | 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 | 94 | // 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 { | |
| 342 | 96 | log := logging.FromContext(ctx).With().Str("component", "handlers").Logger() | |
| 343 | 97 | ||
| 344 | 98 | // 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 | }, nil | 61 | }, 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.ResetKeybindingRequest | 68 | 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"}, nil | 79 | 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"}, nil | 91 | 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 keybindings | 98 | // Get all keybindings | ||
M internal/infrastructure/webkit/handlers/register.go +23 -1
| @@ -2,6 +2,7 @@ package handlers | |||
| 2 | 2 | ||
| 3 | 3 | import ( | |
| 4 | 4 | "context" | |
| 5 | + | "fmt" | |
| 5 | 6 | ||
| 6 | 7 | "github.com/bnema/dumber/internal/application/port" | |
| 7 | 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 | 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 | 46 | return err | |
| 42 | 47 | } | |
| 43 | 48 | ||
| @@ -55,3 +60,20 @@ func RegisterAll(ctx context.Context, router *webkit.MessageRouter, cfg Config) | |||
| 55 | 60 | ||
| 56 | 61 | 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 | + | } | |
| @@ -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 err | 46 | return err | ||
| 42 | } | 47 | } | ||
| 43 | 48 | ||||
| @@ -55,3 +60,20 @@ func RegisterAll(ctx context.Context, router *webkit.MessageRouter, cfg Config) | |||||
| 55 | 60 | ||||
| 56 | return nil | 61 | 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 | |||
| 174 | 174 | func (r *MessageRouter) handleScriptMessage(senderUCM webkit.UserContentManager, valuePtr uintptr) { | |
| 175 | 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 | 179 | if valuePtr == 0 { | |
| 178 | 180 | log.Warn().Msg("received script message with nil value pointer") | |
| 179 | 181 | return | |
| @@ -186,6 +188,7 @@ func (r *MessageRouter) handleScriptMessage(senderUCM webkit.UserContentManager, | |||
| 186 | 188 | } | |
| 187 | 189 | ||
| 188 | 190 | rawJSON := jscValue.ToJson(0) | |
| 191 | + | log.Debug().Int("json_len", len(rawJSON)).Msg("ToJson result") | |
| 189 | 192 | if rawJSON == "" { | |
| 190 | 193 | // This typically happens when the JS side sends non-serializable values | |
| 191 | 194 | // 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 | return | 181 | 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 values | 193 | // This typically happens when the JS side sends non-serializable values | ||
| 191 | // like Svelte 5 Proxy objects or functions. The frontend should use | 194 | // like Svelte 5 Proxy objects or functions. The frontend should use | ||