timer_core 0.7.6

Simple countdown terminal alarm
Documentation
use crate::Result;

use std::error::Error;
use std::fmt;
use std::fs::{File, OpenOptions};
use std::mem::{size_of, MaybeUninit};
use std::os::unix::io::AsRawFd;
use std::sync::LazyLock;
use std::thread::sleep;
use std::time::Duration;

use glob::glob;
#[cfg(target_os = "linux")]
use libc::{c_void, input_event, write};
use nix::ioctl_write_int_bad;

const DEVICE_PATHS: [&str; 2] = [
    "/dev/input/by-path/platform-pcspkr-event-spkr",
    "/dev/console",
];
const EV_SND: u16 = 0x12;
const SND_TONE: u16 = 0x02;
const KIOCSOUND: u64 = 0x4B2F;
const TIMER_FREQUENCY: u32 = 1193182;

static DEVICE: LazyLock<Option<File>> = LazyLock::new(get_device);

fn get_device() -> Option<File> {
    let strings_from_glob = |x| {
        glob(x)
            .unwrap()
            .map(|x| x.unwrap().to_str().unwrap().to_string())
            .collect::<Vec<String>>()
    };
    let all_ttys = strings_from_glob("/dev/tty[0-9]*");
    let all_vcs = strings_from_glob("/dev/vc/[0-9]*");
    DEVICE_PATHS
        .into_iter()
        .map(|s| s.to_string())
        .chain(all_ttys)
        .chain(all_vcs)
        .find_map(|d| OpenOptions::new().append(true).open(d).ok())
}

ioctl_write_int_bad!(kiocsound, KIOCSOUND);

#[derive(Debug, Clone)]
struct DeviceError;

impl fmt::Display for DeviceError {
    fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
        write!(f, "No device found in: {:?}", DEVICE_PATHS)
    }
}

impl Error for DeviceError {}

struct Driver {
    device: &'static File,
    beep: fn(dev: &File, freq: i32) -> nix::Result<()>,
}

impl Driver {
    pub fn beep(&self, freq: i32) -> nix::Result<()> {
        (self.beep)(self.device, freq)
    }
}

#[cfg(target_os = "linux")]
fn driver_evdev(dev: &File, freq: i32) -> nix::Result<()> {
    unsafe {
        let mut e = MaybeUninit::<input_event>::zeroed().assume_init();
        e.type_ = EV_SND;
        e.code = SND_TONE;
        e.value = freq;

        let e_ptr: *mut c_void = &mut e as *mut _ as *mut c_void;
        write(dev.as_raw_fd(), e_ptr, size_of::<input_event>());
    }
    Ok(())
}

fn driver_console(dev: &File, freq: i32) -> nix::Result<()> {
    let period_in_clock_cycles = TIMER_FREQUENCY.checked_div(freq as u32).unwrap_or(0);

    unsafe {
        kiocsound(dev.as_raw_fd(), period_in_clock_cycles as i32)?;
    }
    Ok(())
}

#[cfg(target_os = "linux")]
fn get_driver(device: &'static File) -> Driver {
    match unsafe { kiocsound(device.as_raw_fd(), 0) } {
        Ok(_) => Driver {
            device,
            beep: driver_console,
        },
        Err(_) => Driver {
            device,
            beep: driver_evdev,
        },
    }
}

#[cfg(not(target_os = "linux"))]
fn get_driver(device: &'static File) -> Driver {
    Driver {
        device,
        beep: driver_console,
    }
}

pub fn beep(freq: i32, time: Duration) -> Result<()> {
    let device = DEVICE.as_ref().ok_or(DeviceError)?;
    let driver = get_driver(device);

    driver.beep(freq)?;
    sleep(time);
    driver.beep(0)?;
    Ok(())
}