tuisky 0.2.2

TUI client for Bluesky
Documentation
use super::types::Action;
use crate::config::{ColumnAction, Key, Keybindings};
use color_eyre::Result;
use ratatui::layout::Rect;
use ratatui::style::{Style, Stylize};
use ratatui::text::{Line, Span};
use ratatui::widgets::{Block, Clear, List, ListItem, ListState};
use ratatui::Frame;
use tokio::sync::mpsc::UnboundedSender;

enum MenuAction {
    NewPost(Vec<String>),
    Refresh(Vec<String>),
    Back(Vec<String>),
}

impl<'a> From<&'a MenuAction> for ListItem<'a> {
    fn from(action: &'a MenuAction) -> Self {
        match action {
            MenuAction::NewPost(v) if !v.is_empty() => Self::from(Line::from(vec![
                Span::from("New Post ").reset(),
                Span::from(format!("({})", v.join(", "))).dim(),
            ])),
            MenuAction::NewPost(_) => Self::from("New Post".reset()),
            MenuAction::Refresh(v) if !v.is_empty() => Self::from(Line::from(vec![
                Span::from("Refresh ").reset(),
                Span::from(format!("({})", v.join(", "))).dim(),
            ])),
            MenuAction::Refresh(_) => Self::from("Refresh".reset()),
            MenuAction::Back(v) if !v.is_empty() => Self::from(Line::from(vec![
                Span::from("Back ").reset(),
                Span::from(format!("({})", v.join(", "))).dim(),
            ])),
            MenuAction::Back(_) => Self::from("Back".reset()),
        }
    }
}

pub struct MenuViewComponent {
    action_tx: UnboundedSender<Action>,
    items: Vec<MenuAction>,
    state: ListState,
}

impl MenuViewComponent {
    pub fn new(action_tx: UnboundedSender<Action>, keybindings: &Keybindings) -> Self {
        let mut keys = vec![Vec::new(); 3];
        for (k, v) in &keybindings.column {
            match v {
                ColumnAction::NewPost => keys[0].push(k),
                ColumnAction::Refresh => keys[1].push(k),
                ColumnAction::Back => keys[2].push(k),
                _ => {}
            }
        }
        keys.iter_mut().for_each(|v| v.sort());
        let to_string = |v: &[&Key]| {
            v.iter()
                .filter_map(|k| serde_json::to_string(k).ok())
                .map(|s| s.trim_matches('"').to_string())
                .collect::<Vec<_>>()
        };
        Self {
            action_tx,
            items: vec![
                MenuAction::NewPost(to_string(&keys[0])),
                MenuAction::Refresh(to_string(&keys[1])),
                MenuAction::Back(to_string(&keys[2])),
            ],
            state: ListState::default().with_selected(Some(0)),
        }
    }
    pub fn update(&mut self, action: Action) -> Result<Option<Action>> {
        match action {
            Action::NextItem => {
                if let Some(selected) = self.state.selected() {
                    self.state
                        .select(Some((selected + 1).min(self.items.len() - 1)));
                    return Ok(Some(Action::Render));
                }
            }
            Action::PrevItem => {
                if let Some(selected) = self.state.selected() {
                    self.state.select(Some(selected.max(1) - 1));
                    return Ok(Some(Action::Render));
                }
            }
            Action::Enter => {
                if let Some(selected) = self.state.selected() {
                    let action = match self.items[selected] {
                        MenuAction::NewPost(_) => Action::NewPost,
                        MenuAction::Refresh(_) => Action::Refresh,
                        MenuAction::Back(_) => Action::Back,
                    };
                    self.action_tx.send(action).ok();
                    return Ok(Some(Action::Menu));
                }
            }
            Action::Update(_) | Action::Render => {
                return Ok(None);
            }
            _ => {}
        }
        Ok(Some(Action::Menu))
    }
    pub fn draw(&mut self, f: &mut Frame<'_>, area: Rect) -> Result<()> {
        let area = Rect::new(area.x, area.y, area.width, self.items.len() as u16 + 2);
        f.render_widget(Clear, area);
        f.render_stateful_widget(
            List::new(&self.items)
                .block(Block::bordered().title("Menu").dim())
                .highlight_style(Style::default().reversed()),
            area,
            &mut self.state,
        );
        Ok(())
    }
}