klemmbrett 0.1.0

Minimalist, dumb and fast wayland native clipboard history manager
use std::collections::VecDeque;
use std::error::Error;
use std::fs::File;
use std::io::{self, Read, Write};
use std::os::fd::AsFd;
use std::os::unix::net::{UnixListener, UnixStream};
use std::sync::{Arc, Mutex, MutexGuard};

use crate::proto::{self, Code, Command, Entry, Response, Uuid};
use wayland_client::globals::{GlobalListContents, registry_queue_init};
use wayland_client::protocol::{wl_registry::WlRegistry, wl_seat::WlSeat};
use wayland_client::{Connection, Dispatch, Proxy, QueueHandle, delegate_noop, event_created_child};
use wayland_protocols_wlr::data_control::v1::client::{
    zwlr_data_control_device_v1::{self as device, ZwlrDataControlDeviceV1},
    zwlr_data_control_manager_v1::ZwlrDataControlManagerV1,
    zwlr_data_control_offer_v1::{self as offer, ZwlrDataControlOfferV1},
    zwlr_data_control_source_v1::{self as source, ZwlrDataControlSourceV1},
};

const MARKER_MIME: &str = "application/x-klemmbrett";
const MAX_ENTRIES: usize = 100;

#[derive(Clone)]
struct Daemon {
    conn: Connection,
    qh: QueueHandle<Daemon>,
    manager: ZwlrDataControlManagerV1,
    device: ZwlrDataControlDeviceV1,
    history: Arc<Mutex<VecDeque<Entry>>>,
}

impl Daemon {
    fn history(&self) -> MutexGuard<'_, VecDeque<Entry>> {
        self.history.lock().unwrap()
    }

    fn set(&self, id: Uuid) -> Response<()> {
        let mut history = self.history();
        let idx = history.iter().position(|e| e.id == id).ok_or_else(not_found)?;
        let entry = history.remove(idx).unwrap();
        history.push_front(entry.clone());

        let source = self.manager.create_data_source(&self.qh, entry.clone());
        source.offer(entry.mime);
        source.offer(MARKER_MIME.into());
        self.device.set_selection(Some(&source));
        self.conn.flush().map_err(|e| proto::Error::new(Code::Internal, e.to_string()))
    }
}

pub fn run() -> Result<(), Box<dyn Error>> {
    let conn = Connection::connect_to_env()?;
    let (globals, mut queue) = registry_queue_init::<Daemon>(&conn)?;
    let qh = queue.handle();
    let seat: WlSeat = globals.bind(&qh, 1..=1, ())?;
    let manager: ZwlrDataControlManagerV1 = globals.bind(&qh, 1..=1, ())?;
    let device = manager.get_data_device(&seat, &qh, ());
    let mut daemon = Daemon { conn, qh, manager, device, history: Default::default() };

    let path = proto::socket_path();
    let _ = std::fs::remove_file(&path);
    let listener = UnixListener::bind(&path)?;
    let d = daemon.clone();
    std::thread::spawn(move || {
        for stream in listener.incoming().flatten() {
            let _ = handle_client(stream, &d);
        }
    });

    loop {
        queue.blocking_dispatch(&mut daemon)?;
    }
}

fn not_found() -> proto::Error {
    proto::Error::new(Code::NotFound, "no entry with this uuid")
}

fn io_err(e: io::Error) -> proto::Error {
    proto::Error::new(Code::Internal, e.to_string())
}

fn handle_client(mut stream: UnixStream, daemon: &Daemon) -> io::Result<()> {
    let mut payload = Vec::new();
    let status = respond(&mut stream, &mut payload, daemon);
    proto::write_status(&mut stream, &status)?;
    if status.is_ok() {
        stream.write_all(&payload)?;
    }
    Ok(())
}

fn respond(r: &mut impl Read, out: &mut Vec<u8>, daemon: &Daemon) -> Response<()> {
    if proto::read_u32(r).map_err(io_err)? != proto::MAGIC {
        return Err(proto::Error::new(Code::BadMagic, "bad magic"));
    }
    match Command::try_from(proto::read_u8(r).map_err(io_err)?) {
        Ok(Command::Get) => {
            let id = proto::read_uuid(r).map_err(io_err)?;
            let history = daemon.history();
            let entry = history.iter().find(|e| e.id == id).ok_or_else(not_found)?;
            entry.write(out).map_err(io_err)
        }
        Ok(Command::List) => {
            let amount = proto::read_u8(r).map_err(io_err)? as usize;
            let offset = proto::read_u32(r).map_err(io_err)? as usize;
            let history = daemon.history();
            out.push(history.len().saturating_sub(offset).min(amount) as u8);
            for entry in history.iter().skip(offset).take(amount) {
                entry.write(out).map_err(io_err)?;
            }
            Ok(())
        }
        Ok(Command::Set) => daemon.set(proto::read_uuid(r).map_err(io_err)?),
        Err(cmd) => Err(proto::Error::new(Code::UnknownCommand, format!("unknown command {cmd}"))),
    }
}

fn receive(conn: &Connection, offer: &ZwlrDataControlOfferV1) -> Option<Entry> {
    let mimes = offer.data::<Mutex<Vec<String>>>()?.lock().unwrap();
    if mimes.iter().any(|m| m == MARKER_MIME) {
        return None;
    }
    let mime = mimes
        .iter()
        .find(|m| *m == "text/plain;charset=utf-8")
        .or_else(|| mimes.iter().find(|m| m.contains('/') && m.len() <= u8::MAX as usize))?
        .clone();

    let (mut read, write) = std::io::pipe().ok()?;
    offer.receive(mime.clone(), write.as_fd());
    drop(write);
    conn.flush().ok()?;

    let mut data = Vec::new();
    read.read_to_end(&mut data).ok()?;
    let mut id = [0; 16];
    File::open("/dev/urandom").and_then(|mut f| f.read_exact(&mut id)).ok()?;
    (!data.is_empty()).then_some(Entry { id, mime, data })
}

impl Dispatch<ZwlrDataControlDeviceV1, ()> for Daemon {
    fn event(daemon: &mut Self, _: &ZwlrDataControlDeviceV1, event: device::Event, _: &(), conn: &Connection, _: &QueueHandle<Self>) {
        if let device::Event::Selection { id: Some(offer) } = event {
            if let Some(entry) = receive(conn, &offer) {
                let mut history = daemon.history();
                history.retain(|e| e.data != entry.data);
                history.push_front(entry);
                history.truncate(MAX_ENTRIES);
            }
            offer.destroy();
        }
    }

    event_created_child!(Daemon, ZwlrDataControlDeviceV1, [
        device::EVT_DATA_OFFER_OPCODE => (ZwlrDataControlOfferV1, Mutex::new(Vec::new())),
    ]);
}

impl Dispatch<ZwlrDataControlOfferV1, Mutex<Vec<String>>> for Daemon {
    fn event(_: &mut Self, _: &ZwlrDataControlOfferV1, event: offer::Event, mimes: &Mutex<Vec<String>>, _: &Connection, _: &QueueHandle<Self>) {
        if let offer::Event::Offer { mime_type } = event {
            mimes.lock().unwrap().push(mime_type);
        }
    }
}

impl Dispatch<ZwlrDataControlSourceV1, Entry> for Daemon {
    fn event(_: &mut Self, source: &ZwlrDataControlSourceV1, event: source::Event, entry: &Entry, _: &Connection, _: &QueueHandle<Self>) {
        match event {
            source::Event::Send { mime_type, fd } if mime_type == entry.mime => {
                let _ = File::from(fd).write_all(&entry.data);
            }
            source::Event::Cancelled => source.destroy(),
            _ => {}
        }
    }
}

impl Dispatch<WlRegistry, GlobalListContents> for Daemon {
    fn event(_: &mut Self, _: &WlRegistry, _: <WlRegistry as Proxy>::Event, _: &GlobalListContents, _: &Connection, _: &QueueHandle<Self>) {}
}

delegate_noop!(Daemon: ZwlrDataControlManagerV1);
delegate_noop!(Daemon: ignore WlSeat);