use std::collections::{BTreeMap, BTreeSet};
use std::sync::{Arc, Mutex, PoisonError};
use hidpp::{
device::Device,
feature::{
CreatableFeature, EmittingFeature,
wireless_device_status::{WirelessDeviceStatusEvent, WirelessDeviceStatusFeature},
},
protocol::v20,
};
use openlogi_core::binding::ButtonId;
use tokio::sync::{mpsc, oneshot};
use tracing::{debug, info, warn};
use super::capture_restore::{
ArmedReporting, CaptureStop, ReprogRestore, divert_change, drop_listener_after,
restore_after_stop, rollback_capture_start, stop_for_current_publication,
wait_for_channel_change,
};
use super::gesture::{
CaptureChannel, CaptureSessionFailure, CaptureSessionOutcome, CapturedInput, GestureError,
PendingCaptureRestore, enumerate_controls,
};
use crate::backend::{BackendError, HidBackend};
use crate::channel::route::{DeviceRoute, open_route_channel};
use crate::{ChannelRegistry, DeviceIoGate, SharedChannel};
use crate::reprog_controls::{self, RawControlEvent, ReprogControlsV4};
pub const KEYBOARD_KEY_CIDS: [(u16, ButtonId); 9] = [
(0x00d4, ButtonId::KeySearch),
(0x0103, ButtonId::KeyDictation),
(0x0108, ButtonId::KeyEmoji),
(0x010a, ButtonId::KeyScreenCapture),
(0x011c, ButtonId::KeyMicMute),
(0x00e5, ButtonId::KeyPlayPause),
(0x00e7, ButtonId::KeyMute),
(0x00e8, ButtonId::KeyVolumeDown),
(0x00e9, ButtonId::KeyVolumeUp),
];
pub async fn run_keyboard_capture_session(
backend: &dyn HidBackend,
route: DeviceRoute,
wanted: BTreeMap<u16, ButtonId>,
sink: mpsc::UnboundedSender<CapturedInput>,
shutdown: oneshot::Receiver<()>,
channel_slot: CaptureChannel,
device_io: DeviceIoGate,
) -> Result<CaptureSessionOutcome, CaptureSessionFailure> {
if !device_io.allows_io() {
return Err(device_io_suspended().into());
}
let chan = open_route_channel(backend, &route)
.await
.map_err(GestureError::from)?
.ok_or(GestureError::DeviceNotFound)?;
let shared = SharedChannel::new(chan, route.clone());
run_keyboard_capture_session_on(
shared,
wanted,
sink,
shutdown,
channel_slot,
None,
device_io,
)
.await
}
pub async fn run_keyboard_capture_session_with_registry(
route: DeviceRoute,
wanted: BTreeMap<u16, ButtonId>,
sink: mpsc::UnboundedSender<CapturedInput>,
shutdown: oneshot::Receiver<()>,
channel_slot: CaptureChannel,
registry: &ChannelRegistry,
device_io: DeviceIoGate,
) -> Result<CaptureSessionOutcome, CaptureSessionFailure> {
let shared = registry
.lookup(&route)
.ok_or(GestureError::DeviceNotFound)?;
run_keyboard_capture_session_on(
shared,
wanted,
sink,
shutdown,
channel_slot,
Some(registry),
device_io,
)
.await
}
async fn run_keyboard_capture_session_on(
shared: SharedChannel,
wanted: BTreeMap<u16, ButtonId>,
sink: mpsc::UnboundedSender<CapturedInput>,
shutdown: oneshot::Receiver<()>,
channel_slot: CaptureChannel,
registry: Option<&ChannelRegistry>,
device_io: DeviceIoGate,
) -> Result<CaptureSessionOutcome, CaptureSessionFailure> {
if !device_io.allows_io() {
return Err(device_io_suspended().into());
}
let chan = Arc::clone(shared.channel());
let device_index = shared.device_index();
let device = Device::new(Arc::clone(&chan), device_index)
.await
.map_err(|_| GestureError::DeviceUnreachable(device_index))?;
let info = device
.root()
.get_feature(reprog_controls::FEATURE_ID)
.await
.map_err(|e| GestureError::Hidpp(format!("{e:?}")))?
.ok_or_else(|| GestureError::Hidpp("keyboard exposes no 0x1b04 reprog controls".into()))?;
let rc = ReprogControlsV4::new(Arc::clone(&chan), 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_capture_start(error, pending, &shared, registry).await);
}
let held: Arc<Mutex<BTreeSet<u16>>> = Arc::new(Mutex::new(BTreeSet::new()));
let feature_index = armed.controls.feature_index();
let listener = chan.add_msg_listener_guarded({
let held = Arc::clone(&held);
let diverted = armed.diverted.clone();
let sink = sink.clone();
move |raw, matched| {
if matched {
return;
}
let msg = v20::Message::from(raw);
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);
}
});
let wireless = device
.root()
.get_feature(WirelessDeviceStatusFeature::ID)
.await
.ok()
.flatten()
.map(|info| WirelessDeviceStatusFeature::new(Arc::clone(&chan), device_index, info.index));
if let Ok(mut slot) = channel_slot.write() {
*slot = Some(shared.clone());
}
info!(
index = device_index,
keys = armed.diverted.len(),
wake_rearm = wireless.is_some(),
"keyboard key capture active"
);
let stop = monitor_keyboard_capture(
KeyboardMonitor {
armed: &armed,
device_index,
registry,
shared: &shared,
},
wireless,
shutdown,
device_io,
)
.await;
if let Ok(mut slot) = channel_slot.write()
&& slot
.as_ref()
.is_some_and(|shared| Arc::ptr_eq(shared.channel(), &chan))
{
*slot = None;
}
let pending = armed.into_pending(&shared);
let outcome = drop_listener_after(
listener,
restore_after_stop(stop, pending, &shared, registry),
)
.await;
debug!(index = device_index, "keyboard key capture stopped");
Ok(outcome)
}
struct KeyboardMonitor<'a> {
armed: &'a ArmedKeys,
device_index: u8,
registry: Option<&'a ChannelRegistry>,
shared: &'a SharedChannel,
}
async fn monitor_keyboard_capture(
context: KeyboardMonitor<'_>,
wireless: Option<WirelessDeviceStatusFeature>,
shutdown: oneshot::Receiver<()>,
mut device_io: DeviceIoGate,
) -> CaptureStop {
let mut wake_events = wireless.as_ref().map(EmittingFeature::listen);
let mut shutdown = std::pin::pin!(shutdown);
loop {
if !device_io.allows_io() && !device_io.wait_until_allowed().await {
return stop_for_current_publication(context.registry, context.shared);
}
tokio::select! {
biased;
allowed = device_io.changed() => {
if allowed.is_none() {
return stop_for_current_publication(context.registry, context.shared);
}
}
_ = &mut shutdown => {
return stop_for_current_publication(context.registry, context.shared);
}
transition = wait_for_channel_change(context.registry, context.shared) => {
info!(index = context.device_index, "inventory replaced or removed keyboard capture channel — restarting session");
return transition;
}
event = async {
match wake_events.as_ref() {
Some(events) => events.recv().await.ok(),
None => std::future::pending().await,
}
} => {
let Some(WirelessDeviceStatusEvent::StatusBroadcast(broadcast)) = event else {
wake_events = None;
continue;
};
info!(?broadcast, "keyboard reconnected — re-arming key diversion");
rearm_keys(context.armed, &device_io).await;
}
}
}
}
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 ArmedKeys {
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<(), GestureError> {
for (&cid, &button) in wanted {
if controls.iter().any(|c| c.cid == cid && c.is_divertable()) {
let original = armed
.controls
.get_cid_reporting(cid)
.await
.map_err(|error| GestureError::Hidpp(format!("{error:?}")))?;
armed.reporting.push(ArmedReporting { cid, original });
armed
.controls
.set_cid_reporting_full(cid, divert_change(original, false))
.await
.map_err(|e| GestureError::Hidpp(format!("{e:?}")))?;
armed.diverted.insert(cid, button);
} else {
debug!(
cid = format_args!("{cid:#06x}"),
"bound key not divertable on this keyboard — left native"
);
}
}
Ok(())
}
async fn rearm_keys(armed: &ArmedKeys, device_io: &DeviceIoGate) {
tokio::time::sleep(std::time::Duration::from_millis(200)).await;
if !device_io.allows_io() {
return;
}
for &reporting in &armed.reporting {
if let Err(e) = armed
.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 device_io_suspended() -> GestureError {
GestureError::Hid(BackendError::Backend("host device I/O is suspended".into()))
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn keyboard_snapshots_emit_balanced_edges_without_duplicates() {
let diverted = BTreeMap::from([
(0x00d4, ButtonId::KeySearch),
(0x0103, ButtonId::KeyDictation),
]);
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::KeySearch),
CapturedInput::ButtonDown(ButtonId::KeyDictation),
CapturedInput::ButtonUp(ButtonId::KeySearch),
CapturedInput::ButtonUp(ButtonId::KeyDictation),
]
);
}
}