tui-pages 0.7.2

Core for TUI apps with multiple pages
Documentation
use crate::input::{ChordSequenceTracker, InputRegistry, KeyChord, PipelineResponse};
use crossterm::event::{KeyCode, KeyEvent, KeyModifiers};
use tracing::debug;

#[derive(Debug, Clone)]
pub struct InputPipeline<A> {
    pub registry: InputRegistry<A>,
    pub tracker: ChordSequenceTracker,
}

impl<A> InputPipeline<A> {
    pub fn new(registry: InputRegistry<A>, timeout_ms: u64) -> Self {
        Self {
            registry,
            tracker: ChordSequenceTracker::new(timeout_ms),
        }
    }

    pub fn active(&self) -> bool {
        !self.tracker.is_empty()
    }
}

impl<A: Clone> InputPipeline<A> {
    pub fn process(
        &mut self,
        event: KeyEvent,
        modes: &[impl AsRef<str>],
        accepts_text_input: bool,
    ) -> PipelineResponse<A> {
        let chord = KeyChord::from_event(&event);
        let mode_refs: Vec<&str> = modes.iter().map(|mode| mode.as_ref()).collect();
        let is_plain_char = matches!(chord.code, KeyCode::Char(_))
            && (chord.modifiers == KeyModifiers::empty() || chord.modifiers == KeyModifiers::SHIFT);
        let effective_modes: Vec<&str> = if accepts_text_input && is_plain_char {
            mode_refs
                .iter()
                .filter(|mode| **mode != "general")
                .copied()
                .collect()
        } else {
            mode_refs.clone()
        };

        let response = if !self.tracker.is_empty() {
            self.tracker.maybe_expire();
            if self.tracker.is_empty() {
                PipelineResponse::Cancel
            } else {
                self.tracker.add(chord);
                let current_sequence = self.tracker.get();

                if let Some(action) = self.registry.match_action(current_sequence, &mode_refs) {
                    self.tracker.reset();
                    PipelineResponse::Execute(action)
                } else {
                    let hints = self.registry.get_hints(current_sequence, &mode_refs);
                    if hints.is_empty() {
                        self.tracker.reset();
                        PipelineResponse::Cancel
                    } else {
                        PipelineResponse::Wait(hints)
                    }
                }
            }
        } else {
            let single = [chord];
            if let Some(action) = self.registry.match_action(&single, &effective_modes) {
                PipelineResponse::Execute(action)
            } else if self.registry.starts_sequence(&chord, &effective_modes)
                && (!accepts_text_input || !is_plain_char)
            {
                self.tracker.add(chord);
                PipelineResponse::Wait(self.registry.get_hints(&single, &effective_modes))
            } else {
                PipelineResponse::Type(chord)
            }
        };

        debug!(?event, ?mode_refs, "processed input event");
        response
    }
}