chore: import upstream snapshot with attribution
CI / Coverage (push) Waiting to run
CI / Format (push) Waiting to run
CI / Clippy (push) Waiting to run
CI / Test (push) Waiting to run
CI / Nix Build (push) Waiting to run

This commit is contained in:
wehub-resource-sync
2026-07-13 13:16:47 +08:00
commit 8c89588461
1009 changed files with 148056 additions and 0 deletions
+11
View File
@@ -0,0 +1,11 @@
version: 2
updates:
- package-ecosystem: "cargo"
directory: "/"
schedule:
interval: "daily"
open-pull-requests-limit: 25
reviewers:
- "unhappychoice"
assignees:
- "unhappychoice"
+74
View File
@@ -0,0 +1,74 @@
name: CI
on:
push:
branches: [ main ]
pull_request:
branches: [ main ]
env:
CARGO_TERM_COLOR: always
jobs:
format:
name: Format
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- uses: dtolnay/rust-toolchain@stable
with:
components: rustfmt
- uses: Swatinem/rust-cache@v2
- name: Check formatting
run: cargo fmt --all -- --check
clippy:
name: Clippy
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- uses: dtolnay/rust-toolchain@stable
with:
components: clippy
- uses: Swatinem/rust-cache@v2
- name: Run clippy
run: cargo clippy --all-targets --all-features -- -D warnings
test:
name: Test
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- uses: dtolnay/rust-toolchain@stable
- uses: Swatinem/rust-cache@v2
- name: Run tests
run: cargo test
coverage:
name: Coverage
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- uses: dtolnay/rust-toolchain@stable
with:
components: llvm-tools-preview
- uses: Swatinem/rust-cache@v2
- name: Install cargo-llvm-cov
uses: taiki-e/install-action@cargo-llvm-cov
- name: Generate coverage report
run: cargo llvm-cov --all-features --workspace --lcov --output-path lcov.info
- name: Upload coverage to Codecov
uses: codecov/codecov-action@v3
with:
token: ${{ secrets.CODECOV_TOKEN }}
files: lcov.info
fail_ci_if_error: false
nix-build:
name: Nix Build
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- uses: cachix/install-nix-action@v27
- name: Build unstable package
run: nix build .#unstable
+345
View File
@@ -0,0 +1,345 @@
name: Manual Release
on:
workflow_dispatch:
inputs:
version_type:
description: 'Version bump type'
required: true
default: 'patch'
type: choice
options:
- major
- minor
- patch
env:
CARGO_TERM_COLOR: always
permissions:
contents: write
jobs:
release:
name: Create Release
runs-on: ubuntu-latest
outputs:
new_version: ${{ steps.version.outputs.new_version }}
new_version_no_v: ${{ steps.version.outputs.new_version_no_v }}
upload_url: ${{ steps.create_release.outputs.upload_url }}
tag_sha: ${{ steps.tag_sha.outputs.tag_sha }}
steps:
- uses: actions/checkout@v4
with:
token: ${{ secrets.GITHUB_TOKEN }}
fetch-depth: 0
- uses: dtolnay/rust-toolchain@stable
- uses: Swatinem/rust-cache@v2
# Install cargo-edit for version bumping
- name: Install cargo-edit
run: cargo install cargo-edit
# Bump version in Cargo.toml
- name: Bump version
id: version
run: |
cargo set-version --bump ${{ github.event.inputs.version_type }}
NEW_VERSION=$(cargo metadata --no-deps --format-version 1 | jq -r '.packages[0].version')
echo "new_version=v$NEW_VERSION" >> $GITHUB_OUTPUT
echo "new_version_no_v=$NEW_VERSION" >> $GITHUB_OUTPUT
echo "New version: v$NEW_VERSION"
# Run tests before release
- name: Run tests
run: cargo test
# Install Nix for flake.nix updates
- name: Install Nix
uses: cachix/install-nix-action@v27
# Update flake.nix with new version and hashes
- name: Update flake.nix
run: |
NEW_VERSION=$(echo "${{ steps.version.outputs.new_version }}" | sed 's/^v//')
# Update version in flake.nix
sed -i "s/version = \"[0-9.]\+\"/version = \"$NEW_VERSION\"/" flake.nix
# Calculate hash for source
nix flake prefetch --json github:unhappychoice/gittype/${{ steps.version.outputs.new_version }} > /tmp/prefetch.json || true
# Note: Hashes will be calculated after tag is pushed, so we'll use a placeholder for now
# The actual hash update will happen in a separate job after the tag is created
echo "Version updated in flake.nix to $NEW_VERSION"
# Create temporary tag for changelog generation
- name: Create temporary tag
run: |
git config --local user.email "action@github.com"
git config --local user.name "GitHub Action"
git add Cargo.toml Cargo.lock flake.nix
git commit -m "chore: bump version to ${{ steps.version.outputs.new_version }}"
git tag ${{ steps.version.outputs.new_version }}
# Generate CHANGELOG.md (now with the new tag available)
- name: Generate Changelog
run: |
chmod +x generate_changelog.sh
./generate_changelog.sh
# Commit changelog and push everything
- name: Commit changelog and push
run: |
git add CHANGELOG.md
git commit --amend --no-edit
git tag -f ${{ steps.version.outputs.new_version }}
git push origin main
git push origin ${{ steps.version.outputs.new_version }}
# Resolve the exact commit SHA for the newly created tag (used by Homebrew)
- name: Resolve tag commit SHA
id: tag_sha
run: |
TAG_SHA=$(git rev-list -n 1 ${{ steps.version.outputs.new_version }})
echo "tag_sha=$TAG_SHA" >> $GITHUB_OUTPUT
echo "Tag commit SHA: $TAG_SHA"
# Extract release notes from CHANGELOG.md
- name: Extract Release Notes
id: extract_notes
run: |
VERSION=$(echo "${{ steps.version.outputs.new_version }}" | sed 's/^v//')
# Extract the section for the current version from CHANGELOG.md
awk "/## \[$VERSION\]/{flag=1; next} /## \[/{flag=0} flag" CHANGELOG.md > release_notes.md
if [ ! -s release_notes.md ]; then
echo "## What's Changed" > release_notes.md
echo "" >> release_notes.md
echo "See the full changelog for details." >> release_notes.md
fi
# Store the release notes for use in the next step
echo "RELEASE_NOTES<<EOF" >> $GITHUB_OUTPUT
cat release_notes.md >> $GITHUB_OUTPUT
echo "EOF" >> $GITHUB_OUTPUT
# Create GitHub Release
- name: Create Release
id: create_release
uses: actions/create-release@v1
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
with:
tag_name: ${{ steps.version.outputs.new_version }}
release_name: Release ${{ steps.version.outputs.new_version }}
body: ${{ steps.extract_notes.outputs.RELEASE_NOTES }}
draft: false
prerelease: false
build-and-upload:
name: Build and Upload
needs: release
strategy:
matrix:
include:
- target: x86_64-unknown-linux-gnu
os: ubuntu-latest
- target: aarch64-unknown-linux-gnu
os: ubuntu-latest
- target: x86_64-pc-windows-msvc
os: windows-latest
- target: x86_64-apple-darwin
os: macos-latest
- target: aarch64-apple-darwin
os: macos-latest
runs-on: ${{ matrix.os }}
steps:
- uses: actions/checkout@v4
with:
ref: ${{ needs.release.outputs.new_version }}
- uses: dtolnay/rust-toolchain@stable
with:
targets: ${{ matrix.target }}
- uses: Swatinem/rust-cache@v2
with:
key: ${{ matrix.target }}
- name: Install cross-compilation tools
if: matrix.target == 'aarch64-unknown-linux-gnu'
run: |
sudo apt-get update
sudo apt-get install -y gcc-aarch64-linux-gnu
- name: Build
run: cargo build --release --target ${{ matrix.target }}
env:
OPENSSL_VENDORED: 1
CC_aarch64_unknown_linux_gnu: aarch64-linux-gnu-gcc
AR_aarch64_unknown_linux_gnu: aarch64-linux-gnu-ar
CARGO_TARGET_AARCH64_UNKNOWN_LINUX_GNU_LINKER: aarch64-linux-gnu-gcc
- name: Create archive (Unix)
if: matrix.os != 'windows-latest'
run: |
cd target/${{ matrix.target }}/release
tar -czf ../../../gittype-${{ needs.release.outputs.new_version }}-${{ matrix.target }}.tar.gz gittype
- name: Create archive (Windows)
if: matrix.os == 'windows-latest'
run: |
cd target/${{ matrix.target }}/release
Compress-Archive -Path gittype.exe -DestinationPath ../../../gittype-${{ needs.release.outputs.new_version }}-${{ matrix.target }}.zip
- name: Upload Release Asset (Unix)
if: matrix.os != 'windows-latest'
uses: actions/upload-release-asset@v1
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
with:
upload_url: ${{ needs.release.outputs.upload_url }}
asset_path: ./gittype-${{ needs.release.outputs.new_version }}-${{ matrix.target }}.tar.gz
asset_name: gittype-${{ needs.release.outputs.new_version }}-${{ matrix.target }}.tar.gz
asset_content_type: application/gzip
- name: Upload Release Asset (Windows)
if: matrix.os == 'windows-latest'
uses: actions/upload-release-asset@v1
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
with:
upload_url: ${{ needs.release.outputs.upload_url }}
asset_path: ./gittype-${{ needs.release.outputs.new_version }}-${{ matrix.target }}.zip
asset_name: gittype-${{ needs.release.outputs.new_version }}-${{ matrix.target }}.zip
asset_content_type: application/zip
publish-crates:
name: Publish to crates.io
runs-on: ubuntu-latest
needs: [release, build-and-upload]
steps:
- uses: actions/checkout@v4
with:
ref: ${{ needs.release.outputs.new_version }}
- uses: dtolnay/rust-toolchain@stable
- uses: Swatinem/rust-cache@v2
- name: Publish to crates.io
run: cargo publish --token ${{ secrets.CARGO_REGISTRY_TOKEN }}
update-flake-hashes:
name: Update flake.nix Hashes
runs-on: ubuntu-latest
needs: release
steps:
- uses: actions/checkout@v4
with:
ref: main
token: ${{ secrets.GITHUB_TOKEN }}
fetch-depth: 0
- uses: cachix/install-nix-action@v27
- name: Calculate and update hashes
run: |
VERSION=${{ needs.release.outputs.new_version }}
# Ensure we have the latest main
git pull origin main
# Calculate source hash
SRC_HASH=$(nix flake prefetch --json github:unhappychoice/gittype/$VERSION 2>/dev/null | jq -r .hash)
echo "Source hash: $SRC_HASH"
# Update source hash in flake.nix
sed -i "s|hash = \"sha256-[A-Za-z0-9+/=]\+\"|hash = \"$SRC_HASH\"|" flake.nix
# Try to build to get cargoHash (it will fail but show the correct hash)
nix build .#default 2>&1 | tee /tmp/build.log || true
CARGO_HASH=$(grep -oP "got:\s+\Ksha256-[A-Za-z0-9+/=]+" /tmp/build.log | head -1 || echo "")
if [ -n "$CARGO_HASH" ]; then
echo "Cargo hash: $CARGO_HASH"
sed -i "s|cargoHash = \"sha256-[A-Za-z0-9+/=]\+\"|cargoHash = \"$CARGO_HASH\"|" flake.nix
else
echo "Warning: Could not determine cargo hash"
fi
# Commit and push the updated flake.nix
git config --local user.email "action@github.com"
git config --local user.name "GitHub Action"
git add flake.nix
git commit -m "chore: update flake.nix hashes for $VERSION" || echo "No changes to commit"
git push origin main
update-homebrew:
name: Update Homebrew Formula
runs-on: ubuntu-latest
needs: [release, build-and-upload]
steps:
- name: Checkout homebrew tap
uses: actions/checkout@v4
with:
repository: unhappychoice/homebrew-tap
token: ${{ secrets.HOMEBREW_TAP_TOKEN }}
path: homebrew-tap
- name: Update Homebrew formula
run: |
VERSION=${{ needs.release.outputs.new_version }}
# Calculate SHA256 for each binary release
MACOS_INTEL_URL="https://github.com/unhappychoice/gittype/releases/download/$VERSION/gittype-$VERSION-x86_64-apple-darwin.tar.gz"
MACOS_INTEL_SHA=$(curl -sL "$MACOS_INTEL_URL" | sha256sum | cut -d' ' -f1)
MACOS_ARM_URL="https://github.com/unhappychoice/gittype/releases/download/$VERSION/gittype-$VERSION-aarch64-apple-darwin.tar.gz"
MACOS_ARM_SHA=$(curl -sL "$MACOS_ARM_URL" | sha256sum | cut -d' ' -f1)
LINUX_INTEL_URL="https://github.com/unhappychoice/gittype/releases/download/$VERSION/gittype-$VERSION-x86_64-unknown-linux-gnu.tar.gz"
LINUX_INTEL_SHA=$(curl -sL "$LINUX_INTEL_URL" | sha256sum | cut -d' ' -f1)
LINUX_ARM_URL="https://github.com/unhappychoice/gittype/releases/download/$VERSION/gittype-$VERSION-aarch64-unknown-linux-gnu.tar.gz"
LINUX_ARM_SHA=$(curl -sL "$LINUX_ARM_URL" | sha256sum | cut -d' ' -f1)
echo "Calculated SHAs:"
echo "macOS Intel: $MACOS_INTEL_SHA"
echo "macOS ARM: $MACOS_ARM_SHA"
echo "Linux Intel: $LINUX_INTEL_SHA"
echo "Linux ARM: $LINUX_ARM_SHA"
cd homebrew-tap
# Use sed to update each architecture's URL and SHA
sed -i "s|https://github.com/unhappychoice/gittype/releases/download/v[0-9.]\+/gittype-v[0-9.]\+-x86_64-apple-darwin.tar.gz|$MACOS_INTEL_URL|g" Formula/gittype.rb
sed -i "s|https://github.com/unhappychoice/gittype/releases/download/v[0-9.]\+/gittype-v[0-9.]\+-aarch64-apple-darwin.tar.gz|$MACOS_ARM_URL|g" Formula/gittype.rb
sed -i "s|https://github.com/unhappychoice/gittype/releases/download/v[0-9.]\+/gittype-v[0-9.]\+-x86_64-unknown-linux-gnu.tar.gz|$LINUX_INTEL_URL|g" Formula/gittype.rb
sed -i "s|https://github.com/unhappychoice/gittype/releases/download/v[0-9.]\+/gittype-v[0-9.]\+-aarch64-unknown-linux-gnu.tar.gz|$LINUX_ARM_URL|g" Formula/gittype.rb
# Update SHA values in the same order they appear in the file
# macOS Intel
FIRST_SHA_LINE=$(grep -n 'sha256 "' Formula/gittype.rb | head -1 | cut -d: -f1)
sed -i "${FIRST_SHA_LINE}s/sha256 \"[a-f0-9]\+\"/sha256 \"$MACOS_INTEL_SHA\"/" Formula/gittype.rb
# macOS ARM
SECOND_SHA_LINE=$(grep -n 'sha256 "' Formula/gittype.rb | head -2 | tail -1 | cut -d: -f1)
sed -i "${SECOND_SHA_LINE}s/sha256 \"[a-f0-9]\+\"/sha256 \"$MACOS_ARM_SHA\"/" Formula/gittype.rb
# Linux Intel
THIRD_SHA_LINE=$(grep -n 'sha256 "' Formula/gittype.rb | head -3 | tail -1 | cut -d: -f1)
sed -i "${THIRD_SHA_LINE}s/sha256 \"[a-f0-9]\+\"/sha256 \"$LINUX_INTEL_SHA\"/" Formula/gittype.rb
# Linux ARM
FOURTH_SHA_LINE=$(grep -n 'sha256 "' Formula/gittype.rb | head -4 | tail -1 | cut -d: -f1)
sed -i "${FOURTH_SHA_LINE}s/sha256 \"[a-f0-9]\+\"/sha256 \"$LINUX_ARM_SHA\"/" Formula/gittype.rb
git config --local user.email "action@github.com"
git config --local user.name "GitHub Action"
git add Formula/gittype.rb
git commit -m "chore: update gittype to $VERSION"
git push
update-winget:
name: Update WinGet Package
runs-on: ubuntu-latest
needs: [release, build-and-upload]
steps:
- name: Submit manifest to winget-pkgs
uses: vedantmgoyal9/winget-releaser@v2
with:
identifier: unhappychoice.gittype
version: ${{ needs.release.outputs.new_version_no_v }}
release-tag: ${{ needs.release.outputs.new_version }}
installers-regex: 'x86_64-pc-windows-msvc\.zip$'
token: ${{ secrets.WINGET_TOKEN }}
+46
View File
@@ -0,0 +1,46 @@
# Rust
/target/
Cargo.lock
# IDE
.vscode/
.idea/
*.swp
*.swo
*~
# OS
.DS_Store
.DS_Store?
._*
.Spotlight-V100
.Trashes
ehthumbs.db
Thumbs.db
# Development files (debug builds only)
/logs/
gittype-dev.db
.config/
# Logs
*.log
# Database
*.db
*.sqlite
*.sqlite3
# Backup files
*.bak
*.tmp
# Coverage reports
lcov.info
cobertura.xml
*.profdata
*.profraw
# Claude Code
.claude/
.serena/
@@ -0,0 +1,18 @@
# yaml-language-server: $schema=https://aka.ms/winget-manifest.installer.1.6.0.schema.json
PackageIdentifier: unhappychoice.gittype
PackageVersion: 0.10.0
InstallerType: zip
NestedInstallerType: portable
NestedInstallerFiles:
- RelativeFilePath: gittype.exe
PortableCommandAlias: gittype
Commands:
- gittype
ReleaseDate: 2026-04-20
Installers:
- Architecture: x64
InstallerUrl: https://github.com/unhappychoice/gittype/releases/download/v0.10.0/gittype-v0.10.0-x86_64-pc-windows-msvc.zip
InstallerSha256: 62E625B18AD860235BF9E1A618525FB20B7E0BFF5614D142E2803629D163A8B8
ManifestType: installer
ManifestVersion: 1.6.0
@@ -0,0 +1,34 @@
# yaml-language-server: $schema=https://aka.ms/winget-manifest.defaultLocale.1.6.0.schema.json
PackageIdentifier: unhappychoice.gittype
PackageVersion: 0.10.0
PackageLocale: en-US
Publisher: unhappychoice
PublisherUrl: https://github.com/unhappychoice
PublisherSupportUrl: https://github.com/unhappychoice/gittype/issues
PackageName: GitType
PackageUrl: https://github.com/unhappychoice/gittype
License: MIT
LicenseUrl: https://github.com/unhappychoice/gittype/blob/main/LICENSE
Copyright: Copyright (c) unhappychoice
ShortDescription: A typing practice tool using your own code repositories
Description: |-
GitType is a Rust CLI typing game that turns source code from real repositories
into typing challenges. It parses code with tree-sitter, renders a terminal UI,
tracks real-time WPM/accuracy/consistency metrics, and unlocks developer titles
through a ranking system. Multi-language support includes Rust, TypeScript,
JavaScript, Python, Go, Ruby, Swift, Kotlin, Java, PHP, C#, C, C++, Haskell,
Dart, Scala, Clojure, Elixir, Erlang, and Zig.
Moniker: gittype
Tags:
- cli
- code
- developer
- git
- practice
- terminal
- tui
- typing
ReleaseNotesUrl: https://github.com/unhappychoice/gittype/releases/tag/v0.10.0
ManifestType: defaultLocale
ManifestVersion: 1.6.0
@@ -0,0 +1,7 @@
# yaml-language-server: $schema=https://aka.ms/winget-manifest.version.1.6.0.schema.json
PackageIdentifier: unhappychoice.gittype
PackageVersion: 0.10.0
DefaultLocale: en-US
ManifestType: version
ManifestVersion: 1.6.0
+163
View File
@@ -0,0 +1,163 @@
# Agent Guidelines
Conventions for agents (Claude Code, Codex CLI, etc.) contributing to this repository. Humans should also follow them. For deeper context see `docs/ARCHITECTURE.md` and `docs/CONTRIBUTING.md`.
## Project Overview
`gittype` is a Rust CLI typing game that turns source code from real repositories into typing challenges. It parses code with `tree-sitter`, renders a terminal UI with `ratatui` / `crossterm`, persists session history in SQLite, and is wired together via the `shaku` DI container.
- Entry points: `src/main.rs` (binary), `src/lib.rs` (library root).
- Default binary: `cargo run -- ...`.
- Rust edition: 2021.
## Architecture
The codebase follows a DDD-style three-layer split. Respect the dependency direction: `presentation``domain``infrastructure`. Never have `domain` import from `presentation` or `infrastructure`.
```
src/
├── domain/ # Pure business logic — no I/O, no UI, no DB
│ ├── models/ # Value objects, entities (Challenge, Stage, Session, ...)
│ ├── repositories/ # Repository traits + their default implementations
│ ├── services/ # Domain services (scoring, parsing, version checks, ...)
│ ├── events/ # Domain & presentation events (EventBus)
│ ├── stores/ # In-memory shared state
│ └── error.rs # GitTypeError + Result
├── infrastructure/ # External adapters: filesystem, SQLite, git2, HTTP, terminal
│ ├── database/ # rusqlite + DAOs + migrations
│ ├── storage/ # FileStorage / CompressedFileStorage (with test-mocks)
│ ├── git/ # git2 wrappers
│ ├── http/ # reqwest-based clients (GitHub, OSS Insight)
│ ├── logging.rs # log4rs setup + error/panic file logging
│ └── terminal.rs # ratatui terminal factory (real TTY only)
└── presentation/
├── cli/ # clap-based CLI (Cli, Commands, run_cli)
├── tui/ # ratatui screens, ScreenManager, transitions
├── ui/ # Reusable rendering primitives (colors, gradients, ...)
├── sharing.rs # SharingPlatform enum + share URL building
├── di.rs # AppModule (shaku) wiring all components
└── signal_handler.rs
```
### Dependency injection
All wiring lives in `presentation/di.rs` (`AppModule`). When you add a new repository / service / screen:
1. Define a `pub trait FooInterface: shaku::Interface` in the appropriate layer.
2. Implement it as a `#[derive(shaku::Component)] pub struct FooImpl` with `#[shaku(inject)]` for its dependencies.
3. Register it in `AppModule` (`components` list).
4. Resolve via `module.resolve::<dyn FooInterface>()` or `#[shaku(inject)]`, never construct it ad-hoc.
## Build, test, lint
CI runs these exact commands — all four must pass:
```bash
cargo fmt --all -- --check
cargo clippy --all-targets --all-features -- -D warnings
cargo test
cargo llvm-cov --all-features --workspace --lcov --output-path lcov.info
```
Useful local variants:
- `cargo test <name>` — run a single test.
- `cargo run -- --help` — exercise the CLI.
- `cargo bench` — benchmarks under `benches/`.
## Test conventions
**Never write tests inside `src/`.** No inline `#[cfg(test)] mod tests { ... }` blocks in source files. All tests live under the top-level `tests/` directory, mirroring `src/`:
- `tests/unit/<layer>/<module>_tests.rs` — unit tests against `gittype::...`.
- `tests/integration/...` — multi-component / CLI-level tests, language fixtures, screen snapshots.
- `tests/helpers/`, `tests/fixtures/` — shared utilities and inputs.
When a test needs to construct or inspect something whose fields/methods are private, add a feature-gated test helper on the `src/` side rather than tests in `src/` or making the real API public:
```rust
impl Foo {
#[cfg(feature = "test-mocks")]
pub fn new_for_test(deps: ...) -> Self { ... }
#[cfg(feature = "test-mocks")]
pub fn internal_thing_for_test(&self) -> &Bar { &self.internal_thing }
}
```
The dev-dependency declared in `Cargo.toml` enables `test-mocks` for the tests crate:
```toml
[dev-dependencies]
gittype = { path = ".", default-features = false, features = ["test-mocks"] }
```
so `_for_test` helpers are visible only to tests, never to production builds.
### CI has no TTY
The Linux GitHub Actions runners have no TTY. Tests that construct a real `ratatui` terminal — directly via `CrosstermBackend::new(stdout())` or transitively through `TerminalComponent::get()` / `ScreenManagerFactory::create()` — panic with `Os { code: 11, kind: WouldBlock }`. Either:
- Guard with `if !atty::is(atty::Stream::Stdout) { return; }` (acceptable for "real-terminal" smoke tests), or
- Construct `ratatui::backend::TestBackend::new(w, h)` and wrap it in `Terminal::new(...)` directly.
Prefer `TestBackend` for anything testing rendering or screen transitions.
### Snapshots
Screen rendering uses `insta` for snapshot tests. Review and commit `.snap` files alongside the source change; never auto-accept blindly.
## Coding style
- **Place public items at the top of files**: pub structs / traits / functions first, private helpers below.
- **Prefer higher-order combinators** (`map` / `filter` / `filter_map` / `find_map` / `iter().fold(...)`) over imperative `for` / `while` / mutable accumulators when expressing transformations.
- **Single responsibility**: aim for functions of ~1015 lines, files of ~100 lines. Split when they grow.
- **No comments by default.** Only add a comment when the *why* is non-obvious (a hidden invariant, an upstream bug workaround, a surprising constraint). Don't restate what the code does, and don't reference the PR / task / caller — that information rots.
- **No `unwrap()` / `expect()` in production paths.** Bubble errors via `GitTypeError` and `Result`. `unwrap` is acceptable inside tests and `_for_test` helpers.
- **Match existing patterns.** When adding a new screen, language extractor, repository, etc., copy the structure of the nearest equivalent rather than inventing a new shape.
- **No dead code, no speculative abstractions.** Don't add features, traits, or feature flags for hypothetical future needs. Three similar lines beats a premature trait.
- **Don't fight `clippy`.** If `cargo clippy --all-targets --all-features -- -D warnings` complains, fix the code rather than allowing the lint.
### Adding a language extractor
The full recipe (Cargo dep → `LanguageExtractor` impl → registration → color scheme → fixtures → docs) is in `docs/CONTRIBUTING.md`. Follow it end-to-end; partial additions break the parser registry tests.
## Artifact language
Everything written to the repository, GitHub, npm, or any external system is in **English from the first draft**:
- Source code (identifiers, comments, docstrings)
- Commit messages (subject and body)
- Pull request titles, descriptions, issue text
- Branch names
- Documentation under `docs/`, `README.md`, `CHANGELOG.md`, this file
- Log output, error messages, user-facing strings inside code
- File and directory names
Do not draft in another language and translate later. Exception: locale / i18n resources intended for non-English end users, and any file that is already maintained in another language for consistency.
## Git workflow
- **Branches** are descriptive English kebab-case (`improve-test-coverage-...`, `feat-add-elixir-extractor`).
- **Commits** follow Conventional Commits: `feat:`, `fix:`, `test:`, `refactor:`, `docs:`, `chore:`, `perf:`, `style:`. Subject is short and imperative; details go in the body.
- **Never `git push --force` to `main`.** Force-pushing your own feature branch is fine if you understand the consequences; pause and ask if anyone else may have based work on it.
- **Never bypass hooks** (`--no-verify`, `--no-gpg-sign`, etc.). Fix the underlying failure instead.
- **Pre-flight before pushing**: run `cargo fmt --all -- --check`, `cargo clippy --all-targets --all-features -- -D warnings`, and `cargo test`. CI runs the same and will reject the PR otherwise.
## Pull requests
- Keep PRs focused — one logical change per PR.
- Title in Conventional Commit form; body explains the *why*, references issues, and lists notable behavioural changes.
- For UI / screen changes attach a screenshot or asciicast.
- **Merge with `--merge` (regular merge commit). Never `--squash`.** History on `main` is intentionally not linear.
## Risky operations
Pause and confirm with the human before:
- Force-pushing, `git reset --hard`, deleting branches, dropping rows / tables.
- Touching CI workflow files (`.github/workflows/*`).
- Editing `Cargo.lock` outside of normal `cargo` updates.
- Anything that modifies shared state outside this repository (publishing crates, posting to GitHub Issues / PRs you weren't asked to touch, etc.).
When in doubt, describe the action and ask before running it.
+1025
View File
File diff suppressed because it is too large Load Diff
+1
View File
@@ -0,0 +1 @@
@AGENTS.md
+2
View File
@@ -0,0 +1,2 @@
# Global rule - @unhappychoice owns everything
* @unhappychoice
+107
View File
@@ -0,0 +1,107 @@
[package]
name = "gittype"
version = "0.10.0"
edition = "2021"
authors = ["unhappychoice"]
description = "A typing practice tool using your own code repositories"
license = "MIT"
repository = "https://github.com/unhappychoice/gittype"
homepage = "https://github.com/unhappychoice/gittype"
documentation = "https://docs.rs/gittype"
keywords = ["typing", "practice", "cli", "git", "code"]
categories = ["command-line-utilities", "games"]
default-run = "gittype"
exclude = [
"*.png",
"*.gif",
"*.jpg",
"*.jpeg",
"docs/",
"examples/",
"tests/fixtures/",
".github/",
]
[[bin]]
name = "gittype"
path = "src/main.rs"
[dependencies]
clap = { version = "4.6", features = ["derive"] }
tree-sitter = "0.25"
tree-sitter-rust = "0.24"
tree-sitter-typescript = "0.23"
tree-sitter-javascript = "0.25"
tree-sitter-python = "0.25"
tree-sitter-ruby = "0.23"
tree-sitter-go = "0.25"
tree-sitter-swift = "0.7"
tree-sitter-kotlin-ng = "1.0"
tree-sitter-java = "0.23"
tree-sitter-php = "0.24"
tree-sitter-c-sharp = "0.23"
tree-sitter-c = "0.24"
tree-sitter-clojure = "0.1"
tree-sitter-cpp = "0.23"
tree-sitter-haskell = "0.23"
tree-sitter-dart = "0.2.0"
tree-sitter-elixir = "0.3"
tree-sitter-erlang = "0.19"
tree-sitter-scala = "0.26"
tree-sitter-zig = "1.0"
crossterm = "0.29"
ratatui = "0.30"
ansi-to-tui = "8.0.1"
rusqlite = { version = "0.40", features = ["bundled"] }
anyhow = "1.0"
thiserror = "2.0"
walkdir = "2.0"
glob = "0.3"
ignore = "0.4"
chrono = { version = "0.4", features = ["serde"] }
serde = { version = "1.0", features = ["derive"] }
serde_json = "1.0"
ctrlc = "3.5.2"
dirs = "6.0"
uuid = { version = "1.23", features = ["v4"] }
rand = { version = "0.10", features = ["std_rng"] }
once_cell = "1.21"
rayon = "1.12"
open = "5.3"
urlencoding = "2.1"
git2 = { version = "0.21", features = ["vendored-openssl", "vendored-libgit2"] }
streaming-iterator = "0.1"
log = "0.4"
log4rs = "1.4"
env_logger = "0.11"
tempfile = "3.25"
tokio = { version = "1.50", features = ["macros", "rt-multi-thread"] }
reqwest = { version = "0.13", features = ["json"] }
async-trait = "0.1"
atty = "0.2"
bincode = { version = "2.0", features = ["serde"] }
flate2 = "1.1"
sha2 = "0.11"
shaku = "0.6"
[dev-dependencies]
paste = "1.0"
insta = "1.47"
criterion = "0.8"
gittype = { path = ".", default-features = false, features = ["test-mocks"] }
[features]
default = []
test-mocks = []
[[bench]]
name = "challenge_generator_bench"
harness = false
[[bench]]
name = "source_code_parser_bench"
harness = false
+21
View File
@@ -0,0 +1,21 @@
MIT License
Copyright (c) 2025 unhappychoice
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.
+8140
View File
File diff suppressed because it is too large Load Diff
+182
View File
@@ -0,0 +1,182 @@
<p align="center">
<img src="docs/images/gittype-banner.png" alt="GitType" width="820">
</p>
<p align="center">
<a href="https://crates.io/crates/gittype"><img src="https://img.shields.io/crates/v/gittype.svg?style=flat-square&color=E06B4B" alt="crates.io"></a>
<a href="https://github.com/unhappychoice/gittype/releases"><img src="https://img.shields.io/github/v/release/unhappychoice/gittype?style=flat-square&color=E0C14B&label=release" alt="release"></a>
<a href="https://github.com/unhappychoice/gittype/actions/workflows/ci.yml"><img src="https://img.shields.io/github/actions/workflow/status/unhappychoice/gittype/ci.yml?branch=main&style=flat-square&label=CI" alt="CI"></a>
<a href="https://codecov.io/gh/unhappychoice/gittype"><img src="https://img.shields.io/codecov/c/github/unhappychoice/gittype?style=flat-square" alt="codecov"></a>
<a href="https://github.com/unhappychoice/gittype/blob/main/LICENSE"><img src="https://img.shields.io/crates/l/gittype.svg?style=flat-square" alt="license"></a>
</p>
<p align="center">
<strong>Turn your own source code into typing challenges.</strong><br>
<sub>Show your AI who's boss — just you, your keyboard, and your coding sins.</sub>
</p>
<p align="center">
<img src="docs/images/demo.gif" alt="GitType Demo" width="820">
</p>
## Features ✨
- 🦀🐍⚡🐹💎🍎🎯☕🐘#⃣🔧➕🎭🎯⚡💧📡 **Multi-language**: Rust, TypeScript, JavaScript, Python, Go, Ruby, Swift, Kotlin, Java, PHP, C#, C, C++, Haskell, Dart, Scala, Clojure, Elixir, Erlang, Zig (more languages incoming!)
- 📊 **Real-time metrics**: Live WPM, accuracy, and consistency tracking as you type
- 🏆 **Ranking system**: Unlock developer titles from "Hello World Newbie" to "Quantum Computer" with ASCII art
- 🎮 **Multiple game modes**: Normal, Time Attack, and custom difficulty levels (Easy to Zen)
- ⏸️ **Pause/resume**: Take breaks without ruining your stats
- 🎯 **Your own code**: Type functions from your actual projects, not boring examples
- 🔥 **Trending repositories**: Practice with hot GitHub repositories updated daily
- 🎨 **15+ Themes**: Built-in themes with Dark/Light modes + custom theme support
## Installation 📦
### Quick Install (Recommended)
#### One-liner installation (Linux/macOS/Windows)
```bash
curl -sSL https://raw.githubusercontent.com/unhappychoice/gittype/main/install.sh | bash
```
#### Or with specific version
```bash
curl -sSL https://raw.githubusercontent.com/unhappychoice/gittype/main/install.sh | bash -s -- --version v0.5.0
```
### Homebrew (macOS/Linux)
```bash
brew install gittype
```
### Cargo (Universal)
```bash
cargo install gittype
```
### Nix (NixOS/Nix)
If you have [Nix](https://nixos.org/) installed, you can run GitType directly:
```bash
# Stable version (recommended)
nix run github:unhappychoice/gittype
# Development version (latest from main branch)
nix run github:unhappychoice/gittype#unstable
```
### Binary Downloads
Get pre-compiled binaries for your platform from our [releases page](https://github.com/unhappychoice/gittype/releases/latest).
Available platforms:
- `x86_64-apple-darwin` (Intel Mac)
- `aarch64-apple-darwin` (Apple Silicon Mac)
- `x86_64-unknown-linux-gnu` (Linux x64)
- `aarch64-unknown-linux-gnu` (Linux ARM64)
- `x86_64-pc-windows-msvc` (Windows)
## Quick Start 🚀
```bash
# cd into your messy codebase
cd ~/that-project-you-never-finished
# Start typing your own spaghetti code (uses current directory by default)
gittype
# Or specify a specific repository path
gittype /path/to/another/repo
# Clone and play with any GitHub repository
gittype --repo clap-rs/clap
gittype --repo https://github.com/ratatui-org/ratatui
gittype --repo git@github.com:dtolnay/anyhow.git
# Discover and practice with trending GitHub repositories
gittype trending # Browse trending repos interactively
gittype trending rust # Filter by language (Rust)
# Play with cached repositories interactively
gittype repo play
```
## Why GitType? 🤔
- **Look busy at work** → "I'm studying the codebase" (technically true!)
- **Beat the AI overlords** → Type faster than ChatGPT can generate
- **Stop typing boring stuff** → Your own bugs are way more interesting than lorem ipsum
- **Discover forgotten treasures** → That elegant function you wrote at 3am last year
- **Procrastinate like a pro** → It's code review, but gamified!
- **Embrace your legacy code** → Finally face those variable names you're not proud of
- **Debug your typing skills** → Because `pubic static void main` isn't a typo anymore
- **Therapeutic code reliving** → Type through your programming journey, tears included
- **Climb the dev ladder** → From "Code Monkey" to "Quantum Computer" - each rank comes with fancy ASCII art
*"Basically, you need an excuse to avoid real work, and this one's pretty good."*
## Documentation 📚
Perfect for when the game gets too addictive:
- **[Installation](docs/installation.md)** - `cargo install` and chill
- **[Usage](docs/usage.md)** - All the CLI flags your heart desires
- **[Playing Guide](docs/playing-guide.md)** - Game modes, scoring, and ranks
- **[Themes](docs/themes.md)** - 15+ built-in themes and custom theme creation
- **[Languages](docs/supported-languages.md)** - What we extract and how
- **[Contributing](docs/CONTRIBUTING.md)** - Join the keyboard warriors
- **[Architecture](docs/ARCHITECTURE.md)** - For the curious minds
## Screenshots 📸
![GitType Title Screen](docs/images/title.png)
![GitType Gaming](docs/images/gaming.png)
![GitType Result](docs/images/result.png)
![GitType Result](docs/images/stage-result.png)
![GitType Records](docs/images/records.png)
![GitType Records Detail](docs/images/records-detail.png)
![GitType Analytics Overview](docs/images/analytics-overview.png)
![GitType Analytics Trends](docs/images/analytics-trends.png)
![GitType Analytics Languages](docs/images/analytics-languages.png)
![GitType Analytics Repositories](docs/images/analytics-repositories.png)
![GitType Settings](docs/images/settings-theme.png)
## Related Projects 🎨
Prefer watching code over typing it? Check out [**Gitlogue**](https://github.com/unhappychoice/gitlogue) - A terminal screensaver that animates your Git commit history with realistic typing effects.
## License 📄
[MIT](LICENSE) - Because sharing is caring (and legal requirements)
---
*Built with ❤️ and way too much caffeine by developers who got tired of typing "hello world"*
## Author
[@unhappychoice](https://unhappychoice.com)
## Support
If you find this project useful, please consider:
- ⭐️ [Star on GitHub](https://github.com/unhappychoice/gittype)
- 🐦 [Share on X](https://x.com/intent/post?text=gittype%3A%20a%20CLI%20typing%20game%20that%20uses%20your%20own%20source%20code%20as%20practice%20text%20%F0%9F%8E%AE&url=https%3A//github.com/unhappychoice/gittype&hashtags=gittype,CLI,Rust,typing)
- 🦋 [Share on Bluesky](https://bsky.app/intent/compose?text=gittype%3A%20a%20CLI%20typing%20game%20that%20uses%20your%20own%20source%20code%20as%20practice%20text%20%F0%9F%8E%AE%20%23gittype%20%23CLI%20%23Rust%20%23typing%20https%3A//github.com/unhappychoice/gittype)
- 🧵 [Share on Threads](https://www.threads.net/intent/post?text=gittype%3A%20a%20CLI%20typing%20game%20that%20uses%20your%20own%20source%20code%20as%20practice%20text%20%F0%9F%8E%AE%20%23gittype%20%23CLI%20%23Rust%20%23typing%20https%3A//github.com/unhappychoice/gittype)
- 💼 [Share on LinkedIn](https://www.linkedin.com/sharing/share-offsite/?url=https%3A//github.com/unhappychoice/gittype)
- 📘 [Share on Facebook](https://www.facebook.com/sharer/sharer.php?u=https%3A//github.com/unhappychoice/gittype)
- 🟧 [Submit to Hacker News](https://news.ycombinator.com/submitlink?u=https%3A//github.com/unhappychoice/gittype&t=gittype%3A%20a%20CLI%20typing%20game%20that%20uses%20your%20own%20source%20code%20as%20practice%20text%20%F0%9F%8E%AE)
- 💬 Drop it into your Discord server or developer chat
- ✍️ Write about it on your blog or in a newsletter
Every bit of support helps. Thanks!
+7
View File
@@ -0,0 +1,7 @@
# WeHub 来源说明
- 原始项目:`unhappychoice/gittype`
- 原始仓库:https://github.com/unhappychoice/gittype
- 导入方式:上游默认分支的最新快照
- 原作者、版权和许可证信息以原始仓库及本仓库 LICENSE 为准
- 本文件仅用于记录来源,不代表 WeHub 是原项目作者
+19
View File
@@ -0,0 +1,19 @@
Third-party licenses for the dependencies of gittype.
Overview:
{{#each overview}}
- {{{name}}} ({{count}})
{{/each}}
{{#each licenses}}
================================================================================
{{{name}}}
================================================================================
Used by:
{{#each used_by}}
- {{crate.name}} {{crate.version}}{{#if crate.repository}} ({{{crate.repository}}}){{/if}}
{{/each}}
{{{text}}}
{{/each}}
+9
View File
@@ -0,0 +1,9 @@
accepted = [
"Apache-2.0",
"MIT",
"BSD-3-Clause",
"Unicode-3.0",
"Zlib",
"0BSD",
"MPL-2.0",
]
+23
View File
@@ -0,0 +1,23 @@
{
"lang_rust": "yellow",
"lang_python": "yellow",
"lang_javascript": "yellow",
"lang_typescript": "blue",
"lang_go": "cyan",
"lang_java": "red",
"lang_c": "gray",
"lang_cpp": "blue",
"lang_csharp": "magenta",
"lang_php": "light_blue",
"lang_ruby": "red",
"lang_swift": "red",
"lang_kotlin": "magenta",
"lang_scala": "red",
"lang_haskell": "magenta",
"lang_dart": "cyan",
"lang_zig": "yellow",
"lang_clojure": "green",
"lang_elixir": "magenta",
"lang_erlang": "red",
"lang_default": "white"
}
+23
View File
@@ -0,0 +1,23 @@
{
"lang_rust": {"r": 222, "g": 165, "b": 132},
"lang_python": {"r": 255, "g": 212, "b": 59},
"lang_javascript": {"r": 240, "g": 219, "b": 79},
"lang_typescript": {"r": 49, "g": 120, "b": 198},
"lang_go": {"r": 0, "g": 173, "b": 181},
"lang_java": {"r": 237, "g": 41, "b": 57},
"lang_c": {"r": 85, "g": 85, "b": 85},
"lang_cpp": {"r": 0, "g": 89, "b": 156},
"lang_csharp": {"r": 239, "g": 117, "b": 27},
"lang_php": {"r": 119, "g": 123, "b": 180},
"lang_ruby": {"r": 204, "g": 52, "b": 45},
"lang_swift": {"r": 250, "g": 109, "b": 63},
"lang_kotlin": {"r": 124, "g": 75, "b": 255},
"lang_scala": {"r": 220, "g": 50, "b": 47},
"lang_haskell": {"r": 94, "g": 80, "b": 134},
"lang_dart": {"r": 0, "g": 180, "b": 240},
"lang_zig": {"r": 249, "g": 169, "b": 60},
"lang_clojure": {"r": 92, "g": 181, "b": 68},
"lang_elixir": {"r": 110, "g": 74, "b": 156},
"lang_erlang": {"r": 163, "g": 31, "b": 52},
"lang_default": {"r": 255, "g": 255, "b": 255}
}
+23
View File
@@ -0,0 +1,23 @@
{
"lang_rust": {"r": 222, "g": 165, "b": 132},
"lang_python": {"r": 184, "g": 134, "b": 11},
"lang_javascript": {"r": 181, "g": 137, "b": 0},
"lang_typescript": {"r": 49, "g": 120, "b": 198},
"lang_go": {"r": 0, "g": 173, "b": 181},
"lang_java": {"r": 237, "g": 41, "b": 57},
"lang_c": {"r": 85, "g": 85, "b": 85},
"lang_cpp": {"r": 0, "g": 89, "b": 156},
"lang_csharp": {"r": 239, "g": 117, "b": 27},
"lang_php": {"r": 119, "g": 123, "b": 180},
"lang_ruby": {"r": 204, "g": 52, "b": 45},
"lang_swift": {"r": 250, "g": 109, "b": 63},
"lang_kotlin": {"r": 124, "g": 75, "b": 255},
"lang_scala": {"r": 220, "g": 50, "b": 47},
"lang_haskell": {"r": 94, "g": 80, "b": 134},
"lang_dart": {"r": 0, "g": 120, "b": 180},
"lang_zig": {"r": 189, "g": 99, "b": 0},
"lang_clojure": {"r": 92, "g": 181, "b": 68},
"lang_elixir": {"r": 78, "g": 52, "b": 112},
"lang_erlang": {"r": 130, "g": 20, "b": 40},
"lang_default": {"r": 64, "g": 64, "b": 64}
}
+55
View File
@@ -0,0 +1,55 @@
{
"id": "ascii",
"name": "ASCII",
"description": "Classic terminal color scheme with ASCII-style colors",
"dark": {
"border": "blue",
"title": "white",
"text": "white",
"text_secondary": "dark_gray",
"background": "black",
"background_secondary": "dark_gray",
"status_success": "green",
"status_info": "cyan",
"status_warning": "yellow",
"status_error": "red",
"key_back": "red",
"key_action": "green",
"key_navigation": "cyan",
"metrics_score": "magenta",
"metrics_cpm_wpm": "green",
"metrics_accuracy": "yellow",
"metrics_duration": "cyan",
"metrics_stage_info": "blue",
"typing_untyped_text": "white",
"typing_typed_text": "light_blue",
"typing_cursor_fg": "black",
"typing_cursor_bg": "white",
"typing_mistake_bg": "red"
},
"light": {
"border": "blue",
"title": "black",
"text": "black",
"text_secondary": "dark_gray",
"background": "white",
"background_secondary": "light_gray",
"status_success": "green",
"status_info": "blue",
"status_warning": "yellow",
"status_error": "red",
"key_back": "red",
"key_action": "green",
"key_navigation": "blue",
"metrics_score": "magenta",
"metrics_cpm_wpm": "green",
"metrics_accuracy": "yellow",
"metrics_duration": "blue",
"metrics_stage_info": "blue",
"typing_untyped_text": "blue",
"typing_typed_text": "blue",
"typing_cursor_fg": "white",
"typing_cursor_bg": "black",
"typing_mistake_bg": "red"
}
}
+55
View File
@@ -0,0 +1,55 @@
{
"id": "aurora",
"name": "Aurora",
"description": "Aurora-inspired theme with flowing cyan, pink, and violet",
"dark": {
"border": { "r": 120, "g": 200, "b": 255 },
"title": { "r": 200, "g": 220, "b": 255 },
"text": { "r": 220, "g": 240, "b": 255 },
"text_secondary": { "r": 110, "g": 125, "b": 140 },
"background": { "r": 8, "g": 12, "b": 20 },
"background_secondary": { "r": 20, "g": 28, "b": 40 },
"status_success": { "r": 100, "g": 240, "b": 220 },
"status_info": { "r": 120, "g": 200, "b": 255 },
"status_warning": { "r": 255, "g": 180, "b": 120 },
"status_error": { "r": 255, "g": 100, "b": 180 },
"key_back": { "r": 255, "g": 100, "b": 180 },
"key_action": { "r": 100, "g": 240, "b": 220 },
"key_navigation": { "r": 100, "g": 240, "b": 220 },
"metrics_score": { "r": 120, "g": 200, "b": 255 },
"metrics_cpm_wpm": { "r": 100, "g": 240, "b": 220 },
"metrics_accuracy": { "r": 255, "g": 180, "b": 120 },
"metrics_duration": { "r": 255, "g": 100, "b": 180 },
"metrics_stage_info": { "r": 120, "g": 200, "b": 255 },
"typing_untyped_text": { "r": 150, "g": 170, "b": 200 },
"typing_typed_text": { "r": 120, "g": 200, "b": 255 },
"typing_cursor_fg": { "r": 0, "g": 0, "b": 0 },
"typing_cursor_bg": { "r": 120, "g": 200, "b": 255 },
"typing_mistake_bg": { "r": 255, "g": 100, "b": 180 }
},
"light": {
"border": { "r": 100, "g": 180, "b": 240 },
"title": { "r": 30, "g": 50, "b": 80 },
"text": { "r": 50, "g": 70, "b": 100 },
"text_secondary": { "r": 120, "g": 160, "b": 220 },
"background": { "r": 245, "g": 250, "b": 255 },
"background_secondary": { "r": 225, "g": 235, "b": 245 },
"status_success": { "r": 80, "g": 200, "b": 190 },
"status_info": { "r": 100, "g": 180, "b": 240 },
"status_warning": { "r": 240, "g": 160, "b": 100 },
"status_error": { "r": 220, "g": 80, "b": 160 },
"key_back": { "r": 220, "g": 80, "b": 160 },
"key_action": { "r": 80, "g": 200, "b": 190 },
"key_navigation": { "r": 80, "g": 200, "b": 190 },
"metrics_score": { "r": 100, "g": 180, "b": 240 },
"metrics_cpm_wpm": { "r": 80, "g": 200, "b": 190 },
"metrics_accuracy": { "r": 240, "g": 160, "b": 100 },
"metrics_duration": { "r": 220, "g": 80, "b": 160 },
"metrics_stage_info": { "r": 100, "g": 180, "b": 240 },
"typing_untyped_text": { "r": 90, "g": 120, "b": 150 },
"typing_typed_text": { "r": 100, "g": 180, "b": 240 },
"typing_cursor_fg": { "r": 255, "g": 255, "b": 255 },
"typing_cursor_bg": { "r": 100, "g": 180, "b": 240 },
"typing_mistake_bg": { "r": 220, "g": 80, "b": 160 }
}
}
+55
View File
@@ -0,0 +1,55 @@
{
"id": "blood_oath",
"name": "Blood Oath",
"description": "Dark sacrificial theme with crimson and shadow",
"dark": {
"border": { "r": 180, "g": 0, "b": 0 },
"title": { "r": 240, "g": 210, "b": 210 },
"text": { "r": 230, "g": 210, "b": 210 },
"text_secondary": { "r": 90, "g": 80, "b": 80 },
"background": { "r": 8, "g": 0, "b": 0 },
"background_secondary": { "r": 24, "g": 8, "b": 8 },
"status_success": { "r": 80, "g": 200, "b": 120 },
"status_info": { "r": 160, "g": 200, "b": 255 },
"status_warning": { "r": 255, "g": 190, "b": 80 },
"status_error": { "r": 220, "g": 40, "b": 40 },
"key_back": { "r": 220, "g": 40, "b": 40 },
"key_action": { "r": 80, "g": 200, "b": 120 },
"key_navigation": { "r": 160, "g": 200, "b": 255 },
"metrics_score": { "r": 255, "g": 60, "b": 60 },
"metrics_cpm_wpm": { "r": 80, "g": 200, "b": 120 },
"metrics_accuracy": { "r": 255, "g": 190, "b": 80 },
"metrics_duration": { "r": 160, "g": 200, "b": 255 },
"metrics_stage_info": { "r": 180, "g": 0, "b": 0 },
"typing_untyped_text": { "r": 160, "g": 120, "b": 120 },
"typing_typed_text": { "r": 255, "g": 160, "b": 160 },
"typing_cursor_fg": { "r": 0, "g": 0, "b": 0 },
"typing_cursor_bg": { "r": 220, "g": 40, "b": 40 },
"typing_mistake_bg": { "r": 220, "g": 40, "b": 40 }
},
"light": {
"border": { "r": 200, "g": 20, "b": 20 },
"title": { "r": 20, "g": 0, "b": 0 },
"text": { "r": 30, "g": 10, "b": 10 },
"text_secondary": { "r": 180, "g": 140, "b": 140 },
"background": { "r": 252, "g": 245, "b": 245 },
"background_secondary": { "r": 236, "g": 224, "b": 224 },
"status_success": { "r": 20, "g": 160, "b": 90 },
"status_info": { "r": 90, "g": 130, "b": 200 },
"status_warning": { "r": 230, "g": 160, "b": 60 },
"status_error": { "r": 200, "g": 40, "b": 40 },
"key_back": { "r": 200, "g": 40, "b": 40 },
"key_action": { "r": 20, "g": 160, "b": 90 },
"key_navigation": { "r": 90, "g": 130, "b": 200 },
"metrics_score": { "r": 200, "g": 20, "b": 20 },
"metrics_cpm_wpm": { "r": 20, "g": 160, "b": 90 },
"metrics_accuracy": { "r": 230, "g": 160, "b": 60 },
"metrics_duration": { "r": 120, "g": 160, "b": 220 },
"metrics_stage_info": { "r": 200, "g": 20, "b": 20 },
"typing_untyped_text": { "r": 120, "g": 80, "b": 80 },
"typing_typed_text": { "r": 200, "g": 40, "b": 40 },
"typing_cursor_fg": { "r": 255, "g": 255, "b": 255 },
"typing_cursor_bg": { "r": 200, "g": 20, "b": 20 },
"typing_mistake_bg": { "r": 200, "g": 40, "b": 40 }
}
}
+55
View File
@@ -0,0 +1,55 @@
{
"id": "cyber_void",
"name": "Cyber Void",
"description": "Digital void theme with vivid blue and red neon",
"dark": {
"border": { "r": 0, "g": 160, "b": 255 },
"title": { "r": 255, "g": 60, "b": 80 },
"text": { "r": 220, "g": 230, "b": 255 },
"text_secondary": { "r": 85, "g": 90, "b": 95 },
"background": { "r": 8, "g": 10, "b": 20 },
"background_secondary": { "r": 20, "g": 24, "b": 40 },
"status_success": { "r": 0, "g": 220, "b": 220 },
"status_info": { "r": 0, "g": 160, "b": 255 },
"status_warning": { "r": 255, "g": 100, "b": 220 },
"status_error": { "r": 255, "g": 60, "b": 140 },
"key_back": { "r": 255, "g": 60, "b": 140 },
"key_action": { "r": 0, "g": 220, "b": 220 },
"key_navigation": { "r": 0, "g": 220, "b": 220 },
"metrics_score": { "r": 255, "g": 60, "b": 140 },
"metrics_cpm_wpm": { "r": 0, "g": 220, "b": 220 },
"metrics_accuracy": { "r": 255, "g": 100, "b": 220 },
"metrics_duration": { "r": 0, "g": 160, "b": 255 },
"metrics_stage_info": { "r": 0, "g": 220, "b": 220 },
"typing_untyped_text": { "r": 130, "g": 140, "b": 165 },
"typing_typed_text": { "r": 0, "g": 190, "b": 255 },
"typing_cursor_fg": { "r": 0, "g": 0, "b": 0 },
"typing_cursor_bg": { "r": 255, "g": 60, "b": 140 },
"typing_mistake_bg": { "r": 255, "g": 60, "b": 140 }
},
"light": {
"border": { "r": 0, "g": 140, "b": 220 },
"title": { "r": 220, "g": 40, "b": 60 },
"text": { "r": 50, "g": 60, "b": 90 },
"text_secondary": { "r": 100, "g": 140, "b": 200 },
"background": { "r": 245, "g": 250, "b": 255 },
"background_secondary": { "r": 225, "g": 235, "b": 245 },
"status_success": { "r": 0, "g": 200, "b": 200 },
"status_info": { "r": 0, "g": 140, "b": 220 },
"status_warning": { "r": 255, "g": 130, "b": 220 },
"status_error": { "r": 220, "g": 40, "b": 140 },
"key_back": { "r": 220, "g": 40, "b": 140 },
"key_action": { "r": 0, "g": 200, "b": 200 },
"key_navigation": { "r": 0, "g": 200, "b": 200 },
"metrics_score": { "r": 220, "g": 40, "b": 140 },
"metrics_cpm_wpm": { "r": 0, "g": 200, "b": 200 },
"metrics_accuracy": { "r": 255, "g": 130, "b": 220 },
"metrics_duration": { "r": 0, "g": 140, "b": 220 },
"metrics_stage_info": { "r": 0, "g": 200, "b": 200 },
"typing_untyped_text": { "r": 80, "g": 100, "b": 140 },
"typing_typed_text": { "r": 0, "g": 160, "b": 230 },
"typing_cursor_fg": { "r": 255, "g": 255, "b": 255 },
"typing_cursor_bg": { "r": 220, "g": 40, "b": 140 },
"typing_mistake_bg": { "r": 220, "g": 40, "b": 140 }
}
}
+55
View File
@@ -0,0 +1,55 @@
{
"id": "default",
"name": "Default",
"description": "Default theme with softened contrast and balanced palette for comfortable readability",
"dark": {
"border": {"r": 102, "g": 153, "b": 204},
"title": {"r": 235, "g": 235, "b": 235},
"text": {"r": 220, "g": 220, "b": 220},
"text_secondary": {"r": 60, "g": 60, "b": 60},
"background": {"r": 15, "g": 15, "b": 15},
"background_secondary": {"r": 38, "g": 38, "b": 38},
"status_success": {"r": 0, "g": 175, "b": 95},
"status_info": {"r": 95, "g": 175, "b": 255},
"status_warning": {"r": 255, "g": 175, "b": 95},
"status_error": {"r": 215, "g": 95, "b": 95},
"key_back": {"r": 215, "g": 95, "b": 95},
"key_action": {"r": 0, "g": 175, "b": 95},
"key_navigation": {"r": 95, "g": 175, "b": 255},
"metrics_score": {"r": 175, "g": 135, "b": 255},
"metrics_cpm_wpm": {"r": 95, "g": 215, "b": 135},
"metrics_accuracy": {"r": 255, "g": 215, "b": 135},
"metrics_duration": {"r": 95, "g": 215, "b": 215},
"metrics_stage_info": {"r": 102, "g": 153, "b": 204},
"typing_untyped_text": {"r": 170, "g": 170, "b": 170},
"typing_typed_text": {"r": 135, "g": 175, "b": 255},
"typing_cursor_fg": {"r": 0, "g": 0, "b": 0},
"typing_cursor_bg": {"r": 220, "g": 220, "b": 220},
"typing_mistake_bg": {"r": 175, "g": 95, "b": 95}
},
"light": {
"border": {"r": 120, "g": 160, "b": 220},
"title": {"r": 30, "g": 30, "b": 30},
"text": {"r": 40, "g": 40, "b": 40},
"text_secondary": {"r": 180, "g": 180, "b": 180},
"background": {"r": 250, "g": 250, "b": 250},
"background_secondary": {"r": 225, "g": 225, "b": 225},
"status_success": {"r": 0, "g": 160, "b": 90},
"status_info": {"r": 70, "g": 140, "b": 230},
"status_warning": {"r": 230, "g": 150, "b": 40},
"status_error": {"r": 215, "g": 80, "b": 80},
"key_back": {"r": 215, "g": 80, "b": 80},
"key_action": {"r": 0, "g": 160, "b": 90},
"key_navigation": {"r": 70, "g": 140, "b": 230},
"metrics_score": {"r": 150, "g": 100, "b": 230},
"metrics_cpm_wpm": {"r": 0, "g": 160, "b": 90},
"metrics_accuracy": {"r": 230, "g": 150, "b": 40},
"metrics_duration": {"r": 50, "g": 180, "b": 200},
"metrics_stage_info": {"r": 120, "g": 160, "b": 220},
"typing_untyped_text": {"r": 110, "g": 120, "b": 130},
"typing_typed_text": {"r": 70, "g": 140, "b": 230},
"typing_cursor_fg": {"r": 255, "g": 255, "b": 255},
"typing_cursor_bg": {"r": 60, "g": 60, "b": 60},
"typing_mistake_bg": {"r": 215, "g": 80, "b": 80}
}
}
+55
View File
@@ -0,0 +1,55 @@
{
"id": "eclipse",
"name": "Eclipse",
"description": "Cosmic violet theme with deep purples and radiant highlights",
"dark": {
"border": { "r": 160, "g": 100, "b": 255 },
"title": { "r": 220, "g": 200, "b": 255 },
"text": { "r": 230, "g": 220, "b": 240 },
"text_secondary": { "r": 95, "g": 85, "b": 95 },
"background": { "r": 20, "g": 10, "b": 30 },
"background_secondary": { "r": 40, "g": 20, "b": 60 },
"status_success": { "r": 120, "g": 200, "b": 255 },
"status_info": { "r": 160, "g": 100, "b": 255 },
"status_warning": { "r": 255, "g": 200, "b": 80 },
"status_error": { "r": 200, "g": 60, "b": 200 },
"key_back": { "r": 200, "g": 60, "b": 200 },
"key_action": { "r": 120, "g": 200, "b": 255 },
"key_navigation": { "r": 120, "g": 200, "b": 255 },
"metrics_score": { "r": 160, "g": 100, "b": 255 },
"metrics_cpm_wpm": { "r": 120, "g": 200, "b": 255 },
"metrics_accuracy": { "r": 255, "g": 200, "b": 80 },
"metrics_duration": { "r": 160, "g": 100, "b": 255 },
"metrics_stage_info": { "r": 160, "g": 100, "b": 255 },
"typing_untyped_text": { "r": 160, "g": 140, "b": 180 },
"typing_typed_text": { "r": 160, "g": 100, "b": 255 },
"typing_cursor_fg": { "r": 0, "g": 0, "b": 0 },
"typing_cursor_bg": { "r": 200, "g": 100, "b": 255 },
"typing_mistake_bg": { "r": 200, "g": 60, "b": 200 }
},
"light": {
"border": { "r": 140, "g": 80, "b": 220 },
"title": { "r": 120, "g": 60, "b": 200 },
"text": { "r": 40, "g": 30, "b": 60 },
"text_secondary": { "r": 150, "g": 120, "b": 220 },
"background": { "r": 250, "g": 245, "b": 255 },
"background_secondary": { "r": 235, "g": 225, "b": 245 },
"status_success": { "r": 100, "g": 180, "b": 240 },
"status_info": { "r": 140, "g": 80, "b": 220 },
"status_warning": { "r": 240, "g": 180, "b": 80 },
"status_error": { "r": 180, "g": 60, "b": 180 },
"key_back": { "r": 180, "g": 60, "b": 180 },
"key_action": { "r": 100, "g": 180, "b": 240 },
"key_navigation": { "r": 100, "g": 180, "b": 240 },
"metrics_score": { "r": 140, "g": 80, "b": 220 },
"metrics_cpm_wpm": { "r": 100, "g": 180, "b": 240 },
"metrics_accuracy": { "r": 240, "g": 180, "b": 80 },
"metrics_duration": { "r": 140, "g": 80, "b": 220 },
"metrics_stage_info": { "r": 140, "g": 80, "b": 220 },
"typing_untyped_text": { "r": 80, "g": 70, "b": 100 },
"typing_typed_text": { "r": 140, "g": 80, "b": 220 },
"typing_cursor_fg": { "r": 255, "g": 255, "b": 255 },
"typing_cursor_bg": { "r": 140, "g": 80, "b": 220 },
"typing_mistake_bg": { "r": 180, "g": 60, "b": 180 }
}
}
+55
View File
@@ -0,0 +1,55 @@
{
"id": "glacier",
"name": "Glacier",
"description": "Frozen theme with icy blues and arctic highlights",
"dark": {
"border": { "r": 0, "g": 120, "b": 200 },
"title": { "r": 200, "g": 220, "b": 255 },
"text": { "r": 210, "g": 230, "b": 255 },
"text_secondary": { "r": 100, "g": 110, "b": 120 },
"background": { "r": 6, "g": 12, "b": 24 },
"background_secondary": { "r": 16, "g": 26, "b": 44 },
"status_success": { "r": 0, "g": 180, "b": 220 },
"status_info": { "r": 0, "g": 140, "b": 255 },
"status_warning": { "r": 255, "g": 200, "b": 80 },
"status_error": { "r": 200, "g": 80, "b": 80 },
"key_back": { "r": 200, "g": 80, "b": 80 },
"key_action": { "r": 0, "g": 180, "b": 220 },
"key_navigation": { "r": 0, "g": 180, "b": 220 },
"metrics_score": { "r": 0, "g": 120, "b": 200 },
"metrics_cpm_wpm": { "r": 0, "g": 180, "b": 220 },
"metrics_accuracy": { "r": 0, "g": 140, "b": 255 },
"metrics_duration": { "r": 50, "g": 200, "b": 255 },
"metrics_stage_info": { "r": 0, "g": 120, "b": 200 },
"typing_untyped_text": { "r": 150, "g": 180, "b": 210 },
"typing_typed_text": { "r": 90, "g": 170, "b": 255 },
"typing_cursor_fg": { "r": 0, "g": 0, "b": 0 },
"typing_cursor_bg": { "r": 0, "g": 140, "b": 255 },
"typing_mistake_bg": { "r": 200, "g": 80, "b": 80 }
},
"light": {
"border": { "r": 0, "g": 120, "b": 200 },
"title": { "r": 20, "g": 40, "b": 80 },
"text": { "r": 40, "g": 60, "b": 100 },
"text_secondary": { "r": 100, "g": 140, "b": 200 },
"background": { "r": 240, "g": 250, "b": 255 },
"background_secondary": { "r": 210, "g": 230, "b": 240 },
"status_success": { "r": 0, "g": 180, "b": 220 },
"status_info": { "r": 0, "g": 140, "b": 255 },
"status_warning": { "r": 255, "g": 200, "b": 80 },
"status_error": { "r": 200, "g": 80, "b": 80 },
"key_back": { "r": 200, "g": 80, "b": 80 },
"key_action": { "r": 0, "g": 180, "b": 220 },
"key_navigation": { "r": 0, "g": 180, "b": 220 },
"metrics_score": { "r": 0, "g": 120, "b": 200 },
"metrics_cpm_wpm": { "r": 0, "g": 180, "b": 220 },
"metrics_accuracy": { "r": 0, "g": 140, "b": 255 },
"metrics_duration": { "r": 50, "g": 200, "b": 255 },
"metrics_stage_info": { "r": 0, "g": 120, "b": 200 },
"typing_untyped_text": { "r": 80, "g": 100, "b": 140 },
"typing_typed_text": { "r": 0, "g": 140, "b": 255 },
"typing_cursor_fg": { "r": 255, "g": 255, "b": 255 },
"typing_cursor_bg": { "r": 0, "g": 140, "b": 255 },
"typing_mistake_bg": { "r": 200, "g": 80, "b": 80 }
}
}
+55
View File
@@ -0,0 +1,55 @@
{
"id": "inferno",
"name": "Inferno",
"description": "Molten fire theme with blazing orange and ember glow",
"dark": {
"border": { "r": 255, "g": 100, "b": 0 },
"title": { "r": 255, "g": 160, "b": 0 },
"text": { "r": 230, "g": 220, "b": 200 },
"text_secondary": { "r": 75, "g": 65, "b": 55 },
"background": { "r": 20, "g": 10, "b": 0 },
"background_secondary": { "r": 40, "g": 20, "b": 10 },
"status_success": { "r": 255, "g": 200, "b": 0 },
"status_info": { "r": 255, "g": 140, "b": 0 },
"status_warning": { "r": 255, "g": 200, "b": 60 },
"status_error": { "r": 200, "g": 40, "b": 20 },
"key_back": { "r": 200, "g": 40, "b": 20 },
"key_action": { "r": 255, "g": 200, "b": 0 },
"key_navigation": { "r": 255, "g": 140, "b": 0 },
"metrics_score": { "r": 255, "g": 100, "b": 0 },
"metrics_cpm_wpm": { "r": 255, "g": 200, "b": 0 },
"metrics_accuracy": { "r": 255, "g": 160, "b": 0 },
"metrics_duration": { "r": 255, "g": 140, "b": 0 },
"metrics_stage_info": { "r": 255, "g": 180, "b": 0 },
"typing_untyped_text": { "r": 160, "g": 120, "b": 80 },
"typing_typed_text": { "r": 255, "g": 160, "b": 0 },
"typing_cursor_fg": { "r": 0, "g": 0, "b": 0 },
"typing_cursor_bg": { "r": 255, "g": 200, "b": 0 },
"typing_mistake_bg": { "r": 200, "g": 40, "b": 20 }
},
"light": {
"border": { "r": 255, "g": 140, "b": 0 },
"title": { "r": 255, "g": 120, "b": 0 },
"text": { "r": 40, "g": 30, "b": 20 },
"text_secondary": { "r": 200, "g": 150, "b": 100 },
"background": { "r": 255, "g": 245, "b": 235 },
"background_secondary": { "r": 240, "g": 225, "b": 210 },
"status_success": { "r": 255, "g": 180, "b": 0 },
"status_info": { "r": 255, "g": 140, "b": 0 },
"status_warning": { "r": 255, "g": 200, "b": 60 },
"status_error": { "r": 200, "g": 60, "b": 40 },
"key_back": { "r": 200, "g": 60, "b": 40 },
"key_action": { "r": 255, "g": 180, "b": 0 },
"key_navigation": { "r": 255, "g": 140, "b": 0 },
"metrics_score": { "r": 255, "g": 120, "b": 0 },
"metrics_cpm_wpm": { "r": 255, "g": 200, "b": 0 },
"metrics_accuracy": { "r": 255, "g": 160, "b": 0 },
"metrics_duration": { "r": 255, "g": 140, "b": 0 },
"metrics_stage_info": { "r": 255, "g": 180, "b": 0 },
"typing_untyped_text": { "r": 120, "g": 90, "b": 40 },
"typing_typed_text": { "r": 255, "g": 120, "b": 0 },
"typing_cursor_fg": { "r": 255, "g": 255, "b": 255 },
"typing_cursor_bg": { "r": 255, "g": 160, "b": 0 },
"typing_mistake_bg": { "r": 200, "g": 60, "b": 40 }
}
}
+55
View File
@@ -0,0 +1,55 @@
{
"id": "neon_abyss",
"name": "Neon Abyss",
"description": "Cyberpunk neon theme with glowing magenta, cyan, and lime accents",
"dark": {
"border": { "r": 0, "g": 255, "b": 255 },
"title": { "r": 255, "g": 0, "b": 255 },
"text": { "r": 230, "g": 230, "b": 230 },
"text_secondary": { "r": 75, "g": 75, "b": 85 },
"background": { "r": 10, "g": 10, "b": 20 },
"background_secondary": { "r": 30, "g": 30, "b": 40 },
"status_success": { "r": 150, "g": 255, "b": 120 },
"status_info": { "r": 0, "g": 210, "b": 255 },
"status_warning": { "r": 255, "g": 0, "b": 220 },
"status_error": { "r": 255, "g": 0, "b": 150 },
"key_back": { "r": 255, "g": 0, "b": 150 },
"key_action": { "r": 150, "g": 255, "b": 120 },
"key_navigation": { "r": 150, "g": 255, "b": 120 },
"metrics_score": { "r": 255, "g": 0, "b": 220 },
"metrics_cpm_wpm": { "r": 150, "g": 255, "b": 120 },
"metrics_accuracy": { "r": 255, "g": 0, "b": 220 },
"metrics_duration": { "r": 0, "g": 210, "b": 255 },
"metrics_stage_info": { "r": 255, "g": 0, "b": 220 },
"typing_untyped_text": { "r": 140, "g": 140, "b": 160 },
"typing_typed_text": { "r": 0, "g": 210, "b": 255 },
"typing_cursor_fg": { "r": 0, "g": 0, "b": 0 },
"typing_cursor_bg": { "r": 255, "g": 0, "b": 220 },
"typing_mistake_bg": { "r": 255, "g": 0, "b": 150 }
},
"light": {
"border": { "r": 0, "g": 200, "b": 255 },
"title": { "r": 255, "g": 0, "b": 200 },
"text": { "r": 40, "g": 40, "b": 40 },
"text_secondary": { "r": 140, "g": 140, "b": 200 },
"background": { "r": 245, "g": 245, "b": 255 },
"background_secondary": { "r": 225, "g": 225, "b": 245 },
"status_success": { "r": 120, "g": 210, "b": 140 },
"status_info": { "r": 0, "g": 190, "b": 255 },
"status_warning": { "r": 255, "g": 130, "b": 240 },
"status_error": { "r": 255, "g": 90, "b": 190 },
"key_back": { "r": 255, "g": 90, "b": 190 },
"key_action": { "r": 120, "g": 210, "b": 140 },
"key_navigation": { "r": 0, "g": 190, "b": 255 },
"metrics_score": { "r": 255, "g": 130, "b": 240 },
"metrics_cpm_wpm": { "r": 120, "g": 210, "b": 140 },
"metrics_accuracy": { "r": 255, "g": 130, "b": 240 },
"metrics_duration": { "r": 0, "g": 190, "b": 255 },
"metrics_stage_info": { "r": 255, "g": 130, "b": 240 },
"typing_untyped_text": { "r": 80, "g": 90, "b": 120 },
"typing_typed_text": { "r": 0, "g": 190, "b": 255 },
"typing_cursor_fg": { "r": 255, "g": 255, "b": 255 },
"typing_cursor_bg": { "r": 255, "g": 130, "b": 240 },
"typing_mistake_bg": { "r": 255, "g": 90, "b": 190 }
}
}
+55
View File
@@ -0,0 +1,55 @@
{
"id": "oblivion",
"name": "Oblivion",
"description": "Abyssal theme with turquoise and emerald glow",
"dark": {
"border": { "r": 0, "g": 200, "b": 180 },
"title": { "r": 180, "g": 255, "b": 240 },
"text": { "r": 200, "g": 255, "b": 240 },
"text_secondary": { "r": 75, "g": 85, "b": 80 },
"background": { "r": 5, "g": 10, "b": 10 },
"background_secondary": { "r": 15, "g": 25, "b": 25 },
"status_success": { "r": 0, "g": 200, "b": 120 },
"status_info": { "r": 0, "g": 180, "b": 200 },
"status_warning": { "r": 255, "g": 200, "b": 60 },
"status_error": { "r": 200, "g": 60, "b": 60 },
"key_back": { "r": 200, "g": 60, "b": 60 },
"key_action": { "r": 0, "g": 200, "b": 120 },
"key_navigation": { "r": 0, "g": 255, "b": 200 },
"metrics_score": { "r": 0, "g": 255, "b": 200 },
"metrics_cpm_wpm": { "r": 0, "g": 200, "b": 120 },
"metrics_accuracy": { "r": 255, "g": 200, "b": 60 },
"metrics_duration": { "r": 0, "g": 180, "b": 200 },
"metrics_stage_info": { "r": 0, "g": 200, "b": 180 },
"typing_untyped_text": { "r": 120, "g": 170, "b": 160 },
"typing_typed_text": { "r": 0, "g": 255, "b": 200 },
"typing_cursor_fg": { "r": 0, "g": 0, "b": 0 },
"typing_cursor_bg": { "r": 0, "g": 200, "b": 180 },
"typing_mistake_bg": { "r": 200, "g": 60, "b": 60 }
},
"light": {
"border": { "r": 0, "g": 180, "b": 160 },
"title": { "r": 0, "g": 60, "b": 60 },
"text": { "r": 20, "g": 80, "b": 80 },
"text_secondary": { "r": 100, "g": 180, "b": 160 },
"background": { "r": 240, "g": 255, "b": 250 },
"background_secondary": { "r": 220, "g": 240, "b": 235 },
"status_success": { "r": 0, "g": 160, "b": 120 },
"status_info": { "r": 0, "g": 140, "b": 160 },
"status_warning": { "r": 255, "g": 180, "b": 60 },
"status_error": { "r": 200, "g": 60, "b": 60 },
"key_back": { "r": 200, "g": 60, "b": 60 },
"key_action": { "r": 0, "g": 160, "b": 120 },
"key_navigation": { "r": 0, "g": 200, "b": 160 },
"metrics_score": { "r": 0, "g": 180, "b": 160 },
"metrics_cpm_wpm": { "r": 0, "g": 160, "b": 120 },
"metrics_accuracy": { "r": 255, "g": 180, "b": 60 },
"metrics_duration": { "r": 0, "g": 140, "b": 160 },
"metrics_stage_info": { "r": 0, "g": 180, "b": 160 },
"typing_untyped_text": { "r": 60, "g": 110, "b": 100 },
"typing_typed_text": { "r": 0, "g": 200, "b": 160 },
"typing_cursor_fg": { "r": 255, "g": 255, "b": 255 },
"typing_cursor_bg": { "r": 0, "g": 160, "b": 140 },
"typing_mistake_bg": { "r": 200, "g": 60, "b": 60 }
}
}
+55
View File
@@ -0,0 +1,55 @@
{
"id": "original",
"name": "Original",
"description": "Original theme with precise RGB colors and high contrast",
"dark": {
"border": {"r": 100, "g": 149, "b": 237},
"title": {"r": 255, "g": 255, "b": 255},
"text": {"r": 255, "g": 255, "b": 255},
"text_secondary": {"r": 96, "g": 96, "b": 96},
"background": {"r": 0, "g": 0, "b": 0},
"background_secondary": {"r": 64, "g": 64, "b": 64},
"status_success": {"r": 46, "g": 160, "b": 67},
"status_info": {"r": 96, "g": 165, "b": 250},
"status_warning": {"r": 245, "g": 158, "b": 11},
"status_error": {"r": 239, "g": 68, "b": 68},
"key_back": {"r": 239, "g": 68, "b": 68},
"key_action": {"r": 46, "g": 160, "b": 67},
"key_navigation": {"r": 96, "g": 165, "b": 250},
"metrics_score": {"r": 168, "g": 85, "b": 247},
"metrics_cpm_wpm": {"r": 46, "g": 160, "b": 67},
"metrics_accuracy": {"r": 245, "g": 158, "b": 11},
"metrics_duration": {"r": 34, "g": 211, "b": 238},
"metrics_stage_info": {"r": 100, "g": 149, "b": 237},
"typing_untyped_text": {"r": 192, "g": 192, "b": 192},
"typing_typed_text": {"r": 96, "g": 165, "b": 250},
"typing_cursor_fg": {"r": 0, "g": 0, "b": 0},
"typing_cursor_bg": {"r": 220, "g": 220, "b": 220},
"typing_mistake_bg": {"r": 239, "g": 68, "b": 68}
},
"light": {
"border": {"r": 100, "g": 149, "b": 237},
"title": {"r": 0, "g": 0, "b": 0},
"text": {"r": 0, "g": 0, "b": 0},
"text_secondary": {"r": 150, "g": 150, "b": 150},
"background": {"r": 255, "g": 255, "b": 255},
"background_secondary": {"r": 200, "g": 200, "b": 200},
"status_success": {"r": 46, "g": 160, "b": 67},
"status_info": {"r": 96, "g": 165, "b": 250},
"status_warning": {"r": 245, "g": 158, "b": 11},
"status_error": {"r": 239, "g": 68, "b": 68},
"key_back": {"r": 239, "g": 68, "b": 68},
"key_action": {"r": 46, "g": 160, "b": 67},
"key_navigation": {"r": 96, "g": 165, "b": 250},
"metrics_score": {"r": 168, "g": 85, "b": 247},
"metrics_cpm_wpm": {"r": 46, "g": 160, "b": 67},
"metrics_accuracy": {"r": 245, "g": 158, "b": 11},
"metrics_duration": {"r": 34, "g": 211, "b": 238},
"metrics_stage_info": {"r": 100, "g": 149, "b": 237},
"typing_untyped_text": {"r": 80, "g": 110, "b": 150},
"typing_typed_text": {"r": 96, "g": 165, "b": 250},
"typing_cursor_fg": {"r": 255, "g": 255, "b": 255},
"typing_cursor_bg": {"r": 40, "g": 40, "b": 40},
"typing_mistake_bg": {"r": 239, "g": 68, "b": 68}
}
}
+55
View File
@@ -0,0 +1,55 @@
{
"id": "runic",
"name": "Runic",
"description": "Ancient stone tablet theme with carved amber and weathered granite",
"dark": {
"border": { "r": 180, "g": 130, "b": 80 },
"title": { "r": 220, "g": 180, "b": 120 },
"text": { "r": 200, "g": 170, "b": 130 },
"text_secondary": { "r": 80, "g": 65, "b": 45 },
"background": { "r": 25, "g": 20, "b": 15 },
"background_secondary": { "r": 45, "g": 35, "b": 25 },
"status_success": { "r": 150, "g": 180, "b": 100 },
"status_info": { "r": 120, "g": 160, "b": 180 },
"status_warning": { "r": 200, "g": 150, "b": 80 },
"status_error": { "r": 180, "g": 80, "b": 60 },
"key_back": { "r": 180, "g": 80, "b": 60 },
"key_action": { "r": 150, "g": 180, "b": 100 },
"key_navigation": { "r": 120, "g": 160, "b": 180 },
"metrics_score": { "r": 180, "g": 130, "b": 80 },
"metrics_cpm_wpm": { "r": 150, "g": 180, "b": 100 },
"metrics_accuracy": { "r": 200, "g": 150, "b": 80 },
"metrics_duration": { "r": 120, "g": 160, "b": 180 },
"metrics_stage_info": { "r": 180, "g": 130, "b": 80 },
"typing_untyped_text": { "r": 140, "g": 120, "b": 100 },
"typing_typed_text": { "r": 200, "g": 150, "b": 80 },
"typing_cursor_fg": { "r": 25, "g": 20, "b": 15 },
"typing_cursor_bg": { "r": 180, "g": 130, "b": 80 },
"typing_mistake_bg": { "r": 180, "g": 80, "b": 60 }
},
"light": {
"border": { "r": 140, "g": 100, "b": 60 },
"title": { "r": 100, "g": 70, "b": 40 },
"text": { "r": 60, "g": 45, "b": 30 },
"text_secondary": { "r": 160, "g": 120, "b": 80 },
"background": { "r": 245, "g": 240, "b": 230 },
"background_secondary": { "r": 230, "g": 220, "b": 205 },
"status_success": { "r": 120, "g": 150, "b": 80 },
"status_info": { "r": 80, "g": 120, "b": 160 },
"status_warning": { "r": 180, "g": 130, "b": 60 },
"status_error": { "r": 160, "g": 70, "b": 50 },
"key_back": { "r": 160, "g": 70, "b": 50 },
"key_action": { "r": 120, "g": 150, "b": 80 },
"key_navigation": { "r": 80, "g": 120, "b": 160 },
"metrics_score": { "r": 140, "g": 100, "b": 60 },
"metrics_cpm_wpm": { "r": 120, "g": 150, "b": 80 },
"metrics_accuracy": { "r": 180, "g": 130, "b": 60 },
"metrics_duration": { "r": 80, "g": 120, "b": 160 },
"metrics_stage_info": { "r": 140, "g": 100, "b": 60 },
"typing_untyped_text": { "r": 100, "g": 85, "b": 70 },
"typing_typed_text": { "r": 140, "g": 100, "b": 60 },
"typing_cursor_fg": { "r": 255, "g": 255, "b": 255 },
"typing_cursor_bg": { "r": 100, "g": 70, "b": 40 },
"typing_mistake_bg": { "r": 160, "g": 70, "b": 50 }
}
}
+55
View File
@@ -0,0 +1,55 @@
{
"id": "spectral",
"name": "Spectral",
"description": "Prismatic theme with rainbow spectral highlights",
"dark": {
"border": { "r": 200, "g": 80, "b": 255 },
"title": { "r": 255, "g": 255, "b": 255 },
"text": { "r": 230, "g": 230, "b": 230 },
"text_secondary": { "r": 85, "g": 85, "b": 95 },
"background": { "r": 12, "g": 12, "b": 18 },
"background_secondary": { "r": 28, "g": 28, "b": 36 },
"status_success": { "r": 0, "g": 220, "b": 200 },
"status_info": { "r": 120, "g": 100, "b": 255 },
"status_warning": { "r": 255, "g": 160, "b": 90 },
"status_error": { "r": 255, "g": 80, "b": 180 },
"key_back": { "r": 255, "g": 80, "b": 180 },
"key_action": { "r": 0, "g": 220, "b": 200 },
"key_navigation": { "r": 0, "g": 220, "b": 200 },
"metrics_score": { "r": 120, "g": 100, "b": 255 },
"metrics_cpm_wpm": { "r": 0, "g": 220, "b": 200 },
"metrics_accuracy": { "r": 255, "g": 160, "b": 90 },
"metrics_duration": { "r": 120, "g": 100, "b": 255 },
"metrics_stage_info": { "r": 120, "g": 100, "b": 255 },
"typing_untyped_text": { "r": 150, "g": 150, "b": 170 },
"typing_typed_text": { "r": 120, "g": 100, "b": 255 },
"typing_cursor_fg": { "r": 0, "g": 0, "b": 0 },
"typing_cursor_bg": { "r": 120, "g": 100, "b": 255 },
"typing_mistake_bg": { "r": 255, "g": 80, "b": 180 }
},
"light": {
"border": { "r": 180, "g": 60, "b": 230 },
"title": { "r": 40, "g": 40, "b": 40 },
"text": { "r": 40, "g": 40, "b": 40 },
"text_secondary": { "r": 120, "g": 120, "b": 200 },
"background": { "r": 250, "g": 250, "b": 255 },
"background_secondary": { "r": 230, "g": 230, "b": 240 },
"status_success": { "r": 0, "g": 190, "b": 180 },
"status_info": { "r": 150, "g": 120, "b": 255 },
"status_warning": { "r": 255, "g": 170, "b": 110 },
"status_error": { "r": 225, "g": 90, "b": 170 },
"key_back": { "r": 225, "g": 90, "b": 170 },
"key_action": { "r": 0, "g": 190, "b": 180 },
"key_navigation": { "r": 0, "g": 190, "b": 180 },
"metrics_score": { "r": 150, "g": 120, "b": 255 },
"metrics_cpm_wpm": { "r": 0, "g": 190, "b": 180 },
"metrics_accuracy": { "r": 255, "g": 170, "b": 110 },
"metrics_duration": { "r": 150, "g": 120, "b": 255 },
"metrics_stage_info": { "r": 150, "g": 120, "b": 255 },
"typing_untyped_text": { "r": 70, "g": 80, "b": 110 },
"typing_typed_text": { "r": 150, "g": 120, "b": 255 },
"typing_cursor_fg": { "r": 255, "g": 255, "b": 255 },
"typing_cursor_bg": { "r": 150, "g": 120, "b": 255 },
"typing_mistake_bg": { "r": 225, "g": 90, "b": 170 }
}
}
+55
View File
@@ -0,0 +1,55 @@
{
"id": "starforge",
"name": "Starforge",
"description": "Cosmic smithy theme with molten gold and deep space navy",
"dark": {
"border": { "r": 255, "g": 215, "b": 100 },
"title": { "r": 255, "g": 230, "b": 150 },
"text": { "r": 240, "g": 235, "b": 220 },
"text_secondary": { "r": 105, "g": 100, "b": 95 },
"background": { "r": 15, "g": 20, "b": 35 },
"background_secondary": { "r": 30, "g": 40, "b": 60 },
"status_success": { "r": 255, "g": 200, "b": 50 },
"status_info": { "r": 100, "g": 150, "b": 255 },
"status_warning": { "r": 255, "g": 180, "b": 100 },
"status_error": { "r": 255, "g": 120, "b": 120 },
"key_back": { "r": 255, "g": 120, "b": 120 },
"key_action": { "r": 255, "g": 200, "b": 50 },
"key_navigation": { "r": 100, "g": 150, "b": 255 },
"metrics_score": { "r": 255, "g": 215, "b": 100 },
"metrics_cpm_wpm": { "r": 255, "g": 200, "b": 50 },
"metrics_accuracy": { "r": 255, "g": 180, "b": 100 },
"metrics_duration": { "r": 100, "g": 150, "b": 255 },
"metrics_stage_info": { "r": 255, "g": 215, "b": 100 },
"typing_untyped_text": { "r": 160, "g": 160, "b": 180 },
"typing_typed_text": { "r": 255, "g": 215, "b": 100 },
"typing_cursor_fg": { "r": 15, "g": 20, "b": 35 },
"typing_cursor_bg": { "r": 255, "g": 200, "b": 50 },
"typing_mistake_bg": { "r": 255, "g": 120, "b": 120 }
},
"light": {
"border": { "r": 180, "g": 140, "b": 20 },
"title": { "r": 150, "g": 100, "b": 0 },
"text": { "r": 40, "g": 50, "b": 80 },
"text_secondary": { "r": 160, "g": 120, "b": 80 },
"background": { "r": 250, "g": 248, "b": 240 },
"background_secondary": { "r": 240, "g": 235, "b": 220 },
"status_success": { "r": 200, "g": 150, "b": 30 },
"status_info": { "r": 60, "g": 100, "b": 200 },
"status_warning": { "r": 220, "g": 150, "b": 60 },
"status_error": { "r": 200, "g": 80, "b": 80 },
"key_back": { "r": 200, "g": 80, "b": 80 },
"key_action": { "r": 200, "g": 150, "b": 30 },
"key_navigation": { "r": 60, "g": 100, "b": 200 },
"metrics_score": { "r": 180, "g": 140, "b": 20 },
"metrics_cpm_wpm": { "r": 200, "g": 150, "b": 30 },
"metrics_accuracy": { "r": 220, "g": 150, "b": 60 },
"metrics_duration": { "r": 60, "g": 100, "b": 200 },
"metrics_stage_info": { "r": 180, "g": 140, "b": 20 },
"typing_untyped_text": { "r": 90, "g": 100, "b": 130 },
"typing_typed_text": { "r": 180, "g": 140, "b": 20 },
"typing_cursor_fg": { "r": 255, "g": 255, "b": 255 },
"typing_cursor_bg": { "r": 150, "g": 100, "b": 0 },
"typing_mistake_bg": { "r": 200, "g": 80, "b": 80 }
}
}
+55
View File
@@ -0,0 +1,55 @@
{
"id": "venom",
"name": "Venom",
"description": "Toxic theme with poisonous green and sinister purple",
"dark": {
"border": { "r": 120, "g": 255, "b": 120 },
"title": { "r": 220, "g": 255, "b": 220 },
"text": { "r": 210, "g": 255, "b": 210 },
"text_secondary": { "r": 105, "g": 115, "b": 105 },
"background": { "r": 10, "g": 20, "b": 10 },
"background_secondary": { "r": 30, "g": 40, "b": 30 },
"status_success": { "r": 120, "g": 255, "b": 120 },
"status_info": { "r": 160, "g": 100, "b": 255 },
"status_warning": { "r": 255, "g": 240, "b": 100 },
"status_error": { "r": 200, "g": 60, "b": 160 },
"key_back": { "r": 200, "g": 60, "b": 160 },
"key_action": { "r": 120, "g": 255, "b": 120 },
"key_navigation": { "r": 160, "g": 100, "b": 255 },
"metrics_score": { "r": 160, "g": 100, "b": 255 },
"metrics_cpm_wpm": { "r": 120, "g": 255, "b": 120 },
"metrics_accuracy": { "r": 255, "g": 240, "b": 100 },
"metrics_duration": { "r": 160, "g": 100, "b": 255 },
"metrics_stage_info": { "r": 120, "g": 255, "b": 120 },
"typing_untyped_text": { "r": 160, "g": 200, "b": 160 },
"typing_typed_text": { "r": 120, "g": 255, "b": 120 },
"typing_cursor_fg": { "r": 0, "g": 0, "b": 0 },
"typing_cursor_bg": { "r": 160, "g": 100, "b": 255 },
"typing_mistake_bg": { "r": 200, "g": 60, "b": 160 }
},
"light": {
"border": { "r": 100, "g": 220, "b": 100 },
"title": { "r": 20, "g": 60, "b": 20 },
"text": { "r": 30, "g": 90, "b": 30 },
"text_secondary": { "r": 100, "g": 180, "b": 100 },
"background": { "r": 245, "g": 255, "b": 245 },
"background_secondary": { "r": 225, "g": 245, "b": 225 },
"status_success": { "r": 100, "g": 220, "b": 100 },
"status_info": { "r": 140, "g": 80, "b": 220 },
"status_warning": { "r": 240, "g": 220, "b": 80 },
"status_error": { "r": 180, "g": 40, "b": 140 },
"key_back": { "r": 180, "g": 40, "b": 140 },
"key_action": { "r": 100, "g": 220, "b": 100 },
"key_navigation": { "r": 140, "g": 80, "b": 220 },
"metrics_score": { "r": 140, "g": 80, "b": 220 },
"metrics_cpm_wpm": { "r": 100, "g": 220, "b": 100 },
"metrics_accuracy": { "r": 240, "g": 220, "b": 80 },
"metrics_duration": { "r": 140, "g": 80, "b": 220 },
"metrics_stage_info": { "r": 100, "g": 220, "b": 100 },
"typing_untyped_text": { "r": 70, "g": 100, "b": 80 },
"typing_typed_text": { "r": 100, "g": 220, "b": 100 },
"typing_cursor_fg": { "r": 255, "g": 255, "b": 255 },
"typing_cursor_bg": { "r": 140, "g": 80, "b": 220 },
"typing_mistake_bg": { "r": 180, "g": 40, "b": 140 }
}
}
+79
View File
@@ -0,0 +1,79 @@
use criterion::{criterion_group, criterion_main, Criterion};
use gittype::domain::models::loading::StepType;
use gittype::domain::models::{CodeChunk, ExtractionOptions, Languages};
use gittype::domain::services::challenge_generator::ChallengeGenerator;
use gittype::domain::services::source_code_parser::SourceCodeParser;
use gittype::presentation::tui::screens::loading_screen::ProgressReporter;
use std::path::PathBuf;
use std::sync::{Arc, Mutex};
// Mock ProgressReporter for benchmarking
#[derive(Debug, Default)]
struct BenchProgressReporter {
#[allow(clippy::type_complexity)]
count_calls: Arc<Mutex<Vec<(StepType, usize, usize, Option<String>)>>>,
}
impl BenchProgressReporter {
fn new() -> Self {
Self::default()
}
}
impl ProgressReporter for BenchProgressReporter {
fn set_step(&self, _step_type: StepType) {}
fn set_current_file(&self, _file: Option<String>) {}
fn set_file_counts(
&self,
step_type: StepType,
processed: usize,
total: usize,
current_file: Option<String>,
) {
self.count_calls
.lock()
.unwrap()
.push((step_type, processed, total, current_file));
}
}
fn create_real_chunks_from_fixture(fixture_filename: &str) -> Vec<CodeChunk> {
let fixture_path = PathBuf::from("tests/fixtures").join(fixture_filename);
if !fixture_path.exists() {
return Vec::new();
}
let mut parser = SourceCodeParser::new().expect("Failed to create parser");
let rust_lang = Languages::from_extension("rs").expect("Failed to get Rust language");
let options = ExtractionOptions::default();
let progress = BenchProgressReporter::new();
let files_to_process = vec![(fixture_path, rust_lang)];
parser
.extract_chunks_with_progress(files_to_process, &options, &progress)
.unwrap_or_else(|_| Vec::new())
}
fn benchmark_challenge_generator(c: &mut Criterion) {
let generator = ChallengeGenerator::new();
let progress = BenchProgressReporter::new();
// Get all chunks from all fixtures
let mut all_chunks = create_real_chunks_from_fixture("complex_rust_service.rs");
all_chunks.extend(create_real_chunks_from_fixture("complex_commented_rust.rs"));
c.bench_function("challenge_generator_all_chunks", |b| {
b.iter(|| {
let challenges = generator.convert_with_progress(
std::hint::black_box(all_chunks.clone()),
std::hint::black_box(&progress),
);
std::hint::black_box(challenges)
})
});
}
criterion_group!(benches, benchmark_challenge_generator);
criterion_main!(benches);
+40
View File
@@ -0,0 +1,40 @@
use criterion::{criterion_group, criterion_main, Criterion};
use gittype::domain::models::languages::Rust;
use gittype::domain::services::source_code_parser::parsers::parse_with_thread_local;
use gittype::domain::services::source_code_parser::ChunkExtractor;
use std::path::Path;
// Load test fixture files
fn load_fixture(filename: &str) -> String {
let fixture_path = Path::new("tests/fixtures").join(filename);
std::fs::read_to_string(fixture_path)
.unwrap_or_else(|_| panic!("Failed to load fixture: {}", filename))
}
fn bench_chunk_extractor(c: &mut Criterion) {
let mut group = c.benchmark_group("chunk_extractor");
// Load real fixture data
let rust_code = load_fixture("complex_rust_service.rs");
group.bench_function("extract_chunks_from_tree", |b| {
let tree = parse_with_thread_local("rust", &rust_code).unwrap();
let file_path = Path::new("complex_rust_service.rs");
let git_root = Path::new(".");
b.iter(|| {
std::hint::black_box(ChunkExtractor::extract_chunks_from_tree(
std::hint::black_box(&tree),
std::hint::black_box(&rust_code),
std::hint::black_box(file_path),
std::hint::black_box(git_root),
std::hint::black_box(&Rust),
))
})
});
group.finish();
}
criterion_group!(benches, bench_chunk_extractor);
criterion_main!(benches);
+21
View File
@@ -0,0 +1,21 @@
fn main() {
// Tell Cargo to recompile if theme files change
println!("cargo:rerun-if-changed=assets/themes/default.json");
println!("cargo:rerun-if-changed=assets/themes/original.json");
println!("cargo:rerun-if-changed=assets/themes/ascii.json");
println!("cargo:rerun-if-changed=assets/themes/neon_abyss.json");
println!("cargo:rerun-if-changed=assets/themes/inferno.json");
println!("cargo:rerun-if-changed=assets/themes/eclipse.json");
println!("cargo:rerun-if-changed=assets/themes/glacier.json");
println!("cargo:rerun-if-changed=assets/themes/blood_oath.json");
println!("cargo:rerun-if-changed=assets/themes/oblivion.json");
println!("cargo:rerun-if-changed=assets/themes/spectral.json");
println!("cargo:rerun-if-changed=assets/themes/venom.json");
println!("cargo:rerun-if-changed=assets/themes/aurora.json");
println!("cargo:rerun-if-changed=assets/themes/cyber_void.json");
// Tell Cargo to recompile if language color files change
println!("cargo:rerun-if-changed=assets/languages/lang_dark.json");
println!("cargo:rerun-if-changed=assets/languages/lang_light.json");
println!("cargo:rerun-if-changed=assets/languages/lang_ascii.json");
}
+21
View File
@@ -0,0 +1,21 @@
coverage:
status:
project:
default:
target: 80%
threshold: 1%
patch:
default:
target: 80%
threshold: 1%
ignore:
- "tests/"
- "examples/"
- "benches/"
- "generate_all_ascii_titles.rs"
comment:
layout: "reach,diff,flags,tree"
behavior: default
require_changes: false
+255
View File
@@ -0,0 +1,255 @@
# GitType Architecture
This document describes the overall architecture and design decisions of GitType, a CLI typing game that uses source code as practice material.
## Table of Contents
- [Overview](#overview)
- [Architecture Patterns](#architecture-patterns)
- [Core Modules](#core-modules)
- [External Dependencies](#external-dependencies)
- [Design Decisions](#design-decisions)
---
## Overview
GitType follows a modular architecture with clear separation of concerns:
```
┌─────────────────────────────────────────────────────────────────┐
│ CLI │
├─────────────────────────────────────────────────────────────────┤
│ Game Engine │
├─────────────────────────────────────────────────────────────────┤
│ Models │ Extractor │ Scoring │ Storage │ Sharing │
├─────────────────────────────────────────────────────────────────┤
│ External Dependencies (tree-sitter, etc.) │
└─────────────────────────────────────────────────────────────────┘
```
### Design Principles
- **Modularity**: Each component has a single responsibility
- **Testability**: Components are easily unit tested in isolation
- **Performance**: Async processing and parallel parsing where beneficial
- **Extensibility**: Easy to add new languages and features
- **User Experience**: Responsive UI with real-time feedback
---
## Architecture Patterns
### Module Pattern
Each major feature area is organized as a separate module with clear public APIs:
- `cli`: Command-line interface and configuration.
- `models`: Core data structures for the application (Challenge, Session, etc.).
- `extractor`: Code extraction and parsing from repositories.
- `game`: Game mechanics, state management, and UI rendering.
- `scoring`: Performance metrics calculation and tracking.
- `storage`: Data persistence, session history, and database management.
- `sharing`: Exporting and sharing session data.
### Repository Pattern
The storage layer uses a repository pattern to abstract database access, making it easier to manage data entities and test business logic.
### Strategy Pattern
Language-specific extraction is handled through a strategy pattern, allowing easy extension for new programming languages using tree-sitter.
---
## Core Modules
### 1. CLI Module (`src/cli/`)
**Purpose**: Handles command-line argument parsing, configuration, and dispatching commands. It serves as the main entry point for user interaction.
### 2. Models Module (`src/models/`)
**Purpose**: Defines the core data structures used throughout the application, such as `Challenge`, `Chunk`, `Session`, and `Stage`. This module ensures a consistent data model across different parts of the system.
### 3. Extractor Module (`src/extractor/`)
**Purpose**: Responsible for finding and parsing source code files from a given repository. It uses `tree-sitter` to analyze the code and extract meaningful chunks (like functions and classes) that can be converted into typing challenges.
### 4. Game Module (`src/game/`)
**Purpose**: Manages the entire game lifecycle, including the title screen, loading, countdown, the typing challenge itself, and results screens. It handles user input, manages game state, and renders the UI to the terminal.
### 5. Scoring Module (`src/scoring/`)
**Purpose**: Calculates and tracks user performance. It is divided into sub-modules for real-time scoring during a typing session (`calculator`) and for tracking statistics across stages and sessions (`tracker`).
### 6. Storage Module (`src/storage/`)
**Purpose**: Manages data persistence using SQLite. It uses a repository pattern (`repositories`) and DAOs (`daos`) to handle the storage and retrieval of session history, user statistics, and repository metadata. It also includes database migrations.
### 7. Sharing Module (`src/sharing.rs`)
**Purpose**: Provides functionality to share or export user results and session data.
---
## External Dependencies
### Core Dependencies
| Dependency | Purpose | Usage |
|------------|---------|-------|
| `tree-sitter` | Code parsing | Extract functions, classes from source files |
| `ratatui` | Terminal UI | Render game interface |
| `crossterm` | Terminal control | Handle input, colors, cursor positioning |
| `clap` | CLI parsing | Command-line argument handling |
| `rusqlite` | Database | Store session history and statistics |
| `rayon` | Parallelism | Parallel file processing |
### Language-Specific
| Language | Tree-sitter Grammar | Status |
|----------|-------------------|---------|
| Rust | `tree-sitter-rust` | ✅ Full support |
| TypeScript | `tree-sitter-typescript` | ✅ Full support |
| Python | `tree-sitter-python` | ✅ Full support |
| Go | `tree-sitter-go` | ✅ Full support |
| Ruby | `tree-sitter-ruby` | ✅ Full support (includes class methods, singleton methods, attr_accessor) |
| Swift | `tree-sitter-swift` | ✅ Full support |
---
## Design Decisions
### 1. Tree-sitter for Code Parsing
**Why**: Provides accurate, language-aware parsing that understands code structure rather than just text patterns.
**Benefits**:
- Consistent extraction across languages
- Accurate function/class boundaries
- Syntax highlighting support
- Extensible to new languages
### 2. Terminal UI with Ratatui
**Why**: Provides rich, responsive terminal interface without requiring GUI dependencies.
**Benefits**:
- Cross-platform compatibility
- Low resource usage
- Professional appearance
- Real-time updates
### 3. SQLite for Storage
**Why**: Local, file-based database that requires no setup.
**Benefits**:
- No external database server needed
- ACID transactions
- SQL query capabilities
- Portable data files
### 4. Modular Architecture
**Why**: Separation of concerns makes the codebase maintainable and testable.
**Benefits**:
- Easy to add new features
- Component-level testing
- Clear interfaces
- Parallel development
---
## Performance Considerations
### 1. Parallel Processing
```rust
// Parallel file processing with rayon
files.par_iter()
.map(|file| extract_chunks(file))
.collect()
```
**Benefits**:
- Faster repository scanning
- Efficient multi-core usage
- Better user experience for large codebases
### 2. Lazy Loading
- Code chunks are processed on-demand
- UI screens are rendered only when needed
- Database connections are managed efficiently
### 3. Memory Management
- Streaming file processing for large repositories
- Efficient string handling for code content
- Bounded memory usage regardless of repository size
### 4. Caching Strategy
- Parsed code chunks can be cached between sessions
- Git repository metadata is cached
- Display rendering optimizations
---
## Extension Points
### Adding New Languages
1. Add tree-sitter grammar dependency
2. Implement language-specific queries
3. Update `Language` enum
4. Add language detection logic
5. Include tests and documentation
### Adding New Features
1. **Game Modes**: Extend `GameMode` enum and `StageBuilder`
2. **Scoring Systems**: Add new metrics to `ScoringEngine`
3. **Display Options**: Implement new `Display` trait variants
4. **Export Formats**: Extend `sharing` module
### Adding New Screens
1. Implement `Screen` trait
2. Add to `screens` module
3. Update `StageManager` flow
4. Add navigation logic
---
## Testing Strategy
### Unit Tests
- Each module has comprehensive unit tests
- Mock dependencies for isolated testing
- Property-based testing for parsers
### Integration Tests
- End-to-end CLI testing
- Database integration tests
- Multi-language extraction tests
### Performance Tests
- Benchmarks for parsing large codebases
- Memory usage profiling
- UI responsiveness testing
---
## Future Architecture Considerations
### Planned Improvements
1. **Plugin System**: Allow external language support
2. **Remote Storage**: Cloud-based session synchronization
3. **Multi-Player**: Real-time competitive typing
4. **Analytics**: Advanced performance insights
5. **Web Interface**: Browser-based version
### Scalability
- Modular design supports horizontal feature additions
- Clear interfaces enable component replacement
- Performance optimizations can be added incrementally
---
This architecture provides a solid foundation for GitType's current features while maintaining flexibility for future enhancements.
+483
View File
@@ -0,0 +1,483 @@
# Contributing to GitType
Thank you for your interest in contributing to GitType! This document provides guidelines and information for contributors.
## Table of Contents
- [Code of Conduct](#code-of-conduct)
- [Getting Started](#getting-started)
- [Development Setup](#development-setup)
- [Project Structure](#project-structure)
- [Making Changes](#making-changes)
- [Testing](#testing)
- [Submitting Changes](#submitting-changes)
- [Coding Standards](#coding-standards)
- [Adding Language Support](#adding-language-support)
---
## Code of Conduct
This project follows a standard code of conduct. Please be respectful and constructive in all interactions.
---
## Getting Started
### Prerequisites
- Rust 1.70 or later
- Git
- Basic familiarity with Rust and CLI development
> When developing in a [Nix](https://nixos.org/) environment: `nix develop` will create a development shell with all dependencies.
### Development Setup
1. **Fork and clone the repository:**
```bash
git clone https://github.com/YOUR_USERNAME/gittype.git
cd gittype
```
2. **Set up the development environment:**
```bash
# Install dependencies and build
cargo build
# Run tests to ensure everything works
cargo test
# Try running the application
cargo run -- --help
```
3. **Create a development branch:**
```bash
git checkout -b feature/your-feature-name
```
---
## Project Structure
```
gittype/
├── src/
│ ├── main.rs # CLI entry point
│ ├── lib.rs # Library root
│ ├── extractor/ # Code extraction logic
│ │ ├── mod.rs # Main extractor interface
│ │ ├── repository.rs # Repository loading
│ │ └── languages/ # Language-specific parsers
│ ├── game/ # Game logic and UI
│ │ ├── mod.rs # Game orchestration
│ │ ├── stage_manager.rs # Stage management
│ │ ├── scoring/ # Scoring system
│ │ └── screens/ # UI screens
│ └── database/ # Session storage
├── tests/ # Integration tests
├── Cargo.toml # Project configuration
└── README.md # Project documentation
```
### Key Components
- **Extractor**: Handles parsing source code using tree-sitter
- **Game**: Manages typing challenges and user interface
- **Database**: Stores session history and statistics
- **Scoring**: Calculates accuracy, speed, and other metrics
---
## Making Changes
### Types of Contributions
1. **Bug Fixes**: Fix issues in existing functionality
2. **Features**: Add new functionality or improve existing features
3. **Performance**: Optimize code performance
4. **Documentation**: Improve or add documentation
5. **Language Support**: Add support for new programming languages
### Before You Start
1. Check existing issues and PRs to avoid duplication
2. For major features, consider opening an issue first to discuss
3. Ensure your changes align with the project goals
---
## Testing
### Running Tests
```bash
# Run all tests
cargo test
# Run with output
cargo test -- --nocapture
# Run specific test
cargo test test_name
# Run integration tests
cargo test --test integration_tests
```
### Generating Test Coverage
To generate a test coverage report, you'll need `cargo-llvm-cov`.
1. **Install `cargo-llvm-cov`:**
```bash
cargo install cargo-llvm-cov
```
2. **Generate the report:**
```bash
cargo llvm-cov --all-features --workspace --lcov --output-path lcov.info
```
This will create an `lcov.info` file which can be used by coverage visualization tools.
### Test Coverage
- Unit tests for core logic components
- Integration tests for CLI functionality
- Test files in various languages for extractor testing
### Writing Tests
- Add unit tests for new functions
- Include integration tests for CLI features
- Test edge cases and error conditions
- Use descriptive test names
Example:
```rust
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_extract_rust_functions() {
let code = r#"
fn example_function() {
println!("Hello, world!");
}
"#;
let extractor = RustExtractor::new();
let chunks = extractor.extract(code).unwrap();
assert_eq!(chunks.len(), 1);
assert_eq!(chunks[0].function_name(), "example_function");
}
}
```
---
## Submitting Changes
### Pull Request Process
1. **Ensure your branch is up to date:**
```bash
git checkout main
git pull upstream main
git checkout your-branch
git rebase main
```
2. **Run the full test suite and checks:**
```bash
cargo test
cargo clippy --all-targets --all-features -- -D warnings
cargo fmt --all -- --check
```
3. **Commit your changes:**
```bash
git add .
git commit -m "type: description of changes"
```
4. **Push to your fork:**
```bash
git push origin your-branch
```
5. **Create a pull request:**
- Use a descriptive title
- Explain what changes were made and why
- Reference any related issues
- Include screenshots for UI changes
### Commit Message Format
Use conventional commit format:
```
type(scope): description
body (optional)
footer (optional)
```
Types:
- `feat`: New feature
- `fix`: Bug fix
- `docs`: Documentation changes
- `style`: Code style changes (formatting, etc.)
- `refactor`: Code refactoring
- `perf`: Performance improvements
- `test`: Adding or updating tests
- `chore`: Maintenance tasks
Examples:
- `feat: add support for Go language extraction`
- `fix: handle empty files in extractor`
- `docs: update installation instructions`
---
## Coding Standards
### Rust Style
- Follow the [Rust Style Guide](https://doc.rust-lang.org/nightly/style-guide/)
- Use `rustfmt` for formatting: `cargo fmt`
- Use `clippy` for linting: `cargo clippy`
- Write descriptive variable and function names
- Add documentation comments for public APIs
### Code Quality
- Keep functions focused and reasonably sized
- Use meaningful error types and messages
- Handle all error cases appropriately
- Avoid unwrap() in production code - use proper error handling
- Write self-documenting code with good naming
### Example Code Style
```rust
/// Extracts code chunks from a source file
pub fn extract_chunks(
file_path: &Path,
language: Language,
) -> Result<Vec<CodeChunk>, ExtractionError> {
let content = std::fs::read_to_string(file_path)
.map_err(|e| ExtractionError::FileRead(e))?;
let parser = create_parser(language)?;
let tree = parser.parse(&content, None)
.ok_or(ExtractionError::ParseFailed)?;
extract_from_tree(&tree, &content)
}
```
---
## Adding Language Support
### Steps to Add a New Language
1. **Add the tree-sitter dependency:**
```toml
# In Cargo.toml
tree-sitter-newlang = "0.20"
```
2. **Create language definition:**
```rust
// src/domain/models/languages/newlang.rs
use crate::domain::models::Language;
use crate::presentation::ui::Colors;
use std::hash::Hash;
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
pub struct NewLang;
impl Language for NewLang {
fn name(&self) -> &'static str {
"newlang"
}
fn extensions(&self) -> Vec<&'static str> {
vec!["nl"]
}
fn color(&self) -> ratatui::style::Color {
Colors::lang_newlang()
}
fn display_name(&self) -> &'static str {
"NewLang"
}
fn is_valid_comment_node(&self, node: tree_sitter::Node) -> bool {
node.kind() == "comment"
}
}
```
3. **Create language-specific extractor:**
```rust
// src/domain/services/source_code_parser/parsers/newlang.rs
use super::LanguageExtractor;
use crate::domain::models::ChunkType;
use crate::{GitTypeError, Result};
use tree_sitter::{Node, Parser};
pub struct NewLangExtractor;
impl LanguageExtractor for NewLangExtractor {
fn tree_sitter_language(&self) -> tree_sitter::Language {
tree_sitter_newlang::LANGUAGE.into()
}
fn query_patterns(&self) -> &str {
"(function_declaration name: (identifier) @name) @function"
}
fn comment_query(&self) -> &str {
"(comment) @comment"
}
fn capture_name_to_chunk_type(&self, capture_name: &str) -> Option<ChunkType> {
match capture_name {
"function" => Some(ChunkType::Function),
_ => None,
}
}
fn extract_name(&self, node: Node, source_code: &str, _capture_name: &str) -> Option<String> {
// Extract identifier name from AST node
None
}
fn middle_implementation_query(&self) -> &str {
"" // Optional: for extracting code blocks within functions
}
fn middle_capture_name_to_chunk_type(&self, _capture_name: &str) -> Option<ChunkType> {
None
}
}
impl NewLangExtractor {
pub fn create_parser() -> Result<Parser> {
let mut parser = Parser::new();
parser.set_language(&tree_sitter_newlang::LANGUAGE.into())
.map_err(|e| GitTypeError::ExtractionFailed(format!("Failed to set NewLang language: {}", e)))?;
Ok(parser)
}
}
```
4. **Register the language:**
```rust
// In src/domain/models/languages/mod.rs
pub mod newlang;
pub use newlang::NewLang;
// In src/domain/models/language.rs
pub fn all_languages() -> Vec<Box<dyn Language>> {
vec![
// ... other languages
Box::new(NewLang),
]
}
// In src/domain/services/source_code_parser/parsers/mod.rs
pub mod newlang;
register_language!(NewLang, newlang, NewLangExtractor);
```
5. **Add color scheme support:**
```rust
// In src/domain/models/color_scheme.rs - add field:
pub lang_newlang: SerializableColor,
// In src/presentation/ui/colors.rs - add method:
pub fn lang_newlang() -> Color {
Self::get_color_scheme().lang_newlang.into()
}
// Add to theme JSON files:
// assets/languages/lang_dark.json
// assets/languages/lang_light.json
// assets/languages/lang_ascii.json
"lang_newlang": {"r": 123, "g": 45, "b": 67}
```
6. **Add tests:**
```rust
// tests/integration/languages/newlang/extractor.rs
use crate::integration::languages::extractor::test_language_extractor;
test_language_extractor! {
name: test_newlang_function_extraction,
language: "newlang",
extension: "nl",
source: r#"
function hello() {
print("Hello")
}
"#,
total_chunks: 2,
chunk_counts: {
File: 1,
Function: 1,
}
}
```
7. **Update documentation:**
- Add to `README.md` supported languages list
- Add to `docs/supported-languages.md` with extraction features
- Update CLI help in `src/presentation/cli/args.rs`
- Add example code if needed
### Query Writing Guidelines
- Focus on extracting meaningful code units (functions, classes, methods)
- Ensure extracted chunks are self-contained
- Test queries with various code samples
- Consider edge cases and complex syntax
---
## Release Distribution
GitType is distributed through several package managers. Most channels are updated automatically by the release workflow (`.github/workflows/release.yml`); a few require one-time setup.
### winget (Windows)
The release workflow submits a manifest PR to [`microsoft/winget-pkgs`](https://github.com/microsoft/winget-pkgs) via [`vedantmgoyal9/winget-releaser`](https://github.com/vedantmgoyal9/winget-releaser) on every release.
**One-time setup:**
1. The initial `0.10.0` manifest lives under `.winget/manifests/u/unhappychoice/gittype/0.10.0/` and must be PR'd manually to `microsoft/winget-pkgs` once. After it is merged, subsequent versions are added automatically.
2. Create a fork of `microsoft/winget-pkgs` under the owner account.
3. Create a GitHub PAT (classic) with `public_repo` scope that can push to the fork, and register it as the `WINGET_TOKEN` repository secret.
**Package identifier:** `unhappychoice.gittype`
---
## Getting Help
- **Issues**: Browse existing issues or create a new one
- **Discussions**: Use GitHub Discussions for questions
- **Documentation**: Check the README and code comments
## Recognition
Contributors will be acknowledged in:
- CONTRIBUTORS.md file
- Release notes for significant contributions
- Repository insights and statistics
Thank you for contributing to GitType! 🚀
+212
View File
@@ -0,0 +1,212 @@
# GitType Manual Test Checklist
## 1. Title Screen
### Difficulty Selection
- [x] `←`/`h` to select left difficulty
- [x] `→`/`l` to select right difficulty
- [x] Shows 5 difficulties (Easy, Normal, Hard, Wild, Zen)
### Game Start
- [x] Space key starts game
- [x] Shows error when no challenges available
### Menu Navigation
- [x] `R` opens records screen
- [x] `A` opens analytics screen
- [x] `S` opens settings screen
- [x] `I`/`?` opens help screen
- [x] `Esc` exits application
- [x] `Ctrl+C` exits application
---
## 2. Typing Screen
### Before Start
- [x] Space starts countdown
- [x] `Esc` shows dialog
- [x] `S` in dialog skips stage
- [x] `Q` in dialog goes to failure screen
- [x] `Esc` in dialog closes it
### During Typing
- [x] Code content displays correctly
- [x] Mistakes highlighted in red
- [x] Cursor position accurate
- [x] Real-time stats display
### Stage Progression
- [x] Challenge completion advances to next stage
- [x] All stages complete goes to summary
---
## 3. Stage Summary Screen
- [x] Stage number displays
- [x] Score/WPM/Accuracy displays
- [x] Space advances to next stage
- [x] `R` restarts stage
---
## 4. Session Summary Screen
### Display
- [x] Total score displays
- [x] WPM/Accuracy displays
- [x] Rank displays
### Navigation
- [x] `R` retries session
- [x] `T` returns to title
- [x] `S` opens share screen
- [x] `D` opens details dialog
- [x] `Esc` exits
---
## 5. Session Share Screen
- [x] Result preview displays
- [x] `1` shares to X (Twitter)
- [x] `2` shares to Reddit
- [x]`3` shares to LinkedIn
- [x] `4` shares to Facebook
- [x] `Esc` returns back
---
## 6. Total Summary Screen
- [x] Cumulative stats display
- [x] Total score/keystrokes display
- [x] `S` opens share screen
- [x] `Esc` returns to title
---
## 7. Total Summary Share Screen (Bug Fixed)
- [x] **Score displays correctly** ← Bug fix
- [x] `1`-`4` shares to platforms
- [x] `Esc` returns back
---
## 8. Records Screen
### Filters
- [x] Period filter works (All/7d/30d/90d)
- [x] Sort toggle works (Date/Score/Repo/Duration)
- [x] Asc/Desc toggle works
### Navigation
- [x] `↑`/`↓` moves through list
- [x] Enter opens detail screen
- [x] `Esc` returns to title
---
## 9. Session Detail Screen
- [x] Session info displays
- [x] Each stage result displays
- [x] `Esc` returns back
---
## 10. Analytics Screen
### View Toggle
- [x] `←`/`→` switches views
- [x] Overview/Trends/Repositories/Languages
### Navigation
- [x] `↑`/`↓` moves through list
- [x] `Esc` returns to title
---
## 11. Settings Screen
### Color Mode
- [x] Dark/Light toggle works
- [x] Preview updates immediately
### Theme
- [x] Theme list displays
- [x] Theme selection previews
- [x] Enter saves settings
- [x] `Esc` cancels changes
---
## 12. Help Screen
- [x] All sections display
- [x] `←`/`→` switches sections
- [x] `↑`/`↓` scrolls content
- [x] `Esc` closes screen
---
## 13. Loading Screen
- [x] Progress displays
- [x] File names display
- [x] Spinner animates
- [x] `Ctrl+C` cancels
---
## 14. Trending Feature
### Language Selection
- [x] Language list displays
- [x] `↑`/`↓` selects
- [x] Enter confirms
### Repository Selection
- [x] Trending repos display
- [x] `↑`/`↓` selects
- [x] Enter starts game
---
## 15. Repository Management
- [x] `gittype repo list` shows list
- [x] `gittype repo play` shows selection
- [x] `gittype repo clear` clears cache
---
## 16. Cache Feature (Bug Fixed)
- [x] **Challenge cache saves to `.config/cache/`** ← Bug fix
- [x] **Trending cache saves to `.config/cache/`** ← Bug fix
- [x] Cache reuse speeds up loading
- [x] `gittype cache stats` shows stats
- [x] `gittype cache clear` clears cache
---
## 17. CLI Commands
- [x] `gittype` uses current directory
- [x] `gittype /path` uses specified path
- [x] `gittype --repo owner/repo` clones GitHub repo
- [x] `gittype --langs rust,python` filters languages
- [x] `gittype trending` opens trending
- [x] `gittype --help` shows help
- [x] `gittype --version` shows version
---
## 18. Error Handling
- [x] Invalid repo shows error
- [x] Non-existent path shows error
- [x] Network error shows appropriate message
+62
View File
@@ -0,0 +1,62 @@
# GitType Banner Images
This directory contains the banner images used in the project.
## Files
- `gittype-banner.png` - Main banner image (1200x630px, OGP-ready)
- `gittype-banner.svg` - Vector version of the banner
- `banner-source.sh` - Shell script that generates the banner content
## Regenerating the Banner
To regenerate the banner images, use the following commands:
### Dependencies
```bash
npm install -g oh-my-logo
go install github.com/charmbracelet/freeze@latest
```
### PNG Banner (1200x630px - OGP ready)
```bash
freeze --execute "docs/images/banner-source.sh" \
-o docs/images/gittype-banner.png \
--theme "Dracula" \
--window \
--background="#1e1e2e" \
--margin 10 \
--padding 40 \
--border.radius 15 \
--font.family "Ubuntu Mono" \
--font.size 22 \
--width 1200 \
--height 630
```
### SVG Banner
```bash
freeze --execute "docs/images/banner-source.sh" \
-o docs/images/gittype-banner.svg \
--theme "Dracula" \
--window \
--background="#1e1e2e" \
--margin 10 \
--padding 40 \
--border.radius 15 \
--font.family "Ubuntu Mono" \
--font.size 22 \
--width 1200 \
--height 630
```
## Banner Design
The banner features:
- GitType ASCII logo with purple gradient generated by `oh-my-logo`
- Project tagline: "Show your AI who's boss: just you, your keyboard, and your coding sins"
- Feature highlights with ASCII icons: [*] Addictive gameplay [>] Real-time feedback [+] Track your progress
- Installation commands for brew and usage
- GitHub repository URL
- Terminal-style appearance with window controls and dark Dracula theme background
- Ubuntu Mono font for optimal readability and emoji compatibility
Binary file not shown.

After

Width:  |  Height:  |  Size: 7.6 MiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 7.6 MiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 7.6 MiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 7.5 MiB

+22
View File
@@ -0,0 +1,22 @@
#!/bin/bash
# GitType Banner Generator Script
# This script generates the GitType banner image using freeze
export FORCE_COLOR=1
oh-my-logo "GitType" purple
echo
echo '"Show your AI who'\''s boss: just you, your keyboard,'
echo ' and your coding sins"'
echo
echo "────────────────────────────────────────────────"
echo
echo "Turn your own source code into typing challenges"
echo
echo "[*] Addictive gameplay [>] Real-time feedback [+] Track your progress"
echo
echo "github.com/unhappychoice/gittype"
# To regenerate the banner:
# chmod +x docs/images/banner-source.sh
# freeze --execute "docs/images/banner-source.sh" -o docs/images/gittype-banner.svg \
# --theme "Dracula" --window --background="#1e1e2e" --margin 20 --padding 20 --border.radius 15
Binary file not shown.

After

Width:  |  Height:  |  Size: 9.1 MiB

Binary file not shown.
Binary file not shown.

After

Width:  |  Height:  |  Size: 7.4 MiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 64 KiB

File diff suppressed because one or more lines are too long

After

Width:  |  Height:  |  Size: 13 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 7.7 MiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 7.9 MiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 7.5 MiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 7.6 MiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 7.4 MiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 7.5 MiB

+68
View File
@@ -0,0 +1,68 @@
# Installation Guide
## Prerequisites
- Rust 1.70 or later
- Git (for repository access)
## Install from Source
```bash
# Clone the repository
git clone https://github.com/unhappychoice/gittype.git
cd gittype
# Build and install
cargo build --release
cargo install --path .
```
## Install from Cargo
```bash
cargo install gittype
```
## Install from Homebrew
```bash
brew install gittype
```
## Verify Installation
```bash
gittype --version
```
## Local Data Storage
`gittype` stores its data, including the local database and log files, in the `~/.gittype/` directory.
- **Database**: `~/.gittype/gittype.db` - Contains your typing history, scores, and other session data.
- **Logs**: `~/.gittype/gittype.log` - Used for debugging and monitoring.
You can safely remove this directory if you want to reset all your data.
## Troubleshooting
### Common Issues
1. **Rust version too old**
```bash
rustup update stable
```
2. **Permission denied**
```bash
# On macOS/Linux, ensure cargo bin is in PATH
echo 'export PATH="$HOME/.cargo/bin:$PATH"' >> ~/.bashrc
source ~/.bashrc
```
3. **Build failures**
```bash
# Clean and rebuild
cargo clean
cargo build --release
```
+74
View File
@@ -0,0 +1,74 @@
# Playing GitType
## Scoring System
### Score Calculation Formula
```
Base Score = CPM × (Accuracy / 100) × 10
```
### Metrics Explained
- **CPM** (Characters Per Minute): Total characters typed / minutes
- **WPM** (Words Per Minute): CPM / 5 (average word length)
- **Accuracy**: (Total chars - Mistakes) / Total chars × 100%
### Bonuses & Penalties
- **Consistency Bonus**: Up to 70% extra for high accuracy
- **Time Bonus**: Extra points for fast completion
- **Mistake Penalty**: -5 points per error
```
Final Score = (Base + Consistency + Time - Penalties) × 2 + 100
```
## Game Modes
### Standard Mode
Type code from popular repositories with different difficulty levels:
- **Easy**: Simple code constructs
- **Normal**: Balanced complexity
- **Hard**: Complex patterns and syntax
- **Wild**: Challenging edge cases
- **Zen**: Relaxed, no time pressure
## Challenge Flow
1. **Title Screen**: Welcome and instructions
2. **Loading Screen**: Extracting code chunks from repository
3. **Countdown**: 3-2-1 start timer
4. **Typing Challenge**: Type the displayed code
5. **Results**: View performance metrics and score
6. **Next Challenge**: Continue to next stage
## Code Challenge Types
GitType extracts real code constructs from repositories:
- **Functions, methods, and procedures**
- **Classes, structs, and interfaces**
- **Enums, traits, and type definitions**
- **Variables, constants, and modules**
- **React components and namespaces**
- **Control flow** (loops, conditionals)
## Rank System
### Rank Tiers
- **Beginner**: Fresh developers learning the basics
- **Intermediate**: Solid developers with growing skills
- **Advanced**: Senior developers mastering their craft
- **Expert**: Elite developers becoming legendary
- **Legendary**: Mythical entities beyond comprehension
### Rank Progression
Progress through ranks by achieving higher scores. Each rank requires a minimum score threshold. The highest ranks remain mysterious until achieved!
## Help System
Press **I** or **?** from the title screen to access the in-game help system with detailed information about:
- CLI usage and commands
- Scoring system details
- Complete rank listings
- Game controls and tips
- About and community information
+231
View File
@@ -0,0 +1,231 @@
# Supported Languages
## Current Support
| Language | Extensions | Aliases | Tree-sitter Grammar |
|----------|------------|---------|-------------------|
| C | `.c`, `.h` | - | `tree_sitter_c` |
| C# | `.cs`, `.csx` | `cs`, `c#` | `tree_sitter_c_sharp` |
| C++ | `.cpp`, `.cc`, `.cxx`, `.hpp` | `c++` | `tree_sitter_cpp` |
| Clojure | `.clj`, `.cljs`, `.cljc` | - | `tree_sitter_clojure` |
| Dart | `.dart` | - | `tree_sitter_dart` |
| Elixir | `.ex`, `.exs` | `ex`, `exs` | `tree_sitter_elixir` |
| Erlang | `.erl`, `.hrl` | `erl` | `tree_sitter_erlang` |
| Go | `.go` | - | `tree_sitter_go` |
| Haskell | `.hs`, `.lhs` | `hs` | `tree_sitter_haskell` |
| Java | `.java` | - | `tree_sitter_java` |
| JavaScript | `.js`, `.jsx`, `.mjs`, `.cjs` | `js` | `tree_sitter_javascript` |
| Kotlin | `.kt`, `.kts` | `kt` | `tree_sitter_kotlin_ng` |
| PHP | `.php`, `.phtml`, `.php3`, `.php4`, `.php5` | - | `tree_sitter_php` |
| Python | `.py` | `py` | `tree_sitter_python` |
| Ruby | `.rb` | `rb` | `tree_sitter_ruby` |
| Rust | `.rs` | `rs` | `tree_sitter_rust` |
| Scala | `.sc`, `.scala` | `sc` | `tree_sitter_scala` |
| Swift | `.swift` | - | `tree_sitter_swift` |
| TypeScript | `.ts`, `.tsx` | `ts` | `tree_sitter_typescript` (TSX) |
| Zig | `.zig` | - | `tree_sitter_zig` |
## Extraction Features
### C
- **Functions** (`function_definition`) - Function definitions
- **Structs** (`struct_specifier`) - Struct definitions
- **Unions** (`union_specifier`) - Union definitions
- **Enums** (`enum_specifier`) - Enum definitions
- **Type Definitions** (`typedef`) - Type alias definitions
### C#
- **Classes** (`class_declaration`) - Class definitions
- **Structs** (`struct_declaration`) - Struct definitions
- **Interfaces** (`interface_declaration`) - Interface definitions
- **Enums** (`enum_declaration`) - Enum definitions
- **Records** (`record_declaration`) - Record definitions
- **Methods** (`method_declaration`) - Method definitions
- **Constructors** (`constructor_declaration`) - Constructor definitions
- **Destructors** (`destructor_declaration`) - Destructor definitions
- **Properties** (`property_declaration`) - Property definitions
- **Events** (`event_declaration`) - Event definitions
- **Delegates** (`delegate_declaration`) - Delegate definitions
- **Namespaces** (`namespace_declaration`) - Namespace definitions
### C++
- **Functions** (`function_definition`) - Function definitions
- **Methods** (`function_definition`) - Method definitions
- **Classes** (`class_specifier`) - Class definitions
- **Structs** (`struct_specifier`) - Struct definitions
- **Templates** (`template_declaration`) - Template definitions
- **Namespaces** (`namespace_definition`) - Namespace definitions
### Clojure
- **Functions** (`defn`, `defmacro`, `defn-`) - Function and macro definitions
- **Variables** (`def`) - Variable definitions
- **Namespaces** (`ns`) - Namespace declarations
- **Classes** (`deftype`, `defrecord`) - Type and record definitions
- **Interfaces** (`defprotocol`) - Protocol definitions
### Dart
- **Functions** (`function_signature`) - Function definitions
- **Methods** (`method_signature`) - Method definitions
- **Classes** (`class_definition`) - Class definitions
- **Enums** (`enum_declaration`) - Enum declarations
- **Extensions** (`extension_declaration`) - Extension definitions
- **Mixins** (`mixin_declaration`) - Mixin definitions
### Elixir
- **Functions** (`def`, `defp`) - Public and private function definitions
- **Modules** (`defmodule`) - Module definitions
- **Macros** (`defmacro`, `defmacrop`) - Macro definitions
- **Protocols** (`defprotocol`) - Protocol definitions
- **Implementations** (`defimpl`) - Protocol implementation blocks
- **Structs** (`defstruct`) - Struct definitions
- **Guards** (`defguard`, `defguardp`) - Guard definitions
### Erlang
- **Functions** (`function_clause`) - Function clause definitions
- **Module Attributes** (`-module`) - Module declarations
- **Export Attributes** (`-export`) - Export declarations
- **Records** (`-record`) - Record definitions
- **Type Definitions** (`-type`) - Type specifications
- **Specs** (`-spec`) - Function specifications
- **Behaviours** (`-behaviour`) - Behaviour declarations
### Go
- **Functions** (`function_declaration`) - Function definitions
- **Methods** (`method_declaration`) - Method definitions with receivers
- **Structs** (`type_spec` with `struct_type`) - Struct type definitions
- **Interfaces** (`type_spec` with `interface_type`) - Interface type definitions
- **Constants** (`const_declaration`) - Constant declarations
- **Variables** (`var_declaration`) - Variable declarations
- **Type Aliases** (`type_spec`) - Type alias definitions
### Haskell
- **Functions** (`function_declaration`) - Function definitions
- **Type Signatures** (`type_signature`) - Type signature declarations
- **Data Types** (`data_type`) - Data type definitions
- **Type Classes** (`class_declaration`) - Type class definitions
- **Instances** (`instance_declaration`) - Instance definitions
### Java
- **Classes** (`class_declaration`) - Class definitions
- **Interfaces** (`interface_declaration`) - Interface definitions
- **Methods** (`method_declaration`) - Method definitions
- **Constructors** (`constructor_declaration`) - Constructor definitions
- **Enums** (`enum_declaration`) - Enum definitions
- **Records** (`record_declaration`) - Record definitions (Java 14+)
- **Annotation Types** (`annotation_type_declaration`) - Annotation type definitions
- **Fields** (`field_declaration`) - Field declarations
### JavaScript
- **Functions** (`function_declaration`) - Function declarations
- **Methods** (`method_definition`) - Object and class methods
- **Classes** (`class_declaration`) - ES6+ class definitions
- **Arrow Functions** (`arrow_function`) - Arrow function expressions
- **Function Expressions** (`function_expression`) - Function expression assignments
- **JSX Elements** (`jsx_element`, `jsx_self_closing_element`) - React components
### Kotlin
- **Functions** (`function_declaration`) - Function definitions
- **Classes** (`class_declaration`) - Class definitions
- **Objects** (`object_declaration`) - Object declarations
- **Properties** (`property_declaration`) - Property definitions
- **Enums** (`enum_class_body`) - Enum class definitions
### PHP
- **Functions** (`function_definition`) - Function definitions
- **Methods** (`method_declaration`) - Method definitions
- **Classes** (`class_declaration`) - Class definitions
- **Interfaces** (`interface_declaration`) - Interface definitions
- **Traits** (`trait_declaration`) - Trait definitions
- **Namespaces** (`namespace_definition`) - Namespace definitions
### Python
- **Functions** (`function_definition`) - Function definitions with decorators
- **Classes** (`class_definition`) - Class definitions with methods and inheritance
### Ruby
- **Instance Methods** (`method`) - Instance method definitions
- **Class Methods** (`singleton_method`) - Class method definitions
- **Classes** (`class`) - Class definitions
- **Modules** (`module`) - Module definitions
### Rust
- **Functions** (`function_item`) - Function definitions with parameters and body
- **Implementations** (`impl_item`) - Implementation blocks for structs/traits
- **Structs** (`struct_item`) - Struct definitions with fields
- **Enums** (`enum_item`) - Enum definitions with variants
- **Traits** (`trait_item`) - Trait definitions with associated functions
- **Modules** (`mod_item`) - Module declarations and definitions
- **Type Aliases** (`type_item`) - Type alias declarations
### Scala
- **Functions** (`function_definition`) - Function definitions
- **Classes** (`class_definition`) - Class definitions
- **Objects** (`object_definition`) - Object definitions
- **Traits** (`trait_definition`) - Trait definitions
- **Type Definitions** (`type_definition`) - Type definitions
### Swift
- **Functions** (`function_declaration`) - Function definitions
- **Classes** (`class_declaration`) - Class definitions
- **Structs** (`struct_declaration`) - Struct definitions
- **Enums** (`enum_declaration`) - Enum definitions
- **Protocols** (`protocol_declaration`) - Protocol definitions
- **Extensions** (`extension_declaration`) - Extension definitions
- **Initializers** (`init_declaration`) - Initializer definitions
- **Methods** (`function_declaration`) - Method definitions
### TypeScript
- **Functions** (`function_declaration`) - Function declarations
- **Methods** (`method_definition`) - Class and object methods
- **Classes** (`class_declaration`) - Class definitions with constructors and methods
- **Arrow Functions** (`arrow_function`) - Arrow function expressions
- **Function Expressions** (`function_expression`) - Function expression assignments
- **Interfaces** (`interface_declaration`) - Interface definitions
- **Type Aliases** (`type_alias_declaration`) - Type alias definitions
- **Enums** (`enum_declaration`) - Enum declarations
- **Namespaces** (`internal_module`) - Namespace declarations
- **JSX Elements** (`jsx_element`, `jsx_self_closing_element`) - React components
### Zig
- **Functions** (`function_declaration`) - Function definitions
- **Structs** (`variable_declaration` with `struct_declaration`) - Struct type definitions
- **Enums** (`variable_declaration` with `enum_declaration`) - Enum type definitions
- **Unions** (`variable_declaration` with `union_declaration`) - Union type definitions
## Language-Specific Options
### Filtering by Language
```bash
# Single language
gittype --langs rust
# Multiple languages
gittype --langs rust,typescript,javascript,python
```
### Configuration File
```toml
[default]
langs = ["rust", "typescript", "javascript", "python", "go", "ruby", "swift", "kotlin", "java", "php", "csharp", "c", "cpp", "haskell", "dart", "scala", "zig", "elixir", "erlang"]
```
## Code Extraction Quality
### What Gets Extracted
- **Complete Definitions**: Full function/class/method definitions with signatures and bodies
- **Self-Contained Blocks**: Code that makes sense in isolation for typing practice
- **Real-World Constructs**: Actual code patterns from repository codebases
### What Gets Filtered Out
- **Incomplete Snippets**: Partial code lacking context
- **Comments Only**: Blocks containing only comments
- **Import Statements**: Standalone import/use declarations
- **Very Short Code**: Code blocks under minimum threshold for meaningful practice
## Adding New Language Support
See [CONTRIBUTING.md](CONTRIBUTING.md#adding-language-support) for detailed instructions on adding support for new programming languages.
+216
View File
@@ -0,0 +1,216 @@
# Themes & Customization
GitType provides extensive theming capabilities to personalize your typing experience with beautiful color schemes and custom styling options.
## Built-in Themes
GitType comes with 15 carefully crafted themes to match your coding style and environment:
| Theme | Description |
|-------|-------------|
| `default` | Balanced palette for comfortable readability |
| `original` | Classic GitType color scheme |
| `ascii` | Monochrome terminal aesthetic |
| `aurora` | Northern lights inspired |
| `blood_oath` | Dark red vampire theme |
| `cyber_void` | Futuristic neon cyberpunk |
| `eclipse` | Deep space darkness |
| `glacier` | Cool blue ice tones |
| `inferno` | Hot fire and lava colors |
| `neon_abyss` | Electric neon in darkness |
| `oblivion` | Mysterious dark void |
| `runic` | Ancient mystical symbols |
| `spectral` | Ghostly ethereal colors |
| `starforge` | Cosmic star creation |
| `venom` | Toxic green poison theme |
## Changing Themes
### From Settings Screen
1. Press **Esc** from any screen to return to main menu
2. Navigate to **Settings****Theme**
3. Use **Up/Down** arrows to browse themes
4. Changes are applied instantly for preview
5. Press **Enter** to confirm selection
### Color Mode Toggle
Switch between Dark and Light modes:
- Each theme supports both color modes
- Access via **Settings****Color Mode**
- Dark mode is optimized for low-light environments
- Light mode provides better contrast in bright conditions
## Creating Custom Themes
### Theme File Structure
Custom theme files use simplified JSON format without metadata:
```json
{
"dark": {
"border": {"r": 102, "g": 153, "b": 204},
"title": {"r": 235, "g": 235, "b": 235},
"text": {"r": 220, "g": 220, "b": 220},
"background": {"r": 15, "g": 15, "b": 15},
// ... more color definitions
},
"light": {
"border": {"r": 120, "g": 160, "b": 220},
// ... light mode colors
}
}
```
Note: The custom theme automatically appears as "Custom" in the theme selection menu.
### Color Properties
Each theme defines colors for both `dark` and `light` modes:
#### Interface Elements
- `border` - Window borders and dividers
- `title` - Main titles and headings
- `text` - Primary text content
- `text_secondary` - Secondary/disabled text
- `background` - Main background
- `background_secondary` - Secondary panels
#### Status Colors
- `status_success` - Success messages (typically green)
- `status_info` - Information messages (typically blue)
- `status_warning` - Warning messages (typically yellow)
- `status_error` - Error messages (typically red)
#### Key Indicators
- `key_back` - Back/cancel actions (usually red)
- `key_action` - Confirm/action keys (usually green)
- `key_navigation` - Navigation keys (usually blue)
#### Typing Interface
- `typing_untyped_text` - Text not yet typed
- `typing_typed_text` - Successfully typed text
- `typing_cursor_fg` - Cursor text color
- `typing_cursor_bg` - Cursor background
- `typing_mistake_bg` - Error highlight background
#### Metrics Display
- `metrics_score` - Score display
- `metrics_cpm_wpm` - Speed metrics (CPM/WPM)
- `metrics_accuracy` - Accuracy percentage
- `metrics_duration` - Timer display
- `metrics_stage_info` - Stage information
### Installing Custom Theme
1. **Create theme file**: Save as `~/.gittype/custom-theme.json`
2. **Restart GitType**: The theme will be available as "Custom" in Settings → Theme menu
3. **File location**: Custom theme must be saved as `~/.gittype/custom-theme.json`
### Complete Theme Example
Here's a complete example creating a custom theme:
```json
{
"dark": {
"border": {"r": 0, "g": 255, "b": 0},
"title": {"r": 0, "g": 255, "b": 0},
"text": {"r": 0, "g": 200, "b": 0},
"text_secondary": {"r": 0, "g": 100, "b": 0},
"background": {"r": 0, "g": 0, "b": 0},
"background_secondary": {"r": 10, "g": 10, "b": 10},
"status_success": {"r": 0, "g": 255, "b": 0},
"status_info": {"r": 0, "g": 200, "b": 0},
"status_warning": {"r": 200, "g": 255, "b": 0},
"status_error": {"r": 255, "g": 100, "b": 100},
"key_back": {"r": 255, "g": 100, "b": 100},
"key_action": {"r": 0, "g": 255, "b": 0},
"key_navigation": {"r": 0, "g": 200, "b": 0},
"metrics_score": {"r": 0, "g": 255, "b": 0},
"metrics_cpm_wpm": {"r": 0, "g": 200, "b": 0},
"metrics_accuracy": {"r": 0, "g": 255, "b": 0},
"metrics_duration": {"r": 0, "g": 200, "b": 0},
"metrics_stage_info": {"r": 0, "g": 150, "b": 0},
"typing_untyped_text": {"r": 0, "g": 150, "b": 0},
"typing_typed_text": {"r": 0, "g": 255, "b": 0},
"typing_cursor_fg": {"r": 0, "g": 0, "b": 0},
"typing_cursor_bg": {"r": 0, "g": 255, "b": 0},
"typing_mistake_bg": {"r": 100, "g": 0, "b": 0}
},
"light": {
"border": {"r": 0, "g": 150, "b": 0},
"title": {"r": 0, "g": 100, "b": 0},
"text": {"r": 0, "g": 80, "b": 0},
"text_secondary": {"r": 100, "g": 120, "b": 100},
"background": {"r": 240, "g": 255, "b": 240},
"background_secondary": {"r": 220, "g": 240, "b": 220},
"status_success": {"r": 0, "g": 120, "b": 0},
"status_info": {"r": 0, "g": 100, "b": 0},
"status_warning": {"r": 150, "g": 150, "b": 0},
"status_error": {"r": 150, "g": 0, "b": 0},
"key_back": {"r": 150, "g": 0, "b": 0},
"key_action": {"r": 0, "g": 120, "b": 0},
"key_navigation": {"r": 0, "g": 100, "b": 0},
"metrics_score": {"r": 0, "g": 120, "b": 0},
"metrics_cpm_wpm": {"r": 0, "g": 100, "b": 0},
"metrics_accuracy": {"r": 0, "g": 120, "b": 0},
"metrics_duration": {"r": 0, "g": 100, "b": 0},
"metrics_stage_info": {"r": 0, "g": 80, "b": 0},
"typing_untyped_text": {"r": 100, "g": 120, "b": 100},
"typing_typed_text": {"r": 0, "g": 100, "b": 0},
"typing_cursor_fg": {"r": 255, "g": 255, "b": 255},
"typing_cursor_bg": {"r": 0, "g": 120, "b": 0},
"typing_mistake_bg": {"r": 255, "g": 200, "b": 200}
}
}
```
Save this as `~/.gittype/custom-theme.json` and restart GitType to use it.
## Configuration Files
### Theme Settings
Your current theme preference is stored in `~/.gittype/config.json`:
```json
{
"theme": {
"current_theme_id": "glacier",
"current_color_mode": "Dark"
}
}
```
### Custom Theme Storage
- **Location**: `~/.gittype/custom-theme.json`
- **Format**: JSON format without theme metadata (id, name, description)
- **Auto-detection**: GitType automatically loads the custom theme if the file exists
## Color Format
Colors use RGB format with values from 0-255:
```json
{"r": 255, "g": 128, "b": 64}
```
## Tips for Theme Creation
1. **Test both modes**: Always define both dark and light variants
2. **Maintain contrast**: Ensure sufficient contrast for readability
3. **Consistent palette**: Use a cohesive color scheme
4. **Accessibility**: Consider colorblind-friendly palettes
5. **Preview instantly**: Changes in Settings show immediately for testing
## Troubleshooting
**Theme not appearing**:
- Check JSON syntax with a validator
- Ensure all required color properties are defined
- Restart GitType after adding new themes
**Colors not displaying correctly**:
- Verify RGB values are within 0-255 range
- Check that both "dark" and "light" sections are complete
**Theme reverts to default**:
- Check that `~/.gittype/config.json` has proper write permissions
- Verify the custom theme file is named exactly `custom-theme.json`
+157
View File
@@ -0,0 +1,157 @@
# Usage Guide
## Quick Start
1. **Navigate to any code repository:**
```bash
cd /path/to/your/project
```
2. **Start typing practice (uses current directory by default):**
```bash
gittype
```
3. **Or specify a specific repository:**
```bash
gittype /path/to/another/repo
```
4. **Or clone and use a GitHub repository:**
```bash
gittype --repo unhappychoice/gittype
```
5. **Play with cached repositories interactively:**
```bash
gittype repo play
```
6. **Discover trending repositories:**
```bash
gittype trending
gittype trending rust
```
## Command Line Options
```bash
gittype [OPTIONS] [REPO_PATH] [COMMAND]
```
**Note:** `REPO_PATH` is optional and defaults to the current directory (`.`) if not specified.
### Basic Options
| Option | Description | Default |
|---|---|---|
| `--repo` | GitHub repository URL or path to clone and use | None |
| `--langs` | Filter by programming languages (comma-separated) | All supported |
| `--config` | Path to a custom configuration file | None |
### Examples
```bash
# Practice with Rust and TypeScript files only
gittype --langs rust,typescript
```
### Custom Excludes with `.gittypeignore`
If your repository vendors third-party code, create a `.gittypeignore` file at the repository root.
Each non-empty line is parsed with `.gitignore`-style syntax (comments, rooted paths like `/vendor/`, etc.).
```bash
# Example .gittypeignore
/vendor/
**/third_party/**
```
## Commands
### View Session History
```bash
gittype history
```
Show session history.
### Show Analytics
```bash
gittype stats
```
Show analytics.
### Export Session Data
```bash
gittype export [OPTIONS]
```
Export session data.
| Option | Description | Default |
|---|---|---|
| `--format` | Export format | `json` |
| `--output` | Output file path | stdout |
**Example:**
```bash
# Export history to a JSON file
gittype export --output history.json
```
### Manage Challenge Cache
```bash
gittype cache <COMMAND>
```
#### Cache Commands:
- `gittype cache stats` - Show cache statistics
- `gittype cache clear` - Clear all cached challenges
- `gittype cache list` - List cached repository keys
### Manage Repositories
```bash
gittype repo <COMMAND>
```
#### Repository Commands:
- `gittype repo list` - List all cached repositories
- `gittype repo clear [--force]` - Clear all cached repositories
- `gittype repo play` - Play a cached repository interactively
### Practice with Trending Repositories
```bash
gittype trending [LANGUAGE] [OPTIONS]
```
Discover and practice typing with trending GitHub repositories. Repositories are cached and updated automatically.
#### Options:
| Option | Description | Default |
|---|---|---|
| `LANGUAGE` | Programming language to filter repositories | All languages |
| `--period` | Time period for trending (daily, weekly, monthly) | `daily` |
#### Supported Languages:
- C, C#, C++, Dart, Elixir, Erlang, Go, Haskell, Java, JavaScript, Kotlin, PHP, Python, Ruby, Rust, Scala, Swift, TypeScript, Zig
#### Examples:
```bash
# Browse trending repositories interactively (all languages)
gittype trending
# Show trending Rust repositories for interactive selection
gittype trending rust
# Show weekly trending Python repositories
gittype trending python --period weekly
# Show monthly trending repositories for all languages
gittype trending --period monthly
```
#### How it works:
1. **Interactive Selection**: When no specific repository is provided, GitType shows an interactive list of trending repositories
2. **Language Filtering**: Specify a language to see only repositories in that programming language
3. **Direct Repository Selection**: Provide a repository name to search for and play with that specific repository
4. **Automatic Caching**: Trending data is cached to reduce API calls and improve performance
5. **Seamless Integration**: Selected repositories are automatically cloned and ready for typing practice
+43
View File
@@ -0,0 +1,43 @@
# Development Tools
This directory contains development tools and utilities for GitType.
## Seed Database
Populate the database with sample data for development and testing.
### Usage
```bash
# Generate default dataset (10 repos, 1000 sessions, 3000 stages)
cargo run --example seed_database -- --clear
# Generate custom dataset
cargo run --example seed_database -- --clear --repos 5 --sessions 100 --stages 500
# Small dataset for quick testing
cargo run --example seed_database -- --clear --repos 2 --sessions 20 --stages 50
```
### Options
- `--clear`: Clear existing data before seeding
- `--repos <N>`: Number of repositories to generate (default: 10)
- `--sessions <N>`: Number of sessions to generate (default: 1000)
- `--stages <N>`: Number of stages to generate (default: 3000)
### Generated Data
The tool generates realistic sample data including:
- **Repositories**: Various programming languages and project types
- **Sessions**: Mix of completed and incomplete typing sessions
- **Challenges**: Real code snippets in multiple languages
- **Stages**: Individual typing challenges with timing data
- **Results**: Performance metrics, rankings, and statistics
This data is useful for:
- Testing UI components with realistic data volumes
- Performance testing with large datasets
- Developing analytics and reporting features
- Manual testing of different user scenarios
+82
View File
@@ -0,0 +1,82 @@
mod seeders;
use clap::Parser;
use gittype::{
infrastructure::database::database::{Database, DatabaseInterface},
Result,
};
use seeders::DatabaseSeeder;
use std::sync::Arc;
#[derive(Parser)]
#[command(name = "seed_database")]
#[command(about = "Populate database with seed data for development")]
struct Args {
/// Clear existing data before seeding
#[arg(long)]
clear: bool,
/// Number of repositories to generate
#[arg(long, default_value = "10")]
repos: usize,
/// Number of sessions to generate
#[arg(long, default_value = "1000")]
sessions: usize,
/// Number of stages to generate
#[arg(long, default_value = "3000")]
stages: usize,
}
fn main() -> Result<()> {
let args = Args::parse();
println!("🌱 Starting database seeding...");
let db = Database::new()?;
let database: Arc<dyn DatabaseInterface> = Arc::new(db);
// Initialize database tables if needed
database.init_tables()?;
if args.clear {
println!("🧹 Clearing existing data...");
clear_database(&database)?;
}
println!(
"📊 Generating {} repositories, {} sessions, {} stages...",
args.repos, args.sessions, args.stages
);
let seeder = DatabaseSeeder::new(database);
seeder.seed_with_counts(args.repos, args.sessions, args.stages)?;
println!("✅ Seed data has been successfully loaded!");
println!("💡 You can now use the application with sample data for development and testing.");
Ok(())
}
fn clear_database(database: &Arc<dyn DatabaseInterface>) -> Result<()> {
let conn = database.get_connection()?;
// Disable foreign key checks temporarily
conn.execute("PRAGMA foreign_keys = OFF", [])?;
let tables = vec![
"stage_results",
"session_results",
"stages",
"challenges",
"sessions",
"repositories",
];
for table in tables {
conn.execute(&format!("DELETE FROM {}", table), [])?;
}
// Re-enable foreign key checks
conn.execute("PRAGMA foreign_keys = ON", [])?;
Ok(())
}
+843
View File
@@ -0,0 +1,843 @@
use chrono::{DateTime, Utc};
use rand::prelude::*;
use serde::{Deserialize, Serialize};
#[derive(Debug, Serialize, Deserialize)]
pub struct SeedData {
pub repositories: Vec<SeedRepository>,
pub sessions: Vec<SeedSession>,
pub challenges: Vec<SeedChallenge>,
pub stages: Vec<SeedStage>,
}
#[derive(Debug, Serialize, Deserialize)]
pub struct SeedRepository {
pub id: i64,
pub user_name: String,
pub repository_name: String,
pub remote_url: String,
pub created_at: DateTime<Utc>,
}
#[derive(Debug, Serialize, Deserialize)]
pub struct SeedSession {
pub id: i64,
pub repository_id: i64,
pub started_at: DateTime<Utc>,
pub completed_at: Option<DateTime<Utc>>,
pub branch: String,
pub commit_hash: String,
pub is_dirty: bool,
pub game_mode: String,
pub difficulty_level: String,
pub session_result: Option<SeedSessionResult>,
}
#[derive(Debug, Serialize, Deserialize)]
pub struct SeedSessionResult {
pub keystrokes: i32,
pub mistakes: i32,
pub duration_ms: i64,
pub wpm: f64,
pub cpm: f64,
pub accuracy: f64,
pub stages_completed: i32,
pub stages_attempted: i32,
pub stages_skipped: i32,
pub partial_effort_keystrokes: i32,
pub partial_effort_mistakes: i32,
pub best_stage_wpm: Option<f64>,
pub worst_stage_wpm: Option<f64>,
pub best_stage_accuracy: Option<f64>,
pub worst_stage_accuracy: Option<f64>,
pub score: f64,
pub rank_name: Option<String>,
pub tier_name: Option<String>,
pub rank_position: Option<i32>,
pub rank_total: Option<i32>,
pub position: Option<i32>,
pub total: Option<i32>,
}
#[derive(Debug, Serialize, Deserialize)]
pub struct SeedChallenge {
pub id: String,
pub file_path: String,
pub start_line: i32,
pub end_line: i32,
pub language: String,
pub code_content: String,
pub comment_ranges: String,
pub difficulty_level: String,
pub created_at: DateTime<Utc>,
}
#[derive(Debug, Serialize, Deserialize)]
pub struct SeedStage {
pub id: i64,
pub session_id: i64,
pub challenge_id: String,
pub stage_number: i32,
pub started_at: Option<DateTime<Utc>>,
pub completed_at: Option<DateTime<Utc>>,
}
impl SeedData {
#[allow(dead_code)]
pub fn default_seed_data() -> Self {
Self::generate_seed_data(10, 1000, 3000)
}
pub fn generate_seed_data(repo_count: usize, session_count: usize, stage_count: usize) -> Self {
let mut rng = rand::rng();
let now = Utc::now();
let repositories = Self::generate_repositories(repo_count, &mut rng, now);
let (sessions, challenges) =
Self::generate_sessions_and_challenges(session_count, &repositories, &mut rng, now);
let stages = Self::generate_stages(stage_count, &sessions, &challenges, &mut rng, now);
Self {
repositories,
sessions,
challenges,
stages,
}
}
fn generate_repositories(
count: usize,
rng: &mut impl Rng,
now: DateTime<Utc>,
) -> Vec<SeedRepository> {
let languages = [
"rust",
"typescript",
"javascript",
"python",
"ruby",
"go",
"swift",
"kotlin",
"java",
"php",
"csharp",
"cpp",
];
let project_types = [
"web-app",
"cli-tool",
"library",
"api",
"mobile-app",
"desktop-app",
"game",
"service",
];
let org_names = [
"awesome", "modern", "super", "next-gen", "fast", "secure", "smart", "micro",
];
(1..=count)
.map(|i| {
let lang = languages.choose(rng).unwrap();
let project_type = project_types.choose(rng).unwrap();
let org = org_names.choose(rng).unwrap();
let created_days_ago = rng.random_range(1..=365);
SeedRepository {
id: i as i64,
user_name: org.to_string(),
repository_name: format!(
"{}-{}-{}",
lang,
project_type,
rng.random_range(1..=999)
),
remote_url: format!(
"https://github.com/{}/{}-{}-{}",
org,
lang,
project_type,
rng.random_range(1..=999)
),
created_at: now - chrono::Duration::days(created_days_ago),
}
})
.collect()
}
fn generate_sessions_and_challenges(
session_count: usize,
repositories: &[SeedRepository],
rng: &mut impl Rng,
now: DateTime<Utc>,
) -> (Vec<SeedSession>, Vec<SeedChallenge>) {
let languages = [
"rust",
"typescript",
"javascript",
"python",
"ruby",
"go",
"swift",
"kotlin",
"java",
"php",
];
let game_modes = ["Normal", "Time Attack", "Practice", "Challenge"];
let difficulty_levels = ["Easy", "Medium", "Hard"];
let branch_names = [
"main",
"master",
"develop",
"feature/new-ui",
"fix/bug-123",
"refactor/cleanup",
];
let mut sessions = Vec::new();
let mut challenges = Vec::new();
let mut challenge_id_counter = 1;
for i in 1..=session_count {
let repo = repositories.choose(rng).unwrap();
let started_days_ago = rng.random_range(0..=30);
let started_at = now
- chrono::Duration::days(started_days_ago)
- chrono::Duration::hours(rng.random_range(0..=23))
- chrono::Duration::minutes(rng.random_range(0..=59));
let is_completed = rng.random_bool(0.85); // 85% completed sessions
let completed_at = if is_completed {
Some(started_at + chrono::Duration::minutes(rng.random_range(5..=45)))
} else {
None
};
let game_mode = game_modes.choose(rng).unwrap().to_string();
let difficulty = difficulty_levels.choose(rng).unwrap().to_string();
let session_result = if is_completed {
let stages_attempted = rng.random_range(5..=20);
let stages_completed = rng.random_range(3..=stages_attempted);
let stages_skipped = stages_attempted - stages_completed;
let keystrokes = rng.random_range(800..=2500);
let mistakes = rng.random_range(10..=keystrokes / 15);
let duration_ms = rng.random_range(300000..=2700000); // 5-45 minutes
let accuracy = (keystrokes - mistakes) as f64 / keystrokes as f64 * 100.0;
let wpm = (keystrokes as f64 / 5.0) / (duration_ms as f64 / 60000.0);
let cpm = keystrokes as f64 / (duration_ms as f64 / 60000.0);
Some(SeedSessionResult {
keystrokes,
mistakes,
duration_ms,
wpm,
cpm,
accuracy,
stages_completed,
stages_attempted,
stages_skipped,
partial_effort_keystrokes: rng.random_range(0..=100),
partial_effort_mistakes: rng.random_range(0..=10),
best_stage_wpm: Some(wpm * rng.random_range(1.1..=1.4)),
worst_stage_wpm: Some(wpm * rng.random_range(0.6..=0.9)),
best_stage_accuracy: Some(accuracy * rng.random_range(1.01..=1.05)),
worst_stage_accuracy: Some(accuracy * rng.random_range(0.85..=0.95)),
score: wpm * accuracy / 10.0 + stages_completed as f64 * 50.0,
rank_name: Some(Self::get_rank_name(wpm)),
tier_name: Some(Self::get_tier_name(accuracy)),
rank_position: Some(rng.random_range(1..=100)),
rank_total: Some(100),
position: Some(rng.random_range(1..=500)),
total: Some(500),
})
} else {
None
};
// Generate 2-5 challenges per session
let challenge_count = rng.random_range(2..=5);
for _j in 0..challenge_count {
let lang = languages.choose(rng).unwrap();
let file_extensions = match *lang {
"rust" => vec!["rs"],
"typescript" => vec!["ts", "tsx"],
"javascript" => vec!["js", "jsx"],
"python" => vec!["py"],
"ruby" => vec!["rb"],
"go" => vec!["go"],
"swift" => vec!["swift"],
"kotlin" => vec!["kt"],
"java" => vec!["java"],
"php" => vec!["php"],
_ => vec!["txt"],
};
let ext = file_extensions.choose(rng).unwrap();
let file_path = format!(
"src/{}/{}.{}",
Self::random_directory(rng),
Self::random_filename(lang, rng),
ext
);
challenges.push(SeedChallenge {
id: format!("challenge_{}", challenge_id_counter),
file_path,
start_line: rng.random_range(1..=50),
end_line: rng.random_range(51..=200),
language: lang.to_string(),
code_content: Self::generate_code_content(lang, rng),
comment_ranges: "[]".to_string(),
difficulty_level: difficulty_levels.choose(rng).unwrap().to_string(),
created_at: started_at - chrono::Duration::minutes(rng.random_range(1..=30)),
});
challenge_id_counter += 1;
}
sessions.push(SeedSession {
id: i as i64,
repository_id: repo.id,
started_at,
completed_at,
branch: branch_names.choose(rng).unwrap().to_string(),
commit_hash: Self::generate_commit_hash(rng),
is_dirty: rng.random_bool(0.3), // 30% dirty
game_mode,
difficulty_level: difficulty,
session_result,
});
}
(sessions, challenges)
}
fn generate_stages(
target_count: usize,
sessions: &[SeedSession],
challenges: &[SeedChallenge],
rng: &mut impl Rng,
_now: DateTime<Utc>,
) -> Vec<SeedStage> {
let mut stages = Vec::new();
let mut stage_id = 1;
let stages_per_session = target_count / sessions.len().max(1);
for session in sessions {
let session_stages = if session.session_result.is_some() {
rng.random_range(stages_per_session.saturating_sub(2)..=stages_per_session + 2)
} else {
rng.random_range(1..=3) // Incomplete sessions have fewer stages
};
for stage_num in 1..=session_stages {
let challenge = challenges.choose(rng).unwrap();
let stage_duration_mins = rng.random_range(1..=8);
let started_at = session.started_at
+ chrono::Duration::minutes((stage_num - 1) as i64 * stage_duration_mins);
let completed_at = if session.session_result.is_some() && rng.random_bool(0.9) {
Some(started_at + chrono::Duration::minutes(stage_duration_mins))
} else {
None
};
stages.push(SeedStage {
id: stage_id,
session_id: session.id,
challenge_id: challenge.id.clone(),
stage_number: stage_num as i32,
started_at: Some(started_at),
completed_at,
});
stage_id += 1;
if stages.len() >= target_count {
return stages;
}
}
}
stages
}
fn get_rank_name(wpm: f64) -> String {
match wpm as i32 {
0..=30 => "Beginner",
31..=50 => "Intermediate",
51..=70 => "Advanced",
71..=90 => "Expert",
_ => "Master",
}
.to_string()
}
fn get_tier_name(accuracy: f64) -> String {
match accuracy as i32 {
0..=80 => "Bronze",
81..=90 => "Silver",
91..=95 => "Gold",
96..=98 => "Platinum",
_ => "Diamond",
}
.to_string()
}
fn random_directory(rng: &mut impl Rng) -> String {
let dirs = [
"components",
"utils",
"models",
"services",
"controllers",
"handlers",
"lib",
"core",
"api",
];
dirs.choose(rng).unwrap().to_string()
}
fn random_filename(lang: &str, rng: &mut impl Rng) -> String {
let names = match lang {
"rust" => vec![
"main", "lib", "parser", "handler", "config", "utils", "client", "server",
],
"typescript" => vec![
"component",
"service",
"model",
"utils",
"api",
"types",
"hooks",
"store",
],
"javascript" => vec![
"app",
"component",
"utils",
"api",
"service",
"helper",
"config",
"index",
],
"python" => vec![
"main", "models", "views", "utils", "service", "client", "parser", "config",
],
"java" => vec![
"Main",
"Service",
"Controller",
"Model",
"Utils",
"Client",
"Handler",
"Config",
],
_ => vec![
"main", "utils", "service", "model", "config", "helper", "client", "handler",
],
};
names.choose(rng).unwrap().to_string()
}
fn generate_commit_hash(rng: &mut impl Rng) -> String {
const CHARS: &[u8] = b"0123456789abcdef";
(0..12)
.map(|_| {
let idx = rng.random_range(0..CHARS.len());
CHARS[idx] as char
})
.collect()
}
fn generate_code_content(lang: &str, rng: &mut impl Rng) -> String {
let samples = match lang {
"rust" => vec![
r#"pub fn calculate_fibonacci(n: usize) -> u64 {
if n <= 1 { return n as u64; }
let mut a = 0u64;
let mut b = 1u64;
for _ in 2..=n {
let temp = a + b;
a = b;
b = temp;
}
b
}"#,
r#"impl<T: Clone> Vec<T> {
pub fn duplicate_elements(&self) -> Vec<T> {
let mut result = Vec::with_capacity(self.len() * 2);
for item in self {
result.push(item.clone());
result.push(item.clone());
}
result
}
}"#,
r#"use std::collections::HashMap;
pub struct Cache<K, V> {
data: HashMap<K, V>,
capacity: usize,
}
impl<K, V> Cache<K, V>
where
K: std::hash::Hash + Eq,
{
pub fn new(capacity: usize) -> Self {
Self {
data: HashMap::new(),
capacity,
}
}
}"#,
],
"typescript" => vec![
r#"interface UserProfile {
id: string;
email: string;
name: string;
avatar?: string;
preferences: {
theme: 'light_original' | 'dark_original';
notifications: boolean;
};
}
export const createUserProfile = (data: Partial<UserProfile>): UserProfile => {
return {
id: crypto.randomUUID(),
email: data.email || '',
name: data.name || '',
preferences: {
theme: 'light_original',
notifications: true,
...data.preferences
},
...data
};
};"#,
r#"class ApiClient {
private baseUrl: string;
private headers: Record<string, string>;
constructor(baseUrl: string, apiKey?: string) {
this.baseUrl = baseUrl;
this.headers = {
'Content-Type': 'application/json',
...(apiKey && { 'Authorization': `Bearer ${apiKey}` })
};
}
async get<T>(endpoint: string): Promise<T> {
const response = await fetch(`${this.baseUrl}${endpoint}`, {
method: 'GET',
headers: this.headers
});
return response.json();
}
}"#,
],
"python" => vec![
r#"from typing import List, Optional, Dict, Any
import json
from datetime import datetime
class DataProcessor:
def __init__(self, config: Dict[str, Any]):
self.config = config
self.processed_count = 0
def process_batch(self, items: List[Dict[str, Any]]) -> List[Dict[str, Any]]:
results = []
for item in items:
processed = self._process_single_item(item)
if processed:
results.append(processed)
self.processed_count += 1
return results
def _process_single_item(self, item: Dict[str, Any]) -> Optional[Dict[str, Any]]:
if not self._validate_item(item):
return None
return {
'id': item.get('id'),
'processed_at': datetime.now().isoformat(),
'data': self._transform_data(item.get('data', {}))
}"#,
],
_ => vec![
r#"function processData(input) {
const results = [];
for (let i = 0; i < input.length; i++) {
const item = input[i];
if (validateItem(item)) {
results.push(transformItem(item));
}
}
return results;
}"#,
],
};
samples.choose(rng).unwrap().to_string()
}
// Keep the old small dataset method for testing
#[allow(dead_code)]
pub fn small_seed_data() -> Self {
let now = Utc::now();
let hour_ago = now - chrono::Duration::hours(1);
let day_ago = now - chrono::Duration::days(1);
let week_ago = now - chrono::Duration::weeks(1);
Self {
repositories: vec![
SeedRepository {
id: 1,
user_name: "example".to_string(),
repository_name: "sample-rust-project".to_string(),
remote_url: "https://github.com/example/sample-rust-project".to_string(),
created_at: day_ago,
},
SeedRepository {
id: 2,
user_name: "example".to_string(),
repository_name: "sample-typescript-app".to_string(),
remote_url: "https://github.com/example/sample-typescript-app".to_string(),
created_at: week_ago,
},
SeedRepository {
id: 3,
user_name: "local".to_string(),
repository_name: "python-project".to_string(),
remote_url: "https://github.com/local/python-project".to_string(),
created_at: hour_ago,
},
],
sessions: vec![
SeedSession {
id: 1,
repository_id: 1,
started_at: day_ago,
completed_at: Some(day_ago + chrono::Duration::minutes(15)),
branch: "main".to_string(),
commit_hash: "abc123def456".to_string(),
is_dirty: false,
game_mode: "Normal".to_string(),
difficulty_level: "Medium".to_string(),
session_result: Some(SeedSessionResult {
keystrokes: 1250,
mistakes: 42,
duration_ms: 900000,
wpm: 65.8,
cpm: 329.0,
accuracy: 96.6,
stages_completed: 8,
stages_attempted: 10,
stages_skipped: 2,
partial_effort_keystrokes: 50,
partial_effort_mistakes: 3,
best_stage_wpm: Some(78.2),
worst_stage_wpm: Some(52.1),
best_stage_accuracy: Some(98.9),
worst_stage_accuracy: Some(92.3),
score: 1840.5,
rank_name: Some("Advanced".to_string()),
tier_name: Some("Silver".to_string()),
rank_position: Some(15),
rank_total: Some(100),
position: Some(15),
total: Some(100),
}),
},
SeedSession {
id: 2,
repository_id: 2,
started_at: week_ago,
completed_at: Some(week_ago + chrono::Duration::minutes(22)),
branch: "feature/new-ui".to_string(),
commit_hash: "xyz789abc012".to_string(),
is_dirty: true,
game_mode: "Time Attack".to_string(),
difficulty_level: "Hard".to_string(),
session_result: Some(SeedSessionResult {
keystrokes: 1800,
mistakes: 95,
duration_ms: 1320000,
wpm: 52.3,
cpm: 261.5,
accuracy: 94.7,
stages_completed: 12,
stages_attempted: 15,
stages_skipped: 3,
partial_effort_keystrokes: 120,
partial_effort_mistakes: 8,
best_stage_wpm: Some(68.4),
worst_stage_wpm: Some(41.2),
best_stage_accuracy: Some(97.1),
worst_stage_accuracy: Some(89.5),
score: 2150.8,
rank_name: Some("Intermediate".to_string()),
tier_name: Some("Bronze".to_string()),
rank_position: Some(35),
rank_total: Some(100),
position: Some(35),
total: Some(100),
}),
},
SeedSession {
id: 3,
repository_id: 1,
started_at: hour_ago,
completed_at: None,
branch: "main".to_string(),
commit_hash: "def456ghi789".to_string(),
is_dirty: false,
game_mode: "Practice".to_string(),
difficulty_level: "Easy".to_string(),
session_result: None,
},
],
challenges: vec![
SeedChallenge {
id: "challenge_1".to_string(),
file_path: "src/main.rs".to_string(),
start_line: 1,
end_line: 6,
language: "rust".to_string(),
code_content: r#"fn main() {
println!("Hello, world!");
let numbers = vec![1, 2, 3, 4, 5];
let sum: i32 = numbers.iter().sum();
println!("Sum: {}", sum);
}"#
.to_string(),
comment_ranges: "[]".to_string(),
difficulty_level: "Easy".to_string(),
created_at: day_ago,
},
SeedChallenge {
id: "challenge_2".to_string(),
file_path: "src/lib.rs".to_string(),
start_line: 1,
end_line: 20,
language: "rust".to_string(),
code_content: r#"pub struct Calculator {
result: f64,
}
impl Calculator {
pub fn new() -> Self {
Self { result: 0.0 }
}
pub fn add(&mut self, value: f64) -> &mut Self {
self.result += value;
self
}
pub fn multiply(&mut self, value: f64) -> &mut Self {
self.result *= value;
self
}
pub fn get_result(&self) -> f64 {
self.result
}
}"#
.to_string(),
comment_ranges: "[]".to_string(),
difficulty_level: "Medium".to_string(),
created_at: day_ago,
},
SeedChallenge {
id: "challenge_3".to_string(),
file_path: "src/components/Button.tsx".to_string(),
start_line: 1,
end_line: 20,
language: "typescript".to_string(),
code_content: r#"interface ButtonProps {
label: string;
onClick: () => void;
variant?: 'primary' | 'secondary';
disabled?: boolean;
}
export const Button: React.FC<ButtonProps> = ({
label,
onClick,
variant = 'primary',
disabled = false
}) => {
return (
<button
className={`btn btn-${variant}`}
onClick={onClick}
disabled={disabled}
>
{label}
</button>
);
};"#
.to_string(),
comment_ranges: "[]".to_string(),
difficulty_level: "Hard".to_string(),
created_at: week_ago,
},
],
stages: vec![
SeedStage {
id: 1,
session_id: 1,
challenge_id: "challenge_1".to_string(),
stage_number: 1,
started_at: Some(day_ago),
completed_at: Some(day_ago + chrono::Duration::minutes(2)),
},
SeedStage {
id: 2,
session_id: 1,
challenge_id: "challenge_2".to_string(),
stage_number: 2,
started_at: Some(day_ago + chrono::Duration::minutes(3)),
completed_at: Some(day_ago + chrono::Duration::minutes(6)),
},
SeedStage {
id: 3,
session_id: 2,
challenge_id: "challenge_3".to_string(),
stage_number: 1,
started_at: Some(week_ago),
completed_at: Some(week_ago + chrono::Duration::minutes(5)),
},
SeedStage {
id: 4,
session_id: 2,
challenge_id: "challenge_1".to_string(),
stage_number: 2,
started_at: Some(week_ago + chrono::Duration::minutes(6)),
completed_at: Some(week_ago + chrono::Duration::minutes(8)),
},
SeedStage {
id: 5,
session_id: 3,
challenge_id: "challenge_2".to_string(),
stage_number: 1,
started_at: Some(hour_ago),
completed_at: None,
},
],
}
}
}
+261
View File
@@ -0,0 +1,261 @@
use super::data::SeedData;
use gittype::infrastructure::database::database::{DatabaseInterface, HasDatabase};
use gittype::Result;
use std::sync::Arc;
pub struct DatabaseSeeder {
database: Arc<dyn DatabaseInterface>,
}
impl DatabaseSeeder {
pub fn new(database: Arc<dyn DatabaseInterface>) -> Self {
Self { database }
}
#[allow(dead_code)]
pub fn seed(&self) -> Result<()> {
let seed_data = SeedData::default_seed_data();
self.seed_data(&seed_data)
}
pub fn seed_with_counts(
&self,
repo_count: usize,
session_count: usize,
stage_count: usize,
) -> Result<()> {
let seed_data = SeedData::generate_seed_data(repo_count, session_count, stage_count);
self.seed_data(&seed_data)
}
fn seed_data(&self, seed_data: &SeedData) -> Result<()> {
println!("🗃️ Seeding repositories...");
self.seed_repositories(seed_data)?;
println!("📊 Seeding sessions...");
self.seed_sessions(seed_data)?;
println!("🎯 Seeding challenges...");
self.seed_challenges(seed_data)?;
println!("🎮 Seeding stages...");
self.seed_stages(seed_data)?;
Ok(())
}
fn seed_repositories(&self, seed_data: &SeedData) -> Result<()> {
let conn = self.database.get_connection()?;
for repo in &seed_data.repositories {
conn.execute(
"INSERT INTO repositories (id, user_name, repository_name, remote_url, created_at)
VALUES (?, ?, ?, ?, ?)",
[
&repo.id as &dyn rusqlite::ToSql,
&repo.user_name,
&repo.repository_name,
&repo.remote_url,
&repo.created_at.to_rfc3339(),
],
)?;
}
println!("{} repositories seeded", seed_data.repositories.len());
Ok(())
}
fn seed_sessions(&self, seed_data: &SeedData) -> Result<()> {
let conn = self.database.get_connection()?;
for session in &seed_data.sessions {
conn.execute(
"INSERT INTO sessions (id, repository_id, started_at, completed_at, branch, commit_hash, is_dirty, game_mode, difficulty_level)
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?)",
[
&session.id as &dyn rusqlite::ToSql,
&session.repository_id,
&session.started_at.format("%Y-%m-%d %H:%M:%S").to_string(),
&session.completed_at.map(|dt| dt.format("%Y-%m-%d %H:%M:%S").to_string()),
&session.branch,
&session.commit_hash,
&session.is_dirty,
&session.game_mode,
&session.difficulty_level,
],
)?;
if let Some(result) = &session.session_result {
conn.execute(
"INSERT INTO session_results (session_id, repository_id, keystrokes, mistakes, duration_ms, wpm, cpm, accuracy, stages_completed, stages_attempted, stages_skipped, partial_effort_keystrokes, partial_effort_mistakes, best_stage_wpm, worst_stage_wpm, best_stage_accuracy, worst_stage_accuracy, score, rank_name, tier_name, rank_position, rank_total, position, total, game_mode, difficulty_level)
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)",
[
&session.id as &dyn rusqlite::ToSql,
&session.repository_id,
&result.keystrokes,
&result.mistakes,
&result.duration_ms,
&result.wpm,
&result.cpm,
&result.accuracy,
&result.stages_completed,
&result.stages_attempted,
&result.stages_skipped,
&result.partial_effort_keystrokes,
&result.partial_effort_mistakes,
&result.best_stage_wpm,
&result.worst_stage_wpm,
&result.best_stage_accuracy,
&result.worst_stage_accuracy,
&result.score,
&result.rank_name,
&result.tier_name,
&result.rank_position,
&result.rank_total,
&result.position,
&result.total,
&session.game_mode,
&session.difficulty_level,
],
)?;
}
}
println!("{} sessions seeded", seed_data.sessions.len());
Ok(())
}
fn seed_challenges(&self, seed_data: &SeedData) -> Result<()> {
let conn = self.database.get_connection()?;
for challenge in &seed_data.challenges {
conn.execute(
"INSERT INTO challenges (id, file_path, start_line, end_line, language, code_content, comment_ranges, difficulty_level, created_at)
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?)",
[
&challenge.id as &dyn rusqlite::ToSql,
&challenge.file_path,
&challenge.start_line,
&challenge.end_line,
&challenge.language,
&challenge.code_content,
&challenge.comment_ranges,
&challenge.difficulty_level,
&challenge.created_at.to_rfc3339(),
],
)?;
}
println!("{} challenges seeded", seed_data.challenges.len());
Ok(())
}
fn seed_stages(&self, seed_data: &SeedData) -> Result<()> {
let conn = self.database.get_connection()?;
for stage in &seed_data.stages {
conn.execute(
"INSERT INTO stages (id, session_id, challenge_id, stage_number, started_at, completed_at)
VALUES (?, ?, ?, ?, ?, ?)",
[
&stage.id as &dyn rusqlite::ToSql,
&stage.session_id,
&stage.challenge_id,
&stage.stage_number,
&stage.started_at.map(|dt| dt.format("%Y-%m-%d %H:%M:%S").to_string()),
&stage.completed_at.map(|dt| dt.format("%Y-%m-%d %H:%M:%S").to_string()),
],
)?;
// Add stage_result if stage is completed
if let Some(completed_at) = stage.completed_at {
// Find the session to get repository_id
let session = seed_data
.sessions
.iter()
.find(|s| s.id == stage.session_id)
.expect("Session not found for stage");
// Find the challenge to get language and difficulty
let challenge = seed_data
.challenges
.iter()
.find(|c| c.id == stage.challenge_id)
.expect("Challenge not found for stage");
// Generate realistic stage result data
use rand::RngExt;
let mut rng = rand::rng();
let keystrokes = rng.random_range(20..200);
let mistakes = rng.random_range(0..10);
let duration_ms = rng.random_range(5000..60000);
let wpm = (keystrokes as f64 * 60000.0 / duration_ms as f64) / 5.0; // 5 chars per word
let cpm = keystrokes as f64 * 60000.0 / duration_ms as f64;
let accuracy = if keystrokes > 0 {
((keystrokes - mistakes) as f64 / keystrokes as f64 * 100.0).max(0.0)
} else {
100.0
};
let score = wpm * accuracy / 10.0;
// Generate ranking data
let rank_position = rng.random_range(1..=100);
let rank_total = 100;
let position = rng.random_range(1..=500);
let total = 500;
// Generate rank and tier names
let rank_name = if wpm > 80.0 {
"Expert"
} else if wpm > 60.0 {
"Advanced"
} else if wpm > 40.0 {
"Intermediate"
} else {
"Beginner"
};
let tier_name = if accuracy > 95.0 {
"Gold"
} else if accuracy > 90.0 {
"Silver"
} else {
"Bronze"
};
conn.execute(
"INSERT INTO stage_results (stage_id, session_id, repository_id, keystrokes, mistakes, duration_ms, wpm, cpm, accuracy, consistency_streaks, score, rank_name, tier_name, rank_position, rank_total, position, total, was_skipped, was_failed, completed_at, language, difficulty_level)
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)",
[
&stage.id as &dyn rusqlite::ToSql,
&stage.session_id,
&session.repository_id,
&keystrokes,
&mistakes,
&duration_ms,
&wpm,
&cpm,
&accuracy,
&"[5, 3, 8, 2]", // consistency_streaks JSON
&score,
&rank_name,
&tier_name,
&rank_position,
&rank_total,
&position,
&total,
&false, // was_skipped
&false, // was_failed
&completed_at.format("%Y-%m-%d %H:%M:%S").to_string(),
&challenge.language,
&challenge.difficulty_level,
],
)?;
}
}
println!("{} stages seeded", seed_data.stages.len());
Ok(())
}
}
impl HasDatabase for DatabaseSeeder {
fn database(&self) -> &Arc<dyn DatabaseInterface> {
&self.database
}
}
+4
View File
@@ -0,0 +1,4 @@
pub mod data;
pub mod database_seeder;
pub use database_seeder::DatabaseSeeder;
Generated
+62
View File
@@ -0,0 +1,62 @@
{
"nodes": {
"nixpkgs": {
"locked": {
"lastModified": 1779560665,
"narHash": "sha256-tpyBcxPpcQb8ukyNF7DoCwfSY3VPsxHoYwj00Cayv5o=",
"owner": "NixOS",
"repo": "nixpkgs",
"rev": "64c08a7ca051951c8eae34e3e3cb1e202fe36786",
"type": "github"
},
"original": {
"owner": "NixOS",
"ref": "nixos-unstable",
"repo": "nixpkgs",
"type": "github"
}
},
"nixpkgs_2": {
"locked": {
"lastModified": 1744536153,
"narHash": "sha256-awS2zRgF4uTwrOKwwiJcByDzDOdo3Q1rPZbiHQg/N38=",
"owner": "NixOS",
"repo": "nixpkgs",
"rev": "18dd725c29603f582cf1900e0d25f9f1063dbf11",
"type": "github"
},
"original": {
"owner": "NixOS",
"ref": "nixpkgs-unstable",
"repo": "nixpkgs",
"type": "github"
}
},
"root": {
"inputs": {
"nixpkgs": "nixpkgs",
"rust-overlay": "rust-overlay"
}
},
"rust-overlay": {
"inputs": {
"nixpkgs": "nixpkgs_2"
},
"locked": {
"lastModified": 1780197589,
"narHash": "sha256-FVCr2Ij/jKf59a4LW481eeOF6rJRreOBrVgW/aUBTrw=",
"owner": "oxalica",
"repo": "rust-overlay",
"rev": "21632e942d89bf1cce4e5a63d7e58a215a0cbfcc",
"type": "github"
},
"original": {
"owner": "oxalica",
"repo": "rust-overlay",
"type": "github"
}
}
},
"root": "root",
"version": 7
}
+83
View File
@@ -0,0 +1,83 @@
{
inputs = {
nixpkgs.url = "github:NixOS/nixpkgs/nixos-unstable";
rust-overlay.url = "github:oxalica/rust-overlay";
};
outputs = { self, nixpkgs, rust-overlay }:
let
cargoToml = builtins.fromTOML (builtins.readFile ./Cargo.toml);
pname = cargoToml.package.name;
version = cargoToml.package.version;
supportedSystems = [ "x86_64-linux" "aarch64-linux" "x86_64-darwin" "aarch64-darwin" ];
forAllSystems = f: nixpkgs.lib.genAttrs supportedSystems (system:
f {
pkgs = import nixpkgs {
inherit system;
overlays = [ rust-overlay.overlays.default ];
};
}
);
in
{
description = cargoToml.package.description;
packages = forAllSystems ({ pkgs }: {
default = pkgs.rustPlatform.buildRustPackage rec {
pname = "gittype";
version = "0.10.0";
src = pkgs.fetchFromGitHub {
owner = "unhappychoice";
repo = "gittype";
rev = "v${version}";
hash = "sha256-pzJWXVCGUn85OCHMRlMY5ufrGyJyuhhkYLUk4e01Ri0=";
};
cargoHash = "sha256-E1LKaiTClHmrF7zhGEj1rfELKryIiyVKIf/8Rozm1RQ=";
nativeBuildInputs = [ pkgs.perl pkgs.pkg-config pkgs.git ];
buildInputs = [ pkgs.openssl ];
doCheck = false;
};
unstable = let
rustToolchain = pkgs.rust-bin.stable.latest.default;
rustPlatform = pkgs.makeRustPlatform {
cargo = rustToolchain;
rustc = rustToolchain;
};
in
rustPlatform.buildRustPackage rec {
inherit pname version;
src = ./.;
cargoLock.lockFile = ./Cargo.lock;
nativeBuildInputs = [ pkgs.perl pkgs.pkg-config pkgs.git ];
buildInputs = [ pkgs.openssl ];
doCheck = false;
};
});
devShells = forAllSystems ({ pkgs }: {
default = pkgs.mkShell {
buildInputs = [
pkgs.rust-bin.stable.latest.default
pkgs.openssl
pkgs.pkg-config
pkgs.perl
pkgs.git
];
};
});
defaultPackage = forAllSystems ({ pkgs }: self.packages.${pkgs.system}.default);
defaultDevShell = forAllSystems ({ pkgs }: self.devShells.${pkgs.system}.default);
apps = forAllSystems ({ pkgs }: {
default = {
type = "app";
program = "${self.packages.${pkgs.system}.default}/bin/gittype";
};
unstable = {
type = "app";
program = "${self.packages.${pkgs.system}.unstable}/bin/gittype";
};
});
};
}
+168
View File
@@ -0,0 +1,168 @@
#!/bin/bash
# Generate CHANGELOG.md from git commit history
# Follows Conventional Commits format
set -e
CHANGELOG_FILE="CHANGELOG.md"
TEMP_FILE="${CHANGELOG_FILE}.tmp"
# Function to generate changelog section for a range of commits
function generate_section_for_commits() {
local from_ref="$1"
local to_ref="$2"
# Build git log range
if [ -z "$to_ref" ]; then
local range="$from_ref"
else
local range="${to_ref}..${from_ref}"
fi
# Get commits and categorize them
local features=""
local fixes=""
local breaking=""
local others=""
# Process commits
while IFS= read -r commit; do
if [ -z "$commit" ]; then
continue
fi
local message=$(echo "$commit" | cut -d' ' -f2-)
local commit_hash=$(echo "$commit" | cut -d' ' -f1)
# Get repository URL from git remote
local repo_url=$(git remote get-url origin | sed 's/\.git$//' | sed 's/git@github\.com:/https:\/\/github\.com\//')
# Categorize commit based on conventional commit format
case "$message" in
feat*|feature*)
features="${features}- ${message} ([${commit_hash:0:7}](${repo_url}/commit/${commit_hash}))\n"
;;
fix*)
fixes="${fixes}- ${message} ([${commit_hash:0:7}](${repo_url}/commit/${commit_hash}))\n"
;;
*"BREAKING CHANGE"*|*"!"*)
breaking="${breaking}- ${message} ([${commit_hash:0:7}](${repo_url}/commit/${commit_hash}))\n"
;;
chore*|docs*|style*|refactor*|test*|build*|ci*)
others="${others}- ${message} ([${commit_hash:0:7}](${repo_url}/commit/${commit_hash}))\n"
;;
"Merge pull request"*)
# Extract PR info
local pr_info=$(echo "$message" | sed 's/Merge pull request #\([0-9]*\) from .*/PR #\1/')
others="${others}- ${pr_info}: ${message} ([${commit_hash:0:7}](${repo_url}/commit/${commit_hash}))\n"
;;
*)
others="${others}- ${message} ([${commit_hash:0:7}](${repo_url}/commit/${commit_hash}))\n"
;;
esac
done < <(git log --oneline --no-merges "$range" 2>/dev/null || true)
# Output categorized changes
local has_content=false
if [ -n "$breaking" ]; then
echo "### 💥 BREAKING CHANGES"
echo ""
echo -e "$breaking"
has_content=true
fi
if [ -n "$features" ]; then
echo "### ✨ Features"
echo ""
echo -e "$features"
has_content=true
fi
if [ -n "$fixes" ]; then
echo "### 🐛 Bug Fixes"
echo ""
echo -e "$fixes"
has_content=true
fi
if [ -n "$others" ]; then
echo "### 📝 Other Changes"
echo ""
echo -e "$others"
has_content=true
fi
if [ "$has_content" = false ]; then
echo "- No notable changes"
echo ""
fi
echo ""
}
# Get the current version from Cargo.toml
CURRENT_VERSION=$(grep "^version" Cargo.toml | sed 's/version = "\(.*\)"/\1/')
# Create header
cat > "$TEMP_FILE" << EOF
# Changelog
All notable changes to this project will be documented in this file.
The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/),
and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
EOF
# Get all tags sorted by version (newest first)
TAGS=$(git tag -l --sort=-version:refname | grep -E '^v?[0-9]+\.[0-9]+\.[0-9]+' | head -20)
# If no tags exist, show unreleased changes
if [ -z "$TAGS" ]; then
echo "## [Unreleased]" >> "$TEMP_FILE"
echo "" >> "$TEMP_FILE"
generate_section_for_commits "HEAD" "" >> "$TEMP_FILE"
else
# Get latest tag
LATEST_TAG=$(echo "$TAGS" | head -1)
# Check if there are unreleased changes
UNRELEASED_COMMITS=$(git log --oneline "${LATEST_TAG}..HEAD" --pretty=format:"%s" | wc -l)
if [ "$UNRELEASED_COMMITS" -gt 0 ]; then
echo "## [Unreleased]" >> "$TEMP_FILE"
echo "" >> "$TEMP_FILE"
generate_section_for_commits "HEAD" "$LATEST_TAG" >> "$TEMP_FILE"
fi
# Generate sections for each tag
PREV_TAG=""
TAGS_ARRAY=($TAGS)
for i in "${!TAGS_ARRAY[@]}"; do
TAG="${TAGS_ARRAY[$i]}"
TAG_DATE=$(git log -1 --format="%ai" "$TAG" | cut -d' ' -f1)
CLEAN_VERSION=$(echo "$TAG" | sed 's/^v//')
echo "## [$CLEAN_VERSION] - $TAG_DATE" >> "$TEMP_FILE"
echo "" >> "$TEMP_FILE"
# Get the next (older) tag for the range
if [ $((i + 1)) -lt ${#TAGS_ARRAY[@]} ]; then
PREV_TAG="${TAGS_ARRAY[$((i + 1))]}"
else
PREV_TAG=""
fi
generate_section_for_commits "$TAG" "$PREV_TAG" >> "$TEMP_FILE"
done
fi
# Replace the old changelog with the new one
mv "$TEMP_FILE" "$CHANGELOG_FILE"
echo "✅ CHANGELOG.md has been generated successfully!"
echo "📄 Preview:"
echo "----------------------------------------"
head -20 "$CHANGELOG_FILE"
echo "----------------------------------------"
Executable
+253
View File
@@ -0,0 +1,253 @@
#!/bin/bash
# GitType installer script
# Usage: curl -sSL https://raw.githubusercontent.com/unhappychoice/gittype/main/install.sh | bash
set -e
# Colors for output
RED='\033[0;31m'
GREEN='\033[0;32m'
YELLOW='\033[1;33m'
BLUE='\033[0;34m'
NC='\033[0m' # No Color
# Default values
VERSION="latest"
INSTALL_DIR="/usr/local/bin"
# Help function
show_help() {
cat << EOF
GitType installer script
Usage: $0 [options]
Options:
-v, --version VERSION Install specific version (default: latest)
-d, --dir DIRECTORY Install directory (default: /usr/local/bin)
-h, --help Show this help message
Examples:
$0 # Install latest version to /usr/local/bin
$0 -v v0.5.0 # Install specific version
$0 -d ~/.local/bin # Install to user directory (no sudo required)
One-liner installation:
curl -sSL https://raw.githubusercontent.com/unhappychoice/gittype/main/install.sh | bash
EOF
}
# Parse command line arguments
while [[ $# -gt 0 ]]; do
case $1 in
-v|--version)
VERSION="$2"
shift 2
;;
-d|--dir)
INSTALL_DIR="$2"
shift 2
;;
-h|--help)
show_help
exit 0
;;
*)
echo -e "${RED}Unknown option: $1${NC}" >&2
show_help
exit 1
;;
esac
done
# Detect OS and architecture
detect_platform() {
local os
local arch
case "$(uname -s)" in
Darwin*)
os="apple-darwin"
;;
Linux*)
os="unknown-linux-gnu"
;;
MINGW*|MSYS*|CYGWIN*)
os="pc-windows-msvc"
;;
*)
echo -e "${RED}Unsupported operating system: $(uname -s)${NC}" >&2
exit 1
;;
esac
case "$(uname -m)" in
x86_64|amd64)
arch="x86_64"
;;
aarch64|arm64)
arch="aarch64"
;;
*)
echo -e "${RED}Unsupported architecture: $(uname -m)${NC}" >&2
exit 1
;;
esac
echo "${arch}-${os}"
}
# Get latest version from GitHub API
get_latest_version() {
curl -sSL https://api.github.com/repos/unhappychoice/gittype/releases/latest | \
grep '"tag_name":' | \
head -n 1 | \
cut -d'"' -f4
}
# Check if sudo is needed for installation directory
check_sudo_needed() {
local install_dir="$1"
# If directory doesn't exist, check parent directory
local check_dir="$install_dir"
while [[ ! -d "$check_dir" && "$check_dir" != "/" ]]; do
check_dir="$(dirname "$check_dir")"
done
# Test if we can actually write to the directory by creating a test file
local test_file="$check_dir/.gittype_install_test_$$"
if touch "$test_file" 2>/dev/null; then
rm -f "$test_file" 2>/dev/null
return 1 # No sudo needed
else
return 0 # Sudo needed
fi
}
# Download and install gittype
install_gittype() {
local platform
local download_url
local temp_dir
local binary_name="gittype"
platform=$(detect_platform)
if [[ "$VERSION" == "latest" ]]; then
echo -e "${BLUE}Fetching latest version...${NC}"
VERSION=$(get_latest_version)
fi
if [[ "$platform" == *"pc-windows-msvc"* ]]; then
download_url="https://github.com/unhappychoice/gittype/releases/download/${VERSION}/gittype-${VERSION}-${platform}.zip"
binary_name="gittype.exe"
else
download_url="https://github.com/unhappychoice/gittype/releases/download/${VERSION}/gittype-${VERSION}-${platform}.tar.gz"
fi
echo -e "${BLUE}Installing GitType ${VERSION} for ${platform}...${NC}"
echo -e "${BLUE}Download URL: ${download_url}${NC}"
# Create temporary directory
temp_dir=$(mktemp -d)
trap "rm -rf $temp_dir" EXIT
# Download archive
echo -e "${BLUE}Downloading...${NC}"
if ! curl -sSL "$download_url" -o "$temp_dir/gittype-archive"; then
echo -e "${RED}Failed to download GitType. Please check if version ${VERSION} exists.${NC}" >&2
exit 1
fi
# Extract archive
echo -e "${BLUE}Extracting...${NC}"
cd "$temp_dir"
if [[ "$download_url" == *.zip ]]; then
unzip -q gittype-archive
else
tar -xzf gittype-archive
fi
# Check if sudo is needed
local use_sudo=""
if check_sudo_needed "$INSTALL_DIR"; then
echo -e "${YELLOW}⚠️ Installing to ${INSTALL_DIR} requires sudo privileges${NC}"
# Check if sudo is available
if ! command -v sudo >/dev/null 2>&1; then
echo -e "${RED}Error: sudo is required but not available${NC}" >&2
echo -e "${BLUE}💡 Try installing to a user directory instead:${NC}"
if [[ "$(uname -s)" == "Darwin" ]]; then
echo -e "${BLUE} $0 -d \$HOME/.local/bin${NC}"
echo -e "${BLUE} $0 -d /opt/homebrew/bin # if using Homebrew${NC}"
else
echo -e "${BLUE} $0 -d \$HOME/.local/bin${NC}"
fi
exit 1
fi
echo -e "${BLUE}You may be prompted for your password...${NC}"
use_sudo="sudo"
fi
# Create install directory if it doesn't exist
if [[ -n "$use_sudo" ]]; then
sudo mkdir -p "$INSTALL_DIR"
else
mkdir -p "$INSTALL_DIR"
fi
# Install binary
echo -e "${BLUE}Installing to ${INSTALL_DIR}...${NC}"
if [[ -f "$binary_name" ]]; then
if [[ -n "$use_sudo" ]]; then
sudo cp "$binary_name" "$INSTALL_DIR/"
sudo chmod +x "$INSTALL_DIR/$binary_name"
else
cp "$binary_name" "$INSTALL_DIR/"
chmod +x "$INSTALL_DIR/$binary_name"
fi
else
echo -e "${RED}Binary not found in archive${NC}" >&2
exit 1
fi
echo -e "${GREEN}✅ GitType ${VERSION} installed successfully!${NC}"
echo -e "${GREEN} Location: ${INSTALL_DIR}/${binary_name}${NC}"
# Check if install directory is in PATH
if [[ ":$PATH:" != *":$INSTALL_DIR:"* ]]; then
echo -e "${YELLOW}⚠️ Warning: ${INSTALL_DIR} is not in your PATH${NC}"
echo -e "${YELLOW} Add it to your shell profile:${NC}"
echo -e "${YELLOW} export PATH=\"${INSTALL_DIR}:\$PATH\"${NC}"
echo
fi
# Test installation
if command -v gittype >/dev/null 2>&1; then
echo -e "${GREEN}🎮 Ready to play! Run 'gittype' to start typing practice${NC}"
else
echo -e "${BLUE}💡 Run '${INSTALL_DIR}/gittype' to start typing practice${NC}"
fi
}
# Main execution
main() {
echo -e "${GREEN}GitType Installation Script${NC}"
echo "================================="
echo
# Check required commands
for cmd in curl tar; do
if ! command -v $cmd >/dev/null 2>&1; then
echo -e "${RED}Error: $cmd is required but not installed${NC}" >&2
exit 1
fi
done
install_gittype
}
main "$@"
+79
View File
@@ -0,0 +1,79 @@
use std::path::PathBuf;
// Implement From for Box<dyn Any> downcast errors
impl From<Box<dyn std::any::Any + Send>> for GitTypeError {
fn from(_: Box<dyn std::any::Any + Send>) -> Self {
GitTypeError::ScreenInitializationError("Data type mismatch".to_string())
}
}
impl From<Box<dyn std::any::Any>> for GitTypeError {
fn from(_: Box<dyn std::any::Any>) -> Self {
GitTypeError::ScreenInitializationError("Data type mismatch".to_string())
}
}
#[derive(Debug, thiserror::Error)]
pub enum GitTypeError {
#[error("Repository path does not exist: {0}")]
RepositoryNotFound(PathBuf),
#[error("No supported files found in repository")]
NoSupportedFiles,
#[error("Failed to extract code chunks: {0}")]
ExtractionFailed(String),
#[error("Database error: {0}")]
DatabaseError(#[from] rusqlite::Error),
#[error("IO error: {0}")]
IoError(#[from] std::io::Error),
#[error("Glob pattern error: {0}")]
GlobPatternError(#[from] glob::PatternError),
#[error("Serialization error: {0}")]
SerializationError(#[from] serde_json::Error),
#[error("Terminal error: {0}")]
TerminalError(String),
#[error("Screen initialization error: {0}")]
ScreenInitializationError(String),
#[error("Walk directory error: {0}")]
WalkDirError(#[from] walkdir::Error),
#[error("Repository clone error: {0}")]
RepositoryCloneError(#[from] git2::Error),
#[error("Invalid repository format: {0}")]
InvalidRepositoryFormat(String),
#[error("Tree-sitter language error: {0}")]
TreeSitterLanguageError(#[from] tree_sitter::LanguageError),
#[error("Application panic: {0}")]
PanicError(String),
#[error("HTTP request error: {0}")]
HttpError(#[from] reqwest::Error),
#[error("API error: {0}")]
ApiError(String),
#[error("Validation error: {0}")]
ValidationError(String),
}
impl GitTypeError {
/// Create a custom database error from a string message
pub fn database_error(msg: String) -> Self {
Self::DatabaseError(rusqlite::Error::ToSqlConversionFailure(Box::new(
std::io::Error::other(msg),
)))
}
}
pub type Result<T> = std::result::Result<T, GitTypeError>;
+21
View File
@@ -0,0 +1,21 @@
use super::Event;
use std::any::Any;
use std::time::Instant;
// Unified domain event enum for pattern matching
#[derive(Debug, Clone)]
pub enum DomainEvent {
KeyPressed { key: char, position: usize },
StageStarted { start_time: Instant },
StagePaused,
StageResumed,
StageFinalized,
StageSkipped,
ChallengeLoaded { text: String, source_path: String },
}
impl Event for DomainEvent {
fn as_any(&self) -> &dyn Any {
self
}
}
+83
View File
@@ -0,0 +1,83 @@
use shaku::{Component, Interface};
use std::any::{Any, TypeId};
use std::collections::HashMap;
use std::sync::{Arc, RwLock};
pub mod domain_events;
pub mod presentation_events;
pub trait Event: Send + Sync + 'static {
fn as_any(&self) -> &dyn Any;
}
pub trait EventHandler<E: Event>: Send + Sync {
fn handle(&self, event: &E);
}
type BoxedHandler = Arc<dyn Fn(&dyn Event) + Send + Sync>;
pub trait EventBusInterface: Interface {
fn as_event_bus(&self) -> &EventBus;
}
#[derive(Clone, Component)]
#[shaku(interface = EventBusInterface)]
pub struct EventBus {
#[shaku(default)]
subscribers: Arc<RwLock<HashMap<TypeId, Vec<BoxedHandler>>>>,
}
impl EventBus {
pub fn new() -> Self {
Self {
subscribers: Arc::new(RwLock::new(HashMap::new())),
}
}
pub fn get_subscribers_ptr(&self) -> *const RwLock<HashMap<TypeId, Vec<BoxedHandler>>> {
Arc::as_ptr(&self.subscribers)
}
pub fn publish<E: Event>(&self, event: E) {
let type_id = TypeId::of::<E>();
let handlers: Vec<BoxedHandler> = {
let subscribers = self.subscribers.read().unwrap();
let h = subscribers.get(&type_id).cloned().unwrap_or_default();
h
};
for handler in handlers.iter() {
handler(&event);
}
}
pub fn subscribe<E: Event, F>(&self, handler: F)
where
F: Fn(&E) + Send + Sync + 'static,
{
let type_id = TypeId::of::<E>();
let mut subscribers = self.subscribers.write().unwrap();
let boxed_handler: BoxedHandler = Arc::new(move |event: &dyn Event| {
if let Some(concrete_event) = event.as_any().downcast_ref::<E>() {
handler(concrete_event);
}
});
subscribers.entry(type_id).or_default().push(boxed_handler);
}
}
impl EventBusInterface for EventBus {
fn as_event_bus(&self) -> &EventBus {
self
}
}
impl Default for EventBus {
fn default() -> Self {
Self::new()
}
}
+16
View File
@@ -0,0 +1,16 @@
use std::any::Any;
use crate::domain::events::Event;
/// Event emitted when user requests to exit the application (Ctrl+C)
#[derive(Clone, Debug)]
pub struct ExitRequested;
impl Event for ExitRequested {
fn as_any(&self) -> &dyn Any {
self
}
}
// Re-export ScreenTransition as NavigateTo event
pub use crate::presentation::tui::ScreenTransition as NavigateTo;
+6
View File
@@ -0,0 +1,6 @@
pub mod error;
pub mod events;
pub mod models;
pub mod repositories;
pub mod services;
pub mod stores;
+170
View File
@@ -0,0 +1,170 @@
use super::{git_repository::GitRepository, CodeChunk, DifficultyLevel};
use std::borrow::Cow;
use std::path::Path;
#[derive(Debug, Clone, PartialEq, serde::Serialize, serde::Deserialize)]
pub struct Challenge {
pub id: String,
pub source_file_path: Option<String>,
pub code_content: String,
pub start_line: Option<usize>,
pub end_line: Option<usize>,
pub language: Option<String>,
pub comment_ranges: Vec<(usize, usize)>, // Character-based ranges for comments
pub difficulty_level: Option<DifficultyLevel>,
}
impl Challenge {
pub fn new(id: String, code_content: String) -> Self {
Self {
id,
source_file_path: None,
code_content,
start_line: None,
end_line: None,
language: None,
comment_ranges: Vec::new(),
difficulty_level: None,
}
}
pub fn with_source_info(
mut self,
file_path: String,
start_line: usize,
end_line: usize,
) -> Self {
self.source_file_path = Some(file_path);
self.start_line = Some(start_line);
self.end_line = Some(end_line);
self
}
pub fn with_language(mut self, language: String) -> Self {
self.language = Some(language);
self
}
pub fn with_comment_ranges(mut self, comment_ranges: Vec<(usize, usize)>) -> Self {
self.comment_ranges = comment_ranges;
self
}
pub fn with_difficulty_level(mut self, difficulty_level: DifficultyLevel) -> Self {
self.difficulty_level = Some(difficulty_level);
self
}
pub fn from_chunk(chunk: &CodeChunk, difficulty: Option<DifficultyLevel>) -> Option<Self> {
use uuid::Uuid;
// Early validation
if chunk.content.trim().is_empty() {
return None;
}
let id = Uuid::new_v4().to_string();
let code_content = chunk.content.clone();
let source_file_path = Some(chunk.file_path.to_string_lossy().to_string());
let start_line = Some(chunk.start_line);
let end_line = Some(chunk.end_line);
let language = Some(chunk.language.clone());
Some(Self {
id,
code_content,
source_file_path,
start_line,
end_line,
language,
difficulty_level: difficulty,
comment_ranges: chunk.comment_ranges.clone(),
})
}
pub fn from_content_and_chunk(
content: Cow<str>,
chunk: &crate::domain::models::CodeChunk,
start_line: usize,
end_line: usize,
comment_ranges: &[(usize, usize)],
difficulty: Option<DifficultyLevel>,
) -> Self {
use uuid::Uuid;
let id = Uuid::new_v4().to_string();
let code_content = content.into_owned();
let source_file_path = Some(chunk.file_path.to_string_lossy().to_string());
let language = Some(chunk.language.clone());
Self {
id,
code_content,
source_file_path,
start_line: Some(start_line),
end_line: Some(end_line),
language,
difficulty_level: difficulty,
comment_ranges: comment_ranges.to_vec(),
}
}
pub fn get_display_title(&self) -> String {
if let Some(ref path) = self.source_file_path {
// Convert absolute path to relative path for cleaner display
let relative_path = self.get_relative_path(path);
if let (Some(start), Some(end)) = (self.start_line, self.end_line) {
format!("{}:{}-{}", relative_path, start, end)
} else {
relative_path
}
} else {
format!("Challenge {}", self.id)
}
}
pub fn get_display_title_with_repo(&self, repo_info: &Option<GitRepository>) -> String {
if let Some(ref path) = self.source_file_path {
let relative_path = self.get_relative_path(path);
let file_info = if let (Some(start), Some(end)) = (self.start_line, self.end_line) {
format!("{}:{}-{}", relative_path, start, end)
} else {
relative_path
};
if let Some(repo) = repo_info {
format!(
"[{}/{}] {}",
repo.user_name, repo.repository_name, file_info
)
} else {
file_info
}
} else {
format!("Challenge {}", self.id)
}
}
fn get_relative_path(&self, path: &str) -> String {
// Try to extract just the filename if it's a full path
if let Some(file_name) = Path::new(path).file_name() {
if let Some(parent) = Path::new(path).parent() {
if let Some(parent_name) = parent.file_name() {
// Show parent_dir/filename for better context
format!(
"{}/{}",
parent_name.to_string_lossy(),
file_name.to_string_lossy()
)
} else {
file_name.to_string_lossy().to_string()
}
} else {
file_name.to_string_lossy().to_string()
}
} else {
// Fallback to original path if extraction fails
path.to_string()
}
}
}
+41
View File
@@ -0,0 +1,41 @@
use std::path::PathBuf;
#[derive(Debug, Clone, PartialEq, Eq, Hash)]
pub enum ChunkType {
Function,
Class,
Method,
Struct,
Enum,
Trait,
TypeAlias,
Interface,
Module,
Const,
Variable,
Component, // JSX/TSX React components
Namespace, // For C# namespaces
// New chunk types for middle implementations
Loop, // for/while/loop constructs
Conditional, // if/switch/match statements
ErrorHandling, // try/catch/error handling blocks
FunctionCall, // function/method calls
Lambda, // closures, lambdas, arrow functions
SpecialBlock, // language-specific blocks (with, defer, etc.)
Comprehension, // list/dict comprehensions
CodeBlock, // generic code blocks
File, // entire file for Zen mode
}
#[derive(Debug, Clone)]
pub struct CodeChunk {
pub content: String,
pub file_path: PathBuf,
pub start_line: usize,
pub end_line: usize,
pub language: String,
pub chunk_type: ChunkType,
pub name: String,
pub comment_ranges: Vec<(usize, usize)>, // Character-based ranges for comments relative to content
pub original_indentation: usize, // Indentation width in characters (Extractor-normalized)
}
+8
View File
@@ -0,0 +1,8 @@
use serde::{Deserialize, Serialize};
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq, Hash, Default)]
pub enum ColorMode {
#[default]
Dark,
Light,
}
+238
View File
@@ -0,0 +1,238 @@
use ratatui::style::Color;
use serde::{Deserialize, Serialize};
use std::collections::HashMap;
use crate::domain::models::color_mode::ColorMode;
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct ThemeFile {
pub id: String,
pub name: String,
pub description: String,
pub dark: HashMap<String, SerializableColor>,
pub light: HashMap<String, SerializableColor>,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct CustomThemeFile {
pub dark: HashMap<String, SerializableColor>,
pub light: HashMap<String, SerializableColor>,
}
impl CustomThemeFile {
/// Convert CustomThemeFile to ThemeFile with fixed metadata
pub fn to_theme_file(&self) -> ThemeFile {
ThemeFile {
id: "custom".to_string(),
name: "Custom".to_string(),
description:
"Your personal custom theme - edit ~/.gittype/custom-theme.json to customize"
.to_string(),
dark: self.dark.clone(),
light: self.light.clone(),
}
}
}
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
#[serde(untagged)]
pub enum SerializableColor {
Rgb { r: u8, g: u8, b: u8 },
Name(String),
}
impl From<SerializableColor> for Color {
fn from(serializable_color: SerializableColor) -> Self {
match serializable_color {
SerializableColor::Rgb { r, g, b } => Color::Rgb(r, g, b),
SerializableColor::Name(name) => match name.as_str() {
"reset" => Color::Reset,
"black" => Color::Black,
"red" => Color::Red,
"green" => Color::Green,
"yellow" => Color::Yellow,
"blue" => Color::Blue,
"magenta" => Color::Magenta,
"cyan" => Color::Cyan,
"gray" => Color::Gray,
"dark_gray" => Color::DarkGray,
"light_red" => Color::LightRed,
"light_green" => Color::LightGreen,
"light_yellow" => Color::LightYellow,
"light_blue" => Color::LightBlue,
"light_magenta" => Color::LightMagenta,
"light_cyan" => Color::LightCyan,
"white" => Color::White,
// Add more named colors as needed
_ => {
// Try to parse as hex color (#RRGGBB or #RGB)
if let Some(hex) = name.strip_prefix('#') {
if hex.len() == 6 {
if let Ok(rgb) = u32::from_str_radix(hex, 16) {
let r = ((rgb >> 16) & 0xFF) as u8;
let g = ((rgb >> 8) & 0xFF) as u8;
let b = (rgb & 0xFF) as u8;
return Color::Rgb(r, g, b);
}
} else if hex.len() == 3 {
if let Ok(rgb) = u32::from_str_radix(hex, 16) {
let r = (((rgb >> 8) & 0xF) * 0x11) as u8;
let g = (((rgb >> 4) & 0xF) * 0x11) as u8;
let b = ((rgb & 0xF) * 0x11) as u8;
return Color::Rgb(r, g, b);
}
}
}
// Fallback to white for unknown color names
Color::White
}
},
}
}
}
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
pub struct ColorScheme {
// Primary colors for main UI elements
pub border: SerializableColor,
pub title: SerializableColor,
pub text: SerializableColor,
pub text_secondary: SerializableColor,
pub background: SerializableColor,
pub background_secondary: SerializableColor,
// Status and feedback colors
pub status_success: SerializableColor,
pub status_error: SerializableColor,
pub status_warning: SerializableColor,
pub status_info: SerializableColor,
// Specific UI element colors
pub key_action: SerializableColor,
pub key_navigation: SerializableColor,
pub key_back: SerializableColor,
// Metrics and performance colors
pub metrics_score: SerializableColor,
pub metrics_cpm_wpm: SerializableColor,
pub metrics_accuracy: SerializableColor,
pub metrics_duration: SerializableColor,
pub metrics_stage_info: SerializableColor,
// Typing interface colors
pub typing_typed_text: SerializableColor,
pub typing_cursor_fg: SerializableColor,
pub typing_cursor_bg: SerializableColor,
pub typing_mistake_bg: SerializableColor,
pub typing_untyped_text: SerializableColor,
}
impl ColorScheme {
/// Create ColorScheme from ThemeFile for specific color mode
pub fn from_theme_file(theme_file: &ThemeFile, color_mode: &ColorMode) -> Self {
let colors = match color_mode {
ColorMode::Dark => &theme_file.dark,
ColorMode::Light => &theme_file.light,
};
Self {
border: colors
.get("border")
.cloned()
.unwrap_or(SerializableColor::Name("blue".to_string())),
title: colors
.get("title")
.cloned()
.unwrap_or(SerializableColor::Name("white".to_string())),
text: colors
.get("text")
.cloned()
.unwrap_or(SerializableColor::Name("white".to_string())),
text_secondary: colors
.get("text_secondary")
.cloned()
.unwrap_or(SerializableColor::Name("gray".to_string())),
background: colors
.get("background")
.cloned()
.unwrap_or(SerializableColor::Name("black".to_string())),
background_secondary: colors
.get("background_secondary")
.cloned()
.unwrap_or(SerializableColor::Name("black".to_string())),
status_success: colors
.get("status_success")
.cloned()
.unwrap_or(SerializableColor::Name("green".to_string())),
status_error: colors
.get("status_error")
.cloned()
.unwrap_or(SerializableColor::Name("red".to_string())),
status_warning: colors
.get("status_warning")
.cloned()
.unwrap_or(SerializableColor::Name("yellow".to_string())),
status_info: colors
.get("status_info")
.cloned()
.unwrap_or(SerializableColor::Name("blue".to_string())),
key_action: colors
.get("key_action")
.cloned()
.unwrap_or(SerializableColor::Name("blue".to_string())),
key_navigation: colors
.get("key_navigation")
.cloned()
.unwrap_or(SerializableColor::Name("blue".to_string())),
key_back: colors
.get("key_back")
.cloned()
.unwrap_or(SerializableColor::Name("red".to_string())),
metrics_score: colors
.get("metrics_score")
.cloned()
.unwrap_or(SerializableColor::Name("magenta".to_string())),
metrics_cpm_wpm: colors
.get("metrics_cpm_wpm")
.cloned()
.unwrap_or(SerializableColor::Name("green".to_string())),
metrics_accuracy: colors
.get("metrics_accuracy")
.cloned()
.unwrap_or(SerializableColor::Name("yellow".to_string())),
metrics_duration: colors
.get("metrics_duration")
.cloned()
.unwrap_or(SerializableColor::Name("cyan".to_string())),
metrics_stage_info: colors
.get("metrics_stage_info")
.cloned()
.unwrap_or(SerializableColor::Name("blue".to_string())),
typing_typed_text: colors
.get("typing_typed_text")
.cloned()
.unwrap_or(SerializableColor::Name("green".to_string())),
typing_cursor_fg: colors
.get("typing_cursor_fg")
.cloned()
.unwrap_or(SerializableColor::Name("white".to_string())),
typing_cursor_bg: colors
.get("typing_cursor_bg")
.cloned()
.unwrap_or(SerializableColor::Name("blue".to_string())),
typing_mistake_bg: colors
.get("typing_mistake_bg")
.cloned()
.unwrap_or(SerializableColor::Name("red".to_string())),
typing_untyped_text: colors
.get("typing_untyped_text")
.cloned()
.unwrap_or(SerializableColor::Name("gray".to_string())),
}
}
}
+28
View File
@@ -0,0 +1,28 @@
use serde::{Deserialize, Serialize};
use crate::domain::models::color_mode::ColorMode;
#[derive(Debug, Clone, Serialize, Deserialize, Default)]
pub struct Config {
pub theme: ThemeConfig,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct ThemeConfig {
#[serde(default = "default_theme_id")]
pub current_theme_id: String,
pub current_color_mode: ColorMode,
}
impl Default for ThemeConfig {
fn default() -> Self {
Self {
current_theme_id: "default".to_string(),
current_color_mode: ColorMode::default(),
}
}
}
fn default_theme_id() -> String {
"default".to_string()
}
+124
View File
@@ -0,0 +1,124 @@
use std::time::{Duration, Instant};
pub struct Countdown {
active: bool,
current_number: Option<u8>,
start_time: Option<Instant>,
pause_time: Option<Instant>,
total_paused: Duration,
}
impl Countdown {
pub fn new() -> Self {
Self {
active: false,
current_number: None,
start_time: None,
pause_time: None,
total_paused: Duration::ZERO,
}
}
pub fn start_countdown(&mut self) {
self.active = true;
self.current_number = Some(3);
self.start_time = Some(Instant::now());
self.pause_time = None;
self.total_paused = Duration::ZERO;
}
pub fn update_state(&mut self) -> Option<Instant> {
if !self.active {
return None;
}
if let Some(current_num) = self.current_number {
if let Some(start_time) = self.start_time {
// Calculate elapsed time excluding paused duration
let total_elapsed = start_time.elapsed();
let current_paused = if let Some(pause_time) = self.pause_time {
self.total_paused + pause_time.elapsed()
} else {
self.total_paused
};
let elapsed = total_elapsed.saturating_sub(current_paused);
let required_duration = if current_num == 0 {
// GO! shows for 400ms (shorter duration)
Duration::from_millis(400)
} else {
// Numbers 3, 2, 1 show for 600ms each
Duration::from_millis(600)
};
if elapsed >= required_duration {
if current_num > 1 {
// Move to next countdown number
self.current_number = Some(current_num - 1);
self.start_time = Some(Instant::now());
self.pause_time = None;
self.total_paused = Duration::ZERO;
} else if current_num == 1 {
// Show "GO!" for a brief moment
self.current_number = Some(0); // 0 represents "GO!"
self.start_time = Some(Instant::now());
self.pause_time = None;
self.total_paused = Duration::ZERO;
} else {
// Countdown finished, start typing
self.active = false;
self.current_number = None;
self.start_time = None;
self.pause_time = None;
self.total_paused = Duration::ZERO;
// Return the timestamp for when typing should start
return Some(Instant::now());
}
}
}
}
None
}
pub fn get_current_count(&self) -> Option<u8> {
if self.active {
self.current_number
} else {
None
}
}
pub fn is_active(&self) -> bool {
self.active
}
pub fn pause(&mut self) {
if self.active && self.pause_time.is_none() {
self.pause_time = Some(Instant::now());
}
}
pub fn resume(&mut self) {
if let Some(pause_time) = self.pause_time.take() {
self.total_paused += pause_time.elapsed();
}
}
#[cfg_attr(not(test), allow(dead_code))]
pub fn fast_forward_for_test(&mut self, duration: Duration) {
if let Some(start_time) = self.start_time {
self.start_time = start_time.checked_sub(duration).or(Some(start_time));
}
if let Some(pause_time) = self.pause_time {
self.pause_time = pause_time.checked_sub(duration).or(Some(pause_time));
}
}
}
impl Default for Countdown {
fn default() -> Self {
Self::new()
}
}
+67
View File
@@ -0,0 +1,67 @@
use crate::domain::models::{ChunkType, CodeChunk};
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, serde::Serialize, serde::Deserialize)]
pub enum DifficultyLevel {
Easy, // ~100 characters
Normal, // ~200 characters
Hard, // ~500 characters
Wild, // Entire chunks, unpredictable length
Zen, // Entire file
}
impl DifficultyLevel {
pub fn char_limits(&self) -> (usize, usize) {
match self {
DifficultyLevel::Easy => (20, 100),
DifficultyLevel::Normal => (80, 200),
DifficultyLevel::Hard => (180, 500),
DifficultyLevel::Wild => (0, usize::MAX), // No limits - full chunks
DifficultyLevel::Zen => (0, usize::MAX),
}
}
pub fn description(&self) -> &'static str {
match self {
DifficultyLevel::Easy => "~100 characters",
DifficultyLevel::Normal => "~200 characters",
DifficultyLevel::Hard => "~500 characters",
DifficultyLevel::Wild => "Full chunks",
DifficultyLevel::Zen => "Entire files",
}
}
pub fn subtitle(&self) -> &'static str {
match self {
DifficultyLevel::Easy => "Short code snippets",
DifficultyLevel::Normal => "Medium functions",
DifficultyLevel::Hard => "Long functions or classes",
DifficultyLevel::Wild => "Unpredictable length chunks",
DifficultyLevel::Zen => "Complete files as challenges",
}
}
/// Returns all applicable difficulty levels for a chunk with given character count and type
pub fn applicable_difficulties(
chunk: &CodeChunk,
code_char_count: usize,
) -> Vec<DifficultyLevel> {
[
DifficultyLevel::Easy,
DifficultyLevel::Normal,
DifficultyLevel::Hard,
DifficultyLevel::Wild,
DifficultyLevel::Zen,
]
.iter()
.filter(|&difficulty| match difficulty {
DifficultyLevel::Zen => matches!(chunk.chunk_type, ChunkType::File),
DifficultyLevel::Wild => true,
_ => {
let (min_chars, _) = difficulty.char_limits();
code_char_count >= min_chars
}
})
.copied()
.collect()
}
}
+119
View File
@@ -0,0 +1,119 @@
use crate::domain::models::Languages;
#[derive(Debug, Clone)]
pub struct ExtractionOptions {
pub include_patterns: Vec<String>,
pub exclude_patterns: Vec<String>,
pub languages: Option<Vec<String>>,
/// Maximum file size in bytes to process (default: 2MB)
pub max_file_size_bytes: u64,
}
impl Default for ExtractionOptions {
fn default() -> Self {
Self {
include_patterns: Languages::all_file_patterns(),
exclude_patterns: vec![
// === Common build directories ===
"**/build/**".to_string(),
"**/dist/**".to_string(),
"**/target/**".to_string(),
"**/bin/**".to_string(),
"**/obj/**".to_string(),
// === Dependency directories ===
"**/node_modules/**".to_string(), // JavaScript/TypeScript
"**/vendor/**".to_string(), // Go, PHP, Ruby
// === Language-specific patterns ===
// Python
"**/__pycache__/**".to_string(),
"**/*.pyc".to_string(),
"**/venv/**".to_string(),
"**/.venv/**".to_string(),
"**/env/**".to_string(),
// JavaScript/TypeScript
"**/.next/**".to_string(), // Next.js
"**/.nuxt/**".to_string(), // Nuxt.js
"**/coverage/**".to_string(), // Test coverage
"**/.nyc_output/**".to_string(), // NYC coverage tool
// Java/Kotlin
"**/*.class".to_string(),
"**/gradle/**".to_string(), // Gradle wrapper
"**/.gradle/**".to_string(), // Gradle cache
"**/buildSrc/**".to_string(), // Gradle buildSrc
"**/.m2/**".to_string(), // Maven cache
"**/.ivy2/**".to_string(), // SBT/Ivy cache
// Ruby
"**/bundle/**".to_string(), // Bundler
"**/.bundle/**".to_string(), // Bundler cache
// Swift/iOS
"**/.build/**".to_string(),
"**/DerivedData/**".to_string(),
"**/Pods/**".to_string(), // CocoaPods
"**/Carthage/**".to_string(), // Carthage
// C#/.NET
"**/packages.config".to_string(), // NuGet packages.config
"**/packages/**/*.dll".to_string(), // NuGet compiled libraries
"**/packages/**/*.pdb".to_string(), // NuGet debug symbols
"**/packages/**/*.xml".to_string(), // NuGet documentation
// C/C++
"**/*.o".to_string(),
"**/*.so".to_string(),
"**/*.a".to_string(),
"**/CMakeFiles/**".to_string(),
"**/cmake-build-*/**".to_string(),
"**/.vs/**".to_string(), // Visual Studio
"**/x64/**".to_string(), // VS build dirs
"**/x86/**".to_string(), // VS build dirs
"**/Debug/**".to_string(), // VS/MSBuild
"**/Release/**".to_string(), // VS/MSBuild
// Dart/Flutter
"**/.dart_tool/**".to_string(),
// Haskell
"**/.stack-work/**".to_string(),
"**/dist-newstyle/**".to_string(),
// === Generated code ===
"**/generated/**".to_string(),
"**/.generated/**".to_string(),
"**/gen/**".to_string(),
"**/codegen/**".to_string(),
"**/*_pb2.py".to_string(), // Python protobuf
"**/*.pb.go".to_string(), // Go protobuf
// === Build tools and caches ===
"**/bazel-*/**".to_string(), // Bazel
// === System and temporary files ===
"**/.git/**".to_string(),
"**/tmp/**".to_string(),
"**/temp/**".to_string(),
"**/*.tmp".to_string(),
"**/cache/**".to_string(),
"**/.cache/**".to_string(),
"**/logs/**".to_string(),
"**/*.log".to_string(),
// === Performance test files (large generated files) ===
"**/colorize-fixtures/**".to_string(),
"**/perf-tests/**".to_string(),
],
languages: None,
max_file_size_bytes: 1024 * 1024, // 1MB limit
}
}
}
impl ExtractionOptions {
pub fn apply_language_filter(&mut self) {
if let Some(ref languages) = self.languages {
let registry = Languages::all_languages();
self.include_patterns = registry
.into_iter()
.filter(|lang| {
languages.iter().any(|name| {
let name_lower = name.to_lowercase();
name_lower == lang.name() || lang.aliases().contains(&name_lower.as_str())
})
})
.flat_map(|lang| lang.file_patterns())
.collect();
}
}
}
+73
View File
@@ -0,0 +1,73 @@
use serde::{Deserialize, Serialize};
use std::path::PathBuf;
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
pub struct GitRepository {
pub user_name: String,
pub repository_name: String,
pub remote_url: String,
pub branch: Option<String>,
pub commit_hash: Option<String>,
pub is_dirty: bool,
pub root_path: Option<PathBuf>,
}
impl GitRepository {
/// Generate a cache key from the repository URL.
/// Supports multiple URL formats:
/// - https://github.com/owner/repo -> github_com_owner_repo
/// - git@github.com:owner/repo -> github_com_owner_repo
/// - ssh://git@github.com/owner/repo -> github_com_owner_repo
pub fn cache_key(&self) -> String {
Self::extract_cache_key(&self.remote_url)
}
fn extract_cache_key(repo_url: &str) -> String {
// Handle SSH format: git@host:owner/repo
if let Some(ssh_part) = repo_url.strip_prefix("git@") {
if let Some(colon_pos) = ssh_part.find(':') {
let host = &ssh_part[..colon_pos];
let path = &ssh_part[colon_pos + 1..];
let parts: Vec<&str> = path.split('/').collect();
if parts.len() >= 2 {
let host_clean = host.replace('.', "_");
let owner = parts[0];
let repo = parts[1].trim_end_matches(".git");
return format!("{}_{}_{}", host_clean, owner, repo);
}
}
}
// Handle ssh:// format: ssh://git@host/owner/repo
if let Some(ssh_url) = repo_url.strip_prefix("ssh://") {
if let Some(at_pos) = ssh_url.find('@') {
let host_path = &ssh_url[at_pos + 1..];
let parts: Vec<&str> = host_path.split('/').collect();
if parts.len() >= 3 {
let host = parts[0].replace('.', "_");
let owner = parts[1];
let repo = parts[2].trim_end_matches(".git");
return format!("{}_{}_{}", host, owner, repo);
}
}
}
// Handle HTTP(S) format: https://github.com/owner/repo
if let Some(url_without_protocol) = repo_url
.strip_prefix("https://")
.or_else(|| repo_url.strip_prefix("http://"))
{
let parts: Vec<&str> = url_without_protocol.split('/').collect();
if parts.len() >= 3 {
let host = parts[0].replace('.', "_");
let owner = parts[1];
let repo = parts[2].trim_end_matches(".git");
return format!("{}_{}_{}", host, owner, repo);
}
}
// Fallback for malformed URLs
repo_url.replace(['/', ':', '.'], "_")
}
}
+12
View File
@@ -0,0 +1,12 @@
#[derive(Debug, Clone)]
pub struct GitRepositoryRef {
pub origin: String,
pub owner: String,
pub name: String,
}
impl GitRepositoryRef {
pub fn http_url(&self) -> String {
format!("https://{}/{}/{}.git", self.origin, self.owner, self.name)
}
}
+182
View File
@@ -0,0 +1,182 @@
use std::hash::{Hash, Hasher};
use crate::domain::models::languages::{
CSharp, Clojure, Cpp, Dart, Elixir, Erlang, Go, Haskell, Java, JavaScript, Kotlin, Php, Python,
Ruby, Rust, Scala, Swift, TypeScript, Zig, C,
};
/// Domain trait representing a programming language
pub trait Language: std::fmt::Debug + Send + Sync {
/// Returns the internal name of the language
fn name(&self) -> &'static str;
/// Returns the display name of the language (defaults to name)
fn display_name(&self) -> &'static str {
self.name()
}
/// Returns the file extensions for this language
fn extensions(&self) -> Vec<&'static str>;
/// Returns alternative names/aliases for this language
fn aliases(&self) -> Vec<&'static str> {
vec![]
}
/// Returns file glob patterns for this language
fn file_patterns(&self) -> Vec<String> {
self.extensions()
.into_iter()
.map(|ext| format!("**/*.{}", ext))
.collect()
}
/// Returns a unique hash key for this language
fn as_hash_key(&self) -> &'static str {
self.name()
}
/// Returns true if the tree-sitter node represents a valid comment for this language
fn is_valid_comment_node(&self, node: tree_sitter::Node) -> bool;
/// Returns the color for this language
fn color(&self) -> ratatui::style::Color {
use ratatui::style::Color;
match self.name() {
"rust" => Color::Red,
"python" => Color::Blue,
"javascript" => Color::Yellow,
"typescript" => Color::Blue,
"java" => Color::Red,
"c" => Color::Blue,
"cpp" => Color::Cyan,
"csharp" => Color::Magenta,
"go" => Color::Cyan,
"ruby" => Color::Red,
"php" => Color::Magenta,
"swift" => Color::Red,
"kotlin" => Color::Magenta,
"scala" => Color::Red,
"haskell" => Color::Magenta,
"dart" => Color::Cyan,
"elixir" => Color::Magenta,
"erlang" => Color::Red,
"zig" => Color::Yellow,
_ => Color::White,
}
}
}
pub struct Languages;
impl Languages {
pub fn get_language_by_name(name: &str) -> Option<Box<dyn Language>> {
Self::get_by_name(name)
}
}
impl Languages {
pub fn all_languages() -> Vec<Box<dyn Language>> {
vec![
Box::new(Rust),
Box::new(TypeScript),
Box::new(JavaScript),
Box::new(Python),
Box::new(Ruby),
Box::new(Go),
Box::new(Swift),
Box::new(Kotlin),
Box::new(Java),
Box::new(Php),
Box::new(CSharp),
Box::new(C),
Box::new(Cpp),
Box::new(Haskell),
Box::new(Dart),
Box::new(Scala),
Box::new(Zig),
Box::new(Clojure),
Box::new(Elixir),
Box::new(Erlang),
]
}
pub fn all_file_patterns() -> Vec<String> {
Self::all_languages()
.into_iter()
.flat_map(|lang| lang.file_patterns())
.collect()
}
pub fn get_supported_languages() -> Vec<&'static str> {
Self::all_languages()
.into_iter()
.flat_map(|lang| {
let mut names = vec![lang.name()];
names.extend(lang.aliases());
names
})
.collect()
}
pub fn validate_languages(languages: &[String]) -> Result<(), Vec<String>> {
let supported = Self::get_supported_languages();
let unsupported: Vec<String> = languages
.iter()
.filter(|lang| !supported.contains(&lang.to_lowercase().as_str()))
.cloned()
.collect();
if unsupported.is_empty() {
Ok(())
} else {
Err(unsupported)
}
}
pub fn from_extension(extension: &str) -> Option<Box<dyn Language>> {
Self::all_languages()
.into_iter()
.find(|lang| lang.extensions().contains(&extension))
}
pub fn detect_from_path(path: &std::path::Path) -> String {
match path.extension().and_then(|ext| ext.to_str()) {
Some(ext) => Self::from_extension(ext)
.map(|lang| lang.name().to_string())
.unwrap_or_else(|| "text".to_string()),
None => "text".to_string(),
}
}
pub fn get_by_name(name: &str) -> Option<Box<dyn Language>> {
let name_lower = name.to_lowercase();
Self::all_languages()
.into_iter()
.find(|lang| lang.name() == name_lower || lang.aliases().contains(&name_lower.as_str()))
}
pub fn get_display_name(language: Option<&str>) -> String {
match language {
Some(lang) => Self::get_by_name(lang)
.map(|l| l.display_name().to_string())
.unwrap_or_else(|| lang.to_string()),
None => "Unknown".to_string(),
}
}
}
// Implement Hash for Box<dyn Language>
impl Hash for dyn Language {
fn hash<H: Hasher>(&self, state: &mut H) {
self.as_hash_key().hash(state);
}
}
impl PartialEq for dyn Language {
fn eq(&self, other: &Self) -> bool {
self.as_hash_key() == other.as_hash_key()
}
}
impl Eq for dyn Language {}
+22
View File
@@ -0,0 +1,22 @@
use crate::domain::models::Language;
use std::hash::Hash;
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
pub struct C;
impl Language for C {
fn name(&self) -> &'static str {
"c"
}
fn extensions(&self) -> Vec<&'static str> {
vec!["c", "h"]
}
fn display_name(&self) -> &'static str {
"C"
}
fn is_valid_comment_node(&self, node: tree_sitter::Node) -> bool {
let node_kind = node.kind();
node_kind == "comment"
}
}
+24
View File
@@ -0,0 +1,24 @@
use crate::domain::models::Language;
use std::hash::Hash;
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
pub struct Clojure;
impl Language for Clojure {
fn name(&self) -> &'static str {
"clojure"
}
fn extensions(&self) -> Vec<&'static str> {
vec!["clj", "cljs", "cljc"]
}
fn aliases(&self) -> Vec<&'static str> {
vec!["clojure", "clj", "cljs"]
}
fn display_name(&self) -> &'static str {
"Clojure"
}
fn is_valid_comment_node(&self, node: tree_sitter::Node) -> bool {
node.kind() == "comment"
}
}
+25
View File
@@ -0,0 +1,25 @@
use crate::domain::models::Language;
use std::hash::Hash;
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
pub struct Cpp;
impl Language for Cpp {
fn name(&self) -> &'static str {
"cpp"
}
fn extensions(&self) -> Vec<&'static str> {
vec!["cpp", "cc", "cxx", "hpp"]
}
fn aliases(&self) -> Vec<&'static str> {
vec!["c++"]
}
fn display_name(&self) -> &'static str {
"C++"
}
fn is_valid_comment_node(&self, node: tree_sitter::Node) -> bool {
let node_kind = node.kind();
node_kind == "comment"
}
}
+25
View File
@@ -0,0 +1,25 @@
use crate::domain::models::Language;
use std::hash::Hash;
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
pub struct CSharp;
impl Language for CSharp {
fn name(&self) -> &'static str {
"csharp"
}
fn extensions(&self) -> Vec<&'static str> {
vec!["cs", "csx"]
}
fn aliases(&self) -> Vec<&'static str> {
vec!["cs", "c#"]
}
fn display_name(&self) -> &'static str {
"C#"
}
fn is_valid_comment_node(&self, node: tree_sitter::Node) -> bool {
let node_kind = node.kind();
node_kind == "comment"
}
}
+22
View File
@@ -0,0 +1,22 @@
use crate::domain::models::Language;
use std::hash::Hash;
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
pub struct Dart;
impl Language for Dart {
fn name(&self) -> &'static str {
"dart"
}
fn extensions(&self) -> Vec<&'static str> {
vec!["dart"]
}
fn display_name(&self) -> &'static str {
"Dart"
}
fn is_valid_comment_node(&self, node: tree_sitter::Node) -> bool {
let node_kind = node.kind();
node_kind == "comment" || node_kind == "documentation_comment"
}
}
+24
View File
@@ -0,0 +1,24 @@
use crate::domain::models::Language;
use std::hash::Hash;
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
pub struct Elixir;
impl Language for Elixir {
fn name(&self) -> &'static str {
"elixir"
}
fn extensions(&self) -> Vec<&'static str> {
vec!["ex", "exs"]
}
fn aliases(&self) -> Vec<&'static str> {
vec!["ex", "exs"]
}
fn display_name(&self) -> &'static str {
"Elixir"
}
fn is_valid_comment_node(&self, node: tree_sitter::Node) -> bool {
node.kind() == "comment"
}
}
+24
View File
@@ -0,0 +1,24 @@
use crate::domain::models::Language;
use std::hash::Hash;
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
pub struct Erlang;
impl Language for Erlang {
fn name(&self) -> &'static str {
"erlang"
}
fn extensions(&self) -> Vec<&'static str> {
vec!["erl", "hrl"]
}
fn aliases(&self) -> Vec<&'static str> {
vec!["erl"]
}
fn display_name(&self) -> &'static str {
"Erlang"
}
fn is_valid_comment_node(&self, node: tree_sitter::Node) -> bool {
node.kind() == "comment"
}
}
+22
View File
@@ -0,0 +1,22 @@
use crate::domain::models::Language;
use std::hash::Hash;
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
pub struct Go;
impl Language for Go {
fn name(&self) -> &'static str {
"go"
}
fn extensions(&self) -> Vec<&'static str> {
vec!["go"]
}
fn display_name(&self) -> &'static str {
"Go"
}
fn is_valid_comment_node(&self, node: tree_sitter::Node) -> bool {
let node_kind = node.kind();
node_kind == "comment"
}
}
+25
View File
@@ -0,0 +1,25 @@
use crate::domain::models::Language;
use std::hash::Hash;
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
pub struct Haskell;
impl Language for Haskell {
fn name(&self) -> &'static str {
"haskell"
}
fn extensions(&self) -> Vec<&'static str> {
vec!["hs", "lhs"]
}
fn aliases(&self) -> Vec<&'static str> {
vec!["hs"]
}
fn display_name(&self) -> &'static str {
"Haskell"
}
fn is_valid_comment_node(&self, node: tree_sitter::Node) -> bool {
let node_kind = node.kind();
node_kind == "comment" || node_kind == "haddock"
}
}
+22
View File
@@ -0,0 +1,22 @@
use crate::domain::models::Language;
use std::hash::Hash;
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
pub struct Java;
impl Language for Java {
fn name(&self) -> &'static str {
"java"
}
fn extensions(&self) -> Vec<&'static str> {
vec!["java"]
}
fn display_name(&self) -> &'static str {
"Java"
}
fn is_valid_comment_node(&self, node: tree_sitter::Node) -> bool {
let node_kind = node.kind();
node_kind == "line_comment" || node_kind == "block_comment"
}
}

Some files were not shown because too many files have changed in this diff Show More