tools

various tools I have been using throughout the years
Log | Files | Refs | README | LICENSE

commit 442591b5aa83d4c5778b0c087f9c11fe605554a8
parent 352c5af8004bc103e79dc90de7d207e880861514
Author: mtmn <miro@haravara.org>
Date:   Sat, 20 Jun 2026 23:42:08 +0200

add wrd, update rust deps

Diffstat:
MCargo.lock | 12++++++------
MMODULE.bazel | 5+++++
MMODULE.bazel.lock | 348++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++-
MREADME.md | 2++
Awrd/BUILD | 23+++++++++++++++++++++++
Awrd/README.md | 52++++++++++++++++++++++++++++++++++++++++++++++++++++
Awrd/go.mod | 3+++
Awrd/main.go | 244+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Awrd/tui.go | 332+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
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 +}