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)."