1use std::fmt;
7use std::future::Future;
8use std::sync::{Arc, RwLock};
9
10use hidpp::protocol::v20::Hidpp20Error;
11use thiserror::Error;
12
13use super::restore::{PendingRestore, RestoreOutcome, RestorePlan, SessionFailure};
14use crate::backend::BackendError;
15use crate::reprog_controls::{self, ReprogControlsV4};
16use crate::thumbwheel::{self, Thumbwheel, ThumbwheelStatus, WheelDirection};
17use crate::{ChannelRegistry, DeviceRoute, IoSuspended, SharedChannel};
18
19#[derive(Debug, Clone, PartialEq, Eq)]
21pub enum CaptureSessionStop {
22 Shutdown,
24 Handoff(DeviceRoute),
27}
28
29pub type CaptureChannelSlot = Arc<RwLock<Option<SharedChannel>>>;
32
33#[derive(Debug, Error)]
35pub enum CaptureError {
36 #[error("HID transport error")]
38 Hid(#[from] BackendError),
39 #[error("no connected device matched the capture route")]
41 DeviceNotFound,
42 #[error("device at index {0:#04x} did not respond to HID++")]
44 DeviceUnreachable(u8),
45 #[error("HID++ protocol error: {0}")]
47 Hidpp(String),
48}
49
50impl From<Hidpp20Error> for CaptureError {
51 fn from(error: Hidpp20Error) -> Self {
52 Self::Hidpp(format!("{error:?}"))
53 }
54}
55
56impl From<IoSuspended> for CaptureError {
57 fn from(error: IoSuspended) -> Self {
58 Self::Hid(error.into())
59 }
60}
61
62#[derive(Clone, Copy, Debug, PartialEq, Eq)]
65pub(crate) struct ArmedReporting {
66 pub(crate) cid: u16,
67 pub(crate) original: reprog_controls::CidReporting,
68 policy: ReprogRestorePolicy,
69}
70
71#[derive(Clone, Copy, Debug, PartialEq, Eq)]
72enum ReprogRestorePolicy {
73 Pointer,
75 Keyboard,
77}
78
79impl ArmedReporting {
80 pub(crate) fn pointer(cid: u16, original: reprog_controls::CidReporting) -> Self {
83 Self {
84 cid,
85 original,
86 policy: ReprogRestorePolicy::Pointer,
87 }
88 }
89
90 pub(crate) fn keyboard(cid: u16, original: reprog_controls::CidReporting) -> Self {
93 Self {
94 cid,
95 original,
96 policy: ReprogRestorePolicy::Keyboard,
97 }
98 }
99
100 pub(crate) fn undivert_change(self) -> reprog_controls::CidReportingChange {
102 match self.policy {
103 ReprogRestorePolicy::Pointer => undivert_change(self.original),
104 ReprogRestorePolicy::Keyboard => undivert_keyboard_key(self.original),
105 }
106 }
107}
108
109pub(crate) struct ReprogRestore {
111 feature_index: u8,
112 controls: Vec<ArmedReporting>,
113}
114
115impl ReprogRestore {
116 pub(crate) fn new(feature_index: u8, controls: Vec<ArmedReporting>) -> Option<Self> {
117 (!controls.is_empty()).then_some(Self {
118 feature_index,
119 controls,
120 })
121 }
122}
123
124#[derive(Clone, Copy, Debug, PartialEq, Eq)]
127pub(crate) struct ThumbwheelRestore {
128 feature_index: u8,
129 native_direction: WheelDirection,
130}
131
132impl ThumbwheelRestore {
133 pub(crate) fn new(feature_index: u8, original: Option<ThumbwheelStatus>) -> Self {
137 Self {
138 feature_index,
139 native_direction: thumbwheel::native_direction_to_restore(original),
140 }
141 }
142}
143
144#[derive(Debug, Clone, PartialEq, Eq)]
145pub(crate) enum CaptureStop {
146 Shutdown,
148 ChannelChanged,
150 Handoff(DeviceRoute),
153}
154
155pub struct CaptureRestorePlan {
160 reprog: Option<ReprogRestore>,
161 thumb: Option<ThumbwheelRestore>,
162}
163
164impl fmt::Debug for CaptureRestorePlan {
165 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
166 f.debug_struct("CaptureRestorePlan")
167 .field(
168 "reporting_count",
169 &self
170 .reprog
171 .as_ref()
172 .map_or(0, |reprog| reprog.controls.len()),
173 )
174 .field("has_thumbwheel", &self.thumb.is_some())
175 .finish()
176 }
177}
178
179impl RestorePlan for CaptureRestorePlan {
180 async fn restore_on(&self, current: &SharedChannel) -> bool {
181 let channel = Arc::clone(current.channel());
182 let device_index = current.device_index();
183 let mut restored = true;
184 if let Some(reprog) = &self.reprog {
185 let controls =
186 ReprogControlsV4::new(channel.clone(), device_index, reprog.feature_index);
187 for &reporting in &reprog.controls {
188 restored &= restore_reporting(&controls, reporting, "captured control").await;
189 }
190 }
191 if let Some(thumb) = self.thumb {
192 let thumbwheel = Thumbwheel::new(channel, device_index, thumb.feature_index);
193 restored &= restore_result(
194 thumbwheel.undivert(thumb.native_direction).await,
195 "thumb wheel",
196 );
197 }
198 restored
199 }
200}
201
202pub type PendingCaptureRestore = PendingRestore<CaptureRestorePlan>;
205
206pub type CaptureSessionOutcome = RestoreOutcome<CaptureRestorePlan>;
208
209pub type CaptureSessionFailure = SessionFailure<CaptureError, CaptureRestorePlan>;
212
213impl PendingCaptureRestore {
214 #[must_use]
216 pub fn detached(route: DeviceRoute) -> Self {
217 Self::without_channel(
218 route,
219 CaptureRestorePlan {
220 reprog: None,
221 thumb: None,
222 },
223 )
224 }
225
226 pub(crate) fn new(
229 retired: &SharedChannel,
230 reprog: Option<ReprogRestore>,
231 thumb: Option<ThumbwheelRestore>,
232 ) -> Option<Self> {
233 if reprog.is_none() && thumb.is_none() {
234 return None;
235 }
236 Some(Self::owing(retired, CaptureRestorePlan { reprog, thumb }))
237 }
238}
239
240pub(crate) async fn restore_after_stop(
242 stop: CaptureStop,
243 pending: Option<PendingCaptureRestore>,
244 registry: &ChannelRegistry,
245) -> CaptureSessionOutcome {
246 let Some(pending) = pending else {
247 return CaptureSessionOutcome::Restored;
248 };
249 match stop {
250 CaptureStop::Shutdown => pending.allow_current_channel().retry(registry).await,
251 CaptureStop::ChannelChanged => pending.retry(registry).await,
252 CaptureStop::Handoff(route) => pending.retry_via(route, registry).await,
253 }
254}
255
256pub(crate) fn stop_for_current_publication(
259 registry: &ChannelRegistry,
260 retired: &SharedChannel,
261) -> CaptureStop {
262 if registry.is_current(retired) {
263 CaptureStop::Shutdown
264 } else {
265 CaptureStop::ChannelChanged
266 }
267}
268
269pub(crate) async fn wait_for_channel_change(
273 registry: &ChannelRegistry,
274 retired: &SharedChannel,
275) -> CaptureStop {
276 let mut changes = registry.subscribe();
277 loop {
278 if !registry.is_current(retired) {
279 return CaptureStop::ChannelChanged;
280 }
281 if changes.changed().await.is_err() {
282 return std::future::pending().await;
286 }
287 }
288}
289
290pub(crate) async fn drop_listener_after<T, R>(listener: T, teardown: impl Future<Output = R>) -> R {
292 let result = teardown.await;
293 drop(listener);
294 result
295}
296
297pub(crate) fn divert_change(
299 reporting: reprog_controls::CidReporting,
300 raw_xy: bool,
301) -> reprog_controls::CidReportingChange {
302 reprog_controls::CidReportingChange {
303 diverted: Some(true),
304 raw_xy: Some(raw_xy),
305 remap: reporting.remap,
306 ..Default::default()
307 }
308}
309
310pub(crate) fn divert_keyboard_key(
312 reporting: reprog_controls::CidReporting,
313) -> reprog_controls::CidReportingChange {
314 reprog_controls::CidReportingChange {
315 diverted: Some(true),
316 raw_xy: None,
317 remap: reporting.remap,
318 ..Default::default()
319 }
320}
321
322pub(crate) async fn restore_reporting(
325 controls: &ReprogControlsV4,
326 reporting: ArmedReporting,
327 what: &str,
328) -> bool {
329 let result = controls
330 .set_cid_reporting_full(reporting.cid, reporting.undivert_change())
331 .await
332 .map(|_| ());
333 restore_result(result, what)
334}
335
336pub(crate) fn restore_result<E: fmt::Display>(result: Result<(), E>, what: &str) -> bool {
337 if let Err(error) = result {
338 tracing::warn!(%error, control = what, "failed to restore control mapping");
339 false
340 } else {
341 true
342 }
343}
344
345pub(crate) fn undivert_change(
347 reporting: reprog_controls::CidReporting,
348) -> reprog_controls::CidReportingChange {
349 reprog_controls::CidReportingChange {
350 diverted: Some(false),
351 raw_xy: Some(false),
352 remap: reporting.remap,
353 ..Default::default()
354 }
355}
356
357pub(crate) fn undivert_keyboard_key(
359 reporting: reprog_controls::CidReporting,
360) -> reprog_controls::CidReportingChange {
361 reprog_controls::CidReportingChange {
362 diverted: Some(false),
363 raw_xy: None,
364 remap: reporting.remap,
365 ..Default::default()
366 }
367}