oreq 0.2.6

The tool for interactively creating curl arguments from OpenAPI.
Documentation
use fuzzy_matcher::{skim::SkimMatcherV2, FuzzyMatcher};
use promptuity::{
    event::{KeyCode, KeyModifiers},
    pagination::paginate,
    prompts::{DefaultSelectFormatter, SelectFormatter, SelectOption},
    style::{Color, Styled},
    Error, InputCursor, Prompt, PromptBody, PromptInput, PromptState, RenderPayload,
};

pub struct Enumeration<T>
where
    T: Default + Clone,
{
    formatter: DefaultSelectFormatter,
    message: String,
    page_size: usize,
    options: Vec<SelectOption<T>>,
    filtered_options: Vec<usize>,
    index: usize,
    input: InputCursor,
    matcher: SkimMatcherV2,
}

impl<T> Enumeration<T>
where
    T: Default + Clone,
{
    pub fn new(message: String, options: Vec<SelectOption<T>>) -> Self {
        Self {
            formatter: DefaultSelectFormatter::new(),
            message,
            page_size: 8,
            options,
            filtered_options: Vec::new(),
            index: 0,
            input: InputCursor::default(),
            matcher: SkimMatcherV2::default(),
        }
    }

    fn run_filter(&mut self) {
        let pattern = self.input.value();

        self.filtered_options = self
            .options
            .iter()
            .enumerate()
            .filter_map(|(i, option)| self.matcher.fuzzy_match(&option.label, &pattern).map(|_| i))
            .collect::<Vec<_>>();

        self.index = std::cmp::min(self.filtered_options.len().saturating_sub(1), self.index);
    }

    fn current_option(&self) -> Option<&SelectOption<T>> {
        self.filtered_options
            .get(self.index)
            .and_then(|idx| self.options.get(*idx))
    }
}

impl<T> AsMut<Enumeration<T>> for Enumeration<T>
where
    T: Default + Clone,
{
    fn as_mut(&mut self) -> &mut Self {
        self
    }
}

impl<T> Prompt for Enumeration<T>
where
    T: Default + Clone,
{
    type Output = T;

    fn setup(&mut self) -> Result<(), Error> {
        if self.options.is_empty() {
            return Err(Error::Config("options cannot be empty.".into()));
        }

        self.filtered_options = (0..self.options.len()).collect();

        Ok(())
    }

    fn handle(&mut self, code: KeyCode, modifiers: KeyModifiers) -> promptuity::PromptState {
        match (code, modifiers) {
            (KeyCode::Esc, _) | (KeyCode::Char('c'), KeyModifiers::CONTROL) => PromptState::Cancel,
            (KeyCode::Enter, _) => match self.current_option() {
                Some(_) => PromptState::Submit,
                _ => PromptState::Error("No matches found".into()),
            },
            (KeyCode::Up, _)
            | (KeyCode::Char('k'), KeyModifiers::CONTROL)
            | (KeyCode::Char('p'), KeyModifiers::CONTROL) => {
                self.index = self.index.saturating_sub(1);
                PromptState::Active
            }
            (KeyCode::Down, _)
            | (KeyCode::Char('j'), KeyModifiers::CONTROL)
            | (KeyCode::Char('n'), KeyModifiers::CONTROL) => {
                self.index = std::cmp::min(
                    self.filtered_options.len().saturating_sub(1),
                    self.index.saturating_add(1),
                );
                PromptState::Active
            }
            (KeyCode::Left, _) | (KeyCode::Char('b'), KeyModifiers::CONTROL) => {
                self.input.move_left();
                PromptState::Active
            }
            (KeyCode::Right, _) | (KeyCode::Char('f'), KeyModifiers::CONTROL) => {
                self.input.move_right();
                PromptState::Active
            }
            (KeyCode::Home, _) | (KeyCode::Char('a'), KeyModifiers::CONTROL) => {
                self.input.move_home();
                PromptState::Active
            }
            (KeyCode::End, _) | (KeyCode::Char('e'), KeyModifiers::CONTROL) => {
                self.input.move_end();
                PromptState::Active
            }
            (KeyCode::Backspace, _) | (KeyCode::Char('h'), KeyModifiers::CONTROL) => {
                self.input.delete_left_char();
                self.run_filter();
                PromptState::Active
            }
            (KeyCode::Char('w'), KeyModifiers::CONTROL) => {
                self.input.delete_left_word();
                self.run_filter();
                PromptState::Active
            }
            (KeyCode::Delete, _) | (KeyCode::Char('d'), KeyModifiers::CONTROL) => {
                self.input.delete_right_char();
                self.run_filter();
                PromptState::Active
            }
            (KeyCode::Char('u'), KeyModifiers::CONTROL) => {
                self.input.delete_line();
                self.run_filter();
                PromptState::Active
            }
            (KeyCode::Char(c), _) => {
                self.input.insert(c);
                self.run_filter();
                PromptState::Active
            }
            _ => PromptState::Active,
        }
    }

    fn submit(&mut self) -> Self::Output {
        self.current_option().unwrap().value.clone()
    }

    fn render(&mut self, state: &PromptState) -> Result<RenderPayload, String> {
        let payload = RenderPayload::new(self.message.clone(), None, None);

        match state {
            PromptState::Submit => {
                let option = self
                    .current_option()
                    .map(|x| x.label.clone())
                    .unwrap_or_default();
                Ok(payload.input(PromptInput::Raw(option)))
            }

            _ => {
                let page = paginate(self.page_size, &self.filtered_options, self.index);
                let options = page
                    .items
                    .iter()
                    .enumerate()
                    .map(|(i, idx)| {
                        let option = self.options.get(*idx).unwrap();
                        let active = i == page.cursor;
                        self.formatter.option(
                            self.formatter.option_icon(active),
                            self.formatter.option_label(option.label.clone(), active),
                            self.formatter.option_hint(option.hint.clone(), active),
                            active,
                        )
                    })
                    .collect::<Vec<_>>()
                    .join("\n");

                let raw = if options.is_empty() {
                    Styled::new("<No matches found>")
                        .fg(Color::DarkGrey)
                        .to_string()
                } else {
                    options.to_string()
                };

                Ok(payload
                    .input(PromptInput::Cursor(self.input.clone()))
                    .body(PromptBody::Raw(raw)))
            }
        }
    }
}