feat(ios,p0): iOS P0 platform, audio fixes, channel UX

This commit is contained in:
Edison Jwa
2026-05-17 22:00:00 +09:00
parent a1fefc8ab6
commit 7a59f5b9a1
38 changed files with 1705 additions and 674 deletions
+19 -28
View File
@@ -46,9 +46,7 @@ use zbus::blocking::Connection as BlockingConnection;
use zbus::zvariant::{OwnedValue, Value};
use zbus::{proxy, Connection as AsyncConnection};
use super::{
AudioTransmitGate, DesktopPttBackend, PttBackendError, PttBinding, PttInputClass,
};
use super::{AudioTransmitGate, DesktopPttBackend, PttBackendError, PttBinding, PttInputClass};
use crate::ptt::{PttBackendDescriptor, PttCapabilityLevel};
/// Try to construct a `LinuxGnomeWaylandBackend`. Returns `None`
@@ -84,7 +82,9 @@ fn is_gnome_on_wayland() -> bool {
let desktop = env::var("XDG_CURRENT_DESKTOP")
.unwrap_or_default()
.to_ascii_lowercase();
desktop.split(':').any(|s| s == "gnome" || s == "gnome-flashback")
desktop
.split(':')
.any(|s| s == "gnome" || s == "gnome-flashback")
}
// ---------- D-Bus proxies ----------
@@ -171,11 +171,7 @@ trait Request {
/// is `0` for success, `1` for user cancellation, `2` for
/// other failure.
#[zbus(signal)]
fn response(
&self,
response: u32,
results: HashMap<String, OwnedValue>,
) -> zbus::Result<()>;
fn response(&self, response: u32, results: HashMap<String, OwnedValue>) -> zbus::Result<()>;
/// Cancel an in-flight request.
fn close(&self) -> zbus::Result<()>;
@@ -246,13 +242,11 @@ impl LinuxGnomeWaylandBackend {
let join_result = std::thread::Builder::new()
.name("chanora-ptt-portal-probe".to_string())
.spawn(|| -> Result<u32, String> {
let conn = BlockingConnection::session()
.map_err(|e| format!("session bus: {e}"))?;
let proxy = BlockingGlobalShortcutsProxy::new(&conn)
.map_err(|e| format!("proxy: {e}"))?;
proxy
.version()
.map_err(|e| format!("portal version: {e}"))
let conn =
BlockingConnection::session().map_err(|e| format!("session bus: {e}"))?;
let proxy =
BlockingGlobalShortcutsProxy::new(&conn).map_err(|e| format!("proxy: {e}"))?;
proxy.version().map_err(|e| format!("portal version: {e}"))
})
.map_err(|e| PttBackendError::Init(format!("probe thread spawn: {e}")))?
.join()
@@ -309,9 +303,10 @@ impl DesktopPttBackend for LinuxGnomeWaylandBackend {
// Stash command + worker handles. `try_lock` is fine: the
// backend isn't yet shared, and `start` is called once at
// engine init.
let mut inner = self.inner.try_lock().map_err(|_| {
PttBackendError::Init("backend inner mutex contended".to_string())
})?;
let mut inner = self
.inner
.try_lock()
.map_err(|_| PttBackendError::Init("backend inner mutex contended".to_string()))?;
// Clean up any prior worker (defensive — `start` is
// expected to be called exactly once per backend
// instance).
@@ -546,7 +541,9 @@ async fn create_session(
.get("session_handle")
.and_then(|v| <&str>::try_from(v).ok())
.map(|s| s.to_string())
.ok_or_else(|| zbus::Error::Failure("CreateSession returned no session_handle".to_string()))?;
.ok_or_else(|| {
zbus::Error::Failure("CreateSession returned no session_handle".to_string())
})?;
Ok(zbus::zvariant::OwnedObjectPath::try_from(session_handle)
.map_err(|e| zbus::Error::Failure(format!("session_handle path parse: {e}")))?)
}
@@ -714,19 +711,13 @@ mod tests {
#[test]
fn classify_returns_keyboard_for_typical_trigger_description() {
let v = shortcuts_owned_value(vec![shortcut_entry(SHORTCUT_ID, Some("Ctrl+Alt+P"))]);
assert_eq!(
classify_shortcuts_value(&v),
Some(PttInputClass::Keyboard)
);
assert_eq!(classify_shortcuts_value(&v), Some(PttInputClass::Keyboard));
}
#[test]
fn classify_returns_keyboard_when_trigger_description_missing() {
let v = shortcuts_owned_value(vec![shortcut_entry(SHORTCUT_ID, None)]);
assert_eq!(
classify_shortcuts_value(&v),
Some(PttInputClass::Keyboard)
);
assert_eq!(classify_shortcuts_value(&v), Some(PttInputClass::Keyboard));
}
#[test]
+11 -30
View File
@@ -38,9 +38,7 @@ use std::time::Duration;
use tokio::sync::watch;
use tracing::{info, warn};
use super::{
AudioTransmitGate, DesktopPttBackend, PttBackendError, PttBinding, PttInputClass,
};
use super::{AudioTransmitGate, DesktopPttBackend, PttBackendError, PttBinding, PttInputClass};
use crate::ptt::{PttBackendDescriptor, PttCapabilityLevel};
// ---------- FFI ----------
@@ -407,15 +405,10 @@ impl MacOSEventTapBackend {
/// Build the descriptor for a given permission + binding pair
/// (used by `descriptor()` and the re-query worker).
fn build_descriptor(
permission: PermissionState,
class: PttInputClass,
) -> PttBackendDescriptor {
fn build_descriptor(permission: PermissionState, class: PttInputClass) -> PttBackendDescriptor {
let level = match permission {
PermissionState::Granted => match class {
PttInputClass::MouseSideButton => {
PttCapabilityLevel::L3GlobalWithMouseButtons
}
PttInputClass::MouseSideButton => PttCapabilityLevel::L3GlobalWithMouseButtons,
_ => PttCapabilityLevel::L2GlobalHoldToTalk,
},
// Undetermined or Denied (we wouldn't be here for
@@ -535,9 +528,8 @@ impl DesktopPttBackend for MacOSEventTapBackend {
}
return;
}
let source = unsafe {
CFMachPortCreateRunLoopSource(std::ptr::null_mut(), port, 0)
};
let source =
unsafe { CFMachPortCreateRunLoopSource(std::ptr::null_mut(), port, 0) };
if source.is_null() {
warn!(
target: "chanora_audio",
@@ -613,10 +605,7 @@ impl DesktopPttBackend for MacOSEventTapBackend {
let now = query_permission();
if now != last {
perm_atomic.store(now.to_u8(), Ordering::Relaxed);
let desc = MacOSEventTapBackend::build_descriptor(
now,
perm_binding_class,
);
let desc = MacOSEventTapBackend::build_descriptor(now, perm_binding_class);
let _ = perm_desc_tx.send(desc);
info!(
target: "chanora_audio",
@@ -711,9 +700,7 @@ extern "C" fn tap_callback(
// restart the app. The CGEvent docs explicitly say returning
// the event unchanged is the correct no-op for these
// notification types.
if etype == KCG_EVENT_TAP_DISABLED_BY_TIMEOUT
|| etype == KCG_EVENT_TAP_DISABLED_BY_USER_INPUT
{
if etype == KCG_EVENT_TAP_DISABLED_BY_TIMEOUT || etype == KCG_EVENT_TAP_DISABLED_BY_USER_INPUT {
warn!(
target: "chanora_audio",
event = "tap_disabled",
@@ -735,9 +722,7 @@ extern "C" fn tap_callback(
if bound < 0 {
return event;
}
let kc = unsafe {
CGEventGetIntegerValueField(event, KCG_KEYBOARD_EVENT_KEYCODE)
};
let kc = unsafe { CGEventGetIntegerValueField(event, KCG_KEYBOARD_EVENT_KEYCODE) };
if kc == bound as i64 {
let pressed = etype == KCG_EVENT_KEY_DOWN;
state.gate.set(pressed);
@@ -748,9 +733,7 @@ extern "C" fn tap_callback(
if bound < 0 {
return event;
}
let btn = unsafe {
CGEventGetIntegerValueField(event, KCG_MOUSE_EVENT_BUTTON_NUMBER)
};
let btn = unsafe { CGEventGetIntegerValueField(event, KCG_MOUSE_EVENT_BUTTON_NUMBER) };
if btn == bound as i64 {
let pressed = etype == KCG_EVENT_OTHER_MOUSE_DOWN;
state.gate.set(pressed);
@@ -812,10 +795,8 @@ mod tests {
#[test]
fn build_descriptor_granted_none_reports_L2_keyboard() {
let d = MacOSEventTapBackend::build_descriptor(
PermissionState::Granted,
PttInputClass::None,
);
let d =
MacOSEventTapBackend::build_descriptor(PermissionState::Granted, PttInputClass::None);
assert_eq!(d.level, PttCapabilityLevel::L2GlobalHoldToTalk);
assert_eq!(d.bound_input_class, None);
}
@@ -29,16 +29,15 @@ use windows::core::{w, PCWSTR};
use windows::Win32::Foundation::{HMODULE, HWND, LPARAM, LRESULT, WPARAM};
use windows::Win32::System::LibraryLoader::GetModuleHandleW;
use windows::Win32::UI::Input::{
GetRawInputData, RegisterRawInputDevices, HRAWINPUT, RAWINPUT, RAWINPUTDEVICE,
RAWINPUTHEADER, RID_INPUT, RIDEV_INPUTSINK, RIDEV_REMOVE, RIM_TYPEKEYBOARD, RIM_TYPEMOUSE,
GetRawInputData, RegisterRawInputDevices, HRAWINPUT, RAWINPUT, RAWINPUTDEVICE, RAWINPUTHEADER,
RIDEV_INPUTSINK, RIDEV_REMOVE, RID_INPUT, RIM_TYPEKEYBOARD, RIM_TYPEMOUSE,
};
use windows::Win32::UI::WindowsAndMessaging::{
CallNextHookEx, CreateWindowExW, DefWindowProcW, DispatchMessageW, GetMessageW,
PostThreadMessageW, RegisterClassExW, SetWindowsHookExW, TranslateMessage,
UnhookWindowsHookEx, HC_ACTION, HHOOK, HOOKPROC, KBDLLHOOKSTRUCT, MSG, MSLLHOOKSTRUCT,
WH_KEYBOARD_LL, WH_MOUSE_LL, WINDOW_EX_STYLE, WINDOW_STYLE, WM_INPUT, WM_KEYDOWN, WM_KEYUP,
WM_QUIT, WM_SYSKEYDOWN, WM_SYSKEYUP, WM_XBUTTONDOWN, WM_XBUTTONUP, WNDCLASSEXW, XBUTTON1,
XBUTTON2,
PostThreadMessageW, RegisterClassExW, SetWindowsHookExW, TranslateMessage, UnhookWindowsHookEx,
HC_ACTION, HHOOK, HOOKPROC, KBDLLHOOKSTRUCT, MSG, MSLLHOOKSTRUCT, WH_KEYBOARD_LL, WH_MOUSE_LL,
WINDOW_EX_STYLE, WINDOW_STYLE, WM_INPUT, WM_KEYDOWN, WM_KEYUP, WM_QUIT, WM_SYSKEYDOWN,
WM_SYSKEYUP, WM_XBUTTONDOWN, WM_XBUTTONUP, WNDCLASSEXW, XBUTTON1, XBUTTON2,
};
use super::{AudioTransmitGate, DesktopPttBackend, PttBackendError, PttBinding};
@@ -467,10 +466,7 @@ unsafe fn run_raw_input_loop(
hwndTarget: hwnd,
},
];
let reg_ok = RegisterRawInputDevices(
&devices,
std::mem::size_of::<RAWINPUTDEVICE>() as u32,
);
let reg_ok = RegisterRawInputDevices(&devices, std::mem::size_of::<RAWINPUTDEVICE>() as u32);
if reg_ok.is_err() {
warn!(
target: "chanora_audio",
@@ -920,11 +916,7 @@ unsafe extern "system" fn kbd_hook_proc(code: i32, wparam: WPARAM, lparam: LPARA
/// `KBDLLHOOKSTRUCT` from `lparam` then calls into this helper so
/// the tests can exercise the press-edge translation without
/// installing a global hook.
pub(crate) fn dispatch_hook_keyboard(
ctx: &HookContext,
wparam: WPARAM,
kb: &KBDLLHOOKSTRUCT,
) {
pub(crate) fn dispatch_hook_keyboard(ctx: &HookContext, wparam: WPARAM, kb: &KBDLLHOOKSTRUCT) {
if ctx.binding.class() != 1 {
return;
}
@@ -1206,10 +1198,7 @@ mod tests {
// Keyboard class + mouse-side-button key string: the
// keymap parses the string as a key label and finds no
// match → None.
let r = resolve_binding(&binding(
PttInputClass::Keyboard,
"mouse-side-button:8",
));
let r = resolve_binding(&binding(PttInputClass::Keyboard, "mouse-side-button:8"));
assert_eq!(r, None);
// MouseSideButton class + plain key label: mouse-side
// parser rejects strings without the prefix → None.