commit 17011ea9f7a57bcee97bf2f7b31b22bbe03f1ec0 Author: Lucas Timmins Date: Sun Feb 10 01:28:53 2019 +0800 Initial commit 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() + } +}