Skip to content

Commit dc4976a

Browse files
ljlVinkMivik
andauthored
feat: add initial ohos support (#1)
* feat: add initial ohos support * fix: pr review * doc: adjust text wrap * fix: msrv compatibility * style: use as_str * fix: ohos fix * style: various issues * style: organize import * doc: add ohos --------- Co-authored-by: mivik <mivikq@gmail.com>
1 parent 608956d commit dc4976a

10 files changed

Lines changed: 246 additions & 19 deletions

File tree

Cargo.toml

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -38,3 +38,10 @@ objc2 = "0.6.4"
3838
objc2-core-foundation = "0.3.2"
3939
objc2-foundation = "0.3.2"
4040
objc2-ui-kit = "0.3.2"
41+
42+
[target.'cfg(target_env = "ohos")'.dependencies]
43+
napi-derive-ohos = { version = "1.1.3" }
44+
napi-ohos = { version = "1.1.3", default-features = false, features = [
45+
"napi8",
46+
"async",
47+
] }

README.md

Lines changed: 10 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -106,20 +106,21 @@ InputBox::new()
106106

107107
- **Multiple input modes** — text, password, or multiline
108108
- **Highly customizable** — title, prompt, button labels, dimensions, and more
109-
- **Works on most platforms** — Windows, macOS, Linux, Android, and iOS
109+
- **Works on most platforms** — Windows, macOS, Linux, Android, iOS nad OpenHarmony
110110
- **Pluggable backends** — use a specific backend or let the library pick
111111
- **Synchronous and asynchronous** — safe sync on most platforms, async required on iOS
112112

113113
## Backends
114114

115-
| Backend | Platform | How it works |
116-
| ----------- | -------- | ----------------------------------------------- |
117-
| `PSScript` | Windows | PowerShell + WinForms, no extra install needed |
118-
| `JXAScript` | macOS | `osascript` JXA, built into the OS |
119-
| `Android` | Android | AAR + JNI to show an Android AlertDialog |
120-
| `IOS` | iOS | UIKit alert |
121-
| `Yad` | Linux | [`yad`](https://github.com/v1cont/yad) |
122-
| `Zenity` | Linux | `zenity` — fallback on GNOME systems |
115+
| Backend | Platform | How it works |
116+
| ----------- | ----------- | ----------------------------------------------- |
117+
| `PSScript` | Windows | PowerShell + WinForms, no extra install needed |
118+
| `JXAScript` | macOS | `osascript` JXA, built into the OS |
119+
| `Android` | Android | AAR + JNI to show an Android AlertDialog |
120+
| `IOS` | iOS | UIKit alert |
121+
| `OHOS` | OpenHarmony | NAPI + ArkTS dialog |
122+
| `Yad` | Linux | [`yad`](https://github.com/v1cont/yad) |
123+
| `Zenity` | Linux | `zenity` — fallback on GNOME systems |
123124

124125
### Linux Installation
125126

src/backend/android.rs

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,17 +1,17 @@
11
use std::io;
22

33
use jni::{
4-
Env, EnvUnowned, JavaVM, Outcome,
54
errors::ThrowRuntimeExAndDefault,
65
jni_sig, jni_str,
76
objects::{JClass, JObject, JString, JValue},
87
refs::Global,
98
signature::MethodSignature,
10-
sys::{JNIEnv, jlong},
9+
sys::{jlong, JNIEnv},
10+
Env, EnvUnowned, JavaVM, Outcome,
1111
};
1212
use once_cell::sync::OnceCell;
1313

14-
use crate::{DEFAULT_CANCEL_LABEL, DEFAULT_OK_LABEL, DEFAULT_TITLE, InputBox};
14+
use crate::{InputBox, DEFAULT_CANCEL_LABEL, DEFAULT_OK_LABEL, DEFAULT_TITLE};
1515

1616
use super::Backend;
1717

src/backend/general.rs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
use std::{borrow::Cow, path::Path, process::Command};
22

3-
use crate::{InputBox, InputMode, backend::CommandBackend};
3+
use crate::{backend::CommandBackend, InputBox, InputMode};
44

55
/// Zenity backend.
66
///

src/backend/ios.rs

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -6,16 +6,16 @@ use std::{
66
};
77

88
use block2::StackBlock;
9-
use objc2::{MainThreadMarker, rc::Retained};
9+
use objc2::{rc::Retained, MainThreadMarker};
1010
use objc2_core_foundation::{CGFloat, CGRect, CGSize};
11-
use objc2_foundation::{NSArray, NSObjectNSKeyValueCoding, NSRange, NSString, ns_string};
11+
use objc2_foundation::{ns_string, NSArray, NSObjectNSKeyValueCoding, NSRange, NSString};
1212
use objc2_ui_kit::{
1313
NSLayoutConstraint, UIAlertAction, UIAlertActionStyle, UIAlertController,
1414
UIAlertControllerStyle, UIApplication, UIFont, UITextField, UITextInputTraits, UITextView,
1515
UIViewController, UIWindowScene,
1616
};
1717

18-
use crate::{DEFAULT_CANCEL_LABEL, DEFAULT_OK_LABEL, DEFAULT_TITLE, InputMode, backend::Backend};
18+
use crate::{backend::Backend, InputMode, DEFAULT_CANCEL_LABEL, DEFAULT_OK_LABEL, DEFAULT_TITLE};
1919

2020
/// iOS backend for InputBox using `UIAlertController`.
2121
///

src/backend/macos/mod.rs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
use std::{borrow::Cow, path::Path, process::Command};
22

33
use crate::{
4-
DEFAULT_CANCEL_LABEL, DEFAULT_OK_LABEL, DEFAULT_TITLE, InputBox, backend::CommandBackend,
4+
backend::CommandBackend, InputBox, DEFAULT_CANCEL_LABEL, DEFAULT_OK_LABEL, DEFAULT_TITLE,
55
};
66

77
const JXA_SCRIPT: &str = include_str!("inputbox.jxa.js");

src/backend/mod.rs

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -54,6 +54,11 @@ mod ios;
5454
#[cfg(target_os = "ios")]
5555
pub use ios::IOS;
5656

57+
#[cfg(target_env = "ohos")]
58+
mod ohos;
59+
#[cfg(target_env = "ohos")]
60+
pub use ohos::OHOS;
61+
5762
/// Trait for platform-specific input box backends.
5863
///
5964
/// Implement this trait to add support for different dialog implementations.
@@ -162,6 +167,8 @@ pub fn default_backend() -> Box<dyn Backend> {
162167
Box::new(Android::default())
163168
} else if #[cfg(target_os = "ios")] {
164169
Box::new(IOS::default())
170+
} else if #[cfg(target_env = "ohos")] {
171+
Box::new(OHOS::default())
165172
} else {
166173
Box::new(Zenity::default())
167174
}

src/backend/ohos.rs

Lines changed: 212 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,212 @@
1+
//! OHOS (OpenHarmony) backend for InputBox.
2+
//!
3+
//! This backend uses NAPI to communicate with ArkTS layer for showing native
4+
//! dialogs.
5+
6+
use std::{io, sync::OnceLock};
7+
8+
use napi_derive_ohos::napi;
9+
use napi_ohos::{
10+
bindgen_prelude::*,
11+
threadsafe_function::{ThreadsafeFunction, ThreadsafeFunctionCallMode},
12+
};
13+
14+
use super::Backend;
15+
use crate::{InputBox, DEFAULT_CANCEL_LABEL, DEFAULT_OK_LABEL, DEFAULT_TITLE};
16+
17+
type Callback = Box<dyn FnOnce(io::Result<Option<String>>) + Send>;
18+
19+
static REQUEST_CALLBACK: OnceLock<
20+
ThreadsafeFunction<InputBoxRequest, (), InputBoxRequest, napi_ohos::Status, false, false, 16>,
21+
> = OnceLock::new();
22+
23+
#[napi(object)]
24+
#[derive(Clone)]
25+
pub struct InputBoxRequest {
26+
pub callback: i64,
27+
pub title: String,
28+
pub prompt: Option<String>,
29+
pub default_value: String,
30+
pub mode: String,
31+
pub ok_label: String,
32+
pub cancel_label: String,
33+
pub width: Option<u32>,
34+
pub height: Option<u32>,
35+
pub auto_wrap: bool,
36+
pub scroll_to_end: bool,
37+
}
38+
39+
#[allow(dead_code)]
40+
#[napi(object)]
41+
pub struct InputBoxResponse {
42+
pub callback: i64,
43+
pub text: Option<String>,
44+
pub error: Option<String>,
45+
}
46+
47+
/// OHOS backend for InputBox.
48+
///
49+
/// This backend uses NAPI to call into ArkTS layer for showing native dialogs.
50+
///
51+
/// # Setup
52+
///
53+
/// To use this backend, you need to:
54+
///
55+
/// 1. Import this native library in your ArkTS code.
56+
/// 2. Call [`register_inputbox_callback`] to register the request handler.
57+
/// 3. Implement the dialog display logic in ArkTS.
58+
///
59+
/// # ArkTS Integration Example
60+
///
61+
/// ```typescript
62+
/// import inputbox from 'libinputbox.so';
63+
///
64+
/// // Register the callback handler
65+
/// inputbox.registerInputboxCallback((request: InputBoxRequest) => {
66+
/// // Show your custom dialog using request.title, request.prompt, etc.
67+
/// // When user confirms or cancels, call:
68+
/// inputbox.onInputboxResponse({
69+
/// callback: request.callback,
70+
/// text: userInput, // or null if cancelled
71+
/// error: null
72+
/// });
73+
/// });
74+
/// ```
75+
///
76+
/// # Limitations
77+
///
78+
/// - `width` and `height` are hints only and may be ignored.
79+
///
80+
/// # Defaults
81+
///
82+
/// - `title`: `DEFAULT_TITLE`
83+
/// - `prompt`: empty
84+
/// - `cancel_label`: `DEFAULT_CANCEL_LABEL`
85+
/// - `ok_label`: `DEFAULT_OK_LABEL`
86+
#[derive(Default, Debug, Clone)]
87+
pub struct OHOS {
88+
_priv: (),
89+
}
90+
91+
impl OHOS {
92+
pub fn new() -> Self {
93+
Self::default()
94+
}
95+
}
96+
97+
impl Backend for OHOS {
98+
fn execute_async(
99+
&self,
100+
input: &InputBox,
101+
callback: Box<dyn FnOnce(io::Result<Option<String>>) + Send>,
102+
) -> io::Result<()> {
103+
let tsfn = REQUEST_CALLBACK.get().ok_or_else(|| {
104+
io::Error::new(
105+
io::ErrorKind::Other,
106+
"OHOS callback not registered. Call registerInputboxCallback from ArkTS first.",
107+
)
108+
})?;
109+
let callback_ptr = Box::into_raw(Box::new(callback));
110+
let request = InputBoxRequest {
111+
callback: callback_ptr as i64,
112+
title: input.title.as_deref().unwrap_or(DEFAULT_TITLE).to_string(),
113+
prompt: input.prompt.as_deref().map(|s| s.to_string()),
114+
default_value: input.default.to_string(),
115+
mode: input.mode.as_str().to_owned(),
116+
ok_label: input
117+
.ok_label
118+
.as_deref()
119+
.unwrap_or(DEFAULT_OK_LABEL)
120+
.to_string(),
121+
cancel_label: input
122+
.cancel_label
123+
.as_deref()
124+
.unwrap_or(DEFAULT_CANCEL_LABEL)
125+
.to_string(),
126+
width: input.width,
127+
height: input.height,
128+
auto_wrap: input.auto_wrap,
129+
scroll_to_end: input.scroll_to_end,
130+
};
131+
132+
// Send request to ArkTS layer
133+
let status = tsfn.call(request, ThreadsafeFunctionCallMode::NonBlocking);
134+
if status != napi_ohos::Status::Ok {
135+
// Recover and invoke callback if send failed
136+
let callback = unsafe { Box::from_raw(callback_ptr) };
137+
callback(Err(io::Error::new(
138+
io::ErrorKind::Other,
139+
format!("Failed to send request to ArkTS: {:?}", status),
140+
)));
141+
}
142+
143+
Ok(())
144+
}
145+
}
146+
147+
/// Register the ArkTS callback handler for input box requests.
148+
///
149+
/// This function must be called from ArkTS before using the InputBox API. The
150+
/// callback will receive [`InputBoxRequest`] objects when `show()` is called.
151+
///
152+
/// # Example
153+
///
154+
/// ```typescript
155+
/// import inputbox from 'libinputbox.so';
156+
///
157+
/// inputbox.registerInputboxCallback((request) => {
158+
/// // Display dialog and handle user input
159+
/// });
160+
/// ```
161+
#[allow(dead_code)]
162+
#[napi]
163+
pub fn register_inputbox_callback(
164+
callback: Function<InputBoxRequest, ()>,
165+
) -> napi_ohos::Result<()> {
166+
let tsfn = callback
167+
.build_threadsafe_function()
168+
.max_queue_size::<16>()
169+
.build()?;
170+
171+
REQUEST_CALLBACK
172+
.set(tsfn)
173+
.map_err(|_| napi_ohos::Error::from_reason("Callback already registered"))?;
174+
175+
Ok(())
176+
}
177+
178+
/// Handle response from ArkTS layer.
179+
///
180+
/// This function should be called from ArkTS when the user completes or cancels
181+
/// the input dialog.
182+
///
183+
/// # Example
184+
///
185+
/// ```typescript
186+
/// import inputbox from 'libinputbox.so';
187+
///
188+
/// // When user clicks OK:
189+
/// inputbox.onInputboxResponse({
190+
/// callback: request.callback,
191+
/// text: userInputText,
192+
/// error: null
193+
/// });
194+
///
195+
/// // When user clicks Cancel:
196+
/// inputbox.onInputboxResponse({
197+
/// callback: request.callback,
198+
/// text: null,
199+
/// error: null
200+
/// });
201+
/// ```
202+
#[allow(dead_code)]
203+
#[napi]
204+
pub fn on_inputbox_response(response: InputBoxResponse) {
205+
let callback = unsafe { Box::from_raw(response.callback as *mut Callback) };
206+
207+
if let Some(error) = response.error {
208+
callback(Err(io::Error::new(io::ErrorKind::Other, error)));
209+
} else {
210+
callback(Ok(response.text));
211+
}
212+
}

src/backend/windows/mod.rs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,7 @@ use std::{borrow::Cow, path::Path, process::Command};
22

33
use serde_json::json;
44

5-
use crate::{DEFAULT_CANCEL_LABEL, DEFAULT_OK_LABEL, InputBox, backend::CommandBackend};
5+
use crate::{backend::CommandBackend, InputBox, DEFAULT_CANCEL_LABEL, DEFAULT_OK_LABEL};
66

77
const PS_SCRIPT: &str = include_str!("inputbox.ps1");
88

src/lib.rs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -34,7 +34,7 @@ pub mod backend;
3434

3535
use std::{borrow::Cow, io};
3636

37-
use crate::backend::{Backend, default_backend};
37+
use crate::backend::{default_backend, Backend};
3838

3939
/// Default title for the input box dialog.
4040
pub const DEFAULT_TITLE: &str = "Input";

0 commit comments

Comments
 (0)