chore: import upstream snapshot with attribution
@@ -0,0 +1,11 @@
|
||||
version: 2
|
||||
updates:
|
||||
- package-ecosystem: "cargo"
|
||||
directory: "/"
|
||||
schedule:
|
||||
interval: "daily"
|
||||
open-pull-requests-limit: 25
|
||||
reviewers:
|
||||
- "unhappychoice"
|
||||
assignees:
|
||||
- "unhappychoice"
|
||||
@@ -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
|
||||
@@ -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 }}
|
||||
@@ -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
|
||||
@@ -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 ~10–15 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.
|
||||
@@ -0,0 +1,2 @@
|
||||
# Global rule - @unhappychoice owns everything
|
||||
* @unhappychoice
|
||||
@@ -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
|
||||
|
||||
|
||||
|
||||
@@ -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.
|
||||
@@ -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 📸
|
||||
|
||||

|
||||
|
||||

|
||||
|
||||

|
||||
|
||||

|
||||
|
||||

|
||||
|
||||

|
||||
|
||||

|
||||
|
||||

|
||||
|
||||

|
||||
|
||||

|
||||
|
||||

|
||||
|
||||
## 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!
|
||||
@@ -0,0 +1,7 @@
|
||||
# WeHub 来源说明
|
||||
|
||||
- 原始项目:`unhappychoice/gittype`
|
||||
- 原始仓库:https://github.com/unhappychoice/gittype
|
||||
- 导入方式:上游默认分支的最新快照
|
||||
- 原作者、版权和许可证信息以原始仓库及本仓库 LICENSE 为准
|
||||
- 本文件仅用于记录来源,不代表 WeHub 是原项目作者
|
||||
@@ -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}}
|
||||
@@ -0,0 +1,9 @@
|
||||
accepted = [
|
||||
"Apache-2.0",
|
||||
"MIT",
|
||||
"BSD-3-Clause",
|
||||
"Unicode-3.0",
|
||||
"Zlib",
|
||||
"0BSD",
|
||||
"MPL-2.0",
|
||||
]
|
||||
@@ -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"
|
||||
}
|
||||
@@ -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}
|
||||
}
|
||||
@@ -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}
|
||||
}
|
||||
@@ -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"
|
||||
}
|
||||
}
|
||||
@@ -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 }
|
||||
}
|
||||
}
|
||||
@@ -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 }
|
||||
}
|
||||
}
|
||||
@@ -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 }
|
||||
}
|
||||
}
|
||||
@@ -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}
|
||||
}
|
||||
}
|
||||
@@ -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 }
|
||||
}
|
||||
}
|
||||
@@ -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 }
|
||||
}
|
||||
}
|
||||
@@ -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 }
|
||||
}
|
||||
}
|
||||
@@ -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 }
|
||||
}
|
||||
}
|
||||
@@ -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 }
|
||||
}
|
||||
}
|
||||
@@ -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}
|
||||
}
|
||||
}
|
||||
@@ -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 }
|
||||
}
|
||||
}
|
||||
@@ -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 }
|
||||
}
|
||||
}
|
||||
@@ -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 }
|
||||
}
|
||||
}
|
||||
@@ -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 }
|
||||
}
|
||||
}
|
||||
@@ -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);
|
||||
@@ -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);
|
||||
@@ -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");
|
||||
}
|
||||
@@ -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
|
||||
@@ -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.
|
||||
@@ -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! 🚀
|
||||
@@ -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
|
||||
@@ -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
|
||||
|
After Width: | Height: | Size: 7.6 MiB |
|
After Width: | Height: | Size: 7.6 MiB |
|
After Width: | Height: | Size: 7.6 MiB |
|
After Width: | Height: | Size: 7.5 MiB |
@@ -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
|
||||
|
After Width: | Height: | Size: 9.1 MiB |
|
After Width: | Height: | Size: 7.4 MiB |
|
After Width: | Height: | Size: 64 KiB |
|
After Width: | Height: | Size: 13 KiB |
|
After Width: | Height: | Size: 7.7 MiB |
|
After Width: | Height: | Size: 7.9 MiB |
|
After Width: | Height: | Size: 7.5 MiB |
|
After Width: | Height: | Size: 7.6 MiB |
|
After Width: | Height: | Size: 7.4 MiB |
|
After Width: | Height: | Size: 7.5 MiB |
@@ -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
|
||||
```
|
||||
@@ -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
|
||||
@@ -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.
|
||||
@@ -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`
|
||||
@@ -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
|
||||
@@ -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
|
||||
@@ -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(())
|
||||
}
|
||||
@@ -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,
|
||||
},
|
||||
],
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,4 @@
|
||||
pub mod data;
|
||||
pub mod database_seeder;
|
||||
|
||||
pub use database_seeder::DatabaseSeeder;
|
||||
@@ -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
|
||||
}
|
||||
@@ -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";
|
||||
};
|
||||
});
|
||||
};
|
||||
}
|
||||
@@ -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 "----------------------------------------"
|
||||
@@ -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 "$@"
|
||||
@@ -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>;
|
||||
@@ -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
|
||||
}
|
||||
}
|
||||
@@ -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()
|
||||
}
|
||||
}
|
||||
@@ -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;
|
||||
@@ -0,0 +1,6 @@
|
||||
pub mod error;
|
||||
pub mod events;
|
||||
pub mod models;
|
||||
pub mod repositories;
|
||||
pub mod services;
|
||||
pub mod stores;
|
||||
@@ -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()
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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)
|
||||
}
|
||||
@@ -0,0 +1,8 @@
|
||||
use serde::{Deserialize, Serialize};
|
||||
|
||||
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq, Hash, Default)]
|
||||
pub enum ColorMode {
|
||||
#[default]
|
||||
Dark,
|
||||
Light,
|
||||
}
|
||||
@@ -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())),
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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()
|
||||
}
|
||||
@@ -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()
|
||||
}
|
||||
}
|
||||
@@ -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()
|
||||
}
|
||||
}
|
||||
@@ -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();
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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(['/', ':', '.'], "_")
|
||||
}
|
||||
}
|
||||
@@ -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)
|
||||
}
|
||||
}
|
||||
@@ -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 {}
|
||||
@@ -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"
|
||||
}
|
||||
}
|
||||
@@ -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"
|
||||
}
|
||||
}
|
||||
@@ -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"
|
||||
}
|
||||
}
|
||||
@@ -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"
|
||||
}
|
||||
}
|
||||
@@ -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"
|
||||
}
|
||||
}
|
||||
@@ -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"
|
||||
}
|
||||
}
|
||||
@@ -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"
|
||||
}
|
||||
}
|
||||
@@ -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"
|
||||
}
|
||||
}
|
||||
@@ -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"
|
||||
}
|
||||
}
|
||||
@@ -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"
|
||||
}
|
||||
}
|
||||