use std::{convert::TryFrom, fmt, io::{self, BufWriter, Error, Write}, os::unix::io::{AsRawFd, RawFd}, path::{Path, PathBuf}, time::{Duration, Instant}}; trait IsMinusOne { fn is_minus_one(&self) -> bool; } macro_rules! impl_is_minus_one { ($($t:ident)*) => ($(impl IsMinusOne for $t { fn is_minus_one(&self) -> bool { *self == -1 } })*) } impl_is_minus_one! { i8 i16 i32 i64 isize } /** Convenience syscall wrapper based on its namesake found in the sadly private ``std::sys::unix`` library. */ fn cvt(t: T) -> io::Result { if t.is_minus_one() { Err(Error::last_os_error()) } else { Ok(t) } } /** Convenience syscall wrapper based on its namesake found in the sadly private ``std::sys::unix`` library. */ fn cvt_r(f: &mut dyn FnMut() -> T) -> io::Result { loop { match cvt((*f)()) { Err(ref e) if e.kind() == io::ErrorKind::Interrupted => {}, other => return other, } } } /** Wrappers for ``ioctl_console(2)`` functionality. */ pub mod ioctl { use super::{cvt_r, KbLedState, Palette}; use libc::ioctl; use std::{io::Result, os::unix::io::AsRawFd}; /* XXX: can we get these into ``libc``? */ pub const KDGKBTYPE: libc::c_ulong = 0x4b33; /* kd.h */ pub const GIO_CMAP: libc::c_ulong = 0x00004B70; /* kd.h */ pub const PIO_CMAP: libc::c_ulong = 0x00004B71; /* kd.h */ pub const KB_101: libc::c_char = 0x0002; /* kd.h */ pub const KDGETLED: libc::c_ulong = 0x4b31; /* kd.h */ pub const KDSETLED: libc::c_ulong = 0x4b32; /* kd.h */ pub fn kdgkbtype(fd: &F) -> Result { let mut kb: libc::c_char = 0; let _ = cvt_r(&mut || unsafe { ioctl(fd.as_raw_fd(), KDGKBTYPE, &mut kb as *mut _) })?; Ok(kb) } pub fn pio_cmap(fd: &F, pal: &Palette) -> Result<()> { /* cvt_r because technically it can’t be ruled out that we hit EINTR. */ cvt_r(&mut || { unsafe { ioctl( fd.as_raw_fd(), PIO_CMAP, std::mem::transmute::<&Palette, *const libc::c_void>(&pal), ) } }) .map(|_| ()) } pub fn gio_cmap(fd: &F) -> Result { let mut pal = Palette::new(); /* cvt_r because technically it can’t be ruled out that we hit EINTR. */ cvt_r(&mut || { unsafe { ioctl( fd.as_raw_fd(), GIO_CMAP, std::mem::transmute::<&mut Palette, *mut libc::c_void>( &mut pal, ), ) } }) .map(|_| ())?; Ok(pal) } pub fn kdgetled(fd: &F) -> Result { let mut leds: libc::c_char = 0; cvt_r(&mut || { unsafe { ioctl( fd.as_raw_fd(), KDGETLED, std::mem::transmute::<&mut libc::c_char, *mut libc::c_void>( &mut leds, ), ) } }) .map(|_| ())?; Ok(KbLedState::from(leds)) } /** If ``state`` is ``None`` it is taken to mean “revert to normal” as per the man page: KDSETLED Set the LEDs. The LEDs are set to correspond to the lower three bits of the unsigned long integer in argp. However, if a higher order bit is set, the LEDs revert to normal: displaying the state of the keyboard functions of caps lock, num lock, and scroll lock. */ pub fn kdsetled(fd: &F, state: Option) -> Result<()> { let leds: libc::c_char = if let Some(state) = state { state.into() } else { libc::c_char::MAX }; cvt_r(&mut || { unsafe { ioctl( fd.as_raw_fd(), KDSETLED, std::mem::transmute::<&libc::c_char, *const libc::c_void>( &leds, ), ) } }) .map(|_| ())?; Ok(()) } } #[derive(Clone, Copy, Debug)] pub struct KbLedState(u8); impl KbLedState { pub fn new(cap: bool, num: bool, scr: bool) -> Self { let mut state = 0u8; state |= (cap as u8) << 2; state |= (num as u8) << 1; state |= scr as u8; Self(state) } #[inline] pub fn get(con: &Console) -> io::Result { ioctl::kdgetled(con) } #[inline] pub fn set(&self, con: &Console) -> io::Result<()> { ioctl::kdsetled(con, Some(*self)) } #[inline] pub fn revert(con: &Console) -> io::Result<()> { ioctl::kdsetled(con, None) } #[inline] pub fn cap(&self) -> bool { (self.0 & 0x4) != 0 } #[inline] pub fn num(&self) -> bool { (self.0 & 0x2) != 0 } #[inline] pub fn scr(&self) -> bool { (self.0 & 0x1) != 0 } #[inline] pub fn set_cap(&mut self, set: bool) { let bit = (set as u8) << 2; self.0 = (self.0 & !bit) | bit; } #[inline] pub fn set_num(&mut self, set: bool) { let bit = (set as u8) << 1; self.0 = (self.0 & !bit) | bit; } #[inline] pub fn set_scr(&mut self, set: bool) { let bit = set as u8; self.0 = (self.0 & !bit) | bit; } } impl fmt::Display for KbLedState { fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { write!( f, "caps: {}, num: {}, scroll: {}", self.cap(), self.num(), self.scr() ) } } impl From for KbLedState { fn from(leds: libc::c_char) -> Self { Self::new(leds & 0x4 != 0, leds & 0x2 != 0, leds & 0x1 != 0) } } impl From for libc::c_char { fn from(state: KbLedState) -> Self { state.0 as libc::c_char } } impl From for u8 { fn from(state: KbLedState) -> Self { state.0 } } impl TryFrom for KbLedState { type Error = io::Error; fn try_from(val: u8) -> io::Result { if val <= 0b111 { Ok(Self(val)) } else { Err(io::Error::new( io::ErrorKind::Other, format!( "invalid raw led value: {:#b}; must not exceed 3 b", val ), )) } } } #[cfg(test)] mod kb_led_state { use super::KbLedState; #[test] fn create() { assert_eq!(0u8, KbLedState::new(false, false, false).into()); assert_eq!(1u8, KbLedState::new(false, false, true).into()); assert_eq!(2u8, KbLedState::new(false, true, false).into()); assert_eq!(4u8, KbLedState::new(true, false, false).into()); assert_eq!(6u8, KbLedState::new(true, true, false).into()); assert_eq!(0u8, KbLedState::from(0u8 as libc::c_char).into()); assert_eq!(1u8, KbLedState::from(1u8 as libc::c_char).into()); assert_eq!(2u8, KbLedState::from(2u8 as libc::c_char).into()); assert_eq!(4u8, KbLedState::from(4u8 as libc::c_char).into()); assert_eq!(6u8, KbLedState::from(6u8 as libc::c_char).into()); } } #[derive(Debug)] pub struct Fd(libc::c_int); impl From for Fd { fn from(fd: libc::c_int) -> Self { Self(fd) } } impl AsRawFd for Fd { fn as_raw_fd(&self) -> RawFd { self.0 } } impl fmt::Display for Fd { fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { write!(f, "Fd({})", self.0) } } const PALETTE_SIZE: usize = 16; const PALETTE_BYTES: usize = PALETTE_SIZE * 3; /* 16 * sizeof(int) */ const RAW_COLEXPR_SIZE: usize = 6; /* e. g. 0xBADF00 */ pub type RawPalette = [u32; PALETTE_SIZE]; #[derive(Debug)] pub enum Color { Black(bool), Red(bool), Green(bool), Yellow(bool), Blue(bool), Magenta(bool), Cyan(bool), White(bool), } impl TryFrom for Color { type Error = io::Error; fn try_from(val: u8) -> io::Result { match val { 0x00 => Ok(Color::Black(false)), 0x01 => Ok(Color::Red(false)), 0x02 => Ok(Color::Green(false)), 0x03 => Ok(Color::Yellow(false)), 0x04 => Ok(Color::Blue(false)), 0x05 => Ok(Color::Magenta(false)), 0x06 => Ok(Color::Cyan(false)), 0x07 => Ok(Color::White(false)), 0x08 => Ok(Color::Black(true)), 0x09 => Ok(Color::Red(true)), 0x0a => Ok(Color::Green(true)), 0x0b => Ok(Color::Yellow(true)), 0x0c => Ok(Color::Blue(true)), 0x0d => Ok(Color::Magenta(true)), 0x0e => Ok(Color::Cyan(true)), 0x0f => Ok(Color::White(true)), _ => Err(io::Error::new( io::ErrorKind::Other, format!("invalid color value: {}", val), )), } } } /* [impl TryFrom for Color] */ impl Color { fn format_brightness(b: bool, s: &str) -> String { if b { "bright ".to_string() + s } else { s.to_string() } } } /* [impl Color] */ impl fmt::Display for Color { fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { let c = match *self { Color::Black(b) => Color::format_brightness(b, "black"), Color::Red(b) => Color::format_brightness(b, "red"), Color::Green(b) => Color::format_brightness(b, "green"), Color::Yellow(b) => Color::format_brightness(b, "yellow"), Color::Blue(b) => Color::format_brightness(b, "blue"), Color::Magenta(b) => Color::format_brightness(b, "magenta"), Color::Cyan(b) => Color::format_brightness(b, "cyan"), Color::White(b) => Color::format_brightness(b, "white"), }; write!(f, "{}", c) } } /* [impl fmt::Display for Color] */ #[derive(Debug, Clone)] pub struct Builtin { names: &'static [&'static str], palette: &'static RawPalette, } /** Vanilla Linux colors. */ const DEFAULT_COLORS: RawPalette = [ 0x000000, 0xaa0000, 0x00aa00, 0xaa5500, 0x0000aa, 0xaa00aa, 0x00aaaa, 0xaaaaaa, 0x555555, 0xff5555, 0x55ff55, 0xffff55, 0x5555ff, 0xff55ff, 0x55ffff, 0xffffff, ]; /** The dark (default) version of the Solarized scheme. */ const SOLARIZED_COLORS_DARK: RawPalette = [ 0x002b36, 0xdc322f, 0x859900, 0xb58900, 0x268bd2, 0xd33682, 0x2aa198, 0xeee8d5, 0x002b36, 0xcb4b16, 0x586e75, 0x657b83, 0x839496, 0x6c71c4, 0x93a1a1, 0xfdf6e3, ]; /** The light version of the Solarized theme. */ const SOLARIZED_COLORS_LIGHT: RawPalette = [ 0xeee8d5, 0xdc322f, 0x859900, 0xb58900, 0x268bd2, 0xd33682, 0x2aa198, 0x073642, 0xfdf6e3, 0xcb4b16, 0x93a1a1, 0x839496, 0x657b83, 0x6c71c4, 0x586e75, 0x002b36, ]; /** Bright green monochrome terminal. */ const MONOCHROME_PHOSPHOR: RawPalette = [ 0x000000, 0x68fc68, 0x68fc68, 0x68fc68, 0x68fc68, 0x68fc68, 0x68fc68, 0x68fc68, 0x68fc68, 0x68fc68, 0x68fc68, 0x68fc68, 0x68fc68, 0x68fc68, 0x68fc68, 0x68fc68, ]; const DUMMY_COLORS: RawPalette = [ 0x000000, 0xffffff, 0x000000, 0xffffff, 0x000000, 0xffffff, 0x000000, 0xffffff, 0x000000, 0xffffff, 0x000000, 0xffffff, 0x000000, 0xffffff, 0x000000, 0xffffff, ]; pub const BUILTIN_SCHEMES: &[Builtin] = &[ Builtin::solarized(), Builtin::solarized_light(), Builtin::default(), Builtin::phosphor(), ]; impl Builtin { pub fn name(&self) -> &'static str { self.names.iter().next().unwrap() } pub fn palette(&self) -> &RawPalette { self.palette } const fn solarized() -> Self { Self { names: &["solarized", "solarized_dark", "sd"], palette: &SOLARIZED_COLORS_DARK, } } const fn solarized_light() -> Self { Self { names: &["solarized_light", "sl"], palette: &SOLARIZED_COLORS_LIGHT, } } const fn default() -> Self { Self { names: &["default", "normal", "linux"], palette: &DEFAULT_COLORS, } } const fn phosphor() -> Self { Self { names: &["phosphor", "matrix"], palette: &MONOCHROME_PHOSPHOR } } } impl TryFrom<&str> for Builtin { type Error = io::Error; fn try_from(name: &str) -> Result { for b in BUILTIN_SCHEMES { if b.names.contains(&name) { return Ok(b.clone()); } } Err(io::Error::new( io::ErrorKind::Other, format!("no such builtin scheme: {}", name), )) } } impl<'a> fmt::Display for Builtin { fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { write!(f, "{}", self.name()) } } #[derive(Debug)] pub enum Scheme { /** One of the predefined schemes. */ Builtin(Builtin), /** Custom ``Palette``. */ Palette(Palette), /** Load from file. */ Custom(Option), } impl fmt::Display for Scheme { fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { match self { Self::Builtin(b) => write!(f, "{}", b), Self::Custom(None) => write!(f, ""), Self::Custom(Some(fname)) => write!(f, "{}", fname.display()), Self::Palette(pal) => write!(f, "palette: {}", pal), } } } /* [impl fmt::String for Scheme] */ impl Scheme { pub fn from_stdin() -> Self { Self::Custom(None) } pub fn from_path>(path: P) -> Self { Self::Custom(Some(path.as_ref().into())) } pub fn base64(&self) -> io::Result { let pal = Palette::try_from(self)?; Ok(base64::encode(&pal.0)) } pub fn from_base64(b64: &str) -> io::Result { base64::decode(b64.as_bytes()) .map_err(|e| { io::Error::new( io::ErrorKind::Other, format!("failed to decode input as base64: {}", e), ) }) .and_then(|b| Palette::from_bytes(&b)) .map(Self::from) } } /* [impl Scheme] */ /** Try to select one of the predefined schemes; if that fails, interpret the argument as a path. */ impl From<&str> for Scheme { fn from(name: &str) -> Scheme { Builtin::try_from(name) .map(Self::Builtin) .unwrap_or_else(|_| Self::from_path(name)) } } /** Try to match the palette against one of the predefined schemes. */ impl From for Scheme { fn from(pal: Palette) -> Scheme { if pal == Palette::from(&DEFAULT_COLORS) { return Self::Builtin(Builtin::default()); } if pal == Palette::from(&SOLARIZED_COLORS_DARK) { return Self::Builtin(Builtin::solarized()); } if pal == Palette::from(&SOLARIZED_COLORS_LIGHT) { return Self::Builtin(Builtin::solarized_light()); } if pal == Palette::from(&MONOCHROME_PHOSPHOR) { return Self::Builtin(Builtin::phosphor()); } Self::Palette(pal) } } #[inline] fn nibble_of_char(chr: u8) -> io::Result { match chr { b'0'..=b'9' => Ok(chr - b'0'), b'a'..=b'f' => Ok(chr - b'a' + 10), b'A'..=b'F' => Ok(chr - b'A' + 10), _ => Err(io::Error::new( io::ErrorKind::Other, format!("junk input ‘{}’ does not represent a hex digit", chr,), )), } } macro_rules! byte_of_hex { ($ar:ident, $off:expr) => { (nibble_of_char($ar[$off])? << 4 | nibble_of_char($ar[$off + 1])?) as u8 }; } struct Rgb(u8, u8, u8); impl Rgb { fn r(&self) -> u8 { self.0 } fn g(&self) -> u8 { self.1 } fn b(&self) -> u8 { self.2 } } impl TryFrom<&[u8; 6]> for Rgb { type Error = io::Error; fn try_from(hex: &[u8; RAW_COLEXPR_SIZE]) -> io::Result { let r: u8 = byte_of_hex!(hex, 0); let g: u8 = byte_of_hex!(hex, 2); let b: u8 = byte_of_hex!(hex, 4); Ok(Self(r, g, b)) } } impl From for Rgb { fn from(rgb: u32) -> Self { let b: u8 = (rgb & 0xff) as u8; let g: u8 = ((rgb >> 8) & 0xff) as u8; let r: u8 = ((rgb >> 16) & 0xff) as u8; Self(r, g, b) } } #[derive(Eq, PartialEq, Clone)] pub struct Palette([u8; PALETTE_BYTES]); impl Palette { /** Construct an all-zero ``Palette``. */ pub fn new() -> Self { Self([0u8; PALETTE_BYTES]) } pub fn dummy() -> Self { Self::from(&DUMMY_COLORS) } pub fn from_buffered_reader( reader: &mut dyn std::io::BufRead, ) -> io::Result { let mut pal_idx: usize = 0; let mut pal: [u8; PALETTE_BYTES] = [0; PALETTE_BYTES]; let mut line: String = String::new(); let mut col: [u8; RAW_COLEXPR_SIZE] = [0; RAW_COLEXPR_SIZE]; while reader.read_line(&mut line).is_ok() { let len = line.len(); if len == 0 { break; } else if len >= 8 { if let Some(off) = line.find('#') { if off != 0 { /* Palette index specified, number prepended */ let parse_res: Result = std::str::FromStr::from_str(&line[0..off]); if let Ok(new_idx) = parse_res { if new_idx < PALETTE_SIZE { pal_idx = new_idx * 3; } } } let off = off + 1; if off > len - 6 { /* no room left for color definition after '#' char */ return Err(io::Error::new( io::ErrorKind::Other, format!("invalid color definition: {}", line), )); } col.copy_from_slice( &line.as_bytes()[off..(off + RAW_COLEXPR_SIZE)], ); let rgb = Rgb::try_from(&col)?; pal[pal_idx] = rgb.r(); pal[pal_idx + 1] = rgb.g(); pal[pal_idx + 2] = rgb.b(); pal_idx = (pal_idx + 3) % PALETTE_BYTES; } } line.truncate(0); } Ok(Self(pal)) } #[allow(rustdoc::invalid_rust_codeblocks)] /** Print palette in a text format that can be re-read back in. Basically we print one hex rgb code per line prefixed with color indices and the canonical names on the right: 00 #002B36 black 01 #DC322F red 02 #859900 green 03 #B58900 yellow 04 #268BD2 blue … */ pub fn dump(&self, out: &mut BufWriter) -> io::Result<()> { let mut buf: [u8; 3] = [0u8, 0u8, 0u8]; for (i, col) in self.0.iter().enumerate() { let idx: usize = i % 3; buf[idx] = *col; if idx == 2 { let col = Color::try_from((i / 3) as u8)?; out.write_all( format!( "{:02} #{:02.X}{:02.X}{:02.X} {}\n", i / 3, buf[0], buf[1], buf[2], col, ) .as_bytes(), )?; } } Ok(()) } pub fn from_file(fname: &Path) -> io::Result { let file = std::fs::File::open(&fname).map_err(|e| { io::Error::new( io::ErrorKind::Other, format!( "failed to open palette specification {}: {}", fname.display(), e ), ) })?; let mut reader = std::io::BufReader::new(file); Self::from_buffered_reader(&mut reader) } /* [Palette::from_file] */ pub fn from_stdin() -> io::Result { let mut reader = std::io::BufReader::new(std::io::stdin()); /* Parse scheme file */ Self::from_buffered_reader(&mut reader) } fn from_bytes(b: &[u8]) -> io::Result { if b.len() != PALETTE_SIZE * 3 { return Err(io::Error::new( io::ErrorKind::Other, format!( "expected {} B of data, got {}", PALETTE_SIZE * 3, b.len() ), )); } let mut res = Self::new(); res.0.copy_from_slice(&b); Ok(res) } /* [Palette::from_stdin] */ } /* [impl Palette] */ impl fmt::Display for Palette { fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { let mut i = 0; while i < PALETTE_BYTES { let _ = write!(f, "{}", if i == 0 { "(" } else { "\n " }); let r = self.0[i]; let g = self.0[i + 1]; let b = self.0[i + 2]; let _ = write!( f, "((r 0x{:02.X}) (g 0x{:02.X}) (b 0x{:02.x}))", r, g, b ); i += 3; } writeln!(f, ")") } } /* [impl fmt::Display for Palette] */ impl fmt::Debug for Palette { fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { let mut i: u8 = 0_u8; while (i as usize) < PALETTE_BYTES { let r = self.0[i as usize]; let g = self.0[i as usize + 1]; let b = self.0[i as usize + 2]; let col = Color::try_from((i / 3) as u8) .map(|c| format!("{}", c)) .unwrap_or_else(|_| "??".into()); let _ = writeln!(f, "{} => 0x{:02.X}{:02.X}{:02.X}", col, r, g, b); i += 3_u8; } std::result::Result::Ok(()) } } /* [impl fmt::Debug for Palette] */ /** Obtain a ``Palette`` from a ``Scheme``. */ impl TryFrom<&Scheme> for Palette { type Error = io::Error; fn try_from(scm: &Scheme) -> io::Result { match scm { Scheme::Builtin(Builtin { palette, .. }) => Ok(Self::from(*palette)), Scheme::Custom(None) => Self::from_stdin(), Scheme::Custom(Some(ref fname)) => Self::from_file(fname), Scheme::Palette(pal) => Ok(pal.clone()), } } } /** Obtain a ``Palette`` from a ``RawPalette``. */ impl From<&RawPalette> for Palette { fn from(colors: &RawPalette) -> Self { let mut idx: usize = 0; let mut pal: [u8; PALETTE_BYTES] = [0; PALETTE_BYTES]; for &def in colors.iter() { let rgb = Rgb::from(def); pal[idx] = rgb.r(); pal[idx + 1] = rgb.g(); pal[idx + 2] = rgb.b(); //println!(">> {} -> {:X} {:X} {:X}", def, r, g, b); idx += 3; } Self(pal) } } const CONSOLE_PATHS: [&str; 6] = [ "/proc/self/fd/0", "/dev/tty", "/dev/tty0", "/dev/vc/0", "/dev/systty", "/dev/console", ]; const CONTROL_CLEAR: &[u8] = b"\x1b[2J"; const CONTROL_CURSOR: &[u8] = b"\x1b[1;1H"; pub struct Console(libc::c_int); impl Console { fn from_fd(fd: libc::c_int) -> io::Result { if unsafe { libc::isatty(fd) } == 0 { return Err(Error::last_os_error()); } let fd = Self(fd); /* Sanity check. */ if ioctl::kdgkbtype(&fd)? != ioctl::KB_101 { return Err(io::Error::new( io::ErrorKind::Other, format!( "console {} exhibiting weird behavior; bailing out", fd ), )); } Ok(fd) } fn from_path>(path: P) -> io::Result { let p = std::ffi::CString::new(path.as_ref().to_str().unwrap()).unwrap(); let fd = cvt_r(&mut || unsafe { libc::open(p.as_ptr(), libc::O_RDWR | libc::O_NOCTTY, 0) })?; Self::from_fd(fd) } /** Try and obtain a handle referring to the console we’re running in. */ pub fn current() -> io::Result { for path in CONSOLE_PATHS.iter() { let path = std::path::Path::new(path); if let Ok(con) = Self::from_path(path) { return Ok(con); } } Err(io::Error::new( io::ErrorKind::Other, format!("could not retrieve fd for any of the search paths"), )) } fn write(&self, buf: &[u8]) -> io::Result<()> { let len = buf.len() as libc::size_t; if cvt_r(&mut || unsafe { libc::write(self.0, buf.as_ptr() as *const libc::c_void, len) })? != len as isize { Err(Error::last_os_error()) } else { Ok(()) } } pub fn clear(&self) -> io::Result<()> { self.write(CONTROL_CLEAR)?; self.write(CONTROL_CURSOR)?; Ok(()) } /** Read the current palette. */ #[inline] pub fn current_palette(&self) -> io::Result { ioctl::gio_cmap(&self.as_raw_fd()) } /** Tell the kernel to use the specified palette on the console. */ #[inline] pub fn apply_palette(&self, pal: &Palette) -> io::Result<()> { ioctl::pio_cmap(&self.as_raw_fd(), pal) } /** Read the current palette and determine the scheme. */ #[inline] pub fn current_scheme(&self) -> io::Result { Ok(Scheme::from(self.current_palette()?)) } /** Convert ``scm`` into a ``Palette`` and tell the kernel to use it. */ #[inline] pub fn apply_scheme(&self, scm: &Scheme) -> io::Result<()> { self.apply_palette(&Palette::try_from(scm)?) } } impl AsRawFd for Console { fn as_raw_fd(&self) -> RawFd { self.0 } } impl fmt::Display for Console { fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { write!(f, "Console(fd={})", self.0) } } /* Use doubles for fractional scaling. */ struct FadePalette([f64; PALETTE_SIZE * 3]); impl FadePalette { fn new() -> Self { Self([0f64; PALETTE_SIZE * 3]) } /** Linear interpolation between self and ``goal``. We scale each byte individually so there can be a maximum of 256 interpolation steps. */ fn towards(&self, goal: &FadePalette, progress: f64) -> FadePalette { let mut res = FadePalette::new(); for (i, (a, b)) in self.0.iter().zip(goal.0.iter()).enumerate() { res.0[i] = a + (b - a) * progress; } res } } impl From<&Palette> for FadePalette { fn from(pal: &Palette) -> Self { let mut fpal = Self::new(); pal.0.iter().enumerate().for_each(|(i, &b)| { fpal.0[i] = b as f64; }); fpal } } impl From<&FadePalette> for Palette { fn from(fpal: &FadePalette) -> Self { let mut pal = Self::new(); fpal.0.iter().enumerate().for_each(|(i, &b)| { let b = if b < 0f64 { 0 } else if 256f64 <= b { 255 } else { b.round() as u8 }; pal.0[i] = b; }); pal } } pub struct Fade { from: Palette, to: Palette, hz: u8, duration: Duration, clear: bool, } impl Fade { pub fn new( from: Palette, to: Palette, duration: Duration, hz: u8, clear: bool, ) -> Self { let hz = if hz == 0 { 1 } else { hz }; Self { from, to, hz, duration, clear } } pub fn commence(self, con: &Console) -> io::Result<()> { let Self { from, to, hz, duration, clear } = self; con.apply_palette(&from)?; let fade = FadePalette::from(&con.current_palette()?); let fade_to = FadePalette::from(&to); let t_0 = Instant::now(); let tick = Duration::from_millis(1_000u64 / hz as u64); let iters = (duration.as_millis() / tick.as_millis()) as u32; let mut i = 0; while i < iters { i += 1; let progress = f64::from(i) / f64::from(iters); let pal = Palette::from(&fade.towards(&fade_to, progress)); con.apply_palette(&pal)?; if clear { con.clear()?; } let next = i * tick; std::thread::sleep(next.saturating_sub(t_0.elapsed())); } Ok(()) } }