cyndikator 0.2.2

A cli rss reader
use super::*;
use crate::db::Entry;
use crossterm::style::{Color, Print, SetForegroundColor};
use crossterm::{cursor, terminal};
use std::borrow::Cow;

pub struct Full<'a> {
    pub selected: u16,
    pub entries: &'a [Entry],
    pub status: Option<String>,
}

impl<'a> Draw for Full<'a> {
    fn draw(&self, out: &mut impl QueueableCommand) -> Result<()> {
        let (width, height) = terminal::size()?;

        let end = (height as usize).min(self.entries.len());
        let rem = (height as usize).saturating_sub(self.entries.len());

        let (end, rem) = match self.status {
            Some(_) if rem > 0 => (end, rem - 1),
            Some(_) => (end - 1, rem),
            None => (end, rem),
        };

        let ents = &self.entries[..end];

        out.queue(cursor::MoveTo(0, 0))?;
        out.queue(terminal::Clear(terminal::ClearType::All))?;

        for (i, entry) in ents.iter().enumerate() {
            Line {
                width,
                selected: i == self.selected as usize,
                entry,
            }
            .draw(out)?;
            out.queue(cursor::MoveToNextLine(1))?;
        }

        out.queue(SetForegroundColor(Color::Blue))?;
        for _ in 0..rem {
            out.queue(Print("~"))?;
            out.queue(cursor::MoveToNextLine(1))?;
        }
        out.queue(SetForegroundColor(Color::Reset))?;

        if let Some(status) = &self.status {
            out.queue(Print(status))?;
        }

        Ok(())
    }
}

struct Line<'a> {
    width: u16,
    selected: bool,
    entry: &'a Entry,
}

impl<'a> Draw for Line<'a> {
    fn draw(&self, out: &mut impl QueueableCommand) -> Result<()> {
        let cat_full = self.entry.categories.join(", ");

        let feed = trunc(
            &self.entry.feed.as_deref().unwrap_or("<untitled feed>"),
            self.width / 4 - 2,
        );
        let title = trunc(
            &self.entry.title.as_deref().unwrap_or("<untitled item>"),
            self.width / 2,
        );
        let cat = trunc(&cat_full, self.width / 4);

        if self.selected {
            out.queue(SetForegroundColor(Color::Yellow))?;
            out.queue(Print("* "))?;
        } else {
            out.queue(Print("  "))?;
        }

        out.queue(SetForegroundColor(Color::Blue))?;
        out.queue(Print(&feed))?;

        out.queue(cursor::MoveToColumn(self.width / 4))?;
        out.queue(SetForegroundColor(Color::Cyan))?;
        out.queue(Print(&title))?;

        out.queue(cursor::MoveToColumn(self.width - cat.len() as u16))?;
        out.queue(SetForegroundColor(Color::Green))?;
        out.queue(Print(&cat))?;

        out.queue(SetForegroundColor(Color::Reset))?;

        Ok(())
    }
}

fn trunc(input: &str, width: u16) -> Cow<str> {
    if input.len() <= width as usize {
        input.into()
    } else {
        let mut buf = String::with_capacity(input.len());

        for (i, ch) in input.chars().enumerate() {
            if i < width as usize {
                buf.push(ch);
            }
        }

        buf.pop();
        buf.pop();
        buf.pop();

        for _ in 0..width.min(3) {
            buf.push('.');
        }

        buf.into()
    }
}

#[test]
fn trunc_test() {
    assert_eq!(trunc("hello", 5), "hello");
    assert_eq!(trunc("hello", 4), "h...");
}

#[test]
fn trunc_test_heart() {
    // bad name but <3 dots
    assert_eq!(trunc("hello", 2), "..");
}

#[test]
fn trunc_panic_test() {
    assert_eq!(trunc("объект", 5), "об...");
}