A simple Terminal Typing Test utility written in Rust using ratatui, inspired by Monkeytype.
git clone https://github.com/semanavasco/ttt.git
cd ttt
cargo install --path .$ ttt
A simple Terminal Typing Test utility.
Usage: ttt [OPTIONS]
Options:
-t, --text <TEXT> The text to get the words from
-w, --words <WORDS> The number of words the test includes [modes: words]
-m, --mode <MODE> The game mode to use [possible values: clock, words, ...]
-d, --duration <DURATION> The duration of the test [modes: clock]
-c, --config <CONFIG> Read config from file
-s, --save-config Save config, applies overrides provided by other arguments
--defaults Use default settings
-h, --help Print help
-V, --version Print version# Run with saved config or defaults
$ ttt
# Run with custom settings
$ ttt --mode clock --words 50 --duration 45
# Use a specific language
$ ttt --text spanish
# Save current settings as default
$ ttt --words 75 --duration 60 --save-config
# Load from custom config file
$ ttt --config ~/my-config.toml| Name | Description |
|---|---|
lorem |
100 words of Lorem Ipsum (default) |
english |
100 most common English words |
spanish |
100 most common Spanish words |
portuguese |
100 most common Portuguese words |
german |
100 most common German words |
swedish |
100 most common Swedish words |
Config file location: ~/.config/ttt/config.toml
[defaults]
text = "english"
mode = "clock"
duration = 30CLI arguments override config file settings.
Custom texts can be placed at: ~/.config/ttt/texts/
- Create
src/app/modes/newmode.rsand implementHandler+Renderertraits:
use crate::app::{State, modes::{Action, Handler, Renderer}};
use crate::config::Config;
use crossterm::event::KeyEvent;
use ratatui::{buffer::Buffer, layout::Rect};
pub struct NewMode {
// your fields
}
impl Handler for NewMode {
fn initialize(&mut self, config: &Config) { /* ... */ }
fn handle_input(&mut self, key: KeyEvent) -> Action {
// Process input and return an Action to trigger state changes
// e.g., Action::SwitchState(State::Running)
Action::None
}
}
impl Renderer for NewMode {
fn render_home_body(&self, area: Rect, buf: &mut Buffer) { /* ... */ }
fn render_running_body(&self, area: Rect, buf: &mut Buffer) { /* ... */ }
fn render_complete_body(&self, area: Rect, buf: &mut Buffer) { /* ... */ }
// Footer methods have default implementations in Renderer trait,
// but you can override them:
// fn render_home_footer(&self, area: Rect, buf: &mut Buffer) { /* ... */ }
}- Add to
src/app/modes/mod.rs:
pub mod newmode;
// ...
pub const AVAILABLE_MODES: &[&str] = &["clock", "words", "newmode"];
pub fn create_mode(mode: &Mode) -> Box<dyn GameMode> {
match mode {
Mode::Clock { duration } => Box::new(Clock::new(*duration)),
Mode::Words { count } => Box::new(Words::new(*count)),
Mode::NewMode { /* ... */ } => Box::new(NewMode::new(/* ... */)),
}
}
#[derive(Serialize, Deserialize, Clone)]
pub enum Mode {
Clock { /* ... */ },
Words { /* ... */ },
NewMode { /* your config */ },
}
impl Mode {
pub fn from_string(mode: &str) -> Option<Self> {
match mode {
"clock" => Some(Mode::Clock { /* ... */ }),
"words" => Some(Mode::Words { /* ... */ }),
"newmode" => Some(Mode::NewMode { /* ... */ }),
_ => None,
}
}
}- Open a PR with your new mode (or enjoy it locally...)!
This is one of my first Rust projects and I'm actively learning! I'm open to suggestions, code reviews, and constructive criticism. Feel free to open issues. I'd appreciate if you'd let me fix them rather than opening PRs with written solutions. Thank you!
This project is licensed under the MIT License - see the LICENSE file for details.
