#[cfg(test)]
mod tests;
use std::cmp::Reverse;
use std::sync::Arc;
use hidpp::{
channel::HidppChannel,
device::Device,
feature::{
CreatableFeature as _,
hosts_info::HostIndex,
multi_platform::{
MultiPlatformCapabilities, MultiPlatformFeature, OsMask, PlatformDescriptor,
},
},
};
use tracing::debug;
use crate::SharedChannel;
use super::{HidppOperation, WriteError, classify_hidpp_error, open_feature};
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
pub struct HostOs {
pub os: OsMask,
pub version: Option<OsVersion>,
}
impl HostOs {
#[must_use]
pub const fn current() -> Option<Self> {
let os = if cfg!(target_os = "macos") {
OsMask::MACOS
} else if cfg!(windows) {
OsMask::WINDOWS
} else if cfg!(target_os = "linux") {
OsMask::LINUX
} else {
return None;
};
Some(Self { os, version: None })
}
}
#[derive(Clone, Copy, Debug, PartialEq, Eq, PartialOrd, Ord)]
pub struct OsVersion {
pub major: u8,
pub revision: u8,
}
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
pub enum OsLayoutOutcome {
AlreadySet {
platform_index: u8,
},
Switched {
from: Option<u8>,
to: u8,
},
NotSettable,
NoMatchingPlatform,
Withdrawn,
}
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
struct Candidate {
platform: u8,
descriptor: u8,
os: OsMask,
from: u16,
to: u16,
}
impl From<&PlatformDescriptor> for Candidate {
fn from(descriptor: &PlatformDescriptor) -> Self {
Self {
platform: descriptor.platform_index,
descriptor: descriptor.descriptor_index,
os: descriptor.os_mask,
from: u16::from_be_bytes([descriptor.from_version, descriptor.from_revision]),
to: u16::from_be_bytes([descriptor.to_version, descriptor.to_revision]),
}
}
}
impl Candidate {
fn covers(self, host: HostOs) -> bool {
if !self.os.intersects(host.os) {
return false;
}
match host.version {
Some(version) => {
let version = u16::from_be_bytes([version.major, version.revision]);
(self.from == 0 || version >= self.from) && (self.to == 0 || version <= self.to)
}
None => self.to == 0,
}
}
}
fn select_platform(candidates: impl IntoIterator<Item = Candidate>, host: HostOs) -> Option<u8> {
candidates
.into_iter()
.filter(|candidate| candidate.covers(host))
.min_by_key(|candidate| {
(
Reverse(candidate.to),
candidate.from,
candidate.platform,
candidate.descriptor,
)
})
.map(|candidate| candidate.platform)
}
pub async fn keep_os_layout_on(
shared: &SharedChannel,
host: HostOs,
still_wanted: impl Fn() -> bool,
) -> Result<OsLayoutOutcome, WriteError> {
keep_os_layout_on_channel(shared.channel(), shared.device_index(), host, still_wanted).await
}
async fn keep_os_layout_on_channel(
channel: &Arc<HidppChannel>,
index: u8,
host: HostOs,
still_wanted: impl Fn() -> bool,
) -> Result<OsLayoutOutcome, WriteError> {
if !still_wanted() {
return Ok(OsLayoutOutcome::Withdrawn);
}
let classify =
|e| classify_hidpp_error(e, HidppOperation::WriteOsLayout, MultiPlatformFeature::ID);
let mut device = Device::new(Arc::clone(channel), index)
.await
.map_err(|_| WriteError::DeviceUnreachable { index })?;
let feature = open_feature::<MultiPlatformFeature>(&mut device).await?;
let info = feature.get_feature_infos().await.map_err(classify)?;
let slot = match info.current_host {
HostIndex::Slot(slot) if slot < info.host_count => HostIndex::Slot(slot),
_ => return Ok(OsLayoutOutcome::NotSettable),
};
if !info
.capabilities
.contains(MultiPlatformCapabilities::SET_HOST_PLATFORM)
{
return Ok(OsLayoutOutcome::NotSettable);
}
let mut descriptors = Vec::with_capacity(usize::from(info.descriptor_count));
for descriptor_index in 0..info.descriptor_count {
descriptors.push(
feature
.get_platform_descriptor(descriptor_index)
.await
.map_err(classify)?,
);
}
let Some(target) = select_platform(descriptors.iter().map(Candidate::from), host)
.filter(|&platform| platform < info.platform_count)
else {
return Ok(OsLayoutOutcome::NoMatchingPlatform);
};
let before = feature
.get_host_platform(slot)
.await
.map_err(classify)?
.platform_index;
if before == Some(target) {
return Ok(OsLayoutOutcome::AlreadySet {
platform_index: target,
});
}
if !still_wanted() {
return Ok(OsLayoutOutcome::Withdrawn);
}
feature
.set_host_platform(slot, target)
.await
.map_err(classify)?;
let after = feature
.get_host_platform(slot)
.await
.map_err(classify)?
.platform_index;
if after != Some(target) {
return Err(WriteError::UnsupportedResponse {
operation: HidppOperation::WriteOsLayout,
feature_hex: MultiPlatformFeature::ID,
});
}
debug!(
index,
?before,
target,
"keyboard platform set to the host OS"
);
Ok(OsLayoutOutcome::Switched {
from: before,
to: target,
})
}