⏎. Web search options are configured under `appConfig.webSearch`.
-#### Setting Search Engine
+### Setting Search Engine
Set your default search engine using the `webSearch.searchEngine` property. This defaults to DuckDuckGo. Search engine must be referenced by their key, the following providers are supported:
- [`duckduckgo`](https://duckduckgo.com), [`google`](https://google.com), [`whoogle`](https://whoogle.sdf.org), [`qwant`](https://www.qwant.com), [`startpage`](https://www.startpage.com), [`searx-bar`](https://searx.bar), [`searx-info`](https://searx.info)
- [`searx-tiekoetter`](https://searx.tiekoetter.com), [`searx-bissisoft`](https://searx.bissisoft.com), [`ecosia`](https://www.ecosia.org), [`metager`](https://metager.org/meta), [`swisscows`](https://swisscows.com), [`mojeek`](https://www.mojeek.com)
- [`wikipedia`](https://en.wikipedia.org), [`wolframalpha`](https://www.wolframalpha.com), [`stackoverflow`](https://stackoverflow.com), [`github`](https://github.com), [`reddit`](https://www.reddit.com), [`youtube`](https://youtube.com), [`bbc`](https://www.bbc.co.uk)
-#### Using Custom Search Engine
+### Using Custom Search Engine
You can also use a custom search engine, that isn't included in the above list (like a self-hosted instance of [Whoogle](https://github.com/benbusby/whoogle-search) or [Searx](https://searx.github.io/searx/)). Set `searchEngine: custom`, and then specify the URL (plus query params) to you're search engine under `customSearchEngine`.
For example:
@@ -67,10 +67,34 @@ appConfig:
customSearchEngine: 'https://searx.local/search?q='
```
-#### Setting Opening Method
+### Setting Opening Method
In a similar way to opening apps, you can specify where you would like search results to be opened. This is done under the `openingMethod` attribute, and can be set to either `newtab`, `sametab` or `workspace`. By default results are opened in a new tab.
-#### Disabling Web Search
+### Using Bangs
+An insanely useful feature of DDG is [Bangs](https://duckduckgo.com/bang), where you type a specific character combination at the start of your search query, and it will be redirected the that website, such as '!w Docker' will display the Docker wikipedia page. Dashy has a similar feature, enabling you to define your own custom bangs to redirect search results to a specific app, website or search engine.
+
+This is done under the `searchBangs` property, with a list of key value pairs. The key is what you will type, and the value is the destination, either as an identifier or a URL with query parameters.
+
+For example:
+
+```yaml
+appConfig:
+ webSearch:
+ searchEngine: 'duckduckgo'
+ openingMethod: 'newtab'
+ searchBangs:
+ /r: reddit
+ /w: wikipedia
+ /s: https://whoogle.local/search?q=
+ /a: https://www.amazon.co.uk/s?k=
+ ':wolf': wolframalpha
+ ':so': stackoverflow
+ ':git': github
+```
+
+Note that bangs begging with `!` or `:` must be surrounded them in quotes
+
+### Disabling Web Search
Web search can be disabled, by setting `disableWebSearch`, for example:
```yaml
diff --git a/package.json b/package.json
index 02f40e3e09..ba073aa1dc 100644
--- a/package.json
+++ b/package.json
@@ -1,6 +1,6 @@
{
"name": "Dashy",
- "version": "1.7.5",
+ "version": "1.7.6",
"license": "MIT",
"main": "server",
"scripts": {
diff --git a/src/components/Settings/SearchBar.vue b/src/components/Settings/SearchBar.vue
index 7979e3b32f..6174f9a7f9 100644
--- a/src/components/Settings/SearchBar.vue
+++ b/src/components/Settings/SearchBar.vue
@@ -9,7 +9,7 @@
:placeholder="$t('search.search-placeholder')"
v-on:input="userIsTypingSomething"
@keydown.esc="clearFilterInput" />
-
+
{{ $t('search.enter-to-search-web') }}
@@ -25,7 +25,13 @@ import router from '@/router';
import ArrowKeyNavigation from '@/utils/ArrowKeyNavigation';
import ErrorHandler from '@/utils/ErrorHandler';
import { getCustomKeyShortcuts } from '@/utils/ConfigHelpers';
-import { searchEngineUrls, defaultSearchEngine, defaultSearchOpeningMethod } from '@/utils/defaults';
+import { getSearchEngineFromBang, findUrlForSearchEngine, stripBangs } from '@/utils/Search';
+import {
+ searchEngineUrls,
+ defaultSearchEngine,
+ defaultSearchOpeningMethod,
+ searchBangs as defaultSearchBangs,
+} from '@/utils/defaults';
export default {
name: 'FilterTile',
@@ -41,37 +47,39 @@ export default {
};
},
computed: {
- webSearchEnabled() {
- const { appConfig } = this.config;
- if (appConfig && appConfig.webSearch) {
- return !appConfig.webSearch.disableWebSearch;
- }
- return true;
+ searchPrefs() {
+ return this.config.appConfig.webSearch || {};
},
},
mounted() {
- window.addEventListener('keydown', (event) => {
+ window.addEventListener('keydown', this.handleKeyPress);
+ },
+ beforeDestroy() {
+ window.removeEventListener('keydown', this.handleKeyPress);
+ },
+ methods: {
+ /* Call correct function dependending on which key is pressed */
+ handleKeyPress(event) {
const currentElem = document.activeElement.id;
const { key, keyCode } = event;
- /* If a modal is open, then do nothing */
+ const notAlreadySearching = currentElem !== 'filter-tiles';
+ // If a modal is open, then do nothing
if (!this.active) return;
- if (/^[a-zA-Z]$/.test(key) && currentElem !== 'filter-tiles') {
- /* Letter key pressed - start searching */
+ if (/^[/:!a-zA-Z]$/.test(key) && notAlreadySearching) {
+ // Letter or bang key pressed - start searching
if (this.$refs.filter) this.$refs.filter.focus();
this.userIsTypingSomething();
} else if (/^[0-9]$/.test(key)) {
- /* Number key pressed, check if user has a custom binding */
+ // Number key pressed, check if user has a custom binding
this.handleHotKey(key);
} else if (keyCode >= 37 && keyCode <= 40) {
- /* Arrow key pressed - start navigation */
+ // Arrow key pressed - start navigation
this.akn.arrowNavigation(keyCode);
} else if (keyCode === 27) {
- /* Esc key pressed - reset form */
+ // Esc key pressed - reset form
this.clearFilterInput();
}
- });
- },
- methods: {
+ },
/* Emmits users's search term up to parent */
userIsTypingSomething() {
this.$emit('user-is-searchin', this.input);
@@ -83,6 +91,7 @@ export default {
document.activeElement.blur(); // Remove focus
this.akn.resetIndex(); // Reset current element index
},
+ /* If configured, launch specific app when hotkey pressed */
handleHotKey(key) {
const usersHotKeys = this.getCustomKeyShortcuts();
usersHotKeys.forEach((hotkey) => {
@@ -91,6 +100,7 @@ export default {
}
});
},
+ /* Launch search results, with users desired opening method */
launchWebSearch(url, method) {
switch (method) {
case 'newtab':
@@ -107,22 +117,24 @@ export default {
window.open(url, '_blank');
}
},
+
+ /* Launch web search, to correct search engine, passing in users query */
searchSubmitted() {
// Get search preferences from appConfig
- const { appConfig } = this.config;
- const searchPrefs = appConfig.webSearch || {};
- if (this.webSearchEnabled) { // Only proceed if user hasn't disabled web search
+ const { searchPrefs } = this;
+ if (!searchPrefs.disableWebSearch) { // Only proceed if user hasn't disabled web search
+ const bangList = { ...defaultSearchBangs, ...(searchPrefs.searchBangs || {}) };
const openingMethod = searchPrefs.openingMethod || defaultSearchOpeningMethod;
- // Get search engine, and make URL
+ const searchBang = getSearchEngineFromBang(this.input, bangList);
const searchEngine = searchPrefs.searchEngine || defaultSearchEngine;
- let searchUrl = searchEngineUrls[searchEngine];
- if (!searchUrl) ErrorHandler(`Search engine not found - ${searchEngine}`);
- if (searchEngine === 'custom' && searchPrefs.customSearchEngine) {
- searchUrl = searchPrefs.customSearchEngine;
+ // Use either search bang, or preffered search engine
+ const desiredSearchEngine = searchBang || searchEngine;
+ let searchUrl = findUrlForSearchEngine(desiredSearchEngine, searchEngineUrls);
+ if (searchUrl) { // Append search query to URL, and launch
+ searchUrl += encodeURIComponent(stripBangs(this.input, bangList));
+ this.launchWebSearch(searchUrl, openingMethod);
+ this.clearFilterInput();
}
- // Append users encoded query onto search URL, and launch
- searchUrl += encodeURIComponent(this.input);
- this.launchWebSearch(searchUrl, openingMethod);
}
},
},
diff --git a/src/utils/ConfigSchema.json b/src/utils/ConfigSchema.json
index 1e88e49cf6..d3bf8ff653 100644
--- a/src/utils/ConfigSchema.json
+++ b/src/utils/ConfigSchema.json
@@ -260,6 +260,17 @@
],
"default": "newtab",
"description": "Set where you would like search results to open to"
+ },
+ "searchBangs": {
+ "type": "object",
+ "additionalProperties": true,
+ "examples": [
+ {
+ "/r": "reddit",
+ "!w": "https://whoogle.local/search?q="
+ }
+ ],
+ "description": "A KV-pair of custom search bangs. The key should be the shortcut to type, and the value is the search engine, specified either by key or full URL"
}
}
},
diff --git a/src/utils/Search.js b/src/utils/Search.js
index e50b88bf2f..f03078bca6 100644
--- a/src/utils/Search.js
+++ b/src/utils/Search.js
@@ -1,6 +1,7 @@
/* Dashy: Licensed under MIT, (C) Alicia Sykes 2021 */
/* Tile filtering utility */
+import ErrorHandler from '@/utils/ErrorHandler';
/**
* Extracts the site name from domain
@@ -35,7 +36,7 @@ const filterHelper = (compareStr, searchStr) => {
* @param {string} searchTerm The users search term
* @returns A filtered array of tiles
*/
-const search = (allTiles, searchTerm) => {
+export const searchTiles = (allTiles, searchTerm) => {
if (!allTiles) return []; // If no data, then skip
return allTiles.filter((tile) => {
const {
@@ -49,4 +50,30 @@ const search = (allTiles, searchTerm) => {
});
};
-export default search;
+/* From a list of search bangs, return the URL associated with it */
+export const getSearchEngineFromBang = (searchQuery, bangList) => {
+ const bangNames = Object.keys(bangList);
+ const foundBang = bangNames.find((bang) => searchQuery.includes(bang));
+ return bangList[foundBang];
+};
+
+/* For a given search engine key, return the corresponding URL, or throw error */
+export const findUrlForSearchEngine = (searchEngine, availableSearchEngines) => {
+ // If missing search engine, report error return false
+ if (!searchEngine) { ErrorHandler('No search engine specified'); return undefined; }
+ // If search engine is already a URL, then return it
+ if ((/(http|https):\/\/[^]*/).test(searchEngine)) return searchEngine;
+ // If search engine was found successfully, return the URL
+ if (availableSearchEngines[searchEngine]) return availableSearchEngines[searchEngine];
+ // Otherwise, there's been an error, log it and return false
+ ErrorHandler(`Specified Search Engine was not Found: '${searchEngine}'`);
+ return undefined;
+};
+
+/* Removes all known bangs from a search query */
+export const stripBangs = (searchQuery, bangList) => {
+ const bangNames = Object.keys(bangList || {});
+ let q = searchQuery;
+ bangNames.forEach((bang) => { q = q.replace(bang, ''); });
+ return q.trim();
+};
diff --git a/src/utils/defaults.js b/src/utils/defaults.js
index 4ed1c35c58..40365210f9 100644
--- a/src/utils/defaults.js
+++ b/src/utils/defaults.js
@@ -185,6 +185,17 @@ module.exports = {
},
defaultSearchEngine: 'duckduckgo',
defaultSearchOpeningMethod: 'newtab',
+ searchBangs: {
+ '/b': 'bbc',
+ '/d': 'duckduckgo',
+ '/g': 'google',
+ '/r': 'reddit',
+ '/w': 'wikipedia',
+ '/y': 'youtube',
+ '/gh': 'github',
+ '/so': 'stackoverflow',
+ '/wa': 'wolframalpha',
+ },
/* Available built-in colors for the theme builder */
swatches: [
['#eb5cad', '#985ceb', '#5346f3', '#5c90eb'],
diff --git a/src/views/Home.vue b/src/views/Home.vue
index a157ddc95c..097a747fac 100644
--- a/src/views/Home.vue
+++ b/src/views/Home.vue
@@ -46,7 +46,7 @@
import SettingsContainer from '@/components/Settings/SettingsContainer.vue';
import Section from '@/components/LinkItems/Section.vue';
-import SearchUtil from '@/utils/Search';
+import { searchTiles } from '@/utils/Search';
import Defaults, { localStorageKeys, iconCdns } from '@/utils/defaults';
export default {
@@ -115,7 +115,7 @@ export default {
},
/* Returns only the tiles that match the users search query */
filterTiles(allTiles, searchTerm) {
- return SearchUtil(allTiles, searchTerm);
+ return searchTiles(allTiles, searchTerm);
},
/* Returns optional section display preferences if available */
getDisplayData(section) {
diff --git a/src/views/Minimal.vue b/src/views/Minimal.vue
index f8b4750659..cf2b9aa91b 100644
--- a/src/views/Minimal.vue
+++ b/src/views/Minimal.vue
@@ -54,7 +54,7 @@ import MinimalSection from '@/components/MinimalView/MinimalSection.vue';
import MinimalHeading from '@/components/MinimalView/MinimalHeading.vue';
import MinimalSearch from '@/components/MinimalView/MinimalSearch.vue';
import { GetTheme, ApplyLocalTheme, ApplyCustomVariables } from '@/utils/ThemeHelper';
-import SearchUtil from '@/utils/Search';
+import { searchTiles } from '@/utils/Search';
import Defaults, { localStorageKeys } from '@/utils/defaults';
import ConfigLauncher from '@/components/Settings/ConfigLauncher';
@@ -123,7 +123,7 @@ export default {
/* Returns only the tiles that match the users search query */
filterTiles(allTiles) {
if (!allTiles) return [];
- return SearchUtil(allTiles, this.searchValue);
+ return searchTiles(allTiles, this.searchValue);
},
/* Update data when modal is open (so that key bindings can be disabled) */
updateModalVisibility(modalState) {