termit 0.7.0

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

use async_channel::Sender;
use log::info;
use std::io;
use termit::prelude::*;

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

    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)?;

    let mut app = AppState::new(termit.refresh_time_sender());

    let mut ui = Stack::layered_boxes(vec![])
        .add_box(Canvas::new(Colors::default().height(9).left(10).right(10)).back(Color::black()))
        .add_box(Nice::default())
        .add_box(Exit);

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

    info!("done");

    Ok(())
}

#[derive(Default)]
struct AppState {
    pub is_terminating: bool,
    pub refresh_sender: Option<Sender<Duration>>,
    pub refresh_interval: u64,
}

impl AppState {
    fn new(lag: Sender<Duration>) -> Self {
        Self {
            is_terminating: false,
            refresh_sender: Some(lag),
            refresh_interval: 1000,
        }
    }
    fn refresh_every(&mut self, ms: u64) {
        if ms != self.refresh_interval {
            if let Some(sender) = &self.refresh_sender {
                if let Ok(()) = sender.send_blocking(Duration::from_millis(ms)) {
                    self.refresh_interval = ms
                }
            }
        }
    }
}

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 Colors {
    size: Point,
    r: u8,
    g: u8,
    b: u8,
    input: String,
}
impl Colors {
    fn render(&mut self, screen: &mut Screen, painter: &Painter) -> Window {
        let d = format!(
            "Press numbers 123789 to change the RGB color, q to quit.\nr:{} g:{} b:{} \nlast input: {:?}\nsize: {:?}\ntime: {}",
            self.r,self.g,self.b,
            self.input,
            self.size,
            chrono::Local::now().format("%Y-%m-%d %H:%M:%S.%f")
        );

        painter
            .back(Color::rgb(self.r, self.g, self.b))
            .fill(screen);

        painter
            .with_scope(window(
                painter.scope().position() + point(2, 1),
                painter.scope().size() - point(4, 2),
            ))
            .fill(screen);

        painter
            .with_scope(window(
                painter.scope().position() + point(4, 2),
                painter.scope().size() - point(8, 4),
            ))
            .paint(&d, screen, 0, false);

        painter.scope()
    }
    fn digest<X: AppEvent>(&mut self, input: &Event<X>) {
        if !matches!(input, Event::Refresh(_)) {
            self.input = format!("{:?}", input);
        }
        match input {
            Event::Resize(size) => self.size = *size,
            Event::Key(KeyEvent {
                keycode: KeyCode::Char(c),
                ..
            }) => match c {
                '7' => self.r = self.r.wrapping_add(1),
                '1' => self.r = self.r.wrapping_sub(1),
                '8' => self.g = self.g.wrapping_add(1),
                '2' => self.g = self.g.wrapping_sub(1),
                '9' => self.b = self.b.wrapping_add(1),
                '3' => self.b = self.b.wrapping_sub(1),
                _ => {}
            },
            _ => {}
        }
    }
}
impl AnchorPlacementEnabled for Colors {}
impl<M, A: AppEvent> Widget<M, A> for Colors {
    fn update(
        &mut self,
        _model: &mut M,
        input: &Event<A>,
        screen: &mut Screen,
        painter: &Painter,
    ) -> Window {
        self.digest(input);

        self.render(screen, painter)
    }
}

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

/// Reduce cycling if not in use
pub struct Nice {
    durations: VecDeque<Duration>,
    last: Instant,
    show: bool,
    control_refresh: bool,
}
impl Default for Nice {
    fn default() -> Self {
        Self {
            durations: vec![Duration::ZERO; 600].into_iter().collect(),
            last: Instant::now(),
            show: true,
            control_refresh: true,
        }
    }
}
impl<A: AppEvent> Widget<AppState, A> for Nice {
    fn update(
        &mut self,
        model: &mut AppState,
        input: &Event<A>,
        screen: &mut Screen,
        painter: &Painter,
    ) -> Window {
        match input {
            Event::Focus(focused) => {
                if self.control_refresh {
                    model.refresh_every(if *focused { 1000 } else { 60000 });
                }
            }
            _ => {}
        }

        if let (true, Event::Refresh(_beats)) = (self.show, input) {
            //if *_beats != 0 {
            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}\nlag:{}", model.refresh_interval)
                .update_asserted(model, input, screen, painter)
        } else {
            // no space taken
            painter.scope().trim(point(0, 0))
        }
    }
}