Initial commit
This commit is contained in:
769
Cargo.lock
generated
Normal file
769
Cargo.lock
generated
Normal file
@@ -0,0 +1,769 @@
|
||||
# This file is automatically @generated by Cargo.
|
||||
# It is not intended for manual editing.
|
||||
version = 4
|
||||
|
||||
[[package]]
|
||||
name = "android-tzdata"
|
||||
version = "0.1.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "e999941b234f3131b00bc13c22d06e8c5ff726d1b6318ac7eb276997bbb4fef0"
|
||||
|
||||
[[package]]
|
||||
name = "android_system_properties"
|
||||
version = "0.1.5"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "819e7219dbd41043ac279b19830f2efc897156490d7fd6ea916720117ee66311"
|
||||
dependencies = [
|
||||
"libc",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "anyhow"
|
||||
version = "1.0.99"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "b0674a1ddeecb70197781e945de4b3b8ffb61fa939a5597bcf48503737663100"
|
||||
|
||||
[[package]]
|
||||
name = "autocfg"
|
||||
version = "1.5.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "c08606f8c3cbf4ce6ec8e28fb0014a2c086708fe954eaa885384a6165172e7e8"
|
||||
|
||||
[[package]]
|
||||
name = "bitflags"
|
||||
version = "2.9.4"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "2261d10cca569e4643e526d8dc2e62e433cc8aba21ab764233731f8d369bf394"
|
||||
|
||||
[[package]]
|
||||
name = "bumpalo"
|
||||
version = "3.19.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "46c5e41b57b8bba42a04676d81cb89e9ee8e859a1a66f80a5a72e1cb76b34d43"
|
||||
|
||||
[[package]]
|
||||
name = "cc"
|
||||
version = "1.2.36"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "5252b3d2648e5eedbc1a6f501e3c795e07025c1e93bbf8bbdd6eef7f447a6d54"
|
||||
dependencies = [
|
||||
"find-msvc-tools",
|
||||
"jobserver",
|
||||
"libc",
|
||||
"shlex",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "cfg-if"
|
||||
version = "1.0.3"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "2fd1289c04a9ea8cb22300a459a72a385d7c73d3259e2ed7dcb2af674838cfa9"
|
||||
|
||||
[[package]]
|
||||
name = "chrono"
|
||||
version = "0.4.41"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "c469d952047f47f91b68d1cba3f10d63c11d73e4636f24f08daf0278abf01c4d"
|
||||
dependencies = [
|
||||
"android-tzdata",
|
||||
"iana-time-zone",
|
||||
"js-sys",
|
||||
"num-traits",
|
||||
"wasm-bindgen",
|
||||
"windows-link",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "core-foundation-sys"
|
||||
version = "0.8.7"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "773648b94d0e5d620f64f280777445740e61fe701025087ec8b57f45c791888b"
|
||||
|
||||
[[package]]
|
||||
name = "displaydoc"
|
||||
version = "0.2.5"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "97369cbbc041bc366949bc74d34658d6cda5621039731c6310521892a3a20ae0"
|
||||
dependencies = [
|
||||
"proc-macro2",
|
||||
"quote",
|
||||
"syn",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "eider"
|
||||
version = "0.1.0"
|
||||
dependencies = [
|
||||
"anyhow",
|
||||
"chrono",
|
||||
"fastcgi",
|
||||
"git2",
|
||||
"hex",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "fastcgi"
|
||||
version = "1.0.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "c4159a0f48bea0281602e508eb070d7d7ba1f6ac2480f9db1a60a39274aea1cc"
|
||||
dependencies = [
|
||||
"libc",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "find-msvc-tools"
|
||||
version = "0.1.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "7fd99930f64d146689264c637b5af2f0233a933bef0d8570e2526bf9e083192d"
|
||||
|
||||
[[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 = "getrandom"
|
||||
version = "0.3.3"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "26145e563e54f2cadc477553f1ec5ee650b00862f0a58bcd12cbdc5f0ea2d2f4"
|
||||
dependencies = [
|
||||
"cfg-if",
|
||||
"libc",
|
||||
"r-efi",
|
||||
"wasi",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "git2"
|
||||
version = "0.20.2"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "2deb07a133b1520dc1a5690e9bd08950108873d7ed5de38dcc74d3b5ebffa110"
|
||||
dependencies = [
|
||||
"bitflags",
|
||||
"libc",
|
||||
"libgit2-sys",
|
||||
"log",
|
||||
"openssl-probe",
|
||||
"openssl-sys",
|
||||
"url",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "hex"
|
||||
version = "0.4.3"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "7f24254aa9a54b5c858eaee2f5bccdb46aaf0e486a595ed5fd8f86ba55232a70"
|
||||
|
||||
[[package]]
|
||||
name = "iana-time-zone"
|
||||
version = "0.1.63"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "b0c919e5debc312ad217002b8048a17b7d83f80703865bbfcfebb0458b0b27d8"
|
||||
dependencies = [
|
||||
"android_system_properties",
|
||||
"core-foundation-sys",
|
||||
"iana-time-zone-haiku",
|
||||
"js-sys",
|
||||
"log",
|
||||
"wasm-bindgen",
|
||||
"windows-core",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "iana-time-zone-haiku"
|
||||
version = "0.1.2"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "f31827a206f56af32e590ba56d5d2d085f558508192593743f16b2306495269f"
|
||||
dependencies = [
|
||||
"cc",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "icu_collections"
|
||||
version = "2.0.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "200072f5d0e3614556f94a9930d5dc3e0662a652823904c3a75dc3b0af7fee47"
|
||||
dependencies = [
|
||||
"displaydoc",
|
||||
"potential_utf",
|
||||
"yoke",
|
||||
"zerofrom",
|
||||
"zerovec",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "icu_locale_core"
|
||||
version = "2.0.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "0cde2700ccaed3872079a65fb1a78f6c0a36c91570f28755dda67bc8f7d9f00a"
|
||||
dependencies = [
|
||||
"displaydoc",
|
||||
"litemap",
|
||||
"tinystr",
|
||||
"writeable",
|
||||
"zerovec",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "icu_normalizer"
|
||||
version = "2.0.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "436880e8e18df4d7bbc06d58432329d6458cc84531f7ac5f024e93deadb37979"
|
||||
dependencies = [
|
||||
"displaydoc",
|
||||
"icu_collections",
|
||||
"icu_normalizer_data",
|
||||
"icu_properties",
|
||||
"icu_provider",
|
||||
"smallvec",
|
||||
"zerovec",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "icu_normalizer_data"
|
||||
version = "2.0.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "00210d6893afc98edb752b664b8890f0ef174c8adbb8d0be9710fa66fbbf72d3"
|
||||
|
||||
[[package]]
|
||||
name = "icu_properties"
|
||||
version = "2.0.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "016c619c1eeb94efb86809b015c58f479963de65bdb6253345c1a1276f22e32b"
|
||||
dependencies = [
|
||||
"displaydoc",
|
||||
"icu_collections",
|
||||
"icu_locale_core",
|
||||
"icu_properties_data",
|
||||
"icu_provider",
|
||||
"potential_utf",
|
||||
"zerotrie",
|
||||
"zerovec",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "icu_properties_data"
|
||||
version = "2.0.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "298459143998310acd25ffe6810ed544932242d3f07083eee1084d83a71bd632"
|
||||
|
||||
[[package]]
|
||||
name = "icu_provider"
|
||||
version = "2.0.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "03c80da27b5f4187909049ee2d72f276f0d9f99a42c306bd0131ecfe04d8e5af"
|
||||
dependencies = [
|
||||
"displaydoc",
|
||||
"icu_locale_core",
|
||||
"stable_deref_trait",
|
||||
"tinystr",
|
||||
"writeable",
|
||||
"yoke",
|
||||
"zerofrom",
|
||||
"zerotrie",
|
||||
"zerovec",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "idna"
|
||||
version = "1.1.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "3b0875f23caa03898994f6ddc501886a45c7d3d62d04d2d90788d47be1b1e4de"
|
||||
dependencies = [
|
||||
"idna_adapter",
|
||||
"smallvec",
|
||||
"utf8_iter",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "idna_adapter"
|
||||
version = "1.2.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "3acae9609540aa318d1bc588455225fb2085b9ed0c4f6bd0d9d5bcd86f1a0344"
|
||||
dependencies = [
|
||||
"icu_normalizer",
|
||||
"icu_properties",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "jobserver"
|
||||
version = "0.1.34"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "9afb3de4395d6b3e67a780b6de64b51c978ecf11cb9a462c66be7d4ca9039d33"
|
||||
dependencies = [
|
||||
"getrandom",
|
||||
"libc",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "js-sys"
|
||||
version = "0.3.78"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "0c0b063578492ceec17683ef2f8c5e89121fbd0b172cbc280635ab7567db2738"
|
||||
dependencies = [
|
||||
"once_cell",
|
||||
"wasm-bindgen",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "libc"
|
||||
version = "0.2.175"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "6a82ae493e598baaea5209805c49bbf2ea7de956d50d7da0da1164f9c6d28543"
|
||||
|
||||
[[package]]
|
||||
name = "libgit2-sys"
|
||||
version = "0.18.2+1.9.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "1c42fe03df2bd3c53a3a9c7317ad91d80c81cd1fb0caec8d7cc4cd2bfa10c222"
|
||||
dependencies = [
|
||||
"cc",
|
||||
"libc",
|
||||
"libssh2-sys",
|
||||
"libz-sys",
|
||||
"openssl-sys",
|
||||
"pkg-config",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "libssh2-sys"
|
||||
version = "0.3.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "220e4f05ad4a218192533b300327f5150e809b54c4ec83b5a1d91833601811b9"
|
||||
dependencies = [
|
||||
"cc",
|
||||
"libc",
|
||||
"libz-sys",
|
||||
"openssl-sys",
|
||||
"pkg-config",
|
||||
"vcpkg",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "libz-sys"
|
||||
version = "1.1.22"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "8b70e7a7df205e92a1a4cd9aaae7898dac0aa555503cc0a649494d0d60e7651d"
|
||||
dependencies = [
|
||||
"cc",
|
||||
"libc",
|
||||
"pkg-config",
|
||||
"vcpkg",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "litemap"
|
||||
version = "0.8.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "241eaef5fd12c88705a01fc1066c48c4b36e0dd4377dcdc7ec3942cea7a69956"
|
||||
|
||||
[[package]]
|
||||
name = "log"
|
||||
version = "0.4.28"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "34080505efa8e45a4b816c349525ebe327ceaa8559756f0356cba97ef3bf7432"
|
||||
|
||||
[[package]]
|
||||
name = "num-traits"
|
||||
version = "0.2.19"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "071dfc062690e90b734c0b2273ce72ad0ffa95f0c74596bc250dcfd960262841"
|
||||
dependencies = [
|
||||
"autocfg",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "once_cell"
|
||||
version = "1.21.3"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "42f5e15c9953c5e4ccceeb2e7382a716482c34515315f7b03532b8b4e8393d2d"
|
||||
|
||||
[[package]]
|
||||
name = "openssl-probe"
|
||||
version = "0.1.6"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "d05e27ee213611ffe7d6348b942e8f942b37114c00cc03cec254295a4a17852e"
|
||||
|
||||
[[package]]
|
||||
name = "openssl-sys"
|
||||
version = "0.9.109"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "90096e2e47630d78b7d1c20952dc621f957103f8bc2c8359ec81290d75238571"
|
||||
dependencies = [
|
||||
"cc",
|
||||
"libc",
|
||||
"pkg-config",
|
||||
"vcpkg",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "percent-encoding"
|
||||
version = "2.3.2"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "9b4f627cb1b25917193a259e49bdad08f671f8d9708acfd5fe0a8c1455d87220"
|
||||
|
||||
[[package]]
|
||||
name = "pkg-config"
|
||||
version = "0.3.32"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "7edddbd0b52d732b21ad9a5fab5c704c14cd949e5e9a1ec5929a24fded1b904c"
|
||||
|
||||
[[package]]
|
||||
name = "potential_utf"
|
||||
version = "0.1.3"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "84df19adbe5b5a0782edcab45899906947ab039ccf4573713735ee7de1e6b08a"
|
||||
dependencies = [
|
||||
"zerovec",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "proc-macro2"
|
||||
version = "1.0.101"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "89ae43fd86e4158d6db51ad8e2b80f313af9cc74f5c0e03ccb87de09998732de"
|
||||
dependencies = [
|
||||
"unicode-ident",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "quote"
|
||||
version = "1.0.40"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "1885c039570dc00dcb4ff087a89e185fd56bae234ddc7f056a945bf36467248d"
|
||||
dependencies = [
|
||||
"proc-macro2",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "r-efi"
|
||||
version = "5.3.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "69cdb34c158ceb288df11e18b4bd39de994f6657d83847bdffdbd7f346754b0f"
|
||||
|
||||
[[package]]
|
||||
name = "rustversion"
|
||||
version = "1.0.22"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "b39cdef0fa800fc44525c84ccb54a029961a8215f9619753635a9c0d2538d46d"
|
||||
|
||||
[[package]]
|
||||
name = "serde"
|
||||
version = "1.0.219"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "5f0e2c6ed6606019b4e29e69dbaba95b11854410e5347d525002456dbbb786b6"
|
||||
dependencies = [
|
||||
"serde_derive",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "serde_derive"
|
||||
version = "1.0.219"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "5b0276cf7f2c73365f7157c8123c21cd9a50fbbd844757af28ca1f5925fc2a00"
|
||||
dependencies = [
|
||||
"proc-macro2",
|
||||
"quote",
|
||||
"syn",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "shlex"
|
||||
version = "1.3.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "0fda2ff0d084019ba4d7c6f371c95d8fd75ce3524c3cb8fb653a3023f6323e64"
|
||||
|
||||
[[package]]
|
||||
name = "smallvec"
|
||||
version = "1.15.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "67b1b7a3b5fe4f1376887184045fcf45c69e92af734b7aaddc05fb777b6fbd03"
|
||||
|
||||
[[package]]
|
||||
name = "stable_deref_trait"
|
||||
version = "1.2.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "a8f112729512f8e442d81f95a8a7ddf2b7c6b8a1a6f509a95864142b30cab2d3"
|
||||
|
||||
[[package]]
|
||||
name = "syn"
|
||||
version = "2.0.106"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "ede7c438028d4436d71104916910f5bb611972c5cfd7f89b8300a8186e6fada6"
|
||||
dependencies = [
|
||||
"proc-macro2",
|
||||
"quote",
|
||||
"unicode-ident",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "synstructure"
|
||||
version = "0.13.2"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "728a70f3dbaf5bab7f0c4b1ac8d7ae5ea60a4b5549c8a5914361c99147a709d2"
|
||||
dependencies = [
|
||||
"proc-macro2",
|
||||
"quote",
|
||||
"syn",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "tinystr"
|
||||
version = "0.8.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "5d4f6d1145dcb577acf783d4e601bc1d76a13337bb54e6233add580b07344c8b"
|
||||
dependencies = [
|
||||
"displaydoc",
|
||||
"zerovec",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "unicode-ident"
|
||||
version = "1.0.18"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "5a5f39404a5da50712a4c1eecf25e90dd62b613502b7e925fd4e4d19b5c96512"
|
||||
|
||||
[[package]]
|
||||
name = "url"
|
||||
version = "2.5.7"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "08bc136a29a3d1758e07a9cca267be308aeebf5cfd5a10f3f67ab2097683ef5b"
|
||||
dependencies = [
|
||||
"form_urlencoded",
|
||||
"idna",
|
||||
"percent-encoding",
|
||||
"serde",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "utf8_iter"
|
||||
version = "1.0.4"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "b6c140620e7ffbb22c2dee59cafe6084a59b5ffc27a8859a5f0d494b5d52b6be"
|
||||
|
||||
[[package]]
|
||||
name = "vcpkg"
|
||||
version = "0.2.15"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "accd4ea62f7bb7a82fe23066fb0957d48ef677f6eeb8215f372f52e48bb32426"
|
||||
|
||||
[[package]]
|
||||
name = "wasi"
|
||||
version = "0.14.4+wasi-0.2.4"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "88a5f4a424faf49c3c2c344f166f0662341d470ea185e939657aaff130f0ec4a"
|
||||
dependencies = [
|
||||
"wit-bindgen",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "wasm-bindgen"
|
||||
version = "0.2.101"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "7e14915cadd45b529bb8d1f343c4ed0ac1de926144b746e2710f9cd05df6603b"
|
||||
dependencies = [
|
||||
"cfg-if",
|
||||
"once_cell",
|
||||
"rustversion",
|
||||
"wasm-bindgen-macro",
|
||||
"wasm-bindgen-shared",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "wasm-bindgen-backend"
|
||||
version = "0.2.101"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "e28d1ba982ca7923fd01448d5c30c6864d0a14109560296a162f80f305fb93bb"
|
||||
dependencies = [
|
||||
"bumpalo",
|
||||
"log",
|
||||
"proc-macro2",
|
||||
"quote",
|
||||
"syn",
|
||||
"wasm-bindgen-shared",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "wasm-bindgen-macro"
|
||||
version = "0.2.101"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "7c3d463ae3eff775b0c45df9da45d68837702ac35af998361e2c84e7c5ec1b0d"
|
||||
dependencies = [
|
||||
"quote",
|
||||
"wasm-bindgen-macro-support",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "wasm-bindgen-macro-support"
|
||||
version = "0.2.101"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "7bb4ce89b08211f923caf51d527662b75bdc9c9c7aab40f86dcb9fb85ac552aa"
|
||||
dependencies = [
|
||||
"proc-macro2",
|
||||
"quote",
|
||||
"syn",
|
||||
"wasm-bindgen-backend",
|
||||
"wasm-bindgen-shared",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "wasm-bindgen-shared"
|
||||
version = "0.2.101"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "f143854a3b13752c6950862c906306adb27c7e839f7414cec8fea35beab624c1"
|
||||
dependencies = [
|
||||
"unicode-ident",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "windows-core"
|
||||
version = "0.61.2"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "c0fdd3ddb90610c7638aa2b3a3ab2904fb9e5cdbecc643ddb3647212781c4ae3"
|
||||
dependencies = [
|
||||
"windows-implement",
|
||||
"windows-interface",
|
||||
"windows-link",
|
||||
"windows-result",
|
||||
"windows-strings",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "windows-implement"
|
||||
version = "0.60.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "a47fddd13af08290e67f4acabf4b459f647552718f683a7b415d290ac744a836"
|
||||
dependencies = [
|
||||
"proc-macro2",
|
||||
"quote",
|
||||
"syn",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "windows-interface"
|
||||
version = "0.59.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "bd9211b69f8dcdfa817bfd14bf1c97c9188afa36f4750130fcdf3f400eca9fa8"
|
||||
dependencies = [
|
||||
"proc-macro2",
|
||||
"quote",
|
||||
"syn",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "windows-link"
|
||||
version = "0.1.3"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "5e6ad25900d524eaabdbbb96d20b4311e1e7ae1699af4fb28c17ae66c80d798a"
|
||||
|
||||
[[package]]
|
||||
name = "windows-result"
|
||||
version = "0.3.4"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "56f42bd332cc6c8eac5af113fc0c1fd6a8fd2aa08a0119358686e5160d0586c6"
|
||||
dependencies = [
|
||||
"windows-link",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "windows-strings"
|
||||
version = "0.4.2"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "56e6c93f3a0c3b36176cb1327a4958a0353d5d166c2a35cb268ace15e91d3b57"
|
||||
dependencies = [
|
||||
"windows-link",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "wit-bindgen"
|
||||
version = "0.45.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "5c573471f125075647d03df72e026074b7203790d41351cd6edc96f46bcccd36"
|
||||
|
||||
[[package]]
|
||||
name = "writeable"
|
||||
version = "0.6.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "ea2f10b9bb0928dfb1b42b65e1f9e36f7f54dbdf08457afefb38afcdec4fa2bb"
|
||||
|
||||
[[package]]
|
||||
name = "yoke"
|
||||
version = "0.8.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "5f41bb01b8226ef4bfd589436a297c53d118f65921786300e427be8d487695cc"
|
||||
dependencies = [
|
||||
"serde",
|
||||
"stable_deref_trait",
|
||||
"yoke-derive",
|
||||
"zerofrom",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "yoke-derive"
|
||||
version = "0.8.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "38da3c9736e16c5d3c8c597a9aaa5d1fa565d0532ae05e27c24aa62fb32c0ab6"
|
||||
dependencies = [
|
||||
"proc-macro2",
|
||||
"quote",
|
||||
"syn",
|
||||
"synstructure",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "zerofrom"
|
||||
version = "0.1.6"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "50cc42e0333e05660c3587f3bf9d0478688e15d870fab3346451ce7f8c9fbea5"
|
||||
dependencies = [
|
||||
"zerofrom-derive",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "zerofrom-derive"
|
||||
version = "0.1.6"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "d71e5d6e06ab090c67b5e44993ec16b72dcbaabc526db883a360057678b48502"
|
||||
dependencies = [
|
||||
"proc-macro2",
|
||||
"quote",
|
||||
"syn",
|
||||
"synstructure",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "zerotrie"
|
||||
version = "0.2.2"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "36f0bbd478583f79edad978b407914f61b2972f5af6fa089686016be8f9af595"
|
||||
dependencies = [
|
||||
"displaydoc",
|
||||
"yoke",
|
||||
"zerofrom",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "zerovec"
|
||||
version = "0.11.4"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "e7aa2bd55086f1ab526693ecbe444205da57e25f4489879da80635a46d90e73b"
|
||||
dependencies = [
|
||||
"yoke",
|
||||
"zerofrom",
|
||||
"zerovec-derive",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "zerovec-derive"
|
||||
version = "0.11.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "5b96237efa0c878c64bd89c436f661be4e46b2f3eff1ebb976f7ef2321d2f58f"
|
||||
dependencies = [
|
||||
"proc-macro2",
|
||||
"quote",
|
||||
"syn",
|
||||
]
|
||||
19
Cargo.toml
Normal file
19
Cargo.toml
Normal file
@@ -0,0 +1,19 @@
|
||||
[package]
|
||||
name = "eider"
|
||||
version = "0.1.0"
|
||||
edition = "2024"
|
||||
|
||||
[dependencies]
|
||||
anyhow = "1.0.99"
|
||||
chrono = "0.4.41"
|
||||
fastcgi = "1.0.0"
|
||||
git2 = "0.20.2"
|
||||
hex = "0.4.3"
|
||||
|
||||
[[bin]]
|
||||
name = "eider-md"
|
||||
path = "src/eider-md/main.rs"
|
||||
|
||||
[[bin]]
|
||||
name = "eider-git"
|
||||
path = "src/eider-git/main.rs"
|
||||
19
LICENCE
Normal file
19
LICENCE
Normal file
@@ -0,0 +1,19 @@
|
||||
(C) 2025-2026 Olive <hello@grasswren.net>
|
||||
|
||||
Permission is hereby granted, free of charge, to any person obtaining a copy of
|
||||
this software and associated documentation files (the "Software"), to deal in
|
||||
the Software without restriction, including without limitation the rights to
|
||||
use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies
|
||||
of the Software, and to permit persons to whom the Software is furnished to do
|
||||
so, subject to the following conditions:
|
||||
|
||||
The above copyright notice and this permission notice shall be included in all
|
||||
copies or substantial portions of the Software.
|
||||
|
||||
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
||||
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
||||
SOFTWARE.
|
||||
343
src/eider-git/main.rs
Normal file
343
src/eider-git/main.rs
Normal file
@@ -0,0 +1,343 @@
|
||||
/* eider-git - git cgi
|
||||
* Copyright (C) 2025-2026 Olive <hello@grasswren.net>
|
||||
* see LICENCE file for licensing information */
|
||||
|
||||
use std::collections::HashMap;
|
||||
use std::io::Write as IoWrite;
|
||||
use std::fmt::Write as FmtWrite;
|
||||
use std::path::{Path, PathBuf};
|
||||
|
||||
use anyhow::{anyhow, Result};
|
||||
use chrono::{FixedOffset, TimeZone};
|
||||
use git2::{DiffFormat, ObjectType, Oid, Repository, Time, TreeWalkMode,
|
||||
TreeWalkResult};
|
||||
|
||||
fn escapeu8(content: &[u8]) -> String {
|
||||
let ret = String::from_utf8_lossy(&content);
|
||||
escape(&ret)
|
||||
}
|
||||
|
||||
fn escape(content: &str) -> String {
|
||||
let mut ret = String::new();
|
||||
for char in content.chars() {
|
||||
match char {
|
||||
'&' => ret.push_str("&"),
|
||||
'>' => ret.push_str(">"),
|
||||
'<' => ret.push_str("<"),
|
||||
'\'' => ret.push_str("'"),
|
||||
char => ret.push(char),
|
||||
}
|
||||
}
|
||||
ret
|
||||
}
|
||||
|
||||
fn date(time: Time) -> String {
|
||||
format!("{}", FixedOffset::east_opt(time.offset_minutes())
|
||||
.unwrap()
|
||||
.timestamp_opt(time.seconds(), 0)
|
||||
.unwrap()
|
||||
.format("%F"))
|
||||
}
|
||||
|
||||
fn output(
|
||||
desc: &str,
|
||||
link: &str,
|
||||
name: &str,
|
||||
tree: bool,
|
||||
content: String
|
||||
) -> Result<String> {
|
||||
let (title, desc) = desc.split_once(" - ").unwrap_or(("", desc));
|
||||
let mut ret = String::new();
|
||||
write!(ret, include_str!("../html/head.html"),
|
||||
escape(title), escape(desc), escape(title), escape(desc),
|
||||
"", "", "class='active' ")?;
|
||||
write!(ret, include_str!("../html/git.html"),
|
||||
title, link, name,
|
||||
if tree { "tree" } else { "log" },
|
||||
if tree { "class='active' " } else { "" },
|
||||
if !tree { "class='active' " } else { "" })?;
|
||||
ret.push_str(&content);
|
||||
ret.push_str(include_str!("../html/foot.html"));
|
||||
Ok(ret)
|
||||
}
|
||||
|
||||
fn landing(path: &mut PathBuf) -> Result<String> {
|
||||
let entries: Vec<PathBuf> = std::fs::read_dir(&path)?
|
||||
.into_iter()
|
||||
.filter(|r| r.is_ok()) /* ignore file errors */
|
||||
.map(|r| r.unwrap().path()) /* get the path */
|
||||
.collect();
|
||||
|
||||
struct Repo { repo: String, time: Time }
|
||||
let mut data: Vec<Repo> = Vec::new();
|
||||
for entry in entries {
|
||||
path.push(&entry);
|
||||
|
||||
let repo = Repository::open_bare(&path)?;
|
||||
let oid = repo.refname_to_id("HEAD")?;
|
||||
let commit = repo.find_commit(oid)?;
|
||||
|
||||
path.push("description");
|
||||
let desc = std::fs::read_to_string(&path)
|
||||
.unwrap_or(entry.to_string_lossy().to_string());
|
||||
data.push(Repo{ repo: desc, time: commit.time() });
|
||||
|
||||
path.pop();
|
||||
path.pop();
|
||||
}
|
||||
data.sort_unstable_by_key(|r| r.time);
|
||||
|
||||
let mut content = String::new();
|
||||
let title = "";
|
||||
let desc = "";
|
||||
write!(content, include_str!("../html/head.html"),
|
||||
title.split(" <").nth(0).unwrap(), escape(desc), title,
|
||||
escape(desc), "", "", "class='active' ")?;
|
||||
|
||||
content.push_str("<section>\n<ul class='two-col'>");
|
||||
for repo in data.iter().rev() {
|
||||
write!(content, concat!("<li>\n<time>{}</time>\n",
|
||||
"<a href='/{}/tree'>{}</a>\n</li>\n"), date(repo.time),
|
||||
repo.repo.split(' ').nth(0).unwrap(), repo.repo.trim())?;
|
||||
}
|
||||
content.push_str("</ul>\n");
|
||||
|
||||
write!(content, include_str!("../html/foot.html"))?;
|
||||
Ok(content)
|
||||
}
|
||||
|
||||
fn tree(repopath: &mut PathBuf, filepath: &PathBuf) -> Result<String> {
|
||||
let repo = Repository::open_bare(&repopath)?;
|
||||
let head_oid = repo.refname_to_id("HEAD")?;
|
||||
let commit = repo.find_commit(head_oid)?;
|
||||
|
||||
let reponame = repopath.file_name().unwrap().to_string_lossy().to_string();
|
||||
repopath.push("description");
|
||||
let desc = std::fs::read_to_string(&repopath).unwrap_or(reponame.clone());
|
||||
let filepathstr = format!("/{}", filepath.to_string_lossy());
|
||||
|
||||
let tree = 'br: {
|
||||
let tree = commit.tree()?;
|
||||
if filepath == Path::new("") {
|
||||
break 'br tree;
|
||||
}
|
||||
|
||||
let entry = tree.get_path(filepath)?;
|
||||
if entry.kind() == Some(ObjectType::Tree) {
|
||||
break 'br repo.find_tree(entry.id())?;
|
||||
}
|
||||
|
||||
let blob = repo.find_blob(entry.id())?;
|
||||
return output(&desc, &filepathstr[1..], &filepathstr, true,
|
||||
format!("<pre><code>{}</code></pre>\n", escapeu8(blob.content())));
|
||||
};
|
||||
|
||||
struct File { name: Vec<u8>, id: Oid, size: usize, binary: bool }
|
||||
let mut files: Vec<File> = Vec::new();
|
||||
let mut dirs: HashMap<Vec<u8>, usize> = HashMap::new();
|
||||
tree.walk(TreeWalkMode::PreOrder, |dir, entry| {
|
||||
if dir.len() != 0 {
|
||||
dirs.entry(dir.into()).and_modify(|c| { *c += 1 }).or_insert(1);
|
||||
return TreeWalkResult::Skip;
|
||||
}
|
||||
|
||||
if entry.kind() == Some(ObjectType::Tree) {
|
||||
if *entry.name_bytes().iter().last().unwrap() == b'/' {
|
||||
dirs.insert(entry.name_bytes().to_owned(), 0);
|
||||
}
|
||||
return TreeWalkResult::Ok;
|
||||
}
|
||||
|
||||
let blob = repo.find_blob(entry.id()).unwrap();
|
||||
files.push(File{
|
||||
name: entry.name_bytes().to_owned(),
|
||||
id: entry.id(),
|
||||
size: if blob.is_binary() {
|
||||
blob.size()
|
||||
} else {
|
||||
blob.content().iter().filter(|c| **c == b'\n').count()
|
||||
},
|
||||
binary: blob.is_binary(),
|
||||
});
|
||||
return TreeWalkResult::Ok;
|
||||
})?;
|
||||
|
||||
let mut times: HashMap<Oid, (Time, Oid)> = HashMap::new();
|
||||
let mut indeces: Vec<usize> = (0..files.len()).collect();
|
||||
let mut save = head_oid;
|
||||
let mut time = commit.time();
|
||||
|
||||
let mut revwalk = repo.revwalk()?;
|
||||
revwalk.push_head()?;
|
||||
while let Some(Ok(oid)) = revwalk.next() {
|
||||
let commit = repo.find_commit(oid)?;
|
||||
let tree = commit.tree()?;
|
||||
|
||||
indeces.retain(|index| {
|
||||
let file = &files[*index];
|
||||
if let Some(entry) = tree.get_id(file.id) {
|
||||
if file.name == entry.name_bytes() && file.id == entry.id() {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
times.insert(file.id, (time, oid));
|
||||
false
|
||||
});
|
||||
|
||||
if indeces.len() == 0 {
|
||||
break;
|
||||
}
|
||||
time = commit.time();
|
||||
save = oid;
|
||||
}
|
||||
for index in indeces {
|
||||
times.insert(files[index].id, (time, save));
|
||||
}
|
||||
|
||||
let mut content = String::new();
|
||||
content.push_str("<ul class='three-col'>\n");
|
||||
let mut dirs: Vec<(Vec<u8>, usize)> = dirs.into_iter().collect();
|
||||
dirs.sort_unstable_by_key(|t| t.1);
|
||||
for (dir, size) in dirs {
|
||||
write!(content, concat!("<li>\n<time></time>\n",
|
||||
"<a href='/{0}/tree{1}{2}'>{2}</a>\n<p>{3} files</p>\n</li>\n"),
|
||||
escape(&reponame), escape(&filepathstr), escapeu8(&dir), size)?;
|
||||
}
|
||||
for file in files {
|
||||
write!(content, concat!("<li>\n",
|
||||
"<a href='/{0}/log/{5}'><time>{6}</time></a>\n",
|
||||
"<a href='/{0}/tree{1}{2}'>{2}</a>\n<p>{3} {4}</p>\n</li>\n"),
|
||||
escape(&reponame), escape(&filepathstr), escapeu8(&file.name),
|
||||
file.size, if file.binary { "bytes" } else { "lines" },
|
||||
hex::encode(times.get(&file.id).unwrap().1.as_bytes()),
|
||||
date(times.get(&file.id).unwrap().0))?;
|
||||
}
|
||||
content.push_str("</ul>\n");
|
||||
|
||||
output(&desc, &filepathstr[1..], &filepathstr, true, content)
|
||||
}
|
||||
|
||||
fn log(repopath: &mut PathBuf) -> Result<String> {
|
||||
let repo = Repository::open_bare(&repopath)?;
|
||||
let mut revwalk = repo.revwalk()?;
|
||||
revwalk.push_head()?;
|
||||
|
||||
let reponame = repopath.file_name().unwrap().to_string_lossy().to_string();
|
||||
repopath.push("description");
|
||||
let desc = std::fs::read_to_string(&repopath).unwrap_or(reponame.clone());
|
||||
|
||||
let mut content = String::new();
|
||||
content.push_str("<ul class='three-col'>");
|
||||
while let Some(Ok(oid)) = revwalk.next() {
|
||||
let commit = repo.find_commit(oid)?;
|
||||
let author = commit.author();
|
||||
write!(content, concat!("<li>\n<time>{}</time>\n",
|
||||
"<a href='/{}/log/{}'>{}</a>\n<a href='mailto:{}'>{}</a>\n</li>\n"),
|
||||
date(commit.time()), escape(&reponame), hex::encode(oid.as_bytes()),
|
||||
escapeu8(commit.summary_bytes().unwrap()),
|
||||
escapeu8(author.email_bytes()), escapeu8(author.name_bytes()))?;
|
||||
}
|
||||
content.push_str("</ul>\n");
|
||||
|
||||
output(&desc, "", "", false, content)
|
||||
}
|
||||
|
||||
fn commit(repopath: &mut PathBuf, id: &str) -> Result<String> {
|
||||
let repo = Repository::open_bare(&repopath)?;
|
||||
let oid = Oid::from_str(id)?;
|
||||
let commit = repo.find_commit(oid)?;
|
||||
|
||||
let reponame = repopath.file_name().unwrap().to_string_lossy().to_string();
|
||||
repopath.push("description");
|
||||
let desc = std::fs::read_to_string(&repopath).unwrap_or(reponame.clone());
|
||||
|
||||
let mut content = String::new();
|
||||
write!(content, "<h2>{}</h2>\n", escapeu8(commit.summary_bytes().unwrap()))?;
|
||||
|
||||
content.push_str("<ul class='two-col'>\n");
|
||||
let author = commit.author();
|
||||
let committer = commit.committer();
|
||||
write!(content, concat!("<li>\n<time>{}</time>",
|
||||
"<p>Author: <a href='mailto:{}'>{}</a></p>\n</li>\n"),
|
||||
date(author.when()),
|
||||
escapeu8(author.email_bytes()), escapeu8(author.name_bytes()))?;
|
||||
write!(content, concat!("<li>\n<time>{}</time>",
|
||||
"<p>Committer: <a href='mailto:{}'>{}</a></p>\n</li>\n"),
|
||||
date(committer.when()),
|
||||
escapeu8(committer.email_bytes()), escapeu8(committer.name_bytes()))?;
|
||||
content.push_str("</ul>");
|
||||
|
||||
write!(content, "<p>{}</p>\n",
|
||||
escapeu8(commit.body_bytes().unwrap_or(b"")))?;
|
||||
|
||||
content.push_str("<ul class='two-col'>\n");
|
||||
for parent in commit.parents() {
|
||||
write!(content, concat!("<li>\n<time>{}</time>\n",
|
||||
"<a href='/{}/log/{}'>{}</a>\n</li>\n"),
|
||||
date(parent.time()), escape(&reponame),
|
||||
hex::encode(parent.id().as_bytes()),
|
||||
escapeu8(parent.summary_bytes().unwrap()))?;
|
||||
|
||||
let diffs = repo.diff_tree_to_tree(
|
||||
Some(&repo.find_tree(parent.tree_id())?),
|
||||
Some(&repo.find_tree(commit.tree_id())?),
|
||||
None
|
||||
);
|
||||
content.push_str("<pre><code>");
|
||||
for diff in diffs.iter() {
|
||||
diff.print(DiffFormat::Patch, |_, _, line| {
|
||||
write!(content, "{}{}",
|
||||
if line.origin() == '+' || line.origin() == '-' {
|
||||
line.origin().to_string()
|
||||
} else {
|
||||
"".to_string()
|
||||
}, escapeu8(line.content())).unwrap();
|
||||
true
|
||||
})?;
|
||||
}
|
||||
}
|
||||
content.push_str("</ul>\n");
|
||||
|
||||
output(&desc, &hex::encode(commit.id().as_bytes()),
|
||||
&format!("<time>{}</time>", date(commit.time())), false, content)
|
||||
}
|
||||
|
||||
fn run(uri: &str) -> Result<String> {
|
||||
let mut path = PathBuf::from("/var/git");
|
||||
if uri == "" {
|
||||
return landing(&mut path);
|
||||
}
|
||||
|
||||
let (repo, cmd, args) = match uri.split_once('/') {
|
||||
None => (uri, "tree", ""),
|
||||
Some((repo, cmd)) => match cmd.split_once('/') {
|
||||
None => (repo, cmd, ""),
|
||||
Some((cmd, args)) => (repo, cmd, args),
|
||||
}
|
||||
};
|
||||
path.push(repo);
|
||||
|
||||
match cmd {
|
||||
"tree" => tree(&mut path, &PathBuf::from(args)),
|
||||
"log" => if args.len() == 0 {
|
||||
log(&mut path)
|
||||
} else {
|
||||
commit(&mut path, args)
|
||||
}
|
||||
&_ => Err(anyhow!("Invalid command argument")),
|
||||
}
|
||||
}
|
||||
|
||||
fn main() -> Result<()> {
|
||||
let listener = std::net::TcpListener::bind("localhost:9999")?;
|
||||
fastcgi::run_tcp(|mut req| {
|
||||
let uri = &req.param("URI").unwrap()[1..];
|
||||
match run(uri) {
|
||||
Ok(val) => { let _ = write!(&mut req.stdout(),
|
||||
"Content-Type: text/html\r\n\r\n{}", val); }
|
||||
Err(err) => { let _ = write!(&mut req.stdout(),
|
||||
"Status: 500\r\n\r\n{:?}", err); }
|
||||
}
|
||||
}, &listener);
|
||||
Ok(())
|
||||
}
|
||||
311
src/eider-md/main.rs
Normal file
311
src/eider-md/main.rs
Normal file
@@ -0,0 +1,311 @@
|
||||
/* eider-md - markdown processor
|
||||
* Copyright (C) 2025-2026 Olive <hello@grasswren.net>
|
||||
* see LICENCE file for licensing information */
|
||||
|
||||
use std::error::Error;
|
||||
use std::fmt::Write as FmtWrite;
|
||||
use std::fs::File;
|
||||
use std::path::PathBuf;
|
||||
use std::io::{BufRead, BufReader, BufWriter, Lines, Write as IoWrite};
|
||||
|
||||
struct Metadata {
|
||||
file: String,
|
||||
title: String,
|
||||
desc: String,
|
||||
date: String,
|
||||
}
|
||||
|
||||
enum Mode { None, Code, List, Description }
|
||||
#[derive(PartialEq)]
|
||||
enum State { None, Code, Link, Info, Image }
|
||||
#[derive(PartialEq)]
|
||||
enum Target { None, ExpectOne, One, ExpectTwo, Two }
|
||||
|
||||
fn escape(line: &str) -> String {
|
||||
let mut ret = String::new();
|
||||
for char in line.chars() {
|
||||
match char {
|
||||
'&' => ret.push_str("&"),
|
||||
'>' => ret.push_str(">"),
|
||||
'<' => ret.push_str("<"),
|
||||
'\'' => ret.push_str("'"),
|
||||
char => ret.push(char),
|
||||
}
|
||||
}
|
||||
ret
|
||||
}
|
||||
|
||||
fn nohtml(line: &str) -> String {
|
||||
let mut print = true;
|
||||
line.chars().filter(|&c| {
|
||||
match c {
|
||||
'<' => print = false,
|
||||
'>' => print = true,
|
||||
_ => return print,
|
||||
}
|
||||
false
|
||||
}).collect::<String>()
|
||||
}
|
||||
|
||||
fn kind(state: State, info: &String, link: &String) -> Result<String, Box<dyn Error>> {
|
||||
let mut ret = String::new();
|
||||
match state {
|
||||
State::Link => write!(ret, "<a href='{}'{}>{}</a>", link,
|
||||
if link.contains("https://grasswren.net") { "" }
|
||||
else { " target='_blank'" }, info)?,
|
||||
State::Info => write!(ret, "<span class='n' title='{}'>{}</span>",
|
||||
link, info)?,
|
||||
State::Image => {
|
||||
let (_, ext) = link.rsplit_once('.')
|
||||
.expect("image file has no extension");
|
||||
match ext {
|
||||
"webp" => write!(ret, "<img src='{0}' alt='{1}' title='{1}'>",
|
||||
link, info)?,
|
||||
"mp4" => write!(ret, "<video controls>\n<source src='{}' \
|
||||
type='video/mp4' alt='{}'>\n</video>", link, info)?,
|
||||
"pdf" => write!(ret, "<object data='{0}' type='application\
|
||||
/pdf'>\n<a href='{0}'>{1}</a>\n</object>", link, info)?,
|
||||
&_ => todo!(),
|
||||
}
|
||||
}
|
||||
_ => (),
|
||||
}
|
||||
Ok(ret)
|
||||
}
|
||||
|
||||
fn text(line: &str) -> Result<String, Box<dyn Error>> {
|
||||
let mut state = State::None;
|
||||
let mut target = Target::None;
|
||||
let mut escape = false;
|
||||
|
||||
let mut ret = String::new();
|
||||
let mut hold0 = String::new();
|
||||
let mut hold1 = String::new();
|
||||
let mut hold2 = String::new();
|
||||
|
||||
for char in line.chars() {
|
||||
match char {
|
||||
'@' if state == State::None && !escape => {
|
||||
state = State::Link;
|
||||
target = Target::ExpectOne;
|
||||
hold0.push('@');
|
||||
}
|
||||
'^' if state == State::None && !escape => {
|
||||
state = State::Info;
|
||||
target = Target::ExpectOne;
|
||||
hold0.push('^');
|
||||
}
|
||||
'!' if state == State::None && !escape => {
|
||||
state = State::Image;
|
||||
target = Target::ExpectOne;
|
||||
hold0.push('!');
|
||||
}
|
||||
|
||||
'{' if state != State::None && target == Target::ExpectOne => {
|
||||
hold0.push('{');
|
||||
target = Target::One;
|
||||
}
|
||||
'}' if state != State::None && target == Target::One && !escape => {
|
||||
hold0.push('}');
|
||||
target = Target::ExpectTwo;
|
||||
}
|
||||
'{' if state != State::None && target == Target::ExpectTwo => {
|
||||
hold0.push('{');
|
||||
target = Target::Two;
|
||||
}
|
||||
'}' if state != State::None && target == Target::Two && !escape => {
|
||||
write!(ret, "{}", kind(state, &hold1, &hold2)?)?;
|
||||
|
||||
hold0.clear();
|
||||
hold1.clear();
|
||||
hold2.clear();
|
||||
state = State::None;
|
||||
target = Target::None;
|
||||
}
|
||||
|
||||
'`' if state == State::None && !escape => {
|
||||
ret.push_str("<code>");
|
||||
state = State::Code;
|
||||
}
|
||||
'`' if state == State::Code && !escape => {
|
||||
ret.push_str("</code>");
|
||||
state = State::None;
|
||||
}
|
||||
|
||||
char => {
|
||||
match char {
|
||||
'\\' if !escape => { escape = true; continue; }
|
||||
'&' => hold0.push_str("&"),
|
||||
'>' => hold0.push_str(">"),
|
||||
'<' => hold0.push_str("<"),
|
||||
'\'' => hold0.push_str("'"),
|
||||
char => hold0.push(char),
|
||||
}
|
||||
|
||||
match target {
|
||||
Target::One => hold1.push(char),
|
||||
Target::Two => hold2.push(char),
|
||||
_ => {
|
||||
ret.push_str(&hold0);
|
||||
|
||||
hold0.clear();
|
||||
hold1.clear();
|
||||
hold2.clear();
|
||||
if state != State::Code {
|
||||
state = State::None;
|
||||
}
|
||||
target = Target::None;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
escape = false;
|
||||
}
|
||||
|
||||
Ok(ret)
|
||||
}
|
||||
|
||||
fn file(
|
||||
file: &str,
|
||||
lines: &mut Lines<BufReader<File>>,
|
||||
output: &mut BufWriter<File>
|
||||
) -> Result<Metadata, Box<dyn Error>> {
|
||||
let title = lines.next().unwrap().unwrap();
|
||||
let desc = text(&lines.next().unwrap().unwrap())?;
|
||||
let date = lines.next().unwrap().unwrap();
|
||||
let (home, blog) = if file == "index.md" {
|
||||
("class='active' ", "")
|
||||
} else {
|
||||
("", "class='active' ")
|
||||
};
|
||||
|
||||
let htitle = title.split(" <").nth(0).unwrap();
|
||||
let hdesc = nohtml(&desc);
|
||||
|
||||
write!(output, include_str!("../html/head.html"), htitle, hdesc, title, desc,
|
||||
home, blog, "")?;
|
||||
write!(output, "<section>\n")?;
|
||||
|
||||
let mut mode = Mode::None;
|
||||
while let Some(Ok(line)) = lines.next() {
|
||||
match mode {
|
||||
Mode::None => (),
|
||||
Mode::List if line.starts_with("- ") => {
|
||||
writeln!(output, "<li>{}</li>", text(&line[2..])?)?;
|
||||
continue;
|
||||
}
|
||||
Mode::List => {
|
||||
writeln!(output, "</ul>")?;
|
||||
mode = Mode::None;
|
||||
}
|
||||
Mode::Description if line.starts_with("~ ") => {
|
||||
writeln!(output, "<li>{}</li>", text(&line[2..])?)?;
|
||||
continue;
|
||||
}
|
||||
Mode::Description => {
|
||||
writeln!(output, "</ul>")?;
|
||||
mode = Mode::None;
|
||||
}
|
||||
Mode::Code if line != "```" => {
|
||||
writeln!(output, "{}", escape(&line))?;
|
||||
continue;
|
||||
}
|
||||
Mode::Code => {
|
||||
writeln!(output, "</code></pre>")?;
|
||||
mode = Mode::None;
|
||||
continue;
|
||||
}
|
||||
}
|
||||
|
||||
match line {
|
||||
line if line.starts_with("# ") =>
|
||||
writeln!(output, "<h2>{}</h2>", text(&line[2..])?)?,
|
||||
line if line.starts_with("## ") =>
|
||||
writeln!(output, "<h3>{}</h3>", text(&line[3..])?)?,
|
||||
line if line.starts_with("- ") => {
|
||||
writeln!(output, "<ul class='l'>\n<li>{}</li>",
|
||||
text(&line[2..])?)?;
|
||||
mode = Mode::List;
|
||||
}
|
||||
line if line.starts_with("~ ") => {
|
||||
writeln!(output, "<ul>\n<li>{}</li>", text(&line[2..])?)?;
|
||||
mode = Mode::Description;
|
||||
}
|
||||
line if line.starts_with("```") => {
|
||||
write!(output, "<pre><code class='language-{}'>",
|
||||
text(&line[3..])?)?;
|
||||
mode = Mode::Code;
|
||||
}
|
||||
line => if line.len() == 0 {
|
||||
writeln!(output, "</section>\n<section>")?;
|
||||
} else if line.starts_with("!{") {
|
||||
writeln!(output, "<div>{}</div>", text(&line)?)?;
|
||||
} else {
|
||||
writeln!(output, "<p>{}</p>", text(&line)?)?;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
write!(output, include_str!("../html/foot.html"))?;
|
||||
|
||||
Ok(Metadata {
|
||||
file: file.to_string(),
|
||||
title: title,
|
||||
desc: desc,
|
||||
date: date,
|
||||
})
|
||||
}
|
||||
|
||||
fn main() -> Result<(), Box<dyn Error>> {
|
||||
let mut mddir = PathBuf::from("/olive/code/eider/src/md");
|
||||
let mut htmldir = PathBuf::from("/olive/code/eider/src/out");
|
||||
let entries: Vec<PathBuf> = std::fs::read_dir(&mddir)
|
||||
.unwrap()
|
||||
.into_iter()
|
||||
.filter(|e| e.is_ok())
|
||||
.map(|e| e.unwrap().path())
|
||||
.collect();
|
||||
|
||||
let mut metadata: Vec<Metadata> = Vec::new();
|
||||
for entry in entries {
|
||||
mddir.push(&entry.file_name().unwrap());
|
||||
htmldir.push(&entry.file_name().unwrap());
|
||||
htmldir.set_extension("html");
|
||||
|
||||
let input = File::open(&mddir).unwrap();
|
||||
let output = File::create(&htmldir).unwrap();
|
||||
let reader = BufReader::new(input);
|
||||
let mut lines = reader.lines();
|
||||
let mut writer = BufWriter::new(output);
|
||||
|
||||
mddir.pop();
|
||||
htmldir.pop();
|
||||
|
||||
let filename = entry.file_name().unwrap().to_string_lossy().to_string();
|
||||
let meta = file(&filename, &mut lines, &mut writer)?;
|
||||
if filename != "index.md" {
|
||||
metadata.push(meta);
|
||||
}
|
||||
}
|
||||
|
||||
htmldir.push("blog.html");
|
||||
let output = File::create(htmldir).unwrap();
|
||||
let mut writer = BufWriter::new(output);
|
||||
|
||||
let title = "Olive's Blog <i>lipu_pi_soweli_kili</i>";
|
||||
let desc = "Posts about my hobbies and projects";
|
||||
write!(writer, include_str!("../html/head.html"),
|
||||
title.split(" <").nth(0).unwrap(),
|
||||
escape(desc), title, escape(desc), "", "class='active' ", "")?;
|
||||
writeln!(writer, "<section>\n<ul class='two-col'>")?;
|
||||
for meta in metadata {
|
||||
writeln!(writer,
|
||||
"<li title='{}'>\n<time>{}</time>\n<a href='/{}'>{}</a>\n</li>",
|
||||
escape(&nohtml(&meta.desc)), meta.date,
|
||||
meta.file.split('.').nth(0).unwrap(), escape(&meta.title))?;
|
||||
}
|
||||
writeln!(writer, "</ul>")?;
|
||||
write!(writer, include_str!("../html/foot.html"))?;
|
||||
|
||||
Ok(())
|
||||
}
|
||||
8
src/html/foot.html
Normal file
8
src/html/foot.html
Normal file
@@ -0,0 +1,8 @@
|
||||
</section>
|
||||
<footer>
|
||||
<div><p>by <a href='mailto:hello@grasswren.net'>Olive</a> <small>(she/her)</small></p></div>
|
||||
<div><p>ΘΔ · 🏳️⚧️</p></div>
|
||||
</footer>
|
||||
</main>
|
||||
</body>
|
||||
</html>
|
||||
8
src/html/git.html
Normal file
8
src/html/git.html
Normal file
@@ -0,0 +1,8 @@
|
||||
<nav>
|
||||
<div><a href='/{0}/{3}/{1}'>{2}</a></div>
|
||||
<ul>
|
||||
<li><a {4}href='/{0}/tree'>Tree</a></li>
|
||||
<li><a {5}href='/{0}/log'>Log</a></li>
|
||||
</ul>
|
||||
</nav>
|
||||
<section>
|
||||
34
src/html/head.html
Normal file
34
src/html/head.html
Normal file
@@ -0,0 +1,34 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang='en'>
|
||||
<head>
|
||||
<meta charset='utf-8'>
|
||||
<title>{0}</title>
|
||||
<meta name='description' content='{1}'>
|
||||
<meta name='og:image' content='https://grasswren.net/espurr.webp'>
|
||||
<meta name='theme-color' content='#ab5aed' data-react-helmet='true'>
|
||||
<meta name='viewport' content='width=device-width'>
|
||||
<link rel='preconnect' href='https://rsms.me'>
|
||||
<link rel='stylesheet' href='https://rsms.me/inter/inter.css'>
|
||||
<link rel='stylesheet' href='https://grasswren.net/style.css'>
|
||||
<link rel='stylesheet' href='https://grasswren.net/hljs.css'>
|
||||
<link rel='icon' href='https://grasswren.net/espurr.webp'>
|
||||
<script src="https://cdnjs.cloudflare.com/ajax/libs/highlight.js/11.11.1/highlight.min.js"></script>
|
||||
<script>hljs.highlightAll();</script>
|
||||
</head>
|
||||
<body>
|
||||
<main>
|
||||
<nav>
|
||||
<div><a href='https://grasswren.net'>grasswren.net</a></div>
|
||||
<ul>
|
||||
<li><a {4}href='https://grasswren.net'>Home</a></li>
|
||||
<li><a {5}href='https://blog.grasswren.net'>Blog</a></li>
|
||||
<li><a {6}href='https://code.grasswren.net'>Code</a></li>
|
||||
</ul>
|
||||
</nav>
|
||||
<header>
|
||||
<div>
|
||||
<h1>{2}</h1>
|
||||
<p>{3}</p>
|
||||
</div>
|
||||
<img src='https://grasswren.net/espurr.webp' alt='espurr'>
|
||||
</header>
|
||||
19
src/md/index.md
Normal file
19
src/md/index.md
Normal file
@@ -0,0 +1,19 @@
|
||||
Olive's Website <i>lipu_pi_soweli_kili</i>
|
||||
Computer Engineer & @{Certified Punk Rocker}{https://youtu.be/z9LiPuVRyU8&t=261}
|
||||
|
||||
# About Me
|
||||
Howdy! My name is Olive, and this is my website! I host my some of my projects here (and the blog posts I write about them). I'm interested in low-level computing, FPGAs, radio communication, and analog synthesisers. When not tinkering with 'puters, I like to crochet, make jewellery, work leather, learn languages, listen to music, read, and watch movies.
|
||||
|
||||
# Technology
|
||||
Besides the follies which cause me to reinstall Arch Linux every few months, I do a pretty good job ordering 'puters around. Here is some of the software I've written:
|
||||
~ @{hive}{https://code.grasswren.net/Olive/hive} – Web-based code auto-grader for programming students
|
||||
~ @{rd}{https://code.grasswren.net/Olive/rd} – Ultra-light Linux privilege elevator for systems with few users
|
||||
~ @{arcane}{https://code.grasswren.net/Olive/arcane} – QMK firmware configuration for my split keyboard
|
||||
~ @{eider}{https://code.grasswren.net/Olive/eider} – CGI web program that displays git repositories
|
||||
~ @{hrtime}{https://code.grasswren.net/Olive/hrtime} – Form website for a medical research project
|
||||
For many years, I have programmed using C, shell, make, git, and the POSIX/Linux ABI. I also do server administration with Linux. Lately, I've picked up Rust for writing small programs and web backends with simple frontends in plain HTML/CSS.
|
||||
My dream job would be writing firmware in C or designing programs for FPGA (once I finally learn Verilog at university).
|
||||
|
||||
# Music
|
||||
My favourite genres of music are grunge, punk, hyperpop, breakcore, and digital hardcore. I really enjoy the music scene where I live, and I think it's critical that everybody gets out there and interacts with local artists in some capacity.
|
||||
Because I self-host my music library (and therefore don't have an algorithm), I rely on others to tell me what I should be listening to. If you have any suggestions, @{send me an email}{mailto:hello@grasswren.net}, and I'll be sure to let you know what I think.
|
||||
80
src/md/qmk.md
Normal file
80
src/md/qmk.md
Normal file
@@ -0,0 +1,80 @@
|
||||
Designing Custom Keyboard Backlighting
|
||||
Using @{QMK}{https://github.com/qmk/qmk_firmware} to update the firmware on my @{ErgoDox EZ}{https://ergodox-ez.com/}
|
||||
2024-02-19
|
||||
# Intro
|
||||
I own an Ergodox EZ, which is a split, ^{columnar}{the key columns are arranged without any horizontal offset} keyboard. It does an even better job precluding RSI in my wrists than my old single-board split Microsoft keyboard did. Ergodox EZ keyboards also run open-source QMK firmware, meaning they are extensively configurable! I love my keyboard, but everybody laughs at how silly it looks. :(
|
||||
The firmware I designed for my Ergodox EZ implements a flashy reactive backlighting and has a keymap designed for @{C programming}{https://en.wikipedia.org/wiki/C_(programming_language)} with @{Neovim}{https://neovim.io/}.
|
||||
|
||||
# Key Layout & Functions
|
||||
I use @{QWERTY}{https://en.wikipedia.org/wiki/QWERTY} but with added thumb keys:
|
||||
!{Keyboard Layout}{https://grasswren.net/layout.webp}
|
||||
Escape is used to ^{change modes}{Neovim is a modal editor, so being able to switch between modes quickly is paramount for efficiency} in Neovim, so it sits in an easily accessible spot. I use @{snake case}{https://en.wikipedia.org/wiki/Snake_case} for variables and types in C, so the extra underscore key next to space makes typing variables much faster. That key with @{Tux}{https://en.wikipedia.org/wiki/Tux_(mascot)} on it is equivalent to the Windows key.
|
||||
My ^{function keys}{F1 through F12} use QMK's @{tap dance}{https://github.com/qmk/qmk_firmware/blob/master/docs/feature_tap_dance.md} functionality. I've configured them to output a different key when pressed once, twice quickly, or thrice quickly to use fewer keys for less common keycodes.
|
||||
Opposite the underscore key sits a macro key that outputs `->`, which is a common token in C for accessing `struct` and `union` members through a pointer. When I press the macro key, the `MACRO` keycode is sent, and the `process_record_user` function shown below intercepts it and uses the `SEND_STRING()` macro to output `->`.
|
||||
```c
|
||||
switch (keycode) {
|
||||
case KC_BSPC:
|
||||
if (last)
|
||||
unregister_code(KC_BSPC), register_code(KC_BSPC);
|
||||
break;
|
||||
case MACRO:
|
||||
SEND_STRING(SS_TAP(X_MINUS) SS_LSFT(SS_TAP(X_DOT)));
|
||||
break;
|
||||
case DISCO:
|
||||
disco = !disco, lkeys = rkeys = 0;
|
||||
}
|
||||
last = (keycode == MACRO);
|
||||
```
|
||||
This function looks for two other keycodes: `KC_BSPC` (backspace) and `DISCO`. When `MACRO` was the last keycode sent, backspace is released and pressed an extra time to completely erase the `->` token. The `DISCO` keycode intuitively toggles disco mode.
|
||||
|
||||
# Disco Mode
|
||||
Solid or breathing backlighting is totally pedestrian, so I implemented my own, special kind of backlighting! It follows the following specification, with each keyboard half working independently of the other:
|
||||
- Every key press changes the backlight colour
|
||||
- When all keys are released, the backlights fade out in a nice way
|
||||
- Backlight LEDs closer to the key pressed most recently will be brighter than those further away
|
||||
Tracking key presses makes it easy to know when colours should be changed and lights should be faded out. Determining the @{intensity}{https://en.wikipedia.org/wiki/Brightness} of each backlight at any given time is a little more challenging. Before I explain how we do that, here is the function that implements my specification:
|
||||
```c
|
||||
void
|
||||
post_process_record_user(uint16_t keycode, keyrecord_t *record)
|
||||
{
|
||||
if (!disco)
|
||||
return;
|
||||
|
||||
if (!record->event.pressed) {
|
||||
if (record->event.key.row < 7 && lkeys > 0)
|
||||
--lkeys;
|
||||
else if (record->event.key.row >= 7 && rkeys > 0)
|
||||
--rkeys;
|
||||
return;
|
||||
}
|
||||
|
||||
if (record->event.key.row < 7) {
|
||||
lhue = rand() % 256, lval = 255, ++lkeys, lmid = 15;
|
||||
if (record->event.key.col != 5)
|
||||
lmid = 28 - 2 * record->event.key.row;
|
||||
for (uint8_t i = RGBLED_NUM / 2; i < RGBLED_NUM; ++i)
|
||||
sethsv(lhue, 255, BASE(lval, i, lmid), &led[i]);
|
||||
} else {
|
||||
rhue = rand() % 256, rval = 255, ++rkeys, rmid = 14;
|
||||
if (record->event.key.col != 5)
|
||||
rmid = 27 - 2 * record->event.key.row;
|
||||
for (uint8_t i = 0; i < RGBLED_NUM / 2; ++i)
|
||||
sethsv(rhue, 255, BASE(rval, i, rmid), &led[i]);
|
||||
}
|
||||
rgblight_set();
|
||||
}
|
||||
```
|
||||
The first two if blocks handle disco mode being disabled and key release tracking respectively. The final if block handles key presses and is duplicated for each side of the keyboard. The first line randomly chooses the new colour (`lhue = rand() % 255`), sets the base intensity (`lval = 255`), and increments the key press tracker (`++lkeys`). The second and third line apply a linear transformation to the position of the key pressed to get the index of the closest LED. The fourth and fifth lines choose the colour of all the backlights. Finally, `rgblight_set()` is run, which actually displays the chosen colours.
|
||||
## Distance Scaling & Cubic Fade
|
||||
The `BASE` macro does a lot of heavy lifting. It scales each LED's intensity by its distance to the key pressed and applies a function to fade it non-linearly. Here is the macro:
|
||||
```c
|
||||
#define BASE(val, ind, mid) ((uint32_t)(val) * (val) * (val) / 255 / 255) * \
|
||||
5 / (((ind) > (mid) ? (ind) - (mid) : (mid) - (ind)) + 5)
|
||||
```
|
||||
The human perception of light is not linear, but rather follows a @{logarithmic scale}{https://en.wikipedia.org/wiki/Weber%E2%80%93Fechner_law}. This means that linear fading doesn't look great to the human eye, so I scale the intensity using a @{cubic ease-out function}{https://easings.net/#easeOutCubic}: `f(x) = x^3 / 65025` (I divide by 255 squared to move the invariant points of the function as the maximum intensity is 255).
|
||||
Taking the difference between the index of the current LED (`ind`) and the closest LED (`mid`) and plugging it into the function `f(x) = a / (x + a)` gives a scaled version of `x` that decreases nicely as `x` grows. `a` is set to 5 in my implementation of `BASE`, but can be decreased for sharper scaling.
|
||||
|
||||
# Result
|
||||
With a few counters and some math, the backlights react to key presses on each side of the board, fading nicely when all keys are released.
|
||||
Here is the firmware in action:
|
||||
!{Custom Keyboard Backlighting in Action}{https://grasswren.net/keyboard.mp4}
|
||||
74
src/style/hljs.css
Normal file
74
src/style/hljs.css
Normal file
@@ -0,0 +1,74 @@
|
||||
:root {
|
||||
--co-pur: rgba(171, 90, 237, 1);
|
||||
--co-gre: rgba(130, 155, 137, 1);
|
||||
--co-ora: rgba(239, 111, 108, 1);
|
||||
--co-yel: rgba(236, 164, 0, 1);
|
||||
--co-red: rgba(96, 49, 64, 1);
|
||||
--co-spe: rgba(52, 52, 74, 1);
|
||||
}
|
||||
|
||||
.hljs-comment {
|
||||
color: var(--co-spe);
|
||||
}
|
||||
.hljs-punctuation,
|
||||
.hljs-tag {
|
||||
color: var(--co-red);
|
||||
}
|
||||
.hljs-tag .hljs-attr,
|
||||
.hljs-tag .hljs-name {
|
||||
color: var(--co-red);
|
||||
}
|
||||
.hljs-attribute,
|
||||
.hljs-doctag,
|
||||
.hljs-keyword,
|
||||
.hljs-meta .hljs-keyword,
|
||||
.hljs-name,
|
||||
.hljs-selector-tag {
|
||||
color: var(--co-yel);
|
||||
font-weight: 700;
|
||||
}
|
||||
.hljs-deletion,
|
||||
.hljs-number,
|
||||
.hljs-quote,
|
||||
.hljs-selector-class,
|
||||
.hljs-selector-id,
|
||||
.hljs-string,
|
||||
.hljs-template-tag,
|
||||
.hljs-type {
|
||||
color: var(--co-gre);
|
||||
}
|
||||
.hljs-section,
|
||||
.hljs-title {
|
||||
}
|
||||
.hljs-link,
|
||||
.hljs-operator,
|
||||
.hljs-regexp,
|
||||
.hljs-selector-attr,
|
||||
.hljs-selector-pseudo,
|
||||
.hljs-symbol,
|
||||
.hljs-template-variable,
|
||||
.hljs-variable {
|
||||
color: var(--co-pur);
|
||||
}
|
||||
.hljs-literal {
|
||||
color: var(--co-ora);
|
||||
}
|
||||
.hljs-addition,
|
||||
.hljs-built_in,
|
||||
.hljs-bullet,
|
||||
.hljs-code {
|
||||
color: var(--co-gre);
|
||||
}
|
||||
.hljs-meta {
|
||||
& * {
|
||||
color: var(--co-ora) !important;
|
||||
font-weight: 500 !important;
|
||||
}
|
||||
color: var(--co-ora);
|
||||
}
|
||||
.hljs-emphasis {
|
||||
font-style: italic;
|
||||
}
|
||||
.hljs-strong {
|
||||
font-weight: 700;
|
||||
}
|
||||
360
src/style/style.css
Normal file
360
src/style/style.css
Normal file
@@ -0,0 +1,360 @@
|
||||
:root {
|
||||
--bg-col: rgba(18, 15, 18, 1 );
|
||||
--co-col: rgba(9, 7, 9, 1 );
|
||||
--tx-col: rgba(255, 255, 255, 1 );
|
||||
--hl-col: rgba(171, 90, 237, 0.3);
|
||||
--pr-col: rgba(171, 90, 237, 1 );
|
||||
--pt-col: rgba(171, 90, 237, 0.8);
|
||||
--hv-col: rgba(194, 135, 242, 1 );
|
||||
--gr-col: rgba(32, 40, 34, 1 );
|
||||
|
||||
--content-width: 800px;
|
||||
--extra-width: 4em;
|
||||
}
|
||||
|
||||
@font-face {
|
||||
font-family: "linja-pona-4";
|
||||
src: url('https://ajlee2006.github.io/linjaponasandbox/linja-pona-4.2.otf') format('opentype');
|
||||
}
|
||||
|
||||
html {
|
||||
color-scheme: dark;
|
||||
|
||||
font-family: 'Inter', sans-serif;
|
||||
text-rendering: optimizeLegibility;
|
||||
font-size: 1.125rem;
|
||||
line-height: 1.25;
|
||||
|
||||
color: var(--tx-col);
|
||||
background: var(--bg-col) url('data:image/svg+xml;utf8,<svg xmlns="http://www.w3.org/2000/svg" height="2em" viewBox="0 0 3 1.7320508076"><path d="M0 0.8660254038h1L1.5 0h1L3 0.8660254038L2.5 1.7320508076h-1L1 0.8660254038" stroke="%232D3931" stroke-width="0.05" fill="none"/></svg>');
|
||||
background-position: 1.25em 0.5em;
|
||||
min-height: 100vh;
|
||||
}
|
||||
@supports (font-variable-settings: normal) {
|
||||
html {
|
||||
font-family: 'InterVariable', sans-serif;
|
||||
}
|
||||
}
|
||||
|
||||
html, body {
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
overflow-x: hidden;
|
||||
}
|
||||
|
||||
::selection {
|
||||
background-color: var(--hl-col);
|
||||
}
|
||||
|
||||
main {
|
||||
background-color: var(--bg-col);
|
||||
border: 1px solid var(--pt-col);
|
||||
border-radius: 8px;
|
||||
|
||||
margin: 1em auto;
|
||||
max-width: var(--content-width);
|
||||
|
||||
& > * {
|
||||
padding: 2em;
|
||||
|
||||
& > :first-child {
|
||||
margin-top: 0;
|
||||
}
|
||||
& > :last-child {
|
||||
margin-bottom: 0;
|
||||
}
|
||||
}
|
||||
& > * + * {
|
||||
border-top: 1px solid var(--pt-col);
|
||||
}
|
||||
}
|
||||
@media (max-width: 800px) {
|
||||
html {
|
||||
background: var(--bg-col);
|
||||
}
|
||||
|
||||
main {
|
||||
margin: 0;
|
||||
border: none;
|
||||
|
||||
& > * {
|
||||
padding: 1em;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
h1, h2, h3, h4, h5, h6 {
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
margin-bottom: 0.5em;
|
||||
font-size: 1rem;
|
||||
}
|
||||
h2 {
|
||||
font-size: 1.25rem;
|
||||
}
|
||||
|
||||
a {
|
||||
color: var(--pr-col);
|
||||
transition-property: color;
|
||||
transition-duration: 0.5s;
|
||||
|
||||
&:hover, &:focus, &:active {
|
||||
color: var(--hv-col);
|
||||
}
|
||||
}
|
||||
span.n {
|
||||
text-decoration-line: underline;
|
||||
}
|
||||
a, span.n {
|
||||
text-underline-offset: 2.5px;
|
||||
text-underline-thickness: 1px;
|
||||
}
|
||||
i {
|
||||
font-family: linja-pona-4;
|
||||
font-style: normal;
|
||||
white-space: nowrap;
|
||||
|
||||
&::before {
|
||||
content: "[_";
|
||||
}
|
||||
&::after {
|
||||
content: "]";
|
||||
}
|
||||
}
|
||||
|
||||
header {
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
align-items: center;
|
||||
|
||||
padding-top: 0.5em;
|
||||
padding-bottom: 0.5em;
|
||||
|
||||
& > div {
|
||||
flex: 1;
|
||||
|
||||
& > h1 {
|
||||
font-size: 1.5rem;
|
||||
margin-top: 1rem;
|
||||
line-height: 1.5rem;
|
||||
}
|
||||
& > p:last-child {
|
||||
margin-bottom: 1em;
|
||||
}
|
||||
}
|
||||
|
||||
& > img {
|
||||
border-radius: 8px;
|
||||
aspect-ratio: 1/1;
|
||||
height: 5em;
|
||||
}
|
||||
}
|
||||
|
||||
nav {
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
|
||||
gap: 0.7ch;
|
||||
padding-top: 1rem;
|
||||
padding-bottom: 1rem;
|
||||
|
||||
& a {
|
||||
text-decoration: none;
|
||||
}
|
||||
|
||||
& > ul {
|
||||
display: inline-flex;
|
||||
flex-direction: row;
|
||||
|
||||
font-size: smaller;
|
||||
gap: 0.25em;
|
||||
margin: 0;
|
||||
padding: 0.25em;
|
||||
|
||||
list-style: none;
|
||||
|
||||
background-color: var(--co-col);
|
||||
border-radius: 2em;
|
||||
|
||||
& > li {
|
||||
& > a {
|
||||
display: inline-block;
|
||||
padding: 0.5em 1em;
|
||||
|
||||
border-radius: 1.25em;
|
||||
text-decoration: none;
|
||||
|
||||
color: var(--tx-col);
|
||||
|
||||
&.active {
|
||||
background-color: var(--pr-col);
|
||||
color: var(--co-col);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
footer {
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
|
||||
padding-top: 1.5em;
|
||||
padding-bottom: 1.5em;
|
||||
|
||||
& * {
|
||||
padding: 0;
|
||||
margin: 0;
|
||||
}
|
||||
}
|
||||
|
||||
@media (min-width: 881px) {
|
||||
section {
|
||||
& pre, & div {
|
||||
margin-left: calc(var(--extra-width) * -1);
|
||||
width: calc(var(--content-width) + var(--extra-width));
|
||||
|
||||
position: relative;
|
||||
z-index: 2;
|
||||
|
||||
&::before {
|
||||
content: "";
|
||||
position: absolute;
|
||||
border-radius: 6px;
|
||||
top: 0;
|
||||
left: 0;
|
||||
height: 100%;
|
||||
width: 100%;
|
||||
background-color: var(--bg-col);
|
||||
border: 1px solid var(--pt-col);
|
||||
z-index: -1;
|
||||
|
||||
clip-path: polygon(
|
||||
-1px -1px,
|
||||
calc(var(--extra-width) / 2) -1px,
|
||||
calc(var(--extra-width) / 2) 3px,
|
||||
calc(100% - 2px - var(--extra-width) / 2) 3px,
|
||||
calc(100% - 2px - var(--extra-width) / 2) -1px,
|
||||
calc(100% + 1px) -1px,
|
||||
calc(100% + 1px) calc(100% + 1px),
|
||||
calc(100% - 2px - var(--extra-width) / 2) calc(100% + 1px),
|
||||
calc(100% - 2px - var(--extra-width) / 2) calc(100% - 3px),
|
||||
calc(var(--extra-width) / 2) calc(100% - 3px),
|
||||
calc(var(--extra-width) / 2) calc(100% + 1px),
|
||||
-1px calc(100% + 1px)
|
||||
);
|
||||
}
|
||||
|
||||
& > code, & > img, & > video {
|
||||
padding: 1em 1.5em;
|
||||
padding-bottom: calc(0.85em);
|
||||
background-color: transparent;
|
||||
border: none;
|
||||
}
|
||||
|
||||
& > img, & > video {
|
||||
width: calc(100% - 2.85em);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
code, kbd {
|
||||
font-family: "Monaspace Neon", ui-monospace, SFMono-Regular, SF Mono, Menlo, Consolas, Liberation Mono, monospace !important;
|
||||
font-size: 0.9em;
|
||||
font-size-adjust: from-font;
|
||||
font-weight: 400;
|
||||
|
||||
display: inline-block;
|
||||
padding: 0.05em 0.35em;
|
||||
background: var(--co-col);
|
||||
color: var(--tx-col);
|
||||
border-radius: 8px;
|
||||
}
|
||||
pre > code {
|
||||
overflow-x: scroll;
|
||||
|
||||
display: block;
|
||||
|
||||
border-radius: 0;
|
||||
padding: 1em 0.5em;
|
||||
|
||||
line-height: 1.4;
|
||||
|
||||
background-color: var(--co-col);
|
||||
|
||||
border: 1px solid;
|
||||
border-color: var(--pt-col);
|
||||
}
|
||||
|
||||
div > img, div > video {
|
||||
max-width: 100%;
|
||||
overflow-x: auto;
|
||||
display: block;
|
||||
}
|
||||
@media (max-width: 880px) {
|
||||
pre > code, div > img, div > video {
|
||||
margin: 0 -2em;
|
||||
padding: 1.5em 2em;
|
||||
border-left: none;
|
||||
border-right: none;
|
||||
}
|
||||
|
||||
}
|
||||
@media (max-width: 800px) {
|
||||
pre > code, div > img, div > video {
|
||||
margin: 0 -2em;
|
||||
}
|
||||
}
|
||||
|
||||
section ul {
|
||||
list-style: none;
|
||||
padding: 0;
|
||||
margin: 0;
|
||||
|
||||
& li {
|
||||
margin: 0.25em 0;
|
||||
}
|
||||
|
||||
&.l > li::before {
|
||||
content: "–";
|
||||
padding-right: 1rem;
|
||||
}
|
||||
}
|
||||
|
||||
span.n {
|
||||
text-decoration-line: underline;
|
||||
text-underline-offset: 2.5px;
|
||||
text-underline-thickness: 1px;
|
||||
}
|
||||
|
||||
ul.two-col {
|
||||
display: grid;
|
||||
column-gap: 2em;
|
||||
row-gap: 0.25em;
|
||||
grid-template-columns: 6em 1fr;
|
||||
grid-template-areas: "date headline";
|
||||
padding: 0;
|
||||
margin: 0;
|
||||
|
||||
& > li {
|
||||
display: contents;
|
||||
padding: 0;
|
||||
margin: 0;
|
||||
|
||||
& > time {
|
||||
grid-area: "date";
|
||||
}
|
||||
& > :not(time) {
|
||||
grid-area: "headline";
|
||||
}
|
||||
& > p {
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
}
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user