vesktop-glass

Transparent Vesktop for macOS — frosted glass, default Discord look, one opacity knob.
Log | Files | Refs | README | LICENSE

enable-frost.sh (9944B)


      1 #!/usr/bin/env bash
      2 #
      3 # enable-frost.sh — make Vesktop's frosted transparency STAY ON when the window
      4 # loses focus (macOS). Optional companion to the Glass theme.
      5 #
      6 # Why this exists: Vesktop never sets Electron's `visualEffectState`, so macOS
      7 # deactivates the window vibrancy whenever Vesktop isn't focused and the frost
      8 # collapses to opaque. There's no setting for it — it's a window-construction
      9 # option — so we patch it in. This script:
     10 #   1) finds your Vesktop, prints what it sees (hardware/OS/version)
     11 #   2) backs up app.asar + Info.plist (originals, once)
     12 #   3) injects  visualEffectState:"active"  next to the vibrancy assignment,
     13 #      matching it by PATTERN so it survives minifier renames across builds
     14 #   4) repacks the asar, recomputes the ElectronAsarIntegrity hash (if present)
     15 #   5) ad-hoc re-signs the app (preserving entitlements, so mic/camera keep working)
     16 #
     17 # Revert anytime:  ./scripts/enable-frost.sh --revert   (or run disable-frost.sh)
     18 # Note: a Vesktop UPDATE replaces app.asar, so re-run this after updating.
     19 #
     20 # Usage:
     21 #   ./scripts/enable-frost.sh            # interactive
     22 #   ./scripts/enable-frost.sh --yes      # no prompt
     23 #   ./scripts/enable-frost.sh --revert   # restore the backup
     24 #   VESKTOP_APP=/path/Vesktop.app ./scripts/enable-frost.sh   # override location
     25 
     26 set -euo pipefail
     27 
     28 YES=0; REVERT=0
     29 for a in "$@"; do
     30   case "$a" in
     31     --yes|-y) YES=1 ;;
     32     --revert|--uninstall) REVERT=1 ;;
     33     -h|--help) sed -n '2,30p' "$0"; exit 0 ;;
     34     *) echo "unknown arg: $a" >&2; exit 64 ;;
     35   esac
     36 done
     37 
     38 die(){ echo "✗ $*" >&2; exit 1; }
     39 say(){ echo "  $*"; }
     40 step(){ echo; echo "▸ $*"; }
     41 
     42 # ── locate Vesktop ──────────────────────────────────────────────────────────
     43 APP="${VESKTOP_APP:-}"
     44 if [ -z "$APP" ]; then
     45   for p in "/Applications/Vesktop.app" "$HOME/Applications/Vesktop.app"; do
     46     [ -d "$p" ] && APP="$p" && break
     47   done
     48 fi
     49 [ -z "$APP" ] && APP="$(mdfind "kMDItemCFBundleIdentifier == 'dev.vencord.vesktop'" 2>/dev/null | head -1 || true)"
     50 [ -n "$APP" ] && [ -d "$APP" ] || die "Couldn't find Vesktop.app. Pass VESKTOP_APP=/path/to/Vesktop.app"
     51 
     52 RES="$APP/Contents/Resources"
     53 ASAR="$RES/app.asar"
     54 PLIST="$APP/Contents/Info.plist"
     55 [ -f "$ASAR" ]  || die "No app.asar at $ASAR"
     56 [ -f "$PLIST" ] || die "No Info.plist at $PLIST"
     57 
     58 # ── environment report (hardware-aware) ─────────────────────────────────────
     59 step "Environment"
     60 say "macOS:    $(sw_vers -productVersion) ($(sw_vers -buildVersion))"
     61 say "arch:     $(uname -m)"
     62 say "Vesktop:  $(/usr/libexec/PlistBuddy -c 'Print :CFBundleShortVersionString' "$PLIST" 2>/dev/null || echo '?')   at $APP"
     63 RT="$(defaults read com.apple.universalaccess reduceTransparency 2>/dev/null || echo 0)"
     64 [ "$RT" = "1" ] && say "⚠  'Reduce Transparency' is ON — turn it OFF (System Settings → Accessibility → Display) or you'll see no transparency."
     65 
     66 # ── revert path ─────────────────────────────────────────────────────────────
     67 if [ "$REVERT" = "1" ]; then
     68   step "Reverting"
     69   [ -f "$ASAR.bak" ] || die "No backup ($ASAR.bak) — nothing to revert."
     70   osascript -e 'quit app "Vesktop"' 2>/dev/null || true; sleep 1; pkill -x vesktop 2>/dev/null || true
     71   cp -p "$ASAR.bak" "$ASAR"
     72   [ -f "$PLIST.bak" ] && cp -p "$PLIST.bak" "$PLIST"
     73   codesign --force --deep --sign - --preserve-metadata=entitlements "$APP" 2>/dev/null \
     74     || codesign --force --deep --sign - "$APP"
     75   say "Restored original app.asar + Info.plist and re-signed. Backups kept."
     76   echo; echo "✓ Reverted."; exit 0
     77 fi
     78 
     79 # ── preconditions ───────────────────────────────────────────────────────────
     80 step "Checks"
     81 command -v node >/dev/null || die "Node.js required (the asar tooling needs it). Install: brew install node"
     82 command -v npm  >/dev/null || die "npm required (ships with Node)."
     83 command -v codesign >/dev/null || die "codesign required (install Xcode Command Line Tools: xcode-select --install)."
     84 if [ ! -w "$ASAR" ] || [ ! -w "$PLIST" ]; then
     85   die "No write permission to $APP. Re-run with sudo:  sudo VESKTOP_APP='$APP' $0 ${*:-}"
     86 fi
     87 if grep -aq 'visualEffectState' "$ASAR"; then
     88   say "Already patched (visualEffectState present). Nothing to do."
     89   echo; echo "✓ Already enabled."; exit 0
     90 fi
     91 say "OK — Node $(node -v), writable app bundle, not yet patched."
     92 
     93 # ── confirm ─────────────────────────────────────────────────────────────────
     94 if [ "$YES" != "1" ]; then
     95   echo
     96   echo "This will modify and re-sign:"
     97   echo "    $APP"
     98   echo "Originals are backed up to app.asar.bak / Info.plist.bak (revert with --revert)."
     99   printf "Continue? [y/N] "
    100   read -r ans; case "$ans" in y|Y|yes|YES) ;; *) echo "Aborted."; exit 0 ;; esac
    101 fi
    102 
    103 # ── quit Vesktop ────────────────────────────────────────────────────────────
    104 step "Quitting Vesktop"
    105 osascript -e 'quit app "Vesktop"' 2>/dev/null || true
    106 for _ in 1 2 3 4 5 6 7 8 9 10; do pgrep -x vesktop >/dev/null || break; sleep 0.5; done
    107 pgrep -x vesktop >/dev/null && pkill -x vesktop 2>/dev/null || true
    108 sleep 1; pgrep -x vesktop >/dev/null && pkill -9 -x vesktop 2>/dev/null || true
    109 say "stopped."
    110 
    111 # ── backups (originals only) ────────────────────────────────────────────────
    112 step "Backup"
    113 [ -f "$ASAR.bak" ]  || cp -p "$ASAR"  "$ASAR.bak"
    114 [ -f "$PLIST.bak" ] || cp -p "$PLIST" "$PLIST.bak"
    115 say "app.asar.bak + Info.plist.bak in place."
    116 
    117 # ── patch + repack + hash (Node, with @electron/asar) ───────────────────────
    118 step "Patching app.asar"
    119 WORK="$(mktemp -d)"; trap 'rm -rf "$WORK"' EXIT
    120 say "installing @electron/asar (temporary)…"
    121 ( cd "$WORK" && npm init -y >/dev/null 2>&1 && npm i @electron/asar@4 >/dev/null 2>&1 ) || die "npm install of @electron/asar failed (offline?)."
    122 
    123 cat > "$WORK/patch.mjs" <<'NODE'
    124 import { extractAll, createPackage, getRawHeader } from '@electron/asar';
    125 import fs from 'node:fs'; import path from 'node:path'; import crypto from 'node:crypto';
    126 const ASAR = process.env.ASAR, EX = path.join(process.env.WORK, 'x');
    127 fs.rmSync(EX, { recursive: true, force: true });
    128 extractAll(ASAR, EX);
    129 // Match `OBJ.vibrancy = VALUE` (any minified names) and inject visualEffectState
    130 // on the same object, right after. Survives Vesktop rebuilds / renames.
    131 const RE = /([A-Za-z_$][\w$]*)\.vibrancy\s*=\s*([^,;)]+)/g;
    132 let patched = 0;
    133 const walk = d => { for (const e of fs.readdirSync(d, { withFileTypes: true })) {
    134   const p = path.join(d, e.name);
    135   if (e.isDirectory()) walk(p);
    136   else if (e.name.endsWith('.js')) {
    137     let s = fs.readFileSync(p, 'utf8');
    138     if (!RE.test(s)) { RE.lastIndex = 0; continue; } RE.lastIndex = 0;
    139     if (s.includes('visualEffectState')) continue;
    140     const out = s.replace(RE, (_m, obj, val) => `${obj}.vibrancy=${val},${obj}.visualEffectState="active"`);
    141     if (out !== s) { fs.writeFileSync(p, out); patched++; }
    142   }
    143 } };
    144 walk(EX);
    145 if (patched === 0) { console.error('NOPATTERN'); process.exit(2); }
    146 await createPackage(EX, ASAR);
    147 if (!fs.readFileSync(ASAR).includes('visualEffectState')) { console.error('VERIFYFAIL'); process.exit(3); }
    148 const { headerString } = getRawHeader(ASAR);
    149 process.stdout.write('HASH=' + crypto.createHash('sha256').update(headerString).digest('hex') + ' FILES=' + patched);
    150 NODE
    151 
    152 RESULT="$(ASAR="$ASAR" WORK="$WORK" node "$WORK/patch.mjs" 2>"$WORK/err" || true)"
    153 if [ -z "$RESULT" ]; then
    154   ERR="$(cat "$WORK/err" 2>/dev/null || true)"
    155   cp -p "$ASAR.bak" "$ASAR"   # restore on failure
    156   case "$ERR" in
    157     *NOPATTERN*) die "Couldn't find the vibrancy assignment in this Vesktop build. Please open an issue with your Vesktop version; original restored." ;;
    158     *) die "Patch failed ($ERR). Original restored." ;;
    159   esac
    160 fi
    161 HASH="${RESULT#HASH=}"; HASH="${HASH%% *}"
    162 say "injected visualEffectState — ${RESULT#*FILES=} file(s) patched."
    163 
    164 # ── update asar integrity hash (only if the fuse uses it) ───────────────────
    165 step "Integrity"
    166 if /usr/libexec/PlistBuddy -c "Print :ElectronAsarIntegrity:Resources/app.asar:hash" "$PLIST" >/dev/null 2>&1; then
    167   /usr/libexec/PlistBuddy -c "Set :ElectronAsarIntegrity:Resources/app.asar:hash $HASH" "$PLIST"
    168   say "ElectronAsarIntegrity hash updated."
    169 else
    170   say "No asar-integrity fuse on this build — skipping (nothing to update)."
    171 fi
    172 
    173 # ── re-sign (ad-hoc, keep entitlements so mic/camera survive) ───────────────
    174 step "Re-signing (ad-hoc)"
    175 codesign --force --deep --sign - --preserve-metadata=entitlements "$APP" 2>/dev/null \
    176   || codesign --force --deep --sign - "$APP" \
    177   || die "codesign failed. Restore with: cp '$ASAR.bak' '$ASAR' && cp '$PLIST.bak' '$PLIST'"
    178 codesign --verify --deep "$APP" 2>/dev/null && say "signature valid." || say "⚠ verify reported issues (it may still launch)."
    179 
    180 # ── done ────────────────────────────────────────────────────────────────────
    181 step "Launching"
    182 open -a Vesktop || true
    183 echo
    184 echo "✓ Frost is now persistent — it will stay when Vesktop loses focus."
    185 echo "  Revert:  $0 --revert"
    186 echo "  Re-run this after any Vesktop update (updates replace app.asar)."