Make PageUp/PageDown behavior configurable and default to 50%
This commit is contained in:
@ -24,6 +24,7 @@ tracing-subscriber = { version = "0.3.17", features = ["env-filter", "serde"] }
|
|||||||
clap = { version = "4.4.5", features = ["derive", "cargo", "wrap_help", "unicode", "string", "unstable-styles"] }
|
clap = { version = "4.4.5", features = ["derive", "cargo", "wrap_help", "unicode", "string", "unstable-styles"] }
|
||||||
|
|
||||||
# Config
|
# Config
|
||||||
|
bounded-integer = { version = "0.5.7", features = ["types"] }
|
||||||
config = "0.13.3"
|
config = "0.13.3"
|
||||||
derive_deref = "1.1.1"
|
derive_deref = "1.1.1"
|
||||||
directories = "5.0.1"
|
directories = "5.0.1"
|
||||||
@ -31,6 +32,7 @@ hostname = "0.3.1"
|
|||||||
json5 = "0.4.1"
|
json5 = "0.4.1"
|
||||||
serde = { version = "1.0.188", features = ["derive"] }
|
serde = { version = "1.0.188", features = ["derive"] }
|
||||||
serde_json = "1.0.107"
|
serde_json = "1.0.107"
|
||||||
|
serde_path_to_error = "0.1.14"
|
||||||
# TODO: switch to toml_edit to preserve (and write) doc comments
|
# TODO: switch to toml_edit to preserve (and write) doc comments
|
||||||
|
|
||||||
# Error handling
|
# Error handling
|
||||||
|
@ -22,6 +22,7 @@ use enum_dispatch::enum_dispatch;
|
|||||||
use ratatui::{prelude::*, widgets::*};
|
use ratatui::{prelude::*, widgets::*};
|
||||||
|
|
||||||
use crate::components::Action;
|
use crate::components::Action;
|
||||||
|
use crate::config::{Config, ScrollAmount};
|
||||||
use crate::widgets::prerender::{PrerenderInner, PrerenderValue};
|
use crate::widgets::prerender::{PrerenderInner, PrerenderValue};
|
||||||
use crate::widgets::{
|
use crate::widgets::{
|
||||||
BacklogItemWidget, BottomAlignedParagraph, Divider, EmptyWidget, OverlappableWidget,
|
BacklogItemWidget, BottomAlignedParagraph, Divider, EmptyWidget, OverlappableWidget,
|
||||||
@ -38,8 +39,11 @@ struct ScrollPosition {
|
|||||||
relative_scroll: i64,
|
relative_scroll: i64,
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Default, Debug)]
|
#[derive(Debug)]
|
||||||
pub struct Backlog {
|
pub struct Backlog {
|
||||||
|
config: Config,
|
||||||
|
/// Used to compute scroll on PageUp/PageDown when configured to a percentage.
|
||||||
|
last_height: u16,
|
||||||
scroll_position: Option<ScrollPosition>,
|
scroll_position: Option<ScrollPosition>,
|
||||||
/// Fallback used if the scroll_position is missing or unusable
|
/// Fallback used if the scroll_position is missing or unusable
|
||||||
absolute_scroll: u64,
|
absolute_scroll: u64,
|
||||||
@ -58,16 +62,22 @@ impl Component for Backlog {
|
|||||||
modifiers: KeyModifiers::NONE,
|
modifiers: KeyModifiers::NONE,
|
||||||
kind: KeyEventKind::Press,
|
kind: KeyEventKind::Press,
|
||||||
state: _,
|
state: _,
|
||||||
} => {
|
} => match self.config.keyboard.scroll_page {
|
||||||
self.scroll_up(20); // TODO: use the component height
|
ScrollAmount::Absolute(n) => self.scroll_up(n.into()),
|
||||||
|
ScrollAmount::Percentage(n) => {
|
||||||
|
self.scroll_up(u64::from(n) * u64::from(self.last_height) / 100)
|
||||||
|
},
|
||||||
},
|
},
|
||||||
KeyEvent {
|
KeyEvent {
|
||||||
code: KeyCode::PageDown,
|
code: KeyCode::PageDown,
|
||||||
modifiers: KeyModifiers::NONE,
|
modifiers: KeyModifiers::NONE,
|
||||||
kind: KeyEventKind::Press,
|
kind: KeyEventKind::Press,
|
||||||
state: _,
|
state: _,
|
||||||
} => {
|
} => match self.config.keyboard.scroll_page {
|
||||||
self.scroll_down(20) // TODO: use the component height
|
ScrollAmount::Absolute(n) => self.scroll_down(n.into()),
|
||||||
|
ScrollAmount::Percentage(n) => {
|
||||||
|
self.scroll_down(u64::from(n) * u64::from(self.last_height) / 100)
|
||||||
|
},
|
||||||
},
|
},
|
||||||
_ => {},
|
_ => {},
|
||||||
}
|
}
|
||||||
@ -96,6 +106,16 @@ impl Component for Backlog {
|
|||||||
}
|
}
|
||||||
|
|
||||||
impl Backlog {
|
impl Backlog {
|
||||||
|
pub fn new(config: Config) -> Self {
|
||||||
|
Backlog {
|
||||||
|
config,
|
||||||
|
last_height: 30, // Arbitrary default, only useful when user scrolls before first render
|
||||||
|
scroll_position: None,
|
||||||
|
absolute_scroll: 0,
|
||||||
|
pending_scroll: 0,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
pub fn scroll_up(&mut self, lines: u64) {
|
pub fn scroll_up(&mut self, lines: u64) {
|
||||||
self.pending_scroll = self.pending_scroll.saturating_add(lines as i64);
|
self.pending_scroll = self.pending_scroll.saturating_add(lines as i64);
|
||||||
}
|
}
|
||||||
@ -156,6 +176,7 @@ impl Backlog {
|
|||||||
let block = Block::new().borders(Borders::ALL);
|
let block = Block::new().borders(Borders::ALL);
|
||||||
let mut text_area = block.inner(area);
|
let mut text_area = block.inner(area);
|
||||||
block.render(area, frame_buffer);
|
block.render(area, frame_buffer);
|
||||||
|
self.last_height = text_area.height;
|
||||||
|
|
||||||
// Recompute absolute scroll position if we are not at the bottom of the backlog
|
// Recompute absolute scroll position if we are not at the bottom of the backlog
|
||||||
if self.absolute_scroll != 0 || self.pending_scroll != 0 {
|
if self.absolute_scroll != 0 || self.pending_scroll != 0 {
|
||||||
@ -206,7 +227,7 @@ impl Backlog {
|
|||||||
};
|
};
|
||||||
let expected_height = self.get_item_height(&item, text_area.width);
|
let expected_height = self.get_item_height(&item, text_area.width);
|
||||||
|
|
||||||
if scroll.saturating_sub(expected_height) > text_area.height.into() {
|
if scroll.saturating_sub(expected_height) > u64::from(text_area.height) {
|
||||||
// Paragraph is too far down, not displayed
|
// Paragraph is too far down, not displayed
|
||||||
scroll -= expected_height;
|
scroll -= expected_height;
|
||||||
continue;
|
continue;
|
||||||
|
@ -43,7 +43,7 @@ impl Home {
|
|||||||
let mut self_ = Home {
|
let mut self_ = Home {
|
||||||
command_tx: None,
|
command_tx: None,
|
||||||
buflist: Buflist::new(config.clone()),
|
buflist: Buflist::new(config.clone()),
|
||||||
backlog: Backlog::default(),
|
backlog: Backlog::new(config.clone()),
|
||||||
textarea: TextArea::default(),
|
textarea: TextArea::default(),
|
||||||
config,
|
config,
|
||||||
};
|
};
|
||||||
|
@ -1,5 +1,6 @@
|
|||||||
use std::{collections::HashMap, fmt, path::PathBuf};
|
use std::{collections::HashMap, fmt, path::PathBuf};
|
||||||
|
|
||||||
|
use bounded_integer::BoundedU8;
|
||||||
use color_eyre::eyre::Result;
|
use color_eyre::eyre::Result;
|
||||||
use config::Value;
|
use config::Value;
|
||||||
use crossterm::event::{KeyCode, KeyEvent, KeyModifiers};
|
use crossterm::event::{KeyCode, KeyEvent, KeyModifiers};
|
||||||
@ -40,11 +41,77 @@ fn default_device_name() -> String {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[derive(Clone, Debug)]
|
||||||
|
pub enum ScrollAmount {
|
||||||
|
Percentage(BoundedU8<1, 100>),
|
||||||
|
Absolute(u16),
|
||||||
|
}
|
||||||
|
|
||||||
|
impl<'de> Deserialize<'de> for ScrollAmount {
|
||||||
|
fn deserialize<D>(deserializer: D) -> Result<ScrollAmount, D::Error>
|
||||||
|
where
|
||||||
|
D: Deserializer<'de>,
|
||||||
|
{
|
||||||
|
deserializer.deserialize_str(ScrollAmountVisitor)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
struct ScrollAmountVisitor;
|
||||||
|
|
||||||
|
impl<'de> Visitor<'de> for ScrollAmountVisitor {
|
||||||
|
type Value = ScrollAmount;
|
||||||
|
|
||||||
|
fn expecting(&self, formatter: &mut fmt::Formatter) -> fmt::Result {
|
||||||
|
formatter.write_str("a positive integer or a percentage")
|
||||||
|
}
|
||||||
|
|
||||||
|
fn visit_u16<E>(self, value: u16) -> Result<Self::Value, E>
|
||||||
|
where
|
||||||
|
E: de::Error,
|
||||||
|
{
|
||||||
|
Ok(ScrollAmount::Absolute(value))
|
||||||
|
}
|
||||||
|
|
||||||
|
fn visit_str<E>(self, s: &str) -> Result<Self::Value, E>
|
||||||
|
where
|
||||||
|
E: de::Error,
|
||||||
|
{
|
||||||
|
let s: String = s.chars().filter(|c| !c.is_whitespace()).collect(); // strip whitespaces
|
||||||
|
match s.strip_suffix('%') {
|
||||||
|
Some(percent_str) => match percent_str.parse() {
|
||||||
|
Ok(percent) => {
|
||||||
|
if 0 < percent && percent <= 100 {
|
||||||
|
Ok(ScrollAmount::Percentage(percent))
|
||||||
|
} else {
|
||||||
|
Err(E::invalid_value(
|
||||||
|
de::Unexpected::Unsigned(percent.into()),
|
||||||
|
&"integer between 1 and 100 (inclusive)",
|
||||||
|
))
|
||||||
|
}
|
||||||
|
},
|
||||||
|
Err(_) => Err(E::invalid_value(
|
||||||
|
de::Unexpected::Other(percent_str),
|
||||||
|
&"integer between 1 and 100 (inclusive)",
|
||||||
|
)),
|
||||||
|
},
|
||||||
|
None => Err(E::invalid_value(
|
||||||
|
de::Unexpected::Str(&s),
|
||||||
|
&"integer or quoted percentage (ending with '%')",
|
||||||
|
)),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
#[derive(Clone, Debug, Deserialize)]
|
#[derive(Clone, Debug, Deserialize)]
|
||||||
pub struct MouseConfig {
|
pub struct MouseConfig {
|
||||||
pub enable: bool,
|
pub enable: bool,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[derive(Clone, Debug, Deserialize)]
|
||||||
|
pub struct KeyboardConfig {
|
||||||
|
pub scroll_page: ScrollAmount,
|
||||||
|
}
|
||||||
|
|
||||||
#[derive(Clone, Debug, Deserialize)]
|
#[derive(Clone, Debug, Deserialize)]
|
||||||
pub struct BuflistLayoutConfig {
|
pub struct BuflistLayoutConfig {
|
||||||
pub column_width: u16,
|
pub column_width: u16,
|
||||||
@ -84,13 +151,14 @@ pub struct Config {
|
|||||||
pub accounts: nonempty::NonEmpty<AccountConfig>,
|
pub accounts: nonempty::NonEmpty<AccountConfig>,
|
||||||
#[serde(default)]
|
#[serde(default)]
|
||||||
pub keybindings: KeyBindings,
|
pub keybindings: KeyBindings,
|
||||||
|
pub keyboard: KeyboardConfig,
|
||||||
pub mouse: MouseConfig,
|
pub mouse: MouseConfig,
|
||||||
pub style: StylesConfig,
|
pub style: StylesConfig,
|
||||||
pub layout: LayoutConfig,
|
pub layout: LayoutConfig,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl Config {
|
impl Config {
|
||||||
pub fn new() -> Result<Self, config::ConfigError> {
|
pub fn new() -> Result<Self> {
|
||||||
let data_dir = crate::utils::get_data_dir();
|
let data_dir = crate::utils::get_data_dir();
|
||||||
let config_dir = crate::utils::get_config_dir();
|
let config_dir = crate::utils::get_config_dir();
|
||||||
let mut builder = config::Config::builder()
|
let mut builder = config::Config::builder()
|
||||||
@ -123,7 +191,7 @@ impl Config {
|
|||||||
log::error!("No configuration file found. Application may not behave as expected");
|
log::error!("No configuration file found. Application may not behave as expected");
|
||||||
}
|
}
|
||||||
|
|
||||||
builder.build()?.try_deserialize()
|
Ok(serde_path_to_error::deserialize(builder.build()?)?)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -6,6 +6,11 @@
|
|||||||
"<Alt-left>" = "/previous"
|
"<Alt-left>" = "/previous"
|
||||||
"<Alt-right>" = "/next"
|
"<Alt-right>" = "/next"
|
||||||
|
|
||||||
|
[keyboard]
|
||||||
|
# How much to scroll when pressing PageUp/PageDown. This can be either a number of lines
|
||||||
|
# eg. 42) or a percentage wrapped in quotes (eg. "100%") of the screen size.
|
||||||
|
scroll_page = "50%"
|
||||||
|
|
||||||
[mouse]
|
[mouse]
|
||||||
enable = true
|
enable = true
|
||||||
|
|
||||||
|
Reference in New Issue
Block a user