pfx_gpu/screens/
device.rs1use serde::{Deserialize, Serialize};
2
3pub const DMI_PRODUCT_NAME: &str = "/sys/class/dmi/id/product_name";
4pub const STEAM_DECK_VAR: &str = "SteamDeck";
5pub const DESKTOP_VAR: &str = "XDG_CURRENT_DESKTOP";
6pub const GAMESCOPE_VAR: &str = "GAMESCOPE_WAYLAND_DISPLAY";
7pub const OVERRIDE_VAR: &str = "PFX_DEVICE";
8
9#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash, Serialize, Deserialize)]
10#[serde(rename_all = "lowercase")]
11pub enum DeckModel {
12 Lcd,
13 Oled,
14}
15
16#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash, Serialize, Deserialize)]
17#[serde(rename_all = "snake_case")]
18pub enum Device {
19 SteamDeck { model: DeckModel },
20 Desktop,
21}
22
23impl Device {
24 pub fn is_deck(self) -> bool {
25 matches!(self, Device::SteamDeck { .. })
26 }
27
28 pub fn label(self) -> &'static str {
29 match self {
30 Device::SteamDeck {
31 model: DeckModel::Lcd,
32 } => "steam-deck-lcd",
33 Device::SteamDeck {
34 model: DeckModel::Oled,
35 } => "steam-deck-oled",
36 Device::Desktop => "desktop",
37 }
38 }
39
40 pub fn from_label(label: &str) -> Option<Self> {
41 [
42 Device::SteamDeck {
43 model: DeckModel::Lcd,
44 },
45 Device::SteamDeck {
46 model: DeckModel::Oled,
47 },
48 Device::Desktop,
49 ]
50 .into_iter()
51 .find(|device| {
52 device.label() == label || device.label().strip_prefix("steam-") == Some(label)
53 })
54 }
55}
56
57#[derive(Clone, Debug, PartialEq, Eq)]
58pub enum Evidence {
59 Override,
60 SteamDeckVar,
61 ProductName(String),
62 Gamescope,
63 Nothing,
64}
65
66#[derive(Clone, Debug, PartialEq, Eq)]
67pub struct Detected {
68 pub device: Device,
69 pub evidence: Evidence,
70}
71
72pub trait DeviceSource {
73 fn var(&self, name: &str) -> Option<String>;
74 fn product_name(&self) -> Option<String>;
75}
76
77#[derive(Clone, Copy, Debug, Default)]
78pub struct SystemSource;
79
80impl DeviceSource for SystemSource {
81 fn var(&self, name: &str) -> Option<String> {
82 std::env::var(name).ok()
83 }
84
85 fn product_name(&self) -> Option<String> {
86 std::fs::read_to_string(DMI_PRODUCT_NAME).ok()
87 }
88}
89
90pub fn deck_model(product_name: &str) -> Option<DeckModel> {
91 match product_name.trim() {
92 "Jupiter" => Some(DeckModel::Lcd),
93 "Galileo" => Some(DeckModel::Oled),
94 _ => None,
95 }
96}
97
98pub fn detect(source: &impl DeviceSource) -> Detected {
99 if let Some(device) = source
100 .var(OVERRIDE_VAR)
101 .and_then(|value| Device::from_label(value.trim()))
102 {
103 return Detected {
104 device,
105 evidence: Evidence::Override,
106 };
107 }
108 let product = source.product_name();
109 let model = product.as_deref().and_then(deck_model);
110 if source.var(STEAM_DECK_VAR).as_deref().map(str::trim) == Some("1") {
111 return Detected {
112 device: Device::SteamDeck {
113 model: model.unwrap_or(DeckModel::Lcd),
114 },
115 evidence: Evidence::SteamDeckVar,
116 };
117 }
118 if let Some(model) = model {
119 return Detected {
120 device: Device::SteamDeck { model },
121 evidence: Evidence::ProductName(product.unwrap_or_default().trim().to_owned()),
122 };
123 }
124 let desktop = source
125 .var(DESKTOP_VAR)
126 .is_some_and(|desktop| desktop.trim().eq_ignore_ascii_case("gamescope"));
127 let nested = source
128 .var(GAMESCOPE_VAR)
129 .is_some_and(|display| !display.trim().is_empty());
130 if desktop || nested {
131 return Detected {
132 device: Device::SteamDeck {
133 model: DeckModel::Lcd,
134 },
135 evidence: Evidence::Gamescope,
136 };
137 }
138 Detected {
139 device: Device::Desktop,
140 evidence: Evidence::Nothing,
141 }
142}