commit d809bdb9cff509fa60d941b61fc7912e8b88eb9e
parent 955a77a99bf88e9aa45232372803379913e58189
Author: Shimmy Xu <shimmy.xu@shimmy1996.com>
Date: Fri, 4 Oct 2019 12:04:26 -0400
Use a separate Entry struct for all the parsing logic
Diffstat:
A | src/entry.rs | | | 127 | +++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++ |
M | src/main.rs | | | 1 | + |
M | src/timeline.rs | | | 93 | +++++++++++-------------------------------------------------------------------- |
M | src/tweet.rs | | | 15 | ++------------- |
4 files changed, 142 insertions(+), 94 deletions(-)
diff --git a/src/entry.rs b/src/entry.rs
@@ -0,0 +1,127 @@
+use chrono::{DateTime, Duration, FixedOffset, Local, SecondsFormat};
+
+use std::fmt;
+
+#[derive(Ord, PartialOrd, PartialEq, Eq)]
+pub struct Entry {
+ pub timestamp: DateTime<FixedOffset>,
+ pub author: Option<String>,
+ pub content: String,
+}
+
+impl Entry {
+ /// Generates a new entry from given content.
+ pub fn new(content: String, author: Option<String>) -> Entry {
+ Entry {
+ timestamp: Local::now().into(),
+ author,
+ content,
+ }
+ }
+
+ /// Attempt to parse given line into entry format.
+ pub fn parse(line: &str, author: Option<&str>) -> Result<Entry, ()> {
+ if let Some(seperator_idx) = line.find('\t') {
+ if let Ok(timestamp) = DateTime::parse_from_rfc3339(&line[..seperator_idx]) {
+ Ok(Entry {
+ timestamp,
+ author: author.and_then(|x| Some(x.to_owned())),
+ content: line[seperator_idx + 1..].to_owned(),
+ })
+ } else {
+ Err(())
+ }
+ } else {
+ Err(())
+ }
+ }
+
+ /// Generates spec representation for given entry.
+ pub fn to_string(&self) -> String {
+ format!(
+ "{}\t{}\n",
+ &self.timestamp.to_rfc3339_opts(SecondsFormat::Secs, true),
+ &self.content
+ )
+ }
+
+ /// 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)
+ }
+ }
+}
+
+impl fmt::Display for Entry {
+ /// Formats a tweet for display in terminal.
+ /// Alternate format uses absolute time.
+ fn fmt(&self, formatter: &mut fmt::Formatter) -> fmt::Result {
+ let timestamp = if formatter.alternate() {
+ self.timestamp
+ .with_timezone(&Local)
+ .to_rfc3339_opts(SecondsFormat::Secs, true)
+ } else {
+ Self::format_duration(Local::now() - self.timestamp.with_timezone(&Local))
+ };
+
+ write!(
+ formatter,
+ "\n@{} {}\n{}",
+ self.author.as_ref().unwrap_or(&"".to_string()),
+ ×tamp,
+ &self.content
+ )
+ }
+}
+
+#[cfg(test)]
+mod tests {
+ use super::*;
+
+ #[test]
+ fn test_format_duration() {
+ assert_eq!(
+ Entry::format_duration(Duration::days(365 * 2)),
+ "2 years ago"
+ );
+ assert_eq!(Entry::format_duration(Duration::days(365)), "1 year ago");
+ assert_eq!(
+ Entry::format_duration(Duration::days(30 * 3)),
+ "3 months ago"
+ );
+ assert_eq!(Entry::format_duration(Duration::days(30)), "1 month ago");
+ assert_eq!(Entry::format_duration(Duration::weeks(4)), "4 weeks ago");
+ assert_eq!(Entry::format_duration(Duration::weeks(1)), "1 week ago");
+ assert_eq!(Entry::format_duration(Duration::days(4)), "4 days ago");
+ assert_eq!(Entry::format_duration(Duration::days(1)), "1 day ago");
+ assert_eq!(Entry::format_duration(Duration::hours(23)), "23 hours ago");
+ assert_eq!(Entry::format_duration(Duration::hours(1)), "1 hour ago");
+ assert_eq!(
+ Entry::format_duration(Duration::minutes(5)),
+ "5 minutes ago"
+ );
+ assert_eq!(Entry::format_duration(Duration::minutes(1)), "1 minute ago");
+ assert_eq!(Entry::format_duration(Duration::seconds(30)), "just now");
+ }
+}
diff --git a/src/main.rs b/src/main.rs
@@ -3,6 +3,7 @@ use clap::{crate_version, App, Arg, SubCommand};
use std::path::Path;
mod config;
+mod entry;
mod follow;
mod timeline;
mod tweet;
diff --git a/src/timeline.rs b/src/timeline.rs
@@ -1,15 +1,15 @@
-use chrono::{DateTime, Duration, FixedOffset, Local, SecondsFormat};
use clap::ArgMatches;
use reqwest::Client;
use std::collections::BinaryHeap;
use crate::config::Config;
+use crate::entry::Entry;
/// Print timeline from following.
pub fn timeline(config: &Config, _subcommand: &ArgMatches) {
// Store (post_time, nick, content).
- let mut all_tweets = BinaryHeap::<(DateTime<FixedOffset>, String, String)>::new();
+ let mut all_tweets = BinaryHeap::<Entry>::new();
// Pull and parse twtxt files from user and each followed source.
for (nick, twturl) in config
@@ -17,102 +17,33 @@ pub fn timeline(config: &Config, _subcommand: &ArgMatches) {
.iter()
.chain(vec![(&config.nick, &config.twturl)].into_iter())
{
- let tweets = parse_twtxt(twturl);
- for (post_time, content) in tweets {
- all_tweets.push((post_time, nick.to_owned(), content));
- }
+ all_tweets.append(&mut parse_twtxt(twturl, nick));
}
// Print the most recent tweets.
- let now = Local::now();
for _ in 0..config.limit_timeline {
if let Some(tweet) = all_tweets.pop() {
- println!("{}", format_tweet(&tweet, &now, config.use_abs_time));
+ if config.use_abs_time {
+ println!("{:#}", tweet);
+ } else {
+ println!("{}", tweet);
+ }
}
}
}
/// Parses given twtxt url, returns a Vec of (post_time, content).
-fn parse_twtxt(twturl: &str) -> Vec<(DateTime<FixedOffset>, String)> {
+fn parse_twtxt(twturl: &str, nick: &str) -> BinaryHeap<Entry> {
let client = Client::new();
- let mut tweets = Vec::new();
+ let mut tweets = BinaryHeap::new();
if let Ok(resp_text) = client.get(twturl).send().and_then(|mut resp| resp.text()) {
for line in resp_text.lines() {
- if let Some(seperator_idx) = line.find('\t') {
- if let Ok(tweet_time) = DateTime::parse_from_rfc3339(&line[..seperator_idx]) {
- tweets.push((tweet_time, line[seperator_idx + 1..].to_owned()));
- };
+ if let Ok(parsed) = Entry::parse(line, Some(nick)) {
+ tweets.push(parsed);
};
}
};
tweets
}
-
-/// Formats a tweet for display in terminal.
-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)
- } else {
- format_duration(*now - tweet.0.with_timezone(&now.timezone()))
- };
-
- format!("\n@{} {}\n{}", &tweet.1, ×tamp, &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");
- }
-}
diff --git a/src/tweet.rs b/src/tweet.rs
@@ -1,4 +1,3 @@
-use chrono::{Local, SecondsFormat};
use clap::ArgMatches;
use std::fs::OpenOptions;
@@ -7,6 +6,7 @@ use std::path::Path;
use std::process::Command;
use crate::config::Config;
+use crate::entry::Entry;
/// Helper to run the tweet subcommand.
pub fn tweet(config: &Config, subcommand: &ArgMatches) {
@@ -41,7 +41,7 @@ pub fn tweet(config: &Config, subcommand: &ArgMatches) {
.create(true)
.open(Path::new(&config.twtfile))
.unwrap()
- .write(compose(content).as_bytes())
+ .write(Entry::new(content, None).to_string().as_bytes())
.expect("Unable to write new post");
// Run post tweet hook.
@@ -51,14 +51,3 @@ pub fn tweet(config: &Config, subcommand: &ArgMatches) {
.expect("Failed to run post tweet hook");
}
}
-
-/// Formats given content into twtxt format by adding datetime.
-fn compose(content: String) -> String {
- let timestamp = Local::now().to_rfc3339_opts(SecondsFormat::Secs, true);
- let mut post = String::new();
- post.push_str(×tamp);
- post.push('\t');
- post.push_str(&content);
- post.push('\n');
- post
-}