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}