typing_test/
lib.rs

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
pub mod data_provider;
pub mod screen;
use self::screen::ThemeName;
use data_provider::Data;

use serde::{Deserialize, Serialize};

use std::env::{self, Args};
use std::error::Error;
use std::path::{Path, PathBuf};
use std::{fs, io};

pub fn parse_args(mut args: Args) -> Result<(Data, Config), Box<dyn Error>> {
    args.next();

    let mut words_file = None;
    let mut quotes_file = None;

    let mut config_fname: PathBuf = [env::var("HOME").unwrap().as_str(), ".typing_test.toml"]
        .iter()
        .collect();

    while let Some(arg) = args.next() {
        match arg.as_str() {
            "--words" | "-w" => {
                if let Some(f) = args.next() {
                    words_file = Some(f);
                };
            }
            "--quotes" | "-q" => {
                if let Some(f) = args.next() {
                    quotes_file = Some(f);
                }
            }
            //"--online" => return Data::new_online(words_file),
            "--help" | "-h" => help(),
            "--config" | "-c" => {
                if let Some(f) = args.next() {
                    config_fname = PathBuf::from(f);
                }
            }
            _ => (),
        }
    }

    let data = Data::new_offline(words_file, quotes_file)?;

    Ok((data, Config::new(config_fname.as_path())))
}

fn help() {
    let help = [
        "Usage: typing_test [-w <file>] [-q <file>] [-c <file>]\n",
        "Test your typing speed from built-in words and quotes, or provide your own words and quotes. You can also choose to get quotes from scrapping popular quotes on the Internet (not implemented yet).\n",
        "Controls:",
        "   <Tab>                   Cycle forward between buttons.",
        "   <Shift-Tab>             Cycle backward between buttons.",
        "   <Enter>                 Click selected button. Alternatively, you can use your mouse to click on buttons.",
        "   <Super>=                Increase font size.",
        "   <Super>-                Decrease font size.",
        "   <Super>0                Reset font size.\n",
        "Options:",
        "   -w, --words <file>      Provide your own words file.",
        "                           Words are separated by a new line character.",
        "   -q, --quotes <file>     Provide your own quotes file.",
        "                           Sources are separated by 2 new line characters.",
        "                           The first line is the source name, and each quote belonging",
        "                           to that source is separated by a new line character.",
        "                           See built-in quotes at the repository.",
        //"   -o, --online            Get quotes from the web. Must be connected to the internet.",
        "   -c, --config <file>     Provide custom config file. Default is ~/.typing_test.toml",
        "   -h, --help              Print this help.",
    ]
    .join("\n");

    println!("{}", help);
    std::process::exit(1);
}

#[derive(Serialize, Deserialize, Debug)]
pub struct ThemeParams {
    bg_color: u32,
    text_color: u32,
    error_color: u32,
    ghost_color: u32,
}

// TODO: extra themes need to be able to be selected
#[derive(Serialize, Deserialize, Debug, Clone)]
pub struct Config {
    #[serde(skip)]
    pub config_file: PathBuf,

    pub theme: ThemeName,
    pub font_size: f32,
    // extra_themes: HashMap<String, ThemeParams>,
}

impl Default for Config {
    fn default() -> Self {
        Config {
            config_file: [env::var("HOME").unwrap().as_str(), ".typing_test.toml"]
                .iter()
                .collect(),
            theme: ThemeName::default(),
            font_size: 30.0,
        }
    }
}

impl Config {
    pub fn new(filename: &Path) -> Self {
        match fs::read_to_string(filename) {
            Ok(s) => {
                let mut config = match toml::from_str::<Config>(&s) {
                    Ok(c) => c,
                    Err(e) => {
                        println!("Config Error, using defaults. {}", e);
                        Config::default()
                    }
                };
                config.config_file = filename.to_path_buf();
                config
            }
            Err(e) => {
                match e.kind() {
                    io::ErrorKind::NotFound => {
                        let config_fname: PathBuf =
                            [env::var("HOME").unwrap().as_str(), ".typing_test.toml"]
                                .iter()
                                .collect();

                        if let Err(e) =
                            fs::write(config_fname, toml::to_string(&Config::default()).unwrap())
                        {
                            println!("Can't create default config file. {}", e);
                        };
                    }
                    _ => {
                        println!("Can't read config file, using defaults. {}", e.kind());
                    }
                }
                Config::default()
            }
        }
    }

    pub fn update_file(&self) {
        if let Err(e) = fs::write(&self.config_file, toml::to_string(&self).unwrap()) {
            println!("Can't write to config file. {}", e);
        };
    }
}