use std::fmt;
use std::future::Future;
use std::sync::{Arc, RwLock};
use hidpp::protocol::v20::Hidpp20Error;
use thiserror::Error;
use super::restore::{PendingRestore, RestoreOutcome, RestorePlan, SessionFailure};
use crate::backend::BackendError;
use crate::reprog_controls::{self, ReprogControlsV4};
use crate::thumbwheel::{self, Thumbwheel, ThumbwheelStatus, WheelDirection};
use crate::{ChannelRegistry, DeviceRoute, IoSuspended, SharedChannel};
#[derive(Debug, Clone, PartialEq, Eq)]
pub enum CaptureSessionStop {
Shutdown,
Handoff(DeviceRoute),
}
pub type CaptureChannelSlot = Arc<RwLock<Option<SharedChannel>>>;
#[derive(Debug, Error)]
pub enum CaptureError {
#[error("HID transport error")]
Hid(#[from] BackendError),
#[error("no connected device matched the capture route")]
DeviceNotFound,
#[error("device at index {0:#04x} did not respond to HID++")]
DeviceUnreachable(u8),
#[error("HID++ protocol error: {0}")]
Hidpp(String),
}
impl From<Hidpp20Error> for CaptureError {
fn from(error: Hidpp20Error) -> Self {
Self::Hidpp(format!("{error:?}"))
}
}
impl From<IoSuspended> for CaptureError {
fn from(error: IoSuspended) -> Self {
Self::Hid(error.into())
}
}
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
pub(crate) struct ArmedReporting {
pub(crate) cid: u16,
pub(crate) original: reprog_controls::CidReporting,
policy: ReprogRestorePolicy,
}
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
enum ReprogRestorePolicy {
Pointer,
Keyboard,
}
impl ArmedReporting {
pub(crate) fn pointer(cid: u16, original: reprog_controls::CidReporting) -> Self {
Self {
cid,
original,
policy: ReprogRestorePolicy::Pointer,
}
}
pub(crate) fn keyboard(cid: u16, original: reprog_controls::CidReporting) -> Self {
Self {
cid,
original,
policy: ReprogRestorePolicy::Keyboard,
}
}
pub(crate) fn undivert_change(self) -> reprog_controls::CidReportingChange {
match self.policy {
ReprogRestorePolicy::Pointer => undivert_change(self.original),
ReprogRestorePolicy::Keyboard => undivert_keyboard_key(self.original),
}
}
}
pub(crate) struct ReprogRestore {
feature_index: u8,
controls: Vec<ArmedReporting>,
}
impl ReprogRestore {
pub(crate) fn new(feature_index: u8, controls: Vec<ArmedReporting>) -> Option<Self> {
(!controls.is_empty()).then_some(Self {
feature_index,
controls,
})
}
}
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
pub(crate) struct ThumbwheelRestore {
feature_index: u8,
native_direction: WheelDirection,
}
impl ThumbwheelRestore {
pub(crate) fn new(feature_index: u8, original: Option<ThumbwheelStatus>) -> Self {
Self {
feature_index,
native_direction: thumbwheel::native_direction_to_restore(original),
}
}
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub(crate) enum CaptureStop {
Shutdown,
ChannelChanged,
Handoff(DeviceRoute),
}
pub struct CaptureRestorePlan {
reprog: Option<ReprogRestore>,
thumb: Option<ThumbwheelRestore>,
}
impl fmt::Debug for CaptureRestorePlan {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
f.debug_struct("CaptureRestorePlan")
.field(
"reporting_count",
&self
.reprog
.as_ref()
.map_or(0, |reprog| reprog.controls.len()),
)
.field("has_thumbwheel", &self.thumb.is_some())
.finish()
}
}
impl RestorePlan for CaptureRestorePlan {
async fn restore_on(&self, current: &SharedChannel) -> bool {
let channel = Arc::clone(current.channel());
let device_index = current.device_index();
let mut restored = true;
if let Some(reprog) = &self.reprog {
let controls =
ReprogControlsV4::new(channel.clone(), device_index, reprog.feature_index);
for &reporting in &reprog.controls {
restored &= restore_reporting(&controls, reporting, "captured control").await;
}
}
if let Some(thumb) = self.thumb {
let thumbwheel = Thumbwheel::new(channel, device_index, thumb.feature_index);
restored &= restore_result(
thumbwheel.undivert(thumb.native_direction).await,
"thumb wheel",
);
}
restored
}
}
pub type PendingCaptureRestore = PendingRestore<CaptureRestorePlan>;
pub type CaptureSessionOutcome = RestoreOutcome<CaptureRestorePlan>;
pub type CaptureSessionFailure = SessionFailure<CaptureError, CaptureRestorePlan>;
impl PendingCaptureRestore {
#[must_use]
pub fn detached(route: DeviceRoute) -> Self {
Self::without_channel(
route,
CaptureRestorePlan {
reprog: None,
thumb: None,
},
)
}
pub(crate) fn new(
retired: &SharedChannel,
reprog: Option<ReprogRestore>,
thumb: Option<ThumbwheelRestore>,
) -> Option<Self> {
if reprog.is_none() && thumb.is_none() {
return None;
}
Some(Self::owing(retired, CaptureRestorePlan { reprog, thumb }))
}
}
pub(crate) async fn restore_after_stop(
stop: CaptureStop,
pending: Option<PendingCaptureRestore>,
registry: &ChannelRegistry,
) -> CaptureSessionOutcome {
let Some(pending) = pending else {
return CaptureSessionOutcome::Restored;
};
match stop {
CaptureStop::Shutdown => pending.allow_current_channel().retry(registry).await,
CaptureStop::ChannelChanged => pending.retry(registry).await,
CaptureStop::Handoff(route) => pending.retry_via(route, registry).await,
}
}
pub(crate) fn stop_for_current_publication(
registry: &ChannelRegistry,
retired: &SharedChannel,
) -> CaptureStop {
if registry.is_current(retired) {
CaptureStop::Shutdown
} else {
CaptureStop::ChannelChanged
}
}
pub(crate) async fn wait_for_channel_change(
registry: &ChannelRegistry,
retired: &SharedChannel,
) -> CaptureStop {
let mut changes = registry.subscribe();
loop {
if !registry.is_current(retired) {
return CaptureStop::ChannelChanged;
}
if changes.changed().await.is_err() {
return std::future::pending().await;
}
}
}
pub(crate) async fn drop_listener_after<T, R>(listener: T, teardown: impl Future<Output = R>) -> R {
let result = teardown.await;
drop(listener);
result
}
pub(crate) fn divert_change(
reporting: reprog_controls::CidReporting,
raw_xy: bool,
) -> reprog_controls::CidReportingChange {
reprog_controls::CidReportingChange {
diverted: Some(true),
raw_xy: Some(raw_xy),
remap: reporting.remap,
..Default::default()
}
}
pub(crate) fn divert_keyboard_key(
reporting: reprog_controls::CidReporting,
) -> reprog_controls::CidReportingChange {
reprog_controls::CidReportingChange {
diverted: Some(true),
raw_xy: None,
remap: reporting.remap,
..Default::default()
}
}
pub(crate) async fn restore_reporting(
controls: &ReprogControlsV4,
reporting: ArmedReporting,
what: &str,
) -> bool {
let result = controls
.set_cid_reporting_full(reporting.cid, reporting.undivert_change())
.await
.map(|_| ());
restore_result(result, what)
}
pub(crate) fn restore_result<E: fmt::Display>(result: Result<(), E>, what: &str) -> bool {
if let Err(error) = result {
tracing::warn!(%error, control = what, "failed to restore control mapping");
false
} else {
true
}
}
pub(crate) fn undivert_change(
reporting: reprog_controls::CidReporting,
) -> reprog_controls::CidReportingChange {
reprog_controls::CidReportingChange {
diverted: Some(false),
raw_xy: Some(false),
remap: reporting.remap,
..Default::default()
}
}
pub(crate) fn undivert_keyboard_key(
reporting: reprog_controls::CidReporting,
) -> reprog_controls::CidReportingChange {
reprog_controls::CidReportingChange {
diverted: Some(false),
raw_xy: None,
remap: reporting.remap,
..Default::default()
}
}