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"
|
||||
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"] }
|
||||
thiserror = "1.0.57"
|
||||
|
||||
[dev-dependencies]
|
||||
sctk = { package = "smithay-client-toolkit", version = "0.18.0", default-features = false, features = ["calloop", "xkbcommon"] }
|
||||
url = "2.5.0"
|
||||
dirs = "5.0.1"
|
||||
|
||||
[features]
|
||||
default = ["dlopen"]
|
||||
|
|
|
|||
|
|
@ -2,6 +2,9 @@
|
|||
// application. For more details on what is going on, consult the
|
||||
// `smithay-client-toolkit` examples.
|
||||
|
||||
use std::borrow::Cow;
|
||||
use std::str::{FromStr, Utf8Error};
|
||||
|
||||
use sctk::compositor::{CompositorHandler, CompositorState};
|
||||
use sctk::output::{OutputHandler, OutputState};
|
||||
use sctk::reexports::calloop::{EventLoop, LoopHandle};
|
||||
|
|
@ -21,7 +24,10 @@ use sctk::{
|
|||
delegate_compositor, delegate_keyboard, delegate_output, delegate_registry, delegate_seat,
|
||||
delegate_shm, delegate_xdg_shell, delegate_xdg_window, registry_handlers,
|
||||
};
|
||||
use smithay_clipboard::mime::{AllowedMimeTypes, AsMimeTypes, MimeType};
|
||||
use smithay_clipboard::Clipboard;
|
||||
use thiserror::Error;
|
||||
use url::Url;
|
||||
|
||||
const MIN_DIM_SIZE: usize = 256;
|
||||
|
||||
|
|
@ -277,27 +283,53 @@ impl KeyboardHandler for SimpleWindow {
|
|||
) {
|
||||
match event.utf8.as_deref() {
|
||||
// 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}"),
|
||||
Err(err) => eprintln!("Error loading from primary clipboard: {err}"),
|
||||
},
|
||||
// Paste clipboard.
|
||||
Some("p") => match self.clipboard.load() {
|
||||
Some("p") => match self.clipboard.load_text() {
|
||||
Ok(contents) => println!("Paste from clipboard: {contents}"),
|
||||
Err(err) => eprintln!("Error loading from clipboard: {err}"),
|
||||
},
|
||||
// Copy primary.
|
||||
Some("C") => {
|
||||
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);
|
||||
},
|
||||
// Copy clipboard.
|
||||
Some("c") => {
|
||||
let to_store = "Copy";
|
||||
self.clipboard.store(to_store);
|
||||
self.clipboard.store_text(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_output!(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::sync::mpsc::{self, Receiver};
|
||||
|
||||
use mime::{AllowedMimeTypes, AsMimeTypes, MimeType};
|
||||
use sctk::reexports::calloop::channel::{self, Sender};
|
||||
use sctk::reexports::client::backend::Backend;
|
||||
use sctk::reexports::client::Connection;
|
||||
use state::SelectionTarget;
|
||||
use text::Text;
|
||||
|
||||
mod mime;
|
||||
pub mod mime;
|
||||
mod state;
|
||||
mod text;
|
||||
mod worker;
|
||||
|
||||
/// Access to a Wayland clipboard.
|
||||
pub struct Clipboard {
|
||||
request_sender: Sender<worker::Command>,
|
||||
request_receiver: Receiver<Result<String>>,
|
||||
request_receiver: Receiver<Result<(Vec<u8>, MimeType)>>,
|
||||
clipboard_thread: Option<std::thread::JoinHandle<()>>,
|
||||
}
|
||||
|
||||
|
|
@ -46,14 +50,21 @@ impl Clipboard {
|
|||
Self { request_receiver, request_sender, clipboard_thread }
|
||||
}
|
||||
|
||||
/// Load clipboard data.
|
||||
///
|
||||
/// Loads content from a clipboard on a last observed seat.
|
||||
pub fn load(&self) -> Result<String> {
|
||||
let _ = self.request_sender.send(worker::Command::Load);
|
||||
fn load_inner<T: AllowedMimeTypes + 'static>(&self, target: SelectionTarget) -> Result<T>
|
||||
where
|
||||
<T as TryFrom<(Vec<u8>, MimeType)>>::Error: std::error::Error + Send + Sync,
|
||||
{
|
||||
let _ = self.request_sender.send(worker::Command::Load(T::allowed().to_vec(), target));
|
||||
|
||||
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 {
|
||||
// The clipboard thread is dead, however we shouldn't crash downstream, so
|
||||
// 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.
|
||||
pub fn store<T: Into<String>>(&self, text: T) {
|
||||
let request = worker::Command::Store(text.into());
|
||||
let _ = self.request_sender.send(request);
|
||||
/// Load the requested type from a clipboard on the last observed seat.
|
||||
pub fn load<T: AllowedMimeTypes + 'static>(&self) -> Result<T>
|
||||
where
|
||||
<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.
|
||||
///
|
||||
/// Loads content from a primary clipboard on a last observed seat.
|
||||
pub fn load_primary(&self) -> Result<String> {
|
||||
let _ = self.request_sender.send(worker::Command::LoadPrimary);
|
||||
pub fn load_primary_text(&self) -> Result<String> {
|
||||
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.
|
||||
///
|
||||
/// Stores to a primary clipboard on a last observed seat.
|
||||
pub fn store_primary<T: Into<String>>(&self, text: T) {
|
||||
let request = worker::Command::StorePrimary(text.into());
|
||||
let _ = self.request_sender.send(request);
|
||||
pub fn store_primary_text<T: Into<String>>(&self, text: T) {
|
||||
self.store_primary(Text(text.into()));
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
|||
72
src/mime.rs
72
src/mime.rs
|
|
@ -1,10 +1,21 @@
|
|||
use std::borrow::Cow;
|
||||
use thiserror::Error;
|
||||
|
||||
/// 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"];
|
||||
|
||||
#[derive(Error, Debug)]
|
||||
pub enum Error {
|
||||
#[error("Unsupported mime type")]
|
||||
Unsupported,
|
||||
}
|
||||
|
||||
/// Mime type supported by clipboard.
|
||||
#[derive(Clone, Copy, Eq, PartialEq, Debug)]
|
||||
#[derive(Clone, Eq, PartialEq, Debug, Default)]
|
||||
#[repr(u8)]
|
||||
pub enum MimeType {
|
||||
#[default]
|
||||
/// text/plain;charset=utf-8 mime type.
|
||||
///
|
||||
/// The primary mime type used by most clients
|
||||
|
|
@ -18,6 +29,37 @@ pub enum MimeType {
|
|||
///
|
||||
/// Fallback without charset parameter.
|
||||
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 {
|
||||
|
|
@ -25,26 +67,22 @@ impl MimeType {
|
|||
///
|
||||
/// `find_allowed()` searches for mime type clipboard supports, if we have a
|
||||
/// match, returns `Some(MimeType)`, otherwise `None`.
|
||||
pub fn find_allowed(offered_mime_types: &[String]) -> Option<Self> {
|
||||
let mut fallback = None;
|
||||
for offered_mime_type in offered_mime_types.iter() {
|
||||
if offered_mime_type == ALLOWED_MIME_TYPES[Self::TextPlainUtf8 as usize] {
|
||||
return Some(Self::TextPlainUtf8);
|
||||
} else if offered_mime_type == ALLOWED_MIME_TYPES[Self::Utf8String as usize] {
|
||||
return Some(Self::Utf8String);
|
||||
} 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
|
||||
pub fn find_allowed(offered_mime_types: &[String], allowed: &[Self]) -> Option<Self> {
|
||||
allowed
|
||||
.iter()
|
||||
.find(|allowed| {
|
||||
offered_mime_types.iter().any(|offered| offered.as_str() == allowed.as_ref())
|
||||
})
|
||||
.cloned()
|
||||
}
|
||||
}
|
||||
|
||||
impl std::fmt::Display for MimeType {
|
||||
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 crate::mime::{normalize_to_lf, MimeType, ALLOWED_MIME_TYPES};
|
||||
use crate::mime::{AsMimeTypes, MimeType};
|
||||
use crate::text::Text;
|
||||
|
||||
pub struct State {
|
||||
pub primary_selection_manager_state: Option<PrimarySelectionManagerState>,
|
||||
pub data_device_manager_state: Option<DataDeviceManagerState>,
|
||||
pub reply_tx: Sender<Result<String>>,
|
||||
pub reply_tx: Sender<Result<(Vec<u8>, MimeType)>>,
|
||||
pub exit: bool,
|
||||
|
||||
registry_state: RegistryState,
|
||||
|
|
@ -55,10 +56,12 @@ pub struct State {
|
|||
queue_handle: QueueHandle<Self>,
|
||||
|
||||
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_selection_content: Rc<[u8]>,
|
||||
data_selection_content: Box<dyn AsMimeTypes>,
|
||||
data_selection_mime_types: Rc<Cow<'static, [MimeType]>>,
|
||||
}
|
||||
|
||||
impl State {
|
||||
|
|
@ -67,7 +70,7 @@ impl State {
|
|||
globals: &GlobalList,
|
||||
queue_handle: &QueueHandle<Self>,
|
||||
loop_handle: LoopHandle<'static, Self>,
|
||||
reply_tx: Sender<Result<String>>,
|
||||
reply_tx: Sender<Result<(Vec<u8>, MimeType)>>,
|
||||
) -> Option<Self> {
|
||||
let mut seats = HashMap::new();
|
||||
|
||||
|
|
@ -87,8 +90,8 @@ impl State {
|
|||
|
||||
Some(Self {
|
||||
registry_state: RegistryState::new(globals),
|
||||
primary_selection_content: Rc::from([]),
|
||||
data_selection_content: Rc::from([]),
|
||||
primary_selection_content: Box::new(Text(String::new())),
|
||||
data_selection_content: Box::new(Text(String::new())),
|
||||
queue_handle: queue_handle.clone(),
|
||||
primary_selection_manager_state,
|
||||
primary_sources: Vec::new(),
|
||||
|
|
@ -100,13 +103,19 @@ impl State {
|
|||
seat_state,
|
||||
reply_tx,
|
||||
seats,
|
||||
primary_selection_mime_types: Rc::new(Default::default()),
|
||||
data_selection_mime_types: Rc::new(Default::default()),
|
||||
})
|
||||
}
|
||||
|
||||
/// Store selection for the given target.
|
||||
///
|
||||
/// 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 seat = self.seats.get_mut(latest)?;
|
||||
|
||||
|
|
@ -114,22 +123,22 @@ impl State {
|
|||
return None;
|
||||
}
|
||||
|
||||
let contents = Rc::from(contents.into_bytes());
|
||||
|
||||
match ty {
|
||||
SelectionTarget::Clipboard => {
|
||||
let mgr = self.data_device_manager_state.as_ref()?;
|
||||
let mime_types = contents.available();
|
||||
self.data_selection_content = contents;
|
||||
let source =
|
||||
mgr.create_copy_paste_source(&self.queue_handle, ALLOWED_MIME_TYPES.iter());
|
||||
let source = mgr.create_copy_paste_source(&self.queue_handle, mime_types.iter());
|
||||
self.data_selection_mime_types = Rc::new(mime_types);
|
||||
source.set_selection(seat.data_device.as_ref().unwrap(), seat.latest_serial);
|
||||
self.data_sources.push(source);
|
||||
},
|
||||
SelectionTarget::Primary => {
|
||||
let mgr = self.primary_selection_manager_state.as_ref()?;
|
||||
let mime_types = contents.available();
|
||||
self.primary_selection_content = contents;
|
||||
let source =
|
||||
mgr.create_selection_source(&self.queue_handle, ALLOWED_MIME_TYPES.iter());
|
||||
let source = mgr.create_selection_source(&self.queue_handle, mime_types.iter());
|
||||
self.primary_selection_mime_types = Rc::new(mime_types);
|
||||
source.set_selection(seat.primary_device.as_ref().unwrap(), seat.latest_serial);
|
||||
self.primary_sources.push(source);
|
||||
},
|
||||
|
|
@ -139,7 +148,11 @@ impl State {
|
|||
}
|
||||
|
||||
/// 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
|
||||
.latest_seat
|
||||
.as_ref()
|
||||
|
|
@ -153,7 +166,7 @@ impl State {
|
|||
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 => {
|
||||
let selection = seat
|
||||
.data_device
|
||||
|
|
@ -161,8 +174,9 @@ impl State {
|
|||
.and_then(|data| data.data().selection_offer())
|
||||
.ok_or_else(|| Error::new(ErrorKind::Other, "selection is empty"))?;
|
||||
|
||||
let mime_type =
|
||||
selection.with_mime_types(MimeType::find_allowed).ok_or_else(|| {
|
||||
let mime_type = selection
|
||||
.with_mime_types(|offered| MimeType::find_allowed(offered, allowed_mime_types))
|
||||
.ok_or_else(|| {
|
||||
Error::new(ErrorKind::NotFound, "supported mime-type is not found")
|
||||
})?;
|
||||
|
||||
|
|
@ -183,8 +197,9 @@ impl State {
|
|||
.and_then(|data| data.data().selection_offer())
|
||||
.ok_or_else(|| Error::new(ErrorKind::Other, "selection is empty"))?;
|
||||
|
||||
let mime_type =
|
||||
selection.with_mime_types(MimeType::find_allowed).ok_or_else(|| {
|
||||
let mime_type = selection
|
||||
.with_mime_types(|offered| MimeType::find_allowed(offered, allowed_mime_types))
|
||||
.ok_or_else(|| {
|
||||
Error::new(ErrorKind::NotFound, "supported mime-type is not found")
|
||||
})?;
|
||||
|
||||
|
|
@ -204,26 +219,9 @@ impl State {
|
|||
loop {
|
||||
match file.read(&mut reader_buffer) {
|
||||
Ok(0) => {
|
||||
let utf8 = String::from_utf8_lossy(&content);
|
||||
let content = match utf8 {
|
||||
Cow::Borrowed(_) => {
|
||||
// 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));
|
||||
let _ = state
|
||||
.reply_tx
|
||||
.send(Ok((mem::take(&mut content), mem::take(&mut mime_type))));
|
||||
break PostAction::Remove;
|
||||
},
|
||||
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) {
|
||||
// We can only send strings, so don't do anything with the mime-type.
|
||||
if MimeType::find_allowed(&[mime]).is_none() {
|
||||
let Some(mime_type) = MimeType::find_allowed(&[mime], match ty {
|
||||
SelectionTarget::Clipboard => &self.data_selection_mime_types,
|
||||
SelectionTarget::Primary => &self.primary_selection_mime_types,
|
||||
}) else {
|
||||
return;
|
||||
}
|
||||
};
|
||||
|
||||
// Mark FD as non-blocking so we won't block ourselves.
|
||||
unsafe {
|
||||
|
|
@ -255,8 +255,12 @@ impl State {
|
|||
// Don't access the content on the state directly, since it could change during
|
||||
// the send.
|
||||
let contents = match ty {
|
||||
SelectionTarget::Clipboard => self.data_selection_content.clone(),
|
||||
SelectionTarget::Primary => self.primary_selection_content.clone(),
|
||||
SelectionTarget::Clipboard => self.data_selection_content.as_bytes(&mime_type),
|
||||
SelectionTarget::Primary => self.primary_selection_content.as_bytes(&mime_type),
|
||||
};
|
||||
|
||||
let Some(contents) = contents else {
|
||||
return;
|
||||
};
|
||||
|
||||
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::Connection;
|
||||
|
||||
use crate::mime::{AsMimeTypes, MimeType};
|
||||
use crate::state::{SelectionTarget, State};
|
||||
|
||||
/// Spawn a clipboard worker, which dispatches its own `EventQueue` and handles
|
||||
|
|
@ -15,7 +16,7 @@ pub fn spawn(
|
|||
name: String,
|
||||
display: Connection,
|
||||
rx_chan: Channel<Command>,
|
||||
worker_replier: Sender<Result<String>>,
|
||||
worker_replier: Sender<Result<(Vec<u8>, MimeType)>>,
|
||||
) -> Option<std::thread::JoinHandle<()>> {
|
||||
std::thread::Builder::new()
|
||||
.name(name)
|
||||
|
|
@ -26,16 +27,11 @@ pub fn spawn(
|
|||
}
|
||||
|
||||
/// Clipboard worker thread command.
|
||||
#[derive(Eq, PartialEq)]
|
||||
pub enum Command {
|
||||
/// Store data to a clipboard.
|
||||
Store(String),
|
||||
/// Store data to a primary selection.
|
||||
StorePrimary(String),
|
||||
/// Load data from a clipboard.
|
||||
Load,
|
||||
/// Load primary selection.
|
||||
LoadPrimary,
|
||||
/// Loads data for the first available mime type in the provided list
|
||||
Load(Vec<MimeType>, SelectionTarget),
|
||||
Store(Box<dyn AsMimeTypes + Send>, SelectionTarget),
|
||||
/// Store Data with the given Mime Types
|
||||
/// Shutdown the worker.
|
||||
Exit,
|
||||
}
|
||||
|
|
@ -44,7 +40,7 @@ pub enum Command {
|
|||
fn worker_impl(
|
||||
connection: Connection,
|
||||
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) {
|
||||
Ok(data) => data,
|
||||
|
|
@ -64,29 +60,23 @@ fn worker_impl(
|
|||
.insert_source(rx_chan, |event, _, state| {
|
||||
if let channel::Event::Msg(event) = event {
|
||||
match event {
|
||||
Command::StorePrimary(contents) => {
|
||||
state.store_selection(SelectionTarget::Primary, contents);
|
||||
Command::Exit => state.exit = true,
|
||||
Command::Store(data, target) => {
|
||||
state.store_selection(target, data);
|
||||
},
|
||||
Command::Store(contents) => {
|
||||
state.store_selection(SelectionTarget::Clipboard, contents);
|
||||
},
|
||||
Command::Load if state.data_device_manager_state.is_some() => {
|
||||
if let Err(err) = state.load_selection(SelectionTarget::Clipboard) {
|
||||
Command::Load(mime_types, target)
|
||||
if state.data_device_manager_state.is_some() =>
|
||||
{
|
||||
if let Err(err) = state.load_selection(target, &mime_types) {
|
||||
let _ = state.reply_tx.send(Err(err));
|
||||
}
|
||||
},
|
||||
Command::LoadPrimary if state.data_device_manager_state.is_some() => {
|
||||
if let Err(err) = state.load_selection(SelectionTarget::Primary) {
|
||||
let _ = state.reply_tx.send(Err(err));
|
||||
}
|
||||
},
|
||||
Command::Load | Command::LoadPrimary => {
|
||||
Command::Load(..) => {
|
||||
let _ = state.reply_tx.send(Err(Error::new(
|
||||
ErrorKind::Other,
|
||||
"requested selection is not supported",
|
||||
)));
|
||||
},
|
||||
Command::Exit => state.exit = true,
|
||||
}
|
||||
}
|
||||
})
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue