Skip to content

Instantly share code, notes, and snippets.

@abusayed0206
Last active March 12, 2026 13:18
Show Gist options
  • Select an option

  • Save abusayed0206/4f4efaf6df801ec95dd252b04bc2e074 to your computer and use it in GitHub Desktop.

Select an option

Save abusayed0206/4f4efaf6df801ec95dd252b04bc2e074 to your computer and use it in GitHub Desktop.
Kriti Bangla Font Installer
#!/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