Skip to main content

a3s_tui/
input.rs

1//! Input routing for global, focused, and captured command scopes.
2//!
3//! [`InputRouter`] builds on [`Keymap`](crate::keymap::Keymap) and
4//! [`FocusManager`](crate::focus::FocusManager). It lets application shells keep
5//! global shortcuts, focused component shortcuts, and modal/overlay shortcuts in
6//! one predictable resolution order.
7
8use crate::event::{Event, KeyEvent};
9use crate::focus::FocusId;
10use crate::keymap::{KeyBinding, Keymap};
11use std::collections::HashMap;
12
13/// A namespace for input bindings outside the global keymap.
14#[derive(Debug, Clone, PartialEq, Eq, Hash)]
15pub enum InputScope {
16    /// Bindings owned by a focusable component.
17    Focus(FocusId),
18    /// Bindings owned by an overlay, modal, mode, or named tool surface.
19    Named(String),
20}
21
22impl InputScope {
23    /// Create a scope for a focusable component.
24    pub fn focused(id: FocusId) -> Self {
25        Self::Focus(id)
26    }
27
28    /// Create a named scope for an overlay, modal, mode, or tool surface.
29    pub fn named(name: impl Into<String>) -> Self {
30        Self::Named(name.into())
31    }
32}
33
34impl From<FocusId> for InputScope {
35    fn from(id: FocusId) -> Self {
36        Self::focused(id)
37    }
38}
39
40impl From<&str> for InputScope {
41    fn from(name: &str) -> Self {
42        Self::named(name)
43    }
44}
45
46impl From<String> for InputScope {
47    fn from(name: String) -> Self {
48        Self::named(name)
49    }
50}
51
52/// How a captured scope behaves when it does not handle a key.
53#[derive(Debug, Clone, Copy, PartialEq, Eq)]
54pub enum InputCaptureMode {
55    /// Stop routing when the captured scope does not handle the key.
56    Exclusive,
57    /// Continue routing to older captures, focused bindings, and globals.
58    Passthrough,
59}
60
61/// A scope currently capturing input.
62#[derive(Debug, Clone, PartialEq, Eq)]
63pub struct InputCapture {
64    scope: InputScope,
65    mode: InputCaptureMode,
66}
67
68impl InputCapture {
69    /// Scope that owns this capture.
70    pub fn scope(&self) -> &InputScope {
71        &self.scope
72    }
73
74    /// Capture behavior for unhandled keys.
75    pub fn mode(&self) -> InputCaptureMode {
76        self.mode
77    }
78}
79
80/// Where a routed action was resolved.
81#[derive(Debug, Clone, PartialEq, Eq)]
82pub enum InputRoute {
83    /// A captured scope handled the key.
84    Captured(InputScope),
85    /// The currently focused component handled the key.
86    Focus(FocusId),
87    /// The global keymap handled the key.
88    Global,
89}
90
91/// A resolved input action and its source route.
92#[derive(Debug, Clone, PartialEq, Eq)]
93pub struct RoutedInput<A> {
94    /// Action associated with the key binding.
95    pub action: A,
96    /// Route that produced the action.
97    pub route: InputRoute,
98}
99
100/// A help entry from a route that is currently eligible for input.
101#[derive(Debug, Clone, PartialEq, Eq)]
102pub struct InputHelpEntry {
103    /// Route that owns this binding.
104    pub route: InputRoute,
105    /// Human-readable key name, such as `Ctrl+c`.
106    pub key: String,
107    /// Binding description.
108    pub description: String,
109}
110
111/// Routes key events through captured, focused, and global keymaps.
112///
113/// Resolution order is:
114///
115/// 1. Captured scopes, newest first.
116/// 2. Bindings for the current focused component.
117/// 3. Global bindings.
118///
119/// Exclusive captures stop routing when they do not handle a key, which is
120/// useful for blocking modals. Passthrough captures let unhandled keys continue
121/// to older layers, which is useful for temporary modes or non-blocking
122/// overlays.
123pub struct InputRouter<A: Clone> {
124    global: Keymap<A>,
125    focused: HashMap<FocusId, Keymap<A>>,
126    named: HashMap<String, Keymap<A>>,
127    captures: Vec<InputCapture>,
128}
129
130impl<A: Clone> InputRouter<A> {
131    /// Create an empty input router.
132    pub fn new() -> Self {
133        Self {
134            global: Keymap::new(),
135            focused: HashMap::new(),
136            named: HashMap::new(),
137            captures: Vec::new(),
138        }
139    }
140
141    /// Add a global binding and return the router.
142    pub fn bind_global(mut self, key: KeyBinding, action: A, description: &str) -> Self {
143        self.register_global(key, action, description);
144        self
145    }
146
147    /// Add a focused component binding and return the router.
148    pub fn bind_focus(
149        mut self,
150        id: FocusId,
151        key: KeyBinding,
152        action: A,
153        description: &str,
154    ) -> Self {
155        self.register_focus(id, key, action, description);
156        self
157    }
158
159    /// Add a named or focus-scope binding and return the router.
160    pub fn bind_scope<S>(mut self, scope: S, key: KeyBinding, action: A, description: &str) -> Self
161    where
162        S: Into<InputScope>,
163    {
164        self.register_scope(scope, key, action, description);
165        self
166    }
167
168    /// Register a global binding.
169    pub fn register_global(&mut self, key: KeyBinding, action: A, description: &str) {
170        self.global.register(key, action, description);
171    }
172
173    /// Register a binding for a focusable component.
174    pub fn register_focus(&mut self, id: FocusId, key: KeyBinding, action: A, description: &str) {
175        self.focused
176            .entry(id)
177            .or_default()
178            .register(key, action, description);
179    }
180
181    /// Register a binding for a named or focus scope.
182    pub fn register_scope<S>(&mut self, scope: S, key: KeyBinding, action: A, description: &str)
183    where
184        S: Into<InputScope>,
185    {
186        match scope.into() {
187            InputScope::Focus(id) => self.register_focus(id, key, action, description),
188            InputScope::Named(name) => {
189                self.named
190                    .entry(name)
191                    .or_default()
192                    .register(key, action, description);
193            }
194        }
195    }
196
197    /// Push an exclusive capture for a scope.
198    pub fn push_capture<S>(&mut self, scope: S)
199    where
200        S: Into<InputScope>,
201    {
202        self.push_capture_with_mode(scope, InputCaptureMode::Exclusive);
203    }
204
205    /// Push a capture with explicit unhandled-key behavior.
206    pub fn push_capture_with_mode<S>(&mut self, scope: S, mode: InputCaptureMode)
207    where
208        S: Into<InputScope>,
209    {
210        self.captures.push(InputCapture {
211            scope: scope.into(),
212            mode,
213        });
214    }
215
216    /// Remove the newest capture and return it.
217    pub fn pop_capture(&mut self) -> Option<InputCapture> {
218        self.captures.pop()
219    }
220
221    /// Remove the newest capture for a scope.
222    pub fn remove_capture<S>(&mut self, scope: S) -> bool
223    where
224        S: Into<InputScope>,
225    {
226        let scope = scope.into();
227        if let Some(pos) = self
228            .captures
229            .iter()
230            .rposition(|capture| capture.scope == scope)
231        {
232            self.captures.remove(pos);
233            true
234        } else {
235            false
236        }
237    }
238
239    /// Remove all active captures.
240    pub fn clear_captures(&mut self) {
241        self.captures.clear();
242    }
243
244    /// Return the currently active capture, if any.
245    pub fn active_capture(&self) -> Option<&InputCapture> {
246        self.captures.last()
247    }
248
249    /// Resolve a key event against the current captures, focus, and globals.
250    pub fn resolve_key(&self, key: &KeyEvent, focused: Option<FocusId>) -> Option<RoutedInput<A>> {
251        for capture in self.captures.iter().rev() {
252            if let Some(keymap) = self.keymap_for_scope(&capture.scope) {
253                if let Some(action) = keymap.resolve(key) {
254                    return Some(RoutedInput {
255                        action,
256                        route: InputRoute::Captured(capture.scope.clone()),
257                    });
258                }
259            }
260
261            if capture.mode == InputCaptureMode::Exclusive {
262                return None;
263            }
264        }
265
266        if let Some(id) = focused {
267            if let Some(keymap) = self.focused.get(&id) {
268                if let Some(action) = keymap.resolve(key) {
269                    return Some(RoutedInput {
270                        action,
271                        route: InputRoute::Focus(id),
272                    });
273                }
274            }
275        }
276
277        self.global.resolve(key).map(|action| RoutedInput {
278            action,
279            route: InputRoute::Global,
280        })
281    }
282
283    /// Resolve a terminal event. Non-key events are ignored.
284    pub fn resolve_event(&self, event: &Event, focused: Option<FocusId>) -> Option<RoutedInput<A>> {
285        match event {
286            Event::Key(key) => self.resolve_key(key, focused),
287            _ => None,
288        }
289    }
290
291    /// Help entries for global bindings.
292    pub fn global_help(&self) -> Vec<(String, String)> {
293        self.global.help()
294    }
295
296    /// Help entries for a focusable component.
297    pub fn focus_help(&self, id: FocusId) -> Vec<(String, String)> {
298        self.focused.get(&id).map(Keymap::help).unwrap_or_default()
299    }
300
301    /// Help entries for a named or focus scope.
302    pub fn scope_help<S>(&self, scope: S) -> Vec<(String, String)>
303    where
304        S: Into<InputScope>,
305    {
306        self.keymap_for_scope(&scope.into())
307            .map(Keymap::help)
308            .unwrap_or_default()
309    }
310
311    /// Help entries in the same order that key routing would consider them.
312    pub fn active_help(&self, focused: Option<FocusId>) -> Vec<InputHelpEntry> {
313        let mut entries = Vec::new();
314
315        for capture in self.captures.iter().rev() {
316            entries.extend(self.help_entries_for_scope(
317                &capture.scope,
318                InputRoute::Captured(capture.scope.clone()),
319            ));
320            if capture.mode == InputCaptureMode::Exclusive {
321                return entries;
322            }
323        }
324
325        if let Some(id) = focused {
326            entries
327                .extend(self.help_entries_for_keymap(InputRoute::Focus(id), self.focused.get(&id)));
328        }
329
330        entries.extend(self.help_entries_for_keymap(InputRoute::Global, Some(&self.global)));
331        entries
332    }
333
334    fn keymap_for_scope(&self, scope: &InputScope) -> Option<&Keymap<A>> {
335        match scope {
336            InputScope::Focus(id) => self.focused.get(id),
337            InputScope::Named(name) => self.named.get(name),
338        }
339    }
340
341    fn help_entries_for_scope(&self, scope: &InputScope, route: InputRoute) -> Vec<InputHelpEntry> {
342        self.help_entries_for_keymap(route, self.keymap_for_scope(scope))
343    }
344
345    fn help_entries_for_keymap(
346        &self,
347        route: InputRoute,
348        keymap: Option<&Keymap<A>>,
349    ) -> Vec<InputHelpEntry> {
350        keymap
351            .map(|keymap| {
352                keymap
353                    .help()
354                    .into_iter()
355                    .map(|(key, description)| InputHelpEntry {
356                        route: route.clone(),
357                        key,
358                        description,
359                    })
360                    .collect()
361            })
362            .unwrap_or_default()
363    }
364}
365
366impl<A: Clone> Default for InputRouter<A> {
367    fn default() -> Self {
368        Self::new()
369    }
370}