identicon.sh 4.9 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157
  1. #!/usr/bin/env bash
  2. # Generate a symmetric pixel art identicon from a hash
  3. # Usage: bash identicon.sh <path_or_string> [--compact]
  4. #
  5. # 11x11 pixel grid (mirrored from 6 columns), rendered with Unicode
  6. # half-block characters for double vertical resolution. Each project
  7. # gets a unique colored portrait derived from sha256 of its canonical path.
  8. set -e
  9. INPUT="${1:-$PWD}"
  10. COMPACT=false
  11. [[ "${2:-}" == "--compact" || "${1:-}" == "--compact" ]] && COMPACT=true
  12. [[ "${1:-}" == "--compact" ]] && INPUT="$PWD"
  13. # Identity: git root commit hash > canonical path hash
  14. # This must match mail-db.sh project_hash() logic
  15. if [ -d "$INPUT" ]; then
  16. CANONICAL=$(cd "$INPUT" && pwd -P)
  17. ROOT_COMMIT=$(git -C "$INPUT" rev-list --max-parents=0 HEAD 2>/dev/null | head -1)
  18. if [ -n "$ROOT_COMMIT" ]; then
  19. # Use full root commit for visual entropy, short ID from first 6
  20. HASH=$(printf '%s' "$ROOT_COMMIT" | shasum -a 256 | cut -c1-40)
  21. SHORT="${ROOT_COMMIT:0:6}"
  22. else
  23. HASH=$(printf '%s' "$CANONICAL" | shasum -a 256 | cut -c1-40)
  24. SHORT="${HASH:0:6}"
  25. fi
  26. else
  27. CANONICAL="$INPUT"
  28. HASH=$(printf '%s' "$CANONICAL" | shasum -a 256 | cut -c1-40)
  29. SHORT="${HASH:0:6}"
  30. fi
  31. NAME=$(basename "$CANONICAL")
  32. # --- Color palette ---
  33. # Two colors per identicon: foreground + accent, from different hash regions
  34. FG_IDX=$(( $(printf '%d' "0x${HASH:6:2}") % 7 ))
  35. BG_IDX=$(( $(printf '%d' "0x${HASH:8:2}") % 4 ))
  36. # Foreground: vivid ANSI colors
  37. FG_CODES=(31 32 33 34 35 36 91)
  38. FG="\033[${FG_CODES[$FG_IDX]}m"
  39. # Shade characters: full, dark, medium, light
  40. CHARS=("█" "▓" "▒" "░")
  41. RESET="\033[0m"
  42. DIM="\033[2m"
  43. # --- Build 11x12 pixel grid ---
  44. # 6 columns generated, mirrored to 11 (c0 c1 c2 c3 c4 c5 c4 c3 c2 c1 c0)
  45. # 12 rows, rendered as 6 lines using half-block characters
  46. # Each cell has 2 bits (4 shade levels): 6 cols * 12 rows = 72 cells = 144 bits
  47. # We have 160 bits from 40 hex chars
  48. declare -a GRID # GRID[row*6+col] = shade level (0-3)
  49. bit_pos=0
  50. for row in $(seq 0 11); do
  51. for col in $(seq 0 5); do
  52. hex_pos=$((bit_pos / 4))
  53. bit_offset=$((bit_pos % 4))
  54. hex_char="${HASH:$hex_pos:1}"
  55. nibble=$(printf '%d' "0x${hex_char}")
  56. # Extract 2 bits for shade level
  57. if [ $bit_offset -le 2 ]; then
  58. shade=$(( (nibble >> bit_offset) & 3 ))
  59. else
  60. # Straddle nibble boundary
  61. next_char="${HASH:$((hex_pos+1)):1}"
  62. next_nibble=$(printf '%d' "0x${next_char}")
  63. shade=$(( ((nibble >> bit_offset) | (next_nibble << (4 - bit_offset))) & 3 ))
  64. fi
  65. GRID[$((row * 6 + col))]=$shade
  66. bit_pos=$((bit_pos + 2))
  67. done
  68. done
  69. # --- Render with half-blocks ---
  70. # Each output line combines two pixel rows using ▀▄█ and space
  71. # Top pixel = upper half, Bottom pixel = lower half
  72. #
  73. # Both filled = █ (full block)
  74. # Top only = ▀ (upper half)
  75. # Bottom only = ▄ (lower half)
  76. # Neither = " " (space)
  77. get_mirrored_col() {
  78. local col=$1
  79. # Mirror pattern: 0 1 2 3 4 5 4 3 2 1 0
  80. if [ $col -le 5 ]; then
  81. echo $col
  82. else
  83. echo $((10 - col))
  84. fi
  85. }
  86. render_cell() {
  87. local top_shade=$1
  88. local bot_shade=$2
  89. # Threshold: shades 0-1 = filled, 2-3 = empty (gives ~50% fill)
  90. local top_on=$(( top_shade <= 1 ? 1 : 0 ))
  91. local bot_on=$(( bot_shade <= 1 ? 1 : 0 ))
  92. if [ $top_on -eq 1 ] && [ $bot_on -eq 1 ]; then
  93. # Both filled - use shade of top for character choice
  94. printf '%s' "${CHARS[$top_shade]}"
  95. elif [ $top_on -eq 1 ]; then
  96. printf '▀'
  97. elif [ $bot_on -eq 1 ]; then
  98. printf '▄'
  99. else
  100. printf ' '
  101. fi
  102. }
  103. # Width: 11 columns, each 1 char wide = 11 chars inside frame
  104. BORDER_TOP="${DIM}┌───────────┐${RESET}"
  105. BORDER_BOT="${DIM}└───────────┘${RESET}"
  106. if [ "$COMPACT" = true ]; then
  107. # Compact: no frame, just the icon + hash
  108. for line in $(seq 0 5); do
  109. top_row=$((line * 2))
  110. bot_row=$((line * 2 + 1))
  111. printf '%b' "${FG}"
  112. for col in $(seq 0 10); do
  113. src_col=$(get_mirrored_col $col)
  114. top_shade=${GRID[$((top_row * 6 + src_col))]}
  115. bot_shade=${GRID[$((bot_row * 6 + src_col))]}
  116. render_cell $top_shade $bot_shade
  117. done
  118. printf '%b\n' "${RESET}"
  119. done
  120. echo -e "${FG}${SHORT}${RESET}"
  121. else
  122. # Framed display
  123. echo -e "$BORDER_TOP"
  124. for line in $(seq 0 5); do
  125. top_row=$((line * 2))
  126. bot_row=$((line * 2 + 1))
  127. printf '%b' "${DIM}│${RESET}${FG}"
  128. for col in $(seq 0 10); do
  129. src_col=$(get_mirrored_col $col)
  130. top_shade=${GRID[$((top_row * 6 + src_col))]}
  131. bot_shade=${GRID[$((bot_row * 6 + src_col))]}
  132. render_cell $top_shade $bot_shade
  133. done
  134. printf '%b\n' "${RESET}${DIM}│${RESET}"
  135. done
  136. echo -e "$BORDER_BOT"
  137. echo -e " ${FG}${NAME}${RESET} ${DIM}${SHORT}${RESET}"
  138. fi