1
1
mirror of https://github.com/wez/wezterm.git synced 2024-11-22 22:42:48 +03:00

Implement primary selection support

This commit is contained in:
Timmy Xiao 2024-01-24 15:54:32 -08:00 committed by Wez Furlong
parent 561d360e0a
commit 125e89c3ae
No known key found for this signature in database
GPG Key ID: 7A7F66A31EC9B387
4 changed files with 275 additions and 58 deletions

View File

@ -1,7 +1,15 @@
use anyhow::{anyhow, Error};
use anyhow::{anyhow, Error, bail};
use filedescriptor::{FileDescriptor, Pipe};
use smithay_client_toolkit as toolkit;
use std::os::fd::AsRawFd;
use toolkit::globals::GlobalData;
use wayland_client::{Dispatch, event_created_child};
use wayland_client::globals::{GlobalList, BindError};
use wayland_protocols::wp::primary_selection::zv1::client::zwp_primary_selection_device_manager_v1::ZwpPrimarySelectionDeviceManagerV1;
use wayland_protocols::wp::primary_selection::zv1::client::zwp_primary_selection_device_v1::{ZwpPrimarySelectionDeviceV1, self, Event as PrimarySelectionDeviceEvent};
use wayland_protocols::wp::primary_selection::zv1::client::zwp_primary_selection_offer_v1::{ZwpPrimarySelectionOfferV1, Event as PrimarySelectionOfferEvent};
use wayland_protocols::wp::primary_selection::zv1::client::zwp_primary_selection_source_v1::{ZwpPrimarySelectionSourceV1, Event as PrimarySelectionSourceEvent};
use std::io::Write;
use std::os::fd::{AsRawFd, FromRawFd, IntoRawFd};
use std::sync::{Arc, Mutex};
use toolkit::reexports::client::protocol::wl_data_offer::WlDataOffer;
@ -30,32 +38,71 @@ impl CopyAndPaste {
pub(super) fn get_clipboard_data(
&mut self,
_clipboard: Clipboard,
clipboard: Clipboard,
) -> anyhow::Result<FileDescriptor> {
// TODO; primary selection
let offer = self
.data_offer
.as_ref()
.ok_or_else(|| anyhow!("no data offer"))?;
let pipe = Pipe::new().map_err(Error::msg)?;
offer.receive(TEXT_MIME_TYPE.to_string(), pipe.write.as_raw_fd());
Ok(pipe.read)
let conn = crate::Connection::get().unwrap().wayland();
let wayland_state = conn.wayland_state.borrow();
let primary_selection = if let Clipboard::PrimarySelection = clipboard {
wayland_state.primary_selection_manager.as_ref()
} else {
None
};
match primary_selection {
Some(primary_selection) => {
let inner = primary_selection.inner.lock().unwrap();
let offer = inner
.offer
.as_ref()
.ok_or_else(|| anyhow!("no primary selection offer"))?;
let pipe = Pipe::new().map_err(Error::msg)?;
offer.receive(TEXT_MIME_TYPE.to_string(), pipe.write.as_raw_fd());
Ok(pipe.read)
}
None => {
let offer = self
.data_offer
.as_ref()
.ok_or_else(|| anyhow!("no data offer"))?;
let pipe = Pipe::new().map_err(Error::msg)?;
offer.receive(TEXT_MIME_TYPE.to_string(), pipe.write.as_raw_fd());
Ok(pipe.read)
}
}
}
pub(super) fn set_clipboard_data(&mut self, _clipboard: Clipboard, data: String) {
// TODO: primary selection
pub(super) fn set_clipboard_data(&mut self, clipboard: Clipboard, data: String) {
let conn = crate::Connection::get().unwrap().wayland();
let qh = conn.event_queue.borrow().handle();
let mut wayland_state = conn.wayland_state.borrow_mut();
let last_serial = *wayland_state.last_serial.borrow();
let data_device = &wayland_state.data_device;
let source = wayland_state
.data_device_manager_state
.create_copy_paste_source(&qh, vec![TEXT_MIME_TYPE]);
source.set_selection(data_device.as_ref().unwrap(), last_serial);
wayland_state.copy_paste_source.replace((source, data));
let primary_selection = if let Clipboard::PrimarySelection = clipboard {
wayland_state.primary_selection_manager.as_ref()
} else {
None
};
match primary_selection {
Some(primary_selection) => {
let manager = &primary_selection.manager;
let selection_device = wayland_state.primary_select_device.as_ref().unwrap();
let source = manager.create_source(&qh, PrimarySelectionManagerData::default());
source.offer(TEXT_MIME_TYPE.to_string());
selection_device.set_selection(Some(&source), last_serial);
wayland_state
.primary_selection_source
.replace((source, data));
}
None => {
let data_device = &wayland_state.data_device;
let source = wayland_state
.data_device_manager_state
.create_copy_paste_source(&qh, vec![TEXT_MIME_TYPE]);
source.set_selection(data_device.as_ref().unwrap(), last_serial);
wayland_state.copy_paste_source.replace((source, data));
}
}
}
pub(super) fn confirm_selection(&mut self, offer: WlDataOffer) {
@ -74,3 +121,186 @@ impl WaylandState {
}
}
}
pub(super) fn write_selection_to_pipe(fd: FileDescriptor, text: &str) {
if let Err(e) = write_pipe_with_timeout(fd, text.as_bytes()) {
log::error!("while sending primary selection to pipe: {}", e);
}
}
fn write_pipe_with_timeout(mut file: FileDescriptor, data: &[u8]) -> anyhow::Result<()> {
file.set_non_blocking(true)?;
let mut pfd = libc::pollfd {
fd: file.as_raw_fd(),
events: libc::POLLOUT,
revents: 0,
};
let mut buf = data;
while !buf.is_empty() {
if unsafe { libc::poll(&mut pfd, 1, 3000) == 1 } {
match file.write(buf) {
Ok(size) if size == 0 => {
bail!("zero byte write");
}
Ok(size) => {
buf = &buf[size..];
}
Err(e) => bail!("error writing to pipe: {}", e),
}
} else {
bail!("timed out writing to pipe");
}
}
Ok(())
}
// Smithay has their own primary selection handler in 0.18
// Some code borrowed from https://github.com/Smithay/client-toolkit/commit/4a5c4f59f640bc588a55277261bbed1bd2abea98
pub(super) struct PrimarySelectionManagerState {
pub(super) manager: ZwpPrimarySelectionDeviceManagerV1,
inner: Mutex<PrimaryInner>,
}
#[derive(Default, Debug)]
struct PrimaryInner {
pending_offer: Option<ZwpPrimarySelectionOfferV1>,
offer: Option<ZwpPrimarySelectionOfferV1>,
valid_mime: bool,
}
#[derive(Default)]
pub(super) struct PrimarySelectionManagerData {}
impl PrimarySelectionManagerState {
pub(super) fn bind(
globals: &GlobalList,
queue_handle: &wayland_client::QueueHandle<WaylandState>,
) -> Result<Self, BindError> {
let manager = globals.bind(queue_handle, 1..=1, GlobalData)?;
Ok(Self {
manager,
inner: Mutex::new(PrimaryInner::default()),
})
}
}
impl Dispatch<ZwpPrimarySelectionDeviceManagerV1, GlobalData, WaylandState>
for PrimarySelectionManagerState
{
fn event(
_state: &mut WaylandState,
_proxy: &ZwpPrimarySelectionDeviceManagerV1,
_event: <ZwpPrimarySelectionDeviceManagerV1 as wayland_client::Proxy>::Event,
_data: &GlobalData,
_conn: &wayland_client::Connection,
_qhandle: &wayland_client::QueueHandle<WaylandState>,
) {
unreachable!("primary selection manager has no events");
}
}
impl Dispatch<ZwpPrimarySelectionSourceV1, PrimarySelectionManagerData, WaylandState>
for PrimarySelectionManagerState
{
fn event(
state: &mut WaylandState,
source: &ZwpPrimarySelectionSourceV1,
event: <ZwpPrimarySelectionSourceV1 as wayland_client::Proxy>::Event,
_data: &PrimarySelectionManagerData,
_conn: &wayland_client::Connection,
_qhandle: &wayland_client::QueueHandle<WaylandState>,
) {
match event {
PrimarySelectionSourceEvent::Send { mime_type, fd } => {
if mime_type != TEXT_MIME_TYPE {
return;
};
if let Some((ps_source, data)) = &state.primary_selection_source {
if ps_source != source {
return;
}
let fd = unsafe { FileDescriptor::from_raw_fd(fd.into_raw_fd()) };
write_selection_to_pipe(fd, data);
}
}
PrimarySelectionSourceEvent::Cancelled => {
state.primary_selection_source.take();
source.destroy();
}
_ => unreachable!(),
}
}
}
impl Dispatch<ZwpPrimarySelectionOfferV1, PrimarySelectionManagerData, WaylandState>
for PrimarySelectionManagerState
{
fn event(
state: &mut WaylandState,
_proxy: &ZwpPrimarySelectionOfferV1,
event: <ZwpPrimarySelectionOfferV1 as wayland_client::Proxy>::Event,
_data: &PrimarySelectionManagerData,
_conn: &wayland_client::Connection,
_qhandle: &wayland_client::QueueHandle<WaylandState>,
) {
match event {
PrimarySelectionOfferEvent::Offer { mime_type } => {
if mime_type == TEXT_MIME_TYPE {
let mgr = state.primary_selection_manager.as_ref().unwrap();
let mut inner = mgr.inner.lock().unwrap();
inner.valid_mime = true;
}
}
_ => unreachable!(),
}
}
}
impl Dispatch<ZwpPrimarySelectionDeviceV1, PrimarySelectionManagerData, WaylandState>
for PrimarySelectionManagerState
{
event_created_child!(WaylandState, ZwpPrimarySelectionDeviceV1, [
zwp_primary_selection_device_v1::EVT_DATA_OFFER_OPCODE => (ZwpPrimarySelectionOfferV1, PrimarySelectionManagerData::default())
]);
fn event(
state: &mut WaylandState,
_primary_selection_device: &ZwpPrimarySelectionDeviceV1,
event: <ZwpPrimarySelectionDeviceV1 as wayland_client::Proxy>::Event,
_data: &PrimarySelectionManagerData,
_conn: &wayland_client::Connection,
_qhandle: &wayland_client::QueueHandle<WaylandState>,
) {
let psm = state.primary_selection_manager.as_ref().unwrap();
let mut inner = psm.inner.lock().unwrap();
match event {
PrimarySelectionDeviceEvent::DataOffer { offer } => {
inner.pending_offer = Some(offer);
}
PrimarySelectionDeviceEvent::Selection { id } => {
if !inner.valid_mime {
return;
}
if let Some(offer) = inner.offer.take() {
offer.destroy();
}
if id == inner.pending_offer {
inner.offer = inner.pending_offer.take();
} else {
// Remove the pending offer, assign the new delivered one.
if let Some(offer) = inner.pending_offer.take() {
offer.destroy()
}
inner.offer = id;
}
}
_ => unreachable!(),
}
}
}

View File

@ -1,7 +1,5 @@
use std::io::Write;
use std::os::fd::{AsRawFd, FromRawFd, IntoRawFd};
use std::os::fd::{FromRawFd, IntoRawFd};
use anyhow::bail;
use filedescriptor::FileDescriptor;
use smithay_client_toolkit::data_device_manager::data_device::{
DataDevice, DataDeviceDataExt, DataDeviceHandler,
@ -16,6 +14,7 @@ use crate::wayland::drag_and_drop::SurfaceAndOffer;
use crate::wayland::pointer::PointerUserData;
use crate::wayland::SurfaceUserData;
use super::copy_and_paste::write_selection_to_pipe;
use super::drag_and_drop::{DragAndDrop, SurfaceAndPipe};
use super::state::WaylandState;
@ -239,38 +238,3 @@ impl DataSourceHandler for WaylandState {
) {
}
}
fn write_selection_to_pipe(fd: FileDescriptor, text: &str) {
if let Err(e) = write_pipe_with_timeout(fd, text.as_bytes()) {
log::error!("while sending primary selection to pipe: {}", e);
}
}
fn write_pipe_with_timeout(mut file: FileDescriptor, data: &[u8]) -> anyhow::Result<()> {
file.set_non_blocking(true)?;
let mut pfd = libc::pollfd {
fd: file.as_raw_fd(),
events: libc::POLLOUT,
revents: 0,
};
let mut buf = data;
while !buf.is_empty() {
if unsafe { libc::poll(&mut pfd, 1, 3000) == 1 } {
match file.write(buf) {
Ok(size) if size == 0 => {
bail!("zero byte write");
}
Ok(size) => {
buf = &buf[size..];
}
Err(e) => bail!("error writing to pipe: {}", e),
}
} else {
bail!("timed out writing to pipe");
}
}
Ok(())
}

View File

@ -3,6 +3,7 @@ use smithay_client_toolkit::seat::{Capability, SeatHandler, SeatState};
use wayland_client::protocol::wl_seat::WlSeat;
use wayland_client::{Connection, QueueHandle};
use crate::wayland::copy_and_paste::PrimarySelectionManagerData;
use crate::wayland::keyboard::KeyboardData;
use crate::wayland::pointer::PointerUserData;
@ -48,6 +49,12 @@ impl SeatHandler for WaylandState {
let data_device_manager = &self.data_device_manager_state;
let data_device = data_device_manager.get_data_device(qh, &seat);
self.data_device.replace(data_device);
let primary_select_device = self.primary_selection_manager.as_ref().map(|m| {
m.manager
.get_device(&seat, qh, PrimarySelectionManagerData::default())
});
self.primary_select_device = primary_select_device;
}
}

View File

@ -30,11 +30,16 @@ use wayland_client::protocol::wl_output::WlOutput;
use wayland_client::protocol::wl_pointer::WlPointer;
use wayland_client::protocol::wl_surface::WlSurface;
use wayland_client::{delegate_dispatch, Connection, QueueHandle};
use wayland_protocols::wp::primary_selection::zv1::client::zwp_primary_selection_device_manager_v1::ZwpPrimarySelectionDeviceManagerV1;
use wayland_protocols::wp::primary_selection::zv1::client::zwp_primary_selection_device_v1::ZwpPrimarySelectionDeviceV1;
use wayland_protocols::wp::primary_selection::zv1::client::zwp_primary_selection_offer_v1::ZwpPrimarySelectionOfferV1;
use wayland_protocols::wp::primary_selection::zv1::client::zwp_primary_selection_source_v1::ZwpPrimarySelectionSourceV1;
use wayland_protocols::wp::text_input::zv3::client::zwp_text_input_manager_v3::ZwpTextInputManagerV3;
use wayland_protocols::wp::text_input::zv3::client::zwp_text_input_v3::ZwpTextInputV3;
use crate::x11::KeyboardWithFallback;
use super::copy_and_paste::{PrimarySelectionManagerData, PrimarySelectionManagerState};
use super::inputhandler::{TextInputData, TextInputState};
use super::pointer::{PendingMouse, PointerUserData};
use super::{OutputManagerData, OutputManagerState, SurfaceUserData, WaylandWindowInner};
@ -65,6 +70,9 @@ pub(super) struct WaylandState {
pub(super) data_device_manager_state: DataDeviceManagerState,
pub(super) data_device: Option<DataDevice>,
pub(super) copy_paste_source: Option<(CopyPasteSource, String)>,
pub(super) primary_selection_manager: Option<PrimarySelectionManagerState>,
pub(super) primary_select_device: Option<ZwpPrimarySelectionDeviceV1>,
pub(super) primary_selection_source: Option<(ZwpPrimarySelectionSourceV1, String)>,
pub(super) shm: Shm,
pub(super) mem_pool: RefCell<SlotPool>,
}
@ -98,6 +106,9 @@ impl WaylandState {
data_device_manager_state: DataDeviceManagerState::bind(globals, qh)?,
data_device: None,
copy_paste_source: None,
primary_selection_manager: PrimarySelectionManagerState::bind(globals, qh).ok(),
primary_select_device: None,
primary_selection_source: None,
shm,
mem_pool: RefCell::new(mem_pool),
};
@ -169,3 +180,8 @@ delegate_dispatch!(WaylandState: [ZwpTextInputV3: TextInputData] => TextInputSta
delegate_dispatch!(WaylandState: [ZwlrOutputManagerV1: GlobalData] => OutputManagerState);
delegate_dispatch!(WaylandState: [ZwlrOutputHeadV1: OutputManagerData] => OutputManagerState);
delegate_dispatch!(WaylandState: [ZwlrOutputModeV1: OutputManagerData] => OutputManagerState);
delegate_dispatch!(WaylandState: [ZwpPrimarySelectionDeviceManagerV1: GlobalData] => PrimarySelectionManagerState);
delegate_dispatch!(WaylandState: [ZwpPrimarySelectionDeviceV1: PrimarySelectionManagerData] => PrimarySelectionManagerState);
delegate_dispatch!(WaylandState: [ZwpPrimarySelectionSourceV1: PrimarySelectionManagerData] => PrimarySelectionManagerState);
delegate_dispatch!(WaylandState: [ZwpPrimarySelectionOfferV1: PrimarySelectionManagerData] => PrimarySelectionManagerState);