Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1,549 changes: 1,400 additions & 149 deletions Cargo.lock

Large diffs are not rendered by default.

12 changes: 5 additions & 7 deletions Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -10,15 +10,13 @@ authors = ["Max Niederman <max@maxniederman.com>"]
edition = "2021"

[dependencies]
clap = { version = "^4.5", features = ["derive"] }
clap = { version = "4.6.0", features = ["derive"] }
clap_complete = "^4.5"
crossterm = "0.29.0"
dirs = "^5.0"
crossterm = "^0.27"
rust-embed = "^8.2"
toml = "^0.8"

[dependencies.ratatui]
version = "^0.25"
ratatui = { version = "0.30.0", features = ["serde"] }
rust-embed = "8.11.0"
toml = "1.0.7"

[dependencies.rand]
version = "^0.8"
Expand Down
File renamed without changes.
File renamed without changes.
File renamed without changes.
File renamed without changes.
File renamed without changes.
File renamed without changes.
File renamed without changes.
File renamed without changes.
File renamed without changes.
File renamed without changes.
File renamed without changes.
File renamed without changes.
File renamed without changes.
File renamed without changes.
File renamed without changes.
File renamed without changes.
File renamed without changes.
245 changes: 2 additions & 243 deletions src/config.rs
Original file line number Diff line number Diff line change
Expand Up @@ -2,10 +2,7 @@ use ratatui::{
style::{Color, Modifier, Style},
widgets::BorderType,
};
use serde::{
de::{self, IntoDeserializer},
Deserialize,
};
use serde::Deserialize;

#[derive(Debug, Deserialize)]
#[serde(default)]
Expand All @@ -26,56 +23,37 @@ impl Default for Config {
#[derive(Debug, Deserialize)]
#[serde(default)]
pub struct Theme {
#[serde(deserialize_with = "deserialize_style")]
pub default: Style,
#[serde(deserialize_with = "deserialize_style")]
pub title: Style,

// test widget
#[serde(deserialize_with = "deserialize_style")]
pub input_border: Style,
#[serde(deserialize_with = "deserialize_style")]
pub prompt_border: Style,

#[serde(deserialize_with = "deserialize_border_type")]
#[serde(skip)]
pub border_type: BorderType,

#[serde(deserialize_with = "deserialize_style")]
pub prompt_correct: Style,
#[serde(deserialize_with = "deserialize_style")]
pub prompt_incorrect: Style,
#[serde(deserialize_with = "deserialize_style")]
pub prompt_untyped: Style,

#[serde(deserialize_with = "deserialize_style")]
pub prompt_current_correct: Style,
#[serde(deserialize_with = "deserialize_style")]
pub prompt_current_incorrect: Style,
#[serde(deserialize_with = "deserialize_style")]
pub prompt_current_untyped: Style,

#[serde(deserialize_with = "deserialize_style")]
pub prompt_cursor: Style,

// results widget
#[serde(deserialize_with = "deserialize_style")]
pub results_overview: Style,
#[serde(deserialize_with = "deserialize_style")]
pub results_overview_border: Style,

#[serde(deserialize_with = "deserialize_style")]
pub results_worst_keys: Style,
#[serde(deserialize_with = "deserialize_style")]
pub results_worst_keys_border: Style,

#[serde(deserialize_with = "deserialize_style")]
pub results_chart: Style,
#[serde(deserialize_with = "deserialize_style")]
pub results_chart_x: Style,
#[serde(deserialize_with = "deserialize_style")]
pub results_chart_y: Style,

#[serde(deserialize_with = "deserialize_style")]
pub results_restart_prompt: Style,
}

Expand Down Expand Up @@ -129,222 +107,3 @@ impl Default for Theme {
}
}
}

fn deserialize_style<'de, D>(deserializer: D) -> Result<Style, D::Error>
where
D: de::Deserializer<'de>,
{
struct StyleVisitor;
impl de::Visitor<'_> for StyleVisitor {
type Value = Style;

fn expecting(&self, formatter: &mut std::fmt::Formatter) -> std::fmt::Result {
formatter.write_str("a string describing a text style")
}

fn visit_str<E: de::Error>(self, value: &str) -> Result<Self::Value, E> {
let (colors, modifiers) = value.split_once(';').unwrap_or((value, ""));
let (fg, bg) = colors.split_once(':').unwrap_or((colors, "none"));

let mut style = Style {
fg: match fg {
"none" | "" => None,
_ => Some(deserialize_color(fg.into_deserializer())?),
},
bg: match bg {
"none" | "" => None,
_ => Some(deserialize_color(bg.into_deserializer())?),
},
..Default::default()
};

for modifier in modifiers.split_terminator(';') {
style = style.add_modifier(match modifier {
"bold" => Modifier::BOLD,
"crossed_out" => Modifier::CROSSED_OUT,
"dim" => Modifier::DIM,
"hidden" => Modifier::HIDDEN,
"italic" => Modifier::ITALIC,
"rapid_blink" => Modifier::RAPID_BLINK,
"slow_blink" => Modifier::SLOW_BLINK,
"reversed" => Modifier::REVERSED,
"underlined" => Modifier::UNDERLINED,
_ => {
return Err(E::invalid_value(
de::Unexpected::Str(modifier),
&"a style modifier",
))
}
});
}

Ok(style)
}
}

deserializer.deserialize_str(StyleVisitor)
}

fn deserialize_color<'de, D>(deserializer: D) -> Result<Color, D::Error>
where
D: de::Deserializer<'de>,
{
struct ColorVisitor;
impl de::Visitor<'_> for ColorVisitor {
type Value = Color;

fn expecting(&self, formatter: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
formatter.write_str("a color name or hexadecimal color code")
}

fn visit_str<E: de::Error>(self, value: &str) -> Result<Self::Value, E> {
match value {
"reset" => Ok(Color::Reset),
"black" => Ok(Color::Black),
"white" => Ok(Color::White),
"red" => Ok(Color::Red),
"green" => Ok(Color::Green),
"yellow" => Ok(Color::Yellow),
"blue" => Ok(Color::Blue),
"magenta" => Ok(Color::Magenta),
"cyan" => Ok(Color::Cyan),
"gray" => Ok(Color::Gray),
"darkgray" => Ok(Color::DarkGray),
"lightred" => Ok(Color::LightRed),
"lightgreen" => Ok(Color::LightGreen),
"lightyellow" => Ok(Color::LightYellow),
"lightblue" => Ok(Color::LightBlue),
"lightmagenta" => Ok(Color::LightMagenta),
"lightcyan" => Ok(Color::LightCyan),
_ => {
if value.len() == 6 {
let parse_error = |_| E::custom("color code was not valid hexadecimal");

Ok(Color::Rgb(
u8::from_str_radix(&value[0..2], 16).map_err(parse_error)?,
u8::from_str_radix(&value[2..4], 16).map_err(parse_error)?,
u8::from_str_radix(&value[4..6], 16).map_err(parse_error)?,
))
} else {
Err(E::invalid_value(
de::Unexpected::Str(value),
&"a color name or hexadecimal color code",
))
}
}
}
}
}

deserializer.deserialize_str(ColorVisitor)
}

fn deserialize_border_type<'de, D>(deserializer: D) -> Result<BorderType, D::Error>
where
D: de::Deserializer<'de>,
{
struct BorderTypeVisitor;
impl de::Visitor<'_> for BorderTypeVisitor {
type Value = BorderType;

fn expecting(&self, formatter: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
formatter.write_str("a border type")
}

fn visit_str<E: de::Error>(self, value: &str) -> Result<Self::Value, E> {
match value {
"plain" => Ok(BorderType::Plain),
"rounded" => Ok(BorderType::Rounded),
"double" => Ok(BorderType::Double),
"thick" => Ok(BorderType::Thick),
"quadrantinside" => Ok(BorderType::QuadrantInside),
"quadrantoutside" => Ok(BorderType::QuadrantOutside),
_ => Err(E::invalid_value(
de::Unexpected::Str(value),
&"a border type",
)),
}
}
}

deserializer.deserialize_str(BorderTypeVisitor)
}

#[cfg(test)]
mod tests {
use super::*;

#[test]
fn deserializes_basic_colors() {
fn color(string: &str) -> Color {
deserialize_color(de::IntoDeserializer::<de::value::Error>::into_deserializer(
string,
))
.expect("failed to deserialize color")
}

assert_eq!(color("black"), Color::Black);
assert_eq!(color("000000"), Color::Rgb(0, 0, 0));
assert_eq!(color("ffffff"), Color::Rgb(0xff, 0xff, 0xff));
assert_eq!(color("FFFFFF"), Color::Rgb(0xff, 0xff, 0xff));
}

#[test]
fn deserializes_styles() {
fn style(string: &str) -> Style {
deserialize_style(de::IntoDeserializer::<de::value::Error>::into_deserializer(
string,
))
.expect("failed to deserialize style")
}

assert_eq!(style("none"), Style::default());
assert_eq!(style("none:none"), Style::default());
assert_eq!(style("none:none;"), Style::default());

assert_eq!(style("black"), Style::default().fg(Color::Black));
assert_eq!(
style("black:white"),
Style::default().fg(Color::Black).bg(Color::White)
);

assert_eq!(
style("none;bold"),
Style::default().add_modifier(Modifier::BOLD)
);
assert_eq!(
style("none;bold;italic;underlined;"),
Style::default()
.add_modifier(Modifier::BOLD)
.add_modifier(Modifier::ITALIC)
.add_modifier(Modifier::UNDERLINED)
);

assert_eq!(
style("00ff00:000000;bold;dim;italic;slow_blink"),
Style::default()
.fg(Color::Rgb(0, 0xff, 0))
.bg(Color::Rgb(0, 0, 0))
.add_modifier(Modifier::BOLD)
.add_modifier(Modifier::DIM)
.add_modifier(Modifier::ITALIC)
.add_modifier(Modifier::SLOW_BLINK)
);
}

#[test]
fn deserializes_border_types() {
fn border_type(string: &str) -> BorderType {
deserialize_border_type(de::IntoDeserializer::<de::value::Error>::into_deserializer(
string,
))
.expect("failed to deserialize border type")
}
assert_eq!(border_type("plain"), BorderType::Plain);
assert_eq!(border_type("rounded"), BorderType::Rounded);
assert_eq!(border_type("double"), BorderType::Double);
assert_eq!(border_type("thick"), BorderType::Thick);
assert_eq!(border_type("quadrantinside"), BorderType::QuadrantInside);
assert_eq!(border_type("quadrantoutside"), BorderType::QuadrantOutside);
}
}
12 changes: 6 additions & 6 deletions src/main.rs
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,7 @@ use rand::{seq::SliceRandom, thread_rng};
use ratatui::{
backend::CrosstermBackend,
layout::{Constraint, Direction, Layout},
terminal::Terminal,
Terminal,
};
use rust_embed::RustEmbed;
use std::{
Expand All @@ -29,7 +29,7 @@ use std::{
};

#[derive(RustEmbed)]
#[folder = "resources/runtime"]
#[folder = "resources/languages"]
struct Resources;

#[derive(Debug, Parser)]
Expand Down Expand Up @@ -201,11 +201,11 @@ impl State {
&self,
terminal: &mut Terminal<B>,
config: &Config,
) -> io::Result<()> {
) -> Result<(), B::Error> {
match self {
State::Test(test) => {
terminal.draw(|f| {
let area = f.size();
let area = f.area();
f.render_widget(config.theme.apply_to(test), area);

// Position cursor at end of input for IME composition support
Expand All @@ -219,12 +219,12 @@ impl State {
ratatui::text::Line::from(test.words[test.current_word].progress.as_str())
.width() as u16;
let max_cursor_x = chunks[0].right().saturating_sub(2);
f.set_cursor((inner_x + progress_width).min(max_cursor_x), inner_y);
f.set_cursor_position(((inner_x + progress_width).min(max_cursor_x), inner_y));
})?;
}
State::Results(results) => {
terminal.draw(|f| {
f.render_widget(config.theme.apply_to(results), f.size());
f.render_widget(config.theme.apply_to(results), f.area());
})?;
}
}
Expand Down
2 changes: 1 addition & 1 deletion src/ui.rs
Original file line number Diff line number Diff line change
Expand Up @@ -410,7 +410,7 @@ impl ThemedWidget for &results::Results {
(y_label_min..y_label_max)
.step_by(5)
.map(|n| Span::raw(format!("{}", n)))
.collect(),
.collect::<Vec<_>>(),
),
);
wpm_chart.render(res_chunks[1], buf);
Expand Down
Loading