Initial commit: Piglet - Animated figlet wrapper
Add complete Rust implementation with: - 20+ motion effects (fade, slide, scale, typewriter, wave, bounce, etc.) - 18+ easing functions (linear, quad, cubic, elastic, back, bounce) - Color support (CSS4 colors, hex codes, gradients) - Figlet integration with custom fonts and options - Cross-platform support (Linux, macOS, Windows) - Comprehensive CI/CD workflows - Full test suite with integration tests - Documentation (README.md, CLAUDE.md) 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude <noreply@anthropic.com>
This commit is contained in:
21
.github/dependabot.yml
vendored
Normal file
21
.github/dependabot.yml
vendored
Normal file
@@ -0,0 +1,21 @@
|
|||||||
|
version: 2
|
||||||
|
updates:
|
||||||
|
# Maintain dependencies for Cargo
|
||||||
|
- package-ecosystem: "cargo"
|
||||||
|
directory: "/"
|
||||||
|
schedule:
|
||||||
|
interval: "weekly"
|
||||||
|
open-pull-requests-limit: 10
|
||||||
|
labels:
|
||||||
|
- "dependencies"
|
||||||
|
- "rust"
|
||||||
|
|
||||||
|
# Maintain dependencies for GitHub Actions
|
||||||
|
- package-ecosystem: "github-actions"
|
||||||
|
directory: "/"
|
||||||
|
schedule:
|
||||||
|
interval: "weekly"
|
||||||
|
open-pull-requests-limit: 5
|
||||||
|
labels:
|
||||||
|
- "dependencies"
|
||||||
|
- "github-actions"
|
||||||
42
.github/workflows/benchmark.yml
vendored
Normal file
42
.github/workflows/benchmark.yml
vendored
Normal file
@@ -0,0 +1,42 @@
|
|||||||
|
name: Benchmark
|
||||||
|
|
||||||
|
on:
|
||||||
|
push:
|
||||||
|
branches: [ main ]
|
||||||
|
pull_request:
|
||||||
|
branches: [ main ]
|
||||||
|
|
||||||
|
jobs:
|
||||||
|
benchmark:
|
||||||
|
name: Run Benchmarks
|
||||||
|
runs-on: ubuntu-latest
|
||||||
|
steps:
|
||||||
|
- name: Checkout code
|
||||||
|
uses: actions/checkout@v4
|
||||||
|
|
||||||
|
- name: Install Rust toolchain
|
||||||
|
uses: dtolnay/rust-toolchain@stable
|
||||||
|
|
||||||
|
- name: Install figlet
|
||||||
|
run: sudo apt-get update && sudo apt-get install -y figlet
|
||||||
|
|
||||||
|
- name: Cache cargo registry
|
||||||
|
uses: actions/cache@v3
|
||||||
|
with:
|
||||||
|
path: ~/.cargo/registry
|
||||||
|
key: ${{ runner.os }}-cargo-registry-${{ hashFiles('**/Cargo.lock') }}
|
||||||
|
|
||||||
|
- name: Cache cargo index
|
||||||
|
uses: actions/cache@v3
|
||||||
|
with:
|
||||||
|
path: ~/.cargo/git
|
||||||
|
key: ${{ runner.os }}-cargo-git-${{ hashFiles('**/Cargo.lock') }}
|
||||||
|
|
||||||
|
- name: Run benchmarks
|
||||||
|
run: cargo bench --no-fail-fast
|
||||||
|
|
||||||
|
- name: Upload benchmark results
|
||||||
|
uses: actions/upload-artifact@v3
|
||||||
|
with:
|
||||||
|
name: benchmark-results
|
||||||
|
path: target/criterion/
|
||||||
176
.github/workflows/ci.yml
vendored
Normal file
176
.github/workflows/ci.yml
vendored
Normal file
@@ -0,0 +1,176 @@
|
|||||||
|
name: CI
|
||||||
|
|
||||||
|
on:
|
||||||
|
push:
|
||||||
|
branches: [ main, develop ]
|
||||||
|
pull_request:
|
||||||
|
branches: [ main, develop ]
|
||||||
|
|
||||||
|
env:
|
||||||
|
CARGO_TERM_COLOR: always
|
||||||
|
RUST_BACKTRACE: 1
|
||||||
|
|
||||||
|
jobs:
|
||||||
|
lint:
|
||||||
|
name: Lint
|
||||||
|
runs-on: ubuntu-latest
|
||||||
|
steps:
|
||||||
|
- name: Checkout code
|
||||||
|
uses: actions/checkout@v4
|
||||||
|
|
||||||
|
- name: Install Rust toolchain
|
||||||
|
uses: dtolnay/rust-toolchain@stable
|
||||||
|
with:
|
||||||
|
components: rustfmt, clippy
|
||||||
|
|
||||||
|
- name: Cache cargo registry
|
||||||
|
uses: actions/cache@v3
|
||||||
|
with:
|
||||||
|
path: ~/.cargo/registry
|
||||||
|
key: ${{ runner.os }}-cargo-registry-${{ hashFiles('**/Cargo.lock') }}
|
||||||
|
|
||||||
|
- name: Cache cargo index
|
||||||
|
uses: actions/cache@v3
|
||||||
|
with:
|
||||||
|
path: ~/.cargo/git
|
||||||
|
key: ${{ runner.os }}-cargo-git-${{ hashFiles('**/Cargo.lock') }}
|
||||||
|
|
||||||
|
- name: Cache cargo build
|
||||||
|
uses: actions/cache@v3
|
||||||
|
with:
|
||||||
|
path: target
|
||||||
|
key: ${{ runner.os }}-cargo-build-target-${{ hashFiles('**/Cargo.lock') }}
|
||||||
|
|
||||||
|
- name: Check formatting
|
||||||
|
run: cargo fmt --all -- --check
|
||||||
|
|
||||||
|
- name: Run clippy
|
||||||
|
run: cargo clippy --all-targets --all-features -- -D warnings
|
||||||
|
|
||||||
|
- name: Check documentation
|
||||||
|
run: cargo doc --no-deps --all-features
|
||||||
|
env:
|
||||||
|
RUSTDOCFLAGS: -D warnings
|
||||||
|
|
||||||
|
test:
|
||||||
|
name: Test
|
||||||
|
runs-on: ${{ matrix.os }}
|
||||||
|
strategy:
|
||||||
|
matrix:
|
||||||
|
os: [ubuntu-latest, macos-latest, windows-latest]
|
||||||
|
rust: [stable, beta]
|
||||||
|
steps:
|
||||||
|
- name: Checkout code
|
||||||
|
uses: actions/checkout@v4
|
||||||
|
|
||||||
|
- name: Install Rust toolchain
|
||||||
|
uses: dtolnay/rust-toolchain@master
|
||||||
|
with:
|
||||||
|
toolchain: ${{ matrix.rust }}
|
||||||
|
|
||||||
|
- name: Install figlet (Ubuntu)
|
||||||
|
if: matrix.os == 'ubuntu-latest'
|
||||||
|
run: sudo apt-get update && sudo apt-get install -y figlet
|
||||||
|
|
||||||
|
- name: Install figlet (macOS)
|
||||||
|
if: matrix.os == 'macos-latest'
|
||||||
|
run: brew install figlet
|
||||||
|
|
||||||
|
- name: Install figlet (Windows)
|
||||||
|
if: matrix.os == 'windows-latest'
|
||||||
|
run: |
|
||||||
|
choco install figlet -y
|
||||||
|
echo "C:\ProgramData\chocolatey\lib\figlet\tools" | Out-File -FilePath $env:GITHUB_PATH -Encoding utf8 -Append
|
||||||
|
|
||||||
|
- name: Cache cargo registry
|
||||||
|
uses: actions/cache@v3
|
||||||
|
with:
|
||||||
|
path: ~/.cargo/registry
|
||||||
|
key: ${{ runner.os }}-cargo-registry-${{ hashFiles('**/Cargo.lock') }}
|
||||||
|
|
||||||
|
- name: Cache cargo index
|
||||||
|
uses: actions/cache@v3
|
||||||
|
with:
|
||||||
|
path: ~/.cargo/git
|
||||||
|
key: ${{ runner.os }}-cargo-git-${{ hashFiles('**/Cargo.lock') }}
|
||||||
|
|
||||||
|
- name: Cache cargo build
|
||||||
|
uses: actions/cache@v3
|
||||||
|
with:
|
||||||
|
path: target
|
||||||
|
key: ${{ runner.os }}-cargo-build-target-${{ hashFiles('**/Cargo.lock') }}
|
||||||
|
|
||||||
|
- name: Run tests
|
||||||
|
run: cargo test --verbose --all-features
|
||||||
|
|
||||||
|
- name: Run tests (no default features)
|
||||||
|
run: cargo test --verbose --no-default-features
|
||||||
|
|
||||||
|
build:
|
||||||
|
name: Build
|
||||||
|
runs-on: ${{ matrix.os }}
|
||||||
|
strategy:
|
||||||
|
matrix:
|
||||||
|
include:
|
||||||
|
- os: ubuntu-latest
|
||||||
|
target: x86_64-unknown-linux-gnu
|
||||||
|
- os: ubuntu-latest
|
||||||
|
target: x86_64-unknown-linux-musl
|
||||||
|
- os: macos-latest
|
||||||
|
target: x86_64-apple-darwin
|
||||||
|
- os: macos-latest
|
||||||
|
target: aarch64-apple-darwin
|
||||||
|
- os: windows-latest
|
||||||
|
target: x86_64-pc-windows-msvc
|
||||||
|
steps:
|
||||||
|
- name: Checkout code
|
||||||
|
uses: actions/checkout@v4
|
||||||
|
|
||||||
|
- name: Install Rust toolchain
|
||||||
|
uses: dtolnay/rust-toolchain@stable
|
||||||
|
with:
|
||||||
|
targets: ${{ matrix.target }}
|
||||||
|
|
||||||
|
- name: Install musl tools (Linux musl)
|
||||||
|
if: matrix.target == 'x86_64-unknown-linux-musl'
|
||||||
|
run: sudo apt-get update && sudo apt-get install -y musl-tools
|
||||||
|
|
||||||
|
- name: Cache cargo registry
|
||||||
|
uses: actions/cache@v3
|
||||||
|
with:
|
||||||
|
path: ~/.cargo/registry
|
||||||
|
key: ${{ runner.os }}-cargo-registry-${{ hashFiles('**/Cargo.lock') }}
|
||||||
|
|
||||||
|
- name: Cache cargo index
|
||||||
|
uses: actions/cache@v3
|
||||||
|
with:
|
||||||
|
path: ~/.cargo/git
|
||||||
|
key: ${{ runner.os }}-cargo-git-${{ hashFiles('**/Cargo.lock') }}
|
||||||
|
|
||||||
|
- name: Cache cargo build
|
||||||
|
uses: actions/cache@v3
|
||||||
|
with:
|
||||||
|
path: target
|
||||||
|
key: ${{ runner.os }}-${{ matrix.target }}-cargo-build-target-${{ hashFiles('**/Cargo.lock') }}
|
||||||
|
|
||||||
|
- name: Build
|
||||||
|
run: cargo build --release --target ${{ matrix.target }} --verbose
|
||||||
|
|
||||||
|
- name: Upload artifact
|
||||||
|
uses: actions/upload-artifact@v3
|
||||||
|
with:
|
||||||
|
name: piglet-${{ matrix.target }}
|
||||||
|
path: |
|
||||||
|
target/${{ matrix.target }}/release/piglet${{ matrix.os == 'windows-latest' && '.exe' || '' }}
|
||||||
|
|
||||||
|
security-audit:
|
||||||
|
name: Security Audit
|
||||||
|
runs-on: ubuntu-latest
|
||||||
|
steps:
|
||||||
|
- name: Checkout code
|
||||||
|
uses: actions/checkout@v4
|
||||||
|
|
||||||
|
- name: Run cargo-audit
|
||||||
|
uses: rustsec/audit-check@v1
|
||||||
|
with:
|
||||||
|
token: ${{ secrets.GITHUB_TOKEN }}
|
||||||
36
.github/workflows/coverage.yml
vendored
Normal file
36
.github/workflows/coverage.yml
vendored
Normal file
@@ -0,0 +1,36 @@
|
|||||||
|
name: Code Coverage
|
||||||
|
|
||||||
|
on:
|
||||||
|
push:
|
||||||
|
branches: [ main ]
|
||||||
|
pull_request:
|
||||||
|
branches: [ main ]
|
||||||
|
|
||||||
|
jobs:
|
||||||
|
coverage:
|
||||||
|
name: Code Coverage
|
||||||
|
runs-on: ubuntu-latest
|
||||||
|
steps:
|
||||||
|
- name: Checkout code
|
||||||
|
uses: actions/checkout@v4
|
||||||
|
|
||||||
|
- name: Install Rust toolchain
|
||||||
|
uses: dtolnay/rust-toolchain@stable
|
||||||
|
with:
|
||||||
|
components: llvm-tools-preview
|
||||||
|
|
||||||
|
- name: Install figlet
|
||||||
|
run: sudo apt-get update && sudo apt-get install -y figlet
|
||||||
|
|
||||||
|
- name: Install cargo-llvm-cov
|
||||||
|
uses: taiki-e/install-action@cargo-llvm-cov
|
||||||
|
|
||||||
|
- name: Generate coverage
|
||||||
|
run: cargo llvm-cov --all-features --workspace --lcov --output-path lcov.info
|
||||||
|
|
||||||
|
- name: Upload to codecov.io
|
||||||
|
uses: codecov/codecov-action@v3
|
||||||
|
with:
|
||||||
|
files: lcov.info
|
||||||
|
fail_ci_if_error: true
|
||||||
|
token: ${{ secrets.CODECOV_TOKEN }}
|
||||||
41
.github/workflows/release.yml
vendored
Normal file
41
.github/workflows/release.yml
vendored
Normal file
@@ -0,0 +1,41 @@
|
|||||||
|
name: Release
|
||||||
|
|
||||||
|
on:
|
||||||
|
push:
|
||||||
|
tags:
|
||||||
|
- 'v*.*.*'
|
||||||
|
|
||||||
|
env:
|
||||||
|
CARGO_TERM_COLOR: always
|
||||||
|
|
||||||
|
jobs:
|
||||||
|
create-release:
|
||||||
|
name: Create Release
|
||||||
|
runs-on: ubuntu-latest
|
||||||
|
outputs:
|
||||||
|
upload_url: ${{ steps.create_release.outputs.upload_url }}
|
||||||
|
version: ${{ steps.get_version.outputs.version }}
|
||||||
|
steps:
|
||||||
|
- name: Checkout code
|
||||||
|
uses: actions/checkout@v4
|
||||||
|
|
||||||
|
- name: Get version from tag
|
||||||
|
id: get_version
|
||||||
|
run: echo "version=${GITHUB_REF#refs/tags/v}" >> $GITHUB_OUTPUT
|
||||||
|
|
||||||
|
- name: Create Release
|
||||||
|
id: create_release
|
||||||
|
uses: actions/create-release@v1
|
||||||
|
env:
|
||||||
|
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||||||
|
with:
|
||||||
|
tag_name: ${{ github.ref }}
|
||||||
|
release_name: Release ${{ github.ref }}
|
||||||
|
body: |
|
||||||
|
## Changes in this Release
|
||||||
|
|
||||||
|
See [CHANGELOG.md](https://github.com/${{ github.repository }}/blob/main/CHANGELOG.md) for details.
|
||||||
|
|
||||||
|
## Installation
|
||||||
|
|
||||||
|
### Linux (x86_64)
|
||||||
34
.github/workflows/security.yml
vendored
Normal file
34
.github/workflows/security.yml
vendored
Normal file
@@ -0,0 +1,34 @@
|
|||||||
|
name: Security Audit
|
||||||
|
|
||||||
|
on:
|
||||||
|
schedule:
|
||||||
|
- cron: '0 0 * * *' # Run daily at midnight UTC
|
||||||
|
push:
|
||||||
|
paths:
|
||||||
|
- '**/Cargo.toml'
|
||||||
|
- '**/Cargo.lock'
|
||||||
|
workflow_dispatch:
|
||||||
|
|
||||||
|
jobs:
|
||||||
|
audit:
|
||||||
|
name: Security Audit
|
||||||
|
runs-on: ubuntu-latest
|
||||||
|
steps:
|
||||||
|
- name: Checkout code
|
||||||
|
uses: actions/checkout@v4
|
||||||
|
|
||||||
|
- name: Run cargo-audit
|
||||||
|
uses: rustsec/audit-check@v1
|
||||||
|
with:
|
||||||
|
token: ${{ secrets.GITHUB_TOKEN }}
|
||||||
|
|
||||||
|
dependency-review:
|
||||||
|
name: Dependency Review
|
||||||
|
runs-on: ubuntu-latest
|
||||||
|
if: github.event_name == 'pull_request'
|
||||||
|
steps:
|
||||||
|
- name: Checkout code
|
||||||
|
uses: actions/checkout@v4
|
||||||
|
|
||||||
|
- name: Dependency Review
|
||||||
|
uses: actions/dependency-review-action@v3
|
||||||
183
CLAUDE.md
Normal file
183
CLAUDE.md
Normal file
@@ -0,0 +1,183 @@
|
|||||||
|
# CLAUDE.md
|
||||||
|
|
||||||
|
This file provides guidance to Claude Code (claude.ai/code) when working with code in this repository.
|
||||||
|
|
||||||
|
## Overview
|
||||||
|
|
||||||
|
Piglet is an animated and colorful figlet wrapper written in Rust that renders ASCII art with motion effects, color gradients, and easing functions. It wraps the `figlet` command-line tool to generate ASCII art and applies real-time terminal animations using crossterm.
|
||||||
|
|
||||||
|
## Prerequisites
|
||||||
|
|
||||||
|
- **figlet** must be installed on the system:
|
||||||
|
- Ubuntu/Debian: `sudo apt-get install figlet`
|
||||||
|
- macOS: `brew install figlet`
|
||||||
|
- Windows: `choco install figlet -y`
|
||||||
|
- Rust toolchain (managed via rustup)
|
||||||
|
|
||||||
|
## Common Commands
|
||||||
|
|
||||||
|
### Development
|
||||||
|
```bash
|
||||||
|
# Build the project
|
||||||
|
cargo build
|
||||||
|
|
||||||
|
# Build for release
|
||||||
|
cargo build --release
|
||||||
|
|
||||||
|
# Run the binary
|
||||||
|
cargo run -- "Hello" -p "#FF5733,#33FF57"
|
||||||
|
|
||||||
|
# Run with specific effect
|
||||||
|
cargo run -- "World" -g "linear-gradient(90deg, red, blue)" -e fade-in
|
||||||
|
```
|
||||||
|
|
||||||
|
### Testing
|
||||||
|
```bash
|
||||||
|
# Run all tests (requires figlet installed)
|
||||||
|
cargo test --verbose --all-features
|
||||||
|
|
||||||
|
# Run tests without default features
|
||||||
|
cargo test --verbose --no-default-features
|
||||||
|
|
||||||
|
# Run a single test
|
||||||
|
cargo test test_figlet_wrapper
|
||||||
|
|
||||||
|
# Run integration tests only
|
||||||
|
cargo test --test integration_tests
|
||||||
|
```
|
||||||
|
|
||||||
|
### Linting and Code Quality
|
||||||
|
```bash
|
||||||
|
# Check formatting
|
||||||
|
cargo fmt --all -- --check
|
||||||
|
|
||||||
|
# Apply formatting
|
||||||
|
cargo fmt --all
|
||||||
|
|
||||||
|
# Run clippy with all warnings as errors
|
||||||
|
cargo clippy --all-targets --all-features -- -D warnings
|
||||||
|
|
||||||
|
# Check documentation
|
||||||
|
cargo doc --no-deps --all-features
|
||||||
|
```
|
||||||
|
|
||||||
|
### Cross-compilation Targets
|
||||||
|
The project supports multiple targets (see CI configuration):
|
||||||
|
- `x86_64-unknown-linux-gnu`
|
||||||
|
- `x86_64-unknown-linux-musl` (requires musl-tools)
|
||||||
|
- `x86_64-apple-darwin`
|
||||||
|
- `aarch64-apple-darwin`
|
||||||
|
- `x86_64-pc-windows-msvc`
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# Build for specific target
|
||||||
|
cargo build --release --target x86_64-unknown-linux-musl
|
||||||
|
```
|
||||||
|
|
||||||
|
## Architecture
|
||||||
|
|
||||||
|
### Module Structure
|
||||||
|
|
||||||
|
**Core Pipeline Flow:**
|
||||||
|
1. **CLI Parsing** (`cli.rs`) → Parses command-line arguments using clap
|
||||||
|
2. **Figlet Wrapper** (`figlet.rs`) → Executes figlet command to generate base ASCII art
|
||||||
|
3. **Parsers** (`parser/`) → Parses duration, colors, and gradients from CLI strings
|
||||||
|
4. **Color Engine** (`color/`) → Manages palettes and gradients, interpolates colors
|
||||||
|
5. **Animation Engine** (`animation/`) → Applies motion effects and easing functions
|
||||||
|
6. **Terminal Manager** (`utils/terminal.rs`) → Handles terminal setup, rendering, and cleanup
|
||||||
|
|
||||||
|
### Key Components
|
||||||
|
|
||||||
|
**figlet.rs - FigletWrapper**
|
||||||
|
- Wraps external `figlet` binary execution
|
||||||
|
- Validates figlet installation via `which` crate
|
||||||
|
- Supports custom fonts and arguments passed through to figlet
|
||||||
|
|
||||||
|
**parser/ - Input Parsers**
|
||||||
|
- `duration.rs`: Parses time strings (e.g., "3000ms", "0.3s", "5m", "0.5h") to milliseconds using regex
|
||||||
|
- `color.rs`: Parses hex colors (#FF5733) and CSS4 color names (red, blue) using csscolorparser
|
||||||
|
- `gradient.rs`: Parses CSS gradient syntax (e.g., "linear-gradient(90deg, red, blue)")
|
||||||
|
|
||||||
|
**color/ - Color System**
|
||||||
|
- `palette.rs`: Manages discrete color palettes (comma-separated colors)
|
||||||
|
- `gradient.rs`: Generates smooth color transitions using gradient definitions
|
||||||
|
- `apply.rs`: Applies colors to ASCII art characters
|
||||||
|
- Uses `palette` crate for color space conversions and interpolation
|
||||||
|
|
||||||
|
**animation/ - Animation System**
|
||||||
|
- `easing.rs`: Implements 18+ easing functions (linear, ease-in/out, quad, cubic, back, elastic, bounce) using scirs2-interpolate
|
||||||
|
- `effects/`: Motion effects including fade-in/out, slide, scale, typewriter, wave, rainbow
|
||||||
|
- `renderer.rs`: Frame-by-frame rendering loop with tokio async timing
|
||||||
|
- `timeline.rs`: Manages animation progress and frame timing
|
||||||
|
|
||||||
|
**utils/ - Utilities**
|
||||||
|
- `terminal.rs`: Terminal manipulation using crossterm (alternate screen, cursor hiding, clearing)
|
||||||
|
- `ascii.rs`: ASCII art data structure and character-level manipulation
|
||||||
|
|
||||||
|
### External Dependencies
|
||||||
|
|
||||||
|
**Critical dependencies:**
|
||||||
|
- `clap` (4.4+): CLI argument parsing with derive macros
|
||||||
|
- `crossterm` (0.27): Terminal manipulation and ANSI escape sequences
|
||||||
|
- `tokio` (1.35): Async runtime for frame timing
|
||||||
|
- `scirs2-interpolate` (0.1): Easing function implementations
|
||||||
|
- `palette` (0.7): Color space conversions and interpolation
|
||||||
|
- `csscolorparser` (0.6): Parsing hex and CSS color names
|
||||||
|
- `which` (5.0): Locating figlet binary in PATH
|
||||||
|
- `nom` (7.1): Parser combinators for gradient syntax
|
||||||
|
- `regex` (1.10): Duration string parsing
|
||||||
|
|
||||||
|
### Animation Flow
|
||||||
|
|
||||||
|
1. Parse CLI args → validate duration, colors, effect, easing
|
||||||
|
2. Execute figlet → capture ASCII art output as String
|
||||||
|
3. Parse colors → build ColorEngine with palette or gradient
|
||||||
|
4. Create AnimationEngine → configure effect + easing + colors
|
||||||
|
5. Setup terminal → enter alternate screen, hide cursor
|
||||||
|
6. Render loop → for each frame:
|
||||||
|
- Calculate progress (0.0 to 1.0)
|
||||||
|
- Apply easing function to progress
|
||||||
|
- Apply effect transformation to ASCII art
|
||||||
|
- Apply color at current progress
|
||||||
|
- Render to terminal
|
||||||
|
- Sleep until next frame (based on fps)
|
||||||
|
7. Cleanup → restore terminal state
|
||||||
|
|
||||||
|
### Testing Strategy
|
||||||
|
|
||||||
|
- Unit tests embedded in modules (e.g., `parser/duration.rs`, `figlet.rs`)
|
||||||
|
- Integration tests in `tests/integration_tests.rs` testing full pipeline
|
||||||
|
- Tests validate: parsers, color interpolation, easing functions, effects, gradient rendering
|
||||||
|
- CI runs tests on Ubuntu, macOS, Windows with stable and beta Rust
|
||||||
|
|
||||||
|
## CLI Usage Examples
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# Simple color palette
|
||||||
|
piglet "Hello" -p "#FF5733,#33FF57"
|
||||||
|
|
||||||
|
# Gradient with motion effect
|
||||||
|
piglet "World" -g "linear-gradient(90deg, red, blue)" -e fade-in
|
||||||
|
|
||||||
|
# Typewriter with custom duration and easing
|
||||||
|
piglet "Cool!" -e typewriter -d 2s -i ease-out
|
||||||
|
|
||||||
|
# Loop animation infinitely
|
||||||
|
piglet "Loop" -e wave -l
|
||||||
|
|
||||||
|
# Custom figlet font and arguments
|
||||||
|
piglet "Custom" -f slant -- -w 200 -c
|
||||||
|
|
||||||
|
# List available effects/easing/colors
|
||||||
|
piglet --list-effects
|
||||||
|
piglet --list-easing
|
||||||
|
piglet --list-colors
|
||||||
|
```
|
||||||
|
|
||||||
|
## Available Effects
|
||||||
|
|
||||||
|
fade-in, fade-out, fade-in-out, slide-in-top, slide-in-bottom, slide-in-left, slide-in-right, scale-up, scale-down, pulse, bounce-in, bounce-out, typewriter, typewriter-reverse, wave, jello, color-cycle, rainbow, gradient-flow, rotate-in, rotate-out
|
||||||
|
|
||||||
|
## Available Easing Functions
|
||||||
|
|
||||||
|
linear, ease-in, ease-out, ease-in-out, ease-in-quad, ease-out-quad, ease-in-out-quad, ease-in-cubic, ease-out-cubic, ease-in-out-cubic, ease-in-back, ease-out-back, ease-in-out-back, ease-in-elastic, ease-out-elastic, ease-in-out-elastic, ease-in-bounce, ease-out-bounce, ease-in-out-bounce
|
||||||
48
Cargo.toml
Normal file
48
Cargo.toml
Normal file
@@ -0,0 +1,48 @@
|
|||||||
|
[package]
|
||||||
|
name = "piglet"
|
||||||
|
version = "0.1.0"
|
||||||
|
edition = "2021"
|
||||||
|
authors = ["Your Name"]
|
||||||
|
description = "Animated and colorful figlet wrapper with motion effects"
|
||||||
|
license = "MIT"
|
||||||
|
repository = "https://github.com/valknarthing/piglet"
|
||||||
|
|
||||||
|
[dependencies]
|
||||||
|
# CLI
|
||||||
|
clap = { version = "4.4", features = ["derive", "cargo"] }
|
||||||
|
|
||||||
|
# Color handling
|
||||||
|
csscolorparser = "0.6"
|
||||||
|
palette = "0.7"
|
||||||
|
|
||||||
|
# Animation & Interpolation
|
||||||
|
scirs2-interpolate = "0.1"
|
||||||
|
|
||||||
|
# Terminal manipulation
|
||||||
|
crossterm = "0.27"
|
||||||
|
|
||||||
|
# Async runtime (for timing)
|
||||||
|
tokio = { version = "1.35", features = ["time", "rt"] }
|
||||||
|
|
||||||
|
# Process execution
|
||||||
|
which = "5.0"
|
||||||
|
|
||||||
|
# Parsing
|
||||||
|
nom = "7.1"
|
||||||
|
regex = "1.10"
|
||||||
|
|
||||||
|
# Error handling
|
||||||
|
anyhow = "1.0"
|
||||||
|
thiserror = "1.0"
|
||||||
|
|
||||||
|
# Utilities
|
||||||
|
itertools = "0.12"
|
||||||
|
lazy_static = "1.4"
|
||||||
|
rand = "0.8"
|
||||||
|
|
||||||
|
[dev-dependencies]
|
||||||
|
pretty_assertions = "1.4"
|
||||||
|
|
||||||
|
[[bin]]
|
||||||
|
name = "piglet"
|
||||||
|
path = "src/main.rs"
|
||||||
290
README.md
Normal file
290
README.md
Normal file
@@ -0,0 +1,290 @@
|
|||||||
|
# 🐷 Piglet
|
||||||
|
|
||||||
|
<div align="center">
|
||||||
|
|
||||||
|
**Animated and colorful figlet wrapper with motion effects**
|
||||||
|
|
||||||
|
[](https://github.com/valknarthing/piglet/actions/workflows/ci.yml)
|
||||||
|
[](https://github.com/valknarthing/piglet/actions/workflows/security.yml)
|
||||||
|
[](https://github.com/valknarthing/piglet/actions/workflows/coverage.yml)
|
||||||
|
[](https://opensource.org/licenses/MIT)
|
||||||
|
[](https://www.rust-lang.org)
|
||||||
|
[](https://crates.io/crates/piglet)
|
||||||
|
|
||||||
|
</div>
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## ✨ Features
|
||||||
|
|
||||||
|
- 🎨 **Rich Color Support**: CSS4 colors, hex codes, and smooth gradients
|
||||||
|
- 🎬 **20+ Motion Effects**: Fade, slide, scale, typewriter, wave, bounce, and more
|
||||||
|
- ⚡ **18+ Easing Functions**: Linear, quad, cubic, elastic, back, bounce variations
|
||||||
|
- 🖼️ **Figlet Integration**: Full support for figlet fonts and options
|
||||||
|
- 🔄 **Looping Animations**: Infinite or single-run modes
|
||||||
|
- 🎯 **High Performance**: Async rendering with configurable FPS
|
||||||
|
- 🌈 **Gradient Engine**: Parse and apply CSS-style gradients
|
||||||
|
- 📦 **Cross-Platform**: Linux, macOS, and Windows support
|
||||||
|
|
||||||
|
## 📦 Installation
|
||||||
|
|
||||||
|
### From Source
|
||||||
|
|
||||||
|
```bash
|
||||||
|
git clone https://github.com/valknarthing/piglet.git
|
||||||
|
cd piglet
|
||||||
|
cargo build --release
|
||||||
|
```
|
||||||
|
|
||||||
|
The binary will be available at `target/release/piglet`.
|
||||||
|
|
||||||
|
### Prerequisites
|
||||||
|
|
||||||
|
Piglet requires `figlet` to be installed on your system:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# Ubuntu/Debian
|
||||||
|
sudo apt-get install figlet
|
||||||
|
|
||||||
|
# macOS
|
||||||
|
brew install figlet
|
||||||
|
|
||||||
|
# Windows
|
||||||
|
choco install figlet -y
|
||||||
|
```
|
||||||
|
|
||||||
|
## 🚀 Quick Start
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# Simple color palette animation
|
||||||
|
piglet "Hello World" -p "#FF5733,#33FF57,#3357FF"
|
||||||
|
|
||||||
|
# Gradient with fade-in effect
|
||||||
|
piglet "Rainbow" -g "linear-gradient(90deg, red, orange, yellow, green, blue, purple)" -e fade-in
|
||||||
|
|
||||||
|
# Typewriter effect with custom timing
|
||||||
|
piglet "Type It Out" -e typewriter -d 3s -i ease-out
|
||||||
|
|
||||||
|
# Bouncing text with loop
|
||||||
|
piglet "Bounce!" -e bounce-in -l
|
||||||
|
```
|
||||||
|
|
||||||
|
## 📖 Usage
|
||||||
|
|
||||||
|
```
|
||||||
|
piglet [TEXT] [OPTIONS]
|
||||||
|
|
||||||
|
Arguments:
|
||||||
|
<TEXT> Text to render with figlet
|
||||||
|
|
||||||
|
Options:
|
||||||
|
-d, --duration <DURATION> Duration of animation [default: 3s]
|
||||||
|
Formats: 3000ms, 0.3s, 5m, 0.5h
|
||||||
|
|
||||||
|
-p, --color-palette <COLORS> Color palette (comma-separated)
|
||||||
|
Example: "#FF5733,#33FF57,blue,red"
|
||||||
|
|
||||||
|
-g, --color-gradient <GRADIENT> CSS gradient definition
|
||||||
|
Example: "linear-gradient(90deg, red, blue)"
|
||||||
|
|
||||||
|
-e, --motion-effect <EFFECT> Motion effect to apply [default: fade-in]
|
||||||
|
|
||||||
|
-i, --motion-ease <EASING> Easing function [default: ease-in-out]
|
||||||
|
|
||||||
|
-f, --font <FONT> Figlet font to use
|
||||||
|
|
||||||
|
-l, --loop Loop animation infinitely
|
||||||
|
|
||||||
|
--fps <FPS> Frame rate [default: 30]
|
||||||
|
|
||||||
|
--list-effects List all available effects
|
||||||
|
--list-easing List all available easing functions
|
||||||
|
--list-colors List all CSS4 color names
|
||||||
|
|
||||||
|
-- <FIGLET_ARGS>... Additional figlet options
|
||||||
|
Example: -- -w 200 -c
|
||||||
|
|
||||||
|
-h, --help Print help
|
||||||
|
-V, --version Print version
|
||||||
|
```
|
||||||
|
|
||||||
|
## 🎬 Motion Effects
|
||||||
|
|
||||||
|
| Effect | Description | Effect | Description |
|
||||||
|
|--------|-------------|--------|-------------|
|
||||||
|
| `fade-in` | Fade from transparent | `fade-out` | Fade to transparent |
|
||||||
|
| `fade-in-out` | Fade in then out | `slide-in-top` | Slide from top |
|
||||||
|
| `slide-in-bottom` | Slide from bottom | `slide-in-left` | Slide from left |
|
||||||
|
| `slide-in-right` | Slide from right | `scale-up` | Scale from small |
|
||||||
|
| `scale-down` | Scale from large | `pulse` | Pulsing effect |
|
||||||
|
| `bounce-in` | Bounce into view | `bounce-out` | Bounce out of view |
|
||||||
|
| `typewriter` | Type character by character | `typewriter-reverse` | Untype backwards |
|
||||||
|
| `wave` | Wave motion | `jello` | Jello wobble |
|
||||||
|
| `color-cycle` | Cycle through colors | `rainbow` | Rainbow effect |
|
||||||
|
| `gradient-flow` | Flowing gradient | `rotate-in` | Rotate into view |
|
||||||
|
| `rotate-out` | Rotate out of view | | |
|
||||||
|
|
||||||
|
## ⚡ Easing Functions
|
||||||
|
|
||||||
|
| Category | Functions |
|
||||||
|
|----------|-----------|
|
||||||
|
| **Linear** | `linear` |
|
||||||
|
| **Basic** | `ease-in`, `ease-out`, `ease-in-out` |
|
||||||
|
| **Quadratic** | `ease-in-quad`, `ease-out-quad`, `ease-in-out-quad` |
|
||||||
|
| **Cubic** | `ease-in-cubic`, `ease-out-cubic`, `ease-in-out-cubic` |
|
||||||
|
| **Back** | `ease-in-back`, `ease-out-back`, `ease-in-out-back` |
|
||||||
|
| **Elastic** | `ease-in-elastic`, `ease-out-elastic`, `ease-in-out-elastic` |
|
||||||
|
| **Bounce** | `ease-in-bounce`, `ease-out-bounce`, `ease-in-out-bounce` |
|
||||||
|
|
||||||
|
## 🎨 Color Options
|
||||||
|
|
||||||
|
### Palette Mode
|
||||||
|
Provide a comma-separated list of colors:
|
||||||
|
```bash
|
||||||
|
piglet "Text" -p "red,blue,green"
|
||||||
|
piglet "Text" -p "#FF5733,#33FF57,#3357FF"
|
||||||
|
piglet "Text" -p "crimson,gold,navy"
|
||||||
|
```
|
||||||
|
|
||||||
|
### Gradient Mode
|
||||||
|
Use CSS gradient syntax:
|
||||||
|
```bash
|
||||||
|
piglet "Text" -g "linear-gradient(90deg, red, blue)"
|
||||||
|
piglet "Text" -g "linear-gradient(to right, #FF5733 0%, #33FF57 50%, #3357FF 100%)"
|
||||||
|
```
|
||||||
|
|
||||||
|
Supports:
|
||||||
|
- Hex colors (`#FF5733`)
|
||||||
|
- CSS4 color names (`red`, `blue`, `crimson`, etc.)
|
||||||
|
- Position percentages (`0%`, `50%`, `100%`)
|
||||||
|
- Angle notation (`90deg`, `180deg`, `to right`, `to bottom`)
|
||||||
|
|
||||||
|
## 💡 Examples
|
||||||
|
|
||||||
|
### Basic Animation
|
||||||
|
```bash
|
||||||
|
piglet "Welcome" -e fade-in -d 2s
|
||||||
|
```
|
||||||
|
|
||||||
|
### Rainbow Gradient with Typewriter
|
||||||
|
```bash
|
||||||
|
piglet "Rainbow Text" \
|
||||||
|
-g "linear-gradient(90deg, red, orange, yellow, green, blue, indigo, violet)" \
|
||||||
|
-e typewriter \
|
||||||
|
-d 4s \
|
||||||
|
-i ease-in-out
|
||||||
|
```
|
||||||
|
|
||||||
|
### Bouncing Logo with Custom Font
|
||||||
|
```bash
|
||||||
|
piglet "LOGO" \
|
||||||
|
-f slant \
|
||||||
|
-e bounce-in \
|
||||||
|
-p "#FF6B6B,#4ECDC4,#45B7D1" \
|
||||||
|
-i ease-out-bounce \
|
||||||
|
-l
|
||||||
|
```
|
||||||
|
|
||||||
|
### Infinite Wave with Gradient Flow
|
||||||
|
```bash
|
||||||
|
piglet "Ocean Waves" \
|
||||||
|
-g "linear-gradient(180deg, #0077be, #00c9ff, #0077be)" \
|
||||||
|
-e wave \
|
||||||
|
-l
|
||||||
|
```
|
||||||
|
|
||||||
|
### Custom Figlet Options
|
||||||
|
```bash
|
||||||
|
# Center text with width 200
|
||||||
|
piglet "Centered" -- -w 200 -c
|
||||||
|
|
||||||
|
# Use specific font with kerning
|
||||||
|
piglet "Custom" -f banner -- -k
|
||||||
|
```
|
||||||
|
|
||||||
|
## 🏗️ Architecture
|
||||||
|
|
||||||
|
```
|
||||||
|
CLI Input → Figlet Wrapper → Parser (duration/colors/gradients)
|
||||||
|
↓
|
||||||
|
Color Engine (palette/gradient interpolation)
|
||||||
|
↓
|
||||||
|
Animation Engine (effects + easing)
|
||||||
|
↓
|
||||||
|
Terminal Manager → Render Loop → Output
|
||||||
|
```
|
||||||
|
|
||||||
|
### Key Components
|
||||||
|
- **FigletWrapper**: Executes figlet and captures ASCII output
|
||||||
|
- **Parser**: Converts CLI strings to structured data (duration, colors, gradients)
|
||||||
|
- **ColorEngine**: Manages color palettes and gradient interpolation
|
||||||
|
- **AnimationEngine**: Applies motion effects with easing functions
|
||||||
|
- **TerminalManager**: Handles terminal setup/cleanup and frame rendering
|
||||||
|
|
||||||
|
## 🔧 Development
|
||||||
|
|
||||||
|
### Build
|
||||||
|
```bash
|
||||||
|
cargo build
|
||||||
|
```
|
||||||
|
|
||||||
|
### Test
|
||||||
|
```bash
|
||||||
|
cargo test --all-features
|
||||||
|
```
|
||||||
|
|
||||||
|
### Lint
|
||||||
|
```bash
|
||||||
|
cargo fmt --all -- --check
|
||||||
|
cargo clippy --all-targets --all-features -- -D warnings
|
||||||
|
```
|
||||||
|
|
||||||
|
### Documentation
|
||||||
|
```bash
|
||||||
|
cargo doc --no-deps --all-features
|
||||||
|
```
|
||||||
|
|
||||||
|
## 🎯 Cross-Platform Support
|
||||||
|
|
||||||
|
Piglet builds on:
|
||||||
|
- **Linux**: `x86_64-unknown-linux-gnu`, `x86_64-unknown-linux-musl`
|
||||||
|
- **macOS**: `x86_64-apple-darwin`, `aarch64-apple-darwin`
|
||||||
|
- **Windows**: `x86_64-pc-windows-msvc`
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# Build for specific target
|
||||||
|
cargo build --release --target x86_64-unknown-linux-musl
|
||||||
|
```
|
||||||
|
|
||||||
|
## 🤝 Contributing
|
||||||
|
|
||||||
|
Contributions are welcome! Please feel free to submit issues or pull requests.
|
||||||
|
|
||||||
|
1. Fork the repository
|
||||||
|
2. Create your feature branch (`git checkout -b feature/amazing-effect`)
|
||||||
|
3. Commit your changes (`git commit -m 'Add amazing effect'`)
|
||||||
|
4. Push to the branch (`git push origin feature/amazing-effect`)
|
||||||
|
5. Open a Pull Request
|
||||||
|
|
||||||
|
## 📝 License
|
||||||
|
|
||||||
|
This project is licensed under the MIT License - see the [LICENSE](LICENSE) file for details.
|
||||||
|
|
||||||
|
## 🙏 Acknowledgments
|
||||||
|
|
||||||
|
- [figlet](http://www.figlet.org/) - The original ASCII art generator
|
||||||
|
- [crossterm](https://github.com/crossterm-rs/crossterm) - Terminal manipulation
|
||||||
|
- [palette](https://github.com/Ogeon/palette) - Color space conversions
|
||||||
|
- [scirs2-interpolate](https://github.com/scirs/scirs2-interpolate) - Easing functions
|
||||||
|
|
||||||
|
## 📊 Project Stats
|
||||||
|
|
||||||
|

|
||||||
|

|
||||||
|

|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
<div align="center">
|
||||||
|
Made with ❤️ and Rust
|
||||||
|
</div>
|
||||||
49
src/animation/easing.rs
Normal file
49
src/animation/easing.rs
Normal file
@@ -0,0 +1,49 @@
|
|||||||
|
use anyhow::{Result, bail};
|
||||||
|
use scirs2_interpolate::*;
|
||||||
|
|
||||||
|
pub trait EasingFunction: Send + Sync {
|
||||||
|
fn ease(&self, t: f64) -> f64;
|
||||||
|
fn name(&self) -> &str;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Linear
|
||||||
|
pub struct Linear;
|
||||||
|
impl EasingFunction for Linear {
|
||||||
|
fn ease(&self, t: f64) -> f64 { t }
|
||||||
|
fn name(&self) -> &str { "linear" }
|
||||||
|
}
|
||||||
|
|
||||||
|
// Quadratic
|
||||||
|
pub struct EaseInQuad;
|
||||||
|
impl EasingFunction for EaseInQuad {
|
||||||
|
fn ease(&self, t: f64) -> f64 { quad_ease_in(t, 0.0, 1.0, 1.0) }
|
||||||
|
fn name(&self) -> &str { "ease-in-quad" }
|
||||||
|
}
|
||||||
|
|
||||||
|
pub struct EaseOutQuad;
|
||||||
|
impl EasingFunction for EaseOutQuad {
|
||||||
|
fn ease(&self, t: f64) -> f64 { quad_ease_out(t, 0.0, 1.0, 1.0) }
|
||||||
|
fn name(&self) -> &str { "ease-out-quad" }
|
||||||
|
}
|
||||||
|
|
||||||
|
pub struct EaseInOutQuad;
|
||||||
|
impl EasingFunction for EaseInOutQuad {
|
||||||
|
fn ease(&self, t: f64) -> f64 { quad_ease_in_out(t, 0.0, 1.0, 1.0) }
|
||||||
|
fn name(&self) -> &str { "ease-in-out-quad" }
|
||||||
|
}
|
||||||
|
|
||||||
|
// Cubic
|
||||||
|
pub struct EaseInCubic;
|
||||||
|
impl EasingFunction for EaseInCubic {
|
||||||
|
fn ease(&self, t: f64) -> f64 { cubic_ease_in(t, 0.0, 1.0, 1.0) }
|
||||||
|
fn name(&self) -> &str { "ease-in-cubic" }
|
||||||
|
}
|
||||||
|
|
||||||
|
pub struct EaseOutCubic;
|
||||||
|
impl EasingFunction for EaseOutCubic {
|
||||||
|
fn ease(&self, t: f64) -> f64 { cubic_ease_out(t, 0.0, 1.0, 1.0) }
|
||||||
|
fn name(&self) -> &str { "ease-out-cubic" }
|
||||||
|
}
|
||||||
|
|
||||||
|
pub struct EaseInOutCubic;
|
||||||
|
impl EasingFunction for EaseInOut
|
||||||
58
src/animation/mod.rs
Normal file
58
src/animation/mod.rs
Normal file
@@ -0,0 +1,58 @@
|
|||||||
|
pub mod effects;
|
||||||
|
pub mod easing;
|
||||||
|
pub mod timeline;
|
||||||
|
pub mod renderer;
|
||||||
|
|
||||||
|
use crate::color::ColorEngine;
|
||||||
|
use crate::utils::{ascii::AsciiArt, terminal::TerminalManager};
|
||||||
|
use anyhow::Result;
|
||||||
|
|
||||||
|
pub struct AnimationEngine {
|
||||||
|
ascii_art: AsciiArt,
|
||||||
|
duration_ms: u64,
|
||||||
|
fps: u32,
|
||||||
|
effect: Box<dyn effects::Effect>,
|
||||||
|
easing: Box<dyn easing::EasingFunction>,
|
||||||
|
color_engine: ColorEngine,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl AnimationEngine {
|
||||||
|
pub fn new(ascii_text: String, duration_ms: u64, fps: u32) -> Self {
|
||||||
|
Self {
|
||||||
|
ascii_art: AsciiArt::new(ascii_text),
|
||||||
|
duration_ms,
|
||||||
|
fps,
|
||||||
|
effect: Box::new(effects::FadeIn),
|
||||||
|
easing: Box::new(easing::Linear),
|
||||||
|
color_engine: ColorEngine::new(),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn with_effect(mut self, effect_name: &str) -> Result<Self> {
|
||||||
|
self.effect = effects::get_effect(effect_name)?;
|
||||||
|
Ok(self)
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn with_easing(mut self, easing_name: &str) -> Result<Self> {
|
||||||
|
self.easing = easing::get_easing_function(easing_name)?;
|
||||||
|
Ok(self)
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn with_color_engine(mut self, color_engine: ColorEngine) -> Self {
|
||||||
|
self.color_engine = color_engine;
|
||||||
|
self
|
||||||
|
}
|
||||||
|
|
||||||
|
pub async fn run(&self, terminal: &mut TerminalManager) -> Result<()> {
|
||||||
|
let renderer = renderer::Renderer::new(
|
||||||
|
&self.ascii_art,
|
||||||
|
self.duration_ms,
|
||||||
|
self.fps,
|
||||||
|
&*self.effect,
|
||||||
|
&*self.easing,
|
||||||
|
&self.color_engine,
|
||||||
|
);
|
||||||
|
|
||||||
|
renderer.render(terminal).await
|
||||||
|
}
|
||||||
|
}
|
||||||
71
src/cli.rs
Normal file
71
src/cli.rs
Normal file
@@ -0,0 +1,71 @@
|
|||||||
|
use clap::Parser;
|
||||||
|
|
||||||
|
#[derive(Parser, Debug)]
|
||||||
|
#[command(author, version, about, long_about = None)]
|
||||||
|
#[command(name = "piglet")]
|
||||||
|
#[command(about = "🐷 Animated and colorful figlet wrapper", long_about = None)]
|
||||||
|
pub struct PigletCli {
|
||||||
|
/// Text to render with figlet
|
||||||
|
#[arg(value_name = "TEXT")]
|
||||||
|
pub text: String,
|
||||||
|
|
||||||
|
/// Duration of animation (e.g., 3000ms, 0.3s, 0.5h, 5m)
|
||||||
|
#[arg(short, long, default_value = "3s")]
|
||||||
|
pub duration: String,
|
||||||
|
|
||||||
|
/// Color palette (hex or CSS4 colors, comma-separated)
|
||||||
|
/// Example: "#FF5733,#33FF57,#3357FF" or "red,green,blue"
|
||||||
|
#[arg(short = 'p', long, value_delimiter = ',')]
|
||||||
|
pub color_palette: Option<Vec<String>>,
|
||||||
|
|
||||||
|
/// Color gradient (CSS4 gradient definition)
|
||||||
|
/// Example: "linear-gradient(90deg, red, blue)"
|
||||||
|
#[arg(short = 'g', long)]
|
||||||
|
pub color_gradient: Option<String>,
|
||||||
|
|
||||||
|
/// Motion easing function
|
||||||
|
/// Options: linear, ease-in, ease-out, ease-in-out, ease-in-quad,
|
||||||
|
/// ease-out-quad, ease-in-out-quad, ease-in-cubic, ease-out-cubic,
|
||||||
|
/// ease-in-out-cubic, ease-in-back, ease-out-back, ease-in-out-back,
|
||||||
|
/// ease-in-elastic, ease-out-elastic, ease-in-out-elastic,
|
||||||
|
/// ease-in-bounce, ease-out-bounce, ease-in-out-bounce
|
||||||
|
#[arg(short = 'i', long, default_value = "ease-in-out")]
|
||||||
|
pub motion_ease: String,
|
||||||
|
|
||||||
|
/// Motion effect name
|
||||||
|
/// Options: fade-in, fade-out, fade-in-out, slide-in-top, slide-in-bottom,
|
||||||
|
/// slide-in-left, slide-in-right, scale-up, scale-down, pulse,
|
||||||
|
/// bounce-in, bounce-out, typewriter, typewriter-reverse, wave,
|
||||||
|
/// jello, color-cycle, rainbow, gradient-flow, rotate-in, rotate-out
|
||||||
|
#[arg(short, long, default_value = "fade-in")]
|
||||||
|
pub motion_effect: String,
|
||||||
|
|
||||||
|
/// Figlet font
|
||||||
|
#[arg(short = 'f', long)]
|
||||||
|
pub font: Option<String>,
|
||||||
|
|
||||||
|
/// Additional figlet options (use after --)
|
||||||
|
/// Example: piglet "Text" -- -w 200 -c
|
||||||
|
#[arg(last = true)]
|
||||||
|
pub figlet_args: Vec<String>,
|
||||||
|
|
||||||
|
/// Loop animation infinitely
|
||||||
|
#[arg(short, long)]
|
||||||
|
pub loop_animation: bool,
|
||||||
|
|
||||||
|
/// Frame rate (fps)
|
||||||
|
#[arg(long, default_value = "30")]
|
||||||
|
pub fps: u32,
|
||||||
|
|
||||||
|
/// List all available effects
|
||||||
|
#[arg(long)]
|
||||||
|
pub list_effects: bool,
|
||||||
|
|
||||||
|
/// List all available easing functions
|
||||||
|
#[arg(long)]
|
||||||
|
pub list_easing: bool,
|
||||||
|
|
||||||
|
/// List all available CSS4 colors
|
||||||
|
#[arg(long)]
|
||||||
|
pub list_colors: bool,
|
||||||
|
}
|
||||||
63
src/color/apply.rs
Normal file
63
src/color/apply.rs
Normal file
@@ -0,0 +1,63 @@
|
|||||||
|
use crate::parser::color::Color;
|
||||||
|
use crossterm::style::Color as CrosstermColor;
|
||||||
|
|
||||||
|
pub fn apply_color_to_char(ch: char, color: Color) -> String {
|
||||||
|
use crossterm::style::Stylize;
|
||||||
|
|
||||||
|
let crossterm_color = CrosstermColor::Rgb {
|
||||||
|
r: color.r,
|
||||||
|
g: color.g,
|
||||||
|
b: color.b,
|
||||||
|
};
|
||||||
|
|
||||||
|
format!("{}", ch.to_string().with(crossterm_color))
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn apply_color_to_line(line: &str, colors: &[Color]) -> String {
|
||||||
|
if colors.is_empty() {
|
||||||
|
return line.to_string();
|
||||||
|
}
|
||||||
|
|
||||||
|
line.chars()
|
||||||
|
.enumerate()
|
||||||
|
.map(|(i, ch)| {
|
||||||
|
if ch.is_whitespace() {
|
||||||
|
ch.to_string()
|
||||||
|
} else {
|
||||||
|
let color = colors[i % colors.len()];
|
||||||
|
apply_color_to_char(ch, color)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
.collect()
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn apply_gradient_to_text(text: &str, colors: &[Color]) -> String {
|
||||||
|
let lines: Vec<&str> = text.lines().collect();
|
||||||
|
let total_chars: usize = lines.iter().map(|l| l.chars().count()).sum();
|
||||||
|
|
||||||
|
if total_chars == 0 || colors.is_empty() {
|
||||||
|
return text.to_string();
|
||||||
|
}
|
||||||
|
|
||||||
|
let mut result = String::new();
|
||||||
|
let mut char_index = 0;
|
||||||
|
|
||||||
|
for (line_idx, line) in lines.iter().enumerate() {
|
||||||
|
for ch in line.chars() {
|
||||||
|
if ch.is_whitespace() {
|
||||||
|
result.push(ch);
|
||||||
|
} else {
|
||||||
|
let color_index = (char_index * colors.len()) / total_chars.max(1);
|
||||||
|
let color = colors[color_index.min(colors.len() - 1)];
|
||||||
|
result.push_str(&apply_color_to_char(ch, color));
|
||||||
|
char_index += 1;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if line_idx < lines.len() - 1 {
|
||||||
|
result.push('\n');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
result
|
||||||
|
}
|
||||||
26
src/color/gradient.rs
Normal file
26
src/color/gradient.rs
Normal file
@@ -0,0 +1,26 @@
|
|||||||
|
use crate::parser::gradient::Gradient;
|
||||||
|
use crate::parser::color::Color;
|
||||||
|
use anyhow::Result;
|
||||||
|
|
||||||
|
pub struct GradientEngine {
|
||||||
|
gradient: Gradient,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl GradientEngine {
|
||||||
|
pub fn new(gradient: Gradient) -> Self {
|
||||||
|
Self { gradient }
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn from_string(gradient_str: &str) -> Result<Self> {
|
||||||
|
let gradient = Gradient::parse(gradient_str)?;
|
||||||
|
Ok(Self::new(gradient))
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn color_at(&self, t: f64) -> Color {
|
||||||
|
self.gradient.color_at(t)
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn colors(&self, steps: usize) -> Vec<Color> {
|
||||||
|
self.gradient.colors(steps)
|
||||||
|
}
|
||||||
|
}
|
||||||
21
src/color/palette.rs
Normal file
21
src/color/palette.rs
Normal file
@@ -0,0 +1,21 @@
|
|||||||
|
"#ffff00".to_string(),
|
||||||
|
]).unwrap()
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Create ocean palette
|
||||||
|
pub fn ocean() -> Self {
|
||||||
|
Self::from_strings(&[
|
||||||
|
"#000080".to_string(),
|
||||||
|
"#0000ff".to_string(),
|
||||||
|
"#4169e1".to_string(),
|
||||||
|
"#87ceeb".to_string(),
|
||||||
|
"#add8e6".to_string(),
|
||||||
|
]).unwrap()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Default for ColorPalette {
|
||||||
|
fn default() -> Self {
|
||||||
|
Self::rainbow()
|
||||||
|
}
|
||||||
|
}
|
||||||
118
src/figlet.rs
Normal file
118
src/figlet.rs
Normal file
@@ -0,0 +1,118 @@
|
|||||||
|
use anyhow::{Context, Result, bail};
|
||||||
|
use std::process::Command;
|
||||||
|
use which::which;
|
||||||
|
|
||||||
|
pub struct FigletWrapper {
|
||||||
|
font: Option<String>,
|
||||||
|
args: Vec<String>,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl FigletWrapper {
|
||||||
|
pub fn new() -> Self {
|
||||||
|
Self {
|
||||||
|
font: None,
|
||||||
|
args: Vec::new(),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn with_font(mut self, font: Option<&str>) -> Self {
|
||||||
|
self.font = font.map(|s| s.to_string());
|
||||||
|
self
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn with_args(mut self, args: Vec<String>) -> Self {
|
||||||
|
self.args = args;
|
||||||
|
self
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn render(&self, text: &str) -> Result<String> {
|
||||||
|
let mut cmd = Command::new("figlet");
|
||||||
|
|
||||||
|
// Add font if specified
|
||||||
|
if let Some(font) = &self.font {
|
||||||
|
cmd.arg("-f").arg(font);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Add additional arguments
|
||||||
|
for arg in &self.args {
|
||||||
|
cmd.arg(arg);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Add the text
|
||||||
|
cmd.arg(text);
|
||||||
|
|
||||||
|
// Execute and capture output
|
||||||
|
let output = cmd.output()
|
||||||
|
.context("Failed to execute figlet")?;
|
||||||
|
|
||||||
|
if !output.status.success() {
|
||||||
|
let stderr = String::from_utf8_lossy(&output.stderr);
|
||||||
|
bail!("Figlet error: {}", stderr);
|
||||||
|
}
|
||||||
|
|
||||||
|
let result = String::from_utf8(output.stdout)
|
||||||
|
.context("Figlet output is not valid UTF-8")?;
|
||||||
|
|
||||||
|
Ok(result)
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn check_installed() -> Result<()> {
|
||||||
|
which("figlet")
|
||||||
|
.context("figlet not found. Please install figlet first.\n\
|
||||||
|
On Ubuntu/Debian: sudo apt-get install figlet\n\
|
||||||
|
On macOS: brew install figlet\n\
|
||||||
|
On Arch: sudo pacman -S figlet")?;
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn list_fonts() -> Result<Vec<String>> {
|
||||||
|
let output = Command::new("figlet")
|
||||||
|
.arg("-l")
|
||||||
|
.output()
|
||||||
|
.context("Failed to list figlet fonts")?;
|
||||||
|
|
||||||
|
if !output.status.success() {
|
||||||
|
bail!("Failed to list fonts");
|
||||||
|
}
|
||||||
|
|
||||||
|
let result = String::from_utf8_lossy(&output.stdout);
|
||||||
|
let fonts: Vec<String> = result
|
||||||
|
.lines()
|
||||||
|
.skip(1) // Skip header
|
||||||
|
.filter_map(|line| {
|
||||||
|
line.split_whitespace()
|
||||||
|
.next()
|
||||||
|
.map(|s| s.to_string())
|
||||||
|
})
|
||||||
|
.collect();
|
||||||
|
|
||||||
|
Ok(fonts)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Default for FigletWrapper {
|
||||||
|
fn default() -> Self {
|
||||||
|
Self::new()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[cfg(test)]
|
||||||
|
mod tests {
|
||||||
|
use super::*;
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test_figlet_installed() {
|
||||||
|
// This test will fail if figlet is not installed
|
||||||
|
assert!(FigletWrapper::check_installed().is_ok());
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test_basic_render() {
|
||||||
|
let figlet = FigletWrapper::new();
|
||||||
|
let result = figlet.render("Hi");
|
||||||
|
assert!(result.is_ok());
|
||||||
|
let ascii = result.unwrap();
|
||||||
|
assert!(!ascii.is_empty());
|
||||||
|
assert!(ascii.contains("H") || ascii.contains("_") || ascii.contains("|"));
|
||||||
|
}
|
||||||
|
}
|
||||||
8
src/lib.rs
Normal file
8
src/lib.rs
Normal file
@@ -0,0 +1,8 @@
|
|||||||
|
pub mod cli;
|
||||||
|
pub mod figlet;
|
||||||
|
pub mod color;
|
||||||
|
pub mod animation;
|
||||||
|
pub mod parser;
|
||||||
|
pub mod utils;
|
||||||
|
|
||||||
|
pub use cli::PigletCli;
|
||||||
101
src/main.rs
Normal file
101
src/main.rs
Normal file
@@ -0,0 +1,101 @@
|
|||||||
|
mod cli;
|
||||||
|
mod figlet;
|
||||||
|
mod color;
|
||||||
|
mod animation;
|
||||||
|
mod parser;
|
||||||
|
mod utils;
|
||||||
|
|
||||||
|
use anyhow::Result;
|
||||||
|
use cli::PigletCli;
|
||||||
|
use clap::Parser;
|
||||||
|
|
||||||
|
#[tokio::main]
|
||||||
|
async fn main() -> Result<()> {
|
||||||
|
// Parse CLI arguments
|
||||||
|
let args = PigletCli::parse();
|
||||||
|
|
||||||
|
// Show banner on first run
|
||||||
|
if std::env::args().len() == 1 {
|
||||||
|
show_welcome();
|
||||||
|
return Ok(());
|
||||||
|
}
|
||||||
|
|
||||||
|
// Verify figlet is installed
|
||||||
|
figlet::FigletWrapper::check_installed()?;
|
||||||
|
|
||||||
|
// Run the piglet magic
|
||||||
|
run_piglet(args).await?;
|
||||||
|
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
|
async fn run_piglet(args: PigletCli) -> Result<()> {
|
||||||
|
use crate::animation::AnimationEngine;
|
||||||
|
use crate::color::ColorEngine;
|
||||||
|
use crate::utils::terminal::TerminalManager;
|
||||||
|
|
||||||
|
// Parse duration
|
||||||
|
let duration_ms = parser::duration::parse_duration(&args.duration)?;
|
||||||
|
|
||||||
|
// Create figlet wrapper and render base ASCII art
|
||||||
|
let figlet = figlet::FigletWrapper::new()
|
||||||
|
.with_font(args.font.as_deref())
|
||||||
|
.with_args(args.figlet_args);
|
||||||
|
|
||||||
|
let ascii_art = figlet.render(&args.text)?;
|
||||||
|
|
||||||
|
// Setup color engine
|
||||||
|
let color_engine = ColorEngine::new()
|
||||||
|
.with_palette(args.color_palette.as_deref())
|
||||||
|
.with_gradient(args.color_gradient.as_deref())?;
|
||||||
|
|
||||||
|
// Setup animation engine
|
||||||
|
let animation_engine = AnimationEngine::new(
|
||||||
|
ascii_art,
|
||||||
|
duration_ms,
|
||||||
|
args.fps,
|
||||||
|
)
|
||||||
|
.with_effect(&args.motion_effect)?
|
||||||
|
.with_easing(&args.motion_ease)?
|
||||||
|
.with_color_engine(color_engine);
|
||||||
|
|
||||||
|
// Setup terminal
|
||||||
|
let mut terminal = TerminalManager::new()?;
|
||||||
|
terminal.setup()?;
|
||||||
|
|
||||||
|
// Run animation
|
||||||
|
loop {
|
||||||
|
animation_engine.run(&mut terminal).await?;
|
||||||
|
|
||||||
|
if !args.loop_animation {
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Cleanup
|
||||||
|
terminal.cleanup()?;
|
||||||
|
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
|
fn show_welcome() {
|
||||||
|
println!(r#"
|
||||||
|
____ _ __ __
|
||||||
|
/ __ \(_)___ _/ /__ / /_
|
||||||
|
/ /_/ / / __ `/ / _ \/ __/
|
||||||
|
/ ____/ / /_/ / / __/ /_
|
||||||
|
/_/ /_/\__, /_/\___/\__/
|
||||||
|
/____/
|
||||||
|
|
||||||
|
🐷 Piglet - Animated Figlet Wrapper
|
||||||
|
|
||||||
|
Usage: piglet [TEXT] [OPTIONS]
|
||||||
|
|
||||||
|
Examples:
|
||||||
|
piglet "Hello" -p "#FF5733,#33FF57"
|
||||||
|
piglet "World" -g "linear-gradient(90deg, red, blue)" -e fade-in
|
||||||
|
piglet "Cool!" -e typewriter -d 2s -i ease-out
|
||||||
|
|
||||||
|
Run 'piglet --help' for more information.
|
||||||
|
"#);
|
||||||
|
}
|
||||||
24
src/parser/color.rs
Normal file
24
src/parser/color.rs
Normal file
@@ -0,0 +1,24 @@
|
|||||||
|
use anyhow::{Result, Context};
|
||||||
|
use csscolorparser::Color as CssColor;
|
||||||
|
use palette::rgb::Rgb;
|
||||||
|
|
||||||
|
#[derive(Debug, Clone, Copy)]
|
||||||
|
pub struct Color {
|
||||||
|
pub r: u8,
|
||||||
|
pub g: u8,
|
||||||
|
pub b: u8,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Color {
|
||||||
|
pub fn new(r: u8, g: u8, b: u8) -> Self {
|
||||||
|
Self { r, g, b }
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn from_hex(hex: &str) -> Result<Self> {
|
||||||
|
let color = CssColor::parse(hex)
|
||||||
|
.context(format!("Failed to parse hex color: {}", hex))?;
|
||||||
|
|
||||||
|
Ok(Self {
|
||||||
|
r: (color.r * 255.0) as u8,
|
||||||
|
g: (color.g * 255.0) as u8,
|
||||||
|
b
|
||||||
72
src/parser/duration.rs
Normal file
72
src/parser/duration.rs
Normal file
@@ -0,0 +1,72 @@
|
|||||||
|
use anyhow::{Result, bail};
|
||||||
|
use regex::Regex;
|
||||||
|
use lazy_static::lazy_static;
|
||||||
|
|
||||||
|
lazy_static! {
|
||||||
|
static ref DURATION_REGEX: Regex = Regex::new(
|
||||||
|
r"^(\d+(?:\.\d+)?)(ms|s|m|h)$"
|
||||||
|
).unwrap();
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Parse duration string to milliseconds
|
||||||
|
/// Supports: 3000ms, 0.3s, 5m, 0.5h
|
||||||
|
pub fn parse_duration(duration: &str) -> Result<u64> {
|
||||||
|
let caps = DURATION_REGEX.captures(duration.trim())
|
||||||
|
.ok_or_else(|| anyhow::anyhow!("Invalid duration format: {}", duration))?;
|
||||||
|
|
||||||
|
let value: f64 = caps[1].parse()
|
||||||
|
.map_err(|_| anyhow::anyhow!("Invalid numeric value in duration"))?;
|
||||||
|
|
||||||
|
let unit = &caps[2];
|
||||||
|
|
||||||
|
let milliseconds = match unit {
|
||||||
|
"ms" => value,
|
||||||
|
"s" => value * 1000.0,
|
||||||
|
"m" => value * 60.0 * 1000.0,
|
||||||
|
"h" => value * 60.0 * 60.0 * 1000.0,
|
||||||
|
_ => bail!("Unknown time unit: {}", unit),
|
||||||
|
};
|
||||||
|
|
||||||
|
if milliseconds < 0.0 {
|
||||||
|
bail!("Duration cannot be negative");
|
||||||
|
}
|
||||||
|
|
||||||
|
Ok(milliseconds as u64)
|
||||||
|
}
|
||||||
|
|
||||||
|
#[cfg(test)]
|
||||||
|
mod tests {
|
||||||
|
use super::*;
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test_parse_milliseconds() {
|
||||||
|
assert_eq!(parse_duration("3000ms").unwrap(), 3000);
|
||||||
|
assert_eq!(parse_duration("500ms").unwrap(), 500);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test_parse_seconds() {
|
||||||
|
assert_eq!(parse_duration("3s").unwrap(), 3000);
|
||||||
|
assert_eq!(parse_duration("0.5s").unwrap(), 500);
|
||||||
|
assert_eq!(parse_duration("1.5s").unwrap(), 1500);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test_parse_minutes() {
|
||||||
|
assert_eq!(parse_duration("1m").unwrap(), 60000);
|
||||||
|
assert_eq!(parse_duration("0.5m").unwrap(), 30000);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test_parse_hours() {
|
||||||
|
assert_eq!(parse_duration("1h").unwrap(), 3600000);
|
||||||
|
assert_eq!(parse_duration("0.5h").unwrap(), 1800000);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test_invalid_format() {
|
||||||
|
assert!(parse_duration("invalid").is_err());
|
||||||
|
assert!(parse_duration("10").is_err());
|
||||||
|
assert!(parse_duration("10x").is_err());
|
||||||
|
}
|
||||||
|
}
|
||||||
3
src/parser/mod.rs
Normal file
3
src/parser/mod.rs
Normal file
@@ -0,0 +1,3 @@
|
|||||||
|
pub mod duration;
|
||||||
|
pub mod color;
|
||||||
|
pub mod gradient;
|
||||||
131
src/utils/ascii.rs
Normal file
131
src/utils/ascii.rs
Normal file
@@ -0,0 +1,131 @@
|
|||||||
|
use crate::parser::color::Color;
|
||||||
|
|
||||||
|
#[derive(Debug, Clone)]
|
||||||
|
pub struct AsciiArt {
|
||||||
|
lines: Vec<String>,
|
||||||
|
width: usize,
|
||||||
|
height: usize,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl AsciiArt {
|
||||||
|
pub fn new(text: String) -> Self {
|
||||||
|
let lines: Vec<String> = text.lines().map(|s| s.to_string()).collect();
|
||||||
|
let width = lines.iter().map(|l| l.len()).max().unwrap_or(0);
|
||||||
|
let height = lines.len();
|
||||||
|
|
||||||
|
Self {
|
||||||
|
lines,
|
||||||
|
width,
|
||||||
|
height,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn get_lines(&self) -> &[String] {
|
||||||
|
&self.lines
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn width(&self) -> usize {
|
||||||
|
self.width
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn height(&self) -> usize {
|
||||||
|
self.height
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn to_string(&self) -> String {
|
||||||
|
self.lines.join("\n")
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Get character at position
|
||||||
|
pub fn char_at(&self, x: usize, y: usize) -> Option<char> {
|
||||||
|
self.lines.get(y)?.chars().nth(x)
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Count non-whitespace characters
|
||||||
|
pub fn char_count(&self) -> usize {
|
||||||
|
self.lines.iter()
|
||||||
|
.flat_map(|line| line.chars())
|
||||||
|
.filter(|c| !c.is_whitespace())
|
||||||
|
.count()
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Get all character positions
|
||||||
|
pub fn char_positions(&self) -> Vec<(usize, usize, char)> {
|
||||||
|
let mut positions = Vec::new();
|
||||||
|
|
||||||
|
for (y, line) in self.lines.iter().enumerate() {
|
||||||
|
for (x, ch) in line.chars().enumerate() {
|
||||||
|
if !ch.is_whitespace() {
|
||||||
|
positions.push((x, y, ch));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
positions
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Apply fade effect (0.0 = invisible, 1.0 = visible)
|
||||||
|
pub fn apply_fade(&self, opacity: f64) -> String {
|
||||||
|
if opacity >= 1.0 {
|
||||||
|
return self.to_string();
|
||||||
|
}
|
||||||
|
|
||||||
|
if opacity <= 0.0 {
|
||||||
|
return " ".repeat(self.width).repeat(self.height);
|
||||||
|
}
|
||||||
|
|
||||||
|
// For ASCII, we can simulate fade by replacing chars with lighter ones
|
||||||
|
let fade_chars = [' ', '.', '·', '-', '~', '=', '+', '*', '#', '@'];
|
||||||
|
let index = (opacity * (fade_chars.len() - 1) as f64) as usize;
|
||||||
|
let fade_char = fade_chars[index];
|
||||||
|
|
||||||
|
self.lines.iter()
|
||||||
|
.map(|line| {
|
||||||
|
line.chars()
|
||||||
|
.map(|ch| {
|
||||||
|
if ch.is_whitespace() {
|
||||||
|
ch
|
||||||
|
} else {
|
||||||
|
fade_char
|
||||||
|
}
|
||||||
|
})
|
||||||
|
.collect::<String>()
|
||||||
|
})
|
||||||
|
.collect::<Vec<_>>()
|
||||||
|
.join("\n")
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Scale the ASCII art
|
||||||
|
pub fn scale(&self, factor: f64) -> Self {
|
||||||
|
if factor <= 0.0 {
|
||||||
|
return Self::new(String::new());
|
||||||
|
}
|
||||||
|
|
||||||
|
if (factor - 1.0).abs() < 0.01 {
|
||||||
|
return self.clone();
|
||||||
|
}
|
||||||
|
|
||||||
|
// Simple scaling by character repetition
|
||||||
|
let lines = if factor > 1.0 {
|
||||||
|
self.lines.iter()
|
||||||
|
.flat_map(|line| {
|
||||||
|
let scaled_line: String = line.chars()
|
||||||
|
.flat_map(|ch| std::iter::repeat(ch).take(factor as usize))
|
||||||
|
.collect();
|
||||||
|
std::iter::repeat(scaled_line).take(factor as usize)
|
||||||
|
})
|
||||||
|
.collect()
|
||||||
|
} else {
|
||||||
|
self.lines.iter()
|
||||||
|
.step_by((1.0 / factor) as usize)
|
||||||
|
.map(|line| {
|
||||||
|
line.chars()
|
||||||
|
.step_by((1.0 / factor) as usize)
|
||||||
|
.collect()
|
||||||
|
})
|
||||||
|
.collect()
|
||||||
|
};
|
||||||
|
|
||||||
|
Self::new(lines.join("\n"))
|
||||||
|
}
|
||||||
|
}
|
||||||
2
src/utils/mod.rs
Normal file
2
src/utils/mod.rs
Normal file
@@ -0,0 +1,2 @@
|
|||||||
|
pub mod terminal;
|
||||||
|
pub mod ascii;
|
||||||
101
src/utils/terminal.rs
Normal file
101
src/utils/terminal.rs
Normal file
@@ -0,0 +1,101 @@
|
|||||||
|
use anyhow::Result;
|
||||||
|
use crossterm::{
|
||||||
|
cursor,
|
||||||
|
execute,
|
||||||
|
terminal::{self, ClearType},
|
||||||
|
ExecutableCommand,
|
||||||
|
};
|
||||||
|
use std::io::{stdout, Write};
|
||||||
|
|
||||||
|
pub struct TerminalManager {
|
||||||
|
width: u16,
|
||||||
|
height: u16,
|
||||||
|
original_state: bool,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl TerminalManager {
|
||||||
|
pub fn new() -> Result<Self> {
|
||||||
|
let (width, height) = terminal::size()?;
|
||||||
|
Ok(Self {
|
||||||
|
width,
|
||||||
|
height,
|
||||||
|
original_state: false,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn setup(&mut self) -> Result<()> {
|
||||||
|
terminal::enable_raw_mode()?;
|
||||||
|
execute!(
|
||||||
|
stdout(),
|
||||||
|
terminal::EnterAlternateScreen,
|
||||||
|
cursor::Hide
|
||||||
|
)?;
|
||||||
|
self.original_state = true;
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn cleanup(&mut self) -> Result<()> {
|
||||||
|
if self.original_state {
|
||||||
|
execute!(
|
||||||
|
stdout(),
|
||||||
|
cursor::Show,
|
||||||
|
terminal::LeaveAlternateScreen
|
||||||
|
)?;
|
||||||
|
terminal::disable_raw_mode()?;
|
||||||
|
self.original_state = false;
|
||||||
|
}
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn clear(&self) -> Result<()> {
|
||||||
|
execute!(stdout(), terminal::Clear(ClearType::All))?;
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn move_to(&self, x: u16, y: u16) -> Result<()> {
|
||||||
|
execute!(stdout(), cursor::MoveTo(x, y))?;
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn get_size(&self) -> (u16, u16) {
|
||||||
|
(self.width, self.height)
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn refresh_size(&mut self) -> Result<()> {
|
||||||
|
let (width, height) = terminal::size()?;
|
||||||
|
self.width = width;
|
||||||
|
self.height = height;
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn print_at(&self, x: u16, y: u16, text: &str) -> Result<()> {
|
||||||
|
self.move_to(x, y)?;
|
||||||
|
print!("{}", text);
|
||||||
|
stdout().flush()?;
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn print_centered(&self, text: &str) -> Result<()> {
|
||||||
|
let lines: Vec<&str> = text.lines().collect();
|
||||||
|
let max_width = lines.iter().map(|l| l.len()).max().unwrap_or(0) as u16;
|
||||||
|
let height = lines.len() as u16;
|
||||||
|
|
||||||
|
let start_x = (self.width.saturating_sub(max_width)) / 2;
|
||||||
|
let start_y = (self.height.saturating_sub(height)) / 2;
|
||||||
|
|
||||||
|
for (i, line) in lines.iter().enumerate() {
|
||||||
|
let line_width = line.len() as u16;
|
||||||
|
let x = start_x + (max_width.saturating_sub(line_width)) / 2;
|
||||||
|
let y = start_y + i as u16;
|
||||||
|
self.print_at(x, y, line)?;
|
||||||
|
}
|
||||||
|
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Drop for TerminalManager {
|
||||||
|
fn drop(&mut self) {
|
||||||
|
let _ = self.cleanup();
|
||||||
|
}
|
||||||
|
}
|
||||||
171
tests/integration_tests.rs
Normal file
171
tests/integration_tests.rs
Normal file
@@ -0,0 +1,171 @@
|
|||||||
|
use piglet::{
|
||||||
|
figlet::FigletWrapper,
|
||||||
|
parser::{duration::parse_duration, color::Color, gradient::Gradient},
|
||||||
|
color::{ColorEngine, palette::ColorPalette},
|
||||||
|
animation::easing::get_easing_function,
|
||||||
|
animation::effects::get_effect,
|
||||||
|
};
|
||||||
|
use anyhow::Result;
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test_figlet_wrapper() -> Result<()> {
|
||||||
|
let figlet = FigletWrapper::new();
|
||||||
|
let result = figlet.render("Test")?;
|
||||||
|
assert!(!result.is_empty());
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test_duration_parser() -> Result<()> {
|
||||||
|
assert_eq!(parse_duration("1000ms")?, 1000);
|
||||||
|
assert_eq!(parse_duration("1s")?, 1000);
|
||||||
|
assert_eq!(parse_duration("1m")?, 60000);
|
||||||
|
assert_eq!(parse_duration("1h")?, 3600000);
|
||||||
|
assert_eq!(parse_duration("0.5s")?, 500);
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test_color_parser() -> Result<()> {
|
||||||
|
let color = Color::parse("#FF5733")?;
|
||||||
|
assert_eq!(color.r, 255);
|
||||||
|
assert_eq!(color.g, 87);
|
||||||
|
assert_eq!(color.b, 51);
|
||||||
|
|
||||||
|
let color = Color::parse("red")?;
|
||||||
|
assert_eq!(color.r, 255);
|
||||||
|
assert_eq!(color.g, 0);
|
||||||
|
assert_eq!(color.b, 0);
|
||||||
|
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test_gradient_parser() -> Result<()> {
|
||||||
|
let gradient = Gradient::parse("linear-gradient(90deg, red, blue)")?;
|
||||||
|
assert_eq!(gradient.stops.len(), 2);
|
||||||
|
|
||||||
|
let gradient = Gradient::parse(
|
||||||
|
"linear-gradient(to right, #FF5733 0%, #33FF57 50%, #3357FF 100%)"
|
||||||
|
)?;
|
||||||
|
assert_eq!(gradient.stops.len(), 3);
|
||||||
|
assert_eq!(gradient.stops[0].position, 0.0);
|
||||||
|
assert_eq!(gradient.stops[1].position, 0.5);
|
||||||
|
assert_eq!(gradient.stops[2].position, 1.0);
|
||||||
|
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test_color_interpolation() {
|
||||||
|
let red = Color::new(255, 0, 0);
|
||||||
|
let blue = Color::new(0, 0, 255);
|
||||||
|
let purple = red.interpolate(&blue, 0.5);
|
||||||
|
|
||||||
|
assert_eq!(purple.r, 127);
|
||||||
|
assert_eq!(purple.g, 0);
|
||||||
|
assert_eq!(purple.b, 127);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test_color_palette() -> Result<()> {
|
||||||
|
let palette = ColorPalette::from_strings(&[
|
||||||
|
"red".to_string(),
|
||||||
|
"green".to_string(),
|
||||||
|
"blue".to_string(),
|
||||||
|
])?;
|
||||||
|
|
||||||
|
assert_eq!(palette.len(), 3);
|
||||||
|
|
||||||
|
let color = palette.get_color(0);
|
||||||
|
assert_eq!(color.r, 255);
|
||||||
|
assert_eq!(color.g, 0);
|
||||||
|
assert_eq!(color.b, 0);
|
||||||
|
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test_easing_functions() -> Result<()> {
|
||||||
|
let linear = get_easing_function("linear")?;
|
||||||
|
assert_eq!(linear.ease(0.5), 0.5);
|
||||||
|
|
||||||
|
let ease_in = get_easing_function("ease-in")?;
|
||||||
|
let result = ease_in.ease(0.5);
|
||||||
|
assert!(result >= 0.0 && result <= 1.0);
|
||||||
|
|
||||||
|
let ease_out_bounce = get_easing_function("ease-out-bounce")?;
|
||||||
|
let result = ease_out_bounce.ease(0.5);
|
||||||
|
assert!(result >= 0.0 && result <= 1.5); // Bounce can overshoot
|
||||||
|
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test_effects() -> Result<()> {
|
||||||
|
let fade_in = get_effect("fade-in")?;
|
||||||
|
assert_eq!(fade_in.name(), "fade-in");
|
||||||
|
|
||||||
|
let typewriter = get_effect("typewriter")?;
|
||||||
|
assert_eq!(typewriter.name(), "typewriter");
|
||||||
|
|
||||||
|
let bounce = get_effect("bounce-in")?;
|
||||||
|
assert_eq!(bounce.name(), "bounce-in");
|
||||||
|
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test_color_engine() -> Result<()> {
|
||||||
|
let engine = ColorEngine::new()
|
||||||
|
.with_palette(Some(&["red".to_string(), "blue".to_string()]))?;
|
||||||
|
|
||||||
|
assert!(engine.has_colors());
|
||||||
|
|
||||||
|
let color = engine.get_color(0.0, 0);
|
||||||
|
assert!(color.is_some());
|
||||||
|
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test_gradient_color_at() -> Result<()> {
|
||||||
|
let gradient = Gradient::parse("linear-gradient(red, blue)")?;
|
||||||
|
|
||||||
|
let color_start = gradient.color_at(0.0);
|
||||||
|
assert_eq!(color_start.r, 255);
|
||||||
|
assert_eq!(color_start.b, 0);
|
||||||
|
|
||||||
|
let color_end = gradient.color_at(1.0);
|
||||||
|
assert_eq!(color_end.r, 0);
|
||||||
|
assert_eq!(color_end.b, 255);
|
||||||
|
|
||||||
|
let color_mid = gradient.color_at(0.5);
|
||||||
|
assert!(color_mid.r > 0 && color_mid.r < 255);
|
||||||
|
assert!(color_mid.b > 0 && color_mid.b < 255);
|
||||||
|
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test_invalid_duration() {
|
||||||
|
assert!(parse_duration("invalid").is_err());
|
||||||
|
assert!(parse_duration("10").is_err());
|
||||||
|
assert!(parse_duration("10x").is_err());
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test_invalid_color() {
|
||||||
|
assert!(Color::parse("notacolor").is_err());
|
||||||
|
assert!(Color::parse("#GGGGGG").is_err());
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test_invalid_effect() {
|
||||||
|
assert!(get_effect("not-an-effect").is_err());
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test_invalid_easing() {
|
||||||
|
assert!(get_easing_function("not-an-easing").is_err());
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user