snailshell/
lib.rs

1//! # snailshell
2//! Tiny library for making terminal text display with pleasant RPG-style animations.
3//!
4//! ## Examples
5//! ```
6//! # use snailshell::*;
7//! // basic
8//! snailprint("MUDKIP used WATER GUN!");
9//!
10//! // custom speeds
11//! snailprint_d("This fully prints in exactly one second.", 1.0);
12//! snailprint_s("This prints six characters per second", 6.0);
13//! ```
14//!
15//! ## Colored Text
16//! ```
17//! use snailshell::*;
18//!
19//! // use any library you like.
20//! // Snailshell works on any type that implements display.
21//! // That means any type which you can use print!(), println!(), or format!() with!
22//! use crossterm::style::Stylize;
23//!
24//! snailprint("flamingo, oh oh ou-oh".magenta());
25//!
26//! ```
27//!
28//! ### Refresh Rate
29//! You can change the refresh rate with [set_snail_fps].
30//! This is entirely optional.
31//!
32//! Default fps is 60.
33
34use std::fmt::Display;
35use std::io::{stdout, Write};
36use std::sync::RwLock;
37use std::thread::sleep;
38use std::time::Instant;
39use unicode_segmentation::UnicodeSegmentation;
40use once_cell::sync::Lazy;
41
42
43/// refresh rate of animated text
44///
45/// Text will only flush stdout buffer when characters should be updated.
46/// Think of this as maximum FPS.
47///
48static FPS: Lazy<RwLock<u8>> = Lazy::new(||{
49    RwLock::new(60)
50});
51
52/// Sets the global fps of animated text.
53pub fn set_snail_fps(fps: u8){
54    if let Ok(mut f) = FPS.write(){
55        *f = fps;
56    }
57}
58
59/// Animate text with a fixed duration of two seconds.
60///
61/// ### Example
62/// ```
63/// # use snailshell::snailprint;
64/// snailprint("The simplest way to use snailshell");
65/// ```
66pub fn snailprint<T: Display>(text: T){
67    snailprint_d(text, 2.0);
68}
69
70/// Animate text at a constant speed of chars per second. This will animate so that each character
71/// printed takes a predictable speed, unlike [snailprint_d](snailprint_d()).
72/// ### Example
73/// ```
74/// # use snailshell::snailprint_s;
75/// snailprint_s("this will print one character per second", 1.0);
76/// snailprint_s("this will print 50 characters per second", 50.0);
77/// ```
78pub fn snailprint_s<T: Display>(text: T, speed: f32){
79    let s = format_graphemes(text);
80    let l = s.len();
81    snailprint_internal(s, l as f32 / speed);
82}
83
84/// Animate text with custom fixed duration. If you are printing a message with 10 characters and
85/// a one with 200, they will both take the same amount of time if passed the same duration.
86///### Example
87/// ```
88/// # use snailshell::snailprint_d;
89/// snailprint_d("This message will take five seconds to print", 5.0);
90/// snailprint_d("And so will this one", 5.0);
91/// ```
92///
93///
94pub fn snailprint_d<T: Display>(text: T, duration: f32){
95    let mut graphemes = format_graphemes(text);
96    snailprint_internal(graphemes, duration);
97}
98
99/// formats Display type to vec of grapheme clusters
100fn format_graphemes<T: Display>(text: T) -> Vec<String>{
101    let s = format!("{}", text);
102    s
103        .graphemes(true)
104        .map(|g| g.to_string())
105        .rev()
106        .collect::<Vec<String>>()
107}
108
109/// Animates text through the terminal.
110/// Decoupled so grapheme cluster separation only has to occur once.
111/// Duration is calculated from grapheme clusters which makes each cluster render at the same speed.
112fn snailprint_internal(mut graphemes: Vec<String>, duration: f32){
113    let time = Instant::now();
114
115    let graph_len = graphemes.len();
116
117    let fps = match FPS.read() {
118        Ok(f) => {
119            *f as f32
120        }
121        Err(_) => {
122            60.0
123        }
124    };
125
126    let delta = 1.0 / fps;
127
128    'outer:while !graphemes.is_empty(){
129        let char_targ = (graph_len as f32 * time.elapsed().as_secs_f32() / duration) as usize;
130
131        while char_targ > graph_len - graphemes.len(){
132            if !graphemes.is_empty(){
133                print!("{}", graphemes.pop().unwrap());
134                stdout().flush().unwrap();
135            }else{
136                // this is so sleep() is not called when this loop breaks
137                break 'outer;
138            }
139        }
140        sleep(std::time::Duration::from_secs_f32(delta));
141    }
142    println!();
143}