updatePlugin(update, plugin.name)}
/>
)
}
}
const categorize = (plugins, callback) => {
const result = []
let remainder = plugins
categories.forEach((category) => {
const [title, filter] = category
const [matched, others] = partition(remainder, filter)
if (matched.length) result.push(title, ...matched)
remainder = others
})
plugins.splice(0, plugins.length)
plugins.push(...result)
callback()
}
const fn = ({
term, display, hide, update
}) => {
const match = term.match(/^plugins?\s*(.+)?$/i)
if (match) {
display({
icon,
id: 'loading',
title: 'Looking for plugins...'
})
loadPlugins().then(flow(
partialRight(search, [match[1], toString]),
tap((plugins) => categorize(plugins, () => hide('loading'))),
map(pluginToResult(update)),
display
))
}
}
const setStatusBar = (text) => {
store.dispatch(statusBar.setValue(text))
}
export default {
icon,
fn,
initializeAsync,
name: 'Manage plugins',
keyword: 'plugins',
onMessage: (type) => {
if (type === 'plugins:start-installation') {
setStatusBar('Installing default plugins...')
}
if (type === 'plugins:finish-installation') {
setTimeout(() => {
setStatusBar(null)
}, 2000)
}
}
}
================================================
FILE: app/plugins/core/plugins/initializeAsync.js
================================================
import { client } from 'lib/plugins'
import config from 'lib/config'
import {
flow, filter, map, property
} from 'lodash/fp'
import loadPlugins from './loadPlugins'
import getInstalledPlugins from './getInstalledPlugins'
const OS_APPS_PLUGIN = {
darwin: '@cerebroapp/cerebro-mac-apps',
DEFAULT: '@cerebroapp/cerebro-basic-apps'
}
const DEFAULT_PLUGINS = [
OS_APPS_PLUGIN[process.platform] || OS_APPS_PLUGIN.DEFAULT,
'@cerebroapp/search',
'cerebro-math',
'cerebro-converter',
'cerebro-open-web',
'cerebro-files-nav'
]
/**
* Check plugins for updates and start plugins autoupdater
*/
async function checkForUpdates() {
console.log('Run plugins autoupdate')
const plugins = await loadPlugins()
const updatePromises = flow(
filter(property('isUpdateAvailable')),
map((plugin) => client.update(plugin.name))
)(plugins)
await Promise.all(updatePromises)
console.log(updatePromises.length > 0
? `${updatePromises.length} plugins are updated`
: 'All plugins are up to date')
// Run autoupdate every 12 hours
setTimeout(checkForUpdates, 12 * 60 * 60 * 1000)
}
/**
* Migrate plugins: default plugins were extracted to separate packages
* so if default plugins are not installed – start installation
*/
async function migratePlugins(sendMessage) {
if (config.get('isMigratedPlugins')) {
// Plugins are already migrated
return
}
console.log('Start installation of default plugins')
const installedPlugins = await getInstalledPlugins()
const promises = flow(
filter((plugin) => !installedPlugins[plugin]),
map((plugin) => client.install(plugin))
)(DEFAULT_PLUGINS)
if (promises.length > 0) {
sendMessage('plugins:start-installation')
}
Promise.all(promises).then(() => {
console.log('All default plugins are installed!')
config.set('isMigratedPlugins', true)
sendMessage('plugins:finish-installation')
})
}
export default (sendMessage) => {
checkForUpdates()
migratePlugins(sendMessage)
}
================================================
FILE: app/plugins/core/plugins/loadPlugins.js
================================================
import { memoize } from 'cerebro-tools'
import validVersion from 'semver/functions/valid'
import compareVersions from 'semver/functions/gt'
import availablePlugins from './getAvailablePlugins'
import getInstalledPlugins from './getInstalledPlugins'
import getDebuggingPlugins from './getDebuggingPlugins'
import blacklist from './blacklist'
const maxAge = 5 * 60 * 1000 // 5 minutes
const getAvailablePlugins = memoize(availablePlugins, { maxAge })
const parseVersion = (version) => (
validVersion((version || '').replace(/^\^/, '')) || '0.0.0'
)
export default async () => {
const [available, installed, debuggingPlugins] = await Promise.all([
getAvailablePlugins(),
getInstalledPlugins(),
getDebuggingPlugins()
])
const listOfInstalledPlugins = Object.entries(installed).map(([name, { version }]) => ({
name,
version,
installedVersion: parseVersion(version),
isInstalled: true,
settings: installed[name].settings,
isUpdateAvailable: false
}))
const listOfAvailablePlugins = available.map((plugin) => {
const installedVersion = installed[plugin.name]?.version
if (!installedVersion) { return plugin }
const isUpdateAvailable = compareVersions(plugin.version, parseVersion(installedVersion))
const installedPluginInfo = listOfInstalledPlugins.find((p) => p.name === plugin.name)
return {
...plugin,
...installedPluginInfo,
installedVersion,
isInstalled: true,
isUpdateAvailable
}
})
console.log('Debugging Plugins: ', debuggingPlugins)
const listOfDebuggingPlugins = debuggingPlugins.map((name) => ({
name,
description: '',
version: 'dev',
isDebugging: true
}))
const plugins = [
...listOfInstalledPlugins,
...listOfAvailablePlugins,
...listOfDebuggingPlugins
].filter((plugin) => !blacklist.includes(plugin.name))
return plugins
}
================================================
FILE: app/plugins/core/quit/index.js
================================================
import { ipcRenderer } from 'electron'
import { search } from 'cerebro-tools'
import icon from '../icon.png'
const KEYWORDS = ['Quit', 'Exit']
const subtitle = 'Quit from Cerebro'
const onSelect = () => ipcRenderer.send('quit')
/**
* Plugin to exit from Cerebro
*
* @param {String} options.term
* @param {Function} options.display
*/
const fn = ({ term, display }) => {
const result = search(KEYWORDS, term).map((title) => ({
icon,
title,
subtitle,
onSelect,
term: title,
}))
display(result)
}
export default { fn }
================================================
FILE: app/plugins/core/reload/index.js
================================================
import { ipcRenderer } from 'electron'
import icon from '../icon.png'
const keyword = 'reload'
const title = 'Reload'
const subtitle = 'Reload Cerebro App'
const onSelect = (event) => {
ipcRenderer.send('reload')
event.preventDefault()
}
/**
* Plugin to reload Cerebro
*
* @param {String} options.term
* @param {Function} options.display
*/
const fn = ({ term, display }) => {
const match = term.match(/^reload\s*/)
if (match) {
display({
icon, title, subtitle, onSelect
})
}
}
export default {
keyword, fn, icon, name: 'Reload'
}
================================================
FILE: app/plugins/core/settings/Settings/Hotkey.js
================================================
import React from 'react'
import PropTypes from 'prop-types'
import styles from './styles.module.css'
const ASCII = {
188: '44',
109: '45',
190: '46',
191: '47',
192: '96',
220: '92',
222: '39',
221: '93',
219: '91',
173: '45',
187: '61',
186: '59',
189: '45'
}
const SHIFT_UPS = {
96: '~',
49: '!',
50: '@',
51: '#',
52: '$',
53: '%',
54: '^',
55: '&',
56: '*',
57: '(',
48: ')',
45: '_',
61: '+',
91: '{',
93: '}',
92: '|',
59: ':',
39: '"',
44: '<',
46: '>',
47: '?'
}
const KEYCODES = {
8: 'Backspace',
9: 'Tab',
13: 'Enter',
27: 'Esc',
32: 'Space',
37: 'Left',
38: 'Up',
39: 'Right',
40: 'Down',
112: 'F1',
113: 'F2',
114: 'F3',
115: 'F4',
116: 'F5',
117: 'F6',
118: 'F7',
119: 'F8',
120: 'F9',
121: 'F10',
122: 'F11',
123: 'F12',
}
const osKeyDelimiter = process.platform === 'darwin' ? '' : '+'
const keyToSign = (key) => {
if (process.platform === 'darwin') {
return key.replace(/control/i, '⌃')
.replace(/alt/i, '⌥')
.replace(/shift/i, '⇧')
.replace(/command/i, '⌘')
.replace(/enter/i, '↩')
.replace(/backspace/i, '⌫')
}
return key
}
const charCodeToSign = ({ keyCode, shiftKey }) => {
if (KEYCODES[keyCode]) {
return KEYCODES[keyCode]
}
const valid = (keyCode > 47 && keyCode < 58) // number keys
|| (keyCode > 64 && keyCode < 91) // letter keys
|| (keyCode > 95 && keyCode < 112) // numpad keys
|| (keyCode > 185 && keyCode < 193) // ;=,-./` (in order)
|| (keyCode > 218 && keyCode < 223) // [\]' (in order)
if (!valid) {
return null
}
const code = ASCII[keyCode] ? ASCII[keyCode] : keyCode
if (!shiftKey && (code >= 65 && code <= 90)) {
return String.fromCharCode(code + 32)
}
if (shiftKey && SHIFT_UPS[code]) {
return SHIFT_UPS[code]
}
return String.fromCharCode(code)
}
function Hotkey({ hotkey, onChange }) {
const onKeyDown = (event) => {
if (!event.ctrlKey && !event.altKey && !event.metaKey) {
// Do not allow to set global shorcut without modifier keys
// At least one of alt, cmd or ctrl is required
return
}
event.preventDefault()
event.stopPropagation()
const key = charCodeToSign(event)
if (!key) return
const keys = []
if (event.ctrlKey) keys.push('Control')
if (event.altKey) keys.push('Alt')
if (event.shiftKey) keys.push('Shift')
if (event.metaKey) keys.push('Command')
keys.push(key)
onChange(keys.join('+'))
}
const keys = hotkey.split('+').map(keyToSign).join(osKeyDelimiter)
return (
)
}
Hotkey.propTypes = {
hotkey: PropTypes.string.isRequired,
onChange: PropTypes.func.isRequired,
}
export default Hotkey
================================================
FILE: app/plugins/core/settings/Settings/countries.js
================================================
export default [
{ value: 'AF', label: 'Afghanistan' },
{ value: 'AX', label: 'Åland Islands' },
{ value: 'AL', label: 'Albania' },
{ value: 'DZ', label: 'Algeria' },
{ value: 'AS', label: 'American Samoa' },
{ value: 'AD', label: 'Andorra' },
{ value: 'AO', label: 'Angola' },
{ value: 'AI', label: 'Anguilla' },
{ value: 'AQ', label: 'Antarctica' },
{ value: 'AG', label: 'Antigua and Barbuda' },
{ value: 'AR', label: 'Argentina' },
{ value: 'AM', label: 'Armenia' },
{ value: 'AW', label: 'Aruba' },
{ value: 'AU', label: 'Australia' },
{ value: 'AT', label: 'Austria' },
{ value: 'AZ', label: 'Azerbaijan' },
{ value: 'BS', label: 'The Bahamas' },
{ value: 'BH', label: 'Bahrain' },
{ value: 'BD', label: 'Bangladesh' },
{ value: 'BB', label: 'Barbados' },
{ value: 'BY', label: 'Belarus' },
{ value: 'BE', label: 'Belgium' },
{ value: 'BZ', label: 'Belize' },
{ value: 'BJ', label: 'Benin' },
{ value: 'BM', label: 'Bermuda' },
{ value: 'BT', label: 'Bhutan' },
{ value: 'BO', label: 'Bolivia' },
{ value: 'BQ', label: 'Bonaire' },
{ value: 'BA', label: 'Bosnia and Herzegovina' },
{ value: 'BW', label: 'Botswana' },
{ value: 'BV', label: 'Bouvet Island' },
{ value: 'BR', label: 'Brazil' },
{ value: 'IO', label: 'British Indian Ocean Territory' },
{ value: 'UM', label: 'United States Minor Outlying Islands' },
{ value: 'VG', label: 'Virgin Islands (British)' },
{ value: 'VI', label: 'Virgin Islands (U.S.)' },
{ value: 'BN', label: 'Brunei' },
{ value: 'BG', label: 'Bulgaria' },
{ value: 'BF', label: 'Burkina Faso' },
{ value: 'BI', label: 'Burundi' },
{ value: 'KH', label: 'Cambodia' },
{ value: 'CM', label: 'Cameroon' },
{ value: 'CA', label: 'Canada' },
{ value: 'CV', label: 'Cape Verde' },
{ value: 'KY', label: 'Cayman Islands' },
{ value: 'CF', label: 'Central African Republic' },
{ value: 'TD', label: 'Chad' },
{ value: 'CL', label: 'Chile' },
{ value: 'CN', label: 'China' },
{ value: 'CX', label: 'Christmas Island' },
{ value: 'CC', label: 'Cocos (Keeling) Islands' },
{ value: 'CO', label: 'Colombia' },
{ value: 'KM', label: 'Comoros' },
{ value: 'CG', label: 'Republic of the Congo' },
{ value: 'CD', label: 'Democratic Republic of the Congo' },
{ value: 'CK', label: 'Cook Islands' },
{ value: 'CR', label: 'Costa Rica' },
{ value: 'HR', label: 'Croatia' },
{ value: 'CU', label: 'Cuba' },
{ value: 'CW', label: 'Curaçao' },
{ value: 'CY', label: 'Cyprus' },
{ value: 'CZ', label: 'Czech Republic' },
{ value: 'DK', label: 'Denmark' },
{ value: 'DJ', label: 'Djibouti' },
{ value: 'DM', label: 'Dominica' },
{ value: 'DO', label: 'Dominican Republic' },
{ value: 'EC', label: 'Ecuador' },
{ value: 'EG', label: 'Egypt' },
{ value: 'SV', label: 'El Salvador' },
{ value: 'GQ', label: 'Equatorial Guinea' },
{ value: 'ER', label: 'Eritrea' },
{ value: 'EE', label: 'Estonia' },
{ value: 'ET', label: 'Ethiopia' },
{ value: 'FK', label: 'Falkland Islands' },
{ value: 'FO', label: 'Faroe Islands' },
{ value: 'FJ', label: 'Fiji' },
{ value: 'FI', label: 'Finland' },
{ value: 'FR', label: 'France' },
{ value: 'GF', label: 'French Guiana' },
{ value: 'PF', label: 'French Polynesia' },
{ value: 'TF', label: 'French Southern and Antarctic Lands' },
{ value: 'GA', label: 'Gabon' },
{ value: 'GM', label: 'The Gambia' },
{ value: 'GE', label: 'Georgia' },
{ value: 'DE', label: 'Germany' },
{ value: 'GH', label: 'Ghana' },
{ value: 'GI', label: 'Gibraltar' },
{ value: 'GR', label: 'Greece' },
{ value: 'GL', label: 'Greenland' },
{ value: 'GD', label: 'Grenada' },
{ value: 'GP', label: 'Guadeloupe' },
{ value: 'GU', label: 'Guam' },
{ value: 'GT', label: 'Guatemala' },
{ value: 'GG', label: 'Guernsey' },
{ value: 'GW', label: 'Guinea-Bissau' },
{ value: 'GY', label: 'Guyana' },
{ value: 'HT', label: 'Haiti' },
{ value: 'HM', label: 'Heard Island and McDonald Islands' },
{ value: 'VA', label: 'Holy See' },
{ value: 'HN', label: 'Honduras' },
{ value: 'HK', label: 'Hong Kong' },
{ value: 'HU', label: 'Hungary' },
{ value: 'IS', label: 'Iceland' },
{ value: 'IN', label: 'India' },
{ value: 'ID', label: 'Indonesia' },
{ value: 'CI', label: 'Ivory Coast' },
{ value: 'IR', label: 'Iran' },
{ value: 'IQ', label: 'Iraq' },
{ value: 'IE', label: 'Republic of Ireland' },
{ value: 'IM', label: 'Isle of Man' },
{ value: 'IL', label: 'Israel' },
{ value: 'IT', label: 'Italy' },
{ value: 'JM', label: 'Jamaica' },
{ value: 'JP', label: 'Japan' },
{ value: 'JE', label: 'Jersey' },
{ value: 'JO', label: 'Jordan' },
{ value: 'KZ', label: 'Kazakhstan' },
{ value: 'KE', label: 'Kenya' },
{ value: 'KI', label: 'Kiribati' },
{ value: 'KW', label: 'Kuwait' },
{ value: 'KG', label: 'Kyrgyzstan' },
{ value: 'LA', label: 'Laos' },
{ value: 'LV', label: 'Latvia' },
{ value: 'LB', label: 'Lebanon' },
{ value: 'LS', label: 'Lesotho' },
{ value: 'LR', label: 'Liberia' },
{ value: 'LY', label: 'Libya' },
{ value: 'LI', label: 'Liechtenstein' },
{ value: 'LT', label: 'Lithuania' },
{ value: 'LU', label: 'Luxembourg' },
{ value: 'MO', label: 'Macau' },
{ value: 'MK', label: 'Republic of Macedonia' },
{ value: 'MG', label: 'Madagascar' },
{ value: 'MW', label: 'Malawi' },
{ value: 'MY', label: 'Malaysia' },
{ value: 'MV', label: 'Maldives' },
{ value: 'ML', label: 'Mali' },
{ value: 'MT', label: 'Malta' },
{ value: 'MH', label: 'Marshall Islands' },
{ value: 'MQ', label: 'Martinique' },
{ value: 'MR', label: 'Mauritania' },
{ value: 'MU', label: 'Mauritius' },
{ value: 'YT', label: 'Mayotte' },
{ value: 'MX', label: 'Mexico' },
{ value: 'FM', label: 'Federated States of Micronesia' },
{ value: 'MD', label: 'Moldova' },
{ value: 'MC', label: 'Monaco' },
{ value: 'MN', label: 'Mongolia' },
{ value: 'ME', label: 'Montenegro' },
{ value: 'MS', label: 'Montserrat' },
{ value: 'MA', label: 'Morocco' },
{ value: 'MZ', label: 'Mozambique' },
{ value: 'MM', label: 'Myanmar' },
{ value: 'NA', label: 'Namibia' },
{ value: 'NR', label: 'Nauru' },
{ value: 'NP', label: 'Nepal' },
{ value: 'NL', label: 'Netherlands' },
{ value: 'NC', label: 'New Caledonia' },
{ value: 'NZ', label: 'New Zealand' },
{ value: 'NI', label: 'Nicaragua' },
{ value: 'NE', label: 'Niger' },
{ value: 'NG', label: 'Nigeria' },
{ value: 'NU', label: 'Niue' },
{ value: 'NF', label: 'Norfolk Island' },
{ value: 'KP', label: 'North Korea' },
{ value: 'MP', label: 'Northern Mariana Islands' },
{ value: 'NO', label: 'Norway' },
{ value: 'OM', label: 'Oman' },
{ value: 'PK', label: 'Pakistan' },
{ value: 'PW', label: 'Palau' },
{ value: 'PS', label: 'Palestine' },
{ value: 'PA', label: 'Panama' },
{ value: 'PG', label: 'Papua New Guinea' },
{ value: 'PY', label: 'Paraguay' },
{ value: 'PE', label: 'Peru' },
{ value: 'PH', label: 'Philippines' },
{ value: 'PN', label: 'Pitcairn Islands' },
{ value: 'PL', label: 'Poland' },
{ value: 'PT', label: 'Portugal' },
{ value: 'PR', label: 'Puerto Rico' },
{ value: 'QA', label: 'Qatar' },
{ value: 'XK', label: 'Republic of Kosovo' },
{ value: 'RE', label: 'Réunion' },
{ value: 'RO', label: 'Romania' },
{ value: 'RU', label: 'Russia' },
{ value: 'RW', label: 'Rwanda' },
{ value: 'BL', label: 'Saint Barthélemy' },
{ value: 'SH', label: 'Saint Helena' },
{ value: 'KN', label: 'Saint Kitts and Nevis' },
{ value: 'LC', label: 'Saint Lucia' },
{ value: 'MF', label: 'Saint Martin' },
{ value: 'PM', label: 'Saint Pierre and Miquelon' },
{ value: 'VC', label: 'Saint Vincent and the Grenadines' },
{ value: 'WS', label: 'Samoa' },
{ value: 'SM', label: 'San Marino' },
{ value: 'ST', label: 'São Tomé and Príncipe' },
{ value: 'SA', label: 'Saudi Arabia' },
{ value: 'SN', label: 'Senegal' },
{ value: 'RS', label: 'Serbia' },
{ value: 'SC', label: 'Seychelles' },
{ value: 'SL', label: 'Sierra Leone' },
{ value: 'SG', label: 'Singapore' },
{ value: 'SX', label: 'Sint Maarten' },
{ value: 'SK', label: 'Slovakia' },
{ value: 'SI', label: 'Slovenia' },
{ value: 'SB', label: 'Solomon Islands' },
{ value: 'SO', label: 'Somalia' },
{ value: 'ZA', label: 'South Africa' },
{ value: 'GS', label: 'South Georgia' },
{ value: 'KR', label: 'South Korea' },
{ value: 'SS', label: 'South Sudan' },
{ value: 'ES', label: 'Spain' },
{ value: 'LK', label: 'Sri Lanka' },
{ value: 'SD', label: 'Sudan' },
{ value: 'SR', label: 'Surinae' },
{ value: 'SJ', label: 'Svalbard and Jan Mayen' },
{ value: 'SZ', label: 'Swaziland' },
{ value: 'SE', label: 'Sweden' },
{ value: 'CH', label: 'Switzerland' },
{ value: 'SY', label: 'Syria' },
{ value: 'TW', label: 'Taiwan' },
{ value: 'TJ', label: 'Tajikistan' },
{ value: 'TZ', label: 'Tanzania' },
{ value: 'TH', label: 'Thailand' },
{ value: 'TL', label: 'East Timor' },
{ value: 'TG', label: 'Togo' },
{ value: 'TK', label: 'Tokelau' },
{ value: 'TO', label: 'Tonga' },
{ value: 'TT', label: 'Trinidad and Tobago' },
{ value: 'TN', label: 'Tunisia' },
{ value: 'TR', label: 'Turkey' },
{ value: 'TM', label: 'Turkmenistan' },
{ value: 'TC', label: 'Turks and Caicos Islands' },
{ value: 'TV', label: 'Tuvalu' },
{ value: 'UG', label: 'Uganda' },
{ value: 'UA', label: 'Ukraine' },
{ value: 'AE', label: 'United Arab Emirates' },
{ value: 'GB', label: 'United Kingdom' },
{ value: 'US', label: 'United States' },
{ value: 'UY', label: 'Uruguay' },
{ value: 'UZ', label: 'Uzbekistan' },
{ value: 'VU', label: 'Vanuatu' },
{ value: 'VE', label: 'Venezuela' },
{ value: 'VN', label: 'Vietnam' },
{ value: 'WF', label: 'Wallis and Futuna' },
{ value: 'EH', label: 'Western Sahara' },
{ value: 'YE', label: 'Yemen' },
{ value: 'ZM', label: 'Zambia' },
{ value: 'ZW', label: 'Zimbabwe' }
]
================================================
FILE: app/plugins/core/settings/Settings/index.js
================================================
import React, { useState } from 'react'
import PropTypes from 'prop-types'
import { FormComponents } from '@cerebroapp/cerebro-ui'
import themes from 'lib/themes'
import Hotkey from './Hotkey'
import countries from './countries'
import styles from './styles.module.css'
const {
Select, Checkbox, Wrapper, Text
} = FormComponents
function Settings({ get, set }) {
const [state, setState] = useState(() => ({
hotkey: get('hotkey'),
showInTray: get('showInTray'),
country: get('country'),
theme: get('theme'),
proxy: get('proxy'),
developerMode: get('developerMode'),
cleanOnHide: get('cleanOnHide'),
selectOnShow: get('selectOnShow'),
pluginsSettings: get('plugins'),
openAtLogin: get('openAtLogin'),
searchBarPlaceholder: get('searchBarPlaceholder')
}))
const changeConfig = (key, value) => {
set(key, value)
setState((prevState) => ({ ...prevState, [key]: value }))
}
return (
changeConfig('hotkey', key)}
/>
)
}
Settings.propTypes = {
get: PropTypes.func.isRequired,
set: PropTypes.func.isRequired
}
export default Settings
================================================
FILE: app/plugins/core/settings/Settings/styles.module.css
================================================
.settings {
display: flex;
align-self: flex-start;
flex-direction: column;
align-items: center;
}
.label {
margin-right: 15px;
margin-top: 8px;
min-width: 60px;
max-width: 60px;
}
.checkbox {
margin-right: 5px;
}
.settingItem {
padding: 20px;
box-sizing: border-box;
width: 100%;
border-color: #d9d9d9 #ccc #b3b3b3;
border-top: 1px solid #ccc;
margin-top: 16px;
}
.header {
font-weight: bold;
}
.input {
font-size: 16px;
line-height: 34px;
padding: 0 10px;
box-sizing: border-box;
width: 100%;
border-color: #d9d9d9 #ccc #b3b3b3;
border-radius: 4px;
border: 1px solid #ccc;
}
================================================
FILE: app/plugins/core/settings/index.js
================================================
import React from 'react'
import { search } from 'cerebro-tools'
import Settings from './Settings'
import icon from '../icon.png'
// Settings plugin name
const NAME = 'Cerebro Settings'
// Settings plugins in the end of list
const order = 9
// Phrases that used to find settings plugins
const KEYWORDS = [
NAME,
'Cerebro Preferences',
'cfg',
'config',
'params'
]
/**
* Plugin to show app settings in results list
*
* @param {String} options.term
* @param {Function} options.display
*/
const settingsPlugin = ({
term, display, config, actions
}) => {
const found = search(KEYWORDS, term).length > 0
if (found) {
const results = [{
order,
icon,
title: NAME,
term: NAME,
getPreview: () => (
config.set(key, value)}
get={(key) => config.get(key)}
/>
),
onSelect: (event) => {
event.preventDefault()
actions.replaceTerm(NAME)
}
}]
display(results)
}
}
export default {
name: NAME,
fn: settingsPlugin
}
================================================
FILE: app/plugins/core/version/index.js
================================================
import React from 'react'
import { search } from 'cerebro-tools'
import icon from '../icon.png'
// Settings plugin name
const NAME = 'Cerebro Version'
// Settings plugins in the end of list
const order = 9
// Phrases that used to find settings plugins
const KEYWORDS = [
NAME,
'ver',
'version'
]
const { CEREBRO_VERSION } = process.env
/**
* Plugin to show app settings in results list
*
* @param {String} options.term
* @param {Function} options.display
*/
const versionPlugin = ({ term, display, actions }) => {
const found = search(KEYWORDS, term).length > 0
if (found) {
const results = [{
order,
icon,
title: NAME,
term: NAME,
getPreview: () => ({CEREBRO_VERSION}
),
onSelect: (event) => {
event.preventDefault()
actions.replaceTerm(NAME)
}
}]
display(results)
}
}
export default { name: NAME, fn: versionPlugin }
================================================
FILE: app/plugins/externalPlugins.js
================================================
import debounce from 'lodash/debounce'
import chokidar from 'chokidar'
import path from 'path'
import initPlugin from 'lib/initPlugin'
import { modulesDirectory, ensureFiles, settings } from 'lib/plugins'
const plugins = {}
const requirePlugin = (pluginPath) => {
try {
let plugin = window.require(pluginPath)
// Fallback for plugins with structure like `{default: {fn: ...}}`
const keys = Object.keys(plugin)
if (keys.length === 1 && keys[0] === 'default') {
plugin = plugin.default
}
return plugin
} catch (error) {
// catch all errors from plugin loading
console.log('Error requiring', pluginPath)
console.log(error)
}
}
/**
* Validate plugin module signature
*
* @param {Object} plugin
* @return {Boolean}
*/
const isPluginValid = (plugin) => (
plugin
// Check existing of main plugin function
&& typeof plugin.fn === 'function'
// Check that plugin function accepts 0 or 1 argument
&& plugin.fn.length <= 1
)
ensureFiles()
/* As we support scoped plugins, using 'base' as plugin name is no longer valid
because it is not unique. '@example/plugin' and '@test/plugin' would both be treated as 'plugin'
So now we must introduce the scope to the plugin name
This function returns the name with the scope if it is present in the path
*/
const getPluginName = (pluginPath) => {
const { base, dir } = path.parse(pluginPath)
const scope = dir.match(/@.+$/)
if (!scope) return base
return `${scope[0]}/${base}`
}
const setupPluginsWatcher = () => {
if (global.isBackground) return
const pluginsWatcher = chokidar.watch(modulesDirectory, { depth: 1 })
pluginsWatcher.on('unlinkDir', (pluginPath) => {
const { base, dir } = path.parse(pluginPath)
if (base.match(/node_modules/) || base.match(/^@/)) return
if (!dir.match(/node_modules$/) && !dir.match(/@.+$/)) return
const pluginName = getPluginName(pluginPath)
const requirePath = window.require.resolve(pluginPath)
delete plugins[pluginName]
delete window.require.cache[requirePath]
console.log(`[${pluginName}] Plugin removed`)
})
pluginsWatcher.on('addDir', (pluginPath) => {
const { base, dir } = path.parse(pluginPath)
if (base.match(/node_modules/) || base.match(/^@/)) return
if (!dir.match(/node_modules$/) && !dir.match(/@.+$/)) return
const pluginName = getPluginName(pluginPath)
setTimeout(() => {
console.group(`Load plugin: ${pluginName}`)
console.log(`Path: ${pluginPath}...`)
const plugin = requirePlugin(pluginPath)
if (!isPluginValid(plugin)) {
console.log('Plugin is not valid, skipped')
console.groupEnd()
return
}
if (!settings.validate(plugin)) {
console.log('Invalid plugins settings')
console.groupEnd()
return
}
console.log('Loaded.')
const requirePath = window.require.resolve(pluginPath)
const watcher = chokidar.watch(pluginPath, { depth: 0 })
watcher.on('change', debounce(() => {
console.log(`[${pluginName}] Update plugin`)
delete window.require.cache[requirePath]
plugins[pluginName] = window.require(pluginPath)
console.log(`[${pluginName}] Plugin updated`)
}, 1000))
plugins[pluginName] = plugin
initPlugin(plugin, pluginName)
console.groupEnd()
}, 1000)
})
}
setupPluginsWatcher()
export default plugins
================================================
FILE: app/plugins/index.ts
================================================
import core from './core'
import externalPlugins from './externalPlugins'
const pluginsService = {
corePlugins: core,
allPlugins: Object.assign(externalPlugins, core),
externalPlugins,
}
export default pluginsService
================================================
FILE: babel.config.js
================================================
module.exports = {
presets: [
'@babel/preset-typescript',
[
'@babel/preset-env', {
/** Targets must match the versions supported by electron.
* See https://www.electronjs.org/
*/
targets: {
node: '16',
chrome: '102'
}
}
],
'@babel/preset-react'
]
}
================================================
FILE: build/installer.nsh
================================================
!macro customInstall
DetailPrint "Register cerebro URI Handler"
DeleteRegKey HKCR "cerebro"
WriteRegStr HKCR "cerebro" "" "URL:cerebro"
WriteRegStr HKCR "cerebro" "URL Protocol" ""
WriteRegStr HKCR "cerebro\DefaultIcon" "" "$INSTDIR\${APP_EXECUTABLE_FILENAME}"
WriteRegStr HKCR "cerebro\shell" "" ""
WriteRegStr HKCR "cerebro\shell\Open" "" ""
WriteRegStr HKCR "cerebro\shell\Open\command" "" "$INSTDIR\${APP_EXECUTABLE_FILENAME} %1"
!macroend
================================================
FILE: electron-builder.json
================================================
{
"productName": "Cerebro",
"appId": "com.cerebroapp.Cerebro",
"protocols": {
"name": "Cerebro URLs",
"role": "Viewer",
"schemes": [
"cerebro"
]
},
"directories": {
"app": "./app",
"output": "release"
},
"linux": {
"target": [
{
"target": "deb",
"arch": [
"x64"
]
},
{
"target": "AppImage",
"arch": [
"x64"
]
}
],
"category": "Utility"
},
"mac": {
"category": "public.app-category.productivity"
},
"dmg": {
"contents": [
{
"x": 410,
"y": 150,
"type": "link",
"path": "/Applications"
},
{
"x": 130,
"y": 150,
"type": "file"
}
]
},
"win": {
"target": [
{
"target": "nsis",
"arch": [
"x64",
"ia32"
]
},
{
"target": "portable",
"arch": [
"x64",
"ia32"
]
}
]
},
"nsis": {
"include": "build/installer.nsh",
"perMachine": true
},
"files": [
"dist/",
"main/index.html",
"main/css,",
"background/index.html",
"tray_icon.png",
"tray_icon.ico",
"tray_iconTemplate@2x.png",
"node_modules/",
"app/node_modules/",
"main.js",
"main.js.map",
"package.json",
"!**/node_modules/*/{CHANGELOG.md,README.md,README,readme.md,readme,test,__tests__,tests,powered-test,example,examples,*.d.ts}",
"!**/node_modules/.bin",
"!**/*.{o,hprof,orig,pyc,pyo,rbc}",
"!**/{.DS_Store,.git,.hg,.svn,CVS,RCS,SCCS,__pycache__,thumbs.db,.gitignore,.gitattributes,.editorconfig,.flowconfig,.yarn-metadata.json,.idea,appveyor.yml,.travis.yml,circle.yml,npm-debug.log,.nyc_output,yarn.lock,.yarn-integrity}"
],
"squirrelWindows": {
"iconUrl": "https://raw.githubusercontent.com/cerebroapp/cerebro/master/build/icon.ico"
},
"publish": {
"provider": "github",
"vPrefixedTagName": true,
"releaseType": "release"
}
}
================================================
FILE: jest.config.js
================================================
module.exports = {
collectCoverage: true,
moduleDirectories: ['node_modules', 'app'],
moduleNameMapper: {
'\\.(jpg|ico|jpeg|png|gif|eot|otf|webp|svg|ttf|woff|woff2|mp4|webm|wav|mp3|m4a|aac|oga)$': '/__mocks__/fileMock.js',
'\\.(css|less)$': '/__mocks__/fileMock.js'
}
}
================================================
FILE: package.json
================================================
{
"name": "cerebro",
"productName": "cerebro",
"version": "0.11.0",
"description": "Cerebro is an open-source launcher to improve your productivity and efficiency",
"main": "./app/main.js",
"scripts": {
"test": "cross-env NODE_ENV=test CEREBRO_DATA_PATH=userdata jest",
"test-watch": "jest -- --watch",
"lint": "eslint app/background app/lib app/main app/plugins *.js",
"hot-server": "node -r @babel/register server.js",
"build-main": "webpack --mode production --config webpack.config.electron.js",
"build-main-dev": "webpack --mode development --config webpack.config.electron.js",
"build-renderer": "webpack --config webpack.config.production.js",
"bundle-analyze": "cross-env ANALYZE=true node ./node_modules/webpack/bin/webpack --config webpack.config.production.js && open ./app/dist/stats.html",
"build": "run-p build-main build-renderer",
"start": "cross-env NODE_ENV=production electron ./app",
"start-hot": "yarn build-main-dev && cross-env NODE_ENV=development electron ./app",
"release": "build -mwl --draft",
"dev": "run-p hot-server start-hot",
"postinstall": "electron-builder install-app-deps",
"package": "yarn build && npx electron-builder",
"prepare": "husky install",
"commit": "cz"
},
"repository": {
"type": "git",
"url": "git+https://github.com/cerebroapp/cerebro.git"
},
"author": {
"name": "CerebroApp Organization",
"email": "kelionweb@gmail.com",
"url": "https://github.com/cerebroapp"
},
"contributors": [
"Alexandr Subbotin (https://github.com/KELiON)",
"Gustavo Pereira =16.x"
},
"config": {
"commitizen": {
"path": "./node_modules/cz-conventional-changelog"
}
}
}
================================================
FILE: postcss.config.js
================================================
module.exports = {
plugins: {
'postcss-nested': {},
autoprefixer: {}
}
}
================================================
FILE: server.js
================================================
const express = require('express')
const webpack = require('webpack')
const webpackDevMiddleware = require('webpack-dev-middleware')
const webpackHotMiddleware = require('webpack-hot-middleware')
const config = require('./webpack.config.development')
const app = express()
const compiler = webpack(config)
const PORT = 3000
const wdm = webpackDevMiddleware(compiler)
app.use(wdm)
app.use(webpackHotMiddleware(compiler))
const server = app.listen(PORT, 'localhost', (err) => {
if (err) {
console.error(err)
return
}
console.log(`Listening at http://localhost:${PORT}`)
})
process.on('SIGTERM', () => {
console.log('Stopping dev server')
wdm.close()
server.close(() => {
process.exit(0)
})
})
================================================
FILE: tsconfig.json
================================================
{
"compilerOptions": {
"baseUrl": "app",
"jsx": "react",
"allowJs": true,
"noImplicitAny": true,
"sourceMap": true,
"esModuleInterop": true,
},
"include": ["./app"]
}
================================================
FILE: webpack.config.base.js
================================================
const path = require('path')
const CopyWebpackPlugin = require('copy-webpack-plugin')
const LodashModuleReplacementPlugin = require('lodash-webpack-plugin')
module.exports = {
module: {
rules: [{
test: /\.(js|ts)x?$/,
use: 'babel-loader',
exclude: /node_modules/
}, {
test: /\.jpe?g$|\.gif$|\.png$|\.svg$|\.woff$|\.ttf$|\.wav$|\.mp3$/,
type: 'asset/inline'
}]
},
output: {
path: path.join(__dirname, 'app'),
filename: '[name].bundle.js',
libraryTarget: 'commonjs2'
},
resolve: {
modules: [
path.join(__dirname, 'app'),
'node_modules'
],
extensions: ['.ts', '.js', '.tsx', '.jsx'],
},
plugins: [
new LodashModuleReplacementPlugin(),
new CopyWebpackPlugin({
patterns: [{
from: 'app/main/css/themes/*',
to: './main/css/themes/[name][ext]'
}]
})
]
}
================================================
FILE: webpack.config.development.js
================================================
const webpack = require('webpack')
const baseConfig = require('./webpack.config.base')
const config = {
...baseConfig,
mode: 'development',
devtool: 'inline-source-map',
entry: {
background: [
'webpack-hot-middleware/client?path=http://localhost:3000/__webpack_hmr',
'./app/background/background',
],
main: [
'webpack-hot-middleware/client?path=http://localhost:3000/__webpack_hmr',
'./app/main/main',
]
},
output: {
...baseConfig.output,
publicPath: 'http://localhost:3000/dist/'
},
module: {
...baseConfig.module,
rules: [
...baseConfig.module.rules,
{
test: /\.css$/,
use: [
'style-loader',
{
loader: 'css-loader',
options: {
modules: true,
sourceMap: true,
importLoaders: 1,
},
},
'postcss-loader',
],
include: /\.module\.s?(c|a)ss$/,
},
{
test: /\.css$/,
use: ['style-loader', 'css-loader', 'postcss-loader'],
exclude: /\.module\.css$/,
},
]
},
plugins: [
...baseConfig.plugins,
new webpack.LoaderOptionsPlugin({
debug: true
}),
new webpack.HotModuleReplacementPlugin(),
],
stats: {
colors: true,
},
target: 'electron-renderer'
}
module.exports = config
================================================
FILE: webpack.config.electron.js
================================================
const baseConfig = require('./webpack.config.base')
module.exports = {
...baseConfig,
module: {
rules: [{
test: /\.(js|ts)x?$/,
exclude: /node_modules/,
use: ['babel-loader']
}]
},
devtool: 'source-map',
entry: './app/main.development',
output: {
...baseConfig.output,
filename: './main.js'
},
target: 'electron-main'
}
================================================
FILE: webpack.config.production.js
================================================
const path = require('path')
const MiniCssExtractPlugin = require('mini-css-extract-plugin')
const Visualizer = require('webpack-visualizer-plugin')
const baseConfig = require('./webpack.config.base')
const config = {
...baseConfig,
mode: 'production',
devtool: 'source-map',
entry: {
main: './app/main/main',
background: './app/background/background'
},
output: {
...baseConfig.output,
path: path.join(__dirname, 'app', 'dist'),
publicPath: '../dist/'
},
module: {
...baseConfig.module,
rules: [
...baseConfig.module.rules,
{
test: /\.css$/,
use: [
{ loader: MiniCssExtractPlugin.loader },
{
loader: 'css-loader',
options: {
modules: true,
sourceMap: true,
importLoaders: 1,
},
},
'postcss-loader',
],
include: /\.module\.css$/,
},
{
test: /\.css$/,
use: [MiniCssExtractPlugin.loader, 'css-loader', 'postcss-loader'],
exclude: /\.module\.css$/,
},
]
},
plugins: [
...baseConfig.plugins,
new MiniCssExtractPlugin()
],
target: 'electron-renderer'
}
if (process.env.ANALYZE) {
config.plugins.push(new Visualizer())
}
module.exports = config