binocular-cli 0.2.3

Not exactly a telescope, but it's useful sometimes. TUI to search/navigate through files and workspaces.
Documentation
use super::command_parser::{parse_pending_operator_intent, OperatorMotion, PendingOperatorIntent};
use crossterm::event::KeyEvent;

pub enum PendingOperatorResult {
    Unhandled,
    AwaitingMore,
    Applied { changed: bool },
    Cleared,
}

pub trait PendingOperatorTarget {
    fn set_modifier(&mut self, modifier: char) -> bool;
    fn repeat_operator(&mut self, op: char, count: usize) -> bool;
    fn apply_motion(&mut self, op: char, motion: OperatorMotion, count: usize) -> bool;
    fn clear_pending(&mut self);
}

pub fn handle_pending_operator<T: PendingOperatorTarget>(
    target: &mut T,
    key: KeyEvent,
    op: char,
    count: usize,
) -> PendingOperatorResult {
    let Some(intent) = parse_pending_operator_intent(key, op) else {
        return PendingOperatorResult::Unhandled;
    };

    match intent {
        PendingOperatorIntent::Cancel => {
            target.clear_pending();
            PendingOperatorResult::Cleared
        }
        PendingOperatorIntent::SetModifier(modifier) => {
            if target.set_modifier(modifier) {
                PendingOperatorResult::AwaitingMore
            } else {
                target.clear_pending();
                PendingOperatorResult::Cleared
            }
        }
        PendingOperatorIntent::RepeatOperator => {
            let changed = target.repeat_operator(op, count);
            target.clear_pending();
            PendingOperatorResult::Applied { changed }
        }
        PendingOperatorIntent::Motion(motion) => {
            let changed = target.apply_motion(op, motion, count);
            target.clear_pending();
            PendingOperatorResult::Applied { changed }
        }
    }
}

#[cfg(test)]
mod tests {
    use super::{handle_pending_operator, PendingOperatorResult, PendingOperatorTarget};
    use crate::input::vim::command_parser::OperatorMotion;
    use crossterm::event::{KeyCode, KeyEvent, KeyModifiers};

    #[derive(Default)]
    struct FakeTarget {
        modifier: Option<char>,
        cleared: bool,
        repeated: Option<(char, usize)>,
        motion: Option<(char, OperatorMotion, usize)>,
    }

    impl PendingOperatorTarget for FakeTarget {
        fn set_modifier(&mut self, modifier: char) -> bool {
            self.modifier = Some(modifier);
            true
        }

        fn repeat_operator(&mut self, op: char, count: usize) -> bool {
            self.repeated = Some((op, count));
            true
        }

        fn apply_motion(&mut self, op: char, motion: OperatorMotion, count: usize) -> bool {
            self.motion = Some((op, motion, count));
            true
        }

        fn clear_pending(&mut self) {
            self.cleared = true;
        }
    }

    #[test]
    fn shared_handler_tracks_modifier_repeat_and_motion() {
        let mut target = FakeTarget::default();
        assert!(matches!(
            handle_pending_operator(
                &mut target,
                KeyEvent::new(KeyCode::Char('i'), KeyModifiers::NONE),
                'd',
                1
            ),
            PendingOperatorResult::AwaitingMore
        ));
        assert_eq!(target.modifier, Some('i'));

        assert!(matches!(
            handle_pending_operator(
                &mut target,
                KeyEvent::new(KeyCode::Char('d'), KeyModifiers::NONE),
                'd',
                2
            ),
            PendingOperatorResult::Applied { changed: true }
        ));
        assert_eq!(target.repeated, Some(('d', 2)));

        assert!(matches!(
            handle_pending_operator(
                &mut target,
                KeyEvent::new(KeyCode::Char('w'), KeyModifiers::NONE),
                'c',
                3
            ),
            PendingOperatorResult::Applied { changed: true }
        ));
        assert_eq!(target.motion, Some(('c', OperatorMotion::WordForward, 3)));
    }
}