From 17011ea9f7a57bcee97bf2f7b31b22bbe03f1ec0 Mon Sep 17 00:00:00 2001 From: Lucas Timmins Date: Sun, 10 Feb 2019 01:28:53 +0800 Subject: [PATCH] Initial commit --- .gitignore | 3 + .travis.yml | 76 +++++++++++++++ CHANGELOG.md | 0 CONTRIBUTING.md | 9 ++ Cargo.toml | 11 +++ LICENSE | 19 ++++ README.md | 13 +++ doc_index.html | 6 ++ examples/clipboard.rs | 214 ++++++++++++++++++++++++++++++++++++++++++ src/lib.rs | 159 +++++++++++++++++++++++++++++++ 10 files changed, 510 insertions(+) create mode 100644 .gitignore create mode 100644 .travis.yml create mode 100644 CHANGELOG.md create mode 100644 CONTRIBUTING.md create mode 100644 Cargo.toml create mode 100644 LICENSE create mode 100644 README.md create mode 100644 doc_index.html create mode 100644 examples/clipboard.rs create mode 100644 src/lib.rs diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..6936990 --- /dev/null +++ b/.gitignore @@ -0,0 +1,3 @@ +/target +**/*.rs.bk +Cargo.lock diff --git a/.travis.yml b/.travis.yml new file mode 100644 index 0000000..ca832bf --- /dev/null +++ b/.travis.yml @@ -0,0 +1,76 @@ +language: rust + +# only cache cargo subcommand binaries and wayland libs .so +# the build artifacts take a lot of space and are slower to +# cache than to actually rebuild anyway... +# We need to cache the whole .cargo directory to keep the +# .crates.toml file. +cache: + directories: + - /home/travis/.cargo +# But don't cache the cargo registry +before_cache: + - rm -rf /home/travis/.cargo/registry + +dist: trusty + +sudo: required + +rust: + - stable + - beta + - nightly + +matrix: + allow_failures: + - rust: nightly + include: + - rust: stable + env: BUILD_FMT=1 + - rust: stable + env: BUILD_DOC=1 + +branches: + only: + - master + +before_script: + - cargo fetch + - | + if [ -n "$BUILD_FMT" ]; then + rustup component add rustfmt-preview + elif [ -n "$BUILD_DOC" ]; then + echo "Building doc, nothing to install..." + fi +os: + - linux + +script: + - | + if [ -n "$BUILD_FMT" ]; then + cargo fmt -- --check + elif [ -n "$BUILD_DOC" ]; then + cargo doc --no-deps --all-features + fi +after_success: + - | + if [ -n "$BUILD_DOC" ]; then + cp ./doc_index.html ./target/doc/index.html + fi +deploy: + provider: pages + skip_cleanup: true + github_token: $GITHUB_TOKEN + local_dir: "target/doc" + on: + branch: master + rust: stable + condition: $BUILD_DOC = 1 + +notifications: + webhooks: + urls: + - "https://scalar.vector.im/api/neb/services/hooks/dHJhdmlzLWNpLyU0MGxldmFucyUzQXNhZmFyYWRlZy5uZXQvJTIxRkt4aGprSUNwakJWelZlQ2RGJTNBc2FmYXJhZGVnLm5ldA" + on_success: change + on_failure: always +on_start: never diff --git a/CHANGELOG.md b/CHANGELOG.md new file mode 100644 index 0000000..e69de29 diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md new file mode 100644 index 0000000..23a0271 --- /dev/null +++ b/CONTRIBUTING.md @@ -0,0 +1,9 @@ +# Contributing + +Smithay Wayland Clipboard is open to contributions from anyone. + +## Smithay Project + +There is a Matrix room dedicated to the Smithay project: +[#smithay:matrix.org](https://matrix.to/#/#smithay:matrix.org). If you don't want to use matrix, this room is +also bridged to gitter: https://gitter.im/smithay/Lobby. diff --git a/Cargo.toml b/Cargo.toml new file mode 100644 index 0000000..ff2b048 --- /dev/null +++ b/Cargo.toml @@ -0,0 +1,11 @@ +[package] +name = "smithay-wayland-clipboard" +version = "0.1.0" +authors = ["Lucas Timmins "] +edition = "2018" + +[dependencies] +sctk = { package = "smithay-client-toolkit", version = "0.5" } + +[dev-dependencies] +andrew = "0.2" diff --git a/LICENSE b/LICENSE new file mode 100644 index 0000000..c2ac424 --- /dev/null +++ b/LICENSE @@ -0,0 +1,19 @@ +Copyright (c) 2018 Lucas Timmins & Victor Berger + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in +all copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +THE SOFTWARE. diff --git a/README.md b/README.md new file mode 100644 index 0000000..7339cd6 --- /dev/null +++ b/README.md @@ -0,0 +1,13 @@ +[![crates.io](http://meritbadge.herokuapp.com/smithay-wayland-clipboard)](https://crates.io/crates/smithay-wayland-clipboard) +[![Build Status](https://travis-ci.org/Smithay/wayland-clipboard.svg?branch=master)](https://travis-ci.org/Smithay/wayland-clipboard) + + +# Smithay Wayland Clipboard + +This crate provides access to the wayland clipboard with only requirement being a WlDisplay object. + +## Documentation + +The documentation for the master branch is [available online](https://smithay.github.io/wayland-clipboard/). + +The documentation for the releases can be found on [docs.rs](https://docs.rs/smithay-wayland-clipboard). diff --git a/doc_index.html b/doc_index.html new file mode 100644 index 0000000..5cdfc8e --- /dev/null +++ b/doc_index.html @@ -0,0 +1,6 @@ + + + + + + diff --git a/examples/clipboard.rs b/examples/clipboard.rs new file mode 100644 index 0000000..0ea5354 --- /dev/null +++ b/examples/clipboard.rs @@ -0,0 +1,214 @@ +use std::io::{Read, Seek, SeekFrom, Write}; +use std::sync::{Arc, Mutex}; + +use sctk::keyboard::{map_keyboard_auto, Event as KbEvent, KeyState}; +use sctk::utils::{DoubleMemPool, MemPool}; +use sctk::window::{ConceptFrame, Event as WEvent, Window}; +use sctk::Environment; + +use sctk::reexports::client::protocol::{wl_shm, wl_surface}; +use sctk::reexports::client::{Display, NewProxy}; + +use andrew::shapes::rectangle; +use andrew::text; +use andrew::text::fontconfig; + +fn main() { + let (display, mut event_queue) = + Display::connect_to_env().expect("Failed to connect to the wayland server."); + let env = Environment::from_display(&*display, &mut event_queue).unwrap(); + + let mut clipboard = wayland_clipboard::WaylandClipboard::new_threaded( + display.get_display_ptr() as *mut std::ffi::c_void, + ); + let cb_contents = Arc::new(Mutex::new(String::new())); + + let seat = env + .manager + .instantiate_range(1, 6, NewProxy::implement_dummy) + .unwrap(); + + let cb_contents_clone = cb_contents.clone(); + map_keyboard_auto(&seat, move |event: KbEvent, _| { + if let KbEvent::Key { + state: KeyState::Pressed, + utf8: Some(text), + .. + } = event + { + if text == " " { + *cb_contents_clone.lock().unwrap() = dbg!(clipboard.load()); + } else if text == "s" { + clipboard + .store("This is an example text thats been copied to the wayland clipboard :)"); + } + } + }) + .unwrap(); + + let mut dimensions = (320u32, 240u32); + let surface = env + .compositor + .create_surface(NewProxy::implement_dummy) + .unwrap(); + + let next_action = Arc::new(Mutex::new(None::)); + + let waction = next_action.clone(); + let mut window = Window::::init_from_env(&env, surface, dimensions, move |evt| { + let mut next_action = waction.lock().unwrap(); + // Keep last event in priority order : Close > Configure > Refresh + let replace = match (&evt, &*next_action) { + (_, &None) + | (_, &Some(WEvent::Refresh)) + | (&WEvent::Configure { .. }, &Some(WEvent::Configure { .. })) + | (&WEvent::Close, _) => true, + _ => false, + }; + if replace { + *next_action = Some(evt); + } + }) + .expect("Failed to create a window !"); + + window.new_seat(&seat); + window.set_title("Clipboard".to_string()); + + let mut pools = DoubleMemPool::new(&env.shm, || {}).expect("Failed to create a memory pool !"); + + let mut font_data = Vec::new(); + std::fs::File::open( + &fontconfig::FontConfig::new() + .unwrap() + .get_regular_family_fonts("sans") + .unwrap()[0], + ) + .unwrap() + .read_to_end(&mut font_data) + .unwrap(); + + if !env.shell.needs_configure() { + // initial draw to bootstrap on wl_shell + if let Some(pool) = pools.pool() { + redraw( + pool, + window.surface(), + dimensions, + &font_data, + "".to_string(), + ); + } + window.refresh(); + } + + loop { + match next_action.lock().unwrap().take() { + Some(WEvent::Close) => break, + Some(WEvent::Refresh) => { + window.refresh(); + window.surface().commit(); + } + Some(WEvent::Configure { new_size, .. }) => { + if let Some((w, h)) = new_size { + window.resize(w, h); + dimensions = (w, h) + } + window.refresh(); + if let Some(pool) = pools.pool() { + redraw( + pool, + window.surface(), + dimensions, + &font_data, + cb_contents.lock().unwrap().clone(), + ); + } + } + None => {} + } + + event_queue.dispatch().unwrap(); + } +} + +fn redraw( + pool: &mut MemPool, + surface: &wl_surface::WlSurface, + dimensions: (u32, u32), + font_data: &[u8], + cb_contents: String, +) { + let (buf_x, buf_y) = (dimensions.0 as usize, dimensions.1 as usize); + + pool.resize(4 * buf_x * buf_y) + .expect("Failed to resize the memory pool."); + + let mut buf: Vec = vec![0; 4 * buf_x * buf_y]; + let mut canvas = + andrew::Canvas::new(&mut buf, buf_x, buf_y, 4 * buf_x, andrew::Endian::native()); + + let bg = rectangle::Rectangle::new((0, 0), (buf_x, buf_y), None, Some([255, 170, 20, 45])); + canvas.draw(&bg); + + let text_box = rectangle::Rectangle::new( + (buf_x / 30, buf_y / 35), + (buf_x - 2 * (buf_x / 30), (buf_x as f32 / 14.) as usize), + Some((3, [255, 255, 255, 255], rectangle::Sides::ALL, Some(4))), + None, + ); + canvas.draw(&text_box); + + let helper_text = text::Text::new( + (buf_x / 25, buf_y / 30), + [255, 255, 255, 255], + font_data, + buf_x as f32 / 40., + 2.0, + "Press space to draw clipboard contents", + ); + canvas.draw(&helper_text); + + let helper_text = text::Text::new( + (buf_x / 25, buf_y / 15), + [255, 255, 255, 255], + font_data, + buf_x as f32 / 40., + 2.0, + "Press 's' to store example text to clipboard", + ); + canvas.draw(&helper_text); + + for i in (0..cb_contents.len()).step_by(36) { + let content = if cb_contents.len() < i + 36 { + cb_contents[i..].to_string() + } else { + cb_contents[i..i + 36].to_string() + }; + let text = text::Text::new( + ( + buf_x / 10, + buf_y / 8 + (i as f32 * buf_y as f32 / 1000.) as usize, + ), + [255, 255, 255, 255], + font_data, + buf_x as f32 / 40., + 2.0, + content, + ); + canvas.draw(&text); + } + + pool.seek(SeekFrom::Start(0)).unwrap(); + pool.write_all(canvas.buffer).unwrap(); + pool.flush().unwrap(); + + let new_buffer = pool.buffer( + 0, + buf_x as i32, + buf_y as i32, + 4 * buf_x as i32, + wl_shm::Format::Argb8888, + ); + surface.attach(Some(&new_buffer), 0, 0); + surface.commit(); +} diff --git a/src/lib.rs b/src/lib.rs new file mode 100644 index 0000000..94c8894 --- /dev/null +++ b/src/lib.rs @@ -0,0 +1,159 @@ +//! Smithay Wayland Clipboard +//! +//! Provides access to the wayland clipboard with only requirement being a WlDisplay +//! object +//! +//! ```no_run +//! let (display, mut event_queue) = +//! Display::connect_to_env().expect("Failed to connect to the wayland server."); +//! let mut clipboard = wayland_clipboard::WaylandClipboard::new_threaded( +//! display.get_display_ptr() as *mut std::ffi::c_void, +//! ); +//! clipboard.store("Test data"); +//! println!(clipboard.load()); +//! ``` + +#![warn(missing_docs)] + +use std::io::{Read, Write}; +use std::os::raw::c_void; +use std::sync::mpsc; +use std::sync::{Arc, Mutex}; +use std::thread::sleep; +use std::time::Duration; + +use sctk::data_device::DataDevice; +use sctk::data_device::DataSource; +use sctk::data_device::DataSourceEvent; +use sctk::keyboard::{map_keyboard_auto, Event as KbEvent}; +use sctk::reexports::client::Display; +use sctk::wayland_client::sys::client::wl_display; +use sctk::Environment; + +enum WaylandRequest { + Store(String), + Load, + Kill, +} + +/// Object representing the Wayland clipboard +pub struct WaylandClipboard { + request_send: mpsc::Sender, + load_recv: mpsc::Receiver, +} + +impl Drop for WaylandClipboard { + fn drop(&mut self) { + self.request_send.send(WaylandRequest::Kill).unwrap() + } +} + +impl WaylandClipboard { + /// Creates a new WaylandClipboard object + /// + /// Spawns a new thread to dispatch messages to the wayland server every + /// 50ms to ensure the server can read stored data + pub fn new_threaded(wayland_display: *mut c_void) -> Self { + let (request_send, request_recv) = mpsc::channel::(); + let (load_send, load_recv) = mpsc::channel(); + + let wayland_display = unsafe { (wayland_display as *mut wl_display).as_mut().unwrap() }; + + std::thread::spawn(move || { + let (display, mut event_queue) = + unsafe { Display::from_external_display(wayland_display as *mut wl_display) }; + let env = Environment::from_display(&*display, &mut event_queue).unwrap(); + + let seat = env + .manager + .instantiate_range(1, 6, |seat| seat.implement_dummy()) + .unwrap(); + + let device = DataDevice::init_for_seat(&env.data_device_manager, &seat, |_| {}); + + let enter_serial = Arc::new(Mutex::new(None)); + let my_enter_serial = enter_serial.clone(); + let _keyboard = map_keyboard_auto(&seat, move |event, _| { + if let KbEvent::Enter { serial, .. } = event { + *(my_enter_serial.lock().unwrap()) = Some(serial); + } + }); + + loop { + if let Ok(request) = request_recv.try_recv() { + match request { + WaylandRequest::Load => { + // Load + let mut reader = None; + device.with_selection(|offer| { + if let Some(offer) = offer { + offer.with_mime_types(|types| { + for t in types { + if t == "text/plain;charset=utf-8" { + reader = Some( + offer + .receive("text/plain;charset=utf-8".into()) + .unwrap(), + ); + } + } + }); + } + }); + event_queue.sync_roundtrip().unwrap(); + if let Some(mut reader) = reader { + let mut contents = String::new(); + reader.read_to_string(&mut contents).unwrap(); + load_send.send(contents).unwrap(); + } else { + load_send.send("".to_string()).unwrap(); + } + } + WaylandRequest::Store(contents) => { + let data_source = DataSource::new( + &env.data_device_manager, + &["text/plain;charset=utf-8"], + move |source_event| { + if let DataSourceEvent::Send { mut pipe, .. } = source_event { + write!(pipe, "{}", contents).unwrap(); + } + }, + ); + if let Some(enter_serial) = *enter_serial.lock().unwrap() { + device.set_selection(&Some(data_source), enter_serial); + } + event_queue.sync_roundtrip().unwrap(); + } + WaylandRequest::Kill => break, + } + } + event_queue.dispatch_pending().unwrap(); + sleep(Duration::from_millis(50)); + } + }); + + WaylandClipboard { + request_send, + load_recv, + } + } + + /// Returns text from the wayland clipboard + /// + /// Only works when the window connected to the WlDisplay has + /// keyboard focus + pub fn load(&mut self) -> String { + self.request_send.send(WaylandRequest::Load).unwrap(); + self.load_recv.recv().unwrap() + } + + /// Stores text in the wayland clipboard + /// + /// Only works when the window connected to the WlDisplay has + /// keyboard focus + pub fn store>(&mut self, text: S) { + self.request_send + .send(WaylandRequest::Store(text.into())) + .unwrap() + } +}