1package ui
2
3import (
4 "context"
5
6 "github.com/bnema/dumber/internal/application/dto"
7 "github.com/bnema/dumber/internal/application/port"
8 "github.com/bnema/dumber/internal/domain/entity"
9 "github.com/bnema/dumber/internal/logging"
10)
11
12// captureVimPageKeys routes this window's Vim Mode keys to the in-page
13// interaction running in wv until the page reports its end. The outcome names
14// the interaction and the sub-mode it starts in.
15func (a *App) captureVimPageKeys(ctx context.Context, bw *browserWindow, wv port.WebView, outcome dto.VimNavigationOutcome) {
16 if bw == nil || bw.keyboardHandler == nil || wv == nil {
17 return
18 }
19 bw.vimPageInteractionWebView = wv
20 bw.vimPageInteractionKind = outcome.PageKind
21 label, _ := vimPageLabel(outcome.PageKind, outcome.PageMode)
22 // Capture first: it resets the pending sequence, and the toast and legend
23 // below must reflect the sub-mode, not a cleared sequence.
24 bw.keyboardHandler.SetPageKeyCapture(func(key string) {
25 if err := a.vimNavigationUseCase().SendPageKey(ctx, wv, key); err != nil {
26 logging.FromContext(ctx).Debug().Err(err).Msg("vim page key forwarding failed")
27 a.endVimPageInteraction(ctx, bw, a.vimModeActiveForBrowserWindow(bw))
28 }
29 })
30 a.showVimModeToast(ctx, bw, label)
31 if bw.modeToaster != nil {
32 bw.modeToaster.setSuspended(true)
33 }
34 logging.FromContext(ctx).Debug().
35 Str("window_id", bw.id).
36 Str("sub_mode", label).
37 Msg("vim page interaction capturing keys")
38}
39
40// releaseVimPageKeys returns this window's keys to Vim Mode bindings and
41// restores the plain indicator and legend when stillVim reports the window
42// remains in Vim Mode. The caller supplies it because the mode-change callback
43// runs under the modal lock, where reading the mode back would deadlock.
44// It reports the WebView that owned the interaction, if any.
45func (a *App) releaseVimPageKeys(ctx context.Context, bw *browserWindow, reason string, stillVim bool) port.WebView {
46 wv := bw.vimPageInteractionWebView
47 bw.vimPageInteractionWebView = nil
48 bw.vimPageInteractionKind = 0
49 if bw.keyboardHandler != nil {
50 bw.keyboardHandler.ClearPageKeyCapture()
51 }
52 if wv == nil {
53 return nil
54 }
55 if stillVim {
56 a.showVimModeToast(ctx, bw, "")
57 if bw.modeToaster != nil {
58 bw.modeToaster.setSuspended(false)
59 }
60 }
61 logging.FromContext(ctx).Debug().
62 Str("window_id", bw.id).
63 Str("reason", reason).
64 Msg("vim page interaction released keys")
65 return wv
66}
67
68// vimPageInteractionOwner returns the window whose key capture belongs to the
69// interaction running in paneID's WebView, or nil when that pane owns none.
70func (a *App) vimPageInteractionOwner(paneID entity.PaneID) *browserWindow {
71 bw := a.browserWindowForAnyPane(paneID)
72 if bw == nil || bw.vimPageInteractionWebView == nil || a.contentCoord == nil {
73 return nil
74 }
75 if a.contentCoord.GetWebView(paneID) != bw.vimPageInteractionWebView {
76 return nil
77 }
78 return bw
79}
80
81// handleVimPageInteractionEnded releases key capture when the page reports
82// that its hint or visual interaction finished.
83func (a *App) handleVimPageInteractionEnded(ctx context.Context, paneID entity.PaneID) {
84 bw := a.vimPageInteractionOwner(paneID)
85 if bw == nil {
86 return
87 }
88 a.releaseVimPageKeys(ctx, bw, "page-reported-end", a.vimModeActiveForBrowserWindow(bw))
89}
90
91// handleVimPageModeChanged repaints the indicator when the page moves its
92// interaction to another sub-mode (hints, caret, visual). Key capture and the
93// legend suspension stay as they are.
94func (a *App) handleVimPageModeChanged(ctx context.Context, paneID entity.PaneID, mode dto.VimPageMode) {
95 if bw := a.vimPageInteractionOwner(paneID); bw != nil {
96 a.showVimPageMode(ctx, bw, mode)
97 }
98}
99
100// showVimPageMode paints the sub-mode label for the interaction owned by bw.
101func (a *App) showVimPageMode(ctx context.Context, bw *browserWindow, mode dto.VimPageMode) {
102 if bw.vimPageInteractionWebView == nil {
103 return
104 }
105 if label, ok := vimPageLabel(bw.vimPageInteractionKind, mode); ok {
106 a.showVimModeToast(ctx, bw, label)
107 }
108}
109
110// vimPageModeLabel names a page-reported sub-mode for the Vim Mode indicator.
111func vimPageModeLabel(mode dto.VimPageMode) (string, bool) {
112 switch mode {
113 case dto.VimPageModeHints:
114 return "HINTS", true
115 case dto.VimPageModeCaret:
116 return "CARET", true
117 case dto.VimPageModeVisual:
118 return "VISUAL", true
119 case dto.VimPageModeVisualLine:
120 return "VISUAL LINE", true
121 default:
122 return "", false
123 }
124}
125
126// vimPageLabel is the single source of the indicator label: the sub-mode name,
127// plus the target of link hints that do more than follow (new pane, yank URL).
128func vimPageLabel(kind dto.VimPageInteractionKind, mode dto.VimPageMode) (string, bool) {
129 label, ok := vimPageModeLabel(mode)
130 if !ok || mode != dto.VimPageModeHints {
131 return label, ok
132 }
133 switch kind {
134 case dto.VimPageHintFollowNew:
135 return label + " · NEW PANE", true
136 case dto.VimPageHintYankURL:
137 return label + " · YANK URL", true
138 default:
139 return label, true
140 }
141}
142
143// endVimPageInteraction cancels any in-page interaction owned by this window.
144// stillVim reports whether the window stays in Vim Mode (see releaseVimPageKeys).
145func (a *App) endVimPageInteraction(ctx context.Context, bw *browserWindow, stillVim bool) {
146 if bw == nil {
147 return
148 }
149 wv := a.releaseVimPageKeys(ctx, bw, "canceled", stillVim)
150 if wv == nil {
151 return
152 }
153 if err := a.vimNavigationUseCase().CancelPageInteraction(ctx, wv); err != nil {
154 logging.FromContext(ctx).Debug().Err(err).Msg("failed to cancel vim page interaction")
155 }
156}