gum draws its confirm UI on stderr, so the migration's `2>/dev/null` threw away the whole prompt while gum still held the terminal in raw mode reading keys. With a browser open, an update stopped after "Running migration (1786643346)" on an unpainted screen with no way to tell it was waiting for an answer. Nothing else in the repo suppresses gum's stderr; the redirect only ever hid gum's own error in the no-terminal case, where the migration already explains itself on stderr before deferring. Fixes #6841 Claude-Session: https://claude.ai/code/session_01So4yKzTDdJJp7orjnF8i1J Co-authored-by: Claude Opus 5 (1M context) <noreply@anthropic.com>
179 lines
6.2 KiB
Bash
179 lines
6.2 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. gum draws
|
|
# the prompt on stderr, so it must stay attached: suppressing it leaves gum
|
|
# waiting for a keypress behind an unpainted screen. 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"; 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
|