use std::io;
use vt100::Color;
#[cfg(feature = "backend-termina")]
mod termina;
#[cfg(feature = "backend-termina")]
pub use self::termina::TerminaBackend;
#[cfg(feature = "backend-crossterm")]
mod crossterm;
#[cfg(feature = "backend-crossterm")]
pub use self::crossterm::CrosstermBackend;
#[cfg(feature = "backend-qwertty")]
mod qwertty;
#[cfg(feature = "backend-qwertty")]
pub use self::qwertty::QwerttyBackend;
#[cfg(feature = "backend-termina")]
pub type DefaultBackend = TerminaBackend;
#[cfg(all(feature = "backend-crossterm", not(feature = "backend-termina")))]
pub type DefaultBackend = CrosstermBackend;
#[cfg(all(
feature = "backend-qwertty",
not(feature = "backend-termina"),
not(feature = "backend-crossterm")
))]
pub type DefaultBackend = QwerttyBackend;
#[cfg(not(any(
feature = "backend-termina",
feature = "backend-crossterm",
feature = "backend-qwertty"
)))]
compile_error!(
"koh's client needs a terminal backend: enable `backend-termina` (default), `backend-crossterm`, or `backend-qwertty`"
);
pub(crate) const RESET_FORWARDED_MODES: &[u8] =
b"\x1b[?9l\x1b[?2004l\x1b[?1000l\x1b[?1002l\x1b[?1003l\x1b[?1005l\x1b[?1006l\x1b[?1l\x1b>";
#[derive(PartialEq, Eq, Clone, Copy)]
pub struct CellStyle {
pub fg: Color,
pub bg: Color,
pub bold: bool,
pub dim: bool,
pub italic: bool,
pub underline: bool,
pub inverse: bool,
}
pub trait KohBackend {
fn write_bytes(&mut self, bytes: &[u8]) -> io::Result<()>;
fn flush(&mut self) -> io::Result<()>;
fn enter_raw_mode(&mut self) -> io::Result<()>;
fn leave_raw_mode(&mut self) -> io::Result<()>;
fn size(&self) -> io::Result<(u16, u16)>;
fn enter_alt_screen(&mut self) -> io::Result<()> {
self.write_bytes(b"\x1b[?1049h\x1b[?25l")?;
self.flush()
}
fn leave_alt_screen(&mut self) -> io::Result<()> {
self.write_bytes(RESET_FORWARDED_MODES)?;
self.write_bytes(b"\x1b[?25h\x1b[?1049l")?;
self.flush()
}
fn begin_frame(&mut self) -> io::Result<()> {
self.write_bytes(b"\x1b[?2026h\x1b[?25l")
}
fn end_frame(&mut self) -> io::Result<()> {
self.write_bytes(b"\x1b[?2026l")
}
fn move_to(&mut self, row: u16, col: u16) -> io::Result<()> {
self.write_bytes(format!("\x1b[{};{}H", u32::from(row) + 1, u32::from(col) + 1).as_bytes())
}
fn set_style(&mut self, style: CellStyle) -> io::Result<()> {
self.reset_sgr()?; if style.bold {
self.write_bytes(b"\x1b[1m")?;
}
if style.dim {
self.write_bytes(b"\x1b[2m")?;
}
if style.italic {
self.write_bytes(b"\x1b[3m")?;
}
if style.underline {
self.write_bytes(b"\x1b[4m")?;
}
if style.inverse {
self.write_bytes(b"\x1b[7m")?;
}
write_sgr_color(self, style.fg, true)?;
write_sgr_color(self, style.bg, false)
}
fn reset_sgr(&mut self) -> io::Result<()> {
self.write_bytes(b"\x1b[m")
}
fn set_reverse(&mut self) -> io::Result<()> {
self.write_bytes(b"\x1b[7m")
}
fn print(&mut self, glyph: &str) -> io::Result<()> {
self.write_bytes(glyph.as_bytes())
}
fn show_cursor(&mut self) -> io::Result<()> {
self.write_bytes(b"\x1b[?25h")
}
fn set_window_title(&mut self, title: &str) -> io::Result<()> {
self.write_bytes(format!("\x1b]0;{title}\x07").as_bytes())
}
fn set_window_icon_and_title(&mut self, icon: &str, title: &str) -> io::Result<()> {
self.write_bytes(format!("\x1b]1;{icon}\x07\x1b]2;{title}\x07").as_bytes())
}
fn set_clipboard(&mut self, base64: &str) -> io::Result<()> {
self.write_bytes(format!("\x1b]52;c;{base64}\x07").as_bytes())
}
fn bell(&mut self) -> io::Result<()> {
self.write_bytes(b"\x07")
}
fn write_input_modes(&mut self, bytes: &[u8]) -> io::Result<()> {
self.write_bytes(bytes)
}
}
fn write_sgr_color(out: &mut (impl KohBackend + ?Sized), color: Color, fg: bool) -> io::Result<()> {
match color {
Color::Default => out.write_bytes(if fg { b"\x1b[39m" } else { b"\x1b[49m" }),
Color::Idx(i) if i < 8 => {
let base: u16 = if fg { 30 } else { 40 };
out.write_bytes(format!("\x1b[{}m", base + u16::from(i)).as_bytes())
}
Color::Idx(i) if i < 16 => {
let base: u16 = if fg { 82 } else { 92 };
out.write_bytes(format!("\x1b[{}m", base + u16::from(i)).as_bytes())
}
Color::Idx(i) => {
let lead = if fg { 38 } else { 48 };
out.write_bytes(format!("\x1b[{lead};5;{i}m").as_bytes())
}
Color::Rgb(r, g, b) => {
let lead = if fg { 38 } else { 48 };
out.write_bytes(format!("\x1b[{lead};2;{r};{g};{b}m").as_bytes())
}
}
}
#[cfg(test)]
#[derive(Default)]
pub(crate) struct CaptureBackend {
pub bytes: Vec<u8>,
}
#[cfg(test)]
impl KohBackend for CaptureBackend {
fn write_bytes(&mut self, bytes: &[u8]) -> io::Result<()> {
self.bytes.extend_from_slice(bytes);
Ok(())
}
fn flush(&mut self) -> io::Result<()> {
Ok(())
}
fn enter_raw_mode(&mut self) -> io::Result<()> {
Ok(())
}
fn leave_raw_mode(&mut self) -> io::Result<()> {
Ok(())
}
fn size(&self) -> io::Result<(u16, u16)> {
Ok((24, 80))
}
}
#[cfg(test)]
mod tests {
use super::*;
fn emit(f: impl FnOnce(&mut CaptureBackend) -> io::Result<()>) -> Vec<u8> {
let mut b = CaptureBackend::default();
f(&mut b).expect("capture backend never errors");
b.bytes
}
#[test]
fn sgr_colors_match_the_classic_theme_aware_codes() {
assert_eq!(
emit(|b| write_sgr_color(b, Color::Idx(1), true)),
b"\x1b[31m"
);
assert_eq!(
emit(|b| write_sgr_color(b, Color::Idx(7), true)),
b"\x1b[37m"
);
assert_eq!(
emit(|b| write_sgr_color(b, Color::Idx(8), true)),
b"\x1b[90m"
);
assert_eq!(
emit(|b| write_sgr_color(b, Color::Idx(15), true)),
b"\x1b[97m"
);
assert_eq!(
emit(|b| write_sgr_color(b, Color::Idx(196), true)),
b"\x1b[38;5;196m"
);
assert_eq!(
emit(|b| write_sgr_color(b, Color::Rgb(0, 0, 255), true)),
b"\x1b[38;2;0;0;255m"
);
assert_eq!(
emit(|b| write_sgr_color(b, Color::Default, true)),
b"\x1b[39m"
);
assert_eq!(
emit(|b| write_sgr_color(b, Color::Idx(1), false)),
b"\x1b[41m"
);
assert_eq!(
emit(|b| write_sgr_color(b, Color::Idx(8), false)),
b"\x1b[100m"
);
assert_eq!(
emit(|b| write_sgr_color(b, Color::Idx(196), false)),
b"\x1b[48;5;196m"
);
assert_eq!(
emit(|b| write_sgr_color(b, Color::Default, false)),
b"\x1b[49m"
);
}
#[test]
fn move_to_is_one_based_and_overflow_safe() {
assert_eq!(emit(|b| b.move_to(0, 0)), b"\x1b[1;1H");
assert_eq!(emit(|b| b.move_to(23, 79)), b"\x1b[24;80H");
assert_eq!(emit(|b| b.move_to(u16::MAX, 0)), b"\x1b[65536;1H");
}
#[test]
fn frame_and_screen_control_bytes() {
assert_eq!(emit(KohBackend::begin_frame), b"\x1b[?2026h\x1b[?25l");
assert_eq!(emit(KohBackend::end_frame), b"\x1b[?2026l");
assert_eq!(emit(KohBackend::enter_alt_screen), b"\x1b[?1049h\x1b[?25l");
let bytes = emit(KohBackend::leave_alt_screen);
assert!(bytes.starts_with(RESET_FORWARDED_MODES));
assert!(bytes.ends_with(b"\x1b[?25h\x1b[?1049l"));
}
#[test]
fn out_of_band_escapes() {
assert_eq!(emit(|b| b.set_window_title("x")), b"\x1b]0;x\x07");
assert_eq!(
emit(|b| b.set_window_icon_and_title("i", "t")),
b"\x1b]1;i\x07\x1b]2;t\x07"
);
assert_eq!(emit(|b| b.set_clipboard("aGk=")), b"\x1b]52;c;aGk=\x07");
assert_eq!(emit(KohBackend::bell), b"\x07");
}
}