//! Platform-specific desktop Push-to-Talk backends (SDD-081 trait, //! SDD-083..087 implementations). //! //! The cross-platform trait surface lives in this module's root; //! per-OS implementations live in the platform sub-modules and are //! conditionally compiled. `select()` is the runtime factory the //! audio engine calls during `start_audio`. //! //! Every backend has the same shape: //! * `start(gate, binding)` arms the backend. //! * `stop()` releases OS-level resources. //! * `rebind(binding)` updates the active binding without //! restarting the backend (used by the UI binding-capture //! flow). //! * `descriptor()` returns the privacy-safe descriptor for //! diagnostics + the UI capability badge. //! //! Backends never log raw key codes or scan codes — only the //! stable `bound_input_class` string ("keyboard", //! "mouse-side-button") plus the backend identifier. use core::fmt; use crate::ptt::{AudioTransmitGate, PttBackendDescriptor}; mod focused; #[cfg(target_os = "windows")] mod windows; #[cfg(target_os = "windows")] mod windows_keymap; #[cfg(target_os = "macos")] mod macos; #[cfg(target_os = "linux")] mod linux; pub use focused::FocusedPttBackend; /// User-bound PTT input. The struct deliberately carries only /// privacy-safe coarse-grained values so the diagnostics rule /// (DEC-027) holds at the type level. The OS-side key identity /// stays inside the platform backend implementation and is never /// exposed across this surface. #[derive(Debug, Clone, PartialEq, Eq)] pub struct PttBinding { /// Coarse-grained class. Stable values: `"keyboard"`, /// `"mouse-side-button"`. Future levels (DEC-026 mouse buttons /// on Linux portal) may add `"mouse-other"`. pub input_class: PttInputClass, /// Opaque platform-defined key value. The value is a string so /// diverse platform representations (Windows scan code, macOS /// key code, Linux portal trigger description) all fit. The /// diagnostics sanitizer's banned-field rule (SAD-077 / /// SDD-090) prevents this field from being logged because it /// never appears in a tracing record — the audio + bridge /// layers consult only `input_class` and the backend `descriptor()`. pub platform_key: String, } impl PttBinding { /// A "no binding" sentinel. Backends never produce a /// transmit-active event from this value. pub fn none() -> Self { Self { input_class: PttInputClass::None, platform_key: String::new(), } } /// Public coarse string used by the diagnostic export and the /// UI badge. pub fn class_str(&self) -> &'static str { self.input_class.as_str() } } /// Coarse input class. Stable strings shared by the diagnostics /// export, the UI capability badge, and the release verification /// record. #[derive(Debug, Clone, Copy, PartialEq, Eq)] pub enum PttInputClass { /// No binding is active. None, /// A keyboard key. Keyboard, /// A mouse side button (Mouse4 / Mouse5). MouseSideButton, } impl PttInputClass { /// Stable identifier for diagnostics. The value is never the /// raw key code or scan code — see DEC-027. pub fn as_str(self) -> &'static str { match self { Self::None => "", Self::Keyboard => "keyboard", Self::MouseSideButton => "mouse-side-button", } } } impl fmt::Display for PttInputClass { fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { f.write_str(self.as_str()) } } /// Errors raised by a desktop PTT backend. #[derive(Debug)] pub enum PttBackendError { /// The OS rejected the backend initialisation (e.g. Raw Input /// registration failed, event tap creation failed). Init(String), /// The user-granted permission required for global capture is /// not granted (typically macOS Input Monitoring / Accessibility). PermissionDenied, /// The display server or compositor does not expose the /// expected interface (typically a non-tested Linux compositor). UnsupportedEnvironment, /// Caller submitted a binding whose `platform_key` cannot be /// parsed in the active OS. InvalidBinding(String), } impl fmt::Display for PttBackendError { fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { match self { Self::Init(s) => write!(f, "init failed: {s}"), Self::PermissionDenied => f.write_str("permission denied"), Self::UnsupportedEnvironment => f.write_str("unsupported environment"), Self::InvalidBinding(s) => write!(f, "invalid binding: {s}"), } } } impl std::error::Error for PttBackendError {} /// Cross-platform desktop PTT backend (SDD-081). /// /// All implementations call exactly the audio transmit gate's /// `set(bool)` method to drive `transmit_active`; they never log /// raw key data. pub trait DesktopPttBackend: Send { /// Privacy-safe descriptor of this backend instance. fn descriptor(&self) -> PttBackendDescriptor; /// Subscribe to descriptor transitions. Implementations that /// cannot transition asynchronously (`FocusedPttBackend`, /// `WindowsRawInputBackend`, `WindowsHookBackend`, /// `MacOSEventTapBackend` today) return a receiver that /// observes only the initial value and never fires again. The /// Linux portal backend uses this channel to publish the /// post-`BindShortcuts` capability transition (Bound / /// Cancelled). fn descriptor_watch(&self) -> tokio::sync::watch::Receiver { let (_tx, rx) = tokio::sync::watch::channel(self.descriptor()); // Hold `_tx` alive by leaking it — receivers detect the // sender drop via `changed()` returning `Err`, which the // core supervisor treats as "no further updates". By // leaking we instead keep the receiver "live" indefinitely // (no spurious closed-channel events), and the channel is // dropped together with the audio engine because the // backend Drop releases the box. std::mem::forget(_tx); rx } /// Arm the backend. After this call the backend listens for /// the bound input and toggles the gate accordingly. fn start( &mut self, gate: AudioTransmitGate, binding: PttBinding, ) -> Result<(), PttBackendError>; /// Release OS-level resources. Idempotent. The backend /// instance may be dropped immediately after. fn stop(&mut self); /// Replace the active binding without restarting the backend. /// May fail with `PttBackendError::InvalidBinding` if the new /// binding cannot be honoured. fn rebind(&mut self, binding: PttBinding) -> Result<(), PttBackendError>; } /// Runtime factory (SAD-071 / `platform_input::select`). Returns /// the highest-capability backend the current OS, permission set, /// and display server permit, falling back through the ladder /// described in `desktop-ptt-architecture.md` to the universal /// `FocusedPttBackend`. /// /// The factory never fails — `FocusedPttBackend` is always /// constructible. pub fn select() -> Box { #[cfg(target_os = "windows")] { if let Some(b) = windows::try_select() { return b; } } #[cfg(target_os = "macos")] { if let Some(b) = macos::try_select() { return b; } } #[cfg(target_os = "linux")] { if let Some(b) = linux::try_select() { return b; } } Box::new(FocusedPttBackend::new()) }