1#[cfg(test)]
15mod tests;
16
17use std::cmp::Reverse;
18use std::sync::Arc;
19
20use hidpp::{
21 channel::HidppChannel,
22 device::Device,
23 feature::{
24 CreatableFeature as _,
25 hosts_info::HostIndex,
26 multi_platform::{
27 MultiPlatformCapabilities, MultiPlatformFeature, OsMask, PlatformDescriptor,
28 },
29 },
30};
31use tracing::debug;
32
33use crate::SharedChannel;
34
35use super::{HidppOperation, WriteError, classify_hidpp_error, open_feature};
36
37#[derive(Clone, Copy, Debug, PartialEq, Eq)]
39pub struct HostOs {
40 pub os: OsMask,
42 pub version: Option<OsVersion>,
46}
47
48impl HostOs {
49 #[must_use]
52 pub const fn current() -> Option<Self> {
53 let os = if cfg!(target_os = "macos") {
54 OsMask::MACOS
55 } else if cfg!(windows) {
56 OsMask::WINDOWS
57 } else if cfg!(target_os = "linux") {
58 OsMask::LINUX
59 } else {
60 return None;
61 };
62 Some(Self { os, version: None })
63 }
64}
65
66#[derive(Clone, Copy, Debug, PartialEq, Eq, PartialOrd, Ord)]
69pub struct OsVersion {
70 pub major: u8,
72 pub revision: u8,
74}
75
76#[derive(Clone, Copy, Debug, PartialEq, Eq)]
78pub enum OsLayoutOutcome {
79 AlreadySet {
81 platform_index: u8,
83 },
84 Switched {
86 from: Option<u8>,
88 to: u8,
90 },
91 NotSettable,
94 NoMatchingPlatform,
96 Withdrawn,
99}
100
101#[derive(Clone, Copy, Debug, PartialEq, Eq)]
105struct Candidate {
106 platform: u8,
107 descriptor: u8,
108 os: OsMask,
109 from: u16,
110 to: u16,
111}
112
113impl From<&PlatformDescriptor> for Candidate {
114 fn from(descriptor: &PlatformDescriptor) -> Self {
115 Self {
116 platform: descriptor.platform_index,
117 descriptor: descriptor.descriptor_index,
118 os: descriptor.os_mask,
119 from: u16::from_be_bytes([descriptor.from_version, descriptor.from_revision]),
120 to: u16::from_be_bytes([descriptor.to_version, descriptor.to_revision]),
121 }
122 }
123}
124
125impl Candidate {
126 fn covers(self, host: HostOs) -> bool {
128 if !self.os.intersects(host.os) {
129 return false;
130 }
131 match host.version {
132 Some(version) => {
133 let version = u16::from_be_bytes([version.major, version.revision]);
134 (self.from == 0 || version >= self.from) && (self.to == 0 || version <= self.to)
135 }
136 None => self.to == 0,
137 }
138 }
139}
140
141fn select_platform(candidates: impl IntoIterator<Item = Candidate>, host: HostOs) -> Option<u8> {
147 candidates
148 .into_iter()
149 .filter(|candidate| candidate.covers(host))
150 .min_by_key(|candidate| {
151 (
152 Reverse(candidate.to),
153 candidate.from,
154 candidate.platform,
155 candidate.descriptor,
156 )
157 })
158 .map(|candidate| candidate.platform)
159}
160
161pub async fn keep_os_layout_on(
171 shared: &SharedChannel,
172 host: HostOs,
173 still_wanted: impl Fn() -> bool,
174) -> Result<OsLayoutOutcome, WriteError> {
175 keep_os_layout_on_channel(shared.channel(), shared.device_index(), host, still_wanted).await
176}
177
178async fn keep_os_layout_on_channel(
179 channel: &Arc<HidppChannel>,
180 index: u8,
181 host: HostOs,
182 still_wanted: impl Fn() -> bool,
183) -> Result<OsLayoutOutcome, WriteError> {
184 if !still_wanted() {
185 return Ok(OsLayoutOutcome::Withdrawn);
186 }
187 let classify =
188 |e| classify_hidpp_error(e, HidppOperation::WriteOsLayout, MultiPlatformFeature::ID);
189 let mut device = Device::new(Arc::clone(channel), index)
190 .await
191 .map_err(|_| WriteError::DeviceUnreachable { index })?;
192 let feature = open_feature::<MultiPlatformFeature>(&mut device).await?;
193 let info = feature.get_feature_infos().await.map_err(classify)?;
194 let slot = match info.current_host {
195 HostIndex::Slot(slot) if slot < info.host_count => HostIndex::Slot(slot),
196 _ => return Ok(OsLayoutOutcome::NotSettable),
197 };
198 if !info
199 .capabilities
200 .contains(MultiPlatformCapabilities::SET_HOST_PLATFORM)
201 {
202 return Ok(OsLayoutOutcome::NotSettable);
203 }
204 let mut descriptors = Vec::with_capacity(usize::from(info.descriptor_count));
205 for descriptor_index in 0..info.descriptor_count {
206 descriptors.push(
207 feature
208 .get_platform_descriptor(descriptor_index)
209 .await
210 .map_err(classify)?,
211 );
212 }
213 let Some(target) = select_platform(descriptors.iter().map(Candidate::from), host)
214 .filter(|&platform| platform < info.platform_count)
215 else {
216 return Ok(OsLayoutOutcome::NoMatchingPlatform);
217 };
218 let before = feature
219 .get_host_platform(slot)
220 .await
221 .map_err(classify)?
222 .platform_index;
223 if before == Some(target) {
224 return Ok(OsLayoutOutcome::AlreadySet {
225 platform_index: target,
226 });
227 }
228 if !still_wanted() {
229 return Ok(OsLayoutOutcome::Withdrawn);
230 }
231 feature
232 .set_host_platform(slot, target)
233 .await
234 .map_err(classify)?;
235 let after = feature
236 .get_host_platform(slot)
237 .await
238 .map_err(classify)?
239 .platform_index;
240 if after != Some(target) {
241 return Err(WriteError::UnsupportedResponse {
242 operation: HidppOperation::WriteOsLayout,
243 feature_hex: MultiPlatformFeature::ID,
244 });
245 }
246 debug!(
247 index,
248 ?before,
249 target,
250 "keyboard platform set to the host OS"
251 );
252 Ok(OsLayoutOutcome::Switched {
253 from: before,
254 to: target,
255 })
256}