Skip to content

Commit fd3c043

Browse files
authored
🔀 Merge pull request gchq#224 from Lissy93/FEATURE/multi-search
[FEATURE] Multi-Search with Custom Bangs Fixes gchq#206
2 parents 3e6a1fa + ee56183 commit fd3c043

File tree

10 files changed

+129
-40
lines changed

10 files changed

+129
-40
lines changed

.github/CHANGELOG.md

+3
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,8 @@
11
# Changelog
22

3+
## ✨ 1.7.6 - Adds Multi-Search Support with Bangs [PR #224](https://github.com/Lissy93/dashy/pull/224)
4+
- Adds option for user to add custom search bangs, in order to specify search engine/ target app. Re: #206
5+
36
## 🎨 1.7.5 - Improved Language Detection & UI [PR #223](https://github.com/Lissy93/dashy/pull/223)
47
- Makes the auto language detection algo smarter
58
- Improves responsiveness for the language selector form

docs/configuring.md

+1
Original file line numberDiff line numberDiff line change
@@ -141,6 +141,7 @@ For more info, see the **[Authentication Docs](/docs/authentication.md)**
141141
**`searchEngine`** | `string` | _Optional_ | Set the key name for your search engine. Can also use a custom engine by setting this property to `custom`. Currently supported: `duckduckgo`, `google`, `whoogle`, `qwant`, `startpage`, `searx-bar` and `searx-info`. Defaults to `duckduckgo`
142142
**`customSearchEngine`** | `string` | _Optional_ | You can also use a custom search engine, or your own self-hosted instance. This requires `searchEngine: custom` to be set. Then add the URL of your service, with GET query string included here
143143
**`openingMethod`** | `string` | _Optional_ | Set your preferred opening method for search results: `newtab`, `sametab`, `workspace`. Defaults to `newtab`
144+
**`searchBangs`** | `object` | _Optional_ | A key-value-pair set of custom search _bangs_ for redirecting query to a specific app or search engine. The key of each should be the bang you will type (typically starting with `/`, `!` or `:`), and value is the destination, either as a search engine key (e.g. `reddit`) or a URL with search parameters (e.g. `https://en.wikipedia.org/w/?search=`)
144145

145146

146147
**[⬆️ Back to Top](#configuring)**

docs/searching.md

+28-4
Original file line numberDiff line numberDiff line change
@@ -50,13 +50,13 @@ In the above example, pressing <kbd>2</kbd> will launch Bookstack. Or hitting <k
5050
## Web Search
5151
It's possible to search the web directly from Dashy, which might be useful if you're using Dashy as your start page. This can be done by typing your query as normal, and then pressing <kbd>⏎</kbd>. Web search options are configured under `appConfig.webSearch`.
5252

53-
#### Setting Search Engine
53+
### Setting Search Engine
5454
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:
5555
- [`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)
5656
- [`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)
5757
- [`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)
5858

59-
#### Using Custom Search Engine
59+
### Using Custom Search Engine
6060
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`.
6161

6262
For example:
@@ -67,10 +67,34 @@ appConfig:
6767
customSearchEngine: 'https://searx.local/search?q='
6868
```
6969

70-
#### Setting Opening Method
70+
### Setting Opening Method
7171
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.
7272

73-
#### Disabling Web Search
73+
### Using Bangs
74+
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.
75+
76+
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.
77+
78+
For example:
79+
80+
```yaml
81+
appConfig:
82+
webSearch:
83+
searchEngine: 'duckduckgo'
84+
openingMethod: 'newtab'
85+
searchBangs:
86+
/r: reddit
87+
/w: wikipedia
88+
/s: https://whoogle.local/search?q=
89+
/a: https://www.amazon.co.uk/s?k=
90+
':wolf': wolframalpha
91+
':so': stackoverflow
92+
':git': github
93+
```
94+
95+
Note that bangs begging with `!` or `:` must be surrounded them in quotes
96+
97+
### Disabling Web Search
7498
Web search can be disabled, by setting `disableWebSearch`, for example:
7599

76100
```yaml

package.json

+1-1
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
{
22
"name": "Dashy",
3-
"version": "1.7.5",
3+
"version": "1.7.6",
44
"license": "MIT",
55
"main": "server",
66
"scripts": {

src/components/Settings/SearchBar.vue

+41-29
Original file line numberDiff line numberDiff line change
@@ -9,7 +9,7 @@
99
:placeholder="$t('search.search-placeholder')"
1010
v-on:input="userIsTypingSomething"
1111
@keydown.esc="clearFilterInput" />
12-
<p v-if="webSearchEnabled && input.length > 0" class="web-search-note">
12+
<p v-if="(!searchPrefs.disableWebSearch) && input.length > 0" class="web-search-note">
1313
{{ $t('search.enter-to-search-web') }}
1414
</p>
1515
</div>
@@ -25,7 +25,13 @@ import router from '@/router';
2525
import ArrowKeyNavigation from '@/utils/ArrowKeyNavigation';
2626
import ErrorHandler from '@/utils/ErrorHandler';
2727
import { getCustomKeyShortcuts } from '@/utils/ConfigHelpers';
28-
import { searchEngineUrls, defaultSearchEngine, defaultSearchOpeningMethod } from '@/utils/defaults';
28+
import { getSearchEngineFromBang, findUrlForSearchEngine, stripBangs } from '@/utils/Search';
29+
import {
30+
searchEngineUrls,
31+
defaultSearchEngine,
32+
defaultSearchOpeningMethod,
33+
searchBangs as defaultSearchBangs,
34+
} from '@/utils/defaults';
2935
3036
export default {
3137
name: 'FilterTile',
@@ -41,37 +47,39 @@ export default {
4147
};
4248
},
4349
computed: {
44-
webSearchEnabled() {
45-
const { appConfig } = this.config;
46-
if (appConfig && appConfig.webSearch) {
47-
return !appConfig.webSearch.disableWebSearch;
48-
}
49-
return true;
50+
searchPrefs() {
51+
return this.config.appConfig.webSearch || {};
5052
},
5153
},
5254
mounted() {
53-
window.addEventListener('keydown', (event) => {
55+
window.addEventListener('keydown', this.handleKeyPress);
56+
},
57+
beforeDestroy() {
58+
window.removeEventListener('keydown', this.handleKeyPress);
59+
},
60+
methods: {
61+
/* Call correct function dependending on which key is pressed */
62+
handleKeyPress(event) {
5463
const currentElem = document.activeElement.id;
5564
const { key, keyCode } = event;
56-
/* If a modal is open, then do nothing */
65+
const notAlreadySearching = currentElem !== 'filter-tiles';
66+
// If a modal is open, then do nothing
5767
if (!this.active) return;
58-
if (/^[a-zA-Z]$/.test(key) && currentElem !== 'filter-tiles') {
59-
/* Letter key pressed - start searching */
68+
if (/^[/:!a-zA-Z]$/.test(key) && notAlreadySearching) {
69+
// Letter or bang key pressed - start searching
6070
if (this.$refs.filter) this.$refs.filter.focus();
6171
this.userIsTypingSomething();
6272
} else if (/^[0-9]$/.test(key)) {
63-
/* Number key pressed, check if user has a custom binding */
73+
// Number key pressed, check if user has a custom binding
6474
this.handleHotKey(key);
6575
} else if (keyCode >= 37 && keyCode <= 40) {
66-
/* Arrow key pressed - start navigation */
76+
// Arrow key pressed - start navigation
6777
this.akn.arrowNavigation(keyCode);
6878
} else if (keyCode === 27) {
69-
/* Esc key pressed - reset form */
79+
// Esc key pressed - reset form
7080
this.clearFilterInput();
7181
}
72-
});
73-
},
74-
methods: {
82+
},
7583
/* Emmits users's search term up to parent */
7684
userIsTypingSomething() {
7785
this.$emit('user-is-searchin', this.input);
@@ -83,6 +91,7 @@ export default {
8391
document.activeElement.blur(); // Remove focus
8492
this.akn.resetIndex(); // Reset current element index
8593
},
94+
/* If configured, launch specific app when hotkey pressed */
8695
handleHotKey(key) {
8796
const usersHotKeys = this.getCustomKeyShortcuts();
8897
usersHotKeys.forEach((hotkey) => {
@@ -91,6 +100,7 @@ export default {
91100
}
92101
});
93102
},
103+
/* Launch search results, with users desired opening method */
94104
launchWebSearch(url, method) {
95105
switch (method) {
96106
case 'newtab':
@@ -107,22 +117,24 @@ export default {
107117
window.open(url, '_blank');
108118
}
109119
},
120+
121+
/* Launch web search, to correct search engine, passing in users query */
110122
searchSubmitted() {
111123
// Get search preferences from appConfig
112-
const { appConfig } = this.config;
113-
const searchPrefs = appConfig.webSearch || {};
114-
if (this.webSearchEnabled) { // Only proceed if user hasn't disabled web search
124+
const { searchPrefs } = this;
125+
if (!searchPrefs.disableWebSearch) { // Only proceed if user hasn't disabled web search
126+
const bangList = { ...defaultSearchBangs, ...(searchPrefs.searchBangs || {}) };
115127
const openingMethod = searchPrefs.openingMethod || defaultSearchOpeningMethod;
116-
// Get search engine, and make URL
128+
const searchBang = getSearchEngineFromBang(this.input, bangList);
117129
const searchEngine = searchPrefs.searchEngine || defaultSearchEngine;
118-
let searchUrl = searchEngineUrls[searchEngine];
119-
if (!searchUrl) ErrorHandler(`Search engine not found - ${searchEngine}`);
120-
if (searchEngine === 'custom' && searchPrefs.customSearchEngine) {
121-
searchUrl = searchPrefs.customSearchEngine;
130+
// Use either search bang, or preffered search engine
131+
const desiredSearchEngine = searchBang || searchEngine;
132+
let searchUrl = findUrlForSearchEngine(desiredSearchEngine, searchEngineUrls);
133+
if (searchUrl) { // Append search query to URL, and launch
134+
searchUrl += encodeURIComponent(stripBangs(this.input, bangList));
135+
this.launchWebSearch(searchUrl, openingMethod);
136+
this.clearFilterInput();
122137
}
123-
// Append users encoded query onto search URL, and launch
124-
searchUrl += encodeURIComponent(this.input);
125-
this.launchWebSearch(searchUrl, openingMethod);
126138
}
127139
},
128140
},

src/utils/ConfigSchema.json

+11
Original file line numberDiff line numberDiff line change
@@ -260,6 +260,17 @@
260260
],
261261
"default": "newtab",
262262
"description": "Set where you would like search results to open to"
263+
},
264+
"searchBangs": {
265+
"type": "object",
266+
"additionalProperties": true,
267+
"examples": [
268+
{
269+
"/r": "reddit",
270+
"!w": "https://whoogle.local/search?q="
271+
}
272+
],
273+
"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"
263274
}
264275
}
265276
},

src/utils/Search.js

+29-2
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
/* Dashy: Licensed under MIT, (C) Alicia Sykes 2021 <https://aliciasykes.com> */
22

33
/* Tile filtering utility */
4+
import ErrorHandler from '@/utils/ErrorHandler';
45

56
/**
67
* Extracts the site name from domain
@@ -35,7 +36,7 @@ const filterHelper = (compareStr, searchStr) => {
3536
* @param {string} searchTerm The users search term
3637
* @returns A filtered array of tiles
3738
*/
38-
const search = (allTiles, searchTerm) => {
39+
export const searchTiles = (allTiles, searchTerm) => {
3940
if (!allTiles) return []; // If no data, then skip
4041
return allTiles.filter((tile) => {
4142
const {
@@ -49,4 +50,30 @@ const search = (allTiles, searchTerm) => {
4950
});
5051
};
5152

52-
export default search;
53+
/* From a list of search bangs, return the URL associated with it */
54+
export const getSearchEngineFromBang = (searchQuery, bangList) => {
55+
const bangNames = Object.keys(bangList);
56+
const foundBang = bangNames.find((bang) => searchQuery.includes(bang));
57+
return bangList[foundBang];
58+
};
59+
60+
/* For a given search engine key, return the corresponding URL, or throw error */
61+
export const findUrlForSearchEngine = (searchEngine, availableSearchEngines) => {
62+
// If missing search engine, report error return false
63+
if (!searchEngine) { ErrorHandler('No search engine specified'); return undefined; }
64+
// If search engine is already a URL, then return it
65+
if ((/(http|https):\/\/[^]*/).test(searchEngine)) return searchEngine;
66+
// If search engine was found successfully, return the URL
67+
if (availableSearchEngines[searchEngine]) return availableSearchEngines[searchEngine];
68+
// Otherwise, there's been an error, log it and return false
69+
ErrorHandler(`Specified Search Engine was not Found: '${searchEngine}'`);
70+
return undefined;
71+
};
72+
73+
/* Removes all known bangs from a search query */
74+
export const stripBangs = (searchQuery, bangList) => {
75+
const bangNames = Object.keys(bangList || {});
76+
let q = searchQuery;
77+
bangNames.forEach((bang) => { q = q.replace(bang, ''); });
78+
return q.trim();
79+
};

src/utils/defaults.js

+11
Original file line numberDiff line numberDiff line change
@@ -185,6 +185,17 @@ module.exports = {
185185
},
186186
defaultSearchEngine: 'duckduckgo',
187187
defaultSearchOpeningMethod: 'newtab',
188+
searchBangs: {
189+
'/b': 'bbc',
190+
'/d': 'duckduckgo',
191+
'/g': 'google',
192+
'/r': 'reddit',
193+
'/w': 'wikipedia',
194+
'/y': 'youtube',
195+
'/gh': 'github',
196+
'/so': 'stackoverflow',
197+
'/wa': 'wolframalpha',
198+
},
188199
/* Available built-in colors for the theme builder */
189200
swatches: [
190201
['#eb5cad', '#985ceb', '#5346f3', '#5c90eb'],

src/views/Home.vue

+2-2
Original file line numberDiff line numberDiff line change
@@ -46,7 +46,7 @@
4646

4747
import SettingsContainer from '@/components/Settings/SettingsContainer.vue';
4848
import Section from '@/components/LinkItems/Section.vue';
49-
import SearchUtil from '@/utils/Search';
49+
import { searchTiles } from '@/utils/Search';
5050
import Defaults, { localStorageKeys, iconCdns } from '@/utils/defaults';
5151

5252
export default {
@@ -115,7 +115,7 @@ export default {
115115
},
116116
/* Returns only the tiles that match the users search query */
117117
filterTiles(allTiles, searchTerm) {
118-
return SearchUtil(allTiles, searchTerm);
118+
return searchTiles(allTiles, searchTerm);
119119
},
120120
/* Returns optional section display preferences if available */
121121
getDisplayData(section) {

src/views/Minimal.vue

+2-2
Original file line numberDiff line numberDiff line change
@@ -54,7 +54,7 @@ import MinimalSection from '@/components/MinimalView/MinimalSection.vue';
5454
import MinimalHeading from '@/components/MinimalView/MinimalHeading.vue';
5555
import MinimalSearch from '@/components/MinimalView/MinimalSearch.vue';
5656
import { GetTheme, ApplyLocalTheme, ApplyCustomVariables } from '@/utils/ThemeHelper';
57-
import SearchUtil from '@/utils/Search';
57+
import { searchTiles } from '@/utils/Search';
5858
import Defaults, { localStorageKeys } from '@/utils/defaults';
5959
import ConfigLauncher from '@/components/Settings/ConfigLauncher';
6060
@@ -123,7 +123,7 @@ export default {
123123
/* Returns only the tiles that match the users search query */
124124
filterTiles(allTiles) {
125125
if (!allTiles) return [];
126-
return SearchUtil(allTiles, this.searchValue);
126+
return searchTiles(allTiles, this.searchValue);
127127
},
128128
/* Update data when modal is open (so that key bindings can be disabled) */
129129
updateModalVisibility(modalState) {

0 commit comments

Comments
 (0)