about summary refs log blame commit diff stats
path: root/sys/nixpkgs/pkgs/comments/src/main.rs
blob: 6e4f72e9b8b22aa722334ff46e3db9f4228caddc (plain) (tree)




































































































                                                                           
                                








































                                                                             
                                










































































                                                                                   
                                                                      




                                                                                        
                             










                                                                  
                             









                                                                  
                             







                                                                      
                             

























                                                                                                                    
                                        





                                                                  
                                       
 
                                                                        

                                                       
                                                               
       
                                                         










                                                                   
use std::{
    env,
    fmt::Display,
    fs::{self, File},
    io::{BufReader, Write},
    mem,
    path::PathBuf,
    process::{Command, Stdio},
};

use anyhow::Context;
use chrono::{Local, TimeZone};
use chrono_humanize::{Accuracy, HumanTime, Tense};
use info_json::{Comment, InfoJson, Parent};
use regex::Regex;

mod info_json;

fn get_runtime_path(component: &'static str) -> anyhow::Result<PathBuf> {
    let out: PathBuf = format!(
        "{}/{}",
        env::var("XDG_RUNTIME_DIR").expect("This should always exist"),
        component
    )
    .into();
    fs::create_dir_all(out.parent().expect("Parent should exist"))?;
    Ok(out)
}

const STATUS_PATH: &str = "ytcc/running";
pub fn status_path() -> anyhow::Result<PathBuf> {
    get_runtime_path(STATUS_PATH)
}

#[derive(Debug, Clone)]
pub struct CommentExt {
    pub value: Comment,
    pub replies: Vec<CommentExt>,
}

#[derive(Debug, Default)]
pub struct Comments {
    vec: Vec<CommentExt>,
}

impl Comments {
    pub fn new() -> Self {
        Self::default()
    }
    pub fn push(&mut self, value: CommentExt) {
        self.vec.push(value);
    }
    pub fn get_mut(&mut self, key: &str) -> Option<&mut CommentExt> {
        self.vec.iter_mut().filter(|c| c.value.id.id == key).last()
    }
    pub fn insert(&mut self, key: &str, value: CommentExt) {
        let parent = self
            .vec
            .iter_mut()
            .filter(|c| c.value.id.id == key)
            .last()
            .expect("One of these should exist");
        parent.push_reply(value);
    }
}
impl CommentExt {
    pub fn push_reply(&mut self, value: CommentExt) {
        self.replies.push(value)
    }
    pub fn get_mut_reply(&mut self, key: &str) -> Option<&mut CommentExt> {
        self.replies
            .iter_mut()
            .filter(|c| c.value.id.id == key)
            .last()
    }
}

impl From<Comment> for CommentExt {
    fn from(value: Comment) -> Self {
        Self {
            replies: vec![],
            value,
        }
    }
}

impl Display for Comments {
    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
        macro_rules! c {
            ($color:expr, $write:ident) => {
                $write.write_str(concat!("\x1b[", $color, "m"))?
            };
        }

        fn format(
            comment: &CommentExt,
            f: &mut std::fmt::Formatter<'_>,
            ident_count: u32,
        ) -> std::fmt::Result {
            let ident = &(0..ident_count).map(|_| " ").collect::<String>();
            let value = &comment.value;

            f.write_str(ident)?;

            if value.author_is_uploader {
                c!("91;1", f);
            } else {
                c!("35", f);
            }

            f.write_str(&value.author)?;
            c!("0", f);
            if value.edited || value.is_favorited {
                f.write_str("[")?;
                if value.edited {
                    f.write_str("")?;
                }
                if value.edited && value.is_favorited {
                    f.write_str(" ")?;
                }
                if value.is_favorited {
                    f.write_str("")?;
                }
                f.write_str("]")?;
            }

            c!("36;1", f);
            write!(
                f,
                " {}",
                HumanTime::from(
                    Local
                        .timestamp_opt(value.timestamp, 0)
                        .single()
                        .expect("This should be valid")
                )
                .to_text_en(Accuracy::Rough, Tense::Past)
            )?;
            c!("0", f);

            // c!("31;1", f);
            // f.write_fmt(format_args!(" [{}]", comment.value.like_count))?;
            // c!("0", f);

            f.write_str(":\n")?;
            f.write_str(ident)?;

            f.write_str(&value.text.replace('\n', &format!("\n{}", ident)))?;
            f.write_str("\n")?;

            if !comment.replies.is_empty() {
                let mut children = comment.replies.clone();
                children.sort_by(|a, b| a.value.timestamp.cmp(&b.value.timestamp));

                for child in children {
                    format(&child, f, ident_count + 4)?;
                }
            } else {
                f.write_str("\n")?;
            }

            Ok(())
        }

        if !&self.vec.is_empty() {
            let mut children = self.vec.clone();
            children.sort_by(|a, b| b.value.like_count.cmp(&a.value.like_count));

            for child in children {
                format(&child, f, 0)?
            }
        }
        Ok(())
    }
}

fn main() -> anyhow::Result<()> {
    cli_log::init_cli_log!();
    let args: Option<String> = env::args().skip(1).last();
    let mut info_json: InfoJson = {
        let status_path = if let Some(arg) = args {
            PathBuf::from(arg)
        } else {
            status_path().context("Failed to get status path")?
        };

        let reader =
            BufReader::new(File::open(&status_path).with_context(|| {
                format!("Failed to open status file at {}", status_path.display())
            })?);

        serde_json::from_reader(reader)?
    };

    let base_comments = mem::take(&mut info_json.comments);
    drop(info_json);

    let mut comments = Comments::new();
    base_comments.into_iter().for_each(|c| {
        if let Parent::Id(id) = &c.parent {
            comments.insert(&(id.clone()), CommentExt::from(c));
        } else {
            comments.push(CommentExt::from(c));
        }
    });

    comments.vec.iter_mut().for_each(|comment| {
        let replies = mem::take(&mut comment.replies);
        let mut output_replies: Vec<CommentExt>  = vec![];

        let re = Regex::new(r"\u{200b}?(@[^\t\s]+)\u{200b}?").unwrap();
        for reply in replies {
            if let Some(replyee_match) =  re.captures(&reply.value.text){
                let full_match = replyee_match.get(0).expect("This always exists");
                let text = reply.
                    value.
                    text[0..full_match.start()]
                    .to_owned()
                    +
                    &reply
                    .value
                    .text[full_match.end()..];
                let text: &str = text.trim().trim_matches('\u{200b}');

                let replyee = replyee_match.get(1).expect("This should exist").as_str();


                if let Some(parent) = output_replies
                    .iter_mut()
                    // .rev()
                    .flat_map(|com| &mut com.replies)
                    .flat_map(|com| &mut com.replies)
                    .flat_map(|com| &mut com.replies)
                    .filter(|com| com.value.author == replyee)
                    .last()
                {
                    parent.replies.push(CommentExt::from(Comment {
                        text: text.to_owned(),
                        ..reply.value
                    }))
                } else if let Some(parent) = output_replies
                    .iter_mut()
                    // .rev()
                    .flat_map(|com| &mut com.replies)
                    .flat_map(|com| &mut com.replies)
                    .filter(|com| com.value.author == replyee)
                    .last()
                {
                    parent.replies.push(CommentExt::from(Comment {
                        text: text.to_owned(),
                        ..reply.value
                    }))
                } else if let Some(parent) = output_replies
                    .iter_mut()
                    // .rev()
                    .flat_map(|com| &mut com.replies)
                    .filter(|com| com.value.author == replyee)
                    .last()
                {
                    parent.replies.push(CommentExt::from(Comment {
                        text: text.to_owned(),
                        ..reply.value
                    }))
                } else if let Some(parent) = output_replies.iter_mut()
                    // .rev()
                    .filter(|com| com.value.author == replyee)
                    .last()
                {
                    parent.replies.push(CommentExt::from(Comment {
                        text: text.to_owned(),
                        ..reply.value
                    }))
                } else {
                    eprintln!(
                    "Failed to find a parent for ('{}') both directly and via replies! The reply text was:\n'{}'\n",
                    replyee,
                    reply.value.text
                );
                    output_replies.push(reply);
                }
            } else {
                output_replies.push(reply);
            }
        }
        comment.replies = output_replies;
    });

    let mut less = Command::new("less")
        .args(["--raw-control-chars"])
        .stdin(Stdio::piped())
        .stderr(Stdio::inherit())
        .spawn()
        .context("Failed to run less")?;

    let mut child = Command::new("fmt")
        .args(["--uniform-spacing", "--split-only", "--width=90"])
        .stdin(Stdio::piped())
        .stderr(Stdio::inherit())
        .stdout(less.stdin.take().expect("Should be open"))
        .spawn()
        .context("Failed to run fmt")?;

    let mut stdin = child.stdin.take().context("Failed to open stdin")?;
    std::thread::spawn(move || {
        stdin
            .write_all(comments.to_string().as_bytes())
            .expect("Should be able to write to stdin of fmt");
    });

    let _ = less.wait().context("Failed to await less")?;

    Ok(())
}

#[cfg(test)]
mod test {
    #[test]
    fn test_string_replacement() {
        let s = "A \n\nB\n\nC".to_owned();
        assert_eq!("A \n  \n  B\n  \n  C", s.replace('\n', "\n  "))
    }
}