diff --git a/Cargo.toml b/Cargo.toml index 563fdcf..ea35a11 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -38,3 +38,10 @@ objc2 = "0.6.4" objc2-core-foundation = "0.3.2" objc2-foundation = "0.3.2" objc2-ui-kit = "0.3.2" + +[target.'cfg(target_env = "ohos")'.dependencies] +napi-derive-ohos = { version = "1.1.3" } +napi-ohos = { version = "1.1.3", default-features = false, features = [ + "napi8", + "async", +] } diff --git a/README.md b/README.md index 61e676c..6777149 100644 --- a/README.md +++ b/README.md @@ -106,20 +106,21 @@ InputBox::new() - **Multiple input modes** — text, password, or multiline - **Highly customizable** — title, prompt, button labels, dimensions, and more -- **Works on most platforms** — Windows, macOS, Linux, Android, and iOS +- **Works on most platforms** — Windows, macOS, Linux, Android, iOS nad OpenHarmony - **Pluggable backends** — use a specific backend or let the library pick - **Synchronous and asynchronous** — safe sync on most platforms, async required on iOS ## Backends -| Backend | Platform | How it works | -| ----------- | -------- | ----------------------------------------------- | -| `PSScript` | Windows | PowerShell + WinForms, no extra install needed | -| `JXAScript` | macOS | `osascript` JXA, built into the OS | -| `Android` | Android | AAR + JNI to show an Android AlertDialog | -| `IOS` | iOS | UIKit alert | -| `Yad` | Linux | [`yad`](https://github.com/v1cont/yad) | -| `Zenity` | Linux | `zenity` — fallback on GNOME systems | +| Backend | Platform | How it works | +| ----------- | ----------- | ----------------------------------------------- | +| `PSScript` | Windows | PowerShell + WinForms, no extra install needed | +| `JXAScript` | macOS | `osascript` JXA, built into the OS | +| `Android` | Android | AAR + JNI to show an Android AlertDialog | +| `IOS` | iOS | UIKit alert | +| `OHOS` | OpenHarmony | NAPI + ArkTS dialog | +| `Yad` | Linux | [`yad`](https://github.com/v1cont/yad) | +| `Zenity` | Linux | `zenity` — fallback on GNOME systems | ### Linux Installation diff --git a/src/backend/android.rs b/src/backend/android.rs index 6825fd7..6730f15 100644 --- a/src/backend/android.rs +++ b/src/backend/android.rs @@ -1,17 +1,17 @@ use std::io; use jni::{ - Env, EnvUnowned, JavaVM, Outcome, errors::ThrowRuntimeExAndDefault, jni_sig, jni_str, objects::{JClass, JObject, JString, JValue}, refs::Global, signature::MethodSignature, - sys::{JNIEnv, jlong}, + sys::{jlong, JNIEnv}, + Env, EnvUnowned, JavaVM, Outcome, }; use once_cell::sync::OnceCell; -use crate::{DEFAULT_CANCEL_LABEL, DEFAULT_OK_LABEL, DEFAULT_TITLE, InputBox}; +use crate::{InputBox, DEFAULT_CANCEL_LABEL, DEFAULT_OK_LABEL, DEFAULT_TITLE}; use super::Backend; diff --git a/src/backend/general.rs b/src/backend/general.rs index c59128b..ef26558 100644 --- a/src/backend/general.rs +++ b/src/backend/general.rs @@ -1,6 +1,6 @@ use std::{borrow::Cow, path::Path, process::Command}; -use crate::{InputBox, InputMode, backend::CommandBackend}; +use crate::{backend::CommandBackend, InputBox, InputMode}; /// Zenity backend. /// diff --git a/src/backend/ios.rs b/src/backend/ios.rs index 28f44bd..65d89ee 100644 --- a/src/backend/ios.rs +++ b/src/backend/ios.rs @@ -6,16 +6,16 @@ use std::{ }; use block2::StackBlock; -use objc2::{MainThreadMarker, rc::Retained}; +use objc2::{rc::Retained, MainThreadMarker}; use objc2_core_foundation::{CGFloat, CGRect, CGSize}; -use objc2_foundation::{NSArray, NSObjectNSKeyValueCoding, NSRange, NSString, ns_string}; +use objc2_foundation::{ns_string, NSArray, NSObjectNSKeyValueCoding, NSRange, NSString}; use objc2_ui_kit::{ NSLayoutConstraint, UIAlertAction, UIAlertActionStyle, UIAlertController, UIAlertControllerStyle, UIApplication, UIFont, UITextField, UITextInputTraits, UITextView, UIViewController, UIWindowScene, }; -use crate::{DEFAULT_CANCEL_LABEL, DEFAULT_OK_LABEL, DEFAULT_TITLE, InputMode, backend::Backend}; +use crate::{backend::Backend, InputMode, DEFAULT_CANCEL_LABEL, DEFAULT_OK_LABEL, DEFAULT_TITLE}; /// iOS backend for InputBox using `UIAlertController`. /// diff --git a/src/backend/macos/mod.rs b/src/backend/macos/mod.rs index cd56bcf..1067f52 100644 --- a/src/backend/macos/mod.rs +++ b/src/backend/macos/mod.rs @@ -1,7 +1,7 @@ use std::{borrow::Cow, path::Path, process::Command}; use crate::{ - DEFAULT_CANCEL_LABEL, DEFAULT_OK_LABEL, DEFAULT_TITLE, InputBox, backend::CommandBackend, + backend::CommandBackend, InputBox, DEFAULT_CANCEL_LABEL, DEFAULT_OK_LABEL, DEFAULT_TITLE, }; const JXA_SCRIPT: &str = include_str!("inputbox.jxa.js"); diff --git a/src/backend/mod.rs b/src/backend/mod.rs index 5a475cd..83349c8 100644 --- a/src/backend/mod.rs +++ b/src/backend/mod.rs @@ -54,6 +54,11 @@ mod ios; #[cfg(target_os = "ios")] pub use ios::IOS; +#[cfg(target_env = "ohos")] +mod ohos; +#[cfg(target_env = "ohos")] +pub use ohos::OHOS; + /// Trait for platform-specific input box backends. /// /// Implement this trait to add support for different dialog implementations. @@ -162,6 +167,8 @@ pub fn default_backend() -> Box { Box::new(Android::default()) } else if #[cfg(target_os = "ios")] { Box::new(IOS::default()) + } else if #[cfg(target_env = "ohos")] { + Box::new(OHOS::default()) } else { Box::new(Zenity::default()) } diff --git a/src/backend/ohos.rs b/src/backend/ohos.rs new file mode 100644 index 0000000..a8eccd3 --- /dev/null +++ b/src/backend/ohos.rs @@ -0,0 +1,212 @@ +//! OHOS (OpenHarmony) backend for InputBox. +//! +//! This backend uses NAPI to communicate with ArkTS layer for showing native +//! dialogs. + +use std::{io, sync::OnceLock}; + +use napi_derive_ohos::napi; +use napi_ohos::{ + bindgen_prelude::*, + threadsafe_function::{ThreadsafeFunction, ThreadsafeFunctionCallMode}, +}; + +use super::Backend; +use crate::{InputBox, DEFAULT_CANCEL_LABEL, DEFAULT_OK_LABEL, DEFAULT_TITLE}; + +type Callback = Box>) + Send>; + +static REQUEST_CALLBACK: OnceLock< + ThreadsafeFunction, +> = OnceLock::new(); + +#[napi(object)] +#[derive(Clone)] +pub struct InputBoxRequest { + pub callback: i64, + pub title: String, + pub prompt: Option, + pub default_value: String, + pub mode: String, + pub ok_label: String, + pub cancel_label: String, + pub width: Option, + pub height: Option, + pub auto_wrap: bool, + pub scroll_to_end: bool, +} + +#[allow(dead_code)] +#[napi(object)] +pub struct InputBoxResponse { + pub callback: i64, + pub text: Option, + pub error: Option, +} + +/// OHOS backend for InputBox. +/// +/// This backend uses NAPI to call into ArkTS layer for showing native dialogs. +/// +/// # Setup +/// +/// To use this backend, you need to: +/// +/// 1. Import this native library in your ArkTS code. +/// 2. Call [`register_inputbox_callback`] to register the request handler. +/// 3. Implement the dialog display logic in ArkTS. +/// +/// # ArkTS Integration Example +/// +/// ```typescript +/// import inputbox from 'libinputbox.so'; +/// +/// // Register the callback handler +/// inputbox.registerInputboxCallback((request: InputBoxRequest) => { +/// // Show your custom dialog using request.title, request.prompt, etc. +/// // When user confirms or cancels, call: +/// inputbox.onInputboxResponse({ +/// callback: request.callback, +/// text: userInput, // or null if cancelled +/// error: null +/// }); +/// }); +/// ``` +/// +/// # Limitations +/// +/// - `width` and `height` are hints only and may be ignored. +/// +/// # Defaults +/// +/// - `title`: `DEFAULT_TITLE` +/// - `prompt`: empty +/// - `cancel_label`: `DEFAULT_CANCEL_LABEL` +/// - `ok_label`: `DEFAULT_OK_LABEL` +#[derive(Default, Debug, Clone)] +pub struct OHOS { + _priv: (), +} + +impl OHOS { + pub fn new() -> Self { + Self::default() + } +} + +impl Backend for OHOS { + fn execute_async( + &self, + input: &InputBox, + callback: Box>) + Send>, + ) -> io::Result<()> { + let tsfn = REQUEST_CALLBACK.get().ok_or_else(|| { + io::Error::new( + io::ErrorKind::Other, + "OHOS callback not registered. Call registerInputboxCallback from ArkTS first.", + ) + })?; + let callback_ptr = Box::into_raw(Box::new(callback)); + let request = InputBoxRequest { + callback: callback_ptr as i64, + title: input.title.as_deref().unwrap_or(DEFAULT_TITLE).to_string(), + prompt: input.prompt.as_deref().map(|s| s.to_string()), + default_value: input.default.to_string(), + mode: input.mode.as_str().to_owned(), + ok_label: input + .ok_label + .as_deref() + .unwrap_or(DEFAULT_OK_LABEL) + .to_string(), + cancel_label: input + .cancel_label + .as_deref() + .unwrap_or(DEFAULT_CANCEL_LABEL) + .to_string(), + width: input.width, + height: input.height, + auto_wrap: input.auto_wrap, + scroll_to_end: input.scroll_to_end, + }; + + // Send request to ArkTS layer + let status = tsfn.call(request, ThreadsafeFunctionCallMode::NonBlocking); + if status != napi_ohos::Status::Ok { + // Recover and invoke callback if send failed + let callback = unsafe { Box::from_raw(callback_ptr) }; + callback(Err(io::Error::new( + io::ErrorKind::Other, + format!("Failed to send request to ArkTS: {:?}", status), + ))); + } + + Ok(()) + } +} + +/// Register the ArkTS callback handler for input box requests. +/// +/// This function must be called from ArkTS before using the InputBox API. The +/// callback will receive [`InputBoxRequest`] objects when `show()` is called. +/// +/// # Example +/// +/// ```typescript +/// import inputbox from 'libinputbox.so'; +/// +/// inputbox.registerInputboxCallback((request) => { +/// // Display dialog and handle user input +/// }); +/// ``` +#[allow(dead_code)] +#[napi] +pub fn register_inputbox_callback( + callback: Function, +) -> napi_ohos::Result<()> { + let tsfn = callback + .build_threadsafe_function() + .max_queue_size::<16>() + .build()?; + + REQUEST_CALLBACK + .set(tsfn) + .map_err(|_| napi_ohos::Error::from_reason("Callback already registered"))?; + + Ok(()) +} + +/// Handle response from ArkTS layer. +/// +/// This function should be called from ArkTS when the user completes or cancels +/// the input dialog. +/// +/// # Example +/// +/// ```typescript +/// import inputbox from 'libinputbox.so'; +/// +/// // When user clicks OK: +/// inputbox.onInputboxResponse({ +/// callback: request.callback, +/// text: userInputText, +/// error: null +/// }); +/// +/// // When user clicks Cancel: +/// inputbox.onInputboxResponse({ +/// callback: request.callback, +/// text: null, +/// error: null +/// }); +/// ``` +#[allow(dead_code)] +#[napi] +pub fn on_inputbox_response(response: InputBoxResponse) { + let callback = unsafe { Box::from_raw(response.callback as *mut Callback) }; + + if let Some(error) = response.error { + callback(Err(io::Error::new(io::ErrorKind::Other, error))); + } else { + callback(Ok(response.text)); + } +} diff --git a/src/backend/windows/mod.rs b/src/backend/windows/mod.rs index 7ffb45e..8e7b8ef 100644 --- a/src/backend/windows/mod.rs +++ b/src/backend/windows/mod.rs @@ -2,7 +2,7 @@ use std::{borrow::Cow, path::Path, process::Command}; use serde_json::json; -use crate::{DEFAULT_CANCEL_LABEL, DEFAULT_OK_LABEL, InputBox, backend::CommandBackend}; +use crate::{backend::CommandBackend, InputBox, DEFAULT_CANCEL_LABEL, DEFAULT_OK_LABEL}; const PS_SCRIPT: &str = include_str!("inputbox.ps1"); diff --git a/src/lib.rs b/src/lib.rs index 59ff500..d87c10f 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -34,7 +34,7 @@ pub mod backend; use std::{borrow::Cow, io}; -use crate::backend::{Backend, default_backend}; +use crate::backend::{default_backend, Backend}; /// Default title for the input box dialog. pub const DEFAULT_TITLE: &str = "Input";