use std::collections::{BTreeMap, BTreeSet};
use std::sync::{Arc, Mutex, PoisonError};
use hidpp::protocol::v20;
use openlogi_core::binding::ButtonId;
use tokio::sync::mpsc;
use tracing::{debug, info, warn};
use super::capture::{ArmedCapture, CaptureHost, Liveness, open_device, run_capture};
use super::capture_restore::{ArmedReporting, ReprogRestore, divert_change};
use super::gesture::{
CaptureError, CaptureSessionFailure, CaptureSessionOutcome, CapturedInput,
PendingCaptureRestore, enumerate_controls,
};
use super::restore::rollback_start;
use crate::channel::route::DeviceRoute;
use crate::{ChannelRegistry, SharedChannel};
use crate::reprog_controls::{self, RawControlEvent, ReprogControlsV4};
pub const RESERVED_KEYBOARD_CONTROLS: [u16; 5] = [
reprog_controls::control_ids::HOST_SWITCH_CHANNEL_1.0,
reprog_controls::control_ids::HOST_SWITCH_CHANNEL_2.0,
reprog_controls::control_ids::HOST_SWITCH_CHANNEL_3.0,
0x0050,
0x0051,
];
#[must_use]
pub fn is_reserved_keyboard_control(cid: u16) -> bool {
RESERVED_KEYBOARD_CONTROLS.contains(&cid)
}
pub async fn run_keyboard_capture_session(
route: DeviceRoute,
wanted: BTreeMap<u16, ButtonId>,
host: CaptureHost<'_>,
) -> Result<CaptureSessionOutcome, CaptureSessionFailure> {
let shared = host.channel_for(&route)?;
let armed = arm_keyboard(&shared, &wanted, host.registry).await?;
Ok(run_capture(shared, armed, host).await)
}
async fn arm_keyboard(
shared: &SharedChannel,
wanted: &BTreeMap<u16, ButtonId>,
registry: &ChannelRegistry,
) -> Result<ArmedKeys, CaptureSessionFailure> {
let device = open_device(shared).await?;
let info = device
.root()
.get_feature(reprog_controls::FEATURE_ID)
.await
.map_err(CaptureError::from)?
.ok_or_else(|| CaptureError::Hidpp("keyboard exposes no 0x1b04 reprog controls".into()))?;
let rc = ReprogControlsV4::new(
Arc::clone(shared.channel()),
shared.device_index(),
info.index,
);
let controls = enumerate_controls(&rc).await?;
let mut armed = ArmedKeys {
controls: rc,
reporting: Vec::new(),
diverted: BTreeMap::new(),
};
if let Err(error) = arm_keys(&controls, wanted, &mut armed).await {
let pending = armed.into_pending(shared);
return Err(rollback_start(error, pending, registry).await);
}
Ok(armed)
}
fn emit_button_edges(
down: &mut BTreeSet<u16>,
cids: &[u16],
diverted: &BTreeMap<u16, ButtonId>,
sink: &mpsc::UnboundedSender<CapturedInput>,
) {
for (&cid, &button) in diverted {
let now = cids.contains(&cid);
let was = down.contains(&cid);
if now && !was {
let _ = sink.send(CapturedInput::ButtonDown(button));
} else if !now && was {
let _ = sink.send(CapturedInput::ButtonUp(button));
}
if now {
down.insert(cid);
} else {
down.remove(&cid);
}
}
}
struct ArmedKeys {
controls: ReprogControlsV4,
reporting: Vec<ArmedReporting>,
diverted: BTreeMap<u16, ButtonId>,
}
impl ArmedCapture for ArmedKeys {
const NAME: &'static str = "keyboard key";
const LIVENESS: Liveness = Liveness::Unwatched;
fn log_active(&self, device_index: u8, wake_rearm: bool) {
info!(
index = device_index,
keys = self.diverted.len(),
wake_rearm,
"keyboard key capture active"
);
}
fn report_handler(
&self,
device_index: u8,
sink: mpsc::UnboundedSender<CapturedInput>,
) -> impl Fn(&v20::Message) + Send + Sync + 'static {
let held: Mutex<BTreeSet<u16>> = Mutex::new(BTreeSet::new());
let feature_index = self.controls.feature_index();
let diverted = self.diverted.clone();
move |msg| {
let Some(RawControlEvent::DivertedButtons(cids)) =
reprog_controls::decode_event(msg, device_index, feature_index)
else {
return;
};
let mut down = held.lock().unwrap_or_else(PoisonError::into_inner);
emit_button_edges(&mut down, &cids, &diverted, &sink);
}
}
async fn rearm(&self) {
for &reporting in &self.reporting {
if let Err(e) = self
.controls
.set_cid_reporting_full(reporting.cid, divert_change(reporting.original, false))
.await
{
warn!(
cid = format_args!("{:#06x}", reporting.cid),
error = ?e,
"re-divert after wake failed — key stays native until next wake"
);
}
}
}
fn into_pending(self, retired: &SharedChannel) -> Option<PendingCaptureRestore> {
let feature_index = self.controls.feature_index();
PendingCaptureRestore::new(
retired,
ReprogRestore::new(feature_index, self.reporting),
None,
)
}
}
async fn arm_keys(
controls: &[reprog_controls::CtrlIdInfo],
wanted: &BTreeMap<u16, ButtonId>,
armed: &mut ArmedKeys,
) -> Result<(), CaptureError> {
for (&cid, &button) in wanted {
if is_reserved_keyboard_control(cid) {
warn!(
cid = format_args!("{cid:#06x}"),
"reserved control requested as a keyboard key — left native"
);
continue;
}
if controls.iter().any(|c| c.cid == cid && c.is_divertable()) {
let original = armed.controls.get_cid_reporting(cid).await?;
armed.reporting.push(ArmedReporting { cid, original });
armed
.controls
.set_cid_reporting_full(cid, divert_change(original, false))
.await?;
armed.diverted.insert(cid, button);
} else {
debug!(
cid = format_args!("{cid:#06x}"),
"bound key not divertable on this keyboard — left native"
);
}
}
Ok(())
}
#[cfg(test)]
mod tests {
use super::*;
use openlogi_core::binding::KNOWN_CONTROLS;
#[test]
fn keyboard_catalog_never_claims_a_mouse_owned_or_reserved_control() {
let mouse_owned: Vec<u16> = reprog_controls::BACK_CIDS
.into_iter()
.chain(reprog_controls::FORWARD_CIDS)
.chain(reprog_controls::DPI_MODE_SHIFT_CIDS)
.chain([
reprog_controls::GESTURE_BUTTON_CID,
reprog_controls::HAPTIC_PANEL_CID,
])
.chain(RESERVED_KEYBOARD_CONTROLS)
.collect();
let claimed: Vec<_> = KNOWN_CONTROLS
.iter()
.filter(|control| mouse_owned.contains(&control.id.raw()))
.map(|control| control.name)
.collect();
assert!(claimed.is_empty(), "{claimed:?}");
}
#[test]
fn keyboard_snapshots_emit_balanced_edges_without_duplicates() {
let diverted = BTreeMap::from([
(0x00d4, ButtonId::control(0x00d4)),
(0x0103, ButtonId::control(0x0103)),
]);
let (sink, mut inputs) = mpsc::unbounded_channel();
let mut down = BTreeSet::new();
emit_button_edges(&mut down, &[0x00d4], &diverted, &sink);
emit_button_edges(&mut down, &[0x00d4], &diverted, &sink);
emit_button_edges(&mut down, &[0x00d4, 0x0103], &diverted, &sink);
emit_button_edges(&mut down, &[0x0103], &diverted, &sink);
emit_button_edges(&mut down, &[], &diverted, &sink);
assert_eq!(
std::iter::from_fn(|| inputs.try_recv().ok()).collect::<Vec<_>>(),
vec![
CapturedInput::ButtonDown(ButtonId::control(0x00d4)),
CapturedInput::ButtonDown(ButtonId::control(0x0103)),
CapturedInput::ButtonUp(ButtonId::control(0x00d4)),
CapturedInput::ButtonUp(ButtonId::control(0x0103)),
]
);
}
}