paru 0.99.2

Aur helper and pacman wrapper
use crate::config::Config;
use crate::fmt::print_indent;
use crate::{sprint, sprintln};

use std::str::Chars;

use ansi_term::Style;
use anyhow::{Context, Result};
use htmlescape::decode_html;
use rss::Channel;

enum Tag {
    CodeOpen,
    CodeClose,
    PClose,
    Other,
}

pub fn newest_pkg(config: &Config) -> Result<i64> {
    let max = config
        .alpm
        .localdb()
        .pkgs()?
        .map(|p| p.build_date())
        .max()
        .unwrap_or_default();

    Ok(max)
}

pub fn news(config: &Config) -> Result<()> {
    let url = config.arch_url.join("feeds/news")?;

    let resp = reqwest::blocking::get(url.clone()).with_context(|| format!("{}", url))?;
    let bytes = resp.bytes()?;
    let channel = Channel::read_from(bytes.as_ref())?;
    let c = config.color;

    let mut printed = false;

    for item in channel.into_items().into_iter().rev() {
        let date = item.pub_date().unwrap_or_default();

        match chrono::DateTime::parse_from_rfc2822(date) {
            Ok(date) => {
                if config.news < 2 && date.timestamp() < newest_pkg(config)? {
                    continue;
                }

                sprint!("{} ", c.news_date.paint(date.format("%F").to_string()));
            }
            Err(_) => sprint!("No Date "),
        }

        let title = item.title().unwrap_or("No Title");
        sprintln!("{}", c.bold.paint(title));

        printed = true;
        parse_html(config, item.description().unwrap_or_default());
    }

    if !printed {
        sprintln!("no new news");
    }

    Ok(())
}

fn parse_html(config: &Config, html: &str) {
    let code = config.color.code;
    let mut words = String::with_capacity(html.len());

    let mut chars = html.chars();
    while let Some(c) = chars.next() {
        if c == '<' {
            let tag = parse_tag(&mut chars);

            match tag {
                Tag::CodeOpen => {
                    words.push(' ');
                    words.push_str(&code.prefix().to_string());
                }
                Tag::CodeClose => words.push_str(&code.suffix().to_string()),
                Tag::PClose => words.push('\n'),
                Tag::Other => (),
            }
        } else {
            words.push(c);
        }
    }

    words.push_str(&code.suffix().to_string());
    let words = words;
    let words = decode_html(&words).unwrap_or(words);

    for line in words.lines() {
        sprint!("    ");
        let line = line.split_whitespace();
        print_indent(Style::new(), 4, 4, config.cols, " ", line);
    }
}

fn parse_tag(iter: &mut Chars) -> Tag {
    if iter.as_str().starts_with("code>") {
        iter.by_ref().take(5).count();
        Tag::CodeOpen
    } else if iter.as_str().starts_with("/code>") {
        iter.by_ref().take(6).count();
        Tag::CodeClose
    } else if iter.as_str().starts_with("/p>") {
        iter.by_ref().take(3).count();
        Tag::PClose
    } else {
        iter.by_ref().any(|c| c == '>');
        Tag::Other
    }
}