use std::collections::BTreeMap;
use serde::{Deserialize, Serialize};
mod core;
pub use core::{CorePlacement, CorePreferences, CorePriority};
mod body;
pub use body::{BODY_PX_MIN, BodyPx};
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 ReadingFace {
Interface,
Libron,
}
#[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,
}
#[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,
#[serde(default)]
pub reading: Option<ReadingFace>,
pub body_px: Option<BodyPx>,
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 core: CorePreferences,
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>,
pub theme: ThemeOverride,
}
#[derive(Debug, Clone, Default, PartialEq, Eq, Serialize, Deserialize)]
#[serde(default, deny_unknown_fields)]
#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
pub struct ThemeOverride {
pub tokens: BTreeMap<String, String>,
pub css: Option<String>,
}
impl Default for PreferencesAnswer {
fn default() -> Self {
PreferencesAnswer {
lang: None,
core: CorePreferences::default(),
welcomed: false,
panel: true,
tier: None,
appearance: Appearance::default(),
proxying: kernel::Proxying::ExceptLocal,
chords: Vec::new(),
tags: Vec::new(),
theme: ThemeOverride::default(),
}
}
}
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(),
reading: None,
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(priority) => self.core.priority = priority,
PreferencePatch::CorePlacement(placement) => self.core.placement = placement,
PreferencePatch::RunMemory(memory) => self.core.memory_bytes = memory,
PreferencePatch::Tags(next) => tag::retagged(&mut self.tags, next),
PreferencePatch::Theme(theme) => self.theme = theme,
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),
CorePlacement(CorePlacement),
RunMemory(Option<std::num::NonZeroU64>),
Tags(SessionTags),
Theme(ThemeOverride),
}
#[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_body_size_under_the_floor_is_refused_and_any_size_above_it_reads() {
let appearance = |px: u32| {
serde_json::json!({ "appearance": {
"lighting": "dark", "sans": "geist", "mono": "geist",
"sans_stack": "", "mono_stack": "", "body_px": px,
"density": "compact", "chroma": "full", "motion": "system",
}})
};
assert!(serde_json::from_value::<PreferencesAnswer>(appearance(11)).is_err());
for px in [12, 24, 96] {
let held: PreferencesAnswer = serde_json::from_value(appearance(px)).unwrap();
assert_eq!(
serde_json::to_value(&held)
.unwrap()
.pointer("/appearance/body_px"),
Some(&serde_json::json!(px))
);
}
}
#[test]
fn the_reading_face_reads_back_as_stated_and_absent_as_never_said() {
let appearance = |reading: Option<&str>| {
let mut stated = serde_json::json!({
"lighting": "dark", "sans": "geist", "mono": "geist",
"sans_stack": "", "mono_stack": "", "body_px": null,
"density": "compact", "chroma": "full", "motion": "system",
});
if let (Some(face), Some(fields)) = (reading, stated.as_object_mut()) {
fields.insert("reading".to_owned(), serde_json::json!(face));
}
serde_json::json!({ "appearance": stated })
};
let read_back = |reading: Option<&str>| {
let held: PreferencesAnswer = serde_json::from_value(appearance(reading)).unwrap();
serde_json::to_value(&held)
.unwrap()
.pointer("/appearance/reading")
.cloned()
};
assert_eq!(
[
read_back(Some("libron")),
read_back(Some("interface")),
read_back(None)
],
[
Some(serde_json::json!("libron")),
Some(serde_json::json!("interface")),
Some(serde_json::Value::Null),
]
);
assert!(serde_json::from_value::<PreferencesAnswer>(appearance(Some("georgia"))).is_err());
}
#[test]
fn a_key_no_field_claims_is_refused() {
assert!(serde_json::from_str::<PreferencesAnswer>(r#"{"welcomd":true}"#).is_err());
}
}