tentacli 15.2.0

Framework for building extensible network protocol clients via modular plugins.
use async_event_emitter::AsyncEventEmitter;
use crossterm::event::{KeyCode, KeyEvent};
use ratatui::Frame;
use ratatui::layout::{Constraint, Rect};
use ratatui::prelude::Color::LightYellow;
use ratatui::style::{Color, Modifier, Style, Stylize};
use ratatui::symbols::border;
use ratatui::text::{Line, Span};
use ratatui::widgets::{Block, Clear, ListItem};
use std::sync::Arc;

pub mod events;

use crate::client::prelude::*;
use crate::events_runtime;
use crate::plugins::tui::components::app::events::ChoicesEvent;
use crate::plugins::tui::components::list::{ItemType, StyledList};
use crate::plugins::tui::events::traits::{
    EventHandler, EventSystem, EventsRuntime, WithEventSystem,
};
use crate::plugins::tui::layout::center;
use crate::plugins::tui::theme::{KEY_BTN_BG, KEY_BTN_FG};
use crate::plugins::tui::traits::{Focusable, UIComponent};

#[derive(Default)]
pub struct Modal {
    list: StyledList,
    title: String,
    opened: bool,
    event_system: EventSystem,
}

impl Modal {
    pub fn set_items(&mut self, items: ChoiceItems) {
        self.list.set_items(
            ItemType::Message(MsgType::default()),
            items
                .iter()
                .map(|item| {
                    ListItem::new(Line::from(vec![Span::styled(
                        item.to_string(),
                        Style::new().fg(Color::Magenta).add_modifier(Modifier::BOLD),
                    )]))
                })
                .collect(),
        );
    }

    pub fn open(&mut self) {
        self.opened = true;
    }

    pub fn close(&mut self) {
        self.opened = false;
    }

    pub fn opened(&self) -> bool {
        self.opened
    }
}

impl UIComponent for Modal {
    fn render(&mut self, frame: &mut Frame, rect: Rect) {
        if self.opened() {
            let instructions = {
                let mut items = vec![
                    Span::styled(
                        "To navigate use ",
                        Style::new().add_modifier(Modifier::BOLD),
                    ),
                    Span::styled(
                        "<ArrowDn>",
                        Style::new()
                            .fg(KEY_BTN_FG)
                            .bg(KEY_BTN_BG)
                            .add_modifier(Modifier::BOLD),
                    ),
                    Span::styled(" and ", Style::new().add_modifier(Modifier::BOLD)),
                    Span::styled(
                        "<ArrowUp>",
                        Style::new()
                            .fg(KEY_BTN_FG)
                            .bg(KEY_BTN_BG)
                            .add_modifier(Modifier::BOLD),
                    ),
                ];

                if self.list.selected().is_some() {
                    items.extend_from_slice(&[
                        Span::styled(
                            ", to confirm press ",
                            Style::new().add_modifier(Modifier::BOLD),
                        ),
                        Span::styled(
                            "<Enter>",
                            Style::new()
                                .fg(KEY_BTN_FG)
                                .bg(KEY_BTN_BG)
                                .add_modifier(Modifier::BOLD),
                        ),
                    ]);
                }

                if self.list.markable() {
                    items.extend_from_slice(&[
                        Span::styled(", to mark use ", Style::new().add_modifier(Modifier::BOLD)),
                        Span::styled(
                            "<Space>",
                            Style::new().fg(Color::Yellow).add_modifier(Modifier::BOLD),
                        ),
                    ]);
                }

                Line::from(items)
            };

            let title = Line::from(
                Span::raw(&self.title)
                    .fg(Color::LightRed)
                    .bg(LightYellow)
                    .add_modifier(Modifier::BOLD),
            );

            let block = Block::bordered()
                .title(title.centered())
                .title_bottom(instructions.centered())
                .border_set(border::ROUNDED);

            frame.render_widget(Clear, rect);
            frame.render_widget(block, rect);

            self.list.render(
                frame,
                center(rect, Constraint::Percentage(50), Constraint::Percentage(50)),
            );
        }
    }
}

impl Focusable for Modal {
    fn holds_focus(&mut self) -> bool {
        self.opened()
    }
}

impl WithEventSystem for Modal {
    fn event_system(&mut self) -> &mut EventSystem {
        &mut self.event_system
    }
}

impl EventHandler<KeyEvent> for Modal {
    async fn callback(&mut self, event: KeyEvent) -> anyhow::Result<Vec<Echo>> {
        let mut output = vec![];

        if event.code == KeyCode::Enter
            && let Some(selected) = self.list.selected()
        {
            output.push(Echo::Choose(selected));
            self.close();
            self.emit_up(events::Close).await?;
        }

        self.list.emit_down(event).await?;

        Ok(output)
    }
}

impl EventHandler<ChoicesEvent> for Modal {
    async fn callback(&mut self, event: ChoicesEvent) -> anyhow::Result<Vec<Echo>> {
        self.open();

        let ChoicesEvent(items) = event;
        self.set_items(items);

        Ok(vec![])
    }
}

events_runtime! {
    impl EventsRuntime for Modal {
        local_events = [KeyEvent];
        parent_events = [KeyEvent, ChoicesEvent];
        children = [list];
    }
}