Make the Copy URL extension reliable across install and upgrade
The Copy URL extension was intermittently failing to copy anything, and
its notification looked oversized with no icon. Several distinct problems:
- The MV3 service worker was pinned to stale cached code. Chromium caches
the worker script for a --load-extension command-line extension and does
NOT re-register it when the file changes in place — not on restart, not on
a manifest version bump, not even after a clean shutdown. So updated code
never took effect: the old worker kept calling chrome.scripting.executeScript,
which throws once the scripting permission is gone, and the copy silently
failed. Renaming the worker to a versioned filename (background-2.js) is a
new script URL, which forces a fresh registration for everyone — new
installs and existing installs alike. Rename it again on any future worker
change (see the note at the top of background-2.js).
- Clipboard writes now go through an offscreen document (MV3's sanctioned
path) using a textarea + execCommand('copy'), replacing executeScript +
navigator.clipboard. This works on chrome:// pages and needs no scripting
permission. execCommand is used deliberately: navigator.clipboard.writeText
rejects in an unfocused offscreen document.
- Add a toolbar action (chrome.action.onClicked) so the extension is
clickable, not keyboard-only — it was greyed out in the extensions menu
with nothing to invoke.
- Slim the "URL copied" notification: lead the summary with a glyph so the
omarchy notification shell collapses it to a single-line toast with an
icon, instead of an oversized card with a blank icon slot.
- Guard the Quattro shortcut-repair migration: it edits Chromium Preferences
to move the Alt+Shift+L binding to the new extension id, but a running
browser rewrites Preferences on exit and reverts the edit. Prompt (via gum)
to close the browser first, only when a browser is running and there is
actually a stale binding to repair.
This commit is contained in:
@@ -1716,6 +1716,11 @@ CHROMIUM_FLAGS_PATCH_PY
|
||||
done
|
||||
}
|
||||
|
||||
# True if any Chromium-family browser that stores the Copy URL shortcut is running.
|
||||
browser_is_running() {
|
||||
pgrep -x 'chromium|chrome|brave|msedge|vivaldi-bin|vivaldi|opera|helium' >/dev/null 2>&1
|
||||
}
|
||||
|
||||
repair_chromium_copy_url_shortcuts() {
|
||||
local profile_root preferences
|
||||
local -a profile_roots=(
|
||||
@@ -1730,6 +1735,7 @@ repair_chromium_copy_url_shortcuts() {
|
||||
"$HOME/.config/opera"
|
||||
"$HOME/.config/helium"
|
||||
)
|
||||
local -a pending=()
|
||||
|
||||
for profile_root in "${profile_roots[@]}"; do
|
||||
[[ -d $profile_root ]] || continue
|
||||
@@ -1737,8 +1743,23 @@ repair_chromium_copy_url_shortcuts() {
|
||||
for preferences in "$profile_root"/*/Preferences; do
|
||||
[[ -f $preferences ]] || continue
|
||||
grep -qF 'bocglpkldciamkbmlphanhkfnhpmnbma' "$preferences" || continue
|
||||
pending+=("$preferences")
|
||||
done
|
||||
done
|
||||
|
||||
python3 - "$preferences" "$preferences.omarchy-upgrade-to-quattro.$BACKUP_SUFFIX.bak" <<'CHROMIUM_SHORTCUTS_PATCH_PY'
|
||||
(( ${#pending[@]} )) || return 0
|
||||
|
||||
# A running browser holds Preferences in memory and rewrites it on exit,
|
||||
# which would revert this repair. Ask the user to close it first.
|
||||
if browser_is_running && (( ! yes )) && [[ -r /dev/tty ]]; then
|
||||
if ! gum confirm "Close all browser windows before the Copy URL shortcut is repaired, then continue"; then
|
||||
echo "Skipped Copy URL shortcut repair; re-run after closing your browser." >&2
|
||||
return 0
|
||||
fi
|
||||
fi
|
||||
|
||||
for preferences in "${pending[@]}"; do
|
||||
python3 - "$preferences" "$preferences.omarchy-upgrade-to-quattro.$BACKUP_SUFFIX.bak" <<'CHROMIUM_SHORTCUTS_PATCH_PY'
|
||||
import json
|
||||
import shutil
|
||||
import sys
|
||||
@@ -1768,7 +1789,6 @@ if changed:
|
||||
shutil.copy2(path, backup_path)
|
||||
path.write_text(json.dumps(preferences, separators=(",", ":")))
|
||||
CHROMIUM_SHORTCUTS_PATCH_PY
|
||||
done
|
||||
done
|
||||
}
|
||||
|
||||
@@ -1935,7 +1955,7 @@ repair_sleep_lock_unit_override
|
||||
install_bash_startup
|
||||
|
||||
unset rel file backup retired_config_files always_copy_config_files uwsm_env known_config_default_hashes_by_key
|
||||
unset -f file_sha256 known_config_default_hashes default_hash_paths is_known_default_hash backup_config_file migrate_uwsm_env_customizations is_retired_config_file copy_config_default copy_missing_config_defaults refresh_known_config_defaults mark_removed_preinstalls_from_legacy_bindings copy_always_config_defaults repair_chromium_copy_url_extension_flags repair_chromium_copy_url_shortcuts repair_sleep_lock_unit_override is_known_bashrc_default install_bash_startup
|
||||
unset -f file_sha256 known_config_default_hashes default_hash_paths is_known_default_hash backup_config_file migrate_uwsm_env_customizations is_retired_config_file copy_config_default copy_missing_config_defaults refresh_known_config_defaults mark_removed_preinstalls_from_legacy_bindings copy_always_config_defaults repair_chromium_copy_url_extension_flags browser_is_running repair_chromium_copy_url_shortcuts repair_sleep_lock_unit_override is_known_bashrc_default install_bash_startup
|
||||
|
||||
mkdir -p "$HOME/.agents/skills" "$HOME/.claude/skills" "$HOME/.codex/skills" "$HOME/.pi/agent/skills"
|
||||
if [[ -d $root/default/omarchy-skill ]]; then
|
||||
|
||||
@@ -0,0 +1,65 @@
|
||||
// NOTE: this file is intentionally version-numbered (background-2.js). Chromium
|
||||
// caches the service worker for a command-line --load-extension and does NOT
|
||||
// re-register it when the script changes in place, so an updated worker never
|
||||
// takes effect on existing installs. Changing the filename is a new script URL,
|
||||
// which forces a fresh registration. If you change this worker's code, rename it
|
||||
// (background-3.js, ...) and update manifest.json's background.service_worker.
|
||||
|
||||
let creatingOffscreenDocument;
|
||||
|
||||
async function ensureOffscreenDocument() {
|
||||
if (await chrome.offscreen.hasDocument()) return;
|
||||
|
||||
if (!creatingOffscreenDocument) {
|
||||
creatingOffscreenDocument = chrome.offscreen.createDocument({
|
||||
url: 'offscreen.html',
|
||||
reasons: ['CLIPBOARD'],
|
||||
justification: 'Copy the active tab URL to the clipboard'
|
||||
}).finally(() => {
|
||||
creatingOffscreenDocument = undefined;
|
||||
});
|
||||
}
|
||||
|
||||
await creatingOffscreenDocument;
|
||||
}
|
||||
|
||||
async function copyUrl(url) {
|
||||
if (!url) return;
|
||||
|
||||
try {
|
||||
await ensureOffscreenDocument();
|
||||
const copied = await chrome.runtime.sendMessage({
|
||||
target: 'offscreen',
|
||||
type: 'copy-url',
|
||||
url
|
||||
});
|
||||
|
||||
if (!copied) return;
|
||||
|
||||
// The omarchy notification shell renders a chromium toast slim (no icon
|
||||
// slot) when the summary begins with a glyph followed by 2+ spaces and the
|
||||
// body is empty. iconUrl is a required field, so keep a 1x1 transparent png.
|
||||
chrome.notifications.create({
|
||||
type: 'basic',
|
||||
iconUrl: 'data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAEAAAABCAYAAAAfFcSJAAAAC0lEQVR4nGNgAAIAAAUAAXpeqz8AAAAASUVORK5CYII=',
|
||||
title: ' URL copied to clipboard',
|
||||
message: ''
|
||||
});
|
||||
} catch (error) {
|
||||
console.error('[copy-url] failed:', error);
|
||||
}
|
||||
}
|
||||
|
||||
// Keyboard shortcut (Alt+Shift+L).
|
||||
chrome.commands.onCommand.addListener((command) => {
|
||||
if (command !== 'copy-url') return;
|
||||
|
||||
chrome.tabs.query({ active: true, currentWindow: true }, (tabs) => {
|
||||
copyUrl(tabs[0] && tabs[0].url);
|
||||
});
|
||||
});
|
||||
|
||||
// Clicking the extension's toolbar icon.
|
||||
chrome.action.onClicked.addListener((tab) => {
|
||||
copyUrl(tab && tab.url);
|
||||
});
|
||||
@@ -1,21 +0,0 @@
|
||||
chrome.commands.onCommand.addListener((command) => {
|
||||
if (command === 'copy-url') {
|
||||
chrome.tabs.query({ active: true, currentWindow: true }, (tabs) => {
|
||||
const currentTab = tabs[0];
|
||||
|
||||
chrome.scripting.executeScript({
|
||||
target: { tabId: currentTab.id },
|
||||
func: () => {
|
||||
navigator.clipboard.writeText(window.location.href);
|
||||
}
|
||||
}).then(() => {
|
||||
chrome.notifications.create({
|
||||
type: 'basic',
|
||||
iconUrl: 'data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAEAAAABCAYAAAAfFcSJAAAAC0lEQVR4nGNgAAIAAAUAAXpeqz8AAAAASUVORK5CYII=',
|
||||
title: ' URL copied to clipboard',
|
||||
message: ''
|
||||
});
|
||||
});
|
||||
});
|
||||
}
|
||||
});
|
||||
@@ -1,10 +1,11 @@
|
||||
{
|
||||
"manifest_version": 3,
|
||||
"name": "Copy URL",
|
||||
"version": "1.0",
|
||||
"version": "1.3",
|
||||
"description": "Copy current URL to clipboard, this extension is installed by Omarchy",
|
||||
"action": { "default_title": "Copy URL" },
|
||||
"key": "MIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEAxbOMKgGcG20uelP6lx5OnuafGP9gNR1ajzURuQ45tv/Is5sc1tn9ii9PsKxpzDzPL9Z5lAqrTFgEcKHyTJnZXAGOMIruB0odJ0BW6eoQ3rxaQ/fBK2yGN7FZAfygPOWHj+Fdrh2eYQ7Ap92Dmlz8x0ceGyais415KmeevVcPwS7jjHFd1bGBp17cEMOBQcOx/3zCnko1hGtCzSdyZQl3cNMebW/FGvmxlPUMLVfWhysfFUuzxMRRf58+/j0mo/9iKQ0uTuxbQ8+W64OmYOVR87IXYNgbo+hEtORjiQhVg1KzaBx1bZ6clQMv96W/xYy1cIzUA5yI9y7lNr7n9zKijwIDAQAB",
|
||||
"permissions": ["activeTab", "scripting", "notifications"],
|
||||
"permissions": ["activeTab", "clipboardWrite", "notifications", "offscreen"],
|
||||
"icons": {
|
||||
"16": "icon.png",
|
||||
"48": "icon.png",
|
||||
@@ -16,5 +17,5 @@
|
||||
"description": "Copy URL"
|
||||
}
|
||||
},
|
||||
"background": {"service_worker": "background.js"}
|
||||
"background": {"service_worker": "background-2.js"}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,9 @@
|
||||
<!doctype html>
|
||||
<html>
|
||||
<head>
|
||||
<script src="offscreen.js"></script>
|
||||
</head>
|
||||
<body>
|
||||
<textarea id="target"></textarea>
|
||||
</body>
|
||||
</html>
|
||||
@@ -0,0 +1,17 @@
|
||||
chrome.runtime.onMessage.addListener((message, _sender, sendResponse) => {
|
||||
if (message.target !== 'offscreen' || message.type !== 'copy-url') return;
|
||||
|
||||
// Offscreen documents are never focused, so navigator.clipboard.writeText
|
||||
// rejects with "Document is not focused". execCommand('copy') has no focus
|
||||
// requirement and is authorized by the clipboardWrite permission.
|
||||
try {
|
||||
const target = document.getElementById('target');
|
||||
target.value = message.url;
|
||||
target.focus();
|
||||
target.select();
|
||||
sendResponse(document.execCommand('copy'));
|
||||
} catch (error) {
|
||||
console.error('[copy-url offscreen] error:', error);
|
||||
sendResponse(false);
|
||||
}
|
||||
});
|
||||
@@ -36,6 +36,23 @@ JS
|
||||
fail "copy-url extension manifest has the stable id" "$copy_url_id"
|
||||
pass "copy-url extension manifest has the stable id"
|
||||
|
||||
jq -e '
|
||||
.manifest_version == 3 and
|
||||
(.permissions | index("clipboardWrite")) and
|
||||
(.permissions | index("offscreen")) and
|
||||
(.background.service_worker | startswith("background-"))
|
||||
' "$ROOT/default/chromium/extensions/copy-url/manifest.json" >/dev/null ||
|
||||
fail "copy-url extension uses an offscreen clipboard document"
|
||||
[[ -f $ROOT/default/chromium/extensions/copy-url/offscreen.html &&
|
||||
-f $ROOT/default/chromium/extensions/copy-url/offscreen.js ]] ||
|
||||
fail "copy-url extension ships its offscreen clipboard document"
|
||||
pass "copy-url extension uses an offscreen clipboard document"
|
||||
|
||||
jq -e '.action != null' "$ROOT/default/chromium/extensions/copy-url/manifest.json" >/dev/null &&
|
||||
grep -q 'action.onClicked' "$ROOT/default/chromium/extensions/copy-url/"background-*.js ||
|
||||
fail "copy-url extension is clickable from the toolbar"
|
||||
pass "copy-url extension is clickable from the toolbar"
|
||||
|
||||
TMPDIR=$(mktemp -d)
|
||||
preferences="$TMPDIR/Preferences"
|
||||
backup="$TMPDIR/Preferences.bak"
|
||||
|
||||
Reference in New Issue
Block a user