use anyhow::{Result, anyhow};
use clap::Subcommand;
use openlogi_hid::{DeviceRoute, dump_features};
pub mod dpi;
pub mod features;
pub mod lighting;
pub mod smartshift;
#[derive(Debug, Subcommand)]
pub enum DiagCmd {
Features(features::FeaturesArgs),
Dpi(dpi::DpiArgs),
Smartshift(smartshift::SmartshiftArgs),
Lighting(lighting::LightingArgs),
}
impl DiagCmd {
pub async fn run(self) -> Result<()> {
match self {
Self::Features(args) => features::run(args).await,
Self::Dpi(args) => dpi::run(args).await,
Self::Smartshift(args) => smartshift::run(args).await,
Self::Lighting(args) => lighting::run(args).await,
}
}
}
struct Candidate {
route: DeviceRoute,
name: String,
}
async fn online_devices() -> Result<Vec<Candidate>> {
let inventories = openlogi_hid::enumerate().await?;
let mut out = Vec::new();
for inv in inventories {
for paired in inv.paired.into_iter().filter(|p| p.online) {
let route = match &inv.receiver.unique_id {
Some(uid) => DeviceRoute::Bolt {
receiver_uid: uid.clone(),
slot: paired.slot,
},
None => DeviceRoute::Direct {
vendor_id: inv.receiver.vendor_id,
product_id: inv.receiver.product_id,
},
};
let name = paired
.codename
.unwrap_or_else(|| format!("Slot {}", paired.slot));
out.push(Candidate { route, name });
}
}
Ok(out)
}
fn no_match_err(devices: &[Candidate], query: Option<&str>) -> anyhow::Error {
if devices.is_empty() {
return anyhow!("no online HID++ device found — is a Logi device paired and awake?");
}
let list = devices
.iter()
.map(|c| format!(" - {} ({})", c.name, c.route))
.collect::<Vec<_>>()
.join("\n");
match query {
Some(q) => anyhow!("no online device matches `--device {q}`.\n online devices:\n{list}"),
None => anyhow!(
"could not pick a device automatically.\n online devices:\n{list}\n \
pass --device <name> to choose one."
),
}
}
pub(crate) async fn select_device(
query: Option<&str>,
required_features: &[u16],
) -> Result<(DeviceRoute, String)> {
let devices = online_devices().await?;
if let Some(q) = query {
let needle = q.to_lowercase();
return devices
.iter()
.find(|c| c.name.to_lowercase().contains(&needle))
.map(|c| (c.route.clone(), c.name.clone()))
.ok_or_else(|| no_match_err(&devices, query));
}
if !required_features.is_empty() {
for c in &devices {
match dump_features(&c.route).await {
Ok(entries) => {
if entries.iter().any(|e| required_features.contains(&e.id)) {
return Ok((c.route.clone(), c.name.clone()));
}
}
Err(e) => {
tracing::warn!(
"skipping {} ({}): feature probe failed: {e:#}",
c.name,
c.route
);
}
}
}
}
devices
.into_iter()
.next()
.map(|c| (c.route, c.name))
.ok_or_else(|| no_match_err(&[], None))
}