nuhxboard 0.2.0

A Linux port of NohBoard
use color_eyre::{eyre::WrapErr, Section};
use iced::advanced::subscription::Recipe;
use regex::Regex;
use std::{
    hash::Hash,
    io::{self, prelude::*},
    process::Command,
    task::Poll,
};

pub struct InputSubscription;

impl Recipe for InputSubscription {
    type Output = crate::Message;

    fn hash(&self, state: &mut iced::advanced::Hasher) {
        struct Marker;
        std::any::TypeId::of::<Marker>().hash(state);
    }

    fn stream(
        self: Box<Self>,
        _input: iced::advanced::subscription::EventStream,
    ) -> futures::stream::BoxStream<'static, Self::Output> {
        let mut child = Command::new("xinput")
            .arg("test-xi2")
            .arg("--root")
            .stdout(std::process::Stdio::piped())
            .spawn()
            .wrap_err("Error spawning input listener")
            .note("NuhxBoard relies on XInput. If you are running on Windows, use NohBoard, and if you're running on Linux, make sure you have the `xorg-xinput` package installed.")
            .unwrap();

        let reader = io::BufReader::new(child.stdout.take().unwrap());
        std::boxed::Box::pin(InputStream { reader })
    }
}

struct InputStream {
    reader: io::BufReader<std::process::ChildStdout>,
}

enum InputEvent {
    MouseButtonPress,
    MouseButtonRelease,
    Motion,
    KeyPress,
    KeyRelease,
}

impl futures::stream::Stream for InputStream {
    type Item = crate::Message;

    fn poll_next(
        self: std::pin::Pin<&mut Self>,
        _cx: &mut std::task::Context<'_>,
    ) -> Poll<Option<Self::Item>> {
        let mut line = String::new();
        let reader = &mut self.get_mut().reader;
        reader.read_line(&mut line).unwrap();
        if line.is_empty() {
            return Poll::Ready(Some(crate::Message::Dummy));
        }
        let event_type_re = Regex::new(r"EVENT type ([0-9]+) ").unwrap();
        let event_type = match event_type_re.captures(&line) {
            Some(caps) => match caps.get(1).unwrap().as_str() {
                "2" => InputEvent::KeyPress,
                "3" => InputEvent::KeyRelease,
                "15" => InputEvent::MouseButtonPress,
                "16" => InputEvent::MouseButtonRelease,
                "17" => InputEvent::Motion,
                _ => return Poll::Ready(Some(crate::Message::Dummy)),
            },
            None => return Poll::Ready(Some(crate::Message::Dummy)),
        };
        match event_type {
            InputEvent::KeyPress | InputEvent::KeyRelease => {
                for _ in 0..2 {
                    reader.read_line(&mut line).unwrap();
                }
                let keycode_re = Regex::new(r"detail: ([0-9]+)").unwrap();
                let keycode = keycode_re
                    .captures(&line)
                    .unwrap()
                    .get(1)
                    .unwrap()
                    .as_str()
                    .parse::<u32>()
                    .unwrap();

                for _ in 0..5 {
                    reader.read_line(&mut line).unwrap();
                }
                let modifiers_re = Regex::new(r"modifiers: locked (?:0x)?([a-f0-9]+)").unwrap();
                let modifiers = u8::from_str_radix(
                    modifiers_re
                        .captures(&line)
                        .unwrap()
                        .get(1)
                        .unwrap()
                        .as_str(),
                    16,
                )
                .unwrap();
                let caps = modifiers & 0b10 != 0;
                match event_type {
                    InputEvent::KeyPress => {
                        Poll::Ready(Some(crate::Message::KeyPress { keycode, caps }))
                    }
                    InputEvent::KeyRelease => {
                        Poll::Ready(Some(crate::Message::KeyRelease { keycode, caps }))
                    }
                    _ => unreachable!(),
                }
            }
            InputEvent::MouseButtonPress | InputEvent::MouseButtonRelease => {
                for _ in 0..2 {
                    reader.read_line(&mut line).unwrap();
                }
                let button_code_re = Regex::new(r"detail: ([0-9]+)").unwrap();
                let button_code = button_code_re
                    .captures(&line)
                    .unwrap()
                    .get(1)
                    .unwrap()
                    .as_str()
                    .parse::<u32>()
                    .unwrap();

                match event_type {
                    InputEvent::MouseButtonPress => {
                        if (4..=7).contains(&button_code) {
                            return Poll::Ready(Some(crate::Message::ScrollButtonPress(
                                button_code,
                            )));
                        }
                        Poll::Ready(Some(crate::Message::MouseButtonPress(button_code)))
                    }
                    InputEvent::MouseButtonRelease => {
                        // See main.rs:36
                        if button_code == 4 || button_code == 5 {
                            return Poll::Ready(Some(crate::Message::Dummy));
                        }
                        Poll::Ready(Some(crate::Message::MouseButtonRelease(button_code)))
                    }
                    _ => unreachable!(),
                }
            }
            InputEvent::Motion => {
                for _ in 0..5 {
                    reader.read_line(&mut line).unwrap();
                }
                let scroll_check_re = Regex::new(r"(?:2|3): -?[0-9]+").unwrap();
                if scroll_check_re.captures(&line).is_some() {
                    return Poll::Ready(Some(crate::Message::Dummy));
                }
                let x_vel_re = Regex::new(r"0: (-?[0-9]+)").unwrap();
                let x_vel = x_vel_re
                    .captures(&line)
                    .unwrap()
                    .get(1)
                    .unwrap()
                    .as_str()
                    .parse::<f32>()
                    .unwrap();
                reader.read_line(&mut line).unwrap();
                let y_vel_re = Regex::new(r"1: (-?[0-9]+)").unwrap();
                let y_vel = y_vel_re
                    .captures(&line)
                    .unwrap()
                    .get(1)
                    .unwrap()
                    .as_str()
                    .parse::<f32>()
                    .unwrap();

                Poll::Ready(Some(crate::Message::Motion { x: x_vel, y: y_vel }))
            }
        }
    }
}