├── .devcontainer └── devcontainer.json ├── .github ├── images │ ├── recent.png │ └── select.png ├── release.yml └── workflows │ ├── ci.yml │ ├── pr-title.yml │ ├── publish.yml │ └── release.yml ├── .gitignore ├── .vscode └── settings.json ├── Cargo.lock ├── Cargo.toml ├── LICENSE ├── README.md ├── renovate.json ├── rust-toolchain.toml ├── src ├── history.rs ├── launch.rs ├── main.rs ├── opts.rs ├── ui.rs ├── uri.rs └── workspace.rs └── tests └── fixtures └── devcontainer.json /.devcontainer/devcontainer.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "Rust", 3 | "image": "mcr.microsoft.com/devcontainers/rust:1-bullseye", 4 | "features": { 5 | "ghcr.io/guiyomh/features/just:0": {} 6 | }, 7 | "customizations": { 8 | "vscode": { 9 | "extensions": [ 10 | "rust-lang.rust-analyzer", 11 | "tamasfe.even-better-toml", 12 | "serayuzgur.crates", 13 | "kokakiwi.vscode-just" 14 | ], 15 | "settings": { 16 | "terminal.integrated.defaultProfile": "zsh" 17 | } 18 | } 19 | } 20 | } 21 | -------------------------------------------------------------------------------- /.github/images/recent.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/michidk/vscli/c61b1712b6047b202fcb388eeb522973e4fa37c7/.github/images/recent.png -------------------------------------------------------------------------------- /.github/images/select.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/michidk/vscli/c61b1712b6047b202fcb388eeb522973e4fa37c7/.github/images/select.png -------------------------------------------------------------------------------- /.github/release.yml: -------------------------------------------------------------------------------- 1 | changelog: 2 | exclude: 3 | labels: 4 | - no-release-note 5 | categories: 6 | - title: Enhancements 7 | labels: 8 | - enhancement 9 | - title: Bugfixes 10 | labels: 11 | - bug 12 | - title: Dependency updates 13 | labels: 14 | - "dependencies" 15 | -------------------------------------------------------------------------------- /.github/workflows/ci.yml: -------------------------------------------------------------------------------- 1 | name: Test & Lint 2 | 3 | on: 4 | push: 5 | paths: 6 | - 'Cargo.lock' 7 | - 'Cargo.toml' 8 | - 'rust-toolchain.toml' 9 | - '.rustfmt.toml' 10 | - 'src/**' 11 | pull_request: 12 | paths: 13 | - 'Cargo.lock' 14 | - 'Cargo.toml' 15 | - 'rust-toolchain.toml' 16 | - '.rustfmt.toml' 17 | - 'src/**' 18 | 19 | env: 20 | CARGO_INCREMENTAL: 0 21 | CARGO_NET_RETRY: 10 22 | RUST_BACKTRACE: short 23 | RUSTFLAGS: "-D warnings -W rust-2021-compatibility" 24 | RUSTUP_MAX_RETRIES: 10 25 | 26 | defaults: 27 | run: 28 | shell: bash 29 | 30 | jobs: 31 | test: 32 | name: Test Suite 33 | runs-on: ubuntu-24.04 34 | steps: 35 | - name: Checkout sources 36 | uses: actions/checkout@v4 37 | 38 | - name: Install toolchain 39 | uses: dtolnay/rust-toolchain@stable 40 | 41 | # This plugin should be loaded after toolchain setup 42 | - name: Cache 43 | uses: Swatinem/rust-cache@v2 44 | 45 | # `test` is used instead of `build` to also include the test modules in 46 | # the compilation check. 47 | - name: Compile 48 | run: cargo test --no-run 49 | 50 | - name: Test 51 | run: cargo test -- --nocapture --quiet 52 | 53 | lints: 54 | name: Lints 55 | runs-on: ubuntu-24.04 56 | steps: 57 | - name: Checkout sources 58 | uses: actions/checkout@v4 59 | 60 | - name: Install toolchain 61 | uses: dtolnay/rust-toolchain@stable 62 | with: 63 | components: rustfmt, clippy 64 | 65 | # This pluging should be loaded after toolchain setup 66 | - name: Cache 67 | uses: Swatinem/rust-cache@v2 68 | 69 | - name: Run cargo fmt 70 | run: cargo fmt --all -- --check --color always 71 | 72 | - name: Run cargo clippy 73 | run: cargo clippy --all-targets --all-features -- -D warnings 74 | -------------------------------------------------------------------------------- /.github/workflows/pr-title.yml: -------------------------------------------------------------------------------- 1 | name: Check PR title 2 | 3 | on: [pull_request_target] 4 | 5 | jobs: 6 | lint: 7 | runs-on: ubuntu-24.04 8 | permissions: 9 | statuses: write 10 | steps: 11 | - uses: aslafy-z/conventional-pr-title-action@v3 12 | env: 13 | GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} 14 | -------------------------------------------------------------------------------- /.github/workflows/publish.yml: -------------------------------------------------------------------------------- 1 | name: Publish 2 | 3 | on: 4 | workflow_dispatch: 5 | inputs: 6 | tag-name: 7 | description: 'The git tag to publish' 8 | required: true 9 | type: string 10 | 11 | jobs: 12 | publish-cratesio: 13 | name: Publish to crates.io 14 | runs-on: ubuntu-24.04 15 | environment: "publish-crates.io" 16 | steps: 17 | - name: Checkout repository 18 | uses: actions/checkout@v4 19 | with: 20 | ref: ${{ github.event.inputs.tag-name }} 21 | 22 | - name: Install toolchain 23 | uses: dtolnay/rust-toolchain@stable 24 | 25 | # This plugin should be loaded after toolchain setup 26 | - name: Cache 27 | uses: Swatinem/rust-cache@v2 28 | 29 | - name: Upload to crates.io 30 | run: cargo publish 31 | env: 32 | CARGO_REGISTRY_TOKEN: ${{ secrets.CARGO_REGISTRY_TOKEN }} 33 | 34 | publish-homebrew: 35 | name: Publish to Homebrew 36 | runs-on: ubuntu-24.04 37 | environment: "publish-homebrew" 38 | steps: 39 | - name: Checkout homebrew-tools Repository 40 | uses: actions/checkout@v4 41 | with: 42 | repository: michidk/homebrew-tools 43 | token: ${{ secrets.COMMITTER_TOKEN }} 44 | path: homebrew-tools 45 | ref: main 46 | 47 | - name: Update vscli.rb Formula 48 | run: | 49 | FORMULA_PATH="homebrew-tools/Formula/vscli.rb" 50 | mkdir -p artifacts 51 | 52 | artifacts=( 53 | "vscli-x86_64-apple-darwin.tar.gz" 54 | "vscli-aarch64-apple-darwin.tar.gz" 55 | "vscli-x86_64-unknown-linux-musl.tar.gz" 56 | "vscli-arm-unknown-linux-gnueabihf.tar.gz" 57 | ) 58 | 59 | for asset in "${artifacts[@]}"; do 60 | identifier="${asset%.tar.gz}" 61 | wget -q -O "artifacts/${asset}" "https://github.com/michidk/vscli/releases/download/${{ github.event.inputs.tag-name }}/${asset}" || exit 1 62 | sha256=$(sha256sum "artifacts/${asset}" | awk '{print $1}') 63 | sed -i "s|sha256 \".*\" # sha:${identifier}|sha256 \"${sha256}\" # sha:${identifier}|" "$FORMULA_PATH" 64 | done 65 | 66 | # Extract version number by removing the leading 'v' from the tag 67 | version_number="${{ github.event.inputs.tag-name }}" 68 | version_number="${version_number#v}" 69 | 70 | sed -i "s/version \".*\"/version \"${version_number}\"/" "$FORMULA_PATH" 71 | 72 | - name: Commit and Push Changes 73 | run: | 74 | cd homebrew-tools 75 | git config user.name "github-actions[bot]" 76 | git config user.email "github-actions[bot]@users.noreply.github.com" 77 | git add Formula/vscli.rb 78 | git commit -m "Update vscli to version ${{ github.event.inputs.tag-name }}" 79 | git push origin main 80 | -------------------------------------------------------------------------------- /.github/workflows/release.yml: -------------------------------------------------------------------------------- 1 | 2 | name: Release 3 | 4 | # References: 5 | # - https://github.com/BurntSushi/ripgrep/blob/master/.github/workflows/release.yml 6 | 7 | on: 8 | push: 9 | tags: 10 | - 'v[0-9]+.[0-9]+.[0-9]+' 11 | 12 | permissions: 13 | contents: write 14 | 15 | defaults: 16 | run: 17 | shell: bash 18 | 19 | env: 20 | APP_NAME: vscli 21 | 22 | jobs: 23 | create-release: 24 | name: Create release 25 | runs-on: ubuntu-24.04 26 | outputs: 27 | upload_url: ${{ steps.release.outputs.upload_url }} 28 | steps: 29 | - name: Get release version 30 | if: env.VERSION == '' 31 | run: | 32 | # Get the version from github tag 33 | echo "VERSION=${GITHUB_REF#refs/tags/v}" >> $GITHUB_ENV 34 | echo "Version: ${{ env.VERSION }}" 35 | 36 | - name: Create release 37 | id: release 38 | uses: mikepenz/action-gh-release@v1 39 | if: startsWith(github.ref, 'refs/tags/') 40 | with: 41 | name: ${{ env.VERSION }} 42 | draft: true 43 | generate_release_notes: true 44 | 45 | build-release: 46 | name: Build release 47 | needs: ['create-release'] 48 | runs-on: ${{ matrix.os }} 49 | env: 50 | # Build tool. For some builds this can be cross. 51 | CARGO: cargo 52 | # When `CARGO` is set to `cross` this will be set to `--target {{matrix.target}}`. 53 | TARGET_FLAGS: "" 54 | # When `CARGO` is set to `cross` this will be set to `./target/{{matrix.target}}`. 55 | TARGET_DIR: ./target 56 | # Get backtraces on panics. 57 | RUST_BACKTRACE: 1 58 | strategy: 59 | matrix: 60 | include: 61 | - build: linux 62 | os: ubuntu-latest 63 | target: x86_64-unknown-linux-musl 64 | - build: linux-arm 65 | os: ubuntu-latest 66 | target: arm-unknown-linux-gnueabihf 67 | - build: macos 68 | os: macos-latest 69 | target: x86_64-apple-darwin 70 | - build: macos-arm 71 | os: macos-latest 72 | target: aarch64-apple-darwin 73 | - build: win32-msvc 74 | os: windows-latest 75 | target: i686-pc-windows-msvc 76 | - build: win-msvc 77 | os: windows-latest 78 | target: x86_64-pc-windows-msvc 79 | steps: 80 | - name: Checkout repository 81 | uses: actions/checkout@v4 82 | 83 | - name: Install toolchain 84 | uses: dtolnay/rust-toolchain@stable 85 | with: 86 | targets: ${{ matrix.target }} 87 | 88 | - name: Setup Cross 89 | if: matrix.target != '' 90 | run: | 91 | cargo install cross 92 | echo "CARGO=cross" >> $GITHUB_ENV 93 | echo "TARGET_FLAGS=--target ${{ matrix.target }}" >> $GITHUB_ENV 94 | echo "TARGET_DIR=./target/${{ matrix.target }}" >> $GITHUB_ENV 95 | 96 | - name: Show command used for Cargo 97 | run: | 98 | echo "cargo command is: ${{ env.CARGO }}" 99 | echo "target flag is: ${{ env.TARGET_FLAGS }}" 100 | echo "target dir is: ${{ env.TARGET_DIR }}" 101 | 102 | - name: Build release binary without features 103 | run: ${{ env.CARGO }} build --release --verbose ${{ env.TARGET_FLAGS }} 104 | 105 | - name: Strip release binary (linux) 106 | if: matrix.build == 'linux' || matrix.os == 'macos' 107 | run: strip "${{ env.TARGET_DIR }}/release/${{ env.APP_NAME }}" 108 | 109 | - name: Strip release binary (arm) 110 | if: matrix.build == 'linux-arm' 111 | run: | 112 | docker run --rm -v \ 113 | "$PWD/target:/target:Z" \ 114 | rustembedded/cross:arm-unknown-linux-gnueabihf \ 115 | arm-linux-gnueabihf-strip \ 116 | /target/arm-unknown-linux-gnueabihf/release/${{ env.APP_NAME }} 117 | 118 | - name: Build archive 119 | run: | 120 | staging="${{ env.APP_NAME }}-${{ matrix.target }}" 121 | mkdir -p "$staging" 122 | 123 | if [[ "${{ matrix.os }}" = "windows-latest" ]]; then 124 | echo "Archiving windows build" 125 | cp "${{ env.TARGET_DIR }}/release/${{ env.APP_NAME }}.exe" "$staging/" 126 | 7z a "$staging.zip" "$staging" 127 | echo "ASSET=$staging.zip" >> $GITHUB_ENV 128 | else 129 | echo "Archiving unix build" 130 | cp "${{ env.TARGET_DIR }}/release/${{ env.APP_NAME }}" "$staging/" 131 | tar czf "$staging.tar.gz" "$staging" 132 | echo "ASSET=$staging.tar.gz" >> $GITHUB_ENV 133 | fi 134 | 135 | - name: Upload archive 136 | uses: shogo82148/actions-upload-release-asset@v1 137 | with: 138 | upload_url: ${{ needs.create-release.outputs.upload_url }} 139 | asset_path: ${{ env.ASSET }} 140 | asset_name: ${{ env.ASSET }} 141 | asset_content_type: application/octet-stream 142 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | /target 2 | .history.json 3 | -------------------------------------------------------------------------------- /.vscode/settings.json: -------------------------------------------------------------------------------- 1 | { 2 | "cSpell.words": [ 3 | "behaviour", 4 | "canonicalize", 5 | "Canonicalized", 6 | "canonicalizing", 7 | "crossterm", 8 | "Devcontainer", 9 | "devcontainers", 10 | "monomorphization", 11 | "ratatui", 12 | "vscli", 13 | "VSCLI", 14 | "vscode", 15 | "walkdir", 16 | "WSLENV", 17 | "wslpath" 18 | ], 19 | "cSpell.ignorePaths": [ 20 | "package-lock.json", 21 | "vscode-extension", 22 | ".git/objects", 23 | ".vscode", 24 | ".vscode-insiders", 25 | ".devcontainer", 26 | ".github" 27 | ] 28 | } 29 | -------------------------------------------------------------------------------- /Cargo.lock: -------------------------------------------------------------------------------- 1 | # This file is automatically @generated by Cargo. 2 | # It is not intended for manual editing. 3 | version = 4 4 | 5 | [[package]] 6 | name = "addr2line" 7 | version = "0.21.0" 8 | source = "registry+https://github.com/rust-lang/crates.io-index" 9 | checksum = "8a30b2e23b9e17a9f90641c7ab1549cd9b44f296d3ccbf309d2863cfe398a0cb" 10 | dependencies = [ 11 | "gimli", 12 | ] 13 | 14 | [[package]] 15 | name = "adler" 16 | version = "1.0.2" 17 | source = "registry+https://github.com/rust-lang/crates.io-index" 18 | checksum = "f26201604c87b1e01bd3d98f8d5d9a8fcbb815e8cedb41ffccbeb4bf593a35fe" 19 | 20 | [[package]] 21 | name = "aho-corasick" 22 | version = "1.1.3" 23 | source = "registry+https://github.com/rust-lang/crates.io-index" 24 | checksum = "8e60d3430d3a69478ad0993f19238d2df97c507009a52b3c10addcd7f6bcb916" 25 | dependencies = [ 26 | "memchr", 27 | ] 28 | 29 | [[package]] 30 | name = "allocator-api2" 31 | version = "0.2.21" 32 | source = "registry+https://github.com/rust-lang/crates.io-index" 33 | checksum = "683d7910e743518b0e34f1186f92494becacb047c7b6bf616c96772180fef923" 34 | 35 | [[package]] 36 | name = "android-tzdata" 37 | version = "0.1.1" 38 | source = "registry+https://github.com/rust-lang/crates.io-index" 39 | checksum = "e999941b234f3131b00bc13c22d06e8c5ff726d1b6318ac7eb276997bbb4fef0" 40 | 41 | [[package]] 42 | name = "android_system_properties" 43 | version = "0.1.5" 44 | source = "registry+https://github.com/rust-lang/crates.io-index" 45 | checksum = "819e7219dbd41043ac279b19830f2efc897156490d7fd6ea916720117ee66311" 46 | dependencies = [ 47 | "libc", 48 | ] 49 | 50 | [[package]] 51 | name = "anstream" 52 | version = "0.6.18" 53 | source = "registry+https://github.com/rust-lang/crates.io-index" 54 | checksum = "8acc5369981196006228e28809f761875c0327210a891e941f4c683b3a99529b" 55 | dependencies = [ 56 | "anstyle", 57 | "anstyle-parse", 58 | "anstyle-query", 59 | "anstyle-wincon", 60 | "colorchoice", 61 | "is_terminal_polyfill", 62 | "utf8parse", 63 | ] 64 | 65 | [[package]] 66 | name = "anstyle" 67 | version = "1.0.10" 68 | source = "registry+https://github.com/rust-lang/crates.io-index" 69 | checksum = "55cc3b69f167a1ef2e161439aa98aed94e6028e5f9a59be9a6ffb47aef1651f9" 70 | 71 | [[package]] 72 | name = "anstyle-parse" 73 | version = "0.2.6" 74 | source = "registry+https://github.com/rust-lang/crates.io-index" 75 | checksum = "3b2d16507662817a6a20a9ea92df6652ee4f94f914589377d69f3b21bc5798a9" 76 | dependencies = [ 77 | "utf8parse", 78 | ] 79 | 80 | [[package]] 81 | name = "anstyle-query" 82 | version = "1.1.2" 83 | source = "registry+https://github.com/rust-lang/crates.io-index" 84 | checksum = "79947af37f4177cfead1110013d678905c37501914fba0efea834c3fe9a8d60c" 85 | dependencies = [ 86 | "windows-sys 0.59.0", 87 | ] 88 | 89 | [[package]] 90 | name = "anstyle-wincon" 91 | version = "3.0.7" 92 | source = "registry+https://github.com/rust-lang/crates.io-index" 93 | checksum = "ca3534e77181a9cc07539ad51f2141fe32f6c3ffd4df76db8ad92346b003ae4e" 94 | dependencies = [ 95 | "anstyle", 96 | "once_cell", 97 | "windows-sys 0.59.0", 98 | ] 99 | 100 | [[package]] 101 | name = "autocfg" 102 | version = "1.4.0" 103 | source = "registry+https://github.com/rust-lang/crates.io-index" 104 | checksum = "ace50bade8e6234aa140d9a2f552bbee1db4d353f69b8217bc503490fc1a9f26" 105 | 106 | [[package]] 107 | name = "backtrace" 108 | version = "0.3.71" 109 | source = "registry+https://github.com/rust-lang/crates.io-index" 110 | checksum = "26b05800d2e817c8b3b4b54abd461726265fa9789ae34330622f2db9ee696f9d" 111 | dependencies = [ 112 | "addr2line", 113 | "cc", 114 | "cfg-if", 115 | "libc", 116 | "miniz_oxide", 117 | "object", 118 | "rustc-demangle", 119 | ] 120 | 121 | [[package]] 122 | name = "bitflags" 123 | version = "1.3.2" 124 | source = "registry+https://github.com/rust-lang/crates.io-index" 125 | checksum = "bef38d45163c2f1dde094a7dfd33ccf595c92905c8f8f4fdc18d06fb1037718a" 126 | 127 | [[package]] 128 | name = "bitflags" 129 | version = "2.8.0" 130 | source = "registry+https://github.com/rust-lang/crates.io-index" 131 | checksum = "8f68f53c83ab957f72c32642f3868eec03eb974d1fb82e453128456482613d36" 132 | 133 | [[package]] 134 | name = "block-buffer" 135 | version = "0.10.4" 136 | source = "registry+https://github.com/rust-lang/crates.io-index" 137 | checksum = "3078c7629b62d3f0439517fa394996acacc5cbc91c5a20d8c658e77abd503a71" 138 | dependencies = [ 139 | "generic-array", 140 | ] 141 | 142 | [[package]] 143 | name = "bumpalo" 144 | version = "3.17.0" 145 | source = "registry+https://github.com/rust-lang/crates.io-index" 146 | checksum = "1628fb46dfa0b37568d12e5edd512553eccf6a22a78e8bde00bb4aed84d5bdbf" 147 | 148 | [[package]] 149 | name = "byteorder" 150 | version = "1.5.0" 151 | source = "registry+https://github.com/rust-lang/crates.io-index" 152 | checksum = "1fd0f2584146f6f2ef48085050886acf353beff7305ebd1ae69500e27c67f64b" 153 | 154 | [[package]] 155 | name = "cassowary" 156 | version = "0.3.0" 157 | source = "registry+https://github.com/rust-lang/crates.io-index" 158 | checksum = "df8670b8c7b9dae1793364eafadf7239c40d669904660c5960d74cfd80b46a53" 159 | 160 | [[package]] 161 | name = "castaway" 162 | version = "0.2.3" 163 | source = "registry+https://github.com/rust-lang/crates.io-index" 164 | checksum = "0abae9be0aaf9ea96a3b1b8b1b55c602ca751eba1b1500220cea4ecbafe7c0d5" 165 | dependencies = [ 166 | "rustversion", 167 | ] 168 | 169 | [[package]] 170 | name = "cc" 171 | version = "1.2.15" 172 | source = "registry+https://github.com/rust-lang/crates.io-index" 173 | checksum = "c736e259eea577f443d5c86c304f9f4ae0295c43f3ba05c21f1d66b5f06001af" 174 | dependencies = [ 175 | "shlex", 176 | ] 177 | 178 | [[package]] 179 | name = "cfg-if" 180 | version = "1.0.0" 181 | source = "registry+https://github.com/rust-lang/crates.io-index" 182 | checksum = "baf1de4339761588bc0619e3cbc0120ee582ebb74b53b4efbf79117bd2da40fd" 183 | 184 | [[package]] 185 | name = "chrono" 186 | version = "0.4.40" 187 | source = "registry+https://github.com/rust-lang/crates.io-index" 188 | checksum = "1a7964611d71df112cb1730f2ee67324fcf4d0fc6606acbbe9bfe06df124637c" 189 | dependencies = [ 190 | "android-tzdata", 191 | "iana-time-zone", 192 | "num-traits", 193 | "serde", 194 | "windows-link", 195 | ] 196 | 197 | [[package]] 198 | name = "clap" 199 | version = "4.5.32" 200 | source = "registry+https://github.com/rust-lang/crates.io-index" 201 | checksum = "6088f3ae8c3608d19260cd7445411865a485688711b78b5be70d78cd96136f83" 202 | dependencies = [ 203 | "clap_builder", 204 | "clap_derive", 205 | ] 206 | 207 | [[package]] 208 | name = "clap-verbosity-flag" 209 | version = "3.0.2" 210 | source = "registry+https://github.com/rust-lang/crates.io-index" 211 | checksum = "2678fade3b77aa3a8ff3aae87e9c008d3fb00473a41c71fbf74e91c8c7b37e84" 212 | dependencies = [ 213 | "clap", 214 | "log", 215 | ] 216 | 217 | [[package]] 218 | name = "clap_builder" 219 | version = "4.5.32" 220 | source = "registry+https://github.com/rust-lang/crates.io-index" 221 | checksum = "22a7ef7f676155edfb82daa97f99441f3ebf4a58d5e32f295a56259f1b6facc8" 222 | dependencies = [ 223 | "anstream", 224 | "anstyle", 225 | "clap_lex", 226 | "strsim", 227 | "terminal_size", 228 | ] 229 | 230 | [[package]] 231 | name = "clap_derive" 232 | version = "4.5.32" 233 | source = "registry+https://github.com/rust-lang/crates.io-index" 234 | checksum = "09176aae279615badda0765c0c0b3f6ed53f4709118af73cf4655d85d1530cd7" 235 | dependencies = [ 236 | "heck", 237 | "proc-macro2", 238 | "quote", 239 | "syn", 240 | ] 241 | 242 | [[package]] 243 | name = "clap_lex" 244 | version = "0.7.4" 245 | source = "registry+https://github.com/rust-lang/crates.io-index" 246 | checksum = "f46ad14479a25103f283c0f10005961cf086d8dc42205bb44c46ac563475dca6" 247 | 248 | [[package]] 249 | name = "color-eyre" 250 | version = "0.6.3" 251 | source = "registry+https://github.com/rust-lang/crates.io-index" 252 | checksum = "55146f5e46f237f7423d74111267d4597b59b0dad0ffaf7303bce9945d843ad5" 253 | dependencies = [ 254 | "backtrace", 255 | "color-spantrace", 256 | "eyre", 257 | "indenter", 258 | "once_cell", 259 | "owo-colors", 260 | "tracing-error", 261 | ] 262 | 263 | [[package]] 264 | name = "color-spantrace" 265 | version = "0.2.1" 266 | source = "registry+https://github.com/rust-lang/crates.io-index" 267 | checksum = "cd6be1b2a7e382e2b98b43b2adcca6bb0e465af0bdd38123873ae61eb17a72c2" 268 | dependencies = [ 269 | "once_cell", 270 | "owo-colors", 271 | "tracing-core", 272 | "tracing-error", 273 | ] 274 | 275 | [[package]] 276 | name = "colorchoice" 277 | version = "1.0.3" 278 | source = "registry+https://github.com/rust-lang/crates.io-index" 279 | checksum = "5b63caa9aa9397e2d9480a9b13673856c78d8ac123288526c37d7839f2a86990" 280 | 281 | [[package]] 282 | name = "compact_str" 283 | version = "0.8.1" 284 | source = "registry+https://github.com/rust-lang/crates.io-index" 285 | checksum = "3b79c4069c6cad78e2e0cdfcbd26275770669fb39fd308a752dc110e83b9af32" 286 | dependencies = [ 287 | "castaway", 288 | "cfg-if", 289 | "itoa", 290 | "rustversion", 291 | "ryu", 292 | "static_assertions", 293 | ] 294 | 295 | [[package]] 296 | name = "core-foundation-sys" 297 | version = "0.8.7" 298 | source = "registry+https://github.com/rust-lang/crates.io-index" 299 | checksum = "773648b94d0e5d620f64f280777445740e61fe701025087ec8b57f45c791888b" 300 | 301 | [[package]] 302 | name = "cpufeatures" 303 | version = "0.2.17" 304 | source = "registry+https://github.com/rust-lang/crates.io-index" 305 | checksum = "59ed5838eebb26a2bb2e58f6d5b5316989ae9d08bab10e0e6d103e656d1b0280" 306 | dependencies = [ 307 | "libc", 308 | ] 309 | 310 | [[package]] 311 | name = "crossterm" 312 | version = "0.25.0" 313 | source = "registry+https://github.com/rust-lang/crates.io-index" 314 | checksum = "e64e6c0fbe2c17357405f7c758c1ef960fce08bdfb2c03d88d2a18d7e09c4b67" 315 | dependencies = [ 316 | "bitflags 1.3.2", 317 | "crossterm_winapi", 318 | "libc", 319 | "mio 0.8.11", 320 | "parking_lot", 321 | "signal-hook", 322 | "signal-hook-mio", 323 | "winapi", 324 | ] 325 | 326 | [[package]] 327 | name = "crossterm" 328 | version = "0.28.1" 329 | source = "registry+https://github.com/rust-lang/crates.io-index" 330 | checksum = "829d955a0bb380ef178a640b91779e3987da38c9aea133b20614cfed8cdea9c6" 331 | dependencies = [ 332 | "bitflags 2.8.0", 333 | "crossterm_winapi", 334 | "mio 1.0.3", 335 | "parking_lot", 336 | "rustix", 337 | "signal-hook", 338 | "signal-hook-mio", 339 | "winapi", 340 | ] 341 | 342 | [[package]] 343 | name = "crossterm_winapi" 344 | version = "0.9.1" 345 | source = "registry+https://github.com/rust-lang/crates.io-index" 346 | checksum = "acdd7c62a3665c7f6830a51635d9ac9b23ed385797f70a83bb8bafe9c572ab2b" 347 | dependencies = [ 348 | "winapi", 349 | ] 350 | 351 | [[package]] 352 | name = "crypto-common" 353 | version = "0.1.6" 354 | source = "registry+https://github.com/rust-lang/crates.io-index" 355 | checksum = "1bfb12502f3fc46cca1bb51ac28df9d618d813cdc3d2f25b9fe775a34af26bb3" 356 | dependencies = [ 357 | "generic-array", 358 | "typenum", 359 | ] 360 | 361 | [[package]] 362 | name = "darling" 363 | version = "0.20.10" 364 | source = "registry+https://github.com/rust-lang/crates.io-index" 365 | checksum = "6f63b86c8a8826a49b8c21f08a2d07338eec8d900540f8630dc76284be802989" 366 | dependencies = [ 367 | "darling_core", 368 | "darling_macro", 369 | ] 370 | 371 | [[package]] 372 | name = "darling_core" 373 | version = "0.20.10" 374 | source = "registry+https://github.com/rust-lang/crates.io-index" 375 | checksum = "95133861a8032aaea082871032f5815eb9e98cef03fa916ab4500513994df9e5" 376 | dependencies = [ 377 | "fnv", 378 | "ident_case", 379 | "proc-macro2", 380 | "quote", 381 | "strsim", 382 | "syn", 383 | ] 384 | 385 | [[package]] 386 | name = "darling_macro" 387 | version = "0.20.10" 388 | source = "registry+https://github.com/rust-lang/crates.io-index" 389 | checksum = "d336a2a514f6ccccaa3e09b02d41d35330c07ddf03a62165fcec10bb561c7806" 390 | dependencies = [ 391 | "darling_core", 392 | "quote", 393 | "syn", 394 | ] 395 | 396 | [[package]] 397 | name = "digest" 398 | version = "0.10.7" 399 | source = "registry+https://github.com/rust-lang/crates.io-index" 400 | checksum = "9ed9a281f7bc9b7576e61468ba615a66a5c8cfdff42420a70aa82701a3b1e292" 401 | dependencies = [ 402 | "block-buffer", 403 | "crypto-common", 404 | ] 405 | 406 | [[package]] 407 | name = "dirs" 408 | version = "6.0.0" 409 | source = "registry+https://github.com/rust-lang/crates.io-index" 410 | checksum = "c3e8aa94d75141228480295a7d0e7feb620b1a5ad9f12bc40be62411e38cce4e" 411 | dependencies = [ 412 | "dirs-sys", 413 | ] 414 | 415 | [[package]] 416 | name = "dirs-sys" 417 | version = "0.5.0" 418 | source = "registry+https://github.com/rust-lang/crates.io-index" 419 | checksum = "e01a3366d27ee9890022452ee61b2b63a67e6f13f58900b651ff5665f0bb1fab" 420 | dependencies = [ 421 | "libc", 422 | "option-ext", 423 | "redox_users", 424 | "windows-sys 0.59.0", 425 | ] 426 | 427 | [[package]] 428 | name = "displaydoc" 429 | version = "0.2.5" 430 | source = "registry+https://github.com/rust-lang/crates.io-index" 431 | checksum = "97369cbbc041bc366949bc74d34658d6cda5621039731c6310521892a3a20ae0" 432 | dependencies = [ 433 | "proc-macro2", 434 | "quote", 435 | "syn", 436 | ] 437 | 438 | [[package]] 439 | name = "dyn-clone" 440 | version = "1.0.18" 441 | source = "registry+https://github.com/rust-lang/crates.io-index" 442 | checksum = "feeef44e73baff3a26d371801df019877a9866a8c493d315ab00177843314f35" 443 | 444 | [[package]] 445 | name = "either" 446 | version = "1.13.0" 447 | source = "registry+https://github.com/rust-lang/crates.io-index" 448 | checksum = "60b1af1c220855b6ceac025d3f6ecdd2b7c4894bfe9cd9bda4fbb4bc7c0d4cf0" 449 | 450 | [[package]] 451 | name = "env_filter" 452 | version = "0.1.3" 453 | source = "registry+https://github.com/rust-lang/crates.io-index" 454 | checksum = "186e05a59d4c50738528153b83b0b0194d3a29507dfec16eccd4b342903397d0" 455 | dependencies = [ 456 | "log", 457 | "regex", 458 | ] 459 | 460 | [[package]] 461 | name = "env_logger" 462 | version = "0.11.7" 463 | source = "registry+https://github.com/rust-lang/crates.io-index" 464 | checksum = "c3716d7a920fb4fac5d84e9d4bce8ceb321e9414b4409da61b07b75c1e3d0697" 465 | dependencies = [ 466 | "anstream", 467 | "anstyle", 468 | "env_filter", 469 | "jiff", 470 | "log", 471 | ] 472 | 473 | [[package]] 474 | name = "equivalent" 475 | version = "1.0.2" 476 | source = "registry+https://github.com/rust-lang/crates.io-index" 477 | checksum = "877a4ace8713b0bcf2a4e7eec82529c029f1d0619886d18145fea96c3ffe5c0f" 478 | 479 | [[package]] 480 | name = "errno" 481 | version = "0.3.10" 482 | source = "registry+https://github.com/rust-lang/crates.io-index" 483 | checksum = "33d852cb9b869c2a9b3df2f71a3074817f01e1844f839a144f5fcef059a4eb5d" 484 | dependencies = [ 485 | "libc", 486 | "windows-sys 0.59.0", 487 | ] 488 | 489 | [[package]] 490 | name = "eyre" 491 | version = "0.6.12" 492 | source = "registry+https://github.com/rust-lang/crates.io-index" 493 | checksum = "7cd915d99f24784cdc19fd37ef22b97e3ff0ae756c7e492e9fbfe897d61e2aec" 494 | dependencies = [ 495 | "indenter", 496 | "once_cell", 497 | ] 498 | 499 | [[package]] 500 | name = "fnv" 501 | version = "1.0.7" 502 | source = "registry+https://github.com/rust-lang/crates.io-index" 503 | checksum = "3f9eec918d3f24069decb9af1554cad7c880e2da24a9afd88aca000531ab82c1" 504 | 505 | [[package]] 506 | name = "foldhash" 507 | version = "0.1.4" 508 | source = "registry+https://github.com/rust-lang/crates.io-index" 509 | checksum = "a0d2fde1f7b3d48b8395d5f2de76c18a528bd6a9cdde438df747bfcba3e05d6f" 510 | 511 | [[package]] 512 | name = "form_urlencoded" 513 | version = "1.2.1" 514 | source = "registry+https://github.com/rust-lang/crates.io-index" 515 | checksum = "e13624c2627564efccf4934284bdd98cbaa14e79b0b5a141218e507b3a823456" 516 | dependencies = [ 517 | "percent-encoding", 518 | ] 519 | 520 | [[package]] 521 | name = "fuzzy-matcher" 522 | version = "0.3.7" 523 | source = "registry+https://github.com/rust-lang/crates.io-index" 524 | checksum = "54614a3312934d066701a80f20f15fa3b56d67ac7722b39eea5b4c9dd1d66c94" 525 | dependencies = [ 526 | "thread_local", 527 | ] 528 | 529 | [[package]] 530 | name = "fxhash" 531 | version = "0.2.1" 532 | source = "registry+https://github.com/rust-lang/crates.io-index" 533 | checksum = "c31b6d751ae2c7f11320402d34e41349dd1016f8d5d45e48c4312bc8625af50c" 534 | dependencies = [ 535 | "byteorder", 536 | ] 537 | 538 | [[package]] 539 | name = "generic-array" 540 | version = "0.14.7" 541 | source = "registry+https://github.com/rust-lang/crates.io-index" 542 | checksum = "85649ca51fd72272d7821adaf274ad91c288277713d9c18820d8499a7ff69e9a" 543 | dependencies = [ 544 | "typenum", 545 | "version_check", 546 | ] 547 | 548 | [[package]] 549 | name = "getrandom" 550 | version = "0.2.15" 551 | source = "registry+https://github.com/rust-lang/crates.io-index" 552 | checksum = "c4567c8db10ae91089c99af84c68c38da3ec2f087c3f82960bcdbf3656b6f4d7" 553 | dependencies = [ 554 | "cfg-if", 555 | "libc", 556 | "wasi", 557 | ] 558 | 559 | [[package]] 560 | name = "gimli" 561 | version = "0.28.1" 562 | source = "registry+https://github.com/rust-lang/crates.io-index" 563 | checksum = "4271d37baee1b8c7e4b708028c57d816cf9d2434acb33a549475f78c181f6253" 564 | 565 | [[package]] 566 | name = "hashbrown" 567 | version = "0.15.2" 568 | source = "registry+https://github.com/rust-lang/crates.io-index" 569 | checksum = "bf151400ff0baff5465007dd2f3e717f3fe502074ca563069ce3a6629d07b289" 570 | dependencies = [ 571 | "allocator-api2", 572 | "equivalent", 573 | "foldhash", 574 | ] 575 | 576 | [[package]] 577 | name = "heck" 578 | version = "0.5.0" 579 | source = "registry+https://github.com/rust-lang/crates.io-index" 580 | checksum = "2304e00983f87ffb38b55b444b5e3b60a884b5d30c0fca7d82fe33449bbe55ea" 581 | 582 | [[package]] 583 | name = "hex" 584 | version = "0.4.3" 585 | source = "registry+https://github.com/rust-lang/crates.io-index" 586 | checksum = "7f24254aa9a54b5c858eaee2f5bccdb46aaf0e486a595ed5fd8f86ba55232a70" 587 | 588 | [[package]] 589 | name = "iana-time-zone" 590 | version = "0.1.61" 591 | source = "registry+https://github.com/rust-lang/crates.io-index" 592 | checksum = "235e081f3925a06703c2d0117ea8b91f042756fd6e7a6e5d901e8ca1a996b220" 593 | dependencies = [ 594 | "android_system_properties", 595 | "core-foundation-sys", 596 | "iana-time-zone-haiku", 597 | "js-sys", 598 | "wasm-bindgen", 599 | "windows-core", 600 | ] 601 | 602 | [[package]] 603 | name = "iana-time-zone-haiku" 604 | version = "0.1.2" 605 | source = "registry+https://github.com/rust-lang/crates.io-index" 606 | checksum = "f31827a206f56af32e590ba56d5d2d085f558508192593743f16b2306495269f" 607 | dependencies = [ 608 | "cc", 609 | ] 610 | 611 | [[package]] 612 | name = "icu_collections" 613 | version = "1.5.0" 614 | source = "registry+https://github.com/rust-lang/crates.io-index" 615 | checksum = "db2fa452206ebee18c4b5c2274dbf1de17008e874b4dc4f0aea9d01ca79e4526" 616 | dependencies = [ 617 | "displaydoc", 618 | "yoke", 619 | "zerofrom", 620 | "zerovec", 621 | ] 622 | 623 | [[package]] 624 | name = "icu_locid" 625 | version = "1.5.0" 626 | source = "registry+https://github.com/rust-lang/crates.io-index" 627 | checksum = "13acbb8371917fc971be86fc8057c41a64b521c184808a698c02acc242dbf637" 628 | dependencies = [ 629 | "displaydoc", 630 | "litemap", 631 | "tinystr", 632 | "writeable", 633 | "zerovec", 634 | ] 635 | 636 | [[package]] 637 | name = "icu_locid_transform" 638 | version = "1.5.0" 639 | source = "registry+https://github.com/rust-lang/crates.io-index" 640 | checksum = "01d11ac35de8e40fdeda00d9e1e9d92525f3f9d887cdd7aa81d727596788b54e" 641 | dependencies = [ 642 | "displaydoc", 643 | "icu_locid", 644 | "icu_locid_transform_data", 645 | "icu_provider", 646 | "tinystr", 647 | "zerovec", 648 | ] 649 | 650 | [[package]] 651 | name = "icu_locid_transform_data" 652 | version = "1.5.0" 653 | source = "registry+https://github.com/rust-lang/crates.io-index" 654 | checksum = "fdc8ff3388f852bede6b579ad4e978ab004f139284d7b28715f773507b946f6e" 655 | 656 | [[package]] 657 | name = "icu_normalizer" 658 | version = "1.5.0" 659 | source = "registry+https://github.com/rust-lang/crates.io-index" 660 | checksum = "19ce3e0da2ec68599d193c93d088142efd7f9c5d6fc9b803774855747dc6a84f" 661 | dependencies = [ 662 | "displaydoc", 663 | "icu_collections", 664 | "icu_normalizer_data", 665 | "icu_properties", 666 | "icu_provider", 667 | "smallvec", 668 | "utf16_iter", 669 | "utf8_iter", 670 | "write16", 671 | "zerovec", 672 | ] 673 | 674 | [[package]] 675 | name = "icu_normalizer_data" 676 | version = "1.5.0" 677 | source = "registry+https://github.com/rust-lang/crates.io-index" 678 | checksum = "f8cafbf7aa791e9b22bec55a167906f9e1215fd475cd22adfcf660e03e989516" 679 | 680 | [[package]] 681 | name = "icu_properties" 682 | version = "1.5.1" 683 | source = "registry+https://github.com/rust-lang/crates.io-index" 684 | checksum = "93d6020766cfc6302c15dbbc9c8778c37e62c14427cb7f6e601d849e092aeef5" 685 | dependencies = [ 686 | "displaydoc", 687 | "icu_collections", 688 | "icu_locid_transform", 689 | "icu_properties_data", 690 | "icu_provider", 691 | "tinystr", 692 | "zerovec", 693 | ] 694 | 695 | [[package]] 696 | name = "icu_properties_data" 697 | version = "1.5.0" 698 | source = "registry+https://github.com/rust-lang/crates.io-index" 699 | checksum = "67a8effbc3dd3e4ba1afa8ad918d5684b8868b3b26500753effea8d2eed19569" 700 | 701 | [[package]] 702 | name = "icu_provider" 703 | version = "1.5.0" 704 | source = "registry+https://github.com/rust-lang/crates.io-index" 705 | checksum = "6ed421c8a8ef78d3e2dbc98a973be2f3770cb42b606e3ab18d6237c4dfde68d9" 706 | dependencies = [ 707 | "displaydoc", 708 | "icu_locid", 709 | "icu_provider_macros", 710 | "stable_deref_trait", 711 | "tinystr", 712 | "writeable", 713 | "yoke", 714 | "zerofrom", 715 | "zerovec", 716 | ] 717 | 718 | [[package]] 719 | name = "icu_provider_macros" 720 | version = "1.5.0" 721 | source = "registry+https://github.com/rust-lang/crates.io-index" 722 | checksum = "1ec89e9337638ecdc08744df490b221a7399bf8d164eb52a665454e60e075ad6" 723 | dependencies = [ 724 | "proc-macro2", 725 | "quote", 726 | "syn", 727 | ] 728 | 729 | [[package]] 730 | name = "ident_case" 731 | version = "1.0.1" 732 | source = "registry+https://github.com/rust-lang/crates.io-index" 733 | checksum = "b9e0384b61958566e926dc50660321d12159025e767c18e043daf26b70104c39" 734 | 735 | [[package]] 736 | name = "idna" 737 | version = "1.0.3" 738 | source = "registry+https://github.com/rust-lang/crates.io-index" 739 | checksum = "686f825264d630750a544639377bae737628043f20d38bbc029e8f29ea968a7e" 740 | dependencies = [ 741 | "idna_adapter", 742 | "smallvec", 743 | "utf8_iter", 744 | ] 745 | 746 | [[package]] 747 | name = "idna_adapter" 748 | version = "1.2.0" 749 | source = "registry+https://github.com/rust-lang/crates.io-index" 750 | checksum = "daca1df1c957320b2cf139ac61e7bd64fed304c5040df000a745aa1de3b4ef71" 751 | dependencies = [ 752 | "icu_normalizer", 753 | "icu_properties", 754 | ] 755 | 756 | [[package]] 757 | name = "indenter" 758 | version = "0.3.3" 759 | source = "registry+https://github.com/rust-lang/crates.io-index" 760 | checksum = "ce23b50ad8242c51a442f3ff322d56b02f08852c77e4c0b4d3fd684abc89c683" 761 | 762 | [[package]] 763 | name = "indoc" 764 | version = "2.0.5" 765 | source = "registry+https://github.com/rust-lang/crates.io-index" 766 | checksum = "b248f5224d1d606005e02c97f5aa4e88eeb230488bcc03bc9ca4d7991399f2b5" 767 | 768 | [[package]] 769 | name = "inquire" 770 | version = "0.7.5" 771 | source = "registry+https://github.com/rust-lang/crates.io-index" 772 | checksum = "0fddf93031af70e75410a2511ec04d49e758ed2f26dad3404a934e0fb45cc12a" 773 | dependencies = [ 774 | "bitflags 2.8.0", 775 | "crossterm 0.25.0", 776 | "dyn-clone", 777 | "fuzzy-matcher", 778 | "fxhash", 779 | "newline-converter", 780 | "once_cell", 781 | "unicode-segmentation", 782 | "unicode-width 0.1.14", 783 | ] 784 | 785 | [[package]] 786 | name = "instability" 787 | version = "0.3.7" 788 | source = "registry+https://github.com/rust-lang/crates.io-index" 789 | checksum = "0bf9fed6d91cfb734e7476a06bde8300a1b94e217e1b523b6f0cd1a01998c71d" 790 | dependencies = [ 791 | "darling", 792 | "indoc", 793 | "proc-macro2", 794 | "quote", 795 | "syn", 796 | ] 797 | 798 | [[package]] 799 | name = "is_terminal_polyfill" 800 | version = "1.70.1" 801 | source = "registry+https://github.com/rust-lang/crates.io-index" 802 | checksum = "7943c866cc5cd64cbc25b2e01621d07fa8eb2a1a23160ee81ce38704e97b8ecf" 803 | 804 | [[package]] 805 | name = "itertools" 806 | version = "0.13.0" 807 | source = "registry+https://github.com/rust-lang/crates.io-index" 808 | checksum = "413ee7dfc52ee1a4949ceeb7dbc8a33f2d6c088194d9f922fb8318faf1f01186" 809 | dependencies = [ 810 | "either", 811 | ] 812 | 813 | [[package]] 814 | name = "itoa" 815 | version = "1.0.14" 816 | source = "registry+https://github.com/rust-lang/crates.io-index" 817 | checksum = "d75a2a4b1b190afb6f5425f10f6a8f959d2ea0b9c2b1d79553551850539e4674" 818 | 819 | [[package]] 820 | name = "jiff" 821 | version = "0.2.3" 822 | source = "registry+https://github.com/rust-lang/crates.io-index" 823 | checksum = "5c163c633eb184a4ad2a5e7a5dacf12a58c830d717a7963563d4eceb4ced079f" 824 | dependencies = [ 825 | "jiff-static", 826 | "log", 827 | "portable-atomic", 828 | "portable-atomic-util", 829 | "serde", 830 | ] 831 | 832 | [[package]] 833 | name = "jiff-static" 834 | version = "0.2.3" 835 | source = "registry+https://github.com/rust-lang/crates.io-index" 836 | checksum = "dbc3e0019b0f5f43038cf46471b1312136f29e36f54436c6042c8f155fec8789" 837 | dependencies = [ 838 | "proc-macro2", 839 | "quote", 840 | "syn", 841 | ] 842 | 843 | [[package]] 844 | name = "js-sys" 845 | version = "0.3.77" 846 | source = "registry+https://github.com/rust-lang/crates.io-index" 847 | checksum = "1cfaf33c695fc6e08064efbc1f72ec937429614f25eef83af942d0e227c3a28f" 848 | dependencies = [ 849 | "once_cell", 850 | "wasm-bindgen", 851 | ] 852 | 853 | [[package]] 854 | name = "json5" 855 | version = "0.4.1" 856 | source = "registry+https://github.com/rust-lang/crates.io-index" 857 | checksum = "96b0db21af676c1ce64250b5f40f3ce2cf27e4e47cb91ed91eb6fe9350b430c1" 858 | dependencies = [ 859 | "pest", 860 | "pest_derive", 861 | "serde", 862 | ] 863 | 864 | [[package]] 865 | name = "lazy_static" 866 | version = "1.5.0" 867 | source = "registry+https://github.com/rust-lang/crates.io-index" 868 | checksum = "bbd2bcb4c963f2ddae06a2efc7e9f3591312473c50c6685e1f298068316e66fe" 869 | 870 | [[package]] 871 | name = "libc" 872 | version = "0.2.170" 873 | source = "registry+https://github.com/rust-lang/crates.io-index" 874 | checksum = "875b3680cb2f8f71bdcf9a30f38d48282f5d3c95cbf9b3fa57269bb5d5c06828" 875 | 876 | [[package]] 877 | name = "libredox" 878 | version = "0.1.3" 879 | source = "registry+https://github.com/rust-lang/crates.io-index" 880 | checksum = "c0ff37bd590ca25063e35af745c343cb7a0271906fb7b37e4813e8f79f00268d" 881 | dependencies = [ 882 | "bitflags 2.8.0", 883 | "libc", 884 | ] 885 | 886 | [[package]] 887 | name = "linux-raw-sys" 888 | version = "0.4.15" 889 | source = "registry+https://github.com/rust-lang/crates.io-index" 890 | checksum = "d26c52dbd32dccf2d10cac7725f8eae5296885fb5703b261f7d0a0739ec807ab" 891 | 892 | [[package]] 893 | name = "litemap" 894 | version = "0.7.4" 895 | source = "registry+https://github.com/rust-lang/crates.io-index" 896 | checksum = "4ee93343901ab17bd981295f2cf0026d4ad018c7c31ba84549a4ddbb47a45104" 897 | 898 | [[package]] 899 | name = "lock_api" 900 | version = "0.4.12" 901 | source = "registry+https://github.com/rust-lang/crates.io-index" 902 | checksum = "07af8b9cdd281b7915f413fa73f29ebd5d55d0d3f0155584dade1ff18cea1b17" 903 | dependencies = [ 904 | "autocfg", 905 | "scopeguard", 906 | ] 907 | 908 | [[package]] 909 | name = "log" 910 | version = "0.4.26" 911 | source = "registry+https://github.com/rust-lang/crates.io-index" 912 | checksum = "30bde2b3dc3671ae49d8e2e9f044c7c005836e7a023ee57cffa25ab82764bb9e" 913 | 914 | [[package]] 915 | name = "lru" 916 | version = "0.12.5" 917 | source = "registry+https://github.com/rust-lang/crates.io-index" 918 | checksum = "234cf4f4a04dc1f57e24b96cc0cd600cf2af460d4161ac5ecdd0af8e1f3b2a38" 919 | dependencies = [ 920 | "hashbrown", 921 | ] 922 | 923 | [[package]] 924 | name = "memchr" 925 | version = "2.7.4" 926 | source = "registry+https://github.com/rust-lang/crates.io-index" 927 | checksum = "78ca9ab1a0babb1e7d5695e3530886289c18cf2f87ec19a575a0abdce112e3a3" 928 | 929 | [[package]] 930 | name = "miniz_oxide" 931 | version = "0.7.4" 932 | source = "registry+https://github.com/rust-lang/crates.io-index" 933 | checksum = "b8a240ddb74feaf34a79a7add65a741f3167852fba007066dcac1ca548d89c08" 934 | dependencies = [ 935 | "adler", 936 | ] 937 | 938 | [[package]] 939 | name = "mio" 940 | version = "0.8.11" 941 | source = "registry+https://github.com/rust-lang/crates.io-index" 942 | checksum = "a4a650543ca06a924e8b371db273b2756685faae30f8487da1b56505a8f78b0c" 943 | dependencies = [ 944 | "libc", 945 | "log", 946 | "wasi", 947 | "windows-sys 0.48.0", 948 | ] 949 | 950 | [[package]] 951 | name = "mio" 952 | version = "1.0.3" 953 | source = "registry+https://github.com/rust-lang/crates.io-index" 954 | checksum = "2886843bf800fba2e3377cff24abf6379b4c4d5c6681eaf9ea5b0d15090450bd" 955 | dependencies = [ 956 | "libc", 957 | "log", 958 | "wasi", 959 | "windows-sys 0.52.0", 960 | ] 961 | 962 | [[package]] 963 | name = "newline-converter" 964 | version = "0.3.0" 965 | source = "registry+https://github.com/rust-lang/crates.io-index" 966 | checksum = "47b6b097ecb1cbfed438542d16e84fd7ad9b0c76c8a65b7f9039212a3d14dc7f" 967 | dependencies = [ 968 | "unicode-segmentation", 969 | ] 970 | 971 | [[package]] 972 | name = "nucleo-matcher" 973 | version = "0.3.1" 974 | source = "registry+https://github.com/rust-lang/crates.io-index" 975 | checksum = "bf33f538733d1a5a3494b836ba913207f14d9d4a1d3cd67030c5061bdd2cac85" 976 | dependencies = [ 977 | "memchr", 978 | "unicode-segmentation", 979 | ] 980 | 981 | [[package]] 982 | name = "num-traits" 983 | version = "0.2.19" 984 | source = "registry+https://github.com/rust-lang/crates.io-index" 985 | checksum = "071dfc062690e90b734c0b2273ce72ad0ffa95f0c74596bc250dcfd960262841" 986 | dependencies = [ 987 | "autocfg", 988 | ] 989 | 990 | [[package]] 991 | name = "object" 992 | version = "0.32.2" 993 | source = "registry+https://github.com/rust-lang/crates.io-index" 994 | checksum = "a6a622008b6e321afc04970976f62ee297fdbaa6f95318ca343e3eebb9648441" 995 | dependencies = [ 996 | "memchr", 997 | ] 998 | 999 | [[package]] 1000 | name = "once_cell" 1001 | version = "1.20.3" 1002 | source = "registry+https://github.com/rust-lang/crates.io-index" 1003 | checksum = "945462a4b81e43c4e3ba96bd7b49d834c6f61198356aa858733bc4acf3cbe62e" 1004 | 1005 | [[package]] 1006 | name = "option-ext" 1007 | version = "0.2.0" 1008 | source = "registry+https://github.com/rust-lang/crates.io-index" 1009 | checksum = "04744f49eae99ab78e0d5c0b603ab218f515ea8cfe5a456d7629ad883a3b6e7d" 1010 | 1011 | [[package]] 1012 | name = "owo-colors" 1013 | version = "3.5.0" 1014 | source = "registry+https://github.com/rust-lang/crates.io-index" 1015 | checksum = "c1b04fb49957986fdce4d6ee7a65027d55d4b6d2265e5848bbb507b58ccfdb6f" 1016 | 1017 | [[package]] 1018 | name = "parking_lot" 1019 | version = "0.12.3" 1020 | source = "registry+https://github.com/rust-lang/crates.io-index" 1021 | checksum = "f1bf18183cf54e8d6059647fc3063646a1801cf30896933ec2311622cc4b9a27" 1022 | dependencies = [ 1023 | "lock_api", 1024 | "parking_lot_core", 1025 | ] 1026 | 1027 | [[package]] 1028 | name = "parking_lot_core" 1029 | version = "0.9.10" 1030 | source = "registry+https://github.com/rust-lang/crates.io-index" 1031 | checksum = "1e401f977ab385c9e4e3ab30627d6f26d00e2c73eef317493c4ec6d468726cf8" 1032 | dependencies = [ 1033 | "cfg-if", 1034 | "libc", 1035 | "redox_syscall", 1036 | "smallvec", 1037 | "windows-targets 0.52.6", 1038 | ] 1039 | 1040 | [[package]] 1041 | name = "paste" 1042 | version = "1.0.15" 1043 | source = "registry+https://github.com/rust-lang/crates.io-index" 1044 | checksum = "57c0d7b74b563b49d38dae00a0c37d4d6de9b432382b2892f0574ddcae73fd0a" 1045 | 1046 | [[package]] 1047 | name = "percent-encoding" 1048 | version = "2.3.1" 1049 | source = "registry+https://github.com/rust-lang/crates.io-index" 1050 | checksum = "e3148f5046208a5d56bcfc03053e3ca6334e51da8dfb19b6cdc8b306fae3283e" 1051 | 1052 | [[package]] 1053 | name = "pest" 1054 | version = "2.7.15" 1055 | source = "registry+https://github.com/rust-lang/crates.io-index" 1056 | checksum = "8b7cafe60d6cf8e62e1b9b2ea516a089c008945bb5a275416789e7db0bc199dc" 1057 | dependencies = [ 1058 | "memchr", 1059 | "thiserror", 1060 | "ucd-trie", 1061 | ] 1062 | 1063 | [[package]] 1064 | name = "pest_derive" 1065 | version = "2.7.15" 1066 | source = "registry+https://github.com/rust-lang/crates.io-index" 1067 | checksum = "816518421cfc6887a0d62bf441b6ffb4536fcc926395a69e1a85852d4363f57e" 1068 | dependencies = [ 1069 | "pest", 1070 | "pest_generator", 1071 | ] 1072 | 1073 | [[package]] 1074 | name = "pest_generator" 1075 | version = "2.7.15" 1076 | source = "registry+https://github.com/rust-lang/crates.io-index" 1077 | checksum = "7d1396fd3a870fc7838768d171b4616d5c91f6cc25e377b673d714567d99377b" 1078 | dependencies = [ 1079 | "pest", 1080 | "pest_meta", 1081 | "proc-macro2", 1082 | "quote", 1083 | "syn", 1084 | ] 1085 | 1086 | [[package]] 1087 | name = "pest_meta" 1088 | version = "2.7.15" 1089 | source = "registry+https://github.com/rust-lang/crates.io-index" 1090 | checksum = "e1e58089ea25d717bfd31fb534e4f3afcc2cc569c70de3e239778991ea3b7dea" 1091 | dependencies = [ 1092 | "once_cell", 1093 | "pest", 1094 | "sha2", 1095 | ] 1096 | 1097 | [[package]] 1098 | name = "pin-project-lite" 1099 | version = "0.2.16" 1100 | source = "registry+https://github.com/rust-lang/crates.io-index" 1101 | checksum = "3b3cff922bd51709b605d9ead9aa71031d81447142d828eb4a6eba76fe619f9b" 1102 | 1103 | [[package]] 1104 | name = "portable-atomic" 1105 | version = "1.11.0" 1106 | source = "registry+https://github.com/rust-lang/crates.io-index" 1107 | checksum = "350e9b48cbc6b0e028b0473b114454c6316e57336ee184ceab6e53f72c178b3e" 1108 | 1109 | [[package]] 1110 | name = "portable-atomic-util" 1111 | version = "0.2.4" 1112 | source = "registry+https://github.com/rust-lang/crates.io-index" 1113 | checksum = "d8a2f0d8d040d7848a709caf78912debcc3f33ee4b3cac47d73d1e1069e83507" 1114 | dependencies = [ 1115 | "portable-atomic", 1116 | ] 1117 | 1118 | [[package]] 1119 | name = "proc-macro2" 1120 | version = "1.0.93" 1121 | source = "registry+https://github.com/rust-lang/crates.io-index" 1122 | checksum = "60946a68e5f9d28b0dc1c21bb8a97ee7d018a8b322fa57838ba31cc878e22d99" 1123 | dependencies = [ 1124 | "unicode-ident", 1125 | ] 1126 | 1127 | [[package]] 1128 | name = "quote" 1129 | version = "1.0.38" 1130 | source = "registry+https://github.com/rust-lang/crates.io-index" 1131 | checksum = "0e4dccaaaf89514f546c693ddc140f729f958c247918a13380cccc6078391acc" 1132 | dependencies = [ 1133 | "proc-macro2", 1134 | ] 1135 | 1136 | [[package]] 1137 | name = "ratatui" 1138 | version = "0.29.0" 1139 | source = "registry+https://github.com/rust-lang/crates.io-index" 1140 | checksum = "eabd94c2f37801c20583fc49dd5cd6b0ba68c716787c2dd6ed18571e1e63117b" 1141 | dependencies = [ 1142 | "bitflags 2.8.0", 1143 | "cassowary", 1144 | "compact_str", 1145 | "crossterm 0.28.1", 1146 | "indoc", 1147 | "instability", 1148 | "itertools", 1149 | "lru", 1150 | "paste", 1151 | "strum", 1152 | "unicode-segmentation", 1153 | "unicode-truncate", 1154 | "unicode-width 0.2.0", 1155 | ] 1156 | 1157 | [[package]] 1158 | name = "redox_syscall" 1159 | version = "0.5.9" 1160 | source = "registry+https://github.com/rust-lang/crates.io-index" 1161 | checksum = "82b568323e98e49e2a0899dcee453dd679fae22d69adf9b11dd508d1549b7e2f" 1162 | dependencies = [ 1163 | "bitflags 2.8.0", 1164 | ] 1165 | 1166 | [[package]] 1167 | name = "redox_users" 1168 | version = "0.5.0" 1169 | source = "registry+https://github.com/rust-lang/crates.io-index" 1170 | checksum = "dd6f9d3d47bdd2ad6945c5015a226ec6155d0bcdfd8f7cd29f86b71f8de99d2b" 1171 | dependencies = [ 1172 | "getrandom", 1173 | "libredox", 1174 | "thiserror", 1175 | ] 1176 | 1177 | [[package]] 1178 | name = "regex" 1179 | version = "1.11.1" 1180 | source = "registry+https://github.com/rust-lang/crates.io-index" 1181 | checksum = "b544ef1b4eac5dc2db33ea63606ae9ffcfac26c1416a2806ae0bf5f56b201191" 1182 | dependencies = [ 1183 | "aho-corasick", 1184 | "memchr", 1185 | "regex-automata", 1186 | "regex-syntax", 1187 | ] 1188 | 1189 | [[package]] 1190 | name = "regex-automata" 1191 | version = "0.4.9" 1192 | source = "registry+https://github.com/rust-lang/crates.io-index" 1193 | checksum = "809e8dc61f6de73b46c85f4c96486310fe304c434cfa43669d7b40f711150908" 1194 | dependencies = [ 1195 | "aho-corasick", 1196 | "memchr", 1197 | "regex-syntax", 1198 | ] 1199 | 1200 | [[package]] 1201 | name = "regex-syntax" 1202 | version = "0.8.5" 1203 | source = "registry+https://github.com/rust-lang/crates.io-index" 1204 | checksum = "2b15c43186be67a4fd63bee50d0303afffcef381492ebe2c5d87f324e1b8815c" 1205 | 1206 | [[package]] 1207 | name = "rustc-demangle" 1208 | version = "0.1.24" 1209 | source = "registry+https://github.com/rust-lang/crates.io-index" 1210 | checksum = "719b953e2095829ee67db738b3bfa9fa368c94900df327b3f07fe6e794d2fe1f" 1211 | 1212 | [[package]] 1213 | name = "rustix" 1214 | version = "0.38.44" 1215 | source = "registry+https://github.com/rust-lang/crates.io-index" 1216 | checksum = "fdb5bc1ae2baa591800df16c9ca78619bf65c0488b41b96ccec5d11220d8c154" 1217 | dependencies = [ 1218 | "bitflags 2.8.0", 1219 | "errno", 1220 | "libc", 1221 | "linux-raw-sys", 1222 | "windows-sys 0.59.0", 1223 | ] 1224 | 1225 | [[package]] 1226 | name = "rustversion" 1227 | version = "1.0.19" 1228 | source = "registry+https://github.com/rust-lang/crates.io-index" 1229 | checksum = "f7c45b9784283f1b2e7fb61b42047c2fd678ef0960d4f6f1eba131594cc369d4" 1230 | 1231 | [[package]] 1232 | name = "ryu" 1233 | version = "1.0.19" 1234 | source = "registry+https://github.com/rust-lang/crates.io-index" 1235 | checksum = "6ea1a2d0a644769cc99faa24c3ad26b379b786fe7c36fd3c546254801650e6dd" 1236 | 1237 | [[package]] 1238 | name = "same-file" 1239 | version = "1.0.6" 1240 | source = "registry+https://github.com/rust-lang/crates.io-index" 1241 | checksum = "93fc1dc3aaa9bfed95e02e6eadabb4baf7e3078b0bd1b4d7b6b0b68378900502" 1242 | dependencies = [ 1243 | "winapi-util", 1244 | ] 1245 | 1246 | [[package]] 1247 | name = "scopeguard" 1248 | version = "1.2.0" 1249 | source = "registry+https://github.com/rust-lang/crates.io-index" 1250 | checksum = "94143f37725109f92c262ed2cf5e59bce7498c01bcc1502d7b9afe439a4e9f49" 1251 | 1252 | [[package]] 1253 | name = "serde" 1254 | version = "1.0.219" 1255 | source = "registry+https://github.com/rust-lang/crates.io-index" 1256 | checksum = "5f0e2c6ed6606019b4e29e69dbaba95b11854410e5347d525002456dbbb786b6" 1257 | dependencies = [ 1258 | "serde_derive", 1259 | ] 1260 | 1261 | [[package]] 1262 | name = "serde_derive" 1263 | version = "1.0.219" 1264 | source = "registry+https://github.com/rust-lang/crates.io-index" 1265 | checksum = "5b0276cf7f2c73365f7157c8123c21cd9a50fbbd844757af28ca1f5925fc2a00" 1266 | dependencies = [ 1267 | "proc-macro2", 1268 | "quote", 1269 | "syn", 1270 | ] 1271 | 1272 | [[package]] 1273 | name = "serde_json" 1274 | version = "1.0.140" 1275 | source = "registry+https://github.com/rust-lang/crates.io-index" 1276 | checksum = "20068b6e96dc6c9bd23e01df8827e6c7e1f2fddd43c21810382803c136b99373" 1277 | dependencies = [ 1278 | "itoa", 1279 | "memchr", 1280 | "ryu", 1281 | "serde", 1282 | ] 1283 | 1284 | [[package]] 1285 | name = "sha2" 1286 | version = "0.10.8" 1287 | source = "registry+https://github.com/rust-lang/crates.io-index" 1288 | checksum = "793db75ad2bcafc3ffa7c68b215fee268f537982cd901d132f89c6343f3a3dc8" 1289 | dependencies = [ 1290 | "cfg-if", 1291 | "cpufeatures", 1292 | "digest", 1293 | ] 1294 | 1295 | [[package]] 1296 | name = "sharded-slab" 1297 | version = "0.1.7" 1298 | source = "registry+https://github.com/rust-lang/crates.io-index" 1299 | checksum = "f40ca3c46823713e0d4209592e8d6e826aa57e928f09752619fc696c499637f6" 1300 | dependencies = [ 1301 | "lazy_static", 1302 | ] 1303 | 1304 | [[package]] 1305 | name = "shlex" 1306 | version = "1.3.0" 1307 | source = "registry+https://github.com/rust-lang/crates.io-index" 1308 | checksum = "0fda2ff0d084019ba4d7c6f371c95d8fd75ce3524c3cb8fb653a3023f6323e64" 1309 | 1310 | [[package]] 1311 | name = "signal-hook" 1312 | version = "0.3.17" 1313 | source = "registry+https://github.com/rust-lang/crates.io-index" 1314 | checksum = "8621587d4798caf8eb44879d42e56b9a93ea5dcd315a6487c357130095b62801" 1315 | dependencies = [ 1316 | "libc", 1317 | "signal-hook-registry", 1318 | ] 1319 | 1320 | [[package]] 1321 | name = "signal-hook-mio" 1322 | version = "0.2.4" 1323 | source = "registry+https://github.com/rust-lang/crates.io-index" 1324 | checksum = "34db1a06d485c9142248b7a054f034b349b212551f3dfd19c94d45a754a217cd" 1325 | dependencies = [ 1326 | "libc", 1327 | "mio 0.8.11", 1328 | "mio 1.0.3", 1329 | "signal-hook", 1330 | ] 1331 | 1332 | [[package]] 1333 | name = "signal-hook-registry" 1334 | version = "1.4.2" 1335 | source = "registry+https://github.com/rust-lang/crates.io-index" 1336 | checksum = "a9e9e0b4211b72e7b8b6e85c807d36c212bdb33ea8587f7569562a84df5465b1" 1337 | dependencies = [ 1338 | "libc", 1339 | ] 1340 | 1341 | [[package]] 1342 | name = "smallvec" 1343 | version = "1.14.0" 1344 | source = "registry+https://github.com/rust-lang/crates.io-index" 1345 | checksum = "7fcf8323ef1faaee30a44a340193b1ac6814fd9b7b4e88e9d4519a3e4abe1cfd" 1346 | 1347 | [[package]] 1348 | name = "stable_deref_trait" 1349 | version = "1.2.0" 1350 | source = "registry+https://github.com/rust-lang/crates.io-index" 1351 | checksum = "a8f112729512f8e442d81f95a8a7ddf2b7c6b8a1a6f509a95864142b30cab2d3" 1352 | 1353 | [[package]] 1354 | name = "static_assertions" 1355 | version = "1.1.0" 1356 | source = "registry+https://github.com/rust-lang/crates.io-index" 1357 | checksum = "a2eb9349b6444b326872e140eb1cf5e7c522154d69e7a0ffb0fb81c06b37543f" 1358 | 1359 | [[package]] 1360 | name = "strsim" 1361 | version = "0.11.1" 1362 | source = "registry+https://github.com/rust-lang/crates.io-index" 1363 | checksum = "7da8b5736845d9f2fcb837ea5d9e2628564b3b043a70948a3f0b778838c5fb4f" 1364 | 1365 | [[package]] 1366 | name = "strum" 1367 | version = "0.26.3" 1368 | source = "registry+https://github.com/rust-lang/crates.io-index" 1369 | checksum = "8fec0f0aef304996cf250b31b5a10dee7980c85da9d759361292b8bca5a18f06" 1370 | dependencies = [ 1371 | "strum_macros", 1372 | ] 1373 | 1374 | [[package]] 1375 | name = "strum_macros" 1376 | version = "0.26.4" 1377 | source = "registry+https://github.com/rust-lang/crates.io-index" 1378 | checksum = "4c6bee85a5a24955dc440386795aa378cd9cf82acd5f764469152d2270e581be" 1379 | dependencies = [ 1380 | "heck", 1381 | "proc-macro2", 1382 | "quote", 1383 | "rustversion", 1384 | "syn", 1385 | ] 1386 | 1387 | [[package]] 1388 | name = "syn" 1389 | version = "2.0.98" 1390 | source = "registry+https://github.com/rust-lang/crates.io-index" 1391 | checksum = "36147f1a48ae0ec2b5b3bc5b537d267457555a10dc06f3dbc8cb11ba3006d3b1" 1392 | dependencies = [ 1393 | "proc-macro2", 1394 | "quote", 1395 | "unicode-ident", 1396 | ] 1397 | 1398 | [[package]] 1399 | name = "synstructure" 1400 | version = "0.13.1" 1401 | source = "registry+https://github.com/rust-lang/crates.io-index" 1402 | checksum = "c8af7666ab7b6390ab78131fb5b0fce11d6b7a6951602017c35fa82800708971" 1403 | dependencies = [ 1404 | "proc-macro2", 1405 | "quote", 1406 | "syn", 1407 | ] 1408 | 1409 | [[package]] 1410 | name = "terminal_size" 1411 | version = "0.4.1" 1412 | source = "registry+https://github.com/rust-lang/crates.io-index" 1413 | checksum = "5352447f921fda68cf61b4101566c0bdb5104eff6804d0678e5227580ab6a4e9" 1414 | dependencies = [ 1415 | "rustix", 1416 | "windows-sys 0.59.0", 1417 | ] 1418 | 1419 | [[package]] 1420 | name = "thiserror" 1421 | version = "2.0.11" 1422 | source = "registry+https://github.com/rust-lang/crates.io-index" 1423 | checksum = "d452f284b73e6d76dd36758a0c8684b1d5be31f92b89d07fd5822175732206fc" 1424 | dependencies = [ 1425 | "thiserror-impl", 1426 | ] 1427 | 1428 | [[package]] 1429 | name = "thiserror-impl" 1430 | version = "2.0.11" 1431 | source = "registry+https://github.com/rust-lang/crates.io-index" 1432 | checksum = "26afc1baea8a989337eeb52b6e72a039780ce45c3edfcc9c5b9d112feeb173c2" 1433 | dependencies = [ 1434 | "proc-macro2", 1435 | "quote", 1436 | "syn", 1437 | ] 1438 | 1439 | [[package]] 1440 | name = "thread_local" 1441 | version = "1.1.8" 1442 | source = "registry+https://github.com/rust-lang/crates.io-index" 1443 | checksum = "8b9ef9bad013ada3808854ceac7b46812a6465ba368859a37e2100283d2d719c" 1444 | dependencies = [ 1445 | "cfg-if", 1446 | "once_cell", 1447 | ] 1448 | 1449 | [[package]] 1450 | name = "tinystr" 1451 | version = "0.7.6" 1452 | source = "registry+https://github.com/rust-lang/crates.io-index" 1453 | checksum = "9117f5d4db391c1cf6927e7bea3db74b9a1c1add8f7eda9ffd5364f40f57b82f" 1454 | dependencies = [ 1455 | "displaydoc", 1456 | "zerovec", 1457 | ] 1458 | 1459 | [[package]] 1460 | name = "tracing" 1461 | version = "0.1.41" 1462 | source = "registry+https://github.com/rust-lang/crates.io-index" 1463 | checksum = "784e0ac535deb450455cbfa28a6f0df145ea1bb7ae51b821cf5e7927fdcfbdd0" 1464 | dependencies = [ 1465 | "pin-project-lite", 1466 | "tracing-core", 1467 | ] 1468 | 1469 | [[package]] 1470 | name = "tracing-core" 1471 | version = "0.1.33" 1472 | source = "registry+https://github.com/rust-lang/crates.io-index" 1473 | checksum = "e672c95779cf947c5311f83787af4fa8fffd12fb27e4993211a84bdfd9610f9c" 1474 | dependencies = [ 1475 | "once_cell", 1476 | "valuable", 1477 | ] 1478 | 1479 | [[package]] 1480 | name = "tracing-error" 1481 | version = "0.2.1" 1482 | source = "registry+https://github.com/rust-lang/crates.io-index" 1483 | checksum = "8b1581020d7a273442f5b45074a6a57d5757ad0a47dac0e9f0bd57b81936f3db" 1484 | dependencies = [ 1485 | "tracing", 1486 | "tracing-subscriber", 1487 | ] 1488 | 1489 | [[package]] 1490 | name = "tracing-subscriber" 1491 | version = "0.3.19" 1492 | source = "registry+https://github.com/rust-lang/crates.io-index" 1493 | checksum = "e8189decb5ac0fa7bc8b96b7cb9b2701d60d48805aca84a238004d665fcc4008" 1494 | dependencies = [ 1495 | "sharded-slab", 1496 | "thread_local", 1497 | "tracing-core", 1498 | ] 1499 | 1500 | [[package]] 1501 | name = "tui-textarea" 1502 | version = "0.7.0" 1503 | source = "registry+https://github.com/rust-lang/crates.io-index" 1504 | checksum = "0a5318dd619ed73c52a9417ad19046724effc1287fb75cdcc4eca1d6ac1acbae" 1505 | dependencies = [ 1506 | "crossterm 0.28.1", 1507 | "ratatui", 1508 | "unicode-width 0.2.0", 1509 | ] 1510 | 1511 | [[package]] 1512 | name = "typenum" 1513 | version = "1.18.0" 1514 | source = "registry+https://github.com/rust-lang/crates.io-index" 1515 | checksum = "1dccffe3ce07af9386bfd29e80c0ab1a8205a2fc34e4bcd40364df902cfa8f3f" 1516 | 1517 | [[package]] 1518 | name = "ucd-trie" 1519 | version = "0.1.7" 1520 | source = "registry+https://github.com/rust-lang/crates.io-index" 1521 | checksum = "2896d95c02a80c6d6a5d6e953d479f5ddf2dfdb6a244441010e373ac0fb88971" 1522 | 1523 | [[package]] 1524 | name = "unicode-ident" 1525 | version = "1.0.17" 1526 | source = "registry+https://github.com/rust-lang/crates.io-index" 1527 | checksum = "00e2473a93778eb0bad35909dff6a10d28e63f792f16ed15e404fca9d5eeedbe" 1528 | 1529 | [[package]] 1530 | name = "unicode-segmentation" 1531 | version = "1.12.0" 1532 | source = "registry+https://github.com/rust-lang/crates.io-index" 1533 | checksum = "f6ccf251212114b54433ec949fd6a7841275f9ada20dddd2f29e9ceea4501493" 1534 | 1535 | [[package]] 1536 | name = "unicode-truncate" 1537 | version = "1.1.0" 1538 | source = "registry+https://github.com/rust-lang/crates.io-index" 1539 | checksum = "b3644627a5af5fa321c95b9b235a72fd24cd29c648c2c379431e6628655627bf" 1540 | dependencies = [ 1541 | "itertools", 1542 | "unicode-segmentation", 1543 | "unicode-width 0.1.14", 1544 | ] 1545 | 1546 | [[package]] 1547 | name = "unicode-width" 1548 | version = "0.1.14" 1549 | source = "registry+https://github.com/rust-lang/crates.io-index" 1550 | checksum = "7dd6e30e90baa6f72411720665d41d89b9a3d039dc45b8faea1ddd07f617f6af" 1551 | 1552 | [[package]] 1553 | name = "unicode-width" 1554 | version = "0.2.0" 1555 | source = "registry+https://github.com/rust-lang/crates.io-index" 1556 | checksum = "1fc81956842c57dac11422a97c3b8195a1ff727f06e85c84ed2e8aa277c9a0fd" 1557 | 1558 | [[package]] 1559 | name = "url" 1560 | version = "2.5.4" 1561 | source = "registry+https://github.com/rust-lang/crates.io-index" 1562 | checksum = "32f8b686cadd1473f4bd0117a5d28d36b1ade384ea9b5069a1c40aefed7fda60" 1563 | dependencies = [ 1564 | "form_urlencoded", 1565 | "idna", 1566 | "percent-encoding", 1567 | ] 1568 | 1569 | [[package]] 1570 | name = "utf16_iter" 1571 | version = "1.0.5" 1572 | source = "registry+https://github.com/rust-lang/crates.io-index" 1573 | checksum = "c8232dd3cdaed5356e0f716d285e4b40b932ac434100fe9b7e0e8e935b9e6246" 1574 | 1575 | [[package]] 1576 | name = "utf8_iter" 1577 | version = "1.0.4" 1578 | source = "registry+https://github.com/rust-lang/crates.io-index" 1579 | checksum = "b6c140620e7ffbb22c2dee59cafe6084a59b5ffc27a8859a5f0d494b5d52b6be" 1580 | 1581 | [[package]] 1582 | name = "utf8parse" 1583 | version = "0.2.2" 1584 | source = "registry+https://github.com/rust-lang/crates.io-index" 1585 | checksum = "06abde3611657adf66d383f00b093d7faecc7fa57071cce2578660c9f1010821" 1586 | 1587 | [[package]] 1588 | name = "valuable" 1589 | version = "0.1.1" 1590 | source = "registry+https://github.com/rust-lang/crates.io-index" 1591 | checksum = "ba73ea9cf16a25df0c8caa16c51acb937d5712a8429db78a3ee29d5dcacd3a65" 1592 | 1593 | [[package]] 1594 | name = "version_check" 1595 | version = "0.9.5" 1596 | source = "registry+https://github.com/rust-lang/crates.io-index" 1597 | checksum = "0b928f33d975fc6ad9f86c8f283853ad26bdd5b10b7f1542aa2fa15e2289105a" 1598 | 1599 | [[package]] 1600 | name = "vscli" 1601 | version = "1.3.0" 1602 | dependencies = [ 1603 | "chrono", 1604 | "clap", 1605 | "clap-verbosity-flag", 1606 | "color-eyre", 1607 | "crossterm 0.28.1", 1608 | "dirs", 1609 | "env_logger", 1610 | "hex", 1611 | "inquire", 1612 | "json5", 1613 | "log", 1614 | "nucleo-matcher", 1615 | "ratatui", 1616 | "serde", 1617 | "serde_json", 1618 | "tui-textarea", 1619 | "url", 1620 | "walkdir", 1621 | "wslpath2", 1622 | ] 1623 | 1624 | [[package]] 1625 | name = "walkdir" 1626 | version = "2.5.0" 1627 | source = "registry+https://github.com/rust-lang/crates.io-index" 1628 | checksum = "29790946404f91d9c5d06f9874efddea1dc06c5efe94541a7d6863108e3a5e4b" 1629 | dependencies = [ 1630 | "same-file", 1631 | "winapi-util", 1632 | ] 1633 | 1634 | [[package]] 1635 | name = "wasi" 1636 | version = "0.11.0+wasi-snapshot-preview1" 1637 | source = "registry+https://github.com/rust-lang/crates.io-index" 1638 | checksum = "9c8d87e72b64a3b4db28d11ce29237c246188f4f51057d65a7eab63b7987e423" 1639 | 1640 | [[package]] 1641 | name = "wasm-bindgen" 1642 | version = "0.2.100" 1643 | source = "registry+https://github.com/rust-lang/crates.io-index" 1644 | checksum = "1edc8929d7499fc4e8f0be2262a241556cfc54a0bea223790e71446f2aab1ef5" 1645 | dependencies = [ 1646 | "cfg-if", 1647 | "once_cell", 1648 | "rustversion", 1649 | "wasm-bindgen-macro", 1650 | ] 1651 | 1652 | [[package]] 1653 | name = "wasm-bindgen-backend" 1654 | version = "0.2.100" 1655 | source = "registry+https://github.com/rust-lang/crates.io-index" 1656 | checksum = "2f0a0651a5c2bc21487bde11ee802ccaf4c51935d0d3d42a6101f98161700bc6" 1657 | dependencies = [ 1658 | "bumpalo", 1659 | "log", 1660 | "proc-macro2", 1661 | "quote", 1662 | "syn", 1663 | "wasm-bindgen-shared", 1664 | ] 1665 | 1666 | [[package]] 1667 | name = "wasm-bindgen-macro" 1668 | version = "0.2.100" 1669 | source = "registry+https://github.com/rust-lang/crates.io-index" 1670 | checksum = "7fe63fc6d09ed3792bd0897b314f53de8e16568c2b3f7982f468c0bf9bd0b407" 1671 | dependencies = [ 1672 | "quote", 1673 | "wasm-bindgen-macro-support", 1674 | ] 1675 | 1676 | [[package]] 1677 | name = "wasm-bindgen-macro-support" 1678 | version = "0.2.100" 1679 | source = "registry+https://github.com/rust-lang/crates.io-index" 1680 | checksum = "8ae87ea40c9f689fc23f209965b6fb8a99ad69aeeb0231408be24920604395de" 1681 | dependencies = [ 1682 | "proc-macro2", 1683 | "quote", 1684 | "syn", 1685 | "wasm-bindgen-backend", 1686 | "wasm-bindgen-shared", 1687 | ] 1688 | 1689 | [[package]] 1690 | name = "wasm-bindgen-shared" 1691 | version = "0.2.100" 1692 | source = "registry+https://github.com/rust-lang/crates.io-index" 1693 | checksum = "1a05d73b933a847d6cccdda8f838a22ff101ad9bf93e33684f39c1f5f0eece3d" 1694 | dependencies = [ 1695 | "unicode-ident", 1696 | ] 1697 | 1698 | [[package]] 1699 | name = "winapi" 1700 | version = "0.3.9" 1701 | source = "registry+https://github.com/rust-lang/crates.io-index" 1702 | checksum = "5c839a674fcd7a98952e593242ea400abe93992746761e38641405d28b00f419" 1703 | dependencies = [ 1704 | "winapi-i686-pc-windows-gnu", 1705 | "winapi-x86_64-pc-windows-gnu", 1706 | ] 1707 | 1708 | [[package]] 1709 | name = "winapi-i686-pc-windows-gnu" 1710 | version = "0.4.0" 1711 | source = "registry+https://github.com/rust-lang/crates.io-index" 1712 | checksum = "ac3b87c63620426dd9b991e5ce0329eff545bccbbb34f3be09ff6fb6ab51b7b6" 1713 | 1714 | [[package]] 1715 | name = "winapi-util" 1716 | version = "0.1.9" 1717 | source = "registry+https://github.com/rust-lang/crates.io-index" 1718 | checksum = "cf221c93e13a30d793f7645a0e7762c55d169dbb0a49671918a2319d289b10bb" 1719 | dependencies = [ 1720 | "windows-sys 0.48.0", 1721 | ] 1722 | 1723 | [[package]] 1724 | name = "winapi-x86_64-pc-windows-gnu" 1725 | version = "0.4.0" 1726 | source = "registry+https://github.com/rust-lang/crates.io-index" 1727 | checksum = "712e227841d057c1ee1cd2fb22fa7e5a5461ae8e48fa2ca79ec42cfc1931183f" 1728 | 1729 | [[package]] 1730 | name = "windows-core" 1731 | version = "0.52.0" 1732 | source = "registry+https://github.com/rust-lang/crates.io-index" 1733 | checksum = "33ab640c8d7e35bf8ba19b884ba838ceb4fba93a4e8c65a9059d08afcfc683d9" 1734 | dependencies = [ 1735 | "windows-targets 0.52.6", 1736 | ] 1737 | 1738 | [[package]] 1739 | name = "windows-link" 1740 | version = "0.1.0" 1741 | source = "registry+https://github.com/rust-lang/crates.io-index" 1742 | checksum = "6dccfd733ce2b1753b03b6d3c65edf020262ea35e20ccdf3e288043e6dd620e3" 1743 | 1744 | [[package]] 1745 | name = "windows-sys" 1746 | version = "0.48.0" 1747 | source = "registry+https://github.com/rust-lang/crates.io-index" 1748 | checksum = "677d2418bec65e3338edb076e806bc1ec15693c5d0104683f2efe857f61056a9" 1749 | dependencies = [ 1750 | "windows-targets 0.48.5", 1751 | ] 1752 | 1753 | [[package]] 1754 | name = "windows-sys" 1755 | version = "0.52.0" 1756 | source = "registry+https://github.com/rust-lang/crates.io-index" 1757 | checksum = "282be5f36a8ce781fad8c8ae18fa3f9beff57ec1b52cb3de0789201425d9a33d" 1758 | dependencies = [ 1759 | "windows-targets 0.52.6", 1760 | ] 1761 | 1762 | [[package]] 1763 | name = "windows-sys" 1764 | version = "0.59.0" 1765 | source = "registry+https://github.com/rust-lang/crates.io-index" 1766 | checksum = "1e38bc4d79ed67fd075bcc251a1c39b32a1776bbe92e5bef1f0bf1f8c531853b" 1767 | dependencies = [ 1768 | "windows-targets 0.52.6", 1769 | ] 1770 | 1771 | [[package]] 1772 | name = "windows-targets" 1773 | version = "0.48.5" 1774 | source = "registry+https://github.com/rust-lang/crates.io-index" 1775 | checksum = "9a2fa6e2155d7247be68c096456083145c183cbbbc2764150dda45a87197940c" 1776 | dependencies = [ 1777 | "windows_aarch64_gnullvm 0.48.5", 1778 | "windows_aarch64_msvc 0.48.5", 1779 | "windows_i686_gnu 0.48.5", 1780 | "windows_i686_msvc 0.48.5", 1781 | "windows_x86_64_gnu 0.48.5", 1782 | "windows_x86_64_gnullvm 0.48.5", 1783 | "windows_x86_64_msvc 0.48.5", 1784 | ] 1785 | 1786 | [[package]] 1787 | name = "windows-targets" 1788 | version = "0.52.6" 1789 | source = "registry+https://github.com/rust-lang/crates.io-index" 1790 | checksum = "9b724f72796e036ab90c1021d4780d4d3d648aca59e491e6b98e725b84e99973" 1791 | dependencies = [ 1792 | "windows_aarch64_gnullvm 0.52.6", 1793 | "windows_aarch64_msvc 0.52.6", 1794 | "windows_i686_gnu 0.52.6", 1795 | "windows_i686_gnullvm", 1796 | "windows_i686_msvc 0.52.6", 1797 | "windows_x86_64_gnu 0.52.6", 1798 | "windows_x86_64_gnullvm 0.52.6", 1799 | "windows_x86_64_msvc 0.52.6", 1800 | ] 1801 | 1802 | [[package]] 1803 | name = "windows_aarch64_gnullvm" 1804 | version = "0.48.5" 1805 | source = "registry+https://github.com/rust-lang/crates.io-index" 1806 | checksum = "2b38e32f0abccf9987a4e3079dfb67dcd799fb61361e53e2882c3cbaf0d905d8" 1807 | 1808 | [[package]] 1809 | name = "windows_aarch64_gnullvm" 1810 | version = "0.52.6" 1811 | source = "registry+https://github.com/rust-lang/crates.io-index" 1812 | checksum = "32a4622180e7a0ec044bb555404c800bc9fd9ec262ec147edd5989ccd0c02cd3" 1813 | 1814 | [[package]] 1815 | name = "windows_aarch64_msvc" 1816 | version = "0.48.5" 1817 | source = "registry+https://github.com/rust-lang/crates.io-index" 1818 | checksum = "dc35310971f3b2dbbf3f0690a219f40e2d9afcf64f9ab7cc1be722937c26b4bc" 1819 | 1820 | [[package]] 1821 | name = "windows_aarch64_msvc" 1822 | version = "0.52.6" 1823 | source = "registry+https://github.com/rust-lang/crates.io-index" 1824 | checksum = "09ec2a7bb152e2252b53fa7803150007879548bc709c039df7627cabbd05d469" 1825 | 1826 | [[package]] 1827 | name = "windows_i686_gnu" 1828 | version = "0.48.5" 1829 | source = "registry+https://github.com/rust-lang/crates.io-index" 1830 | checksum = "a75915e7def60c94dcef72200b9a8e58e5091744960da64ec734a6c6e9b3743e" 1831 | 1832 | [[package]] 1833 | name = "windows_i686_gnu" 1834 | version = "0.52.6" 1835 | source = "registry+https://github.com/rust-lang/crates.io-index" 1836 | checksum = "8e9b5ad5ab802e97eb8e295ac6720e509ee4c243f69d781394014ebfe8bbfa0b" 1837 | 1838 | [[package]] 1839 | name = "windows_i686_gnullvm" 1840 | version = "0.52.6" 1841 | source = "registry+https://github.com/rust-lang/crates.io-index" 1842 | checksum = "0eee52d38c090b3caa76c563b86c3a4bd71ef1a819287c19d586d7334ae8ed66" 1843 | 1844 | [[package]] 1845 | name = "windows_i686_msvc" 1846 | version = "0.48.5" 1847 | source = "registry+https://github.com/rust-lang/crates.io-index" 1848 | checksum = "8f55c233f70c4b27f66c523580f78f1004e8b5a8b659e05a4eb49d4166cca406" 1849 | 1850 | [[package]] 1851 | name = "windows_i686_msvc" 1852 | version = "0.52.6" 1853 | source = "registry+https://github.com/rust-lang/crates.io-index" 1854 | checksum = "240948bc05c5e7c6dabba28bf89d89ffce3e303022809e73deaefe4f6ec56c66" 1855 | 1856 | [[package]] 1857 | name = "windows_x86_64_gnu" 1858 | version = "0.48.5" 1859 | source = "registry+https://github.com/rust-lang/crates.io-index" 1860 | checksum = "53d40abd2583d23e4718fddf1ebec84dbff8381c07cae67ff7768bbf19c6718e" 1861 | 1862 | [[package]] 1863 | name = "windows_x86_64_gnu" 1864 | version = "0.52.6" 1865 | source = "registry+https://github.com/rust-lang/crates.io-index" 1866 | checksum = "147a5c80aabfbf0c7d901cb5895d1de30ef2907eb21fbbab29ca94c5b08b1a78" 1867 | 1868 | [[package]] 1869 | name = "windows_x86_64_gnullvm" 1870 | version = "0.48.5" 1871 | source = "registry+https://github.com/rust-lang/crates.io-index" 1872 | checksum = "0b7b52767868a23d5bab768e390dc5f5c55825b6d30b86c844ff2dc7414044cc" 1873 | 1874 | [[package]] 1875 | name = "windows_x86_64_gnullvm" 1876 | version = "0.52.6" 1877 | source = "registry+https://github.com/rust-lang/crates.io-index" 1878 | checksum = "24d5b23dc417412679681396f2b49f3de8c1473deb516bd34410872eff51ed0d" 1879 | 1880 | [[package]] 1881 | name = "windows_x86_64_msvc" 1882 | version = "0.48.5" 1883 | source = "registry+https://github.com/rust-lang/crates.io-index" 1884 | checksum = "ed94fce61571a4006852b7389a063ab983c02eb1bb37b47f8272ce92d06d9538" 1885 | 1886 | [[package]] 1887 | name = "windows_x86_64_msvc" 1888 | version = "0.52.6" 1889 | source = "registry+https://github.com/rust-lang/crates.io-index" 1890 | checksum = "589f6da84c646204747d1270a2a5661ea66ed1cced2631d546fdfb155959f9ec" 1891 | 1892 | [[package]] 1893 | name = "write16" 1894 | version = "1.0.0" 1895 | source = "registry+https://github.com/rust-lang/crates.io-index" 1896 | checksum = "d1890f4022759daae28ed4fe62859b1236caebfc61ede2f63ed4e695f3f6d936" 1897 | 1898 | [[package]] 1899 | name = "writeable" 1900 | version = "0.5.5" 1901 | source = "registry+https://github.com/rust-lang/crates.io-index" 1902 | checksum = "1e9df38ee2d2c3c5948ea468a8406ff0db0b29ae1ffde1bcf20ef305bcc95c51" 1903 | 1904 | [[package]] 1905 | name = "wslpath2" 1906 | version = "0.1.3" 1907 | source = "registry+https://github.com/rust-lang/crates.io-index" 1908 | checksum = "2db8388b8fbbf9d67e346efc2c1cb216dde5b78981e2eae644a071c160f3acd5" 1909 | 1910 | [[package]] 1911 | name = "yoke" 1912 | version = "0.7.5" 1913 | source = "registry+https://github.com/rust-lang/crates.io-index" 1914 | checksum = "120e6aef9aa629e3d4f52dc8cc43a015c7724194c97dfaf45180d2daf2b77f40" 1915 | dependencies = [ 1916 | "serde", 1917 | "stable_deref_trait", 1918 | "yoke-derive", 1919 | "zerofrom", 1920 | ] 1921 | 1922 | [[package]] 1923 | name = "yoke-derive" 1924 | version = "0.7.5" 1925 | source = "registry+https://github.com/rust-lang/crates.io-index" 1926 | checksum = "2380878cad4ac9aac1e2435f3eb4020e8374b5f13c296cb75b4620ff8e229154" 1927 | dependencies = [ 1928 | "proc-macro2", 1929 | "quote", 1930 | "syn", 1931 | "synstructure", 1932 | ] 1933 | 1934 | [[package]] 1935 | name = "zerofrom" 1936 | version = "0.1.5" 1937 | source = "registry+https://github.com/rust-lang/crates.io-index" 1938 | checksum = "cff3ee08c995dee1859d998dea82f7374f2826091dd9cd47def953cae446cd2e" 1939 | dependencies = [ 1940 | "zerofrom-derive", 1941 | ] 1942 | 1943 | [[package]] 1944 | name = "zerofrom-derive" 1945 | version = "0.1.5" 1946 | source = "registry+https://github.com/rust-lang/crates.io-index" 1947 | checksum = "595eed982f7d355beb85837f651fa22e90b3c044842dc7f2c2842c086f295808" 1948 | dependencies = [ 1949 | "proc-macro2", 1950 | "quote", 1951 | "syn", 1952 | "synstructure", 1953 | ] 1954 | 1955 | [[package]] 1956 | name = "zerovec" 1957 | version = "0.10.4" 1958 | source = "registry+https://github.com/rust-lang/crates.io-index" 1959 | checksum = "aa2b893d79df23bfb12d5461018d408ea19dfafe76c2c7ef6d4eba614f8ff079" 1960 | dependencies = [ 1961 | "yoke", 1962 | "zerofrom", 1963 | "zerovec-derive", 1964 | ] 1965 | 1966 | [[package]] 1967 | name = "zerovec-derive" 1968 | version = "0.10.3" 1969 | source = "registry+https://github.com/rust-lang/crates.io-index" 1970 | checksum = "6eafa6dfb17584ea3e2bd6e76e0cc15ad7af12b09abdd1ca55961bed9b1063c6" 1971 | dependencies = [ 1972 | "proc-macro2", 1973 | "quote", 1974 | "syn", 1975 | ] 1976 | -------------------------------------------------------------------------------- /Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "vscli" 3 | version = "1.3.0" 4 | edition = "2024" 5 | authors = ["Michael Lohr "] 6 | description = "A CLI tool to launch vscode projects, which supports devcontainers." 7 | readme = "README.md" 8 | repository = "https://github.com/michidk/vscli" 9 | documentation = "https://github.com/michidk/vscli" 10 | homepage = "https://github.com/michidk/vscli" 11 | license = "MIT" 12 | keywords = ["development", "settings", "cli"] 13 | categories = ["command-line-utilities"] 14 | 15 | [dependencies] 16 | chrono = { version = "0.4.38", default-features = false, features = ["std", "clock", "serde"]} 17 | clap-verbosity-flag = "3.0.0" 18 | clap = { version = "4.5.4", features = ["derive", "color", "help", "usage", "suggestions", "wrap_help", "env"] } 19 | color-eyre = "0.6.3" 20 | crossterm = { version = "0.28.1"} 21 | dirs = "6.0.0" 22 | env_logger = "0.11.3" 23 | hex = "0.4.3" 24 | inquire = "0.7.5" 25 | json5 = "0.4.1" 26 | log = "0.4.21" 27 | ratatui = { version = "0.29.0"} 28 | serde = { version = "1.0.199", features = ["derive"] } 29 | serde_json = "1.0.116" 30 | url = "2.5.0" 31 | walkdir = "2.5.0" 32 | wslpath2 = "0.1.3" 33 | tui-textarea = "0.7.0" 34 | nucleo-matcher = "0.3.1" 35 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Permission is hereby granted, free of charge, to any person obtaining a copy 4 | of this software and associated documentation files (the "Software"), to deal 5 | in the Software without restriction, including without limitation the rights 6 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 7 | copies of the Software, and to permit persons to whom the Software is 8 | furnished to do so, subject to the following conditions: 9 | 10 | The above copyright notice and this permission notice shall be included in all 11 | copies or substantial portions of the Software. 12 | 13 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 14 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 15 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 16 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 17 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 18 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 19 | SOFTWARE. 20 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # vscli 2 | 3 | [![MIT License](https://img.shields.io/crates/l/vscli)](https://choosealicense.com/licenses/mit/) [![Continuous integration](https://github.com/michidk/vscli/actions/workflows/ci.yml/badge.svg)](https://github.com/michidk/vscli/actions/workflows/ci.yml) 4 | 5 | A CLI/TUI which makes it easy to launch [Visual Studio Code](https://code.visualstudio.com/) (vscode) [dev containers](https://containers.dev/). Also supports other editors like [Cursor](https://www.cursor.com/). 6 | 7 | ![Screenshot showing the recent UI feature.](.github/images/recent.png) 8 | 9 | Read [here](https://blog.lohr.dev/launching-dev-containers) about the journey of reverse engineering Microsoft's dev container CLI in order to make this. 10 | 11 | ## Features 12 | 13 | - A shorthand for launching vscode projects (to be used like the `code` command but with dev container support) 14 | - Supports different editors like `vscode`, `vscode-insiders`, `cursor` and other vscode forks 15 | - Detects whether a project is a [dev container](https://containers.dev/) project, and launches the dev container instead 16 | - Supports [multiple dev containers](https://github.com/microsoft/vscode-docs/blob/main/remote-release-notes/v1_75.md#folders-with-multiple-devcontainerjson-files) in the same project 17 | - Tracks your projects and allows you to open them using a CLI-based UI 18 | 19 | ## Installation 20 | 21 | [![Packaging status](https://repology.org/badge/vertical-allrepos/vscli.svg)](https://repology.org/project/vscli/versions) 22 | 23 | [![Homebrew](https://img.shields.io/badge/homebrew-available-blue?style=flat)](https://github.com/michidk/homebrew-tools/blob/main/Formula/vscli.rb) 24 | 25 | ### [Cargo](https://doc.rust-lang.org/cargo/) 26 | 27 | Install [vscli using cargo](https://crates.io/crates/vscli) on Windows or Linux: 28 | 29 | ```sh 30 | cargo install vscli 31 | ``` 32 | 33 | ### [Homebrew](https://brew.sh/) 34 | 35 | Install [vscli using homebrew](https://github.com/michidk/homebrew-tools/blob/main/Formula/vscli.rb) on Linux or Mac: 36 | 37 | ```sh 38 | brew install michidk/tools/vscli 39 | ``` 40 | 41 | ### [Chocolatey](https://chocolatey.org/) 42 | 43 | Install [vscli using Chocolatey](https://community.chocolatey.org/packages/vscli) on Windows: 44 | 45 | ```sh 46 | choco install vscli 47 | ``` 48 | 49 | ### [Winget](https://learn.microsoft.com/en-us/windows/package-manager/winget/) 50 | 51 | Install [vscli using winget](https://github.com/microsoft/winget-pkgs/tree/master/manifests/m/michidk/vscli) on Windows: 52 | 53 | ```sh 54 | winget install vscli 55 | ``` 56 | 57 | ### Additional steps 58 | 59 | You can set a shorthand alias for `vscli` in your shell's configuration file: 60 | 61 | ```sh 62 | alias vs="vscli open" 63 | alias vsr="vscli recent" 64 | ``` 65 | 66 | ## Usage 67 | 68 | ### Commands 69 | 70 | After installation, the `vscli` command will be available: 71 | 72 | ``` 73 | Usage: vscli [OPTIONS] 74 | 75 | Commands: 76 | open Opens a dev container 77 | recent Opens an interactive list of recently used workspaces 78 | help Print this message or the help of the given subcommand(s) 79 | 80 | Options: 81 | -s, --history-path Overwrite the default path to the history file [env: HISTORY_PATH=] 82 | -d, --dry-run Whether to launch in dry-run mode (not actually open vscode) [env: DRY_RUN=] 83 | -v, --verbose... Increase logging verbosity 84 | -q, --quiet... Decrease logging verbosity 85 | -h, --help Print help 86 | -V, --version Print version 87 | ``` 88 | 89 | #### Open Dev Containers 90 | 91 | Opens a dev container. 92 | 93 | ``` 94 | Usage: vscli open [OPTIONS] [PATH] [ARGS]... 95 | 96 | Arguments: 97 | [PATH] The path of the vscode project to open [default: .] 98 | [ARGS]... Additional arguments to pass to the editor [env: ARGS=] 99 | 100 | Options: 101 | -c, --command The editor command to use (e.g. "code", "code-insiders", "cursor") [env: COMMAND=] 102 | -s, --history-path Overwrite the default path to the history file [env: HISTORY_PATH=] 103 | -b, --behavior Launch behavior [possible values: detect, force-container, force-classic] 104 | -d, --dry-run Whether to launch in dry-run mode (not actually open vscode) [env: DRY_RUN=] 105 | --config Overwrites the path to the dev container config file [env: CONFIG=] 106 | -v, --verbose... Increase logging verbosity 107 | -q, --quiet... Decrease logging verbosity 108 | -h, --help Print help (see more with '--help') 109 | ``` 110 | 111 | #### Recent UI 112 | 113 | Opens an interactive list of recently used workspaces. 114 | 115 | ``` 116 | Usage: vscli recent [OPTIONS] [ARGS]... 117 | 118 | Arguments: 119 | [ARGS]... Additional arguments to pass to the editor [env: ARGS=] 120 | 121 | Options: 122 | --hide-instructions Hide the instruction message in the UI 123 | -s, --history-path Overwrite the default path to the history file [env: HISTORY_PATH=] 124 | -d, --dry-run Whether to launch in dry-run mode (not actually open vscode) [env: DRY_RUN=] 125 | --hide-info Hide additional information like strategy, command, args and dev container path in the UI 126 | -c, --command The editor command to use (e.g. "code", "code-insiders", "cursor") [env: COMMAND=] 127 | -v, --verbose... Increase logging verbosity 128 | -b, --behavior Launch behavior [possible values: detect, force-container, force-classic] 129 | -q, --quiet... Decrease logging verbosity 130 | --config Overwrites the path to the dev container config file [env: CONFIG=] 131 | -h, --help Print help (see more with '--help') 132 | ``` 133 | 134 | Both the `open` and `recent` commands share the same set of launch arguments: 135 | 136 | - `--command`: Specify which editor command to use (e.g., "code", "code-insiders", "cursor") 137 | - `--behavior`: Set the launch behavior ("detect", "force-container", "force-classic") 138 | - `--config`: Override the path to the dev container config file 139 | - Additional arguments can be passed to the editor executable by specifying them after `--` 140 | 141 | The `recent` command additionally supports: 142 | - `--hide-instructions`: Hide the keybinding instructions from the UI 143 | - `--hide-info`: Hide additional information like strategy, command, args and dev container path 144 | 145 | ##### Keybindings 146 | 147 | | Key/Key Combination | Action | Description | 148 | | ------------------------------- | --------------------- | -------------------------------------- | 149 | | `Esc`, `Ctrl+Q` or `Ctrl+C` | Quit | Exits the application. | 150 | | `Down` or `Ctrl+J` | Select Next | Moves to the next selectable item. | 151 | | `Up` or `Ctrl+K` | Select Previous | Moves to the previous selectable item. | 152 | | `KeypadBegin` or `Ctrl+1` | Select First | Selects the first item. | 153 | | `End` or `Ctrl+0` | Select Last | Selects the last item. | 154 | | `Enter` or `Ctrl+O` | Open Selected | Opens the currently selected item. | 155 | | `Delete`, `Ctrl+R`, or `Ctrl+X` | Delete Selected Entry | Deletes the currently selected item. | 156 | 157 | Note: If an input does not match any of the defined keybindings, it is treated as part of a search input. 158 | 159 | ##### Mouse Interactions 160 | 161 | | Mouse Action | Description | 162 | | ------------------------------ | ------------------------------------------------------------ | 163 | | Left Click | Selects an item. Clicking the same item again opens it. | 164 | | Mouse Wheel | Scrolls through the list, moving selection up/down. | 165 | 166 | ##### Launch Behavior 167 | 168 | There are three launch behaviors: 169 | 170 | - `force-classic`: Launch vscode without a dev container 171 | - `force-container`: Launch vscode with a dev container, error if no dev container is found 172 | - `detect`: Detect whether the project is a dev container project, and launch the dev container if it is 173 | 174 | ##### Detection Algorithm 175 | 176 | The detection algorithm determines which dev container config to launch. 177 | 178 | - First, check whether a dev container config was specified via the `--config` flag -> launch it 179 | - Then loads the first dev container it finds 180 | - If more than one exists -> show a interactive list of dev containers and let the user select one 181 | - If one exists -> launch it 182 | - If none exists -> launch vscode normally without a dev container 183 | 184 | ### Examples 185 | 186 | #### Launching a project 187 | 188 | You can launch a project using the default behavior: 189 | 190 | ```sh 191 | vscli open # open vscode in the current directory 192 | vscli open . # open vscode in the current directory 193 | vscli open /path/to/project # open vscode in the specified directory 194 | ``` 195 | 196 | The default behavior tries to detect whether the project is a [dev container](https://containers.dev/) project. If it is, it will launch the dev container instead - if not it will launch vscode normally. 197 | 198 | You can change the launch behavior using the `--behavior` flag: 199 | 200 | ```sh 201 | vscli open --behavior force-container . # force open vscode dev container (even if vscli did not detect a dev container) 202 | vscli open --behavior force-classic . # force open vscode without a dev container (even if vscli did detect a dev container) 203 | ``` 204 | 205 | When you open a project containing more than one dev container config, you will be prompted to select one: 206 | ![Screenshot showing the dev container selection UI.](.github/images/select.png) 207 | 208 | You can specify which editor command to use with the `--command` flag: 209 | 210 | ```sh 211 | vscli open --command cursor . # open using cursor editor 212 | vscli open --command code . # open using vscode (default) 213 | vscli open --command code-insiders . # open using vscode insiders 214 | ``` 215 | 216 | Additional arguments can be passed to the editor executable, by specifying them after `--`: 217 | 218 | ```sh 219 | vscli open . -- --disable-gpu # open the current directory without GPU hardware acceleration 220 | ``` 221 | 222 | Read more about the editor flags by executing `code --help` (or `cursor --help`, etc). 223 | 224 | #### CLI UI 225 | 226 | You can open a CLI-based user interface to display a list of recently opened projects using the `recent` command: 227 | 228 | ```sh 229 | vscli recent # open the CLI-based UI to select a recently opened project to open 230 | vscli recent --command cursor # open the selected project with cursor, ignoring the editor stored in history 231 | vscli recent --behavior force-container # force open the selected project in a dev container 232 | vscli recent --command cursor --behavior detect # open with cursor and detect if dev container should be used 233 | vscli recent --config .devcontainer/custom.json # open with a specific dev container config 234 | vscli recent -- --disable-gpu # pass additional arguments to the editor 235 | vscli recent --hide-instructions # hide the keybinding instructions from the UI 236 | vscli recent --hide-info # hide additional information like strategy, command, args and dev container path 237 | ``` 238 | 239 | The UI mode provides a convenient way to browse and manage your recent workspaces, with customizable display options and full support for all launch configurations. 240 | -------------------------------------------------------------------------------- /renovate.json: -------------------------------------------------------------------------------- 1 | { 2 | "$schema": "https://docs.renovatebot.com/renovate-schema.json", 3 | "extends": [ 4 | "config:recommended", 5 | ":enableVulnerabilityAlertsWithLabel('security')" 6 | ], 7 | "labels": [ 8 | "no-stale", 9 | "bot" 10 | ], 11 | "packageRules": [ 12 | { 13 | "matchManagers": [ 14 | "cargo" 15 | ], 16 | "groupName": "All Cargo Dependencies" 17 | } 18 | ] 19 | } 20 | -------------------------------------------------------------------------------- /rust-toolchain.toml: -------------------------------------------------------------------------------- 1 | [toolchain] 2 | channel = "stable" 3 | components = [ "rustfmt", "clippy" ] 4 | -------------------------------------------------------------------------------- /src/history.rs: -------------------------------------------------------------------------------- 1 | use chrono::{DateTime, Utc}; 2 | use color_eyre::eyre::{Context, Result, eyre}; 3 | use log::{debug, warn}; 4 | use serde::{Deserialize, Serialize}; 5 | use std::{ 6 | cmp::Ordering, 7 | collections::HashMap, 8 | fs::{self, File}, 9 | path::PathBuf, 10 | sync::atomic::AtomicUsize, 11 | }; 12 | 13 | use crate::launch::Behavior; 14 | 15 | /// The maximum number of entries to keep in the history 16 | // This is an arbitrary number, but it should be enough to keep the history manageable 17 | const MAX_HISTORY_ENTRIES: usize = 35; 18 | 19 | /// An entry in the history 20 | #[derive(Debug, Clone, Serialize, Deserialize)] 21 | pub struct Entry { 22 | /// The name of the workspace 23 | pub workspace_name: String, 24 | /// The name of the dev container, if it exists 25 | pub dev_container_name: Option, 26 | /// The path to the vscode workspace 27 | pub workspace_path: PathBuf, 28 | /// The path to the dev container config, if it exists 29 | pub config_path: Option, 30 | /// The launch behavior 31 | pub behavior: Behavior, 32 | /// The time this entry was last opened 33 | pub last_opened: DateTime, // not used in PartialEq, Eq, Hash 34 | } 35 | 36 | // Custom comparison which ignores `last_opened` (and `name`) 37 | // This is used so that we don't add duplicate entries with different timestamps 38 | impl PartialEq for Entry { 39 | fn eq(&self, other: &Self) -> bool { 40 | self.workspace_path == other.workspace_path 41 | && self.config_path == other.config_path 42 | && self.behavior == other.behavior 43 | } 44 | } 45 | 46 | impl Eq for Entry {} 47 | 48 | // Required by BTreeSet since it's sorted 49 | impl Ord for Entry { 50 | fn cmp(&self, other: &Self) -> std::cmp::Ordering { 51 | // check if two are equal by comparing all properties, ignoring `last_opened` (calling custom `.eq()`) 52 | if self.eq(other) { 53 | return Ordering::Equal; 54 | } 55 | // If they are not equal, the ordering is given by `last_opened` 56 | self.last_opened.cmp(&other.last_opened) 57 | } 58 | } 59 | 60 | // Same as `Ord` 61 | impl PartialOrd for Entry { 62 | fn partial_cmp(&self, other: &Self) -> Option { 63 | Some(self.cmp(other)) 64 | } 65 | } 66 | 67 | #[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash)] 68 | pub struct EntryId(usize); 69 | 70 | impl EntryId { 71 | pub fn new() -> Self { 72 | static GLOBAL_ID: AtomicUsize = AtomicUsize::new(0); 73 | Self(GLOBAL_ID.fetch_add(1, std::sync::atomic::Ordering::SeqCst)) 74 | } 75 | } 76 | 77 | /// Contains the recent used workspaces 78 | /// 79 | /// # Note 80 | /// We use a `BTreeSet` so it's sorted and does not contain duplicates 81 | #[derive(Default, Debug, Clone)] 82 | pub struct History(HashMap); 83 | 84 | impl History { 85 | pub fn from_entries(entries: Vec) -> Self { 86 | Self( 87 | entries 88 | .into_iter() 89 | .map(|entry| (EntryId::new(), entry)) 90 | .collect(), 91 | ) 92 | } 93 | 94 | pub fn insert(&mut self, entry: Entry) -> EntryId { 95 | let id = EntryId::new(); 96 | assert_eq!(self.0.insert(id, entry), None); 97 | id 98 | } 99 | 100 | pub fn update(&mut self, id: EntryId, entry: Entry) -> Option { 101 | if let std::collections::hash_map::Entry::Occupied(mut e) = self.0.entry(id) { 102 | return Some(e.insert(entry)); 103 | } 104 | None 105 | } 106 | 107 | pub fn delete(&mut self, id: EntryId) -> Option { 108 | self.0.remove(&id) 109 | } 110 | 111 | pub fn upsert(&mut self, entry: Entry) -> EntryId { 112 | if let Some(id) = self 113 | .0 114 | .iter_mut() 115 | .find_map(|(id, history_entry)| (history_entry == &entry).then_some(*id)) 116 | { 117 | assert!( 118 | self.update(id, entry).is_some(), 119 | "Existing history entry to be replaced" 120 | ); 121 | id 122 | } else { 123 | self.insert(entry) 124 | } 125 | } 126 | 127 | pub fn iter(&self) -> std::collections::hash_map::Iter<'_, EntryId, Entry> { 128 | self.0.iter() 129 | } 130 | 131 | pub fn into_entries(self) -> Vec { 132 | self.0.into_values().collect() 133 | } 134 | } 135 | 136 | /// Manages the history and tracks the recently used workspaces 137 | pub struct Tracker { 138 | /// The path to the history file 139 | path: PathBuf, 140 | /// The history struct 141 | pub history: History, 142 | } 143 | 144 | impl Tracker { 145 | /// Loads the history from a file 146 | pub fn load>(path: P) -> Result { 147 | // Code size optimization: With rusts monomorphization it would generate 148 | // a "new/separate" function for each generic argument used to call this function. 149 | // Having this inner function does not prevent it but can drastically cuts down on generated code size. 150 | fn load_inner(path: PathBuf) -> Result { 151 | if !path.exists() { 152 | // cap of 1, because in the application lifetime, we only ever add one element before exiting 153 | return Ok(Tracker { 154 | path, 155 | history: History::default(), 156 | }); 157 | } 158 | 159 | let file = File::open(&path)?; 160 | match serde_json::from_reader::<_, Vec>(file) { 161 | Ok(entries) => { 162 | debug!("Imported {:?} history entries", entries.len()); 163 | 164 | Ok(Tracker { 165 | path, 166 | history: History::from_entries(entries), 167 | }) 168 | } 169 | Err(err) => { 170 | // ignore parsing errors 171 | // move the file and start from scratch 172 | 173 | // find a non-existent backup file 174 | let new_path = (0..10_000) // Set an upper limit of filename checks. 175 | .map(|i| path.with_file_name(format!(".history_{i}.json.bak"))) 176 | .find(|path| !path.exists()) 177 | .unwrap_or_else(|| path.with_file_name(".history.json.bak")); 178 | 179 | fs::rename(&path, &new_path).wrap_err_with(|| { 180 | format!( 181 | "Could not move history file from `{}` to `{}`", 182 | path.display(), 183 | new_path.display() 184 | ) 185 | })?; 186 | 187 | warn!( 188 | "Could not read history file: {err}\nMoved broken file to `{}`", 189 | new_path.display() 190 | ); 191 | 192 | Ok(Tracker { 193 | path, 194 | history: History::default(), 195 | }) 196 | } 197 | } 198 | } 199 | 200 | let path = path.into(); 201 | load_inner(path) 202 | } 203 | 204 | /// Saves the history, guaranteeing a size of `MAX_HISTORY_ENTRIES` 205 | pub fn store(self) -> Result<()> { 206 | fs::create_dir_all( 207 | self.path 208 | .parent() 209 | .ok_or_else(|| eyre!("Parent directory not found"))?, 210 | )?; 211 | let file = File::create(self.path)?; 212 | 213 | // since history is sorted, we can remove the first entries to limit the max size 214 | let entries: Vec = self 215 | .history 216 | .into_entries() 217 | .into_iter() 218 | .take(MAX_HISTORY_ENTRIES) 219 | .collect(); 220 | 221 | serde_json::to_writer_pretty(file, &entries)?; 222 | Ok(()) 223 | } 224 | } 225 | -------------------------------------------------------------------------------- /src/launch.rs: -------------------------------------------------------------------------------- 1 | use std::{ffi::OsString, fmt::Display, path::PathBuf, str::FromStr}; 2 | 3 | use clap::ValueEnum; 4 | use color_eyre::eyre::{self, Result, bail, eyre}; 5 | use inquire::Select; 6 | use log::{info, trace}; 7 | use serde::{Deserialize, Serialize}; 8 | 9 | use crate::workspace::{DevContainer, Workspace}; 10 | 11 | pub const LAUNCH_DETECT: &str = "detect"; 12 | pub const LAUNCH_FORCE_CONTAINER: &str = "force-container"; 13 | pub const LAUNCH_FORCE_CLASSIC: &str = "force-classic"; 14 | 15 | /// Set the dev container launch strategy of vscode. 16 | #[derive( 17 | Debug, 18 | Default, 19 | Clone, 20 | Copy, 21 | PartialEq, 22 | Eq, 23 | PartialOrd, 24 | Ord, 25 | Hash, 26 | ValueEnum, 27 | Serialize, 28 | Deserialize, 29 | )] 30 | pub enum ContainerStrategy { 31 | /// Use dev container if it was detected 32 | #[default] 33 | Detect, 34 | /// Force open with dev container, even if no config was found 35 | ForceContainer, 36 | /// Ignore dev container 37 | ForceClassic, 38 | } 39 | 40 | impl FromStr for ContainerStrategy { 41 | type Err = eyre::Error; 42 | 43 | fn from_str(s: &str) -> Result { 44 | match s { 45 | LAUNCH_DETECT => Ok(Self::Detect), 46 | LAUNCH_FORCE_CONTAINER => Ok(Self::ForceContainer), 47 | LAUNCH_FORCE_CLASSIC => Ok(Self::ForceClassic), 48 | _ => Err(eyre!("Invalid launch behavior: {}", s)), 49 | } 50 | } 51 | } 52 | 53 | impl Display for ContainerStrategy { 54 | fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { 55 | match self { 56 | Self::Detect => f.write_str(LAUNCH_DETECT), 57 | Self::ForceContainer => f.write_str(LAUNCH_FORCE_CONTAINER), 58 | Self::ForceClassic => f.write_str(LAUNCH_FORCE_CLASSIC), 59 | } 60 | } 61 | } 62 | 63 | /// The launch behavior that is used to start vscode (saved in the history file) 64 | #[derive(Debug, Clone, PartialEq, Eq, Hash, PartialOrd, Ord, Serialize, Deserialize)] 65 | pub struct Behavior { 66 | /// The strategy to use for launching the container. 67 | pub strategy: ContainerStrategy, 68 | /// Additional arguments to pass to the editor. 69 | pub args: Vec, 70 | /// The editor command to use (e.g. "code", "code-insiders", "cursor") 71 | #[serde(default = "default_editor_command")] 72 | pub command: String, 73 | } 74 | 75 | fn default_editor_command() -> String { 76 | "code".to_string() 77 | } 78 | 79 | /// Formats the editor name based on the command for display in messages. 80 | fn format_editor_name(command: &str) -> String { 81 | match command.to_lowercase().as_str() { 82 | "code" => "Visual Studio Code".to_string(), 83 | "code-insiders" => "Visual Studio Code Insiders".to_string(), 84 | "cursor" => "Cursor".to_string(), 85 | "codium" => "VSCodium".to_string(), 86 | "positron" => "Positron".to_string(), 87 | _ => format!("'{command}'"), 88 | } 89 | } 90 | 91 | /// The configuration for the launch behavior 92 | #[derive(Debug, Clone, PartialEq, Eq, Hash)] 93 | pub struct Setup { 94 | /// The workspace configuration. 95 | workspace: Workspace, 96 | /// The behavior configuration. 97 | behavior: Behavior, 98 | /// Whether to perform a dry run, not actually launching the editor. 99 | dry_run: bool, 100 | } 101 | 102 | impl Setup { 103 | pub fn new(workspace: Workspace, behavior: Behavior, dry_run: bool) -> Self { 104 | Self { 105 | workspace, 106 | behavior, 107 | dry_run, 108 | } 109 | } 110 | 111 | /// Selects the dev container that should be used. 112 | fn detect(&self, config: Option) -> Result> { 113 | let name = self.workspace.name.clone(); 114 | 115 | if let Some(config) = config { 116 | trace!("Dev container set by path: {config:?}"); 117 | Ok(Some(DevContainer::from_config(config.as_path(), &name)?)) 118 | } else { 119 | let configs = self.workspace.find_dev_container_configs(); 120 | let dev_containers = self.workspace.load_dev_containers(&configs)?; 121 | 122 | match configs.len() { 123 | 0 => { 124 | trace!("No dev container specified."); 125 | Ok(None) 126 | } 127 | 1 => { 128 | trace!("Selected the only existing dev container."); 129 | Ok(dev_containers.into_iter().next()) 130 | } 131 | _ => Ok(Some( 132 | Select::new( 133 | "Multiple dev containers found! Please select one:", 134 | dev_containers, 135 | ) 136 | .prompt()?, 137 | )), 138 | } 139 | } 140 | } 141 | 142 | /// Launches vscode with the given configuration. 143 | /// Returns the dev container that was used, if any. 144 | pub fn launch(self, config: Option) -> Result> { 145 | let editor_name = format_editor_name(&self.behavior.command); 146 | 147 | match self.behavior.strategy { 148 | ContainerStrategy::Detect => { 149 | let dev_container = self.detect(config)?; 150 | 151 | if let Some(ref dev_container) = dev_container { 152 | info!("Opening dev container with {}...", editor_name); 153 | self.workspace.open( 154 | self.behavior.args, 155 | self.dry_run, 156 | dev_container, 157 | &self.behavior.command, 158 | )?; 159 | } else { 160 | info!( 161 | "No dev container found, opening on host system with {}...", 162 | editor_name 163 | ); 164 | self.workspace.open_classic( 165 | self.behavior.args, 166 | self.dry_run, 167 | &self.behavior.command, 168 | )?; 169 | } 170 | Ok(dev_container) 171 | } 172 | ContainerStrategy::ForceContainer => { 173 | let dev_container = self.detect(config)?; 174 | 175 | if let Some(ref dev_container) = dev_container { 176 | info!("Force opening dev container with {}...", editor_name); 177 | self.workspace.open( 178 | self.behavior.args, 179 | self.dry_run, 180 | dev_container, 181 | &self.behavior.command, 182 | )?; 183 | } else { 184 | bail!( 185 | "No dev container found, but was forced to open it using dev containers." 186 | ); 187 | } 188 | Ok(dev_container) 189 | } 190 | ContainerStrategy::ForceClassic => { 191 | info!("Opening without dev containers using {}...", editor_name); 192 | self.workspace.open_classic( 193 | self.behavior.args, 194 | self.dry_run, 195 | &self.behavior.command, 196 | )?; 197 | Ok(None) 198 | } 199 | } 200 | } 201 | } 202 | -------------------------------------------------------------------------------- /src/main.rs: -------------------------------------------------------------------------------- 1 | #![warn( 2 | missing_docs, 3 | missing_debug_implementations, 4 | missing_copy_implementations 5 | )] 6 | #![warn(clippy::pedantic)] 7 | 8 | //! A CLI tool to launch vscode projects, which supports dev container. 9 | 10 | mod history; 11 | mod launch; 12 | mod opts; 13 | mod ui; 14 | mod uri; 15 | mod workspace; 16 | 17 | use chrono::Utc; 18 | use clap::Parser; 19 | use color_eyre::eyre::Result; 20 | use log::trace; 21 | use std::io::Write; 22 | 23 | use crate::history::{Entry, Tracker}; 24 | 25 | use crate::{ 26 | launch::{Behavior, Setup}, 27 | opts::Opts, 28 | workspace::Workspace, 29 | }; 30 | 31 | /// Entry point for `vscli`. 32 | fn main() -> Result<()> { 33 | color_eyre::install()?; 34 | 35 | let opts = Opts::parse(); 36 | let opts_dbg = format!("{opts:#?}"); 37 | 38 | env_logger::Builder::from_default_env() 39 | .filter_level(opts.verbose.log_level_filter()) 40 | .format(move |buf, record| log_format(buf, record, opts.verbose.log_level_filter())) 41 | .init(); 42 | 43 | trace!("Parsed Opts:\n{}", opts_dbg); 44 | 45 | // Setup the tracker 46 | let mut tracker = { 47 | let tracker_path = if let Some(path) = opts.history_path { 48 | path 49 | } else { 50 | let mut tracker_path = dirs::data_local_dir().expect("Local data dir not found."); 51 | tracker_path.push("vscli"); 52 | tracker_path.push("history.json"); 53 | tracker_path 54 | }; 55 | Tracker::load(tracker_path)? 56 | }; 57 | 58 | match opts.command { 59 | opts::Commands::Open { path, launch } => { 60 | // Get workspace from args 61 | let path = path.as_path(); 62 | let ws = Workspace::from_path(path)?; 63 | let ws_name = ws.name.clone(); 64 | 65 | // Open the container 66 | let behavior = Behavior { 67 | strategy: launch.behavior.unwrap_or_default(), 68 | args: launch.args, 69 | command: launch.command.unwrap_or_else(|| "code".to_string()), 70 | }; 71 | let setup = Setup::new(ws, behavior.clone(), opts.dry_run); 72 | let dev_container = setup.launch(launch.config)?; 73 | 74 | // Store the workspace in the history 75 | tracker.history.upsert(Entry { 76 | workspace_name: ws_name, 77 | dev_container_name: dev_container.as_ref().and_then(|dc| dc.name.clone()), 78 | workspace_path: path.canonicalize()?, 79 | config_path: dev_container.map(|dc| dc.config_path), 80 | behavior, 81 | last_opened: Utc::now(), 82 | }); 83 | } 84 | opts::Commands::Recent { 85 | launch, 86 | hide_instructions, 87 | hide_info, 88 | } => { 89 | // Get workspace from user selection 90 | let res = ui::start(&mut tracker, hide_instructions, hide_info)?; 91 | if let Some((id, mut entry)) = res { 92 | let ws = Workspace::from_path(&entry.workspace_path)?; 93 | let ws_name = ws.name.clone(); 94 | 95 | // Override command if specified 96 | if let Some(cmd) = launch.command { 97 | entry.behavior.command = cmd; 98 | } 99 | 100 | // Override behavior if specified 101 | if let Some(beh) = launch.behavior { 102 | entry.behavior.strategy = beh; 103 | } 104 | 105 | // Override args if specified and non-empty 106 | if !launch.args.is_empty() { 107 | entry.behavior.args = launch.args; 108 | } 109 | 110 | // Override config if specified 111 | if launch.config.is_some() { 112 | entry.config_path = launch.config; 113 | } 114 | 115 | // Open the container 116 | let setup = Setup::new(ws, entry.behavior.clone(), opts.dry_run); 117 | let dev_container = setup.launch(entry.config_path)?; 118 | 119 | // Update the tracker entry 120 | tracker.history.update( 121 | id, 122 | Entry { 123 | workspace_name: ws_name, 124 | dev_container_name: dev_container.as_ref().and_then(|dc| dc.name.clone()), 125 | workspace_path: entry.workspace_path.clone(), 126 | config_path: dev_container.map(|dc| dc.config_path), 127 | behavior: entry.behavior.clone(), 128 | last_opened: Utc::now(), 129 | }, 130 | ); 131 | } 132 | } 133 | } 134 | 135 | tracker.store()?; 136 | 137 | Ok(()) 138 | } 139 | 140 | /// Formats the log messages in a minimalistic way, since we don't have a lot of output. 141 | fn log_format( 142 | buf: &mut env_logger::fmt::Formatter, 143 | record: &log::Record, 144 | filter: log::LevelFilter, 145 | ) -> std::io::Result<()> { 146 | let level = record.level(); 147 | let level_char = match level { 148 | log::Level::Trace => 'T', 149 | log::Level::Debug => 'D', 150 | log::Level::Info => 'I', 151 | log::Level::Warn => 'W', 152 | log::Level::Error => 'E', 153 | }; 154 | // color using shell escape codes 155 | let colored_level = match level { 156 | log::Level::Trace => format!("\x1b[37m{level_char}\x1b[0m"), 157 | log::Level::Debug => format!("\x1b[36m{level_char}\x1b[0m"), 158 | log::Level::Info => format!("\x1b[32m{level_char}\x1b[0m"), 159 | log::Level::Warn => format!("\x1b[33m{level_char}\x1b[0m"), 160 | log::Level::Error => format!("\x1b[31m{level_char}\x1b[0m"), 161 | }; 162 | 163 | // Default behavior (for info messages): only print message 164 | // but if level is not info and filter is set, prefix it with the colored level 165 | if level == log::Level::Info && filter == log::LevelFilter::Info { 166 | writeln!(buf, "{}", record.args()) 167 | } else { 168 | writeln!(buf, "{}: {}", colored_level, record.args()) 169 | } 170 | } 171 | -------------------------------------------------------------------------------- /src/opts.rs: -------------------------------------------------------------------------------- 1 | use std::{ffi::OsString, path::PathBuf}; 2 | 3 | use clap::{Args, Parser, Subcommand, command}; 4 | 5 | use crate::launch::ContainerStrategy; 6 | 7 | /// Main CLI arguments 8 | #[derive(Parser, Debug)] 9 | #[command( 10 | name = "vscli", 11 | about = "A CLI tool to launch vscode projects, which supports dev containers.", 12 | author, 13 | version, 14 | about 15 | )] 16 | pub(crate) struct Opts { 17 | /// Overwrite the default path to the history file 18 | #[arg(short = 's', long, env, global = true)] 19 | pub history_path: Option, 20 | 21 | /// Whether to launch in dry-run mode (not actually open vscode) 22 | #[arg(short, long, alias = "dry", env, global = true)] 23 | pub dry_run: bool, 24 | 25 | /// The verbosity of the output 26 | #[command(flatten)] 27 | pub verbose: clap_verbosity_flag::Verbosity, 28 | 29 | /// The command to run 30 | #[command(subcommand)] 31 | pub command: Commands, 32 | } 33 | 34 | /// Arguments for launching an editor 35 | #[derive(Args, Debug, Clone)] 36 | pub(crate) struct LaunchArgs { 37 | /// The editor command to use (e.g. "code", "code-insiders", "cursor") 38 | #[arg(short, long, env)] 39 | pub command: Option, 40 | 41 | /// Launch behavior 42 | #[arg(short, long, ignore_case = true)] 43 | pub behavior: Option, 44 | 45 | /// Overwrites the path to the dev container config file 46 | #[arg(long, env)] 47 | pub config: Option, 48 | 49 | /// Additional arguments to pass to the editor 50 | #[arg(value_parser, env)] 51 | pub args: Vec, 52 | } 53 | 54 | #[derive(Subcommand, Debug)] 55 | pub(crate) enum Commands { 56 | /// Opens a dev container. 57 | #[clap(alias = "o")] 58 | Open { 59 | /// The path of the vscode project to open 60 | #[arg(value_parser, default_value = ".")] 61 | path: PathBuf, 62 | 63 | #[command(flatten)] 64 | launch: LaunchArgs, 65 | }, 66 | /// Opens an interactive list of recently used workspaces. 67 | #[clap(alias = "ui")] 68 | Recent { 69 | /// Hide the instruction message in the UI 70 | #[arg(long)] 71 | hide_instructions: bool, 72 | 73 | /// Hide additional information like strategy, command, args and dev container path in the UI 74 | #[arg(long)] 75 | hide_info: bool, 76 | 77 | #[command(flatten)] 78 | launch: LaunchArgs, 79 | }, 80 | } 81 | -------------------------------------------------------------------------------- /src/ui.rs: -------------------------------------------------------------------------------- 1 | use chrono::{DateTime, Local}; 2 | use color_eyre::eyre::Result; 3 | use crossterm::{ 4 | event::{ 5 | self, DisableMouseCapture, EnableMouseCapture, Event, KeyCode, KeyEventKind, KeyModifiers, 6 | MouseButton, MouseEvent, MouseEventKind, 7 | }, 8 | execute, 9 | terminal::{EnterAlternateScreen, LeaveAlternateScreen, disable_raw_mode, enable_raw_mode}, 10 | }; 11 | use log::debug; 12 | use nucleo_matcher::{ 13 | Matcher, Utf32Str, 14 | pattern::{AtomKind, CaseMatching, Normalization, Pattern}, 15 | }; 16 | use ratatui::{ 17 | Frame, Terminal, 18 | backend::{Backend, CrosstermBackend}, 19 | layout::{Constraint, Layout}, 20 | prelude::{Alignment, Rect}, 21 | style::{Color, Style}, 22 | text::Span, 23 | widgets::{ 24 | Block, Borders, Cell, Padding, Paragraph, Row, Scrollbar, ScrollbarOrientation, 25 | ScrollbarState, Table, TableState, 26 | }, 27 | }; 28 | use std::{borrow::Cow, io}; 29 | use tui_textarea::TextArea; 30 | 31 | use crate::history::{Entry, EntryId, History, Tracker}; 32 | 33 | /// All "user triggered" action which the app might want to perform. 34 | #[derive(Debug, Clone, PartialEq, Eq)] 35 | enum AppAction { 36 | Quit, 37 | SelectNext, 38 | SelectPrevious, 39 | SelectFirst, 40 | SelectLast, 41 | OpenSelected, 42 | DeleteSelectedEntry, 43 | SearchInput(tui_textarea::Input), 44 | TableClick(u16), // New variant for table clicks with row position 45 | } 46 | 47 | /// Represents a single record/entry of the UI table. 48 | /// 49 | /// Additional to the representation ([`Self::row`]) it also contains other meta information to 50 | /// make e.g. filtering possible and efficient. 51 | #[derive(Debug, Clone)] 52 | struct TableRow { 53 | id: EntryId, 54 | entry: Entry, 55 | row: Row<'static>, 56 | search_score: Option, 57 | } 58 | 59 | impl From<(EntryId, Entry)> for TableRow { 60 | fn from((id, value): (EntryId, Entry)) -> Self { 61 | let cells: Vec = vec![ 62 | value.workspace_name.to_string(), 63 | value 64 | .dev_container_name 65 | .as_deref() 66 | .unwrap_or("") 67 | .to_string(), 68 | value.workspace_path.to_string_lossy().to_string(), 69 | DateTime::::from(value.last_opened) 70 | .format("%Y-%m-%d %H:%M:%S") 71 | .to_string(), 72 | ]; 73 | let row = Row::new(cells).height(1); 74 | 75 | Self { 76 | id, 77 | row, 78 | entry: value, 79 | search_score: Some(0), 80 | } 81 | } 82 | } 83 | 84 | /// Contains all UI related elements to display and operate on the entries of the table. 85 | #[derive(Debug, Clone)] 86 | struct TableData { 87 | /// Be very careful when accessing this value directly as it represents all values regardless of 88 | /// applied filter or not. It only makes sense if the action does not care about the filter and 89 | /// if entries are accessed by id and not index/position. 90 | /// 91 | /// Most of the times [`Self::to_rows`] or [`Self::as_rows_full`] are desired. 92 | rows: Vec, 93 | 94 | /// Caches the longest workspace name [`Self::rows`] contains. 95 | /// 96 | /// Note that this value does not change for a "session" even if a filter is applied and/or the 97 | /// longest entry is deleted. 98 | max_worspace_name_len: Option, 99 | 100 | /// Caches the longest devcontainer name [`Self::rows`] contains. 101 | /// 102 | /// Note that this value does not change for a "session" even if a filter is applied and/or the 103 | /// longest entry is deleted. 104 | max_devcontainer_name_len: Option, 105 | } 106 | 107 | impl TableData { 108 | pub const HEADER: [&'static str; 4] = ["Workspace", "Dev Container", "Path", "Last Opened"]; 109 | 110 | pub fn from_iter>(iter: I) -> Self { 111 | let mut this = Self { 112 | rows: iter.into_iter().map(TableRow::from).collect(), 113 | 114 | max_devcontainer_name_len: None, 115 | max_worspace_name_len: None, 116 | }; 117 | 118 | // Sort by `Last Opened` to keep same logic as previous versions 119 | // Inverted to have newest at the top with ASC order 120 | this.rows 121 | .sort_by_key(|entry| -entry.entry.last_opened.timestamp()); 122 | 123 | this.max_worspace_name_len = this 124 | .rows 125 | .iter() 126 | .map(|row| row.entry.workspace_name.len()) 127 | .max(); 128 | 129 | this.max_devcontainer_name_len = this 130 | .rows 131 | .iter() 132 | .map(|row| row.entry.dev_container_name.as_deref().unwrap_or("").len()) 133 | .max(); 134 | 135 | this 136 | } 137 | 138 | pub fn to_rows(&self) -> Vec> { 139 | self.rows 140 | .iter() 141 | .filter(|row| row.search_score.is_some()) 142 | .map(|row| &row.row) 143 | .cloned() 144 | .collect() 145 | } 146 | 147 | pub fn as_rows_full(&self) -> impl Iterator { 148 | self.rows.iter().filter(|row| row.search_score.is_some()) 149 | } 150 | 151 | pub fn apply_filter(&mut self, pattern: &str) -> bool { 152 | let mut changes = false; 153 | let mut matcher = Matcher::default(); 154 | let mut buf = Vec::new(); 155 | 156 | let pattern = Pattern::new( 157 | pattern, 158 | CaseMatching::Ignore, 159 | Normalization::Smart, 160 | AtomKind::Fuzzy, 161 | ); 162 | 163 | for row in &mut self.rows { 164 | let workspace_name = row.entry.workspace_name.as_str(); 165 | let container_name = row.entry.dev_container_name.as_deref().unwrap_or(""); 166 | let path_str = row.entry.workspace_path.to_string_lossy(); 167 | 168 | let new_search_score = add_num_opt( 169 | add_num_opt( 170 | pattern.score(Utf32Str::new(workspace_name, &mut buf), &mut matcher), 171 | pattern.score(Utf32Str::new(container_name, &mut buf), &mut matcher), 172 | ), 173 | pattern.score(Utf32Str::new(path_str.as_ref(), &mut buf), &mut matcher), 174 | ); 175 | changes |= new_search_score != row.search_score; 176 | row.search_score = new_search_score; 177 | } 178 | 179 | self.rows 180 | .sort_by_key(|row| u32::MAX - row.search_score.unwrap_or(0)); 181 | 182 | changes 183 | } 184 | 185 | pub fn reset_filter(&mut self) { 186 | for row in &mut self.rows { 187 | row.search_score = Some(0); 188 | } 189 | 190 | // Sort by `Last Opened` to keep same logic as previous versions 191 | // Inverted to have newest at the top with ASC order 192 | self.rows 193 | .sort_by_key(|entry| -entry.entry.last_opened.timestamp()); 194 | } 195 | } 196 | 197 | /// The UI state 198 | struct UI<'a> { 199 | search: TextArea<'a>, 200 | table_state: TableState, 201 | table_data: TableData, 202 | hide_instructions: bool, 203 | hide_info: bool, 204 | last_clicked_index: Option, // Track the last clicked row 205 | } 206 | 207 | impl<'a> UI<'a> { 208 | /// Create new empty state from history tracker reference 209 | pub fn new(history: &History, hide_instructions: bool, hide_info: bool) -> UI<'a> { 210 | UI { 211 | search: TextArea::default(), 212 | table_state: TableState::default(), 213 | table_data: TableData::from_iter( 214 | history.iter().map(|(id, entry)| (*id, entry.clone())), 215 | ), 216 | hide_instructions, 217 | hide_info, 218 | last_clicked_index: None, 219 | } 220 | } 221 | 222 | /// Select the next entry with wrapping 223 | pub fn select_next(&mut self) { 224 | let len = self.table_data.as_rows_full().count(); 225 | if len == 0 { 226 | return; 227 | } 228 | 229 | let i = self.table_state.selected().unwrap_or(0); 230 | self.table_state.select(Some((i + 1) % len)); 231 | } 232 | 233 | /// Select the previous entry with wrapping 234 | pub fn select_previous(&mut self) { 235 | let len = self.table_data.as_rows_full().count(); 236 | if len == 0 { 237 | return; 238 | } 239 | 240 | let i = self.table_state.selected().unwrap_or(len - 1); 241 | self.table_state.select(Some((i + len - 1) % len)); 242 | } 243 | 244 | pub fn select_first(&mut self) { 245 | self.table_state.select_first(); 246 | } 247 | 248 | pub fn select_last(&mut self) { 249 | self.table_state.select_last(); 250 | } 251 | 252 | pub fn apply_filter(&mut self, pattern: Option<&str>) { 253 | let pattern = pattern.unwrap_or(""); 254 | 255 | let prev_selected = self.get_selected_row(); 256 | 257 | let update_selected = if pattern.trim().is_empty() { 258 | self.reset_filter(); 259 | true 260 | } else { 261 | self.table_data.apply_filter(pattern) 262 | }; 263 | 264 | if !update_selected { 265 | return; 266 | } 267 | 268 | // See if selected item is still visible. If not select first, else reselect (index changed) 269 | if let Some(selected) = prev_selected { 270 | let new_rows = self.table_data.as_rows_full(); 271 | 272 | match new_rows 273 | .enumerate() 274 | .find_map(|(index, entry)| (entry.id == selected.id).then_some(index)) 275 | { 276 | Some(index) => { 277 | // Update index 278 | self.table_state.select(Some(index)); 279 | } 280 | _ => { 281 | self.table_state.select_first(); 282 | } 283 | } 284 | } else { 285 | self.table_state.select_first(); 286 | } 287 | } 288 | 289 | pub fn reset_filter(&mut self) { 290 | self.table_data.reset_filter(); 291 | } 292 | 293 | fn get_selected_row(&self) -> Option { 294 | let index = self.table_state.selected()?; 295 | self.table_data.as_rows_full().nth(index).cloned() 296 | } 297 | 298 | fn delete(&mut self, entry_id: EntryId) -> bool { 299 | if let Some(index) = self 300 | .table_data 301 | .rows 302 | .iter() 303 | .position(|entry| entry.id == entry_id) 304 | { 305 | self.table_data.rows.remove(index); 306 | return true; 307 | } 308 | 309 | false 310 | } 311 | 312 | fn reset_selected(&mut self) { 313 | self.table_state.select(Some(0)); 314 | } 315 | 316 | /// Replaces the previous [`Self::table_data`] with a newly calculated one. 317 | /// 318 | /// This should only be done if there is a "desync" issue (e.g. deleted from history but failed 319 | /// to delete from table data). 320 | fn resync_table(&mut self, history: &History) { 321 | self.table_data = 322 | TableData::from_iter(history.iter().map(|(id, entry)| (*id, entry.clone()))); 323 | self.reset_selected(); 324 | } 325 | } 326 | 327 | /// Starts the UI and returns the selected/resulting entry 328 | pub(crate) fn start( 329 | tracker: &mut Tracker, 330 | hide_instructions: bool, 331 | hide_info: bool, 332 | ) -> Result> { 333 | debug!("Starting UI..."); 334 | 335 | // setup terminal 336 | debug!("Entering raw mode & alternate screen..."); 337 | enable_raw_mode()?; 338 | let mut stdout = io::stdout(); 339 | execute!(stdout, EnterAlternateScreen, EnableMouseCapture)?; 340 | 341 | let backend = CrosstermBackend::new(stdout); 342 | let mut terminal = Terminal::new(backend)?; 343 | 344 | // create app and run it 345 | let res = run_app( 346 | &mut terminal, 347 | UI::new(&tracker.history, hide_instructions, hide_info), 348 | tracker, 349 | ); 350 | 351 | // restore terminal 352 | disable_raw_mode()?; 353 | execute!( 354 | terminal.backend_mut(), 355 | LeaveAlternateScreen, 356 | DisableMouseCapture 357 | )?; 358 | terminal.show_cursor()?; 359 | debug!("Terminal restored"); 360 | 361 | Ok(res?.and_then(|selected_id| { 362 | tracker 363 | .history 364 | .iter() 365 | .find(|(id, _)| **id == selected_id) 366 | .map(|(id, entry)| (*id, entry.clone())) 367 | })) 368 | } 369 | 370 | /// UI main loop 371 | fn run_app( 372 | terminal: &mut Terminal, 373 | mut app: UI, 374 | tracker: &mut Tracker, 375 | ) -> io::Result> { 376 | app.table_state.select(Some(0)); // Select the most recent element by default 377 | 378 | loop { 379 | terminal.draw(|f| render(f, &mut app))?; 380 | 381 | let input = event::read()?; 382 | let action = handle_input(input); 383 | 384 | if let Some(action) = action { 385 | match action { 386 | AppAction::Quit => return Ok(None), 387 | AppAction::SelectNext => { 388 | app.select_next(); 389 | app.last_clicked_index = None; // Reset click tracking on navigation 390 | } 391 | AppAction::SelectPrevious => { 392 | app.select_previous(); 393 | app.last_clicked_index = None; // Reset click tracking on navigation 394 | } 395 | AppAction::SelectFirst => { 396 | app.select_first(); 397 | app.last_clicked_index = None; // Reset click tracking on navigation 398 | } 399 | AppAction::SelectLast => { 400 | app.select_last(); 401 | app.last_clicked_index = None; // Reset click tracking on navigation 402 | } 403 | AppAction::OpenSelected => { 404 | if let Some(selected) = app.get_selected_row() { 405 | return Ok(Some(selected.id)); 406 | } 407 | } 408 | AppAction::DeleteSelectedEntry => { 409 | if let Some(selected) = app.get_selected_row() { 410 | let entry_id = selected.id; 411 | if tracker.history.delete(entry_id).is_some() && !app.delete(entry_id) { 412 | app.resync_table(&tracker.history); 413 | } 414 | } 415 | app.last_clicked_index = None; // Reset click tracking after deletion 416 | } 417 | AppAction::TableClick(row) => { 418 | // Check if click is within table area (accounting for borders and header) 419 | let table_area = terminal.get_frame().area(); 420 | if row >= table_area.y + 2 && row < table_area.y + table_area.height - 1 { 421 | let clicked_index = (row - table_area.y - 2) as usize; 422 | let visible_rows = app.table_data.as_rows_full().count(); 423 | 424 | if clicked_index < visible_rows { 425 | // If clicking the same row that was previously clicked and selected 426 | if app.last_clicked_index == Some(clicked_index) 427 | && app.table_state.selected() == Some(clicked_index) 428 | { 429 | // Launch the container 430 | if let Some(selected) = app.get_selected_row() { 431 | return Ok(Some(selected.id)); 432 | } 433 | } else { 434 | // Just select the row on first click 435 | app.table_state.select(Some(clicked_index)); 436 | app.last_clicked_index = Some(clicked_index); 437 | } 438 | } 439 | } 440 | } 441 | AppAction::SearchInput(input) => { 442 | if app.search.input(input) { 443 | let line = app.search.lines().first().cloned(); 444 | app.apply_filter(line.as_deref()); 445 | app.last_clicked_index = None; // Reset click tracking on search 446 | } 447 | } 448 | } 449 | } 450 | } 451 | } 452 | 453 | fn handle_input(input: Event) -> Option { 454 | match input { 455 | Event::Key(key) => { 456 | if key.kind != KeyEventKind::Press { 457 | return None; 458 | } 459 | 460 | let is_key = |code: KeyCode| key.code == code; 461 | let is_char = |c: char| is_key(KeyCode::Char(c)); 462 | let is_ctrl_char = 463 | |c: char| key.modifiers.contains(KeyModifiers::CONTROL) && is_char(c); 464 | 465 | if is_key(KeyCode::Esc) || is_ctrl_char('q') || is_ctrl_char('c') { 466 | return Some(AppAction::Quit); 467 | } else if is_key(KeyCode::Down) || is_ctrl_char('j') { 468 | return Some(AppAction::SelectNext); 469 | } else if is_key(KeyCode::Up) || is_ctrl_char('k') { 470 | return Some(AppAction::SelectPrevious); 471 | } else if is_key(KeyCode::KeypadBegin) || is_ctrl_char('1') { 472 | return Some(AppAction::SelectFirst); 473 | } else if is_key(KeyCode::End) || is_ctrl_char('0') { 474 | return Some(AppAction::SelectLast); 475 | } else if is_key(KeyCode::Enter) || is_ctrl_char('o') { 476 | return Some(AppAction::OpenSelected); 477 | } else if is_key(KeyCode::Delete) || is_ctrl_char('r') || is_ctrl_char('x') { 478 | return Some(AppAction::DeleteSelectedEntry); 479 | } 480 | } 481 | Event::Mouse(MouseEvent { kind, row, .. }) => match kind { 482 | MouseEventKind::Down(MouseButton::Left) => { 483 | return Some(AppAction::TableClick(row)); 484 | } 485 | MouseEventKind::ScrollDown => { 486 | return Some(AppAction::SelectNext); 487 | } 488 | MouseEventKind::ScrollUp => { 489 | return Some(AppAction::SelectPrevious); 490 | } 491 | _ => {} 492 | }, 493 | _ => {} 494 | } 495 | 496 | Some(AppAction::SearchInput(input.into())) 497 | } 498 | 499 | /// Main render function 500 | fn render(frame: &mut Frame, app: &mut UI) { 501 | // Setup crossterm UI layout & style 502 | let constraints = if app.hide_info { 503 | vec![ 504 | Constraint::Percentage(100), 505 | Constraint::Min(3), 506 | Constraint::Min(1), 507 | ] 508 | } else { 509 | vec![ 510 | Constraint::Percentage(100), 511 | Constraint::Min(3), 512 | Constraint::Min(1), 513 | Constraint::Min(1), 514 | Constraint::Min(1), 515 | ] 516 | }; 517 | 518 | let area = Layout::default() 519 | .constraints(&constraints) 520 | .horizontal_margin(1) 521 | .split(frame.area()); 522 | 523 | // Calculate the longest workspace and dev container names 524 | let longest_ws_name = app 525 | .table_data 526 | .max_worspace_name_len 527 | .unwrap_or(20) 528 | .clamp(9, 60); 529 | 530 | let longest_dc_name = app 531 | .table_data 532 | .max_devcontainer_name_len 533 | .unwrap_or(20) 534 | .clamp(9, 60); 535 | 536 | // Render the main table 537 | render_table( 538 | frame, 539 | app, 540 | area[0], 541 | u16::try_from(longest_ws_name).unwrap_or(u16::MAX), 542 | u16::try_from(longest_dc_name).unwrap_or(u16::MAX), 543 | ); 544 | 545 | render_search_input(frame, app, area[1]); 546 | 547 | let selected: Option = app.get_selected_row().map(|row| row.entry); 548 | 549 | // Render status area and additional info 550 | render_status_area( 551 | frame, 552 | selected.as_ref(), 553 | &area[2..], 554 | app.hide_instructions, 555 | app.hide_info, 556 | ); 557 | } 558 | 559 | fn render_search_input(frame: &mut Frame, app: &mut UI, area: Rect) { 560 | let style = Style::default().fg(Color::Blue); 561 | 562 | app.search.set_block( 563 | Block::default() 564 | .borders(Borders::all()) 565 | .title("Search") 566 | .border_style(style), 567 | ); 568 | 569 | frame.render_widget(&app.search, area); 570 | } 571 | 572 | /// Renders the main table 573 | fn render_table( 574 | frame: &mut Frame, 575 | app: &mut UI, 576 | area: Rect, 577 | longest_ws_name: u16, 578 | longest_dc_name: u16, 579 | ) { 580 | let (header_style, selected_style) = ( 581 | Style::default().bg(Color::Blue), 582 | Style::default().bg(Color::DarkGray), 583 | ); 584 | 585 | let header_cells = TableData::HEADER 586 | .iter() 587 | .map(|header| Cell::from(*header).style(Style::default().fg(Color::White))); 588 | let header = Row::new(header_cells).style(header_style).height(1); 589 | 590 | let widths = [ 591 | Constraint::Min(longest_ws_name + 1), 592 | Constraint::Min(longest_dc_name + 1), 593 | Constraint::Percentage(70), 594 | Constraint::Min(20), 595 | ]; 596 | 597 | let table = Table::new(app.table_data.to_rows(), widths) 598 | .header(header) 599 | .block( 600 | Block::default() 601 | .borders(Borders::ALL) 602 | .title("Recent Workspaces"), 603 | ) 604 | .row_highlight_style(selected_style) 605 | .highlight_symbol("> "); 606 | frame.render_stateful_widget(table, area, &mut app.table_state); 607 | 608 | // Calculate if scrollbar is needed 609 | let total_items = app.table_data.as_rows_full().count(); 610 | let viewport_height = (area.height - 2) as usize; // Subtract 2 for borders 611 | 612 | // Show scrollbar if there's any content not visible in the viewport 613 | if total_items >= viewport_height { 614 | let mut scrollbar_state = ScrollbarState::default() 615 | .content_length(total_items) 616 | .viewport_content_length(viewport_height) 617 | .position(app.table_state.selected().unwrap_or(0)); 618 | 619 | // Create a new area for the scrollbar that overlaps with the right border 620 | let scrollbar_area = Rect { 621 | x: area.x + area.width - 1, // Place on the right border 622 | y: area.y + 2, // Start two lines below the top (one line after header) 623 | width: 1, 624 | height: area.height - 3, // Account for top border + header and bottom border 625 | }; 626 | 627 | // Render scrollbar 628 | frame.render_stateful_widget( 629 | Scrollbar::new(ScrollbarOrientation::VerticalRight) 630 | .begin_symbol(Some("↑")) 631 | .end_symbol(Some("↓")), 632 | scrollbar_area, 633 | &mut scrollbar_state, 634 | ); 635 | } 636 | } 637 | 638 | /// Renders the status area and additional info 639 | fn render_status_area( 640 | frame: &mut Frame, 641 | selected: Option<&Entry>, 642 | areas: &[Rect], 643 | hide_instructions: bool, 644 | hide_info: bool, 645 | ) { 646 | // Render instructions using full width if not hidden 647 | if !hide_instructions { 648 | let instruction = Span::styled( 649 | "↑/↓ to navigate • Del/Ctrl+X to remove • Enter to open • Type to filter • Esc/Ctrl+C to quit", 650 | Style::default().fg(Color::Gray), 651 | ); 652 | let instructions_par = Paragraph::new(instruction) 653 | .block(Block::default().padding(Padding::new(2, 2, 0, 0))) 654 | .alignment(Alignment::Left); 655 | frame.render_widget(instructions_par, areas[0]); 656 | } 657 | 658 | // Render additional info if not hidden and we have more areas 659 | if !hide_info && areas.len() > 1 { 660 | // Strategy, command and args info 661 | let strategy = selected.map_or_else( 662 | || String::from("-"), 663 | |entry| entry.behavior.strategy.to_string(), 664 | ); 665 | 666 | let command = 667 | selected.map_or_else(|| String::from("-"), |entry| entry.behavior.command.clone()); 668 | 669 | let args_count = selected.map_or(0, |entry| entry.behavior.args.len()); 670 | let args = selected.map_or_else( 671 | || String::from("-"), 672 | |entry| { 673 | let converted_str: Vec> = entry 674 | .behavior 675 | .args 676 | .iter() 677 | .map(|arg| arg.to_string_lossy()) 678 | .collect(); 679 | converted_str.join(", ") 680 | }, 681 | ); 682 | 683 | let additional_info = Span::styled( 684 | format!("Strategy: {strategy} • Command: {command} • Args ({args_count}): {args}"), 685 | Style::default().fg(Color::DarkGray), 686 | ); 687 | 688 | let status_block = Block::default().padding(Padding::new(2, 2, 0, 0)); 689 | let additional_info_par = Paragraph::new(additional_info) 690 | .block(status_block) 691 | .alignment(Alignment::Left); 692 | frame.render_widget(additional_info_par, areas[1]); 693 | 694 | // Dev container path 695 | if areas.len() > 2 { 696 | let dc_path = selected.map_or_else(String::new, |entry| { 697 | entry 698 | .config_path 699 | .as_ref() 700 | .map(|f| f.to_string_lossy().into_owned()) 701 | .unwrap_or_default() 702 | }); 703 | let dc_path_info = Span::styled( 704 | format!("Dev Container: {dc_path}"), 705 | Style::default().fg(Color::DarkGray), 706 | ); 707 | let dc_path_info_par = Paragraph::new(dc_path_info) 708 | .block(Block::default().padding(Padding::new(2, 2, 0, 0))) 709 | .alignment(Alignment::Left); 710 | 711 | frame.render_widget(dc_path_info_par, areas[2]); 712 | } 713 | } 714 | } 715 | 716 | /// Adds two optional [`u32`]s. 717 | /// 718 | /// If at least one of the inputs is [`Option::Some`] then the result will also be [`Option::Some`]. 719 | fn add_num_opt(o1: Option, o2: Option) -> Option { 720 | match (o1, o2) { 721 | (Some(n1), Some(n2)) => Some(n1 + n2), 722 | (Some(n), None) | (None, Some(n)) => Some(n), 723 | _ => None, 724 | } 725 | } 726 | -------------------------------------------------------------------------------- /src/uri.rs: -------------------------------------------------------------------------------- 1 | use serde::{Serialize, ser::SerializeMap}; 2 | use url::Url; 3 | 4 | /// Represents a single file path to a dev container config as expected by the code CLI. 5 | #[derive(Debug, Clone, PartialEq, Eq, Hash)] 6 | pub struct FileUriJson { 7 | path: Url, 8 | authority: Option, 9 | } 10 | 11 | impl FileUriJson { 12 | /// Creates a new `FileUri` from a given string slice 13 | pub fn new(uri: &str) -> Self { 14 | let fixed_uri = format!("file://{uri}") 15 | .replace("\\\\", "") 16 | .replace('\\', "/"); 17 | let parsed_url = Url::parse(&fixed_uri).expect("Invalid URI"); 18 | 19 | Self { 20 | authority: parsed_url.host_str().map(ToString::to_string), 21 | path: parsed_url, 22 | } 23 | } 24 | } 25 | 26 | impl Serialize for FileUriJson { 27 | /// Creates the JSON representation of the `FileUri`. 28 | fn serialize(&self, serializer: S) -> Result { 29 | let mut map = serializer.serialize_map(None)?; 30 | map.serialize_entry("scheme", "file")?; 31 | if let Some(authority) = &self.authority { 32 | map.serialize_entry("authority", authority)?; 33 | } 34 | map.serialize_entry("path", self.path.path())?; 35 | map.end() 36 | } 37 | } 38 | 39 | /// Represents a dev container launch argument as expected by the code CLI. 40 | #[derive(Debug, Clone, PartialEq, Eq, Hash, Serialize)] 41 | pub struct DevcontainerUriJson { 42 | /// The path to the dev container workspace 43 | #[serde(rename = "hostPath")] 44 | pub host_path: String, 45 | // The path to the dev container config file 46 | #[serde(rename = "configFile")] 47 | pub config_file: FileUriJson, 48 | } 49 | -------------------------------------------------------------------------------- /src/workspace.rs: -------------------------------------------------------------------------------- 1 | use color_eyre::eyre::{Result, WrapErr, bail, eyre}; 2 | use log::{debug, trace}; 3 | use std::ffi::OsString; 4 | use std::fmt::Display; 5 | use std::path::{Path, PathBuf}; 6 | use std::process::Command; 7 | use walkdir::WalkDir; 8 | 9 | use crate::uri::{DevcontainerUriJson, FileUriJson}; 10 | 11 | #[derive(Debug, Clone, PartialEq, Eq, Hash)] 12 | pub struct DevContainer { 13 | pub config_path: PathBuf, 14 | pub name: Option, 15 | pub workspace_path_in_container: String, 16 | } 17 | 18 | // Used in the inquire select prompt 19 | impl Display for DevContainer { 20 | fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { 21 | let path = self.config_path.display(); 22 | if let Some(name) = &self.name { 23 | write!(f, "{name} ({path})") 24 | } else { 25 | write!(f, "{path}") 26 | } 27 | } 28 | } 29 | 30 | impl DevContainer { 31 | /// Creates a new `DevContainer` from a dev container config file and fallback workspace name. 32 | pub fn from_config(path: &Path, workspace_name: &str) -> Result { 33 | let dev_container = Self::parse_dev_container_config(path)?; 34 | trace!("dev container config: {:?}", dev_container); 35 | 36 | let folder: String = if let Some(folder) = dev_container["workspaceFolder"].as_str() { 37 | debug!("Read workspace folder from config: {}", folder); 38 | folder.to_owned() 39 | } else { 40 | debug!("Could not read workspace folder from config -> using default folder"); 41 | format!("/workspaces/{workspace_name}") 42 | }; 43 | trace!("Workspace folder: {folder}"); 44 | 45 | let name = if let Some(name) = dev_container["name"].as_str() { 46 | debug!("Read workspace name from config: {}", name); 47 | Some(name.to_owned()) 48 | } else { 49 | debug!("Could not read workspace name from config"); 50 | None 51 | }; 52 | trace!("Workspace name: {name:?}"); 53 | 54 | Ok(DevContainer { 55 | config_path: path.to_owned(), 56 | workspace_path_in_container: folder, 57 | name, 58 | }) 59 | } 60 | 61 | /// Parses the dev container config file. 62 | /// `https://code.visualstudio.com/remote/advancedcontainers/change-default-source-mount` 63 | fn parse_dev_container_config(path: &Path) -> Result { 64 | let content = std::fs::read_to_string(path) 65 | .wrap_err_with(|| format!("Failed to read dev container config file: {path:?}"))?; 66 | 67 | let config: serde_json::Value = json5::from_str(&content) 68 | .wrap_err_with(|| format!("Failed to parse json file: {path:?}"))?; 69 | 70 | debug!("Parsed dev container config: {:?}", path); 71 | Ok(config) 72 | } 73 | } 74 | 75 | /// A workspace is a folder which contains a vscode project. 76 | #[derive(Debug, Clone, PartialEq, Eq, Hash)] 77 | pub struct Workspace { 78 | /// The path of the workspace. 79 | pub path: PathBuf, 80 | /// The name of the workspace. 81 | pub name: String, 82 | } 83 | 84 | impl Workspace { 85 | /// Creates a new `Workspace` from the given path to a folder. 86 | pub fn from_path(path: &Path) -> Result { 87 | // check for valid path 88 | if !path.exists() { 89 | bail!("Path {} does not exist", path.display()); 90 | } 91 | 92 | // canonicalize path 93 | let path = std::fs::canonicalize(path) 94 | .wrap_err_with(|| format!("Error canonicalizing path: {path:?}"))?; 95 | trace!("Canonicalized path: {}", path.display()); 96 | 97 | // get workspace name (either directory or file name) 98 | let workspace_name = path 99 | .file_name() 100 | .ok_or_else(|| eyre!("Error getting workspace from path"))? 101 | .to_string_lossy() 102 | .into_owned(); 103 | trace!("Workspace name: {workspace_name}"); 104 | 105 | let ws = Workspace { 106 | path, 107 | name: workspace_name, 108 | }; 109 | trace!("{ws:?}"); 110 | Ok(ws) 111 | } 112 | 113 | /// Finds all dev container configs in the workspace. 114 | /// 115 | /// # Note 116 | /// This searches in the following locations: 117 | /// - A `.devcontainer.json` defined directly in the workspace folder. 118 | /// - A `.devcontainer/devcontainer.json` defined in the `.devcontainer/` folder. 119 | /// - Any `.devcontainer/**/devcontainer.json` file in any `.devcontainer/` subfolder (only one level deep). 120 | /// 121 | /// This should results in a dev container detection algorithm similar to the one vscode uses. 122 | pub fn find_dev_container_configs(&self) -> Vec { 123 | let mut configs = Vec::new(); 124 | 125 | // check if we have a `devcontainer.json` directly in the workspace 126 | let direct_config = self.path.join(".devcontainer.json"); 127 | if direct_config.is_file() { 128 | trace!("Found dev container config: {}", direct_config.display()); 129 | configs.push(direct_config); 130 | } 131 | 132 | // check configs one level deep in `.devcontainer/` 133 | let dev_container_dir = self.path.join(".devcontainer"); 134 | for entry in WalkDir::new(dev_container_dir) 135 | .max_depth(2) 136 | .sort_by_file_name() 137 | .into_iter() 138 | .filter(|e| matches!(e, Ok(x) if x.file_type().is_file() && x.file_name() == "devcontainer.json")) 139 | .flatten() 140 | { 141 | let path = entry.into_path(); 142 | trace!( 143 | "Found dev container config in .devcontainer folder: {}", 144 | path.display() 145 | ); 146 | configs.push(path); 147 | } 148 | 149 | debug!( 150 | "Found {} dev container configs: {:?}", 151 | configs.len(), 152 | configs 153 | ); 154 | 155 | configs 156 | } 157 | 158 | pub fn load_dev_containers(&self, paths: &[PathBuf]) -> Result> { 159 | // parse dev containers and their properties 160 | paths 161 | .iter() 162 | .map(|config_path| DevContainer::from_config(config_path, &self.name)) 163 | .collect::, _>>() 164 | } 165 | 166 | /// Open vscode using the specified dev container. 167 | pub fn open( 168 | &self, 169 | mut args: Vec, 170 | dry_run: bool, 171 | dev_container: &DevContainer, 172 | command: &str, 173 | ) -> Result<()> { 174 | // Checking if '--folder-uri' is present in the arguments 175 | if args.iter().any(|arg| arg == "--folder-uri") { 176 | bail!("Specifying `--folder-uri` is not possible while using vscli."); 177 | } 178 | 179 | // get the folder path from the selected dev container 180 | let container_folder: String = dev_container.workspace_path_in_container.clone(); 181 | 182 | let mut ws_path: String = self.path.to_string_lossy().into_owned(); 183 | let mut dc_path: String = dev_container.config_path.to_string_lossy().into_owned(); 184 | 185 | // detect WSL (excluding Docker containers) 186 | let is_wsl: bool = { 187 | #[cfg(unix)] 188 | { 189 | // Execute `uname -a` and capture the output 190 | let output = Command::new("uname") 191 | .arg("-a") 192 | .output() 193 | .expect("Failed to execute command"); 194 | 195 | // Convert the output to a UTF-8 string 196 | let uname_output = String::from_utf8(output.stdout)?; 197 | 198 | // Check if the output contains "Microsoft" or "WSL" which are indicators of WSL environment 199 | // Also we want to check for the WSLENV variable, which is not available in Docker containers 200 | (uname_output.contains("Microsoft") || uname_output.contains("WSL")) 201 | && std::env::var("WSLENV").is_ok() 202 | } 203 | #[cfg(windows)] 204 | { 205 | false 206 | } 207 | }; 208 | 209 | if is_wsl { 210 | debug!("WSL detected"); 211 | 212 | ws_path = wslpath2::convert( 213 | ws_path.as_str(), 214 | None, 215 | wslpath2::Conversion::WslToWindows, 216 | true, 217 | ) 218 | .map_err(|e| eyre!("Error while getting wslpath: {} (path: {ws_path:?})", e))?; 219 | dc_path = wslpath2::convert( 220 | dc_path.as_str(), 221 | None, 222 | wslpath2::Conversion::WslToWindows, 223 | true, 224 | ) 225 | .map_err(|e| eyre!("Error while getting wslpath: {} (path: {dc_path:?})", e))?; 226 | } 227 | 228 | #[cfg(windows)] 229 | { 230 | ws_path = ws_path.replace("\\\\?\\", ""); 231 | dc_path = dc_path.replace("\\\\?\\", ""); 232 | } 233 | 234 | let folder_uri = DevcontainerUriJson { 235 | host_path: ws_path, 236 | config_file: FileUriJson::new(dc_path.as_str()), 237 | }; 238 | let json = serde_json::to_string(&folder_uri)?; 239 | 240 | trace!("Folder uri JSON: {json}"); 241 | 242 | let hex = hex::encode(json.as_bytes()); 243 | let uri = format!("vscode-remote://dev-container+{hex}{container_folder}"); 244 | 245 | args.push(OsString::from("--folder-uri")); 246 | args.push(OsString::from(uri.as_str())); 247 | 248 | exec_code(args, dry_run, command) 249 | .wrap_err_with(|| "Error opening vscode using dev container...") 250 | } 251 | 252 | /// Open vscode like with the `code` command 253 | pub fn open_classic( 254 | &self, 255 | mut args: Vec, 256 | dry_run: bool, 257 | command: &str, 258 | ) -> Result<()> { 259 | trace!("path: {}", self.path.display()); 260 | trace!("args: {:?}", args); 261 | 262 | args.insert(0, self.path.as_os_str().to_owned()); 263 | exec_code(args, dry_run, command) 264 | .wrap_err_with(|| "Error opening vscode the classic way...") 265 | } 266 | } 267 | 268 | /// Executes the vscode executable with the given arguments on Unix. 269 | #[cfg(unix)] 270 | fn exec_code(args: Vec, dry_run: bool, command: &str) -> Result<()> { 271 | // test if cmd exists 272 | Command::new(command) 273 | .arg("-v") 274 | .output() 275 | .wrap_err_with(|| format!("`{command}` does not exists."))?; 276 | 277 | run(command, args, dry_run) 278 | } 279 | 280 | /// Executes the vscode executable with the given arguments on Windows. 281 | #[cfg(windows)] 282 | fn exec_code(mut args: Vec, dry_run: bool, command: &str) -> Result<()> { 283 | let cmd = "cmd"; 284 | args.insert(0, OsString::from("/c")); 285 | args.insert(1, OsString::from(command)); 286 | 287 | // test if cmd exists 288 | Command::new(cmd) 289 | .arg("-v") 290 | .output() 291 | .wrap_err_with(|| format!("`{cmd}` does not exists."))?; 292 | 293 | run(cmd, args, dry_run) 294 | } 295 | 296 | /// Executes a command with given arguments and debug outputs, with an option for dry run 297 | fn run(cmd: &str, args: Vec, dry_run: bool) -> Result<()> { 298 | debug!("executable: {}", cmd); 299 | debug!("final args: {:?}", args); 300 | 301 | if !dry_run { 302 | let output = Command::new(cmd).args(args).output()?; 303 | debug!("Command output: {:?}", output); 304 | } 305 | 306 | Ok(()) 307 | } 308 | 309 | #[cfg(test)] 310 | mod tests { 311 | use super::*; 312 | 313 | #[test] 314 | fn test_deserialize_devcontainer() { 315 | let path = PathBuf::from("tests/fixtures/devcontainer.json"); 316 | let result = DevContainer::from_config(&path, "test"); 317 | assert!(result.is_ok()); 318 | let dev_container = result.unwrap(); 319 | 320 | assert_eq!(dev_container.config_path, path); 321 | assert_eq!(dev_container.name, Some(String::from("Rust"))); 322 | assert_eq!( 323 | dev_container.workspace_path_in_container, 324 | "/workspaces/test" 325 | ); 326 | } 327 | } 328 | -------------------------------------------------------------------------------- /tests/fixtures/devcontainer.json: -------------------------------------------------------------------------------- 1 | // This file is used to test the deserialization of a devcontainer.json file 2 | { 3 | // Comment to verify deserialization can handle comments 4 | "name": "Rust", 5 | "image": "mcr.microsoft.com/devcontainers/rust:1-bullseye", 6 | "features": { 7 | // Trailing commas shouldn't break deserialization 8 | "ghcr.io/guiyomh/features/just:0": {}, 9 | }, 10 | } 11 | --------------------------------------------------------------------------------