snapshot of https://github.com/tauri-apps/plugins-workspace.git (v2, 2026-09-23; blobs > 131072B omitted)
Some checks failed
integration tests / run-integration-tests (windows-latest) (push) Has been cancelled
Test Rust / test (push) Has been cancelled
test plugins e2e (mobile) / ios (push) Has been cancelled
test plugins e2e / test (map[name:Windows os:windows-latest]) (push) Has been cancelled
test plugins e2e / test (map[name:macOS os:macos-latest]) (push) Has been cancelled
integration tests / run-integration-tests (macos-latest) (push) Has been cancelled
version or publish / version-or-publish (push) Failing after 0s
integration tests / run-integration-tests (ubuntu-22.04) (push) Failing after 0s
Lint JavaScript / eslint (push) Failing after 0s
Sync / sync-to-mirrors (push) Failing after 0s
test plugins e2e (mobile) / android (push) Failing after 0s
test plugins e2e / test (map[name:Linux os:ubuntu-latest]) (push) Failing after 0s
Test Rust / changes (push) Failing after 0s
Lint Rust / changes (push) Failing after 0s
Lint Rust / clippy (push) Has been skipped
Audit Rust / audit-rust (push) Failing after 1s
Audit JavaScript / audit-js (push) Failing after 1s
Some checks failed
integration tests / run-integration-tests (windows-latest) (push) Has been cancelled
Test Rust / test (push) Has been cancelled
test plugins e2e (mobile) / ios (push) Has been cancelled
test plugins e2e / test (map[name:Windows os:windows-latest]) (push) Has been cancelled
test plugins e2e / test (map[name:macOS os:macos-latest]) (push) Has been cancelled
integration tests / run-integration-tests (macos-latest) (push) Has been cancelled
version or publish / version-or-publish (push) Failing after 0s
integration tests / run-integration-tests (ubuntu-22.04) (push) Failing after 0s
Lint JavaScript / eslint (push) Failing after 0s
Sync / sync-to-mirrors (push) Failing after 0s
test plugins e2e (mobile) / android (push) Failing after 0s
test plugins e2e / test (map[name:Linux os:ubuntu-latest]) (push) Failing after 0s
Test Rust / changes (push) Failing after 0s
Lint Rust / changes (push) Failing after 0s
Lint Rust / clippy (push) Has been skipped
Audit Rust / audit-rust (push) Failing after 1s
Audit JavaScript / audit-js (push) Failing after 1s
This commit is contained in:
80
plugins/clipboard-manager/src/commands.rs
Normal file
80
plugins/clipboard-manager/src/commands.rs
Normal file
@@ -0,0 +1,80 @@
|
||||
// Copyright 2019-2023 Tauri Programme within The Commons Conservancy
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
// SPDX-License-Identifier: MIT
|
||||
|
||||
use tauri::{command, image::JsImage, AppHandle, Manager, ResourceId, Runtime, State, Webview};
|
||||
|
||||
use crate::{Clipboard, Result};
|
||||
|
||||
#[command]
|
||||
#[cfg(desktop)]
|
||||
pub(crate) async fn write_text<R: Runtime>(
|
||||
_app: AppHandle<R>,
|
||||
clipboard: State<'_, Clipboard<R>>,
|
||||
text: &str,
|
||||
#[allow(unused)] label: Option<String>,
|
||||
) -> Result<()> {
|
||||
clipboard.write_text(text)
|
||||
}
|
||||
|
||||
#[command]
|
||||
#[cfg(not(desktop))]
|
||||
pub(crate) async fn write_text<R: Runtime>(
|
||||
_app: AppHandle<R>,
|
||||
clipboard: State<'_, Clipboard<R>>,
|
||||
text: &str,
|
||||
#[allow(unused)] label: Option<&str>,
|
||||
) -> Result<()> {
|
||||
match label {
|
||||
Some(label) => clipboard.write_text_with_label(text, label),
|
||||
None => clipboard.write_text(text),
|
||||
}
|
||||
}
|
||||
|
||||
#[command]
|
||||
pub(crate) async fn read_text<R: Runtime>(
|
||||
_app: AppHandle<R>,
|
||||
clipboard: State<'_, Clipboard<R>>,
|
||||
) -> Result<String> {
|
||||
clipboard.read_text()
|
||||
}
|
||||
|
||||
#[command]
|
||||
pub(crate) async fn write_image<R: Runtime>(
|
||||
webview: Webview<R>,
|
||||
clipboard: State<'_, Clipboard<R>>,
|
||||
image: JsImage,
|
||||
) -> Result<()> {
|
||||
let resources_table = webview.resources_table();
|
||||
let image = image.into_img(&resources_table)?;
|
||||
clipboard.write_image(&image)
|
||||
}
|
||||
|
||||
#[command]
|
||||
pub(crate) async fn read_image<R: Runtime>(
|
||||
webview: Webview<R>,
|
||||
clipboard: State<'_, Clipboard<R>>,
|
||||
) -> Result<ResourceId> {
|
||||
let image = clipboard.read_image()?.to_owned();
|
||||
let mut resources_table = webview.resources_table();
|
||||
let rid = resources_table.add(image);
|
||||
Ok(rid)
|
||||
}
|
||||
|
||||
#[command]
|
||||
pub(crate) async fn write_html<R: Runtime>(
|
||||
_app: AppHandle<R>,
|
||||
clipboard: State<'_, Clipboard<R>>,
|
||||
html: &str,
|
||||
alt_text: Option<&str>,
|
||||
) -> Result<()> {
|
||||
clipboard.write_html(html, alt_text)
|
||||
}
|
||||
|
||||
#[command]
|
||||
pub(crate) async fn clear<R: Runtime>(
|
||||
_app: AppHandle<R>,
|
||||
clipboard: State<'_, Clipboard<R>>,
|
||||
) -> Result<()> {
|
||||
clipboard.clear()
|
||||
}
|
||||
148
plugins/clipboard-manager/src/desktop.rs
Normal file
148
plugins/clipboard-manager/src/desktop.rs
Normal file
@@ -0,0 +1,148 @@
|
||||
// Copyright 2019-2023 Tauri Programme within The Commons Conservancy
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
// SPDX-License-Identifier: MIT
|
||||
|
||||
use arboard::ImageData;
|
||||
use serde::de::DeserializeOwned;
|
||||
use tauri::{image::Image, plugin::PluginApi, AppHandle, Runtime};
|
||||
|
||||
use std::{borrow::Cow, sync::Mutex};
|
||||
|
||||
pub fn init<R: Runtime, C: DeserializeOwned>(
|
||||
app: &AppHandle<R>,
|
||||
_api: PluginApi<R, C>,
|
||||
) -> crate::Result<Clipboard<R>> {
|
||||
Ok(Clipboard {
|
||||
app: app.clone(),
|
||||
clipboard: arboard::Clipboard::new().map(|c| Mutex::new(Some(c))),
|
||||
})
|
||||
}
|
||||
|
||||
/// Access to the clipboard APIs.
|
||||
pub struct Clipboard<R: Runtime> {
|
||||
#[allow(dead_code)]
|
||||
app: AppHandle<R>,
|
||||
// According to arboard docs the clipboard must be dropped before exit.
|
||||
// Since tauri doesn't call drop on exit we'll use an Option to take() on RunEvent::Exit.
|
||||
clipboard: Result<Mutex<Option<arboard::Clipboard>>, arboard::Error>,
|
||||
}
|
||||
|
||||
impl<R: Runtime> Clipboard<R> {
|
||||
/// Writes plain text to the system clipboard.
|
||||
///
|
||||
/// # Errors
|
||||
///
|
||||
/// Returns [`crate::Error::Clipboard`] if the clipboard could not be initialized or the
|
||||
/// underlying [`arboard`] operation fails.
|
||||
pub fn write_text<'a, T: Into<Cow<'a, str>>>(&self, text: T) -> crate::Result<()> {
|
||||
match &self.clipboard {
|
||||
Ok(clipboard) => clipboard
|
||||
.lock()
|
||||
.unwrap()
|
||||
.as_mut()
|
||||
.unwrap()
|
||||
.set_text(text)
|
||||
.map_err(Into::into),
|
||||
Err(e) => Err(crate::Error::Clipboard(e.to_string())),
|
||||
}
|
||||
}
|
||||
|
||||
/// Writes an image to the system clipboard as RGBA data.
|
||||
///
|
||||
/// # Errors
|
||||
///
|
||||
/// Returns [`crate::Error::Clipboard`] if the clipboard could not be initialized or the
|
||||
/// underlying [`arboard`] operation fails.
|
||||
pub fn write_image(&self, image: &Image<'_>) -> crate::Result<()> {
|
||||
match &self.clipboard {
|
||||
Ok(clipboard) => clipboard
|
||||
.lock()
|
||||
.unwrap()
|
||||
.as_mut()
|
||||
.unwrap()
|
||||
.set_image(ImageData {
|
||||
bytes: Cow::Borrowed(image.rgba()),
|
||||
width: image.width() as usize,
|
||||
height: image.height() as usize,
|
||||
})
|
||||
.map_err(Into::into),
|
||||
Err(e) => Err(crate::Error::Clipboard(e.to_string())),
|
||||
}
|
||||
}
|
||||
|
||||
/// Warning: This method should not be used on the main thread! Otherwise the underlying libraries may deadlock on Linux, freezing the whole app, when trying to copy data copied from this app, for example if the user copies text from the WebView.
|
||||
pub fn read_text(&self) -> crate::Result<String> {
|
||||
match &self.clipboard {
|
||||
Ok(clipboard) => {
|
||||
let text = clipboard.lock().unwrap().as_mut().unwrap().get_text()?;
|
||||
Ok(text)
|
||||
}
|
||||
Err(e) => Err(crate::Error::Clipboard(e.to_string())),
|
||||
}
|
||||
}
|
||||
|
||||
/// Writes HTML to the system clipboard, with an optional plain text fallback for targets
|
||||
/// that cannot render it.
|
||||
///
|
||||
/// # Errors
|
||||
///
|
||||
/// Returns [`crate::Error::Clipboard`] if the clipboard could not be initialized or the
|
||||
/// underlying [`arboard`] operation fails.
|
||||
pub fn write_html<'a, T: Into<Cow<'a, str>>>(
|
||||
&self,
|
||||
html: T,
|
||||
alt_text: Option<T>,
|
||||
) -> crate::Result<()> {
|
||||
match &self.clipboard {
|
||||
Ok(clipboard) => clipboard
|
||||
.lock()
|
||||
.unwrap()
|
||||
.as_mut()
|
||||
.unwrap()
|
||||
.set_html(html, alt_text)
|
||||
.map_err(Into::into),
|
||||
Err(e) => Err(crate::Error::Clipboard(e.to_string())),
|
||||
}
|
||||
}
|
||||
|
||||
/// Clears the system clipboard.
|
||||
///
|
||||
/// # Errors
|
||||
///
|
||||
/// Returns [`crate::Error::Clipboard`] if the clipboard could not be initialized or the
|
||||
/// underlying [`arboard`] operation fails.
|
||||
pub fn clear(&self) -> crate::Result<()> {
|
||||
match &self.clipboard {
|
||||
Ok(clipboard) => clipboard
|
||||
.lock()
|
||||
.unwrap()
|
||||
.as_mut()
|
||||
.unwrap()
|
||||
.clear()
|
||||
.map_err(Into::into),
|
||||
Err(e) => Err(crate::Error::Clipboard(e.to_string())),
|
||||
}
|
||||
}
|
||||
|
||||
/// Warning: This method should not be used on the main thread! Otherwise the underlying libraries may deadlock on Linux, freezing the whole app, when trying to copy data copied from this app, for example if the user copies text from the WebView.
|
||||
pub fn read_image(&self) -> crate::Result<Image<'_>> {
|
||||
match &self.clipboard {
|
||||
Ok(clipboard) => {
|
||||
let image = clipboard.lock().unwrap().as_mut().unwrap().get_image()?;
|
||||
let image = Image::new_owned(
|
||||
image.bytes.to_vec(),
|
||||
image.width as u32,
|
||||
image.height as u32,
|
||||
);
|
||||
Ok(image)
|
||||
}
|
||||
Err(e) => Err(crate::Error::Clipboard(e.to_string())),
|
||||
}
|
||||
}
|
||||
|
||||
pub(crate) fn cleanup(&self) {
|
||||
if let Ok(clipboard) = &self.clipboard {
|
||||
clipboard.lock().unwrap().take();
|
||||
}
|
||||
}
|
||||
}
|
||||
39
plugins/clipboard-manager/src/error.rs
Normal file
39
plugins/clipboard-manager/src/error.rs
Normal file
@@ -0,0 +1,39 @@
|
||||
// Copyright 2019-2023 Tauri Programme within The Commons Conservancy
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
// SPDX-License-Identifier: MIT
|
||||
|
||||
use serde::{ser::Serializer, Serialize};
|
||||
|
||||
/// Alias for `Result<T, Error>` used throughout this crate.
|
||||
pub type Result<T> = std::result::Result<T, Error>;
|
||||
|
||||
/// Errors that can occur while interacting with the system clipboard.
|
||||
#[derive(Debug, thiserror::Error)]
|
||||
pub enum Error {
|
||||
/// Forwarding a request to, or receiving a response from, the mobile plugin failed.
|
||||
#[cfg(mobile)]
|
||||
#[error(transparent)]
|
||||
PluginInvoke(#[from] tauri::plugin::mobile::PluginInvokeError),
|
||||
/// The underlying clipboard operation failed, or the operation is not supported on this platform.
|
||||
#[error("{0}")]
|
||||
Clipboard(String),
|
||||
/// An error forwarded from the Tauri core.
|
||||
#[error(transparent)]
|
||||
Tauri(#[from] tauri::Error),
|
||||
}
|
||||
|
||||
impl Serialize for Error {
|
||||
fn serialize<S>(&self, serializer: S) -> std::result::Result<S::Ok, S::Error>
|
||||
where
|
||||
S: Serializer,
|
||||
{
|
||||
serializer.serialize_str(self.to_string().as_ref())
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(desktop)]
|
||||
impl From<arboard::Error> for Error {
|
||||
fn from(error: arboard::Error) -> Self {
|
||||
Self::Clipboard(error.to_string())
|
||||
}
|
||||
}
|
||||
70
plugins/clipboard-manager/src/lib.rs
Normal file
70
plugins/clipboard-manager/src/lib.rs
Normal file
@@ -0,0 +1,70 @@
|
||||
// Copyright 2019-2023 Tauri Programme within The Commons Conservancy
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
// SPDX-License-Identifier: MIT
|
||||
|
||||
//! Read and write to the system clipboard.
|
||||
|
||||
#![doc(
|
||||
html_logo_url = "https://github.com/tauri-apps/tauri/raw/dev/app-icon.png",
|
||||
html_favicon_url = "https://github.com/tauri-apps/tauri/raw/dev/app-icon.png"
|
||||
)]
|
||||
|
||||
use tauri::{
|
||||
plugin::{Builder, TauriPlugin},
|
||||
Manager, RunEvent, Runtime,
|
||||
};
|
||||
|
||||
#[cfg(desktop)]
|
||||
mod desktop;
|
||||
#[cfg(mobile)]
|
||||
mod mobile;
|
||||
|
||||
mod commands;
|
||||
mod error;
|
||||
|
||||
pub use error::{Error, Result};
|
||||
|
||||
#[cfg(desktop)]
|
||||
pub use desktop::Clipboard;
|
||||
#[cfg(mobile)]
|
||||
pub use mobile::Clipboard;
|
||||
|
||||
/// Extensions to [`tauri::App`], [`tauri::AppHandle`], [`tauri::WebviewWindow`], [`tauri::Webview`] and [`tauri::Window`] to access the clipboard APIs.
|
||||
pub trait ClipboardExt<R: Runtime> {
|
||||
/// Returns a handle to the [`Clipboard`] APIs.
|
||||
fn clipboard(&self) -> &Clipboard<R>;
|
||||
}
|
||||
|
||||
impl<R: Runtime, T: Manager<R>> crate::ClipboardExt<R> for T {
|
||||
fn clipboard(&self) -> &Clipboard<R> {
|
||||
self.state::<Clipboard<R>>().inner()
|
||||
}
|
||||
}
|
||||
|
||||
/// Initializes the plugin.
|
||||
pub fn init<R: Runtime>() -> TauriPlugin<R> {
|
||||
Builder::new("clipboard-manager")
|
||||
.invoke_handler(tauri::generate_handler![
|
||||
commands::write_text,
|
||||
commands::read_text,
|
||||
commands::read_image,
|
||||
commands::write_image,
|
||||
commands::write_html,
|
||||
commands::clear
|
||||
])
|
||||
.setup(|app, api| {
|
||||
#[cfg(mobile)]
|
||||
let clipboard = mobile::init(app, api)?;
|
||||
#[cfg(desktop)]
|
||||
let clipboard = desktop::init(app, api)?;
|
||||
app.manage(clipboard);
|
||||
Ok(())
|
||||
})
|
||||
.on_event(|_app, _event| {
|
||||
#[cfg(desktop)]
|
||||
if let RunEvent::Exit = _event {
|
||||
_app.clipboard().cleanup();
|
||||
}
|
||||
})
|
||||
.build()
|
||||
}
|
||||
149
plugins/clipboard-manager/src/mobile.rs
Normal file
149
plugins/clipboard-manager/src/mobile.rs
Normal file
@@ -0,0 +1,149 @@
|
||||
// Copyright 2019-2023 Tauri Programme within The Commons Conservancy
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
// SPDX-License-Identifier: MIT
|
||||
|
||||
use serde::de::DeserializeOwned;
|
||||
use serde::{Deserialize, Serialize};
|
||||
use tauri::{
|
||||
image::Image,
|
||||
plugin::{PluginApi, PluginHandle},
|
||||
AppHandle, Runtime,
|
||||
};
|
||||
|
||||
use std::borrow::Cow;
|
||||
|
||||
#[cfg(target_os = "android")]
|
||||
const PLUGIN_IDENTIFIER: &str = "app.tauri.clipboard";
|
||||
|
||||
#[cfg(target_os = "ios")]
|
||||
tauri::ios_plugin_binding!(init_plugin_clipboard);
|
||||
|
||||
// initializes the Kotlin or Swift plugin classes
|
||||
pub fn init<R: Runtime, C: DeserializeOwned>(
|
||||
_app: &AppHandle<R>,
|
||||
api: PluginApi<R, C>,
|
||||
) -> crate::Result<Clipboard<R>> {
|
||||
#[cfg(target_os = "android")]
|
||||
let handle = api.register_android_plugin(PLUGIN_IDENTIFIER, "ClipboardPlugin")?;
|
||||
#[cfg(target_os = "ios")]
|
||||
let handle = api.register_ios_plugin(init_plugin_clipboard)?;
|
||||
Ok(Clipboard(handle))
|
||||
}
|
||||
|
||||
/// Access to the clipboard APIs.
|
||||
pub struct Clipboard<R: Runtime>(PluginHandle<R>);
|
||||
|
||||
impl<R: Runtime> Clipboard<R> {
|
||||
/// Writes plain text to the system clipboard, without a label.
|
||||
///
|
||||
/// # Errors
|
||||
///
|
||||
/// Returns [`crate::Error::PluginInvoke`] if the underlying mobile plugin call fails.
|
||||
pub fn write_text<'a, T: Into<Cow<'a, str>>>(&self, text: T) -> crate::Result<()> {
|
||||
let text = text.into().to_string();
|
||||
self.0
|
||||
.run_mobile_plugin("writeText", ClipKind::PlainText { text, label: None })
|
||||
.map_err(Into::into)
|
||||
}
|
||||
|
||||
/// Writes plain text to the system clipboard along with a label describing the content.
|
||||
///
|
||||
/// The label is only used on Android (it becomes the `ClipData` label); iOS ignores it.
|
||||
///
|
||||
/// # Errors
|
||||
///
|
||||
/// Returns [`crate::Error::PluginInvoke`] if the underlying mobile plugin call fails.
|
||||
pub fn write_text_with_label<'a, T: Into<Cow<'a, str>>>(
|
||||
&self,
|
||||
text: T,
|
||||
label: T,
|
||||
) -> crate::Result<()> {
|
||||
let text = text.into().to_string();
|
||||
let label = label.into().to_string();
|
||||
self.0
|
||||
.run_mobile_plugin(
|
||||
"writeText",
|
||||
ClipKind::PlainText {
|
||||
text,
|
||||
label: Some(label),
|
||||
},
|
||||
)
|
||||
.map_err(Into::into)
|
||||
}
|
||||
|
||||
/// Not supported on mobile.
|
||||
///
|
||||
/// # Errors
|
||||
///
|
||||
/// Always returns [`crate::Error::Clipboard`].
|
||||
pub fn write_image(&self, _image: &Image<'_>) -> crate::Result<()> {
|
||||
Err(crate::Error::Clipboard(
|
||||
"Unsupported on this platform".to_string(),
|
||||
))
|
||||
}
|
||||
|
||||
/// Reads the system clipboard as plain text.
|
||||
///
|
||||
/// # Errors
|
||||
///
|
||||
/// Returns [`crate::Error::PluginInvoke`] if the underlying mobile plugin call fails.
|
||||
pub fn read_text(&self) -> crate::Result<String> {
|
||||
self.0
|
||||
.run_mobile_plugin("readText", ())
|
||||
.map(|c| match c {
|
||||
ClipboardContents::PlainText { text } => text,
|
||||
})
|
||||
.map_err(Into::into)
|
||||
}
|
||||
|
||||
/// Not supported on mobile.
|
||||
///
|
||||
/// # Errors
|
||||
///
|
||||
/// Always returns [`crate::Error::Clipboard`].
|
||||
pub fn read_image(&self) -> crate::Result<Image<'_>> {
|
||||
Err(crate::Error::Clipboard(
|
||||
"Unsupported on this platform".to_string(),
|
||||
))
|
||||
}
|
||||
|
||||
// Treat HTML as unsupported on mobile until tested
|
||||
/// Not supported on mobile.
|
||||
///
|
||||
/// # Errors
|
||||
///
|
||||
/// Always returns [`crate::Error::Clipboard`].
|
||||
pub fn write_html<'a, T: Into<Cow<'a, str>>>(
|
||||
&self,
|
||||
_html: T,
|
||||
_alt_text: Option<T>,
|
||||
) -> crate::Result<()> {
|
||||
Err(crate::Error::Clipboard(
|
||||
"Unsupported on this platform".to_string(),
|
||||
))
|
||||
}
|
||||
|
||||
/// Clears the system clipboard.
|
||||
///
|
||||
/// On Android this only works on SDK 28 and above; on older versions the clipboard is
|
||||
/// instead overwritten with an empty string.
|
||||
///
|
||||
/// # Errors
|
||||
///
|
||||
/// Returns [`crate::Error::PluginInvoke`] if the underlying mobile plugin call fails.
|
||||
pub fn clear(&self) -> crate::Result<()> {
|
||||
self.0.run_mobile_plugin("clear", ()).map_err(Into::into)
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug, Deserialize, Serialize)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
enum ClipKind {
|
||||
PlainText { label: Option<String>, text: String },
|
||||
}
|
||||
|
||||
#[derive(Debug, Serialize, Deserialize)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
enum ClipboardContents {
|
||||
PlainText { text: String },
|
||||
}
|
||||
Reference in New Issue
Block a user