#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum PersonaOs {
Windows,
MacOs,
Linux,
AndroidMobile,
}
impl PersonaOs {
pub fn as_str(self) -> &'static str {
match self {
Self::Windows => "Windows",
Self::MacOs => "macOS",
Self::Linux => "Linux",
Self::AndroidMobile => "Android",
}
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum PersonaGpu {
Intel,
Nvidia,
Amd,
Apple,
AdrenoMobile,
}
impl PersonaGpu {
pub fn keyword(self) -> &'static str {
match self {
Self::Intel => "intel",
Self::Nvidia => "nvidia",
Self::Amd => "amd",
Self::Apple => "apple",
Self::AdrenoMobile => "adreno",
}
}
}
#[derive(Debug, Clone)]
pub struct PersonaProfile {
pub name: &'static str,
pub description: &'static str,
pub os: PersonaOs,
pub platform: &'static str, pub ua_platform: &'static str, pub locale: &'static str,
pub languages_json: &'static str,
pub accept_language: &'static str,
pub timezone: &'static str,
pub tz_offset_min: i32,
pub screen_w: u32,
pub screen_h: u32,
pub avail_screen_w: u32,
pub avail_screen_h: u32,
pub viewport_w: u32,
pub viewport_h: u32,
pub device_pixel_ratio: f32,
pub color_depth: u32,
pub scrollbar_width: u32,
pub device_memory: u32,
pub hardware_concurrency: u32,
pub heap_size_limit: u64,
pub gpu: PersonaGpu,
pub webgl_vendor: &'static str,
pub webgl_renderer: &'static str,
pub webgl_unmasked_vendor: &'static str,
pub webgl_unmasked_renderer: &'static str,
pub webgpu_adapter_description: &'static str,
pub max_texture_size: u32,
pub max_viewport_dims: (u32, u32),
pub audio_sample_rate: u32,
pub fonts_json: &'static str,
pub media_mic_count: u8,
pub media_cam_count: u8,
pub media_speaker_count: u8,
}
pub fn catalog() -> &'static [PersonaProfile] {
&PERSONA_CATALOG
}
pub fn pick(seed: u64) -> &'static PersonaProfile {
let idx = (seed as usize) % PERSONA_CATALOG.len();
&PERSONA_CATALOG[idx]
}
pub fn first_for(os: PersonaOs) -> Option<&'static PersonaProfile> {
PERSONA_CATALOG.iter().find(|p| p.os == os)
}
pub fn lookup_by_name(name: &str) -> Option<&'static PersonaProfile> {
let lower = name.to_ascii_lowercase();
PERSONA_CATALOG.iter().find(|p| p.name == lower)
}
pub fn names() -> &'static [&'static str] {
&["tux", "office", "gamer", "atlas", "pixel"]
}
const FONTS_LINUX: &str = r#"["DejaVu Sans","DejaVu Serif","DejaVu Sans Mono","Liberation Sans","Liberation Serif","Liberation Mono","Noto Sans","Noto Serif","Ubuntu","Ubuntu Mono","FreeSans","FreeMono","Droid Sans"]"#;
const FONTS_WINDOWS: &str = r#"["Arial","Arial Black","Calibri","Cambria","Candara","Comic Sans MS","Consolas","Courier New","Georgia","Impact","Lucida Console","Lucida Sans Unicode","Microsoft Sans Serif","Palatino Linotype","Segoe UI","Tahoma","Times New Roman","Trebuchet MS","Verdana","Webdings"]"#;
const FONTS_MACOS: &str = r#"["Apple Color Emoji","Helvetica","Helvetica Neue","Lucida Grande","Menlo","Monaco","Optima","SF Pro","SF Pro Display","SF Pro Text","SF Mono","Courier","Courier New","Geneva","Georgia","Times","Times New Roman"]"#;
const FONTS_ANDROID: &str = r#"["Roboto","Noto Sans","Noto Serif","Droid Sans","Droid Serif","Droid Sans Mono","sans-serif","serif","monospace"]"#;
const PERSONA_CATALOG: [PersonaProfile; 5] = [
PersonaProfile {
name: "tux",
description: "Linux desktop, Intel UHD 630, en-US, America/Sao_Paulo",
os: PersonaOs::Linux,
platform: "Linux x86_64",
ua_platform: "Linux",
locale: "en-US",
languages_json: r#"["en-US","en"]"#,
accept_language: "en-US,en;q=0.9",
timezone: "America/Sao_Paulo",
tz_offset_min: 180,
screen_w: 1920,
screen_h: 1080,
avail_screen_w: 1920,
avail_screen_h: 1050,
viewport_w: 1920,
viewport_h: 960,
device_pixel_ratio: 1.0,
color_depth: 24,
scrollbar_width: 15,
device_memory: 8,
hardware_concurrency: 8,
heap_size_limit: 2_147_483_648,
gpu: PersonaGpu::Intel,
webgl_vendor: "Google Inc. (Intel)",
webgl_renderer: "ANGLE (Intel, Mesa Intel(R) UHD Graphics 630 (CFL GT2), OpenGL 4.6)",
webgl_unmasked_vendor: "Google Inc. (Intel)",
webgl_unmasked_renderer:
"ANGLE (Intel, Mesa Intel(R) UHD Graphics 630 (CFL GT2), OpenGL 4.6)",
webgpu_adapter_description:
"ANGLE (Intel, Mesa Intel(R) UHD Graphics 630 (CFL GT2), OpenGL 4.6)",
max_texture_size: 16384,
max_viewport_dims: (32767, 32767),
audio_sample_rate: 48000,
fonts_json: FONTS_LINUX,
media_mic_count: 2,
media_cam_count: 1,
media_speaker_count: 2,
},
PersonaProfile {
name: "office",
description: "Windows 10 laptop, Intel UHD 620, en-US, America/New_York",
os: PersonaOs::Windows,
platform: "Win32",
ua_platform: "Windows",
locale: "en-US",
languages_json: r#"["en-US","en"]"#,
accept_language: "en-US,en;q=0.9",
timezone: "America/New_York",
tz_offset_min: 300,
screen_w: 1920,
screen_h: 1080,
avail_screen_w: 1920,
avail_screen_h: 1040,
viewport_w: 1536,
viewport_h: 864,
device_pixel_ratio: 1.25,
color_depth: 24,
scrollbar_width: 17,
device_memory: 8,
hardware_concurrency: 8,
heap_size_limit: 2_147_483_648,
gpu: PersonaGpu::Intel,
webgl_vendor: "Google Inc. (Intel)",
webgl_renderer:
"ANGLE (Intel, Intel(R) UHD Graphics 620 Direct3D11 vs_5_0 ps_5_0), or similar",
webgl_unmasked_vendor: "Google Inc. (Intel)",
webgl_unmasked_renderer:
"ANGLE (Intel, Intel(R) UHD Graphics 620 Direct3D11 vs_5_0 ps_5_0), or similar",
webgpu_adapter_description: "Intel(R) UHD Graphics 620",
max_texture_size: 16384,
max_viewport_dims: (32767, 32767),
audio_sample_rate: 48000,
fonts_json: FONTS_WINDOWS,
media_mic_count: 3,
media_cam_count: 1,
media_speaker_count: 2,
},
PersonaProfile {
name: "gamer",
description: "Windows 10 desktop, NVIDIA GTX 1060, pt-BR, America/Sao_Paulo",
os: PersonaOs::Windows,
platform: "Win32",
ua_platform: "Windows",
locale: "pt-BR",
languages_json: r#"["pt-BR","pt","en-US","en"]"#,
accept_language: "pt-BR,pt;q=0.9,en-US;q=0.8,en;q=0.7",
timezone: "America/Sao_Paulo",
tz_offset_min: 180,
screen_w: 1920,
screen_h: 1080,
avail_screen_w: 1920,
avail_screen_h: 1040,
viewport_w: 1920,
viewport_h: 969,
device_pixel_ratio: 1.0,
color_depth: 24,
scrollbar_width: 17,
device_memory: 8,
hardware_concurrency: 12,
heap_size_limit: 4_294_967_296,
gpu: PersonaGpu::Nvidia,
webgl_vendor: "Google Inc. (NVIDIA)",
webgl_renderer:
"ANGLE (NVIDIA, NVIDIA GeForce GTX 1060 Direct3D11 vs_5_0 ps_5_0), or similar",
webgl_unmasked_vendor: "Google Inc. (NVIDIA)",
webgl_unmasked_renderer:
"ANGLE (NVIDIA, NVIDIA GeForce GTX 1060 Direct3D11 vs_5_0 ps_5_0), or similar",
webgpu_adapter_description: "NVIDIA GeForce GTX 1060",
max_texture_size: 32768,
max_viewport_dims: (32768, 32768),
audio_sample_rate: 48000,
fonts_json: FONTS_WINDOWS,
media_mic_count: 3,
media_cam_count: 1,
media_speaker_count: 2,
},
PersonaProfile {
name: "atlas",
description: "macOS laptop, Apple M1 (8-core), en-US, America/Los_Angeles",
os: PersonaOs::MacOs,
platform: "MacIntel",
ua_platform: "macOS",
locale: "en-US",
languages_json: r#"["en-US","en"]"#,
accept_language: "en-US,en;q=0.9",
timezone: "America/Los_Angeles",
tz_offset_min: 480,
screen_w: 1512,
screen_h: 982,
avail_screen_w: 1512,
avail_screen_h: 944,
viewport_w: 1440,
viewport_h: 820,
device_pixel_ratio: 2.0,
color_depth: 30,
scrollbar_width: 0,
device_memory: 8,
hardware_concurrency: 8,
heap_size_limit: 2_147_483_648,
gpu: PersonaGpu::Apple,
webgl_vendor: "Google Inc. (Apple)",
webgl_renderer: "ANGLE (Apple, Apple M1, OpenGL 4.1)",
webgl_unmasked_vendor: "Google Inc. (Apple)",
webgl_unmasked_renderer: "ANGLE (Apple, Apple M1, OpenGL 4.1)",
webgpu_adapter_description: "Apple M1",
max_texture_size: 16384,
max_viewport_dims: (16384, 16384),
audio_sample_rate: 48000,
fonts_json: FONTS_MACOS,
media_mic_count: 2,
media_cam_count: 1,
media_speaker_count: 1,
},
PersonaProfile {
name: "pixel",
description: "Android mobile (Pixel-class), Adreno 640, pt-BR, America/Sao_Paulo",
os: PersonaOs::AndroidMobile,
platform: "Linux armv8l",
ua_platform: "Android",
locale: "pt-BR",
languages_json: r#"["pt-BR","pt","en-US","en"]"#,
accept_language: "pt-BR,pt;q=0.9,en-US;q=0.8,en;q=0.7",
timezone: "America/Sao_Paulo",
tz_offset_min: 180,
screen_w: 412,
screen_h: 892,
avail_screen_w: 412,
avail_screen_h: 892,
viewport_w: 412,
viewport_h: 823,
device_pixel_ratio: 2.625,
color_depth: 24,
scrollbar_width: 0,
device_memory: 4,
hardware_concurrency: 8,
heap_size_limit: 536_870_912,
gpu: PersonaGpu::AdrenoMobile,
webgl_vendor: "Google Inc. (Qualcomm)",
webgl_renderer: "ANGLE (Qualcomm, Adreno (TM) 640, OpenGL ES 3.2)",
webgl_unmasked_vendor: "Google Inc. (Qualcomm)",
webgl_unmasked_renderer: "ANGLE (Qualcomm, Adreno (TM) 640, OpenGL ES 3.2)",
webgpu_adapter_description: "Adreno (TM) 640",
max_texture_size: 8192,
max_viewport_dims: (8192, 8192),
audio_sample_rate: 48000,
fonts_json: FONTS_ANDROID,
media_mic_count: 1,
media_cam_count: 2,
media_speaker_count: 1,
},
];
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn catalog_has_five_rows() {
assert_eq!(catalog().len(), 5);
}
#[test]
fn every_persona_has_unique_name() {
let names: Vec<&str> = catalog().iter().map(|p| p.name).collect();
let mut sorted = names.clone();
sorted.sort_unstable();
sorted.dedup();
assert_eq!(
names.len(),
sorted.len(),
"duplicate persona name in catalog: {names:?}"
);
for n in &names {
assert!(!n.is_empty(), "empty persona name");
assert!(
n.chars().all(|c| c.is_ascii_lowercase() || c == '-'),
"persona name `{n}` should be lowercase ASCII (with optional dashes)"
);
}
}
#[test]
fn lookup_by_name_is_case_insensitive() {
assert!(lookup_by_name("tux").is_some());
assert!(lookup_by_name("TUX").is_some());
assert!(lookup_by_name("Tux").is_some());
assert!(lookup_by_name("nonexistent").is_none());
}
#[test]
fn names_constant_matches_catalog_order() {
let cat_names: Vec<&str> = catalog().iter().map(|p| p.name).collect();
assert_eq!(cat_names, names());
}
#[test]
fn pixel_persona_is_the_only_mobile() {
let mobile_count = catalog()
.iter()
.filter(|p| p.os == PersonaOs::AndroidMobile)
.count();
assert_eq!(mobile_count, 1);
let pixel = lookup_by_name("pixel").expect("pixel persona present");
assert_eq!(pixel.os, PersonaOs::AndroidMobile);
}
#[test]
fn each_row_has_avail_not_exceeding_screen() {
for p in catalog() {
assert!(p.avail_screen_w <= p.screen_w, "row os={:?}", p.os);
assert!(p.avail_screen_h <= p.screen_h, "row os={:?}", p.os);
assert!(p.viewport_w <= p.avail_screen_w, "row os={:?}", p.os);
assert!(p.viewport_h <= p.avail_screen_h, "row os={:?}", p.os);
}
}
#[test]
fn gpu_and_os_coherent() {
for p in catalog() {
match (p.gpu, p.os) {
(PersonaGpu::Apple, PersonaOs::MacOs) => {}
(PersonaGpu::Apple, _) => panic!("Apple GPU on non-mac row"),
(PersonaGpu::AdrenoMobile, PersonaOs::AndroidMobile) => {}
(PersonaGpu::AdrenoMobile, _) => panic!("Adreno on non-mobile row"),
(PersonaGpu::Intel | PersonaGpu::Nvidia | PersonaGpu::Amd, _) => {}
}
}
}
#[test]
fn fonts_match_os_keyword() {
for p in catalog() {
let f = p.fonts_json;
match p.os {
PersonaOs::Linux => {
assert!(
f.contains("DejaVu") || f.contains("Liberation"),
"linux fonts"
);
assert!(!f.contains("Segoe UI"), "windows font in linux row");
}
PersonaOs::Windows => {
assert!(
f.contains("Segoe UI") || f.contains("Calibri"),
"windows fonts"
);
assert!(!f.contains("DejaVu"), "linux font in windows row");
assert!(!f.contains("Helvetica Neue"), "mac font in windows row");
}
PersonaOs::MacOs => {
assert!(f.contains("Helvetica") || f.contains("SF Pro"), "mac fonts");
assert!(!f.contains("Segoe UI"), "windows font in mac row");
assert!(!f.contains("DejaVu"), "linux font in mac row");
}
PersonaOs::AndroidMobile => {
assert!(f.contains("Roboto"), "android needs Roboto");
}
}
}
}
#[test]
fn pick_is_deterministic() {
let a = pick(0xdead_beef);
let b = pick(0xdead_beef);
assert_eq!(a.os, b.os);
assert_eq!(a.gpu as u32, b.gpu as u32);
}
}