diff options
author | Thang Pham <phamducthang1234@gmail.com> | 2022-05-21 16:02:32 -0400 |
---|---|---|
committer | GitHub <noreply@github.com> | 2022-05-21 16:02:32 -0400 |
commit | 9c687657920aa4adad5df9fb2bff7d1a3616e4df (patch) | |
tree | fe707e963673873463287205650ae9ec63e920c3 | |
parent | 3829dcd237ee6a8dbbe95cf6d24993ca4eeb5048 (diff) |
Code refactor and clean part II (#75)
## Brief description of changes
- moved UI initialization codes from `main.rs` to `view::mod.rs`
- updated application's logging level
- renamed functions, added more comments/code documentations
- added some code cleanups
-rw-r--r-- | hackernews_tui/src/client/mod.rs | 3 | ||||
-rw-r--r-- | hackernews_tui/src/client/parser.rs | 3 | ||||
-rw-r--r-- | hackernews_tui/src/client/query.rs | 16 | ||||
-rw-r--r-- | hackernews_tui/src/config/mod.rs | 6 | ||||
-rw-r--r-- | hackernews_tui/src/main.rs | 108 | ||||
-rw-r--r-- | hackernews_tui/src/view/article_view.rs | 13 | ||||
-rw-r--r-- | hackernews_tui/src/view/async_view.rs | 14 | ||||
-rw-r--r-- | hackernews_tui/src/view/comment_view.rs | 142 | ||||
-rw-r--r-- | hackernews_tui/src/view/mod.rs | 125 | ||||
-rw-r--r-- | hackernews_tui/src/view/search_view.rs | 19 | ||||
-rw-r--r-- | hackernews_tui/src/view/story_view.rs | 46 | ||||
-rw-r--r-- | hackernews_tui/src/view/utils.rs | 2 |
12 files changed, 295 insertions, 202 deletions
diff --git a/hackernews_tui/src/client/mod.rs b/hackernews_tui/src/client/mod.rs index 5d4ad4d..d7d7e98 100644 --- a/hackernews_tui/src/client/mod.rs +++ b/hackernews_tui/src/client/mod.rs @@ -29,6 +29,7 @@ pub struct HNClient { client: ureq::Agent, } +/// A macro to log the runtime of an expression macro_rules! log { ($e:expr, $desc:expr) => {{ let time = std::time::SystemTime::now(); @@ -81,8 +82,6 @@ impl HNClient { // loads first 5 comments to ensure the corresponding `CommentView` has data to render self.load_comments(&sender, &mut ids, 5)?; - // used to test loading bar - // std::thread::sleep(std::time::Duration::from_secs(1000)); std::thread::spawn({ let client = self.clone(); let sleep_dur = std::time::Duration::from_millis(1000); diff --git a/hackernews_tui/src/client/parser.rs b/hackernews_tui/src/client/parser.rs index 0756ee4..fb76276 100644 --- a/hackernews_tui/src/client/parser.rs +++ b/hackernews_tui/src/client/parser.rs @@ -147,12 +147,13 @@ pub struct HnText { pub level: usize, pub state: CollapseState, pub text: StyledString, + /// The minimized version of the text used to display the text component when it's partially collapsed. pub minimized_text: StyledString, pub links: Vec<String>, } #[derive(Debug, Clone)] -/// The collapse state of a text component +/// The collapse state of a HN text component pub enum CollapseState { Collapsed, PartiallyCollapsed, diff --git a/hackernews_tui/src/client/query.rs b/hackernews_tui/src/client/query.rs index 26071b7..072b4e2 100644 --- a/hackernews_tui/src/client/query.rs +++ b/hackernews_tui/src/client/query.rs @@ -7,15 +7,15 @@ pub struct FilterInterval<T> { end: Option<T>, } -impl<T: std::fmt::Display + Copy> FilterInterval<T> { +impl<T: std::fmt::Display> FilterInterval<T> { pub fn query(&self, field: &str) -> String { format!( "{}{}", - match self.start { + match self.start.as_ref() { Some(x) => format!(",{}>={}", field, x), None => "".to_string(), }, - match self.end { + match self.end.as_ref() { Some(x) => format!(",{}<{}", field, x), None => "".to_string(), }, @@ -26,11 +26,11 @@ impl<T: std::fmt::Display + Copy> FilterInterval<T> { format!( "{}: [{}:{}]", field, - match self.start { + match self.start.as_ref() { Some(x) => x.to_string(), None => "".to_string(), }, - match self.end { + match self.end.as_ref() { Some(x) => x.to_string(), None => "".to_string(), } @@ -93,3 +93,9 @@ impl StoryNumericFilters { } } } + +impl std::fmt::Display for StoryNumericFilters { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + write!(f, "{}", self.desc()) + } +} diff --git a/hackernews_tui/src/config/mod.rs b/hackernews_tui/src/config/mod.rs index 0235749..b23f6ca 100644 --- a/hackernews_tui/src/config/mod.rs +++ b/hackernews_tui/src/config/mod.rs @@ -80,10 +80,8 @@ pub fn load_config(config_file_str: &str) { let config = match Config::from_config_file(&config_file) { Err(err) => { tracing::error!( - "failed to load configurations from the file {}: {} \ - \n...Use the default configurations instead", - config_file_str, - err + "failed to load configurations from the file {config_file_str}: {err:#}\ + \nUse the default configurations instead", ); Config::default() } diff --git a/hackernews_tui/src/main.rs b/hackernews_tui/src/main.rs index 73e40e3..10a43b5 100644 --- a/hackernews_tui/src/main.rs +++ b/hackernews_tui/src/main.rs @@ -10,112 +10,16 @@ const DEFAULT_LOG_FILE: &str = "hn-tui.log"; use clap::*; use prelude::*; -use view::help_view::HasHelpView; - -macro_rules! set_up_switch_view_shortcut { - ($key:expr,$tag:expr,$s:expr,$client:expr) => { - $s.set_on_post_event($key, move |s| { - view::story_view::add_story_view_layer( - s, - $client, - $tag, - true, - 0, - client::StoryNumericFilters::default(), - false, - ); - }); - }; -} - -fn set_up_global_callbacks(s: &mut Cursive, client: &'static client::HNClient) { - s.clear_global_callbacks(Event::CtrlChar('c')); - - let global_keymap = config::get_global_keymap().clone(); - - // ............................................................. - // global shortcuts for switching between different Story Views - // ............................................................. - - set_up_switch_view_shortcut!(global_keymap.goto_front_page_view, "front_page", s, client); - set_up_switch_view_shortcut!(global_keymap.goto_all_stories_view, "story", s, client); - set_up_switch_view_shortcut!(global_keymap.goto_ask_hn_view, "ask_hn", s, client); - set_up_switch_view_shortcut!(global_keymap.goto_show_hn_view, "show_hn", s, client); - set_up_switch_view_shortcut!(global_keymap.goto_jobs_view, "job", s, client); - - // custom navigation shortcuts - config::get_config() - .keymap - .custom_keymaps - .iter() - .for_each(|data| { - s.set_on_post_event(data.key.clone(), move |s| { - view::story_view::add_story_view_layer( - s, - client, - &data.tag, - data.by_date, - 0, - data.numeric_filters, - false, - ); - }); - }); - - // ............................................ - // end of navigation shortcuts for Story Views - // ............................................ - - s.set_on_post_event(global_keymap.goto_previous_view, |s| { - if s.screen_mut().len() > 1 { - s.pop_layer(); - } - }); - - s.set_on_post_event(global_keymap.goto_search_view, move |s| { - view::search_view::add_search_view_layer(s, client); - }); - - s.set_on_post_event(global_keymap.open_help_dialog, |s| { - s.add_layer(view::help_view::DefaultHelpView::construct_on_event_help_view()) - }); - - s.set_on_post_event(global_keymap.quit, |s| s.quit()); -} fn run() { - let mut s = cursive::default(); - - let theme = config::get_config_theme(); - s.update_theme(|t| { - t.palette.set_color("view", theme.palette.background.into()); - t.palette - .set_color("primary", theme.palette.foreground.into()); - t.palette - .set_color("title_primary", theme.palette.foreground.into()); - t.palette - .set_color("highlight", theme.palette.selection_background.into()); - t.palette - .set_color("highlight_text", theme.palette.selection_foreground.into()); - }); - // setup HN Client let client = client::init_client(); - set_up_global_callbacks(&mut s, client); - - // render `front_page` story view as the application's default view - view::story_view::add_story_view_layer( - &mut s, - client, - "front_page", - false, - 0, - client::StoryNumericFilters::default(), - false, - ); - // use `cursive_buffered_backend` to fix the flickering issue - // when using `cursive` with `crossterm_backend` (https://github.com/gyscos/Cursive/issues/142) + // setup the application's UI + let s = view::init_ui(client); + + // use `cursive_buffered_backend` crate to fix the flickering issue + // when using `cursive` with `crossterm_backend` (See https://github.com/gyscos/Cursive/issues/142) let crossterm_backend = backends::crossterm::Backend::init().unwrap(); let buffered_backend = Box::new(cursive_buffered_backend::BufferedBackend::new( crossterm_backend, @@ -128,7 +32,7 @@ fn run() { /// initialize application logging fn init_logging(log_dir_str: &str) { if std::env::var("RUST_LOG").is_err() { - std::env::set_var("RUST_LOG", "info") + std::env::set_var("RUST_LOG", "hackernews_tui=info") } let log_dir = std::path::PathBuf::from(log_dir_str); diff --git a/hackernews_tui/src/view/article_view.rs b/hackernews_tui/src/view/article_view.rs index 22b4339..79a3ca7 100644 --- a/hackernews_tui/src/view/article_view.rs +++ b/hackernews_tui/src/view/article_view.rs @@ -98,7 +98,7 @@ impl ScrollViewContainer for ArticleView { } } -pub fn get_article_main_view(article: client::Article) -> OnEventView<ArticleView> { +fn construct_article_main_view(article: client::Article) -> OnEventView<ArticleView> { let is_suffix_key = |c: &Event| -> bool { let article_view_keymap = config::get_article_view_keymap(); article_view_keymap.open_link_in_browser.has_event(c) @@ -158,9 +158,11 @@ pub fn get_article_main_view(article: client::Article) -> OnEventView<ArticleVie .on_scroll_events() } -pub fn get_article_view(article: client::Article) -> impl View { +/// Construct an article view of an article +pub fn construct_article_view(article: client::Article) -> impl View { let desc = format!("Article View - {}", article.title); - let main_view = get_article_main_view(article).full_height(); + let main_view = construct_article_main_view(article).full_height(); + let mut view = LinearLayout::vertical() .child(utils::construct_view_title_bar(&desc)) .child(main_view) @@ -171,7 +173,8 @@ pub fn get_article_view(article: client::Article) -> impl View { view } -pub fn add_article_view_layer(s: &mut Cursive, url: &str) { - let async_view = async_view::get_article_view_async(s, url); +/// Retrieve an article from a given `url` and construct an article view of that article +pub fn construct_and_add_new_article_view(s: &mut Cursive, url: &str) { + let async_view = async_view::construct_article_view_async(s, url); s.screen_mut().add_transparent_layer(Layer::new(async_view)) } diff --git a/hackernews_tui/src/view/async_view.rs b/hackernews_tui/src/view/async_view.rs index 4d9c680..709d950 100644 --- a/hackernews_tui/src/view/async_view.rs +++ b/hackernews_tui/src/view/async_view.rs @@ -4,7 +4,7 @@ use anyhow::Context; use cursive_aligned_view::Alignable; use cursive_async_view::AsyncView; -pub fn get_comment_view_async( +pub fn construct_comment_view_async( siv: &mut Cursive, client: &'static client::HNClient, story: &client::Story, @@ -16,7 +16,7 @@ pub fn get_comment_view_async( move |result: Result<_>| { ResultView::new( result.with_context(|| format!("failed to load comments from story (id={})", id)), - |receiver| comment_view::get_comment_view(&story, receiver), + |receiver| comment_view::construct_comment_view(&story, receiver), ) } }) @@ -25,7 +25,7 @@ pub fn get_comment_view_async( .full_screen() } -pub fn get_story_view_async( +pub fn construct_story_view_async( siv: &mut Cursive, client: &'static client::HNClient, tag: &'static str, @@ -40,12 +40,12 @@ pub fn get_story_view_async( ResultView::new( result.with_context(|| { format!( - "failed to get stories (tag={}, by_date={}, page={}, numeric_filters={:#?}):", + "failed to get stories (tag={}, by_date={}, page={}, numeric_filters={{{}}})", tag, by_date, page, numeric_filters, ) }), |stories| { - story_view::get_story_view(stories, client, tag, by_date, page, numeric_filters) + story_view::construct_story_view(stories, client, tag, by_date, page, numeric_filters) }, ) }, @@ -55,7 +55,7 @@ pub fn get_story_view_async( .full_screen() } -pub fn get_article_view_async(siv: &mut Cursive, article_url: &str) -> impl View { +pub fn construct_article_view_async(siv: &mut Cursive, article_url: &str) -> impl View { let err_context = format!( "Failed to execute the command:\n\ `{} {}`.\n\n\ @@ -73,7 +73,7 @@ pub fn get_article_view_async(siv: &mut Cursive, article_url: &str) -> impl View move |result| { let err_context = err_context.clone(); ResultView::new(result.with_context(|| err_context), |article| { - article_view::get_article_view(article) + article_view::construct_article_view(article) }) }, ) diff --git a/hackernews_tui/src/view/comment_view.rs b/hackernews_tui/src/view/comment_view.rs index 8e59448..925fa67 100644 --- a/hackernews_tui/src/view/comment_view.rs +++ b/hackernews_tui/src/view/comment_view.rs @@ -13,12 +13,16 @@ pub struct CommentView { raw_command: String, } +pub enum NavigationDirection { + Next, + Previous, +} + impl ViewWrapper for CommentView { wrap_impl!(self.view: ScrollView<LinearLayout>); } impl CommentView { - /// Return a new CommentView given a comment list and the discussed story url pub fn new(story_text: client::HnText, receiver: client::CommentReceiver) -> Self { let mut view = CommentView { view: LinearLayout::vertical() @@ -39,7 +43,7 @@ impl CommentView { view } - /// Check the `CommentReceiver` channel if there are new comments loaded + /// Check the comment receiver channel if there are new comments loaded /// then update the internal comment data accordingly. pub fn try_update_comments(&mut self) { let mut new_comments = vec![]; @@ -94,33 +98,31 @@ impl CommentView { &self, start_id: usize, max_level: usize, - direction: bool, + direction: NavigationDirection, ) -> usize { - if direction { - // -> - (start_id + 1..self.len()) + match direction { + NavigationDirection::Next => (start_id + 1..self.len()) .find(|&id| self.comments[id].level <= max_level) - .unwrap_or_else(|| self.len()) - } else { - // <- - (0..start_id) + .unwrap_or_else(|| self.len()), + NavigationDirection::Previous => (0..start_id) .rfind(|&id| self.comments[id].level <= max_level) - .unwrap_or(start_id) + .unwrap_or(start_id), } } - /// Return the id of the next visible comment - pub fn find_next_visible_comment(&self, start_id: usize, direction: bool) -> usize { - if direction { - // -> - (start_id + 1..self.len()) + /// Return the id of the next visible comment (`direction` dependent) + pub fn find_next_visible_comment( + &self, + start_id: usize, + direction: NavigationDirection, + ) -> usize { + match direction { + NavigationDirection::Next => (start_id + 1..self.len()) .find(|&id| self.get_comment_component(id).is_visible()) - .unwrap_or_else(|| self.len()) - } else { - // <- - (0..start_id) + .unwrap_or_else(|| self.len()), + NavigationDirection::Previous => (0..start_id) .rfind(|&id| self.get_comment_component(id).is_visible()) - .unwrap_or(start_id) + .unwrap_or(start_id), } } @@ -138,27 +140,29 @@ impl CommentView { .unwrap() } - /// Toggle the collapsing state of comments whose height is greater - /// than the `min_height`. - /// **Note** `PartiallyCollapsed` comment's state is unchanged, only toggle its visibility. - /// Also, the state and visibility of such comment's children are unaffected. - fn toggle_comment_collapse_state(&mut self, i: usize, min_height: usize) { - if i == self.len() || self.comments[i].level <= min_height { + /// Toggle the collapsing state of comments whose level is greater than the `min_level`. + fn toggle_comment_collapse_state(&mut self, start_id: usize, min_level: usize) { + // This function will be called recursively until it's unable to find any comments. + // + // **Note**: `PartiallyCollapsed` comment's state is unchanged, we only toggle its visibility. + // Also, the state and visibility of such comment's children are unaffected as they should already + // be in a collapsed state. + if start_id == self.len() || self.comments[start_id].level <= min_level { return; } - match self.comments[i].state { + match self.comments[start_id].state { client::CollapseState::Collapsed => { - self.comments[i].state = client::CollapseState::Normal; - self.get_comment_component_mut(i).unhide(); - self.toggle_comment_collapse_state(i + 1, min_height) + self.comments[start_id].state = client::CollapseState::Normal; + self.get_comment_component_mut(start_id).unhide(); + self.toggle_comment_collapse_state(start_id + 1, min_level) } client::CollapseState::Normal => { - self.comments[i].state = client::CollapseState::Collapsed; - self.get_comment_component_mut(i).hide(); - self.toggle_comment_collapse_state(i + 1, min_height) + self.comments[start_id].state = client::CollapseState::Collapsed; + self.get_comment_component_mut(start_id).hide(); + self.toggle_comment_collapse_state(start_id + 1, min_level) } client::CollapseState::PartiallyCollapsed => { - let component = self.get_comment_component_mut(i); + let component = self.get_comment_component_mut(start_id); if component.is_visible() { component.hide(); } else { @@ -166,8 +170,12 @@ impl CommentView { } // skip toggling all child comments of the current comment - let next_id = self.find_comment_id_by_max_level(i, self.comments[i].level, true); - self.toggle_comment_collapse_state(next_id, min_height) + let next_id = self.find_comment_id_by_max_level( + start_id, + self.comments[start_id].level, + NavigationDirection::Next, + ); + self.toggle_comment_collapse_state(next_id, min_level) } }; } @@ -179,7 +187,7 @@ impl CommentView { match comment.state { client::CollapseState::Collapsed => { panic!( - "invalid comment state `Collapsed` when calling `toggle_collapse_focused_comment`" + "invalid collapse state `Collapsed` when calling `toggle_collapse_focused_comment`" ); } client::CollapseState::PartiallyCollapsed => { @@ -233,7 +241,10 @@ impl ScrollViewContainer for CommentView { } } -fn get_comment_main_view(story: &client::Story, receiver: client::CommentReceiver) -> impl View { +fn construct_comment_main_view( + story: &client::Story, + receiver: client::CommentReceiver, +) -> impl View { let is_suffix_key = |c: &Event| -> bool { let comment_view_keymap = config::get_comment_view_keymap(); comment_view_keymap.open_link_in_browser.has_event(c) @@ -263,36 +274,48 @@ fn get_comment_main_view(story: &client::Story, receiver: client::CommentReceive }) // comment navigation shortcuts .on_pre_event_inner(comment_view_keymap.prev_comment, |s, _| { - s.set_focus_index(s.find_next_visible_comment(s.get_focus_index(), false)) + s.set_focus_index( + s.find_next_visible_comment(s.get_focus_index(), NavigationDirection::Previous), + ) }) .on_pre_event_inner(comment_view_keymap.next_comment, |s, _| { - let next_id = s.find_next_visible_comment(s.get_focus_index(), true); + let next_id = + s.find_next_visible_comment(s.get_focus_index(), NavigationDirection::Next); s.set_focus_index(next_id) }) .on_pre_event_inner(comment_view_keymap.next_leq_level_comment, move |s, _| { let id = s.get_focus_index(); - let next_id = s.find_comment_id_by_max_level(id, s.comments[id].level, true); + let next_id = + s.find_comment_id_by_max_level(id, s.comments[id].level, NavigationDirection::Next); s.set_focus_index(next_id) }) .on_pre_event_inner(comment_view_keymap.prev_leq_level_comment, move |s, _| { let id = s.get_focus_index(); - let next_id = s.find_comment_id_by_max_level(id, s.comments[id].level, false); + let next_id = s.find_comment_id_by_max_level( + id, + s.comments[id].level, + NavigationDirection::Previous, + ); s.set_focus_index(next_id) }) .on_pre_event_inner(comment_view_keymap.next_top_level_comment, move |s, _| { let id = s.get_focus_index(); - let next_id = s.find_comment_id_by_max_level(id, 0, true); + let next_id = s.find_comment_id_by_max_level(id, 0, NavigationDirection::Next); s.set_focus_index(next_id) }) .on_pre_event_inner(comment_view_keymap.prev_top_level_comment, move |s, _| { let id = s.get_focus_index(); - let next_id = s.find_comment_id_by_max_level(id, 0, false); + let next_id = s.find_comment_id_by_max_level(id, 0, NavigationDirection::Previous); s.set_focus_index(next_id) }) .on_pre_event_inner(comment_view_keymap.parent_comment, move |s, _| { let id = s.get_focus_index(); if s.comments[id].level > 0 { - let next_id = s.find_comment_id_by_max_level(id, s.comments[id].level - 1, false); + let next_id = s.find_comment_id_by_max_level( + id, + s.comments[id].level - 1, + NavigationDirection::Previous, + ); s.set_focus_index(next_id) } else { Some(EventResult::Consumed(None)) @@ -342,7 +365,7 @@ fn get_comment_main_view(story: &client::Story, receiver: client::CommentReceive let url = story.url.clone(); move |s| { if !url.is_empty() { - article_view::add_article_view_layer(s, &url) + article_view::construct_and_add_new_article_view(s, &url) } } }) @@ -359,14 +382,22 @@ fn get_comment_main_view(story: &client::Story, receiver: client::CommentReceive .full_height() } -pub fn get_comment_view(story: &client::Story, receiver: client::CommentReceiver) -> impl View { - let status_bar = - utils::construct_view_title_bar(&format!("Comment View - {}", story.title.source())); - - let main_view = get_comment_main_view(story, receiver); +/// Construct a comment view of a given story. +/// +/// # Arguments: +/// * `story`: a Hacker News story +/// * `receiver`: a "subscriber" channel that gets comments asynchronously from another thread +pub fn construct_comment_view( + story: &client::Story, + receiver: client::CommentReceiver, +) -> impl View { + let main_view = construct_comment_main_view(story, receiver); let mut view = LinearLayout::vertical() - .child(status_bar) + .child(utils::construct_view_title_bar(&format!( + "Comment View - {}", + story.title.source() + ))) .child(main_view) .child(utils::construct_footer_view::<CommentView>()); view.set_focus_index(1) @@ -375,13 +406,14 @@ pub fn get_comment_view(story: &client::Story, receiver: client::CommentReceiver view } -pub fn add_comment_view_layer( +/// Retrieve comments of a story and construct a comment view of that story +pub fn construct_and_add_new_comment_view( s: &mut Cursive, client: &'static client::HNClient, story: &client::Story, pop_layer: bool, ) { - let async_view = async_view::get_comment_view_async(s, client, story); + let async_view = async_view::construct_comment_view_async(s, client, story); if pop_layer { s.pop_layer(); } diff --git a/hackernews_tui/src/view/mod.rs b/hackernews_tui/src/view/mod.rs index cf3c66c..a41934d 100644 --- a/hackernews_tui/src/view/mod.rs +++ b/hackernews_tui/src/view/mod.rs @@ -11,3 +11,128 @@ pub mod comment_view; pub mod help_view; pub mod search_view; pub mod story_view; + +use crate::view::help_view::HasHelpView; + +use super::prelude::*; + +fn set_up_switch_story_view_shortcut( + keys: config::Keys, + tag: &'static str, + s: &mut Cursive, + client: &'static client::HNClient, + numeric_filters: Option<client::StoryNumericFilters>, +) { + s.set_on_post_event(keys, move |s| { + story_view::construct_and_add_new_story_view( + s, + client, + tag, + true, + 0, + numeric_filters.unwrap_or_default(), + false, + ); + }); +} + +fn set_up_global_callbacks(s: &mut Cursive, client: &'static client::HNClient) { + s.clear_global_callbacks(Event::CtrlChar('c')); + + let global_keymap = config::get_global_keymap().clone(); + + // ............................................................. + // global shortcuts for switching between different Story Views + // ............................................................. + + set_up_switch_story_view_shortcut( + global_keymap.goto_front_page_view, + "front_page", + s, + client, + None, + ); + set_up_switch_story_view_shortcut( + global_keymap.goto_all_stories_view, + "story", + s, + client, + None, + ); + set_up_switch_story_view_shortcut(global_keymap.goto_ask_hn_view, "ask_hn", s, client, None); + set_up_switch_story_view_shortcut(global_keymap.goto_show_hn_view, "show_hn", s, client, None); + set_up_switch_story_view_shortcut(global_keymap.goto_jobs_view, "job", s, client, None); + + // custom navigation shortcuts + config::get_config() + .keymap + .custom_keymaps + .iter() + .for_each(|data| { + s.set_on_post_event(data.key.clone(), move |s| { + story_view::construct_and_add_new_story_view( + s, + client, + &data.tag, + data.by_date, + 0, + data.numeric_filters, + false, + ); + }); + }); + + // ............................................ + // end of navigation shortcuts for Story Views + // ............................................ + + s.set_on_post_event(global_keymap.goto_previous_view, |s| { + if s.screen_mut().len() > 1 { + s.pop_layer(); + } + }); + + s.set_on_post_event(global_keymap.goto_search_view, move |s| { + search_view::construct_and_add_new_search_view(s, client); + }); + + s.set_on_post_event(global_keymap.open_help_dialog, |s| { + s.add_layer(help_view::DefaultHelpView::construct_on_event_help_view()) + }); + + s.set_on_post_event(global_keymap.quit, |s| s.quit()); +} + +/// Initialize the application's UI +pub fn init_ui(client: &'static client::HNClient) -> cursive::CursiveRunnable { + let mut s = cursive::default(); + + // initialize `cursive` color palette which is determined by the application's theme + let theme = config::get_config_theme(); + s.update_theme(|t| { + t.palette.set_color("view", theme.palette.background.into()); + t.palette + .set_color("primary", theme.palette.foreground.into()); + t.palette + .set_color("title_primary", theme.palette.foreground.into()); + t.palette + .set_color("highlight", theme.palette.selection_background.into()); + t.palette + .set_color("highlight_text", theme.palette.selection_foreground.into()); + }); + + set_up_global_callbacks(&mut s, client); + + // render `front_page` story view as the application's startup view + story_view::construct_and_add_new_story_view( + &mut s, + client, + "front_page", + false, + 0, + client::StoryNumericFilters::default(), + false, + ); + + s +} diff --git a/hackernews_tui/src/view/search_view.rs b/hackernews_tui/src/view/search_view.rs index 3086919..7c21940 100644 --- a/hackernews_tui/src/view/search_view.rs +++ b/hackernews_tui/src/view/search_view.rs @@ -44,7 +44,7 @@ impl SearchView { ))) .child(EditableTextView::new()), ) - .child(story_view::get_story_main_view(vec![], client, 0).full_height()); + .child(story_view::construct_story_main_view(vec![], client, 0).full_height()); Self { mode: SearchViewMode::Search, @@ -131,7 +131,7 @@ impl SearchView { self.view.remove_child(1); let starting_id = client::SEARCH_LIMIT * self.page; self.view.add_child( - story_view::get_story_main_view(stories, self.client, starting_id).full_height(), + story_view::construct_story_main_view(stories, self.client, starting_id).full_height(), ); // the old Story View is deleted hence losing the current focus, // we need to place the focus back to the new Story View @@ -155,7 +155,7 @@ impl ViewWrapper for SearchView { } } -fn get_search_main_view(client: &'static client::HNClient, cb_sink: CbSink) -> impl View { +fn construct_search_main_view(client: &'static client::HNClient, cb_sink: CbSink) -> impl View { let story_view_keymap = config::get_story_view_keymap().clone(); let search_view_keymap = config::get_search_view_keymap().clone(); @@ -211,7 +211,8 @@ fn get_search_main_view(client: &'static client::HNClient, cb_sink: CbSink) -> i Some(EventResult::Consumed(None)) } }) - // paging/filtering while in NavigationMode + // paging/filtering commands while in NavigationMode + // Those commands need to be handled differently from the story view. .on_pre_event_inner(story_view_keymap.toggle_sort_by_date, |s, _| match s.mode { SearchViewMode::Navigation => { s.page = 0; @@ -244,20 +245,22 @@ fn get_search_main_view(client: &'static client::HNClient, cb_sink: CbSink) -> i }) } -pub fn get_search_view(client: &'static client::HNClient, cb_sink: CbSink) -> impl View { - let main_view = get_search_main_view(client, cb_sink); +fn construct_search_view(client: &'static client::HNClient, cb_sink: CbSink) -> impl View { + let main_view = construct_search_main_view(client, cb_sink); + let mut view = LinearLayout::vertical() .child(utils::construct_view_title_bar("Search View")) .child(main_view) .child(utils::construct_footer_view::<SearchView>()); + view.set_focus_index(1) .unwrap_or(EventResult::Consumed(None)); view } -pub fn add_search_view_layer(s: &mut Cursive, client: &'static client::HNClient) { +pub fn construct_and_add_new_search_view(s: &mut Cursive, client: &'static client::HNClient) { let cb_sink = s.cb_sink().clone(); s.screen_mut() - .add_transparent_layer(Layer::new(get_search_view(client, cb_sink))); + .add_transparent_layer(Layer::new(construct_search_view(client, cb_sink))); } diff --git a/hackernews_tui/src/view/story_view.rs b/hackernews_tui/src/view/story_view.rs index b70b71f..393fd87 100644 --- a/hackernews_tui/src/view/story_view.rs +++ b/hackernews_tui/src/view/story_view.rs @@ -125,7 +125,7 @@ impl ScrollViewContainer for StoryView { } } -pub fn get_story_main_view( +pub fn construct_story_main_view( stories: Vec<client::Story>, client: &'static client::HNClient, starting_id: usize, @@ -172,7 +172,7 @@ pub fn get_story_main_view( // so it can be cloned without greatly affecting performance |