t-rust-less-lib 0.2.23

Password manager library
Documentation
use std::{
  collections::HashMap,
  error::Error,
  fs::File,
  io::Write,
  os::{fd::AsRawFd, unix::io::FromRawFd},
  sync::{
    atomic::{AtomicBool, Ordering},
    Arc, Mutex, RwLock,
  },
  thread,
};

use log::{debug, error};
use wayland_client::{
  event_created_child,
  globals::{registry_queue_init, BindError, GlobalListContents},
  protocol::{
    wl_registry::WlRegistry,
    wl_seat::{self, WlSeat},
  },
  Connection, Dispatch, EventQueue, Proxy,
};
use wayland_protocols_wlr::data_control::v1::client::{
  zwlr_data_control_device_v1::{self, ZwlrDataControlDeviceV1},
  zwlr_data_control_manager_v1::ZwlrDataControlManagerV1,
  zwlr_data_control_offer_v1::ZwlrDataControlOfferV1,
  zwlr_data_control_source_v1::{self, ZwlrDataControlSourceV1},
};
use zeroize::Zeroize;

use crate::api::{ClipboardProviding, EventData, EventHub};
use crate::clipboard::selection_provider_holder::SelectionProviderHolder;

use super::{ClipboardCommon, ClipboardError, ClipboardResult, SelectionProvider};

const TEXT_MIMES: &[&str] = &[
  "text/plain;charset=utf-8",
  "text/plain",
  "STRING",
  "UTF8_STRING",
  "TEXT",
];

struct Context {
  open: AtomicBool,
  cancel: AtomicBool,
  provider_holder: RwLock<SelectionProviderHolder>,
}

impl Context {
  fn new<T>(provider: T) -> Self
  where
    T: SelectionProvider + 'static,
  {
    Context {
      open: AtomicBool::new(false),
      cancel: AtomicBool::new(false),
      provider_holder: RwLock::new(SelectionProviderHolder::new(provider)),
    }
  }

  fn is_open(&self) -> bool {
    self.open.load(Ordering::Relaxed)
  }

  fn currently_providing(&self) -> Option<ClipboardProviding> {
    self.provider_holder.read().ok()?.current_selection()
  }

  fn provide_next(&self) {
    if let Ok(mut provider_holder) = self.provider_holder.write() {
      provider_holder.get_value();
    }
  }

  fn destroy(&self) {
    self.cancel.store(true, Ordering::Relaxed)
  }
}

struct State {
  context: Arc<Context>,
  clipboard_manager: ZwlrDataControlManagerV1,
  seats: HashMap<WlSeat, SeatData>,
}

impl Dispatch<WlRegistry, GlobalListContents> for State {
  fn event(
    _state: &mut Self,
    _proxy: &WlRegistry,
    _event: <WlRegistry as wayland_client::Proxy>::Event,
    _data: &GlobalListContents,
    _conn: &wayland_client::Connection,
    _qhandle: &wayland_client::QueueHandle<Self>,
  ) {
  }
}

impl Dispatch<WlSeat, ()> for State {
  fn event(
    _state: &mut Self,
    seat: &WlSeat,
    event: <WlSeat as wayland_client::Proxy>::Event,
    _data: &(),
    _conn: &wayland_client::Connection,
    _qh: &wayland_client::QueueHandle<Self>,
  ) {
    if let wl_seat::Event::Name { name } = event {
      _state.seats.get_mut(seat).unwrap().set_name(name);
    }
  }
}

impl Dispatch<ZwlrDataControlManagerV1, ()> for State {
  fn event(
    _state: &mut Self,
    _proxy: &ZwlrDataControlManagerV1,
    _event: <ZwlrDataControlManagerV1 as wayland_client::Proxy>::Event,
    _data: &(),
    _conn: &wayland_client::Connection,
    _qhandle: &wayland_client::QueueHandle<Self>,
  ) {
  }
}

impl Dispatch<ZwlrDataControlSourceV1, ()> for State {
  fn event(
    _state: &mut Self,
    _proxy: &ZwlrDataControlSourceV1,
    _event: <ZwlrDataControlSourceV1 as wayland_client::Proxy>::Event,
    _data: &(),
    _conn: &wayland_client::Connection,
    _qhandle: &wayland_client::QueueHandle<Self>,
  ) {
    match _event {
      zwlr_data_control_source_v1::Event::Send { mime_type, fd } if TEXT_MIMES.contains(&mime_type.as_str()) => {
        debug!("Event send: {mime_type} {fd:?}");
        match _state.context.provider_holder.write() {
          Ok(mut selection_provider) => {
            if let Some(mut content) = selection_provider.get_value() {
              let mut f = unsafe { File::from_raw_fd(fd.as_raw_fd()) };
              f.write_all(content.as_bytes()).ok();
              content.zeroize();
            } else {
              debug!("No more values");
              _state.context.cancel.store(true, Ordering::Relaxed);
            }
          }
          Err(err) => {
            error!("Lock error: {err}");
            _state.context.cancel.store(true, Ordering::Relaxed);
          }
        }
      }
      zwlr_data_control_source_v1::Event::Cancelled => {
        debug!("Event cancel: Lost ownership");
        _state.context.cancel.store(true, Ordering::Relaxed);
      }
      _ => (),
    }
  }
}

impl Dispatch<ZwlrDataControlDeviceV1, WlSeat> for State {
  fn event(
    state: &mut Self,
    _device: &ZwlrDataControlDeviceV1,
    event: <ZwlrDataControlDeviceV1 as Proxy>::Event,
    seat: &WlSeat,
    _conn: &wayland_client::Connection,
    _qhandle: &wayland_client::QueueHandle<Self>,
  ) {
    match event {
      zwlr_data_control_device_v1::Event::DataOffer { id } => id.destroy(),
      zwlr_data_control_device_v1::Event::Finished => {
        state.seats.get_mut(seat).unwrap().set_device(None);
      }
      _ => (),
    }
  }

  event_created_child!(State, ZwlrDataControlDeviceV1, [
      zwlr_data_control_device_v1::EVT_DATA_OFFER_OPCODE => (ZwlrDataControlOfferV1, ()),
  ]);
}

impl Dispatch<ZwlrDataControlOfferV1, ()> for State {
  fn event(
    _state: &mut Self,
    _offer: &ZwlrDataControlOfferV1,
    _event: <ZwlrDataControlOfferV1 as wayland_client::Proxy>::Event,
    _data: &(),
    _conn: &wayland_client::Connection,
    _qhandle: &wayland_client::QueueHandle<Self>,
  ) {
  }
}

pub struct Clipboard {
  context: Arc<Context>,
  handle: Mutex<Option<thread::JoinHandle<()>>>,
}

impl ClipboardCommon for Clipboard {
  fn new<T>(selection_provider: T, event_hub: Arc<dyn EventHub>) -> ClipboardResult<Self>
  where
    T: SelectionProvider + Clone + 'static,
  {
    let conn = Connection::connect_to_env()?;
    let (globals, mut queue) = registry_queue_init::<State>(&conn)?;
    let qh = &queue.handle();
    let clipboard_manager = match globals.bind(qh, 2..=2, ()) {
      Ok(manager) => manager,
      Err(BindError::NotPresent | BindError::UnsupportedVersion) => globals.bind(qh, 1..=1, ())?,
    };
    let registry = globals.registry();
    let seats = globals.contents().with_list(|globals| {
      globals
        .iter()
        .filter(|global| global.interface == WlSeat::interface().name && global.version >= 2)
        .map(|global| {
          let seat = registry.bind(global.name, 2, qh, ());
          (seat, SeatData::default())
        })
        .collect()
    });

    match selection_provider.current_selection() {
      Some(providing) => event_hub.send(EventData::ClipboardProviding(providing)),
      None => return Err(ClipboardError::Other("Empty provider".to_string())),
    };

    let context = Arc::new(Context::new(selection_provider));
    let mut state = State {
      context: context.clone(),
      clipboard_manager,
      seats,
    };

    queue.roundtrip(&mut state)?;

    let handle = thread::spawn({
      let cloned = context.clone();
      move || {
        if let Err(err) = try_run(queue, state) {
          cloned.open.store(false, Ordering::Relaxed);
          error!("Wayland clipboard error: {err}");
        }
      }
    });

    Ok(Clipboard {
      context,
      handle: Mutex::new(Some(handle)),
    })
  }

  fn is_open(&self) -> bool {
    self.context.is_open()
  }

  fn currently_providing(&self) -> Option<ClipboardProviding> {
    self.context.currently_providing()
  }

  fn provide_next(&self) {
    self.context.provide_next()
  }

  fn destroy(&self) {
    self.context.destroy()
  }

  fn wait(&self) -> ClipboardResult<()> {
    let mut maybe_handle = self.handle.lock()?;
    if let Some(handle) = maybe_handle.take() {
      handle
        .join()
        .map_err(|_| ClipboardError::Other("wait timeout".to_string()))?;
    }
    Ok(())
  }
}

fn try_run(mut queue: EventQueue<State>, mut state: State) -> Result<(), Box<dyn Error>> {
  let data_source = state.clipboard_manager.create_data_source(&queue.handle(), ());

  debug!("Seats: {:?}", &state.seats);

  for &mime_type in TEXT_MIMES {
    data_source.offer(mime_type.to_string());
  }

  for (seat, data) in &mut state.seats {
    let device = state
      .clipboard_manager
      .get_data_device(seat, &queue.handle(), seat.clone());
    device.set_selection(Some(&data_source));
    data.set_device(Some(device));
  }

  debug!("Start event loop");
  state.context.open.store(true, Ordering::Relaxed);
  while !state.context.cancel.load(Ordering::Relaxed) {
    queue.blocking_dispatch(&mut state)?;
  }
  state.context.open.store(false, Ordering::Relaxed);
  debug!("End event loop");

  for data in state.seats.values_mut() {
    data.set_device(None);
  }
  data_source.destroy();

  Ok(())
}

#[derive(Default, Debug)]
pub struct SeatData {
  pub name: Option<String>,

  pub device: Option<ZwlrDataControlDeviceV1>,
}

impl SeatData {
  pub fn set_name(&mut self, name: String) {
    self.name = Some(name)
  }

  pub fn set_device(&mut self, device: Option<ZwlrDataControlDeviceV1>) {
    let old_device = self.device.take();
    self.device = device;

    if let Some(device) = old_device {
      device.destroy();
    }
  }
}