├── .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 | [](https://addons.mozilla.org/firefox/addon/switchsearch)
5 | [](https://chromewebstore.google.com/detail/switchsearch/fmhncjaheleephdddbbjondoglignpfd)
6 | [](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 |
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 |
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 |
--------------------------------------------------------------------------------