feat: support custom mime types

This commit is contained in:
Ashley Wulber 2024-02-27 17:50:33 -05:00
parent eebb02816e
commit 3e56207b3a
No known key found for this signature in database
GPG key ID: 5216D4F46A90A820
7 changed files with 335 additions and 116 deletions

View file

@ -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"]

View file

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

View file

@ -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()));
}
}

View file

@ -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]),
}
}
}

View file

@ -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
View 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,
}
}
}

View file

@ -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,
}
}
})