├── .DS_Store ├── .github ├── dependabot.yml └── workflows │ ├── ci.yml │ ├── release.yml │ └── web.yml ├── .gitignore ├── Cargo.lock ├── Cargo.toml ├── LICENSE ├── README.md ├── demo.cast ├── demo.gif ├── oranda.json ├── src ├── app.rs ├── db.rs └── main.rs └── wix └── main.wxs /.DS_Store: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/KMJ-007/lazygh/366d4ba94d8718c75e8e03b8b6df28a227400892/.DS_Store -------------------------------------------------------------------------------- /.github/dependabot.yml: -------------------------------------------------------------------------------- 1 | # To get started with Dependabot version updates, you'll need to specify which 2 | # package ecosystems to update and where the package manifests are located. 3 | # Please see the documentation for all configuration options: 4 | # https://docs.github.com/github/administering-a-repository/configuration-options-for-dependency-updates 5 | 6 | version: 2 7 | updates: 8 | # Maintain dependencies for Cargo 9 | - package-ecosystem: "cargo" 10 | directory: "/" # Location of package manifests 11 | schedule: 12 | interval: "weekly" 13 | # Maintain dependencies for GitHub Actions 14 | - package-ecosystem: github-actions 15 | directory: "/" 16 | schedule: 17 | interval: weekly 18 | -------------------------------------------------------------------------------- /.github/workflows/ci.yml: -------------------------------------------------------------------------------- 1 | name: CI 2 | 3 | on: 4 | pull_request: 5 | push: 6 | branches: 7 | - main 8 | - master 9 | - develop 10 | 11 | env: 12 | CARGO_TERM_COLOR: always 13 | 14 | # ensure that the workflow is only triggered once per PR, subsequent pushes to the PR will cancel 15 | # and restart the workflow. See https://docs.github.com/en/actions/using-jobs/using-concurrency 16 | concurrency: 17 | group: ${{ github.workflow }}-${{ github.head_ref || github.run_id }} 18 | cancel-in-progress: true 19 | 20 | jobs: 21 | fmt: 22 | name: fmt 23 | runs-on: ubuntu-latest 24 | steps: 25 | - name: Checkout 26 | uses: actions/checkout@v4 27 | - name: Install Rust stable 28 | uses: dtolnay/rust-toolchain@stable 29 | with: 30 | components: rustfmt 31 | - name: check formatting 32 | run: cargo fmt -- --check 33 | - name: Cache Cargo dependencies 34 | uses: Swatinem/rust-cache@v2 35 | clippy: 36 | name: clippy 37 | runs-on: ubuntu-latest 38 | permissions: 39 | checks: write 40 | steps: 41 | - name: Checkout 42 | uses: actions/checkout@v4 43 | - name: Install Rust stable 44 | uses: dtolnay/rust-toolchain@stable 45 | with: 46 | components: clippy 47 | - name: Run clippy action 48 | uses: clechasseur/rs-clippy-check@v3 49 | - name: Cache Cargo dependencies 50 | uses: Swatinem/rust-cache@v2 51 | doc: 52 | # run docs generation on nightly rather than stable. This enables features like 53 | # https://doc.rust-lang.org/beta/unstable-book/language-features/doc-cfg.html which allows an 54 | # API be documented as only available in some specific platforms. 55 | name: doc 56 | runs-on: ubuntu-latest 57 | steps: 58 | - uses: actions/checkout@v4 59 | - name: Install Rust nightly 60 | uses: dtolnay/rust-toolchain@nightly 61 | - name: Run cargo doc 62 | run: cargo doc --no-deps --all-features 63 | env: 64 | RUSTDOCFLAGS: --cfg docsrs 65 | test: 66 | runs-on: ${{ matrix.os }} 67 | name: test ${{ matrix.os }} 68 | strategy: 69 | fail-fast: false 70 | matrix: 71 | os: [macos-latest, windows-latest] 72 | steps: 73 | # if your project needs OpenSSL, uncomment this to fix Windows builds. 74 | # it's commented out by default as the install command takes 5-10m. 75 | # - run: echo "VCPKG_ROOT=$env:VCPKG_INSTALLATION_ROOT" | Out-File -FilePath $env:GITHUB_ENV -Append 76 | # if: runner.os == 'Windows' 77 | # - run: vcpkg install openssl:x64-windows-static-md 78 | # if: runner.os == 'Windows' 79 | - uses: actions/checkout@v4 80 | - name: Install Rust 81 | uses: dtolnay/rust-toolchain@stable 82 | # enable this ci template to run regardless of whether the lockfile is checked in or not 83 | - name: cargo generate-lockfile 84 | if: hashFiles('Cargo.lock') == '' 85 | run: cargo generate-lockfile 86 | - name: cargo test --locked 87 | run: cargo test --locked --all-features --all-targets 88 | - name: Cache Cargo dependencies 89 | uses: Swatinem/rust-cache@v2 90 | -------------------------------------------------------------------------------- /.github/workflows/release.yml: -------------------------------------------------------------------------------- 1 | # This file was autogenerated by cargo-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 cargo-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 cargo-dist-able). 28 | # 29 | # If PACKAGE_NAME isn't specified, then the announcement will be for all 30 | # (cargo-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 'cargo 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 cargo-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.22.1/cargo-dist-installer.sh | sh" 67 | - name: Cache cargo-dist 68 | uses: actions/upload-artifact@v4 69 | with: 70 | name: cargo-dist-cache 71 | path: ~/.cargo/bin/cargo-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 | cargo dist ${{ (!github.event.pull_request && format('host --steps=create --tag={0}', github.ref_name)) || 'plan' }} --output-format=json > plan-dist-manifest.json 80 | echo "cargo 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 cargo-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 cargo dist 103 | # - install-dist: expression to run to install cargo-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 | env: 111 | GH_TOKEN: ${{ secrets.GITHUB_TOKEN }} 112 | BUILD_MANIFEST_NAME: target/distrib/${{ join(matrix.targets, '-') }}-dist-manifest.json 113 | steps: 114 | - name: enable windows longpaths 115 | run: | 116 | git config --global core.longpaths true 117 | - uses: actions/checkout@v4 118 | with: 119 | submodules: recursive 120 | - name: Install cargo-dist 121 | run: ${{ matrix.install_dist }} 122 | # Get the dist-manifest 123 | - name: Fetch local artifacts 124 | uses: actions/download-artifact@v4 125 | with: 126 | pattern: artifacts-* 127 | path: target/distrib/ 128 | merge-multiple: true 129 | - name: Install dependencies 130 | run: | 131 | ${{ matrix.packages_install }} 132 | - name: Build artifacts 133 | run: | 134 | # Actually do builds and make zips and whatnot 135 | cargo dist build ${{ needs.plan.outputs.tag-flag }} --print=linkage --output-format=json ${{ matrix.dist_args }} > dist-manifest.json 136 | echo "cargo dist ran successfully" 137 | - id: cargo-dist 138 | name: Post-build 139 | # We force bash here just because github makes it really hard to get values up 140 | # to "real" actions without writing to env-vars, and writing to env-vars has 141 | # inconsistent syntax between shell and powershell. 142 | shell: bash 143 | run: | 144 | # Parse out what we just built and upload it to scratch storage 145 | echo "paths<> "$GITHUB_OUTPUT" 146 | jq --raw-output ".upload_files[]" dist-manifest.json >> "$GITHUB_OUTPUT" 147 | echo "EOF" >> "$GITHUB_OUTPUT" 148 | 149 | cp dist-manifest.json "$BUILD_MANIFEST_NAME" 150 | - name: "Upload artifacts" 151 | uses: actions/upload-artifact@v4 152 | with: 153 | name: artifacts-build-local-${{ join(matrix.targets, '_') }} 154 | path: | 155 | ${{ steps.cargo-dist.outputs.paths }} 156 | ${{ env.BUILD_MANIFEST_NAME }} 157 | 158 | # Build and package all the platform-agnostic(ish) things 159 | build-global-artifacts: 160 | needs: 161 | - plan 162 | - build-local-artifacts 163 | runs-on: "ubuntu-20.04" 164 | env: 165 | GH_TOKEN: ${{ secrets.GITHUB_TOKEN }} 166 | BUILD_MANIFEST_NAME: target/distrib/global-dist-manifest.json 167 | steps: 168 | - uses: actions/checkout@v4 169 | with: 170 | submodules: recursive 171 | - name: Install cached cargo-dist 172 | uses: actions/download-artifact@v4 173 | with: 174 | name: cargo-dist-cache 175 | path: ~/.cargo/bin/ 176 | - run: chmod +x ~/.cargo/bin/cargo-dist 177 | # Get all the local artifacts for the global tasks to use (for e.g. checksums) 178 | - name: Fetch local artifacts 179 | uses: actions/download-artifact@v4 180 | with: 181 | pattern: artifacts-* 182 | path: target/distrib/ 183 | merge-multiple: true 184 | - id: cargo-dist 185 | shell: bash 186 | run: | 187 | cargo dist build ${{ needs.plan.outputs.tag-flag }} --output-format=json "--artifacts=global" > dist-manifest.json 188 | echo "cargo dist ran successfully" 189 | 190 | # Parse out what we just built and upload it to scratch storage 191 | echo "paths<> "$GITHUB_OUTPUT" 192 | jq --raw-output ".upload_files[]" dist-manifest.json >> "$GITHUB_OUTPUT" 193 | echo "EOF" >> "$GITHUB_OUTPUT" 194 | 195 | cp dist-manifest.json "$BUILD_MANIFEST_NAME" 196 | - name: "Upload artifacts" 197 | uses: actions/upload-artifact@v4 198 | with: 199 | name: artifacts-build-global 200 | path: | 201 | ${{ steps.cargo-dist.outputs.paths }} 202 | ${{ env.BUILD_MANIFEST_NAME }} 203 | # Determines if we should publish/announce 204 | host: 205 | needs: 206 | - plan 207 | - build-local-artifacts 208 | - build-global-artifacts 209 | # Only run if we're "publishing", and only if local and global didn't fail (skipped is fine) 210 | 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') }} 211 | env: 212 | GH_TOKEN: ${{ secrets.GITHUB_TOKEN }} 213 | runs-on: "ubuntu-20.04" 214 | outputs: 215 | val: ${{ steps.host.outputs.manifest }} 216 | steps: 217 | - uses: actions/checkout@v4 218 | with: 219 | submodules: recursive 220 | - name: Install cached cargo-dist 221 | uses: actions/download-artifact@v4 222 | with: 223 | name: cargo-dist-cache 224 | path: ~/.cargo/bin/ 225 | - run: chmod +x ~/.cargo/bin/cargo-dist 226 | # Fetch artifacts from scratch-storage 227 | - name: Fetch artifacts 228 | uses: actions/download-artifact@v4 229 | with: 230 | pattern: artifacts-* 231 | path: target/distrib/ 232 | merge-multiple: true 233 | - id: host 234 | shell: bash 235 | run: | 236 | cargo dist host ${{ needs.plan.outputs.tag-flag }} --steps=upload --steps=release --output-format=json > dist-manifest.json 237 | echo "artifacts uploaded and released successfully" 238 | cat dist-manifest.json 239 | echo "manifest=$(jq -c "." dist-manifest.json)" >> "$GITHUB_OUTPUT" 240 | - name: "Upload dist-manifest.json" 241 | uses: actions/upload-artifact@v4 242 | with: 243 | # Overwrite the previous copy 244 | name: artifacts-dist-manifest 245 | path: dist-manifest.json 246 | # Create a GitHub Release while uploading all files to it 247 | - name: "Download GitHub Artifacts" 248 | uses: actions/download-artifact@v4 249 | with: 250 | pattern: artifacts-* 251 | path: artifacts 252 | merge-multiple: true 253 | - name: Cleanup 254 | run: | 255 | # Remove the granular manifests 256 | rm -f artifacts/*-dist-manifest.json 257 | - name: Create GitHub Release 258 | env: 259 | PRERELEASE_FLAG: "${{ fromJson(steps.host.outputs.manifest).announcement_is_prerelease && '--prerelease' || '' }}" 260 | ANNOUNCEMENT_TITLE: "${{ fromJson(steps.host.outputs.manifest).announcement_title }}" 261 | ANNOUNCEMENT_BODY: "${{ fromJson(steps.host.outputs.manifest).announcement_github_body }}" 262 | RELEASE_COMMIT: "${{ github.sha }}" 263 | run: | 264 | # Write and read notes from a file to avoid quoting breaking things 265 | echo "$ANNOUNCEMENT_BODY" > $RUNNER_TEMP/notes.txt 266 | 267 | gh release create "${{ needs.plan.outputs.tag }}" --target "$RELEASE_COMMIT" $PRERELEASE_FLAG --title "$ANNOUNCEMENT_TITLE" --notes-file "$RUNNER_TEMP/notes.txt" artifacts/* 268 | 269 | publish-homebrew-formula: 270 | needs: 271 | - plan 272 | - host 273 | runs-on: "ubuntu-20.04" 274 | env: 275 | GH_TOKEN: ${{ secrets.GITHUB_TOKEN }} 276 | PLAN: ${{ needs.plan.outputs.val }} 277 | GITHUB_USER: "axo bot" 278 | GITHUB_EMAIL: "admin+bot@axo.dev" 279 | if: ${{ !fromJson(needs.plan.outputs.val).announcement_is_prerelease || fromJson(needs.plan.outputs.val).publish_prereleases }} 280 | steps: 281 | - uses: actions/checkout@v4 282 | with: 283 | repository: "kmj-007/homebrew-lazygh" 284 | token: ${{ secrets.HOMEBREW_TAP_TOKEN }} 285 | # So we have access to the formula 286 | - name: Fetch homebrew formulae 287 | uses: actions/download-artifact@v4 288 | with: 289 | pattern: artifacts-* 290 | path: Formula/ 291 | merge-multiple: true 292 | # This is extra complex because you can make your Formula name not match your app name 293 | # so we need to find releases with a *.rb file, and publish with that filename. 294 | - name: Commit formula files 295 | run: | 296 | git config --global user.name "${GITHUB_USER}" 297 | git config --global user.email "${GITHUB_EMAIL}" 298 | 299 | for release in $(echo "$PLAN" | jq --compact-output '.releases[] | select([.artifacts[] | endswith(".rb")] | any)'); do 300 | filename=$(echo "$release" | jq '.artifacts[] | select(endswith(".rb"))' --raw-output) 301 | name=$(echo "$filename" | sed "s/\.rb$//") 302 | version=$(echo "$release" | jq .app_version --raw-output) 303 | 304 | export PATH="/home/linuxbrew/.linuxbrew/bin:$PATH" 305 | brew update 306 | # We avoid reformatting user-provided data such as the app description and homepage. 307 | brew style --except-cops FormulaAudit/Homepage,FormulaAudit/Desc,FormulaAuditStrict --fix "Formula/${filename}" || true 308 | 309 | git add "Formula/${filename}" 310 | git commit -m "${name} ${version}" 311 | done 312 | git push 313 | 314 | announce: 315 | needs: 316 | - plan 317 | - host 318 | - publish-homebrew-formula 319 | # use "always() && ..." to allow us to wait for all publish jobs while 320 | # still allowing individual publish jobs to skip themselves (for prereleases). 321 | # "host" however must run to completion, no skipping allowed! 322 | if: ${{ always() && needs.host.result == 'success' && (needs.publish-homebrew-formula.result == 'skipped' || needs.publish-homebrew-formula.result == 'success') }} 323 | runs-on: "ubuntu-20.04" 324 | env: 325 | GH_TOKEN: ${{ secrets.GITHUB_TOKEN }} 326 | steps: 327 | - uses: actions/checkout@v4 328 | with: 329 | submodules: recursive 330 | -------------------------------------------------------------------------------- /.github/workflows/web.yml: -------------------------------------------------------------------------------- 1 | # Workflow to build your docs with oranda (and mdbook) 2 | # and deploy them to Github Pages 3 | name: Web 4 | 5 | # We're going to push to the gh-pages branch, so we need that permission 6 | permissions: 7 | contents: write 8 | 9 | # What situations do we want to build docs in? 10 | # All of these work independently and can be removed / commented out 11 | # if you don't want oranda/mdbook running in that situation 12 | on: 13 | # Check that a PR didn't break docs! 14 | # 15 | # Note that the "Deploy to Github Pages" step won't run in this mode, 16 | # so this won't have any side-effects. But it will tell you if a PR 17 | # completely broke oranda/mdbook. Sadly we don't provide previews (yet)! 18 | pull_request: 19 | 20 | # Whenever something gets pushed to main, update the docs! 21 | # This is great for getting docs changes live without cutting a full release. 22 | # 23 | # Note that if you're using cargo-dist, this will "race" the Release workflow 24 | # that actually builds the Github Release that oranda tries to read (and 25 | # this will almost certainly complete first). As a result you will publish 26 | # docs for the latest commit but the oranda landing page won't know about 27 | # the latest release. The workflow_run trigger below will properly wait for 28 | # cargo-dist, and so this half-published state will only last for ~10 minutes. 29 | # 30 | # If you only want docs to update with releases, disable this, or change it to 31 | # a "release" branch. You can, of course, also manually trigger a workflow run 32 | # when you want the docs to update. 33 | push: 34 | branches: 35 | - main 36 | 37 | # Whenever a workflow called "Release" completes, update the docs! 38 | # 39 | # If you're using cargo-dist, this is recommended, as it will ensure that 40 | # oranda always sees the latest release right when it's available. Note 41 | # however that Github's UI is wonky when you use workflow_run, and won't 42 | # show this workflow as part of any commit. You have to go to the "actions" 43 | # tab for your repo to see this one running (the gh-pages deploy will also 44 | # only show up there). 45 | workflow_run: 46 | workflows: [ "Release" ] 47 | types: 48 | - completed 49 | 50 | # Alright, let's do it! 51 | jobs: 52 | web: 53 | name: Build and deploy site and docs 54 | runs-on: ubuntu-latest 55 | steps: 56 | # Setup 57 | - uses: actions/checkout@v3 58 | with: 59 | fetch-depth: 0 60 | - uses: dtolnay/rust-toolchain@stable 61 | - uses: swatinem/rust-cache@v2 62 | 63 | # If you use any mdbook plugins, here's the place to install them! 64 | 65 | # Install and run oranda (and mdbook)! 66 | # 67 | # This will write all output to ./public/ (including copying mdbook's output to there). 68 | - name: Install and run oranda 69 | run: | 70 | curl --proto '=https' --tlsv1.2 -LsSf https://github.com/axodotdev/oranda/releases/download/v0.6.1/oranda-installer.sh | sh 71 | oranda build 72 | 73 | - name: Prepare HTML for link checking 74 | # untitaker/hyperlink supports no site prefixes, move entire site into 75 | # a subfolder 76 | run: mkdir /tmp/public/ && cp -R public /tmp/public/oranda 77 | 78 | # Deploy to our gh-pages branch (creating it if it doesn't exist). 79 | # The "public" dir that oranda made above will become the root dir 80 | # of this branch. 81 | # 82 | # Note that once the gh-pages branch exists, you must 83 | # go into repo's settings > pages and set "deploy from branch: gh-pages". 84 | # The other defaults work fine. 85 | - name: Deploy to Github Pages 86 | uses: JamesIves/github-pages-deploy-action@v4.7.2 87 | # ONLY if we're on main (so no PRs or feature branches allowed!) 88 | if: ${{ github.ref == 'refs/heads/main' }} 89 | with: 90 | branch: gh-pages 91 | # Gotta tell the action where to find oranda's output 92 | folder: public 93 | token: ${{ secrets.GITHUB_TOKEN }} 94 | single-commit: true -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | /target 2 | # Generated by `oranda generate ci` 3 | public/ -------------------------------------------------------------------------------- /Cargo.lock: -------------------------------------------------------------------------------- 1 | # This file is automatically @generated by Cargo. 2 | # It is not intended for manual editing. 3 | version = 4 4 | 5 | [[package]] 6 | name = "addr2line" 7 | version = "0.21.0" 8 | source = "registry+https://github.com/rust-lang/crates.io-index" 9 | checksum = "8a30b2e23b9e17a9f90641c7ab1549cd9b44f296d3ccbf309d2863cfe398a0cb" 10 | dependencies = [ 11 | "gimli", 12 | ] 13 | 14 | [[package]] 15 | name = "adler" 16 | version = "1.0.2" 17 | source = "registry+https://github.com/rust-lang/crates.io-index" 18 | checksum = "f26201604c87b1e01bd3d98f8d5d9a8fcbb815e8cedb41ffccbeb4bf593a35fe" 19 | 20 | [[package]] 21 | name = "adler2" 22 | version = "2.0.0" 23 | source = "registry+https://github.com/rust-lang/crates.io-index" 24 | checksum = "512761e0bb2578dd7380c6baaa0f4ce03e84f95e960231d1dec8bf4d7d6e2627" 25 | 26 | [[package]] 27 | name = "ahash" 28 | version = "0.8.11" 29 | source = "registry+https://github.com/rust-lang/crates.io-index" 30 | checksum = "e89da841a80418a9b391ebaea17f5c112ffaaa96f621d2c285b5174da76b9011" 31 | dependencies = [ 32 | "cfg-if", 33 | "once_cell", 34 | "version_check", 35 | "zerocopy", 36 | ] 37 | 38 | [[package]] 39 | name = "aho-corasick" 40 | version = "1.1.3" 41 | source = "registry+https://github.com/rust-lang/crates.io-index" 42 | checksum = "8e60d3430d3a69478ad0993f19238d2df97c507009a52b3c10addcd7f6bcb916" 43 | dependencies = [ 44 | "memchr", 45 | ] 46 | 47 | [[package]] 48 | name = "allocator-api2" 49 | version = "0.2.18" 50 | source = "registry+https://github.com/rust-lang/crates.io-index" 51 | checksum = "5c6cb57a04249c6480766f7f7cef5467412af1490f8d1e243141daddada3264f" 52 | 53 | [[package]] 54 | name = "arboard" 55 | version = "3.5.0" 56 | source = "registry+https://github.com/rust-lang/crates.io-index" 57 | checksum = "c1df21f715862ede32a0c525ce2ca4d52626bb0007f8c18b87a384503ac33e70" 58 | dependencies = [ 59 | "clipboard-win", 60 | "image", 61 | "log", 62 | "objc2", 63 | "objc2-app-kit", 64 | "objc2-core-foundation", 65 | "objc2-core-graphics", 66 | "objc2-foundation", 67 | "parking_lot", 68 | "percent-encoding", 69 | "windows-sys", 70 | "x11rb", 71 | ] 72 | 73 | [[package]] 74 | name = "autocfg" 75 | version = "1.3.0" 76 | source = "registry+https://github.com/rust-lang/crates.io-index" 77 | checksum = "0c4b4d0bd25bd0b74681c0ad21497610ce1b7c91b1022cd21c80c6fbdd9476b0" 78 | 79 | [[package]] 80 | name = "backtrace" 81 | version = "0.3.71" 82 | source = "registry+https://github.com/rust-lang/crates.io-index" 83 | checksum = "26b05800d2e817c8b3b4b54abd461726265fa9789ae34330622f2db9ee696f9d" 84 | dependencies = [ 85 | "addr2line", 86 | "cc", 87 | "cfg-if", 88 | "libc", 89 | "miniz_oxide 0.7.4", 90 | "object", 91 | "rustc-demangle", 92 | ] 93 | 94 | [[package]] 95 | name = "bitflags" 96 | version = "1.3.2" 97 | source = "registry+https://github.com/rust-lang/crates.io-index" 98 | checksum = "bef38d45163c2f1dde094a7dfd33ccf595c92905c8f8f4fdc18d06fb1037718a" 99 | 100 | [[package]] 101 | name = "bitflags" 102 | version = "2.6.0" 103 | source = "registry+https://github.com/rust-lang/crates.io-index" 104 | checksum = "b048fb63fd8b5923fc5aa7b340d8e156aec7ec02f0c78fa8a6ddc2613f6f71de" 105 | 106 | [[package]] 107 | name = "bytemuck" 108 | version = "1.18.0" 109 | source = "registry+https://github.com/rust-lang/crates.io-index" 110 | checksum = "94bbb0ad554ad961ddc5da507a12a29b14e4ae5bda06b19f575a3e6079d2e2ae" 111 | 112 | [[package]] 113 | name = "byteorder-lite" 114 | version = "0.1.0" 115 | source = "registry+https://github.com/rust-lang/crates.io-index" 116 | checksum = "8f1fe948ff07f4bd06c30984e69f5b4899c516a3ef74f34df92a2df2ab535495" 117 | 118 | [[package]] 119 | name = "cassowary" 120 | version = "0.3.0" 121 | source = "registry+https://github.com/rust-lang/crates.io-index" 122 | checksum = "df8670b8c7b9dae1793364eafadf7239c40d669904660c5960d74cfd80b46a53" 123 | 124 | [[package]] 125 | name = "castaway" 126 | version = "0.2.3" 127 | source = "registry+https://github.com/rust-lang/crates.io-index" 128 | checksum = "0abae9be0aaf9ea96a3b1b8b1b55c602ca751eba1b1500220cea4ecbafe7c0d5" 129 | dependencies = [ 130 | "rustversion", 131 | ] 132 | 133 | [[package]] 134 | name = "cc" 135 | version = "1.1.19" 136 | source = "registry+https://github.com/rust-lang/crates.io-index" 137 | checksum = "2d74707dde2ba56f86ae90effb3b43ddd369504387e718014de010cec7959800" 138 | dependencies = [ 139 | "shlex", 140 | ] 141 | 142 | [[package]] 143 | name = "cfg-if" 144 | version = "1.0.0" 145 | source = "registry+https://github.com/rust-lang/crates.io-index" 146 | checksum = "baf1de4339761588bc0619e3cbc0120ee582ebb74b53b4efbf79117bd2da40fd" 147 | 148 | [[package]] 149 | name = "clipboard-win" 150 | version = "5.4.0" 151 | source = "registry+https://github.com/rust-lang/crates.io-index" 152 | checksum = "15efe7a882b08f34e38556b14f2fb3daa98769d06c7f0c1b076dfd0d983bc892" 153 | dependencies = [ 154 | "error-code", 155 | ] 156 | 157 | [[package]] 158 | name = "color-eyre" 159 | version = "0.6.3" 160 | source = "registry+https://github.com/rust-lang/crates.io-index" 161 | checksum = "55146f5e46f237f7423d74111267d4597b59b0dad0ffaf7303bce9945d843ad5" 162 | dependencies = [ 163 | "backtrace", 164 | "color-spantrace", 165 | "eyre", 166 | "indenter", 167 | "once_cell", 168 | "owo-colors", 169 | "tracing-error", 170 | ] 171 | 172 | [[package]] 173 | name = "color-spantrace" 174 | version = "0.2.1" 175 | source = "registry+https://github.com/rust-lang/crates.io-index" 176 | checksum = "cd6be1b2a7e382e2b98b43b2adcca6bb0e465af0bdd38123873ae61eb17a72c2" 177 | dependencies = [ 178 | "once_cell", 179 | "owo-colors", 180 | "tracing-core", 181 | "tracing-error", 182 | ] 183 | 184 | [[package]] 185 | name = "compact_str" 186 | version = "0.8.0" 187 | source = "registry+https://github.com/rust-lang/crates.io-index" 188 | checksum = "6050c3a16ddab2e412160b31f2c871015704239bca62f72f6e5f0be631d3f644" 189 | dependencies = [ 190 | "castaway", 191 | "cfg-if", 192 | "itoa", 193 | "rustversion", 194 | "ryu", 195 | "static_assertions", 196 | ] 197 | 198 | [[package]] 199 | name = "crc32fast" 200 | version = "1.4.2" 201 | source = "registry+https://github.com/rust-lang/crates.io-index" 202 | checksum = "a97769d94ddab943e4510d138150169a2758b5ef3eb191a9ee688de3e23ef7b3" 203 | dependencies = [ 204 | "cfg-if", 205 | ] 206 | 207 | [[package]] 208 | name = "crossterm" 209 | version = "0.28.1" 210 | source = "registry+https://github.com/rust-lang/crates.io-index" 211 | checksum = "829d955a0bb380ef178a640b91779e3987da38c9aea133b20614cfed8cdea9c6" 212 | dependencies = [ 213 | "bitflags 2.6.0", 214 | "crossterm_winapi", 215 | "mio", 216 | "parking_lot", 217 | "rustix", 218 | "signal-hook", 219 | "signal-hook-mio", 220 | "winapi", 221 | ] 222 | 223 | [[package]] 224 | name = "crossterm_winapi" 225 | version = "0.9.1" 226 | source = "registry+https://github.com/rust-lang/crates.io-index" 227 | checksum = "acdd7c62a3665c7f6830a51635d9ac9b23ed385797f70a83bb8bafe9c572ab2b" 228 | dependencies = [ 229 | "winapi", 230 | ] 231 | 232 | [[package]] 233 | name = "dirs" 234 | version = "4.0.0" 235 | source = "registry+https://github.com/rust-lang/crates.io-index" 236 | checksum = "ca3aa72a6f96ea37bbc5aa912f6788242832f75369bdfdadcb0e38423f100059" 237 | dependencies = [ 238 | "dirs-sys", 239 | ] 240 | 241 | [[package]] 242 | name = "dirs-sys" 243 | version = "0.3.7" 244 | source = "registry+https://github.com/rust-lang/crates.io-index" 245 | checksum = "1b1d1d91c932ef41c0f2663aa8b0ca0342d444d842c06914aa0a7e352d0bada6" 246 | dependencies = [ 247 | "libc", 248 | "redox_users", 249 | "winapi", 250 | ] 251 | 252 | [[package]] 253 | name = "either" 254 | version = "1.13.0" 255 | source = "registry+https://github.com/rust-lang/crates.io-index" 256 | checksum = "60b1af1c220855b6ceac025d3f6ecdd2b7c4894bfe9cd9bda4fbb4bc7c0d4cf0" 257 | 258 | [[package]] 259 | name = "errno" 260 | version = "0.3.9" 261 | source = "registry+https://github.com/rust-lang/crates.io-index" 262 | checksum = "534c5cf6194dfab3db3242765c03bbe257cf92f22b38f6bc0c58d59108a820ba" 263 | dependencies = [ 264 | "libc", 265 | "windows-sys", 266 | ] 267 | 268 | [[package]] 269 | name = "error-code" 270 | version = "3.3.1" 271 | source = "registry+https://github.com/rust-lang/crates.io-index" 272 | checksum = "a5d9305ccc6942a704f4335694ecd3de2ea531b114ac2d51f5f843750787a92f" 273 | 274 | [[package]] 275 | name = "eyre" 276 | version = "0.6.12" 277 | source = "registry+https://github.com/rust-lang/crates.io-index" 278 | checksum = "7cd915d99f24784cdc19fd37ef22b97e3ff0ae756c7e492e9fbfe897d61e2aec" 279 | dependencies = [ 280 | "indenter", 281 | "once_cell", 282 | ] 283 | 284 | [[package]] 285 | name = "fallible-iterator" 286 | version = "0.2.0" 287 | source = "registry+https://github.com/rust-lang/crates.io-index" 288 | checksum = "4443176a9f2c162692bd3d352d745ef9413eec5782a80d8fd6f8a1ac692a07f7" 289 | 290 | [[package]] 291 | name = "fallible-streaming-iterator" 292 | version = "0.1.9" 293 | source = "registry+https://github.com/rust-lang/crates.io-index" 294 | checksum = "7360491ce676a36bf9bb3c56c1aa791658183a54d2744120f27285738d90465a" 295 | 296 | [[package]] 297 | name = "fdeflate" 298 | version = "0.3.4" 299 | source = "registry+https://github.com/rust-lang/crates.io-index" 300 | checksum = "4f9bfee30e4dedf0ab8b422f03af778d9612b63f502710fc500a334ebe2de645" 301 | dependencies = [ 302 | "simd-adler32", 303 | ] 304 | 305 | [[package]] 306 | name = "flate2" 307 | version = "1.0.33" 308 | source = "registry+https://github.com/rust-lang/crates.io-index" 309 | checksum = "324a1be68054ef05ad64b861cc9eaf1d623d2d8cb25b4bf2cb9cdd902b4bf253" 310 | dependencies = [ 311 | "crc32fast", 312 | "miniz_oxide 0.8.0", 313 | ] 314 | 315 | [[package]] 316 | name = "gethostname" 317 | version = "0.4.3" 318 | source = "registry+https://github.com/rust-lang/crates.io-index" 319 | checksum = "0176e0459c2e4a1fe232f984bca6890e681076abb9934f6cea7c326f3fc47818" 320 | dependencies = [ 321 | "libc", 322 | "windows-targets 0.48.5", 323 | ] 324 | 325 | [[package]] 326 | name = "getrandom" 327 | version = "0.2.15" 328 | source = "registry+https://github.com/rust-lang/crates.io-index" 329 | checksum = "c4567c8db10ae91089c99af84c68c38da3ec2f087c3f82960bcdbf3656b6f4d7" 330 | dependencies = [ 331 | "cfg-if", 332 | "libc", 333 | "wasi", 334 | ] 335 | 336 | [[package]] 337 | name = "gimli" 338 | version = "0.28.1" 339 | source = "registry+https://github.com/rust-lang/crates.io-index" 340 | checksum = "4271d37baee1b8c7e4b708028c57d816cf9d2434acb33a549475f78c181f6253" 341 | 342 | [[package]] 343 | name = "hashbrown" 344 | version = "0.14.5" 345 | source = "registry+https://github.com/rust-lang/crates.io-index" 346 | checksum = "e5274423e17b7c9fc20b6e7e208532f9b19825d82dfd615708b70edd83df41f1" 347 | dependencies = [ 348 | "ahash", 349 | "allocator-api2", 350 | ] 351 | 352 | [[package]] 353 | name = "hashlink" 354 | version = "0.8.4" 355 | source = "registry+https://github.com/rust-lang/crates.io-index" 356 | checksum = "e8094feaf31ff591f651a2664fb9cfd92bba7a60ce3197265e9482ebe753c8f7" 357 | dependencies = [ 358 | "hashbrown", 359 | ] 360 | 361 | [[package]] 362 | name = "heck" 363 | version = "0.5.0" 364 | source = "registry+https://github.com/rust-lang/crates.io-index" 365 | checksum = "2304e00983f87ffb38b55b444b5e3b60a884b5d30c0fca7d82fe33449bbe55ea" 366 | 367 | [[package]] 368 | name = "hermit-abi" 369 | version = "0.3.9" 370 | source = "registry+https://github.com/rust-lang/crates.io-index" 371 | checksum = "d231dfb89cfffdbc30e7fc41579ed6066ad03abda9e567ccafae602b97ec5024" 372 | 373 | [[package]] 374 | name = "image" 375 | version = "0.25.2" 376 | source = "registry+https://github.com/rust-lang/crates.io-index" 377 | checksum = "99314c8a2152b8ddb211f924cdae532d8c5e4c8bb54728e12fff1b0cd5963a10" 378 | dependencies = [ 379 | "bytemuck", 380 | "byteorder-lite", 381 | "num-traits", 382 | "png", 383 | "tiff", 384 | ] 385 | 386 | [[package]] 387 | name = "indenter" 388 | version = "0.3.3" 389 | source = "registry+https://github.com/rust-lang/crates.io-index" 390 | checksum = "ce23b50ad8242c51a442f3ff322d56b02f08852c77e4c0b4d3fd684abc89c683" 391 | 392 | [[package]] 393 | name = "indoc" 394 | version = "2.0.5" 395 | source = "registry+https://github.com/rust-lang/crates.io-index" 396 | checksum = "b248f5224d1d606005e02c97f5aa4e88eeb230488bcc03bc9ca4d7991399f2b5" 397 | 398 | [[package]] 399 | name = "instability" 400 | version = "0.3.2" 401 | source = "registry+https://github.com/rust-lang/crates.io-index" 402 | checksum = "b23a0c8dfe501baac4adf6ebbfa6eddf8f0c07f56b058cc1288017e32397846c" 403 | dependencies = [ 404 | "quote", 405 | "syn", 406 | ] 407 | 408 | [[package]] 409 | name = "itertools" 410 | version = "0.13.0" 411 | source = "registry+https://github.com/rust-lang/crates.io-index" 412 | checksum = "413ee7dfc52ee1a4949ceeb7dbc8a33f2d6c088194d9f922fb8318faf1f01186" 413 | dependencies = [ 414 | "either", 415 | ] 416 | 417 | [[package]] 418 | name = "itoa" 419 | version = "1.0.11" 420 | source = "registry+https://github.com/rust-lang/crates.io-index" 421 | checksum = "49f1f14873335454500d59611f1cf4a4b0f786f9ac11f4312a78e4cf2566695b" 422 | 423 | [[package]] 424 | name = "jpeg-decoder" 425 | version = "0.3.1" 426 | source = "registry+https://github.com/rust-lang/crates.io-index" 427 | checksum = "f5d4a7da358eff58addd2877a45865158f0d78c911d43a5784ceb7bbf52833b0" 428 | 429 | [[package]] 430 | name = "lazy_static" 431 | version = "1.5.0" 432 | source = "registry+https://github.com/rust-lang/crates.io-index" 433 | checksum = "bbd2bcb4c963f2ddae06a2efc7e9f3591312473c50c6685e1f298068316e66fe" 434 | 435 | [[package]] 436 | name = "lazygh" 437 | version = "0.5.0" 438 | dependencies = [ 439 | "arboard", 440 | "color-eyre", 441 | "crossterm", 442 | "dirs", 443 | "ratatui", 444 | "regex", 445 | "rusqlite", 446 | ] 447 | 448 | [[package]] 449 | name = "libc" 450 | version = "0.2.158" 451 | source = "registry+https://github.com/rust-lang/crates.io-index" 452 | checksum = "d8adc4bb1803a324070e64a98ae98f38934d91957a99cfb3a43dcbc01bc56439" 453 | 454 | [[package]] 455 | name = "libredox" 456 | version = "0.1.3" 457 | source = "registry+https://github.com/rust-lang/crates.io-index" 458 | checksum = "c0ff37bd590ca25063e35af745c343cb7a0271906fb7b37e4813e8f79f00268d" 459 | dependencies = [ 460 | "bitflags 2.6.0", 461 | "libc", 462 | ] 463 | 464 | [[package]] 465 | name = "libsqlite3-sys" 466 | version = "0.25.2" 467 | source = "registry+https://github.com/rust-lang/crates.io-index" 468 | checksum = "29f835d03d717946d28b1d1ed632eb6f0e24a299388ee623d0c23118d3e8a7fa" 469 | dependencies = [ 470 | "cc", 471 | "pkg-config", 472 | "vcpkg", 473 | ] 474 | 475 | [[package]] 476 | name = "linux-raw-sys" 477 | version = "0.4.14" 478 | source = "registry+https://github.com/rust-lang/crates.io-index" 479 | checksum = "78b3ae25bc7c8c38cec158d1f2757ee79e9b3740fbc7ccf0e59e4b08d793fa89" 480 | 481 | [[package]] 482 | name = "lock_api" 483 | version = "0.4.12" 484 | source = "registry+https://github.com/rust-lang/crates.io-index" 485 | checksum = "07af8b9cdd281b7915f413fa73f29ebd5d55d0d3f0155584dade1ff18cea1b17" 486 | dependencies = [ 487 | "autocfg", 488 | "scopeguard", 489 | ] 490 | 491 | [[package]] 492 | name = "log" 493 | version = "0.4.22" 494 | source = "registry+https://github.com/rust-lang/crates.io-index" 495 | checksum = "a7a70ba024b9dc04c27ea2f0c0548feb474ec5c54bba33a7f72f873a39d07b24" 496 | 497 | [[package]] 498 | name = "lru" 499 | version = "0.12.4" 500 | source = "registry+https://github.com/rust-lang/crates.io-index" 501 | checksum = "37ee39891760e7d94734f6f63fedc29a2e4a152f836120753a72503f09fcf904" 502 | dependencies = [ 503 | "hashbrown", 504 | ] 505 | 506 | [[package]] 507 | name = "memchr" 508 | version = "2.7.4" 509 | source = "registry+https://github.com/rust-lang/crates.io-index" 510 | checksum = "78ca9ab1a0babb1e7d5695e3530886289c18cf2f87ec19a575a0abdce112e3a3" 511 | 512 | [[package]] 513 | name = "miniz_oxide" 514 | version = "0.7.4" 515 | source = "registry+https://github.com/rust-lang/crates.io-index" 516 | checksum = "b8a240ddb74feaf34a79a7add65a741f3167852fba007066dcac1ca548d89c08" 517 | dependencies = [ 518 | "adler", 519 | "simd-adler32", 520 | ] 521 | 522 | [[package]] 523 | name = "miniz_oxide" 524 | version = "0.8.0" 525 | source = "registry+https://github.com/rust-lang/crates.io-index" 526 | checksum = "e2d80299ef12ff69b16a84bb182e3b9df68b5a91574d3d4fa6e41b65deec4df1" 527 | dependencies = [ 528 | "adler2", 529 | ] 530 | 531 | [[package]] 532 | name = "mio" 533 | version = "1.0.2" 534 | source = "registry+https://github.com/rust-lang/crates.io-index" 535 | checksum = "80e04d1dcff3aae0704555fe5fee3bcfaf3d1fdf8a7e521d5b9d2b42acb52cec" 536 | dependencies = [ 537 | "hermit-abi", 538 | "libc", 539 | "log", 540 | "wasi", 541 | "windows-sys", 542 | ] 543 | 544 | [[package]] 545 | name = "num-traits" 546 | version = "0.2.19" 547 | source = "registry+https://github.com/rust-lang/crates.io-index" 548 | checksum = "071dfc062690e90b734c0b2273ce72ad0ffa95f0c74596bc250dcfd960262841" 549 | dependencies = [ 550 | "autocfg", 551 | ] 552 | 553 | [[package]] 554 | name = "objc2" 555 | version = "0.6.0" 556 | source = "registry+https://github.com/rust-lang/crates.io-index" 557 | checksum = "3531f65190d9cff863b77a99857e74c314dd16bf56c538c4b57c7cbc3f3a6e59" 558 | dependencies = [ 559 | "objc2-encode", 560 | ] 561 | 562 | [[package]] 563 | name = "objc2-app-kit" 564 | version = "0.3.0" 565 | source = "registry+https://github.com/rust-lang/crates.io-index" 566 | checksum = "5906f93257178e2f7ae069efb89fbd6ee94f0592740b5f8a1512ca498814d0fb" 567 | dependencies = [ 568 | "bitflags 2.6.0", 569 | "objc2", 570 | "objc2-core-graphics", 571 | "objc2-foundation", 572 | ] 573 | 574 | [[package]] 575 | name = "objc2-core-foundation" 576 | version = "0.3.0" 577 | source = "registry+https://github.com/rust-lang/crates.io-index" 578 | checksum = "daeaf60f25471d26948a1c2f840e3f7d86f4109e3af4e8e4b5cd70c39690d925" 579 | dependencies = [ 580 | "bitflags 2.6.0", 581 | "objc2", 582 | ] 583 | 584 | [[package]] 585 | name = "objc2-core-graphics" 586 | version = "0.3.0" 587 | source = "registry+https://github.com/rust-lang/crates.io-index" 588 | checksum = "f8dca602628b65356b6513290a21a6405b4d4027b8b250f0b98dddbb28b7de02" 589 | dependencies = [ 590 | "bitflags 2.6.0", 591 | "objc2", 592 | "objc2-core-foundation", 593 | "objc2-io-surface", 594 | ] 595 | 596 | [[package]] 597 | name = "objc2-encode" 598 | version = "4.1.0" 599 | source = "registry+https://github.com/rust-lang/crates.io-index" 600 | checksum = "ef25abbcd74fb2609453eb695bd2f860d389e457f67dc17cafc8b8cbc89d0c33" 601 | 602 | [[package]] 603 | name = "objc2-foundation" 604 | version = "0.3.0" 605 | source = "registry+https://github.com/rust-lang/crates.io-index" 606 | checksum = "3a21c6c9014b82c39515db5b396f91645182611c97d24637cf56ac01e5f8d998" 607 | dependencies = [ 608 | "bitflags 2.6.0", 609 | "objc2", 610 | "objc2-core-foundation", 611 | ] 612 | 613 | [[package]] 614 | name = "objc2-io-surface" 615 | version = "0.3.0" 616 | source = "registry+https://github.com/rust-lang/crates.io-index" 617 | checksum = "161a8b87e32610086e1a7a9e9ec39f84459db7b3a0881c1f16ca5a2605581c19" 618 | dependencies = [ 619 | "bitflags 2.6.0", 620 | "objc2", 621 | "objc2-core-foundation", 622 | ] 623 | 624 | [[package]] 625 | name = "object" 626 | version = "0.32.2" 627 | source = "registry+https://github.com/rust-lang/crates.io-index" 628 | checksum = "a6a622008b6e321afc04970976f62ee297fdbaa6f95318ca343e3eebb9648441" 629 | dependencies = [ 630 | "memchr", 631 | ] 632 | 633 | [[package]] 634 | name = "once_cell" 635 | version = "1.20.0" 636 | source = "registry+https://github.com/rust-lang/crates.io-index" 637 | checksum = "33ea5043e58958ee56f3e15a90aee535795cd7dfd319846288d93c5b57d85cbe" 638 | 639 | [[package]] 640 | name = "owo-colors" 641 | version = "3.5.0" 642 | source = "registry+https://github.com/rust-lang/crates.io-index" 643 | checksum = "c1b04fb49957986fdce4d6ee7a65027d55d4b6d2265e5848bbb507b58ccfdb6f" 644 | 645 | [[package]] 646 | name = "parking_lot" 647 | version = "0.12.3" 648 | source = "registry+https://github.com/rust-lang/crates.io-index" 649 | checksum = "f1bf18183cf54e8d6059647fc3063646a1801cf30896933ec2311622cc4b9a27" 650 | dependencies = [ 651 | "lock_api", 652 | "parking_lot_core", 653 | ] 654 | 655 | [[package]] 656 | name = "parking_lot_core" 657 | version = "0.9.10" 658 | source = "registry+https://github.com/rust-lang/crates.io-index" 659 | checksum = "1e401f977ab385c9e4e3ab30627d6f26d00e2c73eef317493c4ec6d468726cf8" 660 | dependencies = [ 661 | "cfg-if", 662 | "libc", 663 | "redox_syscall", 664 | "smallvec", 665 | "windows-targets 0.52.6", 666 | ] 667 | 668 | [[package]] 669 | name = "paste" 670 | version = "1.0.15" 671 | source = "registry+https://github.com/rust-lang/crates.io-index" 672 | checksum = "57c0d7b74b563b49d38dae00a0c37d4d6de9b432382b2892f0574ddcae73fd0a" 673 | 674 | [[package]] 675 | name = "percent-encoding" 676 | version = "2.3.1" 677 | source = "registry+https://github.com/rust-lang/crates.io-index" 678 | checksum = "e3148f5046208a5d56bcfc03053e3ca6334e51da8dfb19b6cdc8b306fae3283e" 679 | 680 | [[package]] 681 | name = "pin-project-lite" 682 | version = "0.2.14" 683 | source = "registry+https://github.com/rust-lang/crates.io-index" 684 | checksum = "bda66fc9667c18cb2758a2ac84d1167245054bcf85d5d1aaa6923f45801bdd02" 685 | 686 | [[package]] 687 | name = "pkg-config" 688 | version = "0.3.30" 689 | source = "registry+https://github.com/rust-lang/crates.io-index" 690 | checksum = "d231b230927b5e4ad203db57bbcbee2802f6bce620b1e4a9024a07d94e2907ec" 691 | 692 | [[package]] 693 | name = "png" 694 | version = "0.17.13" 695 | source = "registry+https://github.com/rust-lang/crates.io-index" 696 | checksum = "06e4b0d3d1312775e782c86c91a111aa1f910cbb65e1337f9975b5f9a554b5e1" 697 | dependencies = [ 698 | "bitflags 1.3.2", 699 | "crc32fast", 700 | "fdeflate", 701 | "flate2", 702 | "miniz_oxide 0.7.4", 703 | ] 704 | 705 | [[package]] 706 | name = "proc-macro2" 707 | version = "1.0.86" 708 | source = "registry+https://github.com/rust-lang/crates.io-index" 709 | checksum = "5e719e8df665df0d1c8fbfd238015744736151d4445ec0836b8e628aae103b77" 710 | dependencies = [ 711 | "unicode-ident", 712 | ] 713 | 714 | [[package]] 715 | name = "quote" 716 | version = "1.0.37" 717 | source = "registry+https://github.com/rust-lang/crates.io-index" 718 | checksum = "b5b9d34b8991d19d98081b46eacdd8eb58c6f2b201139f7c5f643cc155a633af" 719 | dependencies = [ 720 | "proc-macro2", 721 | ] 722 | 723 | [[package]] 724 | name = "ratatui" 725 | version = "0.29.0" 726 | source = "registry+https://github.com/rust-lang/crates.io-index" 727 | checksum = "eabd94c2f37801c20583fc49dd5cd6b0ba68c716787c2dd6ed18571e1e63117b" 728 | dependencies = [ 729 | "bitflags 2.6.0", 730 | "cassowary", 731 | "compact_str", 732 | "crossterm", 733 | "indoc", 734 | "instability", 735 | "itertools", 736 | "lru", 737 | "paste", 738 | "strum", 739 | "unicode-segmentation", 740 | "unicode-truncate", 741 | "unicode-width 0.2.0", 742 | ] 743 | 744 | [[package]] 745 | name = "redox_syscall" 746 | version = "0.5.4" 747 | source = "registry+https://github.com/rust-lang/crates.io-index" 748 | checksum = "0884ad60e090bf1345b93da0a5de8923c93884cd03f40dfcfddd3b4bee661853" 749 | dependencies = [ 750 | "bitflags 2.6.0", 751 | ] 752 | 753 | [[package]] 754 | name = "redox_users" 755 | version = "0.4.6" 756 | source = "registry+https://github.com/rust-lang/crates.io-index" 757 | checksum = "ba009ff324d1fc1b900bd1fdb31564febe58a8ccc8a6fdbb93b543d33b13ca43" 758 | dependencies = [ 759 | "getrandom", 760 | "libredox", 761 | "thiserror", 762 | ] 763 | 764 | [[package]] 765 | name = "regex" 766 | version = "1.11.1" 767 | source = "registry+https://github.com/rust-lang/crates.io-index" 768 | checksum = "b544ef1b4eac5dc2db33ea63606ae9ffcfac26c1416a2806ae0bf5f56b201191" 769 | dependencies = [ 770 | "aho-corasick", 771 | "memchr", 772 | "regex-automata", 773 | "regex-syntax", 774 | ] 775 | 776 | [[package]] 777 | name = "regex-automata" 778 | version = "0.4.8" 779 | source = "registry+https://github.com/rust-lang/crates.io-index" 780 | checksum = "368758f23274712b504848e9d5a6f010445cc8b87a7cdb4d7cbee666c1288da3" 781 | dependencies = [ 782 | "aho-corasick", 783 | "memchr", 784 | "regex-syntax", 785 | ] 786 | 787 | [[package]] 788 | name = "regex-syntax" 789 | version = "0.8.5" 790 | source = "registry+https://github.com/rust-lang/crates.io-index" 791 | checksum = "2b15c43186be67a4fd63bee50d0303afffcef381492ebe2c5d87f324e1b8815c" 792 | 793 | [[package]] 794 | name = "rusqlite" 795 | version = "0.28.0" 796 | source = "registry+https://github.com/rust-lang/crates.io-index" 797 | checksum = "01e213bc3ecb39ac32e81e51ebe31fd888a940515173e3a18a35f8c6e896422a" 798 | dependencies = [ 799 | "bitflags 1.3.2", 800 | "fallible-iterator", 801 | "fallible-streaming-iterator", 802 | "hashlink", 803 | "libsqlite3-sys", 804 | "smallvec", 805 | ] 806 | 807 | [[package]] 808 | name = "rustc-demangle" 809 | version = "0.1.24" 810 | source = "registry+https://github.com/rust-lang/crates.io-index" 811 | checksum = "719b953e2095829ee67db738b3bfa9fa368c94900df327b3f07fe6e794d2fe1f" 812 | 813 | [[package]] 814 | name = "rustix" 815 | version = "0.38.37" 816 | source = "registry+https://github.com/rust-lang/crates.io-index" 817 | checksum = "8acb788b847c24f28525660c4d7758620a7210875711f79e7f663cc152726811" 818 | dependencies = [ 819 | "bitflags 2.6.0", 820 | "errno", 821 | "libc", 822 | "linux-raw-sys", 823 | "windows-sys", 824 | ] 825 | 826 | [[package]] 827 | name = "rustversion" 828 | version = "1.0.17" 829 | source = "registry+https://github.com/rust-lang/crates.io-index" 830 | checksum = "955d28af4278de8121b7ebeb796b6a45735dc01436d898801014aced2773a3d6" 831 | 832 | [[package]] 833 | name = "ryu" 834 | version = "1.0.18" 835 | source = "registry+https://github.com/rust-lang/crates.io-index" 836 | checksum = "f3cb5ba0dc43242ce17de99c180e96db90b235b8a9fdc9543c96d2209116bd9f" 837 | 838 | [[package]] 839 | name = "scopeguard" 840 | version = "1.2.0" 841 | source = "registry+https://github.com/rust-lang/crates.io-index" 842 | checksum = "94143f37725109f92c262ed2cf5e59bce7498c01bcc1502d7b9afe439a4e9f49" 843 | 844 | [[package]] 845 | name = "sharded-slab" 846 | version = "0.1.7" 847 | source = "registry+https://github.com/rust-lang/crates.io-index" 848 | checksum = "f40ca3c46823713e0d4209592e8d6e826aa57e928f09752619fc696c499637f6" 849 | dependencies = [ 850 | "lazy_static", 851 | ] 852 | 853 | [[package]] 854 | name = "shlex" 855 | version = "1.3.0" 856 | source = "registry+https://github.com/rust-lang/crates.io-index" 857 | checksum = "0fda2ff0d084019ba4d7c6f371c95d8fd75ce3524c3cb8fb653a3023f6323e64" 858 | 859 | [[package]] 860 | name = "signal-hook" 861 | version = "0.3.17" 862 | source = "registry+https://github.com/rust-lang/crates.io-index" 863 | checksum = "8621587d4798caf8eb44879d42e56b9a93ea5dcd315a6487c357130095b62801" 864 | dependencies = [ 865 | "libc", 866 | "signal-hook-registry", 867 | ] 868 | 869 | [[package]] 870 | name = "signal-hook-mio" 871 | version = "0.2.4" 872 | source = "registry+https://github.com/rust-lang/crates.io-index" 873 | checksum = "34db1a06d485c9142248b7a054f034b349b212551f3dfd19c94d45a754a217cd" 874 | dependencies = [ 875 | "libc", 876 | "mio", 877 | "signal-hook", 878 | ] 879 | 880 | [[package]] 881 | name = "signal-hook-registry" 882 | version = "1.4.2" 883 | source = "registry+https://github.com/rust-lang/crates.io-index" 884 | checksum = "a9e9e0b4211b72e7b8b6e85c807d36c212bdb33ea8587f7569562a84df5465b1" 885 | dependencies = [ 886 | "libc", 887 | ] 888 | 889 | [[package]] 890 | name = "simd-adler32" 891 | version = "0.3.7" 892 | source = "registry+https://github.com/rust-lang/crates.io-index" 893 | checksum = "d66dc143e6b11c1eddc06d5c423cfc97062865baf299914ab64caa38182078fe" 894 | 895 | [[package]] 896 | name = "smallvec" 897 | version = "1.13.2" 898 | source = "registry+https://github.com/rust-lang/crates.io-index" 899 | checksum = "3c5e1a9a646d36c3599cd173a41282daf47c44583ad367b8e6837255952e5c67" 900 | 901 | [[package]] 902 | name = "static_assertions" 903 | version = "1.1.0" 904 | source = "registry+https://github.com/rust-lang/crates.io-index" 905 | checksum = "a2eb9349b6444b326872e140eb1cf5e7c522154d69e7a0ffb0fb81c06b37543f" 906 | 907 | [[package]] 908 | name = "strum" 909 | version = "0.26.3" 910 | source = "registry+https://github.com/rust-lang/crates.io-index" 911 | checksum = "8fec0f0aef304996cf250b31b5a10dee7980c85da9d759361292b8bca5a18f06" 912 | dependencies = [ 913 | "strum_macros", 914 | ] 915 | 916 | [[package]] 917 | name = "strum_macros" 918 | version = "0.26.4" 919 | source = "registry+https://github.com/rust-lang/crates.io-index" 920 | checksum = "4c6bee85a5a24955dc440386795aa378cd9cf82acd5f764469152d2270e581be" 921 | dependencies = [ 922 | "heck", 923 | "proc-macro2", 924 | "quote", 925 | "rustversion", 926 | "syn", 927 | ] 928 | 929 | [[package]] 930 | name = "syn" 931 | version = "2.0.77" 932 | source = "registry+https://github.com/rust-lang/crates.io-index" 933 | checksum = "9f35bcdf61fd8e7be6caf75f429fdca8beb3ed76584befb503b1569faee373ed" 934 | dependencies = [ 935 | "proc-macro2", 936 | "quote", 937 | "unicode-ident", 938 | ] 939 | 940 | [[package]] 941 | name = "thiserror" 942 | version = "1.0.63" 943 | source = "registry+https://github.com/rust-lang/crates.io-index" 944 | checksum = "c0342370b38b6a11b6cc11d6a805569958d54cfa061a29969c3b5ce2ea405724" 945 | dependencies = [ 946 | "thiserror-impl", 947 | ] 948 | 949 | [[package]] 950 | name = "thiserror-impl" 951 | version = "1.0.63" 952 | source = "registry+https://github.com/rust-lang/crates.io-index" 953 | checksum = "a4558b58466b9ad7ca0f102865eccc95938dca1a74a856f2b57b6629050da261" 954 | dependencies = [ 955 | "proc-macro2", 956 | "quote", 957 | "syn", 958 | ] 959 | 960 | [[package]] 961 | name = "thread_local" 962 | version = "1.1.8" 963 | source = "registry+https://github.com/rust-lang/crates.io-index" 964 | checksum = "8b9ef9bad013ada3808854ceac7b46812a6465ba368859a37e2100283d2d719c" 965 | dependencies = [ 966 | "cfg-if", 967 | "once_cell", 968 | ] 969 | 970 | [[package]] 971 | name = "tiff" 972 | version = "0.9.1" 973 | source = "registry+https://github.com/rust-lang/crates.io-index" 974 | checksum = "ba1310fcea54c6a9a4fd1aad794ecc02c31682f6bfbecdf460bf19533eed1e3e" 975 | dependencies = [ 976 | "flate2", 977 | "jpeg-decoder", 978 | "weezl", 979 | ] 980 | 981 | [[package]] 982 | name = "tracing" 983 | version = "0.1.40" 984 | source = "registry+https://github.com/rust-lang/crates.io-index" 985 | checksum = "c3523ab5a71916ccf420eebdf5521fcef02141234bbc0b8a49f2fdc4544364ef" 986 | dependencies = [ 987 | "pin-project-lite", 988 | "tracing-core", 989 | ] 990 | 991 | [[package]] 992 | name = "tracing-core" 993 | version = "0.1.32" 994 | source = "registry+https://github.com/rust-lang/crates.io-index" 995 | checksum = "c06d3da6113f116aaee68e4d601191614c9053067f9ab7f6edbcb161237daa54" 996 | dependencies = [ 997 | "once_cell", 998 | "valuable", 999 | ] 1000 | 1001 | [[package]] 1002 | name = "tracing-error" 1003 | version = "0.2.0" 1004 | source = "registry+https://github.com/rust-lang/crates.io-index" 1005 | checksum = "d686ec1c0f384b1277f097b2f279a2ecc11afe8c133c1aabf036a27cb4cd206e" 1006 | dependencies = [ 1007 | "tracing", 1008 | "tracing-subscriber", 1009 | ] 1010 | 1011 | [[package]] 1012 | name = "tracing-subscriber" 1013 | version = "0.3.18" 1014 | source = "registry+https://github.com/rust-lang/crates.io-index" 1015 | checksum = "ad0f048c97dbd9faa9b7df56362b8ebcaa52adb06b498c050d2f4e32f90a7a8b" 1016 | dependencies = [ 1017 | "sharded-slab", 1018 | "thread_local", 1019 | "tracing-core", 1020 | ] 1021 | 1022 | [[package]] 1023 | name = "unicode-ident" 1024 | version = "1.0.13" 1025 | source = "registry+https://github.com/rust-lang/crates.io-index" 1026 | checksum = "e91b56cd4cadaeb79bbf1a5645f6b4f8dc5bde8834ad5894a8db35fda9efa1fe" 1027 | 1028 | [[package]] 1029 | name = "unicode-segmentation" 1030 | version = "1.12.0" 1031 | source = "registry+https://github.com/rust-lang/crates.io-index" 1032 | checksum = "f6ccf251212114b54433ec949fd6a7841275f9ada20dddd2f29e9ceea4501493" 1033 | 1034 | [[package]] 1035 | name = "unicode-truncate" 1036 | version = "1.1.0" 1037 | source = "registry+https://github.com/rust-lang/crates.io-index" 1038 | checksum = "b3644627a5af5fa321c95b9b235a72fd24cd29c648c2c379431e6628655627bf" 1039 | dependencies = [ 1040 | "itertools", 1041 | "unicode-segmentation", 1042 | "unicode-width 0.1.13", 1043 | ] 1044 | 1045 | [[package]] 1046 | name = "unicode-width" 1047 | version = "0.1.13" 1048 | source = "registry+https://github.com/rust-lang/crates.io-index" 1049 | checksum = "0336d538f7abc86d282a4189614dfaa90810dfc2c6f6427eaf88e16311dd225d" 1050 | 1051 | [[package]] 1052 | name = "unicode-width" 1053 | version = "0.2.0" 1054 | source = "registry+https://github.com/rust-lang/crates.io-index" 1055 | checksum = "1fc81956842c57dac11422a97c3b8195a1ff727f06e85c84ed2e8aa277c9a0fd" 1056 | 1057 | [[package]] 1058 | name = "valuable" 1059 | version = "0.1.0" 1060 | source = "registry+https://github.com/rust-lang/crates.io-index" 1061 | checksum = "830b7e5d4d90034032940e4ace0d9a9a057e7a45cd94e6c007832e39edb82f6d" 1062 | 1063 | [[package]] 1064 | name = "vcpkg" 1065 | version = "0.2.15" 1066 | source = "registry+https://github.com/rust-lang/crates.io-index" 1067 | checksum = "accd4ea62f7bb7a82fe23066fb0957d48ef677f6eeb8215f372f52e48bb32426" 1068 | 1069 | [[package]] 1070 | name = "version_check" 1071 | version = "0.9.5" 1072 | source = "registry+https://github.com/rust-lang/crates.io-index" 1073 | checksum = "0b928f33d975fc6ad9f86c8f283853ad26bdd5b10b7f1542aa2fa15e2289105a" 1074 | 1075 | [[package]] 1076 | name = "wasi" 1077 | version = "0.11.0+wasi-snapshot-preview1" 1078 | source = "registry+https://github.com/rust-lang/crates.io-index" 1079 | checksum = "9c8d87e72b64a3b4db28d11ce29237c246188f4f51057d65a7eab63b7987e423" 1080 | 1081 | [[package]] 1082 | name = "weezl" 1083 | version = "0.1.8" 1084 | source = "registry+https://github.com/rust-lang/crates.io-index" 1085 | checksum = "53a85b86a771b1c87058196170769dd264f66c0782acf1ae6cc51bfd64b39082" 1086 | 1087 | [[package]] 1088 | name = "winapi" 1089 | version = "0.3.9" 1090 | source = "registry+https://github.com/rust-lang/crates.io-index" 1091 | checksum = "5c839a674fcd7a98952e593242ea400abe93992746761e38641405d28b00f419" 1092 | dependencies = [ 1093 | "winapi-i686-pc-windows-gnu", 1094 | "winapi-x86_64-pc-windows-gnu", 1095 | ] 1096 | 1097 | [[package]] 1098 | name = "winapi-i686-pc-windows-gnu" 1099 | version = "0.4.0" 1100 | source = "registry+https://github.com/rust-lang/crates.io-index" 1101 | checksum = "ac3b87c63620426dd9b991e5ce0329eff545bccbbb34f3be09ff6fb6ab51b7b6" 1102 | 1103 | [[package]] 1104 | name = "winapi-x86_64-pc-windows-gnu" 1105 | version = "0.4.0" 1106 | source = "registry+https://github.com/rust-lang/crates.io-index" 1107 | checksum = "712e227841d057c1ee1cd2fb22fa7e5a5461ae8e48fa2ca79ec42cfc1931183f" 1108 | 1109 | [[package]] 1110 | name = "windows-sys" 1111 | version = "0.52.0" 1112 | source = "registry+https://github.com/rust-lang/crates.io-index" 1113 | checksum = "282be5f36a8ce781fad8c8ae18fa3f9beff57ec1b52cb3de0789201425d9a33d" 1114 | dependencies = [ 1115 | "windows-targets 0.52.6", 1116 | ] 1117 | 1118 | [[package]] 1119 | name = "windows-targets" 1120 | version = "0.48.5" 1121 | source = "registry+https://github.com/rust-lang/crates.io-index" 1122 | checksum = "9a2fa6e2155d7247be68c096456083145c183cbbbc2764150dda45a87197940c" 1123 | dependencies = [ 1124 | "windows_aarch64_gnullvm 0.48.5", 1125 | "windows_aarch64_msvc 0.48.5", 1126 | "windows_i686_gnu 0.48.5", 1127 | "windows_i686_msvc 0.48.5", 1128 | "windows_x86_64_gnu 0.48.5", 1129 | "windows_x86_64_gnullvm 0.48.5", 1130 | "windows_x86_64_msvc 0.48.5", 1131 | ] 1132 | 1133 | [[package]] 1134 | name = "windows-targets" 1135 | version = "0.52.6" 1136 | source = "registry+https://github.com/rust-lang/crates.io-index" 1137 | checksum = "9b724f72796e036ab90c1021d4780d4d3d648aca59e491e6b98e725b84e99973" 1138 | dependencies = [ 1139 | "windows_aarch64_gnullvm 0.52.6", 1140 | "windows_aarch64_msvc 0.52.6", 1141 | "windows_i686_gnu 0.52.6", 1142 | "windows_i686_gnullvm", 1143 | "windows_i686_msvc 0.52.6", 1144 | "windows_x86_64_gnu 0.52.6", 1145 | "windows_x86_64_gnullvm 0.52.6", 1146 | "windows_x86_64_msvc 0.52.6", 1147 | ] 1148 | 1149 | [[package]] 1150 | name = "windows_aarch64_gnullvm" 1151 | version = "0.48.5" 1152 | source = "registry+https://github.com/rust-lang/crates.io-index" 1153 | checksum = "2b38e32f0abccf9987a4e3079dfb67dcd799fb61361e53e2882c3cbaf0d905d8" 1154 | 1155 | [[package]] 1156 | name = "windows_aarch64_gnullvm" 1157 | version = "0.52.6" 1158 | source = "registry+https://github.com/rust-lang/crates.io-index" 1159 | checksum = "32a4622180e7a0ec044bb555404c800bc9fd9ec262ec147edd5989ccd0c02cd3" 1160 | 1161 | [[package]] 1162 | name = "windows_aarch64_msvc" 1163 | version = "0.48.5" 1164 | source = "registry+https://github.com/rust-lang/crates.io-index" 1165 | checksum = "dc35310971f3b2dbbf3f0690a219f40e2d9afcf64f9ab7cc1be722937c26b4bc" 1166 | 1167 | [[package]] 1168 | name = "windows_aarch64_msvc" 1169 | version = "0.52.6" 1170 | source = "registry+https://github.com/rust-lang/crates.io-index" 1171 | checksum = "09ec2a7bb152e2252b53fa7803150007879548bc709c039df7627cabbd05d469" 1172 | 1173 | [[package]] 1174 | name = "windows_i686_gnu" 1175 | version = "0.48.5" 1176 | source = "registry+https://github.com/rust-lang/crates.io-index" 1177 | checksum = "a75915e7def60c94dcef72200b9a8e58e5091744960da64ec734a6c6e9b3743e" 1178 | 1179 | [[package]] 1180 | name = "windows_i686_gnu" 1181 | version = "0.52.6" 1182 | source = "registry+https://github.com/rust-lang/crates.io-index" 1183 | checksum = "8e9b5ad5ab802e97eb8e295ac6720e509ee4c243f69d781394014ebfe8bbfa0b" 1184 | 1185 | [[package]] 1186 | name = "windows_i686_gnullvm" 1187 | version = "0.52.6" 1188 | source = "registry+https://github.com/rust-lang/crates.io-index" 1189 | checksum = "0eee52d38c090b3caa76c563b86c3a4bd71ef1a819287c19d586d7334ae8ed66" 1190 | 1191 | [[package]] 1192 | name = "windows_i686_msvc" 1193 | version = "0.48.5" 1194 | source = "registry+https://github.com/rust-lang/crates.io-index" 1195 | checksum = "8f55c233f70c4b27f66c523580f78f1004e8b5a8b659e05a4eb49d4166cca406" 1196 | 1197 | [[package]] 1198 | name = "windows_i686_msvc" 1199 | version = "0.52.6" 1200 | source = "registry+https://github.com/rust-lang/crates.io-index" 1201 | checksum = "240948bc05c5e7c6dabba28bf89d89ffce3e303022809e73deaefe4f6ec56c66" 1202 | 1203 | [[package]] 1204 | name = "windows_x86_64_gnu" 1205 | version = "0.48.5" 1206 | source = "registry+https://github.com/rust-lang/crates.io-index" 1207 | checksum = "53d40abd2583d23e4718fddf1ebec84dbff8381c07cae67ff7768bbf19c6718e" 1208 | 1209 | [[package]] 1210 | name = "windows_x86_64_gnu" 1211 | version = "0.52.6" 1212 | source = "registry+https://github.com/rust-lang/crates.io-index" 1213 | checksum = "147a5c80aabfbf0c7d901cb5895d1de30ef2907eb21fbbab29ca94c5b08b1a78" 1214 | 1215 | [[package]] 1216 | name = "windows_x86_64_gnullvm" 1217 | version = "0.48.5" 1218 | source = "registry+https://github.com/rust-lang/crates.io-index" 1219 | checksum = "0b7b52767868a23d5bab768e390dc5f5c55825b6d30b86c844ff2dc7414044cc" 1220 | 1221 | [[package]] 1222 | name = "windows_x86_64_gnullvm" 1223 | version = "0.52.6" 1224 | source = "registry+https://github.com/rust-lang/crates.io-index" 1225 | checksum = "24d5b23dc417412679681396f2b49f3de8c1473deb516bd34410872eff51ed0d" 1226 | 1227 | [[package]] 1228 | name = "windows_x86_64_msvc" 1229 | version = "0.48.5" 1230 | source = "registry+https://github.com/rust-lang/crates.io-index" 1231 | checksum = "ed94fce61571a4006852b7389a063ab983c02eb1bb37b47f8272ce92d06d9538" 1232 | 1233 | [[package]] 1234 | name = "windows_x86_64_msvc" 1235 | version = "0.52.6" 1236 | source = "registry+https://github.com/rust-lang/crates.io-index" 1237 | checksum = "589f6da84c646204747d1270a2a5661ea66ed1cced2631d546fdfb155959f9ec" 1238 | 1239 | [[package]] 1240 | name = "x11rb" 1241 | version = "0.13.1" 1242 | source = "registry+https://github.com/rust-lang/crates.io-index" 1243 | checksum = "5d91ffca73ee7f68ce055750bf9f6eca0780b8c85eff9bc046a3b0da41755e12" 1244 | dependencies = [ 1245 | "gethostname", 1246 | "rustix", 1247 | "x11rb-protocol", 1248 | ] 1249 | 1250 | [[package]] 1251 | name = "x11rb-protocol" 1252 | version = "0.13.1" 1253 | source = "registry+https://github.com/rust-lang/crates.io-index" 1254 | checksum = "ec107c4503ea0b4a98ef47356329af139c0a4f7750e621cf2973cd3385ebcb3d" 1255 | 1256 | [[package]] 1257 | name = "zerocopy" 1258 | version = "0.7.35" 1259 | source = "registry+https://github.com/rust-lang/crates.io-index" 1260 | checksum = "1b9b4fd18abc82b8136838da5d50bae7bdea537c574d8dc1a34ed098d6c166f0" 1261 | dependencies = [ 1262 | "zerocopy-derive", 1263 | ] 1264 | 1265 | [[package]] 1266 | name = "zerocopy-derive" 1267 | version = "0.7.35" 1268 | source = "registry+https://github.com/rust-lang/crates.io-index" 1269 | checksum = "fa4f8080344d4671fb4e831a13ad1e68092748387dfc4f55e356242fae12ce3e" 1270 | dependencies = [ 1271 | "proc-macro2", 1272 | "quote", 1273 | "syn", 1274 | ] 1275 | -------------------------------------------------------------------------------- /Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "lazygh" 3 | version = "0.5.0" 4 | authors = ["Karan Janthe "] 5 | license = "MIT" 6 | edition = "2021" 7 | repository = "https://github.com/kmj-007/lazygh" 8 | homepage = "https://kmj-007.github.io/lazygh" 9 | description = "A Terminal User Interface (TUI) application for managing multiple GitHub accounts easily" 10 | keywords = ["github", "git", "tui", "account-management", "ssh-keys"] 11 | categories = ["command-line-utilities", "development-tools"] 12 | readme = "README.md" 13 | 14 | [package.metadata.wix] 15 | upgrade-guid = "B4DC81DF-8698-485D-B31B-FBF71C73F319" 16 | path-guid = "17F93089-B520-4CFD-94D9-183527AEBA4A" 17 | license = false 18 | eula = false 19 | 20 | [dependencies] 21 | crossterm = "0.28.1" 22 | ratatui = "0.29.0" 23 | color-eyre = "0.6.3" 24 | arboard = "3.5.0" 25 | regex = "1.11" 26 | rusqlite = { version = "0.28.0", features = ["bundled"] } 27 | dirs = "4.0" 28 | 29 | # The profile that 'cargo dist' will build with 30 | [profile.dist] 31 | inherits = "release" 32 | lto = "thin" 33 | 34 | # Config for 'cargo dist' 35 | [workspace.metadata.dist] 36 | # The preferred cargo-dist version to use in CI (Cargo.toml SemVer syntax) 37 | cargo-dist-version = "0.22.1" 38 | # CI backends to support 39 | ci = "github" 40 | # The installers to generate for each app 41 | installers = ["shell", "powershell", "npm", "homebrew", "msi"] 42 | # Target platforms to build apps for (Rust target-triple syntax) 43 | targets = ["aarch64-apple-darwin", "x86_64-apple-darwin", "x86_64-unknown-linux-gnu", "x86_64-unknown-linux-musl", "x86_64-pc-windows-msvc"] 44 | # The archive format to use for windows builds (defaults .zip) 45 | windows-archive = ".tar.gz" 46 | # The archive format to use for non-windows builds (defaults .tar.xz) 47 | unix-archive = ".tar.gz" 48 | # Path that installers should place binaries in 49 | install-path = "CARGO_HOME" 50 | # Whether to install an updater program 51 | install-updater = true 52 | # A GitHub repo to push Homebrew formulas to 53 | tap = "kmj-007/homebrew-lazygh" 54 | # Publish jobs to run in CI 55 | publish-jobs = ["homebrew"] 56 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | 3 | Copyright (c) Karan Janthe 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # LazyGH 2 | 3 | ⚠️ **WARNING: This project is highly under development. Do not use in production. There are several security issues which are yet to be addressed.** ⚠️ 4 | 5 | LazyGH is a Terminal User Interface (TUI) application for managing multiple GitHub accounts easily. It allows you to switch between different Git configurations and SSH keys seamlessly. 6 | 7 | ## Demo 8 | 9 | Check out the demo to see LazyGH in action: 10 | 11 | ![LazyGH Demo](https://cloud-hq5v2c9l2-hack-club-bot.vercel.app/0demo.gif) 12 | 13 | ## Features 14 | 15 | - Manage multiple GitHub accounts 16 | - Switch between accounts with ease 17 | - Automatically update Git global configuration 18 | - Generate and manage SSH keys for each account 19 | - Copy SSH public keys to clipboard 20 | 21 | ## Contributing 22 | 23 | Contributions are welcome! Here's how you can contribute: 24 | 25 | 1. Fork the repository 26 | 2. Create your feature branch (`git checkout -b feature/AmazingFeature`) 27 | 3. Commit your changes (`git commit -m 'Add some AmazingFeature'`) 28 | 4. Push to the branch (`git push origin feature/AmazingFeature`) 29 | 5. Open a Pull Request 30 | 31 | Please make sure to update tests as appropriate and adhere to the existing coding style. 32 | 33 | ## Issues 34 | 35 | If you encounter any problems or have suggestions for improvements, please open an issue on the [GitHub Issues page](https://github.com/KMJ-007/lazygh/issues). 36 | 37 | ## Roadmap 38 | 39 | - [ ] fix the cargo auto release version mangment with release workflow 40 | - [ ] adding lazygh to binstall 41 | - [ ] adding lazygh to nix-env 42 | - [ ] adding lazygh to nix flake 43 | - [ ] ability to maintain multiple gh config files and able to categorise them in workspace fashion but in simpler way 44 | - [ ] don't know other things to add, create an issue if you have any ideas 45 | 46 | ## Security 47 | 48 | LazyGH takes your security seriously. If you discover a security vulnerability within LazyGH, please send an e-mail to Karan Janthe via karanjanthe@gmail.com. All security vulnerabilities will be addressed. 49 | 50 | ## Acknowledgements 51 | 52 | - [Ratatui](https://github.com/ratatui-org/ratatui) for the excellent TUI framework 53 | - [Tweet](https://x.com/KaranJanthe/status/1835380523235193310) for the idea and inspiration 54 | - All the contributors who have helped shape LazyGH 55 | 56 | ## Contact 57 | 58 | Karan Janthe - [@KaranJanthe](https://twitter.com/karanjanthe) 59 | 60 | Project Link: [https://github.com/KMJ-007/lazygh](https://github.com/KMJ-007/lazygh) 61 | -------------------------------------------------------------------------------- /demo.cast: -------------------------------------------------------------------------------- 1 | {"version": 2, "width": 187, "height": 57, "timestamp": 1726461822, "env": {"SHELL": "/bin/zsh", "TERM": "xterm-256color"}} 2 | [1.063505, "o", "\u001b[1m\u001b[7m%\u001b[27m\u001b[1m\u001b[0m \r \r"] 3 | [1.064108, "o", "\u001b]2;karanjanthe@Karans-MacBook-Air:~/Projects/lazygh\u0007\u001b]1;..ojects/lazygh\u0007"] 4 | [1.065562, "o", "\u001b]7;file://Karans-MacBook-Air.local/Users/karanjanthe/Projects/lazygh\u001b\\"] 5 | [1.078216, "o", "\r\u001b[0m\u001b[27m\u001b[24m\u001b[J(base) \u001b[01;32m➜ \u001b[36mlazygh\u001b[00m \u001b[K"] 6 | [1.07833, "o", "\u001b[?1h\u001b=\u001b[?2004h"] 7 | [1.092312, "o", "\r\r\u001b[0m\u001b[27m\u001b[24m\u001b[J(base) \u001b[01;32m➜ \u001b[36mlazygh\u001b[00m \u001b[01;34mgit:(\u001b[31mmain\u001b[34m) \u001b[33m✗\u001b[00m \u001b[K"] 8 | [1.618527, "o", "l"] 9 | [1.636008, "o", "\b\u001b[32ml\u001b[39m"] 10 | [1.636444, "o", "\b\u001b[32ml\u001b[39m\u001b[90mazygh\u001b[39m\b\b\b\b\b"] 11 | [1.7208, "o", "\b\u001b[32ml\u001b[32ma\u001b[39m"] 12 | [2.009532, "o", "\u001b[39mz\u001b[39my\u001b[39mg\u001b[39mh"] 13 | [2.011826, "o", "\b\b\b\b\b\b\u001b[32ml\u001b[32ma\u001b[32mz\u001b[32my\u001b[32mg\u001b[32mh\u001b[39m"] 14 | [2.511878, "o", "\u001b[?1l\u001b>"] 15 | [2.512141, "o", "\u001b[?2004l"] 16 | [2.51391, "o", "\r\r\n"] 17 | [2.515564, "o", "\u001b]2;lazygh\u0007\u001b]1;lazygh\u0007"] 18 | [2.533655, "o", "\u001b[?1049h"] 19 | [2.563707, "o", "\u001b[2;2H\u001b[38;5;8;49m┌LazyGH─────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────┐\u001b[3;2H│ \u001b[1m\u001b[38;5;10;49mAccounts\u001b[22m\u001b[38;5;8;49m │ Help │\u001b[4;2H└──────────────────────────────────────────────────────────────────────────"] 20 | [2.563808, "o", "─────────────────────────────────────────────────────────────────────────────────────────────────────────────┘\u001b[5;2H\u001b[39;49m┌Accounts──────────────────────────────────────────────┐┌Account\u001b[5;67HInfo───────────────────────────────────────────────────────────────────────────────────────────────────────────────────┐\u001b[6;2H│\u001b[6;6H\u001b[38;5;7;49manother\u001b[6;57H\u001b[39;49m││\u001b[1mName: \u001b[22m\u001b[38;5;10;49mkaran\u001b[6;186H\u001b[39;49m│\u001b[7;2H│\u001b[7;6H\u001b[38;5;8;49mantoert@gmail.com\u001b[7;57H\u001b"] 21 | [2.563846, "o", "[39;49m││\u001b[1mEmail: \u001b[22m\u001b[38;5;12;49mkaranjanthe@gmail.com\u001b[7;186H\u001b[39;49m│\u001b[8;2H│\u001b[8;6H\u001b[38;5;7;49mhello\u001b[8;57H\u001b[39;49m││\u001b[38;5;10;49m● Active\u001b[8;186H\u001b[39;49m│\u001b[9;2H│\u001b[9;6H\u001b[38;5;8;49mhello@gmail.com\u001b[9;57H\u001b[39;49m││\u001b[9;186H│\u001b[10;2H│\u001b[38;5;15;48;5;8m>> \u001b[1mkaran\u001b[22m \u001b[39;49m││\u001b[38;5;3;49mEnter: Copy SSH key | Space: Set active\u001b[10;186H\u001b[39;49m│\u001b[11;2H│\u001b[38;5;15;48;5;8m \u001b[1mkaranjanthe@gmail.com\u001b[22m \u001b[39;49m││\u001b[38;5;6;49ma: Add new account | r: Remove account\u001b[11;186H\u001b[39;49m│\u001b[12;2H│\u001b[12;6H\u001b[38;5;7;49manother account\u001b[12;57H\u001b[39;49m││\u001b[12;186H│\u001b[13;2H│\u001b[13;6H\u001b[38;5;8;49manasda@gmail.com\u001b[13;57H\u001b[39;49m││\u001b[13;186H│\u001b[14;2H│\u001b[14;57H││\u001b[14;186H│\u001b[15;2H│\u001b[15;57H││\u001b[15;186H│\u001b[16;2H│\u001b[16;57H││\u001b[16;186H│\u001b[17;2H│\u001b[17;57H││\u001b[17;186H│\u001b[18;2H│\u001b[18;57H││\u001b[18;186H│\u001b[19;2H│\u001b[19;57H││\u001b[19;186H│\u001b[20;2H│\u001b[20;57H││\u001b[20;186H│\u001b[21;2H│\u001b[21;"] 22 | [2.563905, "o", "57H││\u001b[21;186H│\u001b[22;2H│\u001b[22;57H││\u001b[22;186H│\u001b[23;2H│\u001b[23;57H││\u001b[23;186H│\u001b[24;2H│\u001b[24;57H││\u001b[24;186H│\u001b[25;2H│\u001b[25;57H││\u001b[25;186H│\u001b[26;2H│\u001b[26;57H││\u001b[26;186H│\u001b[27;2H│\u001b[27;57H││\u001b[27;186H│\u001b[28;2H│\u001b[28;57H││\u001b[28;186H│\u001b[29;2H│\u001b[29;57H││\u001b[29;186H│\u001b[30;2H│\u001b[30;57H││\u001b[30;186H│\u001b[31;2H│\u001b[31;57H││\u001b[31;186H│\u001b[32;2H│\u001b[32;57H││\u001b[32;186H│\u001b[33;2H│\u001b[33;57H││\u001b[33;186H│\u001b[34;2H│\u001b[34;57H││\u001b[34;186H│\u001b[35;2H│\u001b[35;57H││\u001b[35;186H│\u001b[36;2H│\u001b[36;57H││\u001b[36;186H│\u001b[37;2H│\u001b[37;57H││\u001b[37;186H│\u001b[38;2H│\u001b[38;57H││\u001b[38;186H│\u001b[39;2H│\u001b[39;57H││\u001b[39;186H│\u001b[40;2H│\u001b[40;57H││\u001b[40;186H│\u001b[41;2H│\u001b[41;57H││\u001b[41;186H│\u001b[42;2H│\u001b[42;57H││\u001b[42;186H│\u001b[43;2H│\u001b[43;57H││\u001b[43;186H│\u001b[44;2H│\u001b[44;57H││\u001b[44;186H│\u001b[45;2H│\u001b[45;57H││\u001b[45;186H│\u001b[46;2H│\u001b[46;57H││\u001b[46;186H│\u001b[47;2H│\u001b[47;57H││\u001b[47;186H│\u001b[48;2H│\u001b[48;57H││\u001b[48;186H│\u001b[49;2H│\u001b[49;57H││\u001b[49;"] 23 | [2.563937, "o", "186H│\u001b[50;2H│\u001b[50;57H││\u001b[50;186H│\u001b[51;2H│\u001b[51;57H││\u001b[51;186H│\u001b[52;2H│\u001b[52;57H││\u001b[52;186H│\u001b[53;2H│\u001b[53;57H││\u001b[53;186H│\u001b[54;2H│\u001b[54;57H││\u001b[54;186H│\u001b[55;2H└──────────────────────────────────────────────────────┘└───────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────┘\u001b[56;2H\u001b[38;5;14;49mWelcome to LazyGH! \u001b[39m\u001b[49m\u001b[59m\u001b[0m\u001b[?25l"] 24 | [4.177941, "o", "\u001b[6;65H\u001b[38;5;10;49mhello\u001b[7;66H\u001b[38;5;12;49mhello@gmail.com\u001b[39;49m \u001b[8;3H\u001b[38;5;15;48;5;8m>> hello \u001b[8;59H\u001b[38;5;7;49m○ Inactive\u001b[9;3H\u001b[38;5;15;48;5;8m hello@gmail.com \u001b[10;3H\u001b[39;49m \u001b[1m\u001b[38;5;10;49mkaran\u001b[22m\u001b[39;49m \u001b[11;3H \u001b[1m\u001b[38;5;8;49mkaranjanthe@gmail.com\u001b[22m\u001b[39;49m \u001b[56;2H\u001b[38;5;14;49mS\u001b[56;5Hect\u001b[56;9Hd previous account\u001b[39m\u001b[49m\u001b[59m\u001b[0m\u001b[?25l"] 25 | [4.368698, "o", "\u001b[6;3H\u001b[38;5;15;48;5;8m>> another \u001b[6;65H\u001b[38;5;10;49manother\u001b[7;3H\u001b[38;5;15;48;5;8m antoert@gmail.com \u001b[7;66H\u001b[38;5;12;49mantoert@gmail.com\u001b[8;3H\u001b[39;49m \u001b[38;5;7;49mhello\u001b[39;49m \u001b[9;3H \u001b[38;5;8;49mhello@gmail.com\u001b[39;49m \u001b[39m\u001b[49m\u001b[59m\u001b[0m\u001b[?25l"] 26 | [4.521773, "o", "\u001b[6;3H \u001b[38;5;7;49manother\u001b[39;49m \u001b[6;72H\u001b[38;5;10;49m account\u001b[7;3H\u001b[39;49m \u001b[38;5;8;49mantoert@gmail.com\u001b[39;49m \u001b[7;68H\u001b[38;5;12;49masda@gmail.com\u001b[39;49m \u001b[12;3H\u001b[38;5;15;48;5;8m>> another account \u001b[13;3H anasda@gmail.com \u001b[39m\u001b[49m\u001b[59m\u001b[0m\u001b[?25l"] 27 | [5.217206, "o", "\u001b[6;65H\u001b[38;5;10;49mkaran\u001b[39;49m \u001b[7;66H\u001b[38;5;12;49mkaranjanthe@gmail.com\u001b[8;59H\u001b[38;5;10;49m● Active\u001b[39;49m \u001b[10;3H\u001b[38;5;15;48;5;8m>> \u001b[1mkaran\u001b[22m \u001b[11;3H \u001b[1mkaranjanthe@gmail.com\u001b[22m \u001b[12;3H\u001b[39;49m \u001b[38;5;7;49manother account\u001b[39;49m \u001b[13;3H \u001b[38;5;8;49manasda@gmail.com\u001b[39;49m \u001b[39m\u001b[49m\u001b[59m\u001b[0m\u001b[?25l"] 28 | [5.37638, "o", "\u001b[6;65H\u001b[38;5;10;49mhello\u001b[7;66H\u001b[38;5;12;49mhello@gmail.com\u001b[39;49m \u001b[8;3H\u001b[38;5;15;48;5;8m>> hello \u001b[8;59H\u001b[38;5;7;49m○ Inactive\u001b[9;3H\u001b[38;5;15;48;5;8m hello@gmail.com \u001b[10;3H\u001b[39;49m \u001b[1m\u001b[38;5;10;49mkaran\u001b[22m\u001b[39;49m \u001b[11;3H \u001b[1m\u001b[38;5;8;49mkaranjanthe@gmail.com\u001b[22m\u001b[39;49m \u001b[39m\u001b[49m\u001b[59m\u001b[0m\u001b[?25l"] 29 | [5.516459, "o", "\u001b[6;3H\u001b[38;5;15;48;5;8m>> another \u001b[6;65H\u001b[38;5;10;49manother\u001b[7;3H\u001b[38;5;15;48;5;8m antoert@gmail.com \u001b[7;66H\u001b[38;5;12;49mantoert@gmail.com\u001b[8;3H\u001b[39;49m \u001b[38;5;7;49mhello\u001b[39;49m \u001b[9;3H \u001b[38;5;8;49mhello@gmail.com\u001b[39;49m \u001b[39m\u001b[49m\u001b[59m\u001b[0m\u001b[?25l"] 30 | [5.75843, "o", "\u001b[3;4H\u001b[38;5;8;49mAccounts\u001b[3;15H\u001b[1m\u001b[38;5;10;49mHelp\u001b[5;2H\u001b[22m\u001b[38;5;15;49m┌Help───────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────┐\u001b[6;2H│\u001b[1mKeyboard Shortcuts\u001b[22m │\u001b[7;2H│ │"] 31 | [5.758659, "o", "\u001b[8;2H│\u001b[38;5;3;49mTab\u001b[38;5;15;49m: Switch between tabs │\u001b[9;2H│\u001b[38;5;10;49m↑/↓\u001b[38;5;15;49m: Navigate accounts │\u001b[10;2H│\u001b[38;5;11;49mEnter\u001b[38;5;15;49m: Copy SSH key to clipboard │\u001b[11;2H│\u001b[38;5;11;49mSpace\u001b[38;5;15;49m: Set active account │\u001b[12;2H│\u001b[38;5;6;49ma\u001b[38;5;15;49m: Add new account "] 32 | [5.758696, "o", " │\u001b[13;2H│\u001b[38;5;1;49mr\u001b[38;5;15;49m: Remove selected account │\u001b[14;2H│\u001b[38;5;9;49mq\u001b[38;5;15;49m: Quit │\u001b[15;2H│ │\u001b[16;2H│Follow the developer on Twitter/X: \u001b[3m\u001b[4m\u001b[38;5;12;49m@KaranJanthe\u001b[23m\u001b[24m\u001b[38;5;15;49m │\u001b[17;2H│ "] 33 | [5.75885, "o", " │\u001b[18;2H│ │\u001b[19;2H│ │\u001b[20;2H│ │\u001b[21;2H│ │\u001b[22;2H│ "] 34 | [5.75888, "o", " │\u001b[23;2H│ │\u001b[24;2H│ │\u001b[25;2H│ │\u001b[26;2H│ │\u001b[27;2H│ "] 35 | [5.758987, "o", " │\u001b[28;2H│ │\u001b[29;2H│ │\u001b[30;2H│ │\u001b[31;2H│ │\u001b[32;2H│ "] 36 | [5.759073, "o", " │\u001b[33;2H│ │\u001b[34;2H│ │\u001b[35;2H│ │\u001b[36;2H│ │\u001b[37;2H│ │\u001b[38;2H│ "] 37 | [5.759157, "o", " │\u001b[39;2H│ │\u001b[40;2H│ │\u001b[41;2H│ │\u001b[42;2H│ │\u001b[43;2H│ "] 38 | [5.759185, "o", " │\u001b[44;2H│ │\u001b[45;2H│ │\u001b[46;2H│ │\u001b[47;2H│ │\u001b[48;2H│ "] 39 | [5.759278, "o", " │\u001b[49;2H│ │\u001b[50;2H│ │\u001b[51;2H│ │\u001b[52;2H│ │\u001b[53;2H│ "] 40 | [5.759303, "o", " │\u001b[54;2H│ │\u001b[55;2H└───────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────┘\u001b[56;3H\u001b[38;5;14;49mwit\u001b[56;7Hh\u001b[56;11Hto Help t\u001b[56;21Hb \u001b[39m\u001b[49m\u001b[59m\u001b[0m\u001b[?25l"] 41 | [6.156908, "o", "\u001b[3;4H\u001b[1m\u001b[38;5;10;49mAccounts\u001b[3;15H\u001b[22m\u001b[38;5;8;49mHelp\u001b[5;2H\u001b[39;49m┌Accounts──────────────────────────────────────────────┐┌Account Info───────────────────────────────────────────────────────────────────────────────────────────────────────────────────┐\u001b[6;2H│\u001b[38;5;15;48;5;8m>> another \u001b[39;49m││\u001b[1mName: \u001b[22m\u001b[38;5;10;49manother\u001b[39;49m │\u001b[7;2H│\u001b[38;5;15;48;5;8m antoert@gmail.com \u001b[39;49m││\u001b[1mEmail: \u001b[22m\u001b[38;5;12;49mantoert@gmail.com\u001b[39;49m "] 42 | [6.157015, "o", " │\u001b[8;2H│ \u001b[38;5;7;49mhello\u001b[39;49m ││\u001b[38;5;7;49m○ Inactive\u001b[39;49m │\u001b[9;2H│ \u001b[38;5;8;49mhello@gmail.com\u001b[39;49m ││ │\u001b[10;2H│ \u001b[1m\u001b[38;5;10;49mkaran\u001b[22m\u001b[39;49m ││\u001b[38;5;3;49mEnter: Copy SSH key | Space: Set active\u001b[39;49m │\u001b[11;2H│ \u001b[1m\u001b[38;5;8;49mkaranjanthe@gmail.com\u001b[22m\u001b[39;49m ││\u001b[38;5;6;49ma: Add new account | r: Remove account\u001b[39;49m "] 43 | [6.157187, "o", " │\u001b[12;2H│ \u001b[38;5;7;49manother account\u001b[39;49m ││ │\u001b[13;2H│ \u001b[38;5;8;49manasda@gmail.com\u001b[39;49m ││ │\u001b[14;2H│ ││ │\u001b[15;2H│ ││ │\u001b[16;2H│ ││ "] 44 | [6.157286, "o", " │\u001b[17;2H│ ││ │\u001b[18;2H│ ││ │\u001b[19;2H│ ││ │\u001b[20;2H│ ││ │\u001b[21;2H│ ││ │\u001b"] 45 | [6.157341, "o", "[22;2H│ ││ │\u001b[23;2H│ ││ │\u001b[24;2H│ ││ │\u001b[25;2H│ ││ │\u001b[26;2H│ ││ │\u001b[27;2H│ "] 46 | [6.157444, "o", " ││ │\u001b[28;2H│ ││ │\u001b[29;2H│ ││ │\u001b[30;2H│ ││ │\u001b[31;2H│ ││ │\u001b[32;2H│ "] 47 | [6.157521, "o", " ││ │\u001b[33;2H│ ││ │\u001b[34;2H│ ││ │\u001b[35;2H│ ││ │\u001b[36;2H│ ││ │\u001b[37;2H│ ││ "] 48 | [6.157546, "o", " │\u001b[38;2H│ ││ │\u001b[39;2H│ ││ │\u001b[40;2H│ ││ │\u001b[41;2H│ ││ │\u001b[42;2H│ ││ "] 49 | [6.157651, "o", " │\u001b[43;2H│ ││ │\u001b[44;2H│ ││ │\u001b[45;2H│ ││ │\u001b[46;2H│ ││ │\u001b[47;2H│ ││ "] 50 | [6.157673, "o", " │\u001b[48;2H│ ││ │\u001b[49;2H│ ││ │\u001b[50;2H│ ││ │\u001b[51;2H│ ││ │\u001b[52;2H│ ││ "] 51 | [6.157776, "o", " │\u001b[53;2H│ ││ │\u001b[54;2H│ ││ │\u001b[55;2H└──────────────────────────────────────────────────────┘└───────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────┘\u001b[56;1"] 52 | [6.157799, "o", "4H\u001b[38;5;14;49mAccounts\u001b[56;23Htab\u001b[39m\u001b[49m\u001b[59m\u001b[0m\u001b[?25l"] 53 | [7.068488, "o", "\u001b[6;6H\u001b[1m\u001b[38;5;15;48;5;8manother\u001b[7;6Hantoert@gmail.com\u001b[8;59H\u001b[22m\u001b[38;5;10;49m● Active\u001b[39;49m \u001b[10;6H\u001b[38;5;7;49mkaran\u001b[11;6H\u001b[38;5;8;49mkaranjanthe@gmail.com\u001b[56;2H\u001b[38;5;14;49mActivated account: another\u001b[39m\u001b[49m\u001b[59m\u001b[0m\u001b[?25l"] 54 | [7.499018, "o", "\u001b[6;3H \u001b[1m\u001b[38;5;10;49manother\u001b[22m\u001b[39;49m \u001b[6;65H\u001b[38;5;10;49mhello\u001b[39;49m \u001b[7;3H \u001b[1m\u001b[38;5;8;49mantoert@gmail.com\u001b[22m\u001b[39;49m \u001b[7;66H\u001b[38;5;12;49mhello@gmail.com\u001b[39;49m \u001b[8;3H\u001b[38;5;15;48;5;8m>> hello \u001b[8;59H\u001b[38;5;7;49m○ Inactive\u001b[9;3H\u001b[38;5;15;48;5;8m hello@gmail.com \u001b[56;2H\u001b[38;5;14;49mSelected next account \u001b[39m\u001b[49m\u001b[59m\u001b[0m\u001b[?25l"] 55 | [8.032045, "o", "\u001b[6;6H\u001b[38;5;7;49manother\u001b[7;6H\u001b[38;5;8;49mantoert@gmail.com\u001b[8;6H\u001b[1m\u001b[38;5;15;48;5;8mhello\u001b[8;59H\u001b[22m\u001b[38;5;10;49m● Active\u001b[39;49m \u001b[9;6H\u001b[1m\u001b[38;5;15;48;5;8mhello@gmail.com\u001b[56;2H\u001b[22m\u001b[38;5;14;49mActivated account: hello\u001b[39m\u001b[49m\u001b[59m\u001b[0m\u001b[?25l"] 56 | [8.362386, "o", "\u001b[6;65H\u001b[38;5;10;49mkaran\u001b[7;66H\u001b[38;5;12;49mkaranjanthe@gmail.com\u001b[8;3H\u001b[39;49m \u001b[1m\u001b[38;5;10;49mhello\u001b[22m\u001b[39;49m \u001b[8;59H\u001b[38;5;7;49m○ Inactive\u001b[9;3H\u001b[39;49m \u001b[1m\u001b[38;5;8;49mhello@gmail.com\u001b[22m\u001b[39;49m \u001b[10;3H\u001b[38;5;15;48;5;8m>> karan \u001b[11;3H karanjanthe@gmail.com \u001b[56;2H\u001b[38;5;14;49mSelected next account \u001b[39m\u001b[49m\u001b[59m\u001b[0m\u001b[?25l"] 57 | [8.908002, "o", "\u001b[8;6H\u001b[38;5;7;49mhello\u001b[8;59H\u001b[38;5;10;49m● Active\u001b[39;49m \u001b[9;6H\u001b[38;5;8;49mhello@gmail.com\u001b[10;6H\u001b[1m\u001b[38;5;15;48;5;8mkaran\u001b[11;6Hkaranjanthe@gmail.com\u001b[56;2H\u001b[22m\u001b[38;5;14;49mActivated account: karan\u001b[39m\u001b[49m\u001b[59m\u001b[0m\u001b[?25l"] 58 | [9.621785, "o", "\u001b[56;2H\u001b[38;5;14;49mSSH key copied to clipboard\u001b[39m\u001b[49m\u001b[59m\u001b[0m\u001b[?25l"] 59 | [10.673659, "o", "\u001b[24;38H\u001b[38;5;12;49m┌Add Account────────────────────────────────────────────────────────────────────────────────────────────────────┐\u001b[25;38H│\u001b[1m\u001b[38;5;2;49mAdd New Account\u001b[22m\u001b[38;5;15;49m \u001b[38;5;12;49m│\u001b[26;38H│\u001b[38;5;15;49m \u001b[38;5;12;49m│\u001b[27;38H│\u001b[1m\u001b[38;5;3;49mName: \u001b[22m\u001b[38;5;15;49m \u001b[38;5;12;49m│\u001b[28;38H│\u001b[38;5;7;49mEmail: \u001b[38;5;15;49m \u001b[38;5;12;49m│\u001b[29;38H│\u001b[38;5;15;49m "] 60 | [10.673711, "o", " \u001b[38;5;12;49m│\u001b[30;38H│\u001b[38;5;5;49m↑/↓: Switch fields | Enter: Submit | Esc: Cancel\u001b[38;5;15;49m \u001b[38;5;12;49m│\u001b[31;38H│\u001b[38;5;15;49m \u001b[38;5;12;49m│\u001b[32;38H│\u001b[38;5;15;49m \u001b[38;5;12;49m│\u001b[33;38H│\u001b[38;5;15;49m \u001b[38;5;12;49m│\u001b[34;38H└──────────────────────────────────────────────────────────────────────────────────────────"] 61 | [10.67379, "o", "─────────────────────┘\u001b[39m\u001b[49m\u001b[59m\u001b[0m\u001b[?25l"] 62 | [13.587866, "o", "\u001b[27;45H\u001b[1m\u001b[38;5;3;49ma\u001b[39m\u001b[49m\u001b[59m\u001b[0m\u001b[?25l"] 63 | [13.726268, "o", "\u001b[27;46H\u001b[1m\u001b[38;5;3;49mn\u001b[39m\u001b[49m\u001b[59m\u001b[0m\u001b[?25l"] 64 | [13.851493, "o", "\u001b[27;47H\u001b[1m\u001b[38;5;3;49mo\u001b[39m\u001b[49m\u001b[59m\u001b[0m\u001b[?25l"] 65 | [14.010693, "o", "\u001b[27;48H\u001b[1m\u001b[38;5;3;49mt\u001b[39m\u001b[49m\u001b[59m\u001b[0m\u001b[?25l"] 66 | [14.110603, "o", "\u001b[27;49H\u001b[1m\u001b[38;5;3;49mh\u001b[39m\u001b[49m\u001b[59m\u001b[0m\u001b[?25l"] 67 | [14.193806, "o", "\u001b[27;50H\u001b[1m\u001b[38;5;3;49me\u001b[39m\u001b[49m\u001b[59m\u001b[0m\u001b[?25l"] 68 | [14.27813, "o", "\u001b[27;51H\u001b[1m\u001b[38;5;3;49mr\u001b[39m\u001b[49m\u001b[59m\u001b[0m\u001b[?25l"] 69 | [14.378259, "o", "\u001b[27;52H\u001b[1m\u001b[38;5;3;49me\u001b[39m\u001b[49m\u001b[59m\u001b[0m\u001b[?25l"] 70 | [14.48458, "o", "\u001b[27;53H\u001b[1m\u001b[38;5;3;49mr\u001b[39m\u001b[49m\u001b[59m\u001b[0m\u001b[?25l"] 71 | [15.109257, "o", "\u001b[27;54H\u001b[1m\u001b[38;5;3;49m \u001b[39m\u001b[49m\u001b[59m\u001b[0m\u001b[?25l"] 72 | [15.232375, "o", "\u001b[27;55H\u001b[1m\u001b[38;5;3;49ma\u001b[39m\u001b[49m\u001b[59m\u001b[0m\u001b[?25l"] 73 | [15.318023, "o", "\u001b[27;56H\u001b[1m\u001b[38;5;3;49mc\u001b[39m\u001b[49m\u001b[59m\u001b[0m\u001b[?25l"] 74 | [15.684889, "o", "\u001b[27;57H\u001b[1m\u001b[38;5;3;49mc\u001b[39m\u001b[49m\u001b[59m\u001b[0m\u001b[?25l"] 75 | [15.800121, "o", "\u001b[27;58H\u001b[1m\u001b[38;5;3;49mo\u001b[39m\u001b[49m\u001b[59m\u001b[0m\u001b[?25l"] 76 | [16.003776, "o", "\u001b[27;59H\u001b[1m\u001b[38;5;3;49mu\u001b[39m\u001b[49m\u001b[59m\u001b[0m\u001b[?25l"] 77 | [16.065656, "o", "\u001b[27;60H\u001b[1m\u001b[38;5;3;49mn\u001b[39m\u001b[49m\u001b[59m\u001b[0m\u001b[?25l"] 78 | [16.168624, "o", "\u001b[27;61H\u001b[1m\u001b[38;5;3;49mt\u001b[39m\u001b[49m\u001b[59m\u001b[0m\u001b[?25l"] 79 | [16.803252, "o", "\u001b[27;39H\u001b[38;5;7;49mName: anotherer account\u001b[28;39H\u001b[1m\u001b[38;5;6;49mEmail: \u001b[39m\u001b[49m\u001b[59m\u001b[0m\u001b[?25l"] 80 | [17.454954, "o", "\u001b[28;46H\u001b[1m\u001b[38;5;6;49mk\u001b[39m\u001b[49m\u001b[59m\u001b[0m\u001b[?25l"] 81 | [17.47622, "o", "\u001b[28;47H\u001b[1m\u001b[38;5;6;49ma\u001b[39m\u001b[49m\u001b[59m\u001b[0m\u001b[?25l"] 82 | [17.613664, "o", "\u001b[28;48H\u001b[1m\u001b[38;5;6;49mr\u001b[39m\u001b[49m\u001b[59m\u001b[0m\u001b[?25l"] 83 | [17.707475, "o", "\u001b[28;49H\u001b[1m\u001b[38;5;6;49ms\u001b[39m\u001b[49m\u001b[59m\u001b[0m\u001b[?25l"] 84 | [17.854147, "o", "\u001b[28;50H\u001b[1m\u001b[38;5;6;49ma\u001b[39m\u001b[49m\u001b[59m\u001b[0m\u001b[?25l"] 85 | [17.941551, "o", "\u001b[28;51H\u001b[1m\u001b[38;5;6;49md\u001b[39m\u001b[49m\u001b[59m\u001b[0m\u001b[?25l"] 86 | [18.001225, "o", "\u001b[28;52H\u001b[1m\u001b[38;5;6;49ms\u001b[39m\u001b[49m\u001b[59m\u001b[0m\u001b[?25l"] 87 | [18.11652, "o", "\u001b[28;53H\u001b[1m\u001b[38;5;6;49ma\u001b[39m\u001b[49m\u001b[59m\u001b[0m\u001b[?25l"] 88 | [18.136378, "o", "\u001b[28;54H\u001b[1m\u001b[38;5;6;49ms\u001b[39m\u001b[49m\u001b[59m\u001b[0m\u001b[?25l"] 89 | [18.507542, "o", "\u001b[28;55H\u001b[1m\u001b[38;5;6;49m@\u001b[39m\u001b[49m\u001b[59m\u001b[0m\u001b[?25l"] 90 | [18.738287, "o", "\u001b[28;56H\u001b[1m\u001b[38;5;6;49mg\u001b[39m\u001b[49m\u001b[59m\u001b[0m\u001b[?25l"] 91 | [18.856553, "o", "\u001b[28;57H\u001b[1m\u001b[38;5;6;49mm\u001b[39m\u001b[49m\u001b[59m\u001b[0m\u001b[?25l"] 92 | [18.981875, "o", "\u001b[28;58H\u001b[1m\u001b[38;5;6;49ma\u001b[39m\u001b[49m\u001b[59m\u001b[0m\u001b[?25l"] 93 | [19.065093, "o", "\u001b[28;59H\u001b[1m\u001b[38;5;6;49mi\u001b[39m\u001b[49m\u001b[59m\u001b[0m\u001b[?25l"] 94 | [19.540111, "o", "\u001b[28;60H\u001b[1m\u001b[38;5;6;49ml\u001b[39m\u001b[49m\u001b[59m\u001b[0m\u001b[?25l"] 95 | [19.760751, "o", "\u001b[28;61H\u001b[1m\u001b[38;5;6;49m.\u001b[39m\u001b[49m\u001b[59m\u001b[0m\u001b[?25l"] 96 | [19.94346, "o", "\u001b[28;62H\u001b[1m\u001b[38;5;6;49mc\u001b[39m\u001b[49m\u001b[59m\u001b[0m\u001b[?25l"] 97 | [20.056329, "o", "\u001b[28;63H\u001b[1m\u001b[38;5;6;49mo\u001b[39m\u001b[49m\u001b[59m\u001b[0m\u001b[?25l"] 98 | [20.117765, "o", "\u001b[28;64H\u001b[1m\u001b[38;5;6;49mm\u001b[39m\u001b[49m\u001b[59m\u001b[0m\u001b[?25l"] 99 | [20.82768, "o", "\u001b[14;6H\u001b[38;5;7;49manotherer account\u001b[15;6H\u001b[38;5;8;49mkarsadsas@gmail.com\u001b[24;38H\u001b[39;49m ││ \u001b[25;38H ││ \u001b[26;38H ││ \u001b[27;38H ││ \u001b[28;38H ││ \u001b[29;38H ││ \u001b[30;38H ││ \u001b[31;38H ││ "] 100 | [20.827756, "o", " \u001b[32;38H ││ \u001b[33;38H ││ \u001b[34;38H ││ \u001b[56;2H\u001b[38;5;14;49mNew\u001b[56;6Haccount a\u001b[56;16Hded\u001b[56;20Hsuccessfully!\u001b[39m\u001b[49m\u001b[59m\u001b[0m\u001b[?25l"] 101 | [21.539314, "o", "\u001b[6;65H\u001b[38;5;10;49manother account\u001b[7;66H\u001b[38;5;12;49manasda@gmail.com\u001b[39;49m \u001b[8;59H\u001b[38;5;7;49m○ Inactive\u001b[10;3H\u001b[39;49m \u001b[1m\u001b[38;5;10;49mkaran\u001b[22m\u001b[39;49m \u001b[11;3H \u001b[1m\u001b[38;5;8;49mkaranjanthe@gmail.com\u001b[22m\u001b[39;49m \u001b[12;3H\u001b[38;5;15;48;5;8m>> another account \u001b[13;3H anasda@gmail.com \u001b[56;2H\u001b[38;5;14;49mS\u001b[56;4Hlected \u001b[56;12Hext account \u001b[39m\u001b[49m\u001b[59m\u001b[0m\u001b[?25l"] 102 | [21.801196, "o", "\u001b[6;72H\u001b[38;5;10;49mer account\u001b[7;66H\u001b[38;5;12;49mkar\u001b[7;70Hadsas@gmail.com\u001b[12;3H\u001b[39;49m \u001b[38;5;7;49manother account\u001b[39;49m \u001b[13;3H \u001b[38;5;8;49manasda@gmail.com\u001b[39;49m \u001b[14;3H\u001b[38;5;15;48;5;8m>> anotherer account \u001b[15;3H karsadsas@gmail.com \u001b[39m\u001b[49m\u001b[59m\u001b[0m\u001b[?25l"] 103 | [22.411904, "o", "\u001b[8;59H\u001b[38;5;10;49m● Active\u001b[39;49m \u001b[10;6H\u001b[38;5;7;49mkaran\u001b[11;6H\u001b[38;5;8;49mkaranjanthe@gmail.com\u001b[14;6H\u001b[1m\u001b[38;5;15;48;5;8manotherer account\u001b[15;6Hkarsadsas@gmail.com\u001b[56;2H\u001b[22m\u001b[38;5;14;49mActivated account: anotherer\u001b[56;31Haccount\u001b[39m\u001b[49m\u001b[59m\u001b[0m\u001b[?25l"] 104 | [22.928567, "o", "\u001b[6;72H\u001b[38;5;10;49m account\u001b[39;49m \u001b[7;66H\u001b[38;5;12;49mana\u001b[7;70Hda@gmail.com\u001b[39;49m \u001b[8;59H\u001b[38;5;7;49m○ Inactive\u001b[12;3H\u001b[38;5;15;48;5;8m>> another account \u001b[13;3H anasda@gmail.com \u001b[14;3H\u001b[39;49m \u001b[1m\u001b[38;5;10;49manotherer account\u001b[22m\u001b[39;49m \u001b[15;3H \u001b[1m\u001b[38;5;8;49mkarsadsas@gmail.com\u001b[22m\u001b[39;49m \u001b[56;2H\u001b[38;5;14;49mSelected previous acc\u001b[56;24Hunt \u001b[56;31H \u001b[39m\u001b[49m\u001b[59m\u001b[0m\u001b[?25l"] 105 | [23.123668, "o", "\u001b[6;65H\u001b[38;5;10;49mkaran\u001b[39;49m \u001b[7;66H\u001b[38;5;12;49mkaranjanthe@gmail.com\u001b[10;3H\u001b[38;5;15;48;5;8m>> karan \u001b[11;3H karanjanthe@gmail.com \u001b[12;3H\u001b[39;49m \u001b[38;5;7;49manother account\u001b[39;49m \u001b[13;3H \u001b[38;5;8;49manasda@gmail.com\u001b[39;49m \u001b[39m\u001b[49m\u001b[59m\u001b[0m\u001b[?25l"] 106 | [23.524405, "o", "\u001b[8;59H\u001b[38;5;10;49m● Active\u001b[39;49m \u001b[10;6H\u001b[1m\u001b[38;5;15;48;5;8mkaran\u001b[11;6Hkaranjanthe@gmail.com\u001b[14;6H\u001b[22m\u001b[38;5;7;49manotherer account\u001b[15;6H\u001b[38;5;8;49mkarsadsas@gmail.com\u001b[56;2H\u001b[38;5;14;49mActivated account: kara\u001b[56;26H \u001b[39m\u001b[49m\u001b[59m\u001b[0m\u001b[?25l"] 107 | [24.178778, "o", "\u001b[56;2H\u001b[38;5;14;49mSSH key copied to clipboard\u001b[39m\u001b[49m\u001b[59m\u001b[0m\u001b[?25l"] 108 | [24.738437, "o", "\u001b[6;65H\u001b[38;5;10;49mhello\u001b[7;66H\u001b[38;5;12;49mhello@gmail.com\u001b[39;49m \u001b[8;3H\u001b[38;5;15;48;5;8m>> hello \u001b[8;59H\u001b[38;5;7;49m○ Inactive\u001b[9;3H\u001b[38;5;15;48;5;8m hello@gmail.com \u001b[10;3H\u001b[39;49m \u001b[1m\u001b[38;5;10;49mkaran\u001b[22m\u001b[39;49m \u001b[11;3H \u001b[1m\u001b[38;5;8;49mkaranjanthe@gmail.com\u001b[22m\u001b[39;49m \u001b[56;3H\u001b[38;5;14;49melected previous\u001b[56;20Haccount \u001b[39m\u001b[49m\u001b[59m\u001b[0m\u001b[?25l"] 109 | [24.933082, "o", "\u001b[6;3H\u001b[38;5;15;48;5;8m>> another \u001b[6;65H\u001b[38;5;10;49manother\u001b[7;3H\u001b[38;5;15;48;5;8m antoert@gmail.com \u001b[7;66H\u001b[38;5;12;49mantoert@gmail.com\u001b[8;3H\u001b[39;49m \u001b[38;5;7;49mhello\u001b[39;49m \u001b[9;3H \u001b[38;5;8;49mhello@gmail.com\u001b[39;49m \u001b[39m\u001b[49m\u001b[59m\u001b[0m\u001b[?25l"] 110 | [25.520997, "o", "\u001b[56;3H\u001b[38;5;14;49mSH key copied to\u001b[56;20Hclipboard\u001b[39m\u001b[49m\u001b[59m\u001b[0m\u001b[?25l"] 111 | [28.246748, "o", "\u001b[39m\u001b[49m\u001b[59m\u001b[0m\u001b[?25l"] 112 | [28.684314, "o", "\u001b[?1049l\u001b[?25h"] 113 | [28.687068, "o", "\u001b[1m\u001b[7m%\u001b[27m\u001b[1m\u001b[0m \r \r"] 114 | [28.689038, "o", "\u001b]2;karanjanthe@Karans-MacBook-Air:~/Projects/lazygh\u0007\u001b]1;..ojects/lazygh\u0007"] 115 | [28.694786, "o", "\u001b]7;file://Karans-MacBook-Air.local/Users/karanjanthe/Projects/lazygh\u001b\\"] 116 | [28.718743, "o", "\r\u001b[0m\u001b[27m\u001b[24m\u001b[J(base) \u001b[01;32m➜ \u001b[36mlazygh\u001b[00m \u001b[01;34mgit:(\u001b[31mmain\u001b[34m) \u001b[33m✗\u001b[00m \u001b[K"] 117 | [28.718901, "o", "\u001b[?1h\u001b=\u001b[?2004h"] 118 | [29.247476, "o", "\u001b[?2004l"] 119 | [29.247541, "o", "\r\r\n"] 120 | -------------------------------------------------------------------------------- /demo.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/KMJ-007/lazygh/366d4ba94d8718c75e8e03b8b6df28a227400892/demo.gif -------------------------------------------------------------------------------- /oranda.json: -------------------------------------------------------------------------------- 1 | { 2 | "build": { 3 | "path_prefix": "lazygh" 4 | }, 5 | "project": { 6 | "homepage": "https://kmj-007.github.io/lazygh", 7 | "repository": "https://github.com/KMJ-007/lazygh" 8 | }, 9 | "styles": { 10 | "theme": "axodark", 11 | "favicon": "https://www.axo.dev/favicon.ico" 12 | }, 13 | "marketing": { 14 | "analytics": { 15 | "google": { 16 | "tracking_id": "G-TKS59X89PP" 17 | } 18 | }, 19 | "social": { 20 | "image": "https://www.axo.dev/meta_small.jpeg", 21 | "image_alt": "axo", 22 | "twitter_account": "@KaranJanthe" 23 | } 24 | }, 25 | "components": { 26 | "changelog": true, 27 | "artifacts": { 28 | "package_managers": { 29 | "preferred": { 30 | "cargo": "cargo install lazygh" 31 | }, 32 | "additional": { 33 | "npx": "npx lazygh" 34 | } 35 | } 36 | } 37 | } 38 | } 39 | -------------------------------------------------------------------------------- /src/app.rs: -------------------------------------------------------------------------------- 1 | // shitty code, need to modularise it 2 | 3 | use arboard::Clipboard; 4 | use color_eyre::Result; 5 | use crossterm::event::{self, Event, KeyCode, KeyEvent, KeyEventKind, KeyModifiers}; 6 | use ratatui::{ 7 | layout::{Constraint, Direction, Layout, Rect}, 8 | style::{Color, Modifier, Style}, 9 | text::{Line, Span, Text}, 10 | widgets::{Block, Borders, Clear, List, ListItem, ListState, Paragraph, Tabs}, 11 | Frame, 12 | }; 13 | use regex::Regex; 14 | use std::default::Default; 15 | use std::fmt; 16 | use std::{ 17 | process::Command, 18 | time::{Duration, Instant}, 19 | }; 20 | 21 | use crate::db::{ 22 | add_account, get_current_user, get_ssh_key, init_db, list_accounts, remove_account, 23 | switch_account, Account as DbAccount, 24 | }; 25 | 26 | pub struct App { 27 | running: bool, 28 | accounts: Vec, 29 | active_account_index: Option, 30 | current_tab: Tab, 31 | input: String, 32 | input_mode: InputMode, 33 | status_message: String, 34 | status_time: Instant, 35 | active_account: Option, 36 | popup: PopupType, 37 | new_account_name: String, 38 | new_account_email: String, 39 | clipboard: Clipboard, 40 | add_account_focus: AddAccountField, 41 | } 42 | 43 | impl fmt::Debug for App { 44 | fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { 45 | f.debug_struct("App") 46 | .field("running", &self.running) 47 | .field("accounts", &self.accounts) 48 | .field("active_account_index", &self.active_account_index) 49 | .field("current_tab", &self.current_tab) 50 | .field("input", &self.input) 51 | .field("input_mode", &self.input_mode) 52 | .field("status_message", &self.status_message) 53 | .field("status_time", &self.status_time) 54 | .field("active_account", &self.active_account) 55 | .field("popup", &self.popup) 56 | .field("new_account_name", &self.new_account_name) 57 | .field("new_account_email", &self.new_account_email) 58 | .field("add_account_focus", &self.add_account_focus) 59 | .finish_non_exhaustive() 60 | } 61 | } 62 | 63 | #[derive(Debug, PartialEq, Clone, Copy)] 64 | enum Tab { 65 | Accounts, 66 | Help, 67 | } 68 | 69 | #[derive(Debug, PartialEq, Clone, Copy)] 70 | enum InputMode { 71 | Normal, 72 | Editing, 73 | } 74 | 75 | #[derive(Debug, PartialEq, Clone)] 76 | enum PopupType { 77 | AddAccount, 78 | RemoveConfirmation, 79 | None, 80 | } 81 | 82 | #[derive(Debug, PartialEq, Clone, Copy)] 83 | enum AddAccountField { 84 | Name, 85 | Email, 86 | } 87 | 88 | impl Default for Tab { 89 | fn default() -> Self { 90 | Tab::Accounts 91 | } 92 | } 93 | 94 | impl Default for InputMode { 95 | fn default() -> Self { 96 | InputMode::Normal 97 | } 98 | } 99 | 100 | impl Default for App { 101 | fn default() -> Self { 102 | Self { 103 | running: false, 104 | accounts: Vec::new(), 105 | active_account_index: None, 106 | current_tab: Tab::default(), 107 | input: String::new(), 108 | input_mode: InputMode::default(), 109 | status_message: String::new(), 110 | status_time: Instant::now(), 111 | active_account: None, 112 | popup: PopupType::None, 113 | new_account_name: String::new(), 114 | new_account_email: String::new(), 115 | clipboard: Clipboard::new().expect("Failed to initialize clipboard"), 116 | add_account_focus: AddAccountField::Name, 117 | } 118 | } 119 | } 120 | 121 | impl App { 122 | pub fn new() -> Result { 123 | init_db()?; 124 | let accounts = list_accounts().unwrap_or_default(); 125 | let mut app = Self { 126 | running: true, 127 | accounts, 128 | active_account_index: None, 129 | current_tab: Tab::default(), 130 | input: String::new(), 131 | input_mode: InputMode::default(), 132 | status_message: String::new(), 133 | status_time: Instant::now(), 134 | active_account: None, 135 | popup: PopupType::None, 136 | new_account_name: String::new(), 137 | new_account_email: String::new(), 138 | clipboard: Clipboard::new().expect("Failed to initialize clipboard"), 139 | add_account_focus: AddAccountField::Name, 140 | }; 141 | app.status_message = "Welcome to LazyGH!".to_string(); 142 | 143 | // Set the active account index based on the current Git user 144 | if let Ok(Some(current_user)) = get_current_user() { 145 | if let Some(index) = app 146 | .accounts 147 | .iter() 148 | .position(|a| a.email == current_user.email) 149 | { 150 | app.active_account_index = Some(index); 151 | app.active_account = Some(index); 152 | } 153 | } 154 | 155 | Ok(app) 156 | } 157 | 158 | pub fn run( 159 | &mut self, 160 | terminal: &mut ratatui::Terminal, 161 | ) -> Result<()> { 162 | while self.running { 163 | terminal.draw(|frame| self.draw(frame))?; 164 | 165 | if let Event::Key(key) = event::read()? { 166 | self.handle_key_event(key); 167 | } 168 | } 169 | Ok(()) 170 | } 171 | 172 | fn draw(&mut self, frame: &mut Frame) { 173 | let chunks = Layout::default() 174 | .direction(Direction::Vertical) 175 | .margin(1) 176 | .constraints( 177 | [ 178 | Constraint::Length(3), 179 | Constraint::Min(0), 180 | Constraint::Length(1), 181 | ] 182 | .as_ref(), 183 | ) 184 | .split(frame.size()); 185 | 186 | self.draw_tabs(frame, chunks[0]); 187 | self.draw_content(frame, chunks[1]); 188 | self.draw_status_bar(frame, chunks[2]); 189 | 190 | // Draw popup if there is one 191 | if self.popup != PopupType::None { 192 | self.draw_popup(frame); 193 | } 194 | } 195 | 196 | fn draw_tabs(&self, frame: &mut Frame, area: Rect) { 197 | let titles = vec!["Accounts", "Help"]; 198 | let tabs = Tabs::new(titles) 199 | .block(Block::default().borders(Borders::ALL).title("LazyGH")) 200 | .select(self.current_tab as usize) 201 | .style(Style::default().fg(Color::DarkGray)) 202 | .highlight_style( 203 | Style::default() 204 | .fg(Color::LightGreen) 205 | .add_modifier(Modifier::BOLD), 206 | ); 207 | frame.render_widget(tabs, area); 208 | } 209 | 210 | fn draw_content(&mut self, frame: &mut Frame, area: Rect) { 211 | match self.current_tab { 212 | Tab::Accounts => self.draw_accounts(frame, area), 213 | Tab::Help => self.draw_help(frame, area), 214 | } 215 | } 216 | 217 | fn draw_accounts(&mut self, frame: &mut Frame, area: Rect) { 218 | let chunks = Layout::default() 219 | .direction(Direction::Horizontal) 220 | .constraints([Constraint::Percentage(30), Constraint::Percentage(70)].as_ref()) 221 | .split(area); 222 | 223 | if self.accounts.is_empty() { 224 | let message = vec![ 225 | Line::from(Span::styled( 226 | "No accounts", 227 | Style::default().fg(Color::Yellow), 228 | )), 229 | Line::from(""), 230 | Line::from(Span::styled( 231 | "Press 'a' to add a new account", 232 | Style::default().fg(Color::Green), 233 | )), 234 | ]; 235 | let paragraph = Paragraph::new(message) 236 | .block(Block::default().borders(Borders::ALL).title("Accounts")) 237 | .alignment(ratatui::layout::Alignment::Center); 238 | frame.render_widget(paragraph, chunks[0]); 239 | } else { 240 | let items: Vec = self 241 | .accounts 242 | .iter() 243 | .enumerate() 244 | .map(|(index, account)| { 245 | let style = if Some(index) == self.active_account { 246 | Style::default() 247 | .fg(Color::LightGreen) 248 | .add_modifier(Modifier::BOLD) 249 | } else { 250 | Style::default().fg(Color::Gray) 251 | }; 252 | ListItem::new(vec![ 253 | Line::from(Span::styled(&account.name, style)), 254 | Line::from(Span::styled(&account.email, style.fg(Color::DarkGray))), 255 | ]) 256 | }) 257 | .collect(); 258 | 259 | let accounts = List::new(items) 260 | .block(Block::default().borders(Borders::ALL).title("Accounts")) 261 | .highlight_style(Style::default().bg(Color::DarkGray).fg(Color::White)) 262 | .highlight_symbol(">> "); 263 | 264 | let mut list_state = ListState::default(); 265 | list_state.select(self.active_account_index); 266 | frame.render_stateful_widget(accounts, chunks[0], &mut list_state); 267 | } 268 | 269 | if let Some(index) = self.active_account_index { 270 | let account = &self.accounts[index]; 271 | let info = Paragraph::new(vec![ 272 | Line::from(vec![ 273 | Span::styled("Name: ", Style::default().add_modifier(Modifier::BOLD)), 274 | Span::styled(&account.name, Style::default().fg(Color::LightGreen)), 275 | ]), 276 | Line::from(vec![ 277 | Span::styled("Email: ", Style::default().add_modifier(Modifier::BOLD)), 278 | Span::styled(&account.email, Style::default().fg(Color::LightBlue)), 279 | ]), 280 | Line::from(Span::styled( 281 | if Some(index) == self.active_account { 282 | "● Active" 283 | } else { 284 | "○ Inactive" 285 | }, 286 | Style::default().fg(if Some(index) == self.active_account { 287 | Color::LightGreen 288 | } else { 289 | Color::Gray 290 | }), 291 | )), 292 | Line::from(""), 293 | Line::from(Span::styled( 294 | "Enter: Copy SSH key | Space: Set active", 295 | Style::default().fg(Color::Yellow), 296 | )), 297 | Line::from(Span::styled( 298 | "a: Add new account | r: Remove account", 299 | Style::default().fg(Color::Cyan), 300 | )), 301 | ]) 302 | .block(Block::default().borders(Borders::ALL).title("Account Info")); 303 | frame.render_widget(info, chunks[1]); 304 | } else { 305 | let info = Paragraph::new(vec![ 306 | Line::from(Span::styled( 307 | "No account selected", 308 | Style::default().fg(Color::Yellow), 309 | )), 310 | Line::from(""), 311 | Line::from("Select an account from the list"), 312 | Line::from("or add a new one with 'a'"), 313 | ]) 314 | .block(Block::default().borders(Borders::ALL).title("Account Info")) 315 | .alignment(ratatui::layout::Alignment::Center); 316 | frame.render_widget(info, chunks[1]); 317 | } 318 | } 319 | 320 | fn draw_help(&self, frame: &mut Frame, area: Rect) { 321 | let text = vec![ 322 | Line::from(Span::styled( 323 | "Keyboard Shortcuts", 324 | Style::default().add_modifier(Modifier::BOLD), 325 | )), 326 | Line::from(""), 327 | Line::from(vec![ 328 | Span::styled("Tab", Style::default().fg(Color::Yellow)), 329 | Span::raw(": Switch between tabs"), 330 | ]), 331 | Line::from(vec![ 332 | Span::styled("↑/↓", Style::default().fg(Color::LightGreen)), 333 | Span::raw(": Navigate accounts"), 334 | ]), 335 | Line::from(vec![ 336 | Span::styled("Enter", Style::default().fg(Color::LightYellow)), 337 | Span::raw(": Copy SSH key to clipboard"), 338 | ]), 339 | Line::from(vec![ 340 | Span::styled("Space", Style::default().fg(Color::LightYellow)), 341 | Span::raw(": Set active account"), 342 | ]), 343 | Line::from(vec![ 344 | Span::styled("a", Style::default().fg(Color::Cyan)), 345 | Span::raw(": Add new account"), 346 | ]), 347 | Line::from(vec![ 348 | Span::styled("r", Style::default().fg(Color::Red)), 349 | Span::raw(": Remove selected account"), 350 | ]), 351 | Line::from(vec![ 352 | Span::styled("q", Style::default().fg(Color::LightRed)), 353 | Span::raw(": Quit"), 354 | ]), 355 | Line::from(""), 356 | Line::from(vec![ 357 | Span::raw("Follow the developer on Twitter/X: "), 358 | Span::styled( 359 | "@KaranJanthe", 360 | Style::default() 361 | .fg(Color::LightBlue) 362 | .add_modifier(Modifier::UNDERLINED) 363 | .add_modifier(Modifier::ITALIC), 364 | ), 365 | ]), 366 | ]; 367 | let help = Paragraph::new(text) 368 | .block(Block::default().borders(Borders::ALL).title("Help")) 369 | .style(Style::default().fg(Color::White)); 370 | frame.render_widget(help, area); 371 | } 372 | 373 | fn draw_status_bar(&self, frame: &mut Frame, area: Rect) { 374 | let status = Paragraph::new(self.status_message.clone()) 375 | .style(Style::default().fg(Color::LightCyan)); 376 | frame.render_widget(status, area); 377 | } 378 | 379 | fn draw_popup(&self, frame: &mut Frame) { 380 | let area = centered_rect(60, 20, frame.size()); 381 | frame.render_widget(Clear, area); // Clear the background 382 | 383 | match &self.popup { 384 | PopupType::AddAccount => { 385 | let name_style = if self.add_account_focus == AddAccountField::Name { 386 | Style::default() 387 | .fg(Color::Yellow) 388 | .add_modifier(Modifier::BOLD) 389 | } else { 390 | Style::default().fg(Color::Gray) 391 | }; 392 | let email_style = if self.add_account_focus == AddAccountField::Email { 393 | Style::default() 394 | .fg(Color::Cyan) 395 | .add_modifier(Modifier::BOLD) 396 | } else { 397 | Style::default().fg(Color::Gray) 398 | }; 399 | 400 | let popup = Paragraph::new(vec![ 401 | Line::from(Span::styled( 402 | "Add New Account", 403 | Style::default() 404 | .fg(Color::Green) 405 | .add_modifier(Modifier::BOLD), 406 | )), 407 | Line::from(""), 408 | Line::from(vec![ 409 | Span::styled("Name: ", name_style), 410 | Span::styled(&self.new_account_name, name_style), 411 | ]), 412 | Line::from(vec![ 413 | Span::styled("Email: ", email_style), 414 | Span::styled(&self.new_account_email, email_style), 415 | ]), 416 | Line::from(""), 417 | Line::from(Span::styled( 418 | "↑/↓: Switch fields | Enter: Submit | Esc: Cancel", 419 | Style::default().fg(Color::Magenta), 420 | )), 421 | ]) 422 | .block( 423 | Block::default() 424 | .title("Add Account") 425 | .borders(Borders::ALL) 426 | .border_style(Style::default().fg(Color::LightBlue)), 427 | ) 428 | .style(Style::default().fg(Color::White)); 429 | frame.render_widget(popup, area); 430 | } 431 | PopupType::RemoveConfirmation => { 432 | if let Some(index) = self.active_account_index { 433 | if let Some(account) = self.accounts.get(index) { 434 | let popup = Paragraph::new(vec![ 435 | Line::from(Span::styled( 436 | "Remove Account", 437 | Style::default().fg(Color::Red).add_modifier(Modifier::BOLD), 438 | )), 439 | Line::from(""), 440 | Line::from(vec![Span::raw( 441 | "Are you sure you want to remove this account?", 442 | )]), 443 | Line::from(""), 444 | Line::from(vec![ 445 | Span::styled( 446 | "Name: ", 447 | Style::default().add_modifier(Modifier::BOLD), 448 | ), 449 | Span::styled(&account.name, Style::default().fg(Color::Yellow)), 450 | ]), 451 | Line::from(vec![ 452 | Span::styled( 453 | "Email: ", 454 | Style::default().add_modifier(Modifier::BOLD), 455 | ), 456 | Span::styled(&account.email, Style::default().fg(Color::Cyan)), 457 | ]), 458 | Line::from(""), 459 | Line::from(Span::styled( 460 | "Press 'y' to confirm", 461 | Style::default().fg(Color::Green), 462 | )), 463 | Line::from(Span::styled( 464 | "Press 'n' to cancel", 465 | Style::default().fg(Color::Red), 466 | )), 467 | ]) 468 | .block( 469 | Block::default() 470 | .title("Confirm Removal") 471 | .borders(Borders::ALL) 472 | .border_style(Style::default().fg(Color::LightRed)), 473 | ) 474 | .style(Style::default().fg(Color::White)); 475 | frame.render_widget(popup, area); 476 | } else { 477 | // Handle the case where the account doesn't exist 478 | let error_popup = Paragraph::new("Error: Account not found") 479 | .block(Block::default().title("Error").borders(Borders::ALL)) 480 | .style(Style::default().fg(Color::Red)); 481 | frame.render_widget(error_popup, area); 482 | } 483 | } else { 484 | // Handle the case where no account is selected 485 | let error_popup = Paragraph::new("Error: No account selected") 486 | .block(Block::default().title("Error").borders(Borders::ALL)) 487 | .style(Style::default().fg(Color::Red)); 488 | frame.render_widget(error_popup, area); 489 | } 490 | } 491 | PopupType::None => {} 492 | } 493 | } 494 | 495 | fn handle_key_event(&mut self, key: KeyEvent) { 496 | match &self.popup { 497 | PopupType::None => match (self.input_mode, key.code) { 498 | (InputMode::Normal, KeyCode::Char('q')) => self.quit(), 499 | (InputMode::Normal, KeyCode::Char('c')) 500 | if key.modifiers == KeyModifiers::CONTROL => 501 | { 502 | self.quit() 503 | } 504 | (InputMode::Normal, KeyCode::Tab) => self.next_tab(), 505 | (InputMode::Normal, KeyCode::BackTab) => self.previous_tab(), 506 | (InputMode::Normal, KeyCode::Char('h')) => self.switch_to_tab(Tab::Help), 507 | (InputMode::Normal, KeyCode::Char('1')) => self.switch_to_tab(Tab::Accounts), 508 | (InputMode::Normal, KeyCode::Char('a')) => { 509 | self.popup = PopupType::AddAccount; 510 | self.new_account_name.clear(); 511 | self.new_account_email.clear(); 512 | self.input_mode = InputMode::Editing; 513 | } 514 | (InputMode::Normal, KeyCode::Char('r')) => { 515 | if self.active_account_index.is_some() { 516 | self.popup = PopupType::RemoveConfirmation; 517 | } 518 | } 519 | (InputMode::Normal, KeyCode::Up) => self.select_previous_account(), 520 | (InputMode::Normal, KeyCode::Down) => self.select_next_account(), 521 | (InputMode::Normal, KeyCode::Enter) => { 522 | if let Some(index) = self.active_account_index { 523 | self.copy_ssh_key_to_clipboard(index); 524 | } 525 | } 526 | (InputMode::Normal, KeyCode::Char(' ')) => { 527 | if let Some(index) = self.active_account_index { 528 | self.set_active_account(index); 529 | } 530 | } 531 | _ => {} 532 | }, 533 | PopupType::AddAccount => match key.code { 534 | KeyCode::Enter => { 535 | if !self.new_account_name.is_empty() && !self.new_account_email.is_empty() { 536 | self.submit_new_account(); 537 | } else { 538 | self.set_status("Please fill both name and email"); 539 | } 540 | } 541 | KeyCode::Esc => { 542 | self.popup = PopupType::None; 543 | self.input_mode = InputMode::Normal; 544 | } 545 | KeyCode::Up | KeyCode::Down | KeyCode::Tab | KeyCode::BackTab => { 546 | self.add_account_focus = match self.add_account_focus { 547 | AddAccountField::Name => AddAccountField::Email, 548 | AddAccountField::Email => AddAccountField::Name, 549 | }; 550 | } 551 | KeyCode::Char(c) => match self.add_account_focus { 552 | AddAccountField::Name => self.new_account_name.push(c), 553 | AddAccountField::Email => self.new_account_email.push(c), 554 | }, 555 | KeyCode::Backspace => match self.add_account_focus { 556 | AddAccountField::Name => { 557 | self.new_account_name.pop(); 558 | } 559 | AddAccountField::Email => { 560 | self.new_account_email.pop(); 561 | } 562 | }, 563 | _ => {} 564 | }, 565 | PopupType::RemoveConfirmation => match key.code { 566 | KeyCode::Char('y') | KeyCode::Char('Y') => { 567 | self.remove_account(); 568 | self.popup = PopupType::None; 569 | } 570 | KeyCode::Char('n') | KeyCode::Char('N') | KeyCode::Esc => { 571 | self.popup = PopupType::None; 572 | } 573 | _ => {} 574 | }, 575 | } 576 | } 577 | 578 | fn set_status(&mut self, message: &str) { 579 | self.status_message = message.to_string(); 580 | self.status_time = Instant::now(); 581 | } 582 | 583 | fn quit(&mut self) { 584 | self.running = false; 585 | } 586 | 587 | fn next_tab(&mut self) { 588 | self.current_tab = match self.current_tab { 589 | Tab::Accounts => Tab::Help, 590 | Tab::Help => Tab::Accounts, 591 | }; 592 | self.set_status(&format!("Switched to {} tab", self.current_tab_name())); 593 | } 594 | 595 | fn previous_tab(&mut self) { 596 | self.next_tab(); // Since we only have two tabs, previous is the same as next 597 | } 598 | 599 | fn current_tab_name(&self) -> &'static str { 600 | match self.current_tab { 601 | Tab::Accounts => "Accounts", 602 | Tab::Help => "Help", 603 | } 604 | } 605 | 606 | fn select_previous_account(&mut self) { 607 | if !self.accounts.is_empty() { 608 | self.active_account_index = Some(self.active_account_index.map_or(0, |i| { 609 | if i == 0 { 610 | self.accounts.len() - 1 611 | } else { 612 | i - 1 613 | } 614 | })); 615 | self.set_status("Selected previous account"); 616 | } 617 | } 618 | 619 | fn select_next_account(&mut self) { 620 | if !self.accounts.is_empty() { 621 | self.active_account_index = Some(self.active_account_index.map_or(0, |i| { 622 | if i == self.accounts.len() - 1 { 623 | 0 624 | } else { 625 | i + 1 626 | } 627 | })); 628 | self.set_status("Selected next account"); 629 | } 630 | } 631 | 632 | fn submit_new_account(&mut self) { 633 | if !self.new_account_name.is_empty() && !self.new_account_email.is_empty() { 634 | if self.is_valid_email(&self.new_account_email) { 635 | match add_account(&self.new_account_name, &self.new_account_email) { 636 | Ok(public_key) => { 637 | self.set_status("New account added successfully!"); 638 | self.accounts = list_accounts().unwrap_or_default(); 639 | self.new_account_name.clear(); 640 | self.new_account_email.clear(); 641 | self.popup = PopupType::None; 642 | self.input_mode = InputMode::Normal; 643 | } 644 | Err(e) => self.set_status(&format!("Failed to add account: {}", e)), 645 | } 646 | } else { 647 | self.set_status("Invalid email format. Please enter a valid email."); 648 | } 649 | } else { 650 | self.set_status("Invalid input. Name and email are required."); 651 | } 652 | } 653 | 654 | fn is_valid_email(&self, email: &str) -> bool { 655 | let email_regex = Regex::new(r"^[a-zA-Z0-9._%+-]+@[a-zA-Z0-9.-]+\.[a-zA-Z]{2,}$").unwrap(); 656 | email_regex.is_match(email) 657 | } 658 | 659 | fn remove_account(&mut self) { 660 | if let Some(index) = self.active_account_index { 661 | if index < self.accounts.len() { 662 | let account = &self.accounts[index]; 663 | if let Err(e) = remove_account(&account.email) { 664 | self.set_status(&format!("Failed to remove account: {}", e)); 665 | } else { 666 | self.set_status(&format!("Removed account: {}", account.name)); 667 | self.accounts = list_accounts().unwrap_or_default(); 668 | if self.accounts.is_empty() { 669 | self.active_account_index = None; 670 | } else { 671 | self.active_account_index = Some(index.min(self.accounts.len() - 1)); 672 | } 673 | } 674 | } 675 | } 676 | } 677 | 678 | fn switch_to_tab(&mut self, tab: Tab) { 679 | self.current_tab = tab; 680 | self.set_status(&format!("Switched to {} tab", self.current_tab_name())); 681 | } 682 | 683 | fn set_active_account(&mut self, index: usize) { 684 | if index < self.accounts.len() { 685 | let email = self.accounts[index].email.clone(); 686 | if let Err(e) = switch_account(&email) { 687 | self.set_status(&format!("Failed to switch account: {}", e)); 688 | } else { 689 | // Update the active status for all accounts 690 | for acc in &mut self.accounts { 691 | acc.is_active = acc.email == email; 692 | } 693 | self.active_account = Some(index); 694 | self.active_account_index = Some(index); 695 | self.set_status(&format!("Activated account: {}", self.accounts[index].name)); 696 | } 697 | } 698 | } 699 | 700 | fn copy_ssh_key_to_clipboard(&mut self, index: usize) { 701 | if let Some(account) = self.accounts.get(index) { 702 | match get_ssh_key(&account.email) { 703 | Ok(ssh_key) => { 704 | if let Err(e) = self.clipboard.set_text(&ssh_key) { 705 | self.set_status(&format!("Failed to copy SSH key: {}", e)); 706 | } else { 707 | self.set_status("SSH key copied to clipboard"); 708 | } 709 | } 710 | Err(e) => self.set_status(&format!("Failed to get SSH key: {}", e)), 711 | } 712 | } 713 | } 714 | } 715 | fn centered_rect(percent_x: u16, percent_y: u16, r: Rect) -> Rect { 716 | let popup_layout = Layout::default() 717 | .direction(Direction::Vertical) 718 | .constraints([ 719 | Constraint::Percentage((100 - percent_y) / 2), 720 | Constraint::Percentage(percent_y), 721 | Constraint::Percentage((100 - percent_y) / 2), 722 | ]) 723 | .split(r); 724 | 725 | Layout::default() 726 | .direction(Direction::Horizontal) 727 | .constraints([ 728 | Constraint::Percentage((100 - percent_x) / 2), 729 | Constraint::Percentage(percent_x), 730 | Constraint::Percentage((100 - percent_x) / 2), 731 | ]) 732 | .split(popup_layout[1])[1] 733 | } 734 | -------------------------------------------------------------------------------- /src/db.rs: -------------------------------------------------------------------------------- 1 | use dirs; 2 | use rusqlite::{params, Connection, Result as SqliteResult}; 3 | use std::fs; 4 | use std::path::PathBuf; 5 | use std::process::Command; 6 | 7 | #[derive(Debug, Clone)] 8 | pub struct Account { 9 | pub email: String, 10 | pub name: String, 11 | pub is_active: bool, 12 | } 13 | 14 | pub struct SSHKey { 15 | pub email: String, 16 | pub private_key: Vec, 17 | pub public_key: Vec, 18 | } 19 | 20 | pub fn get_db_path() -> PathBuf { 21 | let home_dir = dirs::home_dir().unwrap(); 22 | home_dir.join(".git_ledger_tui.db") 23 | } 24 | 25 | pub fn get_ssh_dir() -> PathBuf { 26 | let home_dir = dirs::home_dir().unwrap(); 27 | home_dir.join(".ssh") 28 | } 29 | 30 | pub fn init_db() -> SqliteResult<()> { 31 | let conn = Connection::open(get_db_path())?; 32 | conn.execute( 33 | "CREATE TABLE IF NOT EXISTS accounts ( 34 | email TEXT PRIMARY KEY, 35 | name TEXT, 36 | is_active INTEGER 37 | )", 38 | [], 39 | )?; 40 | conn.execute( 41 | "CREATE TABLE IF NOT EXISTS ssh_keys ( 42 | email TEXT PRIMARY KEY, 43 | private_key BLOB, 44 | public_key BLOB, 45 | FOREIGN KEY(email) REFERENCES accounts(email) ON DELETE CASCADE 46 | )", 47 | [], 48 | )?; 49 | 50 | Ok(()) 51 | } 52 | 53 | pub fn list_accounts() -> SqliteResult> { 54 | let conn = Connection::open(get_db_path())?; 55 | let mut stmt = conn.prepare("SELECT name, email, is_active FROM accounts")?; 56 | let accounts = stmt 57 | .query_map([], |row| { 58 | Ok(Account { 59 | name: row.get(0)?, 60 | email: row.get(1)?, 61 | is_active: row.get::<_, i32>(2)? == 1, 62 | }) 63 | })? 64 | .collect::, _>>()?; 65 | Ok(accounts) 66 | } 67 | 68 | pub fn add_account(name: &str, email: &str) -> Result { 69 | let conn = Connection::open(get_db_path()).map_err(|e| e.to_string())?; 70 | 71 | conn.execute("UPDATE accounts SET is_active = 0", []) 72 | .map_err(|e| e.to_string())?; 73 | conn.execute( 74 | "INSERT OR REPLACE INTO accounts (email, name, is_active) VALUES (?, ?, 1)", 75 | params![email, name], 76 | ) 77 | .map_err(|e| e.to_string())?; 78 | 79 | let (private_key, public_key) = generate_ssh_key(email)?; 80 | 81 | conn.execute( 82 | "INSERT OR REPLACE INTO ssh_keys (email, private_key, public_key) VALUES (?, ?, ?)", 83 | params![email, &private_key, &public_key], 84 | ) 85 | .map_err(|e| e.to_string())?; 86 | 87 | run_git_command(&["config", "--global", "user.name", name])?; 88 | run_git_command(&["config", "--global", "user.email", email])?; 89 | 90 | Ok(String::from_utf8(public_key).map_err(|e| e.to_string())?) 91 | } 92 | 93 | pub fn remove_account(email: &str) -> Result<(), String> { 94 | let conn = Connection::open(get_db_path()).map_err(|e| e.to_string())?; 95 | 96 | let is_active: bool = conn 97 | .query_row( 98 | "SELECT is_active FROM accounts WHERE email = ?", 99 | params![email], 100 | |row| row.get(0), 101 | ) 102 | .unwrap_or(false); 103 | 104 | conn.execute("DELETE FROM accounts WHERE email = ?", params![email]) 105 | .map_err(|e| e.to_string())?; 106 | conn.execute("DELETE FROM ssh_keys WHERE email = ?", params![email]) 107 | .map_err(|e| e.to_string())?; 108 | 109 | if is_active { 110 | run_git_command(&["config", "--global", "--unset", "user.name"])?; 111 | run_git_command(&["config", "--global", "--unset", "user.email"])?; 112 | 113 | let ssh_dir = get_ssh_dir(); 114 | fs::remove_file(ssh_dir.join("id_ed25519")).ok(); 115 | fs::remove_file(ssh_dir.join("id_ed25519.pub")).ok(); 116 | } 117 | 118 | Ok(()) 119 | } 120 | 121 | pub fn switch_account(email: &str) -> Result<(), String> { 122 | let conn = Connection::open(get_db_path()).map_err(|e| e.to_string())?; 123 | 124 | conn.execute("UPDATE accounts SET is_active = 0", []) 125 | .map_err(|e| e.to_string())?; 126 | conn.execute( 127 | "UPDATE accounts SET is_active = 1 WHERE email = ?", 128 | params![email], 129 | ) 130 | .map_err(|e| e.to_string())?; 131 | 132 | let name: String = conn 133 | .query_row( 134 | "SELECT name FROM accounts WHERE email = ?", 135 | params![email], 136 | |row| row.get(0), 137 | ) 138 | .map_err(|e| e.to_string())?; 139 | 140 | set_active_ssh_key(email)?; 141 | 142 | run_git_command(&["config", "--global", "user.name", &name])?; 143 | run_git_command(&["config", "--global", "user.email", email])?; 144 | 145 | Ok(()) 146 | } 147 | 148 | fn generate_ssh_key(email: &str) -> Result<(Vec, Vec), String> { 149 | let ssh_dir = get_ssh_dir(); 150 | fs::create_dir_all(&ssh_dir).map_err(|e| e.to_string())?; 151 | 152 | let private_key_path = ssh_dir.join("id_ed25519"); 153 | let public_key_path = ssh_dir.join("id_ed25519.pub"); 154 | 155 | fs::remove_file(&private_key_path).ok(); 156 | fs::remove_file(&public_key_path).ok(); 157 | 158 | Command::new("ssh-keygen") 159 | .args(&[ 160 | "-t", 161 | "ed25519", 162 | "-f", 163 | private_key_path.to_str().unwrap(), 164 | "-N", 165 | "", 166 | "-C", 167 | email, 168 | ]) 169 | .output() 170 | .map_err(|e| e.to_string())?; 171 | 172 | let private_key = fs::read(&private_key_path).map_err(|e| e.to_string())?; 173 | let public_key = fs::read(&public_key_path).map_err(|e| e.to_string())?; 174 | 175 | Ok((private_key, public_key)) 176 | } 177 | 178 | fn set_active_ssh_key(email: &str) -> Result<(), String> { 179 | let conn = Connection::open(get_db_path()).map_err(|e| e.to_string())?; 180 | let (private_key, public_key): (Vec, Vec) = conn 181 | .query_row( 182 | "SELECT private_key, public_key FROM ssh_keys WHERE email = ?", 183 | params![email], 184 | |row| Ok((row.get(0)?, row.get(1)?)), 185 | ) 186 | .map_err(|e| e.to_string())?; 187 | 188 | let ssh_dir = get_ssh_dir(); 189 | fs::write(ssh_dir.join("id_ed25519"), private_key).map_err(|e| e.to_string())?; 190 | fs::write(ssh_dir.join("id_ed25519.pub"), public_key).map_err(|e| e.to_string())?; 191 | 192 | Ok(()) 193 | } 194 | 195 | pub fn get_ssh_key(email: &str) -> Result { 196 | let conn = Connection::open(get_db_path()).map_err(|e| e.to_string())?; 197 | let public_key: Vec = conn 198 | .query_row( 199 | "SELECT public_key FROM ssh_keys WHERE email = ?", 200 | params![email], 201 | |row| row.get(0), 202 | ) 203 | .map_err(|e| e.to_string())?; 204 | 205 | String::from_utf8(public_key).map_err(|e| e.to_string()) 206 | } 207 | 208 | fn run_git_command(args: &[&str]) -> Result { 209 | let output = Command::new("git") 210 | .args(args) 211 | .output() 212 | .map_err(|e| e.to_string())?; 213 | 214 | if output.status.success() { 215 | Ok(String::from_utf8_lossy(&output.stdout).trim().to_string()) 216 | } else { 217 | Err(String::from_utf8_lossy(&output.stderr).trim().to_string()) 218 | } 219 | } 220 | 221 | pub fn get_current_user() -> Result, String> { 222 | let name = run_git_command(&["config", "--global", "user.name"])?; 223 | let email = run_git_command(&["config", "--global", "user.email"])?; 224 | 225 | if name.is_empty() || email.is_empty() { 226 | return Ok(None); 227 | } 228 | 229 | let conn = Connection::open(get_db_path()).map_err(|e| e.to_string())?; 230 | let is_active: bool = conn 231 | .query_row( 232 | "SELECT is_active FROM accounts WHERE email = ?", 233 | params![&email], 234 | |row| row.get(0), 235 | ) 236 | .unwrap_or(false); 237 | 238 | Ok(Some(Account { 239 | name, 240 | email, 241 | is_active, 242 | })) 243 | } 244 | -------------------------------------------------------------------------------- /src/main.rs: -------------------------------------------------------------------------------- 1 | use color_eyre::Result; 2 | use crossterm::{ 3 | execute, 4 | terminal::{disable_raw_mode, enable_raw_mode, EnterAlternateScreen, LeaveAlternateScreen}, 5 | }; 6 | use ratatui::backend::CrosstermBackend; 7 | use ratatui::Terminal; 8 | use std::io; 9 | 10 | mod app; 11 | mod db; 12 | 13 | fn main() -> Result<()> { 14 | color_eyre::install()?; 15 | 16 | // Setup terminal 17 | enable_raw_mode()?; 18 | let mut stdout = io::stdout(); 19 | execute!(stdout, EnterAlternateScreen)?; 20 | let backend = CrosstermBackend::new(stdout); 21 | let mut terminal = Terminal::new(backend)?; 22 | 23 | // Create app and run it 24 | let mut app = app::App::new()?; 25 | let res = app.run(&mut terminal); 26 | 27 | // Restore terminal 28 | disable_raw_mode()?; 29 | execute!(terminal.backend_mut(), LeaveAlternateScreen)?; 30 | terminal.show_cursor()?; 31 | 32 | // If there was an error, print it to stderr 33 | if let Err(err) = res { 34 | eprintln!("Error: {:?}", err); 35 | } 36 | 37 | Ok(()) 38 | } 39 | -------------------------------------------------------------------------------- /wix/main.wxs: -------------------------------------------------------------------------------- 1 | 2 | 17 | 18 | 46 | 47 | 53 | 54 | 55 | 56 | 57 | 58 | 59 | 60 | 61 | 69 | 70 | 80 | 81 | 84 | 85 | 86 | 87 | 88 | 89 | 90 | 91 | 92 | 106 | 111 | 112 | 113 | 114 | 122 | 123 | 124 | 130 | 131 | 132 | 133 | 134 | 135 | 136 | 145 | 146 | 150 | 151 | 152 | 153 | 154 | 160 | 161 | 162 | 163 | 164 | 165 | 166 | 167 | 173 | 174 | 175 | 176 | 177 | 178 | 179 | 180 | 181 | 191 | 1 192 | 1 193 | 194 | 195 | 196 | 197 | 202 | 203 | 204 | 205 | 213 | 214 | 215 | 216 | 224 | 225 | 226 | 227 | 228 | 229 | --------------------------------------------------------------------------------