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

wayland: implement clipboard

This was honestly a PITA because of its complexity.  The `clipboard`
crate (now dropped as a dep) didn't support wayland, so I looked at
the `smithay-clipboard` crate, which caused all of my input to become
laggy just by enabling it--even without actually copying or pasting!

Both of those crates try to hide a number of details of working with
the clipboard from the embedding application, but that works against
our window crate implementation, so I decided to integrate it into
the window crate using Futures so that the underlying IPC timing and
potential for the peer to flake out are not completely hidden.

This first commit removes the SystemClipboard type from wezterm
and instead bridges the window crate clipboard to the term crate
Clipboard concept.

The clipboard must be associated with a window in order to function
at all on Wayland, to we place the get/set operations in WindowOps.

This commit effectively breaks the keyboard on the other window
environments; will fix those up in follow on commits.
This commit is contained in:
Wez Furlong 2019-11-29 07:47:43 -08:00
parent c84a3f6c4c
commit 4ef20480c5
11 changed files with 416 additions and 111 deletions

View File

@ -15,7 +15,6 @@ embed-resource = "1.3"
base64 = "0.10"
base91 = { path = "base91" }
bitflags = "1.0"
clipboard = "0.5"
crossbeam-channel = "0.3"
dirs = "1.0"
downcast-rs = "1.0"

View File

@ -10,6 +10,7 @@ if test -e /etc/centos-release || test -e /etc/fedora-release; then
libxcb-devel \
libxkbcommon-devel \
libxkbcommon-x11-devel \
wayland-devel \
mesa-libEGL-devel \
xcb-util-keysyms-devel \
xcb-util-wm-devel

View File

@ -1,58 +0,0 @@
use clipboard::{ClipboardContext, ClipboardProvider};
use failure::{format_err, Fallible};
use std::sync::Mutex;
use term::terminal::Clipboard;
pub struct SystemClipboard {
inner: Mutex<Inner>,
}
struct Inner {
/// macOS gets unhappy if we set up the clipboard too early,
/// so we use an Option to defer it until we use it
clipboard: Option<ClipboardContext>,
}
impl Inner {
fn new() -> Self {
Self { clipboard: None }
}
fn clipboard(&mut self) -> Fallible<&mut ClipboardContext> {
if self.clipboard.is_none() {
self.clipboard = Some(ClipboardContext::new().map_err(|e| format_err!("{}", e))?);
}
Ok(self.clipboard.as_mut().unwrap())
}
}
impl SystemClipboard {
pub fn new() -> Self {
Self {
inner: Mutex::new(Inner::new()),
}
}
}
impl Clipboard for SystemClipboard {
fn get_contents(&self) -> Fallible<String> {
let mut inner = self.inner.lock().unwrap();
inner
.clipboard()?
.get_contents()
.map_err(|e| format_err!("{}", e))
}
fn set_contents(&self, data: Option<String>) -> Fallible<()> {
let mut inner = self.inner.lock().unwrap();
let clip = inner.clipboard()?;
clip.set_contents(data.unwrap_or_else(|| "".into()))
.map_err(|e| format_err!("{}", e))?;
// Request the clipboard contents we just set; on some systems
// if we copy and paste in wezterm, the clipboard isn't visible
// to us again until the second call to get_clipboard.
clip.get_contents()
.map(|_| ())
.map_err(|e| format_err!("{}", e))
}
}

View File

@ -1,7 +1,6 @@
use super::quad::*;
use super::renderstate::*;
use super::utilsprites::RenderMetrics;
use crate::clipboard::SystemClipboard;
use crate::config::{configuration, ConfigHandle};
use crate::font::{FontConfiguration, FontSystemSelection};
use crate::frontend::gui::tabbar::{TabBarItem, TabBarState};
@ -24,6 +23,23 @@ use term::color::ColorPalette;
use term::{CursorPosition, Line, Underline};
use termwiz::color::RgbColor;
/// ClipboardHelper bridges between the window crate clipboard
/// manipulation and the term crate clipboard interface
struct ClipboardHelper {
window: Window,
}
impl term::Clipboard for ClipboardHelper {
fn get_contents(&self) -> Fallible<String> {
self.window.get_clipboard().wait()
}
fn set_contents(&self, data: Option<String>) -> Fallible<()> {
self.window.set_clipboard(data.unwrap_or_else(String::new));
Ok(())
}
}
pub struct TermWindow {
window: Option<Window>,
fonts: Rc<FontConfiguration>,
@ -31,7 +47,6 @@ pub struct TermWindow {
mux_window_id: MuxWindowId,
render_metrics: RenderMetrics,
render_state: RenderState,
clipboard: Arc<dyn term::Clipboard>,
keys: KeyMap,
show_tab_bar: bool,
tab_bar: TabBarState,
@ -43,7 +58,7 @@ pub struct TermWindow {
struct Host<'a> {
writer: &'a mut dyn std::io::Write,
context: &'a dyn WindowOps,
clipboard: &'a Arc<dyn term::Clipboard>,
clipboard: Arc<dyn term::Clipboard>,
}
impl<'a> term::TerminalHost for Host<'a> {
@ -52,7 +67,7 @@ impl<'a> term::TerminalHost for Host<'a> {
}
fn get_clipboard(&mut self) -> Fallible<Arc<dyn term::Clipboard>> {
Ok(Arc::clone(self.clipboard))
Ok(Arc::clone(&self.clipboard))
}
fn set_title(&mut self, title: &str) {
@ -199,7 +214,9 @@ impl WindowCallbacks for TermWindow {
&mut Host {
writer: &mut *tab.writer(),
context,
clipboard: &self.clipboard,
clipboard: Arc::new(ClipboardHelper {
window: self.window.as_ref().unwrap().clone(),
}),
},
)
.ok();
@ -401,7 +418,6 @@ impl TermWindow {
dpi: 96,
},
render_state,
clipboard: Arc::new(SystemClipboard::new()),
keys: KeyMap::new(),
show_tab_bar: config.enable_tab_bar,
tab_bar: TabBarState::default(),
@ -784,10 +800,26 @@ impl TermWindow {
// self.toggle_full_screen(),
}
Copy => {
self.clipboard.set_contents(tab.selection_text())?;
// self.clipboard.set_contents(tab.selection_text())?;
log::error!("Copy pressed");
if let Some(text) = tab.selection_text() {
self.window.as_ref().unwrap().set_clipboard(text);
}
}
Paste => {
tab.trickle_paste(self.clipboard.get_contents()?)?;
let tab_id = tab.tab_id();
let future = self.window.as_ref().unwrap().get_clipboard();
Connection::get().unwrap().spawn_task(async move {
if let Ok(clip) = future.await {
promise::Future::with_executor(executor(), move || {
let mux = Mux::get().unwrap();
if let Some(tab) = mux.get_tab(tab_id) {
tab.trickle_paste(clip)?;
}
Ok(())
});
}
});
}
ActivateTabRelative(n) => {
self.activate_tab_relative(*n)?;

View File

@ -16,7 +16,6 @@ use window::{Connection, ConnectionOps};
use std::rc::Rc;
use std::sync::Arc;
mod clipboard;
mod config;
mod frontend;
mod keyassignment;

View File

@ -1,4 +1,3 @@
use crate::clipboard::SystemClipboard;
use crate::frontend::executor;
use crate::mux::domain::DomainId;
use crate::mux::renderable::Renderable;
@ -21,7 +20,7 @@ use std::sync::{Arc, Mutex};
use std::time::{Duration, Instant};
use term::color::ColorPalette;
use term::selection::SelectionRange;
use term::{Clipboard, CursorPosition, Line};
use term::{CursorPosition, Line};
use term::{KeyCode, KeyModifiers, MouseButton, MouseEvent, MouseEventKind, TerminalHost};
use termwiz::hyperlink::Hyperlink;
use termwiz::input::KeyEvent;
@ -133,7 +132,6 @@ pub struct ClientTab {
writer: RefCell<TabWriter>,
reader: Pipe,
mouse: Arc<Mutex<MouseState>>,
clipboard: Arc<dyn Clipboard>,
}
impl ClientTab {
@ -182,11 +180,6 @@ impl ClientTab {
local_tab_id,
renderable: RefCell::new(render),
writer: RefCell::new(writer),
// FIXME: ideally we'd pass down an instance of Clipboard
// rather than creating a new SystemClipboard here.
// That will be important if we end up with multiple chained
// domains in the future.
clipboard: Arc::new(SystemClipboard::new()),
reader,
}
}
@ -202,7 +195,7 @@ impl ClientTab {
.apply_changes_to_surface(delta.sequence_no, delta.changes);
}
Pdu::SetClipboard(SetClipboard { clipboard, .. }) => {
self.clipboard.set_contents(clipboard)?;
log::error!("Ignoring SetClipboard request {:?}", clipboard);
}
Pdu::OpenURL(OpenURL { url, .. }) => {
// FIXME: ideally we'd have a provider that we can

View File

@ -8,6 +8,16 @@ pub trait Clipboard {
fn set_contents(&self, data: Option<String>) -> Fallible<()>;
}
impl Clipboard for Box<dyn Clipboard> {
fn get_contents(&self) -> Fallible<String> {
self.as_ref().get_contents()
}
fn set_contents(&self, data: Option<String>) -> Fallible<()> {
self.as_ref().set_contents(data)
}
}
/// Represents the host of the terminal.
/// Provides a means for sending data to the connected pty,
/// and for operating on the clipboard

View File

@ -220,6 +220,16 @@ pub trait WindowOps {
where
Self: Sized,
R: Send + 'static;
/// Initiate textual transfer from the clipboard
fn get_clipboard(&self) -> Future<String> {
Future::err(failure::err_msg("no clip"))
}
/// Set some text in the clipboard
fn set_clipboard(&self, _text: String) -> Future<()> {
Future::err(failure::err_msg("no clip"))
}
}
pub trait WindowOpsMut {

View File

@ -143,7 +143,7 @@ impl WaylandConnection {
impl ConnectionOps for WaylandConnection {
fn spawn_task<F: std::future::Future<Output = ()> + 'static>(&self, future: F) {
let id = self.tasks.add_task(Task(Box::pin(future)));
Self::wake_task_by_id(id);
Connection::wake_task_by_id(id);
}
fn wake_task_by_id(_slot: usize) {

View File

@ -9,22 +9,31 @@ use crate::{
WindowCallbacks, WindowOps, WindowOpsMut,
};
use failure::Fallible;
use promise::Future;
use filedescriptor::{FileDescriptor, Pipe};
use promise::{Future, Promise};
use smithay_client_toolkit as toolkit;
use std::any::Any;
use std::cell::RefCell;
use std::io::{Read, Write};
use std::os::unix::io::{AsRawFd, FromRawFd};
use std::rc::Rc;
use std::sync::{Arc, Mutex};
use toolkit::keyboard::{
map_keyboard_auto_with_repeat, Event as KbEvent, KeyRepeatEvent, KeyRepeatKind, KeyState,
ModifiersState,
};
use toolkit::reexports::client::protocol::wl_data_device::{
Event as DataDeviceEvent, WlDataDevice,
};
use toolkit::reexports::client::protocol::wl_data_offer::{Event as DataOfferEvent, WlDataOffer};
use toolkit::reexports::client::protocol::wl_data_source::{
Event as DataSourceEvent, WlDataSource,
};
use toolkit::reexports::client::protocol::wl_pointer::{
self, Axis, AxisSource, Event as PointerEvent,
};
use toolkit::reexports::client::protocol::wl_seat::WlSeat;
use toolkit::reexports::client::protocol::wl_seat::{Event as SeatEvent, WlSeat};
use toolkit::reexports::client::protocol::wl_surface::WlSurface;
use toolkit::reexports::client::NewProxy;
use toolkit::shell::ShellSurface;
use toolkit::utils::MemPool;
use toolkit::window::Event;
@ -214,11 +223,70 @@ impl toolkit::window::Theme for MyTheme {
}
}
struct CopyAndPaste {
data_offer: Option<WlDataOffer>,
last_serial: u32,
data_device: Option<WlDataDevice>,
}
const TEXT_MIME_TYPE: &str = "text/plain;charset=utf-8";
impl CopyAndPaste {
fn update_last_serial(&mut self, serial: u32) {
if serial != 0 {
self.last_serial = serial;
}
}
fn get_clipboard_data(&mut self) -> Fallible<FileDescriptor> {
let offer = self
.data_offer
.as_ref()
.ok_or_else(|| failure::err_msg("no data offer"))?;
let pipe = Pipe::new()?;
offer.receive(TEXT_MIME_TYPE.to_string(), pipe.write.as_raw_fd());
Ok(pipe.read)
}
fn handle_data_offer(&mut self, event: DataOfferEvent, offer: WlDataOffer) {
match event {
DataOfferEvent::Offer { mime_type } => {
if mime_type == TEXT_MIME_TYPE {
offer.accept(self.last_serial, Some(mime_type));
self.data_offer.replace(offer);
} else {
// Refuse other mime types
offer.accept(self.last_serial, None);
}
}
DataOfferEvent::SourceActions { source_actions } => {
log::error!("Offer source_actions {}", source_actions);
}
DataOfferEvent::Action { dnd_action } => {
log::error!("Offer dnd_action {}", dnd_action);
}
_ => {}
}
}
fn confirm_selection(&mut self, offer: WlDataOffer) {
self.data_offer.replace(offer);
}
fn set_selection(&mut self, source: WlDataSource) {
if let Some(dev) = self.data_device.as_ref() {
dev.set_selection(Some(&source), self.last_serial);
}
}
}
pub struct WaylandWindowInner {
window_id: usize,
callbacks: Box<dyn WindowCallbacks>,
surface: WlSurface,
#[allow(dead_code)]
seat: WlSeat,
copy_and_paste: Arc<Mutex<CopyAndPaste>>,
window: Option<toolkit::window::Window<toolkit::window::ConceptFrame>>,
pool: MemPool,
dimensions: (u32, u32),
@ -273,6 +341,7 @@ impl WaylandWindow {
window.set_app_id(class_name.to_string());
window.set_decorate(true);
window.set_resizable(true);
window.set_title(name.to_string());
window.set_theme(MyTheme {});
let pool = MemPool::new(&conn.environment.borrow().shm, || {})?;
@ -281,10 +350,86 @@ impl WaylandWindow {
.environment
.borrow()
.manager
.instantiate_range(1, 6, NewProxy::implement_dummy)
.instantiate_range(1, 6, move |seat| {
seat.implement_closure(
move |event, _seat| {
if let SeatEvent::Name { name } = event {
log::error!("seat name is {}", name);
}
},
(),
)
})
.map_err(|_| failure::format_err!("Failed to create seat"))?;
window.new_seat(&seat);
let copy_and_paste = Arc::new(Mutex::new(CopyAndPaste {
data_offer: None,
last_serial: 0,
data_device: None,
}));
let data_device = conn
.environment
.borrow()
.data_device_manager
.get_data_device(&seat, {
let copy_and_paste = Arc::clone(&copy_and_paste);
move |device| {
device.implement_closure(
{
let copy_and_paste = Arc::clone(&copy_and_paste);
move |event, _device| match event {
DataDeviceEvent::DataOffer { id } => {
log::error!("DataDeviceEvent DataOffer");
id.implement_closure(
{
let copy_and_paste = Arc::clone(&copy_and_paste);
move |event, offer| {
copy_and_paste
.lock()
.unwrap()
.handle_data_offer(event, offer);
}
},
(),
);
}
DataDeviceEvent::Enter { .. } => {
log::error!("DataDeviceEvent Enter")
}
DataDeviceEvent::Leave { .. } => {
log::error!("DataDeviceEvent Leave")
}
DataDeviceEvent::Motion { .. } => {
log::error!("DataDeviceEvent Motion")
}
DataDeviceEvent::Drop => log::error!("DataDeviceEvent Drop"),
DataDeviceEvent::Selection { id } => {
log::error!(
"DataDeviceEvent Selection {}",
if id.is_some() { "Y" } else { "None" }
);
if let Some(offer) = id {
copy_and_paste.lock().unwrap().confirm_selection(offer);
}
}
_ => {}
}
},
(),
)
}
})
.map_err(|_| failure::format_err!("Failed to configure data_device"))?;
copy_and_paste
.lock()
.unwrap()
.data_device
.replace(data_device);
seat.get_pointer(move |ptr| {
ptr.implement_closure(
move |evt, _| {
@ -302,31 +447,44 @@ impl WaylandWindow {
map_keyboard_auto_with_repeat(
&seat,
KeyRepeatKind::System,
move |event: KbEvent, _| match event {
KbEvent::Key {
rawkey,
keysym,
state,
utf8,
..
} => {
WaylandConnection::with_window_inner(window_id, move |inner| {
inner.handle_key(state == KeyState::Pressed, rawkey, keysym, utf8.clone());
Ok(())
});
{
let copy_and_paste = Arc::clone(&copy_and_paste);
move |event: KbEvent, _| match event {
KbEvent::Enter { serial, .. } => {
copy_and_paste.lock().unwrap().update_last_serial(serial);
}
KbEvent::Key {
rawkey,
keysym,
state,
utf8,
serial,
..
} => {
WaylandConnection::with_window_inner(window_id, move |inner| {
inner.handle_key(
serial,
state == KeyState::Pressed,
rawkey,
keysym,
utf8.clone(),
);
Ok(())
});
}
KbEvent::Modifiers { modifiers } => {
let mods = modifier_keys(modifiers);
WaylandConnection::with_window_inner(window_id, move |inner| {
inner.handle_modifiers(mods);
Ok(())
});
}
_ => {}
}
KbEvent::Modifiers { modifiers } => {
let mods = modifier_keys(modifiers);
WaylandConnection::with_window_inner(window_id, move |inner| {
inner.handle_modifiers(mods);
Ok(())
});
}
_ => {}
},
move |event: KeyRepeatEvent, _| {
WaylandConnection::with_window_inner(window_id, move |inner| {
inner.handle_key(true, event.rawkey, event.keysym, event.utf8.clone());
inner.handle_key(0, true, event.rawkey, event.keysym, event.utf8.clone());
Ok(())
});
},
@ -334,6 +492,7 @@ impl WaylandWindow {
.map_err(|_| failure::format_err!("Failed to configure keyboard callback"))?;
let inner = Rc::new(RefCell::new(WaylandWindowInner {
copy_and_paste,
window_id,
callbacks,
surface,
@ -358,7 +517,18 @@ impl WaylandWindow {
}
impl WaylandWindowInner {
fn handle_key(&mut self, key_is_down: bool, rawkey: u32, keysym: u32, utf8: Option<String>) {
fn handle_key(
&mut self,
serial: u32,
key_is_down: bool,
rawkey: u32,
keysym: u32,
utf8: Option<String>,
) {
self.copy_and_paste
.lock()
.unwrap()
.update_last_serial(serial);
let raw_key = keysym_to_keycode(keysym);
let (key, raw_key) = match utf8 {
Some(text) if text.chars().count() == 1 => {
@ -397,16 +567,21 @@ impl WaylandWindowInner {
fn handle_pointer(&mut self, evt: SendablePointerEvent) {
match evt {
SendablePointerEvent::Enter { .. } => {}
SendablePointerEvent::Enter { serial, .. } => {
self.copy_and_paste
.lock()
.unwrap()
.update_last_serial(serial);
}
SendablePointerEvent::Leave { .. } => {}
SendablePointerEvent::AxisSource { .. } => {}
SendablePointerEvent::AxisStop { .. } => {}
SendablePointerEvent::AxisDiscrete { .. } => {}
SendablePointerEvent::Frame => {}
SendablePointerEvent::Motion {
time,
surface_x,
surface_y,
..
} => {
let factor = toolkit::surface::get_dpi_factor(&self.surface);
let coords = Point::new(
@ -427,7 +602,16 @@ impl WaylandWindowInner {
self.callbacks
.mouse_event(&event, &Window::Wayland(WaylandWindow(self.window_id)));
}
SendablePointerEvent::Button { button, state, .. } => {
SendablePointerEvent::Button {
button,
state,
serial,
..
} => {
self.copy_and_paste
.lock()
.unwrap()
.update_last_serial(serial);
fn linux_button(b: u32) -> Option<MousePress> {
// See BTN_LEFT and friends in <linux/input-event-codes.h>
match b {
@ -747,6 +931,126 @@ impl WindowOps for WaylandWindow {
)
})
}
fn get_clipboard(&self) -> Future<String> {
let mut promise = Promise::new();
let future = promise.get_future().unwrap();
let promise = Arc::new(Mutex::new(promise));
WaylandConnection::with_window_inner(self.0, move |inner| {
let read = inner.copy_and_paste.lock().unwrap().get_clipboard_data()?;
let promise = Arc::clone(&promise);
std::thread::spawn(move || {
let mut promise = promise.lock().unwrap();
match read_pipe_with_timeout(read) {
Ok(result) => {
promise.ok(result);
}
Err(e) => {
log::error!("while reading clipboard: {}", e);
promise.err(failure::format_err!("{}", e));
}
};
});
Ok(())
});
future
}
fn set_clipboard(&self, text: String) -> Future<()> {
WaylandConnection::with_window_inner(self.0, move |inner| {
let text = text.clone();
let conn = Connection::get().unwrap().wayland();
let source = conn
.environment
.borrow()
.data_device_manager
.create_data_source(move |source| {
source.implement_closure(
move |event, _source| match event {
DataSourceEvent::Send { fd, .. } => {
let fd = unsafe { FileDescriptor::from_raw_fd(fd) };
if let Err(e) = write_pipe_with_timeout(fd, text.as_bytes()) {
log::error!("while sending paste to pipe: {}", e);
}
}
_ => {}
},
(),
)
})
.map_err(|_| failure::format_err!("failed to create data source"))?;
source.offer(TEXT_MIME_TYPE.to_string());
inner.copy_and_paste.lock().unwrap().set_selection(source);
Ok(())
})
}
}
fn write_pipe_with_timeout(mut file: FileDescriptor, data: &[u8]) -> Fallible<()> {
let on: libc::c_int = 1;
unsafe {
libc::ioctl(file.as_raw_fd(), libc::FIONBIO, &on);
}
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 => {
failure::bail!("zero byte write");
}
Ok(size) => {
buf = &buf[size..];
}
Err(e) => failure::bail!("error writing to pipe: {}", e),
}
} else {
failure::bail!("timed out writing to pipe");
}
}
Ok(())
}
fn read_pipe_with_timeout(mut file: FileDescriptor) -> Fallible<String> {
let mut result = Vec::new();
let on: libc::c_int = 1;
unsafe {
libc::ioctl(file.as_raw_fd(), libc::FIONBIO, &on);
}
let mut pfd = libc::pollfd {
fd: file.as_raw_fd(),
events: libc::POLLIN,
revents: 0,
};
let mut buf = [0u8; 8192];
loop {
if unsafe { libc::poll(&mut pfd, 1, 3000) == 1 } {
match file.read(&mut buf) {
Ok(size) if size == 0 => {
break;
}
Ok(size) => {
result.extend_from_slice(&buf[..size]);
}
Err(e) => failure::bail!("error reading from pipe: {}", e),
}
} else {
failure::bail!("timed out reading from pipe");
}
}
Ok(String::from_utf8(result)?)
}
impl WindowOpsMut for WaylandWindowInner {
@ -774,16 +1078,16 @@ impl WindowOpsMut for WaylandWindowInner {
}
}
fn set_cursor(&mut self, cursor: Option<MouseCursor>) {}
fn set_cursor(&mut self, _cursor: Option<MouseCursor>) {}
fn invalidate(&mut self) {
self.need_paint = true;
self.do_paint().unwrap();
}
fn set_inner_size(&self, width: usize, height: usize) {}
fn set_inner_size(&self, _width: usize, _height: usize) {}
fn set_window_position(&self, coords: ScreenPoint) {}
fn set_window_position(&self, _coords: ScreenPoint) {}
/// Change the title for the window manager
fn set_title(&mut self, title: &str) {

View File

@ -251,4 +251,19 @@ impl WindowOps for Window {
Self::Wayland(w) => w.enable_opengl(func),
}
}
fn get_clipboard(&self) -> Future<String> {
match self {
Self::X11(x) => x.get_clipboard(),
#[cfg(feature = "wayland")]
Self::Wayland(w) => w.get_clipboard(),
}
}
fn set_clipboard(&self, text: String) -> Future<()> {
match self {
Self::X11(x) => x.set_clipboard(text),
#[cfg(feature = "wayland")]
Self::Wayland(w) => w.set_clipboard(text),
}
}
}