commit 442591b5aa83d4c5778b0c087f9c11fe605554a8
parent 352c5af8004bc103e79dc90de7d207e880861514
Author: mtmn <miro@haravara.org>
Date: Sat, 20 Jun 2026 23:42:08 +0200
add wrd, update rust deps
Diffstat:
9 files changed, 1013 insertions(+), 8 deletions(-)
diff --git a/Cargo.lock b/Cargo.lock
@@ -116,15 +116,15 @@ checksum = "72f5acc6cb2ba439de613abc23857ec3d78374d8ed5ac84e9d11336e87da8649"
[[package]]
name = "bytes"
-version = "1.11.1"
+version = "1.12.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "1e748733b7cbc798e1434b6ac524f0c1ff2ab456fe201501e6497c8417a4fc33"
+checksum = "8ae3f5d315924270530207e2a68396c3cc547f6dca3fbdca317cfb1a51edb593"
[[package]]
name = "cc"
-version = "1.2.64"
+version = "1.2.65"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "dad887fd958be91b5098c0248def011f4523ab786cd411be668777e55063501f"
+checksum = "e228eec9be7c17ccb640b59b36a5cd805ea2a564a4c5e162c2f659fea30d3b96"
dependencies = [
"find-msvc-tools",
"jobserver",
@@ -822,9 +822,9 @@ checksum = "92daf443525c4cce67b150400bc2316076100ce0b3686209eb8cf3c31612e6f0"
[[package]]
name = "log"
-version = "0.4.32"
+version = "0.4.33"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "953f07c43838f8e6f9758cab68bf5bed85465e7587ebe0b823f1bcd81978ad3a"
+checksum = "0ceec5bc11778974d1bcb055b18002eba7f4b3518b6a0081b3af5f21666da9ad"
[[package]]
name = "lru-slab"
diff --git a/MODULE.bazel b/MODULE.bazel
@@ -8,6 +8,7 @@ bazel_dep(name = "rules_cc", version = "0.2.19")
bazel_dep(name = "rules_zig", version = "0.16.0")
bazel_dep(name = "rules_rust", version = "0.70.0")
bazel_dep(name = "rules_python", version = "2.0.3")
+bazel_dep(name = "rules_go", version = "0.61.1")
# https://github.com/bazelbuild/rules_rust
rust = use_extension("@rules_rust//rust:extensions.bzl", "rust")
@@ -53,3 +54,7 @@ use_repo(pip, "pypi")
# https://github.com/aherrmann/rules_zig
zig = use_extension("@rules_zig//zig:extensions.bzl", "zig")
zig.toolchain(zig_version = "0.16.0")
+
+# https://github.com/bazel-contrib/rules_go
+go_sdk = use_extension("@rules_go//go:extensions.bzl", "go_sdk")
+go_sdk.download(version = "1.26.3")
diff --git a/MODULE.bazel.lock b/MODULE.bazel.lock
@@ -27,6 +27,7 @@
"https://bcr.bazel.build/modules/aspect_bazel_lib/2.22.5/MODULE.bazel": "004ba890363d05372a97248c37205ae64b6fa31047629cd2c0895a9d0c7779e8",
"https://bcr.bazel.build/modules/aspect_bazel_lib/2.22.5/source.json": "ac2c3213df8f985785f1d0aeb7f0f73d5324e6e67d593d9b9470fb74a25d4a9b",
"https://bcr.bazel.build/modules/aspect_bazel_lib/2.8.1/MODULE.bazel": "812d2dd42f65dca362152101fbec418029cc8fd34cbad1a2fde905383d705838",
+ "https://bcr.bazel.build/modules/bazel_features/1.1.0/MODULE.bazel": "cfd42ff3b815a5f39554d97182657f8c4b9719568eb7fded2b9135f084bf760b",
"https://bcr.bazel.build/modules/bazel_features/1.1.1/MODULE.bazel": "27b8c79ef57efe08efccbd9dd6ef70d61b4798320b8d3c134fd571f78963dbcd",
"https://bcr.bazel.build/modules/bazel_features/1.10.0/MODULE.bazel": "f75e8807570484a99be90abcd52b5e1f390362c258bcb73106f4544957a48101",
"https://bcr.bazel.build/modules/bazel_features/1.11.0/MODULE.bazel": "f9382337dd5a474c3b7d334c2f83e50b6eaedc284253334cf823044a26de03e8",
@@ -42,6 +43,7 @@
"https://bcr.bazel.build/modules/bazel_features/1.30.0/MODULE.bazel": "a14b62d05969a293b80257e72e597c2da7f717e1e69fa8b339703ed6731bec87",
"https://bcr.bazel.build/modules/bazel_features/1.32.0/MODULE.bazel": "095d67022a58cb20f7e20e1aefecfa65257a222c18a938e2914fd257b5f1ccdc",
"https://bcr.bazel.build/modules/bazel_features/1.33.0/MODULE.bazel": "8b8dc9d2a4c88609409c3191165bccec0e4cb044cd7a72ccbe826583303459f6",
+ "https://bcr.bazel.build/modules/bazel_features/1.36.0/MODULE.bazel": "596cb62090b039caf1cad1d52a8bc35cf188ca9a4e279a828005e7ee49a1bec3",
"https://bcr.bazel.build/modules/bazel_features/1.4.1/MODULE.bazel": "e45b6bb2350aff3e442ae1111c555e27eac1d915e77775f6fdc4b351b758b5d7",
"https://bcr.bazel.build/modules/bazel_features/1.42.1/MODULE.bazel": "275a59b5406ff18c01739860aa70ad7ccb3cfb474579411decca11c93b951080",
"https://bcr.bazel.build/modules/bazel_features/1.43.0/MODULE.bazel": "defa2226f06ba20550d6548c3a2ea2a7929634437a52973869c20c225450eb91",
@@ -71,6 +73,12 @@
"https://bcr.bazel.build/modules/buildozer/8.5.1/source.json": "e3386e6ff4529f2442800dee47ad28d3e6487f36a1f75ae39ae56c70f0cd2fbd",
"https://bcr.bazel.build/modules/gawk/5.3.2.bcr.1/MODULE.bazel": "cdf8cbe5ee750db04b78878c9633cc76e80dcf4416cbe982ac3a9222f80713c8",
"https://bcr.bazel.build/modules/gawk/5.3.2.bcr.1/source.json": "fa7b512dfcb5eafd90ce3959cf42a2a6fe96144ebbb4b3b3928054895f2afac2",
+ "https://bcr.bazel.build/modules/gazelle/0.32.0/MODULE.bazel": "b499f58a5d0d3537f3cf5b76d8ada18242f64ec474d8391247438bf04f58c7b8",
+ "https://bcr.bazel.build/modules/gazelle/0.33.0/MODULE.bazel": "a13a0f279b462b784fb8dd52a4074526c4a2afe70e114c7d09066097a46b3350",
+ "https://bcr.bazel.build/modules/gazelle/0.34.0/MODULE.bazel": "abdd8ce4d70978933209db92e436deb3a8b737859e9354fb5fd11fb5c2004c8a",
+ "https://bcr.bazel.build/modules/gazelle/0.36.0/MODULE.bazel": "e375d5d6e9a6ca59b0cb38b0540bc9a05b6aa926d322f2de268ad267a2ee74c0",
+ "https://bcr.bazel.build/modules/gazelle/0.51.3/MODULE.bazel": "618a729142f66de1e2cb776d026413763be5b80d5e3d29ffb9d3d90c5defde90",
+ "https://bcr.bazel.build/modules/gazelle/0.51.3/source.json": "fbe5312a01fb4a2a58caff4a0d40dcf384b6f0bda75e85546663360da1d7538a",
"https://bcr.bazel.build/modules/google_benchmark/1.8.2/MODULE.bazel": "a70cf1bba851000ba93b58ae2f6d76490a9feb74192e57ab8e8ff13c34ec50cb",
"https://bcr.bazel.build/modules/googletest/1.11.0/MODULE.bazel": "3a83f095183f66345ca86aa13c58b59f9f94a2f81999c093d4eeaa2d262d12f4",
"https://bcr.bazel.build/modules/googletest/1.14.0.bcr.1/MODULE.bazel": "22c31a561553727960057361aa33bf20fb2e98584bc4fec007906e27053f80c6",
@@ -88,7 +96,8 @@
"https://bcr.bazel.build/modules/nlohmann_json/3.6.1/source.json": "f448c6e8963fdfa7eb831457df83ad63d3d6355018f6574fb017e8169deb43a9",
"https://bcr.bazel.build/modules/package_metadata/0.0.2/MODULE.bazel": "fb8d25550742674d63d7b250063d4580ca530499f045d70748b1b142081ebb92",
"https://bcr.bazel.build/modules/package_metadata/0.0.3/MODULE.bazel": "77890552ecea9e284b5424c9de827a58099348763a4359e975c359a83d4faa83",
- "https://bcr.bazel.build/modules/package_metadata/0.0.3/source.json": "742075a428ad12a3fa18a69014c2f57f01af910c6d9d18646c990200853e641a",
+ "https://bcr.bazel.build/modules/package_metadata/0.0.5/MODULE.bazel": "ef4f9439e3270fdd6b9fd4dbc3d2f29d13888e44c529a1b243f7a31dfbc2e8e4",
+ "https://bcr.bazel.build/modules/package_metadata/0.0.5/source.json": "2326db2f6592578177751c3e1f74786b79382cd6008834c9d01ec865b9126a85",
"https://bcr.bazel.build/modules/platforms/0.0.10/MODULE.bazel": "8cb8efaf200bdeb2150d93e162c40f388529a25852b332cec879373771e48ed5",
"https://bcr.bazel.build/modules/platforms/0.0.11/MODULE.bazel": "0daefc49732e227caa8bfa834d65dc52e8cc18a2faf80df25e8caea151a9413f",
"https://bcr.bazel.build/modules/platforms/0.0.4/MODULE.bazel": "9b328e31ee156f53f3c416a64f8491f7eb731742655a47c9eec4703a71644aee",
@@ -102,10 +111,15 @@
"https://bcr.bazel.build/modules/platforms/1.1.0/source.json": "fcf351c47596c939140ab0d333dfdd08ed1ea6ce33c2fe70c12493a301cf1344",
"https://bcr.bazel.build/modules/protobuf/21.7/MODULE.bazel": "a5a29bb89544f9b97edce05642fac225a808b5b7be74038ea3640fae2f8e66a7",
"https://bcr.bazel.build/modules/protobuf/27.0/MODULE.bazel": "7873b60be88844a0a1d8f80b9d5d20cfbd8495a689b8763e76c6372998d3f64c",
+ "https://bcr.bazel.build/modules/protobuf/27.1/MODULE.bazel": "703a7b614728bb06647f965264967a8ef1c39e09e8f167b3ca0bb1fd80449c0d",
+ "https://bcr.bazel.build/modules/protobuf/29.0-rc2.bcr.1/MODULE.bazel": "52f4126f63a2f0bbf36b99c2a87648f08467a4eaf92ba726bc7d6a500bbf770c",
"https://bcr.bazel.build/modules/protobuf/29.0-rc2/MODULE.bazel": "6241d35983510143049943fc0d57937937122baf1b287862f9dc8590fc4c37df",
"https://bcr.bazel.build/modules/protobuf/29.0-rc3/MODULE.bazel": "33c2dfa286578573afc55a7acaea3cada4122b9631007c594bf0729f41c8de92",
+ "https://bcr.bazel.build/modules/protobuf/29.0/MODULE.bazel": "319dc8bf4c679ff87e71b1ccfb5a6e90a6dbc4693501d471f48662ac46d04e4e",
"https://bcr.bazel.build/modules/protobuf/29.1/MODULE.bazel": "557c3457560ff49e122ed76c0bc3397a64af9574691cb8201b4e46d4ab2ecb95",
"https://bcr.bazel.build/modules/protobuf/3.19.0/MODULE.bazel": "6b5fbb433f760a99a22b18b6850ed5784ef0e9928a72668b66e4d7ccd47db9b0",
+ "https://bcr.bazel.build/modules/protobuf/3.19.2/MODULE.bazel": "532ffe5f2186b69fdde039efe6df13ba726ff338c6bc82275ad433013fa10573",
+ "https://bcr.bazel.build/modules/protobuf/3.19.6/MODULE.bazel": "9233edc5e1f2ee276a60de3eaa47ac4132302ef9643238f23128fea53ea12858",
"https://bcr.bazel.build/modules/protobuf/32.1/MODULE.bazel": "89cd2866a9cb07fee9ff74c41ceace11554f32e0d849de4e23ac55515cfada4d",
"https://bcr.bazel.build/modules/protobuf/33.4/MODULE.bazel": "114775b816b38b6d0ca620450d6b02550c60ceedfdc8d9a229833b34a223dc42",
"https://bcr.bazel.build/modules/protobuf/33.4/source.json": "555f8686b4c7d6b5ba731fbea13bf656b4bfd9a7ff629c1d9d3f6e1d6155de79",
@@ -145,6 +159,12 @@
"https://bcr.bazel.build/modules/rules_cc/0.2.8/MODULE.bazel": "f1df20f0bf22c28192a794f29b501ee2018fa37a3862a1a2132ae2940a23a642",
"https://bcr.bazel.build/modules/rules_foreign_cc/0.9.0/MODULE.bazel": "c9e8c682bf75b0e7c704166d79b599f93b72cfca5ad7477df596947891feeef6",
"https://bcr.bazel.build/modules/rules_fuzzing/0.5.2/MODULE.bazel": "40c97d1144356f52905566c55811f13b299453a14ac7769dfba2ac38192337a8",
+ "https://bcr.bazel.build/modules/rules_go/0.41.0/MODULE.bazel": "55861d8e8bb0e62cbd2896f60ff303f62ffcb0eddb74ecb0e5c0cbe36fc292c8",
+ "https://bcr.bazel.build/modules/rules_go/0.42.0/MODULE.bazel": "8cfa875b9aa8c6fce2b2e5925e73c1388173ea3c32a0db4d2b4804b453c14270",
+ "https://bcr.bazel.build/modules/rules_go/0.46.0/MODULE.bazel": "3477df8bdcc49e698b9d25f734c4f3a9f5931ff34ee48a2c662be168f5f2d3fd",
+ "https://bcr.bazel.build/modules/rules_go/0.59.0/MODULE.bazel": "b7e43e7414a3139a7547d1b4909b29085fbe5182b6c58cbe1ed4c6272815aeae",
+ "https://bcr.bazel.build/modules/rules_go/0.61.1/MODULE.bazel": "b599cc67f98e8dbe040631129fbd23f190a557f7be506d4781619d0fd4dbbbec",
+ "https://bcr.bazel.build/modules/rules_go/0.61.1/source.json": "ff52d46fca8e2ac87c610c11f05c3a9f0fa08dc4294664c6a31449092409c5aa",
"https://bcr.bazel.build/modules/rules_java/4.0.0/MODULE.bazel": "5a78a7ae82cd1a33cef56dc578c7d2a46ed0dca12643ee45edbb8417899e6f74",
"https://bcr.bazel.build/modules/rules_java/5.3.5/MODULE.bazel": "a4ec4f2db570171e3e5eb753276ee4b389bae16b96207e9d3230895c99644b86",
"https://bcr.bazel.build/modules/rules_java/6.0.0/MODULE.bazel": "8a43b7df601a7ec1af61d79345c17b31ea1fedc6711fd4abfd013ea612978e39",
@@ -183,6 +203,7 @@
"https://bcr.bazel.build/modules/rules_proto/5.3.0-21.7/MODULE.bazel": "e8dff86b0971688790ae75528fe1813f71809b5afd57facb44dad9e8eca631b7",
"https://bcr.bazel.build/modules/rules_proto/6.0.0-rc1/MODULE.bazel": "1e5b502e2e1a9e825eef74476a5a1ee524a92297085015a052510b09a1a09483",
"https://bcr.bazel.build/modules/rules_proto/6.0.2/MODULE.bazel": "ce916b775a62b90b61888052a416ccdda405212b6aaeb39522f7dc53431a5e73",
+ "https://bcr.bazel.build/modules/rules_proto/7.0.2/MODULE.bazel": "bf81793bd6d2ad89a37a40693e56c61b0ee30f7a7fdbaf3eabbf5f39de47dea2",
"https://bcr.bazel.build/modules/rules_proto/7.1.0/MODULE.bazel": "002d62d9108f75bb807cd56245d45648f38275cb3a99dcd45dfb864c5d74cb96",
"https://bcr.bazel.build/modules/rules_proto/7.1.0/source.json": "39f89066c12c24097854e8f57ab8558929f9c8d474d34b2c00ac04630ad8940e",
"https://bcr.bazel.build/modules/rules_python/0.10.2/MODULE.bazel": "cc82bc96f2997baa545ab3ce73f196d040ffb8756fd2d66125a530031cd90e5f",
@@ -230,6 +251,7 @@
"https://bcr.bazel.build/modules/yq.bzl/0.1.1/MODULE.bazel": "9039681f9bcb8958ee2c87ffc74bdafba9f4369096a2b5634b88abc0eaefa072",
"https://bcr.bazel.build/modules/yq.bzl/0.1.1/source.json": "2d2bad780a9f2b9195a4a370314d2c17ae95eaa745cefc2e12fbc49759b15aa3",
"https://bcr.bazel.build/modules/zlib/1.2.11/MODULE.bazel": "07b389abc85fdbca459b69e2ec656ae5622873af3f845e1c9d80fe179f3effa0",
+ "https://bcr.bazel.build/modules/zlib/1.2.12/MODULE.bazel": "3b1a8834ada2a883674be8cbd36ede1b6ec481477ada359cd2d3ddc562340b27",
"https://bcr.bazel.build/modules/zlib/1.3.1.bcr.5/MODULE.bazel": "eec517b5bbe5492629466e11dae908d043364302283de25581e3eb944326c4ca",
"https://bcr.bazel.build/modules/zlib/1.3.1.bcr.5/source.json": "22bc55c47af97246cfc093d0acf683a7869377de362b5d1c552c2c2e16b7a806",
"https://bcr.bazel.build/modules/zlib/1.3.1/MODULE.bazel": "751c9940dcfe869f5f7274e1295422a34623555916eb98c174c1e945594bf198"
@@ -318,7 +340,7 @@
"@@rules_python+//python/uv:uv.bzl%uv": {
"general": {
"bzlTransitiveDigest": "I8FPZMevE2oI/peSpMBRVIN++WOtfjtJVjbPsBZQ87A=",
- "usagesDigest": "R0pij/Kri0Z/7GcM2UBot6XmOqgduje0j8y9wNMWjLA=",
+ "usagesDigest": "XGwR3eTI95rhVlQBWeWHCf6MeLufEzhWrWRWAtF6Zv4=",
"recordedInputs": [
"REPO_MAPPING:rules_python+,bazel_tools bazel_tools",
"REPO_MAPPING:rules_python+,platforms platforms"
@@ -3611,6 +3633,328 @@
}
},
"facts": {
+ "@@rules_go+//go:extensions.bzl%go_sdk": {
+ "1.25.0": {
+ "aix_ppc64": [
+ "go1.25.0.aix-ppc64.tar.gz",
+ "e5234a7dac67bc86c528fe9752fc9d63557918627707a733ab4cac1a6faed2d4"
+ ],
+ "darwin_amd64": [
+ "go1.25.0.darwin-amd64.tar.gz",
+ "5bd60e823037062c2307c71e8111809865116714d6f6b410597cf5075dfd80ef"
+ ],
+ "darwin_arm64": [
+ "go1.25.0.darwin-arm64.tar.gz",
+ "544932844156d8172f7a28f77f2ac9c15a23046698b6243f633b0a0b00c0749c"
+ ],
+ "dragonfly_amd64": [
+ "go1.25.0.dragonfly-amd64.tar.gz",
+ "5ed3cf9a810a1483822538674f1336c06b51aa1b94d6d545a1a0319a48177120"
+ ],
+ "freebsd_386": [
+ "go1.25.0.freebsd-386.tar.gz",
+ "abea5d5c6697e6b5c224731f2158fe87c602996a2a233ac0c4730cd57bf8374e"
+ ],
+ "freebsd_amd64": [
+ "go1.25.0.freebsd-amd64.tar.gz",
+ "86e6fe0a29698d7601c4442052dac48bd58d532c51cccb8f1917df648138730b"
+ ],
+ "freebsd_arm": [
+ "go1.25.0.freebsd-arm.tar.gz",
+ "d90b78e41921f72f30e8bbc81d9dec2cff7ff384a33d8d8debb24053e4336bfe"
+ ],
+ "freebsd_arm64": [
+ "go1.25.0.freebsd-arm64.tar.gz",
+ "451d0da1affd886bfb291b7c63a6018527b269505db21ce6e14724f22ab0662e"
+ ],
+ "freebsd_riscv64": [
+ "go1.25.0.freebsd-riscv64.tar.gz",
+ "7b565f76bd8bda46549eeaaefe0e53b251e644c230577290c0f66b1ecdb3cdbe"
+ ],
+ "illumos_amd64": [
+ "go1.25.0.illumos-amd64.tar.gz",
+ "b1e1fdaab1ad25aa1c08d7a36c97d45d74b98b89c3f78c6d2145f77face54a2c"
+ ],
+ "linux_386": [
+ "go1.25.0.linux-386.tar.gz",
+ "8c602dd9d99bc9453b3995d20ce4baf382cc50855900a0ece5de9929df4a993a"
+ ],
+ "linux_amd64": [
+ "go1.25.0.linux-amd64.tar.gz",
+ "2852af0cb20a13139b3448992e69b868e50ed0f8a1e5940ee1de9e19a123b613"
+ ],
+ "linux_arm64": [
+ "go1.25.0.linux-arm64.tar.gz",
+ "05de75d6994a2783699815ee553bd5a9327d8b79991de36e38b66862782f54ae"
+ ],
+ "linux_armv6l": [
+ "go1.25.0.linux-armv6l.tar.gz",
+ "a5a8f8198fcf00e1e485b8ecef9ee020778bf32a408a4e8873371bfce458cd09"
+ ],
+ "linux_loong64": [
+ "go1.25.0.linux-loong64.tar.gz",
+ "cab86b1cf761b1cb3bac86a8877cfc92e7b036fc0d3084123d77013d61432afc"
+ ],
+ "linux_mips": [
+ "go1.25.0.linux-mips.tar.gz",
+ "d66b6fb74c3d91b9829dc95ec10ca1f047ef5e89332152f92e136cf0e2da5be1"
+ ],
+ "linux_mips64": [
+ "go1.25.0.linux-mips64.tar.gz",
+ "4082e4381a8661bc2a839ff94ba3daf4f6cde20f8fb771b5b3d4762dc84198a2"
+ ],
+ "linux_mips64le": [
+ "go1.25.0.linux-mips64le.tar.gz",
+ "70002c299ec7f7175ac2ef673b1b347eecfa54ae11f34416a6053c17f855afcc"
+ ],
+ "linux_mipsle": [
+ "go1.25.0.linux-mipsle.tar.gz",
+ "b00a3a39eff099f6df9f1c7355bf28e4589d0586f42d7d4a394efb763d145a73"
+ ],
+ "linux_ppc64": [
+ "go1.25.0.linux-ppc64.tar.gz",
+ "df166f33bd98160662560a72ff0b4ba731f969a80f088922bddcf566a88c1ec1"
+ ],
+ "linux_ppc64le": [
+ "go1.25.0.linux-ppc64le.tar.gz",
+ "0f18a89e7576cf2c5fa0b487a1635d9bcbf843df5f110e9982c64df52a983ad0"
+ ],
+ "linux_riscv64": [
+ "go1.25.0.linux-riscv64.tar.gz",
+ "c018ff74a2c48d55c8ca9b07c8e24163558ffec8bea08b326d6336905d956b67"
+ ],
+ "linux_s390x": [
+ "go1.25.0.linux-s390x.tar.gz",
+ "34e5a2e19f2292fbaf8783e3a241e6e49689276aef6510a8060ea5ef54eee408"
+ ],
+ "netbsd_386": [
+ "go1.25.0.netbsd-386.tar.gz",
+ "f8586cdb7aa855657609a5c5f6dbf523efa00c2bbd7c76d3936bec80aa6c0aba"
+ ],
+ "netbsd_amd64": [
+ "go1.25.0.netbsd-amd64.tar.gz",
+ "ae8dc1469385b86a157a423bb56304ba45730de8a897615874f57dd096db2c2a"
+ ],
+ "netbsd_arm": [
+ "go1.25.0.netbsd-arm.tar.gz",
+ "1ff7e4cc764425fc9dd6825eaee79d02b3c7cafffbb3691687c8d672ade76cb7"
+ ],
+ "netbsd_arm64": [
+ "go1.25.0.netbsd-arm64.tar.gz",
+ "e1b310739f26724216aa6d7d7208c4031f9ff54c9b5b9a796ddc8bebcb4a5f16"
+ ],
+ "openbsd_386": [
+ "go1.25.0.openbsd-386.tar.gz",
+ "4802a9b20e533da91adb84aab42e94aa56cfe3e5475d0550bed3385b182e69d8"
+ ],
+ "openbsd_amd64": [
+ "go1.25.0.openbsd-amd64.tar.gz",
+ "c016cd984bebe317b19a4f297c4f50def120dc9788490540c89f28e42f1dabe1"
+ ],
+ "openbsd_arm": [
+ "go1.25.0.openbsd-arm.tar.gz",
+ "a1e31d0bf22172ddde42edf5ec811ef81be43433df0948ece52fecb247ccfd8d"
+ ],
+ "openbsd_arm64": [
+ "go1.25.0.openbsd-arm64.tar.gz",
+ "343ea8edd8c218196e15a859c6072d0dd3246fbbb168481ab665eb4c4140458d"
+ ],
+ "openbsd_ppc64": [
+ "go1.25.0.openbsd-ppc64.tar.gz",
+ "694c14da1bcaeb5e3332d49bdc2b6d155067648f8fe1540c5de8f3cf8e157154"
+ ],
+ "openbsd_riscv64": [
+ "go1.25.0.openbsd-riscv64.tar.gz",
+ "aa510ad25cf54c06cd9c70b6d80ded69cb20188ac6e1735655eef29ff7e7885f"
+ ],
+ "plan9_386": [
+ "go1.25.0.plan9-386.tar.gz",
+ "46f8cef02086cf04bf186c5912776b56535178d4cb319cd19c9fdbdd29231986"
+ ],
+ "plan9_amd64": [
+ "go1.25.0.plan9-amd64.tar.gz",
+ "29b34391d84095e44608a228f63f2f88113a37b74a79781353ec043dfbcb427b"
+ ],
+ "plan9_arm": [
+ "go1.25.0.plan9-arm.tar.gz",
+ "0a047107d13ebe7943aaa6d54b1d7bbd2e45e68ce449b52915a818da715799c2"
+ ],
+ "solaris_amd64": [
+ "go1.25.0.solaris-amd64.tar.gz",
+ "9977f9e4351984364a3b2b78f8b88bfd1d339812356d5237678514594b7d3611"
+ ],
+ "windows_386": [
+ "go1.25.0.windows-386.zip",
+ "df9f39db82a803af0db639e3613a36681ab7a42866b1384b3f3a1045663961a7"
+ ],
+ "windows_amd64": [
+ "go1.25.0.windows-amd64.zip",
+ "89efb4f9b30812eee083cc1770fdd2913c14d301064f6454851428f9707d190b"
+ ],
+ "windows_arm64": [
+ "go1.25.0.windows-arm64.zip",
+ "27bab004c72b3d7bd05a69b6ec0fc54a309b4b78cc569dd963d8b3ec28bfdb8c"
+ ]
+ },
+ "1.26.3": {
+ "aix_ppc64": [
+ "go1.26.3.aix-ppc64.tar.gz",
+ "9fd8b45c4aa58fa6adf7f347343a3b50c02b17a4b5c43381dd9bf87be563183d"
+ ],
+ "darwin_amd64": [
+ "go1.26.3.darwin-amd64.tar.gz",
+ "278d580b32e299fe4a9c990fcf2d02acfe538c7e551a6ee18f9c7164573d2c63"
+ ],
+ "darwin_arm64": [
+ "go1.26.3.darwin-arm64.tar.gz",
+ "875cf54a15311eee2c99b9dd67c68c4a49351d489ab622bf2cfd28c8f2078d3c"
+ ],
+ "dragonfly_amd64": [
+ "go1.26.3.dragonfly-amd64.tar.gz",
+ "6bfeaae407b12affd477cd48674e51d34931b6d98afc59de0f50ef93523ea4bf"
+ ],
+ "freebsd_386": [
+ "go1.26.3.freebsd-386.tar.gz",
+ "270df83863a4fbeb716565e91915f54af4ed911ec503651fbce6c14f9e00018c"
+ ],
+ "freebsd_amd64": [
+ "go1.26.3.freebsd-amd64.tar.gz",
+ "2a5a3b0265f24cdf3878bbab19bb1086f71ae5d29566f238214847d1e3745b4e"
+ ],
+ "freebsd_arm": [
+ "go1.26.3.freebsd-arm.tar.gz",
+ "db3700f0173ef7d15b96b4a2fa34c7fce90455e3125491183d751c9270b63d96"
+ ],
+ "freebsd_arm64": [
+ "go1.26.3.freebsd-arm64.tar.gz",
+ "07431d472522d3e3b9fce3f5d1ea825e2fbb14d7b0b7fbfa548726654217127c"
+ ],
+ "illumos_amd64": [
+ "go1.26.3.illumos-amd64.tar.gz",
+ "2c5bc6a2c7c43e09a91b19117fa18ce9012393a9f42fc0d153cad345fd328dad"
+ ],
+ "linux_386": [
+ "go1.26.3.linux-386.tar.gz",
+ "0ef3626a149b5811c813838c62b7d6618d03ea36047b32c90b0e4851cc42b1fa"
+ ],
+ "linux_amd64": [
+ "go1.26.3.linux-amd64.tar.gz",
+ "2b2cfc7148493da5e73981bffbf3353af381d5f93e789c82c79aff64962eb556"
+ ],
+ "linux_arm64": [
+ "go1.26.3.linux-arm64.tar.gz",
+ "9d89a3ea57d141c2b22d70083f2c8459ba3890f2d9e818e7e933b75614936565"
+ ],
+ "linux_armv6l": [
+ "go1.26.3.linux-armv6l.tar.gz",
+ "d44133d4c66b1451a1e247da26db7716f76a081c0169a75e6c84e1871e394320"
+ ],
+ "linux_loong64": [
+ "go1.26.3.linux-loong64.tar.gz",
+ "05215802b85a33dcfdb933a6c3ab881f4f0405587ee6581d33f34cc5c2ab740c"
+ ],
+ "linux_mips": [
+ "go1.26.3.linux-mips.tar.gz",
+ "76800ce7007d5eacabfe25d038e0a99e73a0fb70c4dd13f8a9662045fb4a52a7"
+ ],
+ "linux_mips64": [
+ "go1.26.3.linux-mips64.tar.gz",
+ "f2c755d17c6834dd3ae805d815a1b9e2eda66375de6ca910efb903a257ff3a3d"
+ ],
+ "linux_mips64le": [
+ "go1.26.3.linux-mips64le.tar.gz",
+ "54f7b00bf2d5cf4b21734cc8eb3c61c51a76177d3d20cd667e4b1ba8fb3343d5"
+ ],
+ "linux_mipsle": [
+ "go1.26.3.linux-mipsle.tar.gz",
+ "31f7d8c886136b725d36880f6ee16fe22a7243751839a2de2ea2da3cb4fd3fe1"
+ ],
+ "linux_ppc64": [
+ "go1.26.3.linux-ppc64.tar.gz",
+ "459746b1b06eb24836d1e4699c6568220c95e82de9b1b155c74eb4fdfd711532"
+ ],
+ "linux_ppc64le": [
+ "go1.26.3.linux-ppc64le.tar.gz",
+ "dbd82b50530ead2beb1fd72215117380df3cb16332b51467116dc35b3691dd75"
+ ],
+ "linux_riscv64": [
+ "go1.26.3.linux-riscv64.tar.gz",
+ "3b8fd5112340b72587e42c619f43270f1bc21f63cfdb587e6b72e0336580727c"
+ ],
+ "linux_s390x": [
+ "go1.26.3.linux-s390x.tar.gz",
+ "5c0605b7175449f1c8e8cb02efaba2695caab914fad4dcedc764c2f4c6dfe6ca"
+ ],
+ "netbsd_386": [
+ "go1.26.3.netbsd-386.tar.gz",
+ "15c74255bb23fe9690faac4e489c654cea35d5059a59744e0afd0f78a29a6e53"
+ ],
+ "netbsd_amd64": [
+ "go1.26.3.netbsd-amd64.tar.gz",
+ "a622ef5f3a6d42661ca75bdc939d8cb0468bb1a4418755b6d8c1b4acb82dd03c"
+ ],
+ "netbsd_arm": [
+ "go1.26.3.netbsd-arm.tar.gz",
+ "696df9d8562ac0118c3b8fd6578978d1c4e35583fe2d655e18b6520da7508ec9"
+ ],
+ "netbsd_arm64": [
+ "go1.26.3.netbsd-arm64.tar.gz",
+ "faca070ad7866db5f5a085fe4380408394e2427e093e9770146620c0db508251"
+ ],
+ "openbsd_386": [
+ "go1.26.3.openbsd-386.tar.gz",
+ "a29ad1b1f1a0a3a0f7e70a579f8ab1a26c03778bdcae4f58bd5a29f303104af9"
+ ],
+ "openbsd_amd64": [
+ "go1.26.3.openbsd-amd64.tar.gz",
+ "b2d952b8f0b74a6d2c1a01251aca75ec8eb00a505ebc993f192b792c6762c800"
+ ],
+ "openbsd_arm": [
+ "go1.26.3.openbsd-arm.tar.gz",
+ "1038789f09e31a6b7b5cd7a5e5b3f65cb88ceefb68e34875a7e41b1a28fe2fb7"
+ ],
+ "openbsd_arm64": [
+ "go1.26.3.openbsd-arm64.tar.gz",
+ "1e2df1dc7a4af8bf2a937e7641f300b855bf12bb84a01c44d00a2d68ec18ce26"
+ ],
+ "openbsd_ppc64": [
+ "go1.26.3.openbsd-ppc64.tar.gz",
+ "42ac85fff0f26a1121ba94d7677f6c51cf3d92b53ad2980e54d80f0b196d57a9"
+ ],
+ "openbsd_riscv64": [
+ "go1.26.3.openbsd-riscv64.tar.gz",
+ "c3ada901258530e4ccb58d58537fb9203733ef76155e589125fd2de2e33e857a"
+ ],
+ "plan9_386": [
+ "go1.26.3.plan9-386.tar.gz",
+ "0c5ee46c1345f16edb9ed7317050c20178fdb2b67f0adc2d7f5cf9339147bee5"
+ ],
+ "plan9_amd64": [
+ "go1.26.3.plan9-amd64.tar.gz",
+ "d672908489ef1982b63110597e1804656a5a9519544337af382233a171a1c96e"
+ ],
+ "plan9_arm": [
+ "go1.26.3.plan9-arm.tar.gz",
+ "8ff8f45a52b4900febf8ecf0aa9ac0d49233c76fa4efe405e6ff0d81a6a50749"
+ ],
+ "solaris_amd64": [
+ "go1.26.3.solaris-amd64.tar.gz",
+ "e9c5eab8d081c57fad50895b99e18faf78cccbbe957dea231eba3a32c964d7e7"
+ ],
+ "windows_386": [
+ "go1.26.3.windows-386.zip",
+ "cefec7bd234f57dcc22e2ad2b2e98e45840d998770c5b10b22daddf728dc7cac"
+ ],
+ "windows_amd64": [
+ "go1.26.3.windows-amd64.zip",
+ "20d2ceafb4ed41b96b879010927b28bc92a5be57a7c1801ce365a9ca51d3224a"
+ ],
+ "windows_arm64": [
+ "go1.26.3.windows-arm64.zip",
+ "95cd63bc6b0da77409ba819215afea9ddf5702c55a3b20af3dd90ea95c7b130c"
+ ]
+ }
+ },
"@@rules_python+//python/extensions:pip.bzl%pip": {
"dist_hashes": {
"https://pypi.org/simple": {
diff --git a/README.md b/README.md
@@ -19,6 +19,7 @@ Various tools I have been using throughout the years.
| [speediness](speediness/) | network speed test |
| [tempfox](tempfox/) | Firefox user.js preference overrides |
| [wlr-river-title](wlr-river-title/) | River/Wayland focused view title printer |
+| [wrd](wrd/) | full-text search and reader for an offline web archive |
## Toolchains
@@ -27,6 +28,7 @@ Various tools I have been using throughout the years.
| Rust | nightly (2026-05-11) |
| Zig | 0.16.0 |
| Python | 3.14 |
+| Go | 1.26.3 |
## Building
diff --git a/wrd/BUILD b/wrd/BUILD
@@ -0,0 +1,23 @@
+load("@rules_go//go:def.bzl", "go_binary", "go_library")
+load("//bazel:local-deploy.bzl", "local_deploy")
+
+go_library(
+ name = "wrd_lib",
+ srcs = [
+ "main.go",
+ "tui.go",
+ ],
+ importpath = "wrd",
+ visibility = ["//visibility:private"],
+)
+
+go_binary(
+ name = "wrd",
+ embed = [":wrd_lib"],
+ visibility = ["//visibility:public"],
+)
+
+local_deploy(
+ name = "deploy",
+ srcs = [":wrd"],
+)
diff --git a/wrd/README.md b/wrd/README.md
@@ -0,0 +1,52 @@
+# wrd
+
+*web recall daemon* — full-text search and reader for the offline web archive in `~/misc/www`. Launches into live full-text search across the rendered text of every page; `Tab` switches to browsing by path. Selecting a result opens it in the [cha](https://sr.ht/~bptato/chawan/) terminal browser — markdown is rendered to HTML with `lowdown` first.
+
+Search reads a plain-text cache under `$XDG_CACHE_HOME/wrd`, rebuilt incrementally as pages change. Everything runs in-process.
+
+## Building
+
+```sh
+bazel build //wrd
+```
+
+Install it locally (to `~/.local/share/tools`) with:
+
+```sh
+bazel run //wrd:deploy
+```
+
+## Usage
+
+```sh
+wrd [scope]
+```
+
+### Options
+
+| Argument | Description |
+|----------|-------------|
+| `<scope>` | Optional. Restrict to pages whose path contains this substring (e.g. `wrd purescript`) |
+
+### Controls
+
+| Key | Action |
+|-----|--------|
+| *type* | Filter results |
+| `Tab` | Toggle content / browse mode |
+| `↑` `↓` / `Ctrl-n` `Ctrl-p` | Move selection |
+| `PgUp` `PgDn` | Page through results |
+| `Enter` | Open the page in cha |
+| `Ctrl-o` | Open the reconstructed live URL in cha |
+| `Ctrl-y` | Yank the live URL (`wl-copy`) |
+| `Ctrl-u` | Clear the query |
+| `Esc` / `Ctrl-c` | Quit |
+
+## Requirements
+
+| Tool | Used for |
+|------|----------|
+| `cha` | Rendering and displaying pages |
+| `lowdown` | Markdown → HTML for display |
+| `pandoc` | Markdown → text for the search index |
+| `wl-copy` | Clipboard (`Ctrl-y`) |
diff --git a/wrd/go.mod b/wrd/go.mod
@@ -0,0 +1,3 @@
+module wrd
+
+go 1.26
diff --git a/wrd/main.go b/wrd/main.go
@@ -0,0 +1,244 @@
+package main
+
+import (
+ "cmp"
+ "fmt"
+ "os"
+ "os/exec"
+ "path/filepath"
+ "runtime"
+ "strings"
+ "sync"
+ "time"
+)
+
+// caps how many content matches are collected per keystroke
+const resultLimit = 2000
+
+type doc struct {
+ src string
+ rel string
+ mtime time.Time
+ lines []string
+ linesLower []string
+}
+
+type match struct {
+ d *doc
+ line int // 1-based, 0 in browse mode
+}
+
+func (m match) text() string {
+ if m.line == 0 {
+ return m.d.rel
+ }
+ return m.d.rel + ":" + strings.TrimSpace(m.d.lines[m.line-1])
+}
+
+type app struct {
+ base, txt string
+ scope string
+ docs []*doc
+
+ content bool
+ query []rune
+ results []match
+ sel int
+ top int
+
+ rows, cols int
+ savedStty string
+}
+
+func main() {
+ home, _ := os.UserHomeDir()
+ cache := cmp.Or(os.Getenv("XDG_CACHE_HOME"), filepath.Join(home, ".cache"))
+ a := &app{
+ base: filepath.Join(home, "misc", "www"),
+ txt: filepath.Join(cache, "wrd", "text"),
+ content: true,
+ }
+ if len(os.Args) > 1 && !strings.HasPrefix(os.Args[1], "-") {
+ a.scope = os.Args[1]
+ }
+
+ a.scan()
+ if len(a.docs) == 0 {
+ fmt.Fprintln(os.Stderr, "wrd: no pages under", a.base)
+ os.Exit(1)
+ }
+ a.index()
+ a.load()
+ a.run()
+}
+
+func (a *app) scan() {
+ _ = filepath.WalkDir(a.base, func(p string, d os.DirEntry, err error) error {
+ if err != nil || d.IsDir() {
+ return nil // skip unreadable entries, keep walking
+ }
+ ext := strings.ToLower(filepath.Ext(p))
+ if ext != ".html" && ext != ".md" {
+ return nil
+ }
+ rel, _ := filepath.Rel(a.base, p)
+ if a.scope != "" && !strings.Contains(rel, a.scope) {
+ return nil
+ }
+ fi, err := os.Stat(p)
+ if err != nil {
+ return nil // broken symlink or a vanished file
+ }
+ a.docs = append(a.docs, &doc{src: p, rel: rel, mtime: fi.ModTime()})
+ return nil
+ })
+}
+
+// index renders each page to a text cache, skipping ones already up to date
+func (a *app) index() {
+ type job struct{ src, out string }
+ var jobs []job
+ for _, d := range a.docs {
+ out := filepath.Join(a.txt, d.rel+".txt")
+ if oi, err := os.Stat(out); err == nil &&
+ !d.mtime.Truncate(time.Second).After(oi.ModTime().Truncate(time.Second)) {
+ continue
+ }
+ jobs = append(jobs, job{d.src, out})
+ }
+ if len(jobs) == 0 {
+ return
+ }
+
+ fmt.Fprintf(os.Stderr, "indexing %d pages\n", len(jobs))
+ ch := make(chan job)
+ var wg sync.WaitGroup
+ for range runtime.NumCPU() {
+ wg.Add(1)
+ go func() {
+ defer wg.Done()
+ for j := range ch {
+ if err := render(j.src, j.out); err != nil {
+ fmt.Fprintf(os.Stderr, "wrd: %s: %v\n", j.src, err)
+ }
+ }
+ }()
+ }
+ for _, j := range jobs {
+ ch <- j
+ }
+ close(ch)
+ wg.Wait()
+}
+
+func render(src, out string) error {
+ fi, err := os.Stat(src)
+ if err != nil {
+ return err
+ }
+ if err := os.MkdirAll(filepath.Dir(out), 0o755); err != nil {
+ return err
+ }
+ f, err := os.Create(out)
+ if err != nil {
+ return err
+ }
+
+ html := strings.EqualFold(filepath.Ext(src), ".html")
+ cmd := exec.Command("pandoc", "-t", "plain", src)
+ if html {
+ cmd = exec.Command("cha", "-d", src)
+ }
+ cmd.Stdout = f
+ rerr := cmd.Run()
+ if rerr != nil && !html { // fall back to the raw markdown
+ _, _ = f.Seek(0, 0)
+ _ = f.Truncate(0)
+ if b, e := os.ReadFile(src); e == nil {
+ _, _ = f.Write(b)
+ rerr = nil
+ }
+ }
+ _ = f.Close()
+
+ // stamp the cache with the source mtime
+ _ = os.Chtimes(out, time.Now(), fi.ModTime())
+ return rerr
+}
+
+func (a *app) load() {
+ for _, d := range a.docs {
+ b, err := os.ReadFile(filepath.Join(a.txt, d.rel+".txt"))
+ if err != nil {
+ continue
+ }
+ s := strings.TrimRight(string(b), "\n")
+ if strings.TrimSpace(s) == "" {
+ continue // missing or empty render: leave it as no-text
+ }
+ d.lines = strings.Split(s, "\n")
+ d.linesLower = make([]string, len(d.lines))
+ for i, ln := range d.lines {
+ d.linesLower[i] = strings.ToLower(ln)
+ }
+ }
+}
+
+func (a *app) search() {
+ q := strings.TrimSpace(string(a.query))
+ fold := q == strings.ToLower(q) // lowercase query => case-insensitive
+ a.results = a.results[:0]
+
+ switch {
+ case a.content && q == "":
+ case a.content:
+ for _, d := range a.docs {
+ lines := d.lines
+ if fold {
+ lines = d.linesLower
+ }
+ for i, ln := range lines {
+ if strings.Contains(ln, q) {
+ a.results = append(a.results, match{d, i + 1})
+ if len(a.results) >= resultLimit {
+ goto done
+ }
+ }
+ }
+ }
+ default:
+ for _, d := range a.docs {
+ hay := d.rel
+ if fold {
+ hay = strings.ToLower(hay)
+ }
+ if fuzzy(hay, q) {
+ a.results = append(a.results, match{d: d})
+ }
+ }
+ }
+done:
+ a.sel = max(0, min(a.sel, len(a.results)-1))
+ a.top = 0
+}
+
+func fuzzy(hay, needle string) bool {
+ nr := []rune(needle)
+ i := 0
+ for _, c := range hay {
+ if i == len(nr) {
+ break
+ }
+ if c == nr[i] {
+ i++
+ }
+ }
+ return i == len(nr)
+}
+
+func url(rel string) string {
+ if strings.HasSuffix(rel, "/index.html") {
+ rel = strings.TrimSuffix(rel, "index.html")
+ }
+ return "https://" + rel
+}
diff --git a/wrd/tui.go b/wrd/tui.go
@@ -0,0 +1,332 @@
+package main
+
+import (
+ "cmp"
+ "fmt"
+ "os"
+ "os/exec"
+ "os/signal"
+ "sort"
+ "strings"
+ "syscall"
+ "unicode/utf8"
+ "unsafe"
+)
+
+// Terminal state is saved once, in enter(); later transitions only flip raw mode
+const (
+ altOn = "\x1b[?1049h\x1b[?25l" // alt screen, hide cursor
+ altOff = "\x1b[?25h\x1b[?1049l" // show cursor, primary screen
+)
+
+func (a *app) enter() {
+ cmd := exec.Command("stty", "-g")
+ cmd.Stdin = os.Stdin
+ if out, err := cmd.Output(); err == nil {
+ a.savedStty = strings.TrimSpace(string(out))
+ }
+ a.resume()
+}
+
+func (a *app) resume() { a.raw(); fmt.Print(altOn) }
+func (a *app) leave() { fmt.Print(altOff); a.cooked() }
+
+// raw disables isig/ixon too, so ctrl-c/s/q arrive as bytes instead of signals
+func (a *app) raw() { a.stty("-echo", "-icanon", "-isig", "-ixon", "min", "1", "time", "0") }
+
+func (a *app) cooked() { a.stty(cmp.Or(a.savedStty, "sane")) }
+
+func (a *app) stty(args ...string) {
+ cmd := exec.Command("stty", args...)
+ cmd.Stdin = os.Stdin
+ _ = cmd.Run()
+}
+
+// size queries the window via ioctl
+func (a *app) size() {
+ var ws struct{ row, col, x, y uint16 }
+ _, _, _ = syscall.Syscall(syscall.SYS_IOCTL, os.Stdin.Fd(),
+ uintptr(syscall.TIOCGWINSZ), uintptr(unsafe.Pointer(&ws)))
+ a.rows = max(int(ws.row), 5)
+ a.cols = max(int(ws.col), 20)
+}
+
+func (a *app) run() {
+ sort.SliceStable(a.docs, func(i, j int) bool {
+ return a.docs[i].mtime.After(a.docs[j].mtime)
+ })
+ a.enter()
+ defer a.leave()
+
+ // defer does not run on signal death; restore the terminal by hand
+ sig := make(chan os.Signal, 1)
+ signal.Notify(sig, syscall.SIGINT, syscall.SIGTERM, syscall.SIGHUP)
+ go func() { <-sig; a.leave(); os.Exit(1) }()
+
+ a.size()
+ a.search()
+ a.draw()
+
+ buf := make([]byte, 64)
+ for {
+ n, err := os.Stdin.Read(buf)
+ if err != nil || n == 0 {
+ return
+ }
+ if a.handle(buf[:n]) {
+ return
+ }
+ a.draw()
+ }
+}
+
+func (a *app) handle(b []byte) bool {
+ changed := false
+ for i := 0; i < len(b); {
+ c := b[i]
+ switch {
+ case c == 0x1b: // CSI sequence; a lone esc quits; ignore alt/partial
+ if i+1 < len(b) && b[i+1] == '[' {
+ j := i + 2
+ for j < len(b) && (b[j] < 0x40 || b[j] > 0x7e) {
+ j++
+ }
+ if j < len(b) {
+ switch param, final := string(b[i+2:j]), b[j]; {
+ case final == 'A':
+ a.move(-1)
+ case final == 'B':
+ a.move(1)
+ case final == '~' && param == "5":
+ a.move(-(a.rows - 2))
+ case final == '~' && param == "6":
+ a.move(a.rows - 2)
+ }
+ i = j + 1
+ continue
+ }
+ }
+ if len(b) == 1 {
+ return true
+ }
+ // alt-prefixed key or a split escape: drop the esc, keep going
+ case c == 3: // ctrl-c
+ return true
+ case c == 9: // tab
+ a.content = !a.content
+ changed = true
+ case c == 13 || c == 10: // enter
+ a.open()
+ case c == 15: // ctrl-o
+ a.openURL()
+ case c == 25: // ctrl-y
+ a.yank()
+ case c == 14: // ctrl-n
+ a.move(1)
+ case c == 16: // ctrl-p
+ a.move(-1)
+ case c == 21: // ctrl-u
+ a.query = a.query[:0]
+ changed = true
+ case c == 127 || c == 8: // backspace
+ if len(a.query) > 0 {
+ a.query = a.query[:len(a.query)-1]
+ changed = true
+ }
+ case c < 32:
+ default:
+ r, n := utf8.DecodeRune(b[i:])
+ if r == utf8.RuneError && n <= 1 {
+ i++ // invalid or a multibyte rune split across reads
+ continue
+ }
+ if r >= 32 && r != 0x7f {
+ a.query = append(a.query, r)
+ changed = true
+ }
+ i += n
+ continue
+ }
+ i++
+ }
+ if changed {
+ a.search()
+ }
+ return false
+}
+
+func (a *app) move(d int) {
+ if len(a.results) > 0 {
+ a.sel = max(0, min(a.sel+d, len(a.results)-1))
+ }
+}
+
+// cur returns the selected match, or false if there is none
+func (a *app) cur() (match, bool) {
+ if a.sel < len(a.results) {
+ return a.results[a.sel], true
+ }
+ return match{}, false
+}
+
+func (a *app) draw() {
+ a.size()
+ listH := a.rows - 1
+ leftW := min(max(a.cols*2/5, 24), a.cols-10)
+ prevW := a.cols - leftW - 1
+
+ if a.sel < a.top {
+ a.top = a.sel
+ }
+ if a.sel >= a.top+listH {
+ a.top = a.sel - listH + 1
+ }
+
+ var sb strings.Builder
+ sb.WriteString("\x1b[H\x1b[2J")
+
+ mode := "content"
+ if !a.content {
+ mode = "browse"
+ }
+ prompt := fmt.Sprintf("\x1b[1m%s\x1b[0m ▸ %s", mode, string(a.query))
+ count := fmt.Sprintf("\x1b[2m%d/%d\x1b[0m", min(len(a.results), a.sel+1), len(a.results))
+ sb.WriteString(pad(prompt, a.cols-visLen(count)) + count + "\r\n")
+
+ prev := a.previewLines(prevW, listH)
+ for i := 0; i < listH; i++ {
+ left := strings.Repeat(" ", leftW)
+ if ri := a.top + i; ri < len(a.results) {
+ left = padRaw(truncate(a.results[ri].text(), leftW), leftW)
+ if ri == a.sel {
+ left = "\x1b[7m" + left + "\x1b[0m"
+ }
+ }
+ right := ""
+ if i < len(prev) {
+ right = prev[i]
+ }
+ sb.WriteString(left + " \x1b[2m│\x1b[0m " + right + "\r\n")
+ }
+ fmt.Print(strings.TrimSuffix(sb.String(), "\r\n"))
+}
+
+// previewLines renders the selection's text centred on the matched line
+func (a *app) previewLines(w, h int) []string {
+ m, ok := a.cur()
+ if !ok || w < 4 {
+ return nil
+ }
+ if len(m.d.lines) == 0 {
+ return []string{"\x1b[2m(no cached text)\x1b[0m"}
+ }
+ start := 0
+ if m.line > 0 {
+ start = max(0, m.line-1-h/2)
+ }
+ var out []string
+ for i := start; i < len(m.d.lines) && len(out) < h; i++ {
+ row := truncate(m.d.lines[i], w-2)
+ if i+1 == m.line {
+ out = append(out, "\x1b[1m"+row+"\x1b[0m")
+ } else {
+ out = append(out, "\x1b[2m"+row+"\x1b[0m")
+ }
+ }
+ return out
+}
+
+func (a *app) withExternal(fn func()) {
+ a.leave()
+ fn()
+ a.resume()
+}
+
+func (a *app) open() {
+ m, ok := a.cur()
+ if !ok {
+ return
+ }
+ src := m.d.src
+ a.withExternal(func() {
+ if strings.HasSuffix(strings.ToLower(src), ".md") {
+ tmp, err := os.CreateTemp("", "wrd-*.html")
+ if err != nil {
+ return
+ }
+ defer func() { _ = os.Remove(tmp.Name()) }()
+ out, _ := exec.Command("lowdown", src).Output()
+ _, _ = tmp.Write(out)
+ _ = tmp.Close()
+ src = tmp.Name()
+ }
+ runTTY(exec.Command("cha", src))
+ })
+}
+
+func (a *app) openURL() {
+ if m, ok := a.cur(); ok {
+ u := url(m.d.rel)
+ a.withExternal(func() { runTTY(exec.Command("cha", u)) })
+ }
+}
+
+func (a *app) yank() {
+ m, ok := a.cur()
+ if !ok {
+ return
+ }
+ cmd := exec.Command("wl-copy")
+ cmd.Stdin = strings.NewReader(url(m.d.rel))
+ _ = cmd.Run()
+}
+
+func runTTY(cmd *exec.Cmd) {
+ cmd.Stdin, cmd.Stdout, cmd.Stderr = os.Stdin, os.Stdout, os.Stderr
+ _ = cmd.Run()
+}
+
+// visLen counts visible runes, ignoring SGR escapes
+func visLen(s string) int {
+ n, esc := 0, false
+ for _, r := range s {
+ switch {
+ case esc:
+ esc = r != 'm'
+ case r == 0x1b:
+ esc = true
+ default:
+ n++
+ }
+ }
+ return n
+}
+
+func truncate(s string, w int) string {
+ rs := []rune(s)
+ if w <= 0 {
+ return ""
+ }
+ if len(rs) <= w {
+ return s
+ }
+ if w == 1 {
+ return string(rs[:1])
+ }
+ return string(rs[:w-1]) + "…"
+}
+
+func padRaw(s string, w int) string {
+ rs := []rune(s)
+ if len(rs) >= w {
+ return string(rs[:w])
+ }
+ return s + strings.Repeat(" ", w-len(rs))
+}
+
+func pad(s string, w int) string {
+ if v := visLen(s); v < w {
+ return s + strings.Repeat(" ", w-v)
+ }
+ return s
+}