termina 0.3.3

A cross-platform VT manipulation library
Documentation
//! Operating System Command (OSC) escape sequences.
//!
//! OSC sequences carry string-style terminal integration commands such as window titles, clipboard
//! access, and dynamic color queries. Termina stores string payloads by borrowing where possible
//! so callers can format an OSC command without first allocating an owned [`String`].
//!
//! # Examples
//!
//! ```
//! use termina::escape::osc::Osc;
//!
//! assert_eq!(Osc::SetWindowTitle("demo").to_string(), "\x1b]2;demo\x1b\\");
//! ```
//!
//! OSC 52 selection payloads are base64-encoded when formatted:
//!
//! ```
//! use termina::escape::osc::{Osc, Selection};
//!
//! let command = Osc::SetSelection(Selection::CLIPBOARD, "copied text");
//! assert_eq!(command.to_string(), "\x1b]52;c;Y29waWVkIHRleHQ=\x1b\\");
//! ```
//!
//! # Implementation Notes
//!
//! This is intentionally a shallow copy of [termwiz's OSC support]. Termina removes the
//! macro-heavy parts, replaces the base64 implementation, and changes command payloads to borrow
//! `str` values instead of owning [`String`] values.
//!
//! [termwiz's OSC support]: https://docs.rs/termwiz/latest/termwiz/escape/struct.Osc.html

use std::fmt::{self, Display};

use crate::{base64, style::RgbColor};

/// An Operating System Command string control.
///
/// Formatting writes the OSC introducer, a command number or command letter, the command payload,
/// and the string terminator. The numbered variants use common xterm-compatible assignments: OSC
/// 2 sets the window title, OSC 52 manages selections, and OSC 10-19 manage dynamic colors.
#[derive(Debug, Clone, PartialEq, Eq)]
pub enum Osc<'a> {
    /// OSC 0: set both the icon name and window title.
    SetIconNameAndWindowTitle(&'a str),

    /// OSC 2: set the window title.
    SetWindowTitle(&'a str),

    /// Sun terminal form: set the window title with `OSC l`.
    SetWindowTitleSun(&'a str),

    /// OSC 1: set the icon name.
    SetIconName(&'a str),

    /// Sun terminal form: set the icon name with `OSC L`.
    SetIconNameSun(&'a str),

    /// OSC 52: clear one or more terminal selections described by [`Selection`].
    ///
    /// Terminals use OSC 52 to expose clipboard-like selections. Clearing sends a selection target
    /// without content.
    ClearSelection(Selection),

    /// OSC 52: query one or more terminal selections described by [`Selection`].
    ///
    /// Querying sends `?` as the content payload and lets a cooperating terminal report the
    /// selection contents.
    QuerySelection(Selection),

    /// OSC 52: set one or more terminal selections described by [`Selection`].
    ///
    /// The string payload is base64-encoded when formatted, as required by OSC 52.
    SetSelection(Selection, &'a str),

    /// OSC 10-19: change or query dynamic terminal colors.
    ///
    /// Each [`DynamicColorNumber`] identifies the color slot. [`ColorOrQuery::Query`] formats as
    /// `?`, asking the terminal to report its current value; [`ColorOrQuery::Color`] carries the
    /// [`RgbColor`] to set.
    ChangeDynamicColors(DynamicColorNumber, Vec<ColorOrQuery>),

    /// OSC 110-119: reset a [`DynamicColorNumber`] slot to its default value.
    ///
    /// xterm defines reset commands by adding 100 to the dynamic color number.
    ResetDynamicColor(DynamicColorNumber),
    // TODO: I didn't copy many available commands yet...
}

impl Display for Osc<'_> {
    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
        f.write_str(super::OSC)?;
        match self {
            Self::SetIconNameAndWindowTitle(s) => write!(f, "0;{s}")?,
            Self::SetWindowTitle(s) => write!(f, "2;{s}")?,
            Self::SetWindowTitleSun(s) => write!(f, "l{s}")?,
            Self::SetIconName(s) => write!(f, "1;{s}")?,
            Self::SetIconNameSun(s) => write!(f, "L{s}")?,
            Self::ClearSelection(selection) => write!(f, "52;{selection}")?,
            Self::QuerySelection(selection) => write!(f, "52;{selection};?")?,
            Self::SetSelection(selection, content) => {
                // TODO: it'd be nice to avoid allocating a string to base64 encode.
                write!(f, "52;{selection};{}", base64::encode(content.as_bytes()))?
            }
            Self::ChangeDynamicColors(color, colors) => {
                write!(f, "{}", *color as u8)?;
                for color in colors {
                    write!(f, ";{color}")?
                }
            }
            Self::ResetDynamicColor(color) => write!(f, "{}", 100 + *color as u8)?,
        }
        f.write_str(super::ST)?;
        Ok(())
    }
}

bitflags::bitflags! {
    /// OSC 52 selection targets.
    ///
    /// Multiple targets can be combined. Formatting concatenates the target letters/numbers in the
    /// order expected by xterm-compatible terminals.
    #[derive(Debug, Clone, Copy, PartialEq, Eq)]
    pub struct Selection : u16 {
        /// No OSC 52 selection target.
        const NONE = 0;

        /// OSC 52 selection target `c`: the clipboard selection.
        const CLIPBOARD = 1<<1;

        /// OSC 52 selection target `p`: the primary selection.
        const PRIMARY=1<<2;

        /// OSC 52 selection target `s`: the select selection.
        const SELECT=1<<3;

        /// OSC 52 selection target `0`: cut buffer 0.
        const CUT0=1<<4;

        /// OSC 52 selection target `1`: cut buffer 1.
        const CUT1=1<<5;

        /// OSC 52 selection target `2`: cut buffer 2.
        const CUT2=1<<6;

        /// OSC 52 selection target `3`: cut buffer 3.
        const CUT3=1<<7;

        /// OSC 52 selection target `4`: cut buffer 4.
        const CUT4=1<<8;

        /// OSC 52 selection target `5`: cut buffer 5.
        const CUT5=1<<9;

        /// OSC 52 selection target `6`: cut buffer 6.
        const CUT6=1<<10;

        /// OSC 52 selection target `7`: cut buffer 7.
        const CUT7=1<<11;

        /// OSC 52 selection target `8`: cut buffer 8.
        const CUT8=1<<12;

        /// OSC 52 selection target `9`: cut buffer 9.
        const CUT9=1<<13;
    }
}

impl Display for Selection {
    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
        if self.contains(Self::CLIPBOARD) {
            write!(f, "c")?;
        }
        if self.contains(Self::PRIMARY) {
            write!(f, "p")?;
        }
        if self.contains(Self::SELECT) {
            write!(f, "s")?;
        }
        if self.contains(Self::CUT0) {
            write!(f, "0")?;
        }
        if self.contains(Self::CUT1) {
            write!(f, "1")?;
        }
        if self.contains(Self::CUT2) {
            write!(f, "2")?;
        }
        if self.contains(Self::CUT3) {
            write!(f, "3")?;
        }
        if self.contains(Self::CUT4) {
            write!(f, "4")?;
        }
        if self.contains(Self::CUT5) {
            write!(f, "5")?;
        }
        if self.contains(Self::CUT6) {
            write!(f, "6")?;
        }
        if self.contains(Self::CUT7) {
            write!(f, "7")?;
        }
        if self.contains(Self::CUT8) {
            write!(f, "8")?;
        }
        if self.contains(Self::CUT9) {
            write!(f, "9")?;
        }
        Ok(())
    }
}

/// Dynamic color slots addressed by OSC 10-19.
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
#[repr(u8)]
pub enum DynamicColorNumber {
    /// OSC 10: the default text foreground color used for normal cells.
    TextForegroundColor = 10,
    /// OSC 11: the default text background color used for normal cells.
    TextBackgroundColor = 11,
    /// OSC 12: the text cursor color.
    TextCursorColor = 12,
    /// OSC 13: the pointer foreground color used by xterm-compatible terminals.
    MouseForegroundColor = 13,
    /// OSC 14: the pointer background color used by xterm-compatible terminals.
    MouseBackgroundColor = 14,
    /// OSC 15: the Tektronix foreground color.
    TektronixForegroundColor = 15,
    /// OSC 16: the Tektronix background color.
    TektronixBackgroundColor = 16,
    /// OSC 17: the selection highlight background color.
    HighlightBackgroundColor = 17,
    /// OSC 18: the Tektronix cursor color.
    TektronixCursorColor = 18,
    /// OSC 19: the selection highlight foreground color.
    HighlightForegroundColor = 19,
}

impl DynamicColorNumber {
    pub(crate) fn from_index(index: u8) -> Option<Self> {
        match index {
            10 => Some(Self::TextForegroundColor),
            11 => Some(Self::TextBackgroundColor),
            12 => Some(Self::TextCursorColor),
            13 => Some(Self::MouseForegroundColor),
            14 => Some(Self::MouseBackgroundColor),
            15 => Some(Self::TektronixForegroundColor),
            16 => Some(Self::TektronixBackgroundColor),
            17 => Some(Self::HighlightBackgroundColor),
            18 => Some(Self::TektronixCursorColor),
            19 => Some(Self::HighlightForegroundColor),
            _ => None,
        }
    }
}

/// A dynamic color value or query marker for OSC color commands.
///
/// Use [`Self::Query`] to ask the terminal for a slot's current value, or [`Self::Color`] to set a
/// new RGB value:
///
/// ```
/// use termina::{
///     escape::osc::{ColorOrQuery, DynamicColorNumber, Osc},
///     style::RgbColor,
/// };
///
/// let query = Osc::ChangeDynamicColors(
///     DynamicColorNumber::TextBackgroundColor,
///     vec![ColorOrQuery::Query],
/// );
/// assert_eq!(query.to_string(), "\x1b]11;?\x1b\\");
///
/// let set = Osc::ChangeDynamicColors(
///     DynamicColorNumber::TextForegroundColor,
///     vec![RgbColor::new(40, 40, 40).into()],
/// );
/// assert_eq!(set.to_string(), "\x1b]10;rgb:2828/2828/2828\x1b\\");
/// ```
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum ColorOrQuery {
    /// Set the dynamic color to an RGB value.
    ///
    /// Formatting emits the `rgb:RRRR/GGGG/BBBB` form used by xterm OSC color controls.
    Color(RgbColor),

    /// Query the current dynamic color value.
    ///
    /// Formatting emits `?`, which asks the terminal to report the current value for that slot.
    Query,
}

impl Display for ColorOrQuery {
    fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
        match self {
            ColorOrQuery::Query => write!(f, "?"),
            ColorOrQuery::Color(c) => {
                // rgb:RRRR/GGGG/BBBB
                write!(
                    f,
                    "rgb:{red:02x}{red:02x}/{green:02x}{green:02x}/{blue:02x}{blue:02x}",
                    red = c.red,
                    green = c.green,
                    blue = c.blue
                )
            }
        }
    }
}

impl From<RgbColor> for ColorOrQuery {
    fn from(color: RgbColor) -> Self {
        Self::Color(color)
    }
}

#[cfg(test)]
mod test {
    use super::*;

    #[test]
    fn encoding() {
        // OSC 11 query, asks the terminal for the background color.
        // <https://terminalguide.namepad.de/seq/osc-11/>
        // <https://terminalguide.namepad.de/seq/osc-4/>
        assert_eq!(
            "\x1b]11;?\x1b\\",
            Osc::ChangeDynamicColors(
                DynamicColorNumber::TextBackgroundColor,
                vec![ColorOrQuery::Query]
            )
            .to_string()
        );

        assert_eq!(
            "\x1b]11;rgb:2828/2828/2828\x1b\\",
            Osc::ChangeDynamicColors(
                DynamicColorNumber::TextBackgroundColor,
                vec![RgbColor::new(40, 40, 40).into()]
            )
            .to_string()
        );
    }
}