From ff74498d55ff35fa9a5ae607e96a089be9c05aac Mon Sep 17 00:00:00 2001 From: Alex Kenji Date: Wed, 23 Dec 2020 00:03:44 +0100 Subject: [PATCH 01/23] Add BigScroll Options Namely: BigUp, BigDown, PageUp, PageDown, GoTop, GoBot - BigUp, BigDown depend on the size of the window, and on a scaling constant BIG_SCOLL_AMOUNT. - PageUp, PageDown depend n the size of the window. - GoTop, GoBot use the maximum of the variables. --- config.toml | 8 ++- src/config.rs | 22 ++++++++ src/keymap.rs | 7 +++ src/types.rs | 16 ++++++ src/ui/menu.rs | 83 ++++++++++++++++++------------ src/ui/mod.rs | 134 +++++++++++++++++++++++++++++-------------------- 6 files changed, 182 insertions(+), 88 deletions(-) diff --git a/config.toml b/config.toml index 82b1c37..2260633 100644 --- a/config.toml +++ b/config.toml @@ -69,6 +69,12 @@ left = [ "Left", "h" ] right = [ "Right", "l" ] up = [ "Up", "k" ] down = [ "Down", "j" ] +big_up = [ "K" ] +big_down = [ "L" ] +page_up = [ "PgUp" ] +page_down = [ "PgDn" ] +go_top = [ "g" ] +go_bot = [ "G" ] add_feed = [ "a" ] sync = [ "s" ] @@ -86,4 +92,4 @@ remove = [ "r" ] remove_all = [ "R" ] help = [ "?" ] -quit = [ "q" ] \ No newline at end of file +quit = [ "q" ] diff --git a/src/config.rs b/src/config.rs index ff49265..0efb625 100644 --- a/src/config.rs +++ b/src/config.rs @@ -25,6 +25,10 @@ pub const EPISODE_PUBDATE_LENGTH: usize = 60; // display the details panel pub const DETAILS_PANEL_LENGTH: i32 = 135; +// How many lines will be scrolled by the big scroll, +// in relation to the rows eg: 4 = 1/4 of the screen +pub const BIG_SCROLL_AMOUNT: i32 = 4; + /// Identifies the user's selection for what to do with new episodes /// when syncing. @@ -67,6 +71,12 @@ struct KeybindingsFromToml { right: Option>, up: Option>, down: Option>, + big_up: Option>, + big_down: Option>, + go_top: Option>, + go_bot: Option>, + page_up: Option>, + page_down: Option>, add_feed: Option>, sync: Option>, sync_all: Option>, @@ -107,6 +117,12 @@ impl Config { right: None, up: None, down: None, + big_up: None, + big_down: None, + go_top: None, + go_bot: None, + page_up: None, + page_down: None, add_feed: None, sync: None, sync_all: None, @@ -149,6 +165,12 @@ fn config_with_defaults(config_toml: &ConfigFromToml) -> Config { (&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.big_up, UserAction::BigUp, vec!["K".to_string()]), + (&config_toml.keybindings.big_down, UserAction::BigDown, vec!["J".to_string()]), + (&config_toml.keybindings.page_up, UserAction::PageUp, vec!["PgUp".to_string()]), + (&config_toml.keybindings.page_down, UserAction::PageDown, vec!["PgDn".to_string()]), + (&config_toml.keybindings.go_top, UserAction::GoTop, vec!["g".to_string()]), + (&config_toml.keybindings.go_bot, UserAction::GoBot, vec!["G".to_string()]), (&config_toml.keybindings.add_feed, UserAction::AddFeed, vec!["a".to_string()]), (&config_toml.keybindings.sync, UserAction::Sync, vec!["s".to_string()]), diff --git a/src/keymap.rs b/src/keymap.rs index e4aa4b6..9b643ed 100644 --- a/src/keymap.rs +++ b/src/keymap.rs @@ -10,6 +10,13 @@ pub enum UserAction { Up, Down, + BigUp, + BigDown, + PageUp, + PageDown, + GoTop, + GoBot, + AddFeed, Sync, SyncAll, diff --git a/src/types.rs b/src/types.rs index adff788..d571a4f 100644 --- a/src/types.rs +++ b/src/types.rs @@ -361,6 +361,22 @@ impl LockVec { }; } + /// Maps a closure to every element in the LockVec, in the same way + /// as the `filter_map()` does on an Iterator, both mapping and + /// filtering, over a specified range. + /// Does not check if the range is valid! + /// However, to avoid issues with keeping the borrow + /// alive, the function returns a Vec of the collected results, + /// rather than an iterator. + pub fn map_by_range(&self, start: usize, end: usize, mut f: F) -> Vec + where F: FnMut(&T) -> Option { + let (map, order) = self.borrow(); + return (start..end) + .into_iter() + .filter_map(|id| f(map.get(order.get(id).unwrap()).unwrap())) + .collect(); + } + /// Maps a closure to every element in the LockVec, in the same way /// as the `filter_map()` does on an Iterator, both mapping and /// filtering. However, to avoid issues with keeping the borrow diff --git a/src/ui/menu.rs b/src/ui/menu.rs index e7a409c..bddd9cd 100644 --- a/src/ui/menu.rs +++ b/src/ui/menu.rs @@ -1,4 +1,5 @@ use std::cmp::min; +use std::cmp::max; use std::collections::hash_map::Entry; use super::ColorType; @@ -121,8 +122,9 @@ impl Menu { /// represent the new visible list. pub fn scroll(&mut self, lines: i32) { let mut old_selected; - let old_played; - let new_played; + let checked_lines; + let apply_color_played; + let get_titles; let list_len = self.items.len(); if list_len == 0 { @@ -130,11 +132,18 @@ impl Menu { } let n_row = self.panel.get_rows(); + let max_lines = list_len as i32 + self.start_row; + let check_max = | lines | min(lines, max_lines); + + // check the bounds of lines and adjust accordingly + if lines.checked_add(self.top_row + n_row).is_some() { + checked_lines = lines; + } else { + checked_lines = lines - self.top_row - n_row; + } - // TODO: currently only handles scroll value of 1; need to extend - // to be able to scroll multiple lines at a time old_selected = self.selected; - self.selected += lines; + self.selected += checked_lines; // don't allow scrolling past last item in list (if shorter // than self.panel.get_rows()) @@ -143,50 +152,58 @@ impl Menu { self.selected = abs_bottom; } + // given a selection, apply correct play status and highlight + apply_color_played = | menu: &mut Menu, selected, color : ColorType | { + let played = menu.items + .map_single_by_index(menu.get_menu_idx(selected), |el| el.is_played()) + .unwrap(); + menu.set_attrs(selected, played, color); + }; + + // return a vec with sorted titles in range start, end (exclusive) + get_titles = | menu: &mut Menu, start, end | + menu + .items + .map_by_range(start, end, |el| { + Some(el.get_title(menu.panel.get_cols() as usize))}); + // scroll list if necessary: // 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 (self.selected) > (n_row - 1) { + // for scrolls that don't start at the bottom + apply_color_played(self, old_selected, ColorType::Normal); + let delta = n_row - old_selected - 1; + + let titles = get_titles( + self, + (self.top_row + n_row) as usize, + (check_max(checked_lines + self.top_row + n_row - delta)) as usize); + for title in titles.into_iter() { self.top_row += 1; self.panel.delete_line(self.start_row); old_selected -= 1; - self.panel.delete_line(n_row - 1); self.panel.write_line(n_row - 1, title); + apply_color_played(self, n_row - 1, ColorType::Normal); } + self.selected = n_row - 1; // scroll up } 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| { - el.get_title(self.panel.get_cols() as usize) - }) - { + let titles = get_titles( + self, + max(0,self.top_row + self.selected) as usize, + (self.top_row) as usize); + for title in titles.into_iter().rev() { self.top_row -= 1; self.panel.insert_line(self.start_row, title); + apply_color_played(self, 1, ColorType::Normal); old_selected += 1; } + self.selected = self.start_row; } - - old_played = self - .items - .map_single_by_index(self.get_menu_idx(old_selected), |el| el.is_played()) - .unwrap(); - new_played = self - .items - .map_single_by_index(self.get_menu_idx(self.selected), |el| el.is_played()) - .unwrap(); - - self.set_attrs(old_selected, old_played, ColorType::Normal); - self.set_attrs(self.selected, new_played, ColorType::HighlightedActive); + apply_color_played(self, old_selected, ColorType::Normal); + apply_color_played(self, self.selected, ColorType::HighlightedActive); self.panel.refresh(); } diff --git a/src/ui/mod.rs b/src/ui/mod.rs index a5a4e1b..751b375 100644 --- a/src/ui/mod.rs +++ b/src/ui/mod.rs @@ -97,8 +97,7 @@ 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(); @@ -281,7 +280,13 @@ impl<'a> UI<'a> { Some(a @ UserAction::Down) | Some(a @ UserAction::Up) | Some(a @ UserAction::Left) - | Some(a @ UserAction::Right) => { + | Some(a @ UserAction::Right) + | Some(a @ UserAction::PageUp) + | Some(a @ UserAction::PageDown) + | Some(a @ UserAction::BigUp) + | Some(a @ UserAction::BigDown) + | Some(a @ UserAction::GoTop) + | Some(a @ UserAction::GoBot) => { self.move_cursor(a, curr_pod_id, curr_ep_id) } @@ -444,55 +449,14 @@ impl<'a> UI<'a> { 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(); - } - } - } + self.scroll_current_window(curr_pod_id, 1); } 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(); - } - } - } + self.scroll_current_window(curr_pod_id, - 1); } UserAction::Left => { @@ -527,20 +491,84 @@ impl<'a> UI<'a> { } } + UserAction::PageUp => { + self.scroll_current_window( + curr_pod_id, + - self.n_row + 3, + ); + } + + UserAction::PageDown => { + self.scroll_current_window( + curr_pod_id, + self.n_row - 3, + ); + } + + UserAction::BigUp => { + self.scroll_current_window( + curr_pod_id, + -self.n_row / crate::config::BIG_SCROLL_AMOUNT, + ); + } + + UserAction::BigDown => { + self.scroll_current_window( + curr_pod_id, + self.n_row / crate::config::BIG_SCROLL_AMOUNT, + ); + } + + UserAction::GoTop => { + self.scroll_current_window(curr_pod_id, -i32::MAX); + } + + UserAction::GoBot => { + self.scroll_current_window(curr_pod_id, i32::MAX); + } + // this shouldn't occur because we only trigger this - // function when the UserAction is Up, Down, Left, or Right. + // function when the UserAction is Up, Down, Left, Right, BigUp, BigDown, + // PageUp, PageDown, GoBot and GoTop _ => (), } } + /// Scrolls the current active menu by + /// the specified amount and refreshes + /// the window. + /// Positive Scroll is down. + pub fn scroll_current_window(&mut self, pod_id: Option, scroll: i32) { + match self.active_menu { + ActiveMenu::PodcastMenu => { + if pod_id.is_some() { + self.podcast_menu.scroll(scroll); + + 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 pod_id.is_some() { + self.episode_menu.scroll(scroll); + self.update_details_panel(); + } + } + } + } + /// 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 - { + ) -> Option { if let Some(pod_id) = curr_pod_id { if let Some(ep_id) = curr_ep_id { if let Some(played) = self @@ -602,8 +630,7 @@ impl<'a> UI<'a> { &mut self, curr_pod_id: Option, curr_ep_id: Option, - ) -> 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 { @@ -808,8 +835,7 @@ impl<'a> UI<'a> { n_col: i32, start_y: i32, start_x: i32, - ) -> Panel - { + ) -> Panel { return Panel::new( colors, "Details".to_string(), From ddd79112b84d75f26675c987e7d33364fd2acf4f Mon Sep 17 00:00:00 2001 From: Jeff Hughes Date: Wed, 10 Feb 2021 19:28:04 -0500 Subject: [PATCH 02/23] Adjust formatting with rustfmt --- src/ui/menu.rs | 27 +++++++++++++++------------ src/ui/mod.rs | 12 +++--------- 2 files changed, 18 insertions(+), 21 deletions(-) diff --git a/src/ui/menu.rs b/src/ui/menu.rs index bddd9cd..adff299 100644 --- a/src/ui/menu.rs +++ b/src/ui/menu.rs @@ -1,5 +1,5 @@ -use std::cmp::min; use std::cmp::max; +use std::cmp::min; use std::collections::hash_map::Entry; use super::ColorType; @@ -133,7 +133,7 @@ impl Menu { let n_row = self.panel.get_rows(); let max_lines = list_len as i32 + self.start_row; - let check_max = | lines | min(lines, max_lines); + let check_max = |lines| min(lines, max_lines); // check the bounds of lines and adjust accordingly if lines.checked_add(self.top_row + n_row).is_some() { @@ -153,19 +153,20 @@ impl Menu { } // given a selection, apply correct play status and highlight - apply_color_played = | menu: &mut Menu, selected, color : ColorType | { - let played = menu.items + apply_color_played = |menu: &mut Menu, selected, color: ColorType| { + let played = menu + .items .map_single_by_index(menu.get_menu_idx(selected), |el| el.is_played()) .unwrap(); menu.set_attrs(selected, played, color); }; // return a vec with sorted titles in range start, end (exclusive) - get_titles = | menu: &mut Menu, start, end | - menu - .items - .map_by_range(start, end, |el| { - Some(el.get_title(menu.panel.get_cols() as usize))}); + get_titles = |menu: &mut Menu, start, end| { + menu.items.map_by_range(start, end, |el| { + Some(el.get_title(menu.panel.get_cols() as usize)) + }) + }; // scroll list if necessary: // scroll down @@ -177,7 +178,8 @@ impl Menu { let titles = get_titles( self, (self.top_row + n_row) as usize, - (check_max(checked_lines + self.top_row + n_row - delta)) as usize); + (check_max(checked_lines + self.top_row + n_row - delta)) as usize, + ); for title in titles.into_iter() { self.top_row += 1; self.panel.delete_line(self.start_row); @@ -192,8 +194,9 @@ impl Menu { } else if self.selected < self.start_row { let titles = get_titles( self, - max(0,self.top_row + self.selected) as usize, - (self.top_row) as usize); + max(0, self.top_row + self.selected) as usize, + (self.top_row) as usize, + ); for title in titles.into_iter().rev() { self.top_row -= 1; self.panel.insert_line(self.start_row, title); diff --git a/src/ui/mod.rs b/src/ui/mod.rs index 751b375..2413001 100644 --- a/src/ui/mod.rs +++ b/src/ui/mod.rs @@ -456,7 +456,7 @@ impl<'a> UI<'a> { } UserAction::Up => { - self.scroll_current_window(curr_pod_id, - 1); + self.scroll_current_window(curr_pod_id, -1); } UserAction::Left => { @@ -492,17 +492,11 @@ impl<'a> UI<'a> { } UserAction::PageUp => { - self.scroll_current_window( - curr_pod_id, - - self.n_row + 3, - ); + self.scroll_current_window(curr_pod_id, -self.n_row + 3); } UserAction::PageDown => { - self.scroll_current_window( - curr_pod_id, - self.n_row - 3, - ); + self.scroll_current_window(curr_pod_id, self.n_row - 3); } UserAction::BigUp => { From e5a145ad01a276f86a1106f8ecfa936d1a373540 Mon Sep 17 00:00:00 2001 From: Jeff Hughes Date: Wed, 10 Feb 2021 19:28:36 -0500 Subject: [PATCH 03/23] Change `big_down` keybinding to be consistent with `down` --- config.toml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/config.toml b/config.toml index 2260633..5434374 100644 --- a/config.toml +++ b/config.toml @@ -70,7 +70,7 @@ right = [ "Right", "l" ] up = [ "Up", "k" ] down = [ "Down", "j" ] big_up = [ "K" ] -big_down = [ "L" ] +big_down = [ "J" ] page_up = [ "PgUp" ] page_down = [ "PgDn" ] go_top = [ "g" ] From d12c9b6b2a3cf0f1efa345bd7b3e5045a3c878b3 Mon Sep 17 00:00:00 2001 From: Jeff Hughes Date: Wed, 10 Feb 2021 19:40:11 -0500 Subject: [PATCH 04/23] Reformat and fix clippy errors --- src/db.rs | 15 +++++---------- src/downloads.rs | 3 +-- src/feeds.rs | 6 ++---- src/keymap.rs | 2 +- src/opml.rs | 14 ++++++++------ src/ui/mock_panel.rs | 6 ++---- src/ui/mod.rs | 20 ++++---------------- src/ui/panel.rs | 6 ++---- 8 files changed, 25 insertions(+), 47 deletions(-) diff --git a/src/db.rs b/src/db.rs index 7c89b20..ac01d38 100644 --- a/src/db.rs +++ b/src/db.rs @@ -181,8 +181,7 @@ 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, @@ -228,8 +227,7 @@ impl Database { &self, podcast_id: i64, episode: &EpisodeNoId, - ) -> Result> - { + ) -> Result> { let conn = self.conn.as_ref().unwrap(); let pubdate = match episode.pubdate { @@ -260,8 +258,7 @@ impl Database { &self, episode_id: i64, path: &PathBuf, - ) -> Result<(), Box> - { + ) -> Result<(), Box> { let conn = self.conn.as_ref().unwrap(); let _ = conn.execute( @@ -317,8 +314,7 @@ 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 = ?, @@ -352,8 +348,7 @@ impl Database { podcast_id: i64, podcast_title: String, episodes: Vec, - ) -> SyncResult - { + ) -> SyncResult { let conn = self.conn.as_ref().unwrap(); let old_episodes = self.get_episodes(podcast_id); diff --git a/src/downloads.rs b/src/downloads.rs index a29807e..9161b10 100644 --- a/src/downloads.rs +++ b/src/downloads.rs @@ -38,8 +38,7 @@ 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(); diff --git a/src/feeds.rs b/src/feeds.rs index c2d6688..7651663 100644 --- a/src/feeds.rs +++ b/src/feeds.rs @@ -50,8 +50,7 @@ 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) => { @@ -74,8 +73,7 @@ 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/keymap.rs b/src/keymap.rs index 9b643ed..c7bdfa0 100644 --- a/src/keymap.rs +++ b/src/keymap.rs @@ -224,7 +224,7 @@ pub fn input_to_str(input: Input) -> Option { } _ => "", }; - if code == "" { + if code.is_empty() { return None; } else { return Some(code.to_string()); diff --git a/src/opml.rs b/src/opml.rs index a845dc9..c5c721c 100644 --- a/src/opml.rs +++ b/src/opml.rs @@ -47,12 +47,14 @@ 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(); - opml.head = Some(Head { - title: Some("Shellcaster Podcast Feeds".to_string()), - date_created: Some(date.to_rfc2822()), - ..Head::default() - }); + let mut opml = OPML { + head: Some(Head { + title: Some("Shellcaster Podcast Feeds".to_string()), + date_created: Some(date.to_rfc2822()), + ..Head::default() + }), + ..Default::default() + }; let mut outlines = Vec::new(); diff --git a/src/ui/mock_panel.rs b/src/ui/mock_panel.rs index 6bdb048..3d65e60 100644 --- a/src/ui/mock_panel.rs +++ b/src/ui/mock_panel.rs @@ -30,8 +30,7 @@ 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 = @@ -159,8 +158,7 @@ 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 2413001..52e178d 100644 --- a/src/ui/mod.rs +++ b/src/ui/mod.rs @@ -608,10 +608,7 @@ impl<'a> UI<'a> { // 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 - }; + delete = ask_delete.unwrap_or(false); // default not to delete } return Some(UiMsg::RemovePodcast(pod_id, delete)); @@ -641,10 +638,7 @@ impl<'a> UI<'a> { .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 - }; + delete = ask_delete.unwrap_or(false); // default not to delete } return Some(UiMsg::RemoveEpisode(pod_id, ep_id, delete)); @@ -662,10 +656,7 @@ impl<'a> UI<'a> { // 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 - }; + delete = ask_delete.unwrap_or(false); // default not to delete } return Some(UiMsg::RemoveAllEpisodes(pod_id, delete)); } @@ -739,10 +730,7 @@ impl<'a> UI<'a> { /// '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, - } + self.spawn_yes_no_notif(message).unwrap_or(false) } /// Adds a notification to the bottom of the screen that solicits diff --git a/src/ui/panel.rs b/src/ui/panel.rs index 179f55d..d8055eb 100644 --- a/src/ui/panel.rs +++ b/src/ui/panel.rs @@ -39,8 +39,7 @@ 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 { @@ -222,8 +221,7 @@ impl Panel { nchars: i32, attr: pancurses::chtype, color: ColorType, - ) - { + ) { self.window.mvchgat( self.abs_y(y), self.abs_x(x), From defdddd66bb2ed914f40bf7fac61394e27258ef2 Mon Sep 17 00:00:00 2001 From: Jeff Hughes Date: Mon, 26 Apr 2021 18:36:54 -0400 Subject: [PATCH 05/23] Add new navigation keys to help menu and readme --- README.md | 4 ++++ src/ui/popup.rs | 9 +++++++++ 2 files changed, 13 insertions(+) diff --git a/README.md b/README.md index 843c1c1..05a8343 100644 --- a/README.md +++ b/README.md @@ -177,6 +177,10 @@ The sample file above provides comments that should walk you through all the ava | ------- | -------------- | | ? | Open help window | | Arrow keys / h,j,k,l | Navigate menus | +| Shift+K | Up 1/4 page | +| Shift+J | Down 1/4 page | +| PgUp | Page up | +| PgDn | Page down | | a | Add new feed | | q | Quit program | | s | Synchronize selected feed | diff --git a/src/ui/popup.rs b/src/ui/popup.rs index c109b7f..7807693 100644 --- a/src/ui/popup.rs +++ b/src/ui/popup.rs @@ -3,6 +3,7 @@ use std::cmp::min; use super::{ColorType, Colors}; use super::{Menu, Panel, UiMsg}; +use crate::config::BIG_SCROLL_AMOUNT; use crate::keymap::{Keybindings, UserAction}; use crate::types::*; @@ -158,11 +159,19 @@ impl<'a> PopupWin<'a> { /// Create a new Panel holding a help window. pub fn make_help_win(&self) -> Panel { + let big_scroll_up = format!("Up 1/{} page:", BIG_SCROLL_AMOUNT); + let big_scroll_dn = format!("Down 1/{} page:", BIG_SCROLL_AMOUNT); let actions = vec![ (Some(UserAction::Left), "Left:"), (Some(UserAction::Right), "Right:"), (Some(UserAction::Up), "Up:"), (Some(UserAction::Down), "Down:"), + (Some(UserAction::BigUp), &big_scroll_up), + (Some(UserAction::BigDown), &big_scroll_dn), + (Some(UserAction::PageUp), "Page up:"), + (Some(UserAction::PageDown), "Page down:"), + (Some(UserAction::GoTop), "Go to top:"), + (Some(UserAction::GoBot), "Go to bottom:"), // (None, ""), (Some(UserAction::AddFeed), "Add feed:"), (Some(UserAction::Sync), "Sync:"), From 8e2267a89f6e8ad6ccb452f27085050b83e309b0 Mon Sep 17 00:00:00 2001 From: Jeff Hughes Date: Mon, 26 Apr 2021 19:10:39 -0400 Subject: [PATCH 06/23] Change hash function for podcast/episode hash maps Default hash map implementation uses a cryptographically secure and DoS resistant hasher, which is unnecessary in this case. Switched it out to just use the i64 value as the lookup key directly. --- Cargo.lock | 7 +++++++ Cargo.toml | 1 + src/types.rs | 15 +++++++++++---- 3 files changed, 19 insertions(+), 4 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index ba19742..4a4d96c 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -476,6 +476,12 @@ dependencies = [ "pkg-config", ] +[[package]] +name = "nohash-hasher" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2bf50223579dc7cdcfb3bfcacf7069ff68243f8c363f62ffa99cf000a6b9c451" + [[package]] name = "num-integer" version = "0.1.44" @@ -870,6 +876,7 @@ dependencies = [ "dirs-next", "escaper", "lazy_static", + "nohash-hasher", "opml", "pancurses", "regex", diff --git a/Cargo.toml b/Cargo.toml index c55bf62..8f7c5e3 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -29,6 +29,7 @@ sanitize-filename = "0.2.1" shellexpand = "2.0.0" dirs = { package = "dirs-next", version = "1.0.1" } opml = "0.2.4" +nohash-hasher = "0.2.0" unicode-segmentation = "1.6.0" textwrap = "0.12.1" escaper = "0.1.0" diff --git a/src/types.rs b/src/types.rs index d571a4f..0b28387 100644 --- a/src/types.rs +++ b/src/types.rs @@ -6,6 +6,7 @@ use unicode_segmentation::UnicodeSegmentation; use chrono::{DateTime, Utc}; use lazy_static::lazy_static; +use nohash_hasher::BuildNoHashHasher; use regex::Regex; use crate::downloads::DownloadMsg; @@ -273,14 +274,14 @@ impl Menuable for NewEpisode { pub struct LockVec where T: Clone + Menuable { - data: Arc>>, + data: Arc>>>, order: Arc>>, } impl LockVec { /// Create a new LockVec. pub fn new(data: Vec) -> LockVec { - let mut hm = HashMap::new(); + let mut hm = HashMap::with_hasher(BuildNoHashHasher::default()); let mut order = Vec::new(); for i in data.into_iter() { let id = i.get_id(); @@ -295,7 +296,7 @@ impl LockVec { } /// Lock the LockVec hashmap for reading/writing. - pub fn borrow_map(&self) -> MutexGuard> { + pub fn borrow_map(&self) -> MutexGuard>> { return self.data.lock().unwrap(); } @@ -305,7 +306,13 @@ impl LockVec { } /// Lock the LockVec hashmap for reading/writing. - pub fn borrow(&self) -> (MutexGuard>, MutexGuard>) { + #[allow(clippy::type_complexity)] + pub fn borrow( + &self, + ) -> ( + MutexGuard>>, + MutexGuard>, + ) { return (self.data.lock().unwrap(), self.order.lock().unwrap()); } From 638297bac398c32a215d35e77f5c9926ba66cd6c Mon Sep 17 00:00:00 2001 From: Jeff Hughes Date: Mon, 26 Apr 2021 19:48:46 -0400 Subject: [PATCH 07/23] Use cached SQL statements --- src/db.rs | 132 ++++++++++++++++++++++++++---------------------------- 1 file changed, 63 insertions(+), 69 deletions(-) diff --git a/src/db.rs b/src/db.rs index ac01d38..aa06f57 100644 --- a/src/db.rs +++ b/src/db.rs @@ -183,26 +183,22 @@ impl Database { podcast: PodcastNoId, ) -> Result> { let conn = self.conn.as_ref().unwrap(); - let _ = conn.execute( + let mut stmt = conn.prepare_cached( "INSERT INTO podcasts (title, url, description, author, explicit, last_checked) VALUES (?, ?, ?, ?, ?, ?);", - params![ - podcast.title, - podcast.url, - podcast.description, - podcast.author, - podcast.explicit, - podcast.last_checked.timestamp() - ], )?; - - 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)) - .unwrap(); + stmt.execute(params![ + podcast.title, + podcast.url, + podcast.description, + podcast.author, + podcast.explicit, + podcast.last_checked.timestamp() + ])?; + + let mut stmt = conn.prepare_cached("SELECT id FROM podcasts WHERE url = ?")?; + let pod_id = stmt.query_row::(params![podcast.url], |row| row.get(0))?; let mut ep_ids = Vec::new(); for ep in podcast.episodes.iter().rev() { let id = self.insert_episode(pod_id, &ep)?; @@ -235,21 +231,21 @@ impl Database { None => None, }; - let _ = conn.execute( + let mut stmt = conn.prepare_cached( "INSERT INTO episodes (podcast_id, title, url, description, pubdate, duration, played, hidden) VALUES (?, ?, ?, ?, ?, ?, ?, ?);", - params![ - podcast_id, - episode.title, - episode.url, - episode.description, - pubdate, - episode.duration, - false, - false, - ], )?; + stmt.execute(params![ + podcast_id, + episode.title, + episode.url, + episode.description, + pubdate, + episode.duration, + false, + false, + ])?; return Ok(conn.last_insert_rowid()); } @@ -261,11 +257,11 @@ impl Database { ) -> Result<(), Box> { let conn = self.conn.as_ref().unwrap(); - let _ = conn.execute( + let mut stmt = conn.prepare_cached( "INSERT INTO files (episode_id, path) VALUES (?, ?);", - params![episode_id, path.to_str(),], )?; + stmt.execute(params![episode_id, path.to_str(),])?; return Ok(()); } @@ -273,11 +269,10 @@ 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 - ]) + let mut stmt = conn + .prepare_cached("DELETE FROM files WHERE episode_id = ?;") .unwrap(); + stmt.execute(params![episode_id]).unwrap(); } /// Removes all file listings for the selected episode ids. @@ -288,11 +283,10 @@ impl Database { 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 - ]) + let mut stmt = conn + .prepare_cached("DELETE FROM files WHERE episode_id = (?);") .unwrap(); + stmt.execute(params![episodes]).unwrap(); } /// Removes a podcast, all episodes, and files from the database. @@ -302,9 +296,10 @@ 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]) + let mut stmt = conn + .prepare_cached("DELETE FROM podcasts WHERE id = ?;") .unwrap(); + stmt.execute(params![podcast_id]).unwrap(); } /// Updates an existing podcast in the database, where metadata is @@ -316,20 +311,20 @@ impl Database { podcast: PodcastNoId, ) -> Result> { let conn = self.conn.as_ref().unwrap(); - let _ = conn.execute( + let mut stmt = conn.prepare_cached( "UPDATE podcasts SET title = ?, url = ?, description = ?, author = ?, explicit = ?, last_checked = ? WHERE id = ?;", - params![ - podcast.title, - podcast.url, - podcast.description, - podcast.author, - podcast.explicit, - podcast.last_checked.timestamp(), - pod_id, - ], )?; + stmt.execute(params![ + podcast.title, + podcast.url, + podcast.description, + podcast.author, + podcast.explicit, + podcast.last_checked.timestamp(), + pod_id, + ])?; let result = self.update_episodes(pod_id, podcast.title, podcast.episodes); return Ok(result); @@ -399,21 +394,22 @@ impl Database { match existing_id { Some(id) => { if update { - let _ = conn - .execute( + let mut stmt = conn + .prepare_cached( "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(); + stmt.execute(params![ + new_ep.title, + new_ep.url, + new_ep.description, + new_pd, + new_ep.duration, + id, + ]) + .unwrap(); update_ep.push(id); } } @@ -440,11 +436,10 @@ 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 - ]) + let mut stmt = conn + .prepare_cached("UPDATE episodes SET played = ? WHERE id = ?;") .unwrap(); + stmt.execute(params![played, episode_id]).unwrap(); } /// Updates an episode to "remove" it by hiding it. "Removed" @@ -453,18 +448,17 @@ 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 - ]) + let mut stmt = conn + .prepare_cached("UPDATE episodes SET hidden = ? WHERE id = ?;") .unwrap(); + stmt.execute(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 mut stmt = conn.prepare_cached("SELECT * FROM podcasts;").unwrap(); let podcast_iter = stmt .query_map(params![], |row| { let pod_id = row.get("id")?; @@ -505,7 +499,7 @@ impl Database { pub fn get_episodes(&self, pod_id: i64) -> Vec { if let Some(conn) = &self.conn { let mut stmt = conn - .prepare( + .prepare_cached( "SELECT * FROM episodes LEFT JOIN files ON episodes.id = files.episode_id WHERE episodes.podcast_id = ? From 3fd961a55de4f0557728189e9ddc0310032633bf Mon Sep 17 00:00:00 2001 From: Jeff Hughes Date: Mon, 26 Apr 2021 19:56:21 -0400 Subject: [PATCH 08/23] Fix clippy warnings --- src/config.rs | 4 ++-- src/db.rs | 8 ++++---- src/downloads.rs | 6 +++--- src/main.rs | 8 ++++---- src/main_controller.rs | 8 ++++---- src/ui/mod.rs | 10 +++++----- 6 files changed, 22 insertions(+), 22 deletions(-) diff --git a/src/config.rs b/src/config.rs index 0efb625..bf198d1 100644 --- a/src/config.rs +++ b/src/config.rs @@ -1,7 +1,7 @@ use serde::Deserialize; use std::fs::File; use std::io::Read; -use std::path::PathBuf; +use std::path::{Path, PathBuf}; use crate::keymap::{Keybindings, UserAction}; @@ -98,7 +98,7 @@ impl Config { /// Given a file path, this reads a TOML config file and returns a /// Config struct with keybindings, etc. Inserts defaults if config /// file does not exist, or if specific values are not set. - pub fn new(path: &PathBuf) -> Config { + pub fn new(path: &Path) -> Config { let mut config_string = String::new(); let config_toml: ConfigFromToml; diff --git a/src/db.rs b/src/db.rs index aa06f57..5990b6d 100644 --- a/src/db.rs +++ b/src/db.rs @@ -1,4 +1,4 @@ -use std::path::PathBuf; +use std::path::{Path, PathBuf}; use chrono::{DateTime, NaiveDateTime, Utc}; use lazy_static::lazy_static; @@ -30,8 +30,8 @@ pub struct Database { impl Database { /// Creates a new connection to the database (and creates database if /// it does not already exist). Panics if database cannot be accessed. - pub fn connect(path: &PathBuf) -> Database { - let mut db_path = path.clone(); + pub fn connect(path: &Path) -> Database { + let mut db_path = path.to_path_buf(); if std::fs::create_dir_all(&db_path).is_err() { panic!("Unable to create subdirectory for database."); } @@ -253,7 +253,7 @@ impl Database { pub fn insert_file( &self, episode_id: i64, - path: &PathBuf, + path: &Path, ) -> Result<(), Box> { let conn = self.conn.as_ref().unwrap(); diff --git a/src/downloads.rs b/src/downloads.rs index 9161b10..ca15a26 100644 --- a/src/downloads.rs +++ b/src/downloads.rs @@ -1,5 +1,5 @@ use std::fs::File; -use std::path::PathBuf; +use std::path::{Path, PathBuf}; use std::sync::mpsc::Sender; use sanitize_filename::{sanitize_with_options, Options}; @@ -34,7 +34,7 @@ pub struct EpData { /// by the user while there are still ongoing jobs. pub fn download_list( episodes: Vec, - dest: &PathBuf, + dest: &Path, max_retries: usize, threadpool: &Threadpool, tx_to_main: Sender, @@ -42,7 +42,7 @@ pub fn download_list( // parse episode details and push to queue for ep in episodes.into_iter() { let tx = tx_to_main.clone(); - let dest2 = dest.clone(); + let dest2 = dest.to_path_buf(); threadpool.execute(move || { let result = download_file(ep, dest2, max_retries); tx.send(Message::Dl(result)).unwrap(); diff --git a/src/main.rs b/src/main.rs index c15a638..3460ab6 100644 --- a/src/main.rs +++ b/src/main.rs @@ -1,6 +1,6 @@ use std::fs::File; use std::io::{Read, Write}; -use std::path::PathBuf; +use std::path::{Path, PathBuf}; use std::process; use std::sync::mpsc; @@ -179,7 +179,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) { +fn sync_podcasts(db_path: &Path, config: Config, args: &clap::ArgMatches) { let db_inst = Database::connect(db_path); let podcast_list = db_inst.get_podcasts(); @@ -247,7 +247,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. -fn import(db_path: &PathBuf, config: Config, args: &clap::ArgMatches) { +fn import(db_path: &Path, config: Config, args: &clap::ArgMatches) { // read from file or from stdin let xml = match args.value_of("file") { Some(filepath) => { @@ -380,7 +380,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) { +fn export(db_path: &Path, args: &clap::ArgMatches) { let db_inst = Database::connect(&db_path); let podcast_list = db_inst.get_podcasts(); let opml = opml::export(podcast_list); diff --git a/src/main_controller.rs b/src/main_controller.rs index c5f1463..443e741 100644 --- a/src/main_controller.rs +++ b/src/main_controller.rs @@ -1,6 +1,6 @@ use std::collections::HashSet; use std::fs; -use std::path::PathBuf; +use std::path::{Path, PathBuf}; use std::sync::mpsc; use sanitize_filename::{sanitize_with_options, Options}; @@ -12,7 +12,7 @@ use crate::feeds::{self, FeedMsg, PodcastFeed}; use crate::play_file; use crate::threadpool::Threadpool; use crate::types::*; -use crate::ui::{UiMsg, UI}; +use crate::ui::{Ui, UiMsg}; /// Enum used for communicating with other threads. #[derive(Debug)] @@ -45,7 +45,7 @@ impl MainController { /// Instantiates the main controller (used during app startup), which /// sets up the connection to the database, download manager, and UI /// thread, and reads the list of podcasts from the database. - pub fn new(config: Config, db_path: &PathBuf) -> MainController { + pub fn new(config: Config, db_path: &Path) -> 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(); @@ -65,7 +65,7 @@ impl MainController { // set up UI in new thread let tx_ui_to_main = mpsc::Sender::clone(&tx_to_main); - let ui_thread = UI::spawn( + let ui_thread = Ui::spawn( config.clone(), podcast_list.clone(), rx_from_main, diff --git a/src/ui/mod.rs b/src/ui/mod.rs index 52e178d..c9c3c2e 100644 --- a/src/ui/mod.rs +++ b/src/ui/mod.rs @@ -75,7 +75,7 @@ enum ActiveMenu { /// encapsulates the pancurses windows, and holds data about the size of /// the screen. #[derive(Debug)] -pub struct UI<'a> { +pub struct Ui<'a> { stdscr: Window, n_row: i32, n_col: i32, @@ -89,7 +89,7 @@ pub struct UI<'a> { popup_win: PopupWin<'a>, } -impl<'a> 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( @@ -99,7 +99,7 @@ impl<'a> UI<'a> { tx_to_main: mpsc::Sender, ) -> thread::JoinHandle<()> { return thread::spawn(move || { - let mut ui = UI::new(&config, &items); + let mut ui = Ui::new(&config, &items); ui.init(); let mut message_iter = rx_from_main.try_iter(); // this is the main event loop: on each loop, we update @@ -142,7 +142,7 @@ impl<'a> UI<'a> { /// Initializes the UI with a list of podcasts and podcast episodes, /// creates the pancurses window and draws it to the screen, and /// returns a UI object for future manipulation. - pub fn new(config: &'a Config, items: &LockVec) -> UI<'a> { + pub fn new(config: &'a Config, items: &LockVec) -> Ui<'a> { let stdscr = pancurses::initscr(); // set some options @@ -207,7 +207,7 @@ impl<'a> UI<'a> { let notif_win = NotifWin::new(colors.clone(), n_row, n_col); let popup_win = PopupWin::new(colors.clone(), &config.keybindings, n_row, n_col); - return UI { + return Ui { stdscr, n_row, n_col, From e6f9fcd46634197f104d5a5fe1d9d0583589cc33 Mon Sep 17 00:00:00 2001 From: Jeff Hughes Date: Mon, 26 Apr 2021 20:04:04 -0400 Subject: [PATCH 09/23] Update dependency versions --- Cargo.lock | 381 +++++++++++++++++++++-------------------------------- Cargo.toml | 16 +-- 2 files changed, 159 insertions(+), 238 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index 4a4d96c..c939d54 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -19,16 +19,16 @@ dependencies = [ ] [[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.2" +name = "atom_syndication" +version = "0.9.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "23b62fc65de8e4e7f52534fb52b0f3ed04746ae267519eef2a83941e8085068b" +checksum = "2d5016bf52ff4f3ed28bf3ec1fed96b53daf4b137d5e6b9f97a8cfae7b57a3a2" +dependencies = [ + "chrono", + "derive_builder", + "diligent-date-parser", + "quick-xml", +] [[package]] name = "atty" @@ -47,12 +47,6 @@ version = "1.0.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "cdb031dd78e28731d87d56cc8ffef4a8f36ca26c38fe2de700543e627f8a464a" -[[package]] -name = "base64" -version = "0.12.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3441f0f7b02788e948e47f457ca01f1d7e6d92c693bc132c22b087d3141c03ff" - [[package]] name = "base64" version = "0.13.0" @@ -65,34 +59,17 @@ version = "1.2.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "cf1de2fe8c75bc145a2f577add951f8134889b4795d47466a54a5c846d691693" -[[package]] -name = "blake2b_simd" -version = "0.5.11" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "afa748e348ad3be8263be728124b24a24f268266f6f5d58af9d75f6a40b5c587" -dependencies = [ - "arrayref", - "arrayvec", - "constant_time_eq", -] - [[package]] name = "bumpalo" -version = "3.4.0" +version = "3.6.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2e8c087f005730276d1096a652e92a8bacee2e2472bcc9715a74d2bec38b5820" +checksum = "63396b8a4b9de3f4fdfb320ab6080762242f66a8ef174c49d8e19b674db4cdbe" [[package]] name = "cc" -version = "1.0.65" +version = "1.0.67" 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" +checksum = "e3c69b077ad434294d3ce9f1f6143a2a4b89a8a2d54ef813d85003a4fd1137fd" [[package]] name = "cfg-if" @@ -115,9 +92,9 @@ dependencies = [ [[package]] name = "chunked_transfer" -version = "1.3.0" +version = "1.4.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7477065d45a8fe57167bf3cf8bcd3729b54cfcb81cca49bda2d038ea89ae82ca" +checksum = "fff857943da45f546682664a79488be82e69e43c1a7a2307679ab9afb3a66d2e" [[package]] name = "clap" @@ -134,12 +111,6 @@ dependencies = [ "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.9.1" @@ -156,17 +127,6 @@ 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", - "cfg-if 0.1.10", - "lazy_static", -] - [[package]] name = "darling" version = "0.10.2" @@ -228,41 +188,29 @@ dependencies = [ ] [[package]] -name = "dirs" -version = "2.0.2" +name = "diligent-date-parser" +version = "0.1.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "13aea89a5c93364a98e9b37b2fa237effbb694d5cfe01c5b70941f7eb087d5e3" +checksum = "e37ea528f01b8bfca1f71bcd06a8e6c898bf8fdfbf24dd9dbc7fb49338ed6d84" dependencies = [ - "cfg-if 0.1.10", - "dirs-sys", + "chrono", ] [[package]] name = "dirs-next" -version = "1.0.2" +version = "2.0.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "cf36e65a80337bea855cd4ef9b8401ffce06a7baedf2e85ec467b1ac3f6e82b6" +checksum = "b98cf8ebf19c3d1b223e151f99a4f9f0690dca41414773390fc824184ac833e1" dependencies = [ - "cfg-if 1.0.0", + "cfg-if", "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", - "redox_users", - "winapi", -] - [[package]] name = "dirs-sys-next" -version = "0.1.1" +version = "0.1.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "99de365f605554ae33f115102a02057d4fc18b01f3284d6870be0938743cfe7d" +checksum = "4ebda144c4fe02d1f7ea1a7d9641b6fc6b580adcfa024ae48797ecdeb6825b4d" dependencies = [ "libc", "redox_users", @@ -271,11 +219,11 @@ dependencies = [ [[package]] name = "encoding_rs" -version = "0.8.26" +version = "0.8.28" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "801bbab217d7f79c0062f4f7205b5d4427c6d1a7bd7aafdd1475f7c59d62b283" +checksum = "80df024fbc5ac80f87dfef0d9f5209a252f2a497f7f42944cff24d8253cac065" dependencies = [ - "cfg-if 1.0.0", + "cfg-if", ] [[package]] @@ -328,9 +276,9 @@ checksum = "00b0228411908ca8685dba7fc2cdd70ec9990a6e753e89b6ac91a84c40fbaf4b" [[package]] name = "form_urlencoded" -version = "1.0.0" +version = "1.0.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ece68d15c92e84fa4f19d3780f1294e5ca82a78a6d515f1efaabcc144688be00" +checksum = "5fc25a87fa4fd2094bffb06925852034d90a17f0d1e05197d4956d3555752191" dependencies = [ "matches", "percent-encoding", @@ -338,20 +286,20 @@ dependencies = [ [[package]] name = "getrandom" -version = "0.1.15" +version = "0.2.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "fc587bc0ec293155d5bfa6b9891ec18a1e330c234f896ea47fbada4cadbe47e6" +checksum = "c9495705279e7140bf035dde1f6e750c162df8b625267cd52cc44e0b156732c8" dependencies = [ - "cfg-if 0.1.10", + "cfg-if", "libc", - "wasi 0.9.0+wasi-snapshot-preview1", + "wasi", ] [[package]] name = "hermit-abi" -version = "0.1.17" +version = "0.1.18" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5aca5565f760fb5b220e499d72710ed156fdb74e631659e99377d9ebfbd13ae8" +checksum = "322f4de77956e22ed0e5032c359a0f1273f1f7f0d79bfa3b8ffbc730d7fbcc5c" dependencies = [ "libc", ] @@ -364,9 +312,9 @@ checksum = "b9e0384b61958566e926dc50660321d12159025e767c18e043daf26b70104c39" [[package]] name = "idna" -version = "0.2.0" +version = "0.2.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "02e2673c30ee86b5b96a9cb52ad15718aa1f966f5ab9ad54a8b95d5ca33120a9" +checksum = "418a0a6fab821475f634efe3ccc45c013f742efe03d853e8d3355d5cb850ecf8" dependencies = [ "matches", "unicode-bidi", @@ -381,9 +329,9 @@ checksum = "5f25cca2463cb19dbb1061eb3bd38a8b5e4ce1cc5a5a9fc0e02de486d92b9b05" [[package]] name = "js-sys" -version = "0.3.45" +version = "0.3.50" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ca059e81d9486668f12d455a4ea6daa600bd408134cd17e3d3fb5a32d1f016f8" +checksum = "2d99f9e3e84b8f67f846ef5b4cbbc3b1c29f6c759fcbce6f01aa0e73d932a24c" dependencies = [ "wasm-bindgen", ] @@ -396,9 +344,9 @@ checksum = "e2abad23fbc42b3700f2f279844dc832adb2b2eb069b2df918f455c4e18cc646" [[package]] name = "libc" -version = "0.2.80" +version = "0.2.94" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4d58d1b70b004888f764dfbf6a26a3b0342a1632d33968e4a179d8011c760614" +checksum = "18794a8ad5b29321f790b55d93dfba91e125cb1a9edbd4f8e3150acc771c1a5e" [[package]] name = "libsqlite3-sys" @@ -413,17 +361,17 @@ dependencies = [ [[package]] name = "linked-hash-map" -version = "0.5.3" +version = "0.5.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8dd5a6d5999d9907cda8ed67bbd137d3af8085216c2ac62de5be860bd41f304a" +checksum = "7fb9b38af92608140b86b693604b9ffcc5824240a484d1ecd4795bacb2fe88f3" [[package]] name = "log" -version = "0.4.11" +version = "0.4.14" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4fabed175da42fed1fa0746b0ea71f412aa9d35e76e95e59b192c64b9dc2bf8b" +checksum = "51b9bbe6c47d51fc3e1a9b945965946b4c44142ab8792c50835a980d362c2710" dependencies = [ - "cfg-if 0.1.10", + "cfg-if", ] [[package]] @@ -449,9 +397,9 @@ checksum = "0ee1c47aaa256ecabcaea351eae4a9b01ef39ed810004e298d2511ed284b1525" [[package]] name = "native-tls" -version = "0.2.6" +version = "0.2.7" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6fcc7939b5edc4e4f86b1b4a04bb1498afaaf871b1a6691838ed06fcb48d3a3f" +checksum = "b8d96b2e1c8da3957d58100b09f102c6d9cfdfced01b7ec5a8974044bb09dbd4" dependencies = [ "lazy_static", "libc", @@ -467,9 +415,9 @@ dependencies = [ [[package]] name = "ncurses" -version = "5.99.0" +version = "5.101.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "15699bee2f37e9f8828c7b35b2bc70d13846db453f2d507713b758fabe536b82" +checksum = "5e2c5d34d72657dc4b638a1c25d40aae81e4f1c699062f72f467237920752032" dependencies = [ "cc", "libc", @@ -503,21 +451,21 @@ dependencies = [ [[package]] name = "once_cell" -version = "1.5.2" +version = "1.7.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "13bd41f508810a131401606d54ac32a467c97172d74ba7662562ebba5ad07fa0" +checksum = "af8b08b04175473088b46763e51ee54da5f9a164bc162f615b91bc179dbf15a3" [[package]] name = "openssl" -version = "0.10.30" +version = "0.10.33" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8d575eff3665419f9b83678ff2815858ad9d11567e082f5ac1814baba4e2bcb4" +checksum = "a61075b62a23fef5a29815de7536d940aa35ce96d18ce0cc5076272db678a577" dependencies = [ "bitflags", - "cfg-if 0.1.10", + "cfg-if", "foreign-types", - "lazy_static", "libc", + "once_cell", "openssl-sys", ] @@ -529,9 +477,9 @@ checksum = "77af24da69f9d9341038eba93a073b1fdaaa1b788221b00a69bce9e762cb32de" [[package]] name = "openssl-sys" -version = "0.9.58" +version = "0.9.61" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a842db4709b604f0fe5d1170ae3565899be2ad3d9cbc72dedc789ac0511f78de" +checksum = "313752393519e876837e09e1fa183ddef0be7735868dced3196f4472d536277f" dependencies = [ "autocfg", "cc", @@ -594,9 +542,9 @@ checksum = "ac74c624d6b2d21f425f752262f42188365d7b8ff1aff74c82e45136510a4857" [[package]] name = "proc-macro2" -version = "1.0.24" +version = "1.0.26" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1e0704ee1a7e00d7bb417d0770ea303c1bccbabf0ef1667dae92b5967f5f8a71" +checksum = "a152013215dca273577e18d2bf00fa862b89b24169fb78c4c95aeb07992c9cec" dependencies = [ "unicode-xid", ] @@ -612,9 +560,9 @@ dependencies = [ [[package]] name = "quick-xml" -version = "0.17.2" +version = "0.20.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "fe1e430bdcf30c9fdc25053b9c459bb1a4672af4617b6c783d7d91dc17c6bbb0" +checksum = "26aab6b48e2590e4a64d1ed808749ba06257882b461d01ca71baeb747074a6dd" dependencies = [ "encoding_rs", "memchr", @@ -622,20 +570,19 @@ dependencies = [ [[package]] name = "quote" -version = "1.0.7" +version = "1.0.9" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "aa563d17ecb180e500da1cfd2b028310ac758de548efdd203e18f283af693f37" +checksum = "c3d0b9745dc2debf507c8422de05d7226cc1f0644216dfdfead988f9b1ab32a7" dependencies = [ "proc-macro2", ] [[package]] name = "rand" -version = "0.7.3" +version = "0.8.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6a6b1679d49b24bbfe0c803429aa1874472f50d9b363131f0e89fc356b544d03" +checksum = "0ef9e7e66b4468674bfcb0c81af8b7fa0bb154fa9f28eb840da5c447baeb8d7e" dependencies = [ - "getrandom", "libc", "rand_chacha", "rand_core", @@ -644,9 +591,9 @@ dependencies = [ [[package]] name = "rand_chacha" -version = "0.2.2" +version = "0.3.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f4c8ed856279c9737206bf725bf36935d8666ead7aa69b52be55af369d193402" +checksum = "e12735cf05c9e10bf21534da50a147b924d555dc7a547c42e6bb2d5b6017ae0d" dependencies = [ "ppv-lite86", "rand_core", @@ -654,56 +601,57 @@ dependencies = [ [[package]] name = "rand_core" -version = "0.5.1" +version = "0.6.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "90bde5296fc891b0cef12a6d03ddccc162ce7b2aff54160af9338f8d40df6d19" +checksum = "34cf66eb183df1c5876e2dcf6b13d57340741e8dc255b48e40a26de954d06ae7" dependencies = [ "getrandom", ] [[package]] name = "rand_hc" -version = "0.2.0" +version = "0.3.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ca3129af7b92a17112d59ad498c6f81eaf463253766b90396d39ea7a39d6613c" +checksum = "3190ef7066a446f2e7f42e239d161e905420ccab01eb967c9eb27d21b2322a73" dependencies = [ "rand_core", ] [[package]] name = "redox_syscall" -version = "0.1.57" +version = "0.2.6" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "41cc0f7e4d5d4544e8861606a285bb08d3e70712ccc7d2b84d7c0ccfaf4b05ce" +checksum = "8270314b5ccceb518e7e578952f0b72b88222d02e8f77f5ecf7abbb673539041" +dependencies = [ + "bitflags", +] [[package]] name = "redox_users" -version = "0.3.5" +version = "0.4.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "de0737333e7a9502c789a36d7c7fa6092a49895d4faa31ca5df163857ded2e9d" +checksum = "528532f3d801c87aec9def2add9ca802fe569e44a544afe633765267840abe64" dependencies = [ "getrandom", "redox_syscall", - "rust-argon2", ] [[package]] name = "regex" -version = "1.4.2" +version = "1.4.6" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "38cf2c13ed4745de91a5eb834e11c00bcc3709e773173b2ce4c56c9fbde04b9c" +checksum = "2a26af418b574bd56588335b3a3659a65725d4e636eb1016c2f9e3b38c7cc759" dependencies = [ "aho-corasick", "memchr", "regex-syntax", - "thread_local", ] [[package]] name = "regex-syntax" -version = "0.6.21" +version = "0.6.23" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3b181ba2dcf07aaccad5448e8ead58db5b742cf85dfe035e2227f137a539a189" +checksum = "24d5f089152e60f62d28b835fbff2cd2e8dc0baf1ac13343bef92ab7eed84548" [[package]] name = "remove_dir_all" @@ -716,9 +664,9 @@ dependencies = [ [[package]] name = "ring" -version = "0.16.17" +version = "0.16.20" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c5911690c9b773bab7e657471afc207f3827b249a657241327e3544d79bcabdd" +checksum = "3053cf52e236a3ed746dfc745aa9cacf1b791d846bdaf412f60a8d7d6e17c8fc" dependencies = [ "cc", "libc", @@ -731,10 +679,11 @@ dependencies = [ [[package]] name = "rss" -version = "1.9.0" +version = "1.10.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "99979205510c60f80a119dedbabd0b8426517384edf205322f8bcd51796bcef9" +checksum = "02e70d6ae72f8a4333af8ce9dce58942020528430eb0d46ee2fcb5e8d4d16377" dependencies = [ + "atom_syndication", "derive_builder", "quick-xml", ] @@ -754,25 +703,13 @@ dependencies = [ "time", ] -[[package]] -name = "rust-argon2" -version = "0.8.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9dab61250775933275e84053ac235621dfb739556d5c54a2f2e9313b7cf43a19" -dependencies = [ - "base64 0.12.3", - "blake2b_simd", - "constant_time_eq", - "crossbeam-utils", -] - [[package]] name = "rustls" -version = "0.18.1" +version = "0.19.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5d1126dcf58e93cee7d098dbda643b5f92ed724f1f6a63007c1116eed6700c81" +checksum = "35edb675feee39aec9c99fa5ff985081995a06d594114ae14cbe797ad7b7a6d7" dependencies = [ - "base64 0.12.3", + "base64", "log", "ring", "sct", @@ -801,9 +738,9 @@ dependencies = [ [[package]] name = "sct" -version = "0.6.0" +version = "0.6.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e3042af939fca8c3453b7af0f1c66e533a15a86169e39de2657310ade8f98d3c" +checksum = "b362b83898e0e69f38515b82ee15aa80636befe47c3b6d3d89a911e78fc228ce" dependencies = [ "ring", "untrusted", @@ -811,9 +748,9 @@ dependencies = [ [[package]] name = "security-framework" -version = "2.0.0" +version = "2.2.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c1759c2e3c8580017a484a7ac56d3abc5a6c1feadf88db2f3633f12ae4268c69" +checksum = "3670b1d2fdf6084d192bc71ead7aabe6c06aa2ea3fbd9cc3ac111fa5c2b1bd84" dependencies = [ "bitflags", "core-foundation", @@ -824,9 +761,9 @@ dependencies = [ [[package]] name = "security-framework-sys" -version = "2.0.0" +version = "2.2.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f99b9d5e26d2a71633cc4f2ebae7cc9f874044e0c351a27e17892d76dce5678b" +checksum = "3676258fd3cfe2c9a0ec99ce3038798d847ce3e4bb17746373eb9f0f1ac16339" dependencies = [ "core-foundation-sys", "libc", @@ -849,18 +786,18 @@ checksum = "388a1df253eca08550bef6c72392cfe7c30914bf41df5269b68cbd6ff8f570a3" [[package]] name = "serde" -version = "1.0.117" +version = "1.0.125" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b88fa983de7720629c9387e9f517353ed404164b1e482c970a90c1a4aaf7dc1a" +checksum = "558dc50e1a5a5fa7112ca2ce4effcb321b0300c0d4ccf0776a9f60cd89031171" dependencies = [ "serde_derive", ] [[package]] name = "serde_derive" -version = "1.0.117" +version = "1.0.125" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "cbd1ae72adb44aab48f325a02444a5fc079349a8d804c1fc922aed3f7454c74e" +checksum = "b093b7a2bb58203b5da3056c05b4ec1fed827dcfdb37347a8841695263b3d06d" dependencies = [ "proc-macro2", "quote", @@ -894,11 +831,11 @@ dependencies = [ [[package]] name = "shellexpand" -version = "2.0.0" +version = "2.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9a2b22262a9aaf9464d356f656fea420634f78c881c5eebd5ef5e66d8b9bc603" +checksum = "83bdb7831b2d85ddf4a7b148aa19d0587eddbe8671a436b7bd1182eaad0f2829" dependencies = [ - "dirs", + "dirs-next", ] [[package]] @@ -909,9 +846,9 @@ checksum = "6e63cff320ae2c57904679ba7cb63280a3dc4613885beafb148ee7bf9aa9042d" [[package]] name = "strong-xml" -version = "0.6.0" +version = "0.6.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ee06e7e5baf4508dea83506a83fcc5b80a404d4c0e9c473c9a4b38b802af3a07" +checksum = "73d7a5a280d6097649ea2254eb65d38e81f1752fa47ea69d5fa2179470c8bf4c" dependencies = [ "jetscii", "lazy_static", @@ -922,9 +859,9 @@ dependencies = [ [[package]] name = "strong-xml-derive" -version = "0.6.0" +version = "0.6.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d2e4e25fb64e61f55d495134d9e5ac68b1fa4bb2855b5a5b53857b9460e2bfde" +checksum = "2c3fa5e97f5557b119549b559e37bd3990528534110d0fdfa6c7e9b4c9a9d75a" dependencies = [ "proc-macro2", "quote", @@ -945,9 +882,9 @@ checksum = "6446ced80d6c486436db5c078dde11a9f73d42b57fb273121e160b84f63d894c" [[package]] name = "syn" -version = "1.0.50" +version = "1.0.70" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "443b4178719c5a851e1bde36ce12da21d74a0e60b4d982ec3385a933c812f0f6" +checksum = "b9505f307c872bab8eb46f77ae357c8eba1fdacead58ee5a850116b1d7f82883" dependencies = [ "proc-macro2", "quote", @@ -956,11 +893,11 @@ dependencies = [ [[package]] name = "tempfile" -version = "3.1.0" +version = "3.2.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7a6e24d9338a0a5be79593e2fa15a648add6138caa803e2d5bc782c371732ca9" +checksum = "dac1c663cfc93810f88aed9b8941d48cabf856a1b111c29a40439018d870eb22" dependencies = [ - "cfg-if 0.1.10", + "cfg-if", "libc", "rand", "redox_syscall", @@ -986,31 +923,21 @@ dependencies = [ "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", -] - [[package]] name = "time" -version = "0.1.44" +version = "0.1.43" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6db9e6914ab8b1ae1c260a4ae7a49b6c5611b40328a735b21862567685e73255" +checksum = "ca8a50ef2360fbd1eeb0ecd46795a87a19024eb4b53c5dc916ca1fd95fe62438" dependencies = [ "libc", - "wasi 0.10.0+wasi-snapshot-preview1", "winapi", ] [[package]] name = "tinyvec" -version = "1.1.0" +version = "1.2.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ccf8dbc19eb42fba10e8feaaec282fb50e2c14b2726d6301dbfeed0f73306a6f" +checksum = "5b5220f05bb7de7f3f53c7c065e1199b3172696fe2db9f9c4d8ad9b4ee74c342" dependencies = [ "tinyvec_macros", ] @@ -1023,36 +950,36 @@ checksum = "cda74da7e1a664f795bb1f8a87ec406fb89a02522cf6e50620d016add6dbbf5c" [[package]] name = "toml" -version = "0.5.7" +version = "0.5.8" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "75cf45bb0bef80604d001caaec0d09da99611b3c0fd39d3080468875cdb65645" +checksum = "a31142970826733df8241ef35dc040ef98c679ab14d7c3e54d827099b3acecaa" dependencies = [ "serde", ] [[package]] name = "unicode-bidi" -version = "0.3.4" +version = "0.3.5" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "49f2bd0c6468a8230e1db229cff8029217cf623c767ea5d60bfbd42729ea54d5" +checksum = "eeb8be209bb1c96b7c177c7420d26e04eccacb0eeae6b980e35fcb74678107e0" dependencies = [ "matches", ] [[package]] name = "unicode-normalization" -version = "0.1.16" +version = "0.1.17" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a13e63ab62dbe32aeee58d1c5408d35c36c392bba5d9d3142287219721afe606" +checksum = "07fbfce1c8a97d547e8b5334978438d9d6ec8c20e38f56d4a4374d181493eaef" dependencies = [ "tinyvec", ] [[package]] name = "unicode-segmentation" -version = "1.7.0" +version = "1.7.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "db8716a166f290ff49dabc18b44aa407cb7c6dbe1aa0971b44b8a24b0ca35aae" +checksum = "bb0d2e7be6ae3a5fa87eed5fb451aff96f2573d2694942e40543ae0bbe19c796" [[package]] name = "unicode-width" @@ -1074,11 +1001,11 @@ checksum = "a156c684c91ea7d62626509bce3cb4e1d9ed5c4d978f7b4352658f96a4c26b4a" [[package]] name = "ureq" -version = "1.5.2" +version = "1.5.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a599426c7388ab189dfd0eeb84c8d879490abc73e3e62a0b6a40e286f6427ab7" +checksum = "294b85ef5dbc3670a72e82a89971608a1fcc4ed5c7c5a2895230d31a95f0569b" dependencies = [ - "base64 0.13.0", + "base64", "chunked_transfer", "log", "native-tls", @@ -1092,9 +1019,9 @@ dependencies = [ [[package]] name = "url" -version = "2.2.0" +version = "2.2.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5909f2b0817350449ed73e8bcd81c8c3c8d9a7a5d8acba4b27db277f1868976e" +checksum = "9ccd964113622c8e9322cfac19eb1004a07e636c545f325da085d5cdde6f1f8b" dependencies = [ "form_urlencoded", "idna", @@ -1104,9 +1031,9 @@ dependencies = [ [[package]] name = "vcpkg" -version = "0.2.10" +version = "0.2.12" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6454029bf181f092ad1b853286f23e2c507d8e8194d01d92da4a55c274a5508c" +checksum = "cbdbff6266a24120518560b5dc983096efb98462e51d0d68169895b237be3e5d" [[package]] name = "vec_map" @@ -1116,31 +1043,25 @@ 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" +version = "0.10.2+wasi-snapshot-preview1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1a143597ca7c7793eff794def352d41792a93c481eb1042423ff7ff72ba2c31f" +checksum = "fd6fbd9a79829dd1ad0cc20627bf1ed606756a7f77edff7b66b7064f9cb327c6" [[package]] name = "wasm-bindgen" -version = "0.2.68" +version = "0.2.73" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1ac64ead5ea5f05873d7c12b545865ca2b8d28adfc50a49b84770a3a97265d42" +checksum = "83240549659d187488f91f33c0f8547cbfef0b2088bc470c116d1d260ef623d9" dependencies = [ - "cfg-if 0.1.10", + "cfg-if", "wasm-bindgen-macro", ] [[package]] name = "wasm-bindgen-backend" -version = "0.2.68" +version = "0.2.73" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f22b422e2a757c35a73774860af8e112bff612ce6cb604224e8e47641a9e4f68" +checksum = "ae70622411ca953215ca6d06d3ebeb1e915f0f6613e3b495122878d7ebec7dae" dependencies = [ "bumpalo", "lazy_static", @@ -1153,9 +1074,9 @@ dependencies = [ [[package]] name = "wasm-bindgen-macro" -version = "0.2.68" +version = "0.2.73" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6b13312a745c08c469f0b292dd2fcd6411dba5f7160f593da6ef69b64e407038" +checksum = "3e734d91443f177bfdb41969de821e15c516931c3c3db3d318fa1b68975d0f6f" dependencies = [ "quote", "wasm-bindgen-macro-support", @@ -1163,9 +1084,9 @@ dependencies = [ [[package]] name = "wasm-bindgen-macro-support" -version = "0.2.68" +version = "0.2.73" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f249f06ef7ee334cc3b8ff031bfc11ec99d00f34d86da7498396dc1e3b1498fe" +checksum = "d53739ff08c8a68b0fdbcd54c372b8ab800b1449ab3c9d706503bc7dd1621b2c" dependencies = [ "proc-macro2", "quote", @@ -1176,15 +1097,15 @@ dependencies = [ [[package]] name = "wasm-bindgen-shared" -version = "0.2.68" +version = "0.2.73" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1d649a3145108d7d3fbcde896a468d1bd636791823c9921135218ad89be08307" +checksum = "d9a543ae66aa233d14bb765ed9af4a33e81b8b58d1584cf1b47ff8cd0b9e4489" [[package]] name = "web-sys" -version = "0.3.45" +version = "0.3.50" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4bf6ef87ad7ae8008e15a355ce696bed26012b7caa21605188cfd8214ab51e2d" +checksum = "a905d57e488fec8861446d3393670fb50d27a262344013181c2cdf9fff5481be" dependencies = [ "js-sys", "wasm-bindgen", @@ -1192,9 +1113,9 @@ dependencies = [ [[package]] name = "webpki" -version = "0.21.3" +version = "0.21.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ab146130f5f790d45f82aeeb09e55a256573373ec64409fc19a6fb82fb1032ae" +checksum = "b8e38c0608262c46d4a56202ebabdeb094cef7e560ca7a226c6bf055188aa4ea" dependencies = [ "ring", "untrusted", @@ -1202,9 +1123,9 @@ dependencies = [ [[package]] name = "webpki-roots" -version = "0.20.0" +version = "0.21.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0f20dea7535251981a9670857150d571846545088359b28e4951d350bdaf179f" +checksum = "aabe153544e473b775453675851ecc86863d2a81d786d741f6b76778f2a48940" dependencies = [ "webpki", ] diff --git a/Cargo.toml b/Cargo.toml index 8f7c5e3..3680b3f 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -17,26 +17,26 @@ readme = "README.md" [dependencies] pancurses = "0.16.1" -rss = "1.9.0" +rss = "1.10.0" rusqlite = "0.21.0" clap = "2.33.1" -toml = "0.5.6" -serde = { version = "1.0.106", features = ["derive"] } +toml = "0.5.8" +serde = { version = "1.0.125", features = ["derive"] } chrono = "0.4.11" lazy_static = "1.4.0" -regex = "1.3.6" +regex = "1.4.6" sanitize-filename = "0.2.1" -shellexpand = "2.0.0" -dirs = { package = "dirs-next", version = "1.0.1" } +shellexpand = "2.1.0" +dirs = { package = "dirs-next", version = "2.0.0" } opml = "0.2.4" nohash-hasher = "0.2.0" -unicode-segmentation = "1.6.0" +unicode-segmentation = "1.7.1" textwrap = "0.12.1" escaper = "0.1.0" semver = "0.10.0" [dependencies.ureq] -version = "1.3.0" +version = "1.5.4" default-features = false From bdf923ddc5abf6997fde9b137f410e485a9952ac Mon Sep 17 00:00:00 2001 From: Jeff Hughes Date: Mon, 3 May 2021 18:50:39 -0400 Subject: [PATCH 10/23] Add more error handling Adds anyhow as project dependency, and changes many functions to return anyhow::Result, which reduces many of the unwrap() calls. There are still many places where the code could potentially panic, but the majority of the remaining unwrap() calls are at least changed to expect() to provide a message as well. --- Cargo.lock | 7 + Cargo.toml | 1 + src/config.rs | 58 +++--- src/db.rs | 390 ++++++++++++++++++----------------------- src/downloads.rs | 3 +- src/feeds.rs | 18 +- src/main.rs | 326 +++++++++++++++++----------------- src/main_controller.rs | 123 +++++++++---- src/opml.rs | 5 +- src/play_file.rs | 7 +- src/sanitizer.rs | 4 +- src/threadpool.rs | 16 +- src/types.rs | 24 ++- src/ui/colors.rs | 2 +- src/ui/menu.rs | 17 +- src/ui/mod.rs | 20 ++- src/ui/notification.rs | 5 +- src/ui/popup.rs | 6 +- 18 files changed, 541 insertions(+), 491 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index c939d54..ec23b22 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -18,6 +18,12 @@ dependencies = [ "winapi", ] +[[package]] +name = "anyhow" +version = "1.0.40" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "28b2cd92db5cbd74e8e5028f7e27dd7aa3090e89e4f2a197cc7c8dfb69c7063b" + [[package]] name = "atom_syndication" version = "0.9.1" @@ -808,6 +814,7 @@ dependencies = [ name = "shellcaster" version = "1.1.0" dependencies = [ + "anyhow", "chrono", "clap", "dirs-next", diff --git a/Cargo.toml b/Cargo.toml index 3680b3f..cd461f9 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -21,6 +21,7 @@ rss = "1.10.0" rusqlite = "0.21.0" clap = "2.33.1" toml = "0.5.8" +anyhow = "1.0.40" serde = { version = "1.0.125", features = ["derive"] } chrono = "0.4.11" lazy_static = "1.4.0" diff --git a/src/config.rs b/src/config.rs index bf198d1..bb27f19 100644 --- a/src/config.rs +++ b/src/config.rs @@ -1,3 +1,4 @@ +use anyhow::{anyhow, Context, Result}; use serde::Deserialize; use std::fs::File; use std::io::Read; @@ -98,16 +99,17 @@ impl Config { /// Given a file path, this reads a TOML config file and returns a /// Config struct with keybindings, etc. Inserts defaults if config /// file does not exist, or if specific values are not set. - pub fn new(path: &Path) -> Config { + pub fn new(path: &Path) -> Result { let mut config_string = String::new(); let config_toml: ConfigFromToml; match File::open(path) { Ok(mut file) => { - file.read_to_string(&mut config_string) - .expect("Error reading config.toml. Please ensure file is readable."); + file.read_to_string(&mut config_string).with_context(|| { + "Could not read config.toml. Please ensure file is readable." + })?; config_toml = toml::from_str(&config_string) - .expect("Error parsing config.toml. Please check file syntax."); + .with_context(|| "Could not parse config.toml. Please check file syntax.")?; } Err(_) => { // if we can't find the file, set everything to empty @@ -157,7 +159,7 @@ impl Config { /// that specifies user settings where indicated, and defaults for any /// settings that were not specified by the user. #[allow(clippy::type_complexity)] -fn config_with_defaults(config_toml: &ConfigFromToml) -> Config { +fn config_with_defaults(config_toml: &ConfigFromToml) -> Result { // specify all default keybindings for actions #[rustfmt::skip] let action_map: Vec<(&Option>, UserAction, Vec)> = vec![ @@ -204,7 +206,7 @@ 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()); + 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(), @@ -231,14 +233,14 @@ fn config_with_defaults(config_toml: &ConfigFromToml) -> Config { None => 3, }; - return Config { + return Ok(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, - }; + }); } @@ -248,29 +250,35 @@ fn config_with_defaults(config_toml: &ConfigFromToml) -> Config { /// variables cannot be found, if OS could not produce the appropriate /// default directory, or if the specified directories in the path could /// not be created. -fn parse_create_dir(user_dir: Option<&str>, default: Option) -> PathBuf { +fn parse_create_dir(user_dir: Option<&str>, default: Option) -> Result { let final_path = match user_dir { - Some(path) => { - match shellexpand::full(path) { - Ok(realpath) => PathBuf::from(realpath.as_ref()), - Err(err) => panic!("Could not parse environment variable {} in config.toml. Reason: {}", err.var_name, err.cause), + Some(path) => match shellexpand::full(path) { + Ok(realpath) => PathBuf::from(realpath.as_ref()), + Err(err) => { + return Err(anyhow!( + "Could not parse environment variable {} in config.toml. Reason: {}", + err.var_name, + err.cause + )) } }, None => { - match default { - Some(mut path) => { - path.push("shellcaster"); - path - }, - None => panic!("Could not identify a default directory for your OS. Please specify paths manually in config.toml."), + if let Some(mut path) = default { + path.push("shellcaster"); + path + } else { + return Err(anyhow!("Could not identify a default directory for your OS. Please specify paths manually in config.toml.")); } - }, + } }; // create directories if they do not exist - if let Err(err) = std::fs::create_dir_all(&final_path) { - panic!("Could not create filepath: {}", err); - } - - return final_path; + std::fs::create_dir_all(&final_path).with_context(|| { + format!( + "Could not create filepath: {}", + final_path.to_string_lossy() + ) + })?; + + return Ok(final_path); } diff --git a/src/db.rs b/src/db.rs index 5990b6d..a5075ae 100644 --- a/src/db.rs +++ b/src/db.rs @@ -1,3 +1,4 @@ +use anyhow::{Context, Result}; use std::path::{Path, PathBuf}; use chrono::{DateTime, NaiveDateTime, Utc}; @@ -11,7 +12,7 @@ use crate::types::*; lazy_static! { /// Regex for removing "A", "An", and "The" from the beginning of /// podcast titles - static ref RE_ARTICLES: Regex = Regex::new(r"^(a|an|the) ").unwrap(); + static ref RE_ARTICLES: Regex = Regex::new(r"^(a|an|the) ").expect("Regex error."); } @@ -30,70 +31,63 @@ pub struct Database { impl Database { /// Creates a new connection to the database (and creates database if /// it does not already exist). Panics if database cannot be accessed. - pub fn connect(path: &Path) -> Database { + pub fn connect(path: &Path) -> Result { let mut db_path = path.to_path_buf(); - if std::fs::create_dir_all(&db_path).is_err() { - panic!("Unable to create subdirectory for database."); - } + std::fs::create_dir_all(&db_path) + .with_context(|| "Unable to create subdirectory for database.")?; db_path.push("data.db"); - match Connection::open(db_path) { - Ok(conn) => { - let db_conn = Database { - conn: Some(conn), - }; - db_conn.create(); - - { - let conn = db_conn.conn.as_ref().unwrap(); - - // SQLite defaults to foreign key support off - 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 db_version = stmt.query_row(params![], |row| { - let vstr: String = row.get("version")?; - Ok(Version::parse(&vstr).unwrap()) - }); - - // compare to current app version - let curr_ver = Version::parse(crate::VERSION).unwrap(); - // (db_version exists, needs update) - let to_update = match db_version { - Ok(dbv) => { - if dbv < curr_ver { - (true, true) - } else { - (true, false) - } - } - Err(_) => (false, true), - }; - - if to_update.1 { - // any version checks for DB migrations should go - // here first, before we update the version - - db_conn.update_version(curr_ver, to_update.0); - } + let conn = Connection::open(db_path)?; + let db_conn = Database { + conn: Some(conn), + }; + db_conn.create()?; + + { + let conn = db_conn + .conn + .as_ref() + .expect("Error connecting to database."); + + // SQLite defaults to foreign key support off + conn.execute("PRAGMA foreign_keys=ON;", params![]) + .expect("Could not set database parameters."); + + // get version number stored in database + let mut stmt = conn.prepare("SELECT version FROM version WHERE id = 1;")?; + let vstr: Result = + stmt.query_row(params![], |row| row.get("version")); + + // compare to current app version + let curr_ver = Version::parse(crate::VERSION)?; + + // (db_version exists, needs update) + let to_update = match vstr { + Ok(vstr) => { + let db_version = Version::parse(&vstr)?; + (true, db_version < curr_ver) } + Err(_) => (false, true), + }; + + if to_update.1 { + // any version checks for DB migrations should go + // here first, before we update the version - return db_conn; + db_conn.update_version(curr_ver, to_update.0)?; } - Err(err) => panic!("Could not open database: {}", err), - }; + } + + return Ok(db_conn); } /// Creates the necessary database tables, if they do not already /// exist. Panics if database cannot be accessed, or if tables cannot /// be created. - pub fn create(&self) { - let conn = self.conn.as_ref().unwrap(); + pub fn create(&self) -> Result<()> { + let conn = self.conn.as_ref().expect("Error connecting to database."); // create podcasts table - match conn.execute( + conn.execute( "CREATE TABLE IF NOT EXISTS podcasts ( id INTEGER PRIMARY KEY NOT NULL, title TEXT NOT NULL, @@ -104,13 +98,11 @@ impl Database { last_checked INTEGER );", params![], - ) { - Ok(_) => (), - Err(err) => panic!("Could not create podcasts database table: {}", err), - } + ) + .with_context(|| "Could not create podcasts database table")?; // create episodes table - match conn.execute( + conn.execute( "CREATE TABLE IF NOT EXISTS episodes ( id INTEGER PRIMARY KEY NOT NULL, podcast_id INTEGER NOT NULL, @@ -124,13 +116,11 @@ impl Database { FOREIGN KEY(podcast_id) REFERENCES podcasts(id) ON DELETE CASCADE );", params![], - ) { - Ok(_) => (), - Err(err) => panic!("Could not create episodes database table: {}", err), - } + ) + .with_context(|| "Could not create episodes database table")?; // create files table - match conn.execute( + conn.execute( "CREATE TABLE IF NOT EXISTS files ( id INTEGER PRIMARY KEY NOT NULL, episode_id INTEGER NOT NULL, @@ -138,51 +128,46 @@ impl Database { FOREIGN KEY (episode_id) REFERENCES episodes(id) ON DELETE CASCADE );", params![], - ) { - Ok(_) => (), - Err(err) => panic!("Could not create files database table: {}", err), - } + ) + .with_context(|| "Could not create files database table")?; - match conn.execute( + conn.execute( "CREATE TABLE IF NOT EXISTS version ( id INTEGER PRIMARY KEY NOT NULL, version TEXT NOT NULL );", params![], - ) { - Ok(_) => (), - Err(err) => panic!("Could not create version database table: {}", err), - } + ) + .with_context(|| "Could not create version database table")?; + return Ok(()); } /// If version stored in database is less than the current version /// of the app, this updates the value stored in the database to /// match. - fn update_version(&self, current_version: Version, update: bool) { - let conn = self.conn.as_ref().unwrap(); + fn update_version(&self, current_version: Version, update: bool) -> Result<()> { + let conn = self.conn.as_ref().expect("Error connecting to database."); if update { - let _ = conn.execute( + conn.execute( "UPDATE version SET version = ? WHERE id = ?;", params![current_version.to_string(), 1], - ); + )?; } else { - let _ = conn.execute( + conn.execute( "INSERT INTO version (id, version) VALUES (?, ?)", params![1, current_version.to_string()], - ); + )?; } + return Ok(()); } /// Inserts a new podcast and list of podcast episodes into the /// database. - pub fn insert_podcast( - &self, - podcast: PodcastNoId, - ) -> Result> { - let conn = self.conn.as_ref().unwrap(); + pub fn insert_podcast(&self, podcast: PodcastNoId) -> Result { + let conn = self.conn.as_ref().expect("Error connecting to database."); let mut stmt = conn.prepare_cached( "INSERT INTO podcasts (title, url, description, author, explicit, last_checked) @@ -219,12 +204,8 @@ impl Database { } /// Inserts a podcast episode into the database. - pub fn insert_episode( - &self, - podcast_id: i64, - episode: &EpisodeNoId, - ) -> Result> { - let conn = self.conn.as_ref().unwrap(); + pub fn insert_episode(&self, podcast_id: i64, episode: &EpisodeNoId) -> Result { + let conn = self.conn.as_ref().expect("Error connecting to database."); let pubdate = match episode.pubdate { Some(dt) => Some(dt.timestamp()), @@ -250,12 +231,8 @@ impl Database { } /// Inserts a filepath to a downloaded episode. - pub fn insert_file( - &self, - episode_id: i64, - path: &Path, - ) -> Result<(), Box> { - let conn = self.conn.as_ref().unwrap(); + pub fn insert_file(&self, episode_id: i64, path: &Path) -> Result<()> { + let conn = self.conn.as_ref().expect("Error connecting to database."); let mut stmt = conn.prepare_cached( "INSERT INTO files (episode_id, path) @@ -267,50 +244,43 @@ impl Database { /// Removes a file listing for an episode from the database when the /// user has chosen to delete the file. - pub fn remove_file(&self, episode_id: i64) { - let conn = self.conn.as_ref().unwrap(); - let mut stmt = conn - .prepare_cached("DELETE FROM files WHERE episode_id = ?;") - .unwrap(); - stmt.execute(params![episode_id]).unwrap(); + pub fn remove_file(&self, episode_id: i64) -> Result<()> { + let conn = self.conn.as_ref().expect("Error connecting to database."); + let mut stmt = conn.prepare_cached("DELETE FROM files WHERE episode_id = ?;")?; + stmt.execute(params![episode_id])?; + return Ok(()); } /// Removes all file listings for the selected episode ids. - pub fn remove_files(&self, episode_ids: &[i64]) { - let conn = self.conn.as_ref().unwrap(); + pub fn remove_files(&self, episode_ids: &[i64]) -> Result<()> { + let conn = self.conn.as_ref().expect("Error connecting to database."); // convert list of episode ids into a comma-separated String let episode_list: Vec = episode_ids.iter().map(|x| x.to_string()).collect(); let episodes = episode_list.join(", "); - let mut stmt = conn - .prepare_cached("DELETE FROM files WHERE episode_id = (?);") - .unwrap(); - stmt.execute(params![episodes]).unwrap(); + let mut stmt = conn.prepare_cached("DELETE FROM files WHERE episode_id = (?);")?; + stmt.execute(params![episodes])?; + return Ok(()); } /// Removes a podcast, all episodes, and files from the database. - pub fn remove_podcast(&self, podcast_id: i64) { - let conn = self.conn.as_ref().unwrap(); + pub fn remove_podcast(&self, podcast_id: i64) -> Result<()> { + let conn = self.conn.as_ref().expect("Error connecting to database."); // Note: Because of the foreign key constraints on `episodes` // and `files` tables, all associated episodes for this podcast // will also be deleted, and all associated file entries for // those episodes as well. - let mut stmt = conn - .prepare_cached("DELETE FROM podcasts WHERE id = ?;") - .unwrap(); - stmt.execute(params![podcast_id]).unwrap(); + let mut stmt = conn.prepare_cached("DELETE FROM podcasts WHERE id = ?;")?; + stmt.execute(params![podcast_id])?; + return Ok(()); } /// 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> { - let conn = self.conn.as_ref().unwrap(); + pub fn update_podcast(&self, pod_id: i64, podcast: PodcastNoId) -> Result { + let conn = self.conn.as_ref().expect("Error connecting to database."); let mut stmt = conn.prepare_cached( "UPDATE podcasts SET title = ?, url = ?, description = ?, author = ?, explicit = ?, last_checked = ? @@ -326,7 +296,7 @@ impl Database { pod_id, ])?; - let result = self.update_episodes(pod_id, podcast.title, podcast.episodes); + let result = self.update_episodes(pod_id, podcast.title, podcast.episodes)?; return Ok(result); } @@ -343,10 +313,10 @@ impl Database { podcast_id: i64, podcast_title: String, episodes: Vec, - ) -> SyncResult { - let conn = self.conn.as_ref().unwrap(); + ) -> Result { + let conn = self.conn.as_ref().expect("Error connecting to database."); - let old_episodes = self.get_episodes(podcast_id); + let old_episodes = self.get_episodes(podcast_id)?; let mut insert_ep = Vec::new(); let mut update_ep = Vec::new(); @@ -394,13 +364,11 @@ impl Database { match existing_id { Some(id) => { if update { - let mut stmt = conn - .prepare_cached( - "UPDATE episodes SET title = ?, url = ?, + let mut stmt = conn.prepare_cached( + "UPDATE episodes SET title = ?, url = ?, description = ?, pubdate = ?, duration = ? WHERE id = ?;", - ) - .unwrap(); + )?; stmt.execute(params![ new_ep.title, new_ep.url, @@ -408,13 +376,12 @@ impl Database { new_pd, new_ep.duration, id, - ]) - .unwrap(); + ])?; update_ep.push(id); } } None => { - let id = self.insert_episode(podcast_id, &new_ep).unwrap(); + let id = self.insert_episode(podcast_id, &new_ep)?; let new_ep = NewEpisode { id: id, pod_id: podcast_id, @@ -426,123 +393,114 @@ impl Database { } } } - return SyncResult { + return Ok(SyncResult { added: insert_ep, updated: update_ep, - }; + }); } /// Updates an episode to mark it as played or unplayed. - pub fn set_played_status(&self, episode_id: i64, played: bool) { - let conn = self.conn.as_ref().unwrap(); + pub fn set_played_status(&self, episode_id: i64, played: bool) -> Result<()> { + let conn = self.conn.as_ref().expect("Error connecting to database."); - let mut stmt = conn - .prepare_cached("UPDATE episodes SET played = ? WHERE id = ?;") - .unwrap(); - stmt.execute(params![played, episode_id]).unwrap(); + let mut stmt = conn.prepare_cached("UPDATE episodes SET played = ? WHERE id = ?;")?; + stmt.execute(params![played, episode_id])?; + return Ok(()); } /// Updates an episode to "remove" it by hiding it. "Removed" /// episodes need to stay in the database so that they don't get /// re-added when the podcast is synced again. - pub fn hide_episode(&self, episode_id: i64, hide: bool) { - let conn = self.conn.as_ref().unwrap(); + pub fn hide_episode(&self, episode_id: i64, hide: bool) -> Result<()> { + let conn = self.conn.as_ref().expect("Error connecting to database."); - let mut stmt = conn - .prepare_cached("UPDATE episodes SET hidden = ? WHERE id = ?;") - .unwrap(); - stmt.execute(params![hide, episode_id]).unwrap(); + let mut stmt = conn.prepare_cached("UPDATE episodes SET hidden = ? WHERE id = ?;")?; + stmt.execute(params![hide, episode_id])?; + return Ok(()); } /// 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_cached("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(); - let mut podcasts = Vec::new(); - for pc in podcast_iter { - podcasts.push(pc.unwrap()); - } - podcasts.sort_unstable(); - - return podcasts; - } else { - return Vec::new(); + pub fn get_podcasts(&self) -> Result> { + let conn = self.conn.as_ref().expect("Error connecting to database."); + let mut stmt = conn.prepare_cached("SELECT * FROM podcasts;")?; + let podcast_iter = stmt.query_map(params![], |row| { + let pod_id = row.get("id")?; + let episodes = match self.get_episodes(pod_id) { + Ok(ep_list) => Ok(ep_list), + Err(_) => Err(rusqlite::Error::QueryReturnedNoRows), + }?; + + // 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 podcasts = Vec::new(); + for pc in podcast_iter { + podcasts.push(pc?); } + podcasts.sort_unstable(); + + return Ok(podcasts); } /// 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_cached( - "SELECT * FROM episodes + pub fn get_episodes(&self, pod_id: i64) -> Result> { + let conn = self.conn.as_ref().expect("Error connecting to database."); + let mut stmt = conn.prepare_cached( + "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")?, - }) - }) - .unwrap(); - let mut episodes = Vec::new(); - for ep in episode_iter { - episodes.push(ep.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")?, + }) + })?; + let mut episodes = Vec::new(); + for ep in episode_iter { + if let Ok(ep) = ep { + episodes.push(ep); } - return episodes; - } else { - return Vec::new(); } + return Ok(episodes); } /// Deletes all rows in all tables - pub fn clear_db(&self) -> Result<(), rusqlite::Error> { - let conn = self.conn.as_ref().unwrap(); + pub fn clear_db(&self) -> Result<()> { + let conn = self.conn.as_ref().expect("Error connecting to database."); conn.execute("DELETE FROM files;", params![])?; conn.execute("DELETE FROM episodes;", params![])?; conn.execute("DELETE FROM podcasts;", params![])?; - Ok(()) + return Ok(()); } } diff --git a/src/downloads.rs b/src/downloads.rs index ca15a26..cd28fe4 100644 --- a/src/downloads.rs +++ b/src/downloads.rs @@ -45,7 +45,8 @@ pub fn download_list( let dest2 = dest.to_path_buf(); threadpool.execute(move || { let result = download_file(ep, dest2, max_retries); - tx.send(Message::Dl(result)).unwrap(); + tx.send(Message::Dl(result)) + .expect("Thread messaging error"); }); } } diff --git a/src/feeds.rs b/src/feeds.rs index 7651663..da74b5c 100644 --- a/src/feeds.rs +++ b/src/feeds.rs @@ -1,3 +1,4 @@ +use anyhow::{anyhow, Result}; use std::io::Read; use std::sync::mpsc; @@ -13,7 +14,7 @@ use crate::types::*; lazy_static! { /// Regex for parsing an episode "duration", which could take the form /// of HH:MM:SS, MM:SS, or SS. - static ref RE_DURATION: Regex = Regex::new(r"(\d+)(?::(\d+))?(?::(\d+))?").unwrap(); + static ref RE_DURATION: Regex = Regex::new(r"(\d+)(?::(\d+))?(?::(\d+))?").expect("Regex error"); } /// Enum for communicating back to the main thread after feed data has @@ -56,25 +57,22 @@ pub fn check_feed( Some(id) => { tx_to_main .send(Message::Feed(FeedMsg::SyncData((id, pod)))) - .unwrap(); + .expect("Thread messaging error"); } None => tx_to_main .send(Message::Feed(FeedMsg::NewData(pod))) - .unwrap(), + .expect("Thread messaging error"), }, Err(_err) => tx_to_main .send(Message::Feed(FeedMsg::Error(feed))) - .unwrap(), + .expect("Thread messaging error"), }); } /// 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> { - let request: Result> = loop { +fn get_feed_data(url: String, mut max_retries: usize) -> Result { + let request: Result = loop { let response = ureq::get(&url) .timeout_connect(5000) .timeout_read(15000) @@ -82,7 +80,7 @@ fn get_feed_data( if response.error() { max_retries -= 1; if max_retries == 0 { - break Err(String::from("TODO: Better error handling here.").into()); + break Err(anyhow!("No response from feed")); } } else { break Ok(response); diff --git a/src/main.rs b/src/main.rs index 3460ab6..d6ceba3 100644 --- a/src/main.rs +++ b/src/main.rs @@ -4,6 +4,7 @@ use std::path::{Path, PathBuf}; use std::process; use std::sync::mpsc; +use anyhow::{anyhow, Context, Result}; use clap::{App, Arg, SubCommand}; mod config; @@ -56,7 +57,7 @@ const VERSION: &str = env!("CARGO_PKG_VERSION"); /// Connects to the sqlite database, and reads all podcasts into an OPML /// file, with the location specified from the command line arguments. #[allow(clippy::while_let_on_iterator)] -fn main() { +fn main() -> Result<()> { // SETUP ----------------------------------------------------------- // set up the possible command line arguments and subcommands @@ -111,44 +112,38 @@ fn main() { // config location for OS let config_path = get_config_path(args.value_of("config")) .unwrap_or_else(|| { - println!("Could not identify your operating system's default directory to store configuration files. Please specify paths manually using config.toml and use `-c` or `--config` flag to specify where config.toml is located when launching the program."); + eprintln!("Could not identify your operating system's default directory to store configuration files. Please specify paths manually using config.toml and use `-c` or `--config` flag to specify where config.toml is located when launching the program."); process::exit(1); }); - let config = Config::new(&config_path); + let config = Config::new(&config_path)?; let mut db_path = config_path; if !db_path.pop() { - println!("Could not correctly parse the config file location. Please specify a valid path to the config file."); - process::exit(1); + return Err(anyhow!("Could not correctly parse the config file location. Please specify a valid path to the config file.")); } - match args.subcommand() { + return match args.subcommand() { // SYNC SUBCOMMAND ---------------------------------------------- - ("sync", Some(sub_args)) => { - sync_podcasts(&db_path, config, sub_args); - } + ("sync", Some(sub_args)) => sync_podcasts(&db_path, config, sub_args), // IMPORT SUBCOMMAND -------------------------------------------- - ("import", Some(sub_args)) => { - import(&db_path, config, sub_args); - } + ("import", Some(sub_args)) => import(&db_path, config, sub_args), // EXPORT SUBCOMMAND -------------------------------------------- - ("export", Some(sub_args)) => { - export(&db_path, sub_args); - } + ("export", Some(sub_args)) => export(&db_path, sub_args), // MAIN COMMAND ------------------------------------------------- _ => { - let mut main_ctrl = MainController::new(config, &db_path); + let mut main_ctrl = MainController::new(config, &db_path)?; 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 + Ok(()) } - } + }; } @@ -179,230 +174,223 @@ 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: &Path, config: Config, args: &clap::ArgMatches) { - let db_inst = Database::connect(db_path); - let podcast_list = db_inst.get_podcasts(); +fn sync_podcasts(db_path: &Path, config: Config, args: &clap::ArgMatches) -> Result<()> { + let db_inst = Database::connect(db_path)?; + let podcast_list = db_inst.get_podcasts()?; if podcast_list.is_empty() { if !args.is_present("quiet") { println!("No podcasts to sync."); } - } else { - let threadpool = Threadpool::new(config.simultaneous_downloads); - let (tx_to_main, rx_to_main) = mpsc::channel(); + return Ok(()); + } - 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()); - } + let threadpool = Threadpool::new(config.simultaneous_downloads); + let (tx_to_main, rx_to_main) = mpsc::channel(); - let mut msg_counter: usize = 0; - let mut failure = false; - while let Some(message) = rx_to_main.iter().next() { - match message { - 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); + 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()); + } + + let mut msg_counter: usize = 0; + let mut failure = false; + while let Some(message) = rx_to_main.iter().next() { + match message { + 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); } } - } - - Message::Feed(FeedMsg::Error(feed)) => { - failure = true; - match feed.title { - Some(t) => eprintln!("Error retrieving RSS feed for {}.", t), - None => eprintln!("Error retrieving RSS feed."), + Err(_err) => { + failure = true; + eprintln!("Error synchronizing {}", title); } } - _ => (), } - msg_counter += 1; - if msg_counter >= podcast_list.len() { - break; + Message::Feed(FeedMsg::Error(feed)) => { + failure = true; + match feed.title { + Some(t) => eprintln!("Error retrieving RSS feed for {}.", t), + None => eprintln!("Error retrieving RSS feed."), + } } + _ => (), } - if failure { - eprintln!("Process finished with errors."); - process::exit(2); - } else if !args.is_present("quiet") { - println!("Sync successful."); + msg_counter += 1; + if msg_counter >= podcast_list.len() { + break; } } + + if failure { + return Err(anyhow!("Process finished with errors.")); + } else if !args.is_present("quiet") { + println!("Sync successful."); + } + return Ok(()); } /// 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. -fn import(db_path: &Path, config: Config, args: &clap::ArgMatches) { +fn import(db_path: &Path, config: Config, args: &clap::ArgMatches) -> Result<()> { // read from file or from stdin let xml = match args.value_of("file") { Some(filepath) => { - let mut f = File::open(filepath).unwrap_or_else(|err| { - eprintln!("Error opening OPML file: {}", err); - process::exit(4); - }); + let mut f = File::open(filepath) + .with_context(|| format!("Could not open OPML file: {}", filepath))?; let mut contents = String::new(); - f.read_to_string(&mut contents).unwrap_or_else(|err| { - eprintln!("Error reading from OPML file: {}", err); - process::exit(4); - }); + f.read_to_string(&mut contents) + .with_context(|| format!("Failed to read from OPML file: {}", filepath))?; contents } 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); - }); + .with_context(|| "Failed to read OPML file from stdin")?; contents } }; - let mut podcast_list = opml::import(xml).unwrap_or_else(|err| { - eprintln!("Error parsing OPML file: {}", err); - process::exit(5); - }); + let mut podcast_list = opml::import(xml).with_context(|| { + "Could not properly parse OPML file -- file may be formatted improperly or corrupted." + })?; if podcast_list.is_empty() { if !args.is_present("quiet") { println!("No podcasts to import."); } + return Ok(()); + } + + let db_inst = Database::connect(db_path)?; + + // delete database if we are replacing the data + if args.is_present("replace") { + db_inst + .clear_db() + .with_context(|| "Error clearing database")?; } else { - let db_inst = Database::connect(db_path); - - // delete database if we are replacing the data - if args.is_present("replace") { - db_inst.clear_db().unwrap_or_else(|err| { - eprintln!("Error clearing database: {}", err); - process::exit(4); - }); - } else { - 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; - } + 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; } - return true; - }) - .collect(); + } + return true; + }) + .collect(); + } + + // check again, now that we may have removed feeds after looking at + // the database + if podcast_list.is_empty() { + if !args.is_present("quiet") { + println!("No podcasts to import."); } + return Ok(()); + } - if podcast_list.is_empty() { - if !args.is_present("quiet") { - println!("No podcasts to import."); - } - } else { - println!("Importing {} podcasts...", podcast_list.len()); - - let threadpool = Threadpool::new(config.simultaneous_downloads); - 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(), - ); - } + println!("Importing {} podcasts...", podcast_list.len()); - let mut msg_counter: usize = 0; - let mut failure = false; - while let Some(message) = rx_to_main.iter().next() { - match message { - 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); - } + let threadpool = Threadpool::new(config.simultaneous_downloads); + 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(), + ); + } + + let mut msg_counter: usize = 0; + let mut failure = false; + while let Some(message) = rx_to_main.iter().next() { + match message { + 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); } } - - Message::Feed(FeedMsg::Error(feed)) => { + Err(_err) => { failure = true; - if let Some(t) = feed.title { - eprintln!("Error retrieving RSS feed: {}", t); - } else { - eprintln!("Error retrieving RSS feed"); - } + eprintln!("Error adding {}", title); } - _ => (), } + } - msg_counter += 1; - if msg_counter >= podcast_list.len() { - break; + Message::Feed(FeedMsg::Error(feed)) => { + failure = true; + if let Some(t) = feed.title { + eprintln!("Error retrieving RSS feed: {}", t); + } else { + eprintln!("Error retrieving RSS feed"); } } + _ => (), + } - if failure { - eprintln!("Process finished with errors."); - process::exit(2); - } else if !args.is_present("quiet") { - println!("Import successful."); - } + msg_counter += 1; + if msg_counter >= podcast_list.len() { + break; } } + + if failure { + return Err(anyhow!("Process finished with errors.")); + } else if !args.is_present("quiet") { + println!("Import successful."); + } + return Ok(()); } /// Exports all podcasts to OPML format, either printing to stdout or /// exporting to a file. -fn export(db_path: &Path, args: &clap::ArgMatches) { - let db_inst = Database::connect(&db_path); - let podcast_list = db_inst.get_podcasts(); +fn export(db_path: &Path, args: &clap::ArgMatches) -> Result<()> { + let db_inst = Database::connect(&db_path)?; + let podcast_list = db_inst.get_podcasts()?; let opml = opml::export(podcast_list); - let xml = opml.to_xml().unwrap_or_else(|err| { - eprintln!("Error creating OPML format: {}", err); - process::exit(3); - }); + let xml = opml + .to_xml() + .map_err(|err| anyhow!(err)) + .with_context(|| "Could not create OPML format")?; match args.value_of("file") { // export to file Some(file) => { - let mut dst = File::create(file).unwrap_or_else(|err| { - eprintln!("Error creating output file: {}", err); - process::exit(4); - }); - dst.write_all(xml.as_bytes()).unwrap_or_else(|err| { - eprintln!("Error copying OPML data to output file: {}", err); - process::exit(4); - }); + let mut dst = File::create(file) + .with_context(|| format!("Could not create output file: {}", file))?; + dst.write_all(xml.as_bytes()) + .with_context(|| format!("Could not copy OPML data to output file: {}", file))?; } // print to stdout None => println!("{}", xml), } + return Ok(()); } diff --git a/src/main_controller.rs b/src/main_controller.rs index 443e741..89aef6e 100644 --- a/src/main_controller.rs +++ b/src/main_controller.rs @@ -1,3 +1,4 @@ +use anyhow::Result; use std::collections::HashSet; use std::fs; use std::path::{Path, PathBuf}; @@ -45,13 +46,13 @@ impl MainController { /// Instantiates the main controller (used during app startup), which /// sets up the connection to the database, download manager, and UI /// thread, and reads the list of podcasts from the database. - pub fn new(config: Config, db_path: &Path) -> MainController { + pub fn new(config: Config, db_path: &Path) -> Result { // 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); + let db_inst = Database::connect(&db_path)?; // set up threadpool let threadpool = Threadpool::new(config.simultaneous_downloads); @@ -61,7 +62,7 @@ impl MainController { // "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()); + 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); @@ -73,7 +74,7 @@ impl MainController { ); // TODO: Can we do this without cloning the config? - return MainController { + return Ok(MainController { config: config, db: db_inst, threadpool: threadpool, @@ -85,7 +86,7 @@ impl MainController { tx_to_ui: tx_to_ui, tx_to_main: tx_to_main, rx_to_main: rx_to_main, - }; + }); } /// Initiates the main loop where the controller waits for messages coming in from the UI and other threads, and processes them. @@ -173,7 +174,7 @@ impl MainController { error, crate::config::MESSAGE_TIME, )) - .unwrap(); + .expect("Thread messaging error"); } /// Sends a persistent notification to the UI, which will display at @@ -181,14 +182,14 @@ impl MainController { pub fn persistent_notif_to_ui(&self, message: String, error: bool) { self.tx_to_ui .send(MainMessage::UiSpawnPersistentNotif(message, error)) - .unwrap(); + .expect("Thread messaging error"); } /// Clears persistent notifications in the UI. pub fn clear_persistent_notif(&self) { self.tx_to_ui .send(MainMessage::UiClearPersistentNotif) - .unwrap(); + .expect("Thread messaging error"); } /// Updates the persistent notification about syncing podcasts and @@ -283,9 +284,15 @@ impl MainController { match db_result { Ok(result) => { { - self.podcasts.replace_all(self.db.get_podcasts()); + self.podcasts.replace_all( + self.db + .get_podcasts() + .expect("Error retrieving info from database."), + ); } - self.tx_to_ui.send(MainMessage::UiUpdateMenus).unwrap(); + self.tx_to_ui + .send(MainMessage::UiUpdateMenus) + .expect("Thread messaging error"); if pod_id.is_some() { self.sync_tracker.push(result); @@ -324,12 +331,12 @@ impl MainController { DownloadNewEpisodes::AskSelected => { self.tx_to_ui .send(MainMessage::UiSpawnDownloadPopup(new_eps, true)) - .unwrap(); + .expect("Thread messaging error"); } DownloadNewEpisodes::AskUnselected => { self.tx_to_ui .send(MainMessage::UiSpawnDownloadPopup(new_eps, false)) - .unwrap(); + .expect("Thread messaging error"); } _ => (), } @@ -385,11 +392,13 @@ impl MainController { let mut episode = podcast.episodes.clone_episode(ep_id).unwrap(); episode.played = played; - self.db.set_played_status(episode.id, played); + let _ = self.db.set_played_status(episode.id, played); podcast.episodes.replace(ep_id, episode); self.podcasts.replace(pod_id, podcast); - self.tx_to_ui.send(MainMessage::UiUpdateMenus).unwrap(); + self.tx_to_ui + .send(MainMessage::UiUpdateMenus) + .expect("Thread messaging error"); } /// Given a podcast, it marks all episodes for that podcast as @@ -400,15 +409,19 @@ impl MainController { { let borrowed_ep_list = podcast.episodes.borrow_order(); for ep in borrowed_ep_list.iter() { - self.db.set_played_status(*ep, played); + let _ = self.db.set_played_status(*ep, played); } } - podcast - .episodes - .replace_all(self.db.get_episodes(podcast.id)); + podcast.episodes.replace_all( + self.db + .get_episodes(podcast.id) + .expect("Error retrieving info from database."), + ); self.podcasts.replace(pod_id, podcast); - self.tx_to_ui.send(MainMessage::UiUpdateMenus).unwrap(); + self.tx_to_ui + .send(MainMessage::UiUpdateMenus) + .expect("Thread messaging error"); } /// Given a podcast index (and not an episode index), this will send @@ -499,7 +512,17 @@ impl MainController { /// Handles logic for what to do when a download successfully completes. pub fn download_complete(&mut self, ep_data: EpData) { let file_path = ep_data.file_path.unwrap(); - let _ = self.db.insert_file(ep_data.id, &file_path); + let res = self.db.insert_file(ep_data.id, &file_path); + if res.is_err() { + self.notif_to_ui( + format!( + "Could not add episode file to database: {}", + file_path.to_string_lossy() + ), + true, + ); + return; + } { // TODO: Try to do this without cloning the podcast... let podcast = self.podcasts.clone_podcast(ep_data.pod_id).unwrap(); @@ -514,7 +537,9 @@ impl MainController { self.notif_to_ui("Downloads complete.".to_string(), false); } - self.tx_to_ui.send(MainMessage::UiUpdateMenus).unwrap(); + self.tx_to_ui + .send(MainMessage::UiUpdateMenus) + .expect("Thread messaging error"); } /// Given a podcast title, creates a download directory for that @@ -539,11 +564,20 @@ impl MainController { let title = episode.title.clone(); match fs::remove_file(episode.path.unwrap()) { Ok(_) => { - self.db.remove_file(episode.id); + let res = self.db.remove_file(episode.id); + if res.is_err() { + self.notif_to_ui( + format!("Could not remove file from database: {}", title), + true, + ); + return; + } episode.path = None; podcast.episodes.replace(ep_id, episode); - self.tx_to_ui.send(MainMessage::UiUpdateMenus).unwrap(); + self.tx_to_ui + .send(MainMessage::UiUpdateMenus) + .expect("Thread messaging error"); self.notif_to_ui(format!("Deleted \"{}\"", title), false); } Err(_) => self.notif_to_ui(format!("Error deleting \"{}\"", title), true), @@ -576,8 +610,13 @@ impl MainController { } } - self.db.remove_files(&eps_to_remove); - self.tx_to_ui.send(MainMessage::UiUpdateMenus).unwrap(); + let res = self.db.remove_files(&eps_to_remove); + if res.is_err() { + success = false; + } + self.tx_to_ui + .send(MainMessage::UiUpdateMenus) + .expect("Thread messaging error"); if success { self.notif_to_ui("Files successfully deleted.".to_string(), false); @@ -594,11 +633,21 @@ impl MainController { } let pod_id = self.podcasts.map_single(pod_id, |pod| pod.id).unwrap(); - self.db.remove_podcast(pod_id); + let res = self.db.remove_podcast(pod_id); + if res.is_err() { + self.notif_to_ui("Could not remove podcast from database".to_string(), true); + return; + } { - self.podcasts.replace_all(self.db.get_podcasts()); + self.podcasts.replace_all( + self.db + .get_podcasts() + .expect("Error retrieving info from database."), + ); } - self.tx_to_ui.send(MainMessage::UiUpdateMenus).unwrap(); + self.tx_to_ui + .send(MainMessage::UiUpdateMenus) + .expect("Thread messaging error"); } /// Removes an episode from the list, optionally deleting local files @@ -608,13 +657,19 @@ impl MainController { self.delete_file(pod_id, ep_id); } - self.db.hide_episode(ep_id, true); + let _ = self.db.hide_episode(ep_id, true); { let mut borrowed_map = self.podcasts.borrow_map(); let podcast = borrowed_map.get_mut(&pod_id).unwrap(); - podcast.episodes.replace_all(self.db.get_episodes(pod_id)); + podcast.episodes.replace_all( + self.db + .get_episodes(pod_id) + .expect("Error retrieving info from database."), + ); } - self.tx_to_ui.send(MainMessage::UiUpdateMenus).unwrap(); + self.tx_to_ui + .send(MainMessage::UiUpdateMenus) + .expect("Thread messaging error"); } /// Removes all episodes for a podcast from the list, optionally @@ -626,11 +681,13 @@ impl MainController { let mut podcast = self.podcasts.clone_podcast(pod_id).unwrap(); podcast.episodes.map(|ep| { - self.db.hide_episode(ep.id, true); + let _ = self.db.hide_episode(ep.id, true); }); podcast.episodes = LockVec::new(Vec::new()); self.podcasts.replace(pod_id, podcast); - self.tx_to_ui.send(MainMessage::UiUpdateMenus).unwrap(); + self.tx_to_ui + .send(MainMessage::UiUpdateMenus) + .expect("Thread messaging error"); } } diff --git a/src/opml.rs b/src/opml.rs index c5c721c..12fce85 100644 --- a/src/opml.rs +++ b/src/opml.rs @@ -1,3 +1,4 @@ +use anyhow::{anyhow, Result}; use chrono::Utc; use opml::{Body, Head, Outline, OPML}; @@ -6,9 +7,9 @@ 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> { +pub fn import(xml: String) -> Result> { return match OPML::new(&xml) { - Err(err) => Err(err), + Err(err) => Err(anyhow!(err)), Ok(opml) => { let mut feeds = Vec::new(); for pod in opml.body.outlines.iter() { diff --git a/src/play_file.rs b/src/play_file.rs index 97fc27d..ca51a2d 100644 --- a/src/play_file.rs +++ b/src/play_file.rs @@ -1,13 +1,14 @@ +use anyhow::{anyhow, Result}; use std::process::{Command, Stdio}; /// Execute an external shell command to play an episode file and/or URL. -pub fn execute(command: &str, path: &str) -> Result<(), std::io::Error> { +pub fn execute(command: &str, path: &str) -> Result<()> { // Command expects a command and then optional arguments (giving // everything to it in a string doesn't work), so we need to split // on white space and treat everything after the first word as args let cmd_string = String::from(command); let mut parts = cmd_string.trim().split_whitespace(); - let base_cmd = parts.next().unwrap(); + let base_cmd = parts.next().ok_or_else(|| anyhow!("Invalid command."))?; let args_iter = parts; let mut args: Vec; @@ -30,6 +31,6 @@ pub fn execute(command: &str, path: &str) -> Result<(), std::io::Error> { cmd.args(args).stdout(Stdio::null()).stderr(Stdio::null()); match cmd.spawn() { Ok(_) => Ok(()), - Err(err) => Err(err), + Err(err) => Err(anyhow!(err)), } } diff --git a/src/sanitizer.rs b/src/sanitizer.rs index 5546278..e30ed12 100644 --- a/src/sanitizer.rs +++ b/src/sanitizer.rs @@ -28,10 +28,10 @@ pub fn sanitize_rfc822_like_date>(s: S) -> String { fn pad_zeros(s: String) -> String { lazy_static! { /// If it matchers a pattern of 2:2:2, return. - static ref OK_RGX: Regex = Regex::new(r"(\d{2}):(\d{2}):(\d{2})").unwrap(); + static ref OK_RGX: Regex = Regex::new(r"(\d{2}):(\d{2}):(\d{2})").expect("Regex error"); /// hours, minutes, seconds = cap[1], cap[2], cap[3] - static ref RE_RGX: Regex = Regex::new(r"(\d{1,2}):(\d{1,2}):(\d{1,2})").unwrap(); + static ref RE_RGX: Regex = Regex::new(r"(\d{1,2}):(\d{1,2}):(\d{1,2})").expect("Regex error"); } if OK_RGX.is_match(&s) { diff --git a/src/threadpool.rs b/src/threadpool.rs index d16718e..b833025 100644 --- a/src/threadpool.rs +++ b/src/threadpool.rs @@ -36,7 +36,9 @@ impl Threadpool { pub fn execute(&self, func: F) where F: FnOnce() + Send + 'static { let job = Box::new(func); - self.sender.send(JobMessage::NewJob(job)).unwrap(); + self.sender + .send(JobMessage::NewJob(job)) + .expect("Thread messaging error"); } } @@ -45,13 +47,15 @@ impl Drop for Threadpool { /// all workers but allows them to complete current jobs. fn drop(&mut self) { for _ in &self.workers { - self.sender.send(JobMessage::Terminate).unwrap(); + self.sender + .send(JobMessage::Terminate) + .expect("Thread messaging error"); } for worker in &mut self.workers { if let Some(thread) = worker.thread.take() { // joins to ensure threads finish job before stopping - thread.join().unwrap(); + thread.join().expect("Error dropping threads"); } } } @@ -76,7 +80,11 @@ impl Worker { /// Threadpool. fn new(receiver: Arc>>) -> Worker { let thread = thread::spawn(move || loop { - let message = receiver.lock().unwrap().recv().unwrap(); + let message = receiver + .lock() + .expect("Threadpool error") + .recv() + .expect("Thread messaging error"); match message { JobMessage::NewJob(job) => job(), diff --git a/src/types.rs b/src/types.rs index 0b28387..15e47ee 100644 --- a/src/types.rs +++ b/src/types.rs @@ -16,7 +16,7 @@ use crate::ui::UiMsg; lazy_static! { /// Regex for removing "A", "An", and "The" from the beginning of /// podcast titles - static ref RE_ARTICLES: Regex = Regex::new(r"^(a|an|the) ").unwrap(); + static ref RE_ARTICLES: Regex = Regex::new(r"^(a|an|the) ").expect("Regex error"); } /// Defines interface used for both podcasts and episodes, to be @@ -297,12 +297,12 @@ impl LockVec { /// Lock the LockVec hashmap for reading/writing. pub fn borrow_map(&self) -> MutexGuard>> { - return self.data.lock().unwrap(); + return self.data.lock().expect("Mutex error"); } /// Lock the LockVec order vector for reading/writing. pub fn borrow_order(&self) -> MutexGuard> { - return self.order.lock().unwrap(); + return self.order.lock().expect("Mutex error"); } /// Lock the LockVec hashmap for reading/writing. @@ -313,7 +313,10 @@ impl LockVec { MutexGuard>>, MutexGuard>, ) { - return (self.data.lock().unwrap(), self.order.lock().unwrap()); + return ( + self.data.lock().expect("Mutex error"), + self.order.lock().expect("Mutex error"), + ); } /// Given an id, this takes a new T and replaces the old T with that @@ -342,7 +345,10 @@ impl LockVec { pub fn map(&self, mut f: F) -> Vec 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).expect("Index error in LockVec"))) + .collect(); } /// Maps a closure to a single element in the LockVec, specified by @@ -380,7 +386,11 @@ impl LockVec { let (map, order) = self.borrow(); return (start..end) .into_iter() - .filter_map(|id| f(map.get(order.get(id).unwrap()).unwrap())) + .filter_map(|id| { + f(map + .get(order.get(id).expect("Index error in LockVec")) + .expect("Index error in LockVec")) + }) .collect(); } @@ -394,7 +404,7 @@ impl LockVec { let (map, order) = self.borrow(); return order .iter() - .filter_map(|id| f(map.get(id).unwrap())) + .filter_map(|id| f(map.get(id).expect("Index error in LockVec"))) .collect(); } diff --git a/src/ui/colors.rs b/src/ui/colors.rs index a095a23..225c424 100644 --- a/src/ui/colors.rs +++ b/src/ui/colors.rs @@ -28,7 +28,7 @@ impl Colors { } pub fn get(&self, color: ColorType) -> i16 { - return *self.map.get(&color).unwrap(); + return *self.map.get(&color).expect("Error retrieving color type."); } } diff --git a/src/ui/menu.rs b/src/ui/menu.rs index adff299..c7280ec 100644 --- a/src/ui/menu.rs +++ b/src/ui/menu.rs @@ -77,7 +77,7 @@ impl Menu { // for visible rows, print strings from list 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(); + let elem = map.get(&elem_id).expect("Could not retrieve menu item."); self.panel .write_line(i, elem.get_title(self.panel.get_cols() as usize)); @@ -157,7 +157,7 @@ impl Menu { let played = menu .items .map_single_by_index(menu.get_menu_idx(selected), |el| el.is_played()) - .unwrap(); + .unwrap_or(false); menu.set_attrs(selected, played, color); }; @@ -284,12 +284,13 @@ impl Menu { /// currently selected podcast. pub fn get_episodes(&self) -> LockVec { let index = self.get_menu_idx(self.selected); - let pod_id = self.items.borrow_order().get(index).copied().unwrap(); - return self - .items - .borrow_map() - .get(&pod_id) - .unwrap() + let (borrowed_map, borrowed_order) = self.items.borrow(); + let pod_id = borrowed_order + .get(index) + .expect("Could not retrieve podcast."); + return borrowed_map + .get(pod_id) + .expect("Could not retrieve podcast info.") .episodes .clone(); } diff --git a/src/ui/mod.rs b/src/ui/mod.rs index c9c3c2e..0b4b7eb 100644 --- a/src/ui/mod.rs +++ b/src/ui/mod.rs @@ -29,13 +29,13 @@ use crate::types::*; lazy_static! { /// Regex for finding
tags -- also captures any surrounding /// line breaks - static ref RE_BR_TAGS: Regex = Regex::new(r"((\r\n)|\r|\n)*
((\r\n)|\r|\n)*").unwrap(); + static ref RE_BR_TAGS: Regex = Regex::new(r"((\r\n)|\r|\n)*
((\r\n)|\r|\n)*").expect("Regex error"); /// Regex for finding HTML tags - static ref RE_HTML_TAGS: Regex = Regex::new(r"<[^<>]*>").unwrap(); + static ref RE_HTML_TAGS: Regex = Regex::new(r"<[^<>]*>").expect("Regex error"); /// Regex for finding more than two line breaks - static ref RE_MULT_LINE_BREAKS: Regex = Regex::new(r"((\r\n)|\r|\n){3,}").unwrap(); + static ref RE_MULT_LINE_BREAKS: Regex = Regex::new(r"((\r\n)|\r|\n){3,}").expect("Regex error"); } @@ -110,7 +110,9 @@ impl<'a> Ui<'a> { match ui.getch() { UiMsg::Noop => (), - input => tx_to_main.send(Message::Ui(input)).unwrap(), + input => tx_to_main + .send(Message::Ui(input)) + .expect("Thread messaging error"), } if let Some(message) = message_iter.next() { @@ -522,8 +524,8 @@ impl<'a> Ui<'a> { } // this shouldn't occur because we only trigger this - // function when the UserAction is Up, Down, Left, Right, BigUp, BigDown, - // PageUp, PageDown, GoBot and GoTop + // function when the UserAction is Up, Down, Left, Right, + // BigUp, BigDown, PageUp, PageDown, GoBot and GoTop _ => (), } } @@ -635,7 +637,7 @@ impl<'a> Ui<'a> { .episode_menu .items .map_single(ep_id, |ep| ep.path.is_some()) - .unwrap(); + .unwrap_or(false); if is_downloaded { let ask_delete = self.spawn_yes_no_notif("Delete local file too?"); delete = ask_delete.unwrap_or(false); // default not to delete @@ -712,7 +714,9 @@ impl<'a> Ui<'a> { 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_pod = borrowed_map + .get(&pod_id) + .expect("Could not retrieve podcast info."); let borrowed_ep_list = borrowed_pod.episodes.borrow_map(); diff --git a/src/ui/notification.rs b/src/ui/notification.rs index a256da4..6105c82 100644 --- a/src/ui/notification.rs +++ b/src/ui/notification.rs @@ -67,7 +67,10 @@ impl NotifWin { // compare expiry times of all notifications to current time, // remove expired ones let now = Instant::now(); - self.msg_stack.retain(|x| now < x.expiry.unwrap()); + self.msg_stack.retain(|x| match x.expiry { + Some(exp) => now < exp, + None => true, + }); if !self.msg_stack.is_empty() { // check if last item changed, and update screen if it has diff --git a/src/ui/popup.rs b/src/ui/popup.rs index 7807693..fd612f2 100644 --- a/src/ui/popup.rs +++ b/src/ui/popup.rs @@ -229,7 +229,11 @@ impl<'a> PopupWin<'a> { // 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 longest_line = key_strs + .iter() + .map(|x| x.chars().count()) + .max() + .expect("Could not parse keybindings."); let col_spacing = 5; let n_cols = if help_win.get_cols() > (longest_line * 2 + col_spacing) as i32 { 2 From 151fd484afe0258fe755083a127eb1a28b996595 Mon Sep 17 00:00:00 2001 From: Jeff Hughes Date: Mon, 3 May 2021 19:24:20 -0400 Subject: [PATCH 11/23] Add timestamp to filenames of downloaded files --- src/downloads.rs | 8 +++++++- src/main_controller.rs | 2 ++ 2 files changed, 9 insertions(+), 1 deletion(-) diff --git a/src/downloads.rs b/src/downloads.rs index cd28fe4..2328bb4 100644 --- a/src/downloads.rs +++ b/src/downloads.rs @@ -2,6 +2,7 @@ use std::fs::File; use std::path::{Path, PathBuf}; use std::sync::mpsc::Sender; +use chrono::{DateTime, Utc}; use sanitize_filename::{sanitize_with_options, Options}; use crate::threadpool::Threadpool; @@ -25,6 +26,7 @@ pub struct EpData { pub pod_id: i64, pub title: String, pub url: String, + pub pubdate: Option>, pub file_path: Option, } @@ -87,12 +89,16 @@ 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 { + let mut 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: "", }); + if let Some(pubdate) = ep_data.pubdate { + file_name = format!("{}_{}", file_name, pubdate.format("%Y%m%d_%H%M%S")); + } + let mut file_path = dest; file_path.push(format!("{}.{}", file_name, ext)); diff --git a/src/main_controller.rs b/src/main_controller.rs index 89aef6e..34bf88b 100644 --- a/src/main_controller.rs +++ b/src/main_controller.rs @@ -450,6 +450,7 @@ impl MainController { pod_id: ep.pod_id, title: ep.title.clone(), url: ep.url.clone(), + pubdate: ep.pubdate, file_path: None, }, ep.path.is_none(), @@ -469,6 +470,7 @@ impl MainController { pod_id: ep.pod_id, title: ep.title.clone(), url: ep.url.clone(), + pubdate: ep.pubdate, file_path: None, }) } else { From 424ca0336cab2c1dcae0bbc261b814530e9d045b Mon Sep 17 00:00:00 2001 From: Jeff Hughes Date: Tue, 4 May 2021 09:46:47 -0400 Subject: [PATCH 12/23] Fix issue with removed episodes reappearing after sync --- src/db.rs | 29 +++++++++++++++++++---------- src/main_controller.rs | 4 ++-- 2 files changed, 21 insertions(+), 12 deletions(-) diff --git a/src/db.rs b/src/db.rs index a5075ae..8280872 100644 --- a/src/db.rs +++ b/src/db.rs @@ -316,7 +316,7 @@ impl Database { ) -> Result { let conn = self.conn.as_ref().expect("Error connecting to database."); - let old_episodes = self.get_episodes(podcast_id)?; + let old_episodes = self.get_episodes(podcast_id, true)?; let mut insert_ep = Vec::new(); let mut update_ep = Vec::new(); @@ -426,7 +426,7 @@ impl Database { let mut stmt = conn.prepare_cached("SELECT * FROM podcasts;")?; let podcast_iter = stmt.query_map(params![], |row| { let pod_id = row.get("id")?; - let episodes = match self.get_episodes(pod_id) { + let episodes = match self.get_episodes(pod_id, false) { Ok(ep_list) => Ok(ep_list), Err(_) => Err(rusqlite::Error::QueryReturnedNoRows), }?; @@ -459,15 +459,24 @@ impl Database { } /// Generates list of episodes for a given podcast. - pub fn get_episodes(&self, pod_id: i64) -> Result> { + pub fn get_episodes(&self, pod_id: i64, include_hidden: bool) -> Result> { let conn = self.conn.as_ref().expect("Error connecting to database."); - let mut stmt = conn.prepare_cached( - "SELECT * FROM episodes - LEFT JOIN files ON episodes.id = files.episode_id - WHERE episodes.podcast_id = ? - AND episodes.hidden = 0 - ORDER BY pubdate DESC;", - )?; + let mut stmt = if include_hidden { + conn.prepare_cached( + "SELECT * FROM episodes + LEFT JOIN files ON episodes.id = files.episode_id + WHERE episodes.podcast_id = ? + ORDER BY pubdate DESC;", + )? + } else { + conn.prepare_cached( + "SELECT * FROM episodes + LEFT JOIN files ON episodes.id = files.episode_id + WHERE episodes.podcast_id = ? + AND episodes.hidden = 0 + ORDER BY pubdate DESC;", + )? + }; let episode_iter = stmt.query_map(params![pod_id], |row| { let path = match row.get::<&str, String>("path") { Ok(val) => Some(PathBuf::from(val)), diff --git a/src/main_controller.rs b/src/main_controller.rs index 34bf88b..96de19e 100644 --- a/src/main_controller.rs +++ b/src/main_controller.rs @@ -414,7 +414,7 @@ impl MainController { } podcast.episodes.replace_all( self.db - .get_episodes(podcast.id) + .get_episodes(podcast.id, false) .expect("Error retrieving info from database."), ); @@ -665,7 +665,7 @@ impl MainController { let podcast = borrowed_map.get_mut(&pod_id).unwrap(); podcast.episodes.replace_all( self.db - .get_episodes(pod_id) + .get_episodes(pod_id, false) .expect("Error retrieving info from database."), ); } From e36eebaa1bc0c4fc77f64ecadbfed7961ce29daa Mon Sep 17 00:00:00 2001 From: Jeff Hughes Date: Fri, 7 May 2021 19:42:51 -0400 Subject: [PATCH 13/23] Move colors to lazy static reference --- src/ui/menu.rs | 10 +------- src/ui/mock_panel.rs | 5 +--- src/ui/mod.rs | 52 ++++++++---------------------------------- src/ui/notification.rs | 25 +++++++------------- src/ui/panel.rs | 17 +++++--------- src/ui/popup.rs | 9 ++------ 6 files changed, 27 insertions(+), 91 deletions(-) diff --git a/src/ui/menu.rs b/src/ui/menu.rs index c7280ec..cd586ce 100644 --- a/src/ui/menu.rs +++ b/src/ui/menu.rs @@ -409,15 +409,7 @@ mod tests { }); } - let panel = Panel::new( - crate::ui::colors::set_colors(), - "Episodes".to_string(), - 1, - n_row, - n_col, - 0, - 0, - ); + let panel = Panel::new("Episodes".to_string(), 1, n_row, n_col, 0, 0); return Menu { panel: panel, header: None, diff --git a/src/ui/mock_panel.rs b/src/ui/mock_panel.rs index 3d65e60..d30ae8f 100644 --- a/src/ui/mock_panel.rs +++ b/src/ui/mock_panel.rs @@ -1,4 +1,4 @@ -use super::{ColorType, Colors}; +use super::ColorType; use chrono::{DateTime, Utc}; /// Struct holding the raw data used for building the details panel. @@ -15,7 +15,6 @@ pub struct Details { pub struct Panel { pub window: Vec<(String, pancurses::chtype, ColorType)>, pub screen_pos: usize, - pub colors: Colors, pub title: String, pub n_row: i32, pub n_col: i32, @@ -23,7 +22,6 @@ pub struct Panel { impl Panel { pub fn new( - colors: Colors, title: String, screen_pos: usize, n_row: i32, @@ -39,7 +37,6 @@ impl Panel { return Panel { window: panel_win, screen_pos: screen_pos, - colors: colors, title: title, n_row: n_row, n_col: n_col, diff --git a/src/ui/mod.rs b/src/ui/mod.rs index 0b4b7eb..76f9348 100644 --- a/src/ui/mod.rs +++ b/src/ui/mod.rs @@ -36,6 +36,8 @@ lazy_static! { /// Regex for finding more than two line breaks static ref RE_MULT_LINE_BREAKS: Regex = Regex::new(r"((\r\n)|\r|\n){3,}").expect("Regex error"); + + pub static ref COLORS: Colors = self::colors::set_colors(); } @@ -80,7 +82,6 @@ pub struct Ui<'a> { n_row: i32, n_col: i32, keymap: &'a Keybindings, - colors: Colors, podcast_menu: Menu, episode_menu: Menu, active_menu: ActiveMenu, @@ -157,32 +158,14 @@ impl<'a> Ui<'a> { // key codes stdscr.nodelay(true); // getch() will not wait for user input - // set colors - let colors = self::colors::set_colors(); - let (n_row, n_col) = stdscr.get_max_yx(); let (pod_col, ep_col, det_col) = Self::calculate_sizes(n_col); - let podcast_panel = Panel::new( - colors.clone(), - "Podcasts".to_string(), - 0, - n_row - 1, - pod_col, - 0, - 0, - ); + let podcast_panel = Panel::new("Podcasts".to_string(), 0, n_row - 1, pod_col, 0, 0); let podcast_menu = Menu::new(podcast_panel, None, items.clone()); - let episode_panel = Panel::new( - colors.clone(), - "Episodes".to_string(), - 1, - n_row - 1, - ep_col, - 0, - pod_col - 1, - ); + let episode_panel = + Panel::new("Episodes".to_string(), 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) { @@ -196,7 +179,6 @@ 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, @@ -206,15 +188,14 @@ impl<'a> Ui<'a> { None }; - let notif_win = NotifWin::new(colors.clone(), n_row, n_col); - let popup_win = PopupWin::new(colors.clone(), &config.keybindings, n_row, n_col); + let notif_win = NotifWin::new(n_row, n_col); + let popup_win = PopupWin::new(&config.keybindings, n_row, n_col); return Ui { stdscr, n_row, n_col, keymap: &config.keybindings, - colors: colors, podcast_menu: podcast_menu, episode_menu: episode_menu, active_menu: ActiveMenu::PodcastMenu, @@ -417,7 +398,6 @@ impl<'a> Ui<'a> { } } else if det_col > 0 { self.details_panel = Some(Self::make_details_panel( - self.colors.clone(), n_row - 1, det_col, 0, @@ -815,22 +795,8 @@ 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 { - return Panel::new( - colors, - "Details".to_string(), - 2, - n_row, - n_col, - start_y, - start_x, - ); + pub fn make_details_panel(n_row: i32, n_col: i32, start_y: i32, start_x: i32) -> Panel { + return Panel::new("Details".to_string(), 2, n_row, n_col, start_y, start_x); } /// Updates the details panel with information about the current diff --git a/src/ui/notification.rs b/src/ui/notification.rs index 6105c82..0ff67e0 100644 --- a/src/ui/notification.rs +++ b/src/ui/notification.rs @@ -1,6 +1,6 @@ use std::time::{Duration, Instant}; -use super::colors::{ColorType, Colors}; +use super::{ColorType, COLORS}; use pancurses::{Input, Window}; /// Holds details of a notification message. @@ -37,7 +37,6 @@ impl Notification { #[derive(Debug)] pub struct NotifWin { window: Window, - colors: Colors, total_rows: i32, total_cols: i32, msg_stack: Vec, @@ -47,11 +46,10 @@ pub struct NotifWin { impl NotifWin { /// Creates a new NotifWin. - pub fn new(colors: Colors, total_rows: i32, total_cols: i32) -> Self { + pub fn new(total_rows: i32, total_cols: i32) -> Self { let win = pancurses::newwin(1, total_cols, total_rows - 1, 0); return Self { window: win, - colors: colors, total_rows: total_rows, total_cols: total_cols, msg_stack: Vec::new(), @@ -100,9 +98,8 @@ impl NotifWin { // otherwise, there was a notification before but there // isn't now, so erase self.window.erase(); - self.window.bkgdset(pancurses::ColorPair( - self.colors.get(ColorType::Normal) as u8 - )); + self.window + .bkgdset(pancurses::ColorPair(COLORS.get(ColorType::Normal) as u8)); self.window.refresh(); self.current_msg = None; } @@ -195,13 +192,8 @@ 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, COLORS.get(ColorType::Error)); } self.window.refresh(); } @@ -254,9 +246,8 @@ impl NotifWin { ); oldwin.delwin(); - self.window.bkgdset(pancurses::ColorPair( - self.colors.get(ColorType::Normal) as u8 - )); + self.window + .bkgdset(pancurses::ColorPair(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 d8055eb..af5e8d2 100644 --- a/src/ui/panel.rs +++ b/src/ui/panel.rs @@ -1,7 +1,7 @@ use chrono::{DateTime, Utc}; use pancurses::{Attribute, Window}; -use super::{ColorType, Colors}; +use super::{ColorType, COLORS}; /// Struct holding the raw data used for building the details panel. pub struct Details { @@ -23,7 +23,6 @@ pub struct Details { pub struct Panel { window: Window, screen_pos: usize, - colors: Colors, title: String, n_row: i32, n_col: i32, @@ -32,7 +31,6 @@ pub struct Panel { impl Panel { /// Creates a new panel. pub fn new( - colors: Colors, title: String, screen_pos: usize, n_row: i32, @@ -45,7 +43,6 @@ impl Panel { return Panel { window: panel_win, screen_pos: screen_pos, - colors: colors, title: title, n_row: n_row, n_col: n_col, @@ -59,9 +56,8 @@ impl Panel { /// Redraws borders and refreshes the window to display on terminal. pub fn refresh(&self) { - self.window.bkgdset(pancurses::ColorPair( - self.colors.get(ColorType::Normal) as u8 - )); + self.window + .bkgdset(pancurses::ColorPair(COLORS.get(ColorType::Normal) as u8)); self.draw_border(); self.window.refresh(); } @@ -98,9 +94,8 @@ impl Panel { /// not refresh the screen. pub fn erase(&self) { self.window.erase(); - self.window.bkgdset(pancurses::ColorPair( - self.colors.get(ColorType::Normal) as u8 - )); + self.window + .bkgdset(pancurses::ColorPair(COLORS.get(ColorType::Normal) as u8)); self.draw_border(); } @@ -227,7 +222,7 @@ impl Panel { self.abs_x(x), nchars, attr, - self.colors.get(color), + COLORS.get(color), ); } diff --git a/src/ui/popup.rs b/src/ui/popup.rs index fd612f2..7962122 100644 --- a/src/ui/popup.rs +++ b/src/ui/popup.rs @@ -1,7 +1,7 @@ use pancurses::Input; use std::cmp::min; -use super::{ColorType, Colors}; +use super::ColorType; use super::{Menu, Panel, UiMsg}; use crate::config::BIG_SCROLL_AMOUNT; use crate::keymap::{Keybindings, UserAction}; @@ -39,7 +39,6 @@ impl ActivePopup { pub struct PopupWin<'a> { popup: ActivePopup, new_episodes: Vec, - colors: Colors, keymap: &'a Keybindings, total_rows: i32, total_cols: i32, @@ -50,11 +49,10 @@ pub struct PopupWin<'a> { 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 { + pub fn new(keymap: &'a Keybindings, total_rows: i32, total_cols: i32) -> Self { return Self { popup: ActivePopup::None, new_episodes: Vec::new(), - colors: colors, keymap: keymap, total_rows: total_rows, total_cols: total_cols, @@ -123,7 +121,6 @@ impl<'a> PopupWin<'a> { // 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, @@ -212,7 +209,6 @@ impl<'a> PopupWin<'a> { // 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, @@ -285,7 +281,6 @@ impl<'a> PopupWin<'a> { // confused between panel.rs and mock_panel.rs #[allow(unused_mut)] let mut download_panel = Panel::new( - self.colors.clone(), "New episodes".to_string(), 0, self.total_rows - 1, From c137b75f9e68d6bdcd3f3d46cf65968aef1ae20f Mon Sep 17 00:00:00 2001 From: Jeff Hughes Date: Sat, 8 May 2021 12:37:53 -0400 Subject: [PATCH 14/23] Remove a bunch of unnecessary string cloning --- src/config.rs | 69 ++++++++++++++++++++---------------------- src/downloads.rs | 13 ++++---- src/keymap.rs | 6 ++-- src/opml.rs | 19 +++--------- src/play_file.rs | 23 +++++--------- src/sanitizer.rs | 6 ++-- src/types.rs | 8 ++--- src/ui/menu.rs | 2 +- src/ui/mock_panel.rs | 24 +++++++-------- src/ui/mod.rs | 16 +++++----- src/ui/notification.rs | 22 +++++++------- src/ui/panel.rs | 31 +++++++++---------- src/ui/popup.rs | 16 ++++------ 13 files changed, 112 insertions(+), 143 deletions(-) diff --git a/src/config.rs b/src/config.rs index bb27f19..689de0f 100644 --- a/src/config.rs +++ b/src/config.rs @@ -151,7 +151,7 @@ impl Config { } } - return config_with_defaults(&config_toml); + return config_with_defaults(config_toml); } } @@ -159,48 +159,45 @@ impl Config { /// that specifies user settings where indicated, and defaults for any /// settings that were not specified by the user. #[allow(clippy::type_complexity)] -fn config_with_defaults(config_toml: &ConfigFromToml) -> Result { +fn config_with_defaults(config_toml: ConfigFromToml) -> Result { // 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.big_up, UserAction::BigUp, vec!["K".to_string()]), - (&config_toml.keybindings.big_down, UserAction::BigDown, vec!["J".to_string()]), - (&config_toml.keybindings.page_up, UserAction::PageUp, vec!["PgUp".to_string()]), - (&config_toml.keybindings.page_down, UserAction::PageDown, vec!["PgDn".to_string()]), - (&config_toml.keybindings.go_top, UserAction::GoTop, vec!["g".to_string()]), - (&config_toml.keybindings.go_bot, UserAction::GoBot, vec!["G".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.help, UserAction::Help, vec!["?".to_string()]), - (&config_toml.keybindings.quit, UserAction::Quit, vec!["q".to_string()]), + 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.big_up, UserAction::BigUp, vec!["K".to_string()]), + (config_toml.keybindings.big_down, UserAction::BigDown, vec!["J".to_string()]), + (config_toml.keybindings.page_up, UserAction::PageUp, vec!["PgUp".to_string()]), + (config_toml.keybindings.page_down, UserAction::PageDown, vec!["PgDn".to_string()]), + (config_toml.keybindings.go_top, UserAction::GoTop, vec!["g".to_string()]), + (config_toml.keybindings.go_bot, UserAction::GoBot, vec!["G".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.help, UserAction::Help, vec!["?".to_string()]), + (config_toml.keybindings.quit, UserAction::Quit, vec!["q".to_string()]), ]; // for each action, if user preference is set, use that, otherwise, // use the default let mut keymap = Keybindings::new(); - for (config, action, defaults) in action_map.iter() { - match config { - Some(v) => keymap.insert_from_vec(v, *action), - None => keymap.insert_from_vec(&defaults, *action), - } + for (config, action, defaults) in action_map.into_iter() { + keymap.insert_from_vec(config.unwrap_or(defaults), action); } // paths are set by user, or they resolve to OS-specific path as diff --git a/src/downloads.rs b/src/downloads.rs index 2328bb4..a7e72c2 100644 --- a/src/downloads.rs +++ b/src/downloads.rs @@ -56,8 +56,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 { - let mut data = ep_data.clone(); +fn download_file(mut ep_data: EpData, dest: PathBuf, mut max_retries: usize) -> DownloadMsg { let request: Result = loop { let response = ureq::get(&ep_data.url) .timeout_connect(5000) @@ -74,7 +73,7 @@ fn download_file(ep_data: EpData, dest: PathBuf, mut max_retries: usize) -> Down }; if request.is_err() { - return DownloadMsg::ResponseError(data); + return DownloadMsg::ResponseError(ep_data); }; let response = request.unwrap(); @@ -104,14 +103,14 @@ fn download_file(ep_data: EpData, dest: PathBuf, mut max_retries: usize) -> Down let dst = File::create(&file_path); if dst.is_err() { - return DownloadMsg::FileCreateError(data); + return DownloadMsg::FileCreateError(ep_data); }; - data.file_path = Some(file_path); + ep_data.file_path = Some(file_path); let mut reader = response.into_reader(); return match std::io::copy(&mut reader, &mut dst.unwrap()) { - Ok(_) => DownloadMsg::Complete(data), - Err(_) => DownloadMsg::FileWriteError(data), + Ok(_) => DownloadMsg::Complete(ep_data), + Err(_) => DownloadMsg::FileWriteError(ep_data), }; } diff --git a/src/keymap.rs b/src/keymap.rs index c7bdfa0..8fcdf43 100644 --- a/src/keymap.rs +++ b/src/keymap.rs @@ -70,9 +70,9 @@ impl Keybindings { /// Inserts a set of new keybindings into the hash map, each one /// corresponding to the same UserAction. Will overwrite the value /// of keys that already exist. - pub fn insert_from_vec(&mut self, vec: &[String], action: UserAction) { - for key in vec.iter() { - self.insert(key.to_string(), action); + pub fn insert_from_vec(&mut self, vec: Vec, action: UserAction) { + for key in vec.into_iter() { + self.insert(key, action); } } diff --git a/src/opml.rs b/src/opml.rs index 12fce85..fcc02fd 100644 --- a/src/opml.rs +++ b/src/opml.rs @@ -12,32 +12,23 @@ pub fn import(xml: String) -> Result> { Err(err) => Err(anyhow!(err)), Ok(opml) => { let mut feeds = Vec::new(); - for pod in opml.body.outlines.iter() { - if let Some(url) = pod.xml_url.clone() { + for pod in opml.body.outlines.into_iter() { + if pod.xml_url.is_some() { // match against title attribute first -- if this is // not set or empty, then match against the text // attribute; this must be set, but can be empty - let temp_title = match &pod.title { - Some(t) => { - if t.is_empty() { - None - } else { - Some(t.clone()) - } - } - None => None, - }; + let temp_title = pod.title.filter(|t| !t.is_empty()); let title = match temp_title { Some(t) => Some(t), None => { if pod.text.is_empty() { None } else { - Some(pod.text.clone()) + Some(pod.text) } } }; - feeds.push(PodcastFeed::new(None, url, title)); + feeds.push(PodcastFeed::new(None, pod.xml_url.unwrap(), title)); } } Ok(feeds) diff --git a/src/play_file.rs b/src/play_file.rs index ca51a2d..76d3b43 100644 --- a/src/play_file.rs +++ b/src/play_file.rs @@ -6,29 +6,20 @@ pub fn execute(command: &str, path: &str) -> Result<()> { // Command expects a command and then optional arguments (giving // everything to it in a string doesn't work), so we need to split // on white space and treat everything after the first word as args - let cmd_string = String::from(command); + let cmd_string = command.to_string(); let mut parts = cmd_string.trim().split_whitespace(); let base_cmd = parts.next().ok_or_else(|| anyhow!("Invalid command."))?; - let args_iter = parts; + let mut cmd = Command::new(base_cmd); - 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(); + // if command contains "%s", replace the path with that value + cmd.args(parts.map(|a| if a == "%s" { path } else { a })); } else { - args = args_iter.map(|a| a.to_string()).collect(); - args.push(path.to_string()); + // otherwise, add path to the end of the command + cmd.args(parts.chain(vec![path].into_iter())); } - let mut cmd = Command::new(base_cmd); - cmd.args(args).stdout(Stdio::null()).stderr(Stdio::null()); + cmd.stdout(Stdio::null()).stderr(Stdio::null()); match cmd.spawn() { Ok(_) => Ok(()), Err(err) => Err(anyhow!(err)), diff --git a/src/sanitizer.rs b/src/sanitizer.rs index e30ed12..e7113b7 100644 --- a/src/sanitizer.rs +++ b/src/sanitizer.rs @@ -27,7 +27,7 @@ pub fn sanitize_rfc822_like_date>(s: S) -> String { /// Pad HH:MM:SS with exta zeros if needed. fn pad_zeros(s: String) -> String { lazy_static! { - /// If it matchers a pattern of 2:2:2, return. + /// If it matches a pattern of 2:2:2, return. static ref OK_RGX: Regex = Regex::new(r"(\d{2}):(\d{2}):(\d{2})").expect("Regex error"); /// hours, minutes, seconds = cap[1], cap[2], cap[3] @@ -48,8 +48,8 @@ fn pad_zeros(s: String) -> String { tm.push_str(m_str); tm.push(':'); }); - tm.pop(); // Pop leftover last separator (at no penalty, since we only allocate once - // either way) + tm.pop(); // Pop leftover last separator (at no penalty, since + // we only allocate once either way) return s.replace(&cap[0], &tm); } diff --git a/src/types.rs b/src/types.rs index 15e47ee..d7f1040 100644 --- a/src/types.rs +++ b/src/types.rs @@ -164,7 +164,7 @@ 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"); let meta_str = format!("({}) {}", pd, meta_dur); let added_len = meta_str.chars().count(); @@ -251,11 +251,7 @@ impl Menuable for NewEpisode { /// 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 selected = if self.selected { "✓" } else { " " }; let full_string = format!("[{}] {} ({})", selected, self.title, self.pod_title); return full_string.substr(0, length); } diff --git a/src/ui/menu.rs b/src/ui/menu.rs index cd586ce..6fdc130 100644 --- a/src/ui/menu.rs +++ b/src/ui/menu.rs @@ -108,7 +108,7 @@ impl Menu { /// 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; + return self.panel.write_wrap_line(0, header) + 2; } else { return 0; } diff --git a/src/ui/mock_panel.rs b/src/ui/mock_panel.rs index d30ae8f..ecd6e5c 100644 --- a/src/ui/mock_panel.rs +++ b/src/ui/mock_panel.rs @@ -70,7 +70,7 @@ impl Panel { .push((String::new(), pancurses::A_NORMAL, ColorType::Normal)); } - pub fn write_wrap_line(&mut self, start_y: i32, string: String) -> i32 { + pub fn write_wrap_line(&mut self, start_y: i32, string: &str) -> i32 { let mut row = start_y; let max_row = self.get_rows(); let wrapper = textwrap::wrap_iter(&string, self.get_cols() as usize); @@ -90,14 +90,14 @@ impl Panel { // 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"), } // 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"), } row += 1; // blank line @@ -106,7 +106,7 @@ impl Panel { if let Some(date) = details.pubdate { let new_row = self.write_wrap_line( row + 1, - format!("Published: {}", date.format("%B %-d, %Y").to_string()), + &format!("Published: {}", date.format("%B %-d, %Y")), ); self.change_attr(row + 1, 0, 10, pancurses::A_UNDERLINE, ColorType::Normal); row = new_row; @@ -114,7 +114,7 @@ impl Panel { // duration if let Some(dur) = details.duration { - let new_row = self.write_wrap_line(row + 1, format!("Duration: {}", dur)); + 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; } @@ -122,9 +122,9 @@ impl Panel { // 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") } else { - self.write_wrap_line(row + 1, "Explicit: No".to_string()) + self.write_wrap_line(row + 1, "Explicit: No") }; self.change_attr(row + 1, 0, 9, pancurses::A_UNDERLINE, ColorType::Normal); row = new_row; @@ -135,11 +135,11 @@ impl Panel { // 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:"); + 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."); } } } diff --git a/src/ui/mod.rs b/src/ui/mod.rs index 76f9348..6c93b69 100644 --- a/src/ui/mod.rs +++ b/src/ui/mod.rs @@ -100,7 +100,7 @@ impl<'a> Ui<'a> { tx_to_main: mpsc::Sender, ) -> thread::JoinHandle<()> { return thread::spawn(move || { - let mut ui = Ui::new(&config, &items); + let mut ui = Ui::new(&config, items); ui.init(); let mut message_iter = rx_from_main.try_iter(); // this is the main event loop: on each loop, we update @@ -145,7 +145,7 @@ impl<'a> Ui<'a> { /// Initializes the UI with a list of podcasts and podcast episodes, /// creates the pancurses window and draws it to the screen, and /// returns a UI object for future manipulation. - pub fn new(config: &'a Config, items: &LockVec) -> Ui<'a> { + pub fn new(config: &'a Config, items: LockVec) -> Ui<'a> { let stdscr = pancurses::initscr(); // set some options @@ -161,12 +161,6 @@ impl<'a> Ui<'a> { let (n_row, n_col) = stdscr.get_max_yx(); let (pod_col, ep_col, det_col) = Self::calculate_sizes(n_col); - let podcast_panel = Panel::new("Podcasts".to_string(), 0, n_row - 1, pod_col, 0, 0); - let podcast_menu = Menu::new(podcast_panel, None, items.clone()); - - let episode_panel = - Panel::new("Episodes".to_string(), 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(), @@ -175,6 +169,12 @@ impl<'a> Ui<'a> { None => LockVec::new(Vec::new()), }; + let podcast_panel = Panel::new("Podcasts".to_string(), 0, n_row - 1, pod_col, 0, 0); + let podcast_menu = Menu::new(podcast_panel, None, items); + + let episode_panel = + Panel::new("Episodes".to_string(), 1, n_row - 1, ep_col, 0, pod_col - 1); + let episode_menu = Menu::new(episode_panel, None, first_pod); let details_panel = if n_col > crate::config::DETAILS_PANEL_LENGTH { diff --git a/src/ui/notification.rs b/src/ui/notification.rs index 0ff67e0..28effb8 100644 --- a/src/ui/notification.rs +++ b/src/ui/notification.rs @@ -72,26 +72,26 @@ impl NotifWin { if !self.msg_stack.is_empty() { // check if last item changed, and update screen if it has - let last_item = self.msg_stack[self.msg_stack.len() - 1].clone(); + let last_item = &self.msg_stack[self.msg_stack.len() - 1]; match &self.current_msg { Some(curr) => { - if &last_item != curr { - self.display_notif(last_item.clone()); + if last_item != curr { + self.display_notif(last_item); } } - None => self.display_notif(last_item.clone()), + None => self.display_notif(last_item), }; - self.current_msg = Some(last_item); + self.current_msg = Some(last_item.clone()); } else if let Some(msg) = &self.persistent_msg { // if no other timed notifications exist, display a // persistent notification if there is one match &self.current_msg { Some(curr) => { if msg != curr { - self.display_notif(msg.clone()); + self.display_notif(msg); } } - None => self.display_notif(msg.clone()), + None => self.display_notif(msg), }; self.current_msg = Some(msg.clone()); } else { @@ -185,11 +185,11 @@ impl NotifWin { } /// Prints a notification to the window. - fn display_notif(&self, notif: Notification) { + fn display_notif(&self, notif: &Notification) { self.window.erase(); self.window.mv(self.total_rows - 1, 0); self.window.attrset(pancurses::A_NORMAL); - self.window.addstr(notif.message); + self.window.addstr(¬if.message); if notif.error { self.window @@ -215,7 +215,7 @@ impl NotifWin { let notif = Notification::new(message, error, None); self.persistent_msg = Some(notif.clone()); if self.msg_stack.is_empty() { - self.display_notif(notif.clone()); + self.display_notif(¬if); self.current_msg = Some(notif); } } @@ -249,7 +249,7 @@ impl NotifWin { self.window .bkgdset(pancurses::ColorPair(COLORS.get(ColorType::Normal) as u8)); if let Some(curr) = &self.current_msg { - self.display_notif(curr.clone()); + self.display_notif(curr); } self.window.refresh(); } diff --git a/src/ui/panel.rs b/src/ui/panel.rs index af5e8d2..7d01eb3 100644 --- a/src/ui/panel.rs +++ b/src/ui/panel.rs @@ -87,7 +87,7 @@ impl Panel { pancurses::ACS_LRCORNER(), ); - self.window.mvaddstr(0, 2, self.title.clone()); + self.window.mvaddstr(0, 2, &self.title); } /// Erases all content on the window, and redraws the border. Does @@ -126,13 +126,12 @@ impl Panel { /// when necessary. `start_y` refers to the row to start at (word /// wrapping makes it unknown where text will end). Returns the row /// on which the text ended. - pub fn write_wrap_line(&self, start_y: i32, string: String) -> i32 { + pub fn write_wrap_line(&self, start_y: i32, string: &str) -> i32 { let mut row = start_y; let max_row = self.get_rows(); - let wrapper = textwrap::wrap_iter(&string, self.get_cols() as usize); + 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); row += 1; if row >= max_row { @@ -150,14 +149,14 @@ impl Panel { 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"), } // 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"), } self.window.attroff(Attribute::Bold); @@ -167,7 +166,7 @@ impl Panel { if let Some(date) = details.pubdate { let new_row = self.write_wrap_line( row + 1, - format!("Published: {}", date.format("%B %-d, %Y").to_string()), + &format!("Published: {}", date.format("%B %-d, %Y")), ); self.change_attr(row + 1, 0, 10, pancurses::A_UNDERLINE, ColorType::Normal); row = new_row; @@ -175,7 +174,7 @@ impl Panel { // duration if let Some(dur) = details.duration { - let new_row = self.write_wrap_line(row + 1, format!("Duration: {}", dur)); + 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; } @@ -183,9 +182,9 @@ impl Panel { // 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") } else { - self.write_wrap_line(row + 1, "Explicit: No".to_string()) + self.write_wrap_line(row + 1, "Explicit: No") }; self.change_attr(row + 1, 0, 9, pancurses::A_UNDERLINE, ColorType::Normal); row = new_row; @@ -197,12 +196,12 @@ impl Panel { 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:"); 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."); } } } diff --git a/src/ui/popup.rs b/src/ui/popup.rs index 7962122..fae5cff 100644 --- a/src/ui/popup.rs +++ b/src/ui/popup.rs @@ -130,20 +130,16 @@ impl<'a> PopupWin<'a> { ); 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!"); 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])); + &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(), + "More details of how to customize shellcaster can be found on the Github repo readme:", ); + let _ = welcome_win.write_wrap_line(row + 1, "https://github.com/jeff-hughes/shellcaster"); return welcome_win; } @@ -218,7 +214,7 @@ impl<'a> PopupWin<'a> { ); let mut row = 0; - row = help_win.write_wrap_line(row + 1, "Available keybindings:".to_string()); + row = help_win.write_wrap_line(row + 1, "Available keybindings:"); help_win.change_attr(row, 0, 22, pancurses::A_UNDERLINE, ColorType::Normal); row += 1; @@ -261,7 +257,7 @@ impl<'a> PopupWin<'a> { row += 1; } - let _ = help_win.write_wrap_line(row + 2, "Press \"q\" to close this window.".to_string()); + let _ = help_win.write_wrap_line(row + 2, "Press \"q\" to close this window."); return help_win; } From 0ba2528d096ddbee84f26280358286d8fd173be8 Mon Sep 17 00:00:00 2001 From: Jeff Hughes Date: Sun, 9 May 2021 19:26:31 -0400 Subject: [PATCH 15/23] Move keybinding defaults to keymap module --- src/config.rs | 101 ++++++++++++++++------------------------------ src/keymap.rs | 109 +++++++++++++++++++++++++++++++++++++++++++++----- 2 files changed, 132 insertions(+), 78 deletions(-) diff --git a/src/config.rs b/src/config.rs index 689de0f..fae319c 100644 --- a/src/config.rs +++ b/src/config.rs @@ -4,7 +4,7 @@ use std::fs::File; use std::io::Read; use std::path::{Path, PathBuf}; -use crate::keymap::{Keybindings, UserAction}; +use crate::keymap::Keybindings; // Specifies how long, in milliseconds, to display messages at the // bottom of the screen in the UI. @@ -61,37 +61,37 @@ struct ConfigFromToml { download_new_episodes: Option, simultaneous_downloads: Option, max_retries: Option, - keybindings: KeybindingsFromToml, + keybindings: Option, } /// A temporary struct used to deserialize keybinding data from the TOML /// configuration file. #[derive(Debug, Deserialize)] -struct KeybindingsFromToml { - left: Option>, - right: Option>, - up: Option>, - down: Option>, - big_up: Option>, - big_down: Option>, - go_top: Option>, - go_bot: Option>, - page_up: Option>, - page_down: Option>, - add_feed: Option>, - sync: Option>, - sync_all: Option>, - play: Option>, - mark_played: Option>, - mark_all_played: Option>, - download: Option>, - download_all: Option>, - delete: Option>, - delete_all: Option>, - remove: Option>, - remove_all: Option>, - help: Option>, - quit: Option>, +pub struct KeybindingsFromToml { + pub left: Option>, + pub right: Option>, + pub up: Option>, + pub down: Option>, + pub big_up: Option>, + pub big_down: Option>, + pub go_top: Option>, + pub go_bot: Option>, + pub page_up: Option>, + pub page_down: Option>, + pub add_feed: Option>, + pub sync: Option>, + pub sync_all: Option>, + pub play: Option>, + pub mark_played: Option>, + pub mark_all_played: Option>, + pub download: Option>, + pub download_all: Option>, + pub delete: Option>, + pub delete_all: Option>, + pub remove: Option>, + pub remove_all: Option>, + pub help: Option>, + pub quit: Option>, } @@ -140,13 +140,14 @@ impl Config { help: None, quit: None, }; + config_toml = ConfigFromToml { download_path: None, play_command: None, download_new_episodes: None, simultaneous_downloads: None, max_retries: None, - keybindings: keybindings, + keybindings: Some(keybindings), }; } } @@ -160,45 +161,11 @@ impl Config { /// settings that were not specified by the user. #[allow(clippy::type_complexity)] fn config_with_defaults(config_toml: ConfigFromToml) -> Result { - // 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.big_up, UserAction::BigUp, vec!["K".to_string()]), - (config_toml.keybindings.big_down, UserAction::BigDown, vec!["J".to_string()]), - (config_toml.keybindings.page_up, UserAction::PageUp, vec!["PgUp".to_string()]), - (config_toml.keybindings.page_down, UserAction::PageDown, vec!["PgDn".to_string()]), - (config_toml.keybindings.go_top, UserAction::GoTop, vec!["g".to_string()]), - (config_toml.keybindings.go_bot, UserAction::GoBot, vec!["G".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.help, UserAction::Help, vec!["?".to_string()]), - (config_toml.keybindings.quit, UserAction::Quit, vec!["q".to_string()]), - ]; - - // for each action, if user preference is set, use that, otherwise, - // use the default - let mut keymap = Keybindings::new(); - for (config, action, defaults) in action_map.into_iter() { - keymap.insert_from_vec(config.unwrap_or(defaults), action); - } + // specify keybindings + let keymap = match config_toml.keybindings { + Some(kb) => Keybindings::from_config(kb), + None => Keybindings::default(), + }; // paths are set by user, or they resolve to OS-specific path as // provided by dirs crate diff --git a/src/keymap.rs b/src/keymap.rs index 8fcdf43..016d9e1 100644 --- a/src/keymap.rs +++ b/src/keymap.rs @@ -1,9 +1,11 @@ use pancurses::Input; use std::collections::HashMap; +use crate::config::KeybindingsFromToml; + /// Enum delineating all actions that may be performed by the user, and /// thus have keybindings associated with them. -#[derive(Debug, Clone, Copy, PartialEq)] +#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)] pub enum UserAction { Left, Right, @@ -40,23 +42,71 @@ pub enum UserAction { /// keys may perform the same action, but each key may only perform one /// action. #[derive(Debug, Clone)] -pub struct Keybindings { - map: HashMap, -} +pub struct Keybindings(HashMap); impl Keybindings { /// Returns a new Keybindings struct. - pub fn new() -> Keybindings { - return Keybindings { - map: HashMap::new(), - }; + pub fn new() -> Self { + return Self(HashMap::new()); + } + + /// Returns a Keybindings struct with all default values set. + pub fn default() -> Self { + let defaults = Self::_defaults(); + let mut keymap = Self::new(); + for (action, defaults) in defaults.into_iter() { + keymap.insert_from_vec(defaults, action); + } + return keymap; + } + + /// Given a struct deserialized from config.toml (for which any or + /// all fields may be missing), create a Keybindings struct using + /// user-defined keys where specified, and default values otherwise. + pub fn from_config(config: KeybindingsFromToml) -> Self { + let defaults = Self::_defaults(); + let config_actions: Vec<(Option>, UserAction)> = vec![ + (config.left, UserAction::Left), + (config.right, UserAction::Right), + (config.up, UserAction::Up), + (config.down, UserAction::Down), + (config.big_up, UserAction::BigUp), + (config.big_down, UserAction::BigDown), + (config.page_up, UserAction::PageUp), + (config.page_down, UserAction::PageDown), + (config.go_top, UserAction::GoTop), + (config.go_bot, UserAction::GoBot), + (config.add_feed, UserAction::AddFeed), + (config.sync, UserAction::Sync), + (config.sync_all, UserAction::SyncAll), + (config.play, UserAction::Play), + (config.mark_played, UserAction::MarkPlayed), + (config.mark_all_played, UserAction::MarkAllPlayed), + (config.download, UserAction::Download), + (config.download_all, UserAction::DownloadAll), + (config.delete, UserAction::Delete), + (config.delete_all, UserAction::DeleteAll), + (config.remove, UserAction::Remove), + (config.remove_all, UserAction::RemoveAll), + (config.help, UserAction::Help), + (config.quit, UserAction::Quit), + ]; + + let mut keymap = Self::new(); + for (config, action) in config_actions.into_iter() { + keymap.insert_from_vec( + config.unwrap_or_else(|| defaults.get(&action).unwrap().clone()), + action, + ); + } + return keymap; } /// Takes an Input object from pancurses and returns the associated /// 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.0.get(&code), None => None, } } @@ -64,7 +114,7 @@ impl Keybindings { /// Inserts a new keybinding into the hash map. Will overwrite the /// value of a key if it already exists. pub fn insert(&mut self, code: String, action: UserAction) { - self.map.insert(code, action); + self.0.insert(code, action); } /// Inserts a set of new keybindings into the hash map, each one @@ -80,7 +130,7 @@ impl Keybindings { /// action. pub fn keys_for_action(&self, action: UserAction) -> Vec { return self - .map + .0 .iter() .filter_map(|(key, &val)| { if val == action { @@ -91,6 +141,43 @@ impl Keybindings { }) .collect(); } + + fn _defaults() -> HashMap> { + let action_map: Vec<(UserAction, Vec)> = vec![ + (UserAction::Left, vec!["Left".to_string(), "h".to_string()]), + (UserAction::Right, vec![ + "Right".to_string(), + "l".to_string(), + ]), + (UserAction::Up, vec!["Up".to_string(), "k".to_string()]), + (UserAction::Down, vec!["Down".to_string(), "j".to_string()]), + (UserAction::BigUp, vec!["K".to_string()]), + (UserAction::BigDown, vec!["J".to_string()]), + (UserAction::PageUp, vec!["PgUp".to_string()]), + (UserAction::PageDown, vec!["PgDn".to_string()]), + (UserAction::GoTop, vec!["g".to_string()]), + (UserAction::GoBot, vec!["G".to_string()]), + (UserAction::AddFeed, vec!["a".to_string()]), + (UserAction::Sync, vec!["s".to_string()]), + (UserAction::SyncAll, vec!["S".to_string()]), + (UserAction::Play, vec!["Enter".to_string(), "p".to_string()]), + (UserAction::MarkPlayed, vec!["m".to_string()]), + (UserAction::MarkAllPlayed, vec!["M".to_string()]), + (UserAction::Download, vec!["d".to_string()]), + (UserAction::DownloadAll, vec!["D".to_string()]), + (UserAction::Delete, vec!["x".to_string()]), + (UserAction::DeleteAll, vec!["X".to_string()]), + (UserAction::Remove, vec!["r".to_string()]), + (UserAction::RemoveAll, vec!["R".to_string()]), + (UserAction::Help, vec!["?".to_string()]), + (UserAction::Quit, vec!["q".to_string()]), + ]; + let mut default_map = HashMap::new(); + for (action, defaults) in action_map.into_iter() { + default_map.insert(action, defaults); + } + return default_map; + } } /// Helper function converting a pancurses Input object to a unique From e1c53ee2d6943483de94bcf155ec4204128ff8e0 Mon Sep 17 00:00:00 2001 From: Jeff Hughes Date: Sun, 9 May 2021 19:28:36 -0400 Subject: [PATCH 16/23] Add customizable app colors (#25) This required reverting some of the changes to move colors to a static value, but now is handled with Rc to pass around a single reference. --- config.toml | 32 +++++ src/config.rs | 103 +++++++++++++++ src/ui/colors.rs | 288 ++++++++++++++++++++++++++++++++++++----- src/ui/menu.rs | 13 +- src/ui/mock_panel.rs | 7 +- src/ui/mod.rs | 60 +++++++-- src/ui/notification.rs | 26 ++-- src/ui/panel.rs | 19 ++- src/ui/popup.rs | 15 ++- 9 files changed, 503 insertions(+), 60 deletions(-) diff --git a/config.toml b/config.toml index 5434374..1fec78e 100644 --- a/config.toml +++ b/config.toml @@ -93,3 +93,35 @@ remove_all = [ "R" ] help = [ "?" ] quit = [ "q" ] + + +[colors] + +# Colors can be identified in three ways: +# 1. Using color names defined by your terminal: +# - black, blue, cyan, green, magenta, red, white, or yellow +# - The special color name "terminal" can also be used to specify +# your terminal's default foreground or background color; this is +# particularly useful if your terminal background is transparent -- +# use "terminal" for the background colors below. +# 2. Using a hex code in the format "#ff0000" or "#FF0000" to specify +# RGB values. +# 3. Using an RGB value in the format "rgb(255, 0, 0)" where each number +# is a value between 0 and 255. + +# all regular text +normal_foreground = "rgb(173, 173, 173)" +normal_background = "black" + +# colors for the currently selected podcast/episode +highlighted_active_foreground = "rgb(85, 85, 85)" +highlighted_active_background = "rgb(209, 164, 0)" + +# colors for the selected podcast, when the cursor is on the episode +# menu; podcast is selected, but not currently "active" +highlighted_foreground = "rgb(85, 85, 85)" +highlighted_background = "rgb(173, 173, 173)" + +# text for error messages +error_foreground = "red" +error_background = "black" diff --git a/src/config.rs b/src/config.rs index fae319c..8e91012 100644 --- a/src/config.rs +++ b/src/config.rs @@ -5,6 +5,7 @@ use std::io::Read; use std::path::{Path, PathBuf}; use crate::keymap::Keybindings; +use crate::ui::colors::ColorValue; // Specifies how long, in milliseconds, to display messages at the // bottom of the screen in the UI. @@ -50,6 +51,7 @@ pub struct Config { pub simultaneous_downloads: usize, pub max_retries: usize, pub keybindings: Keybindings, + pub colors: AppColors, } /// A temporary struct used to deserialize data from the TOML configuration @@ -62,6 +64,7 @@ struct ConfigFromToml { simultaneous_downloads: Option, max_retries: Option, keybindings: Option, + colors: Option, } /// A temporary struct used to deserialize keybinding data from the TOML @@ -94,6 +97,84 @@ pub struct KeybindingsFromToml { pub quit: Option>, } +/// Holds information about the colors to use in the application. Tuple +/// values represent (foreground, background), respectively. +#[derive(Debug, Clone)] +pub struct AppColors { + pub normal: (ColorValue, ColorValue), + pub highlighted_active: (ColorValue, ColorValue), + pub highlighted: (ColorValue, ColorValue), + pub error: (ColorValue, ColorValue), +} + +impl AppColors { + pub fn default() -> Self { + return Self { + normal: (ColorValue::White, ColorValue::Black), + highlighted_active: (ColorValue::Black, ColorValue::Yellow), + highlighted: (ColorValue::Black, ColorValue::White), + error: (ColorValue::Red, ColorValue::Black), + }; + } + + pub fn add_from_config(&mut self, config: AppColorsFromToml) { + if let Some(val) = config.normal_foreground { + if let Ok(v) = ColorValue::from_str(&val) { + self.normal.0 = v; + } + } + if let Some(val) = config.normal_background { + if let Ok(v) = ColorValue::from_str(&val) { + self.normal.1 = v; + } + } + if let Some(val) = config.highlighted_active_foreground { + if let Ok(v) = ColorValue::from_str(&val) { + self.highlighted_active.0 = v; + } + } + if let Some(val) = config.highlighted_active_background { + if let Ok(v) = ColorValue::from_str(&val) { + self.highlighted_active.1 = v; + } + } + if let Some(val) = config.highlighted_foreground { + if let Ok(v) = ColorValue::from_str(&val) { + self.highlighted.0 = v; + } + } + if let Some(val) = config.highlighted_background { + if let Ok(v) = ColorValue::from_str(&val) { + self.highlighted.1 = v; + } + } + if let Some(val) = config.error_foreground { + if let Ok(v) = ColorValue::from_str(&val) { + self.error.0 = v; + } + } + if let Some(val) = config.error_background { + if let Ok(v) = ColorValue::from_str(&val) { + self.error.1 = v; + } + } + } +} + +/// A temporary struct used to deserialize colors data from the TOML +/// configuration file. +#[derive(Debug, Deserialize)] +pub struct AppColorsFromToml { + normal_foreground: Option, + normal_background: Option, + highlighted_active_foreground: Option, + highlighted_active_background: Option, + highlighted_foreground: Option, + highlighted_background: Option, + error_foreground: Option, + error_background: Option, +} + impl Config { /// Given a file path, this reads a TOML config file and returns a @@ -141,6 +222,16 @@ impl Config { quit: None, }; + let colors = AppColorsFromToml { + normal_foreground: None, + normal_background: None, + highlighted_active_foreground: None, + highlighted_active_background: None, + highlighted_foreground: None, + highlighted_background: None, + error_foreground: None, + error_background: None, + }; config_toml = ConfigFromToml { download_path: None, play_command: None, @@ -148,6 +239,7 @@ impl Config { simultaneous_downloads: None, max_retries: None, keybindings: Some(keybindings), + colors: Some(colors), }; } } @@ -167,6 +259,16 @@ fn config_with_defaults(config_toml: ConfigFromToml) -> Result { None => Keybindings::default(), }; + // specify app colors + let colors = match config_toml.colors { + Some(clrs) => { + let mut colors = AppColors::default(); + colors.add_from_config(clrs); + colors + } + None => AppColors::default(), + }; + // paths are set by user, or they resolve to OS-specific path as // provided by dirs crate let download_path = @@ -204,6 +306,7 @@ fn config_with_defaults(config_toml: ConfigFromToml) -> Result { simultaneous_downloads: simultaneous_downloads, max_retries: max_retries, keybindings: keymap, + colors: colors, }); } diff --git a/src/ui/colors.rs b/src/ui/colors.rs index 225c424..862e3a5 100644 --- a/src/ui/colors.rs +++ b/src/ui/colors.rs @@ -1,5 +1,109 @@ +use anyhow::{anyhow, Result}; use std::collections::HashMap; +use lazy_static::lazy_static; +use regex::Regex; + +use crate::config::AppColors; + +lazy_static! { + /// Regex for parsing a color specified as hex code. + static ref RE_COLOR_HEX: Regex = Regex::new(r"(?i)#([0-9a-f]{2})([0-9a-f]{2})([0-9a-f]{2})").expect("Regex error"); + + /// Regex for parsing a color specified as an rgb(x, y, z) value. + static ref RE_COLOR_RGB: Regex = Regex::new(r"(?i)rgb\(([0-9]+), ?([0-9]+), ?([0-9]+)\)").expect("Regex error"); +} + +/// Stores information about a single color value, specified either as +/// a word in the set black, blue, cyan, green, magenta, red, white, +/// yellow, or terminal, or an RGB code with values from 0 to 255. +#[derive(Debug, Clone, PartialEq)] +pub enum ColorValue { + Black, + Blue, + Cyan, + Green, + Magenta, + Red, + White, + Yellow, + Terminal, + Rgb(u8, u8, u8), +} + +impl ColorValue { + /// Parses a string that specifies a color either in hex format + /// (e.g., "#ff0000"), in RGB format (e.g., "rgb(255, 0, 0)"), or + /// as one of a set of allowed color names. + pub fn from_str(text: &str) -> Result { + if text.starts_with('#') { + if let Some(cap) = RE_COLOR_HEX.captures(text) { + return Ok(Self::Rgb( + u8::from_str_radix(&cap[1], 16)?, + u8::from_str_radix(&cap[2], 16)?, + u8::from_str_radix(&cap[3], 16)?, + )); + } + return Err(anyhow!("Invalid color hex code")); + } else if text.starts_with("rgb") || text.starts_with("RGB") { + if let Some(cap) = RE_COLOR_RGB.captures(text) { + return Ok(Self::Rgb( + u8::from_str_radix(&cap[1], 10)?, + u8::from_str_radix(&cap[2], 10)?, + u8::from_str_radix(&cap[3], 10)?, + )); + } + return Err(anyhow!("Invalid color RGB code")); + } else { + let text_lower = text.to_lowercase(); + return match &text_lower[..] { + "black" => Ok(Self::Black), + "blue" => Ok(Self::Blue), + "cyan" => Ok(Self::Cyan), + "green" => Ok(Self::Green), + "magenta" => Ok(Self::Magenta), + "red" => Ok(Self::Red), + "white" => Ok(Self::White), + "yellow" => Ok(Self::Yellow), + "terminal" => Ok(Self::Terminal), + _ => Err(anyhow!("Invalid color code")), + }; + } + } + + /// Converts a ColorValue to one of the built-in ncurses numeric + /// color identifiers. Note that ColorValue::Rgb(_, _, _) returns + /// None and must be handled separately. + fn to_ncurses_val(&self) -> Option { + return match self { + Self::Black => Some(pancurses::COLOR_BLACK), + Self::Blue => Some(pancurses::COLOR_BLUE), + Self::Cyan => Some(pancurses::COLOR_CYAN), + Self::Green => Some(pancurses::COLOR_GREEN), + Self::Magenta => Some(pancurses::COLOR_MAGENTA), + Self::Red => Some(pancurses::COLOR_RED), + Self::White => Some(pancurses::COLOR_WHITE), + Self::Yellow => Some(pancurses::COLOR_YELLOW), + Self::Terminal => Some(-1), + Self::Rgb(_, _, _) => None, + }; + } + + /// Returns whether ColorValue is of variant Terminal. + fn is_terminal(&self) -> bool { + return matches!(self, Self::Terminal); + } + + /// For variant ColorValue::Rgb, returns the RGB associated values. + fn get_rgb(&self) -> Option<(u8, u8, u8)> { + return match self { + Self::Rgb(r, g, b) => Some((*r, *g, *b)), + _ => None, + }; + } +} + + /// Enum identifying relevant text states that will be associated with /// distinct colors. #[derive(Eq, PartialEq, Hash, Copy, Clone, Debug)] @@ -12,30 +116,26 @@ pub enum ColorType { /// Keeps a hashmap associating ColorTypes with ncurses color pairs. #[derive(Debug, Clone)] -pub struct Colors { - map: HashMap, -} +pub struct Colors(HashMap); impl Colors { - pub fn new() -> Colors { - return Colors { - map: HashMap::new(), - }; + pub fn new() -> Self { + return Self(HashMap::new()); } pub fn insert(&mut self, color: ColorType, num: i16) { - self.map.insert(color, num); + self.0.insert(color, num); } pub fn get(&self, color: ColorType) -> i16 { - return *self.map.get(&color).expect("Error retrieving color type."); + return *self.0.get(&color).expect("Error retrieving color type."); } } /// Sets up hashmap for ColorTypes in app, initiates color palette, and /// sets up ncurses color pairs. -pub fn set_colors() -> Colors { +pub fn set_colors(config: &AppColors) -> Colors { // set up a hashmap for easier reference let mut colors = Colors::new(); colors.insert(ColorType::Normal, 0); @@ -43,31 +143,161 @@ pub fn set_colors() -> Colors { colors.insert(ColorType::HighlightedActive, 2); colors.insert(ColorType::Error, 3); - // specify some colors by RGB value - pancurses::init_color(pancurses::COLOR_WHITE, 680, 680, 680); - pancurses::init_color(pancurses::COLOR_YELLOW, 820, 643, 0); + // if the user has specified any colors to be "terminal" (i.e., to + // use their terminal's default foreground and background colors), + // then we must tell ncurses to allow the use of those colors. + if check_for_terminal(config) { + pancurses::use_default_colors(); + } - // instantiate curses color pairs - pancurses::init_pair( + // check if we have any RGB-specified values + // if count_app_colors(config, ColorValue::Rgb(0, 0, 0)) > 0 { + // let replace_color_order = vec![ColorValue::Cyan, ColorValue::Magenta, ColorValue::Blue, ColorValue::Green, ColorValue::Yellow, ColorValue::Red, ColorValue::Black, ColorValue::White]; + // } + let mut replace_counter = 8; + replace_counter = set_color_pair( colors.get(ColorType::Normal), - pancurses::COLOR_WHITE, - pancurses::COLOR_BLACK, + &config.normal, + replace_counter, ); - pancurses::init_pair( - colors.get(ColorType::Highlighted), - pancurses::COLOR_BLACK, - pancurses::COLOR_WHITE, - ); - pancurses::init_pair( + replace_counter = set_color_pair( colors.get(ColorType::HighlightedActive), - pancurses::COLOR_BLACK, - pancurses::COLOR_YELLOW, + &config.highlighted_active, + replace_counter, ); - pancurses::init_pair( - colors.get(ColorType::Error), - pancurses::COLOR_RED, - pancurses::COLOR_BLACK, + replace_counter = set_color_pair( + colors.get(ColorType::Highlighted), + &config.highlighted, + replace_counter, ); + let _ = set_color_pair(colors.get(ColorType::Error), &config.error, replace_counter); return colors; } + +/// Check for any app colors that are set to "Terminal", which means that +/// we should attempt to use the terminal's default foreground/background +/// colors. +fn check_for_terminal(app_colors: &AppColors) -> bool { + if app_colors.normal.0.is_terminal() { + return true; + } + if app_colors.normal.1.is_terminal() { + return true; + } + if app_colors.highlighted_active.0.is_terminal() { + return true; + } + if app_colors.highlighted_active.1.is_terminal() { + return true; + } + if app_colors.highlighted.0.is_terminal() { + return true; + } + if app_colors.highlighted.1.is_terminal() { + return true; + } + if app_colors.error.0.is_terminal() { + return true; + } + if app_colors.error.1.is_terminal() { + return true; + } + return false; +} + + +/// Helper function that takes a set of ColorValues indicating foreground +/// and background colors, initiates customized colors if necessary, and +/// adds the pair to ncurses with the key of `pair_index`. +fn set_color_pair( + pair_index: i16, + config: &(ColorValue, ColorValue), + mut replace_index: i16, +) -> i16 { + let mut c1 = config.0.to_ncurses_val(); + let mut c2 = config.1.to_ncurses_val(); + + if c1.is_none() { + let rgb = config.0.get_rgb().unwrap(); + pancurses::init_color( + replace_index, + u8_to_i16(rgb.0), + u8_to_i16(rgb.1), + u8_to_i16(rgb.2), + ); + c1 = Some(replace_index); + replace_index += 1; + } + if c2.is_none() { + let rgb = config.1.get_rgb().unwrap(); + pancurses::init_color( + replace_index, + u8_to_i16(rgb.0), + u8_to_i16(rgb.1), + u8_to_i16(rgb.2), + ); + c2 = Some(replace_index); + replace_index += 1; + } + + pancurses::init_pair(pair_index, c1.unwrap(), c2.unwrap()); + return replace_index; +} + +/// Converts a value from 0 to 255 to a value from 0 to 1000, because +/// ncurses has a weird color format. +fn u8_to_i16(val: u8) -> i16 { + return (val as f32 / 255.0 * 1000.0) as i16; +} + + +// TESTS ----------------------------------------------------------------- +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn color_hex() { + let color = String::from("#ff0000"); + let parsed = ColorValue::from_str(&color); + assert!(parsed.is_ok()); + assert_eq!(parsed.unwrap(), ColorValue::Rgb(255, 0, 0)); + } + + #[test] + fn color_invalid_hex() { + let color = String::from("#gg0000"); + assert!(ColorValue::from_str(&color).is_err()); + } + + #[test] + fn color_invalid_hex2() { + let color = String::from("#ff000"); + assert!(ColorValue::from_str(&color).is_err()); + } + + #[test] + fn color_rgb() { + let color = String::from("rgb(255, 0, 0)"); + let parsed = ColorValue::from_str(&color); + assert!(parsed.is_ok()); + assert_eq!(parsed.unwrap(), ColorValue::Rgb(255, 0, 0)); + } + + #[test] + fn color_rgb_upper() { + let color = String::from("RGB(255, 0, 0)"); + let parsed = ColorValue::from_str(&color); + assert!(parsed.is_ok()); + assert_eq!(parsed.unwrap(), ColorValue::Rgb(255, 0, 0)); + } + + #[test] + fn color_rgb_no_space() { + let color = String::from("rgb(255,0,0)"); + let parsed = ColorValue::from_str(&color); + assert!(parsed.is_ok()); + assert_eq!(parsed.unwrap(), ColorValue::Rgb(255, 0, 0)); + } +} diff --git a/src/ui/menu.rs b/src/ui/menu.rs index 6fdc130..9798b16 100644 --- a/src/ui/menu.rs +++ b/src/ui/menu.rs @@ -382,6 +382,7 @@ impl Menu { mod tests { use super::*; use chrono::Utc; + use std::rc::Rc; fn create_menu(n_row: i32, n_col: i32, top_row: i32, selected: i32) -> Menu { let titles = vec![ @@ -409,7 +410,17 @@ mod tests { }); } - let panel = Panel::new("Episodes".to_string(), 1, n_row, n_col, 0, 0); + let panel = Panel::new( + "Episodes".to_string(), + 1, + Rc::new(crate::ui::colors::set_colors( + &crate::config::AppColors::default(), + )), + n_row, + n_col, + 0, + 0, + ); return Menu { panel: panel, header: None, diff --git a/src/ui/mock_panel.rs b/src/ui/mock_panel.rs index ecd6e5c..fecac59 100644 --- a/src/ui/mock_panel.rs +++ b/src/ui/mock_panel.rs @@ -1,4 +1,6 @@ -use super::ColorType; +use std::rc::Rc; + +use super::{ColorType, Colors}; use chrono::{DateTime, Utc}; /// Struct holding the raw data used for building the details panel. @@ -15,6 +17,7 @@ pub struct Details { pub struct Panel { pub window: Vec<(String, pancurses::chtype, ColorType)>, pub screen_pos: usize, + pub colors: Rc, pub title: String, pub n_row: i32, pub n_col: i32, @@ -24,6 +27,7 @@ impl Panel { pub fn new( title: String, screen_pos: usize, + colors: Rc, n_row: i32, n_col: i32, _start_y: i32, @@ -37,6 +41,7 @@ impl Panel { return Panel { window: panel_win, screen_pos: screen_pos, + colors: colors, title: title, n_row: n_row, n_col: n_col, diff --git a/src/ui/mod.rs b/src/ui/mod.rs index 6c93b69..707a11e 100644 --- a/src/ui/mod.rs +++ b/src/ui/mod.rs @@ -1,3 +1,4 @@ +use std::rc::Rc; use std::sync::mpsc; use std::thread; use std::time::Duration; @@ -6,7 +7,7 @@ use std::time::Duration; #[cfg_attr(test, path = "mock_panel.rs")] mod panel; -mod colors; +pub mod colors; mod menu; mod notification; mod popup; @@ -36,8 +37,6 @@ lazy_static! { /// Regex for finding more than two line breaks static ref RE_MULT_LINE_BREAKS: Regex = Regex::new(r"((\r\n)|\r|\n){3,}").expect("Regex error"); - - pub static ref COLORS: Colors = self::colors::set_colors(); } @@ -82,6 +81,7 @@ pub struct Ui<'a> { n_row: i32, n_col: i32, keymap: &'a Keybindings, + colors: Rc, podcast_menu: Menu, episode_menu: Menu, active_menu: ActiveMenu, @@ -158,6 +158,8 @@ impl<'a> Ui<'a> { // key codes stdscr.nodelay(true); // getch() will not wait for user input + let colors = Rc::new(self::colors::set_colors(&config.colors)); + let (n_row, n_col) = stdscr.get_max_yx(); let (pod_col, ep_col, det_col) = Self::calculate_sizes(n_col); @@ -169,16 +171,32 @@ impl<'a> Ui<'a> { None => LockVec::new(Vec::new()), }; - let podcast_panel = Panel::new("Podcasts".to_string(), 0, n_row - 1, pod_col, 0, 0); + let podcast_panel = Panel::new( + "Podcasts".to_string(), + 0, + colors.clone(), + n_row - 1, + pod_col, + 0, + 0, + ); let podcast_menu = Menu::new(podcast_panel, None, items); - let episode_panel = - Panel::new("Episodes".to_string(), 1, n_row - 1, ep_col, 0, pod_col - 1); + let episode_panel = Panel::new( + "Episodes".to_string(), + 1, + colors.clone(), + n_row - 1, + ep_col, + 0, + pod_col - 1, + ); 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( + colors.clone(), n_row - 1, det_col, 0, @@ -188,14 +206,15 @@ impl<'a> Ui<'a> { None }; - let notif_win = NotifWin::new(n_row, n_col); - let popup_win = PopupWin::new(&config.keybindings, n_row, n_col); + let notif_win = NotifWin::new(colors.clone(), n_row, n_col); + let popup_win = PopupWin::new(colors.clone(), &config.keybindings, n_row, n_col); return Ui { - stdscr, - n_row, - n_col, + stdscr: stdscr, + n_row: n_row, + n_col: n_col, keymap: &config.keybindings, + colors: colors, podcast_menu: podcast_menu, episode_menu: episode_menu, active_menu: ActiveMenu::PodcastMenu, @@ -398,6 +417,7 @@ impl<'a> Ui<'a> { } } else if det_col > 0 { self.details_panel = Some(Self::make_details_panel( + self.colors.clone(), n_row - 1, det_col, 0, @@ -795,8 +815,22 @@ impl<'a> Ui<'a> { } /// Create a details panel. - pub fn make_details_panel(n_row: i32, n_col: i32, start_y: i32, start_x: i32) -> Panel { - return Panel::new("Details".to_string(), 2, n_row, n_col, start_y, start_x); + pub fn make_details_panel( + colors: Rc, + n_row: i32, + n_col: i32, + start_y: i32, + start_x: i32, + ) -> Panel { + return Panel::new( + "Details".to_string(), + 2, + colors, + n_row, + n_col, + start_y, + start_x, + ); } /// Updates the details panel with information about the current diff --git a/src/ui/notification.rs b/src/ui/notification.rs index 28effb8..3dd3fef 100644 --- a/src/ui/notification.rs +++ b/src/ui/notification.rs @@ -1,6 +1,7 @@ +use std::rc::Rc; use std::time::{Duration, Instant}; -use super::{ColorType, COLORS}; +use super::{ColorType, Colors}; use pancurses::{Input, Window}; /// Holds details of a notification message. @@ -37,6 +38,7 @@ impl Notification { #[derive(Debug)] pub struct NotifWin { window: Window, + colors: Rc, total_rows: i32, total_cols: i32, msg_stack: Vec, @@ -46,10 +48,11 @@ pub struct NotifWin { impl NotifWin { /// Creates a new NotifWin. - pub fn new(total_rows: i32, total_cols: i32) -> Self { + pub fn new(colors: Rc, total_rows: i32, total_cols: i32) -> Self { let win = pancurses::newwin(1, total_cols, total_rows - 1, 0); return Self { window: win, + colors: colors, total_rows: total_rows, total_cols: total_cols, msg_stack: Vec::new(), @@ -98,8 +101,9 @@ impl NotifWin { // otherwise, there was a notification before but there // isn't now, so erase self.window.erase(); - self.window - .bkgdset(pancurses::ColorPair(COLORS.get(ColorType::Normal) as u8)); + self.window.bkgdset(pancurses::ColorPair( + self.colors.get(ColorType::Normal) as u8 + )); self.window.refresh(); self.current_msg = None; } @@ -192,8 +196,13 @@ impl NotifWin { self.window.addstr(¬if.message); if notif.error { - self.window - .mvchgat(0, 0, -1, pancurses::A_BOLD, COLORS.get(ColorType::Error)); + self.window.mvchgat( + 0, + 0, + -1, + pancurses::A_BOLD, + self.colors.get(ColorType::Error), + ); } self.window.refresh(); } @@ -246,8 +255,9 @@ impl NotifWin { ); oldwin.delwin(); - self.window - .bkgdset(pancurses::ColorPair(COLORS.get(ColorType::Normal) as u8)); + self.window.bkgdset(pancurses::ColorPair( + self.colors.get(ColorType::Normal) as u8 + )); if let Some(curr) = &self.current_msg { self.display_notif(curr); } diff --git a/src/ui/panel.rs b/src/ui/panel.rs index 7d01eb3..2282ae9 100644 --- a/src/ui/panel.rs +++ b/src/ui/panel.rs @@ -1,7 +1,9 @@ +use std::rc::Rc; + use chrono::{DateTime, Utc}; use pancurses::{Attribute, Window}; -use super::{ColorType, COLORS}; +use super::{ColorType, Colors}; /// Struct holding the raw data used for building the details panel. pub struct Details { @@ -23,6 +25,7 @@ pub struct Details { pub struct Panel { window: Window, screen_pos: usize, + colors: Rc, title: String, n_row: i32, n_col: i32, @@ -33,6 +36,7 @@ impl Panel { pub fn new( title: String, screen_pos: usize, + colors: Rc, n_row: i32, n_col: i32, start_y: i32, @@ -43,6 +47,7 @@ impl Panel { return Panel { window: panel_win, screen_pos: screen_pos, + colors: colors, title: title, n_row: n_row, n_col: n_col, @@ -56,8 +61,9 @@ impl Panel { /// Redraws borders and refreshes the window to display on terminal. pub fn refresh(&self) { - self.window - .bkgdset(pancurses::ColorPair(COLORS.get(ColorType::Normal) as u8)); + self.window.bkgdset(pancurses::ColorPair( + self.colors.get(ColorType::Normal) as u8 + )); self.draw_border(); self.window.refresh(); } @@ -94,8 +100,9 @@ impl Panel { /// not refresh the screen. pub fn erase(&self) { self.window.erase(); - self.window - .bkgdset(pancurses::ColorPair(COLORS.get(ColorType::Normal) as u8)); + self.window.bkgdset(pancurses::ColorPair( + self.colors.get(ColorType::Normal) as u8 + )); self.draw_border(); } @@ -221,7 +228,7 @@ impl Panel { self.abs_x(x), nchars, attr, - COLORS.get(color), + self.colors.get(color), ); } diff --git a/src/ui/popup.rs b/src/ui/popup.rs index fae5cff..44400d0 100644 --- a/src/ui/popup.rs +++ b/src/ui/popup.rs @@ -1,7 +1,8 @@ use pancurses::Input; use std::cmp::min; +use std::rc::Rc; -use super::ColorType; +use super::{ColorType, Colors}; use super::{Menu, Panel, UiMsg}; use crate::config::BIG_SCROLL_AMOUNT; use crate::keymap::{Keybindings, UserAction}; @@ -39,6 +40,7 @@ impl ActivePopup { pub struct PopupWin<'a> { popup: ActivePopup, new_episodes: Vec, + colors: Rc, keymap: &'a Keybindings, total_rows: i32, total_cols: i32, @@ -49,10 +51,16 @@ pub struct PopupWin<'a> { impl<'a> PopupWin<'a> { /// Set up struct for handling popup windows. - pub fn new(keymap: &'a Keybindings, total_rows: i32, total_cols: i32) -> Self { + pub fn new( + colors: Rc, + keymap: &'a Keybindings, + total_rows: i32, + total_cols: i32, + ) -> Self { return Self { popup: ActivePopup::None, new_episodes: Vec::new(), + colors: colors, keymap: keymap, total_rows: total_rows, total_cols: total_cols, @@ -123,6 +131,7 @@ impl<'a> PopupWin<'a> { let mut welcome_win = Panel::new( "Shellcaster".to_string(), 0, + self.colors.clone(), self.total_rows - 1, self.total_cols, 0, @@ -207,6 +216,7 @@ impl<'a> PopupWin<'a> { let mut help_win = Panel::new( "Help".to_string(), 0, + self.colors.clone(), self.total_rows - 1, self.total_cols, 0, @@ -279,6 +289,7 @@ impl<'a> PopupWin<'a> { let mut download_panel = Panel::new( "New episodes".to_string(), 0, + self.colors.clone(), self.total_rows - 1, self.total_cols, 0, From 01913fad82eca102262bd73978b02fc1710ae3ee Mon Sep 17 00:00:00 2001 From: Jeff Hughes Date: Mon, 10 May 2021 08:04:31 -0400 Subject: [PATCH 17/23] Fix issues with setting background colors of windows --- config.toml | 2 +- src/ui/menu.rs | 2 +- src/ui/mod.rs | 6 ++++++ src/ui/notification.rs | 9 +++++++++ src/ui/panel.rs | 7 +------ 5 files changed, 18 insertions(+), 8 deletions(-) diff --git a/config.toml b/config.toml index 1fec78e..25fb11d 100644 --- a/config.toml +++ b/config.toml @@ -110,7 +110,7 @@ quit = [ "q" ] # is a value between 0 and 255. # all regular text -normal_foreground = "rgb(173, 173, 173)" +normal_foreground = "white" normal_background = "black" # colors for the currently selected podcast/episode diff --git a/src/ui/menu.rs b/src/ui/menu.rs index 9798b16..56bd011 100644 --- a/src/ui/menu.rs +++ b/src/ui/menu.rs @@ -52,7 +52,7 @@ impl Menu { /// Prints the list of visible items to the pancurses window and /// refreshes it. pub fn init(&mut self) { - self.panel.init(); + self.panel.refresh(); self.update_items(); } diff --git a/src/ui/mod.rs b/src/ui/mod.rs index 707a11e..847d2f4 100644 --- a/src/ui/mod.rs +++ b/src/ui/mod.rs @@ -231,8 +231,14 @@ impl<'a> Ui<'a> { self.podcast_menu.init(); self.podcast_menu.activate(); self.episode_menu.init(); + + if let Some(ref panel) = self.details_panel { + panel.refresh(); + } self.update_details_panel(); + self.notif_win.init(); + // welcome screen if user does not have any podcasts yet if self.podcast_menu.items.is_empty() { self.popup_win.spawn_welcome_win(); diff --git a/src/ui/notification.rs b/src/ui/notification.rs index 3dd3fef..bd05810 100644 --- a/src/ui/notification.rs +++ b/src/ui/notification.rs @@ -61,6 +61,15 @@ impl NotifWin { }; } + /// Initiates the window -- primarily, sets the background on the + /// window. + pub fn init(&mut self) { + self.window.bkgd(pancurses::ColorPair( + self.colors.get(ColorType::Normal) as u8 + )); + self.window.refresh(); + } + /// Checks if the current notification needs to be changed, and /// updates the message window accordingly. pub fn check_notifs(&mut self) { diff --git a/src/ui/panel.rs b/src/ui/panel.rs index 2282ae9..24d9974 100644 --- a/src/ui/panel.rs +++ b/src/ui/panel.rs @@ -54,14 +54,9 @@ impl Panel { }; } - /// Initiates the menu -- primarily, draws borders on the window. - pub fn init(&self) { - self.draw_border(); - } - /// Redraws borders and refreshes the window to display on terminal. pub fn refresh(&self) { - self.window.bkgdset(pancurses::ColorPair( + self.window.bkgd(pancurses::ColorPair( self.colors.get(ColorType::Normal) as u8 )); self.draw_border(); From 7e75e0e2a328b1f10df45338dca957fda7b27de2 Mon Sep 17 00:00:00 2001 From: Jeff Hughes Date: Mon, 10 May 2021 09:03:09 -0400 Subject: [PATCH 18/23] Remove the Colors hashmap I honestly don't know why I ever thought I needed it in the first place...just one of those things you implement early on and never go back and ask yourself why you did it that way. Anyway, it's now an enum. --- src/ui/colors.rs | 60 +++++++++++------------------------------- src/ui/menu.rs | 13 +-------- src/ui/mock_panel.rs | 9 +------ src/ui/mod.rs | 52 +++++++----------------------------- src/ui/notification.rs | 31 +++++++--------------- src/ui/panel.rs | 26 +++++------------- src/ui/popup.rs | 15 ++--------- 7 files changed, 45 insertions(+), 161 deletions(-) diff --git a/src/ui/colors.rs b/src/ui/colors.rs index 862e3a5..0012723 100644 --- a/src/ui/colors.rs +++ b/src/ui/colors.rs @@ -1,5 +1,4 @@ use anyhow::{anyhow, Result}; -use std::collections::HashMap; use lazy_static::lazy_static; use regex::Regex; @@ -106,43 +105,20 @@ impl ColorValue { /// Enum identifying relevant text states that will be associated with /// distinct colors. -#[derive(Eq, PartialEq, Hash, Copy, Clone, Debug)] +#[derive(Debug, Copy, Clone)] +#[repr(u8)] pub enum ColorType { - Normal, - Highlighted, - HighlightedActive, - Error, + // Colorpair 0 is reserved in ncurses for white text on black, and + // can't be changed, so we just skip it + Normal = 1, + Highlighted = 2, + HighlightedActive = 3, + Error = 4, } -/// Keeps a hashmap associating ColorTypes with ncurses color pairs. -#[derive(Debug, Clone)] -pub struct Colors(HashMap); - -impl Colors { - pub fn new() -> Self { - return Self(HashMap::new()); - } - - pub fn insert(&mut self, color: ColorType, num: i16) { - self.0.insert(color, num); - } - - pub fn get(&self, color: ColorType) -> i16 { - return *self.0.get(&color).expect("Error retrieving color type."); - } -} - - /// Sets up hashmap for ColorTypes in app, initiates color palette, and /// sets up ncurses color pairs. -pub fn set_colors(config: &AppColors) -> Colors { - // set up a hashmap for easier reference - let mut colors = Colors::new(); - colors.insert(ColorType::Normal, 0); - colors.insert(ColorType::Highlighted, 1); - colors.insert(ColorType::HighlightedActive, 2); - colors.insert(ColorType::Error, 3); - +pub fn set_colors(config: &AppColors) { // if the user has specified any colors to be "terminal" (i.e., to // use their terminal's default foreground and background colors), // then we must tell ncurses to allow the use of those colors. @@ -155,24 +131,18 @@ pub fn set_colors(config: &AppColors) -> Colors { // let replace_color_order = vec![ColorValue::Cyan, ColorValue::Magenta, ColorValue::Blue, ColorValue::Green, ColorValue::Yellow, ColorValue::Red, ColorValue::Black, ColorValue::White]; // } let mut replace_counter = 8; + replace_counter = set_color_pair(ColorType::Normal as u8, &config.normal, replace_counter); replace_counter = set_color_pair( - colors.get(ColorType::Normal), - &config.normal, - replace_counter, - ); - replace_counter = set_color_pair( - colors.get(ColorType::HighlightedActive), + ColorType::HighlightedActive as u8, &config.highlighted_active, replace_counter, ); replace_counter = set_color_pair( - colors.get(ColorType::Highlighted), + ColorType::Highlighted as u8, &config.highlighted, replace_counter, ); - let _ = set_color_pair(colors.get(ColorType::Error), &config.error, replace_counter); - - return colors; + let _ = set_color_pair(ColorType::Error as u8, &config.error, replace_counter); } /// Check for any app colors that are set to "Terminal", which means that @@ -211,7 +181,7 @@ fn check_for_terminal(app_colors: &AppColors) -> bool { /// and background colors, initiates customized colors if necessary, and /// adds the pair to ncurses with the key of `pair_index`. fn set_color_pair( - pair_index: i16, + pair_index: u8, config: &(ColorValue, ColorValue), mut replace_index: i16, ) -> i16 { @@ -241,7 +211,7 @@ fn set_color_pair( replace_index += 1; } - pancurses::init_pair(pair_index, c1.unwrap(), c2.unwrap()); + pancurses::init_pair(pair_index as i16, c1.unwrap(), c2.unwrap()); return replace_index; } diff --git a/src/ui/menu.rs b/src/ui/menu.rs index 56bd011..bf4f559 100644 --- a/src/ui/menu.rs +++ b/src/ui/menu.rs @@ -382,7 +382,6 @@ impl Menu { mod tests { use super::*; use chrono::Utc; - use std::rc::Rc; fn create_menu(n_row: i32, n_col: i32, top_row: i32, selected: i32) -> Menu { let titles = vec![ @@ -410,17 +409,7 @@ mod tests { }); } - let panel = Panel::new( - "Episodes".to_string(), - 1, - Rc::new(crate::ui::colors::set_colors( - &crate::config::AppColors::default(), - )), - n_row, - n_col, - 0, - 0, - ); + let panel = Panel::new("Episodes".to_string(), 1, n_row, n_col, 0, 0); return Menu { panel: panel, header: None, diff --git a/src/ui/mock_panel.rs b/src/ui/mock_panel.rs index fecac59..3488da2 100644 --- a/src/ui/mock_panel.rs +++ b/src/ui/mock_panel.rs @@ -1,6 +1,4 @@ -use std::rc::Rc; - -use super::{ColorType, Colors}; +use super::ColorType; use chrono::{DateTime, Utc}; /// Struct holding the raw data used for building the details panel. @@ -17,7 +15,6 @@ pub struct Details { pub struct Panel { pub window: Vec<(String, pancurses::chtype, ColorType)>, pub screen_pos: usize, - pub colors: Rc, pub title: String, pub n_row: i32, pub n_col: i32, @@ -27,7 +24,6 @@ impl Panel { pub fn new( title: String, screen_pos: usize, - colors: Rc, n_row: i32, n_col: i32, _start_y: i32, @@ -41,15 +37,12 @@ impl Panel { return Panel { window: panel_win, screen_pos: screen_pos, - colors: colors, title: title, n_row: n_row, n_col: n_col, }; } - pub fn init(&self) {} - pub fn refresh(&self) {} pub fn erase(&mut self) { diff --git a/src/ui/mod.rs b/src/ui/mod.rs index 847d2f4..4555a55 100644 --- a/src/ui/mod.rs +++ b/src/ui/mod.rs @@ -1,4 +1,3 @@ -use std::rc::Rc; use std::sync::mpsc; use std::thread; use std::time::Duration; @@ -12,7 +11,7 @@ mod menu; mod notification; mod popup; -use self::colors::{ColorType, Colors}; +use self::colors::ColorType; use self::menu::Menu; use self::notification::NotifWin; use self::panel::{Details, Panel}; @@ -81,7 +80,6 @@ pub struct Ui<'a> { n_row: i32, n_col: i32, keymap: &'a Keybindings, - colors: Rc, podcast_menu: Menu, episode_menu: Menu, active_menu: ActiveMenu, @@ -158,7 +156,7 @@ impl<'a> Ui<'a> { // key codes stdscr.nodelay(true); // getch() will not wait for user input - let colors = Rc::new(self::colors::set_colors(&config.colors)); + self::colors::set_colors(&config.colors); let (n_row, n_col) = stdscr.get_max_yx(); let (pod_col, ep_col, det_col) = Self::calculate_sizes(n_col); @@ -171,32 +169,16 @@ impl<'a> Ui<'a> { None => LockVec::new(Vec::new()), }; - let podcast_panel = Panel::new( - "Podcasts".to_string(), - 0, - colors.clone(), - n_row - 1, - pod_col, - 0, - 0, - ); + let podcast_panel = Panel::new("Podcasts".to_string(), 0, n_row - 1, pod_col, 0, 0); let podcast_menu = Menu::new(podcast_panel, None, items); - let episode_panel = Panel::new( - "Episodes".to_string(), - 1, - colors.clone(), - n_row - 1, - ep_col, - 0, - pod_col - 1, - ); + let episode_panel = + Panel::new("Episodes".to_string(), 1, n_row - 1, ep_col, 0, pod_col - 1); 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( - colors.clone(), n_row - 1, det_col, 0, @@ -206,15 +188,14 @@ impl<'a> Ui<'a> { None }; - let notif_win = NotifWin::new(colors.clone(), n_row, n_col); - let popup_win = PopupWin::new(colors.clone(), &config.keybindings, n_row, n_col); + let notif_win = NotifWin::new(n_row, n_col); + let popup_win = PopupWin::new(&config.keybindings, n_row, n_col); return Ui { stdscr: stdscr, n_row: n_row, n_col: n_col, keymap: &config.keybindings, - colors: colors, podcast_menu: podcast_menu, episode_menu: episode_menu, active_menu: ActiveMenu::PodcastMenu, @@ -423,7 +404,6 @@ impl<'a> Ui<'a> { } } else if det_col > 0 { self.details_panel = Some(Self::make_details_panel( - self.colors.clone(), n_row - 1, det_col, 0, @@ -821,22 +801,8 @@ impl<'a> Ui<'a> { } /// Create a details panel. - pub fn make_details_panel( - colors: Rc, - n_row: i32, - n_col: i32, - start_y: i32, - start_x: i32, - ) -> Panel { - return Panel::new( - "Details".to_string(), - 2, - colors, - n_row, - n_col, - start_y, - start_x, - ); + pub fn make_details_panel(n_row: i32, n_col: i32, start_y: i32, start_x: i32) -> Panel { + return Panel::new("Details".to_string(), 2, n_row, n_col, start_y, start_x); } /// Updates the details panel with information about the current diff --git a/src/ui/notification.rs b/src/ui/notification.rs index bd05810..bb99fa4 100644 --- a/src/ui/notification.rs +++ b/src/ui/notification.rs @@ -1,7 +1,6 @@ -use std::rc::Rc; use std::time::{Duration, Instant}; -use super::{ColorType, Colors}; +use super::ColorType; use pancurses::{Input, Window}; /// Holds details of a notification message. @@ -38,7 +37,6 @@ impl Notification { #[derive(Debug)] pub struct NotifWin { window: Window, - colors: Rc, total_rows: i32, total_cols: i32, msg_stack: Vec, @@ -48,11 +46,10 @@ pub struct NotifWin { impl NotifWin { /// Creates a new NotifWin. - pub fn new(colors: Rc, total_rows: i32, total_cols: i32) -> Self { + pub fn new(total_rows: i32, total_cols: i32) -> Self { let win = pancurses::newwin(1, total_cols, total_rows - 1, 0); return Self { window: win, - colors: colors, total_rows: total_rows, total_cols: total_cols, msg_stack: Vec::new(), @@ -64,9 +61,8 @@ impl NotifWin { /// Initiates the window -- primarily, sets the background on the /// window. pub fn init(&mut self) { - self.window.bkgd(pancurses::ColorPair( - self.colors.get(ColorType::Normal) as u8 - )); + self.window + .bkgd(pancurses::ColorPair(ColorType::Normal as u8)); self.window.refresh(); } @@ -110,9 +106,8 @@ impl NotifWin { // otherwise, there was a notification before but there // isn't now, so erase self.window.erase(); - self.window.bkgdset(pancurses::ColorPair( - self.colors.get(ColorType::Normal) as u8 - )); + self.window + .bkgdset(pancurses::ColorPair(ColorType::Normal as u8)); self.window.refresh(); self.current_msg = None; } @@ -205,13 +200,8 @@ impl NotifWin { self.window.addstr(¬if.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, ColorType::Error as i16); } self.window.refresh(); } @@ -264,9 +254,8 @@ impl NotifWin { ); oldwin.delwin(); - self.window.bkgdset(pancurses::ColorPair( - self.colors.get(ColorType::Normal) as u8 - )); + self.window + .bkgdset(pancurses::ColorPair(ColorType::Normal as u8)); if let Some(curr) = &self.current_msg { self.display_notif(curr); } diff --git a/src/ui/panel.rs b/src/ui/panel.rs index 24d9974..cbd2236 100644 --- a/src/ui/panel.rs +++ b/src/ui/panel.rs @@ -1,9 +1,7 @@ -use std::rc::Rc; - use chrono::{DateTime, Utc}; use pancurses::{Attribute, Window}; -use super::{ColorType, Colors}; +use super::ColorType; /// Struct holding the raw data used for building the details panel. pub struct Details { @@ -25,7 +23,6 @@ pub struct Details { pub struct Panel { window: Window, screen_pos: usize, - colors: Rc, title: String, n_row: i32, n_col: i32, @@ -36,7 +33,6 @@ impl Panel { pub fn new( title: String, screen_pos: usize, - colors: Rc, n_row: i32, n_col: i32, start_y: i32, @@ -47,7 +43,6 @@ impl Panel { return Panel { window: panel_win, screen_pos: screen_pos, - colors: colors, title: title, n_row: n_row, n_col: n_col, @@ -56,9 +51,8 @@ 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.window + .bkgd(pancurses::ColorPair(ColorType::Normal as u8)); self.draw_border(); self.window.refresh(); } @@ -95,9 +89,8 @@ impl Panel { /// not refresh the screen. pub fn erase(&self) { self.window.erase(); - self.window.bkgdset(pancurses::ColorPair( - self.colors.get(ColorType::Normal) as u8 - )); + self.window + .bkgdset(pancurses::ColorPair(ColorType::Normal as u8)); self.draw_border(); } @@ -218,13 +211,8 @@ impl Panel { attr: pancurses::chtype, color: ColorType, ) { - self.window.mvchgat( - self.abs_y(y), - self.abs_x(x), - nchars, - attr, - self.colors.get(color), - ); + self.window + .mvchgat(self.abs_y(y), self.abs_x(x), nchars, attr, color as i16); } /// Updates window size diff --git a/src/ui/popup.rs b/src/ui/popup.rs index 44400d0..fae5cff 100644 --- a/src/ui/popup.rs +++ b/src/ui/popup.rs @@ -1,8 +1,7 @@ use pancurses::Input; use std::cmp::min; -use std::rc::Rc; -use super::{ColorType, Colors}; +use super::ColorType; use super::{Menu, Panel, UiMsg}; use crate::config::BIG_SCROLL_AMOUNT; use crate::keymap::{Keybindings, UserAction}; @@ -40,7 +39,6 @@ impl ActivePopup { pub struct PopupWin<'a> { popup: ActivePopup, new_episodes: Vec, - colors: Rc, keymap: &'a Keybindings, total_rows: i32, total_cols: i32, @@ -51,16 +49,10 @@ pub struct PopupWin<'a> { impl<'a> PopupWin<'a> { /// Set up struct for handling popup windows. - pub fn new( - colors: Rc, - keymap: &'a Keybindings, - total_rows: i32, - total_cols: i32, - ) -> Self { + pub fn new(keymap: &'a Keybindings, total_rows: i32, total_cols: i32) -> Self { return Self { popup: ActivePopup::None, new_episodes: Vec::new(), - colors: colors, keymap: keymap, total_rows: total_rows, total_cols: total_cols, @@ -131,7 +123,6 @@ impl<'a> PopupWin<'a> { let mut welcome_win = Panel::new( "Shellcaster".to_string(), 0, - self.colors.clone(), self.total_rows - 1, self.total_cols, 0, @@ -216,7 +207,6 @@ impl<'a> PopupWin<'a> { let mut help_win = Panel::new( "Help".to_string(), 0, - self.colors.clone(), self.total_rows - 1, self.total_cols, 0, @@ -289,7 +279,6 @@ impl<'a> PopupWin<'a> { let mut download_panel = Panel::new( "New episodes".to_string(), 0, - self.colors.clone(), self.total_rows - 1, self.total_cols, 0, From 7279b9bfafc13881623f33106392503085eb44e2 Mon Sep 17 00:00:00 2001 From: Jeff Hughes Date: Tue, 11 May 2021 17:21:10 -0400 Subject: [PATCH 19/23] Handle cases with no colors, or no customizable ones --- src/ui/colors.rs | 94 ++++++++++++++++++++++++++++++++++++------------ src/ui/mod.rs | 1 - 2 files changed, 71 insertions(+), 24 deletions(-) diff --git a/src/ui/colors.rs b/src/ui/colors.rs index 0012723..aece3fe 100644 --- a/src/ui/colors.rs +++ b/src/ui/colors.rs @@ -119,30 +119,78 @@ pub enum ColorType { /// Sets up hashmap for ColorTypes in app, initiates color palette, and /// sets up ncurses color pairs. pub fn set_colors(config: &AppColors) { - // if the user has specified any colors to be "terminal" (i.e., to - // use their terminal's default foreground and background colors), - // then we must tell ncurses to allow the use of those colors. - if check_for_terminal(config) { - pancurses::use_default_colors(); - } + pancurses::start_color(); // allows colours if available + if pancurses::has_colors() { + // if the user has specified any colors to be "terminal" (i.e., + // to use their terminal's default foreground and background + // colors), then we must tell ncurses to allow the use of those + // colors. + if check_for_terminal(config) { + pancurses::use_default_colors(); + } - // check if we have any RGB-specified values - // if count_app_colors(config, ColorValue::Rgb(0, 0, 0)) > 0 { - // let replace_color_order = vec![ColorValue::Cyan, ColorValue::Magenta, ColorValue::Blue, ColorValue::Green, ColorValue::Yellow, ColorValue::Red, ColorValue::Black, ColorValue::White]; - // } - let mut replace_counter = 8; - replace_counter = set_color_pair(ColorType::Normal as u8, &config.normal, replace_counter); - replace_counter = set_color_pair( - ColorType::HighlightedActive as u8, - &config.highlighted_active, - replace_counter, - ); - replace_counter = set_color_pair( - ColorType::Highlighted as u8, - &config.highlighted, - replace_counter, - ); - let _ = set_color_pair(ColorType::Error as u8, &config.error, replace_counter); + if pancurses::can_change_color() { + // set customized colors + let mut replace_counter = 10; + replace_counter = + set_color_pair(ColorType::Normal as u8, &config.normal, replace_counter); + replace_counter = set_color_pair( + ColorType::HighlightedActive as u8, + &config.highlighted_active, + replace_counter, + ); + replace_counter = set_color_pair( + ColorType::Highlighted as u8, + &config.highlighted, + replace_counter, + ); + let _ = set_color_pair(ColorType::Error as u8, &config.error, replace_counter); + } else { + // we have color, but we're limited to the built-in ones + pancurses::init_pair( + ColorType::Normal as i16, + pancurses::COLOR_WHITE, + pancurses::COLOR_BLACK, + ); + pancurses::init_pair( + ColorType::HighlightedActive as i16, + pancurses::COLOR_BLACK, + pancurses::COLOR_YELLOW, + ); + pancurses::init_pair( + ColorType::Highlighted as i16, + pancurses::COLOR_BLACK, + pancurses::COLOR_WHITE, + ); + pancurses::init_pair( + ColorType::Error as i16, + pancurses::COLOR_RED, + pancurses::COLOR_BLACK, + ); + } + } else { + // cap'n, we got no color! + pancurses::init_pair( + ColorType::Normal as i16, + pancurses::COLOR_WHITE, + pancurses::COLOR_BLACK, + ); + pancurses::init_pair( + ColorType::HighlightedActive as i16, + pancurses::COLOR_BLACK, + pancurses::COLOR_WHITE, + ); + pancurses::init_pair( + ColorType::Highlighted as i16, + pancurses::COLOR_BLACK, + pancurses::COLOR_WHITE, + ); + pancurses::init_pair( + ColorType::Error as i16, + pancurses::COLOR_WHITE, + pancurses::COLOR_BLACK, + ); + } } /// Check for any app colors that are set to "Terminal", which means that diff --git a/src/ui/mod.rs b/src/ui/mod.rs index 4555a55..17809c6 100644 --- a/src/ui/mod.rs +++ b/src/ui/mod.rs @@ -150,7 +150,6 @@ impl<'a> Ui<'a> { 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 From 275aee9a488466d242b56a3a896133fcf892fa8f Mon Sep 17 00:00:00 2001 From: Jeff Hughes Date: Tue, 11 May 2021 17:23:47 -0400 Subject: [PATCH 20/23] Upgrade textwrap and regex dependencies Textwrap now has a new algorithm that can work better on short lines of text. --- Cargo.lock | 29 ++++++++++++++++++----------- Cargo.toml | 4 ++-- src/ui/mock_panel.rs | 2 +- src/ui/panel.rs | 2 +- 4 files changed, 22 insertions(+), 15 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index ec23b22..0b193c3 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -2,9 +2,9 @@ # It is not intended for manual editing. [[package]] name = "aho-corasick" -version = "0.7.15" +version = "0.7.18" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7404febffaa47dac81aa44dba71523c9d069b1bdc50a77db41195149e17f68e5" +checksum = "1e37cfd5e7657ada45f742d6e99ca5788580b5c529dc78faf11ece6dc702656f" dependencies = [ "memchr", ] @@ -397,9 +397,9 @@ checksum = "7ffc5c5338469d4d3ea17d269fa8ea3512ad247247c30bd2df69e68309ed0a08" [[package]] name = "memchr" -version = "2.3.4" +version = "2.4.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0ee1c47aaa256ecabcaea351eae4a9b01ef39ed810004e298d2511ed284b1525" +checksum = "b16bd47d9e329435e309c58469fe0791c2d0d1ba96ec0954152a5ae2b04387dc" [[package]] name = "native-tls" @@ -644,9 +644,9 @@ dependencies = [ [[package]] name = "regex" -version = "1.4.6" +version = "1.5.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2a26af418b574bd56588335b3a3659a65725d4e636eb1016c2f9e3b38c7cc759" +checksum = "d07a8629359eb56f1e2fb1652bb04212c072a87ba68546a04065d525673ac461" dependencies = [ "aho-corasick", "memchr", @@ -655,9 +655,9 @@ dependencies = [ [[package]] name = "regex-syntax" -version = "0.6.23" +version = "0.6.25" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "24d5f089152e60f62d28b835fbff2cd2e8dc0baf1ac13343bef92ab7eed84548" +checksum = "f497285884f3fcff424ffc933e56d7cbca511def0c9831a7f9b5f6153e3cc89b" [[package]] name = "remove_dir_all" @@ -830,7 +830,7 @@ dependencies = [ "semver", "serde", "shellexpand", - "textwrap 0.12.1", + "textwrap 0.13.4", "toml", "unicode-segmentation", "ureq", @@ -845,6 +845,12 @@ dependencies = [ "dirs-next", ] +[[package]] +name = "smawk" +version = "0.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f67ad224767faa3c7d8b6d91985b78e70a1324408abcb1cfcc2be4c06bc06043" + [[package]] name = "spin" version = "0.5.2" @@ -923,10 +929,11 @@ dependencies = [ [[package]] name = "textwrap" -version = "0.12.1" +version = "0.13.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "203008d98caf094106cfaba70acfed15e18ed3ddb7d94e49baec153a2b462789" +checksum = "cd05616119e612a8041ef58f2b578906cc2531a6069047ae092cfb86a325d835" dependencies = [ + "smawk", "unicode-width", ] diff --git a/Cargo.toml b/Cargo.toml index cd461f9..f9f420e 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -25,14 +25,14 @@ anyhow = "1.0.40" serde = { version = "1.0.125", features = ["derive"] } chrono = "0.4.11" lazy_static = "1.4.0" -regex = "1.4.6" +regex = "1.5.4" sanitize-filename = "0.2.1" shellexpand = "2.1.0" dirs = { package = "dirs-next", version = "2.0.0" } opml = "0.2.4" nohash-hasher = "0.2.0" unicode-segmentation = "1.7.1" -textwrap = "0.12.1" +textwrap = "0.13.4" escaper = "0.1.0" semver = "0.10.0" diff --git a/src/ui/mock_panel.rs b/src/ui/mock_panel.rs index 3488da2..e227a53 100644 --- a/src/ui/mock_panel.rs +++ b/src/ui/mock_panel.rs @@ -71,7 +71,7 @@ impl Panel { pub fn write_wrap_line(&mut self, start_y: i32, string: &str) -> i32 { let mut row = start_y; let max_row = self.get_rows(); - let wrapper = textwrap::wrap_iter(&string, self.get_cols() as usize); + let wrapper = textwrap::wrap(&string, self.get_cols() as usize); for line in wrapper { self.write_line(row, line.to_string()); row += 1; diff --git a/src/ui/panel.rs b/src/ui/panel.rs index cbd2236..24e9186 100644 --- a/src/ui/panel.rs +++ b/src/ui/panel.rs @@ -124,7 +124,7 @@ impl Panel { pub fn write_wrap_line(&self, start_y: i32, string: &str) -> i32 { let mut row = start_y; let max_row = self.get_rows(); - let wrapper = textwrap::wrap_iter(string, self.get_cols() as usize); + let wrapper = textwrap::wrap(string, self.get_cols() as usize); for line in wrapper { self.window.mvaddstr(self.abs_y(row), self.abs_x(0), line); row += 1; From 69ca88d0b97ec41033e3d8d62855318ae0269d1c Mon Sep 17 00:00:00 2001 From: Jeff Hughes Date: Tue, 11 May 2021 17:26:05 -0400 Subject: [PATCH 21/23] Add note about support for colors --- config.toml | 3 +++ 1 file changed, 3 insertions(+) diff --git a/config.toml b/config.toml index 25fb11d..b0a9bea 100644 --- a/config.toml +++ b/config.toml @@ -108,6 +108,9 @@ quit = [ "q" ] # RGB values. # 3. Using an RGB value in the format "rgb(255, 0, 0)" where each number # is a value between 0 and 255. +# Note that, as might be expected, the ability to set colors depends on +# the capabilities of your terminal. Config options set below are ignored +# on terminals without the ability to add/change colors. # all regular text normal_foreground = "white" From 5ee8b27033df943582d951cde659b10a3e624ee4 Mon Sep 17 00:00:00 2001 From: Jeff Hughes Date: Tue, 11 May 2021 17:31:09 -0400 Subject: [PATCH 22/23] Update readme with info on customizing colors --- README.md | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/README.md b/README.md index 05a8343..137e099 100644 --- a/README.md +++ b/README.md @@ -197,6 +197,10 @@ The sample file above provides comments that should walk you through all the ava **Note:** Actions can be mapped to more than one key (e.g., "Enter" and "p" both play an episode), but a single key may not do more than one action (e.g., you can't set "d" to both download and delete episodes). +#### Customizable colors + +You can set the colors in the app with either built-in terminal colors or (provided your terminal supports it) customizable colors as well. See the "colors" section in the [config.toml](https://github.com/jeff-hughes/shellcaster/blob/master/config.toml) for details about how to specify these colors! + ## Syncing without the UI Some users may wish to sync their podcasts automatically on a regular basis, e.g., every morning. The `shellcaster sync` subcommand can be used to do this without opening up the UI, and does a full sync of all podcasts in the database. This could be used to set up a cron job or systemd timer, for example. Please refer to the relevant documentation for these systems for setting it up on the schedule of your choice. From 017c392f0c3ad0a3fcb142aa84594f5d99de2512 Mon Sep 17 00:00:00 2001 From: Jeff Hughes Date: Tue, 11 May 2021 17:35:47 -0400 Subject: [PATCH 23/23] Bump to v1.2.0 --- CHANGELOG.md | 16 ++++++++++++++++ Cargo.lock | 2 +- Cargo.toml | 2 +- 3 files changed, 18 insertions(+), 2 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index b4cd3c3..d870741 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,21 @@ # Changelog +## v1.2.0 (2021-05-11) +- Adds new keybindings for scrolling up/down a quarter of the page, + scrolling a full page up/down, and scrolling to the top or bottom + (thanks to contributor [a-kenji](https://github.com/a-kenji)) +- Adds support for customizable colors + - This is a backwards-compatible change and does not require any + modification; however, if you wish to customize the colors after + upgrading, you will need to [update your config.toml file](https://github.com/jeff-hughes/shellcaster/blob/master/config.toml) + to add the new options under the "colors" section +- Filenames of downloaded files now include the publication date, which + reduces potential conflicts with rebroadcasted episodes +- Bug fix: + - Fixed issue with "removed" episodes reappearing after syncing the + podcast again +- Some minor performance improvements, particularly when loading the app + ## v1.1.0 (2020-12-01) - Help menu showing the current keybindings (accessible by pressing "?" by default) diff --git a/Cargo.lock b/Cargo.lock index 0b193c3..f45d3d9 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -812,7 +812,7 @@ dependencies = [ [[package]] name = "shellcaster" -version = "1.1.0" +version = "1.2.0" dependencies = [ "anyhow", "chrono", diff --git a/Cargo.toml b/Cargo.toml index f9f420e..913882e 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "shellcaster" -version = "1.1.0" +version = "1.2.0" authors = ["Jeff Hughes "] edition = "2018" license = "GPL-3.0-or-later"