experiments in a post-browser web
10
fork

Configure Feed

Select the types of activity you want to include in your feed.

feat(cmd): add Cmd+L URL-only navigation mode

+328 -2
+13 -1
extensions/cmd/background.js
··· 392 392 * Register shortcuts: global (Option+Space) and local (Cmd+K) 393 393 */ 394 394 const LOCAL_SHORTCUT = 'CommandOrControl+K'; 395 + const URL_MODE_SHORTCUT = 'CommandOrControl+L'; 395 396 396 397 const initShortcut = (prefs) => { 397 398 if (registeredShortcut) { 398 399 api.shortcuts.unregister(registeredShortcut, { global: true }); 399 400 } 400 401 api.shortcuts.unregister(LOCAL_SHORTCUT); 402 + api.shortcuts.unregister(URL_MODE_SHORTCUT); 401 403 402 404 registeredShortcut = prefs.shortcutKey; 403 405 api.shortcuts.register(prefs.shortcutKey, () => { ··· 409 411 openPanelWindow(prefs); 410 412 }); 411 413 412 - log('ext:cmd', 'Registered shortcuts:', prefs.shortcutKey, '(global),', LOCAL_SHORTCUT, '(local)'); 414 + // URL mode shortcut (Cmd+L) — opens panel in URL-only navigation mode 415 + // Page host windows intercept Cmd+L via before-input-event for their navbar, 416 + // so this only fires from non-page windows. 417 + api.shortcuts.register(URL_MODE_SHORTCUT, () => { 418 + api.publish('cmd:url-mode', {}, api.scopes.GLOBAL); 419 + openPanelWindow(prefs); 420 + }); 421 + 422 + log('ext:cmd', 'Registered shortcuts:', prefs.shortcutKey, '(global),', LOCAL_SHORTCUT, '(local),', URL_MODE_SHORTCUT, '(url-mode)'); 413 423 }; 414 424 415 425 /** ··· 423 433 registeredShortcut = null; 424 434 } 425 435 api.shortcuts.unregister(LOCAL_SHORTCUT); 436 + api.shortcuts.unregister(URL_MODE_SHORTCUT); 426 437 427 438 // Note: We don't clear the command registry here because other extensions 428 439 // may still be running. The registry will be rebuilt on next init. ··· 440 451 registeredShortcut = null; 441 452 } 442 453 api.shortcuts.unregister(LOCAL_SHORTCUT); 454 + api.shortcuts.unregister(URL_MODE_SHORTCUT); 443 455 444 456 // Load new settings and re-register 445 457 currentSettings = await loadSettings();
+33
extensions/cmd/panel.html
··· 189 189 margin-left: 4px; 190 190 } 191 191 192 + /* URL mode result items */ 193 + .url-result-item { 194 + display: flex; 195 + align-items: center; 196 + gap: 8px; 197 + } 198 + 199 + .url-favicon { 200 + flex-shrink: 0; 201 + border-radius: 2px; 202 + } 203 + 204 + .url-title { 205 + flex: 1; 206 + min-width: 0; 207 + overflow: hidden; 208 + text-overflow: ellipsis; 209 + white-space: nowrap; 210 + } 211 + 212 + .url-subtitle { 213 + flex-shrink: 0; 214 + max-width: 200px; 215 + overflow: hidden; 216 + text-overflow: ellipsis; 217 + white-space: nowrap; 218 + } 219 + 192 220 /* Chain indicator */ 193 221 #chain-indicator { 194 222 display: none; ··· 465 493 .mode-indicator[data-mode="settings"] { 466 494 background: rgba(204, 153, 102, 0.4); 467 495 border-color: rgba(204, 153, 102, 0.6); 496 + } 497 + 498 + .mode-indicator[data-mode="url"] { 499 + background: rgba(52, 199, 89, 0.4); 500 + border-color: rgba(52, 199, 89, 0.6); 468 501 } 469 502 470 503 .mode-indicator[data-mode="default"] {
+282 -1
extensions/cmd/panel.js
··· 87 87 log('cmd:panel', 'Loaded adaptive data'); 88 88 }); 89 89 90 + // Listen for URL mode activation (from Cmd+L shortcut in background.js) 91 + api.subscribe('cmd:url-mode', () => { 92 + log('cmd:panel', 'URL mode requested'); 93 + pendingUrlMode = true; 94 + // If panel is already visible (no upcoming visibilitychange), enter immediately 95 + if (!document.hidden && !urlModeActive) { 96 + enterUrlMode(); 97 + } 98 + }, api.scopes.GLOBAL); 99 + 90 100 // Chain popup state (module-level for cleanup) 91 101 let chainPopupWindowId = null; 92 102 ··· 100 110 let currentMode = 'default'; 101 111 let currentModeMetadata = {}; 102 112 let modeWasSet = false; 113 + 114 + // URL mode state — activated by Cmd+L shortcut 115 + let pendingUrlMode = false; 116 + let urlModeActive = false; 117 + let urlModeItems = []; // frecency-sorted URL items for display 103 118 104 119 // Expose state for testing — allows waitForFunction to check command loading 105 120 // Provide backward-compatible boolean accessors on top of machine data ··· 1186 1201 return bestMatch; 1187 1202 } 1188 1203 1204 + // ===== URL Mode Functions ===== 1205 + 1206 + /** 1207 + * Query frecency-sorted URL items from the datastore. 1208 + * Optionally filter by search text (matches title, domain, content). 1209 + */ 1210 + async function queryUrlsByFrecency(search = '', limit = 20) { 1211 + const query = { type: 'url', sortBy: 'frecency', limit }; 1212 + if (search) query.search = search; 1213 + 1214 + const result = await api.datastore.queryItems(query); 1215 + if (!result.success) return []; 1216 + return result.data || []; 1217 + } 1218 + 1219 + /** 1220 + * Enter URL mode: query frecency URLs and show them as results. 1221 + */ 1222 + async function enterUrlMode() { 1223 + urlModeActive = true; 1224 + pendingUrlMode = false; 1225 + 1226 + // Set mode indicator 1227 + currentMode = 'url'; 1228 + updateModeIndicator(); 1229 + 1230 + // Update placeholder 1231 + const input = document.getElementById('command-input'); 1232 + if (input) { 1233 + input.placeholder = 'Search URLs or type an address...'; 1234 + } 1235 + 1236 + // Query frecency-sorted URLs 1237 + urlModeItems = await queryUrlsByFrecency('', 20); 1238 + log('cmd:panel', 'URL mode: loaded', urlModeItems.length, 'frecency URLs'); 1239 + 1240 + // Show results immediately 1241 + renderUrlResults(urlModeItems); 1242 + } 1243 + 1244 + /** 1245 + * Exit URL mode and restore normal panel behavior. 1246 + */ 1247 + function exitUrlMode() { 1248 + urlModeActive = false; 1249 + urlModeItems = []; 1250 + 1251 + const input = document.getElementById('command-input'); 1252 + if (input) { 1253 + input.placeholder = 'Feed meeeee'; 1254 + } 1255 + } 1256 + 1257 + /** 1258 + * Filter URL mode items based on typed text. 1259 + */ 1260 + async function filterUrlModeItems(text) { 1261 + if (!urlModeActive) return; 1262 + 1263 + if (!text) { 1264 + // Show default frecency list 1265 + urlModeItems = await queryUrlsByFrecency('', 20); 1266 + } else { 1267 + // Query with search filter 1268 + urlModeItems = await queryUrlsByFrecency(text, 20); 1269 + } 1270 + 1271 + renderUrlResults(urlModeItems); 1272 + } 1273 + 1274 + /** 1275 + * Render URL results in the results container. 1276 + * Shows favicon, title, and domain/URL for each item. 1277 + */ 1278 + function renderUrlResults(items) { 1279 + const resultsContainer = document.getElementById('results'); 1280 + resultsContainer.innerHTML = ''; 1281 + 1282 + if (items.length === 0) { 1283 + resultsContainer.classList.remove('visible'); 1284 + updateWindowSize(); 1285 + return; 1286 + } 1287 + 1288 + resultsContainer.classList.add('visible'); 1289 + 1290 + items.forEach((item, index) => { 1291 + const el = document.createElement('div'); 1292 + el.className = 'command-item url-result-item'; 1293 + if (index === state.matchIndex) el.classList.add('selected'); 1294 + 1295 + // Favicon 1296 + const faviconUrl = item.favicon || ''; 1297 + if (faviconUrl) { 1298 + const favicon = document.createElement('img'); 1299 + favicon.className = 'url-favicon'; 1300 + favicon.src = faviconUrl; 1301 + favicon.width = 16; 1302 + favicon.height = 16; 1303 + favicon.onerror = () => { favicon.style.display = 'none'; }; 1304 + el.appendChild(favicon); 1305 + } 1306 + 1307 + // Title 1308 + const titleSpan = document.createElement('span'); 1309 + titleSpan.className = 'cmd-name url-title'; 1310 + let title = item.title || ''; 1311 + if (!title && item.metadata) { 1312 + try { 1313 + const meta = typeof item.metadata === 'string' ? JSON.parse(item.metadata) : item.metadata; 1314 + title = meta.title || ''; 1315 + } catch (e) {} 1316 + } 1317 + titleSpan.textContent = title || item.content || 'Untitled'; 1318 + el.appendChild(titleSpan); 1319 + 1320 + // Domain / URL subtitle 1321 + const urlSpan = document.createElement('span'); 1322 + urlSpan.className = 'cmd-desc url-subtitle'; 1323 + urlSpan.textContent = item.domain || item.content || ''; 1324 + el.appendChild(urlSpan); 1325 + 1326 + el.addEventListener('click', () => { 1327 + state.matchIndex = index; 1328 + openUrlModeItem(index); 1329 + }); 1330 + 1331 + resultsContainer.appendChild(el); 1332 + }); 1333 + 1334 + // Update match state for keyboard navigation 1335 + state.matches = items.map((_, i) => `__url_${i}`); 1336 + state.matchIndex = 0; 1337 + 1338 + updateWindowSize(); 1339 + } 1340 + 1341 + /** 1342 + * Open a URL from URL mode results by index. 1343 + */ 1344 + function openUrlModeItem(index) { 1345 + const item = urlModeItems[index]; 1346 + if (!item) return; 1347 + 1348 + const url = item.content; 1349 + if (!url) return; 1350 + 1351 + log('cmd:panel', 'URL mode: opening', url); 1352 + 1353 + api.window.open(url, { 1354 + role: 'content', 1355 + width: 1024, 1356 + height: 768, 1357 + trackingSource: 'cmd', 1358 + trackingSourceId: 'url-mode' 1359 + }).catch(error => { 1360 + log.error('cmd:panel', 'Failed to open URL from URL mode:', error); 1361 + }); 1362 + 1363 + // Record frecency 1364 + updateMatchCount(url); 1365 + 1366 + // Close panel 1367 + setTimeout(() => window.close(), 100); 1368 + } 1369 + 1370 + /** 1371 + * Update the ghost text overlay in URL mode. 1372 + * Shows the selected URL's title as ghost completion. 1373 + */ 1374 + function updateCommandUI_UrlMode(trimmedText) { 1375 + const commandText = document.getElementById('command-text'); 1376 + commandText.innerHTML = ''; 1377 + 1378 + if (!trimmedText) return; 1379 + 1380 + const typedSpan = document.createElement('span'); 1381 + typedSpan.className = 'typed'; 1382 + typedSpan.textContent = trimmedText; 1383 + commandText.appendChild(typedSpan); 1384 + 1385 + syncScrollPosition(); 1386 + } 1387 + 1189 1388 function hasModifier(e) { 1190 1389 return e.altKey || e.ctrlKey || e.metaKey; 1191 1390 } ··· 2024 2223 state.chainPopupActive = false; 2025 2224 state.showResults = false; 2026 2225 2226 + // Exit URL mode if active from previous invocation 2227 + exitUrlMode(); 2228 + 2027 2229 currentMode = 'default'; 2028 2230 currentModeMetadata = {}; 2029 2231 modeWasSet = false; ··· 2044 2246 2045 2247 updateCommandUI(); 2046 2248 updateResultsUI(); 2249 + 2250 + // Check if URL mode was requested (Cmd+L) 2251 + if (pendingUrlMode) { 2252 + enterUrlMode(); 2253 + } 2047 2254 }, 2048 2255 }; 2049 2256 } ··· 2063 2270 commandInput.addEventListener('keyup', syncScrollPosition); 2064 2271 commandInput.addEventListener('click', syncScrollPosition); 2065 2272 2066 - // Input event -> dispatch to state machine 2273 + // Input event -> dispatch to state machine (or filter URL mode) 2067 2274 commandInput.addEventListener('input', () => { 2068 2275 const value = commandInput.value; 2069 2276 const trimmedText = value.trim(); 2070 2277 2278 + // URL mode: filter URLs instead of dispatching normal command matching 2279 + if (urlModeActive) { 2280 + state.typed = value; 2281 + state.matchIndex = 0; 2282 + filterUrlModeItems(trimmedText); 2283 + updateCommandUI_UrlMode(trimmedText); 2284 + return; 2285 + } 2286 + 2071 2287 // Pre-compute whether param mode should be entered 2072 2288 // (the action handler will check, but the guard needs the signal) 2073 2289 let enterParamMode = false; ··· 2113 2329 2114 2330 // Keydown event -> dispatch special keys through state machine 2115 2331 commandInput.addEventListener('keydown', (e) => { 2332 + // URL mode handles its own key events 2333 + if (urlModeActive) { 2334 + if (e.key === 'Escape' && !hasModifier(e)) { 2335 + e.preventDefault(); 2336 + exitUrlMode(); 2337 + machine.dispatch(Events.ESCAPE); 2338 + return; 2339 + } 2340 + if (e.key === 'Enter' && !hasModifier(e)) { 2341 + e.preventDefault(); 2342 + const trimmedText = commandInput.value.trim(); 2343 + const urlResult = getValidURL(trimmedText); 2344 + 2345 + if (urlModeItems.length > 0 && state.matchIndex < urlModeItems.length) { 2346 + // Open selected URL from results 2347 + openUrlModeItem(state.matchIndex); 2348 + } else if (urlResult.valid) { 2349 + // Open typed URL directly 2350 + log('cmd:panel', 'URL mode: opening typed URL', urlResult.url); 2351 + api.window.open(urlResult.url, { 2352 + role: 'content', 2353 + width: 1024, 2354 + height: 768, 2355 + trackingSource: 'cmd', 2356 + trackingSourceId: 'url-mode-typed' 2357 + }).catch(error => { 2358 + log.error('cmd:panel', 'Failed to open typed URL:', error); 2359 + }); 2360 + updateMatchCount(urlResult.url); 2361 + setTimeout(() => window.close(), 100); 2362 + } 2363 + return; 2364 + } 2365 + if (e.key === 'ArrowDown') { 2366 + e.preventDefault(); 2367 + if (urlModeItems.length > 0) { 2368 + state.matchIndex = Math.min(state.matchIndex + 1, urlModeItems.length - 1); 2369 + renderUrlResults(urlModeItems); 2370 + } 2371 + return; 2372 + } 2373 + if (e.key === 'ArrowUp') { 2374 + e.preventDefault(); 2375 + if (urlModeItems.length > 0) { 2376 + state.matchIndex = Math.max(state.matchIndex - 1, 0); 2377 + renderUrlResults(urlModeItems); 2378 + } 2379 + return; 2380 + } 2381 + if (e.key === 'Tab') { 2382 + e.preventDefault(); 2383 + // Tab cycles forward through results in URL mode 2384 + if (urlModeItems.length > 0) { 2385 + state.matchIndex = (state.matchIndex + 1) % urlModeItems.length; 2386 + renderUrlResults(urlModeItems); 2387 + } 2388 + return; 2389 + } 2390 + // Let other keys through to the input handler 2391 + return; 2392 + } 2393 + 2116 2394 if (e.key === 'Escape' && !hasModifier(e)) { 2117 2395 // Primary escape handler — dispatch directly to the state machine. 2118 2396 // The IZUI before-input-event handler may also fire (calling onEscape below), ··· 2240 2518 2241 2519 // Register IZUI escape handler — single escape through dispatch (bug fix #1) 2242 2520 api.escape.onEscape(() => { 2521 + if (urlModeActive) { 2522 + exitUrlMode(); 2523 + } 2243 2524 return machine.handleEscape(); 2244 2525 });