├── .gitignore ├── README.md ├── background.js ├── images ├── icon128.png └── icon48.png ├── manifest.json ├── popup ├── popup.css ├── popup.html └── popup.js ├── settings ├── settings.css ├── settings.html └── settings.js └── shared └── search-engines.js /.gitignore: -------------------------------------------------------------------------------- 1 | .DS_Store -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # SwitchSearch 2 | One click Search Engine switcher 3 | 4 | [![firefox web store](https://extensionworkshop.com/assets/img/documentation/publish/get-the-addon-129x45px.8041c789.png)](https://addons.mozilla.org/firefox/addon/switchsearch) 5 | [![chrome web store](https://developer.chrome.com/static/docs/webstore/branding/image/206x58-chrome-web-bcb82d15b2486.png)](https://chromewebstore.google.com/detail/switchsearch/fmhncjaheleephdddbbjondoglignpfd) 6 | [![SwitchSearch - One click Search Engine switcher for Your browser | Product Hunt](https://api.producthunt.com/widgets/embed-image/v1/featured.svg?post_id=969237&theme=light&t=1751874265240)](https://www.producthunt.com/products/switchsearch?embed=true&utm_source=badge-featured&utm_medium=badge&utm_source=badge-switchsearch) 7 | 8 | 9 | https://github.com/user-attachments/assets/3a6af6f8-c623-4f05-a13c-489dc9cc2227 10 | 11 | # Why use it? 12 | - Tired of one search engine monopoly 13 | - Use best sides of each search engine 14 | - Exit the information bubble 15 | - Explore Deep Web 16 | 17 | # Features 18 | - Built in Google, Yandex, Brave, DuckDuckGo, Perplexity, ChatGPT, Wiby, Marginalia 19 | - Add any other search engine you'd like 20 | - Google and Yandex image search (right click a pic) 21 | -------------------------------------------------------------------------------- /background.js: -------------------------------------------------------------------------------- 1 | import { TextSearchEngines, ImageSearchEngines } from "./shared/search-engines.js"; 2 | 3 | chrome.runtime.onInstalled.addListener(async () => { 4 | 5 | // keep records user has added before update 6 | let session = await chrome.storage.local.get() 7 | let prevSavedTextSearchEngines = session["TextSearchEngines"] 8 | 9 | // TO DO: remove isArray check after few versions 10 | let prevEnabled = {} 11 | if (Array.isArray(prevSavedTextSearchEngines)) { 12 | for (let i in prevSavedTextSearchEngines) { 13 | let prevSaved = prevSavedTextSearchEngines[i] 14 | prevEnabled[prevSaved.name] = prevSaved.enabled 15 | } 16 | for (let i in TextSearchEngines) { 17 | let searchName = TextSearchEngines[i].name 18 | if (Object.hasOwn(prevEnabled, searchName)) { 19 | TextSearchEngines[i].enabled = prevEnabled[searchName] 20 | } 21 | } 22 | } 23 | 24 | chrome.storage.local.set({TextSearchEngines: TextSearchEngines}); 25 | chrome.storage.local.set({ImageSearchEngines: ImageSearchEngines}); 26 | 27 | // create context menu for image search 28 | for (let i in ImageSearchEngines) { 29 | let name = ImageSearchEngines[i].name 30 | 31 | chrome.contextMenus.create({ 32 | id: `${name}.ImageSearch`, 33 | title: `Search image with ${name}`, 34 | contexts: ["image"] 35 | }); 36 | } 37 | }); 38 | 39 | chrome.contextMenus.onClicked.addListener(async (info, tab) => { 40 | 41 | let { ImageSearchEngines } = await chrome.storage.local.get("ImageSearchEngines") 42 | if (!ImageSearchEngines) { 43 | return 44 | } 45 | 46 | let [searchName, searchType] = info.menuItemId.split(".") 47 | 48 | if (searchType === "ImageSearch") { 49 | let search = ImageSearchEngines.find(e => e.name == searchName) 50 | if (search) { 51 | let url = new URL(search.url) 52 | url.searchParams.set(search.qparam, info.srcUrl) 53 | chrome.tabs.create({ url: url.href }); 54 | } 55 | } 56 | 57 | }); 58 | -------------------------------------------------------------------------------- /images/icon128.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/thedmdim/switchsearch/eae096f204b1fdb3727bc3b686b9ab675ab8f423/images/icon128.png -------------------------------------------------------------------------------- /images/icon48.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/thedmdim/switchsearch/eae096f204b1fdb3727bc3b686b9ab675ab8f423/images/icon48.png -------------------------------------------------------------------------------- /manifest.json: -------------------------------------------------------------------------------- 1 | { 2 | "manifest_version": 3, 3 | "name": "SwitchSearch", 4 | "version": "1.6", 5 | "description": "Switch Search Engines Without Losing Your Query", 6 | "permissions": ["background", "storage", "tabs", "contextMenus", "activeTab"], 7 | "action": { 8 | "default_popup": "popup/popup.html", 9 | "default_icon": "images/icon48.png" 10 | }, 11 | "background": { 12 | "type": "module", 13 | "service_worker": "background.js" 14 | }, 15 | "options_page": "settings/settings.html", 16 | "icons": { 17 | "48": "images/icon48.png", 18 | "128": "images/icon128.png" 19 | }, 20 | "browser_specific_settings": { 21 | "gecko": { 22 | "id": "switchsearch@thedmdim.github" 23 | } 24 | } 25 | } 26 | -------------------------------------------------------------------------------- /popup/popup.css: -------------------------------------------------------------------------------- 1 | :root[theme="light"] { 2 | --bg: #f5faff; 3 | --text: black; 4 | --legeng-bg: #709AD3; 5 | --fieldset-bg: white; 6 | } 7 | 8 | :root[theme="dark"] { 9 | --bg: #2d2d2d; 10 | --text: white; 11 | --legeng-bg: #00000000; 12 | --fieldset-bg: #3f3f3f; 13 | } 14 | 15 | body { 16 | font-family: monospace; 17 | font-size: 1.1em; 18 | margin: 0; 19 | padding: 10px; 20 | width: 200px; 21 | background-color: var(--bg); 22 | color: var(--text); 23 | } 24 | 25 | #popup-container { 26 | display: flex; 27 | flex-direction: column; 28 | align-items: flex-start; 29 | } 30 | 31 | legend { 32 | background-color: var(--legeng-bg); 33 | color: #FFF203; 34 | font-weight: bold; 35 | } 36 | 37 | label { 38 | margin: 5px 0; 39 | cursor: pointer; 40 | } 41 | 42 | input[type="radio"] { 43 | margin-right: 5px; 44 | } 45 | 46 | #settings { 47 | color: var(--text); 48 | margin: 0.3em 0 0 0.6em; 49 | display: block; 50 | /* text-align: center; */ 51 | } 52 | 53 | #settings:link { 54 | text-decoration: none; 55 | } 56 | 57 | #settings:hover { 58 | text-decoration: underline; 59 | } -------------------------------------------------------------------------------- /popup/popup.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | Search Switcher 8 | 9 | 10 |
11 | SwitchSearch 12 |
13 | settings 14 | 15 | 16 | 17 | -------------------------------------------------------------------------------- /popup/popup.js: -------------------------------------------------------------------------------- 1 | document.addEventListener("DOMContentLoaded", async () => { 2 | 3 | // theme 4 | let { theme } = await chrome.storage.local.get("theme"); 5 | if (!theme || theme === "auto") { 6 | theme = window.matchMedia("(prefers-color-scheme: dark)").matches ? "dark" : "light"; 7 | } 8 | document.documentElement.setAttribute("theme", theme); 9 | 10 | // search engines list 11 | let { TextSearchEngines } = await chrome.storage.local.get("TextSearchEngines") 12 | 13 | let [fieldset] = document.getElementsByTagName("fieldset"); 14 | 15 | const [tab] = await chrome.tabs.query({ active: true, currentWindow: true }); 16 | const tabUrl = new URL(tab.url); 17 | 18 | let currSearchEngine 19 | for (let i in TextSearchEngines) { 20 | let search = TextSearchEngines[i] 21 | let url = new URL(search.url) 22 | if (url.hostname == tabUrl.hostname) { 23 | currSearchEngine = search 24 | break 25 | } 26 | } 27 | 28 | for (let i in TextSearchEngines) { 29 | let search = TextSearchEngines[i] 30 | 31 | if (!search.enabled) { 32 | continue 33 | } 34 | 35 | const label = document.createElement("label"); 36 | label.style.display = "block"; 37 | 38 | const input = document.createElement("input"); 39 | 40 | input.type = "radio" 41 | input.name = "search-engine" 42 | input.value = search.name 43 | 44 | if (currSearchEngine && currSearchEngine.name == input.value) { 45 | input.setAttribute('checked', 'checked'); 46 | } 47 | 48 | label.appendChild(input) 49 | label.innerHTML += search.name 50 | fieldset.appendChild(label); 51 | } 52 | 53 | fieldset.addEventListener("change", async (event) => { 54 | if (event.target.name === "search-engine") { 55 | let nextSearchEngine = TextSearchEngines.find(e => e.name == event.target.value) 56 | let nextURL = new URL(nextSearchEngine.url) 57 | 58 | if (currSearchEngine) { 59 | 60 | let { lastq } = await chrome.storage.local.get("lastq") 61 | let currq = tabUrl.searchParams.get(currSearchEngine.qparam) 62 | 63 | if (tabUrl.pathname == "/" && !currq && !currSearchEngine.useLastq) { 64 | chrome.tabs.update(tab.id, { url: nextURL.origin }); 65 | return 66 | } 67 | 68 | let q = currq || lastq 69 | nextURL.searchParams.set(nextSearchEngine.qparam, q) 70 | chrome.tabs.update(tab.id, { url: nextURL.href }); 71 | chrome.storage.local.set({ lastq: q }) 72 | return 73 | 74 | } else { 75 | chrome.tabs.create( { url: nextURL.origin } ) 76 | } 77 | } 78 | }); 79 | }); -------------------------------------------------------------------------------- /settings/settings.css: -------------------------------------------------------------------------------- 1 | html { 2 | height: 100%; 3 | background-size: 450px; 4 | image-rendering: pixelated; 5 | } 6 | 7 | :root[theme="light"] { 8 | --bg: white; 9 | --text: black; 10 | } 11 | 12 | :root[theme="dark"] { 13 | --bg: #1e1e1e; 14 | --text: white; 15 | } 16 | 17 | body { 18 | display: grid; 19 | grid-template-columns: 1fr; 20 | grid-gap: 20px; 21 | max-width: 900px; 22 | margin: auto; 23 | background-color: var(--bg); 24 | color: var(--text); 25 | font-family: monospace; 26 | font-size: 1.4em; 27 | padding: 0.5em 1em; 28 | border-radius: 0 0 7px 7px; 29 | } 30 | 31 | table, th, td { 32 | border: 1px solid var(--text); 33 | border-left: none; 34 | border-right: none; 35 | border-collapse: collapse; 36 | } 37 | 38 | td { 39 | padding: 10px; 40 | } 41 | 42 | 43 | -------------------------------------------------------------------------------- /settings/settings.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | Search Engine Settings 7 | 8 | 9 | 10 |

SwitchSearch Settings

11 |
12 | 13 | 14 | 15 |
16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 |
NameEnabledURLQuery
31 | 32 | 33 | -------------------------------------------------------------------------------- /settings/settings.js: -------------------------------------------------------------------------------- 1 | function createTableRow(searchData) { 2 | let row = document.createElement("tr") 3 | 4 | let col0 = document.createElement("td"); 5 | col0.innerHTML = searchData.name 6 | row.appendChild(col0) 7 | 8 | let col1 = document.createElement("td"); 9 | let input = document.createElement("input") 10 | input.name = "enabled" 11 | input.value = searchData.name 12 | input.type = "checkbox" 13 | if (searchData.enabled) { 14 | input.setAttribute('checked', 'checked'); 15 | } 16 | col1.appendChild(input) 17 | row.appendChild(col1) 18 | 19 | let col2 = document.createElement("td"); 20 | col2.innerHTML = searchData.url 21 | row.appendChild(col2) 22 | 23 | let col3 = document.createElement("td"); 24 | col3.innerHTML = searchData.qparam 25 | row.appendChild(col3) 26 | 27 | 28 | let col4 = document.createElement("td"); 29 | if (searchData.builtIn) { 30 | col4.innerHTML = "built in" 31 | } else { 32 | input = document.createElement("button") 33 | input.innerHTML = "remove" 34 | input.name = "remove" 35 | input.type = "button" 36 | input.value = searchData.name 37 | col4.appendChild(input) 38 | } 39 | row.appendChild(col4) 40 | return row 41 | } 42 | 43 | document.addEventListener("DOMContentLoaded", async () => { 44 | 45 | // theme settings 46 | 47 | let applyTheme = theme => { 48 | if (theme == "auto") { 49 | theme = window.matchMedia("(prefers-color-scheme: dark)").matches ? "dark" : "light"; 50 | document.documentElement.setAttribute("theme", theme); 51 | return 52 | } 53 | document.documentElement.setAttribute("theme", theme); 54 | } 55 | 56 | let { theme } = await chrome.storage.local.get("theme"); 57 | if (!theme) { 58 | theme = "auto" 59 | } 60 | applyTheme(theme) 61 | 62 | document.querySelectorAll("input[name='theme']").forEach(input => { 63 | if (input.value === theme) { 64 | input.checked = true 65 | } 66 | input.addEventListener("change", () => { 67 | chrome.storage.local.set({ theme: input.value }); 68 | applyTheme(input.value) 69 | }); 70 | }); 71 | 72 | // search engines list 73 | 74 | let appendForm = document.getElementById("append-form") 75 | let table = appendForm.parentNode 76 | let { TextSearchEngines } = await chrome.storage.local.get("TextSearchEngines"); 77 | for (let i in TextSearchEngines) { 78 | let row = createTableRow(TextSearchEngines[i]) 79 | table.insertBefore(row, appendForm) 80 | } 81 | 82 | document.getElementById("append").onclick = async () => { 83 | let name = document.getElementById("name") 84 | let url = document.getElementById("url") 85 | let qparam = document.getElementById("qparam") 86 | 87 | if ( !name.value || !url.value || !qparam.value ) { 88 | alert("fill all the forms") 89 | return 90 | } 91 | 92 | let searchData = { 93 | name: name.value, 94 | url: url.value, 95 | qparam: qparam.value, 96 | builtIn: false, 97 | enabled: true 98 | } 99 | let { TextSearchEngines } = await chrome.storage.local.get("TextSearchEngines"); 100 | TextSearchEngines.push(searchData) 101 | chrome.storage.local.set({TextSearchEngines: TextSearchEngines}); 102 | 103 | let row = createTableRow(searchData) 104 | table.insertBefore(row, appendForm) 105 | } 106 | 107 | table.addEventListener("click", async event => { 108 | if (event.target.name == "enabled") { 109 | let i = TextSearchEngines.findIndex(e => e.name == event.target.value) 110 | TextSearchEngines[i].enabled = event.target.checked 111 | chrome.storage.local.set({TextSearchEngines: TextSearchEngines}); 112 | return 113 | }; 114 | 115 | if (event.target.name == "remove") { 116 | let { TextSearchEngines } = await chrome.storage.local.get("TextSearchEngines"); 117 | chrome.storage.local.set({TextSearchEngines: TextSearchEngines.filter(e => e.name !== event.target.value)}); 118 | chrome.tabs.reload() 119 | } 120 | }); 121 | }); 122 | -------------------------------------------------------------------------------- /shared/search-engines.js: -------------------------------------------------------------------------------- 1 | // initialize text search engines 2 | const TextSearchEngines = [ 3 | { 4 | name: "Google", 5 | url: "https://www.google.com/search", 6 | qparam: "q", 7 | builtIn: true, 8 | enabled: true, 9 | useLastq: false 10 | }, 11 | { 12 | name: "Yandex", 13 | url: "https://ya.ru/search/", 14 | qparam: "text", 15 | builtIn: true, 16 | enabled: true, 17 | useLastq: false 18 | }, 19 | { 20 | name: "Brave", 21 | url: "https://search.brave.com/search", 22 | qparam: "q", 23 | builtIn: true, 24 | enabled: true, 25 | useLastq: false 26 | }, 27 | { 28 | name: "DuckDuckGo", 29 | url: "https://duckduckgo.com/", 30 | qparam: "q", 31 | builtIn: true, 32 | enabled: false, 33 | useLastq: false 34 | }, 35 | { 36 | name: "Perplexity", 37 | url: "https://www.perplexity.ai/search/new", 38 | qparam: "q", 39 | builtIn: true, 40 | enabled: true, 41 | useLastq: true 42 | }, 43 | { 44 | name: "ChatGPT", 45 | url: "https://chatgpt.com/?hints=search", 46 | qparam: "q", 47 | builtIn: true, 48 | enabled: false, 49 | useLastq: true 50 | }, 51 | { 52 | name: "Wiby", 53 | url: "https://wiby.me/", 54 | qparam: "q", 55 | builtIn: true, 56 | enabled: false, 57 | useLastq: false 58 | }, 59 | { 60 | name: "Marginalia", 61 | url: "https://marginalia-search.com/search", 62 | qparam: "query", 63 | builtIn: true, 64 | enabled: false, 65 | useLastq: false 66 | } 67 | ]; 68 | 69 | // initialize image search engines 70 | const ImageSearchEngines = [ 71 | { 72 | name: "Google", 73 | url: "https://lens.google.com/uploadbyurl", 74 | qparam: "url" 75 | }, 76 | { 77 | name: "Yandex", 78 | url: "https://ya.ru/images/search?rpt=imageview", 79 | qparam: "url" 80 | } 81 | ]; 82 | 83 | 84 | export { TextSearchEngines, ImageSearchEngines } 85 | --------------------------------------------------------------------------------