use std::io::{self, Write};
use crossterm::{queue, style};
use crate::buffer::Cell;
use crate::style::{Attrs, Color};
pub(crate) fn to_ct(c: Color) -> style::Color {
match c {
Color::Reset => style::Color::Reset,
Color::Black => style::Color::Black,
Color::Red => style::Color::Red,
Color::Green => style::Color::Green,
Color::Yellow => style::Color::Yellow,
Color::Blue => style::Color::Blue,
Color::Magenta => style::Color::Magenta,
Color::Cyan => style::Color::Cyan,
Color::White => style::Color::White,
Color::DarkGrey => style::Color::DarkGrey,
Color::Rgb(r, g, b) => style::Color::Rgb { r, g, b },
Color::Ansi(n) => style::Color::AnsiValue(n),
}
}
pub(crate) fn ct_attrs(a: Attrs) -> style::Attributes {
let mut attrs = style::Attributes::default();
if a.bold {
attrs.set(style::Attribute::Bold);
}
if a.dim {
attrs.set(style::Attribute::Dim);
}
if a.italic {
attrs.set(style::Attribute::Italic);
}
if a.underline {
attrs.set(style::Attribute::Underlined);
}
attrs
}
pub(crate) fn osc52_copy_payload(text: &str) -> String {
format!("\x1b]52;c;{}\x07", base64_encode(text.as_bytes()))
}
fn base64_encode(bytes: &[u8]) -> String {
const ALPHABET: &[u8; 64] = b"ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/";
let mut out = String::with_capacity(bytes.len().div_ceil(3) * 4);
for chunk in bytes.chunks(3) {
let b0 = chunk[0] as u32;
let b1 = *chunk.get(1).unwrap_or(&0) as u32;
let b2 = *chunk.get(2).unwrap_or(&0) as u32;
let n = (b0 << 16) | (b1 << 8) | b2;
out.push(ALPHABET[(n >> 18) as usize & 0x3f] as char);
out.push(ALPHABET[(n >> 12) as usize & 0x3f] as char);
out.push(if chunk.len() > 1 {
ALPHABET[(n >> 6) as usize & 0x3f] as char
} else {
'='
});
out.push(if chunk.len() > 2 {
ALPHABET[n as usize & 0x3f] as char
} else {
'='
});
}
out
}
#[cfg(test)]
pub(crate) fn write_cell(out: &mut impl Write, cell: &Cell) -> io::Result<()> {
queue!(
out,
style::SetAttribute(style::Attribute::Reset),
style::SetAttributes(ct_attrs(cell.attrs)),
style::SetForegroundColor(to_ct(cell.fg)),
style::SetBackgroundColor(to_ct(cell.bg)),
style::Print(cell.ch),
)
}
pub(crate) fn write_row(out: &mut impl Write, cells: &[Cell]) -> io::Result<()> {
let end = cells
.iter()
.rposition(|c| *c != Cell::default())
.map(|i| i + 1)
.unwrap_or(0);
let cells = &cells[..end];
let mut i = 0;
while i < cells.len() {
let start = &cells[i];
let mut run = String::new();
run.push(start.ch);
let mut j = i + 1;
while j < cells.len()
&& cells[j].fg == start.fg
&& cells[j].bg == start.bg
&& cells[j].attrs == start.attrs
{
run.push(cells[j].ch);
j += 1;
}
queue!(
out,
style::SetAttribute(style::Attribute::Reset),
style::SetAttributes(ct_attrs(start.attrs)),
style::SetForegroundColor(to_ct(start.fg)),
style::SetBackgroundColor(to_ct(start.bg)),
style::Print(run),
)?;
i = j;
}
queue!(out, style::SetAttribute(style::Attribute::Reset))
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn to_ct_maps_every_color_variant() {
assert_eq!(to_ct(Color::Reset), style::Color::Reset);
assert_eq!(to_ct(Color::Black), style::Color::Black);
assert_eq!(to_ct(Color::Red), style::Color::Red);
assert_eq!(to_ct(Color::Green), style::Color::Green);
assert_eq!(to_ct(Color::Yellow), style::Color::Yellow);
assert_eq!(to_ct(Color::Blue), style::Color::Blue);
assert_eq!(to_ct(Color::Magenta), style::Color::Magenta);
assert_eq!(to_ct(Color::Cyan), style::Color::Cyan);
assert_eq!(to_ct(Color::White), style::Color::White);
assert_eq!(to_ct(Color::DarkGrey), style::Color::DarkGrey);
assert_eq!(
to_ct(Color::Rgb(1, 2, 3)),
style::Color::Rgb { r: 1, g: 2, b: 3 }
);
assert_eq!(to_ct(Color::Ansi(42)), style::Color::AnsiValue(42));
}
#[test]
fn write_cell_emits_every_attribute() {
let mut out = Vec::new();
write_cell(
&mut out,
&Cell {
ch: 'x',
fg: Color::Red,
bg: Color::Blue,
attrs: Attrs {
bold: true,
dim: true,
italic: true,
underline: true,
},
},
)
.unwrap();
assert!(String::from_utf8(out).unwrap().contains('x'));
}
#[test]
fn write_row_all_blank_emits_only_reset() {
let mut out = Vec::new();
write_row(&mut out, &[Cell::default(), Cell::default()]).unwrap();
assert!(!String::from_utf8(out).unwrap().contains('x'));
}
#[test]
fn write_row_breaks_the_run_on_a_style_change() {
let mut out = Vec::new();
let cells = [
Cell {
ch: 'h',
fg: Color::Red,
..Cell::default()
},
Cell {
ch: 'i',
fg: Color::Blue,
..Cell::default()
},
];
write_row(&mut out, &cells).unwrap();
let s = String::from_utf8(out).unwrap();
assert!(s.contains('h') && s.contains('i'));
assert!(!s.contains("hi"));
let red = format!("{}", style::SetForegroundColor(to_ct(Color::Red)));
let blue = format!("{}", style::SetForegroundColor(to_ct(Color::Blue)));
assert_eq!(s.matches(&red).count(), 1);
assert_eq!(s.matches(&blue).count(), 1);
}
#[test]
fn write_row_trims_trailing_blanks() {
let mut out = Vec::new();
let cells = [
Cell {
ch: 'h',
..Cell::default()
},
Cell {
ch: 'i',
..Cell::default()
},
Cell::default(),
Cell::default(),
];
write_row(&mut out, &cells).unwrap();
let s = String::from_utf8(out).unwrap();
assert!(s.contains('h') && s.contains('i'));
assert_eq!(s.matches('h').count() + s.matches('i').count(), 2);
}
#[test]
fn base64_matches_rfc4648_vectors() {
let vectors: &[(&str, &str)] = &[
("", ""),
("f", "Zg=="),
("fo", "Zm8="),
("foo", "Zm9v"),
("foob", "Zm9vYg=="),
("fooba", "Zm9vYmE="),
("foobar", "Zm9vYmFy"),
];
for (input, expected) in vectors {
assert_eq!(
base64_encode(input.as_bytes()),
*expected,
"input {input:?}"
);
}
}
#[test]
fn base64_handles_non_utf8_bytes() {
assert_eq!(base64_encode(&[0xff, 0xfe, 0xfd]), "//79");
}
#[test]
fn osc52_payload_wraps_base64_in_the_escape_sequence() {
assert_eq!(osc52_copy_payload("foo"), "\x1b]52;c;Zm9v\x07");
assert_eq!(osc52_copy_payload(""), "\x1b]52;c;\x07");
}
}