Init
This commit is contained in:
commit
b7299f7c1c
12
.gitignore
vendored
Normal file
12
.gitignore
vendored
Normal file
|
@ -0,0 +1,12 @@
|
|||
# Rust
|
||||
target/
|
||||
|
||||
# Nix
|
||||
result
|
||||
|
||||
# Direnv
|
||||
.direnv/
|
||||
|
||||
# Editor
|
||||
.vim/
|
||||
.nvimrc
|
1909
Cargo.lock
generated
Normal file
1909
Cargo.lock
generated
Normal file
File diff suppressed because it is too large
Load diff
38
Cargo.toml
Normal file
38
Cargo.toml
Normal file
|
@ -0,0 +1,38 @@
|
|||
[package]
|
||||
name = "dttyper"
|
||||
description = "Terminal-based typing test."
|
||||
version = "1.4.2"
|
||||
readme = "README.md"
|
||||
repository = "https://git.berryez.xyz/berry/dttyper.git"
|
||||
homepage = "https://git.berryez.xyz/berry/dttyper"
|
||||
license = "MIT"
|
||||
authors = [
|
||||
"Max Niederman <max@maxniederman.com>",
|
||||
"berry <neurofen@fedora.email>",
|
||||
]
|
||||
edition = "2021"
|
||||
|
||||
[dependencies]
|
||||
reqwest = { version = "0.11.25", features = ["blocking"] }
|
||||
|
||||
structopt = "^0.3"
|
||||
dirs = "^5.0"
|
||||
crossterm = "^0.27"
|
||||
rust-embed = "^8.2"
|
||||
toml = "^0.8"
|
||||
serde_json = "1.0.114"
|
||||
|
||||
chrono = "0.4.35"
|
||||
[dependencies.ratatui]
|
||||
version = "^0.25"
|
||||
|
||||
[dependencies.rand]
|
||||
version = "^0.8"
|
||||
features = ["alloc"]
|
||||
|
||||
[dependencies.serde]
|
||||
version = "^1.0"
|
||||
features = ["derive"]
|
||||
|
||||
[build-dependencies]
|
||||
dirs = "^5.0"
|
21
LICENSE.md
Normal file
21
LICENSE.md
Normal file
|
@ -0,0 +1,21 @@
|
|||
MIT License
|
||||
|
||||
Copyright (c) 2022 Max Niederman
|
||||
|
||||
Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||
of this software and associated documentation files (the "Software"), to deal
|
||||
in the Software without restriction, including without limitation the rights
|
||||
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
||||
copies of the Software, and to permit persons to whom the Software is
|
||||
furnished to do so, subject to the following conditions:
|
||||
|
||||
The above copyright notice and this permission notice shall be included in all
|
||||
copies or substantial portions of the Software.
|
||||
|
||||
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
||||
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
||||
SOFTWARE.
|
236
README.md
Normal file
236
README.md
Normal file
|
@ -0,0 +1,236 @@
|
|||
# dttyper
|
||||
|
||||
dttyper is a terminal-based typing test built with Rust and tui-rs forked from [ttyper](https://github.com/max-niederman/ttyper), that exports each tests into an influx database.
|
||||
|
||||
![Recording](./resources/recording.gif)
|
||||
|
||||
## installation
|
||||
|
||||
```sh
|
||||
cargo build
|
||||
cargo install --path .
|
||||
```
|
||||
|
||||
# Develop (using crate2nix)
|
||||
|
||||
a. Enter dev shell via nix
|
||||
|
||||
```bash
|
||||
nix develop
|
||||
```
|
||||
|
||||
b. modify code and then run with nix
|
||||
|
||||
```bash
|
||||
nix run
|
||||
```
|
||||
|
||||
## usage
|
||||
|
||||
For usage instructions, you can run `dttyper --help`:
|
||||
|
||||
```
|
||||
dttyper
|
||||
Terminal-based typing test.
|
||||
|
||||
USAGE:
|
||||
dttyper [FLAGS] [OPTIONS] [contents]
|
||||
|
||||
FLAGS:
|
||||
-d, --debug
|
||||
-h, --help Prints help information
|
||||
--list-languages List installed languages
|
||||
--no-backtrack Disable backtracking to completed words
|
||||
-V, --version Prints version information
|
||||
|
||||
OPTIONS:
|
||||
-c, --config <config> Use config file
|
||||
-l, --language <language> Specify test language
|
||||
--language-file <language-file> Specify test language in file
|
||||
-w, --words <words> Specify word count [default: 50]
|
||||
|
||||
ARGS:
|
||||
<contents>
|
||||
```
|
||||
|
||||
### examples
|
||||
|
||||
| command | test contents |
|
||||
| :------------------------------ | ----------------------------------------: |
|
||||
| `dttyper` | 50 of the 200 most common english words |
|
||||
| `dttyper -w 100` | 100 of the 200 most common English words |
|
||||
| `dttyper -w 100 -l english1000` | 100 of the 1000 most common English words |
|
||||
| `dttyper --language-file lang` | 50 random words from the file `lang` |
|
||||
| `dttyper text.txt` | contents of `text.txt` split at newlines |
|
||||
|
||||
## languages
|
||||
|
||||
The following languages are available by default:
|
||||
|
||||
| name | description |
|
||||
| :----------------- | ----------------------------------: |
|
||||
| `c` | The C programming language |
|
||||
| `csharp` | The C# programming language |
|
||||
| `english100` | 100 most common English words |
|
||||
| `english200` | 200 most common English words |
|
||||
| `english1000` | 1000 most common English words |
|
||||
| `english5000` | 5000 most common English words |
|
||||
| `english10000` | 10000 most common English words |
|
||||
| `english-advanced` | Advanced English words |
|
||||
| `english-pirate` | 50 pirate speak English words |
|
||||
| `german` | 207 most common German words |
|
||||
| `german1000` | 1000 most common German words |
|
||||
| `german10000` | 10000 most common German words |
|
||||
| `go` | The Go programming language |
|
||||
| `html` | HyperText Markup Language |
|
||||
| `java` | The Java programming language |
|
||||
| `javascript` | The Javascript programming language |
|
||||
| `norwegian` | 200 most common Norwegian words |
|
||||
| `php` | The PHP programming language |
|
||||
| `portuguese` | 100 most common Portuguese words |
|
||||
| `python` | The Python programming language |
|
||||
| `qt` | The QT GUI framework |
|
||||
| `ruby` | The Ruby programming language |
|
||||
| `rust` | The Rust programming language |
|
||||
| `spanish` | 100 most common Spanish words |
|
||||
| `ukrainian` | 100 most common Ukrainian words |
|
||||
|
||||
Additional languages can be added by creating a file in `DTTYPER_CONFIG_DIR/language` with a word on each line. On Linux, the config directory is `$HOME/.config/dttyper`; on Windows, it's `C:\Users\user\AppData\Roaming\dttyper`; and on macOS it's `$HOME/Library/Application Support/dttyper`.
|
||||
|
||||
# Statistics review
|
||||
|
||||
A [grafana panel](./panel.json) can be imported to view your reports!
|
||||
![GrafanaStats](./resources/grafana.png)
|
||||
|
||||
## config
|
||||
|
||||
Configuration is specified by the `config.toml` file in the config directory (e.g. `$HOME/.config/dttyper/config.toml`).
|
||||
|
||||
The default values with explanations are below:
|
||||
|
||||
```toml
|
||||
default_language = "english1000"
|
||||
server = "http://localhost:8086"
|
||||
token = "token"
|
||||
bucket = "dttyper"
|
||||
org = "dttyper"
|
||||
keyboard = "Generic"
|
||||
|
||||
|
||||
[theme]
|
||||
# default style (this includes empty cells)
|
||||
default = "none"
|
||||
|
||||
# title text styling
|
||||
title = "white;bold"
|
||||
|
||||
## test styles ##
|
||||
|
||||
# input box border
|
||||
input_border = "cyan"
|
||||
# prompt box border
|
||||
prompt_border = "green"
|
||||
|
||||
# correctly typed words
|
||||
prompt_correct = "green"
|
||||
# incorrectly typed words
|
||||
prompt_incorrect = "red"
|
||||
# untyped words
|
||||
prompt_untyped = "gray"
|
||||
|
||||
# correctly typed letters in current word
|
||||
prompt_current_correct = "green;bold"
|
||||
# incorrectly typed letters in current word
|
||||
prompt_current_incorrect = "red;bold"
|
||||
# untyped letters in current word
|
||||
prompt_current_untyped = "blue;bold"
|
||||
|
||||
# cursor character
|
||||
prompt_cursor = "none;underlined"
|
||||
|
||||
## results styles ##
|
||||
|
||||
# overview text
|
||||
results_overview = "cyan;bold"
|
||||
# overview border
|
||||
results_overview_border = "cyan"
|
||||
|
||||
# worst keys text
|
||||
results_worst_keys = "cyan;bold"
|
||||
# worst keys border
|
||||
results_worst_keys_border = "cyan"
|
||||
|
||||
# results chart default (includes plotted data)
|
||||
results_chart = "cyan"
|
||||
# results chart x-axis label
|
||||
results_chart_x = "cyan"
|
||||
# results chart y-axis label
|
||||
results_chart_y = "gray;italic"
|
||||
|
||||
# restart/quit prompt in results ui
|
||||
results_restart_prompt = "gray;italic"
|
||||
```
|
||||
|
||||
### style format
|
||||
|
||||
The configuration uses a custom style format which can specify most [ANSI escape styling codes](<https://en.wikipedia.org/wiki/ANSI_escape_code#SGR_(Select_Graphic_Rendition)_parameters>), encoded as a string.
|
||||
|
||||
Styles begin with the color specification, which can be a single color (the foreground), or two colors separated by a colon (the foreground and background). Colors can be one of sixteen specified by your terminal, a 24-bit hex color code, `none`, or `reset`.
|
||||
|
||||
After the colors, you can optionally specify modifiers separated by a semicolon. A list of modifiers is below:
|
||||
|
||||
- `bold`
|
||||
- `crossed_out`
|
||||
- `dim`
|
||||
- `hidden`
|
||||
- `italic`
|
||||
- `rapid_blink`
|
||||
- `slow_blink`
|
||||
- `reversed`
|
||||
- `underlined`
|
||||
|
||||
Some examples:
|
||||
|
||||
- `blue:white;italic` specifies italic blue text on a white background.
|
||||
- `none;italic;bold;underlined` specifies underlined, italicized, and bolded text with no set color or background.
|
||||
- `00ff00:000000` specifies text of color `#00ff00` (pure green) on a background of `#000000` (pure black).
|
||||
|
||||
In [extended Backus-Naur form](https://en.wikipedia.org/wiki/Extended_Backus%E2%80%93Naur_form):
|
||||
|
||||
```ebnf
|
||||
style = colors, { ";", modifier }, [ ";" ] ;
|
||||
|
||||
colors = color, [ ":", color ] ;
|
||||
color = "none"
|
||||
| "reset"
|
||||
| "black"
|
||||
| "white"
|
||||
| "red"
|
||||
| "green"
|
||||
| "yellow"
|
||||
| "blue"
|
||||
| "magenta"
|
||||
| "cyan"
|
||||
| "gray"
|
||||
| "darkgray"
|
||||
| "lightred"
|
||||
| "lightgreen"
|
||||
| "lightyellow"
|
||||
| "lightblue"
|
||||
| "lightmagenta"
|
||||
| "lightcyan"
|
||||
| 6 * hex digit ;
|
||||
hex digit = ? hexadecimal digit; 1-9, a-z, and A-Z ? ;
|
||||
|
||||
modifier = "bold"
|
||||
| "crossed_out"
|
||||
| "dim"
|
||||
| "hidden"
|
||||
| "italic"
|
||||
| "rapid_blink"
|
||||
| "slow_blink"
|
||||
| "reversed"
|
||||
| "underlined" ;
|
||||
```
|
||||
|
||||
If you're familiar with [serde](https://serde.rs), you can also read [the deserialization code](./src/config.rs).
|
54
build.rs
Normal file
54
build.rs
Normal file
|
@ -0,0 +1,54 @@
|
|||
use std::env;
|
||||
use std::fs;
|
||||
use std::path::{Path, PathBuf};
|
||||
|
||||
fn copy<U: AsRef<Path>, V: AsRef<Path>>(from: U, to: V) -> std::io::Result<()> {
|
||||
let mut stack = vec![PathBuf::from(from.as_ref())];
|
||||
|
||||
let output_root = PathBuf::from(to.as_ref());
|
||||
let input_root = PathBuf::from(from.as_ref()).components().count();
|
||||
|
||||
while let Some(working_path) = stack.pop() {
|
||||
// Generate a relative path
|
||||
let src: PathBuf = working_path.components().skip(input_root).collect();
|
||||
|
||||
// Create a destination if missing
|
||||
let dest = if src.components().count() == 0 {
|
||||
output_root.clone()
|
||||
} else {
|
||||
output_root.join(&src)
|
||||
};
|
||||
if fs::metadata(&dest).is_err() {
|
||||
fs::create_dir_all(&dest)?;
|
||||
}
|
||||
|
||||
for entry in fs::read_dir(working_path)? {
|
||||
let entry = entry?;
|
||||
let path = entry.path();
|
||||
if path.is_dir() {
|
||||
stack.push(path);
|
||||
} else if let Some(filename) = path.file_name() {
|
||||
let dest_path = dest.join(filename);
|
||||
fs::copy(&path, &dest_path)?;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[allow(unused_must_use)]
|
||||
fn main() -> std::io::Result<()> {
|
||||
let install_path = dirs::config_dir()
|
||||
.expect("Couldn't find a configuration directory to install to.")
|
||||
.join("dttyper");
|
||||
fs::create_dir_all(&install_path);
|
||||
|
||||
let resources_path = env::current_dir()
|
||||
.expect("Couldn't find the source directory.")
|
||||
.join("resources")
|
||||
.join("runtime");
|
||||
copy(resources_path, &install_path);
|
||||
|
||||
Ok(())
|
||||
}
|
121
flake.lock
Normal file
121
flake.lock
Normal file
|
@ -0,0 +1,121 @@
|
|||
{
|
||||
"nodes": {
|
||||
"advisory-db": {
|
||||
"flake": false,
|
||||
"locked": {
|
||||
"lastModified": 1711896428,
|
||||
"narHash": "sha256-cZfXcw6dkd+00dOnD0tD/GLX7gEU/piVUF8SOKRIjf4=",
|
||||
"owner": "rustsec",
|
||||
"repo": "advisory-db",
|
||||
"rev": "799ff4a10673405b2334f6653519fb092aa99845",
|
||||
"type": "github"
|
||||
},
|
||||
"original": {
|
||||
"owner": "rustsec",
|
||||
"repo": "advisory-db",
|
||||
"type": "github"
|
||||
}
|
||||
},
|
||||
"crane": {
|
||||
"inputs": {
|
||||
"nixpkgs": [
|
||||
"nixpkgs"
|
||||
]
|
||||
},
|
||||
"locked": {
|
||||
"lastModified": 1712015038,
|
||||
"narHash": "sha256-opeWL/FPV7nnbfUavSWIDy+N5bUshF2CyJK6beVvjv4=",
|
||||
"owner": "ipetkov",
|
||||
"repo": "crane",
|
||||
"rev": "b245ee3472cbfd82394047b536e117a32b4c7850",
|
||||
"type": "github"
|
||||
},
|
||||
"original": {
|
||||
"owner": "ipetkov",
|
||||
"repo": "crane",
|
||||
"type": "github"
|
||||
}
|
||||
},
|
||||
"fenix": {
|
||||
"inputs": {
|
||||
"nixpkgs": [
|
||||
"nixpkgs"
|
||||
],
|
||||
"rust-analyzer-src": []
|
||||
},
|
||||
"locked": {
|
||||
"lastModified": 1712038998,
|
||||
"narHash": "sha256-bVIEz07/SLxPRRo+1G0cUd26KhoCj8yQc8myhf/93FM=",
|
||||
"owner": "nix-community",
|
||||
"repo": "fenix",
|
||||
"rev": "b1b59b4d908d3e64a7e923a7b434e94e03626ec0",
|
||||
"type": "github"
|
||||
},
|
||||
"original": {
|
||||
"owner": "nix-community",
|
||||
"repo": "fenix",
|
||||
"type": "github"
|
||||
}
|
||||
},
|
||||
"flake-utils": {
|
||||
"inputs": {
|
||||
"systems": "systems"
|
||||
},
|
||||
"locked": {
|
||||
"lastModified": 1710146030,
|
||||
"narHash": "sha256-SZ5L6eA7HJ/nmkzGG7/ISclqe6oZdOZTNoesiInkXPQ=",
|
||||
"owner": "numtide",
|
||||
"repo": "flake-utils",
|
||||
"rev": "b1d9ab70662946ef0850d488da1c9019f3a9752a",
|
||||
"type": "github"
|
||||
},
|
||||
"original": {
|
||||
"owner": "numtide",
|
||||
"repo": "flake-utils",
|
||||
"type": "github"
|
||||
}
|
||||
},
|
||||
"nixpkgs": {
|
||||
"locked": {
|
||||
"lastModified": 1711715736,
|
||||
"narHash": "sha256-9slQ609YqT9bT/MNX9+5k5jltL9zgpn36DpFB7TkttM=",
|
||||
"owner": "NixOS",
|
||||
"repo": "nixpkgs",
|
||||
"rev": "807c549feabce7eddbf259dbdcec9e0600a0660d",
|
||||
"type": "github"
|
||||
},
|
||||
"original": {
|
||||
"owner": "NixOS",
|
||||
"ref": "nixpkgs-unstable",
|
||||
"repo": "nixpkgs",
|
||||
"type": "github"
|
||||
}
|
||||
},
|
||||
"root": {
|
||||
"inputs": {
|
||||
"advisory-db": "advisory-db",
|
||||
"crane": "crane",
|
||||
"fenix": "fenix",
|
||||
"flake-utils": "flake-utils",
|
||||
"nixpkgs": "nixpkgs"
|
||||
}
|
||||
},
|
||||
"systems": {
|
||||
"locked": {
|
||||
"lastModified": 1681028828,
|
||||
"narHash": "sha256-Vy1rq5AaRuLzOxct8nz4T6wlgyUR7zLU309k9mBC768=",
|
||||
"owner": "nix-systems",
|
||||
"repo": "default",
|
||||
"rev": "da67096a3b9bf56a91d16901293e51ba5b49a27e",
|
||||
"type": "github"
|
||||
},
|
||||
"original": {
|
||||
"owner": "nix-systems",
|
||||
"repo": "default",
|
||||
"type": "github"
|
||||
}
|
||||
}
|
||||
},
|
||||
"root": "root",
|
||||
"version": 7
|
||||
}
|
134
flake.nix
Normal file
134
flake.nix
Normal file
|
@ -0,0 +1,134 @@
|
|||
{
|
||||
description = "Build a cargo project";
|
||||
|
||||
inputs = {
|
||||
nixpkgs.url = "github:NixOS/nixpkgs/nixpkgs-unstable";
|
||||
|
||||
crane = {
|
||||
url = "github:ipetkov/crane";
|
||||
inputs.nixpkgs.follows = "nixpkgs";
|
||||
};
|
||||
|
||||
fenix = {
|
||||
url = "github:nix-community/fenix";
|
||||
inputs.nixpkgs.follows = "nixpkgs";
|
||||
inputs.rust-analyzer-src.follows = "";
|
||||
};
|
||||
|
||||
flake-utils.url = "github:numtide/flake-utils";
|
||||
|
||||
advisory-db = {
|
||||
url = "github:rustsec/advisory-db";
|
||||
flake = false;
|
||||
};
|
||||
};
|
||||
|
||||
outputs = { self, nixpkgs, crane, fenix, flake-utils, advisory-db, ... }:
|
||||
flake-utils.lib.eachDefaultSystem (system:
|
||||
let
|
||||
pkgs = nixpkgs.legacyPackages.${system};
|
||||
|
||||
inherit (pkgs) lib;
|
||||
|
||||
craneLib = crane.lib.${system};
|
||||
src = craneLib.cleanCargoSource (craneLib.path ./.);
|
||||
|
||||
# Common arguments can be set here to avoid repeating them later
|
||||
commonArgs = {
|
||||
inherit src;
|
||||
strictDeps = true;
|
||||
|
||||
buildInputs = [
|
||||
# Add additional build inputs here
|
||||
] ++ lib.optionals pkgs.stdenv.isDarwin [
|
||||
# Additional darwin specific inputs can be set here
|
||||
pkgs.libiconv
|
||||
];
|
||||
|
||||
# Additional environment variables can be set directly
|
||||
# MY_CUSTOM_VAR = "some value";
|
||||
};
|
||||
|
||||
craneLibLLvmTools = craneLib.overrideToolchain
|
||||
(fenix.packages.${system}.complete.withComponents [
|
||||
"cargo"
|
||||
"llvm-tools"
|
||||
"rustc"
|
||||
]);
|
||||
|
||||
# Build *just* the cargo dependencies, so we can reuse
|
||||
# all of that work (e.g. via cachix) when running in CI
|
||||
cargoArtifacts = craneLib.buildDepsOnly commonArgs;
|
||||
|
||||
# Build the actual crate itself, reusing the dependency
|
||||
# artifacts from above.
|
||||
dttyper = craneLib.buildPackage (commonArgs // {
|
||||
inherit cargoArtifacts;
|
||||
});
|
||||
in
|
||||
{
|
||||
checks = {
|
||||
# Build the crate as part of `nix flake check` for convenience
|
||||
inherit dttyper;
|
||||
|
||||
# Run clippy (and deny all warnings) on the crate source,
|
||||
# again, reusing the dependency artifacts from above.
|
||||
#
|
||||
# Note that this is done as a separate derivation so that
|
||||
# we can block the CI if there are issues here, but not
|
||||
# prevent downstream consumers from building our crate by itself.
|
||||
dttyper-clippy = craneLib.cargoClippy (commonArgs // {
|
||||
inherit cargoArtifacts;
|
||||
cargoClippyExtraArgs = "--all-targets -- --deny warnings";
|
||||
});
|
||||
|
||||
dttyper-doc = craneLib.cargoDoc (commonArgs // {
|
||||
inherit cargoArtifacts;
|
||||
});
|
||||
|
||||
# Check formatting
|
||||
dttyper-fmt = craneLib.cargoFmt {
|
||||
inherit src;
|
||||
};
|
||||
|
||||
# Audit dependencies
|
||||
dttyper-audit = craneLib.cargoAudit {
|
||||
inherit src advisory-db;
|
||||
};
|
||||
|
||||
# Audit licenses
|
||||
dttyper-deny = craneLib.cargoDeny {
|
||||
inherit src;
|
||||
};
|
||||
|
||||
# Run tests with cargo-nextest
|
||||
# Consider setting `doCheck = false` on `dttyper` if you do not want
|
||||
# the tests to run twice
|
||||
dttyper-nextest = craneLib.cargoNextest (commonArgs // {
|
||||
inherit cargoArtifacts;
|
||||
partitions = 1;
|
||||
partitionType = "count";
|
||||
});
|
||||
};
|
||||
|
||||
packages = {
|
||||
default = dttyper;
|
||||
} // lib.optionalAttrs (!pkgs.stdenv.isDarwin) {
|
||||
dttyper-llvm-coverage = craneLibLLvmTools.cargoLlvmCov (commonArgs // {
|
||||
inherit cargoArtifacts;
|
||||
});
|
||||
};
|
||||
|
||||
apps.default = flake-utils.lib.mkApp {
|
||||
drv = dttyper;
|
||||
};
|
||||
|
||||
devShells.default = craneLib.devShell {
|
||||
checks = self.checks.${system};
|
||||
OPENSSL_NO_VENDOR = 1;
|
||||
packages = [
|
||||
pkgs.openssl
|
||||
];
|
||||
};
|
||||
});
|
||||
}
|
976
panel.json
Normal file
976
panel.json
Normal file
|
@ -0,0 +1,976 @@
|
|||
{
|
||||
"__inputs": [
|
||||
{
|
||||
"name": "DS_INFLUXDB",
|
||||
"label": "influxdb",
|
||||
"description": "",
|
||||
"type": "datasource",
|
||||
"pluginId": "influxdb",
|
||||
"pluginName": "InfluxDB"
|
||||
}
|
||||
],
|
||||
"__elements": {},
|
||||
"__requires": [
|
||||
{
|
||||
"type": "grafana",
|
||||
"id": "grafana",
|
||||
"name": "Grafana",
|
||||
"version": "10.3.3"
|
||||
},
|
||||
{
|
||||
"type": "datasource",
|
||||
"id": "influxdb",
|
||||
"name": "InfluxDB",
|
||||
"version": "1.0.0"
|
||||
},
|
||||
{
|
||||
"type": "panel",
|
||||
"id": "piechart",
|
||||
"name": "Pie chart",
|
||||
"version": ""
|
||||
},
|
||||
{
|
||||
"type": "panel",
|
||||
"id": "stat",
|
||||
"name": "Stat",
|
||||
"version": ""
|
||||
},
|
||||
{
|
||||
"type": "panel",
|
||||
"id": "table",
|
||||
"name": "Table",
|
||||
"version": ""
|
||||
},
|
||||
{
|
||||
"type": "panel",
|
||||
"id": "timeseries",
|
||||
"name": "Time series",
|
||||
"version": ""
|
||||
}
|
||||
],
|
||||
"annotations": {
|
||||
"list": [
|
||||
{
|
||||
"builtIn": 1,
|
||||
"datasource": {
|
||||
"type": "grafana",
|
||||
"uid": "-- Grafana --"
|
||||
},
|
||||
"enable": true,
|
||||
"hide": true,
|
||||
"iconColor": "rgba(0, 211, 255, 1)",
|
||||
"name": "Annotations & Alerts",
|
||||
"type": "dashboard"
|
||||
}
|
||||
]
|
||||
},
|
||||
"editable": true,
|
||||
"fiscalYearStartMonth": 0,
|
||||
"graphTooltip": 0,
|
||||
"id": null,
|
||||
"links": [],
|
||||
"liveNow": false,
|
||||
"panels": [
|
||||
{
|
||||
"gridPos": {
|
||||
"h": 1,
|
||||
"w": 24,
|
||||
"x": 0,
|
||||
"y": 0
|
||||
},
|
||||
"id": 19,
|
||||
"title": "Stats",
|
||||
"type": "row"
|
||||
},
|
||||
{
|
||||
"datasource": {
|
||||
"type": "influxdb",
|
||||
"uid": "${DS_INFLUXDB}"
|
||||
},
|
||||
"description": "",
|
||||
"fieldConfig": {
|
||||
"defaults": {
|
||||
"color": {
|
||||
"mode": "thresholds"
|
||||
},
|
||||
"mappings": [],
|
||||
"thresholds": {
|
||||
"mode": "absolute",
|
||||
"steps": [
|
||||
{
|
||||
"color": "yellow",
|
||||
"value": null
|
||||
}
|
||||
]
|
||||
},
|
||||
"unitScale": true
|
||||
},
|
||||
"overrides": []
|
||||
},
|
||||
"gridPos": {
|
||||
"h": 4,
|
||||
"w": 4,
|
||||
"x": 0,
|
||||
"y": 1
|
||||
},
|
||||
"id": 9,
|
||||
"options": {
|
||||
"colorMode": "value",
|
||||
"graphMode": "area",
|
||||
"justifyMode": "auto",
|
||||
"orientation": "auto",
|
||||
"reduceOptions": {
|
||||
"calcs": [
|
||||
"lastNotNull"
|
||||
],
|
||||
"fields": "",
|
||||
"values": false
|
||||
},
|
||||
"showPercentChange": false,
|
||||
"textMode": "auto",
|
||||
"wideLayout": true
|
||||
},
|
||||
"pluginVersion": "10.3.3",
|
||||
"targets": [
|
||||
{
|
||||
"datasource": {
|
||||
"type": "influxdb",
|
||||
"uid": "${DS_INFLUXDB}"
|
||||
},
|
||||
"query": "from(bucket: \"dttyper\")\n |> range(start: v.timeRangeStart, stop: v.timeRangeStop)\n |> filter(fn: (r) => r[\"_measurement\"] == \"test\")\n |> filter(fn: (r) => r[\"_field\"] == \"correct_types\")\n |> aggregateWindow(every: 999d, fn: sum, createEmpty: false)\n |> group()\n |> sum(column: \"_value\")",
|
||||
"refId": "A"
|
||||
}
|
||||
],
|
||||
"title": "Correct words typed",
|
||||
"type": "stat"
|
||||
},
|
||||
{
|
||||
"datasource": {
|
||||
"type": "influxdb",
|
||||
"uid": "${DS_INFLUXDB}"
|
||||
},
|
||||
"fieldConfig": {
|
||||
"defaults": {
|
||||
"color": {
|
||||
"mode": "thresholds"
|
||||
},
|
||||
"mappings": [],
|
||||
"thresholds": {
|
||||
"mode": "absolute",
|
||||
"steps": [
|
||||
{
|
||||
"color": "green",
|
||||
"value": null
|
||||
},
|
||||
{
|
||||
"color": "red",
|
||||
"value": 30
|
||||
},
|
||||
{
|
||||
"color": "#EAB839",
|
||||
"value": 40
|
||||
},
|
||||
{
|
||||
"color": "#6ED0E0",
|
||||
"value": 50
|
||||
},
|
||||
{
|
||||
"color": "yellow",
|
||||
"value": 60
|
||||
},
|
||||
{
|
||||
"color": "dark-orange",
|
||||
"value": 70
|
||||
},
|
||||
{
|
||||
"color": "#1F78C1",
|
||||
"value": 80
|
||||
},
|
||||
{
|
||||
"color": "green",
|
||||
"value": 90
|
||||
},
|
||||
{
|
||||
"color": "dark-green",
|
||||
"value": 95
|
||||
}
|
||||
]
|
||||
},
|
||||
"unit": "percent",
|
||||
"unitScale": true
|
||||
},
|
||||
"overrides": []
|
||||
},
|
||||
"gridPos": {
|
||||
"h": 4,
|
||||
"w": 3,
|
||||
"x": 4,
|
||||
"y": 1
|
||||
},
|
||||
"id": 18,
|
||||
"options": {
|
||||
"colorMode": "value",
|
||||
"graphMode": "area",
|
||||
"justifyMode": "auto",
|
||||
"orientation": "auto",
|
||||
"reduceOptions": {
|
||||
"calcs": [
|
||||
"lastNotNull"
|
||||
],
|
||||
"fields": "",
|
||||
"values": false
|
||||
},
|
||||
"showPercentChange": false,
|
||||
"textMode": "auto",
|
||||
"wideLayout": true
|
||||
},
|
||||
"pluginVersion": "10.3.3",
|
||||
"targets": [
|
||||
{
|
||||
"datasource": {
|
||||
"type": "influxdb",
|
||||
"uid": "${DS_INFLUXDB}"
|
||||
},
|
||||
"query": "from(bucket: \"dttyper\")\n |> range(start: v.timeRangeStart, stop: v.timeRangeStop)\n |> filter(fn: (r) => r[\"_measurement\"] == \"test\")\n |> filter(fn: (r) => r[\"_field\"] == \"accuracy\")\n |> aggregateWindow(every: 999d, fn: mean, createEmpty: false)\n |> group()\n |> mean(column: \"_value\")",
|
||||
"refId": "A"
|
||||
}
|
||||
],
|
||||
"title": "Overall accuracy %",
|
||||
"type": "stat"
|
||||
},
|
||||
{
|
||||
"datasource": {
|
||||
"type": "influxdb",
|
||||
"uid": "${DS_INFLUXDB}"
|
||||
},
|
||||
"description": "",
|
||||
"fieldConfig": {
|
||||
"defaults": {
|
||||
"color": {
|
||||
"mode": "thresholds"
|
||||
},
|
||||
"mappings": [],
|
||||
"thresholds": {
|
||||
"mode": "absolute",
|
||||
"steps": [
|
||||
{
|
||||
"color": "red",
|
||||
"value": null
|
||||
}
|
||||
]
|
||||
},
|
||||
"unitScale": true
|
||||
},
|
||||
"overrides": []
|
||||
},
|
||||
"gridPos": {
|
||||
"h": 4,
|
||||
"w": 4,
|
||||
"x": 7,
|
||||
"y": 1
|
||||
},
|
||||
"id": 22,
|
||||
"options": {
|
||||
"colorMode": "value",
|
||||
"graphMode": "area",
|
||||
"justifyMode": "auto",
|
||||
"orientation": "auto",
|
||||
"reduceOptions": {
|
||||
"calcs": [
|
||||
"lastNotNull"
|
||||
],
|
||||
"fields": "",
|
||||
"values": false
|
||||
},
|
||||
"showPercentChange": false,
|
||||
"textMode": "auto",
|
||||
"wideLayout": true
|
||||
},
|
||||
"pluginVersion": "10.3.3",
|
||||
"targets": [
|
||||
{
|
||||
"datasource": {
|
||||
"type": "influxdb",
|
||||
"uid": "${DS_INFLUXDB}"
|
||||
},
|
||||
"query": "from(bucket: \"dttyper\")\n |> range(start: v.timeRangeStart, stop: v.timeRangeStop)\n |> filter(fn: (r) => r[\"_measurement\"] == \"test\")\n |> filter(fn: (r) => r[\"_field\"] == \"incorrect_types\")\n |> aggregateWindow(every: 999d, fn: sum, createEmpty: false)\n |> group()\n |> sum(column: \"_value\")",
|
||||
"refId": "A"
|
||||
}
|
||||
],
|
||||
"title": "Incorrectly typed word",
|
||||
"type": "stat"
|
||||
},
|
||||
{
|
||||
"datasource": {
|
||||
"type": "influxdb",
|
||||
"uid": "${DS_INFLUXDB}"
|
||||
},
|
||||
"fieldConfig": {
|
||||
"defaults": {
|
||||
"color": {
|
||||
"mode": "thresholds"
|
||||
},
|
||||
"mappings": [],
|
||||
"thresholds": {
|
||||
"mode": "absolute",
|
||||
"steps": [
|
||||
{
|
||||
"color": "green",
|
||||
"value": null
|
||||
}
|
||||
]
|
||||
},
|
||||
"unitScale": true
|
||||
},
|
||||
"overrides": []
|
||||
},
|
||||
"gridPos": {
|
||||
"h": 4,
|
||||
"w": 4,
|
||||
"x": 11,
|
||||
"y": 1
|
||||
},
|
||||
"id": 12,
|
||||
"options": {
|
||||
"colorMode": "value",
|
||||
"graphMode": "area",
|
||||
"justifyMode": "auto",
|
||||
"orientation": "auto",
|
||||
"reduceOptions": {
|
||||
"calcs": [
|
||||
"lastNotNull"
|
||||
],
|
||||
"fields": "",
|
||||
"values": false
|
||||
},
|
||||
"showPercentChange": false,
|
||||
"textMode": "auto",
|
||||
"wideLayout": true
|
||||
},
|
||||
"pluginVersion": "10.3.3",
|
||||
"targets": [
|
||||
{
|
||||
"datasource": {
|
||||
"type": "influxdb",
|
||||
"uid": "${DS_INFLUXDB}"
|
||||
},
|
||||
"query": "from(bucket: \"dttyper\")\n |> range(start: v.timeRangeStart, stop: v.timeRangeStop)\n |> filter(fn: (r) => r[\"_measurement\"] == \"test\")\n // get dummy field\n |> filter(fn: (r) => r[\"_field\"] == \"incorrect_types\")\n |> group()\n |> count()",
|
||||
"refId": "A"
|
||||
}
|
||||
],
|
||||
"title": "Tests completed",
|
||||
"type": "stat"
|
||||
},
|
||||
{
|
||||
"datasource": {
|
||||
"type": "influxdb",
|
||||
"uid": "${DS_INFLUXDB}"
|
||||
},
|
||||
"fieldConfig": {
|
||||
"defaults": {
|
||||
"color": {
|
||||
"mode": "thresholds"
|
||||
},
|
||||
"mappings": [],
|
||||
"thresholds": {
|
||||
"mode": "absolute",
|
||||
"steps": [
|
||||
{
|
||||
"color": "green",
|
||||
"value": null
|
||||
},
|
||||
{
|
||||
"color": "dark-red",
|
||||
"value": 40
|
||||
},
|
||||
{
|
||||
"color": "red",
|
||||
"value": 50
|
||||
},
|
||||
{
|
||||
"color": "#EAB839",
|
||||
"value": 60
|
||||
},
|
||||
{
|
||||
"color": "#6ED0E0",
|
||||
"value": 70
|
||||
},
|
||||
{
|
||||
"color": "#EF843C",
|
||||
"value": 80
|
||||
},
|
||||
{
|
||||
"color": "#E24D42",
|
||||
"value": 90
|
||||
},
|
||||
{
|
||||
"color": "#1F78C1",
|
||||
"value": 100
|
||||
},
|
||||
{
|
||||
"color": "green",
|
||||
"value": 110
|
||||
},
|
||||
{
|
||||
"color": "#BA43A9",
|
||||
"value": 120
|
||||
},
|
||||
{
|
||||
"color": "dark-purple",
|
||||
"value": 130
|
||||
},
|
||||
{
|
||||
"color": "#705DA0",
|
||||
"value": 140
|
||||
},
|
||||
{
|
||||
"color": "#508642",
|
||||
"value": 150
|
||||
}
|
||||
]
|
||||
},
|
||||
"unitScale": true
|
||||
},
|
||||
"overrides": []
|
||||
},
|
||||
"gridPos": {
|
||||
"h": 4,
|
||||
"w": 3,
|
||||
"x": 15,
|
||||
"y": 1
|
||||
},
|
||||
"id": 16,
|
||||
"options": {
|
||||
"colorMode": "value",
|
||||
"graphMode": "area",
|
||||
"justifyMode": "auto",
|
||||
"orientation": "auto",
|
||||
"reduceOptions": {
|
||||
"calcs": [
|
||||
"max"
|
||||
],
|
||||
"fields": "",
|
||||
"values": false
|
||||
},
|
||||
"showPercentChange": false,
|
||||
"textMode": "auto",
|
||||
"wideLayout": true
|
||||
},
|
||||
"pluginVersion": "10.3.3",
|
||||
"targets": [
|
||||
{
|
||||
"datasource": {
|
||||
"type": "influxdb",
|
||||
"uid": "${DS_INFLUXDB}"
|
||||
},
|
||||
"query": "from(bucket: \"dttyper\")\n |> range(start: v.timeRangeStart, stop: v.timeRangeStop)\n |> filter(fn: (r) => r[\"_measurement\"] == \"test\")\n |> filter(fn: (r) => r[\"_field\"] == \"wpm\")\n |> aggregateWindow(every: v.windowPeriod, fn: max, createEmpty: false)\n |> group()",
|
||||
"refId": "A"
|
||||
}
|
||||
],
|
||||
"title": "Highest wpm",
|
||||
"type": "stat"
|
||||
},
|
||||
{
|
||||
"datasource": {
|
||||
"type": "influxdb",
|
||||
"uid": "${DS_INFLUXDB}"
|
||||
},
|
||||
"fieldConfig": {
|
||||
"defaults": {
|
||||
"color": {
|
||||
"mode": "thresholds"
|
||||
},
|
||||
"mappings": [],
|
||||
"thresholds": {
|
||||
"mode": "absolute",
|
||||
"steps": [
|
||||
{
|
||||
"color": "green",
|
||||
"value": null
|
||||
},
|
||||
{
|
||||
"color": "dark-red",
|
||||
"value": 40
|
||||
},
|
||||
{
|
||||
"color": "red",
|
||||
"value": 50
|
||||
},
|
||||
{
|
||||
"color": "#EAB839",
|
||||
"value": 60
|
||||
},
|
||||
{
|
||||
"color": "#6ED0E0",
|
||||
"value": 70
|
||||
},
|
||||
{
|
||||
"color": "#EF843C",
|
||||
"value": 80
|
||||
},
|
||||
{
|
||||
"color": "#E24D42",
|
||||
"value": 90
|
||||
},
|
||||
{
|
||||
"color": "#1F78C1",
|
||||
"value": 100
|
||||
},
|
||||
{
|
||||
"color": "green",
|
||||
"value": 110
|
||||
},
|
||||
{
|
||||
"color": "#BA43A9",
|
||||
"value": 120
|
||||
},
|
||||
{
|
||||
"color": "dark-purple",
|
||||
"value": 130
|
||||
},
|
||||
{
|
||||
"color": "#705DA0",
|
||||
"value": 140
|
||||
},
|
||||
{
|
||||
"color": "#508642",
|
||||
"value": 150
|
||||
}
|
||||
]
|
||||
},
|
||||
"unitScale": true
|
||||
},
|
||||
"overrides": []
|
||||
},
|
||||
"gridPos": {
|
||||
"h": 4,
|
||||
"w": 3,
|
||||
"x": 18,
|
||||
"y": 1
|
||||
},
|
||||
"id": 21,
|
||||
"options": {
|
||||
"colorMode": "value",
|
||||
"graphMode": "area",
|
||||
"justifyMode": "auto",
|
||||
"orientation": "auto",
|
||||
"reduceOptions": {
|
||||
"calcs": [
|
||||
"lastNotNull"
|
||||
],
|
||||
"fields": "",
|
||||
"values": false
|
||||
},
|
||||
"showPercentChange": false,
|
||||
"textMode": "auto",
|
||||
"wideLayout": true
|
||||
},
|
||||
"pluginVersion": "10.3.3",
|
||||
"targets": [
|
||||
{
|
||||
"datasource": {
|
||||
"type": "influxdb",
|
||||
"uid": "${DS_INFLUXDB}"
|
||||
},
|
||||
"query": "from(bucket: \"dttyper\")\n |> range(start: v.timeRangeStart, stop: v.timeRangeStop)\n |> filter(fn: (r) => r[\"_measurement\"] == \"test\")\n |> filter(fn: (r) => r[\"_field\"] == \"wpm\")\n |> aggregateWindow(every: v.windowPeriod, fn: min, createEmpty: false)\n |> group()",
|
||||
"refId": "A"
|
||||
}
|
||||
],
|
||||
"title": "Average WPM",
|
||||
"type": "stat"
|
||||
},
|
||||
{
|
||||
"datasource": {
|
||||
"type": "influxdb",
|
||||
"uid": "${DS_INFLUXDB}"
|
||||
},
|
||||
"fieldConfig": {
|
||||
"defaults": {
|
||||
"color": {
|
||||
"mode": "thresholds"
|
||||
},
|
||||
"mappings": [],
|
||||
"thresholds": {
|
||||
"mode": "absolute",
|
||||
"steps": [
|
||||
{
|
||||
"color": "green",
|
||||
"value": null
|
||||
},
|
||||
{
|
||||
"color": "dark-red",
|
||||
"value": 40
|
||||
},
|
||||
{
|
||||
"color": "red",
|
||||
"value": 50
|
||||
},
|
||||
{
|
||||
"color": "#EAB839",
|
||||
"value": 60
|
||||
},
|
||||
{
|
||||
"color": "#6ED0E0",
|
||||
"value": 70
|
||||
},
|
||||
{
|
||||
"color": "#EF843C",
|
||||
"value": 80
|
||||
},
|
||||
{
|
||||
"color": "#E24D42",
|
||||
"value": 90
|
||||
},
|
||||
{
|
||||
"color": "#1F78C1",
|
||||
"value": 100
|
||||
},
|
||||
{
|
||||
"color": "green",
|
||||
"value": 110
|
||||
},
|
||||
{
|
||||
"color": "#BA43A9",
|
||||
"value": 120
|
||||
},
|
||||
{
|
||||
"color": "dark-purple",
|
||||
"value": 130
|
||||
},
|
||||
{
|
||||
"color": "#705DA0",
|
||||
"value": 140
|
||||
},
|
||||
{
|
||||
"color": "#508642",
|
||||
"value": 150
|
||||
}
|
||||
]
|
||||
},
|
||||
"unitScale": true
|
||||
},
|
||||
"overrides": []
|
||||
},
|
||||
"gridPos": {
|
||||
"h": 4,
|
||||
"w": 3,
|
||||
"x": 21,
|
||||
"y": 1
|
||||
},
|
||||
"id": 13,
|
||||
"options": {
|
||||
"colorMode": "value",
|
||||
"graphMode": "area",
|
||||
"justifyMode": "auto",
|
||||
"orientation": "auto",
|
||||
"reduceOptions": {
|
||||
"calcs": [
|
||||
"min"
|
||||
],
|
||||
"fields": "",
|
||||
"values": false
|
||||
},
|
||||
"showPercentChange": false,
|
||||
"textMode": "auto",
|
||||
"wideLayout": true
|
||||
},
|
||||
"pluginVersion": "10.3.3",
|
||||
"targets": [
|
||||
{
|
||||
"datasource": {
|
||||
"type": "influxdb",
|
||||
"uid": "${DS_INFLUXDB}"
|
||||
},
|
||||
"query": "from(bucket: \"dttyper\")\n |> range(start: v.timeRangeStart, stop: v.timeRangeStop)\n |> filter(fn: (r) => r[\"_measurement\"] == \"test\")\n |> filter(fn: (r) => r[\"_field\"] == \"wpm\")\n |> aggregateWindow(every: 999d, fn: min, createEmpty: false)\n |> group()",
|
||||
"refId": "A"
|
||||
}
|
||||
],
|
||||
"title": "Lowest wpm ",
|
||||
"type": "stat"
|
||||
},
|
||||
{
|
||||
"collapsed": false,
|
||||
"gridPos": {
|
||||
"h": 1,
|
||||
"w": 24,
|
||||
"x": 0,
|
||||
"y": 5
|
||||
},
|
||||
"id": 3,
|
||||
"panels": [],
|
||||
"title": "Global",
|
||||
"type": "row"
|
||||
},
|
||||
{
|
||||
"datasource": {
|
||||
"type": "influxdb",
|
||||
"uid": "${DS_INFLUXDB}"
|
||||
},
|
||||
"fieldConfig": {
|
||||
"defaults": {
|
||||
"color": {
|
||||
"mode": "palette-classic"
|
||||
},
|
||||
"custom": {
|
||||
"axisBorderShow": false,
|
||||
"axisCenteredZero": false,
|
||||
"axisColorMode": "text",
|
||||
"axisLabel": "",
|
||||
"axisPlacement": "auto",
|
||||
"barAlignment": 0,
|
||||
"drawStyle": "line",
|
||||
"fillOpacity": 0,
|
||||
"gradientMode": "none",
|
||||
"hideFrom": {
|
||||
"legend": false,
|
||||
"tooltip": false,
|
||||
"viz": false
|
||||
},
|
||||
"insertNulls": false,
|
||||
"lineInterpolation": "linear",
|
||||
"lineWidth": 1,
|
||||
"pointSize": 5,
|
||||
"scaleDistribution": {
|
||||
"type": "linear"
|
||||
},
|
||||
"showPoints": "auto",
|
||||
"spanNulls": false,
|
||||
"stacking": {
|
||||
"group": "A",
|
||||
"mode": "none"
|
||||
},
|
||||
"thresholdsStyle": {
|
||||
"mode": "off"
|
||||
}
|
||||
},
|
||||
"mappings": [],
|
||||
"thresholds": {
|
||||
"mode": "absolute",
|
||||
"steps": [
|
||||
{
|
||||
"color": "green",
|
||||
"value": null
|
||||
},
|
||||
{
|
||||
"color": "red",
|
||||
"value": 80
|
||||
}
|
||||
]
|
||||
},
|
||||
"unitScale": true
|
||||
},
|
||||
"overrides": []
|
||||
},
|
||||
"gridPos": {
|
||||
"h": 11,
|
||||
"w": 24,
|
||||
"x": 0,
|
||||
"y": 6
|
||||
},
|
||||
"id": 20,
|
||||
"options": {
|
||||
"legend": {
|
||||
"calcs": [],
|
||||
"displayMode": "list",
|
||||
"placement": "bottom",
|
||||
"showLegend": true
|
||||
},
|
||||
"tooltip": {
|
||||
"mode": "single",
|
||||
"sort": "none"
|
||||
}
|
||||
},
|
||||
"targets": [
|
||||
{
|
||||
"datasource": {
|
||||
"type": "influxdb",
|
||||
"uid": "${DS_INFLUXDB}"
|
||||
},
|
||||
"query": "from(bucket: \"dttyper\")\n |> range(start: v.timeRangeStart, stop: v.timeRangeStop)\n |> filter(fn: (r) => r[\"_measurement\"] == \"test\")\n |> filter(fn: (r) => r[\"_field\"] == \"wpm\")",
|
||||
"refId": "A"
|
||||
}
|
||||
],
|
||||
"title": "WPM overtime",
|
||||
"type": "timeseries"
|
||||
},
|
||||
{
|
||||
"datasource": {
|
||||
"type": "influxdb",
|
||||
"uid": "${DS_INFLUXDB}"
|
||||
},
|
||||
"fieldConfig": {
|
||||
"defaults": {
|
||||
"color": {
|
||||
"mode": "palette-classic"
|
||||
},
|
||||
"custom": {
|
||||
"hideFrom": {
|
||||
"legend": false,
|
||||
"tooltip": false,
|
||||
"viz": false
|
||||
}
|
||||
},
|
||||
"mappings": [],
|
||||
"unitScale": true
|
||||
},
|
||||
"overrides": []
|
||||
},
|
||||
"gridPos": {
|
||||
"h": 9,
|
||||
"w": 10,
|
||||
"x": 0,
|
||||
"y": 17
|
||||
},
|
||||
"id": 23,
|
||||
"options": {
|
||||
"legend": {
|
||||
"displayMode": "list",
|
||||
"placement": "bottom",
|
||||
"showLegend": true
|
||||
},
|
||||
"pieType": "pie",
|
||||
"reduceOptions": {
|
||||
"calcs": [
|
||||
"mean"
|
||||
],
|
||||
"fields": "",
|
||||
"values": false
|
||||
},
|
||||
"tooltip": {
|
||||
"mode": "single",
|
||||
"sort": "none"
|
||||
}
|
||||
},
|
||||
"pluginVersion": "10.3.3",
|
||||
"targets": [
|
||||
{
|
||||
"datasource": {
|
||||
"type": "influxdb",
|
||||
"uid": "${DS_INFLUXDB}"
|
||||
},
|
||||
"query": "from(bucket: \"dttyper\")\n |> range(start: v.timeRangeStart, stop: v.timeRangeStop)\n |> filter(fn: (r) => r[\"_measurement\"] == \"test\")\n |> filter(fn: (r) => r[\"_field\"] == \"wpm\")",
|
||||
"refId": "A"
|
||||
}
|
||||
],
|
||||
"title": "WPM per test",
|
||||
"transformations": [],
|
||||
"type": "piechart"
|
||||
},
|
||||
{
|
||||
"datasource": {
|
||||
"type": "influxdb",
|
||||
"uid": "${DS_INFLUXDB}"
|
||||
},
|
||||
"description": "",
|
||||
"fieldConfig": {
|
||||
"defaults": {
|
||||
"color": {
|
||||
"mode": "thresholds"
|
||||
},
|
||||
"custom": {
|
||||
"align": "auto",
|
||||
"cellOptions": {
|
||||
"type": "auto"
|
||||
},
|
||||
"inspect": false
|
||||
},
|
||||
"mappings": [],
|
||||
"thresholds": {
|
||||
"mode": "absolute",
|
||||
"steps": [
|
||||
{
|
||||
"color": "green",
|
||||
"value": null
|
||||
},
|
||||
{
|
||||
"color": "red",
|
||||
"value": 80
|
||||
}
|
||||
]
|
||||
},
|
||||
"unitScale": true
|
||||
},
|
||||
"overrides": []
|
||||
},
|
||||
"gridPos": {
|
||||
"h": 9,
|
||||
"w": 14,
|
||||
"x": 10,
|
||||
"y": 17
|
||||
},
|
||||
"id": 24,
|
||||
"options": {
|
||||
"cellHeight": "sm",
|
||||
"footer": {
|
||||
"countRows": false,
|
||||
"fields": "",
|
||||
"reducer": [
|
||||
"sum"
|
||||
],
|
||||
"show": false
|
||||
},
|
||||
"frameIndex": 2,
|
||||
"showHeader": true
|
||||
},
|
||||
"pluginVersion": "10.3.3",
|
||||
"targets": [
|
||||
{
|
||||
"datasource": {
|
||||
"type": "influxdb",
|
||||
"uid": "${DS_INFLUXDB}"
|
||||
},
|
||||
"query": "from(bucket: \"dttyper\")\n |> range(start: v.timeRangeStart, stop: v.timeRangeStop)\n |> filter(fn: (r) => r[\"_measurement\"] == \"test\")",
|
||||
"refId": "A"
|
||||
}
|
||||
],
|
||||
"title": "History",
|
||||
"type": "table"
|
||||
},
|
||||
{
|
||||
"collapsed": false,
|
||||
"gridPos": {
|
||||
"h": 1,
|
||||
"w": 24,
|
||||
"x": 0,
|
||||
"y": 26
|
||||
},
|
||||
"id": 2,
|
||||
"panels": [],
|
||||
"title": "Query",
|
||||
"type": "row"
|
||||
}
|
||||
],
|
||||
"refresh": false,
|
||||
"schemaVersion": 39,
|
||||
"tags": [
|
||||
"wpm",
|
||||
"keyboard",
|
||||
"dtyper"
|
||||
],
|
||||
"templating": {
|
||||
"list": []
|
||||
},
|
||||
"time": {
|
||||
"from": "now-7d",
|
||||
"to": "now"
|
||||
},
|
||||
"timepicker": {
|
||||
"refresh_intervals": [
|
||||
"5s",
|
||||
"10s",
|
||||
"30s",
|
||||
"1m",
|
||||
"5m",
|
||||
"15m",
|
||||
"30m",
|
||||
"1h",
|
||||
"2h",
|
||||
"1d"
|
||||
]
|
||||
},
|
||||
"timezone": "",
|
||||
"title": "Ttyper",
|
||||
"uid": "dd78f974-f0dc-456c-8a59-199df1f81eaf",
|
||||
"version": 34,
|
||||
"weekStart": ""
|
||||
}
|
BIN
resources/grafana.png
Normal file
BIN
resources/grafana.png
Normal file
Binary file not shown.
After Width: | Height: | Size: 162 KiB |
BIN
resources/recording.gif
Normal file
BIN
resources/recording.gif
Normal file
Binary file not shown.
After Width: | Height: | Size: 1.2 MiB |
52
resources/runtime/language/c
Normal file
52
resources/runtime/language/c
Normal file
|
@ -0,0 +1,52 @@
|
|||
int
|
||||
char
|
||||
unsigned
|
||||
float
|
||||
void
|
||||
main
|
||||
union
|
||||
long
|
||||
double
|
||||
printf
|
||||
sprintf
|
||||
if
|
||||
else
|
||||
struct
|
||||
fork
|
||||
switch
|
||||
for
|
||||
define
|
||||
return
|
||||
include
|
||||
case
|
||||
&&
|
||||
||
|
||||
break
|
||||
bool
|
||||
static
|
||||
public
|
||||
enum
|
||||
typedef
|
||||
private
|
||||
exit
|
||||
<stdio.h>
|
||||
scanf
|
||||
NULL
|
||||
malloc
|
||||
calloc
|
||||
free
|
||||
realloc
|
||||
<string.h>
|
||||
fgets
|
||||
strcmp
|
||||
strcpy
|
||||
fputs
|
||||
stdout
|
||||
EOF
|
||||
getc
|
||||
while
|
||||
fclose
|
||||
fopen
|
||||
do
|
||||
fscanf
|
||||
extern
|
139
resources/runtime/language/cpp
Normal file
139
resources/runtime/language/cpp
Normal file
|
@ -0,0 +1,139 @@
|
|||
--i
|
||||
-=
|
||||
!=
|
||||
*=
|
||||
/=
|
||||
&&
|
||||
&=
|
||||
#define
|
||||
#include
|
||||
#pragma
|
||||
%=
|
||||
^=
|
||||
++i
|
||||
+=
|
||||
<<
|
||||
<<=
|
||||
<=
|
||||
<iostream>
|
||||
<map>
|
||||
<string>
|
||||
<vector>
|
||||
==
|
||||
>=
|
||||
>>
|
||||
>>=
|
||||
|=
|
||||
||
|
||||
alignas
|
||||
alignof
|
||||
and
|
||||
and_eq
|
||||
asm
|
||||
atomic_cancel
|
||||
atomic_commit
|
||||
atomic_noexcept
|
||||
auto
|
||||
bitand
|
||||
bitor
|
||||
bool
|
||||
break
|
||||
case
|
||||
catch
|
||||
char
|
||||
char16_t
|
||||
char32_t
|
||||
char8_t
|
||||
class
|
||||
co_await
|
||||
co_return
|
||||
co_yield
|
||||
compl
|
||||
concept
|
||||
const
|
||||
const_cast
|
||||
consteval
|
||||
constexpr
|
||||
constinit
|
||||
continue
|
||||
decltype
|
||||
default
|
||||
delete
|
||||
do
|
||||
double
|
||||
dynamic_cast
|
||||
else
|
||||
enum
|
||||
explicit
|
||||
export
|
||||
extern
|
||||
false
|
||||
final
|
||||
float
|
||||
for
|
||||
friend
|
||||
goto
|
||||
i--
|
||||
i++
|
||||
if
|
||||
import
|
||||
inline
|
||||
int
|
||||
long
|
||||
module
|
||||
mutable
|
||||
namespace
|
||||
new
|
||||
noexcept
|
||||
not
|
||||
not_eq
|
||||
nullptr
|
||||
operator
|
||||
or
|
||||
or_eq
|
||||
override
|
||||
private
|
||||
protected
|
||||
public
|
||||
reflexpr
|
||||
register
|
||||
reinterpret_cast
|
||||
requires
|
||||
return
|
||||
short
|
||||
signed
|
||||
sizeof
|
||||
sizeof(long long)
|
||||
static
|
||||
static_assert
|
||||
static_cast
|
||||
std::cout
|
||||
std::endl
|
||||
std::map
|
||||
std::move
|
||||
std::string
|
||||
std::vector
|
||||
struct
|
||||
switch
|
||||
synchronized
|
||||
template
|
||||
template<class T>
|
||||
template<typename T>
|
||||
this
|
||||
thread_local
|
||||
throw
|
||||
true
|
||||
try
|
||||
typedef
|
||||
typeid
|
||||
typename
|
||||
union
|
||||
unsigned
|
||||
using
|
||||
virtual
|
||||
void
|
||||
volatile
|
||||
wchar_t
|
||||
while
|
||||
xor
|
||||
xor_eq
|
108
resources/runtime/language/csharp
Normal file
108
resources/runtime/language/csharp
Normal file
|
@ -0,0 +1,108 @@
|
|||
abstract
|
||||
as
|
||||
base
|
||||
bool
|
||||
break
|
||||
byte
|
||||
case
|
||||
catch
|
||||
char
|
||||
checked
|
||||
class
|
||||
const
|
||||
continue
|
||||
decimal
|
||||
default
|
||||
delegate
|
||||
do
|
||||
double
|
||||
else
|
||||
enum
|
||||
even
|
||||
explicit
|
||||
extern
|
||||
false
|
||||
finally
|
||||
fixed
|
||||
float
|
||||
for
|
||||
foreach
|
||||
goto
|
||||
if
|
||||
implicit
|
||||
in
|
||||
init
|
||||
int
|
||||
interface
|
||||
internal
|
||||
is
|
||||
lock
|
||||
long
|
||||
namespace
|
||||
new
|
||||
null
|
||||
object
|
||||
operator
|
||||
out
|
||||
override
|
||||
params
|
||||
private
|
||||
protected
|
||||
public
|
||||
readonly
|
||||
ref
|
||||
return
|
||||
sbyte
|
||||
sealed
|
||||
short
|
||||
sizeof
|
||||
stackalloc
|
||||
static
|
||||
string
|
||||
struct
|
||||
switch
|
||||
this
|
||||
throw
|
||||
true
|
||||
try
|
||||
typeof
|
||||
uint
|
||||
ulong
|
||||
unchecked
|
||||
unsafe
|
||||
ushort
|
||||
using
|
||||
virtual
|
||||
void
|
||||
volatile
|
||||
add
|
||||
alias
|
||||
ascending
|
||||
async
|
||||
await
|
||||
by
|
||||
descending
|
||||
dynamic
|
||||
equals
|
||||
from
|
||||
get
|
||||
global
|
||||
group
|
||||
into
|
||||
join
|
||||
let
|
||||
nameof
|
||||
notnull
|
||||
on
|
||||
orderby
|
||||
partial
|
||||
remove
|
||||
select
|
||||
set
|
||||
unmanaged
|
||||
value
|
||||
var
|
||||
when
|
||||
where
|
||||
with
|
||||
yield
|
2000
resources/runtime/language/english-advanced
Normal file
2000
resources/runtime/language/english-advanced
Normal file
File diff suppressed because it is too large
Load diff
300
resources/runtime/language/english-ngrams
Normal file
300
resources/runtime/language/english-ngrams
Normal file
|
@ -0,0 +1,300 @@
|
|||
ou
|
||||
te
|
||||
me
|
||||
ve
|
||||
li
|
||||
ed
|
||||
ng
|
||||
ne
|
||||
at
|
||||
ll
|
||||
of
|
||||
ha
|
||||
he
|
||||
in
|
||||
st
|
||||
ce
|
||||
le
|
||||
as
|
||||
ti
|
||||
or
|
||||
se
|
||||
om
|
||||
is
|
||||
al
|
||||
nt
|
||||
co
|
||||
ro
|
||||
io
|
||||
re
|
||||
it
|
||||
on
|
||||
to
|
||||
es
|
||||
th
|
||||
ar
|
||||
en
|
||||
hi
|
||||
er
|
||||
ea
|
||||
si
|
||||
nd
|
||||
ri
|
||||
ma
|
||||
be
|
||||
ic
|
||||
de
|
||||
ra
|
||||
ch
|
||||
ur
|
||||
an
|
||||
con
|
||||
lit
|
||||
pre
|
||||
din
|
||||
tha
|
||||
art
|
||||
ore
|
||||
lan
|
||||
tri
|
||||
der
|
||||
ren
|
||||
orm
|
||||
tho
|
||||
ion
|
||||
eri
|
||||
ran
|
||||
ove
|
||||
ast
|
||||
mor
|
||||
wit
|
||||
can
|
||||
anc
|
||||
rie
|
||||
ies
|
||||
res
|
||||
rou
|
||||
ard
|
||||
nes
|
||||
ime
|
||||
ght
|
||||
age
|
||||
hat
|
||||
one
|
||||
sio
|
||||
hin
|
||||
spe
|
||||
cou
|
||||
ure
|
||||
ntr
|
||||
pla
|
||||
hei
|
||||
omp
|
||||
ers
|
||||
ass
|
||||
ont
|
||||
rec
|
||||
nde
|
||||
ant
|
||||
she
|
||||
ith
|
||||
iti
|
||||
ese
|
||||
out
|
||||
ver
|
||||
tte
|
||||
oth
|
||||
sin
|
||||
and
|
||||
nal
|
||||
men
|
||||
whe
|
||||
rit
|
||||
but
|
||||
ear
|
||||
int
|
||||
nte
|
||||
are
|
||||
cha
|
||||
wor
|
||||
tic
|
||||
ave
|
||||
ter
|
||||
nat
|
||||
sta
|
||||
rat
|
||||
nts
|
||||
cti
|
||||
sen
|
||||
ame
|
||||
ces
|
||||
lar
|
||||
oun
|
||||
rin
|
||||
whi
|
||||
ide
|
||||
ven
|
||||
end
|
||||
ted
|
||||
rea
|
||||
you
|
||||
ive
|
||||
pos
|
||||
oug
|
||||
par
|
||||
uld
|
||||
ati
|
||||
ble
|
||||
les
|
||||
ica
|
||||
era
|
||||
tur
|
||||
ind
|
||||
ons
|
||||
ric
|
||||
ned
|
||||
ate
|
||||
ing
|
||||
ste
|
||||
ust
|
||||
ort
|
||||
ssi
|
||||
all
|
||||
fro
|
||||
the
|
||||
tes
|
||||
ess
|
||||
tiv
|
||||
han
|
||||
hou
|
||||
ent
|
||||
ial
|
||||
ell
|
||||
ome
|
||||
ere
|
||||
ona
|
||||
eat
|
||||
dis
|
||||
tra
|
||||
ugh
|
||||
ich
|
||||
eas
|
||||
str
|
||||
ist
|
||||
ins
|
||||
tio
|
||||
den
|
||||
igh
|
||||
und
|
||||
wer
|
||||
rom
|
||||
use
|
||||
ice
|
||||
thi
|
||||
abl
|
||||
ern
|
||||
man
|
||||
ten
|
||||
ple
|
||||
hav
|
||||
lat
|
||||
red
|
||||
lly
|
||||
enc
|
||||
ite
|
||||
lin
|
||||
our
|
||||
was
|
||||
sti
|
||||
nti
|
||||
tin
|
||||
tor
|
||||
tat
|
||||
hic
|
||||
tan
|
||||
hen
|
||||
not
|
||||
eme
|
||||
had
|
||||
inc
|
||||
oul
|
||||
eve
|
||||
per
|
||||
sed
|
||||
ene
|
||||
app
|
||||
cal
|
||||
pro
|
||||
any
|
||||
for
|
||||
ect
|
||||
een
|
||||
ine
|
||||
ill
|
||||
por
|
||||
ral
|
||||
com
|
||||
hey
|
||||
nce
|
||||
ity
|
||||
ord
|
||||
ose
|
||||
her
|
||||
ndi
|
||||
ain
|
||||
ous
|
||||
tim
|
||||
act
|
||||
min
|
||||
est
|
||||
his
|
||||
form
|
||||
ctio
|
||||
ally
|
||||
hich
|
||||
ring
|
||||
whic
|
||||
that
|
||||
ting
|
||||
nter
|
||||
ions
|
||||
othe
|
||||
thei
|
||||
ould
|
||||
were
|
||||
thin
|
||||
tive
|
||||
ence
|
||||
over
|
||||
sion
|
||||
ever
|
||||
heir
|
||||
ture
|
||||
inte
|
||||
ight
|
||||
some
|
||||
cont
|
||||
this
|
||||
ents
|
||||
ment
|
||||
part
|
||||
ough
|
||||
rati
|
||||
ding
|
||||
ther
|
||||
cons
|
||||
here
|
||||
ning
|
||||
ance
|
||||
ecti
|
||||
comp
|
||||
from
|
||||
tion
|
||||
ated
|
||||
they
|
||||
ical
|
||||
atio
|
||||
with
|
||||
able
|
||||
have
|
||||
pres
|
50
resources/runtime/language/english-pirate
Normal file
50
resources/runtime/language/english-pirate
Normal file
|
@ -0,0 +1,50 @@
|
|||
ahoy
|
||||
anchor
|
||||
arrr
|
||||
avast
|
||||
aye
|
||||
beast
|
||||
bilge
|
||||
blackbeard
|
||||
blimey
|
||||
bombard
|
||||
booty
|
||||
bounty
|
||||
buccaneer
|
||||
bumboo
|
||||
capsize
|
||||
cutlass
|
||||
doubloon
|
||||
doughboy
|
||||
eyepatch
|
||||
freebooter
|
||||
grog
|
||||
hardtack
|
||||
hogshead
|
||||
hornswoggle
|
||||
island
|
||||
keel
|
||||
landlubber
|
||||
man-o-war
|
||||
marooned
|
||||
matey
|
||||
mutiny
|
||||
parley
|
||||
peg-leg
|
||||
picaroon
|
||||
pillage
|
||||
plank
|
||||
plunder
|
||||
poop-deck
|
||||
privateer
|
||||
rigging
|
||||
rullock
|
||||
rumfustian
|
||||
sail
|
||||
scallywag
|
||||
scupper
|
||||
scurvy
|
||||
sea
|
||||
seamen
|
||||
swashbuckler
|
||||
treasure
|
151
resources/runtime/language/english-text
Normal file
151
resources/runtime/language/english-text
Normal file
|
@ -0,0 +1,151 @@
|
|||
The manager of the fruit stand always sat and only sold vegetables.
|
||||
Always bring cinnamon buns on a deep-sea diving expedition.
|
||||
He spiked his hair green to support his iguana.
|
||||
As he waited for the shower to warm, he noticed that he could hear water change temperature.
|
||||
The door slammed on the watermelon.
|
||||
The two walked down the slot canyon oblivious to the sound of thunder in the distance.
|
||||
We should play with legos at camp.
|
||||
The delicious aroma from the kitchen was ruined by cigarette smoke.
|
||||
Everyone says they love nature until they realize how dangerous she can be.
|
||||
Eating eggs on Thursday for choir practice was recommended.
|
||||
Thigh-high in the water, the fisherman’s hope for dinner soon turned to despair.
|
||||
The beauty of the African sunset disguised the danger lurking nearby.
|
||||
He learned the hardest lesson of his life and had the scars, both physical and mental, to prove it.
|
||||
Seek success, but always be prepared for random cats.
|
||||
She advised him to come back at once.
|
||||
This book is sure to liquefy your brain.
|
||||
He found his art never progressed when he literally used his sweat and tears.
|
||||
He is no James Bond; his name is Roger Moore.
|
||||
He excelled at firing people nicely.
|
||||
There was no ice cream in the freezer, nor did they have money to go to the store.
|
||||
When I cook spaghetti, I like to boil it a few minutes past al dente so the noodles are super slippery.
|
||||
He went back to the video to see what had been recorded and was shocked at what he saw.
|
||||
He strives to keep the best lawn in the neighborhood.
|
||||
Even with the snow falling outside, she felt it appropriate to wear her bikini.
|
||||
He turned in the research paper on Friday; otherwise, he would have not passed the class.
|
||||
8% of 25 is the same as 25% of 8 and one of them is much easier to do in your head.
|
||||
Had he known what was going to happen, he would have never stepped into the shower.
|
||||
Tomatoes make great weapons when water balloons aren’t available.
|
||||
The rain pelted the windshield as the darkness engulfed us.
|
||||
There should have been a time and a place, but this wasn't it.
|
||||
Peter found road kill an excellent way to save money on dinner.
|
||||
Kevin embraced his ability to be at the wrong place at the wrong time.
|
||||
Flesh-colored yoga pants were far worse than even he feared.
|
||||
This made him feel like an old-style rootbeer float smells.
|
||||
She was amazed by the large chunks of ice washing up on the beach.
|
||||
It caught him off guard that space smelled of seared steak.
|
||||
You realize you're not alone as you sit in your bedroom massaging your calves after a long day of playing tug-of-war with Grandpa Joe in the hospital.
|
||||
To the surprise of everyone, the Rapture happened yesterday but it didn't quite go as expected.
|
||||
Pink horses galloped across the sea.
|
||||
The complicated school homework left the parents trying to help their kids quite confused.
|
||||
It was obvious she was hot, sweaty, and tired.
|
||||
I really want to go to work, but I am too sick to drive.
|
||||
While all her friends were positive that Mary had a sixth sense, she knew she actually had a seventh sense.
|
||||
I only enjoy window shopping when the windows are transparent.
|
||||
I want more detailed information.
|
||||
Their argument could be heard across the parking lot.
|
||||
Writing a list of random sentences is harder than I initially thought it would be.
|
||||
He picked up trash in his spare time to dump in his neighbor's yard.
|
||||
He had a vague sense that trees gave birth to dinosaurs.
|
||||
He colored deep space a soft yellow.
|
||||
I'm worried by the fact that my daughter looks to the local carpet seller as a role model.
|
||||
It caught him off guard that space smelled of seared steak.
|
||||
Art doesn't have to be intentional.
|
||||
She found it strange that people use their cellphones to actually talk to one another.
|
||||
He wondered why at 18 he was old enough to go to war, but not old enough to buy cigarettes.
|
||||
The father died during childbirth.
|
||||
Kevin embraced his ability to be at the wrong place at the wrong time.
|
||||
The fact that there's a stairway to heaven and a highway to hell explains life well.
|
||||
You've been eyeing me all day and waiting for your move like a lion stalking a gazelle in a savannah.
|
||||
Twin 4-month-olds slept in the shade of the palm tree while the mother tanned in the sun.
|
||||
The paintbrush was angry at the color the artist chose to use.
|
||||
The fifty mannequin heads floating in the pool kind of freaked them out.
|
||||
He told us a very exciting adventure story.
|
||||
The Great Dane looked more like a horse than a dog.
|
||||
Jeanne wished she has chosen the red button.
|
||||
The sudden rainstorm washed crocodiles into the ocean.
|
||||
He put heat on the wound to see what would grow.
|
||||
The bread dough reminded her of Santa Clause’s belly.
|
||||
He decided to fake his disappearance to avoid jail.
|
||||
Let me help you with your baggage.
|
||||
The stranger officiates the meal.
|
||||
As the rental car rolled to a stop on the dark road, her fear increased by the moment.
|
||||
The sunblock was handed to the girl before practice, but the burned skin was proof she did not apply it.
|
||||
Happiness can be found in the depths of chocolate pudding.
|
||||
Of course, she loves her pink bunny slippers.
|
||||
After fighting off the alligator, Brian still had to face the anaconda.
|
||||
She couldn't decide of the glass was half empty or half full so she drank it.
|
||||
I want more detailed information.
|
||||
Gary didn't understand why Doug went upstairs to get one dollar bills when he invited him to go cow tipping.
|
||||
She works two jobs to make ends meet; at least, that was her reason for not having time to join us.
|
||||
I cheated while playing the darts tournament by using a longbow.
|
||||
All they could see was the blue water surrounding their sailboat.
|
||||
He was the type of guy who liked Christmas lights on his house in the middle of July.
|
||||
It was a slippery slope and he was willing to slide all the way to the deepest depths.
|
||||
The fish listened intently to what the frogs had to say.
|
||||
The book is in front of the table.
|
||||
You have every right to be angry, but that doesn't give you the right to be mean.
|
||||
He waited for the stop sign to turn to a go sign.
|
||||
The gloves protect my feet from excess work.
|
||||
Nancy thought the best way to create a welcoming home was to line it with barbed wire.
|
||||
Best friends are like old tomatoes and shoelaces.
|
||||
He fumbled in the darkness looking for the light switch, but when he finally found it there was someone already there.
|
||||
They say that dogs are man's best friend, but this cat was setting out to sabotage that theory.
|
||||
Some bathing suits just shouldn’t be worn by some people.
|
||||
Dolores wouldn't have eaten the meal if she had known what it actually was.
|
||||
She did her best to help him.
|
||||
Pantyhose and heels are an interesting choice of attire for the beach.
|
||||
Jason didn’t understand why his parents wouldn’t let him sell his little sister at the garage sale.
|
||||
Smoky the Bear secretly started the fires.
|
||||
He wondered if she would appreciate his toenail collection.
|
||||
The thick foliage and intertwined vines made the hike nearly impossible.
|
||||
Purple is the best city in the forest.
|
||||
Nothing seemed out of place except the washing machine in the bar.
|
||||
He didn't understand why the bird wanted to ride the bicycle.
|
||||
Jerry liked to look at paintings while eating garlic ice cream.
|
||||
This book is sure to liquefy your brain.
|
||||
She saw the brake lights, but not in time.
|
||||
I'm worried by the fact that my daughter looks to the local carpet seller as a role model.
|
||||
Whenever he saw a red flag warning at the beach he grabbed his surfboard.
|
||||
She wanted to be rescued, but only if it was Tuesday and raining.
|
||||
Patricia found the meaning of life in a bowl of Cheerios.
|
||||
There was no telling what thoughts would come from the machine.
|
||||
The beach was crowded with snow leopards.
|
||||
He found the chocolate covered roaches quite tasty.
|
||||
She opened up her third bottle of wine of the night.
|
||||
The urgent care center was flooded with patients after the news of a new deadly virus was made public.
|
||||
Lucifer was surprised at the amount of life at Death Valley.
|
||||
When he had to picnic on the beach, he purposely put sand in other people’s food.
|
||||
The book is in front of the table.
|
||||
Gary didn't understand why Doug went upstairs to get one dollar bills when he invited him to go cow tipping.
|
||||
She traveled because it cost the same as therapy and was a lot more enjoyable.
|
||||
Eating eggs on Thursday for choir practice was recommended.
|
||||
Normal activities took extraordinary amounts of concentration at the high altitude.
|
||||
If eating three-egg omelets causes weight-gain, budgie eggs are a good substitute.
|
||||
His mind was blown that there was nothing in space except space itself.
|
||||
The clouds formed beautiful animals in the sky that eventually created a tornado to wreak havoc.
|
||||
That must be the tenth time I've been arrested for selling deep-fried cigars.
|
||||
Never underestimate the willingness of the greedy to throw you under the bus.
|
||||
The team members were hard to tell apart since they all wore their hair in a ponytail.
|
||||
When I cook spaghetti, I like to boil it a few minutes past al dente so the noodles are super slippery.
|
||||
Their argument could be heard across the parking lot.
|
||||
He decided that the time had come to be stronger than any of the excuses he'd used until then.
|
||||
All she wanted was the answer, but she had no idea how much she would hate it.
|
||||
If my calculator had a history, it would be more embarrassing than my browser history.
|
||||
Just go ahead and press that button.
|
||||
I caught my squirrel rustling through my gym bag.
|
||||
On each full moon
|
||||
It was the scarcity that fueled his creativity.
|
||||
Two seats were vacant.
|
||||
The tree fell unexpectedly short.
|
||||
Getting up at dawn is for the birds.
|
||||
Please put on these earmuffs because I can't you hear.
|
||||
Her scream silenced the rowdy teenagers.
|
||||
Traveling became almost extinct during the pandemic.
|
||||
I can't believe this is the eighth time I'm smashing open my piggy bank on the same day!
|
||||
For some unfathomable reason, the response team didn't consider a lack of milk for my cereal as a proper emergency.
|
||||
The beauty of the African sunset disguised the danger lurking nearby.
|
||||
Acres of almond trees lined the interstate highway which complimented the crazy driving nuts.
|
||||
The fish listened intently to what the frogs had to say.
|
||||
She wondered what his eyes were saying beneath his mirrored sunglasses.
|
||||
|
200
resources/runtime/language/english100
Normal file
200
resources/runtime/language/english100
Normal file
|
@ -0,0 +1,200 @@
|
|||
the
|
||||
be
|
||||
of
|
||||
and
|
||||
a
|
||||
to
|
||||
in
|
||||
he
|
||||
have
|
||||
it
|
||||
that
|
||||
for
|
||||
they
|
||||
I
|
||||
with
|
||||
as
|
||||
not
|
||||
on
|
||||
she
|
||||
at
|
||||
by
|
||||
this
|
||||
we
|
||||
you
|
||||
do
|
||||
but
|
||||
from
|
||||
or
|
||||
which
|
||||
one
|
||||
would
|
||||
all
|
||||
will
|
||||
there
|
||||
say
|
||||
who
|
||||
make
|
||||
when
|
||||
can
|
||||
more
|
||||
if
|
||||
no
|
||||
man
|
||||
out
|
||||
other
|
||||
so
|
||||
what
|
||||
time
|
||||
up
|
||||
go
|
||||
about
|
||||
than
|
||||
into
|
||||
could
|
||||
state
|
||||
only
|
||||
new
|
||||
year
|
||||
some
|
||||
take
|
||||
come
|
||||
these
|
||||
know
|
||||
see
|
||||
use
|
||||
get
|
||||
like
|
||||
then
|
||||
first
|
||||
any
|
||||
work
|
||||
now
|
||||
may
|
||||
such
|
||||
give
|
||||
over
|
||||
think
|
||||
most
|
||||
even
|
||||
find
|
||||
day
|
||||
also
|
||||
after
|
||||
way
|
||||
many
|
||||
must
|
||||
look
|
||||
before
|
||||
great
|
||||
back
|
||||
through
|
||||
long
|
||||
where
|
||||
much
|
||||
should
|
||||
well
|
||||
people
|
||||
down
|
||||
own
|
||||
just
|
||||
because
|
||||
good
|
||||
each
|
||||
those
|
||||
feel
|
||||
seem
|
||||
how
|
||||
high
|
||||
too
|
||||
place
|
||||
little
|
||||
world
|
||||
very
|
||||
still
|
||||
nation
|
||||
hand
|
||||
old
|
||||
life
|
||||
tell
|
||||
write
|
||||
become
|
||||
here
|
||||
show
|
||||
house
|
||||
both
|
||||
between
|
||||
need
|
||||
mean
|
||||
call
|
||||
develop
|
||||
under
|
||||
last
|
||||
right
|
||||
move
|
||||
thing
|
||||
general
|
||||
school
|
||||
never
|
||||
same
|
||||
another
|
||||
begin
|
||||
while
|
||||
number
|
||||
part
|
||||
turn
|
||||
real
|
||||
leave
|
||||
might
|
||||
want
|
||||
point
|
||||
form
|
||||
off
|
||||
child
|
||||
few
|
||||
small
|
||||
since
|
||||
against
|
||||
ask
|
||||
late
|
||||
home
|
||||
interest
|
||||
large
|
||||
person
|
||||
end
|
||||
open
|
||||
public
|
||||
follow
|
||||
during
|
||||
present
|
||||
without
|
||||
again
|
||||
hold
|
||||
govern
|
||||
around
|
||||
possible
|
||||
head
|
||||
consider
|
||||
word
|
||||
program
|
||||
problem
|
||||
however
|
||||
lead
|
||||
system
|
||||
set
|
||||
order
|
||||
eye
|
||||
plan
|
||||
run
|
||||
keep
|
||||
face
|
||||
fact
|
||||
group
|
||||
play
|
||||
stand
|
||||
increase
|
||||
early
|
||||
course
|
||||
change
|
||||
help
|
||||
line
|
1000
resources/runtime/language/english1000
Normal file
1000
resources/runtime/language/english1000
Normal file
File diff suppressed because it is too large
Load diff
9949
resources/runtime/language/english10000
Normal file
9949
resources/runtime/language/english10000
Normal file
File diff suppressed because it is too large
Load diff
200
resources/runtime/language/english200
Normal file
200
resources/runtime/language/english200
Normal file
|
@ -0,0 +1,200 @@
|
|||
the
|
||||
of
|
||||
and
|
||||
to
|
||||
in
|
||||
for
|
||||
is
|
||||
on
|
||||
that
|
||||
by
|
||||
this
|
||||
with
|
||||
you
|
||||
it
|
||||
not
|
||||
or
|
||||
be
|
||||
are
|
||||
from
|
||||
at
|
||||
as
|
||||
your
|
||||
all
|
||||
have
|
||||
new
|
||||
more
|
||||
an
|
||||
was
|
||||
we
|
||||
will
|
||||
home
|
||||
can
|
||||
us
|
||||
about
|
||||
if
|
||||
page
|
||||
my
|
||||
has
|
||||
search
|
||||
free
|
||||
but
|
||||
our
|
||||
one
|
||||
other
|
||||
do
|
||||
no
|
||||
information
|
||||
time
|
||||
they
|
||||
site
|
||||
he
|
||||
up
|
||||
may
|
||||
what
|
||||
which
|
||||
their
|
||||
news
|
||||
out
|
||||
use
|
||||
any
|
||||
there
|
||||
see
|
||||
only
|
||||
so
|
||||
his
|
||||
when
|
||||
contact
|
||||
here
|
||||
business
|
||||
who
|
||||
web
|
||||
also
|
||||
now
|
||||
help
|
||||
get
|
||||
pm
|
||||
view
|
||||
online
|
||||
first
|
||||
am
|
||||
been
|
||||
would
|
||||
how
|
||||
were
|
||||
me
|
||||
services
|
||||
some
|
||||
these
|
||||
click
|
||||
its
|
||||
like
|
||||
service
|
||||
than
|
||||
find
|
||||
price
|
||||
date
|
||||
back
|
||||
top
|
||||
people
|
||||
had
|
||||
list
|
||||
name
|
||||
just
|
||||
over
|
||||
state
|
||||
year
|
||||
day
|
||||
into
|
||||
email
|
||||
two
|
||||
health
|
||||
world
|
||||
re
|
||||
next
|
||||
used
|
||||
go
|
||||
work
|
||||
last
|
||||
most
|
||||
products
|
||||
music
|
||||
buy
|
||||
data
|
||||
make
|
||||
them
|
||||
should
|
||||
product
|
||||
system
|
||||
post
|
||||
her
|
||||
city
|
||||
add
|
||||
policy
|
||||
number
|
||||
such
|
||||
please
|
||||
available
|
||||
copyright
|
||||
support
|
||||
message
|
||||
after
|
||||
best
|
||||
software
|
||||
then
|
||||
jan
|
||||
good
|
||||
video
|
||||
well
|
||||
where
|
||||
info
|
||||
rights
|
||||
public
|
||||
books
|
||||
high
|
||||
school
|
||||
through
|
||||
each
|
||||
links
|
||||
she
|
||||
review
|
||||
years
|
||||
order
|
||||
very
|
||||
privacy
|
||||
book
|
||||
items
|
||||
company
|
||||
read
|
||||
group
|
||||
need
|
||||
many
|
||||
user
|
||||
said
|
||||
de
|
||||
does
|
||||
set
|
||||
under
|
||||
general
|
||||
research
|
||||
university
|
||||
january
|
||||
mail
|
||||
full
|
||||
map
|
||||
reviews
|
||||
program
|
||||
life
|
||||
know
|
||||
games
|
||||
way
|
||||
days
|
||||
management
|
||||
part
|
||||
could
|
||||
great
|
||||
united
|
||||
hotel
|
||||
real
|
||||
item
|
||||
international
|
4958
resources/runtime/language/english5000
Normal file
4958
resources/runtime/language/english5000
Normal file
File diff suppressed because it is too large
Load diff
207
resources/runtime/language/german
Normal file
207
resources/runtime/language/german
Normal file
|
@ -0,0 +1,207 @@
|
|||
die
|
||||
der
|
||||
und
|
||||
in
|
||||
zu
|
||||
den
|
||||
das
|
||||
nicht
|
||||
von
|
||||
sie
|
||||
ist
|
||||
des
|
||||
sich
|
||||
mit
|
||||
dem
|
||||
dass
|
||||
er
|
||||
es
|
||||
ein
|
||||
ich
|
||||
auf
|
||||
so
|
||||
eine
|
||||
auch
|
||||
als
|
||||
an
|
||||
nach
|
||||
wie
|
||||
im
|
||||
für
|
||||
man
|
||||
aber
|
||||
aus
|
||||
durch
|
||||
wenn
|
||||
nur
|
||||
war
|
||||
noch
|
||||
werden
|
||||
bei
|
||||
hat
|
||||
wir
|
||||
was
|
||||
wird
|
||||
sein
|
||||
einen
|
||||
welche
|
||||
sind
|
||||
oder
|
||||
zur
|
||||
um
|
||||
haben
|
||||
einer
|
||||
mir
|
||||
über
|
||||
ihm
|
||||
diese
|
||||
einem
|
||||
ihr
|
||||
uns
|
||||
da
|
||||
zum
|
||||
kann
|
||||
doch
|
||||
vor
|
||||
dieser
|
||||
mich
|
||||
ihn
|
||||
du
|
||||
hatte
|
||||
seine
|
||||
mehr
|
||||
am
|
||||
denn
|
||||
nun
|
||||
unter
|
||||
sehr
|
||||
selbst
|
||||
schon
|
||||
hier
|
||||
bis
|
||||
habe
|
||||
ihre
|
||||
dann
|
||||
ihnen
|
||||
seiner
|
||||
alle
|
||||
wieder
|
||||
meine
|
||||
Zeit
|
||||
gegen
|
||||
vom
|
||||
ganz
|
||||
einzelnen
|
||||
wo
|
||||
muss
|
||||
ohne
|
||||
eines
|
||||
können
|
||||
sei
|
||||
ja
|
||||
wurde
|
||||
jetzt
|
||||
immer
|
||||
seinen
|
||||
wohl
|
||||
dieses
|
||||
ihren
|
||||
würde
|
||||
diesen
|
||||
sondern
|
||||
weil
|
||||
welcher
|
||||
nichts
|
||||
diesem
|
||||
alles
|
||||
waren
|
||||
will
|
||||
Herr
|
||||
viel
|
||||
mein
|
||||
also
|
||||
soll
|
||||
worden
|
||||
lassen
|
||||
dies
|
||||
machen
|
||||
ihrer
|
||||
weiter
|
||||
Leben
|
||||
recht
|
||||
etwas
|
||||
keine
|
||||
seinem
|
||||
ob
|
||||
dir
|
||||
allen
|
||||
großen
|
||||
Jahre
|
||||
Weise
|
||||
müssen
|
||||
welches
|
||||
wäre
|
||||
erst
|
||||
einmal
|
||||
Mann
|
||||
hätte
|
||||
zwei
|
||||
dich
|
||||
allein
|
||||
Herren
|
||||
während
|
||||
Paragraph
|
||||
anders
|
||||
Liebe
|
||||
kein
|
||||
damit
|
||||
gar
|
||||
Hand
|
||||
Herrn
|
||||
euch
|
||||
sollte
|
||||
konnte
|
||||
ersten
|
||||
deren
|
||||
zwischen
|
||||
wollen
|
||||
denen
|
||||
dessen
|
||||
sagen
|
||||
bin
|
||||
Menschen
|
||||
gut
|
||||
darauf
|
||||
wurden
|
||||
weiß
|
||||
gewesen
|
||||
Seite
|
||||
bald
|
||||
weit
|
||||
große
|
||||
solche
|
||||
hatten
|
||||
eben
|
||||
andern
|
||||
beiden
|
||||
macht
|
||||
sehen
|
||||
ganze
|
||||
anderen
|
||||
lange
|
||||
wer
|
||||
ihrem
|
||||
zwar
|
||||
gemacht
|
||||
dort
|
||||
kommen
|
||||
Welt
|
||||
heute
|
||||
Frau
|
||||
werde
|
||||
derselben
|
||||
ganzen
|
||||
deutschen
|
||||
lässt
|
||||
vielleicht
|
||||
meiner
|
1000
resources/runtime/language/german1000
Normal file
1000
resources/runtime/language/german1000
Normal file
File diff suppressed because it is too large
Load diff
10000
resources/runtime/language/german10000
Normal file
10000
resources/runtime/language/german10000
Normal file
File diff suppressed because it is too large
Load diff
25
resources/runtime/language/go
Normal file
25
resources/runtime/language/go
Normal file
|
@ -0,0 +1,25 @@
|
|||
break
|
||||
default
|
||||
func
|
||||
interface
|
||||
select
|
||||
case
|
||||
defer
|
||||
go
|
||||
map
|
||||
struct
|
||||
chan
|
||||
else
|
||||
goto
|
||||
package
|
||||
switch
|
||||
const
|
||||
fallthrough
|
||||
if
|
||||
range
|
||||
type
|
||||
continue
|
||||
for
|
||||
import
|
||||
return
|
||||
var
|
329
resources/runtime/language/html
Normal file
329
resources/runtime/language/html
Normal file
|
@ -0,0 +1,329 @@
|
|||
action
|
||||
alt
|
||||
class
|
||||
fill
|
||||
for
|
||||
height
|
||||
href
|
||||
href
|
||||
href
|
||||
href
|
||||
id
|
||||
kind
|
||||
max
|
||||
media
|
||||
method
|
||||
min
|
||||
name
|
||||
src
|
||||
src
|
||||
src
|
||||
srclang
|
||||
srcset
|
||||
stroke
|
||||
stroke-width
|
||||
style
|
||||
stylesheet
|
||||
stylesheet
|
||||
title
|
||||
type
|
||||
type
|
||||
value
|
||||
width
|
||||
<!DOCTYPE
|
||||
<!--
|
||||
-->
|
||||
<a
|
||||
</a>
|
||||
<a
|
||||
</a>
|
||||
<a
|
||||
</a>
|
||||
<a
|
||||
</a>
|
||||
<abbr
|
||||
</abbr>
|
||||
<address>
|
||||
</address>
|
||||
<area
|
||||
<article>
|
||||
</article>
|
||||
<aside>
|
||||
</aside>
|
||||
<audio
|
||||
</audio>
|
||||
<b>
|
||||
</b>
|
||||
<b>
|
||||
</b>
|
||||
<b>
|
||||
</b>
|
||||
<b>
|
||||
</b>
|
||||
<base
|
||||
<bdi>
|
||||
</bdi>
|
||||
<bdo
|
||||
</bdo>
|
||||
<blockquote
|
||||
</blockquote>
|
||||
<body>
|
||||
</body>
|
||||
<body>
|
||||
</body>
|
||||
<body>
|
||||
</body>
|
||||
<body>
|
||||
</body>
|
||||
<br>
|
||||
<br>
|
||||
<br>
|
||||
<br>
|
||||
<button
|
||||
</button>
|
||||
<canvas
|
||||
</canvas>
|
||||
<caption>
|
||||
</caption>
|
||||
<cite>
|
||||
</cite>
|
||||
<code>
|
||||
</code>
|
||||
<colgroup>
|
||||
<col
|
||||
</colgroup>
|
||||
<data
|
||||
</data>
|
||||
<datalist
|
||||
</datalist>
|
||||
<dd>
|
||||
</dd>
|
||||
<dl>
|
||||
</dl>
|
||||
<del>
|
||||
</del>
|
||||
<details>
|
||||
</details>
|
||||
<dfn
|
||||
</dfn>
|
||||
<dialog
|
||||
</dialog>
|
||||
<div
|
||||
</div>
|
||||
<div
|
||||
</div>
|
||||
<div
|
||||
</div>
|
||||
<div
|
||||
</div>
|
||||
<dt>
|
||||
</dt>
|
||||
<em>
|
||||
</em>
|
||||
<embed
|
||||
<fieldset>
|
||||
</fieldset>
|
||||
<figcaption>
|
||||
</figcaption>
|
||||
<figure>
|
||||
</figure>
|
||||
<form
|
||||
</form>
|
||||
<footer>
|
||||
</footer>
|
||||
<label
|
||||
</label>
|
||||
<header>
|
||||
</header>
|
||||
<head>
|
||||
</head>
|
||||
<head>
|
||||
</head>
|
||||
<head>
|
||||
</head>
|
||||
<head>
|
||||
</head>
|
||||
<html
|
||||
</html>
|
||||
<html
|
||||
</html>
|
||||
<html
|
||||
</html>
|
||||
<html
|
||||
</html>
|
||||
<hr>
|
||||
<h1>
|
||||
</h1>
|
||||
<h2>
|
||||
</h2>
|
||||
<h3>
|
||||
</h3>
|
||||
<h4>
|
||||
</h4>
|
||||
<h5>
|
||||
</h5>
|
||||
<h6>
|
||||
</h6>
|
||||
<i>
|
||||
</i>
|
||||
<i>
|
||||
</i>
|
||||
<i>
|
||||
</i>
|
||||
<iframe
|
||||
</iframe>
|
||||
<img
|
||||
<img
|
||||
<img
|
||||
<img
|
||||
<input
|
||||
<ins>
|
||||
</ins>
|
||||
<kbd>
|
||||
</kbd>
|
||||
<legend>
|
||||
</legend>
|
||||
<li>
|
||||
</li>
|
||||
<li>
|
||||
</li>
|
||||
<li>
|
||||
</li>
|
||||
<li>
|
||||
</li>
|
||||
<link
|
||||
<link
|
||||
<main>
|
||||
</main>
|
||||
<map
|
||||
</map>
|
||||
<mark>
|
||||
</mark>
|
||||
<meta
|
||||
<meter
|
||||
</meter>
|
||||
<nav>
|
||||
</nav>
|
||||
<noscript>
|
||||
</noscript>
|
||||
<object
|
||||
</object>
|
||||
<ol>
|
||||
</ol>
|
||||
<ol>
|
||||
</ol>
|
||||
<ol>
|
||||
</ol>
|
||||
<ol>
|
||||
</ol>
|
||||
<optgroup
|
||||
</optgroup>
|
||||
<option
|
||||
</option>
|
||||
<output
|
||||
</output>
|
||||
<p>
|
||||
</p>
|
||||
<p>
|
||||
</p>
|
||||
<p>
|
||||
</p>
|
||||
<p>
|
||||
</p>
|
||||
<p>
|
||||
</p>
|
||||
<p>
|
||||
</p>
|
||||
<param
|
||||
<picture>
|
||||
</picture>
|
||||
<pre>
|
||||
</pre>
|
||||
<progress
|
||||
</progress>
|
||||
<q>
|
||||
</q>
|
||||
<rp>
|
||||
</rp>
|
||||
<ruby>
|
||||
</ruby>
|
||||
<rt>
|
||||
</rt>
|
||||
<s>
|
||||
</s>
|
||||
<samp>
|
||||
</samp>
|
||||
<script>
|
||||
</script>
|
||||
<script>
|
||||
</script>
|
||||
<section>
|
||||
</section>
|
||||
<select
|
||||
</select>
|
||||
<source
|
||||
<span
|
||||
</span>
|
||||
<span
|
||||
</span>
|
||||
<span
|
||||
</span>
|
||||
<span
|
||||
</span>
|
||||
<strong>
|
||||
<strong>
|
||||
<strong>
|
||||
<strong>
|
||||
</strong>
|
||||
<style
|
||||
<style>
|
||||
<style
|
||||
<style>
|
||||
<sub>
|
||||
</sub>
|
||||
<summary>
|
||||
</summary>
|
||||
<sup>
|
||||
</sup>
|
||||
<svg
|
||||
</svg>
|
||||
<table>
|
||||
</table>
|
||||
<tbody>
|
||||
</tbody>
|
||||
<td>
|
||||
</td>
|
||||
<template>
|
||||
</template>
|
||||
<textarea
|
||||
</textarea>
|
||||
<tfoot>
|
||||
</tfoot>
|
||||
<thead>
|
||||
</thead>
|
||||
<th>
|
||||
</th>
|
||||
<time
|
||||
<time>
|
||||
</time>
|
||||
<title>
|
||||
</title>
|
||||
<title>
|
||||
</title>
|
||||
<track
|
||||
<u>
|
||||
</u>
|
||||
<ul>
|
||||
</ul>
|
||||
<ul>
|
||||
</ul>
|
||||
<ul>
|
||||
</ul>
|
||||
<ul>
|
||||
</ul>
|
||||
<var>
|
||||
</var>
|
||||
<video
|
||||
</video>
|
||||
<wbr>
|
||||
</wbr>
|
161745
resources/runtime/language/hungarian-all
Normal file
161745
resources/runtime/language/hungarian-all
Normal file
File diff suppressed because it is too large
Load diff
64
resources/runtime/language/java
Normal file
64
resources/runtime/language/java
Normal file
|
@ -0,0 +1,64 @@
|
|||
abstract
|
||||
assert
|
||||
boolean
|
||||
break
|
||||
byte
|
||||
case
|
||||
catch
|
||||
char
|
||||
class
|
||||
continue
|
||||
default
|
||||
do
|
||||
double
|
||||
else
|
||||
enum
|
||||
extends
|
||||
final
|
||||
finally
|
||||
float
|
||||
for
|
||||
if
|
||||
implements
|
||||
import
|
||||
instanceof
|
||||
interface
|
||||
long
|
||||
native
|
||||
new
|
||||
null
|
||||
package
|
||||
private
|
||||
protected
|
||||
public
|
||||
return
|
||||
short
|
||||
static
|
||||
strictfp
|
||||
super
|
||||
switch
|
||||
synchronized
|
||||
this
|
||||
throw
|
||||
throws
|
||||
transient
|
||||
try
|
||||
void
|
||||
volatile
|
||||
while
|
||||
valueOf
|
||||
from
|
||||
parse
|
||||
get
|
||||
contains
|
||||
remove
|
||||
clear
|
||||
put
|
||||
set
|
||||
with
|
||||
throwas
|
||||
build
|
||||
add
|
||||
subtract
|
||||
append
|
||||
length
|
51
resources/runtime/language/javascript
Normal file
51
resources/runtime/language/javascript
Normal file
|
@ -0,0 +1,51 @@
|
|||
this
|
||||
function
|
||||
if
|
||||
var
|
||||
return
|
||||
the
|
||||
to
|
||||
value
|
||||
else
|
||||
for
|
||||
true
|
||||
length
|
||||
false
|
||||
null
|
||||
of
|
||||
in
|
||||
element
|
||||
event
|
||||
and
|
||||
object
|
||||
console
|
||||
object
|
||||
jQuery
|
||||
node
|
||||
while
|
||||
do
|
||||
if
|
||||
break
|
||||
continue
|
||||
attributes
|
||||
childNodes
|
||||
firstChild
|
||||
nodeName
|
||||
nodeType
|
||||
onclick
|
||||
ondbclick
|
||||
onmousedown
|
||||
onmouseenter
|
||||
onmouseup
|
||||
onkeyup
|
||||
onkeydown
|
||||
onkeypress
|
||||
oninput
|
||||
oninvalid
|
||||
onreset
|
||||
onselect
|
||||
ondrag
|
||||
try
|
||||
catch
|
||||
throw
|
||||
finally
|
200
resources/runtime/language/norwegian
Normal file
200
resources/runtime/language/norwegian
Normal file
|
@ -0,0 +1,200 @@
|
|||
i
|
||||
og
|
||||
det
|
||||
er
|
||||
på
|
||||
til
|
||||
som
|
||||
en
|
||||
å
|
||||
for
|
||||
av
|
||||
at
|
||||
har
|
||||
med
|
||||
de
|
||||
ikke
|
||||
den
|
||||
han
|
||||
om
|
||||
et
|
||||
fra
|
||||
men
|
||||
vi
|
||||
var
|
||||
jeg
|
||||
seg
|
||||
sier
|
||||
vil
|
||||
kan
|
||||
ble
|
||||
skal
|
||||
etter
|
||||
også
|
||||
så
|
||||
ut
|
||||
år
|
||||
nå
|
||||
da
|
||||
dette
|
||||
blir
|
||||
ved
|
||||
mot
|
||||
hadde
|
||||
to
|
||||
hun
|
||||
over
|
||||
være
|
||||
ha
|
||||
må
|
||||
går
|
||||
opp
|
||||
få
|
||||
andre
|
||||
eller
|
||||
bare
|
||||
sin
|
||||
mer
|
||||
inn
|
||||
før
|
||||
bli
|
||||
vært
|
||||
enn
|
||||
alle
|
||||
kroner
|
||||
noe
|
||||
når
|
||||
noen
|
||||
selv
|
||||
denne
|
||||
flere
|
||||
mange
|
||||
får
|
||||
du
|
||||
dag
|
||||
der
|
||||
mener
|
||||
nye
|
||||
fikk
|
||||
norge
|
||||
under
|
||||
første
|
||||
sa
|
||||
siden
|
||||
prosent
|
||||
tre
|
||||
kommer
|
||||
ingen
|
||||
man
|
||||
siste
|
||||
mellom
|
||||
mye
|
||||
oslo
|
||||
hele
|
||||
kunne
|
||||
slik
|
||||
norske
|
||||
hva
|
||||
både
|
||||
millioner
|
||||
oss
|
||||
dem
|
||||
store
|
||||
gang
|
||||
skulle
|
||||
kom
|
||||
hans
|
||||
her
|
||||
helt
|
||||
tidligere
|
||||
ville
|
||||
hvor
|
||||
blant
|
||||
sine
|
||||
politiet
|
||||
ta
|
||||
meg
|
||||
sammen
|
||||
blitt
|
||||
igjen
|
||||
rundt
|
||||
mens
|
||||
nok
|
||||
godt
|
||||
saken
|
||||
fått
|
||||
ned
|
||||
alt
|
||||
uten
|
||||
norsk
|
||||
tid
|
||||
ham
|
||||
ny
|
||||
samme
|
||||
gikk
|
||||
gå
|
||||
sitt
|
||||
tilbake
|
||||
annet
|
||||
foto
|
||||
ser
|
||||
hvis
|
||||
se
|
||||
gjøre
|
||||
ifølge
|
||||
fire
|
||||
tror
|
||||
stor
|
||||
gjennom
|
||||
litt
|
||||
tatt
|
||||
folk
|
||||
fordi
|
||||
gjør
|
||||
står
|
||||
disse
|
||||
derfor
|
||||
langt
|
||||
like
|
||||
neste
|
||||
komme
|
||||
mest
|
||||
fjor
|
||||
god
|
||||
tar
|
||||
bedre
|
||||
fortsatt
|
||||
allerede
|
||||
viser
|
||||
gi
|
||||
si
|
||||
gamle
|
||||
stavanger
|
||||
ønsker
|
||||
måtte
|
||||
vet
|
||||
fram
|
||||
først
|
||||
grunn
|
||||
ligger
|
||||
fem
|
||||
gjort
|
||||
likevel
|
||||
tok
|
||||
gir
|
||||
kveld
|
||||
kanskje
|
||||
del
|
||||
aldri
|
||||
heller
|
||||
satt
|
||||
leder
|
||||
beste
|
||||
hvordan
|
||||
svært
|
||||
fredag
|
||||
jo
|
||||
dermed
|
||||
hatt
|
||||
bør
|
||||
lørdag
|
74
resources/runtime/language/php
Normal file
74
resources/runtime/language/php
Normal file
|
@ -0,0 +1,74 @@
|
|||
array_key_exists
|
||||
array_keys
|
||||
array_map
|
||||
array_merge
|
||||
array_pop
|
||||
array_shift
|
||||
array_unshift
|
||||
array_values
|
||||
array_walk
|
||||
basename
|
||||
class_exists
|
||||
compact
|
||||
count
|
||||
date
|
||||
declare
|
||||
defined
|
||||
dirname
|
||||
echo
|
||||
end
|
||||
explode
|
||||
file_exists
|
||||
file_get_contents
|
||||
function_exists
|
||||
get_class
|
||||
implode
|
||||
in_array
|
||||
intval
|
||||
is_array
|
||||
is_bool
|
||||
is_dir
|
||||
is_file
|
||||
is_int
|
||||
is_null
|
||||
is_numeric
|
||||
is_object
|
||||
is_string
|
||||
json_encode
|
||||
max
|
||||
mb_strlen
|
||||
mb_strpos
|
||||
mb_strtolower
|
||||
min
|
||||
preg_match
|
||||
preg_replace
|
||||
print_r
|
||||
realpath
|
||||
reset
|
||||
rtrim
|
||||
sprintf
|
||||
str_repeat
|
||||
str_replace
|
||||
str_starts_with
|
||||
substr
|
||||
time
|
||||
trim
|
||||
uniqid
|
||||
unlink
|
||||
var_dump
|
||||
<?php
|
||||
if
|
||||
else
|
||||
elseif
|
||||
switch
|
||||
case
|
||||
match
|
||||
for
|
||||
foreach
|
||||
while
|
||||
do
|
||||
break
|
||||
continue
|
||||
return
|
||||
=>
|
||||
->
|
100
resources/runtime/language/portuguese
Normal file
100
resources/runtime/language/portuguese
Normal file
|
@ -0,0 +1,100 @@
|
|||
pra
|
||||
também
|
||||
vocês
|
||||
quê
|
||||
algo
|
||||
obrigado
|
||||
dia
|
||||
esse
|
||||
lhe
|
||||
este
|
||||
ir
|
||||
deus
|
||||
essa
|
||||
oh
|
||||
melhor
|
||||
ainda
|
||||
temos
|
||||
cara
|
||||
sem
|
||||
pai
|
||||
sempre
|
||||
vida
|
||||
vez
|
||||
homem
|
||||
estamos
|
||||
talvez
|
||||
mãe
|
||||
anos
|
||||
alguém
|
||||
depois
|
||||
verdade
|
||||
claro
|
||||
boa
|
||||
nem
|
||||
pouco
|
||||
ficar
|
||||
coisas
|
||||
aí
|
||||
tinha
|
||||
dois
|
||||
falar
|
||||
deve
|
||||
antes
|
||||
pelo
|
||||
faz
|
||||
parece
|
||||
todo
|
||||
dele
|
||||
pessoas
|
||||
fora
|
||||
lugar
|
||||
apenas
|
||||
ei
|
||||
fazendo
|
||||
ninguém
|
||||
dinheiro
|
||||
acha
|
||||
vá
|
||||
comigo
|
||||
mundo
|
||||
preciso
|
||||
qual
|
||||
grande
|
||||
estar
|
||||
alguma
|
||||
hoje
|
||||
trabalho
|
||||
suas
|
||||
dar
|
||||
seja
|
||||
disso
|
||||
fez
|
||||
nome
|
||||
será
|
||||
novo
|
||||
filho
|
||||
outro
|
||||
qualquer
|
||||
quanto
|
||||
saber
|
||||
vão
|
||||
meus
|
||||
queria
|
||||
ok
|
||||
podemos
|
||||
nossa
|
||||
poderia
|
||||
outra
|
||||
olá
|
||||
precisa
|
||||
venha
|
||||
nosso
|
||||
mulher
|
||||
sinto
|
||||
desculpe
|
||||
toda
|
||||
diga
|
||||
hora
|
||||
daqui
|
||||
amor
|
164
resources/runtime/language/python
Normal file
164
resources/runtime/language/python
Normal file
|
@ -0,0 +1,164 @@
|
|||
abs
|
||||
all
|
||||
and
|
||||
any
|
||||
append
|
||||
as
|
||||
ascii
|
||||
assert
|
||||
bin
|
||||
bool
|
||||
break
|
||||
bytearray
|
||||
bytes
|
||||
callable
|
||||
capitalise
|
||||
capitalize
|
||||
casefold
|
||||
ceil
|
||||
center
|
||||
chr
|
||||
class
|
||||
classmethod
|
||||
clear
|
||||
compile
|
||||
complex
|
||||
continue
|
||||
copy
|
||||
count
|
||||
def
|
||||
del
|
||||
delattr
|
||||
dict
|
||||
dir
|
||||
divmod
|
||||
elif
|
||||
else
|
||||
encode
|
||||
endswith
|
||||
enumerate
|
||||
eval
|
||||
except
|
||||
exec
|
||||
exp
|
||||
expandtabs
|
||||
extend
|
||||
fabs
|
||||
factorial
|
||||
False
|
||||
filter
|
||||
finally
|
||||
find
|
||||
float
|
||||
for
|
||||
format
|
||||
from
|
||||
fromkeys
|
||||
frozenset
|
||||
get
|
||||
getattr
|
||||
global
|
||||
globals
|
||||
hasattr
|
||||
hash
|
||||
help
|
||||
hex
|
||||
id
|
||||
if
|
||||
import
|
||||
in
|
||||
index
|
||||
input
|
||||
insert
|
||||
int
|
||||
is
|
||||
isalnum
|
||||
isalpha
|
||||
isdecimal
|
||||
isdigit
|
||||
isidentifie
|
||||
isinstance
|
||||
islower
|
||||
isnumeric
|
||||
isprintable
|
||||
isspace
|
||||
issubclass
|
||||
istitle
|
||||
isupper
|
||||
items
|
||||
iter
|
||||
join
|
||||
keys
|
||||
lambda
|
||||
len
|
||||
list
|
||||
ljust
|
||||
locals
|
||||
lower
|
||||
lstrip
|
||||
maketrans
|
||||
map
|
||||
max
|
||||
memoryview
|
||||
min
|
||||
next
|
||||
None
|
||||
not
|
||||
object
|
||||
oct
|
||||
open
|
||||
or
|
||||
ord
|
||||
partition
|
||||
pass
|
||||
pop
|
||||
popitem
|
||||
pow
|
||||
print
|
||||
property
|
||||
raise
|
||||
range
|
||||
remove
|
||||
replace
|
||||
repr
|
||||
return
|
||||
reverse
|
||||
reversed
|
||||
rfind
|
||||
rindex
|
||||
rjust
|
||||
round
|
||||
rpartition
|
||||
rsplit
|
||||
rstrip
|
||||
set
|
||||
setattr
|
||||
setdefault
|
||||
slice
|
||||
sort
|
||||
sorted
|
||||
split
|
||||
splitlines
|
||||
sqrt
|
||||
startswith
|
||||
staticmethod
|
||||
str
|
||||
strip
|
||||
sum
|
||||
super
|
||||
swapcase
|
||||
title
|
||||
translate
|
||||
True
|
||||
try
|
||||
tuple
|
||||
type
|
||||
update
|
||||
upper
|
||||
values
|
||||
vars
|
||||
while
|
||||
with
|
||||
yield
|
||||
zfill
|
||||
zip
|
67
resources/runtime/language/qt
Normal file
67
resources/runtime/language/qt
Normal file
|
@ -0,0 +1,67 @@
|
|||
actionAt
|
||||
actionTriggered
|
||||
addAction
|
||||
allowedAreasChanged
|
||||
animated
|
||||
build
|
||||
centralWidget
|
||||
children
|
||||
connect
|
||||
debug
|
||||
dockNestingEnabled
|
||||
dockOptions
|
||||
documentMode
|
||||
iconSize
|
||||
insertSeperator
|
||||
isMovable
|
||||
make
|
||||
modal
|
||||
movableChanged
|
||||
Orientation
|
||||
paintEvent
|
||||
parent
|
||||
pos
|
||||
private
|
||||
protected
|
||||
public
|
||||
QLabel
|
||||
qmake
|
||||
QPropertyAnimation
|
||||
QPushButton
|
||||
QRect
|
||||
QSize
|
||||
QString
|
||||
QStyleFactory
|
||||
Qt
|
||||
~QToolBar
|
||||
QToolBar
|
||||
release
|
||||
removeToolBarBreak
|
||||
resizeDocks
|
||||
setAnimated
|
||||
setCorner
|
||||
setMenuBar
|
||||
setMovable
|
||||
setTabPosition
|
||||
signals
|
||||
sizeHint
|
||||
slots
|
||||
statusTip
|
||||
styleSheet
|
||||
tabShape
|
||||
toolButtonStyle
|
||||
toolButtonStyleChanged
|
||||
toolTip
|
||||
toolTipDuration
|
||||
topLevelChanged
|
||||
unifiedTitleAndToolBarOnMac
|
||||
updatesEnabled
|
||||
visible
|
||||
whatIsThis
|
||||
width
|
||||
windowFilePath
|
||||
windowFlags
|
||||
windowIcon
|
||||
windowModality
|
||||
windowOpacity
|
||||
windowTitle
|
118
resources/runtime/language/ruby
Normal file
118
resources/runtime/language/ruby
Normal file
|
@ -0,0 +1,118 @@
|
|||
BEGIN
|
||||
class
|
||||
ensure
|
||||
nil
|
||||
self
|
||||
when
|
||||
END
|
||||
def
|
||||
false
|
||||
not
|
||||
super
|
||||
while
|
||||
alias
|
||||
defined?
|
||||
for
|
||||
or
|
||||
then
|
||||
yield
|
||||
and
|
||||
do
|
||||
if
|
||||
redo
|
||||
true
|
||||
__LINE__
|
||||
begin
|
||||
else
|
||||
in
|
||||
rescue
|
||||
undef
|
||||
__FILE__
|
||||
break
|
||||
elsif
|
||||
module
|
||||
retry
|
||||
unless
|
||||
__ENCODING__
|
||||
case
|
||||
end
|
||||
next
|
||||
return
|
||||
until
|
||||
BasicObject
|
||||
Object
|
||||
ARGF.class
|
||||
Array
|
||||
Binding
|
||||
Dir
|
||||
Encoding
|
||||
Encoding::Converter
|
||||
Enumerator
|
||||
Enumerator::ArithmeticSequence
|
||||
Enumerator::Chain
|
||||
Enumerator::Lazy
|
||||
Enumerator::Yielder
|
||||
FalseClass
|
||||
Fiber
|
||||
File::Stat
|
||||
Hash
|
||||
IO
|
||||
File
|
||||
MatchData
|
||||
Method
|
||||
Module
|
||||
Class
|
||||
NilClass
|
||||
Numeric
|
||||
Complex
|
||||
Float
|
||||
Integer
|
||||
Rational
|
||||
ObjectSpace::WeakMap
|
||||
Proc
|
||||
Process::Status
|
||||
Random
|
||||
Range
|
||||
Regexp
|
||||
RubyVM
|
||||
RubyVM::AbstractSyntaxTree::Node
|
||||
RubyVM::InstructionSequence
|
||||
String
|
||||
Struct
|
||||
Process::Tms
|
||||
Symbol
|
||||
Thread
|
||||
Thread::Backtrace::Location
|
||||
Thread::ConditionVariable
|
||||
Thread::Mutex
|
||||
Thread::Queue
|
||||
Thread::SizedQueue
|
||||
ThreadGroup
|
||||
Time
|
||||
TracePoint
|
||||
TrueClass
|
||||
UnboundMethod
|
||||
Comparable
|
||||
Enumerable
|
||||
Errno
|
||||
File::Constants
|
||||
FileTest
|
||||
GC
|
||||
GC::Profiler
|
||||
IO::WaitReadable
|
||||
IO::WaitWritable
|
||||
Kernel
|
||||
Marshal
|
||||
Math
|
||||
ObjectSpace
|
||||
Process
|
||||
Process::GID
|
||||
Process::Sys
|
||||
Process::UID
|
||||
RubyVM::AbstractSyntaxTree
|
||||
RubyVM::MJIT
|
||||
Signal
|
||||
Warning
|
||||
ARGF
|
||||
ENV
|
||||
main
|
98
resources/runtime/language/rust
Normal file
98
resources/runtime/language/rust
Normal file
|
@ -0,0 +1,98 @@
|
|||
as
|
||||
async
|
||||
await
|
||||
break
|
||||
const
|
||||
continue
|
||||
crate
|
||||
dyn
|
||||
else
|
||||
enum
|
||||
extern
|
||||
false
|
||||
fn
|
||||
for
|
||||
if
|
||||
impl
|
||||
in
|
||||
let
|
||||
loop
|
||||
match
|
||||
=>
|
||||
->
|
||||
Option
|
||||
Result
|
||||
Some
|
||||
None
|
||||
Ok
|
||||
Err
|
||||
mod
|
||||
move
|
||||
mut
|
||||
&mut
|
||||
pub
|
||||
ref
|
||||
return
|
||||
Self
|
||||
self
|
||||
static
|
||||
struct
|
||||
super
|
||||
trait
|
||||
true
|
||||
type
|
||||
union
|
||||
unsafe
|
||||
use
|
||||
where
|
||||
while
|
||||
panic!()
|
||||
println!
|
||||
dbg!
|
||||
u8
|
||||
u16
|
||||
u32
|
||||
u64
|
||||
u128
|
||||
usize
|
||||
i8
|
||||
i16
|
||||
i32
|
||||
i64
|
||||
i128
|
||||
isize
|
||||
bool
|
||||
char
|
||||
&str
|
||||
Vec::new()
|
||||
Vec<_>
|
||||
vec![]
|
||||
HashMap
|
||||
String
|
||||
Vec::new()
|
||||
collect::<Vec<_>>()
|
||||
::<>
|
||||
box
|
||||
lazy_static
|
||||
iter()
|
||||
filter(|x| x.is_ok())
|
||||
<'_>
|
||||
map
|
||||
BTreeMap
|
||||
VecDeque
|
||||
LinkedList
|
||||
HashSet
|
||||
&'static
|
||||
&'_
|
||||
..
|
||||
..=
|
||||
?
|
||||
macro_rules!
|
||||
{:?}
|
||||
format!
|
||||
unwrap()
|
||||
#[derive(Debug, Clone)]
|
||||
#[test]
|
||||
///
|
||||
//!
|
||||
//
|
104
resources/runtime/language/spanish
Normal file
104
resources/runtime/language/spanish
Normal file
|
@ -0,0 +1,104 @@
|
|||
como
|
||||
hola
|
||||
eche
|
||||
no
|
||||
se
|
||||
niño
|
||||
piña
|
||||
monda
|
||||
sopas
|
||||
san
|
||||
yo
|
||||
mondongo
|
||||
eñe
|
||||
peo
|
||||
presente
|
||||
méxico
|
||||
mágico
|
||||
exponente
|
||||
número
|
||||
otra
|
||||
otro
|
||||
ni
|
||||
sur
|
||||
historia
|
||||
maestro
|
||||
negro
|
||||
blanco
|
||||
sopas
|
||||
fácil
|
||||
difícil
|
||||
que
|
||||
muchacho
|
||||
calor
|
||||
agua
|
||||
hielo
|
||||
vapor
|
||||
fuego
|
||||
gas
|
||||
aire
|
||||
atmósfera
|
||||
tierra
|
||||
piso
|
||||
suelo
|
||||
metal
|
||||
metálico
|
||||
hierro
|
||||
oro
|
||||
plata
|
||||
plomo
|
||||
sal
|
||||
barro
|
||||
escritorio
|
||||
silla
|
||||
mesa
|
||||
cama
|
||||
dormitorio
|
||||
habitación
|
||||
cuarto
|
||||
oficina
|
||||
panel
|
||||
puerta
|
||||
ventana
|
||||
entrada
|
||||
hogar
|
||||
casa
|
||||
apartamento
|
||||
departamento
|
||||
edificio
|
||||
construcción
|
||||
elevador
|
||||
ascensor
|
||||
escalera
|
||||
cultura
|
||||
autor
|
||||
actuación
|
||||
espectador
|
||||
espectáculo
|
||||
entretenimiento
|
||||
arte
|
||||
cine
|
||||
dibujo
|
||||
pintura
|
||||
música
|
||||
religión
|
||||
dios
|
||||
artículo
|
||||
educación
|
||||
escuela
|
||||
instituto
|
||||
colegio
|
||||
universidad
|
||||
clase
|
||||
curso
|
||||
estudio
|
||||
formación
|
||||
análisis
|
||||
investigación
|
||||
conocimiento
|
||||
idea
|
||||
información
|
||||
dato
|
||||
forma
|
||||
manera
|
||||
champeta
|
100
resources/runtime/language/ukrainian
Normal file
100
resources/runtime/language/ukrainian
Normal file
|
@ -0,0 +1,100 @@
|
|||
або
|
||||
але
|
||||
багато
|
||||
батько
|
||||
без
|
||||
більше
|
||||
будь
|
||||
вибачте
|
||||
вино
|
||||
вівторок
|
||||
він
|
||||
вісім
|
||||
вона
|
||||
вони
|
||||
вчора
|
||||
гарний
|
||||
говорити
|
||||
два
|
||||
де
|
||||
дев'ять
|
||||
день
|
||||
десять
|
||||
дитина
|
||||
дівчина
|
||||
побачення
|
||||
добре
|
||||
друг
|
||||
подруга
|
||||
дружина
|
||||
дуже
|
||||
дякую
|
||||
жінка
|
||||
завтра
|
||||
зараз
|
||||
знову
|
||||
зробити
|
||||
інший
|
||||
кава
|
||||
кафе
|
||||
кінотеатр
|
||||
кішка
|
||||
класно
|
||||
книга
|
||||
коли
|
||||
комп'ютер
|
||||
ласка
|
||||
ліворуч
|
||||
любити
|
||||
магазин
|
||||
мама
|
||||
ми
|
||||
можна
|
||||
музика
|
||||
не
|
||||
неділя
|
||||
ні
|
||||
нічого
|
||||
обід
|
||||
один
|
||||
пиво
|
||||
повтори
|
||||
подобатися
|
||||
понеділок
|
||||
потім
|
||||
праворуч
|
||||
працювати
|
||||
рахунок
|
||||
ресторан
|
||||
ринок
|
||||
розуміти
|
||||
середа
|
||||
сім
|
||||
скільки
|
||||
справи
|
||||
субота
|
||||
сьогодні
|
||||
так
|
||||
також
|
||||
там
|
||||
тільки
|
||||
тому
|
||||
три
|
||||
трохи
|
||||
трошки
|
||||
тут
|
||||
фільм
|
||||
хлопець
|
||||
хотіти
|
||||
хто
|
||||
це
|
||||
цікаво
|
||||
чай
|
||||
четвер
|
||||
чоловік
|
||||
чому
|
||||
чотири
|
||||
чудово
|
||||
шість
|
||||
що
|
||||
я
|
308
src/config.rs
Normal file
308
src/config.rs
Normal file
|
@ -0,0 +1,308 @@
|
|||
use ratatui::style::{Color, Modifier, Style};
|
||||
use serde::{
|
||||
de::{self, IntoDeserializer},
|
||||
Deserialize,
|
||||
};
|
||||
|
||||
#[derive(Debug, Deserialize)]
|
||||
#[serde(default)]
|
||||
pub struct Config {
|
||||
pub default_language: String,
|
||||
pub theme: Theme,
|
||||
pub keyboard: String,
|
||||
|
||||
// server
|
||||
pub server: String,
|
||||
pub bucket: String,
|
||||
pub org: String,
|
||||
pub token: String,
|
||||
}
|
||||
|
||||
impl Default for Config {
|
||||
fn default() -> Self {
|
||||
Self {
|
||||
default_language: "english1000".into(),
|
||||
theme: Theme::default(),
|
||||
keyboard: "Generic".into(),
|
||||
|
||||
server: "http://localhost:3000".into(),
|
||||
bucket: "dttyper".into(),
|
||||
org: "dttyper".into(),
|
||||
token: "none".into(),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[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_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,
|
||||
}
|
||||
|
||||
impl Default for Theme {
|
||||
fn default() -> Self {
|
||||
Self {
|
||||
default: Style::default(),
|
||||
|
||||
title: Style::default()
|
||||
.fg(Color::White)
|
||||
.add_modifier(Modifier::BOLD),
|
||||
|
||||
input_border: Style::default().fg(Color::Cyan),
|
||||
prompt_border: Style::default().fg(Color::Green),
|
||||
|
||||
prompt_correct: Style::default().fg(Color::Green),
|
||||
prompt_incorrect: Style::default().fg(Color::Red),
|
||||
prompt_untyped: Style::default().fg(Color::Gray),
|
||||
|
||||
prompt_current_correct: Style::default()
|
||||
.fg(Color::Green)
|
||||
.add_modifier(Modifier::BOLD),
|
||||
prompt_current_incorrect: Style::default().fg(Color::Red).add_modifier(Modifier::BOLD),
|
||||
prompt_current_untyped: Style::default()
|
||||
.fg(Color::Blue)
|
||||
.add_modifier(Modifier::BOLD),
|
||||
|
||||
prompt_cursor: Style::default().add_modifier(Modifier::UNDERLINED),
|
||||
|
||||
results_overview: Style::default()
|
||||
.fg(Color::Cyan)
|
||||
.add_modifier(Modifier::BOLD),
|
||||
results_overview_border: Style::default().fg(Color::Cyan),
|
||||
|
||||
results_worst_keys: Style::default()
|
||||
.fg(Color::Cyan)
|
||||
.add_modifier(Modifier::BOLD),
|
||||
results_worst_keys_border: Style::default().fg(Color::Cyan),
|
||||
|
||||
results_chart: Style::default().fg(Color::Cyan),
|
||||
results_chart_x: Style::default().fg(Color::Cyan),
|
||||
results_chart_y: Style::default()
|
||||
.fg(Color::Gray)
|
||||
.add_modifier(Modifier::BOLD),
|
||||
|
||||
results_restart_prompt: Style::default()
|
||||
.fg(Color::Gray)
|
||||
.add_modifier(Modifier::ITALIC),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn deserialize_style<'de, D>(deserializer: D) -> Result<Style, D::Error>
|
||||
where
|
||||
D: de::Deserializer<'de>,
|
||||
{
|
||||
struct StyleVisitor;
|
||||
impl<'de> de::Visitor<'de> 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> de::Visitor<'de> 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)
|
||||
}
|
||||
|
||||
#[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)
|
||||
);
|
||||
}
|
||||
}
|
354
src/main.rs
Normal file
354
src/main.rs
Normal file
|
@ -0,0 +1,354 @@
|
|||
mod config;
|
||||
mod save;
|
||||
mod test;
|
||||
mod ui;
|
||||
|
||||
use config::Config;
|
||||
use save::save_result;
|
||||
use test::{results::Results, Test};
|
||||
|
||||
use crossterm::{
|
||||
self, cursor,
|
||||
event::{self, Event, KeyCode, KeyEvent, KeyEventKind, KeyModifiers},
|
||||
execute, terminal,
|
||||
};
|
||||
use rand::{seq::SliceRandom, thread_rng};
|
||||
use ratatui::{backend::CrosstermBackend, terminal::Terminal};
|
||||
use rust_embed::RustEmbed;
|
||||
use std::{
|
||||
ffi::OsString,
|
||||
fs,
|
||||
io::{self, BufRead},
|
||||
num,
|
||||
path::PathBuf,
|
||||
str,
|
||||
};
|
||||
use structopt::StructOpt;
|
||||
|
||||
#[derive(RustEmbed)]
|
||||
#[folder = "resources/runtime"]
|
||||
struct Resources;
|
||||
|
||||
#[derive(Debug, StructOpt)]
|
||||
#[structopt(name = "dttyper", about = "Terminal-based typing test with exporting")]
|
||||
struct Opt {
|
||||
#[structopt(parse(from_os_str))]
|
||||
contents: Option<PathBuf>,
|
||||
|
||||
#[structopt(short, long)]
|
||||
debug: bool,
|
||||
|
||||
/// Specify word count
|
||||
#[structopt(short, long, default_value = "15")]
|
||||
words: num::NonZeroUsize,
|
||||
|
||||
/// Use config file
|
||||
#[structopt(short, long)]
|
||||
config: Option<PathBuf>,
|
||||
|
||||
/// Specify test language in file
|
||||
#[structopt(long, parse(from_os_str))]
|
||||
language_file: Option<PathBuf>,
|
||||
|
||||
/// Specify test language
|
||||
#[structopt(short, long)]
|
||||
language: Option<String>,
|
||||
|
||||
/// List installed languages
|
||||
#[structopt(long)]
|
||||
list_languages: bool,
|
||||
|
||||
/// Disable backtracking to completed words
|
||||
#[structopt(long)]
|
||||
no_backtrack: bool,
|
||||
|
||||
/// Enable sudden death mode to restart on first error
|
||||
#[structopt(long)]
|
||||
sudden_death: bool,
|
||||
}
|
||||
|
||||
impl Opt {
|
||||
fn gen_contents(&self) -> Option<Vec<String>> {
|
||||
match &self.contents {
|
||||
Some(path) => {
|
||||
let lines: Vec<String> = if path.as_os_str() == "-" {
|
||||
std::io::stdin()
|
||||
.lock()
|
||||
.lines()
|
||||
.map_while(Result::ok)
|
||||
.collect()
|
||||
} else {
|
||||
let file = fs::File::open(path).expect("Error reading language file.");
|
||||
io::BufReader::new(file)
|
||||
.lines()
|
||||
.map_while(Result::ok)
|
||||
.collect()
|
||||
};
|
||||
|
||||
Some(lines.iter().map(String::from).collect())
|
||||
}
|
||||
None => {
|
||||
let lang_name = self
|
||||
.language
|
||||
.clone()
|
||||
.unwrap_or_else(|| self.config().default_language);
|
||||
|
||||
let bytes: Vec<u8> = self
|
||||
.language_file
|
||||
.as_ref()
|
||||
.map(fs::read)
|
||||
.and_then(Result::ok)
|
||||
.or_else(|| fs::read(self.language_dir().join(&lang_name)).ok())
|
||||
.or_else(|| {
|
||||
Resources::get(&format!("language/{}", &lang_name))
|
||||
.map(|f| f.data.into_owned())
|
||||
})?;
|
||||
|
||||
let mut rng = thread_rng();
|
||||
|
||||
let mut language: Vec<&str> = str::from_utf8(&bytes)
|
||||
.expect("Language file had non-utf8 encoding.")
|
||||
.lines()
|
||||
.collect();
|
||||
language.shuffle(&mut rng);
|
||||
|
||||
let mut contents: Vec<_> = language
|
||||
.into_iter()
|
||||
.cycle()
|
||||
.take(self.words.get())
|
||||
.map(ToOwned::to_owned)
|
||||
.collect();
|
||||
|
||||
// TODO insert quote logic here :P
|
||||
contents.shuffle(&mut rng);
|
||||
|
||||
Some(contents)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Configuration
|
||||
fn config(&self) -> Config {
|
||||
fs::read(
|
||||
self.config
|
||||
.clone()
|
||||
.unwrap_or_else(|| self.config_dir().join("config.toml")),
|
||||
)
|
||||
.map(|bytes| {
|
||||
toml::from_str(str::from_utf8(&bytes).unwrap_or_default())
|
||||
.expect("Configuration was ill-formed.")
|
||||
})
|
||||
.unwrap_or_default()
|
||||
}
|
||||
|
||||
/// Installed languages under config directory
|
||||
fn languages(&self) -> io::Result<impl Iterator<Item = OsString>> {
|
||||
let builtin = Resources::iter().filter_map(|name| {
|
||||
name.strip_prefix("language/")
|
||||
.map(ToOwned::to_owned)
|
||||
.map(OsString::from)
|
||||
});
|
||||
|
||||
let configured = self
|
||||
.language_dir()
|
||||
.read_dir()
|
||||
.into_iter()
|
||||
.flatten()
|
||||
.map_while(Result::ok)
|
||||
.map(|e| e.file_name());
|
||||
|
||||
Ok(builtin.chain(configured))
|
||||
}
|
||||
|
||||
/// Config directory
|
||||
fn config_dir(&self) -> PathBuf {
|
||||
dirs::config_dir()
|
||||
.expect("Failed to find config directory.")
|
||||
.join("dttyper")
|
||||
}
|
||||
|
||||
/// Language directory under config directory
|
||||
fn language_dir(&self) -> PathBuf {
|
||||
self.config_dir().join("language")
|
||||
}
|
||||
}
|
||||
|
||||
enum State {
|
||||
Test(Test),
|
||||
Results(Results),
|
||||
}
|
||||
|
||||
impl State {
|
||||
fn render_into<B: ratatui::backend::Backend>(
|
||||
&self,
|
||||
terminal: &mut Terminal<B>,
|
||||
config: &Config,
|
||||
) -> io::Result<()> {
|
||||
match self {
|
||||
State::Test(test) => {
|
||||
terminal.draw(|f| {
|
||||
f.render_widget(config.theme.apply_to(test), f.size());
|
||||
})?;
|
||||
}
|
||||
State::Results(results) => {
|
||||
terminal.draw(|f| {
|
||||
f.render_widget(config.theme.apply_to(results), f.size());
|
||||
})?;
|
||||
}
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
|
||||
fn main() -> io::Result<()> {
|
||||
let opt = Opt::from_args();
|
||||
if opt.debug {
|
||||
dbg!(&opt);
|
||||
}
|
||||
|
||||
let config = opt.config();
|
||||
if opt.debug {
|
||||
dbg!(&config);
|
||||
}
|
||||
|
||||
if opt.list_languages {
|
||||
opt.languages()
|
||||
.unwrap()
|
||||
.for_each(|name| println!("{}", name.to_str().expect("Ill-formatted language name.")));
|
||||
|
||||
return Ok(());
|
||||
}
|
||||
|
||||
// TODO add server checking
|
||||
|
||||
let backend = CrosstermBackend::new(io::stdout());
|
||||
let mut terminal = Terminal::new(backend)?;
|
||||
|
||||
terminal::enable_raw_mode()?;
|
||||
execute!(
|
||||
io::stdout(),
|
||||
cursor::Hide,
|
||||
cursor::SavePosition,
|
||||
terminal::EnterAlternateScreen,
|
||||
)?;
|
||||
terminal.clear()?;
|
||||
|
||||
let mut state = State::Test(Test::new(
|
||||
opt.gen_contents().expect(
|
||||
"Couldn't get test contents. Make sure the specified language actually exists.",
|
||||
),
|
||||
!opt.no_backtrack,
|
||||
opt.sudden_death,
|
||||
));
|
||||
|
||||
state.render_into(&mut terminal, &config)?;
|
||||
loop {
|
||||
let event = event::read()?;
|
||||
|
||||
// handle exit controls
|
||||
match event {
|
||||
Event::Key(KeyEvent {
|
||||
code: KeyCode::Char('c'),
|
||||
kind: KeyEventKind::Press,
|
||||
modifiers: KeyModifiers::CONTROL,
|
||||
..
|
||||
}) => break,
|
||||
Event::Key(KeyEvent {
|
||||
code: KeyCode::Esc,
|
||||
kind: KeyEventKind::Press,
|
||||
modifiers: KeyModifiers::NONE,
|
||||
..
|
||||
}) => match state {
|
||||
State::Test(ref test) => {
|
||||
state = State::Results(Results::from(test));
|
||||
}
|
||||
State::Results(_) => break,
|
||||
},
|
||||
_ => {}
|
||||
}
|
||||
|
||||
match state {
|
||||
State::Test(ref mut test) => {
|
||||
if let Event::Key(key) = event {
|
||||
test.handle_key(key);
|
||||
if test.complete {
|
||||
save_result(
|
||||
&Results::from(&*test),
|
||||
config.server.clone(),
|
||||
config.token.clone(),
|
||||
config.keyboard.clone(),
|
||||
match Opt::from_args().language {
|
||||
None => "english".to_string(),
|
||||
Some(lang) => lang,
|
||||
},
|
||||
config.bucket.clone(),
|
||||
config.org.clone(),
|
||||
test.clone(),
|
||||
);
|
||||
|
||||
state = State::Results(Results::from(&*test));
|
||||
}
|
||||
}
|
||||
}
|
||||
State::Results(ref result) => {
|
||||
match event {
|
||||
Event::Key(KeyEvent {
|
||||
code: KeyCode::Char('r'),
|
||||
kind: KeyEventKind::Press,
|
||||
modifiers: KeyModifiers::NONE,
|
||||
..
|
||||
}) => {
|
||||
state = State::Test(Test::new(
|
||||
opt.gen_contents().expect(
|
||||
"Couldn't get test contents. Make sure the specified language actually exists.",
|
||||
),
|
||||
!opt.no_backtrack,
|
||||
opt.sudden_death
|
||||
));
|
||||
}
|
||||
Event::Key(KeyEvent {
|
||||
code: KeyCode::Char('p'),
|
||||
kind: KeyEventKind::Press,
|
||||
modifiers: KeyModifiers::NONE,
|
||||
..
|
||||
}) => {
|
||||
if result.missed_words.is_empty() {
|
||||
continue;
|
||||
}
|
||||
// repeat each missed word 5 times
|
||||
let mut practice_words: Vec<String> = (result.missed_words)
|
||||
.iter()
|
||||
.flat_map(|w| vec![w.clone(); 5])
|
||||
.collect();
|
||||
practice_words.shuffle(&mut thread_rng());
|
||||
state = State::Test(Test::new(
|
||||
practice_words,
|
||||
!opt.no_backtrack,
|
||||
opt.sudden_death,
|
||||
));
|
||||
}
|
||||
Event::Key(KeyEvent {
|
||||
code: KeyCode::Char('q'),
|
||||
kind: KeyEventKind::Press,
|
||||
modifiers: KeyModifiers::NONE,
|
||||
..
|
||||
}) => break,
|
||||
_ => {}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
state.render_into(&mut terminal, &config)?;
|
||||
}
|
||||
|
||||
terminal::disable_raw_mode()?;
|
||||
execute!(
|
||||
io::stdout(),
|
||||
cursor::RestorePosition,
|
||||
cursor::Show,
|
||||
terminal::LeaveAlternateScreen,
|
||||
)?;
|
||||
|
||||
Ok(())
|
||||
}
|
88
src/save.rs
Normal file
88
src/save.rs
Normal file
|
@ -0,0 +1,88 @@
|
|||
use std::{str::FromStr, thread, time::Duration};
|
||||
|
||||
use crossterm::terminal;
|
||||
|
||||
use crate::test::{results::Results, Test, TestWord};
|
||||
|
||||
pub fn save_result(
|
||||
result: &Results,
|
||||
url: String,
|
||||
token: String,
|
||||
keyboard: String,
|
||||
test_type: String,
|
||||
bucket: String,
|
||||
org: String,
|
||||
test: Test,
|
||||
) {
|
||||
// Creating a duplicate of the data
|
||||
let rr = Results {
|
||||
timing: result.timing.clone(),
|
||||
accuracy: result.accuracy.clone(),
|
||||
missed_words: result.missed_words.clone(),
|
||||
};
|
||||
// HTTP client
|
||||
let client = reqwest::blocking::Client::new();
|
||||
|
||||
// parsing language into quotes
|
||||
let language = format!("\"{}\"", test_type);
|
||||
|
||||
// total words typed
|
||||
let words_typed = test.words.len();
|
||||
|
||||
// filtering out incorrectly typed words
|
||||
let words_typed_incorrectly = test
|
||||
.clone()
|
||||
.words
|
||||
.into_iter()
|
||||
.filter(|word| word.events.iter().any(|o| o.correct == Some(false)))
|
||||
.collect::<Vec<TestWord>>();
|
||||
|
||||
// filtering out correctly typed words and checking if they are not already an instance of the incorrectly typed ones
|
||||
// since the user may type the word incorrectly the first time and go back to change it
|
||||
// then turning it into a string
|
||||
let words_typed_correctly = test
|
||||
.clone()
|
||||
.words
|
||||
.into_iter()
|
||||
.filter(|word| {
|
||||
word.events.iter().any(|o| o.correct == Some(true))
|
||||
&& !words_typed_incorrectly.contains(word)
|
||||
})
|
||||
.map(|w| w.text.clone())
|
||||
.collect::<Vec<String>>();
|
||||
|
||||
// turning incorrect words into a string
|
||||
let words_typed_incorrectly = words_typed_incorrectly
|
||||
.iter()
|
||||
.map(|w| w.text.clone())
|
||||
.collect::<Vec<String>>();
|
||||
|
||||
// building influxdb insert query
|
||||
let data = format!("test,language={},keyboard={} words={},incorrect_types={},correct_types={},wpm={},accuracy={},words_typed_correctly={},words_typed_incorrectly={} {}",
|
||||
language.replace('"', ""),
|
||||
"generic",
|
||||
words_typed,
|
||||
words_typed_incorrectly.len(),
|
||||
words_typed_correctly.len(),
|
||||
rr.timing.overall_cps as f64 * 12.0 * ( rr.accuracy.overall.numerator as f64 / rr.accuracy.overall.denominator as f64), //float
|
||||
( rr.accuracy.overall.numerator as f64 / rr.accuracy.overall.denominator as f64) * 100.0, // accuracy
|
||||
format!("\"{}\"", words_typed_correctly.join(",")),
|
||||
format!("\"{}\"", words_typed_incorrectly.join(",")),
|
||||
chrono::Utc::now().timestamp()
|
||||
);
|
||||
|
||||
let mut server_url = url.clone();
|
||||
server_url.push_str("/api/v2/write");
|
||||
|
||||
let mut url = reqwest::Url::from_str(server_url.as_str()).unwrap();
|
||||
url.set_query(Some(
|
||||
format!("org={}&bucket={}&precision=s", org, bucket).as_str(),
|
||||
));
|
||||
|
||||
// TODO error handle
|
||||
client
|
||||
.post(url)
|
||||
.body(data)
|
||||
.header("Authorization", format!("Token {}", token))
|
||||
.send();
|
||||
}
|
172
src/test/mod.rs
Normal file
172
src/test/mod.rs
Normal file
|
@ -0,0 +1,172 @@
|
|||
pub mod results;
|
||||
|
||||
use crossterm::event::{KeyCode, KeyEvent, KeyEventKind, KeyModifiers};
|
||||
use std::fmt;
|
||||
use std::time::Instant;
|
||||
|
||||
#[derive(Clone, PartialEq)]
|
||||
pub struct TestEvent {
|
||||
pub time: Instant,
|
||||
pub key: KeyEvent,
|
||||
pub correct: Option<bool>,
|
||||
}
|
||||
|
||||
pub fn is_missed_word_event(event: &TestEvent) -> bool {
|
||||
event.correct != Some(true)
|
||||
}
|
||||
|
||||
impl fmt::Debug for TestEvent {
|
||||
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
|
||||
f.debug_struct("TestEvent")
|
||||
.field("time", &String::from("Instant { ... }"))
|
||||
.field("key", &self.key)
|
||||
.finish()
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, PartialEq)]
|
||||
pub struct TestWord {
|
||||
pub text: String,
|
||||
pub progress: String,
|
||||
pub events: Vec<TestEvent>,
|
||||
}
|
||||
|
||||
impl From<String> for TestWord {
|
||||
fn from(string: String) -> Self {
|
||||
TestWord {
|
||||
text: string,
|
||||
progress: String::new(),
|
||||
events: Vec::new(),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl From<&str> for TestWord {
|
||||
fn from(string: &str) -> Self {
|
||||
Self::from(string.to_string())
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone)]
|
||||
pub struct Test {
|
||||
pub words: Vec<TestWord>,
|
||||
pub current_word: usize,
|
||||
pub complete: bool,
|
||||
pub backtracking_enabled: bool,
|
||||
pub sudden_death_enabled: bool,
|
||||
}
|
||||
|
||||
impl Test {
|
||||
pub fn new(words: Vec<String>, backtracking_enabled: bool, sudden_death_enabled: bool) -> Self {
|
||||
Self {
|
||||
words: words.into_iter().map(TestWord::from).collect(),
|
||||
current_word: 0,
|
||||
complete: false,
|
||||
backtracking_enabled,
|
||||
sudden_death_enabled,
|
||||
}
|
||||
}
|
||||
|
||||
pub fn handle_key(&mut self, key: KeyEvent) {
|
||||
if key.kind != KeyEventKind::Press {
|
||||
return;
|
||||
}
|
||||
|
||||
let word = &mut self.words[self.current_word];
|
||||
match key.code {
|
||||
KeyCode::Char(' ') | KeyCode::Enter => {
|
||||
if word.text.chars().nth(word.progress.len()) == Some(' ') {
|
||||
word.progress.push(' ');
|
||||
word.events.push(TestEvent {
|
||||
time: Instant::now(),
|
||||
correct: Some(true),
|
||||
key,
|
||||
})
|
||||
} else if !word.progress.is_empty() || word.text.is_empty() {
|
||||
let correct = word.text == word.progress;
|
||||
if self.sudden_death_enabled && !correct {
|
||||
self.reset();
|
||||
} else {
|
||||
word.events.push(TestEvent {
|
||||
time: Instant::now(),
|
||||
correct: Some(correct),
|
||||
key,
|
||||
});
|
||||
self.next_word();
|
||||
}
|
||||
}
|
||||
}
|
||||
KeyCode::Backspace => {
|
||||
if word.progress.is_empty() && self.backtracking_enabled {
|
||||
self.last_word();
|
||||
} else {
|
||||
word.events.push(TestEvent {
|
||||
time: Instant::now(),
|
||||
correct: Some(!word.text.starts_with(&word.progress[..])),
|
||||
key,
|
||||
});
|
||||
word.progress.pop();
|
||||
}
|
||||
}
|
||||
// CTRL-BackSpace and CTRL-W
|
||||
KeyCode::Char('h') | KeyCode::Char('w')
|
||||
if key.modifiers.contains(KeyModifiers::CONTROL) =>
|
||||
{
|
||||
if self.words[self.current_word].progress.is_empty() {
|
||||
self.last_word();
|
||||
}
|
||||
|
||||
let word = &mut self.words[self.current_word];
|
||||
|
||||
word.events.push(TestEvent {
|
||||
time: Instant::now(),
|
||||
correct: None,
|
||||
key,
|
||||
});
|
||||
word.progress.clear();
|
||||
}
|
||||
KeyCode::Char(c) => {
|
||||
word.progress.push(c);
|
||||
let correct = word.text.starts_with(&word.progress[..]);
|
||||
if self.sudden_death_enabled && !correct {
|
||||
self.reset();
|
||||
} else {
|
||||
word.events.push(TestEvent {
|
||||
time: Instant::now(),
|
||||
correct: Some(correct),
|
||||
key,
|
||||
});
|
||||
if word.progress == word.text && self.current_word == self.words.len() - 1 {
|
||||
self.complete = true;
|
||||
self.current_word = 0;
|
||||
}
|
||||
}
|
||||
}
|
||||
_ => {}
|
||||
};
|
||||
}
|
||||
|
||||
fn last_word(&mut self) {
|
||||
if self.current_word != 0 {
|
||||
self.current_word -= 1;
|
||||
}
|
||||
}
|
||||
|
||||
fn next_word(&mut self) {
|
||||
if self.current_word == self.words.len() - 1 {
|
||||
self.complete = true;
|
||||
self.current_word = 0;
|
||||
} else {
|
||||
self.current_word += 1;
|
||||
}
|
||||
}
|
||||
|
||||
fn reset(&mut self) {
|
||||
self.words.iter_mut().for_each(|word: &mut TestWord| {
|
||||
word.progress.clear();
|
||||
word.events.clear();
|
||||
});
|
||||
self.current_word = 0;
|
||||
self.complete = false;
|
||||
}
|
||||
}
|
160
src/test/results.rs
Normal file
160
src/test/results.rs
Normal file
|
@ -0,0 +1,160 @@
|
|||
use super::{is_missed_word_event, Test};
|
||||
|
||||
use crossterm::event::KeyEvent;
|
||||
use serde::Serialize;
|
||||
use std::collections::HashMap;
|
||||
use std::{cmp, fmt};
|
||||
|
||||
#[derive(Clone, Copy, PartialEq, Eq, Debug, Serialize)]
|
||||
pub struct Fraction {
|
||||
pub numerator: usize,
|
||||
pub denominator: usize,
|
||||
}
|
||||
|
||||
impl Fraction {
|
||||
pub const fn new(numerator: usize, denominator: usize) -> Self {
|
||||
Self {
|
||||
numerator,
|
||||
denominator,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl From<Fraction> for f64 {
|
||||
fn from(f: Fraction) -> Self {
|
||||
f.numerator as f64 / f.denominator as f64
|
||||
}
|
||||
}
|
||||
|
||||
impl cmp::Ord for Fraction {
|
||||
fn cmp(&self, other: &Self) -> cmp::Ordering {
|
||||
f64::from(*self).partial_cmp(&f64::from(*other)).unwrap()
|
||||
}
|
||||
}
|
||||
|
||||
impl PartialOrd for Fraction {
|
||||
fn partial_cmp(&self, other: &Self) -> Option<cmp::Ordering> {
|
||||
Some(self.cmp(other))
|
||||
}
|
||||
}
|
||||
|
||||
impl fmt::Display for Fraction {
|
||||
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
|
||||
write!(f, "{}/{}", self.numerator, self.denominator)
|
||||
}
|
||||
}
|
||||
|
||||
pub trait PartialResults {
|
||||
fn progress(&self) -> Fraction;
|
||||
}
|
||||
|
||||
impl PartialResults for Test {
|
||||
fn progress(&self) -> Fraction {
|
||||
Fraction {
|
||||
numerator: self.current_word + 1,
|
||||
denominator: self.words.len(),
|
||||
}
|
||||
}
|
||||
}
|
||||
#[derive(Clone)]
|
||||
pub struct TimingData {
|
||||
pub overall_cps: f64,
|
||||
pub per_event: Vec<f64>,
|
||||
pub per_key: HashMap<KeyEvent, f64>,
|
||||
}
|
||||
|
||||
#[derive(Clone)]
|
||||
pub struct AccuracyData {
|
||||
pub overall: Fraction,
|
||||
pub per_key: HashMap<KeyEvent, Fraction>,
|
||||
}
|
||||
|
||||
#[derive(Clone)]
|
||||
pub struct Results {
|
||||
pub timing: TimingData,
|
||||
pub accuracy: AccuracyData,
|
||||
pub missed_words: Vec<String>,
|
||||
}
|
||||
|
||||
impl From<&Test> for Results {
|
||||
fn from(test: &Test) -> Self {
|
||||
let events: Vec<&super::TestEvent> =
|
||||
test.words.iter().flat_map(|w| w.events.iter()).collect();
|
||||
|
||||
Self {
|
||||
timing: calc_timing(&events),
|
||||
accuracy: calc_accuracy(&events),
|
||||
missed_words: calc_missed_words(test),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn calc_timing(events: &[&super::TestEvent]) -> TimingData {
|
||||
let mut timing = TimingData {
|
||||
overall_cps: -1.0,
|
||||
per_event: Vec::new(),
|
||||
per_key: HashMap::new(),
|
||||
};
|
||||
|
||||
// map of keys to a two-tuple (total time, clicks) for counting average
|
||||
let mut keys: HashMap<KeyEvent, (f64, usize)> = HashMap::new();
|
||||
|
||||
for win in events.windows(2) {
|
||||
let event_dur = win[1]
|
||||
.time
|
||||
.checked_duration_since(win[0].time)
|
||||
.map(|d| d.as_secs_f64());
|
||||
|
||||
if let Some(event_dur) = event_dur {
|
||||
timing.per_event.push(event_dur);
|
||||
|
||||
let key = keys.entry(win[1].key).or_insert((0.0, 0));
|
||||
key.0 += event_dur;
|
||||
key.1 += 1;
|
||||
}
|
||||
}
|
||||
|
||||
timing.per_key = keys
|
||||
.into_iter()
|
||||
.map(|(key, (total, count))| (key, total / count as f64))
|
||||
.collect();
|
||||
|
||||
timing.overall_cps = timing.per_event.len() as f64 / timing.per_event.iter().sum::<f64>();
|
||||
|
||||
timing
|
||||
}
|
||||
|
||||
fn calc_accuracy(events: &[&super::TestEvent]) -> AccuracyData {
|
||||
let mut acc = AccuracyData {
|
||||
overall: Fraction::new(0, 0),
|
||||
per_key: HashMap::new(),
|
||||
};
|
||||
|
||||
events
|
||||
.iter()
|
||||
.filter(|event| event.correct.is_some())
|
||||
.for_each(|event| {
|
||||
let key = acc
|
||||
.per_key
|
||||
.entry(event.key)
|
||||
.or_insert_with(|| Fraction::new(0, 0));
|
||||
|
||||
acc.overall.denominator += 1;
|
||||
key.denominator += 1;
|
||||
|
||||
if event.correct.unwrap() {
|
||||
acc.overall.numerator += 1;
|
||||
key.numerator += 1;
|
||||
}
|
||||
});
|
||||
|
||||
acc
|
||||
}
|
||||
|
||||
fn calc_missed_words(test: &Test) -> Vec<String> {
|
||||
test.words
|
||||
.iter()
|
||||
.filter(|word| word.events.iter().any(is_missed_word_event))
|
||||
.map(|word| word.text.clone())
|
||||
.collect()
|
||||
}
|
512
src/ui.rs
Normal file
512
src/ui.rs
Normal file
|
@ -0,0 +1,512 @@
|
|||
use crate::config::Theme;
|
||||
|
||||
use super::test::{results, Test, TestWord};
|
||||
|
||||
use crossterm::event::KeyCode;
|
||||
use crossterm::event::KeyEvent;
|
||||
use ratatui::{
|
||||
buffer::Buffer,
|
||||
layout::{Constraint, Direction, Layout, Rect},
|
||||
symbols::Marker,
|
||||
text::{Line, Span, Text},
|
||||
widgets::{Axis, Block, BorderType, Borders, Chart, Dataset, GraphType, Paragraph, Widget},
|
||||
};
|
||||
use results::Fraction;
|
||||
|
||||
// Convert CPS to WPM (clicks per second)
|
||||
const WPM_PER_CPS: f64 = 12.0;
|
||||
|
||||
// Width of the moving average window for the WPM chart
|
||||
const WPM_SMA_WIDTH: usize = 10;
|
||||
|
||||
#[derive(Clone)]
|
||||
struct SizedBlock<'a> {
|
||||
block: Block<'a>,
|
||||
area: Rect,
|
||||
}
|
||||
|
||||
impl SizedBlock<'_> {
|
||||
fn render(self, buf: &mut Buffer) {
|
||||
self.block.render(self.area, buf)
|
||||
}
|
||||
}
|
||||
|
||||
trait UsedWidget: Widget {}
|
||||
impl UsedWidget for Paragraph<'_> {}
|
||||
|
||||
trait DrawInner<T> {
|
||||
fn draw_inner(&self, content: T, buf: &mut Buffer);
|
||||
}
|
||||
|
||||
impl DrawInner<&Line<'_>> for SizedBlock<'_> {
|
||||
fn draw_inner(&self, content: &Line, buf: &mut Buffer) {
|
||||
let inner = self.block.inner(self.area);
|
||||
buf.set_line(inner.x, inner.y, content, inner.width);
|
||||
}
|
||||
}
|
||||
|
||||
impl<T: UsedWidget> DrawInner<T> for SizedBlock<'_> {
|
||||
fn draw_inner(&self, content: T, buf: &mut Buffer) {
|
||||
let inner = self.block.inner(self.area);
|
||||
content.render(inner, buf);
|
||||
}
|
||||
}
|
||||
|
||||
pub trait ThemedWidget {
|
||||
fn render(self, area: Rect, buf: &mut Buffer, theme: &Theme);
|
||||
}
|
||||
|
||||
pub struct Themed<'t, W: ?Sized> {
|
||||
theme: &'t Theme,
|
||||
widget: W,
|
||||
}
|
||||
impl<'t, W: ThemedWidget> Widget for Themed<'t, W> {
|
||||
fn render(self, area: Rect, buf: &mut Buffer) {
|
||||
self.widget.render(area, buf, self.theme)
|
||||
}
|
||||
}
|
||||
impl Theme {
|
||||
pub fn apply_to<W>(&self, widget: W) -> Themed<'_, W> {
|
||||
Themed {
|
||||
theme: self,
|
||||
widget,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl ThemedWidget for &Test {
|
||||
fn render(self, area: Rect, buf: &mut Buffer, theme: &Theme) {
|
||||
buf.set_style(area, theme.default);
|
||||
|
||||
// Chunks
|
||||
let chunks = Layout::default()
|
||||
.direction(Direction::Vertical)
|
||||
.constraints([Constraint::Length(3), Constraint::Length(6)])
|
||||
.split(area);
|
||||
|
||||
// Sections
|
||||
let input = SizedBlock {
|
||||
block: Block::default()
|
||||
.title(Line::from(vec![Span::styled("Input", theme.title)]))
|
||||
.borders(Borders::ALL)
|
||||
.border_type(BorderType::Rounded)
|
||||
.border_style(theme.input_border),
|
||||
area: chunks[0],
|
||||
};
|
||||
input.draw_inner(
|
||||
&Line::from(self.words[self.current_word].progress.clone()),
|
||||
buf,
|
||||
);
|
||||
input.render(buf);
|
||||
|
||||
let target_lines: Vec<Line> = {
|
||||
let words = words_to_spans(&self.words, self.current_word, theme);
|
||||
|
||||
let mut lines: Vec<Line> = Vec::new();
|
||||
let mut current_line: Vec<Span> = Vec::new();
|
||||
let mut current_width = 0;
|
||||
for word in words {
|
||||
let word_width: usize = word.iter().map(|s| s.width()).sum();
|
||||
|
||||
if current_width + word_width > chunks[1].width as usize - 2 {
|
||||
current_line.push(Span::raw("\n"));
|
||||
lines.push(Line::from(current_line.clone()));
|
||||
current_line.clear();
|
||||
current_width = 0;
|
||||
}
|
||||
|
||||
current_line.extend(word);
|
||||
current_width += word_width;
|
||||
}
|
||||
lines.push(Line::from(current_line));
|
||||
|
||||
lines
|
||||
};
|
||||
let target = Paragraph::new(target_lines).block(
|
||||
Block::default()
|
||||
.title(Span::styled("Prompt", theme.title))
|
||||
.borders(Borders::ALL)
|
||||
.border_type(BorderType::Rounded)
|
||||
.border_style(theme.prompt_border),
|
||||
);
|
||||
target.render(chunks[1], buf);
|
||||
}
|
||||
}
|
||||
|
||||
fn words_to_spans<'a>(
|
||||
words: &'a [TestWord],
|
||||
current_word: usize,
|
||||
theme: &'a Theme,
|
||||
) -> Vec<Vec<Span<'a>>> {
|
||||
let mut spans = Vec::new();
|
||||
|
||||
for word in &words[..current_word] {
|
||||
let parts = split_typed_word(word);
|
||||
spans.push(word_parts_to_spans(parts, theme));
|
||||
}
|
||||
|
||||
let parts_current = split_current_word(&words[current_word]);
|
||||
spans.push(word_parts_to_spans(parts_current, theme));
|
||||
|
||||
for word in &words[current_word + 1..] {
|
||||
let parts = vec![(word.text.clone(), Status::Untyped)];
|
||||
spans.push(word_parts_to_spans(parts, theme));
|
||||
}
|
||||
spans
|
||||
}
|
||||
|
||||
#[derive(PartialEq, Clone, Copy, Debug)]
|
||||
enum Status {
|
||||
Correct,
|
||||
Incorrect,
|
||||
CurrentUntyped,
|
||||
CurrentCorrect,
|
||||
CurrentIncorrect,
|
||||
Cursor,
|
||||
Untyped,
|
||||
Overtyped,
|
||||
}
|
||||
|
||||
fn split_current_word(word: &TestWord) -> Vec<(String, Status)> {
|
||||
let mut parts = Vec::new();
|
||||
let mut cur_string = String::new();
|
||||
let mut cur_status = Status::Untyped;
|
||||
|
||||
let mut progress = word.progress.chars();
|
||||
for tc in word.text.chars() {
|
||||
let p = progress.next();
|
||||
let status = match p {
|
||||
None => Status::CurrentUntyped,
|
||||
Some(c) => match c {
|
||||
c if c == tc => Status::CurrentCorrect,
|
||||
_ => Status::CurrentIncorrect,
|
||||
},
|
||||
};
|
||||
|
||||
if status == cur_status {
|
||||
cur_string.push(tc);
|
||||
} else {
|
||||
if !cur_string.is_empty() {
|
||||
parts.push((cur_string, cur_status));
|
||||
cur_string = String::new();
|
||||
}
|
||||
cur_string.push(tc);
|
||||
cur_status = status;
|
||||
|
||||
// first currentuntyped is cursor
|
||||
if status == Status::CurrentUntyped {
|
||||
parts.push((cur_string, Status::Cursor));
|
||||
cur_string = String::new();
|
||||
}
|
||||
}
|
||||
}
|
||||
if !cur_string.is_empty() {
|
||||
parts.push((cur_string, cur_status));
|
||||
}
|
||||
let overtyped = progress.collect::<String>();
|
||||
if !overtyped.is_empty() {
|
||||
parts.push((overtyped, Status::Overtyped));
|
||||
}
|
||||
parts
|
||||
}
|
||||
|
||||
fn split_typed_word(word: &TestWord) -> Vec<(String, Status)> {
|
||||
let mut parts = Vec::new();
|
||||
let mut cur_string = String::new();
|
||||
let mut cur_status = Status::Untyped;
|
||||
|
||||
let mut progress = word.progress.chars();
|
||||
for tc in word.text.chars() {
|
||||
let p = progress.next();
|
||||
let status = match p {
|
||||
None => Status::Untyped,
|
||||
Some(c) => match c {
|
||||
c if c == tc => Status::Correct,
|
||||
_ => Status::Incorrect,
|
||||
},
|
||||
};
|
||||
|
||||
if status == cur_status {
|
||||
cur_string.push(tc);
|
||||
} else {
|
||||
if !cur_string.is_empty() {
|
||||
parts.push((cur_string, cur_status));
|
||||
cur_string = String::new();
|
||||
}
|
||||
cur_string.push(tc);
|
||||
cur_status = status;
|
||||
}
|
||||
}
|
||||
if !cur_string.is_empty() {
|
||||
parts.push((cur_string, cur_status));
|
||||
}
|
||||
|
||||
let overtyped = progress.collect::<String>();
|
||||
if !overtyped.is_empty() {
|
||||
parts.push((overtyped, Status::Overtyped));
|
||||
}
|
||||
parts
|
||||
}
|
||||
|
||||
fn word_parts_to_spans(parts: Vec<(String, Status)>, theme: &Theme) -> Vec<Span<'_>> {
|
||||
let mut spans = Vec::new();
|
||||
for (text, status) in parts {
|
||||
let style = match status {
|
||||
Status::Correct => theme.prompt_correct,
|
||||
Status::Incorrect => theme.prompt_incorrect,
|
||||
Status::Untyped => theme.prompt_untyped,
|
||||
Status::CurrentUntyped => theme.prompt_current_untyped,
|
||||
Status::CurrentCorrect => theme.prompt_current_correct,
|
||||
Status::CurrentIncorrect => theme.prompt_current_incorrect,
|
||||
Status::Cursor => theme.prompt_current_untyped.patch(theme.prompt_cursor),
|
||||
Status::Overtyped => theme.prompt_incorrect,
|
||||
};
|
||||
|
||||
spans.push(Span::styled(text, style));
|
||||
}
|
||||
spans.push(Span::styled(" ", theme.prompt_untyped));
|
||||
spans
|
||||
}
|
||||
|
||||
impl ThemedWidget for &results::Results {
|
||||
fn render(self, area: Rect, buf: &mut Buffer, theme: &Theme) {
|
||||
buf.set_style(area, theme.default);
|
||||
|
||||
// Chunks
|
||||
let chunks = Layout::default()
|
||||
.direction(Direction::Vertical)
|
||||
.constraints([Constraint::Min(1), Constraint::Length(1)])
|
||||
.split(area);
|
||||
let res_chunks = Layout::default()
|
||||
.direction(Direction::Vertical)
|
||||
.margin(1) // Graph looks tremendously better with just a little margin
|
||||
.constraints([Constraint::Ratio(1, 3), Constraint::Ratio(2, 3)])
|
||||
.split(chunks[0]);
|
||||
let info_chunks = Layout::default()
|
||||
.direction(Direction::Horizontal)
|
||||
.constraints([Constraint::Ratio(1, 2), Constraint::Ratio(1, 2)])
|
||||
.split(res_chunks[0]);
|
||||
|
||||
let msg = if self.missed_words.is_empty() {
|
||||
"Press 'q' to quit or 'r' for another test"
|
||||
} else {
|
||||
"Press 'q' to quit, 'r' for another test or 'p' to practice missed words"
|
||||
};
|
||||
|
||||
let exit = Span::styled(msg, theme.results_restart_prompt);
|
||||
buf.set_span(chunks[1].x, chunks[1].y, &exit, chunks[1].width);
|
||||
|
||||
// Sections
|
||||
let mut overview_text = Text::styled("", theme.results_overview);
|
||||
overview_text.extend([
|
||||
Line::from(format!(
|
||||
"Adjusted WPM: {:.1}",
|
||||
self.timing.overall_cps * WPM_PER_CPS * f64::from(self.accuracy.overall)
|
||||
)),
|
||||
Line::from(format!(
|
||||
"Accuracy: {:.1}%",
|
||||
f64::from(self.accuracy.overall) * 100f64
|
||||
)),
|
||||
Line::from(format!(
|
||||
"Raw WPM: {:.1}",
|
||||
self.timing.overall_cps * WPM_PER_CPS
|
||||
)),
|
||||
Line::from(format!("Correct Keypresses: {}", self.accuracy.overall)),
|
||||
]);
|
||||
let overview = Paragraph::new(overview_text).block(
|
||||
Block::default()
|
||||
.title(Span::styled("Overview", theme.title))
|
||||
.borders(Borders::ALL)
|
||||
.border_type(BorderType::Rounded)
|
||||
.border_style(theme.results_overview_border),
|
||||
);
|
||||
overview.render(info_chunks[0], buf);
|
||||
|
||||
let mut worst_keys: Vec<(&KeyEvent, &Fraction)> = self
|
||||
.accuracy
|
||||
.per_key
|
||||
.iter()
|
||||
.filter(|(key, _)| matches!(key.code, KeyCode::Char(_)))
|
||||
.collect();
|
||||
worst_keys.sort_unstable_by_key(|x| x.1);
|
||||
|
||||
let mut worst_text = Text::styled("", theme.results_worst_keys);
|
||||
worst_text.extend(
|
||||
worst_keys
|
||||
.iter()
|
||||
.filter_map(|(key, acc)| {
|
||||
if let KeyCode::Char(character) = key.code {
|
||||
let key_accuracy = f64::from(**acc) * 100.0;
|
||||
if key_accuracy != 100.0 {
|
||||
Some(format!("- {} at {:.1}% accuracy", character, key_accuracy))
|
||||
} else {
|
||||
None
|
||||
}
|
||||
} else {
|
||||
None
|
||||
}
|
||||
})
|
||||
.take(5)
|
||||
.map(Line::from),
|
||||
);
|
||||
let worst = Paragraph::new(worst_text).block(
|
||||
Block::default()
|
||||
.title(Span::styled("Worst Keys", theme.title))
|
||||
.borders(Borders::ALL)
|
||||
.border_type(BorderType::Rounded)
|
||||
.border_style(theme.results_worst_keys_border),
|
||||
);
|
||||
worst.render(info_chunks[1], buf);
|
||||
|
||||
let wpm_sma: Vec<(f64, f64)> = self
|
||||
.timing
|
||||
.per_event
|
||||
.windows(WPM_SMA_WIDTH)
|
||||
.enumerate()
|
||||
.map(|(i, window)| {
|
||||
(
|
||||
(i + WPM_SMA_WIDTH) as f64,
|
||||
window.len() as f64 / window.iter().copied().sum::<f64>() * WPM_PER_CPS,
|
||||
)
|
||||
})
|
||||
.collect();
|
||||
|
||||
// Render the chart if possible
|
||||
if !wpm_sma.is_empty() {
|
||||
let wpm_sma_min = wpm_sma
|
||||
.iter()
|
||||
.map(|(_, x)| x)
|
||||
.fold(f64::INFINITY, |a, &b| a.min(b));
|
||||
let wpm_sma_max = wpm_sma
|
||||
.iter()
|
||||
.map(|(_, x)| x)
|
||||
.fold(f64::NEG_INFINITY, |a, &b| a.max(b));
|
||||
|
||||
let wpm_datasets = vec![Dataset::default()
|
||||
.name("WPM")
|
||||
.marker(Marker::Braille)
|
||||
.graph_type(GraphType::Line)
|
||||
.style(theme.results_chart)
|
||||
.data(&wpm_sma)];
|
||||
|
||||
let y_label_min = wpm_sma_min as u16;
|
||||
let y_label_max = (wpm_sma_max as u16).max(y_label_min + 6);
|
||||
|
||||
let wpm_chart = Chart::new(wpm_datasets)
|
||||
.block(Block::default().title(vec![Span::styled("Chart", theme.title)]))
|
||||
.x_axis(
|
||||
Axis::default()
|
||||
.title(Span::styled("Keypresses", theme.results_chart_x))
|
||||
.bounds([0.0, self.timing.per_event.len() as f64]),
|
||||
)
|
||||
.y_axis(
|
||||
Axis::default()
|
||||
.title(Span::styled(
|
||||
"WPM (10-keypress rolling average)",
|
||||
theme.results_chart_y,
|
||||
))
|
||||
.bounds([wpm_sma_min, wpm_sma_max])
|
||||
.labels(
|
||||
(y_label_min..y_label_max)
|
||||
.step_by(5)
|
||||
.map(|n| Span::raw(format!("{}", n)))
|
||||
.collect(),
|
||||
),
|
||||
);
|
||||
wpm_chart.render(res_chunks[1], buf);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
|
||||
mod split_words {
|
||||
use super::Status::*;
|
||||
use super::*;
|
||||
|
||||
struct TestCase {
|
||||
word: &'static str,
|
||||
progress: &'static str,
|
||||
expected: Vec<(&'static str, Status)>,
|
||||
}
|
||||
|
||||
fn setup(test_case: TestCase) -> (TestWord, Vec<(String, Status)>) {
|
||||
let mut word = TestWord::from(test_case.word);
|
||||
word.progress = test_case.progress.to_string();
|
||||
|
||||
let expected = test_case
|
||||
.expected
|
||||
.iter()
|
||||
.map(|(s, v)| (s.to_string(), *v))
|
||||
.collect::<Vec<_>>();
|
||||
|
||||
(word, expected)
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn typed_words_split() {
|
||||
let cases = vec![
|
||||
TestCase {
|
||||
word: "monkeytype",
|
||||
progress: "monkeytype",
|
||||
expected: vec![("monkeytype", Correct)],
|
||||
},
|
||||
TestCase {
|
||||
word: "monkeytype",
|
||||
progress: "monkeXtype",
|
||||
expected: vec![("monke", Correct), ("y", Incorrect), ("type", Correct)],
|
||||
},
|
||||
TestCase {
|
||||
word: "monkeytype",
|
||||
progress: "monkeas",
|
||||
expected: vec![("monke", Correct), ("yt", Incorrect), ("ype", Untyped)],
|
||||
},
|
||||
];
|
||||
|
||||
for case in cases {
|
||||
let (word, expected) = setup(case);
|
||||
let got = split_typed_word(&word);
|
||||
assert_eq!(got, expected);
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn current_word_split() {
|
||||
let cases = vec![
|
||||
TestCase {
|
||||
word: "monkeytype",
|
||||
progress: "monkeytype",
|
||||
expected: vec![("monkeytype", CurrentCorrect)],
|
||||
},
|
||||
TestCase {
|
||||
word: "monkeytype",
|
||||
progress: "monke",
|
||||
expected: vec![
|
||||
("monke", CurrentCorrect),
|
||||
("y", Cursor),
|
||||
("type", CurrentUntyped),
|
||||
],
|
||||
},
|
||||
TestCase {
|
||||
word: "monkeytype",
|
||||
progress: "monkeXt",
|
||||
expected: vec![
|
||||
("monke", CurrentCorrect),
|
||||
("y", CurrentIncorrect),
|
||||
("t", CurrentCorrect),
|
||||
("y", Cursor),
|
||||
("pe", CurrentUntyped),
|
||||
],
|
||||
},
|
||||
];
|
||||
|
||||
for case in cases {
|
||||
let (word, expected) = setup(case);
|
||||
let got = split_current_word(&word);
|
||||
assert_eq!(got, expected);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
Loading…
Reference in a new issue