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 crate::channel_registry::ChannelRegistry;
use crate::gesture::{CaptureChannel, CapturedInput, GestureError, enumerate_controls, restore};
use crate::reprog_controls::{self, RawControlEvent, ReprogControlsV4};
use crate::route::{DeviceRoute, open_route_channel};
use crate::write::SharedChannel;
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(
route: DeviceRoute,
wanted: BTreeMap<u16, ButtonId>,
sink: mpsc::UnboundedSender<CapturedInput>,
shutdown: oneshot::Receiver<()>,
channel_slot: CaptureChannel,
) -> Result<(), GestureError> {
let chan = open_route_channel(&route)
.await?
.ok_or(GestureError::DeviceNotFound)?;
let shared = SharedChannel::new(chan, route.clone());
run_keyboard_capture_session_on(route, shared, wanted, sink, shutdown, channel_slot).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,
) -> Result<(), GestureError> {
let shared = registry
.lookup(&route)
.ok_or(GestureError::DeviceNotFound)?;
run_keyboard_capture_session_on(route, shared, wanted, sink, shutdown, channel_slot).await
}
async fn run_keyboard_capture_session_on(
route: DeviceRoute,
shared: SharedChannel,
wanted: BTreeMap<u16, ButtonId>,
sink: mpsc::UnboundedSender<CapturedInput>,
shutdown: oneshot::Receiver<()>,
channel_slot: CaptureChannel,
) -> Result<(), GestureError> {
let chan = Arc::clone(shared.channel());
let device_index = route.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 diverted = arm_keys(&rc, &controls, &wanted).await?;
let held: Arc<Mutex<BTreeSet<u16>>> = Arc::new(Mutex::new(BTreeSet::new()));
let feature_index = info.index;
let listener = chan.add_msg_listener_guarded({
let held = Arc::clone(&held);
let diverted = 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);
for (&cid, &button) in &diverted {
let now = cids.contains(&cid);
let was = down.contains(&cid);
if now && !was {
let _ = sink.send(CapturedInput::ButtonPressed(button, None));
}
if now {
down.insert(cid);
} else {
down.remove(&cid);
}
}
}
});
let wireless = device
.root()
.get_feature(WirelessDeviceStatusFeature::ID)
.await
.ok()
.flatten()
.map(|info| WirelessDeviceStatusFeature::new(Arc::clone(&chan), device_index, info.index));
let wake_events = wireless.as_ref().map(EmittingFeature::listen);
if let Ok(mut slot) = channel_slot.write() {
*slot = Some(shared);
}
info!(
index = device_index,
keys = diverted.len(),
wake_rearm = wake_events.is_some(),
"keyboard key capture active"
);
let mut shutdown = shutdown;
match wake_events {
None => {
let _ = shutdown.await;
}
Some(wake_events) => loop {
tokio::select! {
_ = &mut shutdown => break,
event = wake_events.recv() => {
let Ok(WirelessDeviceStatusEvent::StatusBroadcast(broadcast)) = event else {
let _ = shutdown.await;
break;
};
info!(?broadcast, "keyboard reconnected — re-arming key diversion");
rearm_keys(&rc, &diverted).await;
}
}
},
}
drop(listener);
if let Ok(mut slot) = channel_slot.write() {
*slot = None;
}
for &cid in diverted.keys() {
restore(
rc.set_cid_reporting(cid, false, false).await,
"keyboard key",
);
}
debug!(index = device_index, "keyboard key capture stopped");
Ok(())
}
async fn arm_keys(
rc: &ReprogControlsV4,
controls: &[reprog_controls::CtrlIdInfo],
wanted: &BTreeMap<u16, ButtonId>,
) -> Result<BTreeMap<u16, ButtonId>, GestureError> {
let mut diverted = BTreeMap::new();
for (&cid, &button) in wanted {
if controls.iter().any(|c| c.cid == cid && c.is_divertable()) {
rc.set_cid_reporting(cid, true, false)
.await
.map_err(|e| GestureError::Hidpp(format!("{e:?}")))?;
diverted.insert(cid, button);
} else {
debug!(
cid = format_args!("{cid:#06x}"),
"bound key not divertable on this keyboard — left native"
);
}
}
Ok(diverted)
}
async fn rearm_keys(rc: &ReprogControlsV4, diverted: &BTreeMap<u16, ButtonId>) {
tokio::time::sleep(std::time::Duration::from_millis(200)).await;
for &cid in diverted.keys() {
if let Err(e) = rc.set_cid_reporting(cid, true, false).await {
warn!(
cid = format_args!("{cid:#06x}"),
error = ?e,
"re-divert after wake failed — key stays native until next wake"
);
}
}
}