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 "))
}
}