trackWork 0.15.0

A terminal-based time tracking application for managing work sessions
//! Query the terminal for the *actual* RGB behind an ANSI palette index, so
//! animations (e.g. the running-entry shimmer) can derive shades that match the
//! user's real theme instead of guessing.
//!
//! Uses the OSC 4 escape: we write `ESC ] 4 ; <index> ; ? BEL` and the terminal
//! replies on stdin with `ESC ] 4 ; <index> ; rgb:RRRR/GGGG/BBBB <terminator>`.
//! Many modern terminals answer; ones that don't simply time out → `None`, and
//! the caller falls back to named colors. Must be called with raw mode enabled
//! (so the reply isn't echoed/line-buffered) and before the event loop starts.

use std::collections::HashMap;
use std::io::Write;
use std::time::{Duration, Instant};

const STDIN_FD: i32 = 0;

/// Query the real RGB of the 16 ANSI palette colors (indices 0–15). Returns an
/// empty map if the terminal doesn't support OSC 4 color queries: we probe
/// index 0 first and bail early so a non-answering terminal costs one timeout,
/// not sixteen.
pub fn query_palette() -> HashMap<u8, (u8, u8, u8)> {
    let mut map = HashMap::new();
    for i in 0u8..16 {
        match query_palette_rgb(i) {
            Some(rgb) => {
                map.insert(i, rgb);
            }
            None if i == 0 => break, // unsupported terminal → don't probe the rest
            None => {}
        }
    }
    map
}

/// Query the RGB of ANSI palette color `index` (e.g. 2 = green). Returns `None`
/// if the terminal doesn't answer within a short timeout.
pub fn query_palette_rgb(index: u8) -> Option<(u8, u8, u8)> {
    let mut out = std::io::stdout();
    write!(out, "\x1b]4;{};?\x07", index).ok()?;
    out.flush().ok()?;

    let deadline = Instant::now() + Duration::from_millis(150);

    let mut buf: Vec<u8> = Vec::with_capacity(64);
    loop {
        let remaining = deadline.saturating_duration_since(Instant::now());
        if remaining.is_zero() || !readable(STDIN_FD, remaining) {
            break;
        }
        // Read raw from the fd, bypassing std::io::Stdin's internal buffer
        // (which would otherwise drain the whole reply and defeat our poll).
        match read_byte(STDIN_FD) {
            Some(b) => {
                buf.push(b);
                // Response ends with BEL or ST (ESC \).
                if b == 0x07 {
                    break;
                }
                if b == b'\\' && buf.len() >= 2 && buf[buf.len() - 2] == 0x1b {
                    break;
                }
                if buf.len() > 64 {
                    break;
                }
            }
            None => break,
        }
    }
    parse_rgb(&buf)
}

/// Read a single byte directly from `fd` (no buffering). `None` on EOF/error.
fn read_byte(fd: i32) -> Option<u8> {
    let mut b = [0u8; 1];
    let n = unsafe { libc::read(fd, b.as_mut_ptr() as *mut libc::c_void, 1) };
    if n == 1 {
        Some(b[0])
    } else {
        None
    }
}

/// Poll `fd` for readability, up to `timeout`.
fn readable(fd: i32, timeout: Duration) -> bool {
    let mut pfd = libc::pollfd {
        fd,
        events: libc::POLLIN,
        revents: 0,
    };
    let ms = timeout.as_millis().min(i32::MAX as u128) as i32;
    let rc = unsafe { libc::poll(&mut pfd, 1, ms) };
    rc > 0 && (pfd.revents & libc::POLLIN) != 0
}

/// Parse `...rgb:RRRR/GGGG/BBBB...` (channels may be 1–4 hex digits) into 8-bit.
fn parse_rgb(buf: &[u8]) -> Option<(u8, u8, u8)> {
    let s = String::from_utf8_lossy(buf);
    let start = s.find("rgb:")? + 4;
    let tail = &s[start..];
    let end = tail.find(['\x1b', '\x07']).unwrap_or(tail.len());
    let parts: Vec<&str> = tail[..end].split('/').collect();
    if parts.len() < 3 {
        return None;
    }
    Some((scale(parts[0])?, scale(parts[1])?, scale(parts[2])?))
}

/// Scale a 1–4 hex-digit channel value to 8 bits.
fn scale(p: &str) -> Option<u8> {
    let p = p.trim();
    let v = u32::from_str_radix(p, 16).ok()?;
    let scaled = match p.len() {
        1 => v * 17,   // 0xF -> 0xFF
        2 => v,        // already 8-bit
        3 => v >> 4,   // 12-bit -> 8-bit
        4 => v / 257,  // 16-bit -> 8-bit
        _ => return None,
    };
    Some(scaled.min(255) as u8)
}