A fast, local-first "redirection engine" for !bang users with a few extra features ^-^
5
fork

Configure Feed

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

feat: add bang search

+122 -2
+56
src/global.css
··· 455 455 word-break: break-all; 456 456 color: var(--text-color-secondary); 457 457 } 458 + 459 + .bang-search { 460 + width: 100%; 461 + padding: 8px 12px; 462 + border: 1px solid var(--border-color); 463 + border-radius: 4px; 464 + background-color: var(--bg-color-secondary); 465 + color: var(--text-color); 466 + font-size: 14px; 467 + } 468 + 469 + .bang-search-results { 470 + max-height: 200px; 471 + overflow-y: auto; 472 + margin-top: 8px; 473 + border-radius: 4px; 474 + } 475 + 476 + .bang-search-item { 477 + display: flex; 478 + align-items: center; 479 + gap: 12px; 480 + padding: 8px 12px; 481 + border-bottom: 1px solid var(--border-color); 482 + background-color: var(--bg-color-secondary); 483 + } 484 + 485 + .bang-search-item:last-child { 486 + border-bottom: none; 487 + } 488 + 489 + .bang-search-item code { 490 + padding: 2px 6px; 491 + border-radius: 4px; 492 + background-color: var(--bg-color-active); 493 + font-size: 12px; 494 + min-width: 60px; 495 + } 496 + 497 + .bang-search-name { 498 + flex: 1; 499 + font-weight: 500; 500 + } 501 + 502 + .bang-search-domain { 503 + color: var(--text-color-secondary); 504 + font-size: 12px; 505 + } 506 + 507 + .bang-search-empty { 508 + padding: 12px; 509 + text-align: center; 510 + color: var(--text-color-secondary); 511 + background-color: var(--bg-color-secondary); 512 + border-radius: 4px; 513 + }
+66 -2
src/main.ts
··· 244 244 bangBaseUrl: app.querySelector<HTMLInputElement>(".bang-base-url"), 245 245 addBang: app.querySelector<HTMLButtonElement>(".add-bang"), 246 246 removeBangs: app.querySelectorAll<HTMLButtonElement>(".remove-bang"), 247 + bangSearch: app.querySelector<HTMLInputElement>("#bang-search"), 248 + bangSearchResults: app.querySelector<HTMLDivElement>("#bang-search-results"), 247 249 } as const; 248 250 249 251 // Validate all elements exist ··· 503 505 else window.location.reload(); 504 506 }); 505 507 }); 508 + 509 + validatedElements.bangSearch.addEventListener("input", (event) => { 510 + const query = (event.target as HTMLInputElement).value.trim().toLowerCase(); 511 + const resultsContainer = validatedElements.bangSearchResults; 512 + 513 + if (!query) { 514 + resultsContainer.innerHTML = ""; 515 + return; 516 + } 517 + 518 + const allBangs = { ...bangs, ...customBangs }; 519 + const results = Object.entries(allBangs) 520 + .filter(([shortcut, bang]) => { 521 + const searchableText = `${shortcut} ${bang.s} ${bang.d} ${bang.t || ""}`.toLowerCase(); 522 + return searchableText.includes(query); 523 + }) 524 + .sort((a, b) => { 525 + const [shortcutA, bangA] = a; 526 + const [shortcutB, bangB] = b; 527 + const aStartsWithQuery = shortcutA.toLowerCase().startsWith(query) || bangA.s.toLowerCase().startsWith(query); 528 + const bStartsWithQuery = shortcutB.toLowerCase().startsWith(query) || bangB.s.toLowerCase().startsWith(query); 529 + if (aStartsWithQuery && !bStartsWithQuery) return -1; 530 + if (!aStartsWithQuery && bStartsWithQuery) return 1; 531 + return shortcutA.length - shortcutB.length; 532 + }) 533 + .slice(0, 20); 534 + 535 + if (results.length === 0) { 536 + resultsContainer.innerHTML = '<div class="bang-search-empty">No bangs found</div>'; 537 + return; 538 + } 539 + 540 + resultsContainer.innerHTML = results 541 + .map( 542 + ([shortcut, bang]) => { 543 + const displayName = bang.s.replace(/\s*\(Kagi Search\)\s*$/i, " (default search)") || bang.s; 544 + return ` 545 + <div class="bang-search-item"> 546 + <code>!${shortcut}</code> 547 + <span class="bang-search-name">${displayName}</span> 548 + <span class="bang-search-domain">${bang.d}</span> 549 + </div> 550 + `; 551 + }, 552 + ) 553 + .join(""); 554 + }); 506 555 } 507 556 508 557 const LS_DEFAULT_BANG = ··· 540 589 541 590 // Redirect to base domain if cleanQuery is empty 542 591 if (!cleanQuery && selectedBang?.d) { 543 - return ensureProtocol(selectedBang.d); 592 + return ensureProtocol(selectedBang.ad || selectedBang.d); 593 + } 594 + 595 + // Check if this is a "(Kagi Search)" bang that should use the default search provider 596 + let bangUrl = selectedBang?.u || ""; 597 + if (selectedBang?.s?.includes("(Kagi Search)") && selectedBang?.u?.match(/^\/search\?q=\{\{\{s\}\}\}\+site:/)) { 598 + const siteMatch = selectedBang.u.match(/\+site:([^\s&]+)/); 599 + if (siteMatch && defaultBang?.u) { 600 + const siteDomain = siteMatch[1]; 601 + const queryWithSite = `${cleanQuery} site:${siteDomain}`; 602 + const redirectUrl = defaultBang.u.replace( 603 + "{{{s}}}", 604 + encodeURIComponent(queryWithSite).replace(/%2F/g, "/"), 605 + ); 606 + return ensureProtocol(redirectUrl); 607 + } 544 608 } 545 609 546 - const redirectUrl = selectedBang?.u.replace( 610 + const redirectUrl = bangUrl.replace( 547 611 "{{{s}}}", 548 612 encodeURIComponent(cleanQuery).replace(/%2F/g, "/"), 549 613 );