wiki-tui 0.9.2

A simple and easy to use Wikipedia Text User Interface
Documentation
use std::sync::Arc;

use crossterm::event::KeyCode;
use ratatui::{
    layout::{Constraint, Direction, Layout, Rect},
    style::{Modifier, Style, Stylize},
    widgets::{Clear, List, ListItem},
};
use tui_input::{backend::crossterm::EventHandler, Input};
use wiki_api::languages::{Language, LANGUAGES};

use crate::{
    action::{Action, ActionPacket, ActionResult, SearchAction},
    config::{Config, Theme},
    terminal::Frame,
    ui::{centered_rect, StatefulList},
};

use super::Component;

const FOCUS_INPUT: u8 = 0;
const FOCUS_LIST: u8 = 1;

pub struct SearchLanguageSelectionComponent {
    input: Input,
    focus: u8,
    list: StatefulList<Language>,

    config: Arc<Config>,
    theme: Arc<Theme>,
}

impl SearchLanguageSelectionComponent {
    pub fn new(config: Arc<Config>, theme: Arc<Theme>) -> Self {
        Self {
            input: Input::default(),
            list: StatefulList::with_items(Vec::new()),
            focus: 0,

            config,
            theme,
        }
    }

    fn update_list(&mut self) {
        let input_value = self.input.value();
        let sorted_languages = LANGUAGES
            .iter()
            .filter(|lang| {
                let lang = lang.name().to_lowercase();
                let query = input_value.to_lowercase();
                lang.contains(&query)
            })
            .map(|x| x.to_owned())
            .collect::<Vec<Language>>();
        self.list = StatefulList::with_items(sorted_languages);
    }
}

impl Component for SearchLanguageSelectionComponent {
    fn handle_key_events(&mut self, key: crossterm::event::KeyEvent) -> ActionResult {
        if self.config.bindings.global.submit.matches_event(key) {
            if let Some(lang) = self.list.selected() {
                let mut packet =
                    ActionPacket::single(Action::SwitchContextSearch).action(Action::PopPopup);

                if self.config.ui.popup_search_language_changed {
                    packet = packet.action(Action::PopupMessage(
                        "Information".to_string(),
                        format!("Changed the language for searches to '{}'", lang.name()),
                    ));
                }

                return packet
                    .action(Action::Search(SearchAction::ChangeLanguage(
                        lang.to_owned(),
                    )))
                    .into();
            }
            return ActionResult::Ignored;
        }

        if self.config.bindings.global.pop_popup.matches_event(key) {
            return Action::PopPopup.into();
        }

        match key.code {
            KeyCode::Tab | KeyCode::BackTab => {
                if self.focus == FOCUS_INPUT {
                    self.focus = FOCUS_LIST;
                } else if self.focus == FOCUS_LIST {
                    self.focus = FOCUS_INPUT;
                }

                tracing::debug!("focus now: '{}'", self.focus);

                ActionResult::consumed()
            }
            KeyCode::Char('i') if self.focus == FOCUS_LIST => {
                self.focus = FOCUS_INPUT;
                ActionResult::consumed()
            }

            KeyCode::F(2) => Action::PopPopup.into(),

            _ if self.focus == FOCUS_INPUT => {
                self.input.handle_event(&crossterm::event::Event::Key(key));
                self.update_list();
                ActionResult::consumed()
            }
            _ => ActionResult::Ignored,
        }
    }

    fn update(&mut self, action: Action) -> ActionResult {
        match action {
            Action::ScrollUp(n) => {
                for _ in 0..n {
                    self.list.previous()
                }
                ActionResult::consumed()
            }
            Action::ScrollDown(n) => {
                for _ in 0..n {
                    self.list.next()
                }
                ActionResult::consumed()
            }
            Action::UnselectScroll => {
                self.list.unselect();
                ActionResult::consumed()
            }
            _ => ActionResult::Ignored,
        }
    }

    fn render(&mut self, f: &mut Frame<'_>, area: Rect) {
        let popup_block = self
            .theme
            .default_block()
            .title("Switch Search Language")
            .style(Style::default().bg(self.theme.bg));
        let area = centered_rect(area, 25, 60);
        f.render_widget(Clear, area);
        f.render_widget(popup_block, area);

        let (input_area, list_area) = {
            let chunks = Layout::default()
                .direction(Direction::Vertical)
                .margin(1)
                .constraints([Constraint::Length(1), Constraint::Percentage(100)])
                .split(area);
            (chunks[0], chunks[1])
        };

        let scroll = self.input.visual_scroll(input_area.width as usize);
        let cursor = self.input.visual_cursor();
        let value = self.input.value();

        let input_widget = self
            .theme
            .default_paragraph(format!(
                "{}{}",
                value,
                "_".repeat((input_area.width as usize).saturating_sub(value.len()))
            ))
            .scroll((0, scroll as u16));
        f.render_widget(input_widget, input_area);

        if self.focus == FOCUS_INPUT {
            f.set_cursor_position((
                input_area.x + (cursor.max(scroll) - scroll) as u16,
                input_area.y,
            ));
        }

        let list_items = self
            .list
            .get_items()
            .iter()
            .map(|x| ListItem::new(x.name().to_owned()).fg(self.theme.fg));
        let list_widget = List::new(list_items).highlight_style(if self.focus == FOCUS_LIST {
            Style::default()
                .fg(self.theme.selected_fg)
                .bg(self.theme.selected_bg)
                .add_modifier(Modifier::ITALIC)
        } else {
            Style::default()
        });
        f.render_stateful_widget(list_widget, list_area, self.list.get_state_mut());
    }
}