use crate::error::{CliError, Result};
use crossterm::{
cursor::{Hide, MoveTo, Show},
event::{poll, read, Event, KeyCode, KeyModifiers},
execute,
style::ResetColor,
terminal::{
self, disable_raw_mode, enable_raw_mode, Clear, ClearType, EnterAlternateScreen,
LeaveAlternateScreen,
},
};
use glyph_core::Glyph;
use std::fs::File;
use std::io::Write;
use std::path::PathBuf;
use std::thread;
use std::time::{Duration, Instant};
struct TerminalGuard {
stdout: std::io::Stdout,
}
impl TerminalGuard {
fn new() -> Result<Self> {
let mut stdout = std::io::stdout();
enable_raw_mode().map_err(|e| CliError::Terminal(e.to_string()))?;
execute!(
stdout,
EnterAlternateScreen,
Hide,
Clear(ClearType::All),
ResetColor,
)
.map_err(|e| CliError::Terminal(e.to_string()))?;
Ok(Self { stdout })
}
}
impl Drop for TerminalGuard {
fn drop(&mut self) {
let _ = execute!(
self.stdout,
Show,
ResetColor,
Clear(ClearType::All),
LeaveAlternateScreen,
);
let _ = disable_raw_mode();
}
}
pub fn play_glyph(input: PathBuf, loops: u32) -> Result<()> {
let mut file = File::open(&input)
.map_err(|e| CliError::Other(format!("Failed to open {}: {}", input.display(), e)))?;
let glyph = Glyph::read(&mut file)?;
let _guard = TerminalGuard::new()?;
let mut stdout = std::io::stdout();
let (term_width, term_height) =
terminal::size().map_err(|e| CliError::Terminal(e.to_string()))?;
if term_width < glyph.width as u16 || term_height < glyph.height as u16 {
return Err(CliError::Other(format!(
"Terminal size too small. Need at least {}x{}, but got {}x{}",
glyph.width, glyph.height, term_width, term_height
)));
}
play_animation(&mut stdout, &glyph, loops)
}
#[derive(Debug)]
enum PlayerCommand {
Exit,
TogglePause,
PrevFrame,
NextFrame,
None,
}
fn play_animation(stdout: &mut std::io::Stdout, glyph: &Glyph, loops: u32) -> Result<()> {
let mut current_loop = 0;
let mut frame_index = 0;
let mut paused = false;
let mut last_frame_time = Instant::now();
loop {
let (term_width, term_height) =
terminal::size().map_err(|e| CliError::Terminal(e.to_string()))?;
if term_width < glyph.width as u16 || term_height < glyph.height as u16 {
return Err(CliError::Other(format!(
"Terminal size too small. Need at least {}x{}, but got {}x{}",
glyph.width, glyph.height, term_width, term_height
)));
}
if !paused {
let frame = &glyph.frames[frame_index];
let frame_duration = frame.duration_ms.unwrap_or(glyph.default_duration_ms);
execute!(stdout, Clear(ClearType::All), MoveTo(0, 0), ResetColor)
.map_err(|e| CliError::Terminal(e.to_string()))?;
for line in frame.content.lines() {
stdout
.write_all(line.as_bytes())
.map_err(|e| CliError::Terminal(e.to_string()))?;
stdout
.write_all(b"\r\n")
.map_err(|e| CliError::Terminal(e.to_string()))?;
}
stdout
.flush()
.map_err(|e| CliError::Terminal(e.to_string()))?;
let elapsed = last_frame_time.elapsed();
if elapsed < Duration::from_millis(frame_duration as u64) {
let sleep_duration = Duration::from_millis(frame_duration as u64) - elapsed;
let start = Instant::now();
while start.elapsed() < sleep_duration {
if poll(Duration::from_millis(1))
.map_err(|e| CliError::Terminal(e.to_string()))?
{
match handle_input()? {
PlayerCommand::Exit => return Ok(()),
PlayerCommand::TogglePause => {
paused = true;
break;
}
PlayerCommand::PrevFrame => {
frame_index = if frame_index == 0 {
glyph.frames.len() - 1
} else {
frame_index - 1
};
last_frame_time = Instant::now();
break;
}
PlayerCommand::NextFrame => {
frame_index = (frame_index + 1) % glyph.frames.len();
last_frame_time = Instant::now();
break;
}
PlayerCommand::None => {}
}
}
thread::sleep(Duration::from_millis(10));
}
}
if !paused {
last_frame_time = Instant::now();
frame_index = (frame_index + 1) % glyph.frames.len();
if frame_index == 0 {
current_loop += 1;
if loops > 0 && current_loop >= loops {
break;
}
}
}
} else {
if poll(Duration::from_millis(50)).map_err(|e| CliError::Terminal(e.to_string()))? {
match handle_input()? {
PlayerCommand::Exit => return Ok(()),
PlayerCommand::TogglePause => {
paused = false;
last_frame_time = Instant::now();
}
PlayerCommand::PrevFrame => {
frame_index = if frame_index == 0 {
glyph.frames.len() - 1
} else {
frame_index - 1
};
let frame = &glyph.frames[frame_index];
execute!(stdout, Clear(ClearType::All), MoveTo(0, 0))
.map_err(|e| CliError::Terminal(e.to_string()))?;
for line in frame.content.lines() {
stdout
.write_all(line.as_bytes())
.map_err(|e| CliError::Terminal(e.to_string()))?;
stdout
.write_all(b"\r\n")
.map_err(|e| CliError::Terminal(e.to_string()))?;
}
stdout
.flush()
.map_err(|e| CliError::Terminal(e.to_string()))?;
}
PlayerCommand::NextFrame => {
frame_index = (frame_index + 1) % glyph.frames.len();
let frame = &glyph.frames[frame_index];
execute!(stdout, Clear(ClearType::All), MoveTo(0, 0))
.map_err(|e| CliError::Terminal(e.to_string()))?;
for line in frame.content.lines() {
stdout
.write_all(line.as_bytes())
.map_err(|e| CliError::Terminal(e.to_string()))?;
stdout
.write_all(b"\r\n")
.map_err(|e| CliError::Terminal(e.to_string()))?;
}
stdout
.flush()
.map_err(|e| CliError::Terminal(e.to_string()))?;
}
PlayerCommand::None => {}
}
}
}
}
Ok(())
}
fn handle_input() -> Result<PlayerCommand> {
if let Event::Key(key) = read().map_err(|e| CliError::Terminal(e.to_string()))? {
match key.code {
KeyCode::Char('c') if key.modifiers.contains(KeyModifiers::CONTROL) => {
return Ok(PlayerCommand::Exit)
}
KeyCode::Char('q') | KeyCode::Esc => return Ok(PlayerCommand::Exit),
KeyCode::Char(' ') => return Ok(PlayerCommand::TogglePause),
KeyCode::Left => return Ok(PlayerCommand::PrevFrame),
KeyCode::Right => return Ok(PlayerCommand::NextFrame),
_ => {}
}
}
Ok(PlayerCommand::None)
}