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::Rect;
use ratatui::style::{Modifier, Style};
use ratatui::widgets::{List, ListItem, ListState};
use std::collections::{HashMap, HashSet};
use std::sync::Arc;

pub mod events;

use crate::client::prelude::*;
use crate::events_runtime;
use crate::plugins::tui::events::traits::{
    EventHandler, EventSystem, EventsRuntime, WithEventSystem,
};
use crate::plugins::tui::theme::{LIST_HIGHLIGHT_BG, LIST_HIGHLIGHT_FG};
use crate::plugins::tui::traits::{Paginator, UIComponent};

#[derive(Copy, Clone, PartialEq, Eq, Hash)]
pub enum ItemType {
    Message(MsgType),
    Packet(PacketType),
}

#[derive(Default)]
pub struct StyledList {
    pub counter: HashMap<ItemType, usize>,
    pub total_items: usize,
    pub marked: HashSet<usize>,
    items: Vec<(ItemType, ListItem<'static>)>,
    state: ListState,
    markable: bool,
    scroll_mode: ScrollMode,
    event_system: EventSystem,
}

impl StyledList {
    #[allow(dead_code)]
    pub fn set_markable(mut self) -> Self {
        self.markable = true;
        self
    }

    #[allow(dead_code)]
    pub fn markable(&self) -> bool {
        self.markable
    }

    pub fn selected(&self) -> Option<Vec<usize>> {
        let mut selected = vec![];

        if self.markable {
            selected.extend(self.marked.iter().copied());
        } else if let Some(index) = self.state.selected() {
            selected.push(index);
        }

        if selected.is_empty() {
            None
        } else {
            Some(selected)
        }
    }

    pub async fn add_item(
        &mut self,
        item_type: ItemType,
        item: ListItem<'static>,
    ) -> anyhow::Result<()> {
        self.items.push((item_type, item));
        *self.counter.entry(item_type).or_insert(0) += 1;
        self.total_items += 1;

        if self.scroll_mode == ScrollMode::Auto {
            self.state.select_last();
            self.emit_up(events::SelectionChanged).await?;
        }

        Ok(())
    }

    pub fn set_items(&mut self, item_type: ItemType, items: Vec<ListItem<'static>>) {
        self.items = items.into_iter().map(|item| (item_type, item)).collect();
        self.total_items = self.items.len();
    }

    pub fn mark_item(&mut self) {
        if let Some(index) = self.state.selected() {
            self.toggle(index);
        }
    }

    fn toggle(&mut self, index: usize) {
        if self.marked.take(&index).is_none() {
            self.marked.insert(index);
        }
    }

    #[allow(dead_code)]
    pub fn contains(&mut self, index: usize) -> bool {
        self.marked.contains(&index)
    }

    pub fn autoscroll(&mut self, enabled: bool) {
        self.scroll_mode = if enabled {
            ScrollMode::Auto
        } else {
            ScrollMode::Manual
        };
    }
}

impl UIComponent for StyledList {
    fn render(&mut self, frame: &mut Frame, rect: Rect) {
        let items: Vec<ListItem> = self.items.iter().map(|(_, item)| item.clone()).collect();

        let list = List::new(items).highlight_style(
            Style::new()
                .fg(LIST_HIGHLIGHT_FG)
                .bg(LIST_HIGHLIGHT_BG)
                .add_modifier(Modifier::BOLD),
        );

        frame.render_stateful_widget(list, rect, &mut self.state);
    }
}

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

impl EventHandler<KeyEvent> for StyledList {
    async fn callback(&mut self, event: KeyEvent) -> anyhow::Result<Vec<Echo>> {
        match event.code {
            KeyCode::Down => {
                self.autoscroll(false);
                self.next();
                self.emit_up(events::SelectionChanged).await?;
            }
            KeyCode::Up => {
                self.autoscroll(false);
                self.prev();
                self.emit_up(events::SelectionChanged).await?;
            }
            KeyCode::Home => {
                self.autoscroll(false);
                self.first();
                self.emit_up(events::SelectionChanged).await?;
            }
            KeyCode::End => {
                self.autoscroll(true);
                self.last();
                self.emit_up(events::SelectionChanged).await?;
            }
            KeyCode::Esc => {
                self.autoscroll(true);
            }
            KeyCode::Char(' ') if self.markable() => self.mark_item(),
            _ => {}
        }

        Ok(vec![])
    }
}

events_runtime! {
    impl EventsRuntime for StyledList {
        local_events = [KeyEvent];
    }
}

impl Paginator for StyledList {
    fn prev(&mut self) {
        let first_index = 0;

        match self.state.selected() {
            Some(index) if index == first_index => self.state.select_last(),
            Some(index) => self.state.select(Some(index - 1)),
            None => self.state.select_last(),
        }
    }

    fn next(&mut self) {
        match self.state.selected() {
            Some(index) if index == (self.total_items - 1) => self.state.select_first(),
            Some(index) => self.state.select(Some(index + 1)),
            None => self.state.select_first(),
        }
    }

    fn first(&mut self) {
        self.state.select_first();
    }

    fn last(&mut self) {
        self.state.select_last();
    }
}

#[derive(Default, PartialEq)]
enum ScrollMode {
    Auto,
    #[default]
    Manual,
}