1package input
2
3import (
4 "context"
5 "sync"
6 "time"
7
8 "github.com/bnema/dumber/internal/logging"
9)
10
11// Mode represents the current input mode.
12type Mode int
13
14const (
15 // ModeNormal is the default mode where keys pass through to WebView.
16 ModeNormal Mode = iota
17 // ModeTab is the modal tab management mode.
18 ModeTab
19 // ModePane is the modal pane management mode.
20 ModePane
21 // ModeSession is the modal session management mode.
22 ModeSession
23 // ModeResize is the modal pane resizing mode.
24 ModeResize
25)
26
27// String returns a human-readable mode name.
28func (m Mode) String() string {
29 switch m {
30 case ModeNormal:
31 return "normal"
32 case ModeTab:
33 return "tab"
34 case ModePane:
35 return "pane"
36 case ModeSession:
37 return "session"
38 case ModeResize:
39 return "resize"
40 default:
41 return "unknown"
42 }
43}
44
45// ModalState manages the current input mode with optional timeout.
46type ModalState struct {
47 mode Mode
48 timeout time.Duration
49 timer *time.Timer
50
51 // Callback for mode changes (called synchronously under lock).
52 onModeChange func(from, to Mode)
53
54 mu sync.RWMutex
55}
56
57// NewModalState creates a new modal state manager.
58func NewModalState(ctx context.Context) *ModalState {
59 log := logging.FromContext(ctx)
60 log.Debug().Msg("creating modal state")
61 return &ModalState{
62 mode: ModeNormal,
63 }
64}
65
66// Mode returns the current mode (thread-safe).
67func (m *ModalState) Mode() Mode {
68 m.mu.RLock()
69 defer m.mu.RUnlock()
70 return m.mode
71}
72
73// EnterTabMode switches to tab mode with an optional timeout.
74// If timeout is 0, the mode stays until explicitly exited.
75func (m *ModalState) EnterTabMode(ctx context.Context, timeout time.Duration) {
76 log := logging.FromContext(ctx)
77 m.mu.Lock()
78 defer m.mu.Unlock()
79
80 if m.mode == ModeTab {
81 // Already in tab mode, just reset timeout
82 m.resetTimeoutLocked(ctx, timeout)
83 return
84 }
85
86 m.cancelTimerLocked()
87 oldMode := m.mode
88 m.mode = ModeTab
89 m.timeout = timeout
90
91 if timeout > 0 {
92 m.timer = time.AfterFunc(timeout, func() {
93 m.ExitMode(ctx)
94 })
95 }
96
97 log.Debug().
98 Str("from", oldMode.String()).
99 Str("to", m.mode.String()).
100 Dur("timeout", timeout).
101 Msg("entered tab mode")
102
103 if m.onModeChange != nil {
104 m.onModeChange(oldMode, m.mode)
105 }
106}
107
108// EnterPaneMode switches to pane mode with an optional timeout.
109// If timeout is 0, the mode stays until explicitly exited.
110func (m *ModalState) EnterPaneMode(ctx context.Context, timeout time.Duration) {
111 log := logging.FromContext(ctx)
112 m.mu.Lock()
113 defer m.mu.Unlock()
114
115 if m.mode == ModePane {
116 // Already in pane mode, just reset timeout
117 m.resetTimeoutLocked(ctx, timeout)
118 return
119 }
120
121 m.cancelTimerLocked()
122 oldMode := m.mode
123 m.mode = ModePane
124 m.timeout = timeout
125
126 if timeout > 0 {
127 m.timer = time.AfterFunc(timeout, func() {
128 m.ExitMode(ctx)
129 })
130 }
131
132 log.Debug().
133 Str("from", oldMode.String()).
134 Str("to", m.mode.String()).
135 Dur("timeout", timeout).
136 Msg("entered pane mode")
137
138 if m.onModeChange != nil {
139 m.onModeChange(oldMode, m.mode)
140 }
141}
142
143// EnterSessionMode switches to session mode with an optional timeout.
144// If timeout is 0, the mode stays until explicitly exited.
145func (m *ModalState) EnterSessionMode(ctx context.Context, timeout time.Duration) {
146 log := logging.FromContext(ctx)
147 m.mu.Lock()
148 defer m.mu.Unlock()
149
150 if m.mode == ModeSession {
151 // Already in session mode, just reset timeout
152 m.resetTimeoutLocked(ctx, timeout)
153 return
154 }
155
156 m.cancelTimerLocked()
157 oldMode := m.mode
158 m.mode = ModeSession
159 m.timeout = timeout
160
161 if timeout > 0 {
162 m.timer = time.AfterFunc(timeout, func() {
163 m.ExitMode(ctx)
164 })
165 }
166
167 log.Debug().
168 Str("from", oldMode.String()).
169 Str("to", m.mode.String()).
170 Dur("timeout", timeout).
171 Msg("entered session mode")
172
173 if m.onModeChange != nil {
174 m.onModeChange(oldMode, m.mode)
175 }
176}
177
178// EnterResizeMode switches to resize mode with an optional timeout.
179// If timeout is 0, the mode stays until explicitly exited.
180func (m *ModalState) EnterResizeMode(ctx context.Context, timeout time.Duration) {
181 log := logging.FromContext(ctx)
182 m.mu.Lock()
183 defer m.mu.Unlock()
184
185 if m.mode == ModeResize {
186 // Already in resize mode, just reset timeout
187 m.resetTimeoutLocked(ctx, timeout)
188 return
189 }
190
191 m.cancelTimerLocked()
192 oldMode := m.mode
193 m.mode = ModeResize
194 m.timeout = timeout
195
196 if timeout > 0 {
197 m.timer = time.AfterFunc(timeout, func() {
198 m.ExitMode(ctx)
199 })
200 }
201
202 log.Debug().
203 Str("from", oldMode.String()).
204 Str("to", m.mode.String()).
205 Dur("timeout", timeout).
206 Msg("entered resize mode")
207
208 if m.onModeChange != nil {
209 m.onModeChange(oldMode, m.mode)
210 }
211}
212
213// ExitMode returns to normal mode.
214func (m *ModalState) ExitMode(ctx context.Context) {
215 log := logging.FromContext(ctx)
216 m.mu.Lock()
217 defer m.mu.Unlock()
218
219 if m.mode == ModeNormal {
220 return
221 }
222
223 m.cancelTimerLocked()
224 oldMode := m.mode
225 m.mode = ModeNormal
226 m.timeout = 0
227
228 log.Debug().
229 Str("from", oldMode.String()).
230 Str("to", "normal").
231 Msg("exited modal mode")
232
233 if m.onModeChange != nil {
234 m.onModeChange(oldMode, ModeNormal)
235 }
236}
237
238// ResetTimeout restarts the mode timeout (e.g., after a valid keystroke).
239// Does nothing if not in a modal mode or if no timeout was set.
240func (m *ModalState) ResetTimeout(ctx context.Context) {
241 m.mu.Lock()
242 defer m.mu.Unlock()
243
244 if m.mode == ModeNormal || m.timeout == 0 {
245 return
246 }
247
248 m.resetTimeoutLocked(ctx, m.timeout)
249}
250
251// SetOnModeChange sets the callback for mode changes.
252// The callback is invoked synchronously under the lock.
253func (m *ModalState) SetOnModeChange(fn func(from, to Mode)) {
254 m.mu.Lock()
255 defer m.mu.Unlock()
256 m.onModeChange = fn
257}
258
259// cancelTimerLocked stops any active timeout timer.
260// Must be called with m.mu held.
261func (m *ModalState) cancelTimerLocked() {
262 if m.timer != nil {
263 m.timer.Stop()
264 m.timer = nil
265 }
266}
267
268// resetTimeoutLocked resets the timeout timer.
269// Must be called with m.mu held.
270func (m *ModalState) resetTimeoutLocked(ctx context.Context, timeout time.Duration) {
271 m.cancelTimerLocked()
272 m.timeout = timeout
273 if timeout > 0 {
274 m.timer = time.AfterFunc(timeout, func() {
275 m.ExitMode(ctx)
276 })
277 }
278}