Last active
March 12, 2026 13:18
-
-
Save abusayed0206/4f4efaf6df801ec95dd252b04bc2e074 to your computer and use it in GitHub Desktop.
Kriti Bangla Font Installer
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| #!/usr/bin/env bash | |
| # ───────────────────────────────────────────────────────────── | |
| # কৃতি — বাংলা ফন্ট ইন্সটলার (Kriti Bengali Font Installer) | |
| # https://kriti.app | |
| # ───────────────────────────────────────────────────────────── | |
| # | |
| # Quick install — downloads & installs all free Bengali fonts: | |
| # curl -fsSL https://gist.githubusercontent.com/abusayed0206/4f4efaf6df801ec95dd252b04bc2e074/raw/kriti.sh | bash | |
| # wget -qO- https://gist.githubusercontent.com/abusayed0206/4f4efaf6df801ec95dd252b04bc2e074/raw/kriti.sh | bash | |
| # | |
| # Options (when saved as a file): | |
| # --dir <path> Install to a custom directory | |
| # --force Re-download even if fonts already exist | |
| # --list List available fonts without installing | |
| # --help Show this help | |
| # | |
| # ───────────────────────────────────────────────────────────── | |
| set -euo pipefail | |
| # ── Configuration ──────────────────────────────────────────── | |
| SITE="${KRITI_SITE:-https://kriti.app}" | |
| METADATA_URL="${SITE}/metadata/index.json" | |
| # ── OS detection & default font directory ──────────────────── | |
| OS="$(uname -s 2>/dev/null || echo Unknown)" | |
| case "$OS" in | |
| Linux*) FONT_DIR="${HOME}/.local/share/fonts/kriti" ;; | |
| Darwin*) FONT_DIR="${HOME}/Library/Fonts/kriti" ;; | |
| *) | |
| printf "Error: Unsupported OS '%s'. This script supports Linux and macOS.\n" "$OS" >&2 | |
| exit 1 | |
| ;; | |
| esac | |
| # ── Default flags ───────────────────────────────────────────── | |
| FORCE=0 | |
| LIST_ONLY=0 | |
| # ── Parse arguments (only when run as a file, not piped) ───── | |
| if [[ "${BASH_SOURCE[0]:-}" == "${0}" ]]; then | |
| while [[ $# -gt 0 ]]; do | |
| case "$1" in | |
| --dir) | |
| FONT_DIR="${2:?'--dir requires a path'}"; shift 2 ;; | |
| --force) | |
| FORCE=1; shift ;; | |
| --list) | |
| LIST_ONLY=1; shift ;; | |
| --help|-h) | |
| sed -n '2,15p' "$0" | sed 's/^# \{0,1\}//' | |
| exit 0 ;; | |
| *) | |
| printf "Unknown option: %s\n" "$1" >&2; exit 1 ;; | |
| esac | |
| done | |
| fi | |
| # ── Colour helpers ──────────────────────────────────────────── | |
| if [ -t 1 ] && [ "${NO_COLOR:-}" = "" ]; then | |
| C_GREEN='\033[0;32m'; C_CYAN='\033[0;36m'; C_YELLOW='\033[1;33m' | |
| C_RED='\033[0;31m'; C_BOLD='\033[1m'; C_DIM='\033[2m' | |
| C_RESET='\033[0m' | |
| else | |
| C_GREEN=''; C_CYAN=''; C_YELLOW=''; C_RED='' | |
| C_BOLD=''; C_DIM=''; C_RESET='' | |
| fi | |
| info() { printf " ${C_CYAN}→${C_RESET} %s\n" "$*"; } | |
| success() { printf " ${C_GREEN}✔${C_RESET} %s\n" "$*"; } | |
| warn() { printf " ${C_YELLOW}⚠${C_RESET} %s\n" "$*"; } | |
| error() { printf " ${C_RED}✖${C_RESET} %s\n" "$*" >&2; } | |
| dim() { printf " ${C_DIM}%s${C_RESET}\n" "$*"; } | |
| # ── Detect HTTP client ──────────────────────────────────────── | |
| if command -v curl &>/dev/null; then | |
| http_get() { curl -fsSL "$1"; } | |
| http_download() { curl -fsSL -o "$2" "$1"; } | |
| elif command -v wget &>/dev/null; then | |
| http_get() { wget -qO- "$1"; } | |
| http_download() { wget -q -O "$2" "$1"; } | |
| else | |
| error "Neither 'curl' nor 'wget' is available. Please install one." | |
| exit 1 | |
| fi | |
| # ── JSON parser: emit one line per font file to download ────── | |
| # Output format (tab-separated): <filename> <display-name> <full-url> | |
| # | |
| # Takes a JSON file path as $1. Handles both single-style fonts | |
| # (downloadUrl) and multi-style families (fontStyles[].url). | |
| parse_font_list() { | |
| local data_file="$1" | |
| local SITE_ARG="$SITE" | |
| # Prefer python3, fall back to python, then jq | |
| # NOTE: we pass the JSON file as argv[2] so the heredoc can supply | |
| # the Python script via stdin without conflicting with JSON data. | |
| if command -v python3 &>/dev/null; then | |
| python3 - "$SITE_ARG" "$data_file" <<'PYEOF' | |
| import json, sys | |
| site = sys.argv[1] | |
| seen = set() | |
| try: | |
| with open(sys.argv[2]) as fh: | |
| data = json.load(fh) | |
| except (json.JSONDecodeError, OSError) as e: | |
| print("JSON parse error: " + str(e), file=sys.stderr) | |
| sys.exit(1) | |
| for f in data: | |
| if f.get("canDistribute") is not True: | |
| continue | |
| font_name = f.get("name") or f.get("slug", "unknown") | |
| styles = f.get("fontStyles") or [] | |
| if styles: | |
| # Multi-style family: download each non-empty style URL | |
| for s in styles: | |
| url = (s.get("url") or "").strip() | |
| if not url: | |
| continue | |
| full_url = url if url.startswith("http") else site + url | |
| if full_url in seen: | |
| continue | |
| seen.add(full_url) | |
| style_label = (font_name + " " + s.get("name", "")).strip() | |
| filename = s.get("fileName") or url.rsplit("/", 1)[-1] | |
| print(filename + "\t" + style_label + "\t" + full_url) | |
| else: | |
| # Single-style font | |
| url = (f.get("downloadUrl") or "").strip() | |
| if not url: | |
| continue | |
| full_url = url if url.startswith("http") else site + url | |
| if full_url in seen: | |
| continue | |
| seen.add(full_url) | |
| filename = f.get("fileName") or url.rsplit("/", 1)[-1] | |
| print(filename + "\t" + font_name + "\t" + full_url) | |
| PYEOF | |
| elif command -v python &>/dev/null; then | |
| python - "$SITE_ARG" "$data_file" <<'PYEOF' | |
| import json, sys | |
| site = sys.argv[1] | |
| seen = set() | |
| with open(sys.argv[2]) as fh: | |
| data = json.load(fh) | |
| for f in data: | |
| if f.get("canDistribute") is not True: | |
| continue | |
| font_name = f.get("name") or f.get("slug", "unknown") | |
| styles = f.get("fontStyles") or [] | |
| if styles: | |
| for s in styles: | |
| url = (s.get("url") or "").strip() | |
| if not url: | |
| continue | |
| full_url = url if url.startswith("http") else site + url | |
| if full_url in seen: | |
| continue | |
| seen.add(full_url) | |
| style_label = (font_name + " " + s.get("name", "")).strip() | |
| filename = s.get("fileName") or url.rsplit("/", 1)[-1] | |
| print(filename + "\t" + style_label + "\t" + full_url) | |
| else: | |
| url = (f.get("downloadUrl") or "").strip() | |
| if not url: | |
| continue | |
| full_url = url if url.startswith("http") else site + url | |
| if full_url in seen: | |
| continue | |
| seen.add(full_url) | |
| filename = f.get("fileName") or url.rsplit("/", 1)[-1] | |
| print(filename + "\t" + font_name + "\t" + full_url) | |
| PYEOF | |
| elif command -v jq &>/dev/null; then | |
| jq -r --arg site "$SITE_ARG" ' | |
| .[] | select(.canDistribute == true) | | |
| . as $f | | |
| if (.fontStyles // [] | length) > 0 then | |
| .fontStyles[] | select((.url // "") != "") | | |
| [ | |
| (.fileName // (.url | split("/") | last)), | |
| (($f.name // $f.slug) + " " + (.name // "") | ltrimstr(" ") | rtrimstr(" ")), | |
| (if (.url | startswith("http")) then .url else ($site + .url) end) | |
| ] | @tsv | |
| else | |
| select((.downloadUrl // "") != "") | | |
| [ | |
| (.fileName // (.downloadUrl | split("/") | last)), | |
| (.name // .slug), | |
| (if (.downloadUrl | startswith("http")) then .downloadUrl else ($site + .downloadUrl) end) | |
| ] | @tsv | |
| end | |
| ' "$data_file" | |
| else | |
| error "No JSON parser found. Please install python3 or jq." | |
| exit 1 | |
| fi | |
| } | |
| # ── Banner ──────────────────────────────────────────────────── | |
| printf "\n" | |
| printf " ${C_BOLD}কৃতি — বাংলা ফন্ট ইন্সটলার${C_RESET}\n" | |
| printf " Kriti Bengali Font Installer · %s\n" "$SITE" | |
| printf "\n" | |
| info "Platform : $OS" | |
| info "Font dir : $FONT_DIR" | |
| printf "\n" | |
| # ── Fetch metadata ──────────────────────────────────────────── | |
| info "Fetching font list from ${SITE}…" | |
| METADATA="$(http_get "$METADATA_URL")" || { | |
| error "Could not fetch metadata. Check your connection or try again later." | |
| printf " URL: %s\n" "$METADATA_URL" >&2 | |
| exit 1 | |
| } | |
| if [ -z "$METADATA" ]; then | |
| error "Metadata response was empty." | |
| printf " URL: %s\n" "$METADATA_URL" >&2 | |
| printf " The site may not be reachable. Try: curl -I %s\n" "$METADATA_URL" >&2 | |
| exit 1 | |
| fi | |
| first_char="$(printf '%s' "$METADATA" | tr -d '[:space:]' | cut -c1)" | |
| if [ "$first_char" != "[" ] && [ "$first_char" != "{" ]; then | |
| error "Unexpected response from server (not JSON)." | |
| printf " URL: %s\n" "$METADATA_URL" >&2 | |
| printf " First 200 chars: %s\n" "$(printf '%s' "$METADATA" | head -c 200)" >&2 | |
| exit 1 | |
| fi | |
| TMPJSON="$(mktemp)" | |
| TMPRES="$(mktemp -d)" | |
| _cleanup() { rm -rf "$TMPRES"; rm -f "$TMPJSON"; } | |
| _abort() { printf "\n"; warn "Interrupted — cleaning up…"; kill 0 2>/dev/null; _cleanup; exit 130; } | |
| trap '_cleanup' EXIT | |
| trap '_abort' INT TERM | |
| printf '%s' "$METADATA" > "$TMPJSON" | |
| FONT_LIST="$(parse_font_list "$TMPJSON")" | |
| TOTAL="$(printf '%s\n' "$FONT_LIST" | grep -c . 2>/dev/null || echo 0)" | |
| if [ "$TOTAL" -eq 0 ]; then | |
| warn "No distributable fonts found in the metadata." | |
| exit 0 | |
| fi | |
| success "Found ${TOTAL} free Bengali font files" | |
| printf "\n" | |
| # ── List-only mode ──────────────────────────────────────────── | |
| if [ "$LIST_ONLY" -eq 1 ]; then | |
| printf " %-10s %s\n" "File" "Name" | |
| printf " %-10s %s\n" "----------" "----" | |
| while IFS=$'\t' read -r filename name url; do | |
| printf " %-40s %s\n" "$filename" "$name" | |
| done <<< "$FONT_LIST" | |
| printf "\n" | |
| exit 0 | |
| fi | |
| # ── Create font directory ───────────────────────────────────── | |
| if ! mkdir -p "$FONT_DIR" 2>/dev/null; then | |
| error "Cannot create font directory: $FONT_DIR" | |
| error "Try: mkdir -p \"$FONT_DIR\" and re-run, or use --dir to pick another location." | |
| exit 1 | |
| fi | |
| # ── Download fonts ──────────────────────────────────────────── | |
| MAX_JOBS="${KRITI_JOBS:-8}" | |
| INSTALLED=0 | |
| SKIPPED=0 | |
| FAILED=0 | |
| FAILED_NAMES=() | |
| PENDING_FILE="${TMPRES}/pending.tsv" | |
| # Separate already-installed fonts from ones that need downloading | |
| while IFS=$'\t' read -r filename name url; do | |
| if [ -f "${FONT_DIR}/${filename}" ] && [ "$FORCE" -eq 0 ]; then | |
| SKIPPED=$((SKIPPED + 1)) | |
| else | |
| printf '%s\t%s\t%s\n' "$filename" "$name" "$url" >> "$PENDING_FILE" | |
| fi | |
| done <<< "$FONT_LIST" | |
| PENDING_COUNT=0 | |
| [ -f "$PENDING_FILE" ] && PENDING_COUNT="$(wc -l < "$PENDING_FILE" | tr -d ' ')" | |
| if [ "$PENDING_COUNT" -gt 0 ]; then | |
| info "Downloading ${PENDING_COUNT} fonts (${MAX_JOBS} parallel)…" | |
| printf "%s" " 0 / ${PENDING_COUNT}" | |
| IDX=0 | |
| jobs_running=0 | |
| while IFS=$'\t' read -r filename name url; do | |
| dest="${FONT_DIR}/${filename}" | |
| IDX=$((IDX + 1)) | |
| rf="${TMPRES}/${IDX}.r" | |
| ( | |
| if http_download "$url" "$dest" 2>/dev/null; then | |
| printf 'OK\t%s\n' "$name" > "$rf" | |
| else | |
| rm -f "$dest" | |
| printf 'FAIL\t%s\n' "$name" > "$rf" | |
| fi | |
| ) & | |
| jobs_running=$((jobs_running + 1)) | |
| if [ "$jobs_running" -ge "$MAX_JOBS" ]; then | |
| wait | |
| jobs_running=0 | |
| done_count="$(ls -1 "${TMPRES}"/*.r 2>/dev/null | wc -l | tr -d ' ')" | |
| printf "\r %d / %d" "$done_count" "$PENDING_COUNT" | |
| fi | |
| done < "$PENDING_FILE" | |
| wait | |
| printf "\r %d / %d downloaded \n\n" "$PENDING_COUNT" "$PENDING_COUNT" | |
| # Collect results | |
| for rf in "${TMPRES}"/*.r; do | |
| [ -f "$rf" ] || continue | |
| IFS=$'\t' read -r _status _rname < "$rf" | |
| if [ "$_status" = "OK" ]; then | |
| INSTALLED=$((INSTALLED + 1)) | |
| else | |
| FAILED=$((FAILED + 1)) | |
| FAILED_NAMES+=("$_rname") | |
| fi | |
| done | |
| fi | |
| printf "\n" | |
| # ── Summary ─────────────────────────────────────────────────── | |
| success "Installed : ${INSTALLED} font file(s)" | |
| [ "$SKIPPED" -gt 0 ] && \ | |
| dim "Skipped : ${SKIPPED} already installed (run with --force to re-download)" | |
| [ "$FAILED" -gt 0 ] && \ | |
| warn "Failed : ${FAILED} font file(s)" | |
| for n in "${FAILED_NAMES[@]:-}"; do | |
| [ -n "$n" ] && dim " – $n" | |
| done | |
| # ── Refresh font cache ──────────────────────────────────────── | |
| if [ "$INSTALLED" -gt 0 ]; then | |
| printf "\n" | |
| info "Refreshing font cache…" | |
| if command -v fc-cache &>/dev/null; then | |
| fc-cache -f "$FONT_DIR" && success "Font cache updated (fc-cache)" | |
| elif [ "$OS" = "Darwin" ]; then | |
| success "Fonts installed — restart apps to see new fonts" | |
| fi | |
| fi | |
| printf "\n" | |
| printf " ${C_BOLD}Done!${C_RESET} ${INSTALLED} Bengali font files installed to:\n" | |
| printf " ${C_DIM}%s${C_RESET}\n\n" "$FONT_DIR" |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment