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
7 changes: 7 additions & 0 deletions Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -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",
] }
19 changes: 10 additions & 9 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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

Expand Down
6 changes: 3 additions & 3 deletions src/backend/android.rs
Original file line number Diff line number Diff line change
@@ -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;

Expand Down
2 changes: 1 addition & 1 deletion src/backend/general.rs
Original file line number Diff line number Diff line change
@@ -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.
///
Expand Down
6 changes: 3 additions & 3 deletions src/backend/ios.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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`.
///
Expand Down
2 changes: 1 addition & 1 deletion src/backend/macos/mod.rs
Original file line number Diff line number Diff line change
@@ -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");
Expand Down
7 changes: 7 additions & 0 deletions src/backend/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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.
Expand Down Expand Up @@ -162,6 +167,8 @@ pub fn default_backend() -> Box<dyn Backend> {
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())
}
Expand Down
212 changes: 212 additions & 0 deletions src/backend/ohos.rs
Original file line number Diff line number Diff line change
@@ -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<dyn FnOnce(io::Result<Option<String>>) + Send>;

static REQUEST_CALLBACK: OnceLock<
ThreadsafeFunction<InputBoxRequest, (), InputBoxRequest, napi_ohos::Status, false, false, 16>,
> = OnceLock::new();

#[napi(object)]
#[derive(Clone)]
pub struct InputBoxRequest {
pub callback: i64,
pub title: String,
pub prompt: Option<String>,
pub default_value: String,
pub mode: String,
pub ok_label: String,
pub cancel_label: String,
pub width: Option<u32>,
pub height: Option<u32>,
pub auto_wrap: bool,
pub scroll_to_end: bool,
}

#[allow(dead_code)]
#[napi(object)]
pub struct InputBoxResponse {
pub callback: i64,
pub text: Option<String>,
pub error: Option<String>,
}

/// 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<dyn FnOnce(io::Result<Option<String>>) + 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<InputBoxRequest, ()>,
) -> 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));
}
}
2 changes: 1 addition & 1 deletion src/backend/windows/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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");

Expand Down
2 changes: 1 addition & 1 deletion src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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";
Expand Down
Loading