diff --git a/bin/omarchy-upgrade-to-quattro b/bin/omarchy-upgrade-to-quattro index a5c58325..ce1c869a 100755 --- a/bin/omarchy-upgrade-to-quattro +++ b/bin/omarchy-upgrade-to-quattro @@ -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 diff --git a/default/chromium/extensions/copy-url/background-2.js b/default/chromium/extensions/copy-url/background-2.js new file mode 100644 index 00000000..822236a8 --- /dev/null +++ b/default/chromium/extensions/copy-url/background-2.js @@ -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); +}); diff --git a/default/chromium/extensions/copy-url/background.js b/default/chromium/extensions/copy-url/background.js deleted file mode 100644 index 8853a311..00000000 --- a/default/chromium/extensions/copy-url/background.js +++ /dev/null @@ -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: '' - }); - }); - }); - } -}); diff --git a/default/chromium/extensions/copy-url/manifest.json b/default/chromium/extensions/copy-url/manifest.json index 927fd2b0..fac9b7ed 100644 --- a/default/chromium/extensions/copy-url/manifest.json +++ b/default/chromium/extensions/copy-url/manifest.json @@ -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"} } diff --git a/default/chromium/extensions/copy-url/offscreen.html b/default/chromium/extensions/copy-url/offscreen.html new file mode 100644 index 00000000..973a56f6 --- /dev/null +++ b/default/chromium/extensions/copy-url/offscreen.html @@ -0,0 +1,9 @@ + + +
+ + + + + + diff --git a/default/chromium/extensions/copy-url/offscreen.js b/default/chromium/extensions/copy-url/offscreen.js new file mode 100644 index 00000000..a39d1171 --- /dev/null +++ b/default/chromium/extensions/copy-url/offscreen.js @@ -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); + } +}); diff --git a/test/shell.d/chromium-copy-url-test.sh b/test/shell.d/chromium-copy-url-test.sh index d8fe9bb7..b7398cce 100644 --- a/test/shell.d/chromium-copy-url-test.sh +++ b/test/shell.d/chromium-copy-url-test.sh @@ -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"