├── .deepsource.toml ├── .dockerignore ├── .github ├── FUNDING.yml └── workflows │ ├── cd.yml │ └── ci.yml ├── .gitignore ├── Cargo.lock ├── Cargo.toml ├── Dockerfile ├── LICENSE ├── README.md ├── bin └── vcruntime140.dll ├── cliff.toml ├── env ├── env.fish └── env.sh ├── resources ├── bob-icon.png ├── bob.png └── tapes │ ├── demo.gif │ └── demo.tape └── src ├── cli.rs ├── config.rs ├── github_requests.rs ├── handlers ├── erase_handler.rs ├── install_handler.rs ├── list_handler.rs ├── list_remote_handler.rs ├── mod.rs ├── rollback_handler.rs ├── run_handler.rs ├── sync_handler.rs ├── uninstall_handler.rs ├── update_handler.rs └── use_handler.rs ├── helpers ├── checksum.rs ├── directories.rs ├── filesystem.rs ├── mod.rs ├── processes.rs ├── sync.rs ├── unarchive.rs └── version │ ├── mod.rs │ ├── nightly.rs │ └── types.rs └── main.rs /.deepsource.toml: -------------------------------------------------------------------------------- 1 | version = 1 2 | 3 | [[analyzers]] 4 | name = "rust" 5 | 6 | [analyzers.meta] 7 | msrv = "stable" -------------------------------------------------------------------------------- /.dockerignore: -------------------------------------------------------------------------------- 1 | target 2 | resources 3 | bin 4 | -------------------------------------------------------------------------------- /.github/FUNDING.yml: -------------------------------------------------------------------------------- 1 | # These are supported funding model platforms 2 | 3 | polar: MordechaiHadad 4 | github: [MordechaiHadad] 5 | -------------------------------------------------------------------------------- /.github/workflows/cd.yml: -------------------------------------------------------------------------------- 1 | name: Continuous Deployment 2 | 3 | on: 4 | push: 5 | tags: 6 | - "v*.*.*" 7 | 8 | jobs: 9 | check-version: 10 | runs-on: ubuntu-latest 11 | steps: 12 | - name: Checkout code 13 | uses: actions/checkout@v2 14 | - name: Check version 15 | id: check_version 16 | run: | 17 | VERSION=v$(grep '^version =' Cargo.toml | cut -d '"' -f 2 | head -n 1) 18 | GIT_TAG_VERSION=${{ github.ref }} 19 | GIT_TAG_VERSION=${GIT_TAG_VERSION#refs/tags/} 20 | if [[ "$VERSION" != "$GIT_TAG_VERSION" ]]; then 21 | echo "Version in Cargo.toml ($VERSION) does not match pushed tag ($GIT_TAG_VERSION)" 22 | exit 1 23 | fi 24 | 25 | build: 26 | needs: [check-version] 27 | strategy: 28 | matrix: 29 | os: 30 | - { NAME: linux, OS: ubuntu-latest, ARCH: x86_64, PATH: target/optimized/bob, TARGET: "" } 31 | - { NAME: linux, OS: ubuntu-24.04-arm, ARCH: arm, PATH: target/optimized/bob, TARGET: "" } 32 | - { NAME: macos, OS: macos-13, ARCH: x86_64, PATH: target/optimized/bob, TARGET: "" } 33 | - { NAME: windows, OS: windows-latest, ARCH: x86_64, PATH: build, TARGET: "" } 34 | - { NAME: macos, OS: macos-latest, ARCH: arm, PATH: target/optimized/bob, TARGET: "" } 35 | tls: 36 | - { NAME: Rustls, SUFFIX: "", ARGS: "" } 37 | - { NAME: OpenSSL, SUFFIX: "-openssl", ARGS: "--no-default-features --features native-tls" } 38 | runs-on: ${{matrix.os.OS}} 39 | steps: 40 | - uses: actions/checkout@v4 41 | - name: Install Rust 42 | uses: actions-rs/toolchain@v1 43 | with: 44 | toolchain: stable 45 | profile: minimal 46 | override: true 47 | - name: Install OpenSSL libraries 48 | run: sudo apt update && sudo apt install libssl-dev 49 | if: matrix.os.OS == 'ubuntu-latest' && matrix.tls.NAME == 'OpenSSL' 50 | - uses: Swatinem/rust-cache@v1 51 | - name: Build Bob 52 | uses: actions-rs/cargo@v1 53 | with: 54 | command: build 55 | args: --locked --profile optimized ${{ matrix.tls.ARGS }} 56 | - name: Install AppImage tools 57 | if: matrix.os.NAME == 'linux' && matrix.tls.NAME == 'Rustls' 58 | run: | 59 | sudo apt update && sudo apt install -y libfuse2 # Needed by AppImage/linuxdeploy 60 | 61 | # Determine the correct architecture for linuxdeploy download 62 | DOWNLOAD_ARCH=${{ matrix.os.ARCH }} 63 | if [[ "${{ matrix.os.ARCH }}" == "arm" ]]; then 64 | DOWNLOAD_ARCH="aarch64" 65 | fi 66 | 67 | echo "Downloading linuxdeploy tools for architecture: $DOWNLOAD_ARCH" 68 | wget -c "https://github.com/linuxdeploy/linuxdeploy/releases/download/continuous/linuxdeploy-$DOWNLOAD_ARCH.AppImage" -O linuxdeploy 69 | wget -c "https://github.com/linuxdeploy/linuxdeploy-plugin-appimage/releases/download/continuous/linuxdeploy-plugin-appimage-$DOWNLOAD_ARCH.AppImage" -O linuxdeploy-plugin-appimage 70 | chmod +x linuxdeploy linuxdeploy-plugin-appimage 71 | 72 | - name: Prepare AppDir 73 | if: matrix.os.NAME == 'linux' && matrix.tls.NAME == 'Rustls' 74 | run: | 75 | mkdir -p AppDir/usr/bin AppDir/usr/share/icons/hicolor/256x256/apps AppDir/usr/share/applications 76 | cp target/optimized/bob AppDir/usr/bin/ 77 | cp resources/bob-icon.png AppDir/usr/share/icons/hicolor/256x256/apps/bob.png 78 | cat < AppDir/bob.desktop 79 | [Desktop Entry] 80 | Name=Bob Neovim Manager 81 | Exec=bob 82 | Icon=bob 83 | Type=Application 84 | Categories=Utility;Development; 85 | Comment=A cross-platform Neovim version manager 86 | EOF 87 | cp AppDir/bob.desktop AppDir/usr/share/applications/ 88 | 89 | # Verify the file exists right before linuxdeploy 90 | ls -l AppDir/usr/bin/bob 91 | 92 | export UPD_INFO="gh-releases-zsync|Matsuuu|bob|latest|bob-${{ matrix.os.ARCH }}.AppImage.zsync" 93 | export OUTPUT="bob-${{ matrix.os.ARCH }}${{ matrix.tls.SUFFIX }}.AppImage" 94 | 95 | # Change --executable path to be relative to CWD 96 | ./linuxdeploy --appdir AppDir --executable AppDir/usr/bin/bob --desktop-file AppDir/bob.desktop --icon-file AppDir/usr/share/icons/hicolor/256x256/apps/bob.png --output appimage 97 | 98 | 99 | - name: Setup Bob build directory 100 | run: | 101 | mkdir build 102 | copy .\\bin\\vcruntime140.dll .\\build 103 | copy .\\target\\optimized\\bob.exe .\\build 104 | if: matrix.os.OS == 'windows-latest' 105 | - name: Upload Bob binary 106 | uses: actions/upload-artifact@v4 107 | with: 108 | name: "bob-${{ matrix.os.NAME }}-${{ matrix.os.ARCH }}${{ matrix.tls.SUFFIX }}" 109 | path: ${{ matrix.os.PATH }} 110 | if-no-files-found: error 111 | - name: Upload Bob AppImage 112 | if: matrix.os.NAME == 'linux' && matrix.tls.NAME == 'Rustls' 113 | uses: actions/upload-artifact@v4 114 | with: 115 | name: "bob-${{ matrix.os.NAME }}-${{ matrix.os.ARCH }}${{ matrix.tls.SUFFIX }}-appimage" 116 | path: "bob-${{ matrix.os.ARCH }}${{ matrix.tls.SUFFIX }}.AppImage*" 117 | if-no-files-found: error 118 | retention-days: 7 119 | 120 | github-release: 121 | needs: [build] 122 | runs-on: ubuntu-latest 123 | steps: 124 | - name: Checkout 125 | uses: actions/checkout@v3 126 | with: 127 | fetch-depth: 0 128 | - name: Download artifacts 129 | uses: actions/download-artifact@v4 130 | with: 131 | path: artifacts 132 | - name: Prepare Release Assets (Zip binaries, keep AppImages separate) 133 | run: | 134 | cd artifacts 135 | # Zip directories (binaries) 136 | find . -mindepth 1 -maxdepth 1 -type d -print0 | while IFS= read -r -d $'\0' dir; do 137 | base=$(basename "$dir") 138 | zip -r "${base}.zip" "$dir" 139 | rm -r "$dir" # Remove original directory after zipping 140 | done 141 | # Move AppImages and zsync files out of subdirectories if they exist 142 | find . -mindepth 2 -name '*.AppImage*' -exec mv {} . \; 143 | # Clean up any remaining empty directories from AppImage artifacts 144 | find . -mindepth 1 -maxdepth 1 -type d -empty -delete 145 | echo "Prepared assets:" 146 | ls -l 147 | - name: Release 148 | uses: softprops/action-gh-release@v1 149 | if: startsWith(github.ref, 'refs/tags/') 150 | with: 151 | generate_release_notes: true 152 | files: | 153 | ./artifacts/* 154 | 155 | publish-cargo: 156 | needs: github-release 157 | runs-on: ubuntu-latest 158 | steps: 159 | - uses: actions/checkout@v3 160 | - uses: actions-rs/toolchain@v1 161 | with: 162 | toolchain: stable 163 | override: true 164 | - uses: katyo/publish-crates@v2 165 | with: 166 | registry-token: ${{ secrets.CARGO_REGISTRY_TOKEN }} 167 | -------------------------------------------------------------------------------- /.github/workflows/ci.yml: -------------------------------------------------------------------------------- 1 | name: Continuous Integration 2 | on: [push, pull_request] 3 | 4 | jobs: 5 | check: 6 | strategy: 7 | matrix: 8 | os: [macos-latest, windows-latest, ubuntu-latest, ubuntu-24.04-arm, macos-13] 9 | runs-on: ${{matrix.os}} 10 | steps: 11 | - name: Checkout 12 | uses: actions/checkout@v4 13 | - name: Install toolchain 14 | uses: actions-rs/toolchain@v1 15 | with: 16 | toolchain: stable 17 | profile: minimal 18 | override: true 19 | - name: Check 20 | uses: actions-rs/cargo@v1 21 | with: 22 | command: check 23 | args: --locked --verbose 24 | 25 | clippy: 26 | strategy: 27 | matrix: 28 | os: [macos-latest, windows-latest, ubuntu-latest, ubuntu-24.04-arm, macos-13] 29 | runs-on: ${{matrix.os}} 30 | steps: 31 | - name: Checkout 32 | uses: actions/checkout@v4 33 | - name: Install toolchain 34 | uses: actions-rs/toolchain@v1 35 | with: 36 | toolchain: stable 37 | profile: minimal 38 | override: true 39 | - name: Check the lints 40 | uses: actions-rs/cargo@v1 41 | with: 42 | command: clippy 43 | args: --verbose -- -D warnings 44 | 45 | test: 46 | strategy: 47 | matrix: 48 | os: [macos-latest, windows-latest, ubuntu-latest, macos-13, ubuntu-24.04-arm] 49 | runs-on: ${{matrix.os}} 50 | steps: 51 | - name: Checkout 52 | uses: actions/checkout@v4 53 | - name: Install toolchain 54 | uses: actions-rs/toolchain@v1 55 | with: 56 | toolchain: stable 57 | profile: minimal 58 | override: true 59 | - name: Run the tests 60 | uses: actions-rs/cargo@v1 61 | with: 62 | command: test 63 | args: --locked --verbose 64 | 65 | formatting: 66 | runs-on: ubuntu-latest 67 | steps: 68 | - name: Checkout 69 | uses: actions/checkout@v4 70 | - name: Install toolchain 71 | uses: actions-rs/toolchain@v1 72 | with: 73 | toolchain: stable 74 | profile: minimal 75 | override: true 76 | - name: Check the formatting 77 | uses: actions-rs/cargo@v1 78 | with: 79 | command: fmt 80 | args: --all -- --check --verbose 81 | 82 | build: 83 | needs: [clippy, formatting, check, test] 84 | strategy: 85 | matrix: 86 | os: 87 | - { 88 | NAME: linux, 89 | OS: ubuntu-latest, 90 | ARCH: x86_64, 91 | PATH: target/optimized/bob, 92 | TARGET: "", 93 | } 94 | - { 95 | NAME: linux, 96 | OS: ubuntu-24.04-arm, 97 | ARCH: arm, 98 | PATH: target/optimized/bob, 99 | TARGET: "", 100 | } 101 | - { 102 | NAME: macos, 103 | OS: macos-13, 104 | ARCH: x86_64, 105 | PATH: target/optimized/bob, 106 | TARGET: "", 107 | } 108 | - { 109 | NAME: windows, 110 | OS: windows-latest, 111 | ARCH: x86_64, 112 | PATH: build, 113 | TARGET: "", 114 | } 115 | - { 116 | NAME: macos, 117 | OS: macos-latest, 118 | ARCH: arm, 119 | PATH: target/optimized/bob, 120 | TARGET: "", 121 | } 122 | tls: 123 | - { NAME: Rustls, SUFFIX: "", ARGS: "" } 124 | - { 125 | NAME: OpenSSL, 126 | SUFFIX: "-openssl", 127 | ARGS: "--no-default-features --features native-tls", 128 | } 129 | runs-on: ${{matrix.os.OS}} 130 | steps: 131 | - uses: actions/checkout@v4 132 | - name: Install Rust 133 | uses: actions-rs/toolchain@v1 134 | with: 135 | toolchain: stable 136 | profile: minimal 137 | override: true 138 | - name: Install OpenSSL libraries 139 | run: sudo apt update && sudo apt install libssl-dev 140 | if: matrix.os.OS == 'ubuntu-latest' && matrix.tls.NAME == 'OpenSSL' 141 | - uses: Swatinem/rust-cache@v1 142 | - name: Build Bob 143 | uses: actions-rs/cargo@v1 144 | with: 145 | command: build 146 | args: --locked --profile optimized ${{ matrix.tls.ARGS }} 147 | - name: Install AppImage tools 148 | if: matrix.os.NAME == 'linux' && matrix.tls.NAME == 'Rustls' 149 | run: | 150 | sudo apt update && sudo apt install -y libfuse2 # Needed by AppImage/linuxdeploy 151 | 152 | # Determine the correct architecture for linuxdeploy download 153 | DOWNLOAD_ARCH=${{ matrix.os.ARCH }} 154 | if [[ "${{ matrix.os.ARCH }}" == "arm" ]]; then 155 | DOWNLOAD_ARCH="aarch64" 156 | fi 157 | 158 | echo "Downloading linuxdeploy tools for architecture: $DOWNLOAD_ARCH" 159 | wget -c "https://github.com/linuxdeploy/linuxdeploy/releases/download/continuous/linuxdeploy-$DOWNLOAD_ARCH.AppImage" -O linuxdeploy 160 | wget -c "https://github.com/linuxdeploy/linuxdeploy-plugin-appimage/releases/download/continuous/linuxdeploy-plugin-appimage-$DOWNLOAD_ARCH.AppImage" -O linuxdeploy-plugin-appimage 161 | chmod +x linuxdeploy linuxdeploy-plugin-appimage 162 | 163 | - name: Prepare AppDir 164 | if: matrix.os.NAME == 'linux' && matrix.tls.NAME == 'Rustls' 165 | run: | 166 | mkdir -p AppDir/usr/bin AppDir/usr/share/icons/hicolor/256x256/apps AppDir/usr/share/applications 167 | cp target/optimized/bob AppDir/usr/bin/ 168 | cp resources/bob-icon.png AppDir/usr/share/icons/hicolor/256x256/apps/bob.png 169 | cat < AppDir/bob.desktop 170 | [Desktop Entry] 171 | Name=Bob Neovim Manager 172 | Exec=bob 173 | Icon=bob 174 | Type=Application 175 | Categories=Utility;Development; 176 | Comment=A cross-platform Neovim version manager 177 | EOF 178 | cp AppDir/bob.desktop AppDir/usr/share/applications/ 179 | 180 | # Verify the file exists right before linuxdeploy 181 | ls -l AppDir/usr/bin/bob 182 | 183 | export UPD_INFO="gh-releases-zsync|Matsuuu|bob|latest|bob-${{ matrix.os.ARCH }}.AppImage.zsync" 184 | export OUTPUT="bob-${{ matrix.os.ARCH }}${{ matrix.tls.SUFFIX }}.AppImage" 185 | 186 | # Change --executable path to be relative to CWD 187 | ./linuxdeploy --appdir AppDir --executable AppDir/usr/bin/bob --desktop-file AppDir/bob.desktop --icon-file AppDir/usr/share/icons/hicolor/256x256/apps/bob.png --output appimage 188 | 189 | - name: Setup Bob build directory 190 | run: | 191 | mkdir build 192 | copy .\\bin\\vcruntime140.dll .\\build 193 | copy .\\target\\optimized\\bob.exe .\\build 194 | if: matrix.os.OS == 'windows-latest' 195 | - name: Upload Bob binary 196 | uses: actions/upload-artifact@v4 197 | with: 198 | name: "bob-${{ matrix.os.NAME }}-${{ matrix.os.ARCH }}${{ matrix.tls.SUFFIX }}" 199 | path: ${{ matrix.os.PATH }} 200 | if-no-files-found: error 201 | retention-days: 7 202 | - name: Upload Bob AppImage 203 | if: matrix.os.NAME == 'linux' && matrix.tls.NAME == 'Rustls' 204 | uses: actions/upload-artifact@v4 205 | with: 206 | name: "bob-${{ matrix.os.NAME }}-${{ matrix.os.ARCH }}${{ matrix.tls.SUFFIX }}-appimage" 207 | path: "bob-${{ matrix.os.ARCH }}${{ matrix.tls.SUFFIX }}.AppImage*" 208 | if-no-files-found: error 209 | retention-days: 7 210 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | /target 2 | bob.iml 3 | report.json 4 | -------------------------------------------------------------------------------- /Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "bob-nvim" 3 | edition = "2021" 4 | version = "4.1.0" 5 | description = "A version manager for neovim" 6 | readme = "README.md" 7 | keywords = ["neovim", "version-manager"] 8 | categories = ["command-line-utilities"] 9 | license = "MIT" 10 | repository = "https://github.com/MordechaiHadad/bob" 11 | 12 | [features] 13 | default = ["rustls-tls"] 14 | native-tls = ["reqwest/default-tls"] 15 | rustls-tls = ["reqwest/rustls-tls-native-roots"] 16 | 17 | [dependencies] 18 | anyhow = "1.0.52" 19 | cfg-if = "1.0" 20 | indicatif = "0.16.2" 21 | rand = "0.8.5" 22 | serde_json = "1.0" 23 | yansi = "0.5.1" 24 | async-recursion = "1.0.2" 25 | clap_complete = "4.1" 26 | toml = "0.8.8" 27 | semver = "1.0.22" 28 | sha2 = "0.10.8" 29 | what-the-path = "^0.1.3" 30 | 31 | [dependencies.chrono] 32 | version = "0.4.23" 33 | features = ["serde"] 34 | optional = false 35 | 36 | [dependencies.clap] 37 | version = "4.0.15" 38 | features = ["derive"] 39 | optional = false 40 | 41 | [dependencies.dialoguer] 42 | version = "0.10.3" 43 | features = [] 44 | optional = false 45 | default-features = false 46 | 47 | [dependencies.futures-util] 48 | version = "0.3.14" 49 | features = [] 50 | optional = false 51 | default-features = false 52 | 53 | [dependencies.regex] 54 | version = "1.5" 55 | features = [] 56 | optional = false 57 | 58 | [dependencies.reqwest] 59 | version = "0.11" 60 | features = ["stream", "rustls-tls"] 61 | optional = false 62 | default-features = false 63 | 64 | [dependencies.serde] 65 | version = "1.0" 66 | features = ["derive"] 67 | optional = false 68 | 69 | [dependencies.tokio] 70 | version = "1.16.1" 71 | features = ["full"] 72 | optional = false 73 | 74 | [dependencies.tracing] 75 | version = "0.1" 76 | features = [] 77 | optional = false 78 | 79 | [dependencies.tracing-subscriber] 80 | version = "0.2" 81 | optional = false 82 | 83 | [target.'cfg(unix)'.dependencies.nix] 84 | version = "0.28.0" 85 | features = ["signal"] 86 | 87 | [target.'cfg(target_os = "macos")'.dependencies] 88 | flate2 = "1.0.26" 89 | tar = "0.4" 90 | 91 | [target."cfg(windows)".dependencies] 92 | winreg = "0.10.1" 93 | zip = "2.2.0" 94 | 95 | 96 | [[bin]] 97 | path = "src/main.rs" 98 | name = "bob" 99 | plugin = false 100 | proc-macro = false 101 | required-features = [] 102 | 103 | [profile.optimized] 104 | inherits = "release" 105 | opt-level = "z" 106 | strip = true 107 | lto = true 108 | codegen-units = 1 109 | -------------------------------------------------------------------------------- /Dockerfile: -------------------------------------------------------------------------------- 1 | FROM rust:latest 2 | 3 | RUN useradd -m -s /bin/bash bobuser 4 | 5 | WORKDIR /app 6 | 7 | COPY . . 8 | 9 | RUN cargo build 10 | 11 | USER bobuser 12 | 13 | RUN mkdir -p ~/.config/bob && echo '{"version_sync_file_location": "/home/bobuser/.config/nvim/nvim.version"}' > ~/.config/bob/config.json 14 | RUN mkdir -p ~/.config/nvim 15 | 16 | USER root 17 | 18 | RUN cp target/debug/bob /usr/local/bin/ 19 | 20 | USER bobuser 21 | ENV USER=bobuser 22 | 23 | CMD ["echo", "Use 'bob' to start the project"] 24 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2022 Mordechai hadad and Nethanel Menachem Eitan 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 |
2 | 3 | 4 | 5 |
6 | 7 | # Bob 8 | 9 | > Struggle to keep your Neovim versions in check? Bob provides an easy way to install and switch versions on any system! 10 | 11 | Bob is a cross-platform and easy-to-use Neovim version manager, allowing for easy switching between versions right from the command line. 12 | 13 | ## 🌟 Showcase 14 | 15 | 16 | 17 | ## 🔔 Notices 18 | 19 | - **2022-10-29**: Moved bob's symbolic link and downloads folder on macos from `/Users/user/Library/Application Support` to `~/.local/share` please make sure to move all of your downloads to the new folder, run `bob use ` and update your PATH 20 | - **2023-02-13**: Bob has recently switched to using a proxy executable for running Neovim executables. To switch from the old method that Bob used, follow these steps: 21 | 22 | 1. Remove the current Neovim path from your global $PATH environment variable. 23 | 2. Delete the following directory: 24 | On Unix: `~/.local/share/neovim` 25 | On Windows: `C:\Users\\AppData\Local\neovim` 26 | 27 | Secondly the name of the downloads directory property in the configuration file has changed. Please refer to the updated list of properties for the new name. 28 | - **2024-03-04**: Due to Neovim's recent MacOS binary changes, bob now supports arm completely, but unfortunately, it comes with some breaking changes specifically for bob's proxy executable. To fix that, follow these steps (which will not be necessary soon): 29 | 30 | 1. Remove `nvim` binary from `nvim-bin` which is located in the same directory the same as the neovim binaries downloads folder. 31 | 2. Copy your newly downloaded bob binary and put the copy inside of `nvim-bin` 32 | 3. Rename your bob binary inside `nvim-bin` to `nvim`. 33 | 34 | - **2024-05-17**: Support for `nvim-qt` is now deprecated as Neovim no longer supports it in newer releases. If you're currently using `nvim-qt`, we recommend switching to a different Neovim GUI or using Neovim in the terminal. Please refer to the Neovim documentation for more information on supported GUIs. 35 | - **2024-05-19**: Important notice for users who built Neovim from source using a commit hash before the newest Bob version: Due to recent changes in Bob, these versions will need to be rebuilt. Alternatively, you can manually add a file named `full-hash.txt` at the root of the directory. This file should contain the full hash of the commit used to build Neovim. This change ensures better tracking and management of versions built from source. We apologize for any inconvenience and appreciate your understanding. 36 | 37 | ## 📦 Requirements 38 | 39 | Make sure you don't have Neovim already installed via other ways e.g. a package manager. 40 | 41 | #### Building bob 42 | 43 | Make sure [rustup](https://www.rust-lang.org/tools/install) is installed. 44 | 45 | (Optional) `openssl` if built with `native-tls` feature. 46 | 47 | #### Building Neovim 48 | 49 | For further information refer to the [Neovim wiki](https://github.com/neovim/neovim/wiki/Building-Neovim#build-prerequisites). 50 | 51 |
52 | All platforms 53 | 54 | - CMake 55 | - Git 56 | 57 |
58 | 59 |
60 | Windows 61 | 62 | - [Visual Studio Build Tools](https://visualstudio.microsoft.com/visual-cpp-build-tools/) with C++ extension pack 63 | 64 |
65 | 66 |
67 | Unix 68 | 69 | - Clang or GCC 70 | 71 | **MacOS note**: [follow these instructions](https://github.com/neovim/neovim/wiki/Building-Neovim#macos--homebrew) 72 | 73 |
74 | 75 | ## 🔧 Installation 76 | 77 | ### Install from releases 78 | 79 | 1. Download the bob release suitable for your platform: either `bob-{platform}-x86_64.zip` for the standard version or `bob-{platform}-x86_64-openssl.zip` for the OpenSSL version. 80 | 2. Unzip it 81 | 3. Run it with `bob` 82 | 83 | ### Install with pacman 84 | 85 | 1. On Arch Linux, you can install `bob` from the [extra repository](https://archlinux.org/packages/extra/x86_64/bob/) using pacman: `pacman -S bob` 86 | 2. Run it with `bob` 87 | 88 | ### Install from source 89 | 90 | For the standard version: 91 | 92 | 1. `cargo install --git https://github.com/MordechaiHadad/bob.git` 93 | 2. Run Bob with `bob` 94 | 95 | For the OpenSSL version: 96 | 97 | 1. To install, include the `--no-default-features --features native-tls` flags with your command: `cargo install --git https://github.com/MordechaiHadad/bob.git --no-default-features --features native-tls` 98 | 2. Run Bob with `bob` 99 | 100 | ### Install from crates.io 101 | 102 | 1. `cargo install bob-nvim` 103 | 2. Run bob with `bob` 104 | 105 | ## ❓ Usage 106 | 107 | A version-string can either be `vx.x.x` or `x.x.x` examples: `v0.6.1` and `0.6.0` 108 | 109 | --- 110 | 111 | - `bob use |nightly|stable|latest|||` 112 | 113 | `--no-install` flag will prevent bob from auto invoking install command when using `use` 114 | 115 | Switch to the specified version, by default will auto-invoke install command if the version is not installed already 116 | 117 | --- 118 | 119 | - `bob run |nightly|stable|latest||| [args...]` 120 | 121 | Run a specific installed Neovim version with the provided arguments. `[args...]` are passed directly to the Neovim instance. 122 | 123 | Example: `bob run nightly --clean my_file.txt` 124 | 125 | --- 126 | 127 | - `bob install |nightly|stable|latest|||` 128 | 129 | Install the specified version, can also be used to update out-of-date nightly version. 130 | 131 | --- 132 | 133 | - `bob sync` 134 | 135 | If Config::version_sync_file_location is set, the version in that file will be parsed and installed. 136 | 137 | --- 138 | 139 | - `bob uninstall [|nightly|stable|latest|||]` 140 | 141 | Uninstall the specified version. If no version is specified a prompt is used to select all the versions 142 | to be uninstalled. 143 | 144 | --- 145 | 146 | - `bob rollback` 147 | 148 | Rollback to an existing nightly rollback 149 | 150 | --- 151 | 152 | - `bob erase` 153 | 154 | Erase any change bob ever made including Neovim installation, Neovim version downloads and registry changes. 155 | 156 | --- 157 | 158 | - `bob list` 159 | 160 | List all installed and used versions. 161 | 162 | --- 163 | 164 | - `bob complete bash|elvish|fish|powershell|zsh` 165 | 166 | Generate shell completion. 167 | 168 | --- 169 | 170 | - `bob update |nightly|stable|--all|` 171 | 172 | Update existing version, can specify either a version or the flag `--all` 173 | 174 | --- 175 | 176 | - `bob list-remote` 177 | 178 | List all remote neovim versions available for download. 179 | 180 | --- 181 | 182 | ## ⚙ Configuration 183 | 184 | This section is a bit more advanced and thus the user will have to do the work himself since bob doesn't do that. 185 | 186 | Bob's configuration file can be written in either JSON or TOML format. The file should be located at `config_dir/bob/config.json` or `config_dir/bob/config.toml` respectively. However, the location of the configuration file can be customized as explained [below](#config-location), to be more specific: 187 | 188 |
189 | On Linux 190 | 191 | `/home/user/.config/bob/config.json|toml` 192 | 193 |
194 |
195 | On Windows 196 | 197 | `C:\Users\user\AppData\Roaming\bob\config.json|toml` 198 | 199 |
200 |
201 | On MacOS 202 | 203 | `/Users/user/Library/Application Support/bob/config.json|toml` 204 | 205 |
206 | 207 | ### Increasing Github rate-limit 208 | 209 | It is possible to use `GITHUB_TOKEN` to prevent rate-limit for API calls. There are two ways to do it: 210 | 211 | - You can prepend any of the `bob` commands with `GITHUB_TOKEN=` 212 | ```console 213 | GITHUB_TOKEN= bob update -a 214 | ``` 215 | - perform `export GITHUB_TOKEN=` and then run `bob` commands. 216 | ```console 217 | export GITHUB_TOKEN= 218 | bob update -a 219 | ``` 220 | 221 | ### Custom Location 222 | 223 | Bob's config file location can be configured by using an environment variable called `$BOB_CONFIG`. 224 | Example: `export BOB_CONFIG=/path/to/config/config.json|toml` 225 | 226 | ### Syntax 227 | 228 | | Property | Description | Default Value | 229 | | -------------------------------| ---------------------------------------------------------------------------------------------------------------------------------------------------------------| --------------------------------------------------------------------------------------------------------------| 230 | | **enable_nightly_info** | Will show new commits associated with new nightly release if enabled | `true` | 231 | | **enable_release_build** | Compile neovim nightly or a certain hash version as a release build (slightly improved performance, no debug info) | `false` | 232 | | **downloads_location** | The folder in which neovim versions will be downloaded to, bob will error if this option is specified but the folder doesn't exist | unix: `/home//.local/share/bob`, windows: `C:\Users\\AppData\Local\bob` | 233 | | **installation_location** | The path in which the proxied neovim installation will be located in | unix: `/home//.local/share/bob/nvim-bin`, windows: `C:\Users\\AppData\Local\bob\nvim-bin` | 234 | | **version_sync_file_location** | The path to a file that will hold the neovim version string, useful for config version tracking, bob will error if the specified file is not a valid file path | `Disabled by default` | 235 | | **rollback_limit** | The amount of rollbacks before bob starts to delete older ones, can be up to 255 | `3` | 236 | | **github_mirror** | Specify the github mirror to use instead of `https://github.com`, example: `https://mirror.ghproxy.com` | `Disabled by default` | 237 | 238 | ### Example 239 | 240 | ```jsonc 241 | // /home/user/.config/bob/config.json 242 | { 243 | "enable_nightly_info": true, // Will show new commits associated with new nightly release if enabled 244 | "enable_release_build": false, // Compile neovim nightly or a certain hash version as a release build (slightly improved performance, no debug info) 245 | "downloads_location": "$HOME/.local/share/bob", // The folder in which neovim versions will be installed too, bob will error if this option is specified but the folder doesn't exist 246 | "installation_location": "/home/user/.local/share/bob/nvim-bin", // The path in which the used neovim version will be located in 247 | "version_sync_file_location": "/home/user/.config/nvim/nvim.version", // The path to a file that will hold the neovim version string, useful for config version tracking, bob will error if the specified file is not a valid file path 248 | "rollback_limit": 3, // The amount of rollbacks before bob starts to delete older ones, can be up to 225 249 | "github_mirror": "https://github.com" // github or github mirror 250 | } 251 | 252 | ``` 253 | 254 | ## 💻 Shell Completion 255 | 256 | - Bash 257 | 258 | Completion files are commonly stored in `/etc/bash_completion.d/` for system-wide commands, but can be stored in `~/.local/share/bash-completion/completions` for user-specific commands. Run the command: 259 | 260 | ```bash 261 | mkdir -p ~/.local/share/bash-completion/completions 262 | bob complete bash >> ~/.local/share/bash-completion/completions/bob 263 | ``` 264 | 265 | This installs the completion script. You may have to log out and log back in to your shell session for the changes to take effect. 266 | 267 | - Bash (macOS/Homebrew) 268 | 269 | Homebrew stores bash completion files within the Homebrew directory. With the `bash-completion` brew formula installed, run the command: 270 | 271 | ```bash 272 | mkdir -p $(brew --prefix)/etc/bash_completion.d 273 | bob complete bash > $(brew --prefix)/etc/bash_completion.d/bob.bash-completion 274 | ``` 275 | 276 | - Fish 277 | 278 | Fish completion files are commonly stored in `$HOME/.config/fish/completions`. Run the command: 279 | 280 | ```fish 281 | mkdir -p ~/.config/fish/completions 282 | bob complete fish > ~/.config/fish/completions/bob.fish 283 | ``` 284 | 285 | This installs the completion script. You may have to log out and log back in to your shell session for the changes to take effect. 286 | 287 | - Zsh 288 | 289 | Zsh completions are commonly stored in any directory listed in your `$fpath` variable. To use these completions, you must either add the generated script to one of those directories, or add your own to this list. 290 | 291 | Adding a custom directory is often the safest bet if you are unsure of which directory to use. First create the directory; for this example we'll create a hidden directory inside our `$HOME` directory: 292 | 293 | ```zsh 294 | mkdir ~/.zfunc 295 | ``` 296 | 297 | Then add the following lines to your `.zshrc` just before `compinit`: 298 | 299 | ```zsh 300 | fpath+=~/.zfunc 301 | ``` 302 | 303 | Now you can install the completions script using the following command: 304 | 305 | ```zsh 306 | bob complete zsh > ~/.zfunc/_bob 307 | ``` 308 | 309 | You must then either log out and log back in, or simply run 310 | 311 | ```zsh 312 | exec zsh 313 | ``` 314 | 315 | for the new completions to take effect. 316 | 317 | - PowerShell 318 | 319 | The PowerShell completion scripts require PowerShell v5.0+ (which comes with Windows 10, but can be downloaded separately for windows 7 or 8.1). 320 | 321 | First, check if a profile has already been set 322 | 323 | ```powershell 324 | Test-Path $profile 325 | ``` 326 | 327 | If the above command returns `False` run the following 328 | 329 | ```powershell 330 | New-Item -path $profile -type file -force 331 | ``` 332 | 333 | Now open the file provided by `$profile` (if you used the `New-Item` command it will be `${env:USERPROFILE}\Documents\WindowsPowerShell\Microsoft.PowerShell_profile.ps1` 334 | 335 | Next, we either save the completions file into our profile, or into a separate file and source it inside our profile. To save the completions into our profile simply use 336 | 337 | ```powershell 338 | bob complete powershell >> ${env:USERPROFILE}\Documents\WindowsPowerShell\Microsoft.PowerShell_profile.ps1 339 | ``` 340 | 341 | ## 🛠️ Troubleshooting 342 | 343 | `sudo: nvim: command not found` 344 | This error can be caused when `secure_path` is enabled in `/etc/sudoers` like in distros such as Fedora and Ubuntu. Possible workarounds: 345 | 346 | 1. Set `$VISUAL` to location of bob nvim binary and use `sudoedit` instead of `sudo nvim` when running bob as sudo 347 | 2. Run `sudo env "PATH=$PATH" nvim` 348 | 3. Set `$SUDO_USER` to location of bob nvim binary: `SUDO_EDITOR='/home/user/.local/share/bob/nvim-bin/nvim` 349 | 350 | These workarounds were devised by @nfejzic and @s11s11, thanks to them. 351 | 352 | ## :heart: Credits And Inspiration 353 | 354 | - [nvm](https://github.com/nvm-sh/nvm) A node version manager 355 | - [nvenv](https://github.com/NTBBloodbath/nvenv) A Neovim version manager written by NTBBloodbath 356 | -------------------------------------------------------------------------------- /bin/vcruntime140.dll: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/MordechaiHadad/bob/906964f89d23c779991c3f1a0c288e32a157e460/bin/vcruntime140.dll -------------------------------------------------------------------------------- /cliff.toml: -------------------------------------------------------------------------------- 1 | # git-cliff ~ default configuration file 2 | # https://git-cliff.org/docs/configuration 3 | # 4 | # Lines starting with "#" are comments. 5 | # Configuration options are organized into tables and keys. 6 | # See documentation for more information on available options. 7 | 8 | [changelog] 9 | # changelog header 10 | header = """ 11 | # Changelog\n 12 | All notable changes to this project will be documented in this file.\n 13 | """ 14 | # template for the changelog body 15 | # https://keats.github.io/tera/docs/#introduction 16 | body = """ 17 | {% if version %}\ 18 | ## [{{ version | trim_start_matches(pat="v") }}] - {{ timestamp | date(format="%Y-%m-%d") }} 19 | {% else %}\ 20 | ## [unreleased] 21 | {% endif %}\ 22 | {% for group, commits in commits | group_by(attribute="group") %} 23 | ### {{ group | upper_first }} 24 | {% for commit in commits %} 25 | - {% if commit.breaking %}[**breaking**] {% endif %}{{ commit.message | upper_first }}\ 26 | {% endfor %} 27 | {% endfor %}\n 28 | """ 29 | # remove the leading and trailing whitespace from the template 30 | trim = true 31 | # changelog footer 32 | footer = """ 33 | 34 | """ 35 | # postprocessors 36 | postprocessors = [ 37 | # { pattern = '', replace = "https://github.com/orhun/git-cliff" }, # replace repository URL 38 | ] 39 | [git] 40 | # parse the commits based on https://www.conventionalcommits.org 41 | conventional_commits = true 42 | # filter out the commits that are not conventional 43 | filter_unconventional = true 44 | # process each line of a commit as an individual commit 45 | split_commits = false 46 | # regex for preprocessing the commit messages 47 | commit_preprocessors = [ 48 | # { pattern = '\((\w+\s)?#([0-9]+)\)', replace = "([#${2}](/issues/${2}))"}, # replace issue numbers 49 | ] 50 | # regex for parsing and grouping commits 51 | commit_parsers = [ 52 | { message = "^feat", group = "Features" }, 53 | { message = "^fix", group = "Bug Fixes" }, 54 | { message = "^doc", group = "Documentation" }, 55 | { message = "^perf", group = "Performance" }, 56 | { message = "^refactor", group = "Refactor" }, 57 | { message = "^style", group = "Styling" }, 58 | { message = "^test", group = "Testing" }, 59 | { message = "^chore\\(release\\): prepare for", skip = true }, 60 | { message = "^chore\\(deps\\)", skip = true }, 61 | { message = "^chore\\(pr\\)", skip = true }, 62 | { message = "^chore\\(pull\\)", skip = true }, 63 | { message = "^chore|ci", group = "Miscellaneous Tasks" }, 64 | { body = ".*security", group = "Security" }, 65 | { message = "^revert", group = "Revert" }, 66 | ] 67 | # protect breaking changes from being skipped due to matching a skipping commit_parser 68 | protect_breaking_commits = false 69 | # filter out the commits that are not matched by commit parsers 70 | filter_commits = false 71 | # regex for matching git tags 72 | tag_pattern = "v[0-9].*" 73 | 74 | # regex for skipping tags 75 | skip_tags = "v0.1.0-beta.1" 76 | # regex for ignoring tags 77 | ignore_tags = "" 78 | # sort the tags topologically 79 | topo_order = false 80 | # sort the commits inside sections by oldest/newest order 81 | sort_commits = "oldest" 82 | # limit the number of commits included in the changelog. 83 | # limit_commits = 42 84 | -------------------------------------------------------------------------------- /env/env.fish: -------------------------------------------------------------------------------- 1 | # Credit: https://github.com/rust-lang/rustup/blob/master/src/cli/self_update/env.fish 2 | if not contains "{nvim_bin}" $PATH 3 | set -x PATH "{nvim_bin}" $PATH 4 | end -------------------------------------------------------------------------------- /env/env.sh: -------------------------------------------------------------------------------- 1 | #!/bin/sh 2 | # Credit: https://github.com/rust-lang/rustup/blob/master/src/cli/self_update/env.sh 3 | case ":${PATH}:" in 4 | *:"{nvim_bin}":*) 5 | ;; 6 | *) 7 | export PATH="{nvim_bin}:$PATH" 8 | ;; 9 | esac -------------------------------------------------------------------------------- /resources/bob-icon.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/MordechaiHadad/bob/906964f89d23c779991c3f1a0c288e32a157e460/resources/bob-icon.png -------------------------------------------------------------------------------- /resources/bob.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/MordechaiHadad/bob/906964f89d23c779991c3f1a0c288e32a157e460/resources/bob.png -------------------------------------------------------------------------------- /resources/tapes/demo.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/MordechaiHadad/bob/906964f89d23c779991c3f1a0c288e32a157e460/resources/tapes/demo.gif -------------------------------------------------------------------------------- /resources/tapes/demo.tape: -------------------------------------------------------------------------------- 1 | Output demo.gif 2 | 3 | Require bob 4 | 5 | Set FontSize 20 6 | Set Width 1300 7 | Set Height 650 8 | 9 | Type "bob install 0.8.1" 10 | Sleep 500ms 11 | Enter 12 | 13 | Sleep 5s 14 | 15 | Type "bob use 0.8.1" 16 | Sleep 500ms 17 | Enter 18 | 19 | Sleep 3s 20 | 21 | 22 | Type "bob ls" 23 | Sleep 500ms 24 | Enter 25 | 26 | Sleep 3s 27 | 28 | Type "nvim -v" 29 | Sleep 500ms 30 | Enter 31 | 32 | Sleep 3s 33 | 34 | Type "bob use nightly" 35 | Sleep 500ms 36 | Enter 37 | 38 | Sleep 5s 39 | 40 | Type "nvim -v" 41 | Sleep 500ms 42 | Enter 43 | 44 | Sleep 5s 45 | -------------------------------------------------------------------------------- /src/cli.rs: -------------------------------------------------------------------------------- 1 | use crate::{ 2 | config::ConfigFile, 3 | handlers::{ 4 | self, erase_handler, list_handler, list_remote_handler, rollback_handler, run_handler, 5 | sync_handler, uninstall_handler, update_handler, InstallResult, 6 | }, 7 | }; 8 | use anyhow::Result; 9 | use clap::{Args, CommandFactory, Parser}; 10 | use clap_complete::Shell; 11 | use reqwest::header::{HeaderMap, HeaderValue, AUTHORIZATION}; 12 | use reqwest::{Client, Error}; 13 | use tracing::info; 14 | 15 | /// Creates a new `reqwest::Client` with default headers. 16 | /// 17 | /// This function fetches the `GITHUB_TOKEN` environment variable and uses it to set the `Authorization` header for the client. 18 | /// 19 | /// # Returns 20 | /// 21 | /// This function returns a `Result` that contains a `reqwest::Client` if the client was successfully created, or an `Error` if the client could not be created. 22 | /// 23 | /// # Example 24 | /// 25 | /// ```rust 26 | /// let client = create_reqwest_client(); 27 | /// ``` 28 | /// 29 | /// # Errors 30 | /// 31 | /// This function will return an error if the `reqwest::Client` could not be built. 32 | fn create_reqwest_client() -> Result { 33 | // fetch env variable 34 | let github_token = std::env::var("GITHUB_TOKEN"); 35 | 36 | let mut headers = HeaderMap::new(); 37 | 38 | if let Ok(github_token) = github_token { 39 | headers.insert( 40 | AUTHORIZATION, 41 | HeaderValue::from_str(&format!("Bearer {}", github_token)).unwrap(), 42 | ); 43 | } 44 | 45 | let client = reqwest::Client::builder() 46 | .default_headers(headers) 47 | .build()?; 48 | 49 | Ok(client) 50 | } 51 | 52 | // The `Cli` enum represents the different commands that can be used in the command-line interface. 53 | #[derive(Debug, Parser)] 54 | #[command(version)] 55 | enum Cli { 56 | /// Switch to the specified version, by default will auto-invoke 57 | /// install command if the version is not installed already 58 | Use { 59 | /// Version to switch to |nightly|stable||| 60 | /// 61 | /// A version-string can either be `vx.x.x` or `x.x.x` examples: `v0.6.1` and `0.6.0` 62 | version: String, 63 | 64 | /// Whether not to auto-invoke install command 65 | #[arg(short, long)] 66 | no_install: bool, 67 | }, 68 | 69 | /// Install the specified version, can also be used to update 70 | /// out-of-date nightly version 71 | Install { 72 | /// Version to be installed |nightly|stable||| 73 | /// 74 | /// A version-string can either be `vx.x.x` or `x.x.x` examples: `v0.6.1` and `0.6.0` 75 | version: String, 76 | }, 77 | 78 | /// If Config::version_sync_file_location is set, the version in that file 79 | /// will be parsed and installed 80 | Sync, 81 | 82 | /// Uninstall the specified version 83 | #[clap(alias = "remove", visible_alias = "rm")] 84 | Uninstall { 85 | /// Optional Version to be uninstalled |nightly|stable||| 86 | /// 87 | /// A version-string can either be `vx.x.x` or `x.x.x` examples: `v0.6.1` and `0.6.0` 88 | /// 89 | /// If no Version is provided a prompt is used to select the versions to be uninstalled 90 | version: Option, 91 | }, 92 | 93 | /// Rollback to an existing nightly rollback 94 | Rollback, 95 | 96 | /// Erase any change bob ever made, including neovim installation, 97 | /// neovim version downloads and registry changes 98 | Erase, 99 | 100 | /// List all installed and used versions 101 | #[clap(visible_alias = "ls")] 102 | List, 103 | 104 | #[clap(visible_alias = "ls-remote")] 105 | ListRemote, 106 | 107 | /// Generate shell completion 108 | Complete { 109 | /// Shell to generate completion for 110 | shell: Shell, 111 | }, 112 | 113 | /// Update existing version |nightly|stable|--all| 114 | Update(Update), 115 | 116 | #[clap(trailing_var_arg = true)] 117 | Run { 118 | /// Optional version to run |nightly|stable||| 119 | version: String, 120 | 121 | /// Arguments to pass to Neovim (flags, files, commands, etc.) 122 | #[arg(allow_hyphen_values = true)] 123 | args: Vec, 124 | }, 125 | } 126 | 127 | /// Represents an update command in the CLI. 128 | /// 129 | /// This struct contains options for the update command, such as the version to update and whether to update all versions. 130 | /// 131 | /// # Fields 132 | /// 133 | /// * `version: Option` - The version to update. This can be either "nightly" or "stable". This field conflicts with the `all` field, meaning you can't specify a version and use `all` at the same time. 134 | /// * `all: bool` - Whether to apply the update to all versions. If this is `true`, the `version` field must be `None`. 135 | /// 136 | /// # Example 137 | /// 138 | /// ```rust 139 | /// let update = Update { 140 | /// version: Some("nightly".to_string()), 141 | /// all: false, 142 | /// }; 143 | /// ``` 144 | #[derive(Args, Debug)] 145 | pub struct Update { 146 | /// Update specified version |nightly|stable| 147 | #[arg(conflicts_with = "all")] 148 | pub version: Option, 149 | 150 | /// Apply the update to all versions 151 | #[arg(short, long)] 152 | pub all: bool, 153 | } 154 | 155 | /// Starts the CLI application. 156 | /// 157 | /// This function takes a `Config` object as input and returns a `Result`. It creates a Reqwest client, parses the CLI arguments, and then handles the arguments based on their type. 158 | /// 159 | /// # Arguments 160 | /// 161 | /// * `config: Config` - The configuration for the application. 162 | /// 163 | /// # Returns 164 | /// 165 | /// * `Result<()>` - Returns a `Result`. If the function completes successfully, it returns `Ok(())`. If an error occurs, it returns `Err`. 166 | /// 167 | /// # Example 168 | /// 169 | /// ```rust 170 | /// let config = Config::default(); 171 | /// start(config).await.unwrap(); 172 | /// ``` 173 | pub async fn start(config: ConfigFile) -> Result<()> { 174 | let client = create_reqwest_client()?; 175 | let cli = Cli::parse(); 176 | 177 | match cli { 178 | Cli::Use { 179 | version, 180 | no_install, 181 | } => { 182 | let version = super::version::parse_version_type(&client, &version).await?; 183 | 184 | handlers::use_handler::start(version, !no_install, &client, config).await?; 185 | } 186 | Cli::Install { version } => { 187 | let mut version = super::version::parse_version_type(&client, &version).await?; 188 | 189 | match handlers::install_handler::start(&mut version, &client, &config).await? { 190 | InstallResult::InstallationSuccess(location) => { 191 | info!( 192 | "{} has been successfully installed in {location}", 193 | version.tag_name 194 | ); 195 | } 196 | InstallResult::VersionAlreadyInstalled => { 197 | info!("{} is already installed", version.tag_name); 198 | } 199 | InstallResult::NightlyIsUpdated => { 200 | info!("Nightly up to date!"); 201 | } 202 | InstallResult::GivenNightlyRollback => (), 203 | } 204 | } 205 | Cli::Sync => { 206 | info!("Starting sync process"); 207 | sync_handler::start(&client, config).await?; 208 | } 209 | Cli::Uninstall { version } => { 210 | info!("Starting uninstallation process"); 211 | uninstall_handler::start(version.as_deref(), config.config).await?; 212 | } 213 | Cli::Rollback => rollback_handler::start(config.config).await?, 214 | Cli::Erase => erase_handler::start(config.config).await?, 215 | Cli::List => list_handler::start(config.config).await?, 216 | Cli::Complete { shell } => { 217 | clap_complete::generate(shell, &mut Cli::command(), "bob", &mut std::io::stdout()) 218 | } 219 | Cli::Update(data) => { 220 | update_handler::start(data, &client, config).await?; 221 | } 222 | Cli::ListRemote => list_remote_handler::start(config.config, client).await?, 223 | Cli::Run { version, args } => { 224 | run_handler::start(&version, &args, &client, &config.config).await? 225 | } 226 | } 227 | 228 | Ok(()) 229 | } 230 | -------------------------------------------------------------------------------- /src/config.rs: -------------------------------------------------------------------------------- 1 | use anyhow::Result; 2 | use regex::Regex; 3 | use serde::{Deserialize, Serialize}; 4 | use std::{env, path::PathBuf}; 5 | use tokio::{ 6 | fs::{self, File}, 7 | io::AsyncWriteExt, 8 | }; 9 | 10 | #[derive(Debug, Clone)] 11 | pub struct ConfigFile { 12 | pub path: PathBuf, 13 | pub format: ConfigFormat, 14 | pub config: Config, 15 | } 16 | 17 | impl ConfigFile { 18 | pub async fn save_to_file(&self) -> Result<()> { 19 | if let Some(parent) = self.path.parent() { 20 | tokio::fs::create_dir_all(parent).await?; 21 | } 22 | 23 | let data = match self.format { 24 | ConfigFormat::Toml => toml::to_string(&self.config)?, 25 | ConfigFormat::Json => serde_json::to_string_pretty(&self.config)?, 26 | }; 27 | 28 | let tmp_path = self.path.with_extension("tmp"); 29 | let mut file = File::create(&tmp_path).await?; 30 | file.write_all(data.as_bytes()).await?; 31 | file.flush().await?; 32 | 33 | // atomic operation i guess 34 | tokio::fs::rename(tmp_path, &self.path).await?; 35 | 36 | Ok(()) 37 | } 38 | } 39 | 40 | impl ConfigFile { 41 | pub async fn get() -> Result { 42 | let config_file = crate::helpers::directories::get_config_file()?; 43 | let mut config_format = ConfigFormat::Json; 44 | let config = match fs::read_to_string(&config_file).await { 45 | Ok(config) => { 46 | if config_file.extension().unwrap() == "toml" { 47 | let mut config: Config = toml::from_str(&config)?; 48 | handle_envars(&mut config)?; 49 | config_format = ConfigFormat::Toml; 50 | config 51 | } else { 52 | let mut config: Config = serde_json::from_str(&config)?; 53 | handle_envars(&mut config)?; 54 | config 55 | } 56 | } 57 | Err(_) => Config { 58 | enable_nightly_info: None, 59 | enable_release_build: None, 60 | downloads_location: None, 61 | installation_location: None, 62 | version_sync_file_location: None, 63 | github_mirror: None, 64 | rollback_limit: None, 65 | add_neovim_binary_to_path: None, 66 | }, 67 | }; 68 | 69 | Ok(ConfigFile { 70 | path: config_file, 71 | format: config_format, 72 | config, 73 | }) 74 | } 75 | } 76 | 77 | #[derive(Debug, Clone)] 78 | pub enum ConfigFormat { 79 | Toml, 80 | Json, 81 | } 82 | 83 | /// Represents the application configuration. 84 | /// 85 | /// This struct contains various configuration options for the application, such as whether to enable nightly info, whether to enable release build, the location for downloads, the location for installation, the location for the version sync file, the GitHub mirror to use, and the rollback limit. 86 | /// 87 | /// # Fields 88 | /// 89 | /// * `enable_nightly_info: Option` - Whether to enable nightly info. This is optional and may be `None`. 90 | /// * `enable_release_build: Option` - Whether to enable release build. This is optional and may be `None`. 91 | /// * `downloads_location: Option` - The location for downloads. This is optional and may be `None`. 92 | /// * `installation_location: Option` - The location for installation. This is optional and may be `None`. 93 | /// * `version_sync_file_location: Option` - The location for the version sync file. This is optional and may be `None`. 94 | /// * `github_mirror: Option` - The GitHub mirror to use. This is optional and may be `None`. 95 | /// * `rollback_limit: Option` - The rollback limit. This is optional and may be `None`. 96 | /// * `add_neovim_binary_to_path: Option` - Tells bob whenever to add neovim proxy path to $PATH. 97 | /// 98 | /// # Example 99 | /// 100 | /// ```rust 101 | /// let config = Config { 102 | /// enable_nightly_info: Some(true), 103 | /// enable_release_build: Some(false), 104 | /// downloads_location: Some("/path/to/downloads".to_string()), 105 | /// installation_location: Some("/path/to/installation".to_string()), 106 | /// version_sync_file_location: Some("/path/to/version_sync_file".to_string()), 107 | /// github_mirror: Some("https://github.com".to_string()), 108 | /// rollback_limit: Some(5), 109 | /// rollback_limit: Some(true), 110 | /// }; 111 | /// println!("The configuration is {:?}", config); 112 | /// ``` 113 | #[derive(Serialize, Deserialize, Debug, Clone)] 114 | pub struct Config { 115 | #[serde(skip_serializing_if = "Option::is_none")] 116 | pub enable_nightly_info: Option, 117 | #[serde(skip_serializing_if = "Option::is_none")] 118 | pub enable_release_build: Option, 119 | #[serde(skip_serializing_if = "Option::is_none")] 120 | pub downloads_location: Option, 121 | #[serde(skip_serializing_if = "Option::is_none")] 122 | pub installation_location: Option, 123 | #[serde(skip_serializing_if = "Option::is_none")] 124 | pub version_sync_file_location: Option, 125 | #[serde(skip_serializing_if = "Option::is_none")] 126 | pub github_mirror: Option, 127 | #[serde(skip_serializing_if = "Option::is_none")] 128 | pub rollback_limit: Option, 129 | #[serde(skip_serializing_if = "Option::is_none")] 130 | pub add_neovim_binary_to_path: Option, 131 | } 132 | 133 | /// Handles environment variables in the configuration. 134 | /// 135 | /// This function takes a mutable reference to a `Config` object. It creates a `Regex` to match environment variables in the format `$VARIABLE_NAME`. It then calls the `handle_envar` function for each field in the `Config` object that may contain an environment variable. 136 | /// 137 | /// # Arguments 138 | /// 139 | /// * `config: &mut Config` - A mutable reference to a `Config` object that may contain environment variables. 140 | /// 141 | /// # Returns 142 | /// 143 | /// * `Result<()>` - Returns `Ok(())` if the function completes successfully. If an error occurs, it returns `Err`. 144 | /// 145 | /// # Example 146 | /// 147 | /// ```rust 148 | /// let mut config = Config { 149 | /// downloads_location: Some("DOWNLOADS=${DOWNLOADS}".to_string()), 150 | /// github_mirror: Some("GITHUB=${GITHUB}".to_string()), 151 | /// installation_location: Some("INSTALL=${INSTALL}".to_string()), 152 | /// version_sync_file_location: Some("SYNC=${SYNC}".to_string()), 153 | /// }; 154 | /// handle_envars(&mut config).unwrap(); 155 | /// assert_eq!(config.downloads_location, Some(format!("DOWNLOADS={}", env::var("DOWNLOADS").unwrap()))); 156 | /// assert_eq!(config.github_mirror, Some(format!("GITHUB={}", env::var("GITHUB").unwrap()))); 157 | /// assert_eq!(config.installation_location, Some(format!("INSTALL={}", env::var("INSTALL").unwrap()))); 158 | /// assert_eq!(config.version_sync_file_location, Some(format!("SYNC={}", env::var("SYNC").unwrap()))); 159 | /// ``` 160 | fn handle_envars(config: &mut Config) -> Result<()> { 161 | let re = Regex::new(r"\$([A-Z_]+)").unwrap(); 162 | 163 | handle_envar(&mut config.downloads_location, &re)?; 164 | 165 | handle_envar(&mut config.github_mirror, &re)?; 166 | 167 | handle_envar(&mut config.installation_location, &re)?; 168 | 169 | handle_envar(&mut config.version_sync_file_location, &re)?; 170 | 171 | Ok(()) 172 | } 173 | 174 | /// Handles environment variables in the configuration. 175 | /// 176 | /// This function takes a mutable reference to an `Option` and a reference to a `Regex`. If the `Option` is `None`, the function returns `Ok(())`. If the `Option` is `Some(value)`, the function checks if the `value` matches the `Regex`. If it does, the function extracts the environment variable from the `value`, replaces the environment variable in the `value` with its value from the environment, and updates the `Option` with the new `value`. 177 | /// 178 | /// # Arguments 179 | /// 180 | /// * `item: &mut Option` - A mutable reference to an `Option` that may contain an environment variable. 181 | /// * `re: &Regex` - A reference to a `Regex` to match the environment variable in the `Option`. 182 | /// 183 | /// # Returns 184 | /// 185 | /// * `Result<()>` - Returns `Ok(())` if the function completes successfully. If an error occurs, it returns `Err`. 186 | /// 187 | /// # Example 188 | /// 189 | /// ```rust 190 | /// let mut item = Some("HOME=${HOME}".to_string()); 191 | /// let re = Regex::new(r"\$\{(.+?)\}").unwrap(); 192 | /// handle_envar(&mut item, &re).unwrap(); 193 | /// assert_eq!(item, Some(format!("HOME={}", env::var("HOME").unwrap()))); 194 | /// ``` 195 | fn handle_envar(item: &mut Option, re: &Regex) -> Result<()> { 196 | let value = if let Some(value) = item.as_ref() { 197 | value 198 | } else { 199 | return Ok(()); 200 | }; 201 | 202 | if re.is_match(value) { 203 | let extract = re.captures(value).unwrap().get(1).unwrap().as_str(); 204 | let var = 205 | env::var(extract).unwrap_or(format!("Couldn't find {extract} environment variable")); 206 | 207 | *item = Some(value.replace(&format!("${extract}"), &var)) 208 | } 209 | 210 | Ok(()) 211 | } 212 | -------------------------------------------------------------------------------- /src/github_requests.rs: -------------------------------------------------------------------------------- 1 | use anyhow::{anyhow, Result}; 2 | use chrono::{DateTime, Utc}; 3 | use reqwest::Client; 4 | use serde::{de::DeserializeOwned, Deserialize, Serialize}; 5 | 6 | /// Represents the version of the upstream software in the GitHub API. 7 | /// 8 | /// This struct contains the tag name of the version, the target commitish of the version, and the date and time the version was published. 9 | /// 10 | /// # Fields 11 | /// 12 | /// * `tag_name: String` - The tag name of the version. 13 | /// * `target_commitish: Option` - The target commitish of the version. This is optional and may be `None`. 14 | /// * `published_at: DateTime` - The date and time the version was published, represented as a `DateTime` object. 15 | /// 16 | /// # Example 17 | /// 18 | /// ```rust 19 | /// let upstream_version = UpstreamVersion { 20 | /// tag_name: "v1.0.0".to_string(), 21 | /// target_commitish: Some("abc123".to_string()), 22 | /// published_at: Utc::now(), 23 | /// }; 24 | /// println!("The tag name is {}", upstream_version.tag_name); 25 | /// println!("The target commitish is {}", upstream_version.target_commitish.unwrap_or_default()); 26 | /// println!("The published date and time is {}", upstream_version.published_at); 27 | /// ``` 28 | #[derive(Serialize, Deserialize, Debug, Clone)] 29 | pub struct UpstreamVersion { 30 | pub tag_name: String, 31 | pub target_commitish: Option, 32 | pub published_at: DateTime, 33 | } 34 | 35 | /// Represents a repository commit in the GitHub API. 36 | /// 37 | /// This struct contains the SHA of a commit and the commit details, as returned by the GitHub API. 38 | /// 39 | /// # Fields 40 | /// 41 | /// * `sha: String` - The SHA of the commit. 42 | /// * `commit: Commit` - The details of the commit, represented as a `Commit` object. 43 | /// 44 | /// # Example 45 | /// 46 | /// ```rust 47 | /// let commit_author = CommitAuthor { 48 | /// name: "Alice".to_string(), 49 | /// }; 50 | /// let commit = Commit { 51 | /// author: commit_author, 52 | /// message: "Initial commit".to_string(), 53 | /// }; 54 | /// let repo_commit = RepoCommit { 55 | /// sha: "abc123".to_string(), 56 | /// commit: commit, 57 | /// }; 58 | /// println!("The commit SHA is {}", repo_commit.sha); 59 | /// println!("The commit author is {}", repo_commit.commit.author.name); 60 | /// println!("The commit message is {}", repo_commit.commit.message); 61 | /// ``` 62 | #[derive(Serialize, Deserialize, Debug)] 63 | pub struct RepoCommit { 64 | pub sha: String, 65 | pub commit: Commit, 66 | } 67 | 68 | /// Represents a commit in the GitHub API. 69 | /// 70 | /// This struct contains the author of a commit and the commit message, as returned by the GitHub API. 71 | /// 72 | /// # Fields 73 | /// 74 | /// * `author: CommitAuthor` - The author of the commit, represented as a `CommitAuthor` object. 75 | /// * `message: String` - The commit message. 76 | /// 77 | /// # Example 78 | /// 79 | /// ```rust 80 | /// let commit_author = CommitAuthor { 81 | /// name: "Alice".to_string(), 82 | /// }; 83 | /// let commit = Commit { 84 | /// author: commit_author, 85 | /// message: "Initial commit".to_string(), 86 | /// }; 87 | /// println!("The commit author is {}", commit.author.name); 88 | /// println!("The commit message is {}", commit.message); 89 | /// ``` 90 | #[derive(Serialize, Deserialize, Debug)] 91 | pub struct Commit { 92 | pub author: CommitAuthor, 93 | pub message: String, 94 | } 95 | 96 | /// Represents the author of a commit in the GitHub API. 97 | /// 98 | /// This struct contains the name of the author of a commit, as returned by the GitHub API. 99 | /// 100 | /// # Fields 101 | /// 102 | /// * `name: String` - The name of the author of the commit. 103 | /// 104 | /// # Example 105 | /// 106 | /// ```rust 107 | /// let commit_author = CommitAuthor { 108 | /// name: "Alice".to_string(), 109 | /// }; 110 | /// println!("The commit author is {}", commit_author.name); 111 | /// ``` 112 | #[derive(Serialize, Deserialize, Debug)] 113 | pub struct CommitAuthor { 114 | pub name: String, 115 | } 116 | 117 | /// Represents an error response from the GitHub API. 118 | /// 119 | /// This struct contains information about an error response from the GitHub API, including the error message and the URL of the documentation related to the error. 120 | /// 121 | /// # Fields 122 | /// 123 | /// * `message: String` - The error message from the GitHub API. 124 | /// * `documentation_url: String` - The URL of the documentation related to the error. 125 | /// 126 | /// # Example 127 | /// 128 | /// ```rust 129 | /// let error_response = ErrorResponse { 130 | /// message: "Not Found".to_string(), 131 | /// documentation_url: "https://docs.github.com/rest".to_string(), 132 | /// }; 133 | /// println!("The error message is {}", error_response.message); 134 | /// println!("The documentation URL is {}", error_response.documentation_url); 135 | /// ``` 136 | #[derive(Debug, Deserialize, Serialize)] 137 | pub struct ErrorResponse { 138 | pub message: String, 139 | pub documentation_url: String, 140 | } 141 | 142 | /// Asynchronously makes a GitHub API request. 143 | /// 144 | /// This function takes a reference to a `Client` and a URL as arguments. It sets the "user-agent" header to "bob" and the "Accept" header to "application/vnd.github.v3+json". 145 | /// It then sends the request and awaits the response. It reads the response body as text and returns it as a `String`. 146 | /// 147 | /// # Arguments 148 | /// 149 | /// * `client` - A reference to a `Client` used to make the request. 150 | /// * `url` - A URL that implements `AsRef` and `reqwest::IntoUrl`. 151 | /// 152 | /// # Returns 153 | /// 154 | /// This function returns a `Result` that contains a `String` representing the response body if the operation was successful. 155 | /// If the operation failed, the function returns `Err` with a description of the error. 156 | /// 157 | /// # Example 158 | /// 159 | /// ```rust 160 | /// let client = Client::new(); 161 | /// let url = "https://api.github.com/repos/neovim/neovim/tags"; 162 | /// let response = make_github_request(&client, url).await?; 163 | /// ``` 164 | pub async fn make_github_request + reqwest::IntoUrl>( 165 | client: &Client, 166 | url: T, 167 | ) -> Result { 168 | let response = client 169 | .get(url) 170 | .header("user-agent", "bob") 171 | .header("Accept", "application/vnd.github.v3+json") 172 | .send() 173 | .await? 174 | .text() 175 | .await?; 176 | 177 | Ok(response) 178 | } 179 | 180 | pub async fn get_upstream_nightly(client: &Client) -> Result { 181 | let response = make_github_request( 182 | client, 183 | "https://api.github.com/repos/neovim/neovim/releases/tags/nightly", 184 | ) 185 | .await?; 186 | 187 | deserialize_response(response) 188 | } 189 | 190 | /// Fetches the commits for the nightly version from the GitHub API. 191 | /// 192 | /// This function sends a GET request to the GitHub API to fetch the commits for the nightly version of the software. The commits are fetched for a specified time range, from `since` to `until`. 193 | /// 194 | /// # Parameters 195 | /// 196 | /// * `client: &Client` - The HTTP client used to send the request. 197 | /// * `since: &DateTime` - The start of the time range for which to fetch the commits. 198 | /// * `until: &DateTime` - The end of the time range for which to fetch the commits. 199 | /// 200 | /// # Returns 201 | /// 202 | /// * `Result>` - A vector of `RepoCommit` objects representing the commits for the nightly version, or an error if the request failed. 203 | /// 204 | /// # Example 205 | /// 206 | /// ```rust 207 | /// let client = Client::new(); 208 | /// let since = Utc::now() - Duration::days(1); 209 | /// let until = Utc::now(); 210 | /// let result = get_commits_for_nightly(&client, &since, &until).await; 211 | /// match result { 212 | /// Ok(commits) => println!("Received {} commits", commits.len()), 213 | /// Err(e) => println!("An error occurred: {:?}", e), 214 | /// } 215 | /// ``` 216 | pub async fn get_commits_for_nightly( 217 | client: &Client, 218 | since: &DateTime, 219 | until: &DateTime, 220 | ) -> Result> { 221 | let response = make_github_request(client, format!( 222 | "https://api.github.com/repos/neovim/neovim/commits?since={since}&until={until}&per_page=100")).await?; 223 | 224 | deserialize_response(response) 225 | } 226 | 227 | /// Deserializes a JSON response from the GitHub API. 228 | /// 229 | /// This function takes a JSON response as a string and attempts to deserialize it into a specified type `T`. If the response contains a "message" field, it is treated as an error response and the function will return an error with the message from the response. If the error is related to rate limiting, a specific error message is returned. 230 | /// 231 | /// # Parameters 232 | /// 233 | /// * `response: String` - The JSON response from the GitHub API as a string. 234 | /// 235 | /// # Returns 236 | /// 237 | /// * `Result` - The deserialized response as the specified type `T`, or an error if the response could not be deserialized or contains an error message. 238 | /// 239 | /// # Errors 240 | /// 241 | /// This function will return an error if the response contains a "message" field (indicating an error from the GitHub API), or if the response could not be deserialized into the specified type `T`. 242 | /// 243 | /// # Example 244 | /// 245 | /// ```rust 246 | /// let response = "{\"data\": \"some data\"}"; 247 | /// let result: Result = deserialize_response(response); 248 | /// match result { 249 | /// Ok(data) => println!("Received data: {:?}", data), 250 | /// Err(e) => println!("An error occurred: {:?}", e), 251 | /// } 252 | /// ``` 253 | pub fn deserialize_response(response: String) -> Result { 254 | let value: serde_json::Value = serde_json::from_str(&response)?; 255 | 256 | if value.get("message").is_some() { 257 | let result: ErrorResponse = serde_json::from_value(value)?; 258 | 259 | if result.documentation_url.contains("rate-limiting") { 260 | return Err(anyhow!("Github API rate limit has been reach, either wait an hour or checkout https://github.com/MordechaiHadad/bob#increasing-github-rate-limit")); 261 | } 262 | 263 | return Err(anyhow!(result.message)); 264 | } 265 | 266 | Ok(serde_json::from_value(value)?) 267 | } 268 | -------------------------------------------------------------------------------- /src/handlers/erase_handler.rs: -------------------------------------------------------------------------------- 1 | use anyhow::{anyhow, Result}; 2 | use tokio::fs; 3 | use tracing::info; 4 | 5 | use crate::{ 6 | config::Config, 7 | helpers::directories::{self}, 8 | }; 9 | 10 | /// Starts the erase process based on the provided `Config`. 11 | /// 12 | /// # Arguments 13 | /// 14 | /// * `config: Config` - Contains the configuration settings. 15 | /// 16 | /// # Behavior 17 | /// 18 | /// The function attempts to remove the installation and downloads directories. If successful, it logs a success message. If the directories do not exist, it returns an error. 19 | /// 20 | /// On Windows, it also attempts to remove the Neovim installation path from the registry. If successful, it logs a success message. 21 | /// 22 | /// # Returns 23 | /// 24 | /// * `Result<()>` - Returns `Ok(())` if the function executes successfully, otherwise it returns an error. 25 | /// 26 | /// # Errors 27 | /// 28 | /// This function will return an error if there's a problem with removing the directories or modifying the registry. 29 | /// 30 | /// # Example 31 | /// 32 | /// ```rust 33 | /// let config = Config::default(); 34 | /// start(config).await?; 35 | /// ``` 36 | /// 37 | /// # Note 38 | /// 39 | /// This function is asynchronous and must be awaited. 40 | /// 41 | /// # See Also 42 | /// 43 | /// * [`directories::get_downloads_directory`](src/helpers/directories.rs) 44 | /// * [`directories::get_installation_directory`](src/helpers/directories.rs) 45 | pub async fn start(config: Config) -> Result<()> { 46 | let downloads = directories::get_downloads_directory(&config).await?; 47 | let mut installation_dir = directories::get_installation_directory(&config).await?; 48 | 49 | if config.installation_location.is_some() { 50 | installation_dir.push("nvim"); 51 | if fs::remove_file(&installation_dir).await.is_ok() { 52 | info!("Successfully removed neovim executable"); 53 | } 54 | } else if fs::remove_dir_all(&installation_dir).await.is_ok() { 55 | info!("Successfully removed neovim's installation folder"); 56 | } 57 | if fs::remove_dir_all(downloads).await.is_ok() { 58 | // For some weird reason this check doesn't really work for downloads folder 59 | // as it keeps thinking the folder exists and it runs with no issues even tho the folder 60 | // doesn't exist damn... 61 | info!("Successfully removed neovim downloads folder"); 62 | } else { 63 | return Err(anyhow!("There's nothing to erase")); 64 | } 65 | 66 | cfg_if::cfg_if! { 67 | if #[cfg(windows)] { 68 | use winreg::RegKey; 69 | use winreg::enums::*; 70 | 71 | let current_usr = RegKey::predef(HKEY_CURRENT_USER); 72 | let env = current_usr.open_subkey_with_flags("Environment", KEY_READ | KEY_WRITE)?; 73 | let usr_path: String = env.get_value("Path")?; 74 | if usr_path.contains("neovim") { 75 | let usr_path = usr_path.replace(&format!("{}", installation_dir.display()), ""); 76 | env.set_value("Path", &usr_path)?; 77 | 78 | info!("Successfully removed neovim's installation PATH from registry"); 79 | } 80 | } else { 81 | use what_the_path::shell::Shell; 82 | use crate::helpers::directories::get_downloads_directory; 83 | 84 | let shell = Shell::detect_by_shell_var()?; 85 | 86 | match shell { 87 | Shell::Fish(fish) => { 88 | let files = fish.get_rcfiles()?; 89 | let fish_file = files[0].join("bob.fish"); 90 | if !fish_file.exists() { return Ok(()) } 91 | fs::remove_file(fish_file).await?; 92 | }, 93 | shell => { 94 | let files = shell.get_rcfiles()?; 95 | let downloads_dir = get_downloads_directory(&config).await?; 96 | let env_path = downloads_dir.join("env/env.sh"); 97 | let source_string = format!(". \"{}\"", env_path.display()); 98 | for file in files { 99 | what_the_path::shell::remove_from_rcfile(file, &source_string)?; 100 | } 101 | } 102 | } 103 | } 104 | } 105 | 106 | Ok(()) 107 | } 108 | -------------------------------------------------------------------------------- /src/handlers/install_handler.rs: -------------------------------------------------------------------------------- 1 | use crate::config::{Config, ConfigFile}; 2 | use crate::github_requests::{get_commits_for_nightly, get_upstream_nightly, UpstreamVersion}; 3 | use crate::helpers::checksum::sha256cmp; 4 | use crate::helpers::processes::handle_subprocess; 5 | use crate::helpers::version::nightly::produce_nightly_vec; 6 | use crate::helpers::version::types::{LocalVersion, ParsedVersion, VersionType}; 7 | use crate::helpers::{self, directories, filesystem, unarchive}; 8 | use anyhow::{anyhow, Result}; 9 | use futures_util::stream::StreamExt; 10 | use indicatif::{ProgressBar, ProgressStyle}; 11 | use reqwest::Client; 12 | use semver::Version; 13 | use std::cmp::min; 14 | use std::env; 15 | use std::path::Path; 16 | use std::process::Stdio; 17 | use tokio::fs::File; 18 | use tokio::io::AsyncWriteExt; 19 | use tokio::{fs, process::Command}; 20 | use tracing::info; 21 | use yansi::Paint; 22 | 23 | use super::{InstallResult, PostDownloadVersionType}; 24 | 25 | /// Starts the installation process for a given version. 26 | /// 27 | /// # Arguments 28 | /// 29 | /// * `version` - A mutable reference to a `ParsedVersion` object representing the version to be installed. 30 | /// * `client` - A reference to a `Client` object used for making HTTP requests. 31 | /// * `config` - A reference to a `Config` object containing the configuration settings. 32 | /// 33 | /// # Returns 34 | /// 35 | /// * `Result` - Returns a `Result` that contains an `InstallResult` enum on success, or an error on failure. 36 | /// 37 | /// # Errors 38 | /// 39 | /// This function will return an error if: 40 | /// * The version is below 0.2.2. 41 | /// * There is a problem setting the current directory. 42 | /// * There is a problem checking if the version is already installed. 43 | /// * There is a problem getting the upstream nightly version. 44 | /// * There is a problem getting the local nightly version. 45 | /// * There is a problem handling a rollback. 46 | /// * There is a problem printing commits. 47 | /// * There is a problem downloading the version. 48 | /// * There is a problem handling building from source. 49 | /// * There is a problem unarchiving the downloaded file. 50 | /// * There is a problem creating the file `nightly/bob.json`. 51 | /// 52 | /// # Panics 53 | /// 54 | /// This function does not panic. 55 | /// 56 | /// # Examples 57 | /// 58 | /// ```rust 59 | /// let mut version = ParsedVersion::new(VersionType::Normal, "1.0.0"); 60 | /// let client = Client::new(); 61 | /// let config = Config::default(); 62 | /// let result = start(&mut version, &client, &config).await; 63 | /// ``` 64 | pub async fn start( 65 | version: &mut ParsedVersion, 66 | client: &Client, 67 | config: &ConfigFile, 68 | ) -> Result { 69 | if version.version_type == VersionType::NightlyRollback { 70 | return Ok(InstallResult::GivenNightlyRollback); 71 | } 72 | 73 | if let Some(version) = &version.semver { 74 | if version <= &Version::new(0, 2, 2) { 75 | return Err(anyhow!("Versions below 0.2.2 are not supported")); 76 | } 77 | } 78 | 79 | let root = directories::get_downloads_directory(&config.config).await?; 80 | 81 | env::set_current_dir(&root)?; 82 | let root = root.as_path(); 83 | 84 | let is_version_installed = 85 | helpers::version::is_version_installed(&version.tag_name, &config.config).await?; 86 | 87 | if is_version_installed && version.version_type != VersionType::Nightly { 88 | return Ok(InstallResult::VersionAlreadyInstalled); 89 | } 90 | 91 | let nightly_version = if version.version_type == VersionType::Nightly { 92 | Some(get_upstream_nightly(client).await?) 93 | } else { 94 | None 95 | }; 96 | 97 | if is_version_installed && version.version_type == VersionType::Nightly { 98 | info!("Looking for nightly updates"); 99 | 100 | let upstream_nightly = nightly_version.as_ref().unwrap(); 101 | let local_nightly = helpers::version::nightly::get_local_nightly(&config.config).await?; 102 | 103 | if upstream_nightly.published_at == local_nightly.published_at { 104 | return Ok(InstallResult::NightlyIsUpdated); 105 | } 106 | 107 | handle_rollback(&config.config).await?; 108 | 109 | match config.config.enable_nightly_info { 110 | Some(boolean) if boolean => { 111 | print_commits(client, &local_nightly, upstream_nightly).await? 112 | } 113 | None => print_commits(client, &local_nightly, upstream_nightly).await?, 114 | _ => (), 115 | } 116 | } 117 | 118 | let downloaded_archive = match version.version_type { 119 | VersionType::Normal | VersionType::Latest => { 120 | download_version(client, version, root, &config.config, false).await 121 | } 122 | VersionType::Nightly => { 123 | if config.config.enable_release_build == Some(true) { 124 | handle_building_from_source(version, &config.config).await 125 | } else { 126 | download_version(client, version, root, &config.config, false).await 127 | } 128 | } 129 | VersionType::Hash => handle_building_from_source(version, &config.config).await, 130 | VersionType::NightlyRollback => Ok(PostDownloadVersionType::None), 131 | }?; 132 | 133 | if let PostDownloadVersionType::Standard(downloaded_archive) = downloaded_archive { 134 | if version.semver.is_some() && version.semver.as_ref().unwrap() <= &Version::new(0, 4, 4) { 135 | unarchive::start(downloaded_archive).await? 136 | } else { 137 | let downloaded_checksum = 138 | download_version(client, version, root, &config.config, true).await?; 139 | 140 | if let PostDownloadVersionType::Standard(downloaded_checksum) = downloaded_checksum { 141 | let archive_path = root.join(format!( 142 | "{}.{}", 143 | downloaded_archive.file_name, downloaded_archive.file_format 144 | )); 145 | 146 | let checksum_path = root.join(format!( 147 | "{}.{}", 148 | downloaded_checksum.file_name, downloaded_checksum.file_format 149 | )); 150 | 151 | let platform = helpers::get_platform_name_download(&version.semver); 152 | 153 | if !sha256cmp( 154 | &archive_path, 155 | &checksum_path, 156 | &format!("{}.{}", platform, downloaded_archive.file_format), 157 | )? { 158 | tokio::fs::remove_file(archive_path).await?; 159 | tokio::fs::remove_file(checksum_path).await?; 160 | return Err(anyhow!("Checksum mismatch!")); 161 | } 162 | 163 | info!("Checksum matched!"); 164 | tokio::fs::remove_file(checksum_path).await?; 165 | unarchive::start(downloaded_archive).await? 166 | } 167 | } 168 | } 169 | 170 | if let VersionType::Nightly = version.version_type { 171 | if let Some(nightly_version) = nightly_version { 172 | let nightly_string = serde_json::to_string(&nightly_version)?; 173 | 174 | let downloads_dir = root.join("nightly").join("bob.json"); 175 | let mut json_file = File::create(downloads_dir).await?; 176 | 177 | if let Err(error) = json_file.write_all(nightly_string.as_bytes()).await { 178 | return Err(anyhow!( 179 | "Failed to create file nightly/bob.json, reason: {error}" 180 | )); 181 | } 182 | } 183 | } 184 | 185 | Ok(InstallResult::InstallationSuccess( 186 | root.display().to_string(), 187 | )) 188 | } 189 | 190 | /// Asynchronously handles the rollback of the nightly version of Neovim. 191 | /// 192 | /// This function checks if the nightly version is used and if the rollback limit is not zero. 193 | /// If these conditions are met, it produces a vector of nightly versions and removes the oldest version if the vector's length is greater than or equal to the rollback limit. 194 | /// It also handles permissions for older installations of nightly on Unix platforms. 195 | /// Finally, it creates a rollback by copying the nightly directory to a new directory with the ID of the target commitish and updates the JSON file in the new directory. 196 | /// 197 | /// # Arguments 198 | /// 199 | /// * `config` - A reference to the configuration object. 200 | /// 201 | /// # Returns 202 | /// 203 | /// * `Result<()>` - Returns a `Result` that contains `()` on success, or an error on failure. 204 | /// 205 | /// # Errors 206 | /// 207 | /// This function will return an error if: 208 | /// * There is a failure in producing the vector of nightly versions. 209 | /// * There is a failure in removing the oldest version. 210 | /// * There is a failure in reading the nightly JSON file. 211 | /// * There is a failure in parsing the JSON file. 212 | /// * There is a failure in copying the nightly directory. 213 | /// * There is a failure in writing the updated JSON file. 214 | /// 215 | /// # Example 216 | /// 217 | /// ```rust 218 | /// let config = Config::default(); 219 | /// handle_rollback(&config).await?; 220 | /// ` 221 | async fn handle_rollback(config: &Config) -> Result<()> { 222 | if !helpers::version::is_version_used("nightly", config).await { 223 | return Ok(()); 224 | } 225 | 226 | let rollback_limit = config.rollback_limit.unwrap_or(3); 227 | 228 | if rollback_limit == 0 { 229 | return Ok(()); 230 | } 231 | 232 | let mut nightly_vec = produce_nightly_vec(config).await?; 233 | 234 | if nightly_vec.len() >= rollback_limit.into() { 235 | let oldest_path = nightly_vec.pop().unwrap().path; 236 | fs::remove_dir_all(oldest_path).await?; 237 | } 238 | 239 | let nightly_file = fs::read_to_string("nightly/bob.json").await?; 240 | let mut json_struct: UpstreamVersion = serde_json::from_str(&nightly_file)?; 241 | let id: String = json_struct 242 | .target_commitish 243 | .as_ref() 244 | .unwrap() 245 | .chars() 246 | .take(7) 247 | .collect(); 248 | 249 | info!("Creating rollback: nightly-{id}"); 250 | filesystem::copy_dir_async("nightly", format!("nightly-{id}")).await?; 251 | 252 | json_struct.tag_name += &format!("-{id}"); 253 | 254 | let json_file = serde_json::to_string(&json_struct)?; 255 | fs::write(format!("nightly-{id}/bob.json"), json_file).await?; 256 | 257 | Ok(()) 258 | } 259 | 260 | /// Asynchronously prints the commits between two versions of Neovim. 261 | /// 262 | /// This function fetches the commits between the published dates of the local and upstream versions of Neovim. 263 | /// It then prints each commit with the author's name in blue and the commit message. 264 | /// 265 | /// # Arguments 266 | /// 267 | /// * `client` - A reference to the HTTP client. 268 | /// * `local` - A reference to the local version of Neovim. 269 | /// * `upstream` - A reference to the upstream version of Neovim. 270 | /// 271 | /// # Returns 272 | /// 273 | /// * `Result<()>` - Returns a `Result` that contains `()` on success, or an error on failure. 274 | /// 275 | /// # Errors 276 | /// 277 | /// This function will return an error if: 278 | /// * There is a failure in fetching the commits between the published dates of the local and upstream versions. 279 | /// 280 | /// # Example 281 | /// 282 | /// ```rust 283 | /// let client = Client::new(); 284 | /// let local = UpstreamVersion::get_local_version(); 285 | /// let upstream = UpstreamVersion::get_upstream_version(&client).await?; 286 | /// print_commits(&client, &local, &upstream).await?; 287 | /// ``` 288 | async fn print_commits( 289 | client: &Client, 290 | local: &UpstreamVersion, 291 | upstream: &UpstreamVersion, 292 | ) -> Result<()> { 293 | let commits = 294 | get_commits_for_nightly(client, &local.published_at, &upstream.published_at).await?; 295 | 296 | for commit in commits { 297 | println!( 298 | "| {} {}\n", 299 | Paint::blue(commit.commit.author.name).bold(), 300 | commit.commit.message.replace('\n', "\n| ") 301 | ); 302 | } 303 | 304 | Ok(()) 305 | } 306 | 307 | /// Asynchronously downloads a specified version of Neovim. 308 | /// 309 | /// This function sends a request to download the specified version of Neovim based on the version type. 310 | /// If the version type is Normal, Nightly, or Latest, it sends a request to download the version. 311 | /// If the version type is Hash, it handles building from source. 312 | /// If the version type is NightlyRollback, it does nothing. 313 | /// 314 | /// # Arguments 315 | /// 316 | /// * `client` - A reference to the HTTP client. 317 | /// * `version` - A reference to the parsed version of Neovim to be downloaded. 318 | /// * `root` - A reference to the path where the downloaded file will be saved. 319 | /// * `config` - A reference to the configuration object. 320 | /// * `sha256sum` - A boolean indicating whether to get the sha256sum 321 | /// 322 | /// # Returns 323 | /// 324 | /// * `Result` - Returns a `Result` that contains a `PostDownloadVersionType` on success, or an error on failure. 325 | /// 326 | /// # Errors 327 | /// 328 | /// This function will return an error if: 329 | /// * There is a failure in sending the request to download the version. 330 | /// * The response status is not 200. 331 | /// * There is a failure in creating the file where the downloaded version will be saved. 332 | /// * There is a failure in writing the downloaded bytes to the file. 333 | /// 334 | /// # Example 335 | /// 336 | /// ```rust 337 | /// let client = Client::new(); 338 | /// let version = ParsedVersion::parse("0.5.0"); 339 | /// let root = Path::new("/path/to/save"); 340 | /// let config = Config::default(); 341 | /// let result = download_version(&client, &version, &root, &config).await; 342 | /// ``` 343 | async fn download_version( 344 | client: &Client, 345 | version: &ParsedVersion, 346 | root: &Path, 347 | config: &Config, 348 | get_sha256sum: bool, 349 | ) -> Result { 350 | match version.version_type { 351 | VersionType::Normal | VersionType::Nightly | VersionType::Latest => { 352 | let response = send_request(client, config, version, get_sha256sum).await; 353 | 354 | match response { 355 | Ok(response) => { 356 | if response.status() == 200 { 357 | let total_size = response.content_length().unwrap_or(0); 358 | let mut response_bytes = response.bytes_stream(); 359 | 360 | // Progress Bar Setup 361 | let pb = ProgressBar::new(total_size); 362 | pb.set_style(ProgressStyle::default_bar() 363 | .template("{msg}\n{spinner:.green} [{elapsed_precise}] [{wide_bar:.cyan/blue}] {bytes}/{total_bytes} ({bytes_per_sec}, {eta})") 364 | .progress_chars("█ ")); 365 | let dl = if get_sha256sum { "checksum" } else { "version" }; 366 | pb.set_message(format!("Downloading {dl}: {}", version.tag_name)); 367 | 368 | let file_type = helpers::get_file_type(); 369 | let file_type = if get_sha256sum { 370 | if version.version_type == VersionType::Nightly 371 | || version.semver.as_ref().unwrap() > &Version::new(0, 10, 4) 372 | { 373 | "shasum.txt".to_string() 374 | } else { 375 | format!("{file_type}.sha256sum") 376 | } 377 | } else { 378 | file_type.to_owned() 379 | }; 380 | 381 | let mut file = 382 | tokio::fs::File::create(format!("{}.{file_type}", version.tag_name)) 383 | .await?; 384 | 385 | let mut downloaded: u64 = 0; 386 | 387 | while let Some(item) = response_bytes.next().await { 388 | let chunk = item.map_err(|_| anyhow!("hello"))?; 389 | file.write_all(&chunk).await?; 390 | let new = min(downloaded + (chunk.len() as u64), total_size); 391 | downloaded = new; 392 | pb.set_position(new); 393 | } 394 | 395 | file.flush().await?; 396 | file.sync_all().await?; 397 | 398 | pb.finish_with_message(format!( 399 | "Downloaded {dl} {} to {}/{}.{file_type}", 400 | version.tag_name, 401 | root.display(), 402 | version.tag_name 403 | )); 404 | 405 | Ok(PostDownloadVersionType::Standard(LocalVersion { 406 | file_name: version.tag_name.to_owned(), 407 | file_format: file_type.to_string(), 408 | path: root.display().to_string(), 409 | semver: version.semver.clone(), 410 | })) 411 | } else { 412 | let error_text = response.text().await?; 413 | if error_text.contains("Not Found") { 414 | Err(anyhow!( 415 | "Version does not exist in Neovim releases. Please check available versions with 'bob list-remote'" 416 | )) 417 | } else { 418 | Err(anyhow!( 419 | "Please provide an existing neovim version, {}", 420 | error_text 421 | )) 422 | } 423 | } 424 | } 425 | Err(error) => Err(anyhow!(error)), 426 | } 427 | } 428 | VersionType::Hash => handle_building_from_source(version, config).await, 429 | VersionType::NightlyRollback => Ok(PostDownloadVersionType::None), 430 | } 431 | } 432 | 433 | /// Asynchronously handles the building of a specified version from source. 434 | /// 435 | /// This function checks for the presence of necessary tools (like Clang, GCC, Cmake, and Git) in the system. 436 | /// It then proceeds to create a directory named "neovim-git" if it doesn't exist, and sets the current directory to it. 437 | /// It initializes a Git repository if one doesn't exist, and sets the remote to Neovim's GitHub repository. 438 | /// It fetches the specified version from the remote repository and checks out the fetched files. 439 | /// It then builds the fetched files and installs them to a specified location. 440 | /// 441 | /// # Arguments 442 | /// 443 | /// * `version` - A reference to the parsed version of Neovim to be built. 444 | /// * `config` - A reference to the configuration object. 445 | /// 446 | /// # Returns 447 | /// 448 | /// * `Result` - Returns a `Result` that contains a `PostDownloadVersionType` on success, or an error on failure. 449 | /// 450 | /// # Errors 451 | /// 452 | /// This function will return an error if: 453 | /// * The necessary tools are not installed in the system. 454 | /// * There is a failure in creating the "neovim-git" directory. 455 | /// * There is a failure in initializing the Git repository. 456 | /// * There is a failure in setting the remote repository. 457 | /// * There is a failure in fetching the specified version from the remote repository. 458 | /// * There is a failure in checking out the fetched files. 459 | /// * There is a failure in building and installing the fetched files. 460 | /// 461 | /// # Example 462 | /// 463 | /// ```rust 464 | /// let version = ParsedVersion::parse("0.5.0"); 465 | /// let config = Config::default(); 466 | /// let result = handle_building_from_source(&version, &config).await; 467 | /// ``` 468 | async fn handle_building_from_source( 469 | version: &ParsedVersion, 470 | config: &Config, 471 | ) -> Result { 472 | cfg_if::cfg_if! { 473 | if #[cfg(windows)] { 474 | if env::var("VisualStudioVersion").is_err() { 475 | return Err(anyhow!("Please make sure you are using Developer PowerShell/Command Prompt for VS")); 476 | } 477 | 478 | } else { 479 | let is_clang_present = match Command::new("clang").output().await { 480 | Ok(_) => true, 481 | Err(error) => !matches!(error.kind(), std::io::ErrorKind::NotFound) 482 | }; 483 | let is_gcc_present = match Command::new("gcc").output().await { 484 | Ok(_) => true, 485 | Err(error) => !matches!(error.kind(), std::io::ErrorKind::NotFound) 486 | }; 487 | if !is_gcc_present && !is_clang_present { 488 | return Err(anyhow!( 489 | "Clang or GCC have to be installed in order to build neovim from source" 490 | )); 491 | } 492 | 493 | } 494 | 495 | } 496 | match Command::new("cmake").output().await { 497 | Ok(_) => (), 498 | Err(error) => { 499 | if error.kind() == std::io::ErrorKind::NotFound { 500 | return Err(anyhow!( 501 | "Cmake has to be installed in order to build neovim from source" 502 | )); 503 | } 504 | } 505 | } 506 | 507 | if let Err(error) = Command::new("git").output().await { 508 | if error.kind() == std::io::ErrorKind::NotFound { 509 | return Err(anyhow!( 510 | "Git has to be installed in order to build neovim from source" 511 | )); 512 | } 513 | } 514 | 515 | // create neovim-git if it does not exist 516 | let dirname = "neovim-git"; 517 | if let Err(error) = fs::metadata(dirname).await { 518 | match error.kind() { 519 | std::io::ErrorKind::NotFound => { 520 | fs::create_dir(dirname).await?; 521 | } 522 | _ => return Err(anyhow!("unknown error: {}", error)), 523 | } 524 | } 525 | 526 | env::set_current_dir(dirname)?; // cd into neovim-git 527 | 528 | // check if repo is initialized 529 | if let Err(error) = fs::metadata(".git").await { 530 | match error.kind() { 531 | std::io::ErrorKind::NotFound => { 532 | Command::new("git") 533 | .arg("init") 534 | .stdout(Stdio::null()) 535 | .spawn()? 536 | .wait() 537 | .await?; 538 | } 539 | 540 | _ => return Err(anyhow!("unknown error: {}", error)), 541 | } 542 | }; 543 | 544 | // check if repo has a remote 545 | let remote = Command::new("git") 546 | .arg("remote") 547 | .arg("get-url") 548 | .arg("origin") 549 | .stdout(Stdio::null()) 550 | .spawn()? 551 | .wait() 552 | .await?; 553 | 554 | if !remote.success() { 555 | // add neovim's remote 556 | Command::new("git") 557 | .arg("remote") 558 | .arg("add") 559 | .arg("origin") 560 | .arg("https://github.com/neovim/neovim.git") 561 | .spawn()? 562 | .wait() 563 | .await?; 564 | } else { 565 | // set neovim's remote otherwise 566 | Command::new("git") 567 | .arg("remote") 568 | .arg("set-url") 569 | .arg("origin") 570 | .arg("https://github.com/neovim/neovim.git") 571 | .spawn()? 572 | .wait() 573 | .await?; 574 | }; 575 | // fetch version from origin 576 | let fetch_successful = Command::new("git") 577 | .arg("fetch") 578 | .arg("--depth") 579 | .arg("1") 580 | .arg("origin") 581 | .arg(&version.non_parsed_string) 582 | .spawn()? 583 | .wait() 584 | .await? 585 | .success(); 586 | 587 | if !fetch_successful { 588 | return Err(anyhow!( 589 | "fetching remote failed, try providing the full commit hash" 590 | )); 591 | } 592 | 593 | // checkout fetched files 594 | Command::new("git") 595 | .arg("checkout") 596 | .arg("FETCH_HEAD") 597 | .stdout(Stdio::null()) 598 | .spawn()? 599 | .wait() 600 | .await?; 601 | 602 | if fs::metadata("build").await.is_ok() { 603 | filesystem::remove_dir("build").await?; 604 | } 605 | fs::create_dir("build").await?; 606 | 607 | let downloads_location = directories::get_downloads_directory(config).await?; 608 | let folder_name = downloads_location.join(&version.tag_name[0..7]); 609 | 610 | let build_type = match config.enable_release_build { 611 | Some(true) => "Release", 612 | _ => "RelWithDebInfo", 613 | }; 614 | 615 | let build_arg = format!("CMAKE_BUILD_TYPE={}", build_type); 616 | 617 | cfg_if::cfg_if! { 618 | if #[cfg(windows)] { 619 | if fs::metadata(".deps").await.is_ok() { 620 | helpers::filesystem::remove_dir(".deps").await?; 621 | } 622 | handle_subprocess(Command::new("cmake").arg("-S").arg("cmake.deps").arg("-B").arg(".deps").arg("-D").arg(&build_arg)).await?; 623 | handle_subprocess(Command::new("cmake").arg("--build").arg(".deps").arg("--config").arg(build_type)).await?; 624 | handle_subprocess(Command::new("cmake").arg("-B").arg("build").arg("-D").arg(&build_arg)).await?; 625 | handle_subprocess(Command::new("cmake").arg("--build").arg("build").arg("--config").arg(build_type)).await?; 626 | handle_subprocess(Command::new("cmake").arg("--install").arg("build").arg("--prefix").arg(&folder_name)).await?; 627 | } else { 628 | let location_arg = format!( 629 | "CMAKE_INSTALL_PREFIX={}", 630 | folder_name.to_string_lossy() 631 | ); 632 | 633 | handle_subprocess(Command::new("make").arg(&location_arg).arg(&build_arg)).await?; 634 | handle_subprocess(Command::new("make").arg("install")).await?; 635 | } 636 | } 637 | 638 | let mut file = File::create(folder_name.join("full-hash.txt")).await?; 639 | file.write_all(version.non_parsed_string.as_bytes()).await?; 640 | 641 | Ok(PostDownloadVersionType::Hash) 642 | } 643 | 644 | /// Sends a GET request to the specified URL to download a specific version of Neovim. 645 | /// 646 | /// # Arguments 647 | /// 648 | /// * `client: &Client` - A reference to the `Client` used for making requests. 649 | /// * `config: &Config` - Contains the configuration settings. 650 | /// * `version: &ParsedVersion` - Contains the version information to be downloaded. 651 | /// * `get_sha256sum: bool` - A boolean indicating whether to get the sha256sum. 652 | /// 653 | /// # Behavior 654 | /// 655 | /// The function constructs the download URL based on the provided `version` and `config.github_mirror`. If `config.github_mirror` is `None`, it defaults to "https://github.com". 656 | /// 657 | /// It then sends a GET request to the constructed URL with the header "user-agent" set to "bob". 658 | /// 659 | /// # Returns 660 | /// 661 | /// * `Result` - Returns a `Result` containing the server's response to the GET request. If the request fails, it returns an error. 662 | /// 663 | /// # Example 664 | /// 665 | /// ```rust 666 | /// let client = Client::new(); 667 | /// let config = Config::default(); 668 | /// let version = ParsedVersion { tag_name: "v0.2.2", semver: Version::parse("0.2.2").unwrap() }; 669 | /// let response = send_request(&client, &config, &version, false).await?; 670 | /// ``` 671 | /// 672 | /// # Note 673 | /// 674 | /// This function is asynchronous and must be awaited. 675 | /// 676 | /// # See Also 677 | /// 678 | /// * [`helpers::get_platform_name_download`](src/helpers/platform.rs) 679 | /// * [`helpers::get_file_type`](src/helpers/file.rs) 680 | async fn send_request( 681 | client: &Client, 682 | config: &Config, 683 | version: &ParsedVersion, 684 | get_sha256sum: bool, 685 | ) -> Result { 686 | let platform = helpers::get_platform_name_download(&version.semver); 687 | let file_type = helpers::get_file_type(); 688 | 689 | let url = match &config.github_mirror { 690 | Some(val) => val.to_string(), 691 | None => "https://github.com".to_string(), 692 | }; 693 | let version_tag = &version.tag_name; 694 | let request_url = if get_sha256sum { 695 | if version.version_type == VersionType::Nightly 696 | || version.semver.as_ref().unwrap() > &Version::new(0, 10, 4) 697 | { 698 | format!("{url}/neovim/neovim/releases/download/{version_tag}/shasum.txt") 699 | } else { 700 | format!( 701 | "{url}/neovim/neovim/releases/download/{version_tag}/{platform}.{file_type}.sha256sum" 702 | ) 703 | } 704 | } else { 705 | format!("{url}/neovim/neovim/releases/download/{version_tag}/{platform}.{file_type}") 706 | }; 707 | 708 | client 709 | .get(request_url) 710 | .header("user-agent", "bob") 711 | .send() 712 | .await 713 | } 714 | -------------------------------------------------------------------------------- /src/handlers/list_handler.rs: -------------------------------------------------------------------------------- 1 | use anyhow::{anyhow, Result}; 2 | use regex::Regex; 3 | use std::{fs, path::PathBuf}; 4 | use yansi::Paint; 5 | 6 | use crate::{ 7 | config::Config, 8 | helpers::{self, directories, version::nightly::produce_nightly_vec}, 9 | }; 10 | 11 | /// Starts the list handler. 12 | /// 13 | /// This function reads the downloads directory and lists all the installed versions in a formatted table. It also checks if a version is currently in use. 14 | /// 15 | /// # Arguments 16 | /// 17 | /// * `config` - The configuration object. 18 | /// 19 | /// # Returns 20 | /// 21 | /// * `Result<()>` - Returns `Ok(())` if the operation is successful, or an error if there are no versions installed or if there is a failure in reading the directory or checking if a version is in use. 22 | /// 23 | /// # Example 24 | /// 25 | /// ```rust 26 | /// let config = Config::default(); 27 | /// let result = start(config).await; 28 | /// assert!(result.is_ok()); 29 | /// ``` 30 | pub async fn start(config: Config) -> Result<()> { 31 | let downloads_dir = directories::get_downloads_directory(&config).await?; 32 | 33 | let paths: Vec = fs::read_dir(downloads_dir)? 34 | .filter_map(Result::ok) 35 | .map(|entry| entry.path()) 36 | .collect(); 37 | 38 | if paths.is_empty() { 39 | return Err(anyhow!("There are no versions installed")); 40 | } 41 | 42 | let version_max_len = if has_rollbacks(&config).await? { 16 } else { 7 }; 43 | let status_max_len = 9; 44 | let padding = 2; 45 | 46 | println!( 47 | "┌{}┬{}┐", 48 | "─".repeat(version_max_len + (padding * 2)), 49 | "─".repeat(status_max_len + (padding * 2)) 50 | ); 51 | println!( 52 | "│{}Version{}│{}Status{}│", 53 | " ".repeat(padding), 54 | " ".repeat(padding + (version_max_len - 7)), 55 | " ".repeat(padding), 56 | " ".repeat(padding + (status_max_len - 6)) 57 | ); 58 | println!( 59 | "├{}┼{}┤", 60 | "─".repeat(version_max_len + (padding * 2)), 61 | "─".repeat(status_max_len + (padding * 2)) 62 | ); 63 | 64 | for path in paths { 65 | if !path.is_dir() { 66 | continue; 67 | } 68 | 69 | let path_name = path.file_name().unwrap().to_str().unwrap(); 70 | 71 | if !is_version(path_name) { 72 | continue; 73 | } 74 | 75 | let version_pr = (version_max_len - path_name.len()) + padding; 76 | let status_pr = padding + status_max_len; 77 | 78 | if helpers::version::is_version_used(path_name, &config).await { 79 | println!( 80 | "│{}{path_name}{}│{}{}{}│", 81 | " ".repeat(padding), 82 | " ".repeat(version_pr), 83 | " ".repeat(padding), 84 | Paint::green("Used"), 85 | " ".repeat(status_pr - 4) 86 | ); 87 | } else { 88 | println!( 89 | "│{}{path_name}{}│{}{}{}│", 90 | " ".repeat(padding), 91 | " ".repeat(version_pr), 92 | " ".repeat(padding), 93 | Paint::yellow("Installed"), 94 | " ".repeat(status_pr - 9) 95 | ); 96 | } 97 | } 98 | 99 | println!( 100 | "└{}┴{}┘", 101 | "─".repeat(version_max_len + (padding * 2)), 102 | "─".repeat(status_max_len + (padding * 2)) 103 | ); 104 | 105 | Ok(()) 106 | } 107 | 108 | /// Checks if there are any rollbacks available. 109 | /// 110 | /// This function produces a vector of nightly versions and checks if it is empty. 111 | /// 112 | /// # Arguments 113 | /// 114 | /// * `config` - A reference to the configuration object. 115 | /// 116 | /// # Returns 117 | /// 118 | /// * `Result` - Returns a `Result` that contains `true` if there are rollbacks available, or `false` otherwise. Returns an error if there is a failure in producing the vector of nightly versions. 119 | /// 120 | /// # Example 121 | /// 122 | /// ```rust 123 | /// let config = Config::default(); 124 | /// let has_rollbacks = has_rollbacks(&config).await?; 125 | /// assert_eq!(has_rollbacks, true); 126 | /// ``` 127 | async fn has_rollbacks(config: &Config) -> Result { 128 | let list = produce_nightly_vec(config).await?; 129 | 130 | Ok(!list.is_empty()) 131 | } 132 | 133 | /// Checks if a given string is a valid version. 134 | /// 135 | /// This function checks if the given string is "stable", contains "nightly", or matches the version or hash regex. 136 | /// 137 | /// # Arguments 138 | /// 139 | /// * `name` - A reference to a string that could be a version. 140 | /// 141 | /// # Returns 142 | /// 143 | /// * `bool` - Returns `true` if the string is a valid version, `false` otherwise. 144 | /// 145 | /// # Example 146 | /// 147 | /// ```rust 148 | /// let version = "v1.0.0"; 149 | /// let is_version = is_version(version); 150 | /// assert_eq!(is_version, true); 151 | /// ``` 152 | fn is_version(name: &str) -> bool { 153 | match name { 154 | "stable" => true, 155 | nightly_name if nightly_name.contains("nightly") => true, 156 | name => { 157 | let version_regex = Regex::new(r"^v?[0-9]+\.[0-9]+\.[0-9]+$").unwrap(); 158 | let hash_regex = Regex::new(r"\b[0-9a-f]{5,40}\b").unwrap(); 159 | 160 | if version_regex.is_match(name) { 161 | return true; 162 | } 163 | 164 | hash_regex.is_match(name) 165 | } 166 | } 167 | } 168 | -------------------------------------------------------------------------------- /src/handlers/list_remote_handler.rs: -------------------------------------------------------------------------------- 1 | use std::{fs, path::PathBuf}; 2 | 3 | use anyhow::Result; 4 | use reqwest::Client; 5 | use serde::{Deserialize, Serialize}; 6 | use yansi::Paint; 7 | 8 | use crate::{ 9 | config::Config, 10 | github_requests::{deserialize_response, make_github_request}, 11 | helpers::{self, directories, version::search_stable_version}, 12 | }; 13 | 14 | /// Asynchronously starts the process of listing remote versions of Neovim. 15 | /// 16 | /// This function takes a `Config` and a `Client` as arguments. It first gets the downloads directory path by calling the `get_downloads_directory` function. 17 | /// It then makes a GitHub API request to get the tags of the Neovim repository, which represent the versions of Neovim. 18 | /// The function then reads the downloads directory and filters the entries that contain 'v' in their names, which represent the local versions of Neovim. 19 | /// It deserializes the response from the GitHub API request into a vector of `RemoteVersion`. 20 | /// It filters the versions that start with 'v' and then iterates over the filtered versions. 21 | /// For each version, it checks if it is installed locally and if it is the stable version. 22 | /// It then prints the version name in green if it is being used, in yellow if it is installed but not being used, and in default color if it is not installed. 23 | /// It also appends ' (stable)' to the version name if it is the stable version. 24 | /// 25 | /// # Arguments 26 | /// 27 | /// * `config` - A `Config` containing the application configuration. 28 | /// * `client` - A `Client` used to make the GitHub API request. 29 | /// 30 | /// # Returns 31 | /// 32 | /// This function returns a `Result` that contains `()` if the operation was successful. 33 | /// If the operation failed, the function returns `Err` with a description of the error. 34 | /// 35 | /// # Example 36 | /// 37 | /// ```rust 38 | /// let config = Config::default(); 39 | /// let client = Client::new(); 40 | /// start(config, client).await?; 41 | /// ``` 42 | pub async fn start(config: Config, client: Client) -> Result<()> { 43 | let downloads_dir = directories::get_downloads_directory(&config).await?; 44 | let response = make_github_request( 45 | &client, 46 | "https://api.github.com/repos/neovim/neovim/tags?per_page=50", 47 | ) 48 | .await?; 49 | 50 | let mut local_versions: Vec = fs::read_dir(downloads_dir)? 51 | .filter_map(Result::ok) 52 | .filter(|entry| { 53 | entry 54 | .path() 55 | .file_name() 56 | .unwrap() 57 | .to_str() 58 | .unwrap() 59 | .contains('v') 60 | }) 61 | .map(|entry| entry.path()) 62 | .collect(); 63 | 64 | let versions: Vec = deserialize_response(response)?; 65 | let filtered_versions: Vec = versions 66 | .into_iter() 67 | .filter(|v| v.name.starts_with('v')) 68 | .collect(); 69 | 70 | let stable_version = search_stable_version(&client).await?; 71 | let padding = " ".repeat(12); 72 | 73 | for version in filtered_versions { 74 | let version_installed = local_versions.iter().any(|v| { 75 | v.file_name() 76 | .and_then(|str| str.to_str()) 77 | .is_some_and(|str| str.contains(&version.name)) 78 | }); 79 | 80 | let stable_version_string = if stable_version == version.name { 81 | " (stable)" 82 | } else { 83 | "" 84 | }; 85 | 86 | if helpers::version::is_version_used(&version.name, &config).await { 87 | println!( 88 | "{padding}{}{}", 89 | Paint::green(version.name), 90 | stable_version_string 91 | ); 92 | } else if version_installed { 93 | println!( 94 | "{padding}{}{}", 95 | Paint::yellow(&version.name), 96 | stable_version_string 97 | ); 98 | 99 | local_versions.retain(|v| { 100 | v.file_name() 101 | .and_then(|str| str.to_str()) 102 | .is_none_or(|str| !str.contains(&version.name)) 103 | }); 104 | } else { 105 | println!("{padding}{}{}", version.name, stable_version_string); 106 | } 107 | } 108 | 109 | Ok(()) 110 | } 111 | 112 | /// Represents a remote version of Neovim. 113 | /// 114 | /// This struct is used to deserialize the response from the GitHub API request that gets the tags of the Neovim repository. 115 | /// Each tag represents a version of Neovim, and the `name` field of the `RemoteVersion` struct represents the name of the version. 116 | /// 117 | /// # Fields 118 | /// 119 | /// * `name` - A `String` that represents the name of the version. 120 | /// 121 | /// # Example 122 | /// 123 | /// ```rust 124 | /// let remote_version = RemoteVersion { 125 | /// name: "v0.5.0".to_string(), 126 | /// }; 127 | /// ``` 128 | #[derive(Serialize, Deserialize, Debug, PartialEq, Eq, Clone)] 129 | struct RemoteVersion { 130 | pub name: String, 131 | } 132 | -------------------------------------------------------------------------------- /src/handlers/mod.rs: -------------------------------------------------------------------------------- 1 | pub mod erase_handler; 2 | pub mod install_handler; 3 | pub mod list_handler; 4 | pub mod list_remote_handler; 5 | pub mod rollback_handler; 6 | pub mod run_handler; 7 | pub mod sync_handler; 8 | pub mod uninstall_handler; 9 | pub mod update_handler; 10 | pub mod use_handler; 11 | 12 | use super::version::types::LocalVersion; 13 | 14 | /// Represents the result of an installation attempt. 15 | /// 16 | /// This enum has four variants: 17 | /// 18 | /// * `InstallationSuccess(String)` - The installation was successful. 19 | /// * `VersionAlreadyInstalled` - The version that was attempted to be installed is already installed. 20 | /// * `NightlyIsUpdated` - The nightly version is updated. 21 | /// * `GivenNightlyRollback` - The given nightly version is a rollback. 22 | pub enum InstallResult { 23 | InstallationSuccess(String), 24 | VersionAlreadyInstalled, 25 | NightlyIsUpdated, 26 | GivenNightlyRollback, 27 | } 28 | 29 | /// Represents the type of a version after it has been downloaded. 30 | /// 31 | /// This enum has three variants: 32 | /// 33 | /// * `None` - No specific version type is assigned. 34 | /// * `Standard(LocalVersion)` - The version is a standard version. The `LocalVersion` contains the details of the version. 35 | /// * `Hash` - The version is identified by a hash. 36 | #[derive(PartialEq)] 37 | pub enum PostDownloadVersionType { 38 | None, 39 | Standard(LocalVersion), 40 | Hash, 41 | } 42 | -------------------------------------------------------------------------------- /src/handlers/rollback_handler.rs: -------------------------------------------------------------------------------- 1 | use crate::helpers::version::nightly::produce_nightly_vec; 2 | use anyhow::Result; 3 | use chrono::{Duration, Utc}; 4 | use dialoguer::{console::Term, theme::ColorfulTheme, Select}; 5 | use tracing::info; 6 | 7 | use crate::{ 8 | config::Config, 9 | handlers::use_handler, 10 | helpers::{self, version::types::ParsedVersion}, 11 | }; 12 | 13 | /// Starts the rollback process. 14 | /// 15 | /// This function presents a list of available versions to the user, allows them to select a version to rollback to, and then performs the rollback. 16 | /// 17 | /// # Arguments 18 | /// 19 | /// * `config` - The configuration for the rollback process. 20 | /// 21 | /// # Returns 22 | /// 23 | /// * `Result<()>` - Returns a `Result` that indicates whether the rollback process was successful or not. 24 | /// 25 | /// # Example 26 | /// 27 | /// ```rust 28 | /// let config = Config::default(); 29 | /// start(config).await.unwrap(); 30 | /// ``` 31 | pub async fn start(config: Config) -> Result<()> { 32 | let nightly_vec = produce_nightly_vec(&config).await?; 33 | 34 | let mut name_list: Vec = Vec::new(); 35 | 36 | for entry in &nightly_vec { 37 | name_list.push( 38 | entry 39 | .path 40 | .file_name() 41 | .unwrap() 42 | .to_os_string() 43 | .into_string() 44 | .unwrap(), 45 | ); 46 | } 47 | 48 | let selection = Select::with_theme(&ColorfulTheme::default()) 49 | .with_prompt("Choose which rollback to use (Newest to Oldest):\n") 50 | .items(&name_list) 51 | .default(0) 52 | .interact_on_opt(&Term::stderr())?; 53 | 54 | match selection { 55 | Some(i) => { 56 | let is_version_used = helpers::version::is_version_used(&name_list[i], &config).await; 57 | 58 | if is_version_used { 59 | info!("{} is already used.", &name_list[i]); 60 | return Ok(()); 61 | } 62 | 63 | use_handler::switch( 64 | &config, 65 | &ParsedVersion { 66 | tag_name: name_list[i].clone(), 67 | version_type: crate::helpers::version::types::VersionType::Normal, 68 | non_parsed_string: "".to_string(), 69 | semver: None, 70 | }, 71 | ) 72 | .await?; 73 | 74 | let find = nightly_vec 75 | .iter() 76 | .find(|item| { 77 | item.path 78 | .file_name() 79 | .unwrap() 80 | .to_os_string() 81 | .into_string() 82 | .unwrap() 83 | .contains(&name_list[i]) 84 | }) 85 | .unwrap(); 86 | 87 | let now = Utc::now(); 88 | let since = now.signed_duration_since(find.data.published_at); 89 | let humanized = humanize_duration(since)?; 90 | info!( 91 | "Successfully rolled back to version '{}' from {} ago", 92 | name_list[i], humanized 93 | ); 94 | } 95 | None => info!("Rollback aborted..."), 96 | } 97 | 98 | Ok(()) 99 | } 100 | 101 | /// Converts a `Duration` into a human-readable string. 102 | /// 103 | /// This function takes a `Duration` and converts it into a string that represents the duration in weeks, days, and hours. 104 | /// 105 | /// # Arguments 106 | /// 107 | /// * `duration` - The `Duration` to be converted. 108 | /// 109 | /// # Returns 110 | /// 111 | /// * `Result` - Returns a `Result` that contains a string representing the duration in a human-readable format, or an error if there is a failure in the conversion process. 112 | /// 113 | /// # Example 114 | /// 115 | /// ```rust 116 | /// let duration = Duration::hours(25); 117 | /// let humanized_duration = humanize_duration(duration).unwrap(); 118 | /// assert_eq!(humanized_duration, "1 day, 1 hour"); 119 | /// ``` 120 | fn humanize_duration(duration: Duration) -> Result { 121 | let mut humanized_duration = String::new(); 122 | 123 | let total_hours = duration.num_hours(); 124 | 125 | let weeks = total_hours / 24 / 7; 126 | let days = (total_hours / 24) % 7; 127 | let hours = total_hours % 24; 128 | 129 | let mut added_duration = false; 130 | 131 | if weeks != 0 { 132 | if added_duration { 133 | humanized_duration += ", "; 134 | } 135 | humanized_duration += &format!("{} week{}", weeks, if weeks > 1 { "s" } else { "" }); 136 | added_duration = true; 137 | } 138 | if days != 0 { 139 | if added_duration { 140 | humanized_duration += ", "; 141 | } 142 | if !humanized_duration.is_empty() { 143 | humanized_duration += " "; 144 | } 145 | humanized_duration += &format!("{} day{}", days, if days > 1 { "s" } else { "" }); 146 | added_duration = true; 147 | } 148 | if hours != 0 { 149 | if added_duration { 150 | humanized_duration += " and"; 151 | } 152 | if !humanized_duration.is_empty() { 153 | humanized_duration += " "; 154 | } 155 | humanized_duration += &format!("{} hour{}", hours, if hours > 1 { "s" } else { "" }); 156 | } 157 | 158 | Ok(humanized_duration) 159 | } 160 | -------------------------------------------------------------------------------- /src/handlers/run_handler.rs: -------------------------------------------------------------------------------- 1 | use anyhow::Result; 2 | use reqwest::Client; 3 | use tokio::process::Command; 4 | 5 | use crate::config::Config; 6 | use crate::helpers; 7 | 8 | /// Starts the process of running a specific version of Neovim with the provided arguments. 9 | /// 10 | /// This function parses the specified version, checks if it's installed, 11 | /// and runs the Neovim binary from that version with the provided arguments. 12 | /// 13 | /// # Arguments 14 | /// 15 | /// * `version` - The version to run (nightly|stable||) 16 | /// * `args` - Arguments to pass to Neovim (flags, files, commands, etc.) 17 | /// * `client` - The client to use for HTTP requests (needed for version parsing) 18 | /// * `config` - The configuration for the operation 19 | /// 20 | /// # Returns 21 | /// 22 | /// * `Result<()>` - Returns a `Result` that indicates whether the operation was successful or not. 23 | pub async fn start(version: &str, args: &[String], client: &Client, config: &Config) -> Result<()> { 24 | // Parse the specified version 25 | let version = crate::version::parse_version_type(client, version).await?; 26 | let downloads_dir = helpers::directories::get_downloads_directory(config).await?; 27 | let version_path = downloads_dir.join(&version.tag_name); 28 | 29 | // If not installed, suggest installing it first 30 | if !version_path.exists() { 31 | anyhow::bail!( 32 | "Version {} is not installed. Install it first with: bob install {}", 33 | version.tag_name, 34 | version.tag_name 35 | ); 36 | } 37 | 38 | // Use the specific version's binary 39 | let bin_path = version_path.join("bin").join("nvim"); 40 | if !bin_path.exists() { 41 | anyhow::bail!( 42 | "Neovim binary not found at expected path: {}", 43 | bin_path.display() 44 | ); 45 | } 46 | 47 | // Run the specific version with the provided args 48 | let mut cmd = Command::new(bin_path); 49 | cmd.args(args); 50 | helpers::processes::handle_subprocess(&mut cmd).await 51 | } 52 | -------------------------------------------------------------------------------- /src/handlers/sync_handler.rs: -------------------------------------------------------------------------------- 1 | use anyhow::{anyhow, Result}; 2 | use reqwest::Client; 3 | use tokio::fs; 4 | use tracing::info; 5 | 6 | use crate::{config::ConfigFile, helpers::version}; 7 | 8 | use super::use_handler; 9 | 10 | /// Starts the synchronization process. 11 | /// 12 | /// This function reads the version from a sync file and starts the use handler with the read version. 13 | /// 14 | /// # Arguments 15 | /// 16 | /// * `client` - The HTTP client to be used for network requests. 17 | /// * `config` - The configuration for the synchronization process. 18 | /// 19 | /// # Returns 20 | /// 21 | /// * `Result<()>` - Returns a `Result` that indicates whether the synchronization process was successful or not. 22 | /// 23 | /// # Errors 24 | /// 25 | /// This function will return an error if: 26 | /// 27 | /// * The `version_sync_file_location` is not set in the configuration. 28 | /// * The sync file is empty. 29 | /// * The version read from the sync file contains "nightly-". 30 | /// 31 | /// # Example 32 | /// 33 | /// ```rust 34 | /// let client = Client::new(); 35 | /// let config = Config::default(); 36 | /// start(&client, config).await.unwrap(); 37 | /// ``` 38 | pub async fn start(client: &Client, config: ConfigFile) -> Result<()> { 39 | let version_sync_file_location = version::get_version_sync_file_location(&config.config) 40 | .await? 41 | .ok_or_else(|| anyhow!("version_sync_file_location needs to be set to use bob sync"))?; 42 | 43 | let version = fs::read_to_string(&version_sync_file_location).await?; 44 | if version.is_empty() { 45 | return Err(anyhow!("Sync file is empty")); 46 | } 47 | let trimmed_version = version.trim(); 48 | 49 | if trimmed_version.contains("nightly-") { 50 | return Err(anyhow!("Cannot sync nightly rollbacks.")); 51 | } 52 | 53 | info!( 54 | "Using version {version} set in {}", 55 | version_sync_file_location 56 | .into_os_string() 57 | .into_string() 58 | .unwrap() 59 | ); 60 | 61 | use_handler::start( 62 | version::parse_version_type(client, trimmed_version).await?, 63 | true, 64 | client, 65 | config, 66 | ) 67 | .await?; 68 | 69 | Ok(()) 70 | } 71 | -------------------------------------------------------------------------------- /src/handlers/uninstall_handler.rs: -------------------------------------------------------------------------------- 1 | use crate::{ 2 | config::Config, 3 | helpers::{self, directories}, 4 | }; 5 | use anyhow::{anyhow, Result}; 6 | use dialoguer::{ 7 | console::{style, Term}, 8 | theme::ColorfulTheme, 9 | Confirm, MultiSelect, 10 | }; 11 | use regex::Regex; 12 | use reqwest::Client; 13 | use tokio::fs; 14 | use tracing::{info, warn}; 15 | 16 | /// Starts the uninstall process. 17 | /// 18 | /// This function creates a new HTTP client, determines the version to uninstall, checks if the version is currently in use, and if not, removes the version's directory. 19 | /// 20 | /// # Arguments 21 | /// 22 | /// * `version` - An optional string that represents the version to uninstall. If `None`, the function will call `uninstall_selections` to allow the user to select versions to uninstall. 23 | /// * `config` - The configuration for the uninstall process. 24 | /// 25 | /// # Returns 26 | /// 27 | /// * `Result<()>` - Returns a `Result` that indicates whether the uninstall process was successful or not. 28 | /// 29 | /// # Errors 30 | /// 31 | /// This function will return an error if: 32 | /// 33 | /// * The version cannot be parsed. 34 | /// * The version is currently in use. 35 | /// * The downloads directory cannot be determined. 36 | /// * The version's directory cannot be removed. 37 | /// 38 | /// # Example 39 | /// 40 | /// ```rust 41 | /// let config = Config::default(); 42 | /// start(Some("1.0.0"), config).await.unwrap(); 43 | /// ``` 44 | pub async fn start(version: Option<&str>, config: Config) -> Result<()> { 45 | let client = Client::new(); 46 | 47 | let version = match version { 48 | Some(value) => value, 49 | None => return uninstall_selections(&client, &config).await, 50 | }; 51 | 52 | let version = helpers::version::parse_version_type(&client, version).await?; 53 | if helpers::version::is_version_used(&version.non_parsed_string, &config).await { 54 | warn!("Switch to a different version before proceeding"); 55 | return Ok(()); 56 | } 57 | 58 | let downloads_dir = match directories::get_downloads_directory(&config).await { 59 | Ok(value) => value, 60 | Err(error) => return Err(anyhow!(error)), 61 | }; 62 | 63 | let version_regex = Regex::new(r"^[0-9]+\.[0-9]+\.[0-9]+$")?; 64 | let path = if version_regex.is_match(&version.non_parsed_string) { 65 | let intermediate = format!("v{}", &version.non_parsed_string); 66 | downloads_dir.join(intermediate) 67 | } else { 68 | downloads_dir.join(&version.non_parsed_string) 69 | }; 70 | 71 | fs::remove_dir_all(path).await?; 72 | info!( 73 | "Successfully uninstalled version: {}", 74 | version.non_parsed_string 75 | ); 76 | Ok(()) 77 | } 78 | 79 | /// Uninstalls selected versions. 80 | /// 81 | /// This function reads the versions from the downloads directory, presents a list of installed versions to the user, allows them to select versions to uninstall, and then uninstalls the selected versions. 82 | /// 83 | /// # Arguments 84 | /// 85 | /// * `client` - The HTTP client to be used for network requests. 86 | /// * `config` - The configuration for the uninstall process. 87 | /// 88 | /// # Returns 89 | /// 90 | /// * `Result<()>` - Returns a `Result` that indicates whether the uninstall process was successful or not. 91 | /// 92 | /// # Errors 93 | /// 94 | /// This function will return an error if: 95 | /// 96 | /// * The downloads directory cannot be read. 97 | /// * The version cannot be parsed from the file name. 98 | /// * The version is currently in use. 99 | /// * The user aborts the uninstall process. 100 | /// 101 | /// # Example 102 | /// 103 | /// ```rust 104 | /// let client = Client::new(); 105 | /// let config = Config::default(); 106 | /// uninstall_selections(&client, &config).await.unwrap(); 107 | /// ``` 108 | async fn uninstall_selections(client: &Client, config: &Config) -> Result<()> { 109 | let downloads_dir = directories::get_downloads_directory(config).await?; 110 | 111 | let mut paths = fs::read_dir(downloads_dir.clone()).await?; 112 | let mut installed_versions: Vec = Vec::new(); 113 | 114 | while let Some(path) = paths.next_entry().await? { 115 | let name = path.file_name().to_str().unwrap().to_owned(); 116 | 117 | let version = match helpers::version::parse_version_type(client, &name).await { 118 | Ok(value) => value, 119 | Err(_) => continue, 120 | }; 121 | 122 | if helpers::version::is_version_used(&version.non_parsed_string, config).await { 123 | continue; 124 | } 125 | installed_versions.push(version.non_parsed_string); 126 | } 127 | 128 | if installed_versions.is_empty() { 129 | info!("You only have one neovim instance installed"); 130 | return Ok(()); 131 | } 132 | 133 | let theme = ColorfulTheme { 134 | checked_item_prefix: style("✓".to_string()).for_stderr().green(), 135 | unchecked_item_prefix: style("✓".to_string()).for_stderr().black(), 136 | ..ColorfulTheme::default() 137 | }; 138 | 139 | let selections = MultiSelect::with_theme(&theme) 140 | .with_prompt("Toogle with space the versions you wish to uninstall:") 141 | .items(&installed_versions) 142 | .interact_on_opt(&Term::stderr())?; 143 | 144 | match &selections { 145 | Some(ids) if !ids.is_empty() => { 146 | let confirm = Confirm::with_theme(&ColorfulTheme::default()) 147 | .with_prompt("Do you wish to continue?") 148 | .interact_on_opt(&Term::stderr())?; 149 | 150 | match confirm { 151 | Some(true) => {} 152 | None | Some(false) => { 153 | info!("Uninstall aborted..."); 154 | return Ok(()); 155 | } 156 | } 157 | 158 | for &i in ids { 159 | let path = downloads_dir.join(&installed_versions[i]); 160 | fs::remove_dir_all(path).await?; 161 | info!( 162 | "Successfully uninstalled version: {}", 163 | &installed_versions[i] 164 | ); 165 | } 166 | } 167 | None | Some(_) => info!("Uninstall aborted..."), 168 | } 169 | Ok(()) 170 | } 171 | -------------------------------------------------------------------------------- /src/handlers/update_handler.rs: -------------------------------------------------------------------------------- 1 | use crate::cli::Update; 2 | use crate::config::ConfigFile; 3 | use crate::helpers::version::is_version_installed; 4 | use anyhow::Result; 5 | use reqwest::Client; 6 | use tracing::{info, warn}; 7 | 8 | use super::{install_handler, InstallResult}; 9 | 10 | /// Starts the update process based on the provided `Update` data, `Client`, and `Config`. 11 | /// 12 | /// # Arguments 13 | /// 14 | /// * `data: Update` - Contains the version information to be updated. If `data.version` is `None` or `data.all` is `true`, it will attempt to update all installed versions. 15 | /// * `client: &Client` - A reference to the `Client` used for making requests. 16 | /// * `config: Config` - Contains the configuration settings. 17 | /// 18 | /// # Behavior 19 | /// 20 | /// If `data.version` is `None` or `data.all` is `true`, the function will attempt to update both the "stable" and "nightly" versions if they are installed. If an update is successful, `did_update` is set to `true`. 21 | /// 22 | /// If neither version is updated, a warning message "There was nothing to update." is logged. 23 | /// 24 | /// If `data.version` is not `None` and `data.all` is not `true`, the function will attempt to update the specified version if it is installed. If the version is not installed, a warning message is logged. 25 | /// 26 | /// # Returns 27 | /// 28 | /// * `Result<()>` - Returns `Ok(())` if the function executes successfully, otherwise it returns an error. 29 | /// 30 | /// # Errors 31 | /// 32 | /// This function will return an error if there's a problem with parsing the version type, checking if a version is installed, or starting the installation process. 33 | /// 34 | /// # Example 35 | /// 36 | /// ```rust 37 | /// let data = Update { version: Some("0.2.2"), all: false }; 38 | /// let client = Client::new(); 39 | /// let config = Config::default(); 40 | /// start(data, &client, config).await?; 41 | /// ``` 42 | /// 43 | /// # Note 44 | /// 45 | /// This function is asynchronous and must be awaited. 46 | /// 47 | /// # See Also 48 | /// 49 | /// * [`crate::version::parse_version_type`](src/version.rs) 50 | /// * [`is_version_installed`](src/helpers/version.rs) 51 | /// * [`install_handler::start`](src/handlers/install_handler.rs) 52 | pub async fn start(data: Update, client: &Client, config: ConfigFile) -> Result<()> { 53 | if data.version.is_none() || data.all { 54 | let mut did_update = false; 55 | 56 | let mut stable = crate::version::parse_version_type(client, "stable").await?; 57 | if is_version_installed(&stable.tag_name, &config.config).await? { 58 | match install_handler::start(&mut stable, client, &config).await? { 59 | InstallResult::InstallationSuccess(_) => did_update = true, 60 | InstallResult::VersionAlreadyInstalled 61 | | InstallResult::NightlyIsUpdated 62 | | InstallResult::GivenNightlyRollback => (), 63 | } 64 | } 65 | 66 | if is_version_installed("nightly", &config.config).await? { 67 | let mut nightly = crate::version::parse_version_type(client, "nightly").await?; 68 | match install_handler::start(&mut nightly, client, &config).await? { 69 | InstallResult::InstallationSuccess(_) => did_update = true, 70 | InstallResult::NightlyIsUpdated 71 | | InstallResult::VersionAlreadyInstalled 72 | | InstallResult::GivenNightlyRollback => (), 73 | } 74 | } 75 | 76 | if !did_update { 77 | warn!("There was nothing to update."); 78 | } 79 | 80 | return Ok(()); 81 | } 82 | 83 | let mut version = crate::version::parse_version_type(client, &data.version.unwrap()).await?; 84 | 85 | if !is_version_installed(&version.tag_name, &config.config).await? { 86 | warn!("{} is not installed.", version.non_parsed_string); 87 | return Ok(()); 88 | } 89 | match install_handler::start(&mut version, client, &config).await? { 90 | InstallResult::NightlyIsUpdated => info!("Nightly is already updated!"), 91 | InstallResult::VersionAlreadyInstalled => info!("Stable is already updated!"), 92 | InstallResult::InstallationSuccess(_) | InstallResult::GivenNightlyRollback => (), 93 | } 94 | Ok(()) 95 | } 96 | -------------------------------------------------------------------------------- /src/handlers/use_handler.rs: -------------------------------------------------------------------------------- 1 | use anyhow::{anyhow, Result}; 2 | use dialoguer::Confirm; 3 | use reqwest::Client; 4 | use std::env; 5 | use std::path::{Path, PathBuf}; 6 | use std::process::Command; 7 | use tokio::fs::{self}; 8 | use tracing::info; 9 | 10 | use crate::config::{Config, ConfigFile}; 11 | use crate::handlers::{install_handler, InstallResult}; 12 | use crate::helpers; 13 | use crate::helpers::directories::get_installation_directory; 14 | use crate::helpers::version::types::{ParsedVersion, VersionType}; 15 | 16 | /// Starts the process of using a specified version. 17 | /// 18 | /// This function checks if the specified version is already used, copies the Neovim proxy to the installation directory, installs the version if it's not already installed and used, switches to the version, and removes the "stable" directory if the version type is "Latest". 19 | /// 20 | /// # Arguments 21 | /// 22 | /// * `version` - The version to use. 23 | /// * `install` - Whether to install the version if it's not already installed. 24 | /// * `client` - The client to use for HTTP requests. 25 | /// * `config` - The configuration for the operation. 26 | /// 27 | /// # Returns 28 | /// 29 | /// * `Result<()>` - Returns a `Result` that indicates whether the operation was successful or not. 30 | /// 31 | /// # Errors 32 | /// 33 | /// This function will return an error if: 34 | /// 35 | /// * The version is not already used and it cannot be installed. 36 | /// * The version cannot be switched to. 37 | /// * The "stable" directory exists and it cannot be removed. 38 | /// 39 | /// # Example 40 | /// 41 | /// ```rust 42 | /// let version = ParsedVersion::new("1.0.0"); 43 | /// let install = true; 44 | /// let client = Client::new(); 45 | /// let config = Config::default(); 46 | /// start(version, install, &client, config).await.unwrap(); 47 | /// ``` 48 | pub async fn start( 49 | mut version: ParsedVersion, 50 | install: bool, 51 | client: &Client, 52 | config: ConfigFile, 53 | ) -> Result<()> { 54 | let is_version_used = 55 | helpers::version::is_version_used(&version.tag_name, &config.config).await; 56 | 57 | copy_nvim_proxy(&config).await?; 58 | if is_version_used && version.tag_name != "nightly" { 59 | info!("{} is already installed and used!", version.tag_name); 60 | return Ok(()); 61 | } 62 | 63 | if install { 64 | match install_handler::start(&mut version, client, &config).await { 65 | Ok(success) => { 66 | if let InstallResult::NightlyIsUpdated = success { 67 | if is_version_used { 68 | info!("Nightly is already updated and used!"); 69 | return Ok(()); 70 | } 71 | } 72 | } 73 | Err(error) => return Err(error), 74 | } 75 | } 76 | 77 | switch(&config.config, &version).await?; 78 | 79 | if let VersionType::Latest = version.version_type { 80 | if fs::metadata("stable").await.is_ok() { 81 | fs::remove_dir_all("stable").await?; 82 | } 83 | } 84 | 85 | let installation_dir = get_installation_directory(&config.config).await?; 86 | 87 | add_to_path(installation_dir, config).await?; 88 | 89 | info!("You can now use {}!", version.tag_name); 90 | 91 | Ok(()) 92 | } 93 | 94 | /// Switches to a specified version. 95 | /// 96 | /// This function changes the current directory to the downloads directory, writes the version to a file named "used", and if the version is different from the version stored in `version_sync_file_location`, it also writes the version to `version_sync_file_location`. 97 | /// 98 | /// # Arguments 99 | /// 100 | /// * `config` - The configuration for the operation. 101 | /// * `version` - The version to switch to. 102 | /// 103 | /// # Returns 104 | /// 105 | /// * `Result<()>` - Returns a `Result` that indicates whether the operation was successful or not. 106 | /// 107 | /// # Errors 108 | /// 109 | /// This function will return an error if: 110 | /// 111 | /// * The downloads directory cannot be determined. 112 | /// * The current directory cannot be changed to the downloads directory. 113 | /// * The version cannot be written to the "used" file. 114 | /// * The version cannot be read from `version_sync_file_location`. 115 | /// * The version cannot be written to `version_sync_file_location`. 116 | /// 117 | /// # Example 118 | /// 119 | /// ```rust 120 | /// let config = Config::default(); 121 | /// let version = ParsedVersion::new("1.0.0"); 122 | /// switch(&config, &version).await.unwrap(); 123 | /// ``` 124 | pub async fn switch(config: &Config, version: &ParsedVersion) -> Result<()> { 125 | std::env::set_current_dir(helpers::directories::get_downloads_directory(config).await?)?; 126 | 127 | let file_version: String = if version.version_type == VersionType::Hash { 128 | if version.non_parsed_string.len() <= 7 { 129 | let mut current_dir = env::current_dir()?; 130 | current_dir.push(&version.non_parsed_string); 131 | current_dir.push("full-hash.txt"); 132 | let hash_result = fs::read_to_string(¤t_dir).await; 133 | 134 | if let Ok(hash) = hash_result { 135 | hash 136 | } else { 137 | return Err(anyhow!( 138 | "Full hash file doesn't exist, please rebuild this commit" 139 | )); 140 | } 141 | } else { 142 | version.non_parsed_string.to_string() 143 | } 144 | } else { 145 | version.tag_name.to_string() 146 | }; 147 | 148 | fs::write("used", &file_version).await?; 149 | if let Some(version_sync_file_location) = 150 | helpers::version::get_version_sync_file_location(config).await? 151 | { 152 | // Write the used version to version_sync_file_location only if it's different 153 | let stored_version = fs::read_to_string(&version_sync_file_location).await?; 154 | if stored_version != version.non_parsed_string { 155 | fs::write(&version_sync_file_location, file_version).await?; 156 | info!( 157 | "Written version to {}", 158 | version_sync_file_location 159 | .into_os_string() 160 | .into_string() 161 | .unwrap() 162 | ); 163 | } 164 | } 165 | 166 | Ok(()) 167 | } 168 | 169 | /// Copies the Neovim proxy to the installation directory. 170 | /// 171 | /// This function gets the current executable's path, determines the installation directory, creates it if it doesn't exist, adds it to the system's PATH, and copies the current executable to the installation directory as "nvim" or "nvim.exe" (on Windows). 172 | /// 173 | /// If a file named "nvim" or "nvim.exe" already exists in the installation directory, the function checks its version. If the version matches the current version, the function does nothing. Otherwise, it replaces the file with the current executable. 174 | /// 175 | /// # Arguments 176 | /// 177 | /// * `config` - The configuration for the operation. 178 | /// 179 | /// # Returns 180 | /// 181 | /// * `Result<()>` - Returns a `Result` that indicates whether the operation was successful or not. 182 | /// 183 | /// # Errors 184 | /// 185 | /// This function will return an error if: 186 | /// 187 | /// * The current executable's path cannot be determined. 188 | /// * The installation directory cannot be created. 189 | /// * The installation directory cannot be added to the PATH. 190 | /// * The version of the existing file cannot be determined. 191 | /// * The existing file cannot be replaced. 192 | /// 193 | /// # Example 194 | /// 195 | /// ```rust 196 | /// let config = Config::default(); 197 | /// copy_nvim_proxy(&config).await.unwrap(); 198 | /// ``` 199 | async fn copy_nvim_proxy(config: &ConfigFile) -> Result<()> { 200 | let exe_path = env::current_exe().unwrap(); 201 | let mut installation_dir = 202 | helpers::directories::get_installation_directory(&config.config).await?; 203 | 204 | if fs::metadata(&installation_dir).await.is_err() { 205 | fs::create_dir_all(&installation_dir).await?; 206 | } 207 | 208 | if cfg!(windows) { 209 | installation_dir.push("nvim.exe"); 210 | } else { 211 | installation_dir.push("nvim"); 212 | } 213 | 214 | if fs::metadata(&installation_dir).await.is_ok() { 215 | let output = Command::new(&installation_dir) 216 | .arg("--&bob") 217 | .output()? 218 | .stdout; 219 | let version = String::from_utf8(output)?.trim().to_string(); 220 | 221 | if version == env!("CARGO_PKG_VERSION") { 222 | return Ok(()); 223 | } 224 | } 225 | 226 | info!("Updating neovim proxy"); 227 | copy_file_with_error_handling(&exe_path, &installation_dir).await?; 228 | 229 | Ok(()) 230 | } 231 | 232 | /// Asynchronously copies a file from `old_path` to `new_path`, handling specific OS errors. 233 | /// 234 | /// This function attempts to copy a file from the specified `old_path` to the specified `new_path`. 235 | /// If the file is being used by another process (OS error 26 or 32), it prints an error message 236 | /// and returns an error indicating that the file is busy. For any other errors, it returns a 237 | /// generic error with additional context. 238 | /// 239 | /// # Arguments 240 | /// 241 | /// * `old_path` - A reference to the source `Path` of the file to be copied. 242 | /// * `new_path` - A reference to the destination `Path` where the file should be copied. 243 | /// 244 | /// # Returns 245 | /// 246 | /// This function returns a `Result<()>`. If the file is successfully copied, it returns `Ok(())`. 247 | /// If an error occurs, it returns an `Err` with a detailed error message. 248 | /// 249 | /// # Errors 250 | /// 251 | /// This function will return an error in the following cases: 252 | /// - If the file is being used by another process (OS error 26 or 32), it returns an error 253 | /// indicating that the file is busy. 254 | /// - For any other errors, it returns a generic error with additional context. 255 | /// 256 | /// # Examples 257 | /// 258 | /// ```rust 259 | /// use std::path::Path; 260 | /// use anyhow::Result; 261 | /// 262 | /// #[tokio::main] 263 | /// async fn main() -> Result<()> { 264 | /// let old_path = Path::new("path/to/source/file"); 265 | /// let new_path = Path::new("path/to/destination/file"); 266 | /// 267 | /// copy_file_with_error_handling(&old_path, &new_path).await?; 268 | /// Ok(()) 269 | /// } 270 | /// ``` 271 | async fn copy_file_with_error_handling(old_path: &Path, new_path: &Path) -> Result<()> { 272 | match fs::copy(&old_path, &new_path).await { 273 | Ok(_) => Ok(()), 274 | Err(e) => match e.raw_os_error() { 275 | Some(26) | Some(32) => Err(anyhow::anyhow!( 276 | "The file {} is busy. Please make sure to close any processes using it.", 277 | old_path.display() 278 | )), 279 | _ => Err(anyhow::anyhow!(e).context("Failed to copy file")), 280 | }, 281 | } 282 | } 283 | 284 | /// Adds the installation directory to the system's PATH. 285 | /// 286 | /// This function checks if the installation directory is already in the PATH. If not, it adds the directory to the PATH. 287 | /// 288 | /// # Arguments 289 | /// 290 | /// * `installation_dir` - The directory to be added to the PATH. 291 | /// 292 | /// # Returns 293 | /// 294 | /// * `Result<()>` - Returns a `Result` that indicates whether the operation was successful or not. 295 | /// 296 | /// # Errors 297 | /// 298 | /// This function will return an error if: 299 | /// 300 | /// * The installation directory cannot be converted to a string. 301 | /// * The current user's environment variables cannot be accessed or modified (Windows only). 302 | /// * The PATH environment variable cannot be read (non-Windows only). 303 | /// 304 | /// # Example 305 | /// 306 | /// ```rust 307 | /// let installation_dir = Path::new("/usr/local/bin"); 308 | /// add_to_path(&installation_dir).unwrap(); 309 | /// ``` 310 | async fn add_to_path(installation_dir: PathBuf, config: ConfigFile) -> Result<()> { 311 | let installation_dir = installation_dir.to_str().unwrap(); 312 | 313 | if what_the_path::shell::exists_in_path("nvim-bin") { 314 | return Ok(()); 315 | } 316 | 317 | if config.config.add_neovim_binary_to_path == Some(false) { 318 | info!("Make sure to add {installation_dir} to $PATH"); 319 | return Ok(()); 320 | } 321 | 322 | if config.config.add_neovim_binary_to_path.is_none() { 323 | let confirmation = Confirm::new() 324 | .with_prompt("Add bob-managed Neovim binary to your $PATH automatically?") 325 | .interact()?; 326 | let mut temp_confg = config.clone(); 327 | 328 | temp_confg.config.add_neovim_binary_to_path = Some(confirmation); 329 | temp_confg.save_to_file().await?; 330 | 331 | if !confirmation { 332 | return Ok(()); 333 | } 334 | 335 | drop(temp_confg); 336 | } 337 | 338 | cfg_if::cfg_if! { 339 | if #[cfg(windows)] { 340 | use winreg::enums::*; 341 | use winreg::RegKey; 342 | 343 | let current_usr = RegKey::predef(HKEY_CURRENT_USER); 344 | let env = current_usr.open_subkey_with_flags("Environment", KEY_READ | KEY_WRITE)?; 345 | let usr_path: String = env.get_value("Path")?; 346 | let usr_path_lower = usr_path.replace('/', "\\").to_lowercase(); 347 | let installation_dir = installation_dir.replace('/', "\\").to_lowercase(); 348 | 349 | if usr_path_lower.contains(&installation_dir) { 350 | return Ok(()); 351 | } 352 | 353 | let new_path = if usr_path_lower.ends_with(';') { 354 | format!("{usr_path_lower}{}", installation_dir) 355 | } else { 356 | format!("{usr_path_lower};{}", installation_dir) 357 | }; 358 | 359 | env.set_value("Path", &new_path)?; 360 | } else { 361 | use tokio::fs::File; 362 | use tokio::io::AsyncWriteExt; 363 | use what_the_path::shell::Shell; 364 | 365 | let shell = Shell::detect_by_shell_var()?; 366 | let env_paths = copy_env_files_if_not_exist(&config.config, installation_dir).await?; 367 | 368 | match shell { 369 | Shell::Fish(fish) => { 370 | let files = fish.get_rcfiles()?; 371 | let fish_file = files[0].join("bob.fish"); 372 | if fish_file.exists() { return Ok(()) } 373 | let mut opened_file = File::create(fish_file).await?; 374 | opened_file.write_all(format!("source \"{}\"\n", env_paths[1].to_str().unwrap()).as_bytes()).await?; 375 | opened_file.flush().await?; 376 | }, 377 | shell => { 378 | let files = shell.get_rcfiles()?; 379 | for file in files { 380 | what_the_path::shell::append_to_rcfile(file, &format!(". \"{}\"", env_paths[0].to_str().unwrap()))?; 381 | } 382 | } 383 | } 384 | } 385 | } 386 | 387 | info!("Added {installation_dir} to system PATH. Please start a new terminal session for changes to take effect."); 388 | 389 | Ok(()) 390 | } 391 | 392 | #[cfg(target_family = "unix")] 393 | async fn copy_env_files_if_not_exist( 394 | config: &Config, 395 | installation_dir: &str, 396 | ) -> Result> { 397 | use crate::helpers::directories::get_downloads_directory; 398 | use tokio::io::AsyncWriteExt; 399 | 400 | let fish_env = include_str!("../../env/env.fish").replace("{nvim_bin}", installation_dir); 401 | let posix_env = include_str!("../../env/env.sh").replace("{nvim_bin}", installation_dir); 402 | let downloads_dir = get_downloads_directory(config).await?; 403 | let env_dir = downloads_dir.join("env"); 404 | 405 | // Ensure the env directory exists 406 | fs::create_dir_all(&env_dir).await?; 407 | 408 | // Define the file paths 409 | let fish_env_path = env_dir.join("env.fish"); 410 | let posix_env_path = env_dir.join("env.sh"); 411 | 412 | // Check if the files exist and write the content if they don't 413 | if !fish_env_path.exists() { 414 | let mut file = fs::File::create(&fish_env_path).await?; 415 | file.write_all(fish_env.as_bytes()).await?; 416 | file.flush().await?; 417 | } 418 | 419 | if !posix_env_path.exists() { 420 | let mut file = fs::File::create(&posix_env_path).await?; 421 | file.write_all(posix_env.as_bytes()).await?; 422 | file.flush().await?; 423 | } 424 | 425 | Ok(vec![posix_env_path, fish_env_path]) 426 | } 427 | -------------------------------------------------------------------------------- /src/helpers/checksum.rs: -------------------------------------------------------------------------------- 1 | use anyhow::anyhow; 2 | use anyhow::Result; 3 | use sha2::{Digest, Sha256}; 4 | use std::path::Path; 5 | use std::{fs, io}; 6 | 7 | /// Checks whether the checksum of the file at path 'a' matches the checksum saved in the file at path 'b'. 8 | /// # Arguments 9 | /// 10 | /// * `a` - A reference to a `&Path` object representing the path of the neovim archive. 11 | /// * `b` - A reference to a `&Path` object representing the path of the checksum file. 12 | /// 13 | /// # Returns 14 | /// 15 | /// This function returns a `Result` that contains a `bool` indicating whether the checksum of the file at path 'a' matches the checksum saved in the file at path 'b'. 16 | /// If there is an error opening or reading the files, the function returns `Err(error)`. 17 | pub fn sha256cmp(a: &Path, b: &Path, filename: &str) -> Result { 18 | let checksum_contents = fs::read_to_string(b)?; 19 | let checksum = checksum_contents 20 | .lines() 21 | .find(|line| line.contains(filename)) 22 | .and_then(|line| line.split_whitespace().next()) 23 | .ok_or_else(|| anyhow!("Checksum not found for {}", filename))?; 24 | 25 | let mut hasher = Sha256::new(); 26 | let mut file = fs::File::open(a)?; 27 | io::copy(&mut file, &mut hasher)?; 28 | 29 | let hash = hasher.finalize(); 30 | let hash = format!("{:x}", hash); 31 | 32 | Ok(hash == checksum) 33 | } 34 | -------------------------------------------------------------------------------- /src/helpers/directories.rs: -------------------------------------------------------------------------------- 1 | use anyhow::{anyhow, Result}; 2 | use std::fs; 3 | use std::path::PathBuf; 4 | 5 | use crate::config::Config; 6 | 7 | /// Returns the home directory path for the current user. 8 | /// 9 | /// This function checks the target operating system using the `cfg!` macro and constructs the home directory path accordingly. 10 | /// For Windows, it uses the "USERPROFILE" environment variable. 11 | /// For macOS, it uses the "/Users/" directory and appends the "SUDO_USER" or "USER" environment variable if they exist and correspond to a valid directory. 12 | /// For other operating systems, it uses the "/home/" directory and appends the "SUDO_USER" or "USER" environment variable if they exist and correspond to a valid directory. 13 | /// If none of the above methods work, it uses the "HOME" environment variable. 14 | /// 15 | /// # Returns 16 | /// 17 | /// This function returns a `Result` that contains a `PathBuf` representing the home directory path if the operation was successful. 18 | /// If the operation failed, the function returns `Err` with a description of the error. 19 | /// 20 | /// # Example 21 | /// 22 | /// ```rust 23 | /// let home_dir = get_home_dir()?; 24 | /// ``` 25 | pub fn get_home_dir() -> Result { 26 | let mut home_str = PathBuf::new(); 27 | 28 | if cfg!(windows) { 29 | home_str.push(std::env::var("USERPROFILE")?); 30 | return Ok(home_str); 31 | } 32 | 33 | if cfg!(target_os = "macos") { 34 | home_str.push("/Users/"); 35 | } else { 36 | home_str.push("/home/") 37 | }; 38 | 39 | if let Ok(value) = std::env::var("SUDO_USER") { 40 | home_str.push(&value); 41 | if fs::metadata(&home_str).is_ok() { 42 | return Ok(home_str); 43 | } 44 | } 45 | 46 | if let Ok(value) = std::env::var("USER") { 47 | home_str.push(&value); 48 | if fs::metadata(&home_str).is_ok() { 49 | return Ok(home_str); 50 | } 51 | } 52 | 53 | let home_value = std::env::var("HOME")?; 54 | home_str = PathBuf::from(home_value); 55 | 56 | Ok(home_str) 57 | } 58 | 59 | /// Returns the local data directory path for the current user. 60 | /// 61 | /// This function first gets the home directory path by calling the `get_home_dir` function. 62 | /// It then checks the target operating system using the `cfg!` macro and constructs the local data directory path accordingly. 63 | /// For Windows, it appends "AppData/Local" to the home directory path. 64 | /// For other operating systems, it appends ".local/share" to the home directory path. 65 | /// 66 | /// # Returns 67 | /// 68 | /// This function returns a `Result` that contains a `PathBuf` representing the local data directory path if the operation was successful. 69 | /// If the operation failed, the function returns `Err` with a description of the error. 70 | /// 71 | /// # Example 72 | /// 73 | /// ```rust 74 | /// let local_data_dir = get_local_data_dir()?; 75 | /// ``` 76 | pub fn get_local_data_dir() -> Result { 77 | let mut home_dir = get_home_dir()?; 78 | if cfg!(windows) { 79 | home_dir.push("AppData\\Local"); 80 | return Ok(home_dir); 81 | } 82 | 83 | home_dir.push(".local/share"); 84 | Ok(home_dir) 85 | } 86 | 87 | /// Returns the local data directory path for the current user. 88 | /// 89 | /// This function first gets the home directory path by calling the `get_home_dir` function. 90 | /// It then checks the target operating system using the `cfg!` macro and constructs the local data directory path accordingly. 91 | /// For Windows, it appends "AppData/Local" to the home directory path. 92 | /// For other operating systems, it appends ".local/share" to the home directory path. 93 | /// 94 | /// # Returns 95 | /// 96 | /// This function returns a `Result` that contains a `PathBuf` representing the local data directory path if the operation was successful. 97 | /// If the operation failed, the function returns `Err` with a description of the error. 98 | /// 99 | /// # Example 100 | /// 101 | /// ```rust 102 | /// let local_data_dir = get_local_data_dir()?; 103 | /// ``` 104 | pub fn get_config_file() -> Result { 105 | if let Ok(value) = std::env::var("BOB_CONFIG") { 106 | return Ok(PathBuf::from(value)); 107 | } 108 | 109 | let mut home_dir = get_home_dir()?; 110 | 111 | if cfg!(windows) { 112 | home_dir.push("AppData\\Roaming"); 113 | } else if cfg!(target_os = "macos") { 114 | home_dir.push("Library/Application Support"); 115 | } else { 116 | home_dir.push(".config"); 117 | } 118 | 119 | home_dir.push("bob/config.toml"); 120 | 121 | if fs::metadata(&home_dir).is_err() { 122 | home_dir.pop(); 123 | home_dir.push("config.json"); 124 | } 125 | 126 | Ok(home_dir) 127 | } 128 | 129 | /// Asynchronously returns the downloads directory path based on the application configuration. 130 | /// 131 | /// This function takes a reference to a `Config` as an argument, which contains the application configuration. 132 | /// It first checks if the `downloads_location` field in the `Config` is set. If it is, it checks if the directory exists. If the directory does not exist, it returns an error. 133 | /// If the `downloads_location` field in the `Config` is not set, it gets the local data directory path by calling the `get_local_data_dir` function and appends "bob" to it. 134 | /// It then checks if the "bob" directory exists. If the directory does not exist, it attempts to create it. If the creation fails, it returns an error. 135 | /// 136 | /// # Arguments 137 | /// 138 | /// * `config` - A reference to a `Config` containing the application configuration. 139 | /// 140 | /// # Returns 141 | /// 142 | /// This function returns a `Result` that contains a `PathBuf` representing the downloads directory path if the operation was successful. 143 | /// If the operation failed, the function returns `Err` with a description of the error. 144 | /// 145 | /// # Example 146 | /// 147 | /// ```rust 148 | /// let config = Config::default(); 149 | /// let downloads_directory = get_downloads_directory(&config).await?; 150 | /// ``` 151 | pub async fn get_downloads_directory(config: &Config) -> Result { 152 | let path = match &config.downloads_location { 153 | Some(path) => { 154 | if tokio::fs::metadata(path).await.is_err() { 155 | return Err(anyhow!("Custom directory {path} doesn't exist!")); 156 | } 157 | 158 | PathBuf::from(path) 159 | } 160 | None => { 161 | let mut data_dir = get_local_data_dir()?; 162 | 163 | data_dir.push("bob"); 164 | let does_folder_exist = tokio::fs::metadata(&data_dir).await.is_ok(); 165 | let is_folder_created = tokio::fs::create_dir_all(&data_dir).await.is_ok(); 166 | 167 | if !does_folder_exist && !is_folder_created { 168 | return Err(anyhow!("Couldn't create downloads directory")); 169 | } 170 | data_dir 171 | } 172 | }; 173 | 174 | Ok(path) 175 | } 176 | 177 | /// Asynchronously returns the installation directory path based on the application configuration. 178 | /// 179 | /// This function takes a reference to a `Config` as an argument, which contains the application configuration. 180 | /// It first checks if the `installation_location` field in the `Config` is set. If it is, it returns the value of this field as a `PathBuf`. 181 | /// If the `installation_location` field in the `Config` is not set, it gets the downloads directory path by calling the `get_downloads_directory` function and appends "nvim-bin" to it. 182 | /// 183 | /// # Arguments 184 | /// 185 | /// * `config` - A reference to a `Config` containing the application configuration. 186 | /// 187 | /// # Returns 188 | /// 189 | /// This function returns a `Result` that contains a `PathBuf` representing the installation directory path if the operation was successful. 190 | /// If the operation failed, the function returns `Err` with a description of the error. 191 | /// 192 | /// # Example 193 | /// 194 | /// ```rust 195 | /// let config = Config::default(); 196 | /// let installation_directory = get_installation_directory(&config).await?; 197 | /// ``` 198 | pub async fn get_installation_directory(config: &Config) -> Result { 199 | match &config.installation_location { 200 | Some(path) => Ok(PathBuf::from(path.clone())), 201 | None => { 202 | let mut installation_location = get_downloads_directory(config).await?; 203 | installation_location.push("nvim-bin"); 204 | 205 | Ok(installation_location) 206 | } 207 | } 208 | } 209 | -------------------------------------------------------------------------------- /src/helpers/filesystem.rs: -------------------------------------------------------------------------------- 1 | use anyhow::{anyhow, Result}; 2 | use async_recursion::async_recursion; 3 | use indicatif::{ProgressBar, ProgressStyle}; 4 | use std::path::Path; 5 | use tokio::fs; 6 | 7 | /// Asynchronously removes a directory and all its contents. 8 | /// 9 | /// This function takes a string reference as an argument, which represents the directory to be removed. 10 | /// It first reads the directory and counts the number of entries. Then, it creates a progress bar with the total number of entries. 11 | /// It iterates over each entry in the directory. If the entry is a directory, it removes the directory and all its contents. If the entry is a file, it removes the file. 12 | /// After removing each entry, it updates the progress bar. 13 | /// Finally, it attempts to remove the directory itself. If this operation fails, it returns an error. 14 | /// 15 | /// # Arguments 16 | /// 17 | /// * `directory` - A string reference representing the directory to be removed. 18 | /// 19 | /// # Returns 20 | /// 21 | /// This function returns a `Result` that indicates whether the operation was successful. 22 | /// If the operation was successful, the function returns `Ok(())`. 23 | /// If the operation failed, the function returns `Err` with a description of the error. 24 | /// 25 | /// # Example 26 | /// 27 | /// ```rust 28 | /// let directory = "/path/to/directory"; 29 | /// remove_dir(directory).await; 30 | /// ``` 31 | pub async fn remove_dir(directory: &str) -> Result<()> { 32 | let path = Path::new(directory); 33 | let size = path.read_dir()?.count(); 34 | let read_dir = path.read_dir()?; 35 | 36 | let pb = ProgressBar::new(size.try_into()?); 37 | pb.set_style(ProgressStyle::default_bar() 38 | .template("{msg}\n{spinner:.green} [{elapsed_precise}] [{wide_bar:.cyan/blue}] {pos}/{len} ({per_sec}, {eta})") 39 | .progress_chars("█ ")); 40 | pb.set_message(format!("Deleting {}", path.display())); 41 | 42 | let mut removed = 0; 43 | 44 | for entry in read_dir.flatten() { 45 | let path = entry.path(); 46 | 47 | if path.is_dir() { 48 | fs::remove_dir_all(&path).await?; 49 | } else { 50 | fs::remove_file(&path).await?; 51 | } 52 | removed += 1; 53 | pb.set_position(removed); 54 | } 55 | 56 | if let Err(e) = fs::remove_dir(directory).await { 57 | return Err(anyhow!("Failed to remove {directory}: {}", e)); 58 | } 59 | 60 | pb.finish_with_message(format!("Finished removing {}", path.display())); 61 | 62 | Ok(()) 63 | } 64 | 65 | /// Asynchronously copies a directory from one location to another. 66 | /// 67 | /// This function takes two arguments: the source directory and the destination directory. Both arguments are implemented as references to `Path` and are static. 68 | /// It first creates the destination directory, then reads the entries of the source directory. 69 | /// For each entry in the source directory, it checks if the entry is a directory or a file. 70 | /// If the entry is a directory, it recursively calls `copy_dir` to copy the directory to the destination. 71 | /// If the entry is a file, it copies the file to the destination. 72 | /// 73 | /// # Arguments 74 | /// 75 | /// * `from` - A reference to a `Path` representing the source directory. 76 | /// * `to` - A reference to a `Path` representing the destination directory. 77 | /// 78 | /// # Returns 79 | /// 80 | /// This function returns a `Result` that indicates whether the operation was successful. 81 | /// If the operation was successful, the function returns `Ok(())`. 82 | /// If the operation failed, the function returns `Err` with a description of the error. 83 | /// 84 | /// # Example 85 | /// 86 | /// ```rust 87 | /// let from = Path::new("/path/to/source"); 88 | /// let to = Path::new("/path/to/destination"); 89 | /// copy_dir(from, to).await; 90 | /// ``` 91 | #[async_recursion(?Send)] 92 | pub async fn copy_dir_async( 93 | from: impl AsRef + 'static, 94 | to: impl AsRef + 'static, 95 | ) -> Result<()> { 96 | let original_path = from.as_ref().to_owned(); 97 | let destination = to.as_ref().to_owned(); 98 | 99 | fs::create_dir(&destination).await?; 100 | 101 | let mut entries = fs::read_dir(original_path).await?; 102 | 103 | while let Some(entry) = entries.next_entry().await? { 104 | let path = entry.path(); 105 | 106 | if path.is_dir() { 107 | let new_dest = destination.join(path.file_name().unwrap()); 108 | copy_dir_async(path, new_dest).await?; 109 | } else { 110 | let new_dest = destination.join(path.file_name().unwrap()); 111 | fs::copy(path, new_dest).await?; 112 | } 113 | } 114 | 115 | Ok(()) 116 | } 117 | 118 | /// Copies a directory from one location to another. 119 | /// 120 | /// This function takes two arguments: the source directory and the destination directory. Both arguments are implemented as references to `Path` and are static. 121 | /// It first creates the destination directory, then reads the entries of the source directory. 122 | /// For each entry in the source directory, it checks if the entry is a directory or a file. 123 | /// If the entry is a directory, it recursively calls `copy_dir` to copy the directory to the destination. 124 | /// If the entry is a file, it copies the file to the destination. 125 | /// 126 | /// # Arguments 127 | /// 128 | /// * `from` - A reference to a `Path` representing the source directory. 129 | /// * `to` - A reference to a `Path` representing the destination directory. 130 | /// 131 | /// # Returns 132 | /// 133 | /// This function returns a `Result` that indicates whether the operation was successful. 134 | /// If the operation was successful, the function returns `Ok(())`. 135 | /// If the operation failed, the function returns `Err` with a description of the error. 136 | /// 137 | /// # Example 138 | /// 139 | /// ```rust 140 | /// let from = Path::new("/path/to/source"); 141 | /// let to = Path::new("/path/to/destination"); 142 | /// copy_dir(from, to).await; 143 | /// ``` 144 | #[cfg(target_os = "linux")] 145 | pub fn copy_dir(from: impl AsRef, to: impl AsRef) -> Result<()> { 146 | let original_path = from.as_ref().to_owned(); 147 | let destination = to.as_ref().to_owned(); 148 | 149 | std::fs::create_dir(&destination)?; 150 | 151 | let entries = std::fs::read_dir(original_path)?; 152 | 153 | for entry in entries { 154 | let path = entry?.path(); 155 | 156 | if path.is_dir() { 157 | let new_dest = destination.join(path.file_name().unwrap()); 158 | copy_dir(path, new_dest)?; 159 | } else { 160 | let new_dest = destination.join(path.file_name().unwrap()); 161 | std::fs::copy(path, new_dest)?; 162 | } 163 | } 164 | 165 | Ok(()) 166 | } 167 | -------------------------------------------------------------------------------- /src/helpers/mod.rs: -------------------------------------------------------------------------------- 1 | pub mod checksum; 2 | pub mod directories; 3 | pub mod filesystem; 4 | pub mod processes; 5 | pub mod sync; 6 | pub mod unarchive; 7 | pub mod version; 8 | use semver::Version; 9 | 10 | /// Returns the file type for the Neovim binary download based on the target operating system. 11 | /// 12 | /// This function checks the target operating system using the `cfg!` macro and returns a string that corresponds to the appropriate file type for the Neovim binary download. 13 | /// For Windows, it returns "zip". 14 | /// For macOS, it returns "tar.gz". 15 | /// For other operating systems, it returns "appimage". 16 | /// 17 | /// # Returns 18 | /// 19 | /// This function returns a `&'static str` that corresponds to the file type for the Neovim binary download. 20 | /// 21 | /// # Example 22 | /// 23 | /// ```rust 24 | /// let file_type = get_file_type(); 25 | /// ``` 26 | pub fn get_file_type() -> &'static str { 27 | if cfg!(target_family = "windows") { 28 | "zip" 29 | } else if cfg!(target_os = "macos") { 30 | "tar.gz" 31 | } else { 32 | "appimage" 33 | } 34 | } 35 | 36 | /// Returns the platform-specific name for the Neovim binary. 37 | /// 38 | /// This function takes an `Option` as an argument, which represents the version of Neovim. 39 | /// It checks the target operating system and architecture using the `cfg!` macro and returns a string that corresponds to the appropriate Neovim binary for the platform. 40 | /// For Windows, it returns "nvim-win64". 41 | /// For macOS, it checks the version of Neovim. If the version is less than or equal to 0.9.5, it returns "nvim-macos". If the target architecture is "aarch64", it returns "nvim-macos-arm64". Otherwise, it returns "nvim-macos-x86_64". 42 | /// For Linux, it checks the version of Neovim. If the version is less than or equal to 0.10.3, it returns "nvim-linux64". If the target architecture is "aarch64", it returns "nvim-linux-arm64". Otherwise, it returns "nvim-linux-x86_64". 43 | /// 44 | /// # Arguments 45 | /// 46 | /// * `version` - An `Option` representing the version of Neovim. 47 | /// 48 | /// # Returns 49 | /// 50 | /// This function returns a `&'static str` that corresponds to the platform-specific name for the Neovim binary. 51 | /// 52 | /// # Example 53 | /// 54 | /// ```rust 55 | /// let version = Some(Version::new(0, 9, 5)); 56 | /// let platform_name = get_platform_name(&version); 57 | /// ``` 58 | pub fn get_platform_name(version: &Option) -> &'static str { 59 | let version_ref = version.as_ref(); 60 | 61 | let is_macos_legacy = version_ref.is_some_and(|v| v <= &Version::new(0, 9, 5)); 62 | let is_linux_legacy = version_ref.is_some_and(|v| v <= &Version::new(0, 10, 3)); 63 | 64 | if cfg!(target_os = "windows") { 65 | "nvim-win64" 66 | } else if cfg!(target_os = "macos") && is_macos_legacy { 67 | "nvim-macos" 68 | } else if cfg!(target_os = "macos") && cfg!(target_arch = "aarch64") { 69 | "nvim-macos-arm64" 70 | } else if cfg!(target_os = "macos") { 71 | "nvim-macos-x86_64" 72 | } else if is_linux_legacy { 73 | "nvim-linux64" 74 | } else if cfg!(target_arch = "aarch64") { 75 | "nvim-linux-arm64" 76 | } else { 77 | "nvim-linux-x86_64" 78 | } 79 | } 80 | 81 | /// Returns the platform-specific name for the Neovim download. 82 | /// 83 | /// This function takes an `Option` as an argument, which represents the version of Neovim to be downloaded. 84 | /// It checks the target operating system and architecture using the `cfg!` macro and returns a string that corresponds to the appropriate Neovim download for the platform. 85 | /// For Windows, it returns "nvim-win64". 86 | /// For macOS, it checks the version of Neovim. If the version is less than or equal to 0.9.5, it returns "nvim-macos". If the target architecture is "aarch64", it returns "nvim-macos-arm64". Otherwise, it returns "nvim-macos-x86_64". 87 | /// For Linux, it checks the version of Neovim. If the version is less than or equal to 0.10.3, it returns "nvim". If the target architecture is "aarch64", it returns "nvim-linux-arm64". Otherwise, it returns "nvim-linux-x86_64". 88 | /// 89 | /// # Arguments 90 | /// 91 | /// * `version` - An `Option` representing the version of Neovim to be downloaded. 92 | /// 93 | /// # Returns 94 | /// 95 | /// This function returns a `&'static str` that corresponds to the platform-specific name for the Neovim download. 96 | /// 97 | /// # Example 98 | /// 99 | /// ```rust 100 | /// let version = Some(Version::new(0, 9, 5)); 101 | /// let platform_name = get_platform_name_download(&version); 102 | /// ``` 103 | pub fn get_platform_name_download(version: &Option) -> &'static str { 104 | let version_ref = version.as_ref(); 105 | 106 | let is_macos_legacy = version_ref.is_some_and(|v| v <= &Version::new(0, 9, 5)); 107 | let is_linux_legacy = version_ref.is_some_and(|v| v <= &Version::new(0, 10, 3)); 108 | 109 | if cfg!(target_os = "windows") { 110 | "nvim-win64" 111 | } else if cfg!(target_os = "macos") && is_macos_legacy { 112 | "nvim-macos" 113 | } else if cfg!(target_os = "macos") && cfg!(target_arch = "aarch64") { 114 | "nvim-macos-arm64" 115 | } else if cfg!(target_os = "macos") { 116 | "nvim-macos-x86_64" 117 | } else if is_linux_legacy { 118 | "nvim" 119 | } else if cfg!(target_arch = "aarch64") { 120 | "nvim-linux-arm64" 121 | } else { 122 | "nvim-linux-x86_64" 123 | } 124 | } 125 | 126 | #[cfg(test)] 127 | mod tests { 128 | 129 | #[test] 130 | fn get_platform_name_none() { 131 | if cfg!(target_os = "windows") { 132 | assert_eq!(super::get_platform_name(&None), "nvim-win64"); 133 | } else if cfg!(target_os = "macos") && cfg!(target_arch = "aarch64") { 134 | assert_eq!(super::get_platform_name(&None), "nvim-macos-arm64"); 135 | assert_eq!(super::get_platform_name_download(&None), "nvim-macos-arm64"); 136 | } else if cfg!(target_os = "macos") && cfg!(target_arch = "x86_64") { 137 | assert_eq!(super::get_platform_name(&None), "nvim-macos-x86_64"); 138 | assert_eq!( 139 | super::get_platform_name_download(&None), 140 | "nvim-macos-x86_64" 141 | ); 142 | } else if cfg!(target_arch = "aarch64") { 143 | assert_eq!(super::get_platform_name(&None), "nvim-linux-arm64"); 144 | assert_eq!(super::get_platform_name_download(&None), "nvim-linux-arm64"); 145 | } else { 146 | assert_eq!(super::get_platform_name(&None), "nvim-linux-x86_64"); 147 | assert_eq!( 148 | super::get_platform_name_download(&None), 149 | "nvim-linux-x86_64" 150 | ); 151 | } 152 | } 153 | 154 | #[test] 155 | fn get_platform_name_lower() { 156 | let version = Some(semver::Version::new(0, 9, 5)); 157 | if cfg!(target_os = "windows") { 158 | assert_eq!(super::get_platform_name(&version), "nvim-win64"); 159 | } else if cfg!(target_os = "macos") { 160 | assert_eq!(super::get_platform_name(&version), "nvim-macos"); 161 | assert_eq!(super::get_platform_name_download(&version), "nvim-macos"); 162 | } else { 163 | assert_eq!(super::get_platform_name(&version), "nvim-linux64"); 164 | assert_eq!(super::get_platform_name_download(&version), "nvim"); 165 | } 166 | } 167 | 168 | #[test] 169 | fn get_platform_name_higher() { 170 | let version = Some(semver::Version::new(0, 10, 5)); 171 | if cfg!(target_os = "windows") { 172 | assert_eq!(super::get_platform_name(&version), "nvim-win64"); 173 | } else if cfg!(target_os = "macos") && cfg!(target_arch = "aarch64") { 174 | assert_eq!(super::get_platform_name(&version), "nvim-macos-arm64"); 175 | assert_eq!( 176 | super::get_platform_name_download(&version), 177 | "nvim-macos-arm64" 178 | ); 179 | } else if cfg!(target_os = "macos") && cfg!(target_arch = "x86_64") { 180 | assert_eq!(super::get_platform_name(&version), "nvim-macos-x86_64"); 181 | assert_eq!( 182 | super::get_platform_name_download(&version), 183 | "nvim-macos-x86_64" 184 | ); 185 | } else if cfg!(target_arch = "aarch64") { 186 | assert_eq!(super::get_platform_name(&version), "nvim-linux-arm64"); 187 | assert_eq!( 188 | super::get_platform_name_download(&version), 189 | "nvim-linux-arm64" 190 | ); 191 | } else { 192 | assert_eq!(super::get_platform_name(&version), "nvim-linux-x86_64"); 193 | assert_eq!( 194 | super::get_platform_name_download(&version), 195 | "nvim-linux-x86_64" 196 | ); 197 | } 198 | } 199 | } 200 | -------------------------------------------------------------------------------- /src/helpers/processes.rs: -------------------------------------------------------------------------------- 1 | use crate::config::Config; 2 | use anyhow::{anyhow, Result}; 3 | use std::time::Duration; 4 | use tokio::{process::Command, time::sleep}; 5 | 6 | use super::{ 7 | directories, get_platform_name, 8 | version::{self, is_hash}, 9 | }; 10 | 11 | /// Handles the execution of a subprocess. 12 | /// 13 | /// This function takes a mutable reference to a `Command` struct, which represents the subprocess to be executed. 14 | /// It then awaits the status of the subprocess. 15 | /// If the subprocess exits with a status code of `0`, the function returns `Ok(())`. 16 | /// If the subprocess exits with a non-zero status code, the function returns an error with the status code as the error message. 17 | /// If the subprocess is terminated by a signal, the function returns an error with the message "process terminated by signal". 18 | /// 19 | /// # Arguments 20 | /// 21 | /// * `process` - A mutable reference to a `Command` struct representing the subprocess to be executed. 22 | /// 23 | /// # Returns 24 | /// 25 | /// This function returns a `Result` that indicates whether the operation was successful. 26 | /// If the operation was successful, the function returns `Ok(())`. 27 | /// If the operation failed, the function returns `Err` with a description of the error. 28 | /// 29 | /// # Errors 30 | /// 31 | /// This function will return an error if: 32 | /// 33 | /// * The subprocess exits with a non-zero status code. 34 | /// * The subprocess is terminated by a signal. 35 | /// 36 | /// # Example 37 | /// 38 | /// ```rust 39 | /// let mut process = Command::new("ls"); 40 | /// handle_subprocess(&mut process).await; 41 | /// ``` 42 | pub async fn handle_subprocess(process: &mut Command) -> Result<()> { 43 | match process.status().await?.code() { 44 | Some(0) => Ok(()), 45 | Some(code) => Err(anyhow!(code)), 46 | None => Err(anyhow!("process terminated by signal")), 47 | } 48 | } 49 | 50 | /// Handles the execution of the Neovim process. 51 | /// 52 | /// This function takes a reference to a `Config` struct and a slice of `String` arguments. 53 | /// It retrieves the downloads directory and the currently used version of Neovim from the configuration. 54 | /// It then constructs the path to the Neovim binary and executes it with the given arguments. 55 | /// 56 | /// On Unix systems, this function uses `exec` to replace the current process with Neovim. 57 | /// On Windows, it spawns a new process and monitors its execution. 58 | /// 59 | /// If running on Windows and the process exits with a non-zero status code, returns an error with the status code. 60 | /// If the process is terminated by a signal on Windows, returns an error with "Process terminated by signal". 61 | /// 62 | /// # Arguments 63 | /// 64 | /// * `config` - A reference to a `Config` struct containing the configuration for the Neovim process. 65 | /// * `args` - A slice of `String` arguments to be passed to the Neovim process. 66 | /// 67 | /// # Returns 68 | /// 69 | /// This function returns a `Result` that indicates whether the operation was successful. 70 | /// If the operation was successful, the function returns `Ok(())`. 71 | /// If the operation failed, the function returns `Err` with a description of the error. 72 | /// 73 | /// # Errors 74 | /// 75 | /// This function will return an error if: 76 | /// 77 | /// * The Neovim process exits with a non-zero status code. 78 | /// * The Neovim process is terminated by a signal. 79 | /// * The function fails to wait on the child process. 80 | /// 81 | /// # Example 82 | /// 83 | /// ```rust 84 | /// let config = Config::default(); 85 | /// let args = vec!["-v".to_string()]; 86 | /// handle_nvim_process(&config, &args).await; 87 | /// ``` 88 | pub async fn handle_nvim_process(config: &Config, args: &[String]) -> Result<()> { 89 | let downloads_dir = directories::get_downloads_directory(config).await?; 90 | let used_version = version::get_current_version(config).await?; 91 | let version = semver::Version::parse(&used_version.replace('v', "")).ok(); 92 | let platform = get_platform_name(&version); 93 | 94 | let new_version: String = if is_hash(&used_version) { 95 | used_version.chars().take(7).collect() 96 | } else { 97 | used_version 98 | }; 99 | 100 | let mut location = downloads_dir.join(&new_version).join("bin").join("nvim"); 101 | 102 | if cfg!(windows) { 103 | location = location.with_extension("exe"); 104 | } 105 | 106 | if !location.exists() { 107 | location = downloads_dir 108 | .join(new_version) 109 | .join(platform) 110 | .join("bin") 111 | .join("nvim"); 112 | 113 | if cfg!(windows) { 114 | location = location.with_extension("exe"); 115 | } 116 | } 117 | 118 | let mut child = std::process::Command::new(location); 119 | child.args(args); 120 | 121 | // On Unix, replace the current process with nvim 122 | if cfg!(unix) { 123 | #[cfg(unix)] 124 | { 125 | use std::os::unix::process::CommandExt; 126 | let err = child.exec(); 127 | return Err(anyhow!("Failed to exec neovim: {}", err)); 128 | } 129 | } 130 | 131 | let mut spawned_child = child.spawn()?; 132 | 133 | loop { 134 | let child_done = spawned_child.try_wait(); 135 | match child_done { 136 | Ok(Some(status)) => match status.code() { 137 | Some(0) => return Ok(()), 138 | Some(code) => return Err(anyhow!("Process exited with error code {}", code)), 139 | None => return Err(anyhow!("Process terminated by signal")), 140 | }, 141 | Ok(None) => { 142 | // short delay to avoid high cpu usage 143 | sleep(Duration::from_millis(200)).await; 144 | } 145 | Err(_) => return Err(anyhow!("Failed to wait on child process")), 146 | } 147 | } 148 | } 149 | -------------------------------------------------------------------------------- /src/helpers/sync.rs: -------------------------------------------------------------------------------- 1 | #[cfg(target_os = "linux")] 2 | use anyhow::{anyhow, Result}; 3 | #[cfg(target_os = "linux")] 4 | use std::process::{Command, Stdio}; 5 | 6 | /// Handles the execution of a subprocess. 7 | /// 8 | /// This function takes a mutable reference to a `Command` struct, which represents the subprocess to be executed. 9 | /// It sets the standard output of the subprocess to `null` and then executes the subprocess. 10 | /// If the subprocess exits with a status code of `0`, the function returns `Ok(())`. 11 | /// If the subprocess exits with a non-zero status code, the function returns an error with the status code as the error message. 12 | /// If the subprocess is terminated by a signal, the function returns an error with the message "process terminated by signal". 13 | /// 14 | /// # Arguments 15 | /// 16 | /// * `process` - A mutable reference to a `Command` struct representing the subprocess to be executed. 17 | /// 18 | /// # Returns 19 | /// 20 | /// This function returns a `Result` that indicates whether the operation was successful. 21 | /// If the operation was successful, the function returns `Ok(())`. 22 | /// If the operation failed, the function returns `Err` with a description of the error. 23 | /// 24 | /// # Errors 25 | /// 26 | /// This function will return an error if: 27 | /// 28 | /// * The subprocess exits with a non-zero status code. 29 | /// * The subprocess is terminated by a signal. 30 | /// 31 | /// # Example 32 | /// 33 | /// ```rust 34 | /// let mut process = Command::new("ls"); 35 | /// handle_subprocess(&mut process); 36 | /// ``` 37 | #[cfg(target_os = "linux")] 38 | pub fn handle_subprocess(process: &mut Command) -> Result<()> { 39 | match process.stdout(Stdio::null()).status()?.code() { 40 | Some(0) => Ok(()), 41 | Some(code) => Err(anyhow!(code)), 42 | None => Err(anyhow!("process terminated by signal")), 43 | } 44 | } 45 | -------------------------------------------------------------------------------- /src/helpers/unarchive.rs: -------------------------------------------------------------------------------- 1 | use anyhow::{anyhow, Result}; 2 | use std::{ 3 | fs, 4 | path::{Path, PathBuf}, 5 | }; 6 | 7 | use super::version::types::LocalVersion; 8 | 9 | /// Starts the process of expanding a downloaded file. 10 | /// 11 | /// This function is asynchronous and uses `tokio::task::spawn_blocking` to run the `expand` function in a separate thread. 12 | /// It takes a `LocalVersion` struct which contains information about the downloaded file, such as its name, format, and path. 13 | /// The function first clones the `LocalVersion` struct and passes it to the `expand` function. 14 | /// If the `expand` function returns an error, the `start` function also returns an error. 15 | /// If the `expand` function is successful, the `start` function removes the original downloaded file. 16 | /// 17 | /// # Arguments 18 | /// 19 | /// * `file` - A `LocalVersion` struct representing the downloaded file. 20 | /// 21 | /// # Returns 22 | /// 23 | /// This function returns a `Result` that indicates whether the operation was successful. 24 | /// If the operation was successful, the function returns `Ok(())`. 25 | /// If the operation failed, the function returns `Err` with a description of the error. 26 | /// 27 | /// # Errors 28 | /// 29 | /// This function will return an error if: 30 | /// 31 | /// * The `expand` function returns an error. 32 | /// * The original downloaded file could not be removed. 33 | /// 34 | /// # Example 35 | /// 36 | /// ```rust 37 | /// let downloaded_file = LocalVersion { 38 | /// file_name: "nvim-linux", 39 | /// file_format: "AppImage", 40 | /// semver: semver::Version::parse("0.5.0").unwrap(), 41 | /// path: "/path/to/downloaded/file", 42 | /// }; 43 | /// start(downloaded_file).await; 44 | /// ``` 45 | pub async fn start(file: LocalVersion) -> Result<()> { 46 | let temp_file = file.clone(); 47 | match tokio::task::spawn_blocking(move || match expand(temp_file) { 48 | Ok(_) => Ok(()), 49 | Err(error) => Err(anyhow!(error)), 50 | }) 51 | .await 52 | { 53 | Ok(_) => (), 54 | Err(error) => return Err(anyhow!(error)), 55 | } 56 | tokio::fs::remove_file(format!( 57 | "{}/{}.{}", 58 | file.path, file.file_name, file.file_format 59 | )) 60 | .await?; 61 | Ok(()) 62 | } 63 | 64 | /// Expands a downloaded file on Linux. 65 | /// 66 | /// This function is specific to Linux due to the use of certain features like `os::unix::fs::PermissionsExt`. 67 | /// It takes a `LocalVersion` struct which contains information about the downloaded file, such as its name and format. 68 | /// The function then checks if a directory with the same name as the downloaded file exists, and if so, removes it. 69 | /// It then sets the permissions of the downloaded file to `0o551` and extracts its contents using the `--appimage-extract` command. 70 | /// After extraction, the function renames the `squashfs-root` directory to the name of the downloaded file and changes the current directory to the renamed directory. 71 | /// It then removes certain files and renames the `usr` directory to `nvim-linux64`. 72 | /// Finally, it changes the current directory back to the parent directory. 73 | /// 74 | /// # Arguments 75 | /// 76 | /// * `downloaded_file` - A `LocalVersion` struct representing the downloaded file. 77 | /// 78 | /// # Returns 79 | /// 80 | /// This function returns a `Result` that indicates whether the operation was successful. 81 | /// If the operation was successful, the function returns `Ok(())`. 82 | /// If the operation failed, the function returns `Err` with a description of the error. 83 | /// 84 | /// # Errors 85 | /// 86 | /// This function will return an error if: 87 | /// 88 | /// * A directory with the same name as the downloaded file could not be removed. 89 | /// * The permissions of the downloaded file could not be set. 90 | /// * The downloaded file could not be extracted. 91 | /// * The `squashfs-root` directory could not be renamed. 92 | /// * The current directory could not be changed. 93 | /// * Certain files could not be removed. 94 | /// * The `usr` directory could not be renamed. 95 | /// 96 | /// # Example 97 | /// 98 | /// ```rust 99 | /// let downloaded_file = LocalVersion { 100 | /// file_name: "nvim-linux", 101 | /// file_format: "AppImage", 102 | /// semver: semver::Version::parse("0.5.0").unwrap(), 103 | /// path: "/path/to/downloaded/file", 104 | /// }; 105 | /// expand(downloaded_file); 106 | /// ``` 107 | #[cfg(target_os = "linux")] 108 | fn expand(downloaded_file: LocalVersion) -> Result<()> { 109 | use crate::helpers::filesystem::copy_dir; 110 | 111 | use super::sync; 112 | use std::fs::remove_dir_all; 113 | use std::os::unix::fs::PermissionsExt; 114 | use std::process::Command; 115 | 116 | if fs::metadata(&downloaded_file.file_name).is_ok() { 117 | fs::remove_dir_all(&downloaded_file.file_name)?; 118 | } 119 | 120 | let file = &format!( 121 | "./{}.{}", 122 | downloaded_file.file_name, downloaded_file.file_format 123 | ); 124 | let mut perms = fs::metadata(file)?.permissions(); 125 | perms.set_mode(0o551); 126 | fs::set_permissions(file, perms)?; 127 | 128 | sync::handle_subprocess(Command::new(file).arg("--appimage-extract"))?; 129 | 130 | let src_root = "squashfs-root"; 131 | let dest = downloaded_file.file_name; 132 | 133 | copy_dir(Path::new(src_root).join("usr"), Path::new(&dest))?; 134 | remove_dir_all(src_root)?; 135 | 136 | Ok(()) 137 | } 138 | 139 | /// Expands a downloaded file on Windows. 140 | /// 141 | /// This function is specific to Windows due to the use of certain features like `zip::ZipArchive`. 142 | /// It takes a `LocalVersion` struct which contains information about the downloaded file, such as its name and format. 143 | /// The function then opens the file and extracts its contents using `zip::ZipArchive`. 144 | /// During the extraction process, a progress bar is displayed to the user. 145 | /// After extraction, the function removes the original zip file. 146 | /// 147 | /// # Arguments 148 | /// 149 | /// * `downloaded_file` - A `LocalVersion` struct representing the downloaded file. 150 | /// 151 | /// # Returns 152 | /// 153 | /// This function returns a `Result` that indicates whether the operation was successful. 154 | /// If the operation was successful, the function returns `Ok(())`. 155 | /// If the operation failed, the function returns `Err` with a description of the error. 156 | /// 157 | /// # Errors 158 | /// 159 | /// This function will return an error if: 160 | /// 161 | /// * The downloaded file could not be opened. 162 | /// * The file could not be extracted. 163 | /// * The original zip file could not be removed. 164 | /// 165 | /// # Example 166 | /// 167 | /// ```rust 168 | /// let downloaded_file = LocalVersion { 169 | /// file_name: "nvim-windows", 170 | /// file_format: "zip", 171 | /// semver: semver::Version::parse("0.5.0").unwrap(), 172 | /// path: "/path/to/downloaded/file", 173 | /// }; 174 | /// expand(downloaded_file); 175 | /// ``` 176 | #[cfg(target_family = "windows")] 177 | fn expand(downloaded_file: LocalVersion) -> Result<()> { 178 | use indicatif::{ProgressBar, ProgressStyle}; 179 | use std::cmp::min; 180 | use std::fs::File; 181 | use std::io; 182 | use std::path::Path; 183 | use zip::ZipArchive; 184 | 185 | if fs::metadata(&downloaded_file.file_name).is_ok() { 186 | fs::remove_dir_all(&downloaded_file.file_name)?; 187 | } 188 | 189 | let file = File::open(format!( 190 | "{}.{}", 191 | downloaded_file.file_name, downloaded_file.file_format 192 | ))?; 193 | 194 | let mut archive = ZipArchive::new(file)?; 195 | let totalsize: u64 = archive.len() as u64; 196 | 197 | let pb = ProgressBar::new(totalsize); 198 | pb.set_style( 199 | ProgressStyle::default_bar() 200 | .template( 201 | "{msg}\n{spinner:.green} [{elapsed_precise}] [{wide_bar:.cyan/blue}] {pos}/{len}", 202 | ) 203 | .progress_chars("█ "), 204 | ); 205 | pb.set_message("Expanding archive"); 206 | 207 | std::fs::create_dir(downloaded_file.file_name.clone())?; 208 | 209 | let mut downloaded: u64 = 0; 210 | 211 | for i in 0..archive.len() { 212 | let mut file = archive.by_index(i)?; 213 | let file_path = remove_base_parent(&file.mangled_name()).unwrap(); 214 | let outpath = Path::new(&downloaded_file.file_name).join(file_path); 215 | 216 | if file.is_dir() { 217 | fs::create_dir_all(outpath)?; 218 | } else { 219 | if let Some(parent) = outpath.parent() { 220 | if !parent.exists() { 221 | fs::create_dir_all(parent)?; 222 | } 223 | } 224 | let mut outfile = fs::File::create(outpath)?; 225 | io::copy(&mut file, &mut outfile)?; 226 | } 227 | let new = min(downloaded + 1, totalsize); 228 | downloaded = new; 229 | pb.set_position(new); 230 | } 231 | pb.finish_with_message(format!( 232 | "Finished unzipping to {}/{}", 233 | downloaded_file.path, downloaded_file.file_name 234 | )); 235 | 236 | Ok(()) 237 | } 238 | 239 | /// Expands a downloaded file on macOS. 240 | /// 241 | /// This function is specific to macOS due to the use of certain features like `os::unix::fs::PermissionsExt`. 242 | /// It takes a `LocalVersion` struct which contains information about the downloaded file, such as its name and format. 243 | /// The function then opens the file, decompresses it using `GzDecoder`, and extracts its contents using `tar::Archive`. 244 | /// During the extraction process, a progress bar is displayed to the user. 245 | /// After extraction, the function renames the `nvim-osx64` directory to `nvim-macos` if it exists. 246 | /// Finally, it sets the permissions of the `nvim` binary to `0o551`. 247 | /// 248 | /// # Arguments 249 | /// 250 | /// * `downloaded_file` - A `LocalVersion` struct representing the downloaded file. 251 | /// 252 | /// # Returns 253 | /// 254 | /// This function returns a `Result` that indicates whether the operation was successful. 255 | /// If the operation was successful, the function returns `Ok(())`. 256 | /// If the operation failed, the function returns `Err` with a description of the error. 257 | /// 258 | /// # Errors 259 | /// 260 | /// This function will return an error if: 261 | /// 262 | /// * The downloaded file could not be opened. 263 | /// * The file could not be decompressed or extracted. 264 | /// * The `nvim-osx64` directory could not be renamed. 265 | /// * The permissions of the `nvim` binary could not be set. 266 | /// 267 | /// # Example 268 | /// 269 | /// ```rust 270 | /// let downloaded_file = LocalVersion { 271 | /// file_name: "nvim-macos", 272 | /// file_format: "tar.gz", 273 | /// semver: semver::Version::parse("0.5.0").unwrap(), 274 | /// path: "/path/to/downloaded/file", 275 | /// }; 276 | /// expand(downloaded_file); 277 | /// ``` 278 | #[cfg(target_os = "macos")] // I don't know if its worth making both expand functions into one function, but the API difference will cause so much if statements 279 | fn expand(downloaded_file: LocalVersion) -> Result<()> { 280 | use flate2::read::GzDecoder; 281 | use indicatif::{ProgressBar, ProgressStyle}; 282 | use std::cmp::min; 283 | use std::fs::File; 284 | use std::io; 285 | use std::{os::unix::fs::PermissionsExt, path::PathBuf}; 286 | use tar::Archive; 287 | 288 | if fs::metadata(&downloaded_file.file_name).is_ok() { 289 | fs::remove_dir_all(&downloaded_file.file_name)?; 290 | } 291 | 292 | let file = match File::open(format!( 293 | "{}.{}", 294 | downloaded_file.file_name, downloaded_file.file_format 295 | )) { 296 | Ok(value) => value, 297 | Err(error) => { 298 | return Err(anyhow!( 299 | "Failed to open file {}.{}, file doesn't exist. additional info: {error}", 300 | downloaded_file.file_name, 301 | downloaded_file.file_format 302 | )) 303 | } 304 | }; 305 | let decompress_stream = GzDecoder::new(file); 306 | let mut archive = Archive::new(decompress_stream); 307 | 308 | let totalsize = 1692; // hard coding this is pretty unwise, but you cant get the length of an archive in tar-rs unlike zip-rs 309 | let pb = ProgressBar::new(totalsize); 310 | pb.set_style( 311 | ProgressStyle::default_bar() 312 | .template( 313 | "{msg}\n{spinner:.green} [{elapsed_precise}] [{wide_bar:.cyan/blue}] {pos}/{len}", 314 | ) 315 | .progress_chars("█ "), 316 | ); 317 | pb.set_message("Expanding archive"); 318 | 319 | let mut downloaded: u64 = 0; 320 | for file in archive.entries()? { 321 | match file { 322 | Ok(mut file) => { 323 | let mut outpath = PathBuf::new(); 324 | outpath.push(&downloaded_file.file_name); 325 | let no_parent_file = remove_base_parent(&file.path().unwrap()).unwrap(); 326 | outpath.push(no_parent_file); 327 | 328 | let file_name = format!("{}", file.path()?.display()); // file.path()?.is_dir() always returns false... weird 329 | if file_name.ends_with('/') { 330 | fs::create_dir_all(outpath)?; 331 | } else { 332 | if let Some(parent) = outpath.parent() { 333 | if !parent.exists() { 334 | fs::create_dir_all(parent)?; 335 | } 336 | } 337 | let mut outfile = fs::File::create(outpath)?; 338 | io::copy(&mut file, &mut outfile)?; 339 | } 340 | let new = min(downloaded + 1, totalsize); 341 | downloaded = new; 342 | pb.set_position(new); 343 | } 344 | Err(error) => println!("{error}"), 345 | } 346 | } 347 | pb.finish_with_message(format!( 348 | "Finished expanding to {}/{}", 349 | downloaded_file.path, downloaded_file.file_name 350 | )); 351 | 352 | let file = &format!("{}/bin/nvim", downloaded_file.file_name); 353 | let mut perms = fs::metadata(file)?.permissions(); 354 | perms.set_mode(0o551); 355 | fs::set_permissions(file, perms)?; 356 | Ok(()) 357 | } 358 | 359 | /// Removes the base parent from a given path. 360 | /// 361 | /// This function takes a path and removes its base parent component. For example, on Windows, 362 | /// if the path is "D:\\test.txt", this function will return "test.txt", effectively removing 363 | /// the drive letter and the root directory. 364 | /// 365 | /// # Arguments 366 | /// 367 | /// * `path` - A reference to a `Path` from which the base parent will be removed. 368 | /// 369 | /// # Returns 370 | /// 371 | /// This function returns an `Option`. If the path has a base parent that can be 372 | /// removed, it returns `Some(PathBuf)` with the modified path. If the path does not have 373 | /// a base parent or cannot be modified, it may return `None`, although in the current 374 | /// implementation, it always returns `Some(PathBuf)` even if the path is unchanged. 375 | /// 376 | /// # Examples 377 | /// 378 | /// Basic usage: 379 | /// 380 | /// ``` 381 | /// use std::path::{Path, PathBuf}; 382 | /// use your_crate_name::remove_base_parent; // Adjust the use path according to your crate's structure 383 | /// 384 | /// let path = Path::new("D:\\test.txt"); 385 | /// let new_path = remove_base_parent(path).unwrap(); 386 | /// assert_eq!(new_path, PathBuf::from("test.txt")); 387 | /// ``` 388 | #[allow(dead_code)] 389 | fn remove_base_parent(path: &Path) -> Option { 390 | let mut components = path.components(); 391 | 392 | components.next(); 393 | 394 | Some(components.as_path().to_path_buf()) 395 | } 396 | -------------------------------------------------------------------------------- /src/helpers/version/mod.rs: -------------------------------------------------------------------------------- 1 | pub mod nightly; 2 | pub mod types; 3 | 4 | use self::types::{ParsedVersion, VersionType}; 5 | use super::directories; 6 | use crate::{ 7 | config::Config, 8 | github_requests::{deserialize_response, RepoCommit, UpstreamVersion}, 9 | }; 10 | use anyhow::{anyhow, Context, Result}; 11 | use regex::Regex; 12 | use reqwest::Client; 13 | use semver::Version; 14 | use std::path::{Path, PathBuf}; 15 | use tokio::{ 16 | fs::{self, File}, 17 | io::AsyncWriteExt, 18 | }; 19 | use tracing::info; 20 | 21 | /// Parses the version type from a version string. 22 | /// 23 | /// This function takes a version string and determines the type of the version. It supports the following version types: `Nightly`, `Latest`, `Hash`, `Normal`, and `NightlyRollback`. 24 | /// 25 | /// # Arguments 26 | /// 27 | /// * `client` - The client to use for fetching the latest version or commit. 28 | /// * `version` - The version string to parse. 29 | /// 30 | /// # Returns 31 | /// 32 | /// * `Result` - Returns a `Result` that contains a `ParsedVersion` struct with the parsed version information, or an error if the operation failed or the version string is not valid. 33 | /// 34 | /// # Errors 35 | /// 36 | /// This function will return an error if: 37 | /// 38 | /// * The version string is not valid. 39 | /// * The latest version or commit cannot be fetched. 40 | /// 41 | /// # Example 42 | /// 43 | /// ```rust 44 | /// let client = Client::new(); 45 | /// let version = "nightly"; 46 | /// let parsed_version = parse_version_type(&client, version).await.unwrap(); 47 | /// println!("The parsed version is {:?}", parsed_version); 48 | /// ``` 49 | pub async fn parse_version_type(client: &Client, version: &str) -> Result { 50 | match version { 51 | "nightly" => Ok(ParsedVersion { 52 | tag_name: version.to_string(), 53 | version_type: VersionType::Nightly, 54 | non_parsed_string: version.to_string(), 55 | semver: None, 56 | }), 57 | "stable" | "latest" => { 58 | info!("Fetching latest version"); 59 | let stable_version = search_stable_version(client).await?; 60 | let cloned_version = stable_version.clone(); 61 | Ok(ParsedVersion { 62 | tag_name: stable_version, 63 | version_type: VersionType::Latest, 64 | non_parsed_string: version.to_string(), 65 | semver: Some(Version::parse(&cloned_version.replace('v', ""))?), 66 | }) 67 | } 68 | "head" | "git" | "HEAD" => { 69 | info!("Fetching latest commit"); 70 | let latest_commit = get_latest_commit(client).await?; 71 | Ok(ParsedVersion { 72 | tag_name: latest_commit.chars().take(7).collect(), 73 | version_type: VersionType::Hash, 74 | non_parsed_string: latest_commit, 75 | semver: None, 76 | }) 77 | } 78 | _ => { 79 | let version_regex = Regex::new(r"^v?[0-9]+\.[0-9]+\.[0-9]+$")?; 80 | if version_regex.is_match(version) { 81 | let mut returned_version = version.to_string(); 82 | if !version.contains('v') { 83 | returned_version.insert(0, 'v'); 84 | } 85 | let cloned_version = returned_version.clone(); 86 | return Ok(ParsedVersion { 87 | tag_name: returned_version, 88 | version_type: VersionType::Normal, 89 | non_parsed_string: version.to_string(), 90 | semver: Some(Version::parse(&cloned_version.replace('v', ""))?), 91 | }); 92 | } else if is_hash(version) { 93 | return Ok(ParsedVersion { 94 | tag_name: version.to_string().chars().take(7).collect(), 95 | version_type: VersionType::Hash, 96 | non_parsed_string: version.to_string(), 97 | semver: None, 98 | }); 99 | } 100 | 101 | let rollback_regex = Regex::new(r"nightly-[a-zA-Z0-9]{7,8}")?; 102 | 103 | if rollback_regex.is_match(version) { 104 | return Ok(ParsedVersion { 105 | tag_name: version.to_string(), 106 | version_type: VersionType::NightlyRollback, 107 | non_parsed_string: version.to_string(), 108 | semver: None, 109 | }); 110 | } 111 | 112 | Err(anyhow!( 113 | "Please provide a proper version string. Valid options are: 114 | 115 | • stable|latest|nightly - Latest stable, most recent, or nightly build 116 | • [v]x.x.x - Specific version (e.g., 0.6.0 or v0.6.0) 117 | • - Specific commit hash" 118 | )) 119 | } 120 | } 121 | } 122 | 123 | /// Checks if a version string is a hash. 124 | /// 125 | /// This function takes a reference to a `str` as an argument and checks if it matches the regular expression `\b[0-9a-f]{5,40}\b`. 126 | /// This regular expression matches a string that contains 5 to 40 hexadecimal characters. 127 | /// The function returns `true` if the version string is a hash and `false` otherwise. 128 | /// 129 | /// # Arguments 130 | /// 131 | /// * `version` - A reference to a `str` that represents the version string to check. 132 | /// 133 | /// # Returns 134 | /// 135 | /// This function returns a `bool` that indicates whether the version string is a hash. 136 | /// 137 | /// # Example 138 | /// 139 | /// ```rust 140 | /// let version = "abc123"; 141 | /// let is_hash = is_hash(version); 142 | /// ``` 143 | pub fn is_hash(version: &str) -> bool { 144 | let hash_regex = Regex::new(r"\b[0-9a-f]{5,40}\b").unwrap(); 145 | hash_regex.is_match(version) 146 | } 147 | 148 | /// Retrieves the location of the version sync file. 149 | /// 150 | /// This function checks the `version_sync_file_location` field of the provided configuration. If the field is `Some`, it checks if a file exists at the specified path. If the file does not exist, it creates a new file at the path. If the field is `None`, it returns `None`. 151 | /// 152 | /// # Arguments 153 | /// 154 | /// * `config` - The configuration to retrieve the `version_sync_file_location` field from. 155 | /// 156 | /// # Returns 157 | /// 158 | /// * `Result>` - Returns a `Result` that contains an `Option` with the `PathBuf` to the version sync file, or `None` if the `version_sync_file_location` field is `None`, or an error if the operation failed. 159 | /// 160 | /// # Errors 161 | /// 162 | /// This function will return an error if: 163 | /// 164 | /// * The file at the specified path cannot be created. 165 | /// 166 | /// # Example 167 | /// 168 | /// ```rust 169 | /// let config = Config::default(); 170 | /// let version_sync_file_location = get_version_sync_file_location(&config).await.unwrap(); 171 | /// println!("The version sync file is located at {:?}", version_sync_file_location); 172 | /// ``` 173 | pub async fn get_version_sync_file_location(config: &Config) -> Result> { 174 | let path = match &config.version_sync_file_location { 175 | Some(path) => { 176 | let path = Path::new(path); 177 | if tokio::fs::metadata(path).await.is_err() { 178 | let mut file = File::create(path).await.context(format!("The path provided, \"{}\", does not exist. Please check the path and try again.", path.parent().unwrap().display()))?; 179 | file.write_all(b"").await?; 180 | } 181 | Some(PathBuf::from(path)) 182 | } 183 | None => return Ok(None), 184 | }; 185 | 186 | Ok(path) 187 | } 188 | 189 | /// Checks if a specific version of Neovim is installed. 190 | /// 191 | /// This function reads the downloads directory and checks if there is a directory with the name matching the specified version. If such a directory is found, it means that the version is installed. 192 | /// 193 | /// # Arguments 194 | /// 195 | /// * `version` - The version to check. 196 | /// * `config` - The configuration to retrieve the downloads directory from. 197 | /// 198 | /// # Returns 199 | /// 200 | /// * `Result` - Returns a `Result` that contains `true` if the version is installed, `false` otherwise, or an error if the operation failed. 201 | /// 202 | /// # Errors 203 | /// 204 | /// This function will return an error if: 205 | /// 206 | /// * The downloads directory cannot be retrieved. 207 | /// * The downloads directory cannot be read. 208 | /// 209 | /// # Example 210 | /// 211 | /// ```rust 212 | /// let config = Config::default(); 213 | /// let version = "1.0.0"; 214 | /// let is_installed = is_version_installed(version, &config).await.unwrap(); 215 | /// println!("Is version {} installed? {}", version, is_installed); 216 | /// ``` 217 | pub async fn is_version_installed(version: &str, config: &Config) -> Result { 218 | let downloads_dir = directories::get_downloads_directory(config).await?; 219 | let mut dir = tokio::fs::read_dir(&downloads_dir).await?; 220 | 221 | while let Some(directory) = dir.next_entry().await? { 222 | let name = directory.file_name().to_str().unwrap().to_owned(); 223 | if !version.eq(&name) { 224 | continue; 225 | } else { 226 | return Ok(true); 227 | } 228 | } 229 | Ok(false) 230 | } 231 | 232 | /// Retrieves the current version of Neovim being used. 233 | /// 234 | /// This function reads the "used" file from the downloads directory, which contains the current version of Neovim being used. If the "used" file cannot be found, it means that Neovim is not installed through bob. 235 | /// 236 | /// # Arguments 237 | /// 238 | /// * `config` - The configuration to retrieve the downloads directory from. 239 | /// 240 | /// # Returns 241 | /// 242 | /// * `Result` - Returns a `Result` that contains the current version of Neovim being used, or an error if the operation failed. 243 | /// 244 | /// # Errors 245 | /// 246 | /// This function will return an error if: 247 | /// 248 | /// * The downloads directory cannot be retrieved. 249 | /// * The "used" file cannot be read. 250 | /// 251 | /// # Example 252 | /// 253 | /// ```rust 254 | /// let config = Config::default(); 255 | /// let current_version = get_current_version(&config).await.unwrap(); 256 | /// println!("The current version is {}", current_version); 257 | pub async fn get_current_version(config: &Config) -> Result { 258 | let mut downloads_dir = directories::get_downloads_directory(config).await?; 259 | downloads_dir.push("used"); 260 | fs::read_to_string(&downloads_dir).await 261 | .map_err(|_| anyhow!("The used file required for bob could not be found. This could mean that Neovim is not installed through bob.")) 262 | } 263 | 264 | /// Checks if a specific version is currently being used. 265 | /// 266 | /// This function retrieves the current version from the configuration and checks if it matches the specified version. 267 | /// 268 | /// # Arguments 269 | /// 270 | /// * `version` - The version to check. 271 | /// * `config` - The configuration to retrieve the current version from. 272 | /// 273 | /// # Returns 274 | /// 275 | /// * `bool` - Returns `true` if the specified version is currently being used, `false` otherwise. 276 | /// 277 | /// # Example 278 | /// 279 | /// ```rust 280 | /// let config = Config::default(); 281 | /// let version = "1.0.0"; 282 | /// let is_used = is_version_used(version, &config).await; 283 | /// println!("Is version {} used? {}", version, is_used); 284 | /// ``` 285 | pub async fn is_version_used(version: &str, config: &Config) -> bool { 286 | match get_current_version(config).await { 287 | Ok(value) => value.eq(version), 288 | Err(_) => false, 289 | } 290 | } 291 | 292 | /// Asynchronously searches for the stable version of Neovim. 293 | /// 294 | /// This function takes a reference to a `Client` as an argument and makes a GitHub API request to get the releases of the Neovim repository. 295 | /// It then deserializes the response into a vector of `UpstreamVersion`. 296 | /// It finds the release that has the tag name "stable" and the release that has the same `target_commitish` as the stable release but does not have the tag name "stable". 297 | /// The function returns the tag name of the found release. 298 | /// 299 | /// # Arguments 300 | /// 301 | /// * `client` - A reference to a `Client` used to make the GitHub API request. 302 | /// 303 | /// # Returns 304 | /// 305 | /// This function returns a `Result` that contains a `String` representing the tag name of the stable version if the operation was successful. 306 | /// If the operation failed, the function returns `Err` with a description of the error. 307 | /// 308 | /// # Example 309 | /// 310 | /// ```rust 311 | /// let client = Client::new(); 312 | /// let stable_version = search_stable_version(&client).await?; 313 | /// ``` 314 | pub async fn search_stable_version(client: &Client) -> Result { 315 | let response = client 316 | .get("https://api.github.com/repos/neovim/neovim/releases?per_page=10") 317 | .header("user-agent", "bob") 318 | .header("Accept", "application/vnd.github.v3+json") 319 | .send() 320 | .await? 321 | .text() 322 | .await?; 323 | 324 | let versions: Vec = deserialize_response(response)?; 325 | let stable_release = versions 326 | .iter() 327 | .find(|v| v.tag_name == "stable") 328 | .ok_or(anyhow!("Cannot find stable release"))?; 329 | let stable_pin_release = versions 330 | .iter() 331 | .find(|v| v.tag_name != "stable" && v.target_commitish == stable_release.target_commitish) 332 | .ok_or(anyhow!("Cannot find version of stable release"))?; 333 | Ok(stable_pin_release.tag_name.clone()) 334 | } 335 | 336 | /// Fetches the latest commit from the Neovim repository on GitHub. 337 | /// 338 | /// This function sends a GET request to the GitHub API to fetch the latest commit from the master branch of the Neovim repository. It then deserializes the response into a `RepoCommit` object and returns the SHA of the commit. 339 | /// 340 | /// # Arguments 341 | /// 342 | /// * `client` - The HTTP client to use for the request. 343 | /// 344 | /// # Returns 345 | /// 346 | /// * `Result` - Returns a `Result` that contains the SHA of the latest commit, or an error if the operation failed. 347 | /// 348 | /// # Errors 349 | /// 350 | /// This function will return an error if: 351 | /// 352 | /// * The GET request to the GitHub API fails. 353 | /// * The response from the GitHub API cannot be deserialized into a `RepoCommit` object. 354 | /// 355 | /// # Example 356 | /// 357 | /// ```rust 358 | /// let client = Client::new(); 359 | /// let latest_commit = get_latest_commit(&client).await.unwrap(); 360 | /// println!("The latest commit is {}", latest_commit); 361 | /// ``` 362 | async fn get_latest_commit(client: &Client) -> Result { 363 | let response = client 364 | .get("https://api.github.com/repos/neovim/neovim/commits/master") 365 | .header("user-agent", "bob") 366 | .header("Accept", "application/vnd.github.v3+json") 367 | .send() 368 | .await? 369 | .text() 370 | .await?; 371 | 372 | let commit: RepoCommit = deserialize_response(response)?; 373 | 374 | Ok(commit.sha) 375 | } 376 | 377 | #[cfg(test)] 378 | mod tests { 379 | use super::*; 380 | 381 | mod is_hash_tests { 382 | use super::*; 383 | 384 | #[test] 385 | fn test_is_hash_with_valid_hash() { 386 | let version = "abc123"; 387 | assert!(is_hash(version)); 388 | } 389 | 390 | #[test] 391 | fn test_is_hash_with_invalid_hash() { 392 | let version = "abc1"; 393 | assert!(!is_hash(version)); 394 | } 395 | 396 | #[test] 397 | fn test_is_hash_with_empty_string() { 398 | let version = ""; 399 | assert!(!is_hash(version)); 400 | } 401 | 402 | #[test] 403 | fn test_is_hash_with_non_hexadecimal_characters() { 404 | let version = "xyz123"; 405 | assert!(!is_hash(version)); 406 | } 407 | 408 | #[test] 409 | fn test_is_hash_with_short_hash() { 410 | let version = "abc1"; 411 | assert!(!is_hash(version)); 412 | } 413 | 414 | #[test] 415 | fn test_is_hash_with_long_hash() { 416 | let version = "abc123abc123abc123abc123abc123abc123abc123"; 417 | assert!(!is_hash(version)); 418 | } 419 | } 420 | } 421 | -------------------------------------------------------------------------------- /src/helpers/version/nightly.rs: -------------------------------------------------------------------------------- 1 | use anyhow::{anyhow, Result}; 2 | use regex::Regex; 3 | use tokio::fs; 4 | 5 | use super::types::LocalNightly; 6 | use crate::{config::Config, github_requests::UpstreamVersion, helpers::directories}; 7 | 8 | /// Retrieves the local nightly version. 9 | /// 10 | /// This function reads the `bob.json` file in the `nightly` directory of the downloads directory and parses it into an `UpstreamVersion` struct. 11 | /// 12 | /// # Arguments 13 | /// 14 | /// * `config` - The configuration to retrieve the downloads directory from. 15 | /// 16 | /// # Returns 17 | /// 18 | /// * `Result` - Returns a `Result` that contains an `UpstreamVersion` struct with the local nightly version, or an error if the operation failed. 19 | /// 20 | /// # Errors 21 | /// 22 | /// This function will return an error if: 23 | /// 24 | /// * The downloads directory cannot be retrieved. 25 | /// * The `bob.json` file cannot be read. 26 | /// * The `bob.json` file cannot be parsed into an `UpstreamVersion` struct. 27 | /// 28 | /// # Example 29 | /// 30 | /// ```rust 31 | /// let config = Config::default(); 32 | /// let local_nightly = get_local_nightly(&config).await.unwrap(); 33 | /// println!("The local nightly version is {:?}", local_nightly); 34 | /// ``` 35 | pub async fn get_local_nightly(config: &Config) -> Result { 36 | let downloads_dir = directories::get_downloads_directory(config).await?; 37 | if let Ok(file) = 38 | fs::read_to_string(format!("{}/nightly/bob.json", downloads_dir.display())).await 39 | { 40 | let file_json: UpstreamVersion = serde_json::from_str(&file)?; 41 | Ok(file_json) 42 | } else { 43 | Err(anyhow!("Couldn't find bob.json")) 44 | } 45 | } 46 | 47 | /// Produces a vector of `LocalNightly` structs from the downloads directory. 48 | /// 49 | /// This function reads the downloads directory and creates a `LocalNightly` struct for each directory that matches the `nightly-[a-zA-Z0-9]{7,8}` pattern. The `LocalNightly` structs are sorted by the `published_at` field in descending order. 50 | /// 51 | /// # Arguments 52 | /// 53 | /// * `config` - The configuration to retrieve the downloads directory from. 54 | /// 55 | /// # Returns 56 | /// 57 | /// * `Result>` - Returns a `Result` that contains a vector of `LocalNightly` structs, or an error if the operation failed. 58 | /// 59 | /// # Errors 60 | /// 61 | /// This function will return an error if: 62 | /// 63 | /// * The downloads directory cannot be retrieved. 64 | /// * The downloads directory cannot be read. 65 | /// * A directory name does not match the `nightly-[a-zA-Z0-9]{7,8}` pattern. 66 | /// * The `bob.json` file in a directory cannot be read. 67 | /// * The `bob.json` file in a directory cannot be parsed into a `UpstreamVersion` struct. 68 | /// 69 | /// # Example 70 | /// 71 | /// ```rust 72 | /// let config = Config::default(); 73 | /// let nightly_vec = produce_nightly_vec(&config).await.unwrap(); 74 | /// println!("There are {} nightly versions.", nightly_vec.len()); 75 | /// ``` 76 | pub async fn produce_nightly_vec(config: &Config) -> Result> { 77 | let downloads_dir = directories::get_downloads_directory(config).await?; 78 | let mut paths = fs::read_dir(&downloads_dir).await?; 79 | 80 | let regex = Regex::new(r"nightly-[a-zA-Z0-9]{7,8}")?; 81 | 82 | let mut nightly_vec: Vec = Vec::new(); 83 | 84 | while let Some(path) = paths.next_entry().await? { 85 | let name = path.file_name().into_string().unwrap(); 86 | 87 | if !regex.is_match(&name) { 88 | continue; 89 | } 90 | 91 | let nightly_content = path.path().join("bob.json"); 92 | let nightly_string = fs::read_to_string(nightly_content).await?; 93 | 94 | let nightly_data: UpstreamVersion = serde_json::from_str(&nightly_string)?; 95 | 96 | let mut nightly_entry = LocalNightly { 97 | data: nightly_data, 98 | path: path.path(), 99 | }; 100 | 101 | nightly_entry.data.tag_name = name; 102 | 103 | nightly_vec.push(nightly_entry); 104 | } 105 | 106 | nightly_vec.sort_by(|a, b| b.data.published_at.cmp(&a.data.published_at)); 107 | 108 | Ok(nightly_vec) 109 | } 110 | -------------------------------------------------------------------------------- /src/helpers/version/types.rs: -------------------------------------------------------------------------------- 1 | use semver::Version; 2 | 3 | use crate::github_requests::UpstreamVersion; 4 | use std::path::PathBuf; 5 | 6 | /// Represents a parsed version of the software. 7 | /// 8 | /// This struct contains information about a parsed version of the software, including the tag name, version type, non-parsed string, and semantic version. 9 | /// 10 | /// # Fields 11 | /// 12 | /// * `tag_name: String` - The tag name of the parsed version. 13 | /// * `version_type: VersionType` - The type of the parsed version. 14 | /// * `non_parsed_string: String` - The non-parsed string of the parsed version. 15 | /// * `semver: Option` - The semantic version of the parsed version, or `None` if the version is not a semantic version. 16 | /// 17 | /// # Example 18 | /// 19 | /// ```rust 20 | /// let parsed_version = ParsedVersion { 21 | /// tag_name: "v1.0.0".to_string(), 22 | /// version_type: VersionType::Normal, 23 | /// non_parsed_string: "version-1.0.0".to_string(), 24 | /// semver: Some(Version::parse("1.0.0").unwrap()), 25 | /// }; 26 | /// println!("The parsed version is {:?}", parsed_version); 27 | /// ``` 28 | pub struct ParsedVersion { 29 | pub tag_name: String, 30 | pub version_type: VersionType, 31 | pub non_parsed_string: String, 32 | pub semver: Option, 33 | } 34 | 35 | /// Represents the type of a software version. 36 | /// 37 | /// This enum is used to distinguish between different types of software versions, such as normal versions, the latest version, nightly versions, versions identified by a hash, and nightly versions that have been rolled back. 38 | /// 39 | /// # Variants 40 | /// 41 | /// * `Normal` - Represents a normal version. 42 | /// * `Latest` - Represents the latest version. 43 | /// * `Nightly` - Represents a nightly version. 44 | /// * `Hash` - Represents a version identified by a hash. 45 | /// * `NightlyRollback` - Represents a nightly version that has been rolled back. 46 | /// 47 | /// # Example 48 | /// 49 | /// ```rust 50 | /// let version_type = VersionType::Nightly; 51 | /// match version_type { 52 | /// VersionType::Normal => println!("This is a normal version."), 53 | /// VersionType::Latest => println!("This is the latest version."), 54 | /// VersionType::Nightly => println!("This is a nightly version."), 55 | /// VersionType::Hash => println!("This is a version identified by a hash."), 56 | /// VersionType::NightlyRollback => println!("This is a nightly version that has been rolled back."), 57 | /// } 58 | /// ``` 59 | #[derive(PartialEq, Eq, Debug)] 60 | pub enum VersionType { 61 | Normal, 62 | Latest, 63 | Nightly, 64 | Hash, 65 | NightlyRollback, 66 | } 67 | 68 | /// Represents a local nightly version of the software. 69 | /// 70 | /// This struct contains information about a local nightly version of the software, including the upstream version data and the path to the version file. 71 | /// 72 | /// # Fields 73 | /// 74 | /// * `data: UpstreamVersion` - The upstream version data for the local nightly version. 75 | /// * `path: PathBuf` - The path to the file that contains the local nightly version. 76 | /// 77 | /// # Example 78 | /// 79 | /// ```rust 80 | /// let upstream_version = UpstreamVersion { 81 | /// // initialize fields 82 | /// }; 83 | /// let local_nightly = LocalNightly { 84 | /// data: upstream_version, 85 | /// path: PathBuf::from("/path/to/nightly/version"), 86 | /// }; 87 | /// println!("The local nightly version is {:?}", local_nightly); 88 | /// ``` 89 | #[derive(Debug, Clone)] 90 | pub struct LocalNightly { 91 | pub data: UpstreamVersion, 92 | pub path: PathBuf, 93 | } 94 | 95 | /// Represents a local version of the software. 96 | /// 97 | /// This struct contains information about a local version of the software, including the file name, file format, path, and semantic version. 98 | /// 99 | /// # Fields 100 | /// 101 | /// * `file_name: String` - The name of the file that contains the local version. 102 | /// * `file_format: String` - The format of the file that contains the local version. 103 | /// * `path: String` - The path to the file that contains the local version. 104 | /// * `semver: Option` - The semantic version of the local version, or `None` if the version is not a semantic version. 105 | /// 106 | /// # Example 107 | /// 108 | /// ```rust 109 | /// let local_version = LocalVersion { 110 | /// file_name: "version-1.0.0.tar.gz".to_string(), 111 | /// file_format: "tar.gz".to_string(), 112 | /// path: "/path/to/version-1.0.0.tar.gz".to_string(), 113 | /// semver: Some(Version::parse("1.0.0").unwrap()), 114 | /// }; 115 | /// println!("The local version is {:?}", local_version); 116 | /// ``` 117 | #[derive(Clone, PartialEq)] 118 | pub struct LocalVersion { 119 | pub file_name: String, 120 | pub file_format: String, 121 | pub path: String, 122 | pub semver: Option, 123 | } 124 | -------------------------------------------------------------------------------- /src/main.rs: -------------------------------------------------------------------------------- 1 | mod cli; 2 | mod config; 3 | pub mod github_requests; 4 | mod handlers; 5 | mod helpers; 6 | 7 | extern crate core; 8 | 9 | use anyhow::Result; 10 | use config::ConfigFile; 11 | use helpers::{processes::handle_nvim_process, version}; 12 | use std::{env, path::Path, process::exit}; 13 | use tracing::{error, Level}; 14 | 15 | #[tokio::main] 16 | async fn main() -> Result<()> { 17 | let collector = tracing_subscriber::fmt() 18 | .with_target(false) 19 | .with_max_level(Level::INFO) 20 | .finish(); 21 | tracing::subscriber::set_global_default(collector)?; 22 | if let Err(error) = run().await { 23 | error!("Error: {error}"); 24 | exit(1); 25 | } 26 | Ok(()) 27 | } 28 | 29 | async fn run() -> Result<()> { 30 | let config = ConfigFile::get().await?; 31 | 32 | let args: Vec = env::args().collect(); 33 | 34 | let exe_name_path = Path::new(&args[0]); 35 | let exe_name = exe_name_path.file_stem().unwrap().to_str().unwrap(); 36 | 37 | let rest_args = &args[1..]; 38 | 39 | if exe_name.eq("nvim") { 40 | if !rest_args.is_empty() && rest_args[0].eq("--&bob") { 41 | print!("{}", env!("CARGO_PKG_VERSION")); 42 | return Ok(()); 43 | } 44 | 45 | handle_nvim_process(&config.config, rest_args).await?; 46 | 47 | return Ok(()); 48 | } 49 | 50 | cli::start(config).await?; 51 | Ok(()) 52 | } 53 | --------------------------------------------------------------------------------