//! evdev-based keyboard input for Linux.
//!
//! Opens every keyboard device under `/dev/input/event*`, `EVIOCGRAB`s each one exclusively, and reads raw scancodes directly from the kernel — this sits below xkb, so the user's active layout (Dvorak/Colemak/etc) does not remap the stream. One thread per grabbed device feeds a shared mpsc channel. Purely event-driven: each `read()` blocks until the kernel delivers an `input_event`.
//!
//! Requires the running user to have read access to `/dev/input/event*` (typically membership in the `input` group).

use super::HidEvent;
use crate::hand::{KeyDirection, KeyEvent};
use crate::layout::keyboard;
use std::ffi::CString;
use std::fs;
use std::os::unix::io::RawFd;
use std::sync::Arc;
use std::sync::atomic::{AtomicBool, Ordering};
use std::sync::mpsc;

// evdev event-type codes
const EV_SYN: u16 = 0x00;
const EV_KEY: u16 = 0x01;
const EV_LED: u16 = 0x11;
const SYN_REPORT: u16 = 0x00;
// LED codes: 0 = num-lock, 1 = caps-lock, 2 = scroll-lock.
// All are potentially reconciled by xkb against its own state; scroll-lock
// is usually the least-managed, but whichever works on the user's box.
const LED_CODE: u16 = 0x00; // LED_NUML
const BUS_VIRTUAL: u16 = 0x06;

// Positional scancodes from linux/input-event-codes.h — these are
// pre-xkb hardware positions, identical whether the user is on QWERTY,
// Dvorak, Colemak, or anything else. The layout-specific chord key
// mappings live in `crate::layout::keyboard`; only non-layout keys are kept here.
const KEY_ESC: u16 = 1;
const KEY_CAPSLOCK: u16 = 58;
/// Keyboard-detection probe. Any standard keyboard advertises KEY_A in its EV_KEY bitmap; mice don't. The role this position plays in rhe depends on the layout — see `crate::layout::keyboard::linux_to_role`.
const KEY_A: u16 = 30;
const KEY_ENTER: u16 = 28;

// OS-modifier scancodes. A press of any of these — when the scancode
// isn't already bound to a chord/word/mod role — flips rhe into
// modifier-passthrough until every key releases.
const KEY_LEFTSHIFT: u16 = 42;
const KEY_RIGHTSHIFT: u16 = 54;
const KEY_LEFTCTRL: u16 = 29;
const KEY_RIGHTCTRL: u16 = 97;
const KEY_LEFTALT: u16 = 56;
const KEY_RIGHTALT: u16 = 100;
const KEY_LEFTMETA: u16 = 125;
const KEY_RIGHTMETA: u16 = 126;
const KEY_FN: u16 = 464;

#[inline]
fn is_modifier_scan(code: u16) -> bool {
    matches!(
        code,
        KEY_LEFTSHIFT
            | KEY_RIGHTSHIFT
            | KEY_LEFTCTRL
            | KEY_RIGHTCTRL
            | KEY_LEFTALT
            | KEY_RIGHTALT
            | KEY_LEFTMETA
            | KEY_RIGHTMETA
            | KEY_FN
    )
}

// EVIOCGRAB = _IOW('E', 0x90, int) — stable Linux ABI, x86-64 generic _IOC layout.
const EVIOCGRAB: libc::c_ulong = 0x40044590;

// uinput ioctls (type 'U' = 0x55). Stable Linux ABI.
const UI_SET_EVBIT: libc::c_ulong = 0x40045564; // _IOW('U', 100, int)
const UI_SET_KEYBIT: libc::c_ulong = 0x40045565; // _IOW('U', 101, int)
const UI_DEV_CREATE: libc::c_ulong = 0x5501; // _IO ('U',   1)
const UI_DEV_SETUP: libc::c_ulong = 0x405c5503; // _IOW('U',   3, struct uinput_setup)

#[repr(C)]
struct InputId {
    bustype: u16,
    vendor: u16,
    product: u16,
    version: u16,
}

#[repr(C)]
struct UinputSetup {
    id: InputId,
    name: [u8; 80],
    ff_effects_max: u32,
}

// EVIOCGBIT(ev, len) = _IOC(_IOC_READ=2, type='E', nr=0x20+ev, size=len).
// Generic _IOC layout: (dir << 30) | (size << 16) | (type << 8) | nr.
fn eviocgbit(ev: u16, len: u32) -> libc::c_ulong {
    let len = len & 0x3FFF;
    ((2u32 << 30) | (len << 16) | (0x45u32 << 8) | (0x20 + ev as u32)) as libc::c_ulong
}

#[repr(C)]
struct InputEvent {
    time: libc::timeval,
    type_: u16,
    code: u16,
    value: i32,
}

/// How the user signals "quit rhe" and whether caps lock toggles the grab.
///
/// In both `CapsLockPlusEsc` and `EscOrCapsPlusEsc`, Caps Lock is intercepted and a solo press-and-release toggles the `enabled` flag (passthrough on/off). `EscAlone` leaves caps alone — for unit tests, not typical use.
#[derive(Clone, Copy)]
pub enum QuitTrigger {
    /// Esc alone quits. Caps lock passes through to OS, doesn't toggle.
    EscAlone,
    /// Caps+Esc quits, Esc alone passes through. Solo Caps press toggles enabled. Used by `rhe run` where Esc is needed by the focused app (vim, etc).
    CapsLockPlusEsc,
    /// Either Esc alone OR Caps+Esc quits. Solo Caps press toggles enabled. Used by the tutor so Esc still exits traditionally, *and* the user can press Caps alone to pause the tutor and type normally for a moment.
    EscOrCapsPlusEsc,
}

/// Callback fired when a solo Caps Lock press toggles the `enabled` flag. Lets the tray (or any other UI) refresh itself without polling.
pub type ToggleHook = Arc<dyn Fn() + Send + Sync + 'static>;

pub struct EvdevInput {
    pub rx: mpsc::Receiver<HidEvent>,
}

impl EvdevInput {
    pub fn start_grab(
        enabled: Arc<AtomicBool>,
        quit: QuitTrigger,
        on_toggle: Option<ToggleHook>,
    ) -> Result<Self, String> {
        let (tx, rx) = mpsc::channel();

        let devices =
            find_keyboards().map_err(|e| format!("evdev: failed to scan /dev/input: {}", e))?;
        if devices.is_empty() {
            return Err(
                "evdev: no keyboard devices found — is the user in the `input` group?".into(),
            );
        }

        let mut grabbed = Vec::new();
        for path in &devices {
            if let Ok(fd) = open_and_grab(path) {
                grabbed.push(fd);
            }
        }
        if grabbed.is_empty() {
            return Err("evdev: could not grab any keyboard device".into());
        }

        // Open a uinput virtual keyboard for passing non-rhe keys back to the OS.
        // If this fails (no permission on /dev/uinput) we still run, we just
        // can't forward — ESC will quit the grab.
        let uinput_fd = match open_uinput() {
            Ok(fd) => Some(fd),
            Err(e) => {
                crate::rerror!(
                    "evdev passthrough disabled ({}); non-rhe keys will be swallowed",
                    e
                );
                None
            }
        };

        // In caps-is-rhe-key mode, set the scroll-lock LED to mirror the
        // initial passthrough state. "LED on = typing normally" → so LED
        // is off when rhe starts enabled.
        let caps_controls_led = matches!(
            quit,
            QuitTrigger::CapsLockPlusEsc | QuitTrigger::EscOrCapsPlusEsc
        );
        if caps_controls_led {
            let led_on = !enabled.load(Ordering::Relaxed);
            for &fd in &grabbed {
                set_scroll_led(fd, led_on);
            }
        }

        for fd in grabbed {
            let tx = tx.clone();
            let enabled = enabled.clone();
            let on_toggle = on_toggle.clone();
            std::thread::spawn(move || reader_loop(fd, uinput_fd, tx, enabled, quit, on_toggle));
        }

        Ok(Self { rx })
    }
}

fn reader_loop(
    fd: RawFd,
    uinput_fd: Option<RawFd>,
    tx: mpsc::Sender<HidEvent>,
    enabled: Arc<AtomicBool>,
    quit: QuitTrigger,
    on_toggle: Option<ToggleHook>,
) {
    const BATCH: usize = 32;
    let mut buf: [InputEvent; BATCH] = unsafe { std::mem::zeroed() };
    let buf_bytes = std::mem::size_of::<[InputEvent; BATCH]>();
    let ev_size = std::mem::size_of::<InputEvent>();

    // Caps-lock state tracking:
    //   caps_held: key physically down right now (used to gate caps+Esc quit).
    //   caps_solo: true since last caps-down, cleared by any other key-down.
    //              On caps-up, if still true, it was a solo press → toggle rhe.
    // In CapsLockPlusEsc mode we intercept caps entirely (OS never sees it);
    // in EscAlone mode caps passes through and doesn't toggle.
    let mut caps_held = false;
    let mut caps_solo = false;
    let caps_is_rhe_key = matches!(
        quit,
        QuitTrigger::CapsLockPlusEsc | QuitTrigger::EscOrCapsPlusEsc
    );

    // Auto-switch tracking: the *scancodes* of currently-held chord
    // keys, so we can (a) derive the finger/word/thumb tallies for
    // the transition heuristic, and (b) know which keys to release
    // on each side of the switch to avoid stuck-key bugs — when
    // rhe flips on, keys that were passing through to OS must get
    // a synthetic key-up so the OS doesn't see them held forever;
    // when rhe flips off, the state machine needs synthetic key-ups
    // so its own live mask doesn't leak either.
    let mut auto_held: Vec<u16> = Vec::new();

    // Modifier-passthrough state. When the user presses any non-rhe
    // modifier (Shift / Ctrl / Alt / Super / Fn, when that scancode
    // isn't bound to a chord/word/mod role), rhe steps out of the
    // way until every key has been released. Lets Ctrl+C, Cmd+Tab,
    // etc. work normally without the chord engine eating half the
    // combo.
    //
    // `all_held` tracks every physical key currently down so we know
    // when "all up" lands and can drop back into rhe mode.
    let mut modifier_passthrough = false;
    let mut all_held: std::collections::HashSet<u16> = std::collections::HashSet::new();

    loop {
        let n = unsafe { libc::read(fd, buf.as_mut_ptr() as *mut libc::c_void, buf_bytes) };
        if n <= 0 {
            break;
        }
        let count = n as usize / ev_size;
        for ev in &buf[..count] {
            if ev.type_ != EV_KEY {
                continue;
            }

            // Caps-lock handling (intercept in run mode, passthrough in tutor).
            if ev.code == KEY_CAPSLOCK {
                match ev.value {
                    1 => {
                        caps_held = true;
                        caps_solo = true;
                    }
                    0 => {
                        caps_held = false;
                        if caps_is_rhe_key && caps_solo {
                            let was = enabled.fetch_xor(true, Ordering::Relaxed);
                            let now_enabled = !was;
                            set_scroll_led(fd, !now_enabled);
                            if let Some(hook) = on_toggle.as_ref() {
                                hook();
                            }
                        }
                        caps_solo = false;
                    }
                    _ => {} // autorepeat — no state change
                }
                if caps_is_rhe_key {
                    continue; // intercept caps; OS doesn't see it
                }
                // tutor mode: fall through to passthrough
            } else if ev.value == 1 {
                // Any non-caps key-down breaks solo-caps tracking.
                caps_solo = false;
            }

            // Track every physical key's down/up so the modifier-
            // passthrough exit condition ("all keys released") can be
            // checked on each event. Autorepeats (value=2) leave the
            // set unchanged.
            match ev.value {
                1 => {
                    all_held.insert(ev.code);
                }
                0 => {
                    all_held.remove(&ev.code);
                }
                _ => {}
            }

            // Modifier-passthrough trigger: any non-rhe modifier
            // press → suspend rhe interpretation until every key has
            // been released. Lets Ctrl+C, Super+Tab, Shift+letter,
            // etc. work without rhe eating one half of the combo.
            // Word + R_THUMB roles count as rhe keys, so pressing
            // them never trips the gate.
            let rhe_role_check = crate::layout::keyboard::linux_to_role(ev.code);
            if ev.value == 1
                && !modifier_passthrough
                && rhe_role_check.is_none()
                && is_modifier_scan(ev.code)
            {
                modifier_passthrough = true;
                // Tell the engine to release any chord keys it
                // currently believes are held — otherwise the state
                // machine carries stale bits across the passthrough
                // window. We track scancodes; map them through the
                // layout to roles before sending.
                for &code in all_held.iter() {
                    if let Some(role) = crate::layout::keyboard::linux_to_role(code) {
                        let _ = tx.send(HidEvent::Key(KeyEvent {
                            scan: role,
                            direction: KeyDirection::Up,
                        }));
                    }
                }
            }

            // While in modifier-passthrough, every event flows
            // straight to the OS — no engine, no auto-switch
            // bookkeeping. Exit the moment the held set empties.
            if modifier_passthrough {
                if let Some(ufd) = uinput_fd {
                    forward_key(ufd, ev.code, ev.value);
                }
                if all_held.is_empty() {
                    modifier_passthrough = false;
                }
                continue;
            }

            // Wide layouts put a chord key on physical Enter, so one
            // of the shift keys is remapped to synthesize Enter (else
            // the user loses newline). Runs before everything else —
            // the synth is a passthrough with a rewritten code, not a
            // chord event, and it bypasses both rhe and caps-lock.
            if let Some(synth_key) = crate::layout::keyboard::linux_enter_synth_key() {
                if ev.code == synth_key {
                    if let Some(ufd) = uinput_fd {
                        forward_key(ufd, KEY_ENTER, ev.value);
                    }
                    continue;
                }
            }

            let rhe_scan = crate::layout::keyboard::linux_to_role(ev.code);

            // Auto-switch tracking: maintain the set of currently-
            // held chord keys. Transitions use it both to detect the
            // chording/typing pattern and to release stuck keys on
            // each side of the switch.
            if crate::layout::keyboard::AUTO_SWITCH && rhe_scan.is_some() {
                match ev.value {
                    1 => {
                        if !auto_held.contains(&ev.code) {
                            auto_held.push(ev.code);
                        }
                    }
                    0 => auto_held.retain(|&c| c != ev.code),
                    _ => {}
                }
            }

            // Auto-switch trigger check (only on key-down).
            let mut auto_switched = false;
            if crate::layout::keyboard::AUTO_SWITCH && ev.value == 1 {
                let (finger, word, thumb) = classify_held(&auto_held);

                // Auto-enable: strong chord pattern while rhe is off.
                if !enabled.load(Ordering::Relaxed)
                    && (finger >= 3 || (finger >= 2 && (word || thumb)))
                {
                    enabled.store(true, Ordering::Relaxed);
                    set_scroll_led(fd, false);
                    // Release the held chord keys from OS's view so
                    // they don't stay physically "pressed" after rhe
                    // takes over.
                    if let Some(ufd) = uinput_fd {
                        for &scan in &auto_held {
                            forward_key(ufd, scan, 0);
                        }
                    }
                    // Bring rhe's state machine up to the same known
                    // state — everything in auto_held is pressed.
                    for &scan in &auto_held {
                        if let Some(role) = crate::layout::keyboard::linux_to_role(scan) {
                            let _ = tx.send(HidEvent::Key(KeyEvent {
                                scan: role,
                                direction: KeyDirection::Down,
                            }));
                        }
                    }
                    if let Some(hook) = on_toggle.as_ref() {
                        hook();
                    }
                    auto_switched = true;
                }

                // Auto-disable: non-home-row letter means "typing
                // prose, not chording."
                if !auto_switched
                    && rhe_scan.is_none()
                    && crate::layout::keyboard::linux_is_non_home_row_letter(ev.code)
                    && enabled.load(Ordering::Relaxed)
                {
                    enabled.store(false, Ordering::Relaxed);
                    set_scroll_led(fd, true);
                    // Tell rhe to release any chord keys it was
                    // tracking — otherwise the state machine leaves
                    // them pressed forever.
                    for &scan in &auto_held {
                        if let Some(role) = crate::layout::keyboard::linux_to_role(scan) {
                            let _ = tx.send(HidEvent::Key(KeyEvent {
                                scan: role,
                                direction: KeyDirection::Up,
                            }));
                        }
                    }
                    // Inject matching key-downs to the OS so
                    // subsequent physical releases pair up — without
                    // this, the OS sees key-ups for keys it never
                    // saw pressed (rhe was swallowing them).
                    if let Some(ufd) = uinput_fd {
                        for &scan in &auto_held {
                            forward_key(ufd, scan, 1);
                        }
                    }
                    if let Some(hook) = on_toggle.as_ref() {
                        hook();
                    }
                }
            }

            // After auto-enable, this event has already been queued
            // to rhe (as part of the held-set replay); skip normal
            // processing to avoid double-sending it.
            if auto_switched {
                continue;
            }

            let active = enabled.load(Ordering::Relaxed);

            // Quit gesture. Esc down only; Esc up + autorepeat ignored.
            if ev.code == KEY_ESC && ev.value == 1 {
                let should_quit = match quit {
                    QuitTrigger::EscAlone => active,
                    QuitTrigger::CapsLockPlusEsc => caps_held,
                    // Tutor: Esc always quits, regardless of enabled state.
                    // If caps-toggled rhe off and user hits Esc, they still
                    // expect the tutor to exit.
                    QuitTrigger::EscOrCapsPlusEsc => true,
                };
                if should_quit {
                    let _ = tx.send(HidEvent::Quit);
                    continue;
                }
                // else: fall through to passthrough so Esc reaches the app.
            }

            // rhe's own keys, when active: consume, don't forward.
            if active {
                if let Some(scan) = rhe_scan {
                    let dir = match ev.value {
                        0 => KeyDirection::Up,
                        1 => KeyDirection::Down,
                        _ => continue, // skip autorepeat on chord keys
                    };
                    let _ = tx.send(HidEvent::Key(KeyEvent {
                        scan,
                        direction: dir,
                    }));
                    continue;
                }
            }

            // Anything else → re-inject into the OS via uinput.
            if let Some(ufd) = uinput_fd {
                forward_key(ufd, ev.code, ev.value);
            }
        }
    }
    unsafe {
        libc::ioctl(fd, EVIOCGRAB, 0i32);
        libc::close(fd);
    }
}

/// Write the indicator-LED state to a grabbed keyboard. Which physical LED lights up depends on `LED_CODE` (num-lock by default; scroll-lock and caps-lock also possible but often xkb-managed).
fn set_scroll_led(fd: RawFd, on: bool) {
    let led_ev = InputEvent {
        time: libc::timeval {
            tv_sec: 0,
            tv_usec: 0,
        },
        type_: EV_LED,
        code: LED_CODE,
        value: if on { 1 } else { 0 },
    };
    let syn_ev = InputEvent {
        time: libc::timeval {
            tv_sec: 0,
            tv_usec: 0,
        },
        type_: EV_SYN,
        code: SYN_REPORT,
        value: 0,
    };
    let ev_size = std::mem::size_of::<InputEvent>();
    unsafe {
        libc::write(fd, &led_ev as *const _ as *const libc::c_void, ev_size);
        libc::write(fd, &syn_ev as *const _ as *const libc::c_void, ev_size);
    }
}

/// Classify a set of currently-held chord scancodes into the three tally categories the auto-switch heuristic reads. Called on each key-down to decide whether to flip rhe on. Linear scan is fine — the set is bounded by the number of simultaneously pressed keys.
fn classify_held(held: &[u16]) -> (u8, bool, bool) {
    let mut finger: u8 = 0;
    let mut word = false;
    let mut thumb = false;
    for &scan in held {
        if let Some(role) = crate::layout::keyboard::linux_to_role(scan) {
            if role == crate::scan::R_THUMB {
                thumb = true;
            } else if role == crate::scan::WORD {
                word = true;
            } else {
                finger = finger.saturating_add(1);
            }
        }
    }
    (finger, word, thumb)
}

fn forward_key(uinput_fd: RawFd, code: u16, value: i32) {
    let key_ev = InputEvent {
        time: libc::timeval {
            tv_sec: 0,
            tv_usec: 0,
        },
        type_: EV_KEY,
        code,
        value,
    };
    let syn_ev = InputEvent {
        time: libc::timeval {
            tv_sec: 0,
            tv_usec: 0,
        },
        type_: EV_SYN,
        code: SYN_REPORT,
        value: 0,
    };
    let ev_size = std::mem::size_of::<InputEvent>();
    unsafe {
        libc::write(
            uinput_fd,
            &key_ev as *const _ as *const libc::c_void,
            ev_size,
        );
        libc::write(
            uinput_fd,
            &syn_ev as *const _ as *const libc::c_void,
            ev_size,
        );
    }
}

/// Create a virtual keyboard via `/dev/uinput`. Returns its fd, which we write `input_event` records to when forwarding non-rhe keys back to the OS.
///
/// Requires write access to `/dev/uinput` (commonly root; can be granted to the `input` group via a udev rule: `KERNEL=="uinput", GROUP="input", MODE="0660"`).
fn open_uinput() -> Result<RawFd, String> {
    let path = CString::new("/dev/uinput").unwrap();
    let fd = unsafe { libc::open(path.as_ptr(), libc::O_WRONLY | libc::O_NONBLOCK) };
    if fd < 0 {
        return Err(format!(
            "open /dev/uinput: {}",
            std::io::Error::last_os_error()
        ));
    }

    unsafe {
        libc::ioctl(fd, UI_SET_EVBIT, EV_KEY as i32);
        libc::ioctl(fd, UI_SET_EVBIT, EV_SYN as i32);
        // Register every key code up through KEY_MICMUTE (0xF8). Covers the
        // full standard keyboard range without needing per-key logic.
        for code in 1..=0xF8i32 {
            libc::ioctl(fd, UI_SET_KEYBIT, code);
        }
    }

    let mut setup: UinputSetup = unsafe { std::mem::zeroed() };
    setup.id.bustype = BUS_VIRTUAL;
    setup.id.vendor = 0x7268; // "rh"
    setup.id.product = 0x6500; // "e\0"
    setup.id.version = 1;
    let name = b"rhe passthrough";
    setup.name[..name.len()].copy_from_slice(name);

    let rc = unsafe {
        libc::ioctl(
            fd,
            UI_DEV_SETUP,
            &setup as *const UinputSetup as *const libc::c_void,
        )
    };
    if rc < 0 {
        let err = std::io::Error::last_os_error();
        unsafe { libc::close(fd) };
        return Err(format!("UI_DEV_SETUP: {}", err));
    }

    let rc = unsafe { libc::ioctl(fd, UI_DEV_CREATE) };
    if rc < 0 {
        let err = std::io::Error::last_os_error();
        unsafe { libc::close(fd) };
        return Err(format!("UI_DEV_CREATE: {}", err));
    }

    Ok(fd)
}

fn find_keyboards() -> std::io::Result<Vec<String>> {
    let mut out = Vec::new();
    for entry in fs::read_dir("/dev/input")? {
        let entry = entry?;
        let name = entry.file_name();
        let Some(name) = name.to_str() else { continue };
        if !name.starts_with("event") {
            continue;
        }
        // Skip our own uinput devices — grabbing them creates a feedback loop
        // where every char we emit comes right back in as input.
        let dev_name = std::fs::read_to_string(format!("/sys/class/input/{}/device/name", name))
            .unwrap_or_default();
        if dev_name.trim().starts_with("rhe ") {
            continue;
        }
        let path = format!("/dev/input/{}", name);
        if is_keyboard(&path) {
            out.push(path);
        }
    }
    Ok(out)
}

/// A device is considered a keyboard if it advertises KEY_A in its EV_KEY capability bitmap. Mice don't claim KEY_A (their button codes live at BTN_LEFT=0x110+), so this cleanly filters pointers out.
fn is_keyboard(path: &str) -> bool {
    let cpath = match CString::new(path) {
        Ok(c) => c,
        Err(_) => return false,
    };
    let fd = unsafe { libc::open(cpath.as_ptr(), libc::O_RDONLY | libc::O_NONBLOCK) };
    if fd < 0 {
        return false;
    }

    let mut bits = [0u8; 32];
    let rc = unsafe {
        libc::ioctl(
            fd,
            eviocgbit(EV_KEY, bits.len() as u32),
            bits.as_mut_ptr() as *mut libc::c_void,
        )
    };
    unsafe { libc::close(fd) };
    if rc < 0 {
        return false;
    }
    let byte = (KEY_A / 8) as usize;
    let bit = KEY_A % 8;
    bits[byte] & (1 << bit) != 0
}

fn open_and_grab(path: &str) -> Result<RawFd, String> {
    let cpath = CString::new(path).map_err(|e| e.to_string())?;
    let fd = unsafe { libc::open(cpath.as_ptr(), libc::O_RDONLY) };
    if fd < 0 {
        return Err(format!("open: {}", std::io::Error::last_os_error()));
    }
    let rc = unsafe { libc::ioctl(fd, EVIOCGRAB, 1i32) };
    if rc < 0 {
        let err = std::io::Error::last_os_error();
        unsafe { libc::close(fd) };
        return Err(format!("EVIOCGRAB: {}", err));
    }
    Ok(fd)
}
