blob: 1bf4f039133ac298698e2d75934b0232a9333f0b [file]
//! 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()
);
}
}