feat: support custom mime types
This commit is contained in:
parent
eebb02816e
commit
3e56207b3a
7 changed files with 335 additions and 116 deletions
|
|
@ -14,9 +14,12 @@ rust-version = "1.65.0"
|
||||||
libc = "0.2.149"
|
libc = "0.2.149"
|
||||||
sctk = { package = "smithay-client-toolkit", version = "0.18.0", default-features = false, features = ["calloop"] }
|
sctk = { package = "smithay-client-toolkit", version = "0.18.0", default-features = false, features = ["calloop"] }
|
||||||
wayland-backend = { version = "0.3.0", default_features = false, features = ["client_system"] }
|
wayland-backend = { version = "0.3.0", default_features = false, features = ["client_system"] }
|
||||||
|
thiserror = "1.0.57"
|
||||||
|
|
||||||
[dev-dependencies]
|
[dev-dependencies]
|
||||||
sctk = { package = "smithay-client-toolkit", version = "0.18.0", default-features = false, features = ["calloop", "xkbcommon"] }
|
sctk = { package = "smithay-client-toolkit", version = "0.18.0", default-features = false, features = ["calloop", "xkbcommon"] }
|
||||||
|
url = "2.5.0"
|
||||||
|
dirs = "5.0.1"
|
||||||
|
|
||||||
[features]
|
[features]
|
||||||
default = ["dlopen"]
|
default = ["dlopen"]
|
||||||
|
|
|
||||||
|
|
@ -2,6 +2,9 @@
|
||||||
// application. For more details on what is going on, consult the
|
// application. For more details on what is going on, consult the
|
||||||
// `smithay-client-toolkit` examples.
|
// `smithay-client-toolkit` examples.
|
||||||
|
|
||||||
|
use std::borrow::Cow;
|
||||||
|
use std::str::{FromStr, Utf8Error};
|
||||||
|
|
||||||
use sctk::compositor::{CompositorHandler, CompositorState};
|
use sctk::compositor::{CompositorHandler, CompositorState};
|
||||||
use sctk::output::{OutputHandler, OutputState};
|
use sctk::output::{OutputHandler, OutputState};
|
||||||
use sctk::reexports::calloop::{EventLoop, LoopHandle};
|
use sctk::reexports::calloop::{EventLoop, LoopHandle};
|
||||||
|
|
@ -21,7 +24,10 @@ use sctk::{
|
||||||
delegate_compositor, delegate_keyboard, delegate_output, delegate_registry, delegate_seat,
|
delegate_compositor, delegate_keyboard, delegate_output, delegate_registry, delegate_seat,
|
||||||
delegate_shm, delegate_xdg_shell, delegate_xdg_window, registry_handlers,
|
delegate_shm, delegate_xdg_shell, delegate_xdg_window, registry_handlers,
|
||||||
};
|
};
|
||||||
|
use smithay_clipboard::mime::{AllowedMimeTypes, AsMimeTypes, MimeType};
|
||||||
use smithay_clipboard::Clipboard;
|
use smithay_clipboard::Clipboard;
|
||||||
|
use thiserror::Error;
|
||||||
|
use url::Url;
|
||||||
|
|
||||||
const MIN_DIM_SIZE: usize = 256;
|
const MIN_DIM_SIZE: usize = 256;
|
||||||
|
|
||||||
|
|
@ -277,27 +283,53 @@ impl KeyboardHandler for SimpleWindow {
|
||||||
) {
|
) {
|
||||||
match event.utf8.as_deref() {
|
match event.utf8.as_deref() {
|
||||||
// Paste primary.
|
// Paste primary.
|
||||||
Some("P") => match self.clipboard.load_primary() {
|
Some("P") => match self.clipboard.load_primary_text() {
|
||||||
Ok(contents) => println!("Paste from primary clipboard: {contents}"),
|
Ok(contents) => println!("Paste from primary clipboard: {contents}"),
|
||||||
Err(err) => eprintln!("Error loading from primary clipboard: {err}"),
|
Err(err) => eprintln!("Error loading from primary clipboard: {err}"),
|
||||||
},
|
},
|
||||||
// Paste clipboard.
|
// Paste clipboard.
|
||||||
Some("p") => match self.clipboard.load() {
|
Some("p") => match self.clipboard.load_text() {
|
||||||
Ok(contents) => println!("Paste from clipboard: {contents}"),
|
Ok(contents) => println!("Paste from clipboard: {contents}"),
|
||||||
Err(err) => eprintln!("Error loading from clipboard: {err}"),
|
Err(err) => eprintln!("Error loading from clipboard: {err}"),
|
||||||
},
|
},
|
||||||
// Copy primary.
|
// Copy primary.
|
||||||
Some("C") => {
|
Some("C") => {
|
||||||
let to_store = "Copy primary";
|
let to_store = "Copy primary";
|
||||||
self.clipboard.store_primary(to_store);
|
self.clipboard.store_primary_text(to_store);
|
||||||
println!("Copied string into primary clipboard: {}", to_store);
|
println!("Copied string into primary clipboard: {}", to_store);
|
||||||
},
|
},
|
||||||
// Copy clipboard.
|
// Copy clipboard.
|
||||||
Some("c") => {
|
Some("c") => {
|
||||||
let to_store = "Copy";
|
let to_store = "Copy";
|
||||||
self.clipboard.store(to_store);
|
self.clipboard.store_text(to_store);
|
||||||
println!("Copied string into clipboard: {}", to_store);
|
println!("Copied string into clipboard: {}", to_store);
|
||||||
},
|
},
|
||||||
|
// Copy URI to primary clipboard.
|
||||||
|
Some("F") => {
|
||||||
|
let home = Uri::home();
|
||||||
|
println!("Copied home dir into primary clipboard: {}", home.0);
|
||||||
|
self.clipboard.store_primary(home);
|
||||||
|
},
|
||||||
|
// Copy URI to clipboard.
|
||||||
|
Some("f") => {
|
||||||
|
let home = Uri::home();
|
||||||
|
println!("Copied home dir into clipboard: {}", home.0);
|
||||||
|
self.clipboard.store(home);
|
||||||
|
},
|
||||||
|
// Read URI from clipboard
|
||||||
|
Some("o") => match self.clipboard.load::<Uri>() {
|
||||||
|
Ok(uri) => {
|
||||||
|
println!("URI from clipboard: {}", uri.0);
|
||||||
|
},
|
||||||
|
Err(err) => eprintln!("Error loading from clipboard: {err}"),
|
||||||
|
},
|
||||||
|
// Read URI from clipboard
|
||||||
|
Some("O") => match self.clipboard.load_primary::<Uri>() {
|
||||||
|
Ok(uri) => {
|
||||||
|
println!("URI from primary clipboard: {}", uri.0);
|
||||||
|
},
|
||||||
|
Err(err) => eprintln!("Error loading from clipboard: {err}"),
|
||||||
|
},
|
||||||
_ => (),
|
_ => (),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -382,6 +414,63 @@ impl SimpleWindow {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[derive(Debug)]
|
||||||
|
pub struct Uri(Url);
|
||||||
|
|
||||||
|
impl Uri {
|
||||||
|
pub fn home() -> Self {
|
||||||
|
let home = dirs::home_dir().unwrap();
|
||||||
|
Uri(Url::from_file_path(home).unwrap())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl AsMimeTypes for Uri {
|
||||||
|
fn available<'a>(&'a self) -> Cow<'static, [MimeType]> {
|
||||||
|
Self::allowed()
|
||||||
|
}
|
||||||
|
|
||||||
|
fn as_bytes(&self, mime_type: &MimeType) -> Option<Cow<'static, [u8]>> {
|
||||||
|
if mime_type == &Self::allowed()[0] {
|
||||||
|
Some(self.0.to_string().as_bytes().to_vec().into())
|
||||||
|
} else {
|
||||||
|
None
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl AllowedMimeTypes for Uri {
|
||||||
|
fn allowed() -> Cow<'static, [MimeType]> {
|
||||||
|
std::borrow::Cow::Borrowed(&[MimeType::Other(Cow::Borrowed("text/uri-list"))])
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Error, Debug)]
|
||||||
|
pub enum UriError {
|
||||||
|
#[error("Unsupported mime type")]
|
||||||
|
Unsupported,
|
||||||
|
#[error("Utf8 error")]
|
||||||
|
Utf8(Utf8Error),
|
||||||
|
#[error("URL parse error")]
|
||||||
|
Parse(url::ParseError),
|
||||||
|
}
|
||||||
|
|
||||||
|
impl TryFrom<(Vec<u8>, MimeType)> for Uri {
|
||||||
|
type Error = UriError;
|
||||||
|
|
||||||
|
fn try_from((data, mime): (Vec<u8>, MimeType)) -> Result<Self, Self::Error> {
|
||||||
|
if mime == Self::allowed()[0] {
|
||||||
|
std::str::from_utf8(&data)
|
||||||
|
.map_err(UriError::Utf8)
|
||||||
|
.and_then(|s| Url::from_str(s).map_err(UriError::Parse))
|
||||||
|
.map(Uri)
|
||||||
|
} else {
|
||||||
|
Err(UriError::Unsupported)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub const URI_MIME_TYPE: &str = "text/uri-list";
|
||||||
|
|
||||||
delegate_compositor!(SimpleWindow);
|
delegate_compositor!(SimpleWindow);
|
||||||
delegate_output!(SimpleWindow);
|
delegate_output!(SimpleWindow);
|
||||||
delegate_shm!(SimpleWindow);
|
delegate_shm!(SimpleWindow);
|
||||||
|
|
|
||||||
99
src/lib.rs
99
src/lib.rs
|
|
@ -8,18 +8,22 @@ use std::ffi::c_void;
|
||||||
use std::io::Result;
|
use std::io::Result;
|
||||||
use std::sync::mpsc::{self, Receiver};
|
use std::sync::mpsc::{self, Receiver};
|
||||||
|
|
||||||
|
use mime::{AllowedMimeTypes, AsMimeTypes, MimeType};
|
||||||
use sctk::reexports::calloop::channel::{self, Sender};
|
use sctk::reexports::calloop::channel::{self, Sender};
|
||||||
use sctk::reexports::client::backend::Backend;
|
use sctk::reexports::client::backend::Backend;
|
||||||
use sctk::reexports::client::Connection;
|
use sctk::reexports::client::Connection;
|
||||||
|
use state::SelectionTarget;
|
||||||
|
use text::Text;
|
||||||
|
|
||||||
mod mime;
|
pub mod mime;
|
||||||
mod state;
|
mod state;
|
||||||
|
mod text;
|
||||||
mod worker;
|
mod worker;
|
||||||
|
|
||||||
/// Access to a Wayland clipboard.
|
/// Access to a Wayland clipboard.
|
||||||
pub struct Clipboard {
|
pub struct Clipboard {
|
||||||
request_sender: Sender<worker::Command>,
|
request_sender: Sender<worker::Command>,
|
||||||
request_receiver: Receiver<Result<String>>,
|
request_receiver: Receiver<Result<(Vec<u8>, MimeType)>>,
|
||||||
clipboard_thread: Option<std::thread::JoinHandle<()>>,
|
clipboard_thread: Option<std::thread::JoinHandle<()>>,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -46,14 +50,21 @@ impl Clipboard {
|
||||||
Self { request_receiver, request_sender, clipboard_thread }
|
Self { request_receiver, request_sender, clipboard_thread }
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Load clipboard data.
|
fn load_inner<T: AllowedMimeTypes + 'static>(&self, target: SelectionTarget) -> Result<T>
|
||||||
///
|
where
|
||||||
/// Loads content from a clipboard on a last observed seat.
|
<T as TryFrom<(Vec<u8>, MimeType)>>::Error: std::error::Error + Send + Sync,
|
||||||
pub fn load(&self) -> Result<String> {
|
{
|
||||||
let _ = self.request_sender.send(worker::Command::Load);
|
let _ = self.request_sender.send(worker::Command::Load(T::allowed().to_vec(), target));
|
||||||
|
|
||||||
if let Ok(reply) = self.request_receiver.recv() {
|
if let Ok(reply) = self.request_receiver.recv() {
|
||||||
reply
|
match reply {
|
||||||
|
Ok((data, mime)) => {
|
||||||
|
T::try_from((data, mime)).map_err(|err| std::io::Error::other(err))
|
||||||
|
},
|
||||||
|
Err(err) => {
|
||||||
|
return Err(err);
|
||||||
|
},
|
||||||
|
}
|
||||||
} else {
|
} else {
|
||||||
// The clipboard thread is dead, however we shouldn't crash downstream, so
|
// The clipboard thread is dead, however we shouldn't crash downstream, so
|
||||||
// propogating an error.
|
// propogating an error.
|
||||||
|
|
@ -61,35 +72,73 @@ impl Clipboard {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Store to a clipboard.
|
/// Load custom clipboard data.
|
||||||
///
|
///
|
||||||
/// Stores to a clipboard on a last observed seat.
|
/// Load the requested type from a clipboard on the last observed seat.
|
||||||
pub fn store<T: Into<String>>(&self, text: T) {
|
pub fn load<T: AllowedMimeTypes + 'static>(&self) -> Result<T>
|
||||||
let request = worker::Command::Store(text.into());
|
where
|
||||||
let _ = self.request_sender.send(request);
|
<T as TryFrom<(Vec<u8>, MimeType)>>::Error: std::error::Error + Send + Sync,
|
||||||
|
{
|
||||||
|
self.load_inner(SelectionTarget::Clipboard)
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Load clipboard data.
|
||||||
|
///
|
||||||
|
/// Loads content from a clipboard on a last observed seat.
|
||||||
|
pub fn load_text(&self) -> Result<String> {
|
||||||
|
self.load::<Text>().map(|t| t.0)
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Load custom primary clipboard data.
|
||||||
|
///
|
||||||
|
/// Load the requested type from a primary clipboard on the last observed
|
||||||
|
/// seat.
|
||||||
|
pub fn load_primary<T: AllowedMimeTypes + 'static>(&self) -> Result<T>
|
||||||
|
where
|
||||||
|
<T as TryFrom<(Vec<u8>, MimeType)>>::Error: std::error::Error + Send + Sync,
|
||||||
|
{
|
||||||
|
self.load_inner(SelectionTarget::Primary)
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Load primary clipboard data.
|
/// Load primary clipboard data.
|
||||||
///
|
///
|
||||||
/// Loads content from a primary clipboard on a last observed seat.
|
/// Loads content from a primary clipboard on a last observed seat.
|
||||||
pub fn load_primary(&self) -> Result<String> {
|
pub fn load_primary_text(&self) -> Result<String> {
|
||||||
let _ = self.request_sender.send(worker::Command::LoadPrimary);
|
self.load_primary::<Text>().map(|t| t.0)
|
||||||
|
|
||||||
if let Ok(reply) = self.request_receiver.recv() {
|
|
||||||
reply
|
|
||||||
} else {
|
|
||||||
// The clipboard thread is dead, however we shouldn't crash downstream, so
|
|
||||||
// propogating an error.
|
|
||||||
Err(std::io::Error::new(std::io::ErrorKind::Other, "clipboard is dead."))
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fn store_inner<T: AsMimeTypes + Send + 'static>(&self, data: T, target: SelectionTarget) {
|
||||||
|
let request = worker::Command::Store(Box::new(data), target);
|
||||||
|
let _ = self.request_sender.send(request);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Store custom data to a clipboard.
|
||||||
|
///
|
||||||
|
/// Stores data of the provided type to a clipboard on a last observed seat.
|
||||||
|
pub fn store<T: AsMimeTypes + Send + 'static>(&self, data: T) {
|
||||||
|
self.store_inner(data, SelectionTarget::Clipboard);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Store to a clipboard.
|
||||||
|
///
|
||||||
|
/// Stores to a clipboard on a last observed seat.
|
||||||
|
pub fn store_text<T: Into<String>>(&self, text: T) {
|
||||||
|
self.store(Text(text.into()));
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Store custom data to a primary clipboard.
|
||||||
|
///
|
||||||
|
/// Stores data of the provided type to a primary clipboard on a last
|
||||||
|
/// observed seat.
|
||||||
|
pub fn store_primary<T: AsMimeTypes + Send + 'static>(&self, data: T) {
|
||||||
|
self.store_inner(data, SelectionTarget::Primary);
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Store to a primary clipboard.
|
/// Store to a primary clipboard.
|
||||||
///
|
///
|
||||||
/// Stores to a primary clipboard on a last observed seat.
|
/// Stores to a primary clipboard on a last observed seat.
|
||||||
pub fn store_primary<T: Into<String>>(&self, text: T) {
|
pub fn store_primary_text<T: Into<String>>(&self, text: T) {
|
||||||
let request = worker::Command::StorePrimary(text.into());
|
self.store_primary(Text(text.into()));
|
||||||
let _ = self.request_sender.send(request);
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
72
src/mime.rs
72
src/mime.rs
|
|
@ -1,10 +1,21 @@
|
||||||
|
use std::borrow::Cow;
|
||||||
|
use thiserror::Error;
|
||||||
|
|
||||||
/// List of allowed mimes.
|
/// List of allowed mimes.
|
||||||
pub static ALLOWED_MIME_TYPES: [&str; 3] =
|
pub static ALLOWED_TEXT_MIME_TYPES: [&str; 3] =
|
||||||
["text/plain;charset=utf-8", "UTF8_STRING", "text/plain"];
|
["text/plain;charset=utf-8", "UTF8_STRING", "text/plain"];
|
||||||
|
|
||||||
|
#[derive(Error, Debug)]
|
||||||
|
pub enum Error {
|
||||||
|
#[error("Unsupported mime type")]
|
||||||
|
Unsupported,
|
||||||
|
}
|
||||||
|
|
||||||
/// Mime type supported by clipboard.
|
/// Mime type supported by clipboard.
|
||||||
#[derive(Clone, Copy, Eq, PartialEq, Debug)]
|
#[derive(Clone, Eq, PartialEq, Debug, Default)]
|
||||||
|
#[repr(u8)]
|
||||||
pub enum MimeType {
|
pub enum MimeType {
|
||||||
|
#[default]
|
||||||
/// text/plain;charset=utf-8 mime type.
|
/// text/plain;charset=utf-8 mime type.
|
||||||
///
|
///
|
||||||
/// The primary mime type used by most clients
|
/// The primary mime type used by most clients
|
||||||
|
|
@ -18,6 +29,37 @@ pub enum MimeType {
|
||||||
///
|
///
|
||||||
/// Fallback without charset parameter.
|
/// Fallback without charset parameter.
|
||||||
TextPlain = 2,
|
TextPlain = 2,
|
||||||
|
/// Other mime type
|
||||||
|
Other(Cow<'static, str>),
|
||||||
|
}
|
||||||
|
|
||||||
|
impl AsRef<str> for MimeType {
|
||||||
|
fn as_ref(&self) -> &str {
|
||||||
|
match self {
|
||||||
|
MimeType::Other(s) => s.as_ref(),
|
||||||
|
m => &ALLOWED_TEXT_MIME_TYPES[m.discriminant() as usize],
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl MimeType {
|
||||||
|
fn discriminant(&self) -> u8 {
|
||||||
|
unsafe { *(self as *const Self as *const u8) }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Describes the mime types which are accepted
|
||||||
|
pub trait AllowedMimeTypes: TryFrom<(Vec<u8>, MimeType)> {
|
||||||
|
fn allowed() -> Cow<'static, [MimeType]>;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Can be converted to data with the available mime types
|
||||||
|
pub trait AsMimeTypes {
|
||||||
|
/// Available mime types for this data
|
||||||
|
fn available<'a>(&'a self) -> Cow<'static, [MimeType]>;
|
||||||
|
|
||||||
|
/// Data as a specific mime_type
|
||||||
|
fn as_bytes(&self, mime_type: &MimeType) -> Option<Cow<'static, [u8]>>;
|
||||||
}
|
}
|
||||||
|
|
||||||
impl MimeType {
|
impl MimeType {
|
||||||
|
|
@ -25,26 +67,22 @@ impl MimeType {
|
||||||
///
|
///
|
||||||
/// `find_allowed()` searches for mime type clipboard supports, if we have a
|
/// `find_allowed()` searches for mime type clipboard supports, if we have a
|
||||||
/// match, returns `Some(MimeType)`, otherwise `None`.
|
/// match, returns `Some(MimeType)`, otherwise `None`.
|
||||||
pub fn find_allowed(offered_mime_types: &[String]) -> Option<Self> {
|
pub fn find_allowed(offered_mime_types: &[String], allowed: &[Self]) -> Option<Self> {
|
||||||
let mut fallback = None;
|
allowed
|
||||||
for offered_mime_type in offered_mime_types.iter() {
|
.iter()
|
||||||
if offered_mime_type == ALLOWED_MIME_TYPES[Self::TextPlainUtf8 as usize] {
|
.find(|allowed| {
|
||||||
return Some(Self::TextPlainUtf8);
|
offered_mime_types.iter().any(|offered| offered.as_str() == allowed.as_ref())
|
||||||
} else if offered_mime_type == ALLOWED_MIME_TYPES[Self::Utf8String as usize] {
|
})
|
||||||
return Some(Self::Utf8String);
|
.cloned()
|
||||||
} else if offered_mime_type == ALLOWED_MIME_TYPES[Self::TextPlain as usize] {
|
|
||||||
// Only use this mime type as a fallback.
|
|
||||||
fallback = Some(Self::TextPlain);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
fallback
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
impl std::fmt::Display for MimeType {
|
impl std::fmt::Display for MimeType {
|
||||||
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
|
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
|
||||||
write!(f, "{}", ALLOWED_MIME_TYPES[*self as usize])
|
match self {
|
||||||
|
MimeType::Other(m) => write!(f, "{}", m),
|
||||||
|
m => write!(f, "{}", ALLOWED_TEXT_MIME_TYPES[m.discriminant() as usize]),
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
94
src/state.rs
94
src/state.rs
|
|
@ -36,12 +36,13 @@ use sctk::reexports::protocols::wp::primary_selection::zv1::client::{
|
||||||
};
|
};
|
||||||
use wayland_backend::client::ObjectId;
|
use wayland_backend::client::ObjectId;
|
||||||
|
|
||||||
use crate::mime::{normalize_to_lf, MimeType, ALLOWED_MIME_TYPES};
|
use crate::mime::{AsMimeTypes, MimeType};
|
||||||
|
use crate::text::Text;
|
||||||
|
|
||||||
pub struct State {
|
pub struct State {
|
||||||
pub primary_selection_manager_state: Option<PrimarySelectionManagerState>,
|
pub primary_selection_manager_state: Option<PrimarySelectionManagerState>,
|
||||||
pub data_device_manager_state: Option<DataDeviceManagerState>,
|
pub data_device_manager_state: Option<DataDeviceManagerState>,
|
||||||
pub reply_tx: Sender<Result<String>>,
|
pub reply_tx: Sender<Result<(Vec<u8>, MimeType)>>,
|
||||||
pub exit: bool,
|
pub exit: bool,
|
||||||
|
|
||||||
registry_state: RegistryState,
|
registry_state: RegistryState,
|
||||||
|
|
@ -55,10 +56,12 @@ pub struct State {
|
||||||
queue_handle: QueueHandle<Self>,
|
queue_handle: QueueHandle<Self>,
|
||||||
|
|
||||||
primary_sources: Vec<PrimarySelectionSource>,
|
primary_sources: Vec<PrimarySelectionSource>,
|
||||||
primary_selection_content: Rc<[u8]>,
|
primary_selection_content: Box<dyn AsMimeTypes>,
|
||||||
|
primary_selection_mime_types: Rc<Cow<'static, [MimeType]>>,
|
||||||
|
|
||||||
data_sources: Vec<CopyPasteSource>,
|
data_sources: Vec<CopyPasteSource>,
|
||||||
data_selection_content: Rc<[u8]>,
|
data_selection_content: Box<dyn AsMimeTypes>,
|
||||||
|
data_selection_mime_types: Rc<Cow<'static, [MimeType]>>,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl State {
|
impl State {
|
||||||
|
|
@ -67,7 +70,7 @@ impl State {
|
||||||
globals: &GlobalList,
|
globals: &GlobalList,
|
||||||
queue_handle: &QueueHandle<Self>,
|
queue_handle: &QueueHandle<Self>,
|
||||||
loop_handle: LoopHandle<'static, Self>,
|
loop_handle: LoopHandle<'static, Self>,
|
||||||
reply_tx: Sender<Result<String>>,
|
reply_tx: Sender<Result<(Vec<u8>, MimeType)>>,
|
||||||
) -> Option<Self> {
|
) -> Option<Self> {
|
||||||
let mut seats = HashMap::new();
|
let mut seats = HashMap::new();
|
||||||
|
|
||||||
|
|
@ -87,8 +90,8 @@ impl State {
|
||||||
|
|
||||||
Some(Self {
|
Some(Self {
|
||||||
registry_state: RegistryState::new(globals),
|
registry_state: RegistryState::new(globals),
|
||||||
primary_selection_content: Rc::from([]),
|
primary_selection_content: Box::new(Text(String::new())),
|
||||||
data_selection_content: Rc::from([]),
|
data_selection_content: Box::new(Text(String::new())),
|
||||||
queue_handle: queue_handle.clone(),
|
queue_handle: queue_handle.clone(),
|
||||||
primary_selection_manager_state,
|
primary_selection_manager_state,
|
||||||
primary_sources: Vec::new(),
|
primary_sources: Vec::new(),
|
||||||
|
|
@ -100,13 +103,19 @@ impl State {
|
||||||
seat_state,
|
seat_state,
|
||||||
reply_tx,
|
reply_tx,
|
||||||
seats,
|
seats,
|
||||||
|
primary_selection_mime_types: Rc::new(Default::default()),
|
||||||
|
data_selection_mime_types: Rc::new(Default::default()),
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Store selection for the given target.
|
/// Store selection for the given target.
|
||||||
///
|
///
|
||||||
/// Selection source is only created when `Some(())` is returned.
|
/// Selection source is only created when `Some(())` is returned.
|
||||||
pub fn store_selection(&mut self, ty: SelectionTarget, contents: String) -> Option<()> {
|
pub fn store_selection(
|
||||||
|
&mut self,
|
||||||
|
ty: SelectionTarget,
|
||||||
|
contents: Box<dyn AsMimeTypes>,
|
||||||
|
) -> Option<()> {
|
||||||
let latest = self.latest_seat.as_ref()?;
|
let latest = self.latest_seat.as_ref()?;
|
||||||
let seat = self.seats.get_mut(latest)?;
|
let seat = self.seats.get_mut(latest)?;
|
||||||
|
|
||||||
|
|
@ -114,22 +123,22 @@ impl State {
|
||||||
return None;
|
return None;
|
||||||
}
|
}
|
||||||
|
|
||||||
let contents = Rc::from(contents.into_bytes());
|
|
||||||
|
|
||||||
match ty {
|
match ty {
|
||||||
SelectionTarget::Clipboard => {
|
SelectionTarget::Clipboard => {
|
||||||
let mgr = self.data_device_manager_state.as_ref()?;
|
let mgr = self.data_device_manager_state.as_ref()?;
|
||||||
|
let mime_types = contents.available();
|
||||||
self.data_selection_content = contents;
|
self.data_selection_content = contents;
|
||||||
let source =
|
let source = mgr.create_copy_paste_source(&self.queue_handle, mime_types.iter());
|
||||||
mgr.create_copy_paste_source(&self.queue_handle, ALLOWED_MIME_TYPES.iter());
|
self.data_selection_mime_types = Rc::new(mime_types);
|
||||||
source.set_selection(seat.data_device.as_ref().unwrap(), seat.latest_serial);
|
source.set_selection(seat.data_device.as_ref().unwrap(), seat.latest_serial);
|
||||||
self.data_sources.push(source);
|
self.data_sources.push(source);
|
||||||
},
|
},
|
||||||
SelectionTarget::Primary => {
|
SelectionTarget::Primary => {
|
||||||
let mgr = self.primary_selection_manager_state.as_ref()?;
|
let mgr = self.primary_selection_manager_state.as_ref()?;
|
||||||
|
let mime_types = contents.available();
|
||||||
self.primary_selection_content = contents;
|
self.primary_selection_content = contents;
|
||||||
let source =
|
let source = mgr.create_selection_source(&self.queue_handle, mime_types.iter());
|
||||||
mgr.create_selection_source(&self.queue_handle, ALLOWED_MIME_TYPES.iter());
|
self.primary_selection_mime_types = Rc::new(mime_types);
|
||||||
source.set_selection(seat.primary_device.as_ref().unwrap(), seat.latest_serial);
|
source.set_selection(seat.primary_device.as_ref().unwrap(), seat.latest_serial);
|
||||||
self.primary_sources.push(source);
|
self.primary_sources.push(source);
|
||||||
},
|
},
|
||||||
|
|
@ -139,7 +148,11 @@ impl State {
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Load selection for the given target.
|
/// Load selection for the given target.
|
||||||
pub fn load_selection(&mut self, ty: SelectionTarget) -> Result<()> {
|
pub fn load_selection(
|
||||||
|
&mut self,
|
||||||
|
ty: SelectionTarget,
|
||||||
|
allowed_mime_types: &[MimeType],
|
||||||
|
) -> Result<()> {
|
||||||
let latest = self
|
let latest = self
|
||||||
.latest_seat
|
.latest_seat
|
||||||
.as_ref()
|
.as_ref()
|
||||||
|
|
@ -153,7 +166,7 @@ impl State {
|
||||||
return Err(Error::new(ErrorKind::Other, "client doesn't have focus"));
|
return Err(Error::new(ErrorKind::Other, "client doesn't have focus"));
|
||||||
}
|
}
|
||||||
|
|
||||||
let (read_pipe, mime_type) = match ty {
|
let (read_pipe, mut mime_type) = match ty {
|
||||||
SelectionTarget::Clipboard => {
|
SelectionTarget::Clipboard => {
|
||||||
let selection = seat
|
let selection = seat
|
||||||
.data_device
|
.data_device
|
||||||
|
|
@ -161,8 +174,9 @@ impl State {
|
||||||
.and_then(|data| data.data().selection_offer())
|
.and_then(|data| data.data().selection_offer())
|
||||||
.ok_or_else(|| Error::new(ErrorKind::Other, "selection is empty"))?;
|
.ok_or_else(|| Error::new(ErrorKind::Other, "selection is empty"))?;
|
||||||
|
|
||||||
let mime_type =
|
let mime_type = selection
|
||||||
selection.with_mime_types(MimeType::find_allowed).ok_or_else(|| {
|
.with_mime_types(|offered| MimeType::find_allowed(offered, allowed_mime_types))
|
||||||
|
.ok_or_else(|| {
|
||||||
Error::new(ErrorKind::NotFound, "supported mime-type is not found")
|
Error::new(ErrorKind::NotFound, "supported mime-type is not found")
|
||||||
})?;
|
})?;
|
||||||
|
|
||||||
|
|
@ -183,8 +197,9 @@ impl State {
|
||||||
.and_then(|data| data.data().selection_offer())
|
.and_then(|data| data.data().selection_offer())
|
||||||
.ok_or_else(|| Error::new(ErrorKind::Other, "selection is empty"))?;
|
.ok_or_else(|| Error::new(ErrorKind::Other, "selection is empty"))?;
|
||||||
|
|
||||||
let mime_type =
|
let mime_type = selection
|
||||||
selection.with_mime_types(MimeType::find_allowed).ok_or_else(|| {
|
.with_mime_types(|offered| MimeType::find_allowed(offered, allowed_mime_types))
|
||||||
|
.ok_or_else(|| {
|
||||||
Error::new(ErrorKind::NotFound, "supported mime-type is not found")
|
Error::new(ErrorKind::NotFound, "supported mime-type is not found")
|
||||||
})?;
|
})?;
|
||||||
|
|
||||||
|
|
@ -204,26 +219,9 @@ impl State {
|
||||||
loop {
|
loop {
|
||||||
match file.read(&mut reader_buffer) {
|
match file.read(&mut reader_buffer) {
|
||||||
Ok(0) => {
|
Ok(0) => {
|
||||||
let utf8 = String::from_utf8_lossy(&content);
|
let _ = state
|
||||||
let content = match utf8 {
|
.reply_tx
|
||||||
Cow::Borrowed(_) => {
|
.send(Ok((mem::take(&mut content), mem::take(&mut mime_type))));
|
||||||
// Don't clone the read data.
|
|
||||||
let mut to_send = Vec::new();
|
|
||||||
mem::swap(&mut content, &mut to_send);
|
|
||||||
String::from_utf8(to_send).unwrap()
|
|
||||||
},
|
|
||||||
Cow::Owned(content) => content,
|
|
||||||
};
|
|
||||||
|
|
||||||
// Post-process the content according to mime type.
|
|
||||||
let content = match mime_type {
|
|
||||||
MimeType::TextPlainUtf8 | MimeType::TextPlain => {
|
|
||||||
normalize_to_lf(content)
|
|
||||||
},
|
|
||||||
MimeType::Utf8String => content,
|
|
||||||
};
|
|
||||||
|
|
||||||
let _ = state.reply_tx.send(Ok(content));
|
|
||||||
break PostAction::Remove;
|
break PostAction::Remove;
|
||||||
},
|
},
|
||||||
Ok(n) => content.extend_from_slice(&reader_buffer[..n]),
|
Ok(n) => content.extend_from_slice(&reader_buffer[..n]),
|
||||||
|
|
@ -240,10 +238,12 @@ impl State {
|
||||||
}
|
}
|
||||||
|
|
||||||
fn send_request(&mut self, ty: SelectionTarget, write_pipe: WritePipe, mime: String) {
|
fn send_request(&mut self, ty: SelectionTarget, write_pipe: WritePipe, mime: String) {
|
||||||
// We can only send strings, so don't do anything with the mime-type.
|
let Some(mime_type) = MimeType::find_allowed(&[mime], match ty {
|
||||||
if MimeType::find_allowed(&[mime]).is_none() {
|
SelectionTarget::Clipboard => &self.data_selection_mime_types,
|
||||||
|
SelectionTarget::Primary => &self.primary_selection_mime_types,
|
||||||
|
}) else {
|
||||||
return;
|
return;
|
||||||
}
|
};
|
||||||
|
|
||||||
// Mark FD as non-blocking so we won't block ourselves.
|
// Mark FD as non-blocking so we won't block ourselves.
|
||||||
unsafe {
|
unsafe {
|
||||||
|
|
@ -255,8 +255,12 @@ impl State {
|
||||||
// Don't access the content on the state directly, since it could change during
|
// Don't access the content on the state directly, since it could change during
|
||||||
// the send.
|
// the send.
|
||||||
let contents = match ty {
|
let contents = match ty {
|
||||||
SelectionTarget::Clipboard => self.data_selection_content.clone(),
|
SelectionTarget::Clipboard => self.data_selection_content.as_bytes(&mime_type),
|
||||||
SelectionTarget::Primary => self.primary_selection_content.clone(),
|
SelectionTarget::Primary => self.primary_selection_content.as_bytes(&mime_type),
|
||||||
|
};
|
||||||
|
|
||||||
|
let Some(contents) = contents else {
|
||||||
|
return;
|
||||||
};
|
};
|
||||||
|
|
||||||
let mut written = 0;
|
let mut written = 0;
|
||||||
|
|
|
||||||
46
src/text.rs
Normal file
46
src/text.rs
Normal file
|
|
@ -0,0 +1,46 @@
|
||||||
|
use std::borrow::Cow;
|
||||||
|
|
||||||
|
use crate::mime::{normalize_to_lf, AllowedMimeTypes, AsMimeTypes, Error, MimeType};
|
||||||
|
|
||||||
|
pub struct Text(pub String);
|
||||||
|
|
||||||
|
impl TryFrom<(Vec<u8>, MimeType)> for Text {
|
||||||
|
type Error = Error;
|
||||||
|
|
||||||
|
fn try_from((content, mime_type): (Vec<u8>, MimeType)) -> Result<Self, Self::Error> {
|
||||||
|
let utf8 = String::from_utf8_lossy(&content);
|
||||||
|
let content = match utf8 {
|
||||||
|
Cow::Borrowed(_) => String::from_utf8(content).unwrap(),
|
||||||
|
Cow::Owned(content) => content,
|
||||||
|
};
|
||||||
|
|
||||||
|
// Post-process the content according to mime type.
|
||||||
|
let content = match mime_type {
|
||||||
|
MimeType::TextPlainUtf8 | MimeType::TextPlain => normalize_to_lf(content),
|
||||||
|
MimeType::Utf8String => content,
|
||||||
|
MimeType::Other(_) => return Err(Error::Unsupported),
|
||||||
|
};
|
||||||
|
Ok(Text(content))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl AllowedMimeTypes for Text {
|
||||||
|
fn allowed() -> Cow<'static, [MimeType]> {
|
||||||
|
Cow::Borrowed(&[MimeType::TextPlainUtf8, MimeType::Utf8String, MimeType::TextPlain])
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl AsMimeTypes for Text {
|
||||||
|
fn available(&self) -> Cow<'static, [MimeType]> {
|
||||||
|
Self::allowed()
|
||||||
|
}
|
||||||
|
|
||||||
|
fn as_bytes<'a>(&'a self, mime_type: &MimeType) -> Option<Cow<'static, [u8]>> {
|
||||||
|
match mime_type {
|
||||||
|
MimeType::TextPlainUtf8 | MimeType::Utf8String | MimeType::TextPlain => {
|
||||||
|
Some(Cow::Owned(self.0.as_bytes().to_owned()))
|
||||||
|
},
|
||||||
|
MimeType::Other(_) => None,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -7,6 +7,7 @@ use sctk::reexports::calloop_wayland_source::WaylandSource;
|
||||||
use sctk::reexports::client::globals::registry_queue_init;
|
use sctk::reexports::client::globals::registry_queue_init;
|
||||||
use sctk::reexports::client::Connection;
|
use sctk::reexports::client::Connection;
|
||||||
|
|
||||||
|
use crate::mime::{AsMimeTypes, MimeType};
|
||||||
use crate::state::{SelectionTarget, State};
|
use crate::state::{SelectionTarget, State};
|
||||||
|
|
||||||
/// Spawn a clipboard worker, which dispatches its own `EventQueue` and handles
|
/// Spawn a clipboard worker, which dispatches its own `EventQueue` and handles
|
||||||
|
|
@ -15,7 +16,7 @@ pub fn spawn(
|
||||||
name: String,
|
name: String,
|
||||||
display: Connection,
|
display: Connection,
|
||||||
rx_chan: Channel<Command>,
|
rx_chan: Channel<Command>,
|
||||||
worker_replier: Sender<Result<String>>,
|
worker_replier: Sender<Result<(Vec<u8>, MimeType)>>,
|
||||||
) -> Option<std::thread::JoinHandle<()>> {
|
) -> Option<std::thread::JoinHandle<()>> {
|
||||||
std::thread::Builder::new()
|
std::thread::Builder::new()
|
||||||
.name(name)
|
.name(name)
|
||||||
|
|
@ -26,16 +27,11 @@ pub fn spawn(
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Clipboard worker thread command.
|
/// Clipboard worker thread command.
|
||||||
#[derive(Eq, PartialEq)]
|
|
||||||
pub enum Command {
|
pub enum Command {
|
||||||
/// Store data to a clipboard.
|
/// Loads data for the first available mime type in the provided list
|
||||||
Store(String),
|
Load(Vec<MimeType>, SelectionTarget),
|
||||||
/// Store data to a primary selection.
|
Store(Box<dyn AsMimeTypes + Send>, SelectionTarget),
|
||||||
StorePrimary(String),
|
/// Store Data with the given Mime Types
|
||||||
/// Load data from a clipboard.
|
|
||||||
Load,
|
|
||||||
/// Load primary selection.
|
|
||||||
LoadPrimary,
|
|
||||||
/// Shutdown the worker.
|
/// Shutdown the worker.
|
||||||
Exit,
|
Exit,
|
||||||
}
|
}
|
||||||
|
|
@ -44,7 +40,7 @@ pub enum Command {
|
||||||
fn worker_impl(
|
fn worker_impl(
|
||||||
connection: Connection,
|
connection: Connection,
|
||||||
rx_chan: Channel<Command>,
|
rx_chan: Channel<Command>,
|
||||||
reply_tx: Sender<Result<String>>,
|
reply_tx: Sender<Result<(Vec<u8>, MimeType)>>,
|
||||||
) {
|
) {
|
||||||
let (globals, event_queue) = match registry_queue_init(&connection) {
|
let (globals, event_queue) = match registry_queue_init(&connection) {
|
||||||
Ok(data) => data,
|
Ok(data) => data,
|
||||||
|
|
@ -64,29 +60,23 @@ fn worker_impl(
|
||||||
.insert_source(rx_chan, |event, _, state| {
|
.insert_source(rx_chan, |event, _, state| {
|
||||||
if let channel::Event::Msg(event) = event {
|
if let channel::Event::Msg(event) = event {
|
||||||
match event {
|
match event {
|
||||||
Command::StorePrimary(contents) => {
|
Command::Exit => state.exit = true,
|
||||||
state.store_selection(SelectionTarget::Primary, contents);
|
Command::Store(data, target) => {
|
||||||
|
state.store_selection(target, data);
|
||||||
},
|
},
|
||||||
Command::Store(contents) => {
|
Command::Load(mime_types, target)
|
||||||
state.store_selection(SelectionTarget::Clipboard, contents);
|
if state.data_device_manager_state.is_some() =>
|
||||||
},
|
{
|
||||||
Command::Load if state.data_device_manager_state.is_some() => {
|
if let Err(err) = state.load_selection(target, &mime_types) {
|
||||||
if let Err(err) = state.load_selection(SelectionTarget::Clipboard) {
|
|
||||||
let _ = state.reply_tx.send(Err(err));
|
let _ = state.reply_tx.send(Err(err));
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
Command::LoadPrimary if state.data_device_manager_state.is_some() => {
|
Command::Load(..) => {
|
||||||
if let Err(err) = state.load_selection(SelectionTarget::Primary) {
|
|
||||||
let _ = state.reply_tx.send(Err(err));
|
|
||||||
}
|
|
||||||
},
|
|
||||||
Command::Load | Command::LoadPrimary => {
|
|
||||||
let _ = state.reply_tx.send(Err(Error::new(
|
let _ = state.reply_tx.send(Err(Error::new(
|
||||||
ErrorKind::Other,
|
ErrorKind::Other,
|
||||||
"requested selection is not supported",
|
"requested selection is not supported",
|
||||||
)));
|
)));
|
||||||
},
|
},
|
||||||
Command::Exit => state.exit = true,
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue