Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions Cargo.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

2 changes: 2 additions & 0 deletions crates/perry-api-manifest/src/entries.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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). ---
Expand Down
5 changes: 0 additions & 5 deletions crates/perry-codegen/src/lower_call.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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" }
Expand All @@ -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)
Expand Down
14 changes: 0 additions & 14 deletions crates/perry-codegen/src/type_analysis.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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.
Expand All @@ -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<Array>.
<<<<<<< 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
Expand Down
12 changes: 12 additions & 0 deletions crates/perry-dispatch/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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",
Expand Down
29 changes: 29 additions & 0 deletions crates/perry-runtime/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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 {
Expand Down
1 change: 1 addition & 0 deletions crates/perry-ui-gtk4/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -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"] }
Expand Down
16 changes: 16 additions & 0 deletions crates/perry-ui-gtk4/src/app.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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.
Expand Down Expand Up @@ -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() {
Expand Down
35 changes: 31 additions & 4 deletions crates/perry-ui-gtk4/src/audio.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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<Option<f64>> = Mutex::new(None);

// Recording state
static RECORDING: AtomicBool = AtomicBool::new(false);
static RECORDED_SAMPLES: Mutex<Vec<f32>> = Mutex::new(Vec::new());
Expand Down Expand Up @@ -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) {
Expand Down Expand Up @@ -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(|| {
Expand Down Expand Up @@ -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;

Expand Down Expand Up @@ -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);
}
}
24 changes: 12 additions & 12 deletions crates/perry-ui-gtk4/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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::<perry_runtime::string::StringHeader>());
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]
Expand All @@ -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.
Expand Down
26 changes: 4 additions & 22 deletions crates/perry-ui-gtk4/src/widgets/webview.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down Expand Up @@ -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<&gtk4::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);
}
}
}
Expand All @@ -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));
}
Expand Down
9 changes: 9 additions & 0 deletions types/perry/system/index.d.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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)
//
Expand Down
Loading