prototype

This commit is contained in:
thek4n 2026-06-03 01:45:45 +03:00
parent 7d5e336868
commit 0d3b85cada
6 changed files with 1001 additions and 81 deletions

554
Cargo.lock generated
View File

@ -52,7 +52,65 @@ dependencies = [
"serde", "serde",
"serde_derive", "serde_derive",
"unicode-ident", "unicode-ident",
"winnow", "winnow 1.0.3",
]
[[package]]
name = "atomic-waker"
version = "1.1.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "1505bd5d3d116872e7271a6d4e16d81d0c8570876c8de68093a09ac269d8aac0"
[[package]]
name = "axum"
version = "0.8.9"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "31b698c5f9a010f6573133b09e0de5408834d0c82f8d7475a89fc1867a71cd90"
dependencies = [
"axum-core",
"bytes",
"form_urlencoded",
"futures-util",
"http",
"http-body",
"http-body-util",
"hyper",
"hyper-util",
"itoa",
"matchit",
"memchr",
"mime",
"percent-encoding",
"pin-project-lite",
"serde_core",
"serde_json",
"serde_path_to_error",
"serde_urlencoded",
"sync_wrapper",
"tokio",
"tower",
"tower-layer",
"tower-service",
"tracing",
]
[[package]]
name = "axum-core"
version = "0.5.6"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "08c78f31d7b1291f7ee735c1c6780ccde7785daae9a9206026862dab7d8792d1"
dependencies = [
"bytes",
"futures-core",
"http",
"http-body",
"http-body-util",
"mime",
"pin-project-lite",
"sync_wrapper",
"tower-layer",
"tower-service",
"tracing",
] ]
[[package]] [[package]]
@ -64,30 +122,281 @@ dependencies = [
"serde", "serde",
] ]
[[package]]
name = "bitflags"
version = "2.12.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "84d7ced0ae9557296835c32bf1b1e02b44c746701f898460fb000d7eaa84f00a"
[[package]]
name = "bytes"
version = "1.11.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "1e748733b7cbc798e1434b6ac524f0c1ff2ab456fe201501e6497c8417a4fc33"
[[package]]
name = "cfg-if"
version = "1.0.4"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "9330f8b2ff13f34540b44e946ef35111825727b38d33286ef986142615121801"
[[package]]
name = "equivalent"
version = "1.0.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "877a4ace8713b0bcf2a4e7eec82529c029f1d0619886d18145fea96c3ffe5c0f"
[[package]]
name = "errno"
version = "0.3.14"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "39cab71617ae0d63f51a36d69f866391735b51691dbda63cf6f96d042b63efeb"
dependencies = [
"libc",
"windows-sys",
]
[[package]]
name = "form_urlencoded"
version = "1.2.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "cb4cb245038516f5f85277875cdaa4f7d2c9a0fa0468de06ed190163b1581fcf"
dependencies = [
"percent-encoding",
]
[[package]]
name = "futures-channel"
version = "0.3.32"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "07bbe89c50d7a535e539b8c17bc0b49bdb77747034daa8087407d655f3f7cc1d"
dependencies = [
"futures-core",
]
[[package]]
name = "futures-core"
version = "0.3.32"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "7e3450815272ef58cec6d564423f6e755e25379b217b0bc688e295ba24df6b1d"
[[package]]
name = "futures-task"
version = "0.3.32"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "037711b3d59c33004d3856fbdc83b99d4ff37a24768fa1be9ce3538a1cde4393"
[[package]]
name = "futures-util"
version = "0.3.32"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "389ca41296e6190b48053de0321d02a77f32f8a5d2461dd38762c0593805c6d6"
dependencies = [
"futures-core",
"futures-task",
"pin-project-lite",
"slab",
]
[[package]] [[package]]
name = "glob" name = "glob"
version = "0.3.3" version = "0.3.3"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "0cc23270f6e1808e30a928bdc84dea0b9b4136a8bc82338574f23baf47bbd280" checksum = "0cc23270f6e1808e30a928bdc84dea0b9b4136a8bc82338574f23baf47bbd280"
[[package]]
name = "hashbrown"
version = "0.17.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "ed5909b6e89a2db4456e54cd5f673791d7eca6732202bbf2a9cc504fe2f9b84a"
[[package]]
name = "http"
version = "1.4.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "8be7462df143984c4598a256ef469b251d7d7f9e271135073e78fc535414f3d0"
dependencies = [
"bytes",
"itoa",
]
[[package]]
name = "http-body"
version = "1.0.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "1efedce1fb8e6913f23e0c92de8e62cd5b772a67e7b3946df930a62566c93184"
dependencies = [
"bytes",
"http",
]
[[package]]
name = "http-body-util"
version = "0.1.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "b021d93e26becf5dc7e1b75b1bed1fd93124b374ceb73f43d4d4eafec896a64a"
dependencies = [
"bytes",
"futures-core",
"http",
"http-body",
"pin-project-lite",
]
[[package]]
name = "httparse"
version = "1.10.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "6dbf3de79e51f3d586ab4cb9d5c3e2c14aa28ed23d180cf89b4df0454a69cc87"
[[package]]
name = "httpdate"
version = "1.0.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "df3b46402a9d5adb4c86a0cf463f42e19994e3ee891101b1841f30a545cb49a9"
[[package]]
name = "hyper"
version = "1.10.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "55281c53a1894c864990125767da440a4e630446785086f52523b20033b74498"
dependencies = [
"atomic-waker",
"bytes",
"futures-channel",
"futures-core",
"http",
"http-body",
"httparse",
"httpdate",
"itoa",
"pin-project-lite",
"smallvec",
"tokio",
]
[[package]]
name = "hyper-util"
version = "0.1.20"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "96547c2556ec9d12fb1578c4eaf448b04993e7fb79cbaad930a656880a6bdfa0"
dependencies = [
"bytes",
"http",
"http-body",
"hyper",
"pin-project-lite",
"tokio",
"tower-service",
]
[[package]]
name = "indexmap"
version = "2.14.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "d466e9454f08e4a911e14806c24e16fba1b4c121d1ea474396f396069cf949d9"
dependencies = [
"equivalent",
"hashbrown",
]
[[package]] [[package]]
name = "itoa" name = "itoa"
version = "1.0.18" version = "1.0.18"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "8f42a60cbdf9a97f5d2305f08a87dc4e09308d1276d28c869c684d7777685682" checksum = "8f42a60cbdf9a97f5d2305f08a87dc4e09308d1276d28c869c684d7777685682"
[[package]]
name = "libc"
version = "0.2.186"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "68ab91017fe16c622486840e4c83c9a37afeff978bd239b5293d61ece587de66"
[[package]]
name = "lock_api"
version = "0.4.14"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "224399e74b87b5f3557511d98dff8b14089b3dadafcab6bb93eab67d3aace965"
dependencies = [
"scopeguard",
]
[[package]]
name = "log"
version = "0.4.31"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "113b30b4cd05f7c06868fdb2854f66a7b9fece9a48425351cd532e810d74024f"
[[package]]
name = "matchit"
version = "0.8.4"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "47e1ffaa40ddd1f3ed91f717a33c8c0ee23fff369e3aa8772b9605cc1d22f4c3"
[[package]] [[package]]
name = "memchr" name = "memchr"
version = "2.8.1" version = "2.8.1"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "6b947ae49db0d222b1dbc6b113ce7248a3fc3a6ca21b696717bfc000ba4484d8" checksum = "6b947ae49db0d222b1dbc6b113ce7248a3fc3a6ca21b696717bfc000ba4484d8"
[[package]]
name = "mime"
version = "0.3.17"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "6877bb514081ee2a7ff5ef9de3281f14a4dd4bceac4c09388074a6b5df8a139a"
[[package]]
name = "mio"
version = "1.2.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "02bd0af71c67b473010cbbc60715ee815645a4dc942899111f494b4b737d6fda"
dependencies = [
"libc",
"wasi",
"windows-sys",
]
[[package]]
name = "once_cell"
version = "1.21.4"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "9f7c3e4beb33f85d45ae3e3a1792185706c8e16d043238c593331cc7cd313b50"
[[package]]
name = "parking_lot"
version = "0.12.5"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "93857453250e3077bd71ff98b6a65ea6621a19bb0f559a85248955ac12c45a1a"
dependencies = [
"lock_api",
"parking_lot_core",
]
[[package]]
name = "parking_lot_core"
version = "0.9.12"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "2621685985a2ebf1c516881c026032ac7deafcda1a2c9b7850dc81e3dfcb64c1"
dependencies = [
"cfg-if",
"libc",
"redox_syscall",
"smallvec",
"windows-link",
]
[[package]] [[package]]
name = "percent-encoding" name = "percent-encoding"
version = "2.3.2" version = "2.3.2"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "9b4f627cb1b25917193a259e49bdad08f671f8d9708acfd5fe0a8c1455d87220" checksum = "9b4f627cb1b25917193a259e49bdad08f671f8d9708acfd5fe0a8c1455d87220"
[[package]]
name = "pin-project-lite"
version = "0.2.17"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "a89322df9ebe1c1578d689c92318e070967d1042b512afbe49518723f4e6d5cd"
[[package]] [[package]]
name = "proc-macro2" name = "proc-macro2"
version = "1.0.106" version = "1.0.106"
@ -106,12 +415,33 @@ dependencies = [
"proc-macro2", "proc-macro2",
] ]
[[package]]
name = "redox_syscall"
version = "0.5.18"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "ed2bf2547551a7053d6fdfafda3f938979645c44812fbfcda098faae3f1a362d"
dependencies = [
"bitflags",
]
[[package]] [[package]]
name = "rustc-hash" name = "rustc-hash"
version = "2.1.2" version = "2.1.2"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "94300abf3f1ae2e2b8ffb7b58043de3d399c73fa6f4b73826402a5c457614dbe" checksum = "94300abf3f1ae2e2b8ffb7b58043de3d399c73fa6f4b73826402a5c457614dbe"
[[package]]
name = "ryu"
version = "1.0.23"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "9774ba4a74de5f7b1c1451ed6cd5285a32eddb5cccb8cc655a4e50009e06477f"
[[package]]
name = "scopeguard"
version = "1.2.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "94143f37725109f92c262ed2cf5e59bce7498c01bcc1502d7b9afe439a4e9f49"
[[package]] [[package]]
name = "serde" name = "serde"
version = "1.0.228" version = "1.0.228"
@ -119,6 +449,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "9a8e94ea7f378bd32cbbd37198a4a91436180c5bb472411e48b5ec2e2124ae9e" checksum = "9a8e94ea7f378bd32cbbd37198a4a91436180c5bb472411e48b5ec2e2124ae9e"
dependencies = [ dependencies = [
"serde_core", "serde_core",
"serde_derive",
] ]
[[package]] [[package]]
@ -154,6 +485,70 @@ dependencies = [
"zmij", "zmij",
] ]
[[package]]
name = "serde_path_to_error"
version = "0.1.20"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "10a9ff822e371bb5403e391ecd83e182e0e77ba7f6fe0160b795797109d1b457"
dependencies = [
"itoa",
"serde",
"serde_core",
]
[[package]]
name = "serde_spanned"
version = "0.6.9"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "bf41e0cfaf7226dca15e8197172c295a782857fcb97fad1808a166870dee75a3"
dependencies = [
"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 = "signal-hook-registry"
version = "1.4.8"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "c4db69cba1110affc0e9f7bcd48bbf87b3f4fc7c61fc9155afd4c469eb3d6c1b"
dependencies = [
"errno",
"libc",
]
[[package]]
name = "slab"
version = "0.4.12"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "0c790de23124f9ab44544d7ac05d60440adc586479ce501c1d6d7da3cd8c9cf5"
[[package]]
name = "smallvec"
version = "1.15.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "67b1b7a3b5fe4f1376887184045fcf45c69e92af734b7aaddc05fb777b6fbd03"
[[package]]
name = "socket2"
version = "0.6.4"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "52d1cfed4120b4d927bf7c0f86d2087a4a7d6027c906d9f9d525a80573b9be51"
dependencies = [
"libc",
"windows-sys",
]
[[package]] [[package]]
name = "syn" name = "syn"
version = "2.0.117" version = "2.0.117"
@ -165,11 +560,138 @@ dependencies = [
"unicode-ident", "unicode-ident",
] ]
[[package]]
name = "sync_wrapper"
version = "1.0.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "0bf256ce5efdfa370213c1dabab5935a12e49f2c58d15e9eac2870d3b4f27263"
[[package]] [[package]]
name = "thek4n-ru" name = "thek4n-ru"
version = "0.1.0" version = "0.1.0"
dependencies = [ dependencies = [
"askama", "askama",
"axum",
"serde",
"tokio",
"toml",
]
[[package]]
name = "tokio"
version = "1.52.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "8fc7f01b389ac15039e4dc9531aa973a135d7a4135281b12d7c1bc79fd57fffe"
dependencies = [
"bytes",
"libc",
"mio",
"parking_lot",
"pin-project-lite",
"signal-hook-registry",
"socket2",
"tokio-macros",
"windows-sys",
]
[[package]]
name = "tokio-macros"
version = "2.7.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "385a6cb71ab9ab790c5fe8d67f1645e6c450a7ce006a33de03daa956cf70a496"
dependencies = [
"proc-macro2",
"quote",
"syn",
]
[[package]]
name = "toml"
version = "0.8.23"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "dc1beb996b9d83529a9e75c17a1686767d148d70663143c7854d8b4a09ced362"
dependencies = [
"serde",
"serde_spanned",
"toml_datetime",
"toml_edit",
]
[[package]]
name = "toml_datetime"
version = "0.6.11"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "22cddaf88f4fbc13c51aebbf5f8eceb5c7c5a9da2ac40a13519eb5b0a0e8f11c"
dependencies = [
"serde",
]
[[package]]
name = "toml_edit"
version = "0.22.27"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "41fe8c660ae4257887cf66394862d21dbca4a6ddd26f04a3560410406a2f819a"
dependencies = [
"indexmap",
"serde",
"serde_spanned",
"toml_datetime",
"toml_write",
"winnow 0.7.15",
]
[[package]]
name = "toml_write"
version = "0.1.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "5d99f8c9a7727884afe522e9bd5edbfc91a3312b36a77b5fb8926e4c31a41801"
[[package]]
name = "tower"
version = "0.5.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "ebe5ef63511595f1344e2d5cfa636d973292adc0eec1f0ad45fae9f0851ab1d4"
dependencies = [
"futures-core",
"futures-util",
"pin-project-lite",
"sync_wrapper",
"tokio",
"tower-layer",
"tower-service",
"tracing",
]
[[package]]
name = "tower-layer"
version = "0.3.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "121c2a6cda46980bb0fcd1647ffaf6cd3fc79a013de288782836f6df9c48780e"
[[package]]
name = "tower-service"
version = "0.3.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "8df9b6e13f2d32c91b9bd719c00d1958837bc7dec474d94952798cc8e69eeec3"
[[package]]
name = "tracing"
version = "0.1.44"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "63e71662fa4b2a2c3a26f570f037eb95bb1f85397f3cd8076caed2f026a6d100"
dependencies = [
"log",
"pin-project-lite",
"tracing-core",
]
[[package]]
name = "tracing-core"
version = "0.1.36"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "db97caf9d906fbde555dd62fa95ddba9eecfd14cb388e4f491a66d74cd5fb79a"
dependencies = [
"once_cell",
] ]
[[package]] [[package]]
@ -178,6 +700,36 @@ version = "1.0.24"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "e6e4313cd5fcd3dad5cafa179702e2b244f760991f45397d14d4ebf38247da75" checksum = "e6e4313cd5fcd3dad5cafa179702e2b244f760991f45397d14d4ebf38247da75"
[[package]]
name = "wasi"
version = "0.11.1+wasi-snapshot-preview1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "ccf3ec651a847eb01de73ccad15eb7d99f80485de043efb2f370cd654f4ea44b"
[[package]]
name = "windows-link"
version = "0.2.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "f0805222e57f7521d6a62e36fa9163bc891acd422f971defe97d64e70d0a4fe5"
[[package]]
name = "windows-sys"
version = "0.61.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "ae137229bcbd6cdf0f7b80a31df61766145077ddf49416a728b02cb3921ff3fc"
dependencies = [
"windows-link",
]
[[package]]
name = "winnow"
version = "0.7.15"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "df79d97927682d2fd8adb29682d1140b343be4ac0f08fd68b7765d9c059d3945"
dependencies = [
"memchr",
]
[[package]] [[package]]
name = "winnow" name = "winnow"
version = "1.0.3" version = "1.0.3"

View File

@ -6,3 +6,7 @@ authors = ["Vladislav Kan <thek4n@yandex.ru>"]
[dependencies] [dependencies]
askama = "0.16.0" askama = "0.16.0"
serde = { version = "1.0", features = ["derive"] }
toml = "0.8"
axum = "0.8"
tokio = { version = "1", features = ["full"] }

12
TODO.md Normal file
View File

@ -0,0 +1,12 @@
# ToDo list
* [ ] Сделать в embed шаблоны
* [ ] Сделать автоматическую локализацию по ip или заголовкам http `Accept-Language`
* [ ] Сделать локализацию по параметру `/?lang=en`
* [X] Сделать авто выбор верстки в зависимости от User-Agent
* [ ] Сделать стили и засунуть их в embed (с уникальным хешем в имени)
* [ ] Сделать скрипты и засунуть их в embed (с уникальным хешем в имени)
* [ ] Отрефачить
* [ ] Сделать определение хоста по которому был запрос и засунуть его в
аргумент `build_html_template()`
* [ ] Сделать текущий год на клиенте или сервере (решить) (на сервере
предпочтительно, чтобы им нельзя было манипулировать на клиенте)

View File

@ -1,9 +1,11 @@
[page] [page]
title = "Hi, I'm Vlad" title.en = "Hi, I'm Vlad"
title.ru = "Владислав Кан"
meta_title = "thek4n" meta_title = "thek4n"
meta_description = "thek4n - developer" meta_description = "thek4n - developer"
description = "Developer" description.en = "Developer"
copyright_author = "thek4n" description.ru = "Разработчик"
copyright_author = "Vladislav Kan <thek4n@yandex.ru>"
# --- # ---
@ -16,7 +18,7 @@ title = "english"
[[languages]] [[languages]]
key = "ru" key = "ru"
title = "russian" title = "русский"
# --- # ---
@ -26,11 +28,11 @@ title = "russian"
title.en = "About Me" title.en = "About Me"
title.ru = "Обо Мне" title.ru = "Обо Мне"
text.en = ''' text.en = '''
About me text...
''' '''
text.ru = ''' text.ru = '''
Обо мне текст...
''' '''
@ -55,9 +57,57 @@ copied_title.en = "Copied!"
copied_title.ru = "Скопировано!" copied_title.ru = "Скопировано!"
value = ''' value = '''
asdfasdf -----BEGIN PGP PUBLIC KEY BLOCK-----
asdfasdf
asdfasdf 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
=h906
-----END PGP PUBLIC KEY BLOCK-----
''' '''

View File

@ -1,5 +1,88 @@
use askama::Template; use askama::Template;
use serde::Deserialize;
use std::collections::HashMap;
use std::fs;
use axum::{
http::header::{USER_AGENT, ACCEPT},
response::{Html, IntoResponse},
routing::get,
extract::Query,
Router,
http::HeaderMap,
};
use std::net::SocketAddr;
#[derive(Debug, Deserialize)]
struct Config {
page: PageConfig,
languages: Vec<LanguageConfig>,
projects: Option<Vec<ProjectConfig>>,
contacts: Option<Vec<ContactConfig>>,
}
#[derive(Debug, Deserialize)]
struct PageConfig {
title: HashMap<String, String>, // теперь HashMap
meta_title: String,
meta_description: String,
description: HashMap<String, String>, // теперь HashMap
copyright_author: String,
aboutme: Option<AboutMeConfig>,
gpg: Option<GPGConfig>,
projects: Option<ProjectsSectionConfig>,
contacts: Option<ContactsSectionConfig>,
}
#[derive(Debug, Deserialize)]
struct AboutMeConfig {
title: HashMap<String, String>,
text: HashMap<String, String>,
}
#[derive(Debug, Deserialize)]
struct GPGConfig {
title: HashMap<String, String>,
url: String,
url_title: String,
showkey_title: HashMap<String, String>,
availat_title: HashMap<String, String>,
copy_title: HashMap<String, String>,
value: String,
}
#[derive(Debug, Deserialize)]
struct ProjectsSectionConfig {
title: HashMap<String, String>,
}
#[derive(Debug, Deserialize)]
struct ContactsSectionConfig {
title: HashMap<String, String>,
}
#[derive(Debug, Deserialize)]
struct ProjectConfig {
title: String,
url: String,
source_url: String,
source_url_title: String,
description: HashMap<String, String>,
}
#[derive(Debug, Deserialize)]
struct ContactConfig {
site_name: HashMap<String, String>,
title: String,
url: String,
}
#[derive(Debug, Deserialize)]
struct LanguageConfig {
key: String,
title: String,
}
// Template structures matching the HTML
#[derive(Template)] #[derive(Template)]
#[template(path = "html_index.jinja2")] #[template(path = "html_index.jinja2")]
struct HtmlPageTemplate<'a> { struct HtmlPageTemplate<'a> {
@ -23,25 +106,26 @@ struct TextPageTemplate<'a> {
description: &'a str, description: &'a str,
copyright_author: &'a str, copyright_author: &'a str,
base_url: &'a str, base_url: &'a str,
meta: PageMeta<'a>,
year: &'a str, year: &'a str,
aboutme_section: AboutMeSection<'a>, aboutme_section: AboutMeSection<'a>,
gpg_section: GPGSection<'a>, gpg_section: GPGSection<'a>,
contacts_section: ContactsSection<'a>, contacts_section: ContactsSection<'a>,
projects_section: ProjectsSection<'a>, projects_section: ProjectsSection<'a>,
languages: Vec<Language<'a>>,
} }
#[derive(Clone, Copy)]
struct PageMeta<'a> { struct PageMeta<'a> {
title: &'a str, title: &'a str,
description: &'a str, description: &'a str,
} }
#[derive(Clone, Copy)]
struct AboutMeSection<'a> { struct AboutMeSection<'a> {
title: &'a str, title: &'a str,
text: &'a str, text: &'a str,
} }
#[derive(Clone, Copy)]
struct GPGSection<'a> { struct GPGSection<'a> {
title: &'a str, title: &'a str,
availat_title: &'a str, availat_title: &'a str,
@ -52,22 +136,26 @@ struct GPGSection<'a> {
value: &'a str, value: &'a str,
} }
#[derive(Clone)]
struct ContactsSection<'a> { struct ContactsSection<'a> {
title: &'a str, title: &'a str,
contacts: Vec<Contact<'a>>, contacts: Vec<Contact<'a>>,
} }
#[derive(Clone)]
struct Contact<'a> { struct Contact<'a> {
site_name: &'a str, site_name: &'a str,
url: &'a str, url: &'a str,
title: &'a str, title: &'a str,
} }
#[derive(Clone)]
struct ProjectsSection<'a> { struct ProjectsSection<'a> {
title: &'a str, title: &'a str,
projects: Vec<Project<'a>>, projects: Vec<Project<'a>>,
} }
#[derive(Clone)]
struct Project<'a> { struct Project<'a> {
title: &'a str, title: &'a str,
url: &'a str, url: &'a str,
@ -76,76 +164,289 @@ struct Project<'a> {
description: &'a str, description: &'a str,
} }
#[derive(Clone)]
struct Language<'a> { struct Language<'a> {
key: &'a str, key: &'a str,
current: bool, current: bool,
value: &'a str, value: &'a str,
} }
fn main() { fn load_config(path: &str) -> Result<Config, Box<dyn std::error::Error>> {
let meta = PageMeta{ let contents = fs::read_to_string(path)?;
title: "thek4n", let config: Config = toml::from_str(&contents)?;
description: "thek4n - developer", Ok(config)
}; }
let aboutme_section = AboutMeSection{ fn build_aboutme_section<'a>(config: &'a Config, lang_key: &'a str) -> AboutMeSection<'a> {
title: "About Me", AboutMeSection {
text: "About me text", title: config
}; .page
.aboutme
let gpg_section = GPGSection{ .as_ref()
title: "GPG Key", .and_then(|a| a.title.get(lang_key))
availat_title: "Available at", .map(|s| s.as_str())
url: "/gpgkey.txt", .unwrap_or(""),
url_title: "thek4n.ru/gpgkey.txt", text: config
showkey_title: "Show key", .page
copy_title: "Copy", .aboutme
value: "key....", .as_ref()
}; .and_then(|a| a.text.get(lang_key))
.map(|s| s.as_str())
let contacts = vec![Contact{ .unwrap_or(""),
site_name: "Telegram", }
url: "https://t.me/thek4n", }
title: "@thek4n",
}]; fn build_gpg_section<'a>(config: &'a Config, lang_key: &'a str) -> GPGSection<'a> {
let gpg_config = config.page.gpg.as_ref().unwrap();
let contacts_section = ContactsSection{ GPGSection {
title: "Contacts", title: gpg_config.title.get(lang_key).map(|s| s.as_str()).unwrap_or(""),
contacts, availat_title: gpg_config
}; .availat_title
.get(lang_key)
let projects = vec![Project{ .map(|s| s.as_str())
title: "project", .unwrap_or(""),
url: "https://paste.thek4n.ru", url: &gpg_config.url,
source_url: "https://gitea.thek4n.ru/thek4n/paste.thek4n.ru", url_title: &gpg_config.url_title,
source_url_title: "thek4n/paste.thek4n.ru", showkey_title: gpg_config
description: "copy/paste service", .showkey_title
}]; .get(lang_key)
.map(|s| s.as_str())
let projects_section = ProjectsSection{ .unwrap_or(""),
title: "Projects", copy_title: gpg_config
projects, .copy_title
}; .get(lang_key)
.map(|s| s.as_str())
let languages = vec![Language{ .unwrap_or(""),
key: "en", value: &gpg_config.value,
current: true, }
value: "English", }
}];
fn build_contacts_section<'a>(config: &'a Config, lang_key: &'a str) -> ContactsSection<'a> {
let index = TextPageTemplate{ let contacts_config = config.page.contacts.as_ref().unwrap();
title: "TheK4n", let contacts: Vec<Contact<'a>> = config
description: "Developer", .contacts
copyright_author: "Vladislav Kan <thek4n@yandex.ru>", .as_ref()
base_url: "https://thek4n.ru", .unwrap()
meta, .iter()
year: "2026", // todo: get current year .map(|contact| Contact {
aboutme_section, site_name: contact
gpg_section, .site_name
projects_section, .get(lang_key)
contacts_section, .map(|s| s.as_str())
languages, .unwrap_or(""),
}; url: &contact.url,
title: &contact.title,
println!("{}", index.render().unwrap()); })
.collect();
ContactsSection {
title: contacts_config
.title
.get(lang_key)
.map(|s| s.as_str())
.unwrap_or(""),
contacts,
}
}
fn build_projects_section<'a>(config: &'a Config, lang_key: &'a str) -> ProjectsSection<'a> {
let projects_config = config.page.projects.as_ref().unwrap();
let projects: Vec<Project<'a>> = config
.projects
.as_ref()
.unwrap()
.iter()
.map(|project| Project {
title: &project.title,
url: &project.url,
source_url: &project.source_url,
source_url_title: &project.source_url_title,
description: project
.description
.get(lang_key)
.map(|s| s.as_str())
.unwrap_or(""),
})
.collect();
ProjectsSection {
title: projects_config
.title
.get(lang_key)
.map(|s| s.as_str())
.unwrap_or(""),
projects,
}
}
fn build_languages<'a>(config: &'a Config, lang_key: &'a str) -> Vec<Language<'a>> {
config
.languages
.iter()
.map(|lang| Language {
key: &lang.key,
current: lang.key == lang_key,
value: &lang.title,
})
.collect()
}
fn build_html_template<'a>(
config: &'a Config,
lang_key: &'a str,
base_url: &'a str,
year: &'a str,
) -> HtmlPageTemplate<'a> {
let meta = PageMeta {
title: &config.page.meta_title,
description: &config.page.meta_description,
};
HtmlPageTemplate {
title: config.page.title.get(lang_key).map(|s| s.as_str()).unwrap_or(""),
description: config.page.description.get(lang_key).map(|s| s.as_str()).unwrap_or(""),
copyright_author: &config.page.copyright_author,
base_url,
meta,
year,
aboutme: build_aboutme_section(config, lang_key),
gpg_section: build_gpg_section(config, lang_key),
contacts_section: build_contacts_section(config, lang_key),
projects_section: build_projects_section(config, lang_key),
languages: build_languages(config, lang_key),
}
}
fn build_text_template<'a>(
config: &'a Config,
lang_key: &'a str,
base_url: &'a str,
year: &'a str,
) -> TextPageTemplate<'a> {
TextPageTemplate {
title: config.page.title.get(lang_key).map(|s| s.as_str()).unwrap_or(""),
description: config.page.description.get(lang_key).map(|s| s.as_str()).unwrap_or(""),
copyright_author: &config.page.copyright_author,
base_url,
year,
aboutme_section: build_aboutme_section(config, lang_key),
gpg_section: build_gpg_section(config, lang_key),
contacts_section: build_contacts_section(config, lang_key),
projects_section: build_projects_section(config, lang_key),
}
}
#[derive(Debug, PartialEq)]
enum ClientType {
Browser,
CliLike, // curl, wget, httpie и т.д.
Unknown,
}
fn detect_client_type(headers: &HeaderMap) -> ClientType {
let user_agent = headers
.get(USER_AGENT)
.and_then(|v| v.to_str().ok())
.unwrap_or("");
let user_agent_lower = user_agent.to_lowercase();
// Проверка Accept header
let accept = headers
.get(ACCEPT)
.and_then(|v| v.to_str().ok())
.unwrap_or("");
let accept_lower = accept.to_lowercase();
// CLI утилиты обычно имеют простой Accept или не имеют его вовсе
let is_likely_cli = accept_lower.is_empty()
|| accept_lower == "*/*"
|| accept_lower == "application/json"
|| accept_lower == "text/plain";
// Браузеры всегда запрашивают text/html в первую очередь
let is_likely_browser = accept_lower.contains("text/html")
&& (accept_lower.contains("application/xhtml+xml")
|| accept_lower.contains("application/xml")
|| accept_lower.contains("*/*"));
// Комбинированная проверка: User-Agent + Accept
if user_agent_lower.contains("curl")
|| user_agent_lower.contains("wget")
|| user_agent_lower.contains("httpie")
|| user_agent_lower.contains("python-requests")
|| user_agent_lower.contains("go-http-client")
|| (is_likely_cli && !user_agent_lower.contains("mozilla/"))
{
return ClientType::CliLike;
}
if user_agent_lower.contains("mozilla/")
&& (user_agent_lower.contains("chrome")
|| user_agent_lower.contains("safari")
|| user_agent_lower.contains("firefox")
|| user_agent_lower.contains("edg"))
&& is_likely_browser
{
return ClientType::Browser;
}
// Fallback с использованием только Accept header
if is_likely_browser {
ClientType::Browser
} else if is_likely_cli {
ClientType::CliLike
} else {
ClientType::Unknown
}
}
#[derive(Debug, Deserialize)]
pub struct RootParams {
pub lang: Option<String>,
}
async fn root_handler(
headers: HeaderMap,
Query(params): Query<RootParams>
) -> impl IntoResponse {
let query_lang = params.lang.unwrap_or("en".to_string());
let config = load_config("docs/config_example.toml").unwrap();
let lang_exists = config.languages.iter().any(|lang_config| lang_config.key == query_lang);
let lang = if lang_exists {
query_lang.as_str()
} else {
"en"
};
match detect_client_type(&headers) {
ClientType::Browser => {
Html(build_html_template(&config, lang, "https://thek4n.ru", "2026")
.render()
.unwrap())
.into_response()
}
ClientType::CliLike | ClientType::Unknown => {
format!("{}\n", build_text_template(&config, lang, "https://thek4n.ru", "2026")).into_response()
}
}
}
#[tokio::main]
async fn main() {
let app = Router::new()
.route("/", get(root_handler));
let addr = SocketAddr::from(([127, 0, 0, 1], 3000));
axum::serve(
tokio::net::TcpListener::bind(addr).await.unwrap(),
app,
)
.await.unwrap();
} }

View File

@ -27,8 +27,7 @@
<h1>{{ projects_section.title }}</h1> <h1>{{ projects_section.title }}</h1>
{% for project in projects_section.projects %} {% for project in projects_section.projects %}
<p> <p>
<span><a href="{{ project.url }}">{{ project.title }}</a></span> <span><a href="{{ project.url }}">{{ project.title }}</a></span>:
:
<span><a href="{{ project.source_url }}">{{ project.source_url_title }}</a></span> <span><a href="{{ project.source_url }}">{{ project.source_url_title }}</a></span>
<span><small class="muted">({{ project.description }})</small> <span><small class="muted">({{ project.description }})</small>
</p> </p>
@ -54,7 +53,9 @@
{{ gpg_section.showkey_title }} {{ gpg_section.showkey_title }}
<a id="copy-gpg" class="copy-link" href="#" onclick="copyGPGKey()">{{ gpg_section.copy_title }}</a> <a id="copy-gpg" class="copy-link" href="#" onclick="copyGPGKey()">{{ gpg_section.copy_title }}</a>
</span> </span>
{{ gpg_section.value }} <code>
{{ gpg_section.value|linebreaksbr }}
</code>
</summary> </summary>
</details> </details>