diff --git a/.github/workflows/release_binaries.yml b/.github/workflows/release_binaries.yml index 861e614..a0bd6c5 100644 --- a/.github/workflows/release_binaries.yml +++ b/.github/workflows/release_binaries.yml @@ -10,19 +10,15 @@ env: jobs: release-linux: + runs-on: ubuntu-latest strategy: matrix: platform: - - release_for: ARM64 Linux - target: aarch64-unknown-linux-gnu + - target: aarch64-unknown-linux-gnu binary: "tgh-linux-arm64" - host: "ubuntu-latest" - - release_for: X86_64 Linux - target: x86_64-unknown-linux-gnu + - target: x86_64-unknown-linux-gnu binary: "tgh-linux-x64" - host: "ubuntu-latest" - runs-on: ubuntu-latest steps: - name: Checkout uses: actions/checkout@v2 @@ -41,19 +37,18 @@ jobs: run: cross build --release --target ${{ matrix.platform.target }} - name: Compress binary - run: tar -czf target/${{ matrix.platform.binary }}.tar.gz -C target/${{ matrix.platform.target }}/release tgh + run: tar -czf target/${{ matrix.platform.target }}.tar.gz -C target/${{ matrix.platform.target }}/release tgh - name: Upload binaries to release uses: svenstaro/upload-release-action@v2 with: repo_token: ${{ secrets.GITHUB_TOKEN }} - file: target/${{ matrix.platform.binary }}.tar.gz - asset_name: ${{ matrix.platform.binary }}.tar.gz + file: target/${{ matrix.platform.target }}.tar.gz + asset_name: tgh-${{ matrix.platform.target }}.tar.gz tag: ${{ github.ref }} release-windows: runs-on: windows-latest - steps: - name: Checkout uses: actions/checkout@v2 @@ -67,28 +62,22 @@ jobs: - name: Build run: cargo build --release + - name: Compress binary + run: Compress-Archive -Path target/release/tgh.exe -DestinationPath target/tgh-pc-windows-msvc.zip + - name: Upload binaries to release uses: svenstaro/upload-release-action@v2 with: repo_token: ${{ secrets.GITHUB_TOKEN }} - file: target/release/tgh.exe - asset_name: tgh-windows-x64.exe + file: target/tgh-pc-windows-msvc.zip + asset_name: tgh-x86_64-pc-windows-msvc.zip tag: ${{ github.ref }} release-macos: + runs-on: macos-latest strategy: matrix: - platform: - - release_for: X86_64 macOS - target: x86_64-apple-darwin - binary: "tgh-darwin-x64" - host: "macos-latest" - - release_for: ARM64 macOS - target: aarch64-apple-darwin - binary: "tgh-darwin-arm64" - host: "macos-latest" - - runs-on: macos-latest + target: [x86_64-apple-darwin, aarch64-apple-darwin] steps: - name: Checkout uses: actions/checkout@v2 @@ -98,21 +87,18 @@ jobs: with: toolchain: stable override: true - target: ${{ matrix.platform.target }} - - - name: Setup cross-compilation - run: cargo install -f cross + target: ${{ matrix.target }} - name: Build - run: cross build --release --target ${{ matrix.platform.target }} + run: cargo build --release --target ${{ matrix.target }} - name: Compress binary - run: tar -czf target/${{ matrix.platform.binary }}.tar.gz -C target/${{ matrix.platform.target }}/release tgh + run: tar -czf target/${{ matrix.target }}.tar.gz -C target/${{ matrix.target }}/release tgh - name: Upload binaries to release uses: svenstaro/upload-release-action@v2 with: repo_token: ${{ secrets.GITHUB_TOKEN }} - file: target/${{ matrix.platform.binary }}.tar.gz - asset_name: ${{ matrix.platform.binary }}.tar.gz + file: target/${{ matrix.target }}.tar.gz + asset_name: tgh-${{ matrix.target }}.tar.gz tag: ${{ github.ref }} diff --git a/Cargo.lock b/Cargo.lock index eaa9015..248577d 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -1,6 +1,6 @@ # This file is automatically @generated by Cargo. # It is not intended for manual editing. -version = 3 +version = 4 [[package]] name = "addr2line" @@ -26,6 +26,15 @@ dependencies = [ "memchr", ] +[[package]] +name = "android_system_properties" +version = "0.1.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "819e7219dbd41043ac279b19830f2efc897156490d7fd6ea916720117ee66311" +dependencies = [ + "libc", +] + [[package]] name = "anstream" version = "0.6.13" @@ -88,7 +97,7 @@ dependencies = [ "objc-foundation", "objc_id", "parking_lot", - "thiserror", + "thiserror 1.0.57", "windows-sys 0.48.0", "x11rb", ] @@ -120,6 +129,18 @@ version = "0.21.7" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "9d297deb1925b89f2ccc13d7635fa0714f12c87adce1c75356b39ca9b7178567" +[[package]] +name = "base64" +version = "0.22.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "72b3254f16251a8381aa12e40e3c4d2f0199f8c6508fbecb9d91f575e0fbb8c6" + +[[package]] +name = "base64ct" +version = "1.8.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7d809780667f4410e7c41b07f52439b94d2bdf8528eeedc287fa38d3b7f95d82" + [[package]] name = "bitflags" version = "1.3.2" @@ -138,6 +159,15 @@ version = "0.1.6" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "0d8c1fef690941d3e7788d328517591fecc684c084084702d6ff1641e993699a" +[[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.15.3" @@ -174,6 +204,19 @@ version = "1.0.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "baf1de4339761588bc0619e3cbc0120ee582ebb74b53b4efbf79117bd2da40fd" +[[package]] +name = "chrono" +version = "0.4.42" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "145052bdd345b87320e369255277e3fb5152762ad123a901ef5c262dd38fe8d2" +dependencies = [ + "iana-time-zone", + "js-sys", + "num-traits", + "wasm-bindgen", + "windows-link", +] + [[package]] name = "clap" version = "4.5.2" @@ -195,7 +238,7 @@ dependencies = [ "clap_lex", "strsim", "unicase", - "unicode-width", + "unicode-width 0.1.11", ] [[package]] @@ -207,7 +250,7 @@ dependencies = [ "heck", "proc-macro2", "quote", - "syn 2.0.52", + "syn 2.0.114", ] [[package]] @@ -237,6 +280,25 @@ version = "1.0.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "acbf1af155f9b9ef647e42cdc158db4b64a1b61f743629225fde6f3e0be2a7c7" +[[package]] +name = "console" +version = "0.15.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0e1f83fc076bd6dd27517eacdf25fef6c4dfe5f1d7448bafaaf3a26f13b5e4eb" +dependencies = [ + "encode_unicode", + "lazy_static", + "libc", + "unicode-width 0.1.11", + "windows-sys 0.52.0", +] + +[[package]] +name = "const-oid" +version = "0.9.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c2459377285ad874054d797f3ccebf984978aa39129f6eafde5cdc8315b612f8" + [[package]] name = "core-foundation" version = "0.9.4" @@ -277,6 +339,15 @@ dependencies = [ "libc", ] +[[package]] +name = "cpufeatures" +version = "0.2.17" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "59ed5838eebb26a2bb2e58f6d5b5316989ae9d08bab10e0e6d103e656d1b0280" +dependencies = [ + "libc", +] + [[package]] name = "crc32fast" version = "1.4.0" @@ -286,6 +357,12 @@ dependencies = [ "cfg-if", ] +[[package]] +name = "crossbeam-utils" +version = "0.8.21" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d0a5c400df2834b80a4c3327b3aad3a4c4cd4de0629063962b03235697506a28" + [[package]] name = "crossterm" version = "0.25.0" @@ -327,12 +404,109 @@ 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 = "curve25519-dalek" +version = "4.1.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "97fb8b7c4503de7d6ae7b42ab72a5a59857b4c937ec27a3d4539dba95b5ab2be" +dependencies = [ + "cfg-if", + "cpufeatures", + "curve25519-dalek-derive", + "digest", + "fiat-crypto", + "rustc_version", + "subtle", + "zeroize", +] + +[[package]] +name = "curve25519-dalek-derive" +version = "0.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f46882e17999c6cc590af592290432be3bce0428cb0d5f8b6715e4dc7b383eb3" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.114", +] + +[[package]] +name = "der" +version = "0.7.10" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e7c1832837b905bbfb5101e07cc24c8deddf52f93225eee6ead5f4d63d53ddcb" +dependencies = [ + "const-oid", + "zeroize", +] + +[[package]] +name = "deranged" +version = "0.5.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ececcb659e7ba858fb4f10388c250a7252eb0a27373f1a72b8748afdd248e587" +dependencies = [ + "powerfmt", +] + +[[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]] name = "dyn-clone" version = "1.0.17" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "0d6ef0072f8a535281e4876be788938b528e9a1d43900b82c2569af7da799125" +[[package]] +name = "ed25519" +version = "2.2.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "115531babc129696a58c64a4fef0a8bf9e9698629fb97e9e40767d235cfbcd53" +dependencies = [ + "pkcs8", + "signature", +] + +[[package]] +name = "ed25519-dalek" +version = "2.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "70e796c081cee67dc755e1a36a0a172b897fab85fc3f6bc48307991f64e4eca9" +dependencies = [ + "curve25519-dalek", + "ed25519", + "serde", + "sha2", + "signature", + "subtle", + "zeroize", +] + +[[package]] +name = "encode_unicode" +version = "0.3.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a357d28ed41a50f9c765dbfe56cbc04a64e53e5fc58ba79fbc34c10ef3df831f" + [[package]] name = "encoding_rs" version = "0.8.33" @@ -366,9 +540,9 @@ checksum = "a0474425d51df81997e2f90a21591180b38eccf27292d755f3e30750225c175b" [[package]] name = "fastrand" -version = "2.0.1" +version = "2.3.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "25cbce373ec4653f1a01a31e8a5e5ec0c622dc27ff9c4e6606eefef5cbbed4a5" +checksum = "37909eebbb50d72f9059c3b6d82c0463f2ff062c9e95845c43a6c9c0355411be" [[package]] name = "fdeflate" @@ -379,6 +553,24 @@ dependencies = [ "simd-adler32", ] +[[package]] +name = "fiat-crypto" +version = "0.2.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "28dea519a9695b9977216879a3ebfddf92f1c08c05d984f8996aecd6ecdc811d" + +[[package]] +name = "filetime" +version = "0.2.26" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bc0505cd1b6fa6580283f6bdf70a73fcf4aba1184038c90902b92b3dd0df63ed" +dependencies = [ + "cfg-if", + "libc", + "libredox", + "windows-sys 0.60.2", +] + [[package]] name = "flate2" version = "1.0.28" @@ -422,7 +614,7 @@ checksum = "1a5c6c585bc94aaf2c7b51dd4c2ba22680844aba4c687be581871a6f518c5742" dependencies = [ "proc-macro2", "quote", - "syn 2.0.52", + "syn 2.0.114", ] [[package]] @@ -461,6 +653,12 @@ version = "0.3.30" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "dfc6580bb841c5a68e9ef15c77ccc837b40a7504914d52e47b8b0e9bbda25a1d" +[[package]] +name = "futures-io" +version = "0.3.31" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9e5c1b78ca4aae1ac06c48a526a655760685149f0d465d21f37abfe57ce075c6" + [[package]] name = "futures-sink" version = "0.3.30" @@ -480,9 +678,12 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "3d6401deb83407ab3da39eba7e33987a73c3df0c82b4bb5813ee871c19c41d48" dependencies = [ "futures-core", + "futures-io", "futures-task", + "memchr", "pin-project-lite", "pin-utils", + "slab", ] [[package]] @@ -503,6 +704,16 @@ dependencies = [ "byteorder", ] +[[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 = "gethostname" version = "0.4.3" @@ -513,6 +724,17 @@ dependencies = [ "windows-targets 0.48.5", ] +[[package]] +name = "getrandom" +version = "0.2.17" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ff2abc00be7fca6ebc474524697ae276ad847ad0a6b3faa4bcb027e9a4614ad0" +dependencies = [ + "cfg-if", + "libc", + "wasi", +] + [[package]] name = "gimli" version = "0.28.1" @@ -636,6 +858,30 @@ dependencies = [ "tokio-native-tls", ] +[[package]] +name = "iana-time-zone" +version = "0.1.64" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "33e57f83510bb73707521ebaffa789ec8caf86f9657cad665b092b581d40e9fb" +dependencies = [ + "android_system_properties", + "core-foundation-sys", + "iana-time-zone-haiku", + "js-sys", + "log", + "wasm-bindgen", + "windows-core", +] + +[[package]] +name = "iana-time-zone-haiku" +version = "0.1.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f31827a206f56af32e590ba56d5d2d085f558508192593743f16b2306495269f" +dependencies = [ + "cc", +] + [[package]] name = "idna" version = "0.5.0" @@ -670,6 +916,19 @@ dependencies = [ "hashbrown", ] +[[package]] +name = "indicatif" +version = "0.17.11" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "183b3088984b400f4cfac3620d5e076c84da5364016b4f49473de574b2586235" +dependencies = [ + "console", + "number_prefix", + "portable-atomic", + "unicode-width 0.2.2", + "web-time", +] + [[package]] name = "inquire" version = "0.7.0" @@ -684,7 +943,7 @@ dependencies = [ "newline-converter", "once_cell", "unicode-segmentation", - "unicode-width", + "unicode-width 0.1.11", ] [[package]] @@ -722,9 +981,20 @@ checksum = "e2abad23fbc42b3700f2f279844dc832adb2b2eb069b2df918f455c4e18cc646" [[package]] name = "libc" -version = "0.2.153" +version = "0.2.180" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9c198f91728a82281a64e1f4f9eeb25d82cb32a5de251c6bd1b5154d63a8e7bd" +checksum = "bcc35a38544a891a5f7c865aca548a982ccb3b8650a5b06d0fd33a10283c56fc" + +[[package]] +name = "libredox" +version = "0.1.12" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3d0b95e02c851351f877147b7deea7b1afb1df71b63aa5f8270716e0c5720616" +dependencies = [ + "bitflags 2.4.2", + "libc", + "redox_syscall 0.7.0", +] [[package]] name = "linux-raw-sys" @@ -824,6 +1094,12 @@ dependencies = [ "unicode-segmentation", ] +[[package]] +name = "num-conv" +version = "0.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "51d515d32fb182ee37cda2ccdcb92950d6a3c2893aa280e540671c2cd0f3b1d9" + [[package]] name = "num-traits" version = "0.2.18" @@ -843,6 +1119,12 @@ dependencies = [ "libc", ] +[[package]] +name = "number_prefix" +version = "0.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "830b246a0e5f20af87141b25c173cd1b609bd7779a4617d6ec582abaf90870f3" + [[package]] name = "objc" version = "0.2.7" @@ -910,7 +1192,7 @@ checksum = "a948666b637a0f465e8564c73e89d4dde00d72d4d473cc972f390fc3dcee7d9c" dependencies = [ "proc-macro2", "quote", - "syn 2.0.52", + "syn 2.0.114", ] [[package]] @@ -959,7 +1241,7 @@ checksum = "4c42a9226546d68acdd9c0a280d17ce19bfe27a46bf68784e4066115788d008e" dependencies = [ "cfg-if", "libc", - "redox_syscall", + "redox_syscall 0.4.1", "smallvec", "windows-targets 0.48.5", ] @@ -982,6 +1264,16 @@ version = "0.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "8b870d8c151b6f2fb93e84a13146138f05d02ed11c7e7c54f8826aaaf7c9f184" +[[package]] +name = "pkcs8" +version = "0.10.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f950b2377845cebe5cf8b5165cb3cc1a5e0fa5cfa3e1f7f55707d8fd82e0a7b7" +dependencies = [ + "der", + "spki", +] + [[package]] name = "pkg-config" version = "0.3.30" @@ -1001,15 +1293,36 @@ dependencies = [ "miniz_oxide", ] +[[package]] +name = "portable-atomic" +version = "1.13.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f89776e4d69bb58bc6993e99ffa1d11f228b839984854c7daeb5d37f87cbe950" + +[[package]] +name = "powerfmt" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "439ee305def115ba05938db6eb1644ff94165c5ab5e9420d1c1bcedbba909391" + [[package]] name = "proc-macro2" -version = "1.0.78" +version = "1.0.105" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e2422ad645d89c99f8f3e6b88a9fdeca7fabeac836b1002371c4367c8f984aae" +checksum = "535d180e0ecab6268a3e718bb9fd44db66bbbc256257165fc699dadf70d16fe7" dependencies = [ "unicode-ident", ] +[[package]] +name = "quick-xml" +version = "0.23.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "11bafc859c6815fbaffbbbf4229ecb767ac913fecb27f9ad4343662e9ef099ea" +dependencies = [ + "memchr", +] + [[package]] name = "quote" version = "1.0.35" @@ -1019,6 +1332,15 @@ dependencies = [ "proc-macro2", ] +[[package]] +name = "rand_core" +version = "0.6.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ec0be4795e2f6a28069bec0b5ff3e2ac9bafc99e6a9a7dc3547996c5c816922c" +dependencies = [ + "getrandom", +] + [[package]] name = "redox_syscall" version = "0.4.1" @@ -1028,6 +1350,15 @@ dependencies = [ "bitflags 1.3.2", ] +[[package]] +name = "redox_syscall" +version = "0.7.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "49f3fe0889e69e2ae9e41f4d6c4c0181701d00e4697b356fb1f74173a5e0ee27" +dependencies = [ + "bitflags 2.4.2", +] + [[package]] name = "regex" version = "1.10.3" @@ -1063,7 +1394,7 @@ version = "0.11.24" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "c6920094eb85afde5e4a138be3f2de8bbdf28000f0029e72c45025a56b042251" dependencies = [ - "base64", + "base64 0.21.7", "bytes", "encoding_rs", "futures-core", @@ -1103,6 +1434,15 @@ version = "0.1.23" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "d626bb9dae77e28219937af045c257c28bfd3f69333c512553507f5f9798cb76" +[[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.31" @@ -1122,7 +1462,7 @@ version = "1.0.4" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "1c74cae0a4cf6ccbbf5f359f08efdf8ee7e1dc532573bf0db71968cb56b1448c" dependencies = [ - "base64", + "base64 0.21.7", ] [[package]] @@ -1175,6 +1515,45 @@ dependencies = [ "libc", ] +[[package]] +name = "self-replace" +version = "1.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "03ec815b5eab420ab893f63393878d89c90fdd94c0bcc44c07abb8ad95552fb7" +dependencies = [ + "fastrand", + "tempfile", + "windows-sys 0.52.0", +] + +[[package]] +name = "self_update" +version = "0.39.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1a34ad8e4a86884ab42e9b8690e9343abdcfe5fa38a0318cfe1565ba9ad437b4" +dependencies = [ + "hyper", + "indicatif", + "log", + "quick-xml", + "regex", + "reqwest", + "self-replace", + "semver", + "serde_json", + "tar", + "tempfile", + "urlencoding", + "zip", + "zipsign-api", +] + +[[package]] +name = "semver" +version = "1.0.27" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d767eb0aabc880b29956c35734170f26ed551a859dbd361d140cdbeca61ab1e2" + [[package]] name = "serde" version = "1.0.197" @@ -1192,7 +1571,7 @@ checksum = "7eb0b34b42edc17f6b7cac84a52a1c5f0e1bb2227e997ca9011ea3dd34e8610b" dependencies = [ "proc-macro2", "quote", - "syn 2.0.52", + "syn 2.0.114", ] [[package]] @@ -1218,6 +1597,17 @@ dependencies = [ "serde", ] +[[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.17" @@ -1248,6 +1638,16 @@ dependencies = [ "libc", ] +[[package]] +name = "signature" +version = "2.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "77549399552de45a898a580c1b41d445bf730df867cc44e6c0233bbc4b8329de" +dependencies = [ + "digest", + "rand_core", +] + [[package]] name = "simd-adler32" version = "0.3.7" @@ -1290,6 +1690,16 @@ dependencies = [ "strum", ] +[[package]] +name = "spki" +version = "0.7.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d91ed6c858b01f942cd56b37a94b3e0a1798290327d1236e4d9cf4eaca44d29d" +dependencies = [ + "base64ct", + "der", +] + [[package]] name = "strsim" version = "0.11.0" @@ -1318,6 +1728,12 @@ dependencies = [ "syn 1.0.109", ] +[[package]] +name = "subtle" +version = "2.6.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "13c2bddecc57b384dee18652358fb23172facb8a2c51ccc10d74c157bdea3292" + [[package]] name = "syn" version = "1.0.109" @@ -1331,9 +1747,9 @@ dependencies = [ [[package]] name = "syn" -version = "2.0.52" +version = "2.0.114" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b699d15b36d1f02c3e7c69f8ffef53de37aefae075d8488d4ba1a7788d574a07" +checksum = "d4d107df263a3013ef9b1879b0df87d706ff80f65a86ea879bd9c31f9b307c2a" dependencies = [ "proc-macro2", "quote", @@ -1367,6 +1783,17 @@ dependencies = [ "libc", ] +[[package]] +name = "tar" +version = "0.4.44" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1d863878d212c87a19c1a610eb53bb01fe12951c0501cf5a0d65f724914a667a" +dependencies = [ + "filetime", + "libc", + "xattr", +] + [[package]] name = "tempfile" version = "3.10.1" @@ -1385,7 +1812,16 @@ version = "1.0.57" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "1e45bcbe8ed29775f228095caf2cd67af7a4ccf756ebff23a306bf3e8b47b24b" dependencies = [ - "thiserror-impl", + "thiserror-impl 1.0.57", +] + +[[package]] +name = "thiserror" +version = "2.0.17" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f63587ca0f12b72a0600bcba1d40081f830876000bb46dd2337a3051618f4fc8" +dependencies = [ + "thiserror-impl 2.0.17", ] [[package]] @@ -1396,7 +1832,18 @@ checksum = "a953cb265bef375dae3de6663da4d3804eee9682ea80d8e2542529b73c531c81" dependencies = [ "proc-macro2", "quote", - "syn 2.0.52", + "syn 2.0.114", +] + +[[package]] +name = "thiserror-impl" +version = "2.0.17" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3ff15c8ecd7de3849db632e14d18d2571fa09dfc5ed93479bc4485c7a517c913" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.114", ] [[package]] @@ -1420,11 +1867,31 @@ dependencies = [ "weezl", ] +[[package]] +name = "time" +version = "0.3.44" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "91e7d9e3bb61134e77bde20dd4825b97c010155709965fedf0f49bb138e52a9d" +dependencies = [ + "deranged", + "num-conv", + "powerfmt", + "serde", + "time-core", +] + +[[package]] +name = "time-core" +version = "0.1.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "40868e7c1d2f0b8d73e4a8c7f0ff63af4f6d19be117e90bd73eb1d62cf831c6b" + [[package]] name = "tiny-git-helper" -version = "0.1.5" +version = "0.0.5" dependencies = [ "arboard", + "chrono", "clap", "crossterm 0.27.0", "fuzzy-matcher", @@ -1433,6 +1900,8 @@ dependencies = [ "openssl", "regex", "reqwest", + "self_update", + "semver", "serde", "serde_json", "spinners", @@ -1481,7 +1950,7 @@ checksum = "5b8a1e28f2deaa14e508979454cb3a223b10b938b45af148bc0986de36f1923b" dependencies = [ "proc-macro2", "quote", - "syn 2.0.52", + "syn 2.0.114", ] [[package]] @@ -1539,6 +2008,12 @@ version = "0.2.5" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "e421abadd41a4225275504ea4d6566923418b7f05506fbc9c0fe86ba7396114b" +[[package]] +name = "typenum" +version = "1.19.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "562d481066bde0658276a35467c4af00bdc6ee726305698a55b86e61d7ad82bb" + [[package]] name = "unicase" version = "2.7.0" @@ -1581,6 +2056,12 @@ version = "0.1.11" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "e51733f11c9c4f72aa0c160008246859e340b00807569a0da0e7a1079b27ba85" +[[package]] +name = "unicode-width" +version = "0.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b4ac048d71ede7ee76d585517add45da530660ef4390e49b098733c6e897f254" + [[package]] name = "url" version = "2.5.0" @@ -1592,6 +2073,12 @@ dependencies = [ "percent-encoding", ] +[[package]] +name = "urlencoding" +version = "2.1.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "daf8dba3b7eb870caf1ddeed7bc9d2a049f3cfdfae7cb521b087cc33ae4c49da" + [[package]] name = "utf8parse" version = "0.2.1" @@ -1646,7 +2133,7 @@ dependencies = [ "once_cell", "proc-macro2", "quote", - "syn 2.0.52", + "syn 2.0.114", "wasm-bindgen-shared", ] @@ -1680,7 +2167,7 @@ checksum = "e94f17b526d0a461a191c78ea52bbce64071ed5c04c9ffe424dcb38f74171bb7" dependencies = [ "proc-macro2", "quote", - "syn 2.0.52", + "syn 2.0.114", "wasm-bindgen-backend", "wasm-bindgen-shared", ] @@ -1701,6 +2188,16 @@ dependencies = [ "wasm-bindgen", ] +[[package]] +name = "web-time" +version = "1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5a6580f308b1fad9207618087a65c04e7a10bc77e02c8e84e9b00dd4b12fa0bb" +dependencies = [ + "js-sys", + "wasm-bindgen", +] + [[package]] name = "weezl" version = "0.1.8" @@ -1729,6 +2226,65 @@ version = "0.4.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "712e227841d057c1ee1cd2fb22fa7e5a5461ae8e48fa2ca79ec42cfc1931183f" +[[package]] +name = "windows-core" +version = "0.62.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b8e83a14d34d0623b51dce9581199302a221863196a1dde71a7663a4c2be9deb" +dependencies = [ + "windows-implement", + "windows-interface", + "windows-link", + "windows-result", + "windows-strings", +] + +[[package]] +name = "windows-implement" +version = "0.60.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "053e2e040ab57b9dc951b72c264860db7eb3b0200ba345b4e4c3b14f67855ddf" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.114", +] + +[[package]] +name = "windows-interface" +version = "0.59.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3f316c4a2570ba26bbec722032c4099d8c8bc095efccdc15688708623367e358" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.114", +] + +[[package]] +name = "windows-link" +version = "0.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f0805222e57f7521d6a62e36fa9163bc891acd422f971defe97d64e70d0a4fe5" + +[[package]] +name = "windows-result" +version = "0.4.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7781fa89eaf60850ac3d2da7af8e5242a5ea78d1a11c49bf2910bb5a73853eb5" +dependencies = [ + "windows-link", +] + +[[package]] +name = "windows-strings" +version = "0.5.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7837d08f69c77cf6b07689544538e017c1bfcf57e34b4c0ff58e6c2cd3b37091" +dependencies = [ + "windows-link", +] + [[package]] name = "windows-sys" version = "0.48.0" @@ -1747,6 +2303,15 @@ dependencies = [ "windows-targets 0.52.4", ] +[[package]] +name = "windows-sys" +version = "0.60.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f2f500e4d28234f72040990ec9d39e3a6b950f9f22d3dba18416c35882612bcb" +dependencies = [ + "windows-targets 0.53.5", +] + [[package]] name = "windows-targets" version = "0.48.5" @@ -1777,6 +2342,23 @@ dependencies = [ "windows_x86_64_msvc 0.52.4", ] +[[package]] +name = "windows-targets" +version = "0.53.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4945f9f551b88e0d65f3db0bc25c33b8acea4d9e41163edf90dcd0b19f9069f3" +dependencies = [ + "windows-link", + "windows_aarch64_gnullvm 0.53.1", + "windows_aarch64_msvc 0.53.1", + "windows_i686_gnu 0.53.1", + "windows_i686_gnullvm", + "windows_i686_msvc 0.53.1", + "windows_x86_64_gnu 0.53.1", + "windows_x86_64_gnullvm 0.53.1", + "windows_x86_64_msvc 0.53.1", +] + [[package]] name = "windows_aarch64_gnullvm" version = "0.48.5" @@ -1789,6 +2371,12 @@ version = "0.52.4" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "bcf46cf4c365c6f2d1cc93ce535f2c8b244591df96ceee75d8e83deb70a9cac9" +[[package]] +name = "windows_aarch64_gnullvm" +version = "0.53.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a9d8416fa8b42f5c947f8482c43e7d89e73a173cead56d044f6a56104a6d1b53" + [[package]] name = "windows_aarch64_msvc" version = "0.48.5" @@ -1801,6 +2389,12 @@ version = "0.52.4" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "da9f259dd3bcf6990b55bffd094c4f7235817ba4ceebde8e6d11cd0c5633b675" +[[package]] +name = "windows_aarch64_msvc" +version = "0.53.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b9d782e804c2f632e395708e99a94275910eb9100b2114651e04744e9b125006" + [[package]] name = "windows_i686_gnu" version = "0.48.5" @@ -1813,6 +2407,18 @@ version = "0.52.4" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "b474d8268f99e0995f25b9f095bc7434632601028cf86590aea5c8a5cb7801d3" +[[package]] +name = "windows_i686_gnu" +version = "0.53.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "960e6da069d81e09becb0ca57a65220ddff016ff2d6af6a223cf372a506593a3" + +[[package]] +name = "windows_i686_gnullvm" +version = "0.53.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fa7359d10048f68ab8b09fa71c3daccfb0e9b559aed648a8f95469c27057180c" + [[package]] name = "windows_i686_msvc" version = "0.48.5" @@ -1825,6 +2431,12 @@ version = "0.52.4" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "1515e9a29e5bed743cb4415a9ecf5dfca648ce85ee42e15873c3cd8610ff8e02" +[[package]] +name = "windows_i686_msvc" +version = "0.53.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1e7ac75179f18232fe9c285163565a57ef8d3c89254a30685b57d83a38d326c2" + [[package]] name = "windows_x86_64_gnu" version = "0.48.5" @@ -1837,6 +2449,12 @@ version = "0.52.4" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "5eee091590e89cc02ad514ffe3ead9eb6b660aedca2183455434b93546371a03" +[[package]] +name = "windows_x86_64_gnu" +version = "0.53.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9c3842cdd74a865a8066ab39c8a7a473c0778a3f29370b5fd6b4b9aa7df4a499" + [[package]] name = "windows_x86_64_gnullvm" version = "0.48.5" @@ -1849,6 +2467,12 @@ version = "0.52.4" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "77ca79f2451b49fa9e2af39f0747fe999fcda4f5e241b2898624dca97a1f2177" +[[package]] +name = "windows_x86_64_gnullvm" +version = "0.53.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0ffa179e2d07eee8ad8f57493436566c7cc30ac536a3379fdf008f47f6bb7ae1" + [[package]] name = "windows_x86_64_msvc" version = "0.48.5" @@ -1861,6 +2485,12 @@ version = "0.52.4" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "32b752e52a2da0ddfbdbcc6fceadfeede4c939ed16d13e648833a61dfb611ed8" +[[package]] +name = "windows_x86_64_msvc" +version = "0.53.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d6bbff5f0aada427a1e5a6da5f1f98158182f26556f345ac9e04d36d0ebed650" + [[package]] name = "winreg" version = "0.50.0" @@ -1887,3 +2517,43 @@ name = "x11rb-protocol" version = "0.13.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "e63e71c4b8bd9ffec2c963173a4dc4cbde9ee96961d4fcb4429db9929b606c34" + +[[package]] +name = "xattr" +version = "1.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8da84f1a25939b27f6820d92aed108f83ff920fdf11a7b19366c27c4cda81d4f" +dependencies = [ + "libc", + "linux-raw-sys", + "rustix", +] + +[[package]] +name = "zeroize" +version = "1.8.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b97154e67e32c85465826e8bcc1c59429aaaf107c1e4a9e53c8d8ccd5eff88d0" + +[[package]] +name = "zip" +version = "0.6.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "760394e246e4c28189f19d488c058bf16f564016aefac5d32bb1f3b51d5e9261" +dependencies = [ + "byteorder", + "crc32fast", + "crossbeam-utils", + "time", +] + +[[package]] +name = "zipsign-api" +version = "0.1.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "dba6063ff82cdbd9a765add16d369abe81e520f836054e997c2db217ceca40c0" +dependencies = [ + "base64 0.22.1", + "ed25519-dalek", + "thiserror 2.0.17", +] diff --git a/Cargo.toml b/Cargo.toml index bd766a2..2c16939 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -1,7 +1,7 @@ [package] name = "tiny-git-helper" description = "tgh - A GitHub CLI written in Rust" -version = "0.1.5" +version = "0.0.5" edition = "2021" [[bin]] @@ -24,3 +24,6 @@ spinners = "4.1.1" tokio = { version = "1.34.0", features = ["full"] } clap = { version = "4.4.11", features = ["derive", "unicode"] } fuzzy-matcher = "0.3.7" +chrono = "0.4.42" +self_update = { version = "0.39", features = ["archive-zip", "archive-tar"] } +semver = "1.0.27" diff --git a/src/config/config.rs b/src/config/config.rs index e4aab7d..fdf8ba5 100644 --- a/src/config/config.rs +++ b/src/config/config.rs @@ -2,8 +2,7 @@ use super::{defines, utils, Config}; use crate::out; /// Loads the config file. -/// If the config file doesn't exist, it will create one. -/// If the config file is invalid, it will create a new one. +/// If the config file doesn't exist or is invalid, it will create a new one. /// /// ### Returns /// A Config struct. @@ -125,74 +124,3 @@ fn ask_fancy() -> bool { } } } - -fn update_username(username: String) { - let config = utils::read_config(); - - let new_config = Config { - username, - token: config.token, - sort: config.sort, - protocol: config.protocol, - color: config.color, - fancy: config.fancy, - }; - - utils::save_config_file(new_config); -} -fn update_sort(sort: defines::SORTING) { - let config = utils::read_config(); - - let new_config = Config { - username: config.username, - token: config.token, - sort, - protocol: config.protocol, - color: config.color, - fancy: config.fancy, - }; - - utils::save_config_file(new_config); -} -fn update_protocol(protocol: defines::PROTOCOL) { - let config = utils::read_config(); - - let new_config = Config { - username: config.username, - token: config.token, - sort: config.sort, - protocol, - color: config.color, - fancy: config.fancy, - }; - - utils::save_config_file(new_config); -} -fn update_color(color: defines::COLOR) { - let config = utils::read_config(); - - let new_config = Config { - username: config.username, - token: config.token, - sort: config.sort, - protocol: config.protocol, - color, - fancy: config.fancy, - }; - - utils::save_config_file(new_config); -} -fn update_fancy(fancy: bool) { - let config = utils::read_config(); - - let new_config = Config { - username: config.username, - token: config.token, - sort: config.sort, - protocol: config.protocol, - color: config.color, - fancy, - }; - - utils::save_config_file(new_config); -} diff --git a/src/config/defines.rs b/src/config/defines.rs index 45eed2a..d9c83af 100644 --- a/src/config/defines.rs +++ b/src/config/defines.rs @@ -40,21 +40,8 @@ impl COLOR { } .to_string() } - - pub fn as_inquire_color(&self) -> inquire::ui::Color { - match self { - COLOR::RED => inquire::ui::Color::LightRed, - COLOR::GREEN => inquire::ui::Color::LightGreen, - COLOR::YELLOW => inquire::ui::Color::LightYellow, - COLOR::BLUE => inquire::ui::Color::LightBlue, - COLOR::MAGENTA => inquire::ui::Color::LightMagenta, - COLOR::CYAN => inquire::ui::Color::LightCyan, - COLOR::WHITE => inquire::ui::Color::White, - COLOR::GRAY => inquire::ui::Color::Grey, - _ => inquire::ui::Color::White, - } - } } + impl std::fmt::Display for COLOR { fn fmt(&self, f: &mut std::fmt::Formatter) -> std::fmt::Result { let color = match self { diff --git a/src/config/git.rs b/src/config/git.rs index ad6d1d6..1be3114 100644 --- a/src/config/git.rs +++ b/src/config/git.rs @@ -1,75 +1,274 @@ -use crate::out; +use regex::Regex; -pub fn check_git() -> bool { +const MIN_GIT_VERSION: &str = "2.20.0"; + +// Create an error message for when git is not installed depending on the OS +#[cfg(target_os = "windows")] +const GIT_INSTALL_INSTRUCTIONS: &str = r#" +You can install it using Chocolatey: +$i ` choco install git` + +or using Winget: +$i ` winget install --id Git.Git -e --source winget` + +Or you can download it from the official website: +$b ` `$u `https://git-scm.com/download/win` +"#; + +#[cfg(target_os = "macos")] +const GIT_INSTALL_INSTRUCTIONS: &str = r#" +You can install it using Homebrew: +$i ` brew install git` + +or using MacPorts: +$i ` sudo port install git` + +Xcode also includes git. You can install it from the App Store. +"#; + +#[cfg(not(any(target_os = "windows", target_os = "macos", target_os = "linux")))] +const GIT_INSTALL_INSTRUCTIONS: &str = r#" +You can install it using your package manager. + +Or you can download it from the official website: +$b ` `$u `https://git-scm.com/` +"#; + +const GIT_NAME_NOT_FOUND: &str = r#" + $b$cr `error`: Git user.name not found. + + You can set it using the following command: + $i ` git config user.name "Your Name"` + + or globally: + $i ` git config --global user.name "Your Name"` + $i$s `this will not work if you set it locally` +"#; + +const GIT_EMAIL_NOT_FOUND: &str = r#" + $b$cr `error`: Git user.email not found. + + You can set it using the following command: + $i ` git config user.email "` + + or globally: + $i ` git config --global user.email "` + $i$s `this will not work if you set it locally` +"#; + +pub enum GitError { + NotInstalled, + VersionNotSupported { current: String, min: String }, +} + +impl GitError { + pub fn to_string(&self) -> String { + match self { + GitError::NotInstalled => { + #[cfg(target_os = "linux")] // For Linux, use the dynamic message (based on distro) + let message = get_git_installation_instructions(); + + #[cfg(not(target_os = "linux"))] // For other OSes, use the static message + let message = GIT_INSTALL_INSTRUCTIONS; + + return format!( + r#" + $b$cr `error`: $b `git` is not installed. + + {}"#, + message, + ); + } + + GitError::VersionNotSupported { current, min } => { + #[cfg(target_os = "linux")] + let install_cmd = get_git_installation_instructions(); + + #[cfg(not(target_os = "linux"))] + let install_cmd = GIT_INSTALL_INSTRUCTIONS; + + let msg = format!( + r#" + $b$cr `error`: $b `git` version not supported. + You need at least version {}, you are currently using version {} + + {} + "#, + min, current, install_cmd + ); + + return msg; + } + } + } +} + +pub fn validate_git_install() -> Result<(), GitError> { let mut command = std::process::Command::new("git"); command.arg("--version"); let output = command.output().unwrap(); - if output.status.success() { - return true; + if !output.status.success() { + return Err(GitError::NotInstalled); } - false -} -/// Checks if the user has a git config. (user.name, user.email) -pub fn check_git_config() { - let mut command = std::process::Command::new("git"); - command.args(["config", "--global", "user.name"]); + let binding = String::from_utf8(output.stdout).unwrap(); + let s = binding.trim(); + if s.len() == 0 { + return Err(GitError::NotInstalled); + } - let output = command.output().unwrap(); + let re = Regex::new(r"(\d+\.\d+\.\d+)").unwrap(); + let version; + + if let Some(captures) = re.captures(s) { + if let Some(v) = captures.get(1) { + version = v.as_str(); + } else { + return Err(GitError::NotInstalled); + } + } else { + return Err(GitError::NotInstalled); + } - if !output.status.success() || output.stdout.len() == 0 { - out::print_error("Error: user.name was not found in git config.\n"); - let name = ask_git_name(); + let version_u32 = version + .split(".") + .collect::>() + .iter() + .map(|s| s.parse::().unwrap_or(0)) + .collect::>(); - let mut command = std::process::Command::new("git"); - command.args(["config", "--global", "user.name", &name]); + let min_version = MIN_GIT_VERSION + .split(".") + .collect::>() + .iter() + .map(|s| s.parse::().unwrap_or(0)) + .collect::>(); - let output = command.output().unwrap(); + if version_u32.len() != 3 || min_version.len() != 3 { + return Err(GitError::VersionNotSupported { + current: version.to_string(), + min: MIN_GIT_VERSION.to_string(), + }); + } - if !output.status.success() { - out::print_error("Error: Failed to set user.name.\n"); - println!("Try setting it manually using `git config --global user.name \"Your Name\"`"); - std::process::exit(1); + for i in 0..3 { + if version_u32[i] < min_version[i] { + return Err(GitError::VersionNotSupported { + current: version.to_string(), + min: MIN_GIT_VERSION.to_string(), + }); + } else if version_u32[i] > min_version[i] { + return Ok(()); } } + Ok(()) +} + +pub enum GitConfigError { + NameNotFound, + EmailNotFound, +} + +impl GitConfigError { + pub fn to_string(&self) -> String { + match self { + GitConfigError::NameNotFound => GIT_NAME_NOT_FOUND.to_string(), + GitConfigError::EmailNotFound => GIT_EMAIL_NOT_FOUND.to_string(), + } + } +} + +/// Checks if the user has a valid git config. (user.name, user.email) +pub fn check_git_config() -> Result<(), GitConfigError> { + let mut command = std::process::Command::new("git"); + command.args(["config", "user.name"]); + + let output = command.output().unwrap(); + let binding = String::from_utf8(output.stdout).unwrap(); + let s = binding.trim(); + + if !output.status.success() || s.len() == 0 { + return Err(GitConfigError::NameNotFound); + } + let mut command = std::process::Command::new("git"); - command.args(["config", "--global", "user.email"]); + command.args(["config", "user.email"]); let output = command.output().unwrap(); + let binding = String::from_utf8(output.stdout).unwrap(); + let s = binding.trim(); - if !output.status.success() || output.stdout.len() == 0 { - out::print_error("Error: user.email was not found in git config.\n"); - let email = ask_git_email(); + if !output.status.success() || s.len() == 0 { + return Err(GitConfigError::EmailNotFound); + } + + Ok(()) +} - let mut command = std::process::Command::new("git"); - command.args(["config", "--global", "user.email", &email]); +#[cfg(target_os = "linux")] +fn get_git_installation_instructions() -> String { + let install_cmd; - let output = command.output().unwrap(); + // Get the distribution + let binding = match std::fs::read_to_string("/etc/os-release") { + Ok(binding) => binding, + Err(_) => { + return r#" + $b$cr `error`: $b `git` is not installed. + + You can download it from the official website: + $b ` `$u `https://git-scm.com/download/linux` + "# + .into(); + } + }; + let distro = binding + .lines() + .find(|line| line.starts_with("ID=")) + .unwrap() + .split('=') + .last() + .unwrap(); - if !output.status.success() { - out::print_error("Error: Failed to set user.email.\n"); - println!( - "Try setting it manually using `git config --global user.email \"Your Email\"`" - ); - std::process::exit(1); + match distro { + "ubuntu" | "debian" => { + install_cmd = "sudo apt install git"; + } + "fedora" | "centos" | "rhel" => { + install_cmd = "sudo dnf install git"; + } + "arch" | "manjaro" => { + install_cmd = "sudo pacman -S git"; + } + "alpine" => { + install_cmd = "apk add git"; + } + _ => { + return r#" + $b$cr `error`: $b `git` is not installed. + + You can download it from the official website: + $b ` `$u `https://git-scm.com/download/linux` + "# + .into(); } } -} -fn ask_git_name() -> String { - let name = inquire::Text::new("Enter name used for git:") - .with_validator(inquire::required!("Name is required.")) - .prompt(); + let instructions = format!( + r#" + $b$cr `error`: $b `git` is not installed. - name.unwrap() -} -fn ask_git_email() -> String { - let email = inquire::Text::new("Enter email used for git:") - .with_validator(super::utils::validate_email) - .prompt(); + You can install it using your package manager: + $i ` {}` + + Or you can download it from the official website: + $b ` `$u `https://git-scm.com/download/linux` + "#, + install_cmd + ); - email.unwrap() + instructions } diff --git a/src/config/github.rs b/src/config/github.rs index a73bb42..1e46bb7 100644 --- a/src/config/github.rs +++ b/src/config/github.rs @@ -1,6 +1,7 @@ use super::utils; use super::Config; use crate::out; +use crate::view; pub fn check_token() -> bool { if !utils::config_exists() || !utils::validate_config_file() { @@ -47,10 +48,10 @@ pub async fn authenticate() -> Result { let login_url = text_split[4].replace("%3A", ":").replace("%2F", "/"); let grant_type = "urn:ietf:params:oauth:grant-type:device_code"; - println!( - "Please visit this URL to authenticate: \x1B[4m{}\x1B[m", + view::printer(&format!( + "\nPlease visit this URL to authenticate: $u `{}`\n", login_url - ); + )); let clipboard = Clipboard::new(); match clipboard { @@ -63,7 +64,7 @@ pub async fn authenticate() -> Result { } Err(_) => { println!( - "Error copying to clipboard, copy the code manually: {}", + "Could not copy the code to the clipboard, copy the code manually: {}", user_code ); } diff --git a/src/config/mod.rs b/src/config/mod.rs index d3af289..780c3b7 100644 --- a/src/config/mod.rs +++ b/src/config/mod.rs @@ -1,10 +1,14 @@ -use crate::utils::out; +use std::time::SystemTime; + +use crate::view; +use git::check_git_config; use serde::{Deserialize, Serialize}; mod config; pub mod defines; mod git; mod github; +pub mod update; pub mod utils; pub use config::load_config; @@ -20,37 +24,70 @@ pub struct Config { pub fancy: bool, } +#[derive(Serialize, Deserialize, Clone)] +pub struct Metadata { + pub last_checked: String, +} + +impl Default for Metadata { + fn default() -> Self { + Metadata { + last_checked: chrono::DateTime::::from(SystemTime::UNIX_EPOCH) + .to_rfc3339(), + } + } +} + /// Checks if the prerequisites for tgh are installed. /// If not, it will print an error and exit. -/// -/// ### Arguments -/// * `args` - A vector of the command line arguments. pub async fn check_prerequisites() { - // Check if git is installed - if !git::check_git() { - out::print_error("Error: Git is not installed.\n"); - println!("Please install using the link below:"); - println!("\x1B[mhttps://git-scm.com/downloads\x1B[m\n"); - std::process::exit(1); + match git::validate_git_install() { + Ok(_) => {} + Err(err) => { + view::printer(&err.to_string()); + std::process::exit(1); + } } // Check for git config - git::check_git_config(); + match check_git_config() { + Ok(_) => {} + Err(err) => { + view::printer(&err.to_string()); + std::process::exit(1); + } + } // Check for a config file if !utils::config_exists() { - out::print_error("Config file not found. Creating one...\n"); + view::printer("\n$b$cr `error`: Config file not found. Creating a new one...\n"); config::create_config(); } else if !utils::validate_config_file() { - out::print_error("Config file is invalid. Creating a new one...\n"); + view::printer("\n$b$cr `error`: Config file is invalid. Creating a new one...\n"); config::create_config(); } // Check for a GitHub token if !github::check_token() { - out::print_error("Error: GitHub token invalid.\n"); + view::printer("\n$b$cr `error`: GitHub token not found. Logging in...\n"); login().await; std::thread::sleep(std::time::Duration::from_secs(1)); } + + if utils::should_check_for_updates() { + match update::check_for_updates().await { + Ok(msg) => { + if msg.len() > 0 { + view::printer(msg); + } + } + Err(err) => { + view::printer(&format!( + "\n$b$cr `error`: Failed to check for updates: {}\n", + err.to_string() + )); + } + } + } } diff --git a/src/config/update.rs b/src/config/update.rs new file mode 100644 index 0000000..709b16c --- /dev/null +++ b/src/config/update.rs @@ -0,0 +1,122 @@ +use self_update::backends::github::{ReleaseList, Update}; + +pub async fn check_for_updates() -> Result> { + let update_check = tokio::task::spawn_blocking(|| tokio_check_for_updates()) + .await + .expect("Blocking task panicked"); + + update_check +} + +fn tokio_check_for_updates() -> Result> { + let current_version = env!("CARGO_PKG_VERSION"); + + let releases = ReleaseList::configure() + .repo_owner("dkomeza") + .repo_name("tiny-git-helper") + .build() + .map_err(|e| Box::new(e) as Box)? + .fetch() + .map_err(|e| Box::new(e) as Box)?; + + let latest_release = releases + .iter() + .filter(|r| !r.version.contains("alpha") && !r.version.contains("beta")) + .max_by(|a, b| { + let version_a = + semver::Version::parse(&a.version).unwrap_or(semver::Version::new(0, 0, 0)); + let version_b = + semver::Version::parse(&b.version).unwrap_or(semver::Version::new(0, 0, 0)); + version_a.cmp(&version_b) + }); + + if let Some(release) = latest_release { + let current = semver::Version::parse(current_version) + .map_err(|e| Box::new(e) as Box)?; + let latest = semver::Version::parse(&release.version) + .map_err(|e| Box::new(e) as Box)?; + + if latest > current { + let update_msg = format!( + "\n$cg$b `📦 New Update Available` + &> $cy `{}` $cw `➜` $cg$b `{}` + &> Run $cc$i `tgh update` $cw `to upgrade`\n", + current_version, release.version + ); + return Ok(update_msg); + } + } + + Ok("".into()) +} + +pub async fn perform_self_update() { + use crate::view::printer; + let current_ver = env!("CARGO_PKG_VERSION"); + + printer(format!( + "\n$cb$b `🔍 Checking for updates...`\n&> $cw `Current version:` $cy `{}`\n", + current_ver + )); + + let update_available = check_for_updates().await; + + match update_available { + Ok(msg) => { + if msg.is_empty() { + printer(format!( + "$cg$b `✔ You are already up to date.`\n&> $cw `Version:` $cg `{}`\n", + current_ver + )); + return; + } else { + printer("\n$cy$b `⬇ Update found! Starting download...`\n"); + } + } + Err(e) => { + printer(format!( + "\n$cr$b `✖ Failed to check for updates`\n&> $cr `Error:` $cw `{}`\n", + e + )); + return; + } + } + + let update_result = tokio::task::spawn_blocking(|| execute_update_logic()) + .await + .expect("Blocking task panicked"); + + // 4. Report Final Status + match update_result { + Ok(new_version) => { + printer(format!( + "\n$cg$b `✨ Update Successful!`\n&> $cw `New version:` $cg `{}`\n&> $cw `Please restart the terminal to use the new version.`\n", + new_version + )); + } + Err(err) => { + printer(format!( + "\n$cr$b `✖ Update Failed`\n&> $cr `Reason:` $cw `{}`\n", + err + )); + } + } +} + +// The internal blocking logic +fn execute_update_logic() -> Result> { + let status = Update::configure() + .repo_owner("dkomeza") + .repo_name("tiny-git-helper") + .bin_name("tgh") + .show_download_progress(true) + .current_version(env!("CARGO_PKG_VERSION")) + .no_confirm(true) + .build() + .map_err(|e| Box::new(e) as Box)? + .update() + .map_err(|e| Box::new(e) as Box)?; + + // Return the new version string + Ok(status.version().to_string()) +} diff --git a/src/config/utils.rs b/src/config/utils.rs index ea77f88..29d4f7b 100644 --- a/src/config/utils.rs +++ b/src/config/utils.rs @@ -10,7 +10,7 @@ pub fn handle_config_folder() { create_dir_all(config_path).unwrap(); } -pub fn get_config_path() -> String { +fn get_config_path() -> String { use home::home_dir; let home = home_dir().unwrap(); @@ -19,6 +19,15 @@ pub fn get_config_path() -> String { config_path } +fn get_metadata_path() -> String { + use home::home_dir; + + let home = home_dir().unwrap(); + let metadata_path = format!("{}/.config/tgh/metadata.json", home.display()); + + metadata_path +} + pub fn config_exists() -> bool { use std::path::Path; @@ -28,30 +37,59 @@ pub fn config_exists() -> bool { config_path.exists() } -pub fn read_config_content() -> String { +fn read_file_content(path: String) -> Result { use std::fs::File; use std::io::prelude::*; - let config_path = get_config_path(); - let mut config_file = File::open(config_path).unwrap(); + let mut config_file = File::open(path)?; let mut config_contents = String::new(); - config_file.read_to_string(&mut config_contents).unwrap(); + config_file.read_to_string(&mut config_contents)?; - config_contents + Ok(config_contents) } pub fn read_config() -> crate::config::Config { - let config_contents = read_config_content(); - let config: crate::config::Config = serde_json::from_str(&config_contents).unwrap(); + let config_contents = read_file_content(get_config_path()); + + let conf = match config_contents { + Err(_) => { + panic!("Failed to read config file."); + } + Ok(contents) => contents, + }; + + let config: crate::config::Config = serde_json::from_str(&conf).unwrap(); config } +pub fn read_metadata() -> crate::config::Metadata { + let metadata_path = get_metadata_path(); + + let metadata_contents = read_file_content(metadata_path); + + let metadata = match metadata_contents { + Err(_) => { + let metadata = crate::config::Metadata::default(); + save_metadata_file(metadata.clone()); + metadata + } + Ok(contents) => serde_json::from_str(&contents).unwrap_or_default(), + }; + + metadata +} + pub fn validate_config_file() -> bool { use crate::config::Config; - let config_contents = read_config_content(); + let config_contents = match read_file_content(get_config_path()) { + Ok(contents) => contents, + Err(_) => { + return false; + } + }; if config_contents.len() == 0 { return false; @@ -100,28 +138,6 @@ pub fn save_config_file(config: crate::config::Config) { config_file.write_all(config_contents.as_bytes()).unwrap(); } -pub fn validate_email( - email: &str, -) -> Result { - use regex::Regex; - - let re = Regex::new(r"^[a-zA-Z0-9_.+-]+@[a-zA-Z0-9-]+\.[a-zA-Z0-9-.]+$").unwrap(); - - if email.len() == 0 { - return Ok(inquire::validator::Validation::Invalid( - "Email cannot be empty".into(), - )); - } - - if !re.is_match(email) { - return Ok(inquire::validator::Validation::Invalid( - "Invalid email".into(), - )); - } - - Ok(inquire::validator::Validation::Valid) -} - #[derive(Clone, Debug)] pub struct CommitLabel { pub label: String, @@ -312,3 +328,39 @@ pub fn get_labels() -> Vec { labels } + +pub fn should_check_for_updates() -> bool { + let metadata = read_metadata(); + + let now = chrono::Utc::now(); + let last_checked = chrono::DateTime::parse_from_rfc3339(&metadata.last_checked) + .unwrap() + .to_utc(); + + let time_diff = now.signed_duration_since(last_checked).num_days(); + + if time_diff >= 1 { + return true; + } + + false +} + +pub fn save_metadata_file(metadata: crate::config::Metadata) { + use std::{fs::File, io::prelude::*}; + + let metadata_path = get_metadata_path(); + + let mut metadata_file = match (File::create(metadata_path)) { + Ok(file) => file, + Err(err) => { + panic!("Failed to create metadata file: {}", err.to_string()); + } + }; + + let metadata_contents = serde_json::to_string_pretty(&metadata).unwrap(); + + metadata_file + .write_all(metadata_contents.as_bytes()) + .unwrap(); +} diff --git a/src/main.rs b/src/main.rs index fae5e68..f002294 100644 --- a/src/main.rs +++ b/src/main.rs @@ -2,27 +2,11 @@ use clap::{Parser, Subcommand}; mod utils; use utils::out; -use utils::out::clear_screen; mod config; mod functions; mod modules; - -fn setup_ui() { - use inquire::ui::{Attributes, Color, RenderConfig, StyleSheet}; - - let mut render_config = RenderConfig::default(); - if config::load_config().color != config::defines::COLOR::NORMAL { - render_config.prompt = - StyleSheet::new().with_fg(config::load_config().color.as_inquire_color()); - } - render_config.answer = StyleSheet::new() - .with_fg(Color::Grey) - .with_attr(Attributes::BOLD); - render_config.help_message = StyleSheet::new().with_fg(Color::DarkGrey); - - inquire::set_global_render_config(render_config); -} +mod view; #[derive(Parser)] #[command(name = "tgh", author, version, about)] @@ -33,23 +17,14 @@ struct Cli { #[derive(Subcommand)] enum SubCommand { - #[clap(name = "commit", about = "Open the commit menu")] - #[clap(visible_alias = "c")] - Commit(modules::commit::CommitOptions), + #[clap(name = "commit", about = "Commit changes to the repository")] + #[clap(visible_alias = "cf")] + CommitFiles(modules::commit::CommitOptions), #[clap(name = "ca", about = "Commit all files")] CommitAll(modules::commit::CommitOptions), - #[clap(name = "cf", about = "Commit specific files")] - CommitFiles(modules::commit::CommitOptions), - #[clap(name = "clone", about = "Clone a repository")] - Clone(modules::clone::CloneOptions), - - #[clap(name = "history", about = "Show commit history")] - #[clap(visible_alias = "log")] - History(modules::history::CommitHistoryOptions), - - #[clap(name = "login", about = "Login to GitHub")] - Login, + #[clap(name = "update", about = "Update tgh to the latest version")] + Update, } #[tokio::main] @@ -57,34 +32,26 @@ async fn main() { let args = Cli::parse(); config::check_prerequisites().await; - setup_ui(); let subcmd = match args.subcmd { Some(subcmd) => subcmd, None => { - out::print_error("\nNo subcommand provided\n"); + view::no_subcommand_error(); return; } }; match subcmd { - SubCommand::Commit(options) => { - return modules::commit::commit_menu(options); - } SubCommand::CommitAll(options) => { - return modules::commit::commit_all_files(options); + modules::commit::commit_all_files(options); } SubCommand::CommitFiles(options) => { - return modules::commit::commit_specific_files(options); + modules::commit::commit_specific_files(options); } - SubCommand::Clone(options) => { - return modules::clone::clone_menu(options).await; - } - SubCommand::History(options) => { - return modules::history::commit_history(options); - } - SubCommand::Login => { - config::login().await + SubCommand::Update => { + config::update::perform_self_update().await; } } + + view::clean_up(); } diff --git a/src/modules/commit.rs b/src/modules/commit.rs index 317f909..359ef22 100644 --- a/src/modules/commit.rs +++ b/src/modules/commit.rs @@ -3,19 +3,19 @@ use clap::Parser; mod functions; mod views; -pub use views::{commit_all_files, commit_menu, commit_specific_files}; +pub use views::commit_specific_files; #[derive(Parser)] pub struct CommitOptions { - /// Don't push changes to remote + /// Don't push changes to the remote #[clap(short, long)] pub no_push: bool, - /// Don't use fancy commit messages + /// Don't use fancy commit message #[clap(long, conflicts_with = "force_fancy")] pub skip_fancy: bool, - /// Force fancy commit messages + /// Force fancy commit message #[clap(long, conflicts_with = "skip_fancy")] pub force_fancy: bool, @@ -33,3 +33,22 @@ impl Default for CommitOptions { } } } + +pub fn commit_all_files(options: CommitOptions) { + functions::is_valid_commit(); + + let message = ask_commit_message(&options); + + println!("Committing all files with message: {}", message); + + // commit_all_files(message, options.no_push); +} + +fn ask_commit_message(options: &CommitOptions) -> String { + if let Some(message) = &options.commit_message { + return message.clone(); + } + + let config = crate::config::load_config(); + String::from("dummy message") // Temporary fix to allow compilation +} diff --git a/src/modules/commit/views.rs b/src/modules/commit/views.rs index 526bf2a..d2f37bd 100644 --- a/src/modules/commit/views.rs +++ b/src/modules/commit/views.rs @@ -1,54 +1,6 @@ use super::CommitOptions; use inquire::{list_option::ListOption, validator::Validation}; -pub fn commit_menu(options: CommitOptions) { - use crate::clear_screen; - use inquire::Select; - use std::process; - - clear_screen(); - - super::functions::is_valid_commit(); - - let choice; - - let menu = Select::new( - "What do you want to commit?", - vec!["Commit specific files", "Commit all files"], - ) - .prompt(); - - match menu { - Ok(option) => { - choice = option; - } - Err(_) => { - process::exit(0); - } - } - - match choice { - "Commit specific files" => { - commit_specific_files(options); - } - "Commit all files" => { - commit_all_files(options); - } - _ => { - println!("Invalid option"); - } - } -} - -pub fn commit_all_files(options: CommitOptions) { - use super::functions::{commit_all_files, is_valid_commit}; - - is_valid_commit(); - - let message = ask_commit_message(&options); - - commit_all_files(message, options.no_push); -} pub fn commit_specific_files(options: CommitOptions) { use super::functions::{commit_specific_files, is_valid_commit}; @@ -60,7 +12,6 @@ pub fn commit_specific_files(options: CommitOptions) { commit_specific_files(files, message, options.no_push); } -pub fn commit_history() {} fn ask_commit_message(options: &CommitOptions) -> String { use inquire::{Select, Text}; diff --git a/src/view/input.rs b/src/view/input.rs new file mode 100644 index 0000000..5450410 --- /dev/null +++ b/src/view/input.rs @@ -0,0 +1,587 @@ +use crossterm::{ + cursor::{MoveDown, MoveToColumn, MoveUp}, + event::{self, KeyCode, KeyEvent, KeyModifiers}, + execute, + terminal::{self, disable_raw_mode, enable_raw_mode, ClearType}, +}; +use fuzzy_matcher::{skim::SkimMatcherV2, FuzzyMatcher}; +use std::{ + fmt::{Debug, Display}, + io::{self, Write}, +}; + +use super::{print, PrintSize}; + +const MAX_ROWS: usize = 12; + +pub enum ReturnType { + Cancel, + Exit, +} + +#[derive(PartialEq, Clone, Copy)] +enum TextInputType { + Text, + Password, +} + +/** +## A struct to handle text input +#### It handles the input, cursor position, and input type (text or password), it only handles input (special actions (like Ctrl+C) are handled in the main function) +*/ +struct TextInput { + input: String, + position: usize, + input_type: TextInputType, + cursor_position: usize, +} + +impl TextInput { + fn new(position: usize, input_type: TextInputType) -> Self { + Self { + input: String::new(), + position, + input_type, + cursor_position: 0, + } + } + + fn handle_event(&mut self, event: KeyEvent) { + match event.modifiers { + KeyModifiers::ALT => match event.code { + KeyCode::Char('b') => { + // Move to the beginning of the word + // If input type is password, go to the beginning of the line + match self.input_type { + TextInputType::Text => { + // Check if the previous character is a whitespace + if self.cursor_position > 0 + && self.input[self.cursor_position - 1..self.cursor_position] + .chars() + .next() + .unwrap() + .is_whitespace() + { + self.cursor_position -= 1; + } + + if let Some(pos) = + self.input[..self.cursor_position].rfind(char::is_whitespace) + { + self.cursor_position = pos + 1; + } else { + self.cursor_position = 0; + } + } + TextInputType::Password => { + self.cursor_position = 0; + } + } + + let column = self.position + self.cursor_position; + execute!(io::stdout(), MoveToColumn(column as u16)).unwrap(); + } + KeyCode::Char('f') => { + match self.input_type { + TextInputType::Text => { + // Move to the end of the word + // Check if the cursor is on a whitespace + if self.cursor_position < self.input.len() + && self.input[self.cursor_position..] + .chars() + .next() + .unwrap() + .is_whitespace() + { + self.cursor_position += 1; + } + + if let Some(pos) = + self.input[self.cursor_position..].find(char::is_whitespace) + { + self.cursor_position += pos; + } else { + self.cursor_position = self.input.len(); + } + } + TextInputType::Password => { + self.cursor_position = self.input.len(); + } + } + + let column = self.position + self.cursor_position; + execute!(io::stdout(), MoveToColumn(column as u16)).unwrap(); + } + _ => {} + }, + KeyModifiers::CONTROL => match event.code { + KeyCode::Char('a') => { + // Move to the beginning of the line + self.cursor_position = 0; + let column = self.position + self.cursor_position; + execute!(io::stdout(), MoveToColumn(column as u16)).unwrap(); + } + KeyCode::Char('e') => { + // Move to the end of the line + self.cursor_position = self.input.len(); + let column = self.position + self.cursor_position; + execute!(io::stdout(), MoveToColumn(column as u16)).unwrap(); + } + KeyCode::Char('u') => { + // Remove all text before the cursor + execute!( + io::stdout(), + MoveToColumn(self.position as u16), + terminal::Clear(ClearType::UntilNewLine) + ) + .unwrap(); + self.input.drain(..self.cursor_position); + self.cursor_position = 0; + + print(format!("{}", self.input)); + io::stdout().flush().unwrap(); + execute!( + io::stdout(), + MoveToColumn((self.position + self.cursor_position) as u16) + ) + .unwrap(); + } + KeyCode::Char('h') | KeyCode::Char('w') => { + match self.input_type { + TextInputType::Text => { + // Remove the word before the cursor + if let Some(pos) = + self.input[..self.cursor_position].rfind(char::is_whitespace) + { + self.input.drain(pos..self.cursor_position); + self.cursor_position = pos; + } else { + self.input.drain(..self.cursor_position); + self.cursor_position = 0; + } + } + TextInputType::Password => { + // Move to the beginning of the line + self.cursor_position = 0; + self.input.clear(); + } + } + execute!( + io::stdout(), + MoveToColumn(self.position as u16), + terminal::Clear(ClearType::UntilNewLine) + ) + .unwrap(); + print(format!("{}", self.input)); + io::stdout().flush().unwrap(); + execute!( + io::stdout(), + MoveToColumn((self.position + self.cursor_position) as u16) + ) + .unwrap(); + } + _ => {} + }, + KeyModifiers::NONE => match event.code { + KeyCode::Backspace => { + if self.cursor_position > 0 { + self.cursor_position -= 1; + self.input.remove(self.cursor_position); + + execute!( + io::stdout(), + MoveToColumn(self.position as u16), + terminal::Clear(ClearType::UntilNewLine) + ) + .unwrap(); + + match self.input_type { + TextInputType::Text => { + print(format!("{}", self.input)); + } + TextInputType::Password => { + print(format!("{}", "*".repeat(self.input.len()))); + } + } + io::stdout().flush().unwrap(); + + let column = self.position + self.cursor_position; + execute!(io::stdout(), MoveToColumn(column as u16)).unwrap(); + } + } + KeyCode::Left => { + if self.cursor_position > 0 { + self.cursor_position -= 1; + let column = self.position + self.cursor_position; + execute!(io::stdout(), MoveToColumn(column as u16)).unwrap(); + } + } + KeyCode::Right => { + if self.cursor_position < self.input.len() { + self.cursor_position += 1; + let column = self.position + self.cursor_position; + execute!(io::stdout(), MoveToColumn(column as u16)).unwrap(); + } + } + KeyCode::Char(c) => { + self.input.insert(self.cursor_position, c); + self.cursor_position += 1; + execute!( + io::stdout(), + MoveToColumn(self.position as u16), + terminal::Clear(ClearType::UntilNewLine) + ) + .unwrap(); + match self.input_type { + TextInputType::Text => { + print(format!("{}", self.input)); + } + TextInputType::Password => { + print(format!("{}", "*".repeat(self.input.len()))); + } + } + + io::stdout().flush().unwrap(); + + let column = self.position + self.cursor_position; + execute!(io::stdout(), MoveToColumn(column as u16)).unwrap(); + } + _ => {} + }, + _ => {} + } + } +} + +struct ListValue { + key: usize, + value: T, + matched: bool, +} + +impl Display for ListValue { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + write!(f, "{}", self.value) + } +} + +pub fn text(prompt: &str) -> Result { + let PrintSize { + cols: prompt_length, + rows: _, + } = print(format!("{}", prompt)); + io::stdout().flush().unwrap(); + + get_user_text_input(prompt_length, TextInputType::Text) +} + +pub fn password(prompt: &str) -> Result { + let PrintSize { + cols: prompt_length, + rows: _, + } = print(format!("{}", prompt)); + io::stdout().flush().unwrap(); + + get_user_text_input(prompt_length, TextInputType::Password) +} + +pub fn list(prompt: &str, items: Vec) -> Result +where + T: Display + Clone, +{ + use fuzzy_matcher::skim::SkimMatcherV2; + + if items.is_empty() { + return Err(ReturnType::Cancel); + } + + let mut kv_items: Vec> = items + .iter() + .enumerate() + .map(|(i, x)| ListValue { + key: i, + value: x.clone(), + matched: true, + }) + .collect(); + + super::init(); + + let mut selected = 0; + let matcher = SkimMatcherV2::default(); + + let PrintSize { + cols: prompt_length, + rows: _, + } = print(format!("{}\n", prompt)); + + let mut text_input = TextInput::new(prompt_length, TextInputType::Text); + let mut available_rows = terminal::size().unwrap().1 as usize - 1; // 1 for the prompt + if available_rows > MAX_ROWS { + available_rows = MAX_ROWS; + } + + let usable_rows; + if kv_items.len() < available_rows { + usable_rows = kv_items.len() + } else { + usable_rows = available_rows; + } + + for _ in 0..usable_rows { + print(format!("\n\r")); + } + execute!(io::stdout(), MoveUp(usable_rows as u16), MoveToColumn(0)).unwrap(); + + let rendered = render_list( + &kv_items, + 0, + 0, + usable_rows, + &matcher, + text_input.input.clone(), + ); + + execute!( + io::stdout(), + MoveUp((rendered + 1) as u16), + MoveToColumn((prompt_length) as u16) + ) + .unwrap(); + io::stdout().flush().unwrap(); + + loop { + if let Ok(event) = event::read() { + match event { + event::Event::Key(event) => match event.code { + KeyCode::Esc => { + execute!( + io::stdout(), + MoveToColumn(0), + terminal::Clear(ClearType::CurrentLine) + ) + .unwrap(); + print(format!("{}$cr `canceled`\n", prompt)); + + disable_raw_mode().unwrap(); + return Err(ReturnType::Cancel); + } + KeyCode::Enter => { + break; + } + KeyCode::Up => { + if selected > 0 { + selected -= 1; + } + + while selected > 0 && !kv_items[selected].matched { + selected -= 1; + } + } + KeyCode::Down => { + if selected < kv_items.len() - 1 { + selected += 1; + + while selected < kv_items.len() - 1 && !kv_items[selected].matched { + selected += 1; + } + } + } + KeyCode::Char(c) => { + if event.modifiers == KeyModifiers::CONTROL && c == 'c' { + write!(io::stdout(), "\n\r").unwrap(); + disable_raw_mode().unwrap(); + return Err(ReturnType::Exit); + } + + text_input.handle_event(event); + } + _ => text_input.handle_event(event), + }, + _ => {} + } + + // Render the prompt + execute!( + io::stdout(), + MoveToColumn(0), + terminal::Clear(ClearType::FromCursorDown) + ) + .unwrap(); + print(format!("{}{}", prompt, text_input.input)); + execute!(io::stdout(), MoveDown(1), MoveToColumn(0)).unwrap(); + + // Render the list + kv_items.iter_mut().for_each(|x| { + x.matched = matcher + .fuzzy_match(&x.value.to_string(), &text_input.input) + .is_some() + }); + let visible = kv_items.iter().filter(|x| x.matched).count(); + + if !kv_items[selected].matched { + if let Some(found) = kv_items.iter().find(|&x| x.matched) { + selected = found.key; + } else { + selected = 0; + } + } + + let diff = (selected + 1) as isize - (usable_rows / 2) as isize; + let mut offset = 0; + if diff <= 0 { + offset = 0; + } else if diff > 0 { + offset = diff as usize; + } + if visible < usable_rows { + offset = 0; + } else if offset > visible - usable_rows { + offset = visible - usable_rows; + } + + let rendered = render_list( + &kv_items, + selected, + offset, + usable_rows, + &matcher, + text_input.input.clone(), + ); + + // Go back to the prompt + execute!( + io::stdout(), + MoveUp((rendered + 1) as u16), + MoveToColumn((prompt_length + text_input.cursor_position) as u16) + ) + .unwrap(); + + io::stdout().flush().unwrap(); + } + } + + disable_raw_mode().unwrap(); + + Ok(items[selected].clone()) +} + +/// Render the list of items +/// This function assumes that the items are already filtered, and correctly offset, and uses the matcher to color the items +fn render_list( + items: &Vec>, + selected: usize, + offset: usize, + usable_rows: usize, + matcher: &SkimMatcherV2, + input: String, +) -> usize +where + T: Display + Clone, +{ + let mut rendered = 0; + let mut i = offset; + while rendered < usable_rows { + if i >= items.len() { + break; + } + + if !items[i].matched { + i += 1; + continue; + } + + if items[i].key == selected { + print("$cc `>` "); + } else if rendered == 0 && offset > 0 { + print("⌃ "); + } else if rendered == usable_rows - 1 && i < items.len() - 1 { + print("⌄ "); + } else { + print(" "); + } + + let matched_letters = matcher + .fuzzy_indices(&items[i].value.to_string(), &input) + .unwrap_or((0, vec![])) + .1; + + let word = items[i].value.to_string(); + + for (byte_idx, ch) in word.char_indices() { + if matched_letters.iter().any(|&x| x == byte_idx) { + print(format!("$cc `{}`", ch)); + } else { + print(format!("{}", ch)); + } + } + + rendered += 1; + i += 1; + + execute!(io::stdout(), MoveDown(1), MoveToColumn(0)).unwrap(); + } + + rendered +} + +fn get_user_text_input(position: usize, input_type: TextInputType) -> Result { + super::init(); + + let mut text_input = TextInput::new(position, input_type); + + loop { + if let Ok(event) = event::read() { + match event { + event::Event::Key(event) => match event.modifiers { + KeyModifiers::CONTROL => match event.code { + KeyCode::Char('c') => { + write!(io::stdout(), "\n\r").unwrap(); + disable_raw_mode().unwrap(); + return Err(ReturnType::Exit); + } + _ => text_input.handle_event(event), + }, + KeyModifiers::NONE => match event.code { + KeyCode::Esc => { + execute!( + io::stdout(), + MoveToColumn(position as u16), + terminal::Clear(ClearType::UntilNewLine) + ) + .unwrap(); + print("$cr `canceled`\n"); + + disable_raw_mode().unwrap(); + return Err(ReturnType::Cancel); + } + KeyCode::Enter => { + execute!( + io::stdout(), + MoveToColumn(position as u16), + terminal::Clear(ClearType::UntilNewLine) + ) + .unwrap(); + if input_type == TextInputType::Password { + print(format!("$cw$b `{}`", "*".repeat(text_input.input.len()))); + } else { + print(format!("$cw$b `{}`", text_input.input)); + } + break; + } + _ => text_input.handle_event(event), + }, + _ => text_input.handle_event(event), + }, + _ => {} + } + + io::stdout().flush().unwrap(); + } + } + + write!(io::stdout(), "\n\r").unwrap(); + + disable_raw_mode().unwrap(); + Ok(text_input.input) +} diff --git a/src/view/mod.rs b/src/view/mod.rs new file mode 100644 index 0000000..77b512f --- /dev/null +++ b/src/view/mod.rs @@ -0,0 +1,273 @@ +use crossterm::{ + cursor::{MoveToColumn, MoveToNextLine}, + execute, + style::{Attribute, Color, Print, SetAttribute, SetBackgroundColor, SetForegroundColor}, + terminal::{disable_raw_mode, enable_raw_mode, Clear, ClearType}, +}; +use std::io::stdout; + +pub mod input; +pub mod spinner; + +pub fn init() { + enable_raw_mode().unwrap(); +} + +pub fn clean_up() { + disable_raw_mode().unwrap(); +} + +enum VisualEffect { + SetAttribute(Attribute), + SetForegroundColor(Color), + SetBackgroundColor(Color), +} + +fn match_effect(effect: &str) -> Vec { + let mut effects: Vec = Vec::new(); + + let mut i = 1; + while i < effect.len() { + let c = effect.chars().nth(i).unwrap(); + + let mut special = String::new(); + special.push(c); + + i += 1; + + while i < effect.len() && effect.chars().nth(i).unwrap() != '$' { + special.push(effect.chars().nth(i).unwrap()); + i += 1; + } + + match special.as_str() { + "b" => effects.push(VisualEffect::SetAttribute(Attribute::Bold)), + "i" => effects.push(VisualEffect::SetAttribute(Attribute::Italic)), + "u" => effects.push(VisualEffect::SetAttribute(Attribute::Underlined)), + "s" => effects.push(VisualEffect::SetAttribute(Attribute::Dim)), + + "cr" => effects.push(VisualEffect::SetForegroundColor(Color::Red)), + "cg" => effects.push(VisualEffect::SetForegroundColor(Color::Green)), + "cb" => effects.push(VisualEffect::SetForegroundColor(Color::Blue)), + "cy" => effects.push(VisualEffect::SetForegroundColor(Color::Yellow)), + "cm" => effects.push(VisualEffect::SetForegroundColor(Color::Magenta)), + "cc" => effects.push(VisualEffect::SetForegroundColor(Color::Cyan)), + "cw" => effects.push(VisualEffect::SetForegroundColor(Color::White)), + + "br" => effects.push(VisualEffect::SetBackgroundColor(Color::Red)), + "bg" => effects.push(VisualEffect::SetBackgroundColor(Color::Green)), + "bb" => effects.push(VisualEffect::SetBackgroundColor(Color::Blue)), + "by" => effects.push(VisualEffect::SetBackgroundColor(Color::Yellow)), + "bm" => effects.push(VisualEffect::SetBackgroundColor(Color::Magenta)), + "bc" => effects.push(VisualEffect::SetBackgroundColor(Color::Cyan)), + "bw" => effects.push(VisualEffect::SetBackgroundColor(Color::White)), + _ => {} + } + + i += 1; + } + + effects +} + +fn set_new_effects(stdout: &mut std::io::Stdout, effects: &Vec>) { + execute!(stdout, SetAttribute(Attribute::Reset)).unwrap(); + for effect in effects { + for e in effect { + match e { + VisualEffect::SetAttribute(a) => { + execute!(stdout, SetAttribute(*a)).unwrap(); + } + VisualEffect::SetForegroundColor(c) => { + execute!(stdout, SetForegroundColor(*c)).unwrap(); + } + VisualEffect::SetBackgroundColor(c) => { + execute!(stdout, SetBackgroundColor(*c)).unwrap(); + } + } + } + } +} + +pub struct PrintSize { + pub cols: usize, + pub rows: usize, +} + +/** +This function is used to print a string with special effects. + +The special effects are defined by the following syntax: $effect `content` +- $b: bold +- $i: italic +- $u: underline + +- $cr: red color +- $cg: green color +- $cb: blue color +- $cy: yellow color +- $cm: magenta color +- $cc: cyan color +- $cw: white color + +- $br: background red color +- $bg: background green color +- $bb: background blue color +- $by: background yellow color +- $bm: background magenta color +- $bc: background cyan color +- $bw: background white color + +- &>: tab (4 spaces) + +Multiple effects can be combined, ex. $b$u - bold underline, or $b `Bold and $u `underline`` + */ +pub fn printer(content: impl AsRef) -> PrintSize { + enable_raw_mode().unwrap(); + let size = print(content); + disable_raw_mode().unwrap(); + + size +} + +/** Print the content with correct formatting, but without going into raw mode +This function is used to print a string with special effects. + +The special effects are defined by the following syntax: $effect `content` +- $b: bold +- $i: italic +- $u: underline + +- $cr: red color +- $cg: green color +- $cb: blue color +- $cy: yellow color +- $cm: magenta color +- $cc: cyan color +- $cw: white color + +- $br: background red color +- $bg: background green color +- $bb: background blue color +- $by: background yellow color +- $bm: background magenta color +- $bc: background cyan color +- $bw: background white color + +- &>: tab (4 spaces) + +Multiple effects can be combined, ex. $b$u - bold underline, or $b `Bold and $u `underline`` +*/ +pub fn print(content: impl AsRef) -> PrintSize { + let mut size = PrintSize { cols: 0, rows: 0 }; + let mut current_width: usize = 0; + + let content = content.as_ref(); + let chars = content.chars(); + let n = chars.count(); + let mut stdout = stdout(); + + let mut effects: Vec> = Vec::new(); + + let mut i = 0; + while i < n { + let c = content.chars().nth(i).unwrap(); + + // Add a special effect + if c == '$' { + let mut special = String::new(); + + while i < n && content.chars().nth(i).unwrap() != ' ' { + special.push(content.chars().nth(i).unwrap()); + i += 1; + } + + let effect = match_effect(&special); + effects.push(effect); + set_new_effects(&mut stdout, &effects); + + i += 2; + + continue; + } + + // Clear the last effect + if c == '`' { + effects.pop(); + + set_new_effects(&mut stdout, &effects); + + i += 1; + continue; + } + + // Print a new line and clear the spaces + if c == '\n' { + if current_width > size.cols { + size.cols = current_width; + size.rows += 1; + } + + execute!(stdout, Print('\n'), MoveToNextLine(1)).unwrap(); + i += 1; + + while i < n && content.chars().nth(i).unwrap() == ' ' { + i += 1; + } + + continue; + } + + if c == '&' { + i += 1; + + if i >= n { + break; + } + + let c = content.chars().nth(i).unwrap(); + + // Print a 4 wide space (tab) + if c == '>' { + execute!(stdout, Print(" ")).unwrap(); + } + + i += 1; + continue; + } + + current_width += 1; + execute!(stdout, Print(c)).unwrap(); + + i += 1; + } + + if current_width > size.cols { + size.cols = current_width; + size.rows += 1; + } + + size +} + +pub fn clear_line() { + execute!( + stdout(), + MoveToColumn(0), + Clear(ClearType::CurrentLine), + MoveToColumn(0) + ) + .unwrap(); +} + +pub fn no_subcommand_error() { + let error_message = r#" + $b$cr `error`: no subcommand provided + + $b$u `Usage`: $b `tgh` [COMMAND] + + For more information try $b `'tgh --help'` + "#; + + printer(error_message); +} diff --git a/src/view/spinner.rs b/src/view/spinner.rs new file mode 100644 index 0000000..e53d80b --- /dev/null +++ b/src/view/spinner.rs @@ -0,0 +1,100 @@ +use super::*; +use std::sync::{Arc, Mutex}; + +const SPINNER_FRAMES: [&str; 10] = ["⠋", "⠙", "⠹", "⠸", "⠼", "⠴", "⠦", "⠧", "⠇", "⠏"]; + +pub struct Spinner<'a> { + message: &'a str, + + thread_handle: Option>, + should_stop: Arc>, +} + +impl<'a> Spinner<'a> { + pub fn new(message: &'a str) -> Self { + let mut spinner = Spinner { + message, + thread_handle: None, + should_stop: Arc::new(Mutex::new(false)), + }; + spinner.start(); + spinner + } + + fn start(&mut self) { + let message = self.message.to_string(); + let should_stop = Arc::clone(&self.should_stop); + self.thread_handle = Some(std::thread::spawn(move || { + let mut i = 0; + loop { + if *(should_stop.lock().unwrap()) { + break; + } + + let frame = SPINNER_FRAMES[i % SPINNER_FRAMES.len()]; + printer(format!("{} {}", frame, message)); + std::io::Write::flush(&mut stdout()).unwrap(); + print!("\r"); + std::thread::sleep(std::time::Duration::from_millis(75)); + i += 1; + } + })); + } + + pub fn stop(&mut self) { + if let Some(handle) = self.thread_handle.take() { + *(self.should_stop.lock().unwrap()) = true; + let _ = handle.join(); + } + } + + pub fn stop_with_message(&mut self, message: &str) { + self.stop(); + clear_line(); + printer(message.to_string()); + if !message.ends_with('\n') { + print!("\n"); + } + } +} + +#[cfg(test)] +mod tests { + use super::*; + use std::time::Duration; + + #[test] + fn test_spinner_lifecycle() { + let mut spinner = Spinner::new("Testing spinner..."); + std::thread::sleep(Duration::from_millis(200)); + + spinner.stop(); + + // Verify state after stop + assert!( + *spinner.should_stop.lock().unwrap(), + "Spinner should be marked as stopped after stop()" + ); + assert!( + spinner.thread_handle.is_none(), + "Thread handle should be None after stop()" + ); + } + + #[test] + fn test_spinner_stop_with_message() { + let mut spinner = Spinner::new("Testing spinner with message..."); + + std::thread::sleep(Duration::from_millis(200)); + spinner.stop_with_message("Done!"); + + assert!( + *spinner.should_stop.lock().unwrap(), + "Spinner should be marked as stopped after stop_with_message()" + ); + assert!( + spinner.thread_handle.is_none(), + "Thread handle should be None after stop_with_message()" + ); + } +}