termit 0.7.0

Terminal UI over crossterm
Documentation
//! Fade through RGB colors showing off some layouting
//! and event info

use log::info;
use std::io;
use std::marker::PhantomData;
use termit::prelude::*;

#[async_std::main]
async fn main() -> io::Result<()> {
    env_logger::init();

    let mut app = AppState::default();

    let mut termit = Terminal::try_system_default()?
        //.controlling(fs::File::open("/dev/tty")?)
        .into_termit::<NoAppEvent>()
        .enter_raw_mode()?
        .capture_mouse(true)?
        .handle_focus_events(true)?
        .use_alternate_screen(true)?
        .show_cursor(false)?
        .true_color(true);

    let mut ui = Stack::layered_boxes(vec![])
        .add_box(Canvas::new(Ball::default()).back(Color::black()))
        .add_box(Pretty::new(Fps::default()).front(Color::black()))
        .add_box(Exit);

    termit
        .refresh_time_sender()
        .send_blocking(Duration::from_millis(15))
        .expect("refresh interval update");

    while !app.is_terminating {
        termit.step(&mut app, &mut ui).await?;
    }

    Ok(())
}

#[derive(Default)]
struct AppState {
    pub is_terminating: bool,
}

struct Exit;

impl<A: AppEvent> Widget<AppState, A> for Exit {
    fn update(
        &mut self,
        model: &mut AppState,
        input: &Event<A>,
        _screen: &mut Screen,
        painter: &Painter,
    ) -> Window {
        if matches!(
            input,
            Event::Key(KeyEvent {
                keycode: KeyCode::Char('q'),
                ..
            })
        ) {
            info!("exit!");
            model.is_terminating = true;
        }
        painter.scope().trim(point(0, 0))
    }
}

#[derive(Default)]
struct Ball<M, A> {
    pos: Point,
    direction: bool,
    phantom_model: PhantomData<M>,
    phantom_appe_event: PhantomData<A>,
}
impl<M, A: AppEvent> Widget<M, A> for Ball<M, A> {
    fn update(
        &mut self,
        _model: &mut M,
        input: &Event<A>,
        screen: &mut Screen,
        painter: &Painter,
    ) -> Window {
        if let Event::Refresh(beats) = input {
            // mumbo jumbo rendering
            for row in painter.scope().row..painter.scope().height {
                for col in painter.scope().col..painter.scope().width {
                    let dx = col as f32 - self.pos.col as f32;
                    let dy = 2.0 * row as f32 - 2.0 * self.pos.row as f32;
                    let d = (dx * dx + dy * dy).sqrt();
                    screen.set(
                        point(col, row),
                        &Style::DEFAULT.back(Color::rgb(
                            (255.0 * 10.0 / d).clamp(1.0, 254.0) as u8,
                            255 - (255.0 * self.pos.row as f32 / painter.scope().height as f32)
                                .clamp(1.0, 254.0) as u8,
                            (d * 5.0).clamp(1.0, 254.0) as u8,
                        )),
                        " ",
                    );
                }
            }

            for _ in 0..*beats {
                // movement
                if self.pos.col >= painter.scope().width + painter.scope().row || self.pos.col <= 0
                {
                    self.direction = !self.direction
                }

                self.pos.row = (painter.scope().height as f32 * (self.pos.col as f32 / 10.0).sin()
                    / 4.0
                    + painter.scope().row as f32
                    + painter.scope().height as f32 / 2.0) as u16;
                self.pos.col = (self.pos.col as i32)
                    .saturating_add(2 * (self.direction) as i32 - 1)
                    .clamp(0, screen.size().col as i32) as u16;
            }
        }
        painter.scope()
    }
}

use std::{
    collections::VecDeque,
    time::{Duration, Instant},
};

/// Reduce cycling if not in use
pub struct Fps {
    durations: VecDeque<Duration>,
    last: Instant,
    show: bool,
}
impl Default for Fps {
    fn default() -> Self {
        Self {
            durations: vec![Duration::ZERO; 600].into_iter().collect(),
            last: Instant::now(),
            show: true,
        }
    }
}
impl<A: AppEvent> Widget<AppState, A> for Fps {
    fn update(
        &mut self,
        model: &mut AppState,
        input: &Event<A>,
        screen: &mut Screen,
        painter: &Painter,
    ) -> Window {
        if let (Event::Refresh(_), true) = (input, self.show) {
            self.durations.push_back(self.last.elapsed());
            self.durations.pop_front();
            self.last = Instant::now();

            // show stats over longer period
            let (count, total_duration) =
                self.durations.iter().rev().fold((0, 0.0), |(c, t), a| {
                    if t < 3.0 {
                        (c + 1, t + a.as_secs_f64())
                    } else {
                        (c, t)
                    }
                });
            let fps = count as f64 / total_duration;
            format!("FPS:{fps:0.5}").update_asserted(model, input, screen, painter)
        } else {
            // no space taken
            painter.scope().trim(point(0, 0))
        }
    }
}