Lands SDD-081..088 + SDD-092 implementations on top of v1.0.0-rc.3.
The cross-platform pieces — `AudioTransmitGate`, the per-platform
backend ladder, and the missed-key-up watchdog — are wired into the
audio engine lifecycle. Per-platform live verification on Windows
/ macOS / GNOME-Wayland reference hosts is the remaining work
(RR-PTT-001..006/008 in `release-readiness-go-nogo-record.md`).
`chanora_audio::ptt`
--------------------
* `AudioTransmitGate` now owns an `Arc<AtomicBool>` plus a
`tokio::sync::watch::Sender<bool>` (SAD-075 / SDD-089). The
encoder feed reads the atomic on the hot path; the watchdog
subscribes to the watch channel.
* `MissedKeyUpWatchdog::spawn(gate, timeout)` watches the gate
transitions and self-clears `transmit_active` if the
`false -> true` lifetime exceeds the configured ceiling
(DEC-028, default 30s). Two unit tests cover the timeout-fires
and the no-fire-on-normal-release paths.
`chanora_audio::ptt_backends`
-----------------------------
* `DesktopPttBackend` trait + `PttBinding` value type + `PttInputClass`
enum + `PttBackendError` (SDD-081). `PttBinding` deliberately
carries only `input_class` and an opaque `platform_key`
string; raw key codes never appear in the type surface.
* `select()` factory (SAD-071): runtime ladder evaluation per
OS. Windows → Raw Input → low-level hook → Focused; macOS →
Event Tap → Focused; Linux → GNOME-Wayland portal probe →
Focused.
* `FocusedPttBackend` (SDD-087): universal terminal fallback;
integrates with the existing Flutter Listener-driven PTT.
* `WindowsRawInputBackend` + `WindowsHookBackend` (SDD-083 /
SDD-084): three-rung ladder evaluated once at engine start.
Each backend runs a dedicated worker thread that holds the
OS-level handle; `start`/`stop` lifecycle is honest. Live
`RegisterRawInputDevices` / `SetWindowsHookEx` wiring is
platform-verification work — the scaffolding lets the
descriptor + watchdog + capability event be exercised
end-to-end now.
* `MacOSEventTapBackend` (SDD-085): two-rung ladder with
explicit `PermissionState` (Granted / Denied / Undetermined).
`Undetermined` resolves to `L0Focused` so capability
advertising matches actual runtime behaviour even before
Input Monitoring is granted. Live `CGEventTap` + `IOHIDCheckAccess`
wiring is platform-verification work.
* `LinuxGnomeWaylandBackend` (SDD-086): probes GNOME-on-Wayland
via `XDG_SESSION_TYPE` + `XDG_CURRENT_DESKTOP`, then verifies
the `org.freedesktop.portal.GlobalShortcuts` D-Bus interface
is reachable by reading the `version` property over a
blocking zbus session. Reports `gnome-wayland-portal` /
`L2GlobalHoldToTalk`. Other Linux environments fall through
to the universal Focused backend (DEC-025).
`chanora_audio::engine`
-----------------------
* Engine now owns `transmit_gate: AudioTransmitGate` and
threads a `flag_arc()` clone into the existing capture
state for the cheap hot-path read. `set_transmit_active` /
`transmit_active()` go through the gate so subscribers see
every transition.
* `start_audio` selects the highest-capability backend via
`ptt_backends::select()`, calls `backend.start(gate, none())`,
and spawns the watchdog. Both are released in `stop()` and
on Drop.
* New `engine.rebind_ptt(binding) -> PttBackendDescriptor`
drives the binding-capture flow without restarting the engine.
* New `engine.ptt_descriptor()` returns the privacy-safe
descriptor for the initial UI render before the first
capability event arrives.
`chanora_core`
--------------
* Re-exports `PttBinding` + `PttInputClass`.
* New `ChanoraSession::set_ptt_binding(binding)` — calls
`audio.rebind_ptt` and broadcasts the freshly-published
`SessionEvent::PttCapability` so the UI badge updates live.
* New `ChanoraSession::ptt_descriptor()` for the initial render.
`chanora_bridge`
----------------
* New `BridgePttInputClass` enum + `set_ptt_binding(input_class,
platform_key)` async function. The `platform_key` string is
opaque to the bridge and never logged.
* New `ptt_descriptor()` async accessor returning the
`(level, backend_id, bound_input_class)` triple.
Flutter
-------
* `_AudioControls` now has a "Configure" button next to the
capability badge; `_PttBindingCaptureDialog` captures the
next key press (via `Focus.onKeyEvent`) or mouse side button
(via `Listener.onPointerDown` filtered to button bitmasks
`0x08` / `0x10`). The captured value is the platform-neutral
`LogicalKeyboardKey.keyLabel` or `mouse-side-button:{button}`.
* The dialog explicitly tells the user that the actual key
value never leaves it (DEC-027).
* New ARB keys: `pttConfigureAction`, `pttConfigureTitle`,
`pttConfigurePrompt`, `pttConfigureWaiting`,
`pttConfigureCaptured`, `pttConfigurePrivacyNote`,
`pttConfigureSaveAction` (en + zh-Hans).
Dependencies
------------
* `chanora_audio` adds (Linux only) `zbus = "5"` with the
`tokio` runtime selector + `blocking-api` feature for the
GlobalShortcuts portal probe.
* `chanora_audio` adds `tokio` `test-util` to dev-deps for
`start_paused` watchdog tests (the live watchdog tests use
multi-threaded real time).
Verification
------------
* `cargo test --workspace` with `CHANORA_DISABLE_KEYRING=1`:
57 tests green (was 53). chanora_audio rises from 4 to 8.
* `cargo deny check`: advisories ok, bans ok, licenses ok,
sources ok.
* `cargo about generate --offline`: regenerates
`docs/security/license-inventory.{md,html}`. The crate count
rises from 364 to 383 with the addition of the zbus tree.
* `tools/dump_flutter_licenses.sh`: 94 packages, zero without
LICENSE (unchanged).
* `flutter analyze`: clean.
* `cargo build -p chanora_bridge --release` + `flutter build
linux --release`: clean Linux x86_64 bundle.
Documentation
-------------
* `docs/release/release-readiness-go-nogo-record.md` flips
RR-PTT-007 (missed-key-up watchdog) to Done with a pointer
to the two passing unit tests; bumps to v0.9.4. Live
per-platform traces (RR-PTT-001..005, RR-PTT-008) remain
open and are blocked only on platform reference hosts.
Per-platform live verification (Raw Input registration, Event Tap
creation under granted permission, GlobalShortcuts CreateSession +
BindShortcuts) is queued for the platform owners' reference hosts
per `staged-release-plan.md`.
443 lines
14 KiB
Dart
443 lines
14 KiB
Dart
// This file is automatically generated, so please do not edit it.
|
|
// @generated by `flutter_rust_bridge`@ 2.12.0.
|
|
|
|
// ignore_for_file: invalid_use_of_internal_member, unused_import, unnecessary_import
|
|
|
|
import 'frb_generated.dart';
|
|
import 'lib.dart';
|
|
import 'package:flutter_rust_bridge/flutter_rust_bridge_for_generated.dart';
|
|
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`, `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
|
|
/// via [`BridgeError::AlreadyConnected`].
|
|
///
|
|
/// `password` is optional — pass an empty string for servers that
|
|
/// don't require one.
|
|
Future<BridgeSnapshot> connect({
|
|
required String host,
|
|
required String nickname,
|
|
required String password,
|
|
}) => RustLib.instance.api.crateApiConnect(
|
|
host: host,
|
|
nickname: nickname,
|
|
password: password,
|
|
);
|
|
|
|
/// Re-fetch a fresh snapshot from the active connection.
|
|
Future<BridgeSnapshot> snapshot() => RustLib.instance.api.crateApiSnapshot();
|
|
|
|
/// Disconnect from the server. No-op if not connected.
|
|
Future<void> disconnect() => RustLib.instance.api.crateApiDisconnect();
|
|
|
|
/// True if a connection is currently active.
|
|
Future<bool> isConnected() => RustLib.instance.api.crateApiIsConnected();
|
|
|
|
/// Start the audio engine on the active connection. Requires a
|
|
/// connection; idempotent (will replace any previous engine).
|
|
Future<void> startAudio() => RustLib.instance.api.crateApiStartAudio();
|
|
|
|
/// Set the push-to-talk state.
|
|
Future<void> 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<void> 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.
|
|
Future<void> moveToChannel({
|
|
required BigInt channelId,
|
|
required String password,
|
|
}) => RustLib.instance.api.crateApiMoveToChannel(
|
|
channelId: channelId,
|
|
password: password,
|
|
);
|
|
|
|
/// Toggle self input-mute (microphone) on the server. Independent
|
|
/// of push-to-talk: a muted client never transmits regardless of
|
|
/// PTT state.
|
|
Future<void> setInputMuted({required bool muted}) =>
|
|
RustLib.instance.api.crateApiSetInputMuted(muted: muted);
|
|
|
|
/// Toggle self output-mute (speaker). Mutes locally *and* informs
|
|
/// the server. The server uses this for the channel icon next to
|
|
/// the client name; the local mute kicks in immediately even
|
|
/// before the server acknowledges.
|
|
Future<void> setOutputMuted({required bool muted}) =>
|
|
RustLib.instance.api.crateApiSetOutputMuted(muted: muted);
|
|
|
|
/// Set master output gain. `1.0` is unity, `0.0` is silent. Values
|
|
/// above `1.0` amplify and can clip downstream. Errors when audio
|
|
/// is not started.
|
|
Future<void> setOutputGain({required double gain}) =>
|
|
RustLib.instance.api.crateApiSetOutputGain(gain: gain);
|
|
|
|
/// User-initiated diagnostic export. Returns a multi-line text
|
|
/// blob, redacted per the production policy, that the user can
|
|
/// share or copy. DEC-016 forbids automatic uploads — this is the
|
|
/// only path that surfaces logs.
|
|
String exportDiagnostics() => RustLib.instance.api.crateApiExportDiagnostics();
|
|
|
|
/// Wire the identity persistence store to a platform-private
|
|
/// directory. Should be called once on app start after Flutter has
|
|
/// resolved `getApplicationSupportDirectory()` (or equivalent).
|
|
///
|
|
/// Subsequent [`connect`] calls will reuse the persisted identity,
|
|
/// or generate-and-persist a fresh one on first use. This keeps the
|
|
/// server-visible UID stable across app restarts.
|
|
///
|
|
/// Beta caveat: the identity is stored as a plain file (mode 0600
|
|
/// on Unix). It is *not* encrypted at rest. RISK-PoC-002 documents
|
|
/// this gap; the v0.4 storage rework lands the proper Secret
|
|
/// Service + Android Keystore + iOS Keychain backends.
|
|
Future<void> initStorage({required String dir}) =>
|
|
RustLib.instance.api.crateApiInitStorage(dir: dir);
|
|
|
|
/// List persisted bookmarks.
|
|
Future<List<BridgeBookmark>> listBookmarks() =>
|
|
RustLib.instance.api.crateApiListBookmarks();
|
|
|
|
/// Insert a bookmark and return its assigned id. The `id` field on
|
|
/// the input is ignored.
|
|
Future<PlatformInt64> addBookmark({required BridgeBookmark b}) =>
|
|
RustLib.instance.api.crateApiAddBookmark(b: b);
|
|
|
|
/// Update an existing bookmark.
|
|
Future<void> updateBookmark({required BridgeBookmark b}) =>
|
|
RustLib.instance.api.crateApiUpdateBookmark(b: b);
|
|
|
|
/// Delete a bookmark by id.
|
|
Future<void> deleteBookmark({required PlatformInt64 id}) =>
|
|
RustLib.instance.api.crateApiDeleteBookmark(id: id);
|
|
|
|
/// Notify the core of the latest OS-reported connectivity state.
|
|
/// Called by the Flutter side from `connectivity_plus` callbacks.
|
|
/// The core's supervisor uses this to (a) pre-charge the watchdog
|
|
/// on Offline and (b) short-circuit reconnect backoff on Online.
|
|
void setNetworkState({required BridgeNetworkState state}) =>
|
|
RustLib.instance.api.crateApiSetNetworkState(state: state);
|
|
|
|
/// Subscribe to lifecycle events. Each call yields a fresh
|
|
/// subscription; multiple subscribers are supported. On slow
|
|
/// consumers, events are dropped rather than blocking the supervisor
|
|
/// (consistent with `tokio::sync::broadcast::Receiver` semantics).
|
|
Stream<BridgeEvent> eventsStream() =>
|
|
RustLib.instance.api.crateApiEventsStream();
|
|
|
|
/// Read audio statistics. Errors if no connection or audio not started.
|
|
Future<BridgeAudioStats> audioStats() =>
|
|
RustLib.instance.api.crateApiAudioStats();
|
|
|
|
/// Statistics from the audio engine.
|
|
class BridgeAudioStats {
|
|
/// Number of Opus frames sent since audio started.
|
|
final int framesSent;
|
|
|
|
/// Number of inbound voice packets decoded.
|
|
final int framesReceived;
|
|
|
|
/// Current push-to-talk state.
|
|
final bool pttActive;
|
|
|
|
const BridgeAudioStats({
|
|
required this.framesSent,
|
|
required this.framesReceived,
|
|
required this.pttActive,
|
|
});
|
|
|
|
@override
|
|
int get hashCode =>
|
|
framesSent.hashCode ^ framesReceived.hashCode ^ pttActive.hashCode;
|
|
|
|
@override
|
|
bool operator ==(Object other) =>
|
|
identical(this, other) ||
|
|
other is BridgeAudioStats &&
|
|
runtimeType == other.runtimeType &&
|
|
framesSent == other.framesSent &&
|
|
framesReceived == other.framesReceived &&
|
|
pttActive == other.pttActive;
|
|
}
|
|
|
|
/// Bookmark DTO mirroring [`chanora_core::Bookmark`].
|
|
class BridgeBookmark {
|
|
/// Row id assigned by SQLite. Use `0` when adding new rows;
|
|
/// the returned id is then meaningful.
|
|
final PlatformInt64 id;
|
|
|
|
/// User-facing label.
|
|
final String displayName;
|
|
|
|
/// `hostname[:port]` or TSDNS name.
|
|
final String host;
|
|
|
|
/// Nickname to use for this bookmark.
|
|
final String nickname;
|
|
|
|
/// Optional remembered password. Empty string = none.
|
|
final String password;
|
|
|
|
const BridgeBookmark({
|
|
required this.id,
|
|
required this.displayName,
|
|
required this.host,
|
|
required this.nickname,
|
|
required this.password,
|
|
});
|
|
|
|
@override
|
|
int get hashCode =>
|
|
id.hashCode ^
|
|
displayName.hashCode ^
|
|
host.hashCode ^
|
|
nickname.hashCode ^
|
|
password.hashCode;
|
|
|
|
@override
|
|
bool operator ==(Object other) =>
|
|
identical(this, other) ||
|
|
other is BridgeBookmark &&
|
|
runtimeType == other.runtimeType &&
|
|
id == other.id &&
|
|
displayName == other.displayName &&
|
|
host == other.host &&
|
|
nickname == other.nickname &&
|
|
password == other.password;
|
|
}
|
|
|
|
/// Channel as seen by Dart. Matches `chanora_protocol::ChannelInfo`
|
|
/// but with primitive `u64` ids so the Dart side gets `BigInt`s
|
|
/// without any wrapper-type ceremony.
|
|
class BridgeChannel {
|
|
/// Stable channel id.
|
|
final BigInt id;
|
|
|
|
/// Parent channel id; 0 means top-level.
|
|
final BigInt parent;
|
|
|
|
/// Display name.
|
|
final String name;
|
|
|
|
/// Server-side ordering hint.
|
|
final PlatformInt64 order;
|
|
|
|
const BridgeChannel({
|
|
required this.id,
|
|
required this.parent,
|
|
required this.name,
|
|
required this.order,
|
|
});
|
|
|
|
@override
|
|
int get hashCode =>
|
|
id.hashCode ^ parent.hashCode ^ name.hashCode ^ order.hashCode;
|
|
|
|
@override
|
|
bool operator ==(Object other) =>
|
|
identical(this, other) ||
|
|
other is BridgeChannel &&
|
|
runtimeType == other.runtimeType &&
|
|
id == other.id &&
|
|
parent == other.parent &&
|
|
name == other.name &&
|
|
order == other.order;
|
|
}
|
|
|
|
/// Client as seen by Dart.
|
|
class BridgeClient {
|
|
/// Stable client id.
|
|
final BigInt id;
|
|
|
|
/// Channel id the client is currently in.
|
|
final BigInt channel;
|
|
|
|
/// Nickname.
|
|
final String name;
|
|
|
|
const BridgeClient({
|
|
required this.id,
|
|
required this.channel,
|
|
required this.name,
|
|
});
|
|
|
|
@override
|
|
int get hashCode => id.hashCode ^ channel.hashCode ^ name.hashCode;
|
|
|
|
@override
|
|
bool operator ==(Object other) =>
|
|
identical(this, other) ||
|
|
other is BridgeClient &&
|
|
runtimeType == other.runtimeType &&
|
|
id == other.id &&
|
|
channel == other.channel &&
|
|
name == other.name;
|
|
}
|
|
|
|
@freezed
|
|
sealed class BridgeEvent with _$BridgeEvent {
|
|
const BridgeEvent._();
|
|
|
|
/// Initial connect succeeded, or a reconnect attempt succeeded.
|
|
const factory BridgeEvent.connected({
|
|
/// Server name reported by the server snapshot.
|
|
required String serverName,
|
|
}) = BridgeEvent_Connected;
|
|
|
|
/// Connection lost; supervisor will retry.
|
|
const factory BridgeEvent.lost({
|
|
/// Reason classification from the protocol layer.
|
|
required String reason,
|
|
}) = BridgeEvent_Lost;
|
|
|
|
/// Supervisor is sleeping before its next reconnect attempt.
|
|
const factory BridgeEvent.reconnecting({
|
|
/// 1-based attempt counter for the current outage.
|
|
required int attempt,
|
|
|
|
/// Seconds the supervisor will sleep before this attempt.
|
|
required int delaySecs,
|
|
}) = BridgeEvent_Reconnecting;
|
|
|
|
/// Session ended (user-requested disconnect or unrecoverable).
|
|
const factory BridgeEvent.disconnected({
|
|
/// Reason classification.
|
|
required String reason,
|
|
}) = BridgeEvent_Disconnected;
|
|
|
|
/// Audio engine started.
|
|
const factory BridgeEvent.audioStarted() = BridgeEvent_AudioStarted;
|
|
|
|
/// Audio engine stopped.
|
|
const factory BridgeEvent.audioStopped() = BridgeEvent_AudioStopped;
|
|
|
|
/// Snapshot probe observed a change in channel/client counts.
|
|
/// UI uses this to drive an auto-refresh without active
|
|
/// polling.
|
|
const factory BridgeEvent.snapshotChanged({
|
|
/// Latest channel count.
|
|
required int channels,
|
|
|
|
/// Latest client count.
|
|
required int clients,
|
|
}) = BridgeEvent_SnapshotChanged;
|
|
|
|
/// Detected desktop Push-to-Talk capability (gen2 v0.9.3,
|
|
/// DEC-023..028). The fields carry only privacy-safe values per
|
|
/// DEC-027: the capability level, a stable backend identifier,
|
|
/// and the bound input class. No key codes, scan codes, or
|
|
/// virtual-key values cross this boundary.
|
|
const factory BridgeEvent.pttCapability({
|
|
/// Stable capability identifier (`"L0Focused"`,
|
|
/// `"L1GlobalShortcut"`, `"L2GlobalHoldToTalk"`,
|
|
/// `"L3GlobalWithMouseButtons"`, or `"L4DeviceAware"`).
|
|
required String level,
|
|
|
|
/// Stable backend identifier (e.g. `"focused"`).
|
|
required String backendId,
|
|
|
|
/// Coarse bound input class (e.g. `"keyboard"`,
|
|
/// `"mouse-side-button"`); empty when no binding is active.
|
|
required String boundInputClass,
|
|
}) = BridgeEvent_PttCapability;
|
|
}
|
|
|
|
/// Coarse OS-reported network state. Mirrors
|
|
/// [`chanora_core::NetworkState`] across the bridge.
|
|
enum BridgeNetworkState {
|
|
/// No signal seen yet.
|
|
unknown,
|
|
|
|
/// OS reports a usable network.
|
|
online,
|
|
|
|
/// OS reports no network.
|
|
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.
|
|
final String serverName;
|
|
|
|
/// Welcome banner text.
|
|
final String welcomeMessage;
|
|
|
|
/// Server platform (e.g. "Linux").
|
|
final String platform;
|
|
|
|
/// Server version string.
|
|
final String version;
|
|
|
|
/// Channels currently known.
|
|
final List<BridgeChannel> channels;
|
|
|
|
/// Clients currently known.
|
|
final List<BridgeClient> clients;
|
|
|
|
const BridgeSnapshot({
|
|
required this.serverName,
|
|
required this.welcomeMessage,
|
|
required this.platform,
|
|
required this.version,
|
|
required this.channels,
|
|
required this.clients,
|
|
});
|
|
|
|
@override
|
|
int get hashCode =>
|
|
serverName.hashCode ^
|
|
welcomeMessage.hashCode ^
|
|
platform.hashCode ^
|
|
version.hashCode ^
|
|
channels.hashCode ^
|
|
clients.hashCode;
|
|
|
|
@override
|
|
bool operator ==(Object other) =>
|
|
identical(this, other) ||
|
|
other is BridgeSnapshot &&
|
|
runtimeType == other.runtimeType &&
|
|
serverName == other.serverName &&
|
|
welcomeMessage == other.welcomeMessage &&
|
|
platform == other.platform &&
|
|
version == other.version &&
|
|
channels == other.channels &&
|
|
clients == other.clients;
|
|
}
|