Last active
March 26, 2024 00:28
-
-
Save Cypher1/1327bdd049a82710d2396b94d0592818 to your computer and use it in GitHub Desktop.
'Interactive' TUI with tokio and crossterm
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
[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" ]} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
// 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