diff --git a/.github/workflows/aur.yml b/.github/workflows/aur.yml index 5bbed3a..370cbee 100644 --- a/.github/workflows/aur.yml +++ b/.github/workflows/aur.yml @@ -16,7 +16,7 @@ jobs: uses: actions/checkout@v6 - name: Publish AUR package - uses: KSXGitHub/github-actions-deploy-aur@v4.1.1 + uses: KSXGitHub/github-actions-deploy-aur@v4.1.2 with: pkgname: rustmius-bin pkgbuild: ./PKGBUILD diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index d033442..28c1f11 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -2,6 +2,10 @@ name: Continuous Integration on: push: + files: + - '**/*.rs' + - 'Cargo.toml' + - 'Cargo.lock' permissions: contents: read diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index 1c91005..ffd7e85 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -13,15 +13,15 @@ on: - minor - major -permissions: - contents: write - jobs: prepare: name: Prepare Version runs-on: ubuntu-latest + permissions: + contents: write outputs: new_version: ${{ steps.bump.outputs.new_version }} + version_only: ${{ steps.bump.outputs.version_only }} sha: ${{ steps.commit.outputs.sha }} steps: - name: Checkout @@ -61,15 +61,14 @@ jobs: NEW_VERSION="v$MAJOR.$MINOR.$PATCH" VERSION_ONLY="$MAJOR.$MINOR.$PATCH" - echo "Bumping $LATEST_TAG -> $NEW_VERSION ($LEVEL)" echo "new_version=$NEW_VERSION" >> $GITHUB_OUTPUT echo "version_only=$VERSION_ONLY" >> $GITHUB_OUTPUT - name: Update Cargo.toml run: | VERSION="${{ steps.bump.outputs.version_only }}" - sed -i "s/^version = \".*\"/version = \"$VERSION\"/" Cargo.toml - cargo metadata --format-version 1 > /dev/null # Updates Cargo.lock if needed + awk '/^\[package\]/ {print; p=1; next} p && /^version =/ {print "version = \"'$VERSION'\""; p=0; next} {print}' Cargo.toml > Cargo.toml.tmp && mv Cargo.toml.tmp Cargo.toml + cargo update --workspace - name: Commit and Push id: commit @@ -78,8 +77,13 @@ jobs: git config --global user.email "github-actions[bot]@users.noreply.github.com" git add Cargo.toml Cargo.lock - git commit -m "chore: bump version to ${{ steps.bump.outputs.new_version }}" - git push + + if [ -z "$(git status --porcelain)" ]; then + echo "nothing to commit" + else + git commit -m "chore: bump version to ${{ steps.bump.outputs.new_version }}" + git push + fi echo "sha=$(git rev-parse HEAD)" >> $GITHUB_OUTPUT @@ -87,6 +91,10 @@ jobs: name: Build (${{ matrix.suffix }}) needs: prepare runs-on: ubuntu-latest + permissions: + contents: read + actions: read + id-token: none strategy: fail-fast: false matrix: @@ -97,20 +105,17 @@ jobs: - target: x86_64-unknown-linux-gnu cpu: x86-64-v3 suffix: x86_64-v3 - - target: aarch64-unknown-linux-gnu - cpu: generic - suffix: aarch64 steps: - name: Checkout code - uses: actions/checkout@v4 + uses: actions/checkout@v6 with: ref: ${{ needs.prepare.outputs.sha }} - name: Install dependencies run: | sudo apt-get update - sudo apt-get install -y libgtk-4-dev libvte-2.91-gtk4-dev libadwaita-1-dev + sudo apt-get install -y libgtk-4-dev libvte-2.91-gtk4-dev - name: Setup Rust uses: dtolnay/rust-toolchain@stable @@ -120,19 +125,11 @@ jobs: - name: Rust Cache uses: Swatinem/rust-cache@v2 - - name: Install Cross - run: cargo install cross --git https://github.com/cross-rs/cross.git - - - name: Build (Super Optimized) + - name: Build (Performance optimized) run: | export RUSTFLAGS="-C target-cpu=${{ matrix.cpu }}" if [ "${{ matrix.cpu }}" = "generic" ]; then export RUSTFLAGS=""; fi - - if [ "${{ matrix.target }}" = "x86_64-unknown-linux-gnu" ]; then - cargo build --release --target ${{ matrix.target }} - else - cross build --release --target ${{ matrix.target }} - fi + cargo build --release --target ${{ matrix.target }} - name: Prepare binary run: | @@ -141,25 +138,27 @@ jobs: mv rustmius rustmius-${{ matrix.suffix }} - name: Upload Artifact - uses: actions/upload-artifact@v4 + uses: actions/upload-artifact@v7 with: name: binary-${{ matrix.suffix }} path: target/${{ matrix.target }}/release/rustmius-${{ matrix.suffix }} release: name: Create Release - needs: [build, prepare] + needs: [prepare, build] runs-on: ubuntu-latest + permissions: + contents: write steps: - name: Download Artifacts - uses: actions/download-artifact@v4 + uses: actions/download-artifact@v8 with: path: artifacts pattern: binary-* merge-multiple: true - name: Release - uses: softprops/action-gh-release@v2 + uses: softprops/action-gh-release@v3 with: tag_name: ${{ needs.prepare.outputs.new_version }} target_commitish: ${{ needs.prepare.outputs.sha }} @@ -167,3 +166,63 @@ jobs: generate_release_notes: true env: GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} + + aur: + name: Update AUR + needs: [prepare, build] + runs-on: ubuntu-latest + permissions: + contents: write + steps: + - name: Checkout + uses: actions/checkout@v6 + + - name: Download Artifacts + uses: actions/download-artifact@v8 + with: + name: binary-x86_64 + path: . + + - name: Update PKGBUILD + run: | + VERSION="${{ needs.prepare.outputs.version_only }}" + HASH_DESKTOP=$(sha256sum rustmius.desktop | cut -d' ' -f1) + HASH_PNG=$(sha256sum rustmius.png | cut -d' ' -f1) + HASH_BINARY=$(sha256sum rustmius-x86_64 | cut -d' ' -f1) + + # Update version, pkgrel and hashes using AWK (more portable than updpkgsums on Ubuntu runners) + awk -v v="$VERSION" -v d="$HASH_DESKTOP" -v p="$HASH_PNG" -v b="$HASH_BINARY" ' + BEGIN { h[0]=d; h[1]=p; h[2]=b; i=0 } + /^pkgver=/ { $0="pkgver=" v } + /^pkgrel=/ { $0="pkgrel=1" } + /'\''[0-9a-f]{64}'\''/ && i < 3 { + sub(/'\''[0-9a-f]{64}'\''/, "'\''" h[i++] "'\''") + } + { print } + ' PKGBUILD > PKGBUILD.tmp && mv PKGBUILD.tmp PKGBUILD + + - name: Commit and Push PKGBUILD + run: | + git config --global user.name "github-actions[bot]" + git config --global user.email "github-actions[bot]@users.noreply.github.com" + + git add PKGBUILD + + if [ -z "$(git status --porcelain)" ]; then + echo "nothing to commit" + else + git commit -m "chore: update PKGBUILD to ${{ needs.prepare.outputs.new_version }}" + git pull --rebase origin master + git push origin master + fi + + - name: Publish AUR package + uses: KSXGitHub/github-actions-deploy-aur@v4.1.2 + with: + pkgname: rustmius-bin + pkgbuild: ./PKGBUILD + commit_username: ${{ secrets.AUR_USERNAME }} + commit_email: ${{ secrets.AUR_EMAIL }} + ssh_private_key: ${{ secrets.AUR_SSH_PRIVATE_KEY }} + commit_message: "Update to ${{ needs.prepare.outputs.new_version }}" + ssh_keyscan_types: rsa,ecdsa,ed25519 diff --git a/Cargo.lock b/Cargo.lock index f6e84ec..49431ce 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -30,112 +30,31 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "7f202df86484c868dbad7eaa557ef785d5c66295e41b460ef922eca0723b842c" [[package]] -name = "async-broadcast" -version = "0.7.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "435a87a52755b8f27fcf321ac4f04b2802e337c8c4872923137471ec39c37532" -dependencies = [ - "event-listener", - "event-listener-strategy", - "futures-core", - "pin-project-lite", -] - -[[package]] -name = "async-channel" -version = "2.5.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "924ed96dd52d1b75e9c1a3e6275715fd320f5f9439fb5a4a11fa51f4221158d2" -dependencies = [ - "concurrent-queue", - "event-listener-strategy", - "futures-core", - "pin-project-lite", -] - -[[package]] -name = "async-executor" -version = "1.14.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c96bf972d85afc50bf5ab8fe2d54d1586b4e0b46c97c50a0c9e71e2f7bcd812a" -dependencies = [ - "async-task", - "concurrent-queue", - "fastrand", - "futures-lite", - "pin-project-lite", - "slab", -] - -[[package]] -name = "async-fs" -version = "2.2.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8034a681df4aed8b8edbd7fbe472401ecf009251c8b40556b304567052e294c5" -dependencies = [ - "async-lock", - "blocking", - "futures-lite", -] - -[[package]] -name = "async-io" -version = "2.6.0" +name = "ashpd" +version = "0.13.9" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "456b8a8feb6f42d237746d4b3e9a178494627745c3c56c6ea55d92ba50d026fc" +checksum = "13bdf0fd848239dcd5e64eeeee35dbc00378ebcc6f3aa4ead0a305eec83d0cfb" dependencies = [ - "autocfg", - "cfg-if", - "concurrent-queue", - "futures-io", - "futures-lite", - "parking", - "polling", - "rustix", - "slab", - "windows-sys 0.61.2", + "enumflags2", + "futures-util", + "getrandom 0.4.2", + "serde", + "tokio", + "zbus", ] [[package]] -name = "async-lock" -version = "3.4.2" +name = "async-broadcast" +version = "0.7.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "290f7f2596bd5b78a9fec8088ccd89180d7f9f55b94b0576823bbbdc72ee8311" +checksum = "435a87a52755b8f27fcf321ac4f04b2802e337c8c4872923137471ec39c37532" dependencies = [ "event-listener", "event-listener-strategy", + "futures-core", "pin-project-lite", ] -[[package]] -name = "async-net" -version = "2.0.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b948000fad4873c1c9339d60f2623323a0cfd3816e5181033c6a5cb68b2accf7" -dependencies = [ - "async-io", - "blocking", - "futures-lite", -] - -[[package]] -name = "async-process" -version = "2.5.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "fc50921ec0055cdd8a16de48773bfeec5c972598674347252c0399676be7da75" -dependencies = [ - "async-channel", - "async-io", - "async-lock", - "async-signal", - "async-task", - "blocking", - "cfg-if", - "event-listener", - "futures-lite", - "rustix", -] - [[package]] name = "async-recursion" version = "1.1.1" @@ -147,30 +66,6 @@ dependencies = [ "syn", ] -[[package]] -name = "async-signal" -version = "0.2.13" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "43c070bbf59cd3570b6b2dd54cd772527c7c3620fce8be898406dd3ed6adc64c" -dependencies = [ - "async-io", - "async-lock", - "atomic-waker", - "cfg-if", - "futures-core", - "futures-io", - "rustix", - "signal-hook-registry", - "slab", - "windows-sys 0.61.2", -] - -[[package]] -name = "async-task" -version = "4.7.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8b75356056920673b02621b35afd0f7dda9306d03c79a30f5c56c44cf256e3de" - [[package]] name = "async-trait" version = "0.1.89" @@ -182,12 +77,6 @@ dependencies = [ "syn", ] -[[package]] -name = "atomic-waker" -version = "1.1.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1505bd5d3d116872e7271a6d4e16d81d0c8570876c8de68093a09ac269d8aac0" - [[package]] name = "autocfg" version = "1.5.0" @@ -218,19 +107,6 @@ dependencies = [ "generic-array", ] -[[package]] -name = "blocking" -version = "1.6.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e83f8d02be6967315521be875afa792a316e28d57b5a2d401897e2a7921b7f21" -dependencies = [ - "async-channel", - "async-task", - "futures-io", - "futures-lite", - "piper", -] - [[package]] name = "bumpalo" version = "3.20.2" @@ -301,12 +177,6 @@ version = "1.0.4" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "9330f8b2ff13f34540b44e946ef35111825727b38d33286ef986142615121801" -[[package]] -name = "cfg_aliases" -version = "0.2.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "613afe47fcd5fac7ccf1db93babcb082c5994d996f20b8b159f2ad1658eb5724" - [[package]] name = "chrono" version = "0.4.44" @@ -374,7 +244,6 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "78c8292055d1c1df0cce5d180393dc8cce0abec0a7102adb6c7b1eef6016d60a" dependencies = [ "generic-array", - "rand_core", "typenum", ] @@ -558,12 +427,6 @@ dependencies = [ "syn", ] -[[package]] -name = "futures-sink" -version = "0.3.32" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c39754e157331b013978ec91992bde1ac089843443c49cbc7f46150b0fad0893" - [[package]] name = "futures-task" version = "0.3.32" @@ -577,11 +440,8 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "389ca41296e6190b48053de0321d02a77f32f8a5d2461dd38762c0593805c6d6" dependencies = [ "futures-core", - "futures-io", "futures-macro", - "futures-sink", "futures-task", - "memchr", "pin-project-lite", "slab", ] @@ -664,6 +524,18 @@ dependencies = [ "wasi", ] +[[package]] +name = "getrandom" +version = "0.3.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "899def5c37c4fd7b2664648c28120ecec138e4d395b459e5ca34f9cce2dd77fd" +dependencies = [ + "cfg-if", + "libc", + "r-efi 5.3.0", + "wasip2", +] + [[package]] name = "getrandom" version = "0.4.2" @@ -672,7 +544,7 @@ checksum = "0de51e6874e94e7bf76d726fc5d13ba782deca734ff60d5bb2fb2607c7406555" dependencies = [ "cfg-if", "libc", - "r-efi", + "r-efi 6.0.0", "wasip2", "wasip3", ] @@ -889,12 +761,6 @@ version = "0.5.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "2304e00983f87ffb38b55b444b5e3b60a884b5d30c0fca7d82fe33449bbe55ea" -[[package]] -name = "hermit-abi" -version = "0.5.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "fc0fef456e4baa96da950455cd02c081ca953b141298e41db3fc7e36b1da849c" - [[package]] name = "hex" version = "0.4.3" @@ -993,15 +859,6 @@ dependencies = [ "wasm-bindgen", ] -[[package]] -name = "lazy_static" -version = "1.5.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "bbd2bcb4c963f2ddae06a2efc7e9f3591312473c50c6685e1f298068316e66fe" -dependencies = [ - "spin", -] - [[package]] name = "leb128fmt" version = "0.1.0" @@ -1112,19 +969,6 @@ 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", - "cfg-if", - "cfg_aliases", - "libc", - "memoffset", -] - [[package]] name = "nucleo-matcher" version = "0.2.0" @@ -1162,15 +1006,15 @@ dependencies = [ [[package]] name = "num-bigint-dig" -version = "0.8.6" +version = "0.9.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e661dda6640fad38e827a6d4a310ff4763082116fe217f279885c97f511bb0b7" +checksum = "a7f9a86e097b0d187ad0e65667c2f58b9254671e86e7dbb78036b16692eae099" dependencies = [ - "lazy_static", "libm", "num-integer", "num-iter", "num-traits", + "once_cell", "rand", "serde", "smallvec", @@ -1234,33 +1078,31 @@ checksum = "9f7c3e4beb33f85d45ae3e3a1792185706c8e16d043238c593331cc7cd313b50" [[package]] name = "oo7" -version = "0.3.3" +version = "0.6.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8fc6ce4692fbfd044ce22ca07dcab1a30fa12432ca2aa5b1294eca50d3332a24" +checksum = "78f2bfed90f1618b4b48dcad9307f25e14ae894e2949642c87c351601d62cebd" dependencies = [ "aes", - "async-fs", - "async-io", - "async-lock", - "async-net", - "blocking", + "ashpd", "cbc", "cipher", "digest", "endi", - "futures-lite", "futures-util", + "getrandom 0.4.2", "hkdf", "hmac", "md-5", "num", "num-bigint-dig", "pbkdf2", - "rand", "serde", + "serde_bytes", "sha2", "subtle", + "tokio", "zbus", + "zbus_macros", "zeroize", "zvariant", ] @@ -1362,37 +1204,12 @@ version = "0.2.17" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "a89322df9ebe1c1578d689c92318e070967d1042b512afbe49518723f4e6d5cd" -[[package]] -name = "piper" -version = "0.2.5" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c835479a4443ded371d6c535cbfd8d31ad92c5d23ae9770a61bc155e4992a3c1" -dependencies = [ - "atomic-waker", - "fastrand", - "futures-io", -] - [[package]] name = "pkg-config" version = "0.3.32" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "7edddbd0b52d732b21ad9a5fab5c704c14cd949e5e9a1ec5929a24fded1b904c" -[[package]] -name = "polling" -version = "3.11.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5d0e4f59085d47d8241c88ead0f274e8a0cb551f3625263c05eb8dd897c34218" -dependencies = [ - "cfg-if", - "concurrent-queue", - "hermit-abi", - "pin-project-lite", - "rustix", - "windows-sys 0.61.2", -] - [[package]] name = "ppv-lite86" version = "0.2.21" @@ -1439,6 +1256,12 @@ dependencies = [ "proc-macro2", ] +[[package]] +name = "r-efi" +version = "5.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "69cdb34c158ceb288df11e18b4bd39de994f6657d83847bdffdbd7f346754b0f" + [[package]] name = "r-efi" version = "6.0.0" @@ -1447,20 +1270,19 @@ checksum = "f8dcc9c7d52a811697d2151c701e0d08956f92b0e24136cf4cf27b57a6a0d9bf" [[package]] name = "rand" -version = "0.8.5" +version = "0.9.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "34af8d1a0e25924bc5b7c43c079c942339d8f0a8b57c39049bef581b46327404" +checksum = "6db2770f06117d490610c7488547d543617b21bfa07796d7a12f6f1bd53850d1" dependencies = [ - "libc", "rand_chacha", "rand_core", ] [[package]] name = "rand_chacha" -version = "0.3.1" +version = "0.9.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e6c10a63a0fa32252be49d21e7709d4d4baf8d231c2dbce1eaa8141b9b127d88" +checksum = "d3022b5f1df60f26e1ffddd6c66e8aa15de382ae63b3a0c1bfc0e4d3e3f325cb" dependencies = [ "ppv-lite86", "rand_core", @@ -1468,11 +1290,11 @@ dependencies = [ [[package]] name = "rand_core" -version = "0.6.4" +version = "0.9.5" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ec0be4795e2f6a28069bec0b5ff3e2ac9bafc99e6a9a7dc3547996c5c816922c" +checksum = "76afc826de14238e6e8c374ddcc1fa19e374fd8dd986b0d2af0d02377261d83c" dependencies = [ - "getrandom 0.2.17", + "getrandom 0.3.4", ] [[package]] @@ -1519,7 +1341,7 @@ dependencies = [ [[package]] name = "rustmius" -version = "2.0.0" +version = "2.1.0" dependencies = [ "anyhow", "chrono", @@ -1561,6 +1383,16 @@ dependencies = [ "serde_derive", ] +[[package]] +name = "serde_bytes" +version = "0.11.19" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a5d440709e79d88e51ac01c4b72fc6cb7314017bb7da9eeff678aa94c10e3ea8" +dependencies = [ + "serde", + "serde_core", +] + [[package]] name = "serde_core" version = "1.0.228" @@ -1614,17 +1446,6 @@ dependencies = [ "serde_core", ] -[[package]] -name = "sha1" -version = "0.10.6" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e3bf829a2d51ab4a5ddf1352d8470c140cadc8301b2ae1789db023f01cedd6ba" -dependencies = [ - "cfg-if", - "cpufeatures", - "digest", -] - [[package]] name = "sha2" version = "0.10.9" @@ -1674,12 +1495,6 @@ dependencies = [ "windows-sys 0.61.2", ] -[[package]] -name = "spin" -version = "0.9.8" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6980e8d7511241f8acf4aebddbb1ff938df5eebe98691418c4468d0b72a96a67" - [[package]] name = "ssh2" version = "0.9.5" @@ -1692,12 +1507,6 @@ dependencies = [ "parking_lot", ] -[[package]] -name = "static_assertions" -version = "1.1.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a2eb9349b6444b326872e140eb1cf5e7c522154d69e7a0ffb0fb81c06b37543f" - [[package]] name = "subtle" version = "2.6.1" @@ -1769,9 +1578,9 @@ dependencies = [ [[package]] name = "tokio" -version = "1.51.0" +version = "1.51.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2bd1c4c0fc4a7ab90fc15ef6daaa3ec3b893f004f915f2392557ed23237820cd" +checksum = "f66bf9585cda4b724d3e78ab34b73fb2bbaba9011b9bfdf69dc836382ea13b8c" dependencies = [ "bytes", "libc", @@ -1781,6 +1590,7 @@ dependencies = [ "signal-hook-registry", "socket2", "tokio-macros", + "tracing", "windows-sys 0.61.2", ] @@ -1807,7 +1617,7 @@ dependencies = [ "toml_datetime", "toml_parser", "toml_writer", - "winnow", + "winnow 1.0.1", ] [[package]] @@ -1828,7 +1638,7 @@ dependencies = [ "indexmap", "toml_datetime", "toml_parser", - "winnow", + "winnow 1.0.1", ] [[package]] @@ -1837,7 +1647,7 @@ version = "1.1.2+spec-1.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "a2abe9b86193656635d2411dc43050282ca48aa31c2451210f4202550afb7526" dependencies = [ - "winnow", + "winnow 1.0.1", ] [[package]] @@ -1912,6 +1722,17 @@ version = "0.2.6" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "ebc1c04c71510c7f702b52b7c350734c9ff1295c464a03335b00bb84fc54f853" +[[package]] +name = "uuid" +version = "1.23.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5ac8b6f42ead25368cf5b098aeb3dc8a1a2c05a3eee8a9a1a68c640edbfc79d9" +dependencies = [ + "js-sys", + "serde_core", + "wasm-bindgen", +] + [[package]] name = "vcpkg" version = "0.2.15" @@ -2131,25 +1952,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.52.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "282be5f36a8ce781fad8c8ae18fa3f9beff57ec1b52cb3de0789201425d9a33d" -dependencies = [ - "windows-targets 0.52.6", -] - -[[package]] -name = "windows-sys" -version = "0.59.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1e38bc4d79ed67fd075bcc251a1c39b32a1776bbe92e5bef1f0bf1f8c531853b" -dependencies = [ - "windows-targets 0.52.6", + "windows-targets", ] [[package]] @@ -2167,29 +1970,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]] @@ -2198,78 +1985,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" @@ -2277,10 +2022,13 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "ed94fce61571a4006852b7389a063ab983c02eb1bb37b47f8272ce92d06d9538" [[package]] -name = "windows_x86_64_msvc" -version = "0.52.6" +name = "winnow" +version = "0.7.15" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "589f6da84c646204747d1270a2a5661ea66ed1cced2631d546fdfb155959f9ec" +checksum = "df79d97927682d2fd8adb29682d1140b343be4ac0f08fd68b7765d9c059d3945" +dependencies = [ + "memchr", +] [[package]] name = "winnow" @@ -2379,49 +2127,31 @@ dependencies = [ "wasmparser", ] -[[package]] -name = "xdg-home" -version = "1.3.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ec1cdab258fb55c0da61328dc52c8764709b249011b2cad0454c72f0bf10a1f6" -dependencies = [ - "libc", - "windows-sys 0.59.0", -] - [[package]] name = "zbus" -version = "4.4.0" +version = "5.14.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "bb97012beadd29e654708a0fdb4c84bc046f537aecfde2c3ee0a9e4b4d48c725" +checksum = "ca82f95dbd3943a40a53cfded6c2d0a2ca26192011846a1810c4256ef92c60bc" dependencies = [ "async-broadcast", - "async-executor", - "async-fs", - "async-io", - "async-lock", - "async-process", "async-recursion", - "async-task", "async-trait", - "blocking", "enumflags2", "event-listener", "futures-core", - "futures-sink", - "futures-util", + "futures-lite", "hex", - "nix", + "libc", "ordered-stream", - "rand", + "rustix", "serde", "serde_repr", - "sha1", - "static_assertions", + "tokio", "tracing", "uds_windows", - "windows-sys 0.52.0", - "xdg-home", + "uuid", + "windows-sys 0.61.2", + "winnow 0.7.15", "zbus_macros", "zbus_names", "zvariant", @@ -2429,25 +2159,27 @@ dependencies = [ [[package]] name = "zbus_macros" -version = "4.4.0" +version = "5.14.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "267db9407081e90bbfa46d841d3cbc60f59c0351838c4bc65199ecd79ab1983e" +checksum = "897e79616e84aac4b2c46e9132a4f63b93105d54fe8c0e8f6bffc21fa8d49222" dependencies = [ "proc-macro-crate", "proc-macro2", "quote", "syn", + "zbus_names", + "zvariant", "zvariant_utils", ] [[package]] name = "zbus_names" -version = "3.0.0" +version = "4.3.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4b9b1fef7d021261cc16cba64c351d291b715febe0fa10dc3a443ac5a5022e6c" +checksum = "ffd8af6d5b78619bab301ff3c560a5bd22426150253db278f164d6cf3b72c50f" dependencies = [ "serde", - "static_assertions", + "winnow 0.7.15", "zvariant", ] @@ -2499,22 +2231,24 @@ checksum = "b8848ee67ecc8aedbaf3e4122217aff892639231befc6a1b58d29fff4c2cabaa" [[package]] name = "zvariant" -version = "4.2.0" +version = "5.10.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2084290ab9a1c471c38fc524945837734fbf124487e105daec2bb57fd48c81fe" +checksum = "5708299b21903bbe348e94729f22c49c55d04720a004aa350f1f9c122fd2540b" dependencies = [ "endi", "enumflags2", "serde", - "static_assertions", + "serde_bytes", + "winnow 0.7.15", "zvariant_derive", + "zvariant_utils", ] [[package]] name = "zvariant_derive" -version = "4.2.0" +version = "5.10.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "73e2ba546bda683a90652bac4a279bc146adad1386f25379cf73200d2002c449" +checksum = "5b59b012ebe9c46656f9cc08d8da8b4c726510aef12559da3e5f1bf72780752c" dependencies = [ "proc-macro-crate", "proc-macro2", @@ -2525,11 +2259,13 @@ dependencies = [ [[package]] name = "zvariant_utils" -version = "2.1.0" +version = "3.3.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c51bcff7cc3dbb5055396bcf774748c3dab426b4b8659046963523cee4808340" +checksum = "f75c23a64ef8f40f13a6989991e643554d9bef1d682a281160cf0c1bc389c5e9" dependencies = [ "proc-macro2", "quote", + "serde", "syn", + "winnow 0.7.15", ] diff --git a/Cargo.toml b/Cargo.toml index 5f6dd9a..0f0fbf5 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "rustmius" -version = "2.0.0" +version = "2.1.0" edition = "2024" [dependencies] @@ -12,7 +12,7 @@ nucleo-matcher = "0.2" serde = { version = "1.0", features = ["derive"] } anyhow = "1.0" directories = "5.0" -oo7 = "0.3" +oo7 = "0.6" chrono = "0.4" [profile.release] diff --git a/LICENSE b/LICENSE new file mode 100644 index 0000000..0ad25db --- /dev/null +++ b/LICENSE @@ -0,0 +1,661 @@ + GNU AFFERO GENERAL PUBLIC LICENSE + Version 3, 19 November 2007 + + Copyright (C) 2007 Free Software Foundation, Inc. + Everyone is permitted to copy and distribute verbatim copies + of this license document, but changing it is not allowed. + + Preamble + + The GNU Affero General Public License is a free, copyleft license for +software and other kinds of works, specifically designed to ensure +cooperation with the community in the case of network server software. + + The licenses for most software and other practical works are designed +to take away your freedom to share and change the works. By contrast, +our General Public Licenses are intended to guarantee your freedom to +share and change all versions of a program--to make sure it remains free +software for all its users. + + When we speak of free software, we are referring to freedom, not +price. Our General Public Licenses are designed to make sure that you +have the freedom to distribute copies of free software (and charge for +them if you wish), that you receive source code or can get it if you +want it, that you can change the software or use pieces of it in new +free programs, and that you know you can do these things. + + Developers that use our General Public Licenses protect your rights +with two steps: (1) assert copyright on the software, and (2) offer +you this License which gives you legal permission to copy, distribute +and/or modify the software. + + A secondary benefit of defending all users' freedom is that +improvements made in alternate versions of the program, if they +receive widespread use, become available for other developers to +incorporate. Many developers of free software are heartened and +encouraged by the resulting cooperation. However, in the case of +software used on network servers, this result may fail to come about. +The GNU General Public License permits making a modified version and +letting the public access it on a server without ever releasing its +source code to the public. + + The GNU Affero General Public License is designed specifically to +ensure that, in such cases, the modified source code becomes available +to the community. It requires the operator of a network server to +provide the source code of the modified version running there to the +users of that server. Therefore, public use of a modified version, on +a publicly accessible server, gives the public access to the source +code of the modified version. + + An older license, called the Affero General Public License and +published by Affero, was designed to accomplish similar goals. This is +a different license, not a version of the Affero GPL, but Affero has +released a new version of the Affero GPL which permits relicensing under +this license. + + The precise terms and conditions for copying, distribution and +modification follow. + + TERMS AND CONDITIONS + + 0. Definitions. + + "This License" refers to version 3 of the GNU Affero General Public License. + + "Copyright" also means copyright-like laws that apply to other kinds of +works, such as semiconductor masks. + + "The Program" refers to any copyrightable work licensed under this +License. Each licensee is addressed as "you". "Licensees" and +"recipients" may be individuals or organizations. + + To "modify" a work means to copy from or adapt all or part of the work +in a fashion requiring copyright permission, other than the making of an +exact copy. The resulting work is called a "modified version" of the +earlier work or a work "based on" the earlier work. + + A "covered work" means either the unmodified Program or a work based +on the Program. + + To "propagate" a work means to do anything with it that, without +permission, would make you directly or secondarily liable for +infringement under applicable copyright law, except executing it on a +computer or modifying a private copy. Propagation includes copying, +distribution (with or without modification), making available to the +public, and in some countries other activities as well. + + To "convey" a work means any kind of propagation that enables other +parties to make or receive copies. Mere interaction with a user through +a computer network, with no transfer of a copy, is not conveying. + + An interactive user interface displays "Appropriate Legal Notices" +to the extent that it includes a convenient and prominently visible +feature that (1) displays an appropriate copyright notice, and (2) +tells the user that there is no warranty for the work (except to the +extent that warranties are provided), that licensees may convey the +work under this License, and how to view a copy of this License. If +the interface presents a list of user commands or options, such as a +menu, a prominent item in the list meets this criterion. + + 1. Source Code. + + The "source code" for a work means the preferred form of the work +for making modifications to it. "Object code" means any non-source +form of a work. + + A "Standard Interface" means an interface that either is an official +standard defined by a recognized standards body, or, in the case of +interfaces specified for a particular programming language, one that +is widely used among developers working in that language. + + The "System Libraries" of an executable work include anything, other +than the work as a whole, that (a) is included in the normal form of +packaging a Major Component, but which is not part of that Major +Component, and (b) serves only to enable use of the work with that +Major Component, or to implement a Standard Interface for which an +implementation is available to the public in source code form. A +"Major Component", in this context, means a major essential component +(kernel, window system, and so on) of the specific operating system +(if any) on which the executable work runs, or a compiler used to +produce the work, or an object code interpreter used to run it. + + The "Corresponding Source" for a work in object code form means all +the source code needed to generate, install, and (for an executable +work) run the object code and to modify the work, including scripts to +control those activities. However, it does not include the work's +System Libraries, or general-purpose tools or generally available free +programs which are used unmodified in performing those activities but +which are not part of the work. For example, Corresponding Source +includes interface definition files associated with source files for +the work, and the source code for shared libraries and dynamically +linked subprograms that the work is specifically designed to require, +such as by intimate data communication or control flow between those +subprograms and other parts of the work. + + The Corresponding Source need not include anything that users +can regenerate automatically from other parts of the Corresponding +Source. + + The Corresponding Source for a work in source code form is that +same work. + + 2. Basic Permissions. + + All rights granted under this License are granted for the term of +copyright on the Program, and are irrevocable provided the stated +conditions are met. This License explicitly affirms your unlimited +permission to run the unmodified Program. The output from running a +covered work is covered by this License only if the output, given its +content, constitutes a covered work. This License acknowledges your +rights of fair use or other equivalent, as provided by copyright law. + + You may make, run and propagate covered works that you do not +convey, without conditions so long as your license otherwise remains +in force. You may convey covered works to others for the sole purpose +of having them make modifications exclusively for you, or provide you +with facilities for running those works, provided that you comply with +the terms of this License in conveying all material for which you do +not control copyright. Those thus making or running the covered works +for you must do so exclusively on your behalf, under your direction +and control, on terms that prohibit them from making any copies of +your copyrighted material outside their relationship with you. + + Conveying under any other circumstances is permitted solely under +the conditions stated below. Sublicensing is not allowed; section 10 +makes it unnecessary. + + 3. Protecting Users' Legal Rights From Anti-Circumvention Law. + + No covered work shall be deemed part of an effective technological +measure under any applicable law fulfilling obligations under article +11 of the WIPO copyright treaty adopted on 20 December 1996, or +similar laws prohibiting or restricting circumvention of such +measures. + + When you convey a covered work, you waive any legal power to forbid +circumvention of technological measures to the extent such circumvention +is effected by exercising rights under this License with respect to +the covered work, and you disclaim any intention to limit operation or +modification of the work as a means of enforcing, against the work's +users, your or third parties' legal rights to forbid circumvention of +technological measures. + + 4. Conveying Verbatim Copies. + + You may convey verbatim copies of the Program's source code as you +receive it, in any medium, provided that you conspicuously and +appropriately publish on each copy an appropriate copyright notice; +keep intact all notices stating that this License and any +non-permissive terms added in accord with section 7 apply to the code; +keep intact all notices of the absence of any warranty; and give all +recipients a copy of this License along with the Program. + + You may charge any price or no price for each copy that you convey, +and you may offer support or warranty protection for a fee. + + 5. Conveying Modified Source Versions. + + You may convey a work based on the Program, or the modifications to +produce it from the Program, in the form of source code under the +terms of section 4, provided that you also meet all of these conditions: + + a) The work must carry prominent notices stating that you modified + it, and giving a relevant date. + + b) The work must carry prominent notices stating that it is + released under this License and any conditions added under section + 7. This requirement modifies the requirement in section 4 to + "keep intact all notices". + + c) You must license the entire work, as a whole, under this + License to anyone who comes into possession of a copy. This + License will therefore apply, along with any applicable section 7 + additional terms, to the whole of the work, and all its parts, + regardless of how they are packaged. This License gives no + permission to license the work in any other way, but it does not + invalidate such permission if you have separately received it. + + d) If the work has interactive user interfaces, each must display + Appropriate Legal Notices; however, if the Program has interactive + interfaces that do not display Appropriate Legal Notices, your + work need not make them do so. + + A compilation of a covered work with other separate and independent +works, which are not by their nature extensions of the covered work, +and which are not combined with it such as to form a larger program, +in or on a volume of a storage or distribution medium, is called an +"aggregate" if the compilation and its resulting copyright are not +used to limit the access or legal rights of the compilation's users +beyond what the individual works permit. Inclusion of a covered work +in an aggregate does not cause this License to apply to the other +parts of the aggregate. + + 6. Conveying Non-Source Forms. + + You may convey a covered work in object code form under the terms +of sections 4 and 5, provided that you also convey the +machine-readable Corresponding Source under the terms of this License, +in one of these ways: + + a) Convey the object code in, or embodied in, a physical product + (including a physical distribution medium), accompanied by the + Corresponding Source fixed on a durable physical medium + customarily used for software interchange. + + b) Convey the object code in, or embodied in, a physical product + (including a physical distribution medium), accompanied by a + written offer, valid for at least three years and valid for as + long as you offer spare parts or customer support for that product + model, to give anyone who possesses the object code either (1) a + copy of the Corresponding Source for all the software in the + product that is covered by this License, on a durable physical + medium customarily used for software interchange, for a price no + more than your reasonable cost of physically performing this + conveying of source, or (2) access to copy the + Corresponding Source from a network server at no charge. + + c) Convey individual copies of the object code with a copy of the + written offer to provide the Corresponding Source. This + alternative is allowed only occasionally and noncommercially, and + only if you received the object code with such an offer, in accord + with subsection 6b. + + d) Convey the object code by offering access from a designated + place (gratis or for a charge), and offer equivalent access to the + Corresponding Source in the same way through the same place at no + further charge. You need not require recipients to copy the + Corresponding Source along with the object code. If the place to + copy the object code is a network server, the Corresponding Source + may be on a different server (operated by you or a third party) + that supports equivalent copying facilities, provided you maintain + clear directions next to the object code saying where to find the + Corresponding Source. Regardless of what server hosts the + Corresponding Source, you remain obligated to ensure that it is + available for as long as needed to satisfy these requirements. + + e) Convey the object code using peer-to-peer transmission, provided + you inform other peers where the object code and Corresponding + Source of the work are being offered to the general public at no + charge under subsection 6d. + + A separable portion of the object code, whose source code is excluded +from the Corresponding Source as a System Library, need not be +included in conveying the object code work. + + A "User Product" is either (1) a "consumer product", which means any +tangible personal property which is normally used for personal, family, +or household purposes, or (2) anything designed or sold for incorporation +into a dwelling. In determining whether a product is a consumer product, +doubtful cases shall be resolved in favor of coverage. For a particular +product received by a particular user, "normally used" refers to a +typical or common use of that class of product, regardless of the status +of the particular user or of the way in which the particular user +actually uses, or expects or is expected to use, the product. A product +is a consumer product regardless of whether the product has substantial +commercial, industrial or non-consumer uses, unless such uses represent +the only significant mode of use of the product. + + "Installation Information" for a User Product means any methods, +procedures, authorization keys, or other information required to install +and execute modified versions of a covered work in that User Product from +a modified version of its Corresponding Source. The information must +suffice to ensure that the continued functioning of the modified object +code is in no case prevented or interfered with solely because +modification has been made. + + If you convey an object code work under this section in, or with, or +specifically for use in, a User Product, and the conveying occurs as +part of a transaction in which the right of possession and use of the +User Product is transferred to the recipient in perpetuity or for a +fixed term (regardless of how the transaction is characterized), the +Corresponding Source conveyed under this section must be accompanied +by the Installation Information. But this requirement does not apply +if neither you nor any third party retains the ability to install +modified object code on the User Product (for example, the work has +been installed in ROM). + + The requirement to provide Installation Information does not include a +requirement to continue to provide support service, warranty, or updates +for a work that has been modified or installed by the recipient, or for +the User Product in which it has been modified or installed. Access to a +network may be denied when the modification itself materially and +adversely affects the operation of the network or violates the rules and +protocols for communication across the network. + + Corresponding Source conveyed, and Installation Information provided, +in accord with this section must be in a format that is publicly +documented (and with an implementation available to the public in +source code form), and must require no special password or key for +unpacking, reading or copying. + + 7. Additional Terms. + + "Additional permissions" are terms that supplement the terms of this +License by making exceptions from one or more of its conditions. +Additional permissions that are applicable to the entire Program shall +be treated as though they were included in this License, to the extent +that they are valid under applicable law. If additional permissions +apply only to part of the Program, that part may be used separately +under those permissions, but the entire Program remains governed by +this License without regard to the additional permissions. + + When you convey a copy of a covered work, you may at your option +remove any additional permissions from that copy, or from any part of +it. (Additional permissions may be written to require their own +removal in certain cases when you modify the work.) You may place +additional permissions on material, added by you to a covered work, +for which you have or can give appropriate copyright permission. + + Notwithstanding any other provision of this License, for material you +add to a covered work, you may (if authorized by the copyright holders of +that material) supplement the terms of this License with terms: + + a) Disclaiming warranty or limiting liability differently from the + terms of sections 15 and 16 of this License; or + + b) Requiring preservation of specified reasonable legal notices or + author attributions in that material or in the Appropriate Legal + Notices displayed by works containing it; or + + c) Prohibiting misrepresentation of the origin of that material, or + requiring that modified versions of such material be marked in + reasonable ways as different from the original version; or + + d) Limiting the use for publicity purposes of names of licensors or + authors of the material; or + + e) Declining to grant rights under trademark law for use of some + trade names, trademarks, or service marks; or + + f) Requiring indemnification of licensors and authors of that + material by anyone who conveys the material (or modified versions of + it) with contractual assumptions of liability to the recipient, for + any liability that these contractual assumptions directly impose on + those licensors and authors. + + All other non-permissive additional terms are considered "further +restrictions" within the meaning of section 10. If the Program as you +received it, or any part of it, contains a notice stating that it is +governed by this License along with a term that is a further +restriction, you may remove that term. If a license document contains +a further restriction but permits relicensing or conveying under this +License, you may add to a covered work material governed by the terms +of that license document, provided that the further restriction does +not survive such relicensing or conveying. + + If you add terms to a covered work in accord with this section, you +must place, in the relevant source files, a statement of the +additional terms that apply to those files, or a notice indicating +where to find the applicable terms. + + Additional terms, permissive or non-permissive, may be stated in the +form of a separately written license, or stated as exceptions; +the above requirements apply either way. + + 8. Termination. + + You may not propagate or modify a covered work except as expressly +provided under this License. Any attempt otherwise to propagate or +modify it is void, and will automatically terminate your rights under +this License (including any patent licenses granted under the third +paragraph of section 11). + + However, if you cease all violation of this License, then your +license from a particular copyright holder is reinstated (a) +provisionally, unless and until the copyright holder explicitly and +finally terminates your license, and (b) permanently, if the copyright +holder fails to notify you of the violation by some reasonable means +prior to 60 days after the cessation. + + Moreover, your license from a particular copyright holder is +reinstated permanently if the copyright holder notifies you of the +violation by some reasonable means, this is the first time you have +received notice of violation of this License (for any work) from that +copyright holder, and you cure the violation prior to 30 days after +your receipt of the notice. + + Termination of your rights under this section does not terminate the +licenses of parties who have received copies or rights from you under +this License. If your rights have been terminated and not permanently +reinstated, you do not qualify to receive new licenses for the same +material under section 10. + + 9. Acceptance Not Required for Having Copies. + + You are not required to accept this License in order to receive or +run a copy of the Program. Ancillary propagation of a covered work +occurring solely as a consequence of using peer-to-peer transmission +to receive a copy likewise does not require acceptance. However, +nothing other than this License grants you permission to propagate or +modify any covered work. These actions infringe copyright if you do +not accept this License. Therefore, by modifying or propagating a +covered work, you indicate your acceptance of this License to do so. + + 10. Automatic Licensing of Downstream Recipients. + + Each time you convey a covered work, the recipient automatically +receives a license from the original licensors, to run, modify and +propagate that work, subject to this License. You are not responsible +for enforcing compliance by third parties with this License. + + An "entity transaction" is a transaction transferring control of an +organization, or substantially all assets of one, or subdividing an +organization, or merging organizations. If propagation of a covered +work results from an entity transaction, each party to that +transaction who receives a copy of the work also receives whatever +licenses to the work the party's predecessor in interest had or could +give under the previous paragraph, plus a right to possession of the +Corresponding Source of the work from the predecessor in interest, if +the predecessor has it or can get it with reasonable efforts. + + You may not impose any further restrictions on the exercise of the +rights granted or affirmed under this License. For example, you may +not impose a license fee, royalty, or other charge for exercise of +rights granted under this License, and you may not initiate litigation +(including a cross-claim or counterclaim in a lawsuit) alleging that +any patent claim is infringed by making, using, selling, offering for +sale, or importing the Program or any portion of it. + + 11. Patents. + + A "contributor" is a copyright holder who authorizes use under this +License of the Program or a work on which the Program is based. The +work thus licensed is called the contributor's "contributor version". + + A contributor's "essential patent claims" are all patent claims +owned or controlled by the contributor, whether already acquired or +hereafter acquired, that would be infringed by some manner, permitted +by this License, of making, using, or selling its contributor version, +but do not include claims that would be infringed only as a +consequence of further modification of the contributor version. For +purposes of this definition, "control" includes the right to grant +patent sublicenses in a manner consistent with the requirements of +this License. + + Each contributor grants you a non-exclusive, worldwide, royalty-free +patent license under the contributor's essential patent claims, to +make, use, sell, offer for sale, import and otherwise run, modify and +propagate the contents of its contributor version. + + In the following three paragraphs, a "patent license" is any express +agreement or commitment, however denominated, not to enforce a patent +(such as an express permission to practice a patent or covenant not to +sue for patent infringement). To "grant" such a patent license to a +party means to make such an agreement or commitment not to enforce a +patent against the party. + + If you convey a covered work, knowingly relying on a patent license, +and the Corresponding Source of the work is not available for anyone +to copy, free of charge and under the terms of this License, through a +publicly available network server or other readily accessible means, +then you must either (1) cause the Corresponding Source to be so +available, or (2) arrange to deprive yourself of the benefit of the +patent license for this particular work, or (3) arrange, in a manner +consistent with the requirements of this License, to extend the patent +license to downstream recipients. "Knowingly relying" means you have +actual knowledge that, but for the patent license, your conveying the +covered work in a country, or your recipient's use of the covered work +in a country, would infringe one or more identifiable patents in that +country that you have reason to believe are valid. + + If, pursuant to or in connection with a single transaction or +arrangement, you convey, or propagate by procuring conveyance of, a +covered work, and grant a patent license to some of the parties +receiving the covered work authorizing them to use, propagate, modify +or convey a specific copy of the covered work, then the patent license +you grant is automatically extended to all recipients of the covered +work and works based on it. + + A patent license is "discriminatory" if it does not include within +the scope of its coverage, prohibits the exercise of, or is +conditioned on the non-exercise of one or more of the rights that are +specifically granted under this License. You may not convey a covered +work if you are a party to an arrangement with a third party that is +in the business of distributing software, under which you make payment +to the third party based on the extent of your activity of conveying +the work, and under which the third party grants, to any of the +parties who would receive the covered work from you, a discriminatory +patent license (a) in connection with copies of the covered work +conveyed by you (or copies made from those copies), or (b) primarily +for and in connection with specific products or compilations that +contain the covered work, unless you entered into that arrangement, +or that patent license was granted, prior to 28 March 2007. + + Nothing in this License shall be construed as excluding or limiting +any implied license or other defenses to infringement that may +otherwise be available to you under applicable patent law. + + 12. No Surrender of Others' Freedom. + + If conditions are imposed on you (whether by court order, agreement or +otherwise) that contradict the conditions of this License, they do not +excuse you from the conditions of this License. If you cannot convey a +covered work so as to satisfy simultaneously your obligations under this +License and any other pertinent obligations, then as a consequence you may +not convey it at all. For example, if you agree to terms that obligate you +to collect a royalty for further conveying from those to whom you convey +the Program, the only way you could satisfy both those terms and this +License would be to refrain entirely from conveying the Program. + + 13. Remote Network Interaction; Use with the GNU General Public License. + + Notwithstanding any other provision of this License, if you modify the +Program, your modified version must prominently offer all users +interacting with it remotely through a computer network (if your version +supports such interaction) an opportunity to receive the Corresponding +Source of your version by providing access to the Corresponding Source +from a network server at no charge, through some standard or customary +means of facilitating copying of software. This Corresponding Source +shall include the Corresponding Source for any work covered by version 3 +of the GNU General Public License that is incorporated pursuant to the +following paragraph. + + Notwithstanding any other provision of this License, you have +permission to link or combine any covered work with a work licensed +under version 3 of the GNU General Public License into a single +combined work, and to convey the resulting work. The terms of this +License will continue to apply to the part which is the covered work, +but the work with which it is combined will remain governed by version +3 of the GNU General Public License. + + 14. Revised Versions of this License. + + The Free Software Foundation may publish revised and/or new versions of +the GNU Affero General Public License from time to time. Such new versions +will be similar in spirit to the present version, but may differ in detail to +address new problems or concerns. + + Each version is given a distinguishing version number. If the +Program specifies that a certain numbered version of the GNU Affero General +Public License "or any later version" applies to it, you have the +option of following the terms and conditions either of that numbered +version or of any later version published by the Free Software +Foundation. If the Program does not specify a version number of the +GNU Affero General Public License, you may choose any version ever published +by the Free Software Foundation. + + If the Program specifies that a proxy can decide which future +versions of the GNU Affero General Public License can be used, that proxy's +public statement of acceptance of a version permanently authorizes you +to choose that version for the Program. + + Later license versions may give you additional or different +permissions. However, no additional obligations are imposed on any +author or copyright holder as a result of your choosing to follow a +later version. + + 15. Disclaimer of Warranty. + + THERE IS NO WARRANTY FOR THE PROGRAM, TO THE EXTENT PERMITTED BY +APPLICABLE LAW. EXCEPT WHEN OTHERWISE STATED IN WRITING THE COPYRIGHT +HOLDERS AND/OR OTHER PARTIES PROVIDE THE PROGRAM "AS IS" WITHOUT WARRANTY +OF ANY KIND, EITHER EXPRESSED OR IMPLIED, INCLUDING, BUT NOT LIMITED TO, +THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR +PURPOSE. THE ENTIRE RISK AS TO THE QUALITY AND PERFORMANCE OF THE PROGRAM +IS WITH YOU. SHOULD THE PROGRAM PROVE DEFECTIVE, YOU ASSUME THE COST OF +ALL NECESSARY SERVICING, REPAIR OR CORRECTION. + + 16. Limitation of Liability. + + IN NO EVENT UNLESS REQUIRED BY APPLICABLE LAW OR AGREED TO IN WRITING +WILL ANY COPYRIGHT HOLDER, OR ANY OTHER PARTY WHO MODIFIES AND/OR CONVEYS +THE PROGRAM AS PERMITTED ABOVE, BE LIABLE TO YOU FOR DAMAGES, INCLUDING ANY +GENERAL, SPECIAL, INCIDENTAL OR CONSEQUENTIAL DAMAGES ARISING OUT OF THE +USE OR INABILITY TO USE THE PROGRAM (INCLUDING BUT NOT LIMITED TO LOSS OF +DATA OR DATA BEING RENDERED INACCURATE OR LOSSES SUSTAINED BY YOU OR THIRD +PARTIES OR A FAILURE OF THE PROGRAM TO OPERATE WITH ANY OTHER PROGRAMS), +EVEN IF SUCH HOLDER OR OTHER PARTY HAS BEEN ADVISED OF THE POSSIBILITY OF +SUCH DAMAGES. + + 17. Interpretation of Sections 15 and 16. + + If the disclaimer of warranty and limitation of liability provided +above cannot be given local legal effect according to their terms, +reviewing courts shall apply local law that most closely approximates +an absolute waiver of all civil liability in connection with the +Program, unless a warranty or assumption of liability accompanies a +copy of the Program in return for a fee. + + END OF TERMS AND CONDITIONS + + How to Apply These Terms to Your New Programs + + If you develop a new program, and you want it to be of the greatest +possible use to the public, the best way to achieve this is to make it +free software which everyone can redistribute and change under these terms. + + To do so, attach the following notices to the program. It is safest +to attach them to the start of each source file to most effectively +state the exclusion of warranty; and each file should have at least +the "copyright" line and a pointer to where the full notice is found. + + + Copyright (C) + + This program is free software: you can redistribute it and/or modify + it under the terms of the GNU Affero General Public License as published + by the Free Software Foundation, either version 3 of the License, or + (at your option) any later version. + + This program is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + GNU Affero General Public License for more details. + + You should have received a copy of the GNU Affero General Public License + along with this program. If not, see . + +Also add information on how to contact you by electronic and paper mail. + + If your software can interact with users remotely through a computer +network, you should also make sure that it provides a way for users to +get its source. For example, if your program is a web application, its +interface could display a "Source" link that leads users to an archive +of the code. There are many ways you could offer source, and different +solutions will be better for different programs; see section 13 for the +specific requirements. + + You should also get your employer (if you work as a programmer) or school, +if any, to sign a "copyright disclaimer" for the program, if necessary. +For more information on this, and how to apply and follow the GNU AGPL, see +. diff --git a/PKGBUILD b/PKGBUILD index 21dd149..4c74f95 100644 --- a/PKGBUILD +++ b/PKGBUILD @@ -1,29 +1,30 @@ pkgname=rustmius-bin _pkgname=rustmius -pkgver=2.0.0 +pkgver=2.1.0 pkgrel=1 -pkgdesc="Une alternative locale complète à Termius pour Linux (GTK4)" -arch=('x86_64' 'aarch64') +pkgdesc="Full local Termius alternative for Linux (GTK4)" +arch=('x86_64') url="https://github.com/Cleboost/Rustmius" license=('MIT') -depends=('libadwaita' 'gtk4' 'vte4') +depends=('gtk4' 'vte4') provides=("$_pkgname") conflicts=("$_pkgname") -source_x86_64=("$url/releases/download/v$pkgver/$_pkgname-x86_64") -source_aarch64=("$url/releases/download/v$pkgver/$_pkgname-aarch64") +source=( + "$_pkgname.desktop::https://raw.githubusercontent.com/Cleboost/Rustmius/refs/heads/master/rustmius.desktop" + "$_pkgname.png::https://raw.githubusercontent.com/Cleboost/Rustmius/refs/heads/master/rustmius.png" + "LICENSE::https://raw.githubusercontent.com/Cleboost/Rustmius/refs/heads/master/LICENSE" + "$_pkgname-$pkgver-x86_64::$url/releases/download/v$pkgver/$_pkgname-x86_64" +) -sha256sums_x86_64=('SKIP') -sha256sums_aarch64=('SKIP') +sha256sums=('1498673b59f13bdd2a5beb43d72fc8e9330599324f9a4d70bd52e1d303ad9959' + 'c8c9adc6e26cc54f2b8d8ce41a093b71b5e9e4338d6e278803d87d2a6e94422d' + '8486a10c4393cee1c25392769ddd3b2d6c242d6ec7928e1414efff7dfb2f07ef' + 'e7e4715eb8bcb62bca4f43200d25a39d3630b4a9973c3b4b69dce534ecb17fb6') package() { - if [ "$CARCH" == "x86_64" ]; then - local _bin="$_pkgname-x86_64" - else - local _bin="$_pkgname-aarch64" - fi - - install -Dm755 "$_bin" "$pkgdir/usr/bin/$_pkgname" + install -Dm755 "$_pkgname-$pkgver-x86_64" "$pkgdir/usr/bin/$_pkgname" install -Dm644 "$_pkgname.desktop" "$pkgdir/usr/share/applications/$_pkgname.desktop" install -Dm644 "$_pkgname.png" "$pkgdir/usr/share/icons/hicolor/512x512/apps/$_pkgname.png" + install -Dm644 "LICENSE" "$pkgdir/usr/share/licenses/$_pkgname/LICENSE" } diff --git a/README.md b/README.md index f991e2a..557aa80 100644 --- a/README.md +++ b/README.md @@ -1,101 +1,75 @@ -# Rustmius +# 🦀 Rustmius -A high-performance, privacy-first SSH client for Linux. Built with gtk-rs (GTK4). +[![Release](https://img.shields.io/github/v/release/Cleboost/Rustmius?style=flat-square&color=orange)](https://github.com/Cleboost/Rustmius/releases) +[![License](https://img.shields.io/github/license/Cleboost/Rustmius?style=flat-square)](LICENSE) +[![Platform](https://img.shields.io/badge/platform-linux-lightgrey?style=flat-square)](https://github.com/Cleboost/Rustmius) -## Features +**Rustmius** is a modern, fast, and local alternative to Termius, designed specifically for the Linux ecosystem. Built with Rust using GTK4, it provides a premium user experience while ensuring maximum security by keeping all your configurations stored locally. -- **Keyboard-driven HUD** (`Ctrl+K`) — fuzzy-search and connect to hosts instantly -- **SSH config sync** — automatically watches and imports `~/.ssh/config` -- **Integrated terminals** — embedded VTE terminal widgets inside the GTK4 GUI -- **Secret management** — integrates with GNOME/KDE keyrings via `libsecret` -- **Server management** — add, edit, and delete hosts directly from the UI -- **No cloud, no telemetry** — everything stays local +--- -## Tech Stack +## ✨ Features -| Component | Technology | -|-----------|------------| -| Language | Rust 2024 Edition | -| UI | GTK4 (no libadwaita) | -| Terminal | vte4-rs | -| SSH | libssh2-rs | -| Async | tokio | -| Fuzzy search | nucleo-matcher | -| Config watching | notify | -| Secret storage | oo7 (libsecret) | +- **Integrated SSH Terminal**: Powered by VTE for robust and high-performance terminal emulation. +- **Advanced SFTP Explorer**: + - Bidirectional Drag & Drop between your local system and remote servers. + - Full context menu support (Rename, Delete, Download, Create Folders/Files). + - File type icons and size formatting. +- **Host Manager**: Centralized SSH connections management with an intuitive interface. +- **Security First**: Utilizes the system keyring (via `oo7`/libsecret) to store your passwords and secrets securely. +- **Extreme Optimization**: Binaries are compiled with LTO (Link Time Optimization) and specific CPU targets for maximum responsiveness. +- **Modern UI**: Seamless integration with modern Linux environments through GTK4. -## Prerequisites - -- Rust (Edition 2024 / recent toolchain) -- GTK4 development libraries -- VTE GTK4 development libraries -- libssh2 development libraries -- libsecret development libraries - -On Debian/Ubuntu: - -```bash -sudo apt install libgtk-4-dev libvte-2.91-gtk4-dev libssh2-1-dev libsecret-1-dev -``` - -On Arch Linux: - -```bash -sudo pacman -S gtk4 vte3 libssh2 libsecret -``` - -On Fedora: +## 🚀 Installation +### Arch Linux (AUR) +The easiest way on Arch Linux is to use the `-bin` package (pre-compiled and optimized): ```bash -sudo dnf install gtk4-devel vte291-devel libssh2-devel libsecret-devel +# Using your favorite AUR helper (e.g., yay) +yay -S rustmius-bin ``` -## Build & Run +### Other Distributions (Binaries) +Download the binary matching your hardware from the [Releases](https://github.com/Cleboost/Rustmius/releases) page: +- `rustmius-x86_64`: For standard 64-bit Linux PCs. +- `rustmius-x86_64-v3`: **Super-Optimized** version for modern CPUs (Haswell+). +### Building from Source +Ensure you have the system dependencies installed (`libgtk-4-dev`, `libvte-2.91-gtk4-dev`): ```bash +git clone https://github.com/Cleboost/Rustmius.git +cd Rustmius cargo build --release -cargo run --release ``` -## Usage - -1. **Open the HUD** — press `Ctrl+K` to search your hosts -2. **Connect** — select a host from the fuzzy search results -3. **Add a server** — use the server list UI to add new hosts to `~/.ssh/config` -4. **Manage servers** — edit or delete existing entries from the server list +## 🗺️ Roadmap -## Architecture +- [ ] **Server Performance Monitoring** (Custom Rust UI, htop-like experience) +- [ ] **Docker Manager** (View images, Pull, Start/Stop/Create containers) +- [x] **SSH Keys Management** (Creation, Deletion, Auto-config on servers, Key-based auth UX) +- [ ] **SyncCloud** (Optional): Cross-device synchronization or backup using fully encrypted GitHub Gists. +- [ ] Global Settings & Themes -Rustmius is a complete GTK4 desktop application (built with gtk-rs). It treats `~/.ssh/config` as the source of truth for host data. A background watcher (`notify`) detects changes and rebuilds the in-memory search index. The GUI provides a HUD overlay for quick host discovery and embedded VTE terminal widgets for active SSH sessions. - -``` -src/ -├── main.rs # Entry point, GTK4 app setup, askpass handler -├── config_observer.rs # SSH config parser and file watcher -├── ssh_engine.rs # SSH session management (libssh2) -└── ui/ - ├── mod.rs - ├── window.rs # Main application window - ├── hud.rs # Ctrl+K fuzzy search overlay - ├── server_list.rs # Host list management - └── add_server_dialog.rs # Add new server dialog -``` +## 🛠️ Development -## Roadmap +Rustmius is built upon a cutting-edge technology stack: +- **Language**: [Rust](https://www.rust-lang.org/) +- **UI**: [GTK4](https://gtk.org/) +- **SSH/SFTP**: `ssh2-rs` +- **Terminal**: `vte4` -### Current (v0.8) -- SSH config sync with file watching -- HUD fuzzy search -- VTE terminal integration -- Password & ssh-agent authentication -- Server list CRUD +## ⚖️ License & Intellectual Property +This software is distributed under the [GNU AGPLv3 license](LICENSE). -### Planned (v1.0) -- BSP tiling for terminal panels -- Saved workspaces and session recovery -- Pure Rust SSH engine (`russh`) -- Advanced keyboard navigation with leader keys +### Name and protection: +- The name "Rustmius", its logo, and its visual identity are the exclusive property of the original author. +- While the source code is open, the use of the name "Rustmius" for derivative works (forks) or third-party commercial products is not permitted without prior written consent. +- If you create a modified version of this software, you must rename it clearly to avoid any confusion with the official version. -## License +### Forking & Usage Constraints: +- Forks and derivative works are permitted and encouraged, provided they remain under the same AGPLv3 license. +- Non-Commercial Use Only: Derivative versions or forks of this project may not be used for commercial purposes or sold as proprietary software. +- All modifications must remain public and accessible to the community +--- -MIT +*Developed with ❤️ by Cleboost.* diff --git a/docs/plans/2026-04-05-remote-file-explorer.md b/docs/plans/2026-04-05-remote-file-explorer.md deleted file mode 100644 index 3904544..0000000 --- a/docs/plans/2026-04-05-remote-file-explorer.md +++ /dev/null @@ -1,82 +0,0 @@ -# Remote File Explorer (SFTP) Implementation Plan - -> **For Gemini:** REQUIRED SUB-SKILL: Use superpowers:executing-plans to implement this plan task-by-task. - -**Goal:** Provide a native file explorer for remote servers via SFTP, accessible from a toolbar above each SSH terminal. - -**Architecture:** -- **UI (Toolbar)**: Wrap the terminal in a `GtkBox` with a top toolbar containing a "Folder" button. -- **UI (Explorer)**: Create a new component `FileExplorer` using a `GtkListBox` to display remote files. -- **Backend (SFTP)**: Use the `ssh2` crate to open an SFTP session when the explorer tab is requested. -- **Data Flow**: Use async Rust to fetch file lists and update the UI without freezing. - -**Tech Stack:** Rust, GTK4, `ssh2` (SFTP), `tokio` (async). - ---- - -### Task 1: Terminal Toolbar & Tab Navigation - -**Files:** -- Modify: `src/ui/window.rs` - -**Step 1: Wrap terminal in a Box with a Toolbar and Folder button** - -```rust -// src/ui/window.rs -// Inside the action_logic for ServerAction::Connect: -let terminal_container = gtk4::Box::new(gtk4::Orientation::Vertical, 0); -let toolbar = gtk4::Box::new(gtk4::Orientation::Horizontal, 6); -toolbar.set_margin_top(4); -toolbar.set_margin_bottom(4); -toolbar.set_margin_start(6); - -let explorer_btn = gtk4::Button::from_icon_name("folder-remote-symbolic"); -explorer_btn.add_css_class("flat"); -toolbar.append(&explorer_btn); - -terminal_container.append(&toolbar); -terminal_container.append(&terminal); -``` - -**Step 2: Implement the "Open Explorer Tab" logic when clicking the Folder button** - -**Step 3: Commit** - -```bash -git add src/ui/window.rs -git commit -m "ui: add session toolbar with explorer button and tab navigation" -``` - ---- - -### Task 2: SFTP Backend Engine - -**Files:** -- Create: `src/sftp_engine.rs` -- Modify: `src/main.rs` - -**Step 1: Implement basic SFTP connection and listing logic** - -**Step 2: Commit** - -```bash -git add src/sftp_engine.rs src/main.rs -git commit -m "feat: implement SFTP backend for remote file listing" -``` - ---- - -### Task 3: File Explorer UI Component - -**Files:** -- Create: `src/ui/file_explorer.rs` -- Modify: `src/ui/mod.rs` - -**Step 1: Build the basic File Explorer view with a list** - -**Step 2: Commit** - -```bash -git add src/ui/file_explorer.rs src/ui/mod.rs -git commit -m "ui: implement remote file explorer view" -``` diff --git a/docs/plans/2026-04-05-robust-alias-management.md b/docs/plans/2026-04-05-robust-alias-management.md deleted file mode 100644 index 596c11d..0000000 --- a/docs/plans/2026-04-05-robust-alias-management.md +++ /dev/null @@ -1,170 +0,0 @@ -# Robust Alias Handling Implementation Plan - -> **For Gemini:** REQUIRED SUB-SKILL: Use superpowers:executing-plans to implement this plan task-by-task. - -**Goal:** Support spaces in server names and prevent duplicate aliases. - -**Architecture:** -- **Parser**: Update `parse_ssh_config` to handle quoted aliases and multi-word aliases correctly. -- **Persistence**: Update `add_host_to_config` to wrap aliases in double quotes if they contain spaces. -- **Validation**: Add a check in `show_server_dialog` to verify if the alias already exists in the current list. - ---- - -### Task 1: Robust Parser & Quoted Persistence - -**Files:** -- Modify: `src/config_observer.rs` - -**Step 1: Update parser to handle spaces and quotes** - -```rust -// src/config_observer.rs -pub fn parse_ssh_config(content: &str) -> Vec { - let mut hosts = Vec::new(); - let mut current_host: Option = None; - - for line in content.lines() { - let line = line.trim(); - if line.is_empty() || line.starts_with('#') { - continue; - } - - // Handle quoted values or multi-word values for Host - let parts: Vec<&str> = line.splitn(2, |c: char| c.is_whitespace()).collect(); - if parts.len() < 2 { - continue; - } - - let key = parts[0].to_lowercase(); - let mut value = parts[1].trim(); - - // Remove quotes if present - if value.starts_with('"') && value.ends_with('"') && value.len() > 1 { - value = &value[1..value.len()-1]; - } - - match key.as_str() { - "host" => { - if let Some(host) = current_host.take() { - if !host.alias.is_empty() && !host.hostname.is_empty() { - hosts.push(host); - } - } - current_host = Some(SshHost { - alias: value.to_string(), - hostname: String::new(), - user: None, - }); - } - "hostname" => { - if let Some(ref mut host) = current_host { - host.hostname = value.to_string(); - } - } - "user" => { - if let Some(ref mut host) = current_host { - host.user = Some(value.to_string()); - } - } - _ => {} - } - } - - if let Some(host) = current_host { - if !host.alias.is_empty() && !host.hostname.is_empty() { - hosts.push(host); - } - } - - hosts -} - -pub fn add_host_to_config(host: &SshHost) -> anyhow::Result<()> { - let path = get_default_config_path().ok_or_else(|| anyhow::anyhow!("Could not find SSH config path"))?; - - if let Some(parent) = path.parent() { - std::fs::create_dir_all(parent)?; - } - - let mut content = if path.exists() { - std::fs::read_to_string(&path)? - } else { - String::new() - }; - - if !content.is_empty() && !content.ends_with('\n') { - content.push('\n'); - } - - // Wrap alias in quotes if it has spaces - let alias_quoted = if host.alias.contains(' ') { - format!("\"{}\"", host.alias) - } else { - host.alias.clone() - }; - - let entry = format!( - "\nHost {}\n HostName {}\n User {}\n", - alias_quoted, - host.hostname, - host.user.as_deref().unwrap_or("root") - ); - - content.push_str(&entry); - std::fs::write(path, content)?; - Ok(()) -} -``` - -**Step 2: Commit** - -```bash -git add src/config_observer.rs -git commit -m "feat: support spaces in aliases using quotes in ssh config" -``` - ---- - -### Task 2: Duplicate Name Validation in UI - -**Files:** -- Modify: `src/ui/add_server_dialog.rs` - -**Step 1: Update show_server_dialog to accept existing aliases and show an error if duplicate** - -```rust -// src/ui/add_server_dialog.rs -pub fn show_server_dialog( - parent: >k4::Window, - initial_host: Option<&SshHost>, - existing_aliases: Vec, - on_save: F -) where F: Fn(SshHost, String) + 'static { - // ... logic to check alias against existing_aliases ... - // If alias exists and not editing same host, show error label -} -``` - -**Step 2: Commit** - -```bash -git add src/ui/add_server_dialog.rs -git commit -m "ui: prevent duplicate server aliases in the creation dialog" -``` - ---- - -### Task 3: Wiring in window.rs - -**Files:** -- Modify: `src/ui/window.rs` - -**Step 1: Update calls to show_server_dialog to pass existing aliases** - -**Step 2: Commit** - -```bash -git add src/ui/window.rs -git commit -m "feat: complete robust alias management" -``` diff --git a/docs/plans/2026-04-05-rustmius-design.md b/docs/plans/2026-04-05-rustmius-design.md deleted file mode 100644 index e55c954..0000000 --- a/docs/plans/2026-04-05-rustmius-design.md +++ /dev/null @@ -1,53 +0,0 @@ -# Rustmius: High-Performance, Local-First SSH Client - -## 1. Vision & Goals -Rustmius is a 100% local, privacy-first, and high-performance alternative to Termius. -- **Privacy**: No cloud, no external sync. Everything is stored locally. -- **Performance**: Native Rust + GTK4 + VTE for a lightweight, snappy experience. -- **UX**: Keyboard-first navigation via a HUD (Heads-Up Display) and intelligent tiling. - ---- - -## 2. Phased Roadmap - -### 🟢 MVP 0.8: "Ship Ultra Vite" -**Goal**: Replace the standard CLI SSH workflow with a superior UI experience. -- **Config Sync**: Watch and import `~/.ssh/config` using the `notify` crate. -- **The HUD (`Ctrl+K`)**: Instant fuzzy-search for host aliases and IP addresses. -- **Terminal Engine**: Native `vte4-rs` implementation. -- **SSH Connectivity**: Use `libssh2-rs` for stability. Support `ssh-agent` and password prompts. -- **Simplified Layout**: Tabs or simple horizontal splits (no full BSP yet). -- **Secret Management**: Native `libsecret` integration with a clean fallback. - -### 🔥 MVP 1.0: "The Rustmius Moment" -**Goal**: Deliver the power-user "claque" with advanced automation. -- **BSP Tiling**: Full Binary Space Partitioning engine for automatic layout management. -- **Intelligent Layouts**: Layout policies (Master-Stack, Grid) that adapt to user preferences. -- **Pure Keyboard Navigation**: Leader-key driven workflow for tile management and navigation. -- **Advanced State**: Saved Workspaces and session recovery. -- **Engine Evolution**: Explore transition to `russh` (pure Rust). - ---- - -## 3. Technical Stack -- **Language**: Rust (Edition 2024). -- **UI Framework**: GTK4 (No `libadwaita` for a neutral, custom-styled look). -- **Terminal**: `vte4-rs`. -- **SSH Engine**: `libssh2-rs` (MVP) -> `russh` (Long-term). -- **Data Persistence**: SQLite for metadata (tags, preferences) + `notify` for config watching. -- **Secret Storage**: `libsecret` (GNOME/KDE Keyrings). - ---- - -## 4. Architecture Detail: The "Hybrid Observer" -- **Discovery**: Rustmius treats `~/.ssh/config` as the source of truth for host data. -- **Augmentation**: A local SQLite database layers "Rustmius metadata" (tags, custom colors, layout states) on top of the system config. -- **Reactive State**: The Rust core manages sessions and layouts asynchronously, ensuring the GTK main loop never freezes. - ---- - -## 5. UI/UX Design Principles -- **Sober Aesthetic**: Clean borders, minimal padding, and professional color schemes via GTK CSS. -- **HUD-First**: Primary interaction happens through `Ctrl+K` to find and connect. -- **Automaticity**: Tiles are placed automatically based on the active layout policy (e.g., "Split the widest tile"). -- **Zero Distraction**: No scrollbars, tabs, or buttons unless absolutely necessary for the current context. diff --git a/docs/plans/2026-04-05-rustmius-mvp-0-8.md b/docs/plans/2026-04-05-rustmius-mvp-0-8.md deleted file mode 100644 index 5913912..0000000 --- a/docs/plans/2026-04-05-rustmius-mvp-0-8.md +++ /dev/null @@ -1,201 +0,0 @@ -# Rustmius MVP 0.8 Implementation Plan - -> **For Gemini:** REQUIRED SUB-SKILL: Use superpowers:executing-plans to implement this plan task-by-task. - -**Goal:** A native Linux SSH client with a keyboard-driven HUD for connecting to hosts from `~/.ssh/config`. - -**Architecture:** Async Rust core managing SSH sessions via `libssh2`, integrated with a GTK4 UI using `vte4` for terminal emulation. A background watcher syncs system SSH configs into an in-memory search index. - -**Tech Stack:** Rust (2024), GTK4, `vte4-rs`, `ssh2` (libssh2 bindings), `tokio`, `notify`, `nucleo-matcher` (fuzzy search). - ---- - -### Task 1: Project Scaffolding & Dependencies - -**Files:** -- Modify: `Cargo.toml` - -**Step 1: Update Cargo.toml with core dependencies** - -```toml -[package] -name = "rustmius" -version = "0.8.0" -edition = "2024" - -[dependencies] -gtk4 = "0.9" -vte4 = "0.9" -ssh2 = "0.9" -tokio = { version = "1", features = ["full"] } -notify = "6.1" -nucleo-matcher = "0.2" -serde = { version = "1.0", features = ["derive"] } -serde_json = "1.0" -anyhow = "1.0" -directories = "5.0" -futures = "0.3" -``` - -**Step 2: Run cargo check to verify dependencies** - -Run: `cargo check` -Expected: Success (downloads and compiles metadata). - -**Step 3: Commit** - -```bash -git add Cargo.toml -git commit -m "chore: scaffold project with MVP 0.8 dependencies" -``` - ---- - -### Task 2: SSH Config Observer - -**Files:** -- Create: `src/config_observer.rs` -- Modify: `src/main.rs` - -**Step 1: Write the failing test for config parsing** - -```rust -#[cfg(test)] -mod tests { - use super::*; - #[test] - fn test_parse_ssh_config_simple() { - let config = "Host my-server\n HostName 1.2.3.4\n User root"; - let hosts = parse_ssh_config(config); - assert_eq!(hosts[0].alias, "my-server"); - assert_eq!(hosts[0].hostname, "1.2.3.4"); - } -} -``` - -**Step 2: Implement the parsing logic and watcher** - -```rust -// src/config_observer.rs -pub struct SshHost { - pub alias: String, - pub hostname: String, - pub user: Option, -} - -pub fn parse_ssh_config(content: &str) -> Vec { - // Basic parser for ~/.ssh/config - let mut hosts = Vec::new(); - // ... implementation logic ... - hosts -} -``` - -**Step 3: Run tests** - -Run: `cargo test config_observer` -Expected: PASS - -**Step 4: Commit** - -```bash -git add src/config_observer.rs -git commit -m "feat: add ssh config observer and parser" -``` - ---- - -### Task 3: The HUD (Fuzzy Search UI) - -**Files:** -- Create: `src/ui/hud.rs` - -**Step 1: Create the GtkPopover for the HUD** - -```rust -// src/ui/hud.rs -use gtk4::prelude::*; - -pub fn create_hud_popover() -> gtk4::Popover { - let popover = gtk4::Popover::new(); - let entry = gtk4::Entry::new(); - entry.set_placeholder_text(Some("Search hosts...")); - popover.set_child(Some(&entry)); - popover -} -``` - -**Step 2: Integrate nucleo-matcher for fuzzy results** - -**Step 3: Commit** - -```bash -git add src/ui/hud.rs -git commit -m "feat: implement HUD search overlay" -``` - ---- - -### Task 4: Basic Terminal UI (GTK4 + VTE) - -**Files:** -- Modify: `src/main.rs` -- Create: `src/ui/window.rs` - -**Step 1: Setup the main GTK window with VTE** - -```rust -// src/ui/window.rs -use gtk4::prelude::*; -use vte4::prelude::*; - -pub fn build_ui(app: >k4::Application) { - let window = gtk4::ApplicationWindow::builder() - .application(app) - .title("Rustmius") - .default_width(800) - .default_height(600) - .build(); - - let terminal = vte4::Terminal::new(); - window.set_child(Some(&terminal)); - window.present(); -} -``` - -**Step 2: Commit** - -```bash -git add src/ui/window.rs src/main.rs -git commit -m "feat: basic GTK4 window with VTE terminal" -``` - ---- - -### Task 5: SSH Connectivity (libssh2) - -**Files:** -- Create: `src/ssh_engine.rs` - -**Step 1: Implement the SSH session handshake** - -```rust -// src/ssh_engine.rs -use ssh2::Session; -use std::net::TcpStream; - -pub fn connect(host: &str, user: &str) -> anyhow::Result { - let tcp = TcpStream::connect(format!("{}:22", host))?; - let mut sess = Session::new()?; - sess.set_tcp_stream(tcp); - sess.handshake()?; - Ok(sess) -} -``` - -**Step 2: Commit** - -```bash -git add src/ssh_engine.rs -git commit -m "feat: implement core SSH connection engine" -``` diff --git a/docs/plans/2026-04-05-rustmius-native-ui.md b/docs/plans/2026-04-05-rustmius-native-ui.md deleted file mode 100644 index b0a6901..0000000 --- a/docs/plans/2026-04-05-rustmius-native-ui.md +++ /dev/null @@ -1,319 +0,0 @@ -# Rustmius "Real Native" UI Implementation Plan - -> **For Gemini:** REQUIRED SUB-SKILL: Use superpowers:executing-plans to implement this plan task-by-task. - -**Goal:** Transform Rustmius into a professional-grade GTK4 application with a sidebar, header bar, and a grid of server cards. - -**Architecture:** -- **Root Layout**: Horizontal `GtkBox`. -- **Sidebar**: Vertical `GtkBox` with flat icon buttons. -- **Content**: `GtkStack` for switching between the Server Grid and the SSH Terminal. -- **Server Grid**: `GtkFlowBox` containing `GtkFrame` widgets (cards) for each host. - -**Tech Stack:** Rust, GTK4, `vte4-rs`, `ssh2`. - ---- - -### Task 1: Main Window & Sidebar Scaffolding - -**Files:** -- Modify: `src/ui/window.rs` - -**Step 1: Implement the Sidebar + Stack structure** - -```rust -// src/ui/window.rs -use gtk4::prelude::*; -use crate::ui::server_list::ServerList; -use vte4::prelude::*; - -pub fn build_ui(app: >k4::Application) { - let window = gtk4::ApplicationWindow::builder() - .application(app) - .title("Rustmius") - .default_width(1100) - .default_height(800) - .build(); - - let root = gtk4::Box::new(gtk4::Orientation::Horizontal, 0); - - // 1. Sidebar - let sidebar = gtk4::Box::new(gtk4::Orientation::Vertical, 6); - sidebar.set_width_request(60); - sidebar.set_margin_top(12); - - let btn_servers = gtk4::Button::from_icon_name("network-server-symbolic"); - btn_servers.add_css_class("flat"); - - let btn_keys = gtk4::Button::from_icon_name("key-symbolic"); - btn_keys.add_css_class("flat"); - - let spacer = gtk4::Box::new(gtk4::Orientation::Vertical, 0); - spacer.set_vexpand(true); - - let btn_settings = gtk4::Button::from_icon_name("emblem-system-symbolic"); - btn_settings.add_css_class("flat"); - - sidebar.append(&btn_servers); - sidebar.append(&btn_keys); - sidebar.append(&spacer); - sidebar.append(&btn_settings); - - let separator = gtk4::Separator::new(gtk4::Orientation::Vertical); - - // 2. Content Area - let content_box = gtk4::Box::new(gtk4::Orientation::Vertical, 0); - content_box.set_hexpand(true); - - let header = gtk4::HeaderBar::new(); - let add_btn = gtk4::Button::from_icon_name("list-add-symbolic"); - add_btn.add_css_class("suggested-action"); - header.pack_start(&add_btn); - - let stack = gtk4::Stack::new(); - stack.set_transition_type(gtk4::StackTransitionType::Crossfade); - - content_box.append(&header); - content_box.append(&stack); - - root.append(&sidebar); - root.append(&separator); - root.append(&content_box); - - window.set_child(Some(&root)); - window.present(); -} -``` - -**Step 2: Commit** - -```bash -git add src/ui/window.rs -git commit -m "ui: implement sidebar and stack structure" -``` - ---- - -### Task 2: Native Server Card & Grid View - -**Files:** -- Modify: `src/ui/server_list.rs` - -**Step 1: Update ServerList to use GtkFlowBox and GtkFrame** - -```rust -// src/ui/server_list.rs -use gtk4::prelude::*; -use crate::config_observer::{SshHost, load_hosts}; - -pub struct ServerList { - pub container: gtk4::ScrolledWindow, - pub flow_box: gtk4::FlowBox, -} - -impl ServerList { - pub fn new(on_connect: F) -> Self - where F: Fn(&SshHost) + 'static + Clone - { - let scrolled = gtk4::ScrolledWindow::builder() - .hscrollbar_policy(gtk4::PolicyType::Never) - .vexpand(true) - .build(); - - let flow_box = gtk4::FlowBox::builder() - .selection_mode(gtk4::SelectionMode::None) - .valign(gtk4::Align::Start) - .max_children_per_line(4) - .min_children_per_line(1) - .column_spacing(12) - .row_spacing(12) - .margin_top(24) - .margin_bottom(24) - .margin_start(24) - .margin_end(24) - .build(); - - scrolled.set_child(Some(&flow_box)); - - let sl = Self { container: scrolled, flow_box }; - sl.refresh(on_connect); - sl - } - - pub fn refresh(&self, on_connect: F) - where F: Fn(&SshHost) + 'static + Clone - { - while let Some(child) = self.flow_box.first_child() { - self.flow_box.remove(&child); - } - - let hosts = load_hosts(); - for host in hosts { - self.add_host_row(&host, on_connect.clone()); - } - } - - fn add_host_row(&self, host: &SshHost, on_connect: F) - where F: Fn(&SshHost) + 'static - { - let frame = gtk4::Frame::new(None); - frame.add_css_class("card"); - - let row_box = gtk4::Box::new(gtk4::Orientation::Vertical, 8); - row_box.set_margin_top(12); - row_box.set_margin_bottom(12); - row_box.set_margin_start(12); - row_box.set_margin_end(12); - - let alias_label = gtk4::Label::builder() - .label(&host.alias) - .halign(gtk4::Align::Start) - .css_classes(vec!["heading".to_string()]) - .build(); - - let host_info = format!("{}@{}", host.user.as_deref().unwrap_or("root"), host.hostname); - let host_label = gtk4::Label::builder() - .label(&host_info) - .halign(gtk4::Align::Start) - .css_classes(vec!["dim-label".to_string(), "caption".to_string()]) - .build(); - - row_box.append(&alias_label); - row_box.append(&host_label); - - let gesture = gtk4::GestureClick::new(); - let host_clone = host.clone(); - gesture.connect_released(move |_, _, _, _| { - on_connect(&host_clone); - }); - frame.add_controller(gesture); - - frame.set_child(Some(&row_box)); - self.flow_box.append(&frame); - } -} -``` - -**Step 2: Commit** - -```bash -git add src/ui/server_list.rs -git commit -m "ui: implement native server cards using GtkFrame and GtkFlowBox" -``` - ---- - -### Task 3: Terminal View Integration & Final Wiring - -**Files:** -- Modify: `src/ui/window.rs` - -**Step 1: Complete the UI with the Terminal view and functional sidebar** - -```rust -// src/ui/window.rs (Final Version) -use gtk4::prelude::*; -use gtk4::{glib, gio}; -use crate::ui::server_list::ServerList; -use vte4::prelude::*; - -pub fn build_ui(app: >k4::Application) { - let window = gtk4::ApplicationWindow::builder() - .application(app) - .title("Rustmius") - .default_width(1100) - .default_height(800) - .build(); - - let root = gtk4::Box::new(gtk4::Orientation::Horizontal, 0); - - // 1. Sidebar - let sidebar = gtk4::Box::new(gtk4::Orientation::Vertical, 6); - sidebar.set_width_request(60); - sidebar.set_margin_top(12); - - let btn_servers = gtk4::Button::from_icon_name("network-server-symbolic"); - btn_servers.add_css_class("flat"); - - let btn_keys = gtk4::Button::from_icon_name("key-symbolic"); - btn_keys.add_css_class("flat"); - - let spacer = gtk4::Box::new(gtk4::Orientation::Vertical, 0); - spacer.set_vexpand(true); - - let btn_settings = gtk4::Button::from_icon_name("emblem-system-symbolic"); - btn_settings.add_css_class("flat"); - - sidebar.append(&btn_servers); - sidebar.append(&btn_keys); - sidebar.append(&spacer); - sidebar.append(&btn_settings); - - let separator = gtk4::Separator::new(gtk4::Orientation::Vertical); - - // 2. Content Area - let content_box = gtk4::Box::new(gtk4::Orientation::Vertical, 0); - content_box.set_hexpand(true); - - let header = gtk4::HeaderBar::new(); - let add_btn = gtk4::Button::from_icon_name("list-add-symbolic"); - add_btn.add_css_class("suggested-action"); - header.pack_start(&add_btn); - - let stack = gtk4::Stack::new(); - stack.set_transition_type(gtk4::StackTransitionType::Crossfade); - - // Terminal View - let terminal_box = gtk4::Box::new(gtk4::Orientation::Vertical, 0); - let terminal = vte4::Terminal::new(); - terminal.set_vexpand(true); - terminal_box.append(&terminal); - - // Navigation logic - let stack_clone = stack.clone(); - btn_servers.connect_clicked(move |_| { - stack_clone.set_visible_child_name("server_grid"); - }); - - let terminal_clone = terminal.clone(); - let stack_clone_2 = stack.clone(); - let server_list = ServerList::new(move |host| { - let host_str = host.hostname.clone(); - let user_str = host.user.clone().unwrap_or_else(|| "root".to_string()); - - stack_clone_2.set_visible_child_name("terminal"); - - terminal_clone.spawn_async( - vte4::PtyFlags::DEFAULT, - None, - &["/usr/bin/ssh", &format!("{}@{}", user_str, host_str)], - &[], - glib::SpawnFlags::SEARCH_PATH, - || {}, - -1, - None::<&gio::Cancellable>, - |_| {} - ); - }); - - stack.add_named(&server_list.container, Some("server_grid")); - stack.add_named(&terminal_box, Some("terminal")); - - content_box.append(&header); - content_box.append(&stack); - - root.append(&sidebar); - root.append(&separator); - root.append(&content_box); - - window.set_child(Some(&root)); - window.present(); -} -``` - -**Step 2: Commit** - -```bash -git add src/ui/window.rs -git commit -m "ui: final wiring of terminal and functional sidebar" -``` diff --git a/docs/plans/2026-04-05-server-management.md b/docs/plans/2026-04-05-server-management.md deleted file mode 100644 index 1589349..0000000 --- a/docs/plans/2026-04-05-server-management.md +++ /dev/null @@ -1,119 +0,0 @@ -# Server Management (Edit & Delete) Implementation Plan - -> **For Gemini:** REQUIRED SUB-SKILL: Use superpowers:executing-plans to implement this plan task-by-task. - -**Goal:** Enable users to modify or remove existing SSH connections from Rustmius and `~/.ssh/config`. - -**Architecture:** -- **Config Layer**: Add `delete_host_from_config` in `src/config_observer.rs`. -- **UI Layer**: - - Update `ServerList` cards to include Edit/Delete buttons (native GTK icons). - - Refactor `add_server_dialog` to support pre-filled fields for editing. -- **Security**: Ensure passwords are also removed from the Keyring when a server is deleted. - ---- - -### Task 1: Config & Keyring Management (The Engine) - -**Files:** -- Modify: `src/config_observer.rs` - -**Step 1: Implement deletion and update logic** - -```rust -// src/config_observer.rs -pub fn delete_host_from_config(alias: &str) -> anyhow::Result<()> { - let path = get_default_config_path().ok_or_else(|| anyhow::anyhow!("No config path"))?; - if !path.exists() { return Ok(()); } - - let content = std::fs::read_to_string(&path)?; - let mut new_lines = Vec::new(); - let mut skip = false; - let target_alias = alias.to_lowercase(); - - for line in content.lines() { - let trimmed = line.trim().to_lowercase(); - if trimmed.starts_with("host ") { - let parts: Vec<&str> = trimmed.split_whitespace().collect(); - if parts.len() > 1 && parts[1] == target_alias { - skip = true; - continue; - } else { - skip = false; - } - } - - if skip { - continue; - } - - new_lines.push(line); - } - - std::fs::write(path, new_lines.join("\n"))?; - Ok(()) -} -``` - -**Step 2: Commit** - -```bash -git add src/config_observer.rs -git commit -m "feat: implement ssh config deletion logic" -``` - ---- - -### Task 2: Refactor Dialog & ServerList UI - -**Files:** -- Modify: `src/ui/add_server_dialog.rs` -- Modify: `src/ui/server_list.rs` - -**Step 1: Update show_add_server_dialog to accept initial data** - -```rust -// src/ui/add_server_dialog.rs -pub fn show_server_dialog(parent: >k4::Window, initial_host: Option<&SshHost>, on_save: F) -where F: Fn(SshHost, String) + 'static -{ - // ... pre-fill entries if initial_host is Some ... -} -``` - -**Step 2: Add Edit/Delete buttons to ServerList cards** - -```rust -// src/ui/server_list.rs -pub enum ServerAction { - Connect(SshHost), - Edit(SshHost), - Delete(SshHost), -} -// Update add_host_row to include buttons and trigger these actions -``` - -**Step 3: Commit** - -```bash -git add src/ui/add_server_dialog.rs src/ui/server_list.rs -git commit -m "ui: add edit/delete buttons and refactor dialog for edition" -``` - ---- - -### Task 3: Final Wiring in Window - -**Files:** -- Modify: `src/ui/window.rs` - -**Step 1: Handle Edit and Delete actions in build_ui** -- For Delete: Call `delete_host_from_config` + Clear Keyring + Refresh list. -- For Edit: Call `delete_host_from_config` + `show_server_dialog` + `add_host_to_config` + Refresh. - -**Step 2: Commit** - -```bash -git add src/ui/window.rs -git commit -m "feat: functional server edition and deletion with keyring cleanup" -``` diff --git a/src/config_observer.rs b/src/config_observer.rs index b693806..dfdc37e 100644 --- a/src/config_observer.rs +++ b/src/config_observer.rs @@ -1,12 +1,26 @@ use std::fs; +use std::path::PathBuf; use directories::UserDirs; +pub fn expand_tilde(path: &str) -> PathBuf { + if path == "~" { + if let Some(home) = UserDirs::new().map(|d| d.home_dir().to_path_buf()) { + return home; + } + } else if let Some(rest) = path.strip_prefix("~/") + && let Some(home) = UserDirs::new().map(|d| d.home_dir().to_path_buf()) { + return home.join(rest); + } + PathBuf::from(path) +} + #[derive(Debug, Clone, serde::Serialize, serde::Deserialize)] pub struct SshHost { pub alias: String, pub hostname: String, pub user: Option, pub port: Option, + pub identity_file: Option, } pub fn get_default_config_path() -> Option { @@ -55,6 +69,7 @@ pub fn parse_ssh_config(content: &str) -> Vec { hostname: String::new(), user: None, port: None, + identity_file: None, }); } "hostname" => { @@ -68,10 +83,14 @@ pub fn parse_ssh_config(content: &str) -> Vec { } } "port" => { - if let Some(ref mut host) = current_host { - if let Ok(p) = value.parse::() { + if let Some(ref mut host) = current_host + && let Ok(p) = value.parse::() { host.port = Some(p); } + } + "identityfile" => { + if let Some(ref mut host) = current_host { + host.identity_file = Some(value.to_string()); } } _ => {} @@ -88,7 +107,6 @@ pub fn parse_ssh_config(content: &str) -> Vec { pub fn add_host_to_config(host: &SshHost) -> anyhow::Result<()> { let path = get_default_config_path().ok_or_else(|| anyhow::anyhow!("Could not find SSH config path"))?; - if let Some(parent) = path.parent() { std::fs::create_dir_all(parent)?; } @@ -109,7 +127,7 @@ pub fn add_host_to_config(host: &SshHost) -> anyhow::Result<()> { host.alias.clone() }; - let entry = format!( + let mut entry = format!( "\nHost {}\n HostName {}\n User {}\n Port {}\n", alias_quoted, host.hostname, @@ -117,15 +135,25 @@ pub fn add_host_to_config(host: &SshHost) -> anyhow::Result<()> { host.port.unwrap_or(22) ); + if let Some(ref id_file) = host.identity_file { + let id_file_quoted = if id_file.contains(' ') { + format!("\"{}\"", id_file) + } else { + id_file.clone() + }; + entry.push_str(&format!(" IdentityFile {}\n", id_file_quoted)); + } + content.push_str(&entry); - std::fs::write(path, content)?; + let tmp_path = path.with_extension("tmp"); + std::fs::write(&tmp_path, &content)?; + std::fs::rename(tmp_path, path)?; Ok(()) } pub fn delete_host_from_config(alias: &str) -> anyhow::Result<()> { let path = get_default_config_path().ok_or_else(|| anyhow::anyhow!("No config path"))?; if !path.exists() { return Ok(()); } - let content = std::fs::read_to_string(&path)?; let mut new_lines = Vec::new(); let mut skip = false; @@ -138,7 +166,6 @@ pub fn delete_host_from_config(alias: &str) -> anyhow::Result<()> { if val.starts_with('"') && val.ends_with('"') && val.len() >= 2 { val = &val[1..val.len()-1]; } - if val == target_alias { skip = true; continue; @@ -146,19 +173,17 @@ pub fn delete_host_from_config(alias: &str) -> anyhow::Result<()> { skip = false; } } - if skip && (line.starts_with(' ') || line.starts_with('\t') || line.trim().is_empty()) { continue; } - if skip { skip = false; } - new_lines.push(line); } - - std::fs::write(path, new_lines.join("\n"))?; + let tmp_path = path.with_extension("tmp"); + std::fs::write(&tmp_path, new_lines.join("\n"))?; + std::fs::rename(tmp_path, path)?; Ok(()) } @@ -184,4 +209,84 @@ mod tests { assert_eq!(hosts.len(), 1); assert_eq!(hosts[0].alias, "My Server"); } -} + + #[test] + fn test_parse_ssh_config_with_identity_file() { + let config = "Host my-server\n HostName 1.2.3.4\n User root\n Port 22\n IdentityFile ~/.ssh/id_ed25519"; + let hosts = parse_ssh_config(config); + assert_eq!(hosts.len(), 1); + assert_eq!(hosts[0].identity_file, Some("~/.ssh/id_ed25519".to_string())); + } + + #[test] + fn test_parse_ssh_config_with_quoted_identity_file() { + let config = "Host my-server\n HostName 1.2.3.4\n User root\n IdentityFile \"/home/user/my keys/id_ed25519\""; + let hosts = parse_ssh_config(config); + assert_eq!(hosts.len(), 1); + assert_eq!(hosts[0].identity_file, Some("/home/user/my keys/id_ed25519".to_string())); + } + + #[test] + fn test_add_host_to_config_emits_identity_file() { + let host = SshHost { + alias: "test-host".to_string(), + hostname: "192.168.1.1".to_string(), + user: Some("admin".to_string()), + port: Some(22), + identity_file: Some("~/.ssh/id_ed25519".to_string()), + }; + let alias_quoted = if host.alias.contains(' ') { + format!("\"{}\"", host.alias) + } else { + host.alias.clone() + }; + let mut entry = format!( + "\nHost {}\n HostName {}\n User {}\n Port {}\n", + alias_quoted, + host.hostname, + host.user.as_deref().unwrap_or("root"), + host.port.unwrap_or(22) + ); + if let Some(ref id_file) = host.identity_file { + let id_file_quoted = if id_file.contains(' ') { + format!("\"{}\"", id_file) + } else { + id_file.clone() + }; + entry.push_str(&format!(" IdentityFile {}\n", id_file_quoted)); + } + assert!(entry.contains("IdentityFile ~/.ssh/id_ed25519")); + let hosts = parse_ssh_config(&entry); + assert_eq!(hosts.len(), 1); + assert_eq!(hosts[0].identity_file, Some("~/.ssh/id_ed25519".to_string())); + } + + #[test] + fn test_add_host_to_config_quotes_identity_file_with_spaces() { + let host = SshHost { + alias: "spaced-host".to_string(), + hostname: "10.0.0.1".to_string(), + user: Some("user".to_string()), + port: Some(22), + identity_file: Some("/home/user/my keys/id_rsa".to_string()), + }; + let mut entry = format!( + "\nHost {}\n HostName {}\n User {}\n Port {}\n", + host.alias, host.hostname, + host.user.as_deref().unwrap_or("root"), + host.port.unwrap_or(22) + ); + if let Some(ref id_file) = host.identity_file { + let id_file_quoted = if id_file.contains(' ') { + format!("\"{}\"", id_file) + } else { + id_file.clone() + }; + entry.push_str(&format!(" IdentityFile {}\n", id_file_quoted)); + } + assert!(entry.contains("IdentityFile \"/home/user/my keys/id_rsa\"")); + let hosts = parse_ssh_config(&entry); + assert_eq!(hosts.len(), 1); + assert_eq!(hosts[0].identity_file, Some("/home/user/my keys/id_rsa".to_string())); + } +} \ No newline at end of file diff --git a/src/main.rs b/src/main.rs index 97823be..3073b3a 100644 --- a/src/main.rs +++ b/src/main.rs @@ -19,7 +19,6 @@ fn log_debug(msg: &str) { #[tokio::main] async fn main() { let _args: Vec = std::env::args().collect(); - if let Ok(alias) = std::env::var("RUSTMIUS_ASKPASS_ALIAS") { log_debug(&format!("AskPass triggered for alias: {}", alias)); if let Ok(keyring) = oo7::Keyring::new().await { @@ -30,9 +29,8 @@ async fn main() { log_debug(&format!("Found {} items in keyring", items.len())); if let Some(item) = items.first() && let Ok(password) = item.secret().await - && let Ok(pass_str) = std::str::from_utf8(&password) { + && let Ok(pass_str) = std::str::from_utf8(password.as_ref()) { log_debug("Password retrieved successfully, sending to SSH"); - print!("{}", pass_str); std::process::exit(0); } @@ -49,4 +47,4 @@ async fn main() { app.connect_activate(build_ui); app.run_with_args::<&str>(&[]); -} +} \ No newline at end of file diff --git a/src/sftp_engine.rs b/src/sftp_engine.rs index 14cb8b8..71b4c3f 100644 --- a/src/sftp_engine.rs +++ b/src/sftp_engine.rs @@ -1,6 +1,7 @@ use ssh2::Session; -use std::net::TcpStream; -use crate::config_observer::SshHost; +use std::net::{TcpStream, ToSocketAddrs}; +use std::time::Duration; +use crate::config_observer::{SshHost, expand_tilde}; use std::path::Path; use std::io::{Read, Write}; use std::sync::{Arc, Mutex, OnceLock}; @@ -14,7 +15,7 @@ pub struct RemoteFile { } pub struct ActiveSession { - _sess: Session, // Keep the session alive for the Sftp pointer + _sess: Session, pub sftp: ssh2::Sftp, } @@ -25,40 +26,59 @@ fn get_session_pool() -> &'static Mutex>> { fn get_or_connect_sftp(host: &SshHost, password: &Option) -> anyhow::Result> { let host_key = format!("{}@{}", host.user.as_deref().unwrap_or("root"), host.hostname); - - if let Ok(mut pool) = get_session_pool().lock() { - if let Some(active) = pool.get(&host_key) { - // Check if connection is still alive using a basic stat + if let Ok(mut pool) = get_session_pool().lock() + && let Some(active) = pool.get(&host_key) { if active.sftp.stat(Path::new(".")).is_ok() { return Ok(active.clone()); } else { - // Connection died, remove it to force reconnect pool.remove(&host_key); } } - } let port = host.port.unwrap_or(22); - let tcp = TcpStream::connect(format!("{}:{}", host.hostname, port))?; + let addrs = format!("{}:{}", host.hostname, port).to_socket_addrs()?; + let mut tcp_opt = None; + for addr in addrs { + if let Ok(stream) = TcpStream::connect_timeout(&addr, Duration::from_secs(5)) { + tcp_opt = Some(stream); + break; + } + } + let tcp = tcp_opt.ok_or_else(|| anyhow::anyhow!("Connection timeout to {}", host.hostname))?; let mut sess = Session::new()?; sess.set_tcp_stream(tcp); sess.handshake()?; let user = host.user.as_deref().unwrap_or("root"); - if let Some(pass) = password { - sess.userauth_password(user, pass)?; - } else { - sess.userauth_agent(user)?; + let mut authenticated = false; + if let Some(ref key_path) = host.identity_file { + let path = expand_tilde(key_path); + if sess.userauth_pubkey_file(user, None, &path, None).is_ok() { + println!("[DEBUG] SFTP connected to {} via Configure SSH Key ({})", host.hostname, key_path); + authenticated = true; + } + } + if !authenticated + && sess.userauth_agent(user).is_ok() { + println!("[DEBUG] SFTP connected to {} via SSH Agent", host.hostname); + authenticated = true; + } + + if !authenticated + && let Some(pass) = password + && sess.userauth_password(user, pass).is_ok() { + println!("[DEBUG] SFTP connected to {} via Password", host.hostname); + authenticated = true; + } + if !authenticated { + return Err(anyhow::anyhow!("Authentication failed (tried key, password, and agent)")); } let sftp = sess.sftp()?; - let active = Arc::new(ActiveSession { _sess: sess, sftp }); - if let Ok(mut pool) = get_session_pool().lock() { pool.insert(host_key, active.clone()); } - Ok(active) } @@ -66,7 +86,6 @@ pub async fn list_files(host: SshHost, password: Option, path: String) - tokio::task::spawn_blocking(move || { let active = get_or_connect_sftp(&host, &password)?; let dir = active.sftp.readdir(Path::new(&path))?; - let mut files = Vec::new(); for (path, stat) in dir { if let Some(name) = path.file_name().and_then(|n| n.to_str()) { @@ -77,7 +96,6 @@ pub async fn list_files(host: SshHost, password: Option, path: String) - }); } } - files.sort_by(|a, b| { if a.is_dir != b.is_dir { b.is_dir.cmp(&a.is_dir) @@ -132,7 +150,6 @@ pub async fn upload_file(host: SshHost, password: Option, local_path: St let active = get_or_connect_sftp(&host, &password)?; let mut local_file = std::fs::File::open(local_path)?; let mut remote_file = active.sftp.create(Path::new(&remote_path))?; - let mut buffer = [0; 16384]; while let Ok(n) = local_file.read(&mut buffer) { if n == 0 { break; } @@ -160,4 +177,4 @@ pub fn download_file_sync(host: SshHost, password: Option, remote_path: local_file.write_all(&buffer[..n])?; } Ok(()) -} +} \ No newline at end of file diff --git a/src/ssh_engine.rs b/src/ssh_engine.rs index 0e6cc3f..5fee3eb 100644 --- a/src/ssh_engine.rs +++ b/src/ssh_engine.rs @@ -1,18 +1,82 @@ use ssh2::Session; -use std::net::TcpStream; +use std::net::{TcpStream, ToSocketAddrs}; +use std::time::Duration; +use std::io::Write; use anyhow::Context; +use crate::config_observer::{SshHost, expand_tilde}; #[allow(dead_code)] pub fn connect(host: &str, _user: &str) -> anyhow::Result { - let tcp = TcpStream::connect(format!("{}:22", host)) - .with_context(|| format!("Failed to connect to {}:22", host))?; - + let addrs = format!("{}:22", host).to_socket_addrs() + .with_context(|| format!("Failed to resolve {}:22", host))?; + let mut tcp_opt = None; + for addr in addrs { + if let Ok(stream) = TcpStream::connect_timeout(&addr, Duration::from_secs(5)) { + tcp_opt = Some(stream); + break; + } + } + let tcp = tcp_opt.ok_or_else(|| anyhow::anyhow!("Connection timeout to {}:22", host))?; let mut sess = Session::new() .context("Failed to create SSH session")?; - sess.set_tcp_stream(tcp); sess.handshake() .context("SSH handshake failed")?; Ok(sess) } + +pub fn deploy_pubkey(host: &SshHost, password: Option, pubkey_content: &str) -> anyhow::Result<()> { + let port = host.port.unwrap_or(22); + let addrs = format!("{}:{}", host.hostname, port).to_socket_addrs()?; + let mut tcp_opt = None; + for addr in addrs { + if let Ok(stream) = TcpStream::connect_timeout(&addr, Duration::from_secs(5)) { + tcp_opt = Some(stream); + break; + } + } + let tcp = tcp_opt.ok_or_else(|| anyhow::anyhow!("Connection timeout to {}", host.hostname))?; + let mut sess = Session::new()?; + sess.set_tcp_stream(tcp); + sess.handshake()?; + let user = host.user.as_deref().unwrap_or("root"); + let mut authenticated = false; + if let Some(ref key_path) = host.identity_file { + let path = expand_tilde(key_path); + if sess.userauth_pubkey_file(user, None, &path, None).is_ok() { + println!("[DEBUG] SSH deploy connected to {} via Configure SSH Key ({})", host.hostname, key_path); + authenticated = true; + } + } + if !authenticated + && sess.userauth_agent(user).is_ok() { + println!("[DEBUG] SSH deploy connected to {} via SSH Agent", host.hostname); + authenticated = true; + } + + if !authenticated + && let Some(pass) = password + && sess.userauth_password(user, &pass).is_ok() { + println!("[DEBUG] SSH deploy connected to {} via Password", host.hostname); + authenticated = true; + } + if !authenticated { + return Err(anyhow::anyhow!("Authentication failed (tried key, password, and agent)")); + } + let mut channel = sess.channel_session()?; + channel.exec("mkdir -p ~/.ssh && chmod 700 ~/.ssh && cat >> ~/.ssh/authorized_keys && chmod 600 ~/.ssh/authorized_keys")?; + + if pubkey_content.ends_with('\n') { + channel.write_all(pubkey_content.as_bytes())?; + } else { + let mut content = pubkey_content.to_owned(); + content.push('\n'); + channel.write_all(content.as_bytes())?; + } + channel.send_eof()?; + channel.wait_eof()?; + channel.close()?; + channel.wait_close()?; + Ok(()) +} \ No newline at end of file diff --git a/src/ui/add_server_dialog.rs b/src/ui/add_server_dialog.rs index e9aac68..a520840 100644 --- a/src/ui/add_server_dialog.rs +++ b/src/ui/add_server_dialog.rs @@ -1,10 +1,11 @@ #![allow(deprecated)] use gtk4::prelude::*; use crate::config_observer::SshHost; +use crate::ui::ssh_keys::load_ssh_keys; pub fn show_server_dialog( - parent: >k4::Window, - initial_host: Option<&SshHost>, + parent: >k4::Window, + initial_host: Option<&SshHost>, existing_aliases: Vec, on_save: F ) @@ -51,6 +52,25 @@ where F: Fn(SshHost, String) + 'static } } + let keys = load_ssh_keys(); + let key_model = gtk4::StringList::new(&[]); + key_model.append("None (Default Auth)"); + for k in &keys { + key_model.append(&k.name); + } + let key_dropdown = gtk4::DropDown::new(Some(key_model), gtk4::Expression::NONE); + + if let Some(host) = initial_host + && let Some(ref id_file) = host.identity_file { + let id_file_expanded = crate::config_observer::expand_tilde(id_file); + for (i, k) in keys.iter().enumerate() { + if k.priv_path == id_file_expanded { + key_dropdown.set_selected((i + 1) as u32); + break; + } + } + } + content.append(>k4::Label::builder().label("Alias").halign(gtk4::Align::Start).build()); content.append(&alias_entry); content.append(&error_label); @@ -62,36 +82,42 @@ where F: Fn(SshHost, String) + 'static content.append(&user_entry); content.append(>k4::Label::builder().label("Password").halign(gtk4::Align::Start).build()); content.append(&pass_entry); + content.append(>k4::Label::builder().label("SSH Key").halign(gtk4::Align::Start).build()); + content.append(&key_dropdown); let ok_button = dialog.add_button(if initial_host.is_some() { "Save" } else { "Add" }, gtk4::ResponseType::Ok); dialog.add_button("Cancel", gtk4::ResponseType::Cancel); let existing_aliases = Rc::new(existing_aliases); let initial_alias = initial_host.map(|h| h.alias.to_lowercase()); - let alias_entry_clone = alias_entry.clone(); let error_label_clone = error_label.clone(); let ok_button_clone = ok_button.clone(); let existing_aliases_clone = existing_aliases.clone(); - alias_entry.connect_changed(move |e| { let text = e.text().to_string().trim().to_lowercase(); let is_duplicate = existing_aliases_clone.contains(&text) && Some(text.clone()) != initial_alias; - error_label_clone.set_visible(is_duplicate); ok_button_clone.set_sensitive(!is_duplicate && !text.is_empty()); }); dialog.connect_response(move |d, res| { if res == gtk4::ResponseType::Ok { + let selected_key_idx = key_dropdown.selected(); + let identity_file = if selected_key_idx > 0 { + let key = &keys[(selected_key_idx - 1) as usize]; + Some(key.priv_path.to_string_lossy().to_string()) + } else { + None + }; let host = SshHost { alias: alias_entry_clone.text().to_string().trim().to_string(), hostname: host_entry.text().to_string().trim().to_string(), user: Some(user_entry.text().to_string().trim().to_string()).filter(|s| !s.is_empty()), port: port_entry.text().to_string().trim().parse::().ok(), + identity_file, }; let password = pass_entry.text().to_string(); - if !host.alias.is_empty() && !host.hostname.is_empty() { on_save(host, password); } @@ -102,4 +128,4 @@ where F: Fn(SshHost, String) + 'static dialog.present(); } -use std::rc::Rc; +use std::rc::Rc; \ No newline at end of file diff --git a/src/ui/file_explorer.rs b/src/ui/file_explorer.rs index 1db4531..adcc98f 100644 --- a/src/ui/file_explorer.rs +++ b/src/ui/file_explorer.rs @@ -14,7 +14,6 @@ pub struct FileExplorer { status_label: gtk4::Label, host: SshHost, password: Option, - files: Rc>>, } @@ -144,7 +143,6 @@ impl FileExplorer { && let Ok(uris_str) = std::str::from_utf8(bytes.as_ref()) { paths.extend(parse_uri_list_paths(uris_str)); } - if paths.is_empty() && let Ok(uris_str) = value.get::() { paths.extend(parse_uri_list_paths(&uris_str)); @@ -352,7 +350,6 @@ impl ExplorerHandle { let h = h_drag.clone(); let f = f_drag.clone(); let remote_path = format!("{}{}", h.current_path.borrow(), f.name); - let ts = std::time::SystemTime::now().duration_since(std::time::UNIX_EPOCH).unwrap().as_micros(); let local_tmp_part = format!("/tmp/rustmius_dnd_{}_{}.part", ts, f.name); let local_tmp = format!("/tmp/rustmius_dnd_{}_{}", ts, f.name); @@ -370,7 +367,6 @@ impl ExplorerHandle { let host = h.host.clone(); let password = h.password.clone(); let rp = remote_path.clone(); - let lp_part = local_tmp_part.clone(); let lp_final = local_tmp.clone(); @@ -420,17 +416,15 @@ impl ExplorerHandle { let hi = h_dl.clone(); let fi = f_dl.clone(); let rp = format!("{}{}", hi.current_path.borrow(), fi.name); let parent_window = hi.list_box.root().and_then(|r| r.downcast::().ok()); - let dialog = gtk4::FileDialog::builder() .title("Save As") .initial_name(&fi.name) .build(); - let hii = hi.clone(); if let Some(w) = parent_window { dialog.save(Some(&w), gio::Cancellable::NONE, move |res| { - if let Ok(file) = res { - if let Some(path) = file.path() { + if let Ok(file) = res + && let Some(path) = file.path() { let lp = path.to_string_lossy().to_string(); hii.status_label.set_text(&format!("Downloading {}...", fi.name)); let hiii = hii.clone(); @@ -444,7 +438,6 @@ impl ExplorerHandle { } }); } - } }); } }); @@ -492,7 +485,6 @@ impl ExplorerHandle { group.add_action(&ren_action); if f.is_dir { - let h_nf = h.clone(); let f_nf = f.clone(); let nf_action = gio::SimpleAction::new("new_file", None); nf_action.connect_activate(move |_, _| { @@ -616,19 +608,15 @@ where F: Fn(String) + 'static if let Some(p) = parent { dialog.set_transient_for(Some(p)); } - let content = dialog.content_area(); content.set_margin_top(12); content.set_margin_bottom(12); content.set_margin_start(12); content.set_margin_end(12); content.set_spacing(12); content.append(>k4::Label::new(Some(label))); - let entry = gtk4::Entry::builder().text(initial).build(); content.append(&entry); - dialog.add_button("Cancel", gtk4::ResponseType::Cancel); dialog.add_button("OK", gtk4::ResponseType::Ok); - dialog.connect_response(move |d, res| { if res == gtk4::ResponseType::Ok { let text = entry.text().to_string(); @@ -655,4 +643,4 @@ where F: Fn() + 'static d.close(); }); dialog.present(); -} +} \ No newline at end of file diff --git a/src/ui/hud.rs b/src/ui/hud.rs index 4ec3d67..7172a4c 100644 --- a/src/ui/hud.rs +++ b/src/ui/hud.rs @@ -27,7 +27,6 @@ impl Hud { let scrolled = gtk4::ScrolledWindow::new(); scrolled.set_min_content_height(300); scrolled.set_min_content_width(400); - let list_box = gtk4::ListBox::new(); scrolled.set_child(Some(&list_box)); box_container.append(&scrolled); @@ -43,7 +42,6 @@ impl Hud { } pub fn update_results(&self, hosts: &[SshHost], query: &str) { - while let Some(row) = self.list_box.first_child() { self.list_box.remove(&row); } @@ -63,7 +61,6 @@ impl Hud { for host in hosts { let text = format!("{} {}", host.alias, host.hostname); let text_utf32 = Utf32String::from(text.as_str()); - if let Some(score) = matcher.fuzzy_match(text_utf32.slice(..), query_utf32.slice(..)) { matches.push((score, host)); } @@ -81,14 +78,12 @@ impl Hud { let alias_label = gtk4::Label::new(Some(&host.alias)); alias_label.set_halign(gtk4::Align::Start); alias_label.add_css_class("heading"); - let host_label = gtk4::Label::new(Some(&format!("{}@{}", host.user.as_deref().unwrap_or(""), host.hostname))); host_label.set_halign(gtk4::Align::Start); host_label.add_css_class("caption"); row_box.append(&alias_label); row_box.append(&host_label); - self.list_box.append(&row_box); } -} +} \ No newline at end of file diff --git a/src/ui/mod.rs b/src/ui/mod.rs index b93ccc4..47447e6 100644 --- a/src/ui/mod.rs +++ b/src/ui/mod.rs @@ -3,3 +3,4 @@ pub mod window; pub mod server_list; pub mod add_server_dialog; pub mod file_explorer; +pub mod ssh_keys; \ No newline at end of file diff --git a/src/ui/server_list.rs b/src/ui/server_list.rs index 2f7001a..eb715e0 100644 --- a/src/ui/server_list.rs +++ b/src/ui/server_list.rs @@ -13,14 +13,13 @@ pub struct ServerList { } impl ServerList { - pub fn new(on_action: F) -> Self + pub fn new(on_action: F) -> Self where F: Fn(ServerAction) + 'static + Clone { let scrolled = gtk4::ScrolledWindow::builder() .hscrollbar_policy(gtk4::PolicyType::Never) .vexpand(true) .build(); - let flow_box = gtk4::FlowBox::builder() .selection_mode(gtk4::SelectionMode::None) .valign(gtk4::Align::Start) @@ -33,7 +32,6 @@ impl ServerList { .margin_start(24) .margin_end(24) .build(); - scrolled.set_child(Some(&flow_box)); let sl = Self { container: scrolled, flow_box }; @@ -41,7 +39,7 @@ impl ServerList { sl } - pub fn refresh(&self, on_action: F) + pub fn refresh(&self, on_action: F) where F: Fn(ServerAction) + 'static + Clone { while let Some(child) = self.flow_box.first_child() { @@ -54,7 +52,7 @@ impl ServerList { } } - fn add_host_row(&self, host: &SshHost, on_action: F) + fn add_host_row(&self, host: &SshHost, on_action: F) where F: Fn(ServerAction) + 'static + Clone { let frame = gtk4::Frame::new(None); @@ -67,7 +65,6 @@ impl ServerList { content_box.set_margin_end(12); let header_box = gtk4::Box::new(gtk4::Orientation::Horizontal, 8); - let alias_label = gtk4::Label::builder() .label(&host.alias) .halign(gtk4::Align::Start) @@ -76,7 +73,6 @@ impl ServerList { .build(); let actions_box = gtk4::Box::new(gtk4::Orientation::Horizontal, 4); - let edit_btn = gtk4::Button::from_icon_name("document-edit-symbolic"); edit_btn.add_css_class("flat"); let host_edit = host.clone(); @@ -121,4 +117,4 @@ impl ServerList { frame.set_child(Some(&content_box)); self.flow_box.insert(&frame, -1); } -} +} \ No newline at end of file diff --git a/src/ui/ssh_keys.rs b/src/ui/ssh_keys.rs new file mode 100644 index 0000000..75d257f --- /dev/null +++ b/src/ui/ssh_keys.rs @@ -0,0 +1,652 @@ +#![allow(deprecated)] +use gtk4::prelude::*; +use gtk4::glib; +use std::rc::Rc; +use std::cell::RefCell; +use directories::UserDirs; +use std::path::PathBuf; +use crate::config_observer::load_hosts; + +fn is_valid_key_name(name: &str) -> bool { + if name.is_empty() || name.contains('\0') { + return false; + } + let p = std::path::Path::new(name); + p.components().count() == 1 + && p.file_name().map(|n| n == std::ffi::OsStr::new(name)).unwrap_or(false) +} + +fn make_error_alert(parent: Option<>k4::Window>, title: &str, secondary: &str) -> gtk4::MessageDialog { + let builder = gtk4::MessageDialog::builder() + .modal(true) + .message_type(gtk4::MessageType::Error) + .buttons(gtk4::ButtonsType::Ok) + .text(title) + .secondary_text(secondary); + let alert = if let Some(w) = parent { + builder.transient_for(w).build() + } else { + builder.build() + }; + alert.connect_response(|a, _| a.close()); + alert +} + +#[derive(Clone)] +pub struct SshKeyPair { + pub name: String, + pub pub_path: PathBuf, + pub priv_path: PathBuf, +} + +fn get_ssh_dir() -> Option { + UserDirs::new().map(|dirs| dirs.home_dir().join(".ssh")) +} + +pub fn load_ssh_keys() -> Vec { + let mut keys = Vec::new(); + if let Some(ssh_dir) = get_ssh_dir() + && let Ok(entries) = std::fs::read_dir(&ssh_dir) { + for entry in entries.flatten() { + let path = entry.path(); + if path.is_file() && path.extension().and_then(|s| s.to_str()) == Some("pub") { + let mut priv_path = path.clone(); + priv_path.set_extension(""); + if priv_path.exists() { + let name = path.file_stem().unwrap_or_default().to_string_lossy().to_string(); + keys.push(SshKeyPair { + name, + pub_path: path, + priv_path, + }); + } + } + } + } + keys.sort_by(|a, b| a.name.cmp(&b.name)); + keys +} + +pub fn build_ssh_keys_ui(window: >k4::ApplicationWindow) -> gtk4::Box { + let main_box = gtk4::Box::new(gtk4::Orientation::Vertical, 12); + main_box.set_margin_top(24); main_box.set_margin_bottom(24); + main_box.set_margin_start(24); main_box.set_margin_end(24); + + let header_box = gtk4::Box::new(gtk4::Orientation::Horizontal, 12); + let title = gtk4::Label::builder().label("SSH Keys").halign(gtk4::Align::Start).hexpand(true).build(); + title.add_css_class("title-1"); + let gen_btn = gtk4::Button::from_icon_name("list-add-symbolic"); + gen_btn.set_tooltip_text(Some("Generate New Key")); + gen_btn.add_css_class("suggested-action"); + let import_btn = gtk4::Button::from_icon_name("document-import-symbolic"); + import_btn.set_tooltip_text(Some("Import Key")); + import_btn.add_css_class("flat"); + let refresh_btn = gtk4::Button::from_icon_name("view-refresh-symbolic"); + refresh_btn.set_tooltip_text(Some("Refresh")); + + header_box.append(&title); + header_box.append(&refresh_btn); + header_box.append(&import_btn); + header_box.append(&gen_btn); + + main_box.append(&header_box); + + let list_box = gtk4::ListBox::new(); + list_box.set_selection_mode(gtk4::SelectionMode::None); + list_box.add_css_class("boxed-list"); + let scrolled = gtk4::ScrolledWindow::builder() + .child(&list_box) + .vexpand(true) + .build(); + main_box.append(&scrolled); + + let list_box_rc = Rc::new(list_box); + let window_rc = window.clone(); + + let refresh_ui: Rc>>> = Rc::new(RefCell::new(None)); + + let do_refresh = { + let lb = list_box_rc.clone(); + let win = window_rc.clone(); + let rwh = Rc::downgrade(&refresh_ui); + Rc::new(move || { + while let Some(child) = lb.first_child() { + lb.remove(&child); + } + + let keys = load_ssh_keys(); + if keys.is_empty() { + let empty_lbl = gtk4::Label::new(Some("No SSH keys found in ~/.ssh/")); + empty_lbl.set_margin_top(24); + empty_lbl.set_margin_bottom(24); + empty_lbl.add_css_class("dim-label"); + lb.append(&empty_lbl); + } else { + for key in keys { + let row = gtk4::ListBoxRow::new(); + let hbox = gtk4::Box::new(gtk4::Orientation::Horizontal, 12); + hbox.set_margin_start(12); hbox.set_margin_end(12); + hbox.set_margin_top(12); hbox.set_margin_bottom(12); + let icon = gtk4::Image::from_icon_name("network-vpn-symbolic"); + icon.set_pixel_size(24); + let name_lbl = gtk4::Label::new(Some(&key.name)); + name_lbl.set_halign(gtk4::Align::Start); + name_lbl.set_hexpand(true); + + let deploy_btn = gtk4::Button::from_icon_name("document-send-symbolic"); + deploy_btn.set_tooltip_text(Some("Deploy to Server")); + deploy_btn.add_css_class("flat"); + + let del_btn = gtk4::Button::from_icon_name("user-trash-symbolic"); + del_btn.set_tooltip_text(Some("Delete Key")); + del_btn.add_css_class("destructive-action"); + del_btn.add_css_class("flat"); + + let key_clone1 = key.clone(); + let key_clone2 = key.clone(); + let w_clone = win.clone(); + let w_clone2 = win.clone(); + let handle = rwh.clone(); + + del_btn.connect_clicked(move |_| { + let dialog = gtk4::MessageDialog::builder() + .transient_for(&w_clone) + .modal(true) + .message_type(gtk4::MessageType::Warning) + .buttons(gtk4::ButtonsType::OkCancel) + .text(format!("Delete key '{}'?", key_clone1.name)) + .secondary_text("This action cannot be undone and will delete both public and private key files.") + .build(); + + let p1 = key_clone1.pub_path.clone(); + let p2 = key_clone1.priv_path.clone(); + let h = handle.clone(); + let w_del = w_clone.clone(); + + dialog.connect_response(move |d, res| { + if res == gtk4::ResponseType::Ok { + if let Err(e) = std::fs::remove_file(&p2) { + let alert = gtk4::MessageDialog::builder() + .transient_for(&w_del) + .modal(true) + .message_type(gtk4::MessageType::Error) + .buttons(gtk4::ButtonsType::Ok) + .text("Failed to Delete Key") + .secondary_text(format!("Could not delete private key: {}", e)) + .build(); + alert.connect_response(|a, _| a.close()); + alert.present(); + d.close(); + return; + } + if let Err(e) = std::fs::remove_file(&p1) { + let alert = gtk4::MessageDialog::builder() + .transient_for(&w_del) + .modal(true) + .message_type(gtk4::MessageType::Error) + .buttons(gtk4::ButtonsType::Ok) + .text("Failed to Delete Key") + .secondary_text(format!("Private key deleted, but could not delete public key: {}", e)) + .build(); + alert.connect_response(|a, _| a.close()); + alert.present(); + d.close(); + return; + } + if let Some(rc) = h.upgrade() + && let Some(r) = rc.borrow().as_ref() { r(); } + } + d.close(); + }); + dialog.present(); + }); + + deploy_btn.connect_clicked(move |_| { + show_deploy_dialog(&w_clone2, &key_clone2); + }); + + hbox.append(&icon); + hbox.append(&name_lbl); + hbox.append(&deploy_btn); + hbox.append(&del_btn); + row.set_child(Some(&hbox)); + lb.append(&row); + } + } + }) + }; + + *refresh_ui.borrow_mut() = Some(do_refresh.clone()); + do_refresh(); + + let r_refresh = do_refresh.clone(); + refresh_btn.connect_clicked(move |_| { r_refresh(); }); + + let r_win = window_rc.clone(); + let g_refresh = do_refresh.clone(); + gen_btn.connect_clicked(move |_| { + show_generate_dialog(&r_win, g_refresh.clone()); + }); + let w_win = window_rc.clone(); + let i_refresh = do_refresh.clone(); + import_btn.connect_clicked(move |_| { + show_import_dialog(&w_win, i_refresh.clone()); + }); + + main_box +} + +fn show_deploy_dialog(parent: >k4::ApplicationWindow, key: &SshKeyPair) { + let dialog = gtk4::Dialog::builder() + .transient_for(parent) + .modal(true) + .title(format!("Deploy key: {}", key.name)) + .default_width(350) + .build(); + + let content = dialog.content_area(); + content.set_margin_top(12); content.set_margin_bottom(12); + content.set_margin_start(12); content.set_margin_end(12); + content.set_spacing(12); + + let hosts = load_hosts(); + if hosts.is_empty() { + content.append(>k4::Label::new(Some("No servers available."))); + dialog.add_button("Close", gtk4::ResponseType::Close); + dialog.connect_response(|d, _| d.close()); + dialog.present(); + return; + } + + let model = gtk4::StringList::new(&[]); + for h in &hosts { + model.append(&h.alias); + } + + let dropdown = gtk4::DropDown::new(Some(model), gtk4::Expression::NONE); + content.append(>k4::Label::builder().label("Select Server").halign(gtk4::Align::Start).build()); + content.append(&dropdown); + + let pass_entry = gtk4::PasswordEntry::builder() + .placeholder_text("Server Password (optional if agent is running)") + .show_peek_icon(true) + .build(); + content.append(>k4::Label::builder().label("Password (for deployment)").halign(gtk4::Align::Start).build()); + content.append(&pass_entry); + + let status_label = gtk4::Label::new(None); + status_label.set_halign(gtk4::Align::Start); + content.append(&status_label); + + let _ok_btn = dialog.add_button("Deploy", gtk4::ResponseType::Ok); + dialog.add_button("Cancel", gtk4::ResponseType::Cancel); + + let key_path = key.pub_path.clone(); + dialog.connect_response(move |d, res| { + if res == gtk4::ResponseType::Ok { + let idx = dropdown.selected(); + if idx < hosts.len() as u32 { + let host = hosts[idx as usize].clone(); + let password = pass_entry.text().to_string(); + let pubkey = match std::fs::read_to_string(&key_path) { + Ok(content) => content, + Err(e) => { + let md = gtk4::MessageDialog::builder() + .modal(true) + .message_type(gtk4::MessageType::Error) + .buttons(gtk4::ButtonsType::Ok) + .text("Failed to Read Public Key") + .secondary_text(format!("Could not read '{}': {}", key_path.display(), e)) + .build(); + if let Some(w) = d.transient_for() { + md.set_transient_for(Some(&w)); + } + md.connect_response(|md, _| md.close()); + md.present(); + return; + } + }; + let parent_win_weak = d.transient_for().and_then(|w| w.downcast::().ok()); + let close_dialog = d.clone(); + glib::MainContext::default().spawn_local(async move { + let mut final_password = None; + if !password.is_empty() { + final_password = Some(password); + } else if let Ok(keyring) = oo7::Keyring::new().await { + let mut attr = std::collections::HashMap::new(); + let alias_lower = host.alias.to_lowercase(); + attr.insert("rustmius-server-alias", alias_lower.as_str()); + if let Ok(items) = keyring.search_items(&attr).await + && let Some(item) = items.first() + && let Ok(pass) = item.secret().await { + final_password = Some(String::from_utf8_lossy(pass.as_ref()).to_string()); + } + } + + let h_c = host.clone(); + let pk_c = pubkey.clone(); + let result = tokio::task::spawn_blocking(move || { + crate::ssh_engine::deploy_pubkey(&h_c, final_password, &pk_c) + }).await.unwrap_or_else(|_| Err(anyhow::anyhow!("Task panic"))); + + match result { + Ok(_) => { + let md = gtk4::MessageDialog::builder() + .modal(true) + .message_type(gtk4::MessageType::Info) + .buttons(gtk4::ButtonsType::Ok) + .text("Deployed Successfully!") + .build(); + if let Some(ref w) = parent_win_weak { + md.set_transient_for(Some(w)); + } + md.connect_response(|md, _| md.close()); + md.present(); + close_dialog.close(); + }, + Err(e) => { + let md = gtk4::MessageDialog::builder() + .modal(true) + .message_type(gtk4::MessageType::Error) + .buttons(gtk4::ButtonsType::Ok) + .text("Deployment Failed") + .secondary_text(e.to_string()) + .build(); + if let Some(ref w) = parent_win_weak { + md.set_transient_for(Some(w)); + } + md.connect_response(|md, _| md.close()); + md.present(); + } + } + }); + } + } else { + d.close(); + } + }); + + dialog.present(); +} + +fn show_generate_dialog(parent: >k4::ApplicationWindow, on_save: Rc) { + let dialog = gtk4::Dialog::builder() + .transient_for(parent) + .modal(true) + .title("Generate SSH Key") + .default_width(350) + .build(); + + let content = dialog.content_area(); + content.set_margin_top(12); content.set_margin_bottom(12); + content.set_margin_start(12); content.set_margin_end(12); + content.set_spacing(12); + + let name_entry = gtk4::Entry::builder().placeholder_text("Key Name (e.g. id_ed25519_mykey)").build(); + let pass_entry = gtk4::PasswordEntry::builder().placeholder_text("Passphrase (optional)").show_peek_icon(true).build(); + let comment_entry = gtk4::Entry::builder().placeholder_text("Comment (optional, e.g. user@hostname)").build(); + + content.append(>k4::Label::builder().label("Key Filename").halign(gtk4::Align::Start).build()); + content.append(&name_entry); + content.append(>k4::Label::builder().label("Passphrase").halign(gtk4::Align::Start).build()); + content.append(&pass_entry); + content.append(>k4::Label::builder().label("Comment").halign(gtk4::Align::Start).build()); + content.append(&comment_entry); + + let ok_btn = dialog.add_button("Generate", gtk4::ResponseType::Ok); + ok_btn.set_sensitive(false); + dialog.add_button("Cancel", gtk4::ResponseType::Cancel); + + let ok_rc = ok_btn.clone(); + name_entry.connect_changed(move |e| { + ok_rc.set_sensitive(!e.text().is_empty()); + }); + + dialog.connect_response(move |d, res| { + if res == gtk4::ResponseType::Ok { + let name = name_entry.text().to_string(); + let pass = pass_entry.text().to_string(); + let comment = comment_entry.text().to_string(); + + if !is_valid_key_name(&name) { + let alert = make_error_alert( + d.transient_for().as_ref().map(|w| w.upcast_ref()), + "Invalid Key Name", + "The key name must be a simple filename with no path separators or special components.", + ); + alert.present(); + return; + } + if let Some(ssh_dir) = get_ssh_dir() { + let file_path = ssh_dir.join(&name); + let pub_path = ssh_dir.join(format!("{}.pub", name)); + + if file_path.exists() || pub_path.exists() { + let alert = make_error_alert( + d.transient_for().as_ref().map(|w| w.upcast_ref()), + "Key Already Exists", + &format!("A file named '{}' or its public key already exists in ~/.ssh/. Choose a different name.", name), + ); + alert.present(); + return; + } + + let parent_win = d.transient_for() + .and_then(|w| w.downcast::().ok()); + d.close(); + + let on_save_spawn = on_save.clone(); + glib::MainContext::default().spawn_local(async move { + let result = tokio::task::spawn_blocking(move || { + let mut cmd = std::process::Command::new("ssh-keygen"); + cmd.arg("-t").arg("ed25519") + .arg("-f").arg(&file_path) + .arg("-N").arg(&pass) + .arg("-q"); + if !comment.is_empty() { + cmd.arg("-C").arg(&comment); + } + cmd.output() + }).await; + + let (success, stderr_msg) = match result { + Ok(Ok(output)) => (output.status.success(), String::from_utf8_lossy(&output.stderr).to_string()), + Ok(Err(e)) => (false, e.to_string()), + Err(e) => (false, e.to_string()), + }; + + if success { + on_save_spawn(); + } else { + let secondary = if stderr_msg.is_empty() { + "ssh-keygen exited with a non-zero status.".to_string() + } else { + stderr_msg + }; + let alert = make_error_alert( + parent_win.as_ref().map(|w| w.upcast_ref()), + "Key Generation Failed!", + &secondary, + ); + alert.present(); + } + }); + } + } else { + d.close(); + } + }); + + dialog.present(); +} + +fn show_import_dialog(parent: >k4::ApplicationWindow, on_save: Rc) { + let dialog = gtk4::Dialog::builder() + .transient_for(parent) + .modal(true) + .title("Import Private Key") + .default_width(450) + .default_height(400) + .build(); + + let content = dialog.content_area(); + content.set_margin_top(12); content.set_margin_bottom(12); + content.set_margin_start(12); content.set_margin_end(12); + content.set_spacing(12); + + let name_entry = gtk4::Entry::builder().placeholder_text("Key Name (e.g. id_rsa)").build(); + let text_buffer = gtk4::TextBuffer::new(None); + let text_view = gtk4::TextView::builder() + .buffer(&text_buffer) + .monospace(true) + .vexpand(true) + .build(); + let scrolled = gtk4::ScrolledWindow::builder() + .child(&text_view) + .min_content_height(250) + .vexpand(true) + .build(); + + content.append(>k4::Label::builder().label("Key Filename").halign(gtk4::Align::Start).build()); + content.append(&name_entry); + content.append(>k4::Label::builder().label("Paste Private Key").halign(gtk4::Align::Start).build()); + content.append(&scrolled); + + let ok_btn = dialog.add_button("Import", gtk4::ResponseType::Ok); + ok_btn.set_sensitive(false); + dialog.add_button("Cancel", gtk4::ResponseType::Cancel); + + let ok_rc = ok_btn.clone(); + name_entry.connect_changed(move |e| { + ok_rc.set_sensitive(!e.text().is_empty()); + }); + + dialog.connect_response(move |d, res| { + if res == gtk4::ResponseType::Ok { + let name = name_entry.text().to_string(); + let (start, end) = text_buffer.bounds(); + let key_content = text_buffer.text(&start, &end, false).to_string(); + + if !is_valid_key_name(&name) { + let alert = make_error_alert( + d.transient_for().as_ref().map(|w| w.upcast_ref()), + "Invalid Key Name", + "The key name must be a simple filename with no path separators or special components.", + ); + alert.present(); + return; + } + if let Some(ssh_dir) = get_ssh_dir() { + let file_path = ssh_dir.join(&name); + let pub_path = ssh_dir.join(format!("{}.pub", name)); + + if file_path.exists() || pub_path.exists() { + let alert = make_error_alert( + d.transient_for().as_ref().map(|w| w.upcast_ref()), + "Key Already Exists", + &format!("A file named '{}' or its public key already exists in ~/.ssh/.", name), + ); + alert.present(); + return; + } + + #[cfg(unix)] + let write_result = { + use std::os::unix::fs::OpenOptionsExt; + std::fs::OpenOptions::new() + .create(true) + .truncate(true) + .write(true) + .mode(0o600) + .open(&file_path) + .and_then(|mut f| { + use std::io::Write; + f.write_all(key_content.as_bytes()) + }) + }; + #[cfg(not(unix))] + let write_result = std::fs::write(&file_path, &key_content); + + if let Err(e) = write_result { + let alert = make_error_alert( + d.transient_for().as_ref().map(|w| w.upcast_ref()), + "Failed to Write Key File", + &e.to_string(), + ); + alert.present(); + return; + } + + let parent_win = d.transient_for() + .and_then(|w| w.downcast::().ok()); + d.close(); + + let on_save_spawn = on_save.clone(); + glib::MainContext::default().spawn_local(async move { + let pub_path = ssh_dir.join(format!("{}.pub", name)); + let file_path_cleanup = ssh_dir.join(&name); + let file_path_keygen = ssh_dir.join(&name); + let result = tokio::task::spawn_blocking(move || { + std::process::Command::new("ssh-keygen") + .arg("-y") + .arg("-f").arg(&file_path_keygen) + .output() + }).await; + + match result { + Ok(Ok(output)) if output.status.success() => { + if let Err(e) = std::fs::write(&pub_path, output.stdout) { + let _ = std::fs::remove_file(&file_path_cleanup); + let alert = make_error_alert( + parent_win.as_ref().map(|w| w.upcast_ref()), + "Failed to Write Public Key", + &e.to_string(), + ); + alert.present(); + } else { + on_save_spawn(); + } + } + Ok(Ok(output)) => { + let _ = std::fs::remove_file(&file_path_cleanup); + let stderr = String::from_utf8_lossy(&output.stderr).to_string(); + let secondary = if stderr.is_empty() { + "Check if the pasted key is a valid private key or if it is encrypted.".to_string() + } else { + stderr + }; + let alert = make_error_alert( + parent_win.as_ref().map(|w| w.upcast_ref()), + "Key Import Failed!", + &secondary, + ); + alert.present(); + } + Ok(Err(e)) => { + let _ = std::fs::remove_file(&file_path_cleanup); + let alert = make_error_alert( + parent_win.as_ref().map(|w| w.upcast_ref()), + "Key Import Failed!", + &e.to_string(), + ); + alert.present(); + } + Err(e) => { + let _ = std::fs::remove_file(&file_path_cleanup); + let alert = make_error_alert( + parent_win.as_ref().map(|w| w.upcast_ref()), + "Key Import Failed!", + &e.to_string(), + ); + alert.present(); + } + } + }); + } + } else { + d.close(); + } + }); + + dialog.present(); +} \ No newline at end of file diff --git a/src/ui/window.rs b/src/ui/window.rs index ab0fe7a..37291b9 100644 --- a/src/ui/window.rs +++ b/src/ui/window.rs @@ -3,6 +3,7 @@ use gtk4::{glib, gio}; use crate::ui::server_list::{ServerList, ServerAction}; use crate::ui::add_server_dialog::show_server_dialog; use crate::ui::file_explorer::FileExplorer; +use crate::ui::ssh_keys::build_ssh_keys_ui; use crate::config_observer::{add_host_to_config, delete_host_from_config, load_hosts}; use vte4::prelude::*; use std::rc::Rc; @@ -78,7 +79,6 @@ pub fn build_ui(app: >k4::Application) { let window_clone = window.clone(); let notebook_clone = notebook.clone(); let refresh_ui_weak = Rc::downgrade(&refresh_ui); - notebook.connect_switch_page(move |nb, _, _| { *last_pg.borrow_mut() = nb.current_page().unwrap_or(0); @@ -90,10 +90,6 @@ pub fn build_ui(app: >k4::Application) { } }); - - - - let do_refresh = { let sc = stack_clone.clone(); let wc = window_clone.clone(); @@ -114,17 +110,14 @@ pub fn build_ui(app: >k4::Application) { let window = sl_window.clone(); let notebook = sl_notebook.clone(); let refresh = sl_refresh_handle.borrow().as_ref().unwrap().clone(); - match action { ServerAction::Connect(host) => { stack.set_visible_child_name("sessions"); - let session_box = gtk4::Box::new(gtk4::Orientation::Vertical, 0); let toolbar = gtk4::Box::new(gtk4::Orientation::Horizontal, 6); toolbar.set_margin_top(4); toolbar.set_margin_bottom(4); toolbar.set_margin_start(6); - let explorer_btn = gtk4::Button::from_icon_name("folder-remote-symbolic"); explorer_btn.add_css_class("flat"); explorer_btn.set_tooltip_text(Some("File Explorer")); @@ -162,7 +155,6 @@ pub fn build_ui(app: >k4::Application) { close_btn.add_css_class("flat"); tab_label_box.append(&label); tab_label_box.append(&close_btn); - let mut insert_pos = notebook.n_pages(); for i in 0..notebook.n_pages() { if let Some(c) = notebook.nth_page(Some(i)) @@ -171,7 +163,6 @@ pub fn build_ui(app: >k4::Application) { notebook.insert_page(&session_box, Some(&tab_label_box), Some(insert_pos)); notebook.set_tab_reorderable(&session_box, true); notebook.set_current_page(Some(insert_pos)); - let nb_close = notebook.clone(); let sb_close = session_box.clone(); @@ -194,7 +185,6 @@ pub fn build_ui(app: >k4::Application) { let h_alias = h_exp.alias.clone(); let nb_spawn = nb_exp.clone(); - glib::MainContext::default().spawn_local(async move { let mut password = None; if let Ok(keyring) = oo7::Keyring::new().await { @@ -204,7 +194,7 @@ pub fn build_ui(app: >k4::Application) { if let Ok(items) = keyring.search_items(&attr).await && let Some(item) = items.first() && let Ok(pass) = item.secret().await { - password = Some(String::from_utf8_lossy(&pass).to_string()); + password = Some(String::from_utf8_lossy(pass.as_ref()).to_string()); } } @@ -251,20 +241,34 @@ pub fn build_ui(app: >k4::Application) { }); }); - - let host_str = host.hostname.clone(); let user_str = host.user.clone().unwrap_or_else(|| "root".to_string()); let host_alias = host.alias.clone(); let exe_path = std::env::current_exe().unwrap_or_default().to_string_lossy().to_string(); let mut envv: Vec = std::env::vars().map(|(k, v)| format!("{}={}", k, v)).collect(); - envv.push(format!("SSH_ASKPASS={}", exe_path)); - envv.push("SSH_ASKPASS_REQUIRE=force".to_string()); - envv.push(format!("RUSTMIUS_ASKPASS_ALIAS={}", host_alias)); + if host.identity_file.is_none() { + envv.push(format!("SSH_ASKPASS={}", exe_path)); + envv.push("SSH_ASKPASS_REQUIRE=force".to_string()); + envv.push(format!("RUSTMIUS_ASKPASS_ALIAS={}", host_alias)); + } envv.push("DISPLAY=:0".to_string()); let env_refs: Vec<&str> = envv.iter().map(|s| s.as_str()).collect(); let port_str = host.port.unwrap_or(22).to_string(); - terminal.spawn_async(vte4::PtyFlags::DEFAULT, None, &["/usr/bin/ssh", "-p", &port_str, "-o", "StrictHostKeyChecking=no", "-o", "PubkeyAuthentication=no", &format!("{}@{}", user_str, host_str)], &env_refs, glib::SpawnFlags::SEARCH_PATH, || {}, -1, None::<&gio::Cancellable>, |_| {}); + let mut ssh_args = vec![ + "/usr/bin/ssh".to_string(), + "-p".to_string(), + port_str, + "-o".to_string(), + "StrictHostKeyChecking=no".to_string(), + ]; + if let Some(identity_file) = &host.identity_file { + ssh_args.push("-i".to_string()); + ssh_args.push(identity_file.clone()); + } + ssh_args.push(format!("{}@{}", user_str, host_str)); + let ssh_args_refs: Vec<&str> = ssh_args.iter().map(|s| s.as_str()).collect(); + + terminal.spawn_async(vte4::PtyFlags::DEFAULT, None, &ssh_args_refs, &env_refs, glib::SpawnFlags::SEARCH_PATH, || {}, -1, None::<&gio::Cancellable>, |_| {}); }, ServerAction::Delete(host) => { let _ = delete_host_from_config(&host.alias); @@ -308,12 +312,10 @@ pub fn build_ui(app: >k4::Application) { if let Some(c) = notebook.nth_page(Some(i)) && c.widget_name() == "server_list_tab" { server_list_idx = Some(i); break; } } - sl.container.set_widget_name("server_list_tab"); let tab_box = gtk4::Box::new(gtk4::Orientation::Horizontal, 6); tab_box.append(>k4::Image::from_icon_name("view-grid-symbolic")); tab_box.append(>k4::Label::new(Some("Connect"))); - if let Some(idx) = server_list_idx { notebook.remove_page(Some(idx)); notebook.insert_page(&sl.container, Some(&tab_box), Some(idx)); @@ -351,16 +353,7 @@ pub fn build_ui(app: >k4::Application) { let stack_nav_settings = stack.clone(); btn_settings.connect_clicked(move |_| { stack_nav_settings.set_visible_child_name("settings"); }); - let keys_box = gtk4::Box::new(gtk4::Orientation::Vertical, 24); - keys_box.set_margin_top(48); keys_box.set_margin_bottom(48); keys_box.set_margin_start(48); keys_box.set_margin_end(48); - keys_box.set_halign(gtk4::Align::Center); keys_box.set_valign(gtk4::Align::Center); - let wip_icon = gtk4::Image::from_icon_name("system-shutdown-symbolic"); - wip_icon.set_pixel_size(96); wip_icon.add_css_class("dim-label"); - let wip_label = gtk4::Label::new(Some("SSH Keys Management - WIP")); - wip_label.add_css_class("title-1"); - let wip_subtitle = gtk4::Label::new(Some("This feature is under development")); - wip_subtitle.add_css_class("dim-label"); wip_subtitle.add_css_class("title-4"); - keys_box.append(&wip_icon); keys_box.append(&wip_label); keys_box.append(&wip_subtitle); + let keys_box = build_ssh_keys_ui(&window); stack.add_named(&keys_box, Some("ssh_keys")); let settings_box = gtk4::Box::new(gtk4::Orientation::Vertical, 24); @@ -401,4 +394,4 @@ pub fn build_ui(app: >k4::Application) { root.append(&sidebar); root.append(&separator); root.append(&content_box); window.set_child(Some(&root)); window.present(); -} +} \ No newline at end of file