Skip to main content

anybox/
capability.rs

1use std::borrow::Cow;
2use std::collections::HashMap;
3use std::fmt;
4use std::time::Duration;
5
6use crate::click::ClickTiming;
7
8/// the box families anybox knows how to talk to
9#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash)]
10#[non_exhaustive]
11pub enum Family {
12    Medius,
13    MakcuV4,
14    Ferrum,
15    Ysc,
16}
17
18impl fmt::Display for Family {
19    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
20        f.write_str(match self {
21            Self::Medius => "Medius",
22            Self::MakcuV4 => "MAKCU V4",
23            Self::Ferrum => "Ferrum",
24            Self::Ysc => "YSC 8K",
25        })
26    }
27}
28
29#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash)]
30#[non_exhaustive]
31pub enum Button {
32    Left,
33    Right,
34    Middle,
35    Side1,
36    Side2,
37    /// a button past side2, numbered from 5. Medius has these when the mouse behind it does, MAKCU only reports
38    /// them as input
39    Extra(u8),
40}
41
42impl fmt::Display for Button {
43    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
44        match self {
45            Self::Left => f.write_str("left"),
46            Self::Right => f.write_str("right"),
47            Self::Middle => f.write_str("middle"),
48            Self::Side1 => f.write_str("side1"),
49            Self::Side2 => f.write_str("side2"),
50            Self::Extra(n) => write!(f, "extra {n}"),
51        }
52    }
53}
54
55/// a physical axis you can lock or scale
56#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash)]
57#[non_exhaustive]
58pub enum Axis {
59    X,
60    Y,
61    Wheel,
62    /// horizontal scroll
63    Pan,
64}
65
66impl fmt::Display for Axis {
67    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
68        f.write_str(match self {
69            Self::X => "X",
70            Self::Y => "Y",
71            Self::Wheel => "wheel",
72            Self::Pan => "pan",
73        })
74    }
75}
76
77/// which way along an axis. positive is the way the box moves for a positive value, so right for X, down for
78/// Y, up for the wheel and right for pan
79#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash)]
80#[non_exhaustive]
81pub enum Direction {
82    Both,
83    Positive,
84    Negative,
85}
86
87/// every action a box gets asked whether it can do, and the one an error is about
88#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash)]
89#[non_exhaustive]
90pub enum Action {
91    Connect,
92    Move,
93    MoveNow,
94    Press,
95    Release,
96    Click,
97    Wheel,
98    Pan,
99    DiscardPendingMoves,
100    ButtonEvents,
101    MotionEvents,
102    KeyEvents,
103    MediaEvents,
104    KeyPress,
105    KeyRelease,
106    KeyTap,
107    MediaPress,
108    MediaRelease,
109    MediaTap,
110    LockButton,
111    LockAxis,
112    LockKey,
113    ScaleAxis,
114}
115
116impl fmt::Display for Action {
117    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
118        f.write_str(match self {
119            Self::Connect => "connect",
120            Self::Move => "move",
121            Self::MoveNow => "move now",
122            Self::Press => "press",
123            Self::Release => "release",
124            Self::Click => "click",
125            Self::Wheel => "scroll",
126            Self::Pan => "scroll sideways",
127            Self::DiscardPendingMoves => "discard pending moves",
128            Self::ButtonEvents => "report button events",
129            Self::MotionEvents => "report motion events",
130            Self::KeyEvents => "report key events",
131            Self::MediaEvents => "report media key events",
132            Self::LockButton => "lock or unlock a button",
133            Self::LockAxis => "lock or unlock an axis",
134            Self::LockKey => "lock or unlock a key",
135            Self::ScaleAxis => "scale an axis",
136            Self::KeyPress => "press a key",
137            Self::KeyRelease => "release a key",
138            Self::KeyTap => "tap a key",
139            Self::MediaPress => "press a media key",
140            Self::MediaRelease => "release a media key",
141            Self::MediaTap => "tap a media key",
142        })
143    }
144}
145
146/// how a box does an action
147#[derive(Clone, Debug, PartialEq, Eq)]
148#[non_exhaustive]
149pub enum Execution {
150    /// the box does it, with its own timing
151    Native,
152    /// anybox builds it on the PC out of things the box can do, so it runs on the PC's timing
153    Host,
154    Unsupported {
155        reason: Cow<'static, str>,
156    },
157}
158
159/// a way this box behaves differently that anybox can't hide, so it tells you instead of smoothing it over
160#[derive(Clone, Debug, PartialEq, Eq)]
161#[non_exhaustive]
162pub enum Quirk {
163    /// after a release the box keeps the button up for a random while, and only then does the physical button
164    /// count again. that's the box's default, a setting on the box that anybox can't read may turn it off
165    ForcedUpOnRelease { min: Duration, max: Duration },
166    /// pressing and letting go of the physical button or key ends a press the box holds for you, the physical one
167    /// wins
168    PhysicalOverride,
169    /// the box only locks a whole axis, so locking or unlocking one way of it alone gets refused
170    WholeAxisOnly,
171    /// the most wheel notches the box takes in one command
172    NotchesPerCommand(u16),
173    /// the box's click has its own timing and can't take a hold, so a click with one is timed from the PC with a
174    /// press and a release
175    FixedClickTiming,
176    /// the longest hold the box's click takes
177    MaxHold(Duration),
178    /// how long the box holds a click you don't give a hold to
179    BoxClickTiming { min: Duration, max: Duration },
180    /// the box can't time a click or tap itself, so this is what anybox does on the PC instead
181    HostClickTiming(ClickTiming),
182    /// the box presses held buttons and keys again by itself after a reconnect or a session reset, so they
183    /// stay held through it instead of being let go
184    KeepsHeld,
185    /// the most keys the box holds down at once, not counting modifiers. it can't report any past that
186    MaxKeysHeld(u8),
187    /// the box puts locks back by itself after a reconnect or a session reset
188    KeepsLocked,
189}
190
191#[derive(Clone, Debug, PartialEq, Eq)]
192#[non_exhaustive]
193pub struct Capability {
194    pub execution: Execution,
195    pub quirks: Vec<Quirk>,
196}
197
198impl Capability {
199    pub(crate) fn native() -> Self {
200        Self {
201            execution: Execution::Native,
202            quirks: Vec::new(),
203        }
204    }
205
206    pub(crate) fn host() -> Self {
207        Self {
208            execution: Execution::Host,
209            quirks: Vec::new(),
210        }
211    }
212
213    pub(crate) fn unsupported(reason: impl Into<Cow<'static, str>>) -> Self {
214        Self {
215            execution: Execution::Unsupported {
216                reason: reason.into(),
217            },
218            quirks: Vec::new(),
219        }
220    }
221
222    pub(crate) fn with(mut self, quirk: Quirk) -> Self {
223        self.quirks.push(quirk);
224        self
225    }
226
227    pub fn is_supported(&self) -> bool {
228        !matches!(self.execution, Execution::Unsupported { .. })
229    }
230
231    /// why it's unsupported, for an error that comes up after the capability check already passed
232    pub(crate) fn reason(&self) -> Cow<'static, str> {
233        match &self.execution {
234            Execution::Unsupported { reason } => reason.clone(),
235            _ => "the box can't do it".into(),
236        }
237    }
238}
239
240/// what this box, on this firmware, with this mouse behind it, can do
241#[derive(Clone, Debug, PartialEq, Eq)]
242pub struct Capabilities {
243    actions: HashMap<Action, Capability>,
244    buttons: Vec<Button>,
245    axes: Vec<Axis>,
246    missing: Capability,
247}
248
249impl Capabilities {
250    pub(crate) fn new(buttons: Vec<Button>) -> Self {
251        Self {
252            actions: HashMap::new(),
253            buttons,
254            axes: Vec::new(),
255            missing: Capability::unsupported("this box doesn't have it"),
256        }
257    }
258
259    pub(crate) fn set(mut self, action: Action, capability: Capability) -> Self {
260        self.actions.insert(action, capability);
261        self
262    }
263
264    /// anything the box didn't list counts as unsupported
265    pub fn get(&self, action: Action) -> &Capability {
266        self.actions.get(&action).unwrap_or(&self.missing)
267    }
268
269    pub fn buttons(&self) -> &[Button] {
270        &self.buttons
271    }
272
273    /// the axes you can lock or scale on this box
274    pub fn axes(&self) -> &[Axis] {
275        &self.axes
276    }
277
278    pub(crate) fn with_axes(mut self, axes: Vec<Axis>) -> Self {
279        self.axes = axes;
280        self
281    }
282
283    pub fn iter(&self) -> impl Iterator<Item = (Action, &Capability)> {
284        self.actions
285            .iter()
286            .map(|(action, capability)| (*action, capability))
287    }
288
289    /// a box that can press and release but has no click or tap of its own gets one built on the PC
290    pub(crate) fn with_host_taps(mut self, timing: ClickTiming) -> Self {
291        for (tap, press, release) in [
292            (Action::Click, Action::Press, Action::Release),
293            (Action::KeyTap, Action::KeyPress, Action::KeyRelease),
294            (Action::MediaTap, Action::MediaPress, Action::MediaRelease),
295        ] {
296            let native = self.get(tap).is_supported();
297            let parts = self.get(press).is_supported() && self.get(release).is_supported();
298            if !native && parts {
299                self.actions
300                    .insert(tap, Capability::host().with(Quirk::HostClickTiming(timing)));
301            }
302        }
303        self
304    }
305}
306
307#[derive(Clone, Debug, PartialEq, Eq)]
308#[non_exhaustive]
309pub enum Firmware {
310    Version {
311        major: u16,
312        minor: u16,
313        patch: u16,
314    },
315    Build(u32),
316    /// the protocol number a box speaks, which only shows up in the error of a Medius that's too old or too new.
317    /// its `Info` has the version
318    Protocol(u8),
319    Text(String),
320}
321
322impl fmt::Display for Firmware {
323    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
324        match self {
325            Self::Version {
326                major,
327                minor,
328                patch,
329            } => write!(f, "{major}.{minor}.{patch}"),
330            Self::Build(build) => write!(f, "build {build}"),
331            Self::Protocol(protocol) => write!(f, "protocol {protocol}"),
332            Self::Text(text) => f.write_str(text),
333        }
334    }
335}
336
337#[derive(Clone, Debug, PartialEq, Eq)]
338#[non_exhaustive]
339pub enum StableId {
340    Mac([u8; 6]),
341    UsbSerial(String),
342}
343
344impl fmt::Display for StableId {
345    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
346        match self {
347            Self::Mac(mac) => {
348                let hex: Vec<String> = mac.iter().map(|byte| format!("{byte:02x}")).collect();
349                write!(f, "MAC {}", hex.join(":"))
350            }
351            Self::UsbSerial(serial) => write!(f, "USB serial {serial}"),
352        }
353    }
354}
355
356#[derive(Clone, Debug, PartialEq, Eq)]
357#[non_exhaustive]
358pub struct Info {
359    /// the port the device was opened on, so you can show it or open the same box again with `Connect::port`
360    pub port: String,
361    pub family: Family,
362    pub model: Option<String>,
363    pub firmware: Option<Firmware>,
364    pub id: Option<StableId>,
365    /// which of Ferrum's two APIs this is, none on other boxes
366    pub ferrum_api: Option<FerrumApi>,
367    /// the USB ids of the device plugged into the box, which is the identity the box shows the PC, so you can
368    /// tell which device on the PC goes through the box. a Ferrum on its Software API and a Medius report it, a
369    /// MAKCU can't and a YSC isn't read yet. it's read when you connect and again when what's plugged in changes,
370    /// so swapping one mouse for another within a second can leave the old ids here until the next reconnect
371    pub peripheral: Option<UsbIds>,
372}
373
374impl Info {
375    pub(crate) fn new(family: Family) -> Self {
376        Self {
377            port: String::new(),
378            family,
379            model: None,
380            firmware: None,
381            id: None,
382            ferrum_api: None,
383            peripheral: None,
384        }
385    }
386}
387
388/// the two ways into a Ferrum
389#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash)]
390#[non_exhaustive]
391pub enum FerrumApi {
392    /// the virtual port the Ferrum app makes, Windows only. everything anybox does on a Ferrum works here
393    Software,
394    /// the box's own port, the only way in from Linux. it only moves, clicks, scrolls, locks buttons and whole
395    /// axes, and reports buttons, and a KMBox B+ answers on it exactly like a Ferrum does
396    Legacy,
397}
398
399impl fmt::Display for FerrumApi {
400    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
401        f.write_str(match self {
402            Self::Software => "the Ferrum app's port",
403            Self::Legacy => "the box's own legacy port",
404        })
405    }
406}
407
408/// a USB vendor and product id
409#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash)]
410#[non_exhaustive]
411pub struct UsbIds {
412    pub vid: u16,
413    pub pid: u16,
414}
415
416impl UsbIds {
417    pub fn new(vid: u16, pid: u16) -> Self {
418        Self { vid, pid }
419    }
420}
421
422/// who the box is and what it can do, both always read from the same connection
423#[derive(Clone, Debug, PartialEq, Eq)]
424#[non_exhaustive]
425pub struct Snapshot {
426    pub info: Info,
427    pub capabilities: Capabilities,
428}
429
430impl Snapshot {
431    pub(crate) fn new(info: Info, capabilities: Capabilities) -> Self {
432        Self { info, capabilities }
433    }
434}