├── .devcontainer ├── Dockerfile └── devcontainer.json ├── .dockerignore ├── .github ├── FUNDING.yml └── workflows │ ├── ci.yml │ └── release.yml ├── .gitignore ├── CODE_OF_CONDUCT.md ├── CONTRIBUTING.md ├── Cargo.lock ├── Cargo.toml ├── Dockerfile ├── LICENSE ├── README.md ├── assets ├── jnv-dark.svg └── jnv-light.svg ├── default.toml ├── dist-workspace.toml └── src ├── config.rs ├── config ├── content_style.rs ├── duration.rs ├── event.rs └── text_editor.rs ├── editor.rs ├── json.rs ├── main.rs ├── processor.rs ├── processor ├── init.rs ├── monitor.rs └── spinner.rs ├── prompt.rs ├── render.rs └── search.rs /.devcontainer/Dockerfile: -------------------------------------------------------------------------------- 1 | # Use buster on Intel for testing Homebrew on Linux. 2 | # https://hub.docker.com/_/microsoft-devcontainers-rust 3 | # Please note that Homebrew on Linux does not support ARM processors. 4 | FROM --platform=linux/x86_64 mcr.microsoft.com/devcontainers/rust:1.0.9-buster 5 | 6 | # Clang 15 7 | RUN apt-get update \ 8 | && apt-get install -y \ 9 | build-essential \ 10 | autoconf \ 11 | libtool \ 12 | git \ 13 | wget \ 14 | software-properties-common \ 15 | && wget -O - https://apt.llvm.org/llvm-snapshot.gpg.key|apt-key add - \ 16 | && apt-add-repository "deb http://apt.llvm.org/buster/ llvm-toolchain-buster-15 main" \ 17 | && apt-get update \ 18 | && apt-get install -y clang-15 lldb-15 lld-15 \ 19 | && apt-get clean \ 20 | && rm -rf /var/lib/apt/lists/* 21 | 22 | RUN ln -s /usr/bin/clang-15 /usr/bin/clang \ 23 | && ln -s /usr/bin/clang++-15 /usr/bin/clang++ 24 | 25 | # Homebrew for Linux 26 | USER vscode 27 | RUN /bin/bash -c "$(curl -fsSL https://raw.githubusercontent.com/Homebrew/install/HEAD/install.sh)" 28 | ENV PATH=${PATH}:/home/linuxbrew/.linuxbrew/bin:/home/linuxbrew/.linuxbrew/sbin/ 29 | -------------------------------------------------------------------------------- /.devcontainer/devcontainer.json: -------------------------------------------------------------------------------- 1 | // For format details, see https://aka.ms/devcontainer.json. For config options, see the 2 | // README at: https://github.com/devcontainers/templates/tree/main/src/rust 3 | { 4 | "name": "Rust", 5 | // Or use a Dockerfile or Docker Compose file. More info: https://containers.dev/guide/dockerfile 6 | // "image": "mcr.microsoft.com/devcontainers/rust:1-1-bullseye", 7 | "build": { 8 | // Path is relative to the devcontainer.json file. 9 | "dockerfile": "Dockerfile" 10 | }, 11 | 12 | "customizations": { 13 | "vscode": { 14 | "extensions": [ 15 | "rust-lang.rust-analyzer" 16 | ] 17 | } 18 | } 19 | 20 | // Use 'mounts' to make the cargo cache persistent in a Docker Volume. 21 | // "mounts": [ 22 | // { 23 | // "source": "devcontainer-cargo-cache-${devcontainerId}", 24 | // "target": "/usr/local/cargo", 25 | // "type": "volume" 26 | // } 27 | // ] 28 | 29 | // Features to add to the dev container. More info: https://containers.dev/features. 30 | // "features": {}, 31 | 32 | // Use 'forwardPorts' to make a list of ports inside the container available locally. 33 | // "forwardPorts": [], 34 | 35 | // Use 'postCreateCommand' to run commands after the container is created. 36 | // "postCreateCommand": "rustc --version", 37 | 38 | // Configure tool-specific properties. 39 | // "customizations": {}, 40 | 41 | // Uncomment to connect as root instead. More info: https://aka.ms/dev-containers-non-root. 42 | // "remoteUser": "root" 43 | } 44 | -------------------------------------------------------------------------------- /.dockerignore: -------------------------------------------------------------------------------- 1 | target 2 | -------------------------------------------------------------------------------- /.github/FUNDING.yml: -------------------------------------------------------------------------------- 1 | github: ynqa 2 | -------------------------------------------------------------------------------- /.github/workflows/ci.yml: -------------------------------------------------------------------------------- 1 | name: ci 2 | 3 | on: [push] 4 | 5 | jobs: 6 | test: 7 | name: test 8 | runs-on: ubuntu-latest 9 | steps: 10 | - uses: actions/checkout@v2 11 | - uses: actions/cache@v4 12 | with: 13 | path: | 14 | ~/.cargo/registry 15 | ~/.cargo/git 16 | target 17 | key: ${{ runner.os }}-cargo-${{ hashFiles('**/Cargo.lock') }} 18 | - uses: actions-rs/toolchain@v1 19 | with: 20 | toolchain: stable 21 | components: rustfmt, clippy 22 | - uses: actions-rs/cargo@v1 23 | with: 24 | command: fmt 25 | args: --all -- --check 26 | - uses: actions-rs/cargo@v1 27 | with: 28 | command: clippy 29 | - uses: actions-rs/cargo@v1 30 | with: 31 | command: test 32 | args: -- --nocapture --format pretty 33 | - uses: actions-rs/cargo@v1 34 | with: 35 | command: build 36 | args: --examples 37 | - uses: actions-rs/cargo@v1 38 | with: 39 | command: build 40 | args: --bins 41 | -------------------------------------------------------------------------------- /.github/workflows/release.yml: -------------------------------------------------------------------------------- 1 | # This file was autogenerated by dist: https://opensource.axo.dev/cargo-dist/ 2 | # 3 | # Copyright 2022-2024, axodotdev 4 | # SPDX-License-Identifier: MIT or Apache-2.0 5 | # 6 | # CI that: 7 | # 8 | # * checks for a Git Tag that looks like a release 9 | # * builds artifacts with dist (archives, installers, hashes) 10 | # * uploads those artifacts to temporary workflow zip 11 | # * on success, uploads the artifacts to a GitHub Release 12 | # 13 | # Note that the GitHub Release will be created with a generated 14 | # title/body based on your changelogs. 15 | 16 | name: Release 17 | permissions: 18 | "contents": "write" 19 | 20 | # This task will run whenever you push a git tag that looks like a version 21 | # like "1.0.0", "v0.1.0-prerelease.1", "my-app/0.1.0", "releases/v1.0.0", etc. 22 | # Various formats will be parsed into a VERSION and an optional PACKAGE_NAME, where 23 | # PACKAGE_NAME must be the name of a Cargo package in your workspace, and VERSION 24 | # must be a Cargo-style SemVer Version (must have at least major.minor.patch). 25 | # 26 | # If PACKAGE_NAME is specified, then the announcement will be for that 27 | # package (erroring out if it doesn't have the given version or isn't dist-able). 28 | # 29 | # If PACKAGE_NAME isn't specified, then the announcement will be for all 30 | # (dist-able) packages in the workspace with that version (this mode is 31 | # intended for workspaces with only one dist-able package, or with all dist-able 32 | # packages versioned/released in lockstep). 33 | # 34 | # If you push multiple tags at once, separate instances of this workflow will 35 | # spin up, creating an independent announcement for each one. However, GitHub 36 | # will hard limit this to 3 tags per commit, as it will assume more tags is a 37 | # mistake. 38 | # 39 | # If there's a prerelease-style suffix to the version, then the release(s) 40 | # will be marked as a prerelease. 41 | on: 42 | pull_request: 43 | push: 44 | tags: 45 | - '**[0-9]+.[0-9]+.[0-9]+*' 46 | 47 | jobs: 48 | # Run 'dist plan' (or host) to determine what tasks we need to do 49 | plan: 50 | runs-on: "ubuntu-20.04" 51 | outputs: 52 | val: ${{ steps.plan.outputs.manifest }} 53 | tag: ${{ !github.event.pull_request && github.ref_name || '' }} 54 | tag-flag: ${{ !github.event.pull_request && format('--tag={0}', github.ref_name) || '' }} 55 | publishing: ${{ !github.event.pull_request }} 56 | env: 57 | GH_TOKEN: ${{ secrets.GITHUB_TOKEN }} 58 | steps: 59 | - uses: actions/checkout@v4 60 | with: 61 | submodules: recursive 62 | - name: Install dist 63 | # we specify bash to get pipefail; it guards against the `curl` command 64 | # failing. otherwise `sh` won't catch that `curl` returned non-0 65 | shell: bash 66 | run: "curl --proto '=https' --tlsv1.2 -LsSf https://github.com/axodotdev/cargo-dist/releases/download/v0.28.0/cargo-dist-installer.sh | sh" 67 | - name: Cache dist 68 | uses: actions/upload-artifact@v4 69 | with: 70 | name: cargo-dist-cache 71 | path: ~/.cargo/bin/dist 72 | # sure would be cool if github gave us proper conditionals... 73 | # so here's a doubly-nested ternary-via-truthiness to try to provide the best possible 74 | # functionality based on whether this is a pull_request, and whether it's from a fork. 75 | # (PRs run on the *source* but secrets are usually on the *target* -- that's *good* 76 | # but also really annoying to build CI around when it needs secrets to work right.) 77 | - id: plan 78 | run: | 79 | dist ${{ (!github.event.pull_request && format('host --steps=create --tag={0}', github.ref_name)) || 'plan' }} --output-format=json > plan-dist-manifest.json 80 | echo "dist ran successfully" 81 | cat plan-dist-manifest.json 82 | echo "manifest=$(jq -c "." plan-dist-manifest.json)" >> "$GITHUB_OUTPUT" 83 | - name: "Upload dist-manifest.json" 84 | uses: actions/upload-artifact@v4 85 | with: 86 | name: artifacts-plan-dist-manifest 87 | path: plan-dist-manifest.json 88 | 89 | # Build and packages all the platform-specific things 90 | build-local-artifacts: 91 | name: build-local-artifacts (${{ join(matrix.targets, ', ') }}) 92 | # Let the initial task tell us to not run (currently very blunt) 93 | needs: 94 | - plan 95 | if: ${{ fromJson(needs.plan.outputs.val).ci.github.artifacts_matrix.include != null && (needs.plan.outputs.publishing == 'true' || fromJson(needs.plan.outputs.val).ci.github.pr_run_mode == 'upload') }} 96 | strategy: 97 | fail-fast: false 98 | # Target platforms/runners are computed by dist in create-release. 99 | # Each member of the matrix has the following arguments: 100 | # 101 | # - runner: the github runner 102 | # - dist-args: cli flags to pass to dist 103 | # - install-dist: expression to run to install dist on the runner 104 | # 105 | # Typically there will be: 106 | # - 1 "global" task that builds universal installers 107 | # - N "local" tasks that build each platform's binaries and platform-specific installers 108 | matrix: ${{ fromJson(needs.plan.outputs.val).ci.github.artifacts_matrix }} 109 | runs-on: ${{ matrix.runner }} 110 | container: ${{ matrix.container && matrix.container.image || null }} 111 | env: 112 | GH_TOKEN: ${{ secrets.GITHUB_TOKEN }} 113 | BUILD_MANIFEST_NAME: target/distrib/${{ join(matrix.targets, '-') }}-dist-manifest.json 114 | steps: 115 | - name: enable windows longpaths 116 | run: | 117 | git config --global core.longpaths true 118 | - uses: actions/checkout@v4 119 | with: 120 | submodules: recursive 121 | - name: Install Rust non-interactively if not already installed 122 | if: ${{ matrix.container }} 123 | run: | 124 | if ! command -v cargo > /dev/null 2>&1; then 125 | curl --proto '=https' --tlsv1.2 -sSf https://sh.rustup.rs | sh -s -- -y 126 | echo "$HOME/.cargo/bin" >> $GITHUB_PATH 127 | fi 128 | - name: Install dist 129 | run: ${{ matrix.install_dist.run }} 130 | # Get the dist-manifest 131 | - name: Fetch local artifacts 132 | uses: actions/download-artifact@v4 133 | with: 134 | pattern: artifacts-* 135 | path: target/distrib/ 136 | merge-multiple: true 137 | - name: Install dependencies 138 | run: | 139 | ${{ matrix.packages_install }} 140 | - name: Build artifacts 141 | run: | 142 | # Actually do builds and make zips and whatnot 143 | dist build ${{ needs.plan.outputs.tag-flag }} --print=linkage --output-format=json ${{ matrix.dist_args }} > dist-manifest.json 144 | echo "dist ran successfully" 145 | - id: cargo-dist 146 | name: Post-build 147 | # We force bash here just because github makes it really hard to get values up 148 | # to "real" actions without writing to env-vars, and writing to env-vars has 149 | # inconsistent syntax between shell and powershell. 150 | shell: bash 151 | run: | 152 | # Parse out what we just built and upload it to scratch storage 153 | echo "paths<> "$GITHUB_OUTPUT" 154 | dist print-upload-files-from-manifest --manifest dist-manifest.json >> "$GITHUB_OUTPUT" 155 | echo "EOF" >> "$GITHUB_OUTPUT" 156 | 157 | cp dist-manifest.json "$BUILD_MANIFEST_NAME" 158 | - name: "Upload artifacts" 159 | uses: actions/upload-artifact@v4 160 | with: 161 | name: artifacts-build-local-${{ join(matrix.targets, '_') }} 162 | path: | 163 | ${{ steps.cargo-dist.outputs.paths }} 164 | ${{ env.BUILD_MANIFEST_NAME }} 165 | 166 | # Build and package all the platform-agnostic(ish) things 167 | build-global-artifacts: 168 | needs: 169 | - plan 170 | - build-local-artifacts 171 | runs-on: "ubuntu-20.04" 172 | env: 173 | GH_TOKEN: ${{ secrets.GITHUB_TOKEN }} 174 | BUILD_MANIFEST_NAME: target/distrib/global-dist-manifest.json 175 | steps: 176 | - uses: actions/checkout@v4 177 | with: 178 | submodules: recursive 179 | - name: Install cached dist 180 | uses: actions/download-artifact@v4 181 | with: 182 | name: cargo-dist-cache 183 | path: ~/.cargo/bin/ 184 | - run: chmod +x ~/.cargo/bin/dist 185 | # Get all the local artifacts for the global tasks to use (for e.g. checksums) 186 | - name: Fetch local artifacts 187 | uses: actions/download-artifact@v4 188 | with: 189 | pattern: artifacts-* 190 | path: target/distrib/ 191 | merge-multiple: true 192 | - id: cargo-dist 193 | shell: bash 194 | run: | 195 | dist build ${{ needs.plan.outputs.tag-flag }} --output-format=json "--artifacts=global" > dist-manifest.json 196 | echo "dist ran successfully" 197 | 198 | # Parse out what we just built and upload it to scratch storage 199 | echo "paths<> "$GITHUB_OUTPUT" 200 | jq --raw-output ".upload_files[]" dist-manifest.json >> "$GITHUB_OUTPUT" 201 | echo "EOF" >> "$GITHUB_OUTPUT" 202 | 203 | cp dist-manifest.json "$BUILD_MANIFEST_NAME" 204 | - name: "Upload artifacts" 205 | uses: actions/upload-artifact@v4 206 | with: 207 | name: artifacts-build-global 208 | path: | 209 | ${{ steps.cargo-dist.outputs.paths }} 210 | ${{ env.BUILD_MANIFEST_NAME }} 211 | # Determines if we should publish/announce 212 | host: 213 | needs: 214 | - plan 215 | - build-local-artifacts 216 | - build-global-artifacts 217 | # Only run if we're "publishing", and only if local and global didn't fail (skipped is fine) 218 | if: ${{ always() && needs.plan.outputs.publishing == 'true' && (needs.build-global-artifacts.result == 'skipped' || needs.build-global-artifacts.result == 'success') && (needs.build-local-artifacts.result == 'skipped' || needs.build-local-artifacts.result == 'success') }} 219 | env: 220 | GH_TOKEN: ${{ secrets.GITHUB_TOKEN }} 221 | runs-on: "ubuntu-20.04" 222 | outputs: 223 | val: ${{ steps.host.outputs.manifest }} 224 | steps: 225 | - uses: actions/checkout@v4 226 | with: 227 | submodules: recursive 228 | - name: Install cached dist 229 | uses: actions/download-artifact@v4 230 | with: 231 | name: cargo-dist-cache 232 | path: ~/.cargo/bin/ 233 | - run: chmod +x ~/.cargo/bin/dist 234 | # Fetch artifacts from scratch-storage 235 | - name: Fetch artifacts 236 | uses: actions/download-artifact@v4 237 | with: 238 | pattern: artifacts-* 239 | path: target/distrib/ 240 | merge-multiple: true 241 | - id: host 242 | shell: bash 243 | run: | 244 | dist host ${{ needs.plan.outputs.tag-flag }} --steps=upload --steps=release --output-format=json > dist-manifest.json 245 | echo "artifacts uploaded and released successfully" 246 | cat dist-manifest.json 247 | echo "manifest=$(jq -c "." dist-manifest.json)" >> "$GITHUB_OUTPUT" 248 | - name: "Upload dist-manifest.json" 249 | uses: actions/upload-artifact@v4 250 | with: 251 | # Overwrite the previous copy 252 | name: artifacts-dist-manifest 253 | path: dist-manifest.json 254 | # Create a GitHub Release while uploading all files to it 255 | - name: "Download GitHub Artifacts" 256 | uses: actions/download-artifact@v4 257 | with: 258 | pattern: artifacts-* 259 | path: artifacts 260 | merge-multiple: true 261 | - name: Cleanup 262 | run: | 263 | # Remove the granular manifests 264 | rm -f artifacts/*-dist-manifest.json 265 | - name: Create GitHub Release 266 | env: 267 | PRERELEASE_FLAG: "${{ fromJson(steps.host.outputs.manifest).announcement_is_prerelease && '--prerelease' || '' }}" 268 | ANNOUNCEMENT_TITLE: "${{ fromJson(steps.host.outputs.manifest).announcement_title }}" 269 | ANNOUNCEMENT_BODY: "${{ fromJson(steps.host.outputs.manifest).announcement_github_body }}" 270 | RELEASE_COMMIT: "${{ github.sha }}" 271 | run: | 272 | # Write and read notes from a file to avoid quoting breaking things 273 | echo "$ANNOUNCEMENT_BODY" > $RUNNER_TEMP/notes.txt 274 | 275 | gh release create "${{ needs.plan.outputs.tag }}" --target "$RELEASE_COMMIT" $PRERELEASE_FLAG --title "$ANNOUNCEMENT_TITLE" --notes-file "$RUNNER_TEMP/notes.txt" artifacts/* 276 | 277 | publish-homebrew-formula: 278 | needs: 279 | - plan 280 | - host 281 | runs-on: "ubuntu-20.04" 282 | env: 283 | GH_TOKEN: ${{ secrets.GITHUB_TOKEN }} 284 | PLAN: ${{ needs.plan.outputs.val }} 285 | GITHUB_USER: "axo bot" 286 | GITHUB_EMAIL: "admin+bot@axo.dev" 287 | if: ${{ !fromJson(needs.plan.outputs.val).announcement_is_prerelease || fromJson(needs.plan.outputs.val).publish_prereleases }} 288 | steps: 289 | - uses: actions/checkout@v4 290 | with: 291 | repository: "ynqa/homebrew-tap" 292 | token: ${{ secrets.HOMEBREW_TAP_TOKEN }} 293 | # So we have access to the formula 294 | - name: Fetch homebrew formulae 295 | uses: actions/download-artifact@v4 296 | with: 297 | pattern: artifacts-* 298 | path: Formula/ 299 | merge-multiple: true 300 | # This is extra complex because you can make your Formula name not match your app name 301 | # so we need to find releases with a *.rb file, and publish with that filename. 302 | - name: Commit formula files 303 | run: | 304 | git config --global user.name "${GITHUB_USER}" 305 | git config --global user.email "${GITHUB_EMAIL}" 306 | 307 | for release in $(echo "$PLAN" | jq --compact-output '.releases[] | select([.artifacts[] | endswith(".rb")] | any)'); do 308 | filename=$(echo "$release" | jq '.artifacts[] | select(endswith(".rb"))' --raw-output) 309 | name=$(echo "$filename" | sed "s/\.rb$//") 310 | version=$(echo "$release" | jq .app_version --raw-output) 311 | 312 | export PATH="/home/linuxbrew/.linuxbrew/bin:$PATH" 313 | brew update 314 | # We avoid reformatting user-provided data such as the app description and homepage. 315 | brew style --except-cops FormulaAudit/Homepage,FormulaAudit/Desc,FormulaAuditStrict --fix "Formula/${filename}" || true 316 | 317 | git add "Formula/${filename}" 318 | git commit -m "${name} ${version}" 319 | done 320 | git push 321 | 322 | announce: 323 | needs: 324 | - plan 325 | - host 326 | - publish-homebrew-formula 327 | # use "always() && ..." to allow us to wait for all publish jobs while 328 | # still allowing individual publish jobs to skip themselves (for prereleases). 329 | # "host" however must run to completion, no skipping allowed! 330 | if: ${{ always() && needs.host.result == 'success' && (needs.publish-homebrew-formula.result == 'skipped' || needs.publish-homebrew-formula.result == 'success') }} 331 | runs-on: "ubuntu-20.04" 332 | env: 333 | GH_TOKEN: ${{ secrets.GITHUB_TOKEN }} 334 | steps: 335 | - uses: actions/checkout@v4 336 | with: 337 | submodules: recursive 338 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Generated by Cargo 2 | # will have compiled files and executables 3 | debug/ 4 | target/ 5 | 6 | # These are backup files generated by rustfmt 7 | **/*.rs.bk 8 | 9 | # MSVC Windows builds of rustc generate these, which store debugging information 10 | *.pdb 11 | -------------------------------------------------------------------------------- /CODE_OF_CONDUCT.md: -------------------------------------------------------------------------------- 1 | # Contributor Covenant Code of Conduct 2 | 3 | ## Our Pledge 4 | 5 | We as members, contributors, and leaders pledge to make participation in our 6 | community a harassment-free experience for everyone, regardless of age, body 7 | size, visible or invisible disability, ethnicity, sex characteristics, gender 8 | identity and expression, level of experience, education, socio-economic status, 9 | nationality, personal appearance, race, religion, or sexual identity 10 | and orientation. 11 | 12 | We pledge to act and interact in ways that contribute to an open, welcoming, 13 | diverse, inclusive, and healthy community. 14 | 15 | ## Our Standards 16 | 17 | Examples of behavior that contributes to a positive environment for our 18 | community include: 19 | 20 | * Demonstrating empathy and kindness toward other people 21 | * Being respectful of differing opinions, viewpoints, and experiences 22 | * Giving and gracefully accepting constructive feedback 23 | * Accepting responsibility and apologizing to those affected by our mistakes, 24 | and learning from the experience 25 | * Focusing on what is best not just for us as individuals, but for the 26 | overall community 27 | 28 | Examples of unacceptable behavior include: 29 | 30 | * The use of sexualized language or imagery, and sexual attention or 31 | advances of any kind 32 | * Trolling, insulting or derogatory comments, and personal or political attacks 33 | * Public or private harassment 34 | * Publishing others' private information, such as a physical or email 35 | address, without their explicit permission 36 | * Other conduct which could reasonably be considered inappropriate in a 37 | professional setting 38 | 39 | ## Enforcement Responsibilities 40 | 41 | Community leaders are responsible for clarifying and enforcing our standards of 42 | acceptable behavior and will take appropriate and fair corrective action in 43 | response to any behavior that they deem inappropriate, threatening, offensive, 44 | or harmful. 45 | 46 | Community leaders have the right and responsibility to remove, edit, or reject 47 | comments, commits, code, wiki edits, issues, and other contributions that are 48 | not aligned to this Code of Conduct, and will communicate reasons for moderation 49 | decisions when appropriate. 50 | 51 | ## Scope 52 | 53 | This Code of Conduct applies within all community spaces, and also applies when 54 | an individual is officially representing the community in public spaces. 55 | Examples of representing our community include using an official e-mail address, 56 | posting via an official social media account, or acting as an appointed 57 | representative at an online or offline event. 58 | 59 | ## Enforcement 60 | 61 | Instances of abusive, harassing, or otherwise unacceptable behavior may be 62 | reported to the community leaders responsible for enforcement at 63 | email (un.pensiero.vano@gmail.com). 64 | All complaints will be reviewed and investigated promptly and fairly. 65 | 66 | All community leaders are obligated to respect the privacy and security of the 67 | reporter of any incident. 68 | 69 | ## Enforcement Guidelines 70 | 71 | Community leaders will follow these Community Impact Guidelines in determining 72 | the consequences for any action they deem in violation of this Code of Conduct: 73 | 74 | ### 1. Correction 75 | 76 | **Community Impact**: Use of inappropriate language or other behavior deemed 77 | unprofessional or unwelcome in the community. 78 | 79 | **Consequence**: A private, written warning from community leaders, providing 80 | clarity around the nature of the violation and an explanation of why the 81 | behavior was inappropriate. A public apology may be requested. 82 | 83 | ### 2. Warning 84 | 85 | **Community Impact**: A violation through a single incident or series 86 | of actions. 87 | 88 | **Consequence**: A warning with consequences for continued behavior. No 89 | interaction with the people involved, including unsolicited interaction with 90 | those enforcing the Code of Conduct, for a specified period of time. This 91 | includes avoiding interactions in community spaces as well as external channels 92 | like social media. Violating these terms may lead to a temporary or 93 | permanent ban. 94 | 95 | ### 3. Temporary Ban 96 | 97 | **Community Impact**: A serious violation of community standards, including 98 | sustained inappropriate behavior. 99 | 100 | **Consequence**: A temporary ban from any sort of interaction or public 101 | communication with the community for a specified period of time. No public or 102 | private interaction with the people involved, including unsolicited interaction 103 | with those enforcing the Code of Conduct, is allowed during this period. 104 | Violating these terms may lead to a permanent ban. 105 | 106 | ### 4. Permanent Ban 107 | 108 | **Community Impact**: Demonstrating a pattern of violation of community 109 | standards, including sustained inappropriate behavior, harassment of an 110 | individual, or aggression toward or disparagement of classes of individuals. 111 | 112 | **Consequence**: A permanent ban from any sort of public interaction within 113 | the community. 114 | 115 | ## Attribution 116 | 117 | This Code of Conduct is adapted from the [Contributor Covenant][homepage], 118 | version 2.0, available at 119 | https://www.contributor-covenant.org/version/2/0/code_of_conduct.html. 120 | 121 | Community Impact Guidelines were inspired by [Mozilla's code of conduct 122 | enforcement ladder](https://github.com/mozilla/diversity). 123 | 124 | [homepage]: https://www.contributor-covenant.org 125 | 126 | For answers to common questions about this code of conduct, see the FAQ at 127 | https://www.contributor-covenant.org/faq. Translations are available at 128 | https://www.contributor-covenant.org/translations. 129 | -------------------------------------------------------------------------------- /CONTRIBUTING.md: -------------------------------------------------------------------------------- 1 | # Contributing to jnv 2 | 3 | We welcome contributions to "jnv" and greatly appreciate your help in making 4 | this project even better. Here's a quick guide to get you started. 5 | 6 | ## How to Contribute 7 | 8 | 1. **Fork the Repository**: Click the "Fork" button at the top right of the 9 | [jnv repository](https://github.com/ynqa/jnv) to create a copy of the 10 | project in your GitHub account. 11 | 12 | 2. **Clone the Repository**: On your local machine, open a terminal and run the 13 | following command, replacing `` with your GitHub username: 14 | 15 | ```bash 16 | git clone https://github.com//jnv.git 17 | ``` 18 | 19 | 3. **Create a Branch**: Before making any changes, create a new branch for your 20 | work: 21 | 22 | ```bash 23 | git checkout -b your-branch-name 24 | ``` 25 | 26 | 4. **Make Changes**: Make your desired code changes, bug fixes, or feature 27 | additions. 28 | 29 | 5. **Commit Your Changes**: Commit your changes with a clear and concise message 30 | explaining the purpose of your contribution: 31 | 32 | ```bash 33 | git commit -m "Your commit message here" 34 | ``` 35 | 36 | 6. **Push to Your Fork**: Push your changes to your forked repository on GitHub: 37 | 38 | ```bash 39 | git push origin your-branch-name 40 | ``` 41 | 42 | 7. **Create a Pull Request (PR)**: Open the 43 | [jnv Pull Request page](https://github.com/ynqa/jnv/pulls) and click the 44 | "New Pull Request" button. Compare and create your PR by following the prompts. 45 | 46 | 8. **Review and Discuss**: Your PR will be reviewed by project maintainers, who 47 | may provide feedback or request further changes. Be prepared for discussion and 48 | updates. 49 | 50 | 9. **Merging**: Once your PR is approved and passes any necessary tests, a 51 | project maintainer will merge it into the main repository. 52 | 53 | ## Code of Conduct 54 | 55 | Please adhere to our [Code of Conduct](CODE_OF_CONDUCT.md) when participating in 56 | this project. We aim to create a respectful and inclusive community for all 57 | contributors. 58 | 59 | Thank you for considering contributing to "jnv"! 60 | -------------------------------------------------------------------------------- /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.22.0" 8 | source = "registry+https://github.com/rust-lang/crates.io-index" 9 | checksum = "6e4503c46a5c0c7844e948c9a4d6acd9f50cccb4de1c48eb9e291ea17470c678" 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 = "ahash" 22 | version = "0.8.11" 23 | source = "registry+https://github.com/rust-lang/crates.io-index" 24 | checksum = "e89da841a80418a9b391ebaea17f5c112ffaaa96f621d2c285b5174da76b9011" 25 | dependencies = [ 26 | "cfg-if", 27 | "getrandom", 28 | "once_cell", 29 | "version_check", 30 | "zerocopy", 31 | ] 32 | 33 | [[package]] 34 | name = "aho-corasick" 35 | version = "1.1.3" 36 | source = "registry+https://github.com/rust-lang/crates.io-index" 37 | checksum = "8e60d3430d3a69478ad0993f19238d2df97c507009a52b3c10addcd7f6bcb916" 38 | dependencies = [ 39 | "memchr", 40 | ] 41 | 42 | [[package]] 43 | name = "allocator-api2" 44 | version = "0.2.18" 45 | source = "registry+https://github.com/rust-lang/crates.io-index" 46 | checksum = "5c6cb57a04249c6480766f7f7cef5467412af1490f8d1e243141daddada3264f" 47 | 48 | [[package]] 49 | name = "anstream" 50 | version = "0.6.13" 51 | source = "registry+https://github.com/rust-lang/crates.io-index" 52 | checksum = "d96bd03f33fe50a863e394ee9718a706f988b9079b20c3784fb726e7678b62fb" 53 | dependencies = [ 54 | "anstyle", 55 | "anstyle-parse", 56 | "anstyle-query", 57 | "anstyle-wincon", 58 | "colorchoice", 59 | "utf8parse", 60 | ] 61 | 62 | [[package]] 63 | name = "anstyle" 64 | version = "1.0.10" 65 | source = "registry+https://github.com/rust-lang/crates.io-index" 66 | checksum = "55cc3b69f167a1ef2e161439aa98aed94e6028e5f9a59be9a6ffb47aef1651f9" 67 | 68 | [[package]] 69 | name = "anstyle-parse" 70 | version = "0.2.3" 71 | source = "registry+https://github.com/rust-lang/crates.io-index" 72 | checksum = "c75ac65da39e5fe5ab759307499ddad880d724eed2f6ce5b5e8a26f4f387928c" 73 | dependencies = [ 74 | "utf8parse", 75 | ] 76 | 77 | [[package]] 78 | name = "anstyle-query" 79 | version = "1.0.2" 80 | source = "registry+https://github.com/rust-lang/crates.io-index" 81 | checksum = "e28923312444cdd728e4738b3f9c9cac739500909bb3d3c94b43551b16517648" 82 | dependencies = [ 83 | "windows-sys 0.52.0", 84 | ] 85 | 86 | [[package]] 87 | name = "anstyle-wincon" 88 | version = "3.0.2" 89 | source = "registry+https://github.com/rust-lang/crates.io-index" 90 | checksum = "1cd54b81ec8d6180e24654d0b371ad22fc3dd083b6ff8ba325b72e00c87660a7" 91 | dependencies = [ 92 | "anstyle", 93 | "windows-sys 0.52.0", 94 | ] 95 | 96 | [[package]] 97 | name = "anyhow" 98 | version = "1.0.97" 99 | source = "registry+https://github.com/rust-lang/crates.io-index" 100 | checksum = "dcfed56ad506cb2c684a14971b8861fdc3baaaae314b9e5f9bb532cbe3ba7a4f" 101 | 102 | [[package]] 103 | name = "arboard" 104 | version = "3.4.1" 105 | source = "registry+https://github.com/rust-lang/crates.io-index" 106 | checksum = "df099ccb16cd014ff054ac1bf392c67feeef57164b05c42f037cd40f5d4357f4" 107 | dependencies = [ 108 | "clipboard-win", 109 | "core-graphics", 110 | "image", 111 | "log", 112 | "objc2", 113 | "objc2-app-kit", 114 | "objc2-foundation", 115 | "parking_lot", 116 | "windows-sys 0.48.0", 117 | "x11rb", 118 | ] 119 | 120 | [[package]] 121 | name = "async-trait" 122 | version = "0.1.88" 123 | source = "registry+https://github.com/rust-lang/crates.io-index" 124 | checksum = "e539d3fca749fcee5236ab05e93a52867dd549cc157c8cb7f99595f3cedffdb5" 125 | dependencies = [ 126 | "proc-macro2", 127 | "quote", 128 | "syn", 129 | ] 130 | 131 | [[package]] 132 | name = "autocfg" 133 | version = "1.2.0" 134 | source = "registry+https://github.com/rust-lang/crates.io-index" 135 | checksum = "f1fdabc7756949593fe60f30ec81974b613357de856987752631dea1e3394c80" 136 | 137 | [[package]] 138 | name = "backtrace" 139 | version = "0.3.73" 140 | source = "registry+https://github.com/rust-lang/crates.io-index" 141 | checksum = "5cc23269a4f8976d0a4d2e7109211a419fe30e8d88d677cd60b6bc79c5732e0a" 142 | dependencies = [ 143 | "addr2line", 144 | "cc", 145 | "cfg-if", 146 | "libc", 147 | "miniz_oxide", 148 | "object", 149 | "rustc-demangle", 150 | ] 151 | 152 | [[package]] 153 | name = "base64" 154 | version = "0.21.7" 155 | source = "registry+https://github.com/rust-lang/crates.io-index" 156 | checksum = "9d297deb1925b89f2ccc13d7635fa0714f12c87adce1c75356b39ca9b7178567" 157 | 158 | [[package]] 159 | name = "bincode" 160 | version = "1.3.3" 161 | source = "registry+https://github.com/rust-lang/crates.io-index" 162 | checksum = "b1f45e9417d87227c7a56d22e471c6206462cba514c7590c09aff4cf6d1ddcad" 163 | dependencies = [ 164 | "serde", 165 | ] 166 | 167 | [[package]] 168 | name = "bitflags" 169 | version = "1.3.2" 170 | source = "registry+https://github.com/rust-lang/crates.io-index" 171 | checksum = "bef38d45163c2f1dde094a7dfd33ccf595c92905c8f8f4fdc18d06fb1037718a" 172 | 173 | [[package]] 174 | name = "bitflags" 175 | version = "2.5.0" 176 | source = "registry+https://github.com/rust-lang/crates.io-index" 177 | checksum = "cf4b9d6a944f767f8e5e0db018570623c85f3d925ac718db4e06d0187adb21c1" 178 | dependencies = [ 179 | "serde", 180 | ] 181 | 182 | [[package]] 183 | name = "block2" 184 | version = "0.5.1" 185 | source = "registry+https://github.com/rust-lang/crates.io-index" 186 | checksum = "2c132eebf10f5cad5289222520a4a058514204aed6d791f1cf4fe8088b82d15f" 187 | dependencies = [ 188 | "objc2", 189 | ] 190 | 191 | [[package]] 192 | name = "bytemuck" 193 | version = "1.16.3" 194 | source = "registry+https://github.com/rust-lang/crates.io-index" 195 | checksum = "102087e286b4677862ea56cf8fc58bb2cdfa8725c40ffb80fe3a008eb7f2fc83" 196 | 197 | [[package]] 198 | name = "byteorder-lite" 199 | version = "0.1.0" 200 | source = "registry+https://github.com/rust-lang/crates.io-index" 201 | checksum = "8f1fe948ff07f4bd06c30984e69f5b4899c516a3ef74f34df92a2df2ab535495" 202 | 203 | [[package]] 204 | name = "bytes" 205 | version = "1.9.0" 206 | source = "registry+https://github.com/rust-lang/crates.io-index" 207 | checksum = "325918d6fe32f23b19878fe4b34794ae41fc19ddbe53b10571a4874d44ffd39b" 208 | 209 | [[package]] 210 | name = "cc" 211 | version = "1.2.5" 212 | source = "registry+https://github.com/rust-lang/crates.io-index" 213 | checksum = "c31a0499c1dc64f458ad13872de75c0eb7e3fdb0e67964610c914b034fc5956e" 214 | dependencies = [ 215 | "shlex", 216 | ] 217 | 218 | [[package]] 219 | name = "cfg-if" 220 | version = "1.0.0" 221 | source = "registry+https://github.com/rust-lang/crates.io-index" 222 | checksum = "baf1de4339761588bc0619e3cbc0120ee582ebb74b53b4efbf79117bd2da40fd" 223 | 224 | [[package]] 225 | name = "chumsky" 226 | version = "0.9.3" 227 | source = "registry+https://github.com/rust-lang/crates.io-index" 228 | checksum = "8eebd66744a15ded14960ab4ccdbfb51ad3b81f51f3f04a80adac98c985396c9" 229 | dependencies = [ 230 | "hashbrown 0.14.3", 231 | ] 232 | 233 | [[package]] 234 | name = "clap" 235 | version = "4.5.34" 236 | source = "registry+https://github.com/rust-lang/crates.io-index" 237 | checksum = "e958897981290da2a852763fe9cdb89cd36977a5d729023127095fa94d95e2ff" 238 | dependencies = [ 239 | "clap_builder", 240 | "clap_derive", 241 | ] 242 | 243 | [[package]] 244 | name = "clap_builder" 245 | version = "4.5.34" 246 | source = "registry+https://github.com/rust-lang/crates.io-index" 247 | checksum = "83b0f35019843db2160b5bb19ae09b4e6411ac33fc6a712003c33e03090e2489" 248 | dependencies = [ 249 | "anstream", 250 | "anstyle", 251 | "clap_lex", 252 | "strsim", 253 | ] 254 | 255 | [[package]] 256 | name = "clap_derive" 257 | version = "4.5.32" 258 | source = "registry+https://github.com/rust-lang/crates.io-index" 259 | checksum = "09176aae279615badda0765c0c0b3f6ed53f4709118af73cf4655d85d1530cd7" 260 | dependencies = [ 261 | "heck", 262 | "proc-macro2", 263 | "quote", 264 | "syn", 265 | ] 266 | 267 | [[package]] 268 | name = "clap_lex" 269 | version = "0.7.4" 270 | source = "registry+https://github.com/rust-lang/crates.io-index" 271 | checksum = "f46ad14479a25103f283c0f10005961cf086d8dc42205bb44c46ac563475dca6" 272 | 273 | [[package]] 274 | name = "clipboard-win" 275 | version = "5.4.0" 276 | source = "registry+https://github.com/rust-lang/crates.io-index" 277 | checksum = "15efe7a882b08f34e38556b14f2fb3daa98769d06c7f0c1b076dfd0d983bc892" 278 | dependencies = [ 279 | "error-code", 280 | ] 281 | 282 | [[package]] 283 | name = "colorchoice" 284 | version = "1.0.0" 285 | source = "registry+https://github.com/rust-lang/crates.io-index" 286 | checksum = "acbf1af155f9b9ef647e42cdc158db4b64a1b61f743629225fde6f3e0be2a7c7" 287 | 288 | [[package]] 289 | name = "core-foundation" 290 | version = "0.9.4" 291 | source = "registry+https://github.com/rust-lang/crates.io-index" 292 | checksum = "91e195e091a93c46f7102ec7818a2aa394e1e1771c3ab4825963fa03e45afb8f" 293 | dependencies = [ 294 | "core-foundation-sys", 295 | "libc", 296 | ] 297 | 298 | [[package]] 299 | name = "core-foundation-sys" 300 | version = "0.8.6" 301 | source = "registry+https://github.com/rust-lang/crates.io-index" 302 | checksum = "06ea2b9bc92be3c2baa9334a323ebca2d6f074ff852cd1d7b11064035cd3868f" 303 | 304 | [[package]] 305 | name = "core-graphics" 306 | version = "0.23.2" 307 | source = "registry+https://github.com/rust-lang/crates.io-index" 308 | checksum = "c07782be35f9e1140080c6b96f0d44b739e2278479f64e02fdab4e32dfd8b081" 309 | dependencies = [ 310 | "bitflags 1.3.2", 311 | "core-foundation", 312 | "core-graphics-types", 313 | "foreign-types", 314 | "libc", 315 | ] 316 | 317 | [[package]] 318 | name = "core-graphics-types" 319 | version = "0.1.3" 320 | source = "registry+https://github.com/rust-lang/crates.io-index" 321 | checksum = "45390e6114f68f718cc7a830514a96f903cccd70d02a8f6d9f643ac4ba45afaf" 322 | dependencies = [ 323 | "bitflags 1.3.2", 324 | "core-foundation", 325 | "libc", 326 | ] 327 | 328 | [[package]] 329 | name = "crc32fast" 330 | version = "1.4.2" 331 | source = "registry+https://github.com/rust-lang/crates.io-index" 332 | checksum = "a97769d94ddab943e4510d138150169a2758b5ef3eb191a9ee688de3e23ef7b3" 333 | dependencies = [ 334 | "cfg-if", 335 | ] 336 | 337 | [[package]] 338 | name = "crossbeam-deque" 339 | version = "0.8.6" 340 | source = "registry+https://github.com/rust-lang/crates.io-index" 341 | checksum = "9dd111b7b7f7d55b72c0a6ae361660ee5853c9af73f70c3c2ef6858b950e2e51" 342 | dependencies = [ 343 | "crossbeam-epoch", 344 | "crossbeam-utils", 345 | ] 346 | 347 | [[package]] 348 | name = "crossbeam-epoch" 349 | version = "0.9.18" 350 | source = "registry+https://github.com/rust-lang/crates.io-index" 351 | checksum = "5b82ac4a3c2ca9c3460964f020e1402edd5753411d7737aa39c3714ad1b5420e" 352 | dependencies = [ 353 | "crossbeam-utils", 354 | ] 355 | 356 | [[package]] 357 | name = "crossbeam-utils" 358 | version = "0.8.21" 359 | source = "registry+https://github.com/rust-lang/crates.io-index" 360 | checksum = "d0a5c400df2834b80a4c3327b3aad3a4c4cd4de0629063962b03235697506a28" 361 | 362 | [[package]] 363 | name = "crossterm" 364 | version = "0.28.1" 365 | source = "registry+https://github.com/rust-lang/crates.io-index" 366 | checksum = "829d955a0bb380ef178a640b91779e3987da38c9aea133b20614cfed8cdea9c6" 367 | dependencies = [ 368 | "bitflags 2.5.0", 369 | "crossterm_winapi", 370 | "filedescriptor", 371 | "futures-core", 372 | "libc", 373 | "mio", 374 | "parking_lot", 375 | "rustix", 376 | "serde", 377 | "signal-hook", 378 | "signal-hook-mio", 379 | "winapi", 380 | ] 381 | 382 | [[package]] 383 | name = "crossterm_winapi" 384 | version = "0.9.1" 385 | source = "registry+https://github.com/rust-lang/crates.io-index" 386 | checksum = "acdd7c62a3665c7f6830a51635d9ac9b23ed385797f70a83bb8bafe9c572ab2b" 387 | dependencies = [ 388 | "winapi", 389 | ] 390 | 391 | [[package]] 392 | name = "darling" 393 | version = "0.20.10" 394 | source = "registry+https://github.com/rust-lang/crates.io-index" 395 | checksum = "6f63b86c8a8826a49b8c21f08a2d07338eec8d900540f8630dc76284be802989" 396 | dependencies = [ 397 | "darling_core", 398 | "darling_macro", 399 | ] 400 | 401 | [[package]] 402 | name = "darling_core" 403 | version = "0.20.10" 404 | source = "registry+https://github.com/rust-lang/crates.io-index" 405 | checksum = "95133861a8032aaea082871032f5815eb9e98cef03fa916ab4500513994df9e5" 406 | dependencies = [ 407 | "fnv", 408 | "ident_case", 409 | "proc-macro2", 410 | "quote", 411 | "strsim", 412 | "syn", 413 | ] 414 | 415 | [[package]] 416 | name = "darling_macro" 417 | version = "0.20.10" 418 | source = "registry+https://github.com/rust-lang/crates.io-index" 419 | checksum = "d336a2a514f6ccccaa3e09b02d41d35330c07ddf03a62165fcec10bb561c7806" 420 | dependencies = [ 421 | "darling_core", 422 | "quote", 423 | "syn", 424 | ] 425 | 426 | [[package]] 427 | name = "deranged" 428 | version = "0.3.11" 429 | source = "registry+https://github.com/rust-lang/crates.io-index" 430 | checksum = "b42b6fa04a440b495c8b04d0e71b707c585f83cb9cb28cf8cd0d976c315e31b4" 431 | dependencies = [ 432 | "powerfmt", 433 | ] 434 | 435 | [[package]] 436 | name = "derive_builder" 437 | version = "0.20.2" 438 | source = "registry+https://github.com/rust-lang/crates.io-index" 439 | checksum = "507dfb09ea8b7fa618fcf76e953f4f5e192547945816d5358edffe39f6f94947" 440 | dependencies = [ 441 | "derive_builder_macro", 442 | ] 443 | 444 | [[package]] 445 | name = "derive_builder_core" 446 | version = "0.20.2" 447 | source = "registry+https://github.com/rust-lang/crates.io-index" 448 | checksum = "2d5bcf7b024d6835cfb3d473887cd966994907effbe9227e8c8219824d06c4e8" 449 | dependencies = [ 450 | "darling", 451 | "proc-macro2", 452 | "quote", 453 | "syn", 454 | ] 455 | 456 | [[package]] 457 | name = "derive_builder_macro" 458 | version = "0.20.2" 459 | source = "registry+https://github.com/rust-lang/crates.io-index" 460 | checksum = "ab63b0e2bf4d5928aff72e83a7dace85d7bba5fe12dcc3c5a572d78caffd3f3c" 461 | dependencies = [ 462 | "derive_builder_core", 463 | "syn", 464 | ] 465 | 466 | [[package]] 467 | name = "dirs" 468 | version = "6.0.0" 469 | source = "registry+https://github.com/rust-lang/crates.io-index" 470 | checksum = "c3e8aa94d75141228480295a7d0e7feb620b1a5ad9f12bc40be62411e38cce4e" 471 | dependencies = [ 472 | "dirs-sys", 473 | ] 474 | 475 | [[package]] 476 | name = "dirs-sys" 477 | version = "0.5.0" 478 | source = "registry+https://github.com/rust-lang/crates.io-index" 479 | checksum = "e01a3366d27ee9890022452ee61b2b63a67e6f13f58900b651ff5665f0bb1fab" 480 | dependencies = [ 481 | "libc", 482 | "option-ext", 483 | "redox_users", 484 | "windows-sys 0.59.0", 485 | ] 486 | 487 | [[package]] 488 | name = "duration-string" 489 | version = "0.5.2" 490 | source = "registry+https://github.com/rust-lang/crates.io-index" 491 | checksum = "04782251e09dc67c90d694d89e9a3e5fc6cfe883df1b203202de672d812fb299" 492 | dependencies = [ 493 | "serde", 494 | ] 495 | 496 | [[package]] 497 | name = "dyn-clone" 498 | version = "1.0.17" 499 | source = "registry+https://github.com/rust-lang/crates.io-index" 500 | checksum = "0d6ef0072f8a535281e4876be788938b528e9a1d43900b82c2569af7da799125" 501 | 502 | [[package]] 503 | name = "either" 504 | version = "1.15.0" 505 | source = "registry+https://github.com/rust-lang/crates.io-index" 506 | checksum = "48c757948c5ede0e46177b7add2e67155f70e33c07fea8284df6576da70b3719" 507 | 508 | [[package]] 509 | name = "equivalent" 510 | version = "1.0.1" 511 | source = "registry+https://github.com/rust-lang/crates.io-index" 512 | checksum = "5443807d6dff69373d433ab9ef5378ad8df50ca6298caf15de6e52e24aaf54d5" 513 | 514 | [[package]] 515 | name = "errno" 516 | version = "0.3.9" 517 | source = "registry+https://github.com/rust-lang/crates.io-index" 518 | checksum = "534c5cf6194dfab3db3242765c03bbe257cf92f22b38f6bc0c58d59108a820ba" 519 | dependencies = [ 520 | "libc", 521 | "windows-sys 0.52.0", 522 | ] 523 | 524 | [[package]] 525 | name = "error-code" 526 | version = "3.2.0" 527 | source = "registry+https://github.com/rust-lang/crates.io-index" 528 | checksum = "a0474425d51df81997e2f90a21591180b38eccf27292d755f3e30750225c175b" 529 | 530 | [[package]] 531 | name = "fdeflate" 532 | version = "0.3.4" 533 | source = "registry+https://github.com/rust-lang/crates.io-index" 534 | checksum = "4f9bfee30e4dedf0ab8b422f03af778d9612b63f502710fc500a334ebe2de645" 535 | dependencies = [ 536 | "simd-adler32", 537 | ] 538 | 539 | [[package]] 540 | name = "filedescriptor" 541 | version = "0.8.2" 542 | source = "registry+https://github.com/rust-lang/crates.io-index" 543 | checksum = "7199d965852c3bac31f779ef99cbb4537f80e952e2d6aa0ffeb30cce00f4f46e" 544 | dependencies = [ 545 | "libc", 546 | "thiserror 1.0.64", 547 | "winapi", 548 | ] 549 | 550 | [[package]] 551 | name = "flate2" 552 | version = "1.0.31" 553 | source = "registry+https://github.com/rust-lang/crates.io-index" 554 | checksum = "7f211bbe8e69bbd0cfdea405084f128ae8b4aaa6b0b522fc8f2b009084797920" 555 | dependencies = [ 556 | "crc32fast", 557 | "miniz_oxide", 558 | ] 559 | 560 | [[package]] 561 | name = "fnv" 562 | version = "1.0.7" 563 | source = "registry+https://github.com/rust-lang/crates.io-index" 564 | checksum = "3f9eec918d3f24069decb9af1554cad7c880e2da24a9afd88aca000531ab82c1" 565 | 566 | [[package]] 567 | name = "foreign-types" 568 | version = "0.5.0" 569 | source = "registry+https://github.com/rust-lang/crates.io-index" 570 | checksum = "d737d9aa519fb7b749cbc3b962edcf310a8dd1f4b67c91c4f83975dbdd17d965" 571 | dependencies = [ 572 | "foreign-types-macros", 573 | "foreign-types-shared", 574 | ] 575 | 576 | [[package]] 577 | name = "foreign-types-macros" 578 | version = "0.2.3" 579 | source = "registry+https://github.com/rust-lang/crates.io-index" 580 | checksum = "1a5c6c585bc94aaf2c7b51dd4c2ba22680844aba4c687be581871a6f518c5742" 581 | dependencies = [ 582 | "proc-macro2", 583 | "quote", 584 | "syn", 585 | ] 586 | 587 | [[package]] 588 | name = "foreign-types-shared" 589 | version = "0.3.1" 590 | source = "registry+https://github.com/rust-lang/crates.io-index" 591 | checksum = "aa9a19cbb55df58761df49b23516a86d432839add4af60fc256da840f66ed35b" 592 | 593 | [[package]] 594 | name = "futures" 595 | version = "0.3.31" 596 | source = "registry+https://github.com/rust-lang/crates.io-index" 597 | checksum = "65bc07b1a8bc7c85c5f2e110c476c7389b4554ba72af57d8445ea63a576b0876" 598 | dependencies = [ 599 | "futures-channel", 600 | "futures-core", 601 | "futures-executor", 602 | "futures-io", 603 | "futures-sink", 604 | "futures-task", 605 | "futures-util", 606 | ] 607 | 608 | [[package]] 609 | name = "futures-channel" 610 | version = "0.3.31" 611 | source = "registry+https://github.com/rust-lang/crates.io-index" 612 | checksum = "2dff15bf788c671c1934e366d07e30c1814a8ef514e1af724a602e8a2fbe1b10" 613 | dependencies = [ 614 | "futures-core", 615 | "futures-sink", 616 | ] 617 | 618 | [[package]] 619 | name = "futures-core" 620 | version = "0.3.31" 621 | source = "registry+https://github.com/rust-lang/crates.io-index" 622 | checksum = "05f29059c0c2090612e8d742178b0580d2dc940c837851ad723096f87af6663e" 623 | 624 | [[package]] 625 | name = "futures-executor" 626 | version = "0.3.31" 627 | source = "registry+https://github.com/rust-lang/crates.io-index" 628 | checksum = "1e28d1d997f585e54aebc3f97d39e72338912123a67330d723fdbb564d646c9f" 629 | dependencies = [ 630 | "futures-core", 631 | "futures-task", 632 | "futures-util", 633 | ] 634 | 635 | [[package]] 636 | name = "futures-io" 637 | version = "0.3.31" 638 | source = "registry+https://github.com/rust-lang/crates.io-index" 639 | checksum = "9e5c1b78ca4aae1ac06c48a526a655760685149f0d465d21f37abfe57ce075c6" 640 | 641 | [[package]] 642 | name = "futures-macro" 643 | version = "0.3.31" 644 | source = "registry+https://github.com/rust-lang/crates.io-index" 645 | checksum = "162ee34ebcb7c64a8abebc059ce0fee27c2262618d7b60ed8faf72fef13c3650" 646 | dependencies = [ 647 | "proc-macro2", 648 | "quote", 649 | "syn", 650 | ] 651 | 652 | [[package]] 653 | name = "futures-sink" 654 | version = "0.3.31" 655 | source = "registry+https://github.com/rust-lang/crates.io-index" 656 | checksum = "e575fab7d1e0dcb8d0c7bcf9a63ee213816ab51902e6d244a95819acacf1d4f7" 657 | 658 | [[package]] 659 | name = "futures-task" 660 | version = "0.3.31" 661 | source = "registry+https://github.com/rust-lang/crates.io-index" 662 | checksum = "f90f7dce0722e95104fcb095585910c0977252f286e354b5e3bd38902cd99988" 663 | 664 | [[package]] 665 | name = "futures-util" 666 | version = "0.3.31" 667 | source = "registry+https://github.com/rust-lang/crates.io-index" 668 | checksum = "9fa08315bb612088cc391249efdc3bc77536f16c91f6cf495e6fbe85b20a4a81" 669 | dependencies = [ 670 | "futures-channel", 671 | "futures-core", 672 | "futures-io", 673 | "futures-macro", 674 | "futures-sink", 675 | "futures-task", 676 | "memchr", 677 | "pin-project-lite", 678 | "pin-utils", 679 | "slab", 680 | ] 681 | 682 | [[package]] 683 | name = "gethostname" 684 | version = "0.4.3" 685 | source = "registry+https://github.com/rust-lang/crates.io-index" 686 | checksum = "0176e0459c2e4a1fe232f984bca6890e681076abb9934f6cea7c326f3fc47818" 687 | dependencies = [ 688 | "libc", 689 | "windows-targets 0.48.5", 690 | ] 691 | 692 | [[package]] 693 | name = "getrandom" 694 | version = "0.2.14" 695 | source = "registry+https://github.com/rust-lang/crates.io-index" 696 | checksum = "94b22e06ecb0110981051723910cbf0b5f5e09a2062dd7663334ee79a9d1286c" 697 | dependencies = [ 698 | "cfg-if", 699 | "libc", 700 | "wasi", 701 | ] 702 | 703 | [[package]] 704 | name = "gimli" 705 | version = "0.29.0" 706 | source = "registry+https://github.com/rust-lang/crates.io-index" 707 | checksum = "40ecd4077b5ae9fd2e9e169b102c6c330d0605168eb0e8bf79952b256dbefffd" 708 | 709 | [[package]] 710 | name = "hashbrown" 711 | version = "0.14.3" 712 | source = "registry+https://github.com/rust-lang/crates.io-index" 713 | checksum = "290f1a1d9242c78d09ce40a5e87e7554ee637af1351968159f4952f028f75604" 714 | dependencies = [ 715 | "ahash", 716 | "allocator-api2", 717 | ] 718 | 719 | [[package]] 720 | name = "hashbrown" 721 | version = "0.15.2" 722 | source = "registry+https://github.com/rust-lang/crates.io-index" 723 | checksum = "bf151400ff0baff5465007dd2f3e717f3fe502074ca563069ce3a6629d07b289" 724 | 725 | [[package]] 726 | name = "heck" 727 | version = "0.5.0" 728 | source = "registry+https://github.com/rust-lang/crates.io-index" 729 | checksum = "2304e00983f87ffb38b55b444b5e3b60a884b5d30c0fca7d82fe33449bbe55ea" 730 | 731 | [[package]] 732 | name = "hermit-abi" 733 | version = "0.3.9" 734 | source = "registry+https://github.com/rust-lang/crates.io-index" 735 | checksum = "d231dfb89cfffdbc30e7fc41579ed6066ad03abda9e567ccafae602b97ec5024" 736 | 737 | [[package]] 738 | name = "hifijson" 739 | version = "0.2.1" 740 | source = "registry+https://github.com/rust-lang/crates.io-index" 741 | checksum = "18ae468bcb4dfecf0e4949ee28abbc99076b6a0077f51ddbc94dbfff8e6a870c" 742 | 743 | [[package]] 744 | name = "ident_case" 745 | version = "1.0.1" 746 | source = "registry+https://github.com/rust-lang/crates.io-index" 747 | checksum = "b9e0384b61958566e926dc50660321d12159025e767c18e043daf26b70104c39" 748 | 749 | [[package]] 750 | name = "image" 751 | version = "0.25.2" 752 | source = "registry+https://github.com/rust-lang/crates.io-index" 753 | checksum = "99314c8a2152b8ddb211f924cdae532d8c5e4c8bb54728e12fff1b0cd5963a10" 754 | dependencies = [ 755 | "bytemuck", 756 | "byteorder-lite", 757 | "num-traits", 758 | "png", 759 | "tiff", 760 | ] 761 | 762 | [[package]] 763 | name = "indexmap" 764 | version = "2.7.0" 765 | source = "registry+https://github.com/rust-lang/crates.io-index" 766 | checksum = "62f822373a4fe84d4bb149bf54e584a7f4abec90e072ed49cda0edea5b95471f" 767 | dependencies = [ 768 | "equivalent", 769 | "hashbrown 0.15.2", 770 | ] 771 | 772 | [[package]] 773 | name = "itoa" 774 | version = "1.0.11" 775 | source = "registry+https://github.com/rust-lang/crates.io-index" 776 | checksum = "49f1f14873335454500d59611f1cf4a4b0f786f9ac11f4312a78e4cf2566695b" 777 | 778 | [[package]] 779 | name = "jaq-core" 780 | version = "1.2.1" 781 | source = "registry+https://github.com/rust-lang/crates.io-index" 782 | checksum = "03d6a5713b8f33675abfac79d1db0022a3f28764b2a6b96a185c199ad8dab86d" 783 | dependencies = [ 784 | "aho-corasick", 785 | "base64", 786 | "hifijson", 787 | "jaq-interpret", 788 | "libm", 789 | "log", 790 | "regex", 791 | "time", 792 | "urlencoding", 793 | ] 794 | 795 | [[package]] 796 | name = "jaq-interpret" 797 | version = "1.2.1" 798 | source = "registry+https://github.com/rust-lang/crates.io-index" 799 | checksum = "f569e38e5fc677db8dfda89ee0b4c25b3f53e811b16434fd14bdc5b43fc362ac" 800 | dependencies = [ 801 | "ahash", 802 | "dyn-clone", 803 | "hifijson", 804 | "indexmap", 805 | "jaq-syn", 806 | "once_cell", 807 | "serde_json", 808 | ] 809 | 810 | [[package]] 811 | name = "jaq-parse" 812 | version = "1.0.2" 813 | source = "registry+https://github.com/rust-lang/crates.io-index" 814 | checksum = "ef6f8beb9f9922546419e774e24199e8a968f54c63a5a2323c8f3ef3321ace14" 815 | dependencies = [ 816 | "chumsky", 817 | "jaq-syn", 818 | ] 819 | 820 | [[package]] 821 | name = "jaq-std" 822 | version = "1.2.1" 823 | source = "registry+https://github.com/rust-lang/crates.io-index" 824 | checksum = "5d7871c59297cbfdd18f6f1bbbafaad24e97fd555ee1e2a1be7a40a5a20f551a" 825 | dependencies = [ 826 | "bincode", 827 | "jaq-parse", 828 | "jaq-syn", 829 | ] 830 | 831 | [[package]] 832 | name = "jaq-syn" 833 | version = "1.1.0" 834 | source = "registry+https://github.com/rust-lang/crates.io-index" 835 | checksum = "a4d60101fb791b20c982731d848ed6e7d25363656497647c2093b68bd88398d6" 836 | dependencies = [ 837 | "serde", 838 | ] 839 | 840 | [[package]] 841 | name = "jnv" 842 | version = "0.6.0" 843 | dependencies = [ 844 | "anyhow", 845 | "arboard", 846 | "async-trait", 847 | "clap", 848 | "derive_builder", 849 | "dirs", 850 | "duration-string", 851 | "futures", 852 | "jaq-core", 853 | "jaq-interpret", 854 | "jaq-parse", 855 | "jaq-std", 856 | "promkit-core", 857 | "promkit-widgets", 858 | "serde", 859 | "tokio", 860 | "tokio-stream", 861 | "toml", 862 | ] 863 | 864 | [[package]] 865 | name = "jpeg-decoder" 866 | version = "0.3.1" 867 | source = "registry+https://github.com/rust-lang/crates.io-index" 868 | checksum = "f5d4a7da358eff58addd2877a45865158f0d78c911d43a5784ceb7bbf52833b0" 869 | 870 | [[package]] 871 | name = "libc" 872 | version = "0.2.169" 873 | source = "registry+https://github.com/rust-lang/crates.io-index" 874 | checksum = "b5aba8db14291edd000dfcc4d620c7ebfb122c613afb886ca8803fa4e128a20a" 875 | 876 | [[package]] 877 | name = "libm" 878 | version = "0.2.8" 879 | source = "registry+https://github.com/rust-lang/crates.io-index" 880 | checksum = "4ec2a862134d2a7d32d7983ddcdd1c4923530833c9f2ea1a44fc5fa473989058" 881 | 882 | [[package]] 883 | name = "libredox" 884 | version = "0.1.3" 885 | source = "registry+https://github.com/rust-lang/crates.io-index" 886 | checksum = "c0ff37bd590ca25063e35af745c343cb7a0271906fb7b37e4813e8f79f00268d" 887 | dependencies = [ 888 | "bitflags 2.5.0", 889 | "libc", 890 | ] 891 | 892 | [[package]] 893 | name = "linux-raw-sys" 894 | version = "0.4.14" 895 | source = "registry+https://github.com/rust-lang/crates.io-index" 896 | checksum = "78b3ae25bc7c8c38cec158d1f2757ee79e9b3740fbc7ccf0e59e4b08d793fa89" 897 | 898 | [[package]] 899 | name = "lock_api" 900 | version = "0.4.11" 901 | source = "registry+https://github.com/rust-lang/crates.io-index" 902 | checksum = "3c168f8615b12bc01f9c17e2eb0cc07dcae1940121185446edc3744920e8ef45" 903 | dependencies = [ 904 | "autocfg", 905 | "scopeguard", 906 | ] 907 | 908 | [[package]] 909 | name = "log" 910 | version = "0.4.22" 911 | source = "registry+https://github.com/rust-lang/crates.io-index" 912 | checksum = "a7a70ba024b9dc04c27ea2f0c0548feb474ec5c54bba33a7f72f873a39d07b24" 913 | 914 | [[package]] 915 | name = "memchr" 916 | version = "2.7.2" 917 | source = "registry+https://github.com/rust-lang/crates.io-index" 918 | checksum = "6c8640c5d730cb13ebd907d8d04b52f55ac9a2eec55b440c8892f40d56c76c1d" 919 | 920 | [[package]] 921 | name = "miniz_oxide" 922 | version = "0.7.4" 923 | source = "registry+https://github.com/rust-lang/crates.io-index" 924 | checksum = "b8a240ddb74feaf34a79a7add65a741f3167852fba007066dcac1ca548d89c08" 925 | dependencies = [ 926 | "adler", 927 | "simd-adler32", 928 | ] 929 | 930 | [[package]] 931 | name = "mio" 932 | version = "1.0.2" 933 | source = "registry+https://github.com/rust-lang/crates.io-index" 934 | checksum = "80e04d1dcff3aae0704555fe5fee3bcfaf3d1fdf8a7e521d5b9d2b42acb52cec" 935 | dependencies = [ 936 | "hermit-abi", 937 | "libc", 938 | "log", 939 | "wasi", 940 | "windows-sys 0.52.0", 941 | ] 942 | 943 | [[package]] 944 | name = "num-conv" 945 | version = "0.1.0" 946 | source = "registry+https://github.com/rust-lang/crates.io-index" 947 | checksum = "51d515d32fb182ee37cda2ccdcb92950d6a3c2893aa280e540671c2cd0f3b1d9" 948 | 949 | [[package]] 950 | name = "num-traits" 951 | version = "0.2.19" 952 | source = "registry+https://github.com/rust-lang/crates.io-index" 953 | checksum = "071dfc062690e90b734c0b2273ce72ad0ffa95f0c74596bc250dcfd960262841" 954 | dependencies = [ 955 | "autocfg", 956 | ] 957 | 958 | [[package]] 959 | name = "objc-sys" 960 | version = "0.3.5" 961 | source = "registry+https://github.com/rust-lang/crates.io-index" 962 | checksum = "cdb91bdd390c7ce1a8607f35f3ca7151b65afc0ff5ff3b34fa350f7d7c7e4310" 963 | 964 | [[package]] 965 | name = "objc2" 966 | version = "0.5.2" 967 | source = "registry+https://github.com/rust-lang/crates.io-index" 968 | checksum = "46a785d4eeff09c14c487497c162e92766fbb3e4059a71840cecc03d9a50b804" 969 | dependencies = [ 970 | "objc-sys", 971 | "objc2-encode", 972 | ] 973 | 974 | [[package]] 975 | name = "objc2-app-kit" 976 | version = "0.2.2" 977 | source = "registry+https://github.com/rust-lang/crates.io-index" 978 | checksum = "e4e89ad9e3d7d297152b17d39ed92cd50ca8063a89a9fa569046d41568891eff" 979 | dependencies = [ 980 | "bitflags 2.5.0", 981 | "block2", 982 | "libc", 983 | "objc2", 984 | "objc2-core-data", 985 | "objc2-core-image", 986 | "objc2-foundation", 987 | "objc2-quartz-core", 988 | ] 989 | 990 | [[package]] 991 | name = "objc2-core-data" 992 | version = "0.2.2" 993 | source = "registry+https://github.com/rust-lang/crates.io-index" 994 | checksum = "617fbf49e071c178c0b24c080767db52958f716d9eabdf0890523aeae54773ef" 995 | dependencies = [ 996 | "bitflags 2.5.0", 997 | "block2", 998 | "objc2", 999 | "objc2-foundation", 1000 | ] 1001 | 1002 | [[package]] 1003 | name = "objc2-core-image" 1004 | version = "0.2.2" 1005 | source = "registry+https://github.com/rust-lang/crates.io-index" 1006 | checksum = "55260963a527c99f1819c4f8e3b47fe04f9650694ef348ffd2227e8196d34c80" 1007 | dependencies = [ 1008 | "block2", 1009 | "objc2", 1010 | "objc2-foundation", 1011 | "objc2-metal", 1012 | ] 1013 | 1014 | [[package]] 1015 | name = "objc2-encode" 1016 | version = "4.0.3" 1017 | source = "registry+https://github.com/rust-lang/crates.io-index" 1018 | checksum = "7891e71393cd1f227313c9379a26a584ff3d7e6e7159e988851f0934c993f0f8" 1019 | 1020 | [[package]] 1021 | name = "objc2-foundation" 1022 | version = "0.2.2" 1023 | source = "registry+https://github.com/rust-lang/crates.io-index" 1024 | checksum = "0ee638a5da3799329310ad4cfa62fbf045d5f56e3ef5ba4149e7452dcf89d5a8" 1025 | dependencies = [ 1026 | "bitflags 2.5.0", 1027 | "block2", 1028 | "libc", 1029 | "objc2", 1030 | ] 1031 | 1032 | [[package]] 1033 | name = "objc2-metal" 1034 | version = "0.2.2" 1035 | source = "registry+https://github.com/rust-lang/crates.io-index" 1036 | checksum = "dd0cba1276f6023976a406a14ffa85e1fdd19df6b0f737b063b95f6c8c7aadd6" 1037 | dependencies = [ 1038 | "bitflags 2.5.0", 1039 | "block2", 1040 | "objc2", 1041 | "objc2-foundation", 1042 | ] 1043 | 1044 | [[package]] 1045 | name = "objc2-quartz-core" 1046 | version = "0.2.2" 1047 | source = "registry+https://github.com/rust-lang/crates.io-index" 1048 | checksum = "e42bee7bff906b14b167da2bac5efe6b6a07e6f7c0a21a7308d40c960242dc7a" 1049 | dependencies = [ 1050 | "bitflags 2.5.0", 1051 | "block2", 1052 | "objc2", 1053 | "objc2-foundation", 1054 | "objc2-metal", 1055 | ] 1056 | 1057 | [[package]] 1058 | name = "object" 1059 | version = "0.36.7" 1060 | source = "registry+https://github.com/rust-lang/crates.io-index" 1061 | checksum = "62948e14d923ea95ea2c7c86c71013138b66525b86bdc08d2dcc262bdb497b87" 1062 | dependencies = [ 1063 | "memchr", 1064 | ] 1065 | 1066 | [[package]] 1067 | name = "once_cell" 1068 | version = "1.19.0" 1069 | source = "registry+https://github.com/rust-lang/crates.io-index" 1070 | checksum = "3fdb12b2476b595f9358c5161aa467c2438859caa136dec86c26fdd2efe17b92" 1071 | 1072 | [[package]] 1073 | name = "option-ext" 1074 | version = "0.2.0" 1075 | source = "registry+https://github.com/rust-lang/crates.io-index" 1076 | checksum = "04744f49eae99ab78e0d5c0b603ab218f515ea8cfe5a456d7629ad883a3b6e7d" 1077 | 1078 | [[package]] 1079 | name = "parking_lot" 1080 | version = "0.12.1" 1081 | source = "registry+https://github.com/rust-lang/crates.io-index" 1082 | checksum = "3742b2c103b9f06bc9fff0a37ff4912935851bee6d36f3c02bcc755bcfec228f" 1083 | dependencies = [ 1084 | "lock_api", 1085 | "parking_lot_core", 1086 | ] 1087 | 1088 | [[package]] 1089 | name = "parking_lot_core" 1090 | version = "0.9.9" 1091 | source = "registry+https://github.com/rust-lang/crates.io-index" 1092 | checksum = "4c42a9226546d68acdd9c0a280d17ce19bfe27a46bf68784e4066115788d008e" 1093 | dependencies = [ 1094 | "cfg-if", 1095 | "libc", 1096 | "redox_syscall", 1097 | "smallvec", 1098 | "windows-targets 0.48.5", 1099 | ] 1100 | 1101 | [[package]] 1102 | name = "pin-project-lite" 1103 | version = "0.2.15" 1104 | source = "registry+https://github.com/rust-lang/crates.io-index" 1105 | checksum = "915a1e146535de9163f3987b8944ed8cf49a18bb0056bcebcdcece385cece4ff" 1106 | 1107 | [[package]] 1108 | name = "pin-utils" 1109 | version = "0.1.0" 1110 | source = "registry+https://github.com/rust-lang/crates.io-index" 1111 | checksum = "8b870d8c151b6f2fb93e84a13146138f05d02ed11c7e7c54f8826aaaf7c9f184" 1112 | 1113 | [[package]] 1114 | name = "png" 1115 | version = "0.17.13" 1116 | source = "registry+https://github.com/rust-lang/crates.io-index" 1117 | checksum = "06e4b0d3d1312775e782c86c91a111aa1f910cbb65e1337f9975b5f9a554b5e1" 1118 | dependencies = [ 1119 | "bitflags 1.3.2", 1120 | "crc32fast", 1121 | "fdeflate", 1122 | "flate2", 1123 | "miniz_oxide", 1124 | ] 1125 | 1126 | [[package]] 1127 | name = "powerfmt" 1128 | version = "0.2.0" 1129 | source = "registry+https://github.com/rust-lang/crates.io-index" 1130 | checksum = "439ee305def115ba05938db6eb1644ff94165c5ab5e9420d1c1bcedbba909391" 1131 | 1132 | [[package]] 1133 | name = "proc-macro2" 1134 | version = "1.0.92" 1135 | source = "registry+https://github.com/rust-lang/crates.io-index" 1136 | checksum = "37d3544b3f2748c54e147655edb5025752e2303145b5aefb3c3ea2c78b973bb0" 1137 | dependencies = [ 1138 | "unicode-ident", 1139 | ] 1140 | 1141 | [[package]] 1142 | name = "promkit-core" 1143 | version = "0.1.0" 1144 | source = "registry+https://github.com/rust-lang/crates.io-index" 1145 | checksum = "afdf9ca2b84218bffc62938a4755eb2c84c3015ed1eb98f2a4833701c9301a52" 1146 | dependencies = [ 1147 | "anyhow", 1148 | "crossterm", 1149 | "unicode-width", 1150 | ] 1151 | 1152 | [[package]] 1153 | name = "promkit-widgets" 1154 | version = "0.1.0" 1155 | source = "registry+https://github.com/rust-lang/crates.io-index" 1156 | checksum = "528fb688a78893954d5b37e9b98ab98b669f9ec997ce6dc285e850b92e4ad257" 1157 | dependencies = [ 1158 | "anyhow", 1159 | "promkit-core", 1160 | "rayon", 1161 | "serde", 1162 | "serde_json", 1163 | ] 1164 | 1165 | [[package]] 1166 | name = "quote" 1167 | version = "1.0.40" 1168 | source = "registry+https://github.com/rust-lang/crates.io-index" 1169 | checksum = "1885c039570dc00dcb4ff087a89e185fd56bae234ddc7f056a945bf36467248d" 1170 | dependencies = [ 1171 | "proc-macro2", 1172 | ] 1173 | 1174 | [[package]] 1175 | name = "rayon" 1176 | version = "1.10.0" 1177 | source = "registry+https://github.com/rust-lang/crates.io-index" 1178 | checksum = "b418a60154510ca1a002a752ca9714984e21e4241e804d32555251faf8b78ffa" 1179 | dependencies = [ 1180 | "either", 1181 | "rayon-core", 1182 | ] 1183 | 1184 | [[package]] 1185 | name = "rayon-core" 1186 | version = "1.12.1" 1187 | source = "registry+https://github.com/rust-lang/crates.io-index" 1188 | checksum = "1465873a3dfdaa8ae7cb14b4383657caab0b3e8a0aa9ae8e04b044854c8dfce2" 1189 | dependencies = [ 1190 | "crossbeam-deque", 1191 | "crossbeam-utils", 1192 | ] 1193 | 1194 | [[package]] 1195 | name = "redox_syscall" 1196 | version = "0.4.1" 1197 | source = "registry+https://github.com/rust-lang/crates.io-index" 1198 | checksum = "4722d768eff46b75989dd134e5c353f0d6296e5aaa3132e776cbdb56be7731aa" 1199 | dependencies = [ 1200 | "bitflags 1.3.2", 1201 | ] 1202 | 1203 | [[package]] 1204 | name = "redox_users" 1205 | version = "0.5.0" 1206 | source = "registry+https://github.com/rust-lang/crates.io-index" 1207 | checksum = "dd6f9d3d47bdd2ad6945c5015a226ec6155d0bcdfd8f7cd29f86b71f8de99d2b" 1208 | dependencies = [ 1209 | "getrandom", 1210 | "libredox", 1211 | "thiserror 2.0.12", 1212 | ] 1213 | 1214 | [[package]] 1215 | name = "regex" 1216 | version = "1.10.4" 1217 | source = "registry+https://github.com/rust-lang/crates.io-index" 1218 | checksum = "c117dbdfde9c8308975b6a18d71f3f385c89461f7b3fb054288ecf2a2058ba4c" 1219 | dependencies = [ 1220 | "aho-corasick", 1221 | "memchr", 1222 | "regex-automata", 1223 | "regex-syntax", 1224 | ] 1225 | 1226 | [[package]] 1227 | name = "regex-automata" 1228 | version = "0.4.6" 1229 | source = "registry+https://github.com/rust-lang/crates.io-index" 1230 | checksum = "86b83b8b9847f9bf95ef68afb0b8e6cdb80f498442f5179a29fad448fcc1eaea" 1231 | dependencies = [ 1232 | "aho-corasick", 1233 | "memchr", 1234 | "regex-syntax", 1235 | ] 1236 | 1237 | [[package]] 1238 | name = "regex-syntax" 1239 | version = "0.8.3" 1240 | source = "registry+https://github.com/rust-lang/crates.io-index" 1241 | checksum = "adad44e29e4c806119491a7f06f03de4d1af22c3a680dd47f1e6e179439d1f56" 1242 | 1243 | [[package]] 1244 | name = "rustc-demangle" 1245 | version = "0.1.24" 1246 | source = "registry+https://github.com/rust-lang/crates.io-index" 1247 | checksum = "719b953e2095829ee67db738b3bfa9fa368c94900df327b3f07fe6e794d2fe1f" 1248 | 1249 | [[package]] 1250 | name = "rustix" 1251 | version = "0.38.34" 1252 | source = "registry+https://github.com/rust-lang/crates.io-index" 1253 | checksum = "70dc5ec042f7a43c4a73241207cecc9873a06d45debb38b329f8541d85c2730f" 1254 | dependencies = [ 1255 | "bitflags 2.5.0", 1256 | "errno", 1257 | "libc", 1258 | "linux-raw-sys", 1259 | "windows-sys 0.52.0", 1260 | ] 1261 | 1262 | [[package]] 1263 | name = "ryu" 1264 | version = "1.0.17" 1265 | source = "registry+https://github.com/rust-lang/crates.io-index" 1266 | checksum = "e86697c916019a8588c99b5fac3cead74ec0b4b819707a682fd4d23fa0ce1ba1" 1267 | 1268 | [[package]] 1269 | name = "scopeguard" 1270 | version = "1.2.0" 1271 | source = "registry+https://github.com/rust-lang/crates.io-index" 1272 | checksum = "94143f37725109f92c262ed2cf5e59bce7498c01bcc1502d7b9afe439a4e9f49" 1273 | 1274 | [[package]] 1275 | name = "serde" 1276 | version = "1.0.219" 1277 | source = "registry+https://github.com/rust-lang/crates.io-index" 1278 | checksum = "5f0e2c6ed6606019b4e29e69dbaba95b11854410e5347d525002456dbbb786b6" 1279 | dependencies = [ 1280 | "serde_derive", 1281 | ] 1282 | 1283 | [[package]] 1284 | name = "serde_derive" 1285 | version = "1.0.219" 1286 | source = "registry+https://github.com/rust-lang/crates.io-index" 1287 | checksum = "5b0276cf7f2c73365f7157c8123c21cd9a50fbbd844757af28ca1f5925fc2a00" 1288 | dependencies = [ 1289 | "proc-macro2", 1290 | "quote", 1291 | "syn", 1292 | ] 1293 | 1294 | [[package]] 1295 | name = "serde_json" 1296 | version = "1.0.140" 1297 | source = "registry+https://github.com/rust-lang/crates.io-index" 1298 | checksum = "20068b6e96dc6c9bd23e01df8827e6c7e1f2fddd43c21810382803c136b99373" 1299 | dependencies = [ 1300 | "indexmap", 1301 | "itoa", 1302 | "memchr", 1303 | "ryu", 1304 | "serde", 1305 | ] 1306 | 1307 | [[package]] 1308 | name = "serde_spanned" 1309 | version = "0.6.8" 1310 | source = "registry+https://github.com/rust-lang/crates.io-index" 1311 | checksum = "87607cb1398ed59d48732e575a4c28a7a8ebf2454b964fe3f224f2afc07909e1" 1312 | dependencies = [ 1313 | "serde", 1314 | ] 1315 | 1316 | [[package]] 1317 | name = "shlex" 1318 | version = "1.3.0" 1319 | source = "registry+https://github.com/rust-lang/crates.io-index" 1320 | checksum = "0fda2ff0d084019ba4d7c6f371c95d8fd75ce3524c3cb8fb653a3023f6323e64" 1321 | 1322 | [[package]] 1323 | name = "signal-hook" 1324 | version = "0.3.17" 1325 | source = "registry+https://github.com/rust-lang/crates.io-index" 1326 | checksum = "8621587d4798caf8eb44879d42e56b9a93ea5dcd315a6487c357130095b62801" 1327 | dependencies = [ 1328 | "libc", 1329 | "signal-hook-registry", 1330 | ] 1331 | 1332 | [[package]] 1333 | name = "signal-hook-mio" 1334 | version = "0.2.4" 1335 | source = "registry+https://github.com/rust-lang/crates.io-index" 1336 | checksum = "34db1a06d485c9142248b7a054f034b349b212551f3dfd19c94d45a754a217cd" 1337 | dependencies = [ 1338 | "libc", 1339 | "mio", 1340 | "signal-hook", 1341 | ] 1342 | 1343 | [[package]] 1344 | name = "signal-hook-registry" 1345 | version = "1.4.2" 1346 | source = "registry+https://github.com/rust-lang/crates.io-index" 1347 | checksum = "a9e9e0b4211b72e7b8b6e85c807d36c212bdb33ea8587f7569562a84df5465b1" 1348 | dependencies = [ 1349 | "libc", 1350 | ] 1351 | 1352 | [[package]] 1353 | name = "simd-adler32" 1354 | version = "0.3.7" 1355 | source = "registry+https://github.com/rust-lang/crates.io-index" 1356 | checksum = "d66dc143e6b11c1eddc06d5c423cfc97062865baf299914ab64caa38182078fe" 1357 | 1358 | [[package]] 1359 | name = "slab" 1360 | version = "0.4.9" 1361 | source = "registry+https://github.com/rust-lang/crates.io-index" 1362 | checksum = "8f92a496fb766b417c996b9c5e57daf2f7ad3b0bebe1ccfca4856390e3d3bb67" 1363 | dependencies = [ 1364 | "autocfg", 1365 | ] 1366 | 1367 | [[package]] 1368 | name = "smallvec" 1369 | version = "1.13.2" 1370 | source = "registry+https://github.com/rust-lang/crates.io-index" 1371 | checksum = "3c5e1a9a646d36c3599cd173a41282daf47c44583ad367b8e6837255952e5c67" 1372 | 1373 | [[package]] 1374 | name = "socket2" 1375 | version = "0.5.8" 1376 | source = "registry+https://github.com/rust-lang/crates.io-index" 1377 | checksum = "c970269d99b64e60ec3bd6ad27270092a5394c4e309314b18ae3fe575695fbe8" 1378 | dependencies = [ 1379 | "libc", 1380 | "windows-sys 0.52.0", 1381 | ] 1382 | 1383 | [[package]] 1384 | name = "strsim" 1385 | version = "0.11.1" 1386 | source = "registry+https://github.com/rust-lang/crates.io-index" 1387 | checksum = "7da8b5736845d9f2fcb837ea5d9e2628564b3b043a70948a3f0b778838c5fb4f" 1388 | 1389 | [[package]] 1390 | name = "syn" 1391 | version = "2.0.95" 1392 | source = "registry+https://github.com/rust-lang/crates.io-index" 1393 | checksum = "46f71c0377baf4ef1cc3e3402ded576dccc315800fbc62dfc7fe04b009773b4a" 1394 | dependencies = [ 1395 | "proc-macro2", 1396 | "quote", 1397 | "unicode-ident", 1398 | ] 1399 | 1400 | [[package]] 1401 | name = "thiserror" 1402 | version = "1.0.64" 1403 | source = "registry+https://github.com/rust-lang/crates.io-index" 1404 | checksum = "d50af8abc119fb8bb6dbabcfa89656f46f84aa0ac7688088608076ad2b459a84" 1405 | dependencies = [ 1406 | "thiserror-impl 1.0.64", 1407 | ] 1408 | 1409 | [[package]] 1410 | name = "thiserror" 1411 | version = "2.0.12" 1412 | source = "registry+https://github.com/rust-lang/crates.io-index" 1413 | checksum = "567b8a2dae586314f7be2a752ec7474332959c6460e02bde30d702a66d488708" 1414 | dependencies = [ 1415 | "thiserror-impl 2.0.12", 1416 | ] 1417 | 1418 | [[package]] 1419 | name = "thiserror-impl" 1420 | version = "1.0.64" 1421 | source = "registry+https://github.com/rust-lang/crates.io-index" 1422 | checksum = "08904e7672f5eb876eaaf87e0ce17857500934f4981c4a0ab2b4aa98baac7fc3" 1423 | dependencies = [ 1424 | "proc-macro2", 1425 | "quote", 1426 | "syn", 1427 | ] 1428 | 1429 | [[package]] 1430 | name = "thiserror-impl" 1431 | version = "2.0.12" 1432 | source = "registry+https://github.com/rust-lang/crates.io-index" 1433 | checksum = "7f7cf42b4507d8ea322120659672cf1b9dbb93f8f2d4ecfd6e51350ff5b17a1d" 1434 | dependencies = [ 1435 | "proc-macro2", 1436 | "quote", 1437 | "syn", 1438 | ] 1439 | 1440 | [[package]] 1441 | name = "tiff" 1442 | version = "0.9.1" 1443 | source = "registry+https://github.com/rust-lang/crates.io-index" 1444 | checksum = "ba1310fcea54c6a9a4fd1aad794ecc02c31682f6bfbecdf460bf19533eed1e3e" 1445 | dependencies = [ 1446 | "flate2", 1447 | "jpeg-decoder", 1448 | "weezl", 1449 | ] 1450 | 1451 | [[package]] 1452 | name = "time" 1453 | version = "0.3.36" 1454 | source = "registry+https://github.com/rust-lang/crates.io-index" 1455 | checksum = "5dfd88e563464686c916c7e46e623e520ddc6d79fa6641390f2e3fa86e83e885" 1456 | dependencies = [ 1457 | "deranged", 1458 | "itoa", 1459 | "num-conv", 1460 | "powerfmt", 1461 | "serde", 1462 | "time-core", 1463 | "time-macros", 1464 | ] 1465 | 1466 | [[package]] 1467 | name = "time-core" 1468 | version = "0.1.2" 1469 | source = "registry+https://github.com/rust-lang/crates.io-index" 1470 | checksum = "ef927ca75afb808a4d64dd374f00a2adf8d0fcff8e7b184af886c3c87ec4a3f3" 1471 | 1472 | [[package]] 1473 | name = "time-macros" 1474 | version = "0.2.18" 1475 | source = "registry+https://github.com/rust-lang/crates.io-index" 1476 | checksum = "3f252a68540fde3a3877aeea552b832b40ab9a69e318efd078774a01ddee1ccf" 1477 | dependencies = [ 1478 | "num-conv", 1479 | "time-core", 1480 | ] 1481 | 1482 | [[package]] 1483 | name = "tokio" 1484 | version = "1.44.1" 1485 | source = "registry+https://github.com/rust-lang/crates.io-index" 1486 | checksum = "f382da615b842244d4b8738c82ed1275e6c5dd90c459a30941cd07080b06c91a" 1487 | dependencies = [ 1488 | "backtrace", 1489 | "bytes", 1490 | "libc", 1491 | "mio", 1492 | "parking_lot", 1493 | "pin-project-lite", 1494 | "signal-hook-registry", 1495 | "socket2", 1496 | "tokio-macros", 1497 | "windows-sys 0.52.0", 1498 | ] 1499 | 1500 | [[package]] 1501 | name = "tokio-macros" 1502 | version = "2.5.0" 1503 | source = "registry+https://github.com/rust-lang/crates.io-index" 1504 | checksum = "6e06d43f1345a3bcd39f6a56dbb7dcab2ba47e68e8ac134855e7e2bdbaf8cab8" 1505 | dependencies = [ 1506 | "proc-macro2", 1507 | "quote", 1508 | "syn", 1509 | ] 1510 | 1511 | [[package]] 1512 | name = "tokio-stream" 1513 | version = "0.1.17" 1514 | source = "registry+https://github.com/rust-lang/crates.io-index" 1515 | checksum = "eca58d7bba4a75707817a2c44174253f9236b2d5fbd055602e9d5c07c139a047" 1516 | dependencies = [ 1517 | "futures-core", 1518 | "pin-project-lite", 1519 | "tokio", 1520 | ] 1521 | 1522 | [[package]] 1523 | name = "toml" 1524 | version = "0.8.20" 1525 | source = "registry+https://github.com/rust-lang/crates.io-index" 1526 | checksum = "cd87a5cdd6ffab733b2f74bc4fd7ee5fff6634124999ac278c35fc78c6120148" 1527 | dependencies = [ 1528 | "serde", 1529 | "serde_spanned", 1530 | "toml_datetime", 1531 | "toml_edit", 1532 | ] 1533 | 1534 | [[package]] 1535 | name = "toml_datetime" 1536 | version = "0.6.8" 1537 | source = "registry+https://github.com/rust-lang/crates.io-index" 1538 | checksum = "0dd7358ecb8fc2f8d014bf86f6f638ce72ba252a2c3a2572f2a795f1d23efb41" 1539 | dependencies = [ 1540 | "serde", 1541 | ] 1542 | 1543 | [[package]] 1544 | name = "toml_edit" 1545 | version = "0.22.24" 1546 | source = "registry+https://github.com/rust-lang/crates.io-index" 1547 | checksum = "17b4795ff5edd201c7cd6dca065ae59972ce77d1b80fa0a84d94950ece7d1474" 1548 | dependencies = [ 1549 | "indexmap", 1550 | "serde", 1551 | "serde_spanned", 1552 | "toml_datetime", 1553 | "winnow", 1554 | ] 1555 | 1556 | [[package]] 1557 | name = "unicode-ident" 1558 | version = "1.0.12" 1559 | source = "registry+https://github.com/rust-lang/crates.io-index" 1560 | checksum = "3354b9ac3fae1ff6755cb6db53683adb661634f67557942dea4facebec0fee4b" 1561 | 1562 | [[package]] 1563 | name = "unicode-width" 1564 | version = "0.2.0" 1565 | source = "registry+https://github.com/rust-lang/crates.io-index" 1566 | checksum = "1fc81956842c57dac11422a97c3b8195a1ff727f06e85c84ed2e8aa277c9a0fd" 1567 | 1568 | [[package]] 1569 | name = "urlencoding" 1570 | version = "2.1.3" 1571 | source = "registry+https://github.com/rust-lang/crates.io-index" 1572 | checksum = "daf8dba3b7eb870caf1ddeed7bc9d2a049f3cfdfae7cb521b087cc33ae4c49da" 1573 | 1574 | [[package]] 1575 | name = "utf8parse" 1576 | version = "0.2.1" 1577 | source = "registry+https://github.com/rust-lang/crates.io-index" 1578 | checksum = "711b9620af191e0cdc7468a8d14e709c3dcdb115b36f838e601583af800a370a" 1579 | 1580 | [[package]] 1581 | name = "version_check" 1582 | version = "0.9.4" 1583 | source = "registry+https://github.com/rust-lang/crates.io-index" 1584 | checksum = "49874b5167b65d7193b8aba1567f5c7d93d001cafc34600cee003eda787e483f" 1585 | 1586 | [[package]] 1587 | name = "wasi" 1588 | version = "0.11.0+wasi-snapshot-preview1" 1589 | source = "registry+https://github.com/rust-lang/crates.io-index" 1590 | checksum = "9c8d87e72b64a3b4db28d11ce29237c246188f4f51057d65a7eab63b7987e423" 1591 | 1592 | [[package]] 1593 | name = "weezl" 1594 | version = "0.1.8" 1595 | source = "registry+https://github.com/rust-lang/crates.io-index" 1596 | checksum = "53a85b86a771b1c87058196170769dd264f66c0782acf1ae6cc51bfd64b39082" 1597 | 1598 | [[package]] 1599 | name = "winapi" 1600 | version = "0.3.9" 1601 | source = "registry+https://github.com/rust-lang/crates.io-index" 1602 | checksum = "5c839a674fcd7a98952e593242ea400abe93992746761e38641405d28b00f419" 1603 | dependencies = [ 1604 | "winapi-i686-pc-windows-gnu", 1605 | "winapi-x86_64-pc-windows-gnu", 1606 | ] 1607 | 1608 | [[package]] 1609 | name = "winapi-i686-pc-windows-gnu" 1610 | version = "0.4.0" 1611 | source = "registry+https://github.com/rust-lang/crates.io-index" 1612 | checksum = "ac3b87c63620426dd9b991e5ce0329eff545bccbbb34f3be09ff6fb6ab51b7b6" 1613 | 1614 | [[package]] 1615 | name = "winapi-x86_64-pc-windows-gnu" 1616 | version = "0.4.0" 1617 | source = "registry+https://github.com/rust-lang/crates.io-index" 1618 | checksum = "712e227841d057c1ee1cd2fb22fa7e5a5461ae8e48fa2ca79ec42cfc1931183f" 1619 | 1620 | [[package]] 1621 | name = "windows-sys" 1622 | version = "0.48.0" 1623 | source = "registry+https://github.com/rust-lang/crates.io-index" 1624 | checksum = "677d2418bec65e3338edb076e806bc1ec15693c5d0104683f2efe857f61056a9" 1625 | dependencies = [ 1626 | "windows-targets 0.48.5", 1627 | ] 1628 | 1629 | [[package]] 1630 | name = "windows-sys" 1631 | version = "0.52.0" 1632 | source = "registry+https://github.com/rust-lang/crates.io-index" 1633 | checksum = "282be5f36a8ce781fad8c8ae18fa3f9beff57ec1b52cb3de0789201425d9a33d" 1634 | dependencies = [ 1635 | "windows-targets 0.52.6", 1636 | ] 1637 | 1638 | [[package]] 1639 | name = "windows-sys" 1640 | version = "0.59.0" 1641 | source = "registry+https://github.com/rust-lang/crates.io-index" 1642 | checksum = "1e38bc4d79ed67fd075bcc251a1c39b32a1776bbe92e5bef1f0bf1f8c531853b" 1643 | dependencies = [ 1644 | "windows-targets 0.52.6", 1645 | ] 1646 | 1647 | [[package]] 1648 | name = "windows-targets" 1649 | version = "0.48.5" 1650 | source = "registry+https://github.com/rust-lang/crates.io-index" 1651 | checksum = "9a2fa6e2155d7247be68c096456083145c183cbbbc2764150dda45a87197940c" 1652 | dependencies = [ 1653 | "windows_aarch64_gnullvm 0.48.5", 1654 | "windows_aarch64_msvc 0.48.5", 1655 | "windows_i686_gnu 0.48.5", 1656 | "windows_i686_msvc 0.48.5", 1657 | "windows_x86_64_gnu 0.48.5", 1658 | "windows_x86_64_gnullvm 0.48.5", 1659 | "windows_x86_64_msvc 0.48.5", 1660 | ] 1661 | 1662 | [[package]] 1663 | name = "windows-targets" 1664 | version = "0.52.6" 1665 | source = "registry+https://github.com/rust-lang/crates.io-index" 1666 | checksum = "9b724f72796e036ab90c1021d4780d4d3d648aca59e491e6b98e725b84e99973" 1667 | dependencies = [ 1668 | "windows_aarch64_gnullvm 0.52.6", 1669 | "windows_aarch64_msvc 0.52.6", 1670 | "windows_i686_gnu 0.52.6", 1671 | "windows_i686_gnullvm", 1672 | "windows_i686_msvc 0.52.6", 1673 | "windows_x86_64_gnu 0.52.6", 1674 | "windows_x86_64_gnullvm 0.52.6", 1675 | "windows_x86_64_msvc 0.52.6", 1676 | ] 1677 | 1678 | [[package]] 1679 | name = "windows_aarch64_gnullvm" 1680 | version = "0.48.5" 1681 | source = "registry+https://github.com/rust-lang/crates.io-index" 1682 | checksum = "2b38e32f0abccf9987a4e3079dfb67dcd799fb61361e53e2882c3cbaf0d905d8" 1683 | 1684 | [[package]] 1685 | name = "windows_aarch64_gnullvm" 1686 | version = "0.52.6" 1687 | source = "registry+https://github.com/rust-lang/crates.io-index" 1688 | checksum = "32a4622180e7a0ec044bb555404c800bc9fd9ec262ec147edd5989ccd0c02cd3" 1689 | 1690 | [[package]] 1691 | name = "windows_aarch64_msvc" 1692 | version = "0.48.5" 1693 | source = "registry+https://github.com/rust-lang/crates.io-index" 1694 | checksum = "dc35310971f3b2dbbf3f0690a219f40e2d9afcf64f9ab7cc1be722937c26b4bc" 1695 | 1696 | [[package]] 1697 | name = "windows_aarch64_msvc" 1698 | version = "0.52.6" 1699 | source = "registry+https://github.com/rust-lang/crates.io-index" 1700 | checksum = "09ec2a7bb152e2252b53fa7803150007879548bc709c039df7627cabbd05d469" 1701 | 1702 | [[package]] 1703 | name = "windows_i686_gnu" 1704 | version = "0.48.5" 1705 | source = "registry+https://github.com/rust-lang/crates.io-index" 1706 | checksum = "a75915e7def60c94dcef72200b9a8e58e5091744960da64ec734a6c6e9b3743e" 1707 | 1708 | [[package]] 1709 | name = "windows_i686_gnu" 1710 | version = "0.52.6" 1711 | source = "registry+https://github.com/rust-lang/crates.io-index" 1712 | checksum = "8e9b5ad5ab802e97eb8e295ac6720e509ee4c243f69d781394014ebfe8bbfa0b" 1713 | 1714 | [[package]] 1715 | name = "windows_i686_gnullvm" 1716 | version = "0.52.6" 1717 | source = "registry+https://github.com/rust-lang/crates.io-index" 1718 | checksum = "0eee52d38c090b3caa76c563b86c3a4bd71ef1a819287c19d586d7334ae8ed66" 1719 | 1720 | [[package]] 1721 | name = "windows_i686_msvc" 1722 | version = "0.48.5" 1723 | source = "registry+https://github.com/rust-lang/crates.io-index" 1724 | checksum = "8f55c233f70c4b27f66c523580f78f1004e8b5a8b659e05a4eb49d4166cca406" 1725 | 1726 | [[package]] 1727 | name = "windows_i686_msvc" 1728 | version = "0.52.6" 1729 | source = "registry+https://github.com/rust-lang/crates.io-index" 1730 | checksum = "240948bc05c5e7c6dabba28bf89d89ffce3e303022809e73deaefe4f6ec56c66" 1731 | 1732 | [[package]] 1733 | name = "windows_x86_64_gnu" 1734 | version = "0.48.5" 1735 | source = "registry+https://github.com/rust-lang/crates.io-index" 1736 | checksum = "53d40abd2583d23e4718fddf1ebec84dbff8381c07cae67ff7768bbf19c6718e" 1737 | 1738 | [[package]] 1739 | name = "windows_x86_64_gnu" 1740 | version = "0.52.6" 1741 | source = "registry+https://github.com/rust-lang/crates.io-index" 1742 | checksum = "147a5c80aabfbf0c7d901cb5895d1de30ef2907eb21fbbab29ca94c5b08b1a78" 1743 | 1744 | [[package]] 1745 | name = "windows_x86_64_gnullvm" 1746 | version = "0.48.5" 1747 | source = "registry+https://github.com/rust-lang/crates.io-index" 1748 | checksum = "0b7b52767868a23d5bab768e390dc5f5c55825b6d30b86c844ff2dc7414044cc" 1749 | 1750 | [[package]] 1751 | name = "windows_x86_64_gnullvm" 1752 | version = "0.52.6" 1753 | source = "registry+https://github.com/rust-lang/crates.io-index" 1754 | checksum = "24d5b23dc417412679681396f2b49f3de8c1473deb516bd34410872eff51ed0d" 1755 | 1756 | [[package]] 1757 | name = "windows_x86_64_msvc" 1758 | version = "0.48.5" 1759 | source = "registry+https://github.com/rust-lang/crates.io-index" 1760 | checksum = "ed94fce61571a4006852b7389a063ab983c02eb1bb37b47f8272ce92d06d9538" 1761 | 1762 | [[package]] 1763 | name = "windows_x86_64_msvc" 1764 | version = "0.52.6" 1765 | source = "registry+https://github.com/rust-lang/crates.io-index" 1766 | checksum = "589f6da84c646204747d1270a2a5661ea66ed1cced2631d546fdfb155959f9ec" 1767 | 1768 | [[package]] 1769 | name = "winnow" 1770 | version = "0.7.4" 1771 | source = "registry+https://github.com/rust-lang/crates.io-index" 1772 | checksum = "0e97b544156e9bebe1a0ffbc03484fc1ffe3100cbce3ffb17eac35f7cdd7ab36" 1773 | dependencies = [ 1774 | "memchr", 1775 | ] 1776 | 1777 | [[package]] 1778 | name = "x11rb" 1779 | version = "0.13.1" 1780 | source = "registry+https://github.com/rust-lang/crates.io-index" 1781 | checksum = "5d91ffca73ee7f68ce055750bf9f6eca0780b8c85eff9bc046a3b0da41755e12" 1782 | dependencies = [ 1783 | "gethostname", 1784 | "rustix", 1785 | "x11rb-protocol", 1786 | ] 1787 | 1788 | [[package]] 1789 | name = "x11rb-protocol" 1790 | version = "0.13.1" 1791 | source = "registry+https://github.com/rust-lang/crates.io-index" 1792 | checksum = "ec107c4503ea0b4a98ef47356329af139c0a4f7750e621cf2973cd3385ebcb3d" 1793 | 1794 | [[package]] 1795 | name = "zerocopy" 1796 | version = "0.7.32" 1797 | source = "registry+https://github.com/rust-lang/crates.io-index" 1798 | checksum = "74d4d3961e53fa4c9a25a8637fc2bfaf2595b3d3ae34875568a5cf64787716be" 1799 | dependencies = [ 1800 | "zerocopy-derive", 1801 | ] 1802 | 1803 | [[package]] 1804 | name = "zerocopy-derive" 1805 | version = "0.7.32" 1806 | source = "registry+https://github.com/rust-lang/crates.io-index" 1807 | checksum = "9ce1b18ccd8e73a9321186f97e46f9f04b778851177567b1975109d26a08d2a6" 1808 | dependencies = [ 1809 | "proc-macro2", 1810 | "quote", 1811 | "syn", 1812 | ] 1813 | -------------------------------------------------------------------------------- /Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "jnv" 3 | version = "0.6.0" 4 | authors = ["ynqa "] 5 | edition = "2021" 6 | description = "JSON navigator and interactive filter leveraging jq" 7 | repository = "https://github.com/ynqa/jnv" 8 | license = "MIT" 9 | readme = "README.md" 10 | 11 | [dependencies] 12 | anyhow = "1.0.97" 13 | arboard = "3.4.1" 14 | async-trait = "0.1.88" 15 | clap = { version = "4.5.34", features = ["derive"] } 16 | duration-string = { version = "0.5.2", features = ["serde"] } 17 | derive_builder = "0.20.2" 18 | dirs = "6.0.0" 19 | futures = "0.3.30" 20 | jaq-core = "1.2.1" 21 | jaq-interpret = "1.2.1" 22 | jaq-parse = "1.0.2" 23 | jaq-std = "1.2.1" 24 | promkit-core = "0.1.0" 25 | promkit-widgets = { version = "0.1.0", features = ["jsonstream", "listbox", "text", "texteditor"] } 26 | serde = "1.0.219" 27 | tokio = { version = "1.44.1", features = ["full"] } 28 | tokio-stream = "0.1.16" 29 | toml = "0.8.20" 30 | 31 | # The profile that 'cargo dist' will build with 32 | [profile.dist] 33 | inherits = "release" 34 | lto = "thin" 35 | -------------------------------------------------------------------------------- /Dockerfile: -------------------------------------------------------------------------------- 1 | # Build stage 2 | FROM rust:1.86.0-slim-bookworm as builder 3 | 4 | WORKDIR /jnv 5 | COPY . /jnv 6 | RUN cargo build --release 7 | 8 | # Final stage 9 | FROM debian:bookworm-slim 10 | 11 | COPY --from=builder /jnv/target/release/jnv /bin/jnv 12 | 13 | ENTRYPOINT ["/bin/jnv"] 14 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | 3 | Copyright (c) 2024 jnv authors 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy of 6 | this software and associated documentation files (the "Software"), to deal in 7 | the Software without restriction, including without limitation the rights to 8 | use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of 9 | the Software, and to permit persons to whom the Software is furnished to do so, 10 | subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS 17 | FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR 18 | COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER 19 | IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN 20 | CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. 21 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | 2 | 3 | Text describing the image 4 | 5 | 6 | [![ci](https://github.com/ynqa/jnv/actions/workflows/ci.yml/badge.svg?branch=main)](https://github.com/ynqa/jnv/actions/workflows/ci.yml) 7 | 8 | *jnv* is designed for navigating JSON, 9 | offering an interactive JSON viewer and `jq` filter editor. 10 | 11 | ![jnv.gif](https://github.com/ynqa/ynqa/blob/master/demo/jnv.gif) 12 | 13 | Inspired by [jid](https://github.com/simeji/jid) 14 | and [jiq](https://github.com/fiatjaf/jiq). 15 | 16 | ## Features 17 | 18 | - Interactive JSON viewer and `jq` filter editor 19 | - Syntax highlighting for JSON 20 | - Use [jaq](https://github.com/01mf02/jaq) to apply `jq` filter 21 | - This eliminates the need for users to prepare `jq` on their own 22 | - Configurable features via TOML configuration 23 | - Toggle hint message display 24 | - Adjust UI reactivity (debounce times and animation speed) 25 | - Editor appearance and behavior 26 | - JSON viewer styling 27 | - Adjust completion feature display and behavior 28 | - Keybinds 29 | - Capable of accommodating various format 30 | - Input: File, stdin 31 | - Data: A JSON or multiple JSON structures 32 | that can be deserialized with 33 | [StreamDeserializer](https://docs.rs/serde_json/latest/serde_json/struct.StreamDeserializer.html), 34 | such as [JSON Lines](https://jsonlines.org/) 35 | - Auto-completion for the filter 36 | - Only supports: 37 | - [Identity](https://jqlang.github.io/jq/manual/#identity) 38 | - [Object Identifier-Index](https://jqlang.github.io/jq/manual/#object-identifier-index) 39 | - [Array Index](https://jqlang.github.io/jq/manual/#array-index) 40 | - Hint message to evaluate the filter 41 | 42 | ## Installation 43 | 44 | [![Packaging status](https://repology.org/badge/vertical-allrepos/jnv.svg)](https://repology.org/project/jnv/versions) 45 | 46 | ### Homebrew 47 | 48 | See [here](https://formulae.brew.sh/formula/jnv) for more info. 49 | 50 | ```bash 51 | brew install jnv 52 | ``` 53 | 54 | Or install via Homebrew Tap: 55 | 56 | ```bash 57 | brew install ynqa/tap/jnv 58 | ``` 59 | 60 | ### MacPorts 61 | 62 | See [here](https://ports.macports.org/port/jnv/) for more info. 63 | 64 | ```bash 65 | sudo port install jnv 66 | ``` 67 | 68 | ### Nix / NixOS 69 | 70 | See [package entry on search.nixos.org](https://search.nixos.org/packages?channel=unstable&query=jnv) for more info. 71 | 72 | ```bash 73 | nix-shell -p jnv 74 | ``` 75 | 76 | ### conda-forge 77 | 78 | See [here](https://prefix.dev/channels/conda-forge/packages/jnv) for more info. 79 | 80 | ```bash 81 | pixi global install jnv 82 | # or 83 | cat data.json | pixi exec jnv 84 | # or 85 | conda install jnv 86 | ``` 87 | 88 | ### Docker 89 | 90 | Build 91 | (In the near future, the image will be available on something of registries) 92 | 93 | ```bash 94 | docker build -t jnv . 95 | ``` 96 | 97 | And Run 98 | (The following commad is just an example. Please modify the path to the file you want to mount) 99 | 100 | ```bash 101 | docker run -it --rm -v $(pwd)/debug.json:/jnv/debug.json jnv /jnv/debug.json 102 | ``` 103 | 104 | ### Cargo 105 | 106 | ```bash 107 | cargo install jnv 108 | ``` 109 | 110 | ## Examples 111 | 112 | ```bash 113 | cat data.json | jnv 114 | # or 115 | jnv data.json 116 | ``` 117 | 118 | ## Keymap 119 | 120 | | Key | Action | 121 | | :- | :- | 122 | | Ctrl + C | Exit | 123 | | Ctrl + Q | Copy jq filter to clipboard | 124 | | Ctrl + O | Copy JSON to clipboard | 125 | | Shift + ↑, Shift + ↓ | Switch to another mode | 126 | 127 | ### Editor mode (default) 128 | 129 | | Key | Action | 130 | | :- | :- | 131 | | Tab | Enter suggestion | 132 | | | Move cursor left | 133 | | | Move cursor right | 134 | | Ctrl + A | Move cursor to line start | 135 | | Ctrl + E | Move cursor to line end | 136 | | Backspace | Delete character before cursor | 137 | | Ctrl + U | Clear entire line | 138 | | Alt + B | Move the cursor to the previous nearest character within set(`.`,`\|`,`(`,`)`,`[`,`]`) | 139 | | Alt + F | Move the cursor to the next nearest character within set(`.`,`\|`,`(`,`)`,`[`,`]`) | 140 | | Ctrl + W | Erase to the previous nearest character within set(`.`,`\|`,`(`,`)`,`[`,`]`) | 141 | | Alt + D | Erase to the next nearest character within set(`.`,`\|`,`(`,`)`,`[`,`]`) | 142 | 143 | #### Suggestion in Editor (after Tab) 144 | 145 | | Key | Action | 146 | | :- | :- | 147 | | Tab, | Select next suggestion | 148 | | | Select previous suggestion | 149 | | Others | Return to editor | 150 | 151 | ### JSON viewer mode 152 | 153 | | Key | Action | 154 | | :- | :- | 155 | | , Ctrl + K | Move up | 156 | | , Ctrl + J | Move down | 157 | | Ctrl + H | Move to last entry | 158 | | Ctrl + L | Move to first entry | 159 | | Enter | Toggle fold | 160 | | Ctrl + P | Expand all | 161 | | Ctrl + N | Collapse all | 162 | 163 | ## Usage 164 | 165 | ```bash 166 | JSON navigator and interactive filter leveraging jq 167 | 168 | Usage: jnv [OPTIONS] [INPUT] 169 | 170 | Examples: 171 | - Read from a file: 172 | jnv data.json 173 | 174 | - Read from standard input: 175 | cat data.json | jnv 176 | 177 | Arguments: 178 | [INPUT] Optional path to a JSON file. If not provided or if "-" is specified, reads from standard input 179 | 180 | Options: 181 | -c, --config Path to the configuration file. 182 | --default-filter Default jq filter to apply to the input data 183 | -h, --help Print help (see more with '--help') 184 | -V, --version Print version 185 | ``` 186 | 187 | ## Configuration 188 | 189 | jnv uses a TOML format configuration file to customize various features. 190 | The configuration file is loaded in the following order of priority: 191 | 192 | 1. Path specified on the command line (`-c` or `--config` option) 193 | 2. Default configuration file path 194 | 195 | ### Default Configuration File Location 196 | 197 | Following the `dirs` crate, 198 | the default configuration file location for each platform is as follows: 199 | 200 | - **Linux**: `~/.config/jnv/config.toml` 201 | - **macOS**: `~/Library/Application Support/jnv/config.toml` 202 | - **Windows**: `C:\Users\{Username}\AppData\Roaming\jnv\config.toml` 203 | 204 | If the configuration file does not exist, 205 | it will be automatically created on first run. 206 | 207 | ### Configuration Options 208 | 209 | The following settings are available in `config.toml`: 210 | 211 | ```toml 212 | # Whether to hide the hint message 213 | no_hint = false 214 | 215 | # Editor settings 216 | [editor] 217 | # Editor mode ("Insert" or "Overwrite") 218 | mode = "Insert" 219 | # Word break characters 220 | word_break_chars = [".", "|", "(", ")", "[", "]"] 221 | 222 | # Theme when editor is focused 223 | [editor.theme_on_focus] 224 | prefix = "❯❯ " 225 | prefix_style = { foreground = "blue" } 226 | active_char_style = { background = "magenta" } 227 | inactive_char_style = {} 228 | 229 | # Theme when editor is not focused 230 | [editor.theme_on_defocus] 231 | prefix = "▼ " 232 | prefix_style = { foreground = "blue", attributes = ["Dim"] } 233 | active_char_style = { attributes = ["Dim"] } 234 | inactive_char_style = { attributes = ["Dim"] } 235 | 236 | # JSON display settings 237 | [json] 238 | # Maximum number of JSON objects to read from stream 239 | # max_streams = 240 | 241 | # JSON theme settings 242 | [json.theme] 243 | indent = 2 244 | curly_brackets_style = { attributes = ["Bold"] } 245 | square_brackets_style = { attributes = ["Bold"] } 246 | key_style = { foreground = "cyan" } 247 | string_value_style = { foreground = "green" } 248 | number_value_style = {} 249 | boolean_value_style = {} 250 | null_value_style = { foreground = "grey" } 251 | 252 | # Completion feature settings 253 | [completion] 254 | lines = 3 255 | cursor = "❯ " 256 | active_item_style = { foreground = "grey", background = "yellow" } 257 | inactive_item_style = { foreground = "grey" } 258 | search_result_chunk_size = 100 259 | search_load_chunk_size = 50000 260 | 261 | # Keybind settings 262 | [keybinds] 263 | # Application exit key 264 | exit = [{ Key = { modifiers = "CONTROL", code = { Char = "c" } } }] 265 | # Copy query to clipboard key 266 | copy_query = [{ Key = { modifiers = "CONTROL", code = { Char = "q" } } }] 267 | # Copy result to clipboard key 268 | copy_result = [{ Key = { modifiers = "CONTROL", code = { Char = "o" } } }] 269 | # Mode switch keys 270 | switch_mode = [ 271 | { Key = { code = "Down", modifiers = "SHIFT" } }, 272 | { Key = { code = "Up", modifiers = "SHIFT" } } 273 | ] 274 | 275 | # Editor operation keybinds 276 | [keybinds.on_editor] 277 | # (Details omitted) 278 | 279 | # JSON viewer keybinds 280 | [keybinds.on_json_viewer] 281 | # (Details omitted) 282 | 283 | # Application reactivity settings 284 | [reactivity_control] 285 | # Delay time after query input 286 | query_debounce_duration = "600ms" 287 | # Redraw delay time after window resize 288 | resize_debounce_duration = "200ms" 289 | # Spinner animation update interval 290 | spin_duration = "300ms" 291 | ``` 292 | 293 | For more details on configuration, please refer to [default.toml](./default.toml) 294 | 295 | > [!WARNING] 296 | > Depending on the type of terminal and environment, 297 | > characters and styles may not be displayed properly. 298 | > Specific key bindings and decorative characters may not 299 | > display or function correctly in certain terminal emulators. 300 | 301 | ## Stargazers over time 302 | [![Stargazers over time](https://starchart.cc/ynqa/jnv.svg?variant=adaptive)](https://starchart.cc/ynqa/jnv) 303 | -------------------------------------------------------------------------------- /default.toml: -------------------------------------------------------------------------------- 1 | # Whether to hide hint messages 2 | no_hint = false 3 | 4 | # Editor settings 5 | [editor] 6 | # Editor mode 7 | # "Insert": Insert characters at the cursor position 8 | # "Overwrite": Replace characters at the cursor position with new ones 9 | mode = "Insert" 10 | 11 | # Characters considered as word boundaries 12 | # These are used to define word movement and deletion behavior in the editor 13 | word_break_chars = [".", "|", "(", ")", "[", "]"] 14 | 15 | # How to configure colors and text attributes 16 | # 17 | # Color specification methods: 18 | # 1. By name: "black", "red", etc. 19 | # 2. By RGB value: "rgb_(255,0,0)" or "#ff0000" 20 | # 3. By ANSI value: "ansi_(16)" 21 | # 22 | # Text attribute specification: 23 | # attributes = ["Bold"], etc. 24 | # 25 | # Configuration example: 26 | # style = { foreground = "blue", background = "magenta", attributes = ["Bold"] } 27 | # 28 | # Detailed information: 29 | # - Color: https://docs.rs/crossterm/0.28.1/crossterm/style/enum.Color.html 30 | # - Attribute: https://docs.rs/crossterm/0.28.1/crossterm/style/enum.Attribute.html 31 | 32 | # Theme settings when the editor is focused 33 | [editor.theme_on_focus] 34 | # Prefix shown before the cursor 35 | prefix = "❯❯ " 36 | # Style for the prefix 37 | prefix_style = { foreground = "blue" } 38 | # Style for the character under the cursor 39 | active_char_style = { background = "magenta" } 40 | # Style for all other characters 41 | inactive_char_style = {} 42 | 43 | # Theme settings when the editor is unfocused 44 | [editor.theme_on_defocus] 45 | # Prefix shown when focus is lost 46 | prefix = "▼ " 47 | # Style for the prefix when unfocused 48 | prefix_style = { foreground = "blue", attributes = ["Dim"] } 49 | # Style for the character under the cursor when unfocused 50 | active_char_style = { attributes = ["Dim"] } 51 | # Style for all other characters when unfocused 52 | inactive_char_style = { attributes = ["Dim"] } 53 | 54 | # JSON display settings 55 | [json] 56 | # Maximum number of JSON objects to read from streams (e.g., JSON Lines format) 57 | # Limits how many objects are processed to reduce memory usage when handling large data streams 58 | # No limit if unset 59 | # max_streams = 60 | 61 | # JSON display theme 62 | [json.theme] 63 | # Number of spaces to use for indentation 64 | indent = 2 65 | # Style for curly brackets {} 66 | curly_brackets_style = { attributes = ["Bold"] } 67 | # Style for square brackets [] 68 | square_brackets_style = { attributes = ["Bold"] } 69 | # Style for JSON keys 70 | key_style = { foreground = "cyan" } 71 | # Style for string values 72 | string_value_style = { foreground = "green" } 73 | # Style for number values 74 | number_value_style = {} 75 | # Style for boolean values 76 | boolean_value_style = {} 77 | # Style for null values 78 | null_value_style = { foreground = "grey" } 79 | 80 | # Completion feature settings 81 | [completion] 82 | # Number of lines to display for completion candidates 83 | lines = 3 84 | # Cursor character shown before the selected candidate 85 | cursor = "❯ " 86 | # Style for the selected candidate 87 | active_item_style = { foreground = "grey", background = "yellow" } 88 | # Style for unselected candidates 89 | inactive_item_style = { foreground = "grey" } 90 | 91 | # Settings for background loading of completion candidates 92 | # 93 | # Number of candidates loaded per chunk for search results 94 | # A larger value displays results faster but uses more memory 95 | search_result_chunk_size = 100 96 | 97 | # Number of items loaded per batch during background loading 98 | # A larger value finishes loading sooner but uses more memory temporarily 99 | search_load_chunk_size = 50000 100 | 101 | # Keybinding settings 102 | [keybinds] 103 | # Key to exit the application 104 | exit = [ 105 | { Key = { modifiers = "CONTROL", code = { Char = "c" } } } 106 | ] 107 | # Key to copy the query to the clipboard 108 | copy_query = [ 109 | { Key = { modifiers = "CONTROL", code = { Char = "q" } } } 110 | ] 111 | # Key to copy the result to the clipboard 112 | copy_result = [ 113 | { Key = { modifiers = "CONTROL", code = { Char = "o" } } } 114 | ] 115 | # Keys to switch focus between editor and JSON viewer 116 | switch_mode = [ 117 | { Key = { code = "Down", modifiers = "SHIFT" } }, 118 | { Key = { code = "Up", modifiers = "SHIFT" } } 119 | ] 120 | 121 | # Keybindings for editor operations 122 | [keybinds.on_editor] 123 | # Move cursor left 124 | backward = [ 125 | { Key = { code = "Left", modifiers = "" } } 126 | ] 127 | # Move cursor right 128 | forward = [ 129 | { Key = { code = "Right", modifiers = "" } } 130 | ] 131 | # Move cursor to beginning of line 132 | move_to_head = [ 133 | { Key = { modifiers = "CONTROL", code = { Char = "a" } } } 134 | ] 135 | # Move cursor to end of line 136 | move_to_tail = [ 137 | { Key = { modifiers = "CONTROL", code = { Char = "e" } } } 138 | ] 139 | # Move cursor to previous word boundary 140 | move_to_previous_nearest = [ 141 | { Key = { modifiers = "ALT", code = { Char = "b" } } } 142 | ] 143 | # Move cursor to next word boundary 144 | move_to_next_nearest = [ 145 | { Key = { modifiers = "ALT", code = { Char = "f" } } } 146 | ] 147 | # Delete character at the cursor 148 | erase = [ 149 | { Key = { code = "Backspace", modifiers = "" } } 150 | ] 151 | # Delete all input 152 | erase_all = [ 153 | { Key = { modifiers = "CONTROL", code = { Char = "u" } } } 154 | ] 155 | # Delete from cursor to previous word boundary 156 | erase_to_previous_nearest = [ 157 | { Key = { modifiers = "CONTROL", code = { Char = "w" } } } 158 | ] 159 | # Delete from cursor to next word boundary 160 | erase_to_next_nearest = [ 161 | { Key = { modifiers = "ALT", code = { Char = "d" } } } 162 | ] 163 | # Trigger completion 164 | completion = [ 165 | { Key = { code = "Tab", modifiers = "" } } 166 | ] 167 | # Move up in the completion list 168 | on_completion.up = [ 169 | { Key = { code = "Up", modifiers = "" } } 170 | ] 171 | # Move down in the completion list 172 | on_completion.down = [ 173 | { Key = { code = "Down", modifiers = "" } }, 174 | { Key = { code = "Tab", modifiers = "" } } 175 | ] 176 | 177 | # Keybindings for JSON viewer operations 178 | [keybinds.on_json_viewer] 179 | # Move up in JSON viewer 180 | up = [ 181 | { Key = { code = "Up", modifiers = "" } }, 182 | { Key = { modifiers = "CONTROL", code = { Char = "k" } } } 183 | ] 184 | # Move down in JSON viewer 185 | down = [ 186 | { Key = { modifiers = "CONTROL", code = { Char = "j" } } }, 187 | { Key = { code = "Down", modifiers = "" } } 188 | ] 189 | # Move to the top of JSON viewer 190 | move_to_head = [ 191 | { Key = { modifiers = "CONTROL", code = { Char = "l" } } } 192 | ] 193 | # Move to the bottom of JSON viewer 194 | move_to_tail = [ 195 | { Key = { modifiers = "CONTROL", code = { Char = "h" } } } 196 | ] 197 | # Toggle expand/collapse of JSON nodes 198 | toggle = [ 199 | { Key = { code = "Enter", modifiers = "" } } 200 | ] 201 | # Expand all JSON nodes 202 | expand = [ 203 | { Key = { modifiers = "CONTROL", code = { Char = "p" } } } 204 | ] 205 | # Collapse all JSON nodes 206 | collapse = [ 207 | { Key = { modifiers = "CONTROL", code = { Char = "n" } } } 208 | ] 209 | 210 | # Application reactivity settings 211 | [reactivity_control] 212 | # Delay before processing query input 213 | # Prevents excessive updates while user is typing 214 | query_debounce_duration = "600ms" 215 | 216 | # Delay before redrawing after window resize 217 | # Prevents frequent redraws during continuous resizing 218 | resize_debounce_duration = "200ms" 219 | 220 | # Interval for spinner animation updates 221 | # Controls the speed of the loading spinner 222 | spin_duration = "300ms" 223 | -------------------------------------------------------------------------------- /dist-workspace.toml: -------------------------------------------------------------------------------- 1 | [workspace] 2 | members = ["cargo:."] 3 | 4 | # Config for 'dist' 5 | [dist] 6 | # The preferred dist version to use in CI (Cargo.toml SemVer syntax) 7 | cargo-dist-version = "0.28.0" 8 | # CI backends to support 9 | ci = "github" 10 | # The installers to generate for each app 11 | installers = ["shell", "homebrew"] 12 | # A GitHub repo to push Homebrew formulas to 13 | tap = "ynqa/homebrew-tap" 14 | # Target platforms to build apps for (Rust target-triple syntax) 15 | targets = ["aarch64-apple-darwin", "aarch64-unknown-linux-gnu", "armv7-unknown-linux-gnueabihf", "x86_64-apple-darwin", "x86_64-unknown-linux-gnu", "x86_64-unknown-linux-musl", "x86_64-pc-windows-msvc"] 16 | # Publish jobs to run in CI 17 | publish-jobs = ["homebrew"] 18 | # Which actions to run on pull requests 19 | pr-run-mode = "plan" 20 | # Path that installers should place binaries in 21 | install-path = "CARGO_HOME" 22 | # Whether to install an updater program 23 | install-updater = false 24 | -------------------------------------------------------------------------------- /src/config.rs: -------------------------------------------------------------------------------- 1 | use std::collections::HashSet; 2 | 3 | use promkit_core::crossterm::{ 4 | event::{KeyCode, KeyModifiers}, 5 | style::{Attribute, Attributes, Color, ContentStyle}, 6 | }; 7 | use promkit_widgets::text_editor::Mode; 8 | use serde::{Deserialize, Serialize}; 9 | use tokio::time::Duration; 10 | 11 | mod content_style; 12 | use content_style::content_style_serde; 13 | mod duration; 14 | use duration::duration_serde; 15 | pub mod event; 16 | use event::{EventDef, EventDefSet, KeyEventDef}; 17 | mod text_editor; 18 | use text_editor::text_editor_mode_serde; 19 | 20 | #[derive(Serialize, Deserialize)] 21 | pub struct EditorConfig { 22 | pub theme_on_focus: EditorTheme, 23 | pub theme_on_defocus: EditorTheme, 24 | #[serde(with = "text_editor_mode_serde")] 25 | pub mode: Mode, 26 | pub word_break_chars: HashSet, 27 | } 28 | 29 | #[derive(Serialize, Deserialize)] 30 | pub struct EditorTheme { 31 | pub prefix: String, 32 | 33 | #[serde(with = "content_style_serde")] 34 | pub prefix_style: ContentStyle, 35 | 36 | #[serde(with = "content_style_serde")] 37 | pub active_char_style: ContentStyle, 38 | 39 | #[serde(with = "content_style_serde")] 40 | pub inactive_char_style: ContentStyle, 41 | } 42 | 43 | impl Default for EditorConfig { 44 | fn default() -> Self { 45 | Self { 46 | theme_on_focus: EditorTheme { 47 | prefix: String::from("❯❯ "), 48 | prefix_style: ContentStyle { 49 | foreground_color: Some(Color::Blue), 50 | ..Default::default() 51 | }, 52 | active_char_style: ContentStyle { 53 | background_color: Some(Color::Magenta), 54 | ..Default::default() 55 | }, 56 | inactive_char_style: ContentStyle::default(), 57 | }, 58 | theme_on_defocus: EditorTheme { 59 | prefix: String::from("▼ "), 60 | prefix_style: ContentStyle { 61 | foreground_color: Some(Color::Blue), 62 | attributes: Attributes::from(Attribute::Dim), 63 | ..Default::default() 64 | }, 65 | active_char_style: ContentStyle { 66 | attributes: Attributes::from(Attribute::Dim), 67 | ..Default::default() 68 | }, 69 | inactive_char_style: ContentStyle { 70 | attributes: Attributes::from(Attribute::Dim), 71 | ..Default::default() 72 | }, 73 | }, 74 | mode: Mode::Insert, 75 | word_break_chars: HashSet::from(['.', '|', '(', ')', '[', ']']), 76 | } 77 | } 78 | } 79 | 80 | #[derive(Serialize, Deserialize)] 81 | pub struct JsonConfig { 82 | pub max_streams: Option, 83 | pub theme: JsonTheme, 84 | } 85 | 86 | #[derive(Serialize, Deserialize)] 87 | pub struct JsonTheme { 88 | pub indent: usize, 89 | 90 | #[serde(with = "content_style_serde")] 91 | pub curly_brackets_style: ContentStyle, 92 | 93 | #[serde(with = "content_style_serde")] 94 | pub square_brackets_style: ContentStyle, 95 | 96 | #[serde(with = "content_style_serde")] 97 | pub key_style: ContentStyle, 98 | 99 | #[serde(with = "content_style_serde")] 100 | pub string_value_style: ContentStyle, 101 | 102 | #[serde(with = "content_style_serde")] 103 | pub number_value_style: ContentStyle, 104 | 105 | #[serde(with = "content_style_serde")] 106 | pub boolean_value_style: ContentStyle, 107 | 108 | #[serde(with = "content_style_serde")] 109 | pub null_value_style: ContentStyle, 110 | } 111 | 112 | impl Default for JsonConfig { 113 | fn default() -> Self { 114 | Self { 115 | max_streams: None, 116 | theme: JsonTheme { 117 | indent: 2, 118 | curly_brackets_style: ContentStyle { 119 | attributes: Attributes::from(Attribute::Bold), 120 | ..Default::default() 121 | }, 122 | square_brackets_style: ContentStyle { 123 | attributes: Attributes::from(Attribute::Bold), 124 | ..Default::default() 125 | }, 126 | key_style: ContentStyle { 127 | foreground_color: Some(Color::Cyan), 128 | ..Default::default() 129 | }, 130 | string_value_style: ContentStyle { 131 | foreground_color: Some(Color::Green), 132 | ..Default::default() 133 | }, 134 | number_value_style: ContentStyle::default(), 135 | boolean_value_style: ContentStyle::default(), 136 | null_value_style: ContentStyle { 137 | foreground_color: Some(Color::Grey), 138 | ..Default::default() 139 | }, 140 | }, 141 | } 142 | } 143 | } 144 | 145 | #[derive(Serialize, Deserialize)] 146 | pub struct CompletionConfig { 147 | pub lines: Option, 148 | pub cursor: String, 149 | pub search_result_chunk_size: usize, 150 | pub search_load_chunk_size: usize, 151 | 152 | #[serde(with = "content_style_serde")] 153 | pub active_item_style: ContentStyle, 154 | 155 | #[serde(with = "content_style_serde")] 156 | pub inactive_item_style: ContentStyle, 157 | } 158 | 159 | impl Default for CompletionConfig { 160 | fn default() -> Self { 161 | Self { 162 | lines: Some(3), 163 | cursor: String::from("❯ "), 164 | search_result_chunk_size: 100, 165 | search_load_chunk_size: 50000, 166 | active_item_style: ContentStyle { 167 | foreground_color: Some(Color::Grey), 168 | background_color: Some(Color::Yellow), 169 | ..Default::default() 170 | }, 171 | inactive_item_style: ContentStyle { 172 | foreground_color: Some(Color::Grey), 173 | ..Default::default() 174 | }, 175 | } 176 | } 177 | } 178 | 179 | // TODO: remove Clone derive 180 | #[derive(Clone, Serialize, Deserialize)] 181 | pub struct Keybinds { 182 | pub exit: EventDefSet, 183 | pub copy_query: EventDefSet, 184 | pub copy_result: EventDefSet, 185 | pub switch_mode: EventDefSet, 186 | pub on_editor: EditorKeybinds, 187 | pub on_json_viewer: JsonViewerKeybinds, 188 | } 189 | 190 | #[derive(Clone, Serialize, Deserialize)] 191 | pub struct EditorKeybinds { 192 | pub backward: EventDefSet, 193 | pub forward: EventDefSet, 194 | pub move_to_head: EventDefSet, 195 | pub move_to_tail: EventDefSet, 196 | pub move_to_previous_nearest: EventDefSet, 197 | pub move_to_next_nearest: EventDefSet, 198 | pub erase: EventDefSet, 199 | pub erase_all: EventDefSet, 200 | pub erase_to_previous_nearest: EventDefSet, 201 | pub erase_to_next_nearest: EventDefSet, 202 | pub completion: EventDefSet, 203 | pub on_completion: CompletionKeybinds, 204 | } 205 | 206 | #[derive(Clone, Serialize, Deserialize)] 207 | pub struct CompletionKeybinds { 208 | pub up: EventDefSet, 209 | pub down: EventDefSet, 210 | } 211 | 212 | #[derive(Clone, Serialize, Deserialize)] 213 | pub struct JsonViewerKeybinds { 214 | pub up: EventDefSet, 215 | pub down: EventDefSet, 216 | pub move_to_head: EventDefSet, 217 | pub move_to_tail: EventDefSet, 218 | pub toggle: EventDefSet, 219 | pub expand: EventDefSet, 220 | pub collapse: EventDefSet, 221 | } 222 | 223 | impl Default for Keybinds { 224 | fn default() -> Self { 225 | Self { 226 | exit: EventDefSet::from(KeyEventDef::new(KeyCode::Char('c'), KeyModifiers::CONTROL)), 227 | copy_query: EventDefSet::from(KeyEventDef::new( 228 | KeyCode::Char('q'), 229 | KeyModifiers::CONTROL, 230 | )), 231 | copy_result: EventDefSet::from(KeyEventDef::new( 232 | KeyCode::Char('o'), 233 | KeyModifiers::CONTROL, 234 | )), 235 | switch_mode: EventDefSet::from_iter([ 236 | EventDef::Key(KeyEventDef::new(KeyCode::Down, KeyModifiers::SHIFT)), 237 | EventDef::Key(KeyEventDef::new(KeyCode::Up, KeyModifiers::SHIFT)), 238 | ]), 239 | on_editor: EditorKeybinds { 240 | backward: EventDefSet::from(KeyEventDef::new(KeyCode::Left, KeyModifiers::NONE)), 241 | forward: EventDefSet::from(KeyEventDef::new(KeyCode::Right, KeyModifiers::NONE)), 242 | move_to_head: EventDefSet::from(KeyEventDef::new( 243 | KeyCode::Char('a'), 244 | KeyModifiers::CONTROL, 245 | )), 246 | move_to_tail: EventDefSet::from(KeyEventDef::new( 247 | KeyCode::Char('e'), 248 | KeyModifiers::CONTROL, 249 | )), 250 | move_to_next_nearest: EventDefSet::from(KeyEventDef::new( 251 | KeyCode::Char('f'), 252 | KeyModifiers::ALT, 253 | )), 254 | move_to_previous_nearest: EventDefSet::from(KeyEventDef::new( 255 | KeyCode::Char('b'), 256 | KeyModifiers::ALT, 257 | )), 258 | erase: EventDefSet::from(KeyEventDef::new(KeyCode::Backspace, KeyModifiers::NONE)), 259 | erase_all: EventDefSet::from(KeyEventDef::new( 260 | KeyCode::Char('u'), 261 | KeyModifiers::CONTROL, 262 | )), 263 | erase_to_previous_nearest: EventDefSet::from(KeyEventDef::new( 264 | KeyCode::Char('w'), 265 | KeyModifiers::CONTROL, 266 | )), 267 | erase_to_next_nearest: EventDefSet::from(KeyEventDef::new( 268 | KeyCode::Char('d'), 269 | KeyModifiers::ALT, 270 | )), 271 | completion: EventDefSet::from(KeyEventDef::new(KeyCode::Tab, KeyModifiers::NONE)), 272 | on_completion: CompletionKeybinds { 273 | up: EventDefSet::from(KeyEventDef::new(KeyCode::Up, KeyModifiers::NONE)), 274 | down: EventDefSet::from_iter([ 275 | EventDef::Key(KeyEventDef::new(KeyCode::Tab, KeyModifiers::NONE)), 276 | EventDef::Key(KeyEventDef::new(KeyCode::Down, KeyModifiers::NONE)), 277 | ]), 278 | }, 279 | }, 280 | on_json_viewer: JsonViewerKeybinds { 281 | up: EventDefSet::from_iter([ 282 | EventDef::Key(KeyEventDef::new(KeyCode::Char('k'), KeyModifiers::CONTROL)), 283 | EventDef::Key(KeyEventDef::new(KeyCode::Up, KeyModifiers::NONE)), 284 | ]), 285 | down: EventDefSet::from_iter([ 286 | EventDef::Key(KeyEventDef::new(KeyCode::Char('j'), KeyModifiers::CONTROL)), 287 | EventDef::Key(KeyEventDef::new(KeyCode::Down, KeyModifiers::NONE)), 288 | ]), 289 | move_to_head: EventDefSet::from(KeyEventDef::new( 290 | KeyCode::Char('l'), 291 | KeyModifiers::CONTROL, 292 | )), 293 | move_to_tail: EventDefSet::from(KeyEventDef::new( 294 | KeyCode::Char('h'), 295 | KeyModifiers::CONTROL, 296 | )), 297 | toggle: EventDefSet::from(KeyEventDef::new(KeyCode::Enter, KeyModifiers::NONE)), 298 | expand: EventDefSet::from(KeyEventDef::new( 299 | KeyCode::Char('p'), 300 | KeyModifiers::CONTROL, 301 | )), 302 | collapse: EventDefSet::from(KeyEventDef::new( 303 | KeyCode::Char('n'), 304 | KeyModifiers::CONTROL, 305 | )), 306 | }, 307 | } 308 | } 309 | } 310 | 311 | #[derive(Serialize, Deserialize)] 312 | pub struct ReactivityControl { 313 | #[serde(with = "duration_serde")] 314 | pub query_debounce_duration: Duration, 315 | 316 | #[serde(with = "duration_serde")] 317 | pub resize_debounce_duration: Duration, 318 | 319 | #[serde(with = "duration_serde")] 320 | pub spin_duration: Duration, 321 | } 322 | 323 | impl Default for ReactivityControl { 324 | fn default() -> Self { 325 | Self { 326 | query_debounce_duration: Duration::from_millis(600), 327 | resize_debounce_duration: Duration::from_millis(200), 328 | spin_duration: Duration::from_millis(300), 329 | } 330 | } 331 | } 332 | 333 | /// Note that the config struct and the `.toml` configuration file are 334 | /// managed separately because the current toml crate 335 | /// does not readily support the following features: 336 | /// 337 | /// - Preserve docstrings as comments in the `.toml` file 338 | /// - https://github.com/toml-rs/toml/issues/376 339 | /// - Output inline tables 340 | /// - https://github.com/toml-rs/toml/issues/592 341 | /// 342 | /// Also difficult to patch `Config` using only the items specified in the configuration file 343 | /// (Premise: To address the complexity of configurations, 344 | /// it assumes using a macro to avoid managing Option-wrapped structures on our side).s 345 | /// 346 | /// The main challenge is that, for nested structs, 347 | /// it is not able to wrap every leaf field with Option<>. 348 | /// https://github.com/colin-kiegel/rust-derive-builder/issues/254 349 | #[derive(Default, Serialize, Deserialize)] 350 | pub struct Config { 351 | pub no_hint: bool, 352 | pub reactivity_control: ReactivityControl, 353 | pub editor: EditorConfig, 354 | pub json: JsonConfig, 355 | pub completion: CompletionConfig, 356 | pub keybinds: Keybinds, 357 | } 358 | 359 | impl Config { 360 | pub fn load_from(content: &str) -> anyhow::Result { 361 | toml::from_str(content).map_err(Into::into) 362 | } 363 | } 364 | -------------------------------------------------------------------------------- /src/config/content_style.rs: -------------------------------------------------------------------------------- 1 | use promkit_core::crossterm::style::{Attribute, Attributes, Color, ContentStyle}; 2 | use serde::{Deserialize, Serialize}; 3 | 4 | #[derive(Serialize, Deserialize)] 5 | struct ContentStyleDef { 6 | foreground: Option, 7 | background: Option, 8 | underline: Option, 9 | attributes: Option>, 10 | } 11 | 12 | impl From<&ContentStyle> for ContentStyleDef { 13 | fn from(style: &ContentStyle) -> Self { 14 | ContentStyleDef { 15 | foreground: style.foreground_color, 16 | background: style.background_color, 17 | underline: style.underline_color, 18 | attributes: if style.attributes.is_empty() { 19 | None 20 | } else { 21 | Some( 22 | Attribute::iterator() 23 | .filter(|x| style.attributes.has(*x)) 24 | .collect(), 25 | ) 26 | }, 27 | } 28 | } 29 | } 30 | 31 | impl From for ContentStyle { 32 | fn from(style_def: ContentStyleDef) -> Self { 33 | let mut style = ContentStyle::new(); 34 | style.foreground_color = style_def.foreground; 35 | style.background_color = style_def.background; 36 | style.underline_color = style_def.underline; 37 | if let Some(attributes) = style_def.attributes { 38 | style.attributes = attributes 39 | .into_iter() 40 | .fold(Attributes::default(), |acc, x| acc | x); 41 | } 42 | style 43 | } 44 | } 45 | 46 | pub mod content_style_serde { 47 | use super::*; 48 | use serde::{Deserializer, Serializer}; 49 | 50 | pub fn serialize(style: &ContentStyle, serializer: S) -> Result 51 | where 52 | S: Serializer, 53 | { 54 | let style_def = ContentStyleDef::from(style); 55 | style_def.serialize(serializer) 56 | } 57 | 58 | pub fn deserialize<'de, D>(deserializer: D) -> Result 59 | where 60 | D: Deserializer<'de>, 61 | { 62 | let style_def = ContentStyleDef::deserialize(deserializer)?; 63 | Ok(ContentStyle::from(style_def)) 64 | } 65 | } 66 | -------------------------------------------------------------------------------- /src/config/duration.rs: -------------------------------------------------------------------------------- 1 | use duration_string::DurationString; 2 | use serde::Deserialize; 3 | use tokio::time::Duration; 4 | 5 | pub mod duration_serde { 6 | use super::*; 7 | use serde::{Deserializer, Serializer}; 8 | 9 | pub fn serialize(duration: &Duration, serializer: S) -> Result 10 | where 11 | S: Serializer, 12 | { 13 | serializer.serialize_str(&DurationString::from(*duration).to_string()) 14 | } 15 | 16 | pub fn deserialize<'de, D>(deserializer: D) -> Result 17 | where 18 | D: Deserializer<'de>, 19 | { 20 | Ok(DurationString::deserialize(deserializer)?.into()) 21 | } 22 | } 23 | -------------------------------------------------------------------------------- /src/config/event.rs: -------------------------------------------------------------------------------- 1 | use std::collections::HashSet; 2 | 3 | use promkit_core::crossterm::event::{ 4 | Event, KeyCode, KeyEvent, KeyModifiers, MouseEvent, MouseEventKind, 5 | }; 6 | use serde::{Deserialize, Serialize}; 7 | 8 | pub trait Matcher { 9 | fn matches(&self, other: &T) -> bool; 10 | } 11 | 12 | #[derive(Clone, Debug, Serialize, Deserialize, PartialEq)] 13 | pub struct EventDefSet(HashSet); 14 | 15 | impl Matcher for EventDefSet { 16 | fn matches(&self, other: &Event) -> bool { 17 | self.0.iter().any(|event_def| event_def.matches(other)) 18 | } 19 | } 20 | 21 | impl FromIterator for EventDefSet { 22 | fn from_iter>(iter: I) -> Self { 23 | EventDefSet(iter.into_iter().collect()) 24 | } 25 | } 26 | 27 | impl From for EventDefSet { 28 | fn from(key_event_def: KeyEventDef) -> Self { 29 | EventDefSet(HashSet::from_iter([EventDef::Key(key_event_def)])) 30 | } 31 | } 32 | 33 | impl From for EventDefSet { 34 | fn from(mouse_event_def: MouseEventDef) -> Self { 35 | EventDefSet(HashSet::from_iter([EventDef::Mouse(mouse_event_def)])) 36 | } 37 | } 38 | 39 | /// A part of `crossterm::event::Event`. 40 | /// It is used for parsing from a config file or 41 | /// for comparison with crossterm::event::Event. 42 | /// https://docs.rs/crossterm/0.28.1/crossterm/event/enum.Event.html 43 | #[derive(Clone, Debug, Serialize, Deserialize, PartialEq, Eq, Hash)] 44 | pub enum EventDef { 45 | Key(KeyEventDef), 46 | Mouse(MouseEventDef), 47 | } 48 | 49 | impl Matcher for EventDef { 50 | fn matches(&self, other: &Event) -> bool { 51 | match (self, other) { 52 | (EventDef::Key(key_def), Event::Key(key_event)) => key_def.matches(key_event), 53 | (EventDef::Mouse(mouse_def), Event::Mouse(mouse_event)) => { 54 | mouse_def.matches(mouse_event) 55 | } 56 | _ => false, 57 | } 58 | } 59 | } 60 | 61 | /// A part of `crossterm::event::KeyEvent`. 62 | /// It is used for parsing from a config file or 63 | /// for comparison with crossterm::event::KeyEvent. 64 | /// https://docs.rs/crossterm/0.28.1/crossterm/event/struct.KeyEvent.html 65 | #[derive(Clone, Debug, Serialize, Deserialize, PartialEq, Eq, Hash)] 66 | pub struct KeyEventDef { 67 | code: KeyCode, 68 | modifiers: KeyModifiers, 69 | } 70 | 71 | impl KeyEventDef { 72 | pub fn new(code: KeyCode, modifiers: KeyModifiers) -> Self { 73 | KeyEventDef { code, modifiers } 74 | } 75 | } 76 | 77 | impl Matcher for KeyEventDef { 78 | fn matches(&self, other: &KeyEvent) -> bool { 79 | self.code == other.code && self.modifiers == other.modifiers 80 | } 81 | } 82 | 83 | /// A part of `crossterm::event::MouseEvent`. 84 | /// It is used for parsing from a config file or 85 | /// for comparison with crossterm::event::MouseEvent. 86 | /// https://docs.rs/crossterm/0.28.1/crossterm/event/struct.MouseEvent.html 87 | #[derive(Clone, Debug, Serialize, Deserialize, PartialEq, Eq, Hash)] 88 | pub struct MouseEventDef { 89 | kind: MouseEventKind, 90 | modifiers: KeyModifiers, 91 | } 92 | 93 | impl MouseEventDef { 94 | #[allow(dead_code)] 95 | pub fn new(kind: MouseEventKind, modifiers: KeyModifiers) -> Self { 96 | MouseEventDef { kind, modifiers } 97 | } 98 | } 99 | 100 | impl Matcher for MouseEventDef { 101 | fn matches(&self, other: &MouseEvent) -> bool { 102 | self.kind == other.kind && self.modifiers == other.modifiers 103 | } 104 | } 105 | -------------------------------------------------------------------------------- /src/config/text_editor.rs: -------------------------------------------------------------------------------- 1 | use promkit_widgets::text_editor::Mode; 2 | use serde::{Deserialize, Deserializer, Serialize, Serializer}; 3 | 4 | pub mod text_editor_mode_serde { 5 | use super::*; 6 | 7 | pub fn serialize(mode: &Mode, serializer: S) -> Result 8 | where 9 | S: Serializer, 10 | { 11 | let mode_str = match mode { 12 | Mode::Insert => "Insert", 13 | Mode::Overwrite => "Overwrite", 14 | // Add other variants if they exist 15 | }; 16 | mode_str.serialize(serializer) 17 | } 18 | 19 | pub fn deserialize<'de, D>(deserializer: D) -> Result 20 | where 21 | D: Deserializer<'de>, 22 | { 23 | let mode_str = String::deserialize(deserializer)?; 24 | match mode_str.as_str() { 25 | "Insert" => Ok(Mode::Insert), 26 | "Overwrite" => Ok(Mode::Overwrite), 27 | // Add other variants if they exist 28 | _ => Err(serde::de::Error::custom(format!( 29 | "Unknown Mode variant: {}", 30 | mode_str 31 | ))), 32 | } 33 | } 34 | } 35 | -------------------------------------------------------------------------------- /src/editor.rs: -------------------------------------------------------------------------------- 1 | use std::{future::Future, pin::Pin}; 2 | 3 | use promkit_core::{ 4 | crossterm::{ 5 | event::{Event, KeyCode, KeyEvent, KeyEventKind, KeyEventState, KeyModifiers}, 6 | style::{Color, ContentStyle}, 7 | }, 8 | Pane, PaneFactory, 9 | }; 10 | use promkit_widgets::{ 11 | text::{self, Text}, 12 | text_editor, 13 | }; 14 | 15 | use crate::{ 16 | config::{event::Matcher, EditorKeybinds, EditorTheme}, 17 | search::IncrementalSearcher, 18 | }; 19 | 20 | pub struct Editor { 21 | handler: Handler, 22 | state: text_editor::State, 23 | focus_theme: EditorTheme, 24 | defocus_theme: EditorTheme, 25 | guide: text::State, 26 | searcher: IncrementalSearcher, 27 | editor_keybinds: EditorKeybinds, 28 | } 29 | 30 | impl Editor { 31 | pub fn new( 32 | state: text_editor::State, 33 | searcher: IncrementalSearcher, 34 | focus_theme: EditorTheme, 35 | defocus_theme: EditorTheme, 36 | editor_keybinds: EditorKeybinds, 37 | ) -> Self { 38 | Self { 39 | handler: BOXED_EDITOR_HANDLER, 40 | state, 41 | focus_theme, 42 | defocus_theme, 43 | guide: text::State::default(), 44 | searcher, 45 | editor_keybinds, 46 | } 47 | } 48 | 49 | pub fn focus(&mut self) { 50 | self.state.prefix = self.focus_theme.prefix.clone(); 51 | self.state.prefix_style = self.focus_theme.prefix_style; 52 | self.state.inactive_char_style = self.focus_theme.inactive_char_style; 53 | self.state.active_char_style = self.focus_theme.active_char_style; 54 | } 55 | 56 | pub fn defocus(&mut self) { 57 | self.state.prefix = self.defocus_theme.prefix.clone(); 58 | self.state.prefix_style = self.defocus_theme.prefix_style; 59 | self.state.inactive_char_style = self.defocus_theme.inactive_char_style; 60 | self.state.active_char_style = self.defocus_theme.active_char_style; 61 | 62 | self.searcher.leave_search(); 63 | self.handler = BOXED_EDITOR_HANDLER; 64 | 65 | self.guide.text = Default::default(); 66 | } 67 | 68 | pub fn text(&self) -> String { 69 | self.state.texteditor.text_without_cursor().to_string() 70 | } 71 | 72 | pub fn create_editor_pane(&self, width: u16, height: u16) -> Pane { 73 | self.state.create_pane(width, height) 74 | } 75 | 76 | pub fn create_searcher_pane(&self, width: u16, height: u16) -> Pane { 77 | self.searcher.create_pane(width, height) 78 | } 79 | 80 | pub fn create_guide_pane(&self, width: u16, height: u16) -> Pane { 81 | self.guide.create_pane(width, height) 82 | } 83 | 84 | pub async fn operate(&mut self, event: &Event) -> anyhow::Result<()> { 85 | (self.handler)(event, self).await 86 | } 87 | } 88 | 89 | pub type Handler = for<'a> fn( 90 | &'a Event, 91 | &'a mut Editor, 92 | ) -> Pin> + Send + 'a>>; 93 | 94 | const BOXED_EDITOR_HANDLER: Handler = 95 | |event, editor| -> Pin> + Send + '_>> { 96 | Box::pin(edit(event, editor)) 97 | }; 98 | const BOXED_SEARCHER_HANDLER: Handler = 99 | |event, editor| -> Pin> + Send + '_>> { 100 | Box::pin(search(event, editor)) 101 | }; 102 | 103 | pub async fn edit<'a>(event: &'a Event, editor: &'a mut Editor) -> anyhow::Result<()> { 104 | editor.guide.text = Default::default(); 105 | 106 | match event { 107 | key if editor.editor_keybinds.completion.matches(key) => { 108 | let prefix = editor.state.texteditor.text_without_cursor().to_string(); 109 | match editor.searcher.start_search(&prefix) { 110 | Ok(result) => match result.head_item { 111 | Some(head) => { 112 | if result.load_state.loaded { 113 | editor.guide.text = Text::from(format!( 114 | "Loaded all ({}) suggestions", 115 | result.load_state.loaded_item_len 116 | )); 117 | editor.guide.style = ContentStyle { 118 | foreground_color: Some(Color::Green), 119 | ..Default::default() 120 | }; 121 | } else { 122 | editor.guide.text = Text::from(format!( 123 | "Loaded partially ({}) suggestions", 124 | result.load_state.loaded_item_len 125 | )); 126 | editor.guide.style = ContentStyle { 127 | foreground_color: Some(Color::Green), 128 | ..Default::default() 129 | }; 130 | } 131 | editor.state.texteditor.replace(&head); 132 | editor.handler = BOXED_SEARCHER_HANDLER; 133 | } 134 | None => { 135 | editor.guide.text = 136 | Text::from(format!("No suggestion found for '{}'", prefix)); 137 | editor.guide.style = ContentStyle { 138 | foreground_color: Some(Color::Yellow), 139 | ..Default::default() 140 | }; 141 | } 142 | }, 143 | Err(e) => { 144 | editor.guide.text = Text::from(format!("Failed to lookup suggestions: {}", e)); 145 | editor.guide.style = ContentStyle { 146 | foreground_color: Some(Color::Yellow), 147 | ..Default::default() 148 | }; 149 | } 150 | } 151 | } 152 | 153 | // Move cursor. 154 | key if editor.editor_keybinds.backward.matches(key) => { 155 | editor.state.texteditor.backward(); 156 | } 157 | key if editor.editor_keybinds.forward.matches(key) => { 158 | editor.state.texteditor.forward(); 159 | } 160 | key if editor.editor_keybinds.move_to_head.matches(key) => { 161 | editor.state.texteditor.move_to_head(); 162 | } 163 | key if editor.editor_keybinds.move_to_tail.matches(key) => { 164 | editor.state.texteditor.move_to_tail(); 165 | } 166 | 167 | // Move cursor to the nearest character. 168 | key if editor.editor_keybinds.move_to_previous_nearest.matches(key) => { 169 | editor 170 | .state 171 | .texteditor 172 | .move_to_previous_nearest(&editor.state.word_break_chars); 173 | } 174 | key if editor.editor_keybinds.move_to_next_nearest.matches(key) => { 175 | editor 176 | .state 177 | .texteditor 178 | .move_to_next_nearest(&editor.state.word_break_chars); 179 | } 180 | 181 | // Erase char(s). 182 | key if editor.editor_keybinds.erase.matches(key) => { 183 | editor.state.texteditor.erase(); 184 | } 185 | key if editor.editor_keybinds.erase_all.matches(key) => { 186 | editor.state.texteditor.erase_all(); 187 | } 188 | 189 | // Erase to the nearest character. 190 | key if editor 191 | .editor_keybinds 192 | .erase_to_previous_nearest 193 | .matches(key) => 194 | { 195 | editor 196 | .state 197 | .texteditor 198 | .erase_to_previous_nearest(&editor.state.word_break_chars); 199 | } 200 | key if editor.editor_keybinds.erase_to_next_nearest.matches(key) => { 201 | editor 202 | .state 203 | .texteditor 204 | .erase_to_next_nearest(&editor.state.word_break_chars); 205 | } 206 | 207 | // Input char. 208 | Event::Key(KeyEvent { 209 | code: KeyCode::Char(ch), 210 | modifiers: KeyModifiers::NONE, 211 | kind: KeyEventKind::Press, 212 | state: KeyEventState::NONE, 213 | }) 214 | | Event::Key(KeyEvent { 215 | code: KeyCode::Char(ch), 216 | modifiers: KeyModifiers::SHIFT, 217 | kind: KeyEventKind::Press, 218 | state: KeyEventState::NONE, 219 | }) => match editor.state.edit_mode { 220 | text_editor::Mode::Insert => editor.state.texteditor.insert(*ch), 221 | text_editor::Mode::Overwrite => editor.state.texteditor.overwrite(*ch), 222 | }, 223 | 224 | _ => {} 225 | } 226 | Ok(()) 227 | } 228 | 229 | pub async fn search<'a>(event: &'a Event, editor: &'a mut Editor) -> anyhow::Result<()> { 230 | match event { 231 | key if editor.editor_keybinds.on_completion.down.matches(key) => { 232 | editor.searcher.down_with_load(); 233 | editor 234 | .state 235 | .texteditor 236 | .replace(&editor.searcher.get_current_item()); 237 | } 238 | 239 | key if editor.editor_keybinds.on_completion.up.matches(key) => { 240 | editor.searcher.up(); 241 | editor 242 | .state 243 | .texteditor 244 | .replace(&editor.searcher.get_current_item()); 245 | } 246 | 247 | _ => { 248 | editor.searcher.leave_search(); 249 | editor.handler = BOXED_EDITOR_HANDLER; 250 | return edit(event, editor).await; 251 | } 252 | } 253 | 254 | Ok(()) 255 | } 256 | -------------------------------------------------------------------------------- /src/json.rs: -------------------------------------------------------------------------------- 1 | use jaq_interpret::{Ctx, FilterT, ParseCtx, RcIter, Val}; 2 | use promkit_core::{ 3 | crossterm::{ 4 | event::Event, 5 | style::{Attribute, Attributes, Color, ContentStyle}, 6 | }, 7 | pane::Pane, 8 | PaneFactory, 9 | }; 10 | 11 | use promkit_widgets::{ 12 | jsonstream::{self, format::RowFormatter, jsonz, JsonStream}, 13 | serde_json::{self, Deserializer, Value}, 14 | text::{self, Text}, 15 | }; 16 | 17 | use crate::{ 18 | config::{event::Matcher, JsonViewerKeybinds}, 19 | processor::{ViewProvider, Visualizer}, 20 | search::SearchProvider, 21 | }; 22 | 23 | // #[derive(Clone)] 24 | pub struct Json { 25 | state: jsonstream::State, 26 | json: &'static [serde_json::Value], 27 | keybinds: JsonViewerKeybinds, 28 | } 29 | 30 | impl Json { 31 | pub fn new( 32 | formatter: RowFormatter, 33 | input_stream: &'static [serde_json::Value], 34 | keybinds: JsonViewerKeybinds, 35 | ) -> anyhow::Result { 36 | Ok(Self { 37 | json: input_stream, 38 | state: jsonstream::State { 39 | stream: JsonStream::new(input_stream.iter()), 40 | formatter, 41 | lines: Default::default(), 42 | }, 43 | keybinds, 44 | }) 45 | } 46 | 47 | fn operate(&mut self, event: &Event) { 48 | match event { 49 | // Move up. 50 | event if self.keybinds.up.matches(event) => { 51 | self.state.stream.up(); 52 | } 53 | 54 | // Move down. 55 | event if self.keybinds.down.matches(event) => { 56 | self.state.stream.down(); 57 | } 58 | 59 | // Move to head 60 | event if self.keybinds.move_to_head.matches(event) => { 61 | self.state.stream.head(); 62 | } 63 | 64 | // Move to tail 65 | event if self.keybinds.move_to_tail.matches(event) => { 66 | self.state.stream.tail(); 67 | } 68 | 69 | // Toggle collapse/expand 70 | event if self.keybinds.toggle.matches(event) => { 71 | self.state.stream.toggle(); 72 | } 73 | 74 | event if self.keybinds.expand.matches(event) => { 75 | self.state.stream.set_nodes_visibility(false); 76 | } 77 | 78 | event if self.keybinds.collapse.matches(event) => { 79 | self.state.stream.set_nodes_visibility(true); 80 | } 81 | 82 | _ => (), 83 | } 84 | } 85 | } 86 | 87 | #[async_trait::async_trait] 88 | impl Visualizer for Json { 89 | async fn content_to_copy(&self) -> String { 90 | self.state 91 | .formatter 92 | .format_raw_json(self.state.stream.rows()) 93 | } 94 | 95 | async fn create_init_pane(&mut self, area: (u16, u16)) -> Pane { 96 | self.state.create_pane(area.0, area.1) 97 | } 98 | 99 | async fn create_pane_from_event(&mut self, area: (u16, u16), event: &Event) -> Pane { 100 | self.operate(event); 101 | self.state.create_pane(area.0, area.1) 102 | } 103 | 104 | async fn create_panes_from_query( 105 | &mut self, 106 | area: (u16, u16), 107 | input: String, 108 | ) -> (Option, Option) { 109 | match run_jaq(&input, self.json) { 110 | Ok(ret) => { 111 | let mut guide = None; 112 | if ret.iter().all(|val| *val == Value::Null) { 113 | guide = Some(text::State { 114 | text: Text::from(format!("jq returned 'null', which may indicate a typo or incorrect filter: `{}`", input)), 115 | style: ContentStyle { 116 | foreground_color: Some(Color::Yellow), 117 | attributes: Attributes::from(Attribute::Bold), 118 | ..Default::default() 119 | }, 120 | ..Default::default() 121 | }.create_pane(area.0, area.1)); 122 | } 123 | 124 | self.state.stream = JsonStream::new(ret.iter()); 125 | 126 | (guide, Some(self.state.create_pane(area.0, area.1))) 127 | } 128 | Err(e) => ( 129 | Some( 130 | text::State { 131 | text: Text::from(format!("jq failed: `{}`", e)), 132 | style: ContentStyle { 133 | foreground_color: Some(Color::Red), 134 | attributes: Attributes::from(Attribute::Bold), 135 | ..Default::default() 136 | }, 137 | ..Default::default() 138 | } 139 | .create_pane(area.0, area.1), 140 | ), 141 | None, 142 | ), 143 | } 144 | } 145 | } 146 | 147 | fn run_jaq( 148 | query: &str, 149 | json_stream: &'static [serde_json::Value], 150 | ) -> anyhow::Result> { 151 | let mut ret = Vec::::new(); 152 | 153 | for input in json_stream { 154 | let mut ctx = ParseCtx::new(Vec::new()); 155 | ctx.insert_natives(jaq_core::core()); 156 | ctx.insert_defs(jaq_std::std()); 157 | 158 | let (f, errs) = jaq_parse::parse(query, jaq_parse::main()); 159 | if !errs.is_empty() { 160 | let error_message = errs 161 | .iter() 162 | .map(|e| e.to_string()) 163 | .collect::>() 164 | .join(", "); 165 | return Err(anyhow::anyhow!(error_message)); 166 | } 167 | 168 | let f = ctx.compile(f.unwrap()); 169 | let inputs = RcIter::new(core::iter::empty()); 170 | let mut out = f.run((Ctx::new([], &inputs), Val::from(input.clone()))); 171 | 172 | while let Some(Ok(val)) = out.next() { 173 | ret.push(val.into()); 174 | } 175 | } 176 | 177 | Ok(ret) 178 | } 179 | 180 | #[derive(Clone)] 181 | pub struct JsonStreamProvider { 182 | formatter: RowFormatter, 183 | max_streams: Option, 184 | } 185 | 186 | impl JsonStreamProvider { 187 | pub fn new(formatter: RowFormatter, max_streams: Option) -> Self { 188 | Self { 189 | formatter, 190 | max_streams, 191 | } 192 | } 193 | 194 | fn deserialize_json(&self, json_str: &str) -> anyhow::Result> { 195 | let deserializer: serde_json::StreamDeserializer<'_, serde_json::de::StrRead<'_>, Value> = 196 | Deserializer::from_str(json_str).into_iter::(); 197 | let results = match self.max_streams { 198 | Some(l) => deserializer.take(l).collect::, _>>(), 199 | None => deserializer.collect::, _>>(), 200 | }; 201 | results.map_err(anyhow::Error::from) 202 | } 203 | } 204 | 205 | #[async_trait::async_trait] 206 | impl ViewProvider for JsonStreamProvider { 207 | async fn provide( 208 | &mut self, 209 | item: &'static str, 210 | keybinds: JsonViewerKeybinds, 211 | ) -> anyhow::Result { 212 | let stream = self.deserialize_json(item)?; 213 | let static_stream = Box::leak(stream.into_boxed_slice()); 214 | Json::new(std::mem::take(&mut self.formatter), static_stream, keybinds) 215 | } 216 | } 217 | 218 | #[async_trait::async_trait] 219 | impl SearchProvider for JsonStreamProvider { 220 | async fn provide( 221 | &mut self, 222 | item: &str, 223 | ) -> anyhow::Result + Send>> { 224 | let stream = self.deserialize_json(item)?; 225 | let static_stream = Box::leak(stream.into_boxed_slice()); 226 | Ok(Box::new(jsonz::get_all_paths(static_stream.iter()))) 227 | } 228 | } 229 | -------------------------------------------------------------------------------- /src/main.rs: -------------------------------------------------------------------------------- 1 | use std::{ 2 | fs::File, 3 | io::{self, Read, Write}, 4 | path::PathBuf, 5 | }; 6 | 7 | use anyhow::anyhow; 8 | use clap::Parser; 9 | use config::Config; 10 | use promkit_core::crossterm::style::Attribute; 11 | use promkit_widgets::{ 12 | jsonstream::format::RowFormatter, 13 | listbox::{self, Listbox}, 14 | text_editor::{self, TextEditor}, 15 | }; 16 | 17 | mod editor; 18 | use editor::Editor; 19 | mod config; 20 | mod json; 21 | use json::JsonStreamProvider; 22 | mod processor; 23 | use processor::{ 24 | init::ViewInitializer, monitor::ContextMonitor, spinner::SpinnerSpawner, Context, Processor, 25 | ViewProvider, Visualizer, 26 | }; 27 | mod prompt; 28 | mod render; 29 | use render::{PaneIndex, Renderer, EMPTY_PANE}; 30 | mod search; 31 | use search::{IncrementalSearcher, SearchProvider}; 32 | 33 | static DEFAULT_CONFIG: &str = include_str!("../default.toml"); 34 | 35 | /// JSON navigator and interactive filter leveraging jq 36 | #[derive(Parser)] 37 | #[command( 38 | name = "jnv", 39 | version, 40 | help_template = " 41 | {about} 42 | 43 | Usage: {usage} 44 | 45 | Examples: 46 | - Read from a file: 47 | {bin} data.json 48 | 49 | - Read from standard input: 50 | cat data.json | {bin} 51 | 52 | Arguments: 53 | {positionals} 54 | 55 | Options: 56 | {options} 57 | " 58 | )] 59 | pub struct Args { 60 | /// Optional path to a JSON file. 61 | /// If not provided or if "-" is specified, 62 | /// reads from standard input. 63 | pub input: Option, 64 | 65 | #[arg(short = 'c', long = "config", help = "Path to the configuration file.")] 66 | pub config_file: Option, 67 | 68 | #[arg( 69 | long = "default-filter", 70 | help = "Default jq filter to apply to the input data", 71 | long_help = " 72 | Sets the default jq filter to apply to the input data. 73 | The filter is applied when the interface is first loaded. 74 | " 75 | )] 76 | default_filter: Option, 77 | } 78 | 79 | /// Parses the input based on the provided arguments. 80 | /// 81 | /// This function reads input data from either a specified file or standard input. 82 | /// If the `input` argument is `None`, or if it is a path 83 | /// that equals "-", data is read from standard input. 84 | /// Otherwise, the function attempts to open and 85 | /// read from the file specified in the `input` argument. 86 | fn parse_input(args: &Args) -> anyhow::Result { 87 | let mut ret = String::new(); 88 | 89 | match &args.input { 90 | None => { 91 | io::stdin().read_to_string(&mut ret)?; 92 | } 93 | Some(path) => { 94 | if path == &PathBuf::from("-") { 95 | io::stdin().read_to_string(&mut ret)?; 96 | } else { 97 | File::open(path)?.read_to_string(&mut ret)?; 98 | } 99 | } 100 | } 101 | 102 | Ok(ret) 103 | } 104 | 105 | /// Ensures the configuration file exists, creating it with default settings if it doesn't 106 | /// 107 | /// If the file already exists, returns Ok. 108 | /// If the file doesn't exist, writes the default configuration in TOML format. 109 | /// Returns an error if file creation fails. 110 | fn ensure_file_exists(path: &PathBuf) -> anyhow::Result<()> { 111 | if path.exists() { 112 | return Ok(()); 113 | } 114 | 115 | if let Some(parent) = path.parent() { 116 | std::fs::create_dir_all(parent) 117 | .map_err(|e| anyhow!("Failed to create directory: {}", e))?; 118 | } 119 | 120 | std::fs::File::create(path)?.write_all(DEFAULT_CONFIG.as_bytes())?; 121 | 122 | Ok(()) 123 | } 124 | 125 | /// Determines the configuration file path with the following precedence: 126 | /// 1. The provided `config_path` argument, if it exists. 127 | /// 2. The default configuration file path in the user's configuration directory. 128 | /// 129 | /// If the configuration file does not exist, it will be created. 130 | /// Returns an error if the file creation fails. 131 | fn determine_config_file(config_path: Option) -> anyhow::Result { 132 | // If a custom path is provided 133 | if let Some(path) = config_path { 134 | ensure_file_exists(&path)?; 135 | return Ok(path); 136 | } 137 | 138 | // Use the default path 139 | let default_path = dirs::config_dir() 140 | .ok_or_else(|| anyhow!("Failed to determine the configuration directory"))? 141 | // TODO: need versions...? 142 | .join("jnv") 143 | .join("config.toml"); 144 | 145 | ensure_file_exists(&default_path)?; 146 | Ok(default_path) 147 | } 148 | 149 | #[tokio::main] 150 | async fn main() -> anyhow::Result<()> { 151 | let args = Args::parse(); 152 | let input = parse_input(&args)?; 153 | 154 | let mut config = Config::default(); 155 | if let Ok(config_file) = determine_config_file(args.config_file) { 156 | // Note that the configuration file absolutely exists. 157 | let content = std::fs::read_to_string(&config_file) 158 | // TODO: output the message as the initial guide pane. 159 | .map_err(|e| anyhow!("Failed to read configuration file: {}", e))?; 160 | config = Config::load_from(&content) 161 | .map_err(|e| anyhow!("Failed to deserialize configuration file: {}", e))?; 162 | } 163 | 164 | let listbox_state = listbox::State { 165 | listbox: Listbox::default(), 166 | cursor: config.completion.cursor, 167 | active_item_style: Some(config.completion.active_item_style), 168 | inactive_item_style: Some(config.completion.inactive_item_style), 169 | lines: config.completion.lines, 170 | }; 171 | 172 | let searcher = 173 | IncrementalSearcher::new(listbox_state, config.completion.search_result_chunk_size); 174 | 175 | let text_editor_state = text_editor::State { 176 | texteditor: if let Some(filter) = args.default_filter { 177 | TextEditor::new(filter) 178 | } else { 179 | Default::default() 180 | }, 181 | history: Default::default(), 182 | prefix: config.editor.theme_on_focus.prefix.clone(), 183 | mask: Default::default(), 184 | prefix_style: config.editor.theme_on_focus.prefix_style, 185 | active_char_style: config.editor.theme_on_focus.active_char_style, 186 | inactive_char_style: config.editor.theme_on_focus.inactive_char_style, 187 | edit_mode: config.editor.mode, 188 | word_break_chars: config.editor.word_break_chars, 189 | lines: Default::default(), 190 | }; 191 | 192 | let provider = &mut JsonStreamProvider::new( 193 | RowFormatter { 194 | curly_brackets_style: config.json.theme.curly_brackets_style, 195 | square_brackets_style: config.json.theme.square_brackets_style, 196 | key_style: config.json.theme.key_style, 197 | string_value_style: config.json.theme.string_value_style, 198 | number_value_style: config.json.theme.number_value_style, 199 | boolean_value_style: config.json.theme.boolean_value_style, 200 | null_value_style: config.json.theme.null_value_style, 201 | active_item_attribute: Attribute::Bold, 202 | inactive_item_attribute: Attribute::Dim, 203 | indent: config.json.theme.indent, 204 | }, 205 | config.json.max_streams, 206 | ); 207 | 208 | let item = Box::leak(input.into_boxed_str()); 209 | 210 | let loading_suggestions_task = 211 | searcher.spawn_load_task(provider, item, config.completion.search_load_chunk_size); 212 | 213 | // TODO: re-consider put editor_task of prompt::run into Editor construction time. 214 | // Overall, there are several cases where it would be sufficient to 215 | // launch a background thread during construction. 216 | let editor = Editor::new( 217 | text_editor_state, 218 | searcher, 219 | config.editor.theme_on_focus, 220 | config.editor.theme_on_defocus, 221 | // TODO: remove clones 222 | config.keybinds.on_editor.clone(), 223 | ); 224 | 225 | // TODO: put all logics here. 226 | prompt::run( 227 | item, 228 | config.reactivity_control, 229 | provider, 230 | editor, 231 | loading_suggestions_task, 232 | config.no_hint, 233 | config.keybinds, 234 | ) 235 | .await?; 236 | 237 | Ok(()) 238 | } 239 | -------------------------------------------------------------------------------- /src/processor.rs: -------------------------------------------------------------------------------- 1 | use std::sync::Arc; 2 | 3 | use async_trait::async_trait; 4 | use promkit_core::{crossterm::event::Event, pane::Pane}; 5 | use tokio::{sync::Mutex, task::JoinHandle}; 6 | 7 | use crate::{PaneIndex, Renderer, EMPTY_PANE}; 8 | pub mod init; 9 | pub use init::ViewProvider; 10 | pub mod monitor; 11 | pub mod spinner; 12 | 13 | #[derive(PartialEq)] 14 | enum State { 15 | Idle, 16 | Loading, 17 | Processing, 18 | } 19 | 20 | #[async_trait] 21 | pub trait Visualizer: Send + Sync + 'static { 22 | async fn content_to_copy(&self) -> String; 23 | async fn create_init_pane(&mut self, area: (u16, u16)) -> Pane; 24 | async fn create_pane_from_event(&mut self, area: (u16, u16), event: &Event) -> Pane; 25 | async fn create_panes_from_query( 26 | &mut self, 27 | area: (u16, u16), 28 | query: String, 29 | ) -> (Option, Option); 30 | } 31 | 32 | pub struct Context { 33 | state: State, 34 | area: (u16, u16), 35 | current_task: Option>, 36 | } 37 | 38 | impl Context { 39 | pub fn new(area: (u16, u16)) -> Self { 40 | Self { 41 | state: State::Idle, 42 | area, 43 | current_task: None, 44 | } 45 | } 46 | } 47 | 48 | pub struct Processor { 49 | shared: Arc>, 50 | } 51 | 52 | impl Processor { 53 | pub fn new(shared: Arc>) -> Self { 54 | Self { shared } 55 | } 56 | 57 | fn spawn_process_task( 58 | &self, 59 | query: String, 60 | shared_visualizer: Arc>, 61 | shared_renderer: Arc>, 62 | ) -> JoinHandle<()> { 63 | let shared = self.shared.clone(); 64 | tokio::spawn(async move { 65 | { 66 | let mut shared_state = shared.lock().await; 67 | shared_state.state = State::Processing; 68 | } 69 | 70 | let (maybe_guide, maybe_resp) = { 71 | let shared_state = shared.lock().await; 72 | let area = shared_state.area; 73 | drop(shared_state); 74 | 75 | let mut visualizer = shared_visualizer.lock().await; 76 | visualizer.create_panes_from_query(area, query).await 77 | }; 78 | 79 | // Set state to Idle to prevent overwriting by spinner frames in terminal. 80 | { 81 | let mut shared_state = shared.lock().await; 82 | shared_state.state = State::Idle; 83 | } 84 | { 85 | // TODO: error handling 86 | let _ = shared_renderer.lock().await.update_and_draw([ 87 | ( 88 | PaneIndex::Guide, 89 | maybe_guide.unwrap_or(EMPTY_PANE.to_owned()), 90 | ), 91 | ( 92 | PaneIndex::Processor, 93 | maybe_resp.unwrap_or(EMPTY_PANE.to_owned()), 94 | ), 95 | ]); 96 | } 97 | }) 98 | } 99 | 100 | pub async fn render_on_resize( 101 | &self, 102 | shared_visualizer: Arc>, 103 | area: (u16, u16), 104 | query: String, 105 | shared_renderer: Arc>, 106 | ) { 107 | { 108 | let mut shared_state = self.shared.lock().await; 109 | shared_state.area = area; 110 | if let Some(task) = shared_state.current_task.take() { 111 | task.abort(); 112 | } 113 | } 114 | 115 | let process_task = self.spawn_process_task(query, shared_visualizer, shared_renderer); 116 | 117 | { 118 | let mut shared_state = self.shared.lock().await; 119 | shared_state.current_task = Some(process_task); 120 | } 121 | } 122 | 123 | pub async fn render_result( 124 | &self, 125 | shared_visualizer: Arc>, 126 | query: String, 127 | shared_renderer: Arc>, 128 | ) { 129 | { 130 | let mut shared_state = self.shared.lock().await; 131 | if let Some(task) = shared_state.current_task.take() { 132 | task.abort(); 133 | } 134 | } 135 | 136 | let process_task = self.spawn_process_task(query, shared_visualizer, shared_renderer); 137 | 138 | { 139 | let mut shared_state = self.shared.lock().await; 140 | shared_state.current_task = Some(process_task); 141 | } 142 | } 143 | } 144 | -------------------------------------------------------------------------------- /src/processor/init.rs: -------------------------------------------------------------------------------- 1 | use std::sync::Arc; 2 | 3 | use async_trait::async_trait; 4 | use tokio::sync::Mutex; 5 | 6 | use super::{Context, State, Visualizer}; 7 | use crate::{config::JsonViewerKeybinds, PaneIndex, Renderer}; 8 | 9 | #[async_trait] 10 | pub trait ViewProvider { 11 | async fn provide( 12 | &mut self, 13 | item: &'static str, 14 | keybinds: JsonViewerKeybinds, 15 | ) -> anyhow::Result; 16 | } 17 | 18 | pub struct ViewInitializer { 19 | shared: Arc>, 20 | } 21 | 22 | impl ViewInitializer { 23 | pub fn new(shared: Arc>) -> Self { 24 | Self { shared } 25 | } 26 | 27 | pub async fn initialize<'a, T: ViewProvider>( 28 | &self, 29 | provider: &'a mut T, 30 | item: &'static str, 31 | area: (u16, u16), 32 | shared_renderer: Arc>, 33 | keybinds: JsonViewerKeybinds, 34 | ) -> anyhow::Result { 35 | { 36 | let mut shared_state = self.shared.lock().await; 37 | if let Some(task) = shared_state.current_task.take() { 38 | task.abort(); 39 | } 40 | shared_state.state = State::Loading; 41 | } 42 | 43 | let mut visualizer = provider.provide(item, keybinds).await?; 44 | let pane = visualizer.create_init_pane(area).await; 45 | 46 | // Set state to Idle to prevent overwriting by spinner frames in terminal. 47 | { 48 | let mut shared_state = self.shared.lock().await; 49 | shared_state.state = State::Idle; 50 | } 51 | { 52 | // TODO: error handling 53 | let _ = shared_renderer 54 | .lock() 55 | .await 56 | .update_and_draw([(PaneIndex::Processor, pane)]); 57 | } 58 | 59 | Ok(visualizer) 60 | } 61 | } 62 | -------------------------------------------------------------------------------- /src/processor/monitor.rs: -------------------------------------------------------------------------------- 1 | use std::sync::Arc; 2 | 3 | use tokio::sync::Mutex; 4 | 5 | use super::{Context, State}; 6 | 7 | pub struct ContextMonitor { 8 | shared: Arc>, 9 | } 10 | 11 | impl ContextMonitor { 12 | pub fn new(shared: Arc>) -> Self { 13 | Self { shared } 14 | } 15 | 16 | pub async fn is_idle(&self) -> bool { 17 | let context = self.shared.lock().await; 18 | context.state == State::Idle 19 | } 20 | } 21 | -------------------------------------------------------------------------------- /src/processor/spinner.rs: -------------------------------------------------------------------------------- 1 | use std::sync::Arc; 2 | 3 | use promkit_core::{grapheme::StyledGraphemes, Pane}; 4 | use tokio::{sync::Mutex, task::JoinHandle, time::Duration}; 5 | 6 | use super::{Context, State}; 7 | use crate::{PaneIndex, Renderer}; 8 | 9 | const LOADING_FRAMES: [&str; 10] = ["⠋", "⠙", "⠹", "⠸", "⠼", "⠴", "⠦", "⠧", "⠇", "⠏"]; 10 | 11 | pub struct SpinnerSpawner { 12 | shared: Arc>, 13 | } 14 | 15 | impl SpinnerSpawner { 16 | pub fn new(shared: Arc>) -> Self { 17 | Self { shared } 18 | } 19 | 20 | pub fn spawn_spin_task( 21 | &self, 22 | shared_renderer: Arc>, 23 | spin_duration: Duration, 24 | ) -> JoinHandle<()> { 25 | let shared = self.shared.clone(); 26 | let mut frame_index = 0; 27 | tokio::spawn(async move { 28 | let mut interval = tokio::time::interval(spin_duration); 29 | loop { 30 | interval.tick().await; 31 | 32 | { 33 | let shared_state = shared.lock().await; 34 | if shared_state.state == State::Idle { 35 | continue; 36 | } 37 | } 38 | 39 | frame_index = (frame_index + 1) % LOADING_FRAMES.len(); 40 | 41 | let pane = Pane::new(vec![StyledGraphemes::from(LOADING_FRAMES[frame_index])], 0); 42 | { 43 | // TODO: error handling 44 | let _ = shared_renderer 45 | .lock() 46 | .await 47 | .update_and_draw([(PaneIndex::Processor, pane)]); 48 | } 49 | } 50 | }) 51 | } 52 | } 53 | -------------------------------------------------------------------------------- /src/prompt.rs: -------------------------------------------------------------------------------- 1 | use std::{io, sync::Arc, time::Duration}; 2 | 3 | use arboard::Clipboard; 4 | use futures::StreamExt; 5 | use promkit_core::{ 6 | crossterm::{ 7 | cursor, 8 | event::{Event, EventStream, KeyCode, KeyEvent, KeyEventKind, KeyEventState, KeyModifiers}, 9 | execute, 10 | style::{Color, ContentStyle}, 11 | terminal::{self, disable_raw_mode, enable_raw_mode}, 12 | }, 13 | PaneFactory, 14 | }; 15 | use promkit_widgets::text::{self, Text}; 16 | use tokio::{ 17 | sync::{mpsc, Mutex, RwLock}, 18 | task::JoinHandle, 19 | }; 20 | 21 | use crate::{ 22 | config::{event::Matcher, Keybinds, ReactivityControl}, 23 | Context, ContextMonitor, Editor, PaneIndex, Processor, Renderer, SearchProvider, 24 | SpinnerSpawner, ViewInitializer, ViewProvider, Visualizer, EMPTY_PANE, 25 | }; 26 | 27 | fn spawn_debouncer( 28 | mut debounce_rx: mpsc::Receiver, 29 | last_tx: mpsc::Sender, 30 | duration: Duration, 31 | ) -> tokio::task::JoinHandle<()> { 32 | tokio::spawn(async move { 33 | let mut last_query = None; 34 | let mut delay = tokio::time::interval(duration); 35 | loop { 36 | tokio::select! { 37 | maybe_query = debounce_rx.recv() => { 38 | if let Some(query) = maybe_query { 39 | last_query = Some(query); 40 | } else { 41 | break; 42 | } 43 | }, 44 | _ = delay.tick() => { 45 | if let Some(text) = last_query.take() { 46 | let _ = last_tx.send(text).await; 47 | } 48 | }, 49 | } 50 | } 51 | }) 52 | } 53 | 54 | fn copy_to_clipboard(content: &str) -> text::State { 55 | match Clipboard::new() { 56 | Ok(mut clipboard) => match clipboard.set_text(content) { 57 | Ok(_) => text::State { 58 | text: Text::from("Copied to clipboard"), 59 | style: ContentStyle { 60 | foreground_color: Some(Color::Green), 61 | ..Default::default() 62 | }, 63 | ..Default::default() 64 | }, 65 | Err(e) => text::State { 66 | text: Text::from(format!("Failed to copy to clipboard: {}", e)), 67 | style: ContentStyle { 68 | foreground_color: Some(Color::Red), 69 | ..Default::default() 70 | }, 71 | ..Default::default() 72 | }, 73 | }, 74 | // arboard fails (in the specific environment like linux?) on Clipboard::new() 75 | // suppress the errors (but still show them) not to break the prompt 76 | // https://github.com/1Password/arboard/issues/153 77 | Err(e) => text::State { 78 | text: Text::from(format!("Failed to setup clipboard: {}", e)), 79 | style: ContentStyle { 80 | foreground_color: Some(Color::Red), 81 | ..Default::default() 82 | }, 83 | ..Default::default() 84 | }, 85 | } 86 | } 87 | 88 | enum Focus { 89 | Editor, 90 | Processor, 91 | } 92 | 93 | #[allow(clippy::too_many_arguments)] 94 | pub async fn run( 95 | item: &'static str, 96 | reactivity_control: ReactivityControl, 97 | provider: &mut T, 98 | editor: Editor, 99 | loading_suggestions_task: JoinHandle>, 100 | no_hint: bool, 101 | keybinds: Keybinds, 102 | ) -> anyhow::Result<()> { 103 | enable_raw_mode()?; 104 | execute!(io::stdout(), cursor::Hide)?; 105 | 106 | let size = terminal::size()?; 107 | 108 | let shared_renderer = Arc::new(Mutex::new(Renderer::try_init_draw( 109 | [ 110 | editor.create_editor_pane(size.0, size.1), 111 | EMPTY_PANE.to_owned(), 112 | EMPTY_PANE.to_owned(), 113 | EMPTY_PANE.to_owned(), 114 | ], 115 | no_hint, 116 | )?)); 117 | 118 | let ctx = Arc::new(Mutex::new(Context::new(size))); 119 | 120 | let (last_query_tx, mut last_query_rx) = mpsc::channel(1); 121 | let (debounce_query_tx, debounce_query_rx) = mpsc::channel(1); 122 | let query_debouncer = spawn_debouncer( 123 | debounce_query_rx, 124 | last_query_tx, 125 | reactivity_control.query_debounce_duration, 126 | ); 127 | if !editor.text().is_empty() { 128 | debounce_query_tx.send(editor.text()).await?; 129 | } 130 | 131 | let (last_resize_tx, mut last_resize_rx) = mpsc::channel::<(u16, u16)>(1); 132 | let (debounce_resize_tx, debounce_resize_rx) = mpsc::channel(1); 133 | let resize_debouncer = spawn_debouncer( 134 | debounce_resize_rx, 135 | last_resize_tx, 136 | reactivity_control.resize_debounce_duration, 137 | ); 138 | 139 | let spinner_spawner = SpinnerSpawner::new(ctx.clone()); 140 | let spinning = 141 | spinner_spawner.spawn_spin_task(shared_renderer.clone(), reactivity_control.spin_duration); 142 | 143 | let mut focus = Focus::Editor; 144 | let (editor_event_tx, mut editor_event_rx) = mpsc::channel::(1); 145 | let (processor_event_tx, mut processor_event_rx) = mpsc::channel::(1); 146 | 147 | let (editor_copy_tx, mut editor_copy_rx) = mpsc::channel::<()>(1); 148 | let (processor_copy_tx, mut processor_copy_rx) = mpsc::channel::<()>(1); 149 | 150 | let (editor_focus_tx, mut editor_focus_rx) = mpsc::channel::(1); 151 | 152 | let mut text_diff = [editor.text(), editor.text()]; 153 | let shared_editor = Arc::new(RwLock::new(editor)); 154 | let processor = Processor::new(ctx.clone()); 155 | let context_monitor = ContextMonitor::new(ctx.clone()); 156 | let initializer = ViewInitializer::new(ctx.clone()); 157 | let initializing = initializer.initialize( 158 | provider, 159 | item, 160 | size, 161 | shared_renderer.clone(), 162 | keybinds.on_json_viewer, 163 | ); 164 | 165 | let main_task: JoinHandle> = { 166 | let mut stream = EventStream::new(); 167 | let shared_renderer = shared_renderer.clone(); 168 | tokio::spawn(async move { 169 | 'main: loop { 170 | tokio::select! { 171 | Some(Ok(event)) = stream.next() => { 172 | match event { 173 | Event::Resize(width, height) => { 174 | debounce_resize_tx.send((width, height)).await?; 175 | }, 176 | event if keybinds.exit.matches(&event) => { 177 | break 'main 178 | }, 179 | Event::Key(KeyEvent { 180 | code: KeyCode::Char('q'), 181 | modifiers: KeyModifiers::CONTROL, 182 | kind: KeyEventKind::Press, 183 | state: KeyEventState::NONE, 184 | }) => { 185 | editor_copy_tx.send(()).await?; 186 | }, 187 | Event::Key(KeyEvent { 188 | code: KeyCode::Char('o'), 189 | modifiers: KeyModifiers::CONTROL, 190 | kind: KeyEventKind::Press, 191 | state: KeyEventState::NONE, 192 | }) => { 193 | let mut pane = EMPTY_PANE.to_owned(); 194 | if context_monitor.is_idle().await { 195 | processor_copy_tx.send(()).await?; 196 | } else { 197 | let size = terminal::size()?; 198 | pane = text::State { 199 | text: Text::from("Failed to copy while rendering is in progress.".to_string()), 200 | style: ContentStyle { 201 | foreground_color: Some(Color::Yellow), 202 | ..Default::default() 203 | }, 204 | ..Default::default() 205 | }.create_pane(size.0, size.1); 206 | } 207 | { 208 | shared_renderer.lock().await.update_and_draw([ 209 | (PaneIndex::Guide, pane), 210 | ])?; 211 | } 212 | }, 213 | Event::Key(KeyEvent { 214 | code: KeyCode::Down, 215 | modifiers: KeyModifiers::SHIFT, 216 | kind: KeyEventKind::Press, 217 | state: KeyEventState::NONE, 218 | }) | Event::Key(KeyEvent { 219 | code: KeyCode::Up, 220 | modifiers: KeyModifiers::SHIFT, 221 | kind: KeyEventKind::Press, 222 | state: KeyEventState::NONE, 223 | }) => { 224 | match focus { 225 | Focus::Editor => { 226 | let mut pane = EMPTY_PANE.to_owned(); 227 | if context_monitor.is_idle().await { 228 | focus = Focus::Processor; 229 | editor_focus_tx.send(false).await?; 230 | } else { 231 | let size = terminal::size()?; 232 | pane = text::State { 233 | text: Text::from("Failed to switch pane while rendering is in progress.".to_string()), 234 | style: ContentStyle { 235 | foreground_color: Some(Color::Yellow), 236 | ..Default::default() 237 | }, 238 | ..Default::default() 239 | }.create_pane(size.0, size.1); 240 | } 241 | { 242 | shared_renderer.lock().await.update_and_draw([ 243 | (PaneIndex::Guide, pane), 244 | ])?; 245 | } 246 | }, 247 | Focus::Processor => { 248 | focus = Focus::Editor; 249 | editor_focus_tx.send(true).await?; 250 | }, 251 | } 252 | }, 253 | event => { 254 | match focus { 255 | Focus::Editor => { 256 | editor_event_tx.send(event).await?; 257 | }, 258 | Focus::Processor => { 259 | processor_event_tx.send(event).await?; 260 | }, 261 | } 262 | }, 263 | } 264 | }, 265 | else => { 266 | break 'main; 267 | } 268 | } 269 | } 270 | Ok(()) 271 | }) 272 | }; 273 | 274 | let editor_task: JoinHandle> = { 275 | let shared_renderer = shared_renderer.clone(); 276 | let shared_editor = shared_editor.clone(); 277 | tokio::spawn(async move { 278 | loop { 279 | tokio::select! { 280 | Some(focus) = editor_focus_rx.recv() => { 281 | let (editor_pane, guide_pane) = { 282 | let mut editor = shared_editor.write().await; 283 | if focus { 284 | editor.focus(); 285 | } else { 286 | editor.defocus(); 287 | } 288 | ( 289 | editor.create_editor_pane(size.0, size.1), 290 | editor.create_guide_pane(size.0, size.1), 291 | ) 292 | }; 293 | { 294 | shared_renderer.lock().await.update_and_draw([ 295 | (PaneIndex::Editor, editor_pane), 296 | (PaneIndex::Guide, guide_pane), 297 | ])?; 298 | } 299 | } 300 | Some(()) = editor_copy_rx.recv() => { 301 | let text = { 302 | let editor = shared_editor.write().await; 303 | editor.text() 304 | }; 305 | let guide = copy_to_clipboard(&text); 306 | let size = terminal::size()?; 307 | let pane = guide.create_pane(size.0, size.1); 308 | { 309 | shared_renderer.lock().await.update_and_draw([ 310 | (PaneIndex::Guide, pane), 311 | ])?; 312 | } 313 | } 314 | Some(event) = editor_event_rx.recv() => { 315 | let size = terminal::size()?; 316 | let (editor_pane, guide_pane, searcher_pane) = { 317 | 318 | let mut editor = shared_editor.write().await; 319 | editor.operate(&event).await?; 320 | 321 | let current_text = editor.text(); 322 | if current_text != text_diff[1] { 323 | debounce_query_tx.send(current_text.clone()).await?; 324 | text_diff[0] = text_diff[1].clone(); 325 | text_diff[1] = current_text; 326 | } 327 | ( 328 | editor.create_editor_pane(size.0, size.1), 329 | editor.create_guide_pane(size.0, size.1), 330 | editor.create_searcher_pane(size.0, size.1), 331 | ) 332 | }; 333 | { 334 | shared_renderer.lock().await.update_and_draw([ 335 | (PaneIndex::Editor, editor_pane), 336 | (PaneIndex::Guide, guide_pane), 337 | (PaneIndex::Search, searcher_pane), 338 | ])?; 339 | } 340 | } 341 | else => { 342 | break 343 | } 344 | } 345 | } 346 | Ok(()) 347 | }) 348 | }; 349 | 350 | let processor_task: JoinHandle> = { 351 | let shared_renderer = shared_renderer.clone(); 352 | let shared_editor = shared_editor.clone(); 353 | let visualizer = initializing.await?; 354 | let shared_visualizer = Arc::new(Mutex::new(visualizer)); 355 | tokio::spawn(async move { 356 | loop { 357 | tokio::select! { 358 | Some(()) = processor_copy_rx.recv() => { 359 | let visualizer = shared_visualizer.lock().await; 360 | let guide = copy_to_clipboard(&visualizer.content_to_copy().await); 361 | let size = terminal::size()?; 362 | let pane = guide.create_pane(size.0, size.1); 363 | { 364 | shared_renderer.lock().await.update_and_draw([ 365 | (PaneIndex::Guide, pane), 366 | ])?; 367 | } 368 | } 369 | Some(event) = processor_event_rx.recv() => { 370 | let pane = { 371 | let mut visualizer = shared_visualizer.lock().await; 372 | visualizer.create_pane_from_event((size.0, size.1), &event).await 373 | }; 374 | { 375 | shared_renderer.lock().await.update_and_draw([ 376 | (PaneIndex::Processor, pane), 377 | ])?; 378 | } 379 | } 380 | Some(query) = last_query_rx.recv() => { 381 | processor.render_result( 382 | shared_visualizer.clone(), 383 | query, 384 | shared_renderer.clone(), 385 | ).await; 386 | } 387 | Some(area) = last_resize_rx.recv() => { 388 | let (editor_pane, guide_pane, searcher_pane) = { 389 | let editor = shared_editor.read().await; 390 | ( 391 | editor.create_editor_pane(size.0, size.1), 392 | editor.create_guide_pane(size.0, size.1), 393 | editor.create_searcher_pane(size.0, size.1), 394 | ) 395 | }; 396 | { 397 | shared_renderer.lock().await.update_and_draw([ 398 | (PaneIndex::Editor, editor_pane), 399 | (PaneIndex::Guide, guide_pane), 400 | (PaneIndex::Search, searcher_pane), 401 | ])?; 402 | } 403 | let text = { 404 | let editor = shared_editor.read().await; 405 | editor.text() 406 | }; 407 | processor.render_on_resize( 408 | shared_visualizer.clone(), 409 | area, 410 | text, 411 | shared_renderer.clone(), 412 | ).await; 413 | } 414 | else => { 415 | break 416 | } 417 | } 418 | } 419 | Ok(()) 420 | }) 421 | }; 422 | 423 | main_task.await??; 424 | 425 | loading_suggestions_task.abort(); 426 | spinning.abort(); 427 | query_debouncer.abort(); 428 | resize_debouncer.abort(); 429 | editor_task.abort(); 430 | processor_task.abort(); 431 | 432 | execute!(io::stdout(), cursor::Show)?; 433 | disable_raw_mode()?; 434 | 435 | Ok(()) 436 | } 437 | -------------------------------------------------------------------------------- /src/render.rs: -------------------------------------------------------------------------------- 1 | use std::sync::LazyLock; 2 | 3 | use promkit_core::{crossterm::cursor, pane::Pane, terminal::Terminal}; 4 | 5 | // TODO: One Guide is sufficient. 6 | #[derive(Debug, PartialEq)] 7 | pub enum PaneIndex { 8 | Editor = 0, 9 | Guide = 1, 10 | Search = 2, 11 | Processor = 3, 12 | } 13 | 14 | pub static EMPTY_PANE: LazyLock = LazyLock::new(|| Pane::new(vec![], 0)); 15 | const PANE_SIZE: usize = PaneIndex::Processor as usize + 1; 16 | 17 | pub struct Renderer { 18 | no_hint: bool, 19 | terminal: Terminal, 20 | panes: [Pane; PANE_SIZE], 21 | } 22 | 23 | impl Renderer { 24 | pub fn try_init_draw(init_panes: [Pane; PANE_SIZE], no_hint: bool) -> anyhow::Result { 25 | let mut ret = Self { 26 | no_hint, 27 | terminal: Terminal { 28 | position: cursor::position()?, 29 | }, 30 | panes: init_panes, 31 | }; 32 | ret.terminal.draw(&ret.panes)?; 33 | Ok(ret) 34 | } 35 | 36 | pub fn update_and_draw>( 37 | &mut self, 38 | iter: I, 39 | ) -> anyhow::Result<()> { 40 | for (index, pane) in iter { 41 | if self.no_hint && index == PaneIndex::Guide { 42 | continue; 43 | } 44 | self.panes[index as usize] = pane; 45 | } 46 | self.terminal.draw(&self.panes)?; 47 | Ok(()) 48 | } 49 | } 50 | -------------------------------------------------------------------------------- /src/search.rs: -------------------------------------------------------------------------------- 1 | use std::{collections::BTreeSet, sync::Arc}; 2 | 3 | use anyhow::anyhow; 4 | use async_trait::async_trait; 5 | use promkit_core::{pane::Pane, PaneFactory}; 6 | use promkit_widgets::listbox::{self, Listbox}; 7 | use tokio::{ 8 | sync::{Mutex, RwLock}, 9 | task::JoinHandle, 10 | }; 11 | 12 | #[async_trait] 13 | pub trait SearchProvider: Clone + Send + 'static { 14 | async fn provide( 15 | &mut self, 16 | item: &str, 17 | ) -> anyhow::Result + Send>>; 18 | } 19 | 20 | #[derive(Clone, Default)] 21 | pub struct LoadState { 22 | pub loaded: bool, 23 | pub loaded_item_len: usize, 24 | } 25 | 26 | pub struct StartSearchResult { 27 | pub head_item: Option, 28 | pub load_state: LoadState, 29 | } 30 | 31 | pub struct IncrementalSearcher { 32 | shared_set: Arc>>, 33 | shared_load_state: Arc>, 34 | state: listbox::State, 35 | search_result_chunk_size: usize, 36 | search_chunk_remaining: Vec, 37 | } 38 | 39 | impl IncrementalSearcher { 40 | pub fn new(state: listbox::State, search_result_chunk_size: usize) -> Self { 41 | Self { 42 | shared_set: Default::default(), 43 | shared_load_state: Default::default(), 44 | state, 45 | search_result_chunk_size, 46 | search_chunk_remaining: Default::default(), 47 | } 48 | } 49 | 50 | pub fn spawn_load_task( 51 | &self, 52 | provider: &mut T, 53 | item: &'static str, 54 | chunk_size: usize, 55 | ) -> JoinHandle> { 56 | let shared_set = self.shared_set.clone(); 57 | let shared_load_state = self.shared_load_state.clone(); 58 | let mut provider = provider.clone(); 59 | tokio::spawn(async move { 60 | let mut batch = Vec::with_capacity(chunk_size); 61 | let iter = provider.provide(item).await?; 62 | 63 | for v in iter { 64 | batch.push(v); 65 | 66 | if batch.len() >= chunk_size { 67 | let mut set = shared_set.lock().await; 68 | for item in batch.drain(..) { 69 | set.insert(item); 70 | } 71 | let mut state = shared_load_state.write().await; 72 | state.loaded_item_len += chunk_size; 73 | } 74 | } 75 | 76 | let remaining = batch.len(); 77 | if !batch.is_empty() { 78 | let mut set = shared_set.lock().await; 79 | for item in batch { 80 | set.insert(item); 81 | } 82 | } 83 | 84 | let mut state = shared_load_state.write().await; 85 | state.loaded = true; 86 | state.loaded_item_len += remaining; 87 | Ok(()) 88 | }) 89 | } 90 | 91 | pub fn up(&mut self) { 92 | self.state.listbox.backward(); 93 | } 94 | 95 | pub fn down_with_load(&mut self) { 96 | self.state.listbox.forward(); 97 | if self 98 | .state 99 | .listbox 100 | .len() 101 | .saturating_sub(self.state.listbox.position()) 102 | < self.state.lines.unwrap_or(1) 103 | { 104 | self.load_more(); 105 | } 106 | } 107 | 108 | pub fn get_current_item(&self) -> String { 109 | self.state.listbox.get().to_string() 110 | } 111 | 112 | pub fn create_pane(&self, width: u16, height: u16) -> Pane { 113 | self.state.create_pane(width, height) 114 | } 115 | 116 | pub fn leave_search(&mut self) { 117 | self.state.listbox = Listbox::from_displayable(Vec::::new()); 118 | self.search_chunk_remaining = Vec::::new(); 119 | } 120 | 121 | pub fn start_search(&mut self, prefix: &str) -> anyhow::Result { 122 | match ( 123 | self.shared_load_state.try_read(), 124 | self.shared_set.try_lock(), 125 | ) { 126 | (Ok(state), Ok(set)) => { 127 | let mut items: Vec<_> = set 128 | .iter() 129 | .filter(|p| p.starts_with(prefix)) 130 | .cloned() 131 | .collect(); 132 | if items.is_empty() { 133 | return Ok(StartSearchResult { 134 | head_item: None, 135 | load_state: state.clone(), 136 | }); 137 | } 138 | let used = items 139 | .drain(..self.search_result_chunk_size.min(items.len())) 140 | .collect::>(); 141 | self.search_chunk_remaining = items; 142 | self.state.listbox = Listbox::from_displayable(used); 143 | Ok(StartSearchResult { 144 | head_item: Some(self.state.listbox.get().to_string()), 145 | load_state: state.clone(), 146 | }) 147 | } 148 | (Err(_), _) | (_, Err(_)) => Err(anyhow!( 149 | "Failed to acquire lock for suggestions. Please try again." 150 | )), 151 | } 152 | } 153 | 154 | fn load_more(&mut self) { 155 | if self.search_chunk_remaining.is_empty() { 156 | return; 157 | } 158 | let items = self.search_chunk_remaining.drain( 159 | ..self 160 | .search_result_chunk_size 161 | .min(self.search_chunk_remaining.len()), 162 | ); 163 | for item in items { 164 | self.state.listbox.push_string(item); 165 | } 166 | } 167 | } 168 | --------------------------------------------------------------------------------