* Rebind ghost Copy URL shortcut registrations to the pinned id Chromium never hands a suggested shortcut to one extension while another — even a long-gone one — still holds the registration. Profiles that first loaded Copy URL before its id was pinned registered Alt+Shift+L under an id derived from the extension's load path at the time, so the pinned extension never receives the shortcut and the keypress does nothing (#6816). The quattro upgrade tried to repair this against one hardcoded path-derived id, which only ever matched a single home directory. The historical ids are unknowable in general — they hash long-gone absolute paths through whatever symlinks existed then — but the registration itself names the command, so a migration now rebinds any copy-url command that points away from the pinned id, unless that id belongs to an extension that is actually installed or the pinned extension already holds a binding of its own. Browsers rewrite Preferences on exit, which reverts any repair made while one runs, so the migration asks for this user's browser windows to be closed first — failing and staying pending when there is no terminal to ask in or the prompt is declined. The backup a repair leaves behind marks it as attempted but unverified: until a browser-free run confirms the registration stayed repaired, the migration keeps itself pending rather than trusting a disk state an open browser may still overwrite. The upgrade-time repair is dropped: the upgrade already runs migrations, so the migration is the single implementation. Fixes #6816 Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> * Pin the WhatsApp Slim extension id Keyless unpacked extensions get path-derived ids, which go stale if the load path or packaging ever changes — the same class of bug that broke the Copy URL shortcut for pre-package installs. Pin the id with a manifest key like the other bundled extensions. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> --------- Co-authored-by: Claude Fable 5 <noreply@anthropic.com>
177 lines
6.1 KiB
Bash
177 lines
6.1 KiB
Bash
echo "Repair the Copy URL shortcut for profiles that predate its pinned extension id"
|
|
|
|
# Chromium never hands a suggested shortcut to one extension while another —
|
|
# even a long-gone one — still holds the registration. Profiles that first
|
|
# loaded Copy URL before its id was pinned registered Alt+Shift+L under an id
|
|
# derived from the extension's load path at the time, so the pinned extension
|
|
# never receives the shortcut and the keypress does nothing. Those historical
|
|
# ids are unknowable in general (they hash long-gone absolute paths), but the
|
|
# registration itself names the command: rebind any copy-url command that
|
|
# points away from the pinned id, unless that id belongs to an extension that
|
|
# is actually installed.
|
|
|
|
pinned_id="bgpiichlckmfanooecilcjemknkcpngb"
|
|
|
|
repair_py=$(cat <<'PY'
|
|
import json
|
|
import shutil
|
|
import sys
|
|
from pathlib import Path
|
|
|
|
path = Path(sys.argv[1])
|
|
pinned_id = sys.argv[2]
|
|
mode = sys.argv[3]
|
|
|
|
try:
|
|
preferences = json.loads(path.read_text())
|
|
except (OSError, ValueError):
|
|
sys.exit(1)
|
|
|
|
extensions = preferences.get("extensions", {})
|
|
commands = extensions.get("commands", {})
|
|
settings = extensions.get("settings", {})
|
|
|
|
def ghost_id(command):
|
|
if command.get("command_name") != "copy-url":
|
|
return None
|
|
extension = command.get("extension")
|
|
if not extension or extension == pinned_id:
|
|
return None
|
|
# An installed extension records its install path in settings; only the
|
|
# ghosts of Copy URL itself may be rebound.
|
|
install_path = settings.get(extension, {}).get("path", "")
|
|
if install_path and not install_path.endswith("/default/chromium/extensions/copy-url"):
|
|
return None
|
|
return extension
|
|
|
|
if mode == "check":
|
|
sys.exit(0 if any(ghost_id(c) for c in commands.values()) else 1)
|
|
|
|
# Chromium keeps one shortcut per command, so a ghost may only be rebound
|
|
# when the pinned extension holds no copy-url binding of its own; further
|
|
# ghosts are dropped rather than doubled up.
|
|
pinned_bound = any(
|
|
c.get("command_name") == "copy-url" and c.get("extension") == pinned_id
|
|
for c in commands.values()
|
|
)
|
|
|
|
changed = False
|
|
for accelerator in list(commands):
|
|
ghost = ghost_id(commands[accelerator])
|
|
if not ghost:
|
|
continue
|
|
if pinned_bound:
|
|
del commands[accelerator]
|
|
else:
|
|
commands[accelerator]["extension"] = pinned_id
|
|
pinned_bound = True
|
|
pinned_command = settings.get(pinned_id, {}).get("commands", {}).get("copy-url", {})
|
|
if pinned_command:
|
|
pinned_command["was_assigned"] = True
|
|
settings.pop(ghost, None)
|
|
changed = True
|
|
|
|
if changed:
|
|
shutil.copy2(path, path.with_name(path.name + ".omarchy-copy-url-repair.bak"))
|
|
path.write_text(json.dumps(preferences, separators=(",", ":")))
|
|
PY
|
|
)
|
|
|
|
profile_roots=(
|
|
"$HOME/.config/chromium"
|
|
"$HOME/.config/google-chrome"
|
|
"$HOME/.config/google-chrome-beta"
|
|
"$HOME/.config/google-chrome-unstable"
|
|
"$HOME/.config/BraveSoftware/Brave-Browser"
|
|
"$HOME/.config/BraveSoftware/Brave-Browser-Beta"
|
|
"$HOME/.config/BraveSoftware/Brave-Browser-Nightly"
|
|
"$HOME/.config/microsoft-edge"
|
|
"$HOME/.config/microsoft-edge-beta"
|
|
"$HOME/.config/microsoft-edge-dev"
|
|
"$HOME/.config/vivaldi"
|
|
"$HOME/.config/opera"
|
|
"$HOME/.config/helium"
|
|
)
|
|
|
|
find_pending() {
|
|
pending=()
|
|
for profile_root in "${profile_roots[@]}"; do
|
|
[[ -d $profile_root ]] || continue
|
|
|
|
for preferences in "$profile_root"/*/Preferences; do
|
|
[[ -f $preferences ]] || continue
|
|
python3 -c "$repair_py" "$preferences" "$pinned_id" check || continue
|
|
pending+=("$preferences")
|
|
done
|
|
done
|
|
}
|
|
|
|
# The backup a repair leaves next to Preferences marks it as attempted but not
|
|
# yet verified: a browser that was open during the repair still holds the
|
|
# stale registration in memory and restores it when it exits, possibly after
|
|
# the disk was already seen clean.
|
|
unverified_repairs_exist() {
|
|
local profile_root backup
|
|
|
|
for profile_root in "${profile_roots[@]}"; do
|
|
for backup in "$profile_root"/*/Preferences.omarchy-copy-url-repair.bak; do
|
|
[[ -f $backup ]] && return 0
|
|
done
|
|
done
|
|
|
|
return 1
|
|
}
|
|
|
|
# Only this user's browsers matter — another user's cannot rewrite these
|
|
# Preferences.
|
|
browsers_running() {
|
|
pgrep -x -u "$UID" 'chromium|chrome|brave|msedge|vivaldi-bin|vivaldi|opera|helium' >/dev/null 2>&1
|
|
}
|
|
|
|
find_pending
|
|
if (( ! ${#pending[@]} )) && ! unverified_repairs_exist; then
|
|
exit 0
|
|
fi
|
|
|
|
# A running browser holds Preferences in memory and rewrites the file on exit,
|
|
# reverting the repair, so ask for the windows to be closed first. Without a
|
|
# terminal to ask in, gum fails; then — as on decline — fail so the migration
|
|
# stays pending, and the login notifier keeps prompting until a rerun goes
|
|
# through with browsers closed.
|
|
while browsers_running; do
|
|
if ! gum confirm "Close all browser windows to repair the Copy URL shortcut, then continue" 2>/dev/null; then
|
|
echo "A running browser would undo the Copy URL shortcut repair." >&2
|
|
echo "Close all browser windows, then run: omarchy-migrate" >&2
|
|
exit 1
|
|
fi
|
|
done
|
|
|
|
# A browser that just closed may have restored ghosts an earlier repair had
|
|
# already removed, so only now is the pending list authoritative.
|
|
find_pending
|
|
|
|
for preferences in "${pending[@]}"; do
|
|
python3 -c "$repair_py" "$preferences" "$pinned_id" repair
|
|
done
|
|
|
|
# A browser that started mid-repair read the stale Preferences and will write
|
|
# them back on exit; stay pending so the next browser-free run can verify the
|
|
# repair stuck.
|
|
if browsers_running; then
|
|
echo "A browser started during the Copy URL shortcut repair and may undo it on exit." >&2
|
|
echo "Close all browser windows, then run: omarchy-migrate" >&2
|
|
exit 1
|
|
fi
|
|
|
|
# One that started and already exited mid-repair wrote its stale in-memory
|
|
# Preferences back over the repair; verify every file again. A browser
|
|
# starting after this point reads the repaired file, so it can no longer
|
|
# restore the ghost.
|
|
for preferences in "${pending[@]}"; do
|
|
if python3 -c "$repair_py" "$preferences" "$pinned_id" check; then
|
|
echo "A browser undid the Copy URL shortcut repair on exit." >&2
|
|
echo "Close all browser windows, then run: omarchy-migrate" >&2
|
|
exit 1
|
|
fi
|
|
done
|