tastty-core 0.1.0

Sans-IO core of the tastty terminal session library: VT parser, screen buffer, and byte encoders.
use std::time::Duration;

use super::super::{ProgressState, Screen, ScreenEvent};

/// Cap on `OSC 9 ; 1 ; <ms> ST` so a misbehaving program cannot
/// stall an embedder that honors the pause.
const SLEEP_MAX_MS: u64 = 10_000;

pub(in crate::screen) fn handle_osc9(screen: &mut Screen, params: &[&[u8]]) {
    // Only the well-defined ConEmu sub-commands surface; the rest of
    // the OSC 9 namespace is reserved.
    match params.len() {
        0 | 1 => {}
        2 => emit_notification(screen, params[1]),
        _ => match params[1] {
            b"1" => emit_sleep(screen, params[2]),
            b"4" => {
                if let Some(value) = params.get(3) {
                    emit_progress(screen, params[2], value);
                }
            }
            _ => {}
        },
    }
}

fn emit_notification(screen: &mut Screen, body: &[u8]) {
    if body.is_empty() {
        return;
    }
    screen
        .pending_events
        .push(ScreenEvent::DesktopNotification {
            title: String::new(),
            body: String::from_utf8_lossy(body).into_owned(),
        });
}

fn emit_sleep(screen: &mut Screen, ms_bytes: &[u8]) {
    let Some(ms) = parse_u64(ms_bytes) else {
        return;
    };
    let clamped = ms.min(SLEEP_MAX_MS);
    screen.pending_events.push(ScreenEvent::Sleep {
        duration: Duration::from_millis(clamped),
    });
}

fn emit_progress(screen: &mut Screen, state_bytes: &[u8], value_bytes: &[u8]) {
    let Some(state_num) = parse_u64(state_bytes) else {
        return;
    };
    let state = match state_num {
        0 => ProgressState::Remove,
        1 => ProgressState::Set,
        2 => ProgressState::Error,
        3 => ProgressState::Indeterminate,
        4 => ProgressState::Warning,
        _ => return,
    };
    let Some(value) = parse_u64(value_bytes) else {
        return;
    };
    let clamped: u8 = value.min(100) as u8;
    screen.pending_events.push(ScreenEvent::ProgressReport {
        state,
        value: clamped,
    });
}

fn parse_u64(bytes: &[u8]) -> Option<u64> {
    std::str::from_utf8(bytes).ok()?.parse().ok()
}

pub(in crate::screen) fn handle_osc777(screen: &mut Screen, params: &[&[u8]]) {
    // Only the `notify` sub-command is recognized; others are reserved.
    // An empty title and body still pass through because the explicit
    // sub-command signals deliberate intent.
    if params.get(1) != Some(&&b"notify"[..]) {
        return;
    }
    let title = params.get(2).copied().unwrap_or(b"");
    let body = params.get(3).copied().unwrap_or(b"");
    screen
        .pending_events
        .push(ScreenEvent::DesktopNotification {
            title: String::from_utf8_lossy(title).into_owned(),
            body: String::from_utf8_lossy(body).into_owned(),
        });
}