use serde::{Deserialize, Serialize};
mod tag;
pub use tag::{SessionTags, TAG_MAX, Tag};
#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
#[serde(rename_all = "snake_case")]
#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
pub enum Lang {
En,
Zh,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
#[serde(rename_all = "snake_case")]
#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
pub enum Lighting {
System,
Dark,
Light,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
#[serde(rename_all = "snake_case")]
#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
pub enum Face {
Geist,
System,
Custom,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
#[serde(rename_all = "snake_case")]
#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
pub enum Density {
Comfortable,
Compact,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
#[serde(rename_all = "snake_case")]
#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
pub enum Chroma {
Full,
Off,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
#[serde(rename_all = "snake_case")]
#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
pub enum Motion {
System,
On,
Off,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
#[serde(rename_all = "snake_case")]
#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
pub enum Glass {
On,
Off,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
#[serde(rename_all = "snake_case")]
#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
pub enum Tier {
Zen,
Blend,
Panorama,
}
pub const BODY_PX_MIN: u32 = 12;
pub const BODY_PX_MAX: u32 = 20;
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
pub struct Appearance {
pub lighting: Lighting,
pub sans: Face,
pub mono: Face,
pub sans_stack: String,
pub mono_stack: String,
pub body_px: Option<u32>,
pub density: Density,
pub chroma: Chroma,
pub motion: Motion,
#[serde(default)]
pub glass: Option<Glass>,
#[serde(default)]
pub blend_percent: Option<u32>,
}
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
pub struct Chord {
pub action: String,
pub spelled: String,
}
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
#[serde(default, deny_unknown_fields)]
#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
pub struct PreferencesAnswer {
pub lang: Option<Lang>,
pub welcomed: bool,
pub panel: bool,
pub tier: Option<Tier>,
pub appearance: Appearance,
pub proxying: kernel::Proxying,
pub chords: Vec<Chord>,
pub tags: Vec<SessionTags>,
}
impl Default for PreferencesAnswer {
fn default() -> Self {
PreferencesAnswer {
lang: None,
welcomed: false,
panel: true,
tier: None,
appearance: Appearance::default(),
proxying: kernel::Proxying::ExceptLocal,
chords: Vec::new(),
tags: Vec::new(),
}
}
}
impl Default for Appearance {
fn default() -> Self {
Appearance {
lighting: Lighting::System,
sans: Face::Geist,
mono: Face::Geist,
sans_stack: String::new(),
mono_stack: String::new(),
body_px: None,
density: Density::Comfortable,
chroma: Chroma::Full,
motion: Motion::System,
glass: None,
blend_percent: None,
}
}
}
impl PreferencesAnswer {
pub fn apply(&mut self, patch: PreferencePatch) {
match patch {
PreferencePatch::Lang(lang) => self.lang = Some(lang),
PreferencePatch::Welcomed(welcomed) => self.welcomed = welcomed,
PreferencePatch::Panel(panel) => self.panel = panel,
PreferencePatch::Tier(tier) => self.tier = Some(tier),
PreferencePatch::Appearance(appearance) => self.appearance = appearance,
PreferencePatch::Proxying(proxying) => self.proxying = proxying,
PreferencePatch::CorePriority(_) => {}
PreferencePatch::Tags(next) => tag::retagged(&mut self.tags, next),
PreferencePatch::Chord(chord) => {
self.chords.retain(|held| held.action != chord.action);
if !chord.spelled.is_empty() {
self.chords.push(chord);
self.chords.sort_by(|one, two| one.action.cmp(&two.action));
}
}
}
}
}
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
#[serde(rename_all = "snake_case")]
#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
pub enum PreferencePatch {
Lang(Lang),
Welcomed(bool),
Panel(bool),
Tier(Tier),
Appearance(Appearance),
Proxying(kernel::Proxying),
Chord(Chord),
CorePriority(CorePriority),
Tags(SessionTags),
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
#[serde(rename_all = "snake_case")]
#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
pub enum CorePriority {
Raised,
Normal,
}
#[cfg(test)]
#[allow(clippy::unwrap_used, clippy::expect_used, reason = "test code")]
mod tests {
use super::*;
#[test]
fn a_patch_travels_under_the_name_of_the_fact_it_changes() {
let text = serde_json::to_string(&PreferencePatch::Lang(Lang::Zh)).unwrap();
assert_eq!(text, r#"{"lang":"zh"}"#);
let back: PreferencePatch = serde_json::from_str(&text).unwrap();
assert_eq!(back, PreferencePatch::Lang(Lang::Zh));
}
#[test]
fn a_word_no_build_offers_is_refused_rather_than_repaired() {
assert!(serde_json::from_str::<Lighting>("\"sepia\"").is_err());
assert!(serde_json::from_str::<PreferencePatch>(r#"{"lang":"fr"}"#).is_err());
}
#[test]
fn an_action_is_bound_once_and_unbinding_it_removes_the_entry() {
let mut held = PreferencesAnswer::default();
let bind = |spelled: &str| {
PreferencePatch::Chord(Chord {
action: "go.city".to_owned(),
spelled: spelled.to_owned(),
})
};
held.apply(bind("accel+2"));
held.apply(bind("accel+3"));
assert_eq!(held.chords.len(), 1);
assert_eq!(
held.chords.first().map(|held| held.spelled.as_str()),
Some("accel+3")
);
held.apply(bind(""));
assert!(held.chords.is_empty());
}
#[test]
fn a_section_that_states_one_fact_leaves_the_others_at_their_posture() {
let held: PreferencesAnswer = serde_json::from_str(r#"{"welcomed":true}"#).unwrap();
assert!(held.welcomed);
assert!(held.panel);
assert_eq!(held.lang, None);
assert_eq!(held.appearance, Appearance::default());
}
#[test]
fn an_appearance_written_before_glass_and_blend_still_reads() {
let held: PreferencesAnswer = serde_json::from_str(
r#"{"appearance":{"lighting":"dark","sans":"geist","mono":"geist","sans_stack":"","mono_stack":"","body_px":null,"density":"compact","chroma":"full","motion":"system"}}"#,
)
.unwrap();
assert_eq!(
(
held.tier,
held.appearance.glass,
held.appearance.blend_percent
),
(None, None, None)
);
let mut chosen = held;
chosen.apply(PreferencePatch::Tier(Tier::Panorama));
assert_eq!(chosen.tier, Some(Tier::Panorama));
}
#[test]
fn a_key_no_field_claims_is_refused() {
assert!(serde_json::from_str::<PreferencesAnswer>(r#"{"welcomd":true}"#).is_err());
}
}