use crate::compat::String;
use crate::platform::clipboard::{ClipboardContent, RichClipboardBackend};
const PLAIN_TEXT_TYPE: &str = "text/plain";
const HTML_TYPE: &str = "text/html";
#[cfg(target_env = "ohos")]
mod sys {
use crate::compat::{String, Vec};
use core::ffi::{c_char, CStr};
const OHOS_OK: i32 = 0;
#[repr(C)]
#[allow(non_camel_case_types)]
pub(super) struct OhPasteboard {
_private: [u8; 0],
}
#[repr(C)]
#[allow(non_camel_case_types)]
pub(super) struct OhUdmfData {
_private: [u8; 0],
}
#[repr(C)]
#[allow(non_camel_case_types)]
pub(super) struct OhUdmfRecord {
_private: [u8; 0],
}
#[repr(C)]
#[allow(non_camel_case_types)]
pub(super) struct OhUdsPlainText {
_private: [u8; 0],
}
#[repr(C)]
#[allow(non_camel_case_types)]
pub(super) struct OhUdsHtml {
_private: [u8; 0],
}
extern "C" {
fn OH_Pasteboard_Create() -> *mut OhPasteboard;
fn OH_Pasteboard_Destroy(pasteboard: *mut OhPasteboard);
fn OH_Pasteboard_SetData(pasteboard: *mut OhPasteboard, data: *mut OhUdmfData) -> i32;
fn OH_Pasteboard_GetData(
pasteboard: *mut OhPasteboard,
status: *mut i32,
) -> *mut OhUdmfData;
fn OH_Pasteboard_HasType(pasteboard: *mut OhPasteboard, data_type: *const c_char) -> bool;
fn OH_UdmfData_Create() -> *mut OhUdmfData;
fn OH_UdmfData_Destroy(data: *mut OhUdmfData);
fn OH_UdmfData_AddRecord(data: *mut OhUdmfData, record: *mut OhUdmfRecord) -> i32;
fn OH_UdmfData_GetPrimaryPlainText(
data: *mut OhUdmfData,
plain_text: *mut OhUdsPlainText,
) -> i32;
fn OH_UdmfData_GetRecord(data: *mut OhUdmfData, index: u32) -> *mut OhUdmfRecord;
fn OH_UdmfRecord_Create() -> *mut OhUdmfRecord;
fn OH_UdmfRecord_Destroy(record: *mut OhUdmfRecord);
fn OH_UdmfRecord_AddPlainText(
record: *mut OhUdmfRecord,
plain_text: *mut OhUdsPlainText,
) -> i32;
fn OH_UdmfRecord_AddHtml(record: *mut OhUdmfRecord, html: *mut OhUdsHtml) -> i32;
fn OH_UdmfRecord_GetHtml(record: *mut OhUdmfRecord, html: *mut OhUdsHtml) -> i32;
fn OH_UdsPlainText_Create() -> *mut OhUdsPlainText;
fn OH_UdsPlainText_Destroy(plain_text: *mut OhUdsPlainText);
fn OH_UdsPlainText_SetContent(
plain_text: *mut OhUdsPlainText,
content: *const c_char,
) -> i32;
fn OH_UdsPlainText_GetContent(plain_text: *mut OhUdsPlainText) -> *const c_char;
fn OH_UdsHtml_Create() -> *mut OhUdsHtml;
fn OH_UdsHtml_Destroy(html: *mut OhUdsHtml);
fn OH_UdsHtml_SetContent(html: *mut OhUdsHtml, content: *const c_char) -> i32;
fn OH_UdsHtml_SetPlainContent(html: *mut OhUdsHtml, content: *const c_char) -> i32;
fn OH_UdsHtml_GetContent(html: *mut OhUdsHtml) -> *const c_char;
}
pub(super) fn available() -> bool {
true
}
fn c_string(value: &str) -> Vec<u8> {
let mut buffer = Vec::with_capacity(value.len() + 1);
buffer.extend_from_slice(value.as_bytes());
buffer.push(0);
buffer
}
unsafe fn read_c_string(pointer: *const c_char) -> Option<String> {
if pointer.is_null() {
return None;
}
Some(CStr::from_ptr(pointer).to_string_lossy().into_owned())
}
fn with_pasteboard<R>(body: impl FnOnce(*mut OhPasteboard) -> R) -> Option<R> {
let pasteboard = unsafe { OH_Pasteboard_Create() };
if pasteboard.is_null() {
log::error!("[harmony pasteboard] OH_Pasteboard_Create returned null");
return None;
}
let result = body(pasteboard);
unsafe { OH_Pasteboard_Destroy(pasteboard) };
Some(result)
}
fn record_html(record: *mut OhUdmfRecord) -> Option<String> {
let uds = unsafe { OH_UdsHtml_Create() };
if uds.is_null() {
log::error!("[harmony pasteboard] OH_UdsHtml_Create returned null");
return None;
}
let status = unsafe { OH_UdmfRecord_GetHtml(record, uds) };
let html = if status == OHOS_OK {
unsafe { read_c_string(OH_UdsHtml_GetContent(uds)) }
} else {
None
};
unsafe { OH_UdsHtml_Destroy(uds) };
html
}
pub(super) fn has_type(data_type: &str) -> bool {
let type_buffer = c_string(data_type);
with_pasteboard(|pasteboard| unsafe {
OH_Pasteboard_HasType(pasteboard, type_buffer.as_ptr().cast())
})
.unwrap_or(false)
}
fn publish(
fill: impl FnOnce(*mut OhUdmfRecord) -> bool,
read_back: fn() -> Option<String>,
expect: &str,
) -> bool {
let data = unsafe { OH_UdmfData_Create() };
let record = unsafe { OH_UdmfRecord_Create() };
if data.is_null() || record.is_null() {
log::error!("[harmony pasteboard] UDMF create failed (data or record was null)");
if !data.is_null() {
unsafe { OH_UdmfData_Destroy(data) };
}
if !record.is_null() {
unsafe { OH_UdmfRecord_Destroy(record) };
}
return false;
}
let attached = fill(record);
let status = if attached { unsafe { OH_UdmfData_AddRecord(data, record) } } else { -1 };
let written = status == OHOS_OK
&& with_pasteboard(|pasteboard| unsafe { OH_Pasteboard_SetData(pasteboard, data) })
.is_some_and(|set_status| set_status == OHOS_OK);
unsafe {
OH_UdmfData_Destroy(data);
OH_UdmfRecord_Destroy(record);
}
if !written {
return false;
}
let confirmed = read_back().is_some_and(|stored| stored == expect);
if !confirmed {
log::warn!(
"[harmony pasteboard] OH_Pasteboard_SetData answered OK but the clipboard does \
not hold the value that was written"
);
}
confirmed
}
pub(super) fn set_text(value: &str) -> bool {
let content = c_string(value);
publish(
|record| {
let uds = unsafe { OH_UdsPlainText_Create() };
if uds.is_null() {
return false;
}
let set = unsafe { OH_UdsPlainText_SetContent(uds, content.as_ptr().cast()) };
let added =
set == OHOS_OK && unsafe { OH_UdmfRecord_AddPlainText(record, uds) } == OHOS_OK;
unsafe { OH_UdsPlainText_Destroy(uds) };
added
},
self::text,
value,
)
}
pub(super) fn set_html(html_text: &str, plain: &str) -> bool {
let html_buffer = c_string(html_text);
let plain_buffer = c_string(plain);
publish(
|record| {
let uds = unsafe { OH_UdsHtml_Create() };
if uds.is_null() {
return false;
}
let set_content =
unsafe { OH_UdsHtml_SetContent(uds, html_buffer.as_ptr().cast()) };
let set_plain =
unsafe { OH_UdsHtml_SetPlainContent(uds, plain_buffer.as_ptr().cast()) };
let added = set_content == OHOS_OK
&& set_plain == OHOS_OK
&& unsafe { OH_UdmfRecord_AddHtml(record, uds) } == OHOS_OK;
unsafe { OH_UdsHtml_Destroy(uds) };
added
},
self::html,
html_text,
)
}
pub(super) fn text() -> Option<String> {
let data = read_data()?;
let text = {
let uds = unsafe { OH_UdsPlainText_Create() };
if uds.is_null() {
None
} else {
let status = unsafe { OH_UdmfData_GetPrimaryPlainText(data, uds) };
let text = if status == OHOS_OK {
unsafe { read_c_string(OH_UdsPlainText_GetContent(uds)) }
} else {
None
};
unsafe { OH_UdsPlainText_Destroy(uds) };
text
}
};
unsafe { OH_UdmfData_Destroy(data) };
text
}
pub(super) fn html() -> Option<String> {
let data = read_data()?;
let html = {
let record = unsafe { OH_UdmfData_GetRecord(data, 0) };
if record.is_null() {
None
} else {
record_html(record)
}
};
unsafe { OH_UdmfData_Destroy(data) };
html
}
fn read_data() -> Option<*mut OhUdmfData> {
with_pasteboard(|pasteboard| {
let mut status = -1i32;
let data = unsafe { OH_Pasteboard_GetData(pasteboard, &mut status) };
if status != OHOS_OK || data.is_null() {
log::warn!("[harmony pasteboard] OH_Pasteboard_GetData answered status {status}");
return None;
}
Some(data)
})
.flatten()
}
pub(super) fn describe() -> &'static str {
"OH_Pasteboard over UDMF (libpasteboard + libudmf)"
}
}
#[cfg(not(target_env = "ohos"))]
mod sys {
use crate::compat::String;
pub(super) fn available() -> bool {
false
}
pub(super) fn has_type(_data_type: &str) -> bool {
false
}
pub(super) fn set_text(_text: &str) -> bool {
false
}
pub(super) fn set_html(_html: &str, _plain: &str) -> bool {
false
}
pub(super) fn text() -> Option<String> {
None
}
pub(super) fn html() -> Option<String> {
None
}
pub(super) fn describe() -> &'static str {
"not built for an OpenHarmony target"
}
}
pub(crate) const PASTEBOARD_AVAILABLE: bool = cfg!(target_env = "ohos");
pub(crate) fn set_text(text: &str) -> Option<bool> {
if !sys::available() {
return None;
}
Some(sys::set_text(text))
}
pub(crate) fn set_html(html: &str, plain: &str) -> Option<bool> {
if !sys::available() {
return None;
}
Some(sys::set_html(html, plain))
}
pub(crate) fn text() -> Option<String> {
if !sys::available() {
return None;
}
sys::text()
}
pub(crate) fn html() -> Option<String> {
if !sys::available() {
return None;
}
sys::html()
}
pub(crate) fn has_format(content_type: &str) -> bool {
if !sys::available() {
return false;
}
match content_type {
PLAIN_TEXT_TYPE | HTML_TYPE => sys::has_type(content_type),
_ => false,
}
}
pub struct HarmonyClipboard;
impl HarmonyClipboard {
pub fn new() -> Self {
Self
}
}
impl Default for HarmonyClipboard {
fn default() -> Self {
Self::new()
}
}
impl RichClipboardBackend for HarmonyClipboard {
fn set_contents(&self, content: ClipboardContent) -> bool {
let written = match content {
ClipboardContent::Text(text) => set_text(&text),
ClipboardContent::Html { html, plain } => set_html(&html, &plain),
other => {
log::warn!(
"[harmony pasteboard] {} is not carried by this backend ({}); the write is \
reported as refused rather than pretended",
other.content_type(),
sys::describe()
);
return false;
}
};
written.unwrap_or(false)
}
fn get_contents(&self) -> Option<ClipboardContent> {
if let Some(html) = html() {
return Some(ClipboardContent::Html { html, plain: text().unwrap_or_default() });
}
text().map(ClipboardContent::Text)
}
fn has_format(&self, content_type: &str) -> bool {
has_format(content_type)
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn a_build_without_the_pasteboard_answers_none_rather_than_an_empty_clipboard() {
if PASTEBOARD_AVAILABLE {
return;
}
assert_eq!(set_text("x"), None);
assert_eq!(set_html("<b>x</b>", "x"), None);
assert_eq!(text(), None);
assert_eq!(html(), None);
assert!(!has_format(PLAIN_TEXT_TYPE));
assert!(!has_format("image/png"));
}
#[test]
fn an_unrepresentable_content_type_is_refused() {
assert!(!has_format("text/rtf"));
assert!(!has_format("text/uri-list"));
assert!(!has_format("image/png"));
}
}