finery 0.2.0

Keyboard-first Jira backlog and Composer with optional MCP access
Documentation
use std::time::Duration;

use tuicore::{Key, KeyEvent};

const TIMEOUT: Duration = Duration::from_secs(1);

#[derive(Default)]
pub(crate) struct TicketNumberJump {
    prefix: String,
    elapsed: Duration,
}

impl TicketNumberJump {
    pub(crate) fn push(&mut self, key: KeyEvent) -> bool {
        let Key::Char(digit) = key.code else {
            return false;
        };
        if !key.modifiers.is_empty() || !digit.is_ascii_digit() {
            return false;
        }
        self.prefix.push(digit);
        self.elapsed = Duration::ZERO;
        true
    }

    pub(crate) fn query(&self) -> Option<&str> {
        (!self.prefix.is_empty()).then_some(&self.prefix)
    }

    pub(crate) fn clear(&mut self) {
        self.prefix.clear();
        self.elapsed = Duration::ZERO;
    }

    pub(crate) fn accepts(&self, key: KeyEvent) -> bool {
        self.query().is_some() && matches!(key.code, Key::Enter) && key.modifiers.is_empty()
    }

    pub(crate) fn cancels(&self, key: KeyEvent) -> bool {
        self.query().is_some()
            && (matches!(key.code, Key::Esc)
                || matches!(key.code, Key::Char('[')
                    if key.modifiers == tuicore::KeyModifiers::CONTROL))
    }

    pub(crate) fn advance(&mut self, dt: Duration) -> bool {
        if self.prefix.is_empty() {
            return false;
        }
        self.elapsed = self.elapsed.saturating_add(dt);
        if self.elapsed < TIMEOUT {
            return false;
        }
        self.clear();
        true
    }

    pub(crate) fn remaining(&self) -> Option<Duration> {
        (!self.prefix.is_empty()).then(|| TIMEOUT.saturating_sub(self.elapsed))
    }
}

pub(crate) fn ticket_number_matches(key: &str, number: &str) -> bool {
    key.rsplit_once('-')
        .is_some_and(|(_, suffix)| suffix.starts_with(number))
}

pub(crate) fn exact_ticket_number_matches(key: &str, number: &str) -> bool {
    key.rsplit_once('-')
        .is_some_and(|(_, suffix)| suffix == number)
}