summaryrefslogtreecommitdiffstats
diff options
context:
space:
mode:
authorThang Pham <phamducthang1234@gmail.com>2022-05-22 17:00:10 -0400
committerThang Pham <phamducthang1234@gmail.com>2022-05-22 17:00:10 -0400
commit0c103c56cf0417a43dd3eec648435d5c80b12814 (patch)
treea2cbc335df1d3e8818569b9dd06b9803c2d079bd
parent39b420e321337f6ae6438ca390bcaba082c22b96 (diff)
parent9c687657920aa4adad5df9fb2bff7d1a3616e4df (diff)
Merge remote-tracking branch 'origin/main'
-rw-r--r--hackernews_tui/src/client/mod.rs3
-rw-r--r--hackernews_tui/src/client/parser.rs3
-rw-r--r--hackernews_tui/src/client/query.rs16
-rw-r--r--hackernews_tui/src/config/mod.rs6
-rw-r--r--hackernews_tui/src/main.rs108
-rw-r--r--hackernews_tui/src/view/article_view.rs13
-rw-r--r--hackernews_tui/src/view/async_view.rs14
-rw-r--r--hackernews_tui/src/view/comment_view.rs142
-rw-r--r--hackernews_tui/src/view/mod.rs125
-rw-r--r--hackernews_tui/src/view/search_view.rs19
-rw-r--r--hackernews_tui/src/view/story_view.rs46
-rw-r--r--hackernews_tui/src/view/utils.rs2
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 3f99af5..3f7ede7 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