33# Used by install.sh and `peon packs install`
44set -euo pipefail
55
6+ # Restore cursor on exit (in case we hid it for progress bars)
7+ trap ' [ -t 1 ] && printf "\033[?25h"' EXIT
8+
69REGISTRY_URL=" https://peonping.github.io/registry/index.json"
710
811# MSYS2/MinGW: Windows Python can't read /c/... paths — convert to C:/... via cygpath
@@ -93,7 +96,7 @@ is_cached_valid() {
9396 [ -s " $filepath " ] || return 1
9497 [ -f " $checksums_file " ] || return 1
9598 local stored_hash current_hash
96- stored_hash=$( grep -F " $filename " " $checksums_file " 2> /dev/null | head -1 | cut -d' ' -f2 )
99+ stored_hash=$( grep -F " $filename " " $checksums_file " 2> /dev/null | head -1 | rev | cut -d' ' -f1 | rev )
97100 [ -n " $stored_hash " ] || return 1
98101 current_hash=$( file_sha256 " $filepath " )
99102 [ " $stored_hash " = " $current_hash " ]
@@ -110,6 +113,7 @@ store_checksum() {
110113 mv " $checksums_file .tmp" " $checksums_file "
111114}
112115
116+
113117# --- Progress bar ---
114118
115119draw_progress () {
@@ -134,7 +138,7 @@ draw_progress() {
134138 size_str=" $bytes B"
135139 fi
136140
137- printf " \r [%${idx_width} d/%d] %-20s [%s] %d/%d (%s)%-10s " \
141+ printf " \r [%${idx_width} d/%d] %-20s [%s] %d/%d (%s)%-16s " \
138142 " $pidx " " $ptotal " " $pname " " $bar " " $cur " " $total " " $size_str " " "
139143}
140144
@@ -288,18 +292,60 @@ IS_TTY=false
288292TOTAL_DOWNLOAD_FILES=0
289293TOTAL_DOWNLOAD_BYTES=0
290294TOTAL_DOWNLOAD_PACKS=0
295+ TOTAL_SKIPPED_PACKS=0
296+ TOTAL_FAILED_PACKS=0
297+ FAILED_PACK_NAMES=()
298+ PARTIAL_PACK_NAMES=()
299+ TOTAL_PARTIAL_PACKS=0
291300
292301echo " "
293- echo " Downloading packs..."
302+ echo " Syncing packs..."
294303for pack in $PACKS ; do
295304 if ! is_safe_pack_name " $pack " ; then
296- echo " Warning: skipping invalid pack name: $pack " >&2
297305 continue
298306 fi
299307
300308 PACK_INDEX=$(( PACK_INDEX + 1 ))
301-
302- mkdir -p " $PEON_DIR /packs/$pack /sounds"
309+ idx_width=${# TOTAL_PACKS}
310+
311+ # Skip packs where every manifest sound has a valid checksum
312+ if [ -s " $PEON_DIR /packs/$pack /openpeon.json" ] && [ -f " $PEON_DIR /packs/$pack /.checksums" ]; then
313+ manifest_check=" $( py_path " $PEON_DIR /packs/$pack /openpeon.json" ) "
314+ checksums_check=" $PEON_DIR /packs/$pack /.checksums"
315+ pack_complete=$( CHECKSUMS=" $checksums_check " PACKS_DIR=" $( py_path " $PEON_DIR /packs/$pack " ) " python3 -c "
316+ import json, os, posixpath, hashlib
317+ m = json.load(open('$manifest_check '))
318+ checksums = {}
319+ cf = os.environ['CHECKSUMS']
320+ packs_dir = os.environ['PACKS_DIR']
321+ if os.path.isfile(cf):
322+ for line in open(cf):
323+ parts = line.strip().rsplit(' ', 1)
324+ if len(parts) == 2:
325+ checksums[parts[0]] = parts[1]
326+ seen = set()
327+ for cat in m.get('categories', {}).values():
328+ for s in cat.get('sounds', []):
329+ f = s['file']
330+ rel = f[len('sounds/'):] if f.startswith('sounds/') else posixpath.basename(f)
331+ seen.add(rel)
332+ def is_valid(rel):
333+ stored = checksums.get(rel)
334+ if not stored:
335+ return False
336+ fp = os.path.join(packs_dir, 'sounds', rel)
337+ if not os.path.isfile(fp):
338+ return False
339+ actual = hashlib.sha256(open(fp, 'rb').read()).hexdigest()
340+ return actual == stored
341+ print('yes' if seen and all(is_valid(r) for r in seen) else 'no')
342+ " 2> /dev/null || echo " no" )
343+ if [ " $pack_complete " = " yes" ]; then
344+ TOTAL_SKIPPED_PACKS=$(( TOTAL_SKIPPED_PACKS + 1 ))
345+ printf " [%${idx_width} d/%d] %s ✓\n" " $PACK_INDEX " " $TOTAL_PACKS " " $pack "
346+ continue
347+ fi
348+ fi
303349
304350 # Get source info from registry (or use fallback)
305351 SOURCE_REPO=" "
@@ -344,9 +390,21 @@ for p in data.get('packs', []):
344390 PACK_BASE=" https://raw.githubusercontent.com/$SOURCE_REPO /$SOURCE_REF "
345391 fi
346392
393+ # Verify manifest exists before creating any local directories
394+ if ! curl -fsSL --head " $PACK_BASE /openpeon.json" > /dev/null 2>&1 ; then
395+ TOTAL_FAILED_PACKS=$(( TOTAL_FAILED_PACKS + 1 ))
396+ FAILED_PACK_NAMES+=(" $pack " )
397+ printf " [%${idx_width} d/%d] %s ❌\n" " $PACK_INDEX " " $TOTAL_PACKS " " $pack "
398+ continue
399+ fi
400+
401+ mkdir -p " $PEON_DIR /packs/$pack /sounds"
402+
347403 # Download manifest
348404 if ! curl -fsSL " $PACK_BASE /openpeon.json" -o " $PEON_DIR /packs/$pack /openpeon.json" 2> /dev/null; then
349- echo " Warning: failed to download manifest for $pack " >&2
405+ TOTAL_FAILED_PACKS=$(( TOTAL_FAILED_PACKS + 1 ))
406+ FAILED_PACK_NAMES+=(" $pack " )
407+ printf " [%${idx_width} d/%d] %s ❌\n" " $PACK_INDEX " " $TOTAL_PACKS " " $pack "
350408 continue
351409 fi
352410
@@ -393,30 +451,23 @@ print(len(seen))
393451 while read -r ifile; do
394452 ifile=" ${ifile% $' \r ' } " # strip Windows CRLF trailing CR (Python on Windows outputs \r\n)
395453 [ -z " $ifile " ] && continue
396- if ! is_safe_filename " $ifile " ; then
397- echo " Warning: skipped unsafe icon path in $pack : $ifile " >&2
398- continue
399- fi
454+ is_safe_filename " $ifile " || continue
400455 mkdir -p " $PEON_DIR /packs/$pack /$( dirname " $ifile " ) "
401- if ! curl -fsSL " $PACK_BASE /$( urlencode_filename " $ifile " ) " \
402- -o " $PEON_DIR /packs/$pack /$ifile " < /dev/null 2> /dev/null; then
403- echo " Warning: failed to download $pack /$ifile " >&2
404- fi
456+ curl -fsSL " $PACK_BASE /$( urlencode_filename " $ifile " ) " \
457+ -o " $PEON_DIR /packs/$pack /$ifile " < /dev/null 2> /dev/null || true
405458 done <<< " $ICON_LIST"
406459 fi
407460
408461 if [ " $IS_TTY " = true ] && [ " $SOUND_COUNT " != " ?" ]; then
409462 local_file_count=0
410463 local_byte_count=0
411464
465+ printf ' \033[?25l' # hide cursor during progress
412466 draw_progress " $PACK_INDEX " " $TOTAL_PACKS " " $pack " 0 " $SOUND_COUNT " 0
413467
414468 while read -r sfile; do
415469 sfile=" ${sfile% $' \r ' } " # strip Windows CRLF trailing CR (Python on Windows outputs \r\n)
416- if ! is_safe_filename " $sfile " ; then
417- echo " Warning: skipped unsafe filename in $pack : $sfile " >&2
418- continue
419- fi
470+ is_safe_filename " $sfile " || continue
420471 mkdir -p " $PEON_DIR /packs/$pack /sounds/$( dirname " $sfile " ) "
421472 if is_cached_valid " $PEON_DIR /packs/$pack /sounds/$sfile " " $CHECKSUMS_FILE " " $sfile " ; then
422473 local_file_count=$(( local_file_count + 1 ))
@@ -429,7 +480,7 @@ print(len(seen))
429480 fsize=$( wc -c < " $PEON_DIR /packs/$pack /sounds/$sfile " | tr -d ' ' )
430481 local_byte_count=$(( local_byte_count + fsize))
431482 else
432- echo " Warning: failed to download $pack /sounds/ $sfile " >&2
483+ break
433484 fi
434485 draw_progress " $PACK_INDEX " " $TOTAL_PACKS " " $pack " \
435486 " $local_file_count " " $SOUND_COUNT " " $local_byte_count "
@@ -449,17 +500,39 @@ for cat in m.get('categories', {}).values():
449500 print(rel)
450501" )
451502
452- draw_progress " $PACK_INDEX " " $TOTAL_PACKS " " $pack " \
453- " $local_file_count " " $SOUND_COUNT " " $local_byte_count "
454- printf " \n"
503+ # Clear the progress bar line and print final status on a clean line
504+ printf ' \033[?25h' # restore cursor
505+ printf " \r%80s\r" " "
506+ if [ " $local_file_count " -eq " $SOUND_COUNT " ]; then
507+ printf " [%${idx_width} d/%d] %s ✅\n" " $PACK_INDEX " " $TOTAL_PACKS " " $pack "
508+ TOTAL_DOWNLOAD_PACKS=$(( TOTAL_DOWNLOAD_PACKS + 1 ))
509+ else
510+ printf " [%${idx_width} d/%d] %s (%d/%d) ⚠️\n" " $PACK_INDEX " " $TOTAL_PACKS " " $pack " " $local_file_count " " $SOUND_COUNT "
511+ TOTAL_PARTIAL_PACKS=$(( TOTAL_PARTIAL_PACKS + 1 ))
512+ PARTIAL_PACK_NAMES+=(" $pack " )
513+ fi
455514
456515 TOTAL_DOWNLOAD_FILES=$(( TOTAL_DOWNLOAD_FILES + local_file_count))
457516 TOTAL_DOWNLOAD_BYTES=$(( TOTAL_DOWNLOAD_BYTES + local_byte_count))
458- TOTAL_DOWNLOAD_PACKS=$(( TOTAL_DOWNLOAD_PACKS + 1 ))
459517 else
460- printf " [%d/%d] %s " " $PACK_INDEX " " $TOTAL_PACKS " " $pack "
518+ printf " [%${idx_width} d/%d] %s " " $PACK_INDEX " " $TOTAL_PACKS " " $pack "
519+ local_file_count=0
461520
462- python3 -c "
521+ while read -r sfile; do
522+ sfile=" ${sfile% $' \r ' } " # strip Windows CRLF trailing CR (Python on Windows outputs \r\n)
523+ is_safe_filename " $sfile " || continue
524+ mkdir -p " $PEON_DIR /packs/$pack /sounds/$( dirname " $sfile " ) "
525+ if is_cached_valid " $PEON_DIR /packs/$pack /sounds/$sfile " " $CHECKSUMS_FILE " " $sfile " ; then
526+ printf " ."
527+ local_file_count=$(( local_file_count + 1 ))
528+ elif curl -fsSL " $PACK_BASE /sounds/$( urlencode_filename " $sfile " ) " -o " $PEON_DIR /packs/$pack /sounds/$sfile " < /dev/null 2> /dev/null; then
529+ store_checksum " $CHECKSUMS_FILE " " $sfile " " $PEON_DIR /packs/$pack /sounds/$sfile "
530+ printf " ."
531+ local_file_count=$(( local_file_count + 1 ))
532+ else
533+ break
534+ fi
535+ done < <( python3 -c "
463536import json, posixpath
464537m = json.load(open('$manifest_py '))
465538seen = set()
@@ -473,37 +546,70 @@ for cat in m.get('categories', {}).values():
473546 if rel not in seen:
474547 seen.add(rel)
475548 print(rel)
476- " | while read -r sfile; do
477- sfile=" ${sfile% $' \r ' } " # strip Windows CRLF trailing CR (Python on Windows outputs \r\n)
478- if ! is_safe_filename " $sfile " ; then
479- echo " Warning: skipped unsafe filename in $pack : $sfile " >&2
480- continue
481- fi
482- mkdir -p " $PEON_DIR /packs/$pack /sounds/$( dirname " $sfile " ) "
483- if is_cached_valid " $PEON_DIR /packs/$pack /sounds/$sfile " " $CHECKSUMS_FILE " " $sfile " ; then
484- printf " ."
485- elif curl -fsSL " $PACK_BASE /sounds/$( urlencode_filename " $sfile " ) " -o " $PEON_DIR /packs/$pack /sounds/$sfile " < /dev/null 2> /dev/null; then
486- store_checksum " $CHECKSUMS_FILE " " $sfile " " $PEON_DIR /packs/$pack /sounds/$sfile "
487- printf " ."
488- else
489- printf " x"
490- echo " Warning: failed to download $pack /sounds/$sfile " >&2
491- fi
492- done
549+ " )
493550
494- printf " %s sounds\n" " $SOUND_COUNT "
495- TOTAL_DOWNLOAD_PACKS=$(( TOTAL_DOWNLOAD_PACKS + 1 ))
551+ if [ " $SOUND_COUNT " != " ?" ] && [ " $local_file_count " -eq " $SOUND_COUNT " ]; then
552+ printf " ✅ %s sounds\n" " $SOUND_COUNT "
553+ TOTAL_DOWNLOAD_PACKS=$(( TOTAL_DOWNLOAD_PACKS + 1 ))
554+ else
555+ printf " ⚠️%s/%s sounds\n" " $local_file_count " " $SOUND_COUNT "
556+ TOTAL_PARTIAL_PACKS=$(( TOTAL_PARTIAL_PACKS + 1 ))
557+ PARTIAL_PACK_NAMES+=(" $pack " )
558+ fi
496559 fi
497560done
498561
499- if [ " $IS_TTY " = true ] && [ " $TOTAL_DOWNLOAD_PACKS " -gt 0 ]; then
500- if [ " $TOTAL_DOWNLOAD_BYTES " -ge 1048576 ]; then
501- SUMMARY_SIZE=" $(( TOTAL_DOWNLOAD_BYTES / 1048576 )) .$(( (TOTAL_DOWNLOAD_BYTES % 1048576 ) * 10 / 1048576 )) MB"
502- elif [ " $TOTAL_DOWNLOAD_BYTES " -ge 1024 ]; then
503- SUMMARY_SIZE=" $(( TOTAL_DOWNLOAD_BYTES / 1024 )) KB"
562+ # --- Summary ---
563+ echo " "
564+ SUMMARY_PARTS=()
565+ if [ " $TOTAL_DOWNLOAD_PACKS " -gt 0 ]; then
566+ if [ " $IS_TTY " = true ] && [ " $TOTAL_DOWNLOAD_BYTES " -gt 0 ]; then
567+ if [ " $TOTAL_DOWNLOAD_BYTES " -ge 1048576 ]; then
568+ SUMMARY_SIZE=" $(( TOTAL_DOWNLOAD_BYTES / 1048576 )) .$(( (TOTAL_DOWNLOAD_BYTES % 1048576 ) * 10 / 1048576 )) MB"
569+ elif [ " $TOTAL_DOWNLOAD_BYTES " -ge 1024 ]; then
570+ SUMMARY_SIZE=" $(( TOTAL_DOWNLOAD_BYTES / 1024 )) KB"
571+ else
572+ SUMMARY_SIZE=" $TOTAL_DOWNLOAD_BYTES B"
573+ fi
574+ SUMMARY_PARTS+=(" ✅ $TOTAL_DOWNLOAD_PACKS downloaded ($TOTAL_DOWNLOAD_FILES files, $SUMMARY_SIZE )" )
504575 else
505- SUMMARY_SIZE= " $TOTAL_DOWNLOAD_BYTES B "
576+ SUMMARY_PARTS+=( " ✅ $TOTAL_DOWNLOAD_PACKS downloaded " )
506577 fi
578+ fi
579+ if [ " $TOTAL_SKIPPED_PACKS " -gt 0 ]; then
580+ SUMMARY_PARTS+=(" ✓ $TOTAL_SKIPPED_PACKS already installed" )
581+ fi
582+ if [ " $TOTAL_PARTIAL_PACKS " -gt 0 ]; then
583+ SUMMARY_PARTS+=(" ⚠️ $TOTAL_PARTIAL_PACKS partial" )
584+ fi
585+ if [ " $TOTAL_FAILED_PACKS " -gt 0 ]; then
586+ SUMMARY_PARTS+=(" ❌ $TOTAL_FAILED_PACKS unavailable" )
587+ fi
588+ if [ " ${# SUMMARY_PARTS[@]} " -gt 0 ]; then
589+ IFS=" " ; echo " ${SUMMARY_PARTS[*]} " ; unset IFS
590+ fi
591+ if [ " ${# PARTIAL_PACK_NAMES[@]} " -gt 0 ]; then
507592 echo " "
508- echo " Downloaded $TOTAL_DOWNLOAD_PACKS packs ($TOTAL_DOWNLOAD_FILES files, $SUMMARY_SIZE )"
593+ echo " Partial downloads (some sounds unavailable):"
594+ for p in " ${PARTIAL_PACK_NAMES[@]} " ; do echo " - $p " ; done
595+ fi
596+ if [ " ${# FAILED_PACK_NAMES[@]} " -gt 0 ]; then
597+ echo " "
598+ echo " Failed downloads (manifest unavailable):"
599+ for p in " ${FAILED_PACK_NAMES[@]} " ; do echo " - $p " ; done
600+ fi
601+
602+ # Report total disk usage for all packs
603+ PACKS_PATH=" $PEON_DIR /packs"
604+ # Resolve symlink to show actual storage location
605+ PACKS_REAL=" $( cd " $PACKS_PATH " 2> /dev/null && pwd -P) "
606+ DISK_BYTES=$( du -sk " $PACKS_REAL " 2> /dev/null | cut -f1)
607+ if [ -n " $DISK_BYTES " ] && [ " $DISK_BYTES " -gt 0 ]; then
608+ DISK_MB=$(( DISK_BYTES / 1024 ))
609+ echo " "
610+ if [ " $PACKS_REAL " != " $PACKS_PATH " ]; then
611+ echo " Disk usage: ${DISK_MB} MB ($PACKS_REAL )"
612+ else
613+ echo " Disk usage: ${DISK_MB} MB"
614+ fi
509615fi
0 commit comments