use crate::{BrailleGrid, Result, TerminalRenderer};
const DEFAULT_WIDTH: usize = 80;
const DEFAULT_HEIGHT: usize = 24;
#[inline]
fn terminal_size() -> (usize, usize) {
crossterm::terminal::size()
.map(|(w, h)| (w as usize, h as usize))
.unwrap_or((DEFAULT_WIDTH, DEFAULT_HEIGHT))
}
fn wait_for_key() -> Result<()> {
use crossterm::event::{self, Event};
loop {
if let Event::Key(_) = event::read()? {
break;
}
}
Ok(())
}
pub fn grid() -> Result<BrailleGrid> {
let (w, h) = terminal_size();
BrailleGrid::new(w, h)
}
pub fn grid_sized(width: usize, height: usize) -> Result<BrailleGrid> {
BrailleGrid::new(width, height)
}
pub fn show(grid: &BrailleGrid) -> Result<()> {
let mut renderer = TerminalRenderer::new()?;
renderer.render(grid)?;
wait_for_key()?;
Ok(())
}
#[cfg(feature = "image")]
pub fn show_image(path: impl AsRef<std::path::Path>) -> Result<()> {
let grid = load_image(path)?;
show(&grid)
}
#[cfg(feature = "image")]
pub fn load_image(path: impl AsRef<std::path::Path>) -> Result<BrailleGrid> {
use crate::image::ImageRenderer;
let (w, h) = terminal_size();
ImageRenderer::new()
.load_from_path(path.as_ref())?
.resize(w, h, true)?
.render()
}
#[cfg(feature = "image")]
pub fn load_image_sized(
path: impl AsRef<std::path::Path>,
width: usize,
height: usize,
) -> Result<BrailleGrid> {
use crate::image::ImageRenderer;
ImageRenderer::new()
.load_from_path(path.as_ref())?
.resize(width, height, true)?
.render()
}
#[cfg(feature = "image")]
pub fn show_file(path: impl AsRef<std::path::Path>) -> Result<()> {
use crate::media::{detect_format, MediaFormat};
use crate::DotmaxError;
let path = path.as_ref();
let format = detect_format(path)?;
match format {
MediaFormat::StaticImage(_) => {
show_image(path)
}
MediaFormat::Svg => {
#[cfg(feature = "svg")]
{
show_svg(path)
}
#[cfg(not(feature = "svg"))]
{
Err(DotmaxError::FormatError {
format: "SVG (requires 'svg' feature)".to_string(),
})
}
}
MediaFormat::AnimatedGif => {
play_animated_gif(path)
}
MediaFormat::AnimatedPng => {
play_animated_png(path)
}
MediaFormat::Video(_codec) => {
#[cfg(feature = "video")]
{
play_video(path)
}
#[cfg(not(feature = "video"))]
{
Err(DotmaxError::FormatError {
format: "video (requires 'video' feature and FFmpeg libraries)".to_string(),
})
}
}
MediaFormat::Unknown => Err(DotmaxError::FormatError {
format: "unknown format".to_string(),
}),
}
}
#[cfg(feature = "image")]
pub fn load_file(path: impl AsRef<std::path::Path>) -> Result<crate::media::MediaContent> {
use crate::media::{detect_format, MediaContent, MediaFormat};
use crate::DotmaxError;
let path = path.as_ref();
let format = detect_format(path)?;
match format {
MediaFormat::StaticImage(_) => {
let grid = load_image(path)?;
Ok(MediaContent::Static(grid))
}
MediaFormat::Svg => {
#[cfg(feature = "svg")]
{
let grid = load_svg(path)?;
Ok(MediaContent::Static(grid))
}
#[cfg(not(feature = "svg"))]
{
Err(DotmaxError::FormatError {
format: "SVG (requires 'svg' feature)".to_string(),
})
}
}
MediaFormat::AnimatedGif => {
use crate::media::GifPlayer;
let player = GifPlayer::new(path)?;
Ok(MediaContent::Animated(Box::new(player)))
}
MediaFormat::AnimatedPng => {
use crate::media::ApngPlayer;
let player = ApngPlayer::new(path)?;
Ok(MediaContent::Animated(Box::new(player)))
}
MediaFormat::Video(_codec) => {
#[cfg(feature = "video")]
{
use crate::media::VideoPlayer;
let player = VideoPlayer::new(path)?;
Ok(MediaContent::Animated(Box::new(player)))
}
#[cfg(not(feature = "video"))]
{
Err(DotmaxError::FormatError {
format: "video (requires 'video' feature and FFmpeg libraries)".to_string(),
})
}
}
MediaFormat::Unknown => Err(DotmaxError::FormatError {
format: "unknown format".to_string(),
}),
}
}
#[cfg(feature = "image")]
fn play_animated_gif(path: impl AsRef<std::path::Path>) -> Result<()> {
use crate::media::{GifPlayer, MediaPlayer};
use crossterm::event::{self, Event, KeyCode};
use crossterm::terminal::{self, EnterAlternateScreen, LeaveAlternateScreen};
use crossterm::{cursor, execute};
use std::io::stdout;
use std::time::{Duration, Instant};
let mut player = GifPlayer::new(path)?;
terminal::enable_raw_mode()?;
let mut stdout = stdout();
execute!(stdout, EnterAlternateScreen, cursor::Hide)?;
let mut renderer = TerminalRenderer::new()?;
let result = (|| -> Result<()> {
while let Some(frame_result) = player.next_frame() {
let (grid, delay) = frame_result?;
renderer.render(&grid)?;
let deadline = Instant::now() + delay;
while Instant::now() < deadline {
if event::poll(Duration::from_millis(10))? {
if let Event::Key(key_event) = event::read()? {
if !matches!(key_event.code, KeyCode::Modifier(_)) {
return Ok(());
}
}
}
}
}
wait_for_key()?;
Ok(())
})();
execute!(stdout, cursor::Show, LeaveAlternateScreen)?;
terminal::disable_raw_mode()?;
result
}
#[cfg(feature = "image")]
fn play_animated_png(path: impl AsRef<std::path::Path>) -> Result<()> {
use crate::media::{ApngPlayer, MediaPlayer};
use crossterm::event::{self, Event, KeyCode};
use crossterm::terminal::{self, EnterAlternateScreen, LeaveAlternateScreen};
use crossterm::{cursor, execute};
use std::io::stdout;
use std::time::{Duration, Instant};
let mut player = ApngPlayer::new(path)?;
terminal::enable_raw_mode()?;
let mut stdout = stdout();
execute!(stdout, EnterAlternateScreen, cursor::Hide)?;
let mut renderer = TerminalRenderer::new()?;
let result = (|| -> Result<()> {
while let Some(frame_result) = player.next_frame() {
let (grid, delay) = frame_result?;
renderer.render(&grid)?;
let deadline = Instant::now() + delay;
while Instant::now() < deadline {
if event::poll(Duration::from_millis(10))? {
if let Event::Key(key_event) = event::read()? {
if !matches!(key_event.code, KeyCode::Modifier(_)) {
return Ok(());
}
}
}
}
}
wait_for_key()?;
Ok(())
})();
execute!(stdout, cursor::Show, LeaveAlternateScreen)?;
terminal::disable_raw_mode()?;
result
}
#[cfg(feature = "video")]
fn play_video(path: impl AsRef<std::path::Path>) -> Result<()> {
use crate::media::{MediaPlayer, VideoPlayer};
use crossterm::event::{self, Event, KeyCode};
use crossterm::terminal::{self, EnterAlternateScreen, LeaveAlternateScreen};
use crossterm::{cursor, execute};
use std::io::stdout;
use std::time::{Duration, Instant};
let mut player = VideoPlayer::new(path)?;
terminal::enable_raw_mode()?;
let mut stdout = stdout();
execute!(stdout, EnterAlternateScreen, cursor::Hide)?;
let mut renderer = TerminalRenderer::new()?;
let result = (|| -> Result<()> {
while let Some(frame_result) = player.next_frame() {
let (grid, delay) = frame_result?;
renderer.render(&grid)?;
let deadline = Instant::now() + delay;
while Instant::now() < deadline {
if event::poll(Duration::from_millis(10))? {
if let Event::Key(key_event) = event::read()? {
if !matches!(key_event.code, KeyCode::Modifier(_)) {
return Ok(());
}
}
}
}
}
wait_for_key()?;
Ok(())
})();
execute!(stdout, cursor::Show, LeaveAlternateScreen)?;
terminal::disable_raw_mode()?;
result
}
#[cfg(feature = "video")]
pub fn show_webcam() -> Result<()> {
use crate::media::WebcamPlayer;
play_webcam_internal(WebcamPlayer::new()?)
}
#[cfg(feature = "video")]
pub fn show_webcam_device(device: impl Into<crate::media::WebcamDeviceId>) -> Result<()> {
use crate::media::WebcamPlayer;
play_webcam_internal(WebcamPlayer::from_device(device)?)
}
#[cfg(feature = "video")]
fn play_webcam_internal(mut player: crate::media::WebcamPlayer) -> Result<()> {
use crate::media::MediaPlayer;
use crossterm::event::{self, Event, KeyCode};
use crossterm::terminal::{self, EnterAlternateScreen, LeaveAlternateScreen};
use crossterm::{cursor, execute};
use std::io::stdout;
use std::time::{Duration, Instant};
terminal::enable_raw_mode()?;
let mut stdout = stdout();
execute!(stdout, EnterAlternateScreen, cursor::Hide)?;
let mut renderer = TerminalRenderer::new()?;
let result = (|| -> Result<()> {
while let Some(frame_result) = player.next_frame() {
let (grid, delay) = frame_result?;
renderer.render(&grid)?;
let deadline = Instant::now() + delay;
while Instant::now() < deadline {
if event::poll(Duration::from_millis(5))? {
match event::read()? {
Event::Key(key_event) => {
if !matches!(key_event.code, KeyCode::Modifier(_)) {
return Ok(());
}
}
Event::Resize(w, h) => {
player.handle_resize(w as usize, h as usize);
}
_ => {}
}
}
}
}
Ok(())
})();
execute!(stdout, cursor::Show, LeaveAlternateScreen)?;
terminal::disable_raw_mode()?;
result
}
#[cfg(all(feature = "image", feature = "svg"))]
fn show_svg(path: impl AsRef<std::path::Path>) -> Result<()> {
let grid = load_svg(path)?;
show(&grid)
}
#[cfg(all(feature = "image", feature = "svg"))]
fn load_svg(path: impl AsRef<std::path::Path>) -> Result<BrailleGrid> {
use crate::image::ImageRenderer;
let (w, h) = terminal_size();
let target_width = (w * 2) as u32;
let target_height = (h * 4) as u32;
ImageRenderer::new()
.load_svg_from_path(path.as_ref(), target_width, target_height)?
.resize(w, h, true)?
.render()
}
#[cfg(test)]
mod tests {
use super::*;
use crate::DotmaxError;
#[test]
fn test_terminal_size_returns_reasonable_values() {
let (w, h) = terminal_size();
assert!(w > 0, "Width should be positive");
assert!(h > 0, "Height should be positive");
assert!(w >= 80 || w > 0, "Width should be at least fallback or positive");
assert!(h >= 24 || h > 0, "Height should be at least fallback or positive");
}
#[test]
fn test_terminal_size_fallback_values() {
let (_w, _h) = terminal_size();
}
#[test]
fn test_grid_creates_valid_grid() {
let result = grid();
assert!(result.is_ok(), "grid() should succeed");
let g = result.unwrap();
assert!(g.width() > 0, "Grid width should be positive");
assert!(g.height() > 0, "Grid height should be positive");
}
#[test]
fn test_grid_sized_creates_exact_dimensions() {
let g = grid_sized(100, 50).unwrap();
assert_eq!(g.width(), 100);
assert_eq!(g.height(), 50);
}
#[test]
fn test_grid_sized_zero_width_fails() {
let result = grid_sized(0, 50);
assert!(result.is_err());
match result {
Err(DotmaxError::InvalidDimensions { width, height }) => {
assert_eq!(width, 0);
assert_eq!(height, 50);
}
_ => panic!("Expected InvalidDimensions error"),
}
}
#[test]
fn test_grid_sized_zero_height_fails() {
let result = grid_sized(100, 0);
assert!(result.is_err());
match result {
Err(DotmaxError::InvalidDimensions { width, height }) => {
assert_eq!(width, 100);
assert_eq!(height, 0);
}
_ => panic!("Expected InvalidDimensions error"),
}
}
#[cfg(feature = "image")]
mod image_tests {
use super::*;
use std::path::Path;
#[test]
fn test_load_image_sized_creates_correct_dimensions() {
let test_image = Path::new("tests/fixtures/images/sample.png");
if test_image.exists() {
let result = load_image_sized(test_image, 40, 20);
assert!(result.is_ok(), "load_image_sized should succeed: {:?}", result.err());
let g = result.unwrap();
assert_eq!(g.width(), 40);
assert_eq!(g.height(), 20);
}
}
#[test]
fn test_load_image_nonexistent_file_fails() {
let result = load_image("nonexistent_image_12345.png");
assert!(result.is_err(), "load_image should fail for nonexistent file");
}
#[test]
fn test_load_image_sized_nonexistent_file_fails() {
let result = load_image_sized("nonexistent_image_12345.png", 100, 50);
assert!(result.is_err(), "load_image_sized should fail for nonexistent file");
}
}
#[test]
fn test_grid_sized_exceeds_max_fails() {
let result = grid_sized(10_001, 100);
assert!(result.is_err(), "Should fail for width > 10000");
}
#[cfg(feature = "image")]
mod media_tests {
use super::*;
use crate::DotmaxError;
use std::path::Path;
#[test]
fn test_load_file_png_returns_static_content() {
let test_image = Path::new("tests/fixtures/images/sample.png");
if test_image.exists() {
let result = load_file(test_image);
assert!(result.is_ok(), "load_file should succeed for PNG: {:?}", result.err());
let content = result.unwrap();
match content {
crate::media::MediaContent::Static(grid) => {
assert!(grid.width() > 0);
assert!(grid.height() > 0);
}
_ => panic!("Expected Static variant for PNG"),
}
}
}
#[test]
fn test_load_file_nonexistent_fails() {
let result = load_file("nonexistent_file_12345.png");
assert!(result.is_err(), "load_file should fail for nonexistent file");
}
#[test]
fn test_load_file_unknown_format_returns_format_error() {
use std::io::Write;
let temp_dir = std::env::temp_dir();
let temp_file = temp_dir.join("test_unknown.xyz");
let mut file = std::fs::File::create(&temp_file).unwrap();
file.write_all(&[0x00, 0x00, 0x00, 0x00]).unwrap();
drop(file);
let result = load_file(&temp_file);
assert!(result.is_err(), "load_file should fail for unknown format");
if let Err(DotmaxError::FormatError { format }) = result {
assert!(format.contains("unknown"), "Error should mention unknown format");
} else {
panic!("Expected FormatError for unknown format");
}
let _ = std::fs::remove_file(&temp_file);
}
#[test]
fn test_show_file_nonexistent_fails() {
let result = show_file("nonexistent_file_12345.png");
assert!(result.is_err(), "show_file should fail for nonexistent file");
}
}
}