~prma/ddd

b5e784f10587283011ec30ee1940d4b92af7502f — Perma Alesheikh 5 months ago fd6acee
add a gui functionality

Signed-off-by: Perma Alesheikh <me@prma.dev>
M Cargo.lock => Cargo.lock +498 -0
@@ 75,6 75,17 @@ dependencies = [
]

[[package]]
name = "async-trait"
version = "0.1.80"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "c6fa2087f2753a7da8cc1c0dbfcf89579dd57458e36769de5ac750b4671737ca"
dependencies = [
 "proc-macro2",
 "quote",
 "syn",
]

[[package]]
name = "atomic"
version = "0.6.0"
source = "registry+https://github.com/rust-lang/crates.io-index"


@@ 90,6 101,74 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "f1fdabc7756949593fe60f30ec81974b613357de856987752631dea1e3394c80"

[[package]]
name = "axum"
version = "0.7.5"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "3a6c9af12842a67734c9a2e355436e5d03b22383ed60cf13cd0c18fbfe3dcbcf"
dependencies = [
 "async-trait",
 "axum-core",
 "axum-macros",
 "bytes",
 "futures-util",
 "http",
 "http-body",
 "http-body-util",
 "hyper",
 "hyper-util",
 "itoa",
 "matchit",
 "memchr",
 "mime",
 "percent-encoding",
 "pin-project-lite",
 "rustversion",
 "serde",
 "serde_json",
 "serde_path_to_error",
 "serde_urlencoded",
 "sync_wrapper 1.0.1",
 "tokio",
 "tower",
 "tower-layer",
 "tower-service",
 "tracing",
]

[[package]]
name = "axum-core"
version = "0.4.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "a15c63fd72d41492dc4f497196f5da1fb04fb7529e631d73630d1b491e47a2e3"
dependencies = [
 "async-trait",
 "bytes",
 "futures-util",
 "http",
 "http-body",
 "http-body-util",
 "mime",
 "pin-project-lite",
 "rustversion",
 "sync_wrapper 0.1.2",
 "tower-layer",
 "tower-service",
 "tracing",
]

[[package]]
name = "axum-macros"
version = "0.4.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "00c055ee2d014ae5981ce1016374e8213682aa14d9bf40e48ab48b5f3ef20eaa"
dependencies = [
 "heck 0.4.1",
 "proc-macro2",
 "quote",
 "syn",
]

[[package]]
name = "backtrace"
version = "0.3.71"
source = "registry+https://github.com/rust-lang/crates.io-index"


@@ 129,6 208,12 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "1fd0f2584146f6f2ef48085050886acf353beff7305ebd1ae69500e27c67f64b"

[[package]]
name = "bytes"
version = "1.6.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "514de17de45fdb8dc022b1a7975556c53c86f9f0aa5f534b98977b171857c2c9"

[[package]]
name = "cc"
version = "1.0.95"
source = "registry+https://github.com/rust-lang/crates.io-index"


@@ 308,6 393,7 @@ dependencies = [
name = "der_die_das"
version = "0.2.5"
dependencies = [
 "axum",
 "clap",
 "clap_complete_command",
 "color-eyre",


@@ 317,9 403,13 @@ dependencies = [
 "figment",
 "fs_extra",
 "inquire",
 "maud",
 "serde",
 "serde_json",
 "time",
 "tokio",
 "tower",
 "tower-http",
 "tracing",
 "tracing-error",
 "tracing-subscriber",


@@ 393,12 483,66 @@ dependencies = [
]

[[package]]
name = "fnv"
version = "1.0.7"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "3f9eec918d3f24069decb9af1554cad7c880e2da24a9afd88aca000531ab82c1"

[[package]]
name = "form_urlencoded"
version = "1.2.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "e13624c2627564efccf4934284bdd98cbaa14e79b0b5a141218e507b3a823456"
dependencies = [
 "percent-encoding",
]

[[package]]
name = "fs_extra"
version = "1.3.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "42703706b716c37f96a77aea830392ad231f44c9e9a67872fa5548707e11b11c"

[[package]]
name = "futures-channel"
version = "0.3.30"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "eac8f7d7865dcb88bd4373ab671c8cf4508703796caa2b1985a9ca867b3fcb78"
dependencies = [
 "futures-core",
]

[[package]]
name = "futures-core"
version = "0.3.30"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "dfc6580bb841c5a68e9ef15c77ccc837b40a7504914d52e47b8b0e9bbda25a1d"

[[package]]
name = "futures-sink"
version = "0.3.30"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "9fb8e00e87438d937621c1c6269e53f536c14d3fbd6a042bb24879e57d474fb5"

[[package]]
name = "futures-task"
version = "0.3.30"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "38d84fa142264698cdce1a9f9172cf383a0c82de1bddcf3092901442c4097004"

[[package]]
name = "futures-util"
version = "0.3.30"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "3d6401deb83407ab3da39eba7e33987a73c3df0c82b4bb5813ee871c19c41d48"
dependencies = [
 "futures-core",
 "futures-task",
 "pin-project-lite",
 "pin-utils",
]

[[package]]
name = "fuzzy-matcher"
version = "0.3.7"
source = "registry+https://github.com/rust-lang/crates.io-index"


@@ 446,6 590,99 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "2304e00983f87ffb38b55b444b5e3b60a884b5d30c0fca7d82fe33449bbe55ea"

[[package]]
name = "hermit-abi"
version = "0.3.9"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "d231dfb89cfffdbc30e7fc41579ed6066ad03abda9e567ccafae602b97ec5024"

[[package]]
name = "http"
version = "1.1.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "21b9ddb458710bc376481b842f5da65cdf31522de232c1ca8146abce2a358258"
dependencies = [
 "bytes",
 "fnv",
 "itoa",
]

[[package]]
name = "http-body"
version = "1.0.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "1cac85db508abc24a2e48553ba12a996e87244a0395ce011e62b37158745d643"
dependencies = [
 "bytes",
 "http",
]

[[package]]
name = "http-body-util"
version = "0.1.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "0475f8b2ac86659c21b64320d5d653f9efe42acd2a4e560073ec61a155a34f1d"
dependencies = [
 "bytes",
 "futures-core",
 "http",
 "http-body",
 "pin-project-lite",
]

[[package]]
name = "http-range-header"
version = "0.4.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "3ce4ef31cda248bbdb6e6820603b82dfcd9e833db65a43e997a0ccec777d11fe"

[[package]]
name = "httparse"
version = "1.8.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "d897f394bad6a705d5f4104762e116a75639e470d80901eed05a860a95cb1904"

[[package]]
name = "httpdate"
version = "1.0.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "df3b46402a9d5adb4c86a0cf463f42e19994e3ee891101b1841f30a545cb49a9"

[[package]]
name = "hyper"
version = "1.3.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "fe575dd17d0862a9a33781c8c4696a55c320909004a67a00fb286ba8b1bc496d"
dependencies = [
 "bytes",
 "futures-channel",
 "futures-util",
 "http",
 "http-body",
 "httparse",
 "httpdate",
 "itoa",
 "pin-project-lite",
 "smallvec",
 "tokio",
]

[[package]]
name = "hyper-util"
version = "0.1.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "ca38ef113da30126bbff9cd1705f9273e15d45498615d138b0c20279ac7a76aa"
dependencies = [
 "bytes",
 "futures-util",
 "http",
 "http-body",
 "hyper",
 "pin-project-lite",
 "socket2",
 "tokio",
]

[[package]]
name = "indenter"
version = "0.3.3"
source = "registry+https://github.com/rust-lang/crates.io-index"


@@ 528,12 765,58 @@ dependencies = [
]

[[package]]
name = "matchit"
version = "0.7.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "0e7465ac9959cc2b1404e8e2367b43684a6d13790fe23056cc8c6c5a6b7bcb94"

[[package]]
name = "maud"
version = "0.26.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "df518b75016b4289cdddffa1b01f2122f4a49802c93191f3133f6dc2472ebcaa"
dependencies = [
 "axum-core",
 "http",
 "itoa",
 "maud_macros",
]

[[package]]
name = "maud_macros"
version = "0.26.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "fa453238ec218da0af6b11fc5978d3b5c3a45ed97b722391a2a11f3306274e18"
dependencies = [
 "proc-macro-error",
 "proc-macro2",
 "quote",
 "syn",
]

[[package]]
name = "memchr"
version = "2.7.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "6c8640c5d730cb13ebd907d8d04b52f55ac9a2eec55b440c8892f40d56c76c1d"

[[package]]
name = "mime"
version = "0.3.17"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "6877bb514081ee2a7ff5ef9de3281f14a4dd4bceac4c09388074a6b5df8a139a"

[[package]]
name = "mime_guess"
version = "2.0.4"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "4192263c238a5f0d0c6bfd21f336a313a4ce1c450542449ca191bb657b4642ef"
dependencies = [
 "mime",
 "unicase",
]

[[package]]
name = "miniz_oxide"
version = "0.7.2"
source = "registry+https://github.com/rust-lang/crates.io-index"


@@ 580,6 863,16 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "51d515d32fb182ee37cda2ccdcb92950d6a3c2893aa280e540671c2cd0f3b1d9"

[[package]]
name = "num_cpus"
version = "1.16.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "4161fcb6d602d4d2081af7c3a45852d875a03dd337a6bfdd6e06407b61342a43"
dependencies = [
 "hermit-abi",
 "libc",
]

[[package]]
name = "num_threads"
version = "0.1.7"
source = "registry+https://github.com/rust-lang/crates.io-index"


@@ 668,18 961,73 @@ dependencies = [
]

[[package]]
name = "percent-encoding"
version = "2.3.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "e3148f5046208a5d56bcfc03053e3ca6334e51da8dfb19b6cdc8b306fae3283e"

[[package]]
name = "pin-project"
version = "1.1.5"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "b6bf43b791c5b9e34c3d182969b4abb522f9343702850a2e57f460d00d09b4b3"
dependencies = [
 "pin-project-internal",
]

[[package]]
name = "pin-project-internal"
version = "1.1.5"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "2f38a4412a78282e09a2cf38d195ea5420d15ba0602cb375210efbc877243965"
dependencies = [
 "proc-macro2",
 "quote",
 "syn",
]

[[package]]
name = "pin-project-lite"
version = "0.2.14"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "bda66fc9667c18cb2758a2ac84d1167245054bcf85d5d1aaa6923f45801bdd02"

[[package]]
name = "pin-utils"
version = "0.1.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "8b870d8c151b6f2fb93e84a13146138f05d02ed11c7e7c54f8826aaaf7c9f184"

[[package]]
name = "powerfmt"
version = "0.2.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "439ee305def115ba05938db6eb1644ff94165c5ab5e9420d1c1bcedbba909391"

[[package]]
name = "proc-macro-error"
version = "1.0.4"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "da25490ff9892aab3fcf7c36f08cfb902dd3e71ca0f9f9517bea02a73a5ce38c"
dependencies = [
 "proc-macro-error-attr",
 "proc-macro2",
 "quote",
 "version_check",
]

[[package]]
name = "proc-macro-error-attr"
version = "1.0.4"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "a1be40180e52ecc98ad80b184934baf3d0d29f979574e439af5a55274b35f869"
dependencies = [
 "proc-macro2",
 "quote",
 "version_check",
]

[[package]]
name = "proc-macro2"
version = "1.0.81"
source = "registry+https://github.com/rust-lang/crates.io-index"


@@ 830,6 1178,28 @@ dependencies = [
]

[[package]]
name = "serde_path_to_error"
version = "0.1.16"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "af99884400da37c88f5e9146b7f1fd0fbcae8f6eec4e9da38b67d05486f814a6"
dependencies = [
 "itoa",
 "serde",
]

[[package]]
name = "serde_urlencoded"
version = "0.7.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "d3491c14715ca2294c4d6a88f15e84739788c1d030eed8c110436aafdaa2f3fd"
dependencies = [
 "form_urlencoded",
 "itoa",
 "ryu",
 "serde",
]

[[package]]
name = "sharded-slab"
version = "0.1.7"
source = "registry+https://github.com/rust-lang/crates.io-index"


@@ 875,6 1245,16 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "3c5e1a9a646d36c3599cd173a41282daf47c44583ad367b8e6837255952e5c67"

[[package]]
name = "socket2"
version = "0.5.6"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "05ffd9c0a93b7543e062e759284fcf5f5e3b098501104bfbdde4d404db792871"
dependencies = [
 "libc",
 "windows-sys 0.52.0",
]

[[package]]
name = "strsim"
version = "0.11.1"
source = "registry+https://github.com/rust-lang/crates.io-index"


@@ 911,6 1291,18 @@ dependencies = [
]

[[package]]
name = "sync_wrapper"
version = "0.1.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "2047c6ded9c721764247e62cd3b03c09ffc529b2ba5b10ec482ae507a4a70160"

[[package]]
name = "sync_wrapper"
version = "1.0.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "a7065abeca94b6a8a577f9bd45aa0867a2238b74e8eb67cf10d492bc39351394"

[[package]]
name = "thiserror"
version = "1.0.59"
source = "registry+https://github.com/rust-lang/crates.io-index"


@@ 974,11 1366,108 @@ dependencies = [
]

[[package]]
name = "tokio"
version = "1.37.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "1adbebffeca75fcfd058afa480fb6c0b81e165a0323f9c9d39c9697e37c46787"
dependencies = [
 "backtrace",
 "bytes",
 "libc",
 "mio",
 "num_cpus",
 "parking_lot",
 "pin-project-lite",
 "signal-hook-registry",
 "socket2",
 "tokio-macros",
 "windows-sys 0.48.0",
]

[[package]]
name = "tokio-macros"
version = "2.2.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "5b8a1e28f2deaa14e508979454cb3a223b10b938b45af148bc0986de36f1923b"
dependencies = [
 "proc-macro2",
 "quote",
 "syn",
]

[[package]]
name = "tokio-util"
version = "0.7.10"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "5419f34732d9eb6ee4c3578b7989078579b7f039cbbb9ca2c4da015749371e15"
dependencies = [
 "bytes",
 "futures-core",
 "futures-sink",
 "pin-project-lite",
 "tokio",
]

[[package]]
name = "tower"
version = "0.4.13"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "b8fa9be0de6cf49e536ce1851f987bd21a43b771b09473c3549a6c853db37c1c"
dependencies = [
 "futures-core",
 "futures-util",
 "pin-project",
 "pin-project-lite",
 "tokio",
 "tower-layer",
 "tower-service",
 "tracing",
]

[[package]]
name = "tower-http"
version = "0.5.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "1e9cd434a998747dd2c4276bc96ee2e0c7a2eadf3cae88e52be55a05fa9053f5"
dependencies = [
 "bitflags 2.5.0",
 "bytes",
 "futures-util",
 "http",
 "http-body",
 "http-body-util",
 "http-range-header",
 "httpdate",
 "mime",
 "mime_guess",
 "percent-encoding",
 "pin-project-lite",
 "tokio",
 "tokio-util",
 "tower-layer",
 "tower-service",
 "tracing",
]

[[package]]
name = "tower-layer"
version = "0.3.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "c20c8dbed6283a09604c3e69b4b7eeb54e298b8a600d4d5ecb5ad39de609f1d0"

[[package]]
name = "tower-service"
version = "0.3.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "b6bc1c9ce2b5135ac7f93c72918fc37feb872bdc6a5533a8b85eb4b86bfdae52"

[[package]]
name = "tracing"
version = "0.1.40"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "c3523ab5a71916ccf420eebdf5521fcef02141234bbc0b8a49f2fdc4544364ef"
dependencies = [
 "log",
 "pin-project-lite",
 "tracing-attributes",
 "tracing-core",


@@ 1054,6 1543,15 @@ dependencies = [
]

[[package]]
name = "unicase"
version = "2.7.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "f7d2d4dafb69621809a81864c9c1b864479e1235c0dd4e199924b9742439ed89"
dependencies = [
 "version_check",
]

[[package]]
name = "unicode-ident"
version = "1.0.12"
source = "registry+https://github.com/rust-lang/crates.io-index"

M Cargo.toml => Cargo.toml +16 -0
@@ 15,12 15,16 @@ categories = ["command-line-utilities", "games"]
[[bin]]
name = "ddd"
path = "src/bin/ddd/main.rs"
[[bin]]
name = "homepage"
path = "src/bin/homepage/main.rs"

[lib]
path = "src/lib.rs"


[dependencies]
axum = { version = "0.7.5", optional = true, features = ["macros"] }
clap = { version = "4.5.4", features = ["derive"] }
clap_complete_command = "0.5.1"
color-eyre = "0.6.3"


@@ 30,12 34,24 @@ dirs = "5.0.1"
figment = { version = "0.10.18", features = ["env", "json"] }
fs_extra = "1.3.0"
inquire = "0.7.4"
maud = { version = "0.26.0", features = ["axum"], optional = true }
serde = { version = "1.0.198", features = ["derive"] }
serde_json = "1.0.116"
time = { version = "0.3.36", features = ["serde", "local-offset", "serde-well-known"] }
tokio = { version = "1.37.0", features = ["full"], optional = true }
tower = { version = "0.4.13", features = ["util"], optional = true }
tower-http = { version = "0.5.2", features = ["fs", "trace"], optional = true }
tracing = "0.1.40"
tracing-error = "0.2.0"
tracing-subscriber = { version = "0.3.18", features = ["env-filter"] }

[lints.clippy]
pedantic = "warn"

[features]
default = ["homepage"]
homepage = ["dep:tokio", "dep:axum",  "dep:maud",  "dep:tower-http", "dep:tower"]


[target.x86_64-pc-windows-gnu]
linker = "x86_64-w64-mingw32-gcc"

A assets/logo.svg => assets/logo.svg +1 -0
@@ 0,0 1,1 @@
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 640 640"><defs><style>.cls-1{fill:#dde9d4;}.cls-2{fill:#8b2a01;}.cls-3{fill:#ff7e47;}.cls-4{fill:#df581f;}</style></defs><g id="Layer_2" data-name="Layer 2"><g id="Layer_1-2" data-name="Layer 1"><path class="cls-1" d="M0,124A124,124,0,0,1,124,0H516A124,124,0,0,1,640,124V516A124,124,0,0,1,516,640H124A124,124,0,0,1,0,516Z"/><path class="cls-2" d="M549.3,400a9,9,0,0,1,9-9h11.4a9,9,0,0,1,9,9v10.2a9,9,0,0,1-9,9H558.3a9,9,0,0,1-9-9Z"/><path class="cls-2" d="M518.8,431.8a9,9,0,0,1,9-9H538a9,9,0,0,1,9,9v11.4a9,9,0,0,1-9,9H527.8a9,9,0,0,1-9-9Z"/><path class="cls-2" d="M487,528.2a9,9,0,0,1,9-9h10.2a9,9,0,0,1,9,9v11.4a9,9,0,0,1-9,9H496a9,9,0,0,1-9-9Z"/><path class="cls-2" d="M487,496.5a9,9,0,0,1,9-9h10.2a9,9,0,0,1,9,9v10.2a9,9,0,0,1-9,9H496a9,9,0,0,1-9-9Z"/><path class="cls-2" d="M487,463.5a9,9,0,0,1,9-9h10.2a9,9,0,0,1,9,9v11.4a9,9,0,0,1-9,9H496a9,9,0,0,1-9-9Z"/><path class="cls-2" d="M487,431.8a9,9,0,0,1,9-9h10.2a9,9,0,0,1,9,9v11.4a9,9,0,0,1-9,9H496a9,9,0,0,1-9-9Z"/><path class="cls-2" d="M487,400a9,9,0,0,1,9-9h10.2a9,9,0,0,1,9,9v10.2a9,9,0,0,1-9,9H496a9,9,0,0,1-9-9Z"/><path class="cls-2" d="M445.5,528.6a9,9,0,0,1,9-9h11.4a9,9,0,0,1,9,9V540a9,9,0,0,1-9,9H454.5a9,9,0,0,1-9-9Z"/><path class="cls-2" d="M445.5,463.9a9,9,0,0,1,9-9h11.4a9,9,0,0,1,9,9v11.4a9,9,0,0,1-9,9H454.5a9,9,0,0,1-9-9Z"/><path class="cls-2" d="M445.5,432.1a9,9,0,0,1,9-9h11.4a9,9,0,0,1,9,9v11.4a9,9,0,0,1-9,9H454.5a9,9,0,0,1-9-9Z"/><path class="cls-2" d="M445.5,400.4a9,9,0,0,1,9-9h11.4a9,9,0,0,1,9,9v10.2a9,9,0,0,1-9,9H454.5a9,9,0,0,1-9-9Z"/><path class="cls-2" d="M413.8,528.6a9,9,0,0,1,9-9h11.4a9,9,0,0,1,9,9V540a9,9,0,0,1-9,9H422.8a9,9,0,0,1-9-9Z"/><path class="cls-2" d="M413.8,463.9a9,9,0,0,1,9-9h11.4a9,9,0,0,1,9,9v11.4a9,9,0,0,1-9,9H422.8a9,9,0,0,1-9-9Z"/><path class="cls-2" d="M413.8,400.4a9,9,0,0,1,9-9h11.4a9,9,0,0,1,9,9v10.2a9,9,0,0,1-9,9H422.8a9,9,0,0,1-9-9Z"/><path class="cls-2" d="M382,528.6a9,9,0,0,1,9-9h11.4a9,9,0,0,1,9,9V540a9,9,0,0,1-9,9H391a9,9,0,0,1-9-9Z"/><path class="cls-2" d="M382,496.8a9,9,0,0,1,9-9h11.4a9,9,0,0,1,9,9V507a9,9,0,0,1-9,9H391a9,9,0,0,1-9-9Z"/><path class="cls-2" d="M382,463.9a9,9,0,0,1,9-9h11.4a9,9,0,0,1,9,9v11.4a9,9,0,0,1-9,9H391a9,9,0,0,1-9-9Z"/><path class="cls-2" d="M382,432.1a9,9,0,0,1,9-9h11.4a9,9,0,0,1,9,9v11.4a9,9,0,0,1-9,9H391a9,9,0,0,1-9-9Z"/><path class="cls-2" d="M382,400.4a9,9,0,0,1,9-9h11.4a9,9,0,0,1,9,9v10.2a9,9,0,0,1-9,9H391a9,9,0,0,1-9-9Z"/><path class="cls-3" d="M550,203a9,9,0,0,1,9-9h11a9,9,0,0,1,9,9v11a9,9,0,0,1-9,9H559a9,9,0,0,1-9-9Z"/><path class="cls-3" d="M550,170a9,9,0,0,1,9-9h11a9,9,0,0,1,9,9v11a9,9,0,0,1-9,9H559a9,9,0,0,1-9-9Z"/><path class="cls-3" d="M550,137a9,9,0,0,1,9-9h11a9,9,0,0,1,9,9v11a9,9,0,0,1-9,9H559a9,9,0,0,1-9-9Z"/><path class="cls-3" d="M550,71a9,9,0,0,1,9-9h11a9,9,0,0,1,9,9V82a9,9,0,0,1-9,9H559a9,9,0,0,1-9-9Z"/><path class="cls-3" d="M519,203a9,9,0,0,1,9-9h10a9,9,0,0,1,9,9v11a9,9,0,0,1-9,9H528a9,9,0,0,1-9-9Z"/><path class="cls-3" d="M519,137a9,9,0,0,1,9-9h10a9,9,0,0,1,9,9v11a9,9,0,0,1-9,9H528a9,9,0,0,1-9-9Z"/><path class="cls-3" d="M519,71a9,9,0,0,1,9-9h10a9,9,0,0,1,9,9V82a9,9,0,0,1-9,9H528a9,9,0,0,1-9-9Z"/><path class="cls-3" d="M487,203a9,9,0,0,1,9-9h10a9,9,0,0,1,9,9v11a9,9,0,0,1-9,9H496a9,9,0,0,1-9-9Z"/><path class="cls-3" d="M487,137a9,9,0,0,1,9-9h10a9,9,0,0,1,9,9v11a9,9,0,0,1-9,9H496a9,9,0,0,1-9-9Z"/><path class="cls-3" d="M487,104a9,9,0,0,1,9-9h10a9,9,0,0,1,9,9v11a9,9,0,0,1-9,9H496a9,9,0,0,1-9-9Z"/><path class="cls-3" d="M487,71a9,9,0,0,1,9-9h10a9,9,0,0,1,9,9V82a9,9,0,0,1-9,9H496a9,9,0,0,1-9-9Z"/><path class="cls-3" d="M446,203a9,9,0,0,1,9-9h11a9,9,0,0,1,9,9v11a9,9,0,0,1-9,9H455a9,9,0,0,1-9-9Z"/><path class="cls-3" d="M446,170a9,9,0,0,1,9-9h11a9,9,0,0,1,9,9v11a9,9,0,0,1-9,9H455a9,9,0,0,1-9-9Z"/><path class="cls-3" d="M446,137a9,9,0,0,1,9-9h11a9,9,0,0,1,9,9v11a9,9,0,0,1-9,9H455a9,9,0,0,1-9-9Z"/><path class="cls-3" d="M446,71a9,9,0,0,1,9-9h11a9,9,0,0,1,9,9V82a9,9,0,0,1-9,9H455a9,9,0,0,1-9-9Z"/><path class="cls-3" d="M414,203a9,9,0,0,1,9-9h11a9,9,0,0,1,9,9v11a9,9,0,0,1-9,9H423a9,9,0,0,1-9-9Z"/><path class="cls-3" d="M414,137a9,9,0,0,1,9-9h11a9,9,0,0,1,9,9v11a9,9,0,0,1-9,9H423a9,9,0,0,1-9-9Z"/><path class="cls-3" d="M414,71a9,9,0,0,1,9-9h11a9,9,0,0,1,9,9V82a9,9,0,0,1-9,9H423a9,9,0,0,1-9-9Z"/><path class="cls-3" d="M382,203a9,9,0,0,1,9-9h11a9,9,0,0,1,9,9v11a9,9,0,0,1-9,9H391a9,9,0,0,1-9-9Z"/><path class="cls-3" d="M382,170a9,9,0,0,1,9-9h11a9,9,0,0,1,9,9v11a9,9,0,0,1-9,9H391a9,9,0,0,1-9-9Z"/><path class="cls-3" d="M382,137a9,9,0,0,1,9-9h11a9,9,0,0,1,9,9v11a9,9,0,0,1-9,9H391a9,9,0,0,1-9-9Z"/><path class="cls-3" d="M446,104a9,9,0,0,1,9-9h11a9,9,0,0,1,9,9v11a9,9,0,0,1-9,9H455a9,9,0,0,1-9-9Z"/><path class="cls-3" d="M382,71a9,9,0,0,1,9-9h11a9,9,0,0,1,9,9V82a9,9,0,0,1-9,9H391a9,9,0,0,1-9-9Z"/><path class="cls-4" d="M550,237a9,9,0,0,1,9-9h11a9,9,0,0,1,9,9v11a9,9,0,0,1-9,9H559a9,9,0,0,1-9-9Z"/><path class="cls-4" d="M519,237a9,9,0,0,1,9-9h10a9,9,0,0,1,9,9v11a9,9,0,0,1-9,9H528a9,9,0,0,1-9-9Z"/><path class="cls-4" d="M487,237a9,9,0,0,1,9-9h10a9,9,0,0,1,9,9v11a9,9,0,0,1-9,9H496a9,9,0,0,1-9-9Z"/><path class="cls-4" d="M382,237a9,9,0,0,1,9-9h11a9,9,0,0,1,9,9v11a9,9,0,0,1-9,9H391a9,9,0,0,1-9-9Z"/><path class="cls-4" d="M550,269a9,9,0,0,1,9-9h11a9,9,0,0,1,9,9v10a9,9,0,0,1-9,9H559a9,9,0,0,1-9-9Z"/><path class="cls-4" d="M487,269a9,9,0,0,1,9-9h10a9,9,0,0,1,9,9v10a9,9,0,0,1-9,9H496a9,9,0,0,1-9-9Z"/><path class="cls-4" d="M550,301a9,9,0,0,1,9-9h11a9,9,0,0,1,9,9v10a9,9,0,0,1-9,9H559a9,9,0,0,1-9-9Z"/><path class="cls-4" d="M519,301a9,9,0,0,1,9-9h10a9,9,0,0,1,9,9v10a9,9,0,0,1-9,9H528a9,9,0,0,1-9-9Z"/><path class="cls-4" d="M487,301a9,9,0,0,1,9-9h10a9,9,0,0,1,9,9v10a9,9,0,0,1-9,9H496a9,9,0,0,1-9-9Z"/><path class="cls-4" d="M382,301a9,9,0,0,1,9-9h11a9,9,0,0,1,9,9v10a9,9,0,0,1-9,9H391a9,9,0,0,1-9-9Z"/><path class="cls-4" d="M487,334a9,9,0,0,1,9-9h10a9,9,0,0,1,9,9v10a9,9,0,0,1-9,9H496a9,9,0,0,1-9-9Z"/><path class="cls-4" d="M382,334a9,9,0,0,1,9-9h11a9,9,0,0,1,9,9v10a9,9,0,0,1-9,9H391a9,9,0,0,1-9-9Z"/><path class="cls-4" d="M550,366a9,9,0,0,1,9-9h11a9,9,0,0,1,9,9v10a9,9,0,0,1-9,9H559a9,9,0,0,1-9-9Z"/><path class="cls-4" d="M519,366a9,9,0,0,1,9-9h10a9,9,0,0,1,9,9v10a9,9,0,0,1-9,9H528a9,9,0,0,1-9-9Z"/><path class="cls-4" d="M487,366a9,9,0,0,1,9-9h10a9,9,0,0,1,9,9v10a9,9,0,0,1-9,9H496a9,9,0,0,1-9-9Z"/><path class="cls-4" d="M382,366a9,9,0,0,1,9-9h11a9,9,0,0,1,9,9v10a9,9,0,0,1-9,9H391a9,9,0,0,1-9-9Z"/><path class="cls-2" d="M208,98a20.06,20.06,0,0,1,20-20h31a20.06,20.06,0,0,1,20,20v31a20.06,20.06,0,0,1-20,20H228a20.06,20.06,0,0,1-20-20Z"/><path class="cls-2" d="M129,98a20.06,20.06,0,0,1,20-20h31a20.06,20.06,0,0,1,20,20v31a20.06,20.06,0,0,1-20,20H149a20.06,20.06,0,0,1-20-20Z"/><path class="cls-2" d="M50,98A20.06,20.06,0,0,1,70,78h31a20.06,20.06,0,0,1,20,20v31a20.06,20.06,0,0,1-20,20H70a20.06,20.06,0,0,1-20-20Z"/><path class="cls-2" d="M287,178a20.06,20.06,0,0,1,20-20h31a20.06,20.06,0,0,1,20,20v31a20.06,20.06,0,0,1-20,20H307a20.06,20.06,0,0,1-20-20Z"/><path class="cls-2" d="M50,178a20.06,20.06,0,0,1,20-20h31a20.06,20.06,0,0,1,20,20v31a20.06,20.06,0,0,1-20,20H70a20.06,20.06,0,0,1-20-20Z"/><path class="cls-2" d="M287,258a20.06,20.06,0,0,1,20-20h31a20.06,20.06,0,0,1,20,20v31a20.06,20.06,0,0,1-20,20H307a20.06,20.06,0,0,1-20-20Z"/><path class="cls-2" d="M50,258a20.06,20.06,0,0,1,20-20h31a20.06,20.06,0,0,1,20,20v31a20.06,20.06,0,0,1-20,20H70a20.06,20.06,0,0,1-20-20Z"/><path class="cls-2" d="M287,338a20.06,20.06,0,0,1,20-20h31a20.06,20.06,0,0,1,20,20v31a20.06,20.06,0,0,1-20,20H307a20.06,20.06,0,0,1-20-20Z"/><path class="cls-2" d="M50,338a20.06,20.06,0,0,1,20-20h31a20.06,20.06,0,0,1,20,20v31a20.06,20.06,0,0,1-20,20H70a20.06,20.06,0,0,1-20-20Z"/><path class="cls-2" d="M287,418a20.06,20.06,0,0,1,20-20h31a20.06,20.06,0,0,1,20,20v31a20.06,20.06,0,0,1-20,20H307a20.06,20.06,0,0,1-20-20Z"/><path class="cls-2" d="M50,418a20.06,20.06,0,0,1,20-20h31a20.06,20.06,0,0,1,20,20v31a20.06,20.06,0,0,1-20,20H70a20.06,20.06,0,0,1-20-20Z"/><path class="cls-2" d="M208,498a20.06,20.06,0,0,1,20-20h31a20.06,20.06,0,0,1,20,20v31a20.06,20.06,0,0,1-20,20H228a20.06,20.06,0,0,1-20-20Z"/><path class="cls-2" d="M129,498a20.06,20.06,0,0,1,20-20h31a20.06,20.06,0,0,1,20,20v31a20.06,20.06,0,0,1-20,20H149a20.06,20.06,0,0,1-20-20Z"/><path class="cls-2" d="M50,498a20.06,20.06,0,0,1,20-20h31a20.06,20.06,0,0,1,20,20v31a20.06,20.06,0,0,1-20,20H70a20.06,20.06,0,0,1-20-20Z"/><path class="cls-4" d="M214,90a20.06,20.06,0,0,1,20-20h31a20.06,20.06,0,0,1,20,20v31a20.06,20.06,0,0,1-20,20H234a20.06,20.06,0,0,1-20-20Z"/><path class="cls-4" d="M135,90a20.06,20.06,0,0,1,20-20h32a20.06,20.06,0,0,1,20,20v31a20.06,20.06,0,0,1-20,20H155a20.06,20.06,0,0,1-20-20Z"/><path class="cls-4" d="M57,90A20.06,20.06,0,0,1,77,70h30a20.06,20.06,0,0,1,20,20v31a20.06,20.06,0,0,1-20,20H77a20.06,20.06,0,0,1-20-20Z"/><path class="cls-4" d="M292,169a20.06,20.06,0,0,1,20-20h31a20.06,20.06,0,0,1,20,20v32a20.06,20.06,0,0,1-20,20H312a20.06,20.06,0,0,1-20-20Z"/><path class="cls-4" d="M57,169a20.06,20.06,0,0,1,20-20h30a20.06,20.06,0,0,1,20,20v32a20.06,20.06,0,0,1-20,20H77a20.06,20.06,0,0,1-20-20Z"/><path class="cls-4" d="M292,249a20.06,20.06,0,0,1,20-20h31a20.06,20.06,0,0,1,20,20v31a20.06,20.06,0,0,1-20,20H312a20.06,20.06,0,0,1-20-20Z"/><path class="cls-4" d="M57,249a20.06,20.06,0,0,1,20-20h30a20.06,20.06,0,0,1,20,20v31a20.06,20.06,0,0,1-20,20H77a20.06,20.06,0,0,1-20-20Z"/><path class="cls-4" d="M292,329a20.06,20.06,0,0,1,20-20h31a20.06,20.06,0,0,1,20,20v31a20.06,20.06,0,0,1-20,20H312a20.06,20.06,0,0,1-20-20Z"/><path class="cls-4" d="M57,329a20.06,20.06,0,0,1,20-20h30a20.06,20.06,0,0,1,20,20v31a20.06,20.06,0,0,1-20,20H77a20.06,20.06,0,0,1-20-20Z"/><path class="cls-4" d="M292,408a20.06,20.06,0,0,1,20-20h31a20.06,20.06,0,0,1,20,20v32a20.06,20.06,0,0,1-20,20H312a20.06,20.06,0,0,1-20-20Z"/><path class="cls-4" d="M57,408a20.06,20.06,0,0,1,20-20h30a20.06,20.06,0,0,1,20,20v32a20.06,20.06,0,0,1-20,20H77a20.06,20.06,0,0,1-20-20Z"/><path class="cls-4" d="M214,488a20.06,20.06,0,0,1,20-20h31a20.06,20.06,0,0,1,20,20v31a20.06,20.06,0,0,1-20,20H234a20.06,20.06,0,0,1-20-20Z"/><path class="cls-4" d="M135,488a20.06,20.06,0,0,1,20-20h32a20.06,20.06,0,0,1,20,20v31a20.06,20.06,0,0,1-20,20H155a20.06,20.06,0,0,1-20-20Z"/><path class="cls-4" d="M57,488a20.06,20.06,0,0,1,20-20h30a20.06,20.06,0,0,1,20,20v31a20.06,20.06,0,0,1-20,20H77a20.06,20.06,0,0,1-20-20Z"/><path class="cls-3" d="M221,82a20.06,20.06,0,0,1,20-20h30a20.06,20.06,0,0,1,20,20v31a20.06,20.06,0,0,1-20,20H241a20.06,20.06,0,0,1-20-20Z"/><path class="cls-3" d="M142,82a20.06,20.06,0,0,1,20-20h31a20.06,20.06,0,0,1,20,20v31a20.06,20.06,0,0,1-20,20H162a20.06,20.06,0,0,1-20-20Z"/><path class="cls-3" d="M65,82A20.06,20.06,0,0,1,85,62h30a20.06,20.06,0,0,1,20,20v31a20.06,20.06,0,0,1-20,20H85a20.06,20.06,0,0,1-20-20Z"/><path class="cls-3" d="M298,161a20.06,20.06,0,0,1,20-20h30a20.06,20.06,0,0,1,20,20v31a20.06,20.06,0,0,1-20,20H318a20.06,20.06,0,0,1-20-20Z"/><path class="cls-3" d="M65,161a20.06,20.06,0,0,1,20-20h30a20.06,20.06,0,0,1,20,20v31a20.06,20.06,0,0,1-20,20H85a20.06,20.06,0,0,1-20-20Z"/><path class="cls-3" d="M298,240a20.06,20.06,0,0,1,20-20h30a20.06,20.06,0,0,1,20,20v30a20.06,20.06,0,0,1-20,20H318a20.06,20.06,0,0,1-20-20Z"/><path class="cls-3" d="M65,240a20.06,20.06,0,0,1,20-20h30a20.06,20.06,0,0,1,20,20v30a20.06,20.06,0,0,1-20,20H85a20.06,20.06,0,0,1-20-20Z"/><path class="cls-3" d="M298,318a20.06,20.06,0,0,1,20-20h30a20.06,20.06,0,0,1,20,20v30a20.06,20.06,0,0,1-20,20H318a20.06,20.06,0,0,1-20-20Z"/><path class="cls-3" d="M65,318a20.06,20.06,0,0,1,20-20h30a20.06,20.06,0,0,1,20,20v30a20.06,20.06,0,0,1-20,20H85a20.06,20.06,0,0,1-20-20Z"/><path class="cls-3" d="M298,397a20.06,20.06,0,0,1,20-20h30a20.06,20.06,0,0,1,20,20v31a20.06,20.06,0,0,1-20,20H318a20.06,20.06,0,0,1-20-20Z"/><path class="cls-3" d="M65,397a20.06,20.06,0,0,1,20-20h30a20.06,20.06,0,0,1,20,20v31a20.06,20.06,0,0,1-20,20H85a20.06,20.06,0,0,1-20-20Z"/><path class="cls-3" d="M221,476a20.06,20.06,0,0,1,20-20h30a20.06,20.06,0,0,1,20,20v30a20.06,20.06,0,0,1-20,20H241a20.06,20.06,0,0,1-20-20Z"/><path class="cls-3" d="M142,476a20.06,20.06,0,0,1,20-20h31a20.06,20.06,0,0,1,20,20v30a20.06,20.06,0,0,1-20,20H162a20.06,20.06,0,0,1-20-20Z"/><path class="cls-3" d="M65,476a20.06,20.06,0,0,1,20-20h30a20.06,20.06,0,0,1,20,20v30a20.06,20.06,0,0,1-20,20H85a20.06,20.06,0,0,1-20-20Z"/></g></g></svg>
\ No newline at end of file

A shell.nix => shell.nix +13 -0
@@ 0,0 1,13 @@
with import <nixpkgs> {
  crossSystem = {
    config = "x86_64-w64-mingw32";
  };
}; let
  pkgs = import <nixpkgs> {crossSystem = {config = "x86_64-w64-mingw32";};};
in
  pkgs.mkShell
  {
    buildInputs = [
      windows.mingw_w64_pthreads
    ];
  }

M src/attempt.rs => src/attempt.rs +1 -1
@@ 16,7 16,7 @@ pub enum Conclusion {
    WrongArticle(Article),
}

pub trait Repo: Clone {
pub trait Repo: Clone + Send + Sync + std::fmt::Debug {
    /// Saves an attempt
    ///
    /// # Errors

M src/bin/ddd/main.rs => src/bin/ddd/main.rs +0 -3
@@ 15,9 15,6 @@ use tracing_subscriber::{fmt, layer::SubscriberExt, util::SubscriberInitExt, Env
fn main() -> Result<()> {
    trace_install()?;

    // // // error message management
    // color_eyre::install()?;

    let config_path = dirs::config_dir()
        .ok_or_eyre("could not file config folder!")?
        .join("ddd")

A src/bin/homepage/arguments.rs => src/bin/homepage/arguments.rs +0 -0
A src/bin/homepage/commands.rs => src/bin/homepage/commands.rs +0 -0
A src/bin/homepage/config.rs => src/bin/homepage/config.rs +166 -0
@@ 0,0 1,166 @@
use std::path::PathBuf;

use color_eyre::eyre::OptionExt;
use figment::{
    providers::{Env, Format, Json},
    Figment,
};

#[derive(
    Clone, Debug, PartialEq, Eq, Hash, PartialOrd, Ord, serde::Serialize, serde::Deserialize,
)]
pub struct GroupConfigFile {
    pub threshold: Option<u8>,
    pub enable: Option<bool>,
}
impl Default for GroupConfigFile {
    fn default() -> Self {
        Self {
            threshold: Some(100),
            enable: Some(true),
        }
    }
}

#[derive(
    Clone, Debug, PartialEq, Eq, Hash, PartialOrd, Ord, serde::Serialize, serde::Deserialize,
)]
pub struct WebConfigFile {
    pub host: Option<String>,
    pub port: Option<u16>,
}
impl Default for WebConfigFile {
    fn default() -> Self {
        Self {
            host: Some("127.0.0.1".to_owned()),
            port: Some(4949),
        }
    }
}

#[derive(
    Clone,
    Debug,
    PartialEq,
    Eq,
    Hash,
    PartialOrd,
    Ord,
    serde::Serialize,
    serde::Deserialize,
    Default,
)]
pub struct ConfigurationFile {
    pub data_path: Option<PathBuf>,
    pub group: Option<GroupConfigFile>,
    pub web: Option<WebConfigFile>,
}

impl TryFrom<PathBuf> for ConfigurationFile {
    type Error = color_eyre::Report;

    fn try_from(value: PathBuf) -> Result<Self, Self::Error> {
        Ok(Figment::new()
            .merge(Json::file(value))
            .merge(Env::prefixed("DDD_"))
            .extract()?)
    }
}

impl TryFrom<&PathBuf> for ConfigurationFile {
    type Error = color_eyre::Report;

    fn try_from(value: &PathBuf) -> Result<Self, Self::Error> {
        Ok(Figment::new()
            .merge(Json::file(value))
            .merge(Env::prefixed("DDD_"))
            .extract()?)
    }
}

#[derive(
    Clone, Debug, PartialEq, Eq, Hash, PartialOrd, Ord, serde::Serialize, serde::Deserialize,
)]
pub struct GroupConfiguration {
    pub threshold: u8,
    pub enable: bool,
}
impl Default for GroupConfiguration {
    fn default() -> Self {
        Self {
            threshold: 100,
            enable: true,
        }
    }
}

#[derive(
    Clone, Debug, PartialEq, Eq, Hash, PartialOrd, Ord, serde::Serialize, serde::Deserialize,
)]
pub struct WebConfiguration {
    pub host: String,
    pub port: u16,
}
impl Default for WebConfiguration {
    fn default() -> Self {
        Self {
            host: "127.0.0.1".to_owned(),
            port: 4949,
        }
    }
}
impl From<WebConfigFile> for WebConfiguration {
    fn from(value: WebConfigFile) -> Self {
        Self {
            host: value.host.unwrap_or("127.0.0.1".to_owned()),
            port: 4949,
        }
    }
}

impl From<GroupConfigFile> for GroupConfiguration {
    fn from(value: GroupConfigFile) -> Self {
        Self {
            threshold: value.threshold.unwrap_or(100),
            enable: value.enable.unwrap_or(true),
        }
    }
}

#[derive(
    Clone, Debug, PartialEq, Eq, Hash, PartialOrd, Ord, serde::Serialize, serde::Deserialize,
)]
pub struct Config {
    pub data_path: PathBuf,
    pub group: GroupConfiguration,
    pub web: WebConfiguration,
}

impl TryFrom<ConfigurationFile> for Config {
    type Error = color_eyre::Report;

    fn try_from(value: ConfigurationFile) -> color_eyre::Result<Self> {
        let group = value.group.unwrap_or_default().into();
        let web = value.web.unwrap_or_default().into();

        if let Some(data_path) = value.data_path {
            return Ok(Config {
                data_path,
                group,
                web,
            });
        };
        let p = dirs::data_dir()
            .ok_or_eyre("Could not find any folder to put the data in.")?
            .join("ddd");
        if !p.try_exists()? {
            fs_extra::dir::create_all(&p, false)?;
        };

        Ok(Config {
            group,
            data_path: p,
            web,
        })
    }
}

A src/bin/homepage/main.rs => src/bin/homepage/main.rs +93 -0
@@ 0,0 1,93 @@
#![cfg(feature = "homepage")]

mod arguments;
mod commands;
mod config;
mod web;

use std::{env, sync::Arc};

use axum::{routing::get, Router};
use color_eyre::{eyre::OptionExt as _, Result};
use tracing::{debug, info, level_filters::LevelFilter};
use tracing_error::ErrorLayer;
use tracing_subscriber::{fmt, layer::SubscriberExt as _, util::SubscriberInitExt as _, EnvFilter};

#[tokio::main(flavor = "current_thread")]
async fn main() -> Result<()> {
    trace_install()?;

    let config_path = dirs::config_dir()
        .ok_or_eyre("could not file config folder!")?
        .join("ddd")
        .join("config.json");

    debug!("the config path is at: {:#?}", &config_path);

    let configs_file = config::ConfigurationFile::try_from(config_path)?;

    debug!("the configuration file is: {:#?}", &configs_file);
    let configs = config::Config::try_from(configs_file)?;

    debug!("the configuration is: {:#?}", &configs);

    let storage = Arc::new(der_die_das::storage::Storage::try_from(configs.data_path)?);

    let app_state = Arc::new(web::AppState {
        nouns_repo: Arc::clone(&storage),
        attempts_repo: Arc::clone(&storage),
        group_config: configs.group,
    });

    let addr = format!("{}:{}", configs.web.host, configs.web.port);
    let listener = tokio::net::TcpListener::bind(&addr).await?;

    let router = Router::new()
        .route("/", get(web::routes::home))
        .route("/send-answer/:word/:attempt", get(web::routes::answer))
        .route("/style.css", get(web::routes::style))
        .route("/logo.svg", get(web::routes::logo))
        .route("/favicon.ico", get(web::routes::logo))
        .with_state(Arc::clone(&app_state));

    info!("running at: {}", addr);
    axum::serve(listener, router).await?;

    Ok(())
}

fn trace_install() -> Result<()> {
    let fmt_layer = fmt::layer().with_target(false);

    let (filter_layer, current_filter) = match EnvFilter::try_from_default_env() {
        Err(_) => (EnvFilter::try_new("warn").unwrap(), LevelFilter::OFF),
        Ok(layer) => {
            let level = layer.max_level_hint().unwrap_or(LevelFilter::OFF);
            (layer, level)
        }
    };

    let registry = tracing_subscriber::registry()
        .with(filter_layer)
        .with(ErrorLayer::default());

    if current_filter == LevelFilter::OFF {
        registry.with(fmt_layer.without_time()).init();
    } else {
        registry.with(fmt_layer).init();
    }

    if env::var("RUST_BACKTRACE").is_err() && current_filter == LevelFilter::TRACE {
        env::set_var("RUST_BACKTRACE", "1");
    }

    let debug = current_filter >= LevelFilter::DEBUG;

    color_eyre::config::HookBuilder::new()
        .capture_span_trace_by_default(debug)
        .display_location_section(debug)
        .display_env_section(false)
        .install()?;

    Ok(())
}

A src/bin/homepage/web.rs => src/bin/homepage/web.rs +11 -0
@@ 0,0 1,11 @@
use der_die_das::{attempt, nouns};

use crate::config::GroupConfiguration;
pub(crate) mod routes;

#[derive(Debug)]
pub struct AppState<A: attempt::Repo, N: nouns::Repo> {
    pub nouns_repo: N,
    pub attempts_repo: A,
    pub group_config: GroupConfiguration,
}

A src/bin/homepage/web/logo.svg => src/bin/homepage/web/logo.svg +1 -0
@@ 0,0 1,1 @@
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 640 640"><defs><style>.cls-1{fill:#dde9d4;}.cls-2{fill:#8b2a01;}.cls-3{fill:#ff7e47;}.cls-4{fill:#df581f;}</style></defs><g id="Layer_2" data-name="Layer 2"><g id="Layer_1-2" data-name="Layer 1"><path class="cls-1" d="M0,124A124,124,0,0,1,124,0H516A124,124,0,0,1,640,124V516A124,124,0,0,1,516,640H124A124,124,0,0,1,0,516Z"/><path class="cls-2" d="M549.3,400a9,9,0,0,1,9-9h11.4a9,9,0,0,1,9,9v10.2a9,9,0,0,1-9,9H558.3a9,9,0,0,1-9-9Z"/><path class="cls-2" d="M518.8,431.8a9,9,0,0,1,9-9H538a9,9,0,0,1,9,9v11.4a9,9,0,0,1-9,9H527.8a9,9,0,0,1-9-9Z"/><path class="cls-2" d="M487,528.2a9,9,0,0,1,9-9h10.2a9,9,0,0,1,9,9v11.4a9,9,0,0,1-9,9H496a9,9,0,0,1-9-9Z"/><path class="cls-2" d="M487,496.5a9,9,0,0,1,9-9h10.2a9,9,0,0,1,9,9v10.2a9,9,0,0,1-9,9H496a9,9,0,0,1-9-9Z"/><path class="cls-2" d="M487,463.5a9,9,0,0,1,9-9h10.2a9,9,0,0,1,9,9v11.4a9,9,0,0,1-9,9H496a9,9,0,0,1-9-9Z"/><path class="cls-2" d="M487,431.8a9,9,0,0,1,9-9h10.2a9,9,0,0,1,9,9v11.4a9,9,0,0,1-9,9H496a9,9,0,0,1-9-9Z"/><path class="cls-2" d="M487,400a9,9,0,0,1,9-9h10.2a9,9,0,0,1,9,9v10.2a9,9,0,0,1-9,9H496a9,9,0,0,1-9-9Z"/><path class="cls-2" d="M445.5,528.6a9,9,0,0,1,9-9h11.4a9,9,0,0,1,9,9V540a9,9,0,0,1-9,9H454.5a9,9,0,0,1-9-9Z"/><path class="cls-2" d="M445.5,463.9a9,9,0,0,1,9-9h11.4a9,9,0,0,1,9,9v11.4a9,9,0,0,1-9,9H454.5a9,9,0,0,1-9-9Z"/><path class="cls-2" d="M445.5,432.1a9,9,0,0,1,9-9h11.4a9,9,0,0,1,9,9v11.4a9,9,0,0,1-9,9H454.5a9,9,0,0,1-9-9Z"/><path class="cls-2" d="M445.5,400.4a9,9,0,0,1,9-9h11.4a9,9,0,0,1,9,9v10.2a9,9,0,0,1-9,9H454.5a9,9,0,0,1-9-9Z"/><path class="cls-2" d="M413.8,528.6a9,9,0,0,1,9-9h11.4a9,9,0,0,1,9,9V540a9,9,0,0,1-9,9H422.8a9,9,0,0,1-9-9Z"/><path class="cls-2" d="M413.8,463.9a9,9,0,0,1,9-9h11.4a9,9,0,0,1,9,9v11.4a9,9,0,0,1-9,9H422.8a9,9,0,0,1-9-9Z"/><path class="cls-2" d="M413.8,400.4a9,9,0,0,1,9-9h11.4a9,9,0,0,1,9,9v10.2a9,9,0,0,1-9,9H422.8a9,9,0,0,1-9-9Z"/><path class="cls-2" d="M382,528.6a9,9,0,0,1,9-9h11.4a9,9,0,0,1,9,9V540a9,9,0,0,1-9,9H391a9,9,0,0,1-9-9Z"/><path class="cls-2" d="M382,496.8a9,9,0,0,1,9-9h11.4a9,9,0,0,1,9,9V507a9,9,0,0,1-9,9H391a9,9,0,0,1-9-9Z"/><path class="cls-2" d="M382,463.9a9,9,0,0,1,9-9h11.4a9,9,0,0,1,9,9v11.4a9,9,0,0,1-9,9H391a9,9,0,0,1-9-9Z"/><path class="cls-2" d="M382,432.1a9,9,0,0,1,9-9h11.4a9,9,0,0,1,9,9v11.4a9,9,0,0,1-9,9H391a9,9,0,0,1-9-9Z"/><path class="cls-2" d="M382,400.4a9,9,0,0,1,9-9h11.4a9,9,0,0,1,9,9v10.2a9,9,0,0,1-9,9H391a9,9,0,0,1-9-9Z"/><path class="cls-3" d="M550,203a9,9,0,0,1,9-9h11a9,9,0,0,1,9,9v11a9,9,0,0,1-9,9H559a9,9,0,0,1-9-9Z"/><path class="cls-3" d="M550,170a9,9,0,0,1,9-9h11a9,9,0,0,1,9,9v11a9,9,0,0,1-9,9H559a9,9,0,0,1-9-9Z"/><path class="cls-3" d="M550,137a9,9,0,0,1,9-9h11a9,9,0,0,1,9,9v11a9,9,0,0,1-9,9H559a9,9,0,0,1-9-9Z"/><path class="cls-3" d="M550,71a9,9,0,0,1,9-9h11a9,9,0,0,1,9,9V82a9,9,0,0,1-9,9H559a9,9,0,0,1-9-9Z"/><path class="cls-3" d="M519,203a9,9,0,0,1,9-9h10a9,9,0,0,1,9,9v11a9,9,0,0,1-9,9H528a9,9,0,0,1-9-9Z"/><path class="cls-3" d="M519,137a9,9,0,0,1,9-9h10a9,9,0,0,1,9,9v11a9,9,0,0,1-9,9H528a9,9,0,0,1-9-9Z"/><path class="cls-3" d="M519,71a9,9,0,0,1,9-9h10a9,9,0,0,1,9,9V82a9,9,0,0,1-9,9H528a9,9,0,0,1-9-9Z"/><path class="cls-3" d="M487,203a9,9,0,0,1,9-9h10a9,9,0,0,1,9,9v11a9,9,0,0,1-9,9H496a9,9,0,0,1-9-9Z"/><path class="cls-3" d="M487,137a9,9,0,0,1,9-9h10a9,9,0,0,1,9,9v11a9,9,0,0,1-9,9H496a9,9,0,0,1-9-9Z"/><path class="cls-3" d="M487,104a9,9,0,0,1,9-9h10a9,9,0,0,1,9,9v11a9,9,0,0,1-9,9H496a9,9,0,0,1-9-9Z"/><path class="cls-3" d="M487,71a9,9,0,0,1,9-9h10a9,9,0,0,1,9,9V82a9,9,0,0,1-9,9H496a9,9,0,0,1-9-9Z"/><path class="cls-3" d="M446,203a9,9,0,0,1,9-9h11a9,9,0,0,1,9,9v11a9,9,0,0,1-9,9H455a9,9,0,0,1-9-9Z"/><path class="cls-3" d="M446,170a9,9,0,0,1,9-9h11a9,9,0,0,1,9,9v11a9,9,0,0,1-9,9H455a9,9,0,0,1-9-9Z"/><path class="cls-3" d="M446,137a9,9,0,0,1,9-9h11a9,9,0,0,1,9,9v11a9,9,0,0,1-9,9H455a9,9,0,0,1-9-9Z"/><path class="cls-3" d="M446,71a9,9,0,0,1,9-9h11a9,9,0,0,1,9,9V82a9,9,0,0,1-9,9H455a9,9,0,0,1-9-9Z"/><path class="cls-3" d="M414,203a9,9,0,0,1,9-9h11a9,9,0,0,1,9,9v11a9,9,0,0,1-9,9H423a9,9,0,0,1-9-9Z"/><path class="cls-3" d="M414,137a9,9,0,0,1,9-9h11a9,9,0,0,1,9,9v11a9,9,0,0,1-9,9H423a9,9,0,0,1-9-9Z"/><path class="cls-3" d="M414,71a9,9,0,0,1,9-9h11a9,9,0,0,1,9,9V82a9,9,0,0,1-9,9H423a9,9,0,0,1-9-9Z"/><path class="cls-3" d="M382,203a9,9,0,0,1,9-9h11a9,9,0,0,1,9,9v11a9,9,0,0,1-9,9H391a9,9,0,0,1-9-9Z"/><path class="cls-3" d="M382,170a9,9,0,0,1,9-9h11a9,9,0,0,1,9,9v11a9,9,0,0,1-9,9H391a9,9,0,0,1-9-9Z"/><path class="cls-3" d="M382,137a9,9,0,0,1,9-9h11a9,9,0,0,1,9,9v11a9,9,0,0,1-9,9H391a9,9,0,0,1-9-9Z"/><path class="cls-3" d="M446,104a9,9,0,0,1,9-9h11a9,9,0,0,1,9,9v11a9,9,0,0,1-9,9H455a9,9,0,0,1-9-9Z"/><path class="cls-3" d="M382,71a9,9,0,0,1,9-9h11a9,9,0,0,1,9,9V82a9,9,0,0,1-9,9H391a9,9,0,0,1-9-9Z"/><path class="cls-4" d="M550,237a9,9,0,0,1,9-9h11a9,9,0,0,1,9,9v11a9,9,0,0,1-9,9H559a9,9,0,0,1-9-9Z"/><path class="cls-4" d="M519,237a9,9,0,0,1,9-9h10a9,9,0,0,1,9,9v11a9,9,0,0,1-9,9H528a9,9,0,0,1-9-9Z"/><path class="cls-4" d="M487,237a9,9,0,0,1,9-9h10a9,9,0,0,1,9,9v11a9,9,0,0,1-9,9H496a9,9,0,0,1-9-9Z"/><path class="cls-4" d="M382,237a9,9,0,0,1,9-9h11a9,9,0,0,1,9,9v11a9,9,0,0,1-9,9H391a9,9,0,0,1-9-9Z"/><path class="cls-4" d="M550,269a9,9,0,0,1,9-9h11a9,9,0,0,1,9,9v10a9,9,0,0,1-9,9H559a9,9,0,0,1-9-9Z"/><path class="cls-4" d="M487,269a9,9,0,0,1,9-9h10a9,9,0,0,1,9,9v10a9,9,0,0,1-9,9H496a9,9,0,0,1-9-9Z"/><path class="cls-4" d="M550,301a9,9,0,0,1,9-9h11a9,9,0,0,1,9,9v10a9,9,0,0,1-9,9H559a9,9,0,0,1-9-9Z"/><path class="cls-4" d="M519,301a9,9,0,0,1,9-9h10a9,9,0,0,1,9,9v10a9,9,0,0,1-9,9H528a9,9,0,0,1-9-9Z"/><path class="cls-4" d="M487,301a9,9,0,0,1,9-9h10a9,9,0,0,1,9,9v10a9,9,0,0,1-9,9H496a9,9,0,0,1-9-9Z"/><path class="cls-4" d="M382,301a9,9,0,0,1,9-9h11a9,9,0,0,1,9,9v10a9,9,0,0,1-9,9H391a9,9,0,0,1-9-9Z"/><path class="cls-4" d="M487,334a9,9,0,0,1,9-9h10a9,9,0,0,1,9,9v10a9,9,0,0,1-9,9H496a9,9,0,0,1-9-9Z"/><path class="cls-4" d="M382,334a9,9,0,0,1,9-9h11a9,9,0,0,1,9,9v10a9,9,0,0,1-9,9H391a9,9,0,0,1-9-9Z"/><path class="cls-4" d="M550,366a9,9,0,0,1,9-9h11a9,9,0,0,1,9,9v10a9,9,0,0,1-9,9H559a9,9,0,0,1-9-9Z"/><path class="cls-4" d="M519,366a9,9,0,0,1,9-9h10a9,9,0,0,1,9,9v10a9,9,0,0,1-9,9H528a9,9,0,0,1-9-9Z"/><path class="cls-4" d="M487,366a9,9,0,0,1,9-9h10a9,9,0,0,1,9,9v10a9,9,0,0,1-9,9H496a9,9,0,0,1-9-9Z"/><path class="cls-4" d="M382,366a9,9,0,0,1,9-9h11a9,9,0,0,1,9,9v10a9,9,0,0,1-9,9H391a9,9,0,0,1-9-9Z"/><path class="cls-2" d="M208,98a20.06,20.06,0,0,1,20-20h31a20.06,20.06,0,0,1,20,20v31a20.06,20.06,0,0,1-20,20H228a20.06,20.06,0,0,1-20-20Z"/><path class="cls-2" d="M129,98a20.06,20.06,0,0,1,20-20h31a20.06,20.06,0,0,1,20,20v31a20.06,20.06,0,0,1-20,20H149a20.06,20.06,0,0,1-20-20Z"/><path class="cls-2" d="M50,98A20.06,20.06,0,0,1,70,78h31a20.06,20.06,0,0,1,20,20v31a20.06,20.06,0,0,1-20,20H70a20.06,20.06,0,0,1-20-20Z"/><path class="cls-2" d="M287,178a20.06,20.06,0,0,1,20-20h31a20.06,20.06,0,0,1,20,20v31a20.06,20.06,0,0,1-20,20H307a20.06,20.06,0,0,1-20-20Z"/><path class="cls-2" d="M50,178a20.06,20.06,0,0,1,20-20h31a20.06,20.06,0,0,1,20,20v31a20.06,20.06,0,0,1-20,20H70a20.06,20.06,0,0,1-20-20Z"/><path class="cls-2" d="M287,258a20.06,20.06,0,0,1,20-20h31a20.06,20.06,0,0,1,20,20v31a20.06,20.06,0,0,1-20,20H307a20.06,20.06,0,0,1-20-20Z"/><path class="cls-2" d="M50,258a20.06,20.06,0,0,1,20-20h31a20.06,20.06,0,0,1,20,20v31a20.06,20.06,0,0,1-20,20H70a20.06,20.06,0,0,1-20-20Z"/><path class="cls-2" d="M287,338a20.06,20.06,0,0,1,20-20h31a20.06,20.06,0,0,1,20,20v31a20.06,20.06,0,0,1-20,20H307a20.06,20.06,0,0,1-20-20Z"/><path class="cls-2" d="M50,338a20.06,20.06,0,0,1,20-20h31a20.06,20.06,0,0,1,20,20v31a20.06,20.06,0,0,1-20,20H70a20.06,20.06,0,0,1-20-20Z"/><path class="cls-2" d="M287,418a20.06,20.06,0,0,1,20-20h31a20.06,20.06,0,0,1,20,20v31a20.06,20.06,0,0,1-20,20H307a20.06,20.06,0,0,1-20-20Z"/><path class="cls-2" d="M50,418a20.06,20.06,0,0,1,20-20h31a20.06,20.06,0,0,1,20,20v31a20.06,20.06,0,0,1-20,20H70a20.06,20.06,0,0,1-20-20Z"/><path class="cls-2" d="M208,498a20.06,20.06,0,0,1,20-20h31a20.06,20.06,0,0,1,20,20v31a20.06,20.06,0,0,1-20,20H228a20.06,20.06,0,0,1-20-20Z"/><path class="cls-2" d="M129,498a20.06,20.06,0,0,1,20-20h31a20.06,20.06,0,0,1,20,20v31a20.06,20.06,0,0,1-20,20H149a20.06,20.06,0,0,1-20-20Z"/><path class="cls-2" d="M50,498a20.06,20.06,0,0,1,20-20h31a20.06,20.06,0,0,1,20,20v31a20.06,20.06,0,0,1-20,20H70a20.06,20.06,0,0,1-20-20Z"/><path class="cls-4" d="M214,90a20.06,20.06,0,0,1,20-20h31a20.06,20.06,0,0,1,20,20v31a20.06,20.06,0,0,1-20,20H234a20.06,20.06,0,0,1-20-20Z"/><path class="cls-4" d="M135,90a20.06,20.06,0,0,1,20-20h32a20.06,20.06,0,0,1,20,20v31a20.06,20.06,0,0,1-20,20H155a20.06,20.06,0,0,1-20-20Z"/><path class="cls-4" d="M57,90A20.06,20.06,0,0,1,77,70h30a20.06,20.06,0,0,1,20,20v31a20.06,20.06,0,0,1-20,20H77a20.06,20.06,0,0,1-20-20Z"/><path class="cls-4" d="M292,169a20.06,20.06,0,0,1,20-20h31a20.06,20.06,0,0,1,20,20v32a20.06,20.06,0,0,1-20,20H312a20.06,20.06,0,0,1-20-20Z"/><path class="cls-4" d="M57,169a20.06,20.06,0,0,1,20-20h30a20.06,20.06,0,0,1,20,20v32a20.06,20.06,0,0,1-20,20H77a20.06,20.06,0,0,1-20-20Z"/><path class="cls-4" d="M292,249a20.06,20.06,0,0,1,20-20h31a20.06,20.06,0,0,1,20,20v31a20.06,20.06,0,0,1-20,20H312a20.06,20.06,0,0,1-20-20Z"/><path class="cls-4" d="M57,249a20.06,20.06,0,0,1,20-20h30a20.06,20.06,0,0,1,20,20v31a20.06,20.06,0,0,1-20,20H77a20.06,20.06,0,0,1-20-20Z"/><path class="cls-4" d="M292,329a20.06,20.06,0,0,1,20-20h31a20.06,20.06,0,0,1,20,20v31a20.06,20.06,0,0,1-20,20H312a20.06,20.06,0,0,1-20-20Z"/><path class="cls-4" d="M57,329a20.06,20.06,0,0,1,20-20h30a20.06,20.06,0,0,1,20,20v31a20.06,20.06,0,0,1-20,20H77a20.06,20.06,0,0,1-20-20Z"/><path class="cls-4" d="M292,408a20.06,20.06,0,0,1,20-20h31a20.06,20.06,0,0,1,20,20v32a20.06,20.06,0,0,1-20,20H312a20.06,20.06,0,0,1-20-20Z"/><path class="cls-4" d="M57,408a20.06,20.06,0,0,1,20-20h30a20.06,20.06,0,0,1,20,20v32a20.06,20.06,0,0,1-20,20H77a20.06,20.06,0,0,1-20-20Z"/><path class="cls-4" d="M214,488a20.06,20.06,0,0,1,20-20h31a20.06,20.06,0,0,1,20,20v31a20.06,20.06,0,0,1-20,20H234a20.06,20.06,0,0,1-20-20Z"/><path class="cls-4" d="M135,488a20.06,20.06,0,0,1,20-20h32a20.06,20.06,0,0,1,20,20v31a20.06,20.06,0,0,1-20,20H155a20.06,20.06,0,0,1-20-20Z"/><path class="cls-4" d="M57,488a20.06,20.06,0,0,1,20-20h30a20.06,20.06,0,0,1,20,20v31a20.06,20.06,0,0,1-20,20H77a20.06,20.06,0,0,1-20-20Z"/><path class="cls-3" d="M221,82a20.06,20.06,0,0,1,20-20h30a20.06,20.06,0,0,1,20,20v31a20.06,20.06,0,0,1-20,20H241a20.06,20.06,0,0,1-20-20Z"/><path class="cls-3" d="M142,82a20.06,20.06,0,0,1,20-20h31a20.06,20.06,0,0,1,20,20v31a20.06,20.06,0,0,1-20,20H162a20.06,20.06,0,0,1-20-20Z"/><path class="cls-3" d="M65,82A20.06,20.06,0,0,1,85,62h30a20.06,20.06,0,0,1,20,20v31a20.06,20.06,0,0,1-20,20H85a20.06,20.06,0,0,1-20-20Z"/><path class="cls-3" d="M298,161a20.06,20.06,0,0,1,20-20h30a20.06,20.06,0,0,1,20,20v31a20.06,20.06,0,0,1-20,20H318a20.06,20.06,0,0,1-20-20Z"/><path class="cls-3" d="M65,161a20.06,20.06,0,0,1,20-20h30a20.06,20.06,0,0,1,20,20v31a20.06,20.06,0,0,1-20,20H85a20.06,20.06,0,0,1-20-20Z"/><path class="cls-3" d="M298,240a20.06,20.06,0,0,1,20-20h30a20.06,20.06,0,0,1,20,20v30a20.06,20.06,0,0,1-20,20H318a20.06,20.06,0,0,1-20-20Z"/><path class="cls-3" d="M65,240a20.06,20.06,0,0,1,20-20h30a20.06,20.06,0,0,1,20,20v30a20.06,20.06,0,0,1-20,20H85a20.06,20.06,0,0,1-20-20Z"/><path class="cls-3" d="M298,318a20.06,20.06,0,0,1,20-20h30a20.06,20.06,0,0,1,20,20v30a20.06,20.06,0,0,1-20,20H318a20.06,20.06,0,0,1-20-20Z"/><path class="cls-3" d="M65,318a20.06,20.06,0,0,1,20-20h30a20.06,20.06,0,0,1,20,20v30a20.06,20.06,0,0,1-20,20H85a20.06,20.06,0,0,1-20-20Z"/><path class="cls-3" d="M298,397a20.06,20.06,0,0,1,20-20h30a20.06,20.06,0,0,1,20,20v31a20.06,20.06,0,0,1-20,20H318a20.06,20.06,0,0,1-20-20Z"/><path class="cls-3" d="M65,397a20.06,20.06,0,0,1,20-20h30a20.06,20.06,0,0,1,20,20v31a20.06,20.06,0,0,1-20,20H85a20.06,20.06,0,0,1-20-20Z"/><path class="cls-3" d="M221,476a20.06,20.06,0,0,1,20-20h30a20.06,20.06,0,0,1,20,20v30a20.06,20.06,0,0,1-20,20H241a20.06,20.06,0,0,1-20-20Z"/><path class="cls-3" d="M142,476a20.06,20.06,0,0,1,20-20h31a20.06,20.06,0,0,1,20,20v30a20.06,20.06,0,0,1-20,20H162a20.06,20.06,0,0,1-20-20Z"/><path class="cls-3" d="M65,476a20.06,20.06,0,0,1,20-20h30a20.06,20.06,0,0,1,20,20v30a20.06,20.06,0,0,1-20,20H85a20.06,20.06,0,0,1-20-20Z"/></g></g></svg>

A src/bin/homepage/web/routes.rs => src/bin/homepage/web/routes.rs +257 -0
@@ 0,0 1,257 @@
use std::sync::Arc;

use axum::{
    extract::{Path, State},
    http::{header, HeaderMap, StatusCode},
    response::IntoResponse,
};
use color_eyre::eyre::OptionExt as _;
use der_die_das::{
    attempt,
    history::History,
    nouns::{self, Article, Noun},
};
use maud::{html, Markup, DOCTYPE};
use tracing::{debug, instrument};

use super::AppState;

pub enum ErrorMessage {
    Internal(color_eyre::Report),
    NoWordsAvailable(color_eyre::Report),
    WordNotFound(color_eyre::Report, i128),
}

fn error_message(input: &str) -> Markup {
    html!(div class="message-error"{
        p class="message" {
           (input)
        }
    })
}

impl IntoResponse for ErrorMessage {
    fn into_response(self) -> axum::response::Response {
        match self {
            ErrorMessage::Internal(e) => {
                (StatusCode::INTERNAL_SERVER_ERROR, e.to_string()).into_response()
            }
            ErrorMessage::NoWordsAvailable(e) => {
                (StatusCode::NOT_FOUND, e.to_string()).into_response()
            }
            ErrorMessage::WordNotFound(e, id) => (
                StatusCode::NOT_FOUND,
                error_message(
                    &e.wrap_err(format!("word with id {id} not found"))
                        .to_string(),
                ),
            )
                .into_response(),
        }
    }
}

#[instrument]
pub async fn style() -> impl IntoResponse {
    let content = include_str!("style.css");

    let mut headers = HeaderMap::new();
    headers.insert(header::CONTENT_TYPE, "text/css".parse().unwrap());
    (headers, content)
}

#[instrument]
pub async fn logo() -> impl IntoResponse {
    let content = include_str!("logo.svg");

    let mut headers = HeaderMap::new();
    headers.insert(header::CONTENT_TYPE, "image/svg+xml".parse().unwrap());
    (headers, content)
}

#[derive(Debug, serde::Deserialize, serde::Serialize)]
pub struct Answer {
    attempt: Article,
    word: i128,
}

#[instrument]
pub async fn answer<A: attempt::Repo, N: nouns::Repo>(
    State(storage): State<Arc<AppState<A, N>>>,
    Path(payload): Path<Answer>,
) -> Result<impl IntoResponse, ErrorMessage> {
    debug!("answer called: {:#?}", payload);
    let noun = storage
        .nouns_repo
        .clone()
        .find_noun_by_id(payload.word)
        .map_err(|e| ErrorMessage::WordNotFound(e, payload.word))?;

    debug!("found noun: {:#?}", noun);
    debug!(
        "here is the result of contains: {}, here is the articles:{:#?}, here is the article: {:#?}",
        noun.articles.contains(&payload.attempt),
        noun.articles,
        payload.attempt,
    );
    if noun.articles.contains(&payload.attempt) {
        debug!("the noun article contains: {:#}", payload.attempt);

        let now = time::OffsetDateTime::now_utc();
        storage
            .attempts_repo
            .clone()
            .save_attempt(attempt::Attempt {
                id: now.unix_timestamp_nanos(),
                at: now,
                for_word: noun.id,
                what_happened: attempt::Conclusion::Success,
            })
            .map_err(ErrorMessage::Internal)?;

        let h = History {
            nouns: storage
                .nouns_repo
                .clone()
                .all_nouns()
                .map_err(ErrorMessage::Internal)?,
            attempts: storage
                .attempts_repo
                .clone()
                .all_attempts()
                .map_err(ErrorMessage::Internal)?,
        };

        let n = h
            .next_with_group(100)
            .ok_or_eyre("no more words!")
            .map_err(ErrorMessage::NoWordsAvailable)?;

        debug!("found this word {:#?}", n);
        return Ok(quize(&n.0, Some(true)));
    }
    Ok(quize(&noun, Some(false)))
}
#[instrument]
pub async fn home<A: attempt::Repo, N: nouns::Repo>(
    State(storage): State<Arc<AppState<A, N>>>,
) -> Result<impl IntoResponse, ErrorMessage> {
    let nouns = storage
        .nouns_repo
        .clone()
        .all_nouns()
        .map_err(ErrorMessage::Internal)?;

    let attempts = storage
        .attempts_repo
        .clone()
        .all_attempts()
        .map_err(ErrorMessage::Internal)?;

    let h = History { nouns, attempts };

    let chosen = if storage.group_config.enable {
        h.next_with_group(storage.group_config.threshold)
    } else {
        h.next()
    }
    .ok_or_eyre("there are no words available for asking. Have you added some?")
    .map_err(ErrorMessage::NoWordsAvailable)?;
    let header_generated = header();

    Ok::<(StatusCode, Markup), _>((
        StatusCode::OK,
        html!(
            (DOCTYPE)
            html{
                head{(title("DerDieDas")) (style_css())}
                body {
                    (base_htmz())
                    header {(header_generated)}
                    main {(quize(&chosen.0, None))}
                    footer {}

                    (htmz())
                }
            }
        ),
    ))
}

#[instrument]
fn header() -> Markup {
    html!(
        div id="header-wrapper" {
            div id="logo-wrapper" {
                img src="/logo.svg" width="100" alt="Der Die Das" {}

            }
        }
    )
}

fn title(title: &str) -> Markup {
    html!(title  {(title)})
}

fn quize(noun: &Noun, res: Option<bool>) -> Markup {
    let res_mark = match res {
        Some(success) => {
            if success {
                html!(div id="super" {"SUPER!"})
            } else {
                html!(div id="sorry" {"sorry! :("})
            }
        }
        None => {
            html!( div id="new-day-new-word" {p id="new-day" {"new day"} p id="new-word"  {"new word"}})
        }
    };

    html!(
        div id="quiz" {
            (question(noun))
            div id="res" {
                (res_mark)

            }
        }


    )
}

fn question(noun: &Noun) -> Markup {
    html!(
        form id="question-box"{
            div class="buttons"{
                button class="article-button" id="der-button" formaction=(format!("/send-answer/{}/der#quiz",noun.id))  {"der"}
                button class="article-button" id="die-button" formaction=(format!("/send-answer/{}/die#quiz",noun.id))  {"die"}
                button class="article-button" id="das-button" formaction=(format!("/send-answer/{}/das#quiz",noun.id))  {"das"}
            }
            div class="word-box"{
                p id="word" {(noun.word)}
                p id="meaning" {(noun.meaning)}
            }
        }
    )
}

fn style_css() -> Markup {
    html!(
        link rel="stylesheet" href="/style.css" {}
    )
}

fn htmz() -> Markup {
    html!(
        iframe  hidden name="htmz" onload="setTimeout(()=>document.querySelector(contentWindow.location.hash||null)?.replaceWith(...contentDocument.body.childNodes))"{}

    )
}

fn base_htmz() -> Markup {
    html!(
        base target="htmz"{}
    )
}

A src/bin/homepage/web/style.css => src/bin/homepage/web/style.css +161 -0
@@ 0,0 1,161 @@
body {
  display: flex;
  justify-content: center;
  align-content: center;
  align-items: center;
  flex-direction: column;
}

header,
body,
footer {
  padding: 1em;
  width: 90%;
}

#header-wrapper {
  display: flex;
  flex-direction: row;
  justify-content: center;
  align-content: center;
  align-items: center;
  padding: 1em;
  border: 1px solid rgba(139, 42, 0, 1);
  border-radius: 1em;
}


#logo {
  display: flex;
  max-width: 10%;
  min-width: 5%;
}

#quiz {
  width: 100%;
  display: flex;
  flex-direction: row;
  max-width: none;
  justify-content: stretch;
  align-content: center;
  align-items: center;
  transition: all ease 0.5s;
}

#new-day-new-word {
  display: flex;
  flex-direction: column;
  justify-items: stretch;
  align-items: center;


  column-gap: 0px;
  padding: 0.7em;
  font-size: 3.5em;
  font-family: system-ui, -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, Oxygen, Ubuntu, Cantarell, 'Open Sans', 'Helvetica Neue', sans-serif;
  color: rgba(139, 42, 0, 1);
}

#new-day {
  margin-block-start: 0em;
  margin-block-end: 0em;
}

#new-word {
  margin-block-start: 0em;
  margin-block-end: 0em;
  transition: all ease 0.5s;
}

#new-word:hover {
  transition: all ease 0.5s;
  transform: rotate(10deg);
}

#sorry,
#super {
  display: flex;
  flex-direction: column;
  justify-items: stretch;
  align-items: center;
  font-size: 4em;
  padding: 1em;
  font-family: system-ui, -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, Oxygen, Ubuntu, Cantarell, 'Open Sans', 'Helvetica Neue', sans-serif;
  color: rgba(139, 42, 0, 1);
}

#question-box {
  display: flex;
  border: 1px solid rgba(139, 42, 0, 1);
  border-radius: 1em;
  align-content: center;
  align-items: center;
  max-width: none;
  min-width: 50%;
  justify-content: space-evenly;

}

.buttons {
  display: flex;
  flex-direction: column;
  padding: 1em;
  column-gap: 1em;
  gap: 1em;
}

.article-button {
  font-size: 1em;
  padding: 1em;

  border: 1px solid rgba(139, 42, 0, 1);
  background-color: rgba(221, 233, 212, 1);
  color: rgba(139, 42, 0, 1);

  border-radius: 1em;
  box-shadow: -0.1em 0.1em 0;
  transition: all ease 0.5s;
  min-width: 10em;

}

.article-button:hover {
  font-weight: bold;
  border-style: solid;
  box-shadow: -0.5em 0.5em 0;
  transition: all ease 0.5s;
}

.word-box {
  display: flex;
  flex-direction: column;
  justify-content: center;
  align-content: center;
  align-items: center;
  padding: 2em;
  margin-block-end: 0;
  margin-block-start: 0;
}

#word {
  font-size: 4em;
  font-weight: 700;
  font-family: system-ui, -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, Oxygen, Ubuntu, Cantarell, 'Open Sans', 'Helvetica Neue', sans-serif;
  color: rgba(139, 42, 0, 1);
  margin-block-end: 0;
  margin-block-start: 0;
}

#meaning {
  font-size: 2em;
  font-family: system-ui, -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, Oxygen, Ubuntu, Cantarell, 'Open Sans', 'Helvetica Neue', sans-serif;
  color: rgba(139, 42, 0, 1);
  margin-block-end: 0;
  margin-block-start: 0;
}

#word:hover {
  text-decoration-style: dashed;
  text-decoration-line: underline;
  transition: all ease 0.5s;
}
\ No newline at end of file

M src/nouns.rs => src/nouns.rs +5 -2
@@ 11,7 11,10 @@ pub struct Noun {
    pub group: String,
}

#[derive(Debug, Clone, Copy, clap::ValueEnum, PartialEq, Eq, Hash)]
#[derive(
    Debug, Clone, Copy, clap::ValueEnum, PartialEq, Eq, Hash, serde::Deserialize, serde::Serialize,
)]
#[serde(rename_all = "lowercase")]
pub enum Article {
    Der,
    Die,


@@ 32,7 35,7 @@ impl Display for Article {
    }
}

pub trait Repo: Clone {
pub trait Repo: Clone + Send + Sync + std::fmt::Debug {
    /// Saves one noun.
    ///
    /// # Errors

M src/storage.rs => src/storage.rs +115 -2
@@ 1,4 1,4 @@
use std::path::PathBuf;
use std::{path::PathBuf, sync::Arc};

use color_eyre::{
    eyre::{eyre, Result},


@@ 12,7 12,9 @@ use crate::{
    nouns::{self, Article, Noun},
};

#[derive(Debug, Clone)]
pub struct Storage(PathBuf);

impl Storage {
    fn attempts_path(&self) -> PathBuf {
        self.0.join("attempts")


@@ 62,6 64,63 @@ impl TryFrom<PathBuf> for Storage {
        Ok(storage)
    }
}
impl nouns::Repo for Storage {
    fn save_noun(self, noun: Noun) -> Result<()> {
        (&self).save_noun(noun)
    }

    fn all_nouns(self) -> Result<Vec<Noun>> {
        (&self).all_nouns()
    }

    fn find_noun_by_id(self, id: i128) -> Result<Noun> {
        (&self).find_noun_by_id(id)
    }

    fn delete_noun_by_id(self, id: i128) -> Result<()> {
        (&self).delete_noun_by_id(id)
    }

    fn edit_noun_by_id(
        self,
        id: i128,
        article: Option<Vec<Article>>,
        word: Option<String>,
        meaning: Option<String>,
        group: Option<String>,
    ) -> Result<()> {
        (&self).edit_noun_by_id(id, article, word, meaning, group)
    }
}
impl nouns::Repo for Arc<Storage> {
    fn save_noun(self, noun: Noun) -> Result<()> {
        self.as_ref().save_noun(noun)
    }

    fn all_nouns(self) -> Result<Vec<Noun>> {
        self.as_ref().all_nouns()
    }

    fn find_noun_by_id(self, id: i128) -> Result<Noun> {
        self.as_ref().find_noun_by_id(id)
    }

    fn delete_noun_by_id(self, id: i128) -> Result<()> {
        self.as_ref().delete_noun_by_id(id)
    }

    fn edit_noun_by_id(
        self,
        id: i128,
        article: Option<Vec<Article>>,
        word: Option<String>,
        meaning: Option<String>,
        group: Option<String>,
    ) -> Result<()> {
        self.as_ref()
            .edit_noun_by_id(id, article, word, meaning, group)
    }
}

impl nouns::Repo for &Storage {
    fn save_noun(self, noun: nouns::Noun) -> Result<()> {


@@ 131,7 190,7 @@ impl nouns::Repo for &Storage {
        meaning: Option<String>,
        group: Option<String>,
    ) -> Result<()> {
        let mut n = self.find_noun_by_id(id)?;
        let mut n = self.clone().find_noun_by_id(id)?;

        if let Some(x) = article {
            debug!(


@@ 301,6 360,49 @@ impl From<Conclusion> for AttemptResultFileV1 {
    }
}

impl attempt::Repo for Arc<Storage> {
    fn save_attempt(self, attempt: Attempt) -> Result<()> {
        self.as_ref().save_attempt(attempt)
    }

    fn all_attempts(self) -> Result<Vec<Attempt>> {
        self.as_ref().all_attempts()
    }

    fn find_attempt_by_id(self, id: i128) -> Result<Attempt> {
        self.as_ref().find_attempt_by_id(id)
    }

    fn find_attempt_by_noun_id(self, id: i128) -> Result<Vec<Attempt>> {
        self.as_ref().find_attempt_by_noun_id(id)
    }

    fn delete_attempt_by_id(self, id: i128) -> Result<()> {
        self.as_ref().delete_attempt_by_id(id)
    }
}

impl attempt::Repo for Storage {
    fn save_attempt(self, attempt: Attempt) -> Result<()> {
        (&self).save_attempt(attempt)
    }

    fn all_attempts(self) -> Result<Vec<Attempt>> {
        (&self).all_attempts()
    }

    fn find_attempt_by_id(self, id: i128) -> Result<Attempt> {
        (&self).find_attempt_by_id(id)
    }

    fn find_attempt_by_noun_id(self, id: i128) -> Result<Vec<Attempt>> {
        (&self).find_attempt_by_noun_id(id)
    }

    fn delete_attempt_by_id(self, id: i128) -> Result<()> {
        (&self).delete_attempt_by_id(id)
    }
}
impl attempt::Repo for &Storage {
    fn save_attempt(self, a: Attempt) -> Result<()> {
        let file_name = format!("{}.json", a.id);


@@ 367,3 469,14 @@ impl attempt::Repo for &Storage {
        Ok(())
    }
}

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

    fn is_normal<T: Sized + Send + Sync + Unpin>() {}
    #[test]
    fn normal_types() {
        is_normal::<Storage>();
    }
}