Created
April 28, 2026 01:56
-
-
Save Thomashighbaugh/aebd0465b3be540f4a97df646aabb930 to your computer and use it in GitHub Desktop.
Justfile + icon font -> HTML page allowing you preview and copy the icon from the comfort of your browser. Also creates a odepointd list and markdown file.
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 | |
| if [ "$#" -lt 1 ]; then | |
| echo "Usage: $0 fontfile1.ttf [fontfile2.otf ...]" | |
| echo | |
| exit 1 | |
| fi | |
| # Check for required commands | |
| command -v ttx >/dev/null 2>&1 || { echo >&2 "Error: ttx (fonttools) is required but not installed. Install with: pip install fonttools"; exit 1; } | |
| command -v xmllint >/dev/null 2>&1 || { echo >&2 "Error: xmllint is required but not installed."; exit 1; } | |
| for fontfile in "$@"; do | |
| if [[ ! -f "$fontfile" ]]; then | |
| echo "File not found: $fontfile" | |
| continue | |
| fi | |
| echo "Processing font: $fontfile" | |
| # Convert font to XML temporarily | |
| ttx -o temp.ttx "$fontfile" >/dev/null 2>&1 | |
| if [ $? -ne 0 ]; then | |
| echo "Failed to convert font to XML using ttx." | |
| continue | |
| fi | |
| # Extract cmap info with xmllint | |
| # The cmap table contains unicode to glyph mappings. | |
| # We parse the <map> elements inside the <cmap_format_4> or <cmap_format_12> subtables. | |
| echo "Unicode Codepoints and Glyph Names:" | |
| # Extract codepoint + name pairs: | |
| xmllint --xpath '//cmap/cmap_format_12/map | //cmap/cmap_format_4/map' temp.ttx 2>/dev/null | \ | |
| sed -n 's/.*code="0x\([0-9A-Fa-f]\+\)".*name="\([^"]*\)".*/U+\1 \2/p' | \ | |
| while read -r codepoint_u glyph_name; do | |
| # Strip 'U+' to get the raw hex using bash substitution | |
| codepoint_hex="${codepoint_u#U+}" | |
| # Convert hex codepoint to decimal for logic checks | |
| codepoint_dec=$((16#$codepoint_hex)) | |
| # Print codepoint, glyph name and (if possible) the glyph character | |
| # Use 65535 instead of 0xFFFF for POSIX [ ... ] compatibility | |
| if [ "$codepoint_dec" -le 65535 ]; then | |
| # Format hex to 4 digits, then feed dynamically to printf's unicode escape sequence | |
| hex_padded=$(printf "%04X" "$codepoint_dec") | |
| printf -v char "\\u$hex_padded" | |
| else | |
| # Format hex to 8 digits, then feed dynamically to printf's unicode escape sequence | |
| hex_padded=$(printf "%08X" "$codepoint_dec") | |
| printf -v char "\\U$hex_padded" | |
| fi | |
| # Some terminals might not render the char correctly, so proceed carefully. | |
| echo -e "$codepoint_u $glyph_name \t $char" | |
| done | |
| # Cleanup | |
| rm -f temp.ttx | |
| echo "---------------------------------------" | |
| done |
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
| # By default, list available commands when just running `just` | |
| default: | |
| @just --list | |
| # Extract font codepoints to a markdown file (Usage: just make-md font.ttf) | |
| make-md font_file: | |
| #!/usr/bin/env bash | |
| font="{{font_file}}" | |
| if [[ ! -x "./codepoints.sh" ]]; then | |
| echo "Error: ./codepoints.sh not found or not executable. Run 'chmod +x codepoints.sh'." | |
| exit 1 | |
| fi | |
| if [[ ! -f "$font" ]]; then | |
| echo "Error: Font file '$font' not found!" | |
| exit 1 | |
| fi | |
| out="${font%.*}.md" | |
| echo "Generating $out..." | |
| ./codepoints.sh "$font" > "$out" | |
| echo "Done! Saved to $out" | |
| # Open icons in rofi and copy selection to clipboard (Usage: just rofi font.ttf) | |
| rofi font_file: | |
| #!/usr/bin/env bash | |
| font="{{font_file}}" | |
| if [[ ! -x "./codepoints.sh" ]]; then | |
| echo "Error: ./codepoints.sh not found. Run 'chmod +x codepoints.sh'." | |
| exit 1 | |
| fi | |
| if [[ ! -f "$font" ]]; then | |
| echo "Error: Font file '$font' not found!" | |
| exit 1 | |
| fi | |
| selection=$(./codepoints.sh "$font" | grep '^U+' | rofi -dmenu -i -p "Copy Icon:") | |
| if [[ -n "$selection" ]]; then | |
| icon=$(echo "$selection" | awk '{print $NF}' | tr -d '\n') | |
| if command -v wl-copy >/dev/null 2>&1; then | |
| echo -n "$icon" | wl-copy | |
| echo "Copied '$icon' to clipboard (Wayland: wl-copy)" | |
| elif command -v xclip >/dev/null 2>&1; then | |
| echo -n "$icon" | xclip -selection clipboard | |
| echo "Copied '$icon' to clipboard (X11: xclip)" | |
| else | |
| echo "Selected icon: $icon" | |
| echo "(Note: 'wl-copy' or 'xclip' is required to automatically copy to clipboard)" | |
| fi | |
| fi | |
| # Create a responsive dark-theme HTML grid with embedded font + Duotone support (Usage: just make-html font.ttf) | |
| make-html font_file: | |
| #!/usr/bin/env bash | |
| font="{{font_file}}" | |
| if [[ ! -x "./codepoints.sh" ]]; then | |
| echo "Error: ./codepoints.sh not found. Run 'chmod +x codepoints.sh'." | |
| exit 1 | |
| fi | |
| if [[ ! -f "$font" ]]; then | |
| echo "Error: Font file '$font' not found!" | |
| exit 1 | |
| fi | |
| out="${font%.*}.html" | |
| echo "Generating HTML visualizer at $out..." | |
| # Determine the CSS format tag | |
| ext="${font##*.}" | |
| format="truetype" | |
| [[ "${ext,,}" == "otf" ]] && format="opentype" | |
| # Base64 encode the font file directly | |
| b64_font=$(base64 "$font" | tr -d '\n') | |
| # Generate HTML Head, CSS, JS, and start Grid | |
| cat <<EOF > "$out" | |
| <!DOCTYPE html> | |
| <html lang="en"> | |
| <head> | |
| <meta charset="UTF-8"> | |
| <meta name="viewport" content="width=device-width, initial-scale=1.0"> | |
| <title>Icon Reference: $font</title> | |
| <style> | |
| @font-face { | |
| font-family: 'EmbeddedFont'; | |
| src: url('data:font/$ext;charset=utf-8;base64,$b64_font') format('$format'); | |
| font-weight: normal; font-style: normal; | |
| } | |
| :root { | |
| --bg: #121212; --fg: #e0e0e0; --accent: #60a5fa; | |
| --card-bg: #1e1e1e; --card-border: #333; --card-hover: #262626; | |
| --hex-bg: #2d2d2d; --hex-fg: #9ca3af; | |
| --btn-bg: #374151; --btn-hover: #4b5563; --btn-active: #1f2937; | |
| } | |
| body { font-family: system-ui, -apple-system, sans-serif; background: var(--bg); color: var(--fg); margin: 0; padding: 2rem; } | |
| h1 { text-align: center; font-size: 2rem; color: #fff; margin-bottom: 2rem; } | |
| /* Responsive Grid System */ | |
| .grid { | |
| display: grid; | |
| grid-template-columns: repeat(auto-fill, minmax(220px, 1fr)); | |
| gap: 1.5rem; | |
| max-width: 1800px; | |
| margin: 0 auto; | |
| } | |
| /* Icon Cards */ | |
| .card { | |
| background: var(--card-bg); border: 1px solid var(--card-border); border-radius: 12px; | |
| padding: 1.5rem; display: flex; flex-direction: column; align-items: center; text-align: center; | |
| transition: transform 0.2s, border-color 0.2s, background 0.2s; box-shadow: 0 4px 6px rgba(0,0,0,0.3); | |
| } | |
| .card:hover { transform: translateY(-3px); border-color: var(--accent); background: var(--card-hover); } | |
| /* Card Contents */ | |
| .icon { | |
| font-family: 'EmbeddedFont'; font-size: 3.5rem; color: var(--fg); | |
| margin-bottom: 1rem; line-height: 1; position: relative; | |
| display: flex; justify-content: center; align-items: center; | |
| width: 1.2em; height: 1.2em; transition: color 0.2s; | |
| } | |
| .card:hover .icon { color: var(--accent); } | |
| /* Duotone Layer Support */ | |
| .icon.duotone .primary { position: relative; z-index: 2; } | |
| .icon.duotone .secondary { position: absolute; opacity: 0.4; z-index: 1; } | |
| .name { font-size: 0.95rem; font-weight: 500; margin-bottom: 0.5rem; display: -webkit-box; -webkit-line-clamp: 2; -webkit-box-orient: vertical; overflow: hidden; height: 2.3em; } | |
| .hex { font-family: monospace; font-size: 0.85rem; color: var(--hex-fg); background: var(--hex-bg); padding: 0.2rem 0.5rem; border-radius: 4px; margin-bottom: 1rem; } | |
| /* Button */ | |
| .copy-btn { cursor: pointer; background: var(--btn-bg); color: #fff; border: none; padding: 0.6rem 1rem; border-radius: 6px; font-size: 0.9rem; font-weight: 500; width: 100%; transition: all 0.2s; } | |
| .copy-btn:hover { background: var(--btn-hover); } | |
| .copy-btn:active { background: var(--btn-active); transform: scale(0.97); } | |
| </style> | |
| <script> | |
| // Automatically copy and show a green "Copied!" feedback | |
| function copyText(button, text) { | |
| navigator.clipboard.writeText(text).then(() => { | |
| const originalText = button.innerText; | |
| button.innerText = "Copied!"; | |
| button.style.background = "#059669"; | |
| setTimeout(() => { | |
| button.innerText = originalText; | |
| button.style.background = ""; | |
| }, 1500); | |
| }); | |
| } | |
| </script> | |
| </head> | |
| <body> | |
| <h1>Icon Reference: $font</h1> | |
| <div class="grid"> | |
| EOF | |
| # Parse codepoints.sh output and append HTML Cards with Awk | |
| # We load everything into memory first so we can intelligently pair Duotone layers | |
| ./codepoints.sh "$font" | grep '^U+' | awk -F '\t' ' | |
| BEGIN { count = 0; } | |
| { | |
| char_val = $2; | |
| gsub(/^[ \t]+|[ \t]+$/, "", char_val); | |
| str = $1; | |
| gsub(/^[ \t]+|[ \t]+$/, "", str); | |
| idx = index(str, " "); | |
| hex_val = substr(str, 1, idx-1); | |
| name_val = substr(str, idx+1); | |
| # Store properties in memory | |
| hex_map[hex_val] = char_val; | |
| name_map[hex_val] = name_val; | |
| keys[count++] = hex_val; | |
| } | |
| END { | |
| for (i = 0; i < count; i++) { | |
| h = keys[i]; | |
| # Skip rendering this as a standalone card if it is a secondary duotone layer. | |
| # (Secondary layers in FA Duotone start with "10" and map back to a base hex) | |
| if (length(h) >= 5 && substr(h, 1, 2) == "10") { | |
| base_hex = substr(h, 3); | |
| if (base_hex in hex_map) { continue; } | |
| } | |
| char_val = hex_map[h]; | |
| name_val = name_map[h]; | |
| # Check if a secondary layer exists for this base icon | |
| sec_hex = "10" h; | |
| sec_char = ""; | |
| if (sec_hex in hex_map) { sec_char = hex_map[sec_hex]; } | |
| # Output card structure (\x27 is the safe ASCII hex for single quotes inside awk) | |
| print " <div class=\"card\">" | |
| if (sec_char != "") { | |
| # Render a combined Duotone layered icon | |
| print " <div class=\"icon duotone\" title=\"" name_val "\">" | |
| print " <span class=\"primary\">" char_val "</span>" | |
| print " <span class=\"secondary\">" sec_char "</span>" | |
| print " </div>" | |
| } else { | |
| # Render a standard icon | |
| print " <div class=\"icon\" title=\"" name_val "\">" char_val "</div>" | |
| } | |
| print " <div class=\"name\" title=\"" name_val "\">" name_val "</div>" | |
| print " <div class=\"hex\">" h "</div>" | |
| print " <button class=\"copy-btn\" onclick=\"copyText(this, \x27" char_val "\x27)\">Copy Icon</button>" | |
| print " </div>" | |
| } | |
| }' >> "$out" | |
| # Close HTML Tags | |
| cat <<EOF >> "$out" | |
| </div> | |
| </body> | |
| </html> | |
| EOF | |
| echo "Done! Open $out in your web browser." |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment