about summary refs log tree commit diff stats
path: root/sys/nixpkgs/pkgs/comments/src/main.rs
diff options
context:
space:
mode:
Diffstat (limited to '')
-rw-r--r--sys/nixpkgs/pkgs/comments/src/main.rs322
1 files changed, 322 insertions, 0 deletions
diff --git a/sys/nixpkgs/pkgs/comments/src/main.rs b/sys/nixpkgs/pkgs/comments/src/main.rs
new file mode 100644
index 00000000..de98d75e
--- /dev/null
+++ b/sys/nixpkgs/pkgs/comments/src/main.rs
@@ -0,0 +1,322 @@
+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()
+        .expect("Should work");
+
+    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()
+        .expect("Failed to spawn child process");
+
+    let mut stdin = child.stdin.take().expect("Failed to open stdin");
+    std::thread::spawn(move || {
+        stdin
+            .write_all(comments.to_string().as_bytes())
+            .expect("Failed to write to stdin");
+    });
+
+    let _ = less.wait().expect("Failed to read stdout");
+
+    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  "))
+    }
+}