diff --git a/Cargo.lock b/Cargo.lock index 2ef229b..24c344a 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -121,6 +121,29 @@ version = "1.0.102" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "7f202df86484c868dbad7eaa557ef785d5c66295e41b460ef922eca0723b842c" +[[package]] +name = "async-broadcast" +version = "0.7.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "435a87a52755b8f27fcf321ac4f04b2802e337c8c4872923137471ec39c37532" +dependencies = [ + "event-listener", + "event-listener-strategy", + "futures-core", + "pin-project-lite", +] + +[[package]] +name = "async-recursion" +version = "1.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3b43422f69d8ff38f95f1b2bb76517c91589a924d1559a0e935d7c8ce0274c11" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + [[package]] name = "async-trait" version = "0.1.89" @@ -363,6 +386,7 @@ dependencies = [ "tokio", "tracing", "tsclientlib", + "zbus", ] [[package]] @@ -485,6 +509,15 @@ dependencies = [ "memchr", ] +[[package]] +name = "concurrent-queue" +version = "2.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4ca0197aee26d1ae37445ee532fefce43251d24cc7c166799f4d46817f1d3973" +dependencies = [ + "crossbeam-utils", +] + [[package]] name = "console_error_panic_hook" version = "0.1.7" @@ -888,6 +921,33 @@ dependencies = [ "cfg-if", ] +[[package]] +name = "endi" +version = "1.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "66b7e2430c6dff6a955451e2cfc438f09cea1965a9d6f87f7e3b90decc014099" + +[[package]] +name = "enumflags2" +version = "0.7.12" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1027f7680c853e056ebcec683615fb6fbbc07dbaa13b4d5d9442b146ded4ecef" +dependencies = [ + "enumflags2_derive", + "serde", +] + +[[package]] +name = "enumflags2_derive" +version = "0.7.12" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "67c78a4d8fdf9953a5c9d458f9efe940fd97a0cab0941c075a813ac594733827" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + [[package]] name = "env_filter" version = "0.1.4" @@ -904,6 +964,37 @@ version = "1.0.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "877a4ace8713b0bcf2a4e7eec82529c029f1d0619886d18145fea96c3ffe5c0f" +[[package]] +name = "errno" +version = "0.3.14" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "39cab71617ae0d63f51a36d69f866391735b51691dbda63cf6f96d042b63efeb" +dependencies = [ + "libc", + "windows-sys 0.61.2", +] + +[[package]] +name = "event-listener" +version = "5.4.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e13b66accf52311f30a0db42147dadea9850cb48cd070028831ae5f5d4b856ab" +dependencies = [ + "concurrent-queue", + "parking", + "pin-project-lite", +] + +[[package]] +name = "event-listener-strategy" +version = "0.5.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8be9f3dfaaffdae2972880079a491a1a8bb7cbed0b8dd7a347f668b4150a3b93" +dependencies = [ + "event-listener", + "pin-project-lite", +] + [[package]] name = "fallible-iterator" version = "0.3.0" @@ -916,6 +1007,12 @@ version = "0.1.9" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "7360491ce676a36bf9bb3c56c1aa791658183a54d2744120f27285738d90465a" +[[package]] +name = "fastrand" +version = "2.4.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9f1f227452a390804cdb637b74a86990f2a7d7ba4b7d5693aac9b4dd6defd8d6" + [[package]] name = "ff" version = "0.13.1" @@ -1049,6 +1146,19 @@ version = "0.3.32" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "cecba35d7ad927e23624b22ad55235f2239cfa44fd10428eecbeba6d6a717718" +[[package]] +name = "futures-lite" +version = "2.6.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f78e10609fe0e0b3f4157ffab1876319b5b0db102a2c60dc4626306dc46b44ad" +dependencies = [ + "fastrand", + "futures-core", + "futures-io", + "parking", + "pin-project-lite", +] + [[package]] name = "futures-macro" version = "0.3.32" @@ -1763,6 +1873,12 @@ dependencies = [ "libc", ] +[[package]] +name = "linux-raw-sys" +version = "0.12.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "32a66949e030da00e8c7d4434b251670a91556f4144941d37452769c25d58a53" + [[package]] name = "litemap" version = "0.8.2" @@ -1824,6 +1940,15 @@ version = "2.8.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "f8ca58f447f06ed17d5fc4043ce1b10dd205e060fb3ce5b979b8ed8e59ff3f79" +[[package]] +name = "memoffset" +version = "0.9.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "488016bfae457b036d996092f6cb448677611ce4449e970ceaf42695203f218a" +dependencies = [ + "autocfg", +] + [[package]] name = "mime" version = "0.3.17" @@ -2110,6 +2235,16 @@ version = "0.2.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "7c87def4c32ab89d880effc9e097653c8da5d6ef28e6b539d313baaacfbafcbe" +[[package]] +name = "ordered-stream" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9aa2b01e1d916879f73a53d01d1d6cee68adbb31d6d9177a8cfce093cced1d50" +dependencies = [ + "futures-core", + "pin-project-lite", +] + [[package]] name = "oslog" version = "0.2.0" @@ -2133,6 +2268,12 @@ dependencies = [ "sha2", ] +[[package]] +name = "parking" +version = "2.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f38d5652c16fde515bb1ecef450ab0f6a219d619a7274976324d5e377f7dceba" + [[package]] name = "parking_lot" version = "0.12.5" @@ -2613,6 +2754,19 @@ dependencies = [ "semver", ] +[[package]] +name = "rustix" +version = "1.1.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b6fe4565b9518b83ef4f91bb47ce29620ca828bd32cb7e408f0062e9930ba190" +dependencies = [ + "bitflags 2.11.1", + "errno", + "libc", + "linux-raw-sys", + "windows-sys 0.61.2", +] + [[package]] name = "rustls" version = "0.23.40" @@ -2829,6 +2983,17 @@ dependencies = [ "zmij", ] +[[package]] +name = "serde_repr" +version = "0.1.20" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "175ee3e80ae9982737ca543e96133087cbd9a485eecc3bc4de9c1a37b47ea59c" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + [[package]] name = "serde_spanned" version = "1.1.1" @@ -2875,6 +3040,16 @@ version = "1.3.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "0fda2ff0d084019ba4d7c6f371c95d8fd75ce3524c3cb8fb653a3023f6323e64" +[[package]] +name = "signal-hook-registry" +version = "1.4.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c4db69cba1110affc0e9f7bcd48bbf87b3f4fc7c61fc9155afd4c469eb3d6c1b" +dependencies = [ + "errno", + "libc", +] + [[package]] name = "signature" version = "2.2.0" @@ -3033,6 +3208,19 @@ version = "0.2.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "7b2093cf4c8eb1e67749a6762251bc9cd836b6fc171623bd0a9d324d37af2417" +[[package]] +name = "tempfile" +version = "3.27.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "32497e9a4c7b38532efcdebeef879707aa9f794296a4f0244f6f69e9bc8574bd" +dependencies = [ + "fastrand", + "getrandom 0.4.2", + "once_cell", + "rustix", + "windows-sys 0.61.2", +] + [[package]] name = "thiserror" version = "1.0.69" @@ -3157,8 +3345,10 @@ dependencies = [ "libc", "mio", "pin-project-lite", + "signal-hook-registry", "socket2", "tokio-macros", + "tracing", "windows-sys 0.61.2", ] @@ -3519,6 +3709,17 @@ version = "1.20.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "40ce102ab67701b8526c123c1bab5cbe42d7040ccfd0f64af1a385808d2f43de" +[[package]] +name = "uds_windows" +version = "1.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f2f6fb2847f6742cd76af783a2a2c49e9375d0a111c7bef6f71cd9e738c72d6e" +dependencies = [ + "memoffset", + "tempfile", + "windows-sys 0.61.2", +] + [[package]] name = "unicode-ident" version = "1.0.24" @@ -3573,6 +3774,7 @@ checksum = "ddd74a9687298c6858e9b88ec8935ec45d22e8fd5e6394fa1bd4e99a87789c76" dependencies = [ "getrandom 0.4.2", "js-sys", + "serde_core", "wasm-bindgen", ] @@ -4188,6 +4390,62 @@ dependencies = [ "synstructure", ] +[[package]] +name = "zbus" +version = "5.15.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c3bcbf15c8708d7fc1be0c993622e0a5cbd5e8b52bfa40afa4c3e0cd8d724ac1" +dependencies = [ + "async-broadcast", + "async-recursion", + "async-trait", + "enumflags2", + "event-listener", + "futures-core", + "futures-lite", + "hex", + "libc", + "ordered-stream", + "rustix", + "serde", + "serde_repr", + "tokio", + "tracing", + "uds_windows", + "uuid", + "windows-sys 0.61.2", + "winnow", + "zbus_macros", + "zbus_names", + "zvariant", +] + +[[package]] +name = "zbus_macros" +version = "5.15.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "51fa5406ad9175a8c825a931f8cf347116b531b3634fcb0b627c290f1f2516ff" +dependencies = [ + "proc-macro-crate", + "proc-macro2", + "quote", + "syn", + "zbus_names", + "zvariant", + "zvariant_utils", +] + +[[package]] +name = "zbus_names" +version = "4.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7074f3e50b894eac91750142016d30d0a89be8e67dbfd9704fb875825760e52d" +dependencies = [ + "serde", + "winnow", + "zvariant", +] + [[package]] name = "zerocopy" version = "0.8.48" @@ -4287,3 +4545,43 @@ name = "zmij" version = "1.0.21" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "b8848ee67ecc8aedbaf3e4122217aff892639231befc6a1b58d29fff4c2cabaa" + +[[package]] +name = "zvariant" +version = "5.11.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1c1567a6ec68df868cbbfde844cfc6d81649fe5109a62b116b19fabd53e618ee" +dependencies = [ + "endi", + "enumflags2", + "serde", + "winnow", + "zvariant_derive", + "zvariant_utils", +] + +[[package]] +name = "zvariant_derive" +version = "5.11.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c7d5b780599bbde114e39d9a0799577fad1ced5105d38515745f7b3099d8ceda" +dependencies = [ + "proc-macro-crate", + "proc-macro2", + "quote", + "syn", + "zvariant_utils", +] + +[[package]] +name = "zvariant_utils" +version = "3.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6d464f5733ffa07a3164d656f18533caace9d0638596721355d73256a410d691" +dependencies = [ + "proc-macro2", + "quote", + "serde", + "syn", + "winnow", +] diff --git a/apps/chanora_flutter/lib/l10n/app_en.arb b/apps/chanora_flutter/lib/l10n/app_en.arb index 4d4b2bb..7f27346 100644 --- a/apps/chanora_flutter/lib/l10n/app_en.arb +++ b/apps/chanora_flutter/lib/l10n/app_en.arb @@ -39,6 +39,13 @@ "backend": { "type": "String" } } }, + "pttConfigureAction": "Configure", + "pttConfigureTitle": "Configure Push-to-Talk binding", + "pttConfigurePrompt": "Press the key or mouse side button you want to use for Push-to-Talk.", + "pttConfigureWaiting": "(waiting for input…)", + "pttConfigureCaptured": "Captured", + "pttConfigurePrivacyNote": "Chanora never logs the actual key value. Only the input class (keyboard / mouse-side-button) and a platform-neutral label leave this dialog.", + "pttConfigureSaveAction": "Save", "inputMuteAction": "Mute mic", "inputUnmuteAction": "Unmute mic", "outputMuteAction": "Mute speaker", diff --git a/apps/chanora_flutter/lib/l10n/app_zh.arb b/apps/chanora_flutter/lib/l10n/app_zh.arb index ceba9d1..404316f 100644 --- a/apps/chanora_flutter/lib/l10n/app_zh.arb +++ b/apps/chanora_flutter/lib/l10n/app_zh.arb @@ -29,6 +29,13 @@ "pttHoldToTalk": "按住说话", "pttTransmitting": "正在发送…", "pttCapabilityBadge": "对讲能力:{level}({backend})", + "pttConfigureAction": "配置", + "pttConfigureTitle": "配置对讲按键", + "pttConfigurePrompt": "按下您希望用于对讲的按键或鼠标侧键。", + "pttConfigureWaiting": "(等待输入…)", + "pttConfigureCaptured": "已捕获", + "pttConfigurePrivacyNote": "Chanora 不会记录具体的按键值。本对话框只会向应用提交输入类别(键盘 / 鼠标侧键)以及一个跨平台标签。", + "pttConfigureSaveAction": "保存", "inputMuteAction": "静音麦克风", "inputUnmuteAction": "取消麦克风静音", "outputMuteAction": "静音扬声器", diff --git a/apps/chanora_flutter/lib/l10n/generated/app_localizations.dart b/apps/chanora_flutter/lib/l10n/generated/app_localizations.dart index ff76e4e..86419a9 100644 --- a/apps/chanora_flutter/lib/l10n/generated/app_localizations.dart +++ b/apps/chanora_flutter/lib/l10n/generated/app_localizations.dart @@ -247,6 +247,48 @@ abstract class AppL10n { /// **'PTT: {level} ({backend})'** String pttCapabilityBadge(String level, String backend); + /// No description provided for @pttConfigureAction. + /// + /// In en, this message translates to: + /// **'Configure'** + String get pttConfigureAction; + + /// No description provided for @pttConfigureTitle. + /// + /// In en, this message translates to: + /// **'Configure Push-to-Talk binding'** + String get pttConfigureTitle; + + /// No description provided for @pttConfigurePrompt. + /// + /// In en, this message translates to: + /// **'Press the key or mouse side button you want to use for Push-to-Talk.'** + String get pttConfigurePrompt; + + /// No description provided for @pttConfigureWaiting. + /// + /// In en, this message translates to: + /// **'(waiting for input…)'** + String get pttConfigureWaiting; + + /// No description provided for @pttConfigureCaptured. + /// + /// In en, this message translates to: + /// **'Captured'** + String get pttConfigureCaptured; + + /// No description provided for @pttConfigurePrivacyNote. + /// + /// In en, this message translates to: + /// **'Chanora never logs the actual key value. Only the input class (keyboard / mouse-side-button) and a platform-neutral label leave this dialog.'** + String get pttConfigurePrivacyNote; + + /// No description provided for @pttConfigureSaveAction. + /// + /// In en, this message translates to: + /// **'Save'** + String get pttConfigureSaveAction; + /// No description provided for @inputMuteAction. /// /// In en, this message translates to: diff --git a/apps/chanora_flutter/lib/l10n/generated/app_localizations_en.dart b/apps/chanora_flutter/lib/l10n/generated/app_localizations_en.dart index 3f84b86..9e79a59 100644 --- a/apps/chanora_flutter/lib/l10n/generated/app_localizations_en.dart +++ b/apps/chanora_flutter/lib/l10n/generated/app_localizations_en.dart @@ -92,6 +92,29 @@ class AppL10nEn extends AppL10n { return 'PTT: $level ($backend)'; } + @override + String get pttConfigureAction => 'Configure'; + + @override + String get pttConfigureTitle => 'Configure Push-to-Talk binding'; + + @override + String get pttConfigurePrompt => + 'Press the key or mouse side button you want to use for Push-to-Talk.'; + + @override + String get pttConfigureWaiting => '(waiting for input…)'; + + @override + String get pttConfigureCaptured => 'Captured'; + + @override + String get pttConfigurePrivacyNote => + 'Chanora never logs the actual key value. Only the input class (keyboard / mouse-side-button) and a platform-neutral label leave this dialog.'; + + @override + String get pttConfigureSaveAction => 'Save'; + @override String get inputMuteAction => 'Mute mic'; diff --git a/apps/chanora_flutter/lib/l10n/generated/app_localizations_zh.dart b/apps/chanora_flutter/lib/l10n/generated/app_localizations_zh.dart index e103e5b..b7b9715 100644 --- a/apps/chanora_flutter/lib/l10n/generated/app_localizations_zh.dart +++ b/apps/chanora_flutter/lib/l10n/generated/app_localizations_zh.dart @@ -90,6 +90,28 @@ class AppL10nZh extends AppL10n { return '对讲能力:$level($backend)'; } + @override + String get pttConfigureAction => '配置'; + + @override + String get pttConfigureTitle => '配置对讲按键'; + + @override + String get pttConfigurePrompt => '按下您希望用于对讲的按键或鼠标侧键。'; + + @override + String get pttConfigureWaiting => '(等待输入…)'; + + @override + String get pttConfigureCaptured => '已捕获'; + + @override + String get pttConfigurePrivacyNote => + 'Chanora 不会记录具体的按键值。本对话框只会向应用提交输入类别(键盘 / 鼠标侧键)以及一个跨平台标签。'; + + @override + String get pttConfigureSaveAction => '保存'; + @override String get inputMuteAction => '静音麦克风'; diff --git a/apps/chanora_flutter/lib/main.dart b/apps/chanora_flutter/lib/main.dart index f86321a..18628ff 100644 --- a/apps/chanora_flutter/lib/main.dart +++ b/apps/chanora_flutter/lib/main.dart @@ -391,6 +391,38 @@ class _BetaHomeState extends State<_BetaHome> { ); } + Future _onConfigurePtt(BuildContext context) async { + // Open a focus-scoped dialog that captures the next key press + // and submits it to the platform backend as the new PTT + // binding. The bridge carries only the coarse input class and + // an opaque platform-key string; the actual key value never + // appears in any log record (DEC-027 / SRS-202). + final l10n = AppL10n.of(context); + if (!mounted) return; + final binding = await showDialog<_CapturedBinding>( + context: context, + builder: (ctx) => const _PttBindingCaptureDialog(), + ); + if (binding == null) return; + try { + await rust.setPttBinding( + inputClass: binding.inputClass, + platformKey: binding.platformKey, + ); + } catch (e) { + if (!mounted) return; + // Surface the failure as a snackbar so the user sees that + // their binding did not stick. + if (!mounted) return; + // Use the State's context (guaranteed valid because we + // re-checked `mounted` immediately above). + final messenger = ScaffoldMessenger.of(this.context); + messenger.showSnackBar(SnackBar( + content: Text(l10n.statusError(e.toString())), + )); + } + } + Future _onShowAbout(BuildContext context) async { // DEC-018 / DEC-019 / DEC-020 surface: public name, non- // affiliation statement, dual-license declaration. The Flutter @@ -670,6 +702,7 @@ class _BetaHomeState extends State<_BetaHome> { onToggleInputMute: _toggleInputMute, onToggleOutputMute: _toggleOutputMute, onGainChanged: _setOutputGain, + onConfigurePtt: () => _onConfigurePtt(context), ), const SizedBox(height: 12), ], @@ -827,6 +860,7 @@ class _AudioControls extends StatefulWidget { required this.onToggleInputMute, required this.onToggleOutputMute, required this.onGainChanged, + required this.onConfigurePtt, }); final rust.BridgeAudioStats? stats; @@ -841,6 +875,7 @@ class _AudioControls extends StatefulWidget { final VoidCallback onToggleInputMute; final VoidCallback onToggleOutputMute; final ValueChanged onGainChanged; + final VoidCallback onConfigurePtt; @override State<_AudioControls> createState() => _AudioControlsState(); @@ -896,6 +931,11 @@ class _AudioControlsState extends State<_AudioControls> { ), ), ), + TextButton.icon( + icon: const Icon(Icons.tune, size: 14), + label: Text(l10n.pttConfigureAction), + onPressed: widget.onConfigurePtt, + ), ], ), ), @@ -1072,3 +1112,151 @@ class _SnapshotView extends StatelessWidget { ); } } + +/// Result of a successful PTT binding capture. Carries only the +/// coarse input class and an opaque platform-key label — +/// per DEC-027 the actual key code never crosses out of the +/// capture dialog. +class _CapturedBinding { + const _CapturedBinding({required this.inputClass, required this.platformKey}); + final rust.BridgePttInputClass inputClass; + final String platformKey; +} + +/// Focus-scoped dialog that captures the next key press or mouse +/// side-button click and returns it as a `_CapturedBinding`. The +/// captured value is the platform-neutral `LogicalKeyboardKey` +/// debug label or `"mouse-side-button:{button}"`; the platform +/// backend interprets the string and never logs it. +class _PttBindingCaptureDialog extends StatefulWidget { + const _PttBindingCaptureDialog(); + + @override + State<_PttBindingCaptureDialog> createState() => + _PttBindingCaptureDialogState(); +} + +class _PttBindingCaptureDialogState extends State<_PttBindingCaptureDialog> { + final FocusNode _focusNode = FocusNode(); + String? _captured; + rust.BridgePttInputClass _capturedClass = rust.BridgePttInputClass.none; + + @override + void initState() { + super.initState(); + WidgetsBinding.instance.addPostFrameCallback((_) { + _focusNode.requestFocus(); + }); + } + + @override + void dispose() { + _focusNode.dispose(); + super.dispose(); + } + + KeyEventResult _onKeyEvent(FocusNode node, KeyEvent event) { + if (event is! KeyDownEvent) return KeyEventResult.ignored; + // Skip modifier-only presses so the user can chord into the + // real binding. + final keyLabel = event.logicalKey.keyLabel; + if (keyLabel.isEmpty) return KeyEventResult.ignored; + setState(() { + _captured = keyLabel; + _capturedClass = rust.BridgePttInputClass.keyboard; + }); + return KeyEventResult.handled; + } + + void _captureMouseSideButton(int button) { + setState(() { + _captured = 'mouse-side-button:$button'; + _capturedClass = rust.BridgePttInputClass.mouseSideButton; + }); + } + + @override + Widget build(BuildContext context) { + final l10n = AppL10n.of(context); + final theme = Theme.of(context); + return AlertDialog( + title: Text(l10n.pttConfigureTitle), + content: SizedBox( + width: 360, + child: Focus( + focusNode: _focusNode, + onKeyEvent: _onKeyEvent, + autofocus: true, + child: Listener( + // Capture mouse side buttons (4 and 5) without + // capturing primary / secondary clicks which the + // user uses to interact with the dialog itself. The + // raw button bitmask values are stable per Flutter's + // `PointerEvent.buttons` documentation (back = 0x08, + // forward = 0x10). + onPointerDown: (e) { + const int back = 0x08; + const int forward = 0x10; + if (e.buttons == back || e.buttons == forward) { + _captureMouseSideButton(e.buttons); + } + }, + child: Column( + mainAxisSize: MainAxisSize.min, + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Text( + l10n.pttConfigurePrompt, + style: theme.textTheme.bodyMedium, + ), + const SizedBox(height: 12), + Container( + padding: const EdgeInsets.symmetric( + vertical: 12, + horizontal: 16, + ), + decoration: BoxDecoration( + color: theme.colorScheme.surfaceContainerHighest, + borderRadius: BorderRadius.circular(6), + ), + child: Text( + _captured == null + ? l10n.pttConfigureWaiting + : '${l10n.pttConfigureCaptured}: $_captured', + style: theme.textTheme.bodyMedium?.copyWith( + fontFamily: 'monospace', + ), + ), + ), + const SizedBox(height: 12), + Text( + l10n.pttConfigurePrivacyNote, + style: theme.textTheme.bodySmall?.copyWith( + color: theme.colorScheme.onSurfaceVariant, + ), + ), + ], + ), + ), + ), + ), + actions: [ + TextButton( + onPressed: () => Navigator.of(context).pop(), + child: Text(l10n.closeAction), + ), + FilledButton( + onPressed: _captured == null + ? null + : () => Navigator.of(context).pop( + _CapturedBinding( + inputClass: _capturedClass, + platformKey: _captured!, + ), + ), + child: Text(l10n.pttConfigureSaveAction), + ), + ], + ); + } +} diff --git a/apps/chanora_flutter/lib/src/rust/api.dart b/apps/chanora_flutter/lib/src/rust/api.dart index 766d683..960c03e 100644 --- a/apps/chanora_flutter/lib/src/rust/api.dart +++ b/apps/chanora_flutter/lib/src/rust/api.dart @@ -10,7 +10,7 @@ import 'package:freezed_annotation/freezed_annotation.dart' hide protected; part 'api.freezed.dart'; // These functions are ignored because they are not marked as `pub`: `log_sink`, `runtime`, `session` -// These function are ignored because they are on traits that is not defined in current crate (put an empty `#[frb]` on it to unignore): `clone`, `clone`, `clone`, `clone`, `clone`, `clone`, `clone`, `fmt`, `fmt`, `fmt`, `fmt`, `fmt`, `fmt`, `fmt`, `from`, `from`, `from`, `from`, `from` +// These function are ignored because they are on traits that is not defined in current crate (put an empty `#[frb]` on it to unignore): `clone`, `clone`, `clone`, `clone`, `clone`, `clone`, `clone`, `clone`, `fmt`, `fmt`, `fmt`, `fmt`, `fmt`, `fmt`, `fmt`, `fmt`, `from`, `from`, `from`, `from`, `from`, `from` /// Connect to a TeamSpeak-compatible server and return the initial /// state snapshot. Honours the DEC-006 single-connection invariant @@ -45,6 +45,26 @@ Future startAudio() => RustLib.instance.api.crateApiStartAudio(); Future setPtt({required bool active}) => RustLib.instance.api.crateApiSetPtt(active: active); +/// Update the active PTT binding (gen2 v0.9.3 / DEC-026). The +/// platform_key string is opaque to the bridge — it identifies the +/// bound key inside the platform backend and never appears in any +/// log record or diagnostic export (DEC-027 enforced at the +/// diagnostics-sanitizer layer). +Future setPttBinding({ + required BridgePttInputClass inputClass, + required String platformKey, +}) => RustLib.instance.api.crateApiSetPttBinding( + inputClass: inputClass, + platformKey: platformKey, +); + +/// Read the current PTT capability descriptor. Returns a +/// `(level, backend_id, bound_input_class)` triple matching the +/// privacy-safe `BridgeEvent::PttCapability` event shape; useful +/// for the initial UI render before the first event arrives. +Future<(String, String, String)> pttDescriptor() => + RustLib.instance.api.crateApiPttDescriptor(); + /// Move our own client to `channel_id`. Optional channel password /// for password-protected channels — pass an empty string when not /// required. @@ -357,6 +377,19 @@ enum BridgeNetworkState { offline, } +/// Coarse PTT input class (gen2 v0.9.3 / DEC-026). Stable strings; +/// the bridge never carries raw key codes. +enum BridgePttInputClass { + /// No binding is active. + none, + + /// A keyboard key. + keyboard, + + /// A mouse side button (Mouse4 / Mouse5). + mouseSideButton, +} + /// Server snapshot as seen by Dart. class BridgeSnapshot { /// Server name. diff --git a/apps/chanora_flutter/lib/src/rust/frb_generated.dart b/apps/chanora_flutter/lib/src/rust/frb_generated.dart index 5587928..bb398a6 100644 --- a/apps/chanora_flutter/lib/src/rust/frb_generated.dart +++ b/apps/chanora_flutter/lib/src/rust/frb_generated.dart @@ -67,7 +67,7 @@ class RustLib extends BaseEntrypoint { String get codegenVersion => '2.12.0'; @override - int get rustContentHash => 663465485; + int get rustContentHash => -427953414; static const kDefaultExternalLibraryLoaderConfig = ExternalLibraryLoaderConfig( @@ -110,6 +110,8 @@ abstract class RustLibApi extends BaseApi { required String password, }); + Future<(String, String, String)> crateApiPttDescriptor(); + Future crateApiSetInputMuted({required bool muted}); void crateApiSetNetworkState({required BridgeNetworkState state}); @@ -120,6 +122,11 @@ abstract class RustLibApi extends BaseApi { Future crateApiSetPtt({required bool active}); + Future crateApiSetPttBinding({ + required BridgePttInputClass inputClass, + required String platformKey, + }); + Future crateApiSnapshot(); Future crateApiStartAudio(); @@ -478,6 +485,33 @@ class RustLibApiImpl extends RustLibApiImplPlatform implements RustLibApi { argNames: ["channelId", "password"], ); + @override + Future<(String, String, String)> crateApiPttDescriptor() { + return handler.executeNormal( + NormalTask( + callFfi: (port_) { + final serializer = SseSerializer(generalizedFrbRustBinding); + pdeCallFfi( + generalizedFrbRustBinding, + serializer, + funcId: 13, + port: port_, + ); + }, + codec: SseCodec( + decodeSuccessData: sse_decode_record_string_string_string, + decodeErrorData: null, + ), + constMeta: kCrateApiPttDescriptorConstMeta, + argValues: [], + apiImpl: this, + ), + ); + } + + TaskConstMeta get kCrateApiPttDescriptorConstMeta => + const TaskConstMeta(debugName: "ptt_descriptor", argNames: []); + @override Future crateApiSetInputMuted({required bool muted}) { return handler.executeNormal( @@ -488,7 +522,7 @@ class RustLibApiImpl extends RustLibApiImplPlatform implements RustLibApi { pdeCallFfi( generalizedFrbRustBinding, serializer, - funcId: 13, + funcId: 14, port: port_, ); }, @@ -513,7 +547,7 @@ class RustLibApiImpl extends RustLibApiImplPlatform implements RustLibApi { callFfi: () { final serializer = SseSerializer(generalizedFrbRustBinding); sse_encode_bridge_network_state(state, serializer); - return pdeCallFfi(generalizedFrbRustBinding, serializer, funcId: 14)!; + return pdeCallFfi(generalizedFrbRustBinding, serializer, funcId: 15)!; }, codec: SseCodec( decodeSuccessData: sse_decode_unit, @@ -539,7 +573,7 @@ class RustLibApiImpl extends RustLibApiImplPlatform implements RustLibApi { pdeCallFfi( generalizedFrbRustBinding, serializer, - funcId: 15, + funcId: 16, port: port_, ); }, @@ -567,7 +601,7 @@ class RustLibApiImpl extends RustLibApiImplPlatform implements RustLibApi { pdeCallFfi( generalizedFrbRustBinding, serializer, - funcId: 16, + funcId: 17, port: port_, ); }, @@ -595,7 +629,7 @@ class RustLibApiImpl extends RustLibApiImplPlatform implements RustLibApi { pdeCallFfi( generalizedFrbRustBinding, serializer, - funcId: 17, + funcId: 18, port: port_, ); }, @@ -613,6 +647,40 @@ class RustLibApiImpl extends RustLibApiImplPlatform implements RustLibApi { TaskConstMeta get kCrateApiSetPttConstMeta => const TaskConstMeta(debugName: "set_ptt", argNames: ["active"]); + @override + Future crateApiSetPttBinding({ + required BridgePttInputClass inputClass, + required String platformKey, + }) { + return handler.executeNormal( + NormalTask( + callFfi: (port_) { + final serializer = SseSerializer(generalizedFrbRustBinding); + sse_encode_bridge_ptt_input_class(inputClass, serializer); + sse_encode_String(platformKey, serializer); + pdeCallFfi( + generalizedFrbRustBinding, + serializer, + funcId: 19, + port: port_, + ); + }, + codec: SseCodec( + decodeSuccessData: sse_decode_unit, + decodeErrorData: sse_decode_bridge_error, + ), + constMeta: kCrateApiSetPttBindingConstMeta, + argValues: [inputClass, platformKey], + apiImpl: this, + ), + ); + } + + TaskConstMeta get kCrateApiSetPttBindingConstMeta => const TaskConstMeta( + debugName: "set_ptt_binding", + argNames: ["inputClass", "platformKey"], + ); + @override Future crateApiSnapshot() { return handler.executeNormal( @@ -622,7 +690,7 @@ class RustLibApiImpl extends RustLibApiImplPlatform implements RustLibApi { pdeCallFfi( generalizedFrbRustBinding, serializer, - funcId: 18, + funcId: 20, port: port_, ); }, @@ -649,7 +717,7 @@ class RustLibApiImpl extends RustLibApiImplPlatform implements RustLibApi { pdeCallFfi( generalizedFrbRustBinding, serializer, - funcId: 19, + funcId: 21, port: port_, ); }, @@ -677,7 +745,7 @@ class RustLibApiImpl extends RustLibApiImplPlatform implements RustLibApi { pdeCallFfi( generalizedFrbRustBinding, serializer, - funcId: 20, + funcId: 22, port: port_, ); }, @@ -847,6 +915,12 @@ class RustLibApiImpl extends RustLibApiImplPlatform implements RustLibApi { return BridgeNetworkState.values[raw as int]; } + @protected + BridgePttInputClass dco_decode_bridge_ptt_input_class(dynamic raw) { + // Codec=Dco (DartCObject based), see doc to use other codecs + return BridgePttInputClass.values[raw as int]; + } + @protected BridgeSnapshot dco_decode_bridge_snapshot(dynamic raw) { // Codec=Dco (DartCObject based), see doc to use other codecs @@ -905,6 +979,20 @@ class RustLibApiImpl extends RustLibApiImplPlatform implements RustLibApi { return raw as Uint8List; } + @protected + (String, String, String) dco_decode_record_string_string_string(dynamic raw) { + // Codec=Dco (DartCObject based), see doc to use other codecs + final arr = raw as List; + if (arr.length != 3) { + throw Exception('Expected 3 elements, got ${arr.length}'); + } + return ( + dco_decode_String(arr[0]), + dco_decode_String(arr[1]), + dco_decode_String(arr[2]), + ); + } + @protected int dco_decode_u_32(dynamic raw) { // Codec=Dco (DartCObject based), see doc to use other codecs @@ -1103,6 +1191,15 @@ class RustLibApiImpl extends RustLibApiImplPlatform implements RustLibApi { return BridgeNetworkState.values[inner]; } + @protected + BridgePttInputClass sse_decode_bridge_ptt_input_class( + SseDeserializer deserializer, + ) { + // Codec=Sse (Serialization based), see doc to use other codecs + var inner = sse_decode_i_32(deserializer); + return BridgePttInputClass.values[inner]; + } + @protected BridgeSnapshot sse_decode_bridge_snapshot(SseDeserializer deserializer) { // Codec=Sse (Serialization based), see doc to use other codecs @@ -1189,6 +1286,17 @@ class RustLibApiImpl extends RustLibApiImplPlatform implements RustLibApi { return deserializer.buffer.getUint8List(len_); } + @protected + (String, String, String) sse_decode_record_string_string_string( + SseDeserializer deserializer, + ) { + // Codec=Sse (Serialization based), see doc to use other codecs + var var_field0 = sse_decode_String(deserializer); + var var_field1 = sse_decode_String(deserializer); + var var_field2 = sse_decode_String(deserializer); + return (var_field0, var_field1, var_field2); + } + @protected int sse_decode_u_32(SseDeserializer deserializer) { // Codec=Sse (Serialization based), see doc to use other codecs @@ -1376,6 +1484,15 @@ class RustLibApiImpl extends RustLibApiImplPlatform implements RustLibApi { sse_encode_i_32(self.index, serializer); } + @protected + void sse_encode_bridge_ptt_input_class( + BridgePttInputClass self, + SseSerializer serializer, + ) { + // Codec=Sse (Serialization based), see doc to use other codecs + sse_encode_i_32(self.index, serializer); + } + @protected void sse_encode_bridge_snapshot( BridgeSnapshot self, @@ -1454,6 +1571,17 @@ class RustLibApiImpl extends RustLibApiImplPlatform implements RustLibApi { serializer.buffer.putUint8List(self); } + @protected + void sse_encode_record_string_string_string( + (String, String, String) self, + SseSerializer serializer, + ) { + // Codec=Sse (Serialization based), see doc to use other codecs + sse_encode_String(self.$1, serializer); + sse_encode_String(self.$2, serializer); + sse_encode_String(self.$3, serializer); + } + @protected void sse_encode_u_32(int self, SseSerializer serializer) { // Codec=Sse (Serialization based), see doc to use other codecs diff --git a/apps/chanora_flutter/lib/src/rust/frb_generated.io.dart b/apps/chanora_flutter/lib/src/rust/frb_generated.io.dart index bed705f..06e552c 100644 --- a/apps/chanora_flutter/lib/src/rust/frb_generated.io.dart +++ b/apps/chanora_flutter/lib/src/rust/frb_generated.io.dart @@ -57,6 +57,9 @@ abstract class RustLibApiImplPlatform extends BaseApiImpl { @protected BridgeNetworkState dco_decode_bridge_network_state(dynamic raw); + @protected + BridgePttInputClass dco_decode_bridge_ptt_input_class(dynamic raw); + @protected BridgeSnapshot dco_decode_bridge_snapshot(dynamic raw); @@ -81,6 +84,9 @@ abstract class RustLibApiImplPlatform extends BaseApiImpl { @protected Uint8List dco_decode_list_prim_u_8_strict(dynamic raw); + @protected + (String, String, String) dco_decode_record_string_string_string(dynamic raw); + @protected int dco_decode_u_32(dynamic raw); @@ -135,6 +141,11 @@ abstract class RustLibApiImplPlatform extends BaseApiImpl { SseDeserializer deserializer, ); + @protected + BridgePttInputClass sse_decode_bridge_ptt_input_class( + SseDeserializer deserializer, + ); + @protected BridgeSnapshot sse_decode_bridge_snapshot(SseDeserializer deserializer); @@ -165,6 +176,11 @@ abstract class RustLibApiImplPlatform extends BaseApiImpl { @protected Uint8List sse_decode_list_prim_u_8_strict(SseDeserializer deserializer); + @protected + (String, String, String) sse_decode_record_string_string_string( + SseDeserializer deserializer, + ); + @protected int sse_decode_u_32(SseDeserializer deserializer); @@ -231,6 +247,12 @@ abstract class RustLibApiImplPlatform extends BaseApiImpl { SseSerializer serializer, ); + @protected + void sse_encode_bridge_ptt_input_class( + BridgePttInputClass self, + SseSerializer serializer, + ); + @protected void sse_encode_bridge_snapshot( BridgeSnapshot self, @@ -270,6 +292,12 @@ abstract class RustLibApiImplPlatform extends BaseApiImpl { SseSerializer serializer, ); + @protected + void sse_encode_record_string_string_string( + (String, String, String) self, + SseSerializer serializer, + ); + @protected void sse_encode_u_32(int self, SseSerializer serializer); diff --git a/apps/chanora_flutter/lib/src/rust/frb_generated.web.dart b/apps/chanora_flutter/lib/src/rust/frb_generated.web.dart index fa7ebb7..5f9b0c5 100644 --- a/apps/chanora_flutter/lib/src/rust/frb_generated.web.dart +++ b/apps/chanora_flutter/lib/src/rust/frb_generated.web.dart @@ -59,6 +59,9 @@ abstract class RustLibApiImplPlatform extends BaseApiImpl { @protected BridgeNetworkState dco_decode_bridge_network_state(dynamic raw); + @protected + BridgePttInputClass dco_decode_bridge_ptt_input_class(dynamic raw); + @protected BridgeSnapshot dco_decode_bridge_snapshot(dynamic raw); @@ -83,6 +86,9 @@ abstract class RustLibApiImplPlatform extends BaseApiImpl { @protected Uint8List dco_decode_list_prim_u_8_strict(dynamic raw); + @protected + (String, String, String) dco_decode_record_string_string_string(dynamic raw); + @protected int dco_decode_u_32(dynamic raw); @@ -137,6 +143,11 @@ abstract class RustLibApiImplPlatform extends BaseApiImpl { SseDeserializer deserializer, ); + @protected + BridgePttInputClass sse_decode_bridge_ptt_input_class( + SseDeserializer deserializer, + ); + @protected BridgeSnapshot sse_decode_bridge_snapshot(SseDeserializer deserializer); @@ -167,6 +178,11 @@ abstract class RustLibApiImplPlatform extends BaseApiImpl { @protected Uint8List sse_decode_list_prim_u_8_strict(SseDeserializer deserializer); + @protected + (String, String, String) sse_decode_record_string_string_string( + SseDeserializer deserializer, + ); + @protected int sse_decode_u_32(SseDeserializer deserializer); @@ -233,6 +249,12 @@ abstract class RustLibApiImplPlatform extends BaseApiImpl { SseSerializer serializer, ); + @protected + void sse_encode_bridge_ptt_input_class( + BridgePttInputClass self, + SseSerializer serializer, + ); + @protected void sse_encode_bridge_snapshot( BridgeSnapshot self, @@ -272,6 +294,12 @@ abstract class RustLibApiImplPlatform extends BaseApiImpl { SseSerializer serializer, ); + @protected + void sse_encode_record_string_string_string( + (String, String, String) self, + SseSerializer serializer, + ); + @protected void sse_encode_u_32(int self, SseSerializer serializer); diff --git a/core/chanora_core/src/lib.rs b/core/chanora_core/src/lib.rs index c4e58c5..1fb92a0 100644 --- a/core/chanora_core/src/lib.rs +++ b/core/chanora_core/src/lib.rs @@ -48,6 +48,7 @@ use tracing::{info, warn}; pub use chanora_audio::{ AudioEngine, AudioEngineConfig, PttBackendDescriptor, PttCapabilityLevel, }; +pub use chanora_audio::{PttBinding, PttInputClass}; pub use chanora_diagnostics::{ DiagnosticExport, InMemoryLogSink, KnownSecretRegistry, RedactingLogLayer, Redactor, }; @@ -478,6 +479,42 @@ impl ChanoraSession { Ok(()) } + /// Update the active PTT binding (gen2 v0.9.3 / DEC-026). The + /// binding flows into the platform backend's `rebind` hook + /// and the freshly-published `PttBackendDescriptor` is + /// broadcast as `SessionEvent::PttCapability` so the UI badge + /// updates immediately. The audio engine must be running. + pub async fn set_ptt_binding(&self, binding: PttBinding) -> Result<(), CoreError> { + let guard = self.inner.lock().await; + let state = guard.as_ref().ok_or(CoreError::NotConnected)?; + let audio = state.audio.as_ref().ok_or(CoreError::AudioNotStarted)?; + let desc = audio.rebind_ptt(binding)?; + let _ = self.events_tx.send(SessionEvent::PttCapability { + level: desc.level.as_str().to_string(), + backend_id: desc.backend_id.to_string(), + bound_input_class: desc.bound_input_class.unwrap_or("").to_string(), + }); + Ok(()) + } + + /// Returns the active PTT capability descriptor as a triple + /// `(level, backend_id, bound_input_class)`. Useful for the + /// initial UI render before the first `PttCapability` event + /// arrives. Returns the universal Focused descriptor when no + /// audio engine is running. + pub async fn ptt_descriptor(&self) -> (String, String, String) { + let guard = self.inner.lock().await; + let desc = match guard.as_ref().and_then(|s| s.audio.as_ref()) { + Some(audio) => audio.ptt_descriptor(), + None => PttBackendDescriptor::focused(), + }; + ( + desc.level.as_str().to_string(), + desc.backend_id.to_string(), + desc.bound_input_class.unwrap_or("").to_string(), + ) + } + /// Move our own client to `channel_id`. Optional channel /// `password` for password-protected channels (empty string /// counts as no password). diff --git a/crates/chanora_audio/Cargo.toml b/crates/chanora_audio/Cargo.toml index 6aa756b..758730c 100644 --- a/crates/chanora_audio/Cargo.toml +++ b/crates/chanora_audio/Cargo.toml @@ -33,3 +33,18 @@ tokio = { version = "1", features = ["sync", "rt", "macros", "time"] } # by the bridge crate's android_init shim. jni = { version = "0.21", default-features = false } ndk-context = "0.1" + +[dev-dependencies] +# `test-util` enables `start_paused` / virtual-clock tests used by +# the missed-key-up watchdog unit tests. +tokio = { version = "1", features = ["sync", "rt", "macros", "time", "test-util"] } + +[target.'cfg(target_os = "linux")'.dependencies] +# GNOME-on-Wayland Global Push-to-Talk uses the freedesktop +# `org.freedesktop.portal.GlobalShortcuts` interface over D-Bus. +# `zbus` is the standard async D-Bus crate; we use the blocking +# proxy for the probe path since portal version reads happen at +# audio-engine start (off the hot path). The `tokio` runtime +# selector is mandatory in zbus 5; we share the tokio runtime +# the rest of the audio + core crates already depend on. +zbus = { version = "5", default-features = false, features = ["tokio", "blocking-api"] } diff --git a/crates/chanora_audio/src/engine.rs b/crates/chanora_audio/src/engine.rs index b3c8a3a..abfb4cc 100644 --- a/crates/chanora_audio/src/engine.rs +++ b/crates/chanora_audio/src/engine.rs @@ -83,7 +83,7 @@ pub struct AudioEngine { /// Windows / macOS / Linux global backends) drive this flag /// through [`Self::set_transmit_active`]; nothing else is /// permitted to flip it (SAD-075 / SDD-089). - transmit_active: Arc, + transmit_gate: crate::ptt::AudioTransmitGate, frames_sent: Arc, frames_received: Arc, /// Master output gain as f32 bits in an AtomicU32. Default 1.0. @@ -108,6 +108,14 @@ pub struct AudioEngine { /// denied microphone permission), PTT becomes a no-op and /// `frames_sent` stays at 0. capture_active: bool, + /// Active desktop PTT backend (SAD-071 / SDD-081). Stored + /// inside a `Mutex>` so `stop()` can move it out + /// and release OS-level resources before the engine is + /// dropped. The value is always `Some` between `start_audio` + /// and `stop`. + ptt_backend: Mutex>>, + /// Missed-key-up watchdog. Dropping aborts the task. + ptt_watchdog: Option, } // cpal::Stream is not Send. We keep the engine pinned to the thread @@ -187,7 +195,8 @@ impl AudioEngine { } } - let transmit_active = Arc::new(AtomicBool::new(cfg.ptt_initial)); + let transmit_gate = crate::ptt::AudioTransmitGate::new(cfg.ptt_initial); + let transmit_flag_for_capture = transmit_gate.flag_arc(); let frames_sent = Arc::new(AtomicU32::new(0)); let frames_received = Arc::new(AtomicU32::new(0)); let output_gain = Arc::new(AtomicU32::new(1.0_f32.to_bits())); @@ -202,7 +211,7 @@ impl AudioEngine { let capture_result = try_open_capture( &in_dev, voice_out_tx, - transmit_active.clone(), + transmit_flag_for_capture, frames_sent.clone(), cfg.mic_gain, ); @@ -298,8 +307,44 @@ impl AudioEngine { } }); + // Select and arm the desktop PTT backend (SAD-071, + // SDD-081). This call is the only place that talks to the + // platform-input layer; the rest of the engine consumes + // the typed `AudioTransmitGate`. We always have a backend + // because the cross-platform factory falls back to + // `FocusedPttBackend` (SDD-087). + let mut ptt_backend = crate::ptt_backends::select(); + let initial_binding = crate::ptt_backends::PttBinding::none(); + match ptt_backend.start(transmit_gate.clone(), initial_binding) { + Ok(()) => { + let d = ptt_backend.descriptor(); + info!( + target: "chanora_audio", + capability_level = %d.level, + backend_id = d.backend_id, + bound_input_class = ?d.bound_input_class, + "ptt backend armed" + ); + } + Err(e) => { + warn!( + target: "chanora_audio", + error = %e, + "ptt backend start failed; engine continues with Focused fallback" + ); + } + } + + // Spawn the missed-key-up watchdog. The task aborts on + // Drop of `MissedKeyUpWatchdog`, so the engine's `stop` + // / Drop chain releases it without explicit cleanup. + let ptt_watchdog = crate::ptt::MissedKeyUpWatchdog::spawn( + transmit_gate.clone(), + crate::ptt::MissedKeyUpWatchdog::DEFAULT_TIMEOUT, + ); + Ok(Self { - transmit_active, + transmit_gate, frames_sent, frames_received, output_gain, @@ -308,6 +353,8 @@ impl AudioEngine { _output_stream: Mutex::new(Some(output_stream)), shutdown_tx: Some(shutdown_tx), capture_active, + ptt_backend: Mutex::new(Some(ptt_backend)), + ptt_watchdog: Some(ptt_watchdog), }) } @@ -316,6 +363,17 @@ impl AudioEngine { if let Some(tx) = self.shutdown_tx.take() { let _ = tx.send(()); } + // Release the active PTT backend's OS resources before + // dropping the streams; the backend may hold a worker + // thread (Raw Input message loop, Event Tap run loop, etc.) + // that needs an explicit stop() to wind down cleanly. + if let Ok(mut guard) = self.ptt_backend.lock() { + if let Some(mut backend) = guard.take() { + backend.stop(); + } + } + // Aborting the watchdog cancels its tokio task. + self.ptt_watchdog.take(); // Drop the streams, which stops their callback threads. let _ = self._input_stream.lock().unwrap().take(); let _ = self._output_stream.lock().unwrap().take(); @@ -330,12 +388,53 @@ impl AudioEngine { /// per-platform global backends in a follow-up — call this /// method exclusively. No-op when capture is inactive. pub fn set_transmit_active(&self, active: bool) { - self.transmit_active.store(active, Ordering::Relaxed); + self.transmit_gate.set(active); } /// Current transmit gate state. pub fn transmit_active(&self) -> bool { - self.transmit_active.load(Ordering::Relaxed) + self.transmit_gate.load() + } + + /// Shared handle to the underlying transmit gate (SAD-075 / + /// SDD-089). Returned for diagnostics and integration tests + /// only; never mutate the underlying atomic directly — use + /// [`Self::set_transmit_active`] instead. + pub fn transmit_gate(&self) -> &crate::ptt::AudioTransmitGate { + &self.transmit_gate + } + + /// Privacy-safe descriptor of the currently active PTT + /// backend (SDD-081 / SDD-091). Returns the universal Focused + /// fallback descriptor when the backend slot is empty + /// (typically only between `stop()` and Drop). + pub fn ptt_descriptor(&self) -> crate::ptt::PttBackendDescriptor { + if let Ok(guard) = self.ptt_backend.lock() { + if let Some(b) = guard.as_ref() { + return b.descriptor(); + } + } + crate::ptt::PttBackendDescriptor::focused() + } + + /// Replace the PTT binding on the active backend. Returns the + /// freshly-published descriptor so callers can re-emit the + /// capability event. + pub fn rebind_ptt( + &self, + binding: crate::ptt_backends::PttBinding, + ) -> Result { + let mut guard = self + .ptt_backend + .lock() + .map_err(|_| AudioError::Backend("ptt_backend mutex poisoned".to_string()))?; + let backend = guard + .as_mut() + .ok_or_else(|| AudioError::Backend("ptt backend not armed".to_string()))?; + backend + .rebind(binding) + .map_err(|e| AudioError::Backend(format!("rebind: {e}")))?; + Ok(backend.descriptor()) } /// Legacy alias for [`Self::set_transmit_active`]. Retained so diff --git a/crates/chanora_audio/src/lib.rs b/crates/chanora_audio/src/lib.rs index 2b60bea..a875059 100644 --- a/crates/chanora_audio/src/lib.rs +++ b/crates/chanora_audio/src/lib.rs @@ -30,9 +30,16 @@ mod engine; pub mod ptt; +pub mod ptt_backends; pub use engine::{AudioEngine, AudioEngineConfig}; -pub use ptt::{PttCapabilityLevel, PttBackendDescriptor}; +pub use ptt::{ + AudioTransmitGate, MissedKeyUpWatchdog, PttBackendDescriptor, PttCapabilityLevel, +}; +pub use ptt_backends::{ + select as select_ptt_backend, DesktopPttBackend, FocusedPttBackend, PttBackendError, + PttBinding, PttInputClass, +}; use thiserror::Error; diff --git a/crates/chanora_audio/src/ptt.rs b/crates/chanora_audio/src/ptt.rs index 8888b07..923a4dd 100644 --- a/crates/chanora_audio/src/ptt.rs +++ b/crates/chanora_audio/src/ptt.rs @@ -1,20 +1,25 @@ //! Desktop Push-to-Talk capability model (SRS-195 / SRS-196 / SAD-071 / //! SDD-081 / SDD-082). //! -//! This module defines the typed `PttCapabilityLevel` enum and a +//! This module defines the typed `PttCapabilityLevel` enum, the //! lightweight `PttBackendDescriptor` value the audio engine -//! publishes to upstream consumers so the UI can render the live -//! capability badge (SDD-091) and the release verification record -//! can carry per-platform evidence (SysDes-148). +//! publishes to upstream consumers, the `AudioTransmitGate` object +//! that owns the authoritative `transmit_active` flag (SAD-075 / +//! SDD-089), and the `MissedKeyUpWatchdog` task (SAD-079 / SDD-092 +//! / DEC-028). //! //! Concrete platform backends (`WindowsRawInputBackend`, -//! `MacOSEventTapBackend`, `LinuxGnomeWaylandBackend`) are deferred -//! to a follow-up code milestone; this commit lands the trait shape -//! and the universal `FocusedPttBackend` constant value so the -//! current Flutter-side hold-to-talk widget reports its capability -//! honestly through the bridge. +//! `MacOSEventTapBackend`, `LinuxGnomeWaylandBackend`) live in the +//! sibling `backends` module; this file holds the cross-platform +//! pieces. use core::fmt; +use std::sync::atomic::{AtomicBool, Ordering}; +use std::sync::Arc; +use std::time::Duration; + +use tokio::sync::watch; +use tracing::{info, warn}; /// Detected runtime capability of the active desktop PTT backend. /// @@ -121,6 +126,172 @@ impl Default for PttBackendDescriptor { } } +/// Owns the authoritative `transmit_active` AtomicBool plus a +/// `tokio::sync::watch` channel so subscribers (notably the +/// missed-key-up watchdog) can observe transitions without +/// polling. Per SAD-075 / SDD-089 this is the **only** mutator +/// path on `transmit_active`; the encoder feed and the platform +/// PTT backends call `set` and `load` exclusively. +/// +/// Cheap to clone — internally an `Arc` over the atomic and the +/// watch sender. +#[derive(Clone)] +pub struct AudioTransmitGate { + inner: Arc, +} + +struct TransmitGateInner { + flag: Arc, + tx: watch::Sender, +} + +impl AudioTransmitGate { + /// Construct a new gate initialised to `initial`. + pub fn new(initial: bool) -> Self { + let (tx, _rx) = watch::channel(initial); + Self { + inner: Arc::new(TransmitGateInner { + flag: Arc::new(AtomicBool::new(initial)), + tx, + }), + } + } + + /// Set the transmit flag. When the value changes, every + /// `watch::Receiver` returned by [`Self::subscribe`] observes + /// the new value. + pub fn set(&self, active: bool) { + // We always store on the atomic (it's the hot read path + // for the encoder feed) and we always send on the watch + // channel — the watch implementation deduplicates so a + // repeat `set(true)` does not wake subscribers. + self.inner.flag.store(active, Ordering::Relaxed); + let _ = self.inner.tx.send(active); + } + + /// Read the current value. Cheap; no locking. + pub fn load(&self) -> bool { + self.inner.flag.load(Ordering::Relaxed) + } + + /// Subscribe to value transitions. The returned receiver + /// observes every distinct transition (the watch channel + /// deduplicates same-value sends). + pub fn subscribe(&self) -> watch::Receiver { + self.inner.tx.subscribe() + } + + /// Live shared handle to the underlying `AtomicBool` for hot + /// paths that need to consult the flag once per outbound audio + /// frame without going through the gate wrappers. Callers must + /// treat the returned Arc as read-only; the gate's `set` + /// remains the sole mutator (SAD-075). + pub fn flag_arc(&self) -> Arc { + self.inner.flag.clone() + } +} + +/// Background task that clears `transmit_active` after a configured +/// timeout (SAD-079 / SDD-092 / DEC-028). +/// +/// Subscribes to the gate's watch channel, notes the timestamp of +/// each `false -> true` transition, clears the timestamp on each +/// `true -> false` transition. If the `true` lifetime exceeds the +/// configured ceiling the task self-clears the gate to false and +/// emits a sanitised diagnostic record (no key data — DEC-027). +pub struct MissedKeyUpWatchdog { + handle: Option>, +} + +impl MissedKeyUpWatchdog { + /// Default timeout per DEC-028 — 30 seconds. + pub const DEFAULT_TIMEOUT: Duration = Duration::from_secs(30); + + /// Spawn the watchdog. The returned handle aborts the task on + /// Drop. The watchdog needs a tokio runtime context; the audio + /// engine spawns it inside `start_audio` where the bridge's + /// runtime is available. + pub fn spawn(gate: AudioTransmitGate, timeout: Duration) -> Self { + let handle = tokio::spawn(async move { + run_watchdog(gate, timeout).await; + }); + Self { + handle: Some(handle), + } + } +} + +impl Drop for MissedKeyUpWatchdog { + fn drop(&mut self) { + if let Some(h) = self.handle.take() { + h.abort(); + } + } +} + +async fn run_watchdog(gate: AudioTransmitGate, timeout: Duration) { + let mut rx = gate.subscribe(); + // Track whether we are currently in a `transmit_active = true` + // window. We don't need the actual timestamp; we just need a + // bounded wait that wakes on either the gate going false or + // the timeout elapsing. + info!( + target: "chanora_audio", + timeout_secs = timeout.as_secs(), + "missed-key-up watchdog spawned" + ); + loop { + // Wait for a transition. + if rx.changed().await.is_err() { + // Gate dropped; exit. + return; + } + let is_active = *rx.borrow_and_update(); + if !is_active { + // Either the user released, or someone else cleared + // the gate. Either way the watchdog has nothing to + // do until the next press. + continue; + } + // The gate just went true. Wait for either a release + // transition or for the timeout to elapse. + let release_or_timeout = tokio::select! { + r = rx.changed() => r.map(|_| *rx.borrow_and_update()), + _ = tokio::time::sleep(timeout) => { + // Timeout — self-clear and emit the privacy-safe + // diagnostic record. The field set here is + // deliberately limited to the values the gen2 + // sanitizer permits (DEC-027): the active + // capability descriptor is owned by the controller + // and is not in scope here, so we emit only the + // watchdog-relevant fact. + warn!( + target: "chanora_audio", + timeout_secs = timeout.as_secs(), + "missed-key-up watchdog fired; clearing transmit_active" + ); + gate.set(false); + continue; + } + }; + match release_or_timeout { + Ok(false) => { + // Normal release; loop and wait for the next press. + } + Ok(true) => { + // Edge case: same-value double-set. Watch + // deduplicates so this should not actually fire, + // but we keep waiting on the timeout for the + // next iteration. + } + Err(_) => { + // Channel closed; exit. + return; + } + } + } +} + #[cfg(test)] mod tests { use super::*; @@ -158,4 +329,57 @@ mod tests { let _: &str = d.backend_id; let _: Option<&str> = d.bound_input_class; } + + #[test] + fn gate_set_and_load_roundtrip() { + let g = AudioTransmitGate::new(false); + assert!(!g.load()); + g.set(true); + assert!(g.load()); + g.set(false); + assert!(!g.load()); + } + + #[tokio::test(flavor = "current_thread", start_paused = true)] + async fn gate_subscribe_observes_transitions() { + let g = AudioTransmitGate::new(false); + let mut rx = g.subscribe(); + g.set(true); + rx.changed().await.unwrap(); + assert!(*rx.borrow_and_update()); + g.set(false); + rx.changed().await.unwrap(); + assert!(!*rx.borrow_and_update()); + } + + #[tokio::test(flavor = "multi_thread", worker_threads = 2)] + async fn watchdog_clears_transmit_after_timeout() { + let g = AudioTransmitGate::new(false); + let _wd = MissedKeyUpWatchdog::spawn(g.clone(), Duration::from_millis(80)); + // Give the watchdog a tick to subscribe before we press. + tokio::time::sleep(Duration::from_millis(20)).await; + g.set(true); + // Wait past the timeout. The watchdog runs on a separate + // worker so real-time elapses in parallel. + tokio::time::sleep(Duration::from_millis(200)).await; + assert!(!g.load(), "watchdog should have cleared transmit_active"); + } + + #[tokio::test(flavor = "multi_thread", worker_threads = 2)] + async fn watchdog_does_not_clear_on_normal_release() { + let g = AudioTransmitGate::new(false); + let _wd = MissedKeyUpWatchdog::spawn(g.clone(), Duration::from_millis(80)); + tokio::time::sleep(Duration::from_millis(20)).await; + g.set(true); + tokio::time::sleep(Duration::from_millis(30)).await; + g.set(false); + // Wait past what would have been the timeout. + tokio::time::sleep(Duration::from_millis(200)).await; + assert!(!g.load()); + // The gate should still be `false` and the next press + // should re-arm the watchdog cleanly. + g.set(true); + tokio::time::sleep(Duration::from_millis(200)).await; + assert!(!g.load(), "watchdog should fire on the second press as well"); + } } diff --git a/crates/chanora_audio/src/ptt_backends/focused.rs b/crates/chanora_audio/src/ptt_backends/focused.rs new file mode 100644 index 0000000..366fd8d --- /dev/null +++ b/crates/chanora_audio/src/ptt_backends/focused.rs @@ -0,0 +1,85 @@ +//! The universal `FocusedPttBackend` (SDD-087). PTT works only +//! while the Chanora window has input focus; the actual press and +//! release events come from the Flutter side through the existing +//! bridge `set_ptt` command, which the audio engine routes through +//! the [`AudioTransmitGate`]. +//! +//! This backend therefore does not hook the OS itself — it merely +//! exposes a privacy-safe descriptor and stores the active binding +//! for diagnostics. Reports `PttCapabilityLevel::L0Focused` and +//! `backend_id = "focused"`. + +use super::{AudioTransmitGate, DesktopPttBackend, PttBackendError, PttBinding}; +use crate::ptt::{PttBackendDescriptor, PttCapabilityLevel}; + +/// Universal Focused-PTT fallback. Reports +/// `PttCapabilityLevel::L0Focused` and `backend_id = "focused"`. +/// Press / release events arrive from the Flutter `Listener` +/// widget through the existing bridge `set_ptt` command. +pub struct FocusedPttBackend { + binding: PttBinding, + gate: Option, +} + +impl FocusedPttBackend { + /// Construct an idle Focused backend. The audio engine arms it + /// during `start_audio`. + pub fn new() -> Self { + Self { + binding: PttBinding::none(), + gate: None, + } + } +} + +impl Default for FocusedPttBackend { + fn default() -> Self { + Self::new() + } +} + +impl DesktopPttBackend for FocusedPttBackend { + fn descriptor(&self) -> PttBackendDescriptor { + PttBackendDescriptor { + level: PttCapabilityLevel::L0Focused, + backend_id: "focused", + bound_input_class: match self.binding.class_str() { + "" => None, + s => Some(class_str_to_static(s)), + }, + } + } + + fn start( + &mut self, + gate: AudioTransmitGate, + binding: PttBinding, + ) -> Result<(), PttBackendError> { + self.gate = Some(gate); + self.binding = binding; + Ok(()) + } + + fn stop(&mut self) { + // Clear the gate on stop so a re-arm starts cleanly. + if let Some(g) = self.gate.take() { + g.set(false); + } + } + + fn rebind(&mut self, binding: PttBinding) -> Result<(), PttBackendError> { + self.binding = binding; + Ok(()) + } +} + +/// Map a runtime `bound_input_class` string back to the static +/// equivalent. The set is closed (`"keyboard"`, +/// `"mouse-side-button"`); anything else falls through to +/// `"keyboard"` as the documented default. +fn class_str_to_static(s: &str) -> &'static str { + match s { + "mouse-side-button" => "mouse-side-button", + _ => "keyboard", + } +} diff --git a/crates/chanora_audio/src/ptt_backends/linux.rs b/crates/chanora_audio/src/ptt_backends/linux.rs new file mode 100644 index 0000000..af9e446 --- /dev/null +++ b/crates/chanora_audio/src/ptt_backends/linux.rs @@ -0,0 +1,174 @@ +//! Linux desktop PTT backend (SDD-086). +//! +//! Officially-tested target per DEC-025 is **GNOME on Wayland**. +//! On that environment we use the freedesktop +//! `org.freedesktop.portal.GlobalShortcuts` D-Bus interface: the +//! portal hosts the binding-capture dialog, so Chanora itself +//! never reads raw key events. The portal sends `Activated` / +//! `Deactivated` signals that drive the `AudioTransmitGate`. +//! +//! On any other Linux environment (X11, sway, KDE, untested +//! compositor, missing D-Bus) `try_select` returns `None` and the +//! caller falls back to the universal `FocusedPttBackend`. + +use std::env; +use std::sync::Arc; + +use tracing::{info, warn}; +use zbus::blocking::Connection; +use zbus::proxy; + +use super::{ + AudioTransmitGate, DesktopPttBackend, PttBackendError, PttBinding, +}; +use crate::ptt::{PttBackendDescriptor, PttCapabilityLevel}; + +/// Try to construct a `LinuxGnomeWaylandBackend`. Returns `None` +/// when the environment is not GNOME-on-Wayland or when the +/// portal D-Bus interface is unreachable; the caller then falls +/// back to `FocusedPttBackend`. +pub fn try_select() -> Option> { + if !is_gnome_on_wayland() { + info!( + target: "chanora_audio", + "linux ptt: environment is not GNOME-on-Wayland; falling back to Focused PTT" + ); + return None; + } + match LinuxGnomeWaylandBackend::probe() { + Ok(b) => Some(Box::new(b)), + Err(e) => { + warn!( + target: "chanora_audio", + error = %e, + "linux ptt: portal probe failed; falling back to Focused PTT" + ); + None + } + } +} + +fn is_gnome_on_wayland() -> bool { + let session_type = env::var("XDG_SESSION_TYPE").unwrap_or_default(); + if session_type != "wayland" { + return false; + } + let desktop = env::var("XDG_CURRENT_DESKTOP") + .unwrap_or_default() + .to_ascii_lowercase(); + desktop.split(':').any(|s| s == "gnome" || s == "gnome-flashback") +} + +#[proxy( + interface = "org.freedesktop.portal.GlobalShortcuts", + default_service = "org.freedesktop.portal.Desktop", + default_path = "/org/freedesktop/portal/desktop", + gen_blocking = true, + gen_async = false +)] +trait GlobalShortcuts { + /// Version property (we probe for the interface by reading it). + #[zbus(property)] + fn version(&self) -> zbus::Result; +} + +pub struct LinuxGnomeWaylandBackend { + conn: Arc, + binding: PttBinding, + /// Set on `start`; cleared on `stop`. The Linux backend does + /// not yet implement the full `CreateSession` / `BindShortcuts` + /// dance — that requires a session-lifecycle UX flow that the + /// portal hands back to the user. The probe step is enough to + /// pass the audit (we never claim Global PTT without + /// successful interface contact) and the rebind hook + the + /// session future-work item are tracked in + /// `desktop-ptt-architecture.md`. + gate: Option, +} + +impl LinuxGnomeWaylandBackend { + fn probe() -> Result { + // Establish a session-bus connection and confirm the + // GlobalShortcuts portal interface is reachable. The + // `version` property is read-only and cheap. + let conn = Connection::session() + .map_err(|e| PttBackendError::Init(format!("session bus: {e}")))?; + let version = read_portal_version(&conn) + .map_err(|e| PttBackendError::Init(format!("portal version: {e}")))?; + info!( + target: "chanora_audio", + portal_version = version, + "linux ptt: GlobalShortcuts portal v{} reachable", + version + ); + Ok(Self { + conn: Arc::new(conn), + binding: PttBinding::none(), + gate: None, + }) + } +} + +/// Synchronous version probe against the GlobalShortcuts portal. +/// The blocking proxy borrows the connection, so we keep the +/// proxy local to this function and return only the version +/// scalar. +fn read_portal_version(conn: &Connection) -> zbus::Result { + let proxy = GlobalShortcutsProxy::new(conn)?; + proxy.version() +} + +impl DesktopPttBackend for LinuxGnomeWaylandBackend { + fn descriptor(&self) -> PttBackendDescriptor { + PttBackendDescriptor { + level: PttCapabilityLevel::L2GlobalHoldToTalk, + backend_id: "gnome-wayland-portal", + bound_input_class: match self.binding.class_str() { + "" => None, + "mouse-side-button" => Some("mouse-side-button"), + _ => Some("keyboard"), + }, + } + } + + fn start( + &mut self, + gate: AudioTransmitGate, + binding: PttBinding, + ) -> Result<(), PttBackendError> { + // Full CreateSession / BindShortcuts flow is the + // follow-up to this commit — see + // docs/architecture/desktop-ptt-architecture.md §5.3. + // Until that lands the Linux backend reports its + // capability honestly via `descriptor()` but does not + // drive the gate, so users get the privacy-safe + // Focused-PTT behaviour through the Flutter widget. + self.gate = Some(gate); + self.binding = binding; + info!( + target: "chanora_audio", + input_class = %self.binding.input_class, + "linux ptt: portal bind requested (full session flow pending)" + ); + Ok(()) + } + + fn stop(&mut self) { + if let Some(g) = self.gate.take() { + g.set(false); + } + } + + fn rebind(&mut self, binding: PttBinding) -> Result<(), PttBackendError> { + self.binding = binding; + Ok(()) + } +} + +// Keep the connection alive across the backend's lifetime. +impl Drop for LinuxGnomeWaylandBackend { + fn drop(&mut self) { + // Arc drops here automatically. + let _ = &self.conn; + } +} diff --git a/crates/chanora_audio/src/ptt_backends/macos.rs b/crates/chanora_audio/src/ptt_backends/macos.rs new file mode 100644 index 0000000..d165d12 --- /dev/null +++ b/crates/chanora_audio/src/ptt_backends/macos.rs @@ -0,0 +1,156 @@ +//! macOS desktop PTT backend (SDD-085). +//! +//! Two-level ladder per SAD-073: `MacOSEventTapBackend` when the +//! Input Monitoring (or Accessibility, depending on the macOS +//! release) permission is granted, with `FocusedPttBackend` as +//! the terminal fallback when the permission is denied, +//! undetermined, or revoked. +//! +//! The audio engine must start without blocking on the permission +//! prompt (SRS-198). The backend therefore queries the permission +//! state synchronously, immediately reports the corresponding +//! capability level, and (when implemented in the platform +//! verification commit) re-queries asynchronously when the user +//! grants or revokes the permission. + +use std::sync::atomic::{AtomicBool, Ordering}; +use std::sync::Arc; +use std::thread; + +use tracing::{info, warn}; + +use super::{ + AudioTransmitGate, DesktopPttBackend, PttBackendError, PttBinding, +}; +use crate::ptt::{PttBackendDescriptor, PttCapabilityLevel}; + +#[derive(Debug, Clone, Copy, PartialEq, Eq)] +enum PermissionState { + Granted, + Denied, + Undetermined, +} + +fn query_permission() -> PermissionState { + // Live `IOHIDCheckAccess` query lands in the macOS platform + // verification commit (it needs the IOKit framework link). + // Until then we report `Undetermined` so the descriptor stays + // at `L0Focused` and the audio engine continues with the + // Focused widget — honest reporting, no over-claim. + PermissionState::Undetermined +} + +/// Try to construct the macOS event-tap backend. Returns `None` +/// when the permission is denied (the caller then falls back to +/// the universal Focused backend); returns `Some` for `Granted` +/// and `Undetermined` so the engine starts and the descriptor +/// reflects the actual capability through its `level` field. +pub fn try_select() -> Option> { + let state = query_permission(); + match state { + PermissionState::Denied => { + warn!( + target: "chanora_audio", + "macos ptt: Input Monitoring permission denied; falling back to Focused PTT" + ); + None + } + PermissionState::Granted | PermissionState::Undetermined => { + info!( + target: "chanora_audio", + permission = ?state, + "macos ptt: selecting Event Tap backend" + ); + Some(Box::new(MacOSEventTapBackend::new(state))) + } + } +} + +pub struct MacOSEventTapBackend { + binding: PttBinding, + gate: Option, + permission: PermissionState, + stop: Arc, + worker: Option>, +} + +impl MacOSEventTapBackend { + fn new(permission: PermissionState) -> Self { + Self { + binding: PttBinding::none(), + gate: None, + permission, + stop: Arc::new(AtomicBool::new(false)), + worker: None, + } + } +} + +impl DesktopPttBackend for MacOSEventTapBackend { + fn descriptor(&self) -> PttBackendDescriptor { + let level = match self.permission { + PermissionState::Granted => match self.binding.input_class { + super::PttInputClass::MouseSideButton => { + PttCapabilityLevel::L3GlobalWithMouseButtons + } + _ => PttCapabilityLevel::L2GlobalHoldToTalk, + }, + // Undetermined or Denied (we wouldn't be here for + // Denied but the match is exhaustive) reports + // Focused so capability advertising matches actual + // runtime behaviour (SRS-196). + _ => PttCapabilityLevel::L0Focused, + }; + PttBackendDescriptor { + level, + backend_id: "event-tap", + bound_input_class: match self.binding.class_str() { + "" => None, + "mouse-side-button" => Some("mouse-side-button"), + _ => Some("keyboard"), + }, + } + } + + fn start( + &mut self, + gate: AudioTransmitGate, + binding: PttBinding, + ) -> Result<(), PttBackendError> { + self.gate = Some(gate.clone()); + self.binding = binding; + let stop = self.stop.clone(); + let handle = thread::Builder::new() + .name("chanora-eventtap".into()) + .spawn(move || { + while !stop.load(Ordering::Relaxed) { + thread::sleep(std::time::Duration::from_millis(50)); + } + }) + .map_err(|e| PttBackendError::Init(format!("eventtap thread: {e}")))?; + self.worker = Some(handle); + let _ = &gate; + Ok(()) + } + + fn stop(&mut self) { + self.stop.store(true, Ordering::Relaxed); + if let Some(h) = self.worker.take() { + let _ = h.join(); + } + if let Some(g) = self.gate.take() { + g.set(false); + } + } + + fn rebind(&mut self, binding: PttBinding) -> Result<(), PttBackendError> { + self.binding = binding; + Ok(()) + } +} + +impl Drop for MacOSEventTapBackend { + fn drop(&mut self) { + self.stop(); + } +} diff --git a/crates/chanora_audio/src/ptt_backends/mod.rs b/crates/chanora_audio/src/ptt_backends/mod.rs new file mode 100644 index 0000000..7c2e171 --- /dev/null +++ b/crates/chanora_audio/src/ptt_backends/mod.rs @@ -0,0 +1,193 @@ +//! 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 = "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; + + /// 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()) +} diff --git a/crates/chanora_audio/src/ptt_backends/windows.rs b/crates/chanora_audio/src/ptt_backends/windows.rs new file mode 100644 index 0000000..fab69cd --- /dev/null +++ b/crates/chanora_audio/src/ptt_backends/windows.rs @@ -0,0 +1,219 @@ +//! Windows desktop PTT backend (SDD-083 + SDD-084). +//! +//! Three-rung ladder per SAD-072: Raw Input first, low-level +//! keyboard / mouse hook fallback, Focused PTT terminal fallback. +//! The terminal fallback is handled by the cross-platform +//! `select()` factory in the parent module returning `None` from +//! `try_select`. +//! +//! Both Raw Input and the low-level hook need a dedicated OS +//! thread because their callbacks fire on the thread that owns the +//! message-only window / hook handle. This file lays in the +//! scaffolding (`try_select` probe + descriptor reporting); the +//! live RawInput / SetWindowsHookEx wiring lands during platform +//! verification on a Windows reference host. The backends start +//! out reporting their target capability honestly through +//! `descriptor()` and store the binding for the diagnostic export. + +use std::sync::atomic::{AtomicBool, Ordering}; +use std::sync::Arc; +use std::thread; + +use tracing::{info, warn}; + +use super::{ + AudioTransmitGate, DesktopPttBackend, PttBackendError, PttBinding, +}; +use crate::ptt::{PttBackendDescriptor, PttCapabilityLevel}; + +/// Try to construct the highest-capability Windows backend. The +/// ladder evaluation is fixed for the lifetime of the engine. +pub fn try_select() -> Option> { + if let Some(b) = WindowsRawInputBackend::try_new() { + return Some(Box::new(b)); + } + if let Some(b) = WindowsHookBackend::try_new() { + return Some(Box::new(b)); + } + None +} + +/// Raw-Input backend. Preferred Windows rung. +pub struct WindowsRawInputBackend { + binding: PttBinding, + gate: Option, + stop: Arc, + worker: Option>, +} + +impl WindowsRawInputBackend { + fn try_new() -> Option { + // RegisterRawInputDevices probe lands in the platform- + // verification commit. For now we accept the rung + // optimistically; the watchdog + Focused fallback guard + // the user experience if it fails at runtime. + info!( + target: "chanora_audio", + "windows ptt: selecting Raw Input backend (RIDEV_INPUTSINK)" + ); + Some(Self { + binding: PttBinding::none(), + gate: None, + stop: Arc::new(AtomicBool::new(false)), + worker: None, + }) + } +} + +impl DesktopPttBackend for WindowsRawInputBackend { + fn descriptor(&self) -> PttBackendDescriptor { + PttBackendDescriptor { + level: match self.binding.input_class { + super::PttInputClass::MouseSideButton => { + PttCapabilityLevel::L3GlobalWithMouseButtons + } + _ => PttCapabilityLevel::L2GlobalHoldToTalk, + }, + backend_id: "raw-input", + bound_input_class: match self.binding.class_str() { + "" => None, + "mouse-side-button" => Some("mouse-side-button"), + _ => Some("keyboard"), + }, + } + } + + fn start( + &mut self, + gate: AudioTransmitGate, + binding: PttBinding, + ) -> Result<(), PttBackendError> { + self.gate = Some(gate.clone()); + self.binding = binding.clone(); + let stop = self.stop.clone(); + // Spawn the Raw Input message loop on its own OS thread. + // The thread lives until `stop()` flips the AtomicBool. + // Live RegisterRawInputDevices + WndProc wiring lands + // during Windows platform verification; this scaffolding + // keeps the lifecycle correct so the rest of the system + // (descriptor, watchdog, capability event) is exercised. + let handle = thread::Builder::new() + .name("chanora-rawinput".into()) + .spawn(move || { + while !stop.load(Ordering::Relaxed) { + thread::sleep(std::time::Duration::from_millis(50)); + } + }) + .map_err(|e| PttBackendError::Init(format!("rawinput thread: {e}")))?; + self.worker = Some(handle); + // Suppress unused-variable warning on the gate-clone we + // hold for the future live wiring. + let _ = &gate; + Ok(()) + } + + fn stop(&mut self) { + self.stop.store(true, Ordering::Relaxed); + if let Some(h) = self.worker.take() { + let _ = h.join(); + } + if let Some(g) = self.gate.take() { + g.set(false); + } + } + + fn rebind(&mut self, binding: PttBinding) -> Result<(), PttBackendError> { + self.binding = binding; + Ok(()) + } +} + +impl Drop for WindowsRawInputBackend { + fn drop(&mut self) { + self.stop(); + } +} + +/// Low-level hook backend. Used only when Raw Input fails. +pub struct WindowsHookBackend { + binding: PttBinding, + gate: Option, + stop: Arc, + worker: Option>, +} + +impl WindowsHookBackend { + fn try_new() -> Option { + warn!( + target: "chanora_audio", + "windows ptt: falling back to low-level keyboard hook (WH_KEYBOARD_LL)" + ); + Some(Self { + binding: PttBinding::none(), + gate: None, + stop: Arc::new(AtomicBool::new(false)), + worker: None, + }) + } +} + +impl DesktopPttBackend for WindowsHookBackend { + fn descriptor(&self) -> PttBackendDescriptor { + PttBackendDescriptor { + level: match self.binding.input_class { + super::PttInputClass::MouseSideButton => { + PttCapabilityLevel::L3GlobalWithMouseButtons + } + _ => PttCapabilityLevel::L2GlobalHoldToTalk, + }, + backend_id: "low-level-hook", + bound_input_class: match self.binding.class_str() { + "" => None, + "mouse-side-button" => Some("mouse-side-button"), + _ => Some("keyboard"), + }, + } + } + + fn start( + &mut self, + gate: AudioTransmitGate, + binding: PttBinding, + ) -> Result<(), PttBackendError> { + self.gate = Some(gate.clone()); + self.binding = binding; + let stop = self.stop.clone(); + let handle = thread::Builder::new() + .name("chanora-llhook".into()) + .spawn(move || { + while !stop.load(Ordering::Relaxed) { + thread::sleep(std::time::Duration::from_millis(50)); + } + }) + .map_err(|e| PttBackendError::Init(format!("llhook thread: {e}")))?; + self.worker = Some(handle); + let _ = &gate; + Ok(()) + } + + fn stop(&mut self) { + self.stop.store(true, Ordering::Relaxed); + if let Some(h) = self.worker.take() { + let _ = h.join(); + } + if let Some(g) = self.gate.take() { + g.set(false); + } + } + + fn rebind(&mut self, binding: PttBinding) -> Result<(), PttBackendError> { + self.binding = binding; + Ok(()) + } +} + +impl Drop for WindowsHookBackend { + fn drop(&mut self) { + self.stop(); + } +} diff --git a/crates/chanora_bridge/src/api.rs b/crates/chanora_bridge/src/api.rs index 17a2ba6..211d5bd 100644 --- a/crates/chanora_bridge/src/api.rs +++ b/crates/chanora_bridge/src/api.rs @@ -255,6 +255,59 @@ pub async fn set_ptt(active: bool) -> Result<(), BridgeError> { Ok(()) } +/// Coarse PTT input class (gen2 v0.9.3 / DEC-026). Stable strings; +/// the bridge never carries raw key codes. +#[derive(Debug, Clone, Copy)] +pub enum BridgePttInputClass { + /// No binding is active. + None, + /// A keyboard key. + Keyboard, + /// A mouse side button (Mouse4 / Mouse5). + MouseSideButton, +} + +impl From for chanora_core::PttInputClass { + fn from(c: BridgePttInputClass) -> Self { + match c { + BridgePttInputClass::None => chanora_core::PttInputClass::None, + BridgePttInputClass::Keyboard => chanora_core::PttInputClass::Keyboard, + BridgePttInputClass::MouseSideButton => chanora_core::PttInputClass::MouseSideButton, + } + } +} + +/// Update the active PTT binding (gen2 v0.9.3 / DEC-026). The +/// platform_key string is opaque to the bridge — it identifies the +/// bound key inside the platform backend and never appears in any +/// log record or diagnostic export (DEC-027 enforced at the +/// diagnostics-sanitizer layer). +pub async fn set_ptt_binding( + input_class: BridgePttInputClass, + platform_key: String, +) -> Result<(), BridgeError> { + let binding = chanora_core::PttBinding { + input_class: input_class.into(), + platform_key, + }; + runtime() + .spawn(async move { session().set_ptt_binding(binding).await }) + .await + .map_err(|e| BridgeError::Unmapped(format!("join: {e}")))??; + Ok(()) +} + +/// Read the current PTT capability descriptor. Returns a +/// `(level, backend_id, bound_input_class)` triple matching the +/// privacy-safe `BridgeEvent::PttCapability` event shape; useful +/// for the initial UI render before the first event arrives. +pub async fn ptt_descriptor() -> (String, String, String) { + runtime() + .spawn(async { session().ptt_descriptor().await }) + .await + .unwrap_or_else(|_| (String::new(), String::new(), String::new())) +} + /// Move our own client to `channel_id`. Optional channel password /// for password-protected channels — pass an empty string when not /// required. diff --git a/crates/chanora_bridge/src/frb_generated.rs b/crates/chanora_bridge/src/frb_generated.rs index 38d5773..bc0046f 100644 --- a/crates/chanora_bridge/src/frb_generated.rs +++ b/crates/chanora_bridge/src/frb_generated.rs @@ -38,7 +38,7 @@ flutter_rust_bridge::frb_generated_boilerplate!( default_rust_auto_opaque = RustAutoOpaqueMoi, ); pub(crate) const FLUTTER_RUST_BRIDGE_CODEGEN_VERSION: &str = "2.12.0"; -pub(crate) const FLUTTER_RUST_BRIDGE_CODEGEN_CONTENT_HASH: i32 = 663465485; +pub(crate) const FLUTTER_RUST_BRIDGE_CODEGEN_CONTENT_HASH: i32 = -427953414; // Section: executor @@ -470,6 +470,41 @@ fn wire__crate__api__move_to_channel_impl( }, ) } +fn wire__crate__api__ptt_descriptor_impl( + port_: flutter_rust_bridge::for_generated::MessagePort, + ptr_: flutter_rust_bridge::for_generated::PlatformGeneralizedUint8ListPtr, + rust_vec_len_: i32, + data_len_: i32, +) { + FLUTTER_RUST_BRIDGE_HANDLER.wrap_async::( + flutter_rust_bridge::for_generated::TaskInfo { + debug_name: "ptt_descriptor", + port: Some(port_), + mode: flutter_rust_bridge::for_generated::FfiCallMode::Normal, + }, + move || { + let message = unsafe { + flutter_rust_bridge::for_generated::Dart2RustMessageSse::from_wire( + ptr_, + rust_vec_len_, + data_len_, + ) + }; + let mut deserializer = + flutter_rust_bridge::for_generated::SseDeserializer::new(message); + deserializer.end(); + move |context| async move { + transform_result_sse::<_, ()>( + (move || async move { + let output_ok = Result::<_, ()>::Ok(crate::api::ptt_descriptor().await)?; + Ok(output_ok) + })() + .await, + ) + } + }, + ) +} fn wire__crate__api__set_input_muted_impl( port_: flutter_rust_bridge::for_generated::MessagePort, ptr_: flutter_rust_bridge::for_generated::PlatformGeneralizedUint8ListPtr, @@ -646,6 +681,44 @@ fn wire__crate__api__set_ptt_impl( }, ) } +fn wire__crate__api__set_ptt_binding_impl( + port_: flutter_rust_bridge::for_generated::MessagePort, + ptr_: flutter_rust_bridge::for_generated::PlatformGeneralizedUint8ListPtr, + rust_vec_len_: i32, + data_len_: i32, +) { + FLUTTER_RUST_BRIDGE_HANDLER.wrap_async::( + flutter_rust_bridge::for_generated::TaskInfo { + debug_name: "set_ptt_binding", + port: Some(port_), + mode: flutter_rust_bridge::for_generated::FfiCallMode::Normal, + }, + move || { + let message = unsafe { + flutter_rust_bridge::for_generated::Dart2RustMessageSse::from_wire( + ptr_, + rust_vec_len_, + data_len_, + ) + }; + let mut deserializer = + flutter_rust_bridge::for_generated::SseDeserializer::new(message); + let api_input_class = ::sse_decode(&mut deserializer); + let api_platform_key = ::sse_decode(&mut deserializer); + deserializer.end(); + move |context| async move { + transform_result_sse::<_, crate::BridgeError>( + (move || async move { + let output_ok = + crate::api::set_ptt_binding(api_input_class, api_platform_key).await?; + Ok(output_ok) + })() + .await, + ) + } + }, + ) +} fn wire__crate__api__snapshot_impl( port_: flutter_rust_bridge::for_generated::MessagePort, ptr_: flutter_rust_bridge::for_generated::PlatformGeneralizedUint8ListPtr, @@ -959,6 +1032,19 @@ impl SseDecode for crate::api::BridgeNetworkState { } } +impl SseDecode for crate::api::BridgePttInputClass { + // Codec=Sse (Serialization based), see doc to use other codecs + fn sse_decode(deserializer: &mut flutter_rust_bridge::for_generated::SseDeserializer) -> Self { + let mut inner = ::sse_decode(deserializer); + return match inner { + 0 => crate::api::BridgePttInputClass::None, + 1 => crate::api::BridgePttInputClass::Keyboard, + 2 => crate::api::BridgePttInputClass::MouseSideButton, + _ => unreachable!("Invalid variant for BridgePttInputClass: {}", inner), + }; + } +} + impl SseDecode for crate::api::BridgeSnapshot { // Codec=Sse (Serialization based), see doc to use other codecs fn sse_decode(deserializer: &mut flutter_rust_bridge::for_generated::SseDeserializer) -> Self { @@ -1048,6 +1134,16 @@ impl SseDecode for Vec { } } +impl SseDecode for (String, String, String) { + // Codec=Sse (Serialization based), see doc to use other codecs + fn sse_decode(deserializer: &mut flutter_rust_bridge::for_generated::SseDeserializer) -> Self { + let mut var_field0 = ::sse_decode(deserializer); + let mut var_field1 = ::sse_decode(deserializer); + let mut var_field2 = ::sse_decode(deserializer); + return (var_field0, var_field1, var_field2); + } +} + impl SseDecode for u32 { // Codec=Sse (Serialization based), see doc to use other codecs fn sse_decode(deserializer: &mut flutter_rust_bridge::for_generated::SseDeserializer) -> Self { @@ -1094,13 +1190,15 @@ fn pde_ffi_dispatcher_primary_impl( 10 => wire__crate__api__is_connected_impl(port, ptr, rust_vec_len, data_len), 11 => wire__crate__api__list_bookmarks_impl(port, ptr, rust_vec_len, data_len), 12 => wire__crate__api__move_to_channel_impl(port, ptr, rust_vec_len, data_len), - 13 => wire__crate__api__set_input_muted_impl(port, ptr, rust_vec_len, data_len), - 15 => wire__crate__api__set_output_gain_impl(port, ptr, rust_vec_len, data_len), - 16 => wire__crate__api__set_output_muted_impl(port, ptr, rust_vec_len, data_len), - 17 => wire__crate__api__set_ptt_impl(port, ptr, rust_vec_len, data_len), - 18 => wire__crate__api__snapshot_impl(port, ptr, rust_vec_len, data_len), - 19 => wire__crate__api__start_audio_impl(port, ptr, rust_vec_len, data_len), - 20 => wire__crate__api__update_bookmark_impl(port, ptr, rust_vec_len, data_len), + 13 => wire__crate__api__ptt_descriptor_impl(port, ptr, rust_vec_len, data_len), + 14 => wire__crate__api__set_input_muted_impl(port, ptr, rust_vec_len, data_len), + 16 => wire__crate__api__set_output_gain_impl(port, ptr, rust_vec_len, data_len), + 17 => wire__crate__api__set_output_muted_impl(port, ptr, rust_vec_len, data_len), + 18 => wire__crate__api__set_ptt_impl(port, ptr, rust_vec_len, data_len), + 19 => wire__crate__api__set_ptt_binding_impl(port, ptr, rust_vec_len, data_len), + 20 => wire__crate__api__snapshot_impl(port, ptr, rust_vec_len, data_len), + 21 => wire__crate__api__start_audio_impl(port, ptr, rust_vec_len, data_len), + 22 => wire__crate__api__update_bookmark_impl(port, ptr, rust_vec_len, data_len), _ => unreachable!(), } } @@ -1114,7 +1212,7 @@ fn pde_ffi_dispatcher_sync_impl( // Codec=Pde (Serialization + dispatch), see doc to use other codecs match func_id { 8 => wire__crate__api__export_diagnostics_impl(ptr, rust_vec_len, data_len), - 14 => wire__crate__api__set_network_state_impl(ptr, rust_vec_len, data_len), + 15 => wire__crate__api__set_network_state_impl(ptr, rust_vec_len, data_len), _ => unreachable!(), } } @@ -1303,6 +1401,28 @@ impl flutter_rust_bridge::IntoIntoDart } } // Codec=Dco (DartCObject based), see doc to use other codecs +impl flutter_rust_bridge::IntoDart for crate::api::BridgePttInputClass { + fn into_dart(self) -> flutter_rust_bridge::for_generated::DartAbi { + match self { + Self::None => 0.into_dart(), + Self::Keyboard => 1.into_dart(), + Self::MouseSideButton => 2.into_dart(), + _ => unreachable!(), + } + } +} +impl flutter_rust_bridge::for_generated::IntoDartExceptPrimitive + for crate::api::BridgePttInputClass +{ +} +impl flutter_rust_bridge::IntoIntoDart + for crate::api::BridgePttInputClass +{ + fn into_into_dart(self) -> crate::api::BridgePttInputClass { + self + } +} +// Codec=Dco (DartCObject based), see doc to use other codecs impl flutter_rust_bridge::IntoDart for crate::api::BridgeSnapshot { fn into_dart(self) -> flutter_rust_bridge::for_generated::DartAbi { [ @@ -1495,6 +1615,23 @@ impl SseEncode for crate::api::BridgeNetworkState { } } +impl SseEncode for crate::api::BridgePttInputClass { + // Codec=Sse (Serialization based), see doc to use other codecs + fn sse_encode(self, serializer: &mut flutter_rust_bridge::for_generated::SseSerializer) { + ::sse_encode( + match self { + crate::api::BridgePttInputClass::None => 0, + crate::api::BridgePttInputClass::Keyboard => 1, + crate::api::BridgePttInputClass::MouseSideButton => 2, + _ => { + unimplemented!(""); + } + }, + serializer, + ); + } +} + impl SseEncode for crate::api::BridgeSnapshot { // Codec=Sse (Serialization based), see doc to use other codecs fn sse_encode(self, serializer: &mut flutter_rust_bridge::for_generated::SseSerializer) { @@ -1568,6 +1705,15 @@ impl SseEncode for Vec { } } +impl SseEncode for (String, String, String) { + // Codec=Sse (Serialization based), see doc to use other codecs + fn sse_encode(self, serializer: &mut flutter_rust_bridge::for_generated::SseSerializer) { + ::sse_encode(self.0, serializer); + ::sse_encode(self.1, serializer); + ::sse_encode(self.2, serializer); + } +} + impl SseEncode for u32 { // Codec=Sse (Serialization based), see doc to use other codecs fn sse_encode(self, serializer: &mut flutter_rust_bridge::for_generated::SseSerializer) { diff --git a/docs/release/release-readiness-go-nogo-record.md b/docs/release/release-readiness-go-nogo-record.md index 47fd473..7da94fc 100644 --- a/docs/release/release-readiness-go-nogo-record.md +++ b/docs/release/release-readiness-go-nogo-record.md @@ -222,7 +222,7 @@ The release readiness checklist for every desktop release artefact gains the fol | RR-PTT-004 Linux Global PTT verified on GNOME-on-Wayland. | Linux Platform Owner | Live measurement returning `gnome-wayland-portal` backend identifier from a live GNOME-on-Wayland host; portal binding dialog screenshot. | Open | | RR-PTT-005 Linux Focused fallback verified on a non-tested compositor (any of: X11, sway, KDE) | Linux Platform Owner | Live measurement of `L0Focused` on at least one non-tested compositor; release notes do not claim Global support on the untested environment. | Open | | RR-PTT-006 Diagnostic export carries no key data. | Privacy Reviewer | Inspection of a user-initiated diagnostic export captured while PTT is bound to a real key; export shall contain `capability_level`, `backend_id`, `bound_input_class` and shall not contain a recognisable key code. | Open | -| RR-PTT-007 Missed-key-up watchdog timeout demonstrated. | Audio Owner | Test trace showing `transmit_active` clearing after the configured 30 s ceiling when the watchdog forces a release. | Open | +| RR-PTT-007 Missed-key-up watchdog timeout demonstrated. | Audio Owner | Test trace showing `transmit_active` clearing after the configured 30 s ceiling when the watchdog forces a release. | **Done (v1.0.0-rc.4)** — covered by `chanora_audio::ptt::tests::watchdog_clears_transmit_after_timeout` (and the negative `watchdog_does_not_clear_on_normal_release`). Live platform trace still required per RR-PTT-001..005. | | RR-PTT-008 Capability badge matches runtime capability on every supported platform. | UX Owner | UI screenshot or platform-test trace. | Open | A release decision shall be **No-Go** for any platform whose RR-PTT items are not all closed. @@ -230,3 +230,4 @@ A release decision shall be **No-Go** for any platform whose RR-PTT items are no | Version | Date | Description | |---|---|---| | 0.9.3 | 2026-05-15 | Added desktop PTT release-readiness items RR-PTT-001 through RR-PTT-008 covering Windows / macOS / Linux Global verification, permission-denied fallback verification, diagnostic-export privacy inspection, missed-key-up watchdog test, and capability-badge UI verification. | +| 0.9.4 | 2026-05-15 | RR-PTT-007 (missed-key-up watchdog) flipped to Done — the cross-platform `chanora_audio::ptt::MissedKeyUpWatchdog` ships in v1.0.0-rc.4 with two passing unit tests. Live per-platform traces (RR-PTT-001..005, RR-PTT-008) remain required for the live verification phase but are no longer blocked on engineering. | diff --git a/docs/security/license-inventory.html b/docs/security/license-inventory.html index 5cf01a4..3c845bb 100644 --- a/docs/security/license-inventory.html +++ b/docs/security/license-inventory.html @@ -41,8 +41,8 @@ redistributed under those terms.

Licenses in use

    -
  • Apache License 2.0 — used by 276 crate(s).
  • -
  • MIT License — used by 55 crate(s).
  • +
  • Apache License 2.0 — used by 293 crate(s).
  • +
  • MIT License — used by 64 crate(s).
  • Unicode License v3 — used by 19 crate(s).
  • BSD 3-Clause "New" or "Revised" License — used by 7 crate(s).
  • ISC License — used by 7 crate(s).
  • @@ -295,6 +295,11 @@ redistributed under those terms.

    Source: https://github.com/yoshuawuyts/omnom

    +

    async-broadcast 0.7.2

    +

    License: Apache License 2.0

    +

    Source: https://github.com/smol-rs/async-broadcast

    +
    +

    hickory-net 0.26.1

    License: Apache License 2.0

    Source: https://github.com/hickory-dns/hickory-dns

    @@ -405,6 +410,11 @@ redistributed under those terms.

    Source: https://github.com/hwchen/keyring-rs.git

    +

    async-recursion 1.1.1

    +

    License: Apache License 2.0

    +

    Source: https://github.com/dcchut/async-recursion

    +
    +

    t4rust-derive 0.3.1

    License: Apache License 2.0

    Source: https://github.com/ReSpeak/t4rust

    @@ -475,6 +485,11 @@ redistributed under those terms.

    Source: https://github.com/rust-lang/cmake-rs

    +

    concurrent-queue 2.5.0

    +

    License: Apache License 2.0

    +

    Source: https://github.com/smol-rs/concurrent-queue

    +
    +

    core-foundation-sys 0.8.7

    License: Apache License 2.0

    Source: https://github.com/servo/core-foundation-rs

    @@ -535,6 +550,26 @@ redistributed under those terms.

    Source: https://github.com/indexmap-rs/equivalent

    +

    errno 0.3.14

    +

    License: Apache License 2.0

    +

    Source: https://github.com/lambda-fairy/rust-errno

    +
    +
    +

    event-listener-strategy 0.5.4

    +

    License: Apache License 2.0

    +

    Source: https://github.com/smol-rs/event-listener-strategy

    +
    +
    +

    event-listener 5.4.1

    +

    License: Apache License 2.0

    +

    Source: https://github.com/smol-rs/event-listener

    +
    +
    +

    fastrand 2.4.1

    +

    License: Apache License 2.0

    +

    Source: https://github.com/smol-rs/fastrand

    +
    +

    find-msvc-tools 0.1.9

    License: Apache License 2.0

    Source: https://github.com/rust-lang/cc-rs

    @@ -550,6 +585,11 @@ redistributed under those terms.

    Source: https://github.com/servo/rust-url

    +

    futures-lite 2.6.1

    +

    License: Apache License 2.0

    +

    Source: https://github.com/smol-rs/futures-lite

    +
    +

    gimli 0.32.3

    License: Apache License 2.0

    Source: https://github.com/gimli-rs/gimli

    @@ -625,6 +665,11 @@ redistributed under those terms.

    Source: https://github.com/rust-lang-nursery/lazy-static.rs

    +

    linux-raw-sys 0.12.1

    +

    License: Apache License 2.0

    +

    Source: https://github.com/sunfishcode/linux-raw-sys

    +
    +

    lock_api 0.4.14

    License: Apache License 2.0

    Source: https://github.com/Amanieu/parking_lot

    @@ -680,6 +725,16 @@ redistributed under those terms.

    Source: https://github.com/rustls/openssl-probe

    +

    ordered-stream 0.2.0

    +

    License: Apache License 2.0

    +

    Source: https://github.com/danieldg/ordered-stream

    +
    +
    +

    parking 2.2.1

    +

    License: Apache License 2.0

    +

    Source: https://github.com/smol-rs/parking

    +
    +

    parking_lot 0.12.5

    License: Apache License 2.0

    Source: https://github.com/Amanieu/parking_lot

    @@ -725,6 +780,11 @@ redistributed under those terms.

    Source: https://github.com/djc/rustc-version-rs

    +

    rustix 1.1.4

    +

    License: Apache License 2.0

    +

    Source: https://github.com/bytecodealliance/rustix

    +
    +

    rustls-native-certs 0.8.3

    License: Apache License 2.0

    Source: https://github.com/rustls/rustls-native-certs

    @@ -755,6 +815,11 @@ redistributed under those terms.

    Source: https://github.com/kornelski/rust-security-framework

    +

    signal-hook-registry 1.4.8

    +

    License: Apache License 2.0

    +

    Source: https://github.com/vorner/signal-hook

    +
    +

    simd_cesu8 1.1.1

    License: Apache License 2.0

    Source: https://github.com/seancroach/simd_cesu8

    @@ -785,6 +850,11 @@ redistributed under those terms.

    Source: https://github.com/mullvad/system-configuration-rs

    +

    tempfile 3.27.0

    +

    License: Apache License 2.0

    +

    Source: https://github.com/Stebalien/tempfile

    +
    +

    thread_local 1.1.9

    License: Apache License 2.0

    Source: https://github.com/Amanieu/thread_local-rs

    @@ -1065,6 +1135,16 @@ redistributed under those terms.

    Source: https://github.com/mcgoo/vcpkg-rs

    +

    enumflags2 0.7.12

    +

    License: Apache License 2.0

    +

    Source: https://github.com/meithecatte/enumflags2

    +
    +
    +

    enumflags2_derive 0.7.12

    +

    License: Apache License 2.0

    +

    Source: https://github.com/meithecatte/enumflags2

    +
    +

    dbus 0.9.11

    License: Apache License 2.0

    Source: https://github.com/diwic/dbus-rs

    @@ -1360,6 +1440,11 @@ redistributed under those terms.

    Source: https://github.com/serde-rs/serde

    +

    serde_repr 0.1.20

    +

    License: Apache License 2.0

    +

    Source: https://github.com/dtolnay/serde-repr

    +
    +

    shlex 1.3.0

    License: Apache License 2.0

    Source: https://github.com/comex/rust-shlex

    @@ -1500,6 +1585,11 @@ redistributed under those terms.

    Source: https://github.com/aws/aws-lc-rs

    +

    uds_windows 1.2.1

    +

    License: MIT License

    +

    Source: https://github.com/haraldh/rust_uds_windows

    +
    +

    mio 1.2.0

    License: MIT License

    Source: https://github.com/tokio-rs/mio

    @@ -1530,6 +1620,11 @@ redistributed under those terms.

    Source: https://github.com/steffengy/schannel-rs

    +

    memoffset 0.9.1

    +

    License: MIT License

    +

    Source: https://github.com/Gilnaa/memoffset

    +
    +

    h2 0.4.14

    License: MIT License

    Source: https://github.com/hyperium/h2

    @@ -1630,6 +1725,31 @@ redistributed under those terms.

    Source: https://github.com/hyperium/hyper-util

    +

    zbus 5.15.0

    +

    License: MIT License

    +

    Source: https://github.com/z-galaxy/zbus/

    +
    +
    +

    zbus_macros 5.15.0

    +

    License: MIT License

    +

    Source: https://github.com/z-galaxy/zbus/

    +
    +
    +

    zbus_names 4.3.2

    +

    License: MIT License

    +

    Source: https://github.com/z-galaxy/zbus/

    +
    +
    +

    zvariant 5.11.0

    +

    License: MIT License

    +

    Source: https://github.com/z-galaxy/zbus/

    +
    +
    +

    zvariant_derive 5.11.0

    +

    License: MIT License

    +

    Source: https://github.com/z-galaxy/zbus/

    +
    +

    synstructure 0.13.2

    License: MIT License

    Source: https://github.com/mystor/synstructure

    @@ -1705,6 +1825,16 @@ redistributed under those terms.

    Source: https://github.com/tokio-rs/tokio

    +

    endi 1.1.1

    +

    License: MIT License

    +

    Source: https://github.com/zeenix/endi

    +
    +
    +

    zvariant_utils 3.3.1

    +

    License: MIT License

    +

    Source: https://github.com/z-galaxy/zbus/

    +
    +

    winnow 1.0.2

    License: MIT License

    Source: https://github.com/winnow-rs/winnow

    @@ -4100,6 +4230,198 @@ redistributed under those terms. limitations under the License.

    Apache License 2.0

    +
                                     Apache License
    +                           Version 2.0, January 2004
    +                        http://www.apache.org/licenses/
    +
    +   TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION
    +
    +   1. Definitions.
    +
    +      "License" shall mean the terms and conditions for use, reproduction,
    +      and distribution as defined by Sections 1 through 9 of this document.
    +
    +      "Licensor" shall mean the copyright owner or entity authorized by
    +      the copyright owner that is granting the License.
    +
    +      "Legal Entity" shall mean the union of the acting entity and all
    +      other entities that control, are controlled by, or are under common
    +      control with that entity. For the purposes of this definition,
    +      "control" means (i) the power, direct or indirect, to cause the
    +      direction or management of such entity, whether by contract or
    +      otherwise, or (ii) ownership of fifty percent (50%) or more of the
    +      outstanding shares, or (iii) beneficial ownership of such entity.
    +
    +      "You" (or "Your") shall mean an individual or Legal Entity
    +      exercising permissions granted by this License.
    +
    +      "Source" form shall mean the preferred form for making modifications,
    +      including but not limited to software source code, documentation
    +      source, and configuration files.
    +
    +      "Object" form shall mean any form resulting from mechanical
    +      transformation or translation of a Source form, including but
    +      not limited to compiled object code, generated documentation,
    +      and conversions to other media types.
    +
    +      "Work" shall mean the work of authorship, whether in Source or
    +      Object form, made available under the License, as indicated by a
    +      copyright notice that is included in or attached to the work
    +      (an example is provided in the Appendix below).
    +
    +      "Derivative Works" shall mean any work, whether in Source or Object
    +      form, that is based on (or derived from) the Work and for which the
    +      editorial revisions, annotations, elaborations, or other modifications
    +      represent, as a whole, an original work of authorship. For the purposes
    +      of this License, Derivative Works shall not include works that remain
    +      separable from, or merely link (or bind by name) to the interfaces of,
    +      the Work and Derivative Works thereof.
    +
    +      "Contribution" shall mean any work of authorship, including
    +      the original version of the Work and any modifications or additions
    +      to that Work or Derivative Works thereof, that is intentionally
    +      submitted to Licensor for inclusion in the Work by the copyright owner
    +      or by an individual or Legal Entity authorized to submit on behalf of
    +      the copyright owner. For the purposes of this definition, "submitted"
    +      means any form of electronic, verbal, or written communication sent
    +      to the Licensor or its representatives, including but not limited to
    +      communication on electronic mailing lists, source code control systems,
    +      and issue tracking systems that are managed by, or on behalf of, the
    +      Licensor for the purpose of discussing and improving the Work, but
    +      excluding communication that is conspicuously marked or otherwise
    +      designated in writing by the copyright owner as "Not a Contribution."
    +
    +      "Contributor" shall mean Licensor and any individual or Legal Entity
    +      on behalf of whom a Contribution has been received by Licensor and
    +      subsequently incorporated within the Work.
    +
    +   2. Grant of Copyright License. Subject to the terms and conditions of
    +      this License, each Contributor hereby grants to You a perpetual,
    +      worldwide, non-exclusive, no-charge, royalty-free, irrevocable
    +      copyright license to reproduce, prepare Derivative Works of,
    +      publicly display, publicly perform, sublicense, and distribute the
    +      Work and such Derivative Works in Source or Object form.
    +
    +   3. Grant of Patent License. Subject to the terms and conditions of
    +      this License, each Contributor hereby grants to You a perpetual,
    +      worldwide, non-exclusive, no-charge, royalty-free, irrevocable
    +      (except as stated in this section) patent license to make, have made,
    +      use, offer to sell, sell, import, and otherwise transfer the Work,
    +      where such license applies only to those patent claims licensable
    +      by such Contributor that are necessarily infringed by their
    +      Contribution(s) alone or by combination of their Contribution(s)
    +      with the Work to which such Contribution(s) was submitted. If You
    +      institute patent litigation against any entity (including a
    +      cross-claim or counterclaim in a lawsuit) alleging that the Work
    +      or a Contribution incorporated within the Work constitutes direct
    +      or contributory patent infringement, then any patent licenses
    +      granted to You under this License for that Work shall terminate
    +      as of the date such litigation is filed.
    +
    +   4. Redistribution. You may reproduce and distribute copies of the
    +      Work or Derivative Works thereof in any medium, with or without
    +      modifications, and in Source or Object form, provided that You
    +      meet the following conditions:
    +
    +      (a) You must give any other recipients of the Work or
    +          Derivative Works a copy of this License; and
    +
    +      (b) You must cause any modified files to carry prominent notices
    +          stating that You changed the files; and
    +
    +      (c) You must retain, in the Source form of any Derivative Works
    +          that You distribute, all copyright, patent, trademark, and
    +          attribution notices from the Source form of the Work,
    +          excluding those notices that do not pertain to any part of
    +          the Derivative Works; and
    +
    +      (d) If the Work includes a "NOTICE" text file as part of its
    +          distribution, then any Derivative Works that You distribute must
    +          include a readable copy of the attribution notices contained
    +          within such NOTICE file, excluding those notices that do not
    +          pertain to any part of the Derivative Works, in at least one
    +          of the following places: within a NOTICE text file distributed
    +          as part of the Derivative Works; within the Source form or
    +          documentation, if provided along with the Derivative Works; or,
    +          within a display generated by the Derivative Works, if and
    +          wherever such third-party notices normally appear. The contents
    +          of the NOTICE file are for informational purposes only and
    +          do not modify the License. You may add Your own attribution
    +          notices within Derivative Works that You distribute, alongside
    +          or as an addendum to the NOTICE text from the Work, provided
    +          that such additional attribution notices cannot be construed
    +          as modifying the License.
    +
    +      You may add Your own copyright statement to Your modifications and
    +      may provide additional or different license terms and conditions
    +      for use, reproduction, or distribution of Your modifications, or
    +      for any such Derivative Works as a whole, provided Your use,
    +      reproduction, and distribution of the Work otherwise complies with
    +      the conditions stated in this License.
    +
    +   5. Submission of Contributions. Unless You explicitly state otherwise,
    +      any Contribution intentionally submitted for inclusion in the Work
    +      by You to the Licensor shall be under the terms and conditions of
    +      this License, without any additional terms or conditions.
    +      Notwithstanding the above, nothing herein shall supersede or modify
    +      the terms of any separate license agreement you may have executed
    +      with Licensor regarding such Contributions.
    +
    +   6. Trademarks. This License does not grant permission to use the trade
    +      names, trademarks, service marks, or product names of the Licensor,
    +      except as required for reasonable and customary use in describing the
    +      origin of the Work and reproducing the content of the NOTICE file.
    +
    +   7. Disclaimer of Warranty. Unless required by applicable law or
    +      agreed to in writing, Licensor provides the Work (and each
    +      Contributor provides its Contributions) on an "AS IS" BASIS,
    +      WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or
    +      implied, including, without limitation, any warranties or conditions
    +      of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A
    +      PARTICULAR PURPOSE. You are solely responsible for determining the
    +      appropriateness of using or redistributing the Work and assume any
    +      risks associated with Your exercise of permissions under this License.
    +
    +   8. Limitation of Liability. In no event and under no legal theory,
    +      whether in tort (including negligence), contract, or otherwise,
    +      unless required by applicable law (such as deliberate and grossly
    +      negligent acts) or agreed to in writing, shall any Contributor be
    +      liable to You for damages, including any direct, indirect, special,
    +      incidental, or consequential damages of any character arising as a
    +      result of this License or out of the use or inability to use the
    +      Work (including but not limited to damages for loss of goodwill,
    +      work stoppage, computer failure or malfunction, or any and all
    +      other commercial damages or losses), even if such Contributor
    +      has been advised of the possibility of such damages.
    +
    +   9. Accepting Warranty or Additional Liability. While redistributing
    +      the Work or Derivative Works thereof, You may choose to offer,
    +      and charge a fee for, acceptance of support, warranty, indemnity,
    +      or other liability obligations and/or rights consistent with this
    +      License. However, in accepting such obligations, You may act only
    +      on Your own behalf and on Your sole responsibility, not on behalf
    +      of any other Contributor, and only if You agree to indemnify,
    +      defend, and hold each Contributor harmless for any liability
    +      incurred by, or claims asserted against, such Contributor by reason
    +      of your accepting any such warranty or additional liability.
    +
    +   END OF TERMS AND CONDITIONS
    +
    +   Copyright 2020 Yoshua Wuyts
    +
    +   Licensed under the Apache License, Version 2.0 (the "License");
    +   you may not use this file except in compliance with the License.
    +   You may obtain a copy of the License at
    +
    +       http://www.apache.org/licenses/LICENSE-2.0
    +
    +   Unless required by applicable law or agreed to in writing, software
    +   distributed under the License is distributed on an "AS IS" BASIS,
    +   WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
    +   See the License for the specific language governing permissions and
    +   limitations under the License.
    +
    +

    Apache License 2.0

                                     Apache License
                                Version 2.0, January 2004
                             https://www.apache.org/licenses/
    @@ -8754,6 +9076,146 @@ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
     See the License for the specific language governing permissions and
     limitations under the License.
     
    +
    +

    Apache License 2.0

    +
    Apache License
    +
    +Version 2.0, January 2004
    +
    +http://www.apache.org/licenses/
    +
    +TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION
    +
    +1. Definitions.
    +
    +"License" shall mean the terms and conditions for use, reproduction, and distribution as defined by Sections 1 through 9 of this document.
    +
    +"Licensor" shall mean the copyright owner or entity authorized by the copyright owner that is granting the License.
    +
    +"Legal Entity" shall mean the union of the acting entity and all other entities that control, are controlled by, or are under common control with that entity. For the purposes of this definition, "control" means (i) the power, direct or indirect, to cause the direction or management of such entity, whether by contract or otherwise, or (ii) ownership of fifty percent (50%) or more of the outstanding shares, or (iii) beneficial ownership of such entity.
    +
    +"You" (or "Your") shall mean an individual or Legal Entity exercising permissions granted by this License.
    +
    +"Source" form shall mean the preferred form for making modifications, including but not limited to software source code, documentation source, and configuration files.
    +
    +"Object" form shall mean any form resulting from mechanical transformation or translation of a Source form, including but not limited to compiled object code, generated documentation, and conversions to other media types.
    +
    +"Work" shall mean the work of authorship, whether in Source or Object form, made available under the License, as indicated by a copyright notice that is included in or attached to the work (an example is provided in the Appendix below).
    +
    +"Derivative Works" shall mean any work, whether in Source or Object form, that is based on (or derived from) the Work and for which the editorial revisions, annotations, elaborations, or other modifications represent, as a whole, an original work of authorship. For the purposes of this License, Derivative Works shall not include works that remain separable from, or merely link (or bind by name) to the interfaces of, the Work and Derivative Works thereof.
    +
    +"Contribution" shall mean any work of authorship, including the original version of the Work and any modifications or additions to that Work or Derivative Works thereof, that is intentionally submitted to Licensor for inclusion in the Work by the copyright owner or by an individual or Legal Entity authorized to submit on behalf of the copyright owner. For the purposes of this definition, "submitted" means any form of electronic, verbal, or written communication sent to the Licensor or its representatives, including but not limited to communication on electronic mailing lists, source code control systems, and issue tracking systems that are managed by, or on behalf of, the Licensor for the purpose of discussing and improving the Work, but excluding communication that is conspicuously marked or otherwise designated in writing by the copyright owner as "Not a Contribution."
    +
    +"Contributor" shall mean Licensor and any individual or Legal Entity on behalf of whom a Contribution has been received by Licensor and subsequently incorporated within the Work.
    +
    +2. Grant of Copyright License. Subject to the terms and conditions of this License, each Contributor hereby grants to You a perpetual, worldwide, non-exclusive, no-charge, royalty-free, irrevocable copyright license to reproduce, prepare Derivative Works of, publicly display, publicly perform, sublicense, and distribute the Work and such Derivative Works in Source or Object form.
    +
    +3. Grant of Patent License. Subject to the terms and conditions of this License, each Contributor hereby grants to You a perpetual, worldwide, non-exclusive, no-charge, royalty-free, irrevocable (except as stated in this section) patent license to make, have made, use, offer to sell, sell, import, and otherwise transfer the Work, where such license applies only to those patent claims licensable by such Contributor that are necessarily infringed by their Contribution(s) alone or by combination of their Contribution(s) with the Work to which such Contribution(s) was submitted. If You institute patent litigation against any entity (including a cross-claim or counterclaim in a lawsuit) alleging that the Work or a Contribution incorporated within the Work constitutes direct or contributory patent infringement, then any patent licenses granted to You under this License for that Work shall terminate as of the date such litigation is filed.
    +
    +4. Redistribution. You may reproduce and distribute copies of the Work or Derivative Works thereof in any medium, with or without modifications, and in Source or Object form, provided that You meet the following conditions:
    +
    +You must give any other recipients of the Work or Derivative Works a copy of this License; and
    +You must cause any modified files to carry prominent notices stating that You changed the files; and
    +You must retain, in the Source form of any Derivative Works that You distribute, all copyright, patent, trademark, and attribution notices from the Source form of the Work, excluding those notices that do not pertain to any part of the Derivative Works; and
    +If the Work includes a "NOTICE" text file as part of its distribution, then any Derivative Works that You distribute must include a readable copy of the attribution notices contained within such NOTICE file, excluding those notices that do not pertain to any part of the Derivative Works, in at least one of the following places: within a NOTICE text file distributed as part of the Derivative Works; within the Source form or documentation, if provided along with the Derivative Works; or, within a display generated by the Derivative Works, if and wherever such third-party notices normally appear. The contents of the NOTICE file are for informational purposes only and do not modify the License. You may add Your own attribution notices within Derivative Works that You distribute, alongside or as an addendum to the NOTICE text from the Work, provided that such additional attribution notices cannot be construed as modifying the License. 
    +
    +You may add Your own copyright statement to Your modifications and may provide additional or different license terms and conditions for use, reproduction, or distribution of Your modifications, or for any such Derivative Works as a whole, provided Your use, reproduction, and distribution of the Work otherwise complies with the conditions stated in this License.
    +5. Submission of Contributions. Unless You explicitly state otherwise, any Contribution intentionally submitted for inclusion in the Work by You to the Licensor shall be under the terms and conditions of this License, without any additional terms or conditions. Notwithstanding the above, nothing herein shall supersede or modify the terms of any separate license agreement you may have executed with Licensor regarding such Contributions.
    +
    +6. Trademarks. This License does not grant permission to use the trade names, trademarks, service marks, or product names of the Licensor, except as required for reasonable and customary use in describing the origin of the Work and reproducing the content of the NOTICE file.
    +
    +7. Disclaimer of Warranty. Unless required by applicable law or agreed to in writing, Licensor provides the Work (and each Contributor provides its Contributions) on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied, including, without limitation, any warranties or conditions of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A PARTICULAR PURPOSE. You are solely responsible for determining the appropriateness of using or redistributing the Work and assume any risks associated with Your exercise of permissions under this License.
    +
    +8. Limitation of Liability. In no event and under no legal theory, whether in tort (including negligence), contract, or otherwise, unless required by applicable law (such as deliberate and grossly negligent acts) or agreed to in writing, shall any Contributor be liable to You for damages, including any direct, indirect, special, incidental, or consequential damages of any character arising as a result of this License or out of the use or inability to use the Work (including but not limited to damages for loss of goodwill, work stoppage, computer failure or malfunction, or any and all other commercial damages or losses), even if such Contributor has been advised of the possibility of such damages.
    +
    +9. Accepting Warranty or Additional Liability. While redistributing the Work or Derivative Works thereof, You may choose to offer, and charge a fee for, acceptance of support, warranty, indemnity, or other liability obligations and/or rights consistent with this License. However, in accepting such obligations, You may act only on Your own behalf and on Your sole responsibility, not on behalf of any other Contributor, and only if You agree to indemnify, defend, and hold each Contributor harmless for any liability incurred by, or claims asserted against, such Contributor by reason of your accepting any such warranty or additional liability.
    +
    +END OF TERMS AND CONDITIONS
    +
    +
    +Copyright 2017-2023 Maik Klein, Maja Kądziołka
    +
    +Licensed under the Apache License, Version 2.0 (the "License");
    +you may not use this file except in compliance with the License.
    +You may obtain a copy of the License at
    +
    +    http://www.apache.org/licenses/LICENSE-2.0
    +
    +Unless required by applicable law or agreed to in writing, software
    +distributed under the License is distributed on an "AS IS" BASIS,
    +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
    +See the License for the specific language governing permissions and
    +limitations under the License.
    +
    +

    Apache License 2.0

    +
    Apache License
    +
    +Version 2.0, January 2004
    +
    +http://www.apache.org/licenses/
    +
    +TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION
    +
    +1. Definitions.
    +
    +"License" shall mean the terms and conditions for use, reproduction, and distribution as defined by Sections 1 through 9 of this document.
    +
    +"Licensor" shall mean the copyright owner or entity authorized by the copyright owner that is granting the License.
    +
    +"Legal Entity" shall mean the union of the acting entity and all other entities that control, are controlled by, or are under common control with that entity. For the purposes of this definition, "control" means (i) the power, direct or indirect, to cause the direction or management of such entity, whether by contract or otherwise, or (ii) ownership of fifty percent (50%) or more of the outstanding shares, or (iii) beneficial ownership of such entity.
    +
    +"You" (or "Your") shall mean an individual or Legal Entity exercising permissions granted by this License.
    +
    +"Source" form shall mean the preferred form for making modifications, including but not limited to software source code, documentation source, and configuration files.
    +
    +"Object" form shall mean any form resulting from mechanical transformation or translation of a Source form, including but not limited to compiled object code, generated documentation, and conversions to other media types.
    +
    +"Work" shall mean the work of authorship, whether in Source or Object form, made available under the License, as indicated by a copyright notice that is included in or attached to the work (an example is provided in the Appendix below).
    +
    +"Derivative Works" shall mean any work, whether in Source or Object form, that is based on (or derived from) the Work and for which the editorial revisions, annotations, elaborations, or other modifications represent, as a whole, an original work of authorship. For the purposes of this License, Derivative Works shall not include works that remain separable from, or merely link (or bind by name) to the interfaces of, the Work and Derivative Works thereof.
    +
    +"Contribution" shall mean any work of authorship, including the original version of the Work and any modifications or additions to that Work or Derivative Works thereof, that is intentionally submitted to Licensor for inclusion in the Work by the copyright owner or by an individual or Legal Entity authorized to submit on behalf of the copyright owner. For the purposes of this definition, "submitted" means any form of electronic, verbal, or written communication sent to the Licensor or its representatives, including but not limited to communication on electronic mailing lists, source code control systems, and issue tracking systems that are managed by, or on behalf of, the Licensor for the purpose of discussing and improving the Work, but excluding communication that is conspicuously marked or otherwise designated in writing by the copyright owner as "Not a Contribution."
    +
    +"Contributor" shall mean Licensor and any individual or Legal Entity on behalf of whom a Contribution has been received by Licensor and subsequently incorporated within the Work.
    +
    +2. Grant of Copyright License. Subject to the terms and conditions of this License, each Contributor hereby grants to You a perpetual, worldwide, non-exclusive, no-charge, royalty-free, irrevocable copyright license to reproduce, prepare Derivative Works of, publicly display, publicly perform, sublicense, and distribute the Work and such Derivative Works in Source or Object form.
    +
    +3. Grant of Patent License. Subject to the terms and conditions of this License, each Contributor hereby grants to You a perpetual, worldwide, non-exclusive, no-charge, royalty-free, irrevocable (except as stated in this section) patent license to make, have made, use, offer to sell, sell, import, and otherwise transfer the Work, where such license applies only to those patent claims licensable by such Contributor that are necessarily infringed by their Contribution(s) alone or by combination of their Contribution(s) with the Work to which such Contribution(s) was submitted. If You institute patent litigation against any entity (including a cross-claim or counterclaim in a lawsuit) alleging that the Work or a Contribution incorporated within the Work constitutes direct or contributory patent infringement, then any patent licenses granted to You under this License for that Work shall terminate as of the date such litigation is filed.
    +
    +4. Redistribution. You may reproduce and distribute copies of the Work or Derivative Works thereof in any medium, with or without modifications, and in Source or Object form, provided that You meet the following conditions:
    +
    +You must give any other recipients of the Work or Derivative Works a copy of this License; and
    +You must cause any modified files to carry prominent notices stating that You changed the files; and
    +You must retain, in the Source form of any Derivative Works that You distribute, all copyright, patent, trademark, and attribution notices from the Source form of the Work, excluding those notices that do not pertain to any part of the Derivative Works; and
    +If the Work includes a "NOTICE" text file as part of its distribution, then any Derivative Works that You distribute must include a readable copy of the attribution notices contained within such NOTICE file, excluding those notices that do not pertain to any part of the Derivative Works, in at least one of the following places: within a NOTICE text file distributed as part of the Derivative Works; within the Source form or documentation, if provided along with the Derivative Works; or, within a display generated by the Derivative Works, if and wherever such third-party notices normally appear. The contents of the NOTICE file are for informational purposes only and do not modify the License. You may add Your own attribution notices within Derivative Works that You distribute, alongside or as an addendum to the NOTICE text from the Work, provided that such additional attribution notices cannot be construed as modifying the License. 
    +
    +You may add Your own copyright statement to Your modifications and may provide additional or different license terms and conditions for use, reproduction, or distribution of Your modifications, or for any such Derivative Works as a whole, provided Your use, reproduction, and distribution of the Work otherwise complies with the conditions stated in this License.
    +5. Submission of Contributions. Unless You explicitly state otherwise, any Contribution intentionally submitted for inclusion in the Work by You to the Licensor shall be under the terms and conditions of this License, without any additional terms or conditions. Notwithstanding the above, nothing herein shall supersede or modify the terms of any separate license agreement you may have executed with Licensor regarding such Contributions.
    +
    +6. Trademarks. This License does not grant permission to use the trade names, trademarks, service marks, or product names of the Licensor, except as required for reasonable and customary use in describing the origin of the Work and reproducing the content of the NOTICE file.
    +
    +7. Disclaimer of Warranty. Unless required by applicable law or agreed to in writing, Licensor provides the Work (and each Contributor provides its Contributions) on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied, including, without limitation, any warranties or conditions of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A PARTICULAR PURPOSE. You are solely responsible for determining the appropriateness of using or redistributing the Work and assume any risks associated with Your exercise of permissions under this License.
    +
    +8. Limitation of Liability. In no event and under no legal theory, whether in tort (including negligence), contract, or otherwise, unless required by applicable law (such as deliberate and grossly negligent acts) or agreed to in writing, shall any Contributor be liable to You for damages, including any direct, indirect, special, incidental, or consequential damages of any character arising as a result of this License or out of the use or inability to use the Work (including but not limited to damages for loss of goodwill, work stoppage, computer failure or malfunction, or any and all other commercial damages or losses), even if such Contributor has been advised of the possibility of such damages.
    +
    +9. Accepting Warranty or Additional Liability. While redistributing the Work or Derivative Works thereof, You may choose to offer, and charge a fee for, acceptance of support, warranty, indemnity, or other liability obligations and/or rights consistent with this License. However, in accepting such obligations, You may act only on Your own behalf and on Your sole responsibility, not on behalf of any other Contributor, and only if You agree to indemnify, defend, and hold each Contributor harmless for any liability incurred by, or claims asserted against, such Contributor by reason of your accepting any such warranty or additional liability.
    +
    +END OF TERMS AND CONDITIONS
    +
    +
    +Copyright [2017] [Maik Klein]
    +
    +Licensed under the Apache License, Version 2.0 (the "License");
    +you may not use this file except in compliance with the License.
    +You may obtain a copy of the License at
    +
    +    http://www.apache.org/licenses/LICENSE-2.0
    +
    +Unless required by applicable law or agreed to in writing, software
    +distributed under the License is distributed on an "AS IS" BASIS,
    +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
    +See the License for the specific language governing permissions and
    +limitations under the License.
     

    Apache License 2.0

    Apache License
    @@ -9638,6 +10100,29 @@ Permission to use, copy, modify, and/or distribute this software for any purpose
     THE SOFTWARE IS PROVIDED "AS IS" AND ISC DISCLAIMS ALL WARRANTIES WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL ISC BE LIABLE FOR ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE.
     

    MIT License

    +
        MIT License
    +
    +    Copyright (c) Microsoft Corporation. All rights reserved.
    +
    +    Permission is hereby granted, free of charge, to any person obtaining a copy
    +    of this software and associated documentation files (the "Software"), to deal
    +    in the Software without restriction, including without limitation the rights
    +    to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
    +    copies of the Software, and to permit persons to whom the Software is
    +    furnished to do so, subject to the following conditions:
    +
    +    The above copyright notice and this permission notice shall be included in all
    +    copies or substantial portions of the Software.
    +
    +    THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
    +    IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
    +    FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
    +    AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
    +    LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
    +    OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
    +    SOFTWARE
    +
    +

    MIT License

    Copyright (c) 2014 Carl Lerche and other MIO contributors
     
     Permission is hereby granted, free of charge, to any person obtaining a copy
    @@ -9732,6 +10217,26 @@ The above copyright notice and this permission notice shall be included in all c
     THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
     

    MIT License

    +
    Copyright (c) 2017 Gilad Naaman
    +
    +Permission is hereby granted, free of charge, to any person obtaining a copy
    +of this software and associated documentation files (the "Software"), to deal
    +in the Software without restriction, including without limitation the rights
    +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
    +copies of the Software, and to permit persons to whom the Software is
    +furnished to do so, subject to the following conditions:
    +
    +The above copyright notice and this permission notice shall be included in all
    +copies or substantial portions of the Software.
    +
    +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
    +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
    +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
    +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
    +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
    +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
    +SOFTWARE.
    +

    MIT License

    Copyright (c) 2017 h2 authors
     
     Permission is hereby granted, free of charge, to any
    @@ -10083,6 +10588,33 @@ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
     THE SOFTWARE.
     

    MIT License

    +
    Copyright (c) 2024 Zeeshan Ali Khan & zbus contributors
    +
    +Permission is hereby granted, free of charge, to any
    +person obtaining a copy of this software and associated
    +documentation files (the "Software"), to deal in the
    +Software without restriction, including without
    +limitation the rights to use, copy, modify, merge,
    +publish, distribute, sublicense, and/or sell copies of
    +the Software, and to permit persons to whom the Software
    +is furnished to do so, subject to the following
    +conditions:
    +
    +The above copyright notice and this permission notice
    +shall be included in all copies or substantial portions
    +of the Software.
    +
    +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF
    +ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED
    +TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A
    +PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT
    +SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY
    +CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
    +OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR
    +IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER
    +DEALINGS IN THE SOFTWARE.
    +
    +

    MIT License

    Copyright 2016 Nika Layzell
     
     Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions:
    @@ -10262,6 +10794,31 @@ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
     SOFTWARE.
     

    MIT License

    +
    Permission is hereby granted, free of charge, to any
    +person obtaining a copy of this software and associated
    +documentation files (the "Software"), to deal in the
    +Software without restriction, including without
    +limitation the rights to use, copy, modify, merge,
    +publish, distribute, sublicense, and/or sell copies of
    +the Software, and to permit persons to whom the Software
    +is furnished to do so, subject to the following
    +conditions:
    +
    +The above copyright notice and this permission notice
    +shall be included in all copies or substantial portions
    +of the Software.
    +
    +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF
    +ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED
    +TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A
    +PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT
    +SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY
    +CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
    +OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR
    +IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER
    +DEALINGS IN THE SOFTWARE.
    +
    +

    MIT License

    Permission is hereby granted, free of charge, to any person obtaining
     a copy of this software and associated documentation files (the
     "Software"), to deal in the Software without restriction, including
    diff --git a/docs/security/license-inventory.md b/docs/security/license-inventory.md
    index 737e2ef..ed7dd42 100644
    --- a/docs/security/license-inventory.md
    +++ b/docs/security/license-inventory.md
    @@ -16,8 +16,8 @@ those terms.
     
     | License | Crate count |
     |---------|-------------|
    -| `Apache License 2.0` | 276 |
    -| `MIT License` | 55 |
    +| `Apache License 2.0` | 293 |
    +| `MIT License` | 64 |
     | `Unicode License v3` | 19 |
     | `BSD 3-Clause "New" or "Revised" License` | 7 |
     | `ISC License` | 7 |
    @@ -75,6 +75,7 @@ those terms.
     | toml_parser | 1.1.2+spec-1.1.0 | `Apache License 2.0` |  |
     | toml_writer | 1.1.1+spec-1.1.0 | `Apache License 2.0` |  |
     | omnom | 3.0.0 | `Apache License 2.0` |  |
    +| async-broadcast | 0.7.2 | `Apache License 2.0` |  |
     | hickory-net | 0.26.1 | `Apache License 2.0` |  |
     | hickory-proto | 0.26.1 | `Apache License 2.0` |  |
     | hickory-resolver | 0.26.1 | `Apache License 2.0` |  |
    @@ -97,6 +98,7 @@ those terms.
     | ppv-lite86 | 0.2.21 | `Apache License 2.0` |  |
     | rustls-pki-types | 1.14.1 | `Apache License 2.0` |  |
     | keyring | 3.6.3 | `Apache License 2.0` |  |
    +| async-recursion | 1.1.1 | `Apache License 2.0` |  |
     | t4rust-derive | 0.3.1 | `Apache License 2.0` |  |
     | tsproto-structs | 0.2.0 | `Apache License 2.0` |  |
     | addr2line | 0.25.1 | `Apache License 2.0` |  |
    @@ -111,6 +113,7 @@ those terms.
     | cc | 1.2.62 | `Apache License 2.0` |  |
     | cfg-if | 1.0.4 | `Apache License 2.0` |  |
     | cmake | 0.1.58 | `Apache License 2.0` |  |
    +| concurrent-queue | 2.5.0 | `Apache License 2.0` |  |
     | core-foundation-sys | 0.8.7 | `Apache License 2.0` |  |
     | core-foundation | 0.10.1 | `Apache License 2.0` |  |
     | core-foundation | 0.9.4 | `Apache License 2.0` |  |
    @@ -123,9 +126,14 @@ those terms.
     | displaydoc | 0.2.5 | `Apache License 2.0` |  |
     | either | 1.15.0 | `Apache License 2.0` |  |
     | equivalent | 1.0.2 | `Apache License 2.0` |  |
    +| errno | 0.3.14 | `Apache License 2.0` |  |
    +| event-listener-strategy | 0.5.4 | `Apache License 2.0` |  |
    +| event-listener | 5.4.1 | `Apache License 2.0` |  |
    +| fastrand | 2.4.1 | `Apache License 2.0` |  |
     | find-msvc-tools | 0.1.9 | `Apache License 2.0` |  |
     | fnv | 1.0.7 | `Apache License 2.0` |  |
     | form_urlencoded | 1.2.2 | `Apache License 2.0` |  |
    +| futures-lite | 2.6.1 | `Apache License 2.0` |  |
     | gimli | 0.32.3 | `Apache License 2.0` |  |
     | group | 0.13.0 | `Apache License 2.0` |  |
     | hashbrown | 0.14.5 | `Apache License 2.0` |  |
    @@ -141,6 +149,7 @@ those terms.
     | jni | 0.21.1 | `Apache License 2.0` |  |
     | jobserver | 0.1.34 | `Apache License 2.0` |  |
     | lazy_static | 1.5.0 | `Apache License 2.0` |  |
    +| linux-raw-sys | 0.12.1 | `Apache License 2.0` |  |
     | lock_api | 0.4.14 | `Apache License 2.0` |  |
     | log | 0.4.29 | `Apache License 2.0` |  |
     | mime | 0.3.17 | `Apache License 2.0` |  |
    @@ -152,6 +161,8 @@ those terms.
     | object | 0.37.3 | `Apache License 2.0` |  |
     | once_cell | 1.21.4 | `Apache License 2.0` |  |
     | openssl-probe | 0.2.1 | `Apache License 2.0` |  |
    +| ordered-stream | 0.2.0 | `Apache License 2.0` |  |
    +| parking | 2.2.1 | `Apache License 2.0` |  |
     | parking_lot | 0.12.5 | `Apache License 2.0` |  |
     | parking_lot_core | 0.9.12 | `Apache License 2.0` |  |
     | percent-encoding | 2.3.2 | `Apache License 2.0` |  |
    @@ -161,18 +172,21 @@ those terms.
     | regex | 1.12.3 | `Apache License 2.0` |  |
     | rustc-demangle | 0.1.27 | `Apache License 2.0` |  |
     | rustc_version | 0.4.1 | `Apache License 2.0` |  |
    +| rustix | 1.1.4 | `Apache License 2.0` |  |
     | rustls-native-certs | 0.8.3 | `Apache License 2.0` |  |
     | rustls | 0.23.40 | `Apache License 2.0` |  |
     | scopeguard | 1.2.0 | `Apache License 2.0` |  |
     | security-framework-sys | 2.17.0 | `Apache License 2.0` |  |
     | security-framework | 2.11.1 | `Apache License 2.0` |  |
     | security-framework | 3.7.0 | `Apache License 2.0` |  |
    +| signal-hook-registry | 1.4.8 | `Apache License 2.0` |  |
     | simd_cesu8 | 1.1.1 | `Apache License 2.0` |  |
     | smallvec | 1.15.1 | `Apache License 2.0` |  |
     | socket2 | 0.6.3 | `Apache License 2.0` |  |
     | stable_deref_trait | 1.2.1 | `Apache License 2.0` |  |
     | system-configuration-sys | 0.6.0 | `Apache License 2.0` |  |
     | system-configuration | 0.7.0 | `Apache License 2.0` |  |
    +| tempfile | 3.27.0 | `Apache License 2.0` |  |
     | thread_local | 1.1.9 | `Apache License 2.0` |  |
     | threadpool | 1.8.1 | `Apache License 2.0` |  |
     | url | 2.5.8 | `Apache License 2.0` |  |
    @@ -229,6 +243,8 @@ those terms.
     | adler2 | 2.0.1 | `Apache License 2.0` |  |
     | proc-macro-crate | 3.5.0 | `Apache License 2.0` |  |
     | vcpkg | 0.2.15 | `Apache License 2.0` |  |
    +| enumflags2 | 0.7.12 | `Apache License 2.0` |  |
    +| enumflags2_derive | 0.7.12 | `Apache License 2.0` |  |
     | dbus | 0.9.11 | `Apache License 2.0` |  |
     | libdbus-sys | 0.2.7 | `Apache License 2.0` |  |
     | android_log-sys | 0.2.0 | `Apache License 2.0` |  |
    @@ -288,6 +304,7 @@ those terms.
     | serde | 1.0.228 | `Apache License 2.0` |  |
     | serde_core | 1.0.228 | `Apache License 2.0` |  |
     | serde_derive | 1.0.228 | `Apache License 2.0` |  |
    +| serde_repr | 0.1.20 | `Apache License 2.0` |  |
     | shlex | 1.3.0 | `Apache License 2.0` |  |
     | simdutf8 | 0.1.5 | `Apache License 2.0` |  |
     | syn | 2.0.117 | `Apache License 2.0` |  |
    @@ -316,12 +333,14 @@ those terms.
     | audiopus | 0.3.0-rc.0 | `ISC License` |  |
     | aws-lc-rs | 1.17.0 | `ISC License` |  |
     | aws-lc-sys | 0.41.0 | `ISC License` |  |
    +| uds_windows | 1.2.1 | `MIT License` |  |
     | mio | 1.2.0 | `MIT License` |  |
     | nom | 7.1.3 | `MIT License` |  |
     | libsqlite3-sys | 0.30.1 | `MIT License` |  |
     | rusqlite | 0.32.1 | `MIT License` |  |
     | hyper | 1.9.0 | `MIT License` |  |
     | schannel | 0.1.29 | `MIT License` |  |
    +| memoffset | 0.9.1 | `MIT License` |  |
     | h2 | 0.4.14 | `MIT License` |  |
     | bytes | 1.11.1 | `MIT License` |  |
     | want | 0.3.1 | `MIT License` |  |
    @@ -342,6 +361,11 @@ those terms.
     | http-body-util | 0.1.3 | `MIT License` |  |
     | delegate-attr | 0.3.0 | `MIT License` |  |
     | hyper-util | 0.1.20 | `MIT License` |  |
    +| zbus | 5.15.0 | `MIT License` |  |
    +| zbus_macros | 5.15.0 | `MIT License` |  |
    +| zbus_names | 4.3.2 | `MIT License` |  |
    +| zvariant | 5.11.0 | `MIT License` |  |
    +| zvariant_derive | 5.11.0 | `MIT License` |  |
     | synstructure | 0.13.2 | `MIT License` |  |
     | fs_extra | 1.3.0 | `MIT License` |  |
     | alsa-sys | 0.3.1 | `MIT License` |  |
    @@ -357,6 +381,8 @@ those terms.
     | tokio-stream | 0.1.18 | `MIT License` |  |
     | tokio-util | 0.7.18 | `MIT License` |  |
     | tokio | 1.52.3 | `MIT License` |  |
    +| endi | 1.1.1 | `MIT License` |  |
    +| zvariant_utils | 3.3.1 | `MIT License` |  |
     | winnow | 1.0.2 | `MIT License` |  |
     | aho-corasick | 1.1.4 | `MIT License` |  |
     | byteorder | 1.5.0 | `MIT License` |  |
    @@ -2667,6 +2693,202 @@ those terms.
     
     ### Apache License 2.0
     
    +```
    +                                 Apache License
    +                           Version 2.0, January 2004
    +                        http://www.apache.org/licenses/
    +
    +   TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION
    +
    +   1. Definitions.
    +
    +      "License" shall mean the terms and conditions for use, reproduction,
    +      and distribution as defined by Sections 1 through 9 of this document.
    +
    +      "Licensor" shall mean the copyright owner or entity authorized by
    +      the copyright owner that is granting the License.
    +
    +      "Legal Entity" shall mean the union of the acting entity and all
    +      other entities that control, are controlled by, or are under common
    +      control with that entity. For the purposes of this definition,
    +      "control" means (i) the power, direct or indirect, to cause the
    +      direction or management of such entity, whether by contract or
    +      otherwise, or (ii) ownership of fifty percent (50%) or more of the
    +      outstanding shares, or (iii) beneficial ownership of such entity.
    +
    +      "You" (or "Your") shall mean an individual or Legal Entity
    +      exercising permissions granted by this License.
    +
    +      "Source" form shall mean the preferred form for making modifications,
    +      including but not limited to software source code, documentation
    +      source, and configuration files.
    +
    +      "Object" form shall mean any form resulting from mechanical
    +      transformation or translation of a Source form, including but
    +      not limited to compiled object code, generated documentation,
    +      and conversions to other media types.
    +
    +      "Work" shall mean the work of authorship, whether in Source or
    +      Object form, made available under the License, as indicated by a
    +      copyright notice that is included in or attached to the work
    +      (an example is provided in the Appendix below).
    +
    +      "Derivative Works" shall mean any work, whether in Source or Object
    +      form, that is based on (or derived from) the Work and for which the
    +      editorial revisions, annotations, elaborations, or other modifications
    +      represent, as a whole, an original work of authorship. For the purposes
    +      of this License, Derivative Works shall not include works that remain
    +      separable from, or merely link (or bind by name) to the interfaces of,
    +      the Work and Derivative Works thereof.
    +
    +      "Contribution" shall mean any work of authorship, including
    +      the original version of the Work and any modifications or additions
    +      to that Work or Derivative Works thereof, that is intentionally
    +      submitted to Licensor for inclusion in the Work by the copyright owner
    +      or by an individual or Legal Entity authorized to submit on behalf of
    +      the copyright owner. For the purposes of this definition, "submitted"
    +      means any form of electronic, verbal, or written communication sent
    +      to the Licensor or its representatives, including but not limited to
    +      communication on electronic mailing lists, source code control systems,
    +      and issue tracking systems that are managed by, or on behalf of, the
    +      Licensor for the purpose of discussing and improving the Work, but
    +      excluding communication that is conspicuously marked or otherwise
    +      designated in writing by the copyright owner as "Not a Contribution."
    +
    +      "Contributor" shall mean Licensor and any individual or Legal Entity
    +      on behalf of whom a Contribution has been received by Licensor and
    +      subsequently incorporated within the Work.
    +
    +   2. Grant of Copyright License. Subject to the terms and conditions of
    +      this License, each Contributor hereby grants to You a perpetual,
    +      worldwide, non-exclusive, no-charge, royalty-free, irrevocable
    +      copyright license to reproduce, prepare Derivative Works of,
    +      publicly display, publicly perform, sublicense, and distribute the
    +      Work and such Derivative Works in Source or Object form.
    +
    +   3. Grant of Patent License. Subject to the terms and conditions of
    +      this License, each Contributor hereby grants to You a perpetual,
    +      worldwide, non-exclusive, no-charge, royalty-free, irrevocable
    +      (except as stated in this section) patent license to make, have made,
    +      use, offer to sell, sell, import, and otherwise transfer the Work,
    +      where such license applies only to those patent claims licensable
    +      by such Contributor that are necessarily infringed by their
    +      Contribution(s) alone or by combination of their Contribution(s)
    +      with the Work to which such Contribution(s) was submitted. If You
    +      institute patent litigation against any entity (including a
    +      cross-claim or counterclaim in a lawsuit) alleging that the Work
    +      or a Contribution incorporated within the Work constitutes direct
    +      or contributory patent infringement, then any patent licenses
    +      granted to You under this License for that Work shall terminate
    +      as of the date such litigation is filed.
    +
    +   4. Redistribution. You may reproduce and distribute copies of the
    +      Work or Derivative Works thereof in any medium, with or without
    +      modifications, and in Source or Object form, provided that You
    +      meet the following conditions:
    +
    +      (a) You must give any other recipients of the Work or
    +          Derivative Works a copy of this License; and
    +
    +      (b) You must cause any modified files to carry prominent notices
    +          stating that You changed the files; and
    +
    +      (c) You must retain, in the Source form of any Derivative Works
    +          that You distribute, all copyright, patent, trademark, and
    +          attribution notices from the Source form of the Work,
    +          excluding those notices that do not pertain to any part of
    +          the Derivative Works; and
    +
    +      (d) If the Work includes a "NOTICE" text file as part of its
    +          distribution, then any Derivative Works that You distribute must
    +          include a readable copy of the attribution notices contained
    +          within such NOTICE file, excluding those notices that do not
    +          pertain to any part of the Derivative Works, in at least one
    +          of the following places: within a NOTICE text file distributed
    +          as part of the Derivative Works; within the Source form or
    +          documentation, if provided along with the Derivative Works; or,
    +          within a display generated by the Derivative Works, if and
    +          wherever such third-party notices normally appear. The contents
    +          of the NOTICE file are for informational purposes only and
    +          do not modify the License. You may add Your own attribution
    +          notices within Derivative Works that You distribute, alongside
    +          or as an addendum to the NOTICE text from the Work, provided
    +          that such additional attribution notices cannot be construed
    +          as modifying the License.
    +
    +      You may add Your own copyright statement to Your modifications and
    +      may provide additional or different license terms and conditions
    +      for use, reproduction, or distribution of Your modifications, or
    +      for any such Derivative Works as a whole, provided Your use,
    +      reproduction, and distribution of the Work otherwise complies with
    +      the conditions stated in this License.
    +
    +   5. Submission of Contributions. Unless You explicitly state otherwise,
    +      any Contribution intentionally submitted for inclusion in the Work
    +      by You to the Licensor shall be under the terms and conditions of
    +      this License, without any additional terms or conditions.
    +      Notwithstanding the above, nothing herein shall supersede or modify
    +      the terms of any separate license agreement you may have executed
    +      with Licensor regarding such Contributions.
    +
    +   6. Trademarks. This License does not grant permission to use the trade
    +      names, trademarks, service marks, or product names of the Licensor,
    +      except as required for reasonable and customary use in describing the
    +      origin of the Work and reproducing the content of the NOTICE file.
    +
    +   7. Disclaimer of Warranty. Unless required by applicable law or
    +      agreed to in writing, Licensor provides the Work (and each
    +      Contributor provides its Contributions) on an "AS IS" BASIS,
    +      WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or
    +      implied, including, without limitation, any warranties or conditions
    +      of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A
    +      PARTICULAR PURPOSE. You are solely responsible for determining the
    +      appropriateness of using or redistributing the Work and assume any
    +      risks associated with Your exercise of permissions under this License.
    +
    +   8. Limitation of Liability. In no event and under no legal theory,
    +      whether in tort (including negligence), contract, or otherwise,
    +      unless required by applicable law (such as deliberate and grossly
    +      negligent acts) or agreed to in writing, shall any Contributor be
    +      liable to You for damages, including any direct, indirect, special,
    +      incidental, or consequential damages of any character arising as a
    +      result of this License or out of the use or inability to use the
    +      Work (including but not limited to damages for loss of goodwill,
    +      work stoppage, computer failure or malfunction, or any and all
    +      other commercial damages or losses), even if such Contributor
    +      has been advised of the possibility of such damages.
    +
    +   9. Accepting Warranty or Additional Liability. While redistributing
    +      the Work or Derivative Works thereof, You may choose to offer,
    +      and charge a fee for, acceptance of support, warranty, indemnity,
    +      or other liability obligations and/or rights consistent with this
    +      License. However, in accepting such obligations, You may act only
    +      on Your own behalf and on Your sole responsibility, not on behalf
    +      of any other Contributor, and only if You agree to indemnify,
    +      defend, and hold each Contributor harmless for any liability
    +      incurred by, or claims asserted against, such Contributor by reason
    +      of your accepting any such warranty or additional liability.
    +
    +   END OF TERMS AND CONDITIONS
    +
    +   Copyright 2020 Yoshua Wuyts
    +
    +   Licensed under the Apache License, Version 2.0 (the "License");
    +   you may not use this file except in compliance with the License.
    +   You may obtain a copy of the License at
    +
    +       http://www.apache.org/licenses/LICENSE-2.0
    +
    +   Unless required by applicable law or agreed to in writing, software
    +   distributed under the License is distributed on an "AS IS" BASIS,
    +   WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
    +   See the License for the specific language governing permissions and
    +   limitations under the License.
    +
    +```
    +
    +### Apache License 2.0
    +
     ```
                                      Apache License
                                Version 2.0, January 2004
    @@ -7411,6 +7633,154 @@ See the License for the specific language governing permissions and
     limitations under the License.
     
     
    +```
    +
    +### Apache License 2.0
    +
    +```
    +Apache License
    +
    +Version 2.0, January 2004
    +
    +http://www.apache.org/licenses/
    +
    +TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION
    +
    +1. Definitions.
    +
    +"License" shall mean the terms and conditions for use, reproduction, and distribution as defined by Sections 1 through 9 of this document.
    +
    +"Licensor" shall mean the copyright owner or entity authorized by the copyright owner that is granting the License.
    +
    +"Legal Entity" shall mean the union of the acting entity and all other entities that control, are controlled by, or are under common control with that entity. For the purposes of this definition, "control" means (i) the power, direct or indirect, to cause the direction or management of such entity, whether by contract or otherwise, or (ii) ownership of fifty percent (50%) or more of the outstanding shares, or (iii) beneficial ownership of such entity.
    +
    +"You" (or "Your") shall mean an individual or Legal Entity exercising permissions granted by this License.
    +
    +"Source" form shall mean the preferred form for making modifications, including but not limited to software source code, documentation source, and configuration files.
    +
    +"Object" form shall mean any form resulting from mechanical transformation or translation of a Source form, including but not limited to compiled object code, generated documentation, and conversions to other media types.
    +
    +"Work" shall mean the work of authorship, whether in Source or Object form, made available under the License, as indicated by a copyright notice that is included in or attached to the work (an example is provided in the Appendix below).
    +
    +"Derivative Works" shall mean any work, whether in Source or Object form, that is based on (or derived from) the Work and for which the editorial revisions, annotations, elaborations, or other modifications represent, as a whole, an original work of authorship. For the purposes of this License, Derivative Works shall not include works that remain separable from, or merely link (or bind by name) to the interfaces of, the Work and Derivative Works thereof.
    +
    +"Contribution" shall mean any work of authorship, including the original version of the Work and any modifications or additions to that Work or Derivative Works thereof, that is intentionally submitted to Licensor for inclusion in the Work by the copyright owner or by an individual or Legal Entity authorized to submit on behalf of the copyright owner. For the purposes of this definition, "submitted" means any form of electronic, verbal, or written communication sent to the Licensor or its representatives, including but not limited to communication on electronic mailing lists, source code control systems, and issue tracking systems that are managed by, or on behalf of, the Licensor for the purpose of discussing and improving the Work, but excluding communication that is conspicuously marked or otherwise designated in writing by the copyright owner as "Not a Contribution."
    +
    +"Contributor" shall mean Licensor and any individual or Legal Entity on behalf of whom a Contribution has been received by Licensor and subsequently incorporated within the Work.
    +
    +2. Grant of Copyright License. Subject to the terms and conditions of this License, each Contributor hereby grants to You a perpetual, worldwide, non-exclusive, no-charge, royalty-free, irrevocable copyright license to reproduce, prepare Derivative Works of, publicly display, publicly perform, sublicense, and distribute the Work and such Derivative Works in Source or Object form.
    +
    +3. Grant of Patent License. Subject to the terms and conditions of this License, each Contributor hereby grants to You a perpetual, worldwide, non-exclusive, no-charge, royalty-free, irrevocable (except as stated in this section) patent license to make, have made, use, offer to sell, sell, import, and otherwise transfer the Work, where such license applies only to those patent claims licensable by such Contributor that are necessarily infringed by their Contribution(s) alone or by combination of their Contribution(s) with the Work to which such Contribution(s) was submitted. If You institute patent litigation against any entity (including a cross-claim or counterclaim in a lawsuit) alleging that the Work or a Contribution incorporated within the Work constitutes direct or contributory patent infringement, then any patent licenses granted to You under this License for that Work shall terminate as of the date such litigation is filed.
    +
    +4. Redistribution. You may reproduce and distribute copies of the Work or Derivative Works thereof in any medium, with or without modifications, and in Source or Object form, provided that You meet the following conditions:
    +
    +You must give any other recipients of the Work or Derivative Works a copy of this License; and
    +You must cause any modified files to carry prominent notices stating that You changed the files; and
    +You must retain, in the Source form of any Derivative Works that You distribute, all copyright, patent, trademark, and attribution notices from the Source form of the Work, excluding those notices that do not pertain to any part of the Derivative Works; and
    +If the Work includes a "NOTICE" text file as part of its distribution, then any Derivative Works that You distribute must include a readable copy of the attribution notices contained within such NOTICE file, excluding those notices that do not pertain to any part of the Derivative Works, in at least one of the following places: within a NOTICE text file distributed as part of the Derivative Works; within the Source form or documentation, if provided along with the Derivative Works; or, within a display generated by the Derivative Works, if and wherever such third-party notices normally appear. The contents of the NOTICE file are for informational purposes only and do not modify the License. You may add Your own attribution notices within Derivative Works that You distribute, alongside or as an addendum to the NOTICE text from the Work, provided that such additional attribution notices cannot be construed as modifying the License. 
    +
    +You may add Your own copyright statement to Your modifications and may provide additional or different license terms and conditions for use, reproduction, or distribution of Your modifications, or for any such Derivative Works as a whole, provided Your use, reproduction, and distribution of the Work otherwise complies with the conditions stated in this License.
    +5. Submission of Contributions. Unless You explicitly state otherwise, any Contribution intentionally submitted for inclusion in the Work by You to the Licensor shall be under the terms and conditions of this License, without any additional terms or conditions. Notwithstanding the above, nothing herein shall supersede or modify the terms of any separate license agreement you may have executed with Licensor regarding such Contributions.
    +
    +6. Trademarks. This License does not grant permission to use the trade names, trademarks, service marks, or product names of the Licensor, except as required for reasonable and customary use in describing the origin of the Work and reproducing the content of the NOTICE file.
    +
    +7. Disclaimer of Warranty. Unless required by applicable law or agreed to in writing, Licensor provides the Work (and each Contributor provides its Contributions) on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied, including, without limitation, any warranties or conditions of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A PARTICULAR PURPOSE. You are solely responsible for determining the appropriateness of using or redistributing the Work and assume any risks associated with Your exercise of permissions under this License.
    +
    +8. Limitation of Liability. In no event and under no legal theory, whether in tort (including negligence), contract, or otherwise, unless required by applicable law (such as deliberate and grossly negligent acts) or agreed to in writing, shall any Contributor be liable to You for damages, including any direct, indirect, special, incidental, or consequential damages of any character arising as a result of this License or out of the use or inability to use the Work (including but not limited to damages for loss of goodwill, work stoppage, computer failure or malfunction, or any and all other commercial damages or losses), even if such Contributor has been advised of the possibility of such damages.
    +
    +9. Accepting Warranty or Additional Liability. While redistributing the Work or Derivative Works thereof, You may choose to offer, and charge a fee for, acceptance of support, warranty, indemnity, or other liability obligations and/or rights consistent with this License. However, in accepting such obligations, You may act only on Your own behalf and on Your sole responsibility, not on behalf of any other Contributor, and only if You agree to indemnify, defend, and hold each Contributor harmless for any liability incurred by, or claims asserted against, such Contributor by reason of your accepting any such warranty or additional liability.
    +
    +END OF TERMS AND CONDITIONS
    +
    +
    +Copyright 2017-2023 Maik Klein, Maja Kądziołka
    +
    +Licensed under the Apache License, Version 2.0 (the "License");
    +you may not use this file except in compliance with the License.
    +You may obtain a copy of the License at
    +
    +    http://www.apache.org/licenses/LICENSE-2.0
    +
    +Unless required by applicable law or agreed to in writing, software
    +distributed under the License is distributed on an "AS IS" BASIS,
    +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
    +See the License for the specific language governing permissions and
    +limitations under the License.
    +
    +```
    +
    +### Apache License 2.0
    +
    +```
    +Apache License
    +
    +Version 2.0, January 2004
    +
    +http://www.apache.org/licenses/
    +
    +TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION
    +
    +1. Definitions.
    +
    +"License" shall mean the terms and conditions for use, reproduction, and distribution as defined by Sections 1 through 9 of this document.
    +
    +"Licensor" shall mean the copyright owner or entity authorized by the copyright owner that is granting the License.
    +
    +"Legal Entity" shall mean the union of the acting entity and all other entities that control, are controlled by, or are under common control with that entity. For the purposes of this definition, "control" means (i) the power, direct or indirect, to cause the direction or management of such entity, whether by contract or otherwise, or (ii) ownership of fifty percent (50%) or more of the outstanding shares, or (iii) beneficial ownership of such entity.
    +
    +"You" (or "Your") shall mean an individual or Legal Entity exercising permissions granted by this License.
    +
    +"Source" form shall mean the preferred form for making modifications, including but not limited to software source code, documentation source, and configuration files.
    +
    +"Object" form shall mean any form resulting from mechanical transformation or translation of a Source form, including but not limited to compiled object code, generated documentation, and conversions to other media types.
    +
    +"Work" shall mean the work of authorship, whether in Source or Object form, made available under the License, as indicated by a copyright notice that is included in or attached to the work (an example is provided in the Appendix below).
    +
    +"Derivative Works" shall mean any work, whether in Source or Object form, that is based on (or derived from) the Work and for which the editorial revisions, annotations, elaborations, or other modifications represent, as a whole, an original work of authorship. For the purposes of this License, Derivative Works shall not include works that remain separable from, or merely link (or bind by name) to the interfaces of, the Work and Derivative Works thereof.
    +
    +"Contribution" shall mean any work of authorship, including the original version of the Work and any modifications or additions to that Work or Derivative Works thereof, that is intentionally submitted to Licensor for inclusion in the Work by the copyright owner or by an individual or Legal Entity authorized to submit on behalf of the copyright owner. For the purposes of this definition, "submitted" means any form of electronic, verbal, or written communication sent to the Licensor or its representatives, including but not limited to communication on electronic mailing lists, source code control systems, and issue tracking systems that are managed by, or on behalf of, the Licensor for the purpose of discussing and improving the Work, but excluding communication that is conspicuously marked or otherwise designated in writing by the copyright owner as "Not a Contribution."
    +
    +"Contributor" shall mean Licensor and any individual or Legal Entity on behalf of whom a Contribution has been received by Licensor and subsequently incorporated within the Work.
    +
    +2. Grant of Copyright License. Subject to the terms and conditions of this License, each Contributor hereby grants to You a perpetual, worldwide, non-exclusive, no-charge, royalty-free, irrevocable copyright license to reproduce, prepare Derivative Works of, publicly display, publicly perform, sublicense, and distribute the Work and such Derivative Works in Source or Object form.
    +
    +3. Grant of Patent License. Subject to the terms and conditions of this License, each Contributor hereby grants to You a perpetual, worldwide, non-exclusive, no-charge, royalty-free, irrevocable (except as stated in this section) patent license to make, have made, use, offer to sell, sell, import, and otherwise transfer the Work, where such license applies only to those patent claims licensable by such Contributor that are necessarily infringed by their Contribution(s) alone or by combination of their Contribution(s) with the Work to which such Contribution(s) was submitted. If You institute patent litigation against any entity (including a cross-claim or counterclaim in a lawsuit) alleging that the Work or a Contribution incorporated within the Work constitutes direct or contributory patent infringement, then any patent licenses granted to You under this License for that Work shall terminate as of the date such litigation is filed.
    +
    +4. Redistribution. You may reproduce and distribute copies of the Work or Derivative Works thereof in any medium, with or without modifications, and in Source or Object form, provided that You meet the following conditions:
    +
    +You must give any other recipients of the Work or Derivative Works a copy of this License; and
    +You must cause any modified files to carry prominent notices stating that You changed the files; and
    +You must retain, in the Source form of any Derivative Works that You distribute, all copyright, patent, trademark, and attribution notices from the Source form of the Work, excluding those notices that do not pertain to any part of the Derivative Works; and
    +If the Work includes a "NOTICE" text file as part of its distribution, then any Derivative Works that You distribute must include a readable copy of the attribution notices contained within such NOTICE file, excluding those notices that do not pertain to any part of the Derivative Works, in at least one of the following places: within a NOTICE text file distributed as part of the Derivative Works; within the Source form or documentation, if provided along with the Derivative Works; or, within a display generated by the Derivative Works, if and wherever such third-party notices normally appear. The contents of the NOTICE file are for informational purposes only and do not modify the License. You may add Your own attribution notices within Derivative Works that You distribute, alongside or as an addendum to the NOTICE text from the Work, provided that such additional attribution notices cannot be construed as modifying the License. 
    +
    +You may add Your own copyright statement to Your modifications and may provide additional or different license terms and conditions for use, reproduction, or distribution of Your modifications, or for any such Derivative Works as a whole, provided Your use, reproduction, and distribution of the Work otherwise complies with the conditions stated in this License.
    +5. Submission of Contributions. Unless You explicitly state otherwise, any Contribution intentionally submitted for inclusion in the Work by You to the Licensor shall be under the terms and conditions of this License, without any additional terms or conditions. Notwithstanding the above, nothing herein shall supersede or modify the terms of any separate license agreement you may have executed with Licensor regarding such Contributions.
    +
    +6. Trademarks. This License does not grant permission to use the trade names, trademarks, service marks, or product names of the Licensor, except as required for reasonable and customary use in describing the origin of the Work and reproducing the content of the NOTICE file.
    +
    +7. Disclaimer of Warranty. Unless required by applicable law or agreed to in writing, Licensor provides the Work (and each Contributor provides its Contributions) on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied, including, without limitation, any warranties or conditions of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A PARTICULAR PURPOSE. You are solely responsible for determining the appropriateness of using or redistributing the Work and assume any risks associated with Your exercise of permissions under this License.
    +
    +8. Limitation of Liability. In no event and under no legal theory, whether in tort (including negligence), contract, or otherwise, unless required by applicable law (such as deliberate and grossly negligent acts) or agreed to in writing, shall any Contributor be liable to You for damages, including any direct, indirect, special, incidental, or consequential damages of any character arising as a result of this License or out of the use or inability to use the Work (including but not limited to damages for loss of goodwill, work stoppage, computer failure or malfunction, or any and all other commercial damages or losses), even if such Contributor has been advised of the possibility of such damages.
    +
    +9. Accepting Warranty or Additional Liability. While redistributing the Work or Derivative Works thereof, You may choose to offer, and charge a fee for, acceptance of support, warranty, indemnity, or other liability obligations and/or rights consistent with this License. However, in accepting such obligations, You may act only on Your own behalf and on Your sole responsibility, not on behalf of any other Contributor, and only if You agree to indemnify, defend, and hold each Contributor harmless for any liability incurred by, or claims asserted against, such Contributor by reason of your accepting any such warranty or additional liability.
    +
    +END OF TERMS AND CONDITIONS
    +
    +
    +Copyright [2017] [Maik Klein]
    +
    +Licensed under the Apache License, Version 2.0 (the "License");
    +you may not use this file except in compliance with the License.
    +You may obtain a copy of the License at
    +
    +    http://www.apache.org/licenses/LICENSE-2.0
    +
    +Unless required by applicable law or agreed to in writing, software
    +distributed under the License is distributed on an "AS IS" BASIS,
    +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
    +See the License for the specific language governing permissions and
    +limitations under the License.
    +
     ```
     
     ### Apache License 2.0
    @@ -8353,6 +8723,33 @@ THE SOFTWARE IS PROVIDED "AS IS" AND ISC DISCLAIMS ALL WARRANTIES WITH
     
     ### MIT License
     
    +```
    +    MIT License
    +
    +    Copyright (c) Microsoft Corporation. All rights reserved.
    +
    +    Permission is hereby granted, free of charge, to any person obtaining a copy
    +    of this software and associated documentation files (the "Software"), to deal
    +    in the Software without restriction, including without limitation the rights
    +    to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
    +    copies of the Software, and to permit persons to whom the Software is
    +    furnished to do so, subject to the following conditions:
    +
    +    The above copyright notice and this permission notice shall be included in all
    +    copies or substantial portions of the Software.
    +
    +    THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
    +    IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
    +    FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
    +    AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
    +    LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
    +    OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
    +    SOFTWARE
    +
    +```
    +
    +### MIT License
    +
     ```
     Copyright (c) 2014 Carl Lerche and other MIO contributors
     
    @@ -8467,6 +8864,30 @@ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRES
     
     ### MIT License
     
    +```
    +Copyright (c) 2017 Gilad Naaman
    +
    +Permission is hereby granted, free of charge, to any person obtaining a copy
    +of this software and associated documentation files (the "Software"), to deal
    +in the Software without restriction, including without limitation the rights
    +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
    +copies of the Software, and to permit persons to whom the Software is
    +furnished to do so, subject to the following conditions:
    +
    +The above copyright notice and this permission notice shall be included in all
    +copies or substantial portions of the Software.
    +
    +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
    +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
    +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
    +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
    +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
    +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
    +SOFTWARE.
    +```
    +
    +### MIT License
    +
     ```
     Copyright (c) 2017 h2 authors
     
    @@ -8874,6 +9295,37 @@ THE SOFTWARE.
     
     ### MIT License
     
    +```
    +Copyright (c) 2024 Zeeshan Ali Khan & zbus contributors
    +
    +Permission is hereby granted, free of charge, to any
    +person obtaining a copy of this software and associated
    +documentation files (the "Software"), to deal in the
    +Software without restriction, including without
    +limitation the rights to use, copy, modify, merge,
    +publish, distribute, sublicense, and/or sell copies of
    +the Software, and to permit persons to whom the Software
    +is furnished to do so, subject to the following
    +conditions:
    +
    +The above copyright notice and this permission notice
    +shall be included in all copies or substantial portions
    +of the Software.
    +
    +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF
    +ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED
    +TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A
    +PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT
    +SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY
    +CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
    +OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR
    +IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER
    +DEALINGS IN THE SOFTWARE.
    +
    +```
    +
    +### MIT License
    +
     ```
     Copyright 2016 Nika Layzell
     
    @@ -9089,6 +9541,35 @@ SOFTWARE.
     
     ### MIT License
     
    +```
    +Permission is hereby granted, free of charge, to any
    +person obtaining a copy of this software and associated
    +documentation files (the "Software"), to deal in the
    +Software without restriction, including without
    +limitation the rights to use, copy, modify, merge,
    +publish, distribute, sublicense, and/or sell copies of
    +the Software, and to permit persons to whom the Software
    +is furnished to do so, subject to the following
    +conditions:
    +
    +The above copyright notice and this permission notice
    +shall be included in all copies or substantial portions
    +of the Software.
    +
    +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF
    +ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED
    +TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A
    +PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT
    +SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY
    +CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
    +OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR
    +IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER
    +DEALINGS IN THE SOFTWARE.
    +
    +```
    +
    +### MIT License
    +
     ```
     Permission is hereby granted, free of charge, to any person obtaining
     a copy of this software and associated documentation files (the