x11-input-supercharger 0.5.0-alpha

Adds system-wide Windows-like scrolling mode and conditional clicking using keyboard
use movie::actor;
use serde_derive::Deserialize;

use std::sync::mpsc::{channel, Receiver, Sender};
use std::time::{Duration, Instant};

use crate::x::xdotool;
use crate::x::xlib::{Event, XLib};
use crate::x::xmodmap;

#[derive(Deserialize, Clone, Debug)]
#[serde(deny_unknown_fields)]
pub struct KeyboardClickConfig {
    pub device: String,
    pub subdevice: u32,
    pub timeout_ms: u64,
    pub key_lmb: u8,
    pub key_rmb: u8,
    pub key_unused1: u8,
    pub key_unused2: u8,
}

#[derive(Debug, Clone)]
pub struct Keys {
    key_lmb: Vec<u64>,
    key_rmb: Vec<u64>,
    key_unused1: Vec<u64>,
    key_unused2: Vec<u64>,
}

pub struct KeyboardClick<'a> {
    timer_thread: TimerThread::Handle,
    timeout_rx: Receiver<()>,
    source_id: u32,
    config: &'a KeyboardClickConfig,
    original_keys: Keys,
    remapped: bool,
}

impl<'a> KeyboardClick<'a> {
    pub fn new(config: &'a KeyboardClickConfig, x: &mut XLib) -> Self {
        use x11::xinput2::*;
        let source_id = x
            .get_device_id(&config.device, config.subdevice)
            .expect("Incorrect device configuration for keyboard clicking feature");
        x.grab(&[XI_RawMotion, XI_RawKeyPress, XI_RawKeyRelease]);

        let original_keys = Keys {
            key_lmb: x.get_keys(config.key_lmb),
            key_rmb: x.get_keys(config.key_rmb),
            key_unused1: x.get_keys(config.key_unused1),
            key_unused2: x.get_keys(config.key_unused2),
        };

        {
            let mut transaction = xmodmap::transaction();
            transaction.bind(config.key_lmb, &[]);
            transaction.bind(config.key_rmb, &[]);
            transaction.bind(config.key_unused1, &original_keys.key_lmb);
            transaction.bind(config.key_unused2, &original_keys.key_rmb);
            transaction.commit();
        }

        let (timeout_tx, timeout_rx) = channel();
        let timeout_time = Duration::from_millis(config.timeout_ms);
        let timer_thread = TimerThread::Actor {
            timeout_tx,
            timeout_time,
        }
        .start();

        let ret = Self {
            config,
            timer_thread,
            timeout_rx,
            source_id,
            original_keys,
            remapped: false,
        };
        ret.register_ctrlc_handler();
        ret
    }
    #[allow(clippy::collapsible_if)]
    pub fn handle(&mut self, ev: &Event) {
        use x11::xinput2::*;
        if self.timeout_rx.try_recv().is_ok() {
            self.remapped = false;
        }
        if ev.source_id == self.source_id && ev.kind == XI_RawMotion {
            self.remapped = true;
            self.timer_thread.send(TimerThread::Input::Event);
        } else if ev.kind == XI_RawKeyPress || ev.kind == XI_RawKeyRelease {
            if ev.detail == self.config.key_lmb || ev.detail == self.config.key_rmb {
                if self.remapped {
                    xdotool::click(ev.kind == XI_RawKeyPress, ev.detail == self.config.key_rmb);
                } else {
                    let is_rmb = ev.detail == self.config.key_rmb;
                    let key_to_hit = if is_rmb {
                        self.config.key_unused2
                    } else {
                        self.config.key_unused1
                    };
                    xdotool::key(ev.kind == XI_RawKeyPress, key_to_hit);
                }
            }
        }
    }
    fn register_ctrlc_handler(&self) {
        let keys = self.original_keys.clone();
        let (key_lmb, key_rmb, key_unused1, key_unused2) = (
            self.config.key_lmb,
            self.config.key_rmb,
            self.config.key_unused1,
            self.config.key_unused2,
        );
        ctrlc::set_handler(move || {
            let mut transaction = xmodmap::transaction();
            transaction.bind(key_lmb, &keys.key_lmb);
            transaction.bind(key_rmb, &keys.key_rmb);
            transaction.bind(key_unused1, &keys.key_unused1);
            transaction.bind(key_unused2, &keys.key_unused2);
            transaction.commit();
            panic!("exiting...");
        })
        .unwrap();
    }
}

actor! {
    TimerThread
    input:
        Event,
    data:
        pub timeout_tx: Sender<()>,
        pub timeout_time: Duration,
    on_init:
        let mut last_event_time = Instant::now();
        let mut warmup = true;
        let mut remapped = false;
        let mut current_time = Instant::now();
    on_message:
        Event => {
            remapped = true;
            last_event_time = current_time;
        }
    tick_interval: 50,
    on_tick:
        current_time = Instant::now();
        let delta_time = current_time.duration_since(last_event_time);
        if warmup {
            if delta_time > self.timeout_time {
                warmup = false;
            }
        } else {
            if remapped && delta_time > self.timeout_time {
                remapped = false;
                self.timeout_tx.send(()).unwrap();
            }
        }
}