use std::{
fmt::{self, Display},
num::NonZeroU16,
};
use crate::{
event::Modifiers,
style::{Blink, ColorSpec, CursorStyle, Font, Intensity, RgbaColor, Underline, VerticalAlign},
OneBased,
};
#[derive(Debug, Clone, PartialEq, Eq)]
pub enum Csi {
Sgr(Sgr),
Cursor(Cursor),
Edit(Edit),
Mode(Mode),
Mouse(MouseReport),
Keyboard(Keyboard),
Device(Device),
Window(Box<Window>),
}
impl Display for Csi {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
f.write_str(super::CSI)?;
match self {
Self::Sgr(sgr) => write!(f, "{sgr}m"),
Self::Cursor(cursor) => cursor.fmt(f),
Self::Edit(edit) => edit.fmt(f),
Self::Mode(mode) => mode.fmt(f),
Self::Mouse(report) => report.fmt(f),
Self::Keyboard(keyboard) => keyboard.fmt(f),
Self::Device(device) => device.fmt(f),
Self::Window(window) => window.fmt(f),
}
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum Sgr {
Reset,
Intensity(Intensity),
Underline(Underline),
Blink(Blink),
Italic(bool),
Reverse(bool),
Invisible(bool),
StrikeThrough(bool),
Overline(bool),
Font(Font),
VerticalAlign(VerticalAlign),
Foreground(ColorSpec),
Background(ColorSpec),
UnderlineColor(ColorSpec),
Attributes(SgrAttributes),
}
impl Display for Sgr {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
fn write_true_color(
code: u8,
RgbaColor {
red,
green,
blue,
alpha,
}: RgbaColor,
f: &mut fmt::Formatter,
) -> fmt::Result {
if alpha == 255 {
write!(f, "{code};2;{red};{green};{blue}")
} else {
write!(f, "{code}:6::{red}:{green}:{blue}:{alpha}")
}
}
match self {
Self::Reset => (),
Self::Intensity(Intensity::Normal) => write!(f, "22")?,
Self::Intensity(Intensity::Bold) => write!(f, "1")?,
Self::Intensity(Intensity::Dim) => write!(f, "2")?,
Self::Underline(Underline::None) => write!(f, "24")?,
Self::Underline(Underline::Single) => write!(f, "4")?,
Self::Underline(Underline::Double) => write!(f, "21")?,
Self::Underline(Underline::Curly) => write!(f, "4:3")?,
Self::Underline(Underline::Dotted) => write!(f, "4:4")?,
Self::Underline(Underline::Dashed) => write!(f, "4:5")?,
Self::Blink(Blink::None) => write!(f, "25")?,
Self::Blink(Blink::Slow) => write!(f, "5")?,
Self::Blink(Blink::Rapid) => write!(f, "6")?,
Self::Italic(true) => write!(f, "3")?,
Self::Italic(false) => write!(f, "23")?,
Self::Reverse(true) => write!(f, "7")?,
Self::Reverse(false) => write!(f, "27")?,
Self::Invisible(true) => write!(f, "8")?,
Self::Invisible(false) => write!(f, "28")?,
Self::StrikeThrough(true) => write!(f, "9")?,
Self::StrikeThrough(false) => write!(f, "29")?,
Self::Overline(true) => write!(f, "53")?,
Self::Overline(false) => write!(f, "55")?,
Self::Font(Font::Default) => write!(f, "10")?,
Self::Font(Font::Alternate(1)) => write!(f, "11")?,
Self::Font(Font::Alternate(2)) => write!(f, "12")?,
Self::Font(Font::Alternate(3)) => write!(f, "13")?,
Self::Font(Font::Alternate(4)) => write!(f, "14")?,
Self::Font(Font::Alternate(5)) => write!(f, "15")?,
Self::Font(Font::Alternate(6)) => write!(f, "16")?,
Self::Font(Font::Alternate(7)) => write!(f, "17")?,
Self::Font(Font::Alternate(8)) => write!(f, "18")?,
Self::Font(Font::Alternate(9)) => write!(f, "19")?,
Self::Font(_) => (),
Self::VerticalAlign(VerticalAlign::BaseLine) => write!(f, "75")?,
Self::VerticalAlign(VerticalAlign::SuperScript) => write!(f, "73")?,
Self::VerticalAlign(VerticalAlign::SubScript) => write!(f, "74")?,
Self::Foreground(ColorSpec::Reset) => write!(f, "39")?,
Self::Foreground(ColorSpec::BLACK) => write!(f, "30")?,
Self::Foreground(ColorSpec::RED) => write!(f, "31")?,
Self::Foreground(ColorSpec::GREEN) => write!(f, "32")?,
Self::Foreground(ColorSpec::YELLOW) => write!(f, "33")?,
Self::Foreground(ColorSpec::BLUE) => write!(f, "34")?,
Self::Foreground(ColorSpec::MAGENTA) => write!(f, "35")?,
Self::Foreground(ColorSpec::CYAN) => write!(f, "36")?,
Self::Foreground(ColorSpec::WHITE) => write!(f, "37")?,
Self::Foreground(ColorSpec::BRIGHT_BLACK) => write!(f, "90")?,
Self::Foreground(ColorSpec::BRIGHT_RED) => write!(f, "91")?,
Self::Foreground(ColorSpec::BRIGHT_GREEN) => write!(f, "92")?,
Self::Foreground(ColorSpec::BRIGHT_YELLOW) => write!(f, "93")?,
Self::Foreground(ColorSpec::BRIGHT_BLUE) => write!(f, "94")?,
Self::Foreground(ColorSpec::BRIGHT_MAGENTA) => write!(f, "95")?,
Self::Foreground(ColorSpec::BRIGHT_CYAN) => write!(f, "96")?,
Self::Foreground(ColorSpec::BRIGHT_WHITE) => write!(f, "97")?,
Self::Foreground(ColorSpec::PaletteIndex(idx)) => write!(f, "38;5;{idx}")?,
Self::Foreground(ColorSpec::TrueColor(color)) => write_true_color(38, *color, f)?,
Self::Background(ColorSpec::Reset) => write!(f, "49")?,
Self::Background(ColorSpec::BLACK) => write!(f, "40")?,
Self::Background(ColorSpec::RED) => write!(f, "41")?,
Self::Background(ColorSpec::GREEN) => write!(f, "42")?,
Self::Background(ColorSpec::YELLOW) => write!(f, "43")?,
Self::Background(ColorSpec::BLUE) => write!(f, "44")?,
Self::Background(ColorSpec::MAGENTA) => write!(f, "45")?,
Self::Background(ColorSpec::CYAN) => write!(f, "46")?,
Self::Background(ColorSpec::WHITE) => write!(f, "47")?,
Self::Background(ColorSpec::BRIGHT_BLACK) => write!(f, "100")?,
Self::Background(ColorSpec::BRIGHT_RED) => write!(f, "101")?,
Self::Background(ColorSpec::BRIGHT_GREEN) => write!(f, "102")?,
Self::Background(ColorSpec::BRIGHT_YELLOW) => write!(f, "103")?,
Self::Background(ColorSpec::BRIGHT_BLUE) => write!(f, "104")?,
Self::Background(ColorSpec::BRIGHT_MAGENTA) => write!(f, "105")?,
Self::Background(ColorSpec::BRIGHT_CYAN) => write!(f, "106")?,
Self::Background(ColorSpec::BRIGHT_WHITE) => write!(f, "107")?,
Self::Background(ColorSpec::PaletteIndex(idx)) => write!(f, "48;5;{idx}")?,
Self::Background(ColorSpec::TrueColor(color)) => write_true_color(48, *color, f)?,
Self::UnderlineColor(ColorSpec::Reset) => write!(f, "59")?,
Self::UnderlineColor(ColorSpec::PaletteIndex(idx)) => write!(f, "58:5:{idx}")?,
Self::UnderlineColor(ColorSpec::TrueColor(RgbaColor {
red,
green,
blue,
alpha: 255,
})) => {
write!(f, "58:2::{red}:{green}:{blue}")?;
}
Self::UnderlineColor(ColorSpec::TrueColor(RgbaColor {
red,
green,
blue,
alpha,
})) => {
write!(f, "58:6::{red}:{green}:{blue}:{alpha}")?;
}
Self::Attributes(attributes) => {
use SgrModifiers as Mod;
let ps_budget = attributes.parameter_chunk_size.get();
let mut ps_written = 0;
let mut first = true;
let mut write = |sgr: Self, n_ps: u16| {
ps_written += n_ps;
if ps_written > ps_budget {
write!(f, "m{}", super::CSI)?;
ps_written = n_ps;
} else if !first {
f.write_str(";")?;
}
first = false;
write!(f, "{sgr}")
};
if attributes.modifiers.contains(Mod::RESET) {
write(Self::Reset, 0)?;
}
if let Some(color) = attributes.foreground {
write(
Self::Foreground(color),
match color {
ColorSpec::Reset => 1,
ColorSpec::PaletteIndex(_) => 3,
ColorSpec::TrueColor(RgbaColor { alpha: 255, .. }) => 5,
ColorSpec::TrueColor(_) => 6,
},
)?;
}
if let Some(color) = attributes.background {
write(
Self::Background(color),
match color {
ColorSpec::Reset => 1,
ColorSpec::PaletteIndex(_) => 3,
ColorSpec::TrueColor(RgbaColor { alpha: 255, .. }) => 5,
ColorSpec::TrueColor(_) => 6,
},
)?;
}
if let Some(color) = attributes.underline_color {
write(
Self::UnderlineColor(color),
match color {
ColorSpec::Reset => 1,
ColorSpec::PaletteIndex(_) => 3,
ColorSpec::TrueColor(_) => 6,
},
)?;
}
if attributes.modifiers.contains(Mod::INTENSITY_NORMAL) {
write(Self::Intensity(Intensity::Normal), 1)?;
}
if attributes.modifiers.contains(Mod::INTENSITY_DIM) {
write(Self::Intensity(Intensity::Dim), 1)?;
}
if attributes.modifiers.contains(Mod::INTENSITY_BOLD) {
write(Self::Intensity(Intensity::Bold), 1)?;
}
if attributes.modifiers.contains(Mod::UNDERLINE_NONE) {
write(Self::Underline(Underline::None), 1)?;
}
if attributes.modifiers.contains(Mod::UNDERLINE_SINGLE) {
write(Self::Underline(Underline::Single), 1)?;
}
if attributes.modifiers.contains(Mod::UNDERLINE_DOUBLE) {
write(Self::Underline(Underline::Double), 1)?;
}
if attributes.modifiers.contains(Mod::UNDERLINE_CURLY) {
write(Self::Underline(Underline::Curly), 2)?;
}
if attributes.modifiers.contains(Mod::UNDERLINE_DOTTED) {
write(Self::Underline(Underline::Dotted), 2)?;
}
if attributes.modifiers.contains(Mod::UNDERLINE_DASHED) {
write(Self::Underline(Underline::Dashed), 2)?;
}
if attributes.modifiers.contains(Mod::BLINK_NONE) {
write(Self::Blink(Blink::None), 1)?;
}
if attributes.modifiers.contains(Mod::BLINK_SLOW) {
write(Self::Blink(Blink::Slow), 1)?;
}
if attributes.modifiers.contains(Mod::BLINK_RAPID) {
write(Self::Blink(Blink::Rapid), 1)?;
}
if attributes.modifiers.contains(Mod::ITALIC) {
write(Self::Italic(true), 1)?;
}
if attributes.modifiers.contains(Mod::NO_ITALIC) {
write(Self::Italic(false), 1)?;
}
if attributes.modifiers.contains(Mod::REVERSE) {
write(Self::Reverse(true), 1)?;
}
if attributes.modifiers.contains(Mod::NO_REVERSE) {
write(Self::Reverse(false), 1)?;
}
if attributes.modifiers.contains(Mod::INVISIBLE) {
write(Self::Invisible(true), 1)?;
}
if attributes.modifiers.contains(Mod::NO_INVISIBLE) {
write(Self::Invisible(false), 1)?;
}
if attributes.modifiers.contains(Mod::STRIKE_THROUGH) {
write(Self::StrikeThrough(true), 1)?;
}
if attributes.modifiers.contains(Mod::NO_STRIKE_THROUGH) {
write(Self::StrikeThrough(false), 1)?;
}
}
}
Ok(())
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub struct SgrAttributes {
pub foreground: Option<ColorSpec>,
pub background: Option<ColorSpec>,
pub underline_color: Option<ColorSpec>,
pub modifiers: SgrModifiers,
pub parameter_chunk_size: NonZeroU16,
}
impl Default for SgrAttributes {
fn default() -> Self {
Self {
foreground: Default::default(),
background: Default::default(),
underline_color: Default::default(),
modifiers: Default::default(),
parameter_chunk_size: unsafe { NonZeroU16::new_unchecked(10) },
}
}
}
impl SgrAttributes {
#[inline]
pub fn is_empty(&self) -> bool {
self.foreground.is_none()
&& self.background.is_none()
&& self.underline_color.is_none()
&& self.modifiers.is_empty()
}
}
bitflags::bitflags! {
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub struct SgrModifiers: u32 {
const NONE = 0;
const RESET = 1 << 1;
const INTENSITY_NORMAL = 1 << 2;
const INTENSITY_DIM = 1 << 3;
const INTENSITY_BOLD = 1 << 4;
const UNDERLINE_NONE = 1 << 5;
const UNDERLINE_SINGLE = 1 << 6;
const UNDERLINE_DOUBLE = 1 << 7;
const UNDERLINE_CURLY = 1 << 8;
const UNDERLINE_DOTTED = 1 << 9;
const UNDERLINE_DASHED = 1 << 10;
const BLINK_NONE = 1 << 11;
const BLINK_SLOW = 1 << 12;
const BLINK_RAPID = 1 << 13;
const ITALIC = 1 << 14;
const NO_ITALIC = 1 << 15;
const REVERSE = 1 << 16;
const NO_REVERSE = 1 << 17;
const INVISIBLE = 1 << 18;
const NO_INVISIBLE = 1 << 19;
const STRIKE_THROUGH = 1 << 20;
const NO_STRIKE_THROUGH = 1 << 21;
}
}
impl Default for SgrModifiers {
fn default() -> Self {
Self::NONE
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum MultiCursorShape {
Style(CursorStyle),
FollowMainCursor,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum MultiCursorCapability {
BlockShape = 1,
BeamShape = 2,
UnderlineShape = 3,
FollowMainCursorShape = 29,
TextColor = 30,
CursorColor = 40,
QueryCurrentCursors = 100,
QueryColors = 101,
}
impl TryFrom<u8> for MultiCursorCapability {
type Error = u8;
fn try_from(value: u8) -> std::result::Result<Self, Self::Error> {
match value {
1 => Ok(Self::BlockShape),
2 => Ok(Self::BeamShape),
3 => Ok(Self::UnderlineShape),
29 => Ok(Self::FollowMainCursorShape),
30 => Ok(Self::TextColor),
40 => Ok(Self::CursorColor),
100 => Ok(Self::QueryCurrentCursors),
101 => Ok(Self::QueryColors),
_ => Err(value),
}
}
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub enum Cursor {
BackwardTabulation(u32),
TabulationClear(TabulationClear),
CharacterAbsolute(OneBased),
CharacterPositionAbsolute(OneBased),
CharacterPositionBackward(u32),
CharacterPositionForward(u32),
CharacterAndLinePosition {
line: OneBased,
col: OneBased,
},
LinePositionAbsolute(u32),
LinePositionBackward(u32),
LinePositionForward(u32),
ForwardTabulation(u32),
NextLine(u32),
PrecedingLine(u32),
ActivePositionReport {
line: OneBased,
col: OneBased,
},
RequestActivePositionReport,
SaveCursor,
RestoreCursor,
TabulationControl(CursorTabulationControl),
Left(u32),
Down(u32),
Right(u32),
Up(u32),
Position {
line: OneBased,
col: OneBased,
},
LineTabulation(u32),
SetTopAndBottomMargins {
top: OneBased,
bottom: OneBased,
},
SetLeftAndRightMargins {
left: OneBased,
right: OneBased,
},
CursorStyle(CursorStyle),
QueryCursorShape,
CursorShapeQueryResponse(Vec<MultiCursorCapability>),
SetMultipleCursors {
shape: MultiCursorShape,
positions: Vec<(OneBased, OneBased)>,
},
ClearSecondaryCursors,
}
impl Cursor {
pub const fn default_position() -> Self {
Self::Position {
line: OneBased::from_zero_based(0),
col: OneBased::from_zero_based(0),
}
}
}
impl Display for Cursor {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
fn write_csi<T: Default + Eq + Display>(
value: T,
f: &mut fmt::Formatter<'_>,
control: &str,
) -> fmt::Result {
if value == T::default() {
write!(f, "{control}")
} else {
write!(f, "{value}{control}")
}
}
match self {
Cursor::BackwardTabulation(n) => write_csi(*n, f, "Z"),
Cursor::TabulationClear(n) => write_csi(*n, f, "g"),
Cursor::CharacterAbsolute(n) => write_csi(*n, f, "G"),
Cursor::CharacterPositionAbsolute(n) => write_csi(*n, f, "``"),
Cursor::CharacterPositionBackward(n) => write_csi(*n, f, "j"),
Cursor::CharacterPositionForward(n) => write_csi(*n, f, "a"),
Cursor::CharacterAndLinePosition { line, col } => write!(f, "{line};{col}f"),
Cursor::LinePositionAbsolute(n) => write_csi(*n, f, "d"),
Cursor::LinePositionBackward(n) => write_csi(*n, f, "k"),
Cursor::LinePositionForward(n) => write_csi(*n, f, "e"),
Cursor::ForwardTabulation(n) => write_csi(*n, f, "I"),
Cursor::NextLine(n) => write_csi(*n, f, "E"),
Cursor::PrecedingLine(n) => write_csi(*n, f, "F"),
Cursor::ActivePositionReport { line, col } => write!(f, "{line};{col}R"),
Cursor::RequestActivePositionReport => write!(f, "6n"),
Cursor::SaveCursor => write!(f, "s"),
Cursor::RestoreCursor => write!(f, "u"),
Cursor::TabulationControl(n) => write_csi(*n, f, "W"),
Cursor::Left(n) => write_csi(*n, f, "D"),
Cursor::Down(n) => write_csi(*n, f, "B"),
Cursor::Right(n) => write_csi(*n, f, "C"),
Cursor::Up(n) => write_csi(*n, f, "A"),
Cursor::Position { line, col } => write!(f, "{line};{col}H"),
Cursor::LineTabulation(n) => write_csi(*n, f, "Y"),
Cursor::SetTopAndBottomMargins { top, bottom } => {
if top.get() == 1 && bottom.get() == u16::MAX {
write!(f, "r")
} else {
write!(f, "{top};{bottom}r")
}
}
Cursor::SetLeftAndRightMargins { left, right } => {
if left.get() == 1 && right.get() == u16::MAX {
write!(f, "s")
} else {
write!(f, "{left};{right}s")
}
}
Cursor::CursorStyle(style) => write!(f, "{} q", *style as u8),
Cursor::QueryCursorShape => write!(f, "> q"),
Cursor::CursorShapeQueryResponse(caps) => {
write!(f, ">")?;
for (i, cap) in caps.iter().enumerate() {
if i > 0 {
write!(f, ";")?;
}
write!(f, "{}", *cap as u8)?;
}
write!(f, " q")
}
Cursor::SetMultipleCursors { shape, positions } => {
write!(
f,
">{}",
match shape {
MultiCursorShape::Style(style) => *style as u8,
MultiCursorShape::FollowMainCursor => 29,
}
)?;
for (line, col) in positions {
write!(f, ";2:{}:{}", line, col)?;
}
write!(f, " q")
}
Cursor::ClearSecondaryCursors => write!(f, ">0;4 q"),
}
}
}
#[derive(Debug, Default, Clone, Copy, PartialEq, Eq)]
pub enum CursorTabulationControl {
#[default]
SetCharacterTabStopAtActivePosition = 0,
SetLineTabStopAtActiveLine = 1,
ClearCharacterTabStopAtActivePosition = 2,
ClearLineTabstopAtActiveLine = 3,
ClearAllCharacterTabStopsAtActiveLine = 4,
ClearAllCharacterTabStops = 5,
ClearAllLineTabStops = 6,
}
impl Display for CursorTabulationControl {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
write!(f, "{}", *self as u8)
}
}
#[derive(Debug, Default, Clone, Copy, PartialEq, Eq)]
pub enum TabulationClear {
#[default]
ClearCharacterTabStopAtActivePosition = 0,
ClearLineTabStopAtActiveLine = 1,
ClearCharacterTabStopsAtActiveLine = 2,
ClearAllCharacterTabStops = 3,
ClearAllLineTabStops = 4,
ClearAllTabStops = 5,
}
impl Display for TabulationClear {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
write!(f, "{}", *self as u8)
}
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub enum Edit {
DeleteCharacter(u32),
DeleteLine(u32),
EraseCharacter(u32),
EraseInLine(EraseInLine),
InsertCharacter(u32),
InsertLine(u32),
ScrollDown(u32),
ScrollUp(u32),
EraseInDisplay(EraseInDisplay),
Repeat(u32),
}
impl Display for Edit {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
fn write_csi(param: u32, f: &mut fmt::Formatter<'_>, control: &str) -> fmt::Result {
if param == 1 {
write!(f, "{control}")
} else {
write!(f, "{param}{control}")
}
}
match self {
Self::DeleteCharacter(n) => write_csi(*n, f, "P"),
Self::DeleteLine(n) => write_csi(*n, f, "M"),
Self::EraseCharacter(n) => write_csi(*n, f, "X"),
Self::EraseInLine(n) => write_csi(*n as u32, f, "K"),
Self::InsertCharacter(n) => write_csi(*n, f, "@"),
Self::InsertLine(n) => write_csi(*n, f, "L"),
Self::ScrollDown(n) => write_csi(*n, f, "T"),
Self::ScrollUp(n) => write_csi(*n, f, "S"),
Self::EraseInDisplay(n) => write_csi(*n as u32, f, "J"),
Self::Repeat(n) => write_csi(*n, f, "b"),
}
}
}
#[derive(Debug, Default, Clone, Copy, PartialEq, Eq)]
pub enum EraseInLine {
#[default]
EraseToEndOfLine = 0,
EraseToStartOfLine = 1,
EraseLine = 2,
}
#[derive(Debug, Default, Clone, Copy, PartialEq, Eq)]
pub enum EraseInDisplay {
#[default]
EraseToEndOfDisplay = 0,
EraseToStartOfDisplay = 1,
EraseDisplay = 2,
EraseScrollback = 3,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum Mode {
SetDecPrivateMode(DecPrivateMode),
ResetDecPrivateMode(DecPrivateMode),
SaveDecPrivateMode(DecPrivateMode),
RestoreDecPrivateMode(DecPrivateMode),
QueryDecPrivateMode(DecPrivateMode),
ReportDecPrivateMode {
mode: DecPrivateMode,
setting: DecModeSetting,
},
SetMode(TerminalMode),
ResetMode(TerminalMode),
QueryMode(TerminalMode),
XtermKeyMode {
resource: XtermKeyModifierResource,
value: Option<i64>,
},
QueryTheme,
ReportTheme(ThemeMode),
}
impl Display for Mode {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
match self {
Self::SetDecPrivateMode(mode) => write!(f, "?{mode}h"),
Self::ResetDecPrivateMode(mode) => write!(f, "?{mode}l"),
Self::SaveDecPrivateMode(mode) => write!(f, "?{mode}s"),
Self::RestoreDecPrivateMode(mode) => write!(f, "?{mode}r"),
Self::QueryDecPrivateMode(mode) => write!(f, "?{mode}$p"),
Self::ReportDecPrivateMode { mode, setting } => {
write!(f, "?{mode};{}$y", *setting as u8)
}
Self::SetMode(mode) => write!(f, "{mode}h"),
Self::ResetMode(mode) => write!(f, "{mode}l"),
Self::QueryMode(mode) => write!(f, "?{mode}$p"),
Self::XtermKeyMode { resource, value } => {
write!(f, ">{}", *resource as u8)?;
if let Some(value) = value {
write!(f, ";{}", value)?;
} else {
write!(f, ";")?;
}
write!(f, "m")
}
Self::QueryTheme => write!(f, "?996n"),
Self::ReportTheme(mode) => write!(f, "?997;{}n", *mode as u8),
}
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum DecPrivateMode {
Code(DecPrivateModeCode),
Unspecified(u16),
}
impl Display for DecPrivateMode {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
let code = match *self {
Self::Code(code) => code as u16,
Self::Unspecified(code) => code,
};
write!(f, "{code}")
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum DecPrivateModeCode {
ApplicationCursorKeys = 1,
DecAnsiMode = 2,
Select132Columns = 3,
SmoothScroll = 4,
ReverseVideo = 5,
OriginMode = 6,
AutoWrap = 7,
AutoRepeat = 8,
StartBlinkingCursor = 12,
ShowCursor = 25,
ReverseWraparound = 45,
LeftRightMarginMode = 69,
SixelDisplayMode = 80,
MouseTracking = 1000,
HighlightMouseTracking = 1001,
ButtonEventMouse = 1002,
AnyEventMouse = 1003,
FocusTracking = 1004,
Utf8Mouse = 1005,
SGRMouse = 1006,
RXVTMouse = 1015,
SGRPixelsMouse = 1016,
XTermMetaSendsEscape = 1036,
XTermAltSendsEscape = 1039,
SaveCursor = 1048,
ClearAndEnableAlternateScreen = 1049,
EnableAlternateScreen = 47,
OptEnableAlternateScreen = 1047,
BracketedPaste = 2004,
GraphemeClustering = 2027,
Theme = 2031,
UsePrivateColorRegistersForEachGraphic = 1070,
SynchronizedOutput = 2026,
MinTTYApplicationEscapeKeyMode = 7727,
SixelScrollsRight = 8452,
Win32InputMode = 9001,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum TerminalMode {
Code(TerminalModeCode),
Unspecified(u16),
}
impl Display for TerminalMode {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
let code = match *self {
Self::Code(code) => code as u16,
Self::Unspecified(code) => code,
};
write!(f, "{code}")
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum TerminalModeCode {
KeyboardAction = 2,
Insert = 4,
BiDirectionalSupportMode = 8,
SendReceive = 12,
AutomaticNewline = 20,
ShowCursor = 25,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum XtermKeyModifierResource {
Keyboard = 0,
CursorKeys = 1,
FunctionKeys = 2,
OtherKeys = 4,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum DecModeSetting {
NotRecognized = 0,
Set = 1,
Reset = 2,
PermanentlySet = 3,
PermanentlyReset = 4,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum ThemeMode {
Dark = 1,
Light = 2,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum MouseReport {
Sgr1006 {
x: u16,
y: u16,
button: MouseButton,
modifiers: Modifiers,
},
Sgr1016 {
x_pixels: u16,
y_pixels: u16,
button: MouseButton,
modifiers: Modifiers,
},
}
impl Display for MouseReport {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
match self {
MouseReport::Sgr1006 {
x,
y,
button,
modifiers,
} => {
let mut b = 0;
if (*modifiers & Modifiers::SHIFT) != Modifiers::NONE {
b |= 4;
}
if (*modifiers & Modifiers::ALT) != Modifiers::NONE {
b |= 8;
}
if (*modifiers & Modifiers::CONTROL) != Modifiers::NONE {
b |= 16;
}
b |= match button {
MouseButton::Button1Press | MouseButton::Button1Release => 0,
MouseButton::Button2Press | MouseButton::Button2Release => 1,
MouseButton::Button3Press | MouseButton::Button3Release => 2,
MouseButton::Button4Press | MouseButton::Button4Release => 64,
MouseButton::Button5Press | MouseButton::Button5Release => 65,
MouseButton::Button6Press | MouseButton::Button6Release => 66,
MouseButton::Button7Press | MouseButton::Button7Release => 67,
MouseButton::Button1Drag => 32,
MouseButton::Button2Drag => 33,
MouseButton::Button3Drag => 34,
MouseButton::None => 35,
};
let trailer = match button {
MouseButton::Button1Press
| MouseButton::Button2Press
| MouseButton::Button3Press
| MouseButton::Button4Press
| MouseButton::Button5Press
| MouseButton::Button1Drag
| MouseButton::Button2Drag
| MouseButton::Button3Drag
| MouseButton::None => 'M',
_ => 'm',
};
write!(f, "<{b};{x};{y}{trailer}")
}
MouseReport::Sgr1016 {
x_pixels,
y_pixels,
button,
modifiers,
} => {
let mut b = 0;
if (*modifiers & Modifiers::SHIFT) != Modifiers::NONE {
b |= 4;
}
if (*modifiers & Modifiers::ALT) != Modifiers::NONE {
b |= 8;
}
if (*modifiers & Modifiers::CONTROL) != Modifiers::NONE {
b |= 16;
}
b |= match button {
MouseButton::Button1Press | MouseButton::Button1Release => 0,
MouseButton::Button2Press | MouseButton::Button2Release => 1,
MouseButton::Button3Press | MouseButton::Button3Release => 2,
MouseButton::Button4Press | MouseButton::Button4Release => 64,
MouseButton::Button5Press | MouseButton::Button5Release => 65,
MouseButton::Button6Press | MouseButton::Button6Release => 66,
MouseButton::Button7Press | MouseButton::Button7Release => 67,
MouseButton::Button1Drag => 32,
MouseButton::Button2Drag => 33,
MouseButton::Button3Drag => 34,
MouseButton::None => 35,
};
let trailer = match button {
MouseButton::Button1Press
| MouseButton::Button2Press
| MouseButton::Button3Press
| MouseButton::Button4Press
| MouseButton::Button5Press
| MouseButton::Button1Drag
| MouseButton::Button2Drag
| MouseButton::Button3Drag
| MouseButton::None => 'M',
_ => 'm',
};
write!(f, "<{b};{x_pixels};{y_pixels}{trailer}")
}
}
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum MouseButton {
Button1Press,
Button2Press,
Button3Press,
Button4Press,
Button5Press,
Button6Press,
Button7Press,
Button1Release,
Button2Release,
Button3Release,
Button4Release,
Button5Release,
Button6Release,
Button7Release,
Button1Drag,
Button2Drag,
Button3Drag,
None,
}
bitflags::bitflags! {
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub struct KittyKeyboardFlags: u8 {
const NONE = 0;
const DISAMBIGUATE_ESCAPE_CODES = 1;
const REPORT_EVENT_TYPES = 2;
const REPORT_ALTERNATE_KEYS = 4;
const REPORT_ALL_KEYS_AS_ESCAPE_CODES = 8;
const REPORT_ASSOCIATED_TEXT = 16;
}
}
impl Display for KittyKeyboardFlags {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
write!(f, "{}", self.bits())
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum Keyboard {
QueryFlags,
ReportFlags(KittyKeyboardFlags),
PushFlags(KittyKeyboardFlags),
PopFlags(u8),
SetFlags {
flags: KittyKeyboardFlags,
mode: SetKeyboardFlagsMode,
},
}
impl Display for Keyboard {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
match self {
Self::QueryFlags => write!(f, "?u"),
Self::ReportFlags(flags) => write!(f, "?{flags}u"),
Self::PushFlags(flags) => write!(f, ">{flags}u"),
Self::PopFlags(number) => write!(f, "<{number}u"),
Self::SetFlags { flags, mode } => write!(f, "={flags};{mode}u"),
}
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum SetKeyboardFlagsMode {
AssignAll = 1,
SetSpecified = 2,
ClearSpecified = 3,
}
impl Display for SetKeyboardFlagsMode {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
write!(f, "{}", *self as u8)
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum Device {
DeviceAttributes(()),
SoftReset,
RequestPrimaryDeviceAttributes,
RequestSecondaryDeviceAttributes,
RequestTertiaryDeviceAttributes,
StatusReport,
RequestTerminalNameAndVersion,
RequestTerminalParameters(i64),
}
impl Display for Device {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
match self {
Self::DeviceAttributes(_) => unimplemented!(),
Self::SoftReset => write!(f, "!p"),
Self::RequestPrimaryDeviceAttributes => write!(f, "c"),
Self::RequestSecondaryDeviceAttributes => write!(f, ">c"),
Self::RequestTertiaryDeviceAttributes => write!(f, "=c"),
Self::StatusReport => write!(f, "5n"),
Self::RequestTerminalNameAndVersion => write!(f, ">q"),
Self::RequestTerminalParameters(n) => write!(f, "{};1;1;128;128;1;0x", n + 2),
}
}
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub enum Window {
DeIconify,
Iconify,
MoveWindow {
x: i64,
y: i64,
},
ResizeWindowPixels {
width: Option<i64>,
height: Option<i64>,
},
RaiseWindow,
LowerWindow,
RefreshWindow,
ResizeWindowCells {
width: Option<i64>,
height: Option<i64>,
},
RestoreMaximizedWindow,
MaximizeWindow,
MaximizeWindowVertically,
MaximizeWindowHorizontally,
UndoFullScreenMode,
ChangeToFullScreenMode,
ToggleFullScreen,
ReportWindowState,
ReportWindowPosition,
ReportTextAreaPosition,
ReportTextAreaSizePixels,
ReportWindowSizePixels,
ReportScreenSizePixels,
ReportCellSizePixels,
ReportCellSizePixelsResponse {
width: Option<i64>,
height: Option<i64>,
},
ReportTextAreaSizeCells,
ReportScreenSizeCells,
ReportIconLabel,
ReportWindowTitle,
PushIconAndWindowTitle,
PushIconTitle,
PushWindowTitle,
PopIconAndWindowTitle,
PopIconTitle,
PopWindowTitle,
ChecksumRectangularArea {
request_id: i64,
page_number: i64,
top: OneBased,
left: OneBased,
bottom: OneBased,
right: OneBased,
},
}
impl Display for Window {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
struct NumstrOrEmpty(Option<i64>);
impl Display for NumstrOrEmpty {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
if let Some(x) = self.0 {
write!(f, "{x}")?
}
Ok(())
}
}
match self {
Window::DeIconify => write!(f, "1t"),
Window::Iconify => write!(f, "2t"),
Window::MoveWindow { x, y } => write!(f, "3;{x};{y}t"),
Window::ResizeWindowPixels { width, height } => {
write!(f, "4;{};{}t", NumstrOrEmpty(*height), NumstrOrEmpty(*width))
}
Window::RaiseWindow => write!(f, "5t"),
Window::LowerWindow => write!(f, "6t"),
Window::RefreshWindow => write!(f, "7t"),
Window::ResizeWindowCells { width, height } => {
write!(f, "8;{};{}t", NumstrOrEmpty(*height), NumstrOrEmpty(*width))
}
Window::RestoreMaximizedWindow => write!(f, "9;0t"),
Window::MaximizeWindow => write!(f, "9;1t"),
Window::MaximizeWindowVertically => write!(f, "9;2t"),
Window::MaximizeWindowHorizontally => write!(f, "9;3t"),
Window::UndoFullScreenMode => write!(f, "10;0t"),
Window::ChangeToFullScreenMode => write!(f, "10;1t"),
Window::ToggleFullScreen => write!(f, "10;2t"),
Window::ReportWindowState => write!(f, "11t"),
Window::ReportWindowPosition => write!(f, "13t"),
Window::ReportTextAreaPosition => write!(f, "13;2t"),
Window::ReportTextAreaSizePixels => write!(f, "14t"),
Window::ReportWindowSizePixels => write!(f, "14;2t"),
Window::ReportScreenSizePixels => write!(f, "15t"),
Window::ReportCellSizePixels => write!(f, "16t"),
Window::ReportCellSizePixelsResponse { width, height } => {
write!(f, "6;{};{}t", NumstrOrEmpty(*height), NumstrOrEmpty(*width))
}
Window::ReportTextAreaSizeCells => write!(f, "18t"),
Window::ReportScreenSizeCells => write!(f, "19t"),
Window::ReportIconLabel => write!(f, "20t"),
Window::ReportWindowTitle => write!(f, "21t"),
Window::PushIconAndWindowTitle => write!(f, "22;0t"),
Window::PushIconTitle => write!(f, "22;1t"),
Window::PushWindowTitle => write!(f, "22;2t"),
Window::PopIconAndWindowTitle => write!(f, "23;0t"),
Window::PopIconTitle => write!(f, "23;1t"),
Window::PopWindowTitle => write!(f, "23;2t"),
Window::ChecksumRectangularArea {
request_id,
page_number,
top,
left,
bottom,
right,
} => write!(
f,
"{request_id};{page_number};{top};{left};{bottom};{right}*y"
),
}
}
}
#[cfg(test)]
mod test {
use crate::style::RgbColor;
use super::*;
const ENTER_ALTERNATE_SCREEN: Csi = Csi::Mode(Mode::SetDecPrivateMode(DecPrivateMode::Code(
DecPrivateModeCode::ClearAndEnableAlternateScreen,
)));
const EXIT_ALTERNATE_SCREEN: Csi = Csi::Mode(Mode::ResetDecPrivateMode(DecPrivateMode::Code(
DecPrivateModeCode::ClearAndEnableAlternateScreen,
)));
#[test]
fn encoding() {
assert_eq!("\x1b[?1049h", ENTER_ALTERNATE_SCREEN.to_string());
assert_eq!("\x1b[?1049l", EXIT_ALTERNATE_SCREEN.to_string());
assert_eq!(
"\x1b[>5u",
Csi::Keyboard(Keyboard::PushFlags(
KittyKeyboardFlags::DISAMBIGUATE_ESCAPE_CODES
| KittyKeyboardFlags::REPORT_ALTERNATE_KEYS
))
.to_string()
);
assert_eq!(
"\x1b[32m",
Csi::Sgr(Sgr::Foreground(ColorSpec::GREEN)).to_string(),
);
assert_eq!(
"\x1b[39m",
Csi::Sgr(Sgr::Foreground(ColorSpec::Reset)).to_string(),
);
assert_eq!(
"\x1b[22;0t",
Csi::Window(Box::new(Window::PushIconAndWindowTitle)).to_string(),
);
assert_eq!(
"\x1b[23;0t",
Csi::Window(Box::new(Window::PopIconAndWindowTitle)).to_string(),
);
assert_eq!(
"\x1b[0 q",
Csi::Cursor(Cursor::CursorStyle(CursorStyle::Default)).to_string()
);
}
#[test]
fn sgr_attributes_csi_param_limit() {
let mut attributes = SgrAttributes {
foreground: Some(ColorSpec::TrueColor(RgbColor::new(80, 100, 120).into())),
background: Some(ColorSpec::TrueColor(RgbColor::new(80, 100, 120).into())),
underline_color: Some(ColorSpec::TrueColor(RgbColor::new(80, 100, 120).into())),
modifiers: SgrModifiers::UNDERLINE_CURLY,
..Default::default()
};
let expected = "\x1b[38;2;80;100;120;48;2;80;100;120m\x1b[58:2::80:100:120;4:3m";
assert_eq!(expected, Csi::Sgr(Sgr::Attributes(attributes)).to_string());
attributes.parameter_chunk_size = NonZeroU16::new(12).unwrap();
assert_eq!(expected, Csi::Sgr(Sgr::Attributes(attributes)).to_string());
}
#[test]
fn multi_cursor_encoding() {
assert_eq!(
"\x1b[> q",
Csi::Cursor(Cursor::QueryCursorShape).to_string()
);
assert_eq!(
"\x1b[>1;2;29;100 q",
Csi::Cursor(Cursor::CursorShapeQueryResponse(vec![
MultiCursorCapability::BlockShape,
MultiCursorCapability::BeamShape,
MultiCursorCapability::FollowMainCursorShape,
MultiCursorCapability::QueryCurrentCursors,
]))
.to_string()
);
assert_eq!(
"\x1b[>29;2:1:1;2:2:5 q",
Csi::Cursor(Cursor::SetMultipleCursors {
shape: MultiCursorShape::FollowMainCursor,
positions: vec![
(OneBased::new(1).unwrap(), OneBased::new(1).unwrap()),
(OneBased::new(2).unwrap(), OneBased::new(5).unwrap()),
],
})
.to_string()
);
assert_eq!(
"\x1b[>2;2:3:10 q",
Csi::Cursor(Cursor::SetMultipleCursors {
shape: MultiCursorShape::Style(CursorStyle::SteadyBlock),
positions: vec![(OneBased::new(3).unwrap(), OneBased::new(10).unwrap()),],
})
.to_string()
);
assert_eq!(
"\x1b[>0;4 q",
Csi::Cursor(Cursor::ClearSecondaryCursors).to_string()
);
}
}