From b49e32c4094adcb415a0b8c256424e8bf62f3d0d Mon Sep 17 00:00:00 2001 From: thunderbiscuit Date: Tue, 18 Aug 2020 11:33:19 -0400 Subject: [PATCH 01/23] Format code using cargo fmt This commit blindly runs the `cargo fmt` command. WIP, as some minor adjustments are likely needed. --- src/config.rs | 128 +++++++++++----- src/db.rs | 283 ++++++++++++++++++---------------- src/downloads.rs | 34 +++-- src/feeds.rs | 86 ++++++----- src/keymap.rs | 22 +-- src/main.rs | 116 +++++++------- src/main_controller.rs | 340 +++++++++++++++++++++++------------------ src/opml.rs | 18 +-- src/play_file.rs | 22 +-- src/sanitizer.rs | 15 +- src/threadpool.rs | 11 +- src/types.rs | 127 ++++++++------- src/ui/colors.rs | 29 ++-- src/ui/menu.rs | 198 ++++++++++++++---------- src/ui/mock_panel.rs | 101 ++++++------ src/ui/mod.rs | 340 ++++++++++++++++++++++------------------- src/ui/notification.rs | 68 ++++----- src/ui/panel.rs | 107 +++++++------ 18 files changed, 1147 insertions(+), 898 deletions(-) diff --git a/src/config.rs b/src/config.rs index da5949e..ba1ed6b 100644 --- a/src/config.rs +++ b/src/config.rs @@ -1,6 +1,6 @@ +use serde::Deserialize; use std::fs::File; use std::io::Read; -use serde::Deserialize; use std::path::PathBuf; use crate::keymap::{Keybindings, UserAction}; @@ -25,8 +25,7 @@ pub const EPISODE_PUBDATE_LENGTH: usize = 60; // display the details panel pub const DETAILS_PANEL_LENGTH: i32 = 135; - -/// Holds information about user configuration of program. +/// Holds information about user configuration of program. #[derive(Debug, Clone)] pub struct Config { pub download_path: PathBuf, @@ -38,8 +37,7 @@ pub struct Config { /// A temporary struct used to deserialize data from the TOML configuration /// file. Will be converted into Config struct. -#[derive(Debug)] -#[derive(Deserialize)] +#[derive(Debug, Deserialize)] struct ConfigFromToml { download_path: Option, play_command: Option, @@ -50,8 +48,7 @@ struct ConfigFromToml { /// A temporary struct used to deserialize keybinding data from the TOML /// configuration file. -#[derive(Debug)] -#[derive(Deserialize)] +#[derive(Debug, Deserialize)] struct KeybindingsFromToml { left: Option>, right: Option>, @@ -72,7 +69,6 @@ struct KeybindingsFromToml { quit: Option>, } - impl Config { /// Given a file path, this reads a TOML config file and returns a /// Config struct with keybindings, etc. Inserts defaults if config @@ -87,7 +83,7 @@ impl Config { .expect("Error reading config.toml. Please ensure file is readable."); config_toml = toml::from_str(&config_string) .expect("Error parsing config.toml. Please check file syntax."); - }, + } Err(_) => { // if we can't find the file, set everything to empty // so we it will use the defaults for everything @@ -131,27 +127,91 @@ impl Config { fn config_with_defaults(config_toml: &ConfigFromToml) -> Config { // specify all default keybindings for actions let action_map: Vec<(&Option>, UserAction, Vec)> = vec![ - (&config_toml.keybindings.left, UserAction::Left, vec!["Left".to_string(), "h".to_string()]), - (&config_toml.keybindings.right, UserAction::Right, vec!["Right".to_string(), "l".to_string()]), - (&config_toml.keybindings.up, UserAction::Up, vec!["Up".to_string(), "k".to_string()]), - (&config_toml.keybindings.down, UserAction::Down, vec!["Down".to_string(), "j".to_string()]), - - (&config_toml.keybindings.add_feed, UserAction::AddFeed, vec!["a".to_string()]), - (&config_toml.keybindings.sync, UserAction::Sync, vec!["s".to_string()]), - (&config_toml.keybindings.sync_all, UserAction::SyncAll, vec!["S".to_string()]), - - (&config_toml.keybindings.play, UserAction::Play, vec!["Enter".to_string(), "p".to_string()]), - (&config_toml.keybindings.mark_played, UserAction::MarkPlayed, vec!["m".to_string()]), - (&config_toml.keybindings.mark_all_played, UserAction::MarkAllPlayed, vec!["M".to_string()]), - - (&config_toml.keybindings.download, UserAction::Download, vec!["d".to_string()]), - (&config_toml.keybindings.download_all, UserAction::DownloadAll, vec!["D".to_string()]), - (&config_toml.keybindings.delete, UserAction::Delete, vec!["x".to_string()]), - (&config_toml.keybindings.delete_all, UserAction::DeleteAll, vec!["X".to_string()]), - (&config_toml.keybindings.remove, UserAction::Remove, vec!["r".to_string()]), - (&config_toml.keybindings.remove_all, UserAction::RemoveAll, vec!["R".to_string()]), - - (&config_toml.keybindings.quit, UserAction::Quit, vec!["q".to_string()]), + ( + &config_toml.keybindings.left, + UserAction::Left, + vec!["Left".to_string(), "h".to_string()], + ), + ( + &config_toml.keybindings.right, + UserAction::Right, + vec!["Right".to_string(), "l".to_string()], + ), + ( + &config_toml.keybindings.up, + UserAction::Up, + vec!["Up".to_string(), "k".to_string()], + ), + ( + &config_toml.keybindings.down, + UserAction::Down, + vec!["Down".to_string(), "j".to_string()], + ), + ( + &config_toml.keybindings.add_feed, + UserAction::AddFeed, + vec!["a".to_string()], + ), + ( + &config_toml.keybindings.sync, + UserAction::Sync, + vec!["s".to_string()], + ), + ( + &config_toml.keybindings.sync_all, + UserAction::SyncAll, + vec!["S".to_string()], + ), + ( + &config_toml.keybindings.play, + UserAction::Play, + vec!["Enter".to_string(), "p".to_string()], + ), + ( + &config_toml.keybindings.mark_played, + UserAction::MarkPlayed, + vec!["m".to_string()], + ), + ( + &config_toml.keybindings.mark_all_played, + UserAction::MarkAllPlayed, + vec!["M".to_string()], + ), + ( + &config_toml.keybindings.download, + UserAction::Download, + vec!["d".to_string()], + ), + ( + &config_toml.keybindings.download_all, + UserAction::DownloadAll, + vec!["D".to_string()], + ), + ( + &config_toml.keybindings.delete, + UserAction::Delete, + vec!["x".to_string()], + ), + ( + &config_toml.keybindings.delete_all, + UserAction::DeleteAll, + vec!["X".to_string()], + ), + ( + &config_toml.keybindings.remove, + UserAction::Remove, + vec!["r".to_string()], + ), + ( + &config_toml.keybindings.remove_all, + UserAction::RemoveAll, + vec!["R".to_string()], + ), + ( + &config_toml.keybindings.quit, + UserAction::Quit, + vec!["q".to_string()], + ), ]; // for each action, if user preference is set, use that, otherwise, @@ -166,9 +226,8 @@ fn config_with_defaults(config_toml: &ConfigFromToml) -> Config { // paths are set by user, or they resolve to OS-specific path as // provided by dirs crate - let download_path = parse_create_dir( - config_toml.download_path.as_deref(), - dirs::data_local_dir()); + let download_path = + parse_create_dir(config_toml.download_path.as_deref(), dirs::data_local_dir()); let play_command = match config_toml.play_command.as_deref() { Some(cmd) => cmd.to_string(), @@ -196,7 +255,6 @@ fn config_with_defaults(config_toml: &ConfigFromToml) -> Config { }; } - /// Helper function that takes an (optionally specified) user directory /// and an (OS-dependent) default directory, expands any environment /// variables, ~ alias, etc. Returns a PathBuf. Panics if environment @@ -228,4 +286,4 @@ fn parse_create_dir(user_dir: Option<&str>, default: Option) -> PathBuf } return final_path; -} \ No newline at end of file +} diff --git a/src/db.rs b/src/db.rs index 003ee1c..9baa9ae 100644 --- a/src/db.rs +++ b/src/db.rs @@ -1,9 +1,9 @@ use std::path::PathBuf; -use rusqlite::{Connection, params}; -use chrono::{NaiveDateTime, DateTime, Utc}; +use chrono::{DateTime, NaiveDateTime, Utc}; use lazy_static::lazy_static; use regex::Regex; +use rusqlite::{params, Connection}; use semver::Version; use crate::types::*; @@ -14,7 +14,6 @@ lazy_static! { static ref RE_ARTICLES: Regex = Regex::new(r"^(a|an|the) ").unwrap(); } - pub struct SyncResult { pub added: Vec, pub updated: Vec, @@ -38,9 +37,7 @@ impl Database { db_path.push("data.db"); match Connection::open(db_path) { Ok(conn) => { - let db_conn = Database { - conn: Some(conn), - }; + let db_conn = Database { conn: Some(conn) }; db_conn.create(); { @@ -50,8 +47,9 @@ impl Database { conn.execute("PRAGMA foreign_keys=ON;", params![]).unwrap(); // get version number stored in database - let mut stmt = conn.prepare( - "SELECT version FROM version WHERE id = 1;").unwrap(); + let mut stmt = conn + .prepare("SELECT version FROM version WHERE id = 1;") + .unwrap(); let db_version = stmt.query_row(params![], |row| { let vstr: String = row.get("version")?; Ok(Version::parse(&vstr).unwrap()) @@ -63,11 +61,11 @@ impl Database { let to_update = match db_version { Ok(dbv) => { if dbv < curr_ver { - (true, true) + (true, true) } else { (true, false) } - }, + } Err(_) => (false, true), }; @@ -80,7 +78,7 @@ impl Database { } return db_conn; - }, + } Err(err) => panic!("Could not open database: {}", err), }; } @@ -147,7 +145,7 @@ impl Database { id INTEGER PRIMARY KEY NOT NULL, version TEXT NOT NULL );", - params![] + params![], ) { Ok(_) => (), Err(err) => panic!("Could not create version database table: {}", err), @@ -161,19 +159,26 @@ impl Database { let conn = self.conn.as_ref().unwrap(); if update { - let _ = conn.execute("UPDATE version SET version = ? - WHERE id = ?;", params![current_version.to_string(), 1]); + let _ = conn.execute( + "UPDATE version SET version = ? + WHERE id = ?;", + params![current_version.to_string(), 1], + ); } else { - let _ = conn.execute("INSERT INTO version (id, version) - VALUES (?, ?)", params![1, current_version.to_string()]); + let _ = conn.execute( + "INSERT INTO version (id, version) + VALUES (?, ?)", + params![1, current_version.to_string()], + ); } } /// Inserts a new podcast and list of podcast episodes into the /// database. - pub fn insert_podcast(&self, podcast: PodcastNoId) -> - Result> { - + pub fn insert_podcast( + &self, + podcast: PodcastNoId, + ) -> Result> { let conn = self.conn.as_ref().unwrap(); let _ = conn.execute( "INSERT INTO podcasts (title, url, description, author, @@ -186,13 +191,14 @@ impl Database { podcast.author, podcast.explicit, podcast.last_checked.timestamp() - ] + ], )?; - let mut stmt = conn.prepare( - "SELECT id FROM podcasts WHERE url = ?").unwrap(); + let mut stmt = conn + .prepare("SELECT id FROM podcasts WHERE url = ?") + .unwrap(); let pod_id = stmt - .query_row::(params![podcast.url], |row| row.get(0)) + .query_row::(params![podcast.url], |row| row.get(0)) .unwrap(); let mut ep_ids = Vec::new(); for ep in podcast.episodes.iter().rev() { @@ -207,9 +213,11 @@ impl Database { } /// Inserts a podcast episode into the database. - pub fn insert_episode(&self, podcast_id: i64, episode: &EpisodeNoId) -> - Result> { - + pub fn insert_episode( + &self, + podcast_id: i64, + episode: &EpisodeNoId, + ) -> Result> { let conn = self.conn.as_ref().unwrap(); let pubdate = match episode.pubdate { @@ -230,24 +238,23 @@ impl Database { episode.duration, false, false, - ] + ], )?; return Ok(conn.last_insert_rowid()); } /// Inserts a filepath to a downloaded episode. - pub fn insert_file(&self, episode_id: i64, path: &PathBuf) -> - Result<(), Box> { - + pub fn insert_file( + &self, + episode_id: i64, + path: &PathBuf, + ) -> Result<(), Box> { let conn = self.conn.as_ref().unwrap(); let _ = conn.execute( "INSERT INTO files (episode_id, path) VALUES (?, ?);", - params![ - episode_id, - path.to_str(), - ] + params![episode_id, path.to_str(),], )?; return Ok(()); } @@ -256,10 +263,12 @@ impl Database { /// user has chosen to delete the file. pub fn remove_file(&self, episode_id: i64) { let conn = self.conn.as_ref().unwrap(); - let _ = conn.execute( - "DELETE FROM files WHERE episode_id = ?;", - params![episode_id] - ).unwrap(); + let _ = conn + .execute( + "DELETE FROM files WHERE episode_id = ?;", + params![episode_id], + ) + .unwrap(); } /// Removes all file listings for the selected episode ids. @@ -267,15 +276,15 @@ impl Database { let conn = self.conn.as_ref().unwrap(); // convert list of episode ids into a comma-separated String - let episode_list: Vec = episode_ids.iter() - .map(|x| x.to_string()) - .collect(); + let episode_list: Vec = episode_ids.iter().map(|x| x.to_string()).collect(); let episodes = episode_list.join(", "); - let _ = conn.execute( - "DELETE FROM files WHERE episode_id = (?);", - params![episodes] - ).unwrap(); + let _ = conn + .execute( + "DELETE FROM files WHERE episode_id = (?);", + params![episodes], + ) + .unwrap(); } /// Removes a podcast, all episodes, and files from the database. @@ -285,16 +294,19 @@ impl Database { // and `files` tables, all associated episodes for this podcast // will also be deleted, and all associated file entries for // those episodes as well. - let _ = conn.execute( - "DELETE FROM podcasts WHERE id = ?;", - params![podcast_id] - ).unwrap(); + let _ = conn + .execute("DELETE FROM podcasts WHERE id = ?;", params![podcast_id]) + .unwrap(); } /// Updates an existing podcast in the database, where metadata is /// changed if necessary, and episodes are updated (modified episodes /// are updated, new episodes are inserted). - pub fn update_podcast(&self, pod_id: i64, podcast: PodcastNoId) -> Result> { + pub fn update_podcast( + &self, + pod_id: i64, + podcast: PodcastNoId, + ) -> Result> { let conn = self.conn.as_ref().unwrap(); let _ = conn.execute( "UPDATE podcasts SET title = ?, url = ?, description = ?, @@ -308,11 +320,10 @@ impl Database { podcast.explicit, podcast.last_checked.timestamp(), pod_id, - ] + ], )?; - let result = self.update_episodes( - pod_id, podcast.episodes); + let result = self.update_episodes(pod_id, podcast.episodes); return Ok(result); } @@ -348,7 +359,7 @@ impl Database { matching += (new_ep.url == old_ep.url) as i32; let mut pd_match = false; - if let Some(pd) = new_pd { + if let Some(pd) = new_pd { if let Some(old_pd) = old_ep.pubdate { matching += (pd == old_pd.timestamp()) as i32; pd_match = true; @@ -360,11 +371,12 @@ impl Database { // if we have a matching episode, check whether there // are details to update - if !(new_ep.title == old_ep.title && - new_ep.url == old_ep.url && - new_ep.description == old_ep.description && - new_ep.duration == old_ep.duration && - pd_match) { + if !(new_ep.title == old_ep.title + && new_ep.url == old_ep.url + && new_ep.description == old_ep.description + && new_ep.duration == old_ep.duration + && pd_match) + { update = true; } break; @@ -374,22 +386,24 @@ impl Database { match existing_id { Some(id) => { if update { - let _ = conn.execute( - "UPDATE episodes SET title = ?, url = ?, + let _ = conn + .execute( + "UPDATE episodes SET title = ?, url = ?, description = ?, pubdate = ?, duration = ? WHERE id = ?;", - params![ - new_ep.title, - new_ep.url, - new_ep.description, - new_pd, - new_ep.duration, - id, - ] - ).unwrap(); + params![ + new_ep.title, + new_ep.url, + new_ep.description, + new_pd, + new_ep.duration, + id, + ], + ) + .unwrap(); update_ep.push(id); } - }, + } None => { let id = self.insert_episode(podcast_id, &new_ep).unwrap(); insert_ep.push(id); @@ -398,7 +412,7 @@ impl Database { } return SyncResult { added: insert_ep, - updated: update_ep + updated: update_ep, }; } @@ -406,10 +420,12 @@ impl Database { pub fn set_played_status(&self, episode_id: i64, played: bool) { let conn = self.conn.as_ref().unwrap(); - let _ = conn.execute( - "UPDATE episodes SET played = ? WHERE id = ?;", - params![played, episode_id] - ).unwrap(); + let _ = conn + .execute( + "UPDATE episodes SET played = ? WHERE id = ?;", + params![played, episode_id], + ) + .unwrap(); } /// Updates an episode to "remove" it by hiding it. "Removed" @@ -418,40 +434,43 @@ impl Database { pub fn hide_episode(&self, episode_id: i64, hide: bool) { let conn = self.conn.as_ref().unwrap(); - let _ = conn.execute( - "UPDATE episodes SET hidden = ? WHERE id = ?;", - params![hide, episode_id] - ).unwrap(); + let _ = conn + .execute( + "UPDATE episodes SET hidden = ? WHERE id = ?;", + params![hide, episode_id], + ) + .unwrap(); } /// Generates list of all podcasts in database. /// TODO: This should probably use a JOIN statement instead. pub fn get_podcasts(&self) -> Vec { if let Some(conn) = &self.conn { - let mut stmt = conn.prepare( - "SELECT * FROM podcasts;").unwrap(); - let podcast_iter = stmt.query_map(params![], |row| { - let pod_id = row.get("id")?; - let episodes = self.get_episodes(pod_id); - - // create a sort title that is lowercased and removes - // articles from the beginning - let title: String = row.get("title")?; - let title_lower = title.to_lowercase(); - let sort_title = RE_ARTICLES.replace(&title_lower, "").to_string(); - - Ok(Podcast { - id: pod_id, - title: title, - sort_title: sort_title, - url: row.get("url")?, - description: row.get("description")?, - author: row.get("author")?, - explicit: row.get("explicit")?, - last_checked: convert_date(row.get("last_checked")).unwrap(), - episodes: LockVec::new(episodes), + let mut stmt = conn.prepare("SELECT * FROM podcasts;").unwrap(); + let podcast_iter = stmt + .query_map(params![], |row| { + let pod_id = row.get("id")?; + let episodes = self.get_episodes(pod_id); + + // create a sort title that is lowercased and removes + // articles from the beginning + let title: String = row.get("title")?; + let title_lower = title.to_lowercase(); + let sort_title = RE_ARTICLES.replace(&title_lower, "").to_string(); + + Ok(Podcast { + id: pod_id, + title: title, + sort_title: sort_title, + url: row.get("url")?, + description: row.get("description")?, + author: row.get("author")?, + explicit: row.get("explicit")?, + last_checked: convert_date(row.get("last_checked")).unwrap(), + episodes: LockVec::new(episodes), + }) }) - }).unwrap(); + .unwrap(); let mut podcasts = Vec::new(); for pc in podcast_iter { podcasts.push(pc.unwrap()); @@ -467,29 +486,34 @@ impl Database { /// Generates list of episodes for a given podcast. pub fn get_episodes(&self, pod_id: i64) -> Vec { if let Some(conn) = &self.conn { - let mut stmt = conn.prepare( - "SELECT * FROM episodes + let mut stmt = conn + .prepare( + "SELECT * FROM episodes LEFT JOIN files ON episodes.id = files.episode_id WHERE episodes.podcast_id = ? AND episodes.hidden = 0 - ORDER BY pubdate DESC;").unwrap(); - let episode_iter = stmt.query_map(params![pod_id], |row| { - let path = match row.get::<&str, String>("path") { - Ok(val) => Some(PathBuf::from(val)), - Err(_) => None, - }; - Ok(Episode { - id: row.get("id")?, - pod_id: row.get("podcast_id")?, - title: row.get("title")?, - url: row.get("url")?, - description: row.get("description")?, - pubdate: convert_date(row.get("pubdate")), - duration: row.get("duration")?, - path: path, - played: row.get("played")?, + ORDER BY pubdate DESC;", + ) + .unwrap(); + let episode_iter = stmt + .query_map(params![pod_id], |row| { + let path = match row.get::<&str, String>("path") { + Ok(val) => Some(PathBuf::from(val)), + Err(_) => None, + }; + Ok(Episode { + id: row.get("id")?, + pod_id: row.get("podcast_id")?, + title: row.get("title")?, + url: row.get("url")?, + description: row.get("description")?, + pubdate: convert_date(row.get("pubdate")), + duration: row.get("duration")?, + path: path, + played: row.get("played")?, + }) }) - }).unwrap(); + .unwrap(); let mut episodes = Vec::new(); for ep in episode_iter { episodes.push(ep.unwrap()); @@ -510,19 +534,14 @@ impl Database { } } - /// Helper function converting an (optional) Unix timestamp to a /// DateTime object -fn convert_date(result: Result) -> - Option> { - +fn convert_date(result: Result) -> Option> { return match result { - Ok(timestamp) => { - match NaiveDateTime::from_timestamp_opt(timestamp, 0) { - Some(ndt) => Some(DateTime::from_utc(ndt, Utc)), - None => None, - } + Ok(timestamp) => match NaiveDateTime::from_timestamp_opt(timestamp, 0) { + Some(ndt) => Some(DateTime::from_utc(ndt, Utc)), + None => None, }, Err(_) => None, }; -} \ No newline at end of file +} diff --git a/src/downloads.rs b/src/downloads.rs index 93dbfbf..d347449 100644 --- a/src/downloads.rs +++ b/src/downloads.rs @@ -4,8 +4,8 @@ use std::sync::mpsc::Sender; use sanitize_filename::{sanitize_with_options, Options}; -use crate::types::Message; use crate::threadpool::Threadpool; +use crate::types::Message; /// Enum used for communicating back to the main controller upon /// successful or unsuccessful downloading of a file. i32 value @@ -32,7 +32,13 @@ pub struct EpData { /// files to download. It uses the threadpool to start jobs /// for every episode to be downloaded. New jobs can be requested /// by the user while there are still ongoing jobs. -pub fn download_list(episodes: Vec, dest: &PathBuf, max_retries: usize, threadpool: &Threadpool, tx_to_main: Sender) { +pub fn download_list( + episodes: Vec, + dest: &PathBuf, + max_retries: usize, + threadpool: &Threadpool, + tx_to_main: Sender, +) { // parse episode details and push to queue for ep in episodes.into_iter() { let tx = tx_to_main.clone(); @@ -44,7 +50,6 @@ pub fn download_list(episodes: Vec, dest: &PathBuf, max_retries: usize, } } - /// Downloads a file to a local filepath, returning DownloadMsg variant /// indicating success or failure. fn download_file(ep_data: EpData, dest: PathBuf, mut max_retries: usize) -> DownloadMsg { @@ -77,18 +82,21 @@ fn download_file(ep_data: EpData, dest: PathBuf, mut max_retries: usize) -> Down Some("video/quicktime") => "mov", Some("video/mp4") => "mp4", Some("video/x-m4v") => "m4v", - _ => "mp3" // assume .mp3 unless we figure out otherwise + _ => "mp3", // assume .mp3 unless we figure out otherwise }; - - let file_name = sanitize_with_options(&ep_data.title, Options { - truncate: true, - windows: true, // for simplicity, we'll just use Windows-friendly paths for everyone - replacement: "" - }); - + + let file_name = sanitize_with_options( + &ep_data.title, + Options { + truncate: true, + windows: true, // for simplicity, we'll just use Windows-friendly paths for everyone + replacement: "", + }, + ); + let mut file_path = dest; file_path.push(format!("{}.{}", file_name, ext)); - + let dst = File::create(&file_path); if dst.is_err() { return DownloadMsg::FileCreateError(data); @@ -101,4 +109,4 @@ fn download_file(ep_data: EpData, dest: PathBuf, mut max_retries: usize) -> Down Ok(_) => DownloadMsg::Complete(data), Err(_) => DownloadMsg::FileWriteError(data), }; -} \ No newline at end of file +} diff --git a/src/feeds.rs b/src/feeds.rs index e1c31b6..83ecb99 100644 --- a/src/feeds.rs +++ b/src/feeds.rs @@ -1,14 +1,14 @@ -use std::sync::mpsc; use std::io::Read; +use std::sync::mpsc; -use rss::{Channel, Item}; -use chrono::{DateTime, Utc}; use crate::sanitizer::parse_from_rfc2822_with_fallback; +use chrono::{DateTime, Utc}; use lazy_static::lazy_static; -use regex::{Regex, Match}; +use regex::{Match, Regex}; +use rss::{Channel, Item}; -use crate::types::*; use crate::threadpool::Threadpool; +use crate::types::*; lazy_static! { /// Regex for parsing an episode "duration", which could take the form @@ -39,33 +39,41 @@ impl PodcastFeed { return Self { id: id, url: url, - title: title + title: title, }; } } /// Spawns a new thread to check a feed and retrieve podcast data. -pub fn check_feed(feed: PodcastFeed, max_retries: usize, threadpool: &Threadpool, tx_to_main: mpsc::Sender) { - threadpool.execute(move || { - match get_feed_data(feed.url.clone(), max_retries) { - Ok(pod) => { - match feed.id { - Some(id) => { - tx_to_main.send( - Message::Feed(FeedMsg::SyncData((id, pod)))).unwrap(); - }, - None => tx_to_main.send( - Message::Feed(FeedMsg::NewData(pod))).unwrap(), - } - }, - Err(_err) => tx_to_main.send(Message::Feed(FeedMsg::Error(feed))).unwrap(), - } +pub fn check_feed( + feed: PodcastFeed, + max_retries: usize, + threadpool: &Threadpool, + tx_to_main: mpsc::Sender, +) { + threadpool.execute(move || match get_feed_data(feed.url.clone(), max_retries) { + Ok(pod) => match feed.id { + Some(id) => { + tx_to_main + .send(Message::Feed(FeedMsg::SyncData((id, pod)))) + .unwrap(); + } + None => tx_to_main + .send(Message::Feed(FeedMsg::NewData(pod))) + .unwrap(), + }, + Err(_err) => tx_to_main + .send(Message::Feed(FeedMsg::Error(feed))) + .unwrap(), }); } /// Given a URL, this attempts to pull the data about a podcast and its /// episodes from an RSS feed. -fn get_feed_data(url: String, mut max_retries: usize) -> Result> { +fn get_feed_data( + url: String, + mut max_retries: usize, +) -> Result> { let request: Result> = loop { let response = ureq::get(&url) .timeout_connect(5000) @@ -89,12 +97,11 @@ fn get_feed_data(url: String, mut max_retries: usize) -> Result Err(err), - } + }; } - /// Given a Channel with the RSS feed data, this parses the data about a /// podcast and its episodes and returns a Podcast. There are existing /// specifications for podcast RSS feeds that a feed should adhere to, but @@ -122,7 +129,7 @@ fn parse_feed_data(channel: Channel, url: &str) -> PodcastNoId { "no" | "clean" | "false" => Some(false), _ => None, } - }, + } }; } @@ -166,13 +173,13 @@ fn parse_episode_data(item: &Item) -> EpisodeNoId { let pubdate = match item.pub_date() { Some(pd) => match parse_from_rfc2822_with_fallback(pd) { Ok(date) => { - // this is a bit ridiculous, but it seems like + // this is a bit ridiculous, but it seems like // you have to convert from a DateTime // to a NaiveDateTime, and then from there create // a DateTime; see // https://github.com/chronotope/chrono/issues/169#issue-239433186 Some(DateTime::from_utc(date.naive_utc(), Utc)) - }, + } Err(_) => None, }, None => None, @@ -238,31 +245,29 @@ fn duration_to_int(duration: Option<&str>) -> Option { 3 => { let result: Result, _> = times.into_iter().collect(); match result { - Ok(v) => Some(v[0]*60*60 + v[1]*60 + v[2]), + Ok(v) => Some(v[0] * 60 * 60 + v[1] * 60 + v[2]), Err(_) => None, } - }, + } // MM:SS 2 => { let result: Result, _> = times.into_iter().collect(); match result { - Ok(v) => Some(v[0]*60 + v[1]), + Ok(v) => Some(v[0] * 60 + v[1]), Err(_) => None, } - }, + } // SS - 1 => { - match times[0] { - Ok(i) => Some(i), - Err(_) => None, - } + 1 => match times[0] { + Ok(i) => Some(i), + Err(_) => None, }, _ => None, } - }, + } None => None, } - }, + } None => None, } } @@ -274,7 +279,6 @@ fn regex_to_int(re_match: Match) -> Result { mstr.parse::() } - // TESTS ----------------------------------------------------------------- #[cfg(test)] mod tests { @@ -375,4 +379,4 @@ mod tests { let duration = String::from("8"); assert_eq!(duration_to_int(Some(&duration)), Some(8)); } -} \ No newline at end of file +} diff --git a/src/keymap.rs b/src/keymap.rs index 66290c3..b849c65 100644 --- a/src/keymap.rs +++ b/src/keymap.rs @@ -1,5 +1,5 @@ -use std::collections::HashMap; use pancurses::Input; +use std::collections::HashMap; /// Enum delineating all actions that may be performed by the user, and /// thus have keybindings associated with them. @@ -48,9 +48,7 @@ impl Keybindings { /// user action, if one exists. pub fn get_from_input(&self, input: Input) -> Option<&UserAction> { match input_to_str(input) { - Some(code) => { - self.map.get(&code) - }, + Some(code) => self.map.get(&code), None => None, } } @@ -71,7 +69,9 @@ impl Keybindings { } pub fn keys_for_action(&self, action: UserAction) -> Vec { - return self.map.iter() + return self + .map + .iter() .filter_map(|(key, &val)| { if val == action { Some(key.clone()) @@ -110,7 +110,7 @@ pub fn input_to_str(input: Input) -> Option { Input::KeyF8 => "F8", Input::KeyF9 => "F9", Input::KeyF10 => "F10", - Input::KeyF11 => "F11", // F11 triggers KeyResize for me + Input::KeyF11 => "F11", // F11 triggers KeyResize for me Input::KeyF12 => "F12", Input::KeyF13 => "F13", Input::KeyF14 => "F14", @@ -127,7 +127,7 @@ pub fn input_to_str(input: Input) -> Option { Input::KeySR => "S_Up", Input::KeyNPage => "PgDn", Input::KeyPPage => "PgUp", - Input::KeySTab => "STab", // this doesn't appear to be Shift+Tab + Input::KeySTab => "STab", // this doesn't appear to be Shift+Tab Input::KeyCTab => "C_Tab", Input::KeyCATab => "CATab", Input::KeyEnter => "Enter", @@ -138,7 +138,7 @@ pub fn input_to_str(input: Input) -> Option { Input::KeyAbort => "Abort", Input::KeySHelp => "SHelp", Input::KeyLHelp => "LHelp", - Input::KeyBTab => "S_Tab", // Shift+Tab + Input::KeyBTab => "S_Tab", // Shift+Tab Input::KeyBeg => "Beg", Input::KeyCancel => "Cancel", Input::KeyClose => "Close", @@ -211,12 +211,12 @@ pub fn input_to_str(input: Input) -> Option { } else { c.encode_utf8(&mut tmp) } - }, - _ => "" + } + _ => "", }; if code == "" { return None; } else { return Some(code.to_string()); } -} \ No newline at end of file +} diff --git a/src/main.rs b/src/main.rs index 882e713..311c9e1 100644 --- a/src/main.rs +++ b/src/main.rs @@ -1,57 +1,57 @@ -use std::process; -use std::path::PathBuf; -use std::sync::mpsc; use std::fs::File; use std::io::{Read, Write}; +use std::path::PathBuf; +use std::process; +use std::sync::mpsc; use clap::{App, Arg, SubCommand}; -mod main_controller; mod config; -mod keymap; mod db; -mod ui; -mod types; -mod threadpool; -mod feeds; -mod sanitizer; mod downloads; -mod play_file; +mod feeds; +mod keymap; +mod main_controller; mod opml; +mod play_file; +mod sanitizer; +mod threadpool; +mod types; +mod ui; -use crate::main_controller::{MainController, MainMessage}; use crate::config::Config; use crate::db::Database; +use crate::feeds::{FeedMsg, PodcastFeed}; +use crate::main_controller::{MainController, MainMessage}; use crate::threadpool::Threadpool; use crate::types::*; -use crate::feeds::{FeedMsg, PodcastFeed}; const VERSION: &str = env!("CARGO_PKG_VERSION"); /// Main controller for shellcaster program. -/// +/// /// *Main command:* -/// Setup involves connecting to the sqlite database (creating it if +/// Setup involves connecting to the sqlite database (creating it if /// necessary), then querying the list of podcasts and episodes. This /// is then passed off to the UI, which instantiates the menus displaying /// the podcast info. -/// +/// /// After this, the program enters a loop that listens for user keyboard /// input, and dispatches to the proper module as necessary. User input /// to quit the program breaks the loop, tears down the UI, and ends the /// program. -/// +/// /// *Sync subcommand:* /// Connects to the sqlite database, then initiates a full sync of all /// podcasts. No UI is created for this, as the intention is to be used /// in a programmatic way (e.g., setting up a cron job to sync /// regularly.) -/// +/// /// *Import subcommand:* /// Reads in an OPML file and adds feeds to the database that do not /// already exist. If the `-r` option is used, the database is wiped /// first. -/// +/// /// *Export subcommand:* /// Connects to the sqlite database, and reads all podcasts into an OPML /// file, with the location specified from the command line arguments. @@ -122,41 +122,39 @@ fn main() { process::exit(1); } - match args.subcommand() { // SYNC SUBCOMMAND ---------------------------------------------- ("sync", Some(sub_args)) => { sync_podcasts(&db_path, config, sub_args); - }, + } // IMPORT SUBCOMMAND -------------------------------------------- ("import", Some(sub_args)) => { import(&db_path, config, sub_args); - }, + } // EXPORT SUBCOMMAND -------------------------------------------- ("export", Some(sub_args)) => { export(&db_path, sub_args); - }, + } // MAIN COMMAND ------------------------------------------------- _ => { let mut main_ctrl = MainController::new(config, &db_path); - main_ctrl.loop_msgs(); // main loop + main_ctrl.loop_msgs(); // main loop main_ctrl.tx_to_ui.send(MainMessage::UiTearDown).unwrap(); - main_ctrl.ui_thread.join().unwrap(); // wait for UI thread to finish teardown + main_ctrl.ui_thread.join().unwrap(); // wait for UI thread to finish teardown } } } - /// Gets the path to the config file if one is specified in the command- /// line arguments, or else returns the default config path for the /// user's operating system. /// Returns None if default OS config directory cannot be determined. -/// +/// /// Note: Right now we only have one possible command-line argument, /// specifying a config path. If the command-line API is /// extended in the future, this will have to be refactored. @@ -170,14 +168,13 @@ fn get_config_path(config: Option<&str>) -> Option { path.push("shellcaster"); path.push("config.toml"); Some(path) - }, + } None => None, - } - }, + } + } }; } - /// Synchronizes RSS feed data for all podcasts, without setting up a UI. fn sync_podcasts(db_path: &PathBuf, config: Config, args: &clap::ArgMatches) { let db_inst = Database::connect(db_path); @@ -193,8 +190,7 @@ fn sync_podcasts(db_path: &PathBuf, config: Config, args: &clap::ArgMatches) { for pod in podcast_list.iter() { let feed = PodcastFeed::new(Some(pod.id), pod.url.clone(), Some(pod.title.clone())); - feeds::check_feed(feed, config.max_retries, &threadpool, - tx_to_main.clone()); + feeds::check_feed(feed, config.max_retries, &threadpool, tx_to_main.clone()); } let mut msg_counter: usize = 0; @@ -204,18 +200,18 @@ fn sync_podcasts(db_path: &PathBuf, config: Config, args: &clap::ArgMatches) { Message::Feed(FeedMsg::SyncData((pod_id, pod))) => { let title = pod.title.clone(); let db_result; - + db_result = db_inst.update_podcast(pod_id, pod); match db_result { Ok(_) => { if !args.is_present("quiet") { println!("Synced {}", title); } - }, + } Err(_err) => { failure = true; eprintln!("Error synchronizing {}", title); - }, + } } } @@ -244,7 +240,6 @@ fn sync_podcasts(db_path: &PathBuf, config: Config, args: &clap::ArgMatches) { } } - /// Imports a list of podcasts from OPML format, either reading from a /// file or from stdin. If the `replace` flag is set, this replaces all /// existing data in the database. @@ -265,12 +260,14 @@ fn import(db_path: &PathBuf, config: Config, args: &clap::ArgMatches) { } None => { let mut contents = String::new(); - std::io::stdin().read_to_string(&mut contents).unwrap_or_else(|err| { - eprintln!("Error reading from stdin: {}", err); - process::exit(5); - }); + std::io::stdin() + .read_to_string(&mut contents) + .unwrap_or_else(|err| { + eprintln!("Error reading from stdin: {}", err); + process::exit(5); + }); contents - }, + } }; let mut podcast_list = opml::import(xml).unwrap_or_else(|err| { @@ -295,14 +292,17 @@ fn import(db_path: &PathBuf, config: Config, args: &clap::ArgMatches) { let old_podcasts = db_inst.get_podcasts(); // if URL is already in database, remove it from import - podcast_list = podcast_list.into_iter().filter(|pod| { - for op in &old_podcasts { - if pod.url == op.url { - return false; + podcast_list = podcast_list + .into_iter() + .filter(|pod| { + for op in &old_podcasts { + if pod.url == op.url { + return false; + } } - } - return true; - }).collect(); + return true; + }) + .collect(); } if podcast_list.is_empty() { @@ -316,7 +316,12 @@ fn import(db_path: &PathBuf, config: Config, args: &clap::ArgMatches) { let (tx_to_main, rx_to_main) = mpsc::channel(); for pod in podcast_list.iter() { - feeds::check_feed(pod.clone(), config.max_retries, &threadpool, tx_to_main.clone()); + feeds::check_feed( + pod.clone(), + config.max_retries, + &threadpool, + tx_to_main.clone(), + ); } let mut msg_counter: usize = 0; @@ -326,18 +331,18 @@ fn import(db_path: &PathBuf, config: Config, args: &clap::ArgMatches) { Message::Feed(FeedMsg::NewData(pod)) => { let title = pod.title.clone(); let db_result; - + db_result = db_inst.insert_podcast(pod); match db_result { Ok(_) => { if !args.is_present("quiet") { println!("Added {}", title); } - }, + } Err(_err) => { failure = true; eprintln!("Error adding {}", title); - }, + } } } @@ -368,7 +373,6 @@ fn import(db_path: &PathBuf, config: Config, args: &clap::ArgMatches) { } } - /// Exports all podcasts to OPML format, either printing to stdout or /// exporting to a file. fn export(db_path: &PathBuf, args: &clap::ArgMatches) { @@ -392,8 +396,8 @@ fn export(db_path: &PathBuf, args: &clap::ArgMatches) { eprintln!("Error copying OPML data to output file: {}", err); process::exit(4); }); - }, + } // print to stdout None => println!("{}", xml), } -} \ No newline at end of file +} diff --git a/src/main_controller.rs b/src/main_controller.rs index 45716b5..77b15f5 100644 --- a/src/main_controller.rs +++ b/src/main_controller.rs @@ -1,18 +1,18 @@ +use std::collections::HashSet; +use std::fs; use std::path::PathBuf; use std::sync::mpsc; -use std::fs; -use std::collections::HashSet; use sanitize_filename::{sanitize_with_options, Options}; -use crate::types::*; use crate::config::Config; -use crate::ui::{UI, UiMsg}; use crate::db::{Database, SyncResult}; -use crate::threadpool::Threadpool; +use crate::downloads::{self, DownloadMsg, EpData}; use crate::feeds::{self, FeedMsg, PodcastFeed}; -use crate::downloads::{self, EpData, DownloadMsg}; use crate::play_file; +use crate::threadpool::Threadpool; +use crate::types::*; +use crate::ui::{UiMsg, UI}; /// Enum used for communicating with other threads. #[derive(Debug)] @@ -48,24 +48,29 @@ impl MainController { // create transmitters and receivers for passing messages between threads let (tx_to_ui, rx_from_main) = mpsc::channel(); let (tx_to_main, rx_to_main) = mpsc::channel(); - + // get connection to the database let db_inst = Database::connect(&db_path); // set up threadpool let threadpool = Threadpool::new(config.simultaneous_downloads); - + // create vector of podcasts, where references are checked at // runtime; this is necessary because we want main.rs to hold the // "ground truth" list of podcasts, and it must be mutable, but // UI needs to check this list and update the screen when // necessary let podcast_list = LockVec::new(db_inst.get_podcasts()); - + // set up UI in new thread let tx_ui_to_main = mpsc::Sender::clone(&tx_to_main); - let ui_thread = UI::spawn(config.clone(), podcast_list.clone(), rx_from_main, tx_ui_to_main); - // TODO: Can we do this without cloning the config? + let ui_thread = UI::spawn( + config.clone(), + podcast_list.clone(), + rx_from_main, + tx_ui_to_main, + ); + // TODO: Can we do this without cloning the config? return MainController { config: config, @@ -87,69 +92,66 @@ impl MainController { while let Some(message) = self.rx_to_main.iter().next() { match message { Message::Ui(UiMsg::Quit) => break, - - Message::Ui(UiMsg::AddFeed(url)) => - self.add_podcast(url), - - Message::Feed(FeedMsg::NewData(pod)) => - self.add_or_sync_data(pod, None), - - Message::Feed(FeedMsg::Error(feed)) => { - match feed.title { - Some(t) => self.notif_to_ui(format!("Error retrieving RSS feed for {}.", t), true), - None => self.notif_to_ui("Error retrieving RSS feed.".to_string(), true), + + Message::Ui(UiMsg::AddFeed(url)) => self.add_podcast(url), + + Message::Feed(FeedMsg::NewData(pod)) => self.add_or_sync_data(pod, None), + + Message::Feed(FeedMsg::Error(feed)) => match feed.title { + Some(t) => { + self.notif_to_ui(format!("Error retrieving RSS feed for {}.", t), true) } + None => self.notif_to_ui("Error retrieving RSS feed.".to_string(), true), }, - - Message::Ui(UiMsg::Sync(pod_id)) => - self.sync(Some(pod_id)), - - Message::Feed(FeedMsg::SyncData((id, pod))) => - self.add_or_sync_data(pod, Some(id)), - - Message::Ui(UiMsg::SyncAll) => - self.sync(None), - - Message::Ui(UiMsg::Play(pod_id, ep_id)) => - self.play_file(pod_id, ep_id), - - Message::Ui(UiMsg::MarkPlayed(pod_id, ep_id, played)) => - self.mark_played(pod_id, ep_id, played), - - Message::Ui(UiMsg::MarkAllPlayed(pod_id, played)) => - self.mark_all_played(pod_id, played), - - Message::Ui(UiMsg::Download(pod_id, ep_id)) => - self.download(pod_id, Some(ep_id)), - - Message::Ui(UiMsg::DownloadAll(pod_id)) => - self.download(pod_id, None), - + + Message::Ui(UiMsg::Sync(pod_id)) => self.sync(Some(pod_id)), + + Message::Feed(FeedMsg::SyncData((id, pod))) => self.add_or_sync_data(pod, Some(id)), + + Message::Ui(UiMsg::SyncAll) => self.sync(None), + + Message::Ui(UiMsg::Play(pod_id, ep_id)) => self.play_file(pod_id, ep_id), + + Message::Ui(UiMsg::MarkPlayed(pod_id, ep_id, played)) => { + self.mark_played(pod_id, ep_id, played) + } + + Message::Ui(UiMsg::MarkAllPlayed(pod_id, played)) => { + self.mark_all_played(pod_id, played) + } + + Message::Ui(UiMsg::Download(pod_id, ep_id)) => self.download(pod_id, Some(ep_id)), + + Message::Ui(UiMsg::DownloadAll(pod_id)) => self.download(pod_id, None), + // downloading can produce any one of these responses - Message::Dl(DownloadMsg::Complete(ep_data)) => - self.download_complete(ep_data), - Message::Dl(DownloadMsg::ResponseError(_)) => - self.notif_to_ui("Error sending download request.".to_string(), true), - Message::Dl(DownloadMsg::FileCreateError(_)) => - self.notif_to_ui("Error creating file.".to_string(), true), - Message::Dl(DownloadMsg::FileWriteError(_)) => - self.notif_to_ui("Error downloading episode.".to_string(), true), - - Message::Ui(UiMsg::Delete(pod_id, ep_id)) => - self.delete_file(pod_id, ep_id), - - Message::Ui(UiMsg::DeleteAll(pod_id)) => - self.delete_files(pod_id), - - Message::Ui(UiMsg::RemovePodcast(pod_id, delete_files)) => - self.remove_podcast(pod_id, delete_files), - - Message::Ui(UiMsg::RemoveEpisode(pod_id, ep_id, delete_files)) => - self.remove_episode(pod_id, ep_id, delete_files), - - Message::Ui(UiMsg::RemoveAllEpisodes(pod_id, delete_files)) => - self.remove_all_episodes(pod_id, delete_files), - + Message::Dl(DownloadMsg::Complete(ep_data)) => self.download_complete(ep_data), + Message::Dl(DownloadMsg::ResponseError(_)) => { + self.notif_to_ui("Error sending download request.".to_string(), true) + } + Message::Dl(DownloadMsg::FileCreateError(_)) => { + self.notif_to_ui("Error creating file.".to_string(), true) + } + Message::Dl(DownloadMsg::FileWriteError(_)) => { + self.notif_to_ui("Error downloading episode.".to_string(), true) + } + + Message::Ui(UiMsg::Delete(pod_id, ep_id)) => self.delete_file(pod_id, ep_id), + + Message::Ui(UiMsg::DeleteAll(pod_id)) => self.delete_files(pod_id), + + Message::Ui(UiMsg::RemovePodcast(pod_id, delete_files)) => { + self.remove_podcast(pod_id, delete_files) + } + + Message::Ui(UiMsg::RemoveEpisode(pod_id, ep_id, delete_files)) => { + self.remove_episode(pod_id, ep_id, delete_files) + } + + Message::Ui(UiMsg::RemoveAllEpisodes(pod_id, delete_files)) => { + self.remove_all_episodes(pod_id, delete_files) + } + Message::Ui(UiMsg::Noop) => (), } } @@ -158,20 +160,28 @@ impl MainController { /// Sends the specified notification to the UI, which will display at /// the bottom of the screen. pub fn notif_to_ui(&self, message: String, error: bool) { - self.tx_to_ui.send(MainMessage::UiSpawnNotif( - message, error, crate::config::MESSAGE_TIME)).unwrap(); + self.tx_to_ui + .send(MainMessage::UiSpawnNotif( + message, + error, + crate::config::MESSAGE_TIME, + )) + .unwrap(); } /// Sends a persistent notification to the UI, which will display at /// the bottom of the screen until cleared. pub fn persistent_notif_to_ui(&self, message: String, error: bool) { - self.tx_to_ui.send(MainMessage::UiSpawnPersistentNotif( - message, error)).unwrap(); + self.tx_to_ui + .send(MainMessage::UiSpawnPersistentNotif(message, error)) + .unwrap(); } /// Clears persistent notifications in the UI. pub fn clear_persistent_notif(&self) { - self.tx_to_ui.send(MainMessage::UiClearPersistentNotif).unwrap(); + self.tx_to_ui + .send(MainMessage::UiClearPersistentNotif) + .unwrap(); } /// Updates the persistent notification about syncing podcasts and @@ -183,7 +193,10 @@ impl MainController { let dl_plural = if dl_len > 1 { "s" } else { "" }; if sync_len > 0 && dl_len > 0 { - let notif = format!("Syncing {} podcast{}, downloading {} episode{}...", sync_len, sync_plural, dl_len, dl_plural); + let notif = format!( + "Syncing {} podcast{}, downloading {} episode{}...", + sync_len, sync_plural, dl_len, dl_plural + ); self.persistent_notif_to_ui(notif, false); } else if sync_len > 0 { let notif = format!("Syncing {} podcast{}...", sync_len, sync_plural); @@ -199,9 +212,13 @@ impl MainController { /// Add a new podcast by fetching the RSS feed data. pub fn add_podcast(&self, url: String) { let feed = PodcastFeed::new(None, url, None); - feeds::check_feed(feed, self.config.max_retries, - &self.threadpool, self.tx_to_main.clone()); - } + feeds::check_feed( + feed, + self.config.max_retries, + &self.threadpool, + self.tx_to_main.clone(), + ); + } /// Synchronize RSS feed data for one or more podcasts. pub fn sync(&mut self, pod_id: Option) { @@ -213,18 +230,28 @@ impl MainController { let mut pod_data = Vec::new(); match pod_id { // just grab one podcast - Some(id) => pod_data.push(self.podcasts - .map_single(id, - |pod| PodcastFeed::new(Some(pod.id), pod.url.clone(), Some(pod.title.clone()))) - .unwrap()), + Some(id) => pod_data.push( + self.podcasts + .map_single(id, |pod| { + PodcastFeed::new(Some(pod.id), pod.url.clone(), Some(pod.title.clone())) + }) + .unwrap(), + ), // get all of 'em! - None => pod_data = self.podcasts - .map(|pod| PodcastFeed::new(Some(pod.id), pod.url.clone(), Some(pod.title.clone()))), + None => { + pod_data = self.podcasts.map(|pod| { + PodcastFeed::new(Some(pod.id), pod.url.clone(), Some(pod.title.clone())) + }) + } } for feed in pod_data.into_iter() { self.sync_counter += 1; - feeds::check_feed(feed, self.config.max_retries, - &self.threadpool, self.tx_to_main.clone()) + feeds::check_feed( + feed, + self.config.max_retries, + &self.threadpool, + self.tx_to_main.clone(), + ) } self.update_tracker_notif(); } @@ -268,12 +295,21 @@ impl MainController { updated += res.updated.len(); } self.sync_tracker = Vec::new(); - self.notif_to_ui(format!("Sync complete: Added {}, updated {} episodes.", added, updated), false); + self.notif_to_ui( + format!( + "Sync complete: Added {}, updated {} episodes.", + added, updated + ), + false, + ); } } else { - self.notif_to_ui(format!("Successfully added {} episodes.", result.added.len()), false); + self.notif_to_ui( + format!("Successfully added {} episodes.", result.added.len()), + false, + ); } - }, + } Err(_err) => self.notif_to_ui(failure, true), } } @@ -282,28 +318,25 @@ impl MainController { /// episode. pub fn play_file(&self, pod_id: i64, ep_id: i64) { self.mark_played(pod_id, ep_id, true); - let episode = self.podcasts - .clone_episode(pod_id, ep_id).unwrap(); + let episode = self.podcasts.clone_episode(pod_id, ep_id).unwrap(); match episode.path { // if there is a local file, try to play that - Some(path) => { - match path.to_str() { - Some(p) => { - if play_file::execute(&self.config.play_command, &p).is_err() { - self.notif_to_ui( - "Error: Could not play file. Check configuration.".to_string(), true); - } - }, - None => self.notif_to_ui( - "Error: Filepath is not valid Unicode.".to_string(), true), + Some(path) => match path.to_str() { + Some(p) => { + if play_file::execute(&self.config.play_command, &p).is_err() { + self.notif_to_ui( + "Error: Could not play file. Check configuration.".to_string(), + true, + ); + } } + None => self.notif_to_ui("Error: Filepath is not valid Unicode.".to_string(), true), }, // otherwise, try to stream the URL None => { if play_file::execute(&self.config.play_command, &episode.url).is_err() { - self.notif_to_ui( - "Error: Could not stream URL.".to_string(),true); + self.notif_to_ui("Error: Could not stream URL.".to_string(), true); } } } @@ -319,7 +352,7 @@ impl MainController { // to clone the episode... let mut episode = podcast.episodes.clone_episode(ep_id).unwrap(); episode.played = played; - + self.db.set_played_status(episode.id, played); podcast.episodes.replace(ep_id, episode); @@ -333,13 +366,14 @@ impl MainController { pub fn mark_all_played(&self, pod_id: i64, played: bool) { let podcast = self.podcasts.clone_podcast(pod_id).unwrap(); { - let borrowed_ep_list = podcast - .episodes.borrow_order(); + let borrowed_ep_list = podcast.episodes.borrow_order(); for ep in borrowed_ep_list.iter() { self.db.set_played_status(ep.clone(), played); } } - podcast.episodes.replace_all(self.db.get_episodes(podcast.id)); + podcast + .episodes + .replace_all(self.db.get_episodes(podcast.id)); self.podcasts.replace(pod_id, podcast); self.tx_to_ui.send(MainMessage::UiUpdateMenus).unwrap(); @@ -362,22 +396,29 @@ impl MainController { match ep_id { Some(ep_id) => { // grab just the relevant data we need - let data = podcast.episodes.map_single(ep_id, - |ep| (EpData { - id: ep.id, - pod_id: ep.pod_id, - title: ep.title.clone(), - url: ep.url.clone(), - file_path: None, - }, ep.path.is_none())).unwrap(); + let data = podcast + .episodes + .map_single(ep_id, |ep| { + ( + EpData { + id: ep.id, + pod_id: ep.pod_id, + title: ep.title.clone(), + url: ep.url.clone(), + file_path: None, + }, + ep.path.is_none(), + ) + }) + .unwrap(); if data.1 { ep_data.push(data.0); } - }, + } None => { // grab just the relevant data we need - ep_data = podcast.episodes - .filter_map(|ep| if ep.path.is_none() { + ep_data = podcast.episodes.filter_map(|ep| { + if ep.path.is_none() { Some(EpData { id: ep.id, pod_id: ep.pod_id, @@ -387,35 +428,40 @@ impl MainController { }) } else { None - }); + } + }); } } } // check against episodes currently being downloaded -- so we // don't needlessly download them again - ep_data.retain(|ep| { - !self.download_tracker.contains(&ep.id) - }); + ep_data.retain(|ep| !self.download_tracker.contains(&ep.id)); if !ep_data.is_empty() { // add directory for podcast, create if it does not exist - let dir_name = sanitize_with_options(&pod_title, Options { - truncate: true, - windows: true, // for simplicity, we'll just use Windows-friendly paths for everyone - replacement: "" - }); + let dir_name = sanitize_with_options( + &pod_title, + Options { + truncate: true, + windows: true, // for simplicity, we'll just use Windows-friendly paths for everyone + replacement: "", + }, + ); match self.create_podcast_dir(dir_name) { Ok(path) => { for ep in ep_data.iter() { self.download_tracker.insert(ep.id); } downloads::download_list( - ep_data, &path, self.config.max_retries, - &self.threadpool, self.tx_to_main.clone()); - }, - Err(_) => self.notif_to_ui( - format!("Could not create dir: {}", pod_title), true), + ep_data, + &path, + self.config.max_retries, + &self.threadpool, + self.tx_to_main.clone(), + ); + } + Err(_) => self.notif_to_ui(format!("Could not create dir: {}", pod_title), true), } self.update_tracker_notif(); } @@ -427,8 +473,7 @@ impl MainController { let _ = self.db.insert_file(ep_data.id, &file_path); { // TODO: Try to do this without cloning the podcast... - let podcast = self.podcasts - .clone_podcast(ep_data.pod_id).unwrap(); + let podcast = self.podcasts.clone_podcast(ep_data.pod_id).unwrap(); let mut episode = podcast.episodes.clone_episode(ep_data.id).unwrap(); episode.path = Some(file_path); podcast.episodes.replace(ep_data.id, episode); @@ -451,7 +496,7 @@ impl MainController { return match std::fs::create_dir_all(&download_path) { Ok(_) => Ok(download_path), Err(err) => Err(err), - } + }; } /// Deletes a downloaded file for an episode from the user's local @@ -470,11 +515,9 @@ impl MainController { podcast.episodes.replace(ep_id, episode); self.tx_to_ui.send(MainMessage::UiUpdateMenus).unwrap(); - self.notif_to_ui( - format!("Deleted \"{}\"", title), false); - }, - Err(_) => self.notif_to_ui( - format!("Error deleting \"{}\"", title), true), + self.notif_to_ui(format!("Deleted \"{}\"", title), false); + } + Err(_) => self.notif_to_ui(format!("Error deleting \"{}\"", title), true), } } } @@ -497,7 +540,7 @@ impl MainController { eps_to_remove.push(episode.id); episode.path = None; *ep = episode; - }, + } Err(_) => success = false, } } @@ -508,11 +551,9 @@ impl MainController { self.tx_to_ui.send(MainMessage::UiUpdateMenus).unwrap(); if success { - self.notif_to_ui( - "Files successfully deleted.".to_string(), false); + self.notif_to_ui("Files successfully deleted.".to_string(), false); } else { - self.notif_to_ui( - "Error while deleting files".to_string(), true); + self.notif_to_ui("Error while deleting files".to_string(), true); } } @@ -523,8 +564,7 @@ impl MainController { self.delete_files(pod_id); } - let pod_id = self.podcasts - .map_single(pod_id, |pod| pod.id).unwrap(); + let pod_id = self.podcasts.map_single(pod_id, |pod| pod.id).unwrap(); self.db.remove_podcast(pod_id); { self.podcasts.replace_all(self.db.get_podcasts()); @@ -564,4 +604,4 @@ impl MainController { self.tx_to_ui.send(MainMessage::UiUpdateMenus).unwrap(); } -} \ No newline at end of file +} diff --git a/src/opml.rs b/src/opml.rs index 96f59f2..3d3275d 100644 --- a/src/opml.rs +++ b/src/opml.rs @@ -1,8 +1,8 @@ -use opml::{OPML, Head, Body, Outline}; use chrono::Utc; +use opml::{Body, Head, Outline, OPML}; -use crate::types::*; use crate::feeds::PodcastFeed; +use crate::types::*; pub fn import(xml: String) -> Result, String> { return match OPML::new(&xml) { @@ -21,7 +21,7 @@ pub fn import(xml: String) -> Result, String> { } else { Some(t.clone()) } - }, + } None => None, }; let title = match temp_title { @@ -46,9 +46,9 @@ pub fn export(podcasts: Vec) -> OPML { let date = Utc::now(); let mut opml = OPML::default(); opml.head = Some(Head { - title: Some("Shellcaster Podcast Feeds".to_string()), - date_created: Some(date.to_rfc2822()), - ..Head::default() + title: Some("Shellcaster Podcast Feeds".to_string()), + date_created: Some(date.to_rfc2822()), + ..Head::default() }); let mut outlines = Vec::new(); @@ -64,8 +64,6 @@ pub fn export(podcasts: Vec) -> OPML { }); } - opml.body = Body { - outlines: outlines, - }; + opml.body = Body { outlines: outlines }; return opml; -} \ No newline at end of file +} diff --git a/src/play_file.rs b/src/play_file.rs index ac33dc4..97fc27d 100644 --- a/src/play_file.rs +++ b/src/play_file.rs @@ -12,24 +12,24 @@ pub fn execute(command: &str, path: &str) -> Result<(), std::io::Error> { let mut args: Vec; if cmd_string.contains("%s") { - args = args_iter.map(|a| { - if a == "%s" { - return a.replace("%s", path); - } else { - return a.to_string(); - } - }).collect(); + args = args_iter + .map(|a| { + if a == "%s" { + return a.replace("%s", path); + } else { + return a.to_string(); + } + }) + .collect(); } else { args = args_iter.map(|a| a.to_string()).collect(); args.push(path.to_string()); } let mut cmd = Command::new(base_cmd); - cmd.args(args) - .stdout(Stdio::null()) - .stderr(Stdio::null()); + cmd.args(args).stdout(Stdio::null()).stderr(Stdio::null()); match cmd.spawn() { Ok(_) => Ok(()), Err(err) => Err(err), } -} \ No newline at end of file +} diff --git a/src/sanitizer.rs b/src/sanitizer.rs index 4962717..5546278 100644 --- a/src/sanitizer.rs +++ b/src/sanitizer.rs @@ -3,14 +3,13 @@ /// which appears to be unmaintained and which has out-of-date /// dependencies. Given that the library is only ~150 lines of code, I /// opted to include it directly in my crate to update the dependencies. -/// +/// /// All credit goes to the original authors; see here for the GPLv3 /// license: https://gitlab.com/alatiera/rfc822_sanitizer/-/blob/master/LICENSE - use chrono::{DateTime, FixedOffset, ParseResult}; -use std::borrow::Cow; use lazy_static::lazy_static; use regex::Regex; +use std::borrow::Cow; /// Tries to fix common ways date generators misshandle rfc822/rfc2822. /// @@ -970,12 +969,10 @@ mod tests { fn test_pad_zeros() { // Would be nice If we had more test cases, // If you stumble(d) upon any online please consider opening a Pullrequest. - let foo = vec![ - ( - "Thu, 30 Aug 2017 1:30:00 PDT", - "Thu, 30 Aug 2017 01:30:00 PDT", - ), - ]; + let foo = vec![( + "Thu, 30 Aug 2017 1:30:00 PDT", + "Thu, 30 Aug 2017 01:30:00 PDT", + )]; foo.iter() .for_each(|&(bad, good)| assert_eq!(pad_zeros(bad.to_string()), good)); diff --git a/src/threadpool.rs b/src/threadpool.rs index d3baeee..4d47c46 100644 --- a/src/threadpool.rs +++ b/src/threadpool.rs @@ -1,4 +1,4 @@ -use std::sync::{Arc, Mutex, mpsc}; +use std::sync::{mpsc, Arc, Mutex}; use std::thread; // Much of the threadpool implementation here was taken directly from @@ -6,7 +6,7 @@ use std::thread; // and https://doc.rust-lang.org/book/ch20-03-graceful-shutdown-and-cleanup.html /// Manages a threadpool of a given size, sending jobs to workers as -/// necessary. Implements Drop trait to allow threads to complete +/// necessary. Implements Drop trait to allow threads to complete /// their current jobs before being stopped. pub struct Threadpool { workers: Vec, @@ -34,8 +34,9 @@ impl Threadpool { /// Adds a new job to the threadpool, passing closure to first /// available worker. pub fn execute(&self, func: F) - where F: FnOnce() + Send + 'static { - + where + F: FnOnce() + Send + 'static, + { let job = Box::new(func); self.sender.send(JobMessage::NewJob(job)).unwrap(); } @@ -89,4 +90,4 @@ impl Worker { thread: Some(thread), }; } -} \ No newline at end of file +} diff --git a/src/types.rs b/src/types.rs index 3ee868e..0bc1fdc 100644 --- a/src/types.rs +++ b/src/types.rs @@ -1,16 +1,16 @@ +use std::cmp::Ordering; +use std::collections::HashMap; use std::path::PathBuf; use std::sync::{Arc, Mutex, MutexGuard}; -use std::collections::HashMap; -use std::cmp::Ordering; use unicode_segmentation::UnicodeSegmentation; use chrono::{DateTime, Utc}; use lazy_static::lazy_static; use regex::Regex; -use crate::ui::UiMsg; -use crate::feeds::FeedMsg; use crate::downloads::DownloadMsg; +use crate::feeds::FeedMsg; +use crate::ui::UiMsg; lazy_static! { /// Regex for removing "A", "An", and "The" from the beginning of @@ -44,7 +44,11 @@ pub struct Podcast { impl Podcast { /// Counts and returns the number of unplayed episodes in the podcast. fn num_unplayed(&self) -> usize { - return self.episodes.map(|ep| !ep.is_played() as usize).iter().sum(); + return self + .episodes + .map(|ep| !ep.is_played() as usize) + .iter() + .sum(); } } @@ -61,20 +65,25 @@ impl Menuable for Podcast { // if the size available is big enough, we add the unplayed data // to the end if length > crate::config::PODCAST_UNPLAYED_TOTALS_LENGTH { - let meta_str = format!("({}/{})", - self.num_unplayed(), self.episodes.len()); + let meta_str = format!("({}/{})", self.num_unplayed(), self.episodes.len()); title_length = length - meta_str.chars().count(); - let out = self.title + let out = self + .title .graphemes(true) .take(title_length) .collect::(); - return format!("{} {:>width$}", out, meta_str, - width=length-out.graphemes(true).count()); - // this pads spaces between title and totals + return format!( + "{} {:>width$}", + out, + meta_str, + width = length - out.graphemes(true).count() + ); + // this pads spaces between title and totals } else { - let out = self.title + let out = self + .title .graphemes(true) .take(title_length) .collect::(); @@ -106,7 +115,6 @@ impl Ord for Podcast { } } - /// Struct holding data about an individual podcast episode. Most of this /// is metadata, but if the episode has been downloaded to the local /// machine, the filepath will be included here as well. `played` indicates @@ -135,7 +143,7 @@ impl Episode { let minutes = seconds / 60; seconds -= minutes * 60; format!("{:02}:{:02}:{:02}", hours, minutes, seconds) - }, + } None => "--:--:--".to_string(), }; } @@ -151,18 +159,14 @@ impl Menuable for Episode { fn get_title(&self, length: usize) -> String { let out = match self.path { Some(_) => { - let title = self.title + let title = self + .title .graphemes(true) - .take(length-4) + .take(length - 4) .collect::(); format!("[D] {}", title) - }, - None => { - self.title - .graphemes(true) - .take(length) - .collect::() - }, + } + None => self.title.graphemes(true).take(length).collect::(), }; let out_len = out.graphemes(true).count(); if length > crate::config::EPISODE_PUBDATE_LENGTH { @@ -171,35 +175,46 @@ impl Menuable for Episode { if let Some(pubdate) = self.pubdate { // print pubdate and duration - let pd = pubdate.format("%F") - .to_string(); + let pd = pubdate.format("%F").to_string(); let meta_str = format!("({}) {}", pd, meta_dur); let added_len = meta_str.chars().count(); let out_added = out .graphemes(true) - .take(length-added_len) + .take(length - added_len) .collect::(); - return format!("{} {:>width$}", out_added, meta_str, - width=length-out_len); + return format!( + "{} {:>width$}", + out_added, + meta_str, + width = length - out_len + ); } else { // just print duration let out_added = out .graphemes(true) - .take(length-meta_dur.chars().count()) + .take(length - meta_dur.chars().count()) .collect::(); - return format!("{} {:>width$}", out_added, meta_dur, - width=length-out_len); + return format!( + "{} {:>width$}", + out_added, + meta_dur, + width = length - out_len + ); } } else if length > crate::config::EPISODE_DURATION_LENGTH { let dur = self.format_duration(); let meta_dur = format!("[{}]", dur); let out_added = out .graphemes(true) - .take(length-meta_dur.chars().count()) + .take(length - meta_dur.chars().count()) .collect::(); - return format!("{} {:>width$}", out_added, meta_dur, - width=length-out_len); + return format!( + "{} {:>width$}", + out_added, + meta_dur, + width = length - out_len + ); } else { return out; } @@ -210,7 +225,6 @@ impl Menuable for Episode { } } - /// Struct holding data about an individual podcast feed, before it has /// been inserted into the database. This includes a /// (possibly empty) vector of episodes. @@ -236,7 +250,6 @@ pub struct EpisodeNoId { pub duration: Option, } - /// Struct used to hold a vector of data inside a reference-counted /// mutex, to allow for multiple owners of mutable data. /// Primarily, the LockVec is used to provide methods that abstract @@ -244,7 +257,9 @@ pub struct EpisodeNoId { /// Arc>. #[derive(Debug)] pub struct LockVec - where T: Clone + Menuable { +where + T: Clone + Menuable, +{ data: Arc>>, order: Arc>>, } @@ -263,7 +278,7 @@ impl LockVec { return LockVec { data: Arc::new(Mutex::new(hm)), order: Arc::new(Mutex::new(order)), - } + }; } /// Lock the LockVec hashmap for reading/writing. @@ -305,19 +320,19 @@ impl LockVec { /// alive, the function returns a Vec of the collected results, /// rather than an iterator. pub fn map(&self, mut f: F) -> Vec - where F: FnMut(&T) -> B { - + where + F: FnMut(&T) -> B, + { let (map, order) = self.borrow(); - return order.iter().map(|id| { - f(map.get(id).unwrap()) - }).collect(); + return order.iter().map(|id| f(map.get(id).unwrap())).collect(); } /// Maps a closure to a single element in the LockVec, specified by /// `id`. If there is no element `id`, this returns None. pub fn map_single(&self, id: i64, f: F) -> Option - where F: FnOnce(&T) -> B { - + where + F: FnOnce(&T) -> B, + { let borrowed = self.borrow_map(); return match borrowed.get(&id) { Some(item) => Some(f(item)), @@ -329,8 +344,9 @@ impl LockVec { /// `index` (position order). If there is no element at that index, /// this returns None. pub fn map_single_by_index(&self, index: usize, f: F) -> Option - where F: FnOnce(&T) -> B { - + where + F: FnOnce(&T) -> B, + { let order = self.borrow_order(); return match order.get(index) { Some(id) => self.map_single(id.clone(), f), @@ -344,12 +360,14 @@ impl LockVec { /// alive, the function returns a Vec of the collected results, /// rather than an iterator. pub fn filter_map(&self, mut f: F) -> Vec - where F: FnMut(&T) -> Option { - + where + F: FnMut(&T) -> Option, + { let (map, order) = self.borrow(); - return order.iter().filter_map(|id| { - f(map.get(id).unwrap()) - }).collect(); + return order + .iter() + .filter_map(|id| f(map.get(id).unwrap())) + .collect(); } /// Returns the number of items in the LockVec. @@ -368,7 +386,7 @@ impl Clone for LockVec { return LockVec { data: Arc::clone(&self.data), order: Arc::clone(&self.order), - } + }; } } @@ -408,7 +426,6 @@ impl LockVec { } } - /// Overarching Message enum that allows multiple threads to communicate /// back to the main thread with a single enum type. #[derive(Debug)] @@ -416,4 +433,4 @@ pub enum Message { Ui(UiMsg), Feed(FeedMsg), Dl(DownloadMsg), -} \ No newline at end of file +} diff --git a/src/ui/colors.rs b/src/ui/colors.rs index 89f0279..2a5ce4a 100644 --- a/src/ui/colors.rs +++ b/src/ui/colors.rs @@ -20,7 +20,7 @@ impl Colors { pub fn new() -> Colors { return Colors { map: HashMap::new(), - } + }; } pub fn insert(&mut self, color: ColorType, num: i16) { @@ -32,7 +32,6 @@ impl Colors { } } - /// Sets up hashmap for ColorTypes in app, initiates color palette, and /// sets up ncurses color pairs. pub fn set_colors() -> Colors { @@ -48,18 +47,26 @@ pub fn set_colors() -> Colors { pancurses::init_color(pancurses::COLOR_YELLOW, 820, 643, 0); // instantiate curses color pairs - pancurses::init_pair(colors.get(ColorType::Normal), + pancurses::init_pair( + colors.get(ColorType::Normal), pancurses::COLOR_WHITE, - pancurses::COLOR_BLACK); - pancurses::init_pair(colors.get(ColorType::Highlighted), pancurses::COLOR_BLACK, - pancurses::COLOR_WHITE); - pancurses::init_pair(colors.get(ColorType::HighlightedActive), + ); + pancurses::init_pair( + colors.get(ColorType::Highlighted), + pancurses::COLOR_BLACK, + pancurses::COLOR_WHITE, + ); + pancurses::init_pair( + colors.get(ColorType::HighlightedActive), pancurses::COLOR_BLACK, - pancurses::COLOR_YELLOW); - pancurses::init_pair(colors.get(ColorType::Error), + pancurses::COLOR_YELLOW, + ); + pancurses::init_pair( + colors.get(ColorType::Error), pancurses::COLOR_RED, - pancurses::COLOR_BLACK); + pancurses::COLOR_BLACK, + ); return colors; -} \ No newline at end of file +} diff --git a/src/ui/menu.rs b/src/ui/menu.rs index 2ca4418..8d90697 100644 --- a/src/ui/menu.rs +++ b/src/ui/menu.rs @@ -1,9 +1,8 @@ use std::cmp::min; -use crate::types::*; use super::ColorType; use super::Panel; - +use crate::types::*; /// Generic struct holding details about a list menu. These menus are /// contained by the UI, and hold the list of podcasts or podcast @@ -11,7 +10,7 @@ use super::Panel; /// to the user. /// /// * `screen_pos` stores the position of the window on the screen, from -/// left to right +/// left to right /// * `n_row` and `n_col` store the size of the `window` /// * `top_row` indicates the top line of text that is shown on screen /// (since the list of items can be longer than the available size of @@ -22,11 +21,13 @@ use super::Panel; /// 0 and (n_row - 1) #[derive(Debug)] pub struct Menu - where T: Clone + Menuable { +where + T: Clone + Menuable, +{ pub panel: Panel, pub items: LockVec, pub top_row: i32, // top row of text shown in window - pub selected: i32, // which line of text is highlighted + pub selected: i32, // which line of text is highlighted } impl Menu { @@ -56,8 +57,8 @@ impl Menu { let item_idx = (self.top_row + i) as usize; if let Some(elem_id) = order.get(item_idx) { let elem = map.get(&elem_id).unwrap(); - self.panel.write_line(i, - elem.get_title(self.panel.get_cols() as usize)); + self.panel + .write_line(i, elem.get_title(self.panel.get_cols() as usize)); // this is literally the same logic as // self.set_attrs(), but it's complaining about @@ -67,8 +68,13 @@ impl Menu { } else { pancurses::A_BOLD }; - self.panel.change_attr(i, -1, self.panel.get_cols() + 3, - attr, ColorType::Normal); + self.panel.change_attr( + i, + -1, + self.panel.get_cols() + 3, + attr, + ColorType::Normal, + ); } else { break; } @@ -79,7 +85,7 @@ impl Menu { /// Scrolls the menu up or down by `lines` lines. Negative values of /// `lines` will scroll the menu up. - /// + /// /// This function examines the new selected value, ensures it does /// not fall out of bounds, and then updates the pancurses window to /// represent the new visible list. @@ -87,7 +93,7 @@ impl Menu { let mut old_selected; let old_played; let new_played; - + let list_len = self.items.len(); if list_len == 0 { return; @@ -102,8 +108,7 @@ impl Menu { // don't allow scrolling past last item in list (if shorter // than self.panel.get_rows()) - let abs_bottom = min(self.panel.get_rows(), - (list_len - 1) as i32); + let abs_bottom = min(self.panel.get_rows(), (list_len - 1) as i32); if self.selected > abs_bottom { self.selected = abs_bottom; } @@ -112,37 +117,43 @@ impl Menu { // scroll down if self.selected > (n_row - 1) { self.selected = n_row - 1; - if let Some(title) = self.items - .map_single_by_index((self.top_row + n_row) as usize, - |el| el.get_title(self.panel.get_cols() as usize)) { - + if let Some(title) = self + .items + .map_single_by_index((self.top_row + n_row) as usize, |el| { + el.get_title(self.panel.get_cols() as usize) + }) + { self.top_row += 1; self.panel.delete_line(0); old_selected -= 1; - self.panel.delete_line(n_row-1); - self.panel.write_line(n_row-1, title); + self.panel.delete_line(n_row - 1); + self.panel.write_line(n_row - 1, title); } // scroll up } else if self.selected < 0 { self.selected = 0; - if let Some(title) = self.items - .map_single_by_index((self.top_row - 1) as usize, - |el| el.get_title(self.panel.get_cols() as usize)) { - + if let Some(title) = self + .items + .map_single_by_index((self.top_row - 1) as usize, |el| { + el.get_title(self.panel.get_cols() as usize) + }) + { self.top_row -= 1; self.panel.insert_line(0, title); old_selected += 1; } } - old_played = self.items - .map_single_by_index((self.top_row + old_selected) as usize, - |el| el.is_played()).unwrap(); - new_played = self.items - .map_single_by_index((self.top_row + self.selected) as usize, - |el| el.is_played()).unwrap(); + old_played = self + .items + .map_single_by_index((self.top_row + old_selected) as usize, |el| el.is_played()) + .unwrap(); + new_played = self + .items + .map_single_by_index((self.top_row + self.selected) as usize, |el| el.is_played()) + .unwrap(); self.set_attrs(old_selected, old_played, ColorType::Normal); self.set_attrs(self.selected, new_played, ColorType::HighlightedActive); @@ -160,16 +171,16 @@ impl Menu { } else { pancurses::A_BOLD }; - self.panel.change_attr(index, -1, self.panel.get_cols() + 3, - attr, color); + self.panel + .change_attr(index, -1, self.panel.get_cols() + 3, attr, color); } /// Highlights the currently selected item in the menu, based on /// whether the menu is currently active or not. pub fn highlight_selected(&mut self, active_menu: bool) { - let is_played = self.items - .map_single_by_index((self.top_row + self.selected) as usize, - |el| el.is_played()); + let is_played = self + .items + .map_single_by_index((self.top_row + self.selected) as usize, |el| el.is_played()); if let Some(played) = is_played { if active_menu { @@ -185,10 +196,10 @@ impl Menu { /// for user input to modify state). pub fn activate(&mut self) { // if list is empty, will return None - if let Some(played) = self.items - .map_single_by_index((self.top_row + self.selected) as usize, - |el| el.is_played()) { - + if let Some(played) = self + .items + .map_single_by_index((self.top_row + self.selected) as usize, |el| el.is_played()) + { self.set_attrs(self.selected, played, ColorType::HighlightedActive); self.panel.refresh(); } @@ -208,26 +219,34 @@ impl Menu { } } - impl Menu { /// Returns a cloned reference to the list of episodes from the /// currently selected podcast. pub fn get_episodes(&self) -> LockVec { let index = self.selected + self.top_row; - let pod_id = self.items.borrow_order() - .get(index as usize).copied().unwrap(); - return self.items.borrow_map().get(&pod_id).unwrap() - .episodes.clone(); + let pod_id = self + .items + .borrow_order() + .get(index as usize) + .copied() + .unwrap(); + return self + .items + .borrow_map() + .get(&pod_id) + .unwrap() + .episodes + .clone(); } /// Controls how the window changes when it is inactive (i.e., not /// available for user input to modify state). pub fn deactivate(&mut self) { // if list is empty, will return None - if let Some(played) = self.items - .map_single_by_index((self.top_row + self.selected) as usize, - |el| el.is_played()) { - + if let Some(played) = self + .items + .map_single_by_index((self.top_row + self.selected) as usize, |el| el.is_played()) + { self.set_attrs(self.selected, played, ColorType::Highlighted); self.panel.refresh(); } @@ -239,17 +258,16 @@ impl Menu { /// available for user input to modify state). pub fn deactivate(&mut self) { // if list is empty, will return None - if let Some(played) = self.items - .map_single_by_index((self.top_row + self.selected) as usize, - |el| el.is_played()) { - + if let Some(played) = self + .items + .map_single_by_index((self.top_row + self.selected) as usize, |el| el.is_played()) + { self.set_attrs(self.selected, played, ColorType::Normal); self.panel.refresh(); } } } - // TESTS ----------------------------------------------------------------- #[cfg(test)] mod tests { @@ -264,7 +282,7 @@ mod tests { "How does an episode with emoji sound? 😉", "Here's another title", "Un titre, c'est moi!", - "One more just for good measure" + "One more just for good measure", ]; let mut items = Vec::new(); for (i, t) in titles.iter().enumerate() { @@ -286,8 +304,10 @@ mod tests { crate::ui::colors::set_colors(), "Episodes".to_string(), 1, - n_row, n_col, - 0, 0 + n_row, + n_col, + 0, + 0, ); return Menu { panel: panel, @@ -301,15 +321,19 @@ mod tests { fn scroll_up() { let real_rows = 5; let real_cols = 65; - let mut menu = create_menu(real_rows+2, real_cols+5, 2, 0); + let mut menu = create_menu(real_rows + 2, real_cols + 5, 2, 0); menu.update_items(); menu.scroll(-1); - let expected_top = menu.items.map_single_by_index(1, - |ep| ep.get_title(real_cols as usize)).unwrap(); - let expected_bot = menu.items.map_single_by_index(5, - |ep| ep.get_title(real_cols as usize)).unwrap(); + let expected_top = menu + .items + .map_single_by_index(1, |ep| ep.get_title(real_cols as usize)) + .unwrap(); + let expected_bot = menu + .items + .map_single_by_index(5, |ep| ep.get_title(real_cols as usize)) + .unwrap(); assert_eq!(menu.panel.get_row(0).0, expected_top); assert_eq!(menu.panel.get_row(4).0, expected_bot); @@ -319,15 +343,19 @@ mod tests { fn scroll_down() { let real_rows = 5; let real_cols = 65; - let mut menu = create_menu(real_rows+2, real_cols+5, 0, 4); + let mut menu = create_menu(real_rows + 2, real_cols + 5, 0, 4); menu.update_items(); menu.scroll(1); - let expected_top = menu.items.map_single_by_index(1, - |ep| ep.get_title(real_cols as usize)).unwrap(); - let expected_bot = menu.items.map_single_by_index(5, - |ep| ep.get_title(real_cols as usize)).unwrap(); + let expected_top = menu + .items + .map_single_by_index(1, |ep| ep.get_title(real_cols as usize)) + .unwrap(); + let expected_bot = menu + .items + .map_single_by_index(5, |ep| ep.get_title(real_cols as usize)) + .unwrap(); assert_eq!(menu.panel.get_row(0).0, expected_top); assert_eq!(menu.panel.get_row(4).0, expected_bot); @@ -337,21 +365,27 @@ mod tests { fn resize_bigger() { let real_rows = 5; let real_cols = 65; - let mut menu = create_menu(real_rows+2, real_cols+5, 0, 4); + let mut menu = create_menu(real_rows + 2, real_cols + 5, 0, 4); menu.update_items(); - menu.resize(real_rows+2+5, real_cols+5+5, 0, 0); + menu.resize(real_rows + 2 + 5, real_cols + 5 + 5, 0, 0); menu.update_items(); assert_eq!(menu.top_row, 0); assert_eq!(menu.selected, 4); - let non_empty: Vec = menu.panel.window.iter() - .filter_map(|x| if x.0.is_empty() { + let non_empty: Vec = menu + .panel + .window + .iter() + .filter_map(|x| { + if x.0.is_empty() { None } else { Some(x.0.clone()) - }).collect(); + } + }) + .collect(); assert_eq!(non_empty.len(), menu.items.len()); } @@ -359,29 +393,35 @@ mod tests { fn resize_smaller() { let real_rows = 7; let real_cols = 65; - let mut menu = create_menu(real_rows+2, real_cols+5, 0, 6); + let mut menu = create_menu(real_rows + 2, real_cols + 5, 0, 6); menu.update_items(); - menu.resize(real_rows+2-2, real_cols+5-5, 0, 0); + menu.resize(real_rows + 2 - 2, real_cols + 5 - 5, 0, 0); menu.update_items(); assert_eq!(menu.top_row, 2); assert_eq!(menu.selected, 4); - let non_empty: Vec = menu.panel.window.iter() - .filter_map(|x| if x.0.is_empty() { + let non_empty: Vec = menu + .panel + .window + .iter() + .filter_map(|x| { + if x.0.is_empty() { None } else { Some(x.0.clone()) - }).collect(); - assert_eq!(non_empty.len(), (real_rows-2) as usize); + } + }) + .collect(); + assert_eq!(non_empty.len(), (real_rows - 2) as usize); } #[test] fn chop_accent() { let real_rows = 5; let real_cols = 25; - let mut menu = create_menu(real_rows+2, real_cols+5, 0, 0); + let mut menu = create_menu(real_rows + 2, real_cols + 5, 0, 0); menu.update_items(); let expected = "An episode with le Unicod".to_string(); @@ -393,11 +433,11 @@ mod tests { fn chop_emoji() { let real_rows = 5; let real_cols = 38; - let mut menu = create_menu(real_rows+2, real_cols+5, 0, 0); + let mut menu = create_menu(real_rows + 2, real_cols + 5, 0, 0); menu.update_items(); let expected = "How does an episode with emoji sound? ".to_string(); assert_eq!(menu.panel.get_row(3).0, expected); } -} \ No newline at end of file +} diff --git a/src/ui/mock_panel.rs b/src/ui/mock_panel.rs index 3bd85b6..3d65e60 100644 --- a/src/ui/mock_panel.rs +++ b/src/ui/mock_panel.rs @@ -1,5 +1,5 @@ +use super::{ColorType, Colors}; use chrono::{DateTime, Utc}; -use super::{Colors, ColorType}; /// Struct holding the raw data used for building the details panel. pub struct Details { @@ -22,14 +22,19 @@ pub struct Panel { } impl Panel { - pub fn new(colors: Colors, - title: String, screen_pos: usize, n_row: i32, n_col: i32, _start_y: i32, _start_x: i32) -> Self { - + pub fn new( + colors: Colors, + title: String, + screen_pos: usize, + n_row: i32, + n_col: i32, + _start_y: i32, + _start_x: i32, + ) -> Self { // we represent the window as a vector of Strings instead of // the pancurses window - let panel_win = vec![ - (String::new(), pancurses::A_NORMAL, ColorType::Normal); - (n_row-2) as usize]; + let panel_win = + vec![(String::new(), pancurses::A_NORMAL, ColorType::Normal); (n_row - 2) as usize]; return Panel { window: panel_win, @@ -46,9 +51,8 @@ impl Panel { pub fn refresh(&self) {} pub fn erase(&mut self) { - self.window = vec![ - (String::new(), pancurses::A_NORMAL, ColorType::Normal); - self.n_row as usize]; + self.window = + vec![(String::new(), pancurses::A_NORMAL, ColorType::Normal); self.n_row as usize]; } pub fn write_line(&mut self, y: i32, string: String) { @@ -56,8 +60,8 @@ impl Panel { } pub fn insert_line(&mut self, y: i32, string: String) { - self.window.insert(y as usize, - (string, pancurses::A_NORMAL, ColorType::Normal)); + self.window + .insert(y as usize, (string, pancurses::A_NORMAL, ColorType::Normal)); let _ = self.window.pop(); } @@ -65,7 +69,8 @@ impl Panel { let _ = self.window.remove(y as usize); // add a new empty line to the end so the vector stays the // same size - self.window.push((String::new(), pancurses::A_NORMAL, ColorType::Normal)); + self.window + .push((String::new(), pancurses::A_NORMAL, ColorType::Normal)); } pub fn write_wrap_line(&mut self, start_y: i32, string: String) -> i32 { @@ -80,67 +85,65 @@ impl Panel { break; } } - return row-1; + return row - 1; } pub fn details_template(&mut self, start_y: i32, details: Details) { - let mut row = start_y-1; + let mut row = start_y - 1; // podcast title match details.pod_title { - Some(t) => row = self.write_wrap_line(row+1, t), - None => row = self.write_wrap_line(row+1, "No title".to_string()), + Some(t) => row = self.write_wrap_line(row + 1, t), + None => row = self.write_wrap_line(row + 1, "No title".to_string()), } // episode title match details.ep_title { - Some(t) => row = self.write_wrap_line(row+1, t), - None => row = self.write_wrap_line(row+1, "No title".to_string()), + Some(t) => row = self.write_wrap_line(row + 1, t), + None => row = self.write_wrap_line(row + 1, "No title".to_string()), } - row += 1; // blank line + row += 1; // blank line // published date if let Some(date) = details.pubdate { - let new_row = self.write_wrap_line(row+1, - format!("Published: {}", date.format("%B %-d, %Y").to_string())); - self.change_attr(row+1, 0, 10, - pancurses::A_UNDERLINE, ColorType::Normal); + let new_row = self.write_wrap_line( + row + 1, + format!("Published: {}", date.format("%B %-d, %Y").to_string()), + ); + self.change_attr(row + 1, 0, 10, pancurses::A_UNDERLINE, ColorType::Normal); row = new_row; } // duration if let Some(dur) = details.duration { - let new_row = self.write_wrap_line(row+1, - format!("Duration: {}", dur)); - self.change_attr(row+1, 0, 9, - pancurses::A_UNDERLINE, ColorType::Normal); + let new_row = self.write_wrap_line(row + 1, format!("Duration: {}", dur)); + self.change_attr(row + 1, 0, 9, pancurses::A_UNDERLINE, ColorType::Normal); row = new_row; } // explicit if let Some(exp) = details.explicit { let new_row = if exp { - self.write_wrap_line(row+1, "Explicit: Yes".to_string()) + self.write_wrap_line(row + 1, "Explicit: Yes".to_string()) } else { - self.write_wrap_line(row+1, "Explicit: No".to_string()) + self.write_wrap_line(row + 1, "Explicit: No".to_string()) }; - self.change_attr(row+1, 0, 9, - pancurses::A_UNDERLINE, ColorType::Normal); + self.change_attr(row + 1, 0, 9, pancurses::A_UNDERLINE, ColorType::Normal); row = new_row; } - row += 1; // blank line + row += 1; // blank line // description match details.description { Some(desc) => { - row = self.write_wrap_line(row+1, "Description:".to_string()); - let _row = self.write_wrap_line(row+1, desc); - }, + row = self.write_wrap_line(row + 1, "Description:".to_string()); + let _row = self.write_wrap_line(row + 1, desc); + } None => { - let _row = self.write_wrap_line(row+1, "No description.".to_string()); - }, + let _row = self.write_wrap_line(row + 1, "No description.".to_string()); + } } } @@ -148,7 +151,14 @@ impl Panel { // only applies the attribute to the line as a whole, rather than // specific characters. But I'm primarily using it to change whole // lines anyway. - pub fn change_attr(&mut self, y: i32, _x: i32, _nchars: i32, attr: pancurses::chtype, color: ColorType) { + pub fn change_attr( + &mut self, + y: i32, + _x: i32, + _nchars: i32, + attr: pancurses::chtype, + color: ColorType, + ) { let current = &self.window[y as usize]; self.window[y as usize] = (current.0.clone(), attr, color); } @@ -157,27 +167,28 @@ impl Panel { self.n_row = n_row; self.n_col = n_col; - let new_len = (n_row-2) as usize; + let new_len = (n_row - 2) as usize; let len = self.window.len(); if new_len < len { self.window.truncate(new_len); } else if new_len > len { for _ in (new_len - len)..new_len { - self.window.push((String::new(), pancurses::A_NORMAL, ColorType::Normal)); + self.window + .push((String::new(), pancurses::A_NORMAL, ColorType::Normal)); } } } pub fn get_rows(&self) -> i32 { - return self.n_row - 2; // border on top and bottom + return self.n_row - 2; // border on top and bottom } pub fn get_cols(&self) -> i32 { - return self.n_col - 5; // 2 for border, 2 for margins, and 1 - // extra for some reason... + return self.n_col - 5; // 2 for border, 2 for margins, and 1 + // extra for some reason... } pub fn get_row(&self, row: usize) -> (String, pancurses::chtype, ColorType) { return self.window[row].clone(); } -} \ No newline at end of file +} diff --git a/src/ui/mod.rs b/src/ui/mod.rs index c5102f7..ff4b13c 100644 --- a/src/ui/mod.rs +++ b/src/ui/mod.rs @@ -1,28 +1,28 @@ -use std::thread; use std::sync::mpsc; +use std::thread; use std::time::Duration; -#[cfg_attr(not(test), path="panel.rs")] -#[cfg_attr(test, path="mock_panel.rs")] +#[cfg_attr(not(test), path = "panel.rs")] +#[cfg_attr(test, path = "mock_panel.rs")] mod panel; +mod colors; mod menu; mod notification; -mod colors; -use self::panel::{Panel, Details}; +use self::colors::{ColorType, Colors}; use self::menu::Menu; use self::notification::NotifWin; -use self::colors::{Colors, ColorType}; +use self::panel::{Details, Panel}; -use pancurses::{Window, Input}; use lazy_static::lazy_static; +use pancurses::{Input, Window}; use regex::Regex; +use super::MainMessage; use crate::config::Config; use crate::keymap::{Keybindings, UserAction}; use crate::types::*; -use super::MainMessage; lazy_static! { /// Regex for finding
tags -- also captures any surrounding @@ -36,7 +36,6 @@ lazy_static! { static ref RE_MULT_LINE_BREAKS: Regex = Regex::new(r"((\r\n)|\r|\n){3,}").unwrap(); } - /// Enum used for communicating back to the main controller after user /// input has been captured by the UI. usize values always represent the /// selected podcast, and (if applicable), the selected episode, in that @@ -67,7 +66,6 @@ enum ActiveMenu { EpisodeMenu, } - /// Struct containing all interface elements of the TUI. Functionally, it /// encapsulates the pancurses windows, and holds data about the size of /// the screen. @@ -89,7 +87,12 @@ pub struct UI<'a> { impl<'a> UI<'a> { /// Spawns a UI object in a new thread, with message channels to send /// and receive messages - pub fn spawn(config: Config, items: LockVec, rx_from_main: mpsc::Receiver, tx_to_main: mpsc::Sender) -> thread::JoinHandle<()> { + pub fn spawn( + config: Config, + items: LockVec, + rx_from_main: mpsc::Receiver, + tx_to_main: mpsc::Sender, + ) -> thread::JoinHandle<()> { return thread::spawn(move || { let mut ui = UI::new(&config, &items); ui.init(); @@ -108,8 +111,12 @@ impl<'a> UI<'a> { if let Some(message) = message_iter.next() { match message { MainMessage::UiUpdateMenus => ui.update_menus(), - MainMessage::UiSpawnNotif(msg, duration, error) => ui.timed_notif(msg, error, duration), - MainMessage::UiSpawnPersistentNotif(msg, error) => ui.persistent_notif(msg, error), + MainMessage::UiSpawnNotif(msg, duration, error) => { + ui.timed_notif(msg, error, duration) + } + MainMessage::UiSpawnPersistentNotif(msg, error) => { + ui.persistent_notif(msg, error) + } MainMessage::UiClearPersistentNotif => ui.clear_persistent_notif(), MainMessage::UiTearDown => { ui.tear_down(); @@ -131,14 +138,14 @@ impl<'a> UI<'a> { let stdscr = pancurses::initscr(); // set some options - pancurses::cbreak(); // allows characters to be read one by one - pancurses::noecho(); // turns off automatic echoing of characters - // to the screen as they are input - pancurses::start_color(); // allows colours if available - pancurses::curs_set(0); // turn off cursor - stdscr.keypad(true); // returns special characters as single - // key codes - stdscr.nodelay(true); // getch() will not wait for user input + pancurses::cbreak(); // allows characters to be read one by one + pancurses::noecho(); // turns off automatic echoing of characters + // to the screen as they are input + pancurses::start_color(); // allows colours if available + pancurses::curs_set(0); // turn off cursor + stdscr.keypad(true); // returns special characters as single + // key codes + stdscr.nodelay(true); // getch() will not wait for user input // set colors let colors = self::colors::set_colors(); @@ -150,8 +157,10 @@ impl<'a> UI<'a> { colors.clone(), "Podcasts".to_string(), 0, - n_row - 1, pod_col, - 0, 0 + n_row - 1, + pod_col, + 0, + 0, ); let podcast_menu = Menu { panel: podcast_panel, @@ -164,16 +173,16 @@ impl<'a> UI<'a> { colors.clone(), "Episodes".to_string(), 1, - n_row - 1, ep_col, - 0, pod_col - 1 + n_row - 1, + ep_col, + 0, + pod_col - 1, ); let first_pod = match items.borrow_order().get(0) { - Some(first_id) => { - match items.borrow_map().get(first_id) { - Some(pod) => pod.episodes.clone(), - None => LockVec::new(Vec::new()), - } + Some(first_id) => match items.borrow_map().get(first_id) { + Some(pod) => pod.episodes.clone(), + None => LockVec::new(Vec::new()), }, None => LockVec::new(Vec::new()), }; @@ -188,18 +197,25 @@ impl<'a> UI<'a> { let details_panel = if n_col > crate::config::DETAILS_PANEL_LENGTH { Some(Self::make_details_panel( colors.clone(), - n_row-1, det_col, - 0, pod_col + ep_col - 2)) + n_row - 1, + det_col, + 0, + pod_col + ep_col - 2, + )) } else { None }; - let notif_win = NotifWin::new(colors.clone(), - n_row, n_col); + let notif_win = NotifWin::new(colors.clone(), n_row, n_col); // welcome screen if user does not have any podcasts yet let welcome_win = if items.is_empty() { - Some(UI::make_welcome_win(colors.clone(), &config.keybindings, n_row-1, n_col)) + Some(UI::make_welcome_win( + colors.clone(), + &config.keybindings, + n_row - 1, + n_col, + )) } else { None }; @@ -236,7 +252,7 @@ impl<'a> UI<'a> { /// Waits for user input and, where necessary, provides UiMessages /// back to the main controller. - /// + /// /// Anything UI-related (e.g., scrolling up and down menus) is handled /// internally, producing an empty UiMessage. This allows for some /// greater degree of abstraction; for example, input to add a new @@ -253,32 +269,35 @@ impl<'a> UI<'a> { let (pod_col, ep_col, det_col) = Self::calculate_sizes(n_col); - self.podcast_menu.resize(n_row-1, pod_col, 0, 0); - self.episode_menu.resize(n_row-1, ep_col, 0, pod_col - 1); + self.podcast_menu.resize(n_row - 1, pod_col, 0, 0); + self.episode_menu.resize(n_row - 1, ep_col, 0, pod_col - 1); if self.details_panel.is_some() { if det_col > 0 { let det = self.details_panel.as_mut().unwrap(); - det.resize(n_row-1, det_col, 0, pod_col+ep_col-2); + det.resize(n_row - 1, det_col, 0, pod_col + ep_col - 2); } else { self.details_panel = None; } } else if det_col > 0 { self.details_panel = Some(Self::make_details_panel( self.colors.clone(), - n_row-1, det_col, - 0, pod_col + ep_col - 2)); + n_row - 1, + det_col, + 0, + pod_col + ep_col - 2, + )); } self.stdscr.refresh(); self.update_menus(); - + match self.active_menu { ActiveMenu::PodcastMenu => self.podcast_menu.activate(), ActiveMenu::EpisodeMenu => { self.podcast_menu.activate(); self.episode_menu.activate(); - }, + } } if self.details_panel.is_some() { @@ -289,15 +308,21 @@ impl<'a> UI<'a> { if self.welcome_win.is_some() { let _ = std::mem::replace( &mut self.welcome_win, - Some(UI::make_welcome_win(self.colors.clone(), &self.keymap, n_row-1, n_col))); - + Some(UI::make_welcome_win( + self.colors.clone(), + &self.keymap, + n_row - 1, + n_col, + )), + ); + let ww = self.welcome_win.as_mut().unwrap(); ww.refresh(); } self.notif_win.resize(n_row, n_col); self.stdscr.refresh(); - }, + } Some(input) => { let (curr_pod_id, curr_ep_id) = self.get_current_ids(); @@ -323,15 +348,15 @@ impl<'a> UI<'a> { self.episode_menu.update_items(); self.update_details_panel(); } - }, + } ActiveMenu::EpisodeMenu => { if curr_ep_id.is_some() { self.episode_menu.scroll(1); self.update_details_panel(); } - }, + } } - }, + } Some(UserAction::Up) => { match self.active_menu { @@ -347,15 +372,15 @@ impl<'a> UI<'a> { self.episode_menu.update_items(); self.update_details_panel(); } - }, + } ActiveMenu::EpisodeMenu => { if curr_pod_id.is_some() { self.episode_menu.scroll(-1); self.update_details_panel(); } - }, + } } - }, + } Some(UserAction::Left) => { if curr_pod_id.is_some() { @@ -365,10 +390,10 @@ impl<'a> UI<'a> { self.active_menu = ActiveMenu::PodcastMenu; self.podcast_menu.activate(); self.episode_menu.deactivate(); - }, + } } } - }, + } Some(UserAction::Right) => { if curr_pod_id.is_some() && curr_ep_id.is_some() { @@ -377,51 +402,50 @@ impl<'a> UI<'a> { self.active_menu = ActiveMenu::EpisodeMenu; self.podcast_menu.deactivate(); self.episode_menu.activate(); - }, + } ActiveMenu::EpisodeMenu => (), } } - }, + } Some(UserAction::AddFeed) => { let url = &self.spawn_input_notif("Feed URL: "); if !url.is_empty() { return UiMsg::AddFeed(url.to_string()); } - }, + } Some(UserAction::Sync) => { if let Some(pod_id) = curr_pod_id { return UiMsg::Sync(pod_id); } - }, + } Some(UserAction::SyncAll) => { if curr_pod_id.is_some() { return UiMsg::SyncAll; } - }, + } Some(UserAction::Play) => { if let Some(pod_id) = curr_pod_id { if let Some(ep_id) = curr_ep_id { return UiMsg::Play(pod_id, ep_id); } } - }, - Some(UserAction::MarkPlayed) => { - match self.active_menu { - ActiveMenu::PodcastMenu => (), - ActiveMenu::EpisodeMenu => { - if let Some(pod_id) = curr_pod_id { - if let Some(ep_id) = curr_ep_id { - if let Some(played) = self.episode_menu - .items.map_single(ep_id, - |ep| ep.is_played()) { - - return UiMsg::MarkPlayed(pod_id, ep_id, !played); - } + } + Some(UserAction::MarkPlayed) => match self.active_menu { + ActiveMenu::PodcastMenu => (), + ActiveMenu::EpisodeMenu => { + if let Some(pod_id) = curr_pod_id { + if let Some(ep_id) = curr_ep_id { + if let Some(played) = self + .episode_menu + .items + .map_single(ep_id, |ep| ep.is_played()) + { + return UiMsg::MarkPlayed(pod_id, ep_id, !played); } } - }, + } } }, Some(UserAction::MarkAllPlayed) => { @@ -429,14 +453,15 @@ impl<'a> UI<'a> { // will convert all to played; if all are played // already, only then will it convert all to unplayed if let Some(pod_id) = curr_pod_id { - if let Some(played) = self.podcast_menu - .items.map_single(pod_id, - |pod| pod.is_played()) { - + if let Some(played) = self + .podcast_menu + .items + .map_single(pod_id, |pod| pod.is_played()) + { return UiMsg::MarkAllPlayed(pod_id, !played); } } - }, + } Some(UserAction::Download) => { if let Some(pod_id) = curr_pod_id { @@ -444,24 +469,22 @@ impl<'a> UI<'a> { return UiMsg::Download(pod_id, ep_id); } } - }, + } Some(UserAction::DownloadAll) => { if let Some(pod_id) = curr_pod_id { return UiMsg::DownloadAll(pod_id); } - }, + } - Some(UserAction::Delete) => { - match self.active_menu { - ActiveMenu::PodcastMenu => (), - ActiveMenu::EpisodeMenu => { - if let Some(pod_id) = curr_pod_id { - if let Some(ep_id) = curr_ep_id { - return UiMsg::Delete(pod_id, ep_id); - } + Some(UserAction::Delete) => match self.active_menu { + ActiveMenu::PodcastMenu => (), + ActiveMenu::EpisodeMenu => { + if let Some(pod_id) = curr_pod_id { + if let Some(ep_id) = curr_ep_id { + return UiMsg::Delete(pod_id, ep_id); } - }, + } } }, @@ -469,7 +492,7 @@ impl<'a> UI<'a> { if let Some(pod_id) = curr_pod_id { return UiMsg::DeleteAll(pod_id); } - }, + } Some(UserAction::Remove) => { let mut delete = false; @@ -481,11 +504,9 @@ impl<'a> UI<'a> { let mut any_downloaded = false; { let borrowed_map = self.podcast_menu.items.borrow_map(); - let borrowed_pod = borrowed_map - .get(&pod_id).unwrap(); - - let borrowed_ep_list = borrowed_pod.episodes - .borrow_map(); + let borrowed_pod = borrowed_map.get(&pod_id).unwrap(); + + let borrowed_ep_list = borrowed_pod.episodes.borrow_map(); for (_ep_id, ep) in borrowed_ep_list.iter() { if ep.path.is_some() { @@ -496,52 +517,52 @@ impl<'a> UI<'a> { } if any_downloaded { - let ask_delete = self.spawn_yes_no_notif("Delete local files too?"); + let ask_delete = + self.spawn_yes_no_notif("Delete local files too?"); delete = match ask_delete { Some(val) => val, - None => false, // default not to delete + None => false, // default not to delete }; } return UiMsg::RemovePodcast(pod_id, delete); } - }, + } ActiveMenu::EpisodeMenu => { if let Some(pod_id) = curr_pod_id { if let Some(ep_id) = curr_ep_id { - // check if we have local files first - let is_downloaded = self.episode_menu.items - .map_single(ep_id, - |ep| ep.path.is_some()) + let is_downloaded = self + .episode_menu + .items + .map_single(ep_id, |ep| ep.path.is_some()) .unwrap(); if is_downloaded { - let ask_delete = self.spawn_yes_no_notif("Delete local file too?"); + let ask_delete = + self.spawn_yes_no_notif("Delete local file too?"); delete = match ask_delete { Some(val) => val, - None => false, // default not to delete + None => false, // default not to delete }; } return UiMsg::RemoveEpisode(pod_id, ep_id, delete); } } - }, + } } - }, + } Some(UserAction::RemoveAll) => { if let Some(pod_id) = curr_pod_id { let mut delete = false; - + // check if we have local files first let mut any_downloaded = false; { let borrowed_map = self.podcast_menu.items.borrow_map(); - let borrowed_pod = borrowed_map - .get(&pod_id).unwrap(); - - let borrowed_ep_list = borrowed_pod.episodes - .borrow_map(); + let borrowed_pod = borrowed_map.get(&pod_id).unwrap(); + + let borrowed_ep_list = borrowed_pod.episodes.borrow_map(); for (_ep_id, ep) in borrowed_ep_list.iter() { if ep.path.is_some() { @@ -555,24 +576,24 @@ impl<'a> UI<'a> { let ask_delete = self.spawn_yes_no_notif("Delete local files too?"); delete = match ask_delete { Some(val) => val, - None => false, // default not to delete + None => false, // default not to delete }; } return match self.active_menu { ActiveMenu::PodcastMenu => UiMsg::RemovePodcast(pod_id, delete), ActiveMenu::EpisodeMenu => UiMsg::RemoveAllEpisodes(pod_id, delete), - } + }; } - }, + } Some(UserAction::Quit) => { return UiMsg::Quit; - }, + } None => (), - } // end of input match - }, + } // end of input match + } None => (), - }; // end of getch() match + }; // end of getch() match return UiMsg::Noop; } @@ -580,15 +601,21 @@ impl<'a> UI<'a> { /// menus, returns the IDs of the current podcast and episode (if /// they exist). pub fn get_current_ids(&self) -> (Option, Option) { - let current_pod_index = (self.podcast_menu.selected + - self.podcast_menu.top_row) as usize; - let current_ep_index = (self.episode_menu.selected + - self.episode_menu.top_row) as usize; - - let current_pod_id = self.podcast_menu.items - .borrow_order().get(current_pod_index).copied(); - let current_ep_id = self.episode_menu.items - .borrow_order().get(current_ep_index).copied(); + let current_pod_index = (self.podcast_menu.selected + self.podcast_menu.top_row) as usize; + let current_ep_index = (self.episode_menu.selected + self.episode_menu.top_row) as usize; + + let current_pod_id = self + .podcast_menu + .items + .borrow_order() + .get(current_pod_index) + .copied(); + let current_ep_id = self + .episode_menu + .items + .borrow_order() + .get(current_ep_index) + .copied(); return (current_pod_id, current_ep_id); } @@ -630,7 +657,9 @@ impl<'a> UI<'a> { /// return None. pub fn spawn_yes_no_notif(&self, prefix: &str) -> Option { let mut out_val = None; - let input = self.notif_win.input_notif(&format!("{} {}", prefix, "(y/n) ")); + let input = self + .notif_win + .input_notif(&format!("{} {}", prefix, "(y/n) ")); if let Some(c) = input.trim().chars().next() { if c == 'Y' || c == 'y' { out_val = Some(true); @@ -678,10 +707,10 @@ impl<'a> UI<'a> { ActiveMenu::EpisodeMenu => { self.podcast_menu.highlight_selected(false); self.episode_menu.highlight_selected(true); - }, + } } } - + /// When the program is ending, this performs tear-down functions so /// that the terminal is properly restored to its prior settings. pub fn tear_down(&self) { @@ -689,13 +718,22 @@ impl<'a> UI<'a> { } /// Create a details panel. - pub fn make_details_panel(colors: Colors, n_row: i32, n_col: i32, start_y: i32, start_x: i32) -> Panel { + pub fn make_details_panel( + colors: Colors, + n_row: i32, + n_col: i32, + start_y: i32, + start_x: i32, + ) -> Panel { return Panel::new( colors, "Details".to_string(), 2, - n_row, n_col, - start_y, start_x); + n_row, + n_col, + start_y, + start_x, + ); } /// Updates the details panel with information about the current @@ -710,9 +748,7 @@ impl<'a> UI<'a> { // get a couple details from the current podcast let mut pod_title = None; let mut pod_explicit = None; - if let Some(pod) = self.podcast_menu.items - .borrow_map().get(&pod_id) { - + if let Some(pod) = self.podcast_menu.items.borrow_map().get(&pod_id) { pod_title = if pod.title.is_empty() { None } else { @@ -722,8 +758,7 @@ impl<'a> UI<'a> { }; // the rest of the details come from the current episode - if let Some(ep) = self.episode_menu.items - .borrow_map().get(&ep_id) { + if let Some(ep) = self.episode_menu.items.borrow_map().get(&ep_id) { let ep_title = if ep.title.is_empty() { None } else { @@ -742,7 +777,7 @@ impl<'a> UI<'a> { // convert HTML entities (e.g., &) let decoded = match escaper::decode_html(&stripped_tags) { Err(_) => stripped_tags.to_string(), - Ok(s) => s + Ok(s) => s, }; // remove anything more than two line breaks (i.e., one blank line) @@ -771,9 +806,7 @@ impl<'a> UI<'a> { /// Creates a pancurses window with a welcome message for when users /// start the program for the first time. Responsibility for managing /// the window is given back to the main UI object. - pub fn make_welcome_win(colors: Colors, keymap: &Keybindings, - n_row: i32, n_col:i32) -> Panel { - + pub fn make_welcome_win(colors: Colors, keymap: &Keybindings, n_row: i32, n_col: i32) -> Panel { let add_keys = keymap.keys_for_action(UserAction::AddFeed); let quit_keys = keymap.keys_for_action(UserAction::Quit); @@ -814,22 +847,23 @@ impl<'a> UI<'a> { // the warning on the unused mut is a function of Rust getting // confused between panel.rs and mock_panel.rs #[allow(unused_mut)] - let mut welcome_win = Panel::new( - colors, - "Shellcaster".to_string(), - 0, - n_row, n_col, 0, 0 - ); + let mut welcome_win = Panel::new(colors, "Shellcaster".to_string(), 0, n_row, n_col, 0, 0); let mut row = 0; - row = welcome_win.write_wrap_line(row+1, "Welcome to shellcaster!".to_string()); + row = welcome_win.write_wrap_line(row + 1, "Welcome to shellcaster!".to_string()); row = welcome_win.write_wrap_line(row+2, format!("Your podcast list is currently empty. Press {} to add a new podcast feed, or {} to quit.", add_str, quit_str)); - row = welcome_win.write_wrap_line(row+2, "Other keybindings can be found on the Github repo readme:".to_string()); - let _ = welcome_win.write_wrap_line(row+1, "https://github.com/jeff-hughes/shellcaster".to_string()); + row = welcome_win.write_wrap_line( + row + 2, + "Other keybindings can be found on the Github repo readme:".to_string(), + ); + let _ = welcome_win.write_wrap_line( + row + 1, + "https://github.com/jeff-hughes/shellcaster".to_string(), + ); return welcome_win; } -} \ No newline at end of file +} diff --git a/src/ui/notification.rs b/src/ui/notification.rs index 20f1c92..bfe4990 100644 --- a/src/ui/notification.rs +++ b/src/ui/notification.rs @@ -1,7 +1,7 @@ -use std::time::{Instant, Duration}; +use std::time::{Duration, Instant}; -use super::colors::{Colors, ColorType}; -use pancurses::{Window, Input}; +use super::colors::{ColorType, Colors}; +use pancurses::{Input, Window}; /// Holds details of a notification message. #[derive(Debug, Clone, PartialEq)] @@ -19,7 +19,7 @@ impl Notification { return Self { message: message, error: error, - expiry: expiry + expiry: expiry, }; } } @@ -27,7 +27,7 @@ impl Notification { /// A struct handling the one-line message window at the bottom of the /// screen. Holds state about the size of the window as well as any /// persistent message text. -/// +/// /// The `msg_stack` holds a vector of all timed notifications, each /// pushed on the end of the stack. The last notification on the stack /// will be the one displayed; however, they will be removed from the @@ -48,11 +48,7 @@ pub struct NotifWin { impl NotifWin { /// Creates a new NotifWin. pub fn new(colors: Colors, total_rows: i32, total_cols: i32) -> Self { - let win = pancurses::newwin( - 1, - total_cols, - total_rows-1, - 0); + let win = pancurses::newwin(1, total_cols, total_rows - 1, 0); return Self { window: win, colors: colors, @@ -81,7 +77,7 @@ impl NotifWin { if &last_item != curr { self.display_notif(last_item.clone()); } - }, + } None => self.display_notif(last_item.clone()), }; self.current_msg = Some(last_item); @@ -93,7 +89,7 @@ impl NotifWin { if msg != curr { self.display_notif(msg.clone()); } - }, + } None => self.display_notif(msg.clone()), }; self.current_msg = Some(msg.clone()); @@ -112,12 +108,12 @@ impl NotifWin { /// input line. This returns the user's input; if the user cancels /// their input, the String will be empty. pub fn input_notif(&self, prefix: &str) -> String { - self.window.mv(self.total_rows-1, 0); + self.window.mv(self.total_rows - 1, 0); self.window.addstr(&prefix); self.window.keypad(true); self.window.refresh(); pancurses::curs_set(2); - + let mut inputs = String::new(); let mut cancelled = false; @@ -127,18 +123,15 @@ impl NotifWin { loop { match self.window.getch() { // Cancel input - Some(Input::KeyExit) | - Some(Input::Character('\u{1b}')) => { + Some(Input::KeyExit) | Some(Input::Character('\u{1b}')) => { cancelled = true; break; - }, + } // Complete input - Some(Input::KeyEnter) | - Some(Input::Character('\n')) => { + Some(Input::KeyEnter) | Some(Input::Character('\n')) => { break; - }, - Some(Input::KeyBackspace) | - Some(Input::Character('\u{7f}')) => { + } + Some(Input::KeyBackspace) | Some(Input::Character('\u{7f}')) => { if current_x > min_x { current_x -= 1; cursor_x -= 1; @@ -146,32 +139,32 @@ impl NotifWin { self.window.mv(0, cursor_x); self.window.delch(); } - }, + } Some(Input::KeyDC) => { if cursor_x < current_x { let _ = inputs.remove((cursor_x as usize) - prefix.len()); self.window.delch(); } - }, + } Some(Input::KeyLeft) => { if cursor_x > min_x { cursor_x -= 1; self.window.mv(0, cursor_x); } - }, + } Some(Input::KeyRight) => { if cursor_x < current_x { cursor_x += 1; self.window.mv(0, cursor_x); } - }, + } Some(Input::Character(c)) => { current_x += 1; cursor_x += 1; self.window.insch(c); self.window.mv(0, cursor_x); inputs.push(c); - }, + } Some(_) => (), None => (), } @@ -196,8 +189,13 @@ impl NotifWin { self.window.addstr(notif.message); if notif.error { - self.window.mvchgat(0, 0, -1, pancurses::A_BOLD, - self.colors.get(ColorType::Error)); + self.window.mvchgat( + 0, + 0, + -1, + pancurses::A_BOLD, + self.colors.get(ColorType::Error), + ); } self.window.refresh(); } @@ -207,7 +205,8 @@ impl NotifWin { /// presenting error messages, among other things. pub fn timed_notif(&mut self, message: String, duration: u64, error: bool) { let expiry = Instant::now() + Duration::from_millis(duration); - self.msg_stack.push(Notification::new(message, error, Some(expiry))); + self.msg_stack + .push(Notification::new(message, error, Some(expiry))); } /// Adds a notification that will stay on screen indefinitely. Must @@ -245,15 +244,12 @@ impl NotifWin { // but c'est la vie let oldwin = std::mem::replace( &mut self.window, - pancurses::newwin( - 1, - total_cols, - total_rows-1, - 0)); + pancurses::newwin(1, total_cols, total_rows - 1, 0), + ); oldwin.delwin(); if let Some(curr) = &self.current_msg { self.display_notif(curr.clone()); } } -} \ No newline at end of file +} diff --git a/src/ui/panel.rs b/src/ui/panel.rs index f1587d4..0a523e7 100644 --- a/src/ui/panel.rs +++ b/src/ui/panel.rs @@ -1,7 +1,7 @@ -use pancurses::{Window, Attribute}; use chrono::{DateTime, Utc}; +use pancurses::{Attribute, Window}; -use super::{Colors, ColorType}; +use super::{ColorType, Colors}; /// Struct holding the raw data used for building the details panel. pub struct Details { @@ -31,14 +31,16 @@ pub struct Panel { impl Panel { /// Creates a new panel. - pub fn new(colors: Colors, - title: String, screen_pos: usize, n_row: i32, n_col: i32, start_y: i32, start_x: i32) -> Self { - - let panel_win = pancurses::newwin( - n_row, - n_col, - start_y, - start_x); + pub fn new( + colors: Colors, + title: String, + screen_pos: usize, + n_row: i32, + n_col: i32, + start_y: i32, + start_x: i32, + ) -> Self { + let panel_win = pancurses::newwin(n_row, n_col, start_y, start_x); return Panel { window: panel_win, @@ -83,7 +85,8 @@ impl Panel { top_left, pancurses::ACS_URCORNER(), bot_left, - pancurses::ACS_LRCORNER()); + pancurses::ACS_LRCORNER(), + ); self.window.mvaddstr(0, 2, self.title.clone()); } @@ -127,88 +130,99 @@ impl Panel { let max_row = self.get_rows(); let wrapper = textwrap::wrap_iter(&string, self.get_cols() as usize); for line in wrapper { - self.window.mvaddstr(self.abs_y(row), self.abs_x(0), line.clone()); + self.window + .mvaddstr(self.abs_y(row), self.abs_x(0), line.clone()); row += 1; if row >= max_row { break; } } - return row-1; + return row - 1; } /// Write the specific template used for the details panel. This is /// not the most elegant code, but it works. pub fn details_template(&self, start_y: i32, details: Details) { - let mut row = start_y-1; + let mut row = start_y - 1; self.window.attron(Attribute::Bold); // podcast title match details.pod_title { - Some(t) => row = self.write_wrap_line(row+1, t), - None => row = self.write_wrap_line(row+1, "No title".to_string()), + Some(t) => row = self.write_wrap_line(row + 1, t), + None => row = self.write_wrap_line(row + 1, "No title".to_string()), } // episode title match details.ep_title { - Some(t) => row = self.write_wrap_line(row+1, t), - None => row = self.write_wrap_line(row+1, "No title".to_string()), + Some(t) => row = self.write_wrap_line(row + 1, t), + None => row = self.write_wrap_line(row + 1, "No title".to_string()), } self.window.attroff(Attribute::Bold); - row += 1; // blank line + row += 1; // blank line // published date if let Some(date) = details.pubdate { - let new_row = self.write_wrap_line(row+1, - format!("Published: {}", date.format("%B %-d, %Y").to_string())); - self.change_attr(row+1, 0, 10, - pancurses::A_UNDERLINE, ColorType::Normal); + let new_row = self.write_wrap_line( + row + 1, + format!("Published: {}", date.format("%B %-d, %Y").to_string()), + ); + self.change_attr(row + 1, 0, 10, pancurses::A_UNDERLINE, ColorType::Normal); row = new_row; } // duration if let Some(dur) = details.duration { - let new_row = self.write_wrap_line(row+1, - format!("Duration: {}", dur)); - self.change_attr(row+1, 0, 9, - pancurses::A_UNDERLINE, ColorType::Normal); + let new_row = self.write_wrap_line(row + 1, format!("Duration: {}", dur)); + self.change_attr(row + 1, 0, 9, pancurses::A_UNDERLINE, ColorType::Normal); row = new_row; } // explicit if let Some(exp) = details.explicit { let new_row = if exp { - self.write_wrap_line(row+1, "Explicit: Yes".to_string()) + self.write_wrap_line(row + 1, "Explicit: Yes".to_string()) } else { - self.write_wrap_line(row+1, "Explicit: No".to_string()) + self.write_wrap_line(row + 1, "Explicit: No".to_string()) }; - self.change_attr(row+1, 0, 9, - pancurses::A_UNDERLINE, ColorType::Normal); + self.change_attr(row + 1, 0, 9, pancurses::A_UNDERLINE, ColorType::Normal); row = new_row; } - row += 1; // blank line + row += 1; // blank line // description match details.description { Some(desc) => { self.window.attron(Attribute::Bold); - row = self.write_wrap_line(row+1, "Description:".to_string()); + row = self.write_wrap_line(row + 1, "Description:".to_string()); self.window.attroff(Attribute::Bold); - let _row = self.write_wrap_line(row+1, desc); - }, + let _row = self.write_wrap_line(row + 1, desc); + } None => { - let _row = self.write_wrap_line(row+1, "No description.".to_string()); - }, + let _row = self.write_wrap_line(row + 1, "No description.".to_string()); + } } } /// Changes the attributes (text style and color) for a line of /// text. - pub fn change_attr(&self, y: i32, x: i32, nchars: i32, attr: pancurses::chtype, color: ColorType) { - self.window.mvchgat(self.abs_y(y), self.abs_x(x), nchars, - attr, self.colors.get(color)); + pub fn change_attr( + &self, + y: i32, + x: i32, + nchars: i32, + attr: pancurses::chtype, + color: ColorType, + ) { + self.window.mvchgat( + self.abs_y(y), + self.abs_x(x), + nchars, + attr, + self.colors.get(color), + ); } /// Updates window size @@ -222,21 +236,22 @@ impl Panel { // but c'est la vie let oldwin = std::mem::replace( &mut self.window, - pancurses::newwin(n_row, n_col, start_y, start_x)); + pancurses::newwin(n_row, n_col, start_y, start_x), + ); oldwin.delwin(); } /// Returns the effective number of rows (accounting for borders /// and margins). pub fn get_rows(&self) -> i32 { - return self.n_row - 2; // border on top and bottom + return self.n_row - 2; // border on top and bottom } - /// Returns the effective number of columns (accounting for + /// Returns the effective number of columns (accounting for /// borders and margins). pub fn get_cols(&self) -> i32 { - return self.n_col - 5; // 2 for border, 2 for margins, and 1 - // extra for some reason... + return self.n_col - 5; // 2 for border, 2 for margins, and 1 + // extra for some reason... } /// Calculates the y-value relative to the window rather than to the @@ -250,4 +265,4 @@ impl Panel { fn abs_x(&self, x: i32) -> i32 { return x + 2; } -} \ No newline at end of file +} From 841a37a376aeb9d3511bc0c18912cdaa045a8c2a Mon Sep 17 00:00:00 2001 From: thunderbiscuit Date: Sat, 22 Aug 2020 17:27:09 -0400 Subject: [PATCH 02/23] Running formatter with options set in rustfmt.toml --- rustfmt.toml | 8 +++++ src/config.rs | 77 ++++++++++++++++++------------------------ src/db.rs | 44 ++++++++++++------------ src/downloads.rs | 16 ++++----- src/feeds.rs | 6 ++-- src/main_controller.rs | 13 +++---- src/opml.rs | 4 ++- src/threadpool.rs | 4 +-- src/types.rs | 19 +++-------- src/ui/menu.rs | 3 +- src/ui/mock_panel.rs | 6 ++-- src/ui/mod.rs | 6 ++-- src/ui/panel.rs | 6 ++-- 13 files changed, 101 insertions(+), 111 deletions(-) create mode 100644 rustfmt.toml diff --git a/rustfmt.toml b/rustfmt.toml new file mode 100644 index 0000000..cfba586 --- /dev/null +++ b/rustfmt.toml @@ -0,0 +1,8 @@ +unstable_features = true +blank_lines_upper_bound = 2 +fn_params_layout = "Compressed" +newline_style = "Unix" +overflow_delimited_expr = true +struct_lit_single_line = false +use_try_shorthand = true +where_single_line = true diff --git a/src/config.rs b/src/config.rs index ba1ed6b..d8a45db 100644 --- a/src/config.rs +++ b/src/config.rs @@ -127,46 +127,39 @@ impl Config { fn config_with_defaults(config_toml: &ConfigFromToml) -> Config { // specify all default keybindings for actions let action_map: Vec<(&Option>, UserAction, Vec)> = vec![ - ( - &config_toml.keybindings.left, - UserAction::Left, - vec!["Left".to_string(), "h".to_string()], - ), - ( - &config_toml.keybindings.right, - UserAction::Right, - vec!["Right".to_string(), "l".to_string()], - ), - ( - &config_toml.keybindings.up, - UserAction::Up, - vec!["Up".to_string(), "k".to_string()], - ), - ( - &config_toml.keybindings.down, - UserAction::Down, - vec!["Down".to_string(), "j".to_string()], - ), + (&config_toml.keybindings.left, UserAction::Left, vec![ + "Left".to_string(), + "h".to_string(), + ]), + (&config_toml.keybindings.right, UserAction::Right, vec![ + "Right".to_string(), + "l".to_string(), + ]), + (&config_toml.keybindings.up, UserAction::Up, vec![ + "Up".to_string(), + "k".to_string(), + ]), + (&config_toml.keybindings.down, UserAction::Down, vec![ + "Down".to_string(), + "j".to_string(), + ]), ( &config_toml.keybindings.add_feed, UserAction::AddFeed, vec!["a".to_string()], ), - ( - &config_toml.keybindings.sync, - UserAction::Sync, - vec!["s".to_string()], - ), + (&config_toml.keybindings.sync, UserAction::Sync, vec![ + "s".to_string() + ]), ( &config_toml.keybindings.sync_all, UserAction::SyncAll, vec!["S".to_string()], ), - ( - &config_toml.keybindings.play, - UserAction::Play, - vec!["Enter".to_string(), "p".to_string()], - ), + (&config_toml.keybindings.play, UserAction::Play, vec![ + "Enter".to_string(), + "p".to_string(), + ]), ( &config_toml.keybindings.mark_played, UserAction::MarkPlayed, @@ -187,31 +180,25 @@ fn config_with_defaults(config_toml: &ConfigFromToml) -> Config { UserAction::DownloadAll, vec!["D".to_string()], ), - ( - &config_toml.keybindings.delete, - UserAction::Delete, - vec!["x".to_string()], - ), + (&config_toml.keybindings.delete, UserAction::Delete, vec![ + "x".to_string(), + ]), ( &config_toml.keybindings.delete_all, UserAction::DeleteAll, vec!["X".to_string()], ), - ( - &config_toml.keybindings.remove, - UserAction::Remove, - vec!["r".to_string()], - ), + (&config_toml.keybindings.remove, UserAction::Remove, vec![ + "r".to_string(), + ]), ( &config_toml.keybindings.remove_all, UserAction::RemoveAll, vec!["R".to_string()], ), - ( - &config_toml.keybindings.quit, - UserAction::Quit, - vec!["q".to_string()], - ), + (&config_toml.keybindings.quit, UserAction::Quit, vec![ + "q".to_string() + ]), ]; // for each action, if user preference is set, use that, otherwise, diff --git a/src/db.rs b/src/db.rs index 9baa9ae..f9cb95e 100644 --- a/src/db.rs +++ b/src/db.rs @@ -37,7 +37,9 @@ impl Database { db_path.push("data.db"); match Connection::open(db_path) { Ok(conn) => { - let db_conn = Database { conn: Some(conn) }; + let db_conn = Database { + conn: Some(conn), + }; db_conn.create(); { @@ -178,7 +180,8 @@ impl Database { pub fn insert_podcast( &self, podcast: PodcastNoId, - ) -> Result> { + ) -> Result> + { let conn = self.conn.as_ref().unwrap(); let _ = conn.execute( "INSERT INTO podcasts (title, url, description, author, @@ -217,7 +220,8 @@ impl Database { &self, podcast_id: i64, episode: &EpisodeNoId, - ) -> Result> { + ) -> Result> + { let conn = self.conn.as_ref().unwrap(); let pubdate = match episode.pubdate { @@ -248,7 +252,8 @@ impl Database { &self, episode_id: i64, path: &PathBuf, - ) -> Result<(), Box> { + ) -> Result<(), Box> + { let conn = self.conn.as_ref().unwrap(); let _ = conn.execute( @@ -264,10 +269,9 @@ impl Database { pub fn remove_file(&self, episode_id: i64) { let conn = self.conn.as_ref().unwrap(); let _ = conn - .execute( - "DELETE FROM files WHERE episode_id = ?;", - params![episode_id], - ) + .execute("DELETE FROM files WHERE episode_id = ?;", params![ + episode_id + ]) .unwrap(); } @@ -280,10 +284,9 @@ impl Database { let episodes = episode_list.join(", "); let _ = conn - .execute( - "DELETE FROM files WHERE episode_id = (?);", - params![episodes], - ) + .execute("DELETE FROM files WHERE episode_id = (?);", params![ + episodes + ]) .unwrap(); } @@ -306,7 +309,8 @@ impl Database { &self, pod_id: i64, podcast: PodcastNoId, - ) -> Result> { + ) -> Result> + { let conn = self.conn.as_ref().unwrap(); let _ = conn.execute( "UPDATE podcasts SET title = ?, url = ?, description = ?, @@ -421,10 +425,9 @@ impl Database { let conn = self.conn.as_ref().unwrap(); let _ = conn - .execute( - "UPDATE episodes SET played = ? WHERE id = ?;", - params![played, episode_id], - ) + .execute("UPDATE episodes SET played = ? WHERE id = ?;", params![ + played, episode_id + ]) .unwrap(); } @@ -435,10 +438,9 @@ impl Database { let conn = self.conn.as_ref().unwrap(); let _ = conn - .execute( - "UPDATE episodes SET hidden = ? WHERE id = ?;", - params![hide, episode_id], - ) + .execute("UPDATE episodes SET hidden = ? WHERE id = ?;", params![ + hide, episode_id + ]) .unwrap(); } diff --git a/src/downloads.rs b/src/downloads.rs index d347449..ceefbd9 100644 --- a/src/downloads.rs +++ b/src/downloads.rs @@ -38,7 +38,8 @@ pub fn download_list( max_retries: usize, threadpool: &Threadpool, tx_to_main: Sender, -) { +) +{ // parse episode details and push to queue for ep in episodes.into_iter() { let tx = tx_to_main.clone(); @@ -85,14 +86,11 @@ fn download_file(ep_data: EpData, dest: PathBuf, mut max_retries: usize) -> Down _ => "mp3", // assume .mp3 unless we figure out otherwise }; - let file_name = sanitize_with_options( - &ep_data.title, - Options { - truncate: true, - windows: true, // for simplicity, we'll just use Windows-friendly paths for everyone - replacement: "", - }, - ); + let file_name = sanitize_with_options(&ep_data.title, Options { + truncate: true, + windows: true, // for simplicity, we'll just use Windows-friendly paths for everyone + replacement: "", + }); let mut file_path = dest; file_path.push(format!("{}.{}", file_name, ext)); diff --git a/src/feeds.rs b/src/feeds.rs index 83ecb99..ec84acc 100644 --- a/src/feeds.rs +++ b/src/feeds.rs @@ -50,7 +50,8 @@ pub fn check_feed( max_retries: usize, threadpool: &Threadpool, tx_to_main: mpsc::Sender, -) { +) +{ threadpool.execute(move || match get_feed_data(feed.url.clone(), max_retries) { Ok(pod) => match feed.id { Some(id) => { @@ -73,7 +74,8 @@ pub fn check_feed( fn get_feed_data( url: String, mut max_retries: usize, -) -> Result> { +) -> Result> +{ let request: Result> = loop { let response = ureq::get(&url) .timeout_connect(5000) diff --git a/src/main_controller.rs b/src/main_controller.rs index 30db5fc..55d26fe 100644 --- a/src/main_controller.rs +++ b/src/main_controller.rs @@ -440,14 +440,11 @@ impl MainController { if !ep_data.is_empty() { // add directory for podcast, create if it does not exist - let dir_name = sanitize_with_options( - &pod_title, - Options { - truncate: true, - windows: true, // for simplicity, we'll just use Windows-friendly paths for everyone - replacement: "", - }, - ); + let dir_name = sanitize_with_options(&pod_title, Options { + truncate: true, + windows: true, // for simplicity, we'll just use Windows-friendly paths for everyone + replacement: "", + }); match self.create_podcast_dir(dir_name) { Ok(path) => { for ep in ep_data.iter() { diff --git a/src/opml.rs b/src/opml.rs index 3d3275d..b690c2e 100644 --- a/src/opml.rs +++ b/src/opml.rs @@ -64,6 +64,8 @@ pub fn export(podcasts: Vec) -> OPML { }); } - opml.body = Body { outlines: outlines }; + opml.body = Body { + outlines: outlines, + }; return opml; } diff --git a/src/threadpool.rs b/src/threadpool.rs index 4d47c46..d16718e 100644 --- a/src/threadpool.rs +++ b/src/threadpool.rs @@ -34,9 +34,7 @@ impl Threadpool { /// Adds a new job to the threadpool, passing closure to first /// available worker. pub fn execute(&self, func: F) - where - F: FnOnce() + Send + 'static, - { + where F: FnOnce() + Send + 'static { let job = Box::new(func); self.sender.send(JobMessage::NewJob(job)).unwrap(); } diff --git a/src/types.rs b/src/types.rs index 1944878..a75f4c9 100644 --- a/src/types.rs +++ b/src/types.rs @@ -257,8 +257,7 @@ pub struct EpisodeNoId { /// Arc>. #[derive(Debug)] pub struct LockVec -where - T: Clone + Menuable, +where T: Clone + Menuable { data: Arc>>, order: Arc>>, @@ -320,9 +319,7 @@ impl LockVec { /// alive, the function returns a Vec of the collected results, /// rather than an iterator. pub fn map(&self, mut f: F) -> Vec - where - F: FnMut(&T) -> B, - { + where F: FnMut(&T) -> B { let (map, order) = self.borrow(); return order.iter().map(|id| f(map.get(id).unwrap())).collect(); } @@ -330,9 +327,7 @@ impl LockVec { /// Maps a closure to a single element in the LockVec, specified by /// `id`. If there is no element `id`, this returns None. pub fn map_single(&self, id: i64, f: F) -> Option - where - F: FnOnce(&T) -> B, - { + where F: FnOnce(&T) -> B { let borrowed = self.borrow_map(); return match borrowed.get(&id) { Some(item) => Some(f(item)), @@ -344,9 +339,7 @@ impl LockVec { /// `index` (position order). If there is no element at that index, /// this returns None. pub fn map_single_by_index(&self, index: usize, f: F) -> Option - where - F: FnOnce(&T) -> B, - { + where F: FnOnce(&T) -> B { let order = self.borrow_order(); return match order.get(index) { Some(id) => self.map_single(*id, f), @@ -360,9 +353,7 @@ impl LockVec { /// alive, the function returns a Vec of the collected results, /// rather than an iterator. pub fn filter_map(&self, mut f: F) -> Vec - where - F: FnMut(&T) -> Option, - { + where F: FnMut(&T) -> Option { let (map, order) = self.borrow(); return order .iter() diff --git a/src/ui/menu.rs b/src/ui/menu.rs index 8d90697..11b4dfc 100644 --- a/src/ui/menu.rs +++ b/src/ui/menu.rs @@ -21,8 +21,7 @@ use crate::types::*; /// 0 and (n_row - 1) #[derive(Debug)] pub struct Menu -where - T: Clone + Menuable, +where T: Clone + Menuable { pub panel: Panel, pub items: LockVec, diff --git a/src/ui/mock_panel.rs b/src/ui/mock_panel.rs index 3d65e60..6bdb048 100644 --- a/src/ui/mock_panel.rs +++ b/src/ui/mock_panel.rs @@ -30,7 +30,8 @@ impl Panel { n_col: i32, _start_y: i32, _start_x: i32, - ) -> Self { + ) -> Self + { // we represent the window as a vector of Strings instead of // the pancurses window let panel_win = @@ -158,7 +159,8 @@ impl Panel { _nchars: i32, attr: pancurses::chtype, color: ColorType, - ) { + ) + { let current = &self.window[y as usize]; self.window[y as usize] = (current.0.clone(), attr, color); } diff --git a/src/ui/mod.rs b/src/ui/mod.rs index ff4b13c..9dfa582 100644 --- a/src/ui/mod.rs +++ b/src/ui/mod.rs @@ -92,7 +92,8 @@ impl<'a> UI<'a> { items: LockVec, rx_from_main: mpsc::Receiver, tx_to_main: mpsc::Sender, - ) -> thread::JoinHandle<()> { + ) -> thread::JoinHandle<()> + { return thread::spawn(move || { let mut ui = UI::new(&config, &items); ui.init(); @@ -724,7 +725,8 @@ impl<'a> UI<'a> { n_col: i32, start_y: i32, start_x: i32, - ) -> Panel { + ) -> Panel + { return Panel::new( colors, "Details".to_string(), diff --git a/src/ui/panel.rs b/src/ui/panel.rs index 0a523e7..c858184 100644 --- a/src/ui/panel.rs +++ b/src/ui/panel.rs @@ -39,7 +39,8 @@ impl Panel { n_col: i32, start_y: i32, start_x: i32, - ) -> Self { + ) -> Self + { let panel_win = pancurses::newwin(n_row, n_col, start_y, start_x); return Panel { @@ -215,7 +216,8 @@ impl Panel { nchars: i32, attr: pancurses::chtype, color: ColorType, - ) { + ) + { self.window.mvchgat( self.abs_y(y), self.abs_x(x), From 641fa80899ca58de050c50cea38282eccf9e56c3 Mon Sep 17 00:00:00 2001 From: Jeff Hughes Date: Sun, 23 Aug 2020 17:16:43 -0400 Subject: [PATCH 03/23] Reformat a few minor things, and add a few comments --- .gitignore | 3 +- src/config.rs | 97 +++++++++++++----------------------------------- src/db.rs | 1 + src/downloads.rs | 1 + src/feeds.rs | 2 + src/keymap.rs | 12 +++--- src/main.rs | 5 +++ src/opml.rs | 3 ++ src/types.rs | 8 +++- src/ui/colors.rs | 1 + src/ui/menu.rs | 4 +- src/ui/mod.rs | 2 + 12 files changed, 58 insertions(+), 81 deletions(-) diff --git a/.gitignore b/.gitignore index ebb995c..d4eefd9 100644 --- a/.gitignore +++ b/.gitignore @@ -2,4 +2,5 @@ **/*.rs.bk *.db /.meta -/build \ No newline at end of file +/build +/.vscode \ No newline at end of file diff --git a/src/config.rs b/src/config.rs index d8a45db..f52ea25 100644 --- a/src/config.rs +++ b/src/config.rs @@ -25,6 +25,7 @@ pub const EPISODE_PUBDATE_LENGTH: usize = 60; // display the details panel pub const DETAILS_PANEL_LENGTH: i32 = 135; + /// Holds information about user configuration of program. #[derive(Debug, Clone)] pub struct Config { @@ -69,6 +70,7 @@ struct KeybindingsFromToml { quit: Option>, } + impl Config { /// Given a file path, this reads a TOML config file and returns a /// Config struct with keybindings, etc. Inserts defaults if config @@ -126,79 +128,29 @@ impl Config { #[allow(clippy::type_complexity)] fn config_with_defaults(config_toml: &ConfigFromToml) -> Config { // specify all default keybindings for actions + #[rustfmt::skip] let action_map: Vec<(&Option>, UserAction, Vec)> = vec![ - (&config_toml.keybindings.left, UserAction::Left, vec![ - "Left".to_string(), - "h".to_string(), - ]), - (&config_toml.keybindings.right, UserAction::Right, vec![ - "Right".to_string(), - "l".to_string(), - ]), - (&config_toml.keybindings.up, UserAction::Up, vec![ - "Up".to_string(), - "k".to_string(), - ]), - (&config_toml.keybindings.down, UserAction::Down, vec![ - "Down".to_string(), - "j".to_string(), - ]), - ( - &config_toml.keybindings.add_feed, - UserAction::AddFeed, - vec!["a".to_string()], - ), - (&config_toml.keybindings.sync, UserAction::Sync, vec![ - "s".to_string() - ]), - ( - &config_toml.keybindings.sync_all, - UserAction::SyncAll, - vec!["S".to_string()], - ), - (&config_toml.keybindings.play, UserAction::Play, vec![ - "Enter".to_string(), - "p".to_string(), - ]), - ( - &config_toml.keybindings.mark_played, - UserAction::MarkPlayed, - vec!["m".to_string()], - ), - ( - &config_toml.keybindings.mark_all_played, - UserAction::MarkAllPlayed, - vec!["M".to_string()], - ), - ( - &config_toml.keybindings.download, - UserAction::Download, - vec!["d".to_string()], - ), - ( - &config_toml.keybindings.download_all, - UserAction::DownloadAll, - vec!["D".to_string()], - ), - (&config_toml.keybindings.delete, UserAction::Delete, vec![ - "x".to_string(), - ]), - ( - &config_toml.keybindings.delete_all, - UserAction::DeleteAll, - vec!["X".to_string()], - ), - (&config_toml.keybindings.remove, UserAction::Remove, vec![ - "r".to_string(), - ]), - ( - &config_toml.keybindings.remove_all, - UserAction::RemoveAll, - vec!["R".to_string()], - ), - (&config_toml.keybindings.quit, UserAction::Quit, vec![ - "q".to_string() - ]), + (&config_toml.keybindings.left, UserAction::Left, vec!["Left".to_string(), "h".to_string()]), + (&config_toml.keybindings.right, UserAction::Right, vec!["Right".to_string(), "l".to_string()]), + (&config_toml.keybindings.up, UserAction::Up, vec!["Up".to_string(), "k".to_string()]), + (&config_toml.keybindings.down, UserAction::Down, vec!["Down".to_string(), "j".to_string()]), + + (&config_toml.keybindings.add_feed, UserAction::AddFeed, vec!["a".to_string()]), + (&config_toml.keybindings.sync, UserAction::Sync, vec!["s".to_string()]), + (&config_toml.keybindings.sync_all, UserAction::SyncAll, vec!["S".to_string()]), + + (&config_toml.keybindings.play, UserAction::Play, vec!["Enter".to_string(), "p".to_string()]), + (&config_toml.keybindings.mark_played, UserAction::MarkPlayed, vec!["m".to_string()]), + (&config_toml.keybindings.mark_all_played, UserAction::MarkAllPlayed, vec!["M".to_string()]), + + (&config_toml.keybindings.download, UserAction::Download, vec!["d".to_string()]), + (&config_toml.keybindings.download_all, UserAction::DownloadAll, vec!["D".to_string()]), + (&config_toml.keybindings.delete, UserAction::Delete, vec!["x".to_string()]), + (&config_toml.keybindings.delete_all, UserAction::DeleteAll, vec!["X".to_string()]), + (&config_toml.keybindings.remove, UserAction::Remove, vec!["r".to_string()]), + (&config_toml.keybindings.remove_all, UserAction::RemoveAll, vec!["R".to_string()]), + + (&config_toml.keybindings.quit, UserAction::Quit, vec!["q".to_string()]), ]; // for each action, if user preference is set, use that, otherwise, @@ -242,6 +194,7 @@ fn config_with_defaults(config_toml: &ConfigFromToml) -> Config { }; } + /// Helper function that takes an (optionally specified) user directory /// and an (OS-dependent) default directory, expands any environment /// variables, ~ alias, etc. Returns a PathBuf. Panics if environment diff --git a/src/db.rs b/src/db.rs index f9cb95e..320f037 100644 --- a/src/db.rs +++ b/src/db.rs @@ -14,6 +14,7 @@ lazy_static! { static ref RE_ARTICLES: Regex = Regex::new(r"^(a|an|the) ").unwrap(); } + pub struct SyncResult { pub added: Vec, pub updated: Vec, diff --git a/src/downloads.rs b/src/downloads.rs index ceefbd9..a29807e 100644 --- a/src/downloads.rs +++ b/src/downloads.rs @@ -51,6 +51,7 @@ pub fn download_list( } } + /// Downloads a file to a local filepath, returning DownloadMsg variant /// indicating success or failure. fn download_file(ep_data: EpData, dest: PathBuf, mut max_retries: usize) -> DownloadMsg { diff --git a/src/feeds.rs b/src/feeds.rs index ec84acc..c2d6688 100644 --- a/src/feeds.rs +++ b/src/feeds.rs @@ -104,6 +104,7 @@ fn get_feed_data( }; } + /// Given a Channel with the RSS feed data, this parses the data about a /// podcast and its episodes and returns a Podcast. There are existing /// specifications for podcast RSS feeds that a feed should adhere to, but @@ -281,6 +282,7 @@ fn regex_to_int(re_match: Match) -> Result { mstr.parse::() } + // TESTS ----------------------------------------------------------------- #[cfg(test)] mod tests { diff --git a/src/keymap.rs b/src/keymap.rs index b849c65..6ad6ee7 100644 --- a/src/keymap.rs +++ b/src/keymap.rs @@ -68,6 +68,8 @@ impl Keybindings { } } + /// Returns a Vec with all of the keys mapped to a particular user + /// action. pub fn keys_for_action(&self, action: UserAction) -> Vec { return self .map @@ -83,11 +85,11 @@ impl Keybindings { } } -/// Helper function converting a pancurses Input object to a unique string -/// representing that input. -/// This function is a bit ridiculous, given that 95% of keyboards probably -/// don't even have half these special keys, but at any rate...they're -/// mapped, if anyone wants them. +/// Helper function converting a pancurses Input object to a unique +/// string representing that input. +/// This function is a bit ridiculous, given that 95% of keyboards +/// probably don't even have half these special keys, but at any rate... +/// they're mapped, if anyone wants them. pub fn input_to_str(input: Input) -> Option { let mut tmp = [0; 4]; let code = match input { diff --git a/src/main.rs b/src/main.rs index 311c9e1..c15a638 100644 --- a/src/main.rs +++ b/src/main.rs @@ -122,6 +122,7 @@ fn main() { process::exit(1); } + match args.subcommand() { // SYNC SUBCOMMAND ---------------------------------------------- ("sync", Some(sub_args)) => { @@ -150,6 +151,7 @@ fn main() { } } + /// Gets the path to the config file if one is specified in the command- /// line arguments, or else returns the default config path for the /// user's operating system. @@ -175,6 +177,7 @@ fn get_config_path(config: Option<&str>) -> Option { }; } + /// Synchronizes RSS feed data for all podcasts, without setting up a UI. fn sync_podcasts(db_path: &PathBuf, config: Config, args: &clap::ArgMatches) { let db_inst = Database::connect(db_path); @@ -240,6 +243,7 @@ fn sync_podcasts(db_path: &PathBuf, config: Config, args: &clap::ArgMatches) { } } + /// Imports a list of podcasts from OPML format, either reading from a /// file or from stdin. If the `replace` flag is set, this replaces all /// existing data in the database. @@ -373,6 +377,7 @@ fn import(db_path: &PathBuf, config: Config, args: &clap::ArgMatches) { } } + /// Exports all podcasts to OPML format, either printing to stdout or /// exporting to a file. fn export(db_path: &PathBuf, args: &clap::ArgMatches) { diff --git a/src/opml.rs b/src/opml.rs index b690c2e..a845dc9 100644 --- a/src/opml.rs +++ b/src/opml.rs @@ -4,6 +4,8 @@ use opml::{Body, Head, Outline, OPML}; use crate::feeds::PodcastFeed; use crate::types::*; +/// Import a list of podcast feeds from an OPML file. Supports +/// v1.0, v1.1, and v2.0 OPML files. pub fn import(xml: String) -> Result, String> { return match OPML::new(&xml) { Err(err) => Err(err), @@ -42,6 +44,7 @@ pub fn import(xml: String) -> Result, String> { }; } +/// Converts the current set of podcast feeds to the OPML format pub fn export(podcasts: Vec) -> OPML { let date = Utc::now(); let mut opml = OPML::default(); diff --git a/src/types.rs b/src/types.rs index a75f4c9..ad86b61 100644 --- a/src/types.rs +++ b/src/types.rs @@ -115,10 +115,11 @@ impl Ord for Podcast { } } + /// Struct holding data about an individual podcast episode. Most of this /// is metadata, but if the episode has been downloaded to the local -/// machine, the filepath will be included here as well. `played` indicates -/// whether the podcast has been marked as played or unplayed. +/// machine, the filepath will be included here as well. `played` +/// indicates whether the podcast has been marked as played or unplayed. #[derive(Debug, Clone)] pub struct Episode { pub id: i64, @@ -225,6 +226,7 @@ impl Menuable for Episode { } } + /// Struct holding data about an individual podcast feed, before it has /// been inserted into the database. This includes a /// (possibly empty) vector of episodes. @@ -250,6 +252,7 @@ pub struct EpisodeNoId { pub duration: Option, } + /// Struct used to hold a vector of data inside a reference-counted /// mutex, to allow for multiple owners of mutable data. /// Primarily, the LockVec is used to provide methods that abstract @@ -417,6 +420,7 @@ impl LockVec { } } + /// Overarching Message enum that allows multiple threads to communicate /// back to the main thread with a single enum type. #[derive(Debug)] diff --git a/src/ui/colors.rs b/src/ui/colors.rs index 2a5ce4a..a095a23 100644 --- a/src/ui/colors.rs +++ b/src/ui/colors.rs @@ -32,6 +32,7 @@ impl Colors { } } + /// Sets up hashmap for ColorTypes in app, initiates color palette, and /// sets up ncurses color pairs. pub fn set_colors() -> Colors { diff --git a/src/ui/menu.rs b/src/ui/menu.rs index 11b4dfc..341224d 100644 --- a/src/ui/menu.rs +++ b/src/ui/menu.rs @@ -218,6 +218,7 @@ impl Menu { } } + impl Menu { /// Returns a cloned reference to the list of episodes from the /// currently selected podcast. @@ -267,7 +268,8 @@ impl Menu { } } -// TESTS ----------------------------------------------------------------- + +// TESTS ---------------------------------------------------------------- #[cfg(test)] mod tests { use super::*; diff --git a/src/ui/mod.rs b/src/ui/mod.rs index 9dfa582..8b7eb98 100644 --- a/src/ui/mod.rs +++ b/src/ui/mod.rs @@ -36,6 +36,7 @@ lazy_static! { static ref RE_MULT_LINE_BREAKS: Regex = Regex::new(r"((\r\n)|\r|\n){3,}").unwrap(); } + /// Enum used for communicating back to the main controller after user /// input has been captured by the UI. usize values always represent the /// selected podcast, and (if applicable), the selected episode, in that @@ -66,6 +67,7 @@ enum ActiveMenu { EpisodeMenu, } + /// Struct containing all interface elements of the TUI. Functionally, it /// encapsulates the pancurses windows, and holds data about the size of /// the screen. From 03b975cf080fa8c96205b3455679b6fef77b6615 Mon Sep 17 00:00:00 2001 From: Jeff Hughes Date: Sun, 23 Aug 2020 17:45:36 -0400 Subject: [PATCH 04/23] Add some helper functions for Unicode strings --- src/types.rs | 69 +++++++++++++++++++++++++--------------------------- 1 file changed, 33 insertions(+), 36 deletions(-) diff --git a/src/types.rs b/src/types.rs index ad86b61..2423703 100644 --- a/src/types.rs +++ b/src/types.rs @@ -68,26 +68,12 @@ impl Menuable for Podcast { let meta_str = format!("({}/{})", self.num_unplayed(), self.episodes.len()); title_length = length - meta_str.chars().count(); - let out = self - .title - .graphemes(true) - .take(title_length) - .collect::(); + let out = self.title.substr(0, title_length); - return format!( - "{} {:>width$}", - out, - meta_str, - width = length - out.graphemes(true).count() - ); + return format!("{} {:>width$}", out, meta_str, width = length - out.len()); // this pads spaces between title and totals } else { - let out = self - .title - .graphemes(true) - .take(title_length) - .collect::(); - return out; + return self.title.substr(0, title_length); } } @@ -160,16 +146,12 @@ impl Menuable for Episode { fn get_title(&self, length: usize) -> String { let out = match self.path { Some(_) => { - let title = self - .title - .graphemes(true) - .take(length - 4) - .collect::(); + let title = self.title.substr(0, length - 4); format!("[D] {}", title) } - None => self.title.graphemes(true).take(length).collect::(), + None => self.title.substr(0, length), }; - let out_len = out.graphemes(true).count(); + let out_len = out.len(); if length > crate::config::EPISODE_PUBDATE_LENGTH { let dur = self.format_duration(); let meta_dur = format!("[{}]", dur); @@ -180,10 +162,7 @@ impl Menuable for Episode { let meta_str = format!("({}) {}", pd, meta_dur); let added_len = meta_str.chars().count(); - let out_added = out - .graphemes(true) - .take(length - added_len) - .collect::(); + let out_added = out.substr(0, length - added_len); return format!( "{} {:>width$}", out_added, @@ -192,10 +171,7 @@ impl Menuable for Episode { ); } else { // just print duration - let out_added = out - .graphemes(true) - .take(length - meta_dur.chars().count()) - .collect::(); + let out_added = out.substr(0, length - meta_dur.chars().count()); return format!( "{} {:>width$}", out_added, @@ -206,10 +182,7 @@ impl Menuable for Episode { } else if length > crate::config::EPISODE_DURATION_LENGTH { let dur = self.format_duration(); let meta_dur = format!("[{}]", dur); - let out_added = out - .graphemes(true) - .take(length - meta_dur.chars().count()) - .collect::(); + let out_added = out.substr(0, length - meta_dur.chars().count()); return format!( "{} {:>width$}", out_added, @@ -429,3 +402,27 @@ pub enum Message { Feed(FeedMsg), Dl(DownloadMsg), } + + +/// Some helper functions for dealing with Unicode strings. +pub trait StringUtils { + fn substr(&self, start: usize, length: usize) -> String; + fn len(&self) -> usize; +} + +impl StringUtils for String { + /// Takes a slice of the String, properly separated at Unicode + /// grapheme boundaries. Returns a new String. + fn substr(&self, start: usize, length: usize) -> String { + return self + .graphemes(true) + .skip(start) + .take(length) + .collect::(); + } + + /// Counts the total number of Unicode graphemes in the String. + fn len(&self) -> usize { + return self.graphemes(true).count(); + } +} From 7319fb05bccf393e443e1ba3dd4c88ee82b7e060 Mon Sep 17 00:00:00 2001 From: Jeff Hughes Date: Sun, 23 Aug 2020 17:52:19 -0400 Subject: [PATCH 05/23] Fix bug introduced with previous commit --- src/types.rs | 13 +++++++++---- 1 file changed, 9 insertions(+), 4 deletions(-) diff --git a/src/types.rs b/src/types.rs index 2423703..04efb2a 100644 --- a/src/types.rs +++ b/src/types.rs @@ -70,7 +70,12 @@ impl Menuable for Podcast { let out = self.title.substr(0, title_length); - return format!("{} {:>width$}", out, meta_str, width = length - out.len()); + return format!( + "{} {:>width$}", + out, + meta_str, + width = length - out.grapheme_len() + ); // this pads spaces between title and totals } else { return self.title.substr(0, title_length); @@ -151,7 +156,7 @@ impl Menuable for Episode { } None => self.title.substr(0, length), }; - let out_len = out.len(); + let out_len = out.grapheme_len(); if length > crate::config::EPISODE_PUBDATE_LENGTH { let dur = self.format_duration(); let meta_dur = format!("[{}]", dur); @@ -407,7 +412,7 @@ pub enum Message { /// Some helper functions for dealing with Unicode strings. pub trait StringUtils { fn substr(&self, start: usize, length: usize) -> String; - fn len(&self) -> usize; + fn grapheme_len(&self) -> usize; } impl StringUtils for String { @@ -422,7 +427,7 @@ impl StringUtils for String { } /// Counts the total number of Unicode graphemes in the String. - fn len(&self) -> usize { + fn grapheme_len(&self) -> usize { return self.graphemes(true).count(); } } From b3a1bf7fcb10881c0d455365447e6970575760ca Mon Sep 17 00:00:00 2001 From: a-kenji Date: Wed, 26 Aug 2020 13:59:48 +0200 Subject: [PATCH 06/23] Add update_details_panel for right/left The details panel is "detaching" from the episode panel this still happens, but now it is consistent with up/down and it happens for a short amount of time. --- src/ui/mod.rs | 2 ++ 1 file changed, 2 insertions(+) diff --git a/src/ui/mod.rs b/src/ui/mod.rs index 8b7eb98..1dde112 100644 --- a/src/ui/mod.rs +++ b/src/ui/mod.rs @@ -395,6 +395,7 @@ impl<'a> UI<'a> { self.episode_menu.deactivate(); } } + self.update_details_panel(); } } @@ -408,6 +409,7 @@ impl<'a> UI<'a> { } ActiveMenu::EpisodeMenu => (), } + self.update_details_panel(); } } From 107948f32c0244535439a4f2e598207642d516ff Mon Sep 17 00:00:00 2001 From: Jeff Hughes Date: Sun, 30 Aug 2020 17:40:46 -0400 Subject: [PATCH 07/23] Create more standardized support for popup windows Included in this is a help menu showing the keybindings available for all available user actions. --- config.toml | 1 + src/config.rs | 3 + src/keymap.rs | 1 + src/ui/mod.rs | 710 ++++++++++++++++++++++++++---------------------- src/ui/popup.rs | 283 +++++++++++++++++++ 5 files changed, 667 insertions(+), 331 deletions(-) create mode 100644 src/ui/popup.rs diff --git a/config.toml b/config.toml index 12da92e..ffc8dea 100644 --- a/config.toml +++ b/config.toml @@ -72,4 +72,5 @@ delete_all = [ "X" ] remove = [ "r" ] remove_all = [ "R" ] +help = [ "?" ] quit = [ "q" ] \ No newline at end of file diff --git a/src/config.rs b/src/config.rs index f52ea25..4f65526 100644 --- a/src/config.rs +++ b/src/config.rs @@ -67,6 +67,7 @@ struct KeybindingsFromToml { delete_all: Option>, remove: Option>, remove_all: Option>, + help: Option>, quit: Option>, } @@ -106,6 +107,7 @@ impl Config { delete_all: None, remove: None, remove_all: None, + help: None, quit: None, }; config_toml = ConfigFromToml { @@ -150,6 +152,7 @@ fn config_with_defaults(config_toml: &ConfigFromToml) -> Config { (&config_toml.keybindings.remove, UserAction::Remove, vec!["r".to_string()]), (&config_toml.keybindings.remove_all, UserAction::RemoveAll, vec!["R".to_string()]), + (&config_toml.keybindings.help, UserAction::Help, vec!["?".to_string()]), (&config_toml.keybindings.quit, UserAction::Quit, vec!["q".to_string()]), ]; diff --git a/src/keymap.rs b/src/keymap.rs index 6ad6ee7..e4aa4b6 100644 --- a/src/keymap.rs +++ b/src/keymap.rs @@ -25,6 +25,7 @@ pub enum UserAction { Remove, RemoveAll, + Help, Quit, } diff --git a/src/ui/mod.rs b/src/ui/mod.rs index 8b7eb98..285cada 100644 --- a/src/ui/mod.rs +++ b/src/ui/mod.rs @@ -9,11 +9,13 @@ mod panel; mod colors; mod menu; mod notification; +mod popup; use self::colors::{ColorType, Colors}; use self::menu::Menu; use self::notification::NotifWin; use self::panel::{Details, Panel}; +use self::popup::PopupWin; use lazy_static::lazy_static; use pancurses::{Input, Window}; @@ -83,7 +85,7 @@ pub struct UI<'a> { active_menu: ActiveMenu, details_panel: Option, notif_win: NotifWin, - welcome_win: Option, + popup_win: PopupWin<'a>, } impl<'a> UI<'a> { @@ -210,18 +212,7 @@ impl<'a> UI<'a> { }; let notif_win = NotifWin::new(colors.clone(), n_row, n_col); - - // welcome screen if user does not have any podcasts yet - let welcome_win = if items.is_empty() { - Some(UI::make_welcome_win( - colors.clone(), - &config.keybindings, - n_row - 1, - n_col, - )) - } else { - None - }; + let popup_win = PopupWin::new(colors.clone(), &config.keybindings, n_row, n_col); return UI { stdscr, @@ -234,7 +225,7 @@ impl<'a> UI<'a> { active_menu: ActiveMenu::PodcastMenu, details_panel: details_panel, notif_win: notif_win, - welcome_win: welcome_win, + popup_win: popup_win, }; } @@ -247,9 +238,9 @@ impl<'a> UI<'a> { self.episode_menu.init(); self.update_details_panel(); - if self.welcome_win.is_some() { - let ww = self.welcome_win.as_mut().unwrap(); - ww.refresh(); + // welcome screen if user does not have any podcasts yet + if self.podcast_menu.items.is_empty() { + self.popup_win.spawn_welcome_win(); } } @@ -263,340 +254,347 @@ impl<'a> UI<'a> { /// then passes this data back to the main controller. #[allow(clippy::cognitive_complexity)] pub fn getch(&mut self) -> UiMsg { - match self.stdscr.getch() { - Some(Input::KeyResize) => { - pancurses::resize_term(0, 0); - let (n_row, n_col) = self.stdscr.get_max_yx(); - self.n_row = n_row; - self.n_col = n_col; - - let (pod_col, ep_col, det_col) = Self::calculate_sizes(n_col); - - self.podcast_menu.resize(n_row - 1, pod_col, 0, 0); - self.episode_menu.resize(n_row - 1, ep_col, 0, pod_col - 1); - - if self.details_panel.is_some() { - if det_col > 0 { - let det = self.details_panel.as_mut().unwrap(); - det.resize(n_row - 1, det_col, 0, pod_col + ep_col - 2); - } else { - self.details_panel = None; - } - } else if det_col > 0 { - self.details_panel = Some(Self::make_details_panel( - self.colors.clone(), - n_row - 1, - det_col, - 0, - pod_col + ep_col - 2, - )); - } - + // if there is a popup window active (apart from the welcome + // window which takes no input), then redirect user input there + if self.popup_win.is_non_welcome_popup_active() { + self.popup_win.handle_input(self.stdscr.getch()); + + // need to check if popup window is still active, as handling + // character input above may involve closing the popup window + if !self.popup_win.is_popup_active() { self.stdscr.refresh(); self.update_menus(); - - match self.active_menu { - ActiveMenu::PodcastMenu => self.podcast_menu.activate(), - ActiveMenu::EpisodeMenu => { - self.podcast_menu.activate(); - self.episode_menu.activate(); - } - } - if self.details_panel.is_some() { self.update_details_panel(); } - - // resize welcome window, if it exists - if self.welcome_win.is_some() { - let _ = std::mem::replace( - &mut self.welcome_win, - Some(UI::make_welcome_win( + } + } else { + match self.stdscr.getch() { + Some(Input::KeyResize) => { + pancurses::resize_term(0, 0); + let (n_row, n_col) = self.stdscr.get_max_yx(); + self.n_row = n_row; + self.n_col = n_col; + + let (pod_col, ep_col, det_col) = Self::calculate_sizes(n_col); + + self.podcast_menu.resize(n_row - 1, pod_col, 0, 0); + self.episode_menu.resize(n_row - 1, ep_col, 0, pod_col - 1); + + if self.details_panel.is_some() { + if det_col > 0 { + let det = self.details_panel.as_mut().unwrap(); + det.resize(n_row - 1, det_col, 0, pod_col + ep_col - 2); + } else { + self.details_panel = None; + } + } else if det_col > 0 { + self.details_panel = Some(Self::make_details_panel( self.colors.clone(), - &self.keymap, n_row - 1, - n_col, - )), - ); + det_col, + 0, + pod_col + ep_col - 2, + )); + } - let ww = self.welcome_win.as_mut().unwrap(); - ww.refresh(); - } + self.stdscr.refresh(); + self.update_menus(); - self.notif_win.resize(n_row, n_col); - self.stdscr.refresh(); - } + match self.active_menu { + ActiveMenu::PodcastMenu => self.podcast_menu.activate(), + ActiveMenu::EpisodeMenu => { + self.podcast_menu.activate(); + self.episode_menu.activate(); + } + } - Some(input) => { - let (curr_pod_id, curr_ep_id) = self.get_current_ids(); + if self.details_panel.is_some() { + self.update_details_panel(); + } - // get rid of the "welcome" window once the podcast list - // is no longer empty - if self.welcome_win.is_some() && !self.podcast_menu.items.len() > 0 { - self.welcome_win = None; + self.popup_win.resize(n_row, n_col); + self.notif_win.resize(n_row, n_col); + self.stdscr.refresh(); } - match self.keymap.get_from_input(input) { - Some(UserAction::Down) => { - match self.active_menu { - ActiveMenu::PodcastMenu => { - if curr_pod_id.is_some() { - self.podcast_menu.scroll(1); + Some(input) => { + let (curr_pod_id, curr_ep_id) = self.get_current_ids(); - self.episode_menu.top_row = 0; - self.episode_menu.selected = 0; + // get rid of the "welcome" window once the podcast list + // is no longer empty + if self.popup_win.welcome_win && !self.podcast_menu.items.is_empty() { + self.popup_win.turn_off_welcome_win(); + } - // update episodes menu with new list - self.episode_menu.items = self.podcast_menu.get_episodes(); - self.episode_menu.update_items(); - self.update_details_panel(); + match self.keymap.get_from_input(input) { + Some(UserAction::Down) => { + match self.active_menu { + ActiveMenu::PodcastMenu => { + if curr_pod_id.is_some() { + self.podcast_menu.scroll(1); + + self.episode_menu.top_row = 0; + self.episode_menu.selected = 0; + + // update episodes menu with new list + self.episode_menu.items = self.podcast_menu.get_episodes(); + self.episode_menu.update_items(); + self.update_details_panel(); + } } - } - ActiveMenu::EpisodeMenu => { - if curr_ep_id.is_some() { - self.episode_menu.scroll(1); - self.update_details_panel(); + ActiveMenu::EpisodeMenu => { + if curr_ep_id.is_some() { + self.episode_menu.scroll(1); + self.update_details_panel(); + } } } } - } - Some(UserAction::Up) => { - match self.active_menu { - ActiveMenu::PodcastMenu => { - if curr_pod_id.is_some() { - self.podcast_menu.scroll(-1); + Some(UserAction::Up) => { + match self.active_menu { + ActiveMenu::PodcastMenu => { + if curr_pod_id.is_some() { + self.podcast_menu.scroll(-1); - self.episode_menu.top_row = 0; - self.episode_menu.selected = 0; + self.episode_menu.top_row = 0; + self.episode_menu.selected = 0; - // update episodes menu with new list - self.episode_menu.items = self.podcast_menu.get_episodes(); - self.episode_menu.update_items(); - self.update_details_panel(); + // update episodes menu with new list + self.episode_menu.items = self.podcast_menu.get_episodes(); + self.episode_menu.update_items(); + self.update_details_panel(); + } } - } - ActiveMenu::EpisodeMenu => { - if curr_pod_id.is_some() { - self.episode_menu.scroll(-1); - self.update_details_panel(); + ActiveMenu::EpisodeMenu => { + if curr_pod_id.is_some() { + self.episode_menu.scroll(-1); + self.update_details_panel(); + } } } } - } - Some(UserAction::Left) => { - if curr_pod_id.is_some() { - match self.active_menu { - ActiveMenu::PodcastMenu => (), - ActiveMenu::EpisodeMenu => { - self.active_menu = ActiveMenu::PodcastMenu; - self.podcast_menu.activate(); - self.episode_menu.deactivate(); + Some(UserAction::Left) => { + if curr_pod_id.is_some() { + match self.active_menu { + ActiveMenu::PodcastMenu => (), + ActiveMenu::EpisodeMenu => { + self.active_menu = ActiveMenu::PodcastMenu; + self.podcast_menu.activate(); + self.episode_menu.deactivate(); + } } } } - } - Some(UserAction::Right) => { - if curr_pod_id.is_some() && curr_ep_id.is_some() { - match self.active_menu { - ActiveMenu::PodcastMenu => { - self.active_menu = ActiveMenu::EpisodeMenu; - self.podcast_menu.deactivate(); - self.episode_menu.activate(); + Some(UserAction::Right) => { + if curr_pod_id.is_some() && curr_ep_id.is_some() { + match self.active_menu { + ActiveMenu::PodcastMenu => { + self.active_menu = ActiveMenu::EpisodeMenu; + self.podcast_menu.deactivate(); + self.episode_menu.activate(); + } + ActiveMenu::EpisodeMenu => (), } - ActiveMenu::EpisodeMenu => (), } } - } - Some(UserAction::AddFeed) => { - let url = &self.spawn_input_notif("Feed URL: "); - if !url.is_empty() { - return UiMsg::AddFeed(url.to_string()); + Some(UserAction::AddFeed) => { + let url = &self.spawn_input_notif("Feed URL: "); + if !url.is_empty() { + return UiMsg::AddFeed(url.to_string()); + } } - } - Some(UserAction::Sync) => { - if let Some(pod_id) = curr_pod_id { - return UiMsg::Sync(pod_id); - } - } - Some(UserAction::SyncAll) => { - if curr_pod_id.is_some() { - return UiMsg::SyncAll; + Some(UserAction::Sync) => { + if let Some(pod_id) = curr_pod_id { + return UiMsg::Sync(pod_id); + } } - } - Some(UserAction::Play) => { - if let Some(pod_id) = curr_pod_id { - if let Some(ep_id) = curr_ep_id { - return UiMsg::Play(pod_id, ep_id); + Some(UserAction::SyncAll) => { + if curr_pod_id.is_some() { + return UiMsg::SyncAll; } } - } - Some(UserAction::MarkPlayed) => match self.active_menu { - ActiveMenu::PodcastMenu => (), - ActiveMenu::EpisodeMenu => { + Some(UserAction::Play) => { if let Some(pod_id) = curr_pod_id { if let Some(ep_id) = curr_ep_id { - if let Some(played) = self - .episode_menu - .items - .map_single(ep_id, |ep| ep.is_played()) - { - return UiMsg::MarkPlayed(pod_id, ep_id, !played); - } + return UiMsg::Play(pod_id, ep_id); } } } - }, - Some(UserAction::MarkAllPlayed) => { - // if there are any unplayed episodes, MarkAllPlayed - // will convert all to played; if all are played - // already, only then will it convert all to unplayed - if let Some(pod_id) = curr_pod_id { - if let Some(played) = self - .podcast_menu - .items - .map_single(pod_id, |pod| pod.is_played()) - { - return UiMsg::MarkAllPlayed(pod_id, !played); + Some(UserAction::MarkPlayed) => match self.active_menu { + ActiveMenu::PodcastMenu => (), + ActiveMenu::EpisodeMenu => { + if let Some(pod_id) = curr_pod_id { + if let Some(ep_id) = curr_ep_id { + if let Some(played) = self + .episode_menu + .items + .map_single(ep_id, |ep| ep.is_played()) + { + return UiMsg::MarkPlayed(pod_id, ep_id, !played); + } + } + } } - } - } - - Some(UserAction::Download) => { - if let Some(pod_id) = curr_pod_id { - if let Some(ep_id) = curr_ep_id { - return UiMsg::Download(pod_id, ep_id); + }, + Some(UserAction::MarkAllPlayed) => { + // if there are any unplayed episodes, MarkAllPlayed + // will convert all to played; if all are played + // already, only then will it convert all to unplayed + if let Some(pod_id) = curr_pod_id { + if let Some(played) = self + .podcast_menu + .items + .map_single(pod_id, |pod| pod.is_played()) + { + return UiMsg::MarkAllPlayed(pod_id, !played); + } } } - } - - Some(UserAction::DownloadAll) => { - if let Some(pod_id) = curr_pod_id { - return UiMsg::DownloadAll(pod_id); - } - } - Some(UserAction::Delete) => match self.active_menu { - ActiveMenu::PodcastMenu => (), - ActiveMenu::EpisodeMenu => { + Some(UserAction::Download) => { if let Some(pod_id) = curr_pod_id { if let Some(ep_id) = curr_ep_id { - return UiMsg::Delete(pod_id, ep_id); + return UiMsg::Download(pod_id, ep_id); } } } - }, - Some(UserAction::DeleteAll) => { - if let Some(pod_id) = curr_pod_id { - return UiMsg::DeleteAll(pod_id); + Some(UserAction::DownloadAll) => { + if let Some(pod_id) = curr_pod_id { + return UiMsg::DownloadAll(pod_id); + } } - } - - Some(UserAction::Remove) => { - let mut delete = false; - match self.active_menu { - ActiveMenu::PodcastMenu => { + Some(UserAction::Delete) => match self.active_menu { + ActiveMenu::PodcastMenu => (), + ActiveMenu::EpisodeMenu => { if let Some(pod_id) = curr_pod_id { - // check if we have local files first - let mut any_downloaded = false; - { - let borrowed_map = self.podcast_menu.items.borrow_map(); - let borrowed_pod = borrowed_map.get(&pod_id).unwrap(); - - let borrowed_ep_list = borrowed_pod.episodes.borrow_map(); - - for (_ep_id, ep) in borrowed_ep_list.iter() { - if ep.path.is_some() { - any_downloaded = true; - break; - } - } - } - - if any_downloaded { - let ask_delete = - self.spawn_yes_no_notif("Delete local files too?"); - delete = match ask_delete { - Some(val) => val, - None => false, // default not to delete - }; + if let Some(ep_id) = curr_ep_id { + return UiMsg::Delete(pod_id, ep_id); } - - return UiMsg::RemovePodcast(pod_id, delete); } } - ActiveMenu::EpisodeMenu => { - if let Some(pod_id) = curr_pod_id { - if let Some(ep_id) = curr_ep_id { + }, + + Some(UserAction::DeleteAll) => { + if let Some(pod_id) = curr_pod_id { + return UiMsg::DeleteAll(pod_id); + } + } + + Some(UserAction::Remove) => { + let mut delete = false; + + match self.active_menu { + ActiveMenu::PodcastMenu => { + if let Some(pod_id) = curr_pod_id { // check if we have local files first - let is_downloaded = self - .episode_menu - .items - .map_single(ep_id, |ep| ep.path.is_some()) - .unwrap(); - if is_downloaded { + let mut any_downloaded = false; + { + let borrowed_map = self.podcast_menu.items.borrow_map(); + let borrowed_pod = borrowed_map.get(&pod_id).unwrap(); + + let borrowed_ep_list = + borrowed_pod.episodes.borrow_map(); + + for (_ep_id, ep) in borrowed_ep_list.iter() { + if ep.path.is_some() { + any_downloaded = true; + break; + } + } + } + + if any_downloaded { let ask_delete = - self.spawn_yes_no_notif("Delete local file too?"); + self.spawn_yes_no_notif("Delete local files too?"); delete = match ask_delete { Some(val) => val, None => false, // default not to delete }; } - return UiMsg::RemoveEpisode(pod_id, ep_id, delete); + return UiMsg::RemovePodcast(pod_id, delete); + } + } + ActiveMenu::EpisodeMenu => { + if let Some(pod_id) = curr_pod_id { + if let Some(ep_id) = curr_ep_id { + // check if we have local files first + let is_downloaded = self + .episode_menu + .items + .map_single(ep_id, |ep| ep.path.is_some()) + .unwrap(); + if is_downloaded { + let ask_delete = self + .spawn_yes_no_notif("Delete local file too?"); + delete = match ask_delete { + Some(val) => val, + None => false, // default not to delete + }; + } + + return UiMsg::RemoveEpisode(pod_id, ep_id, delete); + } } } } } - } - Some(UserAction::RemoveAll) => { - if let Some(pod_id) = curr_pod_id { - let mut delete = false; + Some(UserAction::RemoveAll) => { + if let Some(pod_id) = curr_pod_id { + let mut delete = false; - // check if we have local files first - let mut any_downloaded = false; - { - let borrowed_map = self.podcast_menu.items.borrow_map(); - let borrowed_pod = borrowed_map.get(&pod_id).unwrap(); + // check if we have local files first + let mut any_downloaded = false; + { + let borrowed_map = self.podcast_menu.items.borrow_map(); + let borrowed_pod = borrowed_map.get(&pod_id).unwrap(); - let borrowed_ep_list = borrowed_pod.episodes.borrow_map(); + let borrowed_ep_list = borrowed_pod.episodes.borrow_map(); - for (_ep_id, ep) in borrowed_ep_list.iter() { - if ep.path.is_some() { - any_downloaded = true; - break; + for (_ep_id, ep) in borrowed_ep_list.iter() { + if ep.path.is_some() { + any_downloaded = true; + break; + } } } - } - if any_downloaded { - let ask_delete = self.spawn_yes_no_notif("Delete local files too?"); - delete = match ask_delete { - Some(val) => val, - None => false, // default not to delete + if any_downloaded { + let ask_delete = + self.spawn_yes_no_notif("Delete local files too?"); + delete = match ask_delete { + Some(val) => val, + None => false, // default not to delete + }; + } + return match self.active_menu { + ActiveMenu::PodcastMenu => UiMsg::RemovePodcast(pod_id, delete), + ActiveMenu::EpisodeMenu => { + UiMsg::RemoveAllEpisodes(pod_id, delete) + } }; } - return match self.active_menu { - ActiveMenu::PodcastMenu => UiMsg::RemovePodcast(pod_id, delete), - ActiveMenu::EpisodeMenu => UiMsg::RemoveAllEpisodes(pod_id, delete), - }; } - } - Some(UserAction::Quit) => { - return UiMsg::Quit; - } - None => (), - } // end of input match - } - None => (), - }; // end of getch() match + Some(UserAction::Help) => self.popup_win.spawn_help_win(), + + Some(UserAction::Quit) => { + return UiMsg::Quit; + } + None => (), + } // end of input match + } + None => (), + }; // end of getch() match + } return UiMsg::Noop; } @@ -807,67 +805,117 @@ impl<'a> UI<'a> { } } - /// Creates a pancurses window with a welcome message for when users - /// start the program for the first time. Responsibility for managing - /// the window is given back to the main UI object. - pub fn make_welcome_win(colors: Colors, keymap: &Keybindings, n_row: i32, n_col: i32) -> Panel { - let add_keys = keymap.keys_for_action(UserAction::AddFeed); - let quit_keys = keymap.keys_for_action(UserAction::Quit); - - let add_str = match add_keys.len() { - 0 => "".to_string(), - 1 => format!("\"{}\"", &add_keys[0]), - 2 => format!("\"{}\" or \"{}\"", add_keys[0], add_keys[1]), - _ => { - let mut s = "".to_string(); - for i in 0..add_keys.len() { - if i == add_keys.len() - 1 { - s = format!("{}, \"{}\"", s, add_keys[i]); - } else { - s = format!("{}, or \"{}\"", s, add_keys[i]); - } - } - s - } - }; - - let quit_str = match quit_keys.len() { - 0 => "".to_string(), - 1 => format!("\"{}\"", &quit_keys[0]), - 2 => format!("\"{}\" or \"{}\"", quit_keys[0], quit_keys[1]), - _ => { - let mut s = "".to_string(); - for i in 0..quit_keys.len() { - if i == quit_keys.len() - 1 { - s = format!("{}, \"{}\"", s, quit_keys[i]); - } else { - s = format!("{}, or \"{}\"", s, quit_keys[i]); - } - } - s - } - }; - - // the warning on the unused mut is a function of Rust getting - // confused between panel.rs and mock_panel.rs - #[allow(unused_mut)] - let mut welcome_win = Panel::new(colors, "Shellcaster".to_string(), 0, n_row, n_col, 0, 0); - - let mut row = 0; - row = welcome_win.write_wrap_line(row + 1, "Welcome to shellcaster!".to_string()); - - row = welcome_win.write_wrap_line(row+2, - format!("Your podcast list is currently empty. Press {} to add a new podcast feed, or {} to quit.", add_str, quit_str)); - - row = welcome_win.write_wrap_line( - row + 2, - "Other keybindings can be found on the Github repo readme:".to_string(), - ); - let _ = welcome_win.write_wrap_line( - row + 1, - "https://github.com/jeff-hughes/shellcaster".to_string(), - ); - - return welcome_win; - } + // /// Creates a pancurses window with a welcome message for when users + // /// start the program for the first time. Responsibility for managing + // /// the window is given back to the main UI object. + // pub fn make_welcome_win(colors: Colors, keymap: &Keybindings, n_row: i32, n_col: i32) -> Panel { + // let actions = vec![UserAction::AddFeed, UserAction::Quit, UserAction::Help]; + // let mut key_strs = Vec::new(); + // for action in actions { + // let keys = keymap.keys_for_action(action); + // let key_str = match keys.len() { + // 0 => "".to_string(), + // 1 => format!("\"{}\"", &keys[0]), + // 2 => format!("\"{}\" or \"{}\"", &keys[0], &keys[1]), + // _ => { + // let mut s = "".to_string(); + // for i in 0..keys.len() { + // if i == keys.len() - 1 { + // s = format!("{}, \"{}\"", s, keys[i]); + // } else { + // s = format!("{}, or \"{}\"", s, keys[i]); + // } + // } + // s + // } + // }; + // key_strs.push(key_str); + // } + + // // the warning on the unused mut is a function of Rust getting + // // confused between panel.rs and mock_panel.rs + // #[allow(unused_mut)] + // let mut welcome_win = Panel::new(colors, "Shellcaster".to_string(), 0, n_row, n_col, 0, 0); + + // let mut row = 0; + // row = welcome_win.write_wrap_line(row + 1, "Welcome to shellcaster!".to_string()); + + // row = welcome_win.write_wrap_line(row+2, + // format!("Your podcast list is currently empty. Press {} to add a new podcast feed, {} to quit, or see all available commands by typing {} to get help.", key_strs[0], key_strs[1], key_strs[2])); + + // row = welcome_win.write_wrap_line( + // row + 2, + // "More details of how to customize shellcaster can be found on the Github repo readme:" + // .to_string(), + // ); + // let _ = welcome_win.write_wrap_line( + // row + 1, + // "https://github.com/jeff-hughes/shellcaster".to_string(), + // ); + + // return welcome_win; + // } + + // /// Creates a pancurses window showing the keybindings. + // /// Responsibility for managing the window is given back to the + // /// main UI object. + // pub fn make_help_win(colors: Colors, keymap: &Keybindings, n_row: i32, n_col: i32) -> Panel { + // let actions = vec![ + // (Some(UserAction::Left), "Left:"), + // (Some(UserAction::Right), "Right:"), + // (Some(UserAction::Up), "Up:"), + // (Some(UserAction::Down), "Down:"), + // (None, ""), + // (Some(UserAction::AddFeed), "Add feed:"), + // (Some(UserAction::Sync), "Sync:"), + // (Some(UserAction::SyncAll), "Sync all:"), + // (None, ""), + // (Some(UserAction::Play), "Play:"), + // (Some(UserAction::MarkPlayed), "Mark as played:"), + // (Some(UserAction::MarkAllPlayed), "Mark all as played:"), + // (None, ""), + // (Some(UserAction::Download), "Download:"), + // (Some(UserAction::DownloadAll), "Download all:"), + // (Some(UserAction::Delete), "Delete file:"), + // (Some(UserAction::DeleteAll), "Delete all files:"), + // (Some(UserAction::Remove), "Remove from list:"), + // (Some(UserAction::RemoveAll), "Remove all from list:"), + // (None, ""), + // (Some(UserAction::Help), "Help:"), + // (Some(UserAction::Quit), "Quit:"), + // ]; + // let mut key_strs = Vec::new(); + // for (action, action_str) in actions { + // match action { + // Some(action) => { + // let keys = keymap.keys_for_action(action); + // // longest prefix is 21 chars long + // let key_str = match keys.len() { + // 0 => format!("{:>21} ", action_str), + // 1 => format!("{:>21} \"{}\"", action_str, &keys[0]), + // _ => format!("{:>21} \"{}\" or \"{}\"", action_str, &keys[0], &keys[1]), + // }; + // key_strs.push(key_str); + // } + // None => key_strs.push(" ".to_string()), + // } + // } + + // // the warning on the unused mut is a function of Rust getting + // // confused between panel.rs and mock_panel.rs + // #[allow(unused_mut)] + // let mut help_win = Panel::new(colors, "Help".to_string(), 0, n_row, n_col, 0, 0); + + // let mut row = 0; + // row = help_win.write_wrap_line(row + 1, "Available keybindings:".to_string()); + // help_win.change_attr(row, 0, 22, pancurses::A_UNDERLINE, ColorType::Normal); + // row += 1; + + // for key in key_strs { + // row = help_win.write_wrap_line(row + 1, key); + // } + + // let _ = help_win.write_wrap_line(row + 2, "Press \"q\" to close this window.".to_string()); + // return help_win; + // } } diff --git a/src/ui/popup.rs b/src/ui/popup.rs new file mode 100644 index 0000000..2d1417a --- /dev/null +++ b/src/ui/popup.rs @@ -0,0 +1,283 @@ +use pancurses::Input; + +use super::Panel; +use super::{ColorType, Colors}; +use crate::keymap::{Keybindings, UserAction}; + +/// Enum indicating the type of the currently active popup window. +#[derive(Debug, PartialEq)] +pub enum ActivePopup { + WelcomeWin, + HelpWin, + DownloadWin, + None, +} + +/// Holds all state relevant for handling popup windows. +#[derive(Debug)] +pub struct PopupWin<'a> { + panel: Option, + colors: Colors, + keymap: &'a Keybindings, + total_rows: i32, + total_cols: i32, + pub active: ActivePopup, + pub welcome_win: bool, + pub help_win: bool, + pub download_win: bool, +} + +impl<'a> PopupWin<'a> { + /// Set up struct for handling popup windows. + pub fn new(colors: Colors, keymap: &'a Keybindings, total_rows: i32, total_cols: i32) -> Self { + return Self { + panel: None, + colors: colors, + keymap: keymap, + total_rows: total_rows, + total_cols: total_cols, + active: ActivePopup::None, + welcome_win: false, + help_win: false, + download_win: false, + }; + } + + /// Indicates whether any sort of popup window is currently on the + /// screen. + pub fn is_popup_active(&self) -> bool { + return self.welcome_win || self.help_win || self.download_win; + } + + /// Indicates whether a popup window *other than the welcome window* + /// is currently on the screen. + pub fn is_non_welcome_popup_active(&self) -> bool { + return self.help_win || self.download_win; + } + + /// Resize the currently active popup window if one exists. + pub fn resize(&mut self, total_rows: i32, total_cols: i32) { + self.total_rows = total_rows; + self.total_cols = total_cols; + match self.active { + ActivePopup::WelcomeWin => { + let welcome_win = self.make_welcome_win(); + welcome_win.refresh(); + let _ = std::mem::replace(&mut self.panel, Some(welcome_win)); + } + ActivePopup::HelpWin => { + let help_win = self.make_help_win(); + help_win.refresh(); + let _ = std::mem::replace(&mut self.panel, Some(help_win)); + } + ActivePopup::DownloadWin => (), // not yet implemented + ActivePopup::None => (), + } + } + + /// Create a welcome window and draw it to the screen. + pub fn spawn_welcome_win(&mut self) { + self.welcome_win = true; + self.change_win(); + } + + /// Create a new Panel holding a welcome window. + pub fn make_welcome_win(&self) -> Panel { + // get list of all keybindings for adding a feed, quitting + // program, or opening help menu + let actions = vec![UserAction::AddFeed, UserAction::Quit, UserAction::Help]; + let mut key_strs = Vec::new(); + for action in actions { + let keys = self.keymap.keys_for_action(action); + let key_str = match keys.len() { + 0 => "".to_string(), + 1 => format!("\"{}\"", &keys[0]), + 2 => format!("\"{}\" or \"{}\"", &keys[0], &keys[1]), + _ => { + let mut s = "".to_string(); + for i in 0..keys.len() { + if i == keys.len() - 1 { + s = format!("{}, \"{}\"", s, keys[i]); + } else { + s = format!("{}, or \"{}\"", s, keys[i]); + } + } + s + } + }; + key_strs.push(key_str); + } + + // the warning on the unused mut is a function of Rust getting + // confused between panel.rs and mock_panel.rs + #[allow(unused_mut)] + let mut welcome_win = Panel::new( + self.colors.clone(), + "Shellcaster".to_string(), + 0, + self.total_rows - 1, + self.total_cols, + 0, + 0, + ); + + let mut row = 0; + row = welcome_win.write_wrap_line(row + 1, "Welcome to shellcaster!".to_string()); + + row = welcome_win.write_wrap_line(row+2, + format!("Your podcast list is currently empty. Press {} to add a new podcast feed, {} to quit, or see all available commands by typing {} to get help.", key_strs[0], key_strs[1], key_strs[2])); + + row = welcome_win.write_wrap_line( + row + 2, + "More details of how to customize shellcaster can be found on the Github repo readme:" + .to_string(), + ); + let _ = welcome_win.write_wrap_line( + row + 1, + "https://github.com/jeff-hughes/shellcaster".to_string(), + ); + + return welcome_win; + } + + /// Create a new help window and draw it to the screen. + pub fn spawn_help_win(&mut self) { + self.help_win = true; + self.change_win(); + } + + /// Create a new Panel holding a help window. + pub fn make_help_win(&self) -> Panel { + let actions = vec![ + (Some(UserAction::Left), "Left:"), + (Some(UserAction::Right), "Right:"), + (Some(UserAction::Up), "Up:"), + (Some(UserAction::Down), "Down:"), + (None, ""), + (Some(UserAction::AddFeed), "Add feed:"), + (Some(UserAction::Sync), "Sync:"), + (Some(UserAction::SyncAll), "Sync all:"), + (None, ""), + (Some(UserAction::Play), "Play:"), + (Some(UserAction::MarkPlayed), "Mark as played:"), + (Some(UserAction::MarkAllPlayed), "Mark all as played:"), + (None, ""), + (Some(UserAction::Download), "Download:"), + (Some(UserAction::DownloadAll), "Download all:"), + (Some(UserAction::Delete), "Delete file:"), + (Some(UserAction::DeleteAll), "Delete all files:"), + (Some(UserAction::Remove), "Remove from list:"), + (Some(UserAction::RemoveAll), "Remove all from list:"), + (None, ""), + (Some(UserAction::Help), "Help:"), + (Some(UserAction::Quit), "Quit:"), + ]; + let mut key_strs = Vec::new(); + for (action, action_str) in actions { + match action { + Some(action) => { + let keys = self.keymap.keys_for_action(action); + // longest prefix is 21 chars long + let key_str = match keys.len() { + 0 => format!("{:>21} ", action_str), + 1 => format!("{:>21} \"{}\"", action_str, &keys[0]), + _ => format!("{:>21} \"{}\" or \"{}\"", action_str, &keys[0], &keys[1]), + }; + key_strs.push(key_str); + } + None => key_strs.push(" ".to_string()), + } + } + + // the warning on the unused mut is a function of Rust getting + // confused between panel.rs and mock_panel.rs + #[allow(unused_mut)] + let mut help_win = Panel::new( + self.colors.clone(), + "Help".to_string(), + 0, + self.total_rows - 1, + self.total_cols, + 0, + 0, + ); + + let mut row = 0; + row = help_win.write_wrap_line(row + 1, "Available keybindings:".to_string()); + help_win.change_attr(row, 0, 22, pancurses::A_UNDERLINE, ColorType::Normal); + row += 1; + + for key in key_strs { + row = help_win.write_wrap_line(row + 1, key); + } + + let _ = help_win.write_wrap_line(row + 2, "Press \"q\" to close this window.".to_string()); + return help_win; + } + + /// Gets rid of the welcome window. + pub fn turn_off_welcome_win(&mut self) { + self.welcome_win = false; + self.change_win(); + } + + /// Gets rid of the help window. + pub fn turn_off_help_win(&mut self) { + self.help_win = false; + self.change_win(); + } + + /// When there is a change to the active popup window, this should + /// be called to check for other popup windows that are "in the + /// queue" -- this lets one popup window appear over top of another + /// one, while keeping that second one in reserve. This function will + /// check for other popup windows to appear and change the active + /// window accordingly. + fn change_win(&mut self) { + let mut win = None; + let mut new_active = ActivePopup::None; + + // The help window takes precedence over all other popup windows; + // the welcome window is lowest priority and only appears if all + // other windows are inactive + if self.help_win && self.active != ActivePopup::HelpWin { + win = Some(self.make_help_win()); + new_active = ActivePopup::HelpWin; + } else if self.download_win && self.active != ActivePopup::DownloadWin { + // TODO: Not yet implemented + } else if self.welcome_win && self.active != ActivePopup::WelcomeWin { + win = Some(self.make_welcome_win()); + new_active = ActivePopup::WelcomeWin; + } else if !self.help_win + && !self.download_win + && !self.welcome_win + && self.active != ActivePopup::None + { + self.panel = None; + self.active = ActivePopup::None; + } + + if let Some(newwin) = win { + newwin.refresh(); + self.panel = Some(newwin); + self.active = new_active; + } + } + + /// When a popup window is active, this handles the user's keyboard + /// input that is relevant for that window. + pub fn handle_input(&mut self, input: Option) { + if self.active == ActivePopup::HelpWin { + match input { + Some(Input::KeyExit) + | Some(Input::Character('\u{1b}')) // Esc + | Some(Input::Character('q')) + | Some(Input::Character('Q')) => { + self.turn_off_help_win(); + } + Some(_) => (), + None => (), + } + } + } +} From bdbc99013cb0a6c365db063bd93a527959c72736 Mon Sep 17 00:00:00 2001 From: Jeff Hughes Date: Wed, 9 Sep 2020 19:07:01 -0400 Subject: [PATCH 08/23] Slight change to avoid recreating details panel --- src/ui/mod.rs | 8 ++++++-- 1 file changed, 6 insertions(+), 2 deletions(-) diff --git a/src/ui/mod.rs b/src/ui/mod.rs index 3a1b63c..2ecde45 100644 --- a/src/ui/mod.rs +++ b/src/ui/mod.rs @@ -387,7 +387,9 @@ impl<'a> UI<'a> { } } } - self.update_details_panel(); + if let Some(det) = &self.details_panel { + det.refresh(); + } } Some(UserAction::Right) => { @@ -401,7 +403,9 @@ impl<'a> UI<'a> { ActiveMenu::EpisodeMenu => (), } } - self.update_details_panel(); + if let Some(det) = &self.details_panel { + det.refresh(); + } } Some(UserAction::AddFeed) => { From 11c2a9839cca2287b660a441c0240564677f1535 Mon Sep 17 00:00:00 2001 From: Jeff Hughes Date: Wed, 9 Sep 2020 20:04:41 -0400 Subject: [PATCH 09/23] Write help window with multiple columns if possible --- src/ui/popup.rs | 43 +++++++++++++++++++++++++++++++++++++------ 1 file changed, 37 insertions(+), 6 deletions(-) diff --git a/src/ui/popup.rs b/src/ui/popup.rs index 2d1417a..ae127b0 100644 --- a/src/ui/popup.rs +++ b/src/ui/popup.rs @@ -153,22 +153,22 @@ impl<'a> PopupWin<'a> { (Some(UserAction::Right), "Right:"), (Some(UserAction::Up), "Up:"), (Some(UserAction::Down), "Down:"), - (None, ""), + // (None, ""), (Some(UserAction::AddFeed), "Add feed:"), (Some(UserAction::Sync), "Sync:"), (Some(UserAction::SyncAll), "Sync all:"), - (None, ""), + // (None, ""), (Some(UserAction::Play), "Play:"), (Some(UserAction::MarkPlayed), "Mark as played:"), (Some(UserAction::MarkAllPlayed), "Mark all as played:"), - (None, ""), + // (None, ""), (Some(UserAction::Download), "Download:"), (Some(UserAction::DownloadAll), "Download all:"), (Some(UserAction::Delete), "Delete file:"), (Some(UserAction::DeleteAll), "Delete all files:"), (Some(UserAction::Remove), "Remove from list:"), (Some(UserAction::RemoveAll), "Remove all from list:"), - (None, ""), + // (None, ""), (Some(UserAction::Help), "Help:"), (Some(UserAction::Quit), "Quit:"), ]; @@ -207,8 +207,39 @@ impl<'a> PopupWin<'a> { help_win.change_attr(row, 0, 22, pancurses::A_UNDERLINE, ColorType::Normal); row += 1; - for key in key_strs { - row = help_win.write_wrap_line(row + 1, key); + // check how long our strings are, and map to two columns + // if possible; `col_spacing` is the space to leave in between + // the two columns + let longest_line = key_strs.iter().map(|x| x.chars().count()).max().unwrap(); + let col_spacing = 5; + let n_cols = if help_win.get_cols() > (longest_line * 2 + col_spacing) as i32 { + 2 + } else { + 1 + }; + let keys_per_row = key_strs.len() as i32 / n_cols; + + // write each line of keys -- the list will be presented "down" + // rather than "across", but we print to the screen a line at a + // time, so the offset jumps down in the list if we have more + // than one column + for i in 0..keys_per_row { + let mut line = String::new(); + for j in 0..n_cols { + let offset = j * keys_per_row; + if let Some(val) = key_strs.get((i + offset) as usize) { + // apply `col_spacing` to the right side of the + // first column + let width = if n_cols > 1 && offset == 0 { + longest_line + col_spacing + } else { + longest_line + }; + line += &format!("{: Date: Sat, 12 Sep 2020 13:20:20 -0400 Subject: [PATCH 10/23] Fix popup window resizing on terminal resize --- src/ui/mod.rs | 246 +++++++++++------------------------------ src/ui/notification.rs | 1 + src/ui/popup.rs | 21 ++-- 3 files changed, 80 insertions(+), 188 deletions(-) diff --git a/src/ui/mod.rs b/src/ui/mod.rs index 2ecde45..8006e3b 100644 --- a/src/ui/mod.rs +++ b/src/ui/mod.rs @@ -254,79 +254,81 @@ impl<'a> UI<'a> { /// then passes this data back to the main controller. #[allow(clippy::cognitive_complexity)] pub fn getch(&mut self) -> UiMsg { - // if there is a popup window active (apart from the welcome - // window which takes no input), then redirect user input there - if self.popup_win.is_non_welcome_popup_active() { - self.popup_win.handle_input(self.stdscr.getch()); - - // need to check if popup window is still active, as handling - // character input above may involve closing the popup window - if !self.popup_win.is_popup_active() { + match self.stdscr.getch() { + Some(Input::KeyResize) => { + pancurses::resize_term(0, 0); + let (n_row, n_col) = self.stdscr.get_max_yx(); + self.n_row = n_row; + self.n_col = n_col; + + let (pod_col, ep_col, det_col) = Self::calculate_sizes(n_col); + + self.podcast_menu.resize(n_row - 1, pod_col, 0, 0); + self.episode_menu.resize(n_row - 1, ep_col, 0, pod_col - 1); + + if self.details_panel.is_some() { + if det_col > 0 { + let det = self.details_panel.as_mut().unwrap(); + det.resize(n_row - 1, det_col, 0, pod_col + ep_col - 2); + } else { + self.details_panel = None; + } + } else if det_col > 0 { + self.details_panel = Some(Self::make_details_panel( + self.colors.clone(), + n_row - 1, + det_col, + 0, + pod_col + ep_col - 2, + )); + } + self.stdscr.refresh(); self.update_menus(); + + match self.active_menu { + ActiveMenu::PodcastMenu => self.podcast_menu.activate(), + ActiveMenu::EpisodeMenu => { + self.podcast_menu.activate(); + self.episode_menu.activate(); + } + } + if self.details_panel.is_some() { self.update_details_panel(); } - } - } else { - match self.stdscr.getch() { - Some(Input::KeyResize) => { - pancurses::resize_term(0, 0); - let (n_row, n_col) = self.stdscr.get_max_yx(); - self.n_row = n_row; - self.n_col = n_col; - - let (pod_col, ep_col, det_col) = Self::calculate_sizes(n_col); - - self.podcast_menu.resize(n_row - 1, pod_col, 0, 0); - self.episode_menu.resize(n_row - 1, ep_col, 0, pod_col - 1); - - if self.details_panel.is_some() { - if det_col > 0 { - let det = self.details_panel.as_mut().unwrap(); - det.resize(n_row - 1, det_col, 0, pod_col + ep_col - 2); - } else { - self.details_panel = None; - } - } else if det_col > 0 { - self.details_panel = Some(Self::make_details_panel( - self.colors.clone(), - n_row - 1, - det_col, - 0, - pod_col + ep_col - 2, - )); - } - self.stdscr.refresh(); - self.update_menus(); - - match self.active_menu { - ActiveMenu::PodcastMenu => self.podcast_menu.activate(), - ActiveMenu::EpisodeMenu => { - self.podcast_menu.activate(); - self.episode_menu.activate(); - } - } + self.popup_win.resize(n_row, n_col); + self.notif_win.resize(n_row, n_col); + self.stdscr.refresh(); + } - if self.details_panel.is_some() { - self.update_details_panel(); - } + Some(input) => { + let (curr_pod_id, curr_ep_id) = self.get_current_ids(); - self.popup_win.resize(n_row, n_col); - self.notif_win.resize(n_row, n_col); - self.stdscr.refresh(); + // get rid of the "welcome" window once the podcast list + // is no longer empty + if self.popup_win.welcome_win && !self.podcast_menu.items.is_empty() { + self.popup_win.turn_off_welcome_win(); } - Some(input) => { - let (curr_pod_id, curr_ep_id) = self.get_current_ids(); - - // get rid of the "welcome" window once the podcast list - // is no longer empty - if self.popup_win.welcome_win && !self.podcast_menu.items.is_empty() { - self.popup_win.turn_off_welcome_win(); + // if there is a popup window active (apart from the + // welcome window which takes no input), then + // redirect user input there + if self.popup_win.is_non_welcome_popup_active() { + self.popup_win.handle_input(input); + + // need to check if popup window is still active, as + // handling character input above may involve + // closing the popup window + if !self.popup_win.is_popup_active() { + self.stdscr.refresh(); + self.update_menus(); + if self.details_panel.is_some() { + self.update_details_panel(); + } } - + } else { match self.keymap.get_from_input(input) { Some(UserAction::Down) => { match self.active_menu { @@ -598,9 +600,9 @@ impl<'a> UI<'a> { None => (), } // end of input match } - None => (), - }; // end of getch() match - } + } + None => (), + }; // end of getch() match return UiMsg::Noop; } @@ -810,118 +812,4 @@ impl<'a> UI<'a> { } } } - - // /// Creates a pancurses window with a welcome message for when users - // /// start the program for the first time. Responsibility for managing - // /// the window is given back to the main UI object. - // pub fn make_welcome_win(colors: Colors, keymap: &Keybindings, n_row: i32, n_col: i32) -> Panel { - // let actions = vec![UserAction::AddFeed, UserAction::Quit, UserAction::Help]; - // let mut key_strs = Vec::new(); - // for action in actions { - // let keys = keymap.keys_for_action(action); - // let key_str = match keys.len() { - // 0 => "".to_string(), - // 1 => format!("\"{}\"", &keys[0]), - // 2 => format!("\"{}\" or \"{}\"", &keys[0], &keys[1]), - // _ => { - // let mut s = "".to_string(); - // for i in 0..keys.len() { - // if i == keys.len() - 1 { - // s = format!("{}, \"{}\"", s, keys[i]); - // } else { - // s = format!("{}, or \"{}\"", s, keys[i]); - // } - // } - // s - // } - // }; - // key_strs.push(key_str); - // } - - // // the warning on the unused mut is a function of Rust getting - // // confused between panel.rs and mock_panel.rs - // #[allow(unused_mut)] - // let mut welcome_win = Panel::new(colors, "Shellcaster".to_string(), 0, n_row, n_col, 0, 0); - - // let mut row = 0; - // row = welcome_win.write_wrap_line(row + 1, "Welcome to shellcaster!".to_string()); - - // row = welcome_win.write_wrap_line(row+2, - // format!("Your podcast list is currently empty. Press {} to add a new podcast feed, {} to quit, or see all available commands by typing {} to get help.", key_strs[0], key_strs[1], key_strs[2])); - - // row = welcome_win.write_wrap_line( - // row + 2, - // "More details of how to customize shellcaster can be found on the Github repo readme:" - // .to_string(), - // ); - // let _ = welcome_win.write_wrap_line( - // row + 1, - // "https://github.com/jeff-hughes/shellcaster".to_string(), - // ); - - // return welcome_win; - // } - - // /// Creates a pancurses window showing the keybindings. - // /// Responsibility for managing the window is given back to the - // /// main UI object. - // pub fn make_help_win(colors: Colors, keymap: &Keybindings, n_row: i32, n_col: i32) -> Panel { - // let actions = vec![ - // (Some(UserAction::Left), "Left:"), - // (Some(UserAction::Right), "Right:"), - // (Some(UserAction::Up), "Up:"), - // (Some(UserAction::Down), "Down:"), - // (None, ""), - // (Some(UserAction::AddFeed), "Add feed:"), - // (Some(UserAction::Sync), "Sync:"), - // (Some(UserAction::SyncAll), "Sync all:"), - // (None, ""), - // (Some(UserAction::Play), "Play:"), - // (Some(UserAction::MarkPlayed), "Mark as played:"), - // (Some(UserAction::MarkAllPlayed), "Mark all as played:"), - // (None, ""), - // (Some(UserAction::Download), "Download:"), - // (Some(UserAction::DownloadAll), "Download all:"), - // (Some(UserAction::Delete), "Delete file:"), - // (Some(UserAction::DeleteAll), "Delete all files:"), - // (Some(UserAction::Remove), "Remove from list:"), - // (Some(UserAction::RemoveAll), "Remove all from list:"), - // (None, ""), - // (Some(UserAction::Help), "Help:"), - // (Some(UserAction::Quit), "Quit:"), - // ]; - // let mut key_strs = Vec::new(); - // for (action, action_str) in actions { - // match action { - // Some(action) => { - // let keys = keymap.keys_for_action(action); - // // longest prefix is 21 chars long - // let key_str = match keys.len() { - // 0 => format!("{:>21} ", action_str), - // 1 => format!("{:>21} \"{}\"", action_str, &keys[0]), - // _ => format!("{:>21} \"{}\" or \"{}\"", action_str, &keys[0], &keys[1]), - // }; - // key_strs.push(key_str); - // } - // None => key_strs.push(" ".to_string()), - // } - // } - - // // the warning on the unused mut is a function of Rust getting - // // confused between panel.rs and mock_panel.rs - // #[allow(unused_mut)] - // let mut help_win = Panel::new(colors, "Help".to_string(), 0, n_row, n_col, 0, 0); - - // let mut row = 0; - // row = help_win.write_wrap_line(row + 1, "Available keybindings:".to_string()); - // help_win.change_attr(row, 0, 22, pancurses::A_UNDERLINE, ColorType::Normal); - // row += 1; - - // for key in key_strs { - // row = help_win.write_wrap_line(row + 1, key); - // } - - // let _ = help_win.write_wrap_line(row + 2, "Press \"q\" to close this window.".to_string()); - // return help_win; - // } } diff --git a/src/ui/notification.rs b/src/ui/notification.rs index bfe4990..1c2a996 100644 --- a/src/ui/notification.rs +++ b/src/ui/notification.rs @@ -251,5 +251,6 @@ impl NotifWin { if let Some(curr) = &self.current_msg { self.display_notif(curr.clone()); } + self.window.refresh(); } } diff --git a/src/ui/popup.rs b/src/ui/popup.rs index ae127b0..0229234 100644 --- a/src/ui/popup.rs +++ b/src/ui/popup.rs @@ -59,20 +59,24 @@ impl<'a> PopupWin<'a> { pub fn resize(&mut self, total_rows: i32, total_cols: i32) { self.total_rows = total_rows; self.total_cols = total_cols; + self.panel = None; match self.active { ActivePopup::WelcomeWin => { let welcome_win = self.make_welcome_win(); welcome_win.refresh(); - let _ = std::mem::replace(&mut self.panel, Some(welcome_win)); + self.panel = Some(welcome_win); } ActivePopup::HelpWin => { let help_win = self.make_help_win(); help_win.refresh(); - let _ = std::mem::replace(&mut self.panel, Some(help_win)); + self.panel = Some(help_win); } ActivePopup::DownloadWin => (), // not yet implemented ActivePopup::None => (), } + // if let Some(panel) = &self.panel { + // panel.refresh(); + // } } /// Create a welcome window and draw it to the screen. @@ -297,17 +301,16 @@ impl<'a> PopupWin<'a> { /// When a popup window is active, this handles the user's keyboard /// input that is relevant for that window. - pub fn handle_input(&mut self, input: Option) { + pub fn handle_input(&mut self, input: Input) { if self.active == ActivePopup::HelpWin { match input { - Some(Input::KeyExit) - | Some(Input::Character('\u{1b}')) // Esc - | Some(Input::Character('q')) - | Some(Input::Character('Q')) => { + Input::KeyExit + | Input::Character('\u{1b}') // Esc + | Input::Character('q') + | Input::Character('Q') => { self.turn_off_help_win(); } - Some(_) => (), - None => (), + _ => (), } } } From 97b9439e092a83250a2c762eca6b4a8318097856 Mon Sep 17 00:00:00 2001 From: Jeff Hughes Date: Sat, 12 Sep 2020 14:11:35 -0400 Subject: [PATCH 11/23] Refactor main UI input loop --- src/ui/mod.rs | 539 ++++++++++++++++++++++++++++---------------------- 1 file changed, 299 insertions(+), 240 deletions(-) diff --git a/src/ui/mod.rs b/src/ui/mod.rs index 8006e3b..167a918 100644 --- a/src/ui/mod.rs +++ b/src/ui/mod.rs @@ -244,64 +244,17 @@ impl<'a> UI<'a> { } } - /// Waits for user input and, where necessary, provides UiMessages + /// Waits for user input and, where necessary, provides UiMsgs /// back to the main controller. /// /// Anything UI-related (e.g., scrolling up and down menus) is handled - /// internally, producing an empty UiMessage. This allows for some + /// internally, producing an empty UiMsg. This allows for some /// greater degree of abstraction; for example, input to add a new /// podcast feed spawns a UI window to capture the feed URL, and only /// then passes this data back to the main controller. - #[allow(clippy::cognitive_complexity)] pub fn getch(&mut self) -> UiMsg { match self.stdscr.getch() { - Some(Input::KeyResize) => { - pancurses::resize_term(0, 0); - let (n_row, n_col) = self.stdscr.get_max_yx(); - self.n_row = n_row; - self.n_col = n_col; - - let (pod_col, ep_col, det_col) = Self::calculate_sizes(n_col); - - self.podcast_menu.resize(n_row - 1, pod_col, 0, 0); - self.episode_menu.resize(n_row - 1, ep_col, 0, pod_col - 1); - - if self.details_panel.is_some() { - if det_col > 0 { - let det = self.details_panel.as_mut().unwrap(); - det.resize(n_row - 1, det_col, 0, pod_col + ep_col - 2); - } else { - self.details_panel = None; - } - } else if det_col > 0 { - self.details_panel = Some(Self::make_details_panel( - self.colors.clone(), - n_row - 1, - det_col, - 0, - pod_col + ep_col - 2, - )); - } - - self.stdscr.refresh(); - self.update_menus(); - - match self.active_menu { - ActiveMenu::PodcastMenu => self.podcast_menu.activate(), - ActiveMenu::EpisodeMenu => { - self.podcast_menu.activate(); - self.episode_menu.activate(); - } - } - - if self.details_panel.is_some() { - self.update_details_panel(); - } - - self.popup_win.resize(n_row, n_col); - self.notif_win.resize(n_row, n_col); - self.stdscr.refresh(); - } + Some(Input::KeyResize) => self.resize(), Some(input) => { let (curr_pod_id, curr_ep_id) = self.get_current_ids(); @@ -330,84 +283,11 @@ impl<'a> UI<'a> { } } else { match self.keymap.get_from_input(input) { - Some(UserAction::Down) => { - match self.active_menu { - ActiveMenu::PodcastMenu => { - if curr_pod_id.is_some() { - self.podcast_menu.scroll(1); - - self.episode_menu.top_row = 0; - self.episode_menu.selected = 0; - - // update episodes menu with new list - self.episode_menu.items = self.podcast_menu.get_episodes(); - self.episode_menu.update_items(); - self.update_details_panel(); - } - } - ActiveMenu::EpisodeMenu => { - if curr_ep_id.is_some() { - self.episode_menu.scroll(1); - self.update_details_panel(); - } - } - } - } - - Some(UserAction::Up) => { - match self.active_menu { - ActiveMenu::PodcastMenu => { - if curr_pod_id.is_some() { - self.podcast_menu.scroll(-1); - - self.episode_menu.top_row = 0; - self.episode_menu.selected = 0; - - // update episodes menu with new list - self.episode_menu.items = self.podcast_menu.get_episodes(); - self.episode_menu.update_items(); - self.update_details_panel(); - } - } - ActiveMenu::EpisodeMenu => { - if curr_pod_id.is_some() { - self.episode_menu.scroll(-1); - self.update_details_panel(); - } - } - } - } - - Some(UserAction::Left) => { - if curr_pod_id.is_some() { - match self.active_menu { - ActiveMenu::PodcastMenu => (), - ActiveMenu::EpisodeMenu => { - self.active_menu = ActiveMenu::PodcastMenu; - self.podcast_menu.activate(); - self.episode_menu.deactivate(); - } - } - } - if let Some(det) = &self.details_panel { - det.refresh(); - } - } - - Some(UserAction::Right) => { - if curr_pod_id.is_some() && curr_ep_id.is_some() { - match self.active_menu { - ActiveMenu::PodcastMenu => { - self.active_menu = ActiveMenu::EpisodeMenu; - self.podcast_menu.deactivate(); - self.episode_menu.activate(); - } - ActiveMenu::EpisodeMenu => (), - } - } - if let Some(det) = &self.details_panel { - det.refresh(); - } + Some(a @ UserAction::Down) + | Some(a @ UserAction::Up) + | Some(a @ UserAction::Left) + | Some(a @ UserAction::Right) => { + self.move_cursor(a, curr_pod_id, curr_ep_id) } Some(UserAction::AddFeed) => { @@ -427,6 +307,7 @@ impl<'a> UI<'a> { return UiMsg::SyncAll; } } + Some(UserAction::Play) => { if let Some(pod_id) = curr_pod_id { if let Some(ep_id) = curr_ep_id { @@ -437,31 +318,14 @@ impl<'a> UI<'a> { Some(UserAction::MarkPlayed) => match self.active_menu { ActiveMenu::PodcastMenu => (), ActiveMenu::EpisodeMenu => { - if let Some(pod_id) = curr_pod_id { - if let Some(ep_id) = curr_ep_id { - if let Some(played) = self - .episode_menu - .items - .map_single(ep_id, |ep| ep.is_played()) - { - return UiMsg::MarkPlayed(pod_id, ep_id, !played); - } - } + if let Some(ui_msg) = self.mark_played(curr_pod_id, curr_ep_id) { + return ui_msg; } } }, Some(UserAction::MarkAllPlayed) => { - // if there are any unplayed episodes, MarkAllPlayed - // will convert all to played; if all are played - // already, only then will it convert all to unplayed - if let Some(pod_id) = curr_pod_id { - if let Some(played) = self - .podcast_menu - .items - .map_single(pod_id, |pod| pod.is_played()) - { - return UiMsg::MarkAllPlayed(pod_id, !played); - } + if let Some(ui_msg) = self.mark_all_played(curr_pod_id) { + return ui_msg; } } @@ -472,7 +336,6 @@ impl<'a> UI<'a> { } } } - Some(UserAction::DownloadAll) => { if let Some(pod_id) = curr_pod_id { return UiMsg::DownloadAll(pod_id); @@ -489,106 +352,31 @@ impl<'a> UI<'a> { } } }, - Some(UserAction::DeleteAll) => { if let Some(pod_id) = curr_pod_id { return UiMsg::DeleteAll(pod_id); } } - Some(UserAction::Remove) => { - let mut delete = false; - - match self.active_menu { - ActiveMenu::PodcastMenu => { - if let Some(pod_id) = curr_pod_id { - // check if we have local files first - let mut any_downloaded = false; - { - let borrowed_map = self.podcast_menu.items.borrow_map(); - let borrowed_pod = borrowed_map.get(&pod_id).unwrap(); - - let borrowed_ep_list = - borrowed_pod.episodes.borrow_map(); - - for (_ep_id, ep) in borrowed_ep_list.iter() { - if ep.path.is_some() { - any_downloaded = true; - break; - } - } - } - - if any_downloaded { - let ask_delete = - self.spawn_yes_no_notif("Delete local files too?"); - delete = match ask_delete { - Some(val) => val, - None => false, // default not to delete - }; - } - - return UiMsg::RemovePodcast(pod_id, delete); - } + Some(UserAction::Remove) => match self.active_menu { + ActiveMenu::PodcastMenu => { + if let Some(ui_msg) = self.remove_podcast(curr_pod_id) { + return ui_msg; } - ActiveMenu::EpisodeMenu => { - if let Some(pod_id) = curr_pod_id { - if let Some(ep_id) = curr_ep_id { - // check if we have local files first - let is_downloaded = self - .episode_menu - .items - .map_single(ep_id, |ep| ep.path.is_some()) - .unwrap(); - if is_downloaded { - let ask_delete = self - .spawn_yes_no_notif("Delete local file too?"); - delete = match ask_delete { - Some(val) => val, - None => false, // default not to delete - }; - } - - return UiMsg::RemoveEpisode(pod_id, ep_id, delete); - } - } + } + ActiveMenu::EpisodeMenu => { + if let Some(ui_msg) = self.remove_episode(curr_pod_id, curr_ep_id) { + return ui_msg; } } - } + }, Some(UserAction::RemoveAll) => { - if let Some(pod_id) = curr_pod_id { - let mut delete = false; - - // check if we have local files first - let mut any_downloaded = false; - { - let borrowed_map = self.podcast_menu.items.borrow_map(); - let borrowed_pod = borrowed_map.get(&pod_id).unwrap(); - - let borrowed_ep_list = borrowed_pod.episodes.borrow_map(); - - for (_ep_id, ep) in borrowed_ep_list.iter() { - if ep.path.is_some() { - any_downloaded = true; - break; - } - } - } - - if any_downloaded { - let ask_delete = - self.spawn_yes_no_notif("Delete local files too?"); - delete = match ask_delete { - Some(val) => val, - None => false, // default not to delete - }; - } - return match self.active_menu { - ActiveMenu::PodcastMenu => UiMsg::RemovePodcast(pod_id, delete), - ActiveMenu::EpisodeMenu => { - UiMsg::RemoveAllEpisodes(pod_id, delete) - } - }; + let ui_msg = match self.active_menu { + ActiveMenu::PodcastMenu => self.remove_podcast(curr_pod_id), + ActiveMenu::EpisodeMenu => self.remove_all_episodes(curr_pod_id), + }; + if let Some(ui_msg) = ui_msg { + return ui_msg; } } @@ -606,6 +394,259 @@ impl<'a> UI<'a> { return UiMsg::Noop; } + /// Resize all the windows on the screen and refresh. + pub fn resize(&mut self) { + pancurses::resize_term(0, 0); + let (n_row, n_col) = self.stdscr.get_max_yx(); + self.n_row = n_row; + self.n_col = n_col; + + let (pod_col, ep_col, det_col) = Self::calculate_sizes(n_col); + + self.podcast_menu.resize(n_row - 1, pod_col, 0, 0); + self.episode_menu.resize(n_row - 1, ep_col, 0, pod_col - 1); + + if self.details_panel.is_some() { + if det_col > 0 { + let det = self.details_panel.as_mut().unwrap(); + det.resize(n_row - 1, det_col, 0, pod_col + ep_col - 2); + } else { + self.details_panel = None; + } + } else if det_col > 0 { + self.details_panel = Some(Self::make_details_panel( + self.colors.clone(), + n_row - 1, + det_col, + 0, + pod_col + ep_col - 2, + )); + } + + self.stdscr.refresh(); + self.update_menus(); + + match self.active_menu { + ActiveMenu::PodcastMenu => self.podcast_menu.activate(), + ActiveMenu::EpisodeMenu => { + self.podcast_menu.activate(); + self.episode_menu.activate(); + } + } + + if self.details_panel.is_some() { + self.update_details_panel(); + } + + self.popup_win.resize(n_row, n_col); + self.notif_win.resize(n_row, n_col); + self.stdscr.refresh(); + } + + /// Move the menu cursor around and refresh menus when necessary. + pub fn move_cursor( + &mut self, + action: &UserAction, + curr_pod_id: Option, + curr_ep_id: Option, + ) + { + match action { + UserAction::Down => { + match self.active_menu { + ActiveMenu::PodcastMenu => { + if curr_pod_id.is_some() { + self.podcast_menu.scroll(1); + + self.episode_menu.top_row = 0; + self.episode_menu.selected = 0; + + // update episodes menu with new list + self.episode_menu.items = self.podcast_menu.get_episodes(); + self.episode_menu.update_items(); + self.update_details_panel(); + } + } + ActiveMenu::EpisodeMenu => { + if curr_ep_id.is_some() { + self.episode_menu.scroll(1); + self.update_details_panel(); + } + } + } + } + + UserAction::Up => { + match self.active_menu { + ActiveMenu::PodcastMenu => { + if curr_pod_id.is_some() { + self.podcast_menu.scroll(-1); + + self.episode_menu.top_row = 0; + self.episode_menu.selected = 0; + + // update episodes menu with new list + self.episode_menu.items = self.podcast_menu.get_episodes(); + self.episode_menu.update_items(); + self.update_details_panel(); + } + } + ActiveMenu::EpisodeMenu => { + if curr_pod_id.is_some() { + self.episode_menu.scroll(-1); + self.update_details_panel(); + } + } + } + } + + UserAction::Left => { + if curr_pod_id.is_some() { + match self.active_menu { + ActiveMenu::PodcastMenu => (), + ActiveMenu::EpisodeMenu => { + self.active_menu = ActiveMenu::PodcastMenu; + self.podcast_menu.activate(); + self.episode_menu.deactivate(); + } + } + } + if let Some(det) = &self.details_panel { + det.refresh(); + } + } + + UserAction::Right => { + if curr_pod_id.is_some() && curr_ep_id.is_some() { + match self.active_menu { + ActiveMenu::PodcastMenu => { + self.active_menu = ActiveMenu::EpisodeMenu; + self.podcast_menu.deactivate(); + self.episode_menu.activate(); + } + ActiveMenu::EpisodeMenu => (), + } + } + if let Some(det) = &self.details_panel { + det.refresh(); + } + } + + // this shouldn't occur because we only trigger this + // function when the UserAction is Up, Down, Left, or Right. + _ => (), + } + } + + /// Mark an episode as played or unplayed (opposite of its current + /// status). + pub fn mark_played( + &mut self, + curr_pod_id: Option, + curr_ep_id: Option, + ) -> Option + { + if let Some(pod_id) = curr_pod_id { + if let Some(ep_id) = curr_ep_id { + if let Some(played) = self + .episode_menu + .items + .map_single(ep_id, |ep| ep.is_played()) + { + return Some(UiMsg::MarkPlayed(pod_id, ep_id, !played)); + } + } + } + return None; + } + + /// Mark all episodes for a given podcast as played or unplayed. If + /// there are any unplayed episodes, this will convert all episodes + /// to played; if all are played already, only then will it convert + /// all to unplayed. + pub fn mark_all_played(&mut self, curr_pod_id: Option) -> Option { + if let Some(pod_id) = curr_pod_id { + if let Some(played) = self + .podcast_menu + .items + .map_single(pod_id, |pod| pod.is_played()) + { + return Some(UiMsg::MarkAllPlayed(pod_id, !played)); + } + } + return None; + } + + /// Remove a podcast from the list. + pub fn remove_podcast(&mut self, curr_pod_id: Option) -> Option { + let mut delete = false; + + if let Some(pod_id) = curr_pod_id { + // check if we have local files first and if so, ask whether + // to delete those too + if self.check_for_local_files(pod_id) { + let ask_delete = self.spawn_yes_no_notif("Delete local files too?"); + delete = match ask_delete { + Some(val) => val, + None => false, // default not to delete + }; + } + + return Some(UiMsg::RemovePodcast(pod_id, delete)); + } + return None; + } + + /// Remove an episode from the list for the current podcast. + fn remove_episode( + &mut self, + curr_pod_id: Option, + curr_ep_id: Option, + ) -> Option + { + let mut delete = false; + if let Some(pod_id) = curr_pod_id { + if let Some(ep_id) = curr_ep_id { + // check if we have local files first + let is_downloaded = self + .episode_menu + .items + .map_single(ep_id, |ep| ep.path.is_some()) + .unwrap(); + if is_downloaded { + let ask_delete = self.spawn_yes_no_notif("Delete local file too?"); + delete = match ask_delete { + Some(val) => val, + None => false, // default not to delete + }; + } + + return Some(UiMsg::RemoveEpisode(pod_id, ep_id, delete)); + } + } + return None; + } + + /// Remove all episodes from the list for the current podcast. + fn remove_all_episodes(&mut self, curr_pod_id: Option) -> Option { + if let Some(pod_id) = curr_pod_id { + let mut delete = false; + + // check if we have local files first and if so, ask whether + // to delete those too + if self.check_for_local_files(pod_id) { + let ask_delete = self.spawn_yes_no_notif("Delete local files too?"); + delete = match ask_delete { + Some(val) => val, + None => false, // default not to delete + }; + } + return Some(UiMsg::RemoveAllEpisodes(pod_id, delete)); + } + return None; + } + + /// Based on the current selected value of the podcast and episode /// menus, returns the IDs of the current podcast and episode (if /// they exist). @@ -649,6 +690,24 @@ impl<'a> UI<'a> { return (pod_col, ep_col, det_col); } + /// Checks whether the user has downloaded any episodes for the + /// given podcast to their local system. + pub fn check_for_local_files(&self, pod_id: i64) -> bool { + let mut any_downloaded = false; + let borrowed_map = self.podcast_menu.items.borrow_map(); + let borrowed_pod = borrowed_map.get(&pod_id).unwrap(); + + let borrowed_ep_list = borrowed_pod.episodes.borrow_map(); + + for (_ep_id, ep) in borrowed_ep_list.iter() { + if ep.path.is_some() { + any_downloaded = true; + break; + } + } + return any_downloaded; + } + /// Adds a notification to the bottom of the screen that solicits /// user text input. A prefix can be specified as a prompt for the /// user at the beginning of the input line. This returns the user's From 97a74b63685c649f3483f19597baa9235648ff52 Mon Sep 17 00:00:00 2001 From: Jeff Hughes Date: Fri, 25 Sep 2020 18:58:52 -0400 Subject: [PATCH 12/23] Add menu to download panel, and add NewEpisode struct (WIP) --- src/types.rs | 33 ++++++++ src/ui/menu.rs | 2 + src/ui/popup.rs | 194 ++++++++++++++++++++++++++++++++++++------------ 3 files changed, 182 insertions(+), 47 deletions(-) diff --git a/src/types.rs b/src/types.rs index 04efb2a..e15fbac 100644 --- a/src/types.rs +++ b/src/types.rs @@ -230,6 +230,39 @@ pub struct EpisodeNoId { pub duration: Option, } +/// Struct holding data about an individual podcast episode, specifically +/// for the popup window that asks users which new episodes they wish to +/// download. +#[derive(Debug, Clone)] +pub struct NewEpisode { + pub id: i64, + pub pod_id: i64, + pub title: String, + pub pod_title: String, + pub selected: bool, +} + +impl Menuable for NewEpisode { + /// Returns the database ID for the episode. + fn get_id(&self) -> i64 { + return self.id; + } + + /// Returns the title for the episode, up to length characters. + fn get_title(&self, length: usize) -> String { + let selected = if self.selected { + "✓".to_string() + } else { + " ".to_string() + }; + let full_string = format!("[{}] {} ({})", selected, self.title, self.pod_title); + return full_string.substr(0, length); + } + + fn is_played(&self) -> bool { + return false; + } +} /// Struct used to hold a vector of data inside a reference-counted /// mutex, to allow for multiple owners of mutable data. diff --git a/src/ui/menu.rs b/src/ui/menu.rs index 341224d..d0946a3 100644 --- a/src/ui/menu.rs +++ b/src/ui/menu.rs @@ -268,6 +268,8 @@ impl Menu { } } +impl Menu {} + // TESTS ---------------------------------------------------------------- #[cfg(test)] diff --git a/src/ui/popup.rs b/src/ui/popup.rs index 0229234..ba533be 100644 --- a/src/ui/popup.rs +++ b/src/ui/popup.rs @@ -1,27 +1,58 @@ use pancurses::Input; -use super::Panel; use super::{ColorType, Colors}; +use super::{Menu, Panel}; use crate::keymap::{Keybindings, UserAction}; +use crate::types::*; /// Enum indicating the type of the currently active popup window. -#[derive(Debug, PartialEq)] +#[derive(Debug)] pub enum ActivePopup { - WelcomeWin, - HelpWin, - DownloadWin, + WelcomeWin(Panel), + HelpWin(Panel), + DownloadWin(Menu), None, } +impl ActivePopup { + pub fn is_welcome_win(&self) -> bool { + match self { + ActivePopup::WelcomeWin(_) => true, + _ => false, + } + } + + pub fn is_help_win(&self) -> bool { + match self { + ActivePopup::HelpWin(_) => true, + _ => false, + } + } + + pub fn is_download_win(&self) -> bool { + match self { + ActivePopup::DownloadWin(_) => true, + _ => false, + } + } + + pub fn is_none(&self) -> bool { + match self { + ActivePopup::None => true, + _ => false, + } + } +} + /// Holds all state relevant for handling popup windows. #[derive(Debug)] pub struct PopupWin<'a> { - panel: Option, + popup: ActivePopup, + new_episodes: Vec, colors: Colors, keymap: &'a Keybindings, total_rows: i32, total_cols: i32, - pub active: ActivePopup, pub welcome_win: bool, pub help_win: bool, pub download_win: bool, @@ -31,12 +62,12 @@ impl<'a> PopupWin<'a> { /// Set up struct for handling popup windows. pub fn new(colors: Colors, keymap: &'a Keybindings, total_rows: i32, total_cols: i32) -> Self { return Self { - panel: None, + popup: ActivePopup::None, + new_episodes: Vec::new(), colors: colors, keymap: keymap, total_rows: total_rows, total_cols: total_cols, - active: ActivePopup::None, welcome_win: false, help_win: false, download_win: false, @@ -59,19 +90,18 @@ impl<'a> PopupWin<'a> { pub fn resize(&mut self, total_rows: i32, total_cols: i32) { self.total_rows = total_rows; self.total_cols = total_cols; - self.panel = None; - match self.active { - ActivePopup::WelcomeWin => { + match &self.popup { + ActivePopup::WelcomeWin(_win) => { let welcome_win = self.make_welcome_win(); welcome_win.refresh(); - self.panel = Some(welcome_win); + self.popup = ActivePopup::WelcomeWin(welcome_win); } - ActivePopup::HelpWin => { + ActivePopup::HelpWin(_win) => { let help_win = self.make_help_win(); help_win.refresh(); - self.panel = Some(help_win); + self.popup = ActivePopup::HelpWin(help_win); } - ActivePopup::DownloadWin => (), // not yet implemented + ActivePopup::DownloadWin(_win) => (), // not yet implemented ActivePopup::None => (), } // if let Some(panel) = &self.panel { @@ -250,6 +280,58 @@ impl<'a> PopupWin<'a> { return help_win; } + /// Create a new download window and draw it to the screen. + pub fn spawn_download_win(&mut self) { + // TODO: This will be moved to main controller + let ep_titles = vec![ + "Episode title", + "Here's another episode", + "This one is a great episode", + ]; + for (i, title) in ep_titles.iter().enumerate() { + self.new_episodes.push(NewEpisode { + id: i as i64, + pod_id: 0, + title: title.to_string(), + pod_title: "This is a podcast".to_string(), + selected: i == 0, + }); + } + + self.download_win = true; + self.change_win(); + } + + /// Create a new Panel holding a download window. + pub fn make_download_win(&self) -> Menu { + // the warning on the unused mut is a function of Rust getting + // confused between panel.rs and mock_panel.rs + #[allow(unused_mut)] + let mut download_panel = Panel::new( + self.colors.clone(), + "Downloads".to_string(), + 0, + self.total_rows - 1, + self.total_cols, + 0, + 0, + ); + + let mut download_win = Menu:: { + panel: download_panel, + items: LockVec::new(self.new_episodes.clone()), + top_row: 0, + selected: 0, + }; + download_win.init(); + + return download_win; + } + + pub fn add_episodes(&mut self, mut episodes: Vec) { + self.new_episodes.append(&mut episodes); + } + /// Gets rid of the welcome window. pub fn turn_off_welcome_win(&mut self) { self.welcome_win = false; @@ -262,6 +344,12 @@ impl<'a> PopupWin<'a> { self.change_win(); } + /// Gets rid of the download window. + pub fn turn_off_download_win(&mut self) { + self.download_win = false; + self.change_win(); + } + /// When there is a change to the active popup window, this should /// be called to check for other popup windows that are "in the /// queue" -- this lets one popup window appear over top of another @@ -269,49 +357,61 @@ impl<'a> PopupWin<'a> { /// check for other popup windows to appear and change the active /// window accordingly. fn change_win(&mut self) { - let mut win = None; - let mut new_active = ActivePopup::None; - // The help window takes precedence over all other popup windows; // the welcome window is lowest priority and only appears if all // other windows are inactive - if self.help_win && self.active != ActivePopup::HelpWin { - win = Some(self.make_help_win()); - new_active = ActivePopup::HelpWin; - } else if self.download_win && self.active != ActivePopup::DownloadWin { - // TODO: Not yet implemented - } else if self.welcome_win && self.active != ActivePopup::WelcomeWin { - win = Some(self.make_welcome_win()); - new_active = ActivePopup::WelcomeWin; - } else if !self.help_win - && !self.download_win - && !self.welcome_win - && self.active != ActivePopup::None + if self.help_win && !self.popup.is_help_win() { + let win = self.make_help_win(); + win.refresh(); + self.popup = ActivePopup::HelpWin(win); + } else if self.download_win && !self.popup.is_download_win() { + let mut win = self.make_download_win(); + win.update_items(); + win.highlight_selected(true); + self.popup = ActivePopup::DownloadWin(win); + } else if self.welcome_win && !self.popup.is_welcome_win() { + let win = self.make_welcome_win(); + win.refresh(); + self.popup = ActivePopup::WelcomeWin(win); + } else if !self.help_win && !self.download_win && !self.welcome_win && !self.popup.is_none() { - self.panel = None; - self.active = ActivePopup::None; - } - - if let Some(newwin) = win { - newwin.refresh(); - self.panel = Some(newwin); - self.active = new_active; + self.popup = ActivePopup::None; } } /// When a popup window is active, this handles the user's keyboard /// input that is relevant for that window. pub fn handle_input(&mut self, input: Input) { - if self.active == ActivePopup::HelpWin { - match input { - Input::KeyExit - | Input::Character('\u{1b}') // Esc - | Input::Character('q') - | Input::Character('Q') => { - self.turn_off_help_win(); + match self.popup { + ActivePopup::HelpWin(ref mut _win) => { + match input { + Input::KeyExit + | Input::Character('\u{1b}') // Esc + | Input::Character('q') + | Input::Character('Q') => { + self.turn_off_help_win(); + } + _ => (), + } + } + ActivePopup::DownloadWin(ref mut win) => { + match self.keymap.get_from_input(input) { + Some(UserAction::Down) => win.scroll(1), + Some(UserAction::Up) => win.scroll(-1), + Some(_) | None => { + match input { + Input::KeyExit + | Input::Character('\u{1b}') // Esc + | Input::Character('q') + | Input::Character('Q') => { + self.turn_off_download_win(); + } + _ => (), + } + } } - _ => (), } + _ => (), } } } From a3be71259dd7bc0c1118c3279e73ac6949d3a0b5 Mon Sep 17 00:00:00 2001 From: Jeff Hughes Date: Fri, 25 Sep 2020 20:15:06 -0400 Subject: [PATCH 13/23] Select and deselect episodes to download --- src/types.rs | 4 ++-- src/ui/menu.rs | 54 ++++++++++++++++++++++++++++++++++++++++++++++++- src/ui/popup.rs | 19 +++++++++++++---- 3 files changed, 70 insertions(+), 7 deletions(-) diff --git a/src/types.rs b/src/types.rs index e15fbac..77a3a63 100644 --- a/src/types.rs +++ b/src/types.rs @@ -309,8 +309,8 @@ impl LockVec { return (self.data.lock().unwrap(), self.order.lock().unwrap()); } - /// Given an index in the vector, this takes a new T and replaces - /// the old T at that position in the vector. + /// Given an id, this takes a new T and replaces the old T with that + /// id. pub fn replace(&self, id: i64, t: T) { let mut borrowed = self.borrow_map(); borrowed.insert(id, t); diff --git a/src/ui/menu.rs b/src/ui/menu.rs index d0946a3..e62d71e 100644 --- a/src/ui/menu.rs +++ b/src/ui/menu.rs @@ -1,4 +1,5 @@ use std::cmp::min; +use std::collections::hash_map::Entry; use super::ColorType; use super::Panel; @@ -268,7 +269,58 @@ impl Menu { } } -impl Menu {} +impl Menu { + /// Changes the status of the currently highlighted episode -- if it + /// was selected to be downloaded, it will be unselected, and vice + /// versa. + pub fn select_item(&mut self) { + let changed = + self.change_item_selections(vec![(self.top_row + self.selected) as usize], None); + if changed { + self.update_items(); + self.highlight_selected(true); + } + } + + /// Changes the status of all items in the list. If there are any + /// unselected episodes, this will convert all episodes to be + /// selected; if all are selected already, only then will it convert + /// all to unselected. + pub fn select_all_items(&mut self) { + let all_selected = self.items.map(|ep| ep.selected).iter().all(|x| *x); + let changed = + self.change_item_selections((0..self.items.len()).collect(), Some(!all_selected)); + if changed { + self.update_items(); + self.highlight_selected(true); + } + } + + /// Given a list of index values in the menu, this changes the status + /// of these episode -- if they were selected to be downloaded, they + /// will be unselected, and vice versa. If `selection` is a boolean, + /// however, it will be set to this value explicitly rather than just + /// being reversed. + fn change_item_selections(&mut self, indexes: Vec, selection: Option) -> bool { + let mut changed = false; + { + let (mut borrowed_map, borrowed_order) = self.items.borrow(); + for idx in indexes { + if let Some(ep_id) = borrowed_order.get(idx) { + if let Entry::Occupied(mut ep) = borrowed_map.entry(*ep_id) { + let ep = ep.get_mut(); + match selection { + Some(sel) => ep.selected = sel, + None => ep.selected = !ep.selected, + } + changed = true; + } + } + } + } + return changed; + } +} // TESTS ---------------------------------------------------------------- diff --git a/src/ui/popup.rs b/src/ui/popup.rs index ba533be..68d3c28 100644 --- a/src/ui/popup.rs +++ b/src/ui/popup.rs @@ -328,7 +328,7 @@ impl<'a> PopupWin<'a> { return download_win; } - pub fn add_episodes(&mut self, mut episodes: Vec) { + pub fn _add_episodes(&mut self, mut episodes: Vec) { self.new_episodes.append(&mut episodes); } @@ -394,10 +394,21 @@ impl<'a> PopupWin<'a> { _ => (), } } - ActivePopup::DownloadWin(ref mut win) => { + ActivePopup::DownloadWin(ref mut menu) => { match self.keymap.get_from_input(input) { - Some(UserAction::Down) => win.scroll(1), - Some(UserAction::Up) => win.scroll(-1), + Some(UserAction::Down) => menu.scroll(1), + Some(UserAction::Up) => menu.scroll(-1), + + Some(UserAction::Download) + | Some(UserAction::MarkPlayed) + | Some(UserAction::Play) => { + menu.select_item(); + } + + Some(UserAction::DownloadAll) | Some(UserAction::MarkAllPlayed) => { + menu.select_all_items(); + } + Some(_) | None => { match input { Input::KeyExit From 61c861475af27355b587328f5cdb5c1d8e32fcbc Mon Sep 17 00:00:00 2001 From: Eric Douglass Date: Wed, 28 Oct 2020 23:22:27 -0700 Subject: [PATCH 14/23] ask for confirmation before removing podcasts or episodes --- src/ui/mod.rs | 21 +++++++++++++++++++++ 1 file changed, 21 insertions(+) diff --git a/src/ui/mod.rs b/src/ui/mod.rs index 167a918..6766b36 100644 --- a/src/ui/mod.rs +++ b/src/ui/mod.rs @@ -579,6 +579,11 @@ impl<'a> UI<'a> { /// Remove a podcast from the list. pub fn remove_podcast(&mut self, curr_pod_id: Option) -> Option { + let confirm = self.ask_for_confirmation("Are you sure you want to remove the podcast?"); + // If we don't get a confirmation to delete, then don't remove + if !confirm { + return None; + } let mut delete = false; if let Some(pod_id) = curr_pod_id { @@ -604,6 +609,11 @@ impl<'a> UI<'a> { curr_ep_id: Option, ) -> Option { + let confirm = self.ask_for_confirmation("Are you sure you want to remove the episode?"); + // If we don't get a confirmation to delete, then don't remove + if !confirm { + return None; + } let mut delete = false; if let Some(pod_id) = curr_pod_id { if let Some(ep_id) = curr_ep_id { @@ -708,6 +718,17 @@ impl<'a> UI<'a> { return any_downloaded; } + /// Spawns a "(y/n)" notification with the specified input `message` + /// using `spawn_input_notif`. If the the user types 'y', then the + /// function returns `true`, and 'n' returns `false`. Cancelling the + /// action returns `false` as well. + pub fn ask_for_confirmation(&self, message: &str) -> bool { + match self.spawn_yes_no_notif(message) { + Some(val) => val, + None => false + } + } + /// Adds a notification to the bottom of the screen that solicits /// user text input. A prefix can be specified as a prompt for the /// user at the beginning of the input line. This returns the user's From 89175294b4e64012b484f57fa4e54d838b579e27 Mon Sep 17 00:00:00 2001 From: Jeff Hughes Date: Sun, 8 Nov 2020 15:19:22 -0500 Subject: [PATCH 15/23] Allow header text at top of menus --- src/ui/menu.rs | 88 ++++++++++++++++++++++---------- src/ui/mod.rs | 16 ++---- src/ui/popup.rs | 133 ++++++++++++++++++++++++------------------------ 3 files changed, 132 insertions(+), 105 deletions(-) diff --git a/src/ui/menu.rs b/src/ui/menu.rs index e62d71e..529ebcc 100644 --- a/src/ui/menu.rs +++ b/src/ui/menu.rs @@ -12,25 +12,42 @@ use crate::types::*; /// /// * `screen_pos` stores the position of the window on the screen, from /// left to right -/// * `n_row` and `n_col` store the size of the `window` +/// * `start_row` indicates the first row that is used for the menu; +/// this will be 0 if there is no header; otherwise, `start_row` will +/// be the first row below the header. Calculated relative to the +/// panel, i.e., a value between 0 and (n_row - 1) /// * `top_row` indicates the top line of text that is shown on screen /// (since the list of items can be longer than the available size of /// the screen). `top_row` is calculated relative to the `items` index, /// i.e., it will be a value between 0 and items.len() /// * `selected` indicates which item on screen is currently highlighted. -/// It is calculated relative to the screen itself, i.e., a value between +/// It is calculated relative to the panel, i.e., a value between /// 0 and (n_row - 1) #[derive(Debug)] pub struct Menu where T: Clone + Menuable { pub panel: Panel, + pub header: Option, pub items: LockVec, - pub top_row: i32, // top row of text shown in window - pub selected: i32, // which line of text is highlighted + pub start_row: i32, // beginning of first row of menu + pub top_row: i32, // top row of text shown in window + pub selected: i32, // which line of text is highlighted } impl Menu { + /// Creates a new menu. + pub fn new(panel: Panel, header: Option, items: LockVec) -> Self { + return Self { + panel: panel, + header: header, + items: items, + start_row: 0, + top_row: 0, + selected: 0, + }; + } + /// Prints the list of visible items to the pancurses window and /// refreshes it. pub fn init(&mut self) { @@ -42,20 +59,23 @@ impl Menu { /// window and refreshes it. pub fn update_items(&mut self) { self.panel.erase(); + self.start_row = self.print_header(); + if self.selected < self.start_row { + self.selected = self.start_row; + } let (map, order) = self.items.borrow(); if !order.is_empty() { // update selected item if list has gotten shorter - let current_selected = self.selected + self.top_row; + let current_selected = self.get_menu_idx(self.selected) as i32; let list_len = order.len() as i32; if current_selected >= list_len { self.selected = self.selected - (current_selected - list_len) - 1; } // for visible rows, print strings from list - for i in 0..self.panel.get_rows() { - let item_idx = (self.top_row + i) as usize; - if let Some(elem_id) = order.get(item_idx) { + for i in self.start_row..self.panel.get_rows() { + if let Some(elem_id) = order.get(self.get_menu_idx(i)) { let elem = map.get(&elem_id).unwrap(); self.panel .write_line(i, elem.get_title(self.panel.get_cols() as usize)); @@ -83,6 +103,16 @@ impl Menu { self.panel.refresh(); } + /// If a header exists, prints lines of text to the panel to appear + /// above the menu. + fn print_header(&mut self) -> i32 { + if let Some(header) = &self.header { + return self.panel.write_wrap_line(0, header.clone()) + 2; + } else { + return 0; + } + } + /// Scrolls the menu up or down by `lines` lines. Negative values of /// `lines` will scroll the menu up. /// @@ -108,7 +138,7 @@ impl Menu { // don't allow scrolling past last item in list (if shorter // than self.panel.get_rows()) - let abs_bottom = min(self.panel.get_rows(), (list_len - 1) as i32); + let abs_bottom = min(self.panel.get_rows(), list_len as i32 + self.start_row - 1); if self.selected > abs_bottom { self.selected = abs_bottom; } @@ -124,7 +154,7 @@ impl Menu { }) { self.top_row += 1; - self.panel.delete_line(0); + self.panel.delete_line(self.start_row); old_selected -= 1; self.panel.delete_line(n_row - 1); @@ -132,8 +162,8 @@ impl Menu { } // scroll up - } else if self.selected < 0 { - self.selected = 0; + } else if self.selected < self.start_row { + self.selected = self.start_row; if let Some(title) = self .items .map_single_by_index((self.top_row - 1) as usize, |el| { @@ -141,18 +171,18 @@ impl Menu { }) { self.top_row -= 1; - self.panel.insert_line(0, title); + self.panel.insert_line(self.start_row, title); old_selected += 1; } } old_played = self .items - .map_single_by_index((self.top_row + old_selected) as usize, |el| el.is_played()) + .map_single_by_index(self.get_menu_idx(old_selected), |el| el.is_played()) .unwrap(); new_played = self .items - .map_single_by_index((self.top_row + self.selected) as usize, |el| el.is_played()) + .map_single_by_index(self.get_menu_idx(self.selected), |el| el.is_played()) .unwrap(); self.set_attrs(old_selected, old_played, ColorType::Normal); @@ -180,7 +210,7 @@ impl Menu { pub fn highlight_selected(&mut self, active_menu: bool) { let is_played = self .items - .map_single_by_index((self.top_row + self.selected) as usize, |el| el.is_played()); + .map_single_by_index(self.get_menu_idx(self.selected), |el| el.is_played()); if let Some(played) = is_played { if active_menu { @@ -198,7 +228,7 @@ impl Menu { // if list is empty, will return None if let Some(played) = self .items - .map_single_by_index((self.top_row + self.selected) as usize, |el| el.is_played()) + .map_single_by_index(self.get_menu_idx(self.selected), |el| el.is_played()) { self.set_attrs(self.selected, played, ColorType::HighlightedActive); self.panel.refresh(); @@ -217,6 +247,15 @@ impl Menu { self.selected = n_row - 1; } } + + /// Given a row on the panel, this translates it into the + /// corresponding menu item it represents. Note that this does not + /// do any checks to ensure `screen_y` is between 0 and `n_rows`, + /// or that the resulting menu index is between 0 and `n_items`. + /// It's merely a straight translation. + pub fn get_menu_idx(&self, screen_y: i32) -> usize { + return (self.top_row + screen_y - self.start_row) as usize; + } } @@ -224,13 +263,8 @@ impl Menu { /// Returns a cloned reference to the list of episodes from the /// currently selected podcast. pub fn get_episodes(&self) -> LockVec { - let index = self.selected + self.top_row; - let pod_id = self - .items - .borrow_order() - .get(index as usize) - .copied() - .unwrap(); + let index = self.get_menu_idx(self.selected); + let pod_id = self.items.borrow_order().get(index).copied().unwrap(); return self .items .borrow_map() @@ -246,7 +280,7 @@ impl Menu { // if list is empty, will return None if let Some(played) = self .items - .map_single_by_index((self.top_row + self.selected) as usize, |el| el.is_played()) + .map_single_by_index(self.get_menu_idx(self.selected), |el| el.is_played()) { self.set_attrs(self.selected, played, ColorType::Highlighted); self.panel.refresh(); @@ -261,7 +295,7 @@ impl Menu { // if list is empty, will return None if let Some(played) = self .items - .map_single_by_index((self.top_row + self.selected) as usize, |el| el.is_played()) + .map_single_by_index(self.get_menu_idx(self.selected), |el| el.is_played()) { self.set_attrs(self.selected, played, ColorType::Normal); self.panel.refresh(); @@ -366,7 +400,9 @@ mod tests { ); return Menu { panel: panel, + header: None, items: LockVec::new(items), + start_row: 0, top_row: top_row, selected: selected, }; diff --git a/src/ui/mod.rs b/src/ui/mod.rs index 167a918..7e19e59 100644 --- a/src/ui/mod.rs +++ b/src/ui/mod.rs @@ -167,12 +167,7 @@ impl<'a> UI<'a> { 0, 0, ); - let podcast_menu = Menu { - panel: podcast_panel, - items: items.clone(), - top_row: 0, - selected: 0, - }; + let podcast_menu = Menu::new(podcast_panel, None, items.clone()); let episode_panel = Panel::new( colors.clone(), @@ -192,12 +187,7 @@ impl<'a> UI<'a> { None => LockVec::new(Vec::new()), }; - let episode_menu = Menu { - panel: episode_panel, - items: first_pod, - top_row: 0, - selected: 0, - }; + let episode_menu = Menu::new(episode_panel, None, first_pod); let details_panel = if n_col > crate::config::DETAILS_PANEL_LENGTH { Some(Self::make_details_panel( @@ -242,6 +232,8 @@ impl<'a> UI<'a> { if self.podcast_menu.items.is_empty() { self.popup_win.spawn_welcome_win(); } + + self.popup_win.spawn_download_win(); } /// Waits for user input and, where necessary, provides UiMsgs diff --git a/src/ui/popup.rs b/src/ui/popup.rs index 68d3c28..ab9c41d 100644 --- a/src/ui/popup.rs +++ b/src/ui/popup.rs @@ -1,4 +1,5 @@ use pancurses::Input; +use std::cmp::min; use super::{ColorType, Colors}; use super::{Menu, Panel}; @@ -16,31 +17,19 @@ pub enum ActivePopup { impl ActivePopup { pub fn is_welcome_win(&self) -> bool { - match self { - ActivePopup::WelcomeWin(_) => true, - _ => false, - } + return matches!(self, ActivePopup::WelcomeWin(_)); } pub fn is_help_win(&self) -> bool { - match self { - ActivePopup::HelpWin(_) => true, - _ => false, - } + return matches!(self, ActivePopup::HelpWin(_)); } pub fn is_download_win(&self) -> bool { - match self { - ActivePopup::DownloadWin(_) => true, - _ => false, - } + return matches!(self, ActivePopup::DownloadWin(_)); } pub fn is_none(&self) -> bool { - match self { - ActivePopup::None => true, - _ => false, - } + return matches!(self, ActivePopup::None); } } @@ -101,7 +90,11 @@ impl<'a> PopupWin<'a> { help_win.refresh(); self.popup = ActivePopup::HelpWin(help_win); } - ActivePopup::DownloadWin(_win) => (), // not yet implemented + ActivePopup::DownloadWin(_win) => { + let mut download_win = self.make_download_win(); + download_win.highlight_selected(true); + self.popup = ActivePopup::DownloadWin(download_win); + } ActivePopup::None => (), } // if let Some(panel) = &self.panel { @@ -122,24 +115,7 @@ impl<'a> PopupWin<'a> { let actions = vec![UserAction::AddFeed, UserAction::Quit, UserAction::Help]; let mut key_strs = Vec::new(); for action in actions { - let keys = self.keymap.keys_for_action(action); - let key_str = match keys.len() { - 0 => "".to_string(), - 1 => format!("\"{}\"", &keys[0]), - 2 => format!("\"{}\" or \"{}\"", &keys[0], &keys[1]), - _ => { - let mut s = "".to_string(); - for i in 0..keys.len() { - if i == keys.len() - 1 { - s = format!("{}, \"{}\"", s, keys[i]); - } else { - s = format!("{}, or \"{}\"", s, keys[i]); - } - } - s - } - }; - key_strs.push(key_str); + key_strs.push(self.list_keys(action, None)); } // the warning on the unused mut is a function of Rust getting @@ -309,7 +285,7 @@ impl<'a> PopupWin<'a> { #[allow(unused_mut)] let mut download_panel = Panel::new( self.colors.clone(), - "Downloads".to_string(), + "New episodes".to_string(), 0, self.total_rows - 1, self.total_cols, @@ -317,12 +293,16 @@ impl<'a> PopupWin<'a> { 0, ); - let mut download_win = Menu:: { - panel: download_panel, - items: LockVec::new(self.new_episodes.clone()), - top_row: 0, - selected: 0, - }; + let header = format!( + "Select which episodes to download with {}. Select all/none with {}. Press {} to confirm the selection and exit the menu.", + self.list_keys(UserAction::MarkPlayed, Some(2)), + self.list_keys(UserAction::MarkAllPlayed, Some(2)), + self.list_keys(UserAction::Quit, Some(2))); + let mut download_win = Menu::new( + download_panel, + Some(header), + LockVec::new(self.new_episodes.clone()), + ); download_win.init(); return download_win; @@ -394,35 +374,54 @@ impl<'a> PopupWin<'a> { _ => (), } } - ActivePopup::DownloadWin(ref mut menu) => { - match self.keymap.get_from_input(input) { - Some(UserAction::Down) => menu.scroll(1), - Some(UserAction::Up) => menu.scroll(-1), - - Some(UserAction::Download) - | Some(UserAction::MarkPlayed) - | Some(UserAction::Play) => { - menu.select_item(); - } + ActivePopup::DownloadWin(ref mut menu) => match self.keymap.get_from_input(input) { + Some(UserAction::Down) => menu.scroll(1), + Some(UserAction::Up) => menu.scroll(-1), - Some(UserAction::DownloadAll) | Some(UserAction::MarkAllPlayed) => { - menu.select_all_items(); - } + Some(UserAction::MarkPlayed) => { + menu.select_item(); + } - Some(_) | None => { - match input { - Input::KeyExit - | Input::Character('\u{1b}') // Esc - | Input::Character('q') - | Input::Character('Q') => { - self.turn_off_download_win(); - } - _ => (), - } - } + Some(UserAction::MarkAllPlayed) => { + menu.select_all_items(); } - } + + Some(UserAction::Quit) => { + self.turn_off_download_win(); + } + + Some(_) | None => (), + }, _ => (), } } + + + /// Helper function that gets the keybindings for a particular + /// user action, and converts it to a string, e.g., '"a", "b", or + /// "c"'. If `max_num` is set, will only list up to that number of + /// items. + fn list_keys(&self, action: UserAction, max_num: Option) -> String { + let keys = self.keymap.keys_for_action(action); + let mut max_keys = keys.len(); + if let Some(max_num) = max_num { + max_keys = min(keys.len(), max_num); + } + return match max_keys { + 0 => "".to_string(), + 1 => format!("\"{}\"", &keys[0]), + 2 => format!("\"{}\" or \"{}\"", &keys[0], &keys[1]), + _ => { + let mut s = "".to_string(); + for (i, key) in keys.iter().enumerate().take(max_keys) { + if i == max_keys - 1 { + s = format!("{}, \"{}\"", s, key); + } else { + s = format!("{}, or \"{}\"", s, key); + } + } + s + } + }; + } } From 821750069c5039215b8abb790cbae050b41174b2 Mon Sep 17 00:00:00 2001 From: Jeff Hughes Date: Sun, 8 Nov 2020 15:28:06 -0500 Subject: [PATCH 16/23] Fix selecting by index in popup download window --- src/types.rs | 2 +- src/ui/menu.rs | 3 +-- 2 files changed, 2 insertions(+), 3 deletions(-) diff --git a/src/types.rs b/src/types.rs index 77a3a63..adff788 100644 --- a/src/types.rs +++ b/src/types.rs @@ -260,7 +260,7 @@ impl Menuable for NewEpisode { } fn is_played(&self) -> bool { - return false; + return true; } } diff --git a/src/ui/menu.rs b/src/ui/menu.rs index 529ebcc..e7a409c 100644 --- a/src/ui/menu.rs +++ b/src/ui/menu.rs @@ -308,8 +308,7 @@ impl Menu { /// was selected to be downloaded, it will be unselected, and vice /// versa. pub fn select_item(&mut self) { - let changed = - self.change_item_selections(vec![(self.top_row + self.selected) as usize], None); + let changed = self.change_item_selections(vec![self.get_menu_idx(self.selected)], None); if changed { self.update_items(); self.highlight_selected(true); From e8970cc159f2a6d5fb1c57861d001952c0d2a546 Mon Sep 17 00:00:00 2001 From: Jeff Hughes Date: Sun, 8 Nov 2020 16:53:47 -0500 Subject: [PATCH 17/23] Open download popup window on sync --- src/db.rs | 30 +++++++++++++++++++++++++----- src/main_controller.rs | 14 ++++++++++++++ src/ui/mod.rs | 9 ++++++--- src/ui/popup.rs | 37 +++++++++++++++++++------------------ 4 files changed, 64 insertions(+), 26 deletions(-) diff --git a/src/db.rs b/src/db.rs index 320f037..7c89b20 100644 --- a/src/db.rs +++ b/src/db.rs @@ -16,7 +16,7 @@ lazy_static! { pub struct SyncResult { - pub added: Vec, + pub added: Vec, pub updated: Vec, } @@ -207,7 +207,14 @@ impl Database { let mut ep_ids = Vec::new(); for ep in podcast.episodes.iter().rev() { let id = self.insert_episode(pod_id, &ep)?; - ep_ids.push(id); + let new_ep = NewEpisode { + id: id, + pod_id: pod_id, + title: ep.title.clone(), + pod_title: podcast.title.clone(), + selected: false, + }; + ep_ids.push(new_ep); } return Ok(SyncResult { @@ -328,7 +335,7 @@ impl Database { ], )?; - let result = self.update_episodes(pod_id, podcast.episodes); + let result = self.update_episodes(pod_id, podcast.title, podcast.episodes); return Ok(result); } @@ -340,7 +347,13 @@ impl Database { /// episode that has changed either of these fields will show up as /// a "new" episode. The old version will still remain in the /// database. - fn update_episodes(&self, podcast_id: i64, episodes: Vec) -> SyncResult { + fn update_episodes( + &self, + podcast_id: i64, + podcast_title: String, + episodes: Vec, + ) -> SyncResult + { let conn = self.conn.as_ref().unwrap(); let old_episodes = self.get_episodes(podcast_id); @@ -411,7 +424,14 @@ impl Database { } None => { let id = self.insert_episode(podcast_id, &new_ep).unwrap(); - insert_ep.push(id); + let new_ep = NewEpisode { + id: id, + pod_id: podcast_id, + title: new_ep.title.clone(), + pod_title: podcast_title.clone(), + selected: false, + }; + insert_ep.push(new_ep); } } } diff --git a/src/main_controller.rs b/src/main_controller.rs index 55d26fe..782a2e1 100644 --- a/src/main_controller.rs +++ b/src/main_controller.rs @@ -21,6 +21,7 @@ pub enum MainMessage { UiSpawnNotif(String, bool, u64), UiSpawnPersistentNotif(String, bool), UiClearPersistentNotif, + UiSpawnDownloadPopup(Vec), UiTearDown, } @@ -122,6 +123,12 @@ impl MainController { Message::Ui(UiMsg::Download(pod_id, ep_id)) => self.download(pod_id, Some(ep_id)), + Message::Ui(UiMsg::DownloadMulti(vec)) => { + for (pod_id, ep_id) in vec.into_iter() { + self.download(pod_id, Some(ep_id)); + } + } + Message::Ui(UiMsg::DownloadAll(pod_id)) => self.download(pod_id, None), // downloading can produce any one of these responses @@ -290,9 +297,11 @@ impl MainController { // episodes when sync process is finished let mut added = 0; let mut updated = 0; + let mut new_eps = Vec::new(); for res in self.sync_tracker.iter() { added += res.added.len(); updated += res.updated.len(); + new_eps.extend(res.added.clone()); } self.sync_tracker = Vec::new(); self.notif_to_ui( @@ -302,6 +311,11 @@ impl MainController { ), false, ); + if !new_eps.is_empty() { + self.tx_to_ui + .send(MainMessage::UiSpawnDownloadPopup(new_eps)) + .unwrap(); + } } } else { self.notif_to_ui( diff --git a/src/ui/mod.rs b/src/ui/mod.rs index 7e19e59..612f439 100644 --- a/src/ui/mod.rs +++ b/src/ui/mod.rs @@ -52,6 +52,7 @@ pub enum UiMsg { Sync(i64), SyncAll, Download(i64, i64), + DownloadMulti(Vec<(i64, i64)>), DownloadAll(i64), Delete(i64, i64), DeleteAll(i64), @@ -127,6 +128,9 @@ impl<'a> UI<'a> { ui.tear_down(); break; } + MainMessage::UiSpawnDownloadPopup(episodes) => { + ui.popup_win.spawn_download_win(episodes); + } } } @@ -232,8 +236,6 @@ impl<'a> UI<'a> { if self.podcast_menu.items.is_empty() { self.popup_win.spawn_welcome_win(); } - - self.popup_win.spawn_download_win(); } /// Waits for user input and, where necessary, provides UiMsgs @@ -261,7 +263,7 @@ impl<'a> UI<'a> { // welcome window which takes no input), then // redirect user input there if self.popup_win.is_non_welcome_popup_active() { - self.popup_win.handle_input(input); + let popup_msg = self.popup_win.handle_input(input); // need to check if popup window is still active, as // handling character input above may involve @@ -273,6 +275,7 @@ impl<'a> UI<'a> { self.update_details_panel(); } } + return popup_msg; } else { match self.keymap.get_from_input(input) { Some(a @ UserAction::Down) diff --git a/src/ui/popup.rs b/src/ui/popup.rs index ab9c41d..3721237 100644 --- a/src/ui/popup.rs +++ b/src/ui/popup.rs @@ -2,7 +2,7 @@ use pancurses::Input; use std::cmp::min; use super::{ColorType, Colors}; -use super::{Menu, Panel}; +use super::{Menu, Panel, UiMsg}; use crate::keymap::{Keybindings, UserAction}; use crate::types::*; @@ -257,23 +257,10 @@ impl<'a> PopupWin<'a> { } /// Create a new download window and draw it to the screen. - pub fn spawn_download_win(&mut self) { - // TODO: This will be moved to main controller - let ep_titles = vec![ - "Episode title", - "Here's another episode", - "This one is a great episode", - ]; - for (i, title) in ep_titles.iter().enumerate() { - self.new_episodes.push(NewEpisode { - id: i as i64, - pod_id: 0, - title: title.to_string(), - pod_title: "This is a podcast".to_string(), - selected: i == 0, - }); + pub fn spawn_download_win(&mut self, episodes: Vec) { + for ep in episodes { + self.new_episodes.push(ep); } - self.download_win = true; self.change_win(); } @@ -361,7 +348,8 @@ impl<'a> PopupWin<'a> { /// When a popup window is active, this handles the user's keyboard /// input that is relevant for that window. - pub fn handle_input(&mut self, input: Input) { + pub fn handle_input(&mut self, input: Input) -> UiMsg { + let mut msg = UiMsg::Noop; match self.popup { ActivePopup::HelpWin(ref mut _win) => { match input { @@ -387,6 +375,18 @@ impl<'a> PopupWin<'a> { } Some(UserAction::Quit) => { + let mut eps_to_download = Vec::new(); + { + let map = menu.items.borrow_map(); + for (_, ep) in map.iter() { + if ep.selected { + eps_to_download.push((ep.pod_id, ep.id)); + } + } + } + if !eps_to_download.is_empty() { + msg = UiMsg::DownloadMulti(eps_to_download); + } self.turn_off_download_win(); } @@ -394,6 +394,7 @@ impl<'a> PopupWin<'a> { }, _ => (), } + return msg; } From cc96a3296971f6489df7a20d6fe8bd7f2e9464ac Mon Sep 17 00:00:00 2001 From: Jeff Hughes Date: Tue, 10 Nov 2020 18:48:12 -0500 Subject: [PATCH 18/23] Fix bug with input messages not starting at beginning of line --- src/ui/mod.rs | 10 +++++----- src/ui/notification.rs | 2 +- 2 files changed, 6 insertions(+), 6 deletions(-) diff --git a/src/ui/mod.rs b/src/ui/mod.rs index 10f3cbe..876d4b7 100644 --- a/src/ui/mod.rs +++ b/src/ui/mod.rs @@ -713,14 +713,14 @@ impl<'a> UI<'a> { return any_downloaded; } - /// Spawns a "(y/n)" notification with the specified input `message` - /// using `spawn_input_notif`. If the the user types 'y', then the - /// function returns `true`, and 'n' returns `false`. Cancelling the - /// action returns `false` as well. + /// Spawns a "(y/n)" notification with the specified input + /// `message` using `spawn_input_notif`. If the the user types + /// 'y', then the function returns `true`, and 'n' returns + /// `false`. Cancelling the action returns `false` as well. pub fn ask_for_confirmation(&self, message: &str) -> bool { match self.spawn_yes_no_notif(message) { Some(val) => val, - None => false + None => false, } } diff --git a/src/ui/notification.rs b/src/ui/notification.rs index 1c2a996..e446a5d 100644 --- a/src/ui/notification.rs +++ b/src/ui/notification.rs @@ -172,7 +172,7 @@ impl NotifWin { } pancurses::curs_set(0); - self.window.deleteln(); + self.window.clear(); self.window.refresh(); if cancelled { From 40a14cc24990f2c536ce87fb59de4b818388e5e6 Mon Sep 17 00:00:00 2001 From: Jeff Hughes Date: Tue, 10 Nov 2020 19:27:05 -0500 Subject: [PATCH 19/23] Set background on new ncurses windows --- src/ui/notification.rs | 6 ++++++ src/ui/panel.rs | 6 ++++++ 2 files changed, 12 insertions(+) diff --git a/src/ui/notification.rs b/src/ui/notification.rs index e446a5d..5c43bac 100644 --- a/src/ui/notification.rs +++ b/src/ui/notification.rs @@ -97,6 +97,9 @@ impl NotifWin { // otherwise, there was a notification before but there // isn't now, so erase self.window.erase(); + self.window.bkgd(pancurses::ColorPair( + self.colors.get(ColorType::Normal) as u8 + )); self.window.refresh(); self.current_msg = None; } @@ -248,6 +251,9 @@ impl NotifWin { ); oldwin.delwin(); + self.window.bkgd(pancurses::ColorPair( + self.colors.get(ColorType::Normal) as u8 + )); if let Some(curr) = &self.current_msg { self.display_notif(curr.clone()); } diff --git a/src/ui/panel.rs b/src/ui/panel.rs index c858184..0f185b8 100644 --- a/src/ui/panel.rs +++ b/src/ui/panel.rs @@ -60,6 +60,9 @@ impl Panel { /// Redraws borders and refreshes the window to display on terminal. pub fn refresh(&self) { + self.window.bkgd(pancurses::ColorPair( + self.colors.get(ColorType::Normal) as u8 + )); self.draw_border(); self.window.refresh(); } @@ -96,6 +99,9 @@ impl Panel { /// not refresh the screen. pub fn erase(&self) { self.window.erase(); + self.window.bkgd(pancurses::ColorPair( + self.colors.get(ColorType::Normal) as u8 + )); self.draw_border(); } From 4788c39bcb13a6ec938485be64fdc9534ace5bf6 Mon Sep 17 00:00:00 2001 From: Jeff Hughes Date: Tue, 10 Nov 2020 20:03:10 -0500 Subject: [PATCH 20/23] Add config option for how to deal with new episodes --- config.toml | 15 ++++++++++++++- src/config.rs | 22 ++++++++++++++++++++++ src/main_controller.rs | 28 +++++++++++++++++++++++----- src/ui/mod.rs | 4 ++-- src/ui/popup.rs | 5 +++-- 5 files changed, 64 insertions(+), 10 deletions(-) diff --git a/config.toml b/config.toml index ffc8dea..82b1c37 100644 --- a/config.toml +++ b/config.toml @@ -14,7 +14,20 @@ # will be entered to the command. # Default: vlc %s -play_command = "vlc %s" +#play_command = "vlc %s" + + +# Configures what happens when new episodes are found as podcasts are +# synced: +# - "always" will automatically download all new episodes; +# - "ask-selected" will open a popup window to let you select which +# episodes to download, with all of them selected by default; +# - "ask-unselected" will open a popup window to let you select which +# episodes to download, with NONE of them selected by default; +# - "never" will never automatically download new episodes. +# Default: "ask-unselected" + +#download_new_episodes = "ask-unselected" # Maximum number of files to download simultaneously. Setting this too diff --git a/src/config.rs b/src/config.rs index 4f65526..ff49265 100644 --- a/src/config.rs +++ b/src/config.rs @@ -26,11 +26,22 @@ pub const EPISODE_PUBDATE_LENGTH: usize = 60; pub const DETAILS_PANEL_LENGTH: i32 = 135; +/// Identifies the user's selection for what to do with new episodes +/// when syncing. +#[derive(Debug, Clone)] +pub enum DownloadNewEpisodes { + Always, + AskSelected, + AskUnselected, + Never, +} + /// Holds information about user configuration of program. #[derive(Debug, Clone)] pub struct Config { pub download_path: PathBuf, pub play_command: String, + pub download_new_episodes: DownloadNewEpisodes, pub simultaneous_downloads: usize, pub max_retries: usize, pub keybindings: Keybindings, @@ -42,6 +53,7 @@ pub struct Config { struct ConfigFromToml { download_path: Option, play_command: Option, + download_new_episodes: Option, simultaneous_downloads: Option, max_retries: Option, keybindings: KeybindingsFromToml, @@ -113,6 +125,7 @@ impl Config { config_toml = ConfigFromToml { download_path: None, play_command: None, + download_new_episodes: None, simultaneous_downloads: None, max_retries: None, keybindings: keybindings, @@ -176,6 +189,14 @@ fn config_with_defaults(config_toml: &ConfigFromToml) -> Config { None => "vlc %s".to_string(), }; + let download_new_episodes = match config_toml.download_new_episodes.as_deref() { + Some("always") => DownloadNewEpisodes::Always, + Some("ask-selected") => DownloadNewEpisodes::AskSelected, + Some("ask-unselected") => DownloadNewEpisodes::AskUnselected, + Some("never") => DownloadNewEpisodes::Never, + Some(_) | None => DownloadNewEpisodes::AskUnselected, + }; + let simultaneous_downloads = match config_toml.simultaneous_downloads { Some(num) if num > 0 => num, Some(_) => 3, @@ -191,6 +212,7 @@ fn config_with_defaults(config_toml: &ConfigFromToml) -> Config { return Config { download_path: download_path, play_command: play_command, + download_new_episodes: download_new_episodes, simultaneous_downloads: simultaneous_downloads, max_retries: max_retries, keybindings: keymap, diff --git a/src/main_controller.rs b/src/main_controller.rs index 782a2e1..c5f1463 100644 --- a/src/main_controller.rs +++ b/src/main_controller.rs @@ -5,7 +5,7 @@ use std::sync::mpsc; use sanitize_filename::{sanitize_with_options, Options}; -use crate::config::Config; +use crate::config::{Config, DownloadNewEpisodes}; use crate::db::{Database, SyncResult}; use crate::downloads::{self, DownloadMsg, EpData}; use crate::feeds::{self, FeedMsg, PodcastFeed}; @@ -21,7 +21,7 @@ pub enum MainMessage { UiSpawnNotif(String, bool, u64), UiSpawnPersistentNotif(String, bool), UiClearPersistentNotif, - UiSpawnDownloadPopup(Vec), + UiSpawnDownloadPopup(Vec, bool), UiTearDown, } @@ -311,10 +311,28 @@ impl MainController { ), false, ); + + // deal with new episodes once syncing is + // complete, based on user preferences if !new_eps.is_empty() { - self.tx_to_ui - .send(MainMessage::UiSpawnDownloadPopup(new_eps)) - .unwrap(); + match self.config.download_new_episodes { + DownloadNewEpisodes::Always => { + for ep in new_eps.into_iter() { + self.download(ep.pod_id, Some(ep.id)); + } + } + DownloadNewEpisodes::AskSelected => { + self.tx_to_ui + .send(MainMessage::UiSpawnDownloadPopup(new_eps, true)) + .unwrap(); + } + DownloadNewEpisodes::AskUnselected => { + self.tx_to_ui + .send(MainMessage::UiSpawnDownloadPopup(new_eps, false)) + .unwrap(); + } + _ => (), + } } } } else { diff --git a/src/ui/mod.rs b/src/ui/mod.rs index 876d4b7..a5a4e1b 100644 --- a/src/ui/mod.rs +++ b/src/ui/mod.rs @@ -128,8 +128,8 @@ impl<'a> UI<'a> { ui.tear_down(); break; } - MainMessage::UiSpawnDownloadPopup(episodes) => { - ui.popup_win.spawn_download_win(episodes); + MainMessage::UiSpawnDownloadPopup(episodes, selected) => { + ui.popup_win.spawn_download_win(episodes, selected); } } } diff --git a/src/ui/popup.rs b/src/ui/popup.rs index 3721237..c109b7f 100644 --- a/src/ui/popup.rs +++ b/src/ui/popup.rs @@ -257,8 +257,9 @@ impl<'a> PopupWin<'a> { } /// Create a new download window and draw it to the screen. - pub fn spawn_download_win(&mut self, episodes: Vec) { - for ep in episodes { + pub fn spawn_download_win(&mut self, episodes: Vec, selected: bool) { + for mut ep in episodes { + ep.selected = selected; self.new_episodes.push(ep); } self.download_win = true; From 4f3b67067640a7b5cf51fa4ba2874cecc70d5d23 Mon Sep 17 00:00:00 2001 From: Jeff Hughes Date: Mon, 23 Nov 2020 18:38:07 -0500 Subject: [PATCH 21/23] Upgrade dependencies --- Cargo.lock | 904 +++++++++++++++++++++++++++-------------------------- 1 file changed, 469 insertions(+), 435 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index a2f0534..29d4947 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -2,1205 +2,1239 @@ # It is not intended for manual editing. [[package]] name = "aho-corasick" -version = "0.7.13" +version = "0.7.15" source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7404febffaa47dac81aa44dba71523c9d069b1bdc50a77db41195149e17f68e5" dependencies = [ - "memchr 2.3.3 (registry+https://github.com/rust-lang/crates.io-index)", + "memchr", ] [[package]] name = "ansi_term" version = "0.11.0" source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ee49baf6cb617b853aa8d93bf420db2383fab46d314482ca2803b40d5fde979b" dependencies = [ - "winapi 0.3.9 (registry+https://github.com/rust-lang/crates.io-index)", + "winapi", ] [[package]] name = "arrayref" version = "0.3.6" source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a4c527152e37cf757a3f78aae5a06fbeefdb07ccc535c980a3208ee3060dd544" [[package]] name = "arrayvec" -version = "0.5.1" +version = "0.5.2" source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "23b62fc65de8e4e7f52534fb52b0f3ed04746ae267519eef2a83941e8085068b" [[package]] name = "atty" version = "0.2.14" source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d9b39be18770d11421cdb1b9947a45dd3f37e93092cbf377614828a319d5fee8" dependencies = [ - "hermit-abi 0.1.15 (registry+https://github.com/rust-lang/crates.io-index)", - "libc 0.2.74 (registry+https://github.com/rust-lang/crates.io-index)", - "winapi 0.3.9 (registry+https://github.com/rust-lang/crates.io-index)", + "hermit-abi", + "libc", + "winapi", ] [[package]] name = "autocfg" -version = "1.0.0" +version = "1.0.1" source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cdb031dd78e28731d87d56cc8ffef4a8f36ca26c38fe2de700543e627f8a464a" [[package]] name = "base64" -version = "0.11.0" +version = "0.12.3" source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3441f0f7b02788e948e47f457ca01f1d7e6d92c693bc132c22b087d3141c03ff" [[package]] name = "base64" -version = "0.12.3" +version = "0.13.0" source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "904dfeac50f3cdaba28fc6f57fdcddb75f49ed61346676a78c4ffe55877802fd" [[package]] name = "bitflags" version = "1.2.1" source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cf1de2fe8c75bc145a2f577add951f8134889b4795d47466a54a5c846d691693" [[package]] name = "blake2b_simd" -version = "0.5.10" +version = "0.5.11" source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "afa748e348ad3be8263be728124b24a24f268266f6f5d58af9d75f6a40b5c587" dependencies = [ - "arrayref 0.3.6 (registry+https://github.com/rust-lang/crates.io-index)", - "arrayvec 0.5.1 (registry+https://github.com/rust-lang/crates.io-index)", - "constant_time_eq 0.1.5 (registry+https://github.com/rust-lang/crates.io-index)", + "arrayref", + "arrayvec", + "constant_time_eq", ] [[package]] name = "bumpalo" version = "3.4.0" source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2e8c087f005730276d1096a652e92a8bacee2e2472bcc9715a74d2bec38b5820" [[package]] name = "cc" -version = "1.0.58" +version = "1.0.65" source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "95752358c8f7552394baf48cd82695b345628ad3f170d607de3ca03b8dacca15" [[package]] name = "cfg-if" version = "0.1.10" source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4785bdd1c96b2a846b2bd7cc02e86b6b3dbf14e7e53446c4f54c92a361040822" + +[[package]] +name = "cfg-if" +version = "1.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "baf1de4339761588bc0619e3cbc0120ee582ebb74b53b4efbf79117bd2da40fd" [[package]] name = "chrono" -version = "0.4.15" +version = "0.4.19" source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "670ad68c9088c2a963aaa298cb369688cf3f9465ce5e2d4ca10e6e0098a1ce73" dependencies = [ - "num-integer 0.1.43 (registry+https://github.com/rust-lang/crates.io-index)", - "num-traits 0.2.12 (registry+https://github.com/rust-lang/crates.io-index)", - "time 0.1.43 (registry+https://github.com/rust-lang/crates.io-index)", + "libc", + "num-integer", + "num-traits", + "time", + "winapi", ] [[package]] name = "chunked_transfer" -version = "1.2.0" +version = "1.3.0" source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7477065d45a8fe57167bf3cf8bcd3729b54cfcb81cca49bda2d038ea89ae82ca" [[package]] name = "clap" version = "2.33.3" source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "37e58ac78573c40708d45522f0d80fa2f01cc4f9b4e2bf749807255454312002" dependencies = [ - "ansi_term 0.11.0 (registry+https://github.com/rust-lang/crates.io-index)", - "atty 0.2.14 (registry+https://github.com/rust-lang/crates.io-index)", - "bitflags 1.2.1 (registry+https://github.com/rust-lang/crates.io-index)", - "strsim 0.8.0 (registry+https://github.com/rust-lang/crates.io-index)", - "textwrap 0.11.0 (registry+https://github.com/rust-lang/crates.io-index)", - "unicode-width 0.1.8 (registry+https://github.com/rust-lang/crates.io-index)", - "vec_map 0.8.2 (registry+https://github.com/rust-lang/crates.io-index)", + "ansi_term", + "atty", + "bitflags", + "strsim 0.8.0", + "textwrap 0.11.0", + "unicode-width", + "vec_map", ] [[package]] name = "constant_time_eq" version = "0.1.5" source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "245097e9a4535ee1e3e3931fcfcd55a796a44c643e8596ff6566d68f09b87bbc" [[package]] name = "core-foundation" -version = "0.7.0" +version = "0.9.1" source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0a89e2ae426ea83155dccf10c0fa6b1463ef6d5fcb44cee0b224a408fa640a62" dependencies = [ - "core-foundation-sys 0.7.0 (registry+https://github.com/rust-lang/crates.io-index)", - "libc 0.2.74 (registry+https://github.com/rust-lang/crates.io-index)", + "core-foundation-sys", + "libc", ] [[package]] name = "core-foundation-sys" -version = "0.7.0" +version = "0.8.2" source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ea221b5284a47e40033bf9b66f35f984ec0ea2931eb03505246cd27a963f981b" [[package]] name = "crossbeam-utils" version = "0.7.2" source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c3c7c73a2d1e9fc0886a08b93e98eb643461230d5f1925e4036204d5f2e261a8" dependencies = [ - "autocfg 1.0.0 (registry+https://github.com/rust-lang/crates.io-index)", - "cfg-if 0.1.10 (registry+https://github.com/rust-lang/crates.io-index)", - "lazy_static 1.4.0 (registry+https://github.com/rust-lang/crates.io-index)", + "autocfg", + "cfg-if 0.1.10", + "lazy_static", ] [[package]] name = "darling" version = "0.10.2" source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0d706e75d87e35569db781a9b5e2416cff1236a47ed380831f959382ccd5f858" dependencies = [ - "darling_core 0.10.2 (registry+https://github.com/rust-lang/crates.io-index)", - "darling_macro 0.10.2 (registry+https://github.com/rust-lang/crates.io-index)", + "darling_core", + "darling_macro", ] [[package]] name = "darling_core" version = "0.10.2" source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f0c960ae2da4de88a91b2d920c2a7233b400bc33cb28453a2987822d8392519b" dependencies = [ - "fnv 1.0.7 (registry+https://github.com/rust-lang/crates.io-index)", - "ident_case 1.0.1 (registry+https://github.com/rust-lang/crates.io-index)", - "proc-macro2 1.0.19 (registry+https://github.com/rust-lang/crates.io-index)", - "quote 1.0.7 (registry+https://github.com/rust-lang/crates.io-index)", - "strsim 0.9.3 (registry+https://github.com/rust-lang/crates.io-index)", - "syn 1.0.38 (registry+https://github.com/rust-lang/crates.io-index)", + "fnv", + "ident_case", + "proc-macro2", + "quote", + "strsim 0.9.3", + "syn", ] [[package]] name = "darling_macro" version = "0.10.2" source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d9b5a2f4ac4969822c62224815d069952656cadc7084fdca9751e6d959189b72" dependencies = [ - "darling_core 0.10.2 (registry+https://github.com/rust-lang/crates.io-index)", - "quote 1.0.7 (registry+https://github.com/rust-lang/crates.io-index)", - "syn 1.0.38 (registry+https://github.com/rust-lang/crates.io-index)", + "darling_core", + "quote", + "syn", ] [[package]] name = "derive_builder" version = "0.9.0" source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a2658621297f2cf68762a6f7dc0bb7e1ff2cfd6583daef8ee0fed6f7ec468ec0" dependencies = [ - "darling 0.10.2 (registry+https://github.com/rust-lang/crates.io-index)", - "derive_builder_core 0.9.0 (registry+https://github.com/rust-lang/crates.io-index)", - "proc-macro2 1.0.19 (registry+https://github.com/rust-lang/crates.io-index)", - "quote 1.0.7 (registry+https://github.com/rust-lang/crates.io-index)", - "syn 1.0.38 (registry+https://github.com/rust-lang/crates.io-index)", + "darling", + "derive_builder_core", + "proc-macro2", + "quote", + "syn", ] [[package]] name = "derive_builder_core" version = "0.9.0" source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2791ea3e372c8495c0bc2033991d76b512cd799d07491fbd6890124db9458bef" dependencies = [ - "darling 0.10.2 (registry+https://github.com/rust-lang/crates.io-index)", - "proc-macro2 1.0.19 (registry+https://github.com/rust-lang/crates.io-index)", - "quote 1.0.7 (registry+https://github.com/rust-lang/crates.io-index)", - "syn 1.0.38 (registry+https://github.com/rust-lang/crates.io-index)", + "darling", + "proc-macro2", + "quote", + "syn", ] [[package]] name = "dirs" version = "2.0.2" source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "13aea89a5c93364a98e9b37b2fa237effbb694d5cfe01c5b70941f7eb087d5e3" dependencies = [ - "cfg-if 0.1.10 (registry+https://github.com/rust-lang/crates.io-index)", - "dirs-sys 0.3.5 (registry+https://github.com/rust-lang/crates.io-index)", + "cfg-if 0.1.10", + "dirs-sys", ] [[package]] name = "dirs-next" -version = "1.0.1" +version = "1.0.2" source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cf36e65a80337bea855cd4ef9b8401ffce06a7baedf2e85ec467b1ac3f6e82b6" dependencies = [ - "cfg-if 0.1.10 (registry+https://github.com/rust-lang/crates.io-index)", - "dirs-sys-next 0.1.0 (registry+https://github.com/rust-lang/crates.io-index)", + "cfg-if 1.0.0", + "dirs-sys-next", ] [[package]] name = "dirs-sys" version = "0.3.5" source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8e93d7f5705de3e49895a2b5e0b8855a1c27f080192ae9c32a6432d50741a57a" dependencies = [ - "libc 0.2.74 (registry+https://github.com/rust-lang/crates.io-index)", - "redox_users 0.3.4 (registry+https://github.com/rust-lang/crates.io-index)", - "winapi 0.3.9 (registry+https://github.com/rust-lang/crates.io-index)", + "libc", + "redox_users", + "winapi", ] [[package]] name = "dirs-sys-next" -version = "0.1.0" +version = "0.1.1" source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "99de365f605554ae33f115102a02057d4fc18b01f3284d6870be0938743cfe7d" dependencies = [ - "libc 0.2.74 (registry+https://github.com/rust-lang/crates.io-index)", - "redox_users 0.3.4 (registry+https://github.com/rust-lang/crates.io-index)", - "winapi 0.3.9 (registry+https://github.com/rust-lang/crates.io-index)", + "libc", + "redox_users", + "winapi", ] [[package]] name = "encoding_rs" -version = "0.8.23" +version = "0.8.26" source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "801bbab217d7f79c0062f4f7205b5d4427c6d1a7bd7aafdd1475f7c59d62b283" dependencies = [ - "cfg-if 0.1.10 (registry+https://github.com/rust-lang/crates.io-index)", + "cfg-if 1.0.0", ] [[package]] name = "entities" version = "1.0.1" source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b5320ae4c3782150d900b79807611a59a99fc9a1d61d686faafc24b93fc8d7ca" [[package]] name = "escaper" version = "0.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "39da344028c2227132b2dfa7c186e2104ecc153467583d00ed9c398f9ff693b0" dependencies = [ - "entities 1.0.1 (registry+https://github.com/rust-lang/crates.io-index)", + "entities", ] [[package]] name = "fallible-iterator" version = "0.2.0" source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4443176a9f2c162692bd3d352d745ef9413eec5782a80d8fd6f8a1ac692a07f7" [[package]] name = "fallible-streaming-iterator" version = "0.1.9" source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7360491ce676a36bf9bb3c56c1aa791658183a54d2744120f27285738d90465a" [[package]] name = "fnv" version = "1.0.7" source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3f9eec918d3f24069decb9af1554cad7c880e2da24a9afd88aca000531ab82c1" [[package]] name = "foreign-types" version = "0.3.2" source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f6f339eb8adc052cd2ca78910fda869aefa38d22d5cb648e6485e4d3fc06f3b1" dependencies = [ - "foreign-types-shared 0.1.1 (registry+https://github.com/rust-lang/crates.io-index)", + "foreign-types-shared", ] [[package]] name = "foreign-types-shared" version = "0.1.1" source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "00b0228411908ca8685dba7fc2cdd70ec9990a6e753e89b6ac91a84c40fbaf4b" + +[[package]] +name = "form_urlencoded" +version = "1.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ece68d15c92e84fa4f19d3780f1294e5ca82a78a6d515f1efaabcc144688be00" +dependencies = [ + "matches", + "percent-encoding", +] [[package]] name = "getrandom" -version = "0.1.14" +version = "0.1.15" source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fc587bc0ec293155d5bfa6b9891ec18a1e330c234f896ea47fbada4cadbe47e6" dependencies = [ - "cfg-if 0.1.10 (registry+https://github.com/rust-lang/crates.io-index)", - "libc 0.2.74 (registry+https://github.com/rust-lang/crates.io-index)", - "wasi 0.9.0+wasi-snapshot-preview1 (registry+https://github.com/rust-lang/crates.io-index)", + "cfg-if 0.1.10", + "libc", + "wasi 0.9.0+wasi-snapshot-preview1", ] [[package]] name = "hermit-abi" -version = "0.1.15" +version = "0.1.17" source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5aca5565f760fb5b220e499d72710ed156fdb74e631659e99377d9ebfbd13ae8" dependencies = [ - "libc 0.2.74 (registry+https://github.com/rust-lang/crates.io-index)", + "libc", ] [[package]] name = "ident_case" version = "1.0.1" source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b9e0384b61958566e926dc50660321d12159025e767c18e043daf26b70104c39" [[package]] name = "idna" version = "0.2.0" source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "02e2673c30ee86b5b96a9cb52ad15718aa1f966f5ab9ad54a8b95d5ca33120a9" dependencies = [ - "matches 0.1.8 (registry+https://github.com/rust-lang/crates.io-index)", - "unicode-bidi 0.3.4 (registry+https://github.com/rust-lang/crates.io-index)", - "unicode-normalization 0.1.13 (registry+https://github.com/rust-lang/crates.io-index)", + "matches", + "unicode-bidi", + "unicode-normalization", ] [[package]] name = "jetscii" version = "0.4.4" source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5f25cca2463cb19dbb1061eb3bd38a8b5e4ce1cc5a5a9fc0e02de486d92b9b05" [[package]] name = "js-sys" -version = "0.3.44" +version = "0.3.45" source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ca059e81d9486668f12d455a4ea6daa600bd408134cd17e3d3fb5a32d1f016f8" dependencies = [ - "wasm-bindgen 0.2.67 (registry+https://github.com/rust-lang/crates.io-index)", + "wasm-bindgen", ] [[package]] name = "lazy_static" version = "1.4.0" source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e2abad23fbc42b3700f2f279844dc832adb2b2eb069b2df918f455c4e18cc646" [[package]] name = "libc" -version = "0.2.74" +version = "0.2.80" source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4d58d1b70b004888f764dfbf6a26a3b0342a1632d33968e4a179d8011c760614" [[package]] name = "libsqlite3-sys" version = "0.17.3" source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "56d90181c2904c287e5390186be820e5ef311a3c62edebb7d6ca3d6a48ce041d" dependencies = [ - "cc 1.0.58 (registry+https://github.com/rust-lang/crates.io-index)", - "pkg-config 0.3.18 (registry+https://github.com/rust-lang/crates.io-index)", - "vcpkg 0.2.10 (registry+https://github.com/rust-lang/crates.io-index)", + "cc", + "pkg-config", + "vcpkg", ] [[package]] name = "linked-hash-map" version = "0.5.3" source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8dd5a6d5999d9907cda8ed67bbd137d3af8085216c2ac62de5be860bd41f304a" [[package]] name = "log" version = "0.4.11" source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4fabed175da42fed1fa0746b0ea71f412aa9d35e76e95e59b192c64b9dc2bf8b" dependencies = [ - "cfg-if 0.1.10 (registry+https://github.com/rust-lang/crates.io-index)", + "cfg-if 0.1.10", ] [[package]] name = "lru-cache" version = "0.1.2" source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "31e24f1ad8321ca0e8a1e0ac13f23cb668e6f5466c2c57319f6a5cf1cc8e3b1c" dependencies = [ - "linked-hash-map 0.5.3 (registry+https://github.com/rust-lang/crates.io-index)", + "linked-hash-map", ] [[package]] name = "matches" version = "0.1.8" source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7ffc5c5338469d4d3ea17d269fa8ea3512ad247247c30bd2df69e68309ed0a08" [[package]] name = "memchr" -version = "2.3.3" +version = "2.3.4" source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0ee1c47aaa256ecabcaea351eae4a9b01ef39ed810004e298d2511ed284b1525" [[package]] name = "native-tls" -version = "0.2.4" +version = "0.2.6" source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6fcc7939b5edc4e4f86b1b4a04bb1498afaaf871b1a6691838ed06fcb48d3a3f" dependencies = [ - "lazy_static 1.4.0 (registry+https://github.com/rust-lang/crates.io-index)", - "libc 0.2.74 (registry+https://github.com/rust-lang/crates.io-index)", - "log 0.4.11 (registry+https://github.com/rust-lang/crates.io-index)", - "openssl 0.10.30 (registry+https://github.com/rust-lang/crates.io-index)", - "openssl-probe 0.1.2 (registry+https://github.com/rust-lang/crates.io-index)", - "openssl-sys 0.9.58 (registry+https://github.com/rust-lang/crates.io-index)", - "schannel 0.1.19 (registry+https://github.com/rust-lang/crates.io-index)", - "security-framework 0.4.4 (registry+https://github.com/rust-lang/crates.io-index)", - "security-framework-sys 0.4.3 (registry+https://github.com/rust-lang/crates.io-index)", - "tempfile 3.1.0 (registry+https://github.com/rust-lang/crates.io-index)", + "lazy_static", + "libc", + "log", + "openssl", + "openssl-probe", + "openssl-sys", + "schannel", + "security-framework", + "security-framework-sys", + "tempfile", ] [[package]] name = "ncurses" version = "5.99.0" source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "15699bee2f37e9f8828c7b35b2bc70d13846db453f2d507713b758fabe536b82" dependencies = [ - "cc 1.0.58 (registry+https://github.com/rust-lang/crates.io-index)", - "libc 0.2.74 (registry+https://github.com/rust-lang/crates.io-index)", - "pkg-config 0.3.18 (registry+https://github.com/rust-lang/crates.io-index)", + "cc", + "libc", + "pkg-config", ] [[package]] name = "num-integer" -version = "0.1.43" +version = "0.1.44" source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d2cc698a63b549a70bc047073d2949cce27cd1c7b0a4a862d08a8031bc2801db" dependencies = [ - "autocfg 1.0.0 (registry+https://github.com/rust-lang/crates.io-index)", - "num-traits 0.2.12 (registry+https://github.com/rust-lang/crates.io-index)", + "autocfg", + "num-traits", ] [[package]] name = "num-traits" -version = "0.2.12" +version = "0.2.14" source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9a64b1ec5cda2586e284722486d802acf1f7dbdc623e2bfc57e65ca1cd099290" dependencies = [ - "autocfg 1.0.0 (registry+https://github.com/rust-lang/crates.io-index)", + "autocfg", ] [[package]] name = "once_cell" -version = "1.4.0" +version = "1.5.2" source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "13bd41f508810a131401606d54ac32a467c97172d74ba7662562ebba5ad07fa0" [[package]] name = "openssl" version = "0.10.30" source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8d575eff3665419f9b83678ff2815858ad9d11567e082f5ac1814baba4e2bcb4" dependencies = [ - "bitflags 1.2.1 (registry+https://github.com/rust-lang/crates.io-index)", - "cfg-if 0.1.10 (registry+https://github.com/rust-lang/crates.io-index)", - "foreign-types 0.3.2 (registry+https://github.com/rust-lang/crates.io-index)", - "lazy_static 1.4.0 (registry+https://github.com/rust-lang/crates.io-index)", - "libc 0.2.74 (registry+https://github.com/rust-lang/crates.io-index)", - "openssl-sys 0.9.58 (registry+https://github.com/rust-lang/crates.io-index)", + "bitflags", + "cfg-if 0.1.10", + "foreign-types", + "lazy_static", + "libc", + "openssl-sys", ] [[package]] name = "openssl-probe" version = "0.1.2" source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "77af24da69f9d9341038eba93a073b1fdaaa1b788221b00a69bce9e762cb32de" [[package]] name = "openssl-sys" version = "0.9.58" source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a842db4709b604f0fe5d1170ae3565899be2ad3d9cbc72dedc789ac0511f78de" dependencies = [ - "autocfg 1.0.0 (registry+https://github.com/rust-lang/crates.io-index)", - "cc 1.0.58 (registry+https://github.com/rust-lang/crates.io-index)", - "libc 0.2.74 (registry+https://github.com/rust-lang/crates.io-index)", - "pkg-config 0.3.18 (registry+https://github.com/rust-lang/crates.io-index)", - "vcpkg 0.2.10 (registry+https://github.com/rust-lang/crates.io-index)", + "autocfg", + "cc", + "libc", + "pkg-config", + "vcpkg", ] [[package]] name = "opml" version = "0.2.4" source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5cd697395344133c42c91d1e50adb21db8339122cd6fa813a1c6dd8a0bf7a19b" dependencies = [ - "regex 1.3.9 (registry+https://github.com/rust-lang/crates.io-index)", - "serde 1.0.115 (registry+https://github.com/rust-lang/crates.io-index)", - "strong-xml 0.6.0 (registry+https://github.com/rust-lang/crates.io-index)", + "regex", + "serde", + "strong-xml", ] [[package]] name = "pancurses" version = "0.16.1" source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d3058bc37c433096b2ac7afef1c5cdfae49ede0a4ffec3dfc1df1df0959d0ff0" dependencies = [ - "libc 0.2.74 (registry+https://github.com/rust-lang/crates.io-index)", - "log 0.4.11 (registry+https://github.com/rust-lang/crates.io-index)", - "ncurses 5.99.0 (registry+https://github.com/rust-lang/crates.io-index)", - "pdcurses-sys 0.7.1 (registry+https://github.com/rust-lang/crates.io-index)", - "winreg 0.5.1 (registry+https://github.com/rust-lang/crates.io-index)", + "libc", + "log", + "ncurses", + "pdcurses-sys", + "winreg", ] [[package]] name = "pdcurses-sys" version = "0.7.1" source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "084dd22796ff60f1225d4eb6329f33afaf4c85419d51d440ab6b8c6f4529166b" dependencies = [ - "cc 1.0.58 (registry+https://github.com/rust-lang/crates.io-index)", - "libc 0.2.74 (registry+https://github.com/rust-lang/crates.io-index)", + "cc", + "libc", ] [[package]] name = "percent-encoding" version = "2.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d4fd5641d01c8f18a23da7b6fe29298ff4b55afcccdf78973b24cf3175fee32e" [[package]] name = "pkg-config" -version = "0.3.18" +version = "0.3.19" source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3831453b3449ceb48b6d9c7ad7c96d5ea673e9b470a1dc578c2ce6521230884c" [[package]] name = "ppv-lite86" -version = "0.2.8" +version = "0.2.10" source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ac74c624d6b2d21f425f752262f42188365d7b8ff1aff74c82e45136510a4857" [[package]] name = "proc-macro2" -version = "1.0.19" +version = "1.0.24" source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1e0704ee1a7e00d7bb417d0770ea303c1bccbabf0ef1667dae92b5967f5f8a71" dependencies = [ - "unicode-xid 0.2.1 (registry+https://github.com/rust-lang/crates.io-index)", + "unicode-xid", ] [[package]] name = "qstring" version = "0.7.2" source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d464fae65fff2680baf48019211ce37aaec0c78e9264c84a3e484717f965104e" dependencies = [ - "percent-encoding 2.1.0 (registry+https://github.com/rust-lang/crates.io-index)", + "percent-encoding", ] [[package]] name = "quick-xml" version = "0.17.2" source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fe1e430bdcf30c9fdc25053b9c459bb1a4672af4617b6c783d7d91dc17c6bbb0" dependencies = [ - "encoding_rs 0.8.23 (registry+https://github.com/rust-lang/crates.io-index)", - "memchr 2.3.3 (registry+https://github.com/rust-lang/crates.io-index)", + "encoding_rs", + "memchr", ] [[package]] name = "quote" version = "1.0.7" source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "aa563d17ecb180e500da1cfd2b028310ac758de548efdd203e18f283af693f37" dependencies = [ - "proc-macro2 1.0.19 (registry+https://github.com/rust-lang/crates.io-index)", + "proc-macro2", ] [[package]] name = "rand" version = "0.7.3" source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6a6b1679d49b24bbfe0c803429aa1874472f50d9b363131f0e89fc356b544d03" dependencies = [ - "getrandom 0.1.14 (registry+https://github.com/rust-lang/crates.io-index)", - "libc 0.2.74 (registry+https://github.com/rust-lang/crates.io-index)", - "rand_chacha 0.2.2 (registry+https://github.com/rust-lang/crates.io-index)", - "rand_core 0.5.1 (registry+https://github.com/rust-lang/crates.io-index)", - "rand_hc 0.2.0 (registry+https://github.com/rust-lang/crates.io-index)", + "getrandom", + "libc", + "rand_chacha", + "rand_core", + "rand_hc", ] [[package]] name = "rand_chacha" version = "0.2.2" source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f4c8ed856279c9737206bf725bf36935d8666ead7aa69b52be55af369d193402" dependencies = [ - "ppv-lite86 0.2.8 (registry+https://github.com/rust-lang/crates.io-index)", - "rand_core 0.5.1 (registry+https://github.com/rust-lang/crates.io-index)", + "ppv-lite86", + "rand_core", ] [[package]] name = "rand_core" version = "0.5.1" source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "90bde5296fc891b0cef12a6d03ddccc162ce7b2aff54160af9338f8d40df6d19" dependencies = [ - "getrandom 0.1.14 (registry+https://github.com/rust-lang/crates.io-index)", + "getrandom", ] [[package]] name = "rand_hc" version = "0.2.0" source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ca3129af7b92a17112d59ad498c6f81eaf463253766b90396d39ea7a39d6613c" dependencies = [ - "rand_core 0.5.1 (registry+https://github.com/rust-lang/crates.io-index)", + "rand_core", ] [[package]] name = "redox_syscall" version = "0.1.57" source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "41cc0f7e4d5d4544e8861606a285bb08d3e70712ccc7d2b84d7c0ccfaf4b05ce" [[package]] name = "redox_users" -version = "0.3.4" +version = "0.3.5" source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "de0737333e7a9502c789a36d7c7fa6092a49895d4faa31ca5df163857ded2e9d" dependencies = [ - "getrandom 0.1.14 (registry+https://github.com/rust-lang/crates.io-index)", - "redox_syscall 0.1.57 (registry+https://github.com/rust-lang/crates.io-index)", - "rust-argon2 0.7.0 (registry+https://github.com/rust-lang/crates.io-index)", + "getrandom", + "redox_syscall", + "rust-argon2", ] [[package]] name = "regex" -version = "1.3.9" +version = "1.4.2" source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "38cf2c13ed4745de91a5eb834e11c00bcc3709e773173b2ce4c56c9fbde04b9c" dependencies = [ - "aho-corasick 0.7.13 (registry+https://github.com/rust-lang/crates.io-index)", - "memchr 2.3.3 (registry+https://github.com/rust-lang/crates.io-index)", - "regex-syntax 0.6.18 (registry+https://github.com/rust-lang/crates.io-index)", - "thread_local 1.0.1 (registry+https://github.com/rust-lang/crates.io-index)", + "aho-corasick", + "memchr", + "regex-syntax", + "thread_local", ] [[package]] name = "regex-syntax" -version = "0.6.18" +version = "0.6.21" source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3b181ba2dcf07aaccad5448e8ead58db5b742cf85dfe035e2227f137a539a189" [[package]] name = "remove_dir_all" version = "0.5.3" source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3acd125665422973a33ac9d3dd2df85edad0f4ae9b00dafb1a05e43a9f5ef8e7" dependencies = [ - "winapi 0.3.9 (registry+https://github.com/rust-lang/crates.io-index)", + "winapi", ] [[package]] name = "ring" -version = "0.16.15" +version = "0.16.17" source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c5911690c9b773bab7e657471afc207f3827b249a657241327e3544d79bcabdd" dependencies = [ - "cc 1.0.58 (registry+https://github.com/rust-lang/crates.io-index)", - "libc 0.2.74 (registry+https://github.com/rust-lang/crates.io-index)", - "once_cell 1.4.0 (registry+https://github.com/rust-lang/crates.io-index)", - "spin 0.5.2 (registry+https://github.com/rust-lang/crates.io-index)", - "untrusted 0.7.1 (registry+https://github.com/rust-lang/crates.io-index)", - "web-sys 0.3.44 (registry+https://github.com/rust-lang/crates.io-index)", - "winapi 0.3.9 (registry+https://github.com/rust-lang/crates.io-index)", + "cc", + "libc", + "once_cell", + "spin", + "untrusted", + "web-sys", + "winapi", ] [[package]] name = "rss" version = "1.9.0" source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "99979205510c60f80a119dedbabd0b8426517384edf205322f8bcd51796bcef9" dependencies = [ - "derive_builder 0.9.0 (registry+https://github.com/rust-lang/crates.io-index)", - "quick-xml 0.17.2 (registry+https://github.com/rust-lang/crates.io-index)", + "derive_builder", + "quick-xml", ] [[package]] name = "rusqlite" version = "0.21.0" source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "64a656821bb6317a84b257737b7934f79c0dbb7eb694710475908280ebad3e64" dependencies = [ - "bitflags 1.2.1 (registry+https://github.com/rust-lang/crates.io-index)", - "fallible-iterator 0.2.0 (registry+https://github.com/rust-lang/crates.io-index)", - "fallible-streaming-iterator 0.1.9 (registry+https://github.com/rust-lang/crates.io-index)", - "libsqlite3-sys 0.17.3 (registry+https://github.com/rust-lang/crates.io-index)", - "lru-cache 0.1.2 (registry+https://github.com/rust-lang/crates.io-index)", - "memchr 2.3.3 (registry+https://github.com/rust-lang/crates.io-index)", - "time 0.1.43 (registry+https://github.com/rust-lang/crates.io-index)", + "bitflags", + "fallible-iterator", + "fallible-streaming-iterator", + "libsqlite3-sys", + "lru-cache", + "memchr", + "time", ] [[package]] name = "rust-argon2" -version = "0.7.0" +version = "0.8.2" source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9dab61250775933275e84053ac235621dfb739556d5c54a2f2e9313b7cf43a19" dependencies = [ - "base64 0.11.0 (registry+https://github.com/rust-lang/crates.io-index)", - "blake2b_simd 0.5.10 (registry+https://github.com/rust-lang/crates.io-index)", - "constant_time_eq 0.1.5 (registry+https://github.com/rust-lang/crates.io-index)", - "crossbeam-utils 0.7.2 (registry+https://github.com/rust-lang/crates.io-index)", + "base64 0.12.3", + "blake2b_simd", + "constant_time_eq", + "crossbeam-utils", ] [[package]] name = "rustls" -version = "0.18.0" +version = "0.18.1" source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5d1126dcf58e93cee7d098dbda643b5f92ed724f1f6a63007c1116eed6700c81" dependencies = [ - "base64 0.12.3 (registry+https://github.com/rust-lang/crates.io-index)", - "log 0.4.11 (registry+https://github.com/rust-lang/crates.io-index)", - "ring 0.16.15 (registry+https://github.com/rust-lang/crates.io-index)", - "sct 0.6.0 (registry+https://github.com/rust-lang/crates.io-index)", - "webpki 0.21.3 (registry+https://github.com/rust-lang/crates.io-index)", + "base64 0.12.3", + "log", + "ring", + "sct", + "webpki", ] [[package]] name = "sanitize-filename" version = "0.2.1" source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "23fd0fec94ec480abfd86bb8f4f6c57e0efb36dac5c852add176ea7b04c74801" dependencies = [ - "lazy_static 1.4.0 (registry+https://github.com/rust-lang/crates.io-index)", - "regex 1.3.9 (registry+https://github.com/rust-lang/crates.io-index)", + "lazy_static", + "regex", ] [[package]] name = "schannel" version = "0.1.19" source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8f05ba609c234e60bee0d547fe94a4c7e9da733d1c962cf6e59efa4cd9c8bc75" dependencies = [ - "lazy_static 1.4.0 (registry+https://github.com/rust-lang/crates.io-index)", - "winapi 0.3.9 (registry+https://github.com/rust-lang/crates.io-index)", + "lazy_static", + "winapi", ] [[package]] name = "sct" version = "0.6.0" source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e3042af939fca8c3453b7af0f1c66e533a15a86169e39de2657310ade8f98d3c" dependencies = [ - "ring 0.16.15 (registry+https://github.com/rust-lang/crates.io-index)", - "untrusted 0.7.1 (registry+https://github.com/rust-lang/crates.io-index)", + "ring", + "untrusted", ] [[package]] name = "security-framework" -version = "0.4.4" +version = "2.0.0" source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c1759c2e3c8580017a484a7ac56d3abc5a6c1feadf88db2f3633f12ae4268c69" dependencies = [ - "bitflags 1.2.1 (registry+https://github.com/rust-lang/crates.io-index)", - "core-foundation 0.7.0 (registry+https://github.com/rust-lang/crates.io-index)", - "core-foundation-sys 0.7.0 (registry+https://github.com/rust-lang/crates.io-index)", - "libc 0.2.74 (registry+https://github.com/rust-lang/crates.io-index)", - "security-framework-sys 0.4.3 (registry+https://github.com/rust-lang/crates.io-index)", + "bitflags", + "core-foundation", + "core-foundation-sys", + "libc", + "security-framework-sys", ] [[package]] name = "security-framework-sys" -version = "0.4.3" +version = "2.0.0" source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f99b9d5e26d2a71633cc4f2ebae7cc9f874044e0c351a27e17892d76dce5678b" dependencies = [ - "core-foundation-sys 0.7.0 (registry+https://github.com/rust-lang/crates.io-index)", - "libc 0.2.74 (registry+https://github.com/rust-lang/crates.io-index)", + "core-foundation-sys", + "libc", ] [[package]] name = "semver" version = "0.10.0" source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "394cec28fa623e00903caf7ba4fa6fb9a0e260280bb8cdbbba029611108a0190" dependencies = [ - "semver-parser 0.7.0 (registry+https://github.com/rust-lang/crates.io-index)", + "semver-parser", ] [[package]] name = "semver-parser" version = "0.7.0" source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "388a1df253eca08550bef6c72392cfe7c30914bf41df5269b68cbd6ff8f570a3" [[package]] name = "serde" -version = "1.0.115" +version = "1.0.117" source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b88fa983de7720629c9387e9f517353ed404164b1e482c970a90c1a4aaf7dc1a" dependencies = [ - "serde_derive 1.0.115 (registry+https://github.com/rust-lang/crates.io-index)", + "serde_derive", ] [[package]] name = "serde_derive" -version = "1.0.115" +version = "1.0.117" source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cbd1ae72adb44aab48f325a02444a5fc079349a8d804c1fc922aed3f7454c74e" dependencies = [ - "proc-macro2 1.0.19 (registry+https://github.com/rust-lang/crates.io-index)", - "quote 1.0.7 (registry+https://github.com/rust-lang/crates.io-index)", - "syn 1.0.38 (registry+https://github.com/rust-lang/crates.io-index)", + "proc-macro2", + "quote", + "syn", ] [[package]] name = "shellcaster" version = "1.0.1" dependencies = [ - "chrono 0.4.15 (registry+https://github.com/rust-lang/crates.io-index)", - "clap 2.33.3 (registry+https://github.com/rust-lang/crates.io-index)", - "dirs-next 1.0.1 (registry+https://github.com/rust-lang/crates.io-index)", - "escaper 0.1.0 (registry+https://github.com/rust-lang/crates.io-index)", - "lazy_static 1.4.0 (registry+https://github.com/rust-lang/crates.io-index)", - "opml 0.2.4 (registry+https://github.com/rust-lang/crates.io-index)", - "pancurses 0.16.1 (registry+https://github.com/rust-lang/crates.io-index)", - "regex 1.3.9 (registry+https://github.com/rust-lang/crates.io-index)", - "rss 1.9.0 (registry+https://github.com/rust-lang/crates.io-index)", - "rusqlite 0.21.0 (registry+https://github.com/rust-lang/crates.io-index)", - "sanitize-filename 0.2.1 (registry+https://github.com/rust-lang/crates.io-index)", - "semver 0.10.0 (registry+https://github.com/rust-lang/crates.io-index)", - "serde 1.0.115 (registry+https://github.com/rust-lang/crates.io-index)", - "shellexpand 2.0.0 (registry+https://github.com/rust-lang/crates.io-index)", - "textwrap 0.12.1 (registry+https://github.com/rust-lang/crates.io-index)", - "toml 0.5.6 (registry+https://github.com/rust-lang/crates.io-index)", - "unicode-segmentation 1.6.0 (registry+https://github.com/rust-lang/crates.io-index)", - "ureq 1.4.0 (registry+https://github.com/rust-lang/crates.io-index)", + "chrono", + "clap", + "dirs-next", + "escaper", + "lazy_static", + "opml", + "pancurses", + "regex", + "rss", + "rusqlite", + "sanitize-filename", + "semver", + "serde", + "shellexpand", + "textwrap 0.12.1", + "toml", + "unicode-segmentation", + "ureq", ] [[package]] name = "shellexpand" version = "2.0.0" source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9a2b22262a9aaf9464d356f656fea420634f78c881c5eebd5ef5e66d8b9bc603" dependencies = [ - "dirs 2.0.2 (registry+https://github.com/rust-lang/crates.io-index)", + "dirs", ] [[package]] name = "spin" version = "0.5.2" source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6e63cff320ae2c57904679ba7cb63280a3dc4613885beafb148ee7bf9aa9042d" [[package]] name = "strong-xml" version = "0.6.0" source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ee06e7e5baf4508dea83506a83fcc5b80a404d4c0e9c473c9a4b38b802af3a07" dependencies = [ - "jetscii 0.4.4 (registry+https://github.com/rust-lang/crates.io-index)", - "lazy_static 1.4.0 (registry+https://github.com/rust-lang/crates.io-index)", - "memchr 2.3.3 (registry+https://github.com/rust-lang/crates.io-index)", - "strong-xml-derive 0.6.0 (registry+https://github.com/rust-lang/crates.io-index)", - "xmlparser 0.13.2 (registry+https://github.com/rust-lang/crates.io-index)", + "jetscii", + "lazy_static", + "memchr", + "strong-xml-derive", + "xmlparser", ] [[package]] name = "strong-xml-derive" version = "0.6.0" source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d2e4e25fb64e61f55d495134d9e5ac68b1fa4bb2855b5a5b53857b9460e2bfde" dependencies = [ - "proc-macro2 1.0.19 (registry+https://github.com/rust-lang/crates.io-index)", - "quote 1.0.7 (registry+https://github.com/rust-lang/crates.io-index)", - "syn 1.0.38 (registry+https://github.com/rust-lang/crates.io-index)", + "proc-macro2", + "quote", + "syn", ] [[package]] name = "strsim" version = "0.8.0" source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8ea5119cdb4c55b55d432abb513a0429384878c15dde60cc77b1c99de1a95a6a" [[package]] name = "strsim" version = "0.9.3" source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6446ced80d6c486436db5c078dde11a9f73d42b57fb273121e160b84f63d894c" [[package]] name = "syn" -version = "1.0.38" +version = "1.0.50" source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "443b4178719c5a851e1bde36ce12da21d74a0e60b4d982ec3385a933c812f0f6" dependencies = [ - "proc-macro2 1.0.19 (registry+https://github.com/rust-lang/crates.io-index)", - "quote 1.0.7 (registry+https://github.com/rust-lang/crates.io-index)", - "unicode-xid 0.2.1 (registry+https://github.com/rust-lang/crates.io-index)", + "proc-macro2", + "quote", + "unicode-xid", ] [[package]] name = "tempfile" version = "3.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7a6e24d9338a0a5be79593e2fa15a648add6138caa803e2d5bc782c371732ca9" dependencies = [ - "cfg-if 0.1.10 (registry+https://github.com/rust-lang/crates.io-index)", - "libc 0.2.74 (registry+https://github.com/rust-lang/crates.io-index)", - "rand 0.7.3 (registry+https://github.com/rust-lang/crates.io-index)", - "redox_syscall 0.1.57 (registry+https://github.com/rust-lang/crates.io-index)", - "remove_dir_all 0.5.3 (registry+https://github.com/rust-lang/crates.io-index)", - "winapi 0.3.9 (registry+https://github.com/rust-lang/crates.io-index)", + "cfg-if 0.1.10", + "libc", + "rand", + "redox_syscall", + "remove_dir_all", + "winapi", ] [[package]] name = "textwrap" version = "0.11.0" source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d326610f408c7a4eb6f51c37c330e496b08506c9457c9d34287ecc38809fb060" dependencies = [ - "unicode-width 0.1.8 (registry+https://github.com/rust-lang/crates.io-index)", + "unicode-width", ] [[package]] name = "textwrap" version = "0.12.1" source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "203008d98caf094106cfaba70acfed15e18ed3ddb7d94e49baec153a2b462789" dependencies = [ - "unicode-width 0.1.8 (registry+https://github.com/rust-lang/crates.io-index)", + "unicode-width", ] [[package]] name = "thread_local" version = "1.0.1" source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d40c6d1b69745a6ec6fb1ca717914848da4b44ae29d9b3080cbee91d72a69b14" dependencies = [ - "lazy_static 1.4.0 (registry+https://github.com/rust-lang/crates.io-index)", + "lazy_static", ] [[package]] name = "time" -version = "0.1.43" +version = "0.1.44" source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6db9e6914ab8b1ae1c260a4ae7a49b6c5611b40328a735b21862567685e73255" dependencies = [ - "libc 0.2.74 (registry+https://github.com/rust-lang/crates.io-index)", - "winapi 0.3.9 (registry+https://github.com/rust-lang/crates.io-index)", + "libc", + "wasi 0.10.0+wasi-snapshot-preview1", + "winapi", ] [[package]] name = "tinyvec" -version = "0.3.3" +version = "1.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ccf8dbc19eb42fba10e8feaaec282fb50e2c14b2726d6301dbfeed0f73306a6f" +dependencies = [ + "tinyvec_macros", +] + +[[package]] +name = "tinyvec_macros" +version = "0.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cda74da7e1a664f795bb1f8a87ec406fb89a02522cf6e50620d016add6dbbf5c" [[package]] name = "toml" -version = "0.5.6" +version = "0.5.7" source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "75cf45bb0bef80604d001caaec0d09da99611b3c0fd39d3080468875cdb65645" dependencies = [ - "serde 1.0.115 (registry+https://github.com/rust-lang/crates.io-index)", + "serde", ] [[package]] name = "unicode-bidi" version = "0.3.4" source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "49f2bd0c6468a8230e1db229cff8029217cf623c767ea5d60bfbd42729ea54d5" dependencies = [ - "matches 0.1.8 (registry+https://github.com/rust-lang/crates.io-index)", + "matches", ] [[package]] name = "unicode-normalization" -version = "0.1.13" +version = "0.1.16" source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a13e63ab62dbe32aeee58d1c5408d35c36c392bba5d9d3142287219721afe606" dependencies = [ - "tinyvec 0.3.3 (registry+https://github.com/rust-lang/crates.io-index)", + "tinyvec", ] [[package]] name = "unicode-segmentation" -version = "1.6.0" +version = "1.7.0" source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "db8716a166f290ff49dabc18b44aa407cb7c6dbe1aa0971b44b8a24b0ca35aae" [[package]] name = "unicode-width" version = "0.1.8" source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9337591893a19b88d8d87f2cec1e73fad5cdfd10e5a6f349f498ad6ea2ffb1e3" [[package]] name = "unicode-xid" version = "0.2.1" source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f7fe0bb3479651439c9112f72b6c505038574c9fbb575ed1bf3b797fa39dd564" [[package]] name = "untrusted" version = "0.7.1" source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a156c684c91ea7d62626509bce3cb4e1d9ed5c4d978f7b4352658f96a4c26b4a" [[package]] name = "ureq" -version = "1.4.0" +version = "1.5.2" source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a599426c7388ab189dfd0eeb84c8d879490abc73e3e62a0b6a40e286f6427ab7" dependencies = [ - "base64 0.12.3 (registry+https://github.com/rust-lang/crates.io-index)", - "chunked_transfer 1.2.0 (registry+https://github.com/rust-lang/crates.io-index)", - "lazy_static 1.4.0 (registry+https://github.com/rust-lang/crates.io-index)", - "native-tls 0.2.4 (registry+https://github.com/rust-lang/crates.io-index)", - "qstring 0.7.2 (registry+https://github.com/rust-lang/crates.io-index)", - "rustls 0.18.0 (registry+https://github.com/rust-lang/crates.io-index)", - "url 2.1.1 (registry+https://github.com/rust-lang/crates.io-index)", - "webpki 0.21.3 (registry+https://github.com/rust-lang/crates.io-index)", - "webpki-roots 0.20.0 (registry+https://github.com/rust-lang/crates.io-index)", + "base64 0.13.0", + "chunked_transfer", + "log", + "native-tls", + "once_cell", + "qstring", + "rustls", + "url", + "webpki", + "webpki-roots", ] [[package]] name = "url" -version = "2.1.1" +version = "2.2.0" source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5909f2b0817350449ed73e8bcd81c8c3c8d9a7a5d8acba4b27db277f1868976e" dependencies = [ - "idna 0.2.0 (registry+https://github.com/rust-lang/crates.io-index)", - "matches 0.1.8 (registry+https://github.com/rust-lang/crates.io-index)", - "percent-encoding 2.1.0 (registry+https://github.com/rust-lang/crates.io-index)", + "form_urlencoded", + "idna", + "matches", + "percent-encoding", ] [[package]] name = "vcpkg" version = "0.2.10" source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6454029bf181f092ad1b853286f23e2c507d8e8194d01d92da4a55c274a5508c" [[package]] name = "vec_map" version = "0.8.2" source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f1bddf1187be692e79c5ffeab891132dfb0f236ed36a43c7ed39f1165ee20191" [[package]] name = "wasi" version = "0.9.0+wasi-snapshot-preview1" source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cccddf32554fecc6acb585f82a32a72e28b48f8c4c1883ddfeeeaa96f7d8e519" + +[[package]] +name = "wasi" +version = "0.10.0+wasi-snapshot-preview1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1a143597ca7c7793eff794def352d41792a93c481eb1042423ff7ff72ba2c31f" [[package]] name = "wasm-bindgen" -version = "0.2.67" +version = "0.2.68" source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1ac64ead5ea5f05873d7c12b545865ca2b8d28adfc50a49b84770a3a97265d42" dependencies = [ - "cfg-if 0.1.10 (registry+https://github.com/rust-lang/crates.io-index)", - "wasm-bindgen-macro 0.2.67 (registry+https://github.com/rust-lang/crates.io-index)", + "cfg-if 0.1.10", + "wasm-bindgen-macro", ] [[package]] name = "wasm-bindgen-backend" -version = "0.2.67" +version = "0.2.68" source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f22b422e2a757c35a73774860af8e112bff612ce6cb604224e8e47641a9e4f68" dependencies = [ - "bumpalo 3.4.0 (registry+https://github.com/rust-lang/crates.io-index)", - "lazy_static 1.4.0 (registry+https://github.com/rust-lang/crates.io-index)", - "log 0.4.11 (registry+https://github.com/rust-lang/crates.io-index)", - "proc-macro2 1.0.19 (registry+https://github.com/rust-lang/crates.io-index)", - "quote 1.0.7 (registry+https://github.com/rust-lang/crates.io-index)", - "syn 1.0.38 (registry+https://github.com/rust-lang/crates.io-index)", - "wasm-bindgen-shared 0.2.67 (registry+https://github.com/rust-lang/crates.io-index)", + "bumpalo", + "lazy_static", + "log", + "proc-macro2", + "quote", + "syn", + "wasm-bindgen-shared", ] [[package]] name = "wasm-bindgen-macro" -version = "0.2.67" +version = "0.2.68" source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6b13312a745c08c469f0b292dd2fcd6411dba5f7160f593da6ef69b64e407038" dependencies = [ - "quote 1.0.7 (registry+https://github.com/rust-lang/crates.io-index)", - "wasm-bindgen-macro-support 0.2.67 (registry+https://github.com/rust-lang/crates.io-index)", + "quote", + "wasm-bindgen-macro-support", ] [[package]] name = "wasm-bindgen-macro-support" -version = "0.2.67" +version = "0.2.68" source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f249f06ef7ee334cc3b8ff031bfc11ec99d00f34d86da7498396dc1e3b1498fe" dependencies = [ - "proc-macro2 1.0.19 (registry+https://github.com/rust-lang/crates.io-index)", - "quote 1.0.7 (registry+https://github.com/rust-lang/crates.io-index)", - "syn 1.0.38 (registry+https://github.com/rust-lang/crates.io-index)", - "wasm-bindgen-backend 0.2.67 (registry+https://github.com/rust-lang/crates.io-index)", - "wasm-bindgen-shared 0.2.67 (registry+https://github.com/rust-lang/crates.io-index)", + "proc-macro2", + "quote", + "syn", + "wasm-bindgen-backend", + "wasm-bindgen-shared", ] [[package]] name = "wasm-bindgen-shared" -version = "0.2.67" +version = "0.2.68" source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1d649a3145108d7d3fbcde896a468d1bd636791823c9921135218ad89be08307" [[package]] name = "web-sys" -version = "0.3.44" +version = "0.3.45" source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4bf6ef87ad7ae8008e15a355ce696bed26012b7caa21605188cfd8214ab51e2d" dependencies = [ - "js-sys 0.3.44 (registry+https://github.com/rust-lang/crates.io-index)", - "wasm-bindgen 0.2.67 (registry+https://github.com/rust-lang/crates.io-index)", + "js-sys", + "wasm-bindgen", ] [[package]] name = "webpki" version = "0.21.3" source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ab146130f5f790d45f82aeeb09e55a256573373ec64409fc19a6fb82fb1032ae" dependencies = [ - "ring 0.16.15 (registry+https://github.com/rust-lang/crates.io-index)", - "untrusted 0.7.1 (registry+https://github.com/rust-lang/crates.io-index)", + "ring", + "untrusted", ] [[package]] name = "webpki-roots" version = "0.20.0" source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0f20dea7535251981a9670857150d571846545088359b28e4951d350bdaf179f" dependencies = [ - "webpki 0.21.3 (registry+https://github.com/rust-lang/crates.io-index)", + "webpki", ] [[package]] name = "winapi" version = "0.3.9" source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5c839a674fcd7a98952e593242ea400abe93992746761e38641405d28b00f419" dependencies = [ - "winapi-i686-pc-windows-gnu 0.4.0 (registry+https://github.com/rust-lang/crates.io-index)", - "winapi-x86_64-pc-windows-gnu 0.4.0 (registry+https://github.com/rust-lang/crates.io-index)", + "winapi-i686-pc-windows-gnu", + "winapi-x86_64-pc-windows-gnu", ] [[package]] name = "winapi-i686-pc-windows-gnu" version = "0.4.0" source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ac3b87c63620426dd9b991e5ce0329eff545bccbbb34f3be09ff6fb6ab51b7b6" [[package]] name = "winapi-x86_64-pc-windows-gnu" version = "0.4.0" source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "712e227841d057c1ee1cd2fb22fa7e5a5461ae8e48fa2ca79ec42cfc1931183f" [[package]] name = "winreg" version = "0.5.1" source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a27a759395c1195c4cc5cda607ef6f8f6498f64e78f7900f5de0a127a424704a" dependencies = [ - "winapi 0.3.9 (registry+https://github.com/rust-lang/crates.io-index)", + "winapi", ] [[package]] name = "xmlparser" -version = "0.13.2" -source = "registry+https://github.com/rust-lang/crates.io-index" - -[metadata] -"checksum aho-corasick 0.7.13 (registry+https://github.com/rust-lang/crates.io-index)" = "043164d8ba5c4c3035fec9bbee8647c0261d788f3474306f93bb65901cae0e86" -"checksum ansi_term 0.11.0 (registry+https://github.com/rust-lang/crates.io-index)" = "ee49baf6cb617b853aa8d93bf420db2383fab46d314482ca2803b40d5fde979b" -"checksum arrayref 0.3.6 (registry+https://github.com/rust-lang/crates.io-index)" = "a4c527152e37cf757a3f78aae5a06fbeefdb07ccc535c980a3208ee3060dd544" -"checksum arrayvec 0.5.1 (registry+https://github.com/rust-lang/crates.io-index)" = "cff77d8686867eceff3105329d4698d96c2391c176d5d03adc90c7389162b5b8" -"checksum atty 0.2.14 (registry+https://github.com/rust-lang/crates.io-index)" = "d9b39be18770d11421cdb1b9947a45dd3f37e93092cbf377614828a319d5fee8" -"checksum autocfg 1.0.0 (registry+https://github.com/rust-lang/crates.io-index)" = "f8aac770f1885fd7e387acedd76065302551364496e46b3dd00860b2f8359b9d" -"checksum base64 0.11.0 (registry+https://github.com/rust-lang/crates.io-index)" = "b41b7ea54a0c9d92199de89e20e58d49f02f8e699814ef3fdf266f6f748d15c7" -"checksum base64 0.12.3 (registry+https://github.com/rust-lang/crates.io-index)" = "3441f0f7b02788e948e47f457ca01f1d7e6d92c693bc132c22b087d3141c03ff" -"checksum bitflags 1.2.1 (registry+https://github.com/rust-lang/crates.io-index)" = "cf1de2fe8c75bc145a2f577add951f8134889b4795d47466a54a5c846d691693" -"checksum blake2b_simd 0.5.10 (registry+https://github.com/rust-lang/crates.io-index)" = "d8fb2d74254a3a0b5cac33ac9f8ed0e44aa50378d9dbb2e5d83bd21ed1dc2c8a" -"checksum bumpalo 3.4.0 (registry+https://github.com/rust-lang/crates.io-index)" = "2e8c087f005730276d1096a652e92a8bacee2e2472bcc9715a74d2bec38b5820" -"checksum cc 1.0.58 (registry+https://github.com/rust-lang/crates.io-index)" = "f9a06fb2e53271d7c279ec1efea6ab691c35a2ae67ec0d91d7acec0caf13b518" -"checksum cfg-if 0.1.10 (registry+https://github.com/rust-lang/crates.io-index)" = "4785bdd1c96b2a846b2bd7cc02e86b6b3dbf14e7e53446c4f54c92a361040822" -"checksum chrono 0.4.15 (registry+https://github.com/rust-lang/crates.io-index)" = "942f72db697d8767c22d46a598e01f2d3b475501ea43d0db4f16d90259182d0b" -"checksum chunked_transfer 1.2.0 (registry+https://github.com/rust-lang/crates.io-index)" = "1d29eb15132782371f71da8f947dba48b3717bdb6fa771b9b434d645e40a7193" -"checksum clap 2.33.3 (registry+https://github.com/rust-lang/crates.io-index)" = "37e58ac78573c40708d45522f0d80fa2f01cc4f9b4e2bf749807255454312002" -"checksum constant_time_eq 0.1.5 (registry+https://github.com/rust-lang/crates.io-index)" = "245097e9a4535ee1e3e3931fcfcd55a796a44c643e8596ff6566d68f09b87bbc" -"checksum core-foundation 0.7.0 (registry+https://github.com/rust-lang/crates.io-index)" = "57d24c7a13c43e870e37c1556b74555437870a04514f7685f5b354e090567171" -"checksum core-foundation-sys 0.7.0 (registry+https://github.com/rust-lang/crates.io-index)" = "b3a71ab494c0b5b860bdc8407ae08978052417070c2ced38573a9157ad75b8ac" -"checksum crossbeam-utils 0.7.2 (registry+https://github.com/rust-lang/crates.io-index)" = "c3c7c73a2d1e9fc0886a08b93e98eb643461230d5f1925e4036204d5f2e261a8" -"checksum darling 0.10.2 (registry+https://github.com/rust-lang/crates.io-index)" = "0d706e75d87e35569db781a9b5e2416cff1236a47ed380831f959382ccd5f858" -"checksum darling_core 0.10.2 (registry+https://github.com/rust-lang/crates.io-index)" = "f0c960ae2da4de88a91b2d920c2a7233b400bc33cb28453a2987822d8392519b" -"checksum darling_macro 0.10.2 (registry+https://github.com/rust-lang/crates.io-index)" = "d9b5a2f4ac4969822c62224815d069952656cadc7084fdca9751e6d959189b72" -"checksum derive_builder 0.9.0 (registry+https://github.com/rust-lang/crates.io-index)" = "a2658621297f2cf68762a6f7dc0bb7e1ff2cfd6583daef8ee0fed6f7ec468ec0" -"checksum derive_builder_core 0.9.0 (registry+https://github.com/rust-lang/crates.io-index)" = "2791ea3e372c8495c0bc2033991d76b512cd799d07491fbd6890124db9458bef" -"checksum dirs 2.0.2 (registry+https://github.com/rust-lang/crates.io-index)" = "13aea89a5c93364a98e9b37b2fa237effbb694d5cfe01c5b70941f7eb087d5e3" -"checksum dirs-next 1.0.1 (registry+https://github.com/rust-lang/crates.io-index)" = "1cbcf9241d9e8d106295bd496bbe2e9cffd5fa098f2a8c9e2bbcbf09773c11a8" -"checksum dirs-sys 0.3.5 (registry+https://github.com/rust-lang/crates.io-index)" = "8e93d7f5705de3e49895a2b5e0b8855a1c27f080192ae9c32a6432d50741a57a" -"checksum dirs-sys-next 0.1.0 (registry+https://github.com/rust-lang/crates.io-index)" = "9c60f7b8a8953926148223260454befb50c751d3c50e1c178c4fd1ace4083c9a" -"checksum encoding_rs 0.8.23 (registry+https://github.com/rust-lang/crates.io-index)" = "e8ac63f94732332f44fe654443c46f6375d1939684c17b0afb6cb56b0456e171" -"checksum entities 1.0.1 (registry+https://github.com/rust-lang/crates.io-index)" = "b5320ae4c3782150d900b79807611a59a99fc9a1d61d686faafc24b93fc8d7ca" -"checksum escaper 0.1.0 (registry+https://github.com/rust-lang/crates.io-index)" = "39da344028c2227132b2dfa7c186e2104ecc153467583d00ed9c398f9ff693b0" -"checksum fallible-iterator 0.2.0 (registry+https://github.com/rust-lang/crates.io-index)" = "4443176a9f2c162692bd3d352d745ef9413eec5782a80d8fd6f8a1ac692a07f7" -"checksum fallible-streaming-iterator 0.1.9 (registry+https://github.com/rust-lang/crates.io-index)" = "7360491ce676a36bf9bb3c56c1aa791658183a54d2744120f27285738d90465a" -"checksum fnv 1.0.7 (registry+https://github.com/rust-lang/crates.io-index)" = "3f9eec918d3f24069decb9af1554cad7c880e2da24a9afd88aca000531ab82c1" -"checksum foreign-types 0.3.2 (registry+https://github.com/rust-lang/crates.io-index)" = "f6f339eb8adc052cd2ca78910fda869aefa38d22d5cb648e6485e4d3fc06f3b1" -"checksum foreign-types-shared 0.1.1 (registry+https://github.com/rust-lang/crates.io-index)" = "00b0228411908ca8685dba7fc2cdd70ec9990a6e753e89b6ac91a84c40fbaf4b" -"checksum getrandom 0.1.14 (registry+https://github.com/rust-lang/crates.io-index)" = "7abc8dd8451921606d809ba32e95b6111925cd2906060d2dcc29c070220503eb" -"checksum hermit-abi 0.1.15 (registry+https://github.com/rust-lang/crates.io-index)" = "3deed196b6e7f9e44a2ae8d94225d80302d81208b1bb673fd21fe634645c85a9" -"checksum ident_case 1.0.1 (registry+https://github.com/rust-lang/crates.io-index)" = "b9e0384b61958566e926dc50660321d12159025e767c18e043daf26b70104c39" -"checksum idna 0.2.0 (registry+https://github.com/rust-lang/crates.io-index)" = "02e2673c30ee86b5b96a9cb52ad15718aa1f966f5ab9ad54a8b95d5ca33120a9" -"checksum jetscii 0.4.4 (registry+https://github.com/rust-lang/crates.io-index)" = "5f25cca2463cb19dbb1061eb3bd38a8b5e4ce1cc5a5a9fc0e02de486d92b9b05" -"checksum js-sys 0.3.44 (registry+https://github.com/rust-lang/crates.io-index)" = "85a7e2c92a4804dd459b86c339278d0fe87cf93757fae222c3fa3ae75458bc73" -"checksum lazy_static 1.4.0 (registry+https://github.com/rust-lang/crates.io-index)" = "e2abad23fbc42b3700f2f279844dc832adb2b2eb069b2df918f455c4e18cc646" -"checksum libc 0.2.74 (registry+https://github.com/rust-lang/crates.io-index)" = "a2f02823cf78b754822df5f7f268fb59822e7296276d3e069d8e8cb26a14bd10" -"checksum libsqlite3-sys 0.17.3 (registry+https://github.com/rust-lang/crates.io-index)" = "56d90181c2904c287e5390186be820e5ef311a3c62edebb7d6ca3d6a48ce041d" -"checksum linked-hash-map 0.5.3 (registry+https://github.com/rust-lang/crates.io-index)" = "8dd5a6d5999d9907cda8ed67bbd137d3af8085216c2ac62de5be860bd41f304a" -"checksum log 0.4.11 (registry+https://github.com/rust-lang/crates.io-index)" = "4fabed175da42fed1fa0746b0ea71f412aa9d35e76e95e59b192c64b9dc2bf8b" -"checksum lru-cache 0.1.2 (registry+https://github.com/rust-lang/crates.io-index)" = "31e24f1ad8321ca0e8a1e0ac13f23cb668e6f5466c2c57319f6a5cf1cc8e3b1c" -"checksum matches 0.1.8 (registry+https://github.com/rust-lang/crates.io-index)" = "7ffc5c5338469d4d3ea17d269fa8ea3512ad247247c30bd2df69e68309ed0a08" -"checksum memchr 2.3.3 (registry+https://github.com/rust-lang/crates.io-index)" = "3728d817d99e5ac407411fa471ff9800a778d88a24685968b36824eaf4bee400" -"checksum native-tls 0.2.4 (registry+https://github.com/rust-lang/crates.io-index)" = "2b0d88c06fe90d5ee94048ba40409ef1d9315d86f6f38c2efdaad4fb50c58b2d" -"checksum ncurses 5.99.0 (registry+https://github.com/rust-lang/crates.io-index)" = "15699bee2f37e9f8828c7b35b2bc70d13846db453f2d507713b758fabe536b82" -"checksum num-integer 0.1.43 (registry+https://github.com/rust-lang/crates.io-index)" = "8d59457e662d541ba17869cf51cf177c0b5f0cbf476c66bdc90bf1edac4f875b" -"checksum num-traits 0.2.12 (registry+https://github.com/rust-lang/crates.io-index)" = "ac267bcc07f48ee5f8935ab0d24f316fb722d7a1292e2913f0cc196b29ffd611" -"checksum once_cell 1.4.0 (registry+https://github.com/rust-lang/crates.io-index)" = "0b631f7e854af39a1739f401cf34a8a013dfe09eac4fa4dba91e9768bd28168d" -"checksum openssl 0.10.30 (registry+https://github.com/rust-lang/crates.io-index)" = "8d575eff3665419f9b83678ff2815858ad9d11567e082f5ac1814baba4e2bcb4" -"checksum openssl-probe 0.1.2 (registry+https://github.com/rust-lang/crates.io-index)" = "77af24da69f9d9341038eba93a073b1fdaaa1b788221b00a69bce9e762cb32de" -"checksum openssl-sys 0.9.58 (registry+https://github.com/rust-lang/crates.io-index)" = "a842db4709b604f0fe5d1170ae3565899be2ad3d9cbc72dedc789ac0511f78de" -"checksum opml 0.2.4 (registry+https://github.com/rust-lang/crates.io-index)" = "5cd697395344133c42c91d1e50adb21db8339122cd6fa813a1c6dd8a0bf7a19b" -"checksum pancurses 0.16.1 (registry+https://github.com/rust-lang/crates.io-index)" = "d3058bc37c433096b2ac7afef1c5cdfae49ede0a4ffec3dfc1df1df0959d0ff0" -"checksum pdcurses-sys 0.7.1 (registry+https://github.com/rust-lang/crates.io-index)" = "084dd22796ff60f1225d4eb6329f33afaf4c85419d51d440ab6b8c6f4529166b" -"checksum percent-encoding 2.1.0 (registry+https://github.com/rust-lang/crates.io-index)" = "d4fd5641d01c8f18a23da7b6fe29298ff4b55afcccdf78973b24cf3175fee32e" -"checksum pkg-config 0.3.18 (registry+https://github.com/rust-lang/crates.io-index)" = "d36492546b6af1463394d46f0c834346f31548646f6ba10849802c9c9a27ac33" -"checksum ppv-lite86 0.2.8 (registry+https://github.com/rust-lang/crates.io-index)" = "237a5ed80e274dbc66f86bd59c1e25edc039660be53194b5fe0a482e0f2612ea" -"checksum proc-macro2 1.0.19 (registry+https://github.com/rust-lang/crates.io-index)" = "04f5f085b5d71e2188cb8271e5da0161ad52c3f227a661a3c135fdf28e258b12" -"checksum qstring 0.7.2 (registry+https://github.com/rust-lang/crates.io-index)" = "d464fae65fff2680baf48019211ce37aaec0c78e9264c84a3e484717f965104e" -"checksum quick-xml 0.17.2 (registry+https://github.com/rust-lang/crates.io-index)" = "fe1e430bdcf30c9fdc25053b9c459bb1a4672af4617b6c783d7d91dc17c6bbb0" -"checksum quote 1.0.7 (registry+https://github.com/rust-lang/crates.io-index)" = "aa563d17ecb180e500da1cfd2b028310ac758de548efdd203e18f283af693f37" -"checksum rand 0.7.3 (registry+https://github.com/rust-lang/crates.io-index)" = "6a6b1679d49b24bbfe0c803429aa1874472f50d9b363131f0e89fc356b544d03" -"checksum rand_chacha 0.2.2 (registry+https://github.com/rust-lang/crates.io-index)" = "f4c8ed856279c9737206bf725bf36935d8666ead7aa69b52be55af369d193402" -"checksum rand_core 0.5.1 (registry+https://github.com/rust-lang/crates.io-index)" = "90bde5296fc891b0cef12a6d03ddccc162ce7b2aff54160af9338f8d40df6d19" -"checksum rand_hc 0.2.0 (registry+https://github.com/rust-lang/crates.io-index)" = "ca3129af7b92a17112d59ad498c6f81eaf463253766b90396d39ea7a39d6613c" -"checksum redox_syscall 0.1.57 (registry+https://github.com/rust-lang/crates.io-index)" = "41cc0f7e4d5d4544e8861606a285bb08d3e70712ccc7d2b84d7c0ccfaf4b05ce" -"checksum redox_users 0.3.4 (registry+https://github.com/rust-lang/crates.io-index)" = "09b23093265f8d200fa7b4c2c76297f47e681c655f6f1285a8780d6a022f7431" -"checksum regex 1.3.9 (registry+https://github.com/rust-lang/crates.io-index)" = "9c3780fcf44b193bc4d09f36d2a3c87b251da4a046c87795a0d35f4f927ad8e6" -"checksum regex-syntax 0.6.18 (registry+https://github.com/rust-lang/crates.io-index)" = "26412eb97c6b088a6997e05f69403a802a92d520de2f8e63c2b65f9e0f47c4e8" -"checksum remove_dir_all 0.5.3 (registry+https://github.com/rust-lang/crates.io-index)" = "3acd125665422973a33ac9d3dd2df85edad0f4ae9b00dafb1a05e43a9f5ef8e7" -"checksum ring 0.16.15 (registry+https://github.com/rust-lang/crates.io-index)" = "952cd6b98c85bbc30efa1ba5783b8abf12fec8b3287ffa52605b9432313e34e4" -"checksum rss 1.9.0 (registry+https://github.com/rust-lang/crates.io-index)" = "99979205510c60f80a119dedbabd0b8426517384edf205322f8bcd51796bcef9" -"checksum rusqlite 0.21.0 (registry+https://github.com/rust-lang/crates.io-index)" = "64a656821bb6317a84b257737b7934f79c0dbb7eb694710475908280ebad3e64" -"checksum rust-argon2 0.7.0 (registry+https://github.com/rust-lang/crates.io-index)" = "2bc8af4bda8e1ff4932523b94d3dd20ee30a87232323eda55903ffd71d2fb017" -"checksum rustls 0.18.0 (registry+https://github.com/rust-lang/crates.io-index)" = "cac94b333ee2aac3284c5b8a1b7fb4dd11cba88c244e3fe33cdbd047af0eb693" -"checksum sanitize-filename 0.2.1 (registry+https://github.com/rust-lang/crates.io-index)" = "23fd0fec94ec480abfd86bb8f4f6c57e0efb36dac5c852add176ea7b04c74801" -"checksum schannel 0.1.19 (registry+https://github.com/rust-lang/crates.io-index)" = "8f05ba609c234e60bee0d547fe94a4c7e9da733d1c962cf6e59efa4cd9c8bc75" -"checksum sct 0.6.0 (registry+https://github.com/rust-lang/crates.io-index)" = "e3042af939fca8c3453b7af0f1c66e533a15a86169e39de2657310ade8f98d3c" -"checksum security-framework 0.4.4 (registry+https://github.com/rust-lang/crates.io-index)" = "64808902d7d99f78eaddd2b4e2509713babc3dc3c85ad6f4c447680f3c01e535" -"checksum security-framework-sys 0.4.3 (registry+https://github.com/rust-lang/crates.io-index)" = "17bf11d99252f512695eb468de5516e5cf75455521e69dfe343f3b74e4748405" -"checksum semver 0.10.0 (registry+https://github.com/rust-lang/crates.io-index)" = "394cec28fa623e00903caf7ba4fa6fb9a0e260280bb8cdbbba029611108a0190" -"checksum semver-parser 0.7.0 (registry+https://github.com/rust-lang/crates.io-index)" = "388a1df253eca08550bef6c72392cfe7c30914bf41df5269b68cbd6ff8f570a3" -"checksum serde 1.0.115 (registry+https://github.com/rust-lang/crates.io-index)" = "e54c9a88f2da7238af84b5101443f0c0d0a3bbdc455e34a5c9497b1903ed55d5" -"checksum serde_derive 1.0.115 (registry+https://github.com/rust-lang/crates.io-index)" = "609feed1d0a73cc36a0182a840a9b37b4a82f0b1150369f0536a9e3f2a31dc48" -"checksum shellexpand 2.0.0 (registry+https://github.com/rust-lang/crates.io-index)" = "9a2b22262a9aaf9464d356f656fea420634f78c881c5eebd5ef5e66d8b9bc603" -"checksum spin 0.5.2 (registry+https://github.com/rust-lang/crates.io-index)" = "6e63cff320ae2c57904679ba7cb63280a3dc4613885beafb148ee7bf9aa9042d" -"checksum strong-xml 0.6.0 (registry+https://github.com/rust-lang/crates.io-index)" = "ee06e7e5baf4508dea83506a83fcc5b80a404d4c0e9c473c9a4b38b802af3a07" -"checksum strong-xml-derive 0.6.0 (registry+https://github.com/rust-lang/crates.io-index)" = "d2e4e25fb64e61f55d495134d9e5ac68b1fa4bb2855b5a5b53857b9460e2bfde" -"checksum strsim 0.8.0 (registry+https://github.com/rust-lang/crates.io-index)" = "8ea5119cdb4c55b55d432abb513a0429384878c15dde60cc77b1c99de1a95a6a" -"checksum strsim 0.9.3 (registry+https://github.com/rust-lang/crates.io-index)" = "6446ced80d6c486436db5c078dde11a9f73d42b57fb273121e160b84f63d894c" -"checksum syn 1.0.38 (registry+https://github.com/rust-lang/crates.io-index)" = "e69abc24912995b3038597a7a593be5053eb0fb44f3cc5beec0deb421790c1f4" -"checksum tempfile 3.1.0 (registry+https://github.com/rust-lang/crates.io-index)" = "7a6e24d9338a0a5be79593e2fa15a648add6138caa803e2d5bc782c371732ca9" -"checksum textwrap 0.11.0 (registry+https://github.com/rust-lang/crates.io-index)" = "d326610f408c7a4eb6f51c37c330e496b08506c9457c9d34287ecc38809fb060" -"checksum textwrap 0.12.1 (registry+https://github.com/rust-lang/crates.io-index)" = "203008d98caf094106cfaba70acfed15e18ed3ddb7d94e49baec153a2b462789" -"checksum thread_local 1.0.1 (registry+https://github.com/rust-lang/crates.io-index)" = "d40c6d1b69745a6ec6fb1ca717914848da4b44ae29d9b3080cbee91d72a69b14" -"checksum time 0.1.43 (registry+https://github.com/rust-lang/crates.io-index)" = "ca8a50ef2360fbd1eeb0ecd46795a87a19024eb4b53c5dc916ca1fd95fe62438" -"checksum tinyvec 0.3.3 (registry+https://github.com/rust-lang/crates.io-index)" = "53953d2d3a5ad81d9f844a32f14ebb121f50b650cd59d0ee2a07cf13c617efed" -"checksum toml 0.5.6 (registry+https://github.com/rust-lang/crates.io-index)" = "ffc92d160b1eef40665be3a05630d003936a3bc7da7421277846c2613e92c71a" -"checksum unicode-bidi 0.3.4 (registry+https://github.com/rust-lang/crates.io-index)" = "49f2bd0c6468a8230e1db229cff8029217cf623c767ea5d60bfbd42729ea54d5" -"checksum unicode-normalization 0.1.13 (registry+https://github.com/rust-lang/crates.io-index)" = "6fb19cf769fa8c6a80a162df694621ebeb4dafb606470b2b2fce0be40a98a977" -"checksum unicode-segmentation 1.6.0 (registry+https://github.com/rust-lang/crates.io-index)" = "e83e153d1053cbb5a118eeff7fd5be06ed99153f00dbcd8ae310c5fb2b22edc0" -"checksum unicode-width 0.1.8 (registry+https://github.com/rust-lang/crates.io-index)" = "9337591893a19b88d8d87f2cec1e73fad5cdfd10e5a6f349f498ad6ea2ffb1e3" -"checksum unicode-xid 0.2.1 (registry+https://github.com/rust-lang/crates.io-index)" = "f7fe0bb3479651439c9112f72b6c505038574c9fbb575ed1bf3b797fa39dd564" -"checksum untrusted 0.7.1 (registry+https://github.com/rust-lang/crates.io-index)" = "a156c684c91ea7d62626509bce3cb4e1d9ed5c4d978f7b4352658f96a4c26b4a" -"checksum ureq 1.4.0 (registry+https://github.com/rust-lang/crates.io-index)" = "b770aa61edaa144d3af86a8b0ccbb1bf8ca9dd0c1ac2a17081f35943aae6eb82" -"checksum url 2.1.1 (registry+https://github.com/rust-lang/crates.io-index)" = "829d4a8476c35c9bf0bbce5a3b23f4106f79728039b726d292bb93bc106787cb" -"checksum vcpkg 0.2.10 (registry+https://github.com/rust-lang/crates.io-index)" = "6454029bf181f092ad1b853286f23e2c507d8e8194d01d92da4a55c274a5508c" -"checksum vec_map 0.8.2 (registry+https://github.com/rust-lang/crates.io-index)" = "f1bddf1187be692e79c5ffeab891132dfb0f236ed36a43c7ed39f1165ee20191" -"checksum wasi 0.9.0+wasi-snapshot-preview1 (registry+https://github.com/rust-lang/crates.io-index)" = "cccddf32554fecc6acb585f82a32a72e28b48f8c4c1883ddfeeeaa96f7d8e519" -"checksum wasm-bindgen 0.2.67 (registry+https://github.com/rust-lang/crates.io-index)" = "f0563a9a4b071746dd5aedbc3a28c6fe9be4586fb3fbadb67c400d4f53c6b16c" -"checksum wasm-bindgen-backend 0.2.67 (registry+https://github.com/rust-lang/crates.io-index)" = "bc71e4c5efa60fb9e74160e89b93353bc24059999c0ae0fb03affc39770310b0" -"checksum wasm-bindgen-macro 0.2.67 (registry+https://github.com/rust-lang/crates.io-index)" = "97c57cefa5fa80e2ba15641578b44d36e7a64279bc5ed43c6dbaf329457a2ed2" -"checksum wasm-bindgen-macro-support 0.2.67 (registry+https://github.com/rust-lang/crates.io-index)" = "841a6d1c35c6f596ccea1f82504a192a60378f64b3bb0261904ad8f2f5657556" -"checksum wasm-bindgen-shared 0.2.67 (registry+https://github.com/rust-lang/crates.io-index)" = "93b162580e34310e5931c4b792560108b10fd14d64915d7fff8ff00180e70092" -"checksum web-sys 0.3.44 (registry+https://github.com/rust-lang/crates.io-index)" = "dda38f4e5ca63eda02c059d243aa25b5f35ab98451e518c51612cd0f1bd19a47" -"checksum webpki 0.21.3 (registry+https://github.com/rust-lang/crates.io-index)" = "ab146130f5f790d45f82aeeb09e55a256573373ec64409fc19a6fb82fb1032ae" -"checksum webpki-roots 0.20.0 (registry+https://github.com/rust-lang/crates.io-index)" = "0f20dea7535251981a9670857150d571846545088359b28e4951d350bdaf179f" -"checksum winapi 0.3.9 (registry+https://github.com/rust-lang/crates.io-index)" = "5c839a674fcd7a98952e593242ea400abe93992746761e38641405d28b00f419" -"checksum winapi-i686-pc-windows-gnu 0.4.0 (registry+https://github.com/rust-lang/crates.io-index)" = "ac3b87c63620426dd9b991e5ce0329eff545bccbbb34f3be09ff6fb6ab51b7b6" -"checksum winapi-x86_64-pc-windows-gnu 0.4.0 (registry+https://github.com/rust-lang/crates.io-index)" = "712e227841d057c1ee1cd2fb22fa7e5a5461ae8e48fa2ca79ec42cfc1931183f" -"checksum winreg 0.5.1 (registry+https://github.com/rust-lang/crates.io-index)" = "a27a759395c1195c4cc5cda607ef6f8f6498f64e78f7900f5de0a127a424704a" -"checksum xmlparser 0.13.2 (registry+https://github.com/rust-lang/crates.io-index)" = "52613e655f6f11f63c0fe7d1c3b5ef69e44d96df9b65dab296b441ed0e1125f5" +version = "0.13.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "114ba2b24d2167ef6d67d7d04c8cc86522b87f490025f39f0303b7db5bf5e3d8" From 4aad0fcd1ae49f5d72cd4f2d2cddfbfe8f311a25 Mon Sep 17 00:00:00 2001 From: Jeff Hughes Date: Mon, 23 Nov 2020 19:10:43 -0500 Subject: [PATCH 22/23] Fix bug with cursor location introduced by setting window background --- src/ui/notification.rs | 4 ++-- src/ui/panel.rs | 4 ++-- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/src/ui/notification.rs b/src/ui/notification.rs index 5c43bac..a256da4 100644 --- a/src/ui/notification.rs +++ b/src/ui/notification.rs @@ -97,7 +97,7 @@ impl NotifWin { // otherwise, there was a notification before but there // isn't now, so erase self.window.erase(); - self.window.bkgd(pancurses::ColorPair( + self.window.bkgdset(pancurses::ColorPair( self.colors.get(ColorType::Normal) as u8 )); self.window.refresh(); @@ -251,7 +251,7 @@ impl NotifWin { ); oldwin.delwin(); - self.window.bkgd(pancurses::ColorPair( + self.window.bkgdset(pancurses::ColorPair( self.colors.get(ColorType::Normal) as u8 )); if let Some(curr) = &self.current_msg { diff --git a/src/ui/panel.rs b/src/ui/panel.rs index 0f185b8..179f55d 100644 --- a/src/ui/panel.rs +++ b/src/ui/panel.rs @@ -60,7 +60,7 @@ impl Panel { /// Redraws borders and refreshes the window to display on terminal. pub fn refresh(&self) { - self.window.bkgd(pancurses::ColorPair( + self.window.bkgdset(pancurses::ColorPair( self.colors.get(ColorType::Normal) as u8 )); self.draw_border(); @@ -99,7 +99,7 @@ impl Panel { /// not refresh the screen. pub fn erase(&self) { self.window.erase(); - self.window.bkgd(pancurses::ColorPair( + self.window.bkgdset(pancurses::ColorPair( self.colors.get(ColorType::Normal) as u8 )); self.draw_border(); From 2796402fffe2b54eae9a8593b04988c247d21b9c Mon Sep 17 00:00:00 2001 From: Jeff Hughes Date: Tue, 1 Dec 2020 18:19:42 -0500 Subject: [PATCH 23/23] Bump to v1.1.0 and update readme --- CHANGELOG.md | 19 +++++++++++++++++++ Cargo.lock | 2 +- Cargo.toml | 2 +- README.md | 11 +++++++++++ 4 files changed, 32 insertions(+), 2 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 826c7cb..b4cd3c3 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,23 @@ # Changelog +## v1.1.0 (2020-12-01) +- Help menu showing the current keybindings (accessible by pressing + "?" by default) +- New options for downloading new episodes: + - Can select whether to always download new episodes when syncing + podcasts, to never download, or to pop up with a window allowing + you to select which new episodes to download +- Will now ask for confirmation before removing podcasts/episodes + (thanks to contributor [dougli1sqrd](https://github.com/dougli1sqrd)) +- Bug fixes: + - Border gets redrawn properly when scrolling (thanks to contributor [a-kenji](https://github.com/a-kenji)) + - Messages at the bottom of the screen properly reset cursor + position, so they always show up on the far left +- Other notes: + - Added consistent code formatting style with rustfmt (thanks to + contributor [thunderbiscuit](https://github.com/thunderbiscuit)) + + ## v1.0.1 (2020-08-18) - This is a patch release to fix some minor bugs - Bug fixes: @@ -7,6 +25,7 @@ - Fix decoding of HTML entities in episode descriptions to avoid getting cut off in certain cases - Properly import OPML v1.0 files - Correctly segment titles with Unicode letters + - Also some fixes to documentation (thanks to contributor [dwvisser](https://github.com/dwvisser)) ## v1.0.0 (2020-08-13) - Adjusted the criteria for checking existing episodes when syncing, which results in a dramatic speedup in the syncing process diff --git a/Cargo.lock b/Cargo.lock index 29d4947..ba19742 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -863,7 +863,7 @@ dependencies = [ [[package]] name = "shellcaster" -version = "1.0.1" +version = "1.1.0" dependencies = [ "chrono", "clap", diff --git a/Cargo.toml b/Cargo.toml index f920705..c55bf62 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "shellcaster" -version = "1.0.1" +version = "1.1.0" authors = ["Jeff Hughes "] edition = "2018" license = "GPL-3.0-or-later" diff --git a/README.md b/README.md index 9a358e0..843c1c1 100644 --- a/README.md +++ b/README.md @@ -155,6 +155,14 @@ The sample file above provides comments that should walk you through all the ava * Command used to play episodes. Use "%s" to indicate where file/URL will be entered to the command. Note that shellcaster does *not* include a native media player -- it simply passes the file path/URL to the given command with no further checking as to its success or failure. This process is started *in the background*, so be sure to send it to a program that has GUI controls of some kind so you have control over the playback. * Default: "vlc %s" +**download_new_episodes**: +* Configures what happens when new episodes are found as podcasts are synced. Valid options: + * "always" will automatically download all new episodes; + * "ask-selected" will open a popup window to let you select which episodes to download, with all of them selected by default; + * "ask-unselected" will open a popup window to let you select with episodes to download, with none of them selected by default; + * "never" will never automatically download new episodes. +* Default: "ask-unselected" + **simultaneous_downloads**: * Maximum number of files to download simultaneously. Setting this too high could result in network requests being denied. A good general guide would be to set this to the number of processor cores on your computer. * Default: 3 @@ -167,6 +175,7 @@ The sample file above provides comments that should walk you through all the ava | Key | Action | | ------- | -------------- | +| ? | Open help window | | Arrow keys / h,j,k,l | Navigate menus | | a | Add new feed | | q | Quit program | @@ -192,6 +201,8 @@ Some users may wish to sync their podcasts automatically on a regular basis, e.g Contributions from others are welcome! If you wish to contribute, feel free to clone the repo and submit pull requests. **Please ensure you are on the `develop` branch when making your edits**, as this is where the continued development of the app is taking place. Pull requests will only be merged to the `develop` branch, so you can help to avoid merge conflicts by doing your work on that branch in the first place. +Thanks to these fine folks who have made contributions: [a-kenji](https://github.com/a-kenji), [dougli1sqrd](https://github.com/dougli1sqrd), [dwvisser](https://github.com/dwvisser), [thunderbiscuit](https://github.com/thunderbiscuit) + ## Why "shellcaster"? I was trying to come up with a play on the word "podcast", and I liked the use of the word "shell" for several reasons. "Shell" is a synonym for the word "pod". The terminal is also referred to as a shell (and shellcaster is a terminal-based program). In addition, the program is built on Rust, whose mascot is Ferris the crab. Finally, I just personally enjoy that "shellcaster" sounds a lot like "spellcaster", so you can feel like a wizard when you use the program... \ No newline at end of file