use std::time::Duration;
use log::debug;
pub struct Config {
pub theme: String,
pub width: f64,
pub ppi: f32,
pub scale: f64,
pub viewer: ViewerConfig,
}
#[derive(Clone, Copy, Debug, Default, PartialEq, Eq)]
pub enum ScrollMode {
#[default]
Fixed,
Adaptive,
}
#[derive(Clone, Copy, Debug, Default, PartialEq, Eq)]
pub enum ScrollAnimation {
#[default]
ExpDecay,
ExpDecayAdaptive,
Kinetic,
}
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
pub enum ExpPreset {
Adaptive,
}
impl ExpPreset {
fn resolve(self) -> (ScrollMode, ScrollAnimation) {
match self {
Self::Adaptive => (ScrollMode::Adaptive, ScrollAnimation::Kinetic),
}
}
}
pub struct ViewerConfig {
pub scroll_step: u32,
pub scroll_mode: ScrollMode,
pub scroll_animation: ScrollAnimation,
pub frame_budget: Duration,
pub tile_height: f64,
pub sidebar_cols: u16,
pub evict_distance: usize,
pub watch_interval: Duration,
}
impl Default for Config {
fn default() -> Self {
Self {
theme: crate::theme::DEFAULT_THEME.into(),
width: 660.0,
ppi: 144.0,
scale: 1.0,
viewer: ViewerConfig::default(),
}
}
}
impl Default for ViewerConfig {
fn default() -> Self {
Self {
scroll_step: 3,
scroll_mode: ScrollMode::default(),
scroll_animation: ScrollAnimation::default(),
frame_budget: Duration::from_millis(32),
tile_height: 500.0,
sidebar_cols: 6,
evict_distance: 4,
watch_interval: Duration::from_millis(200),
}
}
}
impl Config {
pub fn apply_cli(&mut self, cli: &CliOverrides) {
if let Some(ref v) = cli.theme {
debug!("config: CLI override theme={v}");
self.theme = v.clone();
}
if let Some(v) = cli.width {
debug!("config: CLI override width={v}");
self.width = v;
}
if let Some(v) = cli.ppi {
debug!("config: CLI override ppi={v}");
self.ppi = v;
}
if let Some(v) = cli.tile_height {
debug!("config: CLI override tile_height={v}");
self.viewer.tile_height = v;
}
if let Some(v) = cli.scale {
debug!("config: CLI override scale={v}");
self.scale = v;
}
if let Some(preset) = cli.exp_preset {
let (mode, anim) = preset.resolve();
debug!("config: CLI override exp_preset={preset:?} → mode={mode:?}, anim={anim:?}");
self.viewer.scroll_mode = mode;
self.viewer.scroll_animation = anim;
}
if let Some(mode) = cli.scroll_mode {
debug!("config: CLI override scroll_mode={mode:?}");
self.viewer.scroll_mode = mode;
}
if let Some(anim) = cli.scroll_animation {
debug!("config: CLI override scroll_animation={anim:?}");
self.viewer.scroll_animation = anim;
}
}
}
#[derive(Clone, Debug, Default)]
pub struct CliOverrides {
pub theme: Option<String>,
pub width: Option<f64>,
pub ppi: Option<f32>,
pub tile_height: Option<f64>,
pub scale: Option<f64>,
pub allow_remote_images: bool,
pub scroll_mode: Option<ScrollMode>,
pub scroll_animation: Option<ScrollAnimation>,
pub exp_preset: Option<ExpPreset>,
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn defaults() {
let config = Config::default();
assert_eq!(config.theme, "auto");
assert_eq!(config.width, 660.0);
assert_eq!(config.ppi, 144.0);
assert_eq!(config.viewer.scroll_step, 3);
assert_eq!(config.viewer.scroll_mode, ScrollMode::Fixed);
assert_eq!(config.viewer.sidebar_cols, 6);
assert_eq!(config.viewer.evict_distance, 4);
}
#[test]
fn cli_overrides() {
let mut config = Config::default();
let cli = CliOverrides {
theme: Some("dark".into()),
width: None,
ppi: Some(288.0),
tile_height: None,
scale: None,
allow_remote_images: false,
scroll_mode: None,
scroll_animation: None,
exp_preset: None,
};
config.apply_cli(&cli);
assert_eq!(config.theme, "dark");
assert_eq!(config.ppi, 288.0);
assert_eq!(config.width, 660.0); assert_eq!(config.scale, 1.0); }
#[test]
fn scale_default_and_override() {
let mut config = Config::default();
assert_eq!(config.scale, 1.0);
let cli = CliOverrides {
scale: Some(1.5),
..Default::default()
};
config.apply_cli(&cli);
assert_eq!(config.scale, 1.5);
}
#[test]
fn scroll_animation_override() {
let mut config = Config::default();
assert_eq!(config.viewer.scroll_animation, ScrollAnimation::ExpDecay);
let cli = CliOverrides {
scroll_animation: Some(ScrollAnimation::ExpDecay),
..Default::default()
};
config.apply_cli(&cli);
assert_eq!(config.viewer.scroll_animation, ScrollAnimation::ExpDecay);
}
#[test]
fn exp_preset_adaptive_sets_both_layers() {
let mut config = Config::default();
let cli = CliOverrides {
exp_preset: Some(ExpPreset::Adaptive),
..Default::default()
};
config.apply_cli(&cli);
assert_eq!(config.viewer.scroll_mode, ScrollMode::Adaptive);
assert_eq!(config.viewer.scroll_animation, ScrollAnimation::Kinetic);
}
#[test]
fn individual_flags_override_exp_preset() {
let mut config = Config::default();
let cli = CliOverrides {
exp_preset: Some(ExpPreset::Adaptive),
scroll_mode: Some(ScrollMode::Fixed),
scroll_animation: Some(ScrollAnimation::ExpDecay),
..Default::default()
};
config.apply_cli(&cli);
assert_eq!(config.viewer.scroll_mode, ScrollMode::Fixed);
assert_eq!(config.viewer.scroll_animation, ScrollAnimation::ExpDecay);
}
#[test]
fn scroll_mode_override_does_not_touch_scroll_step() {
for mode in [ScrollMode::Fixed, ScrollMode::Adaptive] {
let mut config = Config::default();
assert_eq!(config.viewer.scroll_step, 3);
let cli = CliOverrides {
scroll_mode: Some(mode),
..Default::default()
};
config.apply_cli(&cli);
assert_eq!(config.viewer.scroll_mode, mode);
assert_eq!(config.viewer.scroll_step, 3); }
}
}