From 68f3c4fa4e604ac43e8ee1f23939da1db29143a8 Mon Sep 17 00:00:00 2001 From: John Doty Date: Tue, 13 Aug 2024 07:23:59 -0700 Subject: [PATCH 01/25] Experimental updates to release workflow --- .github/workflows/release.yaml | 51 ++++++++++++++++++++++------------ 1 file changed, 33 insertions(+), 18 deletions(-) diff --git a/.github/workflows/release.yaml b/.github/workflows/release.yaml index dc2607c..4ef6b2f 100644 --- a/.github/workflows/release.yaml +++ b/.github/workflows/release.yaml @@ -13,27 +13,43 @@ on: tags: - "v[0-9]+.[0-9]+.[0-9]+" +permissions: + contents: write + jobs: create_release: name: Create release - runs-on: ubuntu-22.04 - - outputs: - upload_url: ${{ steps.create_release.outputs.upload_url }} + runs-on: ubuntu-latest steps: + - uses: actions/checkout@v4 + + - name: Get the release version + if: env.VERSION == '' + run: echo "VERSION=${{ github.ref_name }}" >> $GITHUB_ENV + + - name: Show the version + run: | + echo "version is: $VERSION" + + - name: Check that tag version and Cargo.toml version are the same + shell: bash + run: | + if ! grep -q "version = \"$VERSION\"" Cargo.toml; then + echo "version does not match Cargo.toml" >&2 + exit 1 + fi + - name: Create GitHub release - id: create_release - uses: actions/create-release@v1 env: GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} - with: - tag_name: ${{ github.ref }} - release_name: Release ${{ github.ref }} - draft: true + run: gh release create $VERSION --draft --verify-tag --title $VERSION - release_assets: - name: Release assets + outputs: + version: ${{ env.VERSION }} + + build_release: + name: Build all the stuff needs: ['create_release'] # We need to know the upload URL runs-on: ${{ matrix.os }} # We run many different builds env: @@ -48,6 +64,7 @@ jobs: RUST_BACKTRACE: 1 strategy: + fail-fast: false matrix: build: ['linux', 'macos', 'arm-macos', 'windows'] include: @@ -114,11 +131,9 @@ jobs: fi - name: Upload release archive - uses: actions/upload-release-asset@v1.0.2 env: GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} - with: - upload_url: ${{ needs.create_release.outputs.upload_url }} - asset_name: ${{ env.ASSET }} - asset_path: ${{ env.ASSET }} - asset_content_type: application/octet-stream + shell: bash + run: | + version="${{ needs.create_release.outputs.version }}" + gh release upload "$version" ${{ env.ASSET }} From 7e047626dfb47fdef6dea6e3f937cf895b110853 Mon Sep 17 00:00:00 2001 From: John Doty Date: Tue, 13 Aug 2024 07:24:29 -0700 Subject: [PATCH 02/25] Bump to the next version --- Cargo.lock | 2 +- Cargo.toml | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index 18e0d1f..f77556e 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -320,7 +320,7 @@ checksum = "eb1d22c66e66d9d72e1758f0bd7d4fd0bee04cad842ee34587d68c07e45d088c" [[package]] name = "fwd" -version = "0.9.1" +version = "0.9.2" dependencies = [ "anyhow", "assert_matches", diff --git a/Cargo.toml b/Cargo.toml index 107266e..97dfd81 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "fwd" -version = "0.9.1" +version = "0.9.2" edition = "2021" license = "MIT" description = "Automatically forward ports to a remote server over ssh" From b381f716926a6d499182e762ceac44161b10e0a6 Mon Sep 17 00:00:00 2001 From: John Doty Date: Tue, 13 Aug 2024 10:44:58 -0700 Subject: [PATCH 03/25] Move from tui to ratatui Tui is no longer supported, ratatui is the new hotness. Fortunately there is very little difference between the two, except I've noticed a fun new bug in the help screen. (Maybe it's been there the whole time?) --- Cargo.lock | 617 +++++++++++++++++++++++++++++++---------------- Cargo.toml | 4 +- src/client/ui.rs | 38 ++- 3 files changed, 423 insertions(+), 236 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index f77556e..111ae68 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -4,9 +4,9 @@ version = 3 [[package]] name = "addr2line" -version = "0.21.0" +version = "0.22.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8a30b2e23b9e17a9f90641c7ab1549cd9b44f296d3ccbf309d2863cfe398a0cb" +checksum = "6e4503c46a5c0c7844e948c9a4d6acd9f50cccb4de1c48eb9e291ea17470c678" dependencies = [ "gimli", ] @@ -17,6 +17,24 @@ version = "1.0.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "f26201604c87b1e01bd3d98f8d5d9a8fcbb815e8cedb41ffccbeb4bf593a35fe" +[[package]] +name = "ahash" +version = "0.8.11" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e89da841a80418a9b391ebaea17f5c112ffaaa96f621d2c285b5174da76b9011" +dependencies = [ + "cfg-if", + "once_cell", + "version_check", + "zerocopy", +] + +[[package]] +name = "allocator-api2" +version = "0.2.18" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5c6cb57a04249c6480766f7f7cef5467412af1490f8d1e243141daddada3264f" + [[package]] name = "android-tzdata" version = "0.1.1" @@ -34,9 +52,9 @@ dependencies = [ [[package]] name = "anyhow" -version = "1.0.75" +version = "1.0.86" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a4668cab20f66d8d020e1fbc0ebe47217433c1b6c8f2040faf858554e394ace6" +checksum = "b3d1d046238990b9cf5bcde22a3fb3584ee5cf65fb2765f454ed428c7a0063da" [[package]] name = "assert_matches" @@ -46,15 +64,15 @@ checksum = "9b34d609dfbaf33d6889b2b7106d3ca345eacad44200913df5ba02bfd31d2ba9" [[package]] name = "autocfg" -version = "1.1.0" +version = "1.3.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d468802bab17cbc0cc575e9b053f41e72aa36bfa6b7f55e3529ffa43161b97fa" +checksum = "0c4b4d0bd25bd0b74681c0ad21497610ce1b7c91b1022cd21c80c6fbdd9476b0" [[package]] name = "backtrace" -version = "0.3.69" +version = "0.3.73" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2089b7e3f35b9dd2d0ed921ead4f6d318c27680d4a5bd167b3ee120edb105837" +checksum = "5cc23269a4f8976d0a4d2e7109211a419fe30e8d88d677cd60b6bc79c5732e0a" dependencies = [ "addr2line", "cc", @@ -73,9 +91,9 @@ checksum = "bef38d45163c2f1dde094a7dfd33ccf595c92905c8f8f4fdc18d06fb1037718a" [[package]] name = "bitflags" -version = "2.5.0" +version = "2.6.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "cf4b9d6a944f767f8e5e0db018570623c85f3d925ac718db4e06d0187adb21c1" +checksum = "b048fb63fd8b5923fc5aa7b340d8e156aec7ec02f0c78fa8a6ddc2613f6f71de" [[package]] name = "block" @@ -85,9 +103,9 @@ checksum = "0d8c1fef690941d3e7788d328517591fecc684c084084702d6ff1641e993699a" [[package]] name = "bumpalo" -version = "3.14.0" +version = "3.16.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7f30e7476521f6f8af1a1c4c0b8cc94f0bee37d91763d0ca2665f299b6cd8aec" +checksum = "79296716171880943b8470b5f8d03aa55eb2e645a4874bdbb28adb49162e012c" [[package]] name = "byteorder" @@ -97,17 +115,17 @@ checksum = "1fd0f2584146f6f2ef48085050886acf353beff7305ebd1ae69500e27c67f64b" [[package]] name = "bytes" -version = "1.5.0" +version = "1.7.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a2bd12c1caf447e69cd4528f47f94d203fd2582878ecb9e9465484c4148a8223" +checksum = "8318a53db07bb3f8dca91a600466bdb3f2eaadeedfdbcf02e1accbad9271ba50" [[package]] name = "calloop" -version = "0.12.4" +version = "0.13.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "fba7adb4dd5aa98e5553510223000e7148f621165ec5f9acd7113f6ca4995298" +checksum = "b99da2f8558ca23c71f4fd15dc57c906239752dd27ff3c00a1d56b685b7cbfec" dependencies = [ - "bitflags 2.5.0", + "bitflags 2.6.0", "log", "polling", "rustix 0.38.34", @@ -117,9 +135,9 @@ dependencies = [ [[package]] name = "calloop-wayland-source" -version = "0.2.0" +version = "0.3.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0f0ea9b9476c7fad82841a8dbb380e2eae480c21910feba80725b46931ed8f02" +checksum = "95a66a987056935f7efce4ab5668920b5d0dac4a7c99991a67395f13702ddd20" dependencies = [ "calloop", "rustix 0.38.34", @@ -134,14 +152,20 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "df8670b8c7b9dae1793364eafadf7239c40d669904660c5960d74cfd80b46a53" [[package]] -name = "cc" -version = "1.0.83" +name = "castaway" +version = "0.2.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f1174fb0b6ec23863f8b971027804a42614e347eafb0a95bf0b12cdae21fc4d0" +checksum = "0abae9be0aaf9ea96a3b1b8b1b55c602ca751eba1b1500220cea4ecbafe7c0d5" dependencies = [ - "libc", + "rustversion", ] +[[package]] +name = "cc" +version = "1.1.10" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e9e8aabfac534be767c909e0690571677d49f41bd8465ae876fe043d52ba5292" + [[package]] name = "cfg-if" version = "1.0.0" @@ -150,14 +174,14 @@ checksum = "baf1de4339761588bc0619e3cbc0120ee582ebb74b53b4efbf79117bd2da40fd" [[package]] name = "chrono" -version = "0.4.31" +version = "0.4.38" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7f2c685bad3eb3d45a01354cedb7d5faa66194d1d58ba6e267a8de788f79db38" +checksum = "a21f936df1771bf62b77f047b726c4625ff2e8aa607c01ec06e5a05bd8463401" dependencies = [ "android-tzdata", "iana-time-zone", "num-traits", - "windows-targets 0.48.5", + "windows-targets 0.52.6", ] [[package]] @@ -170,6 +194,20 @@ dependencies = [ "winapi", ] +[[package]] +name = "compact_str" +version = "0.8.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6050c3a16ddab2e412160b31f2c871015704239bca62f72f6e5f0be631d3f644" +dependencies = [ + "castaway", + "cfg-if", + "itoa", + "rustversion", + "ryu", + "static_assertions", +] + [[package]] name = "concurrent-queue" version = "2.5.0" @@ -195,15 +233,15 @@ dependencies = [ [[package]] name = "core-foundation-sys" -version = "0.8.4" +version = "0.8.7" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e496a50fda8aacccc86d7529e2c1e0892dbd0f898a6b5645b5561b89c3210efa" +checksum = "773648b94d0e5d620f64f280777445740e61fe701025087ec8b57f45c791888b" [[package]] name = "crc32fast" -version = "1.3.2" +version = "1.4.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b540bd8bc810d3885c6ea91e2018302f68baba2129ab3e88f32389ee9370880d" +checksum = "a97769d94ddab943e4510d138150169a2758b5ef3eb191a9ee688de3e23ef7b3" dependencies = [ "cfg-if", ] @@ -216,16 +254,16 @@ checksum = "22ec99545bb0ed0ea7bb9b8e1e9122ea386ff8a48c0922e43f36d45ab09e0e80" [[package]] name = "crossterm" -version = "0.25.0" +version = "0.28.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e64e6c0fbe2c17357405f7c758c1ef960fce08bdfb2c03d88d2a18d7e09c4b67" +checksum = "829d955a0bb380ef178a640b91779e3987da38c9aea133b20614cfed8cdea9c6" dependencies = [ - "bitflags 1.3.2", + "bitflags 2.6.0", "crossterm_winapi", "futures-core", - "libc", "mio", "parking_lot", + "rustix 0.38.34", "signal-hook", "signal-hook-mio", "winapi", @@ -267,6 +305,12 @@ version = "1.2.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "75b325c5dbd37f80359721ad39aca5a29fb04c89279657cffdda8736d0c0b9d2" +[[package]] +name = "either" +version = "1.13.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "60b1af1c220855b6ceac025d3f6ecdd2b7c4894bfe9cd9bda4fbb4bc7c0d4cf0" + [[package]] name = "env_filter" version = "0.1.2" @@ -298,9 +342,9 @@ dependencies = [ [[package]] name = "flate2" -version = "1.0.28" +version = "1.0.31" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "46303f565772937ffe1d394a4fac6f411c6013172fadde9dcdb1e147a086940e" +checksum = "7f211bbe8e69bbd0cfdea405084f128ae8b4aaa6b0b522fc8f2b009084797920" dependencies = [ "crc32fast", "miniz_oxide", @@ -314,9 +358,9 @@ checksum = "a06f77d526c1a601b7c4cdd98f54b5eaabffc14d5f2f0296febdc7f357c6d3ba" [[package]] name = "futures-core" -version = "0.3.29" +version = "0.3.30" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "eb1d22c66e66d9d72e1758f0bd7d4fd0bee04cad842ee34587d68c07e45d088c" +checksum = "dfc6580bb841c5a68e9ef15c77ccc837b40a7504914d52e47b8b0e9bbda25a1d" [[package]] name = "fwd" @@ -336,12 +380,12 @@ dependencies = [ "pretty_assertions", "procfs", "rand 0.8.5", + "ratatui", "tempdir", "thiserror", "tokio", "tokio-stream", "toml", - "tui", "xdg", ] @@ -357,9 +401,9 @@ dependencies = [ [[package]] name = "getrandom" -version = "0.2.14" +version = "0.2.15" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "94b22e06ecb0110981051723910cbf0b5f5e09a2062dd7663334ee79a9d1286c" +checksum = "c4567c8db10ae91089c99af84c68c38da3ec2f087c3f82960bcdbf3656b6f4d7" dependencies = [ "cfg-if", "libc", @@ -368,15 +412,31 @@ dependencies = [ [[package]] name = "gimli" -version = "0.28.1" +version = "0.29.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4271d37baee1b8c7e4b708028c57d816cf9d2434acb33a549475f78c181f6253" +checksum = "40ecd4077b5ae9fd2e9e169b102c6c330d0605168eb0e8bf79952b256dbefffd" + +[[package]] +name = "hashbrown" +version = "0.14.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e5274423e17b7c9fc20b6e7e208532f9b19825d82dfd615708b70edd83df41f1" +dependencies = [ + "ahash", + "allocator-api2", +] + +[[package]] +name = "heck" +version = "0.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2304e00983f87ffb38b55b444b5e3b60a884b5d30c0fca7d82fe33449bbe55ea" [[package]] name = "hermit-abi" -version = "0.3.3" +version = "0.3.9" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d77f7ec81a6d05a3abb01ab6eb7590f6083d08449fe5a1c8b1e620283546ccb7" +checksum = "d231dfb89cfffdbc30e7fc41579ed6066ad03abda9e567ccafae602b97ec5024" [[package]] name = "hermit-abi" @@ -392,18 +452,18 @@ checksum = "7f24254aa9a54b5c858eaee2f5bccdb46aaf0e486a595ed5fd8f86ba55232a70" [[package]] name = "home" -version = "0.5.5" +version = "0.5.9" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5444c27eef6923071f7ebcc33e3444508466a76f7a2b93da00ed6e19f30c1ddb" +checksum = "e3d1354bf6b7235cb4a0576c2619fd4ed18183f689b12b006a0ee7329eeff9a5" dependencies = [ - "windows-sys 0.48.0", + "windows-sys 0.52.0", ] [[package]] name = "iana-time-zone" -version = "0.1.58" +version = "0.1.60" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8326b86b6cff230b97d0d312a6c40a60726df3332e721f72a1b035f451663b20" +checksum = "e7ffbb5a1b541ea2561f8c41c087286cc091e21e556a4f09a8f6cbf17b69b141" dependencies = [ "android_system_properties", "core-foundation-sys", @@ -428,22 +488,47 @@ version = "1.0.9" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "bfa799dd5ed20a7e349f3b4639aa80d74549c81716d9ec4f994c9b5815598306" +[[package]] +name = "instability" +version = "0.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b23a0c8dfe501baac4adf6ebbfa6eddf8f0c07f56b058cc1288017e32397846c" +dependencies = [ + "quote", + "syn", +] + [[package]] name = "io-lifetimes" version = "1.0.11" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "eae7b9aee968036d54dce06cebaefd919e4472e753296daccd6d344e3e2df0c2" dependencies = [ - "hermit-abi 0.3.3", + "hermit-abi 0.3.9", "libc", "windows-sys 0.48.0", ] [[package]] -name = "js-sys" -version = "0.3.65" +name = "itertools" +version = "0.13.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "54c0c35952f67de54bb584e9fd912b3023117cbafc0a77d8f3dee1fb5f572fe8" +checksum = "413ee7dfc52ee1a4949ceeb7dbc8a33f2d6c088194d9f922fb8318faf1f01186" +dependencies = [ + "either", +] + +[[package]] +name = "itoa" +version = "1.0.11" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "49f1f14873335454500d59611f1cf4a4b0f786f9ac11f4312a78e4cf2566695b" + +[[package]] +name = "js-sys" +version = "0.3.70" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1868808506b929d7b0cfa8f75951347aa71bb21144b7791bae35d9bccfcfe37a" dependencies = [ "wasm-bindgen", ] @@ -456,9 +541,9 @@ checksum = "10257499f089cd156ad82d0a9cd57d9501fa2c989068992a97eb3c27836f206b" [[package]] name = "lazy_static" -version = "1.4.0" +version = "1.5.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e2abad23fbc42b3700f2f279844dc832adb2b2eb069b2df918f455c4e18cc646" +checksum = "bbd2bcb4c963f2ddae06a2efc7e9f3591312473c50c6685e1f298068316e66fe" [[package]] name = "libc" @@ -468,12 +553,12 @@ checksum = "97b3888a4aecf77e811145cadf6eef5901f4782c53886191b2f693f24761847c" [[package]] name = "libloading" -version = "0.8.4" +version = "0.8.5" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e310b3a6b5907f99202fcdb4960ff45b93735d7c7d96b760fcff8db2dc0e103d" +checksum = "4979f22fdb869068da03c9f7528f8297c6fd2606bc3a4affe42e6a823fdb8da4" dependencies = [ "cfg-if", - "windows-targets 0.52.5", + "windows-targets 0.52.6", ] [[package]] @@ -490,9 +575,9 @@ checksum = "78b3ae25bc7c8c38cec158d1f2757ee79e9b3740fbc7ccf0e59e4b08d793fa89" [[package]] name = "lock_api" -version = "0.4.11" +version = "0.4.12" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3c168f8615b12bc01f9c17e2eb0cc07dcae1940121185446edc3744920e8ef45" +checksum = "07af8b9cdd281b7915f413fa73f29ebd5d55d0d3f0155584dade1ff18cea1b17" dependencies = [ "autocfg", "scopeguard", @@ -504,6 +589,15 @@ version = "0.4.22" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "a7a70ba024b9dc04c27ea2f0c0548feb474ec5c54bba33a7f72f873a39d07b24" +[[package]] +name = "lru" +version = "0.12.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "37ee39891760e7d94734f6f63fedc29a2e4a152f836120753a72503f09fcf904" +dependencies = [ + "hashbrown", +] + [[package]] name = "malloc_buf" version = "0.0.6" @@ -515,9 +609,9 @@ dependencies = [ [[package]] name = "memchr" -version = "2.6.4" +version = "2.7.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f665ee40bc4a3c5590afb1e9677db74a508659dfd71e126420da8274909a0167" +checksum = "78ca9ab1a0babb1e7d5695e3530886289c18cf2f87ec19a575a0abdce112e3a3" [[package]] name = "memmap2" @@ -530,44 +624,35 @@ dependencies = [ [[package]] name = "miniz_oxide" -version = "0.7.1" +version = "0.7.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e7810e0be55b428ada41041c41f32c9f1a42817901b4ccf45fa3d4b6561e74c7" +checksum = "b8a240ddb74feaf34a79a7add65a741f3167852fba007066dcac1ca548d89c08" dependencies = [ "adler", ] [[package]] name = "mio" -version = "0.8.9" +version = "1.0.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3dce281c5e46beae905d4de1870d8b1509a9142b62eedf18b443b011ca8343d0" +checksum = "80e04d1dcff3aae0704555fe5fee3bcfaf3d1fdf8a7e521d5b9d2b42acb52cec" dependencies = [ + "hermit-abi 0.3.9", "libc", "log", "wasi", - "windows-sys 0.48.0", + "windows-sys 0.52.0", ] [[package]] name = "num-traits" -version = "0.2.17" +version = "0.2.19" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "39e3200413f237f41ab11ad6d161bc7239c84dcb631773ccd7de3dfe4b5c267c" +checksum = "071dfc062690e90b734c0b2273ce72ad0ffa95f0c74596bc250dcfd960262841" dependencies = [ "autocfg", ] -[[package]] -name = "num_cpus" -version = "1.16.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4161fcb6d602d4d2081af7c3a45852d875a03dd337a6bfdd6e06407b61342a43" -dependencies = [ - "hermit-abi 0.3.3", - "libc", -] - [[package]] name = "objc" version = "0.2.7" @@ -599,18 +684,18 @@ dependencies = [ [[package]] name = "object" -version = "0.32.1" +version = "0.36.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9cf5f9dd3933bd50a9e1f149ec995f39ae2c496d31fd772c1fd45ebc27e902b0" +checksum = "27b64972346851a39438c60b341ebc01bba47464ae329e55cf343eb93964efd9" dependencies = [ "memchr", ] [[package]] name = "once_cell" -version = "1.18.0" +version = "1.19.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "dd8b5dd2ae5ed71462c540258bedcb51965123ad7e7ccf4b9a8cafaa4a63576d" +checksum = "3fdb12b2476b595f9358c5161aa467c2438859caa136dec86c26fdd2efe17b92" [[package]] name = "open" @@ -624,9 +709,9 @@ dependencies = [ [[package]] name = "parking_lot" -version = "0.12.1" +version = "0.12.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3742b2c103b9f06bc9fff0a37ff4912935851bee6d36f3c02bcc755bcfec228f" +checksum = "f1bf18183cf54e8d6059647fc3063646a1801cf30896933ec2311622cc4b9a27" dependencies = [ "lock_api", "parking_lot_core", @@ -634,17 +719,23 @@ dependencies = [ [[package]] name = "parking_lot_core" -version = "0.9.9" +version = "0.9.10" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4c42a9226546d68acdd9c0a280d17ce19bfe27a46bf68784e4066115788d008e" +checksum = "1e401f977ab385c9e4e3ab30627d6f26d00e2c73eef317493c4ec6d468726cf8" dependencies = [ "cfg-if", "libc", "redox_syscall", "smallvec", - "windows-targets 0.48.5", + "windows-targets 0.52.6", ] +[[package]] +name = "paste" +version = "1.0.15" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "57c0d7b74b563b49d38dae00a0c37d4d6de9b432382b2892f0574ddcae73fd0a" + [[package]] name = "pathdiff" version = "0.2.1" @@ -653,9 +744,9 @@ checksum = "8835116a5c179084a830efb3adc117ab007512b535bc1a21c991d3b32a6b44dd" [[package]] name = "pin-project-lite" -version = "0.2.13" +version = "0.2.14" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8afb450f006bf6385ca15ef45d71d2288452bc3683ce2e2cacc0d18e4be60b58" +checksum = "bda66fc9667c18cb2758a2ac84d1167245054bcf85d5d1aaa6923f45801bdd02" [[package]] name = "pkg-config" @@ -665,9 +756,9 @@ checksum = "d231b230927b5e4ad203db57bbcbee2802f6bce620b1e4a9024a07d94e2907ec" [[package]] name = "polling" -version = "3.7.2" +version = "3.7.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a3ed00ed3fbf728b5816498ecd316d1716eecaced9c0c8d2c5a6740ca214985b" +checksum = "cc2790cd301dec6cd3b7a025e4815cf825724a51c98dccfe6a3e55f05ffb6511" dependencies = [ "cfg-if", "concurrent-queue", @@ -675,14 +766,17 @@ dependencies = [ "pin-project-lite", "rustix 0.38.34", "tracing", - "windows-sys 0.52.0", + "windows-sys 0.59.0", ] [[package]] name = "ppv-lite86" -version = "0.2.17" +version = "0.2.20" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5b40af805b3121feab8a3c29f04d8ad262fa8e0561883e7653e024ae4479e6de" +checksum = "77957b295656769bb8ad2b6a6b09d897d94f05c41b069aede1fcdaa675eaea04" +dependencies = [ + "zerocopy", +] [[package]] name = "pretty_assertions" @@ -696,9 +790,9 @@ dependencies = [ [[package]] name = "proc-macro2" -version = "1.0.69" +version = "1.0.86" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "134c189feb4956b20f6f547d2cf727d4c0fe06722b20a0eec87ed445a97f92da" +checksum = "5e719e8df665df0d1c8fbfd238015744736151d4445ec0836b8e628aae103b77" dependencies = [ "unicode-ident", ] @@ -720,18 +814,18 @@ dependencies = [ [[package]] name = "quick-xml" -version = "0.31.0" +version = "0.34.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1004a344b30a54e2ee58d66a71b32d2db2feb0a31f9a2d302bf0536f15de2a33" +checksum = "6f24d770aeca0eacb81ac29dfbc55ebcc09312fdd1f8bbecdc7e4a84e000e3b4" dependencies = [ "memchr", ] [[package]] name = "quote" -version = "1.0.33" +version = "1.0.36" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5267fca4496028628a95160fc423a33e8b2e6af8a5302579e322e4b520293cae" +checksum = "0fa76aaf39101c457836aec0ce2316dbdc3ab723cdda1c6bd4e6ad4208acaca7" dependencies = [ "proc-macro2", ] @@ -794,6 +888,27 @@ dependencies = [ "getrandom", ] +[[package]] +name = "ratatui" +version = "0.28.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5ba6a365afbe5615999275bea2446b970b10a41102500e27ce7678d50d978303" +dependencies = [ + "bitflags 2.6.0", + "cassowary", + "compact_str", + "crossterm", + "instability", + "itertools", + "lru", + "paste", + "strum", + "strum_macros", + "unicode-segmentation", + "unicode-truncate", + "unicode-width", +] + [[package]] name = "rdrand" version = "0.4.0" @@ -805,11 +920,11 @@ dependencies = [ [[package]] name = "redox_syscall" -version = "0.4.1" +version = "0.5.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4722d768eff46b75989dd134e5c353f0d6296e5aaa3132e776cbdb56be7731aa" +checksum = "2a908a6e00f1fdd0dfd9c0eb08ce85126f6d8bbda50017e74bc4a4b7d4a926a4" dependencies = [ - "bitflags 1.3.2", + "bitflags 2.6.0", ] [[package]] @@ -823,9 +938,9 @@ dependencies = [ [[package]] name = "rustc-demangle" -version = "0.1.23" +version = "0.1.24" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d626bb9dae77e28219937af045c257c28bfd3f69333c512553507f5f9798cb76" +checksum = "719b953e2095829ee67db738b3bfa9fa368c94900df327b3f07fe6e794d2fe1f" [[package]] name = "rustix" @@ -847,13 +962,25 @@ version = "0.38.34" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "70dc5ec042f7a43c4a73241207cecc9873a06d45debb38b329f8541d85c2730f" dependencies = [ - "bitflags 2.5.0", + "bitflags 2.6.0", "errno", "libc", "linux-raw-sys 0.4.14", "windows-sys 0.52.0", ] +[[package]] +name = "rustversion" +version = "1.0.17" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "955d28af4278de8121b7ebeb796b6a45735dc01436d898801014aced2773a3d6" + +[[package]] +name = "ryu" +version = "1.0.18" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f3cb5ba0dc43242ce17de99c180e96db90b235b8a9fdc9543c96d2209116bd9f" + [[package]] name = "scoped-tls" version = "1.0.1" @@ -868,18 +995,18 @@ checksum = "94143f37725109f92c262ed2cf5e59bce7498c01bcc1502d7b9afe439a4e9f49" [[package]] name = "serde" -version = "1.0.193" +version = "1.0.207" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "25dd9975e68d0cb5aa1120c288333fc98731bd1dd12f561e468ea4728c042b89" +checksum = "5665e14a49a4ea1b91029ba7d3bca9f299e1f7cfa194388ccc20f14743e784f2" dependencies = [ "serde_derive", ] [[package]] name = "serde_derive" -version = "1.0.193" +version = "1.0.207" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "43576ca501357b9b071ac53cdc7da8ef0cbd9493d8df094cd821777ea6e894d3" +checksum = "6aea2634c86b0e8ef2cfdc0c340baede54ec27b1e46febd7f80dffb2aa44a00e" dependencies = [ "proc-macro2", "quote", @@ -898,9 +1025,9 @@ dependencies = [ [[package]] name = "signal-hook-mio" -version = "0.2.3" +version = "0.2.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "29ad2e15f37ec9a6cc544097b78a1ec90001e9f71b81338ca39f430adaca99af" +checksum = "34db1a06d485c9142248b7a054f034b349b212551f3dfd19c94d45a754a217cd" dependencies = [ "libc", "mio", @@ -909,9 +1036,9 @@ dependencies = [ [[package]] name = "signal-hook-registry" -version = "1.4.1" +version = "1.4.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d8229b473baa5980ac72ef434c4415e70c4b5e71b423043adb4ba059f89c99a1" +checksum = "a9e9e0b4211b72e7b8b6e85c807d36c212bdb33ea8587f7569562a84df5465b1" dependencies = [ "libc", ] @@ -927,17 +1054,17 @@ dependencies = [ [[package]] name = "smallvec" -version = "1.11.2" +version = "1.13.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4dccd0940a2dcdf68d092b8cbab7dc0ad8fa938bf95787e1b916b0e3d0e8e970" +checksum = "3c5e1a9a646d36c3599cd173a41282daf47c44583ad367b8e6837255952e5c67" [[package]] name = "smithay-client-toolkit" -version = "0.18.1" +version = "0.19.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "922fd3eeab3bd820d76537ce8f582b1cf951eceb5475c28500c7457d9d17f53a" +checksum = "3457dea1f0eb631b4034d61d4d8c32074caa6cd1ab2d59f2327bd8461e2c0016" dependencies = [ - "bitflags 2.5.0", + "bitflags 2.6.0", "calloop", "calloop-wayland-source", "cursor-icon", @@ -958,9 +1085,9 @@ dependencies = [ [[package]] name = "smithay-clipboard" -version = "0.7.1" +version = "0.7.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c091e7354ea8059d6ad99eace06dd13ddeedbb0ac72d40a9a6e7ff790525882d" +checksum = "cc8216eec463674a0e90f29e0ae41a4db573ec5b56b1c6c1c71615d249b6d846" dependencies = [ "libc", "smithay-client-toolkit", @@ -969,19 +1096,47 @@ dependencies = [ [[package]] name = "socket2" -version = "0.5.5" +version = "0.5.7" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7b5fac59a5cb5dd637972e5fca70daf0523c9067fcdc4842f053dae04a18f8e9" +checksum = "ce305eb0b4296696835b71df73eb912e0f1ffd2556a501fcede6e0c50349191c" dependencies = [ "libc", - "windows-sys 0.48.0", + "windows-sys 0.52.0", +] + +[[package]] +name = "static_assertions" +version = "1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a2eb9349b6444b326872e140eb1cf5e7c522154d69e7a0ffb0fb81c06b37543f" + +[[package]] +name = "strum" +version = "0.26.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8fec0f0aef304996cf250b31b5a10dee7980c85da9d759361292b8bca5a18f06" +dependencies = [ + "strum_macros", +] + +[[package]] +name = "strum_macros" +version = "0.26.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4c6bee85a5a24955dc440386795aa378cd9cf82acd5f764469152d2270e581be" +dependencies = [ + "heck", + "proc-macro2", + "quote", + "rustversion", + "syn", ] [[package]] name = "syn" -version = "2.0.39" +version = "2.0.74" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "23e78b90f2fcf45d3e842032ce32e3f2d1545ba6636271dcbf24fa306d87be7a" +checksum = "1fceb41e3d546d0bd83421d3409b1460cc7444cd389341a4c880fe7a042cb3d7" dependencies = [ "proc-macro2", "quote", @@ -1000,18 +1155,18 @@ dependencies = [ [[package]] name = "thiserror" -version = "1.0.50" +version = "1.0.63" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f9a7210f5c9a7156bb50aa36aed4c95afb51df0df00713949448cf9e97d382d2" +checksum = "c0342370b38b6a11b6cc11d6a805569958d54cfa061a29969c3b5ce2ea405724" dependencies = [ "thiserror-impl", ] [[package]] name = "thiserror-impl" -version = "1.0.50" +version = "1.0.63" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "266b2e40bc00e5a6c09c3584011e08b06f123c00362c92b975ba9843aaaa14b8" +checksum = "a4558b58466b9ad7ca0f102865eccc95938dca1a74a856f2b57b6629050da261" dependencies = [ "proc-macro2", "quote", @@ -1020,27 +1175,26 @@ dependencies = [ [[package]] name = "tokio" -version = "1.34.0" +version = "1.39.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d0c014766411e834f7af5b8f4cf46257aab4036ca95e9d2c144a10f59ad6f5b9" +checksum = "daa4fb1bc778bd6f04cbfc4bb2d06a7396a8f299dc33ea1900cedaa316f467b1" dependencies = [ "backtrace", "bytes", "libc", "mio", - "num_cpus", "pin-project-lite", "signal-hook-registry", "socket2", "tokio-macros", - "windows-sys 0.48.0", + "windows-sys 0.52.0", ] [[package]] name = "tokio-macros" -version = "2.2.0" +version = "2.4.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5b8a1e28f2deaa14e508979454cb3a223b10b938b45af148bc0986de36f1923b" +checksum = "693d596312e88961bc67d7f1f97af8a70227d9f90c31bba5806eec004978d752" dependencies = [ "proc-macro2", "quote", @@ -1083,19 +1237,6 @@ version = "0.1.32" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "c06d3da6113f116aaee68e4d601191614c9053067f9ab7f6edbcb161237daa54" -[[package]] -name = "tui" -version = "0.19.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ccdd26cbd674007e649a272da4475fb666d3aa0ad0531da7136db6fab0e5bad1" -dependencies = [ - "bitflags 1.3.2", - "cassowary", - "crossterm", - "unicode-segmentation", - "unicode-width", -] - [[package]] name = "unicode-ident" version = "1.0.12" @@ -1104,15 +1245,32 @@ checksum = "3354b9ac3fae1ff6755cb6db53683adb661634f67557942dea4facebec0fee4b" [[package]] name = "unicode-segmentation" -version = "1.10.1" +version = "1.11.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1dd624098567895118886609431a7c3b8f516e41d30e0643f03d94592a147e36" +checksum = "d4c87d22b6e3f4a18d4d40ef354e97c90fcb14dd91d7dc0aa9d8a1172ebf7202" + +[[package]] +name = "unicode-truncate" +version = "1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b3644627a5af5fa321c95b9b235a72fd24cd29c648c2c379431e6628655627bf" +dependencies = [ + "itertools", + "unicode-segmentation", + "unicode-width", +] [[package]] name = "unicode-width" -version = "0.1.11" +version = "0.1.13" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e51733f11c9c4f72aa0c160008246859e340b00807569a0da0e7a1079b27ba85" +checksum = "0336d538f7abc86d282a4189614dfaa90810dfc2c6f6427eaf88e16311dd225d" + +[[package]] +name = "version_check" +version = "0.9.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0b928f33d975fc6ad9f86c8f283853ad26bdd5b10b7f1542aa2fa15e2289105a" [[package]] name = "wasi" @@ -1122,19 +1280,20 @@ checksum = "9c8d87e72b64a3b4db28d11ce29237c246188f4f51057d65a7eab63b7987e423" [[package]] name = "wasm-bindgen" -version = "0.2.88" +version = "0.2.93" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7daec296f25a1bae309c0cd5c29c4b260e510e6d813c286b19eaadf409d40fce" +checksum = "a82edfc16a6c469f5f44dc7b571814045d60404b55a0ee849f9bcfa2e63dd9b5" dependencies = [ "cfg-if", + "once_cell", "wasm-bindgen-macro", ] [[package]] name = "wasm-bindgen-backend" -version = "0.2.88" +version = "0.2.93" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e397f4664c0e4e428e8313a469aaa58310d302159845980fd23b0f22a847f217" +checksum = "9de396da306523044d3302746f1208fa71d7532227f15e347e2d93e4145dd77b" dependencies = [ "bumpalo", "log", @@ -1147,9 +1306,9 @@ dependencies = [ [[package]] name = "wasm-bindgen-macro" -version = "0.2.88" +version = "0.2.93" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5961017b3b08ad5f3fe39f1e79877f8ee7c23c5e5fd5eb80de95abc41f1f16b2" +checksum = "585c4c91a46b072c92e908d99cb1dcdf95c5218eeb6f3bf1efa991ee7a68cccf" dependencies = [ "quote", "wasm-bindgen-macro-support", @@ -1157,9 +1316,9 @@ dependencies = [ [[package]] name = "wasm-bindgen-macro-support" -version = "0.2.88" +version = "0.2.93" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c5353b8dab669f5e10f5bd76df26a9360c748f054f862ff5f3f8aae0c7fb3907" +checksum = "afc340c74d9005395cf9dd098506f7f44e38f2b4a21c6aaacf9a105ea5e1e836" dependencies = [ "proc-macro2", "quote", @@ -1170,15 +1329,15 @@ dependencies = [ [[package]] name = "wasm-bindgen-shared" -version = "0.2.88" +version = "0.2.93" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0d046c5d029ba91a1ed14da14dca44b68bf2f124cfbaf741c54151fdb3e0750b" +checksum = "c62a0a307cb4a311d3a07867860911ca130c3494e8c2719593806c08bc5d0484" [[package]] name = "wayland-backend" -version = "0.3.4" +version = "0.3.6" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "34e9e6b6d4a2bb4e7e69433e0b35c7923b95d4dc8503a84d25ec917a4bbfdf07" +checksum = "f90e11ce2ca99c97b940ee83edbae9da2d56a08f9ea8158550fd77fa31722993" dependencies = [ "cc", "downcast-rs", @@ -1190,11 +1349,11 @@ dependencies = [ [[package]] name = "wayland-client" -version = "0.31.3" +version = "0.31.5" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1e63801c85358a431f986cffa74ba9599ff571fc5774ac113ed3b490c19a1133" +checksum = "7e321577a0a165911bdcfb39cf029302479d7527b517ee58ab0f6ad09edf0943" dependencies = [ - "bitflags 2.5.0", + "bitflags 2.6.0", "rustix 0.38.34", "wayland-backend", "wayland-scanner", @@ -1206,16 +1365,16 @@ version = "0.3.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "625c5029dbd43d25e6aa9615e88b829a5cad13b2819c4ae129fdbb7c31ab4c7e" dependencies = [ - "bitflags 2.5.0", + "bitflags 2.6.0", "cursor-icon", "wayland-backend", ] [[package]] name = "wayland-cursor" -version = "0.31.3" +version = "0.31.5" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a206e8b2b53b1d3fcb9428fec72bc278ce539e2fa81fe2bfc1ab27703d5187b9" +checksum = "6ef9489a8df197ebf3a8ce8a7a7f0a2320035c3743f3c1bd0bdbccf07ce64f95" dependencies = [ "rustix 0.38.34", "wayland-client", @@ -1224,11 +1383,11 @@ dependencies = [ [[package]] name = "wayland-protocols" -version = "0.31.2" +version = "0.32.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8f81f365b8b4a97f422ac0e8737c438024b5951734506b0e1d775c73030561f4" +checksum = "62989625a776e827cc0f15d41444a3cea5205b963c3a25be48ae1b52d6b4daaa" dependencies = [ - "bitflags 2.5.0", + "bitflags 2.6.0", "wayland-backend", "wayland-client", "wayland-scanner", @@ -1236,11 +1395,11 @@ dependencies = [ [[package]] name = "wayland-protocols-wlr" -version = "0.2.0" +version = "0.3.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ad1f61b76b6c2d8742e10f9ba5c3737f6530b4c243132c2a2ccc8aa96fe25cd6" +checksum = "fd993de54a40a40fbe5601d9f1fbcaef0aebcc5fda447d7dc8f6dcbaae4f8953" dependencies = [ - "bitflags 2.5.0", + "bitflags 2.6.0", "wayland-backend", "wayland-client", "wayland-protocols", @@ -1249,9 +1408,9 @@ dependencies = [ [[package]] name = "wayland-scanner" -version = "0.31.2" +version = "0.31.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "67da50b9f80159dec0ea4c11c13e24ef9e7574bd6ce24b01860a175010cea565" +checksum = "d7b56f89937f1cf2ee1f1259cf2936a17a1f45d8f0aa1019fae6d470d304cfa6" dependencies = [ "proc-macro2", "quick-xml", @@ -1260,9 +1419,9 @@ dependencies = [ [[package]] name = "wayland-sys" -version = "0.31.2" +version = "0.31.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "105b1842da6554f91526c14a2a2172897b7f745a805d62af4ce698706be79c12" +checksum = "43676fe2daf68754ecf1d72026e4e6c15483198b5d24e888b74d3f22f887a148" dependencies = [ "dlib", "log", @@ -1294,11 +1453,11 @@ checksum = "712e227841d057c1ee1cd2fb22fa7e5a5461ae8e48fa2ca79ec42cfc1931183f" [[package]] name = "windows-core" -version = "0.51.1" +version = "0.52.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f1f8cf84f35d2db49a46868f947758c7a1138116f7fac3bc844f43ade1292e64" +checksum = "33ab640c8d7e35bf8ba19b884ba838ceb4fba93a4e8c65a9059d08afcfc683d9" dependencies = [ - "windows-targets 0.48.5", + "windows-targets 0.52.6", ] [[package]] @@ -1340,7 +1499,16 @@ version = "0.52.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "282be5f36a8ce781fad8c8ae18fa3f9beff57ec1b52cb3de0789201425d9a33d" dependencies = [ - "windows-targets 0.52.5", + "windows-targets 0.52.6", +] + +[[package]] +name = "windows-sys" +version = "0.59.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1e38bc4d79ed67fd075bcc251a1c39b32a1776bbe92e5bef1f0bf1f8c531853b" +dependencies = [ + "windows-targets 0.52.6", ] [[package]] @@ -1375,18 +1543,18 @@ dependencies = [ [[package]] name = "windows-targets" -version = "0.52.5" +version = "0.52.6" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6f0713a46559409d202e70e28227288446bf7841d3211583a4b53e3f6d96e7eb" +checksum = "9b724f72796e036ab90c1021d4780d4d3d648aca59e491e6b98e725b84e99973" dependencies = [ - "windows_aarch64_gnullvm 0.52.5", - "windows_aarch64_msvc 0.52.5", - "windows_i686_gnu 0.52.5", + "windows_aarch64_gnullvm 0.52.6", + "windows_aarch64_msvc 0.52.6", + "windows_i686_gnu 0.52.6", "windows_i686_gnullvm", - "windows_i686_msvc 0.52.5", - "windows_x86_64_gnu 0.52.5", - "windows_x86_64_gnullvm 0.52.5", - "windows_x86_64_msvc 0.52.5", + "windows_i686_msvc 0.52.6", + "windows_x86_64_gnu 0.52.6", + "windows_x86_64_gnullvm 0.52.6", + "windows_x86_64_msvc 0.52.6", ] [[package]] @@ -1403,9 +1571,9 @@ checksum = "2b38e32f0abccf9987a4e3079dfb67dcd799fb61361e53e2882c3cbaf0d905d8" [[package]] name = "windows_aarch64_gnullvm" -version = "0.52.5" +version = "0.52.6" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7088eed71e8b8dda258ecc8bac5fb1153c5cffaf2578fc8ff5d61e23578d3263" +checksum = "32a4622180e7a0ec044bb555404c800bc9fd9ec262ec147edd5989ccd0c02cd3" [[package]] name = "windows_aarch64_msvc" @@ -1421,9 +1589,9 @@ checksum = "dc35310971f3b2dbbf3f0690a219f40e2d9afcf64f9ab7cc1be722937c26b4bc" [[package]] name = "windows_aarch64_msvc" -version = "0.52.5" +version = "0.52.6" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9985fd1504e250c615ca5f281c3f7a6da76213ebd5ccc9561496568a2752afb6" +checksum = "09ec2a7bb152e2252b53fa7803150007879548bc709c039df7627cabbd05d469" [[package]] name = "windows_i686_gnu" @@ -1439,15 +1607,15 @@ checksum = "a75915e7def60c94dcef72200b9a8e58e5091744960da64ec734a6c6e9b3743e" [[package]] name = "windows_i686_gnu" -version = "0.52.5" +version = "0.52.6" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "88ba073cf16d5372720ec942a8ccbf61626074c6d4dd2e745299726ce8b89670" +checksum = "8e9b5ad5ab802e97eb8e295ac6720e509ee4c243f69d781394014ebfe8bbfa0b" [[package]] name = "windows_i686_gnullvm" -version = "0.52.5" +version = "0.52.6" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "87f4261229030a858f36b459e748ae97545d6f1ec60e5e0d6a3d32e0dc232ee9" +checksum = "0eee52d38c090b3caa76c563b86c3a4bd71ef1a819287c19d586d7334ae8ed66" [[package]] name = "windows_i686_msvc" @@ -1463,9 +1631,9 @@ checksum = "8f55c233f70c4b27f66c523580f78f1004e8b5a8b659e05a4eb49d4166cca406" [[package]] name = "windows_i686_msvc" -version = "0.52.5" +version = "0.52.6" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "db3c2bf3d13d5b658be73463284eaf12830ac9a26a90c717b7f771dfe97487bf" +checksum = "240948bc05c5e7c6dabba28bf89d89ffce3e303022809e73deaefe4f6ec56c66" [[package]] name = "windows_x86_64_gnu" @@ -1481,9 +1649,9 @@ checksum = "53d40abd2583d23e4718fddf1ebec84dbff8381c07cae67ff7768bbf19c6718e" [[package]] name = "windows_x86_64_gnu" -version = "0.52.5" +version = "0.52.6" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4e4246f76bdeff09eb48875a0fd3e2af6aada79d409d33011886d3e1581517d9" +checksum = "147a5c80aabfbf0c7d901cb5895d1de30ef2907eb21fbbab29ca94c5b08b1a78" [[package]] name = "windows_x86_64_gnullvm" @@ -1499,9 +1667,9 @@ checksum = "0b7b52767868a23d5bab768e390dc5f5c55825b6d30b86c844ff2dc7414044cc" [[package]] name = "windows_x86_64_gnullvm" -version = "0.52.5" +version = "0.52.6" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "852298e482cd67c356ddd9570386e2862b5673c85bd5f88df9ab6802b334c596" +checksum = "24d5b23dc417412679681396f2b49f3de8c1473deb516bd34410872eff51ed0d" [[package]] name = "windows_x86_64_msvc" @@ -1517,9 +1685,9 @@ checksum = "ed94fce61571a4006852b7389a063ab983c02eb1bb37b47f8272ce92d06d9538" [[package]] name = "windows_x86_64_msvc" -version = "0.52.5" +version = "0.52.6" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "bec47e5bfd1bff0eeaf6d8b485cc1074891a197ab4225d504cb7a1ab88b02bf0" +checksum = "589f6da84c646204747d1270a2a5661ea66ed1cced2631d546fdfb155959f9ec" [[package]] name = "x11-clipboard" @@ -1550,9 +1718,9 @@ checksum = "ec107c4503ea0b4a98ef47356329af139c0a4f7750e621cf2973cd3385ebcb3d" [[package]] name = "xcursor" -version = "0.3.5" +version = "0.3.8" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6a0ccd7b4a5345edfcd0c3535718a4e9ff7798ffc536bb5b5a0e26ff84732911" +checksum = "0ef33da6b1660b4ddbfb3aef0ade110c8b8a781a3b6382fa5f2b5b040fd55f61" [[package]] name = "xdg" @@ -1571,3 +1739,24 @@ name = "yansi" version = "0.5.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "09041cd90cf85f7f8b2df60c646f853b7f535ce68f85244eb6731cf89fa498ec" + +[[package]] +name = "zerocopy" +version = "0.7.35" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1b9b4fd18abc82b8136838da5d50bae7bdea537c574d8dc1a34ed098d6c166f0" +dependencies = [ + "byteorder", + "zerocopy-derive", +] + +[[package]] +name = "zerocopy-derive" +version = "0.7.35" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fa4f8080344d4671fb4e831a13ad1e68092748387dfc4f55e356242fae12ce3e" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] diff --git a/Cargo.toml b/Cargo.toml index 97dfd81..0426ae9 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -17,18 +17,18 @@ bench = false anyhow = "1.0" bytes = "1" copypasta = "0.10.1" -crossterm = { version = "0.25", features = ["event-stream"] } +crossterm = { version = "0.28.1", features = ["event-stream"] } env_logger = { version = "0.11.5", default-features = false } home = "0.5.4" indoc = "1" log = { version = "0.4", features = ["std"] } open = "3" rand = "0.8.5" +ratatui = "0.28.0" thiserror = "1.0" tokio = { version = "1", features = ["io-std", "io-util", "macros", "net", "process", "rt", "rt-multi-thread", "fs"] } tokio-stream = "0.1" toml = "0.5" -tui = "0.19" xdg = "2" [dev-dependencies] diff --git a/src/client/ui.rs b/src/client/ui.rs index 15ea41c..4a3d2df 100644 --- a/src/client/ui.rs +++ b/src/client/ui.rs @@ -14,15 +14,8 @@ use crossterm::{ }, }; use log::{error, info, warn, Level, Metadata, Record}; -use std::collections::vec_deque::VecDeque; -use std::collections::{HashMap, HashSet}; -use std::io::stdout; -use std::sync::{Arc, Mutex}; -use tokio::sync::mpsc; -use tokio::sync::oneshot; -use tokio_stream::StreamExt; -use tui::{ - backend::{Backend, CrosstermBackend}, +use ratatui::{ + backend::CrosstermBackend, layout::{Constraint, Direction, Layout, Margin, Rect}, style::{Color, Modifier, Style}, widgets::{ @@ -30,6 +23,13 @@ use tui::{ }, Frame, Terminal, }; +use std::collections::vec_deque::VecDeque; +use std::collections::{HashMap, HashSet}; +use std::io::stdout; +use std::sync::{Arc, Mutex}; +use tokio::sync::mpsc; +use tokio::sync::oneshot; +use tokio_stream::StreamExt; pub enum UIEvent { Connected(u16), @@ -301,7 +301,7 @@ impl UI { Ok(code) } - fn render_connected(&mut self, frame: &mut Frame) { + fn render_connected(&mut self, frame: &mut Frame) { let constraints = if self.show_logs { vec![Constraint::Percentage(50), Constraint::Percentage(50)] } else { @@ -311,7 +311,7 @@ impl UI { let chunks = Layout::default() .direction(Direction::Vertical) .constraints(constraints) - .split(frame.size()); + .split(frame.area()); self.render_ports(frame, chunks[0]); if self.show_logs { @@ -322,7 +322,7 @@ impl UI { } } - fn render_ports(&mut self, frame: &mut Frame, size: Rect) { + fn render_ports(&mut self, frame: &mut Frame, size: Rect) { let enabled_port_style = Style::default(); let disabled_port_style = Style::default().fg(Color::DarkGray); let broken_port_style = @@ -358,17 +358,16 @@ impl UI { Constraint::Length(size.width), ]; - let port_list = Table::new(rows) + let port_list = Table::new(rows, &widths) .header(Row::new(vec!["fwd", "Port", "Description"])) .block(Block::default().title("Ports").borders(Borders::ALL)) .column_spacing(1) - .widths(&widths) .highlight_symbol(">> "); frame.render_stateful_widget(port_list, size, &mut self.selection); } - fn render_help(&mut self, frame: &mut Frame) { + fn render_help(&mut self, frame: &mut Frame) { let keybindings = vec![ Row::new(vec!["↑ / k", "Move cursor up"]), Row::new(vec!["↓ / j", "Move cursor down"]), @@ -387,10 +386,10 @@ impl UI { let help_popup_area = centered_rect( 65, keybindings.len() as u16 + border_lines, - frame.size(), + frame.area(), ); let inner_area = - help_popup_area.inner(&Margin { vertical: 1, horizontal: 1 }); + help_popup_area.inner(Margin { vertical: 1, horizontal: 1 }); let key_width = 7; let binding_width = inner_area.width.saturating_sub(key_width); @@ -398,8 +397,7 @@ impl UI { Constraint::Length(key_width), Constraint::Length(binding_width), ]; - let keybindings = Table::new(keybindings) - .widths(keybindings_widths) + let keybindings = Table::new(keybindings, keybindings_widths) .column_spacing(1) .block(Block::default().title("Keys").borders(Borders::ALL)); @@ -407,7 +405,7 @@ impl UI { frame.render_widget(keybindings, inner_area); } - fn render_logs(&mut self, frame: &mut Frame, size: Rect) { + fn render_logs(&mut self, frame: &mut Frame, size: Rect) { let items: Vec<_> = self.lines.iter().map(|l| ListItem::new(&l[..])).collect(); From 4fe255e7d26757a7c2c96c61d068c76a2a678e8b Mon Sep 17 00:00:00 2001 From: John Doty Date: Tue, 13 Aug 2024 10:52:20 -0700 Subject: [PATCH 04/25] Fix colors in the help box When the lines of the help box overlap with disabled or error'd ports you might notice that those lines are dark grey or red. That's surprising! The bug is that Style::default() means "don't change anything", just continue being whatever color the current cell is, which is deeply surprising. What we really want here is `Style::reset()`, which means "reset the colors to whatever the terminal would show by default." --- src/client/ui.rs | 9 +++++---- 1 file changed, 5 insertions(+), 4 deletions(-) diff --git a/src/client/ui.rs b/src/client/ui.rs index 4a3d2df..ae3fcd3 100644 --- a/src/client/ui.rs +++ b/src/client/ui.rs @@ -323,10 +323,10 @@ impl UI { } fn render_ports(&mut self, frame: &mut Frame, size: Rect) { - let enabled_port_style = Style::default(); - let disabled_port_style = Style::default().fg(Color::DarkGray); + let enabled_port_style = Style::reset(); + let disabled_port_style = Style::reset().fg(Color::DarkGray); let broken_port_style = - Style::default().fg(Color::Red).add_modifier(Modifier::DIM); + Style::reset().fg(Color::Red).add_modifier(Modifier::DIM); let mut rows = Vec::new(); let ports = self.get_ui_ports(); @@ -399,7 +399,8 @@ impl UI { ]; let keybindings = Table::new(keybindings, keybindings_widths) .column_spacing(1) - .block(Block::default().title("Keys").borders(Borders::ALL)); + .block(Block::default().title("Keys").borders(Borders::ALL)) + .style(Style::reset()); // keybindings frame.render_widget(keybindings, inner_area); From e44d4dea7aefca81ef08af93ffa38efb0139091b Mon Sep 17 00:00:00 2001 From: John Doty Date: Tue, 13 Aug 2024 10:56:29 -0700 Subject: [PATCH 05/25] Also update the fuzzing targets, I guess --- fuzz/Cargo.lock | 252 +++++++++++++++++++++++++++++++++++++----------- 1 file changed, 194 insertions(+), 58 deletions(-) diff --git a/fuzz/Cargo.lock b/fuzz/Cargo.lock index 463a394..855731d 100644 --- a/fuzz/Cargo.lock +++ b/fuzz/Cargo.lock @@ -17,6 +17,24 @@ version = "1.0.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "f26201604c87b1e01bd3d98f8d5d9a8fcbb815e8cedb41ffccbeb4bf593a35fe" +[[package]] +name = "ahash" +version = "0.8.11" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e89da841a80418a9b391ebaea17f5c112ffaaa96f621d2c285b5174da76b9011" +dependencies = [ + "cfg-if", + "once_cell", + "version_check", + "zerocopy", +] + +[[package]] +name = "allocator-api2" +version = "0.2.18" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5c6cb57a04249c6480766f7f7cef5467412af1490f8d1e243141daddada3264f" + [[package]] name = "android-tzdata" version = "0.1.1" @@ -136,6 +154,15 @@ version = "0.3.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "df8670b8c7b9dae1793364eafadf7239c40d669904660c5960d74cfd80b46a53" +[[package]] +name = "castaway" +version = "0.2.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0abae9be0aaf9ea96a3b1b8b1b55c602ca751eba1b1500220cea4ecbafe7c0d5" +dependencies = [ + "rustversion", +] + [[package]] name = "cc" version = "1.1.10" @@ -174,6 +201,20 @@ dependencies = [ "winapi", ] +[[package]] +name = "compact_str" +version = "0.8.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6050c3a16ddab2e412160b31f2c871015704239bca62f72f6e5f0be631d3f644" +dependencies = [ + "castaway", + "cfg-if", + "itoa", + "rustversion", + "ryu", + "static_assertions", +] + [[package]] name = "concurrent-queue" version = "2.5.0" @@ -220,16 +261,16 @@ checksum = "22ec99545bb0ed0ea7bb9b8e1e9122ea386ff8a48c0922e43f36d45ab09e0e80" [[package]] name = "crossterm" -version = "0.25.0" +version = "0.28.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e64e6c0fbe2c17357405f7c758c1ef960fce08bdfb2c03d88d2a18d7e09c4b67" +checksum = "829d955a0bb380ef178a640b91779e3987da38c9aea133b20614cfed8cdea9c6" dependencies = [ - "bitflags 1.3.2", + "bitflags 2.6.0", "crossterm_winapi", "futures-core", - "libc", - "mio 0.8.11", + "mio", "parking_lot", + "rustix 0.38.34", "signal-hook", "signal-hook-mio", "winapi", @@ -276,6 +317,12 @@ version = "1.2.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "75b325c5dbd37f80359721ad39aca5a29fb04c89279657cffdda8736d0c0b9d2" +[[package]] +name = "either" +version = "1.13.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "60b1af1c220855b6ceac025d3f6ecdd2b7c4894bfe9cd9bda4fbb4bc7c0d4cf0" + [[package]] name = "env_filter" version = "0.1.2" @@ -323,7 +370,7 @@ checksum = "dfc6580bb841c5a68e9ef15c77ccc837b40a7504914d52e47b8b0e9bbda25a1d" [[package]] name = "fwd" -version = "0.9.1" +version = "0.9.2" dependencies = [ "anyhow", "bytes", @@ -332,16 +379,16 @@ dependencies = [ "env_logger", "home", "indoc", + "libc", "log", "open", "procfs", "rand", + "ratatui", "thiserror", "tokio", "tokio-stream", "toml", - "tui", - "users", "xdg", ] @@ -382,6 +429,22 @@ version = "0.29.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "40ecd4077b5ae9fd2e9e169b102c6c330d0605168eb0e8bf79952b256dbefffd" +[[package]] +name = "hashbrown" +version = "0.14.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e5274423e17b7c9fc20b6e7e208532f9b19825d82dfd615708b70edd83df41f1" +dependencies = [ + "ahash", + "allocator-api2", +] + +[[package]] +name = "heck" +version = "0.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2304e00983f87ffb38b55b444b5e3b60a884b5d30c0fca7d82fe33449bbe55ea" + [[package]] name = "hermit-abi" version = "0.3.9" @@ -438,6 +501,16 @@ version = "1.0.9" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "bfa799dd5ed20a7e349f3b4639aa80d74549c81716d9ec4f994c9b5815598306" +[[package]] +name = "instability" +version = "0.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b23a0c8dfe501baac4adf6ebbfa6eddf8f0c07f56b058cc1288017e32397846c" +dependencies = [ + "quote", + "syn", +] + [[package]] name = "io-lifetimes" version = "1.0.11" @@ -449,6 +522,15 @@ dependencies = [ "windows-sys 0.48.0", ] +[[package]] +name = "itertools" +version = "0.13.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "413ee7dfc52ee1a4949ceeb7dbc8a33f2d6c088194d9f922fb8318faf1f01186" +dependencies = [ + "either", +] + [[package]] name = "itoa" version = "1.0.11" @@ -466,9 +548,9 @@ dependencies = [ [[package]] name = "js-sys" -version = "0.3.69" +version = "0.3.70" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "29c15563dc2726973df627357ce0c9ddddbea194836909d655df6a75d2cf296d" +checksum = "1868808506b929d7b0cfa8f75951347aa71bb21144b7791bae35d9bccfcfe37a" dependencies = [ "wasm-bindgen", ] @@ -540,6 +622,15 @@ version = "0.4.22" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "a7a70ba024b9dc04c27ea2f0c0548feb474ec5c54bba33a7f72f873a39d07b24" +[[package]] +name = "lru" +version = "0.12.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "37ee39891760e7d94734f6f63fedc29a2e4a152f836120753a72503f09fcf904" +dependencies = [ + "hashbrown", +] + [[package]] name = "malloc_buf" version = "0.0.6" @@ -573,18 +664,6 @@ dependencies = [ "adler", ] -[[package]] -name = "mio" -version = "0.8.11" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a4a650543ca06a924e8b371db273b2756685faae30f8487da1b56505a8f78b0c" -dependencies = [ - "libc", - "log", - "wasi", - "windows-sys 0.48.0", -] - [[package]] name = "mio" version = "1.0.2" @@ -593,6 +672,7 @@ checksum = "80e04d1dcff3aae0704555fe5fee3bcfaf3d1fdf8a7e521d5b9d2b42acb52cec" dependencies = [ "hermit-abi 0.3.9", "libc", + "log", "wasi", "windows-sys 0.52.0", ] @@ -683,6 +763,12 @@ dependencies = [ "windows-targets 0.52.6", ] +[[package]] +name = "paste" +version = "1.0.15" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "57c0d7b74b563b49d38dae00a0c37d4d6de9b432382b2892f0574ddcae73fd0a" + [[package]] name = "pathdiff" version = "0.2.1" @@ -797,6 +883,27 @@ dependencies = [ "getrandom", ] +[[package]] +name = "ratatui" +version = "0.28.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5ba6a365afbe5615999275bea2446b970b10a41102500e27ce7678d50d978303" +dependencies = [ + "bitflags 2.6.0", + "cassowary", + "compact_str", + "crossterm", + "instability", + "itertools", + "lru", + "paste", + "strum", + "strum_macros", + "unicode-segmentation", + "unicode-truncate", + "unicode-width", +] + [[package]] name = "redox_syscall" version = "0.5.3" @@ -839,6 +946,12 @@ dependencies = [ "windows-sys 0.52.0", ] +[[package]] +name = "rustversion" +version = "1.0.17" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "955d28af4278de8121b7ebeb796b6a45735dc01436d898801014aced2773a3d6" + [[package]] name = "ryu" version = "1.0.18" @@ -859,18 +972,18 @@ checksum = "94143f37725109f92c262ed2cf5e59bce7498c01bcc1502d7b9afe439a4e9f49" [[package]] name = "serde" -version = "1.0.206" +version = "1.0.207" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5b3e4cd94123dd520a128bcd11e34d9e9e423e7e3e50425cb1b4b1e3549d0284" +checksum = "5665e14a49a4ea1b91029ba7d3bca9f299e1f7cfa194388ccc20f14743e784f2" dependencies = [ "serde_derive", ] [[package]] name = "serde_derive" -version = "1.0.206" +version = "1.0.207" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "fabfb6138d2383ea8208cf98ccf69cdfb1aff4088460681d84189aa259762f97" +checksum = "6aea2634c86b0e8ef2cfdc0c340baede54ec27b1e46febd7f80dffb2aa44a00e" dependencies = [ "proc-macro2", "quote", @@ -906,7 +1019,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "34db1a06d485c9142248b7a054f034b349b212551f3dfd19c94d45a754a217cd" dependencies = [ "libc", - "mio 0.8.11", + "mio", "signal-hook", ] @@ -980,6 +1093,34 @@ dependencies = [ "windows-sys 0.52.0", ] +[[package]] +name = "static_assertions" +version = "1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a2eb9349b6444b326872e140eb1cf5e7c522154d69e7a0ffb0fb81c06b37543f" + +[[package]] +name = "strum" +version = "0.26.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8fec0f0aef304996cf250b31b5a10dee7980c85da9d759361292b8bca5a18f06" +dependencies = [ + "strum_macros", +] + +[[package]] +name = "strum_macros" +version = "0.26.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4c6bee85a5a24955dc440386795aa378cd9cf82acd5f764469152d2270e581be" +dependencies = [ + "heck", + "proc-macro2", + "quote", + "rustversion", + "syn", +] + [[package]] name = "syn" version = "2.0.74" @@ -1020,7 +1161,7 @@ dependencies = [ "backtrace", "bytes", "libc", - "mio 1.0.2", + "mio", "pin-project-lite", "signal-hook-registry", "socket2", @@ -1075,19 +1216,6 @@ version = "0.1.32" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "c06d3da6113f116aaee68e4d601191614c9053067f9ab7f6edbcb161237daa54" -[[package]] -name = "tui" -version = "0.19.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ccdd26cbd674007e649a272da4475fb666d3aa0ad0531da7136db6fab0e5bad1" -dependencies = [ - "bitflags 1.3.2", - "cassowary", - "crossterm", - "unicode-segmentation", - "unicode-width", -] - [[package]] name = "unicode-ident" version = "1.0.12" @@ -1100,6 +1228,17 @@ version = "1.11.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "d4c87d22b6e3f4a18d4d40ef354e97c90fcb14dd91d7dc0aa9d8a1172ebf7202" +[[package]] +name = "unicode-truncate" +version = "1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b3644627a5af5fa321c95b9b235a72fd24cd29c648c2c379431e6628655627bf" +dependencies = [ + "itertools", + "unicode-segmentation", + "unicode-width", +] + [[package]] name = "unicode-width" version = "0.1.13" @@ -1107,14 +1246,10 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "0336d538f7abc86d282a4189614dfaa90810dfc2c6f6427eaf88e16311dd225d" [[package]] -name = "users" -version = "0.11.0" +name = "version_check" +version = "0.9.5" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "24cc0f6d6f267b73e5a2cadf007ba8f9bc39c6a6f9666f8cf25ea809a153b032" -dependencies = [ - "libc", - "log", -] +checksum = "0b928f33d975fc6ad9f86c8f283853ad26bdd5b10b7f1542aa2fa15e2289105a" [[package]] name = "wasi" @@ -1124,19 +1259,20 @@ checksum = "9c8d87e72b64a3b4db28d11ce29237c246188f4f51057d65a7eab63b7987e423" [[package]] name = "wasm-bindgen" -version = "0.2.92" +version = "0.2.93" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4be2531df63900aeb2bca0daaaddec08491ee64ceecbee5076636a3b026795a8" +checksum = "a82edfc16a6c469f5f44dc7b571814045d60404b55a0ee849f9bcfa2e63dd9b5" dependencies = [ "cfg-if", + "once_cell", "wasm-bindgen-macro", ] [[package]] name = "wasm-bindgen-backend" -version = "0.2.92" +version = "0.2.93" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "614d787b966d3989fa7bb98a654e369c762374fd3213d212cfc0251257e747da" +checksum = "9de396da306523044d3302746f1208fa71d7532227f15e347e2d93e4145dd77b" dependencies = [ "bumpalo", "log", @@ -1149,9 +1285,9 @@ dependencies = [ [[package]] name = "wasm-bindgen-macro" -version = "0.2.92" +version = "0.2.93" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a1f8823de937b71b9460c0c34e25f3da88250760bec0ebac694b49997550d726" +checksum = "585c4c91a46b072c92e908d99cb1dcdf95c5218eeb6f3bf1efa991ee7a68cccf" dependencies = [ "quote", "wasm-bindgen-macro-support", @@ -1159,9 +1295,9 @@ dependencies = [ [[package]] name = "wasm-bindgen-macro-support" -version = "0.2.92" +version = "0.2.93" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e94f17b526d0a461a191c78ea52bbce64071ed5c04c9ffe424dcb38f74171bb7" +checksum = "afc340c74d9005395cf9dd098506f7f44e38f2b4a21c6aaacf9a105ea5e1e836" dependencies = [ "proc-macro2", "quote", @@ -1172,9 +1308,9 @@ dependencies = [ [[package]] name = "wasm-bindgen-shared" -version = "0.2.92" +version = "0.2.93" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "af190c94f2773fdb3729c55b007a722abb5384da03bc0986df4c289bf5567e96" +checksum = "c62a0a307cb4a311d3a07867860911ca130c3494e8c2719593806c08bc5d0484" [[package]] name = "wayland-backend" From 663ce420166a96493ee4abc2a5c06e2fab4498ee Mon Sep 17 00:00:00 2001 From: John Doty Date: Tue, 13 Aug 2024 10:59:47 -0700 Subject: [PATCH 06/25] tempdir -> tempfile According to the documentation of the tempdir crate --- Cargo.lock | 79 +++++++++++---------------------------------- Cargo.toml | 2 +- src/reverse/unix.rs | 6 ++-- 3 files changed, 22 insertions(+), 65 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index 111ae68..4e43104 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -340,6 +340,12 @@ dependencies = [ "windows-sys 0.52.0", ] +[[package]] +name = "fastrand" +version = "2.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9fc0510504f03c51ada170672ac806f1f105a88aa97a5281117e1ddc3368e51a" + [[package]] name = "flate2" version = "1.0.31" @@ -350,12 +356,6 @@ dependencies = [ "miniz_oxide", ] -[[package]] -name = "fuchsia-cprng" -version = "0.1.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a06f77d526c1a601b7c4cdd98f54b5eaabffc14d5f2f0296febdc7f357c6d3ba" - [[package]] name = "futures-core" version = "0.3.30" @@ -379,9 +379,9 @@ dependencies = [ "open", "pretty_assertions", "procfs", - "rand 0.8.5", + "rand", "ratatui", - "tempdir", + "tempfile", "thiserror", "tokio", "tokio-stream", @@ -830,19 +830,6 @@ dependencies = [ "proc-macro2", ] -[[package]] -name = "rand" -version = "0.4.6" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "552840b97013b1a26992c11eac34bdd778e464601a4c2054b5f0bff7c6761293" -dependencies = [ - "fuchsia-cprng", - "libc", - "rand_core 0.3.1", - "rdrand", - "winapi", -] - [[package]] name = "rand" version = "0.8.5" @@ -851,7 +838,7 @@ checksum = "34af8d1a0e25924bc5b7c43c079c942339d8f0a8b57c39049bef581b46327404" dependencies = [ "libc", "rand_chacha", - "rand_core 0.6.4", + "rand_core", ] [[package]] @@ -861,24 +848,9 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "e6c10a63a0fa32252be49d21e7709d4d4baf8d231c2dbce1eaa8141b9b127d88" dependencies = [ "ppv-lite86", - "rand_core 0.6.4", + "rand_core", ] -[[package]] -name = "rand_core" -version = "0.3.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7a6fdeb83b075e8266dcc8762c22776f6877a63111121f5f8c7411e5be7eed4b" -dependencies = [ - "rand_core 0.4.2", -] - -[[package]] -name = "rand_core" -version = "0.4.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9c33a3c44ca05fa6f1807d8e6743f3824e8509beca625669633be0acbdf509dc" - [[package]] name = "rand_core" version = "0.6.4" @@ -909,15 +881,6 @@ dependencies = [ "unicode-width", ] -[[package]] -name = "rdrand" -version = "0.4.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "678054eb77286b51581ba43620cc911abf02758c91f93f479767aed0f90458b2" -dependencies = [ - "rand_core 0.3.1", -] - [[package]] name = "redox_syscall" version = "0.5.3" @@ -927,15 +890,6 @@ dependencies = [ "bitflags 2.6.0", ] -[[package]] -name = "remove_dir_all" -version = "0.5.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3acd125665422973a33ac9d3dd2df85edad0f4ae9b00dafb1a05e43a9f5ef8e7" -dependencies = [ - "winapi", -] - [[package]] name = "rustc-demangle" version = "0.1.24" @@ -1144,13 +1098,16 @@ dependencies = [ ] [[package]] -name = "tempdir" -version = "0.3.7" +name = "tempfile" +version = "3.12.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "15f2b5fb00ccdf689e0149d1b1b3c03fead81c2b37735d812fa8bddbbf41b6d8" +checksum = "04cbcdd0c794ebb0d4cf35e88edd2f7d2c4c3e9a5a6dab322839b321c6a87a64" dependencies = [ - "rand 0.4.6", - "remove_dir_all", + "cfg-if", + "fastrand", + "once_cell", + "rustix 0.38.34", + "windows-sys 0.59.0", ] [[package]] diff --git a/Cargo.toml b/Cargo.toml index 0426ae9..2111a12 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -34,7 +34,7 @@ xdg = "2" [dev-dependencies] assert_matches = "1" pretty_assertions = "1" -tempdir = "0.3" +tempfile = "3" [target.'cfg(target_os="linux")'.dependencies] procfs = "0.14.1" diff --git a/src/reverse/unix.rs b/src/reverse/unix.rs index b85ed54..1d046b2 100644 --- a/src/reverse/unix.rs +++ b/src/reverse/unix.rs @@ -117,7 +117,7 @@ async fn handle_connection( mod tests { use super::*; use crate::message::MessageWriter; - use tempdir::TempDir; + use tempfile::TempDir; #[test] fn socket_path_repeats() { @@ -131,8 +131,8 @@ mod tests { async fn url_to_message() { let (sender, mut receiver) = mpsc::channel(64); - let tmp_dir = - TempDir::new("url_to_message").expect("Error getting tmpdir"); + let tmp_dir = TempDir::with_prefix("url_to_message") + .expect("Error getting tmpdir"); let path = tmp_dir.path().join("socket"); let path_override = path.clone(); From 38fbfbd918b4fb10f0db867c71c00e7e697fcc5d Mon Sep 17 00:00:00 2001 From: John Doty Date: Wed, 14 Aug 2024 10:51:19 -0700 Subject: [PATCH 07/25] Move config file to ~/.config/fwd/config.toml Presumably this also works for MacOS and windows. While doing this, move away from xdg and home and use this directories-next crate instead. Reverse connections still seem to work. --- Cargo.lock | 60 +++++++++++++++++++++++++++++++------------- Cargo.toml | 4 +-- src/client/config.rs | 12 +++++---- src/reverse/unix.rs | 10 ++++---- 4 files changed, 57 insertions(+), 29 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index 4e43104..3499c83 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -290,6 +290,27 @@ version = "0.1.13" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "56254986775e3233ffa9c4d7d3faaf6d36a2c09d30b20687e9f88bc8bafc16c8" +[[package]] +name = "directories-next" +version = "2.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "339ee130d97a610ea5a5872d2bbb130fdf68884ff09d3028b81bec8a1ac23bbc" +dependencies = [ + "cfg-if", + "dirs-sys-next", +] + +[[package]] +name = "dirs-sys-next" +version = "0.1.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4ebda144c4fe02d1f7ea1a7d9641b6fc6b580adcfa024ae48797ecdeb6825b4d" +dependencies = [ + "libc", + "redox_users", + "winapi", +] + [[package]] name = "dlib" version = "0.5.2" @@ -371,8 +392,8 @@ dependencies = [ "bytes", "copypasta", "crossterm", + "directories-next", "env_logger", - "home", "indoc", "libc", "log", @@ -386,7 +407,6 @@ dependencies = [ "tokio", "tokio-stream", "toml", - "xdg", ] [[package]] @@ -450,15 +470,6 @@ version = "0.4.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "7f24254aa9a54b5c858eaee2f5bccdb46aaf0e486a595ed5fd8f86ba55232a70" -[[package]] -name = "home" -version = "0.5.9" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e3d1354bf6b7235cb4a0576c2619fd4ed18183f689b12b006a0ee7329eeff9a5" -dependencies = [ - "windows-sys 0.52.0", -] - [[package]] name = "iana-time-zone" version = "0.1.60" @@ -561,6 +572,16 @@ dependencies = [ "windows-targets 0.52.6", ] +[[package]] +name = "libredox" +version = "0.1.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c0ff37bd590ca25063e35af745c343cb7a0271906fb7b37e4813e8f79f00268d" +dependencies = [ + "bitflags 2.6.0", + "libc", +] + [[package]] name = "linux-raw-sys" version = "0.1.4" @@ -890,6 +911,17 @@ dependencies = [ "bitflags 2.6.0", ] +[[package]] +name = "redox_users" +version = "0.4.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bd283d9651eeda4b2a83a43c1c91b266c40fd76ecd39a50a8c630ae69dc72891" +dependencies = [ + "getrandom", + "libredox", + "thiserror", +] + [[package]] name = "rustc-demangle" version = "0.1.24" @@ -1679,12 +1711,6 @@ version = "0.3.8" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "0ef33da6b1660b4ddbfb3aef0ade110c8b8a781a3b6382fa5f2b5b040fd55f61" -[[package]] -name = "xdg" -version = "2.5.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "213b7324336b53d2414b2db8537e56544d981803139155afa84f76eeebb7a546" - [[package]] name = "xkeysym" version = "0.2.1" diff --git a/Cargo.toml b/Cargo.toml index 2111a12..c4c05c8 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -18,8 +18,8 @@ anyhow = "1.0" bytes = "1" copypasta = "0.10.1" crossterm = { version = "0.28.1", features = ["event-stream"] } +directories-next = "2" env_logger = { version = "0.11.5", default-features = false } -home = "0.5.4" indoc = "1" log = { version = "0.4", features = ["std"] } open = "3" @@ -29,7 +29,7 @@ thiserror = "1.0" tokio = { version = "1", features = ["io-std", "io-util", "macros", "net", "process", "rt", "rt-multi-thread", "fs"] } tokio-stream = "0.1" toml = "0.5" -xdg = "2" + [dev-dependencies] assert_matches = "1" diff --git a/src/client/config.rs b/src/client/config.rs index f08220c..77d44e8 100644 --- a/src/client/config.rs +++ b/src/client/config.rs @@ -62,13 +62,15 @@ impl Config { pub fn load_config() -> Result { use std::io::ErrorKind; - let mut home = match home::home_dir() { - Some(h) => h, - None => return Ok(default()), + let Some(directories) = directories_next::ProjectDirs::from("", "", "fwd") + else { + return Ok(default()); }; - home.push(".fwd"); - let contents = match std::fs::read_to_string(home) { + let mut config_path = directories.config_dir().to_path_buf(); + config_path.push("config.toml"); + + let contents = match std::fs::read_to_string(config_path) { Ok(contents) => contents, Err(e) => match e.kind() { ErrorKind::NotFound => return Ok(default()), diff --git a/src/reverse/unix.rs b/src/reverse/unix.rs index 1d046b2..a968edd 100644 --- a/src/reverse/unix.rs +++ b/src/reverse/unix.rs @@ -49,11 +49,11 @@ pub fn socket_path() -> Result { } fn socket_directory() -> Result { - let base_directories = xdg::BaseDirectories::new() - .context("Error creating BaseDirectories")?; - match base_directories.place_runtime_file("fwd") { - Ok(path) => Ok(path), - Err(_) => { + match directories_next::ProjectDirs::from("", "", "fwd") + .and_then(|p| p.runtime_dir().map(|p| p.to_path_buf())) + { + Some(p) => Ok(p), + None => { let mut path = std::env::temp_dir(); let uid = unsafe { libc::getuid() }; path.push(format!("fwd{}", uid)); From afa13bf920dd17867e5fb6c1c050094876d92c0b Mon Sep 17 00:00:00 2001 From: John Doty Date: Wed, 14 Aug 2024 10:52:19 -0700 Subject: [PATCH 08/25] This description is out of date Given the introduction of anonymous ports --- src/main.rs | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/src/main.rs b/src/main.rs index 7871d50..a6201e2 100644 --- a/src/main.rs +++ b/src/main.rs @@ -19,10 +19,10 @@ to send the the contents of `file`. Options: --version Print the version of fwd and exit --sudo, -s Run the server side of fwd with `sudo`. This allows the - client to forward ports that are open by processes being - run under other accounts (e.g., docker containers being - run as root), but requires sudo access on the server and - *might* end up forwarding ports that you do not want + client to identify the ports that are open by processes + being run under other accounts (e.g., docker containers + being run as root), but requires sudo access on the server + and *might* end up forwarding ports that you do not want forwarded (e.g., port 22 for sshd, or port 53 for systemd.) --log-filter FILTER Set remote server's log level. Default is `warn`. Supports From cfde429786916509997c95c5d49698b869cf1ae4 Mon Sep 17 00:00:00 2001 From: John Doty Date: Wed, 14 Aug 2024 11:22:50 -0700 Subject: [PATCH 09/25] A man page, somewhat --- doc/fwd.man.md | 150 +++++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 150 insertions(+) create mode 100644 doc/fwd.man.md diff --git a/doc/fwd.man.md b/doc/fwd.man.md new file mode 100644 index 0000000..992254c --- /dev/null +++ b/doc/fwd.man.md @@ -0,0 +1,150 @@ +% fwd(1) +% John Doty +% August 2024 + +# NAME + +fwd - Automatically forward connections to remote machines + +# SYNOPSIS + +**fwd** [OPTIONS] SERVER + +**fwd** [OPTIONS] browse URL + +**fwd** [OPTIONS] clip FILE + +**fwd-browse** URL + +# DESCRIPTION + +**fwd** enumerates the listening ports the remote server and automatically listens for connections on the same ports on the local machine. +When **fwd** receives a connection on the local machine, it automatically forwards that connection to the remote machine. + +**-s**, **-\-sudo** +: Run the server side of fwd with `sudo`. +: This allows the client to forward ports that are open by processes being run under other accounts (e.g., docker containers being run as root), but requires sudo access on the server and *might* end up forwarding ports that you do not want forwarded (e.g., port 22 for sshd, or port 53 for systemd.) + +**-\-log-filter** **FILTER** +: Set remote server's log level. Default is `warn`. +: Supports all of Rust's env_logger filter syntax, e.g. `--log-filter=fwd::trace`. + +**-\-version** +: Print the version of fwd and exit. + +# INTERACTIVE COMMANDS + +Once **fwd** is connected, it displays an interactive list of the ports available on the remote server. + +- Ports that **fwd** is listening on are displayed in the default terminal color. +- Ports that **fwd** is aware of but which are disabled are displayed in dark gray. +- Ports that **fwd** has tried to listen on but which have failed are displayed in red. + Details on the error may be found in the log window. + Disabling and re-enabling the port will cause **fwd** to try again. + +The following commands are available while **fwd** is connected: + +**Esc, q, Ctrl-C** +: Exit **fwd**. + +**?, h** +: Display the help window. + +**Up, k** +: Select the previous port in the list. + +**Down, j** +: Select the next port in the list. + +**Enter** +: Attempt to browse to localhost on the specified port with the default browser. + +**e** +: Enable or disable the selected port. + +**l** +: Show or hide the log window. + +# IDENTIFYING PORTS + +**fwd** enumerates all of the ports that the remote server is listening on, and attempts to identify the process that is listening on each port. +It can identify ports in the following ways: + +*With docker* +: **fwd** will attempt to find and connect to a docker engine on the remote machine. +: If successful, it will list all of the forwarded ports, and identify each port as belonging to that docker container. + +*With procfs* +: On Linux, the listening ports are found by reading procfs and mapping them back to process command lines. +: **fwd** can only identify processes that the user it is connected as has permissions to read on the remote machine. + +Earlier methods take precedence over later methods. + +If **fwd** cannot identify the process that is listening on a given port, then the port is *anonymous*. +Anonymous ports are not enabled by default, but can be enabled manually, either with the UI or by configuration. + +# OPENING BROWSERS +**fwd** can be used to open URLs in the default browser on the local machine. +Run **fwd browse URL** on the remote server to open the `URL` in the default browser on the local machine. + +This only works if **fwd** is connected, and if the user running **fwd browse** is the same as the user that connected the **fwd** session. + +The **fwd-browse** program acts as a wrapper around **fwd browse**, to be used with configurations that can't handle a browser being a program with an argument. + +# CLIPBOARD +**fwd** can be used from the remote machine to place text on the clipboard of the local machine. +Run **fwd clip FILE** to copy the contents of the named file to the clipboard. +If **FILE** is **-**, this reads text from stdin instead. + +# CONFIGURATION +**fwd** can be configured with a configuration file. + +- On Windows, the config file will be in your roaming AppData folder. + (e.g., *c:\\Users\\Winifred\\AppData\\Roaming\\fwd\\config\\config.toml*) +- On MacOS, the config file will be in *$HOME/Library/Application Support/fwd/config.toml*. + (e.g., /Users/Margarie/Library/Application Support/fwd/config.toml) +- On XDG-ish systems (like Linux), the config file is in *~/.config/fwd/config.toml*. + (e.g., */home/lynette/.config/fwd/config.toml*) + +The following is an example of a *config.toml* file: + +``` +auto=true # should `fwd` should enable identified ports + +[servers.foo] # Server-specific settings for foo +auto=true +ports=[1080, 1082] # ports that are always present + +[servers.bar.ports] # `ports` can also be a table with port numbers as keys +1080=true # the values can be booleans (for enabled) + +[servers.bar.ports.1082] # port values can also be tables +enabled=true +description="A humble python" +``` + +Ports that are specified in the configuration file will always be present in the list of ports for a given server, even if no process is listening on that port. + +# TROUBLESHOOTING + +Connections are made via the **ssh** command. +Your **ssh** must: + +- Be on your path, so that **fwd** can find it to invoke it +- Be able to authenticate you to the remote server. + (Interactive authentication is fine.) +- Understand the **-D** command line option, to operate as a SOCKS5 server +- Be able to start the **fwd** command on the remote server + +A typical ssh invocation from **fwd** looks like: + +```bash +ssh -T -D XXXX me@server FWD_LOG=warning FWD_SEND_ANONYMOUS=1 fwd --server +``` + +**fwd** only enumerates ports that are listening on loopback addresses (e.g., 127.0.0.1) or on all addresses (e.g., 0.0.0.0). +If it cannot find a particular port, check to make sure that the process listening on that port is accessible via localhost. + +# SEE ALSO + +ssh(1) From 74e2da2f2941366e76a24bb0a7d63e29d7cb49ba Mon Sep 17 00:00:00 2001 From: John Doty Date: Wed, 14 Aug 2024 11:24:23 -0700 Subject: [PATCH 10/25] Man page edits --- doc/fwd.man.md | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/doc/fwd.man.md b/doc/fwd.man.md index 992254c..9f8f6da 100644 --- a/doc/fwd.man.md +++ b/doc/fwd.man.md @@ -70,15 +70,15 @@ The following commands are available while **fwd** is connected: **fwd** enumerates all of the ports that the remote server is listening on, and attempts to identify the process that is listening on each port. It can identify ports in the following ways: -*With docker* +*docker* : **fwd** will attempt to find and connect to a docker engine on the remote machine. : If successful, it will list all of the forwarded ports, and identify each port as belonging to that docker container. -*With procfs* +*procfs* : On Linux, the listening ports are found by reading procfs and mapping them back to process command lines. : **fwd** can only identify processes that the user it is connected as has permissions to read on the remote machine. -Earlier methods take precedence over later methods. +(Earlier methods take precedence over later methods.) If **fwd** cannot identify the process that is listening on a given port, then the port is *anonymous*. Anonymous ports are not enabled by default, but can be enabled manually, either with the UI or by configuration. From 7a40326719d7dbdb76bbcf2b58132dee2bebcf62 Mon Sep 17 00:00:00 2001 From: John Doty Date: Thu, 15 Aug 2024 10:14:43 -0700 Subject: [PATCH 11/25] Re-work config code Add raw description as a possible config for a port, and update the documentation appropriately. --- doc/fwd.man.md | 13 +- src/client/config.rs | 408 ++++++++++++++++++++++++++++++++++++------- 2 files changed, 348 insertions(+), 73 deletions(-) diff --git a/doc/fwd.man.md b/doc/fwd.man.md index 9f8f6da..b7e5df9 100644 --- a/doc/fwd.man.md +++ b/doc/fwd.man.md @@ -109,14 +109,15 @@ If **FILE** is **-**, this reads text from stdin instead. The following is an example of a *config.toml* file: ``` -auto=true # should `fwd` should enable identified ports +auto=true # should `fwd` should enable identified ports (default true) -[servers.foo] # Server-specific settings for foo -auto=true -ports=[1080, 1082] # ports that are always present +[servers.foo] # Server-specific settings for foo +auto=true # defaults to the global setting +ports=[1080, 1082] # ports that are always present -[servers.bar.ports] # `ports` can also be a table with port numbers as keys -1080=true # the values can be booleans (for enabled) +[servers.bar.ports] # `ports` can also be a table with port numbers as keys +1080=true # the values can be booleans (for enabled)... +1081="My program" # or strings (for descriptions). [servers.bar.ports.1082] # port values can also be tables enabled=true diff --git a/src/client/config.rs b/src/client/config.rs index 77d44e8..e3a3cc0 100644 --- a/src/client/config.rs +++ b/src/client/config.rs @@ -1,15 +1,17 @@ use anyhow::{bail, Result}; use std::collections::hash_map; use std::collections::HashMap; -use toml::Value; +use toml::value::{Table, Value}; #[derive(Debug, Clone)] +#[cfg_attr(test, derive(PartialEq, Eq))] pub struct PortConfig { pub enabled: bool, pub description: Option, } #[derive(Debug, Clone)] +#[cfg_attr(test, derive(PartialEq, Eq))] pub struct ServerConfig { auto: bool, ports: HashMap, @@ -45,6 +47,7 @@ impl ServerConfig { } #[derive(Debug)] +#[cfg_attr(test, derive(PartialEq, Eq))] pub struct Config { auto: bool, servers: HashMap, @@ -85,85 +88,101 @@ fn default() -> Config { Config { auto: true, servers: HashMap::new() } } -fn parse_config(value: &Value) -> Result { - match value { - Value::Table(table) => Ok({ - let auto = match table.get("auto") { - None => true, - Some(Value::Boolean(v)) => *v, - Some(v) => bail!("expected a true or false, got {:?}", v), - }; - Config { auto, servers: get_servers(table, auto)? } - }), - _ => bail!("top level must be a table"), +fn get_bool(table: &Table, key: &str, default: bool) -> Result { + match table.get(key) { + None => Ok(default), + Some(Value::Boolean(v)) => Ok(*v), + Some(v) => bail!("expected a true or false, got {v:?}"), } } -fn get_servers( - table: &toml::value::Table, +fn parse_config(value: &Value) -> Result { + let Value::Table(table) = value else { + bail!("top level must be a table") + }; + + let auto = get_bool(table, "auto", true)?; + let servers = match table.get("servers") { + None => &Table::new(), + Some(Value::Table(t)) => t, + Some(v) => bail!("Expected a table in the servers key, got {v:?}"), + }; + + Ok(Config { + auto, + servers: parse_servers(servers, auto)?, + }) +} + +fn parse_servers( + table: &Table, auto: bool, ) -> Result> { - match table.get("servers") { - None => Ok(HashMap::new()), - Some(Value::Table(table)) => Ok({ - let mut servers = HashMap::new(); - for (k, v) in table { - servers.insert(k.clone(), get_server(v, auto)?); - } - servers - }), - v => bail!("expected a table in the servers key, got {:?}", v), + let mut servers = HashMap::new(); + for (k, v) in table { + let Value::Table(table) = v else { + bail!("expected a table for server {k}, got {v:?}"); + }; + + servers.insert(k.clone(), parse_server(table, auto)?); } + Ok(servers) } -fn get_server(value: &Value, auto: bool) -> Result { +fn parse_server(table: &Table, auto: bool) -> Result { + let auto = get_bool(table, "auto", auto)?; + let ports = match table.get("ports") { + None => HashMap::new(), + Some(v) => parse_ports(v)?, + }; + + Ok(ServerConfig { auto, ports }) +} + +fn parse_ports(value: &Value) -> Result> { match value { - Value::Table(table) => Ok(ServerConfig { - auto: match table.get("auto") { - None => auto, // Default to global default - Some(Value::Boolean(v)) => *v, - Some(v) => bail!("expected true or false, got {:?}", v), - }, - ports: get_ports(table)?, - }), - value => bail!("expected a table, got {:?}", value), - } -} - -fn get_ports(table: &toml::value::Table) -> Result> { - match table.get("ports") { - None => Ok(HashMap::new()), - Some(Value::Table(table)) => Ok({ - let mut ports = HashMap::new(); - for (k,v) in table { - let port:u16 = k.parse()?; - let config = match v { - Value::Boolean(enabled) => PortConfig{enabled:*enabled, description:None}, - Value::Table(table) => PortConfig{ - enabled: match table.get("enabled") { - Some(Value::Boolean(enabled)) => *enabled, - _ => bail!("not implemented"), - }, - description: match table.get("description") { - Some(Value::String(desc)) => Some(desc.clone()), - Some(v) => bail!("expect a string description, got {:?}", v), - None => None, - }, - }, - _ => bail!("expected either a boolean (enabled) or a table for a port config, got {:?}", v), - }; - ports.insert(port, config); - } - ports - }), - Some(Value::Array(array)) => Ok({ + Value::Array(array) => { let mut ports = HashMap::new(); for v in array { - ports.insert(get_port_number(v)?, PortConfig{enabled:true, description:None}); + ports.insert( + get_port_number(v)?, + PortConfig { enabled: true, description: None }, + ); } - ports + Ok(ports) + } + + Value::Table(table) => { + let mut ports = HashMap::new(); + for (k, v) in table { + let port: u16 = k.parse()?; + let config = parse_port_config(v)?; + ports.insert(port, config); + } + Ok(ports) + } + + _ => bail!("ports must be either an array or a table, got {value:?}"), + } +} + +fn parse_port_config(value: &Value) -> Result { + match value { + Value::Boolean(enabled) => Ok(PortConfig{enabled:*enabled, description:None}), + Value::String(description) => Ok(PortConfig{ + enabled: true, + description: Some(description.clone()), }), - Some(v) => bail!("ports must be either a table of ' = ...' or an array of ports, got {:?}", v), + Value::Table(table) => { + let enabled = get_bool(table, "enabled", true)?; + let description = match table.get("description") { + Some(Value::String(desc)) => Some(desc.clone()), + Some(v) => bail!("expect a string description, got {v:?}"), + None => None, + }; + Ok(PortConfig { enabled, description }) + }, + _ => bail!("expected either a boolean (enabled), a string (description), or a table for a port config, got {value:?}"), } } @@ -174,3 +193,258 @@ fn get_port_number(v: &Value) -> Result { }; Ok(port) } + +#[cfg(test)] +mod test { + use super::*; + use pretty_assertions::assert_eq; + + fn config_test(config: &str, expected: Config) { + let config = config.parse::().expect("case not toml"); + let config = parse_config(&config).expect("unable to parse config"); + + assert_eq!(expected, config); + } + + fn config_error_test(config: &str) { + let config = config.parse::().expect("case not toml"); + assert!(parse_config(&config).is_err()); + } + + #[test] + fn empty() { + config_test("", Config { auto: true, servers: HashMap::new() }); + } + + #[test] + fn auto_false() { + config_test( + " +auto=false +", + Config { auto: false, servers: HashMap::new() }, + ); + } + + #[test] + fn auto_not_boolean() { + config_error_test( + " +auto='what is going on' +", + ); + } + + #[test] + fn servers_not_table() { + config_error_test("servers=1234"); + } + + #[test] + fn servers_default() { + config_test("servers.foo={}", { + let mut servers = HashMap::new(); + servers.insert( + "foo".to_string(), + ServerConfig { auto: true, ports: HashMap::new() }, + ); + Config { auto: true, servers } + }) + } + + #[test] + fn servers_auto_false() { + config_test( + " +[servers.foo] +auto=false +", + { + let mut servers = HashMap::new(); + servers.insert( + "foo".to_string(), + ServerConfig { auto: false, ports: HashMap::new() }, + ); + Config { auto: true, servers } + }, + ) + } + + #[test] + fn servers_auto_not_bool() { + config_error_test( + " +[servers.foo] +auto=1234 +", + ) + } + + #[test] + fn servers_ports_list() { + config_test( + " +[servers.foo] +ports=[1,2,3] +", + { + let mut servers = HashMap::new(); + servers.insert( + "foo".to_string(), + ServerConfig { + auto: true, + ports: { + let mut ports = HashMap::new(); + ports.insert( + 1, + PortConfig { enabled: true, description: None }, + ); + ports.insert( + 2, + PortConfig { enabled: true, description: None }, + ); + ports.insert( + 3, + PortConfig { enabled: true, description: None }, + ); + ports + }, + }, + ); + Config { auto: true, servers } + }, + ) + } + + #[test] + fn servers_ports_table_variations() { + config_test( + " +[servers.foo.ports] +1=true +2={enabled=false} +3=false +", + { + let mut servers = HashMap::new(); + servers.insert( + "foo".to_string(), + ServerConfig { + auto: true, + ports: { + let mut ports = HashMap::new(); + ports.insert( + 1, + PortConfig { enabled: true, description: None }, + ); + ports.insert( + 2, + PortConfig { + enabled: false, + description: None, + }, + ); + ports.insert( + 3, + PortConfig { + enabled: false, + description: None, + }, + ); + ports + }, + }, + ); + Config { auto: true, servers } + }, + ) + } + + #[test] + fn servers_ports_table_descriptions() { + config_test( + " +[servers.foo.ports] +1={enabled=false} +2={description='humble'} +", + { + let mut servers = HashMap::new(); + servers.insert( + "foo".to_string(), + ServerConfig { + auto: true, + ports: { + let mut ports = HashMap::new(); + ports.insert( + 1, + PortConfig { + enabled: false, + description: None, + }, + ); + ports.insert( + 2, + PortConfig { + enabled: true, + description: Some("humble".to_string()), + }, + ); + ports + }, + }, + ); + Config { auto: true, servers } + }, + ) + } + + #[test] + fn servers_ports_raw_desc() { + config_test( + " +[servers.foo.ports] +1='humble' +", + { + let mut servers = HashMap::new(); + servers.insert( + "foo".to_string(), + ServerConfig { + auto: true, + ports: { + let mut ports = HashMap::new(); + ports.insert( + 1, + PortConfig { + enabled: true, + description: Some("humble".to_string()), + }, + ); + ports + }, + }, + ); + Config { auto: true, servers } + }, + ) + } + + #[test] + fn servers_inherit_auto() { + config_test( + " +auto=false +servers.foo={} +", + { + let mut servers = HashMap::new(); + servers.insert( + "foo".to_string(), + ServerConfig { auto: false, ports: HashMap::new() }, + ); + Config { auto: false, servers } + }, + ) + } +} From 9c9f7cfa82750a70471f3bdc97d4d685d4219999 Mon Sep 17 00:00:00 2001 From: John Doty Date: Thu, 15 Aug 2024 11:32:36 -0700 Subject: [PATCH 12/25] Release automation There are a lot of steps in preparing the release and so I'm trying to make sure that we're in a place where I can iterate on it locally. --- release.py | 128 +++++++++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 128 insertions(+) create mode 100644 release.py diff --git a/release.py b/release.py new file mode 100644 index 0000000..b3c8546 --- /dev/null +++ b/release.py @@ -0,0 +1,128 @@ +"""A script to automate building and uploading a release archive. + +This is in python instead of bash because I abhor bash. Even though it's a +little nicer for running commands, it's worse at everything else. +""" + +import dataclasses +import os +import os.path +import pathlib +import shutil +import subprocess + +RELEASE_TAG = os.getenv("RELEASE_TAG") + +BUILD = os.getenv("BUILD") +if BUILD is None: + raise Exception("you *must* set the BUILD environment variable") + + +@dataclasses.dataclass +class BuildSettings: + target: str + test: bool = True + man_page: bool = True + strip: bool = True + windows: bool = False + ext: str = "" + + +print(f"doing release: {BUILD}") +build = { + "linux": BuildSettings( + target="x86_64-unknown-linux-musl", + ), + "macos": BuildSettings( + target="x86_64-apple-darwin", + ), + "arm-macos": BuildSettings( + target="aarch64-apple-darwin", + ), + "windows": BuildSettings( + target="x86_64-pc-windows-msvc", + strip=False, + man_page=False, + windows=True, + ext=".exe", + ), +}[BUILD] + +print(f"settings: {build}") + + +target_dir = pathlib.Path("target") / build.target / "release" +bins = [(target_dir / bin).with_suffix(build.ext) for bin in ["fwd", "fwd-browse"]] + + +def build_and_test(staging: pathlib.Path): + # Tools + subprocess.run( + ["rustup", "target", "add", build.target], + check=True, + ) + + # Test...? + if build.test: + subprocess.run( + ["cargo", "test", "--verbose", "--release", "--target", build.target], + check=True, + ) + + # Build + subprocess.run( + ["cargo", "build", "--verbose", "--release", "--target", build.target], + check=True, + ) + + # Strip + if build.strip: + for bin in bins: + subprocess.run(["strip", bin], check=True) + + # Copy + for bin in bins: + shutil.copyfile(bin, os.path.join(staging, os.path.basename(bin))) + + +def build_docs(staging: pathlib.Path): + shutil.copyfile("README.md", staging / "README.md") + if build.man_page: + print("Creating man page...") + proc = subprocess.run( + ["pandoc", "-s", "-tman", os.path.join("doc", "fwd.man.md")], + check=True, + capture_output=True, + encoding="utf8", + ) + contents = proc.stdout + with open(staging / "fwd.1", "w", encoding="utf-8") as f: + f.write(contents) + + +staging = pathlib.Path(f"fwd-{build.target}") +os.makedirs(staging, exist_ok=True) + +build_and_test(staging) +build_docs(staging) + +print("Creating archive...") +if build.windows: + archive = f"{staging}.zip" + subprocess.run(["7z", "a", archive, f"{staging}"], check=True) +else: + archive = f"{staging}.tar.gz" + subprocess.run(["tar", "czf", archive, f"{staging}"], check=True) + +shutil.rmtree(staging) + +if RELEASE_TAG is None: + print("Not releasing to github, RELEASE_TAG is none.") +else: + print(f"Uploading {archive} to github release {RELEASE_TAG}...") + subprocess.run( + ["gh", "release", "upload", RELEASE_TAG, archive, "--clobber"], + check=True, + ) + +os.unlink(archive) From 73126ba7706c08ca522de8562d8ad380a363f7ec Mon Sep 17 00:00:00 2001 From: John Doty Date: Thu, 15 Aug 2024 11:43:53 -0700 Subject: [PATCH 13/25] Update the release workflow Use the python automation script instead --- .github/workflows/release.yaml | 62 +++------------------------------- 1 file changed, 5 insertions(+), 57 deletions(-) diff --git a/.github/workflows/release.yaml b/.github/workflows/release.yaml index 4ef6b2f..b03aa0b 100644 --- a/.github/workflows/release.yaml +++ b/.github/workflows/release.yaml @@ -1,10 +1,3 @@ -# From https://github.com/BurntSushi/ripgrep/blob/master/.github/workflows/release.yml -# Which is also via https://eugene-babichenko.github.io/blog/2020/05/09/github-actions-cross-platform-auto-releases/ -# ...both of which are very good. -# -# I'm sure I don't need half the stuff I have in here (around cargo -# customization and whatnot) but. -# name: release on: @@ -53,13 +46,6 @@ jobs: needs: ['create_release'] # We need to know the upload URL runs-on: ${{ matrix.os }} # We run many different builds env: - # For some builds, we use cross to test on 32-bit and big-endian - # systems. - CARGO: cargo - # When CARGO is set to CROSS, this is set to `--target matrix.target`. - TARGET_FLAGS: "" - # When CARGO is set to CROSS, TARGET_DIR includes matrix.target. - TARGET_DIR: ./target # Emit backtraces on panics. RUST_BACKTRACE: 1 @@ -88,52 +74,14 @@ jobs: - name: Checkout code uses: actions/checkout@v4 - - name: Install Rust + - name: Install rust uses: dtolnay/rust-toolchain@stable with: target: ${{ matrix.target }} - - name: Use Cross + - name: Run the release automation shell: bash - run: | - cargo install cross - echo "CARGO=cross" >> $GITHUB_ENV - echo "TARGET_FLAGS=--target ${{ matrix.target }}" >> $GITHUB_ENV - echo "TARGET_DIR=./target/${{ matrix.target }}" >> $GITHUB_ENV - - - name: Run the tests - run: ${{ env.CARGO }} test --verbose --release ${{ env.TARGET_FLAGS }} - - - name: Build release binary - run: ${{ env.CARGO }} build --verbose --release ${{ env.TARGET_FLAGS }} - - - name: Strip release binary (linux and macos) - if: matrix.build == 'linux' || matrix.build == 'macos' || matrix.build == 'arm-macos' - run: | - strip "target/${{ matrix.target }}/release/fwd" - strip "target/${{ matrix.target }}/release/fwd-browse" - - - name: Build archive - shell: bash - run: | - staging="fwd-${{ matrix.target }}" - mkdir -p "$staging" - - if [ "${{ matrix.os }}" = "windows-2022" ]; then - cp "target/${{ matrix.target }}/release/fwd.exe" "$staging/" - 7z a "$staging.zip" "$staging" - echo "ASSET=$staging.zip" >> $GITHUB_ENV - else - cp "target/${{ matrix.target }}/release/fwd" "$staging/" - cp "target/${{ matrix.target }}/release/fwd-browse" "$staging/" - tar czf "$staging.tar.gz" "$staging" - echo "ASSET=$staging.tar.gz" >> $GITHUB_ENV - fi - - - name: Upload release archive env: - GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} - shell: bash - run: | - version="${{ needs.create_release.outputs.version }}" - gh release upload "$version" ${{ env.ASSET }} + RELEASE_TAG: ${{ needs.create_release.outputs.version }} + BUILD: ${{ matrix.build }} + run: python3 release.py From 666456e4561170fd39cc66a044b9ae017cc01a5c Mon Sep 17 00:00:00 2001 From: John Doty Date: Thu, 15 Aug 2024 11:50:24 -0700 Subject: [PATCH 14/25] More release stuff (tools) --- .github/workflows/release.yaml | 12 ++++++++++++ 1 file changed, 12 insertions(+) diff --git a/.github/workflows/release.yaml b/.github/workflows/release.yaml index b03aa0b..acf1c6a 100644 --- a/.github/workflows/release.yaml +++ b/.github/workflows/release.yaml @@ -74,6 +74,18 @@ jobs: - name: Checkout code uses: actions/checkout@v4 + - name: Install packages (linux) + if: matrix.build == 'linux' + run: | + sudo apt-get update + sudo apt-get install -y pandoc + + - name: Install packages (macos) + if: matrix.build == 'macos' || matrix.build == 'arm-macos' + run: | + brew update + brew install pandoc + - name: Install rust uses: dtolnay/rust-toolchain@stable with: From 241e8e1eea4cb381a55065da00c3476c1fc67e06 Mon Sep 17 00:00:00 2001 From: John Doty Date: Thu, 15 Aug 2024 11:52:50 -0700 Subject: [PATCH 15/25] This is broken because I have consummate vs --- .github/workflows/release.yaml | 8 -------- 1 file changed, 8 deletions(-) diff --git a/.github/workflows/release.yaml b/.github/workflows/release.yaml index acf1c6a..91c222a 100644 --- a/.github/workflows/release.yaml +++ b/.github/workflows/release.yaml @@ -25,14 +25,6 @@ jobs: run: | echo "version is: $VERSION" - - name: Check that tag version and Cargo.toml version are the same - shell: bash - run: | - if ! grep -q "version = \"$VERSION\"" Cargo.toml; then - echo "version does not match Cargo.toml" >&2 - exit 1 - fi - - name: Create GitHub release env: GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} From 2d1c8a4cebdfbee19eca19c0d1dc1e16078e2d71 Mon Sep 17 00:00:00 2001 From: John Doty Date: Thu, 15 Aug 2024 11:59:00 -0700 Subject: [PATCH 16/25] Set the github token for the release action --- .github/workflows/release.yaml | 1 + 1 file changed, 1 insertion(+) diff --git a/.github/workflows/release.yaml b/.github/workflows/release.yaml index 91c222a..b0b236f 100644 --- a/.github/workflows/release.yaml +++ b/.github/workflows/release.yaml @@ -88,4 +88,5 @@ jobs: env: RELEASE_TAG: ${{ needs.create_release.outputs.version }} BUILD: ${{ matrix.build }} + GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} run: python3 release.py From ff92002dcfad760447559ae339462f9d5dead95f Mon Sep 17 00:00:00 2001 From: John Doty Date: Fri, 16 Aug 2024 10:22:53 -0700 Subject: [PATCH 17/25] Update year I guess --- LICENSE | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/LICENSE b/LICENSE index 7857f35..630f272 100644 --- a/LICENSE +++ b/LICENSE @@ -1,4 +1,4 @@ -Copyright 2022 John Doty +Copyright 2024 John Doty Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal From f13139e79bfaf448da162fba73aac5402514fe0f Mon Sep 17 00:00:00 2001 From: John Doty Date: Fri, 16 Aug 2024 10:23:44 -0700 Subject: [PATCH 18/25] Remove older documentation notes --- README.md | 8 +------- 1 file changed, 1 insertion(+), 7 deletions(-) diff --git a/README.md b/README.md index 1e3c07d..e0ac026 100644 --- a/README.md +++ b/README.md @@ -4,7 +4,7 @@ A port-forwarding utility. Here's how it works: 1. Get the latest [release](https://github.com/DeCarabas/fwd/releases) of `fwd` -2. You install `fwd` on the server somewhere in your `$PATH` (like `/usr/bin/`) +2. You install `fwd` on the server somewhere in your `$PATH` (like `/usr/bin/`, or `.local/bin`) 3. You install `fwd` on the client (like your laptop) 4. You run `fwd` on the client to connect to the server, like so: @@ -21,9 +21,3 @@ If the port is something that might be interesting to a web browser, you can pre If something is going wrong, pressing `l` will toggle logs that might explain it. Press `q` to quit. - -## Future Improvements: - -- Clipboard integration: send something from the remote end of the pipe to the host's clipboard. (Sometimes you *really* want to copy some big buffer from the remote side and your terminal just can't make that work.) - -- Client heartbeats: I frequently wind up in a situation where the pipe is stalled: not broken but nothing is getting through. (This happens with my coder.com pipes all the time.) From 940e57346883c1bad485c726d5d0aea2e5885f4a Mon Sep 17 00:00:00 2001 From: John Doty Date: Fri, 16 Aug 2024 10:23:57 -0700 Subject: [PATCH 19/25] Support for debian packaging --- Cargo.toml | 24 +++++++++++++++++-- release.py | 69 ++++++++++++++++++++++++++++++++++++++++-------------- 2 files changed, 73 insertions(+), 20 deletions(-) diff --git a/Cargo.toml b/Cargo.toml index c4c05c8..7e9f5ac 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -1,12 +1,14 @@ [package] name = "fwd" version = "0.9.2" +authors = ["John Doty "] edition = "2021" license = "MIT" -description = "Automatically forward ports to a remote server over ssh" +description = "Automatically forward ports to a remote server" +readme = "README.md" +documentation = "https://github.com/DeCarabas/fwd" homepage = "https://github.com/DeCarabas/fwd" repository = "https://github.com/DeCarabas/fwd" -readme = "README.md" [[bin]] name = "fwd-browse" @@ -41,3 +43,21 @@ procfs = "0.14.1" [target.'cfg(target_family="unix")'.dependencies] libc = "0.2.155" + +[package.metadata.deb] +section = "net" +depends = [] # No auto deps? +assets = [ + ["target/release/fwd", "usr/bin/", "755"], + ["LICENSE", "usr/share/doc/fwd/", "644"], + ["README.md", "usr/share/doc/fwd/README", "644"], + # The man page is automatically generated by fwd's build process. See + # release.py for details. + ["target/release/fwd.1", "usr/share/man/man1/fwd.1", "644"], +] +extended-description = """\ +fwd enumerates the listening ports the remote server and automatically listens +for connections on the same ports on the local machine. When fwd receives a +connection on the local machine, it automatically forwards that connection to +the remote machine. +""" diff --git a/release.py b/release.py index b3c8546..381a2a7 100644 --- a/release.py +++ b/release.py @@ -5,6 +5,7 @@ little nicer for running commands, it's worse at everything else. """ import dataclasses +import enum import os import os.path import pathlib @@ -18,14 +19,20 @@ if BUILD is None: raise Exception("you *must* set the BUILD environment variable") +class Archive(enum.Enum): + TARBALL = 1 + ZIP = 2 + DEB = 3 + + @dataclasses.dataclass class BuildSettings: - target: str - test: bool = True - man_page: bool = True - strip: bool = True - windows: bool = False - ext: str = "" + target: str # The rust target to build for + test: bool = True # Whether or not to run tests + man_page: bool = True # Whether or not to generate a man page + strip: bool = True # Whether or not to strip binaries + archive: Archive = Archive.TARBALL # Archive type + ext: str = "" # The file extension of the binary print(f"doing release: {BUILD}") @@ -33,6 +40,11 @@ build = { "linux": BuildSettings( target="x86_64-unknown-linux-musl", ), + "deb": BuildSettings( + target="x86_64-unknown-linux-musl", + test=False, + archive=Archive.DEB, + ), "macos": BuildSettings( target="x86_64-apple-darwin", ), @@ -43,7 +55,7 @@ build = { target="x86_64-pc-windows-msvc", strip=False, man_page=False, - windows=True, + archive=Archive.ZIP, ext=".exe", ), }[BUILD] @@ -100,29 +112,50 @@ def build_docs(staging: pathlib.Path): f.write(contents) +def build_archive(staging: pathlib.Path) -> pathlib.Path: + print("Creating archive...") + if build.archive == Archive.ZIP: + archive = pathlib.Path(f"{staging}.zip") + subprocess.run(["7z", "a", archive, f"{staging}"], check=True) + + elif build.archive == Archive.DEB: + subprocess.run(["cargo", "install", "cargo-deb"], check=True) + shutil.copyfile(staging / "fwd.1", target_dir / "fwd.1") + subprocess.run(["cargo", "deb", "--target", build.target], check=True) + + # Knowing the deb path means knowing the target version but I don't + # actually have the version here. (Or, like, I have the release tag + # but not in testing.) So just find the hopefully singular .deb that + # we made. + deb_path = pathlib.Path("target") / build.target / "debian" + archives = list(deb_path.glob("*.deb")) + assert len(archives) == 1 + archive = archives[0] + + else: + assert build.archive == Archive.TARBALL + archive = pathlib.Path(f"{staging}.tar.gz") + subprocess.run(["tar", "czf", archive, f"{staging}"], check=True) + + return archive + + staging = pathlib.Path(f"fwd-{build.target}") os.makedirs(staging, exist_ok=True) build_and_test(staging) build_docs(staging) - -print("Creating archive...") -if build.windows: - archive = f"{staging}.zip" - subprocess.run(["7z", "a", archive, f"{staging}"], check=True) -else: - archive = f"{staging}.tar.gz" - subprocess.run(["tar", "czf", archive, f"{staging}"], check=True) +archive = build_archive(staging) shutil.rmtree(staging) +assert archive.exists() if RELEASE_TAG is None: - print("Not releasing to github, RELEASE_TAG is none.") + print(f"Not releasing {archive} to github, RELEASE_TAG is none.") else: print(f"Uploading {archive} to github release {RELEASE_TAG}...") subprocess.run( ["gh", "release", "upload", RELEASE_TAG, archive, "--clobber"], check=True, ) - -os.unlink(archive) + os.unlink(archive) From 9e8fa4d0a6f4d473f2fbf69d0dd3515f679d8733 Mon Sep 17 00:00:00 2001 From: John Doty Date: Fri, 16 Aug 2024 10:26:50 -0700 Subject: [PATCH 20/25] Add debian build to release matrix --- .github/workflows/release.yaml | 15 ++++++++++++--- 1 file changed, 12 insertions(+), 3 deletions(-) diff --git a/.github/workflows/release.yaml b/.github/workflows/release.yaml index b0b236f..2f508d0 100644 --- a/.github/workflows/release.yaml +++ b/.github/workflows/release.yaml @@ -44,36 +44,45 @@ jobs: strategy: fail-fast: false matrix: - build: ['linux', 'macos', 'arm-macos', 'windows'] + build: ['linux', 'debian', 'macos', 'arm-macos', 'windows'] include: - build: linux os: ubuntu-22.04 target: x86_64-unknown-linux-musl + packages: apt + + - build: debian + os: ubuntu-22.04 + target: x86_64-unknown-linux-musl + packages: apt - build: macos os: macos-latest target: x86_64-apple-darwin + packages: brew - build: arm-macos os: macos-latest target: aarch64-apple-darwin + packages: brew - build: windows os: windows-2022 target: x86_64-pc-windows-msvc + packages: none steps: - name: Checkout code uses: actions/checkout@v4 - name: Install packages (linux) - if: matrix.build == 'linux' + if: matrix.packages == 'apt' run: | sudo apt-get update sudo apt-get install -y pandoc - name: Install packages (macos) - if: matrix.build == 'macos' || matrix.build == 'arm-macos' + if: matrix.packages == 'brew' run: | brew update brew install pandoc From a4df8fc588d3fed5de908563b37a263f7cfac040 Mon Sep 17 00:00:00 2001 From: John Doty Date: Fri, 16 Aug 2024 10:29:37 -0700 Subject: [PATCH 21/25] This is a better name for the build --- release.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/release.py b/release.py index 381a2a7..0fa912b 100644 --- a/release.py +++ b/release.py @@ -40,7 +40,7 @@ build = { "linux": BuildSettings( target="x86_64-unknown-linux-musl", ), - "deb": BuildSettings( + "debian": BuildSettings( target="x86_64-unknown-linux-musl", test=False, archive=Archive.DEB, From 3430cae95740ce5a446647ec7ed691bd4867bc23 Mon Sep 17 00:00:00 2001 From: John Doty Date: Fri, 16 Aug 2024 10:36:56 -0700 Subject: [PATCH 22/25] Add fwd-browse to the debian package --- Cargo.toml | 1 + 1 file changed, 1 insertion(+) diff --git a/Cargo.toml b/Cargo.toml index 7e9f5ac..bed9327 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -49,6 +49,7 @@ section = "net" depends = [] # No auto deps? assets = [ ["target/release/fwd", "usr/bin/", "755"], + ["target/release/fwd-browse", "usr/bin/", "755"], ["LICENSE", "usr/share/doc/fwd/", "644"], ["README.md", "usr/share/doc/fwd/README", "644"], # The man page is automatically generated by fwd's build process. See From eede5b0e5061763c267ec7e8aa24bf5e4b010d8a Mon Sep 17 00:00:00 2001 From: John Doty Date: Sat, 17 Aug 2024 08:03:41 -0700 Subject: [PATCH 23/25] Support showing and hiding anonymous ports I'm still not convinced that showing a big list of disabled ports is the right thing to do so here's the ability to turn it off. --- doc/fwd.man.md | 5 ++ src/client/ui.rs | 148 +++++++++++++++++++++++++++++++++++++++++++++++ 2 files changed, 153 insertions(+) diff --git a/doc/fwd.man.md b/doc/fwd.man.md index b7e5df9..5d5f819 100644 --- a/doc/fwd.man.md +++ b/doc/fwd.man.md @@ -59,12 +59,17 @@ The following commands are available while **fwd** is connected: **Enter** : Attempt to browse to localhost on the specified port with the default browser. +**a** +: Hide or show anonymous ports. +: (See "identifying ports" below for more information on anonymous ports.) + **e** : Enable or disable the selected port. **l** : Show or hide the log window. + # IDENTIFYING PORTS **fwd** enumerates all of the ports that the remote server is listening on, and attempts to identify the process that is listening on each port. diff --git a/src/client/ui.rs b/src/client/ui.rs index ae3fcd3..bb5b36d 100644 --- a/src/client/ui.rs +++ b/src/client/ui.rs @@ -155,6 +155,16 @@ impl Listener { "" } + pub fn is_anonymous(&self) -> bool { + // Anonynous ports are not configured and came from the server but + // had no description there. + self.config.is_none() + && match self.desc.as_ref() { + Some(desc) => desc.desc.is_empty(), + None => false, + } + } + fn state(&self) -> State { *self.state.lock().unwrap() } @@ -223,6 +233,7 @@ pub struct UI { show_help: bool, alternate_screen: bool, raw_mode: bool, + show_anonymous: bool, clipboard: Option, } @@ -247,6 +258,7 @@ impl UI { config, alternate_screen: false, raw_mode: false, + show_anonymous: true, clipboard, } } @@ -334,6 +346,10 @@ impl UI { ports.iter().map(|p| format!("{p}")).collect(); for (index, port) in ports.into_iter().enumerate() { let listener = self.ports.get(&port).unwrap(); + if !self.should_render_listener(listener) { + continue; + } + let (symbol, style) = match listener.state() { State::Enabled => (" ✓ ", enabled_port_style), State::Broken => (" ✗ ", broken_port_style), @@ -379,6 +395,7 @@ impl UI { Row::new(vec!["ESC / q", "Quit"]), Row::new(vec!["? / h", "Show this help text"]), Row::new(vec!["l", "Show fwd's logs"]), + Row::new(vec!["a", "Hide/show anonymous ports"]), ]; let border_lines = 3; @@ -477,6 +494,19 @@ impl UI { Ok(()) } + fn toggle_show_anonymous(&mut self) { + self.show_anonymous = !self.show_anonymous; + } + + fn should_render_listener(&self, listener: &Listener) -> bool { + // Named/Configured ports are always rendered + !listener.is_anonymous() + // ...or we might be explicitly asked to render everything + || self.show_anonymous + // ...or the port might be enabled or errored + || listener.state() != State::Disabled + } + async fn handle_events(&mut self, console_events: &mut EventStream) { tokio::select! { ev = console_events.next() => self.handle_console_event(ev), @@ -584,6 +614,10 @@ impl UI { _ = open::that(format!("http://127.0.0.1:{}/", p)); } } + KeyEvent { code: KeyCode::Char('a'), .. } => { + self.toggle_show_anonymous() + } + _ => (), }, Some(Ok(_)) => (), // Don't care about this event... @@ -1250,4 +1284,118 @@ mod tests { drop(sender); } + + #[test] + fn listener_anonymous() { + let (sender, receiver) = mpsc::channel(64); + let mut config = ServerConfig::default(); + config.insert( + 8079, + PortConfig { + enabled: false, + description: Some("body once told me".to_string()), + }, + ); + + let mut ui = UI::new(receiver, config); + + ui.handle_internal_event(Some(UIEvent::Ports(vec![ + PortDesc { + port: 8080, + desc: "python3 blaster.py".to_string(), + }, + PortDesc { port: 8081, desc: "".to_string() }, + PortDesc { port: 8082, desc: "".to_string() }, + ]))); + + // (Pretend that 8082 broke.) + ui.ports.get_mut(&8082).unwrap().state = State::Broken.boxed(); + + let listener = ui.ports.get(&8079).unwrap(); + assert!( + !listener.is_anonymous(), + "Configured ports should not be anonymous" + ); + + let listener = ui.ports.get(&8080).unwrap(); + assert!( + !listener.is_anonymous(), + "Ports with descriptions should not be anonymous" + ); + + let listener = ui.ports.get(&8081).unwrap(); + assert!( + listener.is_anonymous(), + "Not configured, disabled, no description should be anonymous" + ); + + drop(sender); + } + + #[test] + fn render_anonymous() { + let (sender, receiver) = mpsc::channel(64); + let mut config = ServerConfig::default(); + config.insert( + 8079, + PortConfig { + enabled: false, + description: Some("body once told me".to_string()), + }, + ); + + let mut ui = UI::new(receiver, config); + + ui.handle_internal_event(Some(UIEvent::Ports(vec![ + PortDesc { + port: 8080, + desc: "python3 blaster.py".to_string(), + }, + PortDesc { port: 8081, desc: "".to_string() }, + PortDesc { port: 8082, desc: "".to_string() }, + PortDesc { port: 8083, desc: "".to_string() }, + ]))); + + // (Pretend that 8082 broke.) + ui.ports.get_mut(&8082).unwrap().state = State::Broken.boxed(); + + // No showing anonymous ports! + ui.show_anonymous = false; + + let listener = ui.ports.get(&8079).unwrap(); + assert!( + ui.should_render_listener(listener), + "Configured ports should always be rendered" + ); + + let listener = ui.ports.get(&8080).unwrap(); + assert!( + ui.should_render_listener(listener), + "Ports with descriptions should be rendered" + ); + + let listener = ui.ports.get(&8081).unwrap(); + assert!( + !ui.should_render_listener(listener), + "Not configured, disabled, no description should be hidden" + ); + + ui.enable_disable_port(8081); + + let listener = ui.ports.get(&8081).unwrap(); + assert_eq!(listener.state(), State::Enabled); + assert!( + ui.should_render_listener(listener), + "Enabled ports should be rendered" + ); + + let listener = ui.ports.get(&8082).unwrap(); + assert_eq!(listener.state(), State::Broken); + assert!( + ui.should_render_listener(listener), + "Broken ports should be rendered" + ); + + drop(sender); + } } From aad9c74a63d0f0640c6766d5553278b694f28841 Mon Sep 17 00:00:00 2001 From: John Doty Date: Sat, 17 Aug 2024 08:29:27 -0700 Subject: [PATCH 24/25] Vanity --- README.md | 3 +++ doc/screenshot-01.png | Bin 0 -> 85053 bytes src/client/ui.rs | 2 +- 3 files changed, 4 insertions(+), 1 deletion(-) create mode 100644 doc/screenshot-01.png diff --git a/README.md b/README.md index e0ac026..c062243 100644 --- a/README.md +++ b/README.md @@ -13,6 +13,9 @@ doty@my.laptop$ fwd some.server ``` `fwd` will connect to `some.server` via ssh, and then show you a screen listing all of the ports that the server is listening on locally. + +A terminal displaying a list of ports and descriptions. Some are dimmed and one is highlighted. + Use the up and down arrow keys (or `j`/`k`) to select the port you're interested in and press `e` to toggle forwarding of that port. Now, connections to that port locally will be forwarded to the remote server. diff --git a/doc/screenshot-01.png b/doc/screenshot-01.png new file mode 100644 index 0000000000000000000000000000000000000000..4f5315fc2dbbcaea337f9a36788d1649b51c198e GIT binary patch literal 85053 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 ZRKr^#uXVA|QR!(L*@VwRDl?;waQyH8KbQroqoq zk$ln56~>mbYxT4ioqOZzIp!sBxotxl)3o<@H1k z_|SXzMy+1LMs}}2dO5gFH*10LZP7jf2@aX=(_~SZ>wVia3)nMxH~DWXE4x2DV-m}G zPL8|3bmfjKxyj!di!=9_5KdMz`QkQ5h3+dQMdSOhVF_q;CwUGaCkZ`>(Ce_kWHhQU zh^^dnfWFh~*DaC(<}%P2_K3h-AtJ00>Y>2Mzwj6^byos}@ZF1rH%DaD-c}&`*6RQBky1J2gHx>{yNTU&f$=+mU$FJ(v;Q-QXjo2%mdLz z39w?sLF64F;8q5)A>^qnJqD)Tm)l!P81NLbKu#umJ8ER>y2R(eY|wA3@Pbxj)VU)^NxZ zwo^k6yVD~t$Cx}c+yuyP;2g47eD2Xb*I+=1IzR277t_*UP|Z3iQPFh&QBg4v=KOWX zQjqR;VaIgd2mApFDL-MozU|$OUOiAWZn3bG+j7A9JxHkQY5k-Y$C~53&Lb>xkW@b4 zr5%%^+XQ-;k |LwYtkOLlqd7EmoVSpAtx&!BJlh#jvuh`%oeJDRvSEYiQ;o|T#j zcf~iLj|?3`e3@PDDl#VhQoU?GUpTC$3xB56yGfL_u@`?oX7b%QO-lthX)^Wihy5(! z4_vR#ZLuCExaKr8HC`kV7B!25jVE@DwLZv_H@(#Nfmt~daaedyQa-;kMssD6=6*o1 z=Vo}+{j4zo6Ob@sZC?~V)I0FPpw-KVK@Sonz<%zOG1CpxdK0C?!7Wr)Cmm5n!6l@k zr$7G}3-B&BxGUF@UyvPt5xC~_Fo&$4iQ2oGm~g`E4f$pbk8D55%f*8B{A6ViL1BOVPSWUGOhrKp&ivM z>{SqP{&fHH#+6~9wNdY8z(u?LddE)Bv&`WUwRP*U(zIwTOmq-X@!yvK(hXO1-Ggj( zgEZAKf;HsPy+IfMS^3BROUj=fpJG1jd-ThQ={crsWo|MG%M}liN~BxE%5}h@V`^j0 zf5ljsp8Y3`MK6bqk2KcE#YN`@@ouat4_1|q3m$(P!isAEncbCa;((6-rzL5UwUuZj z;f8F#Y5VLXEo$LeXUCADRN4`I5+x4DgcS?DPM3=4FTaO`A3v3iyw;lx9SKY2PXuw` z_IUL=&{?aXXKR7Z!se@|snu(DX?9iW#-b+-`0>Y&nSGs$k8X~)4~$h;Eu3EbEyi-~ zbC%hQ)If7Ao10vfbliKfU2aj>&&Z=lp1bdJSCquZJ!r-SaRdF_2X9Ac;UO(`o#(al znaKfal*pC=lz|)e!**D=$;cVnqrSrde1qt7i!j6FYj@+{HhiPZkhXVBNFq#qI=fMP za-Uu(=GSt*d@&M3mlpaBaN_EzOu$D9;9k`%WYki^i(NJ&K*9w4lS4nZK<4Iw5K~7h z^0&65@cEm=MXPDWdUc%~Od_Qi`fUawR7d|Rq5AE+%M+I0{Dy5$$XSb4GL0h8D_q4D}i6Z&M|!wZHLqtPgridX3G-1C~IP^_p~wmZ34wh|TTBm1rF%(C9wG?aV>(W&3l zfjVHlO@T=E9ooBa-&#~HrS$!>Ue%bX(Hc}P8OogYDTr|nz)EQ zwuZPC2*Humdkduhkgjv!gn>t|D48~ciVLyyf>M*__dEYVo}0dz6ml$v@VmLXTfgZ` zbYl~KOZ;KcCs61A59adyzr$S6v_Pa#j7*}I%K+;-DS948)|r=^`s9xU-dSgD>nLJr zX|CaDuauV2f&-WU(t3pG*y?4EgLhRnMJan-5;J3_B z;l6{3Mqgm@Ou*}t$vr|+1)^U<9$jA8<^3;JfhIzlFhB;$xbqF6H}~&(F2)2!e@n

L*_N^*QPQ3dUQ!@(WaGiH>dr=^^a;wF{e^jvL zxEM-*uOMS>VaFn{tKoy}msLj&%L<$2>o5Bxe~AAeV#xy&d^Agu>6&gNOsxrt3^OY0 zB^uqp2gJwG-utr}Kww|lO2I)?@<<})k28KP7Bn->v*>93DqWEKsp~3GTMP^={ z8}@Tn29yUFO>^Xnl&;|dWVt0cJv_H#%!Q0rkN!6QuWNb|{+E>BA)ev4W-_Wu(~Z-{ z)Nn&L%R2Z>bu!eWxTv=TfrK|Za{Ft0lOHY)j(QTEH(KgU3Vz=mO);6yl@z@(Mc=;~IS12OWVaqhBfB%usbAU4y0;)ZhPq3;vH~6e3gR#M+47aFgLK7uwFqcasO1)umYYqt$U^77ECq?vTGH`|EX=ceGovt?xc; z1aYjLc!^1dm;NidA3~|;v52{P?%H-5>Vp z!F2S|Z~*JfuE2OIEU7@3uLj(rIerq?W((rcFD31a0n4K^YrXnySgPs852{&wcOAx* zcIgq~&J!BAZHL@{v;=%Wr2Tln-zf-mB)3f4TW5tJl9JNyT72ZwEv{W?Tf<}PcvHD> zR~nNswR-%~^J8<&7Hpgsn-=&j?Z)WlQ7tijWWpQPpPLu#N~e48E%nGxNCClj6fw$C zZ#>AfG=ew4q-}Ol1tBXTZ`BFSKjX3H)(hMlfSzNZ@$Mj52z4Vho1VOF)=EkEVqSmG zV)4+gR|=NorNADQH91Vzg(1Hm8xX!@sCooxa(ePo`IDCcxtn(pQZC}mT8TJxyWNrL zA*`Pl3fHR(byK@DwzKByaZ_xPFELH2Wh%lWc-yX6N;WGeZD26pRFpagYe39(Wf7`b z51!2{#9k-WB9o5O!ix`i-UE^}yyq!L-ef1`rTJc438SZi((m70fw#KC#ghLZE+G8o zR0enorYAr9FRp+E8H(;N5h3bU7ydBIO~u4Ya>da~{YxkD8eDZIXl)H=N4n&&j&aW1 z^C_pCz z?~h}|uN&HI#n-IrhH??hcGNw{MlIHd=t^Z_?4ucmR1;jgq6JdBaCpA~$N<&MQGnDj z8;ti5ri1(oWdXU$hlJk2;$JWCKOU=qNozhPyOol=ZmLAD&Vb&^E(g##91^jYJNZ4- ztf30g=-cpj;h{%v0HH=4?hN=jYrIgjc2}%C+k^VedYAH;)upx?7|pc>4L$ds22U5? zgCQecHLU*ThQ4|=B?Bxbm-?&CT}feeod4n+)=QI&FkT#Zl_wuJt}U!Dcwf~0W8>gl zMoRcgp-^z$Fy$wO0J4ww1D&E<;ouJ}?l0z0epwHrzzEY+CU-OiFK)~sQcY4zvUvwvDewSe23w>3w`E;!qI>ncP&J{h+ik@Yk=$*0 zg6Ni1x*SZ9V)LR62_~6zY*_RyLT5cCve4ML@+BE|X4#E|9(3S)U`&2a!u`lKMlgA% z@l3e=kWL)C3tdlBq2y}>eSH(2qiLjQSIQZk_}@_Q>+T5pT$a9P#cK*h{dcr?%j2=^ zD)pS7k~vQD$Bix*(5M#UE8&~5`6GsjCdE)2t{4liViT`(u-ap zeD`m@B4+L1eZ|nuOwp%#WJO{r*Bql=_3%HDMh#|Y z(x_B2)q!e`)2>^X{9(cq`8$9FUMD(eR^Gpm{tHI%n^9QT*}QD9S@x^9a#e{Qo#-)S zA{X?2++oC)97n=xM)ApB0o|1GVPr`r9}p|kRk;c{XcRO^NU(K3lB8_iTcRkim4IR_ zs-m6Xj#q1va&fX?xhh{GUV`#~;;hJ-24|tILCj9I_H{pxY6Ktpzskoz@%EVr^}Xj) z9&_bf=-K&p*zBPzq~%~Wx?b1k6)9^c>}H@pFkLdPh?=K*TDH;%^xx8wAB|K(;+r0@i0gpp4L8`@1ymTWRjNUK>v`zu{ zRacH)wtHZlm6)yJ?&5XcL(rRyUe3{6Y2uqvcINjl(OP&VZ<-d7Q2Ni<4DkFfY*p+g zT^79r4#{;Pps1UbAu#af4gh#{fb;3zB^&VZ*r7~mgmEB<@zd{}|AHPTW4(n3A$Pk! zLvJqa&u}7`!hgLH(4`SC3a&psp`YkKKfrN5$pjC5+!92Ei6;N1B}zW%{D~p`8;|sN zKKxt7z)xFpgf`O$xvh$X$nZO$=IbxDsnJVs43xVA z{q9|zdG$Z037;R=)Nn<@#_a~q z=Q>|s$8PEL|9KLIuiN~&aGl8`I(*$$9b^x<=`e2r#%OYf$pMd<8~)m+RI(m?YC9WR zlV;{SU)$G~h&oYCZM6J*zQ6*!jzg#*p+8fN#@wa*hoeVVOs09I(N%V@2&;zayA7dU z{eK(spSp(sD_l^AHQUQ-=q%+{nELi3ST0}OQ%q(S=dfZ?(&DD6Q5JK?R9%j4N@g^# zKdirQB;sq<Ft|Cn|11C;)d>2-B=8HU(LD-S4@H#$CF$5tDvXpg zwWB_}F<7{_c3u17o}3t!&EIV@@)ux)!3?ZTgj({rv$Kzm=IZSnS}6?#r18+xPJ#o$wDEvbxtc31Cw`8>lmj` zPW4M_7aT%s&|uvi*sj%lzfOS>XL9roIcjh|5XVL<;o~x}u)eUq9!c=D0z)}X!Fx}e&UK?YZ`nA4O11lEQtPf7pTM+q zet5fV=8IReDlI>^E5$*dKH)FA!lh{Vm+Z!UmSlWLU6*vMZG@=7H4JYT+EhjNJ;~fx z_w87+G~^jo?9Ad`=_LB0bP75mI}@ZFf>nX{SD;@jUC% z{ckh+ReN*u&xisX219Ff=IPk2@Mjx1tDCj(dgMrzg`cP{7PB9gvvO2PG^cZo9+;ns zl&bUz;bQ_S)*y`Zc6k+qOCokmMoPC|_dKJA;_SUl(JSYEfA?MC52mNYAXj-QUjVzs zqj0v!9)Q$OA?(uQ1kJq+d}Eh*`wCx^Hb%3I6<-ne2k$Qn$Gce`KLc^IZ`9{8Vaw6D zVhX6Upf^(5SF>Dg>k4BkkeX0qIpivh{2m+V$|@(2E+0xhCgsed)BAldr7wG+TL7ia zs4AV%UMnu*u_7-PV{NzUk{MQOCi{-usMt{hf66BVFU-U#ij5x$<4aLKWK<(oyt~29 zFGXPu=unvs8S*5rrzXnE`yTkjK=`%CsXnP;qWvqcaUFFG046tUaPdS+yjwF{a7nJ? z=)M6pVaNf1{i(=N#;snHK^VjR9 zW$aD-lBdbmqs1t9gI72J*)bADM~N{*-j%d*If0cfDd=~>C_2s^KT<>JSO{8;_!zF) z8t77QrI&eYFIz%${q#xSp3&jPw}Eyc{Hn zM0zBxW!{g>FMEiIh!T)5V&utl1Zy@$S1fTs+x(EOwM+cvxWO{ zB?gsv?ZE|RQPr((pP#sSx;i-ocKi(a%NTD9dcgu$iBAL3Z412BeRl88NFf#Xf`=cz z>OEOcI{~!m$mx2FdOPbR7TG-leoBotV2%5dylZ=FQjSl$F5wAY4#gZuGgn9RCG{IN zy5F|1$|wDsmJ8ulL!metokUsfqXGUb`M7I*?>CSK}yJcyDq?T1tpSLtvt_TnU#Bd9G36(nT`^N zlL*wDt%{rAt|l*dFDjkeT+D?Vi@+!+%3Vo@>n>N;YYy>J{Hr_5Aqgae$yCAx6E=eM z;@CgX56yFm1BZU$c-@(H27GL;Lb{mN6@9Yz_X%vA+i@ee6lo5E0l|VgnroK&`eE?2 zUk^nJrN6U)_?=9l&kiGY?!@R(7msm$jE^Dx2`p6xoD^v^b{|fx`tJB>>r~ggIqDKM zUfFGK9xs})E#dsZ`@q42C~BAvH~wG>#~d*`#apME=;?<4a*V2c^6ZnpapLXZZUdUA zG(L7a*B8x2ipy(J)l#yg?xhLXF7;0yMKRtpN%ciL4oj{ji>0jyq2K}~aY=FP(nD!~ z+)M*;yvM4~H0hN2PQ6XZaxmEA<)2(+-jde;BKu~Ei9Pf4oev)w8A&mMpT@IL)w9aK zqHx{v`08cAfrDeH|H=1vhr`6{qpwvDpMM!!!h%!`2opYg&15eaQJFS9Z%^XNx%|O< z!T=|9+{5fHW~?9c$xRqcdyHC_hIIkj*>6nhmw8@FF+hl z%>^oZwrBXLl1jKX*e8Cl3B&B&G3igz-NkRy3FXr)#97ftQeR(3ZFuQ_QhVSVPmuE* zh#o)86&^h?fsyh2r?1q{Y|D&}r?dIG_KycB96@JN!sJmQ#pP_Js~eeL1TQptUyJ-+ z*16cLTy#EHXzV}yhJpW6)HiSg@FMv0bVZUbL&g)tDI;zKd@ zJmVx|(j$4D^FQhuC9yfm$!bWOi^EmG4~OX|G_;j+H!7k1?I%NI#fe`#hJUX&N!h8g z3qM-#rJi<23(gB2{=4BY0Dk$my2f99zK&EB_b{Zj&+X2I|7?yJdkXsNhh}!aPU`P> z$DdV<(1O3(eE;odGv5R%SY!|6kl@R;NwCX49goQujq#wp;f7M^j)Ej)>1{RG;M22H zu~kQB;gj&j>Rt1T2NqgI0ECBLKzf{bJ`e>>c_$ugn zj-w?lCa5(Gdb|o0dlRg3VF_IDA4IhvO#{2|ueH7BZhpEr#+bt|olP%}i%k0bOQPxB zCXyxmPpQ>~T-Jjm&)b+{N)Pe9M6aq4qKH(K>&(ERuYg}e({h6z7it%nKC2+`)tvDf zC;hZ~66|xH&J#Yi#X=vnGo9Jg`kV!P@zoiGD!HbzaKG~1IplOeHzO*CnDV19z|f9B zmVV^QeoeaX2Iec*Rh9vfGr}THK=AD5vHPH;J3Rj8rV_n{+xA(^JS-l0HHTwTB~D#` zfbxfT0xz;}kGwiOTfbi4CKNde89%Yb~3}S4|1T_qgjpwfb1riz2qeV zA$%PCR5D-&JRdmB^nj=AWVmjf1r;=~?hHkIWZ-u*IbV{QKj=oC2{u4)ZsnOGPfOup zqX1l}-F^{z7oMS9bM&I~T6CvTFRk7hh>E1vjlNFeqQ4kl^|qyVXoPY1m%)JD40-AM zn3?ofn|j>-TDJh_r?lij$SnqX9^sR0S{vu8O~XOpBoexRlqw%IeJdCtc7Al!1Wd+k zTGmv6;Ch#Trw2>8?DBhyNhIk3f=v!O5k|EET{t{K3&G?|`;1qG&81XmH?T&(;BFRd02lI)RYQo%Hrd=3D(YkFi~jhBSTq@`4Tym<&-z8SYNAN9b1e zr3G4~?d5Ucq_JN!cJjQSn4g8kVmHclSd#9g$wir9;OS*R4;y^B4{>$2X()Nz?+sZG zQx^(qQ{cHcKIlHLk9;TT(ald!wLR--Ds=rWSw&KB__PzUOY1D;)$Mgl40u@r4@4-# zA(!0+%AJaidCO|RqXGqC;hs!$ak(BeAZxL$pzniMb(C_V#( z@(IvKz73fhs9oU5tKewzk~>-%y=U;9uJLjkd@-+7Wbt`FumYX#Amj>2xmvaYJxNb~rhnBO{m=@lGobmStDBQaBh{0Kby z5FayZ9ag;cT==|AeDY=~Gm^Uj=$GXHKf76gOrFAzrzZoJ_5#&_>#6D@F6-@O+hRux z{gTMTb?)nm>9rHn`u#b7v7@Yt`I~5*$B0J{qn9O5vSMg5O3u4B*XLh4P08~IjsI$( z8cZGwaq^rElMI~rd|C)ZZXni}O94mwU?+O9-Ie0xbic7dx>~FE7yx2~+ZM;^Vo*CG zDDcR}L-lqwtVFHR@9J|N3+izR9dL!se}@~k_k-)5wZ~*tKV4`ycBCEFPLH+jz&6!; z8-#bzUIOiEmiMgNwG6%&+!>{pejV5N05v)dhdez3Ea;&Y52@jvL&`Bi2m2ia(9}xm zmynfEbJG_^q9wIkia3DNkE`Uy`&c;N@bCGIb0j~z@!M^OUL4u+htA4R{D-4kROaKst%h)OQ6OlyOk@!+Vk!| z&5!{%Aa$dg%H6VR%E|4JYt$(s=|r<;Yp)?arDl=Y5WK&(yjOejNg=M)3wVJe0hE%z zjc8a^^Y41wdEE+!2Q5Rd{D#hgu1l-(jMLxKsZY$5#+vM>siOvY(#x!0in+sSC+AXz zn~JOFf)L5jIz*`+5Agis$DZ>@^_wYRy8D>!v%sx%(Go+;RM9>6eS67T)LYTOtDPg~ zbgrC?$Qab*5#0Z!>FF#<%+2(8^|TY%636+hT^aQqT<3KWXuWe1m-T6WH3Vl5+{N|c zIyMK{*;I9vBHBX$XZ`a^$Baqu9Dj&uMCH%ONS-hF_O7#(0fLv3oPjrSbCNe(%MxCT z5W1CA=)isQ^`M&~K`(hjf(M)iM8T$Y^_I_fTn?wLX-%Q)$aUlMyos$O z90S8z*2la%4aJTVre}t<$ntAxRz&^UrS+BACpO0&$n_|B;DRx{PVfj20MiRtQl=NW zsys{cI{6B}j>HDQ>ozBQ^>{>A7f6!1Yqye@v*>n#EhBP|po@pA9AbcA|HI2jo_ab! zh;h5Jy1o`VnUlP}v6QhFaWQ})7S0>2p_~3^)^rc&kD_>j4x&Zrfj!URP1l6eHs5MG zQzcwx2F#F2JW!Omq<=ORU+riL#8hO2-GdZh2Ad_{j^DBfb_71MUKNEfy zgj9gn=QW*hpibxfk9-|E3FnV<&QM3#H6?E%y(KronF!~+8*=^j7I9FAvPCK9j+~ab z3pzT!O@sFH2j#btX|8jjqFx;Ng+0CiHcOtW{wL`?J{Q8yhQyl+K{qG8HzH8z-rQCZ z^r+Qe!C7iMj~lu$qCgA3nKJh#kjGE(HoAf38P%?|nO9R9O3Q48fm|%;?-~Oy2xQzv zM9yZNn=V?B3e^1w)OaEx1u>$w7r%36#UJc*Roy4<*FA|k`&mm->ag{r}bFU z#8*_WipE*v@$0@pBA{@I21l61T3u?>jXLW5#L(bKsGd9UYB-TjO8lbj>AjuN7_kh% zY=yd5&O-x;4fkN~R(T%v1>UB>Xv71~@7j`AU10Fn+6u%`-i^oTyZGGJI2mTt#e`)F})* zeln`xKHrTQ?t3%t*^4M#%hCL;qe2Bv#zGz;(I=@C7PL`_Ld=><@Ixz3-SYGTx2VCu z(M-`BBuFsb?aGyG%Y8Z(p;#{;mRlhddhcls@cvX)owQO z<{|$3Z9EbO-C(irYskI}kCTN2c?bI+tKXo)L)pBS`IKiX=-1?=;XHKZoaF3$Q4cY0 zF+4(YFMKv0<=;LxPD5Q>?r<3AfdJv)hLph;DyQMu3kF5E=Z?*b#iB1SA7SjGms3BL zR|Mjo>z1aM`*^4Q?&`wpU^>WIpkz?_S#gm=4D@0Ob}y=vNdUQ0irQA5{0{PS6%o`b ztGWjOd1irUs%YjfqhP3MvJqN$RJ&=d40S-RT*#{X)1R`GNbddf(9+)y=dB1ma)b3RZ;4AjywOTs8|6nGDSuzB_8M zSM6^Q2AuA+B6UrXb-;@edc)M7ODw3{MX9rdIg}|z*cFVJ^+&dtt4$ex2@5((4O^o# zaGXl-&kz951)b3AH7<=e@QA-j1)E&G9IV6Mgt?;L-Ak#Zbl9p<$PQy-upptRi4LMA zn7fQVOAoq;W4b4~!0f+Qg=q4p^&3&XC%(9n)wJIfbG6jneWX3!s&~CIqi@9$c=|I! zUxKU1uK(7up`cNqzwZ!(0$n4rX0NgHn6};n+9ioBT0Ps|^o8AaAcflfDo?0#SN-6$ z4SlmPjyk3ZB-;$K-6;lP?&1phRz})E_yr<0K=W8Dzyw8L9%G9qmtI3-3W&pu${*cX4 zu$Uu@KU@xVc>orN3@3?`G{NE;AMa}A@XR2Y1ILQbk7Tq3ZOChkgT_rWe9zgOC3nwv zZG*O_vNEjhe$$<~#oHt?o$G(v9QJ|6_y-~gh7)f-8+Zoe+Fx75p`Qkn5d|(bA-Ne} z8x|R+!bt?tBEXnz`xFlYJs8UgIyxYVfQ$-6J2l-`*`7yTA0YHZW{e;jQP)F~J}~0~ zYk#^R7x-;|csPC2W{Tvh#WwmuE87^dRL=IloLL2?2i)mb_;a0ed{cm(P;2)urG4{Y8Z9VrU3Osv98Amap*OYzCjGwIS@xj3=D3TG9!WqCzX6 zpO{O!Vi#lQG2ULI>cETIV)~0cV9+&PJPGMMw09VIR4+r`w_13ZCTZesFJCQjI95Xo zm91w1La#CkHdg|k{ke#N`bPzv0#P+^*O3}QJM}_Q9udUIwZugst;tOR{uvZif&2MK z9n&oq(8-?6?QrKX9$%z2^zgL6+-1^R;%Yo~ovX}kR{=~Xbd66==LR!Y-^6a%g`VM? zUvH=nq7M23N%zV)c%bX52xH>QL}2Zk=$H?OX@<%05#i%5bJPWtIt+ejqb&Rb*toaU zySL31K71C0f`Dny5v7tF-%EoKQ!vCu(H_jUTLLz|xu9x#eV8cTG#c84{<@hWb>C~@ z)uE^Al9Rg61GmW?M_Jgu=m4)~fY*rl)Wz+vKknokJpE*!DNC*1d+MdBbdr+Tf`1 z?VI&nVF#gu&Hl;RS>dgmPEaL(tpfT|EN+tM91Z_zS5jZu9RP_Ft8RR5Bq8f1lhDP4 ztfZZC_~!;~bTpQ+9pv2}1N4{{e;3b0EE#Y_j<&{~^Os55pJv^`w_cSky+y}Z2Q@&C z-O>8>8O#A(3~4IZ-u#y!F}okarGxXn>krr&ftk^_1T10&kQ@{9tThyPi8mgliZKm(^s+CSIm7F$E`SuqH3` zUf06c{iR0rh`21T44v&w(V**Adeh@0!}MLx>$t#kJ=FE;PV9B^PFbrizA41L#ZlsX z3hfGgRtJHnsYpYoimSKi%*sROJCehKw)2_KGGPQ;9~)i~h~> zWvVf;OC(%Uq?h>_Vf2z70Xmd?reLu-k~1n{&=epH;54kht6)vS9CIQ#_6*Ygos&Z)9#t5$DjmqWsn7b329LH zG+Yqr-@hwxZ`S@FXRX?Q5g;vNf99~2&mDEu!x=G1G8RC0n)RgsD%EzZsV9zY3363J zc)n82qFE7)y$Qy>_VhMWuU;jSLX`9flboEQG0mPtENi$ZVSp;+3mE%3kXIBYCJ2N& z=A2WNTjZaepEmAlTsB{xQGvcU(3BEBW0Z&R8~FCreRpjhozSTJuoDt`B0F+oPnvJ@ zEQOB=FmJH@c1TM0b7@4+Qqj)u;~~m@kV@?A`IRjzdg776P`H))ZQRmvMtLLtBxMZ~ zYMVAQt(Y;5J*x{s#sbBBC9X$c`z)y}1177sLJuLX1Zu^{tR!zcA%7gLnO)gOSDpC+ z`Oz@`2aJqy7(Vgd9c!$h0THFsFvcX#!8{&IMzkb(X|3P=9%P^Ob4ga?$At@v9!Xv3 z)1F2CKX@D7Djx~P5PZCEi75#(K00jlJ;9NHsI;V4W0^nKT5|}_XAM`)x_%awYa9tH zJjcA>&3f$1F^Pr>%w1(J-E1sHhIjf1J0&~>V4MRs|hu<}_mIv)4Q}~KM=y|Y9`j-p*;JsJq;aTLdJ^X&! zk4%7kQbzACft^WA4le6~Zc%1Dxu5)F);R22_lre}K197(5+de&xbVFoLapIhQ);>z zHOYOJC#0O;&;=cXWI6pLE2Y+i?ne>Q1DJU8`o?6od`DA{sd4}VqV_L!$n-4(c-n1V zIlCDqSX$Dk{XpOK(zx1@Y?}H+L;&YJE24R-TR-0gW2oHVJl?+J-tJO@5prUa@D%TL zV)Y{;O^U-{bg_h8xsjoTU%D=~Eb0*X(#J$`Qdz*&gaO}yRXLoxpTwe}$9q9$Cbn&=KI#H`1S~p@q?3kaj}j zbiv;hvYNZNY+grWf3#TUms8ZLHr$*A6Y)P$z5hN$8*?F=JEEL3F+#6 z;2~swNBK*XT%i^CDMc@b3g1}zXFU&1YBNQqH*E!8{zCL0bqZ{rrIzIjN{<)P_+&*T zN4etpCFz()J;hYB7hUK6ZZAbKdNH(M-B^R~3;3rCoqDCPSn#AaIf@oj~LeZ810e~hCkz-GV#T)nW;Ob)>; z{p$APIOvML^Gz?SB38baZXLl0>!K^Bn&<5bEq~(ud=q8qX`3(2y3^)v&soVeIaj-$ zy{{?k>dpKP==SRD+Q3kV(23*nAYTKH0 z*ONZETiEic?}bYTCB`a7e(dLmxt#2`02s@pkP+?je7gD}Y{5E#)O9&d;s;9Uc~7l1 z^Gglly@!R2Uk-VyE8s;4EnzvNtF4d$7SeipK7J-t(0cnlO>M={gj}Bixug-{AQqj; zr1v2mLf2=QV`VY3&Ak@QE>gHb^x(sjxF-pU;3!w(-xG1a7^r;uRyY!Re|XiwzrglY z5k9)=4u4Hue5Y($Bab(0(LexH_Wp!7Audt>teNM2)mwu$HVkwLoy<~yi(voQz&S5; zb3hpIEn(o5p^hG*pBDgX%>j7yI` zw*{AI-3oQR{kkuK3)k2?qXC1@uV;oOEAf`RHCV1pr|yF#dh0PXy0{R6IaT-y1|lH` zVQ$#X2oRK39XzBU{PrZv+>9pl`733Qyzdh}Z$0!K9Apch6!#>Q^Ws^OMtoL`VyAXW zybV?QMHNM)t^-?9zv1+lS<2TGGG!1@63Wl2#ZNR&)(ymZYn%fPI3pvEq%qdG7Z0~&y zU2~i?>!04YPg1{ga~@r$A8A0ML(;L&be1<`CTi-%@Z3?zu@jDPCA~$vuw1f&EuXgF zQ)KMdiS~Up;bNUq4!6eJU}u$e|4}R?Iv(;AGx65_9*YN55AdP&H0}^W&iVIB99Vjw z!58e?+9g@a^8z4BxLu&B3XS&d35nw1L^etTIzvM4mg+4zH9U!DYAV)YVV^$By}_W< zZbBG*7Eg!v0I!~M9wLt+FVK)1pk@BTo+If*qHby{jM(` z8CGoVndz$pRwWpiP}8WQ*&Vot-2PyKRk+x)*XIVB18nlz)w-(uZZ*yb!j4`^MqKGn zJ%{Bl*V75pv}B0_bY)fun;*Um2odB#}sepWqc^_bTbhfWWJ%bb;_TySJ}-ElO>X1gDGoE zavG~9JA)TNAKaI7tbuJh?+>&Z*-ny0e+5Py>P9?Q6iEJ(7YR*ZCF3FRy;Tfu=gmsP ze;uv-E-LwI!>g@m_nQD&);>BP&avJniU@`3cb;NN3TWE*WkxyKc8ZAxlaVvc$R(>O z6At(sszcx9CV#}nL27xGKKf(=&cGSn0 zJRiy{br5NV$jkXzNK+B{D3>)>rLAxA{BvT&Agqd0(w#!C-GE$)Y@`Llv0M9hWW?Fa zWSR7SH?XKdyqogJURZxvaHzJQV5soDG?UG&rpvk=yfA{?>v~-?1P&WO+ z**Dz!ZnRCxK(hnd*t1l&KT{(u4^p1|cT*$r70*itNKx#iML-mRCntWlK_ze}KDXOr z1sn{I1yRj!pk;9ZBQjc^s4V}aCdmShg2#eT5;Q;c_P0Lv3U8`h`KKw?-5xw(eav!* z4$;Mnii#MCzmdij3cS}@xM*#!yT9<6G7;Oxa^MKU4oZ32*9)}+FQhn5Zc!B#jkSuF z(HWL0QcIF7zhrAB9M~)+O?tHVfJEli1?1_rCKgFi(G9UzKKnjC(mi4!4~vzHhUF5$ zXx~wOWdT^nc(z~KF%cqZ8mj2@3`nqA@1;dIJ6x|oHMPt{Y3HHx$ETgW+S?CHQ0?;B zzj9U3qVf{VBr(CMdIm{HLYzl)i(cQ>PW%XFqeS5`?@sUBvum{?+)+@3(9IWExDcAX znSr7it4&F>OW_BQ6!Ruw5a?4ZRk#(tF4H-gQyV7ckjC-%4IuphQB!q=D0mj{m0 zua<_61O&|5wGLxXdSaMw`xy2_&#t60?io{AFck+Bn-qQG_z`KZq@JWk47lg?wOrAv zez*Ot@IN|iPFjB{l;24a6Zi)kWUTlWjn@9zKEtc0NlTtB{1qelEp4)YZKjp`m4!$C z^}WxxDtG}H>N^$~7`Yh{kS^?!coNa(+!Z}tEsZaXni|YXOkx^G^bO>DXhX%DpAc7G zOB?>GF)ro$sp*uPwUcIx_}+Hc%H*?A0wnz1@H9CNXr~VlF1NHEYx%K}nBf_@9c>j= zTCLr-iiXtDNlIi4K#34`hKVu#Zczn!sHqP3=P6(P@2`a3QPV;ZeQ;qe%#jS_vy(fT zf{e|LBN!W>XDB~K%rs0i;;amntdRT^sJ3bY$$cCBk`3EMIAHAh(O7QDV;+YPm#B(c z@O0u{2GED~^hh&IKY#rxFfIN=U*FyU{u*LyqC{n4g6kU`avEco5r_G#vBF&+_Td{3 zVd1W3umH*%CEa3{h$IiBD5!}1g{&8eL4hWZtfX}B6v;gdwOR_8Pbs7~iTD_C8@GPQ zFy1gr+r0!iGUNgxWyvZqL8o2vH=#mDRKjT1UQVntkaxDhI|+n>bH5A~eJOfEV6?9G zRZ9y~o0TKiRWO<=^`mA{kb=$%k%|JiC3!PI>ZY=Y z*#4B5fzFZzrGsT3^|ZOh++{@nNw2j1U9YHQ|C@RxdYH}_!DlGaU@N)q-LU9lm(Poj zW?ATlVx1=-G)U{}Mg>x7+#t=t@KX$GZu^Xh@{Zz(6h;!Re1=Ib(?F2&l;+apssHzmWt_l*so|LmT$D3|#%` zUXQWw1u>c3e`NX?$}?nzxqSaUJzj_rJN|)WE+p!PHEcJ6RYA1BRM6j6^22IZPoLXH zOYJQ`)+9`f&|&Uxyv#smFFD!Vwbu@$YegIjVFgX)0uJ5+|ayJ#ozYz z1zH*v{@piJdbaE+H%j1#Hhg?cejT1SYzG4i)aKt{&MJR=qmh(0YvD2V`ljz)?#&M) zYM-K73zDE>L~mH~{H68P)RqiiMqc;fkEMfyDP(X&>*>De+d;Q2xuq?zg6> z)@pA7L7jJia%d5UZ@~Lz#MMy0{TUW0mU9{JSc$)Ag7m+aXMY6bpBz#kQL_da=OCgL;xq|T~(rc$xP zwlv&P7h;Jg#pr^rg0WRH^1?ezL9Q7=q4a!y{%O!mbi*$X4H7?Ir~W?0n%Hj=&e~>B z<^rK+p#!EyMtFTL)N#sv8_G1cIr|CmGYiVAxRSnS&d7e-c7>Xgok#FXT|>(Lcn}X=2rGi8N0V^o5qh+ zds>+v`ibnq%XeTtS+t(Llgbz!6=Ay9{-#SPwSSPwhJT->a>j$M zbIP4UAUeI%C@gnGZCt8ec)o}%11Q~jKQrahMU?SeVA+>v8L{~WOZ0o9o;0_Wmc&~x zsMYjfU=a6xu`2U1#-Fk#v_8VHXjzB?xW1nUua{lyKXytj1LCt!ABP}e19KHt2%anN zX0xF-egYp>c6XsVm+43B90X$rCU^Ji8%18*z8J;Yc|~}wnMW^2{QD78UoTgc1$ae#t&wjB5Y(VEqQ8crmrCK z$ZIYl&)4>HFs$0Im{JyTL$|i+T#WC5GDKPI!7sqwW`ZDFwYPYa-zGKH7V{B7sgJC< z%+jRvbt-c(vgnI+ivpG}Q<)@P%+qI~ay z49eGwVn=}E=0zgh?#oT-4@3y>jEVqqy@f*vYk{(>=Hk8XEGQLGcP>*#JUe|qf1Vv^ zreWV80i!+TqLoOqf05z-HA3>P=`uZ>i?1~&fQl{6DH9|PVC(bzPTlDky%{b85JEc zuJi81)W7oXu`e zlAVTO(gY%2#cOmoL9-wFMWE3=HV8rr-pP-5F)3jmL&R48)Bwi+u-YBY62#+T`AOi? zq!aS7>mw2LubKDw`i%I!r$O(VPV;Fol;SvWxQ+#Ix?8Am?vg8Ghsd$W;bNyDOX^aM z%Cfe!1ghlpl*1vsJYT!*K3$hWmETI}FGLD4s|ZJQX^C9&UES3lCZ4pB^JU3vLvb-q zvL`e7AQ|m~Ysz1yqJPO0gm6UPORtPM-;S6TWlKGis4{0cmF&W%Hd0aaBvn}G4W;jU zV~OMZ+(KCWFe!2c@K2z$Sx;%XK83pvN!t$uwr0LvNh^$LW7RR<6#MoheSjOw)Kbt; zj0G5x@kNekJS)2Q7*;DohY3=rlf8Q@b=f_`pFO(WGuTi4{%ry*>%CEK3XAY$#MpE} zQjP>L*Xd*Za$7RmyHM;&rk;T=D(liTj+GMsgGQwH17my?9LPMF$g$;*23@>JhRk|D z5&^k+GTLQd<#%daixs3mR{R0E8IjyIz^Ys#3;(Wk^UA!;!vJTiGq2);phSP)+Xxs> zK~uaLs!;^4yfmttrGy}#zp0ilAEhxXimc1Gzhy?THFm!aNQ*}9tF}>JOyYR;!nS%g z9zJSAc}riXpu>!3-EjYv#xwKJ)Y7=9WOg8PHRZ+C@+uOy3~1QRmG|F4#6{>VMirI& z?)O}iD(!TDixe<9?@nj$!04F-##tYtO?y^&P{f-hkY|!O)uLk}oxA@ZN|(U@?@E^{ z{cD+G&O?@fSqUCe$w(-F+qdRa5T$w6b?sG&C0Lq?wm`hSrGSNkdB|;I8*E$bZi`&C z*wxv(slZKubwt{a6B6 z-;{z$1j{I12EM>OsgE)T08H8uab(GqcGP1`qobTQJt=-0 zDme}f!;(*92Vc+v9r%4~`Q2q=AzSk^6%xI%>a7fXLxs1S=U)2TDrFUDW@&~oeHt!! zS5r+^t8b#T@AgPS1aJcIlIlORXD2=`eNK_CW;_&*Elv8w7wKjqr`mvN%zV$z@EF|G zfNJjXYhNUokF)xEDQ(XeIn!9SxW~$!&j1&#pn!_eh;00E2JQz}r^aGKHJpReccCu z*qwoQcWM(|U-4iKe2q*Jql}cwN}6>r%A7WQFa0(i9VGWG_E}g?)!6X8i(6C?%DAWs zC_db^wB4nu_IYpZMEBC7Y7T)zvtcxbg(z6HSIC$pS|e6JSMYwxN;+3dgyhFSmCZvN zA)Es6fC-m@vbEoz`)bEkuJ+XqJN0Ql9r0nw-cM^D81t>3n#NMTPp`3PR0&UZNxG9= zW0w(tbG51V?laR2I-;+3)suAX4>XbS-p5Meq68;M6S^c-2EYewDFwDj0y8AFiij0G zki)1H>etXQjOX{csId~OO>Z|A>lc&iY`(zlmhzKkl*1{=XJbbDxd_LPg4|S&K%SF- ziyz63X5CYt5%p!*l>z~6p65+X?$ElC$=s4nRa^G!SSs`zJGJ!2LL~X$$A%;Rmo!MW zP;$Y4sX=u3|Hm{)bR-0ejGNe(FX_&r%J$qiTkk`E`C5~zknMFvUqF4>37`R6t_D9* zq0+es_Ryd34pwG|z8Mo?oCDV3`Q3n-R-t{l(l<`0S8IOYh1_m-0YwqrLz|~SZ-J!q z8&uI57b{^7VMPn^`;#&FmWC=S} z2Cy&giC=Z4{{51&9m4^~+)2X>_8VJNBN7U6;qPH=P!c^Y=j{BT^ul~o>(g64d6-i) zf7?FWs)J1^pO1+8+?Z_Q$m8YE*vl6J&;3*i^1s}wh{qb-_3;_nC;rk$NGn4JWh@(; z5k%T9yz!g^*+?PiL#7y+Pj!B0kk!dnQNEoT!rA4<Q~A|b1{uL-!m0^YvY0Nk)uxpaY3o<1Nc~yb z6O6gMUuSwyJsZn1c0hUPoSU0`66*2*sW14s7r~BfJ&fP)BNN#XnXh{|!?D zy(@W3RI}mWnfU-CnW~&WOS3fcONL`1ah+DH#Kq6KPFK}FhCix(-0ug}-TuWIdpCZs zq@xN?J+xVp(60E7L98i}Px5fLAso97h-;AHhXIXzSv zVn`5d%mN1?Qg62~zy~!$4?WAyal+KQRfR3NK39NkC}r>LTP3ER=T2>J&XvF4OT*Tl z_Um=%SqdP|9%8E~QU=o^gxkOKBI{4KdU}n|RqRKQW2K%E=!o^+_@4VmoqyeFJ4VmQ zzbf~!p3dvCY^0w2Pdh}PIM*hx2LZsEuuFw&Vq@)_BXaLE+ruva7#9iTeR2E{sQ00{ zd@wj?a8>UU|Eu1IW(27B0qRCKqJT0W_e=7Bw1}Sny+!nbhzHnh`!Ij{jC@N=@@j!y zOQinG@AzE;^!E>tf6;YUw9g+q2U5v#LZth64Pc0a5&AY($>PNiH~-ocIzkmo?6rQy z*=3e>o{F%8b4sE}rbXixCS{j#H)M?pN3Cpc|0L})-@d*PoYxQdc4xB#t*TiIsxR3n zL&72B08&`d)F*fALI)d^OGD17;sz z#v%E$52#-zkvoADOdqD=2y}>!Ty==*ZvE~MJq0>M+dLTz*~@Ktv(cKxDwA$)rw`68 zVoHnbPA>%G*%U*dBI&jMi7NQqG+e@3MALzi*{J2M3NEHV zrdbesxAKDz%tV6*U_a4kbH|ilVXB!vpne6jHaVP}eDTF3W34{^J_w7~gA}MDRW!)$ zH}<%gn~my^uC&ZR%22X*nCEDyP!F~doN&IX9NIV>SdY%YE*?{0=%PcQ<#iKQ8)%WV-|_wjiA*CHUzH;%9mM;QJA3P@aJ1)m1Nys-wBd%q z&D15hiPBHj0Ps5WzV$`@NBNM|Rr%0UsbA$o9i4U!Xa}1>`Ot<(&4J0|%J1L*iAJ{g zNBPj4aSxG5Xovk)Lfy`gDBk9)CF%|=oJH{3#mllGqk0dHYy!&?(_XqK6i^h#?so3A(?5_uz^zk?p^KMg`kLr!>Uq z=35*%_)~BuzCc;>LcUl^H}jc$O2u9~E8hwpqZ#2R>5Cv6CMqK?+BU#Pyd+3OQzf09 z?#0EWVGmiiKUHKPe=^{UplqK`h!X~&GLgv zW_nE}BXGW^k}bwJs}xH)HF&;dDCQxa-F*I|p7a!`CvDXq==A!7N*1#0&3eazRDV}=kBUmjz51hPnm6FaTRHJwT!}et4l?HpitK z#3e(LH>OZwd1tD(PapeQ)isHfr^*H8{A?e!{?$I}9`PvGR1-yy|9B+l+z;z($$P4a zZxAq+wwdD8cq$p#*|gPy!cMhbbOnY>2(jLi@1|#vfDyjekic z@F?eYm_=PYGI9OYP8xIFPRcDzz#&skR=l;^m}UW`2w&di1?VA+dUpNyOAeKvkA?QT z7D)0x^BkdN%n^sCpIS=_=wPbbBQsw>rU019ySnGjehgV5!!leK(dECg09DSl%~U_y z+g_SrcPOSj`B_F5a*2(W^32pmsDXf1TzH++}ey!R+uFYPNWq;K``o zxs}H5i<8TH)Wx?v$3M|nxfE&YMu!B*8fHM&q!A>&Eq}df<-zFK;K)ILvfC_Yff(lfwB~6M;i3B6>n5lMWYr|FmZp`7L)2)^Rn`z`ur?Lb^rbe96 zs`w?>^Y+9%p^O;Yv%P=^$*X4wPZdIXIo~D7N7j=Z%rKTsXF`Z%UVVJzIoYR z!8N|P)_b^HL)cvsB=YS$`Dw7QSF!h1P3dPTb`O?c^y13(m2RV)8*k5G2A!Lq2}R>( zrU|F6CsG4*y7(;hn9oS^aLbm%!hs^vZJ>yBU%v`8E2EtIto&TtJ(k)I5j27Rz4SrT z_%YBFeGNH_EI7~Fv-^tgWmr}KW3gQRVv;rPZzY4?=e8_%k|Icwaa~x-Y6AamTY<6I z(V+6XuvGgu$n28U+}7-zJXzWkf_FG~yJHcC)4?pukOjUP7!88W?mPDs$=leMwd&sb zNjK2_gKog^I5UPH}}JXx&%Lp4LbE4_EM+&S@2MvdS|b`Gh# z^qNYN=XAUrPx58)dM2cpWgeJj(wnugKbxHVSVkHFEKnHOR61&DKYWEab5=F6gsP8y z8*>19O{YSx!N|!#NX&PCTdQ&JsI!U1Dye$JZADC=>CM^|6JbUU_0E$7tXwQ#0V`6M zGg5Fqg&z!tM-Fi1#Bx-Za7)949e#k3J?!(&x#@DAdOcP|ume!wDw1*~#@U4Wrnnjr z5bt@{OxHb0qV5&TUSw|1al&R~(BMK*Lef4FImI>AA;v2?$2Pcfow#dtcR}eOSQ0C@ z>$G(uhQ*${9Op~@By`$yvhLP08`jjf__rO;48|EyIX^uKRtHW^#)nD(C81|ArhnY_ z4pBAG$yBJ4`BU*ElYQG|3QBRhqMV7AY4-EE#ek}}h!0hNQqDq!3jZU$Gq<`BYYGKP zO|_B#jh&oxSu$07Q?*Lm{Y?Ifln~V_hGKAdy8bf)jd=d!s*qd{$#~d~gd2XTSMXzm3 zOS$vikK{X4^RN=dP~fXY79|#k_wJsHV~dyO{DLL~-~Ih7NB+IB>SU{D zq$=~}6?;>ivs=v{V;L5~c8D|^P95t70HFgtt1UnYfg&K>vY|2C#oZUO={Q~_o z(jYVYc<+{<^D3Q4={SbwqjH^zF0M9JdL<^_hqmhz2JdsTO$AF&6%L%q<8cj`L}(2F z+s4#^i&Vd2wN^qC9F#YRs*2o2_d+;HAbK8<2EFTQEj-sPL%Q^kkFFDHbV$okI|T$Y zX;wGuht8%LXnMXWy5=Q|n#C(I?=-FccC{e+0vIkBVK5>xWhs&bZ)AZ>H^K@!?8~5#3e!rj zn7TJ25|r{F-XBYgUK9YXoeT8Ok50GX0V{p7%cSdOQ?<_j|4JXEmTFZnGE>$J6qLET z?z>Ka(qUi?O-?iXK~Olb zGXsiIbs2V)287XHN5-q5N1>+iQ?^w@>gF9p)%ekUXSnZNiz|TUnDYy$(6@pTxv4#k z46pL4;m>>x05AaR@-_*M9{%?tSnucdMVGWtkr(5pJ4`Z+Iu}X~Nwb+pIRL-;I}1nt zru@_X{>6}Ah-V@6TN>%tPmq?+`nJTpK#eR~b|6Q*sQms`o}}_zIT})+#kbvopcH3*bH)B2T!yri_L5zW zt$9bOipIpTm}?w!4b~&R*1dY$xHqz0Vp6DB;yn3IgEQi4@hDvs=giJDSiovHzpsE7 z5%jjTLsX;cg5&jxNBc4}na;6=={OgJ1(ov&w?br(ZkVXw`fQ^L$9S(%RhUi{X^72` z6GU18MWk!s)U|Zh_T%I4DH7u6OHKUVyVNfGJDRO$M}~{t&JMHSk{=?R`8TWCxy-YM z{7EG_8Tvt8{~w*K&H$_d=2?+r{I^b4gMaE|-J3|B{@ux%Qgq^;L8Nvr_F_G;L+J__ zo~8?6x_^+@rekN>f$CAFBKJkioANSHBAh+{;xTJyrgx-IU z*jxb;n?9w4Jga24e|Yb``#lFtgH`rzZe{z6!x`A4Q`000W^Y(uP-t~#N6A>kXvQE= zv^^EIZ*J}gPWee86Qb1E(Md#JH6efp0K+&5UW$(kj=3B!1SQe>6dFyj8OkC)kbgx%A1Ggg~;3u z(YTLeCcXer=UL1*xT;cclLls$LDG$l!u?tI2kxUG0Wb4y5A$Yy*>8pG^yBTj@#~N! zafeF!4U#m!`yaeE&L6zCi-e!N zw&14dsw-X_)7N{mGNu_7UpAAh-Ua@+?@>TL?!uR`eU#dxwoVSWxEnP$Cm70Psp9p* z%Rkx*rpT$}t+`3harBKu-TS460uICe)I$0f7o*y}58>ays30X7-NGU&F=oE|rWoUs zN02qQbraQ{bFqrZu&j@S1?pNh`u6^~2CNX9Ta*ZI<;Tq;C{9=rW_@pDI+P;Kt}$tG zQ_v+BdS3IJ?XLLE-K7Orvf+bYCTP@~6B&(;R^*%UVg<25K1I#*8TkCuOvIqP;t>92 z-E&{9(GAl-a1D*uxCWZD9Nep`DS)v708r)d+VBAPNlZGX%^q#ctIHL3_@)teik1gW zsWoTs{z(Y0iYJpI?47pvl+eXVTI}wP^nY;N+_l+@jZfut1f8uuHOk#5X54gel{_CF zprrvR+1DJI!-V9wREB)*QS(qKrRn^on<)(YbRPxOrlC!9P@LwrtY%4AaW0P-EouN$ zO=Lrd%fhGp?J_rzh>L(b01N+@uoJ+_z;hG7#0PaGUCwt|NRr zjn$W}xrz0M$&=C6^ys+@c_|l|&ZwW9viF7eG#%t@sm%(ZB`RI1%;dE0kVSgoCa9Wd z%TV5{Y9A?e0sK-~QsJS-sqEt7V&#sr(odi>!B3!b-dRa|`5!vy+*U0fM;xIG>9&SH z^}tr?s()%FszTc9qp)Ye`v%|zbL+!$`i(Zs&>lLPP~B*CnXIc@ZC=)(&ow$_@Yc~QJ5r>FFk=6bzwgFs_RUZO*g*K&s3 z9ESp>IB^!C1Kd%s!I3*47<`zJ(JvI$5y?+oN@{@6i!}fEGiiNK;d*AK1PfwLPs-F} z@XK^q3!JUMd7Ex90u7V{^)y`eW~@OBpWA#4=f4YCw; z!uF!O-yxXVfy)5Rf$E=V4jmyk!cxTlX7=m`Tyvi1lC7C4E*~u7_Jpncx6B@bv#PsS zrw{X)fZ{y<4|GFU+8V(%x*?#3S#Qb8geKFqj;QIh))}MEkfMH-^L|)ad!+`hS?u|x zzO^~yK;QV{UeEF-PXM|WXN2%D9XJRNd88MPrU-!IQUFlgWa7(itIr$5Wj}!qf_Q4V z8^jW6uQMVGSYqSVg+!X!xRJRvwouDoC_3t7R};+9+r5*ibK9EqZ~kG_Mh~I zDzRIZE`SSgy7_>O1Jg0;dD)b+*njS-$*P(jlCp&`L7{mq{KsaEVUG?9);%FjzL_l z7i_*i|HYAWs+&Jh`zz?-iFG+G9eim^nC7b_U<;d-N`?i((}=;muid@jM5ll{tAmB%iLLYWr5BFQg5>ya)`OOTjyc6rfaU$pHKkOY|< zkcnH%>Ah8}j>`0+OK&O!;5-l2VYr3B+q(lK9X#)H!5Jk||LP2pY5v6-I(n62uQOjA zFEL_5LpMm8IO#e6dT|}NErd^2%e*&@iV&msf!OWAJ?w<%0noGTe*t@JdBXJJdb}_-M^}#+!?aMnl^=NH5Xl&&@OaB#i$80kH$S6}Yyw zjfda^*;8iE$~oxH?z!ukexeQ^nRX$>$9M?Lrc_@lJqXF0I$53&ap9jh@D-D$Y!{2g zt-TBOT9m&gU0}7C>|RQ|**A(5mszyUSBSOPK9P)VJm{I!L(~Q69i>7Gp5j!W<{G0) ziN>l);r`Dl#}!^VE1TI;K8>|9;px(wfN}{maxX>J(aCXwtzuPD&hok#&S1 z7q|?n>(s{5&e;@=e{5K!|H93nDis&65m#W6W6B{^`4eP!(<{W3kr$xXEuLijpw|)j zN?c>@%Ft+lL5Xc{^XN{6nmn-GJAAI|OAfI?;cHt;+Dzv>&A$sLp_IuU0?ZPR%5qB* zMneDi43eC#hWB^<{!?Q~l5ODJctEM?c)M?;CvI!D3SivzbVqHkv}EbzwRHdlL>O%tmGet+_+8wIMCxqtJjjnQ_d zOeEEsutxco!wmSZ4)xC^_>6#6FM+TpjWRnok@8n#squ!lfRNr7S{g#v2|=>3B|SeZ zL?vCncnAY13wpzEI|x`XPnu*P-3N_6L$uj{6duGImgMwKDU0QiagW z)b&8&oDD!)OZ-AwyJ~HJtI!<)#tQ>U+E0v(On?)gbr{~=VIO>6VK~zopuR7D7hY*q zN1U>SrDmQ7f_;G-V6`I)G+bEz&26$_p{I@;Nm+V|;1lR&J7PE4$*NUMB(NCtVq~14 zW-cfLd+N^XNSw;?-ujdRFz+M$kd51T>Ww16K&iJT)n;`{TgwKKhRnE~q;6-}$A)bA zU~ku?aCQ5(>8*L8{#7w_R?CoiyF_wp0ug|PXQ_y(&DJVr-)U7gIhv!7mR9=K#)03* zXnU$pzG6=z+uSoaU@K8LalROft&f=5MNw zFU?GDJi@1+bggCotO|$El~Q zO(vGbbA(}Ep04wPg8K@CZdyJSLuYo!$!Yj_M=-KsqrFvR zi05?BrkUu5RLOFXYr{R}us)I}_r=Wbgir907r%^Q>C^cRF>9;uJq_}k@x6*M%V=RJrk8Uh5%~hXvcjS&I&V$_|&kfgS73W_M=WbbGGMbti(2P@Ams z0m$m10{7D&dwH0#80U^w_mM?t=VhEd-M^De7ddT3jnZKEvPePZs*@G7^Hd!hsBMW$ zsy**gx>xP1Fceg75UJic|nrrpQdHCFy#tAFYYO*a7gi$M}{jN3*s#A5iK*@A0yP#Ey z)D(FYLvu$y?!51Zi+7yRgt%$W^iA@y9CfewpjGsmqG1u*Wtd2>71@*J>GgP9!Es}- zEMYy`NUvbL-tS4*UNd>XeKs;s zbAVeZ0sFVJvBIbEstUKBU4t^yaFY<_F;s@hl}mx3jV)tPk!? z9j9**`+%+J`TjR8E5SF-Bb@wahAe62UnHd!4js|C#{+Aww@EjNmAlkB$kQV6Bwwif zC#F^2VC%iqP_M{v+5O&#v&1ud2D+OsBjzj%q-DzO%h?yf+z|62^!!!Xy(!4@`Rd{> z>ar#;@_FBrH~)C;xXJXR9>JhT|yNJs)}sL-UiNx+fA75z4|LK z3zFGN;~UNr>|*}IW3-Iq+{V=dn(NaBKj9aUGxL*&H<5C43N(?ew_i(&kUA%&9 zcD_8X=T9(OyQtzv(mN}4c*1WYUMOQrnaiK=!P1N(qHt^uS2BX8D`_27CS*kaD+BAp zYDnH4=S%EF@8!Ly^X=$=>0dPmp;C6vx4-O|mCy6V0)y2(q_X^wlfZE*okAC%uc6)( z`@LC*)0v6-$JqY>@g@HP@tub(zg_AgF{I6?;=Dic&C=lJeUMUQm6cPUv4@Yq)h%1+ z#5K8{aje*!I;SI2e2AG?H1)wYroC>4emXh|_bAf*<~#V};boF0;y*cA3vg{4l|OVGL7QS7?~SO*)X^g2dkwN6E$3c6 zrT+hA?ll?9Xe zdNCL@y|r{CH0jNxFLhYXT>m#U=Q&mOdJ-7kC~tmlKXB-oizUt}iux{j#~JfCwy%m` zM;CBopVKmW=}vS}z8JIfIm-Bf?fXtn)OxW1VEY>D5oWc2VEgdViU^$l#P&V?f$eKl zIiC!8&=!k^)g6lk6(LT-crsd{Sx%D`D^n!m9}XZK;avv2#U|?TdL0_*-K+KSNmjZ< zVu$uWL&0N)-(fvCI<7c`G%f~t9VQq8z*=)A&X*C9Wlr&vrV`Zd##iw_(bR<%--N=D zZ9%;P8nPl-p4XT~s(IFoSTyOMj^o2kK~`2E^Ol!Vo2DkTiKS=`t#EgcNh z$@^3|%s7m(kS^1V`eV*AD4P(+CfVSV@ZqWIzoF+gLY=$+?=!oyUfEg3OG>}Dj{hOy z&`c&Afan(hn?o|sS>2GgHa%3NIXOLzZre2DG(PM3v7boTx;(XX6-uE5(8bkK<)rRs z4m*{_x2RZAKgJtzrl04cH(m*(=~78-9shD-ynQQ}r9>tJ%yY zT=6Wi6$L5Q;^8f7F0E1YD`|B-+e(SzvF75HJS*5JGq1N#t) z@wo;3w73>Y;ys zqRzPqoFV4|qdPpf>8JhaX=s$GPs0AoaE=|Wup*>Sl zJ?mmEkofGQ9gN7R%J6ix>bI6bkYVIS+TL7H)HrV#0-%GGrH*~%9Zpy8TAD87PlpRG z(~5MB9Cy7?yEqE=lg@xWU$E755lDWn+>R{Ln=_EQ1OwePAf=f8ypeDp2@lLYX3~r- z;uH;<5I}oj^gcX2oACakSm#pc+eNqUZl!L0cy>7tL(-*B)5=x&f>T+X_&*&3{O)ltZj+h#2DwmqJb4gN zw&kP~C)pO31Q890cLdchp^}|C6pkxHhv&1gE#l}|HlEWYM{Eg=0OEr2=@or7SWS9c zPyDI(PvE2sAglrAEl4p5@9Q4US2A4ZHrS~+;@95`hd3dx96!X z-2}OGnfbf~xJ;9E!DtGpALl>07!XVmWA)9~>j;sBaGsx2^gf?h=G6|Q%r3Ik3f-UI zM9gJDEZ$sAlQnv3a%a@ClK)iIf>M=hkCzr_>Jg;PjI=6Y*H$agWQ&ZLo#SGk@(4>0 zO2N4?oUp1?MJb00?o&7`GHSDI#I=NRfh&^C#!2fFlG=U;;Y8zb6*U>m4)>ibr;+iq zEot()LJwV&Wsngl(oNI+ChhFr<2;a?u8_eLhZK`BbKezv-qaps12>lzb?9Q$E(#!@ z`n+)>CF;)+GJMJ=(c9eD$4B01TpBa!LYPZzoI=dRV*pw`&|)pA%3z+h;h-vC0(FJ# z&l028X5x9NgpfNZ8L?2P2>MW+sw6IOY+}9Ag4ew}uqQAsS*c~p z8pd2eTYfTR<;7$IGZEC9gnNpQ5{gpv&)XM+d>}0g$xn1PGd`5xOW9WsaVkA2R)#19 z5K4TXhPu>~6)s`voK@5VWYg!)&0EMCG9*P&R@)A*B+#!;lyk%Zs?ZdnFpwWxvBOA} zbJOmm*|JWYHqHS0t4x~E^Awrg{JJg({5SiOICdR&wHn645b`q&5;5b z{_S+&R+QH6;lQ2sUIoeb|8sWOIL9w~dDu^S`BtL$0h*m*Sq}ED)iVSiYDPSVj-bA{Dn!(357h(!!0=DU@&HW_keWJ+63md=-5Z zBbpR8Uo1!wybq!{ISo5+BNrZN0!&CdlT0EZ!d#j*nEtym{&7bK!!r0wDX(tU1t$|| zs@#q>y7kpf1sFRrqY+L86o#kDJjpf2mAoeS z{ShP3?i8a+r%xKf(L^or62&lf$;$L(#lbv`;%Z0Lv8y&UK7 z`l+;8JLR9&Cf$LsIpPO81KrUty0XQfOc(x9^AjEW9s3;+2*w?prc!F@H=c|!f!B+S z2Qkkm_n1hWm9#DjJEC^II>hev$9NsCQ2$k4?E&&iyinw@=>m$P0FrTw|1g3jjxe)# zr00H>3bgwp6}ahiY+5t+i3t&&-PClsvuiaVeFDpuL8AC(5{Hh}%zNBO>FKecOIXEp zV@Nz_bn_v(3c_=6;$uyEKZ;XFCB+mm8NBLn@D<&h^>MfiiUMWFk@z*;+;vyJn=YDs zK*fht9N~F)V2Oti>qkUC25IZd+E0=FVU)|!Mt%u;yR-AH=jp5wM%EoeYdtx=P7Xig za&Sqs0VnPIyn@N3`CbFah@8i*oBA(*B?3Jbv&(6CG<@>6SX6GejH}!G;3lV(E4{KO zLbi76?VK-szlrBhyp$oqpuu$-{l@E!(-c#Tu8?&H?~9rIw~DirSx;ZuObRsYd`k^; zRz53FU{@N&0Q^kiYhi`q9xtsj${8ev@q}Dmn)SxpY-~7FMK4cj$W?KiI%t{7ZMtlj#Wa;nh-&>DU$jG4_K5Np5jQ z-ep8Ta62u64^DgUenwaBKSc!Z9nxqBE9e~N6A5(^f+^*YMWuW9507-foY2Uq2Ia|C zYrbq5{~27F*!DI~UedC3?&-IC)o|++m>1j#@{^Xo!n=10%L_e(Rh;8--&W)?- zi&+3yagHXF{s@m{Q9ZX^wL1&zl1vROQ3R!Ka`z-3@OQ;ccPUjto&STvDuB@9-`Fx` zOXjR5)6AQZr~&}Ep#TF~8`3HkJF`48X3krao^o#VEWUCeURHkQvcmb{y5$6<&WFG) zH7I0(r!fXPC)@w%`&+@2DobVR0?{z`RAR9F6|sA}XoL9_|Y$ zTr{gb2{|p$*5NUReHk>{YZVCH)l2g{E|K*L$58Hc-Sg+etX}^ zI;MTIaN5!A{l=V=NAFSqen+p`mb-B|TTm{HC%|+kr>7MXTEi>9ddTa;+a?E&hIM~G z7pUN{cu=uYIFJZ=<2B{I_<9kD)jd`{d)Jk>9x@<9lCCa$XWqdsV=nsqP_{CuOkMgu z(GbKeet9NrS;UO+i4F`5CTYtZg0yhRx4xS8XOFKSy>X#{2m*b0_Vo&}K9ePUar7!s z$m=X{;AP65b4#J{Bl#rmly<)X5D4Ux(#0z}Q#I}7zSEuJec^g^y8{_`Eb!HKFBbB7Yszr%55-P0?|i`Sv*iFbz}cF=tme>>v(@^$x&C?1Y(*1q`#6?dV$xS`Xm z#lg9q#qN<3fhup>iu(Eui7`tqm1gTZSI>A>J>a`;HH({}Zq8VPO(4UcRg_e9I859Zo2nj?8a0Nod z5RfIZgk=DMVGFVZWC^S6OF;J3cW~zZ?(g2JTlMNyy?TG0>OLoZ>ibQf?*4q2?ynD< z`>|x}Gb0DfR#Bf0I3q-Pzu!;&rCdcrJ`CK>CC zFIiv+98;h}#YZIZJVtr$n@Yx#zrko^^NyyuIgK46^wZP)eyq4 zsk^MdvxcmmfL!x_WFMH_J23Hcp=YK!%Kqujj`wb_`*(|Z8g2vTkwl7J@V)-20>e|A z!NGguJmn0R;mx$WVQ}SUp@|v$4=3ufF&Nd8YOB6%!~3uMq+(6G)$%$MG%}H})ykav#CN*J%qm|DA} zJ$H~yh>HNuNzB{L_eOqa!U6(x2?y@i<~#y-el~yHhspxNMv*F@?f$?FOR+W6LEwN< zQ7TZ;2E--~Esc=Qa1my4GHp(NS8i<>X}Bl3-gV~J<2Anpbhy=@dDMni=b7oHDN{?X z^bbF=7-JR2TgZhflHt%ouwqQHcLnM+*S6tR+kr!&@?uf4&oDIdcF=x#uut|VWRRoF z_(oPK#M!tY|8L=_0v1k(_KpUtV4q%?3-}y#gxzu0@{xi6_Cxc{<^D3=q0@Tf$dkAq)vsmm7ykl=dLg4@4?g@#$O6hw3TtuD_sg>n5OvQF`RLI;NP(X(#mUS_!Tu zKVmmkH?p4la{|LYK$)L&5cE2W+Q-FHo38o$3#iu9+gDvDYx6DKOcKIZ5-;b^}nMm06u9gXrhe z^Sm>u1NQooiTf?4hza-7G(nb9A7j0uyOkW7dM9_1l1hf*A(naSiIA(WXTix;MFMK4 zh$L)9b%jpKOj5G)B7zf|{^^YRyLFXQHuaB-U%zr!tg$ddra{FRy(&zoxV2pMmqT8i=h?_f zM*F_cN6E%{3luXAG54kgkiy&ljupOdGiBWUu>i*Y^gNZ zt<>|HOEklWOy$FWXz7@anhns_=PioGHIi5fmv$X0*x%YsTsg`v8ITm7@}fMy12Z)# z6vn%yIrn$SoO%9R6g+zN{+3;5xJJ)J78#qO$1`){YU-!o98-C;(H;CT4=ecC*HY+| z&$MXP)l-}0VMJtXR*kg3H}P*1?qwY)mG+58YPwGAeqvc$V~Nk>`kt^vCbBF)wzebV zRt-3T3bBP8q|>gzpo7Tam=+TD@cA>#jAIuY!!>;i7u1GZ_nrBc470`-zp9K?i%CuQ zTiYSn7*HLrPOVMg~S0|j0m!F1Lpf-3o=12EpHdCkE_Oh1Q#1(F-ZfUY(xLqX* z+#tacsRsx0yU*)OWswuFR`o)RsjIhK8I+>=@b1b6UWlJ6wCZsQ`vF!FQvjkl$66<} z)2yJmx{U0c`6{iXy5AyAqV9e4LC>p=5t#SqZpV7 zE92f;J9h zlnU#QZtkM^-uW0hvG=yDqg@H4GnqqjW+7gZ|BlWMDv_Ey zi`jbpm5%Y*W*x=X@k(o-(04-+mXSn!Z6s1<(j#;uO!zs{F65rU_TuiOA681CYOVye zKMu^AUQm4c-jlTJF4y$FCrULYIL_Ln!O@qt$z!JgF;A-;JorlX>Ak$}M&kDJ@mZv+ z>`$yDMacEWehac3^LY$JAG3r{{gE_IQdY;Fq$8|rDrfjO(`MrB3NFoEXJNA=KQSQ4 z-6a=4WWg$K+;AP_itbBQ#&3si3{|Bx&cA#-PxfhjFtAWPa^^EGid?xy~x3K!mwL2xyG7pEq4 z=1hxzg^H-y!0~baprFXzZEwBO(R8||B^ozq>uOPYl~*)%da6IdGr)h(^Q|IF=5yn% z7uk4mrlHU0G?SdC7D@uG>#Mvki_r@&r+&sqVnj-^s8A=ZW9j zg_NgFAlUYsr_}o-(J65uCh*{aJfFs$$6)Ui~TOHr-2U_x|a9IU6!fxGSz2>m-sfiG!ck7N1?00`$Neb&Q?o!po z>*dz1XKGN9`Ueh^)utZxu^4A_NxGj! z$OjJt492bTMET*`IDKWJ4SD4yo8!wsn1RUR(?*^q##(}d6O1{PlTPo=x|t!JpsALp zu&8204(~CcysW|BaMnk0sDTCri8;cQnkBezr0v;%n_2MQeP;C8UF1geVBq_=S0Wsl z$h?skB;_-ZBJnKE9k*wd4f|JS@4D>V&B$-+yBvqb4z=%e&2k&`l3`dhPDO%SHX(nCC(}#aqR=6(Tvg)qf>+0GkR?z|K zGeP4(wgdj58n!*3{H_yQ!)@*Mx+X~w`Nr@&kSi@uGA1yOeWDvG3Uugblx#jJi3$<7 zywHj_DNdr#TbNJ*J^YfPgv`c%&}NcJVP?&d$aYZpmH2}($t01tHmrF)SK8YcTyscz zQ!~9=Wsp5@@fbrVTbo!hw08R~SNC>;TMNLYUuX&m>i!?l)aG3`ovA5J+Tiq&DNzkm zTbRAA{9{KV^~<1}>Q}dqr}vk{5!8IS+fiPHyP|` z)`ZZ-ovjsFlVc$*BPWUv-e~#^2T7hW`7+%qOamKQgzPTXDg&EdOpi%R$sPxOd*sN7 zVCNUC0>s!&Oa#pZhx|;r_m2ea$UyF`#{Emfx zN%B$7;;#Ej&$*_4uXwC@VX=7iM|1&V9(pws0dap<_MXGK64AB%6EGG4xELLZEc&C{ zS+8&U{67(C^}Bx}k{x$p?RNpQT17YRncBMl$5rd670P-tWo!-``8|GgdeBwoPb8Wr z|A|D9gBIt_TB8l+`fkk~MW@(>fc9MT!Tran%EVVwls9j`XgVau===@OGpC5efoS;g z4o#;bd$3gwQ*AsFBmir?jqB`K3FKH^d70*uW049H7Vc&UDG%(Tc%%Z z2D^|)dmx|c!8>9}ICZQ|vOs-nC2vXS#hlhHr>->t*OHGhuaZRTG&w5@uDJ~b^KlW2 zp_?zSf8RS?#WxnOeiXF-OjVz&{H1GDS3_|9wklv(d0-G|pS5!D*;nIB3GH_D*=}N{ zOGJTB#9^2p2z906 zngYj;N4^?SzWuGb9&}^5bTBAiPEkqytU?J8THK3+cJ8j7o0OdR|8dD6&vTxIP@$8` ztRV!kFbsyD(P6Y%J|5Yws3sjdd+@2tv=;{{Je>$3R9(XnSgYYgFGy^jb%Ieq!0n^3 z>jDc$jtDrI8vkVde?(5|vJQtG%2Gt-_Co@Scit^fEMngm93`d_$F{|Q1M#^q4h;{Y z{at@?v+3r{i*~`Yi?87-Qp4fb{h<+N@8xW2))2#5_+UN(I1!J3+J}-tSKH+sa@8nxCf|Bu!6rAUiKwF|M$A(L_peCZ%G0!g0d z2JmJlTi#i`>hWr8hvb53ioFz5c?s_0%Jc;?#t-{rwt4qDx4qN2v=WUi^*UH3We{cZ zJ+NhxIrluE4Itf8IsxOwV3Do&d`mMt6j9=RY1Kke=Dzd8#H43D-!ciYoQh~d8p>yE z6|?NnqSJsJviE3W`=3`Raa^yBtm(64N#*s zPF5j%wxSvyP>7zI=XIVV{u7dd{)8l@wEh@&b)P7~$uEQtc8Ve9=F=rNZL%z5PfR97 z4}ah)*}Ls2*B_mLusH+unm3h!rR&0ty(GtiEr;rMITZ^T0?br~_-VD=v10g?C1E!_ zOZ@>;t|f%Bl>ciV*t}Mi`PSrVxsx`^wTGse zNmKZ@Vrhi|mC;sv%gwiea6)9{%2;GZXWk^3KT+w@B|e|0^|t2C_X_Gjw;7FyJJ%r8 zZFQYA#ZxVfZndKf9}h*<2`BQrs;t}NQ)NCHna{fS6Bw)ncDpLeMr*>ku;+4Yy|umI zH+eCAyE-Ez*9l(|Ke_Rq^1fflO6%GHt|^X%E3oEAfdM0e}(r>mHnJ4oKbE=NYHl%byO9P?XL z=XP?4L~?=#uEDZ?v>t`gaAz5SC#Mb6IaaQ*l)1e0Cc5%ma$KWMmSQ^CYjp%%qq5{( z41qw)K{Q+D-G}m*#6Z$25dk;Fs&PLGTHW;GM9yV>SEX-5&HyBIM0)}j((S>Fs-KOs zlai&AEJJofbnGM}Obc`u5j9|(7M%*GDt;b~yZ@iL3CCQ_o=NDvZ6cqZKT*c~L>?#2 zm$y?ipTU)1+uN-XSlRcD<#=^SZUYpC294v6NfvImuFVRS_0i0XfFc2G?7!+t%%wD@lDs0rvlGkf<+QLHrI1BX3Ww^0G z;|GgH0&3eEYKeL89!vay`9`O>-;K3C^Y#w{y_%NT z^s=6}#l0!+4=NtC%d`~>jnyXp5lKv2xa_ibUiv{oqeD#ir@wPse4}sb_cs9%$9j+A zri>?7ntAPFB$ASC?$i5G$>Kr6(c!(7O0_e<&K85AUKa#=#$>Hr_b*(hAXR8+5sNfRiaHFm z6#rK7xV(f_^Q3G3Zl4q#L)}2z&QrB|RhXzADLU5h9mJT)-S8~_3LbGG2r$ncc*NFo z7-p_E(G=eTUW6SyUoISx5O+<+lP1dU2YYI`>uAFuuBtd7E$Lq(|N;Dk}#b_w-iuhEK7$Q=z zIHOXCHtwMaNT{D*6-r4z-nWjpV4--$VQ1Y)1Bb015qN&%vava#s>*&^mCZ5DOsQM! z{Z%714fEk9Um0I~Z)P`v$p?nUmFDSdRLo4DrX`!fP|G0Hiwc-_&kha_PEjS*vFt&y zvbl3p%#e>-F}t~^`=ZHb3$D+L1}P-?c18|ni1$TvJaOtnBeA7Z|ACYmmJBifE1=}C zbF&Z5p!E(A1^n-^)rHtAWnrnCiFSlK40?{_C8Iz~pjw^UU2)GGYjd8qp*V;?u}g7Z zkA|w-D3D-O*44!vZQDL?!^oEni`O5bC^rfbD!qL37V-&s+ZZd!{cAUd$3rMEPUV5dKDmThbIx`Hee_}hQCJgb7EC}K-Q z!URCCxG7F2yY;t^X+4bncOnWZlG$!^c2RZ+yvwdGM&MdsB`r+*Q}+hl#UE;Y&m>_T zhTO58uJYu3dFZY~_Q4Z>cB$EfZ!ce$bCSDmA#5|_#t+T=WZAZi7|3wDoM@)-lb zA_EI4?f9w<5nCVGec?oqS9F-kW4ytle6 z6es)z9DebL_*X~La>ozq-yBgyemIc!RXwDuN=fHG>FLP7!qh*C|G56eSpQ!957mFQ z^^d2k^|pt!43Kh*rhMe h`hERZ%gf&$NFTh3I|`$|TRgmDdJ}4#Z}fB6e*wEb&Pf0O literal 0 HcmV?d00001 diff --git a/src/client/ui.rs b/src/client/ui.rs index bb5b36d..c29f86a 100644 --- a/src/client/ui.rs +++ b/src/client/ui.rs @@ -479,7 +479,7 @@ impl UI { fn enter_alternate_screen(&mut self) -> Result<()> { if !self.alternate_screen { enable_raw_mode()?; - execute!(stdout(), EnterAlternateScreen, DisableLineWrap)?; + execute!(stdout(), EnterAlternateScreen, DisableLineWrap,)?; self.alternate_screen = true; } Ok(()) From 3f6edc7662c4e69247ed1fa36fc1660d89663452 Mon Sep 17 00:00:00 2001 From: John Doty Date: Sat, 17 Aug 2024 08:30:41 -0700 Subject: [PATCH 25/25] Hubris --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index c062243..94a4337 100644 --- a/README.md +++ b/README.md @@ -14,7 +14,7 @@ doty@my.laptop$ fwd some.server `fwd` will connect to `some.server` via ssh, and then show you a screen listing all of the ports that the server is listening on locally. -A terminal displaying a list of ports and descriptions. Some are dimmed and one is highlighted. +A terminal displaying a list of ports and descriptions. Some are dimmed and one is highlighted. Use the up and down arrow keys (or `j`/`k`) to select the port you're interested in and press `e` to toggle forwarding of that port. Now, connections to that port locally will be forwarded to the remote server.