use std::{io::Write, sync::OnceLock};
use crossterm::{
cursor::MoveTo,
queue,
style::{Print, ResetColor},
};
use crate::layout::{Position, Size, centered_block_origin, text_block_size, top_two_thirds};
pub const MASCOT: &str = include_str!("mascot.txt");
static MASCOT_FRAME: OnceLock<Vec<&'static str>> = OnceLock::new();
#[derive(Debug, Clone, Copy)]
pub struct MascotFrames {
frame: &'static [&'static str],
}
impl MascotFrames {
pub fn new(frame: &'static [&'static str]) -> Self {
Self { frame }
}
pub fn blizz() -> Self {
Self::new(lines())
}
pub fn lines(&self) -> &'static [&'static str] {
self.frame
}
pub fn size(&self) -> Size {
text_block_size(self.frame)
}
pub fn centered_origin(&self, terminal_size: Size) -> Position {
centered_block_origin(top_two_thirds(terminal_size), self.size())
}
pub fn queue_at<W: Write>(&self, writer: &mut W, origin: Position) -> std::io::Result<()> {
queue_frame(writer, origin, self.frame)
}
pub fn queue_centered<W: Write>(
&self,
writer: &mut W,
terminal_size: Size,
) -> std::io::Result<()> {
self.queue_at(writer, self.centered_origin(terminal_size))
}
}
impl Default for MascotFrames {
fn default() -> Self {
Self::blizz()
}
}
pub fn lines() -> &'static [&'static str] {
MASCOT_FRAME.get_or_init(|| block_lines(MASCOT)).as_slice()
}
pub fn size() -> Size {
MascotFrames::blizz().size()
}
pub fn centered_origin(terminal_size: Size) -> Position {
MascotFrames::blizz().centered_origin(terminal_size)
}
pub fn queue_frame<W: Write>(
writer: &mut W,
origin: Position,
lines: &[&str],
) -> std::io::Result<()> {
for (offset, line) in lines.iter().enumerate() {
queue!(
writer,
MoveTo(origin.column, origin.row.saturating_add(offset as u16)),
Print(line)
)?;
}
queue!(writer, ResetColor)?;
Ok(())
}
pub fn queue_frame_owned<W: Write>(
writer: &mut W,
origin: Position,
lines: &[String],
) -> std::io::Result<()> {
for (offset, line) in lines.iter().enumerate() {
queue!(
writer,
MoveTo(origin.column, origin.row.saturating_add(offset as u16)),
Print(line)
)?;
}
queue!(writer, ResetColor)?;
Ok(())
}
pub fn queue_centered<W: Write>(writer: &mut W, terminal_size: Size) -> std::io::Result<()> {
MascotFrames::blizz().queue_centered(writer, terminal_size)
}
fn block_lines(block: &'static str) -> Vec<&'static str> {
block.trim_end_matches('\n').lines().collect()
}
#[cfg(test)]
mod tests {
use super::*;
use crate::layout;
#[test]
fn mascot_has_static_dimensions() {
assert_eq!(MascotFrames::blizz().size(), layout::size(38, 30));
assert_eq!(size(), layout::size(38, 30));
}
#[test]
fn centered_origin_uses_top_two_thirds() {
assert_eq!(
MascotFrames::blizz().centered_origin(layout::size(100, 60)),
layout::position(31, 5)
);
assert_eq!(
centered_origin(layout::size(100, 60)),
layout::position(31, 5)
);
}
#[test]
fn default_frames_use_blizz_mascot() {
assert_eq!(MascotFrames::default().lines(), lines());
}
#[test]
fn queue_centered_writes_mascot() {
let mut buffer = Vec::new();
queue_centered(&mut buffer, layout::size(100, 60)).unwrap();
let output = String::from_utf8(buffer).unwrap();
assert!(output.contains("▓"));
}
#[test]
fn queue_frame_writes_all_lines() {
let mut buffer = Vec::new();
queue_frame(&mut buffer, layout::position(0, 0), &["a", "b"]).unwrap();
let output = String::from_utf8(buffer).unwrap();
assert!(output.contains("a"));
assert!(output.contains("b"));
}
#[test]
fn queue_frame_owned_writes_owned_strings() {
let mut buffer = Vec::new();
let lines = vec!["hello".to_string(), "world".to_string()];
queue_frame_owned(&mut buffer, layout::position(0, 0), &lines).unwrap();
let output = String::from_utf8(buffer).unwrap();
assert!(output.contains("hello"));
assert!(output.contains("world"));
}
#[test]
fn queue_at_writes_at_position() {
let mut buffer = Vec::new();
let frames = MascotFrames::blizz();
frames
.queue_at(&mut buffer, layout::position(5, 3))
.unwrap();
assert!(!buffer.is_empty());
}
#[test]
fn new_constructs_from_static_slice() {
static LINES: &[&str] = &["abc", "def"];
let frames = MascotFrames::new(LINES);
assert_eq!(frames.lines(), LINES);
assert_eq!(frames.size(), layout::size(3, 2));
}
#[test]
fn block_lines_splits_on_newlines() {
let result = block_lines("a\nb\nc\n");
assert_eq!(result, vec!["a", "b", "c"]);
}
}