Skip to main content

pfx_gpu/screens/
device.rs

1use 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}