Commit c123dbc1 authored by Darrien Glasser's avatar Darrien Glasser

Update dependencies, and add toml parser

In preparation for config files
parent ea5d07a0
Pipeline #79579453 passed with stage
in 3 minutes and 46 seconds
image: rust:1.36.0-stretch
image: rust:1.37.0-stretch
lint:
script:
......
This diff is collapsed.
[package]
name = "typeracer"
version = "1.1.1"
version = "1.2.0"
authors = ["DarrienG <darrienglasser@outlook.com>", "Jeffas"]
edition = "2018"
# Packaging for crates.io
......@@ -40,8 +40,10 @@ tui = "0.6.2"
termion = "1.5"
unicode-width = "0.1.5"
# For storing lang pack
directories = "1.0"
# For lang pack and config
directories = "2.0.2"
toml = "0.5.3"
serde = { version = "1.0", features = ["derive"] }
# For downloading lang pack
git2 = "0.9"
......
......@@ -37,6 +37,46 @@ $ typeracer -r $(echo 'racing using a passage from elsewhere')
Hit ^C at any time to quit.
## Configuration
What good would a typing game be without a config file?
Where you can find your config file:
Linux:
```
~/.config/typeracer/config.toml
```
And roughly in:
macOS:
```
$HOME/Library/Preferences
```
Windows;
```
{FOLDERID_RoamingAppData}
```
-- Note typeracer uses whatever the proper mechanism is for data and config
folders are for your OS. If you customized the variables used, it may be
elsewhere.
With your config, you can enable or disable language packs.
```toml
[lang_packs]
whitelisted = ["default"]
```
```toml
[lang_packs]
blacklisted = ["harry-potter"]
```
## Building
You need rust version 1.33.0 or higher (using some newer time APIs) and git. If
you're on macOS, you'll probably need to install openssl too.
......
use directories::ProjectDirs;
use serde::Deserialize;
use std::fs;
use std::fs::File;
use std::io::Error;
use std::io::Read;
use std::path::PathBuf;
#[derive(Debug, Deserialize)]
pub struct TyperacerConfig {
pub lang_packs: Option<LangPacks>,
}
#[derive(Debug, Deserialize)]
pub struct LangPacks {
pub whitelisted: Option<Vec<String>>,
pub blacklisted: Option<Vec<String>>,
}
mod validator;
pub fn get_config() -> Result<TyperacerConfig, Error> {
validator::validate_config(get_config_raw())
}
fn get_config_raw() -> TyperacerConfig {
let config_buf = get_config_file();
let mut file_contents = "".to_owned();
File::open(config_buf)
.expect("Unable to open config file")
.read_to_string(&mut file_contents)
.expect("Unable to read config file");
toml::from_str(&file_contents).expect("Unable to parse config file to valid toml")
}
fn get_config_file() -> PathBuf {
let mut config_dir = create_config_dir();
config_dir.push("config.toml");
if config_dir.exists() {
config_dir
} else {
File::create(&config_dir).unwrap();
config_dir
}
}
fn create_config_dir() -> PathBuf {
let dirs = ProjectDirs::from("org", "darrienglasser.com", "typeracer").unwrap();
fs::create_dir_all(dirs.config_dir()).expect("Failed to create config dir");
PathBuf::from(dirs.config_dir())
}
use crate::config::TyperacerConfig;
use std::io::{Error, ErrorKind};
/// Validates that the given config is valid.
/// If config is not valid, returns an err rather than the
/// user's config.
pub fn validate_config(config: TyperacerConfig) -> Result<TyperacerConfig, Error> {
let lang_pack_check = validate_lang_packs(&config);
if lang_pack_check.is_err() {
return Err(lang_pack_check.unwrap_err());
}
Ok(config)
}
/// Validate whether the lang_packs section is valid
/// Having no lang_packs config is valid, as is having
/// neither the whitelisted or blacklisted section filled out is valid.
/// Having both a blacklisted and whitelisted section is invalid.
fn validate_lang_packs(config: &TyperacerConfig) -> Result<(), Error> {
match &config.lang_packs {
None => Ok(()),
Some(x) => {
if x.blacklisted.is_some() && x.whitelisted.is_some() {
Err(Error::new(
ErrorKind::Other,
"Both blacklist and whitelist cannot be filled out",
))
} else {
Ok(())
}
}
}
}
#[cfg(test)]
mod tests {
use super::*;
use crate::config::LangPacks;
#[test]
fn test_empty_config_ok() {
assert!(validate_config(TyperacerConfig { lang_packs: None }).is_ok());
}
#[test]
fn test_exclusive_blacklistwhitelist() {
assert!(validate_config(TyperacerConfig {
lang_packs: Some(LangPacks {
whitelisted: Some(vec!["vrinda".to_string(), "punj".to_string()]),
blacklisted: Some(vec!["tub".to_owned(), "golang".to_owned()]),
}),
})
.is_err());
}
#[test]
fn test_blacklist_or_whitelist_ok() {
assert!(validate_config(TyperacerConfig {
lang_packs: Some(LangPacks {
whitelisted: Some(vec!["vrinda".to_owned(), "punj".to_owned()]),
blacklisted: None,
}),
})
.is_ok());
assert!(validate_config(TyperacerConfig {
lang_packs: Some(LangPacks {
whitelisted: None,
blacklisted: Some(vec!["tub".to_owned(), "golang".to_owned()]),
}),
})
.is_ok());
}
}
......@@ -11,6 +11,7 @@ mod dirs {
}
pub mod actions;
pub mod config;
pub mod stats;
use actions::Action;
......@@ -32,7 +33,7 @@ fn get_version() -> &'static str {
#[cfg(not(debug_assertions))]
fn get_version() -> &'static str {
"1.1.1"
"1.2.0"
}
fn get_lang_pack_version() -> &'static str {
......@@ -40,6 +41,13 @@ fn get_lang_pack_version() -> &'static str {
}
fn main() -> Result<(), Error> {
// Check config before doing anything else
let config_result = config::get_config();
if config_result.is_err() {
return Err(config_result.unwrap_err());
}
let typeracer_config = config_result.unwrap();
let args = clap::App::new("Terminal typing game. Type through passages to see what the fastest times are you can get!")
.version(&*format!("Typeracer version: {}, lang pack version: {}", get_version(), get_lang_pack_version()))
.author("Darrien Glasser <me@darrien.dev>")
......@@ -79,14 +87,13 @@ fn main() -> Result<(), Error> {
)
.get_matches();
let mut passage_controller = passage_controller::Controller::new(20);
let mut passage_controller = passage_controller::Controller::new(20, &typeracer_config);
if args.is_present("SHOW_PACKS") {
let dirs = passage_controller.get_quote_dir_shortnames();
let (filtered_dirs, all_dirs) = passage_controller.get_quote_dir_shortnames();
for dir in dirs {
println!("{}", dir)
}
println!("Enabled packs:\t{}", filtered_dirs.join(", "));
println!("All packs:\t{}", all_dirs.join(", "));
return Ok(());
}
......@@ -99,7 +106,7 @@ fn main() -> Result<(), Error> {
if word == " " || word == "\n" {
continue;
} else {
constructed_string.push_str(word);
constructed_string.push_str(word.trim());
constructed_string.push_str(" ");
}
}
......
......@@ -3,6 +3,7 @@ use std::fs::{read_dir, DirEntry, File};
use std::io::{BufRead, BufReader};
use crate::actions::Action;
use crate::config::TyperacerConfig;
use crate::dirs::setup_dirs;
#[derive(Debug, Clone, PartialEq)]
......@@ -13,18 +14,19 @@ pub struct PassageInfo {
}
#[derive(Debug, Clone)]
pub struct Controller {
pub struct Controller<'a> {
passages: Vec<PassageInfo>,
current_passage_idx: usize,
history_size: usize,
start_idx: usize,
first_run: bool,
config: &'a TyperacerConfig,
}
/// A slightly smarter ringbuffer for preserving history
/// Saves the last 20 passages as history.
impl Controller {
pub fn new(history_size: usize) -> Self {
impl<'a> Controller<'a> {
pub fn new(history_size: usize, config: &'a TyperacerConfig) -> Self {
// We want to initialize one value in the vector before we start.
// We could do all history_size, but not lazy loading with bigger values
// could be expensive.
......@@ -34,6 +36,7 @@ impl Controller {
history_size,
start_idx: 0,
first_run: true,
config,
}
}
......@@ -106,14 +109,33 @@ impl Controller {
});
}
fn pick_quote_dir(&self) -> DirEntry {
let mut quote_dirs = self.get_quote_dirs();
quote_dirs.remove(rand::thread_rng().gen_range(0, quote_dirs.len()))
fn pick_quote_dir(&self) -> Option<DirEntry> {
let mut quote_dirs = self.get_filtered_quote_dirs();
if quote_dirs.is_empty() {
None
} else {
Some(quote_dirs.remove(rand::thread_rng().gen_range(0, quote_dirs.len())))
}
}
/// Get shortnames of quote directories.
pub fn get_quote_dir_shortnames(&self) -> Vec<String> {
let mut dirs: Vec<String> = self
/// Get shortnames of quote directories
/// returns enabled quote dirs first, all quote dirs second
pub fn get_quote_dir_shortnames(&self) -> (Vec<String>, Vec<String>) {
let mut filtered_dirs: Vec<String> = self
.get_filtered_quote_dirs()
.iter()
.map(|dir| {
dir.path()
.file_stem()
.expect("Unable to get file")
.to_string_lossy()
.to_string()
})
.collect();
filtered_dirs.sort();
let mut all_dirs: Vec<String> = self
.get_quote_dirs()
.iter()
.map(|dir| {
......@@ -125,8 +147,12 @@ impl Controller {
})
.collect();
dirs.sort();
dirs
all_dirs.sort();
(filtered_dirs, all_dirs)
}
fn get_filtered_quote_dirs(&self) -> Vec<DirEntry> {
self.filter_user_dirs(self.get_quote_dirs())
}
fn get_quote_dirs(&self) -> Vec<DirEntry> {
......@@ -138,6 +164,81 @@ impl Controller {
)
}
fn filter_user_dirs(&self, entries: Vec<DirEntry>) -> Vec<DirEntry> {
if self.config.lang_packs.is_some() {
if self
.config
.lang_packs
.as_ref()
.unwrap()
.blacklisted
.is_some()
{
self.filter_blacklist(entries)
} else {
self.filter_whitelist(entries)
}
} else {
entries
}
}
fn filter_blacklist(&self, entries: Vec<DirEntry>) -> Vec<DirEntry> {
let fallback_blacklist = vec![];
let blacklist = self
.config
.lang_packs
.as_ref()
.unwrap()
.blacklisted
.as_ref()
.unwrap_or(&fallback_blacklist);
let mut filtered_quote_dirs: Vec<DirEntry> = vec![];
for entry in entries {
let str_entry = entry
.path()
.file_stem()
.unwrap()
.to_str()
.unwrap()
.to_string();
if !blacklist.contains(&str_entry) {
filtered_quote_dirs.push(entry);
}
}
filtered_quote_dirs
}
fn filter_whitelist(&self, entries: Vec<DirEntry>) -> Vec<DirEntry> {
let fallback_whitelist = vec![];
let whitelist = self
.config
.lang_packs
.as_ref()
.unwrap()
.whitelisted
.as_ref()
.unwrap_or(&fallback_whitelist);
if whitelist.is_empty() || whitelist[0] == "*" {
entries
} else {
let mut filtered_quote_dirs: Vec<DirEntry> = vec![];
for entry in entries {
let str_entry = entry
.path()
.file_stem()
.unwrap()
.to_str()
.unwrap()
.to_string();
if whitelist.contains(&str_entry) {
filtered_quote_dirs.push(entry);
}
}
filtered_quote_dirs
}
}
fn without_bad_paths(&self, entries: Vec<DirEntry>) -> Vec<DirEntry> {
let mut true_quote_dirs: Vec<DirEntry> = vec![];
for entry in entries {
......@@ -162,16 +263,24 @@ impl Controller {
// Difficult to test with unit tests. Expects a database file.
#[cfg(not(test))]
fn get_new_passage(&self) -> PassageInfo {
let quote_dir = self.pick_quote_dir();
let num_files: usize = read_dir(quote_dir.path()).unwrap().count();
let random_file_num = rand::thread_rng().gen_range(0, num_files);
let fallback = PassageInfo {
passage: "The quick brown fox jumps over the lazy dog".to_owned(),
title: "darrienglasser.com".to_owned(),
passage_path: "FALLBACK_PATH".to_owned(),
};
let quote_opt = self.pick_quote_dir();
if quote_opt.is_none() {
return fallback;
}
let quote_dir = quote_opt.unwrap();
let num_files: usize = read_dir(quote_dir.path()).unwrap().count();
if num_files > 0 {
let random_file_num = rand::thread_rng().gen_range(0, num_files);
let read_dir_iter = quote_dir.path();
for (count, path) in read_dir(read_dir_iter)
.expect("Failed to read from data dir")
......@@ -220,7 +329,7 @@ mod tests {
fn test_get_next_passage_overwrite() {
// Check to see if we keep asking for next passages they're always valid
// History of 5 so we can loop through history multiple times
let mut passage_controller = Controller::new(5);
let mut passage_controller = Controller::new(5, &TyperacerConfig { lang_packs: None });
for _ in 0..4000 {
passage_controller.retrieve_next_passage();
}
......@@ -235,7 +344,7 @@ mod tests {
// Since we return a reference to a passage_info, and these methods require a mutable
// reference, we have to clone to make the borrow checker happy.
let mut passage_controller = Controller::new(5);
let mut passage_controller = Controller::new(5, &TyperacerConfig { lang_packs: None });
passage_controller.retrieve_next_passage();
let mut previous_passage = (*passage_controller.retrieve_previous_passage()).clone();
for _ in 0..4000 {
......@@ -247,7 +356,7 @@ mod tests {
#[test]
fn test_verify_history_integrity() {
let mut passage_controller = Controller::new(5);
let mut passage_controller = Controller::new(5, &TyperacerConfig { lang_packs: None });
passage_controller.retrieve_next_passage();
let passage0 = (*passage_controller.retrieve_passage(Action::PreviousPassage)).clone();
let passage1 = (*passage_controller.retrieve_passage(Action::NextPassage)).clone();
......@@ -278,7 +387,7 @@ mod tests {
#[test]
fn test_verify_restart() {
let mut passage_controller = Controller::new(5);
let mut passage_controller = Controller::new(5, &TyperacerConfig { lang_packs: None });
passage_controller.retrieve_next_passage();
// restarting on the initial passage doesn't break and gives the correct passage
......
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment