Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat: support custom mime types #56

Open
wants to merge 22 commits into
base: master
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
22 commits
Select commit Hold shift + click to select a range
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
8 changes: 8 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -1,5 +1,13 @@
# Change Log

## 0.8.0

- Add support for loading and storing custom data.
- **Breaking** `Clipboard::load` renamed to `Clipboard::load_text`.
- **Breaking** `Clipboard::load_primary` renamed to `Clipboard::load_primary_text`.
- **Breaking** `Clipboard::store` renamed to `Clipboard::store_text`.
- **Breaking** `Clipboard::store_primary` renamed to `Clipboard::store_primary_text`.

## 0.7.1

- Don't panic on display disconnect
Expand Down
3 changes: 3 additions & 0 deletions Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,10 @@ sctk = { package = "smithay-client-toolkit", version = "0.18.0", default-feature
wayland-backend = { version = "0.3.0", default_features = false, features = ["client_system"] }

[dev-dependencies]
dirs = "5.0.1"
sctk = { package = "smithay-client-toolkit", version = "0.18.0", default-features = false, features = ["calloop", "xkbcommon"] }
thiserror = "1.0.57"
url = "2.5.0"

[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
137 changes: 108 additions & 29 deletions src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@
//! should have surface around.

#![deny(clippy::all, clippy::if_not_else, clippy::enum_glob_use)]
use std::borrow::Cow;
use std::ffi::c_void;
use std::io::Result;
use std::sync::mpsc::{self, Receiver};
Expand All @@ -12,14 +13,19 @@ use sctk::reexports::calloop::channel::{self, Sender};
use sctk::reexports::client::backend::Backend;
use sctk::reexports::client::Connection;

mod mime;
pub mod mime;
mod state;
mod text;
mod worker;

use mime::{AllowedMimeTypes, AsMimeTypes, MimeType};
use state::SelectionTarget;
use text::Text;

/// 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,49 +52,122 @@ impl Clipboard {
Self { request_receiver, request_sender, clipboard_thread }
}

/// Load custom clipboard data.
///
/// Load the requested type from a clipboard on the last observed seat.
pub fn load<T: AllowedMimeTypes + 'static>(&self) -> Result<T> {
self.load_inner(SelectionTarget::Clipboard, T::allowed())
}

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

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."))
}
pub fn load_text(&self) -> Result<String> {
self.load::<Text>().map(|t| t.0)
}

/// Store to a clipboard.
/// Load custom primary 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 primary clipboard on the last observed
/// seat.
pub fn load_primary<T: AllowedMimeTypes + 'static>(&self) -> Result<T> {
self.load_inner(SelectionTarget::Primary, T::allowed())
}

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

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."))
}
pub fn load_primary_text(&self) -> Result<String> {
self.load_primary::<Text>().map(|t| t.0)
}

/// Load clipboard data for sepecific mime types.
///
/// Loads content from a primary clipboard on a last observed seat.
pub fn load_mime<D: TryFrom<(Vec<u8>, MimeType)>>(
&self,
allowed: impl Into<Cow<'static, [MimeType]>>,
) -> Result<D> {
self.load_inner(SelectionTarget::Clipboard, allowed).and_then(|d| {
D::try_from(d).map_err(|_| {
std::io::Error::new(
std::io::ErrorKind::Other,
"Failed to load data of the requested type.",
)
})
})
}

/// Load primary clipboard data for specific mime types.
///
/// Loads content from a primary clipboard on a last observed seat.
pub fn load_primary_mime<D: TryFrom<(Vec<u8>, MimeType)>>(
&self,
allowed: impl Into<Cow<'static, [MimeType]>>,
) -> Result<D> {
self.load_inner(SelectionTarget::Primary, allowed).and_then(|d| {
D::try_from(d).map_err(|_| {
std::io::Error::new(
std::io::ErrorKind::Other,
"Failed to load data of the requested type.",
)
})
})
}

/// 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());
pub fn store_primary_text<T: Into<String>>(&self, text: T) {
self.store_primary(Text(text.into()));
}

fn load_inner<T: TryFrom<(Vec<u8>, MimeType)> + 'static>(
&self,
target: SelectionTarget,
allowed: impl Into<Cow<'static, [MimeType]>>,
) -> Result<T> {
let _ = self.request_sender.send(worker::Command::Load(allowed.into(), target));

match self.request_receiver.recv() {
Ok(res) => res.and_then(|(data, mime)| {
T::try_from((data, mime)).map_err(|_| {
std::io::Error::new(
std::io::ErrorKind::Other,
"Failed to load data of the requested type.",
)
})
}),
// The clipboard thread is dead, however we shouldn't crash downstream,
// so propogating an error.
Err(_) => 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);
}
}
Expand Down
Loading
Loading