twixter

A twtxt command line client in Rust

git clone git://git.shimmy1996.com/twixter.git
commit df562c64d4469a4f2cfd0dcc05859303f176625d
parent e406cbd47f5a8d3129b97df87f0bd3c8919d9a0f
Author: Shimmy Xu <shimmy.xu@shimmy1996.com>
Date:   Sun, 15 Sep 2019 17:43:30 -0400

Add support for relative timestamps

Diffstat:
Mconfig.example | 2++
Msrc/config.rs | 2++
Msrc/timeline.rs | 74++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++----------
3 files changed, 68 insertions(+), 10 deletions(-)
diff --git a/config.example b/config.example
@@ -4,6 +4,8 @@ twtfile = ~/.local/share/twixter/twtxt.txt
 twturl = https://example.org/twtxt.txt
 pre_tweet_hook = "scp buckket@example.org:~/public_html/twtxt.txt {twtfile}"
 post_tweet_hook = "scp {twtfile} buckket@example.org:~/public_html/twtxt.txt"
+limit_timeline = 20
+use_abs_time = false
 
 [following]
 bob = https://example.org/bob.txt
diff --git a/src/config.rs b/src/config.rs
@@ -9,6 +9,7 @@ pub struct Config {
     pub pre_tweet_hook: String,
     pub post_tweet_hook: String,
     pub limit_timeline: i32,
+    pub use_abs_time: bool,
     pub following: HashMap<String, String>,
 }
 
@@ -35,6 +36,7 @@ impl Config {
             pre_tweet_hook: pre_tweet_hook,
             post_tweet_hook: post_tweet_hook,
             limit_timeline: twtxt_config["limit_timeline"].parse::<i32>().unwrap(),
+            use_abs_time: twtxt_config["use_abs_time"].parse::<bool>().unwrap(),
             following: following,
         }
     }
diff --git a/src/timeline.rs b/src/timeline.rs
@@ -1,4 +1,4 @@
-use chrono::{DateTime, FixedOffset, Local, SecondsFormat};
+use chrono::{DateTime, Duration, FixedOffset, Local, SecondsFormat};
 use clap::ArgMatches;
 use reqwest::Client;
 
@@ -27,7 +27,7 @@ pub fn timeline(config: &Config, _subcommand: &ArgMatches) {
     let now = Local::now();
     for _ in 0..config.limit_timeline {
         if let Some(tweet) = all_tweets.pop() {
-            println!("{}", format_tweet(&tweet, &now));
+            println!("{}", format_tweet(&tweet, &now, config.use_abs_time));
         }
     }
 }
@@ -51,14 +51,68 @@ fn parse_twtxt(twturl: &str) -> Vec<(DateTime<FixedOffset>, String)> {
 }
 
 /// Formats a tweet for display in terminal.
-fn format_tweet(tweet: &(DateTime<FixedOffset>, String, String), now: &DateTime<Local>) -> String {
-    format!(
-        "\n@{} {}\n{}",
-        &tweet.1,
-        &tweet
+fn format_tweet(
+    tweet: &(DateTime<FixedOffset>, String, String),
+    now: &DateTime<Local>,
+    use_abs_time: bool,
+) -> String {
+    let timestamp = if use_abs_time {
+        tweet
             .0
             .with_timezone(&now.timezone())
-            .to_rfc3339_opts(SecondsFormat::Secs, true),
-        &tweet.2
-    )
+            .to_rfc3339_opts(SecondsFormat::Secs, true)
+    } else {
+        format_duration(*now - tweet.0.with_timezone(&now.timezone()))
+    };
+
+    format!("\n@{} {}\n{}", &tweet.1, &timestamp, &tweet.2)
+}
+
+/// Formats a time duration in human readable format.
+/// Shows the first non-zero amount of year, month, week, day, hour, or
+/// minute. For duration shorter than one minute, return "just now".
+fn format_duration(duration: Duration) -> String {
+    let (num, unit) = if duration.num_days() >= 365 {
+        (duration.num_days() / 365, "year")
+    } else if duration.num_days() >= 30 {
+        (duration.num_days() / 30, "month")
+    } else if duration.num_weeks() >= 1 {
+        (duration.num_weeks(), "week")
+    } else if duration.num_days() >= 1 {
+        (duration.num_days(), "day")
+    } else if duration.num_hours() >= 1 {
+        (duration.num_hours(), "hour")
+    } else if duration.num_minutes() >= 1 {
+        (duration.num_minutes(), "minute")
+    } else {
+        return "just now".to_string();
+    };
+
+    if num > 1 {
+        format!("{} {}s ago", num, unit)
+    } else {
+        format!("{} {} ago", num, unit)
+    }
+}
+
+#[cfg(test)]
+mod tests {
+    use super::*;
+
+    #[test]
+    fn test_format_duration() {
+        assert_eq!(format_duration(Duration::days(365 * 2)), "2 years ago");
+        assert_eq!(format_duration(Duration::days(365)), "1 year ago");
+        assert_eq!(format_duration(Duration::days(30 * 3)), "3 months ago");
+        assert_eq!(format_duration(Duration::days(30)), "1 month ago");
+        assert_eq!(format_duration(Duration::weeks(4)), "4 weeks ago");
+        assert_eq!(format_duration(Duration::weeks(1)), "1 week ago");
+        assert_eq!(format_duration(Duration::days(4)), "4 days ago");
+        assert_eq!(format_duration(Duration::days(1)), "1 day ago");
+        assert_eq!(format_duration(Duration::hours(23)), "23 hours ago");
+        assert_eq!(format_duration(Duration::hours(1)), "1 hour ago");
+        assert_eq!(format_duration(Duration::minutes(5)), "5 minutes ago");
+        assert_eq!(format_duration(Duration::minutes(1)), "1 minute ago");
+        assert_eq!(format_duration(Duration::seconds(30)), "just now");
+    }
 }