diff --git a/.gitea/workflows/release.yaml b/.gitea/workflows/release.yaml new file mode 100644 index 0000000..7276a83 --- /dev/null +++ b/.gitea/workflows/release.yaml @@ -0,0 +1,68 @@ +name: release + +# På varje push till master/main: kör tester, bygg release-binärer för +# linux arm64 (nativt på Pi5-runnern) + linux x64 (cross) och lägg dem +# på en rullande "latest"-release på släppsidan i Gitea. +on: + push: + branches: [master, main] + workflow_dispatch: {} + +jobs: + build-release: + runs-on: ubuntu-latest + steps: + - name: Checkout + uses: actions/checkout@v4 + + - name: Installera Rust + cross-toolchain + run: | + set -e + sudo apt-get update -qq + sudo apt-get install -y -qq gcc-x86-64-linux-gnu jq curl + curl --proto '=https' --tlsv1.2 -sSf https://sh.rustup.rs \ + | sh -s -- -y --profile minimal --default-toolchain stable + . "$HOME/.cargo/env" + rustup target add x86_64-unknown-linux-gnu + + - name: Testa + bygg (arm64 nativt + x64 cross) + run: | + set -e + . "$HOME/.cargo/env" + # Snäll mot Pi5:n — låg prio och max 2 parallella jobb + export CARGO_BUILD_JOBS=2 + nice -n 10 cargo test --release + nice -n 10 cargo build --release + export CARGO_TARGET_X86_64_UNKNOWN_LINUX_GNU_LINKER=x86_64-linux-gnu-gcc + nice -n 10 cargo build --release --target x86_64-unknown-linux-gnu + mkdir -p build + cp target/release/tui-fm build/tui-fm-linux-arm64 + cp target/x86_64-unknown-linux-gnu/release/tui-fm build/tui-fm-linux-x64 + (cd build && sha256sum tui-fm-linux-* > checksums.txt && ls -la) + + - name: Skapa/uppdatera latest-release + ladda upp binärer + env: + API: http://gitea-d:3000/api/v1 + REPO: ${{ github.repository }} + TOKEN: ${{ secrets.GITHUB_TOKEN }} + run: | + set -e + TAG="latest" + BODY="Rullande bygge från senaste master. Commit: ${{ github.sha }}. Arkitekturer: linux x64 + arm64 (Pi5)." + rid=$(curl -s -X POST "$API/repos/$REPO/releases" \ + -H "Authorization: token $TOKEN" -H 'Content-Type: application/json' \ + -d "{\"tag_name\":\"$TAG\",\"name\":\"$TAG\",\"body\":\"$BODY\"}" | jq -r '.id // empty') + [ -z "$rid" ] && rid=$(curl -s "$API/repos/$REPO/releases/tags/$TAG" \ + -H "Authorization: token $TOKEN" | jq -r '.id') + echo "release id: $rid" + for f in tui-fm-linux-x64 tui-fm-linux-arm64 checksums.txt; do + aid=$(curl -s "$API/repos/$REPO/releases/$rid/assets" \ + -H "Authorization: token $TOKEN" | jq -r ".[] | select(.name==\"$f\") | .id") + if [ -n "$aid" ] && [ "$aid" != "null" ]; then + curl -s -X DELETE "$API/repos/$REPO/releases/$rid/assets/$aid" \ + -H "Authorization: token $TOKEN" -o /dev/null + fi + curl -s -X POST "$API/repos/$REPO/releases/$rid/assets?name=$f" \ + -H "Authorization: token $TOKEN" \ + -F "attachment=@build/$f" -o /dev/null -w " $f -> HTTP %{http_code}\n" + done diff --git a/.vscode/tasks.json b/.vscode/tasks.json index 4d33caf..354622f 100644 --- a/.vscode/tasks.json +++ b/.vscode/tasks.json @@ -1,6 +1,15 @@ { "version": "2.0.0", "tasks": [ + { + "label": "Build alla (Linux + Windows)", + "type": "shell", + "command": "cargo build --release && cargo build --release --target x86_64-pc-windows-gnu && mkdir -p build/linux build/windows && cp target/release/tui-fm build/linux/tui-fm && cp target/x86_64-pc-windows-gnu/release/tui-fm.exe build/windows/tui-fm.exe && echo 'Done: build/linux/tui-fm + build/windows/tui-fm.exe'", + "group": "build", + "presentation": { "reveal": "always", "panel": "shared" }, + "problemMatcher": "$rustc", + "detail": "Bygger båda binärerna: Linux (native) + Windows (kräver mingw-w64)" + }, { "label": "Build - Linux", "type": "shell", diff --git a/Cargo.lock b/Cargo.lock index f561c7f..dbf55b9 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -2,6 +2,15 @@ # It is not intended for manual editing. version = 4 +[[package]] +name = "aho-corasick" +version = "1.1.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ddd31a130427c27518df266943a5308ed92d4b226cc639f5a8f1002816174301" +dependencies = [ + "memchr", +] + [[package]] name = "allocator-api2" version = "0.2.21" @@ -9,16 +18,94 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "683d7910e743518b0e34f1186f92494becacb047c7b6bf616c96772180fef923" [[package]] -name = "bitflags" -version = "2.11.0" +name = "anyhow" +version = "1.0.103" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "843867be96c8daad0d758b57df9392b6d8d271134fce549de6ce169ff98a92af" +checksum = "2a4385e2e34eb35d6b3efe798b9eb88096925d87726c0798709bf56d9ed84af3" [[package]] -name = "cassowary" -version = "0.3.0" +name = "approx" +version = "0.5.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "df8670b8c7b9dae1793364eafadf7239c40d669904660c5960d74cfd80b46a53" +checksum = "cab112f0a86d568ea0e627cc1d6be74a1e9cd55214684db5561995f6dad897c6" +dependencies = [ + "num-traits", +] + +[[package]] +name = "atomic" +version = "0.6.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a89cbf775b137e9b968e67227ef7f775587cde3fd31b0d8599dbd0f598a48340" +dependencies = [ + "bytemuck", +] + +[[package]] +name = "autocfg" +version = "1.5.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f2032f911046de80f0a198e0901378627c33f59ea0ac00e363d481118bd70a53" + +[[package]] +name = "base64" +version = "0.22.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "72b3254f16251a8381aa12e40e3c4d2f0199f8c6508fbecb9d91f575e0fbb8c6" + +[[package]] +name = "bit-set" +version = "0.5.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0700ddab506f33b20a03b13996eccd309a48e5ff77d0d95926aa0210fb4e95f1" +dependencies = [ + "bit-vec", +] + +[[package]] +name = "bit-vec" +version = "0.6.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "349f9b6a179ed607305526ca489b34ad0a41aed5f7980fa90eb03160b69598fb" + +[[package]] +name = "bitflags" +version = "1.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bef38d45163c2f1dde094a7dfd33ccf595c92905c8f8f4fdc18d06fb1037718a" + +[[package]] +name = "bitflags" +version = "2.13.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b4388bee8683e3d04af747c73422af53102d2bd24d9eadb6cbc100baef4b43f8" + +[[package]] +name = "block-buffer" +version = "0.10.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3078c7629b62d3f0439517fa394996acacc5cbc91c5a20d8c658e77abd503a71" +dependencies = [ + "generic-array", +] + +[[package]] +name = "bumpalo" +version = "3.20.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "72f5acc6cb2ba439de613abc23857ec3d78374d8ed5ac84e9d11336e87da8649" + +[[package]] +name = "by_address" +version = "1.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "64fa3c856b712db6612c019f14756e64e4bcea13337a6b33b696333a9eaa2d06" + +[[package]] +name = "bytemuck" +version = "1.25.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d6aedf8ae72766347502cf3cb4f41cf5e9cc37d28bee90f1fdaaae15f9cf9424" [[package]] name = "castaway" @@ -36,10 +123,16 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "9330f8b2ff13f34540b44e946ef35111825727b38d33286ef986142615121801" [[package]] -name = "compact_str" -version = "0.8.1" +name = "cfg_aliases" +version = "0.2.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3b79c4069c6cad78e2e0cdfcbd26275770669fb39fd308a752dc110e83b9af32" +checksum = "613afe47fcd5fac7ccf1db93babcb082c5994d996f20b8b159f2ad1658eb5724" + +[[package]] +name = "compact_str" +version = "0.9.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9dfdd1c2274d9aa354115b09dc9a901d6c5576818cdf70d14cae2bdb47df00ab" dependencies = [ "castaway", "cfg-if", @@ -50,13 +143,39 @@ dependencies = [ ] [[package]] -name = "crossterm" -version = "0.28.1" +name = "convert_case" +version = "0.10.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "829d955a0bb380ef178a640b91779e3987da38c9aea133b20614cfed8cdea9c6" +checksum = "633458d4ef8c78b72454de2d54fd6ab2e60f9e02be22f3c6104cdc8a4e0fceb9" dependencies = [ - "bitflags", + "unicode-segmentation", +] + +[[package]] +name = "cpufeatures" +version = "0.2.17" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "59ed5838eebb26a2bb2e58f6d5b5316989ae9d08bab10e0e6d103e656d1b0280" +dependencies = [ + "libc", +] + +[[package]] +name = "critical-section" +version = "1.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "790eea4361631c5e7d22598ecd5723ff611904e3344ce8720784c93e3d83d40b" + +[[package]] +name = "crossterm" +version = "0.29.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d8b9f2e4c67f833b660cdb0a3523065869fb35570177239812ed4c905aeff87b" +dependencies = [ + "bitflags 2.13.0", "crossterm_winapi", + "derive_more", + "document-features", "futures-core", "mio", "parking_lot", @@ -75,6 +194,26 @@ dependencies = [ "winapi", ] +[[package]] +name = "crypto-common" +version = "0.1.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "78c8292055d1c1df0cce5d180393dc8cce0abec0a7102adb6c7b1eef6016d60a" +dependencies = [ + "generic-array", + "typenum", +] + +[[package]] +name = "csscolorparser" +version = "0.6.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "eb2a7d3066da2de787b7f032c736763eb7ae5d355f81a68bab2675a96008b0bf" +dependencies = [ + "lab", + "phf", +] + [[package]] name = "darling" version = "0.23.0" @@ -95,7 +234,7 @@ dependencies = [ "proc-macro2", "quote", "strsim", - "syn", + "syn 2.0.117", ] [[package]] @@ -106,7 +245,51 @@ checksum = "ac3984ec7bd6cfa798e62b4a642426a5be0e68f9401cfc2a01e3fa9ea2fcdb8d" dependencies = [ "darling_core", "quote", - "syn", + "syn 2.0.117", +] + +[[package]] +name = "deltae" +version = "0.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5729f5117e208430e437df2f4843f5e5952997175992d1414f94c57d61e270b4" + +[[package]] +name = "deranged" +version = "0.5.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7cd812cc2bc1d69d4764bd80df88b4317eaef9e773c75226407d9bc0876b211c" + +[[package]] +name = "derive_more" +version = "2.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d751e9e49156b02b44f9c1815bcb94b984cdcc4396ecc32521c739452808b134" +dependencies = [ + "derive_more-impl", +] + +[[package]] +name = "derive_more-impl" +version = "2.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "799a97264921d8623a957f6c3b9011f3b5492f557bbb7a5a19b7fa6d06ba8dcb" +dependencies = [ + "convert_case", + "proc-macro2", + "quote", + "rustc_version", + "syn 2.0.117", +] + +[[package]] +name = "digest" +version = "0.10.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9ed9a281f7bc9b7576e61468ba615a66a5c8cfdff42420a70aa82701a3b1e292" +dependencies = [ + "block-buffer", + "crypto-common", ] [[package]] @@ -130,6 +313,15 @@ dependencies = [ "windows-sys 0.48.0", ] +[[package]] +name = "document-features" +version = "0.2.12" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d4b8a88685455ed29a21542a33abd9cb6510b6b129abadabdcef0f4c55bc8f61" +dependencies = [ + "litrs", +] + [[package]] name = "either" version = "1.15.0" @@ -153,10 +345,64 @@ dependencies = [ ] [[package]] -name = "foldhash" -version = "0.1.5" +name = "euclid" +version = "0.22.14" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d9c4f5dac5e15c24eb999c26181a6ca40b39fe946cbe4c263c7209467bc83af2" +checksum = "f1a05365e3b1c6d1650318537c7460c6923f1abdd272ad6842baa2b509957a06" +dependencies = [ + "num-traits", +] + +[[package]] +name = "fancy-regex" +version = "0.11.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b95f7c0680e4142284cf8b22c14a476e87d61b004a3a0861872b32ef7ead40a2" +dependencies = [ + "bit-set", + "regex", +] + +[[package]] +name = "fast-srgb8" +version = "1.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "dd2e7510819d6fbf51a5545c8f922716ecfb14df168a3242f7d33e0239efe6a1" + +[[package]] +name = "filedescriptor" +version = "0.8.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e40758ed24c9b2eeb76c35fb0aebc66c626084edd827e07e1552279814c6682d" +dependencies = [ + "libc", + "thiserror 1.0.69", + "winapi", +] + +[[package]] +name = "finl_unicode" +version = "1.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9844ddc3a6e533d62bba727eb6c28b5d360921d5175e9ff0f1e621a5c590a4d5" + +[[package]] +name = "fixedbitset" +version = "0.4.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0ce7134b9999ecaf8bcd65542e436736ef32ddca1b3e06094cb6ec5755203b80" + +[[package]] +name = "fnv" +version = "1.0.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3f9eec918d3f24069decb9af1554cad7c880e2da24a9afd88aca000531ab82c1" + +[[package]] +name = "foldhash" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "77ce24cb58228fbb8aa041425bb1050850ac19177686ea6e0f41a70416f56fdb" [[package]] name = "fs_extra" @@ -170,6 +416,34 @@ version = "0.3.32" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "7e3450815272ef58cec6d564423f6e755e25379b217b0bc688e295ba24df6b1d" +[[package]] +name = "futures-task" +version = "0.3.32" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "037711b3d59c33004d3856fbdc83b99d4ff37a24768fa1be9ce3538a1cde4393" + +[[package]] +name = "futures-util" +version = "0.3.32" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "389ca41296e6190b48053de0321d02a77f32f8a5d2461dd38762c0593805c6d6" +dependencies = [ + "futures-core", + "futures-task", + "pin-project-lite", + "slab", +] + +[[package]] +name = "generic-array" +version = "0.14.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "85649ca51fd72272d7821adaf274ad91c288277713d9c18820d8499a7ff69e9a" +dependencies = [ + "typenum", + "version_check", +] + [[package]] name = "getrandom" version = "0.2.17" @@ -182,10 +456,44 @@ dependencies = [ ] [[package]] -name = "hashbrown" -version = "0.15.5" +name = "getrandom" +version = "0.3.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9229cfe53dfd69f0609a49f65461bd93001ea1ef889cd5529dd176593f5338a1" +checksum = "899def5c37c4fd7b2664648c28120ecec138e4d395b459e5ca34f9cce2dd77fd" +dependencies = [ + "cfg-if", + "libc", + "r-efi 5.3.0", + "wasip2", +] + +[[package]] +name = "getrandom" +version = "0.4.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "300e883d756b2e4ec94e02791f39b04b522276138852cfc41d9fb7e904106099" +dependencies = [ + "cfg-if", + "libc", + "r-efi 6.0.0", +] + +[[package]] +name = "hashbrown" +version = "0.16.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "841d1cc9bed7f9236f321df977030373f4a4163ae1a7dbfe1a51a2c1a51d9100" +dependencies = [ + "allocator-api2", + "equivalent", + "foldhash", +] + +[[package]] +name = "hashbrown" +version = "0.17.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ed5909b6e89a2db4456e54cd5f673791d7eca6732202bbf2a9cc504fe2f9b84a" dependencies = [ "allocator-api2", "equivalent", @@ -198,6 +506,12 @@ version = "0.5.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "2304e00983f87ffb38b55b444b5e3b60a884b5d30c0fca7d82fe33449bbe55ea" +[[package]] +name = "hex" +version = "0.4.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7f24254aa9a54b5c858eaee2f5bccdb46aaf0e486a595ed5fd8f86ba55232a70" + [[package]] name = "ident_case" version = "1.0.1" @@ -223,14 +537,14 @@ dependencies = [ "indoc", "proc-macro2", "quote", - "syn", + "syn 2.0.117", ] [[package]] name = "itertools" -version = "0.13.0" +version = "0.14.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "413ee7dfc52ee1a4949ceeb7dbc8a33f2d6c088194d9f922fb8318faf1f01186" +checksum = "2b192c782037fadd9cfa75548310488aabdbf3d2da73885b31bd0abd03351285" dependencies = [ "either", ] @@ -241,12 +555,52 @@ version = "1.0.18" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "8f42a60cbdf9a97f5d2305f08a87dc4e09308d1276d28c869c684d7777685682" +[[package]] +name = "js-sys" +version = "0.3.103" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "53b44bfcdb3f8d5837a46dae1ca9660a837176eee74a28b229bc626816589102" +dependencies = [ + "cfg-if", + "futures-util", + "wasm-bindgen", +] + +[[package]] +name = "kasuari" +version = "0.4.12" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bde5057d6143cc94e861d90f591b9303d6716c6b9602309150bd068853c10899" +dependencies = [ + "hashbrown 0.16.1", + "portable-atomic", + "thiserror 2.0.18", +] + +[[package]] +name = "lab" +version = "0.11.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bf36173d4167ed999940f804952e6b08197cae5ad5d572eb4db150ce8ad5d58f" + +[[package]] +name = "lazy_static" +version = "1.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bbd2bcb4c963f2ddae06a2efc7e9f3591312473c50c6685e1f298068316e66fe" + [[package]] name = "libc" version = "0.2.183" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "b5b646652bf6661599e1da8901b3b9522896f01e736bad5f723fe7a3a27f899d" +[[package]] +name = "libm" +version = "0.2.16" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b6d2cec3eae94f9f509c767b45932f1ada8350c4bdb85af2fcab4a3c14807981" + [[package]] name = "libredox" version = "0.1.15" @@ -257,10 +611,25 @@ dependencies = [ ] [[package]] -name = "linux-raw-sys" -version = "0.4.15" +name = "line-clipping" +version = "0.3.7" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d26c52dbd32dccf2d10cac7725f8eae5296885fb5703b261f7d0a0739ec807ab" +checksum = "3f50e8f47623268b5407192d26876c4d7f89d686ca130fdc53bced4814cd29f8" +dependencies = [ + "bitflags 2.13.0", +] + +[[package]] +name = "linux-raw-sys" +version = "0.12.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "32a66949e030da00e8c7d4434b251670a91556f4144941d37452769c25d58a53" + +[[package]] +name = "litrs" +version = "1.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "11d3d7f243d5c5a8b9bb5d6dd2b1602c0cb0b9db1621bafc7ed66e35ff9fe092" [[package]] name = "lock_api" @@ -279,11 +648,21 @@ checksum = "5e5032e24019045c762d3c0f28f5b6b8bbf38563a65908389bf7978758920897" [[package]] name = "lru" -version = "0.12.5" +version = "0.18.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "234cf4f4a04dc1f57e24b96cc0cd600cf2af460d4161ac5ecdd0af8e1f3b2a38" +checksum = "0b6180140927ee907000b0aa540091f6ea512ead4447c92b8fc35bc72788a5a6" dependencies = [ - "hashbrown", + "hashbrown 0.17.1", +] + +[[package]] +name = "mac_address" +version = "1.1.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c0aeb26bf5e836cc1c341c8106051b573f1766dfa05aa87f0b98be5e51b02303" +dependencies = [ + "nix", + "winapi", ] [[package]] @@ -292,6 +671,27 @@ version = "2.8.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "f8ca58f447f06ed17d5fc4043ce1b10dd205e060fb3ce5b979b8ed8e59ff3f79" +[[package]] +name = "memmem" +version = "0.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a64a92489e2744ce060c349162be1c5f33c6969234104dbd99ddb5feb08b8c15" + +[[package]] +name = "memoffset" +version = "0.9.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "488016bfae457b036d996092f6cb448677611ce4449e970ceaf42695203f218a" +dependencies = [ + "autocfg", +] + +[[package]] +name = "minimal-lexical" +version = "0.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "68354c5c6bd36d73ff3feceb05efa59b6acb7626617f4962be322a825e61f79a" + [[package]] name = "mio" version = "1.2.0" @@ -304,12 +704,109 @@ dependencies = [ "windows-sys 0.61.2", ] +[[package]] +name = "nix" +version = "0.29.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "71e2746dc3a24dd78b3cfcb7be93368c6de9963d30f43a6a73998a9cf4b17b46" +dependencies = [ + "bitflags 2.13.0", + "cfg-if", + "cfg_aliases", + "libc", + "memoffset", +] + +[[package]] +name = "nom" +version = "7.1.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d273983c5a657a70a3e8f2a01329822f3b8c8172b73826411a55751e404a0a4a" +dependencies = [ + "memchr", + "minimal-lexical", +] + +[[package]] +name = "num-conv" +version = "0.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "521739c6d2bac4aa25192232afe6841231376b2b26d4d9fae5ecf8ca5772e441" + +[[package]] +name = "num-derive" +version = "0.4.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ed3955f1a9c7c0c15e092f9c887db08b1fc683305fdf6eb6684f22555355e202" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.117", +] + +[[package]] +name = "num-traits" +version = "0.2.19" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "071dfc062690e90b734c0b2273ce72ad0ffa95f0c74596bc250dcfd960262841" +dependencies = [ + "autocfg", +] + +[[package]] +name = "num_threads" +version = "0.1.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5c7398b9c8b70908f6371f47ed36737907c87c52af34c268fed0bf0ceb92ead9" +dependencies = [ + "libc", +] + +[[package]] +name = "once_cell" +version = "1.21.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9f7c3e4beb33f85d45ae3e3a1792185706c8e16d043238c593331cc7cd313b50" + [[package]] name = "option-ext" version = "0.2.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "04744f49eae99ab78e0d5c0b603ab218f515ea8cfe5a456d7629ad883a3b6e7d" +[[package]] +name = "ordered-float" +version = "4.6.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7bb71e1b3fa6ca1c61f383464aaf2bb0e2f8e772a1f01d486832464de363b951" +dependencies = [ + "num-traits", +] + +[[package]] +name = "palette" +version = "0.7.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4cbf71184cc5ecc2e4e1baccdb21026c20e5fc3dcf63028a086131b3ab00b6e6" +dependencies = [ + "approx", + "fast-srgb8", + "libm", + "palette_derive", +] + +[[package]] +name = "palette_derive" +version = "0.7.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f5030daf005bface118c096f510ffb781fc28f9ab6a32ab224d8631be6851d30" +dependencies = [ + "by_address", + "proc-macro2", + "quote", + "syn 2.0.117", +] + [[package]] name = "parking_lot" version = "0.12.5" @@ -334,10 +831,116 @@ dependencies = [ ] [[package]] -name = "paste" -version = "1.0.15" +name = "pest" +version = "2.8.7" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "57c0d7b74b563b49d38dae00a0c37d4d6de9b432382b2892f0574ddcae73fd0a" +checksum = "47627dd7305c6a2d6c8c6bcd24c5a4c17dbbf425f4f9c5313e724b38fc9782e9" +dependencies = [ + "memchr", + "ucd-trie", +] + +[[package]] +name = "pest_derive" +version = "2.8.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4b4254325ecad416ab689e27ba51da03ba01a9632bc6e108f5fe7c3c4ad29d58" +dependencies = [ + "pest", + "pest_generator", +] + +[[package]] +name = "pest_generator" +version = "2.8.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6c4c0e91ead7a8f7acecbca6f003fc2e8282b1dbe2dd9c9d2f16aba42995e0a7" +dependencies = [ + "pest", + "pest_meta", + "proc-macro2", + "quote", + "syn 2.0.117", +] + +[[package]] +name = "pest_meta" +version = "2.8.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f9744bc48116fee06334924bb5f2bad41eed5e89bd26e29b0b799f9a3f82c210" +dependencies = [ + "pest", +] + +[[package]] +name = "phf" +version = "0.11.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1fd6780a80ae0c52cc120a26a1a42c1ae51b247a253e4e06113d23d2c2edd078" +dependencies = [ + "phf_macros", + "phf_shared", +] + +[[package]] +name = "phf_codegen" +version = "0.11.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "aef8048c789fa5e851558d709946d6d79a8ff88c0440c587967f8e94bfb1216a" +dependencies = [ + "phf_generator", + "phf_shared", +] + +[[package]] +name = "phf_generator" +version = "0.11.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3c80231409c20246a13fddb31776fb942c38553c51e871f8cbd687a4cfb5843d" +dependencies = [ + "phf_shared", + "rand", +] + +[[package]] +name = "phf_macros" +version = "0.11.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f84ac04429c13a7ff43785d75ad27569f2951ce0ffd30a3321230db2fc727216" +dependencies = [ + "phf_generator", + "phf_shared", + "proc-macro2", + "quote", + "syn 2.0.117", +] + +[[package]] +name = "phf_shared" +version = "0.11.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "67eabc2ef2a60eb7faa00097bd1ffdb5bd28e62bf39990626a582201b7a754e5" +dependencies = [ + "siphasher", +] + +[[package]] +name = "pin-project-lite" +version = "0.2.17" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a89322df9ebe1c1578d689c92318e070967d1042b512afbe49518723f4e6d5cd" + +[[package]] +name = "portable-atomic" +version = "1.13.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c33a9471896f1c69cecef8d20cbe2f7accd12527ce60845ff44c153bb2a21b49" + +[[package]] +name = "powerfmt" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "439ee305def115ba05938db6eb1644ff94165c5ab5e9420d1c1bcedbba909391" [[package]] name = "proc-macro2" @@ -358,24 +961,131 @@ dependencies = [ ] [[package]] -name = "ratatui" -version = "0.29.0" +name = "r-efi" +version = "5.3.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "eabd94c2f37801c20583fc49dd5cd6b0ba68c716787c2dd6ed18571e1e63117b" +checksum = "69cdb34c158ceb288df11e18b4bd39de994f6657d83847bdffdbd7f346754b0f" + +[[package]] +name = "r-efi" +version = "6.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f8dcc9c7d52a811697d2151c701e0d08956f92b0e24136cf4cf27b57a6a0d9bf" + +[[package]] +name = "rand" +version = "0.8.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "22f6172bdec972074665ed81ed53b71da00bfc44b65a753cfde883ec4c702a1a" dependencies = [ - "bitflags", - "cassowary", + "rand_core", +] + +[[package]] +name = "rand_core" +version = "0.6.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ec0be4795e2f6a28069bec0b5ff3e2ac9bafc99e6a9a7dc3547996c5c816922c" + +[[package]] +name = "ratatui" +version = "0.30.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3274ba0a2c5e1bcad2a2005d20f4dc59dad26b2eb0940fb094500dba4099d57d" +dependencies = [ + "instability", + "ratatui-core", + "ratatui-crossterm", + "ratatui-macros", + "ratatui-termina", + "ratatui-termwiz", + "ratatui-widgets", + "serde", +] + +[[package]] +name = "ratatui-core" +version = "0.1.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cbb175c433c8e28a809d1f5773a2ae96e68c0ce40db865cbab1020bf33ae479c" +dependencies = [ + "bitflags 2.13.0", "compact_str", + "critical-section", + "hashbrown 0.17.1", + "itertools", + "kasuari", + "lru", + "palette", + "serde", + "strum", + "thiserror 2.0.18", + "unicode-segmentation", + "unicode-truncate", + "unicode-width", +] + +[[package]] +name = "ratatui-crossterm" +version = "0.1.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "567584a3b0e6a8203c23de40b4861497266725eb5363dbfd18a1edd603cca9f0" +dependencies = [ + "cfg-if", "crossterm", + "instability", + "ratatui-core", +] + +[[package]] +name = "ratatui-macros" +version = "0.7.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ed7dc68daa7498a43e4d68e0eb078427e10c38fbcfbb1e42d955f1fa2140d814" +dependencies = [ + "ratatui-core", + "ratatui-widgets", +] + +[[package]] +name = "ratatui-termina" +version = "0.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c0bf912d9e66f057a759d92e386a280ea886b352ab757d6ac4d653c7ed2c43c2" +dependencies = [ + "instability", + "ratatui-core", + "termina", +] + +[[package]] +name = "ratatui-termwiz" +version = "0.1.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "faf03e0380b7744054d6cb74224fe3adf062a029754933f575ca1e3b4c2ce977" +dependencies = [ + "ratatui-core", + "termwiz", +] + +[[package]] +name = "ratatui-widgets" +version = "0.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "66e3d19bcc9130ca376277d93b60767ff121ace3be06f5f95f81dd68956407d1" +dependencies = [ + "bitflags 2.13.0", + "hashbrown 0.17.1", "indoc", "instability", "itertools", - "lru", - "paste", + "line-clipping", + "ratatui-core", + "serde", "strum", + "time", "unicode-segmentation", - "unicode-truncate", - "unicode-width 0.2.0", + "unicode-width", ] [[package]] @@ -384,7 +1094,7 @@ version = "0.5.18" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "ed2bf2547551a7053d6fdfafda3f938979645c44812fbfcda098faae3f1a362d" dependencies = [ - "bitflags", + "bitflags 2.13.0", ] [[package]] @@ -393,22 +1103,60 @@ version = "0.4.6" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "ba009ff324d1fc1b900bd1fdb31564febe58a8ccc8a6fdbb93b543d33b13ca43" dependencies = [ - "getrandom", + "getrandom 0.2.17", "libredox", - "thiserror", + "thiserror 1.0.69", +] + +[[package]] +name = "regex" +version = "1.13.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2a0e75113e14dc5acb068cd0786884f214f1312650a3d36d269f5c4f3cdee8a2" +dependencies = [ + "aho-corasick", + "memchr", + "regex-automata", + "regex-syntax", +] + +[[package]] +name = "regex-automata" +version = "0.4.15" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1f388202e4b80542a0921078cc23b6333bcf1409c1e3f86404cae4766a6131db" +dependencies = [ + "aho-corasick", + "memchr", + "regex-syntax", +] + +[[package]] +name = "regex-syntax" +version = "0.8.11" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d6f6ff9a378485b298a5286656da665ba74413d36db0979633275d2e708145d4" + +[[package]] +name = "rustc_version" +version = "0.4.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cfcb3a22ef46e85b45de6ee7e79d063319ebb6594faafcf1c225ea92ab6e9b92" +dependencies = [ + "semver", ] [[package]] name = "rustix" -version = "0.38.44" +version = "1.1.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "fdb5bc1ae2baa591800df16c9ca78619bf65c0488b41b96ccec5d11220d8c154" +checksum = "b6fe4565b9518b83ef4f91bb47ce29620ca828bd32cb7e408f0062e9930ba190" dependencies = [ - "bitflags", + "bitflags 2.13.0", "errno", "libc", "linux-raw-sys", - "windows-sys 0.59.0", + "windows-sys 0.61.2", ] [[package]] @@ -429,6 +1177,12 @@ version = "1.2.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "94143f37725109f92c262ed2cf5e59bce7498c01bcc1502d7b9afe439a4e9f49" +[[package]] +name = "semver" +version = "1.0.28" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8a7852d02fc848982e0c167ef163aaff9cd91dc640ba85e263cb1ce46fae51cd" + [[package]] name = "serde" version = "1.0.228" @@ -456,7 +1210,7 @@ checksum = "d540f220d3187173da220f885ab66608367b6574e925011a9353e4badda91d79" dependencies = [ "proc-macro2", "quote", - "syn", + "syn 2.0.117", ] [[package]] @@ -472,6 +1226,17 @@ dependencies = [ "zmij", ] +[[package]] +name = "sha2" +version = "0.10.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a7507d819769d01a365ab707794a4084392c824f54a7a6a7862f8c3d0892b283" +dependencies = [ + "cfg-if", + "cpufeatures", + "digest", +] + [[package]] name = "signal-hook" version = "0.3.18" @@ -503,6 +1268,18 @@ dependencies = [ "libc", ] +[[package]] +name = "siphasher" +version = "1.0.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8ee5873ec9cce0195efcb7a4e9507a04cd49aec9c83d0389df45b1ef7ba2e649" + +[[package]] +name = "slab" +version = "0.4.12" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0c790de23124f9ab44544d7ac05d60440adc586479ce501c1d6d7da3cd8c9cf5" + [[package]] name = "smallvec" version = "1.15.1" @@ -523,24 +1300,34 @@ checksum = "7da8b5736845d9f2fcb837ea5d9e2628564b3b043a70948a3f0b778838c5fb4f" [[package]] name = "strum" -version = "0.26.3" +version = "0.28.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8fec0f0aef304996cf250b31b5a10dee7980c85da9d759361292b8bca5a18f06" +checksum = "9628de9b8791db39ceda2b119bbe13134770b56c138ec1d3af810d045c04f9bd" dependencies = [ "strum_macros", ] [[package]] name = "strum_macros" -version = "0.26.4" +version = "0.28.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4c6bee85a5a24955dc440386795aa378cd9cf82acd5f764469152d2270e581be" +checksum = "ab85eea0270ee17587ed4156089e10b9e6880ee688791d45a905f5b1ca36f664" dependencies = [ "heck", "proc-macro2", "quote", - "rustversion", - "syn", + "syn 2.0.117", +] + +[[package]] +name = "syn" +version = "1.0.109" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "72b64191b275b66ffe2469e8af2c1cfe3bafa67b529ead792a6d0160888b4237" +dependencies = [ + "proc-macro2", + "quote", + "unicode-ident", ] [[package]] @@ -554,13 +1341,98 @@ dependencies = [ "unicode-ident", ] +[[package]] +name = "termina" +version = "0.3.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9048a889effe34a5cddee0af7f53285198b16dca3be510858d38dfdb3e62a04e" +dependencies = [ + "bitflags 2.13.0", + "parking_lot", + "rustix", + "signal-hook", + "windows-sys 0.61.2", +] + +[[package]] +name = "terminfo" +version = "0.9.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d4ea810f0692f9f51b382fff5893887bb4580f5fa246fde546e0b13e7fcee662" +dependencies = [ + "fnv", + "nom", + "phf", + "phf_codegen", +] + +[[package]] +name = "termios" +version = "0.3.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "411c5bf740737c7918b8b1fe232dca4dc9f8e754b8ad5e20966814001ed0ac6b" +dependencies = [ + "libc", +] + +[[package]] +name = "termwiz" +version = "0.23.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4676b37242ccbd1aabf56edb093a4827dc49086c0ffd764a5705899e0f35f8f7" +dependencies = [ + "anyhow", + "base64", + "bitflags 2.13.0", + "fancy-regex", + "filedescriptor", + "finl_unicode", + "fixedbitset", + "hex", + "lazy_static", + "libc", + "log", + "memmem", + "nix", + "num-derive", + "num-traits", + "ordered-float", + "pest", + "pest_derive", + "phf", + "sha2", + "signal-hook", + "siphasher", + "terminfo", + "termios", + "thiserror 1.0.69", + "ucd-trie", + "unicode-segmentation", + "vtparse", + "wezterm-bidi", + "wezterm-blob-leases", + "wezterm-color-types", + "wezterm-dynamic", + "wezterm-input-types", + "winapi", +] + [[package]] name = "thiserror" version = "1.0.69" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "b6aaf5339b578ea85b50e080feb250a3e8ae8cfcdff9a461c9ec2904bc923f52" dependencies = [ - "thiserror-impl", + "thiserror-impl 1.0.69", +] + +[[package]] +name = "thiserror" +version = "2.0.18" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4288b5bcbc7920c07a1149a35cf9590a2aa808e0bc1eafaade0b80947865fbc4" +dependencies = [ + "thiserror-impl 2.0.18", ] [[package]] @@ -571,9 +1443,41 @@ checksum = "4fee6c4efc90059e10f81e6d42c60a18f76588c3d74cb83a0b242a2b6c7504c1" dependencies = [ "proc-macro2", "quote", - "syn", + "syn 2.0.117", ] +[[package]] +name = "thiserror-impl" +version = "2.0.18" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ebc4ee7f67670e9b64d05fa4253e753e016c6c95ff35b89b7941d6b856dec1d5" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.117", +] + +[[package]] +name = "time" +version = "0.3.53" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "18dfaaeddcb932337b5e7866ee7d0ce9b76d2fd092997146f187ec09b4558a50" +dependencies = [ + "deranged", + "libc", + "num-conv", + "num_threads", + "powerfmt", + "serde_core", + "time-core", +] + +[[package]] +name = "time-core" +version = "0.1.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9e1c906769ad99c88eaa54e728060edef082f8e358ff32030cb7c7d315e81109" + [[package]] name = "tui-fm" version = "0.1.0" @@ -584,9 +1488,21 @@ dependencies = [ "ratatui", "serde", "serde_json", - "unicode-width 0.2.0", + "unicode-width", ] +[[package]] +name = "typenum" +version = "1.20.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b6f5e870be6c3b371b77fe0ee0bafb859fa4964b4404c27de1d380043c4dda20" + +[[package]] +name = "ucd-trie" +version = "0.1.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2896d95c02a80c6d6a5d6e953d479f5ddf2dfdb6a244441010e373ac0fb88971" + [[package]] name = "unicode-ident" version = "1.0.24" @@ -601,33 +1517,186 @@ checksum = "9629274872b2bfaf8d66f5f15725007f635594914870f65218920345aa11aa8c" [[package]] name = "unicode-truncate" -version = "1.1.0" +version = "2.0.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b3644627a5af5fa321c95b9b235a72fd24cd29c648c2c379431e6628655627bf" +checksum = "16b380a1238663e5f8a691f9039c73e1cdae598a30e9855f541d29b08b53e9a5" dependencies = [ "itertools", "unicode-segmentation", - "unicode-width 0.1.14", + "unicode-width", ] -[[package]] -name = "unicode-width" -version = "0.1.14" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7dd6e30e90baa6f72411720665d41d89b9a3d039dc45b8faea1ddd07f617f6af" - [[package]] name = "unicode-width" version = "0.2.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "1fc81956842c57dac11422a97c3b8195a1ff727f06e85c84ed2e8aa277c9a0fd" +[[package]] +name = "utf8parse" +version = "0.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "06abde3611657adf66d383f00b093d7faecc7fa57071cce2578660c9f1010821" + +[[package]] +name = "uuid" +version = "1.23.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ea5fab0d6c3c01ae70085a09cb03d4c7a1d6314e2b3e075392783396d724ca0a" +dependencies = [ + "atomic", + "getrandom 0.4.3", + "js-sys", + "wasm-bindgen", +] + +[[package]] +name = "version_check" +version = "0.9.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0b928f33d975fc6ad9f86c8f283853ad26bdd5b10b7f1542aa2fa15e2289105a" + +[[package]] +name = "vtparse" +version = "0.6.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6d9b2acfb050df409c972a37d3b8e08cdea3bddb0c09db9d53137e504cfabed0" +dependencies = [ + "utf8parse", +] + [[package]] name = "wasi" version = "0.11.1+wasi-snapshot-preview1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "ccf3ec651a847eb01de73ccad15eb7d99f80485de043efb2f370cd654f4ea44b" +[[package]] +name = "wasip2" +version = "1.0.4+wasi-0.2.12" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b67efb37e106e55ce722a510d6b5f9c17f083e5fc79afc2badeb12cc313d9487" +dependencies = [ + "wit-bindgen", +] + +[[package]] +name = "wasm-bindgen" +version = "0.2.126" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4b067c0c11094aef6b7a801c1e34a26affafdf3d051dba08456b868789aaf9a4" +dependencies = [ + "cfg-if", + "once_cell", + "rustversion", + "wasm-bindgen-macro", + "wasm-bindgen-shared", +] + +[[package]] +name = "wasm-bindgen-macro" +version = "0.2.126" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "167ce5e579f6bcf889c4f7175a8a5a585de84e8ff93976ce393efa5f2837aab1" +dependencies = [ + "quote", + "wasm-bindgen-macro-support", +] + +[[package]] +name = "wasm-bindgen-macro-support" +version = "0.2.126" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f3997c7839262f4ef12cf90b818d6340c18e80f263f1a94bf157d0ec4420380e" +dependencies = [ + "bumpalo", + "proc-macro2", + "quote", + "syn 2.0.117", + "wasm-bindgen-shared", +] + +[[package]] +name = "wasm-bindgen-shared" +version = "0.2.126" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "dc1b4cb0cc549fcf58d7dfc081778139b3d283a081644e833e84682ad71cea24" +dependencies = [ + "unicode-ident", +] + +[[package]] +name = "wezterm-bidi" +version = "0.2.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0c0a6e355560527dd2d1cf7890652f4f09bb3433b6aadade4c9b5ed76de5f3ec" +dependencies = [ + "log", + "wezterm-dynamic", +] + +[[package]] +name = "wezterm-blob-leases" +version = "0.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "692daff6d93d94e29e4114544ef6d5c942a7ed998b37abdc19b17136ea428eb7" +dependencies = [ + "getrandom 0.3.4", + "mac_address", + "sha2", + "thiserror 1.0.69", + "uuid", +] + +[[package]] +name = "wezterm-color-types" +version = "0.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7de81ef35c9010270d63772bebef2f2d6d1f2d20a983d27505ac850b8c4b4296" +dependencies = [ + "csscolorparser", + "deltae", + "lazy_static", + "wezterm-dynamic", +] + +[[package]] +name = "wezterm-dynamic" +version = "0.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5f2ab60e120fd6eaa68d9567f3226e876684639d22a4219b313ff69ec0ccd5ac" +dependencies = [ + "log", + "ordered-float", + "strsim", + "thiserror 1.0.69", + "wezterm-dynamic-derive", +] + +[[package]] +name = "wezterm-dynamic-derive" +version = "0.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "46c0cf2d539c645b448eaffec9ec494b8b19bd5077d9e58cb1ae7efece8d575b" +dependencies = [ + "proc-macro2", + "quote", + "syn 1.0.109", +] + +[[package]] +name = "wezterm-input-types" +version = "0.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7012add459f951456ec9d6c7e6fc340b1ce15d6fc9629f8c42853412c029e57e" +dependencies = [ + "bitflags 1.3.2", + "euclid", + "lazy_static", + "serde", + "wezterm-dynamic", +] + [[package]] name = "winapi" version = "0.3.9" @@ -662,16 +1731,7 @@ version = "0.48.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "677d2418bec65e3338edb076e806bc1ec15693c5d0104683f2efe857f61056a9" dependencies = [ - "windows-targets 0.48.5", -] - -[[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", + "windows-targets", ] [[package]] @@ -689,29 +1749,13 @@ version = "0.48.5" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "9a2fa6e2155d7247be68c096456083145c183cbbbc2764150dda45a87197940c" dependencies = [ - "windows_aarch64_gnullvm 0.48.5", - "windows_aarch64_msvc 0.48.5", - "windows_i686_gnu 0.48.5", - "windows_i686_msvc 0.48.5", - "windows_x86_64_gnu 0.48.5", - "windows_x86_64_gnullvm 0.48.5", - "windows_x86_64_msvc 0.48.5", -] - -[[package]] -name = "windows-targets" -version = "0.52.6" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9b724f72796e036ab90c1021d4780d4d3d648aca59e491e6b98e725b84e99973" -dependencies = [ - "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.6", - "windows_x86_64_gnu 0.52.6", - "windows_x86_64_gnullvm 0.52.6", - "windows_x86_64_msvc 0.52.6", + "windows_aarch64_gnullvm", + "windows_aarch64_msvc", + "windows_i686_gnu", + "windows_i686_msvc", + "windows_x86_64_gnu", + "windows_x86_64_gnullvm", + "windows_x86_64_msvc", ] [[package]] @@ -720,78 +1764,36 @@ version = "0.48.5" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "2b38e32f0abccf9987a4e3079dfb67dcd799fb61361e53e2882c3cbaf0d905d8" -[[package]] -name = "windows_aarch64_gnullvm" -version = "0.52.6" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "32a4622180e7a0ec044bb555404c800bc9fd9ec262ec147edd5989ccd0c02cd3" - [[package]] name = "windows_aarch64_msvc" version = "0.48.5" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "dc35310971f3b2dbbf3f0690a219f40e2d9afcf64f9ab7cc1be722937c26b4bc" -[[package]] -name = "windows_aarch64_msvc" -version = "0.52.6" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "09ec2a7bb152e2252b53fa7803150007879548bc709c039df7627cabbd05d469" - [[package]] name = "windows_i686_gnu" version = "0.48.5" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "a75915e7def60c94dcef72200b9a8e58e5091744960da64ec734a6c6e9b3743e" -[[package]] -name = "windows_i686_gnu" -version = "0.52.6" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8e9b5ad5ab802e97eb8e295ac6720e509ee4c243f69d781394014ebfe8bbfa0b" - -[[package]] -name = "windows_i686_gnullvm" -version = "0.52.6" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0eee52d38c090b3caa76c563b86c3a4bd71ef1a819287c19d586d7334ae8ed66" - [[package]] name = "windows_i686_msvc" version = "0.48.5" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "8f55c233f70c4b27f66c523580f78f1004e8b5a8b659e05a4eb49d4166cca406" -[[package]] -name = "windows_i686_msvc" -version = "0.52.6" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "240948bc05c5e7c6dabba28bf89d89ffce3e303022809e73deaefe4f6ec56c66" - [[package]] name = "windows_x86_64_gnu" version = "0.48.5" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "53d40abd2583d23e4718fddf1ebec84dbff8381c07cae67ff7768bbf19c6718e" -[[package]] -name = "windows_x86_64_gnu" -version = "0.52.6" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "147a5c80aabfbf0c7d901cb5895d1de30ef2907eb21fbbab29ca94c5b08b1a78" - [[package]] name = "windows_x86_64_gnullvm" version = "0.48.5" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "0b7b52767868a23d5bab768e390dc5f5c55825b6d30b86c844ff2dc7414044cc" -[[package]] -name = "windows_x86_64_gnullvm" -version = "0.52.6" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "24d5b23dc417412679681396f2b49f3de8c1473deb516bd34410872eff51ed0d" - [[package]] name = "windows_x86_64_msvc" version = "0.48.5" @@ -799,10 +1801,10 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "ed94fce61571a4006852b7389a063ab983c02eb1bb37b47f8272ce92d06d9538" [[package]] -name = "windows_x86_64_msvc" -version = "0.52.6" +name = "wit-bindgen" +version = "0.57.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "589f6da84c646204747d1270a2a5661ea66ed1cced2631d546fdfb155959f9ec" +checksum = "1ebf944e87a7c253233ad6766e082e3cd714b5d03812acc24c318f549614536e" [[package]] name = "zmij" diff --git a/Cargo.toml b/Cargo.toml index 336c562..a60356f 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -12,8 +12,8 @@ opt-level = 3 strip = true [dependencies] -ratatui = "0.29" -crossterm = { version = "0.28", features = ["event-stream"] } +ratatui = "0.30" +crossterm = { version = "0.29", features = ["event-stream"] } serde = { version = "1", features = ["derive"] } serde_json = "1" dirs = "5" diff --git a/build/linux/tui-fm b/build/linux/tui-fm index 86e8923..c7733c4 100755 Binary files a/build/linux/tui-fm and b/build/linux/tui-fm differ diff --git a/src/app.rs b/src/app.rs index e0c49c4..dea0e40 100644 --- a/src/app.rs +++ b/src/app.rs @@ -1,6 +1,6 @@ use std::path::PathBuf; -use crate::config::{AppliesTo, Config, CustomMenuItem}; +use crate::config::{AppliesTo, Config}; use crate::file_ops::{self, FileEntry}; #[derive(Debug, Clone, PartialEq)] @@ -24,6 +24,8 @@ pub struct ClipboardEntry { #[derive(Debug, Clone, PartialEq)] pub enum ContextMenuItem { + /// Öppna filen (via TUI-WM om möjligt) eller gå in i katalogen + Open, Copy, Cut, Paste, @@ -107,6 +109,14 @@ pub struct App { pub settings_hover_button: Option, // Terminal capabilities pub unicode_support: bool, + /// Senast kända terminalstorlek (kolumner, rader). Uppdateras från + /// frame-arean vid varje draw och från Event::Resize — pollar aldrig + /// terminalen, vilket gör hit-testing korrekt även i nästlade + /// terminaler (t.ex. inne i TUI-WM). + pub term_size: (u16, u16), + /// Editor-kommando + fil som ska köras genom att TUI:t tillfälligt + /// suspenderas (fallback när TUI-WM inte kör). Hanteras i run_app. + pub pending_spawn: Option<(String, PathBuf)>, } // Which field inside the settings editor is being edited @@ -181,6 +191,32 @@ impl App { settings_custom_selected: 0, settings_hover_button: None, unicode_support: detect_unicode_support(), + term_size: crossterm::terminal::size().unwrap_or((80, 24)), + pending_spawn: None, + } + } + + /// Öppna en fil: via TUI-WM:s socket om vi kör i ett TUI-WM-fönster, + /// annars med den konfigurerade editorn (TUI:t suspenderas under tiden). + pub fn open_path(&mut self, path: &PathBuf) { + if path.is_dir() { + self.navigate_to(path.clone()); + return; + } + if crate::tui_wm::available() { + match crate::tui_wm::open_file(path) { + Ok(()) => self.set_status(format!("Öppnar {} via TUI-WM", path.display())), + Err(e) => self.set_status(format!("TUI-WM-öppning misslyckades: {}", e)), + } + return; + } + let editor = self.config.text_editor.trim().to_string(); + if editor.is_empty() { + self.set_status( + "Ingen editor konfigurerad (kugghjulet ≡) och TUI-WM kör inte".to_string(), + ); + } else { + self.pending_spawn = Some((editor, path.clone())); } } @@ -423,7 +459,9 @@ impl App { pub fn build_context_menu(&self, x: u16, y: u16, is_sidebar_fav: bool, fav_path: Option) -> ContextMenu { let has_selection = !self.selected_indices.is_empty(); let has_clipboard = self.clipboard.is_some(); - let can_paste = has_clipboard && (self.selected_target_dir().is_some() || true); + // Inklistring går till markerad katalog om en sådan finns, annars + // aktuell katalog — så clipboard-innehåll räcker. + let can_paste = has_clipboard; // Determine if selected items are files/dirs for custom item filtering let selected_is_dir = self.selected_indices.iter().all(|&i| { @@ -441,6 +479,7 @@ impl App { } } else { if has_selection { + items.push(ContextMenuItem::Open); items.push(ContextMenuItem::Copy); items.push(ContextMenuItem::Cut); } @@ -486,11 +525,10 @@ impl App { pub fn selected_target_dir(&self) -> Option { if self.selected_indices.len() == 1 { let i = self.selected_indices[0]; - if let Some(e) = self.entries.get(i) { - if e.is_dir() { + if let Some(e) = self.entries.get(i) + && e.is_dir() { return Some(e.path.clone()); } - } } Some(self.current_path.clone()) } diff --git a/src/config.rs b/src/config.rs index 9cede57..443e7c3 100644 --- a/src/config.rs +++ b/src/config.rs @@ -2,9 +2,11 @@ use serde::{Deserialize, Serialize}; use std::path::PathBuf; #[derive(Serialize, Deserialize, Clone, Debug, PartialEq)] +#[derive(Default)] pub enum AppliesTo { File, Dir, + #[default] Both, } @@ -25,9 +27,6 @@ impl AppliesTo { } } -impl Default for AppliesTo { - fn default() -> Self { AppliesTo::Both } -} #[derive(Serialize, Deserialize, Clone, Default, Debug)] pub struct CustomMenuItem { @@ -49,22 +48,19 @@ impl Config { /// Preferred path: config.json next to the running binary. /// Falls back to ~/.config/tui-fm/config.json if the exe path can't be determined. fn config_path() -> Option { - if let Ok(exe) = std::env::current_exe() { - if let Some(dir) = exe.parent() { + if let Ok(exe) = std::env::current_exe() + && let Some(dir) = exe.parent() { return Some(dir.join("config.json")); } - } dirs::config_dir().map(|d| d.join("tui-fm").join("config.json")) } pub fn load() -> Self { - if let Some(path) = Self::config_path() { - if let Ok(data) = std::fs::read_to_string(&path) { - if let Ok(config) = serde_json::from_str::(&data) { + if let Some(path) = Self::config_path() + && let Ok(data) = std::fs::read_to_string(&path) + && let Ok(config) = serde_json::from_str::(&data) { return config; } - } - } Config::default() } diff --git a/src/events.rs b/src/events.rs index 8751f2d..7567b32 100644 --- a/src/events.rs +++ b/src/events.rs @@ -10,8 +10,32 @@ use crate::app::{App, ConfirmAction, ContextMenuItem, DialogMode, Focus, Setting use crate::config::{AppliesTo, CustomMenuItem}; use crate::ui; -pub fn run_app(terminal: &mut Terminal, app: &mut App) -> io::Result<()> { +pub fn run_app>(terminal: &mut Terminal, app: &mut App) -> io::Result<()> { loop { + // Fallback-öppning utanför TUI-WM: suspendera TUI:t, kör editorn + // på samma terminal, återuppta och rita om. + if let Some((editor, path)) = app.pending_spawn.take() { + use crossterm::event::{DisableMouseCapture, EnableMouseCapture}; + use crossterm::terminal::{ + disable_raw_mode, enable_raw_mode, EnterAlternateScreen, LeaveAlternateScreen, + }; + disable_raw_mode()?; + crossterm::execute!(io::stdout(), LeaveAlternateScreen, DisableMouseCapture)?; + let status = std::process::Command::new("sh") + .arg("-c") + .arg(format!("{} '{}'", editor, path.display().to_string().replace('\'', r"'\''"))) + .status(); + enable_raw_mode()?; + crossterm::execute!(io::stdout(), EnterAlternateScreen, EnableMouseCapture)?; + terminal.clear()?; + match status { + Ok(s) if s.success() => {} + Ok(s) => app.set_status(format!("Editorn avslutades med {}", s)), + Err(e) => app.set_status(format!("Kunde inte starta editorn: {}", e)), + } + app.refresh(); + } + terminal.draw(|f| ui::draw(f, app))?; if event::poll(Duration::from_millis(50))? { @@ -32,6 +56,10 @@ fn handle_event(app: &mut App, ev: Event) -> bool { handle_mouse(app, mouse); false } + Event::Resize(w, h) => { + app.term_size = (w, h); + false + } _ => false, } } @@ -42,9 +70,8 @@ fn handle_key(app: &mut App, key: crossterm::event::KeyEvent) -> bool { match key.code { KeyCode::Esc => { app.context_menu = None; return false; } KeyCode::Up => { - if let Some(ref mut m) = app.context_menu { - if m.selected > 0 { m.selected -= 1; } - } + if let Some(ref mut m) = app.context_menu + && m.selected > 0 { m.selected -= 1; } return false; } KeyCode::Down => { @@ -166,8 +193,8 @@ fn handle_key(app: &mut App, key: crossterm::event::KeyEvent) -> bool { app.path_input_sel_anchor = None; } KeyCode::Backspace => { - if !delete_selection(app) { - if app.path_input_cursor > 0 { + if !delete_selection(app) + && app.path_input_cursor > 0 { let remove_pos = app.path_input_cursor - 1; let new: String = app.path_input.chars().enumerate() .filter(|&(i, _)| i != remove_pos) @@ -176,19 +203,17 @@ fn handle_key(app: &mut App, key: crossterm::event::KeyEvent) -> bool { app.path_input = new; app.path_input_cursor -= 1; } - } app.path_input_sel_anchor = None; } KeyCode::Delete => { - if !delete_selection(app) { - if app.path_input_cursor < char_count { + if !delete_selection(app) + && app.path_input_cursor < char_count { let new: String = app.path_input.chars().enumerate() .filter(|&(i, _)| i != app.path_input_cursor) .map(|(_, c)| c) .collect(); app.path_input = new; } - } app.path_input_sel_anchor = None; } KeyCode::Left => { @@ -280,12 +305,11 @@ fn handle_key(app: &mut App, key: crossterm::event::KeyEvent) -> bool { app.navigate_up(); } KeyCode::F(2) => { - if let Some(&i) = app.selected_indices.first() { - if let Some(e) = app.entries.get(i) { + if let Some(&i) = app.selected_indices.first() + && let Some(e) = app.entries.get(i) { app.rename_original = Some(e.path.clone()); app.dialog = DialogMode::Rename(e.name.clone()); } - } } KeyCode::Delete => { if !app.selected_indices.is_empty() { @@ -294,15 +318,12 @@ fn handle_key(app: &mut App, key: crossterm::event::KeyEvent) -> bool { } } KeyCode::Enter => { - if app.focus == Focus::FileView { - if let Some(&i) = app.selected_indices.first() { - if let Some(e) = app.entries.get(i).cloned() { - if e.is_dir() { + if app.focus == Focus::FileView + && let Some(&i) = app.selected_indices.first() + && let Some(e) = app.entries.get(i).cloned() + && e.is_dir() { app.navigate_to(e.path.clone()); } - } - } - } } KeyCode::Up => { if app.focus == Focus::FileView { @@ -384,6 +405,12 @@ fn execute_context_menu(app: &mut App) { app.context_menu = None; if let Some(item) = item { match item { + ContextMenuItem::Open => { + if let Some(&i) = app.selected_indices.first() + && let Some(e) = app.entries.get(i) { + app.open_path(&e.path.clone()); + } + } ContextMenuItem::Copy => app.copy_selected(), ContextMenuItem::Cut => app.cut_selected(), ContextMenuItem::Paste => { @@ -391,12 +418,11 @@ fn execute_context_menu(app: &mut App) { app.paste_into(&dst.clone()); } ContextMenuItem::Rename => { - if let Some(&i) = app.selected_indices.first() { - if let Some(e) = app.entries.get(i) { + if let Some(&i) = app.selected_indices.first() + && let Some(e) = app.entries.get(i) { app.rename_original = Some(e.path.clone()); app.dialog = DialogMode::Rename(e.name.clone()); } - } } ContextMenuItem::Properties => app.compute_properties(), ContextMenuItem::NewFile => app.dialog = DialogMode::NewFile(String::new()), @@ -744,7 +770,7 @@ fn handle_left_click(app: &mut App, col: u16, row: u16) { // Check if click lands in the submenu (only visible when NewSubmenu is highlighted) let ns_idx = m.items.iter().position(|i| matches!(i, ContextMenuItem::NewSubmenu)); let in_sub = if let Some(ns_pos) = ns_idx { - let (term_w, _) = crossterm::terminal::size().unwrap_or((120, 40)); + let (term_w, _) = app.term_size; let sub_x = (mx + MENU_W).min(term_w.saturating_sub(SUB_W)); let sub_y = my + 1 + ns_pos as u16; col >= sub_x && col < sub_x + SUB_W @@ -800,7 +826,7 @@ fn handle_left_click(app: &mut App, col: u16, row: u16) { if row == gear_row && col >= gear_col && col < gear_col + 3 { // If settings is already open and we click inside the dialog – handled by settings_click if app.dialog == DialogMode::Settings { - let dlg = ui::settings_dialog_rect(sb.x + sb.width); + let dlg = ui::settings_dialog_rect(sb.x + sb.width, app.term_size.1); settings_click(app, col, row, dlg); } else { app.dialog = DialogMode::Settings; @@ -810,7 +836,7 @@ fn handle_left_click(app: &mut App, col: u16, row: u16) { // If settings dialog is open, route all clicks into it if app.dialog == DialogMode::Settings { - let dlg = ui::settings_dialog_rect(sb.x + sb.width); + let dlg = ui::settings_dialog_rect(sb.x + sb.width, app.term_size.1); settings_click(app, col, row, dlg); return; } @@ -834,7 +860,7 @@ fn handle_left_click(app: &mut App, col: u16, row: u16) { let char_count = app.path_input.chars().count(); let input_w = inner_w.saturating_sub(hint_width + 1 + 6); let avail = input_w.saturating_sub(label_display_w) as usize; - let scroll_start = if app.path_input_cursor > avail { app.path_input_cursor - avail } else { 0 }; + let scroll_start = app.path_input_cursor.saturating_sub(avail); let new_cursor = (scroll_start + click_offset).min(char_count); app.path_input_cursor = new_cursor; } @@ -880,9 +906,8 @@ fn handle_left_click(app: &mut App, col: u16, row: u16) { if is_double { app.last_click_entry = None; if let Some(e) = app.entries.get(idx).cloned() { - if e.is_dir() { - app.navigate_to(e.path.clone()); - } + // Kataloger navigeras; filer öppnas via TUI-WM/editor + app.open_path(&e.path.clone()); } } else { app.select_only(idx); @@ -895,22 +920,20 @@ fn handle_right_click(app: &mut App, col: u16, row: u16) { // Right-click on sidebar favorite if col < layout.sidebar.x + layout.sidebar.width { - if let Some(item) = hit_test_sidebar(app, col, row, &layout) { - if let SidebarItem::Favorite(_, path) = item { + if let Some(item) = hit_test_sidebar(app, col, row, &layout) + && let SidebarItem::Favorite(_, path) = item { let menu = app.build_context_menu(col, row, true, Some(path)); app.context_menu = Some(menu); return; } - } return; } // Right-click on file - if let Some(idx) = hit_test_file_view(app, col, row, &layout) { - if !app.selected_indices.contains(&idx) { + if let Some(idx) = hit_test_file_view(app, col, row, &layout) + && !app.selected_indices.contains(&idx) { app.select_only(idx); } - } let menu = app.build_context_menu(col, row, false, None); app.context_menu = Some(menu); @@ -920,7 +943,7 @@ fn handle_mouse_move(app: &mut App, col: u16, row: u16) { // Settings hover: highlight rows when mouse moves over the dialog if app.dialog == DialogMode::Settings { let layout = ui::compute_layout(app); - let dlg = ui::settings_dialog_rect(layout.status_bar.x + layout.status_bar.width); + let dlg = ui::settings_dialog_rect(layout.status_bar.x + layout.status_bar.width, app.term_size.1); if col > dlg.x && col < dlg.x + dlg.width.saturating_sub(1) && row > dlg.y && row < dlg.y + dlg.height.saturating_sub(1) { @@ -981,9 +1004,9 @@ fn handle_mouse_move(app: &mut App, col: u16, row: u16) { } // When NewSubmenu is highlighted, check if the mouse moves into the flyout - if let Some(ns_pos) = menu.items.iter().position(|i| matches!(i, ContextMenuItem::NewSubmenu)) { - if menu.selected == ns_pos { - let (term_w, _) = crossterm::terminal::size().unwrap_or((120, 40)); + if let Some(ns_pos) = menu.items.iter().position(|i| matches!(i, ContextMenuItem::NewSubmenu)) + && menu.selected == ns_pos { + let (term_w, _) = app.term_size; let sub_x = (mx + MENU_W).min(term_w.saturating_sub(SUB_W)); let sub_y = my + 1 + ns_pos as u16; @@ -994,10 +1017,8 @@ fn handle_mouse_move(app: &mut App, col: u16, row: u16) { if row > sub_y && row < sub_y + 3 { menu.sub_selected = Some((row - sub_y - 1) as usize); } - return; } } - } } /// Handle mouse drag for text selection in the path input bar. @@ -1028,7 +1049,7 @@ fn handle_mouse_drag(app: &mut App, col: u16, row: u16) { let click_offset = (col - text_start_x) as usize; let char_count = app.path_input.chars().count(); let avail = inner_w.saturating_sub(hint_width + 1 + 6).saturating_sub(label_display_w) as usize; - let scroll_start = if app.path_input_cursor > avail { app.path_input_cursor - avail } else { 0 }; + let scroll_start = app.path_input_cursor.saturating_sub(avail); app.path_input_cursor = (scroll_start + click_offset).min(char_count); } } diff --git a/src/file_ops.rs b/src/file_ops.rs index 41ea7ff..971ca82 100644 --- a/src/file_ops.rs +++ b/src/file_ops.rs @@ -30,19 +30,24 @@ impl FileEntry { .unwrap_or("") .to_lowercase(); match ext.as_str() { + // OBS: alla emoji-ikoner MÅSTE vara East-Asian-Width Wide + // (mäts som 2 kolumner av unicode-width). Tecken med + // EAW=Neutral/Ambiguous (t.ex. 🖼 U+1F5BC, ⚙ U+2699) + // mäts som 1 men ritas 2 breda av emoji-fonter → + // raden förskjuts åt höger. Se icon_widths-testet. "rs" => "🦀", - "png" | "jpg" | "jpeg" | "gif" | "bmp" | "svg" | "webp" | "ico" => "🖼", + "png" | "jpg" | "jpeg" | "gif" | "bmp" | "svg" | "webp" | "ico" => "📷", "mp4" | "mkv" | "avi" | "mov" | "webm" => "🎬", "mp3" | "flac" | "ogg" | "wav" | "aac" => "🎵", "zip" | "tar" | "gz" | "bz2" | "xz" | "7z" | "rar" => "📦", "pdf" => "📕", "txt" | "md" | "log" => "📄", - "json" | "yaml" | "yml" | "toml" | "xml" => "⚙", + "json" | "yaml" | "yml" | "toml" | "xml" => "🔩", "sh" | "bash" | "zsh" | "fish" => "🐚", "py" => "🐍", "js" | "ts" => "📜", "c" | "cpp" | "h" | "hpp" => "🔧", - "exe" | "bin" | "out" | "run" => "⚙", + "exe" | "bin" | "out" | "run" => "⚡", _ => "📎", } } @@ -294,3 +299,56 @@ pub fn get_root_dirs() -> Vec { drives } } + +#[cfg(test)] +mod tests { + use super::*; + use unicode_width::UnicodeWidthStr; + + fn entry(name: &str, ft: FileType) -> FileEntry { + FileEntry { + name: name.to_string(), + path: PathBuf::from(name), + file_type: ft, + size: 0, + is_hidden: false, + } + } + + /// Alla emoji-ikoner måste vara East-Asian-Width Wide (mäts som + /// 2 kolumner av unicode-width). En ikon med EAW=Neutral/Ambiguous + /// (t.ex. 🖼 U+1F5BC eller ⚙ U+2699) mäts som 1 men ritas 2 bred av + /// emoji-fonter — då förskjuts allt efter ikonen åt höger i UI:t. + #[test] + fn emoji_icons_are_wide() { + let mut samples: Vec = vec![ + entry("dir", FileType::Directory), + entry("link", FileType::Symlink), + ]; + for ext in [ + "rs", "png", "mp4", "mp3", "zip", "pdf", "txt", "json", "sh", + "py", "js", "c", "exe", "unknown", + ] { + samples.push(entry(&format!("f.{}", ext), FileType::File)); + } + for e in &samples { + let icon = e.icon(); + assert_eq!( + UnicodeWidthStr::width(icon), 2, + "ikonen {:?} (för {:?}) är inte EAW Wide — den kommer \ + förskjuta raden åt höger i terminaler med emoji-font", + icon, e.name + ); + } + } + + /// ASCII-fallback-ikonerna ska alltid vara exakt 3 tecken ([/], + /// [s], ...) så att kolumnerna ligger fast i ASCII-läge. + #[test] + fn ascii_icons_are_three_cols() { + let e = entry("f.rs", FileType::File); + assert_eq!(UnicodeWidthStr::width(e.icon_ascii()), 3); + let d = entry("dir", FileType::Directory); + assert_eq!(UnicodeWidthStr::width(d.icon_ascii()), 3); + } +} diff --git a/src/main.rs b/src/main.rs index 07bda4d..6e46345 100644 --- a/src/main.rs +++ b/src/main.rs @@ -1,6 +1,8 @@ mod app; mod config; mod file_ops; +mod theme; +mod tui_wm; mod ui; mod events; diff --git a/src/theme.rs b/src/theme.rs new file mode 100644 index 0000000..b8b0d5e --- /dev/null +++ b/src/theme.rs @@ -0,0 +1,95 @@ +//! Centralt tema för TUI-FM. +//! +//! ALLA färger och återkommande stilar definieras här — ändra utseendet +//! på hela appen från en enda fil. UI-koden ska aldrig hårdkoda +//! `Color::`-literaler för sådant som temat täcker. +//! +//! OBS: använd bara tecken med entydig bredd i UI:t — se +//! `file_ops::tests::emoji_icons_are_wide`. + +use ratatui::style::{Color, Modifier, Style}; +use ratatui::widgets::{Block, BorderType, Borders}; + +// ─── Palett ────────────────────────────────────────────────────────────── +/// Fokusmarkering och markerade rader +pub const ACCENT: Color = Color::Cyan; +/// Rubriker och interaktiva knappar +pub const HEADER: Color = Color::Yellow; +/// Navigering ("Gå upp" m.m.) +pub const NAV: Color = Color::Magenta; +/// Positiv handling (skapa, lägg till) +pub const OK: Color = Color::Green; +/// Destruktiv handling (ta bort, stäng) +pub const DANGER: Color = Color::Red; +/// Nedtonad text: hints, avgränsare, ofokuserade ramar +pub const DIM: Color = Color::DarkGray; +/// Normal text +pub const TEXT: Color = Color::White; + +// ─── Stilar ────────────────────────────────────────────────────────────── +pub fn header() -> Style { + Style::default().fg(HEADER).add_modifier(Modifier::BOLD) +} + +pub fn nav() -> Style { + Style::default().fg(NAV).add_modifier(Modifier::BOLD) +} + +pub fn hint() -> Style { + Style::default().fg(DIM) +} + +pub fn button() -> Style { + Style::default().fg(OK).add_modifier(Modifier::BOLD) +} + +pub fn selected_row() -> Style { + Style::default() + .fg(Color::Black) + .bg(ACCENT) + .add_modifier(Modifier::BOLD) +} + +pub fn current_item() -> Style { + Style::default().fg(ACCENT).add_modifier(Modifier::BOLD) +} + +pub fn text() -> Style { + Style::default().fg(TEXT) +} + +// ─── Block/ramar ───────────────────────────────────────────────────────── +/// Standardram för en yta: rundade hörn, accentfärg vid fokus. +pub fn pane_block(title: &str, focused: bool) -> Block<'static> { + let border = if focused { + Style::default().fg(ACCENT) + } else { + Style::default().fg(DIM) + }; + let block = Block::default() + .borders(Borders::ALL) + .border_type(BorderType::Rounded) + .border_style(border); + if title.is_empty() { + block + } else { + block + .title(format!(" {} ", title)) + .title_style(Style::default().fg(TEXT).add_modifier(Modifier::BOLD)) + } +} + +/// Ram för dialoger/popups: rundade hörn, färgad ram + rubrik. +pub fn dialog_block_colored(title: &str, color: Color) -> Block<'static> { + Block::default() + .title(format!(" {} ", title)) + .title_style(Style::default().fg(color).add_modifier(Modifier::BOLD)) + .borders(Borders::ALL) + .border_type(BorderType::Rounded) + .border_style(Style::default().fg(color)) +} + +/// Standarddialog: accentfärgad. +pub fn dialog_block(title: &str) -> Block<'static> { + dialog_block_colored(title, ACCENT) +} diff --git a/src/tui_wm.rs b/src/tui_wm.rs new file mode 100644 index 0000000..cbbcabf --- /dev/null +++ b/src/tui_wm.rs @@ -0,0 +1,69 @@ +//! Integration med TUI-WM:s socket-API. +//! +//! När TUI-FM körs i ett TUI-WM-fönster sätts miljövariabeln +//! `TUI_WM_SOCKET`. Via den kan filer öppnas i egna fönster med +//! användarens filassociationer/standard-editor (TUI-WM visar en +//! "Öppna med"-dialog för okända format). + +use std::path::Path; + +/// Kör TUI-FM inne i TUI-WM? +pub fn available() -> bool { + std::env::var("TUI_WM_SOCKET").is_ok() +} + +/// Be TUI-WM öppna en fil. Blockerar bara för handskakningen. +pub fn open_file(path: &Path) -> Result<(), String> { + let sock = std::env::var("TUI_WM_SOCKET").map_err(|_| "TUI_WM_SOCKET saknas".to_string())?; + open_via_socket(&sock, path) +} + +#[cfg(unix)] +fn open_via_socket(sock: &str, path: &Path) -> Result<(), String> { + use std::io::{Read, Write}; + use std::os::unix::net::UnixStream; + + let mut stream = UnixStream::connect(sock).map_err(|e| format!("anslutning: {}", e))?; + stream + .set_read_timeout(Some(std::time::Duration::from_secs(2))) + .ok(); + + let write_msg = |s: &mut UnixStream, v: serde_json::Value| -> Result<(), String> { + let data = serde_json::to_vec(&v).map_err(|e| e.to_string())?; + s.write_all(&(data.len() as u32).to_be_bytes()) + .and_then(|_| s.write_all(&data)) + .map_err(|e| e.to_string()) + }; + let read_msg = |s: &mut UnixStream| -> Result { + let mut len = [0u8; 4]; + s.read_exact(&mut len).map_err(|e| e.to_string())?; + let mut buf = vec![0u8; u32::from_be_bytes(len) as usize]; + s.read_exact(&mut buf).map_err(|e| e.to_string())?; + serde_json::from_slice(&buf).map_err(|e| e.to_string()) + }; + + write_msg( + &mut stream, + serde_json::json!({"type": "hello", "role": "app", "width": 0, "height": 0}), + )?; + let hello = read_msg(&mut stream)?; + if hello.get("type").and_then(|t| t.as_str()) != Some("hello_ok") { + return Err("oväntat svar från TUI-WM".to_string()); + } + write_msg( + &mut stream, + serde_json::json!({ + "type": "open_file", + "path": path.to_string_lossy(), + "request_id": "tui-fm-open", + }), + )?; + // Vänta in kvittensen så meddelandet hinner behandlas + let _ = read_msg(&mut stream); + Ok(()) +} + +#[cfg(not(unix))] +fn open_via_socket(_sock: &str, _path: &Path) -> Result<(), String> { + Err("socket-öppning stöds inte på denna plattform ännu".to_string()) +} diff --git a/src/ui.rs b/src/ui.rs index 39d5f5b..11f374d 100644 --- a/src/ui.rs +++ b/src/ui.rs @@ -12,6 +12,7 @@ use ratatui::{ use unicode_width::UnicodeWidthStr; use crate::app::{App, ContextMenuItem, DialogMode, Focus}; +use crate::theme; use crate::file_ops::{self, format_size}; /// Compute the display width of a string, accounting for emojis (2 columns) and @@ -56,9 +57,11 @@ pub struct LayoutAreas { pub path_input: Rect, } -/// Compute the layout areas for hit testing (without a frame) -pub fn compute_layout(_app: &App) -> LayoutAreas { - let (width, height) = crossterm::terminal::size().unwrap_or((120, 40)); +/// Compute the layout areas for hit testing (without a frame). +/// Uses the last size seen by draw() instead of querying the terminal, +/// so it stays correct inside nested terminals. +pub fn compute_layout(app: &App) -> LayoutAreas { + let (width, height) = app.term_size; let full = Rect::new(0, 0, width, height); split_layout(full) } @@ -97,6 +100,7 @@ fn split_layout(area: Rect) -> LayoutAreas { pub fn draw(f: &mut Frame, app: &mut App) { let area = f.area(); + app.term_size = (area.width, area.height); let layout = split_layout(area); draw_sidebar(f, app, layout.sidebar); @@ -121,24 +125,15 @@ pub fn draw(f: &mut Frame, app: &mut App) { fn draw_sidebar(f: &mut Frame, app: &App, area: Rect) { let focused = app.focus == Focus::Sidebar; - let border_style = if focused { - Style::default().fg(Color::Cyan) - } else { - Style::default().fg(Color::DarkGray) - }; - - let block = Block::default() - .title(" Navigering ") - .borders(Borders::ALL) - .border_style(border_style); + let block = theme::pane_block("Navigering", focused); let inner = block.inner(area); f.render_widget(block, area); - let header_s = Style::default().fg(Color::Yellow).add_modifier(Modifier::BOLD); - let go_up_s = Style::default().fg(Color::Magenta).add_modifier(Modifier::BOLD); - let hint_s = Style::default().fg(Color::DarkGray); - let btn_s = Style::default().fg(Color::Green).add_modifier(Modifier::BOLD); + let header_s = theme::header(); + let go_up_s = theme::nav(); + let hint_s = theme::hint(); + let btn_s = theme::button(); let sep_str: String = if app.unicode_support { "\u{2500}".repeat(inner.width as usize) @@ -153,9 +148,12 @@ fn draw_sidebar(f: &mut Frame, app: &App, area: Rect) { // ── Go-up button ────────────────────────────────────────────────────────── if app.unicode_support { + // ↑ U+2191 är ett rent textglyf (ingen emoji-presentation) → + // ritas alltid 1 kolumn brett, till skillnad från ⬆ U+2B06 som + // emoji-fonter ritar 2 brett fast unicode-width mäter 1. lines.push(Line::from(vec![ Span::raw(" "), - Span::styled("⬆", go_up_s), + Span::styled("↑", go_up_s), Span::styled(" Gå upp", go_up_s), ])); } else { @@ -181,11 +179,7 @@ fn draw_sidebar(f: &mut Frame, app: &App, area: Rect) { .map(|n| n.to_string_lossy().to_string()) .unwrap_or_else(|| fav.clone()); let is_current = fav == &app.current_path.to_string_lossy().to_string(); - let style = if is_current { - Style::default().fg(Color::Cyan).add_modifier(Modifier::BOLD) - } else { - Style::default().fg(Color::White) - }; + let style = if is_current { theme::current_item() } else { theme::text() }; // ★ U+2605 is East-Asian-Width Narrow → 1 display column, safe for alignment let star = if app.unicode_support { "\u{2605}" } else { "*" }; let icon_w = display_width(star); @@ -280,11 +274,6 @@ fn draw_sidebar(f: &mut Frame, app: &App, area: Rect) { fn draw_file_view(f: &mut Frame, app: &App, area: Rect) { let focused = app.focus == Focus::FileView; - let border_style = if focused { - Style::default().fg(Color::Cyan) - } else { - Style::default().fg(Color::DarkGray) - }; let rows: Vec = app .entries @@ -321,14 +310,11 @@ fn draw_file_view(f: &mut Frame, app: &App, area: Rect) { Cell::from("Namn"), Cell::from("Storlek"), ]) - .style(Style::default().fg(Color::Yellow).add_modifier(Modifier::BOLD | Modifier::UNDERLINED)) + .style(theme::header().add_modifier(Modifier::UNDERLINED)) .height(1); - let path_title = format!(" {} ", app.current_path.to_string_lossy()); - let block = Block::default() - .title(path_title) - .borders(Borders::ALL) - .border_style(border_style); + let path_title = app.current_path.to_string_lossy(); + let block = theme::pane_block(&path_title, focused); let widths = [Constraint::Min(10), Constraint::Length(10)]; @@ -380,13 +366,7 @@ fn draw_file_view(f: &mut Frame, app: &App, area: Rect) { fn draw_bottom_bar(f: &mut Frame, app: &App, area: Rect) { let focused = app.focus == Focus::PathInput; - let outer_block = Block::default() - .borders(Borders::ALL) - .border_style(if focused { - Style::default().fg(Color::Cyan) - } else { - Style::default().fg(Color::DarkGray) - }); + let outer_block = theme::pane_block("", focused); let inner = outer_block.inner(area); f.render_widget(outer_block, area); @@ -441,7 +421,7 @@ fn draw_bottom_bar(f: &mut Frame, app: &App, area: Rect) { ]; if focused { - let scroll_start = if cursor_pos > avail { cursor_pos - avail } else { 0 }; + let scroll_start = cursor_pos.saturating_sub(avail); if let Some((sel_start, sel_end)) = sel_range { // Render with selection highlight @@ -493,8 +473,11 @@ fn draw_bottom_bar(f: &mut Frame, app: &App, area: Rect) { // Gear button at bottom-right let gear_x = inner.x + inner.width.saturating_sub(5); + // ≡ U+2261 är ett textglyf som alla monospace-fonter ritar 1 brett; + // ⚙ U+2699 (EAW=Neutral) ritas 2 brett av emoji-fonter och förskjuter + // raden åt höger. let (gear_label, gear_w) = if app.unicode_support { - (" \u{2699} ", 4u16) + (" \u{2261} ", 4u16) } else { (" [S] ", 5u16) }; @@ -514,7 +497,7 @@ fn scroll_input_text(text: &str, cursor: usize, avail: usize) -> (String, usize) if char_count <= avail { return (text.to_string(), cursor); } - let start = if cursor > avail { cursor - avail } else { 0 }; + let start = cursor.saturating_sub(avail); let end = (start + avail).min(char_count); let slice: String = text.chars().skip(start).take(end - start).collect(); let cursor_in_display = cursor.saturating_sub(start); @@ -535,10 +518,7 @@ fn draw_context_menu(f: &mut Frame, app: &App, menu: crate::app::ContextMenu) { let menu_area = Rect::new(x, y, menu_width, menu_height); f.render_widget(Clear, menu_area); - let block = Block::default() - .title(" Meny ") - .borders(Borders::ALL) - .border_style(Style::default().fg(Color::Cyan)); + let block = theme::dialog_block("Meny"); let inner = block.inner(menu_area); f.render_widget(block, menu_area); @@ -553,6 +533,8 @@ fn draw_context_menu(f: &mut Frame, app: &App, menu: crate::app::ContextMenu) { // Use 2-char ASCII icon codes — emoji (U+1F000+) are 2-wide in terminals and // cause ghost characters and column drift when partially overwritten. let (icon, label, item_style): (&str, &str, Style) = match item { + ContextMenuItem::Open => + ("O ", "Öppna", Style::default().add_modifier(Modifier::BOLD)), ContextMenuItem::Copy => ("C ", "Kopiera", Style::default()), ContextMenuItem::Cut => @@ -616,9 +598,7 @@ fn draw_context_menu(f: &mut Frame, app: &App, menu: crate::app::ContextMenu) { let sub_x = (x + menu_width).min(term_area.width.saturating_sub(sub_w)); let sub_area = Rect::new(sub_x, new_sub_row, sub_w, 4); f.render_widget(Clear, sub_area); - let sub_block = Block::default() - .borders(Borders::ALL) - .border_style(Style::default().fg(Color::Green)); + let sub_block = theme::dialog_block_colored("", theme::OK); let sub_inner = sub_block.inner(sub_area); f.render_widget(sub_block, sub_area); @@ -644,17 +624,16 @@ fn draw_context_menu(f: &mut Frame, app: &App, menu: crate::app::ContextMenu) { } /// Compute the settings dialog rect anchored to the bottom-right corner. -pub fn settings_dialog_rect(right_edge: u16) -> ratatui::layout::Rect { +pub fn settings_dialog_rect(right_edge: u16, term_h: u16) -> ratatui::layout::Rect { let dlg_width: u16 = 72; let dlg_height: u16 = 26; let x = right_edge.saturating_sub(dlg_width + 1); - let (_, term_h) = crossterm::terminal::size().unwrap_or((120, 40)); let y = term_h.saturating_sub(dlg_height + 3); Rect::new(x, y, dlg_width, dlg_height) } fn draw_settings(f: &mut Frame, app: &App, right_edge: u16) { - let area = settings_dialog_rect(right_edge); + let area = settings_dialog_rect(right_edge, f.area().height); let term_area = f.area(); let area = Rect::new( area.x.min(term_area.width.saturating_sub(area.width)), @@ -670,11 +649,7 @@ fn draw_settings(f: &mut Frame, app: &App, right_edge: u16) { } else { " [S] Inställningar " }; - let block = Block::default() - .title(title) - .title_style(Style::default().fg(Color::Yellow).add_modifier(Modifier::BOLD)) - .borders(Borders::ALL) - .border_style(Style::default().fg(Color::Yellow)); + let block = theme::dialog_block_colored(title.trim(), theme::HEADER); let inner = block.inner(area); f.render_widget(block, area); @@ -895,10 +870,7 @@ fn draw_input_dialog(f: &mut Frame, title: &str, current: &str, area: Rect) { let dlg = Rect::new(x, y, dlg_width, dlg_height); f.render_widget(Clear, dlg); - let block = Block::default() - .title(format!(" {} ", title)) - .borders(Borders::ALL) - .border_style(Style::default().fg(Color::Cyan)); + let block = theme::dialog_block(title); let inner = block.inner(dlg); f.render_widget(block, dlg); @@ -936,10 +908,7 @@ fn draw_properties(f: &mut Frame, info: crate::app::PropertiesInfo, area: Rect) let dlg = Rect::new(x, y, dlg_width, dlg_height); f.render_widget(Clear, dlg); - let block = Block::default() - .title(" Egenskaper ") - .borders(Borders::ALL) - .border_style(Style::default().fg(Color::Cyan)); + let block = theme::dialog_block("Egenskaper"); let inner = block.inner(dlg); f.render_widget(block, dlg); @@ -997,11 +966,7 @@ fn draw_error_dialog(f: &mut Frame, msg: &str, area: Rect) { let dlg = Rect::new(x, y, dlg_width, dlg_height); f.render_widget(Clear, dlg); - let block = Block::default() - .title(" Fel ") - .title_style(Style::default().fg(Color::Red).add_modifier(Modifier::BOLD)) - .borders(Borders::ALL) - .border_style(Style::default().fg(Color::Red)); + let block = theme::dialog_block_colored("Fel", theme::DANGER); let inner = block.inner(dlg); f.render_widget(block, dlg); @@ -1036,11 +1001,7 @@ fn draw_confirm_dialog(f: &mut Frame, msg: &str, area: Rect) { let dlg = Rect::new(x, y, dlg_width, dlg_height); f.render_widget(Clear, dlg); - let block = Block::default() - .title(" Bekräfta ") - .title_style(Style::default().fg(Color::Yellow).add_modifier(Modifier::BOLD)) - .borders(Borders::ALL) - .border_style(Style::default().fg(Color::Yellow)); + let block = theme::dialog_block_colored("Bekräfta", theme::HEADER); let inner = block.inner(dlg); f.render_widget(block, dlg);