yaib 0.2.0

yaib is Yet Another i3 Bar, with a focus on displaying lots of data
Documentation
use crate::{collectors::Collection, config::Config};
use anyhow::Result;
use serde_derive::{Deserialize, Serialize};
use std::collections::BTreeMap;
use tokio::sync::mpsc::{unbounded_channel, UnboundedReceiver};

pub const NAME_PAGE_UP: &str = "yaib-page-up";
pub const NAME_PAGE_DOWN: &str = "yaib-page-down";

#[derive(Debug, Clone, Default)]
pub struct Bar {
    state: BTreeMap<String, Block>,
    internal_state: crate::state::ProtectedState,
}

impl Bar {
    pub fn new(internal_state: crate::state::ProtectedState) -> Self {
        Self {
            state: BTreeMap::default(),
            internal_state,
        }
    }

    async fn add_page_blocks(&self, v: &mut Vec<Block>, pages: usize) {
        let page = self.internal_state.lock().await.page;

        if page != pages {
            v.push(Block {
                name: Some(NAME_PAGE_UP.to_string()),
                full_text: "".to_string(),
                ..Default::default()
            });
        }

        if page != 0 {
            v.push(Block {
                name: Some(NAME_PAGE_DOWN.to_string()),
                full_text: "".to_string(),
                ..Default::default()
            });
        }
    }

    pub async fn write_blocks(
        &self,
        mut w: impl std::io::Write + Send + 'static,
        mut data: UnboundedReceiver<Vec<Block>>,
    ) -> Result<()> {
        while let Some(v) = data.recv().await {
            serde_json::to_writer(&mut w, &v)?;
            w.write_all(",\n".as_bytes())?;
            w.flush()?;
        }

        Ok(())
    }

    pub async fn emit_status(
        &mut self,
        config: Config,
        mut w: impl std::io::Write + Send + 'static,
        mut data: UnboundedReceiver<Collection>,
    ) -> Result<()> {
        serde_json::to_writer(
            &mut w,
            &Header {
                version: 1,
                click_events: Some(true),
                ..Default::default()
            },
        )?;
        w.write_all("\n[\n".as_bytes())?;
        w.flush()?;

        let (s, r) = unbounded_channel();
        let obj = self.clone();
        tokio::spawn(async move { obj.write_blocks(w, r).await.unwrap() });

        let mut last_send = chrono::Local::now() - config.update_interval();
        let mut last_sent = Vec::new();

        while let Some(collection) = data.recv().await {
            let block = collection.to_block(self.internal_state.clone()).await?;
            self.state.insert(collection.name(), block);

            let now = chrono::Local::now();
            if last_send + config.update_interval() < now {
                let mut v = Vec::new();
                let items = &config.pages()[self.internal_state.lock().await.page].items();
                for item in items {
                    if let Some(block) = self.state.get(&item.name) {
                        v.push(block.clone())
                    }
                }

                self.add_page_blocks(&mut v, config.pages().len() - 1).await;

                if !last_sent.eq(&v) {
                    s.send(v.clone())?;
                    last_send = now;
                    last_sent = v;
                }
            }
        }

        Ok(())
    }
}

#[derive(Debug, Clone, Default, Serialize, Deserialize)]
pub struct Header {
    version: u8,
    #[serde(skip_serializing_if = "Option::is_none")]
    stop_signal: Option<u8>,
    #[serde(skip_serializing_if = "Option::is_none")]
    cont_signal: Option<u8>,
    #[serde(skip_serializing_if = "Option::is_none")]
    click_events: Option<bool>,
}

#[derive(Debug, Clone, Default, Serialize, Deserialize, PartialEq, Eq)]
pub struct Block {
    pub full_text: String,
    #[serde(skip_serializing_if = "Option::is_none")]
    pub short_text: Option<String>,
    #[serde(skip_serializing_if = "Option::is_none")]
    pub color: Option<String>,
    #[serde(skip_serializing_if = "Option::is_none")]
    pub min_width: Option<String>,
    #[serde(skip_serializing_if = "Option::is_none")]
    pub align: Option<String>,
    #[serde(skip_serializing_if = "Option::is_none")]
    pub name: Option<String>,
    #[serde(skip_serializing_if = "Option::is_none")]
    pub instance: Option<String>,
    #[serde(skip_serializing_if = "Option::is_none")]
    pub urgent: Option<bool>,
    #[serde(skip_serializing_if = "Option::is_none")]
    pub separator: Option<bool>,
    #[serde(skip_serializing_if = "Option::is_none")]
    pub separator_block_width: Option<u16>,
    #[serde(skip_serializing_if = "Option::is_none")]
    pub markup: Option<String>,
    #[serde(skip_serializing_if = "Option::is_none")]
    pub background: Option<String>,
    #[serde(skip_serializing_if = "Option::is_none")]
    pub border: Option<String>,
    #[serde(skip_serializing_if = "Option::is_none")]
    pub border_top: Option<String>,
    #[serde(skip_serializing_if = "Option::is_none")]
    pub border_bottom: Option<String>,
    #[serde(skip_serializing_if = "Option::is_none")]
    pub border_left: Option<String>,
    #[serde(skip_serializing_if = "Option::is_none")]
    pub border_right: Option<String>,
}

#[derive(Debug, Clone, Default, Serialize, Deserialize)]
pub struct Click {
    pub name: String,
    pub instance: Option<String>,
    pub x: u16,
    pub y: u16,
    pub button: u16,
    pub relative_x: u16,
    pub relative_y: u16,
    pub output_x: u16,
    pub output_y: u16,
    pub width: u16,
    pub height: u16,
    pub modifiers: Vec<String>,
}