Skip to content

Instantly share code, notes, and snippets.

@Cypher1
Last active March 26, 2024 00:28
Show Gist options
  • Save Cypher1/1327bdd049a82710d2396b94d0592818 to your computer and use it in GitHub Desktop.
Save Cypher1/1327bdd049a82710d2396b94d0592818 to your computer and use it in GitHub Desktop.
'Interactive' TUI with tokio and crossterm
[package]
name = "rusterm"
version = "0.1.0"
edition = "2021"
# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html
[dependencies]
crokey = "0.5.1"
crossbeam = "0.8.2"
crossterm = "0.24.0"
futures = "0.3.25"
notify = "5.0.0"
shutdown_hooks = "0.1.0"
termimad = "0.20.3"
tokio = { version = "1.21.2", features = [ "full" ]}
// Copyright 2022 Google LLC.
// SPDX-License-Identifier: Apache-2.0
use std::{time::{Duration, Instant}, io::{stdout, Write}, thread};
use shutdown_hooks::add_shutdown_hook;
use crokey::{key, KeyEventFormat};
use crossterm::{
event::{read, Event, KeyEvent},
cursor::{MoveTo, SavePosition, RestorePosition},
terminal::{size, Clear, ClearType, enable_raw_mode, disable_raw_mode},
style::{Color, Print, ResetColor, SetBackgroundColor, SetForegroundColor},
QueueableCommand, Result,
};
use tokio::{self, sync::mpsc::{self, error::TryRecvError}};
const TARGET_FPS: u64 = 60;
const NANOS_PER_SECOND: u64 = 1_000_000_000;
// const TARGET_NANOS: Duration = Duration::from_nanos(NANOS_PER_SECOND/TARGET_FPS);
const MAX_NANOS: Duration = Duration::from_nanos(NANOS_PER_SECOND/(TARGET_FPS+1));
async fn render_loop(mut rx_key_event: tokio::sync::mpsc::Receiver<KeyEvent>) -> Result<()> {
enable_raw_mode()?;
let mut status = "".to_string();
let key_fmt = KeyEventFormat::default();
let mut should_exit = false;
let start = Instant::now();
stdout().queue(Clear(ClearType::All))?;
let mut frames = 0;
loop {
let frame_start = Instant::now();
let (cols, rows) = size()?;
frames += 1;
let nanos_per_frame = (start.elapsed().as_nanos() as f64)/(frames as f64);
let fps = (NANOS_PER_SECOND as f64)/nanos_per_frame;
let mut chars = "".to_string();
loop {
match rx_key_event.try_recv() {
Ok(key_event) => {
match key_event {
key!(ctrl-c) | key!(ctrl-q) => should_exit = true,
_ => {}
};
chars = format!("{}{}", chars, key_fmt.to_string(key_event));
}
Err(TryRecvError::Empty) => break,
Err(TryRecvError::Disconnected) => panic!("Key event sender disconnected!?"),
}
}
status = if chars != "" { format!("You typed {}", chars) } else { status };
stdout()
.queue(SavePosition)?
.queue(Clear(ClearType::All))?;
for i in 0..cols { stdout().queue(MoveTo(i, rows/2))?.queue(Print("-"))?; }
for i in 0..rows { stdout().queue(MoveTo(cols/2, i))?.queue(Print("|"))?; }
stdout()
.queue(SetForegroundColor(Color::Red))?
.queue(SetBackgroundColor(Color::Blue))?
.queue(MoveTo(0, 2))?
.queue(Print("Styled text here."))?
.queue(MoveTo(0, 3))?
.queue(Print(&format!("Size: {},{}", &rows, &cols)))?
.queue(SetForegroundColor(Color::White))?
.queue(SetBackgroundColor(Color::Black))?
.queue(MoveTo(0, 4))?
.queue(Print(&format!("Iterations: {}. Average FPS: {}.", frames, fps)))?
.queue(MoveTo(0, 5))?
.queue(Print(&format!("Status: {}.", status)))?;
stdout()
.queue(ResetColor)?
.queue(RestorePosition)?;
stdout().flush()?;
if should_exit {
stdout().queue(Clear(ClearType::All))?;
stdout().flush()?;
std::process::exit(0)
}
if let Some(remaining) = MAX_NANOS.checked_sub(frame_start.elapsed()) {
// let err = Duration::from_nanos(nanos_per_frame as u64).saturating_sub(TARGET_NANOS);
thread::sleep(remaining);//.saturating_sub(err));
}
}
}
extern "C" fn shutdown() {
let _discard = disable_raw_mode();
}
#[tokio::main]
async fn main() -> Result<()> {
add_shutdown_hook(shutdown);
let (tx_key_event, rx_key_event) = mpsc::channel(100);
tokio::spawn(async move {
loop {
if let Event::Key(key_event) = read().expect("Read should not fail") {
if tx_key_event.send(key_event).await.is_err() { panic!("receiver dropped") }
}
}
});
render_loop(rx_key_event).await?;
Ok(())
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment