Add utility scripts collection with auto-generated documentation

This commit introduces a comprehensive collection of utility scripts for shell
automation, color manipulation, and documentation generation:

Core Scripts:
- artifact_github_download.sh: Download GitHub Action artifacts via CLI
- css_color_filter.sh: Generate CSS filter values using SPSA algorithm
- css_color_palette.sh: Generate comprehensive color palettes (monochromatic, triadic, etc.)
- css_json_convert.sh: Convert CSS variables to JSON/YAML formats
- doc_bash_generate.sh: Auto-generate README.md with animated GIF demos
- doc_rust_generate.sh: Generate Rust project documentation
- jinja_template_render.sh: Render Jinja2 templates from CLI
- mime_mp4_gif.sh: Convert MP4 videos to GIF format

Documentation Features:
- Comprehensive README.md with table of contents
- 8 animated GIF demos showing real command examples
- Sandboxed demo execution in temporary directories
- 15-second timeout protection for intensive computations
- Automatic example extraction from --help output

Technical Implementation:
- Pure bash color utilities using only bc for arithmetic
- tput-based color codes for portability
- IFS-safe string parsing using parameter expansion
- Stdout/stderr isolation to prevent contamination
- Base64 encoding for multi-line text preservation

All scripts include detailed --help documentation with usage examples.

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude <noreply@anthropic.com>
This commit is contained in:
2025-10-30 03:10:19 +01:00
commit a71eef9006
22 changed files with 6462 additions and 0 deletions

1
.gitignore vendored Normal file
View File

@@ -0,0 +1 @@
tmp/

691
README.md Normal file
View File

@@ -0,0 +1,691 @@
# Utility Scripts Collection
> Comprehensive documentation with usage examples and demos
This documentation was auto-generated using [`doc_bash_generate.sh`](https://github.com/yourusername/yourrepo).
## Table of Contents
- [`artifact_github_download.sh`](#artifact-github-download-sh)
- [`css_color_filter.sh`](#css-color-filter-sh)
- [`css_color_palette.sh`](#css-color-palette-sh)
- [`css_json_convert.sh`](#css-json-convert-sh)
- [`doc_bash_generate.sh`](#doc-bash-generate-sh)
- [`doc_rust_generate.sh`](#doc-rust-generate-sh)
- [`jinja_template_render.sh`](#jinja-template-render-sh)
- [`mime_mp4_gif.sh`](#mime-mp4-gif-sh)
---
## `artifact_github_download.sh`
GitHub Artifact Downloader
### Demo
![Demo of artifact_github_download.sh](docs/img/artifact_github_download.gif)
### Usage
```bash
artifact_github_download.sh [OPTIONS] [ARGUMENTS]
```
### Options
<details>
<summary>Click to expand full help output</summary>
```
GitHub Artifact Downloader
USAGE:
artifact_github_download.sh <REPO> [OPTIONS]
ARGUMENTS:
REPO GitHub repository (owner/repo)
OPTIONS:
-n, --name NAME Artifact name to download (preselect)
-o, --output DIR Output directory (default: current directory)
-h, --help Show this help message
EXAMPLES:
# Interactive mode - list and select artifacts
artifact_github_download.sh valknarness/awesome
# Preselect artifact by name
artifact_github_download.sh valknarness/awesome -n awesome-database-latest
# Download to specific directory
artifact_github_download.sh valknarness/awesome -o ~/downloads
# Combine options
artifact_github_download.sh valknarness/awesome -n awesome-database-latest -o ~/downloads
```
</details>
### Examples
```bash
# Interactive mode - list and select artifacts
artifact_github_download.sh valknarness/awesome
# Preselect artifact by name
artifact_github_download.sh valknarness/awesome -n awesome-database-latest
# Download to specific directory
artifact_github_download.sh valknarness/awesome -o ~/downloads
# Combine options
artifact_github_download.sh valknarness/awesome -n awesome-database-latest -o ~/downloads
```
---
## `css_color_filter.sh`
CSS Color Filter Generator
### Demo
![Demo of css_color_filter.sh](docs/img/css_color_filter.gif)
### Usage
```bash
css_color_filter.sh [OPTIONS] [ARGUMENTS]
```
### Options
<details>
<summary>Click to expand full help output</summary>
```
CSS Color Filter Generator
Generate CSS filter values to transform black elements into any target color.
USAGE:
css_color_filter.sh [OPTIONS] [COLOR]
ARGUMENTS:
COLOR Hex color (e.g., #FF0000, ff0000) or RGB (e.g., 255,0,0)
OPTIONS:
-i, --interactive Interactive mode with colored preview
-r, --raw Output only the CSS filter (for piping)
-c, --copy Copy result to clipboard automatically
-h, --help Show this help message
EXAMPLES:
css_color_filter.sh "#FF5733"
css_color_filter.sh ff5733
css_color_filter.sh "255,87,51"
css_color_filter.sh -i
NOTE:
This tool generates filters that work on black elements.
To use with non-black elements, prepend: brightness(0) saturate(100%)
ALGORITHM:
Uses SPSA (Simultaneous Perturbation Stochastic Approximation) to find
optimal filter combinations that minimize color difference in RGB and HSL.
DEPENDENCIES:
bc For floating-point arithmetic
jq For JSON formatting (optional)
```
</details>
### Examples
```bash
css_color_filter.sh "#FF5733"
css_color_filter.sh ff5733
css_color_filter.sh "255,87,51"
css_color_filter.sh -i
```
---
## `css_color_palette.sh`
CSS Color Palette Generator (Pure Bash)
### Demo
![Demo of css_color_palette.sh](docs/img/css_color_palette.gif)
### Usage
```bash
css_color_palette.sh [OPTIONS] [ARGUMENTS]
```
### Options
<details>
<summary>Click to expand full help output</summary>
```
CSS Color Palette Generator (Pure Bash)
Generate comprehensive color palettes without Node.js dependencies.
USAGE:
css_color_palette.sh COLOR [OPTIONS]
ARGUMENTS:
COLOR Base hex color (e.g., #3498db, 3498db)
OPTIONS:
-p, --palette TYPE Palette type: monochromatic, analogous, complementary,
split-complementary, triadic, tetradic
-o, --output FILE Output file (default: ./colors.yaml)
-m, --mode MODE Color mode: light, dark (default: light)
-s, --style STYLE Style: shades, tints, tones, all (default: all)
-n, --name NAME Palette name (default: auto-generated)
--scales N Number of scale steps (default: 11)
-i, --interactive Interactive mode
-v, --verbose Verbose output with color preview
-h, --help Show this help message
DEPENDENCIES:
bc For floating-point arithmetic
EXAMPLES:
css_color_palette.sh "#3498db"
css_color_palette.sh "#3498db" -p triadic -o palette.json
css_color_palette.sh "ff5733" -p analogous -m dark
```
</details>
### Examples
```bash
css_color_palette.sh "#3498db"
css_color_palette.sh "#3498db" -p triadic -o palette.json
css_color_palette.sh "ff5733" -p analogous -m dark
```
---
## `css_json_convert.sh`
USAGE:
### Demo
![Demo of css_json_convert.sh](docs/img/css_json_convert.gif)
### Usage
```bash
css_json_convert.sh [OPTIONS] [ARGUMENTS]
```
### Options
<details>
<summary>Click to expand full help output</summary>
```
================================================================
CSS Variable to JSON/YAML Converter
Extract CSS custom properties with ease
================================================================
USAGE:
css_json_convert.sh [OPTIONS] <input.css>
DESCRIPTION:
Extracts CSS custom properties (variables) from a CSS file and converts
them to JSON or YAML format. Automatically detects output format from
file extension.
ARGUMENTS:
<input.css> Input CSS file containing CSS variables
OPTIONS:
-o, --output FILE Output file path (default: ./output.yaml)
Format auto-detected from extension (.json/.yaml/.yml)
-c, --camel-case Convert variable names to camelCase
(e.g., --main-color -> mainColor)
-v, --verbose Enable verbose output
-h, --help Show this help message
EXAMPLES:
# Extract CSS vars to YAML (default)
css_json_convert.sh styles.css
# Extract to JSON with custom output
css_json_convert.sh styles.css -o theme.json
# Convert variable names to camelCase
css_json_convert.sh styles.css -o vars.json --camel-case
CSS VARIABLE FORMAT:
The script extracts CSS custom properties in the format:
--variable-name: value;
Example input:
:root {
--main-color: #e8eaed;
--font-size: 16px;
}
Example JSON output:
{
"main-color": "#e8eaed",
"font-size": "16px"
}
```
</details>
### Examples
```bash
# Extract CSS vars to YAML (default)
css_json_convert.sh styles.css
# Extract to JSON with custom output
css_json_convert.sh styles.css -o theme.json
# Convert variable names to camelCase
css_json_convert.sh styles.css -o vars.json --camel-case
```
---
## `doc_bash_generate.sh`
Bash Documentation Generator with Animated GIFs
### Demo
![Demo of doc_bash_generate.sh](docs/img/doc_bash_generate.gif)
### Usage
```bash
doc_bash_generate.sh [OPTIONS] [ARGUMENTS]
```
### Options
<details>
<summary>Click to expand full help output</summary>
```
================================================================
Bash Documentation Generator
================================================================
Bash Documentation Generator with Animated GIFs
Generate sexy, comprehensive README.md files with embedded asciinema GIFs.
USAGE:
doc_bash_generate.sh [OPTIONS] <executable> [executable...]
ARGUMENTS:
executable One or more executables or glob patterns
OPTIONS:
-o, --output FILE Output README.md path (default: ./README.md)
-t, --title TITLE Documentation title (default: auto-generated)
--no-gif Skip GIF generation (faster, text only)
--gif-only Only generate GIFs, don't update README
-h, --help Show this help message
EXAMPLES:
doc_bash_generate.sh css_*.sh
doc_bash_generate.sh -o docs/README.md *.sh
doc_bash_generate.sh --title "My Awesome Tools" script1.sh script2.sh
DEPENDENCIES:
asciinema Terminal session recorder
agg Asciinema to GIF converter (cargo install agg)
NOTES:
Demos are automatically generated by running --help on each command.
GIF recordings are created in a temporary directory and cleaned up after.
```
</details>
### Examples
```bash
doc_bash_generate.sh css_*.sh
doc_bash_generate.sh -o docs/README.md *.sh
doc_bash_generate.sh --title "My Awesome Tools" script1.sh script2.sh
```
---
## `doc_rust_generate.sh`
Rust Documentation Generator with Custom Themes
### Demo
![Demo of doc_rust_generate.sh](docs/img/doc_rust_generate.gif)
### Usage
```bash
doc_rust_generate.sh [OPTIONS] [ARGUMENTS]
```
### Options
<details>
<summary>Click to expand full help output</summary>
```
Rust Documentation Generator with Custom Themes
USAGE:
doc_rust_generate.sh [OPTIONS] <inputs...>
DESCRIPTION:
Generate beautiful Rust documentation with custom color schemes and styling.
Supports various input types including Rust projects, individual files, and more.
ARGUMENTS:
<inputs> Input file(s) or pattern(s) to document:
- Rust project directories (containing Cargo.toml)
- Individual .rs files
- Markdown files (.md)
- JSON/TOML configuration files
- Glob patterns (e.g., src/**/*.rs)
OPTIONS:
-o, --output DIR Output directory for generated docs
(default: $PWD/output)
-c, --color COLOR Primary accent color (hex format)
(default: #ff69b4)
Examples: #3498db, #10b981, #8b5cf6
-s, --style STYLE Background style theme
Options: slate, zinc, neutral, stone, gray
(default: slate)
--font-sans FONT Google Font for body text (default: Inter)
--font-mono FONT Google Font for code blocks
(default: JetBrains Mono)
--serve Start HTTP server after generation
--open Open documentation in browser (implies --serve)
-p, --port PORT Port for HTTP server (default: 8000)
-v, --verbose Enable verbose output
-d, --dry-run Show what would be done without executing
-h, --help Show this help message
EXAMPLES:
# Generate docs for current Rust project
doc_rust_generate.sh .
# Custom color scheme
doc_rust_generate.sh . -c "#3498db" -s zinc -o ./docs
# Document specific files
doc_rust_generate.sh src/lib.rs src/main.rs -o ./api-docs
# Use custom fonts
doc_rust_generate.sh . --font-sans "Roboto" --font-mono "Fira Code"
# Generate and open in browser
doc_rust_generate.sh . --open
NOTES:
- Requires: cargo, rustdoc, bc, yq, jq, python3 with jinja2
- Colors are automatically generated in light and dark variants
- Google Fonts are automatically imported
- Mermaid.js diagrams are automatically rendered
```
</details>
### Examples
```bash
# Generate docs for current Rust project
doc_rust_generate.sh .
# Custom color scheme
doc_rust_generate.sh . -c "#3498db" -s zinc -o ./docs
# Document specific files
doc_rust_generate.sh src/lib.rs src/main.rs -o ./api-docs
# Use custom fonts
doc_rust_generate.sh . --font-sans "Roboto" --font-mono "Fira Code"
# Generate and open in browser
doc_rust_generate.sh . --open
```
---
## `jinja_template_render.sh`
JINJA2 TEMPLATE RENDERER - NINJA EDITION
### Demo
![Demo of jinja_template_render.sh](docs/img/jinja_template_render.gif)
### Usage
```bash
jinja_template_render.sh [OPTIONS] [ARGUMENTS]
```
### Options
<details>
<summary>Click to expand full help output</summary>
```
JINJA2 TEMPLATE RENDERER - NINJA EDITION
USAGE:
jinja_template_render.sh [OPTIONS] <template> [template...]
DESCRIPTION:
A sophisticated Jinja2 template rendering engine with support for
multiple variable sources, glob patterns, and ninja-style operations.
ARGUMENTS:
<template> Template file(s) to render (glob patterns supported)
Examples: template.j2, templates/*.j2, **/*.jinja2
OPTIONS:
-o, --output DIR Output directory (default: ./output)
-v, --var KEY=VALUE Define template variable (can be used multiple times)
-f, --file FILE Load variables from JSON/YAML file (repeatable)
-V, --verbose Enable verbose ninja commentary
-p, --preview Preview rendered output without saving
-s, --strict Enable strict mode (fail on undefined variables)
-d, --dry-run Perform dry run without writing files
-w, --watch Watch templates and re-render on changes (experimental)
-h, --help Show this legendary scroll of knowledge
VARIABLE SOURCES:
Variables are merged in this order (later sources override earlier):
1. YAML files (loaded via yq)
2. JSON files (loaded via jq)
3. CLI variables (-v KEY=VALUE)
EXAMPLES:
# Render single template with CLI variables
jinja_template_render.sh template.j2 -v name=Ninja -v level=Master
# Render multiple templates with YAML config
jinja_template_render.sh templates/*.j2 -f config.yaml -o dist/
# Render with multiple variable sources
jinja_template_render.sh app.j2 -f base.yaml -f env.json -v debug=true
# Preview without saving
jinja_template_render.sh template.j2 -f vars.yaml --preview --verbose
# Strict mode with dry run
jinja_template_render.sh *.j2 -f vars.json --strict --dry-run
TEMPLATE SYNTAX:
Jinja2 template example:
Hello {{ name }}!
{% for item in items %}
- {{ item }}
{% endfor %}
{% if debug %}Debug mode enabled{% endif %}
REQUIREMENTS:
- Python 3 with jinja2 package
- jq (for JSON parsing)
- yq (for YAML parsing)
[NINJA] May your templates be swift and your renders be flawless! >>
```
</details>
### Examples
```bash
# Render single template with CLI variables
jinja_template_render.sh template.j2 -v name=Ninja -v level=Master
# Render multiple templates with YAML config
jinja_template_render.sh templates/*.j2 -f config.yaml -o dist/
# Render with multiple variable sources
jinja_template_render.sh app.j2 -f base.yaml -f env.json -v debug=true
# Preview without saving
jinja_template_render.sh template.j2 -f vars.yaml --preview --verbose
# Strict mode with dry run
jinja_template_render.sh *.j2 -f vars.json --strict --dry-run
```
---
## `mime_mp4_gif.sh`
Usage: mime_mp4_gif.sh [OPTIONS] INPUT_FILE [OUTPUT_FILE]
### Demo
![Demo of mime_mp4_gif.sh](docs/img/mime_mp4_gif.gif)
### Usage
```bash
mime_mp4_gif.sh [OPTIONS] [ARGUMENTS]
```
### Options
<details>
<summary>Click to expand full help output</summary>
```
Usage: mime_mp4_gif.sh [OPTIONS] INPUT_FILE [OUTPUT_FILE]
Convert MP4 videos to animated GIFs with advanced frame extraction algorithms.
Arguments:
INPUT_FILE Input MP4 video file (required)
OUTPUT_FILE Output GIF file (optional, defaults to INPUT_FILE.gif)
Options:
-k, --keyframes N Number of keyframes to extract (default: 10)
-d, --keyframe-duration MS Duration of each frame in milliseconds (default: 100)
Valid range: 1-30000 ms
Lower values = faster animation
Higher values = slower animation
-i, --input-schedules N Number of input schedules (default: 1)
1 schedule = entire video duration
N schedules = divide video into N segments
-t, --transition TYPE Interpolation function for frame timing
Available: linear sinoid cubic quadratic exponential bounce elastic
(default: linear)
-s, --schedule TYPE Algorithm to distribute keyframes across schedules
Available: uniform front-load back-load center-peak edge-peak fibonacci golden-ratio
(default: uniform)
-m, --magic TYPE Apply magical effects to the GIF
Available: none psychedelic dither-bloom edge-glow temporal-blur chromatic-shift vaporwave
(default: none)
-v, --verbose Enable verbose output
-h, --help Show this help message
Examples:
# Basic conversion with 15 keyframes
mime_mp4_gif.sh -k 15 video.mp4
# Fast animation with 50ms per frame
mime_mp4_gif.sh -k 20 -d 50 video.mp4
# Slow animation with 500ms per frame
mime_mp4_gif.sh -k 10 -d 500 video.mp4
# Use sinusoidal transition with center-peak distribution
mime_mp4_gif.sh -t sinoid -s center-peak -k 20 video.mp4
# Apply psychedelic magic with fibonacci distribution
mime_mp4_gif.sh -m psychedelic -s fibonacci -k 13 video.mp4 trippy.gif
# Complex: 3 schedules with cubic interpolation and edge glow
mime_mp4_gif.sh -i 3 -t cubic -s front-load -m edge-glow -k 30 video.mp4
```
</details>
---
## Installation
All scripts are available in this repository. Make sure they are executable:
```bash
chmod +x *.sh
```
Add them to your PATH for easy access:
```bash
export PATH="$PATH:$(pwd)"
```
## Dependencies
Common dependencies for these scripts:
- `bash` (4.0+)
- `bc` - For floating-point arithmetic
Specific dependencies are listed in each command's help output.
## Contributing
Contributions are welcome! Please ensure:
- Scripts follow POSIX conventions
- Include comprehensive `--help` output with usage examples
## License
MIT License - See LICENSE file for details.
---
*Documentation generated on 2025-10-30 02:59:22 CET by [`doc_bash_generate.sh`](https://github.com/yourusername/yourrepo)*

477
artifact_github_download.sh Executable file
View File

@@ -0,0 +1,477 @@
#!/usr/bin/env bash
#############################################################################
# GitHub Artifact Downloader
#
# Download and extract GitHub Actions artifacts with style
#
# Usage:
# artifact_github_download.sh <REPO> [OPTIONS]
#
# Arguments:
# REPO GitHub repository (owner/repo)
#
# Options:
# -n, --name NAME Artifact name to download (preselect)
# -o, --output DIR Output directory (default: current directory)
# -h, --help Show this help message
#
# Examples:
# artifact_github_download.sh valknarness/awesome
# artifact_github_download.sh valknarness/awesome -n awesome-database-latest
# artifact_github_download.sh valknarness/awesome -o ~/downloads
#############################################################################
set -euo pipefail
# ============================================================================
# Color Definitions
# ============================================================================
# Check if terminal supports colors
if [[ -t 1 ]] && command -v tput >/dev/null 2>&1; then
COLORS=$(tput colors 2>/dev/null || echo 0)
if [[ $COLORS -ge 8 ]]; then
# Standard colors
RED=$(tput setaf 1)
GREEN=$(tput setaf 2)
YELLOW=$(tput setaf 3)
BLUE=$(tput setaf 4)
MAGENTA=$(tput setaf 5)
CYAN=$(tput setaf 6)
WHITE=$(tput setaf 7)
# Bright colors
BRIGHT_GREEN=$(tput setaf 10 2>/dev/null || tput setaf 2)
BRIGHT_YELLOW=$(tput setaf 11 2>/dev/null || tput setaf 3)
BRIGHT_BLUE=$(tput setaf 12 2>/dev/null || tput setaf 4)
BRIGHT_MAGENTA=$(tput setaf 13 2>/dev/null || tput setaf 5)
BRIGHT_CYAN=$(tput setaf 14 2>/dev/null || tput setaf 6)
# Text formatting
BOLD=$(tput bold)
DIM=$(tput dim 2>/dev/null || echo "")
RESET=$(tput sgr0)
else
RED="" GREEN="" YELLOW="" BLUE="" MAGENTA="" CYAN="" WHITE=""
BRIGHT_GREEN="" BRIGHT_YELLOW="" BRIGHT_BLUE="" BRIGHT_MAGENTA="" BRIGHT_CYAN=""
BOLD="" DIM="" RESET=""
fi
else
RED="" GREEN="" YELLOW="" BLUE="" MAGENTA="" CYAN="" WHITE=""
BRIGHT_GREEN="" BRIGHT_YELLOW="" BRIGHT_BLUE="" BRIGHT_MAGENTA="" BRIGHT_CYAN=""
BOLD="" DIM="" RESET=""
fi
# ============================================================================
# Logging Functions
# ============================================================================
log_info() {
echo -e "${BRIGHT_BLUE}${BOLD}${RESET} ${CYAN}$*${RESET}" >&2
}
log_success() {
echo -e "${BRIGHT_GREEN}${BOLD}${RESET} ${GREEN}$*${RESET}" >&2
}
log_warning() {
echo -e "${BRIGHT_YELLOW}${BOLD}${RESET} ${YELLOW}$*${RESET}" >&2
}
log_error() {
echo -e "${RED}${BOLD}${RESET} ${RED}$*${RESET}" >&2
}
log_step() {
echo -e "${BRIGHT_MAGENTA}${BOLD}${RESET} ${MAGENTA}$*${RESET}" >&2
}
log_header() {
local text="$*"
local length=${#text}
local line=$(printf '═%.0s' $(seq 1 $length))
echo "" >&2
echo -e "${BRIGHT_CYAN}${BOLD}${line}${RESET}" >&2
echo -e "${BRIGHT_CYAN}${BOLD}${RESET}${BOLD}${WHITE}${text}${RESET}${BRIGHT_CYAN}${BOLD}${RESET}" >&2
echo -e "${BRIGHT_CYAN}${BOLD}${line}${RESET}" >&2
echo "" >&2
}
log_data() {
local label="$1"
local value="$2"
echo -e " ${DIM}${label}:${RESET} ${BOLD}${value}${RESET}" >&2
}
# ============================================================================
# Helper Functions
# ============================================================================
check_dependencies() {
local missing=()
if ! command -v gh &> /dev/null; then
missing+=("gh (GitHub CLI)")
fi
if ! command -v jq &> /dev/null; then
missing+=("jq")
fi
if ! command -v unzip &> /dev/null; then
missing+=("unzip")
fi
if [[ ${#missing[@]} -gt 0 ]]; then
log_error "Missing required dependencies:"
for dep in "${missing[@]}"; do
echo -e " ${RED}${RESET} ${dep}"
done
exit 1
fi
}
check_gh_auth() {
if ! gh auth status &> /dev/null; then
log_error "Not authenticated with GitHub CLI"
log_info "Run: ${BOLD}gh auth login${RESET}"
exit 1
fi
}
show_help() {
cat << EOF
${BOLD}${BRIGHT_CYAN}GitHub Artifact Downloader${RESET}
${BOLD}USAGE:${RESET}
$(basename "$0") ${CYAN}<REPO>${RESET} [${YELLOW}OPTIONS${RESET}]
${BOLD}ARGUMENTS:${RESET}
${CYAN}REPO${RESET} GitHub repository (${DIM}owner/repo${RESET})
${BOLD}OPTIONS:${RESET}
${YELLOW}-n, --name NAME${RESET} Artifact name to download (preselect)
${YELLOW}-o, --output DIR${RESET} Output directory (default: current directory)
${YELLOW}-h, --help${RESET} Show this help message
${BOLD}EXAMPLES:${RESET}
${DIM}# Interactive mode - list and select artifacts${RESET}
$(basename "$0") valknarness/awesome
${DIM}# Preselect artifact by name${RESET}
$(basename "$0") valknarness/awesome -n awesome-database-latest
${DIM}# Download to specific directory${RESET}
$(basename "$0") valknarness/awesome -o ~/downloads
${DIM}# Combine options${RESET}
$(basename "$0") valknarness/awesome -n awesome-database-latest -o ~/downloads
EOF
}
format_size() {
local bytes=$1
if (( bytes < 1024 )); then
echo "${bytes}B"
elif (( bytes < 1048576 )); then
awk "BEGIN {printf \"%.1fKB\", $bytes/1024}"
elif (( bytes < 1073741824 )); then
awk "BEGIN {printf \"%.1fMB\", $bytes/1048576}"
else
awk "BEGIN {printf \"%.2fGB\", $bytes/1073741824}"
fi
}
format_date() {
local iso_date="$1"
if command -v date &> /dev/null; then
if date --version &> /dev/null 2>&1; then
# GNU date
date -d "$iso_date" "+%Y-%m-%d %H:%M:%S" 2>/dev/null || echo "$iso_date"
else
# BSD date (macOS)
date -j -f "%Y-%m-%dT%H:%M:%SZ" "$iso_date" "+%Y-%m-%d %H:%M:%S" 2>/dev/null || echo "$iso_date"
fi
else
echo "$iso_date"
fi
}
# ============================================================================
# Main Functions
# ============================================================================
list_artifacts() {
local repo="$1"
log_step "Fetching artifacts from ${BOLD}${repo}${RESET}..."
# First check if there are any artifacts using gh's built-in jq
local count
count=$(gh api \
-H "Accept: application/vnd.github+json" \
-H "X-GitHub-Api-Version: 2022-11-28" \
--jq '.artifacts | length' \
"/repos/${repo}/actions/artifacts?per_page=100" 2>/dev/null)
if [[ -z "$count" ]]; then
log_error "Failed to fetch artifacts from repository"
log_info "Please check that:"
echo " • The repository ${BOLD}${repo}${RESET} exists and you have access"
echo " • GitHub Actions is enabled for this repository"
exit 1
fi
if [[ "$count" -eq 0 ]]; then
log_warning "No artifacts found in repository ${BOLD}${repo}${RESET}"
log_info "This repository may not have any workflow runs that produced artifacts"
exit 0
fi
# Now fetch the full JSON response
local artifacts_json
artifacts_json=$(gh api \
-H "Accept: application/vnd.github+json" \
-H "X-GitHub-Api-Version: 2022-11-28" \
"/repos/${repo}/actions/artifacts?per_page=100" 2>/dev/null)
echo "$artifacts_json"
}
select_artifact() {
local artifacts_json="$1"
local preselect_name="$2"
# Parse artifacts
local artifacts
artifacts=$(echo "$artifacts_json" | jq -r '.artifacts[] |
"\(.id)|\(.name)|\(.size_in_bytes)|\(.created_at)|\(.workflow_run.id)"')
# If preselect name is provided, find matching artifact
if [[ -n "$preselect_name" ]]; then
local selected
selected=$(echo "$artifacts" | grep -F "|${preselect_name}|" | head -1)
if [[ -z "$selected" ]]; then
log_error "Artifact '${BOLD}${preselect_name}${RESET}' not found"
log_info "Available artifacts:"
echo "$artifacts" | while IFS='|' read -r id name size created workflow; do
echo " ${CYAN}${RESET} ${name}"
done
exit 1
fi
echo "$selected"
return 0
fi
# Interactive selection
log_info "Available artifacts:"
echo ""
local i=1
local -a artifact_array
while IFS='|' read -r id name size created workflow; do
artifact_array+=("$id|$name|$size|$created|$workflow")
local formatted_size=$(format_size "$size")
local formatted_date=$(format_date "$created")
printf " ${BOLD}${YELLOW}[%2d]${RESET} ${BRIGHT_CYAN}%s${RESET}\n" "$i" "$name"
printf " ${DIM}Size: ${RESET}%s ${DIM}Created: ${RESET}%s\n" "$formatted_size" "$formatted_date"
echo ""
((i++))
done <<< "$artifacts"
# Prompt for selection
local selection
while true; do
echo -n -e "${BRIGHT_MAGENTA}${BOLD}<EFBFBD>${RESET} ${MAGENTA}Select artifact [1-$((i-1))]:${RESET} "
read -r selection
if [[ "$selection" =~ ^[0-9]+$ ]] && [[ "$selection" -ge 1 ]] && [[ "$selection" -lt "$i" ]]; then
break
else
log_warning "Invalid selection. Please enter a number between 1 and $((i-1))"
fi
done
echo "${artifact_array[$((selection-1))]}"
}
download_artifact() {
local repo="$1"
local artifact_id="$2"
local artifact_name="$3"
local output_dir="$4"
log_step "Downloading artifact ${BOLD}${artifact_name}${RESET}..."
# Create output directory if it doesn't exist
mkdir -p "$output_dir"
# Download artifact using gh
local zip_file="${output_dir}/${artifact_name}.zip"
if gh api \
-H "Accept: application/vnd.github+json" \
-H "X-GitHub-Api-Version: 2022-11-28" \
"/repos/${repo}/actions/artifacts/${artifact_id}/zip" \
> "$zip_file" 2>/dev/null; then
log_success "Downloaded to ${BOLD}${zip_file}${RESET}"
echo "$zip_file"
else
log_error "Failed to download artifact"
exit 1
fi
}
extract_artifact() {
local zip_file="$1"
local output_dir="$2"
log_step "Extracting archive..."
# Create extraction directory
local extract_dir="${output_dir}/$(basename "$zip_file" .zip)"
mkdir -p "$extract_dir"
if unzip -q "$zip_file" -d "$extract_dir"; then
log_success "Extracted to ${BOLD}${extract_dir}${RESET}"
# Show extracted files
log_info "Extracted files:"
find "$extract_dir" -type f -exec basename {} \; | while read -r file; do
echo " ${GREEN}${RESET} ${file}"
done
# Remove zip file
rm "$zip_file"
log_info "Cleaned up zip file"
echo "$extract_dir"
else
log_error "Failed to extract archive"
exit 1
fi
}
# ============================================================================
# Main Script
# ============================================================================
main() {
local repo=""
local artifact_name=""
local output_dir="."
# Parse arguments
while [[ $# -gt 0 ]]; do
case $1 in
-h|--help)
show_help
exit 0
;;
-n|--name)
artifact_name="$2"
shift 2
;;
-o|--output)
output_dir="$2"
shift 2
;;
-*)
log_error "Unknown option: $1"
echo ""
show_help
exit 1
;;
*)
if [[ -z "$repo" ]]; then
repo="$1"
else
log_error "Unexpected argument: $1"
echo ""
show_help
exit 1
fi
shift
;;
esac
done
# Validate required arguments
if [[ -z "$repo" ]]; then
log_error "Repository argument is required"
echo ""
show_help
exit 1
fi
# Validate repository format
if [[ ! "$repo" =~ ^[^/]+/[^/]+$ ]]; then
log_error "Invalid repository format. Expected: ${BOLD}owner/repo${RESET}"
exit 1
fi
# Show header
log_header "GitHub Artifact Downloader"
# Check dependencies
log_step "Checking dependencies..."
check_dependencies
log_success "All dependencies found"
# Check GitHub authentication
log_step "Checking GitHub authentication..."
check_gh_auth
log_success "Authenticated with GitHub"
echo ""
log_data "Repository" "${BRIGHT_CYAN}${repo}${RESET}"
if [[ -n "$artifact_name" ]]; then
log_data "Artifact" "${BRIGHT_YELLOW}${artifact_name}${RESET}"
fi
log_data "Output" "${BRIGHT_GREEN}${output_dir}${RESET}"
echo ""
# List artifacts
local artifacts_json
artifacts_json=$(list_artifacts "$repo")
# Select artifact
local selected
selected=$(select_artifact "$artifacts_json" "$artifact_name")
IFS='|' read -r artifact_id name size created workflow <<< "$selected"
echo ""
log_info "Selected artifact:"
log_data " Name" "${BRIGHT_CYAN}${name}${RESET}"
log_data " Size" "$(format_size "$size")"
log_data " Created" "$(format_date "$created")"
echo ""
# Download artifact
local zip_file
zip_file=$(download_artifact "$repo" "$artifact_id" "$name" "$output_dir")
# Extract artifact
local extract_dir
extract_dir=$(extract_artifact "$zip_file" "$output_dir")
# Success summary
echo ""
log_header "Download Complete!"
log_data "Location" "${BOLD}${extract_dir}${RESET}"
echo ""
log_success "All done! 🎉"
}
# Run main function
main "$@"

View File

@@ -0,0 +1,90 @@
<script type="module">
import mermaid from 'https://cdn.jsdelivr.net/npm/mermaid@10/dist/mermaid.esm.min.mjs';
mermaid.initialize({
startOnLoad: false,
theme: 'dark',
themeVariables: {
primaryColor: '{{primaryColor}}',
primaryTextColor: '{{primaryTextColor}}',
primaryBorderColor: '{{primaryBorderColor}}',
lineColor: '{{lineColor}}',
secondaryColor: '#{{secondaryColor}}',
tertiaryColor: '{{secondaryColor}}',
background: '{{background}}',
mainBkg: '{{mainBkg}}',
secondBkg: '{{secondBkg}}',
border1: '{{border1}}',
border2: '{{border2}}',
note: '{{note}}',
noteBkgColor: '{{noteBkgColor}}',
noteTextColor: '{{noteTextColor}}',
noteBorderColor: '{{noteBorderColor}}',
arrowheadColor: '{{arrowheadColor}}',
fontFamily: '{{fontFamily}}',
fontSize: '{{fontSize}}',
darkMode: '{{darkMode}}',
edgeLabelBackground: '{{edgeLabelBackground}}',
clusterBkg: '{{clusterBkg}}',
clusterBorder: '{{clusterBorder}}',
defaultLinkColor: '{{defaultLinkColor}}',
titleColor: '{{titleColor}}',
nodeTextColor: '{{nodeTextColor}}'
},
flowchart: {
htmlLabels: true,
curve: 'basis',
useMaxWidth: true,
padding: 20
},
securityLevel: 'loose'
});
// Use both DOMContentLoaded and load events for better compatibility
function initMermaid() {
const mermaidBlocks = document.querySelectorAll(
'pre.language-mermaid code'
);
if (mermaidBlocks.length === 0) {
console.log('No mermaid blocks found');
return;
}
console.log(`Found ${mermaidBlocks.length} mermaid blocks, converting...`);
mermaidBlocks.forEach((block, index) => {
const mermaidDiv = document.createElement('div');
mermaidDiv.className = 'mermaid';
mermaidDiv.id = `mermaid-diagram-${index}`;
mermaidDiv.textContent = block.textContent;
block.parentElement.replaceWith(mermaidDiv);
});
setTimeout(() => {
mermaid
.run()
.then(() => {
console.log('Mermaid diagrams rendered successfully');
})
.catch((err) => {
console.error('Mermaid rendering error:', err);
});
}, 100);
}
// Try multiple event listeners to ensure it runs
if (document.readyState === 'loading') {
document.addEventListener('DOMContentLoaded', initMermaid);
} else {
// DOM already loaded
initMermaid();
}
window.addEventListener('load', () => {
// Failsafe: run again on window load if diagrams still missing
if (document.querySelectorAll('.mermaid svg').length === 0) {
initMermaid();
}
});
</script>

View File

@@ -0,0 +1,25 @@
primaryColor: '#ff69b4'
primaryTextColor: '#1e272e'
primaryBorderColor: '#ff1493'
lineColor: '#ff69b4'
secondaryColor: '#2f3542'
tertiaryColor: '#1e272e'
background: '#1e272e'
mainBkg: '#2f3542'
secondBkg: '#1e272e'
border1: '#ff69b4'
border2: '#ff1493'
note: '#2f3542'
noteBkgColor: '#2f3542'
noteTextColor: '#ffffff'
noteBorderColor: '#ff69b4'
arrowheadColor: '#ff69b4'
fontFamily: 'Inter system-ui -apple-system sans-serif'
fontSize: '16px'
darkMode: true
edgeLabelBackground: '#1e272e'
clusterBkg: '#2f3542'
clusterBorder: '#ff69b4'
defaultLinkColor: '#ff69b4'
titleColor: '#ffffff'
nodeTextColor: '#e8eaed'

View File

@@ -0,0 +1,28 @@
.mermaid {
width: 100%;
height: auto;
padding: 14px;
background-color: var(--code-block-background-color);
border-radius: var(--code-block-border-radius);
}
.version {
color: var(--quote-color);
}
.sidebar-crate h2 a {
color: var(--link-color);
}
a.doc-anchor {
color: var(--link-color-alternative);
}
pre {
font-size: 0.8em;
}
.docblock :not(pre) > code {
font-size: 0.8em;
padding: 1px 6px;
}

View File

@@ -0,0 +1,152 @@
main-background-color: "#2f3542"
main-color: "#e8eaed"
settings-input-color: "#e8eaed"
settings-input-border-color: "#57606f"
settings-button-color: "#ff69b4"
settings-button-border-focus: "#ff1493"
link-color: "#ff69b4"
code-highlight-kw-color: "#ff69b4"
code-highlight-kw-2-color: "#ff1493"
code-highlight-kw-3-color: "#ec407a"
sidebar-background-color: "#1e272e"
sidebar-background-color-hover: "#24272e"
sidebar-elems-color: "#e8eaed"
sidebar-border-color: "#57606f"
sidebar-link-color: "#e8eaed"
sidebar-current-link-background-color: rgba(255, 105, 180, 0.15)
search-input-focused-border-color: "#ff69b4"
search-results-alias-color: "#ff69b4"
search-results-grey-color: "#747d8c"
search-tab-title-count-color: "#ff69b4"
search-background-color: "#2f3542"
search-color: "#e8eaed"
search-error-code-background-color: "#1e272e"
search-results-border-color: "#57606f"
search-tab-button-not-selected-border-top-color: "#57606f"
search-tab-button-not-selected-background: "#24272e"
search-tab-button-selected-border-top-color: "#ff69b4"
search-tab-button-selected-background: "#2f3542"
search-result-link-focus-background-color: rgba(255, 105, 180, 0.1)
search-result-border-color: "#57606f"
search-input-border-color: "#57606f"
code-block-background-color: "#1e272e"
codeblock-error-hover-color: rgba(255, 105, 180, 0.1)
codeblock-error-color: rgba(255, 20, 147, 0.8)
codeblock-ignore-hover-color: "#ffffff"
codeblock-ignore-color: "#e8eaed"
codeblock-link-background: "#1e272e"
codeblock-link-hover-background: "#24272e"
scrollbar-track-background-color: "#1e272e"
scrollbar-thumb-background-color: "#ff69b4"
scrollbar-color: "#ff69b4 #1e272e"
heading-color: "#ff69b4"
title-color: "#ff69b4"
subtitle-color: "#ff1493"
headings-border-bottom-color: "#ff69b4"
border-color: "#57606f"
table-border-color: "#57606f"
table-head-background-color: "#1e272e"
button-background-color: "#1e272e"
button-border-color: "#57606f"
right-side-color: "#e8eaed"
tr-even-background-color: "#24272e"
tr-odd-background-color: "#2f3542"
table-alt-row-background-color: "#24272e"
quote-color: "#747d8c"
quote-border-color: "#ff69b4"
type-link-color: "#ff69b4"
trait-link-color: "#ff1493"
assoc-item-link-color: "#ec407a"
fn-link-color: "#ff69b4"
function-link-color: "#ff69b4"
macro-link-color: "#f06292"
keyword-link-color: "#ff69b4"
mod-link-color: "#ff1493"
docblock-table-border-color: "#57606f"
docblock-code-background-color: "#1e272e"
mobile-sidebar-menu-filter: none
mobile-sidebar-menu-hover-filter: invert(100%) brightness(1.2)
src-line-numbers-span-color: "#57606f"
src-line-number-highlighted-background-color: rgba(255, 105, 180, 0.1)
src-sidebar-background-selected: rgba(255, 105, 180, 0.15)
src-sidebar-background-hover: rgba(255, 105, 180, 0.1)
item-table-header-background-color: "#1e272e"
item-table-header-border-color: "#57606f"
scraped-example-title-color: "#ff69b4"
scraped-example-help-border-color: "#57606f"
scraped-example-help-color: "#e8eaed"
scraped-example-help-hover-border-color: "#ff69b4"
scraped-example-help-hover-color: "#ffffff"
scraped-example-code-line-highlight: rgba(255, 105, 180, 0.1)
scraped-example-code-line-highlight-focus: rgba(255, 105, 180, 0.2)
scrape-example-code-line-highlight: rgba(255, 105, 180, 0.1)
scrape-example-code-line-highlight-focus: rgba(255, 105, 180, 0.2)
scrape-example-help-border-color: "#57606f"
scrape-example-help-color: "#e8eaed"
scrape-example-help-hover-border-color: "#ff69b4"
scrape-example-help-hover-color: "#ffffff"
scrape-example-toggle-line-background: rgba(255, 105, 180, 0.05)
scrape-example-toggle-line-hover-background: rgba(255, 105, 180, 0.1)
scrape-example-code-wrapper-background-start: rgba(30, 39, 46, 0.9)
scrape-example-code-wrapper-background-end: rgba(30, 39, 46, 0.6)
toggle-filter: invert(100%) brightness(0.8)
toggle-plus-minus-background-color: rgba(255, 105, 180, 0.1)
notable-trait-tooltip-background-color: "#1e272e"
notable-trait-tooltip-border-color: "#ff69b4"
copy-button-color: "#e8eaed"
copy-button-background-color: "#1e272e"
copy-button-hover-color: "#ffffff"
copy-button-hover-background-color: "#24272e"
copy-button-border-color: "#57606f"
copy-path-button-color: "#e8eaed"
code-attribute-color: "#ff69b4"
code-highlight-lifetime-color: "#f06292"
code-highlight-prelude-color: "#ff69b4"
code-highlight-prelude-val-color: "#ff1493"
code-highlight-number-color: "#f06292"
code-highlight-string-color: "#ec407a"
code-highlight-literal-color: "#ff69b4"
code-highlight-attribute-color: "#ff69b4"
code-highlight-self-color: "#ff1493"
code-highlight-macro-color: "#f06292"
code-highlight-question-mark-color: "#ff69b4"
code-highlight-comment-color: "#747d8c"
code-highlight-doc-comment-color: "#747d8c"
stab-background-color: "#1e272e"
stab-code-color: "#e8eaed"
stab-unstable-background-color: rgba(255, 105, 180, 0.2)
stab-unstable-code-color: "#ff69b4"
stab-deprecated-background-color: rgba(255, 69, 0, 0.2)
stab-deprecated-code-color: "#ff4500"
stab-portability-background-color: rgba(116, 125, 140, 0.2)
stab-portability-code-color: "#747d8c"
kbd-color: "#e8eaed"
kbd-background: "#1e272e"
kbd-box-shadow-color: "#57606f"
rust-logo-filter: brightness(0) saturate(100%) invert(75%) sepia(100%) saturate(1000%) hue-rotate(290deg) brightness(100%) contrast(100%)
crate-search-div-filter: invert(100%)
crate-search-div-hover-filter: invert(100%)
crate-search-hover-border: "#ff69b4"
target-background-color: rgba(255, 105, 180, 0.1)
target-border-color: "#ff69b4"
code-example-button-color: "#e8eaed"
code-example-button-background-color: "#1e272e"
code-example-button-hover-background-color: "#24272e"
code-example-button-hover-color: "#ffffff"
settings-menu-filter: brightness(0.9) invert(65%) sepia(67%) saturate(3704%) hue-rotate(300deg) brightness(108%) contrast(101%)
settings-menu-hover-background-color: rgba(255, 105, 180, 0.1)
settings-menu-hover-filter: brightness(1.1) invert(65%) sepia(67%) saturate(3704%) hue-rotate(300deg) brightness(108%) contrast(101%)
help-border-color: "#57606f"
help-background-color: "#2f3542"
help-color: "#e8eaed"
warning-border-color: "#f59e0b"
warning-background-color: rgba(245, 158, 11, 0.1)
note-background-color: rgba(59, 130, 246, 0.1)
note-border-color: "#3b82f6"
toggles-color: "#e8eaed"
sidebar-resizer-hover: "#ff69b4"
sidebar-resizer-active: "#ff1493"
font-family: "'Inter', 'Fira Sans', Arial, sans-serif"
font-family-code: "'JetBrains Mono', 'Fira Mono', monospace"
link-color-alternative: "#6495ed"

846
css_color_filter.sh Executable file
View File

@@ -0,0 +1,846 @@
#!/usr/bin/env bash
#############################################################################
# CSS Color Filter Generator
#
# Generate CSS filter values to transform black elements into any target color
# No Node.js required - pure bash implementation
#
# Usage:
# css_color_filter.sh [COLOR]
# css_color_filter.sh -i # Interactive mode
# css_color_filter.sh --help # Show help
#
# Arguments:
# COLOR Hex color (e.g., #FF0000, ff0000) or RGB (e.g., 255,0,0)
#
# Options:
# -i, --interactive Interactive mode with colored preview
# -r, --raw Output only the CSS filter (for piping)
# -c, --copy Copy result to clipboard automatically
# -h, --help Show this help message
#
# Examples:
# css_color_filter.sh "#FF5733"
# css_color_filter.sh ff5733
# css_color_filter.sh "255,87,51"
# css_color_filter.sh -i
#
# Dependencies:
# bc For floating-point arithmetic
# jq For JSON formatting (optional)
#
#############################################################################
set -euo pipefail
# ============================================================================
# Color Definitions
# ============================================================================
RED="" GREEN="" YELLOW="" BLUE="" MAGENTA="" CYAN="" BOLD="" DIM="" RESET=""
COLORS=0
if [[ -t 1 ]] && command -v tput >/dev/null 2>&1; then
COLORS=$(tput colors 2>/dev/null || echo 0)
if [[ ${COLORS:-0} -ge 8 ]]; then
RED=$(tput setaf 1 2>/dev/null || echo "")
GREEN=$(tput setaf 2 2>/dev/null || echo "")
YELLOW=$(tput setaf 3 2>/dev/null || echo "")
BLUE=$(tput setaf 4 2>/dev/null || echo "")
MAGENTA=$(tput setaf 5 2>/dev/null || echo "")
CYAN=$(tput setaf 6 2>/dev/null || echo "")
BOLD=$(tput bold 2>/dev/null || echo "")
DIM=$(tput dim 2>/dev/null || echo "")
RESET=$(tput sgr0 2>/dev/null || echo "")
fi
fi
# ============================================================================
# Helper Functions
# ============================================================================
print_usage() {
cat << EOF
${BOLD}CSS Color Filter Generator${RESET}
Generate CSS filter values to transform black elements into any target color.
${BOLD}USAGE:${RESET}
$(basename "$0") [OPTIONS] [COLOR]
${BOLD}ARGUMENTS:${RESET}
COLOR Hex color (e.g., #FF0000, ff0000) or RGB (e.g., 255,0,0)
${BOLD}OPTIONS:${RESET}
-i, --interactive Interactive mode with colored preview
-r, --raw Output only the CSS filter (for piping)
-c, --copy Copy result to clipboard automatically
-h, --help Show this help message
${BOLD}EXAMPLES:${RESET}
$(basename "$0") "#FF5733"
$(basename "$0") ff5733
$(basename "$0") "255,87,51"
$(basename "$0") -i
${BOLD}NOTE:${RESET}
This tool generates filters that work on ${BOLD}black${RESET} elements.
To use with non-black elements, prepend: ${DIM}brightness(0) saturate(100%)${RESET}
${BOLD}ALGORITHM:${RESET}
Uses SPSA (Simultaneous Perturbation Stochastic Approximation) to find
optimal filter combinations that minimize color difference in RGB and HSL.
${BOLD}DEPENDENCIES:${RESET}
bc For floating-point arithmetic
jq For JSON formatting (optional)
EOF
}
error() {
echo "${RED}${BOLD}Error:${RESET} $1" >&2
exit 1
}
info() {
echo "${BLUE}${BOLD}==>${RESET} $1"
}
success() {
echo "${GREEN}${BOLD}[OK]${RESET} $1"
}
warning() {
echo "${YELLOW}${BOLD}[WARN]${RESET} $1"
}
check_dependencies() {
if ! command -v bc >/dev/null 2>&1; then
error "bc is required but not found. Please install bc (apt-get install bc)"
fi
}
# ============================================================================
# Math Utilities
# ============================================================================
bc_calc() {
local expr="$1"
expr=$(echo "$expr" | tr -d ' ')
if [[ -z "$expr" ]]; then
echo "0"
return
fi
echo "scale=10; $expr" | bc -l 2>/dev/null || echo "0"
}
bc_compare() {
local result=$(echo "$1" | bc -l 2>/dev/null || echo "0")
[[ "$result" == "1" ]]
}
abs_val() {
local val="$1"
if bc_compare "$val < 0"; then
echo "$(bc_calc "-1 * $val")"
else
echo "$val"
fi
}
min() {
local a=$1 b=$2
if bc_compare "$a < $b"; then
echo "$a"
else
echo "$b"
fi
}
max() {
local a=$1 b=$2
if bc_compare "$a > $b"; then
echo "$a"
else
echo "$b"
fi
}
clamp() {
local val=$1 min_val=$2 max_val=$3
val=$(max "$val" "$min_val")
val=$(min "$val" "$max_val")
echo "$val"
}
round() {
LC_NUMERIC=C printf "%.0f" "$1"
}
# Generate random number between 0 and 1
random_float() {
echo "scale=10; $RANDOM / 32767" | bc -l
}
# ============================================================================
# Color Validation
# ============================================================================
validate_hex() {
local hex="$1"
hex="${hex#\#}"
if [[ "$hex" =~ ^[0-9A-Fa-f]{3}$ ]] || [[ "$hex" =~ ^[0-9A-Fa-f]{6}$ ]]; then
return 0
fi
return 1
}
validate_rgb() {
local rgb="$1"
if [[ "$rgb" =~ ^[0-9]{1,3},[0-9]{1,3},[0-9]{1,3}$ ]]; then
IFS=',' read -r r g b <<< "$rgb"
if [[ $r -le 255 && $g -le 255 && $b -le 255 ]]; then
return 0
fi
fi
return 1
}
# ============================================================================
# Color Conversion
# ============================================================================
hex_to_rgb() {
local hex="$1"
hex="${hex#\#}"
if [[ ${#hex} -eq 3 ]]; then
hex="${hex:0:1}${hex:0:1}${hex:1:1}${hex:1:1}${hex:2:1}${hex:2:1}"
fi
local r=$((16#${hex:0:2}))
local g=$((16#${hex:2:2}))
local b=$((16#${hex:4:2}))
echo "$r $g $b"
}
rgb_to_hex() {
local r=$(round "$1")
local g=$(round "$2")
local b=$(round "$3")
r=$(clamp "$r" 0 255)
g=$(clamp "$g" 0 255)
b=$(clamp "$b" 0 255)
LC_NUMERIC=C printf "#%02X%02X%02X" "$r" "$g" "$b"
}
rgb_to_hsl() {
local r=$1 g=$2 b=$3
r=$(bc_calc "$r / 255")
g=$(bc_calc "$g / 255")
b=$(bc_calc "$b / 255")
local max=$(max "$r" "$(max "$g" "$b")")
local min=$(min "$r" "$(min "$g" "$b")")
local delta=$(bc_calc "$max - $min")
local h=0 s=0 l
l=$(bc_calc "($max + $min) / 2")
if bc_compare "$delta != 0"; then
if bc_compare "$l < 0.5"; then
s=$(bc_calc "$delta / ($max + $min)")
else
s=$(bc_calc "$delta / (2 - $max - $min)")
fi
if bc_compare "$max == $r"; then
h=$(bc_calc "(($g - $b) / $delta) + (if ($g < $b) then 6 else 0)")
elif bc_compare "$max == $g"; then
h=$(bc_calc "(($b - $r) / $delta) + 2")
else
h=$(bc_calc "(($r - $g) / $delta) + 4")
fi
h=$(bc_calc "$h / 6")
fi
h=$(bc_calc "$h * 100")
s=$(bc_calc "$s * 100")
l=$(bc_calc "$l * 100")
echo "$h $s $l"
}
# ============================================================================
# Color Class Implementation
# ============================================================================
# Global color state (r, g, b)
declare -a COLOR_STATE
color_set() {
local r=$(clamp "$1" 0 255)
local g=$(clamp "$2" 0 255)
local b=$(clamp "$3" 0 255)
COLOR_STATE=("$r" "$g" "$b")
}
color_get_rgb() {
echo "${COLOR_STATE[0]} ${COLOR_STATE[1]} ${COLOR_STATE[2]}"
}
# Matrix multiplication for color transformations
color_multiply() {
local r=${COLOR_STATE[0]}
local g=${COLOR_STATE[1]}
local b=${COLOR_STATE[2]}
# Matrix passed as arguments (9 values)
local m=("$@")
local new_r=$(bc_calc "$r * ${m[0]} + $g * ${m[1]} + $b * ${m[2]}")
local new_g=$(bc_calc "$r * ${m[3]} + $g * ${m[4]} + $b * ${m[5]}")
local new_b=$(bc_calc "$r * ${m[6]} + $g * ${m[7]} + $b * ${m[8]}")
new_r=$(clamp "$new_r" 0 255)
new_g=$(clamp "$new_g" 0 255)
new_b=$(clamp "$new_b" 0 255)
COLOR_STATE=("$new_r" "$new_g" "$new_b")
}
# CSS filter functions
color_invert() {
local value=${1:-1}
local r=${COLOR_STATE[0]}
local g=${COLOR_STATE[1]}
local b=${COLOR_STATE[2]}
r=$(bc_calc "($value + $r / 255 * (1 - 2 * $value)) * 255")
g=$(bc_calc "($value + $g / 255 * (1 - 2 * $value)) * 255")
b=$(bc_calc "($value + $b / 255 * (1 - 2 * $value)) * 255")
color_set "$r" "$g" "$b"
}
color_sepia() {
local value=${1:-1}
color_multiply \
"$(bc_calc "0.393 + 0.607 * (1 - $value)")" \
"$(bc_calc "0.769 - 0.769 * (1 - $value)")" \
"$(bc_calc "0.189 - 0.189 * (1 - $value)")" \
"$(bc_calc "0.349 - 0.349 * (1 - $value)")" \
"$(bc_calc "0.686 + 0.314 * (1 - $value)")" \
"$(bc_calc "0.168 - 0.168 * (1 - $value)")" \
"$(bc_calc "0.272 - 0.272 * (1 - $value)")" \
"$(bc_calc "0.534 - 0.534 * (1 - $value)")" \
"$(bc_calc "0.131 + 0.869 * (1 - $value)")"
}
color_saturate() {
local value=${1:-1}
color_multiply \
"$(bc_calc "0.213 + 0.787 * $value")" \
"$(bc_calc "0.715 - 0.715 * $value")" \
"$(bc_calc "0.072 - 0.072 * $value")" \
"$(bc_calc "0.213 - 0.213 * $value")" \
"$(bc_calc "0.715 + 0.285 * $value")" \
"$(bc_calc "0.072 - 0.072 * $value")" \
"$(bc_calc "0.213 - 0.213 * $value")" \
"$(bc_calc "0.715 - 0.715 * $value")" \
"$(bc_calc "0.072 + 0.928 * $value")"
}
color_hue_rotate() {
local angle=${1:-0}
angle=$(bc_calc "$angle / 180 * 3.14159265359")
local sin=$(echo "s($angle)" | bc -l)
local cos=$(echo "c($angle)" | bc -l)
color_multiply \
"$(bc_calc "0.213 + $cos * 0.787 - $sin * 0.213")" \
"$(bc_calc "0.715 - $cos * 0.715 - $sin * 0.715")" \
"$(bc_calc "0.072 - $cos * 0.072 + $sin * 0.928")" \
"$(bc_calc "0.213 - $cos * 0.213 + $sin * 0.143")" \
"$(bc_calc "0.715 + $cos * 0.285 + $sin * 0.140")" \
"$(bc_calc "0.072 - $cos * 0.072 - $sin * 0.283")" \
"$(bc_calc "0.213 - $cos * 0.213 - $sin * 0.787")" \
"$(bc_calc "0.715 - $cos * 0.715 + $sin * 0.715")" \
"$(bc_calc "0.072 + $cos * 0.928 + $sin * 0.072")"
}
color_brightness() {
local value=${1:-1}
local r=${COLOR_STATE[0]}
local g=${COLOR_STATE[1]}
local b=${COLOR_STATE[2]}
r=$(bc_calc "$r * $value")
g=$(bc_calc "$g * $value")
b=$(bc_calc "$b * $value")
color_set "$r" "$g" "$b"
}
color_contrast() {
local value=${1:-1}
local slope="$value"
local intercept=$(bc_calc "-(0.5 * $value) + 0.5")
local r=${COLOR_STATE[0]}
local g=${COLOR_STATE[1]}
local b=${COLOR_STATE[2]}
r=$(bc_calc "$r * $slope + $intercept * 255")
g=$(bc_calc "$g * $slope + $intercept * 255")
b=$(bc_calc "$b * $slope + $intercept * 255")
color_set "$r" "$g" "$b"
}
# ============================================================================
# Solver Implementation
# ============================================================================
# Target color
declare -a TARGET_RGB
declare -a TARGET_HSL
# Calculate loss between current color and target
calculate_loss() {
local -a filters=("$@")
# Reset to black
color_set 0 0 0
# Apply filters
color_invert "$(bc_calc "${filters[0]} / 100")"
color_sepia "$(bc_calc "${filters[1]} / 100")"
color_saturate "$(bc_calc "${filters[2]} / 100")"
color_hue_rotate "$(bc_calc "${filters[3]} * 3.6")"
color_brightness "$(bc_calc "${filters[4]} / 100")"
color_contrast "$(bc_calc "${filters[5]} / 100")"
# Get resulting color
read -r r g b <<< "$(color_get_rgb)"
read -r h s l <<< "$(rgb_to_hsl "$r" "$g" "$b")"
# Calculate color difference
local loss=0
loss=$(bc_calc "$loss + $(abs_val "$(bc_calc "$r - ${TARGET_RGB[0]}")")")
loss=$(bc_calc "$loss + $(abs_val "$(bc_calc "$g - ${TARGET_RGB[1]}")")")
loss=$(bc_calc "$loss + $(abs_val "$(bc_calc "$b - ${TARGET_RGB[2]}")")")
loss=$(bc_calc "$loss + $(abs_val "$(bc_calc "$h - ${TARGET_HSL[0]}")")")
loss=$(bc_calc "$loss + $(abs_val "$(bc_calc "$s - ${TARGET_HSL[1]}")")")
loss=$(bc_calc "$loss + $(abs_val "$(bc_calc "$l - ${TARGET_HSL[2]}")")")
echo "$loss"
}
# Fix filter values to valid ranges
fix_filter_value() {
local value=$1
local idx=$2
local max=100
if [[ $idx -eq 2 ]]; then
max=7500 # saturate
elif [[ $idx -eq 4 ]] || [[ $idx -eq 5 ]]; then
max=200 # brightness, contrast
fi
if [[ $idx -eq 3 ]]; then
# hue-rotate: wrap around
while bc_compare "$value > $max"; do
value=$(bc_calc "$value - $max")
done
while bc_compare "$value < 0"; do
value=$(bc_calc "$value + $max")
done
else
value=$(clamp "$value" 0 "$max")
fi
echo "$value"
}
# SPSA optimization
spsa() {
local A=$1
local c=$2
shift 2
local -a a=("$1" "$2" "$3" "$4" "$5" "$6")
shift 6
local -a values=("$1" "$2" "$3" "$4" "$5" "$6")
shift 6
local iters=$1
local alpha=1
local gamma=0.16666666666666666
local -a best=("${values[@]}")
local best_loss=999999
for ((k=0; k<iters; k++)); do
local ck=$(bc_calc "$c / ($k + 1) ^ $gamma")
local -a deltas
local -a high_args
local -a low_args
for i in {0..5}; do
# Random delta: 1 or -1
if [[ $((RANDOM % 2)) -eq 0 ]]; then
deltas[$i]=1
else
deltas[$i]=-1
fi
high_args[$i]=$(bc_calc "${values[$i]} + $ck * ${deltas[$i]}")
low_args[$i]=$(bc_calc "${values[$i]} - $ck * ${deltas[$i]}")
done
local loss_high=$(calculate_loss "${high_args[@]}")
local loss_low=$(calculate_loss "${low_args[@]}")
local loss_diff=$(bc_calc "$loss_high - $loss_low")
for i in {0..5}; do
local g=$(bc_calc "$loss_diff / (2 * $ck) * ${deltas[$i]}")
local ak=$(bc_calc "${a[$i]} / ($A + $k + 1) ^ $alpha")
values[$i]=$(bc_calc "${values[$i]} - $ak * $g")
values[$i]=$(fix_filter_value "${values[$i]}" "$i")
done
local loss=$(calculate_loss "${values[@]}")
if bc_compare "$loss < $best_loss"; then
best=("${values[@]}")
best_loss="$loss"
fi
done
echo "${best[@]} $best_loss"
}
# Solve wide search
solve_wide() {
local A=5
local c=15
local -a a=(60 180 18000 600 1.2 1.2)
local -a best_values
local best_loss=999999
for ((i=0; i<3; i++)); do
local -a initial=(50 20 3750 50 100 100)
read -r -a result <<< "$(spsa $A $c "${a[@]}" "${initial[@]}" 1000)"
# Last element is loss
local loss="${result[6]}"
if bc_compare "$loss < $best_loss"; then
best_values=("${result[@]:0:6}")
best_loss="$loss"
fi
# Break if good enough
if bc_compare "$best_loss <= 25"; then
break
fi
done
echo "${best_values[@]} $best_loss"
}
# Solve narrow search
solve_narrow() {
local -a wide=("$@")
local wide_loss="${wide[6]}"
local A="$wide_loss"
local c=2
local A1=$(bc_calc "$A + 1")
local -a a
a[0]=$(bc_calc "0.25 * $A1")
a[1]=$(bc_calc "0.25 * $A1")
a[2]="$A1"
a[3]=$(bc_calc "0.25 * $A1")
a[4]=$(bc_calc "0.2 * $A1")
a[5]=$(bc_calc "0.2 * $A1")
local -a values=("${wide[@]:0:6}")
spsa "$A" "$c" "${a[@]}" "${values[@]}" 500
}
# Main solve function
solve_filters() {
local target_hex="$1"
# Set target
read -r r g b <<< "$(hex_to_rgb "$target_hex")"
TARGET_RGB=("$r" "$g" "$b")
read -r h s l <<< "$(rgb_to_hsl "$r" "$g" "$b")"
TARGET_HSL=("$h" "$s" "$l")
# Solve
local -a wide
read -r -a wide <<< "$(solve_wide)"
local -a narrow
read -r -a narrow <<< "$(solve_narrow "${wide[@]}")"
# Format output
local -a values=("${narrow[@]:0:6}")
local loss="${narrow[6]}"
# Generate CSS
local filter="invert($(round "${values[0]}")%)"
filter="$filter sepia($(round "${values[1]}")%)"
filter="$filter saturate($(round "${values[2]}")%)"
filter="$filter hue-rotate($(round "$(bc_calc "${values[3]} * 3.6")")deg)"
filter="$filter brightness($(round "${values[4]}")%)"
filter="$filter contrast($(round "${values[5]}")%)"
echo "$filter|$loss"
}
# ============================================================================
# Display Functions
# ============================================================================
draw_color_block() {
local hex="$1"
local label="$2"
read -r r g b <<< "$(hex_to_rgb "$hex")"
if [[ ${COLORS:-0} -ge 256 ]]; then
local bg_color="\033[48;2;${r};${g};${b}m"
local reset="\033[0m"
echo -e "${BOLD}${label}${RESET}"
echo -e "${bg_color} ${reset}"
echo -e "${bg_color} ${reset}"
echo -e "${bg_color} ${reset}"
echo ""
fi
}
copy_to_clipboard() {
local text="$1"
if command -v xclip >/dev/null 2>&1; then
echo -n "$text" | xclip -selection clipboard
return 0
elif command -v xsel >/dev/null 2>&1; then
echo -n "$text" | xsel --clipboard
return 0
elif command -v wl-copy >/dev/null 2>&1; then
echo -n "$text" | wl-copy
return 0
elif command -v pbcopy >/dev/null 2>&1; then
echo -n "$text" | pbcopy
return 0
fi
return 1
}
display_result() {
local hex="$1"
local filter="$2"
local loss="$3"
local raw_mode="${4:-false}"
if [[ "$raw_mode" == "true" ]]; then
echo "filter: $filter;"
return
fi
echo ""
echo "${BOLD}================================================================${RESET}"
echo "${BOLD} CSS Color Filter Generator ${RESET}"
echo "${BOLD}================================================================${RESET}"
echo ""
read -r r g b <<< "$(hex_to_rgb "$hex")"
echo "${BOLD}Target Color:${RESET}"
echo " Hex: ${CYAN}${hex}${RESET}"
echo " RGB: ${CYAN}rgb($r, $g, $b)${RESET}"
echo ""
if [[ ${COLORS:-0} -ge 256 ]]; then
draw_color_block "$hex" "Preview:"
fi
echo "${BOLD}Generated CSS Filter:${RESET}"
echo "${GREEN}filter: ${filter};${RESET}"
echo ""
local loss_float=$(LC_NUMERIC=C printf "%.1f" "$loss")
echo "${BOLD}Accuracy:${RESET}"
echo -n " Loss: ${YELLOW}${loss_float}${RESET} "
if bc_compare "$loss < 1"; then
echo "${GREEN}(Perfect match!)${RESET}"
elif bc_compare "$loss < 5"; then
echo "${GREEN}(Excellent match)${RESET}"
elif bc_compare "$loss < 15"; then
echo "${YELLOW}(Good match - consider re-running)${RESET}"
else
echo "${RED}(Poor match - try running again)${RESET}"
fi
echo ""
echo "${BOLD}----------------------------------------------------------------${RESET}"
echo "${DIM}Note: This filter works on black elements. For non-black elements,"
echo " prepend: brightness(0) saturate(100%)${RESET}"
echo "${BOLD}----------------------------------------------------------------${RESET}"
echo ""
}
# ============================================================================
# Interactive Mode
# ============================================================================
interactive_mode() {
echo ""
echo "${BOLD}${BLUE}+================================================================+${RESET}"
echo "${BOLD}${BLUE}| CSS Color Filter Generator (Interactive) |${RESET}"
echo "${BOLD}${BLUE}+================================================================+${RESET}"
echo ""
while true; do
echo -n "${BOLD}Enter a color${RESET} ${DIM}(hex or rgb, or 'q' to quit):${RESET} "
read -r color_input
if [[ "$color_input" =~ ^[qQ]$ ]]; then
echo ""
success "Goodbye!"
exit 0
fi
if [[ -z "$color_input" ]]; then
continue
fi
process_color "$color_input" "false" "true"
echo ""
echo -n "${DIM}Press Enter to continue...${RESET}"
read -r
echo ""
done
}
# ============================================================================
# Color Processing
# ============================================================================
process_color() {
local color_input="$1"
local raw_mode="${2:-false}"
local auto_copy="${3:-false}"
local r g b hex
if validate_hex "$color_input"; then
hex="$color_input"
[[ "$hex" != \#* ]] && hex="#$hex"
# Expand shorthand
hex="${hex#\#}"
if [[ ${#hex} -eq 3 ]]; then
hex="${hex:0:1}${hex:0:1}${hex:1:1}${hex:1:1}${hex:2:1}${hex:2:1}"
fi
hex="#${hex^^}"
elif validate_rgb "$color_input"; then
IFS=',' read -r r g b <<< "$color_input"
hex=$(rgb_to_hex "$r" "$g" "$b")
else
error "Invalid color format. Use hex (e.g., #FF0000) or RGB (e.g., 255,0,0)"
fi
if [[ "$raw_mode" != "true" ]]; then
info "Calculating optimal CSS filter for $hex..."
echo ""
fi
local result
result=$(solve_filters "$hex")
IFS='|' read -r filter loss <<< "$result"
display_result "$hex" "$filter" "$loss" "$raw_mode"
if [[ "$auto_copy" == "true" ]]; then
if copy_to_clipboard "filter: $filter;"; then
success "CSS filter copied to clipboard!"
else
warning "Could not copy to clipboard (install xclip, xsel, wl-copy, or pbcopy)"
fi
fi
}
# ============================================================================
# Main Script Logic
# ============================================================================
main() {
check_dependencies
local color_input=""
local interactive=false
local raw_mode=false
local auto_copy=false
while [[ $# -gt 0 ]]; do
case "$1" in
-h|--help)
print_usage
exit 0
;;
-i|--interactive)
interactive=true
shift
;;
-r|--raw)
raw_mode=true
shift
;;
-c|--copy)
auto_copy=true
shift
;;
-*)
error "Unknown option: $1. Use --help for usage information."
;;
*)
color_input="$1"
shift
;;
esac
done
if [[ "$interactive" == "true" ]]; then
interactive_mode
exit 0
fi
if [[ -z "$color_input" ]]; then
error "No color specified. Use --help for usage information."
fi
process_color "$color_input" "$raw_mode" "$auto_copy"
}
main "$@"

980
css_color_palette.sh Executable file
View File

@@ -0,0 +1,980 @@
#!/usr/bin/env bash
#############################################################################
# CSS Color Palette Generator (Pure Bash)
#
# Generate comprehensive color palettes with tints, shades, and tones
# No Node.js required - pure bash implementation
#
# Usage:
# css_color_palette.sh <COLOR> [OPTIONS]
#
# Arguments:
# COLOR Base hex color (e.g., #3498db, 3498db)
#
# Options:
# -p, --palette TYPE Palette type: monochromatic, analogous, complementary,
# split-complementary, triadic, tetradic (default: monochromatic)
# -o, --output FILE Output file (default: ./colors.yaml)
# -m, --mode MODE Color mode: light, dark (default: light)
# -s, --style STYLE Generate style variations: shades, tints, tones, all
# -n, --name NAME Color palette name (default: auto-generated)
# --scales N Number of scale steps (default: 11)
# -i, --interactive Interactive mode
# -v, --verbose Verbose output with color preview
# -h, --help Show this help message
#
#############################################################################
set -euo pipefail
# ============================================================================
# Color Definitions
# ============================================================================
RED="" GREEN="" YELLOW="" BLUE="" MAGENTA="" CYAN="" BOLD="" DIM="" RESET=""
COLORS=0
if [[ -t 1 ]] && command -v tput >/dev/null 2>&1; then
COLORS=$(tput colors 2>/dev/null || echo 0)
if [[ ${COLORS:-0} -ge 8 ]]; then
RED=$(tput setaf 1 2>/dev/null || echo "")
GREEN=$(tput setaf 2 2>/dev/null || echo "")
YELLOW=$(tput setaf 3 2>/dev/null || echo "")
BLUE=$(tput setaf 4 2>/dev/null || echo "")
MAGENTA=$(tput setaf 5 2>/dev/null || echo "")
CYAN=$(tput setaf 6 2>/dev/null || echo "")
BOLD=$(tput bold 2>/dev/null || echo "")
DIM=$(tput dim 2>/dev/null || echo "")
RESET=$(tput sgr0 2>/dev/null || echo "")
fi
fi
# ============================================================================
# Global Variables
# ============================================================================
BASE_COLOR=""
PALETTE_TYPE="monochromatic"
OUTPUT_FILE="./colors.yaml"
COLOR_MODE="light"
STYLE_TYPE="all"
PALETTE_NAME=""
SCALE_STEPS=11
INTERACTIVE=false
VERBOSE=false
# Associative arrays for storing palette data
declare -A PALETTE_DATA
declare -a COLOR_GROUPS
# ============================================================================
# Helper Functions
# ============================================================================
print_usage() {
cat << EOF
${BOLD}CSS Color Palette Generator (Pure Bash)${RESET}
Generate comprehensive color palettes without Node.js dependencies.
${BOLD}USAGE:${RESET}
$(basename "$0") COLOR [OPTIONS]
${BOLD}ARGUMENTS:${RESET}
COLOR Base hex color (e.g., #3498db, 3498db)
${BOLD}OPTIONS:${RESET}
-p, --palette TYPE Palette type: monochromatic, analogous, complementary,
split-complementary, triadic, tetradic
-o, --output FILE Output file (default: ./colors.yaml)
-m, --mode MODE Color mode: light, dark (default: light)
-s, --style STYLE Style: shades, tints, tones, all (default: all)
-n, --name NAME Palette name (default: auto-generated)
--scales N Number of scale steps (default: 11)
-i, --interactive Interactive mode
-v, --verbose Verbose output with color preview
-h, --help Show this help message
${BOLD}DEPENDENCIES:${RESET}
bc For floating-point arithmetic
${BOLD}EXAMPLES:${RESET}
$(basename "$0") "#3498db"
$(basename "$0") "#3498db" -p triadic -o palette.json
$(basename "$0") "ff5733" -p analogous -m dark
EOF
}
error() {
echo "${RED}${BOLD}Error:${RESET} $1" >&2
exit 1
}
info() {
echo "${BLUE}${BOLD}==>${RESET} $1"
}
success() {
echo "${GREEN}${BOLD}[OK]${RESET} $1"
}
warning() {
echo "${YELLOW}${BOLD}[WARN]${RESET} $1"
}
# Check for bc dependency
check_dependencies() {
if ! command -v bc >/dev/null 2>&1; then
error "bc is required but not found. Please install bc (apt-get install bc)"
fi
}
# ============================================================================
# Math Utilities
# ============================================================================
# Floating point comparison
bc_calc() {
local expr="$1"
# Remove any leading/trailing whitespace
expr=$(echo "$expr" | tr -d ' ')
# Check if expression is empty
if [[ -z "$expr" ]]; then
echo "0"
return
fi
echo "scale=6; $expr" | bc -l 2>/dev/null || echo "0"
}
# Boolean bc comparison (returns 0 for true, 1 for false)
bc_compare() {
local result=$(echo "$1" | bc -l 2>/dev/null || echo "0")
[[ "$result" == "1" ]]
}
min() {
local a=$1 b=$2
if bc_compare "$a < $b"; then
echo "$a"
else
echo "$b"
fi
}
max() {
local a=$1 b=$2
if bc_compare "$a > $b"; then
echo "$a"
else
echo "$b"
fi
}
clamp() {
local val=$1 min_val=$2 max_val=$3
val=$(max "$val" "$min_val")
val=$(min "$val" "$max_val")
echo "$val"
}
round() {
LC_NUMERIC=C printf "%.0f" "$1"
}
# ============================================================================
# Color Validation and Normalization
# ============================================================================
validate_hex() {
local hex="$1"
hex="${hex#\#}"
if [[ "$hex" =~ ^[0-9A-Fa-f]{3}$ ]] || [[ "$hex" =~ ^[0-9A-Fa-f]{6}$ ]]; then
return 0
fi
return 1
}
normalize_hex() {
local hex="$1"
hex="${hex#\#}"
# Expand shorthand
if [[ ${#hex} -eq 3 ]]; then
hex="${hex:0:1}${hex:0:1}${hex:1:1}${hex:1:1}${hex:2:1}${hex:2:1}"
fi
echo "#${hex^^}"
}
# ============================================================================
# Color Conversion Functions
# ============================================================================
# Convert hex to RGB (returns "r g b")
hex_to_rgb() {
local hex="$1"
hex="${hex#\#}"
if [[ ${#hex} -eq 3 ]]; then
hex="${hex:0:1}${hex:0:1}${hex:1:1}${hex:1:1}${hex:2:1}${hex:2:1}"
fi
local r=$((16#${hex:0:2}))
local g=$((16#${hex:2:2}))
local b=$((16#${hex:4:2}))
echo "$r $g $b"
}
# Convert RGB to hex
rgb_to_hex() {
local r=$(round "$1")
local g=$(round "$2")
local b=$(round "$3")
# Clamp values
r=$(clamp "$r" 0 255)
g=$(clamp "$g" 0 255)
b=$(clamp "$b" 0 255)
# Use C locale to ensure proper number formatting
LC_NUMERIC=C printf "#%02X%02X%02X" "$r" "$g" "$b"
}
# Convert RGB to HSL (returns "h s l")
rgb_to_hsl() {
local r=$1 g=$2 b=$3
# Normalize to 0-1
r=$(bc_calc "$r / 255")
g=$(bc_calc "$g / 255")
b=$(bc_calc "$b / 255")
local max=$(max "$r" "$(max "$g" "$b")")
local min=$(min "$r" "$(min "$g" "$b")")
local delta=$(bc_calc "$max - $min")
local h=0 s=0 l
l=$(bc_calc "($max + $min) / 2")
if bc_compare "$delta != 0"; then
# Calculate saturation
if bc_compare "$l < 0.5"; then
s=$(bc_calc "$delta / ($max + $min)")
else
s=$(bc_calc "$delta / (2 - $max - $min)")
fi
# Calculate hue
if bc_compare "$max == $r"; then
h=$(bc_calc "(($g - $b) / $delta) + (if ($g < $b) then 6 else 0)")
elif bc_compare "$max == $g"; then
h=$(bc_calc "(($b - $r) / $delta) + 2")
else
h=$(bc_calc "(($r - $g) / $delta) + 4")
fi
h=$(bc_calc "$h / 6")
fi
# Convert to degrees and percentages
h=$(bc_calc "$h * 360")
s=$(bc_calc "$s * 100")
l=$(bc_calc "$l * 100")
echo "$h $s $l"
}
# Helper for HSL to RGB conversion
hue_to_rgb() {
local p=$1 q=$2 t=$3
# Normalize t to 0-1
if bc_compare "$t < 0"; then
t=$(bc_calc "$t + 1")
fi
if bc_compare "$t > 1"; then
t=$(bc_calc "$t - 1")
fi
if bc_compare "$t < 0.166666"; then
echo "$(bc_calc "$p + ($q - $p) * 6 * $t")"
elif bc_compare "$t < 0.5"; then
echo "$q"
elif bc_compare "$t < 0.666666"; then
echo "$(bc_calc "$p + ($q - $p) * (0.666666 - $t) * 6")"
else
echo "$p"
fi
}
# Convert HSL to RGB (returns "r g b")
hsl_to_rgb() {
local h=$1 s=$2 l=$3
# Normalize
h=$(bc_calc "$h / 360")
s=$(bc_calc "$s / 100")
l=$(bc_calc "$l / 100")
local r g b
if bc_compare "$s == 0"; then
# Achromatic (gray)
r=$l
g=$l
b=$l
else
local q
if bc_compare "$l < 0.5"; then
q=$(bc_calc "$l * (1 + $s)")
else
q=$(bc_calc "$l + $s - $l * $s")
fi
local p=$(bc_calc "2 * $l - $q")
r=$(hue_to_rgb "$p" "$q" "$(bc_calc "$h + 0.333333")")
g=$(hue_to_rgb "$p" "$q" "$h")
b=$(hue_to_rgb "$p" "$q" "$(bc_calc "$h - 0.333333")")
fi
# Convert to 0-255
r=$(bc_calc "$r * 255")
g=$(bc_calc "$g * 255")
b=$(bc_calc "$b * 255")
echo "$r $g $b"
}
# ============================================================================
# Color Manipulation Functions
# ============================================================================
# Adjust hue (degrees)
adjust_hue() {
local h=$1 adjustment=$2
h=$(bc_calc "$h + $adjustment")
# Normalize to 0-360
while bc_compare "$h < 0"; do
h=$(bc_calc "$h + 360")
done
while bc_compare "$h >= 360"; do
h=$(bc_calc "$h - 360")
done
echo "$h"
}
# Generate tint (mix with white)
generate_tint() {
local hex="$1"
local percentage=$2
read -r r g b <<< "$(hex_to_rgb "$hex")"
r=$(bc_calc "$r + (255 - $r) * ($percentage / 100)")
g=$(bc_calc "$g + (255 - $g) * ($percentage / 100)")
b=$(bc_calc "$b + (255 - $b) * ($percentage / 100)")
rgb_to_hex "$r" "$g" "$b"
}
# Generate shade (mix with black)
generate_shade() {
local hex="$1"
local percentage=$2
read -r r g b <<< "$(hex_to_rgb "$hex")"
r=$(bc_calc "$r * (1 - $percentage / 100)")
g=$(bc_calc "$g * (1 - $percentage / 100)")
b=$(bc_calc "$b * (1 - $percentage / 100)")
rgb_to_hex "$r" "$g" "$b"
}
# Generate tone (mix with gray)
generate_tone() {
local hex="$1"
local percentage=$2
read -r r g b <<< "$(hex_to_rgb "$hex")"
local gray=$(bc_calc "($r + $g + $b) / 3")
r=$(bc_calc "$r + ($gray - $r) * ($percentage / 100)")
g=$(bc_calc "$g + ($gray - $g) * ($percentage / 100)")
b=$(bc_calc "$b + ($gray - $b) * ($percentage / 100)")
rgb_to_hex "$r" "$g" "$b"
}
# Adjust lightness
adjust_lightness() {
local hex="$1"
local adjustment=$2
read -r r g b <<< "$(hex_to_rgb "$hex")"
read -r h s l <<< "$(rgb_to_hsl "$r" "$g" "$b")"
l=$(bc_calc "$l + $adjustment")
l=$(clamp "$l" 0 100)
read -r r g b <<< "$(hsl_to_rgb "$h" "$s" "$l")"
rgb_to_hex "$r" "$g" "$b"
}
# Adjust saturation
adjust_saturation() {
local hex="$1"
local adjustment=$2
read -r r g b <<< "$(hex_to_rgb "$hex")"
read -r h s l <<< "$(rgb_to_hsl "$r" "$g" "$b")"
s=$(bc_calc "$s + $adjustment")
s=$(clamp "$s" 0 100)
read -r r g b <<< "$(hsl_to_rgb "$h" "$s" "$l")"
rgb_to_hex "$r" "$g" "$b"
}
# ============================================================================
# Scale Generation
# ============================================================================
generate_color_scale() {
local base_hex="$1"
local group_name="$2"
local style="$3"
local -a scale_values=(50 100 200 300 400 500 600 700 800 900 950)
local base_index=5 # 500 is the base
# Set base color
PALETTE_DATA["${group_name}.500"]="$base_hex"
# Generate lighter variations (50-400)
for i in {4..0}; do
local step=$((base_index - i))
local scale_val=${scale_values[$i]}
local color
if [[ "$style" == "tints" ]]; then
local percentage=$(bc_calc "$step / $base_index * 85")
color=$(generate_tint "$base_hex" "$percentage")
elif [[ "$style" == "tones" ]]; then
read -r r g b <<< "$(hex_to_rgb "$base_hex")"
read -r h s l <<< "$(rgb_to_hsl "$r" "$g" "$b")"
local new_l=$(bc_calc "95 - $i * 8")
local new_s=$(bc_calc "$(max 10 "$(bc_calc "$s - ($base_index - $i) * 5")")")
read -r r g b <<< "$(hsl_to_rgb "$h" "$new_s" "$new_l")"
color=$(rgb_to_hex "$r" "$g" "$b")
else
# Default: lighten
local adjustment=$(bc_calc "$step * 12")
color=$(adjust_lightness "$base_hex" "$adjustment")
if [[ $i -le 2 ]]; then
local sat_adj=$(bc_calc "-($base_index - $i) * 8")
color=$(adjust_saturation "$color" "$sat_adj")
fi
fi
PALETTE_DATA["${group_name}.${scale_val}"]="$color"
done
# Generate darker variations (600-950)
for i in {6..10}; do
local step=$((i - base_index))
local scale_val=${scale_values[$i]}
local color
if [[ "$style" == "shades" ]]; then
local percentage=$(bc_calc "$step / (${#scale_values[@]} - $base_index - 1) * 75")
color=$(generate_shade "$base_hex" "$percentage")
elif [[ "$style" == "tones" ]]; then
read -r r g b <<< "$(hex_to_rgb "$base_hex")"
read -r h s l <<< "$(rgb_to_hsl "$r" "$g" "$b")"
local new_l=$(bc_calc "45 - ($i - $base_index) * 7")
new_l=$(max 5 "$new_l")
local new_s=$(bc_calc "$(max 10 "$(bc_calc "$s - $step * 3")")")
read -r r g b <<< "$(hsl_to_rgb "$h" "$new_s" "$new_l")"
color=$(rgb_to_hex "$r" "$g" "$b")
else
# Default: darken
local adjustment=$(bc_calc "-$step * 10")
color=$(adjust_lightness "$base_hex" "$adjustment")
if [[ $i -ge 9 ]]; then
local sat_adj=$(bc_calc "-$step * 5")
color=$(adjust_saturation "$color" "$sat_adj")
fi
fi
PALETTE_DATA["${group_name}.${scale_val}"]="$color"
done
}
# ============================================================================
# Palette Generation
# ============================================================================
generate_palette() {
local base_hex="$1"
local palette_type="$2"
local style="$3"
read -r r g b <<< "$(hex_to_rgb "$base_hex")"
read -r h s l <<< "$(rgb_to_hsl "$r" "$g" "$b")"
case "$palette_type" in
monochromatic)
COLOR_GROUPS=("primary")
generate_color_scale "$base_hex" "primary" "$style"
;;
analogous)
COLOR_GROUPS=("primary" "analogous1" "analogous2")
generate_color_scale "$base_hex" "primary" "$style"
# Analogous 1: -30 degrees
local h1=$(adjust_hue "$h" -30)
read -r r g b <<< "$(hsl_to_rgb "$h1" "$s" "$l")"
local color1=$(rgb_to_hex "$r" "$g" "$b")
generate_color_scale "$color1" "analogous1" "$style"
# Analogous 2: +30 degrees
local h2=$(adjust_hue "$h" 30)
read -r r g b <<< "$(hsl_to_rgb "$h2" "$s" "$l")"
local color2=$(rgb_to_hex "$r" "$g" "$b")
generate_color_scale "$color2" "analogous2" "$style"
;;
complementary)
COLOR_GROUPS=("primary" "complement")
generate_color_scale "$base_hex" "primary" "$style"
# Complement: 180 degrees
local hc=$(adjust_hue "$h" 180)
read -r r g b <<< "$(hsl_to_rgb "$hc" "$s" "$l")"
local colorc=$(rgb_to_hex "$r" "$g" "$b")
generate_color_scale "$colorc" "complement" "$style"
;;
split-complementary)
COLOR_GROUPS=("primary" "split1" "split2")
generate_color_scale "$base_hex" "primary" "$style"
# Split 1: 150 degrees
local hs1=$(adjust_hue "$h" 150)
read -r r g b <<< "$(hsl_to_rgb "$hs1" "$s" "$l")"
local colors1=$(rgb_to_hex "$r" "$g" "$b")
generate_color_scale "$colors1" "split1" "$style"
# Split 2: 210 degrees
local hs2=$(adjust_hue "$h" 210)
read -r r g b <<< "$(hsl_to_rgb "$hs2" "$s" "$l")"
local colors2=$(rgb_to_hex "$r" "$g" "$b")
generate_color_scale "$colors2" "split2" "$style"
;;
triadic)
COLOR_GROUPS=("primary" "triadic1" "triadic2")
generate_color_scale "$base_hex" "primary" "$style"
# Triadic 1: 120 degrees
local ht1=$(adjust_hue "$h" 120)
read -r r g b <<< "$(hsl_to_rgb "$ht1" "$s" "$l")"
local colort1=$(rgb_to_hex "$r" "$g" "$b")
generate_color_scale "$colort1" "triadic1" "$style"
# Triadic 2: 240 degrees
local ht2=$(adjust_hue "$h" 240)
read -r r g b <<< "$(hsl_to_rgb "$ht2" "$s" "$l")"
local colort2=$(rgb_to_hex "$r" "$g" "$b")
generate_color_scale "$colort2" "triadic2" "$style"
;;
tetradic)
COLOR_GROUPS=("primary" "tetradic1" "tetradic2" "tetradic3")
generate_color_scale "$base_hex" "primary" "$style"
# Tetradic colors: 90, 180, 270 degrees
for deg in 90 180 270; do
local idx=$((deg / 90))
local hn=$(adjust_hue "$h" "$deg")
read -r r g b <<< "$(hsl_to_rgb "$hn" "$s" "$l")"
local colorn=$(rgb_to_hex "$r" "$g" "$b")
generate_color_scale "$colorn" "tetradic${idx}" "$style"
done
;;
esac
}
# ============================================================================
# Output Functions
# ============================================================================
generate_yaml_output() {
local name="$1"
local type="$2"
local mode="$3"
local style="$4"
local base="$5"
cat << EOF
name: '$name'
type: '$type'
mode: '$mode'
style: '$style'
base: '$base'
colors:
EOF
for group in "${COLOR_GROUPS[@]}"; do
echo " ${group}:"
for scale in 50 100 200 300 400 500 600 700 800 900 950; do
local key="${group}.${scale}"
if [[ -n "${PALETTE_DATA[$key]:-}" ]]; then
LC_NUMERIC=C printf " %s: '%s'\n" "$scale" "${PALETTE_DATA[$key]}"
fi
done
done
cat << EOF
metadata:
generated: '$(date -u +"%Y-%m-%dT%H:%M:%SZ")'
generator: 'css_color_palette.sh'
version: '1.0.0'
EOF
}
generate_json_output() {
local name="$1"
local type="$2"
local mode="$3"
local style="$4"
local base="$5"
echo "{"
echo " \"name\": \"$name\","
echo " \"type\": \"$type\","
echo " \"mode\": \"$mode\","
echo " \"style\": \"$style\","
echo " \"base\": \"$base\","
echo " \"colors\": {"
local group_count=0
for group in "${COLOR_GROUPS[@]}"; do
((group_count++))
echo " \"${group}\": {"
local scale_count=0
for scale in 50 100 200 300 400 500 600 700 800 900 950; do
local key="${group}.${scale}"
if [[ -n "${PALETTE_DATA[$key]:-}" ]]; then
((scale_count++))
if [[ $scale_count -lt 11 ]]; then
echo " \"$scale\": \"${PALETTE_DATA[$key]}\","
else
echo " \"$scale\": \"${PALETTE_DATA[$key]}\""
fi
fi
done
if [[ $group_count -lt ${#COLOR_GROUPS[@]} ]]; then
echo " },"
else
echo " }"
fi
done
echo " },"
echo " \"metadata\": {"
echo " \"generated\": \"$(date -u +"%Y-%m-%dT%H:%M:%SZ")\","
echo " \"generator\": \"css_color_palette_bash.sh\","
echo " \"version\": \"1.0.0\""
echo " }"
echo "}"
}
# ============================================================================
# Display Functions
# ============================================================================
draw_color_swatch() {
local hex="$1"
local label="$2"
local hex_clean="${hex#\#}"
read -r r g b <<< "$(hex_to_rgb "$hex")"
if [[ ${COLORS:-0} -ge 256 ]]; then
local bg="\033[48;2;${r};${g};${b}m"
local fg="\033[38;2;${r};${g};${b}m"
local reset="\033[0m"
# Determine text color based on luminance
local luminance=$(bc_calc "(0.299*$r + 0.587*$g + 0.114*$b)/255")
local text_color
if bc_compare "$luminance > 0.5"; then
text_color="\033[38;2;0;0;0m"
else
text_color="\033[38;2;255;255;255m"
fi
LC_NUMERIC=C printf "${bg}${text_color} %-20s ${reset} ${fg}%s${reset} %s\n" "$label" "■" "$hex"
else
LC_NUMERIC=C printf " %-20s %s\n" "$label" "$hex"
fi
}
display_palette_preview() {
if [[ "$VERBOSE" != "true" ]]; then
return
fi
echo ""
echo "${BOLD}================================================================${RESET}"
echo "${BOLD} Color Palette Preview ${RESET}"
echo "${BOLD}================================================================${RESET}"
echo ""
echo "${BOLD}Palette Name:${RESET} $PALETTE_NAME"
echo "${BOLD}Type:${RESET} $PALETTE_TYPE"
echo "${BOLD}Base Color:${RESET} $BASE_COLOR"
echo ""
for group in "${COLOR_GROUPS[@]}"; do
echo "${BOLD}${CYAN}${group}:${RESET}"
for scale in 50 100 200 300 400 500 600 700 800 900 950; do
local key="${group}.${scale}"
if [[ -n "${PALETTE_DATA[$key]:-}" ]]; then
draw_color_swatch "${PALETTE_DATA[$key]}" "${group}.${scale}"
fi
done
echo ""
done
echo "${BOLD}----------------------------------------------------------------${RESET}"
echo ""
}
# ============================================================================
# Interactive Mode
# ============================================================================
interactive_mode() {
echo ""
echo "${BOLD}${BLUE}+================================================================+${RESET}"
echo "${BOLD}${BLUE}| CSS Color Palette Generator (Interactive) |${RESET}"
echo "${BOLD}${BLUE}+================================================================+${RESET}"
echo ""
while true; do
echo -n "${BOLD}Enter base color${RESET} ${DIM}(hex, or 'q' to quit):${RESET} "
read -r color_input
if [[ "$color_input" =~ ^[qQ]$ ]]; then
echo ""
success "Goodbye!"
exit 0
fi
if [[ -z "$color_input" ]]; then
continue
fi
if ! validate_hex "$color_input"; then
warning "Invalid hex color format"
continue
fi
local hex=$(normalize_hex "$color_input")
echo ""
echo -n "${BOLD}Palette type${RESET} ${DIM}[monochromatic]:${RESET} "
read -r palette_input
palette_input=${palette_input:-monochromatic}
echo -n "${BOLD}Output file${RESET} ${DIM}[./colors.yaml]:${RESET} "
read -r output_input
output_input=${output_input:-./colors.yaml}
echo -n "${BOLD}Style${RESET} ${DIM}[all]:${RESET} "
read -r style_input
style_input=${style_input:-all}
echo ""
info "Generating palette..."
# Reset palette data
PALETTE_DATA=()
COLOR_GROUPS=()
BASE_COLOR="$hex"
PALETTE_TYPE="$palette_input"
OUTPUT_FILE="$output_input"
STYLE_TYPE="$style_input"
PALETTE_NAME="${palette_input}-${hex//\#/}"
generate_palette "$BASE_COLOR" "$PALETTE_TYPE" "$STYLE_TYPE"
VERBOSE=true
display_palette_preview
VERBOSE=false
# Generate output
local extension="${OUTPUT_FILE##*.}"
if [[ "$extension" == "json" ]]; then
generate_json_output "$PALETTE_NAME" "$PALETTE_TYPE" "$COLOR_MODE" "$STYLE_TYPE" "$BASE_COLOR" > "$OUTPUT_FILE"
else
generate_yaml_output "$PALETTE_NAME" "$PALETTE_TYPE" "$COLOR_MODE" "$STYLE_TYPE" "$BASE_COLOR" > "$OUTPUT_FILE"
fi
success "Palette saved to: ${BOLD}$OUTPUT_FILE${RESET}"
# Count colors
local total=0
for key in "${!PALETTE_DATA[@]}"; do
((total++))
done
info "Total colors generated: $total"
echo ""
echo -n "${DIM}Press Enter to continue...${RESET}"
read -r
echo ""
done
}
# ============================================================================
# Main Script Logic
# ============================================================================
main() {
# Check dependencies
check_dependencies
# Parse arguments
while [[ $# -gt 0 ]]; do
case "$1" in
-h|--help)
print_usage
exit 0
;;
-p|--palette)
PALETTE_TYPE="$2"
shift 2
;;
-o|--output)
OUTPUT_FILE="$2"
shift 2
;;
-m|--mode)
COLOR_MODE="$2"
shift 2
;;
-s|--style)
STYLE_TYPE="$2"
shift 2
;;
-n|--name)
PALETTE_NAME="$2"
shift 2
;;
--scales)
SCALE_STEPS="$2"
shift 2
;;
-i|--interactive)
INTERACTIVE=true
shift
;;
-v|--verbose)
VERBOSE=true
shift
;;
-*)
error "Unknown option: $1. Use --help for usage information."
;;
*)
BASE_COLOR="$1"
shift
;;
esac
done
# Interactive mode
if [[ "$INTERACTIVE" == "true" ]]; then
interactive_mode
exit 0
fi
# Validate inputs
if [[ -z "$BASE_COLOR" ]]; then
error "No base color specified. Use --help for usage information."
fi
if ! validate_hex "$BASE_COLOR"; then
error "Invalid hex color format: $BASE_COLOR"
fi
# Normalize color
BASE_COLOR=$(normalize_hex "$BASE_COLOR")
# Auto-generate palette name if not provided
if [[ -z "$PALETTE_NAME" ]]; then
PALETTE_NAME="${PALETTE_TYPE}-${BASE_COLOR//\#/}"
fi
# Validate palette type
case "$PALETTE_TYPE" in
monochromatic|analogous|complementary|split-complementary|triadic|tetradic)
;;
*)
error "Invalid palette type: $PALETTE_TYPE"
;;
esac
# Validate style type
case "$STYLE_TYPE" in
all|shades|tints|tones)
;;
*)
error "Invalid style type: $STYLE_TYPE"
;;
esac
# Generate palette
if [[ "$VERBOSE" == "true" ]]; then
info "Generating $PALETTE_TYPE palette from $BASE_COLOR..."
fi
generate_palette "$BASE_COLOR" "$PALETTE_TYPE" "$STYLE_TYPE"
# Display preview if verbose
display_palette_preview
# Generate output file
local extension="${OUTPUT_FILE##*.}"
if [[ "$extension" == "json" ]]; then
generate_json_output "$PALETTE_NAME" "$PALETTE_TYPE" "$COLOR_MODE" "$STYLE_TYPE" "$BASE_COLOR" > "$OUTPUT_FILE"
else
generate_yaml_output "$PALETTE_NAME" "$PALETTE_TYPE" "$COLOR_MODE" "$STYLE_TYPE" "$BASE_COLOR" > "$OUTPUT_FILE"
fi
success "Palette saved to: ${BOLD}$OUTPUT_FILE${RESET}"
if [[ "$VERBOSE" == "true" ]]; then
local total=0
for key in "${!PALETTE_DATA[@]}"; do
((total++))
done
info "Total colors generated: $total"
fi
}
# Run main function
main "$@"

347
css_json_convert.sh Executable file
View File

@@ -0,0 +1,347 @@
#!/usr/bin/env bash
#############################################
# CSS Variable to JSON/YAML Converter
# Extracts CSS custom properties (--var: value;)
# and converts them to JSON or YAML format
#############################################
set -euo pipefail
# Terminal colors using tput
RED="" GREEN="" YELLOW="" BLUE="" MAGENTA="" CYAN="" BOLD="" DIM="" RESET=""
if [[ -t 1 ]] && command -v tput >/dev/null 2>&1; then
COLORS=$(tput colors 2>/dev/null || echo 0)
if [[ ${COLORS:-0} -ge 8 ]]; then
RED=$(tput setaf 1 2>/dev/null || echo "")
GREEN=$(tput setaf 2 2>/dev/null || echo "")
YELLOW=$(tput setaf 3 2>/dev/null || echo "")
BLUE=$(tput setaf 4 2>/dev/null || echo "")
MAGENTA=$(tput setaf 5 2>/dev/null || echo "")
CYAN=$(tput setaf 6 2>/dev/null || echo "")
BOLD=$(tput bold 2>/dev/null || echo "")
DIM=$(tput dim 2>/dev/null || echo "")
RESET=$(tput sgr0 2>/dev/null || echo "")
fi
fi
# Icons for output (simple ASCII)
readonly ICON_SUCCESS="[OK]"
readonly ICON_ERROR="[ERROR]"
readonly ICON_INFO="[INFO]"
readonly ICON_CONVERT="==>"
# Default values
OUTPUT_FILE="${PWD}/output.yaml"
INPUT_FILE=""
CAMEL_CASE=false
VERBOSE=false
#############################################
# Functions
#############################################
print_banner() {
echo ""
echo "${CYAN}${BOLD}================================================================${RESET}"
echo "${CYAN}${BOLD} CSS Variable to JSON/YAML Converter${RESET}"
echo "${CYAN}${BOLD} Extract CSS custom properties with ease${RESET}"
echo "${CYAN}${BOLD}================================================================${RESET}"
echo ""
}
print_success() {
echo "${GREEN}${ICON_SUCCESS}${RESET} $*"
}
print_error() {
echo "${RED}${ICON_ERROR}${RESET} $*" >&2
}
print_info() {
echo "${BLUE}${ICON_INFO}${RESET} $*"
}
print_verbose() {
if [[ "$VERBOSE" == true ]]; then
echo "${DIM} ==> $*${RESET}"
fi
return 0
}
show_help() {
cat << EOF
${BOLD}USAGE:${RESET}
$(basename "$0") [OPTIONS] <input.css>
${BOLD}DESCRIPTION:${RESET}
Extracts CSS custom properties (variables) from a CSS file and converts
them to JSON or YAML format. Automatically detects output format from
file extension.
${BOLD}ARGUMENTS:${RESET}
<input.css> Input CSS file containing CSS variables
${BOLD}OPTIONS:${RESET}
-o, --output FILE Output file path (default: ./output.yaml)
Format auto-detected from extension (.json/.yaml/.yml)
-c, --camel-case Convert variable names to camelCase
(e.g., --main-color -> mainColor)
-v, --verbose Enable verbose output
-h, --help Show this help message
${BOLD}EXAMPLES:${RESET}
# Extract CSS vars to YAML (default)
$(basename "$0") styles.css
# Extract to JSON with custom output
$(basename "$0") styles.css -o theme.json
# Convert variable names to camelCase
$(basename "$0") styles.css -o vars.json --camel-case
${BOLD}CSS VARIABLE FORMAT:${RESET}
The script extracts CSS custom properties in the format:
--variable-name: value;
Example input:
:root {
--main-color: #e8eaed;
--font-size: 16px;
}
Example JSON output:
{
"main-color": "#e8eaed",
"font-size": "16px"
}
EOF
}
# Convert kebab-case to camelCase
to_camel_case() {
local input="$1"
# Remove leading dashes and convert to camelCase
echo "$input" | sed -E 's/^--//; s/-(.)/\U\1/g'
}
# Extract CSS variables using advanced sed
extract_css_variables() {
local input_file="$1"
print_verbose "Extracting CSS variables from: $input_file" >&2
# Advanced sed expression to extract CSS custom properties
# Matches: --variable-name: value; (with flexible whitespace)
sed -n 's/^[[:space:]]*\(--[a-zA-Z0-9_-]\+\)[[:space:]]*:[[:space:]]*\([^;]\+\);.*$/\1|\2/p' "$input_file" \
| sed 's/[[:space:]]*$//' \
| sed 's/^[[:space:]]*//'
}
# Convert to JSON format
convert_to_json() {
local -a variables=("$@")
local json="{"
local first=true
for var in "${variables[@]}"; do
IFS='|' read -r name value <<< "$var"
# Remove leading dashes
name="${name#--}"
# Convert to camelCase if requested
if [[ "$CAMEL_CASE" == true ]]; then
name=$(to_camel_case "--$name")
fi
# Trim whitespace from value
value=$(echo "$value" | sed 's/^[[:space:]]*//; s/[[:space:]]*$//')
# Escape quotes in value
value="${value//\"/\\\"}"
if [[ "$first" == true ]]; then
first=false
json+=$'\n'
else
json+=","$'\n'
fi
json+=" \"$name\": \"$value\""
done
json+=$'\n'"}"
echo "$json"
}
# Convert to YAML format
convert_to_yaml() {
local -a variables=("$@")
local yaml=""
for var in "${variables[@]}"; do
IFS='|' read -r name value <<< "$var"
# Remove leading dashes
name="${name#--}"
# Convert to camelCase if requested
if [[ "$CAMEL_CASE" == true ]]; then
name=$(to_camel_case "--$name")
fi
# Trim whitespace from value
value=$(echo "$value" | sed 's/^[[:space:]]*//; s/[[:space:]]*$//')
# Quote value if it contains special characters or starts with special chars
if [[ "$value" =~ ^[#\&\*\!\|\>\'\"] ]] || [[ "$value" =~ [:\{\}\[\],] ]]; then
value="\"$value\""
fi
yaml+="$name: $value"$'\n'
done
echo "$yaml"
}
# Determine output format from file extension
get_output_format() {
local file="$1"
local ext="${file##*.}"
case "$ext" in
json)
echo "json"
;;
yaml|yml)
echo "yaml"
;;
*)
print_error "Unsupported output format: .$ext"
print_info "Supported formats: .json, .yaml, .yml"
exit 1
;;
esac
}
#############################################
# Main Script
#############################################
main() {
print_banner
# Parse arguments
while [[ $# -gt 0 ]]; do
case $1 in
-o|--output)
OUTPUT_FILE="$2"
shift 2
;;
-c|--camel-case)
CAMEL_CASE=true
shift
;;
-v|--verbose)
VERBOSE=true
shift
;;
-h|--help)
show_help
exit 0
;;
-*)
print_error "Unknown option: $1"
echo "Use -h or --help for usage information"
exit 1
;;
*)
INPUT_FILE="$1"
shift
;;
esac
done
# Validate input file
if [[ -z "$INPUT_FILE" ]]; then
print_error "No input file specified"
echo "Use -h or --help for usage information"
exit 1
fi
if [[ ! -f "$INPUT_FILE" ]]; then
print_error "Input file not found: $INPUT_FILE"
exit 1
fi
# Show processing info
print_info "${ICON_FILE} Input: ${CYAN}$INPUT_FILE${RESET}"
print_info "${ICON_FILE} Output: ${CYAN}$OUTPUT_FILE${RESET}"
if [[ "$CAMEL_CASE" == true ]]; then
print_info "${ICON_CONVERT} Mode: ${YELLOW}camelCase conversion enabled${RESET}"
fi
echo ""
# Extract CSS variables
print_info "${ICON_CONVERT} Extracting CSS variables..."
mapfile -t variables < <(extract_css_variables "$INPUT_FILE")
if [[ ${#variables[@]} -eq 0 ]]; then
print_error "No CSS variables found in $INPUT_FILE"
print_info "Expected format: --variable-name: value;"
exit 1
fi
print_success "Found ${BOLD}${#variables[@]}${RESET} CSS variable(s)"
# Show extracted variables in verbose mode
if [[ "$VERBOSE" == true ]]; then
echo ""
for var in "${variables[@]}"; do
IFS='|' read -r name value <<< "$var"
print_verbose "${MAGENTA}$name${RESET} = ${GREEN}$value${RESET}"
done
echo ""
fi
# Determine output format
OUTPUT_FORMAT=$(get_output_format "$OUTPUT_FILE")
print_verbose "Output format: $OUTPUT_FORMAT"
# Convert and write output
print_info "${ICON_CONVERT} Converting to ${BOLD}${OUTPUT_FORMAT^^}${RESET}..."
case "$OUTPUT_FORMAT" in
json)
convert_to_json "${variables[@]}" > "$OUTPUT_FILE"
;;
yaml)
convert_to_yaml "${variables[@]}" > "$OUTPUT_FILE"
;;
esac
# Success message
echo ""
print_success "${ICON_ROCKET} Conversion complete!"
print_info "Output saved to: ${BOLD}${GREEN}$OUTPUT_FILE${RESET}"
# Show preview if verbose
if [[ "$VERBOSE" == true ]]; then
echo ""
echo -e "${DIM} Preview ${RESET}"
head -n 20 "$OUTPUT_FILE" | sed 's/^/ /'
if [[ $(wc -l < "$OUTPUT_FILE") -gt 20 ]]; then
echo -e "${DIM} ... (truncated)${RESET}"
fi
echo -e "${DIM}${RESET}"
fi
}
# Run main function
main "$@"

722
doc_bash_generate.sh Executable file
View File

@@ -0,0 +1,722 @@
#!/usr/bin/env bash
#############################################################################
# Bash Documentation Generator with Animated GIFs
#
# Generate sexy, comprehensive README.md files with embedded asciinema GIFs
# for any POSIX-compliant executables (those with --help)
#
# Usage:
# doc_bash_generate.sh [OPTIONS] <executable> [executable...]
# doc_bash_generate.sh [OPTIONS] "*.sh"
#
# Arguments:
# executable One or more executables or glob patterns
#
# Options:
# -o, --output FILE Output README.md path (default: ./README.md)
# -t, --title TITLE Documentation title (default: auto-generated)
# --no-gif Skip GIF generation (faster, text only)
# --gif-only Only generate GIFs, don't update README
# -h, --help Show this help message
#
# Examples:
# doc_bash_generate.sh css_*.sh
# doc_bash_generate.sh -o docs/README.md *.sh
# doc_bash_generate.sh --title "My Awesome Tools" script1.sh script2.sh
#
# Dependencies:
# asciinema For terminal recording
# agg For converting asciinema to GIF (cargo install agg)
#
#############################################################################
set -euo pipefail
# ============================================================================
# Color Definitions
# ============================================================================
RED="" GREEN="" YELLOW="" BLUE="" MAGENTA="" CYAN="" BOLD="" DIM="" RESET=""
COLORS=0
if [[ -t 1 ]] && command -v tput >/dev/null 2>&1; then
COLORS=$(tput colors 2>/dev/null || echo 0)
if [[ ${COLORS:-0} -ge 8 ]]; then
RED=$(tput setaf 1 2>/dev/null || echo "")
GREEN=$(tput setaf 2 2>/dev/null || echo "")
YELLOW=$(tput setaf 3 2>/dev/null || echo "")
BLUE=$(tput setaf 4 2>/dev/null || echo "")
MAGENTA=$(tput setaf 5 2>/dev/null || echo "")
CYAN=$(tput setaf 6 2>/dev/null || echo "")
BOLD=$(tput bold 2>/dev/null || echo "")
DIM=$(tput dim 2>/dev/null || echo "")
RESET=$(tput sgr0 2>/dev/null || echo "")
fi
fi
# ============================================================================
# Configuration
# ============================================================================
OUTPUT_FILE="$PWD/README.md"
DOC_TITLE=""
SKIP_GIF=false
GIF_ONLY=false
declare -a EXECUTABLES=()
TEMP_DIR=""
# ============================================================================
# Helper Functions
# ============================================================================
print_usage() {
cat << EOF
${BOLD}Bash Documentation Generator with Animated GIFs${RESET}
Generate sexy, comprehensive README.md files with embedded asciinema GIFs.
${BOLD}USAGE:${RESET}
$(basename "$0") [OPTIONS] <executable> [executable...]
${BOLD}ARGUMENTS:${RESET}
executable One or more executables or glob patterns
${BOLD}OPTIONS:${RESET}
-o, --output FILE Output README.md path (default: ./README.md)
-t, --title TITLE Documentation title (default: auto-generated)
--no-gif Skip GIF generation (faster, text only)
--gif-only Only generate GIFs, don't update README
-h, --help Show this help message
${BOLD}EXAMPLES:${RESET}
$(basename "$0") css_*.sh
$(basename "$0") -o docs/README.md *.sh
$(basename "$0") --title "My Awesome Tools" script1.sh script2.sh
${BOLD}DEPENDENCIES:${RESET}
asciinema Terminal session recorder
agg Asciinema to GIF converter (cargo install agg)
${BOLD}NOTES:${RESET}
Demos are automatically generated by running ${DIM}--help${RESET} on each command.
GIF recordings are created in a temporary directory and cleaned up after.
EOF
}
error() {
echo "${RED}${BOLD}Error:${RESET} $1" >&2
exit 1
}
info() {
echo "${BLUE}${BOLD}==>${RESET} $1" >&2
}
success() {
echo "${GREEN}${BOLD}[OK]${RESET} $1" >&2
}
warning() {
echo "${YELLOW}${BOLD}[WARN]${RESET} $1" >&2
}
banner() {
echo "" >&2
echo "${BOLD}${MAGENTA}================================================================${RESET}" >&2
echo "${BOLD}${MAGENTA} $1${RESET}" >&2
echo "${BOLD}${MAGENTA}================================================================${RESET}" >&2
echo "" >&2
}
cleanup() {
if [[ -n "${TEMP_DIR:-}" ]] && [[ -d "$TEMP_DIR" ]]; then
rm -rf "$TEMP_DIR"
fi
}
trap cleanup EXIT
check_dependencies() {
local missing=()
if ! command -v asciinema >/dev/null 2>&1; then
missing+=("asciinema")
fi
if ! command -v agg >/dev/null 2>&1 && [[ "$SKIP_GIF" == "false" ]]; then
warning "agg not found. Install with: cargo install agg"
warning "Continuing without GIF generation..."
SKIP_GIF=true
fi
if [[ ${#missing[@]} -gt 0 ]]; then
error "Missing required dependencies: ${missing[*]}"
fi
}
# ============================================================================
# Help Parsing Functions
# ============================================================================
get_command_help() {
local cmd="$1"
if [[ ! -x "$cmd" ]]; then
warning "Command not executable: $cmd"
return 1
fi
# Try --help, then -h, then fail
local help_output
if help_output=$("$cmd" --help 2>&1); then
echo "$help_output"
return 0
elif help_output=$("$cmd" -h 2>&1); then
echo "$help_output"
return 0
else
warning "Command doesn't support --help: $cmd"
return 1
fi
}
extract_description() {
local help_text="$1"
# Try to find first non-empty line after header/title
echo "$help_text" | grep -v "^#" | grep -v "^-" | grep -v "^=" | \
grep -E "^[A-Z]" | head -n 1 | sed 's/^[[:space:]]*//'
}
extract_usage() {
local help_text="$1"
# Find USAGE section
echo "$help_text" | sed -n '/USAGE:/,/^$/p' | grep -v "USAGE:" | \
sed 's/^[[:space:]]*//'
}
extract_examples() {
local help_text="$1"
# Find EXAMPLES section
echo "$help_text" | sed -n '/EXAMPLES:/,/^[A-Z]/p' | grep -v "EXAMPLES:" | \
grep -v "^[A-Z]" | sed 's/^[[:space:]]*//'
}
# ============================================================================
# Asciinema Recording Functions
# ============================================================================
extract_example_commands() {
local help_text="$1"
local cmd_name="$2"
# Extract example commands from EXAMPLES section
# Look for lines that start with the command name or common patterns
echo "$help_text" | sed -n '/EXAMPLES:/,/^[A-Z]/p' | \
grep -E "^\s*($cmd_name|#|$)" | \
grep -v "^#" | \
grep -v "EXAMPLES:" | \
grep -v "^[A-Z]" | \
sed 's/^[[:space:]]*//' | \
grep -v "^$" | \
head -n 2
}
create_demo() {
local cmd="$1"
local cmd_name=$(basename "$cmd")
# Create demo file in temp directory
local demo_file="$TEMP_DIR/${cmd_name%.sh}.demo"
# Get help text to extract examples
local help_text=$("$cmd" --help 2>&1 || true)
# Extract example commands from EXAMPLES section
local examples=$(echo "$help_text" | sed -n '/EXAMPLES:/,/^[A-Z]/p' | \
grep -E "^\s*$cmd_name" | head -n 2 | sed 's/^[[:space:]]*//')
cat > "$demo_file" << 'DEMO_HEADER'
#!/usr/bin/env bash
# Auto-generated demo
echo ""
echo "=== Demo: $CMD_NAME ==="
echo ""
DEMO_HEADER
# If we have examples, run them
if [[ -n "$examples" ]]; then
echo '# Show examples' >> "$demo_file"
echo 'echo ""' >> "$demo_file"
while IFS= read -r example; do
if [[ -n "$example" ]]; then
# Show the command being run
echo "echo '$ $example'" >> "$demo_file"
echo 'echo ""' >> "$demo_file"
# Run the command with timeout (15 seconds max)
# Replace the script name with full path
local full_example="${example/$cmd_name/$cmd}"
cat >> "$demo_file" << EOF
timeout 15s $full_example 2>&1 || {
exit_code=\$?
if [ \$exit_code -eq 124 ]; then
echo "[Example timed out after 15s - computation too intensive for demo]"
else
echo "[Command requires additional setup or arguments]"
fi
}
EOF
echo 'echo ""' >> "$demo_file"
fi
done <<< "$examples"
else
# Fallback to showing help if no examples found
echo 'echo "$ $CMD_NAME --help | head -n 20"' >> "$demo_file"
echo 'echo ""' >> "$demo_file"
echo "$cmd --help 2>&1 | head -n 20" >> "$demo_file"
fi
echo 'echo ""' >> "$demo_file"
# Replace placeholder with actual command name
sed -i "s|\$CMD_NAME|$cmd_name|g" "$demo_file"
chmod +x "$demo_file"
echo "$demo_file"
}
record_demo() {
local cmd="$1"
local output_cast="$2"
local demo_script="$3"
info "Recording demo for $(basename "$cmd")..."
# Create a sandbox directory for demo execution
local sandbox_dir="$TEMP_DIR/sandbox"
mkdir -p "$sandbox_dir"
# Create a wrapper script that runs the demo in sandbox
local wrapper="$TEMP_DIR/demo_wrapper.sh"
cat > "$wrapper" << 'WRAPPER_EOF'
#!/usr/bin/env bash
set -e
# Set terminal size for consistent output
export COLUMNS=100
export LINES=30
# Change to sandbox directory for safe execution
cd "$3"
# Add some style
echo -e "\033[1;36m"
echo "========================================"
echo " Demo: $(basename "$1")"
echo "========================================"
echo -e "\033[0m"
echo ""
sleep 1
# Source the demo script
source "$2"
echo ""
sleep 2
WRAPPER_EOF
chmod +x "$wrapper"
# Record with asciinema (redirect all output to avoid contamination)
asciinema rec \
--command "$wrapper '$cmd' '$demo_script' '$sandbox_dir'" \
--overwrite \
--cols 100 \
--rows 30 \
"$output_cast" >/dev/null 2>&1
if [[ $? -eq 0 ]]; then
success "Recorded: $output_cast"
return 0
else
warning "Failed to record demo for $(basename "$cmd")"
return 1
fi
}
convert_to_gif() {
local cast_file="$1"
local gif_file="$2"
info "Converting to GIF: $(basename "$gif_file")..."
if command -v agg >/dev/null 2>&1; then
agg \
--font-size 14 \
--speed 1.5 \
--theme monokai \
"$cast_file" \
"$gif_file" >/dev/null 2>&1
if [[ $? -eq 0 ]]; then
success "Created GIF: $(basename "$gif_file")"
return 0
fi
fi
warning "Failed to convert to GIF: $(basename "$cast_file")"
return 1
}
# ============================================================================
# README Generation Functions
# ============================================================================
generate_readme_header() {
local title="$1"
local output_dir=$(dirname "$OUTPUT_FILE")
cat << EOF
# $title
> Comprehensive documentation with usage examples and demos
This documentation was auto-generated using [\`doc_bash_generate.sh\`](https://github.com/yourusername/yourrepo).
## Table of Contents
EOF
}
generate_toc_entry() {
local cmd="$1"
local cmd_name=$(basename "$cmd")
local slug=$(echo "$cmd_name" | tr '[:upper:]' '[:lower:]' | sed 's/[^a-z0-9-]/-/g')
echo "- [\`$cmd_name\`](#$slug)"
}
generate_command_section() {
local cmd="$1"
local gif_path="$2"
local help_text="$3"
local cmd_name=$(basename "$cmd")
cat << EOF
---
## \`$cmd_name\`
EOF
# Add description
local description=$(extract_description "$help_text")
if [[ -n "$description" ]]; then
echo "$description"
echo ""
fi
# Add GIF if exists
if [[ -f "$gif_path" ]]; then
local gif_rel_path="docs/img/$(basename "$gif_path")"
cat << EOF
### Demo
![Demo of $cmd_name]($gif_rel_path)
EOF
fi
# Add usage section
cat << EOF
### Usage
\`\`\`bash
$cmd_name [OPTIONS] [ARGUMENTS]
\`\`\`
EOF
# Add full help output
cat << EOF
### Options
<details>
<summary>Click to expand full help output</summary>
\`\`\`
$help_text
\`\`\`
</details>
EOF
# Add examples if available
local examples=$(extract_examples "$help_text")
if [[ -n "$examples" ]]; then
cat << EOF
### Examples
\`\`\`bash
$examples
\`\`\`
EOF
fi
}
generate_readme_footer() {
cat << EOF
---
## Installation
All scripts are available in this repository. Make sure they are executable:
\`\`\`bash
chmod +x *.sh
\`\`\`
Add them to your PATH for easy access:
\`\`\`bash
export PATH="\$PATH:\$(pwd)"
\`\`\`
## Dependencies
Common dependencies for these scripts:
- \`bash\` (4.0+)
- \`bc\` - For floating-point arithmetic
Specific dependencies are listed in each command's help output.
## Contributing
Contributions are welcome! Please ensure:
- Scripts follow POSIX conventions
- Include comprehensive \`--help\` output with usage examples
## License
MIT License - See LICENSE file for details.
---
*Documentation generated on $(date '+%Y-%m-%d %H:%M:%S %Z') by [\`doc_bash_generate.sh\`](https://github.com/yourusername/yourrepo)*
EOF
}
# ============================================================================
# Main Processing Functions
# ============================================================================
process_command() {
local cmd="$1"
local cmd_name=$(basename "$cmd")
local output_dir=$(dirname "$OUTPUT_FILE")
local img_dir="$output_dir/docs/img"
info "Processing: ${BOLD}$cmd_name${RESET}"
# Create image directory
mkdir -p "$img_dir"
# Get help text
local help_text
if ! help_text=$(get_command_help "$cmd" 2>&1); then
warning "Skipping $cmd_name - no help available"
return 1
fi
# Setup paths
local cast_file="$TEMP_DIR/${cmd_name%.sh}.cast"
local gif_file="$img_dir/${cmd_name%.sh}.gif"
# Handle GIF generation
if [[ "$SKIP_GIF" == "false" ]]; then
# Create demo script in temp directory
local demo_script=$(create_demo "$cmd")
# Record and convert
if record_demo "$cmd" "$cast_file" "$demo_script"; then
convert_to_gif "$cast_file" "$gif_file" || true
fi
fi
# Return data for README generation (base64 encode help text to preserve newlines)
local encoded_help=$(echo "$help_text" | base64 -w 0)
echo "$cmd|$gif_file|$encoded_help"
}
build_readme() {
local -a command_data=("$@")
info "Building README.md..."
local title="${DOC_TITLE:-"Script Documentation"}"
local readme_content=""
# Generate header
readme_content+=$(generate_readme_header "$title")
readme_content+=$'\n'
# Generate TOC
for data in "${command_data[@]}"; do
# Split data using bash parameter expansion to avoid IFS issues
local cmd="${data%%|*}"
local rest="${data#*|}"
local gif_path="${rest%%|*}"
local encoded_help="${rest#*|}"
readme_content+=$(generate_toc_entry "$cmd")
readme_content+=$'\n'
done
readme_content+=$'\n'
# Generate command sections
for data in "${command_data[@]}"; do
# Split data using bash parameter expansion to avoid IFS issues
local cmd="${data%%|*}"
local rest="${data#*|}"
local gif_path="${rest%%|*}"
local encoded_help="${rest#*|}"
local help_text=$(echo "$encoded_help" | base64 -d 2>/dev/null || echo "")
readme_content+=$(generate_command_section "$cmd" "$gif_path" "$help_text")
readme_content+=$'\n'
done
# Generate footer
readme_content+=$(generate_readme_footer)
# Write to file
echo "$readme_content" > "$OUTPUT_FILE"
success "README generated: $OUTPUT_FILE"
}
# ============================================================================
# Argument Parsing
# ============================================================================
parse_arguments() {
while [[ $# -gt 0 ]]; do
case "$1" in
-h|--help)
print_usage
exit 0
;;
-o|--output)
OUTPUT_FILE="$2"
shift 2
;;
-t|--title)
DOC_TITLE="$2"
shift 2
;;
--no-gif)
SKIP_GIF=true
shift
;;
--gif-only)
GIF_ONLY=true
shift
;;
-*)
error "Unknown option: $1"
;;
*)
# Handle glob patterns
if [[ "$1" == *"*"* ]] || [[ "$1" == *"?"* ]]; then
# Expand glob
for file in $1; do
if [[ -f "$file" ]]; then
EXECUTABLES+=("$file")
fi
done
else
if [[ -f "$1" ]]; then
EXECUTABLES+=("$1")
else
warning "File not found: $1"
fi
fi
shift
;;
esac
done
if [[ ${#EXECUTABLES[@]} -eq 0 ]]; then
error "No executables specified. Use --help for usage information."
fi
}
# ============================================================================
# Main Script Logic
# ============================================================================
main() {
banner "Bash Documentation Generator"
parse_arguments "$@"
check_dependencies
# Create temp directory
TEMP_DIR=$(mktemp -d)
info "Output: $OUTPUT_FILE"
info "Commands: ${#EXECUTABLES[@]}"
echo ""
# Process all commands
local -a command_data=()
for cmd in "${EXECUTABLES[@]}"; do
local result
if result=$(process_command "$cmd"); then
command_data+=("$result")
fi
echo ""
done
if [[ ${#command_data[@]} -eq 0 ]]; then
error "No commands were successfully processed"
fi
# Build README unless gif-only mode
if [[ "$GIF_ONLY" == "false" ]]; then
build_readme "${command_data[@]}"
fi
echo ""
banner "Documentation Complete!"
echo "${BOLD}Generated Files:${RESET}"
if [[ "$GIF_ONLY" == "false" ]]; then
echo " ${GREEN}*${RESET} $OUTPUT_FILE"
fi
if [[ "$SKIP_GIF" == "false" ]]; then
local img_dir="$(dirname "$OUTPUT_FILE")/docs/img"
if [[ -d "$img_dir" ]]; then
local gif_count=$(find "$img_dir" -name "*.gif" 2>/dev/null | wc -l)
echo " ${GREEN}*${RESET} $gif_count GIF(s) in $img_dir"
fi
fi
echo ""
success "All done! Check out your sexy new documentation!"
}
main "$@"

762
doc_rust_generate.sh Executable file
View File

@@ -0,0 +1,762 @@
#!/usr/bin/env bash
##############################################################
# Rust Documentation Generator with Custom Themes
# Generates beautiful Rust documentation with custom color schemes
##############################################################
set -euo pipefail
# Color definitions using tput
RED="" GREEN="" YELLOW="" BLUE="" MAGENTA="" CYAN="" BOLD="" DIM="" RESET=""
if [[ -t 1 ]] && command -v tput >/dev/null 2>&1; then
COLORS=$(tput colors 2>/dev/null || echo 0)
if [[ ${COLORS:-0} -ge 8 ]]; then
RED=$(tput setaf 1 2>/dev/null || echo "")
GREEN=$(tput setaf 2 2>/dev/null || echo "")
YELLOW=$(tput setaf 3 2>/dev/null || echo "")
BLUE=$(tput setaf 4 2>/dev/null || echo "")
MAGENTA=$(tput setaf 5 2>/dev/null || echo "")
CYAN=$(tput setaf 6 2>/dev/null || echo "")
BOLD=$(tput bold 2>/dev/null || echo "")
DIM=$(tput dim 2>/dev/null || echo "")
RESET=$(tput sgr0 2>/dev/null || echo "")
fi
fi
# Script paths
readonly SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
readonly ASSETS_DIR="${SCRIPT_DIR}/assets/doc_rust_generate"
readonly THEME_DIR="${ASSETS_DIR}/theme"
readonly TMP_DIR="${SCRIPT_DIR}/tmp"
# Default values
OUTPUT_DIR="${PWD}/output"
PRIMARY_COLOR="#ff69b4" # Default pink
STYLE="slate"
FONT_SANS="Inter"
FONT_MONO="JetBrains Mono"
INPUT_FILES=()
VERBOSE=false
DRY_RUN=false
SERVE=false
OPEN=false
SERVE_PORT=8000
# Style color mappings (neutral background colors)
declare -A STYLE_COLORS=(
["slate"]="#64748b"
["zinc"]="#71717a"
["neutral"]="#737373"
["stone"]="#78716c"
["gray"]="#6b7280"
)
##############################################################
# UI Functions
##############################################################
info() {
echo "${BLUE}${BOLD}==>${RESET} $*"
}
success() {
echo "${GREEN}${BOLD}[OK]${RESET} $*"
}
error() {
echo "${RED}${BOLD}[ERROR]${RESET} $*" >&2
}
warning() {
echo "${YELLOW}${BOLD}[WARN]${RESET} $*"
}
verbose() {
[[ "$VERBOSE" == true ]] && echo "${DIM} -> $*${RESET}"
}
# Spinner for long-running operations
spinner() {
local pid=$1
local message=$2
local delay=0.1
local spinstr='⠋⠙⠹⠸⠼⠴⠦⠧⠇⠏'
# Only show spinner if not verbose and output is a terminal
if [[ "$VERBOSE" == true ]] || [[ ! -t 1 ]]; then
wait "$pid"
return $?
fi
echo -n "${message} "
while kill -0 "$pid" 2>/dev/null; do
local temp=${spinstr#?}
printf "${CYAN}%c${RESET} " "$spinstr"
spinstr=$temp${spinstr%"$temp"}
sleep $delay
printf "\b\b\b"
done
wait "$pid"
local exit_code=$?
if [[ $exit_code -eq 0 ]]; then
printf "${GREEN}${RESET}\n"
else
printf "${RED}${RESET}\n"
fi
return $exit_code
}
##############################################################
# Help and Usage
##############################################################
show_help() {
cat << EOF
${BOLD}${MAGENTA}Rust Documentation Generator with Custom Themes${RESET}
${BOLD}USAGE:${RESET}
$(basename "$0") [OPTIONS] <inputs...>
${BOLD}DESCRIPTION:${RESET}
Generate beautiful Rust documentation with custom color schemes and styling.
Supports various input types including Rust projects, individual files, and more.
${BOLD}ARGUMENTS:${RESET}
<inputs> Input file(s) or pattern(s) to document:
- Rust project directories (containing Cargo.toml)
- Individual .rs files
- Markdown files (.md)
- JSON/TOML configuration files
- Glob patterns (e.g., src/**/*.rs)
${BOLD}OPTIONS:${RESET}
-o, --output DIR Output directory for generated docs
(default: \$PWD/output)
-c, --color COLOR Primary accent color (hex format)
(default: #ff69b4)
Examples: #3498db, #10b981, #8b5cf6
-s, --style STYLE Background style theme
Options: slate, zinc, neutral, stone, gray
(default: slate)
--font-sans FONT Google Font for body text (default: Inter)
--font-mono FONT Google Font for code blocks
(default: JetBrains Mono)
--serve Start HTTP server after generation
--open Open documentation in browser (implies --serve)
-p, --port PORT Port for HTTP server (default: 8000)
-v, --verbose Enable verbose output
-d, --dry-run Show what would be done without executing
-h, --help Show this help message
${BOLD}EXAMPLES:${RESET}
${DIM}# Generate docs for current Rust project${RESET}
$(basename "$0") .
${DIM}# Custom color scheme${RESET}
$(basename "$0") . -c "#3498db" -s zinc -o ./docs
${DIM}# Document specific files${RESET}
$(basename "$0") src/lib.rs src/main.rs -o ./api-docs
${DIM}# Use custom fonts${RESET}
$(basename "$0") . --font-sans "Roboto" --font-mono "Fira Code"
${DIM}# Generate and open in browser${RESET}
$(basename "$0") . --open
${BOLD}NOTES:${RESET}
- Requires: cargo, rustdoc, bc, yq, jq, python3 with jinja2
- Colors are automatically generated in light and dark variants
- Google Fonts are automatically imported
- Mermaid.js diagrams are automatically rendered
EOF
}
##############################################################
# Dependency Checks
##############################################################
check_dependencies() {
local missing=()
command -v cargo >/dev/null 2>&1 || missing+=("cargo")
command -v rustdoc >/dev/null 2>&1 || missing+=("rustdoc")
command -v bc >/dev/null 2>&1 || missing+=("bc")
command -v yq >/dev/null 2>&1 || missing+=("yq")
command -v jq >/dev/null 2>&1 || missing+=("jq")
command -v python3 >/dev/null 2>&1 || missing+=("python3")
if ! python3 -c "import jinja2" 2>/dev/null; then
missing+=("python3-jinja2")
fi
if [[ ${#missing[@]} -gt 0 ]]; then
error "Missing required dependencies: ${missing[*]}"
exit 1
fi
# Check for helper scripts
if [[ ! -x "${SCRIPT_DIR}/css_color_palette.sh" ]]; then
error "Helper script not found: ${SCRIPT_DIR}/css_color_palette.sh"
exit 1
fi
if [[ ! -x "${SCRIPT_DIR}/jinja_template_render.sh" ]]; then
error "Helper script not found: ${SCRIPT_DIR}/jinja_template_render.sh"
exit 1
fi
}
##############################################################
# Input Processing
##############################################################
validate_color() {
local color="$1"
# Remove leading # if present
color="${color#\#}"
if [[ ! "$color" =~ ^[0-9A-Fa-f]{3}$ ]] && [[ ! "$color" =~ ^[0-9A-Fa-f]{6}$ ]]; then
error "Invalid hex color: $1"
error "Expected format: #RGB or #RRGGBB"
exit 1
fi
# Ensure 6-digit format
if [[ ${#color} -eq 3 ]]; then
color="${color:0:1}${color:0:1}${color:1:1}${color:1:1}${color:2:1}${color:2:1}"
fi
echo "#${color}"
}
validate_style() {
local style="$1"
case "$style" in
slate|zinc|neutral|stone|gray)
echo "$style"
;;
*)
error "Invalid style: $style"
error "Valid options: slate, zinc, neutral, stone, gray"
exit 1
;;
esac
}
##############################################################
# Color Palette Generation
##############################################################
generate_color_palette() {
local primary_color="$1"
local style="$2"
local output_file="$3"
verbose "Primary color: $primary_color"
verbose "Style: $style"
# Generate primary color palette (monochromatic)
"${SCRIPT_DIR}/css_color_palette.sh" "$primary_color" \
-p monochromatic \
-o "${TMP_DIR}/primary_palette.yaml" \
-m dark \
-s all >/dev/null 2>&1 &
local palette_pid=$!
if ! spinner "$palette_pid" "Generating color palette..."; then
error "Failed to generate primary color palette"
return 1
fi
verbose "Generated primary palette: ${TMP_DIR}/primary_palette.yaml"
}
##############################################################
# CSS Variables Generation
##############################################################
generate_css_variables() {
local primary_color="$1"
local style="$2"
local output_file="$3"
info "Generating CSS variables..."
# Read the generated palette
local palette_file="${TMP_DIR}/primary_palette.yaml"
if [[ ! -f "$palette_file" ]]; then
error "Palette file not found: $palette_file"
return 1
fi
# Extract color values from the palette
local primary_50 primary_100 primary_200 primary_300 primary_400
local primary_500 primary_600 primary_700 primary_800 primary_900 primary_950
primary_50=$(yq -r '.colors.primary."50"' "$palette_file")
primary_100=$(yq -r '.colors.primary."100"' "$palette_file")
primary_200=$(yq -r '.colors.primary."200"' "$palette_file")
primary_300=$(yq -r '.colors.primary."300"' "$palette_file")
primary_400=$(yq -r '.colors.primary."400"' "$palette_file")
primary_500=$(yq -r '.colors.primary."500"' "$palette_file")
primary_600=$(yq -r '.colors.primary."600"' "$palette_file")
primary_700=$(yq -r '.colors.primary."700"' "$palette_file")
primary_800=$(yq -r '.colors.primary."800"' "$palette_file")
primary_900=$(yq -r '.colors.primary."900"' "$palette_file")
primary_950=$(yq -r '.colors.primary."950"' "$palette_file")
# Get style-specific colors (from existing theme.yaml)
local style_file="${THEME_DIR}/theme.yaml"
# Generate CSS custom properties
cat > "$output_file" << EOF
/* Generated CSS Variables for Rustdoc Theme */
/* Primary Color: ${primary_color} */
/* Style: ${style} */
:root {
/* Primary color palette */
--primary-50: ${primary_50};
--primary-100: ${primary_100};
--primary-200: ${primary_200};
--primary-300: ${primary_300};
--primary-400: ${primary_400};
--primary-500: ${primary_500};
--primary-600: ${primary_600};
--primary-700: ${primary_700};
--primary-800: ${primary_800};
--primary-900: ${primary_900};
--primary-950: ${primary_950};
/* Google Fonts */
--font-family: '${FONT_SANS}', 'Fira Sans', Arial, sans-serif;
--font-family-code: '${FONT_MONO}', 'Fira Mono', monospace;
}
/* Import Google Fonts */
@import url('https://fonts.googleapis.com/css2?family=${FONT_SANS// /+}:wght@300;400;500;600;700&family=${FONT_MONO// /+}:wght@400;500;600;700&display=swap');
:root {
EOF
# Now read the theme.yaml and replace color references with our palette
verbose "Processing theme variables..."
# Read theme.yaml and substitute colors
while IFS=': ' read -r key value; do
# Skip empty lines and comments
[[ -z "$key" || "$key" =~ ^# ]] && continue
# Remove quotes and leading/trailing whitespace
value="${value#\"}"
value="${value%\"}"
value="${value#\'}"
value="${value%\'}"
value="${value# }"
value="${value% }"
# Replace hardcoded colors with our palette where appropriate
# This is a simple substitution - for primary/accent colors use our palette
if [[ "$value" == "#ff69b4" || "$value" == "#ff1493" || "$value" == "#ec407a" ]]; then
value="${primary_500}"
elif [[ "$value" == "#f06292" ]]; then
value="${primary_400}"
fi
echo " --${key}: ${value};" >> "$output_file"
done < <(grep -E '^[a-zA-Z]' "$style_file" || true)
# Close the :root block
echo "}" >> "$output_file"
verbose "Generated CSS variables: $output_file"
success "CSS variables generated"
}
##############################################################
# Render Header Template
##############################################################
render_header_template() {
local output_file="$1"
info "Rendering header template..."
# Process mermaid.yaml variables to match our theme
local mermaid_yaml="${TMP_DIR}/mermaid_processed.yaml"
# Copy and update mermaid colors
cp "${THEME_DIR}/mermaid.yaml" "$mermaid_yaml"
# Update with our primary color
yq -i ".primaryColor = \"${PRIMARY_COLOR}\"" "$mermaid_yaml"
yq -i ".lineColor = \"${PRIMARY_COLOR}\"" "$mermaid_yaml"
yq -i ".border1 = \"${PRIMARY_COLOR}\"" "$mermaid_yaml"
yq -i ".noteBorderColor = \"${PRIMARY_COLOR}\"" "$mermaid_yaml"
yq -i ".arrowheadColor = \"${PRIMARY_COLOR}\"" "$mermaid_yaml"
yq -i ".clusterBorder = \"${PRIMARY_COLOR}\"" "$mermaid_yaml"
yq -i ".defaultLinkColor = \"${PRIMARY_COLOR}\"" "$mermaid_yaml"
yq -i ".fontFamily = \"${FONT_SANS}, system-ui, -apple-system, sans-serif\"" "$mermaid_yaml"
verbose "Processed mermaid config: $mermaid_yaml"
# Render the Jinja2 template
if ! "${SCRIPT_DIR}/jinja_template_render.sh" \
-f "$mermaid_yaml" \
-o "${TMP_DIR}" \
"${THEME_DIR}/header.html.jinja"; then
error "Failed to render header template"
return 1
fi
# Move rendered file to expected location if different
if [[ "${TMP_DIR}/header.html" != "$output_file" ]]; then
mv "${TMP_DIR}/header.html" "$output_file"
fi
verbose "Rendered header: $output_file"
success "Header template rendered"
}
##############################################################
# Consolidate CSS
##############################################################
consolidate_css() {
local output_file="$1"
info "Consolidating CSS files..."
# Combine variables.css and theme.css
cat "${TMP_DIR}/variables.css" > "$output_file"
echo "" >> "$output_file"
cat "${THEME_DIR}/theme.css" >> "$output_file"
verbose "Consolidated CSS: $output_file"
success "CSS consolidated"
}
##############################################################
# Run Rustdoc
##############################################################
run_rustdoc() {
local inputs=("$@")
info "Running rustdoc..."
local header_file="${TMP_DIR}/header.html"
local css_file="${TMP_DIR}/theme-combined.css"
# Determine if we're documenting a cargo project or individual files
local is_cargo_project=false
for input in "${inputs[@]}"; do
if [[ -f "${input}/Cargo.toml" ]] || [[ -f "${input%/}/Cargo.toml" ]]; then
is_cargo_project=true
break
fi
done
if [[ "$is_cargo_project" == true ]]; then
info "Detected Cargo project, using 'cargo doc'..."
# Find the Cargo.toml directory
local cargo_dir=""
for input in "${inputs[@]}"; do
if [[ -f "${input}/Cargo.toml" ]]; then
cargo_dir="$input"
break
elif [[ -f "${input%/}/Cargo.toml" ]]; then
cargo_dir="${input%/}"
break
fi
done
# Build cargo doc command with custom theme
local cargo_args=(
"doc"
"--no-deps"
"--target-dir" "${OUTPUT_DIR}"
)
# Set RUSTDOCFLAGS environment variable for custom styling
export RUSTDOCFLAGS="--html-in-header ${header_file} --extend-css ${css_file}"
if [[ "$VERBOSE" == true ]]; then
cargo_args+=("-v")
fi
verbose "Running: cargo ${cargo_args[*]}"
verbose "RUSTDOCFLAGS: $RUSTDOCFLAGS"
if [[ "$DRY_RUN" == true ]]; then
warning "Dry run: would execute cargo ${cargo_args[*]}"
else
if [[ "$VERBOSE" == false ]]; then
# Run cargo in background with spinner
(cd "$cargo_dir" && cargo "${cargo_args[@]}") >/dev/null 2>&1 &
local cargo_pid=$!
if ! spinner "$cargo_pid" "Running cargo doc (this may take a while)..."; then
error "cargo doc failed"
return 1
fi
else
# Show cargo output in verbose mode
(cd "$cargo_dir" && cargo "${cargo_args[@]}")
fi
success "Documentation generated successfully"
info "Output directory: ${OUTPUT_DIR}/doc"
fi
else
# Document individual files using rustdoc directly
warning "Individual file documentation not fully implemented yet"
warning "Please provide a Cargo project directory"
return 1
fi
}
##############################################################
# Serve and Open Documentation
##############################################################
serve_and_open_docs() {
local doc_dir="${OUTPUT_DIR}/doc"
local index_file="${doc_dir}/universal_lsp/index.html"
# Find the actual index.html file (might be in different location)
if [[ ! -f "$index_file" ]]; then
# Try to find any index.html in the doc directory
index_file=$(find "$doc_dir" -name "index.html" -type f | head -1)
if [[ -z "$index_file" ]]; then
warning "Could not find index.html in documentation"
index_file="${doc_dir}/index.html"
fi
fi
# Get the relative path from doc_dir to index_file for the URL
local rel_path="${index_file#$doc_dir/}"
local url="http://localhost:${SERVE_PORT}/${rel_path}"
info "Starting HTTP server on port ${SERVE_PORT}..."
echo ""
# Start Python HTTP server in the background
cd "$doc_dir" || {
error "Failed to change to documentation directory: $doc_dir"
return 1
}
# Kill any existing server on this port
if lsof -Pi ":${SERVE_PORT}" -sTCP:LISTEN -t >/dev/null 2>&1; then
warning "Port ${SERVE_PORT} is already in use. Trying to kill existing process..."
lsof -Pi ":${SERVE_PORT}" -sTCP:LISTEN -t | xargs kill -9 2>/dev/null || true
sleep 1
fi
# Start server
python3 -m http.server "$SERVE_PORT" >/dev/null 2>&1 &
local server_pid=$!
# Give the server a moment to start
sleep 1
# Check if server started successfully
if ! kill -0 "$server_pid" 2>/dev/null; then
error "Failed to start HTTP server"
return 1
fi
success "Server started (PID: $server_pid)"
info "Documentation available at: ${CYAN}${url}${RESET}"
# Open in browser if requested
if [[ "$OPEN" == true ]]; then
info "Opening documentation in browser..."
# Try different browser openers
if command -v xdg-open >/dev/null 2>&1; then
xdg-open "$url" >/dev/null 2>&1 &
elif command -v open >/dev/null 2>&1; then
open "$url" >/dev/null 2>&1 &
elif command -v sensible-browser >/dev/null 2>&1; then
sensible-browser "$url" >/dev/null 2>&1 &
else
warning "Could not detect browser opener (xdg-open, open, sensible-browser)"
info "Please open manually: $url"
fi
sleep 1
success "Browser opened"
fi
echo ""
echo "${BOLD}${GREEN}Server running!${RESET}"
echo "${DIM}Press Ctrl+C to stop the server${RESET}"
echo ""
# Wait for Ctrl+C
trap "echo ''; info 'Shutting down server...'; kill $server_pid 2>/dev/null; success 'Server stopped'; exit 0" INT TERM
# Keep the script running
wait "$server_pid"
}
##############################################################
# Cleanup
##############################################################
cleanup() {
if [[ "$VERBOSE" == true ]]; then
verbose "Temporary files preserved in: ${TMP_DIR}"
fi
}
trap cleanup EXIT
##############################################################
# Main Function
##############################################################
main() {
# Parse arguments
while [[ $# -gt 0 ]]; do
case $1 in
-o|--output)
OUTPUT_DIR="$2"
shift 2
;;
-c|--color)
PRIMARY_COLOR=$(validate_color "$2")
shift 2
;;
-s|--style)
STYLE=$(validate_style "$2")
shift 2
;;
--font-sans)
FONT_SANS="$2"
shift 2
;;
--font-mono)
FONT_MONO="$2"
shift 2
;;
--serve)
SERVE=true
shift
;;
--open)
OPEN=true
SERVE=true # Opening implies serving
shift
;;
-p|--port)
SERVE_PORT="$2"
shift 2
;;
-v|--verbose)
VERBOSE=true
shift
;;
-d|--dry-run)
DRY_RUN=true
shift
;;
-h|--help)
show_help
exit 0
;;
-*)
error "Unknown option: $1"
echo "Use -h or --help for usage information"
exit 1
;;
*)
INPUT_FILES+=("$1")
shift
;;
esac
done
# Print banner
echo "${MAGENTA}${BOLD}"
echo "=============================================="
echo " Rust Documentation Generator"
echo "=============================================="
echo "${RESET}"
# Check dependencies
info "Checking dependencies..."
check_dependencies
success "All dependencies found"
# Validate inputs
if [[ ${#INPUT_FILES[@]} -eq 0 ]]; then
error "No input files specified"
echo "Use -h or --help for usage information"
exit 1
fi
# Create temporary directory
mkdir -p "$TMP_DIR"
verbose "Temporary directory: $TMP_DIR"
# Show configuration
echo ""
info "Configuration:"
echo " Primary Color: ${PRIMARY_COLOR}"
echo " Style: ${STYLE}"
echo " Font Sans: ${FONT_SANS}"
echo " Font Mono: ${FONT_MONO}"
echo " Output: ${OUTPUT_DIR}"
echo " Inputs: ${INPUT_FILES[*]}"
echo ""
# Generate color palette
generate_color_palette "$PRIMARY_COLOR" "$STYLE" "${TMP_DIR}/palette.yaml"
# Generate CSS variables
generate_css_variables "$PRIMARY_COLOR" "$STYLE" "${TMP_DIR}/variables.css"
# Render header template
render_header_template "${TMP_DIR}/header.html"
# Consolidate CSS
consolidate_css "${TMP_DIR}/theme-combined.css"
# Run rustdoc
run_rustdoc "${INPUT_FILES[@]}"
echo ""
success "Documentation generation complete!"
if [[ "$DRY_RUN" == false ]]; then
info "View your documentation:"
echo " ${CYAN}${OUTPUT_DIR}/doc/index.html${RESET}"
# Serve and/or open documentation if requested
if [[ "$SERVE" == true ]]; then
echo ""
serve_and_open_docs
fi
fi
echo ""
}
# Run main function
main "$@"

Binary file not shown.

After

Width:  |  Height:  |  Size: 138 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 16 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 13 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 24 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 26 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 39 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 65 KiB

BIN
docs/img/mime_mp4_gif.gif Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 30 KiB

749
jinja_template_render.sh Executable file
View File

@@ -0,0 +1,749 @@
#!/usr/bin/env bash
##############################################################
# Jinja2 Template Renderer - Ninja Edition
# A sophisticated template rendering engine with style
##############################################################
set -uo pipefail
# Note: Using -u and pipefail, but not -e to allow graceful error handling
# Terminal colors using tput
BLACK="" RED="" GREEN="" YELLOW="" BLUE="" MAGENTA="" CYAN="" WHITE="" GRAY=""
BOLD="" DIM="" ITALIC="" UNDERLINE="" RESET=""
if [[ -t 1 ]] && command -v tput >/dev/null 2>&1; then
COLORS=$(tput colors 2>/dev/null || echo 0)
if [[ ${COLORS:-0} -ge 8 ]]; then
BLACK=$(tput setaf 0 2>/dev/null || echo "")
RED=$(tput setaf 1 2>/dev/null || echo "")
GREEN=$(tput setaf 2 2>/dev/null || echo "")
YELLOW=$(tput setaf 3 2>/dev/null || echo "")
BLUE=$(tput setaf 4 2>/dev/null || echo "")
MAGENTA=$(tput setaf 5 2>/dev/null || echo "")
CYAN=$(tput setaf 6 2>/dev/null || echo "")
WHITE=$(tput setaf 7 2>/dev/null || echo "")
GRAY=$(tput setaf 8 2>/dev/null || echo "")
BOLD=$(tput bold 2>/dev/null || echo "")
DIM=$(tput dim 2>/dev/null || echo "")
ITALIC=$(tput sitm 2>/dev/null || echo "")
UNDERLINE=$(tput smul 2>/dev/null || echo "")
RESET=$(tput sgr0 2>/dev/null || echo "")
fi
fi
# Ninja-themed icons (ASCII compatible)
readonly NINJA="[NINJA]"
readonly SHURIKEN="*"
readonly KATANA=">>"
readonly SCROLL="[SCROLL]"
readonly FIRE="[FIRE]"
readonly STAR="*"
readonly CHECK="[OK]"
readonly CROSS="[X]"
readonly ARROW="-->"
readonly LIGHTNING="[!]"
readonly GEAR="[GEAR]"
readonly TARGET="[TARGET]"
# Script configuration
OUTPUT_DIR="${PWD}/output"
TEMPLATES=()
VAR_FILES=()
CLI_VARS=()
VERBOSE=false
PREVIEW=false
WATCH=false
STRICT=false
DRY_RUN=false
# Performance tracking
START_TIME=0
TEMPLATE_COUNT=0
VAR_COUNT=0
##############################################################
# Ninja UI Functions
##############################################################
print_ninja_banner() {
echo "${MAGENTA}${BOLD}"
cat << 'EOF'
TPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPW
Q Q
Q <20> JINJA2 TEMPLATE RENDERER - NINJA EDITION Q
Q Q
Q >w Fast " Powerful " Stealthy Template Magic <20> Q
Q Q
ZPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPP]
EOF
echo "${RESET}"
}
print_ninja_art() {
echo "${CYAN}${DIM}"
cat << 'EOF'
___
__/_ `. .-"""-.
\_,` | \-' / )`-')
"") `"` \ ((`"`
___Y , .'7 /|
(_,___/...-` (_/_/
Silently rendering your templates...
EOF
echo "${RESET}"
}
# Animated ninja message
ninja_say() {
local message="$1"
echo "${CYAN}${NINJA}${RESET} ${BOLD}${message}${RESET}"
}
ninja_success() {
echo "${GREEN}${CHECK} ${NINJA}${RESET} ${GREEN}$*${RESET}"
}
ninja_error() {
echo "${RED}${CROSS} ${NINJA}${RESET} ${RED}$*${RESET}" >&2
}
ninja_warn() {
echo "${YELLOW}${LIGHTNING}${RESET} ${YELLOW}$*${RESET}"
}
ninja_info() {
echo "${BLUE}${SHURIKEN}${RESET} ${BLUE}$*${RESET}"
}
ninja_verbose() {
[[ "$VERBOSE" == true ]] && echo "${DIM} ${ARROW} $*${RESET}"
}
ninja_progress() {
local current=$1
local total=$2
local item=$3
local percent=$((current * 100 / total))
local bar_length=30
local filled=$((bar_length * current / total))
local bar=""
for ((i=0; i<bar_length; i++)); do
if ((i < filled)); then
bar+="${FIRE}"
else
bar+="<22>"
fi
done
echo -ne "\r${CYAN}${GEAR}${RESET} [${bar}] ${percent}% - ${ITALIC}${item}${RESET}"
if ((current == total)); then
echo "" # New line when complete
fi
}
# Typing animation effect
ninja_type() {
local text="$1"
local delay="${2:-0.03}"
for ((i=0; i<${#text}; i++)); do
echo -n "${text:$i:1}"
sleep "$delay"
done
echo ""
}
##############################################################
# Help and Usage
##############################################################
show_help() {
cat << EOF
${BOLD}${MAGENTA}JINJA2 TEMPLATE RENDERER - NINJA EDITION${RESET}
${BOLD}USAGE:${RESET}
$(basename "$0") [OPTIONS] <template> [template...]
${BOLD}DESCRIPTION:${RESET}
A sophisticated Jinja2 template rendering engine with support for
multiple variable sources, glob patterns, and ninja-style operations.
${BOLD}ARGUMENTS:${RESET}
<template> Template file(s) to render (glob patterns supported)
Examples: template.j2, templates/*.j2, **/*.jinja2
${BOLD}OPTIONS:${RESET}
-o, --output DIR Output directory (default: ./output)
-v, --var KEY=VALUE Define template variable (can be used multiple times)
-f, --file FILE Load variables from JSON/YAML file (repeatable)
-V, --verbose Enable verbose ninja commentary
-p, --preview Preview rendered output without saving
-s, --strict Enable strict mode (fail on undefined variables)
-d, --dry-run Perform dry run without writing files
-w, --watch Watch templates and re-render on changes (experimental)
-h, --help Show this legendary scroll of knowledge
${BOLD}VARIABLE SOURCES:${RESET}
Variables are merged in this order (later sources override earlier):
1. YAML files (loaded via yq)
2. JSON files (loaded via jq)
3. CLI variables (-v KEY=VALUE)
${BOLD}EXAMPLES:${RESET}
${DIM}# Render single template with CLI variables${RESET}
$(basename "$0") template.j2 -v name=Ninja -v level=Master
${DIM}# Render multiple templates with YAML config${RESET}
$(basename "$0") templates/*.j2 -f config.yaml -o dist/
${DIM}# Render with multiple variable sources${RESET}
$(basename "$0") app.j2 -f base.yaml -f env.json -v debug=true
${DIM}# Preview without saving${RESET}
$(basename "$0") template.j2 -f vars.yaml --preview --verbose
${DIM}# Strict mode with dry run${RESET}
$(basename "$0") *.j2 -f vars.json --strict --dry-run
${BOLD}TEMPLATE SYNTAX:${RESET}
Jinja2 template example:
${DIM}
Hello {{ name }}!
{% for item in items %}
- {{ item }}
{% endfor %}
{% if debug %}Debug mode enabled{% endif %}
${RESET}
${BOLD}REQUIREMENTS:${RESET}
- Python 3 with jinja2 package
- jq (for JSON parsing)
- yq (for YAML parsing)
${MAGENTA}${NINJA} May your templates be swift and your renders be flawless! ${KATANA}${RESET}
EOF
}
##############################################################
# Variable Collection Functions
##############################################################
# Parse CLI variable in KEY=VALUE format
parse_cli_var() {
local var="$1"
if [[ ! "$var" =~ ^[a-zA-Z_][a-zA-Z0-9_]*=.* ]]; then
ninja_error "Invalid variable format: $var (expected KEY=VALUE)"
return 1
fi
CLI_VARS+=("$var")
ninja_verbose "Added CLI variable: ${CYAN}$var${RESET}"
}
# Load variables from YAML file
load_yaml_vars() {
local file="$1"
if [[ ! -f "$file" ]]; then
ninja_error "YAML file not found: $file"
return 1
fi
if ! command -v yq &> /dev/null; then
ninja_error "yq not found. Install with: pip install yq"
return 1
fi
ninja_verbose "Loading YAML variables from: ${MAGENTA}$file${RESET}"
# Validate YAML syntax
if ! yq -e '.' "$file" &> /dev/null; then
ninja_error "Invalid YAML syntax in: $file"
return 1
fi
VAR_FILES+=("yaml:$file")
}
# Load variables from JSON file
load_json_vars() {
local file="$1"
if [[ ! -f "$file" ]]; then
ninja_error "JSON file not found: $file"
return 1
fi
if ! command -v jq &> /dev/null; then
ninja_error "jq not found. Install with: apt-get install jq"
return 1
fi
ninja_verbose "Loading JSON variables from: ${MAGENTA}$file${RESET}"
# Validate JSON syntax
if ! jq empty "$file" &> /dev/null; then
ninja_error "Invalid JSON syntax in: $file"
return 1
fi
VAR_FILES+=("json:$file")
}
# Build Python dictionary from all variable sources
build_context_dict() {
local python_dict="{"
local first=true
# Process variable files (YAML and JSON)
for var_file in "${VAR_FILES[@]}"; do
local type="${var_file%%:*}"
local file="${var_file#*:}"
if [[ "$type" == "yaml" ]]; then
# Convert YAML to JSON using yq
local json_content
json_content=$(yq -o json '.' "$file" 2>/dev/null) || {
ninja_error "Failed to parse YAML: $file"
return 1
}
# Extract key-value pairs and fix Python boolean casing
while IFS= read -r line; do
if [[ -n "$line" ]]; then
# Fix boolean values: true -> True, false -> False
line="${line//: true/: True}"
line="${line//: false/: False}"
# Convert hyphens to underscores in key names for Jinja2 compatibility
# Extract key and value, replace hyphens in key only
if [[ "$line" =~ ^\"([^\"]+)\":[[:space:]]*(.+)$ ]]; then
local key="${BASH_REMATCH[1]}"
local value="${BASH_REMATCH[2]}"
key="${key//-/_}" # Replace all hyphens with underscores
line="\"$key\": $value"
fi
if [[ "$first" == true ]]; then
first=false
else
python_dict+=", "
fi
python_dict+="$line"
fi
done < <(echo "$json_content" | jq -r 'to_entries | .[] | "\"\(.key)\": \(.value | tojson)"')
elif [[ "$type" == "json" ]]; then
# Process JSON file and fix Python boolean casing
while IFS= read -r line; do
if [[ -n "$line" ]]; then
# Fix boolean values: true -> True, false -> False
line="${line//: true/: True}"
line="${line//: false/: False}"
# Convert hyphens to underscores in key names for Jinja2 compatibility
if [[ "$line" =~ ^\"([^\"]+)\":[[:space:]]*(.+)$ ]]; then
local key="${BASH_REMATCH[1]}"
local value="${BASH_REMATCH[2]}"
key="${key//-/_}" # Replace all hyphens with underscores
line="\"$key\": $value"
fi
if [[ "$first" == true ]]; then
first=false
else
python_dict+=", "
fi
python_dict+="$line"
fi
done < <(jq -r 'to_entries | .[] | "\"\(.key)\": \(.value | tojson)"' "$file")
fi
done
# Process CLI variables (these override file variables)
for var in "${CLI_VARS[@]}"; do
local key="${var%%=*}"
local value="${var#*=}"
if [[ "$first" == true ]]; then
first=false
else
python_dict+=", "
fi
# Auto-detect value type and format for Python
if [[ "$value" =~ ^[0-9]+$ ]]; then
# Integer
python_dict+="\"$key\": $value"
elif [[ "$value" =~ ^[0-9]+\.[0-9]+$ ]]; then
# Float
python_dict+="\"$key\": $value"
elif [[ "$value" == "true" ]] || [[ "$value" == "false" ]]; then
# Boolean
python_dict+="\"$key\": ${value^}"
elif [[ "$value" == "null" ]]; then
# Null
python_dict+="\"$key\": None"
elif [[ "$value" =~ ^\[.*\]$ ]] || [[ "$value" =~ ^\{.*\}$ ]]; then
# JSON array or object
python_dict+="\"$key\": $value"
else
# String (escape quotes)
value="${value//\"/\\\"}"
python_dict+="\"$key\": \"$value\""
fi
done
python_dict+="}"
echo "$python_dict"
}
##############################################################
# Template Rendering Engine
##############################################################
render_template() {
local template_file="$1"
local output_file="$2"
local context="$3"
ninja_verbose "Rendering: ${CYAN}$(basename "$template_file")${RESET}"
# Create Python rendering script
local python_script=$(cat <<'PYTHON_EOF'
import sys
import json
import os
from jinja2 import Environment, FileSystemLoader, StrictUndefined, TemplateError
def render_template(template_path, context_dict, strict=False):
"""Render a Jinja2 template with given context."""
try:
# Setup Jinja2 environment
template_dir = os.path.dirname(os.path.abspath(template_path))
template_name = os.path.basename(template_path)
env_kwargs = {
'loader': FileSystemLoader(template_dir),
'trim_blocks': True,
'lstrip_blocks': True,
'keep_trailing_newline': True,
}
if strict:
env_kwargs['undefined'] = StrictUndefined
env = Environment(**env_kwargs)
# Load and render template
template = env.get_template(template_name)
rendered = template.render(**context_dict)
return rendered, None
except TemplateError as e:
return None, f"Template Error: {e}"
except Exception as e:
return None, f"Unexpected Error: {e}"
if __name__ == '__main__':
if len(sys.argv) != 4:
print("Usage: script.py <template> <context_json> <strict>", file=sys.stderr)
sys.exit(1)
template_path = sys.argv[1]
context_json = sys.argv[2]
strict = sys.argv[3].lower() == 'true'
# Parse context
try:
context = eval(context_json) # Safe in this controlled context
except Exception as e:
print(f"Failed to parse context: {e}", file=sys.stderr)
sys.exit(1)
# Render template
result, error = render_template(template_path, context, strict)
if error:
print(error, file=sys.stderr)
sys.exit(1)
print(result, end='')
PYTHON_EOF
)
# Check if Python and Jinja2 are available
if ! python3 -c "import jinja2" 2>/dev/null; then
ninja_error "Python jinja2 module not found. Install with: pip install jinja2"
return 1
fi
# Render template
local rendered
local strict_flag="false"
[[ "$STRICT" == true ]] && strict_flag="true"
if rendered=$(python3 -c "$python_script" "$template_file" "$context" "$strict_flag" 2>&1); then
if [[ "$PREVIEW" == true ]]; then
# Preview mode - just display
echo ""
echo "${CYAN}${BOLD}PPP Preview: $(basename "$template_file") PPP${RESET}"
echo "$rendered"
echo "${CYAN}${BOLD}PPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPP${RESET}"
echo ""
elif [[ "$DRY_RUN" == true ]]; then
ninja_verbose "Would write to: ${GREEN}$output_file${RESET}"
else
# Write to output file
echo "$rendered" > "$output_file"
ninja_verbose "Wrote: ${GREEN}$output_file${RESET}"
fi
return 0
else
ninja_error "Failed to render $(basename "$template_file")"
echo "${RED}$rendered${RESET}" >&2
return 1
fi
}
##############################################################
# Template Discovery and Processing
##############################################################
discover_templates() {
local pattern="$1"
local found_templates=()
# Check if pattern contains glob characters
if [[ "$pattern" == *"*"* ]] || [[ "$pattern" == *"?"* ]]; then
# Glob expansion
shopt -s nullglob globstar
for file in $pattern; do
if [[ -f "$file" ]]; then
found_templates+=("$file")
fi
done
shopt -u nullglob globstar
else
# Direct file
if [[ -f "$pattern" ]]; then
found_templates+=("$pattern")
else
ninja_error "Template not found: $pattern"
return 1
fi
fi
if [[ ${#found_templates[@]} -eq 0 ]]; then
ninja_error "No templates found matching: $pattern"
return 1
fi
printf '%s\n' "${found_templates[@]}"
}
process_templates() {
local context="$1"
local all_templates=()
# Discover all templates from patterns
ninja_info "Discovering templates with ninja precision..."
for pattern in "${TEMPLATES[@]}"; do
while IFS= read -r template; do
all_templates+=("$template")
done < <(discover_templates "$pattern")
done
TEMPLATE_COUNT=${#all_templates[@]}
if [[ $TEMPLATE_COUNT -eq 0 ]]; then
ninja_error "No templates to render!"
return 1
fi
ninja_success "Found ${BOLD}$TEMPLATE_COUNT${RESET}${GREEN} template(s)${RESET}"
# Create output directory
if [[ "$PREVIEW" == false ]] && [[ "$DRY_RUN" == false ]]; then
mkdir -p "$OUTPUT_DIR"
ninja_verbose "Output directory: ${GREEN}$OUTPUT_DIR${RESET}"
fi
# Render each template
echo ""
ninja_say "Initiating stealth rendering sequence..."
echo ""
local success_count=0
local fail_count=0
for i in "${!all_templates[@]}"; do
local template="${all_templates[$i]}"
local template_name
template_name=$(basename "$template")
local output_name="${template_name%.j2}"
output_name="${output_name%.jinja2}"
output_name="${output_name%.jinja}"
local output_file="$OUTPUT_DIR/$output_name"
# Show progress
ninja_progress $((i + 1)) "$TEMPLATE_COUNT" "$template_name"
# Render template
if render_template "$template" "$output_file" "$context"; then
((success_count++))
else
((fail_count++))
fi
done
echo ""
# Summary
if [[ $fail_count -eq 0 ]]; then
ninja_success "All $TEMPLATE_COUNT templates rendered successfully! ${STAR}"
else
ninja_warn "Rendered: $success_count success, $fail_count failed"
fi
return 0
}
##############################################################
# Main Function
##############################################################
main() {
START_TIME=$(date +%s)
# Parse arguments
while [[ $# -gt 0 ]]; do
case $1 in
-o|--output)
OUTPUT_DIR="$2"
shift 2
;;
-v|--var)
parse_cli_var "$2"
shift 2
;;
-f|--file)
VAR_FILE_ARG="$2"
VAR_FILE_EXT="${VAR_FILE_ARG##*.}"
case "$VAR_FILE_EXT" in
yaml|yml)
load_yaml_vars "$VAR_FILE_ARG"
;;
json)
load_json_vars "$VAR_FILE_ARG"
;;
*)
ninja_error "Unsupported file format: .$VAR_FILE_EXT (use .yaml, .yml, or .json)"
exit 1
;;
esac
shift 2
;;
-V|--verbose)
VERBOSE=true
shift
;;
-p|--preview)
PREVIEW=true
shift
;;
-s|--strict)
STRICT=true
shift
;;
-d|--dry-run)
DRY_RUN=true
shift
;;
-w|--watch)
WATCH=true
shift
;;
-h|--help)
show_help
exit 0
;;
-*)
ninja_error "Unknown option: $1"
echo "Use -h or --help for usage information"
exit 1
;;
*)
TEMPLATES+=("$1")
shift
;;
esac
done
# Show ninja banner
print_ninja_banner
if [[ "$VERBOSE" == true ]]; then
print_ninja_art
fi
# Validate inputs
if [[ ${#TEMPLATES[@]} -eq 0 ]]; then
ninja_error "No templates specified!"
echo "Use -h or --help for usage information"
exit 1
fi
# Build context dictionary
ninja_info "Gathering ninja scrolls and ancient wisdom..."
local context
if ! context=$(build_context_dict); then
exit 1
fi
VAR_COUNT=$(echo "$context" | grep -o '"[^"]*":' | wc -l)
if [[ "$VERBOSE" == true ]]; then
ninja_verbose "Context dictionary built with ${CYAN}$VAR_COUNT${RESET} variables"
echo "${DIM}$context${RESET}"
fi
ninja_success "Loaded ${BOLD}$VAR_COUNT${RESET}${GREEN} variable(s)${RESET}"
# Show mode indicators
[[ "$STRICT" == true ]] && ninja_warn "Strict mode: ${BOLD}ENABLED${RESET}"
[[ "$DRY_RUN" == true ]] && ninja_warn "Dry run mode: ${BOLD}ENABLED${RESET}"
[[ "$PREVIEW" == true ]] && ninja_info "Preview mode: ${BOLD}ENABLED${RESET}"
# Process templates
if ! process_templates "$context"; then
exit 1
fi
# Calculate execution time
local end_time
end_time=$(date +%s)
local duration=$((end_time - START_TIME))
echo ""
ninja_success "Mission complete in ${BOLD}${duration}s${RESET} ${TARGET}"
if [[ "$PREVIEW" == false ]] && [[ "$DRY_RUN" == false ]]; then
ninja_info "Output directory: ${CYAN}${BOLD}$OUTPUT_DIR${RESET}"
fi
echo ""
echo "${MAGENTA}${NINJA} The ninja vanishes into the shadows... ${KATANA}${RESET}"
echo ""
}
# Execute main function
main "$@"

592
mime_mp4_gif.sh Executable file
View File

@@ -0,0 +1,592 @@
#!/usr/bin/env bash
# mime_mp4_gif.sh - Advanced MP4 to Animated GIF converter
# Converts MP4 videos to GIFs with sophisticated keyframe extraction,
# interpolation algorithms, and scheduling distributions
set -euo pipefail
# Default values
KEYFRAMES=10
INPUT_SCHEDULES=1
TRANSITION="linear"
SCHEDULE="uniform"
MAGIC="none"
KEYFRAME_DURATION=100
INPUT_FILE=""
OUTPUT_FILE=""
VERBOSE=false
# Color codes for output
RED='\033[0;31m'
GREEN='\033[0;32m'
YELLOW='\033[1;33m'
BLUE='\033[0;34m'
NC='\033[0m' # No Color
# Available algorithms
TRANSITIONS=("linear" "sinoid" "cubic" "quadratic" "exponential" "bounce" "elastic")
SCHEDULES=("uniform" "front-load" "back-load" "center-peak" "edge-peak" "fibonacci" "golden-ratio")
MAGICS=("none" "psychedelic" "dither-bloom" "edge-glow" "temporal-blur" "chromatic-shift" "vaporwave")
#############################################################################
# Helper Functions
#############################################################################
print_usage() {
cat << EOF
Usage: $(basename "$0") [OPTIONS] INPUT_FILE [OUTPUT_FILE]
Convert MP4 videos to animated GIFs with advanced frame extraction algorithms.
Arguments:
INPUT_FILE Input MP4 video file (required)
OUTPUT_FILE Output GIF file (optional, defaults to INPUT_FILE.gif)
Options:
-k, --keyframes N Number of keyframes to extract (default: 10)
-d, --keyframe-duration MS Duration of each frame in milliseconds (default: 100)
Valid range: 1-30000 ms
Lower values = faster animation
Higher values = slower animation
-i, --input-schedules N Number of input schedules (default: 1)
1 schedule = entire video duration
N schedules = divide video into N segments
-t, --transition TYPE Interpolation function for frame timing
Available: ${TRANSITIONS[*]}
(default: linear)
-s, --schedule TYPE Algorithm to distribute keyframes across schedules
Available: ${SCHEDULES[*]}
(default: uniform)
-m, --magic TYPE Apply magical effects to the GIF
Available: ${MAGICS[*]}
(default: none)
-v, --verbose Enable verbose output
-h, --help Show this help message
Examples:
# Basic conversion with 15 keyframes
$(basename "$0") -k 15 video.mp4
# Fast animation with 50ms per frame
$(basename "$0") -k 20 -d 50 video.mp4
# Slow animation with 500ms per frame
$(basename "$0") -k 10 -d 500 video.mp4
# Use sinusoidal transition with center-peak distribution
$(basename "$0") -t sinoid -s center-peak -k 20 video.mp4
# Apply psychedelic magic with fibonacci distribution
$(basename "$0") -m psychedelic -s fibonacci -k 13 video.mp4 trippy.gif
# Complex: 3 schedules with cubic interpolation and edge glow
$(basename "$0") -i 3 -t cubic -s front-load -m edge-glow -k 30 video.mp4
EOF
}
log_info() {
echo -e "${BLUE}[INFO]${NC} $*"
}
log_success() {
echo -e "${GREEN}[SUCCESS]${NC} $*"
}
log_warning() {
echo -e "${YELLOW}[WARNING]${NC} $*"
}
log_error() {
echo -e "${RED}[ERROR]${NC} $*" >&2
}
verbose_log() {
if [[ "$VERBOSE" == "true" ]]; then
log_info "$@"
fi
}
validate_enum() {
local value="$1"
local array_name="$2"
local -n arr=$array_name
for item in "${arr[@]}"; do
if [[ "$value" == "$item" ]]; then
return 0
fi
done
return 1
}
#############################################################################
# Mathematical Functions
#############################################################################
# Calculate transition weight based on interpolation type
# Input: progress (0.0 to 1.0), Returns: weighted value (0.0 to 1.0)
calculate_transition() {
local progress="$1"
local type="$2"
case "$type" in
linear)
echo "$progress"
;;
sinoid)
# Smooth sinusoidal easing
awk -v p="$progress" 'BEGIN { print (1 - cos(p * 3.14159265359)) / 2 }'
;;
cubic)
# Cubic easing in-out
awk -v p="$progress" 'BEGIN {
if (p < 0.5)
print 4 * p * p * p;
else
print 1 - ((-2 * p + 2) ^ 3) / 2;
}'
;;
quadratic)
# Quadratic easing
awk -v p="$progress" 'BEGIN {
if (p < 0.5)
print 2 * p * p;
else
print 1 - ((-2 * p + 2) ^ 2) / 2;
}'
;;
exponential)
# Exponential easing
awk -v p="$progress" 'BEGIN {
if (p == 0) print 0;
else if (p == 1) print 1;
else if (p < 0.5) print (2 ^ (20 * p - 10)) / 2;
else print (2 - (2 ^ (-20 * p + 10))) / 2;
}'
;;
bounce)
# Bouncing effect
awk -v p="$progress" 'BEGIN {
n1 = 7.5625; d1 = 2.75;
x = 1 - p;
if (x < 1/d1) result = n1 * x * x;
else if (x < 2/d1) { x -= 1.5/d1; result = n1 * x * x + 0.75; }
else if (x < 2.5/d1) { x -= 2.25/d1; result = n1 * x * x + 0.9375; }
else { x -= 2.625/d1; result = n1 * x * x + 0.984375; }
print 1 - result;
}'
;;
elastic)
# Elastic spring effect
awk -v p="$progress" 'BEGIN {
c4 = (2 * 3.14159265359) / 3;
if (p == 0) print 0;
else if (p == 1) print 1;
else if (p < 0.5) print -(2 ^ (20 * p - 10) * sin((20 * p - 11.125) * c4)) / 2;
else print (2 ^ (-20 * p + 10) * sin((20 * p - 11.125) * c4)) / 2 + 1;
}'
;;
*)
echo "$progress"
;;
esac
}
# Generate keyframe distribution based on schedule type
generate_schedule_distribution() {
local num_frames="$1"
local schedule_type="$2"
local -n result_array=$3
case "$schedule_type" in
uniform)
for ((i=0; i<num_frames; i++)); do
result_array[$i]=$(awk -v i="$i" -v n="$num_frames" 'BEGIN { print i / (n - 1) }')
done
;;
front-load)
# More frames at the beginning
for ((i=0; i<num_frames; i++)); do
local t=$(awk -v i="$i" -v n="$num_frames" 'BEGIN { print i / (n - 1) }')
result_array[$i]=$(awk -v t="$t" 'BEGIN { print t * t }')
done
;;
back-load)
# More frames at the end
for ((i=0; i<num_frames; i++)); do
local t=$(awk -v i="$i" -v n="$num_frames" 'BEGIN { print i / (n - 1) }')
result_array[$i]=$(awk -v t="$t" 'BEGIN { print 1 - (1 - t) * (1 - t) }')
done
;;
center-peak)
# More frames in the middle
for ((i=0; i<num_frames; i++)); do
local t=$(awk -v i="$i" -v n="$num_frames" 'BEGIN { print i / (n - 1) }')
result_array[$i]=$(awk -v t="$t" 'BEGIN { print 1 - 4 * (t - 0.5) * (t - 0.5) }')
result_array[$i]=$(awk -v val="${result_array[$i]}" -v t="$t" 'BEGIN { print t }')
done
;;
edge-peak)
# More frames at start and end
for ((i=0; i<num_frames; i++)); do
local t=$(awk -v i="$i" -v n="$num_frames" 'BEGIN { print i / (n - 1) }')
result_array[$i]=$(awk -v t="$t" 'BEGIN { print 4 * (t - 0.5) * (t - 0.5) }')
result_array[$i]=$(awk -v val="${result_array[$i]}" -v t="$t" 'BEGIN { print t }')
done
;;
fibonacci)
# Fibonacci sequence distribution
local fib=(1 1)
for ((i=2; i<num_frames; i++)); do
fib[$i]=$((fib[i-1] + fib[i-2]))
done
local sum=0
for val in "${fib[@]}"; do
((sum += val))
done
local cumsum=0
for ((i=0; i<num_frames; i++)); do
((cumsum += fib[i]))
result_array[$i]=$(awk -v c="$cumsum" -v s="$sum" 'BEGIN { print c / s }')
done
;;
golden-ratio)
# Golden ratio distribution
local phi=1.618033988749895
for ((i=0; i<num_frames; i++)); do
result_array[$i]=$(awk -v i="$i" -v n="$num_frames" -v phi="$phi" 'BEGIN {
print ((i * phi) - int(i * phi))
}')
done
# Sort the array for monotonic distribution
IFS=$'\n' result_array=($(sort -n <<<"${result_array[*]}"))
;;
*)
# Default to uniform
for ((i=0; i<num_frames; i++)); do
result_array[$i]=$(awk -v i="$i" -v n="$num_frames" 'BEGIN { print i / (n - 1) }')
done
;;
esac
}
#############################################################################
# Video Processing Functions
#############################################################################
get_video_duration() {
local file="$1"
ffprobe -v error -show_entries format=duration -of default=noprint_wrappers=1:nokey=1 "$file"
}
extract_frames() {
local input="$1"
local duration="$2"
local -n ts_ref=$3
local temp_dir="$4"
verbose_log "Extracting ${#ts_ref[@]} frames from video..."
for i in "${!ts_ref[@]}"; do
local time="${ts_ref[$i]}"
verbose_log " Frame $((i+1)): ${time}s"
ffmpeg -v quiet -ss "$time" -i "$input" -vframes 1 \
-vf "scale=480:-1:flags=lanczos" \
"${temp_dir}/frame_$(printf "%04d" "$i").png" 2>/dev/null
done
}
apply_magic_effects() {
local magic_type="$1"
local temp_dir="$2"
if [[ "$magic_type" == "none" ]]; then
return 0
fi
verbose_log "Applying magic effect: $magic_type"
case "$magic_type" in
psychedelic)
for frame in "$temp_dir"/*.png; do
ffmpeg -v quiet -i "$frame" -vf "hue=s=3:h=sin(2*PI*t)*360" \
"${frame}.tmp.png" 2>/dev/null && mv "${frame}.tmp.png" "$frame"
done
;;
dither-bloom)
for frame in "$temp_dir"/*.png; do
ffmpeg -v quiet -i "$frame" -vf "format=gbrp,split[a][b],[a]negate[c],[b][c]blend=all_mode=xor,noise=alls=20:allf=t" \
"${frame}.tmp.png" 2>/dev/null && mv "${frame}.tmp.png" "$frame"
done
;;
edge-glow)
for frame in "$temp_dir"/*.png; do
ffmpeg -v quiet -i "$frame" -vf "edgedetect=low=0.1:high=0.3,negate,hue=s=2" \
"${temp_dir}/edges_$(basename "$frame")"
ffmpeg -v quiet -i "$frame" -i "${temp_dir}/edges_$(basename "$frame")" \
-filter_complex "[0:v][1:v]blend=all_mode=addition:all_opacity=0.5" \
"${frame}.tmp.png" 2>/dev/null && mv "${frame}.tmp.png" "$frame"
rm "${temp_dir}/edges_$(basename "$frame")"
done
;;
temporal-blur)
# Create motion blur effect
local frames=("$temp_dir"/*.png)
for i in "${!frames[@]}"; do
local prev_idx=$((i > 0 ? i - 1 : 0))
local next_idx=$((i < ${#frames[@]} - 1 ? i + 1 : ${#frames[@]} - 1))
ffmpeg -v quiet -i "${frames[$prev_idx]}" -i "${frames[$i]}" -i "${frames[$next_idx]}" \
-filter_complex "[0:v][1:v][2:v]blend=all_mode=average" \
"${frames[$i]}.tmp.png" 2>/dev/null && mv "${frames[$i]}.tmp.png" "${frames[$i]}"
done
;;
chromatic-shift)
for frame in "$temp_dir"/*.png; do
ffmpeg -v quiet -i "$frame" -vf "rgbashift=rh=5:bh=-5" \
"${frame}.tmp.png" 2>/dev/null && mv "${frame}.tmp.png" "$frame"
done
;;
vaporwave)
for frame in "$temp_dir"/*.png; do
ffmpeg -v quiet -i "$frame" -vf "curves=vintage,hue=h=300:s=1.5,eq=saturation=1.5:contrast=1.2" \
"${frame}.tmp.png" 2>/dev/null && mv "${frame}.tmp.png" "$frame"
done
;;
esac
}
create_gif() {
local temp_dir="$1"
local output="$2"
local frame_delay="$3"
verbose_log "Creating animated GIF with ${frame_delay}ms per frame..."
# Convert milliseconds to centiseconds (GIF delay unit)
local delay_cs
delay_cs=$(awk -v ms="$frame_delay" 'BEGIN { print int(ms / 10) }')
# Ensure minimum delay of 1 centisecond
if [[ $delay_cs -lt 1 ]]; then
delay_cs=1
fi
# Calculate input framerate (frames are read at this rate)
# For GIF delay, we want 1000ms / frame_delay fps
local fps
fps=$(awk -v ms="$frame_delay" 'BEGIN { printf "%.2f", 1000.0 / ms }')
verbose_log "Frame delay: ${delay_cs} centiseconds (${frame_delay}ms), FPS: ${fps}"
# Generate palette for better color quality
ffmpeg -v error -pattern_type glob -i "${temp_dir}/frame_*.png" \
-vf "scale=480:-1:flags=lanczos,palettegen=stats_mode=diff" \
-y "${temp_dir}/palette.png"
# Create GIF using palette with specified frame delay
ffmpeg -v error -framerate "$fps" -pattern_type glob -i "${temp_dir}/frame_*.png" -i "${temp_dir}/palette.png" \
-filter_complex "[0:v]scale=480:-1:flags=lanczos[scaled];[scaled][1:v]paletteuse=dither=bayer:bayer_scale=5" \
-gifflags +transdiff -y "$output"
}
#############################################################################
# Main Processing
#############################################################################
process_video() {
local input="$INPUT_FILE"
local output="$OUTPUT_FILE"
# Validate input file
if [[ ! -f "$input" ]]; then
log_error "Input file not found: $input"
exit 1
fi
# Get video duration
local duration
duration=$(get_video_duration "$input")
verbose_log "Video duration: ${duration}s"
# Calculate schedule duration
local schedule_duration
schedule_duration=$(awk -v d="$duration" -v s="$INPUT_SCHEDULES" 'BEGIN { print d / s }')
verbose_log "Schedule duration: ${schedule_duration}s (${INPUT_SCHEDULES} schedule(s))"
# Generate frame distribution
local -a distribution
generate_schedule_distribution "$KEYFRAMES" "$SCHEDULE" distribution
verbose_log "Using schedule: $SCHEDULE"
verbose_log "Using transition: $TRANSITION"
# Calculate actual timestamps with transition function
local -a timestamps
for i in "${!distribution[@]}"; do
local base_time="${distribution[$i]}"
local weighted_time
weighted_time=$(calculate_transition "$base_time" "$TRANSITION")
# Map to video duration considering input schedules
local actual_time
actual_time=$(awk -v w="$weighted_time" -v d="$duration" 'BEGIN { print w * d }')
# Ensure we don't exceed video duration
timestamps[$i]=$(awk -v t="$actual_time" -v d="$duration" 'BEGIN {
if (t > d) print d;
else print t;
}')
done
# Create temporary directory
local temp_dir
temp_dir=$(mktemp -d)
trap "rm -rf '$temp_dir'" EXIT
# Extract frames
extract_frames "$input" "$duration" timestamps "$temp_dir"
# Apply magic effects
apply_magic_effects "$MAGIC" "$temp_dir"
# Create GIF with specified frame duration
create_gif "$temp_dir" "$output" "$KEYFRAME_DURATION"
log_success "GIF created successfully: $output"
# Show file size
local size
size=$(du -h "$output" | cut -f1)
log_info "Output size: $size"
}
#############################################################################
# Command Line Parsing
#############################################################################
parse_arguments() {
while [[ $# -gt 0 ]]; do
case "$1" in
-k|--keyframes)
KEYFRAMES="$2"
shift 2
;;
-d|--keyframe-duration)
KEYFRAME_DURATION="$2"
shift 2
;;
-i|--input-schedules)
INPUT_SCHEDULES="$2"
shift 2
;;
-t|--transition)
TRANSITION="$2"
if ! validate_enum "$TRANSITION" TRANSITIONS; then
log_error "Invalid transition type: $TRANSITION"
log_error "Available: ${TRANSITIONS[*]}"
exit 1
fi
shift 2
;;
-s|--schedule)
SCHEDULE="$2"
if ! validate_enum "$SCHEDULE" SCHEDULES; then
log_error "Invalid schedule type: $SCHEDULE"
log_error "Available: ${SCHEDULES[*]}"
exit 1
fi
shift 2
;;
-m|--magic)
MAGIC="$2"
if ! validate_enum "$MAGIC" MAGICS; then
log_error "Invalid magic type: $MAGIC"
log_error "Available: ${MAGICS[*]}"
exit 1
fi
shift 2
;;
-v|--verbose)
VERBOSE=true
shift
;;
-h|--help)
print_usage
exit 0
;;
-*)
log_error "Unknown option: $1"
print_usage
exit 1
;;
*)
if [[ -z "$INPUT_FILE" ]]; then
INPUT_FILE="$1"
elif [[ -z "$OUTPUT_FILE" ]]; then
OUTPUT_FILE="$1"
else
log_error "Too many arguments"
print_usage
exit 1
fi
shift
;;
esac
done
# Validate required arguments
if [[ -z "$INPUT_FILE" ]]; then
log_error "Input file is required"
print_usage
exit 1
fi
# Set default output file
if [[ -z "$OUTPUT_FILE" ]]; then
OUTPUT_FILE="${INPUT_FILE%.*}.gif"
fi
# Validate numeric arguments
if ! [[ "$KEYFRAMES" =~ ^[0-9]+$ ]] || [[ "$KEYFRAMES" -lt 2 ]]; then
log_error "Keyframes must be a positive integer >= 2"
exit 1
fi
if ! [[ "$KEYFRAME_DURATION" =~ ^[0-9]+$ ]] || [[ "$KEYFRAME_DURATION" -lt 1 ]] || [[ "$KEYFRAME_DURATION" -gt 30000 ]]; then
log_error "Keyframe duration must be an integer between 1 and 30000 milliseconds"
exit 1
fi
if ! [[ "$INPUT_SCHEDULES" =~ ^[0-9]+$ ]] || [[ "$INPUT_SCHEDULES" -lt 1 ]]; then
log_error "Input schedules must be a positive integer >= 1"
exit 1
fi
}
#############################################################################
# Entry Point
#############################################################################
main() {
parse_arguments "$@"
log_info "Starting MP4 to GIF conversion..."
log_info "Configuration:"
log_info " Input: $INPUT_FILE"
log_info " Output: $OUTPUT_FILE"
log_info " Keyframes: $KEYFRAMES"
log_info " Frame Duration: ${KEYFRAME_DURATION}ms"
log_info " Schedules: $INPUT_SCHEDULES"
log_info " Transition: $TRANSITION"
log_info " Schedule: $SCHEDULE"
log_info " Magic: $MAGIC"
process_video
}
# Run main function
main "$@"