Skip to content

Commit

Permalink
feat: support custom mime types
Browse files Browse the repository at this point in the history
  • Loading branch information
wash2 committed Feb 27, 2024
1 parent eebb028 commit 7051bd0
Show file tree
Hide file tree
Showing 7 changed files with 338 additions and 116 deletions.
3 changes: 3 additions & 0 deletions Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -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"]
Expand Down
97 changes: 93 additions & 4 deletions examples/clipboard.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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};
Expand All @@ -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;

Expand Down Expand Up @@ -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}"),
},
_ => (),
}
}
Expand Down Expand Up @@ -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);
Expand Down
97 changes: 72 additions & 25 deletions src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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<()>>,
}

Expand All @@ -46,50 +50,93 @@ 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))

Check failure on line 62 in src/lib.rs

View workflow job for this annotation

GitHub Actions / Tests (nightly)

redundant closure

Check failure on line 62 in src/lib.rs

View workflow job for this annotation

GitHub Actions / Tests (nightly)

current MSRV (Minimum Supported Rust Version) is `1.65.0` but this item is stable since `1.74.0`
},
Err(err) => {
return Err(err);

Check failure on line 65 in src/lib.rs

View workflow job for this annotation

GitHub Actions / Tests (nightly)

unneeded `return` statement
},
}
} 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."))
}
}

/// 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)
}

Check failure on line 91 in src/lib.rs

View workflow job for this annotation

GitHub Actions / Check Formatting

Diff in /home/runner/work/smithay-clipboard/smithay-clipboard/src/lib.rs
/// 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()));
}

Check failure on line 127 in src/lib.rs

View workflow job for this annotation

GitHub Actions / Check Formatting

Diff in /home/runner/work/smithay-clipboard/smithay-clipboard/src/lib.rs
/// 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()));
}
}

Expand Down
72 changes: 55 additions & 17 deletions src/mime.rs
Original file line number Diff line number Diff line change
@@ -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
Expand All @@ -18,33 +29,60 @@ 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],

Check failure on line 40 in src/mime.rs

View workflow job for this annotation

GitHub Actions / Tests (nightly)

this expression creates a reference which is immediately dereferenced by the compiler
}
}
}

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]>;

Check failure on line 59 in src/mime.rs

View workflow job for this annotation

GitHub Actions / Tests (nightly)

the following explicit lifetimes could be elided: 'a

/// Data as a specific mime_type
fn as_bytes(&self, mime_type: &MimeType) -> Option<Cow<'static, [u8]>>;
}

impl MimeType {
/// Find first allowed mime type among the `offered_mime_types`.
///
/// `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]),
}
}
}

Expand Down
Loading

0 comments on commit 7051bd0

Please sign in to comment.