modal.go RawHistoryFolder
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)
22
23// String returns a human-readable mode name.
24func (m Mode) String() string {
25 switch m {
26 case ModeNormal:
27 return "normal"
28 case ModeTab:
29 return "tab"
30 case ModePane:
31 return "pane"
32 default:
33 return "unknown"
34 }
35}
36
37// ModalState manages the current input mode with optional timeout.
38type ModalState struct {
39 mode Mode
40 timeout time.Duration
41 timer *time.Timer
42
43 // Callback for mode changes (called synchronously under lock).
44 onModeChange func(from, to Mode)
45
46 mu sync.RWMutex
47}
48
49// NewModalState creates a new modal state manager.
50func NewModalState(ctx context.Context) *ModalState {
51 log := logging.FromContext(ctx)
52 log.Debug().Msg("creating modal state")
53 return &ModalState{
54 mode: ModeNormal,
55 }
56}
57
58// Mode returns the current mode (thread-safe).
59func (m *ModalState) Mode() Mode {
60 m.mu.RLock()
61 defer m.mu.RUnlock()
62 return m.mode
63}
64
65// EnterTabMode switches to tab mode with an optional timeout.
66// If timeout is 0, the mode stays until explicitly exited.
67func (m *ModalState) EnterTabMode(ctx context.Context, timeout time.Duration) {
68 log := logging.FromContext(ctx)
69 m.mu.Lock()
70 defer m.mu.Unlock()
71
72 if m.mode == ModeTab {
73 // Already in tab mode, just reset timeout
74 m.resetTimeoutLocked(ctx, timeout)
75 return
76 }
77
78 m.cancelTimerLocked()
79 oldMode := m.mode
80 m.mode = ModeTab
81 m.timeout = timeout
82
83 if timeout > 0 {
84 m.timer = time.AfterFunc(timeout, func() {
85 m.ExitMode(ctx)
86 })
87 }
88
89 log.Debug().
90 Str("from", oldMode.String()).
91 Str("to", m.mode.String()).
92 Dur("timeout", timeout).
93 Msg("entered tab mode")
94
95 if m.onModeChange != nil {
96 m.onModeChange(oldMode, m.mode)
97 }
98}
99
100// EnterPaneMode switches to pane mode with an optional timeout.
101// If timeout is 0, the mode stays until explicitly exited.
102func (m *ModalState) EnterPaneMode(ctx context.Context, timeout time.Duration) {
103 log := logging.FromContext(ctx)
104 m.mu.Lock()
105 defer m.mu.Unlock()
106
107 if m.mode == ModePane {
108 // Already in pane mode, just reset timeout
109 m.resetTimeoutLocked(ctx, timeout)
110 return
111 }
112
113 m.cancelTimerLocked()
114 oldMode := m.mode
115 m.mode = ModePane
116 m.timeout = timeout
117
118 if timeout > 0 {
119 m.timer = time.AfterFunc(timeout, func() {
120 m.ExitMode(ctx)
121 })
122 }
123
124 log.Debug().
125 Str("from", oldMode.String()).
126 Str("to", m.mode.String()).
127 Dur("timeout", timeout).
128 Msg("entered pane mode")
129
130 if m.onModeChange != nil {
131 m.onModeChange(oldMode, m.mode)
132 }
133}
134
135// ExitMode returns to normal mode.
136func (m *ModalState) ExitMode(ctx context.Context) {
137 log := logging.FromContext(ctx)
138 m.mu.Lock()
139 defer m.mu.Unlock()
140
141 if m.mode == ModeNormal {
142 return
143 }
144
145 m.cancelTimerLocked()
146 oldMode := m.mode
147 m.mode = ModeNormal
148 m.timeout = 0
149
150 log.Debug().
151 Str("from", oldMode.String()).
152 Str("to", "normal").
153 Msg("exited modal mode")
154
155 if m.onModeChange != nil {
156 m.onModeChange(oldMode, ModeNormal)
157 }
158}
159
160// ResetTimeout restarts the mode timeout (e.g., after a valid keystroke).
161// Does nothing if not in a modal mode or if no timeout was set.
162func (m *ModalState) ResetTimeout(ctx context.Context) {
163 m.mu.Lock()
164 defer m.mu.Unlock()
165
166 if m.mode == ModeNormal || m.timeout == 0 {
167 return
168 }
169
170 m.resetTimeoutLocked(ctx, m.timeout)
171}
172
173// SetOnModeChange sets the callback for mode changes.
174// The callback is invoked synchronously under the lock.
175func (m *ModalState) SetOnModeChange(fn func(from, to Mode)) {
176 m.mu.Lock()
177 defer m.mu.Unlock()
178 m.onModeChange = fn
179}
180
181// cancelTimerLocked stops any active timeout timer.
182// Must be called with m.mu held.
183func (m *ModalState) cancelTimerLocked() {
184 if m.timer != nil {
185 m.timer.Stop()
186 m.timer = nil
187 }
188}
189
190// resetTimeoutLocked resets the timeout timer.
191// Must be called with m.mu held.
192func (m *ModalState) resetTimeoutLocked(ctx context.Context, timeout time.Duration) {
193 m.cancelTimerLocked()
194 m.timeout = timeout
195 if timeout > 0 {
196 m.timer = time.AfterFunc(timeout, func() {
197 m.ExitMode(ctx)
198 })
199 }
200}