timerd-rs 0.1.0

A small foreground/background terminal timer for Linux
use std::fs;
use std::io::{BufRead, BufReader, Write};
use std::os::unix::fs::FileTypeExt;
use std::os::unix::net::UnixStream;

use anyhow::{Context, Result, bail};

use crate::paths::AppPaths;
use crate::protocol::{Request, Response};

pub fn request(paths: &AppPaths, request: &Request) -> Result<Response> {
    let mut stream = connect_and_send(paths, request)?;
    let mut line = String::new();
    BufReader::new(&mut stream)
        .read_line(&mut line)
        .context("failed to read daemon response")?;
    if line.is_empty() {
        bail!("timer daemon closed the connection without responding");
    }
    serde_json::from_str(&line).context("timer daemon returned an invalid response")
}

pub fn attach(paths: &AppPaths, id: Option<u64>) -> Result<UnixStream> {
    connect_and_send(paths, &Request::Attach { id })
}

fn connect_and_send(paths: &AppPaths, request: &Request) -> Result<UnixStream> {
    let mut stream = UnixStream::connect(&paths.socket)
        .with_context(|| format!("failed to connect to {}", paths.socket.display()))?;
    serde_json::to_writer(&mut stream, request).context("failed to encode daemon request")?;
    stream
        .write_all(b"\n")
        .context("failed to send daemon request")?;
    stream.flush().context("failed to flush daemon request")?;
    Ok(stream)
}

pub fn current_tty_path() -> Option<String> {
    for descriptor in [1, 0, 2] {
        let Ok(link) = fs::read_link(format!("/proc/self/fd/{descriptor}")) else {
            continue;
        };
        let Ok(metadata) = fs::metadata(&link) else {
            continue;
        };
        if metadata.file_type().is_char_device() {
            if let Some(path) = link.to_str() {
                if path.starts_with("/dev/pts/")
                    || path.starts_with("/dev/tty")
                    || path == "/dev/console"
                {
                    return Some(path.to_owned());
                }
            }
        }
    }
    None
}

pub fn expect_response(response: Response) -> Result<Response> {
    match response {
        Response::Error { message } => bail!(message),
        response => Ok(response),
    }
}