🔀 Merge pull request #224 from Lissy93/FEATURE/multi-search

[FEATURE] Multi-Search with Custom Bangs
Fixes #206
This commit is contained in:
Alicia Sykes 2021-09-11 21:24:19 +01:00 committed by GitHub
commit fd3c043d86
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
10 changed files with 129 additions and 40 deletions

View File

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

View File

@ -141,6 +141,7 @@ For more info, see the **[Authentication Docs](/docs/authentication.md)**
**`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`
**`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
**`openingMethod`** | `string` | _Optional_ | Set your preferred opening method for search results: `newtab`, `sametab`, `workspace`. Defaults to `newtab`
**`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=`)
**[⬆️ Back to Top](#configuring)**

View File

@ -50,13 +50,13 @@ In the above example, pressing <kbd>2</kbd> will launch Bookstack. Or hitting <k
## Web Search
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`.
#### 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

View File

@ -1,6 +1,6 @@
{
"name": "Dashy",
"version": "1.7.5",
"version": "1.7.6",
"license": "MIT",
"main": "server",
"scripts": {

View File

@ -9,7 +9,7 @@
:placeholder="$t('search.search-placeholder')"
v-on:input="userIsTypingSomething"
@keydown.esc="clearFilterInput" />
<p v-if="webSearchEnabled && input.length > 0" class="web-search-note">
<p v-if="(!searchPrefs.disableWebSearch) && input.length > 0" class="web-search-note">
{{ $t('search.enter-to-search-web') }}
</p>
</div>
@ -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);
}
},
},

View File

@ -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"
}
}
},

View File

@ -1,6 +1,7 @@
/* Dashy: Licensed under MIT, (C) Alicia Sykes 2021 <https://aliciasykes.com> */
/* 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();
};

View File

@ -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'],

View File

@ -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) {

View File

@ -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) {