-
Notifications
You must be signed in to change notification settings - Fork 0
Expand file tree
/
Copy pathbackground.js
More file actions
248 lines (218 loc) · 8.72 KB
/
Copy pathbackground.js
File metadata and controls
248 lines (218 loc) · 8.72 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
import {
getSnippets, saveSnippets, getSnippetById,
matchUrl, isInitialized, setInitialized
} from './shared/storage.js';
// --- First-run: load default snippets ---
chrome.runtime.onInstalled.addListener(async (details) => {
if (details.reason === 'install') {
const alreadyInit = await isInitialized();
if (!alreadyInit) {
try {
const resp = await fetch(chrome.runtime.getURL('defaults/default-snippets.json'));
const defaults = await resp.json();
const snippets = defaults.map((s, i) => ({
id: crypto.randomUUID(),
name: s.name,
code: s.code,
allowedUrls: s.allowedUrls,
position: i
}));
await saveSnippets(snippets);
await setInitialized();
} catch (err) {
console.error('Failed to load default snippets:', err);
}
}
}
await rebuildContextMenus();
});
// --- Context menu management ---
let contextMenuRebuildTimer = null;
async function rebuildContextMenus() {
await chrome.contextMenus.removeAll();
// Toolbar-icon (action) menu: right-click the extension icon to dump every
// open tab's URL to the console and clipboard. Recreated on every rebuild so
// it survives the storage-change removeAll() below.
chrome.contextMenus.create({
id: 'list-all-tab-urls',
title: 'Copy all tab URLs to clipboard',
contexts: ['action']
});
chrome.contextMenus.create({
id: 'tcs-parent',
title: 'Trigger Code Snippets',
contexts: ['page']
});
const snippets = await getSnippets();
for (const snippet of snippets) {
const shortcutHint = snippet.position < 9
? ` [Alt+Shift+${snippet.position + 1}]`
: '';
chrome.contextMenus.create({
id: `tcs-snippet-${snippet.id}`,
parentId: 'tcs-parent',
title: snippet.name + shortcutHint,
contexts: ['page'],
enabled: false // disabled by default, updated per tab
});
}
// Update states for the current active tab
try {
const [tab] = await chrome.tabs.query({ active: true, currentWindow: true });
if (tab) await updateContextMenuStates(tab.id);
} catch { /* ignore if no active tab */ }
}
async function updateContextMenuStates(tabId) {
try {
const tab = await chrome.tabs.get(tabId);
if (!tab.url) return; // chrome:// pages etc.
const snippets = await getSnippets();
for (const snippet of snippets) {
try {
const enabled = matchUrl(snippet.allowedUrls, tab.url);
chrome.contextMenus.update(`tcs-snippet-${snippet.id}`, { enabled });
} catch { /* menu item may not exist yet */ }
}
} catch { /* tab may no longer exist */ }
}
// Update context menu states when the active tab changes or navigates
chrome.tabs.onActivated.addListener(({ tabId }) => updateContextMenuStates(tabId));
chrome.tabs.onUpdated.addListener((tabId, changeInfo) => {
if (changeInfo.url || changeInfo.status === 'complete') {
updateContextMenuStates(tabId);
}
});
// --- Context menu click handler ---
chrome.contextMenus.onClicked.addListener(async (info, tab) => {
console.log('[TCS] Context menu clicked:', info.menuItemId);
if (info.menuItemId === 'list-all-tab-urls') {
await listAllTabUrls();
return;
}
if (!info.menuItemId.toString().startsWith('tcs-snippet-')) return;
const snippetId = info.menuItemId.toString().replace('tcs-snippet-', '');
const snippet = await getSnippetById(snippetId);
if (!snippet) { console.log('[TCS] BAIL: context menu snippet not found'); return; }
if (!matchUrl(snippet.allowedUrls, tab.url)) { console.log('[TCS] BAIL: context menu URL mismatch'); return; }
executeSnippet(snippet, tab.id);
});
// --- Snippet execution ---
async function executeSnippet(snippet, tabId) {
console.log('[TCS] executeSnippet called:', snippet.name, 'on tab:', tabId, 'code length:', snippet.code.length);
const target = { tabId };
try {
await chrome.debugger.attach(target, '1.3');
await chrome.debugger.sendCommand(target, 'Runtime.evaluate', {
expression: snippet.code,
userGesture: true
});
await chrome.debugger.detach(target);
console.log('[TCS] executeScript SUCCESS for:', snippet.name);
} catch (err) {
try { await chrome.debugger.detach(target); } catch { /* already detached */ }
console.error('[TCS] executeScript FAILED for:', snippet.name, err);
}
}
// --- List all tab URLs (toolbar-icon menu) ---
async function listAllTabUrls() {
// Spanning mode (no "incognito":"split" in the manifest) means query({})
// returns tabs from every window, including incognito.
const tabs = await chrome.tabs.query({});
const urls = tabs.map(t => t.url).filter(Boolean);
const text = urls.join('\n');
console.log(`[TCS] All tab URLs (${urls.length}):\n${text}`);
return copyToClipboard(text);
}
// --- Offscreen clipboard (service workers have no clipboard access) ---
let creatingOffscreen = null; // in-flight createDocument promise, serializes callers
async function ensureOffscreenDocument() {
const offscreenUrl = chrome.runtime.getURL('offscreen.html');
const existing = await chrome.runtime.getContexts({
contextTypes: ['OFFSCREEN_DOCUMENT'],
documentUrls: [offscreenUrl]
});
if (existing.length > 0) return;
// getContexts alone can't prevent a double-create race (two rapid clicks both
// see zero contexts); the shared `creating` promise makes the second caller
// await the first createDocument instead of firing its own.
if (creatingOffscreen) {
await creatingOffscreen;
} else {
creatingOffscreen = chrome.offscreen.createDocument({
url: 'offscreen.html',
reasons: ['CLIPBOARD'],
justification: 'Copy the list of open tab URLs to the clipboard.'
});
try {
await creatingOffscreen;
} finally {
creatingOffscreen = null;
}
}
}
async function copyToClipboard(text) {
try {
await ensureOffscreenDocument();
// Await the offscreen ack so the write finishes before we close the doc.
const resp = await chrome.runtime.sendMessage({
target: 'tcs-offscreen',
type: 'copy-to-clipboard',
text
});
if (!resp || !resp.ok) console.error('[TCS] Clipboard write did not confirm success');
return !!(resp && resp.ok);
} catch (err) {
console.error('[TCS] Clipboard copy failed:', err);
return false;
} finally {
// CLIPBOARD offscreen docs have no auto-close; free the single-doc slot.
try { await chrome.offscreen.closeDocument(); } catch { /* already closed */ }
}
}
// --- Message listener ---
chrome.runtime.onMessage.addListener((message, sender, sendResponse) => {
console.log('[TCS] Message received:', message.type, message);
if (message.type === 'execute-by-position') {
handleExecuteByPosition(message.position, sender.tab).then(() => sendResponse({ ok: true }));
return true;
} else if (message.type === 'execute-by-id') {
handleExecuteById(message.snippetId, message.tabId).then(() => sendResponse({ ok: true }));
return true;
} else if (message.type === 'copy-all-tab-urls') {
listAllTabUrls().then((ok) => sendResponse({ ok }));
return true;
}
});
async function handleExecuteByPosition(position, tab) {
console.log('[TCS] handleExecuteByPosition:', position, 'tab:', tab?.url);
if (!tab || !tab.url) { console.log('[TCS] BAIL: no tab or url'); return; }
const snippets = await getSnippets();
const snippet = snippets.find(s => s.position === position);
if (!snippet) { console.log('[TCS] BAIL: no snippet at position', position); return; }
console.log('[TCS] Found snippet:', snippet.name, 'allowedUrls:', snippet.allowedUrls);
const urlMatch = matchUrl(snippet.allowedUrls, tab.url);
console.log('[TCS] URL match result:', urlMatch, 'tab.url:', tab.url);
if (!urlMatch) return;
executeSnippet(snippet, tab.id);
}
async function handleExecuteById(snippetId, tabId) {
console.log('[TCS] handleExecuteById:', snippetId, 'tabId:', tabId);
const snippet = await getSnippetById(snippetId);
if (!snippet) { console.log('[TCS] BAIL: snippet not found for id:', snippetId); return; }
console.log('[TCS] Found snippet:', snippet.name, 'allowedUrls:', snippet.allowedUrls);
try {
const tab = await chrome.tabs.get(tabId);
console.log('[TCS] Tab url:', tab.url);
const urlMatch = matchUrl(snippet.allowedUrls, tab.url);
console.log('[TCS] URL match result:', urlMatch);
if (!urlMatch) { console.log('[TCS] BAIL: URL does not match'); return; }
executeSnippet(snippet, tabId);
} catch (e) { console.log('[TCS] BAIL: tab error:', e.message); }
}
// --- Rebuild context menus when snippets change ---
chrome.storage.onChanged.addListener((changes, area) => {
if (area === 'local' && changes.snippets) {
clearTimeout(contextMenuRebuildTimer);
contextMenuRebuildTimer = setTimeout(() => rebuildContextMenus(), 500);
}
});