Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -35,7 +35,7 @@ Set `HN_REDUCED_MOTION=1` to disable visual animations; ANSI and monochrome pale

## Read a story

Use `j/k` or the arrow keys to select a story. Press `1`, `2`, or `3` for Top, New, or Best; `4` opens Saved. Use `n/p` to change pages — or press `j` past the last story to continue on the next page's first story, and `k` at the top to go back a page (further `j/k` wait while the page loads).
Use `j/k` or the arrow keys to select a story. Press `1`, `2`, or `3` for Top, New, or Best; `4` opens Saved. Use `n/p` to change pages — or press `j` past the last story to continue on the next page's first story, and `k` at the top to go back to the last story of the previous page (further `j/k` wait while the page loads).

Press `o` or Enter to open the discussion and its article. Press `c` to read the discussion without opening a new article pane. If a companion pane is already open, it follows the selected story. `O` always opens the story in your default browser.

Expand Down
37 changes: 35 additions & 2 deletions src/app.rs
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,15 @@ use std::{
/// because this is the highest-frequency motion in the app).
pub const SELECTION_GLIDE: Duration = Duration::from_millis(110);

/// Where the selection lands when a j/k-triggered page switch finishes loading.
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
pub enum PageLanding {
/// j past the last story: first story of the next page.
Start,
/// k at the first story: last story of the previous page.
End,
}

#[cfg(test)]
mod app_tests;
mod comment_refresh;
Expand Down Expand Up @@ -95,6 +104,8 @@ pub struct App {
/// Outgoing list (stories, selection, scroll) retained during the dip's
/// out-phase so the old list fades away instead of the new one popping in
pub outgoing_feed: Option<(Vec<HNCLIItem>, usize, usize)>,
/// Landing target for a j/k-triggered page switch, consumed by set_stories_for
pub page_landing: Option<PageLanding>,
/// Current page number
pub current_page: u32,
/// Comments for current story
Expand Down Expand Up @@ -169,6 +180,7 @@ impl App {
selection_glide: None,
feed_switched: None,
outgoing_feed: None,
page_landing: None,
current_page: 1,
comments: Vec::new(),
visible_comment_paths: Vec::new(),
Expand Down Expand Up @@ -224,7 +236,11 @@ impl App {
} else {
let page = self.current_page;
self.next_page();
self.current_page != page
let advanced = self.current_page != page;
if advanced {
self.page_landing = Some(PageLanding::Start);
}
advanced
}
}

Expand All @@ -243,7 +259,11 @@ impl App {
} else {
let page = self.current_page;
self.prev_page();
self.current_page != page
let changed = self.current_page != page;
if changed {
self.page_landing = Some(PageLanding::End);
}
changed
}
}

Expand Down Expand Up @@ -512,6 +532,19 @@ impl App {
self.selected_index,
self.story_scroll,
));
// A j/k-triggered page switch lands on the page edge instead
// of restoring a remembered position.
match self.page_landing.take() {
Some(PageLanding::Start) => {
self.selected_index = 0;
self.story_scroll = 0;
}
Some(PageLanding::End) => {
self.selected_index = stories.len().saturating_sub(1);
self.story_scroll = self.selected_index;
}
None => {}
}
}
}
self.stories_for = Some((story_type, page));
Expand Down
29 changes: 27 additions & 2 deletions src/ui/ui_tests.rs
Original file line number Diff line number Diff line change
Expand Up @@ -785,9 +785,34 @@ fn k_at_page_top_returns_to_previous_page() {
(1..=12).map(story_item).collect(),
);
assert_eq!(
app.selected_index, 0,
"previous page loads like a manual p, starting at its first story"
app.selected_index,
app.stories.len() - 1,
"going back with k lands at the bottom of the previous page"
);
// And j at that bottom flows forward again, landing on the next page's first story.
assert!(app.next_story());
app.set_stories_for(
StoryType::Best,
2,
(20..=31).map(story_item).collect(),
);
assert_eq!(app.selected_index, 0, "j lands on the first story of the next page");
}

#[test]
fn manual_page_keys_keep_position_restore_behavior() {
use crate::app::StoryType;
let mut app = feed();
app.selected_index = app.stories.len() - 1;
app.next_page();
app.set_stories_for(
StoryType::Best,
2,
(20..=31).map(story_item).collect(),
);
assert_eq!(app.selected_index, 0, "manual n starts at the first story");
// No page_landing was set by n/p, so nothing overrides the restore rules.
assert!(app.page_landing.is_none());
}

#[test]
Expand Down
Loading