1package usecase
2
3import (
4 "context"
5 "errors"
6 "fmt"
7
8 "github.com/bnema/dumber/internal/application/dto"
9 "github.com/bnema/dumber/internal/application/port"
10)
11
12var (
13 errUnsupportedVimNavigationAction = errors.New("vim navigation: unsupported action")
14 errUnsupportedVimNavigationEngine = errors.New("vim navigation: unsupported webview capability")
15)
16
17// VimNavigationUseCase maps configured Vim Mode navigation actions to optional
18// WebView capabilities. The UI layer supplies the active WebView and remains
19// responsible only for event and theme context.
20type VimNavigationUseCase struct{}
21
22// NewVimNavigationUseCase creates a stateless Vim navigation use case.
23func NewVimNavigationUseCase() *VimNavigationUseCase {
24 return &VimNavigationUseCase{}
25}
26
27// Execute dispatches a configured Vim action to the active WebView. The
28// outcome tells the UI whether the page now owns key input.
29func (uc *VimNavigationUseCase) Execute(
30 ctx context.Context, wv port.WebView, action string, count int, highlightColor string,
31) (dto.VimNavigationOutcome, error) {
32 if wv == nil {
33 return dto.VimNavigationOutcome{}, errors.New("vim navigation: nil webview")
34 }
35 if request, ok := vimPageInteractionRequest(action, highlightColor); ok {
36 return uc.startPageInteraction(ctx, wv, request)
37 }
38 return dto.VimNavigationOutcome{}, executeVimNavigation(ctx, wv, action, count, highlightColor)
39}
40
41func executeVimNavigation(ctx context.Context, wv port.WebView, action string, count int, highlightColor string) error {
42 switch action {
43 case "confirm":
44 activator, ok := wv.(port.SemanticNavigationActivator)
45 if !ok {
46 return errUnsupportedVimNavigationEngine
47 }
48 return activator.ActivateSemanticNavigationTarget(ctx)
49 case "focus-input":
50 focuser, ok := wv.(port.PageInputFocuser)
51 if !ok {
52 return errUnsupportedVimNavigationEngine
53 }
54 focuser.FocusNextInput()
55 return nil
56 }
57
58 var request dto.SemanticNavigationRequest
59 switch action {
60 case "heading-next":
61 request = dto.SemanticNavigationRequest{
62 Target: dto.SemanticNavigationTargetHeading,
63 Direction: dto.SemanticNavigationForward,
64 Count: count,
65 HighlightColor: highlightColor,
66 }
67 case "heading-prev":
68 request = dto.SemanticNavigationRequest{
69 Target: dto.SemanticNavigationTargetHeading,
70 Direction: dto.SemanticNavigationBackward,
71 Count: count,
72 HighlightColor: highlightColor,
73 }
74 default:
75 return errUnsupportedVimNavigationAction
76 }
77
78 navigator, ok := wv.(port.SemanticNavigable)
79 if !ok {
80 return errUnsupportedVimNavigationEngine
81 }
82 return navigator.NavigateSemantic(ctx, request)
83}
84
85var vimYankObjects = map[string]dto.VimPageTextObject{
86 "yank-section": dto.VimPageTextObjectSection,
87 "yank-paragraph": dto.VimPageTextObjectParagraph,
88 "yank-code": dto.VimPageTextObjectCode,
89 "yank-table": dto.VimPageTextObjectTable,
90 "yank-list": dto.VimPageTextObjectList,
91}
92
93func vimPageInteractionRequest(action, highlightColor string) (dto.VimPageInteractionRequest, bool) {
94 request := dto.VimPageInteractionRequest{HighlightColor: highlightColor}
95 switch action {
96 case "hint-follow":
97 request.Kind = dto.VimPageHintFollow
98 case "hint-follow-new":
99 request.Kind = dto.VimPageHintFollowNew
100 case "hint-yank-url":
101 request.Kind = dto.VimPageHintYankURL
102 case "visual":
103 request.Kind = dto.VimPageVisual
104 default:
105 object, ok := vimYankObjects[action]
106 if !ok {
107 return dto.VimPageInteractionRequest{}, false
108 }
109 request.Kind = dto.VimPageYankObject
110 request.Object = object
111 }
112 return request, true
113}
114
115func (*VimNavigationUseCase) startPageInteraction(
116 ctx context.Context, wv port.WebView, request dto.VimPageInteractionRequest,
117) (dto.VimNavigationOutcome, error) {
118 interactor, ok := wv.(port.VimPageInteractor)
119 if !ok {
120 return dto.VimNavigationOutcome{}, errUnsupportedVimNavigationEngine
121 }
122 if err := interactor.StartVimPageInteraction(ctx, request); err != nil {
123 return dto.VimNavigationOutcome{}, fmt.Errorf("vim navigation: start page interaction: %w", err)
124 }
125 if !request.Kind.CapturesKeys() {
126 return dto.VimNavigationOutcome{}, nil
127 }
128 // Every key-capturing interaction opens on text or link hints; the page
129 // reports later sub-modes itself.
130 return dto.VimNavigationOutcome{CapturePageKeys: true, PageKind: request.Kind, PageMode: dto.VimPageModeHints}, nil
131}
132
133// SendPageKey forwards one canonical key to the active in-page interaction.
134func (*VimNavigationUseCase) SendPageKey(ctx context.Context, wv port.WebView, key string) error {
135 interactor, ok := wv.(port.VimPageInteractor)
136 if !ok {
137 return errUnsupportedVimNavigationEngine
138 }
139 if err := interactor.SendVimPageKey(ctx, key); err != nil {
140 return fmt.Errorf("vim navigation: send page key: %w", err)
141 }
142 return nil
143}
144
145// CancelPageInteraction ends any in-page interaction, removing its overlay or
146// selection. WebViews without the capability have nothing to cancel.
147func (*VimNavigationUseCase) CancelPageInteraction(ctx context.Context, wv port.WebView) error {
148 interactor, ok := wv.(port.VimPageInteractor)
149 if !ok {
150 return nil
151 }
152 if err := interactor.CancelVimPageInteraction(ctx); err != nil {
153 return fmt.Errorf("vim navigation: cancel page interaction: %w", err)
154 }
155 return nil
156}
157
158// ClearSemanticNavigationHighlight removes the visual target left by semantic
159// navigation when the active Vim Mode session ends.
160func (*VimNavigationUseCase) ClearSemanticNavigationHighlight(ctx context.Context, wv port.WebView) error {
161 if wv == nil {
162 return nil
163 }
164 clearer, ok := wv.(port.SemanticNavigationHighlightClearer)
165 if !ok {
166 return nil
167 }
168 return clearer.ClearSemanticNavigationHighlight(ctx)
169}
170
171// NavigatePageFocus keeps focus traversal inside a page when the active page
172// control would otherwise hand the host window a Tab event.
173func (*VimNavigationUseCase) NavigatePageFocus(wv port.WebView, backward bool) bool {
174 if wv == nil {
175 return false
176 }
177 navigator, ok := wv.(port.PageFocusNavigator)
178 if !ok {
179 return false
180 }
181 navigator.NavigatePageFocus(backward)
182 return true
183}