initial commit

This commit is contained in:
Jake Walker 2023-04-17 19:28:35 +01:00
commit e86dec9314
9 changed files with 946 additions and 0 deletions

12
.editorconfig Normal file
View file

@ -0,0 +1,12 @@
# EditorConfig is awesome: https://EditorConfig.org
# top-most EditorConfig file
root = true
[*]
indent_style = space
indent_size = 2
end_of_line = lf
charset = utf-8
trim_trailing_whitespace = true
insert_final_newline = true

216
.gitignore vendored Normal file
View file

@ -0,0 +1,216 @@
# Created by https://www.toptal.com/developers/gitignore/api/intellij+all,go,visualstudiocode,linux,macos,windows
# Edit at https://www.toptal.com/developers/gitignore?templates=intellij+all,go,visualstudiocode,linux,macos,windows
### Go ###
# If you prefer the allow list template instead of the deny list, see community template:
# https://github.com/github/gitignore/blob/main/community/Golang/Go.AllowList.gitignore
#
# Binaries for programs and plugins
*.exe
*.exe~
*.dll
*.so
*.dylib
# Test binary, built with `go test -c`
*.test
# Output of the go coverage tool, specifically when used with LiteIDE
*.out
# Dependency directories (remove the comment below to include it)
# vendor/
# Go workspace file
go.work
### Intellij+all ###
# Covers JetBrains IDEs: IntelliJ, RubyMine, PhpStorm, AppCode, PyCharm, CLion, Android Studio, WebStorm and Rider
# Reference: https://intellij-support.jetbrains.com/hc/en-us/articles/206544839
# User-specific stuff
.idea/**/workspace.xml
.idea/**/tasks.xml
.idea/**/usage.statistics.xml
.idea/**/dictionaries
.idea/**/shelf
# AWS User-specific
.idea/**/aws.xml
# Generated files
.idea/**/contentModel.xml
# Sensitive or high-churn files
.idea/**/dataSources/
.idea/**/dataSources.ids
.idea/**/dataSources.local.xml
.idea/**/sqlDataSources.xml
.idea/**/dynamic.xml
.idea/**/uiDesigner.xml
.idea/**/dbnavigator.xml
# Gradle
.idea/**/gradle.xml
.idea/**/libraries
# Gradle and Maven with auto-import
# When using Gradle or Maven with auto-import, you should exclude module files,
# since they will be recreated, and may cause churn. Uncomment if using
# auto-import.
# .idea/artifacts
# .idea/compiler.xml
# .idea/jarRepositories.xml
# .idea/modules.xml
# .idea/*.iml
# .idea/modules
# *.iml
# *.ipr
# CMake
cmake-build-*/
# Mongo Explorer plugin
.idea/**/mongoSettings.xml
# File-based project format
*.iws
# IntelliJ
out/
# mpeltonen/sbt-idea plugin
.idea_modules/
# JIRA plugin
atlassian-ide-plugin.xml
# Cursive Clojure plugin
.idea/replstate.xml
# SonarLint plugin
.idea/sonarlint/
# Crashlytics plugin (for Android Studio and IntelliJ)
com_crashlytics_export_strings.xml
crashlytics.properties
crashlytics-build.properties
fabric.properties
# Editor-based Rest Client
.idea/httpRequests
# Android studio 3.1+ serialized cache file
.idea/caches/build_file_checksums.ser
### Intellij+all Patch ###
# Ignore everything but code style settings and run configurations
# that are supposed to be shared within teams.
.idea/*
!.idea/codeStyles
!.idea/runConfigurations
### Linux ###
*~
# temporary files which can be created if a process still has a handle open of a deleted file
.fuse_hidden*
# KDE directory preferences
.directory
# Linux trash folder which might appear on any partition or disk
.Trash-*
# .nfs files are created when an open file is removed but is still being accessed
.nfs*
### macOS ###
# General
.DS_Store
.AppleDouble
.LSOverride
# Icon must end with two \r
Icon
# Thumbnails
._*
# Files that might appear in the root of a volume
.DocumentRevisions-V100
.fseventsd
.Spotlight-V100
.TemporaryItems
.Trashes
.VolumeIcon.icns
.com.apple.timemachine.donotpresent
# Directories potentially created on remote AFP share
.AppleDB
.AppleDesktop
Network Trash Folder
Temporary Items
.apdisk
### macOS Patch ###
# iCloud generated files
*.icloud
### VisualStudioCode ###
.vscode/*
!.vscode/settings.json
!.vscode/tasks.json
!.vscode/launch.json
!.vscode/extensions.json
!.vscode/*.code-snippets
# Local History for Visual Studio Code
.history/
# Built Visual Studio Code Extensions
*.vsix
### VisualStudioCode Patch ###
# Ignore all local history of files
.history
.ionide
### Windows ###
# Windows thumbnail cache files
Thumbs.db
Thumbs.db:encryptable
ehthumbs.db
ehthumbs_vista.db
# Dump file
*.stackdump
# Folder config file
[Dd]esktop.ini
# Recycle Bin used on file shares
$RECYCLE.BIN/
# Windows Installer files
*.cab
*.msi
*.msix
*.msm
*.msp
# Windows shortcuts
*.lnk
# End of https://www.toptal.com/developers/gitignore/api/intellij+all,go,visualstudiocode,linux,macos,windows
captain
state.toml
config.toml
# Added by cargo
/target

461
Cargo.lock generated Normal file
View file

@ -0,0 +1,461 @@
# This file is automatically @generated by Cargo.
# It is not intended for manual editing.
version = 3
[[package]]
name = "ansi_term"
version = "0.12.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "d52a9bb7ec0cf484c551830a7ce27bd20d67eac647e1befb56b0be4ee39a55d2"
dependencies = [
"winapi",
]
[[package]]
name = "anstream"
version = "0.3.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "9e579a7752471abc2a8268df8b20005e3eadd975f585398f17efcfd8d4927371"
dependencies = [
"anstyle",
"anstyle-parse",
"anstyle-query",
"anstyle-wincon",
"colorchoice",
"is-terminal",
"utf8parse",
]
[[package]]
name = "anstyle"
version = "1.0.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "41ed9a86bf92ae6580e0a31281f65a1b1d867c0cc68d5346e2ae128dddfa6a7d"
[[package]]
name = "anstyle-parse"
version = "0.2.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "e765fd216e48e067936442276d1d57399e37bce53c264d6fefbe298080cb57ee"
dependencies = [
"utf8parse",
]
[[package]]
name = "anstyle-query"
version = "1.0.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "5ca11d4be1bab0c8bc8734a9aa7bf4ee8316d462a08c6ac5052f888fef5b494b"
dependencies = [
"windows-sys",
]
[[package]]
name = "anstyle-wincon"
version = "1.0.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "4bcd8291a340dd8ac70e18878bc4501dd7b4ff970cfa21c207d36ece51ea88fd"
dependencies = [
"anstyle",
"windows-sys",
]
[[package]]
name = "autocfg"
version = "1.1.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "d468802bab17cbc0cc575e9b053f41e72aa36bfa6b7f55e3529ffa43161b97fa"
[[package]]
name = "bitflags"
version = "1.3.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "bef38d45163c2f1dde094a7dfd33ccf595c92905c8f8f4fdc18d06fb1037718a"
[[package]]
name = "captain"
version = "0.1.0"
dependencies = [
"ansi_term",
"clap",
"serde",
"text_io",
"toml",
]
[[package]]
name = "cc"
version = "1.0.79"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "50d30906286121d95be3d479533b458f87493b30a4b5f79a607db8f5d11aa91f"
[[package]]
name = "clap"
version = "4.2.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "9b802d85aaf3a1cdb02b224ba472ebdea62014fccfcb269b95a4d76443b5ee5a"
dependencies = [
"clap_builder",
"clap_derive",
"once_cell",
]
[[package]]
name = "clap_builder"
version = "4.2.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "14a1a858f532119338887a4b8e1af9c60de8249cd7bafd68036a489e261e37b6"
dependencies = [
"anstream",
"anstyle",
"bitflags",
"clap_lex",
"strsim",
]
[[package]]
name = "clap_derive"
version = "4.2.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "3f9644cd56d6b87dbe899ef8b053e331c0637664e9e21a33dfcdc36093f5c5c4"
dependencies = [
"heck",
"proc-macro2",
"quote",
"syn",
]
[[package]]
name = "clap_lex"
version = "0.4.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "8a2dd5a6fe8c6e3502f568a6353e5273bbb15193ad9a89e457b9970798efbea1"
[[package]]
name = "colorchoice"
version = "1.0.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "acbf1af155f9b9ef647e42cdc158db4b64a1b61f743629225fde6f3e0be2a7c7"
[[package]]
name = "errno"
version = "0.3.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "4bcfec3a70f97c962c307b2d2c56e358cf1d00b558d74262b5f929ee8cc7e73a"
dependencies = [
"errno-dragonfly",
"libc",
"windows-sys",
]
[[package]]
name = "errno-dragonfly"
version = "0.1.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "aa68f1b12764fab894d2755d2518754e71b4fd80ecfb822714a1206c2aab39bf"
dependencies = [
"cc",
"libc",
]
[[package]]
name = "hashbrown"
version = "0.12.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "8a9ee70c43aaf417c914396645a0fa852624801b24ebb7ae78fe8272889ac888"
[[package]]
name = "heck"
version = "0.4.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "95505c38b4572b2d910cecb0281560f54b440a19336cbbcb27bf6ce6adc6f5a8"
[[package]]
name = "hermit-abi"
version = "0.3.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "fed44880c466736ef9a5c5b5facefb5ed0785676d0c02d612db14e54f0d84286"
[[package]]
name = "indexmap"
version = "1.9.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "bd070e393353796e801d209ad339e89596eb4c8d430d18ede6a1cced8fafbd99"
dependencies = [
"autocfg",
"hashbrown",
]
[[package]]
name = "io-lifetimes"
version = "1.0.10"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "9c66c74d2ae7e79a5a8f7ac924adbe38ee42a859c6539ad869eb51f0b52dc220"
dependencies = [
"hermit-abi",
"libc",
"windows-sys",
]
[[package]]
name = "is-terminal"
version = "0.4.7"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "adcf93614601c8129ddf72e2d5633df827ba6551541c6d8c59520a371475be1f"
dependencies = [
"hermit-abi",
"io-lifetimes",
"rustix",
"windows-sys",
]
[[package]]
name = "libc"
version = "0.2.141"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "3304a64d199bb964be99741b7a14d26972741915b3649639149b2479bb46f4b5"
[[package]]
name = "linux-raw-sys"
version = "0.3.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "d59d8c75012853d2e872fb56bc8a2e53718e2cafe1a4c823143141c6d90c322f"
[[package]]
name = "memchr"
version = "2.5.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "2dffe52ecf27772e601905b7522cb4ef790d2cc203488bbd0e2fe85fcb74566d"
[[package]]
name = "once_cell"
version = "1.17.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "b7e5500299e16ebb147ae15a00a942af264cf3688f47923b8fc2cd5858f23ad3"
[[package]]
name = "proc-macro2"
version = "1.0.56"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "2b63bdb0cd06f1f4dedf69b254734f9b45af66e4a031e42a7480257d9898b435"
dependencies = [
"unicode-ident",
]
[[package]]
name = "quote"
version = "1.0.26"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "4424af4bf778aae2051a77b60283332f386554255d722233d09fbfc7e30da2fc"
dependencies = [
"proc-macro2",
]
[[package]]
name = "rustix"
version = "0.37.11"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "85597d61f83914ddeba6a47b3b8ffe7365107221c2e557ed94426489fefb5f77"
dependencies = [
"bitflags",
"errno",
"io-lifetimes",
"libc",
"linux-raw-sys",
"windows-sys",
]
[[package]]
name = "serde"
version = "1.0.160"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "bb2f3770c8bce3bcda7e149193a069a0f4365bda1fa5cd88e03bca26afc1216c"
dependencies = [
"serde_derive",
]
[[package]]
name = "serde_derive"
version = "1.0.160"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "291a097c63d8497e00160b166a967a4a79c64f3facdd01cbd7502231688d77df"
dependencies = [
"proc-macro2",
"quote",
"syn",
]
[[package]]
name = "serde_spanned"
version = "0.6.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "0efd8caf556a6cebd3b285caf480045fcc1ac04f6bd786b09a6f11af30c4fcf4"
dependencies = [
"serde",
]
[[package]]
name = "strsim"
version = "0.10.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "73473c0e59e6d5812c5dfe2a064a6444949f089e20eec9a2e5506596494e4623"
[[package]]
name = "syn"
version = "2.0.15"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "a34fcf3e8b60f57e6a14301a2e916d323af98b0ea63c599441eec8558660c822"
dependencies = [
"proc-macro2",
"quote",
"unicode-ident",
]
[[package]]
name = "text_io"
version = "0.1.12"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "d5f0c8eb2ad70c12a6a69508f499b3051c924f4b1cfeae85bfad96e6bc5bba46"
[[package]]
name = "toml"
version = "0.7.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "b403acf6f2bb0859c93c7f0d967cb4a75a7ac552100f9322faf64dc047669b21"
dependencies = [
"serde",
"serde_spanned",
"toml_datetime",
"toml_edit",
]
[[package]]
name = "toml_datetime"
version = "0.6.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "3ab8ed2edee10b50132aed5f331333428b011c99402b5a534154ed15746f9622"
dependencies = [
"serde",
]
[[package]]
name = "toml_edit"
version = "0.19.8"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "239410c8609e8125456927e6707163a3b1fdb40561e4b803bc041f466ccfdc13"
dependencies = [
"indexmap",
"serde",
"serde_spanned",
"toml_datetime",
"winnow",
]
[[package]]
name = "unicode-ident"
version = "1.0.8"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "e5464a87b239f13a63a501f2701565754bae92d243d4bb7eb12f6d57d2269bf4"
[[package]]
name = "utf8parse"
version = "0.2.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "711b9620af191e0cdc7468a8d14e709c3dcdb115b36f838e601583af800a370a"
[[package]]
name = "winapi"
version = "0.3.9"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "5c839a674fcd7a98952e593242ea400abe93992746761e38641405d28b00f419"
dependencies = [
"winapi-i686-pc-windows-gnu",
"winapi-x86_64-pc-windows-gnu",
]
[[package]]
name = "winapi-i686-pc-windows-gnu"
version = "0.4.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "ac3b87c63620426dd9b991e5ce0329eff545bccbbb34f3be09ff6fb6ab51b7b6"
[[package]]
name = "winapi-x86_64-pc-windows-gnu"
version = "0.4.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "712e227841d057c1ee1cd2fb22fa7e5a5461ae8e48fa2ca79ec42cfc1931183f"
[[package]]
name = "windows-sys"
version = "0.48.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "677d2418bec65e3338edb076e806bc1ec15693c5d0104683f2efe857f61056a9"
dependencies = [
"windows-targets",
]
[[package]]
name = "windows-targets"
version = "0.48.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "7b1eb6f0cd7c80c79759c929114ef071b87354ce476d9d94271031c0497adfd5"
dependencies = [
"windows_aarch64_gnullvm",
"windows_aarch64_msvc",
"windows_i686_gnu",
"windows_i686_msvc",
"windows_x86_64_gnu",
"windows_x86_64_gnullvm",
"windows_x86_64_msvc",
]
[[package]]
name = "windows_aarch64_gnullvm"
version = "0.48.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "91ae572e1b79dba883e0d315474df7305d12f569b400fcf90581b06062f7e1bc"
[[package]]
name = "windows_aarch64_msvc"
version = "0.48.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "b2ef27e0d7bdfcfc7b868b317c1d32c641a6fe4629c171b8928c7b08d98d7cf3"
[[package]]
name = "windows_i686_gnu"
version = "0.48.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "622a1962a7db830d6fd0a69683c80a18fda201879f0f447f065a3b7467daa241"
[[package]]
name = "windows_i686_msvc"
version = "0.48.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "4542c6e364ce21bf45d69fdd2a8e455fa38d316158cfd43b3ac1c5b1b19f8e00"
[[package]]
name = "windows_x86_64_gnu"
version = "0.48.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "ca2b8a661f7628cbd23440e50b05d705db3686f894fc9580820623656af974b1"
[[package]]
name = "windows_x86_64_gnullvm"
version = "0.48.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "7896dbc1f41e08872e9d5e8f8baa8fdd2677f29468c4e156210174edc7f7b953"
[[package]]
name = "windows_x86_64_msvc"
version = "0.48.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "1a515f5799fe4961cb532f983ce2b23082366b898e52ffbce459c86f67c8378a"
[[package]]
name = "winnow"
version = "0.4.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "ae8970b36c66498d8ff1d66685dc86b91b29db0c7739899012f63a63814b4b28"
dependencies = [
"memchr",
]

13
Cargo.toml Normal file
View file

@ -0,0 +1,13 @@
[package]
name = "captain"
version = "0.1.0"
edition = "2021"
# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html
[dependencies]
ansi_term = "0.12.1"
clap = { version = "4.2.2", features = ["derive"] }
serde = { version = "1.0.160", features = ["derive"] }
text_io = "0.1.12"
toml = "0.7.3"

45
README.md Normal file
View file

@ -0,0 +1,45 @@
# Captain
Captain is a simple tool for creating interactive command line lessons.
## Usage
Captain makes use of a `config.toml` file which contains all the exercises for a single lesson. The current progress of the lesson is stored in the `state.toml` file.
### Commands
- **`captain`/`captain view`** - View the current problem.
- **`captain hint`** - View the hint for the current problem.
- **`captain check`** - Run the current problem's check. If the check passes, captain will move onto the next problem.
- **`captain reset`** - Reset progress to the first problem.
### Creating a config
The config file should contain a `success_message` which is printed once everything has been completed, and multiple `problems` like so:
```toml
success_message = "Congrats!"
[[problem]]
text = "What colour is the sky?"
hint = "It rhymes with moo"
check_type = "ask"
check = "blue"
```
Each problem has the following fields:
| Field | Description |
| --- | --- |
| `text` | This is shown to the user to explain what to do. |
| `hint` | This is shown when the user runs `captain hint`. It should give a bit of help towards the answer. It can be left blank. |
| `check_type` | _See below_ |
| `check` | _See below_ |
#### Checks
There are multiple checks available:
* `ask` - This simply asks the user to type and answer and checks whether it equals whatever is in `check`.
* `ask_cmd` - This asks the user to type an answer and checks it against the output of the command from `check`.
* `cmd` - This runs the command in `check` and checks whether it is successful.

24
src/config.rs Normal file
View file

@ -0,0 +1,24 @@
use serde::Deserialize;
use std::fs;
#[derive(Deserialize)]
pub struct Config {
pub success_message: String,
pub problems: Vec<Problem>
}
#[derive(Deserialize)]
pub struct Problem {
pub text: String,
pub hint: Option<String>,
pub check_type: String,
pub check: String
}
impl Config {
pub fn load() -> Self {
let path = std::env::var("CAPTAIN_CONFIG").unwrap_or_else(|_| "./config.toml".to_string());
let contents = fs::read_to_string(path).expect("Should be able to read config");
toml::from_str(&contents).expect("Should be able to parse config")
}
}

124
src/main.rs Normal file
View file

@ -0,0 +1,124 @@
use clap::{Parser, Subcommand};
use styles::Style;
use std::{env, process::Command};
use text_io::read;
mod config;
mod state;
mod styles;
#[derive(Parser)]
#[command(version, about, long_about = None)]
struct Args {
#[command(subcommand)]
command: Option<Commands>
}
#[derive(Subcommand)]
enum Commands {
/// Print the current problem
View,
/// Print the current problem's hint
Hint,
/// Check the solution of, or answer the current problem
Check,
/// Reset progress to the first problem
Reset
}
fn print_problem(config: config::Config, state: state::State) {
let problem = config.problems.get(state.problem_index).expect("Should be able to get current problem");
let name = env::args().next().unwrap_or_else(|| "captain".to_owned());
println!("{}\n{}\n\n{}",
Style::Header.as_style().paint(format!("Problem {}:", state.problem_index)),
problem.text,
Style::Hint.as_style().paint(format!("Run `{} check` once complete", name)))
}
fn print_hint(config: config::Config, state: state::State) {
let problem = config.problems.get(state.problem_index).expect("Should be able to get current problem");
let name = env::args().next().unwrap_or_else(|| "captain".to_owned());
match problem.hint.clone() {
Some(hint) => println!("{}\n{}\n\n{}",
Style::Header.as_style().paint(format!("Problem {} (Hint):", state.problem_index)),
hint,
Style::Hint.as_style().paint(format!("Run `{} check` once complete", name))),
None => println!("{}", Style::Warning.as_style().paint("There is no hint available for this problem"))
}
}
fn reset_state(mut state: state::State) {
state.problem_index = 0;
state.save();
println!("{}", Style::Good.as_style().paint("State has been reset!"))
}
fn run_command(cmd: String) -> Result<std::process::Output, std::io::Error> {
Command::new("bash").arg("-c").arg(cmd).output()
}
fn ask(expected: String) -> bool {
const PROMPT: &str = "What is the answer? ";
for _ in 1..4 {
print!("{}", Style::Prompt.as_style().paint(PROMPT));
let actual: String = read!("{}\n");
if expected.trim() == actual.trim() {
return true;
}
}
false
}
fn check(config: config::Config, mut state: state::State) {
let problem = config.problems.get(state.problem_index).expect("Should be able to get current problem");
println!("{}\n{}\n",
Style::Header.as_style().paint(format!("Problem {}:", state.problem_index)),
problem.text);
let ok = match problem.check_type.as_str() {
"cmd" => run_command(problem.check.clone()).expect("").status.success(),
"ask_cmd" => {
let cmd = run_command(problem.check.clone()).expect("Should be able to run check command successfully");
let expected = std::str::from_utf8(&cmd.stdout).expect("Should be able to parse check command output");
ask(expected.to_owned())
},
"ask" => {
ask(problem.check.clone())
}
_ => panic!("Check type {} is not implemented!", problem.check_type)
};
if !ok {
println!("{}", Style::Bad.as_style().paint("That's not quite right, try again."));
return;
}
if state.problem_index + 1 >= config.problems.len() {
println!("{}", Style::Good.as_style().paint(config.success_message));
return;
}
let name = env::args().next().unwrap_or_else(|| "captain".to_owned());
println!("{}", Style::Good.as_style().paint(format!("Great! Run `{}` for the next problem.", name)));
state.problem_index += 1;
state.save();
}
fn main() {
let args = Args::parse();
let config = config::Config::load();
let state = state::State::load();
match &args.command {
Some(Commands::View) | None => print_problem(config, state),
Some(Commands::Hint) => print_hint(config, state),
Some(Commands::Reset) => reset_state(state),
Some(Commands::Check) => check(config, state)
}
}

27
src/state.rs Normal file
View file

@ -0,0 +1,27 @@
use serde::{Deserialize, Serialize};
use std::{fs, io::ErrorKind};
#[derive(Serialize, Deserialize)]
pub struct State {
pub problem_index: usize
}
impl State {
pub fn load() -> Self {
let path = std::env::var("CAPTAIN_STATE").unwrap_or_else(|_| "./state.toml".to_string());
let contents = fs::read_to_string(path);
match contents {
Ok(str) => toml::from_str(&str).expect("Should be able to parse state"),
Err(error) => match error.kind() {
ErrorKind::NotFound => State { problem_index: 0 },
_ => panic!("Could not read state file: {:?}", error)
}
}
}
pub fn save(&self) {
let path = std::env::var("CAPTAIN_STATE").unwrap_or_else(|_| "./state.toml".to_string());
let contents = toml::to_string(self).expect("Should be able to serialize state");
fs::write(path, contents).expect("Should be able to write state file");
}
}

24
src/styles.rs Normal file
View file

@ -0,0 +1,24 @@
use ansi_term::Style as TermStyle;
use ansi_term::Color;
pub enum Style {
Header,
Good,
Bad,
Warning,
Hint,
Prompt
}
impl Style {
pub fn as_style(&self) -> TermStyle {
match self {
Style::Header => TermStyle::new().bold().underline(),
Style::Good => TermStyle::new().bold().fg(Color::Green),
Style::Bad => TermStyle::new().bold().fg(Color::Red),
Style::Warning => TermStyle::new().fg(Color::Yellow),
Style::Hint => TermStyle::new().dimmed().italic(),
Style::Prompt => TermStyle::new().bold().fg(Color::Purple)
}
}
}