use serde::{Deserialize, Serialize};
use crate::binding::ButtonId;
use crate::color::Rgb;
#[derive(Debug, Clone, Copy, Default, PartialEq, Eq, Serialize, Deserialize)]
#[serde(rename_all = "snake_case")]
pub enum Appearance {
#[default]
System,
Light,
Dark,
}
#[derive(Debug, Clone, Copy, Default, PartialEq, Eq, Serialize, Deserialize)]
#[serde(rename_all = "snake_case")]
pub enum AssetSourcePreference {
#[default]
Automatic,
#[serde(rename = "openlogi")]
OpenLogi,
Cloudflare,
Fastly,
}
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
#[allow(
clippy::struct_excessive_bools,
reason = "independent on/off user preferences, not a state machine"
)]
pub struct AppSettings {
#[serde(default)]
pub launch_at_login: bool,
#[serde(default)]
pub check_for_updates: bool,
#[serde(default)]
pub auto_install_updates: bool,
#[serde(default)]
pub update_prompt_seen: bool,
#[serde(default = "default_true")]
pub show_in_menu_bar: bool,
#[serde(default = "default_true")]
pub capture_mouse_events: bool,
#[serde(default = "default_true")]
pub auto_download_assets: bool,
#[serde(default)]
pub asset_source: AssetSourcePreference,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub language: Option<String>,
#[serde(default = "default_thumbwheel_sensitivity")]
pub thumbwheel_sensitivity: i32,
#[serde(default)]
pub appearance: Appearance,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub theme_light: Option<String>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub theme_dark: Option<String>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub ui_radius: Option<u8>,
}
pub const DEFAULT_THUMBWHEEL_SENSITIVITY: i32 = 14;
pub const MIN_THUMBWHEEL_SENSITIVITY: i32 = 1;
pub const MAX_THUMBWHEEL_SENSITIVITY: i32 = 100;
impl AppSettings {
#[must_use]
pub fn is_default(&self) -> bool {
self == &Self::default()
}
}
impl Default for AppSettings {
fn default() -> Self {
Self {
launch_at_login: false,
check_for_updates: false,
auto_install_updates: false,
update_prompt_seen: false,
show_in_menu_bar: true,
capture_mouse_events: true,
auto_download_assets: true,
asset_source: AssetSourcePreference::Automatic,
language: None,
thumbwheel_sensitivity: DEFAULT_THUMBWHEEL_SENSITIVITY,
appearance: Appearance::System,
theme_light: None,
theme_dark: None,
ui_radius: None,
}
}
}
fn default_true() -> bool {
true
}
const fn default_thumbwheel_sensitivity() -> i32 {
DEFAULT_THUMBWHEEL_SENSITIVITY
}
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
pub struct Lighting {
#[serde(default = "default_lighting_enabled")]
pub enabled: bool,
#[serde(
default = "default_lighting_color",
deserialize_with = "deserialize_lighting_color"
)]
pub color: Rgb,
#[serde(
default = "default_lighting_brightness",
deserialize_with = "deserialize_brightness"
)]
pub brightness: u8,
}
impl Default for Lighting {
fn default() -> Self {
Self {
enabled: default_lighting_enabled(),
color: default_lighting_color(),
brightness: default_lighting_brightness(),
}
}
}
fn default_lighting_enabled() -> bool {
true
}
fn default_lighting_color() -> Rgb {
Rgb::WHITE
}
fn default_lighting_brightness() -> u8 {
100
}
fn deserialize_brightness<'de, D>(deserializer: D) -> Result<u8, D::Error>
where
D: serde::Deserializer<'de>,
{
Ok(u8::deserialize(deserializer)?.min(100))
}
fn deserialize_lighting_color<'de, D>(deserializer: D) -> Result<Rgb, D::Error>
where
D: serde::Deserializer<'de>,
{
let color = String::deserialize(deserializer)?;
Ok(color
.strip_prefix('#')
.unwrap_or(color.as_str())
.parse()
.unwrap_or(Rgb::WHITE))
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
#[serde(rename_all = "snake_case")]
pub enum ScrollResolution {
Low,
High,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
#[serde(rename_all = "snake_case")]
pub enum WheelMode {
Free,
Ratchet,
}
pub const SMARTSHIFT_AUTO_DISENGAGE_DEFAULT: u8 = 16;
pub const SMARTSHIFT_MIN_AUTO_DISENGAGE: u8 = 8;
fn deserialize_auto_disengage<'de, D>(deserializer: D) -> Result<u8, D::Error>
where
D: serde::Deserializer<'de>,
{
let value = u8::deserialize(deserializer)?;
Ok(if value < SMARTSHIFT_MIN_AUTO_DISENGAGE {
tracing::warn!(
value,
min = SMARTSHIFT_MIN_AUTO_DISENGAGE,
default = SMARTSHIFT_AUTO_DISENGAGE_DEFAULT,
"healed persisted SmartShift auto-disengage threshold below supported floor"
);
SMARTSHIFT_AUTO_DISENGAGE_DEFAULT
} else {
value
})
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
pub struct SmartShift {
pub mode: WheelMode,
#[serde(deserialize_with = "deserialize_auto_disengage")]
pub auto_disengage: u8,
pub tunable_torque: u8,
}
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
pub enum GestureOwner {
Off,
Button(ButtonId),
}
impl Serialize for GestureOwner {
fn serialize<S: serde::Serializer>(&self, serializer: S) -> Result<S::Ok, S::Error> {
match self {
GestureOwner::Off => serializer.serialize_str("Off"),
GestureOwner::Button(id) => id.serialize(serializer),
}
}
}
pub(super) fn deserialize_gesture_owner<'de, D>(
deserializer: D,
) -> Result<Option<GestureOwner>, D::Error>
where
D: serde::Deserializer<'de>,
{
let s = String::deserialize(deserializer)?;
if s == "Off" {
return Ok(Some(GestureOwner::Off));
}
let button = ButtonId::deserialize(
serde::de::value::StrDeserializer::<serde::de::value::Error>::new(&s),
)
.ok();
Ok(button.map(GestureOwner::Button))
}
#[cfg(test)]
#[allow(clippy::expect_used, reason = "expect/unwrap are idiomatic in tests")]
mod tests {
use super::*;
#[test]
fn low_auto_disengage_heals_to_default_on_load() {
let heal = |v: u8| {
let body = format!("mode = \"ratchet\"\nauto_disengage = {v}\ntunable_torque = 50\n");
toml::from_str::<SmartShift>(&body)
.expect("parse")
.auto_disengage
};
assert_eq!(heal(0), SMARTSHIFT_AUTO_DISENGAGE_DEFAULT);
assert_eq!(heal(1), SMARTSHIFT_AUTO_DISENGAGE_DEFAULT);
assert_eq!(
heal(SMARTSHIFT_MIN_AUTO_DISENGAGE - 1),
SMARTSHIFT_AUTO_DISENGAGE_DEFAULT
);
assert_eq!(
heal(SMARTSHIFT_MIN_AUTO_DISENGAGE),
SMARTSHIFT_MIN_AUTO_DISENGAGE
);
assert_eq!(heal(16), 16);
assert_eq!(heal(0xff), 0xff);
}
}