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).
+[](https://github.com/Cleboost/Rustmius/releases)
+[](LICENSE)
+[](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