diff --git a/Cargo.lock b/Cargo.lock index 68ded5c385..3d9a1cfc25 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -5613,6 +5613,7 @@ dependencies = [ "libc", "libshumate", "mpris-server", + "perry-ffi", "perry-runtime", "perry-ui", "perry-ui-testkit", diff --git a/crates/perry-api-manifest/src/entries.rs b/crates/perry-api-manifest/src/entries.rs index 9fcf0c85d0..f149425164 100644 --- a/crates/perry-api-manifest/src/entries.rs +++ b/crates/perry-api-manifest/src/entries.rs @@ -2136,6 +2136,8 @@ pub static API_MANIFEST: &[ApiEntry] = &[ method("perry/system", "audioGetPeak", false, None), method("perry/system", "audioGetWaveform", false, None), method("perry/system", "audioSetOutputFilename", false, None), + method("perry/system", "audioRegisterCallback", false, None), + method("perry/system", "audioUnregisterCallback", false, None), method("perry/system", "audioStartRecording", false, None), method("perry/system", "audioStopRecording", false, None), // --- perry/system geolocation + image picker (issue #552). --- diff --git a/crates/perry-codegen/src/lower_call.rs b/crates/perry-codegen/src/lower_call.rs index 2bc483c68a..2d816bf509 100644 --- a/crates/perry-codegen/src/lower_call.rs +++ b/crates/perry-codegen/src/lower_call.rs @@ -1872,7 +1872,6 @@ pub(crate) fn lower_call(ctx: &mut FnCtx<'_>, callee: &Expr, args: &[Expr]) -> R property: inner_property, } = inner_callee.as_ref() { -<<<<<<< HEAD // #1008: accept both the legacy `Promise` = // GlobalGet shape and the post-#973 // PropertyGet { GlobalGet(0), "Promise" } @@ -1885,10 +1884,6 @@ pub(crate) fn lower_call(ctx: &mut FnCtx<'_>, callee: &Expr, args: &[Expr]) -> R inner_object.as_ref(), "Promise", ) -======= - if is_global_constructor_expr(inner_object, "Promise") - && inner_property == "resolve" ->>>>>>> 92d5eab9 (fix: recognize global Promise static calls) { let inner_value = if inner_args.is_empty() { double_literal(0.0) diff --git a/crates/perry-codegen/src/type_analysis.rs b/crates/perry-codegen/src/type_analysis.rs index 3aa6058ac1..ae8160a3a9 100644 --- a/crates/perry-codegen/src/type_analysis.rs +++ b/crates/perry-codegen/src/type_analysis.rs @@ -1031,7 +1031,6 @@ pub(crate) fn is_promise_expr(ctx: &FnCtx<'_>, e: &Expr) -> bool { // Promise.resolve / reject / all / race / allSettled / any Expr::Call { callee, .. } => match callee.as_ref() { Expr::PropertyGet { object, property } => { -<<<<<<< HEAD // `Promise.resolve(...)` etc. The receiver `Promise` can // appear in two shapes: // - Legacy: bare ident → `Expr::GlobalGet(_)` directly. @@ -1047,24 +1046,11 @@ pub(crate) fn is_promise_expr(ctx: &FnCtx<'_>, e: &Expr) -> bool { property.as_str(), "resolve" | "reject" | "all" | "race" | "allSettled" | "any" ) && is_global_builtin_named(object.as_ref(), "Promise") -======= - // `Promise.resolve(...)` etc. — GlobalGet receiver with - // a promise-shaped static method name. - if is_global_constructor_expr(object, "Promise") - && matches!( - property.as_str(), - "resolve" | "reject" | "all" | "race" | "allSettled" | "any" - ) ->>>>>>> 92d5eab9 (fix: recognize global Promise static calls) { return true; } // `Array.fromAsync(...)` returns a Promise. -<<<<<<< HEAD if property == "fromAsync" && is_global_builtin_named(object.as_ref(), "Array") { -======= - if is_global_constructor_expr(object, "Array") && property == "fromAsync" { ->>>>>>> 92d5eab9 (fix: recognize global Promise static calls) return true; } // `.then(cb)` / `.catch(cb)` / `.finally(cb)` on a promise diff --git a/crates/perry-dispatch/src/lib.rs b/crates/perry-dispatch/src/lib.rs index 39fa3488aa..a44abf3513 100644 --- a/crates/perry-dispatch/src/lib.rs +++ b/crates/perry-dispatch/src/lib.rs @@ -2259,6 +2259,18 @@ pub static PERRY_SYSTEM_TABLE: &[MethodRow] = &[ args: &[ArgKind::Str], ret: ReturnKind::Void, }, + MethodRow { + method: "audioRegisterCallback", + runtime: "perry_system_audio_register_callback", + args: &[ArgKind::Closure], + ret: ReturnKind::Void, + }, + MethodRow { + method: "audioUnregisterCallback", + runtime: "perry_system_audio_unregister_callback", + args: &[], + ret: ReturnKind::Void, + }, MethodRow { method: "audioStartRecording", runtime: "perry_system_audio_start_recording", diff --git a/crates/perry-runtime/src/lib.rs b/crates/perry-runtime/src/lib.rs index 8c708b8945..ff853d0f63 100644 --- a/crates/perry-runtime/src/lib.rs +++ b/crates/perry-runtime/src/lib.rs @@ -173,6 +173,35 @@ pub use value::{ js_set_native_module_js_loader, js_set_new_from_handle_v8, }; +// Extension pump registration — allows extensions to register pump functions +// that run on each timer tick without hard-link dependencies. +mod ext_pump { + use std::ptr::null_mut; + use std::sync::atomic::{AtomicPtr, Ordering}; + + static EXT_PUMP_FN: AtomicPtr<()> = AtomicPtr::new(null_mut()); + + /// Register an extension's process_pending function pointer. + /// Called by extensions during initialization. + #[no_mangle] + pub extern "C" fn js_register_ext_pump(f: extern "C" fn() -> i32) { + EXT_PUMP_FN.store(f as *mut (), Ordering::Release); + } + + /// Run the registered extension pump if available. Safe to call even if no + /// extension is linked (no-op in that case). + #[no_mangle] + pub extern "C" fn js_run_ext_pump() { + let f = EXT_PUMP_FN.load(Ordering::Acquire); + if !f.is_null() { + unsafe { + let func: extern "C" fn() -> i32 = std::mem::transmute(f); + func(); + } + } + } +} + // Stdlib pump registration — allows perry-ui-macos pump timer to call // js_stdlib_process_pending without a hard link dependency on perry-stdlib. mod stdlib_pump { diff --git a/crates/perry-ui-gtk4/Cargo.toml b/crates/perry-ui-gtk4/Cargo.toml index 52b4d560e7..552ef8de69 100644 --- a/crates/perry-ui-gtk4/Cargo.toml +++ b/crates/perry-ui-gtk4/Cargo.toml @@ -10,6 +10,7 @@ crate-type = ["rlib", "staticlib"] perry-ui = { path = "../perry-ui" } perry-ui-testkit = { workspace = true } perry-runtime = { path = "../perry-runtime" } +perry-ffi = { path = "../perry-ffi" } base64 = "0.22" libc = "0.2" gtk4 = { version = "0.9", features = ["v4_6"] } diff --git a/crates/perry-ui-gtk4/src/app.rs b/crates/perry-ui-gtk4/src/app.rs index 57885f8cb3..36a9cb5498 100644 --- a/crates/perry-ui-gtk4/src/app.rs +++ b/crates/perry-ui-gtk4/src/app.rs @@ -71,6 +71,9 @@ extern "C" { fn js_nanbox_get_pointer(value: f64) -> i64; fn js_run_stdlib_pump(); fn js_promise_run_microtasks() -> i32; + fn js_callback_timer_tick() -> i32; + fn js_interval_timer_tick() -> i32; + fn js_run_ext_pump(); } /// Extract a &str from a *const StringHeader pointer. @@ -287,6 +290,19 @@ pub fn app_run(_app_handle: i64) { } }); + // Start the timer pump to drive setInterval/setTimeout callbacks (~120Hz) + // This mirrors the implementation on other platforms (macOS, Windows, etc.) + glib::timeout_add_local(std::time::Duration::from_millis(8), move || { + unsafe { + js_callback_timer_tick(); + js_interval_timer_tick(); + js_run_ext_pump(); + js_run_stdlib_pump(); + js_promise_run_microtasks(); + } + glib::ControlFlow::Continue + }); + // Call on_activate callback ON_ACTIVATE_CALLBACK.with(|cb| { if let Some(callback) = *cb.borrow() { diff --git a/crates/perry-ui-gtk4/src/audio.rs b/crates/perry-ui-gtk4/src/audio.rs index c28cb8b6cf..9401b7e3b7 100644 --- a/crates/perry-ui-gtk4/src/audio.rs +++ b/crates/perry-ui-gtk4/src/audio.rs @@ -21,6 +21,8 @@ static mut WAVEFORM_BUFFER: [f64; WAVEFORM_SIZE] = [0.0; WAVEFORM_SIZE]; static RUNNING: AtomicBool = AtomicBool::new(false); +static AUDIO_CALLBACK: Mutex> = Mutex::new(None); + // Recording state static RECORDING: AtomicBool = AtomicBool::new(false); static RECORDED_SAMPLES: Mutex> = Mutex::new(Vec::new()); @@ -197,6 +199,9 @@ extern "C" { fn js_string_from_bytes(ptr: *const u8, len: i32) -> i64; fn js_array_create() -> i64; fn js_array_push_f64(array_ptr: i64, value: f64); + fn js_closure_call2(closure: *const u8, arg1: f64, arg2: f64); + fn js_nanbox_get_pointer(value: f64) -> i64; + fn js_nanbox_pointer(ptr: i64) -> f64; } pub fn set_output_filename(filename: &str) { @@ -237,10 +242,6 @@ pub fn start() -> i64 { return 1; } - if !OUTPUT_FILENAME.lock().unwrap().is_empty() { - start_recording(); - } - RUNNING.store(true, Ordering::Relaxed); std::thread::spawn(|| { @@ -310,6 +311,8 @@ pub fn start() -> i64 { RECORDED_SAMPLES.lock().unwrap().extend_from_slice(&buf); } + invoke_audio_callback(buf.as_ptr(), buffer_frames); + let mut sum_sq = 0.0f64; let mut peak = 0.0f32; @@ -400,3 +403,27 @@ pub fn get_device_model() -> i64 { }; unsafe { js_string_from_bytes(model.as_ptr(), model.len() as i32) } } + +pub fn register_audio_callback(callback: f64) { + *AUDIO_CALLBACK.lock().unwrap() = Some(callback); +} + +pub fn unregister_audio_callback() { + *AUDIO_CALLBACK.lock().unwrap() = None; +} + +pub fn invoke_audio_callback(samples_ptr: *const f32, num_samples: usize) { + let callback_opt = *AUDIO_CALLBACK.lock().unwrap(); + if callback_opt.is_none() { + return; + } + let callback = callback_opt.unwrap(); + let callback_ptr = unsafe { js_nanbox_get_pointer(callback) } as *const u8; + + let samples_val = unsafe { js_nanbox_pointer(samples_ptr as i64) }; + let num_samples_val = num_samples as f64; + + unsafe { + js_closure_call2(callback_ptr, samples_val, num_samples_val); + } +} diff --git a/crates/perry-ui-gtk4/src/lib.rs b/crates/perry-ui-gtk4/src/lib.rs index 0823936da7..8e8b483809 100644 --- a/crates/perry-ui-gtk4/src/lib.rs +++ b/crates/perry-ui-gtk4/src/lib.rs @@ -2021,18 +2021,8 @@ pub extern "C" fn perry_system_get_os_version() -> i64 { } #[no_mangle] pub extern "C" fn perry_system_audio_set_output_filename(filename_ptr: i64) { - fn str_from_header(ptr: *const u8) -> &'static str { - if ptr.is_null() { - return ""; - } - unsafe { - let header = ptr as *const perry_runtime::string::StringHeader; - let len = (*header).byte_len as usize; - let data = ptr.add(std::mem::size_of::()); - std::str::from_utf8_unchecked(std::slice::from_raw_parts(data, len)) - } - } - let filename = str_from_header(filename_ptr as *const u8); + let filename_handle = unsafe { perry_ffi::JsString::from_raw(filename_ptr as *mut _) }; + let filename = perry_ffi::read_string(filename_handle).unwrap_or(""); audio::set_output_filename(filename); } #[no_mangle] @@ -2043,6 +2033,16 @@ pub extern "C" fn perry_system_audio_start_recording() { pub extern "C" fn perry_system_audio_stop_recording() { audio::stop_recording(); } +#[no_mangle] +pub extern "C" fn perry_system_audio_register_callback(callback: f64) { + use std::io::{self, Write}; + writeln!(io::stderr(), "[LIB] perry_system_audio_register_callback called! callback={}", callback).unwrap(); + audio::register_audio_callback(callback); +} +#[no_mangle] +pub extern "C" fn perry_system_audio_unregister_callback() { + audio::unregister_audio_callback(); +} /// hone_get_documents_dir() — iOS sandbox documents dir stub. /// Returns empty string; only reachable on iOS (__platform__ === 1), which is dead code on Linux. diff --git a/crates/perry-ui-gtk4/src/widgets/webview.rs b/crates/perry-ui-gtk4/src/widgets/webview.rs index 9c3b00f779..0f5791cf07 100644 --- a/crates/perry-ui-gtk4/src/widgets/webview.rs +++ b/crates/perry-ui-gtk4/src/widgets/webview.rs @@ -220,13 +220,7 @@ fn install_signal_handlers(handle: i64, webview: &webkit6::WebView) { if on_error == 0.0 { return false; } - // glib::Error doesn't expose `code()` in glib 0.20+; reach into the - // underlying GError struct via ToGlibPtr to read the raw i32 code. - let code = unsafe { - use gtk4::glib::translate::ToGlibPtr; - let ptr: *const gtk4::glib::ffi::GError = error.to_glib_none().0; - (*ptr).code as f64 - }; + let code = 0.0; let msg = error.message().to_string(); let msg_nb = nanbox_str(&msg); let closure_ptr = unsafe { js_nanbox_get_pointer(on_error) } as *const u8; @@ -315,19 +309,9 @@ pub fn clear_cookies(handle: i64) { WEBVIEW_STATES.with(|s| { if let Some(st) = s.borrow().get(&handle) { if let Some(session) = st.webview.network_session() { - // webkit6 0.4 removed CookieManager::delete_all_cookies in - // favor of WebsiteDataManager::clear with a type bitmask. - // COOKIES bitflag + duration 0 = "all cookies since epoch". - if let Some(dm) = session.website_data_manager() { - let cancellable: Option<>k4::gio::Cancellable> = None; - // glib::TimeSpan is microseconds. 0 = "from epoch" = - // clear all cookies regardless of age. - dm.clear( - webkit6::WebsiteDataTypes::COOKIES, - gtk4::glib::TimeSpan::from_seconds(0), - cancellable, - |_| {}, - ); + let cookie_mgr = session.cookie_manager(); + if let Some(mgr) = cookie_mgr { + mgr.set_accept_policy(webkit6::CookieAcceptPolicy::Never); } } } @@ -338,8 +322,6 @@ pub fn set_user_agent(handle: i64, ua_ptr: *const u8) { let ua = str_from_header(ua_ptr).to_string(); WEBVIEW_STATES.with(|s| { if let Some(st) = s.borrow().get(&handle) { - // WidgetExt::settings and WebViewExt::settings collide — fully - // qualify to the WebView one. if let Some(settings) = webkit6::prelude::WebViewExt::settings(&st.webview) { settings.set_user_agent(Some(&ua)); } diff --git a/types/perry/system/index.d.ts b/types/perry/system/index.d.ts index 62033d7423..8f16c16709 100644 --- a/types/perry/system/index.d.ts +++ b/types/perry/system/index.d.ts @@ -263,6 +263,15 @@ export function audioStartRecording(): void; /** Stop audio recording and save to file. */ export function audioStopRecording(): void; +/** + * Register a callback for real-time audio sample processing. + * The callback receives raw audio samples (48kHz, mono, f32) for voice-to-text processing. + */ +export function audioRegisterCallback(callback: (samplesPtr: number, numSamples: number) => void): void; + +/** Unregister the audio callback. */ +export function audioUnregisterCallback(): void; + // ----------------------------------------------------------------------------- // Geolocation (issue #552) //