├── .commitlintrc.json ├── .czrc ├── .envrc ├── .gitattributes ├── .github ├── renovate.json └── workflows │ ├── audit-check.yml │ ├── auto-rebase.yml │ ├── ci.yml │ ├── commitlint.yml │ ├── docker.yml │ └── update-deps.yml ├── .gitignore ├── .prettierignore ├── .prettierrc.toml ├── .releaserc.json ├── .rgignore ├── .taplo.toml ├── CHANGELOG.md ├── Cargo.lock ├── Cargo.toml ├── LICENSE ├── README.md ├── ci └── release │ └── replace_version.sh ├── default.nix ├── demo.gif ├── flake.lock ├── flake.nix ├── rustfmt.toml └── src ├── error.rs ├── events.rs ├── main.rs ├── sweep.rs └── ui.rs /.commitlintrc.json: -------------------------------------------------------------------------------- 1 | { 2 | "rules": { 3 | "body-leading-blank": [1, "always"], 4 | "body-max-line-length": [2, "always", 100], 5 | "footer-leading-blank": [1, "always"], 6 | "footer-max-line-length": [2, "always", 100], 7 | "header-max-length": [2, "always", 100], 8 | "subject-case": [ 9 | 2, 10 | "never", 11 | ["sentence-case", "start-case", "pascal-case", "upper-case"] 12 | ], 13 | "subject-empty": [2, "never"], 14 | "subject-full-stop": [2, "never", "."], 15 | "type-case": [2, "always", "lower-case"], 16 | "type-empty": [2, "never"], 17 | "type-enum": [ 18 | 2, 19 | "always", 20 | [ 21 | "build", 22 | "chore", 23 | "ci", 24 | "docs", 25 | "feat", 26 | "fix", 27 | "perf", 28 | "refactor", 29 | "revert", 30 | "style", 31 | "test" 32 | ] 33 | ] 34 | }, 35 | "prompt": { 36 | "questions": { 37 | "type": { 38 | "description": "Select the type of change that you're committing", 39 | "enum": { 40 | "feat": { 41 | "description": "A new feature", 42 | "title": "Features", 43 | "emoji": "✨" 44 | }, 45 | "fix": { 46 | "description": "A bug fix", 47 | "title": "Bug Fixes", 48 | "emoji": "🐛" 49 | }, 50 | "docs": { 51 | "description": "Documentation only changes", 52 | "title": "Documentation", 53 | "emoji": "📚" 54 | }, 55 | "style": { 56 | "description": "Changes that do not affect the meaning of the code (whitespace, formatting, missing semi-colons, etc)", 57 | "title": "Styles", 58 | "emoji": "💎" 59 | }, 60 | "refactor": { 61 | "description": "A code change that neither fixes a bug nor adds a feature", 62 | "title": "Code Refactoring", 63 | "emoji": "📦" 64 | }, 65 | "perf": { 66 | "description": "A code change that improves performance", 67 | "title": "Performance Improvements", 68 | "emoji": "🚀" 69 | }, 70 | "test": { 71 | "description": "Adding missing tests or fixing existing tests", 72 | "title": "Tests", 73 | "emoji": "🚨" 74 | }, 75 | "build": { 76 | "description": "Changes that affect the build system or external dependencies (example scopes: poetry, nix)", 77 | "title": "Builds", 78 | "emoji": "🛠" 79 | }, 80 | "ci": { 81 | "description": "Changes to our CI configuration files and scripts (example scopes: actions, gh-actions, github-actions)", 82 | "title": "Continuous Integrations", 83 | "emoji": "⚙️" 84 | }, 85 | "chore": { 86 | "description": "Other changes that don't modify source or test files", 87 | "title": "Chores", 88 | "emoji": "♻️" 89 | }, 90 | "revert": { 91 | "description": "Reverts a previous commit", 92 | "title": "Reverts", 93 | "emoji": "🗑" 94 | } 95 | } 96 | }, 97 | "scope": { 98 | "description": "What is the scope of this change (e.g. component or file name)" 99 | }, 100 | "subject": { 101 | "description": "Write a short, imperative tense description of the change" 102 | }, 103 | "body": { 104 | "description": "Provide a longer description of the change" 105 | }, 106 | "isBreaking": { 107 | "description": "Are there any breaking changes?" 108 | }, 109 | "breakingBody": { 110 | "description": "A BREAKING CHANGE commit requires a body. Please enter a longer description of the commit itself" 111 | }, 112 | "breaking": { 113 | "description": "Describe the breaking changes" 114 | }, 115 | "isIssueAffected": { 116 | "description": "Does this change affect any open issues?" 117 | }, 118 | "issuesBody": { 119 | "description": "If issues are closed, the commit requires a body. Please enter a longer description of the commit itself" 120 | }, 121 | "issues": { 122 | "description": "Add issue references (e.g. \"fix #123\", \"re #123\".)" 123 | } 124 | } 125 | } 126 | } 127 | -------------------------------------------------------------------------------- /.czrc: -------------------------------------------------------------------------------- 1 | { 2 | "path": "cz-conventional-changelog" 3 | } 4 | -------------------------------------------------------------------------------- /.envrc: -------------------------------------------------------------------------------- 1 | use flake 2 | -------------------------------------------------------------------------------- /.gitattributes: -------------------------------------------------------------------------------- 1 | flake.lock linguist-generated=true 2 | Cargo.lock linguist-generated=true 3 | -------------------------------------------------------------------------------- /.github/renovate.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": ["config:base"], 3 | "lockFileMaintenance": { "enabled": true }, 4 | "automerge": true, 5 | "labels": ["dependencies"], 6 | "enabledManagers": ["github-actions", "cargo"], 7 | "dependencyDashboard": true, 8 | "semanticCommits": "enabled", 9 | "semanticCommitType": "chore", 10 | "semanticCommitScope": "deps" 11 | } 12 | -------------------------------------------------------------------------------- /.github/workflows/audit-check.yml: -------------------------------------------------------------------------------- 1 | on: 2 | push: 3 | branches: 4 | - main 5 | paths: 6 | - "**/Cargo.toml" 7 | - "**/Cargo.lock" 8 | - "**/*.nix" 9 | - "flake.lock" 10 | 11 | name: Cargo Audit 12 | 13 | concurrency: 14 | group: ${{ github.repository }}-${{ github.head_ref || github.sha }}-${{ github.workflow }} 15 | cancel-in-progress: true 16 | 17 | jobs: 18 | security-audit: 19 | runs-on: ${{ matrix.os }} 20 | strategy: 21 | matrix: 22 | os: 23 | - ubuntu-latest 24 | - macos-10.15 25 | - macos-11 26 | - windows-latest 27 | steps: 28 | - uses: actions/checkout@v4 29 | 30 | - uses: actions-rs/audit-check@v1 31 | with: 32 | token: ${{ secrets.GITHUB_TOKEN }} 33 | -------------------------------------------------------------------------------- /.github/workflows/auto-rebase.yml: -------------------------------------------------------------------------------- 1 | name: Automatic Rebase 2 | on: 3 | push: 4 | branches-ignore: 5 | # Ignore branches automatically created by github-rebase 6 | - rebase-pull-request** 7 | - cherry-pick-rebase-pull-request** 8 | pull_request: 9 | types: 10 | - labeled 11 | 12 | jobs: 13 | auto-rebase: 14 | name: AutoRebase 15 | runs-on: ubuntu-latest 16 | steps: 17 | - uses: actions/create-github-app-token@v1.9.0 18 | id: generate_token 19 | with: 20 | app-id: ${{ secrets.APP_ID }} 21 | private-key: ${{ secrets.APP_PRIVATE_KEY }} 22 | 23 | - uses: Label305/AutoRebase@v0.1 24 | with: 25 | github_token: ${{ steps.generate_token.outputs.token }} 26 | -------------------------------------------------------------------------------- /.github/workflows/ci.yml: -------------------------------------------------------------------------------- 1 | on: 2 | push: 3 | branches: 4 | - main 5 | pull_request: 6 | branches: 7 | - main 8 | 9 | name: CI 10 | 11 | concurrency: 12 | group: ${{ github.repository }}-${{ github.head_ref || github.sha }}-${{ github.workflow }} 13 | cancel-in-progress: true 14 | 15 | jobs: 16 | nix: 17 | runs-on: ubuntu-latest 18 | steps: 19 | - uses: actions/checkout@v4 20 | 21 | - uses: cachix/install-nix-action@6004951b182f8860210c8d6f0d808ec5b1a33d28 # v25 22 | with: 23 | nix_path: nixpkgs=channel:nixos-unstable-small 24 | extra_nix_config: | 25 | access-tokens = github.com=${{ secrets.GITHUB_TOKEN }} 26 | 27 | - uses: cachix/cachix-action@v14 28 | with: 29 | name: minesweep 30 | authToken: ${{ secrets.CACHIX_AUTH_TOKEN }} 31 | extraPullNames: nix-community,naersk 32 | 33 | - name: build image 34 | run: nix build --keep-going --print-build-logs '.#minesweep-image' --no-link 35 | 36 | - name: load image 37 | run: | 38 | set -euo pipefail 39 | 40 | docker load -i "$(nix path-info --print-build-logs '.#minesweep-image')" 41 | 42 | - name: show help 43 | run: | 44 | set -euo pipefail 45 | 46 | docker run --rm "minesweep:$(nix eval --raw '.#minesweep-image.imageTag')" --help 47 | 48 | - run: docker images minesweep 49 | release: 50 | needs: 51 | - nix 52 | runs-on: ubuntu-latest 53 | steps: 54 | - uses: actions/create-github-app-token@v1.9.0 55 | id: generate-token 56 | with: 57 | app-id: ${{ secrets.APP_ID }} 58 | private-key: ${{ secrets.APP_PRIVATE_KEY }} 59 | 60 | - uses: actions/checkout@v4 61 | with: 62 | fetch-depth: 0 63 | token: ${{ steps.generate-token.outputs.token }} 64 | 65 | - uses: cachix/install-nix-action@6004951b182f8860210c8d6f0d808ec5b1a33d28 # v25 66 | with: 67 | nix_path: nixpkgs=channel:nixos-unstable-small 68 | extra_nix_config: | 69 | access-tokens = github.com=${{ secrets.GITHUB_TOKEN }} 70 | 71 | - uses: cachix/cachix-action@v14 72 | with: 73 | name: minesweep 74 | authToken: ${{ secrets.CACHIX_AUTH_TOKEN }} 75 | extraPullNames: nix-community,naersk 76 | 77 | - uses: actions/setup-node@v4 78 | with: 79 | node-version: 14 80 | 81 | - uses: cycjimmy/semantic-release-action@v4.0.0 82 | with: 83 | extra_plugins: | 84 | @semantic-release/changelog@6.0.3 85 | @semantic-release/commit-analyzer@9.0.2 86 | @semantic-release/exec@6.0.3 87 | @semantic-release/git@10.0.1 88 | @semantic-release/github@8.0.7 89 | @semantic-release/release-notes-generator@11.0.1 90 | env: 91 | GITHUB_TOKEN: ${{ steps.generate-token.outputs.token }} 92 | CARGO_REGISTRY_TOKEN: ${{ secrets.MINESWEEP_CARGO_REGISTRY_TOKEN }} 93 | -------------------------------------------------------------------------------- /.github/workflows/commitlint.yml: -------------------------------------------------------------------------------- 1 | on: 2 | pull_request: 3 | branches: 4 | - main 5 | 6 | name: commitlint 7 | 8 | concurrency: 9 | group: ${{ github.repository }}-${{ github.head_ref || github.sha }}-${{ github.workflow }} 10 | cancel-in-progress: true 11 | 12 | jobs: 13 | commitlint: 14 | runs-on: ubuntu-latest 15 | steps: 16 | - uses: actions/checkout@v4 17 | with: 18 | fetch-depth: 0 19 | 20 | - uses: cachix/install-nix-action@6004951b182f8860210c8d6f0d808ec5b1a33d28 # v25 21 | with: 22 | nix_path: nixpkgs=channel:nixos-unstable-small 23 | 24 | - name: commitlint 25 | run: | 26 | nix shell -L -f '' commitlint -c commitlint --from=${{ github.event.pull_request.base.sha }} --to=${{ github.sha }} --verbose 27 | -------------------------------------------------------------------------------- /.github/workflows/docker.yml: -------------------------------------------------------------------------------- 1 | on: 2 | release: 3 | types: 4 | - published 5 | workflow_dispatch: 6 | 7 | name: Publish Docker Image 8 | 9 | jobs: 10 | publish-image: 11 | concurrency: publish-image 12 | runs-on: ubuntu-latest 13 | steps: 14 | - uses: actions/checkout@v4 15 | - uses: cachix/install-nix-action@6004951b182f8860210c8d6f0d808ec5b1a33d28 # v25 16 | with: 17 | nix_path: nixpkgs=channel:nixos-unstable-small 18 | extra_nix_config: | 19 | experimental-features = nix-command flakes 20 | - uses: cachix/cachix-action@v14 21 | with: 22 | name: minesweep 23 | extraPullNames: nix-community,naersk 24 | - run: nix build -L '.#minesweep-image' --no-link 25 | - name: load image 26 | run: | 27 | set -euo pipefail 28 | 29 | docker load -i "$(nix path-info -L '.#minesweep-image')" 30 | - name: create Dockerfile 31 | run: | 32 | set -euo pipefail 33 | 34 | echo "FROM minesweep:$(nix eval --raw '.#minesweep-image.imageTag')" > Dockerfile 35 | - uses: docker/metadata-action@v5 36 | id: meta 37 | with: 38 | images: | 39 | ghcr.io/${{ github.repository_owner }}/minesweep-rs 40 | tags: | 41 | type=semver,pattern=v{{version}} 42 | type=semver,pattern=v{{major}}.{{minor}} 43 | type=semver,pattern=v{{major}} 44 | type=sha 45 | type=sha,format=long 46 | - uses: docker/login-action@v3 47 | with: 48 | registry: ghcr.io 49 | username: ${{ github.actor }} 50 | password: ${{ secrets.GITHUB_TOKEN }} 51 | - uses: docker/build-push-action@v5 52 | with: 53 | context: . 54 | push: true 55 | tags: ${{ steps.meta.outputs.tags }} 56 | labels: ${{ steps.meta.outputs.labels }} 57 | -------------------------------------------------------------------------------- /.github/workflows/update-deps.yml: -------------------------------------------------------------------------------- 1 | name: Update Flakes 2 | on: 3 | schedule: 4 | - cron: "40 0 * * 0" 5 | workflow_dispatch: 6 | jobs: 7 | get-flakes: 8 | runs-on: ubuntu-latest 9 | outputs: 10 | matrix: ${{ steps.get-flakes.outputs.matrix }} 11 | steps: 12 | - uses: actions/checkout@v4 13 | - uses: cachix/install-nix-action@6004951b182f8860210c8d6f0d808ec5b1a33d28 # v25 14 | with: 15 | extra_nix_config: | 16 | access-tokens = github.com=${{ secrets.GITHUB_TOKEN }} 17 | - name: generate flake matrix 18 | id: get-flakes 19 | run: | 20 | set -euo pipefail 21 | 22 | flakes="$(nix flake metadata --json | jq -rcM '.locks.nodes.root.inputs | {flake: keys}')" 23 | echo "::set-output name=matrix::$flakes" 24 | flake-update: 25 | runs-on: ubuntu-latest 26 | needs: 27 | - get-flakes 28 | strategy: 29 | fail-fast: false 30 | matrix: ${{ fromJson(needs.get-flakes.outputs.matrix) }} 31 | steps: 32 | - uses: actions/checkout@v4 33 | 34 | - uses: cachix/install-nix-action@6004951b182f8860210c8d6f0d808ec5b1a33d28 # v25 35 | with: 36 | nix_path: nixpkgs=channel:nixos-unstable-small 37 | extra_nix_config: | 38 | access-tokens = github.com=${{ secrets.GITHUB_TOKEN }} 39 | 40 | - uses: actions/create-github-app-token@v1.9.0 41 | id: generate-token 42 | with: 43 | app-id: ${{ secrets.APP_ID }} 44 | private-key: ${{ secrets.APP_PRIVATE_KEY }} 45 | 46 | - uses: cpcloud/flake-update-action@v2.0.1 47 | with: 48 | dependency: ${{ matrix.flake }} 49 | github-token: ${{ secrets.GITHUB_TOKEN }} 50 | pull-request-token: ${{ steps.generate-token.outputs.token }} 51 | pull-request-author: "phillip-ground[bot] " 52 | pull-request-labels: dependencies,autorebase:opt-in 53 | automerge: true 54 | delete-branch: true 55 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | /target 2 | .direnv 3 | result 4 | .pre-commit-config.yaml 5 | -------------------------------------------------------------------------------- /.prettierignore: -------------------------------------------------------------------------------- 1 | .direnv 2 | .pre-commit-config.yaml 3 | Cargo.lock 4 | target 5 | result 6 | result-* 7 | CHANGELOG.md 8 | -------------------------------------------------------------------------------- /.prettierrc.toml: -------------------------------------------------------------------------------- 1 | tabWidth = 2 2 | semi = true 3 | singleQuote = false 4 | arrowParens = "avoid" 5 | useTabs = false 6 | -------------------------------------------------------------------------------- /.releaserc.json: -------------------------------------------------------------------------------- 1 | { 2 | "branches": ["main"], 3 | "plugins": [ 4 | "@semantic-release/commit-analyzer", 5 | "@semantic-release/release-notes-generator", 6 | ["@semantic-release/changelog", { "changelogTitle": "Release Notes\n---" }], 7 | [ 8 | "@semantic-release/exec", 9 | { 10 | "prepareCmd": "./ci/release/replace_version.sh ${lastRelease.version} ${nextRelease.version} && nix-shell --pure --run 'cargo generate-lockfile' && rm -f .git/hooks/pre-commit", 11 | "publishCmd": "nix-shell --run 'cargo publish'" 12 | } 13 | ], 14 | ["@semantic-release/github", { "successComment": false }], 15 | [ 16 | "@semantic-release/git", 17 | { "assets": ["Cargo.toml", "Cargo.lock", "CHANGELOG.md"] } 18 | ] 19 | ] 20 | } 21 | -------------------------------------------------------------------------------- /.rgignore: -------------------------------------------------------------------------------- 1 | !.github 2 | !.releaserc.yml 3 | -------------------------------------------------------------------------------- /.taplo.toml: -------------------------------------------------------------------------------- 1 | 2 | -------------------------------------------------------------------------------- /CHANGELOG.md: -------------------------------------------------------------------------------- 1 | Release Notes 2 | --- 3 | 4 | ## [6.0.54](https://github.com/cpcloud/minesweep-rs/compare/v6.0.53...v6.0.54) (2024-02-11) 5 | 6 | 7 | ### Bug Fixes 8 | 9 | * **deps:** update rust crate thiserror to ^1.0.57 ([d009c25](https://github.com/cpcloud/minesweep-rs/commit/d009c25f0b45a6f66a9f76d2b2db02485ce4aadb)) 10 | 11 | ## [6.0.53](https://github.com/cpcloud/minesweep-rs/compare/v6.0.52...v6.0.53) (2024-02-08) 12 | 13 | 14 | ### Bug Fixes 15 | 16 | * **deps:** update rust crate num-traits to ^0.2.18 ([4a80e04](https://github.com/cpcloud/minesweep-rs/commit/4a80e046ab2c4fe59070c523441de83fdc1f3955)) 17 | 18 | ## [6.0.52](https://github.com/cpcloud/minesweep-rs/compare/v6.0.51...v6.0.52) (2024-02-02) 19 | 20 | 21 | ### Bug Fixes 22 | 23 | * **deps:** update rust crate termion to v3 ([ccf2e30](https://github.com/cpcloud/minesweep-rs/commit/ccf2e3058cae0c770a93c0fa7fc9c1e0c50f074f)) 24 | 25 | ## [6.0.51](https://github.com/cpcloud/minesweep-rs/compare/v6.0.50...v6.0.51) (2024-02-02) 26 | 27 | 28 | ### Bug Fixes 29 | 30 | * **deps:** update rust crate ratatui to ^0.26.0 ([2ef2956](https://github.com/cpcloud/minesweep-rs/commit/2ef2956dcd700c6ae46f2ae114547e5b0b366423)) 31 | 32 | ## [6.0.50](https://github.com/cpcloud/minesweep-rs/compare/v6.0.49...v6.0.50) (2024-01-16) 33 | 34 | 35 | ### Bug Fixes 36 | 37 | * **deps:** update rust crate typed-builder to ^0.18.1 ([2343c30](https://github.com/cpcloud/minesweep-rs/commit/2343c30105c6d8a9039d5910f2cc89947ed9af8f)) 38 | 39 | ## [6.0.49](https://github.com/cpcloud/minesweep-rs/compare/v6.0.48...v6.0.49) (2024-01-02) 40 | 41 | 42 | ### Bug Fixes 43 | 44 | * **deps:** update rust crate thiserror to ^1.0.56 ([59ed631](https://github.com/cpcloud/minesweep-rs/commit/59ed6311a0706a737c6ea8e9513e19cf51c9558e)) 45 | 46 | ## [6.0.48](https://github.com/cpcloud/minesweep-rs/compare/v6.0.47...v6.0.48) (2024-01-02) 47 | 48 | 49 | ### Bug Fixes 50 | 51 | * **deps:** update rust crate anyhow to ^1.0.79 ([1ff8ee5](https://github.com/cpcloud/minesweep-rs/commit/1ff8ee563a852b623431a1fbdd97612f95bc7ee6)) 52 | 53 | ## [6.0.47](https://github.com/cpcloud/minesweep-rs/compare/v6.0.46...v6.0.47) (2023-12-31) 54 | 55 | 56 | ### Bug Fixes 57 | 58 | * **deps:** update rust crate anyhow to ^1.0.78 ([a6d84e2](https://github.com/cpcloud/minesweep-rs/commit/a6d84e2974eaa6df714de01f2f41b783ffacebf7)) 59 | 60 | ## [6.0.46](https://github.com/cpcloud/minesweep-rs/compare/v6.0.45...v6.0.46) (2023-12-30) 61 | 62 | 63 | ### Bug Fixes 64 | 65 | * **deps:** update rust crate anyhow to ^1.0.77 ([f22ecbc](https://github.com/cpcloud/minesweep-rs/commit/f22ecbc5d57b1fb476d0b8ad910c18c83433edf6)) 66 | * **deps:** update rust crate thiserror to ^1.0.53 ([4ddb1bd](https://github.com/cpcloud/minesweep-rs/commit/4ddb1bde47a5ceca5951f70bacaf30324a69708b)) 67 | 68 | ## [6.0.45](https://github.com/cpcloud/minesweep-rs/compare/v6.0.44...v6.0.45) (2023-12-25) 69 | 70 | 71 | ### Bug Fixes 72 | 73 | * **deps:** update rust crate thiserror to ^1.0.52 ([b059d21](https://github.com/cpcloud/minesweep-rs/commit/b059d219a8c0baf5ed2b16c8df820a3ff117bbea)) 74 | 75 | ## [6.0.44](https://github.com/cpcloud/minesweep-rs/compare/v6.0.43...v6.0.44) (2023-12-22) 76 | 77 | 78 | ### Bug Fixes 79 | 80 | * **deps:** update rust crate ctrlc to ^3.4.2 ([194a789](https://github.com/cpcloud/minesweep-rs/commit/194a789a9d76ca17072ff1093b3196ae4171a386)) 81 | 82 | ## [6.0.43](https://github.com/cpcloud/minesweep-rs/compare/v6.0.42...v6.0.43) (2023-12-21) 83 | 84 | 85 | ### Bug Fixes 86 | 87 | * **deps:** update rust crate anyhow to ^1.0.76 ([7d11c7a](https://github.com/cpcloud/minesweep-rs/commit/7d11c7ac4335cfff774adc1ea2321ff371a19185)) 88 | 89 | ## [6.0.42](https://github.com/cpcloud/minesweep-rs/compare/v6.0.41...v6.0.42) (2023-12-18) 90 | 91 | 92 | ### Bug Fixes 93 | 94 | * **deps:** update rust crate ratatui to ^0.25.0 ([00dde52](https://github.com/cpcloud/minesweep-rs/commit/00dde522f22484826526ee96c2e4dc69e2eaec88)) 95 | 96 | ## [6.0.41](https://github.com/cpcloud/minesweep-rs/compare/v6.0.40...v6.0.41) (2023-12-15) 97 | 98 | 99 | ### Bug Fixes 100 | 101 | * **deps:** update rust crate thiserror to ^1.0.51 ([e487d73](https://github.com/cpcloud/minesweep-rs/commit/e487d736d2f75d79bfab28a75dee3ff4f559da04)) 102 | 103 | ## [6.0.40](https://github.com/cpcloud/minesweep-rs/compare/v6.0.39...v6.0.40) (2023-11-04) 104 | 105 | 106 | ### Bug Fixes 107 | 108 | * **deps:** update rust crate termion to ^2.0.3 ([fe16a8a](https://github.com/cpcloud/minesweep-rs/commit/fe16a8ae0ca08fb6a94e698b2178a870e1003a5f)) 109 | 110 | ## [6.0.39](https://github.com/cpcloud/minesweep-rs/compare/v6.0.38...v6.0.39) (2023-10-23) 111 | 112 | 113 | ### Bug Fixes 114 | 115 | * **deps:** update rust crate ratatui to ^0.24.0 ([037e3a3](https://github.com/cpcloud/minesweep-rs/commit/037e3a3590c4f619532ac45f6e6bf34d86e8f43f)) 116 | 117 | ## [6.0.38](https://github.com/cpcloud/minesweep-rs/compare/v6.0.37...v6.0.38) (2023-10-19) 118 | 119 | 120 | ### Bug Fixes 121 | 122 | * **deps:** update rust crate thiserror to ^1.0.50 ([362f19c](https://github.com/cpcloud/minesweep-rs/commit/362f19ca48a2630a9dcacca409a113f36cd3ab70)) 123 | 124 | ## [6.0.37](https://github.com/cpcloud/minesweep-rs/compare/v6.0.36...v6.0.37) (2023-10-19) 125 | 126 | 127 | ### Bug Fixes 128 | 129 | * **deps:** update rust crate typed-builder to ^0.18.0 ([5278f70](https://github.com/cpcloud/minesweep-rs/commit/5278f7086a17774182d282805f8a169ac29ec915)) 130 | 131 | ## [6.0.36](https://github.com/cpcloud/minesweep-rs/compare/v6.0.35...v6.0.36) (2023-10-15) 132 | 133 | 134 | ### Bug Fixes 135 | 136 | * **deps:** update rust crate typed-builder to ^0.17.0 ([f73f158](https://github.com/cpcloud/minesweep-rs/commit/f73f1585846b9c222aedaae69417a03c03621ffc)) 137 | 138 | ## [6.0.35](https://github.com/cpcloud/minesweep-rs/compare/v6.0.34...v6.0.35) (2023-10-07) 139 | 140 | 141 | ### Bug Fixes 142 | 143 | * **deps:** update rust crate num-traits to ^0.2.17 ([278a0e5](https://github.com/cpcloud/minesweep-rs/commit/278a0e5d7f33ebf62cb9372f95336ade03be7862)) 144 | 145 | ## [6.0.34](https://github.com/cpcloud/minesweep-rs/compare/v6.0.33...v6.0.34) (2023-09-27) 146 | 147 | 148 | ### Bug Fixes 149 | 150 | * **deps:** update rust crate thiserror to ^1.0.49 ([d6beb69](https://github.com/cpcloud/minesweep-rs/commit/d6beb69152ecb7c634431679f3082e88e5ab0b55)) 151 | 152 | ## [6.0.33](https://github.com/cpcloud/minesweep-rs/compare/v6.0.32...v6.0.33) (2023-09-22) 153 | 154 | 155 | ### Bug Fixes 156 | 157 | * **deps:** update rust crate typed-builder to ^0.16.2 ([f19e5f3](https://github.com/cpcloud/minesweep-rs/commit/f19e5f32c6bab0632c6553b6347eb1ed922759cf)) 158 | 159 | ## [6.0.32](https://github.com/cpcloud/minesweep-rs/compare/v6.0.31...v6.0.32) (2023-09-18) 160 | 161 | 162 | ### Bug Fixes 163 | 164 | * **deps:** update rust crate typed-builder to ^0.16.1 ([183c5bb](https://github.com/cpcloud/minesweep-rs/commit/183c5bba3838829ade8d01cc03b47bd552e6cff6)) 165 | 166 | ## [6.0.31](https://github.com/cpcloud/minesweep-rs/compare/v6.0.30...v6.0.31) (2023-09-03) 167 | 168 | 169 | ### Bug Fixes 170 | 171 | * **deps:** update rust crate ctrlc to ^3.4.1 ([22ec571](https://github.com/cpcloud/minesweep-rs/commit/22ec57125207ed3be034e443ddba7f48bb8892f6)) 172 | 173 | ## [6.0.30](https://github.com/cpcloud/minesweep-rs/compare/v6.0.29...v6.0.30) (2023-09-02) 174 | 175 | 176 | ### Bug Fixes 177 | 178 | * **deps:** update rust crate thiserror to ^1.0.48 ([f1ca3f1](https://github.com/cpcloud/minesweep-rs/commit/f1ca3f1edfb0060d598ebdaf1570dc077a06f428)) 179 | 180 | ## [6.0.29](https://github.com/cpcloud/minesweep-rs/compare/v6.0.28...v6.0.29) (2023-08-28) 181 | 182 | 183 | ### Bug Fixes 184 | 185 | * **deps:** update rust crate ratatui to ^0.23.0 ([428d0d6](https://github.com/cpcloud/minesweep-rs/commit/428d0d6df67372b70d5aa17a38a99aaf539a37f2)) 186 | 187 | ## [6.0.28](https://github.com/cpcloud/minesweep-rs/compare/v6.0.27...v6.0.28) (2023-08-26) 188 | 189 | 190 | ### Bug Fixes 191 | 192 | * **deps:** update rust crate typed-builder to ^0.16.0 ([8d83e60](https://github.com/cpcloud/minesweep-rs/commit/8d83e60efea7248f27e3a72056af5a94eeb6b23d)) 193 | 194 | ## [6.0.27](https://github.com/cpcloud/minesweep-rs/compare/v6.0.26...v6.0.27) (2023-08-17) 195 | 196 | 197 | ### Bug Fixes 198 | 199 | * **deps:** update rust crate thiserror to ^1.0.47 ([dad942a](https://github.com/cpcloud/minesweep-rs/commit/dad942a1d5b9748c72ea94199fc7e27430bc0d43)) 200 | 201 | ## [6.0.26](https://github.com/cpcloud/minesweep-rs/compare/v6.0.25...v6.0.26) (2023-08-17) 202 | 203 | 204 | ### Bug Fixes 205 | 206 | * **deps:** update rust crate anyhow to ^1.0.75 ([1cec29e](https://github.com/cpcloud/minesweep-rs/commit/1cec29e3ff8c94702a664b3a35ff313b206f5eac)) 207 | 208 | ## [6.0.25](https://github.com/cpcloud/minesweep-rs/compare/v6.0.24...v6.0.25) (2023-08-15) 209 | 210 | 211 | ### Bug Fixes 212 | 213 | * **deps:** update rust crate thiserror to ^1.0.46 ([592384b](https://github.com/cpcloud/minesweep-rs/commit/592384beabebe5d198d305547cc1abb3dd49164b)) 214 | 215 | ## [6.0.24](https://github.com/cpcloud/minesweep-rs/compare/v6.0.23...v6.0.24) (2023-08-15) 216 | 217 | 218 | ### Bug Fixes 219 | 220 | * **deps:** update rust crate anyhow to ^1.0.74 ([99411f1](https://github.com/cpcloud/minesweep-rs/commit/99411f1923dca5eaee49c03f94add7557f61a24e)) 221 | 222 | ## [6.0.23](https://github.com/cpcloud/minesweep-rs/compare/v6.0.22...v6.0.23) (2023-08-15) 223 | 224 | 225 | ### Bug Fixes 226 | 227 | * **deps:** update rust crate thiserror to ^1.0.45 ([57becae](https://github.com/cpcloud/minesweep-rs/commit/57becae9bf14fa314b15e063711caf23fad85e9f)) 228 | 229 | ## [6.0.22](https://github.com/cpcloud/minesweep-rs/compare/v6.0.21...v6.0.22) (2023-08-15) 230 | 231 | 232 | ### Bug Fixes 233 | 234 | * **deps:** update rust crate anyhow to ^1.0.73 ([c156221](https://github.com/cpcloud/minesweep-rs/commit/c1562211a5f3a45ac52abbbb269a968b8f5d6f5e)) 235 | 236 | ## [6.0.21](https://github.com/cpcloud/minesweep-rs/compare/v6.0.20...v6.0.21) (2023-08-03) 237 | 238 | 239 | ### Bug Fixes 240 | 241 | * **deps:** update rust crate typed-builder to ^0.15.2 ([77a1446](https://github.com/cpcloud/minesweep-rs/commit/77a144670c0cfeddbfd469d807a8dbf1d9e153a5)) 242 | 243 | ## [6.0.20](https://github.com/cpcloud/minesweep-rs/compare/v6.0.19...v6.0.20) (2023-07-27) 244 | 245 | 246 | ### Bug Fixes 247 | 248 | * **win-state:** ensure that if remaining unexposed tiles are all bombs the user wins ([47ec337](https://github.com/cpcloud/minesweep-rs/commit/47ec337c8d762ea64c1c1199e2f23aafcc93aece)) 249 | 250 | ## [6.0.19](https://github.com/cpcloud/minesweep-rs/compare/v6.0.18...v6.0.19) (2023-07-23) 251 | 252 | 253 | ### Bug Fixes 254 | 255 | * **deps:** update rust crate thiserror to ^1.0.44 ([61ad7c4](https://github.com/cpcloud/minesweep-rs/commit/61ad7c47c5db26d4e3e10c2b46889820e7aa46c8)) 256 | 257 | ## [6.0.18](https://github.com/cpcloud/minesweep-rs/compare/v6.0.17...v6.0.18) (2023-07-21) 258 | 259 | 260 | ### Bug Fixes 261 | 262 | * **deps:** update rust crate num-traits to ^0.2.16 ([2e5e27c](https://github.com/cpcloud/minesweep-rs/commit/2e5e27c0eb81cb1f11335ffc8387e69d9fbc2f0b)) 263 | 264 | ## [6.0.17](https://github.com/cpcloud/minesweep-rs/compare/v6.0.16...v6.0.17) (2023-07-17) 265 | 266 | 267 | ### Bug Fixes 268 | 269 | * **deps:** update rust crate ratatui to ^0.22.0 ([e31c545](https://github.com/cpcloud/minesweep-rs/commit/e31c545db4b3f5aac421bc6fae24f58d9e932e4d)) 270 | 271 | ## [6.0.16](https://github.com/cpcloud/minesweep-rs/compare/v6.0.15...v6.0.16) (2023-07-15) 272 | 273 | 274 | ### Bug Fixes 275 | 276 | * **deps:** update rust crate typed-builder to ^0.15.1 ([a7082e8](https://github.com/cpcloud/minesweep-rs/commit/a7082e8d846cb66901db909ad7a16057caf58fbd)) 277 | 278 | ## [6.0.15](https://github.com/cpcloud/minesweep-rs/compare/v6.0.14...v6.0.15) (2023-07-15) 279 | 280 | 281 | ### Bug Fixes 282 | 283 | * **deps:** update rust crate anyhow to ^1.0.72 ([457b824](https://github.com/cpcloud/minesweep-rs/commit/457b824e888f0dfc1d4f870123b46b55f80a7380)) 284 | 285 | ## [6.0.14](https://github.com/cpcloud/minesweep-rs/compare/v6.0.13...v6.0.14) (2023-07-07) 286 | 287 | 288 | ### Bug Fixes 289 | 290 | * **deps:** update rust crate thiserror to ^1.0.43 ([97482f3](https://github.com/cpcloud/minesweep-rs/commit/97482f3f1b9d974a1ff90edcc70c57d93b66d2a9)) 291 | 292 | ## [6.0.13](https://github.com/cpcloud/minesweep-rs/compare/v6.0.12...v6.0.13) (2023-07-06) 293 | 294 | 295 | ### Bug Fixes 296 | 297 | * **deps:** update rust crate typed-builder to ^0.15.0 ([0c2e822](https://github.com/cpcloud/minesweep-rs/commit/0c2e8223b6ecc3cbf43dd30180e2ded00a1288be)) 298 | 299 | ## [6.0.12](https://github.com/cpcloud/minesweep-rs/compare/v6.0.11...v6.0.12) (2023-07-05) 300 | 301 | 302 | ### Bug Fixes 303 | 304 | * **deps:** update rust crate thiserror to ^1.0.41 ([40fa591](https://github.com/cpcloud/minesweep-rs/commit/40fa59148da31aca2425d841f167c27dca0abbcc)) 305 | 306 | ## [6.0.11](https://github.com/cpcloud/minesweep-rs/compare/v6.0.10...v6.0.11) (2023-05-30) 307 | 308 | 309 | ### Bug Fixes 310 | 311 | * **deps:** update rust crate ctrlc to ^3.4.0 ([7886bc7](https://github.com/cpcloud/minesweep-rs/commit/7886bc79e017d891b43bdde7050e26c0e9c2290e)) 312 | 313 | ## [6.0.10](https://github.com/cpcloud/minesweep-rs/compare/v6.0.9...v6.0.10) (2023-05-29) 314 | 315 | 316 | ### Bug Fixes 317 | 318 | * **deps:** update rust crate ratatui to ^0.21.0 ([a131c9f](https://github.com/cpcloud/minesweep-rs/commit/a131c9f2d998b6b8bcee55d45aa264bac138531b)) 319 | 320 | ## [6.0.9](https://github.com/cpcloud/minesweep-rs/compare/v6.0.8...v6.0.9) (2023-05-23) 321 | 322 | 323 | ### Bug Fixes 324 | 325 | * **deps:** update rust crate ctrlc to ^3.3.1 ([80dc98a](https://github.com/cpcloud/minesweep-rs/commit/80dc98a5b2835bbe821595602e102ddfffdfcfb5)) 326 | 327 | ## [6.0.8](https://github.com/cpcloud/minesweep-rs/compare/v6.0.7...v6.0.8) (2023-05-21) 328 | 329 | 330 | ### Bug Fixes 331 | 332 | * **deps:** update rust crate ctrlc to ^3.3.0 ([1b0d38e](https://github.com/cpcloud/minesweep-rs/commit/1b0d38e393496512815309b785a35a669907de6f)) 333 | 334 | ## [6.0.7](https://github.com/cpcloud/minesweep-rs/compare/v6.0.6...v6.0.7) (2023-04-30) 335 | 336 | 337 | ### Bug Fixes 338 | 339 | * **ui:** ensure overflow on grid setup does not happen ([7468748](https://github.com/cpcloud/minesweep-rs/commit/74687482e66ddf79987422c723f381319ba70536)) 340 | 341 | ## [6.0.6](https://github.com/cpcloud/minesweep-rs/compare/v6.0.5...v6.0.6) (2023-04-30) 342 | 343 | 344 | ### Bug Fixes 345 | 346 | * **padding:** work around right side padding issue ([e793b92](https://github.com/cpcloud/minesweep-rs/commit/e793b92b73dada11421e3e46013efaf06ebd52c2)) 347 | 348 | ## [6.0.5](https://github.com/cpcloud/minesweep-rs/compare/v6.0.4...v6.0.5) (2023-04-30) 349 | 350 | 351 | ### Bug Fixes 352 | 353 | * **deps:** bump em all ([af4496b](https://github.com/cpcloud/minesweep-rs/commit/af4496b82efa38e186e75504ad985a49026f137c)) 354 | * **deps:** update rust crate termion to v2 ([c002260](https://github.com/cpcloud/minesweep-rs/commit/c0022608668731bc97b700dee286fa34162eb5c3)) 355 | * **flags:** handle the case where all flags have been used ([be7ce04](https://github.com/cpcloud/minesweep-rs/commit/be7ce042dd7abf3cc8be339158d5b89789415528)) 356 | 357 | ## [6.0.4](https://github.com/cpcloud/minesweep-rs/compare/v6.0.3...v6.0.4) (2022-01-14) 358 | 359 | 360 | ### Performance Improvements 361 | 362 | * shave off another 100k from the binary ([8daf770](https://github.com/cpcloud/minesweep-rs/commit/8daf7703f1ad74cbae80308ed0203fe39c38aea2)) 363 | 364 | ## [6.0.3](https://github.com/cpcloud/minesweep-rs/compare/v6.0.2...v6.0.3) (2022-01-07) 365 | 366 | 367 | ### Bug Fixes 368 | 369 | * **ci:** use update action and refactor release ([d52b15d](https://github.com/cpcloud/minesweep-rs/commit/d52b15d41581db92e444706a61c3dc5021a7e910)) 370 | -------------------------------------------------------------------------------- /Cargo.lock: -------------------------------------------------------------------------------- 1 | # This file is automatically @generated by Cargo. 2 | # It is not intended for manual editing. 3 | version = 3 4 | 5 | [[package]] 6 | name = "ahash" 7 | version = "0.8.8" 8 | source = "registry+https://github.com/rust-lang/crates.io-index" 9 | checksum = "42cd52102d3df161c77a887b608d7a4897d7cc112886a9537b738a887a03aaff" 10 | dependencies = [ 11 | "cfg-if", 12 | "once_cell", 13 | "version_check", 14 | "zerocopy", 15 | ] 16 | 17 | [[package]] 18 | name = "allocator-api2" 19 | version = "0.2.16" 20 | source = "registry+https://github.com/rust-lang/crates.io-index" 21 | checksum = "0942ffc6dcaadf03badf6e6a2d0228460359d5e34b57ccdc720b7382dfbd5ec5" 22 | 23 | [[package]] 24 | name = "ansi_term" 25 | version = "0.12.1" 26 | source = "registry+https://github.com/rust-lang/crates.io-index" 27 | checksum = "d52a9bb7ec0cf484c551830a7ce27bd20d67eac647e1befb56b0be4ee39a55d2" 28 | dependencies = [ 29 | "winapi", 30 | ] 31 | 32 | [[package]] 33 | name = "anyhow" 34 | version = "1.0.79" 35 | source = "registry+https://github.com/rust-lang/crates.io-index" 36 | checksum = "080e9890a082662b09c1ad45f567faeeb47f22b5fb23895fbe1e651e718e25ca" 37 | 38 | [[package]] 39 | name = "atty" 40 | version = "0.2.14" 41 | source = "registry+https://github.com/rust-lang/crates.io-index" 42 | checksum = "d9b39be18770d11421cdb1b9947a45dd3f37e93092cbf377614828a319d5fee8" 43 | dependencies = [ 44 | "hermit-abi", 45 | "libc", 46 | "winapi", 47 | ] 48 | 49 | [[package]] 50 | name = "autocfg" 51 | version = "1.1.0" 52 | source = "registry+https://github.com/rust-lang/crates.io-index" 53 | checksum = "d468802bab17cbc0cc575e9b053f41e72aa36bfa6b7f55e3529ffa43161b97fa" 54 | 55 | [[package]] 56 | name = "bit-set" 57 | version = "0.5.3" 58 | source = "registry+https://github.com/rust-lang/crates.io-index" 59 | checksum = "0700ddab506f33b20a03b13996eccd309a48e5ff77d0d95926aa0210fb4e95f1" 60 | dependencies = [ 61 | "bit-vec", 62 | ] 63 | 64 | [[package]] 65 | name = "bit-vec" 66 | version = "0.6.3" 67 | source = "registry+https://github.com/rust-lang/crates.io-index" 68 | checksum = "349f9b6a179ed607305526ca489b34ad0a41aed5f7980fa90eb03160b69598fb" 69 | 70 | [[package]] 71 | name = "bitflags" 72 | version = "1.3.2" 73 | source = "registry+https://github.com/rust-lang/crates.io-index" 74 | checksum = "bef38d45163c2f1dde094a7dfd33ccf595c92905c8f8f4fdc18d06fb1037718a" 75 | 76 | [[package]] 77 | name = "bitflags" 78 | version = "2.4.2" 79 | source = "registry+https://github.com/rust-lang/crates.io-index" 80 | checksum = "ed570934406eb16438a4e976b1b4500774099c13b8cb96eec99f620f05090ddf" 81 | 82 | [[package]] 83 | name = "cassowary" 84 | version = "0.3.0" 85 | source = "registry+https://github.com/rust-lang/crates.io-index" 86 | checksum = "df8670b8c7b9dae1793364eafadf7239c40d669904660c5960d74cfd80b46a53" 87 | 88 | [[package]] 89 | name = "castaway" 90 | version = "0.2.2" 91 | source = "registry+https://github.com/rust-lang/crates.io-index" 92 | checksum = "8a17ed5635fc8536268e5d4de1e22e81ac34419e5f052d4d51f4e01dcc263fcc" 93 | dependencies = [ 94 | "rustversion", 95 | ] 96 | 97 | [[package]] 98 | name = "cfg-if" 99 | version = "1.0.0" 100 | source = "registry+https://github.com/rust-lang/crates.io-index" 101 | checksum = "baf1de4339761588bc0619e3cbc0120ee582ebb74b53b4efbf79117bd2da40fd" 102 | 103 | [[package]] 104 | name = "clap" 105 | version = "2.34.0" 106 | source = "registry+https://github.com/rust-lang/crates.io-index" 107 | checksum = "a0610544180c38b88101fecf2dd634b174a62eef6946f84dfc6a7127512b381c" 108 | dependencies = [ 109 | "ansi_term", 110 | "atty", 111 | "bitflags 1.3.2", 112 | "strsim", 113 | "textwrap", 114 | "unicode-width", 115 | "vec_map", 116 | ] 117 | 118 | [[package]] 119 | name = "compact_str" 120 | version = "0.7.1" 121 | source = "registry+https://github.com/rust-lang/crates.io-index" 122 | checksum = "f86b9c4c00838774a6d902ef931eff7470720c51d90c2e32cfe15dc304737b3f" 123 | dependencies = [ 124 | "castaway", 125 | "cfg-if", 126 | "itoa", 127 | "ryu", 128 | "static_assertions", 129 | ] 130 | 131 | [[package]] 132 | name = "ctrlc" 133 | version = "3.4.2" 134 | source = "registry+https://github.com/rust-lang/crates.io-index" 135 | checksum = "b467862cc8610ca6fc9a1532d7777cee0804e678ab45410897b9396495994a0b" 136 | dependencies = [ 137 | "nix", 138 | "windows-sys", 139 | ] 140 | 141 | [[package]] 142 | name = "either" 143 | version = "1.10.0" 144 | source = "registry+https://github.com/rust-lang/crates.io-index" 145 | checksum = "11157ac094ffbdde99aa67b23417ebdd801842852b500e395a45a9c0aac03e4a" 146 | 147 | [[package]] 148 | name = "getrandom" 149 | version = "0.2.12" 150 | source = "registry+https://github.com/rust-lang/crates.io-index" 151 | checksum = "190092ea657667030ac6a35e305e62fc4dd69fd98ac98631e5d3a2b1575a12b5" 152 | dependencies = [ 153 | "cfg-if", 154 | "libc", 155 | "wasi", 156 | ] 157 | 158 | [[package]] 159 | name = "hashbrown" 160 | version = "0.14.3" 161 | source = "registry+https://github.com/rust-lang/crates.io-index" 162 | checksum = "290f1a1d9242c78d09ce40a5e87e7554ee637af1351968159f4952f028f75604" 163 | dependencies = [ 164 | "ahash", 165 | "allocator-api2", 166 | ] 167 | 168 | [[package]] 169 | name = "heck" 170 | version = "0.3.3" 171 | source = "registry+https://github.com/rust-lang/crates.io-index" 172 | checksum = "6d621efb26863f0e9924c6ac577e8275e5e6b77455db64ffa6c65c904e9e132c" 173 | dependencies = [ 174 | "unicode-segmentation", 175 | ] 176 | 177 | [[package]] 178 | name = "heck" 179 | version = "0.4.1" 180 | source = "registry+https://github.com/rust-lang/crates.io-index" 181 | checksum = "95505c38b4572b2d910cecb0281560f54b440a19336cbbcb27bf6ce6adc6f5a8" 182 | 183 | [[package]] 184 | name = "hermit-abi" 185 | version = "0.1.19" 186 | source = "registry+https://github.com/rust-lang/crates.io-index" 187 | checksum = "62b467343b94ba476dcb2500d242dadbb39557df889310ac77c5d99100aaac33" 188 | dependencies = [ 189 | "libc", 190 | ] 191 | 192 | [[package]] 193 | name = "indoc" 194 | version = "2.0.4" 195 | source = "registry+https://github.com/rust-lang/crates.io-index" 196 | checksum = "1e186cfbae8084e513daff4240b4797e342f988cecda4fb6c939150f96315fd8" 197 | 198 | [[package]] 199 | name = "itertools" 200 | version = "0.12.1" 201 | source = "registry+https://github.com/rust-lang/crates.io-index" 202 | checksum = "ba291022dbbd398a455acf126c1e341954079855bc60dfdda641363bd6922569" 203 | dependencies = [ 204 | "either", 205 | ] 206 | 207 | [[package]] 208 | name = "itoa" 209 | version = "1.0.10" 210 | source = "registry+https://github.com/rust-lang/crates.io-index" 211 | checksum = "b1a46d1a171d865aa5f83f92695765caa047a9b4cbae2cbf37dbd613a793fd4c" 212 | 213 | [[package]] 214 | name = "lazy_static" 215 | version = "1.4.0" 216 | source = "registry+https://github.com/rust-lang/crates.io-index" 217 | checksum = "e2abad23fbc42b3700f2f279844dc832adb2b2eb069b2df918f455c4e18cc646" 218 | 219 | [[package]] 220 | name = "libc" 221 | version = "0.2.153" 222 | source = "registry+https://github.com/rust-lang/crates.io-index" 223 | checksum = "9c198f91728a82281a64e1f4f9eeb25d82cb32a5de251c6bd1b5154d63a8e7bd" 224 | 225 | [[package]] 226 | name = "libredox" 227 | version = "0.0.2" 228 | source = "registry+https://github.com/rust-lang/crates.io-index" 229 | checksum = "3af92c55d7d839293953fcd0fda5ecfe93297cfde6ffbdec13b41d99c0ba6607" 230 | dependencies = [ 231 | "bitflags 2.4.2", 232 | "libc", 233 | "redox_syscall", 234 | ] 235 | 236 | [[package]] 237 | name = "lru" 238 | version = "0.12.2" 239 | source = "registry+https://github.com/rust-lang/crates.io-index" 240 | checksum = "db2c024b41519440580066ba82aab04092b333e09066a5eb86c7c4890df31f22" 241 | dependencies = [ 242 | "hashbrown", 243 | ] 244 | 245 | [[package]] 246 | name = "minesweep" 247 | version = "6.0.54" 248 | dependencies = [ 249 | "anyhow", 250 | "bit-set", 251 | "ctrlc", 252 | "num-traits", 253 | "rand", 254 | "ratatui", 255 | "structopt", 256 | "termion", 257 | "thiserror", 258 | "typed-builder", 259 | ] 260 | 261 | [[package]] 262 | name = "nix" 263 | version = "0.27.1" 264 | source = "registry+https://github.com/rust-lang/crates.io-index" 265 | checksum = "2eb04e9c688eff1c89d72b407f168cf79bb9e867a9d3323ed6c01519eb9cc053" 266 | dependencies = [ 267 | "bitflags 2.4.2", 268 | "cfg-if", 269 | "libc", 270 | ] 271 | 272 | [[package]] 273 | name = "num-traits" 274 | version = "0.2.18" 275 | source = "registry+https://github.com/rust-lang/crates.io-index" 276 | checksum = "da0df0e5185db44f69b44f26786fe401b6c293d1907744beaa7fa62b2e5a517a" 277 | dependencies = [ 278 | "autocfg", 279 | ] 280 | 281 | [[package]] 282 | name = "numtoa" 283 | version = "0.1.0" 284 | source = "registry+https://github.com/rust-lang/crates.io-index" 285 | checksum = "b8f8bdf33df195859076e54ab11ee78a1b208382d3a26ec40d142ffc1ecc49ef" 286 | 287 | [[package]] 288 | name = "once_cell" 289 | version = "1.19.0" 290 | source = "registry+https://github.com/rust-lang/crates.io-index" 291 | checksum = "3fdb12b2476b595f9358c5161aa467c2438859caa136dec86c26fdd2efe17b92" 292 | 293 | [[package]] 294 | name = "paste" 295 | version = "1.0.14" 296 | source = "registry+https://github.com/rust-lang/crates.io-index" 297 | checksum = "de3145af08024dea9fa9914f381a17b8fc6034dfb00f3a84013f7ff43f29ed4c" 298 | 299 | [[package]] 300 | name = "ppv-lite86" 301 | version = "0.2.17" 302 | source = "registry+https://github.com/rust-lang/crates.io-index" 303 | checksum = "5b40af805b3121feab8a3c29f04d8ad262fa8e0561883e7653e024ae4479e6de" 304 | 305 | [[package]] 306 | name = "proc-macro-error" 307 | version = "1.0.4" 308 | source = "registry+https://github.com/rust-lang/crates.io-index" 309 | checksum = "da25490ff9892aab3fcf7c36f08cfb902dd3e71ca0f9f9517bea02a73a5ce38c" 310 | dependencies = [ 311 | "proc-macro-error-attr", 312 | "proc-macro2", 313 | "quote", 314 | "syn 1.0.109", 315 | "version_check", 316 | ] 317 | 318 | [[package]] 319 | name = "proc-macro-error-attr" 320 | version = "1.0.4" 321 | source = "registry+https://github.com/rust-lang/crates.io-index" 322 | checksum = "a1be40180e52ecc98ad80b184934baf3d0d29f979574e439af5a55274b35f869" 323 | dependencies = [ 324 | "proc-macro2", 325 | "quote", 326 | "version_check", 327 | ] 328 | 329 | [[package]] 330 | name = "proc-macro2" 331 | version = "1.0.78" 332 | source = "registry+https://github.com/rust-lang/crates.io-index" 333 | checksum = "e2422ad645d89c99f8f3e6b88a9fdeca7fabeac836b1002371c4367c8f984aae" 334 | dependencies = [ 335 | "unicode-ident", 336 | ] 337 | 338 | [[package]] 339 | name = "quote" 340 | version = "1.0.35" 341 | source = "registry+https://github.com/rust-lang/crates.io-index" 342 | checksum = "291ec9ab5efd934aaf503a6466c5d5251535d108ee747472c3977cc5acc868ef" 343 | dependencies = [ 344 | "proc-macro2", 345 | ] 346 | 347 | [[package]] 348 | name = "rand" 349 | version = "0.8.5" 350 | source = "registry+https://github.com/rust-lang/crates.io-index" 351 | checksum = "34af8d1a0e25924bc5b7c43c079c942339d8f0a8b57c39049bef581b46327404" 352 | dependencies = [ 353 | "libc", 354 | "rand_chacha", 355 | "rand_core", 356 | ] 357 | 358 | [[package]] 359 | name = "rand_chacha" 360 | version = "0.3.1" 361 | source = "registry+https://github.com/rust-lang/crates.io-index" 362 | checksum = "e6c10a63a0fa32252be49d21e7709d4d4baf8d231c2dbce1eaa8141b9b127d88" 363 | dependencies = [ 364 | "ppv-lite86", 365 | "rand_core", 366 | ] 367 | 368 | [[package]] 369 | name = "rand_core" 370 | version = "0.6.4" 371 | source = "registry+https://github.com/rust-lang/crates.io-index" 372 | checksum = "ec0be4795e2f6a28069bec0b5ff3e2ac9bafc99e6a9a7dc3547996c5c816922c" 373 | dependencies = [ 374 | "getrandom", 375 | ] 376 | 377 | [[package]] 378 | name = "ratatui" 379 | version = "0.26.0" 380 | source = "registry+https://github.com/rust-lang/crates.io-index" 381 | checksum = "154b85ef15a5d1719bcaa193c3c81fe645cd120c156874cd660fe49fd21d1373" 382 | dependencies = [ 383 | "bitflags 2.4.2", 384 | "cassowary", 385 | "compact_str", 386 | "indoc", 387 | "itertools", 388 | "lru", 389 | "paste", 390 | "stability", 391 | "strum", 392 | "termion", 393 | "unicode-segmentation", 394 | "unicode-width", 395 | ] 396 | 397 | [[package]] 398 | name = "redox_syscall" 399 | version = "0.4.1" 400 | source = "registry+https://github.com/rust-lang/crates.io-index" 401 | checksum = "4722d768eff46b75989dd134e5c353f0d6296e5aaa3132e776cbdb56be7731aa" 402 | dependencies = [ 403 | "bitflags 1.3.2", 404 | ] 405 | 406 | [[package]] 407 | name = "redox_termios" 408 | version = "0.1.3" 409 | source = "registry+https://github.com/rust-lang/crates.io-index" 410 | checksum = "20145670ba436b55d91fc92d25e71160fbfbdd57831631c8d7d36377a476f1cb" 411 | 412 | [[package]] 413 | name = "rustversion" 414 | version = "1.0.14" 415 | source = "registry+https://github.com/rust-lang/crates.io-index" 416 | checksum = "7ffc183a10b4478d04cbbbfc96d0873219d962dd5accaff2ffbd4ceb7df837f4" 417 | 418 | [[package]] 419 | name = "ryu" 420 | version = "1.0.16" 421 | source = "registry+https://github.com/rust-lang/crates.io-index" 422 | checksum = "f98d2aa92eebf49b69786be48e4477826b256916e84a57ff2a4f21923b48eb4c" 423 | 424 | [[package]] 425 | name = "stability" 426 | version = "0.1.1" 427 | source = "registry+https://github.com/rust-lang/crates.io-index" 428 | checksum = "ebd1b177894da2a2d9120208c3386066af06a488255caabc5de8ddca22dbc3ce" 429 | dependencies = [ 430 | "quote", 431 | "syn 1.0.109", 432 | ] 433 | 434 | [[package]] 435 | name = "static_assertions" 436 | version = "1.1.0" 437 | source = "registry+https://github.com/rust-lang/crates.io-index" 438 | checksum = "a2eb9349b6444b326872e140eb1cf5e7c522154d69e7a0ffb0fb81c06b37543f" 439 | 440 | [[package]] 441 | name = "strsim" 442 | version = "0.8.0" 443 | source = "registry+https://github.com/rust-lang/crates.io-index" 444 | checksum = "8ea5119cdb4c55b55d432abb513a0429384878c15dde60cc77b1c99de1a95a6a" 445 | 446 | [[package]] 447 | name = "structopt" 448 | version = "0.3.26" 449 | source = "registry+https://github.com/rust-lang/crates.io-index" 450 | checksum = "0c6b5c64445ba8094a6ab0c3cd2ad323e07171012d9c98b0b15651daf1787a10" 451 | dependencies = [ 452 | "clap", 453 | "lazy_static", 454 | "structopt-derive", 455 | ] 456 | 457 | [[package]] 458 | name = "structopt-derive" 459 | version = "0.4.18" 460 | source = "registry+https://github.com/rust-lang/crates.io-index" 461 | checksum = "dcb5ae327f9cc13b68763b5749770cb9e048a99bd9dfdfa58d0cf05d5f64afe0" 462 | dependencies = [ 463 | "heck 0.3.3", 464 | "proc-macro-error", 465 | "proc-macro2", 466 | "quote", 467 | "syn 1.0.109", 468 | ] 469 | 470 | [[package]] 471 | name = "strum" 472 | version = "0.26.1" 473 | source = "registry+https://github.com/rust-lang/crates.io-index" 474 | checksum = "723b93e8addf9aa965ebe2d11da6d7540fa2283fcea14b3371ff055f7ba13f5f" 475 | dependencies = [ 476 | "strum_macros", 477 | ] 478 | 479 | [[package]] 480 | name = "strum_macros" 481 | version = "0.26.1" 482 | source = "registry+https://github.com/rust-lang/crates.io-index" 483 | checksum = "7a3417fc93d76740d974a01654a09777cb500428cc874ca9f45edfe0c4d4cd18" 484 | dependencies = [ 485 | "heck 0.4.1", 486 | "proc-macro2", 487 | "quote", 488 | "rustversion", 489 | "syn 2.0.48", 490 | ] 491 | 492 | [[package]] 493 | name = "syn" 494 | version = "1.0.109" 495 | source = "registry+https://github.com/rust-lang/crates.io-index" 496 | checksum = "72b64191b275b66ffe2469e8af2c1cfe3bafa67b529ead792a6d0160888b4237" 497 | dependencies = [ 498 | "proc-macro2", 499 | "quote", 500 | "unicode-ident", 501 | ] 502 | 503 | [[package]] 504 | name = "syn" 505 | version = "2.0.48" 506 | source = "registry+https://github.com/rust-lang/crates.io-index" 507 | checksum = "0f3531638e407dfc0814761abb7c00a5b54992b849452a0646b7f65c9f770f3f" 508 | dependencies = [ 509 | "proc-macro2", 510 | "quote", 511 | "unicode-ident", 512 | ] 513 | 514 | [[package]] 515 | name = "termion" 516 | version = "3.0.0" 517 | source = "registry+https://github.com/rust-lang/crates.io-index" 518 | checksum = "417813675a504dfbbf21bfde32c03e5bf9f2413999962b479023c02848c1c7a5" 519 | dependencies = [ 520 | "libc", 521 | "libredox", 522 | "numtoa", 523 | "redox_termios", 524 | ] 525 | 526 | [[package]] 527 | name = "textwrap" 528 | version = "0.11.0" 529 | source = "registry+https://github.com/rust-lang/crates.io-index" 530 | checksum = "d326610f408c7a4eb6f51c37c330e496b08506c9457c9d34287ecc38809fb060" 531 | dependencies = [ 532 | "unicode-width", 533 | ] 534 | 535 | [[package]] 536 | name = "thiserror" 537 | version = "1.0.57" 538 | source = "registry+https://github.com/rust-lang/crates.io-index" 539 | checksum = "1e45bcbe8ed29775f228095caf2cd67af7a4ccf756ebff23a306bf3e8b47b24b" 540 | dependencies = [ 541 | "thiserror-impl", 542 | ] 543 | 544 | [[package]] 545 | name = "thiserror-impl" 546 | version = "1.0.57" 547 | source = "registry+https://github.com/rust-lang/crates.io-index" 548 | checksum = "a953cb265bef375dae3de6663da4d3804eee9682ea80d8e2542529b73c531c81" 549 | dependencies = [ 550 | "proc-macro2", 551 | "quote", 552 | "syn 2.0.48", 553 | ] 554 | 555 | [[package]] 556 | name = "typed-builder" 557 | version = "0.18.1" 558 | source = "registry+https://github.com/rust-lang/crates.io-index" 559 | checksum = "444d8748011b93cb168770e8092458cb0f8854f931ff82fdf6ddfbd72a9c933e" 560 | dependencies = [ 561 | "typed-builder-macro", 562 | ] 563 | 564 | [[package]] 565 | name = "typed-builder-macro" 566 | version = "0.18.1" 567 | source = "registry+https://github.com/rust-lang/crates.io-index" 568 | checksum = "563b3b88238ec95680aef36bdece66896eaa7ce3c0f1b4f39d38fb2435261352" 569 | dependencies = [ 570 | "proc-macro2", 571 | "quote", 572 | "syn 2.0.48", 573 | ] 574 | 575 | [[package]] 576 | name = "unicode-ident" 577 | version = "1.0.12" 578 | source = "registry+https://github.com/rust-lang/crates.io-index" 579 | checksum = "3354b9ac3fae1ff6755cb6db53683adb661634f67557942dea4facebec0fee4b" 580 | 581 | [[package]] 582 | name = "unicode-segmentation" 583 | version = "1.11.0" 584 | source = "registry+https://github.com/rust-lang/crates.io-index" 585 | checksum = "d4c87d22b6e3f4a18d4d40ef354e97c90fcb14dd91d7dc0aa9d8a1172ebf7202" 586 | 587 | [[package]] 588 | name = "unicode-width" 589 | version = "0.1.11" 590 | source = "registry+https://github.com/rust-lang/crates.io-index" 591 | checksum = "e51733f11c9c4f72aa0c160008246859e340b00807569a0da0e7a1079b27ba85" 592 | 593 | [[package]] 594 | name = "vec_map" 595 | version = "0.8.2" 596 | source = "registry+https://github.com/rust-lang/crates.io-index" 597 | checksum = "f1bddf1187be692e79c5ffeab891132dfb0f236ed36a43c7ed39f1165ee20191" 598 | 599 | [[package]] 600 | name = "version_check" 601 | version = "0.9.4" 602 | source = "registry+https://github.com/rust-lang/crates.io-index" 603 | checksum = "49874b5167b65d7193b8aba1567f5c7d93d001cafc34600cee003eda787e483f" 604 | 605 | [[package]] 606 | name = "wasi" 607 | version = "0.11.0+wasi-snapshot-preview1" 608 | source = "registry+https://github.com/rust-lang/crates.io-index" 609 | checksum = "9c8d87e72b64a3b4db28d11ce29237c246188f4f51057d65a7eab63b7987e423" 610 | 611 | [[package]] 612 | name = "winapi" 613 | version = "0.3.9" 614 | source = "registry+https://github.com/rust-lang/crates.io-index" 615 | checksum = "5c839a674fcd7a98952e593242ea400abe93992746761e38641405d28b00f419" 616 | dependencies = [ 617 | "winapi-i686-pc-windows-gnu", 618 | "winapi-x86_64-pc-windows-gnu", 619 | ] 620 | 621 | [[package]] 622 | name = "winapi-i686-pc-windows-gnu" 623 | version = "0.4.0" 624 | source = "registry+https://github.com/rust-lang/crates.io-index" 625 | checksum = "ac3b87c63620426dd9b991e5ce0329eff545bccbbb34f3be09ff6fb6ab51b7b6" 626 | 627 | [[package]] 628 | name = "winapi-x86_64-pc-windows-gnu" 629 | version = "0.4.0" 630 | source = "registry+https://github.com/rust-lang/crates.io-index" 631 | checksum = "712e227841d057c1ee1cd2fb22fa7e5a5461ae8e48fa2ca79ec42cfc1931183f" 632 | 633 | [[package]] 634 | name = "windows-sys" 635 | version = "0.52.0" 636 | source = "registry+https://github.com/rust-lang/crates.io-index" 637 | checksum = "282be5f36a8ce781fad8c8ae18fa3f9beff57ec1b52cb3de0789201425d9a33d" 638 | dependencies = [ 639 | "windows-targets", 640 | ] 641 | 642 | [[package]] 643 | name = "windows-targets" 644 | version = "0.52.0" 645 | source = "registry+https://github.com/rust-lang/crates.io-index" 646 | checksum = "8a18201040b24831fbb9e4eb208f8892e1f50a37feb53cc7ff887feb8f50e7cd" 647 | dependencies = [ 648 | "windows_aarch64_gnullvm", 649 | "windows_aarch64_msvc", 650 | "windows_i686_gnu", 651 | "windows_i686_msvc", 652 | "windows_x86_64_gnu", 653 | "windows_x86_64_gnullvm", 654 | "windows_x86_64_msvc", 655 | ] 656 | 657 | [[package]] 658 | name = "windows_aarch64_gnullvm" 659 | version = "0.52.0" 660 | source = "registry+https://github.com/rust-lang/crates.io-index" 661 | checksum = "cb7764e35d4db8a7921e09562a0304bf2f93e0a51bfccee0bd0bb0b666b015ea" 662 | 663 | [[package]] 664 | name = "windows_aarch64_msvc" 665 | version = "0.52.0" 666 | source = "registry+https://github.com/rust-lang/crates.io-index" 667 | checksum = "bbaa0368d4f1d2aaefc55b6fcfee13f41544ddf36801e793edbbfd7d7df075ef" 668 | 669 | [[package]] 670 | name = "windows_i686_gnu" 671 | version = "0.52.0" 672 | source = "registry+https://github.com/rust-lang/crates.io-index" 673 | checksum = "a28637cb1fa3560a16915793afb20081aba2c92ee8af57b4d5f28e4b3e7df313" 674 | 675 | [[package]] 676 | name = "windows_i686_msvc" 677 | version = "0.52.0" 678 | source = "registry+https://github.com/rust-lang/crates.io-index" 679 | checksum = "ffe5e8e31046ce6230cc7215707b816e339ff4d4d67c65dffa206fd0f7aa7b9a" 680 | 681 | [[package]] 682 | name = "windows_x86_64_gnu" 683 | version = "0.52.0" 684 | source = "registry+https://github.com/rust-lang/crates.io-index" 685 | checksum = "3d6fa32db2bc4a2f5abeacf2b69f7992cd09dca97498da74a151a3132c26befd" 686 | 687 | [[package]] 688 | name = "windows_x86_64_gnullvm" 689 | version = "0.52.0" 690 | source = "registry+https://github.com/rust-lang/crates.io-index" 691 | checksum = "1a657e1e9d3f514745a572a6846d3c7aa7dbe1658c056ed9c3344c4109a6949e" 692 | 693 | [[package]] 694 | name = "windows_x86_64_msvc" 695 | version = "0.52.0" 696 | source = "registry+https://github.com/rust-lang/crates.io-index" 697 | checksum = "dff9641d1cd4be8d1a070daf9e3773c5f67e78b4d9d42263020c057706765c04" 698 | 699 | [[package]] 700 | name = "zerocopy" 701 | version = "0.7.32" 702 | source = "registry+https://github.com/rust-lang/crates.io-index" 703 | checksum = "74d4d3961e53fa4c9a25a8637fc2bfaf2595b3d3ae34875568a5cf64787716be" 704 | dependencies = [ 705 | "zerocopy-derive", 706 | ] 707 | 708 | [[package]] 709 | name = "zerocopy-derive" 710 | version = "0.7.32" 711 | source = "registry+https://github.com/rust-lang/crates.io-index" 712 | checksum = "9ce1b18ccd8e73a9321186f97e46f9f04b778851177567b1975109d26a08d2a6" 713 | dependencies = [ 714 | "proc-macro2", 715 | "quote", 716 | "syn 2.0.48", 717 | ] 718 | -------------------------------------------------------------------------------- /Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "minesweep" 3 | description = "A mine sweeping game written in Rust" 4 | version = "6.0.54" 5 | authors = ["Phillip Cloud"] 6 | edition = "2021" 7 | license = "Apache-2.0" 8 | homepage = "https://github.com/cpcloud/minesweep-rs" 9 | repository = "https://github.com/cpcloud/minesweep-rs" 10 | keywords = ["terminal", "minesweeper", "game"] 11 | readme = "./README.md" 12 | 13 | [dependencies] 14 | anyhow = "^1.0.79" 15 | bit-set = "^0.5.3" 16 | ctrlc = "^3.4.2" 17 | num-traits = "^0.2.18" 18 | rand = "^0.8.5" 19 | structopt = "^0.3.26" 20 | termion = "^3.0.0" 21 | thiserror = "^1.0.57" 22 | ratatui = { version = "^0.26.0", features = [ 23 | "termion", 24 | ], default-features = false } 25 | typed-builder = "^0.18.1" 26 | 27 | [profile.release] 28 | panic = "abort" 29 | lto = "fat" 30 | codegen-units = 1 31 | opt-level = 's' 32 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | Apache License 2 | Version 2.0, January 2004 3 | http://www.apache.org/licenses/ 4 | 5 | TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION 6 | 7 | 1. Definitions. 8 | 9 | "License" shall mean the terms and conditions for use, reproduction, 10 | and distribution as defined by Sections 1 through 9 of this document. 11 | 12 | "Licensor" shall mean the copyright owner or entity authorized by 13 | the copyright owner that is granting the License. 14 | 15 | "Legal Entity" shall mean the union of the acting entity and all 16 | other entities that control, are controlled by, or are under common 17 | control with that entity. For the purposes of this definition, 18 | "control" means (i) the power, direct or indirect, to cause the 19 | direction or management of such entity, whether by contract or 20 | otherwise, or (ii) ownership of fifty percent (50%) or more of the 21 | outstanding shares, or (iii) beneficial ownership of such entity. 22 | 23 | "You" (or "Your") shall mean an individual or Legal Entity 24 | exercising permissions granted by this License. 25 | 26 | "Source" form shall mean the preferred form for making modifications, 27 | including but not limited to software source code, documentation 28 | source, and configuration files. 29 | 30 | "Object" form shall mean any form resulting from mechanical 31 | transformation or translation of a Source form, including but 32 | not limited to compiled object code, generated documentation, 33 | and conversions to other media types. 34 | 35 | "Work" shall mean the work of authorship, whether in Source or 36 | Object form, made available under the License, as indicated by a 37 | copyright notice that is included in or attached to the work 38 | (an example is provided in the Appendix below). 39 | 40 | "Derivative Works" shall mean any work, whether in Source or Object 41 | form, that is based on (or derived from) the Work and for which the 42 | editorial revisions, annotations, elaborations, or other modifications 43 | represent, as a whole, an original work of authorship. For the purposes 44 | of this License, Derivative Works shall not include works that remain 45 | separable from, or merely link (or bind by name) to the interfaces of, 46 | the Work and Derivative Works thereof. 47 | 48 | "Contribution" shall mean any work of authorship, including 49 | the original version of the Work and any modifications or additions 50 | to that Work or Derivative Works thereof, that is intentionally 51 | submitted to Licensor for inclusion in the Work by the copyright owner 52 | or by an individual or Legal Entity authorized to submit on behalf of 53 | the copyright owner. For the purposes of this definition, "submitted" 54 | means any form of electronic, verbal, or written communication sent 55 | to the Licensor or its representatives, including but not limited to 56 | communication on electronic mailing lists, source code control systems, 57 | and issue tracking systems that are managed by, or on behalf of, the 58 | Licensor for the purpose of discussing and improving the Work, but 59 | excluding communication that is conspicuously marked or otherwise 60 | designated in writing by the copyright owner as "Not a Contribution." 61 | 62 | "Contributor" shall mean Licensor and any individual or Legal Entity 63 | on behalf of whom a Contribution has been received by Licensor and 64 | subsequently incorporated within the Work. 65 | 66 | 2. Grant of Copyright License. Subject to the terms and conditions of 67 | this License, each Contributor hereby grants to You a perpetual, 68 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 69 | copyright license to reproduce, prepare Derivative Works of, 70 | publicly display, publicly perform, sublicense, and distribute the 71 | Work and such Derivative Works in Source or Object form. 72 | 73 | 3. Grant of Patent License. Subject to the terms and conditions of 74 | this License, each Contributor hereby grants to You a perpetual, 75 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 76 | (except as stated in this section) patent license to make, have made, 77 | use, offer to sell, sell, import, and otherwise transfer the Work, 78 | where such license applies only to those patent claims licensable 79 | by such Contributor that are necessarily infringed by their 80 | Contribution(s) alone or by combination of their Contribution(s) 81 | with the Work to which such Contribution(s) was submitted. If You 82 | institute patent litigation against any entity (including a 83 | cross-claim or counterclaim in a lawsuit) alleging that the Work 84 | or a Contribution incorporated within the Work constitutes direct 85 | or contributory patent infringement, then any patent licenses 86 | granted to You under this License for that Work shall terminate 87 | as of the date such litigation is filed. 88 | 89 | 4. Redistribution. You may reproduce and distribute copies of the 90 | Work or Derivative Works thereof in any medium, with or without 91 | modifications, and in Source or Object form, provided that You 92 | meet the following conditions: 93 | 94 | (a) You must give any other recipients of the Work or 95 | Derivative Works a copy of this License; and 96 | 97 | (b) You must cause any modified files to carry prominent notices 98 | stating that You changed the files; and 99 | 100 | (c) You must retain, in the Source form of any Derivative Works 101 | that You distribute, all copyright, patent, trademark, and 102 | attribution notices from the Source form of the Work, 103 | excluding those notices that do not pertain to any part of 104 | the Derivative Works; and 105 | 106 | (d) If the Work includes a "NOTICE" text file as part of its 107 | distribution, then any Derivative Works that You distribute must 108 | include a readable copy of the attribution notices contained 109 | within such NOTICE file, excluding those notices that do not 110 | pertain to any part of the Derivative Works, in at least one 111 | of the following places: within a NOTICE text file distributed 112 | as part of the Derivative Works; within the Source form or 113 | documentation, if provided along with the Derivative Works; or, 114 | within a display generated by the Derivative Works, if and 115 | wherever such third-party notices normally appear. The contents 116 | of the NOTICE file are for informational purposes only and 117 | do not modify the License. You may add Your own attribution 118 | notices within Derivative Works that You distribute, alongside 119 | or as an addendum to the NOTICE text from the Work, provided 120 | that such additional attribution notices cannot be construed 121 | as modifying the License. 122 | 123 | You may add Your own copyright statement to Your modifications and 124 | may provide additional or different license terms and conditions 125 | for use, reproduction, or distribution of Your modifications, or 126 | for any such Derivative Works as a whole, provided Your use, 127 | reproduction, and distribution of the Work otherwise complies with 128 | the conditions stated in this License. 129 | 130 | 5. Submission of Contributions. Unless You explicitly state otherwise, 131 | any Contribution intentionally submitted for inclusion in the Work 132 | by You to the Licensor shall be under the terms and conditions of 133 | this License, without any additional terms or conditions. 134 | Notwithstanding the above, nothing herein shall supersede or modify 135 | the terms of any separate license agreement you may have executed 136 | with Licensor regarding such Contributions. 137 | 138 | 6. Trademarks. This License does not grant permission to use the trade 139 | names, trademarks, service marks, or product names of the Licensor, 140 | except as required for reasonable and customary use in describing the 141 | origin of the Work and reproducing the content of the NOTICE file. 142 | 143 | 7. Disclaimer of Warranty. Unless required by applicable law or 144 | agreed to in writing, Licensor provides the Work (and each 145 | Contributor provides its Contributions) on an "AS IS" BASIS, 146 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or 147 | implied, including, without limitation, any warranties or conditions 148 | of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A 149 | PARTICULAR PURPOSE. You are solely responsible for determining the 150 | appropriateness of using or redistributing the Work and assume any 151 | risks associated with Your exercise of permissions under this License. 152 | 153 | 8. Limitation of Liability. In no event and under no legal theory, 154 | whether in tort (including negligence), contract, or otherwise, 155 | unless required by applicable law (such as deliberate and grossly 156 | negligent acts) or agreed to in writing, shall any Contributor be 157 | liable to You for damages, including any direct, indirect, special, 158 | incidental, or consequential damages of any character arising as a 159 | result of this License or out of the use or inability to use the 160 | Work (including but not limited to damages for loss of goodwill, 161 | work stoppage, computer failure or malfunction, or any and all 162 | other commercial damages or losses), even if such Contributor 163 | has been advised of the possibility of such damages. 164 | 165 | 9. Accepting Warranty or Additional Liability. While redistributing 166 | the Work or Derivative Works thereof, You may choose to offer, 167 | and charge a fee for, acceptance of support, warranty, indemnity, 168 | or other liability obligations and/or rights consistent with this 169 | License. However, in accepting such obligations, You may act only 170 | on Your own behalf and on Your sole responsibility, not on behalf 171 | of any other Contributor, and only if You agree to indemnify, 172 | defend, and hold each Contributor harmless for any liability 173 | incurred by, or claims asserted against, such Contributor by reason 174 | of your accepting any such warranty or additional liability. 175 | 176 | END OF TERMS AND CONDITIONS 177 | 178 | APPENDIX: How to apply the Apache License to your work. 179 | 180 | To apply the Apache License to your work, attach the following 181 | boilerplate notice, with the fields enclosed by brackets "[]" 182 | replaced with your own identifying information. (Don't include 183 | the brackets!) The text should be enclosed in the appropriate 184 | comment syntax for the file format. We also recommend that a 185 | file or class name and description of purpose be included on the 186 | same "printed page" as the copyright notice for easier 187 | identification within third-party archives. 188 | 189 | Copyright 2021 Phillip Cloud 190 | 191 | Licensed under the Apache License, Version 2.0 (the "License"); 192 | you may not use this file except in compliance with the License. 193 | You may obtain a copy of the License at 194 | 195 | http://www.apache.org/licenses/LICENSE-2.0 196 | 197 | Unless required by applicable law or agreed to in writing, software 198 | distributed under the License is distributed on an "AS IS" BASIS, 199 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 200 | See the License for the specific language governing permissions and 201 | limitations under the License. 202 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # `minesweep-rs` 2 | 3 | [![Build Status](https://github.com/cpcloud/minesweep-rs/actions/workflows/ci.yml/badge.svg)](https://github.com/cpcloud/minesweep-rs/actions/workflows/ci.yml) 4 | 5 | A mine sweeping game written in Rust using `tui-rs`. 6 | 7 | ## Demo 8 | 9 | Run with: 10 | 11 | ``` 12 | $ docker run --rm -it ghcr.io/cpcloud/minesweep-rs:latest 13 | ``` 14 | 15 | ![demo](./demo.gif) 16 | -------------------------------------------------------------------------------- /ci/release/replace_version.sh: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env nix-shell 2 | #!nix-shell --pure -i bash -p sd 3 | # shellcheck shell=bash 4 | 5 | set -euo pipefail 6 | 7 | last_release="$1" 8 | next_release="$2" 9 | 10 | sd "version\s*=\s*\"${last_release}\"" "version = \"${next_release}\"" Cargo.toml 11 | -------------------------------------------------------------------------------- /default.nix: -------------------------------------------------------------------------------- 1 | (import 2 | ( 3 | let 4 | lock = builtins.fromJSON (builtins.readFile ./flake.lock); 5 | in 6 | fetchTarball { 7 | url = "https://github.com/edolstra/flake-compat/archive/${lock.nodes.flake-compat.locked.rev}.tar.gz"; 8 | sha256 = lock.nodes.flake-compat.locked.narHash; 9 | } 10 | ) 11 | { 12 | src = ./.; 13 | }).defaultNix 14 | -------------------------------------------------------------------------------- /demo.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/cpcloud/minesweep-rs/6ca1c255ae1088f811499808499d5b4e18ae7f4f/demo.gif -------------------------------------------------------------------------------- /flake.lock: -------------------------------------------------------------------------------- 1 | { 2 | "nodes": { 3 | "fenix": { 4 | "inputs": { 5 | "nixpkgs": [ 6 | "nixpkgs" 7 | ], 8 | "rust-analyzer-src": "rust-analyzer-src" 9 | }, 10 | "locked": { 11 | "lastModified": 1745044353, 12 | "narHash": "sha256-OoGR5ppBq2nlbGDkCrg4slQ4gU5joYbc9GnQz4R6EOQ=", 13 | "owner": "nix-community", 14 | "repo": "fenix", 15 | "rev": "936f22bba519112ec47aa17a9b3304c8a3dabe54", 16 | "type": "github" 17 | }, 18 | "original": { 19 | "owner": "nix-community", 20 | "repo": "fenix", 21 | "type": "github" 22 | } 23 | }, 24 | "flake-compat": { 25 | "flake": false, 26 | "locked": { 27 | "lastModified": 1733328505, 28 | "narHash": "sha256-NeCCThCEP3eCl2l/+27kNNK7QrwZB1IJCrXfrbv5oqU=", 29 | "owner": "edolstra", 30 | "repo": "flake-compat", 31 | "rev": "ff81ac966bb2cae68946d5ed5fc4994f96d0ffec", 32 | "type": "github" 33 | }, 34 | "original": { 35 | "owner": "edolstra", 36 | "repo": "flake-compat", 37 | "type": "github" 38 | } 39 | }, 40 | "flake-compat_2": { 41 | "flake": false, 42 | "locked": { 43 | "lastModified": 1696426674, 44 | "narHash": "sha256-kvjfFW7WAETZlt09AgDn1MrtKzP7t90Vf7vypd3OL1U=", 45 | "owner": "edolstra", 46 | "repo": "flake-compat", 47 | "rev": "0f9255e01c2351cc7d116c072cb317785dd33b33", 48 | "type": "github" 49 | }, 50 | "original": { 51 | "owner": "edolstra", 52 | "repo": "flake-compat", 53 | "type": "github" 54 | } 55 | }, 56 | "flake-utils": { 57 | "inputs": { 58 | "systems": "systems" 59 | }, 60 | "locked": { 61 | "lastModified": 1731533236, 62 | "narHash": "sha256-l0KFg5HjrsfsO/JpG+r7fRrqm12kzFHyUHqHCVpMMbI=", 63 | "owner": "numtide", 64 | "repo": "flake-utils", 65 | "rev": "11707dc2f618dd54ca8739b309ec4fc024de578b", 66 | "type": "github" 67 | }, 68 | "original": { 69 | "owner": "numtide", 70 | "repo": "flake-utils", 71 | "type": "github" 72 | } 73 | }, 74 | "gitignore": { 75 | "inputs": { 76 | "nixpkgs": [ 77 | "nixpkgs" 78 | ] 79 | }, 80 | "locked": { 81 | "lastModified": 1709087332, 82 | "narHash": "sha256-HG2cCnktfHsKV0s4XW83gU3F57gaTljL9KNSuG6bnQs=", 83 | "owner": "hercules-ci", 84 | "repo": "gitignore.nix", 85 | "rev": "637db329424fd7e46cf4185293b9cc8c88c95394", 86 | "type": "github" 87 | }, 88 | "original": { 89 | "owner": "hercules-ci", 90 | "repo": "gitignore.nix", 91 | "type": "github" 92 | } 93 | }, 94 | "gitignore_2": { 95 | "inputs": { 96 | "nixpkgs": [ 97 | "pre-commit-hooks", 98 | "nixpkgs" 99 | ] 100 | }, 101 | "locked": { 102 | "lastModified": 1709087332, 103 | "narHash": "sha256-HG2cCnktfHsKV0s4XW83gU3F57gaTljL9KNSuG6bnQs=", 104 | "owner": "hercules-ci", 105 | "repo": "gitignore.nix", 106 | "rev": "637db329424fd7e46cf4185293b9cc8c88c95394", 107 | "type": "github" 108 | }, 109 | "original": { 110 | "owner": "hercules-ci", 111 | "repo": "gitignore.nix", 112 | "type": "github" 113 | } 114 | }, 115 | "naersk": { 116 | "inputs": { 117 | "nixpkgs": [ 118 | "nixpkgs" 119 | ] 120 | }, 121 | "locked": { 122 | "lastModified": 1743800763, 123 | "narHash": "sha256-YFKV+fxEpMgP5VsUcM6Il28lI0NlpM7+oB1XxbBAYCw=", 124 | "owner": "nmattia", 125 | "repo": "naersk", 126 | "rev": "ed0232117731a4c19d3ee93aa0c382a8fe754b01", 127 | "type": "github" 128 | }, 129 | "original": { 130 | "owner": "nmattia", 131 | "repo": "naersk", 132 | "type": "github" 133 | } 134 | }, 135 | "nixpkgs": { 136 | "locked": { 137 | "lastModified": 1745088452, 138 | "narHash": "sha256-6SrsUiwNtyQtl+JJNcNKe98iediwPrY9Kldhszqggto=", 139 | "owner": "NixOS", 140 | "repo": "nixpkgs", 141 | "rev": "ae0c0ebf757121ee44bb98e70a71212a9961055d", 142 | "type": "github" 143 | }, 144 | "original": { 145 | "owner": "NixOS", 146 | "ref": "nixos-unstable-small", 147 | "repo": "nixpkgs", 148 | "type": "github" 149 | } 150 | }, 151 | "pre-commit-hooks": { 152 | "inputs": { 153 | "flake-compat": "flake-compat_2", 154 | "gitignore": "gitignore_2", 155 | "nixpkgs": [ 156 | "nixpkgs" 157 | ] 158 | }, 159 | "locked": { 160 | "lastModified": 1742649964, 161 | "narHash": "sha256-DwOTp7nvfi8mRfuL1escHDXabVXFGT1VlPD1JHrtrco=", 162 | "owner": "cachix", 163 | "repo": "pre-commit-hooks.nix", 164 | "rev": "dcf5072734cb576d2b0c59b2ac44f5050b5eac82", 165 | "type": "github" 166 | }, 167 | "original": { 168 | "owner": "cachix", 169 | "repo": "pre-commit-hooks.nix", 170 | "type": "github" 171 | } 172 | }, 173 | "root": { 174 | "inputs": { 175 | "fenix": "fenix", 176 | "flake-compat": "flake-compat", 177 | "flake-utils": "flake-utils", 178 | "gitignore": "gitignore", 179 | "naersk": "naersk", 180 | "nixpkgs": "nixpkgs", 181 | "pre-commit-hooks": "pre-commit-hooks" 182 | } 183 | }, 184 | "rust-analyzer-src": { 185 | "flake": false, 186 | "locked": { 187 | "lastModified": 1745002274, 188 | "narHash": "sha256-W2Na1BK8Kq8eO8mlUmp+NGq7H5CPDpgzaGMpmyBMkas=", 189 | "owner": "rust-lang", 190 | "repo": "rust-analyzer", 191 | "rev": "a09a5502c3713e4287354b19973ea805d31ebcbc", 192 | "type": "github" 193 | }, 194 | "original": { 195 | "owner": "rust-lang", 196 | "ref": "nightly", 197 | "repo": "rust-analyzer", 198 | "type": "github" 199 | } 200 | }, 201 | "systems": { 202 | "locked": { 203 | "lastModified": 1681028828, 204 | "narHash": "sha256-Vy1rq5AaRuLzOxct8nz4T6wlgyUR7zLU309k9mBC768=", 205 | "owner": "nix-systems", 206 | "repo": "default", 207 | "rev": "da67096a3b9bf56a91d16901293e51ba5b49a27e", 208 | "type": "github" 209 | }, 210 | "original": { 211 | "owner": "nix-systems", 212 | "repo": "default", 213 | "type": "github" 214 | } 215 | } 216 | }, 217 | "root": "root", 218 | "version": 7 219 | } 220 | -------------------------------------------------------------------------------- /flake.nix: -------------------------------------------------------------------------------- 1 | { 2 | description = "A minesweeper game, written in Rust"; 3 | 4 | inputs = { 5 | flake-utils.url = "github:numtide/flake-utils"; 6 | 7 | nixpkgs.url = "github:NixOS/nixpkgs/nixos-unstable-small"; 8 | 9 | flake-compat = { 10 | url = "github:edolstra/flake-compat"; 11 | flake = false; 12 | }; 13 | 14 | pre-commit-hooks = { 15 | url = "github:cachix/pre-commit-hooks.nix"; 16 | inputs.nixpkgs.follows = "nixpkgs"; 17 | }; 18 | 19 | naersk = { 20 | url = "github:nmattia/naersk"; 21 | inputs.nixpkgs.follows = "nixpkgs"; 22 | }; 23 | 24 | fenix = { 25 | url = "github:nix-community/fenix"; 26 | inputs.nixpkgs.follows = "nixpkgs"; 27 | }; 28 | 29 | gitignore = { 30 | url = "github:hercules-ci/gitignore.nix"; 31 | inputs.nixpkgs.follows = "nixpkgs"; 32 | }; 33 | }; 34 | 35 | outputs = 36 | { self 37 | , fenix 38 | , flake-utils 39 | , gitignore 40 | , naersk 41 | , nixpkgs 42 | , pre-commit-hooks 43 | , ... 44 | }: 45 | flake-utils.lib.eachDefaultSystem (localSystem: 46 | let 47 | crossSystem = nixpkgs.lib.systems.examples.musl64 // { useLLVM = false; }; 48 | pkgs = import nixpkgs { 49 | inherit localSystem crossSystem; 50 | overlays = [ 51 | fenix.overlays.default 52 | gitignore.overlay 53 | naersk.overlay 54 | (final: prev: { 55 | rustToolchain = 56 | let 57 | fenixPackages = fenix.packages.${localSystem}; 58 | in 59 | final.fenix.combine [ 60 | fenixPackages.latest.clippy-preview 61 | fenixPackages.latest.rust-analysis 62 | fenixPackages.latest.rust-analyzer-preview 63 | fenixPackages.latest.rust-src 64 | fenixPackages.latest.rust-std 65 | fenixPackages.latest.rustfmt-preview 66 | fenixPackages.minimal.cargo 67 | fenixPackages.minimal.rustc 68 | final.fenix.targets.${crossSystem.config}.latest.rust-std 69 | ]; 70 | 71 | rustStdenv = final.pkgsBuildHost.llvmPackages_16.stdenv; 72 | rustLinker = final.pkgsBuildHost.llvmPackages_16.lld; 73 | 74 | naerskBuild = (prev.pkgsBuildHost.naersk.override { 75 | cargo = final.rustToolchain; 76 | rustc = final.rustToolchain; 77 | stdenv = final.rustStdenv; 78 | }).buildPackage; 79 | }) 80 | ]; 81 | }; 82 | inherit (pkgs.lib) mkForce; 83 | in 84 | rec { 85 | packages.minesweep = pkgs.naerskBuild { 86 | pname = "minesweep"; 87 | src = pkgs.gitignoreSource ./.; 88 | 89 | nativeBuildInputs = with pkgs; [ rustStdenv.cc rustLinker ]; 90 | 91 | CARGO_BUILD_TARGET = crossSystem.config; 92 | 93 | RUSTFLAGS = "-C linker-flavor=ld.lld -C target-feature=+crt-static"; 94 | }; 95 | 96 | packages.default = packages.minesweep; 97 | 98 | packages.minesweep-image = pkgs.pkgsBuildBuild.dockerTools.buildLayeredImage { 99 | name = "minesweep"; 100 | config = { 101 | Entrypoint = [ "${packages.minesweep}/bin/minesweep" ]; 102 | Command = [ "${packages.minesweep}/bin/minesweep" ]; 103 | }; 104 | }; 105 | 106 | apps.minesweep = flake-utils.lib.mkApp { drv = packages.minesweep; }; 107 | apps.default = apps.minesweep; 108 | 109 | checks = { 110 | pre-commit-check = pre-commit-hooks.lib.${localSystem}.run { 111 | src = ./.; 112 | hooks = { 113 | statix.enable = true; 114 | deadnix.enable = true; 115 | nixpkgs-fmt.enable = true; 116 | shellcheck.enable = true; 117 | 118 | shfmt = { 119 | enable = true; 120 | entry = mkForce "${pkgs.pkgsBuildBuild.shfmt}/bin/shfmt -i 2 -sr -d -s -l"; 121 | files = "\\.sh$"; 122 | }; 123 | 124 | rustfmt = { 125 | enable = true; 126 | entry = mkForce "${pkgs.pkgsBuildBuild.rustToolchain}/bin/cargo fmt -- --check --color=always"; 127 | }; 128 | 129 | clippy = { 130 | enable = true; 131 | entry = mkForce "${pkgs.pkgsBuildBuild.rustToolchain}/bin/cargo clippy -- -D warnings"; 132 | }; 133 | 134 | cargo-check = { 135 | enable = true; 136 | entry = mkForce "${pkgs.pkgsBuildBuild.rustToolchain}/bin/cargo check"; 137 | }; 138 | 139 | prettier = { 140 | enable = true; 141 | entry = mkForce "${pkgs.pkgsBuildBuild.nodePackages.prettier}/bin/prettier --check"; 142 | types_or = [ "json" "yaml" "markdown" ]; 143 | }; 144 | 145 | taplo = { 146 | enable = true; 147 | entry = mkForce "${pkgs.pkgsBuildBuild.taplo-cli}/bin/taplo fmt"; 148 | types = [ "toml" ]; 149 | }; 150 | }; 151 | }; 152 | }; 153 | 154 | devShells.default = pkgs.mkShell { 155 | inputsFrom = [ packages.minesweep ]; 156 | nativeBuildInputs = with pkgs.pkgsBuildBuild; [ 157 | cacert 158 | cargo-audit 159 | cargo-bloat 160 | cargo-edit 161 | cargo-udeps 162 | deadnix 163 | file 164 | git 165 | nixpkgs-fmt 166 | statix 167 | taplo-cli 168 | ]; 169 | 170 | inherit (self.checks.${localSystem}.pre-commit-check) shellHook; 171 | }; 172 | }); 173 | } 174 | -------------------------------------------------------------------------------- /rustfmt.toml: -------------------------------------------------------------------------------- 1 | edition = "2021" 2 | imports_granularity = "Crate" 3 | -------------------------------------------------------------------------------- /src/error.rs: -------------------------------------------------------------------------------- 1 | #[derive(Debug, thiserror::Error)] 2 | pub(crate) enum Error { 3 | #[error("failed to get tile at coordinate: {0:?}")] 4 | GetTile((usize, usize)), 5 | 6 | #[error("failed to draw to terminal")] 7 | DrawToTerminal(#[source] std::io::Error), 8 | 9 | #[error("failed to get input event")] 10 | GetEvent(#[source] std::sync::mpsc::RecvError), 11 | 12 | #[error("failed to get ctrlc handler")] 13 | SetHandler(#[source] ctrlc::Error), 14 | 15 | #[error("failed to get stdout in raw mode")] 16 | GetStdoutInRawMode(#[source] std::io::Error), 17 | 18 | #[error("failed to get alternate screen for mouse terminal")] 19 | GetAlternateScreenForMouseTerminal(#[source] std::io::Error), 20 | 21 | #[error("failed to create terminal object")] 22 | CreateTerminal(#[source] std::io::Error), 23 | 24 | #[error("failed to convert usize to u16")] 25 | ConvertUsizeToU16(#[source] std::num::TryFromIntError), 26 | } 27 | -------------------------------------------------------------------------------- /src/events.rs: -------------------------------------------------------------------------------- 1 | use std::{io, sync::mpsc, thread, time::Duration}; 2 | use termion::{event::Key, input::TermRead}; 3 | 4 | pub(crate) enum Event { 5 | Input(I), 6 | Tick, 7 | } 8 | 9 | /// A small event handler that wrap termion input and tick events. Each event 10 | /// type is handled in its own thread and returned to a common `Receiver` 11 | pub(crate) struct Events { 12 | rx: mpsc::Receiver>, 13 | _input_handle: thread::JoinHandle<()>, 14 | _tick_handle: thread::JoinHandle<()>, 15 | } 16 | 17 | #[derive(Debug, Clone, Copy)] 18 | pub(crate) struct Config { 19 | tick_rate: Duration, 20 | } 21 | 22 | impl Config { 23 | fn new() -> Self { 24 | Self { 25 | tick_rate: Duration::from_millis(250), 26 | } 27 | } 28 | } 29 | 30 | impl Events { 31 | pub(crate) fn new() -> Self { 32 | Self::with_config(Config::new()) 33 | } 34 | 35 | pub(crate) fn with_config(config: Config) -> Self { 36 | let (tx, rx) = mpsc::channel(); 37 | Self { 38 | rx, 39 | _input_handle: { 40 | let tx = tx.clone(); 41 | thread::spawn(move || { 42 | let stdin = io::stdin(); 43 | for key in stdin.keys().flatten() { 44 | if let Err(err) = tx.send(Event::Input(key)) { 45 | eprintln!("{err}"); 46 | return; 47 | } 48 | } 49 | }) 50 | }, 51 | _tick_handle: { 52 | thread::spawn(move || loop { 53 | if tx.send(Event::Tick).is_err() { 54 | break; 55 | } 56 | thread::sleep(config.tick_rate); 57 | }) 58 | }, 59 | } 60 | } 61 | 62 | pub(crate) fn next(&self) -> Result, mpsc::RecvError> { 63 | self.rx.recv() 64 | } 65 | } 66 | -------------------------------------------------------------------------------- /src/main.rs: -------------------------------------------------------------------------------- 1 | use anyhow::{Context, Result}; 2 | use structopt::StructOpt; 3 | 4 | mod error; 5 | mod events; 6 | mod sweep; 7 | mod ui; 8 | 9 | #[derive(Debug, structopt::StructOpt)] 10 | struct Opt { 11 | /// The number of rows in the grid. 12 | #[structopt(short, long, default_value = "9")] 13 | rows: usize, 14 | 15 | /// The number of columns in the grid. 16 | #[structopt(short, long, default_value = "9")] 17 | columns: usize, 18 | 19 | /// The total number of mines in the grid. The maximum number of mines 20 | /// is the product of the number of rows and the number of columns. 21 | #[structopt(short = "-n", long, default_value = "10")] 22 | mines: usize, 23 | 24 | /// The width of each cell. 25 | #[structopt(short = "-w", long, default_value = "5")] 26 | cell_width: usize, 27 | 28 | /// The height of each cell. 29 | #[structopt(short = "-H", long, default_value = "3")] 30 | cell_height: usize, 31 | } 32 | 33 | fn main() -> Result<()> { 34 | let Opt { 35 | rows, 36 | columns, 37 | mines, 38 | cell_width, 39 | cell_height, 40 | } = Opt::from_args(); 41 | 42 | ui::Ui::builder() 43 | .rows(rows) 44 | .columns(columns) 45 | .mines(mines.min(rows * columns)) 46 | .cell_width(cell_width) 47 | .cell_height(cell_height) 48 | .build() 49 | .run() 50 | .context("sweep failed") 51 | } 52 | -------------------------------------------------------------------------------- /src/sweep.rs: -------------------------------------------------------------------------------- 1 | use crate::error::Error; 2 | use bit_set::BitSet; 3 | use std::collections::VecDeque; 4 | 5 | pub(crate) type Coordinate = (usize, usize); 6 | 7 | #[derive(Debug)] 8 | pub(crate) struct Tile { 9 | adjacent_tiles: BitSet, 10 | pub(crate) mine: bool, 11 | pub(crate) exposed: bool, 12 | pub(crate) flagged: bool, 13 | pub(crate) adjacent_mines: u8, 14 | } 15 | 16 | #[derive(Debug, Copy, Clone, PartialEq)] 17 | enum Increment { 18 | One, 19 | NegOne, 20 | Zero, 21 | } 22 | 23 | impl Increment { 24 | fn offset(&self, value: usize) -> usize { 25 | match *self { 26 | Self::One => value + 1, 27 | Self::NegOne => value.saturating_sub(1), 28 | Self::Zero => value, 29 | } 30 | } 31 | } 32 | 33 | fn adjacent((row, column): Coordinate, rows: usize, columns: usize) -> impl Iterator { 34 | const INCREMENTS: [Increment; 3] = [Increment::One, Increment::NegOne, Increment::Zero]; 35 | 36 | INCREMENTS 37 | .iter() 38 | .copied() 39 | .flat_map(|row_incr| std::iter::repeat(row_incr).zip(INCREMENTS)) 40 | .filter_map(move |(row_incr, column_incr)| { 41 | let row_offset = row_incr.offset(row); 42 | let column_offset = column_incr.offset(column); 43 | 44 | match (row_incr, column_incr) { 45 | (Increment::Zero, Increment::Zero) => None, 46 | (_, _) if row_offset < rows && column_offset < columns => { 47 | Some(index_from_coord((row_offset, column_offset), columns)) 48 | } 49 | _ => None, 50 | } 51 | }) 52 | } 53 | 54 | pub(crate) struct Board { 55 | tiles: Vec, 56 | // number of rows on the board 57 | pub(crate) rows: usize, 58 | // number of columns on the board 59 | pub(crate) columns: usize, 60 | // the total number of mines 61 | mines: usize, 62 | flagged_cells: usize, 63 | // the total number of correctly flagged mines, allows checking a win in O(1) 64 | correctly_flagged_mines: usize, 65 | // the exposed tiles 66 | seen: BitSet, 67 | } 68 | 69 | fn index_from_coord((r, c): Coordinate, columns: usize) -> usize { 70 | r * columns + c 71 | } 72 | 73 | fn coord_from_index(index: usize, columns: usize) -> Coordinate { 74 | (index / columns, index % columns) 75 | } 76 | 77 | impl Board { 78 | pub(crate) fn new(rows: usize, columns: usize, mines: usize) -> Result { 79 | let mut rng = rand::thread_rng(); 80 | let samples = rand::seq::index::sample(&mut rng, rows * columns, mines) 81 | .into_iter() 82 | .collect::(); 83 | 84 | let tiles = (0..rows) 85 | .flat_map(|row| std::iter::repeat(row).zip(0..columns)) 86 | .enumerate() 87 | .map(|(i, point)| { 88 | // compute the tiles adjacent to the one being constructed 89 | let adjacent_tiles = adjacent(point, rows, columns).collect::(); 90 | 91 | // sum the number of adjacent tiles that are in the randomly generated mines set 92 | let adjacent_mines = adjacent_tiles 93 | .iter() 94 | .fold(0, |total, index| total + u8::from(samples.contains(index))); 95 | assert!(adjacent_mines <= 8); 96 | 97 | Tile { 98 | adjacent_tiles, 99 | mine: samples.contains(i), 100 | exposed: false, 101 | flagged: false, 102 | adjacent_mines, 103 | } 104 | }) 105 | .collect::>(); 106 | 107 | Ok(Self { 108 | rows, 109 | columns, 110 | tiles, 111 | mines, 112 | flagged_cells: Default::default(), 113 | correctly_flagged_mines: Default::default(), 114 | seen: Default::default(), 115 | }) 116 | } 117 | 118 | pub(crate) fn available_flags(&self) -> usize { 119 | assert!(self.flagged_cells <= self.mines); 120 | self.mines - self.flagged_cells 121 | } 122 | 123 | pub(crate) fn won(&self) -> bool { 124 | let nseen = self.seen.len(); 125 | let exposed_or_correctly_flagged = nseen + self.correctly_flagged_mines; 126 | let ntiles = self.rows * self.columns; 127 | 128 | assert!(exposed_or_correctly_flagged <= ntiles); 129 | 130 | ntiles == exposed_or_correctly_flagged || (self.tiles.len() - nseen) == self.mines 131 | } 132 | 133 | fn index_from_coord(&self, (r, c): Coordinate) -> usize { 134 | index_from_coord((r, c), self.columns) 135 | } 136 | 137 | pub(crate) fn expose(&mut self, (r, c): Coordinate) -> Result { 138 | if self.tile(r, c)?.mine { 139 | self.tile_mut(r, c)?.exposed = true; 140 | return Ok(true); 141 | } 142 | 143 | let mut coordinates = [(r, c)].iter().copied().collect::>(); 144 | 145 | let columns = self.columns; 146 | 147 | while let Some((r, c)) = coordinates.pop_front() { 148 | if self.seen.insert(self.index_from_coord((r, c))) { 149 | let tile = self.tile_mut(r, c)?; 150 | 151 | tile.exposed = !(tile.mine || tile.flagged); 152 | 153 | if tile.adjacent_mines == 0 { 154 | coordinates.extend( 155 | tile.adjacent_tiles 156 | .iter() 157 | .map(move |index| coord_from_index(index, columns)), 158 | ); 159 | } 160 | }; 161 | } 162 | 163 | Ok(false) 164 | } 165 | 166 | pub(crate) fn expose_all(&mut self) -> Result<(), Error> { 167 | let columns = self.columns; 168 | (0..self.tiles.len()) 169 | .map(move |i| coord_from_index(i, columns)) 170 | .try_for_each(|coord| { 171 | self.expose(coord)?; 172 | Ok(()) 173 | }) 174 | } 175 | 176 | pub(crate) fn tile(&self, i: usize, j: usize) -> Result<&Tile, Error> { 177 | self.tiles 178 | .get(self.index_from_coord((i, j))) 179 | .ok_or(Error::GetTile((i, j))) 180 | } 181 | 182 | pub(crate) fn tile_mut(&mut self, i: usize, j: usize) -> Result<&mut Tile, Error> { 183 | let index = self.index_from_coord((i, j)); 184 | self.tiles.get_mut(index).ok_or(Error::GetTile((i, j))) 185 | } 186 | 187 | pub(crate) fn flag_all(&mut self) { 188 | for tile in self.tiles.iter_mut() { 189 | tile.flagged = !tile.exposed && tile.mine; 190 | } 191 | } 192 | 193 | pub(crate) fn flag(&mut self, i: usize, j: usize) -> Result { 194 | let nflagged = self.flagged_cells; 195 | let tile = self.tile(i, j)?; 196 | let was_flagged = tile.flagged; 197 | let flagged = !was_flagged; 198 | let nmines = self.mines; 199 | self.correctly_flagged_mines += usize::from(flagged && tile.mine); 200 | if was_flagged { 201 | self.flagged_cells = self.flagged_cells.saturating_sub(1); 202 | self.tile_mut(i, j)?.flagged = flagged; 203 | } else if nflagged < nmines && !self.tile(i, j)?.exposed { 204 | self.tile_mut(i, j)?.flagged = flagged; 205 | self.flagged_cells += 1; 206 | } 207 | Ok(flagged) 208 | } 209 | } 210 | -------------------------------------------------------------------------------- /src/ui.rs: -------------------------------------------------------------------------------- 1 | use crate::{ 2 | error::Error, 3 | events::{Event, Events}, 4 | sweep::{Board, Coordinate}, 5 | }; 6 | use num_traits::ToPrimitive; 7 | use ratatui::{ 8 | backend::TermionBackend, 9 | layout::{Alignment, Constraint, Direction, Layout, Rect}, 10 | style::{Color, Modifier, Style}, 11 | text::Span, 12 | widgets::{Block, BorderType, Borders, Clear, Gauge, List, ListItem, Paragraph}, 13 | Terminal, 14 | }; 15 | use std::{ 16 | fmt, io, 17 | sync::{ 18 | atomic::{AtomicBool, Ordering}, 19 | Arc, 20 | }, 21 | }; 22 | use termion::{event::Key, input::MouseTerminal, raw::IntoRawMode, screen::IntoAlternateScreen}; 23 | 24 | fn centered_rect(width: u16, height: u16, r: Rect) -> Rect { 25 | let Rect { 26 | width: grid_width, 27 | height: grid_height, 28 | .. 29 | } = r; 30 | let popup_layout = Layout::default() 31 | .direction(Direction::Vertical) 32 | .constraints( 33 | [ 34 | Constraint::Length(grid_height / 2 - height / 2), 35 | Constraint::Length(height), 36 | Constraint::Length(grid_height / 2 - height / 2), 37 | ] 38 | .as_ref(), 39 | ) 40 | .split(r); 41 | 42 | Layout::default() 43 | .direction(Direction::Horizontal) 44 | .constraints( 45 | [ 46 | Constraint::Length(grid_width / 2 - width / 2), 47 | Constraint::Length(width), 48 | Constraint::Length(grid_width / 2 - width / 2), 49 | ] 50 | .as_ref(), 51 | ) 52 | .split(popup_layout[1])[1] 53 | } 54 | 55 | fn align_strings_to_char(strings: &[&str], c: char) -> Vec { 56 | let (firsts, rests): (Vec<_>, Vec<_>) = strings 57 | .iter() 58 | .map(|&s| s.split_at(s.find(c).unwrap())) 59 | .unzip(); 60 | let max_firsts = firsts.iter().map(|&f| f.len()).max().unwrap(); 61 | let max_rests = rests.iter().map(|&r| r.len()).max().unwrap(); 62 | firsts 63 | .into_iter() 64 | .zip(rests) 65 | .map(|(first, rest)| format!("{first:>max_firsts$}{rest: { 88 | app: &'app App, 89 | row: usize, 90 | column: usize, 91 | } 92 | 93 | impl<'app> Cell<'app> { 94 | fn new(app: &'app App, row: usize, column: usize) -> Self { 95 | Self { app, row, column } 96 | } 97 | 98 | fn is_active(&self) -> bool { 99 | self.app.active() == (self.row, self.column) 100 | } 101 | 102 | fn is_exposed(&self) -> bool { 103 | self.app.board.tile(self.row, self.column).unwrap().exposed 104 | } 105 | 106 | fn is_flagged(&self) -> bool { 107 | self.app.board.tile(self.row, self.column).unwrap().flagged 108 | } 109 | 110 | fn is_mine(&self) -> bool { 111 | self.app.board.tile(self.row, self.column).unwrap().mine 112 | } 113 | 114 | fn block(&self, lost: bool) -> Block { 115 | Block::default() 116 | .borders(Borders::ALL) 117 | .style( 118 | Style::default() 119 | .bg(Color::Black) 120 | .fg(if self.is_active() { 121 | Color::Cyan 122 | } else if lost && self.is_mine() { 123 | Color::LightRed 124 | } else { 125 | Color::White 126 | }) 127 | .add_modifier(if self.is_active() { 128 | Modifier::BOLD 129 | } else { 130 | Modifier::empty() 131 | }), 132 | ) 133 | .border_type(BorderType::Rounded) 134 | } 135 | 136 | fn text_style(&self) -> Style { 137 | Style::default() 138 | .fg(if self.is_exposed() && self.is_mine() { 139 | Color::LightYellow 140 | } else if self.is_exposed() { 141 | Color::White 142 | } else { 143 | Color::Black 144 | }) 145 | .bg(if self.is_exposed() { 146 | Color::Black 147 | } else if self.is_active() { 148 | Color::Cyan 149 | } else { 150 | Color::White 151 | }) 152 | } 153 | } 154 | 155 | impl fmt::Display for Cell<'_> { 156 | fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { 157 | write!( 158 | f, 159 | "{}", 160 | if self.is_flagged() { 161 | FLAG.to_owned() 162 | } else if self.is_mine() && self.is_exposed() { 163 | BOMB.to_owned() 164 | } else if self.is_exposed() { 165 | let num_adjacent_mines = self 166 | .app 167 | .board 168 | .tile(self.row, self.column) 169 | .unwrap() 170 | .adjacent_mines; 171 | if num_adjacent_mines == 0 { 172 | " ".to_owned() 173 | } else { 174 | format!("{num_adjacent_mines}") 175 | } 176 | } else { 177 | " ".to_owned() 178 | } 179 | ) 180 | } 181 | } 182 | 183 | impl App { 184 | fn new(board: Board) -> Self { 185 | Self { 186 | board, 187 | active_column: 0, 188 | active_row: 0, 189 | } 190 | } 191 | 192 | fn up(&mut self) { 193 | if let Some(active_row) = self.active_row.checked_sub(1) { 194 | self.active_row = active_row; 195 | } 196 | } 197 | 198 | fn down(&mut self) { 199 | self.active_row += usize::from(self.active_row < self.board.rows - 1); 200 | } 201 | 202 | fn left(&mut self) { 203 | if let Some(active_column) = self.active_column.checked_sub(1) { 204 | self.active_column = active_column; 205 | } 206 | } 207 | 208 | fn right(&mut self) { 209 | self.active_column += usize::from(self.active_column < self.board.columns - 1); 210 | } 211 | 212 | fn cell(&self, (r, c): Coordinate) -> Cell { 213 | Cell::new(self, r, c) 214 | } 215 | 216 | fn active_cell(&self) -> Cell { 217 | self.cell(self.active()) 218 | } 219 | 220 | fn active(&self) -> Coordinate { 221 | (self.active_row, self.active_column) 222 | } 223 | 224 | fn expose_active_cell(&mut self) -> Result { 225 | self.board.expose(self.active()) 226 | } 227 | 228 | fn expose_all(&mut self) -> Result<(), Error> { 229 | self.board.expose_all() 230 | } 231 | 232 | fn won(&self) -> bool { 233 | self.board.won() 234 | } 235 | 236 | fn flag_active_cell(&mut self) -> Result<(), Error> { 237 | let (r, c) = self.active(); 238 | self.board.flag(r, c)?; 239 | Ok(()) 240 | } 241 | 242 | pub(crate) fn flag_all(&mut self) { 243 | self.board.flag_all() 244 | } 245 | } 246 | 247 | impl Ui { 248 | pub(crate) fn run(&mut self) -> Result<(), Error> { 249 | let events = Events::new(); 250 | let rows = self.rows; 251 | let columns = self.columns; 252 | let mines = self.mines; 253 | 254 | let running = Arc::new(AtomicBool::new(true)); 255 | let running_clone = running.clone(); 256 | 257 | ctrlc::set_handler(move || { 258 | running_clone.store(false, Ordering::SeqCst); 259 | }) 260 | .map_err(Error::SetHandler)?; 261 | 262 | let cell_width = self.cell_width; 263 | let cell_height = self.cell_height; 264 | 265 | let padding = 1; 266 | 267 | let grid_width = 268 | u16::try_from(cell_width * columns + 2 * padding).map_err(Error::ConvertUsizeToU16)?; 269 | let grid_height = 270 | u16::try_from(cell_height * rows + 2 * padding).map_err(Error::ConvertUsizeToU16)?; 271 | 272 | let row_constraints = std::iter::repeat(Constraint::Length( 273 | u16::try_from(cell_height).map_err(Error::ConvertUsizeToU16)?, 274 | )) 275 | .take(rows) 276 | .collect::>(); 277 | 278 | let col_constraints = std::iter::repeat(Constraint::Length( 279 | u16::try_from(cell_width).map_err(Error::ConvertUsizeToU16)?, 280 | )) 281 | .take(columns) 282 | .collect::>(); 283 | 284 | let mut app = App::new(Board::new(rows, columns, mines)?); 285 | let mut lost = false; 286 | 287 | let stdout = io::stdout() 288 | .into_raw_mode() 289 | .map_err(Error::GetStdoutInRawMode)? 290 | .into_alternate_screen() 291 | .map_err(Error::GetAlternateScreenForMouseTerminal)?; 292 | let mouse_terminal = MouseTerminal::from(stdout); 293 | let backend = TermionBackend::new(mouse_terminal); 294 | let mut terminal = Terminal::new(backend).map_err(Error::CreateTerminal)?; 295 | 296 | while running.load(Ordering::SeqCst) { 297 | terminal 298 | .draw(|frame| { 299 | let terminal_rect = frame.size(); 300 | 301 | let outer_block = Block::default() 302 | .borders(Borders::ALL) 303 | .title(Span::styled( 304 | "Minesweeper", 305 | Style::default() 306 | .fg(Color::LightYellow) 307 | .add_modifier(Modifier::BOLD), 308 | )) 309 | .border_type(BorderType::Rounded); 310 | frame.render_widget(outer_block, terminal_rect); 311 | 312 | let outer_rects = Layout::default() 313 | .direction(Direction::Vertical) 314 | .vertical_margin(1) 315 | .horizontal_margin(1) 316 | .constraints(vec![Constraint::Min(grid_height)]) 317 | .split(terminal_rect); 318 | 319 | let mines_rect = outer_rects[0]; 320 | 321 | let available_flags = app.board.available_flags(); 322 | let info_text = Gauge::default() 323 | .block( 324 | Block::default().borders(Borders::ALL).title(Span::styled( 325 | FLAG, 326 | Style::default() 327 | .fg(Color::LightMagenta) 328 | .add_modifier(Modifier::BOLD), 329 | )), 330 | ) 331 | .gauge_style( 332 | Style::default() 333 | .fg(Color::White) 334 | .bg(Color::Black) 335 | .add_modifier(Modifier::BOLD), 336 | ) 337 | .label(format!( 338 | "{:>length$}", 339 | available_flags, 340 | length = available_flags 341 | .to_f64() 342 | .unwrap() 343 | .log10() 344 | .ceil() 345 | .to_usize() 346 | .unwrap_or(0) 347 | + 1 348 | )) 349 | .ratio(available_flags.to_f64().unwrap() / mines.to_f64().unwrap()); 350 | 351 | let horizontal_pad_block_width = terminal_rect 352 | .width 353 | .checked_sub(grid_width) 354 | .unwrap_or(terminal_rect.width) 355 | / 2; 356 | let mines_rects = Layout::default() 357 | .direction(Direction::Horizontal) 358 | .constraints(vec![ 359 | Constraint::Min(horizontal_pad_block_width), 360 | Constraint::Length(grid_width), 361 | // unclear why the right side padding is much smaller than the rest 362 | // 363 | // I suspect it's a consequence of the layout algorithm 364 | // 365 | // I subtract one to give the right side a tad more space 366 | Constraint::Min(horizontal_pad_block_width.saturating_sub(1)), 367 | ]) 368 | .split(mines_rect); 369 | 370 | let vertical_pad_block_height = mines_rect 371 | .height 372 | .checked_sub(grid_height) 373 | .unwrap_or(mines_rect.height) 374 | / 2; 375 | let middle_mines_rects = Layout::default() 376 | .direction(Direction::Vertical) 377 | .constraints(vec![ 378 | Constraint::Min(vertical_pad_block_height), 379 | Constraint::Length(grid_height), 380 | Constraint::Min(vertical_pad_block_height), 381 | ]) 382 | .split(mines_rects[1]); 383 | 384 | let help_text_block = List::new( 385 | align_strings_to_char( 386 | &[ 387 | "movement: hjkl / ← ↓ ↑ →", 388 | "expose tile: spacebar", 389 | "flag tile: f", 390 | "quit: q", 391 | ], 392 | ':', 393 | ) 394 | .into_iter() 395 | .map(|line| format!("{:^width$}", line, width = usize::from(grid_width))) 396 | .map(ListItem::new) 397 | .collect::>(), 398 | ) 399 | .block(Block::default().borders(Borders::NONE)); 400 | frame.render_widget(help_text_block, middle_mines_rects[2]); 401 | 402 | let info_text_split_rects = Layout::default() 403 | .direction(Direction::Vertical) 404 | .constraints(vec![ 405 | Constraint::Min(vertical_pad_block_height - 3), 406 | Constraint::Length(3), 407 | ]) 408 | .split(middle_mines_rects[0]); 409 | 410 | let info_mines_rects = Layout::default() 411 | .direction(Direction::Horizontal) 412 | .constraints(vec![Constraint::Percentage(50), Constraint::Percentage(50)]) 413 | .split(info_text_split_rects[1]); 414 | frame.render_widget(info_text, info_mines_rects[0]); 415 | 416 | let mines_text = Paragraph::new(mines.to_string()) 417 | .block( 418 | Block::default().borders(Borders::ALL).title(Span::styled( 419 | BOMB, 420 | Style::default() 421 | .fg(Color::LightYellow) 422 | .add_modifier(Modifier::BOLD), 423 | )), 424 | ) 425 | .alignment(Alignment::Center); 426 | frame.render_widget(mines_text, info_mines_rects[1]); 427 | 428 | let mines_block = Block::default() 429 | .borders(Borders::ALL) 430 | .border_type(BorderType::Rounded); 431 | 432 | let final_mines_rect = middle_mines_rects[1]; 433 | frame.render_widget(mines_block, final_mines_rect); 434 | 435 | let row_rects = Layout::default() 436 | .direction(Direction::Vertical) 437 | .vertical_margin(1) 438 | .horizontal_margin(0) 439 | .constraints(row_constraints.clone()) 440 | .split(final_mines_rect); 441 | 442 | for (r, row_rect) in row_rects.iter().enumerate() { 443 | let col_rects = Layout::default() 444 | .direction(Direction::Horizontal) 445 | .vertical_margin(0) 446 | .horizontal_margin(1) 447 | .constraints(col_constraints.clone()) 448 | .split(*row_rect); 449 | 450 | for (c, cell_rect) in col_rects.iter().enumerate() { 451 | let cell = app.cell((r, c)); 452 | let single_row_text = 453 | format!("{:^length$}", cell.to_string(), length = cell_width - 2); 454 | let pad_line = " ".repeat(cell_width); 455 | 456 | // 1 line for the text, 1 line each for the top and bottom of the cell == 3 lines 457 | // that are not eligible for padding 458 | let num_pad_lines = cell_height - 3; 459 | 460 | // text is: 461 | // pad with half the pad lines budget 462 | // the interesting text 463 | // pad with half the pad lines budget 464 | // join with newlines 465 | let text = std::iter::repeat(pad_line.clone()) 466 | .take(num_pad_lines / 2) 467 | .chain(std::iter::once(single_row_text)) 468 | .chain(std::iter::repeat(pad_line).take(num_pad_lines / 2)) 469 | .collect::>() 470 | .join("\n"); 471 | 472 | let cell_text = Paragraph::new(text) 473 | .block(cell.block(lost)) 474 | .style(cell.text_style()); 475 | frame.render_widget(cell_text, *cell_rect); 476 | } 477 | } 478 | 479 | // if the user has lost or won, display a banner indicating so 480 | if lost || app.won() { 481 | app.flag_all(); 482 | let area = centered_rect(20, 3, final_mines_rect); 483 | frame.render_widget(Clear, area); // this clears out the background 484 | frame.render_widget( 485 | Paragraph::new(format!("You {}!", if lost { "lose" } else { "won" })) 486 | .block( 487 | Block::default() 488 | .borders(Borders::ALL) 489 | .border_type(BorderType::Thick) 490 | .border_style( 491 | Style::default() 492 | .fg(if lost { 493 | Color::Magenta 494 | } else { 495 | Color::LightGreen 496 | }) 497 | .add_modifier(Modifier::BOLD), 498 | ) 499 | .style(Style::default().add_modifier(Modifier::BOLD)), 500 | ) 501 | .alignment(Alignment::Center) 502 | .style(Style::default()), 503 | area, 504 | ); 505 | } 506 | }) 507 | .map_err(Error::DrawToTerminal)?; 508 | 509 | if let Event::Input(key) = events.next().map_err(Error::GetEvent)? { 510 | match key { 511 | // movement using arrow keys or vim movement keys 512 | Key::Up | Key::Char('k') => app.up(), 513 | Key::Down | Key::Char('j') => app.down(), 514 | Key::Left | Key::Char('h') => app.left(), 515 | Key::Right | Key::Char('l') => app.right(), 516 | Key::Char('f') if !lost && !app.won() => app.flag_active_cell()?, 517 | Key::Char(' ') if !lost && !app.won() && !app.active_cell().is_flagged() => { 518 | lost = app.expose_active_cell()?; 519 | if lost { 520 | app.expose_all()?; 521 | } 522 | } 523 | Key::Char('q') => break, 524 | _ => {} 525 | } 526 | } 527 | } 528 | 529 | Ok(()) 530 | } 531 | } 532 | --------------------------------------------------------------------------------