mod buffer;
mod config;
mod error;
mod scroll;
mod status_bar;
pub mod terminal;
pub use buffer::RenderBuffer;
pub use config::AnimationConfig;
pub use error::RendererError;
pub use scroll::{Action, ScrollState};
pub use status_bar::StatusBar;
pub use terminal::TerminalState;
use crate::pattern::PatternEngine;
use crate::pattern::{PatternConfig, REGISTRY};
use crate::themes;
use crossterm::event::KeyCode;
use crossterm::event::KeyEvent;
use std::io::Write;
use std::time::{Duration, Instant};
pub struct Renderer {
engine: PatternEngine,
config: AnimationConfig,
buffer: RenderBuffer,
terminal: TerminalState,
scroll: ScrollState,
status_bar: StatusBar,
available_themes: Vec<String>,
current_theme_index: usize,
available_patterns: Vec<String>,
current_pattern_index: usize,
last_frame: Option<Instant>,
frame_count: u32,
last_fps_update: Instant,
current_fps: f64,
}
impl Renderer {
pub fn new(engine: PatternEngine, config: AnimationConfig) -> Result<Self, RendererError> {
let terminal = TerminalState::new()?;
let term_size = terminal.size();
let buffer = RenderBuffer::new(term_size);
let scroll = ScrollState::new(term_size.1.saturating_sub(2));
let mut status_bar = StatusBar::new(term_size);
let available_themes = themes::all_themes()
.iter()
.map(|t| t.name.clone())
.collect::<Vec<_>>();
let available_patterns = REGISTRY
.list_patterns()
.into_iter()
.map(|s| s.to_string())
.collect::<Vec<_>>();
let initial_theme = match engine.config().common.theme_name.as_ref() {
Some(name) => name,
None => "rainbow", };
status_bar.set_theme(initial_theme);
let current_theme_index = available_themes
.iter()
.position(|t| t == initial_theme)
.unwrap_or(0);
let initial_pattern = REGISTRY
.get_pattern_id(&engine.config().params)
.unwrap_or("horizontal");
let current_pattern_index = available_patterns
.iter()
.position(|p| p == initial_pattern)
.unwrap_or(0);
status_bar.set_pattern(initial_pattern);
let now = Instant::now();
let fps = config.fps;
Ok(Self {
engine,
config,
buffer,
terminal,
scroll,
status_bar,
available_themes,
current_theme_index,
available_patterns,
current_pattern_index,
last_frame: None,
frame_count: 0,
last_fps_update: now,
current_fps: fps as f64,
})
}
#[inline]
pub fn frame_duration(&self) -> Duration {
self.config.frame_duration()
}
#[inline]
pub fn is_infinite(&self) -> bool {
self.config.infinite
}
#[inline]
pub fn cycle_duration(&self) -> Duration {
self.config.cycle_duration
}
pub fn render_static(&mut self, text: &str) -> Result<(), RendererError> {
self.buffer.prepare_text(text)?;
self.buffer.update_colors_static(&self.engine)?;
let mut stdout = self.terminal.stdout();
self.buffer.render_region(
&mut stdout,
0,
self.buffer.total_lines(),
self.terminal.colors_enabled(),
false,
)?;
stdout.flush()?;
Ok(())
}
pub fn render_frame(&mut self, text: &str, delta_seconds: f64) -> Result<(), RendererError> {
let now = Instant::now();
if !self.buffer.has_content() {
self.terminal.enter_alternate_screen()?;
self.buffer.prepare_text(text)?;
self.scroll.set_total_lines(self.buffer.line_count());
let visible_range = self.scroll.get_visible_range();
self.buffer.update_colors(&self.engine, visible_range.0)?;
self.draw_full_screen()?;
self.last_frame = Some(now);
self.last_fps_update = now;
return Ok(());
}
let frame_time = if let Some(last) = self.last_frame {
now.duration_since(last)
} else {
Duration::from_secs_f64(delta_seconds)
};
let target_frame_time = Duration::from_secs_f64(1.0 / self.config.fps as f64);
if frame_time < target_frame_time {
return Ok(());
}
self.frame_count += 1;
let fps_interval = Duration::from_secs(1);
if now.duration_since(self.last_fps_update) >= fps_interval {
self.current_fps =
self.frame_count as f64 / now.duration_since(self.last_fps_update).as_secs_f64();
self.frame_count = 0;
self.last_fps_update = now;
self.status_bar.set_fps(self.current_fps);
}
self.engine.update(frame_time.as_secs_f64());
let visible_range = self.scroll.get_visible_range();
self.buffer.update_colors(&self.engine, visible_range.0)?;
let mut stdout = self.terminal.stdout();
self.buffer.render_region(
&mut stdout,
visible_range.0,
visible_range.1,
self.terminal.colors_enabled(),
true,
)?;
self.status_bar.render(&mut stdout, &self.scroll)?;
stdout.flush()?;
self.last_frame = Some(now);
Ok(())
}
pub fn handle_resize(&mut self, new_width: u16, new_height: u16) -> Result<(), RendererError> {
if new_width == 0 || new_height == 0 {
return Err(RendererError::InvalidConfig(
"Invalid terminal dimensions".to_string(),
));
}
self.terminal.resize(new_width, new_height)?;
self.scroll.update_viewport(new_height.saturating_sub(2));
self.buffer.resize((new_width, new_height))?;
self.status_bar.resize((new_width, new_height));
self.scroll.validate_viewport();
self.draw_full_screen()?;
Ok(())
}
pub fn handle_key_event(&mut self, key: KeyEvent) -> Result<bool, RendererError> {
match key.code {
KeyCode::Char('t') | KeyCode::Char('T') => {
self.next_theme()?;
self.draw_full_screen()?;
Ok(true)
}
KeyCode::Char('p') | KeyCode::Char('P') => {
self.next_pattern()?;
self.draw_full_screen()?;
Ok(true)
}
_ => match self.scroll.handle_key_event(key) {
Action::Continue => {
let visible_range = self.scroll.get_visible_range();
self.buffer.update_colors(&self.engine, visible_range.0)?;
let mut stdout = self.terminal.stdout();
self.buffer.render_region(
&mut stdout,
visible_range.0,
visible_range.1,
self.terminal.colors_enabled(),
true,
)?;
self.status_bar.render(&mut stdout, &self.scroll)?;
stdout.flush()?;
Ok(true)
}
Action::Exit => Ok(false),
Action::NoChange => Ok(true),
},
}
}
fn draw_full_screen(&mut self) -> Result<(), RendererError> {
let mut stdout = self.terminal.stdout();
let visible_range = self.scroll.get_visible_range();
self.buffer.render_region(
&mut stdout,
visible_range.0,
visible_range.1,
self.terminal.colors_enabled(),
true,
)?;
self.status_bar.render(&mut stdout, &self.scroll)?;
stdout.flush()?;
Ok(())
}
fn update_visible_region(&mut self) -> Result<(), RendererError> {
let visible_range = self.scroll.get_visible_range();
self.buffer.update_colors(&self.engine, visible_range.0)?;
Ok(())
}
pub fn next_theme(&mut self) -> Result<(), RendererError> {
self.current_theme_index = (self.current_theme_index + 1) % self.available_themes.len();
let new_theme = &self.available_themes[self.current_theme_index];
let theme = themes::get_theme(new_theme)?;
let gradient = theme.create_gradient()?;
self.engine.update_gradient(gradient);
let mut config = self.engine.config().clone();
config.common.theme_name = Some(new_theme.clone());
self.engine.update_pattern_config(config);
self.status_bar.set_theme(new_theme);
self.update_visible_region()?;
Ok(())
}
pub fn next_pattern(&mut self) -> Result<(), RendererError> {
self.current_pattern_index =
(self.current_pattern_index + 1) % self.available_patterns.len();
let new_pattern_id = &self.available_patterns[self.current_pattern_index];
let pattern_params = REGISTRY
.create_pattern_params(new_pattern_id)
.ok_or_else(|| {
RendererError::InvalidConfig(format!(
"Failed to create parameters for pattern: {}",
new_pattern_id
))
})?;
let pattern_config = PatternConfig {
common: self.engine.config().common.clone(),
params: pattern_params,
};
self.engine.update_pattern_config(pattern_config);
self.status_bar.set_pattern(new_pattern_id);
self.terminal.clear_screen()?;
self.update_visible_region()?;
Ok(())
}
}
impl Drop for Renderer {
fn drop(&mut self) {
if let Err(e) = self.terminal.cleanup() {
eprintln!("Error cleaning up terminal: {}", e);
}
}
}